@pascal-app/editor 0.7.0 → 0.9.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 +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5837 -12006
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +234 -190
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +435 -158
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +2 -2
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -543
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +57 -1
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +24 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +342 -91
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/tsconfig.json +2 -1
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -230
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
|
+
collectAlignmentAnchors,
|
|
4
5
|
emitter,
|
|
5
6
|
type GridEvent,
|
|
6
7
|
type LevelNode,
|
|
7
8
|
RoofNode,
|
|
8
9
|
RoofSegmentNode,
|
|
9
10
|
sceneRegistry,
|
|
11
|
+
snapScalar,
|
|
10
12
|
useScene,
|
|
11
13
|
} from '@pascal-app/core'
|
|
14
|
+
import { useAlignmentGuides } from '@pascal-app/editor'
|
|
12
15
|
import { useViewer } from '@pascal-app/viewer'
|
|
13
16
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
14
17
|
import * as THREE from 'three'
|
|
@@ -16,12 +19,22 @@ import { BufferGeometry, DoubleSide, type Group, type Line, Vector3 } from 'thre
|
|
|
16
19
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
17
20
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
18
21
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
22
|
+
import {
|
|
23
|
+
resolveAlignmentForActiveBuilding,
|
|
24
|
+
snapWorldXZForActiveBuilding,
|
|
25
|
+
} from '../../../lib/world-grid-snap'
|
|
19
26
|
import useEditor from '../../../store/use-editor'
|
|
20
27
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
21
28
|
|
|
22
29
|
const DEFAULT_WALL_HEIGHT = 0.5
|
|
23
|
-
const
|
|
30
|
+
const DEFAULT_PITCH_DEG = 40
|
|
24
31
|
const GRID_OFFSET = 0.02
|
|
32
|
+
/** Figma-style alignment-snap threshold (meters), matching the move tools. */
|
|
33
|
+
const ALIGNMENT_THRESHOLD_M = 0.08
|
|
34
|
+
|
|
35
|
+
function snapToActiveGrid(value: number): number {
|
|
36
|
+
return snapScalar(value, useEditor.getState().gridSnapStep)
|
|
37
|
+
}
|
|
25
38
|
|
|
26
39
|
/**
|
|
27
40
|
* Creates a roof group with one default gable segment
|
|
@@ -34,6 +47,13 @@ const commitRoofPlacement = (
|
|
|
34
47
|
): AnyNode['id'] => {
|
|
35
48
|
const { createNode, createNodes, nodes } = useScene.getState()
|
|
36
49
|
|
|
50
|
+
// A placed roof preset seeds `toolDefaults.roof` with the flattened
|
|
51
|
+
// subtree params (roofType, pitch, wallHeight, overhang, materials, …)
|
|
52
|
+
// before the tool activates. The footprint (width/depth) and placement
|
|
53
|
+
// come from the drawn rectangle and always win; the segment carries the
|
|
54
|
+
// shape/material params, the roof container picks up the materials.
|
|
55
|
+
const defaults = useEditor.getState().toolDefaults.roof ?? {}
|
|
56
|
+
|
|
37
57
|
const centerX = (corner1[0] + corner2[0]) / 2
|
|
38
58
|
const centerZ = (corner1[2] + corner2[2]) / 2
|
|
39
59
|
|
|
@@ -74,11 +94,12 @@ const commitRoofPlacement = (
|
|
|
74
94
|
}
|
|
75
95
|
|
|
76
96
|
const segment = RoofSegmentNode.parse({
|
|
77
|
-
width,
|
|
78
|
-
depth,
|
|
79
97
|
wallHeight: DEFAULT_WALL_HEIGHT,
|
|
80
|
-
|
|
98
|
+
pitch: DEFAULT_PITCH_DEG,
|
|
81
99
|
roofType: 'gable',
|
|
100
|
+
...defaults,
|
|
101
|
+
width,
|
|
102
|
+
depth,
|
|
82
103
|
position: [localX, 0, localZ],
|
|
83
104
|
})
|
|
84
105
|
|
|
@@ -93,16 +114,19 @@ const commitRoofPlacement = (
|
|
|
93
114
|
|
|
94
115
|
// Create the segment first (centered in its new parent)
|
|
95
116
|
const segment = RoofSegmentNode.parse({
|
|
96
|
-
width,
|
|
97
|
-
depth,
|
|
98
117
|
wallHeight: DEFAULT_WALL_HEIGHT,
|
|
99
|
-
|
|
118
|
+
pitch: DEFAULT_PITCH_DEG,
|
|
100
119
|
roofType: 'gable',
|
|
120
|
+
...defaults,
|
|
121
|
+
width,
|
|
122
|
+
depth,
|
|
101
123
|
position: [0, 0, 0],
|
|
102
124
|
})
|
|
103
125
|
|
|
104
|
-
// Create the roof container
|
|
126
|
+
// Create the roof container. Segment-shaped params (roofType, pitch, …) are
|
|
127
|
+
// dropped by the RoofNode schema; surface materials in `defaults` carry over.
|
|
105
128
|
const roof = RoofNode.parse({
|
|
129
|
+
...defaults,
|
|
106
130
|
name,
|
|
107
131
|
position: [centerX, 0, centerZ],
|
|
108
132
|
children: [segment.id],
|
|
@@ -138,6 +162,10 @@ export const RoofTool: React.FC = () => {
|
|
|
138
162
|
selectedIdsRef.current = selectedIds
|
|
139
163
|
}, [selectedIds])
|
|
140
164
|
|
|
165
|
+
// Clear preset-seeded defaults on deactivation so a later manual roof draw
|
|
166
|
+
// isn't built with a stale preset's parameters. Unmount-only.
|
|
167
|
+
useEffect(() => () => useEditor.getState().setToolDefaults('roof', null), [])
|
|
168
|
+
|
|
141
169
|
const corner1Ref = useRef<[number, number, number] | null>(null)
|
|
142
170
|
const previousGridPosRef = useRef<[number, number] | null>(null)
|
|
143
171
|
const [preview, setPreview] = useState<PreviewState>({
|
|
@@ -151,6 +179,45 @@ export const RoofTool: React.FC = () => {
|
|
|
151
179
|
|
|
152
180
|
outlineRef.current.geometry = new BufferGeometry()
|
|
153
181
|
|
|
182
|
+
// Alignment candidates — anchors of every alignable object; refreshed
|
|
183
|
+
// after each roof commits. Both corners of the rectangle align.
|
|
184
|
+
let alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
|
|
185
|
+
// Snap the drafted corner onto another object's nearest real anchor and
|
|
186
|
+
// publish the guide. The probe is the RAW cursor, NOT the 0.5m-grid-snapped
|
|
187
|
+
// point: resolving against the grid point would only ever catch anchors
|
|
188
|
+
// that happen to sit on a grid line, so off-grid items (furniture, angled
|
|
189
|
+
// walls) would never surface a guide. The matched axis locks exactly to the
|
|
190
|
+
// candidate's coordinate; the other axis keeps its grid snap. Alt bypasses.
|
|
191
|
+
const alignPoint = (
|
|
192
|
+
gridX: number,
|
|
193
|
+
gridZ: number,
|
|
194
|
+
rawX: number,
|
|
195
|
+
rawZ: number,
|
|
196
|
+
bypass: boolean,
|
|
197
|
+
): [number, number] => {
|
|
198
|
+
if (bypass || alignmentCandidates.length === 0) {
|
|
199
|
+
useAlignmentGuides.getState().clear()
|
|
200
|
+
return [gridX, gridZ]
|
|
201
|
+
}
|
|
202
|
+
const ar = resolveAlignmentForActiveBuilding({
|
|
203
|
+
moving: [{ nodeId: '__roof-draft__', kind: 'corner', x: rawX, z: rawZ }],
|
|
204
|
+
candidates: alignmentCandidates,
|
|
205
|
+
threshold: ALIGNMENT_THRESHOLD_M,
|
|
206
|
+
})
|
|
207
|
+
if (ar.guides.length === 0) {
|
|
208
|
+
useAlignmentGuides.getState().clear()
|
|
209
|
+
return [gridX, gridZ]
|
|
210
|
+
}
|
|
211
|
+
useAlignmentGuides.getState().set(ar.guides)
|
|
212
|
+
let x = gridX
|
|
213
|
+
let z = gridZ
|
|
214
|
+
for (const guide of ar.guides) {
|
|
215
|
+
if (guide.axis === 'x') x = guide.coord
|
|
216
|
+
else z = guide.coord
|
|
217
|
+
}
|
|
218
|
+
return [x, z]
|
|
219
|
+
}
|
|
220
|
+
|
|
154
221
|
const updateOutline = (
|
|
155
222
|
corner1: [number, number, number],
|
|
156
223
|
corner2: [number, number, number],
|
|
@@ -173,8 +240,20 @@ export const RoofTool: React.FC = () => {
|
|
|
173
240
|
const onGridMove = (event: GridEvent) => {
|
|
174
241
|
if (!cursorRef.current) return
|
|
175
242
|
|
|
176
|
-
|
|
177
|
-
|
|
243
|
+
// World-grid snap projected into building-local; rotated buildings
|
|
244
|
+
// used to drag every roof corner off the visible grid.
|
|
245
|
+
const snapped = snapWorldXZForActiveBuilding(
|
|
246
|
+
event.position[0],
|
|
247
|
+
event.position[2],
|
|
248
|
+
useEditor.getState().gridSnapStep,
|
|
249
|
+
).local
|
|
250
|
+
const [gridX, gridZ] = alignPoint(
|
|
251
|
+
snapped[0],
|
|
252
|
+
snapped[1],
|
|
253
|
+
event.localPosition[0],
|
|
254
|
+
event.localPosition[2],
|
|
255
|
+
event.nativeEvent?.altKey === true,
|
|
256
|
+
)
|
|
178
257
|
const y = event.localPosition[1]
|
|
179
258
|
|
|
180
259
|
const cursorPosition: [number, number, number] = [gridX, y, gridZ]
|
|
@@ -206,8 +285,20 @@ export const RoofTool: React.FC = () => {
|
|
|
206
285
|
const onGridClick = (event: GridEvent) => {
|
|
207
286
|
if (!currentLevelId) return
|
|
208
287
|
|
|
209
|
-
|
|
210
|
-
|
|
288
|
+
// World-grid snap projected into building-local; rotated buildings
|
|
289
|
+
// used to drag every roof corner off the visible grid.
|
|
290
|
+
const snapped = snapWorldXZForActiveBuilding(
|
|
291
|
+
event.position[0],
|
|
292
|
+
event.position[2],
|
|
293
|
+
useEditor.getState().gridSnapStep,
|
|
294
|
+
).local
|
|
295
|
+
const [gridX, gridZ] = alignPoint(
|
|
296
|
+
snapped[0],
|
|
297
|
+
snapped[1],
|
|
298
|
+
event.localPosition[0],
|
|
299
|
+
event.localPosition[2],
|
|
300
|
+
event.nativeEvent?.altKey === true,
|
|
301
|
+
)
|
|
211
302
|
const y = event.localPosition[1]
|
|
212
303
|
|
|
213
304
|
if (corner1Ref.current) {
|
|
@@ -222,8 +313,11 @@ export const RoofTool: React.FC = () => {
|
|
|
222
313
|
|
|
223
314
|
corner1Ref.current = null
|
|
224
315
|
outlineRef.current.visible = false
|
|
316
|
+
alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
|
|
317
|
+
useAlignmentGuides.getState().clear()
|
|
225
318
|
} else {
|
|
226
319
|
corner1Ref.current = [gridX, y, gridZ]
|
|
320
|
+
sfxEmitter.emit('sfx:structure-build-start')
|
|
227
321
|
setPreview((prev) => ({
|
|
228
322
|
...prev,
|
|
229
323
|
corner1: corner1Ref.current,
|
|
@@ -238,6 +332,7 @@ export const RoofTool: React.FC = () => {
|
|
|
238
332
|
outlineRef.current.visible = false
|
|
239
333
|
setPreview((prev) => ({ ...prev, corner1: null }))
|
|
240
334
|
}
|
|
335
|
+
useAlignmentGuides.getState().clear()
|
|
241
336
|
}
|
|
242
337
|
|
|
243
338
|
emitter.on('grid:move', onGridMove)
|
|
@@ -248,6 +343,7 @@ export const RoofTool: React.FC = () => {
|
|
|
248
343
|
emitter.off('grid:move', onGridMove)
|
|
249
344
|
emitter.off('grid:click', onGridClick)
|
|
250
345
|
emitter.off('tool:cancel', onCancel)
|
|
346
|
+
useAlignmentGuides.getState().clear()
|
|
251
347
|
|
|
252
348
|
corner1Ref.current = null
|
|
253
349
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export let boxSelectHandled = false
|
|
2
|
+
|
|
3
|
+
let resetTimeout: ReturnType<typeof setTimeout> | null = null
|
|
4
|
+
const suppressedPointerIds = new Set<number>()
|
|
5
|
+
const suppressionCleanups = new Map<number, () => void>()
|
|
6
|
+
|
|
7
|
+
type PointerEventLike = {
|
|
8
|
+
pointerId?: number
|
|
9
|
+
nativeEvent?: PointerEvent | PointerEventLike
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function pointerIdFor(event: PointerEvent | PointerEventLike): number | null {
|
|
13
|
+
if ('pointerId' in event && typeof event.pointerId === 'number') {
|
|
14
|
+
return event.pointerId
|
|
15
|
+
}
|
|
16
|
+
const nativeEvent = 'nativeEvent' in event ? event.nativeEvent : undefined
|
|
17
|
+
return nativeEvent ? pointerIdFor(nativeEvent) : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function markBoxSelectHandled() {
|
|
21
|
+
boxSelectHandled = true
|
|
22
|
+
if (resetTimeout) {
|
|
23
|
+
clearTimeout(resetTimeout)
|
|
24
|
+
}
|
|
25
|
+
resetTimeout = setTimeout(() => {
|
|
26
|
+
boxSelectHandled = false
|
|
27
|
+
resetTimeout = null
|
|
28
|
+
}, 50)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function suppressBoxSelectForPointer(event: PointerEvent | PointerEventLike) {
|
|
32
|
+
markBoxSelectHandled()
|
|
33
|
+
|
|
34
|
+
const pointerId = pointerIdFor(event)
|
|
35
|
+
if (pointerId === null || suppressedPointerIds.has(pointerId)) return
|
|
36
|
+
|
|
37
|
+
suppressedPointerIds.add(pointerId)
|
|
38
|
+
|
|
39
|
+
const clear = (releaseEvent?: PointerEvent) => {
|
|
40
|
+
if (releaseEvent && releaseEvent.pointerId !== pointerId) return
|
|
41
|
+
markBoxSelectHandled()
|
|
42
|
+
suppressedPointerIds.delete(pointerId)
|
|
43
|
+
const cleanup = suppressionCleanups.get(pointerId)
|
|
44
|
+
suppressionCleanups.delete(pointerId)
|
|
45
|
+
cleanup?.()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const onPointerUp = (releaseEvent: PointerEvent) => clear(releaseEvent)
|
|
49
|
+
const onPointerCancel = (releaseEvent: PointerEvent) => clear(releaseEvent)
|
|
50
|
+
const onBlur = () => clear()
|
|
51
|
+
const cleanup = () => {
|
|
52
|
+
window.removeEventListener('pointerup', onPointerUp)
|
|
53
|
+
window.removeEventListener('pointercancel', onPointerCancel)
|
|
54
|
+
window.removeEventListener('blur', onBlur)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
suppressionCleanups.set(pointerId, cleanup)
|
|
58
|
+
window.addEventListener('pointerup', onPointerUp)
|
|
59
|
+
window.addEventListener('pointercancel', onPointerCancel)
|
|
60
|
+
window.addEventListener('blur', onBlur)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isBoxSelectPointerSuppressed(event: PointerEvent | PointerEventLike) {
|
|
64
|
+
const pointerId = pointerIdFor(event)
|
|
65
|
+
return pointerId !== null && suppressedPointerIds.has(pointerId)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function clearBoxSelectHandled() {
|
|
69
|
+
if (resetTimeout) {
|
|
70
|
+
clearTimeout(resetTimeout)
|
|
71
|
+
resetTimeout = null
|
|
72
|
+
}
|
|
73
|
+
boxSelectHandled = false
|
|
74
|
+
for (const cleanup of suppressionCleanups.values()) cleanup()
|
|
75
|
+
suppressionCleanups.clear()
|
|
76
|
+
suppressedPointerIds.clear()
|
|
77
|
+
}
|