From 9337f15da171fe4830d12106fb7df63122fcf11c Mon Sep 17 00:00:00 2001 From: yeqing Date: Sat, 28 Feb 2026 18:29:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=A4=9A=E8=BE=B9?= =?UTF-8?q?=E5=BD=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mscanvas.rs | 48 ++++++++++++++++++++ src/paint.rs | 118 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 153 insertions(+), 13 deletions(-) diff --git a/src/mscanvas.rs b/src/mscanvas.rs index 597401a..a2909c7 100644 --- a/src/mscanvas.rs +++ b/src/mscanvas.rs @@ -37,6 +37,20 @@ impl MSColor { }; } +pub struct Path2D { + points: Vec, +} + +impl Path2D { + pub fn new() -> Self { + Self { points: vec![] } + } + + pub fn push(&mut self, point: Point) { + self.points.push(point); + } +} + pub struct MSCanvas { width: i32, height: i32, @@ -50,6 +64,8 @@ pub struct MSCanvas { color: MSColor, line_width: i32, + + path2d: Path2D, } #[allow(unused)] @@ -62,6 +78,7 @@ impl MSCanvas { pixels_bak: Vec::new(), color: MSColor::BLACK, line_width: 1, + path2d: Path2D::new(), } } @@ -635,6 +652,37 @@ impl MSCanvas { let points = vec![begin, p1, p2, end]; self.bezier(points); } + + pub fn begin_path(&mut self) { + self.path2d = Path2D::new(); + } + + pub fn close_path(&mut self) { + let points = &self.path2d.points; + if points.len() < 3 { + return; + } + self.path2d.push(points[0]); + } + + pub fn move_to(&mut self, point: Point) { + self.path2d.push(point); + } + + pub fn line_to(&mut self, point: Point) { + self.path2d.push(point); + } + + pub fn stroke(&mut self) { + let mut points = self.path2d.points.clone(); + if points.len() < 2 { + return; + } + // 连线 + for i in 0..(points.len() - 1) { + self.draw_line_with_circle_brush(points[i], points[i + 1]); + } + } } fn point_muln(point: Point, t: f32) -> Point { diff --git a/src/paint.rs b/src/paint.rs index 8bb7a4d..085c83c 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -53,6 +53,7 @@ enum Message { MousePressed(Point), MouseReleased(Point), MouseMoved(Point), + MouseDoubleClick(Point), Clear, SavePNG, @@ -94,6 +95,34 @@ enum Tool { Count, } +impl std::fmt::Display for Tool { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Tool::FreeFormSelect => "FreeFormSelect", + Tool::Select => "Select", + Tool::Eraser => "Eraser", + Tool::FillWithColor => "FillWithColor", + Tool::PickColor => "PickColor", + Tool::Magnifier => "Magnifier", + Tool::Pencil => "Pencil", + Tool::Brush => "Brush", + Tool::Airbrush => "Airbrush", + Tool::Text => "Text", + Tool::Line => "Line", + Tool::Curve => "Curve", + Tool::Rectangle => "Rectangle", + Tool::Polygon => "Polygon", + Tool::Ellipse => "Ellipse", + Tool::RoundedRectangle => "RoundedRectangle", + Tool::Count => "Count", + } + ) + } +} + impl From for Tool { fn from(tool: usize) -> Self { match tool { @@ -219,6 +248,8 @@ struct PaintApp { is_controlling: bool, control_points: Vec, + is_path_beginning: bool, + /// 用于显示的图像句柄缓存 /// 每次像素变化后需要重新生成 image_handle: image::Handle, @@ -249,6 +280,7 @@ impl PaintApp { end_point: Point::ORIGIN, is_controlling: false, control_points: Vec::with_capacity(2), + is_path_beginning: false, image_handle: image::Handle::from_rgba(width as u32, height as u32, pixels), dirty: false, config, @@ -266,7 +298,8 @@ impl PaintApp { let canvas_area = mouse_area(image_widget) .on_press(|pos| Message::MousePressed(pos)) // 占位,实际逻辑在 on_drag 或自定义 .on_release(|pos| Message::MouseReleased(pos)) - .on_move(|pos| Message::MouseMoved(pos)); + .on_move(|pos| Message::MouseMoved(pos)) + .on_double_click(|pos| Message::MouseDoubleClick(pos)); // 注意:mouse_area 的 on_move 给出的坐标通常是相对于 widget 左上角的,这正是我们需要的! let canvas_area = container(canvas_area) .width(Length::Fill) @@ -293,19 +326,22 @@ impl PaintApp { .on_press(Message::ClickTool(Tool::from(i + 1))); columns.push(row![btn1, btn2].into()); } - let tool_config_area = container("").width(90).height(200).style(|theme: &Theme| { - let palette = theme.extended_palette(); + let tool_config_area = container(text(format!("{}", self.tool_selected))) + .width(90) + .height(200) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); - container::Style { - background: Some(Color::from_rgb8(192, 192, 192).into()), - border: Border { - width: 1.0, - radius: 5.0.into(), - color: palette.background.weak.color, - }, - ..container::Style::default() - } - }); + container::Style { + background: Some(Color::from_rgb8(192, 192, 192).into()), + border: Border { + width: 1.0, + radius: 5.0.into(), + color: palette.background.weak.color, + }, + ..container::Style::default() + } + }); let tool_config_area = container(tool_config_area).padding(padding::top(5).left(5)); columns.push(tool_config_area.into()); let grid = Column::from_vec(columns); @@ -408,6 +444,9 @@ impl PaintApp { Tool::Rectangle => { self.update_with_rectangle(message); } + Tool::Polygon => { + self.update_with_polygon(message); + } _ => {} } @@ -712,6 +751,49 @@ impl PaintApp { } } + pub fn update_with_polygon(&mut self, message: Message) { + match message { + Message::MousePressed(pos) => { + if self.is_path_beginning { + self.is_drawing = true; + self.canvas.restore_pixels(); + self.canvas.stroke(); + self.canvas + .draw_line_with_circle_brush(self.begin_point, pos); + self.dirty = true; + } else { + self.is_path_beginning = true; + self.is_drawing = true; + self.canvas.save_pixels(); + self.begin_point = pos; + self.canvas.begin_path(); + self.canvas.move_to(pos); + } + } + Message::MouseReleased(pos) => { + self.canvas.line_to(pos); + self.begin_point = pos; + } + Message::MouseMoved(pos) => { + if self.is_drawing { + self.canvas.restore_pixels(); + self.canvas.stroke(); + self.canvas + .draw_line_with_circle_brush(self.begin_point, pos); + self.dirty = true; + } + } + Message::MouseDoubleClick(_pos) => { + self.canvas.close_path(); + self.canvas.stroke(); + self.is_drawing = false; + self.is_path_beginning = false; + self.dirty = true; + } + _ => {} + } + } + // endregion pub fn update_tool_states(&mut self, tool: Tool) { @@ -719,12 +801,22 @@ impl PaintApp { if idx >= self.tool_states.len() { return; } + if self.tool_selected == Tool::Polygon { + // 切换到其他工具,闭合路径 + self.canvas.close_path(); + self.canvas.stroke(); + self.dirty = true; + } + let old_value = self.tool_states[idx]; for i in 0..(Tool::Count as usize) { self.tool_states[i] = false; } self.tool_states[idx] = !old_value; self.tool_selected = idx.into(); + + self.is_drawing = false; + self.is_path_beginning = false; } /// 将原始字节转换为 Iced 的图像句柄