Appearance
Trim
1. Identity
A trim removes the portion of a segment that lies between the two intersection nodes flanking a click point; the segment is either shortened, split into two surviving sub-segments, or fully removed (Solvespace-parity, v1 segment-on-segment only).
2. When to use it
- Clean up a draft polyline that overshoots a boundary segment.
- Punch a window through a closed profile by drawing a crossing segment and trimming the interior portion.
- Convert a long reference line into two stub references by removing the middle.
- Discard one side of a bisecting cut after a
crossing(...)has materialised the intersection node.
3. Inputs
- Tool button: Modify toolbar, "Trim" slot.
- Keyboard shortcut: not yet wired (no
k === 't'branch in theCanvas/index.tsxshortcut dispatch around line 2716; tool must be picked from the toolbar). - MotorScript builtin: not yet wired — trim is a canvas-only operation; reverse-codegen does not emit a
trim(...)statement. - Command Palette: "Modify: Trim segment".
- Context menu: not yet wired — trim is exclusively click-driven on canvas.
Activating the tool with an empty scene is harmless; the first click that lands on a trimmable segment commits.
4. State machine
The dispatch lives in Canvas/index.tsx:1668-1700 (active-tool branch). All algorithmic work happens in planTrimAction (app/src/lib/fea2d/tools/trim-tool.ts:157-311), which is a pure plan-builder consumed by the trimAtCursor store action.
Click 1 / Pick segment portion: click anywhere within
hitRadiusof a real (non-construction) segment. The click position is projected onto the segment to obtain a parametert ∈ [0,1]. The nearest hit wins (seetrim-tool.ts:217-236).- Preview after step 1: none today — the canvas has no hover preview wired. Cursor is the default tool cursor. (Gap: a hint-overlay highlight of the to-be-removed sub-segment is reserved for a later lane.)
- Cancel options: switch to another tool, or press Esc; nothing has been mutated until the click commits.
Finalize (auto on click):
planTrimActiongathers every other segment's intersectionton the target (gatherIntersectionParams,trim-tool.ts:88-134), brackets the clicktbetween the largestt_lo < click_tand smallestt_hi > click_t, and returns a plan: the trimmed segment id plus zero, one, or two replacement sub-segments expressed as endpoint coordinates. The store action removes the source segment and inserts the replacements as a single history entry. The status string (Trimmed — N sub-segment(s) kept,Segment removed,Nothing to trim, orTrim v1: arc/circle crossings not yet supported) is surfaced throughsurfaceErroratCanvas/index.tsx:1678-1697.
5. Committed state
After finalize, the scene mutation is:
- The target
Segment(id fromTrimResult.trimmedSegmentId) is removed fromscene.geometry.segments. - 0–2 fresh
Segmentrecords are appended, each with new ids and endpoints at the bracket intersections / original endpoints. Endpoint nodes for these sub-segments are materialised by the store action if no existing node lies at the intersection coordinate. - No constraints are emitted by trim. Constraints that referenced the deleted segment (or its endpoints, if those become orphan) cascade via the same constraint-pruning rules as Delete section 5.
- No MotorScript statement is generated — trim is canvas-only.
6. Constraints / interactions
Trim consumes geometry but does not produce constraints. It does respect two layer rules:
| Primitive at click | Behavior |
|---|---|
| real segment, has flanking intersections | trim normally; produces 0–2 sub-segments |
| real segment, no intersections | no-op; status Nothing to trim |
seg.isConstruction === true | skipped during nearest-segment search (trim-tool.ts:218); the click falls through to the next-nearest real segment, or to no-op |
| arc / circle near click | hint-only: status Trim v1: arc/circle crossings not yet supported (no mutation) |
Cross-refs:
- Segment — endpoint nodes of replacement sub-segments inherit
Segmentinvariants. - Crossing — a
Crossing.virtualNodedoes not bracket a trim; trim only consults real segment-segment intersections (intersectLineLine), so a crossing-virtual node at the clicktneither flanks nor terminates a trim sub-segment. - Delete — the cascade rules (constraint pruning, orphan-node sweep) applied by the trim store action mirror the segment-delete path.
7. Failure modes
- Click in empty space:
bestSegId === null, returns{ changed: false, status: 'Nothing to trim' }. Surfaced as an info toast viasurfaceError(Canvas/index.tsx:1691-1697). - Click on segment with no crossings: both
lowerTandupperTare null; returns no-op with statusNothing to trim(trim-tool.ts:257-266). The whole segment is not deleted in this branch — Solvespace-parity requires a flanking intersection to define a removable portion. - Click on construction segment: skipped (
seg.isConstructioncontinues attrim-tool.ts:218). The trim falls through to any real segment withinhitRadius, otherwise no-op. - Click near only one intersection (e.g. beyond the last
ton the segment): one side of the bracket is null; one surviving sub-segment is emitted, the "trimmed portion" runs from the lone intersection to the original endpoint. Status:Trimmed — 1 sub-segment kept. - NaN or non-finite click: guarded at
trim-tool.ts:163-173; returns no-op with statusNothing to trim. - Arc / circle proximity: detected (
trim-tool.ts:182-206) and surfaces the v1-unsupported status; the underlying arc/circle is untouched.
The contract: trim never partially commits. Either the plan is empty (no-op + status string) or the store applies the full plan atomically inside withHistory.
8. Figures
before after click in the middle portion
of segment AB (between X1 and X2)
•C •C
│ │
A•────X1────●────X2────•B A•────X1 X2────•B
click (gap — middle portion removed)
│ │
•D •DASCII: Segment AB crosses CD at X1 and another segment EF at X2. Clicking AB between X1 and X2 removes the X1–X2 portion; the surviving sub-segments are A–X1 and X2–B. X1 and X2 are materialised as real nodes if not already present.
Figure 2 (before): three segments crossing at X1 and X2; cursor hovers in the middle of the horizontal segment between the two intersections.
Figure 3 (after): the X1–X2 portion is gone; the two surviving sub-segments and the crossing segments are unchanged.
9. Known bugs
None known.
10. Class API
In the current model, the trim-tool pipeline still lives in app/src/lib/fea2d/tools/trim-tool.ts and operates through Scene.removeSegment / addSegment — the class layer participates passively. The relevant class-API surface:
- **
Scene.segmentsByEndpoints / intersectsWith** — derived caches lazily memoised by revision counter feed the "find crossings flanking this click" lookup the trim tool runs. The caches invalidate automatically on the next mutation viaScene.bumpRevision. Segment.length(scene) / direction(scene) / unitDirection(scene)(app/src/lib/fea2d/model/Segment.ts) — used by the trim tool to compute t-parameters of the flanking intersection points when deciding which sub-segments to emit.
The arc-on-segment / arc-on-arc / circle-* trim cases remain deferred per the existing §3 / §10 caveats;