Skip to content

Crossing

1. Identity

A Crossing is an emergent scene feature emitted automatically when two like-layer primitives (segment / arc / circle / spline) intersect; it materialises a virtual node at the live intersection point so users can snap, dimension and constrain at the meeting point without manually placing it.

2. When to use it

Users never "create" a crossing. They encounter it when:

  • Two construction-layer segments are drafted and cross — a yellow virtual node appears at the X for snapping center-finding / dimensioning.
  • A new active segment is drawn through an existing active segment / arc / circle.
  • An imported / scripted geometry contains intersecting primitives — crossings are emitted on scene load.
  • The user wants to snap a subsequent click (e.g., a node, the start of an arc) to the intersection of two already-placed lines.

3. Inputs

Crossings have no user-facing tool. They are emitted by primitive-creation commands.

  • Tool buttonnot applicable; no tool exists.
  • Keyboard shortcutnot applicable.
  • MotorScript builtinnot applicable. No crossing(...) statement exists; crossings are never reverse-codegen'd (they are derived state).
  • Command Palettenot applicable.
  • Context menunot applicable. Right-clicking a virtual crossing node currently exposes only generic node actions (selection, snap-target inspection).

Emission sites: addSegment (commands.ts:625), addArc (commands.ts:~820), addSpline crossings (commands.ts:1110). Re-evaluation site: reEvaluateCrossings (fillet.ts:1388), invoked from store/helpers.ts:204 per mutation and from store/drag.ts:68 at drag-end.

4. State machine

N/A — Crossings are emitted automatically by the evaluator; no user-facing click sequence creates them. The lifecycle is:

  1. A primitive command (addSegment, addArc, addSpline) computes intersections against existing same-layer primitives.
  2. For each intersection point, a virtual Node is inserted into scene.geometry.nodes and a CrossingFeature record is appended to scene.geometry.crossings (commands.ts:722–740).
  3. The virtual node is decorated with virtualRef: { kind: 'crossing-point', crossingId } (commands.ts:556–567).
  4. On every subsequent scene mutation, reEvaluateCrossings walks each CrossingFeature.sources, re-resolves both primitives' current geometry, re-intersects them, and writes the new virtualPoint (fillet.ts:1388).

There is no preview, no cancel, no finalize.

5. Committed state

After a primitive command that produces a crossing:

  • One new Node in scene.geometry.nodes per intersection, carrying virtualRef: { kind: 'crossing-point', crossingId } (see scene-model.ts Node).
  • One CrossingFeature per intersection (scene-model.ts:457–484):
    • id'xf_...' uid.
    • layer'active' | 'construction', inherited from the emitting primitive's layer (cross-layer crossings are intentionally not recorded).
    • virtualPoint{ x, y }, kept in sync by reEvaluateCrossings.
    • virtualNodeId — id of the promoted Node.
    • sources?: [CrossingSource, CrossingSource][existingPrim, newPrim]. Optional during staged rollout: legacy records without sources are silently skipped by re-evaluation (scene-model.ts:477–483).
  • No constraints emitted. The crossing is not pinned by coincident; the intersection is recomputed parametrically every mutation.
  • No MotorScript generated. Crossings are derived; reverse-codegen omits them entirely.

6. Constraints / interactions

ConstraintBehavior on the crossing's virtual node
coincident / horizontal / vertical / fix / distance / any user constraintRejected by the constraint palette per . The virtual node cannot be a direct constraint operand while its virtualRef.kind === 'crossing-point'. The user must first explicitly promote the crossing (drop its virtualRef → free node) before attaching constraints.
snap targetAccepted. Subsequent tool clicks snap to virtualPoint like any other node; if a tool consumes this snap as an endpoint, ensureNodeWithPromotion reuses the virtual node id and the new primitive shares the crossing point.
fillet inputAllowed; the crossing's segments / arcs can be filleted. The crossing record is invalidated if the contributing primitive is consumed by the fillet trim.
trimTrimming one source primitive deletes the crossing if its source-id disappears (reEvaluateCrossings drops records whose resolvePrim returns null — fillet.ts:1419+).

Layer isolation (commands.ts:639, 681): construction-layer primitives only cross other construction primitives; active-layer primitives only cross active primitives. Drafting a construction centerline through an active part-outline produces no crossing — by design, but a frequent source of user confusion ("why doesn't my centerline snap to the boundary?"). Cross-layer snapping requires explicit promotion (e.g., projecting the construction line into active).

7. Failure modes

  • Parallel sourcesintersectLineLine returns null; no crossing emitted; no toast, no warning. Silent and correct.
  • Sources that don't overlap in their parametric range — the infinite-line intersection lies outside both bounded segments; emission code already filters by t ∈ [0,1] so the crossing is suppressed. If the user later extends one source by dragging an endpoint past the other, the next reEvaluateCrossings pass will detect the now-valid intersection only if a CrossingFeature already exists; brand-new crossings on drag are not retroactively created (gap).
  • One source deletedresolvePrim returns null and reEvaluateCrossings removes the CrossingFeature and freezes the orphaned virtual node via freezeOrphanedVirtuals (drag.ts:17). The node persists as a free point at the last computed position.
  • Spline crossings without analytic intersection — sampled approximation, may miss tangential touches.
  • Sources missing sources? field (legacy records) — re-evaluation skips them; the crossing freezes at its initial virtualPoint and silently drifts as parents move.
  • Cross-layer intersections — intentionally invisible (see §6).

8. Figures

     C
      \
       \
   A----X----------B       X = virtual node, glyph: hollow yellow circle
         \                     virtualRef.kind = 'crossing-point'
          \                    virtualNodeId references CrossingFeature.id
           D

Figure 1 (ASCII): Two construction segments AB and CD crossing at X. The virtual node X is rendered yellow (var(--snap-target), Canvas/index.tsx:3692) to indicate system-owned; promoted virtual nodes (with user constraints attached) recolor.

ABCDX (virtualRef: crossing-point)

Figure 1: Two same-layer segments AB and CD cross at X. The virtual node (amber) carries virtualRef: { kind: 'crossing-point', crossingId } and acts as a snap target.

A'(old A)BCDnode stuck on CD (lost virtualRef)

Figure 2: [] After dragging A's endpoint, the virtual node lost its virtualRef during moveNode and slid as a free node along CD instead of re-intersecting AB' and CD.

A (active)BC (construction)Dno virtual node emitted

Figure 3: Layer isolation — an active segment (solid blue) and a construction segment (dashed) cross visually but no Crossing feature emits — intentional layer isolation (commands.ts:639, 681).

9. Known bugs

10. Class API

In the current model, crossings are class instances rather than plain objects.

  • Class file: app/src/lib/fea2d/model/CrossingFeature.ts
  • Constructor: new CrossingFeature(id: CrossingId, layer: 'active' | 'construction', virtualPoint: { x, y }, virtualNodeId: NodeId, sources: [CrossingSource, CrossingSource] | null) — validates finite virtualPoint, non-empty virtualNodeId. sources === null is permitted for legacy persisted records (pre-source-tracking); phase-7 re-evaluation skips such features.
  • Snapshot type: CrossingFeatureSnapshot (app/src/lib/fea2d/model/snapshots/Crossing.ts).
  • Class layer used: CrossingSource (app/src/lib/fea2d/model/CrossingSource.ts) — the abstract source ref with concrete subclasses for each crossable primitive kind.
  • Lifecycle methods:
    • toJSON: CrossingFeatureSnapshot
    • static fromJSON(snap, scene?): CrossingFeature — rehydrates sources through CrossingSource.fromJSON when present.
    • clone: CrossingFeature
    • equals(other: SceneEntity): boolean
  • Polymorphic surface (SceneFeature):
    • referencesEntity(id: string): boolean — true when id matches the feature id, the virtual node id, or either source's referenced primitive id.
    • isStale(_scene: unknown): boolean — phase-7 implementation compares the stored virtualPoint to a fresh intersection of the two sources. (Stubbed false in phases 3–6.)

id is readonly; layer, virtualPoint, virtualNodeId, sources are mutable so phase-7 recompute can update them in place.

motordevs studio — geometry editor specification