feat(trim): TRIM/EXTEND options — edges, fence, crossing, edge, erase (#336)
Both commands gain a shared option mode machine:
- Cutting/Boundary edges (T/B): pick which objects act as the boundary
(picked edges highlight yellow, the hovered candidate half-strength);
Enter rebuilds the boundary set, empty = everything (quick mode).
- Fence (F): draw a dashed fence; every object crossing it is trimmed /
extended at each crossing, re-applied to the surviving pieces so a
fence across both overhanging ends clears both. Live red/cyan result
preview while placing points.
- Crossing (C): crossing-selection semantics — the dashed green window
picks objects crossing its edges AND wholly inside; runs through the
fence machinery with a synthesized inside click.
- Edge (E): boundaries extrapolate (lines run past their ends, arcs
close to full circles). Cuts that land on an implied extension draw a
dashed yellow guide from the boundary's drawn end to the cut point,
so the user sees which edge causes the cut. Extend previews also show
the added tail as a bright dashed slice of the true curve.
- Erase (R, TRIM): picks delete without needing an intersection, with a
red hover preview.
- Shift+click swaps trim<->extend per pick (new set_shift plumbing into
the entity-pick and hover paths); the hover preview follows the held
state. Project is out of scope for the 2D pipeline.
The per-type pick bodies moved to shared free fns (pick_trim_at /
pick_extend_at) used by direct picks, hover previews and the fence
pass; EXTEND's snapshot bookkeeping unified with TRIM's placeholder
flow. Option keywords now take precedence over the hex-handle reading
in feed_active_cmd ("F"/"C"/"E" are valid hex), and fence geos carry
sentinel handles — probing with NULL against NULL-handle geos excluded
the whole fence. Regression test: fence_trims_x_arm.
Closes #336
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5f08b77ebc
commit
9b40881ab1
4 changed files with 1393 additions and 221 deletions
|
|
@ -20,11 +20,13 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let ctrl = self.ctrl_down;
|
let ctrl = self.ctrl_down;
|
||||||
|
let shift = self.shift_down;
|
||||||
let result: Option<CmdResult> = {
|
let result: Option<CmdResult> = {
|
||||||
let Some(cmd) = self.tabs[i].active_cmd.as_mut() else {
|
let Some(cmd) = self.tabs[i].active_cmd.as_mut() else {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
cmd.set_ctrl(ctrl);
|
cmd.set_ctrl(ctrl);
|
||||||
|
cmd.set_shift(shift);
|
||||||
match input {
|
match input {
|
||||||
StepInput::Point(p) => Some(cmd.on_point(p)),
|
StepInput::Point(p) => Some(cmd.on_point(p)),
|
||||||
StepInput::Text(s) => cmd.on_text_input(&s),
|
StepInput::Text(s) => cmd.on_text_input(&s),
|
||||||
|
|
@ -176,6 +178,18 @@ impl OpenCADStudio {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|c| c.needs_entity_pick())
|
.is_some_and(|c| c.needs_entity_pick())
|
||||||
{
|
{
|
||||||
|
// Option keywords take precedence over the handle reading —
|
||||||
|
// "F"/"C"/"E" are valid hex, but during TRIM/EXTEND they are the
|
||||||
|
// Fence/Crossing/Edge options (#336). Only an unconsumed token
|
||||||
|
// falls through to the handle interpretation.
|
||||||
|
let consumed = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|c| c.on_text_input(token));
|
||||||
|
if let Some(r) = consumed {
|
||||||
|
let _ = self.apply_cmd_result(r);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Ok(v) = u64::from_str_radix(token.trim_start_matches("0x"), 16) {
|
if let Ok(v) = u64::from_str_radix(token.trim_start_matches("0x"), 16) {
|
||||||
let handle = Handle::new(v);
|
let handle = Handle::new(v);
|
||||||
let pt = self.tabs[i]
|
let pt = self.tabs[i]
|
||||||
|
|
|
||||||
|
|
@ -1190,10 +1190,16 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
||||||
scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds, self.tabs[i].scene.document.header.lineweight_display)
|
scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds, self.tabs[i].scene.document.header.lineweight_display)
|
||||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||||
.unwrap_or(acadrust::Handle::NULL);
|
.unwrap_or(acadrust::Handle::NULL);
|
||||||
|
let shift = self.shift_down;
|
||||||
let mut p = self.tabs[i]
|
let mut p = self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.map(|c| c.on_hover_entity(hover_handle, effective_wcs))
|
.map(|c| {
|
||||||
|
// Live shift state drives TRIM/EXTEND's
|
||||||
|
// shift-swap preview (#336).
|
||||||
|
c.set_shift(shift);
|
||||||
|
c.on_hover_entity(hover_handle, effective_wcs)
|
||||||
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
// on_hover_entity returns WCS wires; shift to the
|
// on_hover_entity returns WCS wires; shift to the
|
||||||
// offset-relative frame so the preview lands on the
|
// offset-relative frame so the preview lands on the
|
||||||
|
|
@ -2073,10 +2079,15 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let shift = self.shift_down;
|
||||||
let result = self.tabs[i]
|
let result = self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.map(|c| c.on_entity_pick(handle, pick_wcs));
|
.map(|c| {
|
||||||
|
// Shift-swap state for TRIM/EXTEND (#336).
|
||||||
|
c.set_shift(shift);
|
||||||
|
c.on_entity_pick(handle, pick_wcs)
|
||||||
|
});
|
||||||
// HATCHEDIT: after pick, inject hatch model data into the command.
|
// HATCHEDIT: after pick, inject hatch model data into the command.
|
||||||
if self.tabs[i]
|
if self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
|
|
|
||||||
|
|
@ -1213,6 +1213,11 @@ pub trait CadCommand: Send {
|
||||||
/// `ARC_CONT`) store it; most commands ignore it. Default no-op.
|
/// `ARC_CONT`) store it; most commands ignore it. Default no-op.
|
||||||
fn set_ctrl(&mut self, _ctrl: bool) {}
|
fn set_ctrl(&mut self, _ctrl: bool) {}
|
||||||
|
|
||||||
|
/// Push the live Shift-key state into the command before each dispatch.
|
||||||
|
/// TRIM/EXTEND use it for the shift-select swap (Shift+click extends
|
||||||
|
/// during TRIM and trims during EXTEND, #336). Default no-op.
|
||||||
|
fn set_shift(&mut self, _shift: bool) {}
|
||||||
|
|
||||||
/// Called when the user left-clicks in the viewport (point pick).
|
/// Called when the user left-clicks in the viewport (point pick).
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult;
|
fn on_point(&mut self, pt: DVec3) -> CmdResult;
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue