@pascal-app/editor 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +18 -15
- package/src/components/editor/action-menu-button.tsx +4 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
- package/src/components/editor/bake-thumbnail.tsx +3 -2
- package/src/components/editor/custom-camera-controls.tsx +20 -8
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +72 -61
- package/src/components/editor/floating-action-menu.tsx +27 -9
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +285 -132
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/grid.tsx +24 -15
- package/src/components/editor/group-actions.ts +19 -1
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +27 -2
- package/src/components/editor/group-selection-box-3d.tsx +4 -1
- package/src/components/editor/group-transform-shared.ts +20 -0
- package/src/components/editor/index.tsx +130 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +1 -2
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/registry-action-contributions.test.tsx +67 -6
- package/src/components/editor/registry-action-contributions.tsx +115 -15
- package/src/components/editor/riser-diagram-panel.tsx +59 -9
- package/src/components/editor/room-controls.tsx +1 -1
- package/src/components/editor/room-handles-2d.tsx +156 -6
- package/src/components/editor/room-handles.tsx +241 -51
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +29 -15
- package/src/components/editor/selection-manager.tsx +15 -102
- package/src/components/editor/separator-end-grip.tsx +74 -0
- package/src/components/editor/site-edge-labels.tsx +1 -1
- package/src/components/editor/thumbnail-generator.test.ts +15 -6
- package/src/components/editor/thumbnail-generator.tsx +26 -1
- package/src/components/editor/use-floorplan-background-placement.ts +55 -52
- package/src/components/editor/views/view-bar.tsx +111 -26
- package/src/components/editor/wall-measurement-label.tsx +1 -1
- package/src/components/editor/wall-move-side-handles.tsx +5 -1
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
- package/src/components/procedural-items/inspector.tsx +59 -12
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
- package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
- package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/placement-strategies.test.ts +64 -1
- package/src/components/tools/item/placement-strategies.ts +5 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- package/src/components/tools/select/select-candidates.test.ts +45 -0
- package/src/components/tools/select/select-candidates.ts +13 -2
- package/src/components/tools/shared/cursor-sphere.tsx +10 -3
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +1 -1
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/wall/wall-drafting.test.ts +299 -23
- package/src/components/tools/wall/wall-drafting.ts +132 -24
- package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
- package/src/components/tools/wall/wall-pull-snap.ts +563 -0
- package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
- package/src/components/tools/zone/zone-tool.tsx +3 -3
- package/src/components/ui/action-menu/action-button.tsx +2 -0
- package/src/components/ui/action-menu/camera-actions.tsx +8 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -5
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
- package/src/components/ui/action-menu/index.tsx +1 -0
- package/src/components/ui/action-menu/measurement-control.tsx +35 -29
- package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
- package/src/components/ui/action-menu/structure-tools.tsx +25 -23
- package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
- package/src/components/ui/action-menu/view-toggles.tsx +256 -51
- package/src/components/ui/build-panel/build-panel.tsx +163 -25
- package/src/components/ui/command-palette/editor-commands.tsx +38 -11
- package/src/components/ui/controls/finish-swatch.tsx +45 -19
- package/src/components/ui/controls/material-picker.tsx +24 -17
- package/src/components/ui/controls/metric-control.test.ts +52 -0
- package/src/components/ui/controls/metric-control.tsx +62 -24
- package/src/components/ui/controls/segmented-control.tsx +5 -2
- package/src/components/ui/controls/slider-control.test.tsx +23 -0
- package/src/components/ui/controls/slider-control.tsx +30 -14
- package/src/components/ui/floating-level-selector.tsx +100 -48
- package/src/components/ui/helpers/building-helper.tsx +5 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
- package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
- package/src/components/ui/helpers/helper-manager.tsx +82 -57
- package/src/components/ui/helpers/item-helper.tsx +6 -4
- package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
- package/src/components/ui/level-duplicate-dialog.tsx +32 -23
- package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
- package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
- package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
- package/src/components/ui/panels/inspector-extensions.tsx +66 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
- package/src/components/ui/panels/multi-field-value.test.ts +27 -0
- package/src/components/ui/panels/multi-field-value.ts +20 -1
- package/src/components/ui/panels/multi-height-mode.tsx +8 -7
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +5 -3
- package/src/components/ui/panels/panel-wrapper.tsx +22 -109
- package/src/components/ui/panels/parametric-field-control.tsx +13 -10
- package/src/components/ui/panels/parametric-inspector.tsx +18 -12
- package/src/components/ui/panels/reference-panel.tsx +51 -35
- package/src/components/ui/panels/room-construction-rows.tsx +464 -65
- package/src/components/ui/panels/room-name-options.test.ts +36 -3
- package/src/components/ui/panels/room-name-options.ts +110 -11
- package/src/components/ui/panels/room-openings-field.tsx +21 -12
- package/src/components/ui/panels/room-panel.tsx +206 -99
- package/src/components/ui/panels/shape-choice.tsx +7 -5
- package/src/components/ui/panels/wall-region-list.tsx +7 -7
- package/src/components/ui/primitives/shortcut-token.tsx +10 -6
- package/src/components/ui/sidebar/icon-rail.tsx +11 -9
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
- package/src/components/ui/sidebar/tab-bar.tsx +4 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
- package/src/components/ui/snap-target-badge.tsx +7 -6
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
- package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
- package/src/components/viewer/floorplan-compass-button.tsx +12 -3
- package/src/components/viewer/floorplan-preview.tsx +27 -19
- package/src/components/viewer/render-settings-menu.tsx +294 -0
- package/src/components/viewer/sky-control.tsx +710 -0
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
- package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
- package/src/components/viewer/viewer-controls-bar.tsx +278 -260
- package/src/components/viewer/viewer-hover-label.tsx +12 -4
- package/src/components/viewer/viewer-rooms.tsx +61 -4
- package/src/components/viewer/viewer-scene-header.tsx +28 -17
- package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
- package/src/components/viewer-overlay.tsx +8 -0
- package/src/components/walkthrough-hud.tsx +75 -2
- package/src/hooks/use-keyboard.test.ts +85 -4
- package/src/hooks/use-keyboard.ts +65 -7
- package/src/hooks/use-selected-room.ts +12 -1
- package/src/i18n/floor-footprint-name.ts +30 -0
- package/src/i18n/level-name.ts +39 -0
- package/src/i18n/locales/en/messages.po +15117 -0
- package/src/i18n/locales/en/messages.ts +1 -0
- package/src/i18n/locales/en-XA/messages.ts +1 -0
- package/src/i18n/locales/es/messages.po +37501 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +37501 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +37501 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +37501 -0
- package/src/i18n/locales/zh-CN/messages.ts +1 -0
- package/src/i18n/locales.test.ts +63 -0
- package/src/i18n/locales.ts +32 -0
- package/src/i18n/message-slot.tsx +25 -0
- package/src/i18n/node-messages.ts +169 -0
- package/src/i18n/pseudo-locale.ts +17 -0
- package/src/i18n/published-entry.test.ts +17 -0
- package/src/i18n/scene-theme-name.ts +16 -0
- package/src/index.tsx +156 -4
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +95 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/floor-footprints.test.ts +2 -1
- package/src/lib/floor-foundation-support.test.ts +7 -3
- package/src/lib/floorplan/dimension-drive.test.ts +36 -2
- package/src/lib/floorplan/dimension-drive.ts +68 -3
- package/src/lib/floorplan/floorplan-export.test.ts +16 -5
- package/src/lib/floorplan/floorplan-export.tsx +13 -16
- package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
- package/src/lib/floorplan/floorplan-extension.ts +37 -18
- package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
- package/src/lib/floorplan/floorplan-mode.ts +14 -12
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
- package/src/lib/floorplan/floorplan-readonly.ts +3 -3
- package/src/lib/floorplan/schedules.ts +10 -6
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
- package/src/lib/geometry-script/author.test.ts +114 -0
- package/src/lib/geometry-script/author.ts +55 -23
- package/src/lib/geometry-script/client.ts +3 -0
- package/src/lib/geometry-script/finish-scripts.ts +7 -7
- package/src/lib/glb-export.test.ts +327 -0
- package/src/lib/glb-export.ts +70 -2
- package/src/lib/grid-frame.test.ts +56 -3
- package/src/lib/grid-frame.ts +28 -1
- package/src/lib/history.test.ts +2 -1
- package/src/lib/hud-title.ts +48 -24
- package/src/lib/ifc-export.test.ts +41 -1
- package/src/lib/inspector-card-mode.test.ts +3 -31
- package/src/lib/inspector-card-mode.ts +5 -10
- package/src/lib/inspector-expanded.test.ts +13 -0
- package/src/lib/inspector-expanded.ts +13 -1
- package/src/lib/keyboard-pan.test.ts +7 -0
- package/src/lib/keyboard-pan.ts +9 -2
- package/src/lib/layer-thickness.test.ts +22 -0
- package/src/lib/layer-thickness.ts +79 -0
- package/src/lib/lot/drop-in.test.ts +59 -1
- package/src/lib/lot/drop-in.ts +42 -50
- package/src/lib/material-swatch-style.ts +24 -0
- package/src/lib/measurement-parser.ts +7 -3
- package/src/lib/measurements.test.ts +17 -0
- package/src/lib/measurements.ts +20 -5
- package/src/lib/mezzanine-selection.test.ts +3 -1
- package/src/lib/orbit-near-plane.test.ts +12 -0
- package/src/lib/orbit-near-plane.ts +13 -0
- package/src/lib/paint-regions.test.ts +8 -5
- package/src/lib/paint-scope.test.ts +53 -0
- package/src/lib/paint-scope.ts +18 -4
- package/src/lib/panel-rows.ts +17 -8
- package/src/lib/reference-scale.ts +83 -0
- package/src/lib/room-built-on.test.ts +26 -12
- package/src/lib/room-built-on.ts +20 -2
- package/src/lib/room-construction.test.ts +28 -1
- package/src/lib/room-construction.ts +14 -0
- package/src/lib/room-controls-phase6.test.ts +140 -0
- package/src/lib/room-handle-drag.ts +557 -42
- package/src/lib/room-name-catalog.test.ts +8 -0
- package/src/lib/room-name-catalog.ts +8 -1
- package/src/lib/room-name-messages.test.ts +132 -0
- package/src/lib/room-name-messages.ts +147 -0
- package/src/lib/room-name-storage.test.ts +334 -0
- package/src/lib/room-push-dimensions.test.ts +37 -0
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +9 -4
- package/src/lib/selection-cycle.ts +63 -0
- package/src/lib/separator-edit.test.ts +431 -0
- package/src/lib/separator-edit.ts +179 -0
- package/src/lib/separator-lines.ts +38 -0
- package/src/lib/sfx-bus.ts +27 -1
- package/src/lib/sfx-player.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +5 -1
- package/src/lib/tool-shortcuts.ts +130 -0
- package/src/lib/viewer-selection.test.ts +28 -24
- package/src/lib/viewer-selection.ts +20 -5
- package/src/lib/wall-draw-variant.test.ts +7 -6
- package/src/lib/wall-draw-variant.ts +107 -6
- package/src/lib/wall-nudge.test.ts +44 -0
- package/src/lib/wall-nudge.ts +131 -0
- package/src/lib/wall-parallel-snap.test.ts +83 -0
- package/src/lib/wall-parallel-snap.ts +176 -0
- package/src/lib/wall-placement-pointer.test.ts +23 -0
- package/src/lib/wall-placement-pointer.ts +18 -0
- package/src/lib/wall-pull.test.ts +245 -0
- package/src/lib/wall-pull.ts +209 -0
- package/src/lib/wall-push-line-up.test.ts +132 -0
- package/src/lib/wall-region-list.ts +14 -5
- package/src/lib/wall-run-snap.test.ts +188 -0
- package/src/lib/wall-run-snap.ts +83 -0
- package/src/lib/wall-type.test.ts +125 -0
- package/src/lib/wall-type.ts +119 -0
- package/src/lib/window-interaction.test.ts +42 -0
- package/src/lib/window-interaction.ts +47 -5
- package/src/lib/world-grid-snap.test.ts +139 -2
- package/src/lib/world-grid-snap.ts +102 -105
- package/src/store/use-alignment-guides.ts +27 -1
- package/src/store/use-editor.tsx +48 -0
- package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
- package/src/store/use-floorplan-annotation-visibility.ts +47 -22
- package/src/store/use-wall-snap-indicator.ts +15 -2
package/src/lib/lot/drop-in.ts
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* 3. `sitePatchFromParcel` — the site node patch: ring, address,
|
|
10
10
|
* provenance, the street-facing front edge, planning-default setbacks
|
|
11
11
|
* when the site has none, north up.
|
|
12
|
-
* 4.
|
|
13
|
-
* has none) and a building that fell outside the new ring is
|
|
14
|
-
* re-centred on it — `building.position` only, never the walls.
|
|
15
|
-
* 5. `elevation` — USGS ground over the lot into the site's
|
|
12
|
+
* 4. `elevation` — USGS ground over the lot into the site's
|
|
16
13
|
* heightfield (`site.terrain`, terrain.ts; fail-soft, flat lots write
|
|
17
14
|
* nothing), so a foundation can read the hill.
|
|
15
|
+
* 5. The parcel and terrain are applied to the current site together
|
|
16
|
+
* (created at the root when the scene has none). A building outside
|
|
17
|
+
* the new ring is re-centred — `building.position` only, never the walls.
|
|
18
18
|
*
|
|
19
19
|
* Used by the Lot rail panel, the Generate panel (drop in, then generate)
|
|
20
20
|
* and the Site inspector's "Find parcel", so every path behaves the same.
|
|
@@ -271,71 +271,63 @@ export async function dropInLot(
|
|
|
271
271
|
roadsFailure = 'skipped'
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
if (!site) {
|
|
277
|
-
site = SiteNode.parse({ id: generateId('site'), type: 'site', name: 'Site' })
|
|
278
|
-
scene.createNodes([{ node: site as AnyNode }])
|
|
279
|
-
}
|
|
280
|
-
const computed = sitePatchFromParcel(site, input, data, roads, undefined, extras)
|
|
281
|
-
if (!computed)
|
|
274
|
+
const geometry = sitePatchFromParcel(null, input, data, roads, undefined, extras)
|
|
275
|
+
if (!geometry)
|
|
282
276
|
return {
|
|
283
277
|
ok: false,
|
|
284
278
|
error: 'parcel geometry was unusable',
|
|
285
279
|
message: 'The parcel geometry was unusable.',
|
|
286
280
|
}
|
|
287
|
-
// the surveyed contour lines ride the site with the patch (the site plan
|
|
288
|
-
// draws them over the heightfield's own); absent without 3DEP lines
|
|
289
|
-
const contours = dossier && dossierOrigin ? contourLinesFromDossier(dossier, dossierOrigin) : null
|
|
290
|
-
useScene
|
|
291
|
-
.getState()
|
|
292
|
-
.updateNode(
|
|
293
|
-
site.id as AnyNodeId,
|
|
294
|
-
{ ...computed.patch, terrainContours: contours ?? undefined } as Partial<AnyNode>,
|
|
295
|
-
)
|
|
296
|
-
|
|
297
281
|
// The ground over the lot — fail-soft. A sloping lot writes the
|
|
298
282
|
// heightfield; a flat one (or a failed read) clears any terrain the
|
|
299
283
|
// previous lot left behind so the site never shows another parcel's hill.
|
|
300
284
|
let terrain: TerrainSampleSummary | null = null
|
|
301
285
|
let terrainFailure = ''
|
|
286
|
+
let terrainRead: TerrainSampleResult | null = null
|
|
302
287
|
if (options.terrain !== false) {
|
|
303
|
-
|
|
304
|
-
? await sampleLotTerrain(
|
|
288
|
+
terrainRead = data.originLngLat
|
|
289
|
+
? await sampleLotTerrain(geometry.patch.polygon?.points ?? [], data.originLngLat, {
|
|
305
290
|
provider,
|
|
306
291
|
...(options.terrainDeadlineMs ? { deadlineMs: options.terrainDeadlineMs } : {}),
|
|
307
292
|
})
|
|
308
293
|
: { ok: false, reason: 'no parcel origin' }
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
current &&
|
|
312
|
-
typeof current.metadata === 'object' &&
|
|
313
|
-
current.metadata !== null &&
|
|
314
|
-
!Array.isArray(current.metadata)
|
|
315
|
-
? (current.metadata as Record<string, unknown>)
|
|
316
|
-
: {}
|
|
317
|
-
if (read.ok && read.summary) {
|
|
318
|
-
terrain = read.summary
|
|
319
|
-
useScene.getState().updateNode(
|
|
320
|
-
site.id as AnyNodeId,
|
|
321
|
-
{
|
|
322
|
-
terrain: read.terrain,
|
|
323
|
-
metadata: { ...meta, terrainSample: read.summary },
|
|
324
|
-
} as unknown as Partial<AnyNode>,
|
|
325
|
-
)
|
|
326
|
-
} else {
|
|
327
|
-
terrainFailure = read.reason ?? 'no terrain'
|
|
328
|
-
useScene
|
|
329
|
-
.getState()
|
|
330
|
-
.updateNode(
|
|
331
|
-
site.id as AnyNodeId,
|
|
332
|
-
{ terrain: undefined, metadata: meta } as unknown as Partial<AnyNode>,
|
|
333
|
-
)
|
|
334
|
-
}
|
|
294
|
+
if (terrainRead.ok && terrainRead.summary) terrain = terrainRead.summary
|
|
295
|
+
else terrainFailure = terrainRead.reason ?? 'no terrain'
|
|
335
296
|
} else {
|
|
336
297
|
terrainFailure = 'skipped'
|
|
337
298
|
}
|
|
338
299
|
|
|
300
|
+
// Hydration or a person's edit can replace the site while the providers run.
|
|
301
|
+
// Read it after every await, preserve its current settings, and commit the
|
|
302
|
+
// parcel with its terrain so a late read cannot leave the starter polygon.
|
|
303
|
+
const scene = useScene.getState()
|
|
304
|
+
let site = findSiteNode(options.siteId)
|
|
305
|
+
const computed = sitePatchFromParcel(site, input, data, roads, undefined, extras)
|
|
306
|
+
if (!computed)
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
error: 'parcel geometry was unusable',
|
|
310
|
+
message: 'The parcel geometry was unusable.',
|
|
311
|
+
}
|
|
312
|
+
const contours = dossier && dossierOrigin ? contourLinesFromDossier(dossier, dossierOrigin) : null
|
|
313
|
+
const patch: Partial<SiteNode> = {
|
|
314
|
+
...computed.patch,
|
|
315
|
+
terrainContours: contours ?? undefined,
|
|
316
|
+
}
|
|
317
|
+
if (terrainRead) {
|
|
318
|
+
const { terrainSample: _previous, ...metadata } = computed.patch.metadata as Record<
|
|
319
|
+
string,
|
|
320
|
+
unknown
|
|
321
|
+
>
|
|
322
|
+
patch.terrain = terrainRead.ok ? terrainRead.terrain : undefined
|
|
323
|
+
patch.metadata = terrain ? { ...metadata, terrainSample: terrain } : metadata
|
|
324
|
+
}
|
|
325
|
+
if (site) scene.updateNode(site.id as AnyNodeId, patch as Partial<AnyNode>)
|
|
326
|
+
else {
|
|
327
|
+
site = SiteNode.parse({ id: generateId('site'), type: 'site', name: 'Site', ...patch })
|
|
328
|
+
scene.createNodes([{ node: site as AnyNode }])
|
|
329
|
+
}
|
|
330
|
+
|
|
339
331
|
// A house the new lot leaves past its lines stands centred on the buildable area again, facing
|
|
340
332
|
// as it did.
|
|
341
333
|
let recentred = false
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MaterialSwatch } from '@pascal-app/core'
|
|
2
|
+
import type { CSSProperties } from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The CSS that paints a material swatch: the image over its colour, and a tinted
|
|
6
|
+
* albedo multiplied by the colour, as the 3D material tints its map.
|
|
7
|
+
*/
|
|
8
|
+
export function materialSwatchStyle({
|
|
9
|
+
color,
|
|
10
|
+
imageUrl,
|
|
11
|
+
tinted,
|
|
12
|
+
}: Pick<MaterialSwatch, 'color' | 'imageUrl'> & { tinted?: boolean }): CSSProperties {
|
|
13
|
+
return {
|
|
14
|
+
backgroundColor: color,
|
|
15
|
+
...(imageUrl
|
|
16
|
+
? {
|
|
17
|
+
backgroundImage: `url(${JSON.stringify(imageUrl)})`,
|
|
18
|
+
backgroundPosition: 'center',
|
|
19
|
+
backgroundSize: 'cover',
|
|
20
|
+
...(tinted ? { backgroundBlendMode: 'multiply' } : {}),
|
|
21
|
+
}
|
|
22
|
+
: {}),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Must precede the lingo import: lingo clones its kind table with
|
|
2
2
|
// structuredClone while its module body evaluates. See the file's own comment.
|
|
3
3
|
import './structured-clone-fallback'
|
|
4
|
+
import { normalizeDecimalInput } from '@pascal-app/core'
|
|
4
5
|
import { type Kind, parseQuantity, quantity } from '@pascal-app/lingo'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -56,6 +57,8 @@ export interface ParseMeasurementOptions {
|
|
|
56
57
|
bareUnit?: string
|
|
57
58
|
/** Disambiguates gal/ton/cup families; harmless for length/angle. */
|
|
58
59
|
system?: 'metric' | 'us' | 'imperial'
|
|
60
|
+
/** Locale whose decimal separator is accepted in bare values and quantities. */
|
|
61
|
+
locale?: string
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
/**
|
|
@@ -68,7 +71,7 @@ export function parseMeasurement(
|
|
|
68
71
|
spec: LingoUnitSpec,
|
|
69
72
|
options: ParseMeasurementOptions = {},
|
|
70
73
|
): number | null {
|
|
71
|
-
const result = parseQuantity(raw, {
|
|
74
|
+
const result = parseQuantity(normalizeDecimalInput(raw, options.locale), {
|
|
72
75
|
kind: spec.kind,
|
|
73
76
|
unit: options.bareUnit ?? spec.unitId,
|
|
74
77
|
system: options.system,
|
|
@@ -105,9 +108,10 @@ export function measurementHint(
|
|
|
105
108
|
options: MeasurementHintOptions = {},
|
|
106
109
|
): string | null {
|
|
107
110
|
const trimmed = raw.trim()
|
|
108
|
-
|
|
111
|
+
const normalized = normalizeDecimalInput(trimmed, options.locale)
|
|
112
|
+
if (!normalized || PLAIN_NUMBER.test(normalized)) return null
|
|
109
113
|
|
|
110
|
-
const parsed = parseMeasurement(
|
|
114
|
+
const parsed = parseMeasurement(normalized, spec, options)
|
|
111
115
|
if (parsed === null) return null
|
|
112
116
|
const stored = options.clamp ? options.clamp(parsed) : parsed
|
|
113
117
|
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
measurementFloorplanPresentationColor,
|
|
19
19
|
measurementPresentationColor,
|
|
20
20
|
metersToLinearUnit,
|
|
21
|
+
parseLinearMeasurementInput,
|
|
21
22
|
squareMetersToAreaUnit,
|
|
22
23
|
} from './measurements'
|
|
23
24
|
|
|
@@ -129,6 +130,11 @@ describe('linear measurements', () => {
|
|
|
129
130
|
expect(linearControlValueToMeters(3.5, 'metric')).toBe(3.5)
|
|
130
131
|
})
|
|
131
132
|
|
|
133
|
+
test('parses floor-plan measurement input with the scoped locale', () => {
|
|
134
|
+
expect(parseLinearMeasurementInput('2,75', 'metric', 'fr')).toBe(2.75)
|
|
135
|
+
expect(parseLinearMeasurementInput('2,75', 'metric', 'en')).toBeNull()
|
|
136
|
+
})
|
|
137
|
+
|
|
132
138
|
test('clamps numeric control input after converting to meters', () => {
|
|
133
139
|
expect(linearControlValueToMeters(0.1, 'imperial', { minMeters: 0.1 })).toBe(0.1)
|
|
134
140
|
expect(linearControlValueToMeters(0.3, 'imperial', { minMeters: 0.1 })).toBe(0.1)
|
|
@@ -212,4 +218,15 @@ describe('typed measurement fields', () => {
|
|
|
212
218
|
expect(measurementHint('180cm', length)).toBe('= 1.80 m')
|
|
213
219
|
expect(measurementHint('6', length)).toBeNull()
|
|
214
220
|
})
|
|
221
|
+
|
|
222
|
+
test('only treats the active locale decimal separator as a plain decimal', () => {
|
|
223
|
+
const length = { kind: 'length', unitId: 'm' } as const
|
|
224
|
+
|
|
225
|
+
expect(measurementHint('1,000', length, { locale: 'en' })).not.toBeNull()
|
|
226
|
+
expect(measurementHint('-1,000', length, { locale: 'en' })).not.toBeNull()
|
|
227
|
+
expect(measurementHint('1,5', length, { locale: 'en' })).not.toBeNull()
|
|
228
|
+
|
|
229
|
+
expect(measurementHint('1,5', length, { locale: 'fr' })).toBeNull()
|
|
230
|
+
expect(parseMeasurement('2,75 m', length, { locale: 'fr' })).toBe(2.75)
|
|
231
|
+
})
|
|
215
232
|
})
|
package/src/lib/measurements.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { formatNumber, type MeasurementPoint, parseLocalizedNumber } from '@pascal-app/core'
|
|
2
2
|
|
|
3
3
|
export type LinearUnit = 'metric' | 'imperial'
|
|
4
4
|
export type MetricNotation = 'meters' | 'millimeters'
|
|
@@ -119,6 +119,15 @@ export function linearUnitToMeters(value: number, unit: LinearUnit): number {
|
|
|
119
119
|
return unit === 'imperial' ? value * METERS_PER_FOOT : value
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
export function parseLinearMeasurementInput(
|
|
123
|
+
value: string,
|
|
124
|
+
unit: LinearUnit,
|
|
125
|
+
locale: string,
|
|
126
|
+
): number | null {
|
|
127
|
+
const parsed = parseLocalizedNumber(value, locale)
|
|
128
|
+
return parsed === null ? null : linearUnitToMeters(parsed, unit)
|
|
129
|
+
}
|
|
130
|
+
|
|
122
131
|
export function linearControlValueToMeters(
|
|
123
132
|
value: number,
|
|
124
133
|
unit: LinearUnit,
|
|
@@ -153,7 +162,10 @@ export function formatAreaLabel(
|
|
|
153
162
|
): string {
|
|
154
163
|
if (!Number.isFinite(squareMeters)) return '--'
|
|
155
164
|
|
|
156
|
-
return `${squareMetersToAreaUnit(squareMeters, unit)
|
|
165
|
+
return `${formatNumber(squareMetersToAreaUnit(squareMeters, unit), {
|
|
166
|
+
min: fractionDigits,
|
|
167
|
+
max: fractionDigits,
|
|
168
|
+
})}${getAreaUnitLabel(unit)}`
|
|
157
169
|
}
|
|
158
170
|
|
|
159
171
|
export function cubicMetersToVolumeUnit(cubicMeters: number, unit: LinearUnit): number {
|
|
@@ -171,7 +183,10 @@ export function formatVolumeLabel(
|
|
|
171
183
|
): string {
|
|
172
184
|
if (!Number.isFinite(cubicMeters)) return '--'
|
|
173
185
|
|
|
174
|
-
return `${cubicMetersToVolumeUnit(cubicMeters, unit)
|
|
186
|
+
return `${formatNumber(cubicMetersToVolumeUnit(cubicMeters, unit), {
|
|
187
|
+
min: fractionDigits,
|
|
188
|
+
max: fractionDigits,
|
|
189
|
+
})}${getVolumeUnitLabel(unit)}`
|
|
175
190
|
}
|
|
176
191
|
|
|
177
192
|
export function formatLinearMeasurement(
|
|
@@ -200,11 +215,11 @@ export function formatLinearMeasurement(
|
|
|
200
215
|
if (metricNotation === 'millimeters') {
|
|
201
216
|
const roundedMillimeters = Math.round(absoluteMeters * 1000)
|
|
202
217
|
const sign = meters < 0 && roundedMillimeters !== 0 ? '-' : ''
|
|
203
|
-
return `${sign}${roundedMillimeters}mm`
|
|
218
|
+
return `${sign}${formatNumber(roundedMillimeters, { max: 0 })}mm`
|
|
204
219
|
}
|
|
205
220
|
|
|
206
221
|
const roundedMeters = Number.parseFloat(absoluteMeters.toFixed(2))
|
|
207
222
|
const sign = meters < 0 && roundedMeters !== 0 ? '-' : ''
|
|
208
223
|
|
|
209
|
-
return `${sign}${roundedMeters}m`
|
|
224
|
+
return `${sign}${formatNumber(roundedMeters, { max: 2 })}m`
|
|
210
225
|
}
|
|
@@ -169,7 +169,9 @@ describe('mezzanine selection', () => {
|
|
|
169
169
|
expect(gesture(hostId, ROOM_MOVE_DRAG_LABEL)).toBeNull()
|
|
170
170
|
expect(gesture(mezzanineId, 'height')).toBeNull()
|
|
171
171
|
expect(mezzanineGesture(null, nodes())).toBeNull()
|
|
172
|
-
expect(MEZZANINE_GESTURE_HINTS.move[0]!.label).
|
|
172
|
+
expect(MEZZANINE_GESTURE_HINTS.move[0]!.label).toMatchObject({
|
|
173
|
+
message: 'Place inside the room',
|
|
174
|
+
})
|
|
173
175
|
})
|
|
174
176
|
|
|
175
177
|
test('the delete dialog speaks of the mezzanine, and of its items only when it has some', () => {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { CLOSE_UP_NEAR, ORBIT_NEAR, orbitNearPlane } from './orbit-near-plane'
|
|
3
|
+
|
|
4
|
+
describe('the orbit near plane follows the eye in', () => {
|
|
5
|
+
test("standing back it is the viewer's own; close in, a tenth of the distance; never under an inch-off look", () => {
|
|
6
|
+
expect(orbitNearPlane(40)).toBe(ORBIT_NEAR)
|
|
7
|
+
expect(orbitNearPlane(3)).toBe(ORBIT_NEAR)
|
|
8
|
+
expect(orbitNearPlane(1)).toBeCloseTo(0.1, 9)
|
|
9
|
+
expect(orbitNearPlane(0.5)).toBeCloseTo(0.05, 9)
|
|
10
|
+
expect(orbitNearPlane(0.01)).toBe(CLOSE_UP_NEAR)
|
|
11
|
+
})
|
|
12
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The viewer's near plane (`viewerCameraClipping`), kept while the eye stands back. */
|
|
2
|
+
export const ORBIT_NEAR = 0.3
|
|
3
|
+
/** How close the near plane comes in when the eye does: a casing seen from an inch off. */
|
|
4
|
+
export const CLOSE_UP_NEAR = 0.01
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The near plane for an orbit `distance` from its target: a tenth of it, between `CLOSE_UP_NEAR`
|
|
8
|
+
* and the viewer's own, so zooming in on a window's trim never clips it while the eye standing
|
|
9
|
+
* back keeps the depth precision it had.
|
|
10
|
+
*/
|
|
11
|
+
export function orbitNearPlane(distance: number): number {
|
|
12
|
+
return Math.min(ORBIT_NEAR, Math.max(CLOSE_UP_NEAR, distance * 0.1))
|
|
13
|
+
}
|
|
@@ -136,10 +136,11 @@ describe('paint region sub-mode', () => {
|
|
|
136
136
|
})
|
|
137
137
|
|
|
138
138
|
test('off anything it draws on, the HUD says where to go: a floor for Polygon', () => {
|
|
139
|
-
expect(
|
|
140
|
-
'
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
expect(
|
|
140
|
+
paintRegionHints('polygon', null, false).map((hint) =>
|
|
141
|
+
typeof hint.label === 'string' ? hint.label : hint.label.message,
|
|
142
|
+
),
|
|
143
|
+
).toEqual(['Hover a floor or ceiling', 'Cancel'])
|
|
143
144
|
expect(paintRegionHints('rectangle', null, false)[0]).toMatchObject({
|
|
144
145
|
label: 'Hover a wall, floor or ceiling',
|
|
145
146
|
active: true,
|
|
@@ -158,7 +159,9 @@ describe('paint region sub-mode', () => {
|
|
|
158
159
|
|
|
159
160
|
test('the whole-surface HUD says what a click does; only painting teaches the held eyedropper', () => {
|
|
160
161
|
const labels = (mode: Parameters<typeof paintHints>[0]) =>
|
|
161
|
-
paintHints(mode).map((hint) =>
|
|
162
|
+
paintHints(mode).map((hint) =>
|
|
163
|
+
typeof hint.label === 'string' ? hint.label : (hint.label.message ?? ''),
|
|
164
|
+
)
|
|
162
165
|
expect(labels('surface')).toEqual(['Hold to pick a material'])
|
|
163
166
|
expect(labels('erase')).toEqual(['Remove a painted part, or reset a surface'])
|
|
164
167
|
expect(labels('erase').some((label) => /pick|colour|color|paint with/i.test(label))).toBe(false)
|
|
@@ -790,3 +790,56 @@ describe('generated floor plates: scopes per surface', () => {
|
|
|
790
790
|
expect(paintScopeRole(manual, 'surface', 'room')).toBe('surface')
|
|
791
791
|
})
|
|
792
792
|
})
|
|
793
|
+
|
|
794
|
+
describe('a surface that declares its own scopes', () => {
|
|
795
|
+
it('offers its scopes and labels and paints the role it names for the scope', () => {
|
|
796
|
+
const kind = 'paint-surface-scope-test'
|
|
797
|
+
if (!nodeRegistry.has(kind))
|
|
798
|
+
registerNode({
|
|
799
|
+
kind,
|
|
800
|
+
schemaVersion: 1,
|
|
801
|
+
schema: z.object({ type: z.literal(kind) }) as never,
|
|
802
|
+
category: 'structure',
|
|
803
|
+
defaults: () => ({}),
|
|
804
|
+
capabilities: {
|
|
805
|
+
slots: () => [{ slotId: 'end:1', label: 'End' }],
|
|
806
|
+
paint: {
|
|
807
|
+
resolveRole: () => null,
|
|
808
|
+
buildPatch: () => ({}),
|
|
809
|
+
applyPreview: () => null,
|
|
810
|
+
roleLabel: (_node: AnyNode, role: string) => (role === 'end:1' ? 'This end' : null),
|
|
811
|
+
scopes: (_node: AnyNode, role: string) =>
|
|
812
|
+
role === 'end:1'
|
|
813
|
+
? {
|
|
814
|
+
scopes: ['single', 'matching'],
|
|
815
|
+
labels: { matching: 'Every end' },
|
|
816
|
+
roleFor: (scope: PaintScope) => (scope === 'matching' ? 'ends:all' : role),
|
|
817
|
+
}
|
|
818
|
+
: null,
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
floorplanScope: 'level',
|
|
822
|
+
renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
|
|
823
|
+
} as unknown as AnyNodeDefinition)
|
|
824
|
+
const node = { id: 'scoped_1', type: kind } as unknown as AnyNode
|
|
825
|
+
const hover = paintHoverInfo(node, 'end:1', [])
|
|
826
|
+
expect(hover.scopes).toEqual(['single', 'matching'])
|
|
827
|
+
expect(paintScopeLabel('single', hover)).toBe('This end')
|
|
828
|
+
expect(paintScopeLabel('matching', hover)).toBe('Every end')
|
|
829
|
+
const targets = (scope: PaintScope) =>
|
|
830
|
+
resolvePaintScopeTargets({
|
|
831
|
+
node,
|
|
832
|
+
role: 'end:1',
|
|
833
|
+
scope,
|
|
834
|
+
nodes: { scoped_1: node },
|
|
835
|
+
spaces: {},
|
|
836
|
+
slotRolesOf: () => [],
|
|
837
|
+
}).map((target) => `${target.nodeId}:${target.role}`)
|
|
838
|
+
expect(targets('single')).toEqual(['scoped_1:end:1'])
|
|
839
|
+
expect(targets('matching')).toEqual(['scoped_1:ends:all'])
|
|
840
|
+
// a scope the surface does not offer paints the surface alone
|
|
841
|
+
expect(targets('room')).toEqual(['scoped_1:end:1'])
|
|
842
|
+
// other roles of the kind keep the derived set
|
|
843
|
+
expect(availablePaintScopes({ node, slotRoles: [], role: 'side' })).toEqual(['single'])
|
|
844
|
+
})
|
|
845
|
+
})
|
package/src/lib/paint-scope.ts
CHANGED
|
@@ -55,6 +55,11 @@ function nodeOffersRoomScope(node: AnyNode): boolean {
|
|
|
55
55
|
return nodeRegistry.get(node.type)?.capabilities?.paint?.roomScope === true
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** The scopes the hovered surface declares for itself (`capabilities.paint.scopes`), if any. */
|
|
59
|
+
function surfaceScopes(node: AnyNode, role: string) {
|
|
60
|
+
return nodeRegistry.get(node.type)?.capabilities?.paint?.scopes?.(node, role) ?? null
|
|
61
|
+
}
|
|
62
|
+
|
|
58
63
|
const isFloorPlate = (node: AnyNode): boolean => node.type === 'slab' && node.boundary === 'auto'
|
|
59
64
|
|
|
60
65
|
const FOOTPRINT_FACE_LABELS: Record<string, string> = {
|
|
@@ -104,6 +109,8 @@ export function availablePaintScopes(args: {
|
|
|
104
109
|
if (args.node.type === 'wall') return ['single', 'room']
|
|
105
110
|
const plate = args.role === undefined ? null : platePaintScopes(args.node, args.role)
|
|
106
111
|
if (plate) return plate.scopes
|
|
112
|
+
const own = args.role === undefined ? null : surfaceScopes(args.node, args.role)
|
|
113
|
+
if (own) return own.scopes
|
|
107
114
|
const scopes: PaintScope[] = ['single']
|
|
108
115
|
if (args.slotRoles.length > 1) scopes.push('object')
|
|
109
116
|
if (nodeHasAsset(args.node)) scopes.push('matching')
|
|
@@ -123,7 +130,7 @@ export function paintHoverInfo(node: AnyNode, role: string, slotRoles: string[])
|
|
|
123
130
|
scopes: availablePaintScopes({ node, slotRoles, role }),
|
|
124
131
|
slotLabel: paintSurfaceLabel(node, role),
|
|
125
132
|
nodeNoun: node.type,
|
|
126
|
-
labels: platePaintScopes(node, role)?.labels,
|
|
133
|
+
labels: platePaintScopes(node, role)?.labels ?? surfaceScopes(node, role)?.labels,
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
|
|
@@ -168,12 +175,13 @@ export function paintSurfaceLabel(node: AnyNode, role: string): string {
|
|
|
168
175
|
return slotDisplayLabel(node, role)
|
|
169
176
|
}
|
|
170
177
|
|
|
171
|
-
/** Display label for the hovered slot
|
|
178
|
+
/** Display label for the hovered slot, with a capability override for contextual copy. */
|
|
172
179
|
export function slotDisplayLabel(node: AnyNode, role: string): string {
|
|
173
180
|
const capabilities = nodeRegistry.get(node.type)?.capabilities
|
|
181
|
+
const roleLabel = capabilities?.paint?.roleLabel?.(node, role)
|
|
182
|
+
if (roleLabel) return roleLabel
|
|
174
183
|
const declared = capabilities?.slots?.(node)?.find((slot) => slot.slotId === role)
|
|
175
|
-
|
|
176
|
-
return capabilities?.paint?.roleLabel?.(node, role) ?? slotLabelFromId(role)
|
|
184
|
+
return declared?.label ?? slotLabelFromId(role)
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
// ── Fan-out resolution ──────────────────────────────────────────────────────
|
|
@@ -506,6 +514,12 @@ export function resolvePaintScopeTargets(args: {
|
|
|
506
514
|
.map((other) => ({ nodeId: other.id, role: stepRole }))
|
|
507
515
|
return targets.length ? targets : [{ nodeId: node.id as AnyNodeId, role: stepRole }]
|
|
508
516
|
}
|
|
517
|
+
// A surface with its own scope set paints the role it names for the scope.
|
|
518
|
+
const own = surfaceScopes(node, role)
|
|
519
|
+
if (own) {
|
|
520
|
+
const roleFor = own.roleFor?.(effectivePaintScope(scope, own.scopes)) ?? role
|
|
521
|
+
return [{ nodeId: node.id as AnyNodeId, role: roleFor }]
|
|
522
|
+
}
|
|
509
523
|
if (scope === 'single') return single
|
|
510
524
|
|
|
511
525
|
// Whole object: paint every slot of the clicked node. Generic across any
|
package/src/lib/panel-rows.ts
CHANGED
|
@@ -65,19 +65,28 @@ export const PANEL_MODEL_EXTENSION = 'pascal:editor/panel-model'
|
|
|
65
65
|
/**
|
|
66
66
|
* A control the multi-selection inspector renders for every selected node of
|
|
67
67
|
* a kind at once (derived settings the raw parametrics can't express). `value`
|
|
68
|
-
* null means the selection is mixed: no option shows as active.
|
|
68
|
+
* null means the selection is mixed: no option shows as active. `segmented`
|
|
69
|
+
* suits a few short options, `select` a longer list; `readout` only shows a
|
|
70
|
+
* value the selection cannot edit here.
|
|
69
71
|
*/
|
|
70
72
|
export type MultiPanelControl = {
|
|
71
73
|
id: string
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
/**
|
|
75
|
+
* The section the control sits in: a parametrics group title, or a section
|
|
76
|
+
* of its own, shown after the groups in the order controls name them.
|
|
77
|
+
*/
|
|
74
78
|
section: string
|
|
75
79
|
label: string
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
} & (
|
|
81
|
+
| {
|
|
82
|
+
kind: 'segmented' | 'select'
|
|
83
|
+
options: ReadonlyArray<{ value: string; label: string }>
|
|
84
|
+
value: string | null
|
|
85
|
+
/** Applies to every selected node in one undo step. */
|
|
86
|
+
onChange: (value: string) => void
|
|
87
|
+
}
|
|
88
|
+
| { kind: 'readout'; value: string; detail?: string }
|
|
89
|
+
)
|
|
81
90
|
|
|
82
91
|
export type NodePanelModel<N extends AnyNode = AnyNode> = {
|
|
83
92
|
rows: (context: {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { formatNumber, type GuideScaleReference } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
export type ReferenceScaleUnit = 'meters' | 'centimeters' | 'feet' | 'inches'
|
|
4
|
+
|
|
5
|
+
const MAX_REFERENCE_SCALE_LABEL_LENGTH = 64
|
|
6
|
+
|
|
7
|
+
/** Mirrors `GuideScaleReference.displayLength`'s `.max()` in core. */
|
|
8
|
+
export const MAX_REFERENCE_SCALE_DISPLAY_LENGTH = 1_000_000
|
|
9
|
+
|
|
10
|
+
export function isValidReferenceScaleDisplayLength(value: number): boolean {
|
|
11
|
+
return Number.isFinite(value) && value > 0 && value <= MAX_REFERENCE_SCALE_DISPLAY_LENGTH
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const REFERENCE_SCALE_UNIT_LABELS: Record<ReferenceScaleUnit, string> = {
|
|
15
|
+
meters: 'm',
|
|
16
|
+
centimeters: 'cm',
|
|
17
|
+
feet: 'ft',
|
|
18
|
+
inches: 'in',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatScaleValue(value: number, unit: string, locale: string): string {
|
|
22
|
+
return `${formatNumber(value, { locale, max: 2 })} ${unit}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getReferenceScaleUnitLabel(unit: unknown): string | null {
|
|
26
|
+
if (typeof unit !== 'string' || !Object.hasOwn(REFERENCE_SCALE_UNIT_LABELS, unit)) return null
|
|
27
|
+
return REFERENCE_SCALE_UNIT_LABELS[unit as ReferenceScaleUnit]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseLegacyScaleLabel(label: string): { unit: string; value: number } | null {
|
|
31
|
+
// `\s` covers NBSP and narrow NBSP; the whitespace run and digit group are
|
|
32
|
+
// disjoint, so each repetition has exactly one way to match.
|
|
33
|
+
const match = label.trim().match(/^([+−-]?[\d.,]+(?:\s+[\d.,]+)*)\s*(m|cm|ft|in)$/i)
|
|
34
|
+
if (!match) return null
|
|
35
|
+
|
|
36
|
+
const compact = match[1]!.replace(/\s/g, '').replace('−', '-')
|
|
37
|
+
const lastDot = compact.lastIndexOf('.')
|
|
38
|
+
const lastComma = compact.lastIndexOf(',')
|
|
39
|
+
const separatorIndex = Math.max(lastDot, lastComma)
|
|
40
|
+
const trailingDigits = separatorIndex === -1 ? 0 : compact.length - separatorIndex - 1
|
|
41
|
+
const decimalSeparator =
|
|
42
|
+
separatorIndex !== -1 && trailingDigits > 0 && trailingDigits <= 2
|
|
43
|
+
? compact[separatorIndex]
|
|
44
|
+
: null
|
|
45
|
+
const normalized = decimalSeparator
|
|
46
|
+
? `${compact.slice(0, separatorIndex).replace(/[.,]/g, '')}.${compact
|
|
47
|
+
.slice(separatorIndex + 1)
|
|
48
|
+
.replace(/[.,]/g, '')}`
|
|
49
|
+
: compact.replace(/[.,]/g, '')
|
|
50
|
+
|
|
51
|
+
if (!/^[+-]?\d+(?:\.\d+)?$/.test(normalized)) return null
|
|
52
|
+
const value = Number(normalized)
|
|
53
|
+
return Number.isFinite(value) ? { unit: match[2]!.toLowerCase(), value } : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createGuideScaleReference(
|
|
57
|
+
reference: Omit<GuideScaleReference, 'displayLength' | 'displayUnit' | 'label'>,
|
|
58
|
+
displayLength: number,
|
|
59
|
+
displayUnit: ReferenceScaleUnit,
|
|
60
|
+
): GuideScaleReference {
|
|
61
|
+
const unitLabel = REFERENCE_SCALE_UNIT_LABELS[displayUnit]
|
|
62
|
+
const label = formatScaleValue(displayLength, unitLabel, 'en')
|
|
63
|
+
if (!isValidReferenceScaleDisplayLength(displayLength)) return { ...reference, label }
|
|
64
|
+
return { ...reference, displayLength, displayUnit, label }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function formatReferenceScaleLabel(reference: GuideScaleReference, locale: string): string {
|
|
68
|
+
const unitLabel = getReferenceScaleUnitLabel(reference.displayUnit)
|
|
69
|
+
if (
|
|
70
|
+
typeof reference.displayLength === 'number' &&
|
|
71
|
+
Number.isFinite(reference.displayLength) &&
|
|
72
|
+
unitLabel
|
|
73
|
+
) {
|
|
74
|
+
return formatScaleValue(reference.displayLength, unitLabel, locale)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const label: unknown = reference.label
|
|
78
|
+
if (typeof label !== 'string') return typeof label === 'number' ? String(label) : ''
|
|
79
|
+
if (label.length > MAX_REFERENCE_SCALE_LABEL_LENGTH) return label
|
|
80
|
+
|
|
81
|
+
const parsed = parseLegacyScaleLabel(label)
|
|
82
|
+
return parsed ? formatScaleValue(parsed.value, parsed.unit, locale) : label
|
|
83
|
+
}
|