feat: natural-language measurement inputs (UI + MCP tools) via @pascal-app/lingo#472
Conversation
Type any unit into SliderControl / MetricControl fields — "6ft", "180cm", "1m80", "5'11\"", "72in", "45°", "1.57rad" — and it canonicalizes to the unit the field already stores (meters / degrees / radians / inches), independent of the metric/imperial display toggle. A live "= 1.83 m" hint previews the parsed value while typing. Only the typed-text commit path changes: drag-scrub, wheel, and arrow-key editing are untouched, and any unit the adapter doesn't recognise (%, unitless, rad/s) falls back to Number.parseFloat, so nothing regresses. The shared lib/measurement-parser.ts maps each field's `unit` prop to a lingo kind + canonical unit, covering the auto-inspector and every custom panel at once. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…app/lingo
Measurement/angle parameters on the scene tools now accept a bare number OR a
natural-language string ("6 in", "180cm", "45°", "1.57rad"), canonicalized to
the unit the handler already expects (meters for length, radians/degrees for
angles) with min/max bounds and model-readable errors. Makes LLM tool calls
safer — "6 in" no longer has to be pre-converted to 0.1524, and a bad value is
rejected with a message the model can self-correct from.
A shared `measurement(kind, unit)` zod field wraps lingo's parseQuantity as a
`number | string` union+transform; numbers pass through unchanged (backward
compatible), so no handler changes were needed. Covers create_wall, cut_opening,
place_item, create_level, create_story_shell, create_roof,
create_stair_between_levels, create_room, add_door, add_window, photo_to_scene.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rement()
Adversarial-review follow-ups to the lingo measurement() field:
- Add a `positive` option (strict > 0) and use it for the non-zero dimension
params. Swapping z.number().positive() → measurement(..,{min:0}) had started
admitting 0 (the core node schemas have no positivity backstop), so a zero-size
wall/opening/roof could be created. Inclusive-0 fields (overhang, sill height,
knee-wall height, opening offset, roof pitch) keep min:0.
- Escalate AMBIGUOUS_NUMBER to error so "1,234" fails instead of silently reading
as 1234 — a 1000x hazard for European decimals. Matches lingo's own /ai fields.
- roofLevelElevation reverted to z.number() (it's a level ordinal, not meters);
radians fields now advertise radian-appropriate examples.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…elper Records @pascal-app/lingo@^0.1.0 in bun.lock (added to @pascal-app/editor and @pascal-app/mcp), so `bun install --frozen-lockfile` in CI resolves it. Also documents that measurement.ts is intentionally duplicated with the AI-chat copy across the submodule boundary and must be kept in sync. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| unit: options.bareUnit ?? spec.unitId, | ||
| system: options.system, | ||
| strictness: 'forgiving', | ||
| }) |
There was a problem hiding this comment.
Ambiguous commas accepted in UI
Medium Severity
Editor parseMeasurement calls parseQuantity with forgiving strictness but without escalate: { AMBIGUOUS_NUMBER: 'error' }, unlike MCP measurement(). Comma-separated values such as 1,234 can be parsed as thousands of meters instead of being rejected or reverted, so committed scene dimensions may be wrong by orders of magnitude.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit af1fe6a. Configure here.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
SliderControl (which drives the auto-inspector's ~144 fields and most custom panels) previously ignored the viewer unit preference and always rendered raw meters with a static "m" label, while MetricControl converted to feet — so toggling imperial changed some length inputs and not others. Extract the conversion into a shared `useLinearDisplay(unit, precision)` hook and route BOTH controls through it, so every `unit="m"` length field displays and edits in feet when imperial and meters when metric — consistently. Values are always stored in meters; only display, the text field, and drag/ wheel/arrow deltas move to the display unit. For metric and non-length units (°, %, in, …) the conversions are the identity, so those paths are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the unit toggle The property-line vertex X/Z inputs and the Area/Perimeter readout were hardcoded to meters. Convert them to feet / ft² when the viewer preference is imperial, matching every other length input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5ffde5f. Configure here.
| const s = getAdjustedStep(step, e) | ||
| const newValue = clamp(valueRef.current + direction * s) | ||
| const final = Number.parseFloat(newValue.toFixed(stepPrecision(s))) | ||
| const final = applyDisplayDelta(valueRef.current, direction * s, s) |
There was a problem hiding this comment.
Imperial mode mis-scales scrub steps
Medium Severity
With the viewer set to imperial on unit="m" fields, wheel, drag, and arrow adjustments use the raw step prop as feet (or apply toStored to it) even though callers like the parametric inspector still pass step in stored meters. Each tick moves roughly three times less stored length than before, so scrubbing no longer matches field descriptors or prior behavior.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 5ffde5f. Configure here.


What
Integrates
@pascal-app/lingoso users and LLMs can express measurements in natural language, canonicalized to the units the editor already stores. Two surfaces:1. UI property inputs (
packages/editor)Every editable measurement field now accepts
6ft,180cm,1m80,5'11",72in,45°,1.57rad(typo-tolerant too), regardless of the metric/imperial toggle.packages/editor/src/lib/measurement-parser.tsmaps a field'sunitprop → a lingo kind + canonical unit.SliderControl(the auto-inspector's ~144 fields + most custom panels) andMetricControl— only on the typed-text commit path. Drag-scrub, wheel, and arrow-key editing are byte-for-byte unchanged; any unrecognized unit falls back toNumber.parseFloat, so nothing regresses.= 1.83 mconversion hint previews the parsed value while typing (clamped to the field's min/max so it matches what a commit stores).unit="°"/"rad".2. MCP tool inputs (
packages/mcp)Measurement/angle parameters on the scene tools now accept a bare number or a natural-language string, canonicalized to the unit each handler expects (meters for length, radians/degrees for angles) with
min/maxbounds and model-readable errors. Makes LLM tool calls safer:"6 in"no longer has to be pre-converted, and a bad value ("banana", out-of-range, or an ambiguous"1,234") is rejected with a message the model can self-correct from.measurement(kind, unit)zod field wraps lingo'sparseQuantityas anumber | stringunion+transform; numbers pass through unchanged (backward compatible), so no handler changes were needed.create_wall,cut_opening,place_item,create_level,create_story_shell,create_roof,create_stair_between_levels,create_room,add_door,add_window,photo_to_scene. Position coordinates, parametrict(0–1), and integer counts are intentionally left numeric.Why
Users think in mixed units; today a field showing meters forces an imperial user to mentally convert. And LLMs emit
"6 ft"far more reliably than a canonical float (Number("6ft")isNaN). lingo'slengthbase is meters andanglebase is radians — exactly how the editor already stores values — so it drops in with no schema change.Testing
6ft→1.83 m,1m80→1.80 m,2 ft 3 in→0.69 m,1.57rad→90.0°, with the live hint and safe revert on invalid input. Imperial parity is byte-identical to the old× 0.3048path.measurement.test.ts(helper) +create-wall.test.ts(natural-language round-trip through the MCP SDK). Full suite: 293 tests pass. New coverage forpositive/ambiguous-number rejection.bun run check(biome),bun run check-types, core+mcp build all green.Notes
measurement.tsis duplicated betweenpackages/mcpand the private-editor AI-chat package (they're in different repos and can't share a module). Kept in sync; the clean long-term dedup is a zod-field adapter exported from@pascal-app/lingoitself.>0validation that.positive()provided; escalateAMBIGUOUS_NUMBERto error so"1,234"can't silently become 1234).🤖 Generated with Claude Code
Note
Medium Risk
Touches many MCP tool inputs and editor numeric commit paths; wrong unit conversion or parse edge cases could change stored geometry, though numbers still pass through and ambiguous strings are rejected at the MCP boundary.
Overview
Adds
@pascal-app/lingoto editor and MCP so measurement values can be typed or passed as natural language and normalized to the units the app already stores.In the editor, new
measurement-parseranduseLinearDisplaybackMetricControlandSliderControl: typed commits accept strings like6ft,1m80,5'11", and angles; a live= …hint shows the parsed value; imperial m fields keep display/edit in feet while storage stays in meters.SliderControlalso routes wheel, drag, and arrow deltas through display-unit conversion. The site panel property line shows area/perimeter and vertex coords in the viewer’s metric/imperial preference.In MCP, a shared
measurement()Zod field (number | string) wraps lingo parsing with bounds,positive, and hard failure on ambiguous numbers like1,234; construction/scene tools swap rawz.number()length/angle args for this helper. Tests cover the helper andcreate_wallNL round-trips.Reviewed by Cursor Bugbot for commit 5ffde5f. Bugbot is set up for automated code reviews on this repo. Configure here.