@pascal-app/editor 0.5.1 → 0.7.0
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 -7
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +75 -7
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +29 -0
- package/src/components/editor/first-person/build-collider-world.ts +365 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +281 -83
- package/src/components/editor/floating-building-action-menu.tsx +4 -3
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +10442 -3275
- package/src/components/editor/index.tsx +270 -20
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/selection-manager.tsx +766 -12
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +350 -157
- package/src/components/editor/use-floorplan-background-placement.ts +257 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
- package/src/components/editor/use-floorplan-scene-data.ts +189 -0
- package/src/components/editor/wall-measurement-label.tsx +377 -58
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
- package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
- package/src/components/editor-2d/svg-paths.ts +119 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +272 -0
- package/src/components/systems/roof/roof-edit-system.tsx +5 -5
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +2 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
- package/src/components/tools/column/column-tool.tsx +97 -0
- package/src/components/tools/column/move-column-tool.tsx +105 -0
- package/src/components/tools/door/door-tool.tsx +19 -0
- package/src/components/tools/door/move-door-tool.tsx +38 -8
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +27 -8
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +438 -0
- package/src/components/tools/fence/move-fence-tool.tsx +102 -27
- package/src/components/tools/item/move-tool.tsx +19 -1
- package/src/components/tools/item/placement-math.ts +44 -7
- package/src/components/tools/item/placement-strategies.ts +111 -33
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +2 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +701 -61
- package/src/components/tools/roof/move-roof-tool.tsx +111 -43
- package/src/components/tools/shared/polygon-editor.tsx +244 -29
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/move-slab-tool.tsx +182 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +2 -0
- package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
- package/src/components/tools/spawn/spawn-tool.tsx +130 -0
- package/src/components/tools/stair/stair-tool.tsx +11 -3
- package/src/components/tools/tool-manager.tsx +30 -3
- package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +423 -0
- package/src/components/tools/wall/move-wall-tool.tsx +356 -0
- package/src/components/tools/wall/wall-drafting.ts +348 -17
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +28 -0
- package/src/components/tools/window/window-tool.tsx +17 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +37 -5
- package/src/components/ui/action-menu/index.tsx +91 -1
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +424 -35
- package/src/components/ui/command-palette/editor-commands.tsx +27 -5
- package/src/components/ui/command-palette/index.tsx +0 -1
- package/src/components/ui/controls/material-picker.tsx +189 -169
- package/src/components/ui/controls/slider-control.tsx +88 -26
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1121 -1219
- package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
- package/src/components/ui/level-duplicate-dialog.tsx +115 -0
- package/src/components/ui/panels/ceiling-panel.tsx +47 -27
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +986 -294
- package/src/components/ui/panels/fence-panel.tsx +55 -12
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
- package/src/components/ui/panels/node-display.ts +39 -0
- package/src/components/ui/panels/paint-panel.tsx +138 -0
- package/src/components/ui/panels/panel-manager.tsx +241 -30
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +243 -9
- package/src/components/ui/panels/roof-panel.tsx +30 -62
- package/src/components/ui/panels/roof-segment-panel.tsx +8 -23
- package/src/components/ui/panels/slab-panel.tsx +46 -24
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +117 -69
- package/src/components/ui/panels/stair-segment-panel.tsx +13 -27
- package/src/components/ui/panels/wall-panel.tsx +71 -17
- package/src/components/ui/panels/window-panel.tsx +665 -146
- package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +138 -56
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +12 -6
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +96 -2
- package/src/components/viewer-overlay.tsx +25 -19
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-contextual-tools.ts +14 -13
- package/src/hooks/use-keyboard.ts +67 -9
- package/src/hooks/use-mobile.ts +12 -12
- package/src/index.tsx +2 -1
- package/src/lib/door-interaction.ts +88 -0
- package/src/lib/floorplan/geometry.ts +263 -0
- package/src/lib/floorplan/index.ts +38 -0
- package/src/lib/floorplan/items.ts +179 -0
- package/src/lib/floorplan/selection-tool.ts +231 -0
- package/src/lib/floorplan/stairs.ts +478 -0
- package/src/lib/floorplan/types.ts +57 -0
- package/src/lib/floorplan/walls.ts +23 -0
- package/src/lib/guide-events.ts +10 -0
- package/src/lib/history.ts +20 -0
- package/src/lib/level-duplication.test.ts +72 -0
- package/src/lib/level-duplication.ts +153 -0
- package/src/lib/local-guide-image.ts +42 -0
- package/src/lib/material-paint.ts +284 -0
- package/src/lib/roof-duplication.ts +214 -0
- package/src/lib/scene-bounds.test.ts +183 -0
- package/src/lib/scene-bounds.ts +169 -0
- package/src/lib/sfx-player.ts +96 -13
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +279 -15
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNodeId,
|
|
3
3
|
emitter,
|
|
4
|
+
type FenceNode,
|
|
4
5
|
type GridEvent,
|
|
6
|
+
type LevelNode,
|
|
5
7
|
type RoofNode,
|
|
6
8
|
type RoofSegmentNode,
|
|
7
9
|
type StairNode,
|
|
@@ -9,13 +11,17 @@ import {
|
|
|
9
11
|
sceneRegistry,
|
|
10
12
|
useLiveTransforms,
|
|
11
13
|
useScene,
|
|
14
|
+
type WallNode,
|
|
12
15
|
} from '@pascal-app/core'
|
|
13
16
|
import { useViewer } from '@pascal-app/viewer'
|
|
14
17
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
15
18
|
import * as THREE from 'three'
|
|
19
|
+
import { clearRoofDuplicateMetadata } from '../../../lib/roof-duplication'
|
|
16
20
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
17
21
|
import useEditor from '../../../store/use-editor'
|
|
22
|
+
import { snapFenceDraftPoint } from '../fence/fence-drafting'
|
|
18
23
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
24
|
+
import type { WallPlanPoint } from '../wall/wall-drafting'
|
|
19
25
|
|
|
20
26
|
export const MoveRoofTool: React.FC<{
|
|
21
27
|
node: RoofNode | RoofSegmentNode | StairNode | StairSegmentNode
|
|
@@ -29,9 +35,13 @@ export const MoveRoofTool: React.FC<{
|
|
|
29
35
|
const [cursorWorldPos, setCursorWorldPos] = useState<[number, number, number]>(() => {
|
|
30
36
|
const obj = sceneRegistry.nodes.get(movingNode.id)
|
|
31
37
|
if (obj) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
const worldPos = obj.getWorldPosition(new THREE.Vector3())
|
|
39
|
+
// Cursor renders inside the building-local ToolManager group, so convert
|
|
40
|
+
// world → building-local to honor any building rotation.
|
|
41
|
+
const buildingId = useViewer.getState().selection.buildingId
|
|
42
|
+
const buildingObj = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
|
|
43
|
+
if (buildingObj) buildingObj.worldToLocal(worldPos)
|
|
44
|
+
return [worldPos.x, worldPos.y, worldPos.z]
|
|
35
45
|
}
|
|
36
46
|
// Fallback if not registered (e.g. newly created duplicate without mesh yet)
|
|
37
47
|
if (
|
|
@@ -91,6 +101,7 @@ export const MoveRoofTool: React.FC<{
|
|
|
91
101
|
// resetting the mesh position (it resets on dirty) and from triggering
|
|
92
102
|
// expensive merged-mesh CSG rebuilds on every frame.
|
|
93
103
|
let wasCommitted = false
|
|
104
|
+
let wasCancelled = false
|
|
94
105
|
|
|
95
106
|
// Track pending rotation — no store updates during drag
|
|
96
107
|
let pendingRotation: number = movingNode.rotation as number
|
|
@@ -114,10 +125,55 @@ export const MoveRoofTool: React.FC<{
|
|
|
114
125
|
}
|
|
115
126
|
}
|
|
116
127
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
const resolveLevelId = () => {
|
|
129
|
+
if (movingNode.type === 'roof' || movingNode.type === 'stair') {
|
|
130
|
+
return movingNode.parentId ?? null
|
|
131
|
+
}
|
|
120
132
|
|
|
133
|
+
if (
|
|
134
|
+
(movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') &&
|
|
135
|
+
movingNode.parentId
|
|
136
|
+
) {
|
|
137
|
+
const parentNode = useScene.getState().nodes[movingNode.parentId as AnyNodeId]
|
|
138
|
+
return parentNode && 'parentId' in parentNode ? (parentNode.parentId ?? null) : null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const levelId = resolveLevelId()
|
|
145
|
+
const levelNode =
|
|
146
|
+
levelId && useScene.getState().nodes[levelId as AnyNodeId]?.type === 'level'
|
|
147
|
+
? (useScene.getState().nodes[levelId as AnyNodeId] as LevelNode)
|
|
148
|
+
: null
|
|
149
|
+
const levelChildren = levelNode?.children ?? []
|
|
150
|
+
const levelWalls = levelChildren
|
|
151
|
+
.map((childId) => useScene.getState().nodes[childId as AnyNodeId])
|
|
152
|
+
.filter((node): node is WallNode => node?.type === 'wall')
|
|
153
|
+
const levelFences = levelChildren
|
|
154
|
+
.map((childId) => useScene.getState().nodes[childId as AnyNodeId])
|
|
155
|
+
.filter((node): node is FenceNode => node?.type === 'fence')
|
|
156
|
+
const buildingId = useViewer.getState().selection.buildingId
|
|
157
|
+
const buildingObj = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
|
|
158
|
+
|
|
159
|
+
const localToWorldPoint = (localPoint: WallPlanPoint, y: number): [number, number, number] => {
|
|
160
|
+
if (buildingObj) {
|
|
161
|
+
const worldPoint = buildingObj.localToWorld(new THREE.Vector3(localPoint[0], y, localPoint[1]))
|
|
162
|
+
return [worldPoint.x, worldPoint.y, worldPoint.z]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return [localPoint[0], y, localPoint[1]]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const computeLocal = (
|
|
169
|
+
gridX: number,
|
|
170
|
+
gridZ: number,
|
|
171
|
+
y: number,
|
|
172
|
+
buildingLocalX: number,
|
|
173
|
+
buildingLocalZ: number,
|
|
174
|
+
): [number, number] => {
|
|
175
|
+
// Segments have a transformed parent (stair/roof). Convert world → parent-local
|
|
176
|
+
// via Three.js hierarchy so the segment's stored position stays parent-relative.
|
|
121
177
|
if (
|
|
122
178
|
(movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') &&
|
|
123
179
|
movingNode.parentId
|
|
@@ -128,40 +184,42 @@ export const MoveRoofTool: React.FC<{
|
|
|
128
184
|
if (parentObj) {
|
|
129
185
|
const worldVec = new THREE.Vector3(gridX, y, gridZ)
|
|
130
186
|
parentObj.worldToLocal(worldVec)
|
|
131
|
-
|
|
132
|
-
localZ = worldVec.z
|
|
133
|
-
} else {
|
|
134
|
-
const dx = gridX - (parentNode.position[0] as number)
|
|
135
|
-
const dz = gridZ - (parentNode.position[2] as number)
|
|
136
|
-
const angle = -(parentNode.rotation as number)
|
|
137
|
-
localX = dx * Math.cos(angle) - dz * Math.sin(angle)
|
|
138
|
-
localZ = dx * Math.sin(angle) + dz * Math.cos(angle)
|
|
187
|
+
return [worldVec.x, worldVec.z]
|
|
139
188
|
}
|
|
189
|
+
const dx = gridX - (parentNode.position[0] as number)
|
|
190
|
+
const dz = gridZ - (parentNode.position[2] as number)
|
|
191
|
+
const angle = -(parentNode.rotation as number)
|
|
192
|
+
return [
|
|
193
|
+
dx * Math.cos(angle) - dz * Math.sin(angle),
|
|
194
|
+
dx * Math.sin(angle) + dz * Math.cos(angle),
|
|
195
|
+
]
|
|
140
196
|
}
|
|
141
197
|
}
|
|
142
198
|
|
|
143
|
-
|
|
199
|
+
// Stair/roof live directly in the level — their stored position is building-local.
|
|
200
|
+
// event.localPosition is already building-local, so using it handles building rotation.
|
|
201
|
+
return [buildingLocalX, buildingLocalZ]
|
|
144
202
|
}
|
|
145
203
|
|
|
146
204
|
const onGridMove = (event: GridEvent) => {
|
|
147
|
-
const gridX = Math.round(event.position[0] * 2) / 2
|
|
148
|
-
const gridZ = Math.round(event.position[2] * 2) / 2
|
|
149
205
|
const y = event.position[1]
|
|
150
206
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
207
|
+
const snappedLocal = snapFenceDraftPoint({
|
|
208
|
+
point: [event.localPosition[0], event.localPosition[2]],
|
|
209
|
+
walls: levelWalls,
|
|
210
|
+
fences: levelFences,
|
|
211
|
+
})
|
|
212
|
+
const [gridX, , gridZ] = localToWorldPoint(snappedLocal, y)
|
|
213
|
+
|
|
214
|
+
if (previousGridPosRef.current && (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])) {
|
|
155
215
|
sfxEmitter.emit('sfx:grid-snap')
|
|
156
216
|
}
|
|
157
217
|
|
|
158
218
|
previousGridPosRef.current = [gridX, gridZ]
|
|
159
|
-
|
|
160
|
-
const lx = Math.round(event.localPosition[0] * 2) / 2
|
|
161
|
-
const lz = Math.round(event.localPosition[2] * 2) / 2
|
|
219
|
+
const [lx, lz] = snappedLocal
|
|
162
220
|
setCursorWorldPos([lx, event.localPosition[1], lz])
|
|
163
221
|
|
|
164
|
-
const [localX, localZ] = computeLocal(gridX, gridZ, y)
|
|
222
|
+
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
|
|
165
223
|
|
|
166
224
|
// Directly update the Three.js mesh — no store update during drag
|
|
167
225
|
const mesh = sceneRegistry.nodes.get(movingNode.id)
|
|
@@ -178,11 +236,16 @@ export const MoveRoofTool: React.FC<{
|
|
|
178
236
|
}
|
|
179
237
|
|
|
180
238
|
const onGridClick = (event: GridEvent) => {
|
|
181
|
-
const gridX = Math.round(event.position[0] * 2) / 2 // world, for computeLocal
|
|
182
|
-
const gridZ = Math.round(event.position[2] * 2) / 2
|
|
183
239
|
const y = event.position[1]
|
|
240
|
+
const snappedLocal = snapFenceDraftPoint({
|
|
241
|
+
point: [event.localPosition[0], event.localPosition[2]],
|
|
242
|
+
walls: levelWalls,
|
|
243
|
+
fences: levelFences,
|
|
244
|
+
})
|
|
245
|
+
const [gridX, , gridZ] = localToWorldPoint(snappedLocal, y)
|
|
246
|
+
const [lx, lz] = snappedLocal
|
|
184
247
|
|
|
185
|
-
const [localX, localZ] = computeLocal(gridX, gridZ, y)
|
|
248
|
+
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
|
|
186
249
|
|
|
187
250
|
wasCommitted = true
|
|
188
251
|
|
|
@@ -190,11 +253,19 @@ export const MoveRoofTool: React.FC<{
|
|
|
190
253
|
// Resume temporal and apply the final state as a single undoable step.
|
|
191
254
|
useScene.temporal.getState().resume()
|
|
192
255
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
256
|
+
if (isNew && movingNode.type === 'roof') {
|
|
257
|
+
clearRoofDuplicateMetadata(movingNode.id as AnyNodeId, {
|
|
258
|
+
position: [localX, movingNode.position[1], localZ],
|
|
259
|
+
rotation: pendingRotation,
|
|
260
|
+
metadata: committedMeta,
|
|
261
|
+
})
|
|
262
|
+
} else {
|
|
263
|
+
useScene.getState().updateNode(movingNode.id, {
|
|
264
|
+
position: [localX, movingNode.position[1], localZ],
|
|
265
|
+
rotation: pendingRotation,
|
|
266
|
+
metadata: committedMeta,
|
|
267
|
+
})
|
|
268
|
+
}
|
|
198
269
|
|
|
199
270
|
useScene.temporal.getState().pause()
|
|
200
271
|
|
|
@@ -206,6 +277,7 @@ export const MoveRoofTool: React.FC<{
|
|
|
206
277
|
}
|
|
207
278
|
|
|
208
279
|
const onCancel = () => {
|
|
280
|
+
wasCancelled = true
|
|
209
281
|
useLiveTransforms.getState().clear(movingNode.id)
|
|
210
282
|
if (isNew) {
|
|
211
283
|
useScene.getState().deleteNode(movingNode.id)
|
|
@@ -264,16 +336,12 @@ export const MoveRoofTool: React.FC<{
|
|
|
264
336
|
// Clear ephemeral live transform
|
|
265
337
|
useLiveTransforms.getState().clear(movingNode.id)
|
|
266
338
|
|
|
267
|
-
if (!wasCommitted) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
rotation: original.rotation,
|
|
274
|
-
metadata: original.metadata,
|
|
275
|
-
})
|
|
276
|
-
}
|
|
339
|
+
if (!(wasCommitted || wasCancelled || isNew)) {
|
|
340
|
+
useScene.getState().updateNode(movingNode.id, {
|
|
341
|
+
position: original.position,
|
|
342
|
+
rotation: original.rotation,
|
|
343
|
+
metadata: original.metadata,
|
|
344
|
+
})
|
|
277
345
|
}
|
|
278
346
|
useScene.temporal.getState().resume()
|
|
279
347
|
emitter.off('grid:move', onGridMove)
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { emitter, type GridEvent, sceneRegistry } from '@pascal-app/core'
|
|
2
2
|
import { createPortal } from '@react-three/fiber'
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
-
import { BufferGeometry, Float32BufferAttribute, type Line } from 'three'
|
|
4
|
+
import { BufferGeometry, Float32BufferAttribute, type Line, type Object3D } from 'three'
|
|
5
5
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
6
6
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
7
|
+
import { snapToHalf } from '../item/placement-math'
|
|
7
8
|
|
|
8
9
|
const Y_OFFSET = 0.02
|
|
9
10
|
|
|
10
11
|
type DragState = {
|
|
11
12
|
isDragging: boolean
|
|
12
|
-
|
|
13
|
+
mode: 'vertex' | 'polygon' | 'edge'
|
|
14
|
+
vertexIndex: number | null
|
|
15
|
+
edgeIndex?: number
|
|
16
|
+
edgeNormal?: [number, number]
|
|
13
17
|
initialPosition: [number, number]
|
|
18
|
+
initialPolygon: Array<[number, number]>
|
|
14
19
|
pointerId: number
|
|
15
20
|
}
|
|
16
21
|
|
|
@@ -23,6 +28,10 @@ export interface PolygonEditorProps {
|
|
|
23
28
|
levelId?: string
|
|
24
29
|
/** Height of the surface being edited (e.g. slab elevation). Handles adapt to this. */
|
|
25
30
|
surfaceHeight?: number
|
|
31
|
+
/** Whether to show the center handle that moves the entire polygon. */
|
|
32
|
+
allowPolygonMove?: boolean
|
|
33
|
+
/** Whether polygon edges can be dragged along their perpendicular normal. */
|
|
34
|
+
allowEdgeMove?: boolean
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
/**
|
|
@@ -30,6 +39,17 @@ export interface PolygonEditorProps {
|
|
|
30
39
|
* Used by zone and site boundary editors
|
|
31
40
|
*/
|
|
32
41
|
const MIN_HANDLE_HEIGHT = 0.15
|
|
42
|
+
const EDGE_HANDLE_HEIGHT = 0.06
|
|
43
|
+
const EDGE_HANDLE_THICKNESS = 0.12
|
|
44
|
+
|
|
45
|
+
function getEdgeNormal(start: [number, number], end: [number, number]): [number, number] | null {
|
|
46
|
+
const dx = end[0] - start[0]
|
|
47
|
+
const dz = end[1] - start[1]
|
|
48
|
+
const length = Math.hypot(dx, dz)
|
|
49
|
+
if (length < 1e-6) return null
|
|
50
|
+
|
|
51
|
+
return [-dz / length, dx / length]
|
|
52
|
+
}
|
|
33
53
|
|
|
34
54
|
export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
35
55
|
polygon,
|
|
@@ -38,9 +58,43 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
38
58
|
minVertices = 3,
|
|
39
59
|
levelId,
|
|
40
60
|
surfaceHeight = 0,
|
|
61
|
+
allowPolygonMove = false,
|
|
62
|
+
allowEdgeMove = false,
|
|
41
63
|
}) => {
|
|
42
|
-
|
|
43
|
-
|
|
64
|
+
const [levelNode, setLevelNode] = useState<Object3D | null>(() =>
|
|
65
|
+
levelId ? (sceneRegistry.nodes.get(levelId) ?? null) : null,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!levelId) {
|
|
70
|
+
setLevelNode(null)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let frameId = 0
|
|
75
|
+
|
|
76
|
+
const resolveLevelNode = () => {
|
|
77
|
+
const nextLevelNode = sceneRegistry.nodes.get(levelId) ?? null
|
|
78
|
+
setLevelNode((currentLevelNode) => {
|
|
79
|
+
if (currentLevelNode === nextLevelNode) {
|
|
80
|
+
return currentLevelNode
|
|
81
|
+
}
|
|
82
|
+
return nextLevelNode
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (!nextLevelNode) {
|
|
86
|
+
frameId = window.requestAnimationFrame(resolveLevelNode)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
resolveLevelNode()
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
if (frameId) {
|
|
94
|
+
window.cancelAnimationFrame(frameId)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}, [levelId])
|
|
44
98
|
|
|
45
99
|
// When using portal, edit at Y_OFFSET (local to level)
|
|
46
100
|
// When not using portal, edit at world origin
|
|
@@ -51,6 +105,11 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
51
105
|
const [previewPolygon, setPreviewPolygon] = useState<Array<[number, number]> | null>(null)
|
|
52
106
|
const previewPolygonRef = useRef<Array<[number, number]> | null>(null)
|
|
53
107
|
|
|
108
|
+
const updatePreviewPolygon = useCallback((nextPolygon: Array<[number, number]> | null) => {
|
|
109
|
+
previewPolygonRef.current = nextPolygon
|
|
110
|
+
setPreviewPolygon(nextPolygon)
|
|
111
|
+
}, [])
|
|
112
|
+
|
|
54
113
|
// Keep ref in sync
|
|
55
114
|
useEffect(() => {
|
|
56
115
|
previewPolygonRef.current = previewPolygon
|
|
@@ -58,6 +117,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
58
117
|
|
|
59
118
|
const [hoveredVertex, setHoveredVertex] = useState<number | null>(null)
|
|
60
119
|
const [hoveredMidpoint, setHoveredMidpoint] = useState<number | null>(null)
|
|
120
|
+
const [hoveredEdge, setHoveredEdge] = useState<number | null>(null)
|
|
61
121
|
const [cursorPosition, setCursorPosition] = useState<[number, number]>([0, 0])
|
|
62
122
|
|
|
63
123
|
const lineRef = useRef<Line>(null!)
|
|
@@ -68,13 +128,24 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
68
128
|
if (polygon !== lastPolygonRef.current) {
|
|
69
129
|
lastPolygonRef.current = polygon
|
|
70
130
|
// External change (e.g. undo/redo) — clear any stale preview/drag state
|
|
71
|
-
if (previewPolygon)
|
|
131
|
+
if (previewPolygon) updatePreviewPolygon(null)
|
|
72
132
|
if (dragState) setDragState(null)
|
|
73
133
|
}
|
|
74
134
|
|
|
75
135
|
// The polygon to display (preview during drag, or actual polygon)
|
|
76
136
|
const displayPolygon = previewPolygon ?? polygon
|
|
77
137
|
|
|
138
|
+
const polygonCenter = useMemo(() => {
|
|
139
|
+
if (displayPolygon.length === 0) return [0, 0] as [number, number]
|
|
140
|
+
let sumX = 0
|
|
141
|
+
let sumZ = 0
|
|
142
|
+
for (const [x, z] of displayPolygon) {
|
|
143
|
+
sumX += x
|
|
144
|
+
sumZ += z
|
|
145
|
+
}
|
|
146
|
+
return [sumX / displayPolygon.length, sumZ / displayPolygon.length] as [number, number]
|
|
147
|
+
}, [displayPolygon])
|
|
148
|
+
|
|
78
149
|
// Calculate midpoints for adding new vertices
|
|
79
150
|
const midpoints = useMemo(() => {
|
|
80
151
|
if (displayPolygon.length < 2) return []
|
|
@@ -85,17 +156,37 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
85
156
|
})
|
|
86
157
|
}, [displayPolygon])
|
|
87
158
|
|
|
159
|
+
const edgeHandles = useMemo(() => {
|
|
160
|
+
if (displayPolygon.length < 2) return []
|
|
161
|
+
|
|
162
|
+
return displayPolygon.flatMap(([x1, z1], index) => {
|
|
163
|
+
const nextIndex = (index + 1) % displayPolygon.length
|
|
164
|
+
const [x2, z2] = displayPolygon[nextIndex]!
|
|
165
|
+
const dx = x2 - x1
|
|
166
|
+
const dz = z2 - z1
|
|
167
|
+
const length = Math.hypot(dx, dz)
|
|
168
|
+
if (length < 1e-6) return []
|
|
169
|
+
|
|
170
|
+
return [
|
|
171
|
+
{
|
|
172
|
+
index,
|
|
173
|
+
length,
|
|
174
|
+
midpoint: [(x1 + x2) / 2, (z1 + z2) / 2] as [number, number],
|
|
175
|
+
rotationY: -Math.atan2(dz, dx),
|
|
176
|
+
},
|
|
177
|
+
]
|
|
178
|
+
})
|
|
179
|
+
}, [displayPolygon])
|
|
180
|
+
|
|
88
181
|
// Update vertex position using grid cursor position
|
|
89
182
|
const handleVertexDrag = useCallback(
|
|
90
183
|
(vertexIndex: number, position: [number, number]) => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return newPolygon
|
|
96
|
-
})
|
|
184
|
+
const basePolygon = previewPolygonRef.current ?? polygon
|
|
185
|
+
const newPolygon = [...basePolygon]
|
|
186
|
+
newPolygon[vertexIndex] = position
|
|
187
|
+
updatePreviewPolygon(newPolygon)
|
|
97
188
|
},
|
|
98
|
-
[polygon],
|
|
189
|
+
[polygon, updatePreviewPolygon],
|
|
99
190
|
)
|
|
100
191
|
|
|
101
192
|
// Commit polygon changes
|
|
@@ -103,9 +194,9 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
103
194
|
if (previewPolygonRef.current) {
|
|
104
195
|
onPolygonChange(previewPolygonRef.current)
|
|
105
196
|
}
|
|
106
|
-
|
|
197
|
+
updatePreviewPolygon(null)
|
|
107
198
|
setDragState(null)
|
|
108
|
-
}, [onPolygonChange])
|
|
199
|
+
}, [onPolygonChange, updatePreviewPolygon])
|
|
109
200
|
|
|
110
201
|
// Handle adding a new vertex at midpoint
|
|
111
202
|
const handleAddVertex = useCallback(
|
|
@@ -117,10 +208,13 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
117
208
|
...basePolygon.slice(afterIndex + 1),
|
|
118
209
|
]
|
|
119
210
|
|
|
120
|
-
|
|
121
|
-
return
|
|
211
|
+
updatePreviewPolygon(newPolygon)
|
|
212
|
+
return {
|
|
213
|
+
polygon: newPolygon,
|
|
214
|
+
vertexIndex: afterIndex + 1,
|
|
215
|
+
}
|
|
122
216
|
},
|
|
123
|
-
[polygon, previewPolygon],
|
|
217
|
+
[polygon, previewPolygon, updatePreviewPolygon],
|
|
124
218
|
)
|
|
125
219
|
|
|
126
220
|
// Handle deleting a vertex
|
|
@@ -131,16 +225,16 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
131
225
|
|
|
132
226
|
const newPolygon = basePolygon.filter((_, i) => i !== index)
|
|
133
227
|
onPolygonChange(newPolygon)
|
|
134
|
-
|
|
228
|
+
updatePreviewPolygon(null)
|
|
135
229
|
},
|
|
136
|
-
[polygon, previewPolygon, onPolygonChange, minVertices],
|
|
230
|
+
[polygon, previewPolygon, onPolygonChange, minVertices, updatePreviewPolygon],
|
|
137
231
|
)
|
|
138
232
|
|
|
139
233
|
// Listen to grid:move events to track cursor position
|
|
140
234
|
useEffect(() => {
|
|
141
235
|
const onGridMove = (event: GridEvent) => {
|
|
142
|
-
const gridX =
|
|
143
|
-
const gridZ =
|
|
236
|
+
const gridX = snapToHalf(event.localPosition[0])
|
|
237
|
+
const gridZ = snapToHalf(event.localPosition[2])
|
|
144
238
|
const newPosition: [number, number] = [gridX, gridZ]
|
|
145
239
|
|
|
146
240
|
// Play snap sound when cursor moves to a new grid cell during drag
|
|
@@ -158,7 +252,37 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
158
252
|
|
|
159
253
|
// Update vertex position during drag
|
|
160
254
|
if (dragState?.isDragging) {
|
|
161
|
-
|
|
255
|
+
if (dragState.mode === 'vertex' && dragState.vertexIndex !== null) {
|
|
256
|
+
handleVertexDrag(dragState.vertexIndex, newPosition)
|
|
257
|
+
} else if (dragState.mode === 'polygon') {
|
|
258
|
+
const deltaX = newPosition[0] - dragState.initialPosition[0]
|
|
259
|
+
const deltaZ = newPosition[1] - dragState.initialPosition[1]
|
|
260
|
+
updatePreviewPolygon(
|
|
261
|
+
dragState.initialPolygon.map(([x, z]) => [x + deltaX, z + deltaZ] as [number, number]),
|
|
262
|
+
)
|
|
263
|
+
} else if (
|
|
264
|
+
dragState.mode === 'edge' &&
|
|
265
|
+
dragState.edgeIndex !== undefined &&
|
|
266
|
+
dragState.edgeNormal
|
|
267
|
+
) {
|
|
268
|
+
const [normalX, normalZ] = dragState.edgeNormal
|
|
269
|
+
const pointerDeltaX = newPosition[0] - dragState.initialPosition[0]
|
|
270
|
+
const pointerDeltaZ = newPosition[1] - dragState.initialPosition[1]
|
|
271
|
+
const normalDistance = pointerDeltaX * normalX + pointerDeltaZ * normalZ
|
|
272
|
+
const edgeStartIndex = dragState.edgeIndex
|
|
273
|
+
const edgeEndIndex = (edgeStartIndex + 1) % dragState.initialPolygon.length
|
|
274
|
+
const nextPolygon = dragState.initialPolygon.map((point, index) => {
|
|
275
|
+
if (index !== edgeStartIndex && index !== edgeEndIndex) {
|
|
276
|
+
return point
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return [point[0] + normalX * normalDistance, point[1] + normalZ * normalDistance] as [
|
|
280
|
+
number,
|
|
281
|
+
number,
|
|
282
|
+
]
|
|
283
|
+
})
|
|
284
|
+
updatePreviewPolygon(nextPolygon)
|
|
285
|
+
}
|
|
162
286
|
}
|
|
163
287
|
}
|
|
164
288
|
|
|
@@ -166,7 +290,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
166
290
|
return () => {
|
|
167
291
|
emitter.off('grid:move', onGridMove)
|
|
168
292
|
}
|
|
169
|
-
}, [dragState, handleVertexDrag])
|
|
293
|
+
}, [dragState, handleVertexDrag, updatePreviewPolygon])
|
|
170
294
|
|
|
171
295
|
// Set up pointer up listener for ending drag
|
|
172
296
|
useEffect(() => {
|
|
@@ -231,6 +355,8 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
231
355
|
if (displayPolygon.length < minVertices) return null
|
|
232
356
|
|
|
233
357
|
const canDelete = displayPolygon.length > minVertices
|
|
358
|
+
const handleHeight = Math.max(MIN_HANDLE_HEIGHT, surfaceHeight + 0.02)
|
|
359
|
+
const edgeHandleY = editY + handleHeight - EDGE_HANDLE_HEIGHT / 2
|
|
234
360
|
|
|
235
361
|
const editorContent = (
|
|
236
362
|
<group>
|
|
@@ -257,9 +383,9 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
257
383
|
{/* Vertex handles - blue cylinders that match surface height */}
|
|
258
384
|
{displayPolygon.map(([x, z], index) => {
|
|
259
385
|
const isHovered = hoveredVertex === index
|
|
260
|
-
const isDragging = dragState?.vertexIndex === index
|
|
386
|
+
const isDragging = dragState?.mode === 'vertex' && dragState.vertexIndex === index
|
|
261
387
|
const radius = 0.1
|
|
262
|
-
const height =
|
|
388
|
+
const height = handleHeight
|
|
263
389
|
|
|
264
390
|
return (
|
|
265
391
|
<mesh
|
|
@@ -280,10 +406,13 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
280
406
|
onPointerDown={(e) => {
|
|
281
407
|
if (e.button !== 0) return
|
|
282
408
|
e.stopPropagation()
|
|
409
|
+
setHoveredEdge(null)
|
|
283
410
|
setDragState({
|
|
284
411
|
isDragging: true,
|
|
412
|
+
mode: 'vertex',
|
|
285
413
|
vertexIndex: index,
|
|
286
414
|
initialPosition: [x!, z!],
|
|
415
|
+
initialPolygon: displayPolygon.map(([px, pz]) => [px, pz] as [number, number]),
|
|
287
416
|
pointerId: e.pointerId,
|
|
288
417
|
})
|
|
289
418
|
}}
|
|
@@ -305,12 +434,96 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
305
434
|
)
|
|
306
435
|
})}
|
|
307
436
|
|
|
437
|
+
{allowPolygonMove && (
|
|
438
|
+
<mesh
|
|
439
|
+
castShadow
|
|
440
|
+
layers={EDITOR_LAYER}
|
|
441
|
+
onClick={(e) => {
|
|
442
|
+
if (e.button !== 0) return
|
|
443
|
+
e.stopPropagation()
|
|
444
|
+
}}
|
|
445
|
+
onPointerDown={(e) => {
|
|
446
|
+
if (e.button !== 0) return
|
|
447
|
+
e.stopPropagation()
|
|
448
|
+
setHoveredEdge(null)
|
|
449
|
+
setDragState({
|
|
450
|
+
isDragging: true,
|
|
451
|
+
mode: 'polygon',
|
|
452
|
+
vertexIndex: null,
|
|
453
|
+
initialPosition: polygonCenter,
|
|
454
|
+
initialPolygon: displayPolygon.map(([px, pz]) => [px, pz] as [number, number]),
|
|
455
|
+
pointerId: e.pointerId,
|
|
456
|
+
})
|
|
457
|
+
}}
|
|
458
|
+
position={[polygonCenter[0], editY + handleHeight + 0.08, polygonCenter[1]]}
|
|
459
|
+
>
|
|
460
|
+
<sphereGeometry args={[0.09, 20, 20]} />
|
|
461
|
+
<meshStandardMaterial color={dragState?.mode === 'polygon' ? '#22c55e' : '#f59e0b'} />
|
|
462
|
+
</mesh>
|
|
463
|
+
)}
|
|
464
|
+
|
|
465
|
+
{allowEdgeMove &&
|
|
466
|
+
edgeHandles.map(({ index, length, midpoint, rotationY }) => {
|
|
467
|
+
const isHovered = hoveredEdge === index
|
|
468
|
+
const isDragging = dragState?.mode === 'edge' && dragState.edgeIndex === index
|
|
469
|
+
|
|
470
|
+
return (
|
|
471
|
+
<mesh
|
|
472
|
+
key={`edge-${index}`}
|
|
473
|
+
layers={EDITOR_LAYER}
|
|
474
|
+
onClick={(e) => {
|
|
475
|
+
if (e.button !== 0) return
|
|
476
|
+
e.stopPropagation()
|
|
477
|
+
}}
|
|
478
|
+
onPointerDown={(e) => {
|
|
479
|
+
if (e.button !== 0) return
|
|
480
|
+
e.stopPropagation()
|
|
481
|
+
const start = displayPolygon[index]
|
|
482
|
+
const end = displayPolygon[(index + 1) % displayPolygon.length]
|
|
483
|
+
if (!(start && end)) return
|
|
484
|
+
|
|
485
|
+
const edgeNormal = getEdgeNormal(start, end)
|
|
486
|
+
if (!edgeNormal) return
|
|
487
|
+
|
|
488
|
+
setHoveredEdge(null)
|
|
489
|
+
setDragState({
|
|
490
|
+
isDragging: true,
|
|
491
|
+
mode: 'edge',
|
|
492
|
+
vertexIndex: null,
|
|
493
|
+
edgeIndex: index,
|
|
494
|
+
edgeNormal,
|
|
495
|
+
initialPosition: cursorPosition,
|
|
496
|
+
initialPolygon: displayPolygon.map(([px, pz]) => [px, pz] as [number, number]),
|
|
497
|
+
pointerId: e.pointerId,
|
|
498
|
+
})
|
|
499
|
+
}}
|
|
500
|
+
onPointerEnter={(e) => {
|
|
501
|
+
e.stopPropagation()
|
|
502
|
+
setHoveredEdge(index)
|
|
503
|
+
}}
|
|
504
|
+
onPointerLeave={(e) => {
|
|
505
|
+
e.stopPropagation()
|
|
506
|
+
setHoveredEdge(null)
|
|
507
|
+
}}
|
|
508
|
+
position={[midpoint[0], edgeHandleY, midpoint[1]]}
|
|
509
|
+
rotation={[0, rotationY, 0]}
|
|
510
|
+
>
|
|
511
|
+
<boxGeometry args={[length, EDGE_HANDLE_HEIGHT, EDGE_HANDLE_THICKNESS]} />
|
|
512
|
+
<meshStandardMaterial
|
|
513
|
+
color={isDragging ? '#22c55e' : '#94a3b8'}
|
|
514
|
+
opacity={isDragging ? 0.5 : isHovered ? 0.38 : 0.14}
|
|
515
|
+
transparent
|
|
516
|
+
/>
|
|
517
|
+
</mesh>
|
|
518
|
+
)
|
|
519
|
+
})}
|
|
520
|
+
|
|
308
521
|
{/* Midpoint handles - smaller green cylinders for adding vertices (hidden while dragging) */}
|
|
309
522
|
{!dragState &&
|
|
310
523
|
midpoints.map(([x, z], index) => {
|
|
311
524
|
const isHovered = hoveredMidpoint === index
|
|
312
525
|
const radius = 0.06
|
|
313
|
-
const height =
|
|
526
|
+
const height = handleHeight
|
|
314
527
|
|
|
315
528
|
return (
|
|
316
529
|
<mesh
|
|
@@ -323,12 +536,14 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
323
536
|
onPointerDown={(e) => {
|
|
324
537
|
if (e.button !== 0) return
|
|
325
538
|
e.stopPropagation()
|
|
326
|
-
const
|
|
327
|
-
if (
|
|
539
|
+
const insertedVertex = handleAddVertex(index, [x!, z!])
|
|
540
|
+
if (insertedVertex.vertexIndex >= 0) {
|
|
328
541
|
setDragState({
|
|
329
542
|
isDragging: true,
|
|
330
|
-
|
|
543
|
+
mode: 'vertex',
|
|
544
|
+
vertexIndex: insertedVertex.vertexIndex,
|
|
331
545
|
initialPosition: [x!, z!],
|
|
546
|
+
initialPolygon: insertedVertex.polygon,
|
|
332
547
|
pointerId: e.pointerId,
|
|
333
548
|
})
|
|
334
549
|
setHoveredMidpoint(null)
|