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:
Hakan Seven 2026-07-22 14:53:50 +03:00
commit 9b40881ab1
4 changed files with 1393 additions and 221 deletions

View file

@ -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)
.and_then(|s| Scene::handle_from_wire_name(s))
.unwrap_or(acadrust::Handle::NULL);
let shift = self.shift_down;
let mut p = self.tabs[i]
.active_cmd
.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();
// on_hover_entity returns WCS wires; shift to 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]
.active_cmd
.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.
if self.tabs[i]
.active_cmd