Skip to content

Dimensional constraints

Six kinds, all sharing the Constraint record at constraint-schema.ts:111-153: distance, distanceH, distanceV, angle, radius, diameter. Each carries a driving numeric value (mm or degrees) and optionally a valVarRef pointing at a Variable.id in scene.geometry.variables. The solver reads valVarRef ? variable.value : value at solve time — the field is only meaningful for dimensional kinds; the schema explicitly ignores it for geometric kinds (constraint-schema.ts:128).

The 18 MotorScript builtins (12 geometric + these 6) are minted by createConstraints(ctx) in executor/builtins/constraints.ts:28. The TS-fallback solver implements all six in ts-solver.ts (per-kind file:line cited below); the WASM solver maps them onto Solvespace native constraints in slvs-translate.ts.

Smart dimensions

A dimensional constraint with valVarRef: 'var_NNN' set is driven by a variable rather than a literal. The Properties drop-down shows the variable name plus a lock icon (ConstraintRow.tsx:200-217); the constraint badge shows the variable name in parens (ConstraintBadge.tsx:317-333). Cross-ref params + smart dim.

The smart-dim canvas tool auto-emits one of these six kinds together with a valVarRef binding when the user picks a value off the param table instead of typing a literal: the emitted Constraint carries the chosen Variable.id in valVarRef and a snapshot of the variable's current numeric value in value (the snapshot keeps script reads / round-trips well-defined even when the variable is later renamed; the solver still reads the live variable). Re-pointing valVarRef to null reverts to literal-driven (ConstraintRow.tsx:189-195 — "Clear binding").

1. Identity

A dimensional constraint is a Constraint record whose kind is one of distance, distanceH, distanceV, angle, radius, diameter and whose value (or valVarRef-driven variable value) defines the magnitude the solver must drive the referenced entities to.

2. When to use it

  • Locking a side length, gap, or fastener pitch on a stator/rotor profile (distance, distanceH, distanceV).
  • Pinning the included angle between two segments — typical for pole-arc opening and slot-wall splay (angle).
  • Sizing a bearing seat, shaft bore, slot bottom, or pole-tip fillet (radius, diameter).
  • Binding any of the above to a param(...) so the geometry resolves with the parameter slider (smart-dim, see §Smart dimensions above).

3. Inputs

  • Tool button: Dimension toolbar slot (a single button that picks the kind from the current selection — node+node for distance variants, segment+segment for angle, arc/circle for radius/diameter). The H/V projected variants are reached either by toggling axis-lock in the dimension tool or via the Cmd+K palette entries below.
  • Keyboard shortcut: D activates the smart-dim tool; the kind is inferred from selection at finalize (cross-check Canvas/index.tsx tool dispatch around line 2716).
  • MotorScript builtins:
    • distance(p1, p2, val)constraints.ts:215.
    • distanceH(p1, p2, val)constraints.ts:234.
    • distanceV(p1, p2, val)constraints.ts:248.
    • angle(line1, line2, deg)constraints.ts:261.
    • radius(arcOrCircle, val)constraints.ts:278.
    • diameter(circle, val)constraints.ts:291 (note: builtin signature accepts a CircleRef only; the solver itself handles an arc-id entity for diameter, so a script-injected diameter on an arc id still resolves — see §Surprises).
  • Command Palette (Cmd+K): "Distance", "Distance — horizontal", "Distance — vertical", "Angle between two lines", "Radius", "Diameter".
  • Context menu: right-click a node-pair or arc/circle → matching dimensional kind pre-populated.

4. State machine (smart-dim canvas flow)

  1. Click 1: pick first entity (node, segment, or arc/circle). Preview after step 1: highlight on the picked entity; status bar shows "select second entity OR Enter to finalize for single-entity dim".
  2. Click 2 (skipped for radius/diameter): pick second entity of compatible kind. Kind inferred:
    • 2 nodes → distance (smart-dim defaults to plain distance; H/V variants need explicit toggle).
    • 2 segments → angle.
  3. Value entry: floating numeric editor opens at the click location. The user types a literal or clicks the fx button to pick a variable from the param table — that path sets valVarRef and snapshots value (see §Smart dimensions).
  4. Finalize (Enter): the constraint is committed; solver re-runs. Preview during finalize: ghosted constraint badge at the projected anchor point.

Cancel: Esc clears the click buffer; right-click aborts; switching tools discards.

5. Committed state

A single new Constraint is appended to scene.constraints. Fields per kind:

KindentityAentityBentityCentityDvalue
distancen0n1mm
distanceHn0n1mm (signed: x1 - x0)
distanceVn0n1mm (signed: y1 - y0)
anglen0An1An0Bn1Bdeg
radiusarcId or circIdmm
diametercircId (or arcId, see §Surprises)mm

Encoding table mirrored in the schema doc-comment (constraint-schema.ts:91-109). Reverse codegen for each lives in codegen.ts and emits one line per constraint — distance(p1, p2, 12.5) or, with a bound variable, distance(p1, p2, slotWidth) (the variable identifier resolves at execution).

6. Per-kind detail

6a. distance

p1p220 mm

Figure: distance constraint — point-to-point Euclidean distance with dimension annotation.

  • Signature: distance(p1: PointRef, p2: PointRef, val: number)constraints.ts:215. Rejects non-finite, sub-DIST_EPS, and self-distance.
  • TS solver: ts-solver.ts:305-323. Residual f = sqrt((x0-x1)² + (y0-y1)²) − value. The unit-normalised form is used (rather than d² − v²) to avoid quadratic Newton overshoot at large initial gaps.
  • WASM: slvs-translate.ts:708-713C_PT_PT_DISTANCE.
  • valVarRef behavior: at solve time, resolveValue returns variable.value if valVarRef is set, else value. Solver sees a single scalar; no schema difference between literal and driven. Editing the variable triggers a re-solve and the geometry updates live. Cross-ref params + smart dim.
  • Properties drop-down: bound case at ConstraintRow.tsx:205-217 (lock + name); literal case at ConstraintRow.tsx:218+ (numeric input).
  • Badge: glyph + driven marker at ConstraintBadge.tsx:317-333; when valVarRef is set, (varName) is rendered beneath the glyph.
  • Auto-emit: the smart-dim tool emits this kind when the user selects two nodes without forcing an axis lock.

6b. distanceH

p1p215 mm

Figure: distanceH constraint — signed horizontal projection between two nodes.

  • Signature: distanceH(p1, p2, val)constraints.ts:234. Same guard class as distance (positive, finite, distinct).
  • TS solver: ts-solver.ts:326-337. Residual f = (x1 - x0) − value, gradient [-1, +1] on the two x-DOFs only. Linear, converges in one Newton step. The value is signed: putting p2 to the left of p1 and asking for value > 0 will pull p2 rightward.
  • WASM: slvs-translate.ts:869-911C_PROJ_PT_DISTANCE against an auxiliary horizontal reference line that is allocated once per sketch and pinned via slvs.dragged (__axisH, __axisHTip, __axisOrigin keys in entityCache).
  • valVarRef behavior: identical resolve path as distance. The variable value is the signed projected distance.
  • Properties drop-down / Badge: shared dimensional UI (ConstraintRow.tsx:200-217, ConstraintBadge.tsx:317-333).
  • Auto-emit: smart-dim tool with horizontal axis-lock; also auto-emitted by the rect / polygon primitives when the user finalises with axis-aligned sides.

6c. distanceV

p1p212 mm

Figure: distanceV constraint — signed vertical projection between two nodes.

  • Signature: distanceV(p1, p2, val)constraints.ts:248. Same guard class.
  • TS solver: ts-solver.ts:340-351. Residual f = (y1 - y0) − value, gradient [-1, +1] on the two y-DOFs. Linear, one-step.
  • WASM: slvs-translate.ts:869-911C_PROJ_PT_DISTANCE against a vertical reference line (__axisV / __axisVTip). Same entityCache allocation trick as distanceH; the auxiliary axes are shared across every projected-distance constraint in the sketch.
  • valVarRef behavior: identical to distance / distanceH. Signed.
  • Properties drop-down / Badge: shared dimensional UI.
  • Auto-emit: smart-dim tool with vertical axis-lock; rect/polygon vertical sides.

6d. angle

45°

Figure: angle constraint — included angle between two segments sharing a vertex.

  • Signature: angle(line1: LineRef, line2: LineRef, deg: number)constraints.ts:261. Rejects non-finite; negative degrees are allowed (CW vs CCW).
  • TS solver: ts-solver.ts:632-720. Two-equation formulation: f1 = cross − sin(θ)·lenA·lenB, f2 = dot − cos(θ)·lenA·lenB. The sin + cos pair eliminates the degenerate Jacobian that a single sin equation produces at θ = 0° and θ = 180°. Worked-out partials for ∂f1/∂x0a etc. live in the comment block — finite-diff verified by tests/unit/lib/fea2d/solver/jacobian-finite-diff.test.ts.
  • WASM: slvs-translate.ts:725-748C_ANGLE over two Line2D entities. The constraint stores endpoint node ids, and findSegmentByEndpoints resolves the matching Line2D entity at translate time; if no segment exists for (n0A, n1A) or (n0B, n1B) the constraint is dropped with a warning (lines 733-744).
  • valVarRef behavior: same resolve path; the variable's value is interpreted as degrees. A param('coilSpan', 30, ...) in degrees can be bound directly.
  • Properties drop-down / Badge: shared dimensional UI; the badge glyph is the arc-with-vertex angle icon.
  • Auto-emit: smart-dim with two-segment selection. Polygon primitive auto-emits angle constraints internally when constructed with a regular-polygon flag.

6e. radius

R=10

Figure: radius constraint — single radius arrow from center to circumference.

  • Signature: radius(arcOrCircle: ArcRef | CircleRef, val: number)constraints.ts:278. Rejects non-positive and non-finite values.
  • TS solver: ts-solver.ts:357-364. Direct assignment: radius is not a node DOF, so Newton cannot drive it. The solver writes value directly into the working circle / arc record and contributes a trivially satisfied 0 = 0 residual row. Same strategy as diameter.
  • WASM: slvs-translate.ts:714-719C_DIAMETER with valA = 2 * radius. Solvespace has no native radius constraint; both radius and diameter route through C_DIAMETER.
  • valVarRef behavior: standard resolve. Both arc and circle entityA paths read the same scalar; a param('boreR', 25, ...) swept via the slider rebuilds the host circle/arc geometry without recomputing the rest of the scene.
  • Properties drop-down / Badge: shared dimensional UI. The badge glyph is the R<value> text variant.
  • Auto-emit: smart-dim with a single arc or circle selection (no second-click step). arcCenterEnds mode of the arc tool auto-emits onCircle (see arc spec); it does not auto-emit a radius — the radius is fixed via the construction click, not as a driving constraint.

6f. diameter

D=20

Figure: diameter constraint — diameter chord through center.

  • Signature: diameter(circle: CircleRef, val: number)constraints.ts:291. Builtin accepts a CircleRef only; rejects non-positive / non-finite. See §Source-code surprises for the arc-id case.
  • TS solver: ts-solver.ts:790-799. Direct assignment with r = value / 2. The TS path tries the circle table first, then the arc table — i.e. a diameter constraint whose entityA is an arc id is honoured at solve time (covers diameter-on-arc after a circle has been decomposed by a crossing).
  • WASM: slvs-translate.ts:720-724C_DIAMETER with valA = diameter (no *2 adjustment, in contrast to radius).
  • valVarRef behavior: standard resolve. Editing the bound variable updates value; the TS path divides by 2 each solve, the WASM path passes the literal.
  • Properties drop-down / Badge: shared dimensional UI. The badge glyph is the ?<value> text variant.
  • Auto-emit: smart-dim with a single circle selection if the user toggles "diameter" rather than the default "radius" in the dimension-tool sub-mode picker.

7. Failure modes

  • Sub-DIST_EPS / non-finite value (distance, distanceH, distanceV): rejected at the builtin boundary with a script-line error. No constraint added.
  • Self-dim (a.id === b.id): rejected at boundary — distance from a point to itself is 0 and a non-zero target is unsatisfiable.
  • Non-positive radius / diameter: rejected at boundary.
  • Non-finite angle: rejected at boundary; negative angles are valid and pass through.
  • Zero-length segment in angle: lenA² < 1e-18 or lenB² < 1e-18failedIds.add(c.id), residual row skipped (ts-solver.ts:641). The constraint surfaces as a red pill in the Properties drop-down via solverStatus.failedConstraintIds.
  • Segment not in entityCache in angle WASM: constraint dropped, unsupported.push(...), console.warn (slvs-translate.ts:733-744). Symptom: the angle pill stays orange / grey instead of solving.
  • Radius / diameter on an arc decomposed from a circle: TS path resolves to the arc's working record. WASM path resolves through entityCache — if the constraint references the no-longer-present circle id, the tangent fallback comment at slvs-translate.ts:759-787 describes the lookup; analogous coverage is not in place for radius / diameter, so a script that radius-constrains a decomposed circle may surface an "entityC not found" warning.
  • Driven by missing variable (valVarRef points at a deleted Variable): the resolver falls back to value (the last snapshot). The Properties drop-down shows the lock icon greyed; the next save round-trip clears the dangling ref.
  • Mismatched solver path: TS path and WASM path both enforce all six kinds.

All boundary-rejected cases throw a script error with a 1-based line number; the Properties drop-down renders the script error in the code panel rather than committing a half-state constraint.

8. Figures

   p1•───────── d = 12.5 ─────────•p2          (distance)

   p1•                                          (distanceH, signed)

        ╲      dx = 8.0
         •p2 ──→ x

   ╱─────── θ = 30° ───────╲                    (angle, two segments)
  ╱                         ╲
 •n0A         •vertex          •n0B

      ⌒                                          (radius / diameter
     ╱  R=5                                       on circle or arc)

     ╲__⌒

distance literal + driven side-by-sideFigure 1: Two distance constraints — left is literal 12.5, right is bound to a variable slotPitch; the right shows the lock icon in the Properties drop-down and (slotPitch) under the badge. Capture conditions: a horizontal segment with a literal-driven distance, a second segment with a smart-dim binding.

distanceH vs distanceV signed valuesFigure 2: distanceH shown alongside the cartesian basis — point arranged so the signed projection equals the displayed value; verify by flipping p1/p2 selection and observing the sign flip. Capture conditions: two nodes at (0,0) and (10,4), distanceH then distanceV applied.

angle two-segment with arc badgeFigure 3: angle(seg1, seg2, 30°) between two segments sharing a vertex; badge shows the arc glyph. Capture conditions: segments (0,0)→(10,0) and (0,0)→cos(30°)·10, sin(30°)·10.

radius vs diameter on a decomposed arcFigure 4: diameter constraint on an arc that was decomposed from a circle by a crossing segment — verify the TS-path direct assignment lands and the badge renders ?20. Capture conditions: circle of radius 10 at origin, horizontal segment crossing it (triggers decomposition), then diameter constraint on one of the resulting arcs.

9. Known bugs

  • TS path: tangent(arc, arc) is failure-only because arc centres are not DOFs in the TS solver (ts-solver.ts:508-527). Use the WASM path for full enforcement. This does not affect radius / diameter (direct assignment).

Source-code surprises

  • diameter builtin is circle-only, but the solver and WASM honor an arc id. constraints.ts:291-300 rejects non-CircleRef, yet ts-solver.ts:796-797 and slvs-translate.ts:720-724 both treat entityA opaquely and look it up in either table. A constraint synthesized outside the executor (UI-driven smart-dim, JSON load, reverse-codegen of a decomposed circle) can carry an arc id and will solve correctly. The script-author surface is the stricter one.
  • radius and diameter are both C_DIAMETER in Solvespace — the only difference is the valA * 2 multiplier for the radius case (slvs-translate.ts:717 vs :722). The TS path mirrors this with value vs value / 2.
  • distanceH/distanceV allocate a singleton auxiliary axis per sketch in the WASM path (slvs-translate.ts:884-905) via sentinel entityCache keys __axisH, __axisV, __axisOrigin, __axisHTip, __axisVTip. The origin and tips are pinned with slvs.dragged so they cannot drift; without this, every projected-distance constraint would leak its own reference line.
  • angle Jacobian uses two equations (sin AND cos) rather than one. The single-equation form is degenerate at 0° and 180° (∂sin/∂θ = cos = ±1 but ∂lenA·lenB·sin/∂xi collapses); the dot equation supplies the missing rank.
  • valVarRef is schema-level, not kind-level. The Constraint record always has the field; for geometric kinds it is ignored (constraint-schema.ts:128). Properties drop-down / Badge code therefore checks boundVar truthiness, not constraint kind, before rendering the lock icon — kind-specific gating happens implicitly because geometric constraints have no value-input slot to swap out.

Class API

Every dimensional constraint is an instance of a concrete subclass of the abstract Constraint base. The base and dispatch surface are described in detail under Geometric / Class API; this section calls out what's specific to the 6 dimensional kinds.

  • Concrete subclasses (6 dimensional) in app/src/lib/fea2d/model/constraints/:
    • DistanceConstraint.ts, DistanceHConstraint.ts, DistanceVConstraint.ts, AngleConstraint.ts, RadiusConstraint.ts, DiameterConstraint.ts.
  • Constructor (typical): new DistanceConstraint(id, entityA: NodeId, entityB: NodeId, value: number, valVarRef = null, labelOffset = null) — each subclass narrows the entity types and exposes the scalar value field directly on the instance.
  • Snapshot type: ConstraintSnapshot (shared with geometric; the kind discriminator + value field distinguish them).
  • What isDimensional returns: true for all 6 kinds — the dim-annotation builder uses this single virtual call instead of a string-prefix check on kind.
  • Smart-dim wiring through valVarRef: the base's resolveValue(scene) reads the referenced Variable.value when valVarRef is set and the variable exists; otherwise it falls back to the literal value field on the concrete subclass. This is the single seam the solver reads at solve time — it replaces the previous inline c.valVarRef ? scene.variables.find(...).value : c.value ternary scattered across solver / inspector / dim-annotation code.
  • toSlvs(ctx) — dimensional kinds emit one Slvs constraint (e.g. C_PT_PT_DISTANCE for DistanceConstraint) plus, in the DistanceH / DistanceV case, the auxiliary axis handles resolved via ctx.handleFor('__axisH') etc. The auxiliary-axis allocation lives in slvs-translate.ts as before; the per-subclass toSlvs consumes the resolved handles.

The math / algorithm content of §6 is unchanged by the class-based dispatch — every formula previously inline in a case 'distance': arm lives in DistanceConstraint.evaluate(scene) and behaves identically.

motordevs studio — geometry editor specification