@pascal-app/editor 1.0.6 → 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 +18 -15
- 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,7 +1,13 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { type AnyNode, registerNode, useInteractive } from '@pascal-app/core'
|
|
2
|
+
import { type AnyNode, type MechanismPart, registerNode, useInteractive } from '@pascal-app/core'
|
|
3
|
+
import type { MouseEvent, ReactElement } from 'react'
|
|
3
4
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
MechanismButton,
|
|
7
|
+
MechanismPartButton,
|
|
8
|
+
NodeMechanismActions,
|
|
9
|
+
soleSelectedNode,
|
|
10
|
+
} from './registry-action-contributions'
|
|
5
11
|
|
|
6
12
|
const switchMechanism = {
|
|
7
13
|
has: (node: AnyNode) => (node as unknown as { joints: number }).joints > 0,
|
|
@@ -23,6 +29,30 @@ function articulated(joints: number): AnyNode {
|
|
|
23
29
|
return { object: 'node', id: `${kind}_1`, type: kind, joints } as unknown as AnyNode
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
const shutters: MechanismPart = {
|
|
33
|
+
id: 'shutters',
|
|
34
|
+
label: 'Shutters',
|
|
35
|
+
icon: 'shutters',
|
|
36
|
+
verb: 'open',
|
|
37
|
+
has: () => true,
|
|
38
|
+
isOn: (node, state) => Boolean(state.mechanisms[`${node.id}:shutters` as never]),
|
|
39
|
+
set: (node, on) => useInteractive.getState().setMechanism(`${node.id}:shutters` as never, on),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function shuttered(): AnyNode {
|
|
43
|
+
const kind = `test:shuttered-${crypto.randomUUID()}`
|
|
44
|
+
registerNode({
|
|
45
|
+
kind,
|
|
46
|
+
schemaVersion: 1,
|
|
47
|
+
category: 'structure',
|
|
48
|
+
defaults: () => ({}),
|
|
49
|
+
capabilities: {
|
|
50
|
+
mechanism: { ...switchMechanism, verb: 'open', icon: 'window', parts: [shutters] },
|
|
51
|
+
},
|
|
52
|
+
} as never)
|
|
53
|
+
return { object: 'node', id: `${kind}_1`, type: kind, joints: 1 } as unknown as AnyNode
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
const byId = (...nodes: AnyNode[]) => Object.fromEntries(nodes.map((node) => [node.id, node]))
|
|
27
57
|
|
|
28
58
|
afterEach(() => {
|
|
@@ -33,17 +63,20 @@ afterEach(() => {
|
|
|
33
63
|
describe('mechanism action in the action menu', () => {
|
|
34
64
|
test('the only selected node gets Play when its kind declares a mechanism', () => {
|
|
35
65
|
const node = articulated(2)
|
|
36
|
-
expect(
|
|
66
|
+
expect(soleSelectedNode([node.id], byId(node))).toBe(node)
|
|
67
|
+
expect(renderToStaticMarkup(<NodeMechanismActions node={node} />)).toContain(
|
|
68
|
+
'aria-label="Play"',
|
|
69
|
+
)
|
|
37
70
|
})
|
|
38
71
|
|
|
39
72
|
test('no Play without moving parts, for a multi-selection, or without the capability', () => {
|
|
40
73
|
const still = articulated(0)
|
|
41
|
-
expect(
|
|
74
|
+
expect(renderToStaticMarkup(<NodeMechanismActions node={still} />)).toBe('')
|
|
42
75
|
|
|
43
76
|
const node = articulated(2)
|
|
44
77
|
const other = { object: 'node', id: 'other_1', type: 'other' } as unknown as AnyNode
|
|
45
|
-
expect(
|
|
46
|
-
expect(
|
|
78
|
+
expect(soleSelectedNode([node.id, other.id], byId(node, other))).toBeUndefined()
|
|
79
|
+
expect(renderToStaticMarkup(<NodeMechanismActions node={other} />)).toBe('')
|
|
47
80
|
})
|
|
48
81
|
|
|
49
82
|
test('the button reads Play, then Stop while the mechanism runs', () => {
|
|
@@ -61,3 +94,31 @@ describe('mechanism action in the action menu', () => {
|
|
|
61
94
|
expect(running).toContain('aria-pressed="true"')
|
|
62
95
|
})
|
|
63
96
|
})
|
|
97
|
+
|
|
98
|
+
describe('mechanism parts in the action menu', () => {
|
|
99
|
+
test('a window with shutters gets Open shutters beside Open', () => {
|
|
100
|
+
const markup = renderToStaticMarkup(<NodeMechanismActions node={shuttered()} />)
|
|
101
|
+
expect(markup.match(/aria-label="[^"]*"/g)).toEqual([
|
|
102
|
+
'aria-label="Open"',
|
|
103
|
+
'aria-label="Open shutters"',
|
|
104
|
+
])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('the shutters button opens them alone, then reads Close shutters', () => {
|
|
108
|
+
const node = shuttered()
|
|
109
|
+
let button: ReactElement<{ onClick: (event: MouseEvent) => void }> | undefined
|
|
110
|
+
// Rendering the button as a hook call hands over the element it returns, onClick included.
|
|
111
|
+
function Probe() {
|
|
112
|
+
button = MechanismPartButton({ node, part: shutters, running: false }) as typeof button
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
renderToStaticMarkup(<Probe />)
|
|
116
|
+
button?.props.onClick({ stopPropagation: () => {} } as MouseEvent)
|
|
117
|
+
expect(shutters.isOn(node, useInteractive.getState())).toBe(true)
|
|
118
|
+
expect(switchMechanism.isOn(node, useInteractive.getState())).toBe(false)
|
|
119
|
+
|
|
120
|
+
const open = renderToStaticMarkup(<MechanismPartButton node={node} part={shutters} running />)
|
|
121
|
+
expect(open).toContain('aria-label="Close shutters"')
|
|
122
|
+
expect(open).toContain('aria-pressed="true"')
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -4,10 +4,14 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type MechanismCapability,
|
|
7
|
+
type MechanismPart,
|
|
7
8
|
nodeMechanism,
|
|
9
|
+
nodeMechanismParts,
|
|
8
10
|
nodeRegistry,
|
|
9
11
|
toggleMechanism,
|
|
12
|
+
toggleMechanismPart,
|
|
10
13
|
useInteractive,
|
|
14
|
+
usePascalI18n,
|
|
11
15
|
useScene,
|
|
12
16
|
} from '@pascal-app/core'
|
|
13
17
|
import { useViewer } from '@pascal-app/viewer'
|
|
@@ -16,6 +20,7 @@ import { type ComponentType, lazy, Suspense } from 'react'
|
|
|
16
20
|
import { useShallow } from 'zustand/react/shallow'
|
|
17
21
|
import { getFloorplanNodeExtension } from '../../lib/floorplan/floorplan-extension'
|
|
18
22
|
import { ActionMenuButton } from './action-menu-button'
|
|
23
|
+
import { InspectorExtensionActions } from './inspector-extension-actions'
|
|
19
24
|
|
|
20
25
|
type Loader = () => Promise<{ default: ComponentType }>
|
|
21
26
|
const lazyCache = new WeakMap<Loader, ComponentType>()
|
|
@@ -30,13 +35,12 @@ function contribution(kind: string): ComponentType | null {
|
|
|
30
35
|
return component
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
/** The node the action menu's
|
|
34
|
-
export function
|
|
38
|
+
/** The node the action menu's mechanism buttons run: the only selected node. */
|
|
39
|
+
export function soleSelectedNode(
|
|
35
40
|
selectedIds: readonly string[],
|
|
36
41
|
nodes: Readonly<Record<string, AnyNode>>,
|
|
37
42
|
): AnyNode | undefined {
|
|
38
|
-
|
|
39
|
-
return nodeMechanism(node) ? node : undefined
|
|
43
|
+
return selectedIds.length === 1 ? nodes[selectedIds[0]!] : undefined
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
function MechanismGlyph({
|
|
@@ -61,6 +65,34 @@ function MechanismGlyph({
|
|
|
61
65
|
return <Glyph className="h-4 w-4" />
|
|
62
66
|
}
|
|
63
67
|
|
|
68
|
+
/** Two louvred leaves: shut across the window, or swung out to its sides. */
|
|
69
|
+
function ShuttersGlyph({ open }: { open: boolean }) {
|
|
70
|
+
return (
|
|
71
|
+
<svg
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
className="h-4 w-4"
|
|
74
|
+
fill="none"
|
|
75
|
+
stroke="currentColor"
|
|
76
|
+
strokeLinecap="round"
|
|
77
|
+
strokeLinejoin="round"
|
|
78
|
+
strokeWidth={2}
|
|
79
|
+
viewBox="0 0 24 24"
|
|
80
|
+
>
|
|
81
|
+
{open ? (
|
|
82
|
+
<>
|
|
83
|
+
<rect height="16" rx="1" width="8" x="8" y="4" />
|
|
84
|
+
<path d="M8 4 3 6v12l5 2M16 4l5 2v12l-5 2" />
|
|
85
|
+
</>
|
|
86
|
+
) : (
|
|
87
|
+
<>
|
|
88
|
+
<rect height="18" rx="1" width="16" x="4" y="3" />
|
|
89
|
+
<path d="M12 3v18M7 8h2M7 12h2M7 16h2M15 8h2M15 12h2M15 16h2" />
|
|
90
|
+
</>
|
|
91
|
+
)}
|
|
92
|
+
</svg>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
64
96
|
export function MechanismButton({
|
|
65
97
|
node,
|
|
66
98
|
mechanism,
|
|
@@ -70,7 +102,12 @@ export function MechanismButton({
|
|
|
70
102
|
mechanism: MechanismCapability
|
|
71
103
|
running: boolean
|
|
72
104
|
}) {
|
|
73
|
-
const
|
|
105
|
+
const i18n = usePascalI18n()
|
|
106
|
+
const label = i18n._({
|
|
107
|
+
id: 'editor.mechanismAction',
|
|
108
|
+
message: '{action, select, close {Close} open {Open} stop {Stop} other {Play}}',
|
|
109
|
+
values: { action: mechanism.icon ? (running ? 'close' : 'open') : running ? 'stop' : 'play' },
|
|
110
|
+
})
|
|
74
111
|
return (
|
|
75
112
|
<ActionMenuButton
|
|
76
113
|
keys={['E']}
|
|
@@ -86,21 +123,83 @@ export function MechanismButton({
|
|
|
86
123
|
)
|
|
87
124
|
}
|
|
88
125
|
|
|
89
|
-
/**
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
126
|
+
/** A second control beside the main one (a window's shutters), with its own running state. */
|
|
127
|
+
export function MechanismPartButton({
|
|
128
|
+
node,
|
|
129
|
+
part,
|
|
130
|
+
running,
|
|
131
|
+
}: {
|
|
132
|
+
node: AnyNode
|
|
133
|
+
part: MechanismPart
|
|
134
|
+
running: boolean
|
|
135
|
+
}) {
|
|
136
|
+
const i18n = usePascalI18n()
|
|
137
|
+
const label =
|
|
138
|
+
part.icon === 'shutters'
|
|
139
|
+
? i18n._({
|
|
140
|
+
id: 'editor.mechanismPartAction.shutters',
|
|
141
|
+
message: '{action, select, close {Close shutters} other {Open shutters}}',
|
|
142
|
+
values: { action: running ? 'close' : 'open' },
|
|
143
|
+
})
|
|
144
|
+
: i18n._({
|
|
145
|
+
id: 'editor.mechanismAction',
|
|
146
|
+
message: '{action, select, close {Close} open {Open} stop {Stop} other {Play}}',
|
|
147
|
+
values: {
|
|
148
|
+
action: part.verb === 'open' ? (running ? 'close' : 'open') : running ? 'stop' : 'play',
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
return (
|
|
152
|
+
<ActionMenuButton
|
|
153
|
+
label={label}
|
|
154
|
+
onClick={(event) => {
|
|
155
|
+
event.stopPropagation()
|
|
156
|
+
toggleMechanismPart(part, node)
|
|
157
|
+
}}
|
|
158
|
+
pressed={running}
|
|
159
|
+
>
|
|
160
|
+
{part.icon === 'shutters' ? (
|
|
161
|
+
<ShuttersGlyph open={!running} />
|
|
162
|
+
) : (
|
|
163
|
+
<MechanismGlyph icon={undefined} running={running} />
|
|
164
|
+
)}
|
|
165
|
+
</ActionMenuButton>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function MechanismPartAction({ node, part }: { node: AnyNode; part: MechanismPart }) {
|
|
170
|
+
const running = useInteractive((s) => part.isOn(node, s))
|
|
171
|
+
return <MechanismPartButton node={node} part={part} running={running} />
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Play/Stop for a kind declaring `capabilities.mechanism`, then one button per
|
|
176
|
+
* part the node has. Parts don't need the main mechanism: a fixed window can
|
|
177
|
+
* still have shutters.
|
|
178
|
+
*/
|
|
179
|
+
export function NodeMechanismActions({ node }: { node: AnyNode }) {
|
|
93
180
|
const mechanism = nodeMechanism(node)
|
|
94
|
-
const running = useInteractive((s) => (
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
|
|
181
|
+
const running = useInteractive((s) => (mechanism ? mechanism.isOn(node, s) : false))
|
|
182
|
+
return (
|
|
183
|
+
<>
|
|
184
|
+
{mechanism ? <MechanismButton mechanism={mechanism} node={node} running={running} /> : null}
|
|
185
|
+
{nodeMechanismParts(node).map((part) => (
|
|
186
|
+
<MechanismPartAction key={part.id} node={node} part={part} />
|
|
187
|
+
))}
|
|
188
|
+
</>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function MechanismActions() {
|
|
193
|
+
const selected = useViewer((s) => s.selection.selectedIds)
|
|
194
|
+
const node = useScene((s) => soleSelectedNode(selected, s.nodes))
|
|
195
|
+
return node ? <NodeMechanismActions node={node} /> : null
|
|
98
196
|
}
|
|
99
197
|
|
|
100
198
|
/**
|
|
101
199
|
* The buttons kinds add to the action menu for selections holding them
|
|
102
200
|
* (`extensions['pascal:editor/floorplan'].actionMenu.actions`), in 2D and 3D
|
|
103
|
-
* alike
|
|
201
|
+
* alike, then one per plugin inspector extension of the selected node. Each
|
|
202
|
+
* contribution reads the selection and decides its own visibility.
|
|
104
203
|
*/
|
|
105
204
|
export function RegistryActionContributions() {
|
|
106
205
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
@@ -118,7 +217,7 @@ export function RegistryActionContributions() {
|
|
|
118
217
|
)
|
|
119
218
|
return (
|
|
120
219
|
<>
|
|
121
|
-
<
|
|
220
|
+
<MechanismActions />
|
|
122
221
|
{kinds.map((kind) => {
|
|
123
222
|
const Contribution = contribution(kind)
|
|
124
223
|
return Contribution ? (
|
|
@@ -127,6 +226,7 @@ export function RegistryActionContributions() {
|
|
|
127
226
|
</Suspense>
|
|
128
227
|
) : null
|
|
129
228
|
})}
|
|
229
|
+
<InspectorExtensionActions />
|
|
130
230
|
</>
|
|
131
231
|
)
|
|
132
232
|
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
buildRiserDiagram,
|
|
7
|
+
type PascalI18n,
|
|
8
|
+
type RiserMarker,
|
|
9
|
+
usePascalI18n,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
4
12
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
13
|
import { X } from 'lucide-react'
|
|
6
14
|
import { useMemo } from 'react'
|
|
15
|
+
import { inspectorOptionDescriptor } from '../../i18n/node-messages'
|
|
7
16
|
import useEditor from '../../store/use-editor'
|
|
8
17
|
|
|
9
18
|
// Set per theme on the panel root; the light values are darker so the 9-unit
|
|
@@ -33,6 +42,7 @@ export function RiserDiagramPanel() {
|
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
function RiserDiagramContent() {
|
|
45
|
+
const i18n = usePascalI18n()
|
|
36
46
|
const setRiserOpen = useEditor((s) => s.setRiserOpen)
|
|
37
47
|
const nodes = useScene((s) => s.nodes)
|
|
38
48
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
@@ -47,15 +57,30 @@ function RiserDiagramContent() {
|
|
|
47
57
|
const ty = diagram ? -diagram.bounds.minY * SCALE + PADDING : 0
|
|
48
58
|
|
|
49
59
|
return (
|
|
50
|
-
<div
|
|
60
|
+
<div
|
|
61
|
+
data-surface="riser-diagram"
|
|
62
|
+
className="pointer-events-auto absolute top-4 right-4 z-30 flex max-h-[80vh] w-[26rem] flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 text-foreground shadow-lg backdrop-blur-xl [--riser-vent:#9333ea] [--riser-waste:#0369a1] dark:[--riser-vent:#a855f7] dark:[--riser-waste:#0ea5e9]"
|
|
63
|
+
>
|
|
51
64
|
<div className="flex items-center justify-between border-border/40 border-b px-4 py-2.5">
|
|
52
65
|
<div className="flex flex-col">
|
|
53
|
-
<span className="font-medium text-sm">
|
|
54
|
-
|
|
66
|
+
<span className="font-medium text-sm">
|
|
67
|
+
{i18n._({ id: 'editor.riserDiagramPanel.riserDiagram', message: 'Riser Diagram' })}
|
|
68
|
+
</span>
|
|
69
|
+
<span className="text-muted-foreground text-xs">
|
|
70
|
+
{i18n._({
|
|
71
|
+
id: 'editor.riserDiagramPanel.dwvPlumbingIsometric',
|
|
72
|
+
message: 'DWV plumbing isometric',
|
|
73
|
+
})}
|
|
74
|
+
</span>
|
|
55
75
|
</div>
|
|
56
76
|
<button
|
|
77
|
+
aria-label={i18n._({
|
|
78
|
+
id: 'editor.riserDiagramPanel.close',
|
|
79
|
+
message: 'Close riser diagram',
|
|
80
|
+
})}
|
|
57
81
|
className="flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-black/6 dark:hover:bg-white/10"
|
|
58
82
|
onClick={() => setRiserOpen(false)}
|
|
83
|
+
title={i18n._({ id: 'editor.riserDiagramPanel.close', message: 'Close riser diagram' })}
|
|
59
84
|
>
|
|
60
85
|
<X className="h-4 w-4 text-muted-foreground" />
|
|
61
86
|
</button>
|
|
@@ -63,11 +88,12 @@ function RiserDiagramContent() {
|
|
|
63
88
|
|
|
64
89
|
<div className="flex items-center gap-3 border-border/40 border-b px-4 py-2 text-xs">
|
|
65
90
|
<span className="flex items-center gap-1.5">
|
|
66
|
-
<span className="h-0.5 w-4" style={{ background: WASTE_COLOR }} />
|
|
91
|
+
<span className="h-0.5 w-4" style={{ background: WASTE_COLOR }} />{' '}
|
|
92
|
+
{i18n._({ id: 'editor.riserDiagramPanel.waste', message: 'Waste' })}
|
|
67
93
|
</span>
|
|
68
94
|
<span className="flex items-center gap-1.5">
|
|
69
95
|
<span className="h-0 w-4 border-t-2 border-dashed" style={{ borderColor: VENT_COLOR }} />{' '}
|
|
70
|
-
Vent
|
|
96
|
+
{i18n._({ id: 'editor.riserDiagramPanel.vent', message: 'Vent' })}
|
|
71
97
|
</span>
|
|
72
98
|
</div>
|
|
73
99
|
|
|
@@ -76,7 +102,10 @@ function RiserDiagramContent() {
|
|
|
76
102
|
<svg
|
|
77
103
|
height={Math.max(height, 120)}
|
|
78
104
|
role="img"
|
|
79
|
-
aria-label=
|
|
105
|
+
aria-label={i18n._({
|
|
106
|
+
id: 'editor.riserDiagramPanel.dwvRiserDiagram',
|
|
107
|
+
message: 'DWV riser diagram',
|
|
108
|
+
})}
|
|
80
109
|
viewBox={`0 0 ${Math.max(width, 200)} ${Math.max(height, 120)}`}
|
|
81
110
|
width="100%"
|
|
82
111
|
>
|
|
@@ -127,7 +156,7 @@ function RiserDiagramContent() {
|
|
|
127
156
|
<circle fill={MARKER_COLOR} r={3} stroke={MARKER_COLOR} strokeWidth={1.5} />
|
|
128
157
|
)}
|
|
129
158
|
<text fill={MARKER_COLOR} fontSize={9} x={8} y={3}>
|
|
130
|
-
{marker
|
|
159
|
+
{markerLabel(marker, nodes, i18n)}
|
|
131
160
|
</text>
|
|
132
161
|
</g>
|
|
133
162
|
))}
|
|
@@ -135,10 +164,31 @@ function RiserDiagramContent() {
|
|
|
135
164
|
</svg>
|
|
136
165
|
) : (
|
|
137
166
|
<div className="flex h-32 items-center justify-center px-6 text-center text-muted-foreground text-sm">
|
|
138
|
-
|
|
167
|
+
{i18n._({
|
|
168
|
+
id: 'editor.riserDiagramPanel.noDrainWasteOrVentPipes',
|
|
169
|
+
message:
|
|
170
|
+
'No drain, waste, or vent pipes yet. Draw plumbing to see the riser diagram.',
|
|
171
|
+
})}
|
|
139
172
|
</div>
|
|
140
173
|
)}
|
|
141
174
|
</div>
|
|
142
175
|
</div>
|
|
143
176
|
)
|
|
144
177
|
}
|
|
178
|
+
|
|
179
|
+
/** Core's marker labels are English ("2\" P-trap"); the panel words them in the UI language. */
|
|
180
|
+
function markerLabel(marker: RiserMarker, nodes: Record<string, AnyNode>, i18n: PascalI18n) {
|
|
181
|
+
const node = nodes[marker.nodeId]
|
|
182
|
+
if (marker.kind === 'fitting' && node?.type === 'pipe-fitting')
|
|
183
|
+
return i18n._(inspectorOptionDescriptor(node.fittingType))
|
|
184
|
+
if (!(node && 'diameter' in node && typeof node.diameter === 'number')) return marker.label
|
|
185
|
+
const values = { diameter: String(node.diameter) }
|
|
186
|
+
return marker.kind === 'trap'
|
|
187
|
+
? i18n._({ id: 'editor.riserDiagramPanel.trap', message: '{diameter}" P-trap', values })
|
|
188
|
+
: i18n._({
|
|
189
|
+
id: 'editor.riserDiagramPanel.ventTermination',
|
|
190
|
+
message: '{diameter}" VTR',
|
|
191
|
+
comment: 'Vent through roof: where a plumbing vent stack exits the roof.',
|
|
192
|
+
values,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
useScene,
|
|
13
13
|
} from '@pascal-app/core'
|
|
14
14
|
import { getSceneTheme, setSurfaceRaycastLayers, useViewer } from '@pascal-app/viewer'
|
|
15
|
-
import { Html } from '@react-three/drei'
|
|
16
15
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
17
16
|
import { SquareSplitHorizontal } from 'lucide-react'
|
|
18
17
|
import Image from 'next/image'
|
|
@@ -69,6 +68,7 @@ import {
|
|
|
69
68
|
} from '../tools/shared/draft-measurement-label'
|
|
70
69
|
import { ActionMenuButton } from './action-menu-button'
|
|
71
70
|
import { getMenuYOffset } from './floating-action-menu'
|
|
71
|
+
import { Html } from './localized-html'
|
|
72
72
|
import { NodeActionMenu } from './node-action-menu'
|
|
73
73
|
import { RoomFloorHighlight3D } from './room-floor-highlight'
|
|
74
74
|
import { RoomHandleDragPreview3D, RoomHandles3D } from './room-handles'
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
type FloorplanGeometry,
|
|
7
|
+
nodeRegistry,
|
|
8
|
+
type Point,
|
|
9
|
+
roomDrawnFloor,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
4
12
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
13
|
import { Minus, Plus } from 'lucide-react'
|
|
6
14
|
import { type PointerEvent as ReactPointerEvent, useMemo, useState } from 'react'
|
|
7
|
-
import {
|
|
15
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
16
|
+
import { useRoomRecords, useSelectedRoom } from '../../hooks/use-selected-room'
|
|
17
|
+
import { getFloorplanNodeExtension } from '../../lib/floorplan/floorplan-extension'
|
|
8
18
|
import { formatLinearMeasurement, MEASUREMENT_FLOORPLAN_COLOR } from '../../lib/measurements'
|
|
9
19
|
import {
|
|
10
20
|
commitRoomFloorElevation,
|
|
@@ -21,8 +31,13 @@ import {
|
|
|
21
31
|
useRoomHandleDrag,
|
|
22
32
|
type WallPushHandle,
|
|
23
33
|
} from '../../lib/room-handle-drag'
|
|
24
|
-
import
|
|
34
|
+
import {
|
|
35
|
+
mergeSelectionDimensions,
|
|
36
|
+
type OwnedSelectionDimension,
|
|
37
|
+
type RoomDimension,
|
|
38
|
+
} from '../../lib/room-push-dimensions'
|
|
25
39
|
import useEditor from '../../store/use-editor'
|
|
40
|
+
import useFloorplanAnnotationVisibility from '../../store/use-floorplan-annotation-visibility'
|
|
26
41
|
import useInteractionScope from '../../store/use-interaction-scope'
|
|
27
42
|
import { useFloorplanRender } from '../editor-2d/floorplan-render-context'
|
|
28
43
|
import { FloorplanGeometryRenderer } from '../editor-2d/renderers/floorplan-geometry-renderer'
|
|
@@ -158,9 +173,10 @@ function startArrow(event: ReactPointerEvent<SVGPathElement>, run: () => unknown
|
|
|
158
173
|
|
|
159
174
|
/**
|
|
160
175
|
* The selected room's arrows on the plan: one push/pull chevron per boundary
|
|
161
|
-
* span (on the far face, along the normal the handle supplies)
|
|
162
|
-
* mezzanine, one per plate edge.
|
|
163
|
-
*
|
|
176
|
+
* wall span (on the far face, along the normal the handle supplies), one per
|
|
177
|
+
* straight edge of invisible walls and, for a mezzanine, one per plate edge.
|
|
178
|
+
* Mounted after every node's handles so a wall outline never takes the press.
|
|
179
|
+
* The drags are the 3D arrows' own.
|
|
164
180
|
*/
|
|
165
181
|
export function RoomHandles2D({ levelId }: { levelId: string | null }) {
|
|
166
182
|
const room = useRoomControls()
|
|
@@ -327,6 +343,13 @@ export function RoomHandleDragPreview2D({ levelId }: { levelId: string | null })
|
|
|
327
343
|
const unit = useViewer((s) => s.unit)
|
|
328
344
|
const metricNotation = useViewer((s) => s.metricNotation)
|
|
329
345
|
if (!drag || drag.levelId !== levelId || drag.kind === 'elevation') return null
|
|
346
|
+
if (drag.kind === 'wall-group-push')
|
|
347
|
+
return (
|
|
348
|
+
<g data-room-drag-preview-2d pointerEvents="none">
|
|
349
|
+
<PlanRoomDimensions dimensions={drag.dimensions} />
|
|
350
|
+
{drag.message && <PlanReadout at={drag.anchor} refused text={drag.message} />}
|
|
351
|
+
</g>
|
|
352
|
+
)
|
|
330
353
|
const plan = drag as PlanDrag
|
|
331
354
|
const at: Point = [
|
|
332
355
|
plan.anchor[0] + plan.outward[0] * plan.distance,
|
|
@@ -352,3 +375,130 @@ export function RoomHandleDragPreview2D({ levelId }: { levelId: string | null })
|
|
|
352
375
|
</g>
|
|
353
376
|
)
|
|
354
377
|
}
|
|
378
|
+
|
|
379
|
+
function selectionDimensionsOf(node: AnyNode | undefined) {
|
|
380
|
+
return node
|
|
381
|
+
? getFloorplanNodeExtension(nodeRegistry.get(node.type))?.selectionDimensions
|
|
382
|
+
: undefined
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const NO_NODES: readonly AnyNode[] = []
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The selected nodes that show their temporary dimensions now: Select mode,
|
|
389
|
+
* nothing in progress, Display → Dimensions on, kinds with `selectionDimensions`
|
|
390
|
+
* only (a wall among other selected things still shows its own), on the first
|
|
391
|
+
* one's level. A new array only when one of them changes.
|
|
392
|
+
*/
|
|
393
|
+
export function useSelectionDimensionNodes(): readonly AnyNode[] {
|
|
394
|
+
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
395
|
+
const enabled = useEditor((s) => s.mode === 'select')
|
|
396
|
+
const idle = useInteractionScope((s) => s.scope.kind === 'idle')
|
|
397
|
+
const dragging = useRoomHandleDrag((s) => s.drag !== null)
|
|
398
|
+
const shown = useFloorplanAnnotationVisibility((s) => s.showDimensions)
|
|
399
|
+
const active = enabled && idle && !dragging && shown
|
|
400
|
+
return useScene(
|
|
401
|
+
useShallow((s) => {
|
|
402
|
+
if (!active) return NO_NODES
|
|
403
|
+
const found: AnyNode[] = []
|
|
404
|
+
for (const id of selectedIds) {
|
|
405
|
+
const node = s.nodes[id as AnyNodeId]
|
|
406
|
+
if (!node || !selectionDimensionsOf(node)) continue
|
|
407
|
+
if (found.length && node.parentId !== found[0]!.parentId) continue
|
|
408
|
+
found.push(node)
|
|
409
|
+
}
|
|
410
|
+
return found
|
|
411
|
+
}),
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* The selected nodes' temporary dimensions (`useSelectionDimensionNodes`),
|
|
417
|
+
* from each kind's `selectionDimensions` (a wall: the room widths its push
|
|
418
|
+
* shows and, for an exterior wall, the outside size, as Display → Measure
|
|
419
|
+
* walls asks). Several selected walls show each one's, a room two of them
|
|
420
|
+
* bound measured once. Shared by the plan and the 3D view; typing into one
|
|
421
|
+
* drives the node it runs from. Recomputed only when a measured node or its
|
|
422
|
+
* level's rooms change (the room index keeps its records while unrelated
|
|
423
|
+
* nodes edit).
|
|
424
|
+
*/
|
|
425
|
+
export function useSelectedWallDimensions(): {
|
|
426
|
+
levelId: string
|
|
427
|
+
dimensions: readonly OwnedSelectionDimension[]
|
|
428
|
+
} | null {
|
|
429
|
+
const measured = useSelectionDimensionNodes()
|
|
430
|
+
const faces = useFloorplanAnnotationVisibility((s) => s.wallDimensionFaces)
|
|
431
|
+
const levelId = measured[0]?.parentId ?? null
|
|
432
|
+
const rooms = useRoomRecords(levelId)
|
|
433
|
+
return useMemo(() => {
|
|
434
|
+
// `rooms` is the invalidation key: it changes when the level's room
|
|
435
|
+
// topology, slabs, ceilings or zones do.
|
|
436
|
+
if (!levelId || !rooms) return null
|
|
437
|
+
const nodes = useScene.getState().nodes
|
|
438
|
+
const dimensions = mergeSelectionDimensions(
|
|
439
|
+
measured.map((node) => ({
|
|
440
|
+
ownerId: node.id,
|
|
441
|
+
dimensions: selectionDimensionsOf(node)?.({ node, nodes, faces }) ?? [],
|
|
442
|
+
})),
|
|
443
|
+
)
|
|
444
|
+
return dimensions.length ? { levelId, dimensions } : null
|
|
445
|
+
}, [faces, levelId, measured, rooms])
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* The selected walls' temporary dimensions on the plan, drawn as the push
|
|
450
|
+
* drag draws its room widths (indigo line, end dots). Each is a `dimension`
|
|
451
|
+
* owned by the wall it runs from, so a click on its value opens the plan's
|
|
452
|
+
* type-a-value box (`FloorplanDimensionEditOverlay`) and the typed size slides
|
|
453
|
+
* that wall.
|
|
454
|
+
*/
|
|
455
|
+
export function SelectedWallDimensions2D({ levelId }: { levelId: string | null }) {
|
|
456
|
+
const selection = useSelectedWallDimensions()
|
|
457
|
+
const context = useFloorplanRender()
|
|
458
|
+
const unit = useViewer((s) => s.unit)
|
|
459
|
+
const metricNotation = useViewer((s) => s.metricNotation)
|
|
460
|
+
const owners = useMemo(() => {
|
|
461
|
+
const byOwner = new Map<string, FloorplanGeometry[]>()
|
|
462
|
+
for (const { from, to, distance, ownerId } of selection?.dimensions ?? []) {
|
|
463
|
+
const length = Math.hypot(to[0] - from[0], to[1] - from[1]) || 1
|
|
464
|
+
const geometry: FloorplanGeometry = {
|
|
465
|
+
kind: 'dimension',
|
|
466
|
+
start: from,
|
|
467
|
+
end: to,
|
|
468
|
+
offsetNormal: [-(to[1] - from[1]) / length, (to[0] - from[0]) / length],
|
|
469
|
+
offsetDistance: 0,
|
|
470
|
+
extensionOvershoot: 0,
|
|
471
|
+
extensionStartGap: 0,
|
|
472
|
+
terminator: 'dot',
|
|
473
|
+
text: formatLinearMeasurement(distance, unit, metricNotation),
|
|
474
|
+
stroke: MEASUREMENT_FLOORPLAN_COLOR,
|
|
475
|
+
}
|
|
476
|
+
const list = byOwner.get(ownerId)
|
|
477
|
+
if (list) list.push(geometry)
|
|
478
|
+
else byOwner.set(ownerId, [geometry])
|
|
479
|
+
}
|
|
480
|
+
return [...byOwner].map(([ownerId, children]) => ({
|
|
481
|
+
ownerId,
|
|
482
|
+
geometry: { kind: 'group', children } satisfies FloorplanGeometry,
|
|
483
|
+
}))
|
|
484
|
+
}, [metricNotation, selection, unit])
|
|
485
|
+
if (!selection || selection.levelId !== levelId) return null
|
|
486
|
+
return (
|
|
487
|
+
<g
|
|
488
|
+
data-selected-wall-dimensions-2d=""
|
|
489
|
+
// A press on a value is the type-a-value box's, not the plan's marquee or pan.
|
|
490
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
491
|
+
>
|
|
492
|
+
{owners.map(({ ownerId, geometry }) => (
|
|
493
|
+
<g data-node-id={ownerId} key={ownerId}>
|
|
494
|
+
<FloorplanGeometryRenderer
|
|
495
|
+
geometry={geometry}
|
|
496
|
+
pointerEventsOverride="none"
|
|
497
|
+
sceneRotationDeg={context?.sceneRotationDeg ?? 0}
|
|
498
|
+
screenUnitsPerPixel={context?.unitsPerPixel}
|
|
499
|
+
/>
|
|
500
|
+
</g>
|
|
501
|
+
))}
|
|
502
|
+
</g>
|
|
503
|
+
)
|
|
504
|
+
}
|