@pascal-app/editor 1.0.7 → 1.1.1
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 +9 -15
- package/src/components/editor/action-menu-button.tsx +1 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
- package/src/components/editor/custom-camera-controls.tsx +101 -45
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +20 -1983
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +5 -3
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +8 -4
- package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
- package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
- package/src/components/editor/floorplan-panel.tsx +199 -30
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/follow-pascal.test.tsx +1 -2
- package/src/components/editor/follow-pascal.tsx +1 -1
- package/src/components/editor/footprint-height-handle.tsx +2 -2
- package/src/components/editor/group-actions.ts +1 -2
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +1 -2
- package/src/components/editor/group-rotate-handle.tsx +1 -2
- package/src/components/editor/group-transform-shared.ts +3 -1
- package/src/components/editor/handles/use-handle-drag.ts +1 -2
- package/src/components/editor/index.tsx +43 -13
- package/src/components/editor/inspector-extension-actions.tsx +1 -1
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +2 -2
- package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/quick-measurement-card.tsx +1 -1
- package/src/components/editor/room-controls.tsx +10 -6
- package/src/components/editor/room-handles-2d.tsx +3 -3
- package/src/components/editor/room-handles.tsx +4 -5
- package/src/components/editor/room-highlight.tsx +6 -105
- package/src/components/editor/room-labels.test.ts +8 -822
- package/src/components/editor/room-labels.tsx +41 -1670
- package/src/components/editor/room-webgpu-materials.test.ts +1 -1
- package/src/components/editor/selection-manager.tsx +5 -3
- package/src/components/editor/site-edge-labels.tsx +7 -3
- package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
- package/src/components/editor/use-floorplan-background-placement.ts +2 -1
- package/src/components/editor/views/plugins-view.tsx +1 -1
- package/src/components/editor/views/view-bar.tsx +9 -9
- package/src/components/editor/wall-measurement-label.tsx +31 -15
- package/src/components/editor/wall-move-side-handles.tsx +11 -7
- package/src/components/editor/wall-pull-grip.tsx +2 -2
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +13 -13
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +2 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +23 -6
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +14 -3
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +30 -10
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +39 -35
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +33 -21
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +151 -33
- package/src/components/tools/item/move-tool.tsx +1 -6
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +3 -4
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +7 -3
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +39 -16
- package/src/components/tools/shared/cursor-sphere.tsx +1 -1
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +2 -6
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/tool-manager.tsx +1 -24
- package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -2
- package/src/components/ui/action-menu/control-modes.tsx +1 -1
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
- package/src/components/ui/action-menu/index.tsx +3 -3
- package/src/components/ui/action-menu/measurement-control.tsx +3 -3
- package/src/components/ui/action-menu/placement.ts +0 -18
- package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
- package/src/components/ui/action-menu/view-toggles.tsx +5 -5
- package/src/components/ui/controls/finish-swatch.tsx +1 -1
- package/src/components/ui/controls/material-paint-panel.tsx +2 -2
- package/src/components/ui/controls/scene-material-list.tsx +2 -2
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/slider-control.tsx +18 -6
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/floating-level-selector.tsx +3 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.test.tsx +54 -1
- package/src/components/ui/helpers/helper-manager.tsx +55 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
- package/src/components/ui/panels/floor-foundation-panel.tsx +3 -3
- package/src/components/ui/panels/floor-opening-panel.tsx +5 -5
- package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
- package/src/components/ui/panels/multi-height-mode.tsx +3 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +1 -1
- package/src/components/ui/panels/panel-manager.tsx +5 -3
- package/src/components/ui/panels/panel-wrapper.tsx +42 -19
- package/src/components/ui/panels/parametric-inspector.tsx +1 -1
- package/src/components/ui/panels/room-construction-rows.tsx +7 -7
- package/src/components/ui/panels/room-name-options.ts +1 -5
- package/src/components/ui/panels/room-openings-field.tsx +1 -1
- package/src/components/ui/panels/room-panel.tsx +7 -9
- package/src/components/ui/primitives/color-dot.tsx +2 -2
- package/src/components/ui/primitives/opacity-control.tsx +2 -2
- package/src/components/ui/primitives/sidebar.tsx +3 -3
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +22 -11
- package/src/components/ui/sidebar/icon-rail.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +2 -6
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +3 -10
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +2 -2
- package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
- package/src/components/viewer-overlay.tsx +1 -2
- package/src/hooks/use-auto-frame.ts +1 -2
- package/src/hooks/use-keyboard.ts +1 -2
- package/src/hooks/use-selected-room.ts +5 -7
- package/src/i18n/floor-footprint-name.ts +1 -1
- package/src/i18n/locales/en/messages.po +1036 -306
- package/src/i18n/locales/en/messages.ts +1 -1
- package/src/i18n/locales/en-XA/messages.ts +1 -1
- package/src/i18n/locales/es/messages.po +2267 -439
- package/src/i18n/locales/fr/messages.po +2267 -439
- package/src/i18n/locales/ja/messages.po +2267 -439
- package/src/i18n/locales/zh-CN/messages.po +2267 -439
- package/src/i18n/locales.test.ts +1 -1
- package/src/index.tsx +183 -178
- package/src/lib/below-grade.test.tsx +4 -4
- package/src/lib/camera-facing.test.ts +1 -1
- package/src/lib/camera-facing.ts +2 -3
- package/src/lib/contextual-help.ts +6 -0
- package/src/lib/editor-level-display.ts +1 -1
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +3 -3
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +8 -3
- package/src/lib/floor-opening-draft.ts +2 -2
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.ts +1 -1
- package/src/lib/floorplan/floorplan-export.test.ts +1 -1
- package/src/lib/floorplan/floorplan-export.tsx +8 -11
- package/src/lib/floorplan/floorplan-extension.ts +4 -157
- package/src/lib/floorplan/floorplan-mode.ts +2 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
- package/src/lib/floorplan/geometry.ts +2 -112
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +2 -1
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/floorplan/schedules.ts +6 -3
- package/src/lib/floorplan/sheet-export.ts +7 -3
- package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
- package/src/lib/follow-pascal-presentation.ts +1 -1
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/glb-export.ts +1 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/layer-thickness.ts +1 -1
- package/src/lib/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurements.test.ts +7 -5
- package/src/lib/mezzanine-selection.test.ts +1 -1
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/paint-region-mode.ts +1 -1
- package/src/lib/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/point-ask/session.ts +1 -2
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/roof-duplication.test.ts +100 -0
- package/src/lib/roof-duplication.ts +30 -5
- package/src/lib/room-built-on.test.ts +2 -5
- package/src/lib/room-built-on.ts +1 -15
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-controls-phase6.test.ts +1 -1
- package/src/lib/room-handle-drag.ts +8 -37
- package/src/lib/room-name-catalog.test.ts +2 -1
- package/src/lib/room-name-messages.test.ts +2 -1
- package/src/lib/room-name-messages.ts +1 -118
- package/src/lib/room-name-storage.test.ts +2 -1
- package/src/lib/room-push-dimensions.test.ts +1 -1
- package/src/lib/room-selection-commands.ts +2 -2
- package/src/lib/room-selection.test.ts +6 -6
- package/src/lib/room-transform-session.ts +1 -2
- package/src/lib/room-zone-routing.ts +2 -2
- package/src/lib/scene-clipboard.ts +2 -1
- package/src/lib/selection-routing.test.ts +0 -28
- package/src/lib/selection-routing.ts +1 -40
- package/src/lib/sfx-bus.ts +8 -32
- package/src/lib/sfx-player.test.ts +72 -1
- package/src/lib/sfx-player.ts +49 -337
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/viewer-selection.test.ts +38 -10
- package/src/lib/wall-pull.test.ts +1 -1
- package/src/lib/wall-pull.ts +1 -27
- package/src/lib/wall-region-list.ts +3 -1
- package/src/lib/window-interaction.test.ts +4 -2
- package/src/store/use-editor.tsx +22 -26
- package/src/store/use-floorplan-annotation-visibility.ts +5 -5
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/store/use-wall-region-handles-selection.ts +1 -2
- package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
- package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
- package/src/components/editor/first-person/build-collider-world.ts +0 -549
- package/src/components/editor/first-person/ground-holes.test.ts +0 -128
- package/src/components/editor/first-person/ground-holes.ts +0 -130
- package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
- package/src/components/editor/first-person/mechanism-targets.ts +0 -88
- package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
- package/src/components/editor/first-person/terrain-collider.ts +0 -212
- package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
- package/src/components/editor/floorplan-camera-sync.ts +0 -213
- package/src/components/editor/localized-html.tsx +0 -22
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
- package/src/components/editor-2d/svg-paths.ts +0 -124
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/components/ui/action-menu/action-button.tsx +0 -89
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
- package/src/components/ui/primitives/button.tsx +0 -69
- package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
- package/src/components/ui/primitives/popover.tsx +0 -42
- package/src/components/ui/primitives/switch.tsx +0 -29
- package/src/components/ui/primitives/tooltip.tsx +0 -57
- package/src/components/ui/scene-loader.tsx +0 -73
- package/src/components/viewer/floorplan-compass-button.tsx +0 -67
- package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
- package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
- package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
- package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
- package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
- package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
- package/src/components/viewer/floorplan-preview.tsx +0 -1065
- package/src/components/viewer/render-settings-menu.tsx +0 -294
- package/src/components/viewer/sky-control.tsx +0 -710
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
- package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
- package/src/components/viewer/viewer-controls-bar.tsx +0 -496
- package/src/components/viewer/viewer-hover-label.tsx +0 -42
- package/src/components/viewer/viewer-rooms.tsx +0 -248
- package/src/components/viewer/viewer-scene-header.tsx +0 -286
- package/src/components/viewer/viewer-selection-manager.tsx +0 -124
- package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
- package/src/components/viewer/viewer-stage-modes.ts +0 -49
- package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
- package/src/components/viewer/viewer-stage.test.tsx +0 -47
- package/src/components/viewer/viewer-stage.tsx +0 -221
- package/src/components/walkthrough-hud.tsx +0 -193
- package/src/hooks/use-mobile.ts +0 -19
- package/src/i18n/level-name.ts +0 -39
- package/src/i18n/scene-theme-name.ts +0 -16
- package/src/lib/below-grade.ts +0 -133
- package/src/lib/camera-pose.test.ts +0 -237
- package/src/lib/camera-pose.ts +0 -172
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
- package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
- package/src/lib/floorplan/floorplan-readonly.ts +0 -95
- package/src/lib/floorplan/marks.test.ts +0 -213
- package/src/lib/floorplan/marks.ts +0 -238
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/measurements.ts +0 -225
- package/src/lib/node-display-label.ts +0 -37
- package/src/lib/room-assembly-overlay.test.ts +0 -275
- package/src/lib/room-assembly-overlay.ts +0 -265
- package/src/lib/room-selection.ts +0 -417
- package/src/lib/scene-bounds.test.ts +0 -183
- package/src/lib/scene-bounds.ts +0 -169
- package/src/lib/typing-target.ts +0 -9
- package/src/lib/viewer-selection.ts +0 -595
- package/src/lib/window-interaction.ts +0 -142
- package/src/lib/zone-content.ts +0 -133
- package/src/store/camera-pose-store.test.ts +0 -29
- package/src/store/camera-pose-store.ts +0 -27
- package/src/store/navigation-sync-pose-store.test.ts +0 -34
- package/src/store/navigation-sync-pose-store.ts +0 -27
- package/src/store/use-audio.test.ts +0 -146
- package/src/store/use-audio.tsx +0 -169
- package/src/store/use-first-person-hud.ts +0 -40
- package/src/store/use-follow-pascal.test.ts +0 -62
- package/src/store/use-follow-pascal.ts +0 -53
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { LevelNode, WallNode } from '@pascal-app/core/schema'
|
|
3
|
-
import { buildFloorplanContext } from './floorplan-readonly'
|
|
4
|
-
|
|
5
|
-
const viewState = {
|
|
6
|
-
selected: false,
|
|
7
|
-
unit: 'metric' as const,
|
|
8
|
-
highlighted: false,
|
|
9
|
-
hovered: false,
|
|
10
|
-
moving: false,
|
|
11
|
-
palette: undefined,
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe('read-only floorplan context', () => {
|
|
15
|
-
test('does not invent siblings for parentless nodes', () => {
|
|
16
|
-
const first = LevelNode.parse({ id: 'level_first', type: 'level' })
|
|
17
|
-
const second = LevelNode.parse({ id: 'level_second', type: 'level' })
|
|
18
|
-
const nodes = { [first.id]: first, [second.id]: second }
|
|
19
|
-
|
|
20
|
-
expect(buildFloorplanContext(first, nodes, viewState).siblings).toEqual([])
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
test('resolves same-kind siblings from the parent child order', () => {
|
|
24
|
-
const first = WallNode.parse({
|
|
25
|
-
id: 'wall_first',
|
|
26
|
-
type: 'wall',
|
|
27
|
-
parentId: 'level_ground',
|
|
28
|
-
start: [0, 0],
|
|
29
|
-
end: [1, 0],
|
|
30
|
-
})
|
|
31
|
-
const second = WallNode.parse({
|
|
32
|
-
id: 'wall_second',
|
|
33
|
-
type: 'wall',
|
|
34
|
-
parentId: 'level_ground',
|
|
35
|
-
start: [1, 0],
|
|
36
|
-
end: [2, 0],
|
|
37
|
-
})
|
|
38
|
-
const level = LevelNode.parse({
|
|
39
|
-
id: 'level_ground',
|
|
40
|
-
type: 'level',
|
|
41
|
-
children: [first.id, second.id],
|
|
42
|
-
})
|
|
43
|
-
const nodes = { [level.id]: level, [first.id]: first, [second.id]: second }
|
|
44
|
-
|
|
45
|
-
expect(buildFloorplanContext(first, nodes, viewState).siblings).toEqual([second])
|
|
46
|
-
})
|
|
47
|
-
})
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import type { AnyNode, AnyNodeId, FloorplanPalette, GeometryContext } from '@pascal-app/core'
|
|
2
|
-
import {
|
|
3
|
-
createFloorplanContextExtensions,
|
|
4
|
-
type FloorplanWallDimensionFaces,
|
|
5
|
-
} from './floorplan-extension'
|
|
6
|
-
|
|
7
|
-
export type FloorplanViewState = {
|
|
8
|
-
automaticDimensions?: boolean
|
|
9
|
-
selected: boolean
|
|
10
|
-
unit: 'metric' | 'imperial'
|
|
11
|
-
metricNotation?: 'meters' | 'millimeters'
|
|
12
|
-
purpose?: 'edit' | 'document'
|
|
13
|
-
/** Sheet drafting conventions (see `FloorplanContextExtension.drafting`). */
|
|
14
|
-
drafting?: boolean
|
|
15
|
-
/** Room labels drawn as screen pills (see `FloorplanContextExtension.roomLabelOverlay`). */
|
|
16
|
-
roomLabelOverlay?: boolean
|
|
17
|
-
wallDimensionFaces?: FloorplanWallDimensionFaces
|
|
18
|
-
highlighted: boolean
|
|
19
|
-
hovered: boolean
|
|
20
|
-
moving: boolean
|
|
21
|
-
focusedUnitId?: string
|
|
22
|
-
focusedUnitMemberIds?: readonly string[]
|
|
23
|
-
palette: FloorplanPalette | undefined
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function buildFloorplanContext(
|
|
27
|
-
node: AnyNode,
|
|
28
|
-
nodes: Record<string, AnyNode>,
|
|
29
|
-
viewState: FloorplanViewState,
|
|
30
|
-
levelData?: unknown,
|
|
31
|
-
): GeometryContext {
|
|
32
|
-
const resolve = <N = AnyNode>(id: AnyNodeId): N | undefined => nodes[id] as N | undefined
|
|
33
|
-
const childIds = (node as { children?: AnyNodeId[] }).children
|
|
34
|
-
const children = Array.isArray(childIds)
|
|
35
|
-
? childIds.map((id) => nodes[id]).filter((child): child is AnyNode => child !== undefined)
|
|
36
|
-
: []
|
|
37
|
-
const parentId = node.parentId as AnyNodeId | null
|
|
38
|
-
const parent = parentId ? (nodes[parentId] ?? null) : null
|
|
39
|
-
let siblings: AnyNode[] = []
|
|
40
|
-
if (parent) {
|
|
41
|
-
const parentChildIds = (parent as { children?: AnyNodeId[] }).children
|
|
42
|
-
siblings = Array.isArray(parentChildIds)
|
|
43
|
-
? parentChildIds
|
|
44
|
-
.filter((id) => id !== node.id)
|
|
45
|
-
.map((id) => nodes[id])
|
|
46
|
-
.filter((sibling): sibling is AnyNode => sibling?.type === node.type)
|
|
47
|
-
: Object.values(nodes).filter(
|
|
48
|
-
(candidate) =>
|
|
49
|
-
candidate.id !== node.id &&
|
|
50
|
-
candidate.type === node.type &&
|
|
51
|
-
candidate.parentId === node.parentId,
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
resolve,
|
|
57
|
-
children,
|
|
58
|
-
siblings,
|
|
59
|
-
parent,
|
|
60
|
-
levelData,
|
|
61
|
-
sceneNodes: nodes,
|
|
62
|
-
extensions: createFloorplanContextExtensions({
|
|
63
|
-
automaticDimensions: viewState.automaticDimensions,
|
|
64
|
-
metricNotation: viewState.metricNotation ?? 'meters',
|
|
65
|
-
purpose: viewState.purpose ?? 'edit',
|
|
66
|
-
drafting: viewState.drafting === true,
|
|
67
|
-
roomLabelOverlay: viewState.roomLabelOverlay === true,
|
|
68
|
-
wallDimensionFaces: viewState.wallDimensionFaces,
|
|
69
|
-
}),
|
|
70
|
-
viewState: viewState.palette
|
|
71
|
-
? {
|
|
72
|
-
selected: viewState.selected,
|
|
73
|
-
unit: viewState.unit,
|
|
74
|
-
highlighted: viewState.highlighted,
|
|
75
|
-
hovered: viewState.hovered,
|
|
76
|
-
moving: viewState.moving,
|
|
77
|
-
focusedUnitId: viewState.focusedUnitId,
|
|
78
|
-
focusedUnitMemberIds: viewState.focusedUnitMemberIds,
|
|
79
|
-
palette: viewState.palette,
|
|
80
|
-
}
|
|
81
|
-
: undefined,
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function floorplanLayerRank(type: string): number {
|
|
86
|
-
switch (type) {
|
|
87
|
-
case 'zone':
|
|
88
|
-
return 0
|
|
89
|
-
case 'slab':
|
|
90
|
-
case 'ceiling':
|
|
91
|
-
return 1
|
|
92
|
-
default:
|
|
93
|
-
return 2
|
|
94
|
-
}
|
|
95
|
-
}
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import type { AnyNode, AnyNodeId } from '@pascal-app/core'
|
|
3
|
-
import { persistResolvedMarks, resolveMarkDetail, resolveMarks } from './marks'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Fixture: a rectangular level, walls running clockwise on screen from the
|
|
7
|
-
* north-west corner.
|
|
8
|
-
*
|
|
9
|
-
* north [0,0] → [6,0] doors d_n1 @1, d_n2 @4
|
|
10
|
-
* east [6,0] → [6,4] window w_e1 @2
|
|
11
|
-
* south [6,4] → [0,4] door d_s1 @2
|
|
12
|
-
* west [0,4] → [0,0] (interior partition, no openings)
|
|
13
|
-
*/
|
|
14
|
-
function fixture(levelOrdinal = 0): Record<string, AnyNode> {
|
|
15
|
-
const nodes: Record<string, AnyNode> = {}
|
|
16
|
-
const node = (value: Record<string, unknown>) => {
|
|
17
|
-
nodes[value.id as string] = {
|
|
18
|
-
object: 'node',
|
|
19
|
-
parentId: null,
|
|
20
|
-
visible: true,
|
|
21
|
-
metadata: {},
|
|
22
|
-
children: [],
|
|
23
|
-
...value,
|
|
24
|
-
} as unknown as AnyNode
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
node({
|
|
28
|
-
id: 'level_1',
|
|
29
|
-
type: 'level',
|
|
30
|
-
level: levelOrdinal,
|
|
31
|
-
children: ['w_n', 'w_e', 'w_s', 'w_w'],
|
|
32
|
-
})
|
|
33
|
-
node({
|
|
34
|
-
id: 'w_n',
|
|
35
|
-
type: 'wall',
|
|
36
|
-
parentId: 'level_1',
|
|
37
|
-
start: [0, 0],
|
|
38
|
-
end: [6, 0],
|
|
39
|
-
thickness: 0.15,
|
|
40
|
-
frontSide: 'exterior',
|
|
41
|
-
backSide: 'interior',
|
|
42
|
-
children: ['d_n1', 'd_n2'],
|
|
43
|
-
})
|
|
44
|
-
node({
|
|
45
|
-
id: 'w_e',
|
|
46
|
-
type: 'wall',
|
|
47
|
-
parentId: 'level_1',
|
|
48
|
-
start: [6, 0],
|
|
49
|
-
end: [6, 4],
|
|
50
|
-
thickness: 0.15,
|
|
51
|
-
frontSide: 'exterior',
|
|
52
|
-
backSide: 'interior',
|
|
53
|
-
children: ['win_e1'],
|
|
54
|
-
})
|
|
55
|
-
node({
|
|
56
|
-
id: 'w_s',
|
|
57
|
-
type: 'wall',
|
|
58
|
-
parentId: 'level_1',
|
|
59
|
-
start: [6, 4],
|
|
60
|
-
end: [0, 4],
|
|
61
|
-
thickness: 0.15,
|
|
62
|
-
frontSide: 'exterior',
|
|
63
|
-
backSide: 'interior',
|
|
64
|
-
children: ['d_s1'],
|
|
65
|
-
})
|
|
66
|
-
node({
|
|
67
|
-
id: 'w_w',
|
|
68
|
-
type: 'wall',
|
|
69
|
-
parentId: 'level_1',
|
|
70
|
-
start: [0, 4],
|
|
71
|
-
end: [0, 0],
|
|
72
|
-
thickness: 0.1,
|
|
73
|
-
frontSide: 'interior',
|
|
74
|
-
backSide: 'interior',
|
|
75
|
-
children: [],
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
const door = (id: string, wallId: string, along: number) =>
|
|
79
|
-
node({
|
|
80
|
-
id,
|
|
81
|
-
type: 'door',
|
|
82
|
-
parentId: wallId,
|
|
83
|
-
wallId,
|
|
84
|
-
width: 0.9,
|
|
85
|
-
height: 2.1,
|
|
86
|
-
position: [along, 1.05, 0],
|
|
87
|
-
})
|
|
88
|
-
door('d_n1', 'w_n', 1)
|
|
89
|
-
door('d_n2', 'w_n', 4)
|
|
90
|
-
door('d_s1', 'w_s', 2)
|
|
91
|
-
node({
|
|
92
|
-
id: 'win_e1',
|
|
93
|
-
type: 'window',
|
|
94
|
-
parentId: 'w_e',
|
|
95
|
-
wallId: 'w_e',
|
|
96
|
-
width: 1.2,
|
|
97
|
-
height: 1.2,
|
|
98
|
-
position: [2, 1.1, 0],
|
|
99
|
-
})
|
|
100
|
-
return nodes
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
describe('resolveMarks', () => {
|
|
104
|
-
test('doors are D101…, windows W101… on level 0', () => {
|
|
105
|
-
const marks = resolveMarks(fixture(), 'level_1' as AnyNodeId)
|
|
106
|
-
expect(marks.get('d_n1')).toBe('D101')
|
|
107
|
-
expect(marks.get('d_n2')).toBe('D102')
|
|
108
|
-
expect(marks.get('d_s1')).toBe('D103')
|
|
109
|
-
expect(marks.get('win_e1')).toBe('W101')
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
test('level ordinal shifts the base by 100', () => {
|
|
113
|
-
const marks = resolveMarks(fixture(1), 'level_1' as AnyNodeId)
|
|
114
|
-
expect(marks.get('d_n1')).toBe('D201')
|
|
115
|
-
expect(marks.get('win_e1')).toBe('W201')
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
test('openings are ordered clockwise from the north-west, exterior walls first', () => {
|
|
119
|
-
const nodes = fixture()
|
|
120
|
-
// Move the west partition's opening onto it to prove interior comes last.
|
|
121
|
-
;(nodes.w_w as unknown as { children: string[] }).children = ['d_w1']
|
|
122
|
-
nodes.d_w1 = {
|
|
123
|
-
object: 'node',
|
|
124
|
-
id: 'd_w1',
|
|
125
|
-
type: 'door',
|
|
126
|
-
parentId: 'w_w',
|
|
127
|
-
wallId: 'w_w',
|
|
128
|
-
visible: true,
|
|
129
|
-
metadata: {},
|
|
130
|
-
width: 0.8,
|
|
131
|
-
height: 2.1,
|
|
132
|
-
position: [2, 1.05, 0],
|
|
133
|
-
children: [],
|
|
134
|
-
} as unknown as AnyNode
|
|
135
|
-
const marks = resolveMarks(nodes, 'level_1' as AnyNodeId)
|
|
136
|
-
// North (exterior) first, then the other exterior walls, interior last.
|
|
137
|
-
expect(marks.get('d_w1')).toBe('D104')
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
test('an explicit mark wins and is never reassigned', () => {
|
|
141
|
-
const nodes = fixture()
|
|
142
|
-
;(nodes.d_n2 as unknown as { mark: string }).mark = 'D-GARAGE'
|
|
143
|
-
const marks = resolveMarks(nodes, 'level_1' as AnyNodeId)
|
|
144
|
-
expect(marks.get('d_n2')).toBe('D-GARAGE')
|
|
145
|
-
expect(marks.get('d_n1')).toBe('D101')
|
|
146
|
-
expect(marks.get('d_s1')).toBe('D102')
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
test('duplicate explicit marks are reported, not silently renumbered', () => {
|
|
150
|
-
const nodes = fixture()
|
|
151
|
-
;(nodes.d_n1 as unknown as { mark: string }).mark = 'D101'
|
|
152
|
-
;(nodes.d_n2 as unknown as { mark: string }).mark = 'D101'
|
|
153
|
-
const resolution = resolveMarkDetail(nodes, 'level_1' as AnyNodeId)
|
|
154
|
-
expect(resolution.issues).toEqual(['Duplicate door mark D101 (2 instances)'])
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
test('numbering is stable under insertion once persisted', () => {
|
|
158
|
-
const nodes = fixture()
|
|
159
|
-
const first = resolveMarkDetail(nodes, 'level_1' as AnyNodeId)
|
|
160
|
-
expect([...first.assignments.values()]).toEqual(['D101', 'D102', 'D103', 'W101'])
|
|
161
|
-
|
|
162
|
-
// Persist, the way an edit session does the first time it resolves.
|
|
163
|
-
const written = persistResolvedMarks(first, (id, data) => {
|
|
164
|
-
;(nodes[id] as unknown as Record<string, unknown>).mark = data.mark
|
|
165
|
-
})
|
|
166
|
-
expect(written).toBe(4)
|
|
167
|
-
|
|
168
|
-
// Insert a door BETWEEN d_n1 and d_n2 on the same wall.
|
|
169
|
-
;(nodes.w_n as unknown as { children: string[] }).children = ['d_n1', 'd_new', 'd_n2']
|
|
170
|
-
nodes.d_new = {
|
|
171
|
-
object: 'node',
|
|
172
|
-
id: 'd_new',
|
|
173
|
-
type: 'door',
|
|
174
|
-
parentId: 'w_n',
|
|
175
|
-
wallId: 'w_n',
|
|
176
|
-
visible: true,
|
|
177
|
-
metadata: {},
|
|
178
|
-
width: 0.8,
|
|
179
|
-
height: 2.1,
|
|
180
|
-
position: [2.5, 1.05, 0],
|
|
181
|
-
children: [],
|
|
182
|
-
} as unknown as AnyNode
|
|
183
|
-
|
|
184
|
-
const second = resolveMarkDetail(nodes, 'level_1' as AnyNodeId)
|
|
185
|
-
// Existing doors keep their marks; the new one takes the next free number.
|
|
186
|
-
expect(second.marks.get('d_n1')).toBe('D101')
|
|
187
|
-
expect(second.marks.get('d_n2')).toBe('D102')
|
|
188
|
-
expect(second.marks.get('d_s1')).toBe('D103')
|
|
189
|
-
expect(second.marks.get('d_new')).toBe('D104')
|
|
190
|
-
expect([...second.assignments.keys()]).toEqual(['d_new'])
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
test('WITHOUT persisting, inserting mid-wall renumbers — which is why we persist', () => {
|
|
194
|
-
const nodes = fixture()
|
|
195
|
-
;(nodes.w_n as unknown as { children: string[] }).children = ['d_n1', 'd_new', 'd_n2']
|
|
196
|
-
nodes.d_new = {
|
|
197
|
-
object: 'node',
|
|
198
|
-
id: 'd_new',
|
|
199
|
-
type: 'door',
|
|
200
|
-
parentId: 'w_n',
|
|
201
|
-
wallId: 'w_n',
|
|
202
|
-
visible: true,
|
|
203
|
-
metadata: {},
|
|
204
|
-
width: 0.8,
|
|
205
|
-
height: 2.1,
|
|
206
|
-
position: [2.5, 1.05, 0],
|
|
207
|
-
children: [],
|
|
208
|
-
} as unknown as AnyNode
|
|
209
|
-
const marks = resolveMarks(nodes, 'level_1' as AnyNodeId)
|
|
210
|
-
expect(marks.get('d_new')).toBe('D102')
|
|
211
|
-
expect(marks.get('d_n2')).toBe('D103')
|
|
212
|
-
})
|
|
213
|
-
})
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AnyNode,
|
|
3
|
-
AnyNodeId,
|
|
4
|
-
DoorNode,
|
|
5
|
-
LevelNode,
|
|
6
|
-
WallNode,
|
|
7
|
-
WindowNode,
|
|
8
|
-
} from '@pascal-app/core'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Deterministic door / window marks per level — WS3.
|
|
12
|
-
*
|
|
13
|
-
* `packages/core/src/schema/nodes/door.ts:75-79` promises "the deterministic
|
|
14
|
-
* level fallback (101, 102, ...)"; this is the implementation.
|
|
15
|
-
*
|
|
16
|
-
* Numbering
|
|
17
|
-
* ---------
|
|
18
|
-
* base = (level.level + 1) * 100 — level 0 → 100, level 1 → 200
|
|
19
|
-
* doors = `D${base + n}` → D101, D102 …
|
|
20
|
-
* windows= `W${base + n}` → W101, W102 …
|
|
21
|
-
*
|
|
22
|
-
* Order
|
|
23
|
-
* -----
|
|
24
|
-
* Openings are visited **clockwise** around the level, starting from the
|
|
25
|
-
* north-west-most exterior wall, exterior walls first and interior walls
|
|
26
|
-
* after. Clockwise is the ascending-angle sweep of the wall midpoint about
|
|
27
|
-
* the level centroid, anchored on the north-west diagonal (plan axes are
|
|
28
|
-
* x east, z south — z grows down the screen — so a screen-clockwise sweep
|
|
29
|
-
* is increasing atan2(z, x)). Openings on the same wall are ordered along
|
|
30
|
-
* the wall direction.
|
|
31
|
-
*
|
|
32
|
-
* Stability
|
|
33
|
-
* ---------
|
|
34
|
-
* An explicit `mark` on the node always wins and is never reassigned.
|
|
35
|
-
* `resolveMarks` also reports, in `assignments`, the marks it invented for
|
|
36
|
-
* nodes that had none — the caller is expected to PERSIST those back onto
|
|
37
|
-
* `node.mark` the first time they are resolved in an edit session
|
|
38
|
-
* (`persistResolvedMarks`). That is what makes numbering stable under
|
|
39
|
-
* insertion: once written, an existing opening keeps its number forever and
|
|
40
|
-
* a newly inserted opening only ever takes the next free number. Without
|
|
41
|
-
* the write-back, inserting a door in the middle of a wall would silently
|
|
42
|
-
* renumber every door after it.
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
export type OpeningMarkKind = 'door' | 'window'
|
|
46
|
-
|
|
47
|
-
export type MarkResolution = {
|
|
48
|
-
/** Every door and window on the level → its mark. */
|
|
49
|
-
marks: ReadonlyMap<string, string>
|
|
50
|
-
/** Only the marks that were invented (node.mark was empty). */
|
|
51
|
-
assignments: ReadonlyMap<string, string>
|
|
52
|
-
/** Duplicate explicit marks and other reportable defects. */
|
|
53
|
-
issues: readonly string[]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type MarkSceneInput = {
|
|
57
|
-
nodes: Readonly<Record<string, AnyNode>>
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Resolve marks for every door and window under `levelId`.
|
|
62
|
-
*
|
|
63
|
-
* `scene` may be a `{ nodes }` snapshot or the raw node map.
|
|
64
|
-
*/
|
|
65
|
-
export function resolveMarks(
|
|
66
|
-
scene: MarkSceneInput | Readonly<Record<string, AnyNode>>,
|
|
67
|
-
levelId: AnyNodeId,
|
|
68
|
-
): Map<string, string> {
|
|
69
|
-
return new Map(resolveMarkDetail(scene, levelId).marks)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function resolveMarkDetail(
|
|
73
|
-
scene: MarkSceneInput | Readonly<Record<string, AnyNode>>,
|
|
74
|
-
levelId: AnyNodeId,
|
|
75
|
-
): MarkResolution {
|
|
76
|
-
const nodes = normalizeNodes(scene)
|
|
77
|
-
const level = nodes[levelId]
|
|
78
|
-
const ordinal = level && level.type === 'level' ? ((level as LevelNode).level ?? 0) : 0
|
|
79
|
-
const base = (Math.max(0, ordinal) + 1) * 100
|
|
80
|
-
|
|
81
|
-
const openings = orderedOpenings(nodes, levelId)
|
|
82
|
-
const marks = new Map<string, string>()
|
|
83
|
-
const assignments = new Map<string, string>()
|
|
84
|
-
const issues: string[] = []
|
|
85
|
-
|
|
86
|
-
for (const kind of ['door', 'window'] as const) {
|
|
87
|
-
const prefix = kind === 'door' ? 'D' : 'W'
|
|
88
|
-
const list = openings.filter((opening) => opening.type === kind)
|
|
89
|
-
const used = new Set<string>()
|
|
90
|
-
const explicitOwners = new Map<string, string[]>()
|
|
91
|
-
|
|
92
|
-
for (const opening of list) {
|
|
93
|
-
const explicit = opening.mark?.trim()
|
|
94
|
-
if (!explicit) continue
|
|
95
|
-
marks.set(opening.id, explicit)
|
|
96
|
-
const normalized = explicit.toLocaleUpperCase()
|
|
97
|
-
used.add(normalized)
|
|
98
|
-
const owners = explicitOwners.get(normalized)
|
|
99
|
-
if (owners) owners.push(opening.id)
|
|
100
|
-
else explicitOwners.set(normalized, [opening.id])
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let sequence = 1
|
|
104
|
-
for (const opening of list) {
|
|
105
|
-
if (marks.has(opening.id)) continue
|
|
106
|
-
let candidate = `${prefix}${base + sequence}`
|
|
107
|
-
while (used.has(candidate.toLocaleUpperCase())) {
|
|
108
|
-
sequence++
|
|
109
|
-
candidate = `${prefix}${base + sequence}`
|
|
110
|
-
}
|
|
111
|
-
marks.set(opening.id, candidate)
|
|
112
|
-
assignments.set(opening.id, candidate)
|
|
113
|
-
used.add(candidate.toLocaleUpperCase())
|
|
114
|
-
sequence++
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
for (const [mark, owners] of explicitOwners) {
|
|
118
|
-
if (owners.length > 1) {
|
|
119
|
-
issues.push(`Duplicate ${kind} mark ${mark} (${owners.length} instances)`)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return { marks, assignments, issues }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Write invented marks back onto the nodes so numbering never drifts.
|
|
129
|
-
*
|
|
130
|
-
* Call once per level per edit session (the caller decides when — this
|
|
131
|
-
* module stays pure). `update` is typically `sceneApi.update`.
|
|
132
|
-
*/
|
|
133
|
-
export function persistResolvedMarks(
|
|
134
|
-
resolution: MarkResolution,
|
|
135
|
-
update: (id: AnyNodeId, data: Record<string, unknown>) => void,
|
|
136
|
-
): number {
|
|
137
|
-
let written = 0
|
|
138
|
-
for (const [id, mark] of resolution.assignments) {
|
|
139
|
-
update(id as AnyNodeId, { mark })
|
|
140
|
-
written++
|
|
141
|
-
}
|
|
142
|
-
return written
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ── Ordering ─────────────────────────────────────────────────────────
|
|
146
|
-
|
|
147
|
-
type Opening = (DoorNode | WindowNode) & { type: 'door' | 'window' }
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Every door and window under the level, clockwise from the NW-most
|
|
151
|
-
* exterior wall, exterior walls first.
|
|
152
|
-
*/
|
|
153
|
-
export function orderedOpenings(
|
|
154
|
-
nodes: Readonly<Record<string, AnyNode>>,
|
|
155
|
-
levelId: AnyNodeId,
|
|
156
|
-
): Opening[] {
|
|
157
|
-
const walls: WallNode[] = []
|
|
158
|
-
const orphanOpenings: Opening[] = []
|
|
159
|
-
|
|
160
|
-
const visit = (id: string) => {
|
|
161
|
-
const node = nodes[id]
|
|
162
|
-
if (!node) return
|
|
163
|
-
if (node.visible === false) return
|
|
164
|
-
if (node.type === 'wall') walls.push(node as WallNode)
|
|
165
|
-
else if (node.type === 'door' || node.type === 'window') {
|
|
166
|
-
const opening = node as Opening
|
|
167
|
-
if (!opening.wallId || !nodes[opening.wallId]) orphanOpenings.push(opening)
|
|
168
|
-
}
|
|
169
|
-
for (const childId of (node as { children?: string[] }).children ?? []) visit(childId)
|
|
170
|
-
}
|
|
171
|
-
visit(levelId)
|
|
172
|
-
|
|
173
|
-
const centroid = wallCentroid(walls)
|
|
174
|
-
const ordered = [...walls].sort((left, right) => {
|
|
175
|
-
const leftExterior = isExteriorWall(left) ? 0 : 1
|
|
176
|
-
const rightExterior = isExteriorWall(right) ? 0 : 1
|
|
177
|
-
if (leftExterior !== rightExterior) return leftExterior - rightExterior
|
|
178
|
-
return clockwiseKey(left, centroid) - clockwiseKey(right, centroid)
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
const result: Opening[] = []
|
|
182
|
-
for (const wall of ordered) {
|
|
183
|
-
const wallOpenings: Opening[] = []
|
|
184
|
-
for (const childId of wall.children ?? []) {
|
|
185
|
-
const child = nodes[childId]
|
|
186
|
-
if (child && child.visible !== false && (child.type === 'door' || child.type === 'window')) {
|
|
187
|
-
wallOpenings.push(child as Opening)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
wallOpenings.sort((left, right) => left.position[0] - right.position[0])
|
|
191
|
-
result.push(...wallOpenings)
|
|
192
|
-
}
|
|
193
|
-
result.push(...orphanOpenings)
|
|
194
|
-
return result
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function wallCentroid(walls: readonly WallNode[]): [number, number] {
|
|
198
|
-
if (walls.length === 0) return [0, 0]
|
|
199
|
-
let x = 0
|
|
200
|
-
let z = 0
|
|
201
|
-
for (const wall of walls) {
|
|
202
|
-
x += (wall.start[0] + wall.end[0]) / 2
|
|
203
|
-
z += (wall.start[1] + wall.end[1]) / 2
|
|
204
|
-
}
|
|
205
|
-
return [x / walls.length, z / walls.length]
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Sweep key: 0 at the north-west direction, increasing clockwise on screen.
|
|
210
|
-
*
|
|
211
|
-
* Plan axes are x east, z south — z grows DOWN the screen — so a clockwise
|
|
212
|
-
* screen sweep (up → right → down → left) is the INCREASING atan2(z, x)
|
|
213
|
-
* direction. The north-west diagonal sits at atan2(-1, -1) = -3π/4, so
|
|
214
|
-
* anchoring there and wrapping into [0, 2π) yields north, east, south, west.
|
|
215
|
-
*/
|
|
216
|
-
const NORTH_WEST_ANGLE = (-3 * Math.PI) / 4
|
|
217
|
-
|
|
218
|
-
function clockwiseKey(wall: WallNode, centroid: readonly [number, number]): number {
|
|
219
|
-
const midX = (wall.start[0] + wall.end[0]) / 2 - centroid[0]
|
|
220
|
-
const midZ = (wall.start[1] + wall.end[1]) / 2 - centroid[1]
|
|
221
|
-
let sweep = Math.atan2(midZ, midX) - NORTH_WEST_ANGLE
|
|
222
|
-
while (sweep < 0) sweep += Math.PI * 2
|
|
223
|
-
while (sweep >= Math.PI * 2) sweep -= Math.PI * 2
|
|
224
|
-
return sweep
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function isExteriorWall(wall: WallNode): boolean {
|
|
228
|
-
return wall.frontSide === 'exterior' || wall.backSide === 'exterior'
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function normalizeNodes(
|
|
232
|
-
scene: MarkSceneInput | Readonly<Record<string, AnyNode>>,
|
|
233
|
-
): Readonly<Record<string, AnyNode>> {
|
|
234
|
-
const candidate = scene as MarkSceneInput
|
|
235
|
-
return candidate && typeof candidate === 'object' && 'nodes' in candidate
|
|
236
|
-
? candidate.nodes
|
|
237
|
-
: (scene as Readonly<Record<string, AnyNode>>)
|
|
238
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { type MeasurementPoint, measurementCentroid, measurementNormal } from '@pascal-app/core'
|
|
2
|
-
import { ShapeUtils, Vector2, Vector3 } from 'three'
|
|
3
|
-
|
|
4
|
-
type MeasurementPolygonProjection = {
|
|
5
|
-
contour: Vector2[]
|
|
6
|
-
triangles: number[][]
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function projectMeasurementPolygon(
|
|
10
|
-
points: readonly MeasurementPoint[],
|
|
11
|
-
): MeasurementPolygonProjection | null {
|
|
12
|
-
const normalValue = measurementNormal(points)
|
|
13
|
-
const originValue = points[0]
|
|
14
|
-
if (!(normalValue && originValue)) return null
|
|
15
|
-
|
|
16
|
-
const normal = new Vector3(...normalValue)
|
|
17
|
-
const reference = Math.abs(normal.y) < 0.9 ? new Vector3(0, 1, 0) : new Vector3(1, 0, 0)
|
|
18
|
-
const tangent = new Vector3().crossVectors(reference, normal).normalize()
|
|
19
|
-
const bitangent = new Vector3().crossVectors(normal, tangent).normalize()
|
|
20
|
-
const origin = new Vector3(...originValue)
|
|
21
|
-
const contour = points.map((point) => {
|
|
22
|
-
const relative = new Vector3(...point).sub(origin)
|
|
23
|
-
return new Vector2(relative.dot(tangent), relative.dot(bitangent))
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
return { contour, triangles: ShapeUtils.triangulateShape(contour, []) }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function triangulateMeasurementPolygon(points: readonly MeasurementPoint[]): number[][] {
|
|
30
|
-
return projectMeasurementPolygon(points)?.triangles ?? []
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function measurementPolygonLabelAnchor(
|
|
34
|
-
points: readonly MeasurementPoint[],
|
|
35
|
-
): MeasurementPoint | null {
|
|
36
|
-
const projection = projectMeasurementPolygon(points)
|
|
37
|
-
if (!projection || projection.triangles.length === 0) return measurementCentroid(points)
|
|
38
|
-
|
|
39
|
-
let largestTriangle: number[] | null = null
|
|
40
|
-
let largestArea = Number.NEGATIVE_INFINITY
|
|
41
|
-
for (const triangle of projection.triangles) {
|
|
42
|
-
const [firstIndex, secondIndex, thirdIndex] = triangle
|
|
43
|
-
const first = firstIndex === undefined ? null : projection.contour[firstIndex]
|
|
44
|
-
const second = secondIndex === undefined ? null : projection.contour[secondIndex]
|
|
45
|
-
const third = thirdIndex === undefined ? null : projection.contour[thirdIndex]
|
|
46
|
-
if (!(first && second && third)) continue
|
|
47
|
-
const area = Math.abs(
|
|
48
|
-
(second.x - first.x) * (third.y - first.y) - (second.y - first.y) * (third.x - first.x),
|
|
49
|
-
)
|
|
50
|
-
if (area > largestArea) {
|
|
51
|
-
largestArea = area
|
|
52
|
-
largestTriangle = triangle
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (!largestTriangle) return measurementCentroid(points)
|
|
57
|
-
const [firstIndex, secondIndex, thirdIndex] = largestTriangle
|
|
58
|
-
const first = firstIndex === undefined ? null : points[firstIndex]
|
|
59
|
-
const second = secondIndex === undefined ? null : points[secondIndex]
|
|
60
|
-
const third = thirdIndex === undefined ? null : points[thirdIndex]
|
|
61
|
-
if (!(first && second && third)) return measurementCentroid(points)
|
|
62
|
-
|
|
63
|
-
return [
|
|
64
|
-
(first[0] + second[0] + third[0]) / 3,
|
|
65
|
-
(first[1] + second[1] + third[1]) / 3,
|
|
66
|
-
(first[2] + second[2] + third[2]) / 3,
|
|
67
|
-
]
|
|
68
|
-
}
|