Skip to content

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 the Canvas/index.tsx shortcut 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.

  1. Click 1 / Pick segment portion: click anywhere within hitRadius of a real (non-construction) segment. The click position is projected onto the segment to obtain a parameter t ∈ [0,1]. The nearest hit wins (see trim-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.
  2. Finalize (auto on click): planTrimAction gathers every other segment's intersection t on the target (gatherIntersectionParams, trim-tool.ts:88-134), brackets the click t between the largest t_lo < click_t and smallest t_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, or Trim v1: arc/circle crossings not yet supported) is surfaced through surfaceError at Canvas/index.tsx:1678-1697.

5. Committed state

After finalize, the scene mutation is:

  • The target Segment (id from TrimResult.trimmedSegmentId) is removed from scene.geometry.segments.
  • 0–2 fresh Segment records 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 clickBehavior
real segment, has flanking intersectionstrim normally; produces 0–2 sub-segments
real segment, no intersectionsno-op; status Nothing to trim
seg.isConstruction === trueskipped 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 clickhint-only: status Trim v1: arc/circle crossings not yet supported (no mutation)

Cross-refs:

  • Segment — endpoint nodes of replacement sub-segments inherit Segment invariants.
  • Crossing — a Crossing.virtualNode does not bracket a trim; trim only consults real segment-segment intersections (intersectLineLine), so a crossing-virtual node at the click t neither 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 via surfaceError (Canvas/index.tsx:1691-1697).
  • Click on segment with no crossings: both lowerT and upperT are null; returns no-op with status Nothing 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.isConstruction continues at trim-tool.ts:218). The trim falls through to any real segment within hitRadius, otherwise no-op.
  • Click near only one intersection (e.g. beyond the last t on 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 status Nothing 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                                    •D

ASCII: 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.

X1X2click

Figure 2 (before): three segments crossing at X1 and X2; cursor hovers in the middle of the horizontal segment between the two intersections.

(removed)

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 via Scene.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;

motordevs studio — geometry editor specification