@pascal-app/editor 0.7.0 → 0.9.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.
- package/package.json +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5837 -12006
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +234 -190
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +435 -158
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +2 -2
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -543
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +57 -1
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +24 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +342 -91
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/tsconfig.json +2 -1
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -230
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import { getSceneTheme, useViewer } from '@pascal-app/viewer'
|
|
4
4
|
import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
5
5
|
import useEditor from '../../store/use-editor'
|
|
6
6
|
import { MobileTabBar } from '../ui/sidebar/mobile-tab-bar'
|
|
@@ -56,8 +56,8 @@ export function EditorLayoutMobile({
|
|
|
56
56
|
const activePanel = useEditor((s) => s.activeSidebarPanel)
|
|
57
57
|
const setActivePanel = useEditor((s) => s.setActiveSidebarPanel)
|
|
58
58
|
const panelSheetHeight = useEditor((s) => s.mobilePanelSheetHeight)
|
|
59
|
-
const
|
|
60
|
-
const viewerBg =
|
|
59
|
+
const isDark = useViewer((s) => getSceneTheme(s.sceneTheme).appearance === 'dark')
|
|
60
|
+
const viewerBg = isDark ? VIEWER_BG_DARK : VIEWER_BG_LIGHT
|
|
61
61
|
|
|
62
62
|
const middleRef = useRef<HTMLDivElement>(null)
|
|
63
63
|
const sheetRef = useRef<BottomSheetHandle>(null)
|
|
@@ -5,12 +5,14 @@ import { useIsMobile } from '../../hooks/use-mobile'
|
|
|
5
5
|
import useEditor from '../../store/use-editor'
|
|
6
6
|
|
|
7
7
|
import { useSidebarStore } from '../ui/primitives/sidebar'
|
|
8
|
-
import { type SidebarTab
|
|
8
|
+
import { IconRail, type SidebarTab } from '../ui/sidebar/tab-bar'
|
|
9
9
|
import { EditorLayoutMobile } from './editor-layout-mobile'
|
|
10
10
|
|
|
11
11
|
const SIDEBAR_MIN_WIDTH = 300
|
|
12
12
|
const SIDEBAR_MAX_WIDTH = 800
|
|
13
13
|
const SIDEBAR_COLLAPSE_THRESHOLD = 220
|
|
14
|
+
// Matches the `w-14` rail in <IconRail>; the resize math is relative to it.
|
|
15
|
+
const RAIL_WIDTH = 56
|
|
14
16
|
|
|
15
17
|
// ── Left column: resizable panel with tab bar ────────────────────────────────
|
|
16
18
|
|
|
@@ -33,7 +35,6 @@ function LeftColumn({
|
|
|
33
35
|
const setActivePanel = useEditor((s) => s.setActiveSidebarPanel)
|
|
34
36
|
|
|
35
37
|
const isResizing = useRef(false)
|
|
36
|
-
const isExpanding = useRef(false)
|
|
37
38
|
|
|
38
39
|
// Ensure active panel is a valid tab
|
|
39
40
|
useEffect(() => {
|
|
@@ -62,35 +63,40 @@ function LeftColumn({
|
|
|
62
63
|
[setIsDragging],
|
|
63
64
|
)
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
// Rail click: reopen a collapsed panel, collapse when re-clicking the open
|
|
67
|
+
// tab, otherwise switch tabs. Reopening clamps below-min persisted widths
|
|
68
|
+
// up to the minimum so the panel always returns to a usable size.
|
|
69
|
+
const handleRailClick = useCallback(
|
|
70
|
+
(id: string) => {
|
|
71
|
+
if (isCollapsed) {
|
|
72
|
+
setIsCollapsed(false)
|
|
73
|
+
if (width < SIDEBAR_MIN_WIDTH) setWidth(SIDEBAR_MIN_WIDTH)
|
|
74
|
+
setActivePanel(id)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
if (id === activePanel) {
|
|
78
|
+
setIsCollapsed(true)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
setActivePanel(id)
|
|
72
82
|
},
|
|
73
|
-
[
|
|
83
|
+
[isCollapsed, width, activePanel, setIsCollapsed, setWidth, setActivePanel],
|
|
74
84
|
)
|
|
75
85
|
|
|
76
86
|
useEffect(() => {
|
|
77
87
|
const handlePointerMove = (e: PointerEvent) => {
|
|
78
|
-
if (isResizing.current)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
setWidth(Math.max(SIDEBAR_MIN_WIDTH, Math.min(newWidth, SIDEBAR_MAX_WIDTH)))
|
|
85
|
-
}
|
|
86
|
-
} else if (isExpanding.current && e.clientX > 60) {
|
|
88
|
+
if (!isResizing.current) return
|
|
89
|
+
// Rail occupies the leftmost 48px; the panel starts after it.
|
|
90
|
+
const newWidth = e.clientX - RAIL_WIDTH
|
|
91
|
+
if (newWidth < SIDEBAR_COLLAPSE_THRESHOLD) {
|
|
92
|
+
setIsCollapsed(true)
|
|
93
|
+
} else {
|
|
87
94
|
setIsCollapsed(false)
|
|
88
|
-
setWidth(Math.max(SIDEBAR_MIN_WIDTH, Math.min(
|
|
95
|
+
setWidth(Math.max(SIDEBAR_MIN_WIDTH, Math.min(newWidth, SIDEBAR_MAX_WIDTH)))
|
|
89
96
|
}
|
|
90
97
|
}
|
|
91
98
|
const handlePointerUp = () => {
|
|
92
99
|
isResizing.current = false
|
|
93
|
-
isExpanding.current = false
|
|
94
100
|
setIsDragging(false)
|
|
95
101
|
document.body.style.cursor = ''
|
|
96
102
|
document.body.style.userSelect = ''
|
|
@@ -103,37 +109,36 @@ function LeftColumn({
|
|
|
103
109
|
}
|
|
104
110
|
}, [setWidth, setIsCollapsed, setIsDragging])
|
|
105
111
|
|
|
106
|
-
if (isCollapsed) {
|
|
107
|
-
return (
|
|
108
|
-
<div
|
|
109
|
-
className="relative h-full w-2 flex-shrink-0 cursor-col-resize transition-colors hover:bg-primary/20"
|
|
110
|
-
onPointerDown={handleGrabDown}
|
|
111
|
-
title="Expand sidebar"
|
|
112
|
-
/>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
112
|
return (
|
|
117
|
-
<div
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
113
|
+
<div className="relative z-10 flex h-full flex-shrink-0 bg-sidebar text-sidebar-foreground">
|
|
114
|
+
<IconRail
|
|
115
|
+
activeTab={activePanel}
|
|
116
|
+
collapsed={isCollapsed}
|
|
117
|
+
onIconClick={handleRailClick}
|
|
118
|
+
tabs={tabs}
|
|
119
|
+
/>
|
|
120
|
+
{!isCollapsed && (
|
|
121
|
+
<div
|
|
122
|
+
className="relative flex h-full flex-col"
|
|
123
|
+
style={{
|
|
124
|
+
width,
|
|
125
|
+
transition: isDragging ? 'none' : 'width 150ms ease',
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
<div className="relative flex flex-1 flex-col overflow-hidden">
|
|
129
|
+
{renderTabContent(activePanel)}
|
|
130
|
+
{sidebarOverlay && <div className="absolute inset-0 z-50">{sidebarOverlay}</div>}
|
|
131
|
+
</div>
|
|
129
132
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
{/* Resize handle + hit area */}
|
|
134
|
+
<div
|
|
135
|
+
className="absolute inset-y-0 -right-3 z-[100] flex w-6 cursor-col-resize items-center justify-center"
|
|
136
|
+
onPointerDown={handleResizerDown}
|
|
137
|
+
>
|
|
138
|
+
<div className="h-8 w-1 rounded-full bg-neutral-500" />
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
137
142
|
</div>
|
|
138
143
|
)
|
|
139
144
|
}
|
|
@@ -169,10 +174,12 @@ function RightColumn({
|
|
|
169
174
|
)}
|
|
170
175
|
{/* Canvas area */}
|
|
171
176
|
<div className="relative flex-1 overflow-hidden">{children}</div>
|
|
172
|
-
{/* Overlays scoped to the viewer column
|
|
177
|
+
{/* Overlays scoped to the viewer column. `data-viewer-bounds` marks the
|
|
178
|
+
draggable region the floating inspector clamps itself to. */}
|
|
173
179
|
{overlays && (
|
|
174
180
|
<div
|
|
175
181
|
className="pointer-events-none absolute inset-0 z-30"
|
|
182
|
+
data-viewer-bounds
|
|
176
183
|
style={{ transform: 'translateZ(0)' }}
|
|
177
184
|
>
|
|
178
185
|
{overlays}
|
|
@@ -205,6 +212,7 @@ export function EditorLayoutV2({
|
|
|
205
212
|
viewerContent,
|
|
206
213
|
overlays,
|
|
207
214
|
}: EditorLayoutV2Props) {
|
|
215
|
+
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
208
216
|
const isMobile = useIsMobile()
|
|
209
217
|
|
|
210
218
|
if (isMobile) {
|
|
@@ -229,7 +237,7 @@ export function EditorLayoutV2({
|
|
|
229
237
|
|
|
230
238
|
{/* Main content: left column + right column */}
|
|
231
239
|
<div className="flex min-h-0 flex-1">
|
|
232
|
-
{sidebarTabs.length > 0 && (
|
|
240
|
+
{!isCaptureMode && sidebarTabs.length > 0 && (
|
|
233
241
|
<LeftColumn
|
|
234
242
|
renderTabContent={renderTabContent}
|
|
235
243
|
sidebarOverlay={sidebarOverlay}
|
|
@@ -238,8 +246,8 @@ export function EditorLayoutV2({
|
|
|
238
246
|
)}
|
|
239
247
|
<RightColumn
|
|
240
248
|
overlays={overlays}
|
|
241
|
-
toolbarLeft={viewerToolbarLeft}
|
|
242
|
-
toolbarRight={viewerToolbarRight}
|
|
249
|
+
toolbarLeft={isCaptureMode ? undefined : viewerToolbarLeft}
|
|
250
|
+
toolbarRight={isCaptureMode ? undefined : viewerToolbarRight}
|
|
243
251
|
>
|
|
244
252
|
{viewerContent}
|
|
245
253
|
</RightColumn>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
4
|
import { useThree } from '@react-three/fiber'
|
|
5
5
|
import { useEffect } from 'react'
|
|
6
|
+
import type { Mesh, Object3D } from 'three'
|
|
6
7
|
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'
|
|
7
8
|
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js'
|
|
8
9
|
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js'
|
|
@@ -21,10 +22,11 @@ export function ExportManager() {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const date = new Date().toISOString().split('T')[0]
|
|
25
|
+
const exportScene = prepareSceneForExport(sceneGroup)
|
|
24
26
|
|
|
25
27
|
if (format === 'stl') {
|
|
26
28
|
const exporter = new STLExporter()
|
|
27
|
-
const result = exporter.parse(
|
|
29
|
+
const result = exporter.parse(exportScene, { binary: true })
|
|
28
30
|
const blob = new Blob([result], { type: 'model/stl' })
|
|
29
31
|
downloadBlob(blob, `model_${date}.stl`)
|
|
30
32
|
return
|
|
@@ -32,7 +34,7 @@ export function ExportManager() {
|
|
|
32
34
|
|
|
33
35
|
if (format === 'obj') {
|
|
34
36
|
const exporter = new OBJExporter()
|
|
35
|
-
const result = exporter.parse(
|
|
37
|
+
const result = exporter.parse(exportScene)
|
|
36
38
|
const blob = new Blob([result], { type: 'model/obj' })
|
|
37
39
|
downloadBlob(blob, `model_${date}.obj`)
|
|
38
40
|
return
|
|
@@ -43,7 +45,7 @@ export function ExportManager() {
|
|
|
43
45
|
|
|
44
46
|
return new Promise<void>((resolve, reject) => {
|
|
45
47
|
exporter.parse(
|
|
46
|
-
|
|
48
|
+
exportScene,
|
|
47
49
|
(gltf) => {
|
|
48
50
|
const blob = new Blob([gltf as ArrayBuffer], { type: 'model/gltf-binary' })
|
|
49
51
|
downloadBlob(blob, `model_${date}.glb`)
|
|
@@ -68,6 +70,33 @@ export function ExportManager() {
|
|
|
68
70
|
return null
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
function prepareSceneForExport(source: Object3D) {
|
|
74
|
+
const clone = source.clone(true)
|
|
75
|
+
const meshesToRemove: Mesh[] = []
|
|
76
|
+
|
|
77
|
+
clone.traverse((object) => {
|
|
78
|
+
if (isMeshWithInvalidGeometry(object)) meshesToRemove.push(object)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
for (const mesh of meshesToRemove) {
|
|
82
|
+
mesh.removeFromParent()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return clone
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isMeshWithInvalidGeometry(object: Object3D): object is Mesh {
|
|
89
|
+
if (!isMesh(object)) return false
|
|
90
|
+
|
|
91
|
+
// Three exporters can crash when a Mesh has no readable position attribute.
|
|
92
|
+
const position = object.geometry?.getAttribute('position')
|
|
93
|
+
return !position || position.count === 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isMesh(object: Object3D): object is Mesh {
|
|
97
|
+
return (object as Mesh).isMesh === true
|
|
98
|
+
}
|
|
99
|
+
|
|
71
100
|
function downloadBlob(blob: Blob, filename: string) {
|
|
72
101
|
const url = URL.createObjectURL(blob)
|
|
73
102
|
const link = document.createElement('a')
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
CeilingNode,
|
|
6
|
+
ColumnNode,
|
|
7
|
+
ElevatorNode,
|
|
8
|
+
LevelNode,
|
|
9
|
+
nodeRegistry,
|
|
10
|
+
registerNode,
|
|
11
|
+
ShelfNode,
|
|
12
|
+
SiteNode,
|
|
13
|
+
sceneRegistry,
|
|
14
|
+
useScene,
|
|
15
|
+
} from '@pascal-app/core'
|
|
16
|
+
import { BoxGeometry, Group, Mesh, MeshBasicMaterial } from 'three'
|
|
17
|
+
import { buildFirstPersonColliderWorldFromRegistry } from './build-collider-world'
|
|
18
|
+
|
|
19
|
+
function registerColliderDefinition(
|
|
20
|
+
kind: AnyNode['type'],
|
|
21
|
+
schema: AnyNodeDefinition['schema'],
|
|
22
|
+
category: AnyNodeDefinition['category'],
|
|
23
|
+
surfaceRole?: AnyNodeDefinition['surfaceRole'],
|
|
24
|
+
) {
|
|
25
|
+
registerNode({
|
|
26
|
+
kind,
|
|
27
|
+
schema,
|
|
28
|
+
schemaVersion: 1,
|
|
29
|
+
category,
|
|
30
|
+
surfaceRole,
|
|
31
|
+
capabilities: {},
|
|
32
|
+
} as AnyNodeDefinition)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mountNode(
|
|
36
|
+
node: AnyNode,
|
|
37
|
+
box: [number, number, number],
|
|
38
|
+
position: [number, number, number],
|
|
39
|
+
) {
|
|
40
|
+
const group = new Group()
|
|
41
|
+
const mesh = new Mesh(new BoxGeometry(box[0], box[1], box[2]), new MeshBasicMaterial())
|
|
42
|
+
mesh.position.set(position[0], position[1], position[2])
|
|
43
|
+
group.add(mesh)
|
|
44
|
+
group.updateMatrixWorld(true)
|
|
45
|
+
sceneRegistry.nodes.set(node.id, group)
|
|
46
|
+
sceneRegistry.byType[node.type]!.add(node.id)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function mountRegistryGroup(node: AnyNode) {
|
|
50
|
+
const group = new Group()
|
|
51
|
+
group.updateMatrixWorld(true)
|
|
52
|
+
sceneRegistry.nodes.set(node.id, group)
|
|
53
|
+
sceneRegistry.byType[node.type]!.add(node.id)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setSceneNodes(nodes: AnyNode[]) {
|
|
57
|
+
useScene.setState({
|
|
58
|
+
nodes: Object.fromEntries(nodes.map((node) => [node.id, node])),
|
|
59
|
+
rootNodeIds: nodes.map((node) => node.id),
|
|
60
|
+
} as never)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('buildFirstPersonColliderWorldFromRegistry', () => {
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
sceneRegistry.clear()
|
|
66
|
+
nodeRegistry._reset()
|
|
67
|
+
useScene.setState({ nodes: {}, rootNodeIds: [] } as never)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('includes structure and furnish nodes discovered through the node registry', () => {
|
|
71
|
+
registerColliderDefinition('column', ColumnNode, 'structure')
|
|
72
|
+
registerColliderDefinition('shelf', ShelfNode, 'furnish')
|
|
73
|
+
|
|
74
|
+
const column = ColumnNode.parse({ id: 'column_test' })
|
|
75
|
+
const shelf = ShelfNode.parse({ id: 'shelf_test', position: [3, 0, 0] })
|
|
76
|
+
setSceneNodes([column, shelf])
|
|
77
|
+
mountNode(column, [1, 2, 1], [0, 1, 0])
|
|
78
|
+
mountNode(shelf, [2, 1, 1], [3, 0.5, 0])
|
|
79
|
+
|
|
80
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
81
|
+
|
|
82
|
+
expect(world).not.toBeNull()
|
|
83
|
+
expect(world?.bounds?.min.x).toBeCloseTo(-0.5)
|
|
84
|
+
expect(world?.bounds?.max.x).toBeCloseTo(4)
|
|
85
|
+
world?.dispose()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('excludes ceiling surfaces so the walkthrough player passes through them', () => {
|
|
89
|
+
registerColliderDefinition('column', ColumnNode, 'structure')
|
|
90
|
+
registerColliderDefinition('ceiling', CeilingNode, 'structure', 'ceiling')
|
|
91
|
+
|
|
92
|
+
const column = ColumnNode.parse({ id: 'column_test' })
|
|
93
|
+
const ceiling = CeilingNode.parse({ id: 'ceiling_test', polygon: [] })
|
|
94
|
+
setSceneNodes([column, ceiling])
|
|
95
|
+
mountNode(column, [1, 2, 1], [0, 1, 0])
|
|
96
|
+
// A wide ceiling at head height — if it were collected, bounds would span ±5.
|
|
97
|
+
mountNode(ceiling, [10, 0.1, 10], [0, 2.5, 0])
|
|
98
|
+
|
|
99
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
100
|
+
|
|
101
|
+
expect(world).not.toBeNull()
|
|
102
|
+
// Bounds reflect only the 1×1 column; the ceiling contributed no geometry.
|
|
103
|
+
expect(world?.bounds?.min.x).toBeCloseTo(-0.5)
|
|
104
|
+
expect(world?.bounds?.max.x).toBeCloseTo(0.5)
|
|
105
|
+
world?.dispose()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('leaves elevators to their dedicated dynamic collider meshes', () => {
|
|
109
|
+
registerColliderDefinition('elevator', ElevatorNode, 'structure')
|
|
110
|
+
|
|
111
|
+
const elevator = ElevatorNode.parse({ id: 'elevator_test' })
|
|
112
|
+
setSceneNodes([elevator])
|
|
113
|
+
mountNode(elevator, [2, 3, 2], [0, 1.5, 0])
|
|
114
|
+
|
|
115
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
116
|
+
|
|
117
|
+
expect(world).toBeNull()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('adds a fallback floor for a visible level with no slab', () => {
|
|
121
|
+
const level = LevelNode.parse({ id: 'level_test', level: 0 })
|
|
122
|
+
setSceneNodes([level])
|
|
123
|
+
mountRegistryGroup(level)
|
|
124
|
+
|
|
125
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
126
|
+
|
|
127
|
+
expect(world).not.toBeNull()
|
|
128
|
+
expect(world?.bounds?.min.y).toBeCloseTo(-0.08)
|
|
129
|
+
expect(world?.bounds?.max.y).toBeCloseTo(0)
|
|
130
|
+
world?.dispose()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('adds a site ground collider so a spawn on bare ground has a floor', () => {
|
|
134
|
+
const site = SiteNode.parse({ id: 'site_test' })
|
|
135
|
+
setSceneNodes([site])
|
|
136
|
+
mountRegistryGroup(site)
|
|
137
|
+
|
|
138
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
139
|
+
|
|
140
|
+
expect(world).not.toBeNull()
|
|
141
|
+
// Ground slab sits just below the site ground plane (y = 0).
|
|
142
|
+
expect(world?.bounds?.min.y).toBeCloseTo(-0.08)
|
|
143
|
+
expect(world?.bounds?.max.y).toBeCloseTo(0)
|
|
144
|
+
// Default site footprint falls back to the 30 m minimum size.
|
|
145
|
+
expect(world?.bounds?.min.x).toBeCloseTo(-15)
|
|
146
|
+
expect(world?.bounds?.max.x).toBeCloseTo(15)
|
|
147
|
+
world?.dispose()
|
|
148
|
+
})
|
|
149
|
+
})
|