feat: text, numbers and pixelate in the editor export
This commit is contained in:
1 parent
07a4858175
commit
04f51402b3
4 files changed
+369
-14
No files matched your search
@@ -80,6 +80,7 @@ mod tests {
|
|||||||
width: 2.0,
|
width: 2.0,
|
||||||
},
|
},
|
||||||
points: vec![(0.0, 0.0), (x, 0.0)],
|
points: vec![(0.0, 0.0), (x, 0.0)],
|
||||||
|
text: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+271
-7
@@ -6,17 +6,21 @@ use std::path::{Path, PathBuf};
|
|||||||
use image::RgbaImage;
|
use image::RgbaImage;
|
||||||
use tiny_skia::{FillRule, LineCap, LineJoin, Paint, PathBuilder, Pixmap, Stroke, Transform};
|
use tiny_skia::{FillRule, LineCap, LineJoin, Paint, PathBuilder, Pixmap, Stroke, Transform};
|
||||||
|
|
||||||
use crate::editor::shape::{Prim, Shape, geometry};
|
use crate::editor::shape::{
|
||||||
|
P, Prim, Shape, Tool, block_size, geometry, number_diameter, text_size,
|
||||||
|
};
|
||||||
|
use crate::editor::text;
|
||||||
|
use crate::frame::PixelRect;
|
||||||
|
|
||||||
pub fn render(base: &RgbaImage, shapes: &[Shape]) -> RgbaImage {
|
pub fn render<'a>(base: &RgbaImage, shapes: impl IntoIterator<Item = &'a Shape>) -> RgbaImage {
|
||||||
let (w, h) = base.dimensions();
|
let (w, h) = base.dimensions();
|
||||||
let mut pixmap = Pixmap::new(w, h).expect("the image has a size");
|
let mut pixmap = Pixmap::new(w, h).expect("the image has a size");
|
||||||
for (dst, src) in pixmap.pixels_mut().iter_mut().zip(base.pixels()) {
|
for (dst, src) in pixmap.pixels_mut().iter_mut().zip(base.pixels()) {
|
||||||
let [r, g, b, a] = src.0;
|
let [r, g, b, a] = src.0;
|
||||||
*dst = tiny_skia::ColorU8::from_rgba(r, g, b, a).premultiply();
|
*dst = tiny_skia::ColorU8::from_rgba(r, g, b, a).premultiply();
|
||||||
}
|
}
|
||||||
for prim in shapes.iter().flat_map(geometry) {
|
for shape in shapes {
|
||||||
draw(&mut pixmap, &prim);
|
draw_shape(&mut pixmap, shape);
|
||||||
}
|
}
|
||||||
let mut out = RgbaImage::new(w, h);
|
let mut out = RgbaImage::new(w, h);
|
||||||
for (dst, src) in out.pixels_mut().zip(pixmap.pixels()) {
|
for (dst, src) in out.pixels_mut().zip(pixmap.pixels()) {
|
||||||
@@ -26,6 +30,141 @@ pub fn render(base: &RgbaImage, shapes: &[Shape]) -> RgbaImage {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The part of `image` inside the last crop, or all of it.
|
||||||
|
// The editor calls this from Task 3 of the 5b plan on; that task removes this.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn crop(image: RgbaImage, rect: Option<PixelRect>) -> RgbaImage {
|
||||||
|
match rect {
|
||||||
|
Some(r) => image::imageops::crop_imm(&image, r.x, r.y, r.width, r.height).to_image(),
|
||||||
|
None => image,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_shape(pixmap: &mut Pixmap, shape: &Shape) {
|
||||||
|
let [r, g, b] = shape.style.color;
|
||||||
|
match shape.tool {
|
||||||
|
Tool::Text => draw_text(
|
||||||
|
pixmap,
|
||||||
|
&shape.text,
|
||||||
|
text_size(shape.style.width),
|
||||||
|
shape.points[0],
|
||||||
|
[r, g, b, 255],
|
||||||
|
),
|
||||||
|
Tool::Number => draw_number(pixmap, shape, [r, g, b, 255]),
|
||||||
|
Tool::Pixelate => pixelate(
|
||||||
|
pixmap,
|
||||||
|
shape.points[0],
|
||||||
|
shape.points[1],
|
||||||
|
block_size(shape.style.width),
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
for prim in geometry(shape) {
|
||||||
|
draw(pixmap, &prim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(c: [u8; 4]) -> Paint<'static> {
|
||||||
|
let mut paint = Paint::default();
|
||||||
|
paint.set_color_rgba8(c[0], c[1], c[2], c[3]);
|
||||||
|
paint.anti_alias = true;
|
||||||
|
paint
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Near black at 80 %: keeps text readable on any background.
|
||||||
|
const OUTLINE: [u8; 4] = [0x1c, 0x1c, 0x1e, 204];
|
||||||
|
|
||||||
|
/// `text` with its line box at `origin`, over a dark outline.
|
||||||
|
fn draw_text(pixmap: &mut Pixmap, text: &str, size: f32, origin: P, color: [u8; 4]) {
|
||||||
|
let Some(path) = text::path(text, size, origin) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let stroke = Stroke {
|
||||||
|
width: size / 8.0,
|
||||||
|
line_join: LineJoin::Round,
|
||||||
|
..Stroke::default()
|
||||||
|
};
|
||||||
|
pixmap.stroke_path(&path, &paint(OUTLINE), &stroke, Transform::identity(), None);
|
||||||
|
pixmap.fill_path(
|
||||||
|
&path,
|
||||||
|
&paint(color),
|
||||||
|
FillRule::Winding,
|
||||||
|
Transform::identity(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A filled circle with the number in white, centred on its ink.
|
||||||
|
fn draw_number(pixmap: &mut Pixmap, shape: &Shape, color: [u8; 4]) {
|
||||||
|
let d = number_diameter(shape.style.width);
|
||||||
|
let (cx, cy) = shape.points[0];
|
||||||
|
if let Some(circle) = PathBuilder::from_circle(cx, cy, d / 2.0) {
|
||||||
|
pixmap.fill_path(
|
||||||
|
&circle,
|
||||||
|
&paint(color),
|
||||||
|
FillRule::Winding,
|
||||||
|
Transform::identity(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let Some(glyphs) = text::path(&shape.text, d * 0.55, (0.0, 0.0)) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let b = glyphs.bounds();
|
||||||
|
let centre = Transform::from_translate(
|
||||||
|
cx - (b.left() + b.right()) / 2.0,
|
||||||
|
cy - (b.top() + b.bottom()) / 2.0,
|
||||||
|
);
|
||||||
|
if let Some(glyphs) = glyphs.transform(centre) {
|
||||||
|
pixmap.fill_path(
|
||||||
|
&glyphs,
|
||||||
|
&paint([255, 255, 255, 255]),
|
||||||
|
FillRule::Winding,
|
||||||
|
Transform::identity(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces each `block`-sized square of the rectangle from `a` to `b`
|
||||||
|
/// (aligned to its top-left, clipped to the image) with its average.
|
||||||
|
fn pixelate(pixmap: &mut Pixmap, a: P, b: P, block: u32) {
|
||||||
|
let (w, h) = (pixmap.width(), pixmap.height());
|
||||||
|
let clamp = |v: f32, max: u32| v.round().clamp(0.0, max as f32) as u32;
|
||||||
|
let (x0, x1) = (clamp(a.0.min(b.0), w), clamp(a.0.max(b.0), w));
|
||||||
|
let (y0, y1) = (clamp(a.1.min(b.1), h), clamp(a.1.max(b.1), h));
|
||||||
|
let pixels = pixmap.pixels_mut();
|
||||||
|
for by in (y0..y1).step_by(block as usize) {
|
||||||
|
for bx in (x0..x1).step_by(block as usize) {
|
||||||
|
let (ex, ey) = ((bx + block).min(x1), (by + block).min(y1));
|
||||||
|
let mut sum = [0u32; 4];
|
||||||
|
for y in by..ey {
|
||||||
|
for x in bx..ex {
|
||||||
|
let p = pixels[(y * w + x) as usize];
|
||||||
|
for (s, v) in sum
|
||||||
|
.iter_mut()
|
||||||
|
.zip([p.red(), p.green(), p.blue(), p.alpha()])
|
||||||
|
{
|
||||||
|
*s += u32::from(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let n = (ex - bx) * (ey - by);
|
||||||
|
let [r, g, bl, al] = sum.map(|s| ((s + n / 2) / n) as u8);
|
||||||
|
// Averages of premultiplied channels stay at or below alpha.
|
||||||
|
let avg =
|
||||||
|
tiny_skia::PremultipliedColorU8::from_rgba(r.min(al), g.min(al), bl.min(al), al)
|
||||||
|
.expect("channels are clamped to alpha");
|
||||||
|
for y in by..ey {
|
||||||
|
for x in bx..ex {
|
||||||
|
pixels[(y * w + x) as usize] = avg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn draw(pixmap: &mut Pixmap, prim: &Prim) {
|
fn draw(pixmap: &mut Pixmap, prim: &Prim) {
|
||||||
let (points, color) = match prim {
|
let (points, color) = match prim {
|
||||||
Prim::Stroke { points, color, .. } | Prim::Fill { points, color } => (points, color),
|
Prim::Stroke { points, color, .. } | Prim::Fill { points, color } => (points, color),
|
||||||
@@ -40,9 +179,7 @@ fn draw(pixmap: &mut Pixmap, prim: &Prim) {
|
|||||||
pb.close();
|
pb.close();
|
||||||
}
|
}
|
||||||
let Some(path) = pb.finish() else { return };
|
let Some(path) = pb.finish() else { return };
|
||||||
let mut paint = Paint::default();
|
let paint = paint(*color);
|
||||||
paint.set_color_rgba8(color[0], color[1], color[2], color[3]);
|
|
||||||
paint.anti_alias = true;
|
|
||||||
match prim {
|
match prim {
|
||||||
Prim::Fill { .. } => pixmap.fill_path(
|
Prim::Fill { .. } => pixmap.fill_path(
|
||||||
&path,
|
&path,
|
||||||
@@ -85,6 +222,7 @@ pub fn edited_path(path: &Path) -> PathBuf {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::editor::shape::{Style, Tool};
|
use crate::editor::shape::{Style, Tool};
|
||||||
|
use crate::frame::PixelRect;
|
||||||
use image::Rgba;
|
use image::Rgba;
|
||||||
|
|
||||||
fn line(tool: Tool, color: [u8; 3]) -> Shape {
|
fn line(tool: Tool, color: [u8; 3]) -> Shape {
|
||||||
@@ -92,6 +230,7 @@ mod tests {
|
|||||||
tool,
|
tool,
|
||||||
style: Style { color, width: 4.0 },
|
style: Style { color, width: 4.0 },
|
||||||
points: vec![(10.0, 20.0), (90.0, 20.0)],
|
points: vec![(10.0, 20.0), (90.0, 20.0)],
|
||||||
|
text: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +258,7 @@ mod tests {
|
|||||||
width: 8.0,
|
width: 8.0,
|
||||||
},
|
},
|
||||||
points: vec![(10.0, 10.0), (50.0, 30.0)],
|
points: vec![(10.0, 10.0), (50.0, 30.0)],
|
||||||
|
text: String::new(),
|
||||||
};
|
};
|
||||||
let out = render(&base, &[rect]);
|
let out = render(&base, &[rect]);
|
||||||
// A round join would leave the outer corner (6..7, 6..7) white.
|
// A round join would leave the outer corner (6..7, 6..7) white.
|
||||||
@@ -171,4 +311,128 @@ mod tests {
|
|||||||
dir.path().join("Screenshot at 10.00 edited.png")
|
dir.path().join("Screenshot at 10.00 edited.png")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn white(w: u32, h: u32) -> RgbaImage {
|
||||||
|
RgbaImage::from_pixel(w, h, Rgba([255, 255, 255, 255]))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_is_filled_with_the_colour_and_outlined() {
|
||||||
|
let t = Shape {
|
||||||
|
tool: Tool::Text,
|
||||||
|
style: Style {
|
||||||
|
color: [0, 122, 255],
|
||||||
|
width: 8.0,
|
||||||
|
},
|
||||||
|
points: vec![(10.0, 10.0)],
|
||||||
|
text: "I".into(),
|
||||||
|
};
|
||||||
|
let out = render(&white(80, 60), &[t]);
|
||||||
|
// The middle of the I's stem: x ≈ 10 + stem centre, y in the cap height.
|
||||||
|
let blue = out.pixels().filter(|p| p.0 == [0, 122, 255, 255]).count();
|
||||||
|
assert!(blue > 40, "{blue} fully blue pixels");
|
||||||
|
let dark = out.pixels().filter(|p| p.0[0] < 80 && p.0[2] < 80).count();
|
||||||
|
assert!(dark > 10, "the outline: {dark}");
|
||||||
|
assert_eq!(out.get_pixel(70, 55).0, [255, 255, 255, 255]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_number_is_a_filled_circle_with_a_white_digit() {
|
||||||
|
let n = Shape {
|
||||||
|
tool: Tool::Number,
|
||||||
|
style: Style {
|
||||||
|
color: [255, 0, 0],
|
||||||
|
width: 8.0,
|
||||||
|
},
|
||||||
|
points: vec![(30.0, 30.0)],
|
||||||
|
text: "1".into(),
|
||||||
|
};
|
||||||
|
let out = render(&white(60, 60), &[n]);
|
||||||
|
assert_eq!(
|
||||||
|
out.get_pixel(30, 10).0,
|
||||||
|
[255, 0, 0, 255],
|
||||||
|
"inside the 44 px circle, above the digit"
|
||||||
|
);
|
||||||
|
assert_eq!(out.get_pixel(3, 3).0, [255, 255, 255, 255], "outside it");
|
||||||
|
let whites_inside = (20..40)
|
||||||
|
.flat_map(|x| (20..40).map(move |y| (x, y)))
|
||||||
|
.filter(|&(x, y)| out.get_pixel(x, y).0 == [255, 255, 255, 255])
|
||||||
|
.count();
|
||||||
|
assert!(whites_inside > 10, "the digit: {whites_inside}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pixelate_averages_blocks_including_earlier_shapes() {
|
||||||
|
let base = RgbaImage::from_fn(24, 12, |x, _| {
|
||||||
|
if x % 2 == 0 {
|
||||||
|
Rgba([0, 0, 0, 255])
|
||||||
|
} else {
|
||||||
|
Rgba([200, 100, 50, 255])
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let px = Shape {
|
||||||
|
tool: Tool::Pixelate,
|
||||||
|
style: Style {
|
||||||
|
color: [0, 0, 0],
|
||||||
|
width: 4.0,
|
||||||
|
},
|
||||||
|
points: vec![(0.0, 0.0), (24.0, 12.0)],
|
||||||
|
text: String::new(),
|
||||||
|
};
|
||||||
|
let out = render(&base, std::slice::from_ref(&px));
|
||||||
|
let first = out.get_pixel(0, 0).0;
|
||||||
|
for x in 0..12 {
|
||||||
|
for y in 0..12 {
|
||||||
|
assert_eq!(out.get_pixel(x, y).0, first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (got, want) in first.iter().zip([100, 50, 25, 255]) {
|
||||||
|
assert!((*got as i32 - want).abs() <= 1, "{first:?}");
|
||||||
|
}
|
||||||
|
let red = Shape {
|
||||||
|
tool: Tool::Line,
|
||||||
|
style: Style {
|
||||||
|
color: [255, 0, 0],
|
||||||
|
width: 8.0,
|
||||||
|
},
|
||||||
|
points: vec![(0.0, 6.0), (24.0, 6.0)],
|
||||||
|
text: String::new(),
|
||||||
|
};
|
||||||
|
let both = render(&base, &[red, px]);
|
||||||
|
assert!(
|
||||||
|
both.get_pixel(0, 0).0[0] > first[0],
|
||||||
|
"the line is pixelated in"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pixelate_stays_inside_the_image() {
|
||||||
|
let px = Shape {
|
||||||
|
tool: Tool::Pixelate,
|
||||||
|
style: Style {
|
||||||
|
color: [0, 0, 0],
|
||||||
|
width: 4.0,
|
||||||
|
},
|
||||||
|
points: vec![(-10.0, -10.0), (500.0, 500.0)],
|
||||||
|
text: String::new(),
|
||||||
|
};
|
||||||
|
let out = render(&white(30, 20), &[px]);
|
||||||
|
assert_eq!(out.dimensions(), (30, 20));
|
||||||
|
assert_eq!(out.get_pixel(29, 19).0, [255, 255, 255, 255]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crop_cuts_out_the_rectangle() {
|
||||||
|
let img = RgbaImage::from_fn(10, 8, |x, y| Rgba([x as u8, y as u8, 0, 255]));
|
||||||
|
let rect = PixelRect {
|
||||||
|
x: 2,
|
||||||
|
y: 3,
|
||||||
|
width: 4,
|
||||||
|
height: 2,
|
||||||
|
};
|
||||||
|
let out = crop(img.clone(), Some(rect));
|
||||||
|
assert_eq!(out.dimensions(), (4, 2));
|
||||||
|
assert_eq!(out.get_pixel(0, 0).0, [2, 3, 0, 255]);
|
||||||
|
assert_eq!(crop(img.clone(), None), img);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
pub mod doc;
|
pub mod doc;
|
||||||
pub mod export;
|
pub mod export;
|
||||||
pub mod shape;
|
pub mod shape;
|
||||||
// Used from Task 2 of the 5b plan on; that task removes this.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub mod text;
|
pub mod text;
|
||||||
pub mod view;
|
pub mod view;
|
||||||
|
|
||||||
|
|||||||
+97
-5
@@ -11,16 +11,24 @@ pub enum Tool {
|
|||||||
Line,
|
Line,
|
||||||
Pen,
|
Pen,
|
||||||
Highlighter,
|
Highlighter,
|
||||||
|
Text,
|
||||||
|
Pixelate,
|
||||||
|
Number,
|
||||||
|
Crop,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tool {
|
impl Tool {
|
||||||
pub const ALL: [Tool; 6] = [
|
pub const ALL: [Tool; 10] = [
|
||||||
Tool::Arrow,
|
Tool::Arrow,
|
||||||
Tool::Rectangle,
|
Tool::Rectangle,
|
||||||
Tool::Ellipse,
|
Tool::Ellipse,
|
||||||
Tool::Line,
|
Tool::Line,
|
||||||
Tool::Pen,
|
Tool::Pen,
|
||||||
Tool::Highlighter,
|
Tool::Highlighter,
|
||||||
|
Tool::Text,
|
||||||
|
Tool::Pixelate,
|
||||||
|
Tool::Number,
|
||||||
|
Tool::Crop,
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn label(self) -> &'static str {
|
pub fn label(self) -> &'static str {
|
||||||
@@ -31,6 +39,10 @@ impl Tool {
|
|||||||
Tool::Line => "Line",
|
Tool::Line => "Line",
|
||||||
Tool::Pen => "Pen",
|
Tool::Pen => "Pen",
|
||||||
Tool::Highlighter => "Highlighter",
|
Tool::Highlighter => "Highlighter",
|
||||||
|
Tool::Text => "Text",
|
||||||
|
Tool::Pixelate => "Pixelate",
|
||||||
|
Tool::Number => "Number",
|
||||||
|
Tool::Crop => "Crop",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +65,23 @@ pub const PALETTE: [[u8; 3]; 7] = [
|
|||||||
/// S, M, L in image pixels; M is the default.
|
/// S, M, L in image pixels; M is the default.
|
||||||
pub const WIDTHS: [(&str, f32); 3] = [("S", 2.0), ("M", 4.0), ("L", 8.0)];
|
pub const WIDTHS: [(&str, f32); 3] = [("S", 2.0), ("M", 4.0), ("L", 8.0)];
|
||||||
|
|
||||||
|
/// 0, 1, 2 for S, M, L.
|
||||||
|
pub fn size_index(width: f32) -> usize {
|
||||||
|
WIDTHS.iter().position(|w| w.1 == width).unwrap_or(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text_size(width: f32) -> f32 {
|
||||||
|
[16.0, 24.0, 36.0][size_index(width)]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn block_size(width: f32) -> u32 {
|
||||||
|
[8, 12, 20][size_index(width)]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn number_diameter(width: f32) -> f32 {
|
||||||
|
[24.0, 32.0, 44.0][size_index(width)]
|
||||||
|
}
|
||||||
|
|
||||||
const HIGHLIGHT_ALPHA: u8 = 102;
|
const HIGHLIGHT_ALPHA: u8 = 102;
|
||||||
const CLICK: f32 = 2.0;
|
const CLICK: f32 = 2.0;
|
||||||
const ELLIPSE_POINTS: usize = 72;
|
const ELLIPSE_POINTS: usize = 72;
|
||||||
@@ -64,18 +93,20 @@ pub struct Style {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// One finished mark. Two-point tools hold [start, end]; Pen and
|
/// One finished mark. Two-point tools hold [start, end]; Pen and
|
||||||
/// Highlighter hold every point.
|
/// Highlighter hold every point; Text holds its top-left and Number its
|
||||||
|
/// centre, with the text or the number in `text`.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Shape {
|
pub struct Shape {
|
||||||
pub tool: Tool,
|
pub tool: Tool,
|
||||||
pub style: Style,
|
pub style: Style,
|
||||||
pub points: Vec<P>,
|
pub points: Vec<P>,
|
||||||
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Shape {
|
impl Shape {
|
||||||
/// A press without a real drag. A Pen click still leaves a dot.
|
/// A press without a real drag. A Pen click still leaves a dot.
|
||||||
pub fn is_click(&self) -> bool {
|
pub fn is_click(&self) -> bool {
|
||||||
if self.tool == Tool::Pen {
|
if matches!(self.tool, Tool::Pen | Tool::Text | Tool::Number) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let first = self.points[0];
|
let first = self.points[0];
|
||||||
@@ -120,6 +151,7 @@ impl Drag {
|
|||||||
tool: self.tool,
|
tool: self.tool,
|
||||||
style: self.style,
|
style: self.style,
|
||||||
points,
|
points,
|
||||||
|
text: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,11 +170,11 @@ pub fn constrain(tool: Tool, start: P, end: P, shift: bool) -> P {
|
|||||||
let len = dx.hypot(dy);
|
let len = dx.hypot(dy);
|
||||||
(start.0 + len * angle.cos(), start.1 + len * angle.sin())
|
(start.0 + len * angle.cos(), start.1 + len * angle.sin())
|
||||||
}
|
}
|
||||||
Tool::Rectangle | Tool::Ellipse => {
|
Tool::Rectangle | Tool::Ellipse | Tool::Pixelate => {
|
||||||
let side = dx.abs().max(dy.abs());
|
let side = dx.abs().max(dy.abs());
|
||||||
(start.0 + side.copysign(dx), start.1 + side.copysign(dy))
|
(start.0 + side.copysign(dx), start.1 + side.copysign(dy))
|
||||||
}
|
}
|
||||||
Tool::Pen | Tool::Highlighter => end,
|
Tool::Pen | Tool::Highlighter | Tool::Text | Tool::Number | Tool::Crop => end,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +237,19 @@ pub fn geometry(shape: &Shape) -> Vec<Prim> {
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
Tool::Pen => vec![stroke(pts.clone(), false)],
|
Tool::Pen => vec![stroke(pts.clone(), false)],
|
||||||
|
// Drawn by the export renderer only.
|
||||||
|
Tool::Text | Tool::Number | Tool::Crop => Vec::new(),
|
||||||
|
// While dragging: the area that will be pixelated.
|
||||||
|
Tool::Pixelate => {
|
||||||
|
let ((x0, y0), (x1, y1)) = (pts[0], pts[1]);
|
||||||
|
vec![Prim::Stroke {
|
||||||
|
points: vec![(x0, y0), (x1, y0), (x1, y1), (x0, y1)],
|
||||||
|
width: 2.0,
|
||||||
|
color: [255, 255, 255, 200],
|
||||||
|
closed: true,
|
||||||
|
round: false,
|
||||||
|
}]
|
||||||
|
}
|
||||||
Tool::Highlighter => vec![Prim::Stroke {
|
Tool::Highlighter => vec![Prim::Stroke {
|
||||||
points: pts.clone(),
|
points: pts.clone(),
|
||||||
width: w * 4.0,
|
width: w * 4.0,
|
||||||
@@ -266,6 +311,7 @@ mod tests {
|
|||||||
tool,
|
tool,
|
||||||
style: RED,
|
style: RED,
|
||||||
points: points.to_vec(),
|
points: points.to_vec(),
|
||||||
|
text: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,4 +445,50 @@ mod tests {
|
|||||||
assert_eq!(d.shape(false).points, vec![(0.0, 0.0), (30.0, 10.0)]);
|
assert_eq!(d.shape(false).points, vec![(0.0, 0.0), (30.0, 10.0)]);
|
||||||
assert_eq!(d.shape(true).points, vec![(0.0, 0.0), (30.0, 30.0)]);
|
assert_eq!(d.shape(true).points, vec![(0.0, 0.0), (30.0, 30.0)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sizes_follow_s_m_l() {
|
||||||
|
let at = |w: f32| Style {
|
||||||
|
color: [0, 0, 0],
|
||||||
|
width: w,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
[2.0, 4.0, 8.0].map(|w| text_size(at(w).width)),
|
||||||
|
[16.0, 24.0, 36.0]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
[2.0, 4.0, 8.0].map(|w| block_size(at(w).width)),
|
||||||
|
[8, 12, 20]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
[2.0, 4.0, 8.0].map(|w| number_diameter(at(w).width)),
|
||||||
|
[24.0, 32.0, 44.0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pixelate_drags_like_a_rectangle() {
|
||||||
|
let mut d = Drag::new(Tool::Pixelate, RED, (0.0, 0.0));
|
||||||
|
d.move_to((30.0, 10.0));
|
||||||
|
assert_eq!(d.shape(true).points, vec![(0.0, 0.0), (30.0, 30.0)]);
|
||||||
|
let g = geometry(&d.shape(false));
|
||||||
|
let Prim::Stroke { color, closed, .. } = &g[0] else {
|
||||||
|
panic!("{g:?}")
|
||||||
|
};
|
||||||
|
assert!(*closed && *color == [255, 255, 255, 200]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_and_numbers_are_never_clicks_and_have_no_egui_geometry() {
|
||||||
|
for tool in [Tool::Text, Tool::Number] {
|
||||||
|
let s = Shape {
|
||||||
|
tool,
|
||||||
|
style: RED,
|
||||||
|
points: vec![(5.0, 5.0)],
|
||||||
|
text: "1".into(),
|
||||||
|
};
|
||||||
|
assert!(!s.is_click());
|
||||||
|
assert!(geometry(&s).is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user