@pascal-app/editor 0.6.0 → 0.8.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 +13 -9
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +74 -5
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +24 -3
- package/src/components/editor/first-person/build-collider-world.ts +363 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +860 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +32 -55
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +9861 -3297
- package/src/components/editor/index.tsx +295 -32
- package/src/components/editor/selection-manager.tsx +575 -13
- package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
- package/src/components/editor/thumbnail-generator.tsx +56 -68
- 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 +267 -36
- 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 +124 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +202 -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 +10 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1 -1
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- 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/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
- 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 +7 -0
- package/src/components/tools/door/move-door-tool.tsx +28 -8
- package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
- package/src/components/tools/fence/fence-drafting.ts +10 -3
- package/src/components/tools/fence/fence-tool.tsx +160 -4
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +139 -25
- package/src/components/tools/fence/move-fence-tool.tsx +111 -40
- package/src/components/tools/item/move-tool.tsx +7 -1
- package/src/components/tools/item/placement-math.ts +32 -5
- package/src/components/tools/item/placement-strategies.ts +110 -31
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +1 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +558 -52
- package/src/components/tools/roof/move-roof-tool.tsx +29 -17
- package/src/components/tools/select/box-select-tool.tsx +12 -17
- package/src/components/tools/shared/polygon-editor.tsx +153 -28
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +1 -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/tool-manager.tsx +20 -5
- package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +131 -27
- package/src/components/tools/wall/move-wall-tool.tsx +6 -4
- package/src/components/tools/wall/wall-drafting.ts +18 -9
- package/src/components/tools/wall/wall-tool.tsx +136 -4
- package/src/components/tools/window/move-window-tool.tsx +18 -0
- package/src/components/tools/window/window-tool.tsx +5 -0
- package/src/components/tools/zone/zone-tool.tsx +20 -5
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +34 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +98 -59
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +418 -41
- package/src/components/ui/command-palette/editor-commands.tsx +24 -5
- package/src/components/ui/command-palette/index.tsx +4 -255
- package/src/components/ui/controls/material-picker.tsx +154 -164
- package/src/components/ui/controls/slider-control.tsx +66 -18
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +10 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +2563 -1239
- package/src/components/ui/item-catalog/item-catalog.tsx +96 -187
- package/src/components/ui/level-duplicate-dialog.tsx +113 -0
- package/src/components/ui/panels/ceiling-panel.tsx +3 -28
- package/src/components/ui/panels/column-panel.tsx +759 -0
- package/src/components/ui/panels/door-panel.tsx +989 -290
- package/src/components/ui/panels/fence-panel.tsx +2 -49
- 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 +163 -0
- package/src/components/ui/panels/panel-manager.tsx +208 -28
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +253 -5
- package/src/components/ui/panels/roof-panel.tsx +13 -64
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
- package/src/components/ui/panels/slab-panel.tsx +4 -30
- package/src/components/ui/panels/spawn-panel.tsx +161 -0
- package/src/components/ui/panels/stair-panel.tsx +20 -74
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
- package/src/components/ui/panels/wall-panel.tsx +10 -8
- package/src/components/ui/panels/window-panel.tsx +668 -139
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- 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/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -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 +2 -2
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +105 -22
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +76 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +11 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +10 -5
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +0 -0
- package/src/components/viewer-zone-system.tsx +0 -0
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-auto-save.ts +14 -0
- package/src/hooks/use-keyboard.ts +74 -7
- package/src/hooks/use-mobile.ts +12 -12
- package/src/index.tsx +8 -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/level-duplication.test.ts +70 -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/scene.ts +0 -0
- package/src/lib/sfx-bus.ts +2 -0
- package/src/lib/sfx-player.ts +5 -5
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +186 -62
- package/tsconfig.json +2 -1
- 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/ui/viewer-toolbar.tsx +0 -395
|
@@ -75,7 +75,9 @@ const CeilingSelectionAffordance = ({
|
|
|
75
75
|
ceiling: CeilingNode
|
|
76
76
|
levelId: string
|
|
77
77
|
}) => {
|
|
78
|
-
const [levelObject, setLevelObject] = useState<Object3D | null>(
|
|
78
|
+
const [levelObject, setLevelObject] = useState<Object3D | null>(
|
|
79
|
+
() => sceneRegistry.nodes.get(levelId) ?? null,
|
|
80
|
+
)
|
|
79
81
|
|
|
80
82
|
const corners = useMemo(() => buildCornerBrackets(ceiling.polygon), [ceiling.polygon])
|
|
81
83
|
|
|
@@ -110,11 +112,7 @@ const CeilingSelectionAffordance = ({
|
|
|
110
112
|
return createPortal(
|
|
111
113
|
<group position={[0, (ceiling.height ?? 2.5) + BRACKET_Y_OFFSET, 0]}>
|
|
112
114
|
{corners.map((corner, index) => (
|
|
113
|
-
<CornerBracket
|
|
114
|
-
ceiling={ceiling}
|
|
115
|
-
corner={corner}
|
|
116
|
-
key={`${ceiling.id}-corner-${index}`}
|
|
117
|
-
/>
|
|
115
|
+
<CornerBracket ceiling={ceiling} corner={corner} key={`${ceiling.id}-corner-${index}`} />
|
|
118
116
|
))}
|
|
119
117
|
</group>,
|
|
120
118
|
levelObject,
|
|
@@ -210,11 +208,7 @@ const BracketLeg = ({
|
|
|
210
208
|
]
|
|
211
209
|
|
|
212
210
|
return (
|
|
213
|
-
<mesh
|
|
214
|
-
onClick={onClick}
|
|
215
|
-
position={position}
|
|
216
|
-
rotation={[0, angle, 0]}
|
|
217
|
-
>
|
|
211
|
+
<mesh onClick={onClick} position={position} rotation={[0, angle, 0]}>
|
|
218
212
|
<boxGeometry args={[length, BRACKET_HEIGHT, BRACKET_THICKNESS]} />
|
|
219
213
|
<meshBasicMaterial color={color} depthWrite={false} opacity={opacity} transparent />
|
|
220
214
|
</mesh>
|
|
@@ -234,7 +228,11 @@ function buildCornerBrackets(polygon: Array<[number, number]>): CornerBracketDat
|
|
|
234
228
|
|
|
235
229
|
const incomingLength = Math.hypot(incomingVector[0], incomingVector[1])
|
|
236
230
|
const outgoingLength = Math.hypot(outgoingVector[0], outgoingVector[1])
|
|
237
|
-
const cornerStrength =
|
|
231
|
+
const cornerStrength =
|
|
232
|
+
1 -
|
|
233
|
+
Math.abs(
|
|
234
|
+
incomingDirection[0] * outgoingDirection[0] + incomingDirection[1] * outgoingDirection[1],
|
|
235
|
+
)
|
|
238
236
|
|
|
239
237
|
return {
|
|
240
238
|
corner,
|
|
@@ -6,7 +6,7 @@ import { useEffect, useRef } from 'react'
|
|
|
6
6
|
* Imperatively toggles the Three.js visibility of roof objects based on the
|
|
7
7
|
* editor selection — without causing React re-renders in RoofRenderer.
|
|
8
8
|
*
|
|
9
|
-
* When a roof
|
|
9
|
+
* When a roof (or one of its segments) is selected:
|
|
10
10
|
* - merged-roof mesh is hidden
|
|
11
11
|
* - segments-wrapper group is shown (individual segments visible for editing)
|
|
12
12
|
* - all children are marked dirty so RoofSystem rebuilds their geometry
|
|
@@ -68,7 +68,7 @@ export const StairEditSystem = () => {
|
|
|
68
68
|
const segmentsWrapper = group.getObjectByName('segments-wrapper')
|
|
69
69
|
const isActive = activeStairIds.has(stairId)
|
|
70
70
|
|
|
71
|
-
if (mergedMesh) mergedMesh.visible = !isActive
|
|
71
|
+
if (mergedMesh) mergedMesh.visible = !(isActive || isCurved)
|
|
72
72
|
if (segmentsWrapper) segmentsWrapper.visible = isActive && !isCurved
|
|
73
73
|
|
|
74
74
|
if (stairNode?.children?.length) {
|
|
File without changes
|
|
File without changes
|
|
@@ -111,15 +111,15 @@ export const CeilingTool: React.FC = () => {
|
|
|
111
111
|
const onGridMove = (event: GridEvent) => {
|
|
112
112
|
if (!(cursorRef.current && gridCursorRef.current)) return
|
|
113
113
|
|
|
114
|
-
const gridX = Math.round(event.
|
|
115
|
-
const gridZ = Math.round(event.
|
|
114
|
+
const gridX = Math.round(event.localPosition[0] * 2) / 2
|
|
115
|
+
const gridZ = Math.round(event.localPosition[2] * 2) / 2
|
|
116
116
|
const gridPosition: [number, number] = [gridX, gridZ]
|
|
117
117
|
|
|
118
118
|
setCursorPosition(gridPosition)
|
|
119
|
-
setLevelY(event.
|
|
119
|
+
setLevelY(event.localPosition[1])
|
|
120
120
|
|
|
121
|
-
const ceilingY = event.
|
|
122
|
-
const gridY = event.
|
|
121
|
+
const ceilingY = event.localPosition[1] + CEILING_HEIGHT
|
|
122
|
+
const gridY = event.localPosition[1] + GRID_OFFSET
|
|
123
123
|
|
|
124
124
|
// Calculate snapped display position (bypass snap when Shift is held)
|
|
125
125
|
const lastPoint = points[points.length - 1]
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
type CeilingNode,
|
|
6
|
+
emitter,
|
|
7
|
+
type GridEvent,
|
|
8
|
+
useScene,
|
|
9
|
+
} from '@pascal-app/core'
|
|
4
10
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
11
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
12
|
+
import { BufferGeometry, DoubleSide, Path, Shape, ShapeGeometry, Vector3 } from 'three'
|
|
6
13
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
7
14
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
8
15
|
import useEditor from '../../../store/use-editor'
|
|
9
16
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
10
|
-
import { BufferGeometry, DoubleSide, Path, Shape, ShapeGeometry, Vector3 } from 'three'
|
|
11
17
|
|
|
12
18
|
function snap(value: number) {
|
|
13
19
|
return Math.round(value * 2) / 2
|
|
@@ -202,6 +208,7 @@ export const MoveCeilingTool: React.FC<{ node: CeilingNode }> = ({ node }) => {
|
|
|
202
208
|
transparent
|
|
203
209
|
/>
|
|
204
210
|
</mesh>
|
|
211
|
+
{/* @ts-ignore */}
|
|
205
212
|
<line geometry={previewOutlineGeometry} position={[0, (node.height ?? 2.5) + 0.02, 0]}>
|
|
206
213
|
<lineBasicMaterial color="#ffffff" depthWrite={false} opacity={0.95} transparent />
|
|
207
214
|
</line>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import '../../../three-types'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
COLUMN_PRESETS,
|
|
5
|
+
ColumnNode,
|
|
6
|
+
type ColumnNode as ColumnNodeType,
|
|
7
|
+
type ColumnPresetId,
|
|
8
|
+
emitter,
|
|
9
|
+
type GridEvent,
|
|
10
|
+
type LevelNode,
|
|
11
|
+
useScene,
|
|
12
|
+
} from '@pascal-app/core'
|
|
13
|
+
import { useEffect, useRef, useState } from 'react'
|
|
14
|
+
import type { Group } from 'three'
|
|
15
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
16
|
+
import useEditor from '../../../store/use-editor'
|
|
17
|
+
import { CursorSphere } from '../shared/cursor-sphere'
|
|
18
|
+
|
|
19
|
+
const COLUMN_ICON = (
|
|
20
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
21
|
+
<img
|
|
22
|
+
alt="Column"
|
|
23
|
+
src="/icons/column.png"
|
|
24
|
+
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const roundToHalf = (value: number) => Math.round(value * 2) / 2
|
|
29
|
+
const DEFAULT_COLUMN_PRESET_ID = 'basicPillar' satisfies ColumnPresetId
|
|
30
|
+
|
|
31
|
+
function createColumnFromPreset(presetId: ColumnPresetId, position: [number, number, number]) {
|
|
32
|
+
const { label, ...preset } = COLUMN_PRESETS[presetId]
|
|
33
|
+
return ColumnNode.parse({
|
|
34
|
+
name: label,
|
|
35
|
+
position,
|
|
36
|
+
rotation: 0,
|
|
37
|
+
...preset,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type ColumnToolProps = {
|
|
42
|
+
currentLevelId: LevelNode['id'] | null
|
|
43
|
+
onPlaced?: (nodeId: ColumnNodeType['id']) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const ColumnTool: React.FC<ColumnToolProps> = ({ currentLevelId, onPlaced }) => {
|
|
47
|
+
const [, setCursorPosition] = useState<[number, number, number] | null>(null)
|
|
48
|
+
const cursorRef = useRef<Group>(null)
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!currentLevelId) return
|
|
52
|
+
|
|
53
|
+
const onGridMove = (event: GridEvent) => {
|
|
54
|
+
const nextPosition: [number, number, number] = [
|
|
55
|
+
roundToHalf(event.localPosition[0]),
|
|
56
|
+
0,
|
|
57
|
+
roundToHalf(event.localPosition[2]),
|
|
58
|
+
]
|
|
59
|
+
setCursorPosition(nextPosition)
|
|
60
|
+
cursorRef.current?.position.set(nextPosition[0], event.localPosition[1], nextPosition[2])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const onGridClick = (event: GridEvent) => {
|
|
64
|
+
const position: [number, number, number] = [
|
|
65
|
+
roundToHalf(event.localPosition[0]),
|
|
66
|
+
0,
|
|
67
|
+
roundToHalf(event.localPosition[2]),
|
|
68
|
+
]
|
|
69
|
+
const column = createColumnFromPreset(DEFAULT_COLUMN_PRESET_ID, position)
|
|
70
|
+
useScene.getState().createNode(column, currentLevelId)
|
|
71
|
+
onPlaced?.(column.id)
|
|
72
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
73
|
+
useEditor.getState().setTool(null)
|
|
74
|
+
useEditor.getState().setMode('select')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
emitter.on('grid:move', onGridMove)
|
|
78
|
+
emitter.on('grid:click', onGridClick)
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
emitter.off('grid:move', onGridMove)
|
|
82
|
+
emitter.off('grid:click', onGridClick)
|
|
83
|
+
}
|
|
84
|
+
}, [currentLevelId, onPlaced])
|
|
85
|
+
|
|
86
|
+
if (!currentLevelId) return null
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<CursorSphere
|
|
90
|
+
color="#a78bfa"
|
|
91
|
+
height={2.8}
|
|
92
|
+
ref={cursorRef}
|
|
93
|
+
showTooltip
|
|
94
|
+
tooltipContent={COLUMN_ICON}
|
|
95
|
+
/>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import '../../../three-types'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
ColumnNode,
|
|
6
|
+
type ColumnNode as ColumnNodeType,
|
|
7
|
+
emitter,
|
|
8
|
+
type GridEvent,
|
|
9
|
+
sceneRegistry,
|
|
10
|
+
useLiveTransforms,
|
|
11
|
+
useScene,
|
|
12
|
+
} from '@pascal-app/core'
|
|
13
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
14
|
+
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
15
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
16
|
+
import useEditor from '../../../store/use-editor'
|
|
17
|
+
import { CursorSphere } from '../shared/cursor-sphere'
|
|
18
|
+
|
|
19
|
+
const roundToHalf = (value: number) => Math.round(value * 2) / 2
|
|
20
|
+
|
|
21
|
+
export function MoveColumnTool({ node }: { node: ColumnNodeType }) {
|
|
22
|
+
const [previewPosition, setPreviewPosition] = useState<[number, number, number]>(node.position)
|
|
23
|
+
|
|
24
|
+
const exitMoveMode = useCallback(() => {
|
|
25
|
+
useEditor.getState().setMovingNode(null)
|
|
26
|
+
}, [])
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
useScene.temporal.getState().pause()
|
|
30
|
+
let committed = false
|
|
31
|
+
|
|
32
|
+
const applyPreview = (position: [number, number, number]) => {
|
|
33
|
+
setPreviewPosition(position)
|
|
34
|
+
useLiveTransforms.getState().set(node.id, {
|
|
35
|
+
position,
|
|
36
|
+
rotation: node.rotation,
|
|
37
|
+
})
|
|
38
|
+
sceneRegistry.nodes.get(node.id)?.position.set(position[0], position[1], position[2])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const onGridMove = (event: GridEvent) => {
|
|
42
|
+
applyPreview([roundToHalf(event.localPosition[0]), 0, roundToHalf(event.localPosition[2])])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const onGridClick = (event: GridEvent) => {
|
|
46
|
+
const position: [number, number, number] = [
|
|
47
|
+
roundToHalf(event.localPosition[0]),
|
|
48
|
+
0,
|
|
49
|
+
roundToHalf(event.localPosition[2]),
|
|
50
|
+
]
|
|
51
|
+
const nodeId = (node as { id?: ColumnNodeType['id'] }).id
|
|
52
|
+
|
|
53
|
+
if (nodeId && useScene.getState().nodes[nodeId]) {
|
|
54
|
+
committed = true
|
|
55
|
+
useLiveTransforms.getState().clear(nodeId)
|
|
56
|
+
useScene.temporal.getState().resume()
|
|
57
|
+
useScene.getState().updateNode(nodeId, { position })
|
|
58
|
+
} else if (node.parentId) {
|
|
59
|
+
const column = ColumnNode.parse({
|
|
60
|
+
...node,
|
|
61
|
+
id: undefined,
|
|
62
|
+
metadata: {},
|
|
63
|
+
position,
|
|
64
|
+
})
|
|
65
|
+
committed = true
|
|
66
|
+
useScene.temporal.getState().resume()
|
|
67
|
+
useScene.getState().createNode(column, node.parentId as AnyNodeId)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
useLiveTransforms.getState().clear(node.id)
|
|
71
|
+
sfxEmitter.emit('sfx:item-place')
|
|
72
|
+
exitMoveMode()
|
|
73
|
+
event.nativeEvent?.stopPropagation?.()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const onCancel = () => {
|
|
77
|
+
useLiveTransforms.getState().clear(node.id)
|
|
78
|
+
sceneRegistry.nodes
|
|
79
|
+
.get(node.id)
|
|
80
|
+
?.position.set(node.position[0], node.position[1], node.position[2])
|
|
81
|
+
useScene.temporal.getState().resume()
|
|
82
|
+
markToolCancelConsumed()
|
|
83
|
+
exitMoveMode()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
emitter.on('grid:move', onGridMove)
|
|
87
|
+
emitter.on('grid:click', onGridClick)
|
|
88
|
+
emitter.on('tool:cancel', onCancel)
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
emitter.off('grid:move', onGridMove)
|
|
92
|
+
emitter.off('grid:click', onGridClick)
|
|
93
|
+
emitter.off('tool:cancel', onCancel)
|
|
94
|
+
useLiveTransforms.getState().clear(node.id)
|
|
95
|
+
if (!committed) {
|
|
96
|
+
sceneRegistry.nodes
|
|
97
|
+
.get(node.id)
|
|
98
|
+
?.position.set(node.position[0], node.position[1], node.position[2])
|
|
99
|
+
useScene.temporal.getState().resume()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}, [exitMoveMode, node])
|
|
103
|
+
|
|
104
|
+
return <CursorSphere color="#a78bfa" height={node.height} position={previewPosition} />
|
|
105
|
+
}
|
|
@@ -248,6 +248,13 @@ export const DoorTool: React.FC = () => {
|
|
|
248
248
|
parentId: event.node.id,
|
|
249
249
|
width: draft.width,
|
|
250
250
|
height: draft.height,
|
|
251
|
+
doorCategory: draft.doorCategory,
|
|
252
|
+
doorType: draft.doorType,
|
|
253
|
+
leafCount: draft.leafCount,
|
|
254
|
+
operationState: draft.operationState,
|
|
255
|
+
slideDirection: draft.slideDirection,
|
|
256
|
+
trackStyle: draft.trackStyle,
|
|
257
|
+
garagePanelCount: draft.garagePanelCount,
|
|
251
258
|
frameThickness: draft.frameThickness,
|
|
252
259
|
frameDepth: draft.frameDepth,
|
|
253
260
|
threshold: draft.threshold,
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
isCurvedWall,
|
|
6
6
|
sceneRegistry,
|
|
7
7
|
spatialGridManager,
|
|
8
|
+
useLiveTransforms,
|
|
8
9
|
useScene,
|
|
9
10
|
type WallEvent,
|
|
10
11
|
} from '@pascal-app/core'
|
|
@@ -97,6 +98,18 @@ export const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNod
|
|
|
97
98
|
edgeMaterial.color.setHex(valid ? 0x22_c5_5e : 0xef_44_44)
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
const getPlacementOrientation = (event: WallEvent) => {
|
|
102
|
+
const faceSide = getSideFromNormal(event.normal)
|
|
103
|
+
const side = movingDoorNode.side ?? faceSide
|
|
104
|
+
const rotationOffset = side !== faceSide ? Math.PI : 0
|
|
105
|
+
return {
|
|
106
|
+
side,
|
|
107
|
+
itemRotation: calculateItemRotation(event.normal) + rotationOffset,
|
|
108
|
+
cursorRotation:
|
|
109
|
+
calculateCursorRotation(event.normal, event.node.start, event.node.end) + rotationOffset,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
100
113
|
const onWallEnter = (event: WallEvent) => {
|
|
101
114
|
if (!isValidWallSideFace(event.normal)) return
|
|
102
115
|
if (isCurvedWall(event.node)) {
|
|
@@ -105,9 +118,7 @@ export const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNod
|
|
|
105
118
|
}
|
|
106
119
|
if (event.node.parentId !== getLevelId()) return
|
|
107
120
|
|
|
108
|
-
const side =
|
|
109
|
-
const itemRotation = calculateItemRotation(event.normal)
|
|
110
|
-
const cursorRotation = calculateCursorRotation(event.normal, event.node.start, event.node.end)
|
|
121
|
+
const { side, itemRotation, cursorRotation } = getPlacementOrientation(event)
|
|
111
122
|
|
|
112
123
|
const localX = snapToHalf(event.localPosition[0])
|
|
113
124
|
const { clampedX, clampedY } = clampToWall(
|
|
@@ -127,6 +138,10 @@ export const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNod
|
|
|
127
138
|
parentId: event.node.id,
|
|
128
139
|
wallId: event.node.id,
|
|
129
140
|
})
|
|
141
|
+
useLiveTransforms.getState().set(movingDoorNode.id, {
|
|
142
|
+
position: [clampedX, clampedY, 0],
|
|
143
|
+
rotation: itemRotation,
|
|
144
|
+
})
|
|
130
145
|
|
|
131
146
|
if (prevWallId && prevWallId !== event.node.id) markWallDirty(prevWallId)
|
|
132
147
|
markWallDirty(event.node.id)
|
|
@@ -162,9 +177,7 @@ export const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNod
|
|
|
162
177
|
}
|
|
163
178
|
if (event.node.parentId !== getLevelId()) return
|
|
164
179
|
|
|
165
|
-
const side =
|
|
166
|
-
const itemRotation = calculateItemRotation(event.normal)
|
|
167
|
-
const cursorRotation = calculateCursorRotation(event.normal, event.node.start, event.node.end)
|
|
180
|
+
const { side, itemRotation, cursorRotation } = getPlacementOrientation(event)
|
|
168
181
|
|
|
169
182
|
const localX = snapToHalf(event.localPosition[0])
|
|
170
183
|
const { clampedX, clampedY } = clampToWall(
|
|
@@ -195,6 +208,10 @@ export const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNod
|
|
|
195
208
|
doorMesh.updateMatrixWorld(true)
|
|
196
209
|
}
|
|
197
210
|
}
|
|
211
|
+
useLiveTransforms.getState().set(movingDoorNode.id, {
|
|
212
|
+
position: [clampedX, clampedY, 0],
|
|
213
|
+
rotation: itemRotation,
|
|
214
|
+
})
|
|
198
215
|
markWallDirty(event.node.id)
|
|
199
216
|
|
|
200
217
|
const valid = !hasWallChildOverlap(
|
|
@@ -225,8 +242,7 @@ export const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNod
|
|
|
225
242
|
if (isCurvedWall(event.node)) return
|
|
226
243
|
if (event.node.parentId !== getLevelId()) return
|
|
227
244
|
|
|
228
|
-
const side =
|
|
229
|
-
const itemRotation = calculateItemRotation(event.normal)
|
|
245
|
+
const { side, itemRotation } = getPlacementOrientation(event)
|
|
230
246
|
|
|
231
247
|
const localX = snapToHalf(event.localPosition[0])
|
|
232
248
|
const { clampedX, clampedY } = clampToWall(
|
|
@@ -291,6 +307,7 @@ export const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNod
|
|
|
291
307
|
}
|
|
292
308
|
|
|
293
309
|
markWallDirty(event.node.id)
|
|
310
|
+
useLiveTransforms.getState().clear(movingDoorNode.id)
|
|
294
311
|
useScene.temporal.getState().pause()
|
|
295
312
|
|
|
296
313
|
sfxEmitter.emit('sfx:item-place')
|
|
@@ -302,6 +319,7 @@ export const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNod
|
|
|
302
319
|
|
|
303
320
|
const onWallLeave = () => {
|
|
304
321
|
hideCursor()
|
|
322
|
+
useLiveTransforms.getState().clear(movingDoorNode.id)
|
|
305
323
|
if (isNew) return
|
|
306
324
|
if (currentWallId && currentWallId !== original.parentId) {
|
|
307
325
|
markWallDirty(currentWallId)
|
|
@@ -318,6 +336,7 @@ export const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNod
|
|
|
318
336
|
}
|
|
319
337
|
|
|
320
338
|
const onCancel = () => {
|
|
339
|
+
useLiveTransforms.getState().clear(movingDoorNode.id)
|
|
321
340
|
if (isNew) {
|
|
322
341
|
useScene.getState().deleteNode(movingDoorNode.id)
|
|
323
342
|
if (currentWallId) markWallDirty(currentWallId)
|
|
@@ -364,6 +383,7 @@ export const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNod
|
|
|
364
383
|
if (original.parentId) markWallDirty(original.parentId)
|
|
365
384
|
}
|
|
366
385
|
}
|
|
386
|
+
useLiveTransforms.getState().clear(movingDoorNode.id)
|
|
367
387
|
useScene.temporal.getState().resume()
|
|
368
388
|
emitter.off('wall:enter', onWallEnter)
|
|
369
389
|
emitter.off('wall:move', onWallMove)
|
|
@@ -83,11 +83,10 @@ export const CurveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => {
|
|
|
83
83
|
? event.localPosition[2]
|
|
84
84
|
: snapScalarToGrid(event.localPosition[2], snapStep)
|
|
85
85
|
|
|
86
|
-
const offsetFromMidpoint =
|
|
87
|
-
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
86
|
+
const offsetFromMidpoint = -(
|
|
87
|
+
(localX - chord.midpoint.x) * chord.normal.x +
|
|
88
|
+
(localZ - chord.midpoint.y) * chord.normal.y
|
|
89
|
+
)
|
|
91
90
|
const snappedOffset = shiftPressedRef.current
|
|
92
91
|
? offsetFromMidpoint
|
|
93
92
|
: snapScalarToGrid(offsetFromMidpoint, snapStep)
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
FenceNode,
|
|
3
|
+
getWallCurveFrameAt,
|
|
4
|
+
getWallCurveLength,
|
|
5
|
+
isCurvedWall,
|
|
6
|
+
useScene,
|
|
7
|
+
type WallNode,
|
|
8
|
+
} from '@pascal-app/core'
|
|
2
9
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
10
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
4
11
|
import {
|
|
12
|
+
findWallSnapTarget,
|
|
5
13
|
getWallAngleSnapStep,
|
|
6
14
|
getWallGridStep,
|
|
7
|
-
type WallPlanPoint,
|
|
8
|
-
findWallSnapTarget,
|
|
9
15
|
isWallLongEnough,
|
|
10
16
|
snapPointTo45Degrees,
|
|
11
17
|
snapPointToGrid,
|
|
18
|
+
type WallPlanPoint,
|
|
12
19
|
} from '../wall/wall-drafting'
|
|
13
20
|
|
|
14
21
|
export type FencePlanPoint = WallPlanPoint
|