@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
@@ -1,23 +1,84 @@
1
1
  'use client'
2
2
 
3
+ import { useScene } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
3
5
  import { AnimatePresence, motion } from 'motion/react'
6
+ import { useEffect, useMemo } from 'react'
7
+ import { MaterialPicker } from './../../../components/ui/controls/material-picker'
4
8
  import { TooltipProvider } from './../../../components/ui/primitives/tooltip'
9
+ import { useIsMobile } from './../../../hooks/use-mobile'
5
10
  import { useReducedMotion } from './../../../hooks/use-reduced-motion'
11
+ import { resolvePaintTargetFromSelection } from './../../../lib/material-paint'
6
12
  import { cn } from './../../../lib/utils'
7
13
  import useEditor from './../../../store/use-editor'
8
- import { ItemCatalog } from '../item-catalog/item-catalog'
9
14
  import { CameraActions } from './camera-actions'
10
15
  import { ControlModes } from './control-modes'
11
- import { FurnishTools } from './furnish-tools'
12
16
  import { StructureTools } from './structure-tools'
13
- import { ViewToggles } from './view-toggles'
17
+ import { GridSnapControl, SecondaryToggles } from './view-toggles'
18
+
19
+ // Mobile bottom offset matches the viewer's overlap behind the sheet's
20
+ // rounded corners (SHEET_OVERLAP_PX in editor-layout-mobile) so the menu sits
21
+ // just above that strip instead of inside it.
22
+ const MOBILE_BOTTOM_OFFSET = 24
23
+
24
+ function PaintMaterialTray() {
25
+ const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
26
+ const activePaintTarget = useEditor((state) => state.activePaintTarget)
27
+ const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial)
28
+ const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget)
29
+ const selectedIds = useViewer((state) => state.selection.selectedIds)
30
+ const nodes = useScene((state) => state.nodes)
31
+ const selectedId = selectedIds.length === 1 ? (selectedIds[0] ?? null) : null
32
+
33
+ useEffect(() => {
34
+ const selectedPaintTarget = resolvePaintTargetFromSelection({
35
+ nodes,
36
+ selectedId,
37
+ })
38
+
39
+ if (selectedPaintTarget) {
40
+ setActivePaintTarget(selectedPaintTarget)
41
+ }
42
+ }, [nodes, selectedId, setActivePaintTarget])
43
+
44
+ return (
45
+ <div className="w-[42rem] max-w-[calc(100vw-2rem)]">
46
+ <MaterialPicker
47
+ onChange={(material) => {
48
+ setActivePaintMaterial({ material, sourceTarget: activePaintTarget })
49
+ }}
50
+ onSelectMaterialPreset={(materialPreset) => {
51
+ setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
52
+ }}
53
+ selectedMaterialPreset={activePaintMaterial?.materialPreset}
54
+ value={activePaintMaterial?.material}
55
+ />
56
+ </div>
57
+ )
58
+ }
14
59
 
15
60
  export function ActionMenu({ className }: { className?: string }) {
16
61
  const phase = useEditor((state) => state.phase)
17
62
  const mode = useEditor((state) => state.mode)
18
63
  const tool = useEditor((state) => state.tool)
19
64
  const catalogCategory = useEditor((state) => state.catalogCategory)
65
+ const isMobile = useIsMobile()
66
+ const hasSelectionOnMobile = useViewer((s) => isMobile && s.selection.selectedIds.length > 0)
67
+ const hasReferenceOnMobile = useEditor((s) => isMobile && Boolean(s.selectedReferenceId))
68
+ const CONTEXTUAL_TABS = new Set(['ai', 'items', 'studio'])
69
+ const isContextualPanelOnMobile = useEditor(
70
+ (s) => isMobile && CONTEXTUAL_TABS.has(s.activeSidebarPanel),
71
+ )
20
72
  const reducedMotion = useReducedMotion()
73
+ const showPaintTray = useMemo(() => mode === 'material-paint', [mode])
74
+
75
+ // On mobile, defer the bottom rail to the selection bar when something
76
+ // is selected — the contextual actions take priority over mode controls.
77
+ // Also hide on Chat / Items / Studio tabs; those are contextual workflows
78
+ // (composing / picking furniture / generating renders) where the build
79
+ // menu is irrelevant.
80
+ if (hasSelectionOnMobile || hasReferenceOnMobile || isContextualPanelOnMobile) return null
81
+
21
82
  const transition = reducedMotion
22
83
  ? { duration: 0 }
23
84
  : { type: 'spring' as const, bounce: 0.2, duration: 0.4 }
@@ -26,49 +87,19 @@ export function ActionMenu({ className }: { className?: string }) {
26
87
  <TooltipProvider>
27
88
  <motion.div
28
89
  className={cn(
29
- 'fixed bottom-6 left-1/2 z-50 -translate-x-1/2',
90
+ 'left-1/2 z-50 -translate-x-1/2',
91
+ isMobile ? 'absolute origin-bottom scale-90' : 'fixed bottom-6',
30
92
  'rounded-2xl border border-border bg-background/90 shadow-2xl backdrop-blur-md',
31
93
  'transition-colors duration-200 ease-out',
32
94
  className,
33
95
  )}
34
96
  layout
97
+ style={isMobile ? { bottom: MOBILE_BOTTOM_OFFSET } : undefined}
35
98
  transition={transition}
36
99
  >
37
- {/* Item Catalog Row - Only show when in build mode with item tool */}
38
- <AnimatePresence>
39
- {mode === 'build' && tool === 'item' && catalogCategory && (
40
- <motion.div
41
- animate={{
42
- opacity: 1,
43
- maxHeight: 160,
44
- paddingTop: 8,
45
- paddingBottom: 8,
46
- borderBottomWidth: 1,
47
- }}
48
- className={cn('overflow-hidden border-border border-b px-2 py-2')}
49
- exit={{
50
- opacity: 0,
51
- maxHeight: 0,
52
- paddingTop: 0,
53
- paddingBottom: 0,
54
- borderBottomWidth: 0,
55
- }}
56
- initial={{
57
- opacity: 0,
58
- maxHeight: 0,
59
- paddingTop: 0,
60
- paddingBottom: 0,
61
- borderBottomWidth: 0,
62
- }}
63
- transition={transition}
64
- >
65
- <ItemCatalog category={catalogCategory} key={catalogCategory} />
66
- </motion.div>
67
- )}
68
- </AnimatePresence>
69
-
100
+ {/* Structure Tools Row - Animated */}
70
101
  <AnimatePresence>
71
- {phase === 'furnish' && mode === 'build' && (
102
+ {phase === 'structure' && mode === 'build' && (
72
103
  <motion.div
73
104
  animate={{
74
105
  opacity: 1,
@@ -77,10 +108,7 @@ export function ActionMenu({ className }: { className?: string }) {
77
108
  paddingBottom: 8,
78
109
  borderBottomWidth: 1,
79
110
  }}
80
- className={cn(
81
- 'overflow-hidden border-border',
82
- 'max-h-20 border-b px-2 py-2 opacity-100',
83
- )}
111
+ className={cn('max-h-20 overflow-hidden border-border border-b px-2 py-2')}
84
112
  exit={{
85
113
  opacity: 0,
86
114
  maxHeight: 0,
@@ -97,25 +125,24 @@ export function ActionMenu({ className }: { className?: string }) {
97
125
  }}
98
126
  transition={transition}
99
127
  >
100
- <div className="mx-auto w-max">
101
- <FurnishTools />
128
+ <div className="w-max">
129
+ <StructureTools />
102
130
  </div>
103
131
  </motion.div>
104
132
  )}
105
133
  </AnimatePresence>
106
134
 
107
- {/* Structure Tools Row - Animated */}
108
135
  <AnimatePresence>
109
- {phase === 'structure' && mode === 'build' && (
136
+ {showPaintTray && (
110
137
  <motion.div
111
138
  animate={{
112
139
  opacity: 1,
113
- maxHeight: 80,
140
+ maxHeight: 96,
114
141
  paddingTop: 8,
115
142
  paddingBottom: 8,
116
143
  borderBottomWidth: 1,
117
144
  }}
118
- className={cn('max-h-20 overflow-hidden border-border border-b px-2 py-2')}
145
+ className={cn('overflow-hidden border-border border-b px-3')}
119
146
  exit={{
120
147
  opacity: 0,
121
148
  maxHeight: 0,
@@ -132,20 +159,32 @@ export function ActionMenu({ className }: { className?: string }) {
132
159
  }}
133
160
  transition={transition}
134
161
  >
135
- <div className="w-max">
136
- <StructureTools />
137
- </div>
162
+ <PaintMaterialTray />
138
163
  </motion.div>
139
164
  )}
140
165
  </AnimatePresence>
141
- {/* Control Mode Row - Always visible, centered */}
142
- <div className="flex items-center justify-center gap-1 px-2 py-1.5">
143
- <ControlModes />
144
- <div className="mx-1 h-5 w-px bg-border" />
145
- <ViewToggles />
146
- <div className="mx-1 h-5 w-px bg-border" />
147
- <CameraActions />
148
- </div>
166
+ {isMobile ? (
167
+ <div className="flex flex-col items-stretch gap-0.5 px-2 py-1.5">
168
+ {/* Row 1: control modes only */}
169
+ <div className="flex items-center justify-center gap-1">
170
+ <ControlModes />
171
+ </div>
172
+ {/* Row 2: grid snap + secondary toggles (orbit + top view hidden) */}
173
+ <div className="flex items-center justify-center gap-1 border-border/50 border-t pt-1">
174
+ <GridSnapControl />
175
+ <SecondaryToggles />
176
+ </div>
177
+ </div>
178
+ ) : (
179
+ <div className="flex items-center justify-center gap-1 px-2 py-1.5">
180
+ <ControlModes />
181
+ <div className="mx-1 h-5 w-px bg-border" />
182
+ <GridSnapControl />
183
+ <SecondaryToggles />
184
+ <div className="mx-1 h-5 w-px bg-border" />
185
+ <CameraActions />
186
+ </div>
187
+ )}
149
188
  </motion.div>
150
189
  </TooltipProvider>
151
190
  )
@@ -24,12 +24,14 @@ export const tools: ToolConfig[] = [
24
24
  // { id: 'custom-room', iconSrc: '/icons/custom-room.png', label: 'Custom Room' },
25
25
  { id: 'slab', iconSrc: '/icons/floor.png', label: 'Slab' },
26
26
  { id: 'ceiling', iconSrc: '/icons/ceiling.png', label: 'Ceiling' },
27
+ { id: 'column', iconSrc: '/icons/column.png', label: 'Column' },
27
28
  { id: 'roof', iconSrc: '/icons/roof.png', label: 'Gable Roof' },
28
29
  { id: 'stair', iconSrc: '/icons/stairs.png', label: 'Stairs' },
29
30
  { id: 'door', iconSrc: '/icons/door.png', label: 'Door' },
30
31
  { id: 'window', iconSrc: '/icons/window.png', label: 'Window' },
31
32
  { id: 'fence', iconSrc: '/icons/fence.png', label: 'Fence' },
32
33
  { id: 'zone', iconSrc: '/icons/zone.png', label: 'Zone' },
34
+ { id: 'spawn', iconSrc: '/icons/site.png', label: 'Spawn Point' },
33
35
  ]
34
36
 
35
37
  export function StructureTools() {