@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.
Files changed (165) hide show
  1. package/package.json +62 -0
  2. package/src/components/editor/custom-camera-controls.tsx +387 -0
  3. package/src/components/editor/editor-layout-v2.tsx +220 -0
  4. package/src/components/editor/export-manager.tsx +78 -0
  5. package/src/components/editor/first-person-controls.tsx +249 -0
  6. package/src/components/editor/floating-action-menu.tsx +231 -0
  7. package/src/components/editor/floorplan-panel.tsx +9609 -0
  8. package/src/components/editor/grid.tsx +161 -0
  9. package/src/components/editor/index.tsx +928 -0
  10. package/src/components/editor/node-action-menu.tsx +66 -0
  11. package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
  12. package/src/components/editor/selection-manager.tsx +897 -0
  13. package/src/components/editor/site-edge-labels.tsx +90 -0
  14. package/src/components/editor/thumbnail-generator.tsx +166 -0
  15. package/src/components/editor/wall-measurement-label.tsx +258 -0
  16. package/src/components/feedback-dialog.tsx +265 -0
  17. package/src/components/pascal-radio.tsx +280 -0
  18. package/src/components/preview-button.tsx +16 -0
  19. package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
  20. package/src/components/systems/roof/roof-edit-system.tsx +69 -0
  21. package/src/components/systems/stair/stair-edit-system.tsx +69 -0
  22. package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
  23. package/src/components/systems/zone/zone-system.tsx +87 -0
  24. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
  25. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
  26. package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
  27. package/src/components/tools/door/door-math.ts +110 -0
  28. package/src/components/tools/door/door-tool.tsx +293 -0
  29. package/src/components/tools/door/move-door-tool.tsx +373 -0
  30. package/src/components/tools/item/item-tool.tsx +26 -0
  31. package/src/components/tools/item/move-tool.tsx +90 -0
  32. package/src/components/tools/item/placement-math.ts +85 -0
  33. package/src/components/tools/item/placement-strategies.ts +556 -0
  34. package/src/components/tools/item/placement-types.ts +117 -0
  35. package/src/components/tools/item/use-draft-node.ts +227 -0
  36. package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
  37. package/src/components/tools/roof/move-roof-tool.tsx +288 -0
  38. package/src/components/tools/roof/roof-tool.tsx +318 -0
  39. package/src/components/tools/select/box-select-tool.tsx +626 -0
  40. package/src/components/tools/shared/cursor-sphere.tsx +119 -0
  41. package/src/components/tools/shared/polygon-editor.tsx +361 -0
  42. package/src/components/tools/site/site-boundary-editor.tsx +42 -0
  43. package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
  44. package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
  45. package/src/components/tools/slab/slab-tool.tsx +322 -0
  46. package/src/components/tools/stair/stair-defaults.ts +7 -0
  47. package/src/components/tools/stair/stair-tool.tsx +194 -0
  48. package/src/components/tools/tool-manager.tsx +120 -0
  49. package/src/components/tools/wall/wall-drafting.ts +140 -0
  50. package/src/components/tools/wall/wall-tool.tsx +210 -0
  51. package/src/components/tools/window/move-window-tool.tsx +410 -0
  52. package/src/components/tools/window/window-math.ts +117 -0
  53. package/src/components/tools/window/window-tool.tsx +303 -0
  54. package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
  55. package/src/components/tools/zone/zone-tool.tsx +364 -0
  56. package/src/components/ui/action-menu/action-button.tsx +59 -0
  57. package/src/components/ui/action-menu/camera-actions.tsx +74 -0
  58. package/src/components/ui/action-menu/control-modes.tsx +240 -0
  59. package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
  60. package/src/components/ui/action-menu/index.tsx +152 -0
  61. package/src/components/ui/action-menu/structure-tools.tsx +100 -0
  62. package/src/components/ui/action-menu/view-toggles.tsx +397 -0
  63. package/src/components/ui/command-palette/editor-commands.tsx +396 -0
  64. package/src/components/ui/command-palette/index.tsx +730 -0
  65. package/src/components/ui/controls/action-button.tsx +33 -0
  66. package/src/components/ui/controls/material-picker.tsx +194 -0
  67. package/src/components/ui/controls/metric-control.tsx +262 -0
  68. package/src/components/ui/controls/panel-section.tsx +65 -0
  69. package/src/components/ui/controls/segmented-control.tsx +45 -0
  70. package/src/components/ui/controls/slider-control.tsx +245 -0
  71. package/src/components/ui/controls/toggle-control.tsx +38 -0
  72. package/src/components/ui/floating-level-selector.tsx +355 -0
  73. package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
  74. package/src/components/ui/helpers/helper-manager.tsx +33 -0
  75. package/src/components/ui/helpers/item-helper.tsx +40 -0
  76. package/src/components/ui/helpers/roof-helper.tsx +16 -0
  77. package/src/components/ui/helpers/slab-helper.tsx +20 -0
  78. package/src/components/ui/helpers/wall-helper.tsx +20 -0
  79. package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
  80. package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
  81. package/src/components/ui/panels/ceiling-panel.tsx +230 -0
  82. package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
  83. package/src/components/ui/panels/door-panel.tsx +600 -0
  84. package/src/components/ui/panels/item-panel.tsx +306 -0
  85. package/src/components/ui/panels/panel-manager.tsx +59 -0
  86. package/src/components/ui/panels/panel-wrapper.tsx +80 -0
  87. package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
  88. package/src/components/ui/panels/reference-panel.tsx +177 -0
  89. package/src/components/ui/panels/roof-panel.tsx +262 -0
  90. package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
  91. package/src/components/ui/panels/slab-panel.tsx +228 -0
  92. package/src/components/ui/panels/stair-panel.tsx +304 -0
  93. package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
  94. package/src/components/ui/panels/wall-panel.tsx +123 -0
  95. package/src/components/ui/panels/window-panel.tsx +441 -0
  96. package/src/components/ui/primitives/button.tsx +69 -0
  97. package/src/components/ui/primitives/card.tsx +75 -0
  98. package/src/components/ui/primitives/color-dot.tsx +61 -0
  99. package/src/components/ui/primitives/context-menu.tsx +227 -0
  100. package/src/components/ui/primitives/dialog.tsx +129 -0
  101. package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
  102. package/src/components/ui/primitives/error-boundary.tsx +52 -0
  103. package/src/components/ui/primitives/input.tsx +21 -0
  104. package/src/components/ui/primitives/number-input.tsx +187 -0
  105. package/src/components/ui/primitives/opacity-control.tsx +79 -0
  106. package/src/components/ui/primitives/popover.tsx +42 -0
  107. package/src/components/ui/primitives/separator.tsx +28 -0
  108. package/src/components/ui/primitives/sheet.tsx +130 -0
  109. package/src/components/ui/primitives/shortcut-token.tsx +64 -0
  110. package/src/components/ui/primitives/sidebar.tsx +855 -0
  111. package/src/components/ui/primitives/skeleton.tsx +13 -0
  112. package/src/components/ui/primitives/slider.tsx +58 -0
  113. package/src/components/ui/primitives/switch.tsx +29 -0
  114. package/src/components/ui/primitives/tooltip.tsx +57 -0
  115. package/src/components/ui/scene-loader.tsx +40 -0
  116. package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
  117. package/src/components/ui/sidebar/icon-rail.tsx +147 -0
  118. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
  119. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
  120. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
  121. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
  122. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
  123. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
  125. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
  126. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
  127. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
  128. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
  130. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
  131. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
  133. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
  134. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
  135. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
  136. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
  137. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
  138. package/src/components/ui/sidebar/tab-bar.tsx +39 -0
  139. package/src/components/ui/slider-demo.tsx +36 -0
  140. package/src/components/ui/slider.tsx +81 -0
  141. package/src/components/ui/viewer-toolbar.tsx +342 -0
  142. package/src/components/viewer-overlay.tsx +499 -0
  143. package/src/components/viewer-zone-system.tsx +48 -0
  144. package/src/contexts/presets-context.tsx +121 -0
  145. package/src/hooks/use-auto-save.ts +194 -0
  146. package/src/hooks/use-contextual-tools.ts +52 -0
  147. package/src/hooks/use-grid-events.ts +106 -0
  148. package/src/hooks/use-keyboard.ts +214 -0
  149. package/src/hooks/use-mobile.ts +19 -0
  150. package/src/hooks/use-reduced-motion.ts +20 -0
  151. package/src/index.tsx +33 -0
  152. package/src/lib/constants.ts +3 -0
  153. package/src/lib/level-selection.ts +31 -0
  154. package/src/lib/scene.ts +394 -0
  155. package/src/lib/sfx/index.ts +2 -0
  156. package/src/lib/sfx-bus.ts +49 -0
  157. package/src/lib/sfx-player.ts +60 -0
  158. package/src/lib/utils.ts +43 -0
  159. package/src/store/use-audio.tsx +45 -0
  160. package/src/store/use-command-registry.ts +36 -0
  161. package/src/store/use-editor.tsx +522 -0
  162. package/src/store/use-palette-view-registry.ts +45 -0
  163. package/src/store/use-upload.ts +90 -0
  164. package/src/three-types.ts +3 -0
  165. 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
+ }