@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,182 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
type AnyNodeId,
|
|
5
|
-
emitter,
|
|
6
|
-
type FenceNode,
|
|
7
|
-
type GridEvent,
|
|
8
|
-
type LevelNode,
|
|
9
|
-
type SlabNode,
|
|
10
|
-
useScene,
|
|
11
|
-
type WallNode,
|
|
12
|
-
} from '@pascal-app/core'
|
|
13
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
14
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
15
|
-
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
16
|
-
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
17
|
-
import useEditor from '../../../store/use-editor'
|
|
18
|
-
import { snapFenceDraftPoint } from '../fence/fence-drafting'
|
|
19
|
-
import { CursorSphere } from '../shared/cursor-sphere'
|
|
20
|
-
|
|
21
|
-
function translatePolygon(
|
|
22
|
-
polygon: Array<[number, number]>,
|
|
23
|
-
deltaX: number,
|
|
24
|
-
deltaZ: number,
|
|
25
|
-
): Array<[number, number]> {
|
|
26
|
-
return polygon.map(([x, z]) => [x + deltaX, z + deltaZ] as [number, number])
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getPolygonCenter(polygon: Array<[number, number]>): [number, number] {
|
|
30
|
-
if (polygon.length === 0) return [0, 0]
|
|
31
|
-
let sumX = 0
|
|
32
|
-
let sumZ = 0
|
|
33
|
-
for (const [x, z] of polygon) {
|
|
34
|
-
sumX += x
|
|
35
|
-
sumZ += z
|
|
36
|
-
}
|
|
37
|
-
return [sumX / polygon.length, sumZ / polygon.length]
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const MoveSlabTool: React.FC<{ node: SlabNode }> = ({ node }) => {
|
|
41
|
-
const activatedAtRef = useRef<number>(Date.now())
|
|
42
|
-
const originalPolygonRef = useRef(node.polygon.map(([x, z]) => [x, z] as [number, number]))
|
|
43
|
-
const originalHolesRef = useRef(
|
|
44
|
-
(node.holes ?? []).map((hole) => hole.map(([x, z]) => [x, z] as [number, number])),
|
|
45
|
-
)
|
|
46
|
-
const dragAnchorRef = useRef<[number, number] | null>(null)
|
|
47
|
-
const previousGridPosRef = useRef<[number, number] | null>(null)
|
|
48
|
-
const previewRef = useRef<{
|
|
49
|
-
polygon: Array<[number, number]>
|
|
50
|
-
holes: Array<Array<[number, number]>>
|
|
51
|
-
} | null>(null)
|
|
52
|
-
|
|
53
|
-
const [cursorLocalPos, setCursorLocalPos] = useState<[number, number, number]>(() => {
|
|
54
|
-
const center = getPolygonCenter(node.polygon)
|
|
55
|
-
return [center[0], 0, center[1]]
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
const exitMoveMode = useCallback(() => {
|
|
59
|
-
useEditor.getState().setMovingNode(null)
|
|
60
|
-
}, [])
|
|
61
|
-
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
const originalPolygon = originalPolygonRef.current
|
|
64
|
-
const originalHoles = originalHolesRef.current
|
|
65
|
-
const levelNode =
|
|
66
|
-
node.parentId && useScene.getState().nodes[node.parentId as AnyNodeId]?.type === 'level'
|
|
67
|
-
? (useScene.getState().nodes[node.parentId as AnyNodeId] as LevelNode)
|
|
68
|
-
: null
|
|
69
|
-
const levelChildren = levelNode?.children ?? []
|
|
70
|
-
const levelWalls = levelChildren
|
|
71
|
-
.map((childId) => useScene.getState().nodes[childId as AnyNodeId])
|
|
72
|
-
.filter((child): child is WallNode => child?.type === 'wall')
|
|
73
|
-
const levelFences = levelChildren
|
|
74
|
-
.map((childId) => useScene.getState().nodes[childId as AnyNodeId])
|
|
75
|
-
.filter((child): child is FenceNode => child?.type === 'fence')
|
|
76
|
-
|
|
77
|
-
useScene.temporal.getState().pause()
|
|
78
|
-
let wasCommitted = false
|
|
79
|
-
|
|
80
|
-
const applyPreview = (
|
|
81
|
-
polygon: Array<[number, number]>,
|
|
82
|
-
holes: Array<Array<[number, number]>>,
|
|
83
|
-
) => {
|
|
84
|
-
previewRef.current = { polygon, holes }
|
|
85
|
-
const center = getPolygonCenter(polygon)
|
|
86
|
-
setCursorLocalPos([center[0], 0, center[1]])
|
|
87
|
-
useScene.getState().updateNode(node.id, { polygon, holes })
|
|
88
|
-
useScene.getState().markDirty(node.id as AnyNodeId)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const restoreOriginal = () => {
|
|
92
|
-
useScene.getState().updateNode(node.id, {
|
|
93
|
-
holes: originalHoles,
|
|
94
|
-
polygon: originalPolygon,
|
|
95
|
-
})
|
|
96
|
-
useScene.getState().markDirty(node.id as AnyNodeId)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const onGridMove = (event: GridEvent) => {
|
|
100
|
-
const [localX, localZ] = snapFenceDraftPoint({
|
|
101
|
-
point: [event.localPosition[0], event.localPosition[2]],
|
|
102
|
-
walls: levelWalls,
|
|
103
|
-
fences: levelFences,
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
previousGridPosRef.current &&
|
|
108
|
-
(localX !== previousGridPosRef.current[0] || localZ !== previousGridPosRef.current[1])
|
|
109
|
-
) {
|
|
110
|
-
sfxEmitter.emit('sfx:grid-snap')
|
|
111
|
-
}
|
|
112
|
-
previousGridPosRef.current = [localX, localZ]
|
|
113
|
-
|
|
114
|
-
const anchor = dragAnchorRef.current ?? [localX, localZ]
|
|
115
|
-
dragAnchorRef.current = anchor
|
|
116
|
-
|
|
117
|
-
const deltaX = localX - anchor[0]
|
|
118
|
-
const deltaZ = localZ - anchor[1]
|
|
119
|
-
|
|
120
|
-
applyPreview(
|
|
121
|
-
translatePolygon(originalPolygon, deltaX, deltaZ),
|
|
122
|
-
originalHoles.map((hole) => translatePolygon(hole, deltaX, deltaZ)),
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const onGridClick = (event: GridEvent) => {
|
|
127
|
-
if (Date.now() - activatedAtRef.current < 150) {
|
|
128
|
-
event.nativeEvent?.stopPropagation?.()
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const preview = previewRef.current ?? { polygon: originalPolygon, holes: originalHoles }
|
|
133
|
-
|
|
134
|
-
wasCommitted = true
|
|
135
|
-
|
|
136
|
-
// Restore original baseline while paused so the next resume+update
|
|
137
|
-
// registers as a single tracked change (undo reverts to original).
|
|
138
|
-
useScene.getState().updateNode(node.id, {
|
|
139
|
-
polygon: originalPolygon,
|
|
140
|
-
holes: originalHoles,
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
useScene.temporal.getState().resume()
|
|
144
|
-
useScene.getState().updateNode(node.id, preview)
|
|
145
|
-
useScene.getState().markDirty(node.id as AnyNodeId)
|
|
146
|
-
useScene.temporal.getState().pause()
|
|
147
|
-
|
|
148
|
-
sfxEmitter.emit('sfx:item-place')
|
|
149
|
-
useViewer.getState().setSelection({ selectedIds: [node.id] })
|
|
150
|
-
exitMoveMode()
|
|
151
|
-
event.nativeEvent?.stopPropagation?.()
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const onCancel = () => {
|
|
155
|
-
restoreOriginal()
|
|
156
|
-
useViewer.getState().setSelection({ selectedIds: [node.id] })
|
|
157
|
-
useScene.temporal.getState().resume()
|
|
158
|
-
markToolCancelConsumed()
|
|
159
|
-
exitMoveMode()
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
emitter.on('grid:move', onGridMove)
|
|
163
|
-
emitter.on('grid:click', onGridClick)
|
|
164
|
-
emitter.on('tool:cancel', onCancel)
|
|
165
|
-
|
|
166
|
-
return () => {
|
|
167
|
-
if (!wasCommitted) {
|
|
168
|
-
restoreOriginal()
|
|
169
|
-
}
|
|
170
|
-
useScene.temporal.getState().resume()
|
|
171
|
-
emitter.off('grid:move', onGridMove)
|
|
172
|
-
emitter.off('grid:click', onGridClick)
|
|
173
|
-
emitter.off('tool:cancel', onCancel)
|
|
174
|
-
}
|
|
175
|
-
}, [exitMoveMode, node.id])
|
|
176
|
-
|
|
177
|
-
return (
|
|
178
|
-
<group>
|
|
179
|
-
<CursorSphere position={cursorLocalPos} showTooltip={false} />
|
|
180
|
-
</group>
|
|
181
|
-
)
|
|
182
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { resolveLevelId, type SlabNode, useScene } from '@pascal-app/core'
|
|
2
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
-
import { useCallback } from 'react'
|
|
4
|
-
import { PolygonEditor } from '../shared/polygon-editor'
|
|
5
|
-
|
|
6
|
-
interface SlabBoundaryEditorProps {
|
|
7
|
-
slabId: SlabNode['id']
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Slab boundary editor - allows editing slab polygon vertices for a specific slab
|
|
12
|
-
* Uses the generic PolygonEditor component
|
|
13
|
-
*/
|
|
14
|
-
export const SlabBoundaryEditor: React.FC<SlabBoundaryEditorProps> = ({ slabId }) => {
|
|
15
|
-
const slabNode = useScene((state) => state.nodes[slabId])
|
|
16
|
-
const updateNode = useScene((state) => state.updateNode)
|
|
17
|
-
const setSelection = useViewer((state) => state.setSelection)
|
|
18
|
-
|
|
19
|
-
const slab = slabNode?.type === 'slab' ? (slabNode as SlabNode) : null
|
|
20
|
-
|
|
21
|
-
const handlePolygonChange = useCallback(
|
|
22
|
-
(newPolygon: Array<[number, number]>) => {
|
|
23
|
-
updateNode(slabId, { polygon: newPolygon })
|
|
24
|
-
// Re-assert selection so the slab stays selected after the edit
|
|
25
|
-
setSelection({ selectedIds: [slabId] })
|
|
26
|
-
},
|
|
27
|
-
[slabId, updateNode, setSelection],
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
if (!slab?.polygon || slab.polygon.length < 3) return null
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<PolygonEditor
|
|
34
|
-
allowEdgeMove
|
|
35
|
-
color="#a3a3a3"
|
|
36
|
-
levelId={resolveLevelId(slab, useScene.getState().nodes)}
|
|
37
|
-
minVertices={3}
|
|
38
|
-
onPolygonChange={handlePolygonChange}
|
|
39
|
-
polygon={slab.polygon}
|
|
40
|
-
surfaceHeight={slab.elevation ?? 0.05}
|
|
41
|
-
/>
|
|
42
|
-
)
|
|
43
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { resolveLevelId, type SlabNode, useScene } from '@pascal-app/core'
|
|
2
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
-
import { useCallback } from 'react'
|
|
4
|
-
import { PolygonEditor } from '../shared/polygon-editor'
|
|
5
|
-
|
|
6
|
-
interface SlabHoleEditorProps {
|
|
7
|
-
slabId: SlabNode['id']
|
|
8
|
-
holeIndex: number
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Slab hole editor - allows editing a specific hole polygon within a slab
|
|
13
|
-
* Uses the generic PolygonEditor component
|
|
14
|
-
*/
|
|
15
|
-
export const SlabHoleEditor: React.FC<SlabHoleEditorProps> = ({ slabId, holeIndex }) => {
|
|
16
|
-
const slabNode = useScene((state) => state.nodes[slabId])
|
|
17
|
-
const updateNode = useScene((state) => state.updateNode)
|
|
18
|
-
const setSelection = useViewer((state) => state.setSelection)
|
|
19
|
-
|
|
20
|
-
const slab = slabNode?.type === 'slab' ? (slabNode as SlabNode) : null
|
|
21
|
-
const holes = slab?.holes || []
|
|
22
|
-
const hole = holes[holeIndex]
|
|
23
|
-
|
|
24
|
-
const handlePolygonChange = useCallback(
|
|
25
|
-
(newPolygon: Array<[number, number]>) => {
|
|
26
|
-
const updatedHoles = [...holes]
|
|
27
|
-
updatedHoles[holeIndex] = newPolygon
|
|
28
|
-
updateNode(slabId, { holes: updatedHoles })
|
|
29
|
-
// Re-assert selection so the slab stays selected after the edit
|
|
30
|
-
setSelection({ selectedIds: [slabId] })
|
|
31
|
-
},
|
|
32
|
-
[slabId, holeIndex, holes, updateNode, setSelection],
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
if (!(slab && hole) || hole.length < 3) return null
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<PolygonEditor
|
|
39
|
-
allowEdgeMove
|
|
40
|
-
allowPolygonMove
|
|
41
|
-
color="#ef4444"
|
|
42
|
-
levelId={resolveLevelId(slab, useScene.getState().nodes)} // red for holes
|
|
43
|
-
minVertices={3}
|
|
44
|
-
onPolygonChange={handlePolygonChange}
|
|
45
|
-
polygon={hole}
|
|
46
|
-
surfaceHeight={slab.elevation ?? 0.05}
|
|
47
|
-
/>
|
|
48
|
-
)
|
|
49
|
-
}
|
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
import { emitter, type GridEvent, type LevelNode, SlabNode, useScene } from '@pascal-app/core'
|
|
2
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
-
import { BufferGeometry, DoubleSide, type Group, type Line, Shape, Vector3 } from 'three'
|
|
5
|
-
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
6
|
-
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
7
|
-
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
8
|
-
import { CursorSphere } from '../shared/cursor-sphere'
|
|
9
|
-
|
|
10
|
-
const Y_OFFSET = 0.02
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Snaps a point to the nearest axis-aligned or 45-degree diagonal from the last point
|
|
14
|
-
*/
|
|
15
|
-
const calculateSnapPoint = (
|
|
16
|
-
lastPoint: [number, number],
|
|
17
|
-
currentPoint: [number, number],
|
|
18
|
-
): [number, number] => {
|
|
19
|
-
const [x1, y1] = lastPoint
|
|
20
|
-
const [x, y] = currentPoint
|
|
21
|
-
|
|
22
|
-
const dx = x - x1
|
|
23
|
-
const dy = y - y1
|
|
24
|
-
const absDx = Math.abs(dx)
|
|
25
|
-
const absDy = Math.abs(dy)
|
|
26
|
-
|
|
27
|
-
// Calculate distances to horizontal, vertical, and diagonal lines
|
|
28
|
-
const horizontalDist = absDy
|
|
29
|
-
const verticalDist = absDx
|
|
30
|
-
const diagonalDist = Math.abs(absDx - absDy)
|
|
31
|
-
|
|
32
|
-
// Find the minimum distance to determine which axis to snap to
|
|
33
|
-
const minDist = Math.min(horizontalDist, verticalDist, diagonalDist)
|
|
34
|
-
|
|
35
|
-
if (minDist === diagonalDist) {
|
|
36
|
-
// Snap to 45° diagonal
|
|
37
|
-
const diagonalLength = Math.min(absDx, absDy)
|
|
38
|
-
return [x1 + Math.sign(dx) * diagonalLength, y1 + Math.sign(dy) * diagonalLength]
|
|
39
|
-
}
|
|
40
|
-
if (minDist === horizontalDist) {
|
|
41
|
-
// Snap to horizontal
|
|
42
|
-
return [x, y1]
|
|
43
|
-
}
|
|
44
|
-
// Snap to vertical
|
|
45
|
-
return [x1, y]
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Creates a slab with the given polygon points and returns its ID
|
|
50
|
-
*/
|
|
51
|
-
const commitSlabDrawing = (levelId: LevelNode['id'], points: Array<[number, number]>): string => {
|
|
52
|
-
const { createNode, nodes } = useScene.getState()
|
|
53
|
-
|
|
54
|
-
// Count existing slabs for naming
|
|
55
|
-
const slabCount = Object.values(nodes).filter((n) => n.type === 'slab').length
|
|
56
|
-
const name = `Slab ${slabCount + 1}`
|
|
57
|
-
|
|
58
|
-
const slab = SlabNode.parse({
|
|
59
|
-
name,
|
|
60
|
-
polygon: points,
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
createNode(slab, levelId)
|
|
64
|
-
sfxEmitter.emit('sfx:structure-build')
|
|
65
|
-
return slab.id
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export const SlabTool: React.FC = () => {
|
|
69
|
-
const cursorRef = useRef<Group>(null)
|
|
70
|
-
const mainLineRef = useRef<Line>(null!)
|
|
71
|
-
const closingLineRef = useRef<Line>(null!)
|
|
72
|
-
const currentLevelId = useViewer((state) => state.selection.levelId)
|
|
73
|
-
const setSelection = useViewer((state) => state.setSelection)
|
|
74
|
-
|
|
75
|
-
const [points, setPoints] = useState<Array<[number, number]>>([])
|
|
76
|
-
const [cursorPosition, setCursorPosition] = useState<[number, number]>([0, 0])
|
|
77
|
-
const [snappedCursorPosition, setSnappedCursorPosition] = useState<[number, number]>([0, 0])
|
|
78
|
-
const [levelY, setLevelY] = useState(0)
|
|
79
|
-
const previousSnappedPointRef = useRef<[number, number] | null>(null)
|
|
80
|
-
const shiftPressed = useRef(false)
|
|
81
|
-
|
|
82
|
-
// Update cursor position and lines on grid move
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
if (!currentLevelId) return
|
|
85
|
-
|
|
86
|
-
const onGridMove = (event: GridEvent) => {
|
|
87
|
-
if (!cursorRef.current) return
|
|
88
|
-
|
|
89
|
-
const gridX = Math.round(event.localPosition[0] * 2) / 2
|
|
90
|
-
const gridZ = Math.round(event.localPosition[2] * 2) / 2
|
|
91
|
-
const gridPosition: [number, number] = [gridX, gridZ]
|
|
92
|
-
|
|
93
|
-
setCursorPosition(gridPosition)
|
|
94
|
-
setLevelY(event.localPosition[1])
|
|
95
|
-
|
|
96
|
-
// Calculate snapped display position (bypass snap when Shift is held)
|
|
97
|
-
const lastPoint = points[points.length - 1]
|
|
98
|
-
const displayPoint =
|
|
99
|
-
shiftPressed.current || !lastPoint
|
|
100
|
-
? gridPosition
|
|
101
|
-
: calculateSnapPoint(lastPoint, gridPosition)
|
|
102
|
-
setSnappedCursorPosition(displayPoint)
|
|
103
|
-
|
|
104
|
-
// Play snap sound when the snapped position actually changes (only when drawing)
|
|
105
|
-
if (
|
|
106
|
-
points.length > 0 &&
|
|
107
|
-
previousSnappedPointRef.current &&
|
|
108
|
-
(displayPoint[0] !== previousSnappedPointRef.current[0] ||
|
|
109
|
-
displayPoint[1] !== previousSnappedPointRef.current[1])
|
|
110
|
-
) {
|
|
111
|
-
sfxEmitter.emit('sfx:grid-snap')
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
previousSnappedPointRef.current = displayPoint
|
|
115
|
-
cursorRef.current.position.set(displayPoint[0], event.localPosition[1], displayPoint[1])
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const onGridClick = (_event: GridEvent) => {
|
|
119
|
-
if (!currentLevelId) return
|
|
120
|
-
|
|
121
|
-
// Use the last displayed snapped position (respects Shift state from onGridMove)
|
|
122
|
-
const clickPoint = previousSnappedPointRef.current ?? cursorPosition
|
|
123
|
-
|
|
124
|
-
// Check if clicking on the first point to close the shape
|
|
125
|
-
const firstPoint = points[0]
|
|
126
|
-
if (
|
|
127
|
-
points.length >= 3 &&
|
|
128
|
-
firstPoint &&
|
|
129
|
-
Math.abs(clickPoint[0] - firstPoint[0]) < 0.25 &&
|
|
130
|
-
Math.abs(clickPoint[1] - firstPoint[1]) < 0.25
|
|
131
|
-
) {
|
|
132
|
-
// Create the slab and select it
|
|
133
|
-
const slabId = commitSlabDrawing(currentLevelId, points)
|
|
134
|
-
setSelection({ selectedIds: [slabId] })
|
|
135
|
-
setPoints([])
|
|
136
|
-
} else {
|
|
137
|
-
// Add point to polygon
|
|
138
|
-
setPoints([...points, clickPoint])
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const onGridDoubleClick = (_event: GridEvent) => {
|
|
143
|
-
if (!currentLevelId) return
|
|
144
|
-
|
|
145
|
-
// Need at least 3 points to form a polygon
|
|
146
|
-
if (points.length >= 3) {
|
|
147
|
-
const slabId = commitSlabDrawing(currentLevelId, points)
|
|
148
|
-
setSelection({ selectedIds: [slabId] })
|
|
149
|
-
setPoints([])
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const onCancel = () => {
|
|
154
|
-
if (points.length > 0) markToolCancelConsumed()
|
|
155
|
-
setPoints([])
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const onKeyDown = (e: KeyboardEvent) => {
|
|
159
|
-
if (e.key === 'Shift') shiftPressed.current = true
|
|
160
|
-
}
|
|
161
|
-
const onKeyUp = (e: KeyboardEvent) => {
|
|
162
|
-
if (e.key === 'Shift') shiftPressed.current = false
|
|
163
|
-
}
|
|
164
|
-
document.addEventListener('keydown', onKeyDown)
|
|
165
|
-
document.addEventListener('keyup', onKeyUp)
|
|
166
|
-
|
|
167
|
-
emitter.on('grid:move', onGridMove)
|
|
168
|
-
emitter.on('grid:click', onGridClick)
|
|
169
|
-
emitter.on('grid:double-click', onGridDoubleClick)
|
|
170
|
-
emitter.on('tool:cancel', onCancel)
|
|
171
|
-
|
|
172
|
-
return () => {
|
|
173
|
-
document.removeEventListener('keydown', onKeyDown)
|
|
174
|
-
document.removeEventListener('keyup', onKeyUp)
|
|
175
|
-
emitter.off('grid:move', onGridMove)
|
|
176
|
-
emitter.off('grid:click', onGridClick)
|
|
177
|
-
emitter.off('grid:double-click', onGridDoubleClick)
|
|
178
|
-
emitter.off('tool:cancel', onCancel)
|
|
179
|
-
}
|
|
180
|
-
}, [currentLevelId, points, cursorPosition, setSelection])
|
|
181
|
-
|
|
182
|
-
// Update line geometries when points change
|
|
183
|
-
useEffect(() => {
|
|
184
|
-
if (!(mainLineRef.current && closingLineRef.current)) return
|
|
185
|
-
|
|
186
|
-
if (points.length === 0) {
|
|
187
|
-
mainLineRef.current.visible = false
|
|
188
|
-
closingLineRef.current.visible = false
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const y = levelY + Y_OFFSET
|
|
193
|
-
const snappedCursor = snappedCursorPosition
|
|
194
|
-
|
|
195
|
-
// Build main line points
|
|
196
|
-
const linePoints: Vector3[] = points.map(([x, z]) => new Vector3(x, y, z))
|
|
197
|
-
linePoints.push(new Vector3(snappedCursor[0], y, snappedCursor[1]))
|
|
198
|
-
|
|
199
|
-
// Update main line
|
|
200
|
-
if (linePoints.length >= 2) {
|
|
201
|
-
mainLineRef.current.geometry.dispose()
|
|
202
|
-
mainLineRef.current.geometry = new BufferGeometry().setFromPoints(linePoints)
|
|
203
|
-
mainLineRef.current.visible = true
|
|
204
|
-
} else {
|
|
205
|
-
mainLineRef.current.visible = false
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Update closing line (from cursor back to first point)
|
|
209
|
-
const firstPoint = points[0]
|
|
210
|
-
if (points.length >= 2 && firstPoint) {
|
|
211
|
-
const closingPoints = [
|
|
212
|
-
new Vector3(snappedCursor[0], y, snappedCursor[1]),
|
|
213
|
-
new Vector3(firstPoint[0], y, firstPoint[1]),
|
|
214
|
-
]
|
|
215
|
-
closingLineRef.current.geometry.dispose()
|
|
216
|
-
closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints)
|
|
217
|
-
closingLineRef.current.visible = true
|
|
218
|
-
} else {
|
|
219
|
-
closingLineRef.current.visible = false
|
|
220
|
-
}
|
|
221
|
-
}, [points, snappedCursorPosition, levelY])
|
|
222
|
-
|
|
223
|
-
// Create preview shape when we have 3+ points
|
|
224
|
-
const previewShape = useMemo(() => {
|
|
225
|
-
if (points.length < 3) return null
|
|
226
|
-
|
|
227
|
-
const snappedCursor = snappedCursorPosition
|
|
228
|
-
|
|
229
|
-
const allPoints = [...points, snappedCursor]
|
|
230
|
-
|
|
231
|
-
// THREE.Shape is in X-Y plane. After rotation of -PI/2 around X:
|
|
232
|
-
// - Shape X -> World X
|
|
233
|
-
// - Shape Y -> World -Z (so we negate Z to get correct orientation)
|
|
234
|
-
const firstPt = allPoints[0]
|
|
235
|
-
if (!firstPt) return null
|
|
236
|
-
|
|
237
|
-
const shape = new Shape()
|
|
238
|
-
shape.moveTo(firstPt[0], -firstPt[1])
|
|
239
|
-
|
|
240
|
-
for (let i = 1; i < allPoints.length; i++) {
|
|
241
|
-
const pt = allPoints[i]
|
|
242
|
-
if (pt) {
|
|
243
|
-
shape.lineTo(pt[0], -pt[1])
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
shape.closePath()
|
|
247
|
-
|
|
248
|
-
return shape
|
|
249
|
-
}, [points, snappedCursorPosition])
|
|
250
|
-
|
|
251
|
-
return (
|
|
252
|
-
<group>
|
|
253
|
-
{/* Cursor */}
|
|
254
|
-
<CursorSphere ref={cursorRef} />
|
|
255
|
-
|
|
256
|
-
{/* Preview fill */}
|
|
257
|
-
{previewShape && (
|
|
258
|
-
<mesh
|
|
259
|
-
frustumCulled={false}
|
|
260
|
-
layers={EDITOR_LAYER}
|
|
261
|
-
position={[0, levelY + Y_OFFSET, 0]}
|
|
262
|
-
rotation={[-Math.PI / 2, 0, 0]}
|
|
263
|
-
>
|
|
264
|
-
<shapeGeometry args={[previewShape]} />
|
|
265
|
-
<meshBasicMaterial
|
|
266
|
-
color="#818cf8"
|
|
267
|
-
depthTest={false}
|
|
268
|
-
opacity={0.15}
|
|
269
|
-
side={DoubleSide}
|
|
270
|
-
transparent
|
|
271
|
-
/>
|
|
272
|
-
</mesh>
|
|
273
|
-
)}
|
|
274
|
-
|
|
275
|
-
{/* Main line */}
|
|
276
|
-
{/* @ts-ignore */}
|
|
277
|
-
<line
|
|
278
|
-
frustumCulled={false}
|
|
279
|
-
layers={EDITOR_LAYER}
|
|
280
|
-
// @ts-expect-error
|
|
281
|
-
ref={mainLineRef}
|
|
282
|
-
renderOrder={1}
|
|
283
|
-
visible={false}
|
|
284
|
-
>
|
|
285
|
-
<bufferGeometry />
|
|
286
|
-
<lineBasicNodeMaterial color="#818cf8" depthTest={false} depthWrite={false} linewidth={3} />
|
|
287
|
-
</line>
|
|
288
|
-
|
|
289
|
-
{/* Closing line */}
|
|
290
|
-
{/* @ts-ignore */}
|
|
291
|
-
<line
|
|
292
|
-
frustumCulled={false}
|
|
293
|
-
layers={EDITOR_LAYER}
|
|
294
|
-
// @ts-expect-error
|
|
295
|
-
ref={closingLineRef}
|
|
296
|
-
renderOrder={1}
|
|
297
|
-
visible={false}
|
|
298
|
-
>
|
|
299
|
-
<bufferGeometry />
|
|
300
|
-
<lineBasicNodeMaterial
|
|
301
|
-
color="#818cf8"
|
|
302
|
-
depthTest={false}
|
|
303
|
-
depthWrite={false}
|
|
304
|
-
linewidth={2}
|
|
305
|
-
opacity={0.5}
|
|
306
|
-
transparent
|
|
307
|
-
/>
|
|
308
|
-
</line>
|
|
309
|
-
|
|
310
|
-
{/* Point markers */}
|
|
311
|
-
{points.map(([x, z], index) => (
|
|
312
|
-
<CursorSphere
|
|
313
|
-
color="#818cf8"
|
|
314
|
-
height={0}
|
|
315
|
-
key={index}
|
|
316
|
-
position={[x, levelY + Y_OFFSET + 0.01, z]}
|
|
317
|
-
showTooltip={false}
|
|
318
|
-
/>
|
|
319
|
-
))}
|
|
320
|
-
</group>
|
|
321
|
-
)
|
|
322
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import '../../../three-types'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
emitter,
|
|
5
|
-
type GridEvent,
|
|
6
|
-
type SpawnNode,
|
|
7
|
-
sceneRegistry,
|
|
8
|
-
useLiveTransforms,
|
|
9
|
-
useScene,
|
|
10
|
-
} from '@pascal-app/core'
|
|
11
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
12
|
-
import { Vector3 } from 'three'
|
|
13
|
-
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
14
|
-
import useEditor from '../../../store/use-editor'
|
|
15
|
-
import { CursorSphere } from '../shared/cursor-sphere'
|
|
16
|
-
|
|
17
|
-
const roundToHalf = (value: number) => Math.round(value * 2) / 2
|
|
18
|
-
const worldVector = new Vector3()
|
|
19
|
-
|
|
20
|
-
function getLevelLocalSpawnPosition(node: SpawnNode, event: GridEvent): [number, number, number] {
|
|
21
|
-
const levelObject = node.parentId ? sceneRegistry.nodes.get(node.parentId) : null
|
|
22
|
-
if (!levelObject) {
|
|
23
|
-
return [
|
|
24
|
-
roundToHalf(event.localPosition[0]),
|
|
25
|
-
event.localPosition[1],
|
|
26
|
-
roundToHalf(event.localPosition[2]),
|
|
27
|
-
]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
worldVector.set(event.position[0], event.position[1], event.position[2])
|
|
31
|
-
levelObject.updateWorldMatrix(true, false)
|
|
32
|
-
levelObject.worldToLocal(worldVector)
|
|
33
|
-
|
|
34
|
-
return [roundToHalf(worldVector.x), worldVector.y, roundToHalf(worldVector.z)]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const MoveSpawnTool: React.FC<{
|
|
38
|
-
node: SpawnNode
|
|
39
|
-
onCommitted?: (nodeId: SpawnNode['id']) => void
|
|
40
|
-
}> = ({ node, onCommitted }) => {
|
|
41
|
-
const [previewPosition, setPreviewPosition] = useState<[number, number, number]>(node.position)
|
|
42
|
-
|
|
43
|
-
const exitMoveMode = useCallback(() => {
|
|
44
|
-
useEditor.getState().setMovingNode(null)
|
|
45
|
-
}, [])
|
|
46
|
-
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
useScene.temporal.getState().pause()
|
|
49
|
-
|
|
50
|
-
let committed = false
|
|
51
|
-
|
|
52
|
-
const onGridMove = (event: GridEvent) => {
|
|
53
|
-
const nextPosition: [number, number, number] = [
|
|
54
|
-
roundToHalf(event.localPosition[0]),
|
|
55
|
-
event.localPosition[1],
|
|
56
|
-
roundToHalf(event.localPosition[2]),
|
|
57
|
-
]
|
|
58
|
-
setPreviewPosition(nextPosition)
|
|
59
|
-
useLiveTransforms.getState().set(node.id, {
|
|
60
|
-
position: [...nextPosition],
|
|
61
|
-
rotation: node.rotation,
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const onGridClick = (event: GridEvent) => {
|
|
66
|
-
const nextPosition = getLevelLocalSpawnPosition(node, event)
|
|
67
|
-
|
|
68
|
-
committed = true
|
|
69
|
-
useScene.temporal.getState().resume()
|
|
70
|
-
useScene.getState().updateNode(node.id, { position: nextPosition })
|
|
71
|
-
onCommitted?.(node.id)
|
|
72
|
-
useLiveTransforms.getState().clear(node.id)
|
|
73
|
-
sfxEmitter.emit('sfx:item-place')
|
|
74
|
-
exitMoveMode()
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const onCancel = () => {
|
|
78
|
-
useLiveTransforms.getState().clear(node.id)
|
|
79
|
-
useScene.temporal.getState().resume()
|
|
80
|
-
exitMoveMode()
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
emitter.on('grid:move', onGridMove)
|
|
84
|
-
emitter.on('grid:click', onGridClick)
|
|
85
|
-
emitter.on('tool:cancel', onCancel)
|
|
86
|
-
|
|
87
|
-
return () => {
|
|
88
|
-
emitter.off('grid:move', onGridMove)
|
|
89
|
-
emitter.off('grid:click', onGridClick)
|
|
90
|
-
emitter.off('tool:cancel', onCancel)
|
|
91
|
-
useLiveTransforms.getState().clear(node.id)
|
|
92
|
-
if (!committed) {
|
|
93
|
-
useScene.temporal.getState().resume()
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}, [exitMoveMode, node, onCommitted])
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<CursorSphere color="#60a5fa" height={2.2} position={previewPosition} showTooltip={false} />
|
|
100
|
-
)
|
|
101
|
-
}
|