Appearance
Units
Timeline: Feb 6, 2026 | Status: done
Next
- editing within dimensionals
Proposal: Units.ts
Location: types/ — pac: conversion tables + symbols are data-driven like Angle, lives next to T_Unit enums. common/ is grab-bag utilities; types/ is domain primitives.
File: di/src/lib/ts/types/Units.tsTests: di/src/lib/ts/tests/Units.test.ts
Storage model
All SO dimensions stored in millimeters. Display is formatting — like date/time stored in µs.
Conversion table
Each T_Unit → factor in mm. One record, all 22 units. Examples: inch: 25.4, meter: 1000, angstrom: 1e-7, fathom: 1828.8, cubit: 457.2.
Display symbols
Each T_Unit → display string. inch: '"', foot: "'", millimeter: ' mm', nautical_mile: ' nmi', etc.
System membership
system_units(system: T_Unit_System): T_Unit[] — returns units belonging to a system. For UI dropdowns.
Formatting (mm → string)
format(mm: number, unit: T_Unit): string
- Metric/marine/archaic: decimal.
1500 mmin cm →"150 cm".457.2 mmin cubits →"1 cubit". - Imperial: fractional.
133.35 mmin inches →"5 1/4\"". Max denominator 64. Reduces (2/4→1/2). Negligible remainder (< 1/128") → whole number only.
Compound display (imperial only)
format_compound(mm: number): string
Feet + fractional inches. Cascades larger → smaller within imperial.
| mm | output |
|---|---|
| 1600.2 | 5' 3" |
| 1606.55 | 5' 3 1/4" |
| 304.8 | 1' |
| 25.4 | 1" |
| 12.7 | 1/2" |
| 0 | 0" |
Rules: no feet if < 12". No inches if remainder is zero. Fractions reduce, max denominator 64. Metric/marine/archaic don't compound — always single unit decimal.
Parsing (string → mm)
parse(input: string, unit: T_Unit): number | null
- Metric: parse decimal.
"5.25 cm"→52.5 mm. - Imperial: parse fractions.
"5 1/4"in inches →133.35 mm. Also handles"5.25"as decimal inches. - Compound:
"5' 3 1/4\""→1606.55 mm. Detect'and"markers. - Returns null for unparseable input. Tolerant of whitespace, optional unit suffix.
SOT for unit preference
Unit system is a display choice, not a property of the geometry. Same object, different viewers — one sees inches, another sees mm. Like locale for date formatting.
Where it lives: Preferences (persisted) + Svelte store (reactive). Same pattern as w_background_color.
Add to managers/Preferences.ts:
T_Preference:
unitSystem = 'unitSystem'
unit = 'unit'Defaults: inches (matches storage — no conversion needed).
Why not on the SO? You don't want your door in inches and your window in cubits. Unit is a viewport/user concern, not per-object.
Editing within dimensionals
Click a dimensional label (1') → inline text input at that screen position. Type a new value → Enter → SO bound updates. Escape → cancel.
Hit detection
Render already computes text position (midX, midY) and size (textWidth, textHeight). Expose per-frame as dimension_rects: { axis: Axis, so: Smart_Object, rect, value_mm }. Cleared at frame start, populated during render_dimensions().
Click → input overlay
Click (mousedown+mouseup, no drag) on a dimensional rect → floating <input> at that screen position. Pre-filled with current formatted value, all selected. Minimal style — feels like editing in place.
Input → parse → update bound
On Enter: parse via units.parse() (handles fractions, compound, decimals, unit suffixes). A dimensional shows a dimension (width = max − min), not a single bound. Changing the dimension → symmetric resize — grow/shrink from center, both min and max move equally.
Unit-agnostic input
Parser accepts any format regardless of current system. System is imperial, user types 762 mm → works. System is metric, user types 5' 3" → compound detected. Bare numbers (e.g., 2) interpreted in current display unit. Needs parse_for_system(input, system) — tries compound first, then bare numbers in the system's display unit.
Files touched
| File | Change |
|---|---|
Render.ts | Collect dimension rects each frame |
Events_3D.ts or new Dimensional_Editor.ts | Click detection, input lifecycle |
Units.ts | parse_for_system(input, system) |
| Svelte component (Graph or parent) | Host floating <input> overlay |
Constraint propagation
When the user edits one dimensional, related dimensions may need to update. Three levels:
Independent bounds (baseline). Each axis edits independently. Change width — height and depth don't move. This is what symmetric resize gives us for free.
Ratio locks. User locks aspect ratio (or two axes). Change width → height scales proportionally. Storage: a set of active constraints per SO, e.g.
{ type: 'ratio', axes: ['x','y'], value: 1.5 }. On edit, solver walks constraints and adjusts other bounds.Algebraic expressions (M14 territory). A bound is defined as a formula referencing other bounds or SOs.
door.height = wall.height - 6". Editing wall height propagates to door height automatically. This is the full constraint graph — lives in the algebra milestone but the dimensional editor needs to trigger it.
For M11, implement level 1 only. Level 2 is a natural follow-on once the editing UX is solid. Level 3 waits for M14's expression engine — the dimensional editor just needs a hook: after updating a bound, call constraints.propagate(so) (no-op until M14 wires it up).
Propagation order: edit → parse → update bound(s) → propagate constraints → re-render. Single synchronous pass — no async, no batching. If a constraint cycle is detected (A→B→A), break it and warn.
Done
- enums
- inch, foot, yard, mile (imperial)
- angstrom, nanometer, micrometer, millimeter, centimeter, meter, kilometer (metric)
- fathom, nautical_mile (marine)
- hand, span, cubit, ell, rod, perch, chain, furlong, league (archaic)
- T_Unit_System: imperial, metric, marine, archaic
- decide which is going to be the "storage units" — millimeters (like date/time in µs: store in mm, display per format rules)
- string computations (eg, 5 1/4 inches)
- fractions for imperial (not metric)
- parsing (string → mm)
- incorporate into Render.ts