@pascal-app/editor 0.9.1 → 0.9.2
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 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Icon } from '@iconify/react'
|
|
4
|
+
import { type IconRef, useScene } from '@pascal-app/core'
|
|
5
|
+
import { ChevronLeft, ChevronRight, ExternalLink, Puzzle } from 'lucide-react'
|
|
6
|
+
import { lazy, type ReactNode, Suspense, useState, useSyncExternalStore } from 'react'
|
|
7
|
+
import { editorHostPanelRegistry } from '../../../../lib/plugin-panels'
|
|
8
|
+
import { Button } from '../../primitives/button'
|
|
9
|
+
|
|
10
|
+
const PLUGIN_AUTHORING_URL =
|
|
11
|
+
'https://editor.pascal.app/docs/developers/plugins'
|
|
12
|
+
|
|
13
|
+
function renderPluginIcon(ref: IconRef): ReactNode {
|
|
14
|
+
if (ref.kind === 'url') {
|
|
15
|
+
return <img alt="" className="h-8 w-8 object-contain" src={ref.src} />
|
|
16
|
+
}
|
|
17
|
+
if (ref.kind === 'iconify') {
|
|
18
|
+
return <Icon height={28} icon={ref.name} width={28} />
|
|
19
|
+
}
|
|
20
|
+
if (ref.kind === 'svg') {
|
|
21
|
+
return (
|
|
22
|
+
<svg height={28} viewBox={ref.viewBox} width={28}>
|
|
23
|
+
<path d={ref.path} fill="currentColor" />
|
|
24
|
+
</svg>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
const LazyIcon = lazy(ref.module)
|
|
28
|
+
return (
|
|
29
|
+
<Suspense fallback={<Puzzle className="h-7 w-7" />}>
|
|
30
|
+
<LazyIcon />
|
|
31
|
+
</Suspense>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function PluginsPanel() {
|
|
36
|
+
const [selectedPluginId, setSelectedPluginId] = useState<string | null>(null)
|
|
37
|
+
const panels = useSyncExternalStore(
|
|
38
|
+
editorHostPanelRegistry.subscribe,
|
|
39
|
+
editorHostPanelRegistry.getSnapshot,
|
|
40
|
+
editorHostPanelRegistry.getSnapshot,
|
|
41
|
+
)
|
|
42
|
+
const installedPlugins = useScene((state) => state.installedPlugins)
|
|
43
|
+
const setInstalledPlugins = useScene((state) => state.setInstalledPlugins)
|
|
44
|
+
const readOnly = useScene((state) => state.readOnly)
|
|
45
|
+
const plugins = Array.from(
|
|
46
|
+
new Map(
|
|
47
|
+
panels
|
|
48
|
+
.filter((panel) => panel.pluginId)
|
|
49
|
+
.map((panel) => [panel.pluginId as string, panel]),
|
|
50
|
+
).entries(),
|
|
51
|
+
)
|
|
52
|
+
const selectedPlugin = selectedPluginId
|
|
53
|
+
? plugins.find(([pluginId]) => pluginId === selectedPluginId)
|
|
54
|
+
: undefined
|
|
55
|
+
|
|
56
|
+
if (selectedPlugin) {
|
|
57
|
+
const [pluginId, panel] = selectedPlugin
|
|
58
|
+
const installed = installedPlugins.includes(pluginId)
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex h-full flex-col overflow-y-auto p-4">
|
|
62
|
+
<div>
|
|
63
|
+
<Button
|
|
64
|
+
className="rounded-full"
|
|
65
|
+
onClick={() => setSelectedPluginId(null)}
|
|
66
|
+
size="sm"
|
|
67
|
+
variant="ghost"
|
|
68
|
+
>
|
|
69
|
+
<ChevronLeft className="h-4 w-4" />
|
|
70
|
+
All plugins
|
|
71
|
+
</Button>
|
|
72
|
+
|
|
73
|
+
<div className="mt-5 flex items-start gap-4">
|
|
74
|
+
<div className="flex h-16 w-16 shrink-0 items-center justify-center rounded-2xl bg-background/60">
|
|
75
|
+
{renderPluginIcon(panel.icon)}
|
|
76
|
+
</div>
|
|
77
|
+
<div className="min-w-0 pt-1">
|
|
78
|
+
<h2 className="font-semibold text-lg text-sidebar-foreground">{panel.label}</h2>
|
|
79
|
+
<p className="text-sidebar-foreground/50 text-sm">
|
|
80
|
+
{installed ? 'Installed' : 'Not installed'}
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<p className="mt-5 text-sidebar-foreground/70 text-sm">
|
|
86
|
+
{panel.description ?? 'Adds a new tool panel to the editor.'}
|
|
87
|
+
</p>
|
|
88
|
+
|
|
89
|
+
<dl className="mt-6 divide-y divide-border/50 rounded-xl border border-border/60">
|
|
90
|
+
<div className="p-3">
|
|
91
|
+
<dt className="text-sidebar-foreground/50 text-xs">Plugin ID</dt>
|
|
92
|
+
<dd className="mt-1 break-all text-sidebar-foreground text-sm">{pluginId}</dd>
|
|
93
|
+
</div>
|
|
94
|
+
{panel.creator && (
|
|
95
|
+
<div className="p-3">
|
|
96
|
+
<dt className="text-sidebar-foreground/50 text-xs">Creator</dt>
|
|
97
|
+
<dd className="mt-1 text-sm">
|
|
98
|
+
{panel.creator.url ? (
|
|
99
|
+
<a
|
|
100
|
+
className="inline-flex items-center gap-1 text-sidebar-foreground underline-offset-4 hover:underline"
|
|
101
|
+
href={panel.creator.url}
|
|
102
|
+
rel="noreferrer"
|
|
103
|
+
target="_blank"
|
|
104
|
+
>
|
|
105
|
+
{panel.creator.name}
|
|
106
|
+
<ExternalLink className="h-3 w-3" />
|
|
107
|
+
</a>
|
|
108
|
+
) : (
|
|
109
|
+
panel.creator.name
|
|
110
|
+
)}
|
|
111
|
+
</dd>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
{panel.pluginUrl && (
|
|
115
|
+
<div className="p-3">
|
|
116
|
+
<dt className="text-sidebar-foreground/50 text-xs">Plugin</dt>
|
|
117
|
+
<dd className="mt-1 text-sm">
|
|
118
|
+
<a
|
|
119
|
+
className="inline-flex items-center gap-1 text-sidebar-foreground underline-offset-4 hover:underline"
|
|
120
|
+
href={panel.pluginUrl}
|
|
121
|
+
rel="noreferrer"
|
|
122
|
+
target="_blank"
|
|
123
|
+
>
|
|
124
|
+
View plugin
|
|
125
|
+
<ExternalLink className="h-3 w-3" />
|
|
126
|
+
</a>
|
|
127
|
+
</dd>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</dl>
|
|
131
|
+
|
|
132
|
+
<Button
|
|
133
|
+
className="mt-5 rounded-full"
|
|
134
|
+
disabled={readOnly}
|
|
135
|
+
onClick={() => {
|
|
136
|
+
const next = installed
|
|
137
|
+
? installedPlugins.filter((id) => id !== pluginId)
|
|
138
|
+
: [...installedPlugins, pluginId]
|
|
139
|
+
setInstalledPlugins(next, { explicit: true })
|
|
140
|
+
}}
|
|
141
|
+
variant={installed ? 'outline' : 'default'}
|
|
142
|
+
>
|
|
143
|
+
{installed ? 'Uninstall' : 'Install'}
|
|
144
|
+
</Button>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div className="mt-auto pt-6">
|
|
148
|
+
<a
|
|
149
|
+
className="inline-flex items-center gap-1.5 text-sidebar-foreground/70 text-sm underline-offset-4 hover:text-sidebar-foreground hover:underline"
|
|
150
|
+
href={PLUGIN_AUTHORING_URL}
|
|
151
|
+
rel="noreferrer"
|
|
152
|
+
target="_blank"
|
|
153
|
+
>
|
|
154
|
+
Create a Pascal plugin
|
|
155
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
156
|
+
</a>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className="flex h-full flex-col overflow-y-auto p-4">
|
|
164
|
+
<div className="mb-5">
|
|
165
|
+
<h2 className="font-semibold text-lg text-sidebar-foreground">Plugins</h2>
|
|
166
|
+
<p className="mt-1 text-sidebar-foreground/60 text-sm">
|
|
167
|
+
Add focused tools and content to this project.
|
|
168
|
+
</p>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div className="flex flex-col gap-3">
|
|
172
|
+
{plugins.map(([pluginId, panel]) => {
|
|
173
|
+
const installed = installedPlugins.includes(pluginId)
|
|
174
|
+
return (
|
|
175
|
+
<button
|
|
176
|
+
className="w-full rounded-xl border border-border/60 bg-accent/20 p-3 text-left transition-colors hover:bg-accent/40"
|
|
177
|
+
key={pluginId}
|
|
178
|
+
onClick={() => setSelectedPluginId(pluginId)}
|
|
179
|
+
type="button"
|
|
180
|
+
>
|
|
181
|
+
<div className="flex items-start gap-3">
|
|
182
|
+
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-background/60">
|
|
183
|
+
{renderPluginIcon(panel.icon)}
|
|
184
|
+
</div>
|
|
185
|
+
<div className="min-w-0 flex-1">
|
|
186
|
+
<div className="flex items-center justify-between gap-3">
|
|
187
|
+
<div>
|
|
188
|
+
<h3 className="font-medium text-sidebar-foreground">{panel.label}</h3>
|
|
189
|
+
<p className="text-sidebar-foreground/50 text-xs">
|
|
190
|
+
{installed ? 'Installed' : 'Not installed'}
|
|
191
|
+
</p>
|
|
192
|
+
</div>
|
|
193
|
+
<ChevronRight className="h-4 w-4 shrink-0 text-sidebar-foreground/50" />
|
|
194
|
+
</div>
|
|
195
|
+
<p className="mt-2 text-sidebar-foreground/60 text-sm">
|
|
196
|
+
{panel.description ?? 'Adds a new tool panel to the editor.'}
|
|
197
|
+
</p>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</button>
|
|
201
|
+
)
|
|
202
|
+
})}
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div className="mt-auto pt-6">
|
|
206
|
+
<a
|
|
207
|
+
className="inline-flex items-center gap-1.5 text-sidebar-foreground/70 text-sm underline-offset-4 hover:text-sidebar-foreground hover:underline"
|
|
208
|
+
href={PLUGIN_AUTHORING_URL}
|
|
209
|
+
rel="noreferrer"
|
|
210
|
+
target="_blank"
|
|
211
|
+
>
|
|
212
|
+
Create a Pascal plugin
|
|
213
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
214
|
+
</a>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { emitter, useScene, validateBuildJson } from '@pascal-app/core'
|
|
1
|
+
import { clearSceneHistory, emitter, useScene, validateBuildJson } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { TreeView, VisualJson } from '@visual-json/react'
|
|
4
|
-
import { Camera, Download, Save, Trash2, Upload } from 'lucide-react'
|
|
4
|
+
import { Camera, Download, Map as MapIcon, Save, Trash2, Upload } from 'lucide-react'
|
|
5
5
|
import {
|
|
6
6
|
type KeyboardEvent,
|
|
7
7
|
type SyntheticEvent,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
useRef,
|
|
11
11
|
useState,
|
|
12
12
|
} from 'react'
|
|
13
|
+
import { exportFloorplanPdf } from '../../../../../lib/floorplan/floorplan-export'
|
|
13
14
|
import { Button } from './../../../../../components/ui/primitives/button'
|
|
14
15
|
import {
|
|
15
16
|
Dialog,
|
|
@@ -179,11 +180,11 @@ export function SettingsPanel({
|
|
|
179
180
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
180
181
|
const nodes = useScene((state) => state.nodes)
|
|
181
182
|
const rootNodeIds = useScene((state) => state.rootNodeIds)
|
|
183
|
+
const installedPlugins = useScene((state) => state.installedPlugins)
|
|
182
184
|
const setScene = useScene((state) => state.setScene)
|
|
183
185
|
const clearScene = useScene((state) => state.clearScene)
|
|
184
186
|
const resetSelection = useViewer((state) => state.resetSelection)
|
|
185
187
|
const exportScene = useViewer((state) => state.exportScene)
|
|
186
|
-
const showGrid = useViewer((state) => state.showGrid)
|
|
187
188
|
const shadows = useViewer((state) => state.shadows)
|
|
188
189
|
const setPhase = useEditor((state) => state.setPhase)
|
|
189
190
|
const [isGeneratingThumbnail, setIsGeneratingThumbnail] = useState(false)
|
|
@@ -206,7 +207,7 @@ export function SettingsPanel({
|
|
|
206
207
|
const isLocalProject = false // Props-based; only show cloud sections when projectId provided
|
|
207
208
|
|
|
208
209
|
const handleSaveBuild = () => {
|
|
209
|
-
const sceneData = { nodes, rootNodeIds }
|
|
210
|
+
const sceneData = { nodes, rootNodeIds, installedPlugins }
|
|
210
211
|
const json = JSON.stringify(sceneData, null, 2)
|
|
211
212
|
const blob = new Blob([json], { type: 'application/json' })
|
|
212
213
|
const url = URL.createObjectURL(blob)
|
|
@@ -262,11 +263,24 @@ export function SettingsPanel({
|
|
|
262
263
|
e.target.value = ''
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
const handleConfirmImport = (parsed: {
|
|
266
|
+
const handleConfirmImport = (parsed: {
|
|
267
|
+
nodes: Record<string, unknown>
|
|
268
|
+
rootNodeIds: string[]
|
|
269
|
+
installedPlugins?: string[]
|
|
270
|
+
}) => {
|
|
271
|
+
const currentScene = useScene.getState()
|
|
266
272
|
setScene(
|
|
267
273
|
parsed.nodes as Parameters<typeof setScene>[0],
|
|
268
274
|
parsed.rootNodeIds as Parameters<typeof setScene>[1],
|
|
275
|
+
{
|
|
276
|
+
installedPlugins: parsed.installedPlugins ?? currentScene.installedPlugins,
|
|
277
|
+
hasExplicitPluginInstallState:
|
|
278
|
+
parsed.installedPlugins !== undefined || currentScene.hasExplicitPluginInstallState,
|
|
279
|
+
},
|
|
269
280
|
)
|
|
281
|
+
// An import is a scene load: it becomes the undo floor. Without this,
|
|
282
|
+
// undo could step back into the pre-import scene state.
|
|
283
|
+
clearSceneHistory()
|
|
270
284
|
resetSelection()
|
|
271
285
|
setPhase('site')
|
|
272
286
|
setPendingImport(null)
|
|
@@ -274,6 +288,9 @@ export function SettingsPanel({
|
|
|
274
288
|
|
|
275
289
|
const handleResetToDefault = () => {
|
|
276
290
|
clearScene()
|
|
291
|
+
// Same floor rule as import — undo after a reset must not resurrect the
|
|
292
|
+
// old scene (or land on the empty intermediate `unloadScene` state).
|
|
293
|
+
clearSceneHistory()
|
|
277
294
|
resetSelection()
|
|
278
295
|
setPhase('structure')
|
|
279
296
|
selectDefaultBuildingAndLevel()
|
|
@@ -331,16 +348,6 @@ export function SettingsPanel({
|
|
|
331
348
|
onCheckedChange={(checked) => handleVisibilityChange('showGuidesPublic', checked)}
|
|
332
349
|
/>
|
|
333
350
|
</div>
|
|
334
|
-
<div className="flex items-center justify-between">
|
|
335
|
-
<div>
|
|
336
|
-
<div className="font-medium text-sm">Show Grid</div>
|
|
337
|
-
<div className="text-muted-foreground text-xs">Visible only in the editor</div>
|
|
338
|
-
</div>
|
|
339
|
-
<Switch
|
|
340
|
-
checked={showGrid}
|
|
341
|
-
onCheckedChange={(checked) => useViewer.getState().setShowGrid(checked)}
|
|
342
|
-
/>
|
|
343
|
-
</div>
|
|
344
351
|
<div className="flex items-center justify-between">
|
|
345
352
|
<div>
|
|
346
353
|
<div className="font-medium text-sm">Shadows</div>
|
|
@@ -355,32 +362,56 @@ export function SettingsPanel({
|
|
|
355
362
|
)}
|
|
356
363
|
|
|
357
364
|
{/* Export Section */}
|
|
358
|
-
<div className="space-y-
|
|
365
|
+
<div className="space-y-4">
|
|
359
366
|
<label className="font-medium text-muted-foreground text-xs uppercase">Export</label>
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
367
|
+
|
|
368
|
+
<div className="space-y-2">
|
|
369
|
+
<div className="font-medium text-muted-foreground text-xs">3D model</div>
|
|
370
|
+
<Button
|
|
371
|
+
className="w-full justify-start gap-2"
|
|
372
|
+
onClick={() => exportScene?.('glb')}
|
|
373
|
+
variant="outline"
|
|
374
|
+
>
|
|
375
|
+
<Download className="size-4" />
|
|
376
|
+
Export GLB
|
|
377
|
+
</Button>
|
|
378
|
+
<Button
|
|
379
|
+
className="w-full justify-start gap-2"
|
|
380
|
+
onClick={() => exportScene?.('stl')}
|
|
381
|
+
variant="outline"
|
|
382
|
+
>
|
|
383
|
+
<Download className="size-4" />
|
|
384
|
+
Export STL
|
|
385
|
+
</Button>
|
|
386
|
+
<Button
|
|
387
|
+
className="w-full justify-start gap-2"
|
|
388
|
+
onClick={() => exportScene?.('obj')}
|
|
389
|
+
variant="outline"
|
|
390
|
+
>
|
|
391
|
+
<Download className="size-4" />
|
|
392
|
+
Export OBJ
|
|
393
|
+
</Button>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<div className="space-y-2">
|
|
397
|
+
<div className="font-medium text-muted-foreground text-xs">Floorplan</div>
|
|
398
|
+
<Button
|
|
399
|
+
className="w-full justify-start gap-2"
|
|
400
|
+
onClick={() => exportFloorplanPdf('full')}
|
|
401
|
+
variant="outline"
|
|
402
|
+
>
|
|
403
|
+
<MapIcon className="size-4" />
|
|
404
|
+
Full floorplan
|
|
405
|
+
</Button>
|
|
406
|
+
<Button
|
|
407
|
+
className="w-full justify-start gap-2"
|
|
408
|
+
onClick={() => exportFloorplanPdf('structure')}
|
|
409
|
+
variant="outline"
|
|
410
|
+
>
|
|
411
|
+
<MapIcon className="size-4" />
|
|
412
|
+
Structure only
|
|
413
|
+
</Button>
|
|
414
|
+
</div>
|
|
384
415
|
</div>
|
|
385
416
|
|
|
386
417
|
{/* Thumbnail Section (only for cloud projects) */}
|
|
@@ -55,6 +55,7 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
|
|
|
55
55
|
shortcuts: [
|
|
56
56
|
{ keys: ['V'], action: 'Switch to Select mode' },
|
|
57
57
|
{ keys: ['B'], action: 'Switch to Build mode' },
|
|
58
|
+
{ keys: ['M'], action: 'Activate the last measurement tool' },
|
|
58
59
|
{ keys: ['X'], action: 'Switch to Delete mode' },
|
|
59
60
|
{
|
|
60
61
|
keys: ['Esc'],
|
|
@@ -71,7 +72,48 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
|
|
|
71
72
|
{
|
|
72
73
|
keys: ['Cmd/Ctrl', 'Left click'],
|
|
73
74
|
action: 'Add or remove an object from multi-selection',
|
|
74
|
-
note: 'Works
|
|
75
|
+
note: 'Works in Select mode on the 3D canvas, the 2D floor plan, and the scene graph.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
keys: ['Shift', 'Left click'],
|
|
79
|
+
action: 'Add or remove an object from canvas multi-selection',
|
|
80
|
+
note: 'In the scene graph, Shift-click selects the visible range like a file browser.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
keys: ['Left click'],
|
|
84
|
+
action: 'Move the whole multi-selection',
|
|
85
|
+
note:
|
|
86
|
+
'With 2+ objects selected, in 2D and 3D alike: drag the selection (or its dashed box) to slide it; click it to pick it up and place with the next click.',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
keys: ['R', 'T'],
|
|
90
|
+
action: 'Rotate a multi-selection ±45° around its center',
|
|
91
|
+
note: 'Also works mid-move while carrying the selection.',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
keys: ['Esc'],
|
|
95
|
+
action: 'Clear the selection',
|
|
96
|
+
note: 'Clicking empty space does the same.',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
title: 'Direct Manipulation',
|
|
102
|
+
shortcuts: [
|
|
103
|
+
{
|
|
104
|
+
keys: ['Cmd/Ctrl', 'Left click'],
|
|
105
|
+
action: 'Move the selected movable object under the cursor',
|
|
106
|
+
note: 'Drag in Select mode with a single object selected. Guided snapping and guides are enabled by default.',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
keys: ['Cmd/Ctrl', 'Right click'],
|
|
110
|
+
action: 'Rotate the selected object under the cursor',
|
|
111
|
+
note: 'Drag left or right in Select mode with a single object selected. Rotation snaps to 15° increments by default.',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
keys: ['Cmd/Ctrl', 'Shift', 'Right click'],
|
|
115
|
+
action: 'Rotate freely',
|
|
116
|
+
note: 'Hold Shift during the drag to bypass the 15° rotation increment.',
|
|
75
117
|
},
|
|
76
118
|
],
|
|
77
119
|
},
|
|
@@ -80,16 +122,27 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
|
|
|
80
122
|
shortcuts: [
|
|
81
123
|
{
|
|
82
124
|
keys: ['Shift'],
|
|
83
|
-
action: '
|
|
84
|
-
note: 'Hold
|
|
125
|
+
action: 'Bypass guided snapping and angle constraints',
|
|
126
|
+
note: 'Hold during the active gesture. Passive guide or measurement feedback may stay visible.',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
keys: ['Shift'],
|
|
130
|
+
action: 'Rotate freely, bypassing the default 15° rotation snap',
|
|
131
|
+
note: 'Hold while dragging a rotate handle or direct-rotation gesture.',
|
|
85
132
|
},
|
|
86
133
|
],
|
|
87
134
|
},
|
|
88
135
|
{
|
|
89
136
|
title: 'Item Placement',
|
|
90
137
|
shortcuts: [
|
|
91
|
-
{
|
|
92
|
-
|
|
138
|
+
{
|
|
139
|
+
keys: ['R', 'T'],
|
|
140
|
+
action: 'Rotate item; with a door selected, R toggles open/closed and T closes',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
keys: ['E'],
|
|
144
|
+
action: 'Operate the selected node — doors, windows, and cabinet doors/drawers animate open/closed',
|
|
145
|
+
},
|
|
93
146
|
{
|
|
94
147
|
keys: ['Shift'],
|
|
95
148
|
action: 'Temporarily bypass placement validation constraints',
|
|
@@ -157,7 +210,8 @@ export function KeyboardShortcutsDialog() {
|
|
|
157
210
|
<DialogHeader className="shrink-0 border-b px-6 py-4">
|
|
158
211
|
<DialogTitle>Keyboard Shortcuts</DialogTitle>
|
|
159
212
|
<DialogDescription>
|
|
160
|
-
Shortcuts are context-aware
|
|
213
|
+
Shortcuts are context-aware. Guided constraints are enabled by default; hold Shift
|
|
214
|
+
during an active gesture to build freely.
|
|
161
215
|
</DialogDescription>
|
|
162
216
|
</DialogHeader>
|
|
163
217
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BuildStats, SchemaIssue, ValidateBuildJsonResult } from '@pascal-app/core'
|
|
2
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
2
3
|
import {
|
|
3
4
|
AlertTriangle,
|
|
4
5
|
AppWindow,
|
|
@@ -22,6 +23,11 @@ import {
|
|
|
22
23
|
DialogHeader,
|
|
23
24
|
DialogTitle,
|
|
24
25
|
} from '../../../../../components/ui/primitives/dialog'
|
|
26
|
+
import {
|
|
27
|
+
getAreaUnitLabel,
|
|
28
|
+
type LinearUnit,
|
|
29
|
+
squareMetersToAreaUnit,
|
|
30
|
+
} from '../../../../../lib/measurements'
|
|
25
31
|
|
|
26
32
|
export type PendingImport = {
|
|
27
33
|
fileName: string
|
|
@@ -80,15 +86,17 @@ function formatFileSize(bytes: number): string {
|
|
|
80
86
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
function formatFloorArea(m2: number): string {
|
|
89
|
+
function formatFloorArea(m2: number, unit: LinearUnit): string {
|
|
84
90
|
if (m2 === 0) return '—'
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
const value = squareMetersToAreaUnit(m2, unit)
|
|
92
|
+
const label = getAreaUnitLabel(unit)
|
|
93
|
+
return `${value < 10 ? value.toFixed(2) : value.toFixed(1)} ${label}`
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) {
|
|
90
97
|
const [showAllWarnings, setShowAllWarnings] = useState(false)
|
|
91
98
|
const [showSchemaIssues, setShowSchemaIssues] = useState(false)
|
|
99
|
+
const unit = useViewer((state) => state.unit)
|
|
92
100
|
|
|
93
101
|
if (!pending) return null
|
|
94
102
|
|
|
@@ -165,7 +173,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) {
|
|
|
165
173
|
<div className="flex items-center justify-between border-t px-3 py-2">
|
|
166
174
|
<span className="text-muted-foreground text-sm">Floor area</span>
|
|
167
175
|
<span className="font-medium text-sm">
|
|
168
|
-
{formatFloorArea(stats.floorAreaM2)}
|
|
176
|
+
{formatFloorArea(stats.floorAreaM2, unit)}
|
|
169
177
|
</span>
|
|
170
178
|
</div>
|
|
171
179
|
)}
|
|
@@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
3
3
|
import Image from 'next/image'
|
|
4
4
|
import { memo, useCallback, useEffect, useState } from 'react'
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow'
|
|
6
|
+
import { formatAreaLabel } from './../../../../../lib/measurements'
|
|
6
7
|
import useEditor from './../../../../../store/use-editor'
|
|
7
8
|
import { InlineRenameInput } from './inline-rename-input'
|
|
8
9
|
import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
|
|
@@ -32,6 +33,7 @@ export const CeilingTreeNode = memo(function CeilingTreeNode({
|
|
|
32
33
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
33
34
|
const setSelection = useViewer((state) => state.setSelection)
|
|
34
35
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
36
|
+
const unit = useViewer((state) => state.unit)
|
|
35
37
|
|
|
36
38
|
// Expand when a descendant is selected — imperative to avoid subscribing to the full selectedIds array
|
|
37
39
|
useEffect(() => {
|
|
@@ -75,8 +77,7 @@ export const CeilingTreeNode = memo(function CeilingTreeNode({
|
|
|
75
77
|
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
76
78
|
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
77
79
|
|
|
78
|
-
const
|
|
79
|
-
const defaultName = `Ceiling (${area}m²)`
|
|
80
|
+
const defaultName = `Ceiling (${formatAreaLabel(calculatePolygonArea(polygon), unit)})`
|
|
80
81
|
|
|
81
82
|
return (
|
|
82
83
|
<TreeNodeWrapper
|
|
@@ -85,7 +86,7 @@ export const CeilingTreeNode = memo(function CeilingTreeNode({
|
|
|
85
86
|
expanded={expanded}
|
|
86
87
|
hasChildren={children.length > 0}
|
|
87
88
|
icon={
|
|
88
|
-
<Image alt="" className="object-contain" height={14} src="/icons/ceiling.
|
|
89
|
+
<Image alt="" className="object-contain" height={14} src="/icons/ceiling.webp" width={14} />
|
|
89
90
|
}
|
|
90
91
|
isHovered={isHovered}
|
|
91
92
|
isLast={isLast}
|
|
@@ -2,6 +2,7 @@ import { type AnyNodeId, type ChimneyNode, useScene } from '@pascal-app/core'
|
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import Image from 'next/image'
|
|
4
4
|
import { memo, useCallback, useState } from 'react'
|
|
5
|
+
import { SnapTargetIcon } from '../../../snap-target-badge'
|
|
5
6
|
import useEditor from './../../../../../store/use-editor'
|
|
6
7
|
import { InlineRenameInput } from './inline-rename-input'
|
|
7
8
|
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
@@ -51,13 +52,15 @@ export const ChimneyTreeNode = memo(function ChimneyTreeNode({
|
|
|
51
52
|
expanded={false}
|
|
52
53
|
hasChildren={false}
|
|
53
54
|
icon={
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
<SnapTargetIcon target="roof">
|
|
56
|
+
<Image
|
|
57
|
+
alt=""
|
|
58
|
+
className="object-contain opacity-60"
|
|
59
|
+
height={14}
|
|
60
|
+
src="/icons/roof.webp"
|
|
61
|
+
width={14}
|
|
62
|
+
/>
|
|
63
|
+
</SnapTargetIcon>
|
|
61
64
|
}
|
|
62
65
|
isHovered={isHovered}
|
|
63
66
|
isLast={isLast}
|
|
@@ -51,7 +51,7 @@ export const ColumnTreeNode = memo(function ColumnTreeNode({
|
|
|
51
51
|
expanded={false}
|
|
52
52
|
hasChildren={false}
|
|
53
53
|
icon={
|
|
54
|
-
<Image alt="" className="object-contain" height={14} src="/icons/column.
|
|
54
|
+
<Image alt="" className="object-contain" height={14} src="/icons/column.webp" width={14} />
|
|
55
55
|
}
|
|
56
56
|
isHovered={isHovered}
|
|
57
57
|
isLast={isLast}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
3
|
+
import { type AnyNodeId, type DoorNode, useScene } from '@pascal-app/core'
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import Image from 'next/image'
|
|
6
6
|
import { memo, useCallback, useState } from 'react'
|
|
7
|
+
import { resolveNodeSnapTarget, SnapTargetIcon } from '../../../snap-target-badge'
|
|
7
8
|
import useEditor from './../../../../../store/use-editor'
|
|
8
9
|
import { InlineRenameInput } from './inline-rename-input'
|
|
9
10
|
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
@@ -22,6 +23,7 @@ export const DoorTreeNode = memo(function DoorTreeNode({
|
|
|
22
23
|
}: DoorTreeNodeProps) {
|
|
23
24
|
const [isEditing, setIsEditing] = useState(false)
|
|
24
25
|
const isVisible = useScene((s) => s.nodes[nodeId as AnyNodeId]?.visible !== false)
|
|
26
|
+
const node = useScene((s) => s.nodes[nodeId] as DoorNode | undefined)
|
|
25
27
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
26
28
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
27
29
|
const setSelection = useViewer((state) => state.setSelection)
|
|
@@ -45,6 +47,7 @@ export const DoorTreeNode = memo(function DoorTreeNode({
|
|
|
45
47
|
|
|
46
48
|
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
47
49
|
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
50
|
+
const snapTarget = resolveNodeSnapTarget(node) ?? 'wall'
|
|
48
51
|
|
|
49
52
|
return (
|
|
50
53
|
<TreeNodeWrapper
|
|
@@ -53,7 +56,9 @@ export const DoorTreeNode = memo(function DoorTreeNode({
|
|
|
53
56
|
expanded={false}
|
|
54
57
|
hasChildren={false}
|
|
55
58
|
icon={
|
|
56
|
-
<
|
|
59
|
+
<SnapTargetIcon target={snapTarget}>
|
|
60
|
+
<Image alt="" className="object-contain" height={14} src="/icons/door.webp" width={14} />
|
|
61
|
+
</SnapTargetIcon>
|
|
57
62
|
}
|
|
58
63
|
isHovered={isHovered}
|
|
59
64
|
isLast={isLast}
|