@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.
Files changed (157) hide show
  1. package/package.json +13 -9
  2. package/src/components/editor/bottom-sheet.tsx +149 -0
  3. package/src/components/editor/custom-camera-controls.tsx +74 -5
  4. package/src/components/editor/editor-layout-mobile.tsx +264 -0
  5. package/src/components/editor/editor-layout-v2.tsx +24 -3
  6. package/src/components/editor/first-person/build-collider-world.ts +363 -0
  7. package/src/components/editor/first-person/bvh-ecctrl.tsx +860 -0
  8. package/src/components/editor/first-person-controls.tsx +496 -143
  9. package/src/components/editor/floating-action-menu.tsx +32 -55
  10. package/src/components/editor/floorplan-background-selection.ts +113 -0
  11. package/src/components/editor/floorplan-panel.tsx +9861 -3297
  12. package/src/components/editor/index.tsx +295 -32
  13. package/src/components/editor/selection-manager.tsx +575 -13
  14. package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
  15. package/src/components/editor/thumbnail-generator.tsx +56 -68
  16. package/src/components/editor/use-floorplan-background-placement.ts +257 -0
  17. package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
  18. package/src/components/editor/use-floorplan-scene-data.ts +189 -0
  19. package/src/components/editor/wall-measurement-label.tsx +267 -36
  20. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
  21. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
  22. package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
  23. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +124 -0
  24. package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
  25. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +202 -0
  26. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
  27. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
  28. package/src/components/editor-2d/svg-paths.ts +119 -0
  29. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +10 -12
  30. package/src/components/systems/roof/roof-edit-system.tsx +1 -1
  31. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  32. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  33. package/src/components/systems/zone/zone-system.tsx +0 -0
  34. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
  35. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
  36. package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
  37. package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
  38. package/src/components/tools/column/column-tool.tsx +97 -0
  39. package/src/components/tools/column/move-column-tool.tsx +105 -0
  40. package/src/components/tools/door/door-tool.tsx +7 -0
  41. package/src/components/tools/door/move-door-tool.tsx +28 -8
  42. package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
  43. package/src/components/tools/fence/fence-drafting.ts +10 -3
  44. package/src/components/tools/fence/fence-tool.tsx +160 -4
  45. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +139 -25
  46. package/src/components/tools/fence/move-fence-tool.tsx +111 -40
  47. package/src/components/tools/item/move-tool.tsx +7 -1
  48. package/src/components/tools/item/placement-math.ts +32 -5
  49. package/src/components/tools/item/placement-strategies.ts +110 -31
  50. package/src/components/tools/item/placement-types.ts +7 -0
  51. package/src/components/tools/item/use-draft-node.ts +1 -0
  52. package/src/components/tools/item/use-placement-coordinator.tsx +558 -52
  53. package/src/components/tools/roof/move-roof-tool.tsx +29 -17
  54. package/src/components/tools/select/box-select-tool.tsx +12 -17
  55. package/src/components/tools/shared/polygon-editor.tsx +153 -28
  56. package/src/components/tools/shared/segment-angle.ts +156 -0
  57. package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
  58. package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
  59. package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
  60. package/src/components/tools/spawn/spawn-tool.tsx +130 -0
  61. package/src/components/tools/tool-manager.tsx +20 -5
  62. package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
  63. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +131 -27
  64. package/src/components/tools/wall/move-wall-tool.tsx +6 -4
  65. package/src/components/tools/wall/wall-drafting.ts +18 -9
  66. package/src/components/tools/wall/wall-tool.tsx +136 -4
  67. package/src/components/tools/window/move-window-tool.tsx +18 -0
  68. package/src/components/tools/window/window-tool.tsx +5 -0
  69. package/src/components/tools/zone/zone-tool.tsx +20 -5
  70. package/src/components/ui/action-menu/camera-actions.tsx +37 -33
  71. package/src/components/ui/action-menu/control-modes.tsx +34 -1
  72. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  73. package/src/components/ui/action-menu/index.tsx +98 -59
  74. package/src/components/ui/action-menu/structure-tools.tsx +2 -0
  75. package/src/components/ui/action-menu/view-toggles.tsx +418 -41
  76. package/src/components/ui/command-palette/editor-commands.tsx +24 -5
  77. package/src/components/ui/command-palette/index.tsx +4 -255
  78. package/src/components/ui/controls/material-picker.tsx +154 -164
  79. package/src/components/ui/controls/slider-control.tsx +66 -18
  80. package/src/components/ui/floating-level-selector.tsx +286 -55
  81. package/src/components/ui/helpers/helper-manager.tsx +10 -0
  82. package/src/components/ui/item-catalog/catalog-items.tsx +2563 -1239
  83. package/src/components/ui/item-catalog/item-catalog.tsx +96 -187
  84. package/src/components/ui/level-duplicate-dialog.tsx +113 -0
  85. package/src/components/ui/panels/ceiling-panel.tsx +3 -28
  86. package/src/components/ui/panels/column-panel.tsx +759 -0
  87. package/src/components/ui/panels/door-panel.tsx +989 -290
  88. package/src/components/ui/panels/fence-panel.tsx +2 -49
  89. package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
  90. package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
  91. package/src/components/ui/panels/node-display.ts +39 -0
  92. package/src/components/ui/panels/paint-panel.tsx +163 -0
  93. package/src/components/ui/panels/panel-manager.tsx +208 -28
  94. package/src/components/ui/panels/panel-wrapper.tsx +48 -39
  95. package/src/components/ui/panels/reference-panel.tsx +253 -5
  96. package/src/components/ui/panels/roof-panel.tsx +13 -64
  97. package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
  98. package/src/components/ui/panels/slab-panel.tsx +4 -30
  99. package/src/components/ui/panels/spawn-panel.tsx +161 -0
  100. package/src/components/ui/panels/stair-panel.tsx +20 -74
  101. package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
  102. package/src/components/ui/panels/wall-panel.tsx +10 -8
  103. package/src/components/ui/panels/window-panel.tsx +668 -139
  104. package/src/components/ui/primitives/number-input.tsx +1 -1
  105. package/src/components/ui/primitives/sidebar.tsx +0 -0
  106. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  107. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  108. package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
  109. package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
  110. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -0
  111. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
  112. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -2
  113. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
  114. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
  115. package/src/components/ui/sidebar/panels/site-panel/index.tsx +105 -22
  116. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
  117. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
  118. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +76 -0
  119. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +11 -3
  120. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +10 -5
  121. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
  122. package/src/components/ui/sidebar/tab-bar.tsx +3 -0
  123. package/src/components/ui/slider.tsx +1 -1
  124. package/src/components/viewer-overlay.tsx +0 -0
  125. package/src/components/viewer-zone-system.tsx +0 -0
  126. package/src/hooks/use-auto-frame.ts +45 -0
  127. package/src/hooks/use-auto-save.ts +14 -0
  128. package/src/hooks/use-keyboard.ts +74 -7
  129. package/src/hooks/use-mobile.ts +12 -12
  130. package/src/index.tsx +8 -1
  131. package/src/lib/door-interaction.ts +88 -0
  132. package/src/lib/floorplan/geometry.ts +263 -0
  133. package/src/lib/floorplan/index.ts +38 -0
  134. package/src/lib/floorplan/items.ts +179 -0
  135. package/src/lib/floorplan/selection-tool.ts +231 -0
  136. package/src/lib/floorplan/stairs.ts +478 -0
  137. package/src/lib/floorplan/types.ts +57 -0
  138. package/src/lib/floorplan/walls.ts +23 -0
  139. package/src/lib/guide-events.ts +10 -0
  140. package/src/lib/level-duplication.test.ts +70 -0
  141. package/src/lib/level-duplication.ts +153 -0
  142. package/src/lib/local-guide-image.ts +42 -0
  143. package/src/lib/material-paint.ts +284 -0
  144. package/src/lib/roof-duplication.ts +214 -0
  145. package/src/lib/scene-bounds.test.ts +183 -0
  146. package/src/lib/scene-bounds.ts +169 -0
  147. package/src/lib/scene.ts +0 -0
  148. package/src/lib/sfx-bus.ts +2 -0
  149. package/src/lib/sfx-player.ts +5 -5
  150. package/src/lib/stair-duplication.ts +126 -0
  151. package/src/lib/window-interaction.ts +86 -0
  152. package/src/store/use-editor.tsx +186 -62
  153. package/tsconfig.json +2 -1
  154. package/src/components/feedback-dialog.tsx +0 -265
  155. package/src/components/pascal-radio.tsx +0 -280
  156. package/src/components/preview-button.tsx +0 -16
  157. package/src/components/ui/viewer-toolbar.tsx +0 -395
@@ -4,12 +4,14 @@ import {
4
4
  type AnyNode,
5
5
  type AnyNodeId,
6
6
  type CeilingNode,
7
+ ColumnNode,
7
8
  DoorNode,
8
9
  FenceNode,
10
+ generateId,
9
11
  ItemNode,
10
- RoofNode,
11
12
  RoofSegmentNode,
12
13
  type SlabNode,
14
+ SpawnNode,
13
15
  StairNode,
14
16
  StairSegmentNode,
15
17
  sceneRegistry,
@@ -23,7 +25,9 @@ import { useFrame } from '@react-three/fiber'
23
25
  import { Move } from 'lucide-react'
24
26
  import { useCallback, useEffect, useRef, useState } from 'react'
25
27
  import * as THREE from 'three'
28
+ import { duplicateRoofSubtree } from '../../lib/roof-duplication'
26
29
  import { sfxEmitter } from '../../lib/sfx-bus'
30
+ import { duplicateStairSubtree } from '../../lib/stair-duplication'
27
31
  import useEditor from '../../store/use-editor'
28
32
  import { NodeActionMenu } from './node-action-menu'
29
33
 
@@ -37,8 +41,10 @@ const ALLOWED_TYPES = [
37
41
  'stair-segment',
38
42
  'wall',
39
43
  'fence',
44
+ 'column',
40
45
  'slab',
41
46
  'ceiling',
47
+ 'spawn',
42
48
  ]
43
49
  const DELETE_ONLY_TYPES: string[] = []
44
50
  const HOLE_TYPES = ['slab', 'ceiling']
@@ -143,10 +149,7 @@ export function FloatingActionMenu() {
143
149
  node.type === 'wall'
144
150
  ? obj.localToWorld(
145
151
  new THREE.Vector3(
146
- Math.hypot(
147
- segment.end[0] - segment.start[0],
148
- segment.end[1] - segment.start[1],
149
- ),
152
+ Math.hypot(segment.end[0] - segment.start[0], segment.end[1] - segment.start[1]),
150
153
  0,
151
154
  0,
152
155
  ),
@@ -182,8 +185,10 @@ export function FloatingActionMenu() {
182
185
  node.type === 'door' ||
183
186
  node.type === 'wall' ||
184
187
  node.type === 'fence' ||
188
+ node.type === 'column' ||
185
189
  node.type === 'slab' ||
186
190
  node.type === 'ceiling' ||
191
+ node.type === 'spawn' ||
187
192
  node.type === 'roof' ||
188
193
  node.type === 'roof-segment' ||
189
194
  node.type === 'stair' ||
@@ -234,6 +239,16 @@ export function FloatingActionMenu() {
234
239
  e.stopPropagation()
235
240
  if (!node?.parentId) return
236
241
  sfxEmitter.emit('sfx:item-pick')
242
+
243
+ if (node.type === 'roof') {
244
+ try {
245
+ duplicateRoofSubtree(node.id as AnyNodeId, { mode: 'move' })
246
+ } catch (error) {
247
+ console.error('Failed to duplicate roof', error)
248
+ }
249
+ return
250
+ }
251
+
237
252
  useScene.temporal.getState().pause()
238
253
 
239
254
  let duplicateInfo = structuredClone(node) as any
@@ -248,16 +263,16 @@ export function FloatingActionMenu() {
248
263
  duplicate = WindowNode.parse(duplicateInfo)
249
264
  } else if (node.type === 'item') {
250
265
  duplicate = ItemNode.parse(duplicateInfo)
266
+ } else if (node.type === 'column') {
267
+ duplicate = ColumnNode.parse(duplicateInfo)
251
268
  } else if (node.type === 'wall') {
252
269
  duplicate = WallNode.parse(duplicateInfo)
253
270
  } else if (node.type === 'fence') {
254
271
  duplicate = FenceNode.parse(duplicateInfo)
255
272
  duplicate.start = [duplicate.start[0] + 1, duplicate.start[1] + 1]
256
273
  duplicate.end = [duplicate.end[0] + 1, duplicate.end[1] + 1]
257
- } else if (node.type === 'roof') {
258
- duplicateInfo.children = []
259
- duplicate = RoofNode.parse(duplicateInfo)
260
274
  } else if (node.type === 'roof-segment') {
275
+ duplicateInfo.id = generateId('rseg')
261
276
  duplicate = RoofSegmentNode.parse(duplicateInfo)
262
277
  } else if (node.type === 'stair') {
263
278
  duplicateInfo.children = []
@@ -266,6 +281,8 @@ export function FloatingActionMenu() {
266
281
  duplicate = StairNode.parse(duplicateInfo)
267
282
  } else if (node.type === 'stair-segment') {
268
283
  duplicate = StairSegmentNode.parse(duplicateInfo)
284
+ } else if (node.type === 'spawn') {
285
+ duplicate = SpawnNode.parse(duplicateInfo)
269
286
  }
270
287
  } catch (error) {
271
288
  console.error('Failed to parse duplicate', error)
@@ -286,7 +303,6 @@ export function FloatingActionMenu() {
286
303
  } else if (duplicate.type === 'fence') {
287
304
  useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
288
305
  } else if (
289
- duplicate.type === 'roof' ||
290
306
  duplicate.type === 'roof-segment' ||
291
307
  duplicate.type === 'stair' ||
292
308
  duplicate.type === 'stair-segment'
@@ -300,64 +316,22 @@ export function FloatingActionMenu() {
300
316
  ]
301
317
  }
302
318
  if (node.type === 'stair' && duplicate.type === 'stair') {
303
- const nodesState = useScene.getState().nodes
304
- const createOps: { node: AnyNode; parentId?: AnyNodeId }[] = [
305
- { node: duplicate, parentId: duplicate.parentId as AnyNodeId },
306
- ]
307
-
308
- for (const childId of node.children ?? []) {
309
- const childNode = nodesState[childId]
310
- if (childNode?.type !== 'stair-segment') {
311
- continue
312
- }
313
-
314
- let childDuplicateInfo = structuredClone(childNode) as any
315
- delete childDuplicateInfo.id
316
- childDuplicateInfo.metadata = { ...childDuplicateInfo.metadata }
317
- delete childDuplicateInfo.metadata?.isNew
318
-
319
- try {
320
- const childDuplicate = StairSegmentNode.parse(childDuplicateInfo)
321
- createOps.push({ node: childDuplicate, parentId: duplicate.id as AnyNodeId })
322
- } catch (e) {
323
- console.error('Failed to duplicate stair segment', e)
324
- }
325
- }
326
-
327
- useScene.getState().createNodes(createOps)
319
+ duplicateStairSubtree(node.id as AnyNodeId, { mode: 'move' })
328
320
  } else {
329
321
  useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
330
322
  }
331
323
 
332
- // Duplicate children for roof nodes
333
- if (node.type === 'roof' && node.children) {
334
- const nodesState = useScene.getState().nodes
335
- for (const childId of node.children) {
336
- const childNode = nodesState[childId]
337
- if (childNode && childNode.type === 'roof-segment') {
338
- let childDuplicateInfo = structuredClone(childNode) as any
339
- delete childDuplicateInfo.id
340
- childDuplicateInfo.metadata = { ...childDuplicateInfo.metadata, isNew: true }
341
- try {
342
- const childDuplicate = RoofSegmentNode.parse(childDuplicateInfo)
343
- useScene.getState().createNode(childDuplicate, duplicate.id as AnyNodeId)
344
- } catch (e) {
345
- console.error('Failed to duplicate roof segment', e)
346
- }
347
- }
348
- }
349
- }
350
-
351
324
  // Duplicate children for stair nodes
352
325
  }
353
326
  if (
354
327
  duplicate.type === 'item' ||
328
+ duplicate.type === 'column' ||
355
329
  duplicate.type === 'wall' ||
356
330
  duplicate.type === 'fence' ||
357
331
  duplicate.type === 'window' ||
358
332
  duplicate.type === 'door' ||
359
- duplicate.type === 'roof' ||
360
333
  duplicate.type === 'roof-segment' ||
334
+ duplicate.type === 'spawn' ||
361
335
  duplicate.type === 'stair-segment'
362
336
  ) {
363
337
  setMovingNode(duplicate as any)
@@ -453,7 +427,10 @@ export function FloatingActionMenu() {
453
427
  }
454
428
  onDelete={handleDelete}
455
429
  onDuplicate={
456
- node && !DELETE_ONLY_TYPES.includes(node.type) && !HOLE_TYPES.includes(node.type)
430
+ node &&
431
+ node.type !== 'spawn' &&
432
+ !DELETE_ONLY_TYPES.includes(node.type) &&
433
+ !HOLE_TYPES.includes(node.type)
457
434
  ? handleDuplicate
458
435
  : undefined
459
436
  }
@@ -0,0 +1,113 @@
1
+ 'use client'
2
+
3
+ import type { Point2D, ZoneNode as ZoneNodeType } from '@pascal-app/core'
4
+ import { isPointInsidePolygon } from '../../lib/floorplan'
5
+ import type { WallPlanPoint } from '../tools/wall/wall-drafting'
6
+
7
+ type ModifierKeys = {
8
+ meta: boolean
9
+ ctrl: boolean
10
+ }
11
+
12
+ type ZoneHitEntry = {
13
+ zone: {
14
+ id: ZoneNodeType['id']
15
+ }
16
+ polygon: Point2D[]
17
+ }
18
+
19
+ type ResolveFloorplanBackgroundSelectionArgs = {
20
+ canSelectElementFloorplanGeometry: boolean
21
+ canSelectFloorplanZones: boolean
22
+ currentSelectedIds: string[]
23
+ getFloorplanHitIdAtPoint: (planPoint: WallPlanPoint) => string | null
24
+ isWallBuildActive: boolean
25
+ modifierKeys: ModifierKeys
26
+ planPoint: WallPlanPoint
27
+ structureLayer: string
28
+ toPoint2D: (point: WallPlanPoint) => Point2D
29
+ visibleZonePolygons: ZoneHitEntry[]
30
+ }
31
+
32
+ export type FloorplanBackgroundSelectionResult =
33
+ | {
34
+ handled: true
35
+ kind: 'select-zone'
36
+ zoneId: ZoneNodeType['id']
37
+ }
38
+ | {
39
+ handled: true
40
+ kind: 'select-elements'
41
+ selectedIds: string[]
42
+ }
43
+ | {
44
+ handled: true
45
+ kind: 'clear-zones'
46
+ }
47
+ | {
48
+ handled: true
49
+ kind: 'clear-elements'
50
+ preserveSelection: boolean
51
+ }
52
+ | {
53
+ handled: false
54
+ }
55
+
56
+ export function resolveFloorplanBackgroundSelection({
57
+ canSelectElementFloorplanGeometry,
58
+ canSelectFloorplanZones,
59
+ currentSelectedIds,
60
+ getFloorplanHitIdAtPoint,
61
+ isWallBuildActive,
62
+ modifierKeys,
63
+ planPoint,
64
+ structureLayer,
65
+ toPoint2D,
66
+ visibleZonePolygons,
67
+ }: ResolveFloorplanBackgroundSelectionArgs): FloorplanBackgroundSelectionResult {
68
+ if (canSelectFloorplanZones) {
69
+ const zoneHit = visibleZonePolygons.find(({ polygon }) =>
70
+ isPointInsidePolygon(toPoint2D(planPoint), polygon),
71
+ )
72
+ if (zoneHit) {
73
+ return {
74
+ handled: true,
75
+ kind: 'select-zone',
76
+ zoneId: zoneHit.zone.id,
77
+ }
78
+ }
79
+ }
80
+
81
+ if (canSelectElementFloorplanGeometry) {
82
+ const hitId = getFloorplanHitIdAtPoint(planPoint)
83
+ if (hitId) {
84
+ return {
85
+ handled: true,
86
+ kind: 'select-elements',
87
+ selectedIds:
88
+ modifierKeys.meta || modifierKeys.ctrl
89
+ ? currentSelectedIds.includes(hitId)
90
+ ? currentSelectedIds.filter((selectedId) => selectedId !== hitId)
91
+ : [...currentSelectedIds, hitId]
92
+ : [hitId],
93
+ }
94
+ }
95
+ }
96
+
97
+ if (!isWallBuildActive) {
98
+ if (structureLayer === 'zones') {
99
+ return {
100
+ handled: true,
101
+ kind: 'clear-zones',
102
+ }
103
+ }
104
+
105
+ return {
106
+ handled: true,
107
+ kind: 'clear-elements',
108
+ preserveSelection: modifierKeys.meta || modifierKeys.ctrl,
109
+ }
110
+ }
111
+
112
+ return { handled: false }
113
+ }