@pascal-app/editor 0.4.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 +62 -0
- package/src/components/editor/custom-camera-controls.tsx +387 -0
- package/src/components/editor/editor-layout-v2.tsx +220 -0
- package/src/components/editor/export-manager.tsx +78 -0
- package/src/components/editor/first-person-controls.tsx +249 -0
- package/src/components/editor/floating-action-menu.tsx +231 -0
- package/src/components/editor/floorplan-panel.tsx +9609 -0
- package/src/components/editor/grid.tsx +161 -0
- package/src/components/editor/index.tsx +928 -0
- package/src/components/editor/node-action-menu.tsx +66 -0
- package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
- package/src/components/editor/selection-manager.tsx +897 -0
- package/src/components/editor/site-edge-labels.tsx +90 -0
- package/src/components/editor/thumbnail-generator.tsx +166 -0
- package/src/components/editor/wall-measurement-label.tsx +258 -0
- package/src/components/feedback-dialog.tsx +265 -0
- package/src/components/pascal-radio.tsx +280 -0
- package/src/components/preview-button.tsx +16 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
- package/src/components/systems/roof/roof-edit-system.tsx +69 -0
- package/src/components/systems/stair/stair-edit-system.tsx +69 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
- package/src/components/systems/zone/zone-system.tsx +87 -0
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
- package/src/components/tools/door/door-math.ts +110 -0
- package/src/components/tools/door/door-tool.tsx +293 -0
- package/src/components/tools/door/move-door-tool.tsx +373 -0
- package/src/components/tools/item/item-tool.tsx +26 -0
- package/src/components/tools/item/move-tool.tsx +90 -0
- package/src/components/tools/item/placement-math.ts +85 -0
- package/src/components/tools/item/placement-strategies.ts +556 -0
- package/src/components/tools/item/placement-types.ts +117 -0
- package/src/components/tools/item/use-draft-node.ts +227 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
- package/src/components/tools/roof/move-roof-tool.tsx +288 -0
- package/src/components/tools/roof/roof-tool.tsx +318 -0
- package/src/components/tools/select/box-select-tool.tsx +626 -0
- package/src/components/tools/shared/cursor-sphere.tsx +119 -0
- package/src/components/tools/shared/polygon-editor.tsx +361 -0
- package/src/components/tools/site/site-boundary-editor.tsx +42 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
- package/src/components/tools/slab/slab-tool.tsx +322 -0
- package/src/components/tools/stair/stair-defaults.ts +7 -0
- package/src/components/tools/stair/stair-tool.tsx +194 -0
- package/src/components/tools/tool-manager.tsx +120 -0
- package/src/components/tools/wall/wall-drafting.ts +140 -0
- package/src/components/tools/wall/wall-tool.tsx +210 -0
- package/src/components/tools/window/move-window-tool.tsx +410 -0
- package/src/components/tools/window/window-math.ts +117 -0
- package/src/components/tools/window/window-tool.tsx +303 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
- package/src/components/tools/zone/zone-tool.tsx +364 -0
- package/src/components/ui/action-menu/action-button.tsx +59 -0
- package/src/components/ui/action-menu/camera-actions.tsx +74 -0
- package/src/components/ui/action-menu/control-modes.tsx +240 -0
- package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
- package/src/components/ui/action-menu/index.tsx +152 -0
- package/src/components/ui/action-menu/structure-tools.tsx +100 -0
- package/src/components/ui/action-menu/view-toggles.tsx +397 -0
- package/src/components/ui/command-palette/editor-commands.tsx +396 -0
- package/src/components/ui/command-palette/index.tsx +730 -0
- package/src/components/ui/controls/action-button.tsx +33 -0
- package/src/components/ui/controls/material-picker.tsx +194 -0
- package/src/components/ui/controls/metric-control.tsx +262 -0
- package/src/components/ui/controls/panel-section.tsx +65 -0
- package/src/components/ui/controls/segmented-control.tsx +45 -0
- package/src/components/ui/controls/slider-control.tsx +245 -0
- package/src/components/ui/controls/toggle-control.tsx +38 -0
- package/src/components/ui/floating-level-selector.tsx +355 -0
- package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
- package/src/components/ui/helpers/helper-manager.tsx +33 -0
- package/src/components/ui/helpers/item-helper.tsx +40 -0
- package/src/components/ui/helpers/roof-helper.tsx +16 -0
- package/src/components/ui/helpers/slab-helper.tsx +20 -0
- package/src/components/ui/helpers/wall-helper.tsx +20 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
- package/src/components/ui/panels/ceiling-panel.tsx +230 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
- package/src/components/ui/panels/door-panel.tsx +600 -0
- package/src/components/ui/panels/item-panel.tsx +306 -0
- package/src/components/ui/panels/panel-manager.tsx +59 -0
- package/src/components/ui/panels/panel-wrapper.tsx +80 -0
- package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
- package/src/components/ui/panels/reference-panel.tsx +177 -0
- package/src/components/ui/panels/roof-panel.tsx +262 -0
- package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
- package/src/components/ui/panels/slab-panel.tsx +228 -0
- package/src/components/ui/panels/stair-panel.tsx +304 -0
- package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
- package/src/components/ui/panels/wall-panel.tsx +123 -0
- package/src/components/ui/panels/window-panel.tsx +441 -0
- package/src/components/ui/primitives/button.tsx +69 -0
- package/src/components/ui/primitives/card.tsx +75 -0
- package/src/components/ui/primitives/color-dot.tsx +61 -0
- package/src/components/ui/primitives/context-menu.tsx +227 -0
- package/src/components/ui/primitives/dialog.tsx +129 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
- package/src/components/ui/primitives/error-boundary.tsx +52 -0
- package/src/components/ui/primitives/input.tsx +21 -0
- package/src/components/ui/primitives/number-input.tsx +187 -0
- package/src/components/ui/primitives/opacity-control.tsx +79 -0
- package/src/components/ui/primitives/popover.tsx +42 -0
- package/src/components/ui/primitives/separator.tsx +28 -0
- package/src/components/ui/primitives/sheet.tsx +130 -0
- package/src/components/ui/primitives/shortcut-token.tsx +64 -0
- package/src/components/ui/primitives/sidebar.tsx +855 -0
- package/src/components/ui/primitives/skeleton.tsx +13 -0
- package/src/components/ui/primitives/slider.tsx +58 -0
- package/src/components/ui/primitives/switch.tsx +29 -0
- package/src/components/ui/primitives/tooltip.tsx +57 -0
- package/src/components/ui/scene-loader.tsx +40 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
- package/src/components/ui/sidebar/icon-rail.tsx +147 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
- package/src/components/ui/sidebar/tab-bar.tsx +39 -0
- package/src/components/ui/slider-demo.tsx +36 -0
- package/src/components/ui/slider.tsx +81 -0
- package/src/components/ui/viewer-toolbar.tsx +342 -0
- package/src/components/viewer-overlay.tsx +499 -0
- package/src/components/viewer-zone-system.tsx +48 -0
- package/src/contexts/presets-context.tsx +121 -0
- package/src/hooks/use-auto-save.ts +194 -0
- package/src/hooks/use-contextual-tools.ts +52 -0
- package/src/hooks/use-grid-events.ts +106 -0
- package/src/hooks/use-keyboard.ts +214 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-reduced-motion.ts +20 -0
- package/src/index.tsx +33 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/level-selection.ts +31 -0
- package/src/lib/scene.ts +394 -0
- package/src/lib/sfx/index.ts +2 -0
- package/src/lib/sfx-bus.ts +49 -0
- package/src/lib/sfx-player.ts +60 -0
- package/src/lib/utils.ts +43 -0
- package/src/store/use-audio.tsx +45 -0
- package/src/store/use-command-registry.ts +36 -0
- package/src/store/use-editor.tsx +522 -0
- package/src/store/use-palette-view-registry.ts +45 -0
- package/src/store/use-upload.ts +90 -0
- package/src/three-types.ts +3 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
type AssetInput,
|
|
4
|
+
ItemNode,
|
|
5
|
+
sceneRegistry,
|
|
6
|
+
useScene,
|
|
7
|
+
} from '@pascal-app/core'
|
|
8
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
9
|
+
import { useCallback, useMemo, useRef } from 'react'
|
|
10
|
+
import type { Vector3 } from 'three'
|
|
11
|
+
import { stripTransient } from './placement-math'
|
|
12
|
+
|
|
13
|
+
interface OriginalState {
|
|
14
|
+
position: [number, number, number]
|
|
15
|
+
rotation: [number, number, number]
|
|
16
|
+
side: ItemNode['side']
|
|
17
|
+
parentId: string | null
|
|
18
|
+
metadata: ItemNode['metadata']
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DraftNodeHandle {
|
|
22
|
+
/** Current draft item, or null */
|
|
23
|
+
readonly current: ItemNode | null
|
|
24
|
+
/** Whether the current draft was adopted (move mode) vs created (create mode) */
|
|
25
|
+
readonly isAdopted: boolean
|
|
26
|
+
/** Create a new draft item at the given position. Returns the created node or null. */
|
|
27
|
+
create: (
|
|
28
|
+
gridPosition: Vector3,
|
|
29
|
+
asset: AssetInput,
|
|
30
|
+
rotation?: [number, number, number],
|
|
31
|
+
scale?: [number, number, number],
|
|
32
|
+
) => ItemNode | null
|
|
33
|
+
/** Take ownership of an existing scene node as the draft (for move mode). */
|
|
34
|
+
adopt: (node: ItemNode) => void
|
|
35
|
+
/** Commit the current draft. Create mode: delete+recreate. Move mode: update in place. */
|
|
36
|
+
commit: (finalUpdate: Partial<ItemNode>) => string | null
|
|
37
|
+
/** Destroy the current draft. Create mode: delete node. Move mode: restore original state. */
|
|
38
|
+
destroy: () => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Hook that manages the lifecycle of a transient (draft) item node.
|
|
43
|
+
* Handles temporal pause/resume for undo/redo isolation.
|
|
44
|
+
*
|
|
45
|
+
* Supports two modes:
|
|
46
|
+
* - Create mode (via `create()`): draft is a new transient node. Commit = delete+recreate (undo removes node).
|
|
47
|
+
* - Move mode (via `adopt()`): draft is an existing node. Commit = update in place (undo reverts position).
|
|
48
|
+
*/
|
|
49
|
+
export function useDraftNode(): DraftNodeHandle {
|
|
50
|
+
const draftRef = useRef<ItemNode | null>(null)
|
|
51
|
+
const adoptedRef = useRef(false)
|
|
52
|
+
const originalStateRef = useRef<OriginalState | null>(null)
|
|
53
|
+
|
|
54
|
+
const create = useCallback(
|
|
55
|
+
(
|
|
56
|
+
gridPosition: Vector3,
|
|
57
|
+
asset: AssetInput,
|
|
58
|
+
rotation?: [number, number, number],
|
|
59
|
+
scale?: [number, number, number],
|
|
60
|
+
): ItemNode | null => {
|
|
61
|
+
const currentLevelId = useViewer.getState().selection.levelId
|
|
62
|
+
if (!currentLevelId) return null
|
|
63
|
+
|
|
64
|
+
const node = ItemNode.parse({
|
|
65
|
+
position: [gridPosition.x, gridPosition.y, gridPosition.z],
|
|
66
|
+
rotation: rotation ?? [0, 0, 0],
|
|
67
|
+
scale: scale ?? [1, 1, 1],
|
|
68
|
+
name: asset.name,
|
|
69
|
+
asset,
|
|
70
|
+
parentId: currentLevelId,
|
|
71
|
+
metadata: { isTransient: true },
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
useScene.getState().createNode(node, currentLevelId)
|
|
75
|
+
draftRef.current = node
|
|
76
|
+
adoptedRef.current = false
|
|
77
|
+
originalStateRef.current = null
|
|
78
|
+
return node
|
|
79
|
+
},
|
|
80
|
+
[],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const adopt = useCallback((node: ItemNode): void => {
|
|
84
|
+
// Save original state so destroy() can restore it
|
|
85
|
+
const meta =
|
|
86
|
+
typeof node.metadata === 'object' && node.metadata !== null && !Array.isArray(node.metadata)
|
|
87
|
+
? (node.metadata as Record<string, unknown>)
|
|
88
|
+
: {}
|
|
89
|
+
|
|
90
|
+
originalStateRef.current = {
|
|
91
|
+
position: [...node.position] as [number, number, number],
|
|
92
|
+
rotation: [...node.rotation] as [number, number, number],
|
|
93
|
+
side: node.side,
|
|
94
|
+
parentId: node.parentId,
|
|
95
|
+
metadata: node.metadata,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
draftRef.current = node
|
|
99
|
+
adoptedRef.current = true
|
|
100
|
+
|
|
101
|
+
// Mark as transient so it renders as a draft
|
|
102
|
+
useScene.getState().updateNode(node.id, {
|
|
103
|
+
metadata: { ...meta, isTransient: true },
|
|
104
|
+
})
|
|
105
|
+
}, [])
|
|
106
|
+
|
|
107
|
+
const commit = useCallback((finalUpdate: Partial<ItemNode>): string | null => {
|
|
108
|
+
const draft = draftRef.current
|
|
109
|
+
if (!draft) return null
|
|
110
|
+
|
|
111
|
+
if (adoptedRef.current) {
|
|
112
|
+
// Move mode: update in place (single undoable action)
|
|
113
|
+
const { parentId: newParentId, ...updateProps } = finalUpdate
|
|
114
|
+
const parentId =
|
|
115
|
+
newParentId ?? originalStateRef.current?.parentId ?? useViewer.getState().selection.levelId
|
|
116
|
+
const original = originalStateRef.current!
|
|
117
|
+
|
|
118
|
+
// Restore original state while paused — so the undo baseline is clean
|
|
119
|
+
useScene.getState().updateNode(draft.id, {
|
|
120
|
+
position: original.position,
|
|
121
|
+
rotation: original.rotation,
|
|
122
|
+
side: original.side,
|
|
123
|
+
parentId: original.parentId,
|
|
124
|
+
metadata: original.metadata,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Resume → tracked update (undo reverts to original)
|
|
128
|
+
useScene.temporal.getState().resume()
|
|
129
|
+
|
|
130
|
+
useScene.getState().updateNode(draft.id, {
|
|
131
|
+
position: updateProps.position ?? draft.position,
|
|
132
|
+
rotation: updateProps.rotation ?? draft.rotation,
|
|
133
|
+
side: updateProps.side ?? draft.side,
|
|
134
|
+
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
|
135
|
+
parentId: parentId as string,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
useScene.temporal.getState().pause()
|
|
139
|
+
|
|
140
|
+
const id = draft.id
|
|
141
|
+
draftRef.current = null
|
|
142
|
+
adoptedRef.current = false
|
|
143
|
+
originalStateRef.current = null
|
|
144
|
+
return id
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Create mode: delete draft (paused), resume, create fresh node (tracked), re-pause
|
|
148
|
+
const { parentId: newParentId, ...updateProps } = finalUpdate
|
|
149
|
+
const parentId = (newParentId ?? useViewer.getState().selection.levelId) as AnyNodeId
|
|
150
|
+
if (!parentId) return null
|
|
151
|
+
|
|
152
|
+
// Delete draft while paused (invisible to undo)
|
|
153
|
+
useScene.getState().deleteNode(draft.id)
|
|
154
|
+
draftRef.current = null
|
|
155
|
+
|
|
156
|
+
// Briefly resume → create fresh node (the single undoable action)
|
|
157
|
+
useScene.temporal.getState().resume()
|
|
158
|
+
|
|
159
|
+
const finalNode = ItemNode.parse({
|
|
160
|
+
name: draft.name,
|
|
161
|
+
asset: draft.asset,
|
|
162
|
+
position: updateProps.position ?? draft.position,
|
|
163
|
+
rotation: updateProps.rotation ?? draft.rotation,
|
|
164
|
+
side: updateProps.side ?? draft.side,
|
|
165
|
+
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
|
166
|
+
})
|
|
167
|
+
useScene.getState().createNode(finalNode, parentId)
|
|
168
|
+
|
|
169
|
+
// Re-pause for next draft cycle
|
|
170
|
+
useScene.temporal.getState().pause()
|
|
171
|
+
|
|
172
|
+
adoptedRef.current = false
|
|
173
|
+
originalStateRef.current = null
|
|
174
|
+
return finalNode.id
|
|
175
|
+
}, [])
|
|
176
|
+
|
|
177
|
+
const destroy = useCallback(() => {
|
|
178
|
+
if (!draftRef.current) return
|
|
179
|
+
|
|
180
|
+
if (adoptedRef.current && originalStateRef.current) {
|
|
181
|
+
// Move mode: restore original state instead of deleting
|
|
182
|
+
const original = originalStateRef.current
|
|
183
|
+
const id = draftRef.current.id
|
|
184
|
+
|
|
185
|
+
useScene.getState().updateNode(id, {
|
|
186
|
+
position: original.position,
|
|
187
|
+
rotation: original.rotation,
|
|
188
|
+
side: original.side,
|
|
189
|
+
parentId: original.parentId,
|
|
190
|
+
metadata: original.metadata,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Also reset the Three.js mesh directly — the store update triggers a React
|
|
194
|
+
// re-render but the mesh position was mutated by useFrame and may not reset
|
|
195
|
+
// until the next render cycle, leaving a visual glitch.
|
|
196
|
+
const mesh = sceneRegistry.nodes.get(id as AnyNodeId)
|
|
197
|
+
if (mesh) {
|
|
198
|
+
mesh.position.set(original.position[0], original.position[1], original.position[2])
|
|
199
|
+
mesh.rotation.y = original.rotation[1] ?? 0
|
|
200
|
+
mesh.visible = true
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
// Create mode: delete the transient node
|
|
204
|
+
useScene.getState().deleteNode(draftRef.current.id)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
draftRef.current = null
|
|
208
|
+
adoptedRef.current = false
|
|
209
|
+
originalStateRef.current = null
|
|
210
|
+
}, [])
|
|
211
|
+
|
|
212
|
+
return useMemo(
|
|
213
|
+
() => ({
|
|
214
|
+
get current() {
|
|
215
|
+
return draftRef.current
|
|
216
|
+
},
|
|
217
|
+
get isAdopted() {
|
|
218
|
+
return adoptedRef.current
|
|
219
|
+
},
|
|
220
|
+
create,
|
|
221
|
+
adopt,
|
|
222
|
+
commit,
|
|
223
|
+
destroy,
|
|
224
|
+
}),
|
|
225
|
+
[create, adopt, commit, destroy],
|
|
226
|
+
)
|
|
227
|
+
}
|