Skip to content

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 mm in cm → "150 cm". 457.2 mm in cubits → "1 cubit".
  • Imperial: fractional. 133.35 mm in inches → "5 1/4\"". Max denominator 64. Reduces (2/41/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.

mmoutput
1600.25' 3"
1606.555' 3 1/4"
304.81'
25.41"
12.71/2"
00"

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

FileChange
Render.tsCollect dimension rects each frame
Events_3D.ts or new Dimensional_Editor.tsClick detection, input lifecycle
Units.tsparse_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:

  1. Independent bounds (baseline). Each axis edits independently. Change width — height and depth don't move. This is what symmetric resize gives us for free.

  2. 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.

  3. 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