@pascal-app/editor 0.5.1 → 0.7.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.
- package/package.json +12 -7
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +75 -7
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +29 -0
- package/src/components/editor/first-person/build-collider-world.ts +365 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +281 -83
- package/src/components/editor/floating-building-action-menu.tsx +4 -3
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +10442 -3275
- package/src/components/editor/index.tsx +270 -20
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/selection-manager.tsx +766 -12
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +350 -157
- package/src/components/editor/use-floorplan-background-placement.ts +257 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
- package/src/components/editor/use-floorplan-scene-data.ts +189 -0
- package/src/components/editor/wall-measurement-label.tsx +377 -58
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
- package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
- package/src/components/editor-2d/svg-paths.ts +119 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +272 -0
- package/src/components/systems/roof/roof-edit-system.tsx +5 -5
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +2 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
- package/src/components/tools/column/column-tool.tsx +97 -0
- package/src/components/tools/column/move-column-tool.tsx +105 -0
- package/src/components/tools/door/door-tool.tsx +19 -0
- package/src/components/tools/door/move-door-tool.tsx +38 -8
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +27 -8
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +438 -0
- package/src/components/tools/fence/move-fence-tool.tsx +102 -27
- package/src/components/tools/item/move-tool.tsx +19 -1
- package/src/components/tools/item/placement-math.ts +44 -7
- package/src/components/tools/item/placement-strategies.ts +111 -33
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +2 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +701 -61
- package/src/components/tools/roof/move-roof-tool.tsx +111 -43
- package/src/components/tools/shared/polygon-editor.tsx +244 -29
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/move-slab-tool.tsx +182 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +2 -0
- package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
- package/src/components/tools/spawn/spawn-tool.tsx +130 -0
- package/src/components/tools/stair/stair-tool.tsx +11 -3
- package/src/components/tools/tool-manager.tsx +30 -3
- package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +423 -0
- package/src/components/tools/wall/move-wall-tool.tsx +356 -0
- package/src/components/tools/wall/wall-drafting.ts +348 -17
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +28 -0
- package/src/components/tools/window/window-tool.tsx +17 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +37 -5
- package/src/components/ui/action-menu/index.tsx +91 -1
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +424 -35
- package/src/components/ui/command-palette/editor-commands.tsx +27 -5
- package/src/components/ui/command-palette/index.tsx +0 -1
- package/src/components/ui/controls/material-picker.tsx +189 -169
- package/src/components/ui/controls/slider-control.tsx +88 -26
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1121 -1219
- package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
- package/src/components/ui/level-duplicate-dialog.tsx +115 -0
- package/src/components/ui/panels/ceiling-panel.tsx +47 -27
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +986 -294
- package/src/components/ui/panels/fence-panel.tsx +55 -12
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
- package/src/components/ui/panels/node-display.ts +39 -0
- package/src/components/ui/panels/paint-panel.tsx +138 -0
- package/src/components/ui/panels/panel-manager.tsx +241 -30
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +243 -9
- package/src/components/ui/panels/roof-panel.tsx +30 -62
- package/src/components/ui/panels/roof-segment-panel.tsx +8 -23
- package/src/components/ui/panels/slab-panel.tsx +46 -24
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +117 -69
- package/src/components/ui/panels/stair-segment-panel.tsx +13 -27
- package/src/components/ui/panels/wall-panel.tsx +71 -17
- package/src/components/ui/panels/window-panel.tsx +665 -146
- package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +138 -56
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +12 -6
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +96 -2
- package/src/components/viewer-overlay.tsx +25 -19
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-contextual-tools.ts +14 -13
- package/src/hooks/use-keyboard.ts +67 -9
- package/src/hooks/use-mobile.ts +12 -12
- package/src/index.tsx +2 -1
- package/src/lib/door-interaction.ts +88 -0
- package/src/lib/floorplan/geometry.ts +263 -0
- package/src/lib/floorplan/index.ts +38 -0
- package/src/lib/floorplan/items.ts +179 -0
- package/src/lib/floorplan/selection-tool.ts +231 -0
- package/src/lib/floorplan/stairs.ts +478 -0
- package/src/lib/floorplan/types.ts +57 -0
- package/src/lib/floorplan/walls.ts +23 -0
- package/src/lib/guide-events.ts +10 -0
- package/src/lib/history.ts +20 -0
- package/src/lib/level-duplication.test.ts +72 -0
- package/src/lib/level-duplication.ts +153 -0
- package/src/lib/local-guide-image.ts +42 -0
- package/src/lib/material-paint.ts +284 -0
- package/src/lib/roof-duplication.ts +214 -0
- package/src/lib/scene-bounds.test.ts +183 -0
- package/src/lib/scene-bounds.ts +169 -0
- package/src/lib/sfx-player.ts +96 -13
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +279 -15
|
@@ -2,7 +2,7 @@ import { type AnyNodeId, type StairNode, type StairSegmentNode, useScene } from
|
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { AnimatePresence } from 'motion/react'
|
|
4
4
|
import Image from 'next/image'
|
|
5
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
5
|
+
import { memo, useCallback, useEffect, useState } from 'react'
|
|
6
6
|
import { useShallow } from 'zustand/react/shallow'
|
|
7
7
|
import useEditor from '../../../../../store/use-editor'
|
|
8
8
|
import { InlineRenameInput } from './inline-rename-input'
|
|
@@ -16,7 +16,11 @@ interface StairTreeNodeProps {
|
|
|
16
16
|
isLast?: boolean
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export
|
|
19
|
+
export const StairTreeNode = memo(function StairTreeNode({
|
|
20
|
+
nodeId,
|
|
21
|
+
depth,
|
|
22
|
+
isLast,
|
|
23
|
+
}: StairTreeNodeProps) {
|
|
20
24
|
const [isEditing, setIsEditing] = useState(false)
|
|
21
25
|
const [expanded, setExpanded] = useState(false)
|
|
22
26
|
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
@@ -147,7 +151,7 @@ export function StairTreeNode({ nodeId, depth, isLast }: StairTreeNodeProps) {
|
|
|
147
151
|
</TreeNodeWrapper>
|
|
148
152
|
</div>
|
|
149
153
|
)
|
|
150
|
-
}
|
|
154
|
+
})
|
|
151
155
|
|
|
152
156
|
function StairSegmentTreeNode({
|
|
153
157
|
node,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type AnyNodeId, emitter, useScene } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { Camera, Eye, EyeOff, Trash2 } from 'lucide-react'
|
|
4
|
-
import { useState } from 'react'
|
|
4
|
+
import { memo, useState } from 'react'
|
|
5
5
|
import {
|
|
6
6
|
Popover,
|
|
7
7
|
PopoverContent,
|
|
@@ -12,7 +12,7 @@ interface TreeNodeActionsProps {
|
|
|
12
12
|
nodeId: AnyNodeId
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function TreeNodeActions({ nodeId }: TreeNodeActionsProps) {
|
|
15
|
+
export const TreeNodeActions = memo(function TreeNodeActions({ nodeId }: TreeNodeActionsProps) {
|
|
16
16
|
const [open, setOpen] = useState(false)
|
|
17
17
|
const updateNode = useScene((state) => state.updateNode)
|
|
18
18
|
const updateNodes = useScene((state) => state.updateNodes)
|
|
@@ -112,4 +112,4 @@ export function TreeNodeActions({ nodeId }: TreeNodeActionsProps) {
|
|
|
112
112
|
</Popover>
|
|
113
113
|
</div>
|
|
114
114
|
)
|
|
115
|
-
}
|
|
115
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type AnyNodeId, emitter, useScene } from '@pascal-app/core'
|
|
2
2
|
import { ChevronRight } from 'lucide-react'
|
|
3
3
|
import { AnimatePresence, motion } from 'motion/react'
|
|
4
|
-
import { forwardRef, useEffect, useRef } from 'react'
|
|
4
|
+
import { forwardRef, memo, useEffect, useRef } from 'react'
|
|
5
5
|
|
|
6
6
|
export function handleTreeSelection(
|
|
7
7
|
e: React.MouseEvent,
|
|
@@ -56,12 +56,14 @@ export function focusTreeNode(nodeId: AnyNodeId) {
|
|
|
56
56
|
import { cn } from '../../../../../lib/utils'
|
|
57
57
|
import { BuildingTreeNode } from './building-tree-node'
|
|
58
58
|
import { CeilingTreeNode } from './ceiling-tree-node'
|
|
59
|
+
import { ColumnTreeNode } from './column-tree-node'
|
|
59
60
|
import { DoorTreeNode } from './door-tree-node'
|
|
60
61
|
import { FenceTreeNode } from './fence-tree-node'
|
|
61
62
|
import { ItemTreeNode } from './item-tree-node'
|
|
62
63
|
import { LevelTreeNode } from './level-tree-node'
|
|
63
64
|
import { RoofTreeNode } from './roof-tree-node'
|
|
64
65
|
import { SlabTreeNode } from './slab-tree-node'
|
|
66
|
+
import { SpawnTreeNode } from './spawn-tree-node'
|
|
65
67
|
import { StairTreeNode } from './stair-tree-node'
|
|
66
68
|
import { WallTreeNode } from './wall-tree-node'
|
|
67
69
|
import { WindowTreeNode } from './window-tree-node'
|
|
@@ -73,20 +75,24 @@ interface TreeNodeProps {
|
|
|
73
75
|
isLast?: boolean
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
export function TreeNode({ nodeId, depth = 0, isLast }: TreeNodeProps) {
|
|
78
|
+
export const TreeNode = memo(function TreeNode({ nodeId, depth = 0, isLast }: TreeNodeProps) {
|
|
77
79
|
const nodeType = useScene((state) => state.nodes[nodeId]?.type)
|
|
78
80
|
|
|
79
81
|
if (!nodeType) return null
|
|
80
82
|
|
|
81
83
|
switch (nodeType) {
|
|
82
84
|
case 'building':
|
|
83
|
-
return <BuildingTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
85
|
+
return <BuildingTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `building_${string}`} />
|
|
84
86
|
case 'ceiling':
|
|
85
87
|
return <CeilingTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
88
|
+
case 'column':
|
|
89
|
+
return <ColumnTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
86
90
|
case 'level':
|
|
87
|
-
return <LevelTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
91
|
+
return <LevelTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `level_${string}`} />
|
|
88
92
|
case 'slab':
|
|
89
93
|
return <SlabTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
94
|
+
case 'spawn':
|
|
95
|
+
return <SpawnTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `spawn_${string}`} />
|
|
90
96
|
case 'wall':
|
|
91
97
|
return <WallTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
92
98
|
case 'fence':
|
|
@@ -102,11 +108,11 @@ export function TreeNode({ nodeId, depth = 0, isLast }: TreeNodeProps) {
|
|
|
102
108
|
case 'window':
|
|
103
109
|
return <WindowTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
104
110
|
case 'zone':
|
|
105
|
-
return <ZoneTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
111
|
+
return <ZoneTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `zone_${string}`} />
|
|
106
112
|
default:
|
|
107
113
|
return null
|
|
108
114
|
}
|
|
109
|
-
}
|
|
115
|
+
})
|
|
110
116
|
|
|
111
117
|
interface TreeNodeWrapperProps {
|
|
112
118
|
nodeId?: string
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type AnyNodeId, useScene, type WallNode } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import Image from 'next/image'
|
|
4
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow'
|
|
6
6
|
import useEditor from './../../../../../store/use-editor'
|
|
7
7
|
import { InlineRenameInput } from './inline-rename-input'
|
|
@@ -14,7 +14,11 @@ interface WallTreeNodeProps {
|
|
|
14
14
|
isLast?: boolean
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export
|
|
17
|
+
export const WallTreeNode = memo(function WallTreeNode({
|
|
18
|
+
nodeId,
|
|
19
|
+
depth,
|
|
20
|
+
isLast,
|
|
21
|
+
}: WallTreeNodeProps) {
|
|
18
22
|
const [expanded, setExpanded] = useState(false)
|
|
19
23
|
const [isEditing, setIsEditing] = useState(false)
|
|
20
24
|
const isVisible = useScene((s) => s.nodes[nodeId as AnyNodeId]?.visible !== false)
|
|
@@ -107,4 +111,4 @@ export function WallTreeNode({ nodeId, depth, isLast }: WallTreeNodeProps) {
|
|
|
107
111
|
))}
|
|
108
112
|
</TreeNodeWrapper>
|
|
109
113
|
)
|
|
110
|
-
}
|
|
114
|
+
})
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import Image from 'next/image'
|
|
6
|
-
import { useCallback, useState } from 'react'
|
|
6
|
+
import { memo, useCallback, useState } from 'react'
|
|
7
7
|
import useEditor from './../../../../../store/use-editor'
|
|
8
8
|
import { InlineRenameInput } from './inline-rename-input'
|
|
9
9
|
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
@@ -15,7 +15,11 @@ interface WindowTreeNodeProps {
|
|
|
15
15
|
isLast?: boolean
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export
|
|
18
|
+
export const WindowTreeNode = memo(function WindowTreeNode({
|
|
19
|
+
nodeId,
|
|
20
|
+
depth,
|
|
21
|
+
isLast,
|
|
22
|
+
}: WindowTreeNodeProps) {
|
|
19
23
|
const [isEditing, setIsEditing] = useState(false)
|
|
20
24
|
const isVisible = useScene((s) => s.nodes[nodeId as AnyNodeId]?.visible !== false)
|
|
21
25
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
@@ -72,4 +76,4 @@ export function WindowTreeNode({ nodeId, depth, isLast }: WindowTreeNodeProps) {
|
|
|
72
76
|
onToggle={() => {}}
|
|
73
77
|
/>
|
|
74
78
|
)
|
|
75
|
-
}
|
|
79
|
+
})
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useScene, type ZoneNode } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
-
import { useCallback, useState } from 'react'
|
|
3
|
+
import { memo, 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
|
-
nodeId:
|
|
10
|
+
nodeId: ZoneNode['id']
|
|
11
11
|
depth: number
|
|
12
12
|
isLast?: boolean
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export
|
|
15
|
+
export const ZoneTreeNode = memo(function ZoneTreeNode({
|
|
16
|
+
nodeId,
|
|
17
|
+
depth,
|
|
18
|
+
isLast,
|
|
19
|
+
}: ZoneTreeNodeProps) {
|
|
16
20
|
const [isEditing, setIsEditing] = useState(false)
|
|
17
21
|
const updateNode = useScene((state) => state.updateNode)
|
|
18
22
|
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
@@ -40,7 +44,7 @@ export function ZoneTreeNode({ nodeId, depth, isLast }: ZoneTreeNodeProps) {
|
|
|
40
44
|
depth={depth}
|
|
41
45
|
expanded={false}
|
|
42
46
|
hasChildren={false}
|
|
43
|
-
icon={<ColorDot color={color} onChange={(c) => updateNode(nodeId, { color: c })} />}
|
|
47
|
+
icon={<ColorDot color={color ?? '#3b82f6'} onChange={(c) => updateNode(nodeId, { color: c })} />}
|
|
44
48
|
isHovered={isHovered}
|
|
45
49
|
isLast={isLast}
|
|
46
50
|
isSelected={isSelected}
|
|
@@ -61,7 +65,7 @@ export function ZoneTreeNode({ nodeId, depth, isLast }: ZoneTreeNodeProps) {
|
|
|
61
65
|
onToggle={() => {}}
|
|
62
66
|
/>
|
|
63
67
|
)
|
|
64
|
-
}
|
|
68
|
+
})
|
|
65
69
|
|
|
66
70
|
/**
|
|
67
71
|
* Calculate the area of a polygon using the shoelace formula
|
|
@@ -74,8 +78,11 @@ function calculatePolygonArea(polygon: Array<[number, number]>): number {
|
|
|
74
78
|
|
|
75
79
|
for (let i = 0; i < n; i++) {
|
|
76
80
|
const j = (i + 1) % n
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
const current = polygon[i]
|
|
82
|
+
const next = polygon[j]
|
|
83
|
+
if (!(current && next)) continue
|
|
84
|
+
area += current[0] * next[1]
|
|
85
|
+
area -= next[0] * current[1]
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
return Math.abs(area) / 2
|
|
@@ -2,11 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
import { Icon as IconifyIcon } from '@iconify/react'
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Check,
|
|
7
|
+
ChevronsLeft,
|
|
8
|
+
ChevronsRight,
|
|
9
|
+
Columns2,
|
|
10
|
+
Eye,
|
|
11
|
+
EyeOff,
|
|
12
|
+
Footprints,
|
|
13
|
+
Grid2X2,
|
|
14
|
+
Moon,
|
|
15
|
+
Sun,
|
|
16
|
+
} from 'lucide-react'
|
|
6
17
|
import { useCallback } from 'react'
|
|
7
18
|
import { cn } from '../../lib/utils'
|
|
8
19
|
import useEditor from '../../store/use-editor'
|
|
9
|
-
import type { ViewMode } from '../../store/use-editor'
|
|
20
|
+
import type { GridSnapStep, ViewMode } from '../../store/use-editor'
|
|
21
|
+
import {
|
|
22
|
+
DropdownMenu,
|
|
23
|
+
DropdownMenuContent,
|
|
24
|
+
DropdownMenuItem,
|
|
25
|
+
DropdownMenuTrigger,
|
|
26
|
+
} from './primitives/dropdown-menu'
|
|
10
27
|
import { useSidebarStore } from './primitives/sidebar'
|
|
11
28
|
import { Tooltip, TooltipContent, TooltipTrigger } from './primitives/tooltip'
|
|
12
29
|
|
|
@@ -174,6 +191,18 @@ const levelModeLabels: Record<string, string> = {
|
|
|
174
191
|
solo: 'Solo',
|
|
175
192
|
}
|
|
176
193
|
|
|
194
|
+
const gridSnapOrder: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
|
|
195
|
+
const gridSnapLabels: Record<GridSnapStep, string> = {
|
|
196
|
+
0.5: '0.50',
|
|
197
|
+
0.25: '0.25',
|
|
198
|
+
0.1: '0.10',
|
|
199
|
+
0.05: '0.05',
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function formatGridSnapStep(step: GridSnapStep): string {
|
|
203
|
+
return gridSnapLabels[step]
|
|
204
|
+
}
|
|
205
|
+
|
|
177
206
|
function LevelModeToggle() {
|
|
178
207
|
const levelMode = useViewer((s) => s.levelMode)
|
|
179
208
|
const setLevelMode = useViewer((s) => s.setLevelMode)
|
|
@@ -219,6 +248,69 @@ function LevelModeToggle() {
|
|
|
219
248
|
)
|
|
220
249
|
}
|
|
221
250
|
|
|
251
|
+
function GridSnapToggle() {
|
|
252
|
+
const gridSnapStep = useEditor((s) => s.gridSnapStep)
|
|
253
|
+
const setGridSnapStep = useEditor((s) => s.setGridSnapStep)
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<DropdownMenu>
|
|
257
|
+
<Tooltip>
|
|
258
|
+
<TooltipTrigger asChild>
|
|
259
|
+
<DropdownMenuTrigger asChild>
|
|
260
|
+
<button className={cn(TOOLBAR_BTN, 'w-auto gap-1.5 px-2.5')} type="button">
|
|
261
|
+
<IconifyIcon height={14} icon="lucide:grid-2x2" width={14} />
|
|
262
|
+
<span className="font-medium text-xs">{formatGridSnapStep(gridSnapStep)}</span>
|
|
263
|
+
</button>
|
|
264
|
+
</DropdownMenuTrigger>
|
|
265
|
+
</TooltipTrigger>
|
|
266
|
+
<TooltipContent side="bottom">Grid snap: {formatGridSnapStep(gridSnapStep)}</TooltipContent>
|
|
267
|
+
</Tooltip>
|
|
268
|
+
<DropdownMenuContent align="center" side="bottom">
|
|
269
|
+
{gridSnapOrder.map((step) => {
|
|
270
|
+
const isActive = step === gridSnapStep
|
|
271
|
+
return (
|
|
272
|
+
<DropdownMenuItem key={step} onSelect={() => setGridSnapStep(step)}>
|
|
273
|
+
<span className="flex min-w-12 items-center justify-between gap-3">
|
|
274
|
+
<span>{formatGridSnapStep(step)}</span>
|
|
275
|
+
{isActive ? <Check className="h-3.5 w-3.5" /> : <span className="h-3.5 w-3.5" />}
|
|
276
|
+
</span>
|
|
277
|
+
</DropdownMenuItem>
|
|
278
|
+
)
|
|
279
|
+
})}
|
|
280
|
+
</DropdownMenuContent>
|
|
281
|
+
</DropdownMenu>
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function GridVisibilityToggle() {
|
|
286
|
+
const showGrid = useViewer((s) => s.showGrid)
|
|
287
|
+
const setShowGrid = useViewer((s) => s.setShowGrid)
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<Tooltip>
|
|
291
|
+
<TooltipTrigger asChild>
|
|
292
|
+
<button
|
|
293
|
+
aria-label={`Grid: ${showGrid ? 'Visible' : 'Hidden'}`}
|
|
294
|
+
aria-pressed={showGrid}
|
|
295
|
+
className={cn(
|
|
296
|
+
TOOLBAR_BTN,
|
|
297
|
+
'w-auto gap-1.5 px-2.5',
|
|
298
|
+
showGrid
|
|
299
|
+
? 'bg-white/10 text-foreground/90'
|
|
300
|
+
: 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0',
|
|
301
|
+
)}
|
|
302
|
+
onClick={() => setShowGrid(!showGrid)}
|
|
303
|
+
type="button"
|
|
304
|
+
>
|
|
305
|
+
<Grid2X2 className="h-3.5 w-3.5" />
|
|
306
|
+
{showGrid ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
|
|
307
|
+
</button>
|
|
308
|
+
</TooltipTrigger>
|
|
309
|
+
<TooltipContent side="bottom">Grid: {showGrid ? 'Visible' : 'Hidden'}</TooltipContent>
|
|
310
|
+
</Tooltip>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
222
314
|
// ── Wall mode toggle ────────────────────────────────────────────────────────
|
|
223
315
|
|
|
224
316
|
const wallModeOrder = ['cutaway', 'up', 'down'] as const
|
|
@@ -330,6 +422,8 @@ export function ViewerToolbarRight() {
|
|
|
330
422
|
<div className={TOOLBAR_CONTAINER}>
|
|
331
423
|
<LevelModeToggle />
|
|
332
424
|
<WallModeToggle />
|
|
425
|
+
<GridSnapToggle />
|
|
426
|
+
<GridVisibilityToggle />
|
|
333
427
|
<div className="my-1.5 w-px bg-border/50" />
|
|
334
428
|
<UnitToggle />
|
|
335
429
|
<ThemeToggle />
|
|
@@ -14,6 +14,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
14
14
|
import { ArrowLeft, Camera, ChevronRight, Diamond, Layers, Moon, Sun } from 'lucide-react'
|
|
15
15
|
import { motion } from 'motion/react'
|
|
16
16
|
import Link from 'next/link'
|
|
17
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
17
18
|
import { cn } from '../lib/utils'
|
|
18
19
|
import { ActionButton } from './ui/action-menu/action-button'
|
|
19
20
|
import { TooltipProvider } from './ui/primitives/tooltip'
|
|
@@ -87,7 +88,6 @@ export const ViewerOverlay = ({
|
|
|
87
88
|
onBack,
|
|
88
89
|
}: ViewerOverlayProps) => {
|
|
89
90
|
const selection = useViewer((s) => s.selection)
|
|
90
|
-
const nodes = useScene((s) => s.nodes)
|
|
91
91
|
const showScans = useViewer((s) => s.showScans)
|
|
92
92
|
const showGuides = useViewer((s) => s.showGuides)
|
|
93
93
|
const cameraMode = useViewer((s) => s.cameraMode)
|
|
@@ -95,24 +95,30 @@ export const ViewerOverlay = ({
|
|
|
95
95
|
const wallMode = useViewer((s) => s.wallMode)
|
|
96
96
|
const theme = useViewer((s) => s.theme)
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
98
|
+
// Subscribe only to the specific nodes we read so that creating an unrelated
|
|
99
|
+
// node elsewhere in the scene doesn't re-render this overlay.
|
|
100
|
+
const firstSelectedId = selection.selectedIds[0] ?? null
|
|
101
|
+
const building = useScene((s) =>
|
|
102
|
+
selection.buildingId ? (s.nodes[selection.buildingId] as BuildingNode | undefined) : null,
|
|
103
|
+
)
|
|
104
|
+
const level = useScene((s) =>
|
|
105
|
+
selection.levelId ? (s.nodes[selection.levelId] as LevelNode | undefined) : null,
|
|
106
|
+
)
|
|
107
|
+
const zone = useScene((s) =>
|
|
108
|
+
selection.zoneId ? (s.nodes[selection.zoneId] as ZoneNode | undefined) : null,
|
|
109
|
+
)
|
|
110
|
+
const selectedNode = useScene((s) =>
|
|
111
|
+
firstSelectedId ? (s.nodes[firstSelectedId as AnyNodeId] as AnyNode | undefined) : null,
|
|
112
|
+
)
|
|
113
|
+
const levels = useScene(
|
|
114
|
+
useShallow((s) => {
|
|
115
|
+
if (!building) return []
|
|
116
|
+
return building.children
|
|
117
|
+
.map((id) => s.nodes[id as AnyNodeId] as LevelNode | undefined)
|
|
118
|
+
.filter((n): n is LevelNode => n?.type === 'level')
|
|
119
|
+
.sort((a, b) => a.level - b.level)
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
116
122
|
|
|
117
123
|
const handleLevelClick = (levelId: LevelNode['id']) => {
|
|
118
124
|
// When switching levels, deselect zone and items
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { emitter, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useEffect, useRef } from 'react'
|
|
5
|
+
import { computeSceneBoundsXZ } from '../lib/scene-bounds'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Auto-frame the camera onto a freshly loaded scene.
|
|
9
|
+
*
|
|
10
|
+
* Motivation: when the MCP `setScene` tool (or any other entry point) swaps
|
|
11
|
+
* the scene graph while the default camera is pointing at empty space, the
|
|
12
|
+
* user sees a black viewport. This hook subscribes to the core scene store
|
|
13
|
+
* and, whenever `nodes` transitions from empty → non-empty, computes the
|
|
14
|
+
* XZ bounds of the new scene and emits `camera-controls:fit-scene`. The
|
|
15
|
+
* `<CustomCameraControls />` component picks up that event and frames the
|
|
16
|
+
* camera onto the bounds.
|
|
17
|
+
*
|
|
18
|
+
* Mount in exactly ONE component (the Editor). It holds no state of its own;
|
|
19
|
+
* the subscription is torn down on unmount.
|
|
20
|
+
*/
|
|
21
|
+
export function useAutoFrame(): void {
|
|
22
|
+
// Track the previous node count so we can detect the empty → non-empty edge.
|
|
23
|
+
const wasEmptyRef = useRef(true)
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
// Initialise from current store state so a remount after a setScene
|
|
27
|
+
// doesn't re-frame an already-populated scene.
|
|
28
|
+
wasEmptyRef.current = Object.keys(useScene.getState().nodes).length === 0
|
|
29
|
+
|
|
30
|
+
const unsubscribe = useScene.subscribe((state) => {
|
|
31
|
+
const isEmpty = Object.keys(state.nodes).length === 0
|
|
32
|
+
const wasEmpty = wasEmptyRef.current
|
|
33
|
+
wasEmptyRef.current = isEmpty
|
|
34
|
+
|
|
35
|
+
// Only react to empty → non-empty transitions. Normal edits keep both
|
|
36
|
+
// flags false; a `clearScene()` goes non-empty → empty and is ignored.
|
|
37
|
+
if (!wasEmpty || isEmpty) return
|
|
38
|
+
|
|
39
|
+
const bounds = computeSceneBoundsXZ(state.nodes)
|
|
40
|
+
emitter.emit('camera-controls:fit-scene', bounds ? { bounds } : {})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return unsubscribe
|
|
44
|
+
}, [])
|
|
45
|
+
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { useMemo } from 'react'
|
|
4
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
4
5
|
import useEditor, { type StructureTool } from '../store/use-editor'
|
|
5
6
|
|
|
6
7
|
export function useContextualTools() {
|
|
7
8
|
const selection = useViewer((s) => s.selection)
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
// Only resubscribe when the *types* of selected nodes change, not when any
|
|
10
|
+
// node in the scene mutates.
|
|
11
|
+
const selectedTypes = useScene(
|
|
12
|
+
useShallow((s) =>
|
|
13
|
+
selection.selectedIds.map((id) => s.nodes[id as AnyNodeId]?.type).filter(Boolean),
|
|
14
|
+
),
|
|
15
|
+
)
|
|
10
16
|
const structureLayer = useEditor((s) => s.structureLayer)
|
|
11
17
|
|
|
12
18
|
return useMemo(() => {
|
|
@@ -26,35 +32,30 @@ export function useContextualTools() {
|
|
|
26
32
|
'window',
|
|
27
33
|
]
|
|
28
34
|
|
|
29
|
-
if (
|
|
35
|
+
if (selectedTypes.length === 0) {
|
|
30
36
|
return defaultTools
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
// Get types of selected nodes
|
|
34
|
-
const selectedTypes = new Set(
|
|
35
|
-
selection.selectedIds.map((id) => nodes[id as AnyNodeId]?.type).filter(Boolean),
|
|
36
|
-
)
|
|
37
|
-
|
|
38
39
|
// If a wall is selected, prioritize wall-hosted elements
|
|
39
|
-
if (selectedTypes.
|
|
40
|
+
if (selectedTypes.includes('wall')) {
|
|
40
41
|
return ['window', 'door', 'wall', 'fence'] as StructureTool[]
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
// If a slab is selected, prioritize slab editing
|
|
44
|
-
if (selectedTypes.
|
|
45
|
+
if (selectedTypes.includes('slab')) {
|
|
45
46
|
return ['slab', 'wall'] as StructureTool[]
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
// If a ceiling is selected, prioritize ceiling editing
|
|
49
|
-
if (selectedTypes.
|
|
50
|
+
if (selectedTypes.includes('ceiling')) {
|
|
50
51
|
return ['ceiling'] as StructureTool[]
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
// If a roof is selected, prioritize roof editing
|
|
54
|
-
if (selectedTypes.
|
|
55
|
+
if (selectedTypes.includes('roof')) {
|
|
55
56
|
return ['roof'] as StructureTool[]
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
return defaultTools
|
|
59
|
-
}, [
|
|
60
|
+
}, [selectedTypes, structureLayer])
|
|
60
61
|
}
|