@pascal-app/editor 1.0.5 → 1.0.7
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 +20 -17
- package/src/components/editor/action-menu-button.tsx +4 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
- package/src/components/editor/bake-thumbnail.tsx +3 -2
- package/src/components/editor/custom-camera-controls.tsx +20 -8
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +72 -61
- package/src/components/editor/floating-action-menu.tsx +27 -9
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +285 -132
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/grid.tsx +24 -15
- package/src/components/editor/group-actions.ts +19 -1
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +27 -2
- package/src/components/editor/group-selection-box-3d.tsx +4 -1
- package/src/components/editor/group-transform-shared.ts +20 -0
- package/src/components/editor/index.tsx +130 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +1 -2
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/registry-action-contributions.test.tsx +67 -6
- package/src/components/editor/registry-action-contributions.tsx +115 -15
- package/src/components/editor/riser-diagram-panel.tsx +59 -9
- package/src/components/editor/room-controls.tsx +1 -1
- package/src/components/editor/room-handles-2d.tsx +156 -6
- package/src/components/editor/room-handles.tsx +241 -51
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +29 -15
- package/src/components/editor/selection-manager.tsx +15 -102
- package/src/components/editor/separator-end-grip.tsx +74 -0
- package/src/components/editor/site-edge-labels.tsx +1 -1
- package/src/components/editor/thumbnail-generator.test.ts +15 -6
- package/src/components/editor/thumbnail-generator.tsx +26 -1
- package/src/components/editor/use-floorplan-background-placement.ts +55 -52
- package/src/components/editor/views/view-bar.tsx +111 -26
- package/src/components/editor/wall-measurement-label.tsx +1 -1
- package/src/components/editor/wall-move-side-handles.tsx +5 -1
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
- package/src/components/procedural-items/inspector.tsx +59 -12
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
- package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
- package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/placement-strategies.test.ts +64 -1
- package/src/components/tools/item/placement-strategies.ts +5 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- package/src/components/tools/select/select-candidates.test.ts +45 -0
- package/src/components/tools/select/select-candidates.ts +13 -2
- package/src/components/tools/shared/cursor-sphere.tsx +10 -3
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +1 -1
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/wall/wall-drafting.test.ts +299 -23
- package/src/components/tools/wall/wall-drafting.ts +132 -24
- package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
- package/src/components/tools/wall/wall-pull-snap.ts +563 -0
- package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
- package/src/components/tools/zone/zone-tool.tsx +3 -3
- package/src/components/ui/action-menu/action-button.tsx +2 -0
- package/src/components/ui/action-menu/camera-actions.tsx +8 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -5
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
- package/src/components/ui/action-menu/index.tsx +1 -0
- package/src/components/ui/action-menu/measurement-control.tsx +35 -29
- package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
- package/src/components/ui/action-menu/structure-tools.tsx +25 -23
- package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
- package/src/components/ui/action-menu/view-toggles.tsx +256 -51
- package/src/components/ui/build-panel/build-panel.tsx +163 -25
- package/src/components/ui/command-palette/editor-commands.tsx +38 -11
- package/src/components/ui/controls/finish-swatch.tsx +45 -19
- package/src/components/ui/controls/material-picker.tsx +24 -17
- package/src/components/ui/controls/metric-control.test.ts +52 -0
- package/src/components/ui/controls/metric-control.tsx +62 -24
- package/src/components/ui/controls/segmented-control.tsx +5 -2
- package/src/components/ui/controls/slider-control.test.tsx +23 -0
- package/src/components/ui/controls/slider-control.tsx +30 -14
- package/src/components/ui/floating-level-selector.tsx +100 -48
- package/src/components/ui/helpers/building-helper.tsx +5 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
- package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
- package/src/components/ui/helpers/helper-manager.tsx +82 -57
- package/src/components/ui/helpers/item-helper.tsx +6 -4
- package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
- package/src/components/ui/level-duplicate-dialog.tsx +32 -23
- package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
- package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
- package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
- package/src/components/ui/panels/inspector-extensions.tsx +66 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
- package/src/components/ui/panels/multi-field-value.test.ts +27 -0
- package/src/components/ui/panels/multi-field-value.ts +20 -1
- package/src/components/ui/panels/multi-height-mode.tsx +8 -7
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +5 -3
- package/src/components/ui/panels/panel-wrapper.tsx +22 -109
- package/src/components/ui/panels/parametric-field-control.tsx +13 -10
- package/src/components/ui/panels/parametric-inspector.tsx +18 -12
- package/src/components/ui/panels/reference-panel.tsx +51 -35
- package/src/components/ui/panels/room-construction-rows.tsx +464 -65
- package/src/components/ui/panels/room-name-options.test.ts +36 -3
- package/src/components/ui/panels/room-name-options.ts +110 -11
- package/src/components/ui/panels/room-openings-field.tsx +21 -12
- package/src/components/ui/panels/room-panel.tsx +206 -99
- package/src/components/ui/panels/shape-choice.tsx +7 -5
- package/src/components/ui/panels/wall-region-list.tsx +7 -7
- package/src/components/ui/primitives/shortcut-token.tsx +10 -6
- package/src/components/ui/sidebar/icon-rail.tsx +11 -9
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
- package/src/components/ui/sidebar/tab-bar.tsx +4 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
- package/src/components/ui/snap-target-badge.tsx +7 -6
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
- package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
- package/src/components/viewer/floorplan-compass-button.tsx +12 -3
- package/src/components/viewer/floorplan-preview.tsx +27 -19
- package/src/components/viewer/render-settings-menu.tsx +294 -0
- package/src/components/viewer/sky-control.tsx +710 -0
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
- package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
- package/src/components/viewer/viewer-controls-bar.tsx +278 -260
- package/src/components/viewer/viewer-hover-label.tsx +12 -4
- package/src/components/viewer/viewer-rooms.tsx +61 -4
- package/src/components/viewer/viewer-scene-header.tsx +28 -17
- package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
- package/src/components/viewer-overlay.tsx +8 -0
- package/src/components/walkthrough-hud.tsx +75 -2
- package/src/hooks/use-keyboard.test.ts +85 -4
- package/src/hooks/use-keyboard.ts +65 -7
- package/src/hooks/use-selected-room.ts +12 -1
- package/src/i18n/floor-footprint-name.ts +30 -0
- package/src/i18n/level-name.ts +39 -0
- package/src/i18n/locales/en/messages.po +15117 -0
- package/src/i18n/locales/en/messages.ts +1 -0
- package/src/i18n/locales/en-XA/messages.ts +1 -0
- package/src/i18n/locales/es/messages.po +37501 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +37501 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +37501 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +37501 -0
- package/src/i18n/locales/zh-CN/messages.ts +1 -0
- package/src/i18n/locales.test.ts +63 -0
- package/src/i18n/locales.ts +32 -0
- package/src/i18n/message-slot.tsx +25 -0
- package/src/i18n/node-messages.ts +169 -0
- package/src/i18n/pseudo-locale.ts +17 -0
- package/src/i18n/published-entry.test.ts +17 -0
- package/src/i18n/scene-theme-name.ts +16 -0
- package/src/index.tsx +156 -4
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +95 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/floor-footprints.test.ts +2 -1
- package/src/lib/floor-foundation-support.test.ts +7 -3
- package/src/lib/floorplan/dimension-drive.test.ts +36 -2
- package/src/lib/floorplan/dimension-drive.ts +68 -3
- package/src/lib/floorplan/floorplan-export.test.ts +16 -5
- package/src/lib/floorplan/floorplan-export.tsx +13 -16
- package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
- package/src/lib/floorplan/floorplan-extension.ts +37 -18
- package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
- package/src/lib/floorplan/floorplan-mode.ts +14 -12
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
- package/src/lib/floorplan/floorplan-readonly.ts +3 -3
- package/src/lib/floorplan/schedules.ts +10 -6
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
- package/src/lib/geometry-script/author.test.ts +114 -0
- package/src/lib/geometry-script/author.ts +55 -23
- package/src/lib/geometry-script/client.ts +3 -0
- package/src/lib/geometry-script/finish-scripts.ts +7 -7
- package/src/lib/glb-export.test.ts +327 -0
- package/src/lib/glb-export.ts +70 -2
- package/src/lib/grid-frame.test.ts +56 -3
- package/src/lib/grid-frame.ts +28 -1
- package/src/lib/history.test.ts +2 -1
- package/src/lib/hud-title.ts +48 -24
- package/src/lib/ifc-export.test.ts +41 -1
- package/src/lib/inspector-card-mode.test.ts +3 -31
- package/src/lib/inspector-card-mode.ts +5 -10
- package/src/lib/inspector-expanded.test.ts +13 -0
- package/src/lib/inspector-expanded.ts +13 -1
- package/src/lib/keyboard-pan.test.ts +7 -0
- package/src/lib/keyboard-pan.ts +9 -2
- package/src/lib/layer-thickness.test.ts +22 -0
- package/src/lib/layer-thickness.ts +79 -0
- package/src/lib/lot/drop-in.test.ts +59 -1
- package/src/lib/lot/drop-in.ts +42 -50
- package/src/lib/material-swatch-style.ts +24 -0
- package/src/lib/measurement-parser.ts +7 -3
- package/src/lib/measurements.test.ts +17 -0
- package/src/lib/measurements.ts +20 -5
- package/src/lib/mezzanine-selection.test.ts +3 -1
- package/src/lib/orbit-near-plane.test.ts +12 -0
- package/src/lib/orbit-near-plane.ts +13 -0
- package/src/lib/paint-regions.test.ts +8 -5
- package/src/lib/paint-scope.test.ts +53 -0
- package/src/lib/paint-scope.ts +18 -4
- package/src/lib/panel-rows.ts +17 -8
- package/src/lib/reference-scale.ts +83 -0
- package/src/lib/room-built-on.test.ts +26 -12
- package/src/lib/room-built-on.ts +20 -2
- package/src/lib/room-construction.test.ts +28 -1
- package/src/lib/room-construction.ts +14 -0
- package/src/lib/room-controls-phase6.test.ts +140 -0
- package/src/lib/room-handle-drag.ts +557 -42
- package/src/lib/room-name-catalog.test.ts +8 -0
- package/src/lib/room-name-catalog.ts +8 -1
- package/src/lib/room-name-messages.test.ts +132 -0
- package/src/lib/room-name-messages.ts +147 -0
- package/src/lib/room-name-storage.test.ts +334 -0
- package/src/lib/room-push-dimensions.test.ts +37 -0
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +9 -4
- package/src/lib/selection-cycle.ts +63 -0
- package/src/lib/separator-edit.test.ts +431 -0
- package/src/lib/separator-edit.ts +179 -0
- package/src/lib/separator-lines.ts +38 -0
- package/src/lib/sfx-bus.ts +27 -1
- package/src/lib/sfx-player.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +5 -1
- package/src/lib/tool-shortcuts.ts +130 -0
- package/src/lib/viewer-selection.test.ts +28 -24
- package/src/lib/viewer-selection.ts +20 -5
- package/src/lib/wall-draw-variant.test.ts +7 -6
- package/src/lib/wall-draw-variant.ts +107 -6
- package/src/lib/wall-nudge.test.ts +44 -0
- package/src/lib/wall-nudge.ts +131 -0
- package/src/lib/wall-parallel-snap.test.ts +83 -0
- package/src/lib/wall-parallel-snap.ts +176 -0
- package/src/lib/wall-placement-pointer.test.ts +23 -0
- package/src/lib/wall-placement-pointer.ts +18 -0
- package/src/lib/wall-pull.test.ts +245 -0
- package/src/lib/wall-pull.ts +209 -0
- package/src/lib/wall-push-line-up.test.ts +132 -0
- package/src/lib/wall-region-list.ts +14 -5
- package/src/lib/wall-run-snap.test.ts +188 -0
- package/src/lib/wall-run-snap.ts +83 -0
- package/src/lib/wall-type.test.ts +125 -0
- package/src/lib/wall-type.ts +119 -0
- package/src/lib/window-interaction.test.ts +42 -0
- package/src/lib/window-interaction.ts +47 -5
- package/src/lib/world-grid-snap.test.ts +139 -2
- package/src/lib/world-grid-snap.ts +102 -105
- package/src/store/use-alignment-guides.ts +27 -1
- package/src/store/use-editor.tsx +48 -0
- package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
- package/src/store/use-floorplan-annotation-visibility.ts +47 -22
- package/src/store/use-wall-snap-indicator.ts +15 -2
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { type PascalI18n, usePascalI18n } from '@pascal-app/core'
|
|
3
4
|
import { ChevronRight } from 'lucide-react'
|
|
4
5
|
import { type ComponentPropsWithoutRef, type ReactNode, useEffect, useState } from 'react'
|
|
5
6
|
import { cn } from '../../../lib/utils'
|
|
6
7
|
import {
|
|
7
|
-
|
|
8
|
+
selectWallKind,
|
|
9
|
+
useWallKind,
|
|
8
10
|
WALL_DRAW_VARIANTS,
|
|
11
|
+
WALL_KINDS,
|
|
9
12
|
type WallDrawVariant,
|
|
13
|
+
type WallKind,
|
|
10
14
|
} from '../../../lib/wall-draw-variant'
|
|
15
|
+
import { SegmentedControl } from '../controls/segmented-control'
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* The Build panel's grouped layout (Rooms / Add to rooms / Outdoor / Advanced),
|
|
@@ -70,6 +75,7 @@ export function BuildPanelAdvancedSection({
|
|
|
70
75
|
description?: string
|
|
71
76
|
hint?: string
|
|
72
77
|
}) {
|
|
78
|
+
const i18n = usePascalI18n()
|
|
73
79
|
const [open, setOpen] = useState(false)
|
|
74
80
|
useEffect(() => setOpen(readAdvancedOpen()), [])
|
|
75
81
|
useEffect(() => {
|
|
@@ -94,7 +100,9 @@ export function BuildPanelAdvancedSection({
|
|
|
94
100
|
open && 'rotate-90',
|
|
95
101
|
)}
|
|
96
102
|
/>
|
|
97
|
-
<h3 className="font-medium text-foreground text-xs">
|
|
103
|
+
<h3 className="font-medium text-foreground text-xs">
|
|
104
|
+
{i18n._({ id: 'editor.buildPanel.advanced', message: 'Advanced' })}
|
|
105
|
+
</h3>
|
|
98
106
|
{hint ? (
|
|
99
107
|
<span className="ml-auto truncate text-[11px] text-muted-foreground">{hint}</span>
|
|
100
108
|
) : null}
|
|
@@ -188,19 +196,32 @@ export function BuildToolTile({
|
|
|
188
196
|
)
|
|
189
197
|
}
|
|
190
198
|
|
|
199
|
+
const CAPTION_LEAD_MARKER = '__PASCAL_CAPTION_LEAD__'
|
|
200
|
+
|
|
191
201
|
/** The line under the Rooms tiles: what the armed shape does, lead in bold. */
|
|
192
|
-
export function BuildPanelCaption({
|
|
202
|
+
export function BuildPanelCaption({ caption, lead }: { caption: string; lead?: string }) {
|
|
203
|
+
const markerIndex = caption.indexOf(CAPTION_LEAD_MARKER)
|
|
204
|
+
const hasMarkedLead = lead && markerIndex >= 0
|
|
193
205
|
return (
|
|
194
206
|
<p className="mt-2 min-h-[2lh] px-0.5 text-[11px] text-muted-foreground leading-relaxed">
|
|
195
|
-
{
|
|
196
|
-
|
|
207
|
+
{hasMarkedLead ? (
|
|
208
|
+
<>
|
|
209
|
+
{caption.slice(0, markerIndex)}
|
|
210
|
+
<span className="font-medium text-foreground">{lead}</span>
|
|
211
|
+
{caption.slice(markerIndex + CAPTION_LEAD_MARKER.length)}
|
|
212
|
+
</>
|
|
213
|
+
) : (
|
|
214
|
+
caption
|
|
215
|
+
)}
|
|
197
216
|
</p>
|
|
198
217
|
)
|
|
199
218
|
}
|
|
200
219
|
|
|
201
220
|
/**
|
|
202
221
|
* The Rooms group: one large tile per wall drawing variant, and the armed
|
|
203
|
-
* variant's caption.
|
|
222
|
+
* variant's caption. While Walls is armed the caption gives way to its kinds
|
|
223
|
+
* (Standard, Invisible, Railing) and a line on the one picked.
|
|
224
|
+
* `activeVariant` is null while the wall tool is not armed.
|
|
204
225
|
*/
|
|
205
226
|
export function BuildPanelRoomsSection({
|
|
206
227
|
activeVariant,
|
|
@@ -214,29 +235,146 @@ export function BuildPanelRoomsSection({
|
|
|
214
235
|
/** Extra attributes per tile (e.g. a host's guide-target hook). */
|
|
215
236
|
tileProps?: (variant: WallDrawVariant) => Record<string, string | undefined>
|
|
216
237
|
}) {
|
|
238
|
+
const i18n = usePascalI18n()
|
|
217
239
|
const active = WALL_DRAW_VARIANTS.find((variant) => variant.id === activeVariant)
|
|
240
|
+
const activeMessages = active ? roomVariantMessages(active.id, i18n) : null
|
|
218
241
|
return (
|
|
219
|
-
<BuildPanelSection
|
|
242
|
+
<BuildPanelSection
|
|
243
|
+
hint={i18n._({
|
|
244
|
+
id: 'editor.buildPanel.rooms.hint',
|
|
245
|
+
message: 'walls · floor · ceiling',
|
|
246
|
+
})}
|
|
247
|
+
id="rooms"
|
|
248
|
+
title={i18n._({ id: 'editor.buildPanel.rooms.title', message: 'Rooms' })}
|
|
249
|
+
>
|
|
220
250
|
<BuildToolGrid columns={3}>
|
|
221
|
-
{WALL_DRAW_VARIANTS.map((variant) =>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
251
|
+
{WALL_DRAW_VARIANTS.map((variant) => {
|
|
252
|
+
const messages = roomVariantMessages(variant.id, i18n)
|
|
253
|
+
return (
|
|
254
|
+
<BuildToolTile
|
|
255
|
+
active={variant.id === activeVariant}
|
|
256
|
+
data-build-tool={`room-${variant.id}`}
|
|
257
|
+
iconSrc={variant.iconSrc}
|
|
258
|
+
key={variant.id}
|
|
259
|
+
label={messages.label}
|
|
260
|
+
onClick={() => onSelect(variant.id)}
|
|
261
|
+
onMouseEnter={() => onHover?.(variant.id)}
|
|
262
|
+
size="lg"
|
|
263
|
+
title={messages.title}
|
|
264
|
+
{...tileProps?.(variant.id)}
|
|
265
|
+
/>
|
|
266
|
+
)
|
|
267
|
+
})}
|
|
235
268
|
</BuildToolGrid>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
269
|
+
{activeVariant === 'walls' ? (
|
|
270
|
+
<BuildPanelWallKinds />
|
|
271
|
+
) : (
|
|
272
|
+
<BuildPanelCaption
|
|
273
|
+
caption={
|
|
274
|
+
activeMessages?.caption ??
|
|
275
|
+
i18n._({
|
|
276
|
+
id: 'editor.buildPanel.rooms.idle',
|
|
277
|
+
message:
|
|
278
|
+
'Pick a shape to start. Every room brings its own walls, floor and ceiling.',
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
lead={activeMessages?.lead}
|
|
282
|
+
/>
|
|
283
|
+
)}
|
|
240
284
|
</BuildPanelSection>
|
|
241
285
|
)
|
|
242
286
|
}
|
|
287
|
+
|
|
288
|
+
function roomVariantMessages(variant: WallDrawVariant, i18n: PascalI18n) {
|
|
289
|
+
if (variant === 'rectangle') {
|
|
290
|
+
return {
|
|
291
|
+
label: i18n._({
|
|
292
|
+
id: 'editor.buildPanel.rooms.rectangle.label',
|
|
293
|
+
message: 'Rectangle',
|
|
294
|
+
}),
|
|
295
|
+
title: i18n._({
|
|
296
|
+
id: 'editor.buildPanel.rooms.rectangle.title',
|
|
297
|
+
message: 'Rectangle room',
|
|
298
|
+
}),
|
|
299
|
+
lead: i18n._({
|
|
300
|
+
id: 'editor.buildPanel.rooms.rectangle.lead',
|
|
301
|
+
message: 'Click two corners',
|
|
302
|
+
}),
|
|
303
|
+
caption: i18n._({
|
|
304
|
+
id: 'editor.buildPanel.rooms.rectangle.caption',
|
|
305
|
+
message: '{lead} on the grid. Walls, floor and ceiling are created together.',
|
|
306
|
+
values: { lead: CAPTION_LEAD_MARKER },
|
|
307
|
+
}),
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (variant === 'walls') {
|
|
311
|
+
// Armed, Walls shows its kinds in place of a caption (`BuildPanelWallKinds`).
|
|
312
|
+
return {
|
|
313
|
+
label: i18n._({ id: 'editor.buildPanel.rooms.walls.label', message: 'Walls' }),
|
|
314
|
+
title: i18n._({ id: 'editor.buildPanel.rooms.walls.title', message: 'Walls' }),
|
|
315
|
+
lead: undefined,
|
|
316
|
+
caption: undefined,
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
label: i18n._({ id: 'editor.buildPanel.rooms.polygon.label', message: 'Polygon' }),
|
|
321
|
+
title: i18n._({
|
|
322
|
+
id: 'editor.buildPanel.rooms.polygon.title',
|
|
323
|
+
message: 'Polygon room',
|
|
324
|
+
}),
|
|
325
|
+
lead: i18n._({
|
|
326
|
+
id: 'editor.buildPanel.rooms.polygon.lead',
|
|
327
|
+
message: 'Click each corner,',
|
|
328
|
+
}),
|
|
329
|
+
caption: i18n._({
|
|
330
|
+
id: 'editor.buildPanel.rooms.polygon.caption',
|
|
331
|
+
message: '{lead} then the first one again to close. For L-shapes and angled rooms.',
|
|
332
|
+
values: { lead: CAPTION_LEAD_MARKER },
|
|
333
|
+
}),
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function wallKindHelp(kind: WallKind, i18n: PascalI18n): string {
|
|
338
|
+
if (kind === 'invisible') {
|
|
339
|
+
return i18n._({
|
|
340
|
+
id: 'editor.buildPanel.rooms.wallKinds.invisibleHelp',
|
|
341
|
+
message:
|
|
342
|
+
'Divides a room or closes off a porch without building a wall; rooms close on it. Change any wall later from its controls.',
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
if (kind === 'railing') {
|
|
346
|
+
return i18n._({
|
|
347
|
+
id: 'editor.buildPanel.rooms.wallKinds.railingHelp',
|
|
348
|
+
message:
|
|
349
|
+
'A guard for stairs, balconies and mezzanines; rooms close on it. Change any wall later from its controls.',
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
return i18n._({
|
|
353
|
+
id: 'editor.buildPanel.rooms.wallKinds.standardHelp',
|
|
354
|
+
message:
|
|
355
|
+
'One wall per two clicks. Its sides become exterior or interior from the rooms it bounds. Change any wall later from its controls.',
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** What the armed Walls variant draws: a plain wall, an invisible wall or a railing. */
|
|
360
|
+
function BuildPanelWallKinds() {
|
|
361
|
+
const i18n = usePascalI18n()
|
|
362
|
+
const kind = useWallKind()
|
|
363
|
+
return (
|
|
364
|
+
<div
|
|
365
|
+
aria-label={i18n._({ id: 'editor.buildPanel.rooms.wallKinds.label', message: 'Wall kind' })}
|
|
366
|
+
className="mt-2 flex flex-col gap-2 rounded-xl border border-border/50 bg-black/4 p-2 dark:bg-muted/40"
|
|
367
|
+
data-build-group="wall-kinds"
|
|
368
|
+
role="group"
|
|
369
|
+
>
|
|
370
|
+
<SegmentedControl
|
|
371
|
+
onChange={selectWallKind}
|
|
372
|
+
options={WALL_KINDS.map((entry) => ({ value: entry.id, label: i18n._(entry.label) }))}
|
|
373
|
+
value={kind}
|
|
374
|
+
/>
|
|
375
|
+
<p className="px-0.5 text-[11px] text-muted-foreground leading-relaxed">
|
|
376
|
+
{wallKindHelp(kind, i18n)}
|
|
377
|
+
</p>
|
|
378
|
+
</div>
|
|
379
|
+
)
|
|
380
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
type LevelNode,
|
|
6
|
+
type UnitNode,
|
|
7
|
+
usePascalI18n,
|
|
8
|
+
useScene,
|
|
9
|
+
} from '@pascal-app/core'
|
|
4
10
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
11
|
import {
|
|
6
12
|
AppWindow,
|
|
@@ -13,6 +19,7 @@ import {
|
|
|
13
19
|
Eye,
|
|
14
20
|
EyeOff,
|
|
15
21
|
FileJson,
|
|
22
|
+
Gauge,
|
|
16
23
|
Grid3X3,
|
|
17
24
|
Group,
|
|
18
25
|
Hexagon,
|
|
@@ -21,8 +28,8 @@ import {
|
|
|
21
28
|
Maximize2,
|
|
22
29
|
Minimize2,
|
|
23
30
|
Moon,
|
|
24
|
-
MousePointer2,
|
|
25
31
|
Mountain,
|
|
32
|
+
MousePointer2,
|
|
26
33
|
Package,
|
|
27
34
|
PaintBucket,
|
|
28
35
|
PencilLine,
|
|
@@ -37,6 +44,7 @@ import {
|
|
|
37
44
|
} from 'lucide-react'
|
|
38
45
|
import { useEffect } from 'react'
|
|
39
46
|
import { addStoreyOnTop } from '../../../lib/add-storey'
|
|
47
|
+
import { offerExteriorWallCopy } from '../level-wall-copy-prompt'
|
|
40
48
|
import { getHistoryCommandState, runRedo, runUndo } from '../../../lib/history'
|
|
41
49
|
import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
|
|
42
50
|
import { getSavedSceneDocument } from '../../../lib/scene'
|
|
@@ -47,6 +55,7 @@ import useEditor from '../../../store/use-editor'
|
|
|
47
55
|
import { useCommandPalette } from './index'
|
|
48
56
|
|
|
49
57
|
export function EditorCommands() {
|
|
58
|
+
const i18n = usePascalI18n()
|
|
50
59
|
const register = useCommandRegistry((s) => s.register)
|
|
51
60
|
const { navigateTo, setInputValue, setOpen } = useCommandPalette()
|
|
52
61
|
|
|
@@ -205,6 +214,7 @@ export function EditorCommands() {
|
|
|
205
214
|
if (!building) return
|
|
206
215
|
const newLevel = addStoreyOnTop(building.id)
|
|
207
216
|
useViewer.getState().setSelection({ levelId: newLevel.id })
|
|
217
|
+
offerExteriorWallCopy(newLevel.id)
|
|
208
218
|
}),
|
|
209
219
|
},
|
|
210
220
|
{
|
|
@@ -311,20 +321,36 @@ export function EditorCommands() {
|
|
|
311
321
|
}),
|
|
312
322
|
},
|
|
313
323
|
{
|
|
314
|
-
id: 'editor.viewer.
|
|
315
|
-
label: '
|
|
324
|
+
id: 'editor.viewer.quality-auto',
|
|
325
|
+
label: i18n._({ id: 'editor.commands.qualityAuto', message: 'Quality: Auto' }),
|
|
326
|
+
group: 'Viewer Controls',
|
|
327
|
+
icon: <Gauge className="h-4 w-4" />,
|
|
328
|
+
keywords: ['quality', 'auto', 'device', 'render'],
|
|
329
|
+
execute: () => run(() => useViewer.getState().setRenderQuality('auto')),
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: 'editor.viewer.quality-low',
|
|
333
|
+
label: i18n._({ id: 'editor.commands.qualityLow', message: 'Quality: Low' }),
|
|
334
|
+
group: 'Viewer Controls',
|
|
335
|
+
icon: <Gauge className="h-4 w-4" />,
|
|
336
|
+
keywords: ['quality', 'low', 'fast', 'performance', 'solid', 'render'],
|
|
337
|
+
execute: () => run(() => useViewer.getState().setRenderQuality('low')),
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
id: 'editor.viewer.quality-medium',
|
|
341
|
+
label: i18n._({ id: 'editor.commands.qualityMedium', message: 'Quality: Medium' }),
|
|
316
342
|
group: 'Viewer Controls',
|
|
317
|
-
icon: <
|
|
318
|
-
keywords: ['
|
|
319
|
-
execute: () => run(() => useViewer.getState().
|
|
343
|
+
icon: <Gauge className="h-4 w-4" />,
|
|
344
|
+
keywords: ['quality', 'medium', 'render'],
|
|
345
|
+
execute: () => run(() => useViewer.getState().setRenderQuality('medium')),
|
|
320
346
|
},
|
|
321
347
|
{
|
|
322
|
-
id: 'editor.viewer.
|
|
323
|
-
label: '
|
|
348
|
+
id: 'editor.viewer.quality-high',
|
|
349
|
+
label: i18n._({ id: 'editor.commands.qualityHigh', message: 'Quality: High' }),
|
|
324
350
|
group: 'Viewer Controls',
|
|
325
351
|
icon: <Sparkles className="h-4 w-4" />,
|
|
326
|
-
keywords: ['
|
|
327
|
-
execute: () => run(() => useViewer.getState().
|
|
352
|
+
keywords: ['quality', 'high', 'rendered', 'render'],
|
|
353
|
+
execute: () => run(() => useViewer.getState().setRenderQuality('high')),
|
|
328
354
|
},
|
|
329
355
|
{
|
|
330
356
|
id: 'editor.viewer.camera-snapshot',
|
|
@@ -449,6 +475,7 @@ export function EditorCommands() {
|
|
|
449
475
|
setPreviewMode,
|
|
450
476
|
exportScene,
|
|
451
477
|
unitListKey,
|
|
478
|
+
i18n,
|
|
452
479
|
])
|
|
453
480
|
|
|
454
481
|
return null
|
|
@@ -4,36 +4,52 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
getCatalogMaterialById,
|
|
6
6
|
getLibraryMaterialIdFromRef,
|
|
7
|
+
getMaterialSwatch,
|
|
7
8
|
getSceneMaterialIdFromRef,
|
|
8
9
|
nodeRegistry,
|
|
10
|
+
type PascalI18n,
|
|
11
|
+
usePascalI18n,
|
|
9
12
|
useScene,
|
|
10
13
|
} from '@pascal-app/core'
|
|
11
14
|
import { useState } from 'react'
|
|
15
|
+
import { materialSwatchStyle } from '../../../lib/material-swatch-style'
|
|
12
16
|
import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
|
|
13
17
|
import { MaterialPicker } from './material-picker'
|
|
14
18
|
|
|
15
|
-
type Swatch = { label: string; color: string | null; image: string | null }
|
|
19
|
+
type Swatch = { label: string; color: string | null; image: string | null; tinted?: boolean }
|
|
16
20
|
|
|
17
21
|
/** How a finish reference reads in a panel: its catalog tile, its colour, or its scene material. */
|
|
18
22
|
function useFinishSwatch(ref: string | undefined, fallback: string): Swatch {
|
|
23
|
+
const i18n = usePascalI18n()
|
|
19
24
|
const materials = useScene((state) => state.materials)
|
|
20
|
-
if (!ref) return { ...describe(fallback, materials), label:
|
|
21
|
-
return describe(ref, materials)
|
|
25
|
+
if (!ref) return { ...describe(fallback, materials, i18n), label: defaultLabel(i18n) }
|
|
26
|
+
return describe(ref, materials, i18n)
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
const defaultLabel = (i18n: PascalI18n) =>
|
|
30
|
+
i18n._({ id: 'editor.controls.finishSwatch.default', message: 'Default' })
|
|
31
|
+
|
|
32
|
+
function describe(
|
|
33
|
+
ref: string,
|
|
34
|
+
materials: ReturnType<typeof useScene.getState>['materials'],
|
|
35
|
+
i18n: PascalI18n,
|
|
36
|
+
) {
|
|
25
37
|
const library = getCatalogMaterialById(getLibraryMaterialIdFromRef(ref) ?? undefined)
|
|
26
|
-
if (library)
|
|
38
|
+
if (library) {
|
|
39
|
+
const swatch = getMaterialSwatch(library)
|
|
27
40
|
return {
|
|
28
41
|
label: library.label,
|
|
29
|
-
color:
|
|
30
|
-
image:
|
|
42
|
+
color: swatch.color,
|
|
43
|
+
image: swatch.imageUrl,
|
|
44
|
+
tinted: swatch.tinted,
|
|
31
45
|
}
|
|
46
|
+
}
|
|
32
47
|
const sceneId = getSceneMaterialIdFromRef(ref)
|
|
33
48
|
const scene = sceneId ? materials[sceneId as keyof typeof materials] : undefined
|
|
34
|
-
if (scene)
|
|
49
|
+
if (scene)
|
|
50
|
+
return { label: scene.name, color: scene.material.properties?.color ?? null, image: null }
|
|
35
51
|
if (ref.startsWith('#')) return { label: ref.toUpperCase(), color: ref, image: null }
|
|
36
|
-
return { label:
|
|
52
|
+
return { label: defaultLabel(i18n), color: null, image: null }
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
/**
|
|
@@ -55,21 +71,22 @@ export function paintSurface(node: AnyNode, role: string, materialPreset: string
|
|
|
55
71
|
export function FinishSwatch({
|
|
56
72
|
label,
|
|
57
73
|
node,
|
|
58
|
-
|
|
74
|
+
paintRole,
|
|
59
75
|
value,
|
|
60
76
|
fallback,
|
|
61
77
|
hint,
|
|
62
78
|
}: {
|
|
63
79
|
label: string
|
|
64
80
|
node: AnyNode
|
|
65
|
-
/** The paint role of the surface (`edge`, `foundation`, …). */
|
|
66
|
-
|
|
81
|
+
/** The paint role of the surface (`edge`, `foundation`, …); not an ARIA role. */
|
|
82
|
+
paintRole: string
|
|
67
83
|
/** The authored finish reference; absent shows `fallback` as "Default". */
|
|
68
84
|
value: string | undefined
|
|
69
85
|
fallback: string
|
|
70
86
|
/** Tooltip on the row. */
|
|
71
87
|
hint?: string
|
|
72
88
|
}) {
|
|
89
|
+
const i18n = usePascalI18n()
|
|
73
90
|
const [open, setOpen] = useState(false)
|
|
74
91
|
const swatch = useFinishSwatch(value, fallback)
|
|
75
92
|
return (
|
|
@@ -78,7 +95,11 @@ export function FinishSwatch({
|
|
|
78
95
|
<Popover onOpenChange={setOpen} open={open}>
|
|
79
96
|
<PopoverTrigger asChild>
|
|
80
97
|
<button
|
|
81
|
-
aria-label={
|
|
98
|
+
aria-label={i18n._({
|
|
99
|
+
id: 'editor.controls.finishSwatch.ariaLabel',
|
|
100
|
+
message: '{label}: {finish}',
|
|
101
|
+
values: { label, finish: swatch.label },
|
|
102
|
+
})}
|
|
82
103
|
className="flex min-w-0 items-center gap-2 rounded-full border border-border/50 py-0.5 pr-2.5 pl-1 text-xs transition-colors hover:bg-accent"
|
|
83
104
|
data-finish-swatch={label}
|
|
84
105
|
type="button"
|
|
@@ -86,18 +107,23 @@ export function FinishSwatch({
|
|
|
86
107
|
<span
|
|
87
108
|
aria-hidden
|
|
88
109
|
className="size-4 shrink-0 overflow-hidden rounded-full border border-border/50"
|
|
89
|
-
style={{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
110
|
+
style={materialSwatchStyle({
|
|
111
|
+
color: swatch.color ?? 'transparent',
|
|
112
|
+
imageUrl: swatch.image,
|
|
113
|
+
tinted: swatch.tinted,
|
|
114
|
+
})}
|
|
115
|
+
/>
|
|
93
116
|
<span className="truncate">{swatch.label}</span>
|
|
94
117
|
</button>
|
|
95
118
|
</PopoverTrigger>
|
|
96
119
|
<PopoverContent align="end" className="flex h-96 w-80 flex-col p-2" side="left">
|
|
97
120
|
<MaterialPicker
|
|
98
121
|
onSelectMaterialPreset={(ref) => {
|
|
99
|
-
const live =
|
|
100
|
-
|
|
122
|
+
const live =
|
|
123
|
+
useScene.getState().nodes[
|
|
124
|
+
node.id as keyof ReturnType<typeof useScene.getState>['nodes']
|
|
125
|
+
]
|
|
126
|
+
if (live) paintSurface(live, paintRole, ref)
|
|
101
127
|
setOpen(false)
|
|
102
128
|
}}
|
|
103
129
|
selectOnCategoryChange={false}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { type MaterialTarget, toLibraryMaterialRef } from '@pascal-app/core'
|
|
3
|
+
import { getMaterialSwatch, type MaterialTarget, toLibraryMaterialRef } from '@pascal-app/core'
|
|
4
4
|
import { Plus } from 'lucide-react'
|
|
5
|
-
import { useMaterialCatalogModel
|
|
5
|
+
import { useMaterialCatalogModel } from '../../../lib/material-catalog-model'
|
|
6
|
+
import { materialSwatchStyle } from '../../../lib/material-swatch-style'
|
|
6
7
|
import { triggerSFX } from '../../../lib/sfx-bus'
|
|
7
8
|
|
|
8
9
|
export type { MaterialSourceFilter } from '../../../lib/material-catalog-model'
|
|
@@ -35,7 +36,21 @@ export function MaterialPicker({
|
|
|
35
36
|
onCreateMaterialRequest,
|
|
36
37
|
selectOnCategoryChange = true,
|
|
37
38
|
}: MaterialPickerProps) {
|
|
38
|
-
const {
|
|
39
|
+
const {
|
|
40
|
+
selectedCategory,
|
|
41
|
+
setSelectedCategory,
|
|
42
|
+
sourceFilter,
|
|
43
|
+
setSourceFilter,
|
|
44
|
+
visibleSourceFilters,
|
|
45
|
+
availableCategories,
|
|
46
|
+
catalogItems,
|
|
47
|
+
select: handleCatalogSelect,
|
|
48
|
+
} = useMaterialCatalogModel(
|
|
49
|
+
selectedMaterialPreset,
|
|
50
|
+
onSelectMaterialPreset,
|
|
51
|
+
disabled,
|
|
52
|
+
selectOnCategoryChange,
|
|
53
|
+
)
|
|
39
54
|
|
|
40
55
|
return (
|
|
41
56
|
<div
|
|
@@ -121,20 +136,12 @@ export function MaterialPicker({
|
|
|
121
136
|
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
122
137
|
type="button"
|
|
123
138
|
>
|
|
124
|
-
<div
|
|
125
|
-
{item.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
/>
|
|
131
|
-
) : (
|
|
132
|
-
<div
|
|
133
|
-
className="h-full w-full"
|
|
134
|
-
style={{ backgroundColor: item.previewColor ?? '#f3f4f6' }}
|
|
135
|
-
/>
|
|
136
|
-
)}
|
|
137
|
-
</div>
|
|
139
|
+
<div
|
|
140
|
+
aria-label={item.label}
|
|
141
|
+
className="relative aspect-square w-full overflow-hidden rounded-lg"
|
|
142
|
+
role="img"
|
|
143
|
+
style={materialSwatchStyle(getMaterialSwatch(item))}
|
|
144
|
+
/>
|
|
138
145
|
<span className="truncate px-0.5 text-left font-medium text-[11px] text-muted-foreground group-hover:text-foreground">
|
|
139
146
|
{item.label}
|
|
140
147
|
</span>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
formatMetricInputValue,
|
|
4
|
+
parseMetricInput,
|
|
5
|
+
roundMetricValueForDisplay,
|
|
6
|
+
} from './metric-control'
|
|
7
|
+
|
|
8
|
+
test('metric controls parse stair dimensions with the scoped locale', () => {
|
|
9
|
+
expect(
|
|
10
|
+
parseMetricInput('1,000', 'm', {
|
|
11
|
+
isImperial: false,
|
|
12
|
+
locale: 'fr',
|
|
13
|
+
parseUnit: 'm',
|
|
14
|
+
}),
|
|
15
|
+
).toBe(1)
|
|
16
|
+
expect(
|
|
17
|
+
parseMetricInput('1,000', 'm', {
|
|
18
|
+
isImperial: false,
|
|
19
|
+
locale: 'en',
|
|
20
|
+
parseUnit: 'm',
|
|
21
|
+
}),
|
|
22
|
+
).toBe(1000)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('metric controls write values back with the scoped decimal separator', () => {
|
|
26
|
+
expect(formatMetricInputValue(2.75, 2, 'fr')).toBe('2,75')
|
|
27
|
+
expect(formatMetricInputValue(2.75, 2, 'en')).toBe('2.75')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('metric write-back rounds negative halves away from zero like the display', () => {
|
|
31
|
+
expect(roundMetricValueForDisplay(-1.25, 1)).toBe(-1.3)
|
|
32
|
+
expect(formatMetricInputValue(-1.25, 1, 'en')).toBe('-1.3')
|
|
33
|
+
expect(roundMetricValueForDisplay(1.25, 1)).toBe(1.3)
|
|
34
|
+
expect(formatMetricInputValue(1.25, 1, 'en')).toBe('1.3')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('metric write-back and display agree for values immediately below a half', () => {
|
|
38
|
+
const positive = 1.2499999999999998
|
|
39
|
+
const negative = -positive
|
|
40
|
+
|
|
41
|
+
expect(roundMetricValueForDisplay(negative, 1)).toBe(-1.2)
|
|
42
|
+
expect(formatMetricInputValue(negative, 1, 'en')).toBe('-1.2')
|
|
43
|
+
expect(roundMetricValueForDisplay(positive, 1)).toBe(1.2)
|
|
44
|
+
expect(formatMetricInputValue(positive, 1, 'en')).toBe('1.2')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('metric write-back preserves Intl rounding for binary-inexact decimal halves', () => {
|
|
48
|
+
expect(roundMetricValueForDisplay(-1.005, 2)).toBe(-1.01)
|
|
49
|
+
expect(formatMetricInputValue(-1.005, 2, 'en')).toBe('-1.01')
|
|
50
|
+
expect(roundMetricValueForDisplay(1.005, 2)).toBe(1.01)
|
|
51
|
+
expect(formatMetricInputValue(1.005, 2, 'en')).toBe('1.01')
|
|
52
|
+
})
|