From 188d31078938431c7bf6c26f257b9dc42a366138 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 18 Jun 2026 21:02:50 +0300 Subject: [PATCH] feat(pline): create polyline as a live entity so it snaps while drawing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The polyline was only committed to the document on finish, so its partial geometry produced no snap points — you could not snap to vertices already placed. Publish it as a real LwPolyline at the second vertex and replace its geometry in place as later vertices land, so it lives in the document (and the snapper) throughout the draw. New CmdResult::CommitLiveEntity / UpdateLiveEntity{finish} plus a set_live_handle() hook let a command own an entity mid-run. The create pushes a single undo snapshot; in-place updates push none, so the whole object reverts as one unit. Updates preserve the entity's handle and layer (a fresh built entity carries a NULL handle that would desync it from the document map key and drop it from rendering). (#119) Co-Authored-By: Claude Opus 4.8 --- src/app/cmd_result.rs | 50 ++++++++++++++++++ src/command/mod.rs | 20 ++++++++ src/modules/draw/draw/polyline.rs | 85 ++++++++++++++++--------------- 3 files changed, 115 insertions(+), 40 deletions(-) diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index b65aecbc5..b0d95086d 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -460,6 +460,56 @@ impl OpenCADStudio { } } } + CmdResult::CommitLiveEntity(entity) => { + let label = self.history_label_from_active_cmd(i, "ENTITY"); + self.push_undo_snapshot(i, label); + let handle = self.commit_entity_handle(entity); + self.tabs[i].dirty = true; + self.tabs[i].scene.clear_preview_wire(); + if let Some(h) = handle { + if let Some(cmd) = self.tabs[i].active_cmd.as_mut() { + cmd.set_live_handle(h); + } + } + let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); + if let Some(p) = prompt { + self.command_line.push_info(&p); + } + } + CmdResult::UpdateLiveEntity { + handle, + entity, + finish, + } => { + // Replace the live entity's geometry in place, preserving its + // handle and layer (the fresh entity from the command carries + // defaults — a NULL handle would desync it from the document + // map key and drop it from rendering / hit-test). No undo + // snapshot — the create already pushed one, so the whole object + // reverts as a unit. + if let Some(old) = self.tabs[i].scene.document.get_entity_mut(handle) { + let old_handle = old.as_entity().handle(); + let layer = old.as_entity().layer().to_string(); + let mut new = entity; + new.as_entity_mut().set_handle(old_handle); + new.as_entity_mut().set_layer(layer); + *old = new; + self.tabs[i].scene.mark_entity_dirty(handle); + self.tabs[i].scene.bump_geometry_no_blocks(); + self.tabs[i].dirty = true; + } + if finish { + self.tabs[i].scene.clear_preview_wire(); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.restore_pre_cmd_tangent(); + } else { + let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); + if let Some(p) = prompt { + self.command_line.push_info(&p); + } + } + } CmdResult::Cancel => { self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; diff --git a/src/command/mod.rs b/src/command/mod.rs index 9a16a9e41..1606fcb58 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -225,6 +225,21 @@ pub enum CmdResult { /// The host should look up the attdefs for `block_name` from the document /// and call `attreq_set_attdefs()` on the command, then loop on text input. AttreqNeeded { block_name: String }, + /// Add a command-owned "live" entity to the document mid-command and hand + /// its assigned handle back to the active command via `set_live_handle()`. + /// One undo snapshot is pushed here, so the whole in-progress object reverts + /// as a single unit. The command stays active. Used by PLINE so the partial + /// polyline is a real, snappable entity while later vertices are placed. + CommitLiveEntity(EntityType), + /// Replace the geometry of the live entity `handle` in place — preserving + /// its layer — without pushing a new undo snapshot. When `finish` is true + /// the command also exits (the entity is already committed, so no separate + /// commit is needed). + UpdateLiveEntity { + handle: Handle, + entity: EntityType, + finish: bool, + }, } /// What kind of value the active command is currently asking for. Drives @@ -417,6 +432,11 @@ pub trait CadCommand: Send { CmdResult::Cancel } + /// Host callback after `CmdResult::CommitLiveEntity`: records the handle the + /// new live entity was assigned so later `UpdateLiveEntity` results can + /// target it. + fn set_live_handle(&mut self, _handle: Handle) {} + /// Point-click pick of domain objects (wire hit-test often misses small markers). fn needs_structure_point_pick(&self) -> bool { false diff --git a/src/modules/draw/draw/polyline.rs b/src/modules/draw/draw/polyline.rs index df3ffb4dc..d508b6fcc 100644 --- a/src/modules/draw/draw/polyline.rs +++ b/src/modules/draw/draw/polyline.rs @@ -11,7 +11,7 @@ use acadrust::entities::LwVertex; use acadrust::types::Vector2; -use acadrust::{EntityType, LwPolyline}; +use acadrust::{EntityType, Handle, LwPolyline}; use glam::{Vec2, Vec3}; use crate::command::{CadCommand, CmdResult}; @@ -46,6 +46,11 @@ pub struct PlineCommand { mode: SegMode, /// Unit direction of the last committed segment (for arc tangent continuity). last_tangent: Option, + /// Handle of the live polyline entity once it exists (created at the 2nd + /// vertex). `None` while only the start point is placed. Having it in the + /// document makes the partial polyline snappable while later vertices are + /// being placed. (#119) + live_handle: Option, } impl PlineCommand { @@ -55,6 +60,24 @@ impl PlineCommand { bulges: Vec::new(), mode: SegMode::Line, last_tangent: None, + live_handle: None, + } + } + + /// Build the result that publishes the current vertices to the document: + /// create the live entity at the 2nd vertex, then replace it in place as + /// more vertices land. `finish` ends the command (used for close/done). + fn sync_live(&self, closed: bool, finish: bool) -> CmdResult { + let entity = self.build_entity(closed); + match (entity, self.live_handle) { + (Some(e), Some(handle)) => CmdResult::UpdateLiveEntity { + handle, + entity: e, + finish, + }, + (Some(e), None) => CmdResult::CommitLiveEntity(e), + // Fewer than 2 vertices: nothing to publish. + (None, _) => CmdResult::Cancel, } } @@ -231,21 +254,26 @@ impl CadCommand for PlineCommand { self.vertices.push(pt); self.bulges.push(0.0); - CmdResult::NeedPoint + // Publish to the document as soon as the polyline has a segment so it + // becomes snappable while the rest is drawn. (#119) + if self.vertices.len() >= 2 { + self.sync_live(false, false) + } else { + CmdResult::NeedPoint + } + } + + fn set_live_handle(&mut self, handle: Handle) { + self.live_handle = Some(handle); } fn on_enter(&mut self) -> CmdResult { - match self.build_entity(false) { - Some(e) => CmdResult::CommitAndExit(e), - None => CmdResult::Cancel, - } + // The committed vertices already live in the document; finalise it. + self.sync_live(false, true) } fn on_escape(&mut self) -> CmdResult { - match self.build_entity(false) { - Some(e) => CmdResult::CommitAndExit(e), - None => CmdResult::Cancel, - } + self.sync_live(false, true) } fn wants_text_input(&self) -> bool { @@ -269,44 +297,21 @@ impl CadCommand for PlineCommand { self.mode = SegMode::Line; Some(CmdResult::NeedPoint) } - "C" | "CLOSE" => match self.build_entity(true) { - Some(e) => Some(CmdResult::CommitAndExit(e)), - None => Some(CmdResult::Cancel), - }, + "C" | "CLOSE" => Some(self.sync_live(true, true)), _ => None, } } fn on_mouse_move(&mut self, pt: Vec3) -> Option { - if self.vertices.is_empty() { - return None; - } - let last = *self.vertices.last().unwrap(); + // The committed vertices already render as a real document entity, so + // the preview is just the pending segment from the last vertex to the + // cursor. (#119) + let last = *self.vertices.last()?; - // Build committed segments first. let mut pts: Vec<[f32; 3]> = Vec::new(); - - // Re-emit all committed vertices + arc samples between them. - for i in 0..self.vertices.len() { - let v = self.vertices[i]; - if i == 0 { - pts.push([v.x, v.y, v.z]); - } else { - let prev = self.vertices[i - 1]; - let b = self.bulges.get(i - 1).copied().unwrap_or(0.0); - if b.abs() > 1e-6 { - // Arc segment: sample it. - let arc_pts = arc_sample_points(prev, b, v, 16); - pts.extend_from_slice(&arc_pts[1..]); // skip first (already added) - } else { - pts.push([v.x, v.y, v.z]); - } - } - } - - // Rubber-band to cursor. match self.mode { SegMode::Line => { + pts.push([last.x, last.y, last.z]); pts.push([pt.x, pt.y, pt.z]); } SegMode::Arc => { @@ -315,7 +320,7 @@ impl CadCommand for PlineCommand { let tangent = self.last_tangent.unwrap_or(Vec2::new(1.0, 0.0)); let bulge = compute_bulge(a, tangent, b); let arc_pts = arc_sample_points(last, bulge, pt, 16); - pts.extend_from_slice(&arc_pts[1..]); + pts.extend_from_slice(&arc_pts); } }