@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,265 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AnyNode,
|
|
3
|
-
type AnyNodeId,
|
|
4
|
-
area,
|
|
5
|
-
getWallArcData,
|
|
6
|
-
getWallBaseElevationForNodes,
|
|
7
|
-
getWallCurveFrameAt,
|
|
8
|
-
getWallFaceOffsets,
|
|
9
|
-
getWallTop,
|
|
10
|
-
resolveCeilingHeight,
|
|
11
|
-
roomDrawnFloor,
|
|
12
|
-
type WallNode,
|
|
13
|
-
} from '@pascal-app/core'
|
|
14
|
-
import { ShapeUtils, Vector2 } from 'three'
|
|
15
|
-
import type { RoomSelectionGeometry, RoomSelectionRecord } from './room-selection'
|
|
16
|
-
|
|
17
|
-
type Point = readonly [number, number]
|
|
18
|
-
type Ring = readonly Point[]
|
|
19
|
-
|
|
20
|
-
/** Everything the assembly depends on besides the room's topology geometry. */
|
|
21
|
-
export type RoomAssemblyHeights = {
|
|
22
|
-
/** Level-local Y of the room's plate top (the level plane without a plate). */
|
|
23
|
-
floorY: number
|
|
24
|
-
/** Level-local Y of each boundary wall's top. */
|
|
25
|
-
wallTops: ReadonlyMap<string, number>
|
|
26
|
-
ceiling: { polygon: Ring; holes: readonly Ring[]; y: number } | null
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type RoomWallFacePortion = { wallId: string; face: 'a' | 'b'; from: Point; to: Point }
|
|
30
|
-
|
|
31
|
-
/** Flat level-local position buffers; the renderer only wraps them. */
|
|
32
|
-
export type RoomAssembly = {
|
|
33
|
-
floor: Float32Array
|
|
34
|
-
surfaces: Float32Array
|
|
35
|
-
outline: Float32Array
|
|
36
|
-
faces: RoomWallFacePortion[]
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const ON_FACE = 1e-3
|
|
40
|
-
const FLOOR_LIFT = 0.005
|
|
41
|
-
const CEILING_DROP = 0.02
|
|
42
|
-
const CURVE_SAMPLES = 24
|
|
43
|
-
const MIN_PIECE_AREA = 0.01
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Curved boundaries leave hairline slivers between the sampled room face and
|
|
47
|
-
* the sampled wall footprint; they would add stray face runs.
|
|
48
|
-
*/
|
|
49
|
-
function clearPieces(geometry: RoomSelectionGeometry) {
|
|
50
|
-
return geometry.clearPolygon.filter((piece) => area([piece]) >= MIN_PIECE_AREA)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function resolveRoomAssemblyHeights(
|
|
54
|
-
room: RoomSelectionRecord,
|
|
55
|
-
nodes: Readonly<Record<string, AnyNode>>,
|
|
56
|
-
): RoomAssemblyHeights {
|
|
57
|
-
const sceneNodes = nodes as Record<AnyNodeId, AnyNode>
|
|
58
|
-
const { levelId } = room.key
|
|
59
|
-
const zone = nodes[room.zoneId]
|
|
60
|
-
let floorY = 0
|
|
61
|
-
// A drawn slab floors the room even with its floor switched off.
|
|
62
|
-
if (zone?.type === 'zone' && (zone.hasFloor !== false || roomDrawnFloor(nodes, zone.id))) {
|
|
63
|
-
const level = nodes[levelId]
|
|
64
|
-
const plate =
|
|
65
|
-
level?.type === 'level'
|
|
66
|
-
? level.children
|
|
67
|
-
.map((id) => nodes[id])
|
|
68
|
-
.find((node) => node?.type === 'slab' && node.zoneIds?.includes(room.zoneId))
|
|
69
|
-
: undefined
|
|
70
|
-
const drawn = plate ? null : roomDrawnFloor(nodes, room.zoneId)
|
|
71
|
-
const linked =
|
|
72
|
-
plate ?? (drawn ? nodes[drawn.slabId] : room.slabId ? nodes[room.slabId] : undefined)
|
|
73
|
-
floorY = (linked?.type === 'slab' ? linked.elevation : zone.floor?.elevation) ?? 0
|
|
74
|
-
}
|
|
75
|
-
const wallTops = new Map<string, number>()
|
|
76
|
-
for (const id of room.boundaryWallIds) {
|
|
77
|
-
const wall = nodes[id]
|
|
78
|
-
if (wall?.type !== 'wall') continue
|
|
79
|
-
const base = getWallBaseElevationForNodes(wall, sceneNodes)
|
|
80
|
-
wallTops.set(id, getWallTop(wall, levelId, sceneNodes, base))
|
|
81
|
-
}
|
|
82
|
-
const ceilingNode = room.ceilingId ? nodes[room.ceilingId] : undefined
|
|
83
|
-
const ceiling =
|
|
84
|
-
ceilingNode?.type === 'ceiling'
|
|
85
|
-
? {
|
|
86
|
-
polygon: ceilingNode.polygon,
|
|
87
|
-
holes: ceilingNode.holes,
|
|
88
|
-
y: resolveCeilingHeight(ceilingNode, sceneNodes),
|
|
89
|
-
}
|
|
90
|
-
: null
|
|
91
|
-
return { floorY, wallTops, ceiling }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function heightsKey({ floorY, wallTops, ceiling }: RoomAssemblyHeights) {
|
|
95
|
-
const rings = ceiling ? [ceiling.polygon, ...ceiling.holes].map((ring) => ring.join(' ')) : []
|
|
96
|
-
return [floorY, ...wallTops, ceiling?.y, ...rings].join('|')
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const cache = new WeakMap<RoomSelectionGeometry, { key: string; assembly: RoomAssembly }>()
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* The room geometry object is reused for as long as the level topology
|
|
103
|
-
* revision stands, so it keys the cache; heights and the ceiling outline are
|
|
104
|
-
* compared by value. Hover changes and pointer moves only ever hit.
|
|
105
|
-
*/
|
|
106
|
-
export function getRoomAssembly(
|
|
107
|
-
geometry: RoomSelectionGeometry,
|
|
108
|
-
heights: RoomAssemblyHeights,
|
|
109
|
-
): RoomAssembly {
|
|
110
|
-
const key = heightsKey(heights)
|
|
111
|
-
const cached = cache.get(geometry)
|
|
112
|
-
if (cached?.key === key) return cached.assembly
|
|
113
|
-
const assembly = buildRoomAssembly(geometry, heights)
|
|
114
|
-
cache.set(geometry, { key, assembly })
|
|
115
|
-
return assembly
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function faceTest(wall: WallNode, face: 'a' | 'b') {
|
|
119
|
-
const offset = getWallFaceOffsets(wall)[face]
|
|
120
|
-
const arc = getWallArcData(wall)
|
|
121
|
-
if (arc) {
|
|
122
|
-
const radius = arc.radius - arc.direction * offset
|
|
123
|
-
return ([x, z]: Point) =>
|
|
124
|
-
Math.abs(Math.hypot(x - arc.center.x, z - arc.center.y) - radius) < ON_FACE
|
|
125
|
-
}
|
|
126
|
-
const dx = wall.end[0] - wall.start[0]
|
|
127
|
-
const dz = wall.end[1] - wall.start[1]
|
|
128
|
-
const length = Math.hypot(dx, dz)
|
|
129
|
-
if (length < 1e-9) return () => false
|
|
130
|
-
return ([x, z]: Point) =>
|
|
131
|
-
Math.abs(((x - wall.start[0]) * -dz + (z - wall.start[1]) * dx) / length - offset) < ON_FACE
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function* ringEdges(ring: Ring) {
|
|
135
|
-
for (let i = 0; i < ring.length; i++) yield [ring[i]!, ring[(i + 1) % ring.length]!] as const
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function overlap(p: Point, q: Point, a: Point, b: Point): [Point, Point] | null {
|
|
139
|
-
const ex = b[0] - a[0]
|
|
140
|
-
const ez = b[1] - a[1]
|
|
141
|
-
const lengthSq = ex * ex + ez * ez
|
|
142
|
-
if (lengthSq < 1e-12) return null
|
|
143
|
-
const length = Math.sqrt(lengthSq)
|
|
144
|
-
const offLine = ([x, z]: Point) => Math.abs((x - a[0]) * ez - (z - a[1]) * ex) / length
|
|
145
|
-
if (offLine(p) > ON_FACE || offLine(q) > ON_FACE) return null
|
|
146
|
-
const up = ((p[0] - a[0]) * ex + (p[1] - a[1]) * ez) / lengthSq
|
|
147
|
-
const uq = ((q[0] - a[0]) * ex + (q[1] - a[1]) * ez) / lengthSq
|
|
148
|
-
const lo = Math.max(0, Math.min(up, uq))
|
|
149
|
-
const hi = Math.min(1, Math.max(up, uq))
|
|
150
|
-
if ((hi - lo) * length < 1e-4) return null
|
|
151
|
-
return [
|
|
152
|
-
[a[0] + ex * lo, a[1] + ez * lo],
|
|
153
|
-
[a[0] + ex * hi, a[1] + ez * hi],
|
|
154
|
-
]
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* The room-facing part of each boundary wall face. The clear polygon's
|
|
159
|
-
* boundary is exactly the mitered faces the room sees, clipped by T-stems
|
|
160
|
-
* and separators, so its edges are matched against each wall's footprint
|
|
161
|
-
* face edges; that also keeps collinear walls apart.
|
|
162
|
-
*/
|
|
163
|
-
export function roomWallFaces(geometry: RoomSelectionGeometry): RoomWallFacePortion[] {
|
|
164
|
-
const rings = clearPieces(geometry).flatMap(({ outer, holes }) => [outer, ...holes])
|
|
165
|
-
const faces: RoomWallFacePortion[] = []
|
|
166
|
-
const seen = new Set<string>()
|
|
167
|
-
for (const span of geometry.spans) {
|
|
168
|
-
if (span.kind !== 'wall') continue
|
|
169
|
-
const wallId = span.boundaryId
|
|
170
|
-
const wall = geometry.context.walls.get(wallId)
|
|
171
|
-
if (!wall) continue
|
|
172
|
-
if (!rings.length) {
|
|
173
|
-
faces.push(...sampledFace(wall, span.face, span.t0, span.t1))
|
|
174
|
-
continue
|
|
175
|
-
}
|
|
176
|
-
const key = `${wallId}:${span.face}`
|
|
177
|
-
const footprint = geometry.context.wallFootprints.get(wallId)
|
|
178
|
-
if (seen.has(key) || !footprint) continue
|
|
179
|
-
seen.add(key)
|
|
180
|
-
const onFace = faceTest(wall, span.face)
|
|
181
|
-
const faceEdges = [...ringEdges(footprint)].filter(([a, b]) => onFace(a) && onFace(b))
|
|
182
|
-
for (const ring of rings) {
|
|
183
|
-
for (const [p, q] of ringEdges(ring)) {
|
|
184
|
-
if (!(onFace(p) && onFace(q))) continue
|
|
185
|
-
for (const [a, b] of faceEdges) {
|
|
186
|
-
const portion = overlap(p, q, a, b)
|
|
187
|
-
if (portion) faces.push({ wallId, face: span.face, from: portion[0], to: portion[1] })
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return faces
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Only used when the clear polygon could not be computed for the room.
|
|
196
|
-
function sampledFace(wall: WallNode, face: 'a' | 'b', t0: number, t1: number) {
|
|
197
|
-
const offset = getWallFaceOffsets(wall)[face]
|
|
198
|
-
const steps = getWallArcData(wall) ? CURVE_SAMPLES : 1
|
|
199
|
-
const points = Array.from({ length: steps + 1 }, (_, i): Point => {
|
|
200
|
-
const { point, normal } = getWallCurveFrameAt(wall, t0 + ((t1 - t0) * i) / steps)
|
|
201
|
-
return [point.x + normal.x * offset, point.y + normal.y * offset]
|
|
202
|
-
})
|
|
203
|
-
return points
|
|
204
|
-
.slice(1)
|
|
205
|
-
.map((to, i): RoomWallFacePortion => ({ wallId: wall.id, face, from: points[i]!, to }))
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function pushTriangles(target: number[], outer: Ring, holes: readonly Ring[], y: number) {
|
|
209
|
-
const contour = outer.map(([x, z]) => new Vector2(x, z))
|
|
210
|
-
const holePoints = holes.map((ring) => ring.map(([x, z]) => new Vector2(x, z)))
|
|
211
|
-
const points = [...contour, ...holePoints.flat()]
|
|
212
|
-
for (const triangle of ShapeUtils.triangulateShape(contour, holePoints)) {
|
|
213
|
-
for (const index of triangle) target.push(points[index]!.x, y, points[index]!.y)
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function pushRing(target: number[], ring: Ring, y: number) {
|
|
218
|
-
for (const [[ax, az], [bx, bz]] of ringEdges(ring)) target.push(ax, y, az, bx, y, bz)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function buildRoomAssembly(
|
|
222
|
-
geometry: RoomSelectionGeometry,
|
|
223
|
-
heights: RoomAssemblyHeights,
|
|
224
|
-
): RoomAssembly {
|
|
225
|
-
const floor: number[] = []
|
|
226
|
-
const surfaces: number[] = []
|
|
227
|
-
const outline: number[] = []
|
|
228
|
-
const floorY = heights.floorY + FLOOR_LIFT
|
|
229
|
-
for (const { outer, holes } of clearPieces(geometry)) {
|
|
230
|
-
pushTriangles(floor, outer, holes, floorY)
|
|
231
|
-
for (const ring of [outer, ...holes]) pushRing(outline, ring, floorY)
|
|
232
|
-
}
|
|
233
|
-
const faces = roomWallFaces(geometry)
|
|
234
|
-
const ends = new Map<string, { point: Point; top: number; count: number }>()
|
|
235
|
-
for (const { wallId, from, to } of faces) {
|
|
236
|
-
const top = heights.wallTops.get(wallId)
|
|
237
|
-
if (top === undefined || top <= floorY) continue
|
|
238
|
-
const [ax, az] = from
|
|
239
|
-
const [bx, bz] = to
|
|
240
|
-
surfaces.push(ax, floorY, az, bx, floorY, bz, bx, top, bz)
|
|
241
|
-
surfaces.push(ax, floorY, az, bx, top, bz, ax, top, az)
|
|
242
|
-
outline.push(ax, top, az, bx, top, bz)
|
|
243
|
-
for (const point of [from, to]) {
|
|
244
|
-
const key = `${wallId}:${point[0].toFixed(4)},${point[1].toFixed(4)}`
|
|
245
|
-
const end = ends.get(key)
|
|
246
|
-
if (end) end.count++
|
|
247
|
-
else ends.set(key, { point, top, count: 1 })
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// Verticals only where a face run starts or stops, not between curve chords.
|
|
251
|
-
for (const { point, top, count } of ends.values()) {
|
|
252
|
-
if (count === 1) outline.push(point[0], floorY, point[1], point[0], top, point[1])
|
|
253
|
-
}
|
|
254
|
-
if (heights.ceiling) {
|
|
255
|
-
const { polygon, holes, y } = heights.ceiling
|
|
256
|
-
pushTriangles(surfaces, polygon, holes, y - CEILING_DROP)
|
|
257
|
-
for (const ring of [polygon, ...holes]) pushRing(outline, ring, y - CEILING_DROP)
|
|
258
|
-
}
|
|
259
|
-
return {
|
|
260
|
-
floor: new Float32Array(floor),
|
|
261
|
-
surfaces: new Float32Array(surfaces),
|
|
262
|
-
outline: new Float32Array(outline),
|
|
263
|
-
faces,
|
|
264
|
-
}
|
|
265
|
-
}
|
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AnyNode,
|
|
3
|
-
area,
|
|
4
|
-
type BoundaryNode,
|
|
5
|
-
type BoundarySpan,
|
|
6
|
-
containsPoint,
|
|
7
|
-
createRoomTopologyIndex,
|
|
8
|
-
getWallArcData,
|
|
9
|
-
getWallFaceOffsets,
|
|
10
|
-
type MultiPolygon,
|
|
11
|
-
roomClearPolygon,
|
|
12
|
-
type TopologyRoom,
|
|
13
|
-
type WallNode,
|
|
14
|
-
type ZoneNode,
|
|
15
|
-
} from '@pascal-app/core'
|
|
16
|
-
import type { SelectionModifierKeys } from './selection-routing'
|
|
17
|
-
import { polygonMatchesZoneFootprint } from './zone-content'
|
|
18
|
-
|
|
19
|
-
export type RoomKey = { levelId: string; zoneId: string }
|
|
20
|
-
|
|
21
|
-
export function sameRoom(a: RoomKey | null, b: RoomKey | null): boolean {
|
|
22
|
-
return a === b || (!!a && !!b && a.levelId === b.levelId && a.zoneId === b.zoneId)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function shouldSelectRoom(
|
|
26
|
-
selected: RoomKey | null,
|
|
27
|
-
hit: RoomKey | null,
|
|
28
|
-
modifiers: SelectionModifierKeys,
|
|
29
|
-
): boolean {
|
|
30
|
-
return (
|
|
31
|
-
!!hit &&
|
|
32
|
-
!sameRoom(selected, hit) &&
|
|
33
|
-
!(modifiers.alt || modifiers.shift || modifiers.ctrl || modifiers.meta)
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type RoomSelectionGeometry = TopologyRoom & {
|
|
38
|
-
key: RoomKey
|
|
39
|
-
boundaryWallIds: string[]
|
|
40
|
-
clearPolygon: ReturnType<typeof roomClearPolygon>
|
|
41
|
-
area: number
|
|
42
|
-
hitPolygon: MultiPolygon
|
|
43
|
-
/** A stacked open-below room (`floor.support: 'open'`) inside its host room. */
|
|
44
|
-
mezzanine: { hostZoneId: string } | null
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export type RoomSelectionRecord = RoomSelectionGeometry & {
|
|
48
|
-
geometry: RoomSelectionGeometry
|
|
49
|
-
name: string
|
|
50
|
-
zoneId: string
|
|
51
|
-
slabId: string | null
|
|
52
|
-
slabName: string | null
|
|
53
|
-
ceilingId: string | null
|
|
54
|
-
ceilingName: string | null
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
type Nodes = Record<string, AnyNode>
|
|
58
|
-
|
|
59
|
-
function describeRoomGeometry(
|
|
60
|
-
levelId: string,
|
|
61
|
-
room: TopologyRoom,
|
|
62
|
-
zone: ZoneNode,
|
|
63
|
-
): RoomSelectionGeometry {
|
|
64
|
-
const clearPolygon = roomClearPolygon(room)
|
|
65
|
-
return {
|
|
66
|
-
...room,
|
|
67
|
-
key: { levelId, zoneId: zone.id },
|
|
68
|
-
boundaryWallIds: [
|
|
69
|
-
...new Set(room.spans.filter((span) => span.kind === 'wall').map((span) => span.boundaryId)),
|
|
70
|
-
],
|
|
71
|
-
clearPolygon,
|
|
72
|
-
area: area([{ outer: zone.polygon, holes: zone.holes }]),
|
|
73
|
-
hitPolygon: [{ outer: room.polygon, holes: room.holes }],
|
|
74
|
-
mezzanine: null,
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function isMezzanineZone(node: AnyNode | undefined): node is ZoneNode {
|
|
79
|
-
return node?.type === 'zone' && node.floor?.support === 'open'
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* A mezzanine as a selectable room. It has no walls of its own: its outline is
|
|
84
|
-
* the zone's polygon, its floor the zone's own plate, and it bounds nothing.
|
|
85
|
-
*/
|
|
86
|
-
function describeMezzanineGeometry(
|
|
87
|
-
levelId: string,
|
|
88
|
-
zone: ZoneNode,
|
|
89
|
-
context: TopologyRoom['context'],
|
|
90
|
-
): RoomSelectionGeometry {
|
|
91
|
-
const polygon = zone.polygon.map(([x, z]) => [x, z] as [number, number])
|
|
92
|
-
const holes = (zone.holes ?? []).map((ring) => ring.map(([x, z]) => [x, z] as [number, number]))
|
|
93
|
-
const outline = [{ outer: polygon, holes }]
|
|
94
|
-
return {
|
|
95
|
-
id: `mezzanine:${zone.id}`,
|
|
96
|
-
polygon,
|
|
97
|
-
holes,
|
|
98
|
-
spans: [],
|
|
99
|
-
context,
|
|
100
|
-
key: { levelId, zoneId: zone.id },
|
|
101
|
-
boundaryWallIds: [],
|
|
102
|
-
clearPolygon: outline,
|
|
103
|
-
area: area(outline),
|
|
104
|
-
hitPolygon: outline,
|
|
105
|
-
mezzanine: { hostZoneId: zone.hostZoneId ?? '' },
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const NO_WALLS: TopologyRoom['context'] = {
|
|
110
|
-
revision: -1,
|
|
111
|
-
walls: new Map(),
|
|
112
|
-
wallFootprints: new Map(),
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* A zone that bounds no room (a drawn area), outlined the way a room is: its
|
|
117
|
-
* polygon as the floor, no walls of its own.
|
|
118
|
-
*/
|
|
119
|
-
export function describeZoneOutline(levelId: string, zone: ZoneNode): RoomSelectionRecord {
|
|
120
|
-
const geometry: RoomSelectionGeometry = {
|
|
121
|
-
...describeMezzanineGeometry(levelId, zone, NO_WALLS),
|
|
122
|
-
id: `zone:${zone.id}`,
|
|
123
|
-
mezzanine: null,
|
|
124
|
-
}
|
|
125
|
-
return {
|
|
126
|
-
...geometry,
|
|
127
|
-
geometry,
|
|
128
|
-
name: zone.name || 'Zone',
|
|
129
|
-
zoneId: zone.id,
|
|
130
|
-
slabId: null,
|
|
131
|
-
slabName: null,
|
|
132
|
-
ceilingId: null,
|
|
133
|
-
ceilingName: null,
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function describeRoom(
|
|
138
|
-
geometry: RoomSelectionGeometry,
|
|
139
|
-
candidates: readonly AnyNode[],
|
|
140
|
-
): RoomSelectionRecord {
|
|
141
|
-
const { clearPolygon } = geometry
|
|
142
|
-
const matches = (node: AnyNode) =>
|
|
143
|
-
(node.type === 'zone' || node.type === 'slab' || node.type === 'ceiling') &&
|
|
144
|
-
(polygonMatchesZoneFootprint(node.polygon, geometry.polygon) ||
|
|
145
|
-
clearPolygon.some(({ outer }) => polygonMatchesZoneFootprint(node.polygon, outer)))
|
|
146
|
-
const slab = geometry.mezzanine
|
|
147
|
-
? candidates.find(
|
|
148
|
-
(node) =>
|
|
149
|
-
node.type === 'slab' &&
|
|
150
|
-
node.support === 'open' &&
|
|
151
|
-
!!node.zoneIds?.includes(geometry.key.zoneId),
|
|
152
|
-
)
|
|
153
|
-
: candidates.find(
|
|
154
|
-
(node) =>
|
|
155
|
-
node.type === 'slab' && node.support !== 'open' && node.autoFromWalls && matches(node),
|
|
156
|
-
)
|
|
157
|
-
const ceiling = candidates.find(
|
|
158
|
-
(node) => node.type === 'ceiling' && node.zoneId === geometry.key.zoneId,
|
|
159
|
-
)
|
|
160
|
-
const zone = candidates.find(
|
|
161
|
-
(node): node is ZoneNode => node.type === 'zone' && node.id === geometry.key.zoneId,
|
|
162
|
-
)!
|
|
163
|
-
return {
|
|
164
|
-
...geometry,
|
|
165
|
-
geometry,
|
|
166
|
-
name: zone.name || 'Room',
|
|
167
|
-
area: area([{ outer: zone.polygon, holes: zone.holes }]),
|
|
168
|
-
zoneId: zone.id,
|
|
169
|
-
slabId: slab?.id ?? null,
|
|
170
|
-
slabName: slab ? slab.name || 'Slab' : null,
|
|
171
|
-
ceilingId: ceiling?.id ?? null,
|
|
172
|
-
ceilingName: ceiling ? ceiling.name || 'Ceiling' : null,
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
type RoomHitIndex = Pick<
|
|
177
|
-
ReturnType<typeof createRoomTopologyIndex>,
|
|
178
|
-
'roomAtPoint' | 'roomForWallHit' | 'spansForWall'
|
|
179
|
-
> & {
|
|
180
|
-
/** The selectable room a zone is (a mezzanine's own plate or ceiling points at it). */
|
|
181
|
-
roomForZone?: (levelId: string, zoneId: string) => TopologyRoom | null
|
|
182
|
-
}
|
|
183
|
-
const wallFrames = new WeakMap<WallNode, ReturnType<typeof wallHitFrame>>()
|
|
184
|
-
|
|
185
|
-
function wallHitFrame(wall: WallNode) {
|
|
186
|
-
const dx = wall.end[0] - wall.start[0]
|
|
187
|
-
const dz = wall.end[1] - wall.start[1]
|
|
188
|
-
const length = Math.hypot(dx, dz)
|
|
189
|
-
const offsets = getWallFaceOffsets(wall)
|
|
190
|
-
return {
|
|
191
|
-
dx,
|
|
192
|
-
dz,
|
|
193
|
-
lengthSq: length * length,
|
|
194
|
-
nx: -dz / length,
|
|
195
|
-
nz: dx / length,
|
|
196
|
-
arc: getWallArcData(wall),
|
|
197
|
-
center: (offsets.a + offsets.b) / 2,
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function resolveRoomHit<T extends RoomHitIndex>(
|
|
202
|
-
index: T,
|
|
203
|
-
levelId: string,
|
|
204
|
-
node: AnyNode | null,
|
|
205
|
-
point: [number, number],
|
|
206
|
-
view?: '2d' | '3d',
|
|
207
|
-
): ReturnType<T['roomAtPoint']>
|
|
208
|
-
export function resolveRoomHit(
|
|
209
|
-
index: RoomHitIndex,
|
|
210
|
-
levelId: string,
|
|
211
|
-
node: AnyNode | null,
|
|
212
|
-
point: [number, number],
|
|
213
|
-
view: '2d' | '3d' = '2d',
|
|
214
|
-
): TopologyRoom | null {
|
|
215
|
-
// A mezzanine's plate (railing included) and ceiling name their own room; the
|
|
216
|
-
// host floor under it and every other surface resolve by the point, which
|
|
217
|
-
// never lands in a mezzanine.
|
|
218
|
-
if (node?.type === 'slab' && node.support === 'open')
|
|
219
|
-
return index.roomForZone?.(levelId, node.zoneIds?.[0] ?? '') ?? null
|
|
220
|
-
if (node?.type === 'ceiling' && node.zoneId) {
|
|
221
|
-
const own = index.roomForZone?.(levelId, node.zoneId)
|
|
222
|
-
if (own) return own
|
|
223
|
-
}
|
|
224
|
-
if (!node || node.type === 'slab' || node.type === 'ceiling')
|
|
225
|
-
return index.roomAtPoint(levelId, point)
|
|
226
|
-
if (node.type !== 'wall') return null
|
|
227
|
-
let frame = wallFrames.get(node)
|
|
228
|
-
if (!frame) {
|
|
229
|
-
frame = wallHitFrame(node)
|
|
230
|
-
wallFrames.set(node, frame)
|
|
231
|
-
}
|
|
232
|
-
const { dx, dz, lengthSq, arc } = frame
|
|
233
|
-
if (!lengthSq) return null
|
|
234
|
-
const station = Math.max(
|
|
235
|
-
0,
|
|
236
|
-
Math.min(1, ((point[0] - node.start[0]) * dx + (point[1] - node.start[1]) * dz) / lengthSq),
|
|
237
|
-
)
|
|
238
|
-
let x = node.start[0] + station * dx
|
|
239
|
-
let z = node.start[1] + station * dz
|
|
240
|
-
let nx = frame.nx
|
|
241
|
-
let nz = frame.nz
|
|
242
|
-
if (arc) {
|
|
243
|
-
const angle = Math.atan2(point[1] - arc.center.y, point[0] - arc.center.x)
|
|
244
|
-
const delta = Math.atan2(Math.sin(angle - arc.startAngle), Math.cos(angle - arc.startAngle))
|
|
245
|
-
const t = Math.max(0, Math.min(1, delta / arc.delta))
|
|
246
|
-
const theta = arc.startAngle + arc.delta * t
|
|
247
|
-
const cos = Math.cos(theta)
|
|
248
|
-
const sin = Math.sin(theta)
|
|
249
|
-
x = arc.center.x + cos * arc.radius
|
|
250
|
-
z = arc.center.y + sin * arc.radius
|
|
251
|
-
nx = -cos * arc.direction
|
|
252
|
-
nz = -sin * arc.direction
|
|
253
|
-
}
|
|
254
|
-
const localZ = (point[0] - x) * nx + (point[1] - z) * nz
|
|
255
|
-
// Justified bodies can lie entirely on one side of the reference line.
|
|
256
|
-
const center = view === '3d' ? frame.center : 0
|
|
257
|
-
const face = localZ >= center ? 'a' : 'b'
|
|
258
|
-
// Room first: a face with no room behind it (the exterior) still resolves
|
|
259
|
-
// to the room on the other face, or to the wall's only room; only walls
|
|
260
|
-
// bounding no room reach the wall itself.
|
|
261
|
-
return (
|
|
262
|
-
index.roomForWallHit(levelId, node.id, face, station) ??
|
|
263
|
-
index.roomForWallHit(levelId, node.id, face === 'a' ? 'b' : 'a', station) ??
|
|
264
|
-
soleWallRoom(index, levelId, node.id)
|
|
265
|
-
)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function soleWallRoom(index: RoomHitIndex, levelId: string, wallId: string) {
|
|
269
|
-
let sole: BoundarySpan | null = null
|
|
270
|
-
for (const span of index.spansForWall(levelId, wallId)) {
|
|
271
|
-
if (sole && sole.roomId !== span.roomId) return null
|
|
272
|
-
sole ??= span
|
|
273
|
-
}
|
|
274
|
-
return sole && index.roomForWallHit(levelId, wallId, sole.face, (sole.t0 + sole.t1) / 2)
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function isBoundary(node: AnyNode): node is BoundaryNode {
|
|
278
|
-
return node.type === 'wall' || node.type === 'separator'
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function sameBoundary(a: BoundaryNode, b: AnyNode | undefined) {
|
|
282
|
-
return (
|
|
283
|
-
!!b &&
|
|
284
|
-
isBoundary(b) &&
|
|
285
|
-
a.type === b.type &&
|
|
286
|
-
a.parentId === b.parentId &&
|
|
287
|
-
a.start[0] === b.start[0] &&
|
|
288
|
-
a.start[1] === b.start[1] &&
|
|
289
|
-
a.end[0] === b.end[0] &&
|
|
290
|
-
a.end[1] === b.end[1] &&
|
|
291
|
-
(a.type === 'separator' ||
|
|
292
|
-
(b.type === 'wall' &&
|
|
293
|
-
a.curveOffset === b.curveOffset &&
|
|
294
|
-
a.thickness === b.thickness &&
|
|
295
|
-
a.justification === b.justification))
|
|
296
|
-
)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
export class RoomSelectionIndex {
|
|
300
|
-
readonly topology = createRoomTopologyIndex()
|
|
301
|
-
private nodes: Nodes | null = null
|
|
302
|
-
private records: RoomSelectionRecord[] = []
|
|
303
|
-
private geometry: RoomSelectionGeometry[] = []
|
|
304
|
-
private candidates: AnyNode[] = []
|
|
305
|
-
private revision = -1
|
|
306
|
-
private readonly roomsById = new Map<string, RoomSelectionGeometry>()
|
|
307
|
-
|
|
308
|
-
roomAtPoint(_levelId: string, point: [number, number]): RoomSelectionGeometry | null {
|
|
309
|
-
for (const room of this.geometry)
|
|
310
|
-
if (!room.mezzanine && containsPoint(room.hitPolygon, point)) return room
|
|
311
|
-
return null
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
roomForZone(_levelId: string, zoneId: string): RoomSelectionGeometry | null {
|
|
315
|
-
return this.geometry.find((room) => room.key.zoneId === zoneId) ?? null
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
roomForWallHit(
|
|
319
|
-
levelId: string,
|
|
320
|
-
wallId: string,
|
|
321
|
-
face: 'a' | 'b',
|
|
322
|
-
t: number,
|
|
323
|
-
): RoomSelectionGeometry | null {
|
|
324
|
-
const spans = this.topology.getLevelTopology(levelId)?.spansByBoundary.get(wallId)
|
|
325
|
-
if (!spans) return null
|
|
326
|
-
for (const span of spans) {
|
|
327
|
-
if (span.face === face && t >= span.t0 && (t < span.t1 || (t === 1 && span.t1 === 1)))
|
|
328
|
-
return this.roomsById.get(span.roomId) ?? null
|
|
329
|
-
}
|
|
330
|
-
return null
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
spansForWall(levelId: string, wallId: string) {
|
|
334
|
-
return this.topology.spansForWall(levelId, wallId)
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
constructor(readonly levelId: string) {}
|
|
338
|
-
|
|
339
|
-
update(nodes: Nodes): RoomSelectionRecord[] {
|
|
340
|
-
if (nodes === this.nodes) return this.records
|
|
341
|
-
if (!this.nodes) this.topology.rebuildLevel(this.levelId, nodes)
|
|
342
|
-
else {
|
|
343
|
-
const changed = new Set<string>()
|
|
344
|
-
for (const snapshot of [this.nodes, nodes]) {
|
|
345
|
-
for (const node of Object.values(snapshot)) {
|
|
346
|
-
if (
|
|
347
|
-
isBoundary(node) &&
|
|
348
|
-
node.parentId === this.levelId &&
|
|
349
|
-
!sameBoundary(node, snapshot === nodes ? this.nodes[node.id] : nodes[node.id])
|
|
350
|
-
)
|
|
351
|
-
changed.add(node.id)
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
if (changed.size) this.topology.applyWallDelta(this.levelId, changed, this.nodes, nodes)
|
|
355
|
-
}
|
|
356
|
-
this.nodes = nodes
|
|
357
|
-
const topology = this.topology.getLevelTopology(this.levelId)
|
|
358
|
-
const geometryChanged = topology?.revision !== this.revision
|
|
359
|
-
const candidates = Object.values(nodes).filter(
|
|
360
|
-
(node) =>
|
|
361
|
-
node.parentId === this.levelId &&
|
|
362
|
-
(node.type === 'slab' || node.type === 'ceiling' || node.type === 'zone'),
|
|
363
|
-
)
|
|
364
|
-
const candidatesChanged =
|
|
365
|
-
candidates.length !== this.candidates.length ||
|
|
366
|
-
candidates.some((node, i) => node !== this.candidates[i])
|
|
367
|
-
if (!geometryChanged && !candidatesChanged) return this.records
|
|
368
|
-
if (geometryChanged || candidatesChanged) {
|
|
369
|
-
this.revision = topology?.revision ?? -1
|
|
370
|
-
const previous = new Map(this.geometry.map((room) => [room.key.zoneId, room]))
|
|
371
|
-
const matched = new Set<string>()
|
|
372
|
-
const zones = candidates
|
|
373
|
-
.filter(
|
|
374
|
-
(node): node is ZoneNode =>
|
|
375
|
-
node.type === 'zone' &&
|
|
376
|
-
node.spaceRole === 'room' &&
|
|
377
|
-
node.enclosureStatus !== 'open' &&
|
|
378
|
-
!isMezzanineZone(node),
|
|
379
|
-
)
|
|
380
|
-
.sort((a, b) => a.id.localeCompare(b.id))
|
|
381
|
-
this.geometry = (topology?.rooms ?? []).flatMap((room) => {
|
|
382
|
-
const boundaryIds = [...new Set(room.spans.map((span) => span.boundaryId))].sort().join('|')
|
|
383
|
-
const available = zones.filter((zone) => !matched.has(zone.id))
|
|
384
|
-
const zone =
|
|
385
|
-
available.find(
|
|
386
|
-
(node) =>
|
|
387
|
-
[...node.boundaryWallIds, ...node.boundarySeparatorIds].sort().join('|') ===
|
|
388
|
-
boundaryIds,
|
|
389
|
-
) ??
|
|
390
|
-
available.find(
|
|
391
|
-
(node) =>
|
|
392
|
-
!!node.seed && containsPoint([{ outer: room.polygon, holes: room.holes }], node.seed),
|
|
393
|
-
)
|
|
394
|
-
if (!zone) return []
|
|
395
|
-
matched.add(zone.id)
|
|
396
|
-
return [
|
|
397
|
-
(!geometryChanged && previous.get(zone.id)) ||
|
|
398
|
-
describeRoomGeometry(this.levelId, room, zone),
|
|
399
|
-
]
|
|
400
|
-
})
|
|
401
|
-
const context = topology?.rooms[0]?.context
|
|
402
|
-
if (context)
|
|
403
|
-
for (const zone of candidates) {
|
|
404
|
-
if (!isMezzanineZone(zone) || zone.spaceRole !== 'room') continue
|
|
405
|
-
const hosted =
|
|
406
|
-
zone.hostZoneId && this.geometry.some((room) => room.key.zoneId === zone.hostZoneId)
|
|
407
|
-
if (!hosted) continue
|
|
408
|
-
this.geometry.push(describeMezzanineGeometry(this.levelId, zone, context))
|
|
409
|
-
}
|
|
410
|
-
this.roomsById.clear()
|
|
411
|
-
for (const room of this.geometry) this.roomsById.set(room.id, room)
|
|
412
|
-
}
|
|
413
|
-
this.candidates = candidates
|
|
414
|
-
this.records = this.geometry.map((room) => describeRoom(room, candidates))
|
|
415
|
-
return this.records
|
|
416
|
-
}
|
|
417
|
-
}
|