Appearance
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:
Dactivates the smart-dim tool; the kind is inferred from selection at finalize (cross-checkCanvas/index.tsxtool 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 aCircleRefonly; the solver itself handles an arc-id entity fordiameter, 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)
- 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".
- Click 2 (skipped for
radius/diameter): pick second entity of compatible kind. Kind inferred:- 2 nodes →
distance(smart-dim defaults to plaindistance; H/V variants need explicit toggle). - 2 segments →
angle.
- 2 nodes →
- Value entry: floating numeric editor opens at the click location. The user types a literal or clicks the
fxbutton to pick a variable from the param table — that path setsvalVarRefand snapshotsvalue(see §Smart dimensions). - 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:
| Kind | entityA | entityB | entityC | entityD | value |
|---|---|---|---|---|---|
| distance | n0 | n1 | — | — | mm |
| distanceH | n0 | n1 | — | — | mm (signed: x1 - x0) |
| distanceV | n0 | n1 | — | — | mm (signed: y1 - y0) |
| angle | n0A | n1A | n0B | n1B | deg |
| radius | arcId or circId | — | — | — | mm |
| diameter | circId (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
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. Residualf = sqrt((x0-x1)² + (y0-y1)²) − value. The unit-normalised form is used (rather thand² − v²) to avoid quadratic Newton overshoot at large initial gaps. - WASM:
slvs-translate.ts:708-713—C_PT_PT_DISTANCE. - valVarRef behavior: at solve time,
resolveValuereturnsvariable.valueifvalVarRefis set, elsevalue. 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 atConstraintRow.tsx:218+(numeric input). - Badge: glyph + driven marker at
ConstraintBadge.tsx:317-333; whenvalVarRefis 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
Figure: distanceH constraint — signed horizontal projection between two nodes.
- Signature:
distanceH(p1, p2, val)—constraints.ts:234. Same guard class asdistance(positive, finite, distinct). - TS solver:
ts-solver.ts:326-337. Residualf = (x1 - x0) − value, gradient[-1, +1]on the two x-DOFs only. Linear, converges in one Newton step. The value is signed: puttingp2to the left ofp1and asking forvalue > 0will pullp2rightward. - WASM:
slvs-translate.ts:869-911—C_PROJ_PT_DISTANCEagainst an auxiliary horizontal reference line that is allocated once per sketch and pinned viaslvs.dragged(__axisH,__axisHTip,__axisOriginkeys inentityCache). - 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/polygonprimitives when the user finalises with axis-aligned sides.
6c. distanceV
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. Residualf = (y1 - y0) − value, gradient[-1, +1]on the two y-DOFs. Linear, one-step. - WASM:
slvs-translate.ts:869-911—C_PROJ_PT_DISTANCEagainst a vertical reference line (__axisV/__axisVTip). SameentityCacheallocation trick asdistanceH; 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
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/∂x0aetc. live in the comment block — finite-diff verified bytests/unit/lib/fea2d/solver/jacobian-finite-diff.test.ts. - WASM:
slvs-translate.ts:725-748—C_ANGLEover twoLine2Dentities. The constraint stores endpoint node ids, andfindSegmentByEndpointsresolves the matchingLine2Dentity 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
angleconstraints internally when constructed with a regular-polygon flag.
6e. radius
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 writesvaluedirectly into the working circle / arc record and contributes a trivially satisfied0 = 0residual row. Same strategy asdiameter. - WASM:
slvs-translate.ts:714-719—C_DIAMETERwithvalA = 2 * radius. Solvespace has no native radius constraint; bothradiusanddiameterroute throughC_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).
arcCenterEndsmode of the arc tool auto-emitsonCircle(see arc spec); it does not auto-emit aradius— the radius is fixed via the construction click, not as a driving constraint.
6f. diameter
Figure: diameter constraint — diameter chord through center.
- Signature:
diameter(circle: CircleRef, val: number)—constraints.ts:291. Builtin accepts aCircleRefonly; rejects non-positive / non-finite. See §Source-code surprises for the arc-id case. - TS solver:
ts-solver.ts:790-799. Direct assignment withr = value / 2. The TS path tries the circle table first, then the arc table — i.e. a diameter constraint whoseentityAis 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-724—C_DIAMETERwithvalA = diameter(no*2adjustment, in contrast toradius). - 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-18orlenB² < 1e-18→failedIds.add(c.id), residual row skipped (ts-solver.ts:641). The constraint surfaces as a red pill in the Properties drop-down viasolverStatus.failedConstraintIds. - Segment not in
entityCacheinangleWASM: 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 atslvs-translate.ts:759-787describes the lookup; analogous coverage is not in place forradius/diameter, so a script that radius-constrains a decomposed circle may surface an "entityC not found" warning. - Driven by missing variable (
valVarRefpoints at a deletedVariable): the resolver falls back tovalue(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)
•
╲__⌒Figure 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.
Figure 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.
Figure 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.
Figure 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 affectradius/diameter(direct assignment).
Source-code surprises
diameterbuiltin is circle-only, but the solver and WASM honor an arc id.constraints.ts:291-300rejects non-CircleRef, yetts-solver.ts:796-797andslvs-translate.ts:720-724both treatentityAopaquely 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.radiusanddiameterare bothC_DIAMETERin Solvespace — the only difference is thevalA * 2multiplier for the radius case (slvs-translate.ts:717vs:722). The TS path mirrors this withvaluevsvalue / 2.distanceH/distanceVallocate a singleton auxiliary axis per sketch in the WASM path (slvs-translate.ts:884-905) via sentinelentityCachekeys__axisH,__axisV,__axisOrigin,__axisHTip,__axisVTip. The origin and tips are pinned withslvs.draggedso they cannot drift; without this, every projected-distance constraint would leak its own reference line.angleJacobian uses two equations (sin AND cos) rather than one. The single-equation form is degenerate at 0° and 180° (∂sin/∂θ = cos = ±1but∂lenA·lenB·sin/∂xicollapses); the dot equation supplies the missing rank.- valVarRef is schema-level, not kind-level. The
Constraintrecord always has the field; for geometric kinds it is ignored (constraint-schema.ts:128). Properties drop-down / Badge code therefore checksboundVartruthiness, 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 scalarvaluefield directly on the instance. - Snapshot type:
ConstraintSnapshot(shared with geometric; thekinddiscriminator +valuefield distinguish them). - What
isDimensionalreturns:truefor all 6 kinds — the dim-annotation builder uses this single virtual call instead of a string-prefix check onkind. - Smart-dim wiring through
valVarRef: the base'sresolveValue(scene)reads the referencedVariable.valuewhenvalVarRefis set and the variable exists; otherwise it falls back to the literalvaluefield on the concrete subclass. This is the single seam the solver reads at solve time — it replaces the previous inlinec.valVarRef ? scene.variables.find(...).value : c.valueternary scattered across solver / inspector / dim-annotation code. toSlvs(ctx)— dimensional kinds emit one Slvs constraint (e.g.C_PT_PT_DISTANCEforDistanceConstraint) plus, in theDistanceH/DistanceVcase, the auxiliary axis handles resolved viactx.handleFor('__axisH')etc. The auxiliary-axis allocation lives inslvs-translate.tsas before; the per-subclasstoSlvsconsumes 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.