@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,90 @@
1
+ import type {
2
+ DoorNode,
3
+ ItemNode,
4
+ RoofNode,
5
+ RoofSegmentNode,
6
+ StairNode,
7
+ StairSegmentNode,
8
+ WindowNode,
9
+ } from '@pascal-app/core'
10
+ import { Vector3 } from 'three'
11
+ import { sfxEmitter } from '../../../lib/sfx-bus'
12
+ import useEditor from '../../../store/use-editor'
13
+ import { MoveDoorTool } from '../door/move-door-tool'
14
+ import { MoveRoofTool } from '../roof/move-roof-tool'
15
+ import { MoveWindowTool } from '../window/move-window-tool'
16
+ import type { PlacementState } from './placement-types'
17
+ import { useDraftNode } from './use-draft-node'
18
+ import { usePlacementCoordinator } from './use-placement-coordinator'
19
+
20
+ function getInitialState(node: {
21
+ asset: { attachTo?: string }
22
+ parentId: string | null
23
+ }): PlacementState {
24
+ const attachTo = node.asset.attachTo
25
+ if (attachTo === 'wall' || attachTo === 'wall-side') {
26
+ return { surface: 'wall', wallId: node.parentId, ceilingId: null, surfaceItemId: null }
27
+ }
28
+ if (attachTo === 'ceiling') {
29
+ return { surface: 'ceiling', wallId: null, ceilingId: node.parentId, surfaceItemId: null }
30
+ }
31
+ return { surface: 'floor', wallId: null, ceilingId: null, surfaceItemId: null }
32
+ }
33
+
34
+ function MoveItemContent({ movingNode }: { movingNode: ItemNode }) {
35
+ const draftNode = useDraftNode()
36
+
37
+ const meta =
38
+ typeof movingNode.metadata === 'object' && movingNode.metadata !== null
39
+ ? (movingNode.metadata as Record<string, unknown>)
40
+ : {}
41
+ const isNew = !!meta.isNew
42
+
43
+ const cursor = usePlacementCoordinator({
44
+ asset: movingNode.asset,
45
+ draftNode,
46
+ // Duplicates start fresh in floor mode; wall/ceiling draft is created lazily by ensureDraft
47
+ initialState: isNew
48
+ ? { surface: 'floor', wallId: null, ceilingId: null, surfaceItemId: null }
49
+ : getInitialState(movingNode),
50
+ // Preserve the original item's scale so Y-position calculations use the correct height
51
+ defaultScale: isNew ? movingNode.scale : undefined,
52
+ initDraft: (gridPosition) => {
53
+ if (isNew) {
54
+ // Duplicate: use the same create() path as ItemTool so ghost rendering works correctly.
55
+ // Floor items get a draft immediately; wall/ceiling items are created lazily on surface entry.
56
+ gridPosition.copy(new Vector3(...movingNode.position))
57
+ if (!movingNode.asset.attachTo) {
58
+ draftNode.create(gridPosition, movingNode.asset, movingNode.rotation, movingNode.scale)
59
+ }
60
+ } else {
61
+ draftNode.adopt(movingNode)
62
+ gridPosition.copy(new Vector3(...movingNode.position))
63
+ }
64
+ },
65
+ onCommitted: () => {
66
+ sfxEmitter.emit('sfx:item-place')
67
+ useEditor.getState().setMovingNode(null)
68
+ return false
69
+ },
70
+ onCancel: () => {
71
+ draftNode.destroy()
72
+ useEditor.getState().setMovingNode(null)
73
+ },
74
+ })
75
+
76
+ return <>{cursor}</>
77
+ }
78
+
79
+ export const MoveTool: React.FC = () => {
80
+ const movingNode = useEditor((state) => state.movingNode)
81
+
82
+ if (!movingNode) return null
83
+ if (movingNode.type === 'door') return <MoveDoorTool node={movingNode as DoorNode} />
84
+ if (movingNode.type === 'window') return <MoveWindowTool node={movingNode as WindowNode} />
85
+ if (movingNode.type === 'roof' || movingNode.type === 'roof-segment')
86
+ return <MoveRoofTool node={movingNode as RoofNode | RoofSegmentNode} />
87
+ if (movingNode.type === 'stair' || movingNode.type === 'stair-segment')
88
+ return <MoveRoofTool node={movingNode as StairNode | StairSegmentNode} />
89
+ return <MoveItemContent movingNode={movingNode as ItemNode} />
90
+ }
@@ -0,0 +1,85 @@
1
+ import { isObject } from '@pascal-app/core'
2
+
3
+ /**
4
+ * Snaps a position to 0.5 grid, with an offset to align item edges to grid lines.
5
+ * For items with dimensions like 2.5, the center would be at 1.25 from the edge,
6
+ * which doesn't align with 0.5 grid. This adds an offset so edges align instead.
7
+ */
8
+ export function snapToGrid(position: number, dimension: number): number {
9
+ const halfDim = dimension / 2
10
+ const needsOffset = Math.abs(((halfDim * 2) % 1) - 0.5) < 0.01
11
+ const offset = needsOffset ? 0.25 : 0
12
+ return Math.round((position - offset) * 2) / 2 + offset
13
+ }
14
+
15
+ /**
16
+ * Snap a value to 0.5 increments (used for wall-local positions).
17
+ */
18
+ export function snapToHalf(value: number): number {
19
+ return Math.round(value * 2) / 2
20
+ }
21
+
22
+ /**
23
+ * Calculate cursor rotation in WORLD space from wall normal and orientation.
24
+ */
25
+ export function calculateCursorRotation(
26
+ normal: [number, number, number] | undefined,
27
+ wallStart: [number, number],
28
+ wallEnd: [number, number],
29
+ ): number {
30
+ if (!normal) return 0
31
+
32
+ // Wall direction angle in world XZ plane
33
+ const wallAngle = Math.atan2(wallEnd[1] - wallStart[1], wallEnd[0] - wallStart[0])
34
+
35
+ // In local wall space, front face has normal.z < 0, back face has normal.z > 0
36
+ if (normal[2] < 0) {
37
+ return -wallAngle
38
+ }
39
+ return Math.PI - wallAngle
40
+ }
41
+
42
+ /**
43
+ * Calculate item rotation in WALL-LOCAL space from normal.
44
+ * Items are children of the wall mesh, so their rotation is relative to wall's local space.
45
+ */
46
+ export function calculateItemRotation(normal: [number, number, number] | undefined): number {
47
+ if (!normal) return 0
48
+
49
+ return normal[2] > 0 ? 0 : Math.PI
50
+ }
51
+
52
+ /**
53
+ * Determine which side of the wall based on the normal vector.
54
+ * In wall-local space, the wall runs along X-axis, so the normal points along Z-axis.
55
+ * Positive Z normal = 'front', Negative Z normal = 'back'
56
+ */
57
+ export function getSideFromNormal(normal: [number, number, number] | undefined): 'front' | 'back' {
58
+ if (!normal) return 'front'
59
+ return normal[2] >= 0 ? 'front' : 'back'
60
+ }
61
+
62
+ /**
63
+ * Check if the normal indicates a valid wall side face (front or back).
64
+ * Filters out top face and thickness edges.
65
+ *
66
+ * In wall-local geometry space (after ExtrudeGeometry + rotateX):
67
+ * - X axis: along wall direction
68
+ * - Y axis: up (height)
69
+ * - Z axis: perpendicular to wall (thickness direction)
70
+ *
71
+ * So valid side faces have normals pointing in ±Z direction (local space).
72
+ */
73
+ export function isValidWallSideFace(normal: [number, number, number] | undefined): boolean {
74
+ if (!normal) return false
75
+ return Math.abs(normal[2]) > 0.7
76
+ }
77
+
78
+ /**
79
+ * Strip the `isTransient` flag from node metadata before committing.
80
+ */
81
+ export function stripTransient(meta: any): any {
82
+ if (!isObject(meta)) return meta
83
+ const { isTransient, ...rest } = meta as Record<string, any>
84
+ return rest
85
+ }