feat: add marquee multi-select and always-on middle-button pan
- Drag on empty space in Select tool draws a marquee rectangle; elements intersecting it on release become selected - Shift+click toggles elements in/out of the selection set - Moving and deleting operate on all selected elements at once - Middle mouse button drag now pans unconditionally in every tool mode - Session selection state changed from Option<String> to Vec<String> - New DragState::MarqueeSelecting variant with dashed rect rendering - 29 tests passing, clippy clean
This commit is contained in:
@@ -3,7 +3,7 @@ use crate::canvas::{CanvasInteraction, CanvasState};
|
|||||||
use crate::clipboard::ClipboardManager;
|
use crate::clipboard::ClipboardManager;
|
||||||
use crate::command_palette::{CommandId, CommandPalette};
|
use crate::command_palette::{CommandId, CommandPalette};
|
||||||
use crate::drawing::{
|
use crate::drawing::{
|
||||||
draw_creation_preview, draw_elements, draw_selection, find_handle_at_screen_pos,
|
draw_creation_preview, draw_elements, draw_marquee, draw_selection, find_handle_at_screen_pos,
|
||||||
screen_to_canvas, DragState, DrawingElement, Shape, ShapeStyle, Tool,
|
screen_to_canvas, DragState, DrawingElement, Shape, ShapeStyle, Tool,
|
||||||
};
|
};
|
||||||
use crate::history::{ChangeSource, HistoryTree, NodeId};
|
use crate::history::{ChangeSource, HistoryTree, NodeId};
|
||||||
@@ -347,9 +347,7 @@ impl AgCanvasApp {
|
|||||||
DrawingCommand::Delete { session_id, id } => {
|
DrawingCommand::Delete { session_id, id } => {
|
||||||
if let Some(session) = self.sessions.iter_mut().find(|s| s.id == session_id) {
|
if let Some(session) = self.sessions.iter_mut().find(|s| s.id == session_id) {
|
||||||
session.drawing_elements.retain(|e| e.id != id);
|
session.drawing_elements.retain(|e| e.id != id);
|
||||||
if session.selected_element_id.as_deref() == Some(&id) {
|
session.selected_element_ids.retain(|selected_id| selected_id != &id);
|
||||||
session.selected_element_id = None;
|
|
||||||
}
|
|
||||||
session.record_edit(
|
session.record_edit(
|
||||||
"Agent: Delete Element",
|
"Agent: Delete Element",
|
||||||
ChangeSource::Agent { name: None },
|
ChangeSource::Agent { name: None },
|
||||||
@@ -359,7 +357,7 @@ impl AgCanvasApp {
|
|||||||
DrawingCommand::Clear { session_id } => {
|
DrawingCommand::Clear { session_id } => {
|
||||||
if let Some(session) = self.sessions.iter_mut().find(|s| s.id == session_id) {
|
if let Some(session) = self.sessions.iter_mut().find(|s| s.id == session_id) {
|
||||||
session.drawing_elements.clear();
|
session.drawing_elements.clear();
|
||||||
session.selected_element_id = None;
|
session.selected_element_ids.clear();
|
||||||
session
|
session
|
||||||
.record_edit("Agent: Clear Canvas", ChangeSource::Agent { name: None });
|
.record_edit("Agent: Clear Canvas", ChangeSource::Agent { name: None });
|
||||||
}
|
}
|
||||||
@@ -606,11 +604,19 @@ fn handle_select_tool(
|
|||||||
zoom: f32,
|
zoom: f32,
|
||||||
pointer_pos: Option<egui::Pos2>,
|
pointer_pos: Option<egui::Pos2>,
|
||||||
) {
|
) {
|
||||||
if response.drag_started() {
|
if response.drag_started_by(egui::PointerButton::Primary) {
|
||||||
if let Some(screen_pos) = pointer_pos {
|
if let Some(screen_pos) = pointer_pos {
|
||||||
if let Some(selected_el) = session.selected_element() {
|
let shift_held = response.ctx.input(|i| i.modifiers.shift);
|
||||||
if let Some(handle) =
|
|
||||||
find_handle_at_screen_pos(selected_el, screen_pos, canvas_center, offset, zoom)
|
if let Some((handle, selected_el)) = session
|
||||||
|
.drawing_elements
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.filter(|el| session.selected_element_ids.contains(&el.id))
|
||||||
|
.find_map(|el| {
|
||||||
|
find_handle_at_screen_pos(el, screen_pos, canvas_center, offset, zoom)
|
||||||
|
.map(|handle| (handle, el))
|
||||||
|
})
|
||||||
{
|
{
|
||||||
let original_rect = selected_el.bounding_rect();
|
let original_rect = selected_el.bounding_rect();
|
||||||
let eid = selected_el.id.clone();
|
let eid = selected_el.id.clone();
|
||||||
@@ -621,7 +627,6 @@ fn handle_select_tool(
|
|||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let canvas_pos = screen_to_canvas(screen_pos, canvas_center, offset, zoom);
|
let canvas_pos = screen_to_canvas(screen_pos, canvas_center, offset, zoom);
|
||||||
let hit = session
|
let hit = session
|
||||||
@@ -632,26 +637,50 @@ fn handle_select_tool(
|
|||||||
|
|
||||||
if let Some(el) = hit {
|
if let Some(el) = hit {
|
||||||
let eid = el.id.clone();
|
let eid = el.id.clone();
|
||||||
session.selected_element_id = Some(eid.clone());
|
if shift_held {
|
||||||
session.drag_state = DragState::Moving { element_id: eid };
|
if session.selected_element_ids.contains(&eid) {
|
||||||
|
session.selected_element_ids.retain(|id| id != &eid);
|
||||||
} else {
|
} else {
|
||||||
session.selected_element_id = None;
|
session.selected_element_ids.push(eid);
|
||||||
|
}
|
||||||
session.drag_state = DragState::None;
|
session.drag_state = DragState::None;
|
||||||
|
} else {
|
||||||
|
if !session.selected_element_ids.contains(&eid) {
|
||||||
|
session.selected_element_ids = vec![eid.clone()];
|
||||||
|
}
|
||||||
|
session.drag_state = DragState::Moving {
|
||||||
|
element_ids: session.selected_element_ids.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !shift_held {
|
||||||
|
session.selected_element_ids.clear();
|
||||||
|
}
|
||||||
|
session.drag_state = DragState::MarqueeSelecting {
|
||||||
|
start: canvas_pos,
|
||||||
|
current: canvas_pos,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.dragged() {
|
if response.dragged_by(egui::PointerButton::Primary) {
|
||||||
let delta_screen = response.drag_delta();
|
let delta_screen = response.drag_delta();
|
||||||
let delta_canvas = delta_screen / zoom;
|
let delta_canvas = delta_screen / zoom;
|
||||||
|
|
||||||
match &session.drag_state {
|
match &mut session.drag_state {
|
||||||
DragState::Moving { element_id, .. } => {
|
DragState::Moving { element_ids } => {
|
||||||
let eid = element_id.clone();
|
for eid in element_ids.iter() {
|
||||||
if let Some(el) = session.drawing_elements.iter_mut().find(|e| e.id == eid) {
|
if let Some(el) = session.drawing_elements.iter_mut().find(|e| e.id == *eid) {
|
||||||
el.translate(delta_canvas);
|
el.translate(delta_canvas);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
DragState::MarqueeSelecting { current, .. } => {
|
||||||
|
if let Some(screen_pos) = pointer_pos {
|
||||||
|
*current = screen_to_canvas(screen_pos, canvas_center, offset, zoom);
|
||||||
|
}
|
||||||
|
}
|
||||||
DragState::Resizing {
|
DragState::Resizing {
|
||||||
handle,
|
handle,
|
||||||
element_id,
|
element_id,
|
||||||
@@ -672,25 +701,46 @@ fn handle_select_tool(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
DragState::None => {
|
||||||
session.canvas_state.pan(delta_screen);
|
session.canvas_state.pan(delta_screen);
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.drag_stopped() {
|
if response.drag_stopped_by(egui::PointerButton::Primary) {
|
||||||
match &session.drag_state {
|
match &session.drag_state {
|
||||||
DragState::Moving { .. } => session.record_edit("Move Element", ChangeSource::Human),
|
DragState::Moving { .. } => session.record_edit("Move Element", ChangeSource::Human),
|
||||||
DragState::Resizing { .. } => {
|
DragState::Resizing { .. } => {
|
||||||
session.record_edit("Resize Element", ChangeSource::Human)
|
session.record_edit("Resize Element", ChangeSource::Human)
|
||||||
}
|
}
|
||||||
|
DragState::MarqueeSelecting { start, current } => {
|
||||||
|
let marquee = egui::Rect::from_two_pos(*start, *current);
|
||||||
|
let marquee_hits: Vec<String> = session
|
||||||
|
.drawing_elements
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.bounding_rect().intersects(marquee))
|
||||||
|
.map(|e| e.id.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if response.ctx.input(|i| i.modifiers.shift) {
|
||||||
|
for id in marquee_hits {
|
||||||
|
if !session.selected_element_ids.contains(&id) {
|
||||||
|
session.selected_element_ids.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
session.selected_element_ids = marquee_hits;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
session.drag_state = DragState::None;
|
session.drag_state = DragState::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.clicked() && !response.dragged() {
|
if response.clicked_by(egui::PointerButton::Primary) && !response.dragged() {
|
||||||
if let Some(screen_pos) = pointer_pos {
|
if let Some(screen_pos) = pointer_pos {
|
||||||
|
let shift_held = response.ctx.input(|i| i.modifiers.shift);
|
||||||
let canvas_pos = screen_to_canvas(screen_pos, canvas_center, offset, zoom);
|
let canvas_pos = screen_to_canvas(screen_pos, canvas_center, offset, zoom);
|
||||||
let hit = session
|
let hit = session
|
||||||
.drawing_elements
|
.drawing_elements
|
||||||
@@ -698,7 +748,19 @@ fn handle_select_tool(
|
|||||||
.rev()
|
.rev()
|
||||||
.find(|e| e.contains_point(canvas_pos));
|
.find(|e| e.contains_point(canvas_pos));
|
||||||
|
|
||||||
session.selected_element_id = hit.map(|e| e.id.clone());
|
if shift_held {
|
||||||
|
if let Some(el) = hit {
|
||||||
|
if session.selected_element_ids.contains(&el.id) {
|
||||||
|
session.selected_element_ids.retain(|id| id != &el.id);
|
||||||
|
} else {
|
||||||
|
session.selected_element_ids.push(el.id.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(el) = hit {
|
||||||
|
session.selected_element_ids = vec![el.id.clone()];
|
||||||
|
} else {
|
||||||
|
session.selected_element_ids.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -711,7 +773,7 @@ fn handle_shape_tool(
|
|||||||
zoom: f32,
|
zoom: f32,
|
||||||
pointer_pos: Option<egui::Pos2>,
|
pointer_pos: Option<egui::Pos2>,
|
||||||
) {
|
) {
|
||||||
if response.drag_started() {
|
if response.drag_started_by(egui::PointerButton::Primary) {
|
||||||
if let Some(screen_pos) = pointer_pos {
|
if let Some(screen_pos) = pointer_pos {
|
||||||
let canvas_pos = screen_to_canvas(screen_pos, canvas_center, offset, zoom);
|
let canvas_pos = screen_to_canvas(screen_pos, canvas_center, offset, zoom);
|
||||||
session.drag_state = DragState::Creating {
|
session.drag_state = DragState::Creating {
|
||||||
@@ -721,7 +783,7 @@ fn handle_shape_tool(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.dragged() {
|
if response.dragged_by(egui::PointerButton::Primary) {
|
||||||
if let DragState::Creating { current, .. } = &mut session.drag_state {
|
if let DragState::Creating { current, .. } = &mut session.drag_state {
|
||||||
if let Some(screen_pos) = pointer_pos {
|
if let Some(screen_pos) = pointer_pos {
|
||||||
*current = screen_to_canvas(screen_pos, canvas_center, offset, zoom);
|
*current = screen_to_canvas(screen_pos, canvas_center, offset, zoom);
|
||||||
@@ -729,7 +791,7 @@ fn handle_shape_tool(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.drag_stopped() {
|
if response.drag_stopped_by(egui::PointerButton::Primary) {
|
||||||
if let DragState::Creating { start, current } = &session.drag_state {
|
if let DragState::Creating { start, current } = &session.drag_state {
|
||||||
let start = *start;
|
let start = *start;
|
||||||
let current = *current;
|
let current = *current;
|
||||||
@@ -768,7 +830,7 @@ fn handle_shape_tool(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let element = DrawingElement::new(shape, ShapeStyle::default());
|
let element = DrawingElement::new(shape, ShapeStyle::default());
|
||||||
session.selected_element_id = Some(element.id.clone());
|
session.selected_element_ids = vec![element.id.clone()];
|
||||||
session.drawing_elements.push(element);
|
session.drawing_elements.push(element);
|
||||||
session.record_edit(&format!("Draw {}", tool_label), ChangeSource::Human);
|
session.record_edit(&format!("Draw {}", tool_label), ChangeSource::Human);
|
||||||
}
|
}
|
||||||
@@ -868,7 +930,11 @@ impl eframe::App for AgCanvasApp {
|
|||||||
if toggle_history {
|
if toggle_history {
|
||||||
self.show_history_panel = !self.show_history_panel;
|
self.show_history_panel = !self.show_history_panel;
|
||||||
}
|
}
|
||||||
if delete_selected && !self.show_text_input && !self.show_mermaid_dialog {
|
if delete_selected
|
||||||
|
&& !self.show_text_input
|
||||||
|
&& !self.show_mermaid_dialog
|
||||||
|
&& !self.active_session().selected_element_ids.is_empty()
|
||||||
|
{
|
||||||
self.active_session_mut().delete_selected();
|
self.active_session_mut().delete_selected();
|
||||||
self.active_session_mut()
|
self.active_session_mut()
|
||||||
.record_edit("Delete Element", ChangeSource::Human);
|
.record_edit("Delete Element", ChangeSource::Human);
|
||||||
@@ -1335,7 +1401,7 @@ impl eframe::App for AgCanvasApp {
|
|||||||
self.active_session_mut().drawing_elements.push(element);
|
self.active_session_mut().drawing_elements.push(element);
|
||||||
self.active_session_mut()
|
self.active_session_mut()
|
||||||
.record_edit("Draw Text", ChangeSource::Human);
|
.record_edit("Draw Text", ChangeSource::Human);
|
||||||
self.active_session_mut().selected_element_id = Some(eid);
|
self.active_session_mut().selected_element_ids = vec![eid];
|
||||||
}
|
}
|
||||||
self.show_text_input = false;
|
self.show_text_input = false;
|
||||||
self.text_input_buffer.clear();
|
self.text_input_buffer.clear();
|
||||||
@@ -1353,17 +1419,9 @@ impl eframe::App for AgCanvasApp {
|
|||||||
let response = CanvasInteraction::allocate_canvas(ui);
|
let response = CanvasInteraction::allocate_canvas(ui);
|
||||||
let canvas_center = response.rect.center();
|
let canvas_center = response.rect.center();
|
||||||
|
|
||||||
let active_tool = self.active_session().active_tool;
|
if response.dragged_by(egui::PointerButton::Middle) {
|
||||||
let is_select_idle = active_tool == Tool::Select
|
self.active_session_mut().canvas_state.pan(response.drag_delta());
|
||||||
&& matches!(self.active_session().drag_state, DragState::None);
|
}
|
||||||
|
|
||||||
if is_select_idle {
|
|
||||||
CanvasInteraction::handle(
|
|
||||||
ui,
|
|
||||||
&response,
|
|
||||||
&mut self.active_session_mut().canvas_state,
|
|
||||||
);
|
|
||||||
} else if active_tool != Tool::Select {
|
|
||||||
if response.hovered() {
|
if response.hovered() {
|
||||||
let scroll_delta = ui.input(|i| i.smooth_scroll_delta.y);
|
let scroll_delta = ui.input(|i| i.smooth_scroll_delta.y);
|
||||||
if scroll_delta != 0.0 {
|
if scroll_delta != 0.0 {
|
||||||
@@ -1377,11 +1435,13 @@ impl eframe::App for AgCanvasApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if response.dragged_by(egui::PointerButton::Middle) {
|
|
||||||
self.active_session_mut()
|
let is_idle = matches!(self.active_session().drag_state, DragState::None);
|
||||||
.canvas_state
|
if is_idle
|
||||||
.pan(response.drag_delta());
|
&& response.dragged_by(egui::PointerButton::Primary)
|
||||||
}
|
&& ui.input(|i| i.modifiers.command)
|
||||||
|
{
|
||||||
|
self.active_session_mut().canvas_state.pan(response.drag_delta());
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.show_mermaid_dialog && !self.show_text_input {
|
if !self.show_mermaid_dialog && !self.show_text_input {
|
||||||
@@ -1424,10 +1484,15 @@ impl eframe::App for AgCanvasApp {
|
|||||||
zoom,
|
zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(selected_el) = self.active_session().selected_element() {
|
for selected_el in self.active_session().selected_elements() {
|
||||||
draw_selection(&painter, selected_el, canvas_center, offset, zoom);
|
draw_selection(&painter, selected_el, canvas_center, offset, zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let DragState::MarqueeSelecting { start, current } = &self.active_session().drag_state
|
||||||
|
{
|
||||||
|
draw_marquee(&painter, *start, *current, canvas_center, offset, zoom);
|
||||||
|
}
|
||||||
|
|
||||||
let drag_state = self.active_session().drag_state.clone();
|
let drag_state = self.active_session().drag_state.clone();
|
||||||
draw_creation_preview(
|
draw_creation_preview(
|
||||||
&painter,
|
&painter,
|
||||||
|
|||||||
@@ -1,34 +1,8 @@
|
|||||||
use super::CanvasState;
|
|
||||||
use egui::{Response, Sense, Ui};
|
use egui::{Response, Sense, Ui};
|
||||||
|
|
||||||
pub struct CanvasInteraction;
|
pub struct CanvasInteraction;
|
||||||
|
|
||||||
impl CanvasInteraction {
|
impl CanvasInteraction {
|
||||||
pub fn handle(ui: &Ui, response: &Response, state: &mut CanvasState) {
|
|
||||||
if response.dragged_by(egui::PointerButton::Middle)
|
|
||||||
|| (response.dragged_by(egui::PointerButton::Primary)
|
|
||||||
&& ui.input(|i| i.modifiers.command))
|
|
||||||
{
|
|
||||||
state.pan(response.drag_delta());
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.hovered() {
|
|
||||||
let scroll_delta = ui.input(|i| i.smooth_scroll_delta.y);
|
|
||||||
if scroll_delta != 0.0 {
|
|
||||||
let zoom_factor = 1.0 + scroll_delta * 0.001;
|
|
||||||
if let Some(pointer_pos) = response.hover_pos() {
|
|
||||||
state.zoom_at(pointer_pos, response.rect.center(), zoom_factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.input(|i| {
|
|
||||||
if i.key_pressed(egui::Key::Num0) && i.modifiers.command {
|
|
||||||
state.reset();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn allocate_canvas(ui: &mut Ui) -> Response {
|
pub fn allocate_canvas(ui: &mut Ui) -> Response {
|
||||||
let available_size = ui.available_size();
|
let available_size = ui.available_size();
|
||||||
let (rect, response) = ui.allocate_exact_size(available_size, Sense::click_and_drag());
|
let (rect, response) = ui.allocate_exact_size(available_size, Sense::click_and_drag());
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ mod tool;
|
|||||||
pub use boolean::BooleanOpType;
|
pub use boolean::BooleanOpType;
|
||||||
pub use element::{DrawingElement, Shape, ShapeStyle};
|
pub use element::{DrawingElement, Shape, ShapeStyle};
|
||||||
pub use render::{
|
pub use render::{
|
||||||
draw_creation_preview, draw_elements, draw_selection, find_handle_at_screen_pos,
|
draw_creation_preview, draw_elements, draw_marquee, draw_selection, find_handle_at_screen_pos,
|
||||||
screen_to_canvas,
|
screen_to_canvas,
|
||||||
};
|
};
|
||||||
pub use tool::{DragState, Tool};
|
pub use tool::{DragState, Tool};
|
||||||
|
|||||||
@@ -134,6 +134,80 @@ pub fn draw_selection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn draw_marquee(
|
||||||
|
painter: &Painter,
|
||||||
|
start: Pos2,
|
||||||
|
current: Pos2,
|
||||||
|
canvas_center: Pos2,
|
||||||
|
offset: Vec2,
|
||||||
|
zoom: f32,
|
||||||
|
) {
|
||||||
|
let screen_start = canvas_to_screen(start, canvas_center, offset, zoom);
|
||||||
|
let screen_current = canvas_to_screen(current, canvas_center, offset, zoom);
|
||||||
|
let rect = egui::Rect::from_two_pos(screen_start, screen_current);
|
||||||
|
let fill = Color32::from_rgba_premultiplied(
|
||||||
|
SELECTION_COLOR.r(),
|
||||||
|
SELECTION_COLOR.g(),
|
||||||
|
SELECTION_COLOR.b(),
|
||||||
|
36,
|
||||||
|
);
|
||||||
|
|
||||||
|
painter.rect_filled(rect, 0.0, fill);
|
||||||
|
draw_dashed_rect(painter, rect, Stroke::new(1.5, SELECTION_COLOR), 8.0, 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_dashed_rect(painter: &Painter, rect: egui::Rect, stroke: Stroke, dash: f32, gap: f32) {
|
||||||
|
draw_dashed_line(
|
||||||
|
painter,
|
||||||
|
rect.left_top(),
|
||||||
|
rect.right_top(),
|
||||||
|
stroke,
|
||||||
|
dash,
|
||||||
|
gap,
|
||||||
|
);
|
||||||
|
draw_dashed_line(
|
||||||
|
painter,
|
||||||
|
rect.right_top(),
|
||||||
|
rect.right_bottom(),
|
||||||
|
stroke,
|
||||||
|
dash,
|
||||||
|
gap,
|
||||||
|
);
|
||||||
|
draw_dashed_line(
|
||||||
|
painter,
|
||||||
|
rect.right_bottom(),
|
||||||
|
rect.left_bottom(),
|
||||||
|
stroke,
|
||||||
|
dash,
|
||||||
|
gap,
|
||||||
|
);
|
||||||
|
draw_dashed_line(
|
||||||
|
painter,
|
||||||
|
rect.left_bottom(),
|
||||||
|
rect.left_top(),
|
||||||
|
stroke,
|
||||||
|
dash,
|
||||||
|
gap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_dashed_line(painter: &Painter, from: Pos2, to: Pos2, stroke: Stroke, dash: f32, gap: f32) {
|
||||||
|
let dir = to - from;
|
||||||
|
let len = dir.length();
|
||||||
|
if len <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir_norm = dir / len;
|
||||||
|
let mut covered = 0.0;
|
||||||
|
while covered < len {
|
||||||
|
let seg_start = from + dir_norm * covered;
|
||||||
|
let seg_end = from + dir_norm * (covered + dash).min(len);
|
||||||
|
painter.line_segment([seg_start, seg_end], stroke);
|
||||||
|
covered += dash + gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_path_fill(
|
fn draw_path_fill(
|
||||||
painter: &Painter,
|
painter: &Painter,
|
||||||
polygon: &PathPolygon,
|
polygon: &PathPolygon,
|
||||||
|
|||||||
@@ -45,7 +45,11 @@ pub enum DragState {
|
|||||||
current: Pos2,
|
current: Pos2,
|
||||||
},
|
},
|
||||||
Moving {
|
Moving {
|
||||||
element_id: String,
|
element_ids: Vec<String>,
|
||||||
|
},
|
||||||
|
MarqueeSelecting {
|
||||||
|
start: Pos2,
|
||||||
|
current: Pos2,
|
||||||
},
|
},
|
||||||
Resizing {
|
Resizing {
|
||||||
handle: ResizeHandle,
|
handle: ResizeHandle,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ pub struct Session {
|
|||||||
pub description_text: String,
|
pub description_text: String,
|
||||||
|
|
||||||
pub drawing_elements: Vec<DrawingElement>,
|
pub drawing_elements: Vec<DrawingElement>,
|
||||||
pub selected_element_id: Option<String>,
|
pub selected_element_ids: Vec<String>,
|
||||||
pub active_tool: Tool,
|
pub active_tool: Tool,
|
||||||
pub drag_state: DragState,
|
pub drag_state: DragState,
|
||||||
pub history: HistoryTree,
|
pub history: HistoryTree,
|
||||||
@@ -110,7 +110,7 @@ impl Session {
|
|||||||
svg_source: None,
|
svg_source: None,
|
||||||
description_text: String::new(),
|
description_text: String::new(),
|
||||||
drawing_elements: Vec::new(),
|
drawing_elements: Vec::new(),
|
||||||
selected_element_id: None,
|
selected_element_ids: Vec::new(),
|
||||||
active_tool: Tool::default(),
|
active_tool: Tool::default(),
|
||||||
drag_state: DragState::default(),
|
drag_state: DragState::default(),
|
||||||
history: HistoryTree::new(DocumentSnapshot::new_empty()),
|
history: HistoryTree::new(DocumentSnapshot::new_empty()),
|
||||||
@@ -145,26 +145,37 @@ impl Session {
|
|||||||
self.svg_source = None;
|
self.svg_source = None;
|
||||||
self.description_text.clear();
|
self.description_text.clear();
|
||||||
self.drawing_elements.clear();
|
self.drawing_elements.clear();
|
||||||
self.selected_element_id = None;
|
self.selected_element_ids.clear();
|
||||||
self.drag_state = DragState::default();
|
self.drag_state = DragState::default();
|
||||||
self.canvas_state.reset();
|
self.canvas_state.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn selected_element(&self) -> Option<&DrawingElement> {
|
pub fn selected_element(&self) -> Option<&DrawingElement> {
|
||||||
self.selected_element_id
|
self.selected_element_ids
|
||||||
.as_ref()
|
.iter()
|
||||||
.and_then(|id| self.drawing_elements.iter().find(|e| e.id == *id))
|
.find_map(|id| self.drawing_elements.iter().find(|e| e.id == *id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_elements(&self) -> Vec<&DrawingElement> {
|
||||||
|
self.selected_element_ids
|
||||||
|
.iter()
|
||||||
|
.filter_map(|id| self.drawing_elements.iter().find(|e| e.id == *id))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn selected_element_mut(&mut self) -> Option<&mut DrawingElement> {
|
pub fn selected_element_mut(&mut self) -> Option<&mut DrawingElement> {
|
||||||
let id = self.selected_element_id.clone();
|
let id = self.selected_element_ids.first().cloned();
|
||||||
id.and_then(move |id| self.drawing_elements.iter_mut().find(|e| e.id == id))
|
id.and_then(move |id| self.drawing_elements.iter_mut().find(|e| e.id == id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_selected(&mut self) {
|
pub fn delete_selected(&mut self) {
|
||||||
if let Some(id) = self.selected_element_id.take() {
|
if !self.selected_element_ids.is_empty() {
|
||||||
self.drawing_elements.retain(|e| e.id != id);
|
let selected_ids = self.selected_element_ids.clone();
|
||||||
|
self.drawing_elements
|
||||||
|
.retain(|e| !selected_ids.contains(&e.id));
|
||||||
|
self.selected_element_ids.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +193,7 @@ impl Session {
|
|||||||
self.svg_renderer = None;
|
self.svg_renderer = None;
|
||||||
self.svg_texture = None;
|
self.svg_texture = None;
|
||||||
self.description_text.clear();
|
self.description_text.clear();
|
||||||
self.selected_element_id = None;
|
self.selected_element_ids.clear();
|
||||||
self.drag_state = DragState::default();
|
self.drag_state = DragState::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +205,7 @@ impl Session {
|
|||||||
self.svg_renderer = None;
|
self.svg_renderer = None;
|
||||||
self.svg_texture = None;
|
self.svg_texture = None;
|
||||||
self.description_text.clear();
|
self.description_text.clear();
|
||||||
self.selected_element_id = None;
|
self.selected_element_ids.clear();
|
||||||
self.drag_state = DragState::default();
|
self.drag_state = DragState::default();
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
@@ -210,7 +221,7 @@ impl Session {
|
|||||||
self.svg_renderer = None;
|
self.svg_renderer = None;
|
||||||
self.svg_texture = None;
|
self.svg_texture = None;
|
||||||
self.description_text.clear();
|
self.description_text.clear();
|
||||||
self.selected_element_id = None;
|
self.selected_element_ids.clear();
|
||||||
self.drag_state = DragState::default();
|
self.drag_state = DragState::default();
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user