@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
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import {
|
|
7
7
|
getWallCurveFrameAt,
|
|
8
8
|
getWallCurveLength,
|
|
9
|
+
getWallFaceOffsets,
|
|
9
10
|
isCurvedWall,
|
|
10
11
|
type WallNode,
|
|
11
12
|
} from '@pascal-app/core'
|
|
@@ -124,6 +125,142 @@ export function findWallSnapTarget(
|
|
|
124
125
|
return bestTarget
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
// A drafted segment closer than this to a wall's direction (sine of the angle
|
|
129
|
+
// between them) runs alongside it: the wall topology stacks such walls rather
|
|
130
|
+
// than T-joining them, so the body snap leaves them alone. Two walls meeting
|
|
131
|
+
// closer than this aren't a corner either.
|
|
132
|
+
const WALL_BODY_SNAP_MIN_SINE = 0.5
|
|
133
|
+
|
|
134
|
+
/** A straight wall's reference line as a frame, with the faces its body spans. */
|
|
135
|
+
type WallBodyFrame = {
|
|
136
|
+
wall: WallNode
|
|
137
|
+
length: number
|
|
138
|
+
ux: number
|
|
139
|
+
uz: number
|
|
140
|
+
faces: { a: number; b: number }
|
|
141
|
+
/** Offset along the wall's left normal, the side its face offsets are measured on. */
|
|
142
|
+
across: (q: WallPlanPoint) => number
|
|
143
|
+
/** Station along the reference line from `start`. */
|
|
144
|
+
along: (q: WallPlanPoint) => number
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function wallBodyFrame(wall: WallNode): WallBodyFrame | null {
|
|
148
|
+
if (isCurvedWall(wall)) return null
|
|
149
|
+
const [x1, z1] = wall.start
|
|
150
|
+
const length = Math.hypot(wall.end[0] - x1, wall.end[1] - z1)
|
|
151
|
+
if (length < 1e-9) return null
|
|
152
|
+
const ux = (wall.end[0] - x1) / length
|
|
153
|
+
const uz = (wall.end[1] - z1) / length
|
|
154
|
+
return {
|
|
155
|
+
wall,
|
|
156
|
+
length,
|
|
157
|
+
ux,
|
|
158
|
+
uz,
|
|
159
|
+
faces: getWallFaceOffsets(wall),
|
|
160
|
+
across: (q) => (q[1] - z1) * ux - (q[0] - x1) * uz,
|
|
161
|
+
along: (q) => (q[0] - x1) * ux + (q[1] - z1) * uz,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The corner a join at `target` on `host`'s reference line falls into: where
|
|
167
|
+
* `host` meets another straight wall, at an L or a T, with `target` inside that
|
|
168
|
+
* wall's drawn body. A T-join there would split `host` into a stub buried in
|
|
169
|
+
* the corner block, so the end joins the junction instead. A shared end comes
|
|
170
|
+
* back exactly; walls meeting at a shallow angle aren't a corner.
|
|
171
|
+
*/
|
|
172
|
+
function cornerJunctionAt(
|
|
173
|
+
target: WallPlanPoint,
|
|
174
|
+
host: WallBodyFrame,
|
|
175
|
+
frames: WallBodyFrame[],
|
|
176
|
+
): WallPlanPoint | null {
|
|
177
|
+
for (const other of frames) {
|
|
178
|
+
if (other === host) continue
|
|
179
|
+
if (Math.abs(host.ux * other.uz - host.uz * other.ux) < WALL_BODY_SNAP_MIN_SINE) continue
|
|
180
|
+
const offset = other.across(target)
|
|
181
|
+
if (offset < other.faces.b || offset > other.faces.a) continue
|
|
182
|
+
const ends = [host.wall.start, host.wall.end, other.wall.start, other.wall.end]
|
|
183
|
+
const crossing = segmentIntersection(ends[0]!, ends[1]!, ends[2]!, ends[3]!)
|
|
184
|
+
if (!crossing) continue
|
|
185
|
+
const sharedEnd = ends.find((end) => distanceSquared(end, crossing) <= 1e-12)
|
|
186
|
+
return sharedEnd ? [sharedEnd[0], sharedEnd[1]] : crossing
|
|
187
|
+
}
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Where a drawn wall end joins a wall it is aimed into: the point on a straight
|
|
193
|
+
* wall's reference line (`start` → `end`, the line the wall topology T-joins a
|
|
194
|
+
* partition end on, whatever the wall's justification) for an end inside that
|
|
195
|
+
* wall's drawn body or within `margin` of either face.
|
|
196
|
+
*
|
|
197
|
+
* `point` is the raw cursor and `placed` its grid / angle placement; either one
|
|
198
|
+
* inside a body captures, so the grid can't leave an end a fraction short of a
|
|
199
|
+
* face. Given the drafted segment's `start`, the end slides along start →
|
|
200
|
+
* `placed` onto the line, so a square or angle-locked segment keeps its
|
|
201
|
+
* direction, and a wall the segment runs alongside (or starts on) is skipped.
|
|
202
|
+
* Without a start the end drops square onto the line, keeping its position
|
|
203
|
+
* along the wall. A join that lands inside a corner block, where the wall
|
|
204
|
+
* meets another at an L or a T, takes the junction itself (`cornerJunctionAt`).
|
|
205
|
+
* Curved walls are left to the edge snap (`findWallSnapTarget`).
|
|
206
|
+
*/
|
|
207
|
+
export function findWallBodySnapTarget(
|
|
208
|
+
point: WallPlanPoint,
|
|
209
|
+
walls: WallNode[],
|
|
210
|
+
options?: {
|
|
211
|
+
ignoreWallIds?: string[]
|
|
212
|
+
placed?: WallPlanPoint
|
|
213
|
+
start?: WallPlanPoint
|
|
214
|
+
margin?: number
|
|
215
|
+
},
|
|
216
|
+
): WallPlanPoint | null {
|
|
217
|
+
const ignored = new Set(options?.ignoreWallIds ?? [])
|
|
218
|
+
const placed = options?.placed ?? point
|
|
219
|
+
const start = options?.start
|
|
220
|
+
const margin = options?.margin ?? WALL_CONNECT_SNAP_RADIUS
|
|
221
|
+
const frames = walls.flatMap((wall) => {
|
|
222
|
+
const frame = ignored.has(wall.id) ? null : wallBodyFrame(wall)
|
|
223
|
+
return frame ? [frame] : []
|
|
224
|
+
})
|
|
225
|
+
let best: WallPlanPoint | null = null
|
|
226
|
+
let bestOutside = Number.POSITIVE_INFINITY
|
|
227
|
+
|
|
228
|
+
for (const frame of frames) {
|
|
229
|
+
const { length, ux, uz, faces, across, along } = frame
|
|
230
|
+
const [x1, z1] = frame.wall.start
|
|
231
|
+
const onWall = (q: WallPlanPoint) => along(q) > 0 && along(q) < length
|
|
232
|
+
const outsideBody = (q: WallPlanPoint) =>
|
|
233
|
+
onWall(q) ? Math.max(0, faces.b - across(q), across(q) - faces.a) : Number.POSITIVE_INFINITY
|
|
234
|
+
const outside = Math.min(outsideBody(point), outsideBody(placed))
|
|
235
|
+
if (outside > margin || outside >= bestOutside) continue
|
|
236
|
+
|
|
237
|
+
let target: WallPlanPoint | null
|
|
238
|
+
if (Math.abs(across(point)) <= 1e-9 && onWall(point)) {
|
|
239
|
+
// Already on the line: a joined end snapped again (the plan view hands
|
|
240
|
+
// the 3D tool its resolved point) stays put.
|
|
241
|
+
target = [point[0], point[1]]
|
|
242
|
+
} else if (start) {
|
|
243
|
+
const dx = placed[0] - start[0]
|
|
244
|
+
const dz = placed[1] - start[1]
|
|
245
|
+
const run = Math.hypot(dx, dz)
|
|
246
|
+
if (run < 1e-9 || Math.abs(dx * uz - dz * ux) / run < WALL_BODY_SNAP_MIN_SINE) continue
|
|
247
|
+
const startAcross = across(start)
|
|
248
|
+
const t = startAcross / (startAcross - across(placed))
|
|
249
|
+
if (!(t > 1e-6)) continue
|
|
250
|
+
target = [start[0] + dx * t, start[1] + dz * t]
|
|
251
|
+
if (!onWall(target)) continue
|
|
252
|
+
} else {
|
|
253
|
+
const station = along(placed)
|
|
254
|
+
if (!(station > 0 && station < length)) continue
|
|
255
|
+
target = [x1 + ux * station, z1 + uz * station]
|
|
256
|
+
}
|
|
257
|
+
best = cornerJunctionAt(target, frame, frames) ?? target
|
|
258
|
+
bestOutside = outside
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return best
|
|
262
|
+
}
|
|
263
|
+
|
|
127
264
|
/**
|
|
128
265
|
* Wall ids that contain an already-resolved snap point.
|
|
129
266
|
*
|
|
@@ -184,9 +184,9 @@ export const ZoneTool: React.FC = () => {
|
|
|
184
184
|
updateLines()
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
187
|
+
// Snap on the building's own grid lattice, the one its walls are drafted
|
|
188
|
+
// on (square to a turned house). Grid quantize only in grid mode; off /
|
|
189
|
+
// lines / angles leave the raw cursor.
|
|
190
190
|
const gridPointOf = (event: GridEvent): [number, number] =>
|
|
191
191
|
isGridSnapActive()
|
|
192
192
|
? snapWorldXZForActiveBuilding(
|
|
@@ -44,6 +44,8 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
|
|
|
44
44
|
// These are toggles, not commands: the armed/unarmed state has to be
|
|
45
45
|
// readable without decoding the active-state styling.
|
|
46
46
|
aria-pressed={isActive}
|
|
47
|
+
// Icon-only: the tooltip text is the button's name.
|
|
48
|
+
aria-label={label}
|
|
47
49
|
className={cn('relative h-11 w-11 transition-all', className)}
|
|
48
50
|
onClick={(event) => {
|
|
49
51
|
triggerSFX('sfx:menu-click')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { emitter } from '@pascal-app/core'
|
|
3
|
+
import { emitter, usePascalI18n } from '@pascal-app/core'
|
|
4
4
|
import Image from 'next/image'
|
|
5
5
|
import useEditor from '../../../store/use-editor'
|
|
6
6
|
import { visibleScene } from '../../../store/view-layout'
|
|
@@ -8,6 +8,7 @@ import { ActionButton } from './action-button'
|
|
|
8
8
|
import { FollowPascalToggle } from './follow-pascal-toggle'
|
|
9
9
|
|
|
10
10
|
export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
11
|
+
const i18n = usePascalI18n()
|
|
11
12
|
// Orbit stays useful in 2D-only (it spins the synced floorplan view), but
|
|
12
13
|
// top view only tilts the hidden 3D camera — pointless without the canvas.
|
|
13
14
|
const is2dOnly = useEditor((s) => visibleScene(s) === '2d')
|
|
@@ -31,13 +32,13 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
|
31
32
|
{/* Orbit CCW */}
|
|
32
33
|
<ActionButton
|
|
33
34
|
className="group hover:bg-black/4"
|
|
34
|
-
label=
|
|
35
|
+
label={i18n._({ id: 'editor.actionMenu.cameraActions.orbitLeft', message: 'Orbit Left' })}
|
|
35
36
|
onClick={orbitCCW}
|
|
36
37
|
size="icon"
|
|
37
38
|
variant="ghost"
|
|
38
39
|
>
|
|
39
40
|
<Image
|
|
40
|
-
alt=
|
|
41
|
+
alt={i18n._({ id: 'editor.actionMenu.cameraActions.orbitLeft', message: 'Orbit Left' })}
|
|
41
42
|
className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
42
43
|
height={28}
|
|
43
44
|
src="/icons/rotate.webp"
|
|
@@ -48,13 +49,13 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
|
48
49
|
{/* Orbit CW */}
|
|
49
50
|
<ActionButton
|
|
50
51
|
className="group hover:bg-black/4"
|
|
51
|
-
label=
|
|
52
|
+
label={i18n._({ id: 'editor.actionMenu.cameraActions.orbitRight', message: 'Orbit Right' })}
|
|
52
53
|
onClick={orbitCW}
|
|
53
54
|
size="icon"
|
|
54
55
|
variant="ghost"
|
|
55
56
|
>
|
|
56
57
|
<Image
|
|
57
|
-
alt=
|
|
58
|
+
alt={i18n._({ id: 'editor.actionMenu.cameraActions.orbitRight', message: 'Orbit Right' })}
|
|
58
59
|
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
59
60
|
height={28}
|
|
60
61
|
src="/icons/rotate.webp"
|
|
@@ -71,13 +72,13 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
|
71
72
|
{!is2dOnly && (
|
|
72
73
|
<ActionButton
|
|
73
74
|
className="group hover:bg-black/4"
|
|
74
|
-
label=
|
|
75
|
+
label={i18n._({ id: 'editor.actionMenu.cameraActions.topView', message: 'Top View' })}
|
|
75
76
|
onClick={goToTopView}
|
|
76
77
|
size="icon"
|
|
77
78
|
variant="ghost"
|
|
78
79
|
>
|
|
79
80
|
<Image
|
|
80
|
-
alt=
|
|
81
|
+
alt={i18n._({ id: 'editor.actionMenu.cameraActions.topView', message: 'Top View' })}
|
|
81
82
|
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
82
83
|
height={28}
|
|
83
84
|
src="/icons/topview.webp"
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { usePascalI18n } from '@pascal-app/core'
|
|
4
|
+
import type { MessageDescriptor } from '@pascal-app/core'
|
|
5
|
+
|
|
3
6
|
import { Icon } from '@iconify/react'
|
|
4
7
|
import { type LucideIcon, Trash2 } from 'lucide-react'
|
|
5
8
|
import Image from 'next/image'
|
|
@@ -17,7 +20,7 @@ type ControlConfig = {
|
|
|
17
20
|
icon?: LucideIcon
|
|
18
21
|
iconifyIcon?: string
|
|
19
22
|
imageSrc?: string
|
|
20
|
-
label:
|
|
23
|
+
label: MessageDescriptor
|
|
21
24
|
shortcut?: string
|
|
22
25
|
color: string
|
|
23
26
|
activeColor: string
|
|
@@ -28,7 +31,7 @@ const controls: ControlConfig[] = [
|
|
|
28
31
|
{
|
|
29
32
|
id: 'select',
|
|
30
33
|
imageSrc: '/icons/select.webp',
|
|
31
|
-
label: 'Select',
|
|
34
|
+
label: /* i18n */ { id: 'editor.actionMenu.controlModes.select', message: 'Select' },
|
|
32
35
|
shortcut: 'V',
|
|
33
36
|
color: 'hover:bg-blue-500/20 hover:text-blue-600 dark:hover:text-blue-400',
|
|
34
37
|
activeColor: 'bg-blue-500/20 text-blue-600 dark:text-blue-400 dark:hover:text-accent-foreground',
|
|
@@ -36,7 +39,7 @@ const controls: ControlConfig[] = [
|
|
|
36
39
|
{
|
|
37
40
|
id: 'delete',
|
|
38
41
|
icon: Trash2,
|
|
39
|
-
label: 'Delete',
|
|
42
|
+
label: /* i18n */ { id: 'editor.actionMenu.controlModes.delete', message: 'Delete' },
|
|
40
43
|
shortcut: 'X',
|
|
41
44
|
color: 'hover:bg-red-500/20 hover:text-red-600 dark:hover:text-red-400',
|
|
42
45
|
activeColor: 'bg-red-500/20 text-red-600 dark:text-red-400 dark:hover:text-accent-foreground',
|
|
@@ -44,6 +47,7 @@ const controls: ControlConfig[] = [
|
|
|
44
47
|
]
|
|
45
48
|
|
|
46
49
|
export function ControlModes() {
|
|
50
|
+
const i18n = usePascalI18n()
|
|
47
51
|
const mode = useEditor((state) => state.mode)
|
|
48
52
|
const selectionTool = useEditor((state) => state.floorplanSelectionTool)
|
|
49
53
|
const armToolMode = useEditor((state) => state.armToolMode)
|
|
@@ -94,7 +98,7 @@ export function ControlModes() {
|
|
|
94
98
|
// tour at this button. Nothing here reads it.
|
|
95
99
|
data-guide-target={c.id === 'select' ? 'mode-select' : undefined}
|
|
96
100
|
isActive={isActive}
|
|
97
|
-
label={c.label}
|
|
101
|
+
label={i18n._(c.label)}
|
|
98
102
|
onClick={() => handleClick(c.id)}
|
|
99
103
|
shortcut={c.shortcut}
|
|
100
104
|
size="icon"
|
|
@@ -102,7 +106,7 @@ export function ControlModes() {
|
|
|
102
106
|
>
|
|
103
107
|
{c.imageSrc ? (
|
|
104
108
|
<Image
|
|
105
|
-
alt={c.label}
|
|
109
|
+
alt={i18n._(c.label)}
|
|
106
110
|
className={cn(
|
|
107
111
|
'h-[28px] w-[28px] object-contain transition-[opacity,filter] duration-200',
|
|
108
112
|
isActive
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { usePascalI18n } from '@pascal-app/core'
|
|
3
4
|
import { AnimatePresence, motion } from 'motion/react'
|
|
4
5
|
import { useReducedMotion } from '../../../hooks/use-reduced-motion'
|
|
5
6
|
import useFollowPascal from '../../../store/use-follow-pascal'
|
|
@@ -9,6 +10,7 @@ import useFollowPascal from '../../../store/use-follow-pascal'
|
|
|
9
10
|
* following. It comes up from where the toolbar is (origin-aware) and goes the same way.
|
|
10
11
|
*/
|
|
11
12
|
export function FollowPascalPill() {
|
|
13
|
+
const i18n = usePascalI18n()
|
|
12
14
|
const shows = useFollowPascal((state) => state.on && state.paused && state.building)
|
|
13
15
|
const resume = useFollowPascal((state) => state.resume)
|
|
14
16
|
const reduced = useReducedMotion()
|
|
@@ -24,7 +26,7 @@ export function FollowPascalPill() {
|
|
|
24
26
|
transition={reduced ? { duration: 0.12 } : { type: 'spring', bounce: 0, duration: 0.3 }}
|
|
25
27
|
type="button"
|
|
26
28
|
>
|
|
27
|
-
Resume following
|
|
29
|
+
{i18n._({ id: 'editor.actionMenu.followPascalPill.resumeFollowing', message: 'Resume following' })}
|
|
28
30
|
</motion.button>
|
|
29
31
|
) : null}
|
|
30
32
|
</AnimatePresence>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { usePascalI18n } from '@pascal-app/core'
|
|
3
4
|
import { Video } from 'lucide-react'
|
|
4
5
|
import { cn } from '../../../lib/utils'
|
|
5
6
|
import useFollowPascal from '../../../store/use-follow-pascal'
|
|
@@ -13,6 +14,7 @@ export function FollowPascalToggle({
|
|
|
13
14
|
className?: string
|
|
14
15
|
tooltipClassName?: string
|
|
15
16
|
}) {
|
|
17
|
+
const i18n = usePascalI18n()
|
|
16
18
|
const on = useFollowPascal((state) => state.on)
|
|
17
19
|
const setOn = useFollowPascal((state) => state.setOn)
|
|
18
20
|
return (
|
|
@@ -24,7 +26,11 @@ export function FollowPascalToggle({
|
|
|
24
26
|
: 'hover:bg-black/4 hover:text-violet-600 dark:hover:text-violet-300',
|
|
25
27
|
className,
|
|
26
28
|
)}
|
|
27
|
-
label={
|
|
29
|
+
label={
|
|
30
|
+
on
|
|
31
|
+
? i18n._({ id: 'editor.actionMenu.followPascalToggle.on', message: 'Follow Pascal: On' })
|
|
32
|
+
: i18n._({ id: 'editor.actionMenu.followPascalToggle.off', message: 'Follow Pascal: Off' })
|
|
33
|
+
}
|
|
28
34
|
onClick={() => setOn(!on)}
|
|
29
35
|
size="icon"
|
|
30
36
|
tooltipClassName={tooltipClassName}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import type { MessageDescriptor } from '@pascal-app/core'
|
|
1
2
|
import type { CatalogCategory } from './../../../store/use-editor'
|
|
2
3
|
|
|
3
4
|
export type FurnishToolConfig = {
|
|
4
5
|
id: 'item'
|
|
5
6
|
iconSrc: string
|
|
6
|
-
label:
|
|
7
|
+
label: MessageDescriptor
|
|
7
8
|
catalogCategory: CatalogCategory
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export const furnishTools: FurnishToolConfig[] = [
|
|
11
|
-
{ id: 'item', iconSrc: '/icons/couch.webp', label: 'Furniture', catalogCategory: 'furniture' },
|
|
12
|
-
{ id: 'item', iconSrc: '/icons/appliance.webp', label: 'Appliance', catalogCategory: 'appliance' },
|
|
13
|
-
{ id: 'item', iconSrc: '/icons/kitchen.webp', label: 'Kitchen', catalogCategory: 'kitchen' },
|
|
14
|
-
{ id: 'item', iconSrc: '/icons/bathroom.webp', label: 'Bathroom', catalogCategory: 'bathroom' },
|
|
15
|
-
{ id: 'item', iconSrc: '/icons/tree.webp', label: 'Outdoor', catalogCategory: 'outdoor' },
|
|
12
|
+
{ id: 'item', iconSrc: '/icons/couch.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.furniture', message: 'Furniture' }, catalogCategory: 'furniture' },
|
|
13
|
+
{ id: 'item', iconSrc: '/icons/appliance.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.appliance', message: 'Appliance' }, catalogCategory: 'appliance' },
|
|
14
|
+
{ id: 'item', iconSrc: '/icons/kitchen.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.kitchen', message: 'Kitchen' }, catalogCategory: 'kitchen' },
|
|
15
|
+
{ id: 'item', iconSrc: '/icons/bathroom.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.bathroom', message: 'Bathroom' }, catalogCategory: 'bathroom' },
|
|
16
|
+
{ id: 'item', iconSrc: '/icons/tree.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.outdoor', message: 'Outdoor' }, catalogCategory: 'outdoor' },
|
|
16
17
|
]
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import type { MessageDescriptor } from '@pascal-app/core'
|
|
4
|
+
|
|
5
|
+
import { usePascalI18n } from '@pascal-app/core'
|
|
3
6
|
import type {
|
|
4
7
|
ConstructionDimensionChainMode,
|
|
5
8
|
ConstructionDimensionMode,
|
|
@@ -29,40 +32,41 @@ import { ActionButton } from './action-button'
|
|
|
29
32
|
import { useActionMenuPopupSide } from './placement'
|
|
30
33
|
|
|
31
34
|
const measurementOptions = [
|
|
32
|
-
{ kind: 'distance', label: 'Distance', icon: Ruler },
|
|
33
|
-
{ kind: 'angle', label: 'Angle', icon: Triangle },
|
|
34
|
-
{ kind: 'area', label: 'Area', icon: Square },
|
|
35
|
-
{ kind: 'perimeter', label: 'Perimeter', icon: Waypoints },
|
|
36
|
-
{ kind: 'volume', label: 'Volume', icon: Box },
|
|
35
|
+
{ kind: 'distance', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.distance', message: 'Distance' }, icon: Ruler },
|
|
36
|
+
{ kind: 'angle', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.angle', message: 'Angle' }, icon: Triangle },
|
|
37
|
+
{ kind: 'area', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.area', message: 'Area' }, icon: Square },
|
|
38
|
+
{ kind: 'perimeter', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.perimeter', message: 'Perimeter' }, icon: Waypoints },
|
|
39
|
+
{ kind: 'volume', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.volume', message: 'Volume' }, icon: Box },
|
|
37
40
|
] as const satisfies readonly {
|
|
38
41
|
kind: CreatableMeasurementKind
|
|
39
|
-
label:
|
|
42
|
+
label: MessageDescriptor
|
|
40
43
|
icon: typeof Ruler
|
|
41
44
|
}[]
|
|
42
45
|
|
|
43
46
|
const measurementMenuOptions = [
|
|
44
|
-
{ kind: 'smart', label: 'Smart', icon: ScanSearch },
|
|
47
|
+
{ kind: 'smart', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.smart', message: 'Smart' }, icon: ScanSearch },
|
|
45
48
|
...measurementOptions,
|
|
46
49
|
] as const
|
|
47
50
|
|
|
48
51
|
const constructionDimensionOptions = [
|
|
49
|
-
{ mode: 'linear', chainMode: 'point-to-point', label: 'Linear dimension', icon: Ruler },
|
|
50
|
-
{ mode: 'linear', chainMode: 'continuous', label: 'Continuous dimension', icon: Waypoints },
|
|
51
|
-
{ mode: 'radius', chainMode: 'point-to-point', label: 'Radius dimension', icon: CircleIcon },
|
|
52
|
-
{ mode: 'diameter', chainMode: 'point-to-point', label: 'Diameter dimension', icon: CircleIcon },
|
|
53
|
-
{ mode: 'center-mark', chainMode: 'point-to-point', label: 'Center mark', icon: Crosshair },
|
|
54
|
-
{ mode: 'chord', chainMode: 'point-to-point', label: 'Chord dimension', icon: Minus },
|
|
55
|
-
{ mode: 'arc-length', chainMode: 'point-to-point', label: 'Arc length', icon: CircleIcon },
|
|
56
|
-
{ mode: 'angular', chainMode: 'point-to-point', label: 'Angular dimension', icon: Triangle },
|
|
57
|
-
{ mode: 'coordinate', chainMode: 'continuous', label: 'Coordinate dimensions', icon: Grid2X2 },
|
|
52
|
+
{ mode: 'linear', chainMode: 'point-to-point', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.linearDimension', message: 'Linear dimension' }, icon: Ruler },
|
|
53
|
+
{ mode: 'linear', chainMode: 'continuous', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.continuousDimension', message: 'Continuous dimension' }, icon: Waypoints },
|
|
54
|
+
{ mode: 'radius', chainMode: 'point-to-point', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.radiusDimension', message: 'Radius dimension' }, icon: CircleIcon },
|
|
55
|
+
{ mode: 'diameter', chainMode: 'point-to-point', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.diameterDimension', message: 'Diameter dimension' }, icon: CircleIcon },
|
|
56
|
+
{ mode: 'center-mark', chainMode: 'point-to-point', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.centerMark', message: 'Center mark' }, icon: Crosshair },
|
|
57
|
+
{ mode: 'chord', chainMode: 'point-to-point', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.chordDimension', message: 'Chord dimension' }, icon: Minus },
|
|
58
|
+
{ mode: 'arc-length', chainMode: 'point-to-point', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.arcLength', message: 'Arc length' }, icon: CircleIcon },
|
|
59
|
+
{ mode: 'angular', chainMode: 'point-to-point', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.angularDimension', message: 'Angular dimension' }, icon: Triangle },
|
|
60
|
+
{ mode: 'coordinate', chainMode: 'continuous', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.coordinateDimensions', message: 'Coordinate dimensions' }, icon: Grid2X2 },
|
|
58
61
|
] as const satisfies readonly {
|
|
59
62
|
mode: ConstructionDimensionMode
|
|
60
63
|
chainMode: ConstructionDimensionChainMode
|
|
61
|
-
label:
|
|
64
|
+
label: MessageDescriptor
|
|
62
65
|
icon: typeof Ruler
|
|
63
66
|
}[]
|
|
64
67
|
|
|
65
68
|
export function MeasurementControl() {
|
|
69
|
+
const i18n = usePascalI18n()
|
|
66
70
|
const popupSide = useActionMenuPopupSide()
|
|
67
71
|
const [isOpen, setIsOpen] = useState(false)
|
|
68
72
|
const mode = useEditor((state) => state.mode)
|
|
@@ -98,11 +102,13 @@ export function MeasurementControl() {
|
|
|
98
102
|
: isSmartActive
|
|
99
103
|
? ScanSearch
|
|
100
104
|
: selectedOption.icon
|
|
101
|
-
const selectedLabel =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
const selectedLabel = i18n._(
|
|
106
|
+
isConstructionDimensionActive
|
|
107
|
+
? (activeConstructionDimensionOption ?? constructionDimensionOptions[0]).label
|
|
108
|
+
: isSmartActive
|
|
109
|
+
? measurementMenuOptions[0].label
|
|
110
|
+
: selectedOption.label,
|
|
111
|
+
)
|
|
106
112
|
|
|
107
113
|
const activateMeasurement = (kind: CreatableMeasurementKind) => {
|
|
108
114
|
setLastMeasurementKind(kind)
|
|
@@ -143,7 +149,7 @@ export function MeasurementControl() {
|
|
|
143
149
|
<Popover onOpenChange={setIsOpen} open={isOpen}>
|
|
144
150
|
<div className="flex items-center">
|
|
145
151
|
<ActionButton
|
|
146
|
-
aria-label={
|
|
152
|
+
aria-label={i18n._({ id: 'editor.actionMenu.measurementControl.measureSelectedlabel', message: 'Measure: {selectedLabel}', values: { selectedLabel } })}
|
|
147
153
|
aria-pressed={isControlActive}
|
|
148
154
|
className={cn(
|
|
149
155
|
'rounded-r-none p-0 text-muted-foreground',
|
|
@@ -151,7 +157,7 @@ export function MeasurementControl() {
|
|
|
151
157
|
? 'bg-cyan-500/20 text-cyan-700 hover:bg-cyan-500/20 dark:text-cyan-400 dark:hover:text-accent-foreground'
|
|
152
158
|
: 'hover:bg-cyan-500/15 hover:text-cyan-700 dark:hover:text-cyan-400',
|
|
153
159
|
)}
|
|
154
|
-
label={
|
|
160
|
+
label={i18n._({ id: 'editor.actionMenu.measurementControl.measureSelectedlabel', message: 'Measure: {selectedLabel}', values: { selectedLabel } })}
|
|
155
161
|
onClick={handlePrimaryClick}
|
|
156
162
|
shortcut="M"
|
|
157
163
|
size="icon"
|
|
@@ -164,7 +170,7 @@ export function MeasurementControl() {
|
|
|
164
170
|
<button
|
|
165
171
|
aria-expanded={isOpen}
|
|
166
172
|
aria-haspopup="menu"
|
|
167
|
-
aria-label=
|
|
173
|
+
aria-label={i18n._({ id: 'editor.actionMenu.measurementControl.measurementOptions', message: 'Measurement options' })}
|
|
168
174
|
className={cn(
|
|
169
175
|
'flex h-11 w-6 items-center justify-center rounded-r-lg text-muted-foreground transition-colors',
|
|
170
176
|
isOpen
|
|
@@ -187,7 +193,7 @@ export function MeasurementControl() {
|
|
|
187
193
|
side={popupSide}
|
|
188
194
|
sideOffset={14}
|
|
189
195
|
>
|
|
190
|
-
<div aria-label=
|
|
196
|
+
<div aria-label={i18n._({ id: 'editor.actionMenu.measurementControl.measurementType', message: 'Measurement type' })} className="space-y-1" role="menu">
|
|
191
197
|
{measurementMenuOptions.map((option) => {
|
|
192
198
|
const OptionIcon = option.icon
|
|
193
199
|
const isSmart = option.kind === 'smart'
|
|
@@ -213,7 +219,7 @@ export function MeasurementControl() {
|
|
|
213
219
|
type="button"
|
|
214
220
|
>
|
|
215
221
|
<OptionIcon aria-hidden="true" className="h-4 w-4" />
|
|
216
|
-
<span>{option.label}</span>
|
|
222
|
+
<span>{i18n._(option.label)}</span>
|
|
217
223
|
{isSelected ? <Check aria-hidden="true" className="ml-auto h-4 w-4" /> : null}
|
|
218
224
|
</button>
|
|
219
225
|
)
|
|
@@ -223,7 +229,7 @@ export function MeasurementControl() {
|
|
|
223
229
|
<>
|
|
224
230
|
<div className="my-1.5 h-px bg-border/60" />
|
|
225
231
|
<div className="px-2.5 pt-1 pb-0.5 font-semibold text-[10px] text-muted-foreground uppercase tracking-wider">
|
|
226
|
-
Floor plan
|
|
232
|
+
{i18n._({ id: 'editor.actionMenu.measurementControl.floorPlan', message: 'Floor plan' })}
|
|
227
233
|
</div>
|
|
228
234
|
|
|
229
235
|
{constructionDimensionOptions.map((option) => {
|
|
@@ -248,7 +254,7 @@ export function MeasurementControl() {
|
|
|
248
254
|
type="button"
|
|
249
255
|
>
|
|
250
256
|
<OptionIcon aria-hidden="true" className="h-4 w-4" />
|
|
251
|
-
<span>{option.label}</span>
|
|
257
|
+
<span>{i18n._(option.label)}</span>
|
|
252
258
|
{isSelected ? <Check aria-hidden="true" className="ml-auto h-4 w-4" /> : null}
|
|
253
259
|
</button>
|
|
254
260
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useHasPointAskHandler } from '@pascal-app/core'
|
|
3
|
+
import { useHasPointAskHandler, usePascalI18n } from '@pascal-app/core'
|
|
4
4
|
import { canPoint, enterPointMode, leavePointMode } from '../../../lib/point-ask/session'
|
|
5
5
|
import { cn } from '../../../lib/utils'
|
|
6
6
|
import useEditor from '../../../store/use-editor'
|
|
@@ -14,6 +14,7 @@ import { ActionButton } from './action-button'
|
|
|
14
14
|
* the 2D plan and the walkthrough it is disabled and says so (v1 works in the 3D view).
|
|
15
15
|
*/
|
|
16
16
|
export function PointAskButton() {
|
|
17
|
+
const i18n = usePascalI18n()
|
|
17
18
|
const available = useHasPointAskHandler()
|
|
18
19
|
const active = usePointAsk((s) => s.active)
|
|
19
20
|
const sceneVisible = useEditor((s) => isViewVisible(s, VIEW_3D))
|
|
@@ -31,7 +32,7 @@ export function PointAskButton() {
|
|
|
31
32
|
data-guide-target="mode-point-ask"
|
|
32
33
|
disabled={unavailable}
|
|
33
34
|
isActive={active}
|
|
34
|
-
label=
|
|
35
|
+
label={i18n._({ id: 'editor.actionMenu.pointAskButton.pointAndAsk', message: 'Point and ask' })}
|
|
35
36
|
onClick={() => {
|
|
36
37
|
if (active) leavePointMode()
|
|
37
38
|
else if (canPoint()) enterPointMode(true)
|
|
@@ -39,7 +40,7 @@ export function PointAskButton() {
|
|
|
39
40
|
shortcut="C"
|
|
40
41
|
size="icon"
|
|
41
42
|
tooltipContent={
|
|
42
|
-
unavailable ? <p>Point and ask works in the 3D view</p> : <p>Point and ask (C)</p>
|
|
43
|
+
unavailable ? <p>{i18n._({ id: 'editor.actionMenu.pointAskButton.pointAndAskWorksInThe', message: 'Point and ask works in the 3D view' })}</p> : <p>{i18n._({ id: 'editor.actionMenu.pointAskButton.pointAndAskC', message: 'Point and ask (C)' })}</p>
|
|
43
44
|
}
|
|
44
45
|
variant="ghost"
|
|
45
46
|
>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type { MessageDescriptor } from '@pascal-app/core'
|
|
1
2
|
import type { CatalogCategory, StructureTool } from '../../../store/use-editor'
|
|
2
3
|
|
|
3
4
|
export type ToolConfig = {
|
|
4
5
|
id: StructureTool
|
|
5
6
|
iconSrc: string
|
|
6
|
-
label:
|
|
7
|
+
label: MessageDescriptor
|
|
7
8
|
catalogCategory?: CatalogCategory
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -12,26 +13,27 @@ export type ToolConfig = {
|
|
|
12
13
|
// for cursor/floorplan indicators. Roof-mounted accessories are intentionally
|
|
13
14
|
// absent — they're placed from the roof inspector's "Add element" section.
|
|
14
15
|
export const tools: ToolConfig[] = [
|
|
15
|
-
{ id: 'wall', iconSrc: '/icons/wall.webp', label: 'Wall' },
|
|
16
|
-
{ id: 'door', iconSrc: '/icons/door.webp', label: 'Door' },
|
|
17
|
-
{ id: 'window', iconSrc: '/icons/window.webp', label: 'Window' },
|
|
18
|
-
{ id: 'stair', iconSrc: '/icons/stairs.webp', label: 'Stairs' },
|
|
19
|
-
{ id: 'roof', iconSrc: '/icons/roof.webp', label: 'Gable Roof' },
|
|
20
|
-
{ id: 'fence', iconSrc: '/icons/fence.webp', label: 'Fence' },
|
|
21
|
-
{ id: 'column', iconSrc: '/icons/column.webp', label: 'Column' },
|
|
22
|
-
{ id: '
|
|
23
|
-
{ id: '
|
|
24
|
-
{ id: '
|
|
25
|
-
{ id: '
|
|
26
|
-
{ id: '
|
|
27
|
-
{ id: '
|
|
28
|
-
{ id: '
|
|
29
|
-
{ id: 'duct-
|
|
30
|
-
{ id: 'duct-
|
|
31
|
-
{ id: '
|
|
32
|
-
{ id: '
|
|
33
|
-
{ id: 'pipe-
|
|
34
|
-
{ id: 'pipe-
|
|
35
|
-
{ id: '
|
|
36
|
-
{ id: '
|
|
16
|
+
{ id: 'wall', iconSrc: '/icons/wall.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.wall', message: 'Wall' } },
|
|
17
|
+
{ id: 'door', iconSrc: '/icons/door.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.door', message: 'Door' } },
|
|
18
|
+
{ id: 'window', iconSrc: '/icons/window.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.window', message: 'Window' } },
|
|
19
|
+
{ id: 'stair', iconSrc: '/icons/stairs.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.stairs', message: 'Stairs' } },
|
|
20
|
+
{ id: 'roof', iconSrc: '/icons/roof.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.gableRoof', message: 'Gable Roof' } },
|
|
21
|
+
{ id: 'fence', iconSrc: '/icons/fence.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.fence', message: 'Fence' } },
|
|
22
|
+
{ id: 'column', iconSrc: '/icons/column.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.column', message: 'Column' } },
|
|
23
|
+
{ id: 'beam', iconSrc: '/icons/beam.svg', label: /* i18n */ { id: 'editor.actionMenu.structureTools.beam', message: 'Beam' } },
|
|
24
|
+
{ id: 'elevator', iconSrc: '/icons/elevator.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.elevator', message: 'Elevator' } },
|
|
25
|
+
{ id: 'slab', iconSrc: '/icons/floor.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.slab', message: 'Slab' } },
|
|
26
|
+
{ id: 'ceiling', iconSrc: '/icons/ceiling.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.ceiling', message: 'Ceiling' } },
|
|
27
|
+
{ id: 'zone', iconSrc: '/icons/zone.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.zone', message: 'Zone' } },
|
|
28
|
+
{ id: 'spawn', iconSrc: '/icons/spawn-point.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.spawnPoint', message: 'Spawn Point' } },
|
|
29
|
+
{ id: 'shelf', iconSrc: '/icons/shelf.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.shelf', message: 'Shelf' } },
|
|
30
|
+
{ id: 'duct-segment', iconSrc: '/icons/duct.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.duct', message: 'Duct' } },
|
|
31
|
+
{ id: 'duct-fitting', iconSrc: '/icons/duct-fitting.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.ductFitting', message: 'Duct Fitting' } },
|
|
32
|
+
{ id: 'duct-terminal', iconSrc: '/icons/registers.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.register', message: 'Register' } },
|
|
33
|
+
{ id: 'hvac-equipment', iconSrc: '/icons/HVAC.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.hvacUnit', message: 'HVAC Unit' } },
|
|
34
|
+
{ id: 'pipe-segment', iconSrc: '/icons/dwv-pipes.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.dwvPipe', message: 'DWV Pipe' } },
|
|
35
|
+
{ id: 'pipe-trap', iconSrc: '/icons/dwv-pipes.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.trap', message: 'Trap' } },
|
|
36
|
+
{ id: 'pipe-fitting', iconSrc: '/icons/duct-fitting.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.pipeFitting', message: 'Pipe Fitting' } },
|
|
37
|
+
{ id: 'lineset', iconSrc: '/icons/lineset.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.lineset', message: 'Lineset' } },
|
|
38
|
+
{ id: 'liquid-line', iconSrc: '/icons/lineset.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.liquidLine', message: 'Liquid Line' } },
|
|
37
39
|
]
|