forked from 1b-agent/opencadstudio-upstream
feat(pline): create polyline as a live entity so it snaps while drawing
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 <[email protected]>
This commit is contained in:
1 parent
cdd72c62ca
commit
188d310789
3 files changed
+115
-40
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Vec2>,
|
||||
/// 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<Handle>,
|
||||
}
|
||||
|
||||
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<WireModel> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user