@pascal-app/editor 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +20 -17
- package/src/components/editor/action-menu-button.tsx +4 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
- package/src/components/editor/bake-thumbnail.tsx +3 -2
- package/src/components/editor/custom-camera-controls.tsx +20 -8
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +72 -61
- package/src/components/editor/floating-action-menu.tsx +27 -9
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +285 -132
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/grid.tsx +24 -15
- package/src/components/editor/group-actions.ts +19 -1
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +27 -2
- package/src/components/editor/group-selection-box-3d.tsx +4 -1
- package/src/components/editor/group-transform-shared.ts +20 -0
- package/src/components/editor/index.tsx +130 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +1 -2
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/registry-action-contributions.test.tsx +67 -6
- package/src/components/editor/registry-action-contributions.tsx +115 -15
- package/src/components/editor/riser-diagram-panel.tsx +59 -9
- package/src/components/editor/room-controls.tsx +1 -1
- package/src/components/editor/room-handles-2d.tsx +156 -6
- package/src/components/editor/room-handles.tsx +241 -51
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +29 -15
- package/src/components/editor/selection-manager.tsx +15 -102
- package/src/components/editor/separator-end-grip.tsx +74 -0
- package/src/components/editor/site-edge-labels.tsx +1 -1
- package/src/components/editor/thumbnail-generator.test.ts +15 -6
- package/src/components/editor/thumbnail-generator.tsx +26 -1
- package/src/components/editor/use-floorplan-background-placement.ts +55 -52
- package/src/components/editor/views/view-bar.tsx +111 -26
- package/src/components/editor/wall-measurement-label.tsx +1 -1
- package/src/components/editor/wall-move-side-handles.tsx +5 -1
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
- package/src/components/procedural-items/inspector.tsx +59 -12
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
- package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
- package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/placement-strategies.test.ts +64 -1
- package/src/components/tools/item/placement-strategies.ts +5 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- package/src/components/tools/select/select-candidates.test.ts +45 -0
- package/src/components/tools/select/select-candidates.ts +13 -2
- package/src/components/tools/shared/cursor-sphere.tsx +10 -3
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +1 -1
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/wall/wall-drafting.test.ts +299 -23
- package/src/components/tools/wall/wall-drafting.ts +132 -24
- package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
- package/src/components/tools/wall/wall-pull-snap.ts +563 -0
- package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
- package/src/components/tools/zone/zone-tool.tsx +3 -3
- package/src/components/ui/action-menu/action-button.tsx +2 -0
- package/src/components/ui/action-menu/camera-actions.tsx +8 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -5
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
- package/src/components/ui/action-menu/index.tsx +1 -0
- package/src/components/ui/action-menu/measurement-control.tsx +35 -29
- package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
- package/src/components/ui/action-menu/structure-tools.tsx +25 -23
- package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
- package/src/components/ui/action-menu/view-toggles.tsx +256 -51
- package/src/components/ui/build-panel/build-panel.tsx +163 -25
- package/src/components/ui/command-palette/editor-commands.tsx +38 -11
- package/src/components/ui/controls/finish-swatch.tsx +45 -19
- package/src/components/ui/controls/material-picker.tsx +24 -17
- package/src/components/ui/controls/metric-control.test.ts +52 -0
- package/src/components/ui/controls/metric-control.tsx +62 -24
- package/src/components/ui/controls/segmented-control.tsx +5 -2
- package/src/components/ui/controls/slider-control.test.tsx +23 -0
- package/src/components/ui/controls/slider-control.tsx +30 -14
- package/src/components/ui/floating-level-selector.tsx +100 -48
- package/src/components/ui/helpers/building-helper.tsx +5 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
- package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
- package/src/components/ui/helpers/helper-manager.tsx +82 -57
- package/src/components/ui/helpers/item-helper.tsx +6 -4
- package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
- package/src/components/ui/level-duplicate-dialog.tsx +32 -23
- package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
- package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
- package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
- package/src/components/ui/panels/inspector-extensions.tsx +66 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
- package/src/components/ui/panels/multi-field-value.test.ts +27 -0
- package/src/components/ui/panels/multi-field-value.ts +20 -1
- package/src/components/ui/panels/multi-height-mode.tsx +8 -7
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +5 -3
- package/src/components/ui/panels/panel-wrapper.tsx +22 -109
- package/src/components/ui/panels/parametric-field-control.tsx +13 -10
- package/src/components/ui/panels/parametric-inspector.tsx +18 -12
- package/src/components/ui/panels/reference-panel.tsx +51 -35
- package/src/components/ui/panels/room-construction-rows.tsx +464 -65
- package/src/components/ui/panels/room-name-options.test.ts +36 -3
- package/src/components/ui/panels/room-name-options.ts +110 -11
- package/src/components/ui/panels/room-openings-field.tsx +21 -12
- package/src/components/ui/panels/room-panel.tsx +206 -99
- package/src/components/ui/panels/shape-choice.tsx +7 -5
- package/src/components/ui/panels/wall-region-list.tsx +7 -7
- package/src/components/ui/primitives/shortcut-token.tsx +10 -6
- package/src/components/ui/sidebar/icon-rail.tsx +11 -9
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
- package/src/components/ui/sidebar/tab-bar.tsx +4 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
- package/src/components/ui/snap-target-badge.tsx +7 -6
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
- package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
- package/src/components/viewer/floorplan-compass-button.tsx +12 -3
- package/src/components/viewer/floorplan-preview.tsx +27 -19
- package/src/components/viewer/render-settings-menu.tsx +294 -0
- package/src/components/viewer/sky-control.tsx +710 -0
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
- package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
- package/src/components/viewer/viewer-controls-bar.tsx +278 -260
- package/src/components/viewer/viewer-hover-label.tsx +12 -4
- package/src/components/viewer/viewer-rooms.tsx +61 -4
- package/src/components/viewer/viewer-scene-header.tsx +28 -17
- package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
- package/src/components/viewer-overlay.tsx +8 -0
- package/src/components/walkthrough-hud.tsx +75 -2
- package/src/hooks/use-keyboard.test.ts +85 -4
- package/src/hooks/use-keyboard.ts +65 -7
- package/src/hooks/use-selected-room.ts +12 -1
- package/src/i18n/floor-footprint-name.ts +30 -0
- package/src/i18n/level-name.ts +39 -0
- package/src/i18n/locales/en/messages.po +15117 -0
- package/src/i18n/locales/en/messages.ts +1 -0
- package/src/i18n/locales/en-XA/messages.ts +1 -0
- package/src/i18n/locales/es/messages.po +37501 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +37501 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +37501 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +37501 -0
- package/src/i18n/locales/zh-CN/messages.ts +1 -0
- package/src/i18n/locales.test.ts +63 -0
- package/src/i18n/locales.ts +32 -0
- package/src/i18n/message-slot.tsx +25 -0
- package/src/i18n/node-messages.ts +169 -0
- package/src/i18n/pseudo-locale.ts +17 -0
- package/src/i18n/published-entry.test.ts +17 -0
- package/src/i18n/scene-theme-name.ts +16 -0
- package/src/index.tsx +156 -4
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +95 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/floor-footprints.test.ts +2 -1
- package/src/lib/floor-foundation-support.test.ts +7 -3
- package/src/lib/floorplan/dimension-drive.test.ts +36 -2
- package/src/lib/floorplan/dimension-drive.ts +68 -3
- package/src/lib/floorplan/floorplan-export.test.ts +16 -5
- package/src/lib/floorplan/floorplan-export.tsx +13 -16
- package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
- package/src/lib/floorplan/floorplan-extension.ts +37 -18
- package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
- package/src/lib/floorplan/floorplan-mode.ts +14 -12
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
- package/src/lib/floorplan/floorplan-readonly.ts +3 -3
- package/src/lib/floorplan/schedules.ts +10 -6
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
- package/src/lib/geometry-script/author.test.ts +114 -0
- package/src/lib/geometry-script/author.ts +55 -23
- package/src/lib/geometry-script/client.ts +3 -0
- package/src/lib/geometry-script/finish-scripts.ts +7 -7
- package/src/lib/glb-export.test.ts +327 -0
- package/src/lib/glb-export.ts +70 -2
- package/src/lib/grid-frame.test.ts +56 -3
- package/src/lib/grid-frame.ts +28 -1
- package/src/lib/history.test.ts +2 -1
- package/src/lib/hud-title.ts +48 -24
- package/src/lib/ifc-export.test.ts +41 -1
- package/src/lib/inspector-card-mode.test.ts +3 -31
- package/src/lib/inspector-card-mode.ts +5 -10
- package/src/lib/inspector-expanded.test.ts +13 -0
- package/src/lib/inspector-expanded.ts +13 -1
- package/src/lib/keyboard-pan.test.ts +7 -0
- package/src/lib/keyboard-pan.ts +9 -2
- package/src/lib/layer-thickness.test.ts +22 -0
- package/src/lib/layer-thickness.ts +79 -0
- package/src/lib/lot/drop-in.test.ts +59 -1
- package/src/lib/lot/drop-in.ts +42 -50
- package/src/lib/material-swatch-style.ts +24 -0
- package/src/lib/measurement-parser.ts +7 -3
- package/src/lib/measurements.test.ts +17 -0
- package/src/lib/measurements.ts +20 -5
- package/src/lib/mezzanine-selection.test.ts +3 -1
- package/src/lib/orbit-near-plane.test.ts +12 -0
- package/src/lib/orbit-near-plane.ts +13 -0
- package/src/lib/paint-regions.test.ts +8 -5
- package/src/lib/paint-scope.test.ts +53 -0
- package/src/lib/paint-scope.ts +18 -4
- package/src/lib/panel-rows.ts +17 -8
- package/src/lib/reference-scale.ts +83 -0
- package/src/lib/room-built-on.test.ts +26 -12
- package/src/lib/room-built-on.ts +20 -2
- package/src/lib/room-construction.test.ts +28 -1
- package/src/lib/room-construction.ts +14 -0
- package/src/lib/room-controls-phase6.test.ts +140 -0
- package/src/lib/room-handle-drag.ts +557 -42
- package/src/lib/room-name-catalog.test.ts +8 -0
- package/src/lib/room-name-catalog.ts +8 -1
- package/src/lib/room-name-messages.test.ts +132 -0
- package/src/lib/room-name-messages.ts +147 -0
- package/src/lib/room-name-storage.test.ts +334 -0
- package/src/lib/room-push-dimensions.test.ts +37 -0
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +9 -4
- package/src/lib/selection-cycle.ts +63 -0
- package/src/lib/separator-edit.test.ts +431 -0
- package/src/lib/separator-edit.ts +179 -0
- package/src/lib/separator-lines.ts +38 -0
- package/src/lib/sfx-bus.ts +27 -1
- package/src/lib/sfx-player.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +5 -1
- package/src/lib/tool-shortcuts.ts +130 -0
- package/src/lib/viewer-selection.test.ts +28 -24
- package/src/lib/viewer-selection.ts +20 -5
- package/src/lib/wall-draw-variant.test.ts +7 -6
- package/src/lib/wall-draw-variant.ts +107 -6
- package/src/lib/wall-nudge.test.ts +44 -0
- package/src/lib/wall-nudge.ts +131 -0
- package/src/lib/wall-parallel-snap.test.ts +83 -0
- package/src/lib/wall-parallel-snap.ts +176 -0
- package/src/lib/wall-placement-pointer.test.ts +23 -0
- package/src/lib/wall-placement-pointer.ts +18 -0
- package/src/lib/wall-pull.test.ts +245 -0
- package/src/lib/wall-pull.ts +209 -0
- package/src/lib/wall-push-line-up.test.ts +132 -0
- package/src/lib/wall-region-list.ts +14 -5
- package/src/lib/wall-run-snap.test.ts +188 -0
- package/src/lib/wall-run-snap.ts +83 -0
- package/src/lib/wall-type.test.ts +125 -0
- package/src/lib/wall-type.ts +119 -0
- package/src/lib/window-interaction.test.ts +42 -0
- package/src/lib/window-interaction.ts +47 -5
- package/src/lib/world-grid-snap.test.ts +139 -2
- package/src/lib/world-grid-snap.ts +102 -105
- package/src/store/use-alignment-guides.ts +27 -1
- package/src/store/use-editor.tsx +48 -0
- package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
- package/src/store/use-floorplan-annotation-visibility.ts +47 -22
- package/src/store/use-wall-snap-indicator.ts +15 -2
|
@@ -17,12 +17,16 @@ import {
|
|
|
17
17
|
planWallDivision,
|
|
18
18
|
resolveMovedWallSupportSlabPatch,
|
|
19
19
|
runAsSingleSceneHistoryStep,
|
|
20
|
+
SEPARATOR_JOIN_REACH,
|
|
21
|
+
type SeparatorNode,
|
|
22
|
+
type SeparatorSpan,
|
|
20
23
|
type StructureNodes,
|
|
21
24
|
type StructurePlan,
|
|
22
25
|
setRoomFloorConstruction,
|
|
23
26
|
setWallGeometry,
|
|
24
27
|
setZoneEdges,
|
|
25
28
|
setZoneIntent,
|
|
29
|
+
translateSeparators,
|
|
26
30
|
upperFloorHeightControl,
|
|
27
31
|
useLiveNodeOverrides,
|
|
28
32
|
useScene,
|
|
@@ -46,19 +50,34 @@ import { roomOwnPlate } from './room-built-on'
|
|
|
46
50
|
import {
|
|
47
51
|
mezzanineEdgeDimensions,
|
|
48
52
|
type RoomDimension,
|
|
53
|
+
sameRoomDimension,
|
|
54
|
+
separatorEdgeDimensionRooms,
|
|
55
|
+
type WallDimensionFaces,
|
|
56
|
+
type WallSelectionDimension,
|
|
49
57
|
wallPushDimensionRooms,
|
|
50
58
|
wallPushDimensions,
|
|
59
|
+
wallSelectionDimensions,
|
|
51
60
|
} from './room-push-dimensions'
|
|
52
61
|
import { applyRoomPlan } from './room-structure-commands'
|
|
62
|
+
import { levelSeparatorLines, separatorWallLine } from './separator-lines'
|
|
53
63
|
import { sfxEmitter } from './sfx-bus'
|
|
54
64
|
import { type SpatialPointerId, spatialPointerInput } from './spatial-pointer-input'
|
|
55
65
|
import { isTypingTarget } from './typing-target'
|
|
66
|
+
import {
|
|
67
|
+
nearestWallParallelTarget,
|
|
68
|
+
publishWallParallelGuide,
|
|
69
|
+
type WallParallelTarget,
|
|
70
|
+
wallParallelTargets,
|
|
71
|
+
} from './wall-parallel-snap'
|
|
72
|
+
import { isWallRunFree } from './wall-run-snap'
|
|
56
73
|
|
|
57
74
|
export const ROOM_ELEVATION_DRAG_LABEL = 'room-elevation'
|
|
58
75
|
/** Scope label of every wall push: a selected wall's side arrow and a room's boundary arrow. */
|
|
59
76
|
export const WALL_PUSH_DRAG_LABEL = 'wall-push'
|
|
60
77
|
/** Scope label of a mezzanine edge push (its plate grows or shrinks). */
|
|
61
78
|
export const MEZZANINE_EDGE_DRAG_LABEL = 'mezzanine-edge'
|
|
79
|
+
/** What a wall push the planner refuses reads. */
|
|
80
|
+
export const PUSH_REFUSED_MESSAGE = "Can't move this wall there"
|
|
62
81
|
|
|
63
82
|
/** A wall a push would create (a moved span piece, a bridge), drawn as a ghost. */
|
|
64
83
|
export type RoomPushGhost = { start: Point; end: Point; thickness: number; height: number }
|
|
@@ -93,6 +112,20 @@ export type RoomHandleDrag =
|
|
|
93
112
|
dimensions: RoomDimension[]
|
|
94
113
|
message?: string
|
|
95
114
|
}
|
|
115
|
+
| {
|
|
116
|
+
/** Several selected walls moved together, each along its own normal. */
|
|
117
|
+
kind: 'wall-group-push'
|
|
118
|
+
levelId: string
|
|
119
|
+
/** Where a refusal reads: over the moved walls' middle, in level plan coordinates. */
|
|
120
|
+
anchor: Point
|
|
121
|
+
height: number
|
|
122
|
+
/** The lowest moved wall's base, where the ghost pieces stand. */
|
|
123
|
+
base: number
|
|
124
|
+
ghosts: RoomPushGhost[]
|
|
125
|
+
/** Every moved wall's live room widths, as its own push shows them. */
|
|
126
|
+
dimensions: RoomDimension[]
|
|
127
|
+
message?: string
|
|
128
|
+
}
|
|
96
129
|
| {
|
|
97
130
|
kind: 'mezzanine-edge'
|
|
98
131
|
zoneId: string
|
|
@@ -129,9 +162,16 @@ export function roomFloorElevation(nodes: Nodes, zoneId: string): number {
|
|
|
129
162
|
/**
|
|
130
163
|
* The stretch of a wall a push moves: `[0, 1]` is the whole wall (its
|
|
131
164
|
* neighbours follow through the junction planner), anything shorter is split
|
|
132
|
-
* off first and only that piece moves.
|
|
165
|
+
* off first and only that piece moves. An edge of invisible walls carries its
|
|
166
|
+
* stretches in `separators` (`wallId` is the first one's): they move as one
|
|
167
|
+
* line, the invisible walls joined to them following (`translateSeparators`).
|
|
133
168
|
*/
|
|
134
|
-
export type WallPushSpan = {
|
|
169
|
+
export type WallPushSpan = {
|
|
170
|
+
wallId: string
|
|
171
|
+
t0: number
|
|
172
|
+
t1: number
|
|
173
|
+
separators?: readonly SeparatorSpan[]
|
|
174
|
+
}
|
|
135
175
|
|
|
136
176
|
/** One push/pull arrow: a selected wall's side arrow or a room's boundary arrow. */
|
|
137
177
|
export type WallPushHandle = WallPushSpan & {
|
|
@@ -191,22 +231,27 @@ export function wallPushHandles(wall: WallNode, nodes: Nodes): WallPushHandle[]
|
|
|
191
231
|
})
|
|
192
232
|
}
|
|
193
233
|
|
|
234
|
+
type BoundarySpanRef = {
|
|
235
|
+
boundaryId: string
|
|
236
|
+
kind: string
|
|
237
|
+
face: 'a' | 'b'
|
|
238
|
+
t0: number
|
|
239
|
+
t1: number
|
|
240
|
+
}
|
|
241
|
+
|
|
194
242
|
/**
|
|
195
243
|
* One push/pull arrow per boundary wall span of the room, outside the wall's
|
|
196
244
|
* far face, pointing away from the room — where the wall's own side arrow for
|
|
197
|
-
* that face sits. `face` is the wall face the room lies on (`a` = front).
|
|
245
|
+
* that face sits. `face` is the wall face the room lies on (`a` = front). Its
|
|
246
|
+
* invisible walls get one arrow per straight edge (`separatorEdges`), lying
|
|
247
|
+
* at `floor` (the room's floor height), where their dashed line is drawn.
|
|
198
248
|
*/
|
|
199
249
|
export function roomPushHandles(
|
|
200
250
|
nodes: Nodes,
|
|
201
|
-
spans: ReadonlyArray<
|
|
202
|
-
|
|
203
|
-
kind: string
|
|
204
|
-
face: 'a' | 'b'
|
|
205
|
-
t0: number
|
|
206
|
-
t1: number
|
|
207
|
-
}>,
|
|
251
|
+
spans: ReadonlyArray<BoundarySpanRef>,
|
|
252
|
+
floor = 0,
|
|
208
253
|
): WallPushHandle[] {
|
|
209
|
-
|
|
254
|
+
const walls = spans.flatMap((span) => {
|
|
210
255
|
const wall = nodes[span.boundaryId]
|
|
211
256
|
if (span.kind !== 'wall' || wall?.type !== 'wall') return []
|
|
212
257
|
const handle = wallPushHandle(
|
|
@@ -218,6 +263,145 @@ export function roomPushHandles(
|
|
|
218
263
|
)
|
|
219
264
|
return handle ? [handle] : []
|
|
220
265
|
})
|
|
266
|
+
const edges = separatorEdges(nodes, spans).map((edge) => {
|
|
267
|
+
const first = edge.members[0]!
|
|
268
|
+
return separatorPushHandle(
|
|
269
|
+
edge,
|
|
270
|
+
edge.outward,
|
|
271
|
+
floor,
|
|
272
|
+
`${first.separatorId}:${edge.face}:${first.t0.toFixed(4)}`,
|
|
273
|
+
)
|
|
274
|
+
})
|
|
275
|
+
return [...walls, ...edges]
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** How far outside an invisible wall (m) its push arrow sits, as a mezzanine edge's does. */
|
|
279
|
+
const SEPARATOR_ARROW_OFFSET = 0.3
|
|
280
|
+
/** Its 3D arrow's lift over the floor: where the invisible wall's dashed line is drawn. */
|
|
281
|
+
const SEPARATOR_ARROW_LIFT = 0.02
|
|
282
|
+
/** An invisible-wall edge shorter than this (m) gets no arrow, as a mezzanine edge. */
|
|
283
|
+
const SEPARATOR_EDGE_MIN_LENGTH = 0.2
|
|
284
|
+
/** Stretches this far off each other's line (m) still run along one edge. */
|
|
285
|
+
const EDGE_IN_LINE = 0.01
|
|
286
|
+
|
|
287
|
+
type SeparatorEdge = {
|
|
288
|
+
/** The stretches along the edge, in order. */
|
|
289
|
+
members: SeparatorSpan[]
|
|
290
|
+
start: Point
|
|
291
|
+
end: Point
|
|
292
|
+
/** Away from the room. */
|
|
293
|
+
outward: Point
|
|
294
|
+
face: 'a' | 'b'
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* A room's invisible-wall stretches gathered into its straight edges: stretches
|
|
299
|
+
* end to end on one line, the room on the same side, are one edge and push as
|
|
300
|
+
* one (an edge drawn in two pieces, or broken where another one meets it). Ends
|
|
301
|
+
* meet as far as the room graph joins them (`SEPARATOR_JOIN_REACH`).
|
|
302
|
+
*/
|
|
303
|
+
function separatorEdges(nodes: Nodes, spans: ReadonlyArray<BoundarySpanRef>): SeparatorEdge[] {
|
|
304
|
+
const pieces = spans.flatMap((span): SeparatorEdge[] => {
|
|
305
|
+
const separator = nodes[span.boundaryId]
|
|
306
|
+
if (span.kind !== 'separator' || separator?.type !== 'separator') return []
|
|
307
|
+
const dx = separator.end[0] - separator.start[0]
|
|
308
|
+
const dz = separator.end[1] - separator.start[1]
|
|
309
|
+
const length = Math.hypot(dx, dz)
|
|
310
|
+
if (length < 1e-6 || span.t1 - span.t0 < WHOLE) return []
|
|
311
|
+
// Face `a` is the +normal side: the arrow points the other way, off the room.
|
|
312
|
+
const sign = span.face === 'a' ? -1 : 1
|
|
313
|
+
const at = (t: number): Point => [separator.start[0] + dx * t, separator.start[1] + dz * t]
|
|
314
|
+
return [
|
|
315
|
+
{
|
|
316
|
+
members: [{ separatorId: separator.id, t0: span.t0, t1: span.t1 }],
|
|
317
|
+
start: at(span.t0),
|
|
318
|
+
end: at(span.t1),
|
|
319
|
+
outward: [(-dz / length) * sign, (dx / length) * sign],
|
|
320
|
+
face: span.face,
|
|
321
|
+
},
|
|
322
|
+
]
|
|
323
|
+
})
|
|
324
|
+
const across = (edge: SeparatorEdge, [x, z]: Point) =>
|
|
325
|
+
Math.abs((x - edge.start[0]) * edge.outward[0] + (z - edge.start[1]) * edge.outward[1])
|
|
326
|
+
const joins = (a: SeparatorEdge, b: SeparatorEdge) =>
|
|
327
|
+
a.outward[0] * b.outward[0] + a.outward[1] * b.outward[1] > 1 - 1e-4 &&
|
|
328
|
+
across(a, b.start) < EDGE_IN_LINE &&
|
|
329
|
+
across(a, b.end) < EDGE_IN_LINE &&
|
|
330
|
+
[a.start, a.end].some((p) =>
|
|
331
|
+
[b.start, b.end].some((q) => Math.hypot(p[0] - q[0], p[1] - q[1]) <= SEPARATOR_JOIN_REACH),
|
|
332
|
+
)
|
|
333
|
+
let groups: SeparatorEdge[][] = []
|
|
334
|
+
for (const piece of pieces) {
|
|
335
|
+
const touching = groups.filter((group) => group.some((other) => joins(piece, other)))
|
|
336
|
+
groups = [...groups.filter((group) => !touching.includes(group)), [piece, ...touching.flat()]]
|
|
337
|
+
}
|
|
338
|
+
return groups.flatMap((group): SeparatorEdge[] => {
|
|
339
|
+
const { outward, face } = group[0]!
|
|
340
|
+
const dir: Point = [outward[1], -outward[0]]
|
|
341
|
+
const along = ([x, z]: Point) => x * dir[0] + z * dir[1]
|
|
342
|
+
const ends = group
|
|
343
|
+
.flatMap((piece) => [piece.start, piece.end])
|
|
344
|
+
.sort((p, q) => along(p) - along(q))
|
|
345
|
+
const start = ends[0]!
|
|
346
|
+
const end = ends.at(-1)!
|
|
347
|
+
if (Math.hypot(end[0] - start[0], end[1] - start[1]) < SEPARATOR_EDGE_MIN_LENGTH) return []
|
|
348
|
+
const middle = (piece: SeparatorEdge) => along(piece.start) + along(piece.end)
|
|
349
|
+
const members = [...group].sort((p, q) => middle(p) - middle(q)).flatMap((p) => p.members)
|
|
350
|
+
return [{ members, start, end, outward, face }]
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function separatorPushHandle(
|
|
355
|
+
edge: Pick<SeparatorEdge, 'members' | 'start' | 'end'>,
|
|
356
|
+
outward: Point,
|
|
357
|
+
floor: number,
|
|
358
|
+
key: string,
|
|
359
|
+
): WallPushHandle {
|
|
360
|
+
const first = edge.members[0]!
|
|
361
|
+
const middle: Point = [(edge.start[0] + edge.end[0]) / 2, (edge.start[1] + edge.end[1]) / 2]
|
|
362
|
+
return {
|
|
363
|
+
key,
|
|
364
|
+
wallId: first.separatorId,
|
|
365
|
+
t0: first.t0,
|
|
366
|
+
t1: first.t1,
|
|
367
|
+
separators: edge.members,
|
|
368
|
+
position: [
|
|
369
|
+
middle[0] + outward[0] * SEPARATOR_ARROW_OFFSET,
|
|
370
|
+
middle[1] + outward[1] * SEPARATOR_ARROW_OFFSET,
|
|
371
|
+
],
|
|
372
|
+
height: floor + SEPARATOR_ARROW_LIFT,
|
|
373
|
+
outward,
|
|
374
|
+
line: middle[0] * outward[0] + middle[1] * outward[1],
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* A selected invisible wall's two side arrows, one off each side, at `floor`:
|
|
380
|
+
* each pushes the whole separator, the ones joined to it following.
|
|
381
|
+
*/
|
|
382
|
+
export function separatorPushHandles(separator: SeparatorNode, floor = 0): WallPushHandle[] {
|
|
383
|
+
const dx = separator.end[0] - separator.start[0]
|
|
384
|
+
const dz = separator.end[1] - separator.start[1]
|
|
385
|
+
const length = Math.hypot(dx, dz)
|
|
386
|
+
if (length < 1e-6) return []
|
|
387
|
+
const edge = {
|
|
388
|
+
members: [{ separatorId: separator.id, t0: 0, t1: 1 }],
|
|
389
|
+
start: separator.start,
|
|
390
|
+
end: separator.end,
|
|
391
|
+
}
|
|
392
|
+
return (['a', 'b'] as const).map((side) => {
|
|
393
|
+
const sign = side === 'a' ? 1 : -1
|
|
394
|
+
const outward: Point = [(-dz / length) * sign, (dx / length) * sign]
|
|
395
|
+
return separatorPushHandle(edge, outward, floor, `${separator.id}:${side}`)
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** The floor an invisible wall lies on: the walking surface of a room it closes, else 0. */
|
|
400
|
+
export function separatorFloorElevation(nodes: Nodes, separatorId: string): number {
|
|
401
|
+
const zone = Object.values(nodes).find(
|
|
402
|
+
(node) => node.type === 'zone' && node.boundarySeparatorIds.includes(separatorId),
|
|
403
|
+
)
|
|
404
|
+
return zone ? roomFloorElevation(nodes, zone.id) : 0
|
|
221
405
|
}
|
|
222
406
|
|
|
223
407
|
/**
|
|
@@ -294,7 +478,8 @@ function diffNodes(before: Scratch, after: Scratch): NodeChange[] {
|
|
|
294
478
|
* Pushes the room's stretch of a wall `distance` along `outward`. A span that
|
|
295
479
|
* covers the whole wall moves the wall; a partial span is split off at its
|
|
296
480
|
* ends first (hosted openings stay on their pieces) and only that piece moves,
|
|
297
|
-
* bridged back to the rest by the junction planner.
|
|
481
|
+
* bridged back to the rest by the junction planner. An edge of invisible walls
|
|
482
|
+
* moves as one line (`translateSeparators`). One plan, one undo step.
|
|
298
483
|
*/
|
|
299
484
|
export function planRoomPush(
|
|
300
485
|
nodes: StructureNodes,
|
|
@@ -303,6 +488,12 @@ export function planRoomPush(
|
|
|
303
488
|
distance: number,
|
|
304
489
|
mintId: (kind: 'zone' | 'wall' | 'separator') => string = generateId,
|
|
305
490
|
): StructurePlan {
|
|
491
|
+
if (span.separators?.length)
|
|
492
|
+
return translateSeparators(nodes, {
|
|
493
|
+
spans: span.separators,
|
|
494
|
+
offset: [outward[0] * distance, outward[1] * distance],
|
|
495
|
+
mintId,
|
|
496
|
+
})
|
|
306
497
|
const wall = nodes[span.wallId]
|
|
307
498
|
if (wall?.type !== 'wall') throw Error('Select a wall.')
|
|
308
499
|
const shift = ([x, z]: Point): Point => [x + outward[0] * distance, z + outward[1] * distance]
|
|
@@ -388,14 +579,18 @@ function clearPushPreview() {
|
|
|
388
579
|
function previewSurfaces(nodes: Nodes, levelId: string, after: Scratch) {
|
|
389
580
|
if (surfacePreview?.levelId !== levelId || surfacePreview.nodes !== nodes)
|
|
390
581
|
surfacePreview = { levelId, nodes, run: createLevelStructurePreview(levelId, nodes) }
|
|
391
|
-
const
|
|
392
|
-
(node): node is WallNode => node.type === 'wall' && node.parentId === levelId,
|
|
393
|
-
)
|
|
582
|
+
const level = Object.values(after).filter((node) => node.parentId === levelId)
|
|
394
583
|
return surfacePreview
|
|
395
|
-
.run(
|
|
584
|
+
.run(
|
|
585
|
+
level.filter((node): node is WallNode => node.type === 'wall'),
|
|
586
|
+
level.filter((node): node is SeparatorNode => node.type === 'separator'),
|
|
587
|
+
)
|
|
396
588
|
.map((patch) => [patch.id as AnyNodeId, patch.data as Partial<AnyNode>] as const)
|
|
397
589
|
}
|
|
398
590
|
|
|
591
|
+
/** The ghost of an invisible wall a push would create (a jog): a thin strip on the floor. */
|
|
592
|
+
const SEPARATOR_GHOST = { thickness: 0.03, height: 0.04 }
|
|
593
|
+
|
|
399
594
|
/**
|
|
400
595
|
* Shows the push live: the plan's updates to existing walls — and the room
|
|
401
596
|
* surfaces reshaped around them — ride `useLiveNodeOverrides` (renderers merge
|
|
@@ -410,9 +605,20 @@ export function previewRoomPush(
|
|
|
410
605
|
clearPushPreview()
|
|
411
606
|
if (Math.abs(distance) < 1e-6) return { ghosts: [] }
|
|
412
607
|
const nodes = useScene.getState().nodes
|
|
608
|
+
return previewPushPlan(nodes, nodes[span.wallId as AnyNodeId]?.parentId, () =>
|
|
609
|
+
planRoomPush(nodes, span, outward, distance, previewMint()),
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Shows a push plan live (`previewRoomPush`); the caller has cleared the last one. */
|
|
614
|
+
function previewPushPlan(
|
|
615
|
+
nodes: Nodes,
|
|
616
|
+
levelId: string | null | undefined,
|
|
617
|
+
planPush: () => StructurePlan,
|
|
618
|
+
): { code?: string; ghosts: RoomPushGhost[] } {
|
|
413
619
|
let plan: StructurePlan
|
|
414
620
|
try {
|
|
415
|
-
plan =
|
|
621
|
+
plan = planPush()
|
|
416
622
|
} catch {
|
|
417
623
|
return { code: 'invalid-geometry', ghosts: [] }
|
|
418
624
|
}
|
|
@@ -420,15 +626,17 @@ export function previewRoomPush(
|
|
|
420
626
|
const updates: Array<readonly [AnyNodeId, Partial<AnyNode>]> = plan.changes.flatMap((change) =>
|
|
421
627
|
change.op === 'update' ? [[change.id as AnyNodeId, change.data] as const] : [],
|
|
422
628
|
)
|
|
423
|
-
const levelId = nodes[span.wallId as AnyNodeId]?.parentId
|
|
424
629
|
if (levelId) updates.push(...previewSurfaces(nodes, levelId, applyChanges(nodes, plan.changes)))
|
|
425
630
|
useLiveNodeOverrides
|
|
426
631
|
.getState()
|
|
427
632
|
.setMany(updates.map(([id, data]) => [id, data] as [AnyNodeId, Partial<AnyNode>]))
|
|
428
633
|
previewIds = updates.map(([id]) => id)
|
|
429
634
|
for (const id of previewIds) useScene.getState().markDirty(id as AnyNodeId)
|
|
430
|
-
const ghosts = plan.changes.flatMap((change): RoomPushGhost[] =>
|
|
431
|
-
change.op
|
|
635
|
+
const ghosts = plan.changes.flatMap((change): RoomPushGhost[] => {
|
|
636
|
+
if (change.op !== 'create') return []
|
|
637
|
+
if (change.node.type === 'separator')
|
|
638
|
+
return [{ start: change.node.start, end: change.node.end, ...SEPARATOR_GHOST }]
|
|
639
|
+
return change.node.type === 'wall'
|
|
432
640
|
? [
|
|
433
641
|
{
|
|
434
642
|
start: change.node.start,
|
|
@@ -437,8 +645,8 @@ export function previewRoomPush(
|
|
|
437
645
|
height: getWallEffectiveHeightForNodes(change.node, nodes),
|
|
438
646
|
},
|
|
439
647
|
]
|
|
440
|
-
: []
|
|
441
|
-
)
|
|
648
|
+
: []
|
|
649
|
+
})
|
|
442
650
|
// The floor plan draws the same pieces through its bridge-ghost layer.
|
|
443
651
|
if (ghosts.length) {
|
|
444
652
|
useWallMoveGhosts.getState().setBridges(
|
|
@@ -468,9 +676,13 @@ export function endRoomPushPreview() {
|
|
|
468
676
|
export function commitRoomPush(span: WallPushSpan, outward: Point, distance: number) {
|
|
469
677
|
endRoomPushPreview()
|
|
470
678
|
if (Math.abs(distance) < 1e-6) return true
|
|
679
|
+
return commitPushPlan((nodes) => planRoomPush(nodes, span, outward, distance))
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function commitPushPlan(planPush: (nodes: StructureNodes) => StructurePlan) {
|
|
471
683
|
try {
|
|
472
684
|
return runAsSingleSceneHistoryStep(useScene, () => {
|
|
473
|
-
const plan =
|
|
685
|
+
const plan = planPush(useScene.getState().nodes)
|
|
474
686
|
if (!applyRoomPlan(plan)) return false
|
|
475
687
|
const nodes = useScene.getState().nodes
|
|
476
688
|
const patches = plan.changes.flatMap((change) => {
|
|
@@ -716,12 +928,13 @@ export function cancelRoomHandleDrag() {
|
|
|
716
928
|
/**
|
|
717
929
|
* Runs one handle drag on window listeners, independent of the handle's own
|
|
718
930
|
* mount (handles hide while any scope is active). `sample` maps a pointer to
|
|
719
|
-
* the drag's value (`spatial.sample` a controller ray, for a spatial pointer)
|
|
720
|
-
*
|
|
931
|
+
* the drag's value (`spatial.sample` a controller ray, for a spatial pointer):
|
|
932
|
+
* a distance or a height, or a plan point for an end dragged across the floor.
|
|
933
|
+
* Release commits, Escape / ⌘Z / the gesture owner cancel. Cleanup runs
|
|
721
934
|
* exactly once whatever happens, and releases only this drag's own scope;
|
|
722
935
|
* nothing republishes a preview after it.
|
|
723
936
|
*/
|
|
724
|
-
export function runRoomHandleDrag({
|
|
937
|
+
export function runRoomHandleDrag<T = number>({
|
|
725
938
|
label,
|
|
726
939
|
nodeId,
|
|
727
940
|
zoneId,
|
|
@@ -738,9 +951,9 @@ export function runRoomHandleDrag({
|
|
|
738
951
|
zoneId?: string
|
|
739
952
|
levelId: string
|
|
740
953
|
requires: string[]
|
|
741
|
-
sample: (clientX: number, clientY: number) =>
|
|
742
|
-
spatial?: { pointerId: SpatialPointerId; sample: (ray: Ray) =>
|
|
743
|
-
onValue: (value:
|
|
954
|
+
sample: (clientX: number, clientY: number) => T | null
|
|
955
|
+
spatial?: { pointerId: SpatialPointerId; sample: (ray: Ray) => T | null }
|
|
956
|
+
onValue: (value: T) => void
|
|
744
957
|
onCommit: () => void
|
|
745
958
|
onCancel: () => void
|
|
746
959
|
}) {
|
|
@@ -891,10 +1104,20 @@ export function runWallPushDrag({
|
|
|
891
1104
|
spatial?: { pointerId: SpatialPointerId; along: (ray: Ray) => number | null }
|
|
892
1105
|
}) {
|
|
893
1106
|
const nodes = useScene.getState().nodes
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
const
|
|
1107
|
+
const boundary = nodes[handle.wallId as AnyNodeId]
|
|
1108
|
+
// An edge of invisible walls is measured, and lines up, as the one line it moves as.
|
|
1109
|
+
const edge = handle.separators?.length ? separatorEdgeLine(nodes, handle.separators) : null
|
|
1110
|
+
const wall = edge ?? (boundary?.type === 'wall' ? boundary : null)
|
|
1111
|
+
if (!wall) return null
|
|
1112
|
+
const base =
|
|
1113
|
+
wall.type === 'wall'
|
|
1114
|
+
? getWallBaseElevationForNodes(wall, nodes)
|
|
1115
|
+
: handle.height - SEPARATOR_ARROW_LIFT
|
|
897
1116
|
const rooms = pushDimensionRooms(nodes, levelId, handle, base)
|
|
1117
|
+
const levelWalls = Object.values(nodes).filter(
|
|
1118
|
+
(node): node is WallNode => node.type === 'wall' && node.parentId === levelId,
|
|
1119
|
+
)
|
|
1120
|
+
const moving = new Set([handle.wallId, ...(handle.separators ?? []).map((s) => s.separatorId)])
|
|
898
1121
|
let distance = 0
|
|
899
1122
|
let blocked = false
|
|
900
1123
|
let ghosts: RoomPushGhost[] = []
|
|
@@ -921,15 +1144,49 @@ export function runWallPushDrag({
|
|
|
921
1144
|
message,
|
|
922
1145
|
},
|
|
923
1146
|
})
|
|
924
|
-
|
|
925
|
-
|
|
1147
|
+
// The pushed wall lines up with the parallel walls on its level
|
|
1148
|
+
// (`wall-parallel-snap`) before the grid; Alt pushes it free. Invisible walls
|
|
1149
|
+
// line up with the walls and with the level's other invisible walls.
|
|
1150
|
+
const lineUpTargets =
|
|
1151
|
+
wall.type === 'wall'
|
|
1152
|
+
? wallParallelTargets(wall, handle.outward, levelWalls)
|
|
1153
|
+
: wallParallelTargets(separatorWallLine(wall), handle.outward, [
|
|
1154
|
+
...levelWalls,
|
|
1155
|
+
...levelSeparatorLines(nodes, levelId, moving),
|
|
1156
|
+
])
|
|
1157
|
+
let lineUp: WallParallelTarget | null = null
|
|
1158
|
+
let altHeld = false
|
|
1159
|
+
const trackAlt = (event: KeyboardEvent | PointerEvent) => {
|
|
1160
|
+
altHeld = event.altKey
|
|
1161
|
+
}
|
|
1162
|
+
window.addEventListener('pointermove', trackAlt, true)
|
|
1163
|
+
window.addEventListener('keydown', trackAlt, true)
|
|
1164
|
+
window.addEventListener('keyup', trackAlt, true)
|
|
1165
|
+
const stopLineUp = () => {
|
|
1166
|
+
window.removeEventListener('pointermove', trackAlt, true)
|
|
1167
|
+
window.removeEventListener('keydown', trackAlt, true)
|
|
1168
|
+
window.removeEventListener('keyup', trackAlt, true)
|
|
1169
|
+
if (lineUp) publishWallParallelGuide(null)
|
|
1170
|
+
lineUp = null
|
|
1171
|
+
}
|
|
1172
|
+
const toDistance = (at: number | null) => {
|
|
1173
|
+
if (at === null) return null
|
|
1174
|
+
const raw = at - from
|
|
1175
|
+
const next = isWallRunFree({ altKey: altHeld })
|
|
1176
|
+
? null
|
|
1177
|
+
: nearestWallParallelTarget(lineUpTargets, handle.line + raw)
|
|
1178
|
+
if (next !== lineUp) publishWallParallelGuide(next)
|
|
1179
|
+
lineUp = next
|
|
1180
|
+
if (next) return next.position - handle.line
|
|
1181
|
+
return isWallRunFree({ altKey: altHeld }) ? raw : snapPushDistance(handle.line, raw)
|
|
1182
|
+
}
|
|
926
1183
|
publish()
|
|
927
1184
|
return runRoomHandleDrag({
|
|
928
1185
|
label: WALL_PUSH_DRAG_LABEL,
|
|
929
1186
|
nodeId: handle.wallId,
|
|
930
1187
|
zoneId,
|
|
931
1188
|
levelId,
|
|
932
|
-
requires: zoneId ? [zoneId,
|
|
1189
|
+
requires: zoneId ? [zoneId, ...moving] : [...moving],
|
|
933
1190
|
sample: (x, y) => toDistance(along(x, y)),
|
|
934
1191
|
spatial: spatial && {
|
|
935
1192
|
pointerId: spatial.pointerId,
|
|
@@ -942,13 +1199,17 @@ export function runWallPushDrag({
|
|
|
942
1199
|
const preview = previewRoomPush(handle, handle.outward, distance)
|
|
943
1200
|
ghosts = preview.ghosts
|
|
944
1201
|
blocked = !!preview.code
|
|
945
|
-
publish(preview.code ?
|
|
1202
|
+
publish(preview.code ? PUSH_REFUSED_MESSAGE : undefined)
|
|
946
1203
|
},
|
|
947
1204
|
onCommit: () => {
|
|
1205
|
+
stopLineUp()
|
|
948
1206
|
if (blocked) endRoomPushPreview()
|
|
949
1207
|
else commitRoomPush(handle, handle.outward, distance)
|
|
950
1208
|
},
|
|
951
|
-
onCancel: () =>
|
|
1209
|
+
onCancel: () => {
|
|
1210
|
+
stopLineUp()
|
|
1211
|
+
endRoomPushPreview()
|
|
1212
|
+
},
|
|
952
1213
|
})
|
|
953
1214
|
}
|
|
954
1215
|
|
|
@@ -960,12 +1221,260 @@ function pushDimensionRooms(nodes: Nodes, levelId: string, span: WallPushSpan, b
|
|
|
960
1221
|
const index = getRoomSelectionIndex(levelId)
|
|
961
1222
|
const records = index.update(nodes)
|
|
962
1223
|
const topology = index.topology.getLevelTopology(levelId)
|
|
963
|
-
|
|
1224
|
+
const elevationOf = (roomId: string) => {
|
|
964
1225
|
const zoneId = records.find((record) => record.id === roomId)?.zoneId
|
|
965
1226
|
return zoneId ? roomFloorElevation(nodes, zoneId) : base
|
|
1227
|
+
}
|
|
1228
|
+
const edge = span.separators?.length ? separatorEdgeLine(nodes, span.separators) : null
|
|
1229
|
+
return edge && span.separators
|
|
1230
|
+
? separatorEdgeDimensionRooms(
|
|
1231
|
+
topology?.rooms ?? [],
|
|
1232
|
+
{ line: edge, separators: span.separators },
|
|
1233
|
+
(id) => nodes[id],
|
|
1234
|
+
elevationOf,
|
|
1235
|
+
)
|
|
1236
|
+
: wallPushDimensionRooms(topology?.rooms ?? [], span, elevationOf)
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* The one line an edge of invisible walls pushes as: its first separator,
|
|
1241
|
+
* running from the edge's first end to its last.
|
|
1242
|
+
*/
|
|
1243
|
+
function separatorEdgeLine(nodes: Nodes, spans: readonly SeparatorSpan[]): SeparatorNode | null {
|
|
1244
|
+
const first = nodes[spans[0]?.separatorId as AnyNodeId]
|
|
1245
|
+
if (first?.type !== 'separator') return null
|
|
1246
|
+
const dx = first.end[0] - first.start[0]
|
|
1247
|
+
const dz = first.end[1] - first.start[1]
|
|
1248
|
+
const length = Math.hypot(dx, dz)
|
|
1249
|
+
if (length < 1e-6) return null
|
|
1250
|
+
const along = ([x, z]: Point) =>
|
|
1251
|
+
((x - first.start[0]) * dx + (z - first.start[1]) * dz) / (length * length)
|
|
1252
|
+
let lo = Number.POSITIVE_INFINITY
|
|
1253
|
+
let hi = Number.NEGATIVE_INFINITY
|
|
1254
|
+
for (const span of spans) {
|
|
1255
|
+
const separator = nodes[span.separatorId as AnyNodeId]
|
|
1256
|
+
if (separator?.type !== 'separator') return null
|
|
1257
|
+
for (const t of [span.t0, span.t1]) {
|
|
1258
|
+
const u = along([
|
|
1259
|
+
separator.start[0] + (separator.end[0] - separator.start[0]) * t,
|
|
1260
|
+
separator.start[1] + (separator.end[1] - separator.start[1]) * t,
|
|
1261
|
+
])
|
|
1262
|
+
lo = Math.min(lo, u)
|
|
1263
|
+
hi = Math.max(hi, u)
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
const at = (u: number): Point => [first.start[0] + dx * u, first.start[1] + dz * u]
|
|
1267
|
+
return { ...first, start: at(lo), end: at(hi) }
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* A selected wall's temporary dimensions as the scene stands: the room widths
|
|
1272
|
+
* its push shows, before any push, plus the outside size for an exterior wall.
|
|
1273
|
+
*/
|
|
1274
|
+
export function selectedWallDimensions(
|
|
1275
|
+
nodes: Nodes,
|
|
1276
|
+
wall: WallNode,
|
|
1277
|
+
faces: WallDimensionFaces,
|
|
1278
|
+
): WallSelectionDimension[] {
|
|
1279
|
+
const levelId = wall.parentId
|
|
1280
|
+
if (!levelId) return []
|
|
1281
|
+
const base = getWallBaseElevationForNodes(wall, nodes)
|
|
1282
|
+
const walls = Object.values(nodes).filter(
|
|
1283
|
+
(node): node is WallNode => node.type === 'wall' && node.parentId === levelId,
|
|
1284
|
+
)
|
|
1285
|
+
return wallSelectionDimensions({
|
|
1286
|
+
rooms: pushDimensionRooms(nodes, levelId, { wallId: wall.id, t0: 0, t1: 1 }, base),
|
|
1287
|
+
wall,
|
|
1288
|
+
walls,
|
|
1289
|
+
faces,
|
|
1290
|
+
resolve: (id) => nodes[id],
|
|
1291
|
+
baseElevation: base,
|
|
966
1292
|
})
|
|
967
1293
|
}
|
|
968
1294
|
|
|
1295
|
+
/** An end this close to another moved wall is joined to it. */
|
|
1296
|
+
const GROUP_JOIN_REACH = 1e-3
|
|
1297
|
+
/** Walls whose directions cross by less than this (sine) run in line, never meet at a corner. */
|
|
1298
|
+
const GROUP_PARALLEL_SIN = 1e-4
|
|
1299
|
+
|
|
1300
|
+
/** The way a wall's push arrows point: its `a`-side normal at its middle. */
|
|
1301
|
+
function wallPushNormal(wall: WallNode): Point {
|
|
1302
|
+
const frame = getWallCurveFrameAt(wall, 0.5)
|
|
1303
|
+
return [frame.normal.x, frame.normal.y]
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function wallDirection(wall: WallNode): Point {
|
|
1307
|
+
const dx = wall.end[0] - wall.start[0]
|
|
1308
|
+
const dz = wall.end[1] - wall.start[1]
|
|
1309
|
+
const length = Math.hypot(dx, dz) || 1
|
|
1310
|
+
return [dx / length, dz / length]
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/** Whether `point` is on `wall`: one of its ends, or along a straight one. */
|
|
1314
|
+
function wallHoldsPoint(wall: WallNode, [x, z]: Point): boolean {
|
|
1315
|
+
const near = ([px, pz]: Point) => Math.hypot(px - x, pz - z) <= GROUP_JOIN_REACH
|
|
1316
|
+
if (near(wall.start) || near(wall.end)) return true
|
|
1317
|
+
if ((wall.curveOffset ?? 0) !== 0) return false
|
|
1318
|
+
const [ux, uz] = wallDirection(wall)
|
|
1319
|
+
const along = (x - wall.start[0]) * ux + (z - wall.start[1]) * uz
|
|
1320
|
+
const length = Math.hypot(wall.end[0] - wall.start[0], wall.end[1] - wall.start[1])
|
|
1321
|
+
return (
|
|
1322
|
+
along > 0 &&
|
|
1323
|
+
along < length &&
|
|
1324
|
+
Math.abs((z - wall.start[1]) * ux - (x - wall.start[0]) * uz) <= GROUP_JOIN_REACH
|
|
1325
|
+
)
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Moves several walls at once, each as its own push arrow would: along its
|
|
1330
|
+
* normal by `drag` projected on it, so it stays parallel to where it was, the
|
|
1331
|
+
* walls joined to it following through the junction planner. An end on
|
|
1332
|
+
* another moved wall that is not in line with it (a corner or a T between
|
|
1333
|
+
* them) sits on both moved lines, which is where it lands moved by the whole
|
|
1334
|
+
* drag, so they stay joined. Each wall is planned in turn while the others
|
|
1335
|
+
* already stand at their targets, so no moved wall is pulled along as
|
|
1336
|
+
* another's neighbour. One plan, one undo step.
|
|
1337
|
+
*/
|
|
1338
|
+
export function planWallGroupPush(
|
|
1339
|
+
nodes: StructureNodes,
|
|
1340
|
+
wallIds: readonly string[],
|
|
1341
|
+
drag: Point,
|
|
1342
|
+
mintId: (kind: 'zone' | 'wall' | 'separator') => string = generateId,
|
|
1343
|
+
): StructurePlan {
|
|
1344
|
+
const walls = wallIds.flatMap((id) => {
|
|
1345
|
+
const node = nodes[id]
|
|
1346
|
+
return node?.type === 'wall' ? [node] : []
|
|
1347
|
+
})
|
|
1348
|
+
const crosses = (a: WallNode, b: WallNode) => {
|
|
1349
|
+
const [ux, uz] = wallDirection(a)
|
|
1350
|
+
const [vx, vz] = wallDirection(b)
|
|
1351
|
+
return Math.abs(ux * vz - uz * vx) > GROUP_PARALLEL_SIN
|
|
1352
|
+
}
|
|
1353
|
+
let scratch: Scratch = { ...nodes }
|
|
1354
|
+
for (const wall of walls) {
|
|
1355
|
+
const [nx, nz] = wallPushNormal(wall)
|
|
1356
|
+
const distance = drag[0] * nx + drag[1] * nz
|
|
1357
|
+
const move = (point: Point): Point =>
|
|
1358
|
+
walls.some((other) => other !== wall && crosses(wall, other) && wallHoldsPoint(other, point))
|
|
1359
|
+
? [point[0] + drag[0], point[1] + drag[1]]
|
|
1360
|
+
: [point[0] + nx * distance, point[1] + nz * distance]
|
|
1361
|
+
scratch[wall.id] = { ...wall, start: move(wall.start), end: move(wall.end) }
|
|
1362
|
+
}
|
|
1363
|
+
for (const wall of walls) {
|
|
1364
|
+
const target = scratch[wall.id]
|
|
1365
|
+
if (target?.type !== 'wall') continue
|
|
1366
|
+
scratch[wall.id] = { ...target, start: wall.start, end: wall.end }
|
|
1367
|
+
const moved = setWallGeometry(scratch, {
|
|
1368
|
+
wallId: wall.id,
|
|
1369
|
+
start: target.start,
|
|
1370
|
+
end: target.end,
|
|
1371
|
+
mintId,
|
|
1372
|
+
})
|
|
1373
|
+
if (moved.conflicts?.length) return moved
|
|
1374
|
+
scratch = applyChanges(scratch, moved.changes)
|
|
1375
|
+
}
|
|
1376
|
+
return { changes: diffNodes(nodes, scratch) }
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/** A multi-wall push in flight (`beginWallGroupPush`). */
|
|
1380
|
+
export type WallGroupPush = {
|
|
1381
|
+
/** Previews the walls moved by the level-plan drag. */
|
|
1382
|
+
preview: (drag: Point) => void
|
|
1383
|
+
/** Writes the last preview as one undo step; false when nothing was written. */
|
|
1384
|
+
commit: () => boolean
|
|
1385
|
+
/** Drops the preview; the scene is untouched. */
|
|
1386
|
+
cancel: () => void
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* The live side of `planWallGroupPush`, for a group move whose selection is
|
|
1391
|
+
* walls only: the walls, their neighbours and the room surfaces preview
|
|
1392
|
+
* through overrides, created pieces as ghosts, and each wall's room widths
|
|
1393
|
+
* show as its own push shows them (`useRoomHandleDrag`, kind
|
|
1394
|
+
* `wall-group-push`). The caller owns the gesture: its pointer, keys and scope.
|
|
1395
|
+
*/
|
|
1396
|
+
export function beginWallGroupPush(wallIds: readonly string[], levelId: string): WallGroupPush {
|
|
1397
|
+
const nodes = useScene.getState().nodes
|
|
1398
|
+
const measured = wallIds.flatMap((id) => {
|
|
1399
|
+
const wall = nodes[id as AnyNodeId]
|
|
1400
|
+
if (wall?.type !== 'wall') return []
|
|
1401
|
+
const base = getWallBaseElevationForNodes(wall, nodes)
|
|
1402
|
+
return [
|
|
1403
|
+
{
|
|
1404
|
+
wall,
|
|
1405
|
+
base,
|
|
1406
|
+
top: base + getWallEffectiveHeightForNodes(wall, nodes),
|
|
1407
|
+
normal: wallPushNormal(wall),
|
|
1408
|
+
rooms: pushDimensionRooms(nodes, levelId, { wallId: wall.id, t0: 0, t1: 1 }, base),
|
|
1409
|
+
},
|
|
1410
|
+
]
|
|
1411
|
+
})
|
|
1412
|
+
const middle: Point = [0, 0]
|
|
1413
|
+
for (const { wall } of measured) {
|
|
1414
|
+
middle[0] += (wall.start[0] + wall.end[0]) / 2 / measured.length
|
|
1415
|
+
middle[1] += (wall.start[1] + wall.end[1]) / 2 / measured.length
|
|
1416
|
+
}
|
|
1417
|
+
let drag: Point = [0, 0]
|
|
1418
|
+
let blocked = false
|
|
1419
|
+
let shown = false
|
|
1420
|
+
const dimensions = () => {
|
|
1421
|
+
const kept: RoomDimension[] = []
|
|
1422
|
+
for (const { wall, normal, rooms } of measured) {
|
|
1423
|
+
const distance = drag[0] * normal[0] + drag[1] * normal[1]
|
|
1424
|
+
// Two moved walls across one room both measure it: the width shows once.
|
|
1425
|
+
for (const dimension of wallPushDimensions({
|
|
1426
|
+
rooms,
|
|
1427
|
+
wall,
|
|
1428
|
+
outward: normal,
|
|
1429
|
+
distance,
|
|
1430
|
+
resolve: livePreviewNode,
|
|
1431
|
+
}))
|
|
1432
|
+
if (!kept.some((other) => sameRoomDimension(other, dimension)))
|
|
1433
|
+
kept.push({ ...dimension, key: `${wall.id}:${dimension.key}` })
|
|
1434
|
+
}
|
|
1435
|
+
return kept
|
|
1436
|
+
}
|
|
1437
|
+
const publish = (ghosts: RoomPushGhost[], message?: string) =>
|
|
1438
|
+
useRoomHandleDrag.setState({
|
|
1439
|
+
drag: {
|
|
1440
|
+
kind: 'wall-group-push',
|
|
1441
|
+
levelId,
|
|
1442
|
+
anchor: [middle[0] + drag[0], middle[1] + drag[1]],
|
|
1443
|
+
height: Math.max(...measured.map(({ top }) => top)),
|
|
1444
|
+
base: Math.min(...measured.map(({ base }) => base)),
|
|
1445
|
+
ghosts,
|
|
1446
|
+
dimensions: dimensions(),
|
|
1447
|
+
message,
|
|
1448
|
+
},
|
|
1449
|
+
})
|
|
1450
|
+
const end = () => {
|
|
1451
|
+
endRoomPushPreview()
|
|
1452
|
+
useRoomHandleDrag.setState({ drag: null })
|
|
1453
|
+
}
|
|
1454
|
+
return {
|
|
1455
|
+
preview: (next) => {
|
|
1456
|
+
if (shown && next[0] === drag[0] && next[1] === drag[1]) return
|
|
1457
|
+
shown = true
|
|
1458
|
+
drag = [next[0], next[1]]
|
|
1459
|
+
clearPushPreview()
|
|
1460
|
+
const preview =
|
|
1461
|
+
Math.hypot(drag[0], drag[1]) < 1e-6
|
|
1462
|
+
? { ghosts: [] }
|
|
1463
|
+
: previewPushPlan(useScene.getState().nodes, levelId, () =>
|
|
1464
|
+
planWallGroupPush(useScene.getState().nodes, wallIds, drag, previewMint()),
|
|
1465
|
+
)
|
|
1466
|
+
blocked = !!preview.code
|
|
1467
|
+
publish(preview.ghosts, preview.code ? PUSH_REFUSED_MESSAGE : undefined)
|
|
1468
|
+
},
|
|
1469
|
+
commit: () => {
|
|
1470
|
+
end()
|
|
1471
|
+
if (blocked || Math.hypot(drag[0], drag[1]) < 1e-6) return false
|
|
1472
|
+
return commitPushPlan((current) => planWallGroupPush(current, wallIds, drag))
|
|
1473
|
+
},
|
|
1474
|
+
cancel: end,
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
969
1478
|
/** A node as the push preview draws it: the scene's, with its live override merged. */
|
|
970
1479
|
function livePreviewNode(id: string): AnyNode | undefined {
|
|
971
1480
|
const node = useScene.getState().nodes[id as AnyNodeId]
|
|
@@ -986,6 +1495,7 @@ export function wallPointAt(wall: WallNode, t: number): Point {
|
|
|
986
1495
|
*/
|
|
987
1496
|
export const WALL_PUSH_AFFORDANCE = 'wall-push'
|
|
988
1497
|
|
|
1498
|
+
/** A wall's or an invisible wall's side arrow: `wallId` is either. */
|
|
989
1499
|
export type WallPushArrowPayload = { wallId: string; side: 'a' | 'b' }
|
|
990
1500
|
|
|
991
1501
|
type PlanPointer = (clientX: number, clientY: number) => readonly [number, number] | null
|
|
@@ -1001,8 +1511,9 @@ function planAlong(position: Point, outward: Point, toPlan: PlanPointer) {
|
|
|
1001
1511
|
}
|
|
1002
1512
|
|
|
1003
1513
|
/**
|
|
1004
|
-
* A floor-plan wall arrow: the 3D arrow's handle (`wallPushHandles
|
|
1005
|
-
*
|
|
1514
|
+
* A floor-plan wall arrow: the 3D arrow's handle (`wallPushHandles`, or a
|
|
1515
|
+
* selected invisible wall's `separatorPushHandles`), drag (`runWallPushDrag`)
|
|
1516
|
+
* and commit, with the pointer read in plan coordinates.
|
|
1006
1517
|
*/
|
|
1007
1518
|
export function runFloorplanWallPush(
|
|
1008
1519
|
payload: unknown,
|
|
@@ -1013,8 +1524,12 @@ export function runFloorplanWallPush(
|
|
|
1013
1524
|
const { wallId, side } = (payload ?? {}) as Partial<WallPushArrowPayload>
|
|
1014
1525
|
const nodes = useScene.getState().nodes
|
|
1015
1526
|
const wall = wallId ? nodes[wallId as AnyNodeId] : undefined
|
|
1016
|
-
if (wall?.type !== 'wall' || !wall.parentId) return null
|
|
1017
|
-
const
|
|
1527
|
+
if ((wall?.type !== 'wall' && wall?.type !== 'separator') || !wall.parentId) return null
|
|
1528
|
+
const handles =
|
|
1529
|
+
wall.type === 'wall'
|
|
1530
|
+
? wallPushHandles(wall, nodes)
|
|
1531
|
+
: separatorPushHandles(wall, separatorFloorElevation(nodes, wall.id))
|
|
1532
|
+
const handle = handles.find((h) => h.key === `${wall.id}:${side}`)
|
|
1018
1533
|
if (!handle) return null
|
|
1019
1534
|
return runFloorplanRoomPush({ handle, levelId: wall.parentId, clientX, clientY, toPlan })
|
|
1020
1535
|
}
|