@pascal-app/editor 0.4.0 → 0.5.1

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 (61) hide show
  1. package/package.json +5 -5
  2. package/src/components/editor/floating-action-menu.tsx +101 -29
  3. package/src/components/editor/floating-building-action-menu.tsx +69 -0
  4. package/src/components/editor/floorplan-panel.tsx +31 -13
  5. package/src/components/editor/index.tsx +219 -167
  6. package/src/components/editor/node-action-menu.tsx +26 -10
  7. package/src/components/editor/selection-manager.tsx +38 -2
  8. package/src/components/editor/thumbnail-generator.tsx +245 -64
  9. package/src/components/systems/stair/stair-edit-system.tsx +27 -5
  10. package/src/components/tools/building/move-building-tool.tsx +157 -0
  11. package/src/components/tools/door/door-math.ts +1 -1
  12. package/src/components/tools/door/door-tool.tsx +19 -7
  13. package/src/components/tools/door/move-door-tool.tsx +17 -8
  14. package/src/components/tools/fence/fence-drafting.ts +125 -0
  15. package/src/components/tools/fence/fence-tool.tsx +190 -0
  16. package/src/components/tools/fence/move-fence-tool.tsx +223 -0
  17. package/src/components/tools/item/item-tool.tsx +3 -3
  18. package/src/components/tools/item/move-tool.tsx +7 -0
  19. package/src/components/tools/item/placement-strategies.ts +15 -7
  20. package/src/components/tools/item/use-placement-coordinator.tsx +89 -14
  21. package/src/components/tools/roof/move-roof-tool.tsx +5 -2
  22. package/src/components/tools/roof/roof-tool.tsx +6 -6
  23. package/src/components/tools/select/box-select-tool.tsx +2 -2
  24. package/src/components/tools/shared/polygon-editor.tsx +2 -2
  25. package/src/components/tools/slab/slab-tool.tsx +4 -4
  26. package/src/components/tools/stair/stair-defaults.ts +10 -0
  27. package/src/components/tools/stair/stair-tool.tsx +29 -6
  28. package/src/components/tools/tool-manager.tsx +42 -14
  29. package/src/components/tools/wall/wall-tool.tsx +19 -29
  30. package/src/components/tools/window/move-window-tool.tsx +17 -8
  31. package/src/components/tools/window/window-math.ts +1 -1
  32. package/src/components/tools/window/window-tool.tsx +19 -7
  33. package/src/components/tools/zone/zone-tool.tsx +7 -7
  34. package/src/components/ui/action-menu/structure-tools.tsx +1 -0
  35. package/src/components/ui/helpers/building-helper.tsx +32 -0
  36. package/src/components/ui/helpers/helper-manager.tsx +2 -0
  37. package/src/components/ui/panels/fence-panel.tsx +184 -0
  38. package/src/components/ui/panels/panel-manager.tsx +3 -0
  39. package/src/components/ui/panels/stair-panel.tsx +206 -33
  40. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +22 -15
  41. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +60 -52
  42. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +35 -24
  43. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +65 -0
  44. package/src/components/ui/sidebar/panels/site-panel/index.tsx +59 -40
  45. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +14 -13
  46. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +59 -52
  47. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +27 -22
  48. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +66 -49
  49. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +35 -36
  50. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +66 -49
  51. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +11 -11
  52. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +17 -14
  53. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +57 -53
  54. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +35 -24
  55. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +22 -27
  56. package/src/components/viewer-overlay.tsx +1 -0
  57. package/src/hooks/use-auto-save.ts +3 -6
  58. package/src/hooks/use-contextual-tools.ts +10 -2
  59. package/src/hooks/use-grid-events.ts +13 -1
  60. package/src/hooks/use-keyboard.ts +4 -0
  61. package/src/store/use-editor.tsx +7 -0
@@ -1,62 +1,57 @@
1
- import { useScene, type ZoneNode } from '@pascal-app/core'
1
+ import { type AnyNodeId, useScene, type ZoneNode } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
- import { useState } from 'react'
3
+ import { useCallback, useState } from 'react'
4
4
  import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
5
5
  import { InlineRenameInput } from './inline-rename-input'
6
6
  import { focusTreeNode, TreeNodeWrapper } from './tree-node'
7
7
  import { TreeNodeActions } from './tree-node-actions'
8
8
 
9
9
  interface ZoneTreeNodeProps {
10
- node: ZoneNode
10
+ nodeId: AnyNodeId
11
11
  depth: number
12
12
  isLast?: boolean
13
13
  }
14
14
 
15
- export function ZoneTreeNode({ node, depth, isLast }: ZoneTreeNodeProps) {
15
+ export function ZoneTreeNode({ nodeId, depth, isLast }: ZoneTreeNodeProps) {
16
16
  const [isEditing, setIsEditing] = useState(false)
17
17
  const updateNode = useScene((state) => state.updateNode)
18
- const isSelected = useViewer((state) => state.selection.zoneId === node.id)
19
- const isHovered = useViewer((state) => state.hoveredId === node.id)
18
+ const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
19
+ const color = useScene((s) => (s.nodes[nodeId] as ZoneNode | undefined)?.color)
20
+ const polygon = useScene((s) => (s.nodes[nodeId] as ZoneNode | undefined)?.polygon ?? [])
21
+ const isSelected = useViewer((state) => state.selection.zoneId === nodeId)
22
+ const isHovered = useViewer((state) => state.hoveredId === nodeId)
20
23
  const setSelection = useViewer((state) => state.setSelection)
21
24
  const setHoveredId = useViewer((state) => state.setHoveredId)
22
25
 
23
- const handleClick = () => {
24
- setSelection({ zoneId: node.id })
25
- }
26
-
27
- const handleDoubleClick = () => {
28
- focusTreeNode(node.id)
29
- }
30
-
31
- const handleMouseEnter = () => {
32
- setHoveredId(node.id)
33
- }
34
-
35
- const handleMouseLeave = () => {
36
- setHoveredId(null)
37
- }
26
+ const handleClick = useCallback(() => setSelection({ zoneId: nodeId }), [nodeId, setSelection])
27
+ const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
28
+ const handleMouseEnter = useCallback(() => setHoveredId(nodeId), [nodeId, setHoveredId])
29
+ const handleMouseLeave = useCallback(() => setHoveredId(null), [setHoveredId])
30
+ const handleStartEditing = useCallback(() => setIsEditing(true), [])
31
+ const handleStopEditing = useCallback(() => setIsEditing(false), [])
38
32
 
39
33
  // Calculate approximate area from polygon
40
- const area = calculatePolygonArea(node.polygon).toFixed(1)
34
+ const area = calculatePolygonArea(polygon).toFixed(1)
41
35
  const defaultName = `Zone (${area}m²)`
42
36
 
43
37
  return (
44
38
  <TreeNodeWrapper
45
- actions={<TreeNodeActions node={node} />}
39
+ actions={<TreeNodeActions nodeId={nodeId} />}
46
40
  depth={depth}
47
41
  expanded={false}
48
42
  hasChildren={false}
49
- icon={<ColorDot color={node.color} onChange={(color) => updateNode(node.id, { color })} />}
43
+ icon={<ColorDot color={color} onChange={(c) => updateNode(nodeId, { color: c })} />}
50
44
  isHovered={isHovered}
51
45
  isLast={isLast}
52
46
  isSelected={isSelected}
47
+ isVisible={isVisible}
53
48
  label={
54
49
  <InlineRenameInput
55
50
  defaultName={defaultName}
56
51
  isEditing={isEditing}
57
- node={node}
58
- onStartEditing={() => setIsEditing(true)}
59
- onStopEditing={() => setIsEditing(false)}
52
+ nodeId={nodeId}
53
+ onStartEditing={handleStartEditing}
54
+ onStopEditing={handleStopEditing}
60
55
  />
61
56
  }
62
57
  onClick={handleClick}
@@ -62,6 +62,7 @@ const wallModeConfig = {
62
62
  const getNodeName = (node: AnyNode): string => {
63
63
  if ('name' in node && node.name) return node.name
64
64
  if (node.type === 'wall') return 'Wall'
65
+ if (node.type === 'fence') return 'Fence'
65
66
  if (node.type === 'item') return (node as { asset: { name: string } }).asset?.name || 'Item'
66
67
  if (node.type === 'slab') return 'Slab'
67
68
  if (node.type === 'ceiling') return 'Ceiling'
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { useScene } from '@pascal-app/core'
4
- import { type MutableRefObject, useCallback, useEffect, useRef, useState } from 'react'
4
+ import { type MutableRefObject, useCallback, useEffect, useRef } from 'react'
5
5
  import { type SceneGraph, saveSceneToLocalStorage } from '../lib/scene'
6
6
 
7
7
  const AUTOSAVE_DEBOUNCE_MS = 1000
@@ -26,9 +26,7 @@ export function useAutoSave({
26
26
  onDirty,
27
27
  onSaveStatusChange,
28
28
  isVersionPreviewMode = false,
29
- }: UseAutoSaveOptions): { saveStatus: SaveStatus; isLoadingSceneRef: MutableRefObject<boolean> } {
30
- const [saveStatus, _setSaveStatus] = useState<SaveStatus>('idle')
31
-
29
+ }: UseAutoSaveOptions): { isLoadingSceneRef: MutableRefObject<boolean> } {
32
30
  const saveTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
33
31
  const isSavingRef = useRef(false)
34
32
  const isLoadingSceneRef = useRef(false)
@@ -56,7 +54,6 @@ export function useAutoSave({
56
54
  }, [isVersionPreviewMode])
57
55
 
58
56
  const setSaveStatus = useCallback((status: SaveStatus) => {
59
- _setSaveStatus(status)
60
57
  onSaveStatusChangeRef.current?.(status)
61
58
  }, [])
62
59
 
@@ -190,5 +187,5 @@ export function useAutoSave({
190
187
  setSaveStatus('saved')
191
188
  }, [isVersionPreviewMode, setSaveStatus])
192
189
 
193
- return { saveStatus, isLoadingSceneRef }
190
+ return { isLoadingSceneRef }
194
191
  }
@@ -16,7 +16,15 @@ export function useContextualTools() {
16
16
  }
17
17
 
18
18
  // Default tools when nothing is selected
19
- const defaultTools: StructureTool[] = ['wall', 'slab', 'ceiling', 'roof', 'door', 'window']
19
+ const defaultTools: StructureTool[] = [
20
+ 'wall',
21
+ 'fence',
22
+ 'slab',
23
+ 'ceiling',
24
+ 'roof',
25
+ 'door',
26
+ 'window',
27
+ ]
20
28
 
21
29
  if (selection.selectedIds.length === 0) {
22
30
  return defaultTools
@@ -29,7 +37,7 @@ export function useContextualTools() {
29
37
 
30
38
  // If a wall is selected, prioritize wall-hosted elements
31
39
  if (selectedTypes.has('wall')) {
32
- return ['window', 'door', 'wall'] as StructureTool[]
40
+ return ['window', 'door', 'wall', 'fence'] as StructureTool[]
33
41
  }
34
42
 
35
43
  // If a slab is selected, prioritize slab editing
@@ -1,4 +1,10 @@
1
- import { type EventSuffix, emitter, type GridEvent } from '@pascal-app/core'
1
+ import {
2
+ type AnyNodeId,
3
+ type EventSuffix,
4
+ emitter,
5
+ type GridEvent,
6
+ sceneRegistry,
7
+ } from '@pascal-app/core'
2
8
  import { useViewer } from '@pascal-app/viewer'
3
9
  import { useThree } from '@react-three/fiber'
4
10
  import { useEffect, useRef } from 'react'
@@ -44,9 +50,15 @@ export function useGridEvents(gridY: number) {
44
50
  const point = getIntersection(nativeEvent)
45
51
  if (!point) return
46
52
 
53
+ // Convert world-space point to building-local for tools that live inside a building.
54
+ const buildingId = useViewer.getState().selection.buildingId
55
+ const buildingMesh = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
56
+ const localPoint = buildingMesh ? buildingMesh.worldToLocal(point.clone()) : point
57
+
47
58
  const eventKey = `grid:${suffix}` as `grid:${EventSuffix}`
48
59
  const payload: GridEvent = {
49
60
  position: [point.x, point.y, point.z],
61
+ localPosition: [localPoint.x, localPoint.y, localPoint.z],
50
62
  nativeEvent: nativeEvent as any, // Type compatibility with ThreeEvent
51
63
  }
52
64
 
@@ -84,6 +84,10 @@ export const useKeyboard = ({ isVersionPreviewMode = false } = {}) => {
84
84
  useEditor.getState().setPhase('structure')
85
85
  useEditor.getState().setStructureLayer('elements')
86
86
  useEditor.getState().setMode('build')
87
+ } else if (e.key === 'd' && !e.metaKey && !e.ctrlKey) {
88
+ if (isVersionPreviewMode) return
89
+ e.preventDefault()
90
+ useEditor.getState().setMode('delete')
87
91
  } else if (e.key === 'z' && (e.metaKey || e.ctrlKey)) {
88
92
  if (isVersionPreviewMode) return
89
93
  e.preventDefault()
@@ -4,6 +4,7 @@ import type { AssetInput } from '@pascal-app/core'
4
4
  import {
5
5
  type BuildingNode,
6
6
  type DoorNode,
7
+ type FenceNode,
7
8
  type ItemNode,
8
9
  type LevelNode,
9
10
  type RoofNode,
@@ -33,6 +34,7 @@ export type Mode = 'select' | 'edit' | 'delete' | 'build'
33
34
  // Structure mode tools (building elements)
34
35
  export type StructureTool =
35
36
  | 'wall'
37
+ | 'fence'
36
38
  | 'room'
37
39
  | 'custom-room'
38
40
  | 'slab'
@@ -85,20 +87,24 @@ type EditorState = {
85
87
  | ItemNode
86
88
  | WindowNode
87
89
  | DoorNode
90
+ | FenceNode
88
91
  | RoofNode
89
92
  | RoofSegmentNode
90
93
  | StairNode
91
94
  | StairSegmentNode
95
+ | BuildingNode
92
96
  | null
93
97
  setMovingNode: (
94
98
  node:
95
99
  | ItemNode
96
100
  | WindowNode
97
101
  | DoorNode
102
+ | FenceNode
98
103
  | RoofNode
99
104
  | RoofSegmentNode
100
105
  | StairNode
101
106
  | StairSegmentNode
107
+ | BuildingNode
102
108
  | null,
103
109
  ) => void
104
110
  selectedReferenceId: string | null
@@ -428,6 +434,7 @@ const useEditor = create<EditorState>()(
428
434
  | RoofSegmentNode
429
435
  | StairNode
430
436
  | StairSegmentNode
437
+ | BuildingNode
431
438
  | null,
432
439
  setMovingNode: (node) => set({ movingNode: node }),
433
440
  selectedReferenceId: null,