@pascal-app/editor 0.8.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 +8 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +754 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +58 -51
- 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 +201 -83
- package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
- package/src/components/editor/first-person-controls.tsx +825 -20
- 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 +5829 -12005
- 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 +178 -149
- 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 +433 -156
- 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 +136 -62
- package/src/components/editor/thumbnail-generator.tsx +9 -3
- package/src/components/editor/use-floorplan-background-placement.ts +119 -32
- 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-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- 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 +24 -9
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
- 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 +1 -1
- 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 +41 -105
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +31 -5
- package/src/components/tools/item/placement-strategies.ts +190 -15
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
- 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 -538
- 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 +56 -0
- 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 +16 -6
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/control-modes.tsx +5 -82
- package/src/components/ui/action-menu/index.tsx +1 -113
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +243 -6
- package/src/components/ui/command-palette/editor-commands.tsx +18 -13
- package/src/components/ui/command-palette/index.tsx +2 -1
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +11 -32
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/helper-manager.tsx +16 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/panel-manager.tsx +35 -47
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- 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 +4 -6
- 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 +7 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +61 -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 +10 -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 +115 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +237 -5
- 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.ts +54 -42
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +211 -19
- 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 +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +23 -1
- package/src/lib/sfx-player.ts +70 -22
- package/src/lib/site-boundary.ts +1 -0
- 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 +320 -37
- 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/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- 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 -264
- 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 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- 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 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- 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 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- 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 -280
- 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 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
'use
|
|
2
|
-
|
|
3
|
-
import NextImage from 'next/image'
|
|
4
|
-
import { useContextualTools } from '../../../hooks/use-contextual-tools'
|
|
5
|
-
|
|
6
|
-
import { cn } from '../../../lib/utils'
|
|
7
|
-
import useEditor, {
|
|
8
|
-
type CatalogCategory,
|
|
9
|
-
type StructureTool,
|
|
10
|
-
type Tool,
|
|
11
|
-
} from '../../../store/use-editor'
|
|
12
|
-
import { ActionButton } from './action-button'
|
|
1
|
+
import type { CatalogCategory, StructureTool } from '../../../store/use-editor'
|
|
13
2
|
|
|
14
3
|
export type ToolConfig = {
|
|
15
4
|
id: StructureTool
|
|
@@ -18,86 +7,22 @@ export type ToolConfig = {
|
|
|
18
7
|
catalogCategory?: CatalogCategory
|
|
19
8
|
}
|
|
20
9
|
|
|
10
|
+
// Shared structure-tool metadata (icons + labels). The build palette now lives
|
|
11
|
+
// in the community Build sidebar; this list survives only as the lookup table
|
|
12
|
+
// for cursor/floorplan indicators. Roof-mounted accessories are intentionally
|
|
13
|
+
// absent — they're placed from the roof inspector's "Add element" section.
|
|
21
14
|
export const tools: ToolConfig[] = [
|
|
22
15
|
{ id: 'wall', iconSrc: '/icons/wall.png', label: 'Wall' },
|
|
23
|
-
// { id: 'room', iconSrc: '/icons/room.png', label: 'Room' },
|
|
24
|
-
// { id: 'custom-room', iconSrc: '/icons/custom-room.png', label: 'Custom Room' },
|
|
25
|
-
{ id: 'slab', iconSrc: '/icons/floor.png', label: 'Slab' },
|
|
26
|
-
{ id: 'ceiling', iconSrc: '/icons/ceiling.png', label: 'Ceiling' },
|
|
27
|
-
{ id: 'column', iconSrc: '/icons/column.png', label: 'Column' },
|
|
28
|
-
{ id: 'roof', iconSrc: '/icons/roof.png', label: 'Gable Roof' },
|
|
29
|
-
{ id: 'stair', iconSrc: '/icons/stairs.png', label: 'Stairs' },
|
|
30
16
|
{ id: 'door', iconSrc: '/icons/door.png', label: 'Door' },
|
|
31
17
|
{ id: 'window', iconSrc: '/icons/window.png', label: 'Window' },
|
|
18
|
+
{ id: 'stair', iconSrc: '/icons/stairs.png', label: 'Stairs' },
|
|
19
|
+
{ id: 'roof', iconSrc: '/icons/roof.png', label: 'Gable Roof' },
|
|
32
20
|
{ id: 'fence', iconSrc: '/icons/fence.png', label: 'Fence' },
|
|
21
|
+
{ id: 'column', iconSrc: '/icons/column.png', label: 'Column' },
|
|
22
|
+
{ id: 'elevator', iconSrc: '/icons/elevator.png', label: 'Elevator' },
|
|
23
|
+
{ id: 'slab', iconSrc: '/icons/floor.png', label: 'Slab' },
|
|
24
|
+
{ id: 'ceiling', iconSrc: '/icons/ceiling.png', label: 'Ceiling' },
|
|
33
25
|
{ id: 'zone', iconSrc: '/icons/zone.png', label: 'Zone' },
|
|
34
|
-
{ id: 'spawn', iconSrc: '/icons/
|
|
26
|
+
{ id: 'spawn', iconSrc: '/icons/spawn-point.png', label: 'Spawn Point' },
|
|
27
|
+
{ id: 'shelf', iconSrc: '/icons/shelf.png', label: 'Shelf' },
|
|
35
28
|
]
|
|
36
|
-
|
|
37
|
-
export function StructureTools() {
|
|
38
|
-
const activeTool = useEditor((state) => state.tool)
|
|
39
|
-
const catalogCategory = useEditor((state) => state.catalogCategory)
|
|
40
|
-
const structureLayer = useEditor((state) => state.structureLayer)
|
|
41
|
-
const setTool = useEditor((state) => state.setTool)
|
|
42
|
-
const setCatalogCategory = useEditor((state) => state.setCatalogCategory)
|
|
43
|
-
|
|
44
|
-
const contextualTools = useContextualTools()
|
|
45
|
-
|
|
46
|
-
// Filter tools based on structureLayer
|
|
47
|
-
const visibleTools =
|
|
48
|
-
structureLayer === 'zones'
|
|
49
|
-
? tools.filter((t) => t.id === 'zone')
|
|
50
|
-
: tools.filter((t) => t.id !== 'zone')
|
|
51
|
-
|
|
52
|
-
const hasActiveTool = visibleTools.some(
|
|
53
|
-
(t) =>
|
|
54
|
-
activeTool === t.id && (t.catalogCategory ? catalogCategory === t.catalogCategory : true),
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<div className="flex items-center gap-1.5 px-1">
|
|
59
|
-
{visibleTools.map((tool, index) => {
|
|
60
|
-
// For item tools with catalog category, check both tool and category match
|
|
61
|
-
const isActive =
|
|
62
|
-
activeTool === tool.id &&
|
|
63
|
-
(tool.catalogCategory ? catalogCategory === tool.catalogCategory : true)
|
|
64
|
-
|
|
65
|
-
const isContextual = contextualTools.includes(tool.id)
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<ActionButton
|
|
69
|
-
className={cn(
|
|
70
|
-
'rounded-lg duration-300',
|
|
71
|
-
isActive
|
|
72
|
-
? 'z-10 scale-110 bg-black/40 hover:bg-black/40'
|
|
73
|
-
: 'scale-95 bg-transparent opacity-60 grayscale hover:bg-black/20 hover:opacity-100 hover:grayscale-0',
|
|
74
|
-
)}
|
|
75
|
-
key={`${tool.id}-${tool.catalogCategory ?? index}`}
|
|
76
|
-
label={tool.label}
|
|
77
|
-
onClick={() => {
|
|
78
|
-
if (!isActive) {
|
|
79
|
-
setTool(tool.id)
|
|
80
|
-
setCatalogCategory(tool.catalogCategory ?? null)
|
|
81
|
-
|
|
82
|
-
// Automatically switch to build mode if we select a tool
|
|
83
|
-
if (useEditor.getState().mode !== 'build') {
|
|
84
|
-
useEditor.getState().setMode('build')
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}}
|
|
88
|
-
size="icon"
|
|
89
|
-
variant="ghost"
|
|
90
|
-
>
|
|
91
|
-
<NextImage
|
|
92
|
-
alt={tool.label}
|
|
93
|
-
className="size-full object-contain"
|
|
94
|
-
height={28}
|
|
95
|
-
src={tool.iconSrc}
|
|
96
|
-
width={28}
|
|
97
|
-
/>
|
|
98
|
-
</ActionButton>
|
|
99
|
-
)
|
|
100
|
-
})}
|
|
101
|
-
</div>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
@@ -13,6 +13,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
13
13
|
import { Check, ChevronDown, Eye, EyeOff, Layers2, Plus, Trash2 } from 'lucide-react'
|
|
14
14
|
import { useCallback, useRef, useState } from 'react'
|
|
15
15
|
import { useShallow } from 'zustand/react/shallow'
|
|
16
|
+
import { getLevelDisplayName } from '../../../lib/level-name'
|
|
16
17
|
import { createLocalGuideImage } from '../../../lib/local-guide-image'
|
|
17
18
|
import { cn } from '../../../lib/utils'
|
|
18
19
|
import useEditor, { type GridSnapStep } from '../../../store/use-editor'
|
|
@@ -84,10 +85,6 @@ function useLowerReferenceLevels(): LevelNode[] {
|
|
|
84
85
|
)
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
function getLevelDisplayName(level: LevelNode) {
|
|
88
|
-
return level.name || `Level ${level.level}`
|
|
89
|
-
}
|
|
90
|
-
|
|
91
88
|
// ── Shared upload button for dropdowns ──────────────────────────────────────
|
|
92
89
|
|
|
93
90
|
function UploadButton({ onError }: { onError: (message: string | null) => void }) {
|
|
@@ -593,6 +590,247 @@ function ScansControl() {
|
|
|
593
590
|
)
|
|
594
591
|
}
|
|
595
592
|
|
|
593
|
+
// ── References (merged scans + guides) ──────────────────────────────────────
|
|
594
|
+
// Bottom-bar control that folds the separate Scans and Guides toggles into one
|
|
595
|
+
// "References" split button + a popover holding both, each with its own
|
|
596
|
+
// visibility toggle, upload, and per-item opacity/delete.
|
|
597
|
+
|
|
598
|
+
function ReferenceListSection({
|
|
599
|
+
title,
|
|
600
|
+
iconSrc,
|
|
601
|
+
noun,
|
|
602
|
+
emptyText,
|
|
603
|
+
nodes,
|
|
604
|
+
show,
|
|
605
|
+
setShow,
|
|
606
|
+
onError,
|
|
607
|
+
}: {
|
|
608
|
+
title: string
|
|
609
|
+
iconSrc: string
|
|
610
|
+
noun: string
|
|
611
|
+
emptyText: string
|
|
612
|
+
nodes: (GuideNode | ScanNode)[]
|
|
613
|
+
show: boolean
|
|
614
|
+
setShow: (show: boolean) => void
|
|
615
|
+
onError: (message: string | null) => void
|
|
616
|
+
}) {
|
|
617
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
618
|
+
const updateNode = useScene((state) => state.updateNode)
|
|
619
|
+
const deleteNode = useScene((state) => state.deleteNode)
|
|
620
|
+
const selectedReferenceId = useEditor((state) => state.selectedReferenceId)
|
|
621
|
+
const setSelectedReferenceId = useEditor((state) => state.setSelectedReferenceId)
|
|
622
|
+
const hasItems = nodes.length > 0
|
|
623
|
+
|
|
624
|
+
const handleSelect = useCallback(
|
|
625
|
+
(id: AnyNodeId) => {
|
|
626
|
+
setShow(true)
|
|
627
|
+
setSelectedReferenceId(id)
|
|
628
|
+
setSelection({ selectedIds: [], zoneId: null })
|
|
629
|
+
},
|
|
630
|
+
[setShow, setSelectedReferenceId, setSelection],
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
return (
|
|
634
|
+
<div className="space-y-2">
|
|
635
|
+
<div className="flex items-center gap-2">
|
|
636
|
+
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-background/80">
|
|
637
|
+
<img alt="" className="h-4 w-4 object-contain" src={iconSrc} />
|
|
638
|
+
</span>
|
|
639
|
+
<div className="min-w-0 flex-1">
|
|
640
|
+
<p className="font-medium text-foreground text-sm">{title}</p>
|
|
641
|
+
{hasItems && (
|
|
642
|
+
<p className="text-muted-foreground text-xs">
|
|
643
|
+
{nodes.length} {noun}
|
|
644
|
+
{nodes.length !== 1 ? 's' : ''} on this level
|
|
645
|
+
</p>
|
|
646
|
+
)}
|
|
647
|
+
</div>
|
|
648
|
+
<button
|
|
649
|
+
aria-label={show ? `Hide ${title.toLowerCase()}` : `Show ${title.toLowerCase()}`}
|
|
650
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground"
|
|
651
|
+
onClick={() => setShow(!show)}
|
|
652
|
+
type="button"
|
|
653
|
+
>
|
|
654
|
+
{show ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
|
|
655
|
+
</button>
|
|
656
|
+
<UploadButton onError={onError} />
|
|
657
|
+
</div>
|
|
658
|
+
|
|
659
|
+
{hasItems ? (
|
|
660
|
+
<div className="max-h-40 space-y-2 overflow-y-auto pr-1">
|
|
661
|
+
{nodes.map((node, index) => (
|
|
662
|
+
<div
|
|
663
|
+
className={cn(
|
|
664
|
+
'group/item space-y-2 rounded-xl border bg-background/75 p-2.5 transition-colors',
|
|
665
|
+
selectedReferenceId === node.id
|
|
666
|
+
? 'border-foreground/35 bg-white/10'
|
|
667
|
+
: 'border-border/45',
|
|
668
|
+
)}
|
|
669
|
+
key={node.id}
|
|
670
|
+
>
|
|
671
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
672
|
+
<button
|
|
673
|
+
className="flex min-w-0 flex-1 items-center gap-2 text-left"
|
|
674
|
+
onClick={() => handleSelect(node.id)}
|
|
675
|
+
type="button"
|
|
676
|
+
>
|
|
677
|
+
<img
|
|
678
|
+
alt=""
|
|
679
|
+
className="h-3.5 w-3.5 shrink-0 object-contain opacity-70"
|
|
680
|
+
src={iconSrc}
|
|
681
|
+
/>
|
|
682
|
+
<p className="truncate font-medium text-foreground text-sm">
|
|
683
|
+
{node.name || `${noun.charAt(0).toUpperCase()}${noun.slice(1)} ${index + 1}`}
|
|
684
|
+
</p>
|
|
685
|
+
{selectedReferenceId === node.id && (
|
|
686
|
+
<Check className="ml-auto h-3.5 w-3.5 shrink-0 text-foreground/80" />
|
|
687
|
+
)}
|
|
688
|
+
</button>
|
|
689
|
+
<button
|
|
690
|
+
aria-label={`Delete ${noun}`}
|
|
691
|
+
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-muted-foreground/50 opacity-0 transition-all hover:bg-destructive/10 hover:text-destructive group-hover/item:opacity-100"
|
|
692
|
+
onClick={(event) => {
|
|
693
|
+
event.stopPropagation()
|
|
694
|
+
deleteNode(node.id)
|
|
695
|
+
if (selectedReferenceId === node.id) {
|
|
696
|
+
setSelectedReferenceId(null)
|
|
697
|
+
}
|
|
698
|
+
}}
|
|
699
|
+
type="button"
|
|
700
|
+
>
|
|
701
|
+
<Trash2 className="h-3 w-3" />
|
|
702
|
+
</button>
|
|
703
|
+
</div>
|
|
704
|
+
<SliderControl
|
|
705
|
+
label="Opacity"
|
|
706
|
+
max={100}
|
|
707
|
+
min={0}
|
|
708
|
+
onChange={(value) =>
|
|
709
|
+
updateNode(node.id, { opacity: Math.round(Math.min(100, Math.max(0, value))) })
|
|
710
|
+
}
|
|
711
|
+
precision={0}
|
|
712
|
+
step={1}
|
|
713
|
+
unit="%"
|
|
714
|
+
value={node.opacity}
|
|
715
|
+
/>
|
|
716
|
+
</div>
|
|
717
|
+
))}
|
|
718
|
+
</div>
|
|
719
|
+
) : (
|
|
720
|
+
<div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-3 text-muted-foreground text-sm">
|
|
721
|
+
{emptyText}
|
|
722
|
+
</div>
|
|
723
|
+
)}
|
|
724
|
+
</div>
|
|
725
|
+
)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function ReferencesControl() {
|
|
729
|
+
const showScans = useViewer((state) => state.showScans)
|
|
730
|
+
const setShowScans = useViewer((state) => state.setShowScans)
|
|
731
|
+
const showGuides = useViewer((state) => state.showGuides)
|
|
732
|
+
const setShowGuides = useViewer((state) => state.setShowGuides)
|
|
733
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
734
|
+
const [uploadError, setUploadError] = useState<string | null>(null)
|
|
735
|
+
|
|
736
|
+
const scans = useLevelScans()
|
|
737
|
+
const guides = useLevelGuides()
|
|
738
|
+
const total = scans.length + guides.length
|
|
739
|
+
const anyVisible = showScans || showGuides
|
|
740
|
+
|
|
741
|
+
const toggleAll = useCallback(() => {
|
|
742
|
+
const next = !anyVisible
|
|
743
|
+
setShowScans(next)
|
|
744
|
+
setShowGuides(next)
|
|
745
|
+
}, [anyVisible, setShowScans, setShowGuides])
|
|
746
|
+
|
|
747
|
+
return (
|
|
748
|
+
<Popover onOpenChange={setIsOpen} open={isOpen}>
|
|
749
|
+
<div className="flex items-center">
|
|
750
|
+
<ActionButton
|
|
751
|
+
className={cn(
|
|
752
|
+
'rounded-r-none p-0',
|
|
753
|
+
anyVisible
|
|
754
|
+
? 'bg-white/15'
|
|
755
|
+
: 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0',
|
|
756
|
+
)}
|
|
757
|
+
label={`References: ${anyVisible ? 'Visible' : 'Hidden'}`}
|
|
758
|
+
onClick={toggleAll}
|
|
759
|
+
size="icon"
|
|
760
|
+
variant="ghost"
|
|
761
|
+
>
|
|
762
|
+
<div className="relative">
|
|
763
|
+
<img
|
|
764
|
+
alt="References"
|
|
765
|
+
className="h-[28px] w-[28px] object-contain"
|
|
766
|
+
src="/icons/floorplan.png"
|
|
767
|
+
/>
|
|
768
|
+
<span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
|
|
769
|
+
{total}
|
|
770
|
+
</span>
|
|
771
|
+
</div>
|
|
772
|
+
</ActionButton>
|
|
773
|
+
|
|
774
|
+
<PopoverTrigger asChild>
|
|
775
|
+
<button
|
|
776
|
+
aria-expanded={isOpen}
|
|
777
|
+
aria-label="Reference settings"
|
|
778
|
+
className={cn(
|
|
779
|
+
'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
|
|
780
|
+
anyVisible
|
|
781
|
+
? isOpen
|
|
782
|
+
? 'bg-white/10'
|
|
783
|
+
: 'bg-white/5 hover:bg-white/8'
|
|
784
|
+
: isOpen
|
|
785
|
+
? 'bg-white/8'
|
|
786
|
+
: 'opacity-60 hover:bg-white/5 hover:opacity-100',
|
|
787
|
+
)}
|
|
788
|
+
type="button"
|
|
789
|
+
>
|
|
790
|
+
<ChevronDown className={cn('h-3 w-3 transition-transform', isOpen && 'rotate-180')} />
|
|
791
|
+
</button>
|
|
792
|
+
</PopoverTrigger>
|
|
793
|
+
</div>
|
|
794
|
+
|
|
795
|
+
<PopoverContent
|
|
796
|
+
align="center"
|
|
797
|
+
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
|
|
798
|
+
side="top"
|
|
799
|
+
sideOffset={14}
|
|
800
|
+
>
|
|
801
|
+
<div className="space-y-3">
|
|
802
|
+
{uploadError && (
|
|
803
|
+
<div className="rounded-lg border border-destructive/30 bg-destructive/10 px-2.5 py-2 text-destructive text-xs">
|
|
804
|
+
{uploadError}
|
|
805
|
+
</div>
|
|
806
|
+
)}
|
|
807
|
+
<ReferenceListSection
|
|
808
|
+
emptyText="No scans on this level yet."
|
|
809
|
+
iconSrc="/icons/mesh.png"
|
|
810
|
+
nodes={scans}
|
|
811
|
+
noun="scan"
|
|
812
|
+
onError={setUploadError}
|
|
813
|
+
setShow={setShowScans}
|
|
814
|
+
show={showScans}
|
|
815
|
+
title="Scans"
|
|
816
|
+
/>
|
|
817
|
+
<div className="h-px bg-border/45" />
|
|
818
|
+
<ReferenceListSection
|
|
819
|
+
emptyText="No guide images on this level yet."
|
|
820
|
+
iconSrc="/icons/floorplan.png"
|
|
821
|
+
nodes={guides}
|
|
822
|
+
noun="guide image"
|
|
823
|
+
onError={setUploadError}
|
|
824
|
+
setShow={setShowGuides}
|
|
825
|
+
show={showGuides}
|
|
826
|
+
title="Guide images"
|
|
827
|
+
/>
|
|
828
|
+
</div>
|
|
829
|
+
</PopoverContent>
|
|
830
|
+
</Popover>
|
|
831
|
+
)
|
|
832
|
+
}
|
|
833
|
+
|
|
596
834
|
// ── Reference floor control ────────────────────────────────────────────────────────────────────
|
|
597
835
|
|
|
598
836
|
function ReferenceFloorControl() {
|
|
@@ -756,8 +994,7 @@ export { GridSnapControl }
|
|
|
756
994
|
export function SecondaryToggles() {
|
|
757
995
|
return (
|
|
758
996
|
<div className="flex items-center gap-1">
|
|
759
|
-
<
|
|
760
|
-
<GuidesControl />
|
|
997
|
+
<ReferencesControl />
|
|
761
998
|
</div>
|
|
762
999
|
)
|
|
763
1000
|
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
PencilLine,
|
|
28
28
|
Plus,
|
|
29
29
|
Redo2,
|
|
30
|
+
Sparkles,
|
|
30
31
|
Square,
|
|
31
32
|
SquareStack,
|
|
32
33
|
Sun,
|
|
@@ -190,8 +191,11 @@ export function EditorCommands() {
|
|
|
190
191
|
const { nodes } = useScene.getState()
|
|
191
192
|
const building = Object.values(nodes).find((n) => n.type === 'building')
|
|
192
193
|
if (!building) return
|
|
194
|
+
const levelCount = building.children.filter(
|
|
195
|
+
(childId) => nodes[childId as keyof typeof nodes]?.type === 'level',
|
|
196
|
+
).length
|
|
193
197
|
const newLevel = LevelNode.parse({
|
|
194
|
-
level:
|
|
198
|
+
level: levelCount,
|
|
195
199
|
children: [],
|
|
196
200
|
parentId: building.id,
|
|
197
201
|
})
|
|
@@ -278,19 +282,20 @@ export function EditorCommands() {
|
|
|
278
282
|
}),
|
|
279
283
|
},
|
|
280
284
|
{
|
|
281
|
-
id: 'editor.viewer.
|
|
282
|
-
label:
|
|
283
|
-
const theme = useViewer.getState().theme
|
|
284
|
-
return theme === 'dark' ? 'Switch to Light Theme' : 'Switch to Dark Theme'
|
|
285
|
-
},
|
|
285
|
+
id: 'editor.viewer.shading-solid',
|
|
286
|
+
label: 'Switch to Solid',
|
|
286
287
|
group: 'Viewer Controls',
|
|
287
|
-
icon: <
|
|
288
|
-
keywords: ['
|
|
289
|
-
execute: () =>
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
icon: <Box className="h-4 w-4" />,
|
|
289
|
+
keywords: ['solid', 'shading', 'render', 'mode', 'performance'],
|
|
290
|
+
execute: () => run(() => useViewer.getState().setShading('solid')),
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
id: 'editor.viewer.shading-rendered',
|
|
294
|
+
label: 'Switch to Rendered',
|
|
295
|
+
group: 'Viewer Controls',
|
|
296
|
+
icon: <Sparkles className="h-4 w-4" />,
|
|
297
|
+
keywords: ['rendered', 'shading', 'render', 'mode', 'quality'],
|
|
298
|
+
execute: () => run(() => useViewer.getState().setShading('rendered')),
|
|
294
299
|
},
|
|
295
300
|
{
|
|
296
301
|
id: 'editor.viewer.camera-snapshot',
|
|
@@ -10,6 +10,7 @@ import { useEffect, useState } from 'react'
|
|
|
10
10
|
import { create } from 'zustand'
|
|
11
11
|
import { useShallow } from 'zustand/shallow'
|
|
12
12
|
import { Dialog, DialogContent, DialogTitle } from './../../../components/ui/primitives/dialog'
|
|
13
|
+
import { getLevelDisplayName } from '../../../lib/level-name'
|
|
13
14
|
import { useCommandRegistry } from '../../../store/use-command-registry'
|
|
14
15
|
import { usePaletteViewRegistry } from '../../../store/use-palette-view-registry'
|
|
15
16
|
|
|
@@ -404,7 +405,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
404
405
|
<OptionItem
|
|
405
406
|
isActive={level.id === activeLevelId}
|
|
406
407
|
key={level.id}
|
|
407
|
-
label={level
|
|
408
|
+
label={getLevelDisplayName(level)}
|
|
408
409
|
onSelect={() =>
|
|
409
410
|
run(() => useViewer.getState().setSelection({ levelId: level.id }))
|
|
410
411
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { Eraser, RotateCcw } from 'lucide-react'
|
|
6
|
+
import { useEffect } from 'react'
|
|
7
|
+
import {
|
|
8
|
+
buildResetSurfaceMaterialUpdates,
|
|
9
|
+
resolvePaintTargetFromSelection,
|
|
10
|
+
} from './../../../lib/material-paint'
|
|
11
|
+
import useEditor from './../../../store/use-editor'
|
|
12
|
+
import { Button } from '../primitives/button'
|
|
13
|
+
import { MaterialPicker } from './material-picker'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Material picker for paint mode. Embedders render this wherever paint controls
|
|
17
|
+
* belong (the community editor places it in the Build sidebar while paint mode
|
|
18
|
+
* is active). It owns the paint-target/material wiring so the host only needs
|
|
19
|
+
* to mount it; it fills its container's width.
|
|
20
|
+
*/
|
|
21
|
+
export function MaterialPaintPanel() {
|
|
22
|
+
const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
|
|
23
|
+
const activePaintTarget = useEditor((state) => state.activePaintTarget)
|
|
24
|
+
const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial)
|
|
25
|
+
const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget)
|
|
26
|
+
const paintEraser = useEditor((state) => state.paintEraser)
|
|
27
|
+
const setPaintEraser = useEditor((state) => state.setPaintEraser)
|
|
28
|
+
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
29
|
+
const nodes = useScene((state) => state.nodes)
|
|
30
|
+
const selectedId = selectedIds.length === 1 ? (selectedIds[0] ?? null) : null
|
|
31
|
+
const selectedNode = selectedId ? nodes[selectedId as AnyNodeId] : null
|
|
32
|
+
const canResetSelection =
|
|
33
|
+
selectedNode != null && resolvePaintTargetFromSelection({ nodes, selectedId }) != null
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const selectedPaintTarget = resolvePaintTargetFromSelection({ nodes, selectedId })
|
|
37
|
+
if (selectedPaintTarget) {
|
|
38
|
+
setActivePaintTarget(selectedPaintTarget)
|
|
39
|
+
}
|
|
40
|
+
}, [nodes, selectedId, setActivePaintTarget])
|
|
41
|
+
|
|
42
|
+
const resetSelection = () => {
|
|
43
|
+
if (!selectedNode) return
|
|
44
|
+
useScene.getState().updateNodes(buildResetSurfaceMaterialUpdates(nodes, selectedNode))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="w-full space-y-2">
|
|
49
|
+
<div className="flex items-center gap-2">
|
|
50
|
+
<Button
|
|
51
|
+
aria-pressed={paintEraser}
|
|
52
|
+
className="flex-1"
|
|
53
|
+
onClick={() => setPaintEraser(!paintEraser)}
|
|
54
|
+
size="sm"
|
|
55
|
+
variant={paintEraser ? 'default' : 'outline'}
|
|
56
|
+
>
|
|
57
|
+
<Eraser />
|
|
58
|
+
Erase
|
|
59
|
+
</Button>
|
|
60
|
+
<Button
|
|
61
|
+
className="flex-1"
|
|
62
|
+
disabled={!canResetSelection}
|
|
63
|
+
onClick={resetSelection}
|
|
64
|
+
size="sm"
|
|
65
|
+
variant="outline"
|
|
66
|
+
>
|
|
67
|
+
<RotateCcw />
|
|
68
|
+
Reset all
|
|
69
|
+
</Button>
|
|
70
|
+
</div>
|
|
71
|
+
<MaterialPicker
|
|
72
|
+
onChange={(material) => {
|
|
73
|
+
setActivePaintMaterial({ material, sourceTarget: activePaintTarget })
|
|
74
|
+
}}
|
|
75
|
+
onSelectMaterialPreset={(materialPreset) => {
|
|
76
|
+
setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
|
|
77
|
+
}}
|
|
78
|
+
selectedMaterialPreset={activePaintMaterial?.materialPreset}
|
|
79
|
+
value={activePaintMaterial?.material}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
getCatalogMaterialById,
|
|
5
5
|
getLibraryMaterialIdFromRef,
|
|
6
6
|
getMaterialsForCategory,
|
|
7
|
-
getMaterialsForTarget,
|
|
8
7
|
MATERIAL_CATEGORIES,
|
|
9
8
|
type MaterialSchema,
|
|
10
9
|
type MaterialTarget,
|
|
@@ -23,6 +22,11 @@ type MaterialPickerProps = {
|
|
|
23
22
|
hideSideControl?: boolean
|
|
24
23
|
}
|
|
25
24
|
|
|
25
|
+
function getCategoryLabel(category: (typeof MATERIAL_CATEGORIES)[number]) {
|
|
26
|
+
if (category === 'roof') return 'Roofing'
|
|
27
|
+
return category.charAt(0).toUpperCase() + category.slice(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
26
30
|
export function MaterialPicker({
|
|
27
31
|
value,
|
|
28
32
|
selectedMaterialPreset,
|
|
@@ -35,7 +39,6 @@ export function MaterialPicker({
|
|
|
35
39
|
const [selectedCategory, setSelectedCategory] = useState<(typeof MATERIAL_CATEGORIES)[number]>(
|
|
36
40
|
MATERIAL_CATEGORIES[0],
|
|
37
41
|
)
|
|
38
|
-
const catalogScrollRef = useRef<HTMLDivElement>(null)
|
|
39
42
|
const categoryScrollRef = useRef<HTMLDivElement>(null)
|
|
40
43
|
const catalogItems =
|
|
41
44
|
selectedCategory === 'other'
|
|
@@ -69,27 +72,6 @@ export function MaterialPicker({
|
|
|
69
72
|
onSelectMaterialPreset?.(toLibraryMaterialRef(materialId))
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
const container = catalogScrollRef.current
|
|
74
|
-
if (!container) return
|
|
75
|
-
|
|
76
|
-
const handleWheel = (event: WheelEvent) => {
|
|
77
|
-
const deltaX = event.deltaX
|
|
78
|
-
const deltaY = event.deltaY
|
|
79
|
-
const nextScrollLeft = container.scrollLeft + deltaX + deltaY
|
|
80
|
-
|
|
81
|
-
if (nextScrollLeft === container.scrollLeft) return
|
|
82
|
-
|
|
83
|
-
event.preventDefault()
|
|
84
|
-
container.scrollLeft = nextScrollLeft
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
container.addEventListener('wheel', handleWheel, { passive: false })
|
|
88
|
-
return () => {
|
|
89
|
-
container.removeEventListener('wheel', handleWheel)
|
|
90
|
-
}
|
|
91
|
-
}, [catalogItems.length, onChange, showCustom])
|
|
92
|
-
|
|
93
75
|
useEffect(() => {
|
|
94
76
|
const container = categoryScrollRef.current
|
|
95
77
|
if (!container) return
|
|
@@ -157,20 +139,18 @@ export function MaterialPicker({
|
|
|
157
139
|
}}
|
|
158
140
|
type="button"
|
|
159
141
|
>
|
|
160
|
-
{
|
|
142
|
+
{getCategoryLabel(category)}
|
|
161
143
|
</button>
|
|
162
144
|
))}
|
|
163
145
|
</div>
|
|
164
146
|
</div>
|
|
165
147
|
<div
|
|
166
|
-
className="
|
|
167
|
-
|
|
168
|
-
style={{ msOverflowStyle: 'none', scrollbarWidth: 'none' }}
|
|
148
|
+
className="grid gap-1.5 pb-1"
|
|
149
|
+
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(72px, 1fr))' }}
|
|
169
150
|
>
|
|
170
|
-
|
|
171
|
-
{catalogItems.map((item) => (
|
|
151
|
+
{catalogItems.map((item) => (
|
|
172
152
|
<button
|
|
173
|
-
className={`relative
|
|
153
|
+
className={`relative aspect-square w-full overflow-hidden rounded-lg border transition-all ${
|
|
174
154
|
selectedCatalogId === toLibraryMaterialRef(item.id)
|
|
175
155
|
? 'border-blue-500 ring-2 ring-blue-500/30'
|
|
176
156
|
: 'border-gray-300 hover:border-gray-400'
|
|
@@ -196,7 +176,7 @@ export function MaterialPicker({
|
|
|
196
176
|
))}
|
|
197
177
|
{selectedCategory === 'other' && onChange ? (
|
|
198
178
|
<button
|
|
199
|
-
className={`flex
|
|
179
|
+
className={`flex aspect-square w-full items-center justify-center rounded-lg border font-medium text-[10px] transition-all ${
|
|
200
180
|
showCustom
|
|
201
181
|
? 'border-blue-500 ring-2 ring-blue-500/30'
|
|
202
182
|
: 'border-gray-300 hover:border-gray-400'
|
|
@@ -208,7 +188,6 @@ export function MaterialPicker({
|
|
|
208
188
|
Custom
|
|
209
189
|
</button>
|
|
210
190
|
) : null}
|
|
211
|
-
</div>
|
|
212
191
|
</div>
|
|
213
192
|
</div>
|
|
214
193
|
)}
|