@pascal-app/editor 1.0.0-beta.5 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +5 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +55 -51
- package/src/components/editor/floorplan-panel.tsx +99 -15
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +78 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +23 -3
- package/src/components/editor/index.tsx +151 -24
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +67 -119
- package/src/components/editor/selection-manager.tsx +153 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -239
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +226 -35
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +304 -171
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +10 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +116 -44
- package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +56 -17
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +63 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +44 -21
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +103 -10
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +266 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +35 -164
- package/src/components/ui/panels/reference-panel.tsx +34 -4
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +502 -31
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -7
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/viewer-scene-header.tsx +5 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +144 -0
- package/src/hooks/use-ceiling-events.ts +5 -1
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +132 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +87 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +536 -2
- package/src/lib/floorplan/floorplan-export.tsx +120 -19
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +5 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +1375 -21
- package/src/lib/glb-export.ts +781 -106
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +455 -0
- package/src/lib/portable-export.ts +1042 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +379 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +200 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +38 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +65 -0
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-send-to-app.ts +21 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
resumeSpaceDetection,
|
|
8
8
|
useScene,
|
|
9
9
|
} from '@pascal-app/core'
|
|
10
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
10
|
+
import { cancelPerfAction, markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
11
11
|
import { useEffect } from 'react'
|
|
12
12
|
import { Vector3 } from 'three'
|
|
13
13
|
import {
|
|
@@ -27,11 +27,12 @@ import { steppedRotation } from '../components/tools/item/placement-math'
|
|
|
27
27
|
import { resolveDirectManipulationNode } from '../lib/direct-manipulation'
|
|
28
28
|
import { toggleDoorOpenState } from '../lib/door-interaction'
|
|
29
29
|
import { guideEmitter } from '../lib/guide-events'
|
|
30
|
-
import { runRedo, runUndo } from '../lib/history'
|
|
30
|
+
import { isHistoryShortcut, runRedo, runUndo, shouldCancelDraftOnHistoryJump } from '../lib/history'
|
|
31
31
|
import { isActive } from '../lib/interaction/scope'
|
|
32
32
|
import { copySelectedNodesToEditorClipboard } from '../lib/scene-clipboard'
|
|
33
33
|
import { sfxEmitter } from '../lib/sfx-bus'
|
|
34
34
|
import { activeSiteNode, clampBrushRadius } from '../lib/terrain-sculpt'
|
|
35
|
+
import { leaveUnitFocus } from '../lib/units'
|
|
35
36
|
import { toggleWindowOpenState } from '../lib/window-interaction'
|
|
36
37
|
import useDeleteConfirmation from '../store/use-delete-confirmation'
|
|
37
38
|
import useEditor, { getActiveContinuationContext, getActiveSnapContext } from '../store/use-editor'
|
|
@@ -102,6 +103,9 @@ function rotateGroupSelection(direction: 1 | -1): boolean {
|
|
|
102
103
|
let _toolCancelConsumed = false
|
|
103
104
|
export const markToolCancelConsumed = () => {
|
|
104
105
|
_toolCancelConsumed = true
|
|
106
|
+
// A consumed cancel means the active gesture reverted — the perf ledger must
|
|
107
|
+
// not measure the restore as a committed action's settle.
|
|
108
|
+
cancelPerfAction()
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
// Escape's fall-through when no tool consumed the cancel: drop back to the
|
|
@@ -117,10 +121,10 @@ const exitToSelectAfterUnconsumedCancel = () => {
|
|
|
117
121
|
// From zone mode, return to structure select
|
|
118
122
|
if (currentPhase === 'structure' && currentStructureLayer === 'zones') {
|
|
119
123
|
useEditor.getState().setStructureLayer('elements')
|
|
120
|
-
useEditor.getState().
|
|
124
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
121
125
|
} else {
|
|
122
126
|
// Return to the default select tool while keeping the active building/level context.
|
|
123
|
-
useEditor.getState().
|
|
127
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
useEditor.getState().setFloorplanSelectionTool('click')
|
|
@@ -141,6 +145,9 @@ const cancelInteractionForHistoryShortcut = () => {
|
|
|
141
145
|
guideEmitter.emit('guide:cancel-reference-scale')
|
|
142
146
|
return true
|
|
143
147
|
}
|
|
148
|
+
const activeScope = useInteractionScope.getState().scope
|
|
149
|
+
if (shouldCancelDraftOnHistoryJump()) return false
|
|
150
|
+
if (activeScope.kind === 'mesh-editing' && activeScope.phase === 'selecting') return false
|
|
144
151
|
_toolCancelConsumed = false
|
|
145
152
|
emitter.emit('tool:cancel')
|
|
146
153
|
if (_toolCancelConsumed) return true
|
|
@@ -162,6 +169,56 @@ const cancelInteractionForHistoryShortcut = () => {
|
|
|
162
169
|
return false
|
|
163
170
|
}
|
|
164
171
|
|
|
172
|
+
export const runHistoryShortcut = (direction: 'undo' | 'redo') => {
|
|
173
|
+
if (cancelInteractionForHistoryShortcut()) return false
|
|
174
|
+
if (direction === 'redo') runRedo()
|
|
175
|
+
else runUndo()
|
|
176
|
+
return true
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const isToolOwnedRotation = () => {
|
|
180
|
+
const editor = useEditor.getState()
|
|
181
|
+
const moving = getMovingNode()
|
|
182
|
+
if (
|
|
183
|
+
moving?.type === 'door' ||
|
|
184
|
+
moving?.type === 'window' ||
|
|
185
|
+
moving?.type === 'item' ||
|
|
186
|
+
moving?.type === 'lean-to-extension'
|
|
187
|
+
)
|
|
188
|
+
return true
|
|
189
|
+
return (
|
|
190
|
+
editor.mode === 'build' &&
|
|
191
|
+
(editor.tool === 'door' ||
|
|
192
|
+
editor.tool === 'window' ||
|
|
193
|
+
editor.tool === 'roof' ||
|
|
194
|
+
// The item tool is mounted for the build mode, but it only owns R/T
|
|
195
|
+
// when a catalog item is actually selected and a placement draft can
|
|
196
|
+
// exist. Without this check, selecting an existing item in the 2D plan
|
|
197
|
+
// while the item tool is armed silently drops the global rotate key.
|
|
198
|
+
(editor.tool === 'item' && editor.selectedItem !== null) ||
|
|
199
|
+
editor.tool === 'lean-to-extension')
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const isToolOwnedCanopyForm = () => {
|
|
204
|
+
const editor = useEditor.getState()
|
|
205
|
+
return editor.mode === 'build' && editor.tool === 'lean-to-extension'
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const canRunGlobalRotationShortcut = () =>
|
|
209
|
+
useInteractionScope.getState().scope.kind !== 'mesh-editing'
|
|
210
|
+
|
|
211
|
+
export const canCycleSnappingModeShortcut = (hasActiveContext = getActiveSnapContext() != null) =>
|
|
212
|
+
hasActiveContext
|
|
213
|
+
|
|
214
|
+
export function blocksSnappingShortcut(
|
|
215
|
+
target: Pick<HTMLElement, 'tagName' | 'isContentEditable' | 'hasAttribute'> | null,
|
|
216
|
+
): boolean {
|
|
217
|
+
if (!target) return false
|
|
218
|
+
if (target.tagName === 'INPUT' && target.hasAttribute('data-run-length-input')) return false
|
|
219
|
+
return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable
|
|
220
|
+
}
|
|
221
|
+
|
|
165
222
|
export const useKeyboard = ({
|
|
166
223
|
isVersionPreviewMode = false,
|
|
167
224
|
disabled = false,
|
|
@@ -174,32 +231,30 @@ export const useKeyboard = ({
|
|
|
174
231
|
return
|
|
175
232
|
}
|
|
176
233
|
|
|
177
|
-
// True while
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
// global selection-based R/T handler must stand down to avoid double-firing.
|
|
181
|
-
const isPlacingOpening = () => {
|
|
182
|
-
const ed = useEditor.getState()
|
|
183
|
-
const moving = getMovingNode()
|
|
184
|
-
if (moving?.type === 'door' || moving?.type === 'window') return true
|
|
185
|
-
return ed.mode === 'build' && (ed.tool === 'door' || ed.tool === 'window')
|
|
186
|
-
}
|
|
187
|
-
|
|
234
|
+
// True while an active placement tool owns R/T. Door/window tools flip the
|
|
235
|
+
// draft, item / lean-to placement rotates its draft, and the roof tool turns
|
|
236
|
+
// its draft axes. The global selection handler must stand down to avoid double-firing.
|
|
188
237
|
// Shift cycles the snapping mode (and a clean-tap Ctrl the grid step)
|
|
189
238
|
// whenever there's an active snapping context — i.e. exactly when the HUD
|
|
190
239
|
// shows a snapping chip. That single source covers wall/fence/item drafting,
|
|
191
240
|
// every node move (including wall-hosted items + door/window openings, which
|
|
192
241
|
// now declare `snapProfile`), and endpoint/polygon reshaping, so the keys
|
|
193
242
|
// never silently stop working. Force-place lives on Alt where a tool supports it.
|
|
194
|
-
const isSnappingCycleContext = () => getActiveSnapContext() != null
|
|
195
243
|
// A "clean tap" of Ctrl/Meta (pressed and released with NO other key in
|
|
196
244
|
// between) cycles the grid step — same context as the Shift snapping-mode
|
|
197
245
|
// cycle. `ctrlTapClean` starts true the moment Ctrl/Meta goes down alone
|
|
198
246
|
// and is cleared the instant any other key fires, so chords like Ctrl+Z /
|
|
199
247
|
// Ctrl+C never cycle.
|
|
200
248
|
let ctrlTapClean = false
|
|
249
|
+
let shiftTapClean = false
|
|
201
250
|
|
|
202
251
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
252
|
+
if (e.key === 'Shift') {
|
|
253
|
+
shiftTapClean = !e.repeat && !e.metaKey && !e.ctrlKey && !e.altKey
|
|
254
|
+
} else {
|
|
255
|
+
shiftTapClean = false
|
|
256
|
+
}
|
|
257
|
+
|
|
203
258
|
if (e.key === 'Control' || e.key === 'Meta') {
|
|
204
259
|
// Only a fresh, modifier-free press starts a clean-tap candidate;
|
|
205
260
|
// ignore key-repeat and presses already part of a combo.
|
|
@@ -210,6 +265,18 @@ export const useKeyboard = ({
|
|
|
210
265
|
ctrlTapClean = false
|
|
211
266
|
}
|
|
212
267
|
|
|
268
|
+
if (
|
|
269
|
+
shouldCancelDraftOnHistoryJump() &&
|
|
270
|
+
isHistoryShortcut(e) &&
|
|
271
|
+
e.target instanceof HTMLInputElement &&
|
|
272
|
+
e.target.hasAttribute('data-run-length-input')
|
|
273
|
+
) {
|
|
274
|
+
if (isVersionPreviewMode || useDeleteConfirmation.getState().request) return
|
|
275
|
+
e.preventDefault()
|
|
276
|
+
runHistoryShortcut(e.shiftKey ? 'redo' : 'undo')
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
213
280
|
// Don't handle shortcuts if user is typing in an input
|
|
214
281
|
if (
|
|
215
282
|
e.target instanceof HTMLInputElement ||
|
|
@@ -261,15 +328,6 @@ export const useKeyboard = ({
|
|
|
261
328
|
return
|
|
262
329
|
}
|
|
263
330
|
|
|
264
|
-
if (e.key === 'Shift' && !e.repeat && isSnappingCycleContext()) {
|
|
265
|
-
// Cycle the global snapping mode (grid → lines → angles → off).
|
|
266
|
-
// `'off'` is the snap bypass now, so Shift no longer holds-to-bypass.
|
|
267
|
-
e.preventDefault()
|
|
268
|
-
useEditor.getState().cycleSnappingMode()
|
|
269
|
-
sfxEmitter.emit('sfx:grid-snap')
|
|
270
|
-
return
|
|
271
|
-
}
|
|
272
|
-
|
|
273
331
|
if (
|
|
274
332
|
(e.key === 't' || e.key === 'T') &&
|
|
275
333
|
!e.repeat &&
|
|
@@ -333,37 +391,34 @@ export const useKeyboard = ({
|
|
|
333
391
|
// Only switch to select mode if no tool had an active mid-action to cancel.
|
|
334
392
|
// (e.g. mid-wall draw or mid-slab polygon should only cancel the action, not exit the tool)
|
|
335
393
|
if (!_toolCancelConsumed) {
|
|
336
|
-
|
|
394
|
+
if (leaveUnitFocus()) useEditor.getState().armToolMode({ mode: 'select' })
|
|
395
|
+
else exitToSelectAfterUnconsumedCancel()
|
|
337
396
|
}
|
|
338
397
|
} else if (e.key === '1' && !e.metaKey && !e.ctrlKey) {
|
|
339
398
|
e.preventDefault()
|
|
340
399
|
useEditor.getState().setPhase('site')
|
|
341
|
-
useEditor.getState().
|
|
400
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
342
401
|
} else if (e.key === '2' && !e.metaKey && !e.ctrlKey) {
|
|
343
402
|
e.preventDefault()
|
|
344
403
|
useEditor.getState().setPhase('structure')
|
|
345
|
-
useEditor.getState().
|
|
404
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
346
405
|
} else if (e.key === '3' && !e.metaKey && !e.ctrlKey) {
|
|
347
406
|
e.preventDefault()
|
|
348
407
|
useEditor.getState().setPhase('furnish')
|
|
349
|
-
useEditor.getState().
|
|
408
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
350
409
|
} else if (e.key === 'f' && !e.metaKey && !e.ctrlKey) {
|
|
351
410
|
if (isVersionPreviewMode) return
|
|
411
|
+
if (isToolOwnedCanopyForm()) return
|
|
352
412
|
e.preventDefault()
|
|
353
413
|
useEditor.getState().setPhase('furnish')
|
|
354
|
-
useEditor.getState().
|
|
355
|
-
// Set the item tool explicitly so the active tool never inherits a
|
|
356
|
-
// stale tool from a prior build session.
|
|
357
|
-
useEditor.getState().setTool('item')
|
|
414
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'item' })
|
|
358
415
|
useEditor.getState().setActiveSidebarPanel('items')
|
|
359
416
|
} else if (e.key === 'z' && !e.metaKey && !e.ctrlKey) {
|
|
360
417
|
if (isVersionPreviewMode) return
|
|
361
418
|
e.preventDefault()
|
|
362
419
|
useEditor.getState().setPhase('structure')
|
|
363
420
|
useEditor.getState().setStructureLayer('zones')
|
|
364
|
-
useEditor.getState().
|
|
365
|
-
// Set the zone tool explicitly so it never inherits a stale tool.
|
|
366
|
-
useEditor.getState().setTool('zone')
|
|
421
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'zone' })
|
|
367
422
|
} else if (e.key === 'm' && !e.metaKey && !e.ctrlKey) {
|
|
368
423
|
if (isVersionPreviewMode) return
|
|
369
424
|
e.preventDefault()
|
|
@@ -371,39 +426,33 @@ export const useKeyboard = ({
|
|
|
371
426
|
editor.setPhase('structure')
|
|
372
427
|
editor.setStructureLayer('elements')
|
|
373
428
|
editor.setToolDefaults('measurement', { kind: editor.lastMeasurementKind })
|
|
374
|
-
editor.
|
|
375
|
-
editor.setTool('measurement')
|
|
429
|
+
editor.armToolMode({ mode: 'build', tool: 'measurement' })
|
|
376
430
|
}
|
|
377
431
|
if (e.key === 'v' && !e.metaKey && !e.ctrlKey) {
|
|
378
432
|
e.preventDefault()
|
|
379
|
-
useEditor.getState().
|
|
433
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
380
434
|
useEditor.getState().setFloorplanSelectionTool('click')
|
|
381
435
|
} else if (e.key === 'b' && !e.metaKey && !e.ctrlKey) {
|
|
382
436
|
if (isVersionPreviewMode) return
|
|
383
437
|
e.preventDefault()
|
|
384
438
|
useEditor.getState().setPhase('structure')
|
|
385
439
|
useEditor.getState().setStructureLayer('elements')
|
|
386
|
-
useEditor.getState().
|
|
387
|
-
// Set the wall tool explicitly so B never inherits a stale tool
|
|
388
|
-
// (e.g. fence) left over from a prior build session.
|
|
389
|
-
useEditor.getState().setTool('wall')
|
|
440
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
|
|
390
441
|
} else if (e.key === 'x' && !e.metaKey && !e.ctrlKey) {
|
|
391
442
|
if (isVersionPreviewMode) return
|
|
392
443
|
e.preventDefault()
|
|
393
|
-
useEditor.getState().
|
|
444
|
+
useEditor.getState().armToolMode({ mode: 'delete' })
|
|
394
445
|
} else if (e.key === 'p' && !e.metaKey && !e.ctrlKey) {
|
|
395
446
|
if (isVersionPreviewMode) return
|
|
396
447
|
e.preventDefault()
|
|
397
|
-
useEditor.getState().primeMaterialPaintFromSelection()
|
|
398
448
|
useEditor.getState().setPhase('structure')
|
|
399
449
|
useEditor.getState().setStructureLayer('elements')
|
|
400
|
-
useEditor.getState().
|
|
450
|
+
useEditor.getState().armMaterialPaint()
|
|
401
451
|
} else if (e.key === 'g' && !e.metaKey && !e.ctrlKey) {
|
|
402
452
|
if (isVersionPreviewMode) return
|
|
403
453
|
e.preventDefault()
|
|
404
|
-
// G for ground.
|
|
405
|
-
|
|
406
|
-
useEditor.getState().setMode('terrain-sculpt')
|
|
454
|
+
// G for ground. The ToolMode transition moves to the site phase itself.
|
|
455
|
+
useEditor.getState().armToolMode({ mode: 'terrain-sculpt' })
|
|
407
456
|
} else if (e.key === 'c' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
|
408
457
|
if (isVersionPreviewMode) return
|
|
409
458
|
e.preventDefault()
|
|
@@ -419,13 +468,11 @@ export const useKeyboard = ({
|
|
|
419
468
|
} else if (e.key.toLowerCase() === 'z' && e.shiftKey && (e.metaKey || e.ctrlKey)) {
|
|
420
469
|
if (isVersionPreviewMode) return
|
|
421
470
|
e.preventDefault()
|
|
422
|
-
|
|
423
|
-
runRedo()
|
|
471
|
+
runHistoryShortcut('redo')
|
|
424
472
|
} else if (e.key.toLowerCase() === 'z' && !e.shiftKey && (e.metaKey || e.ctrlKey)) {
|
|
425
473
|
if (isVersionPreviewMode) return
|
|
426
474
|
e.preventDefault()
|
|
427
|
-
|
|
428
|
-
runUndo()
|
|
475
|
+
runHistoryShortcut('undo')
|
|
429
476
|
} else if (e.key === 'ArrowUp' && (e.metaKey || e.ctrlKey)) {
|
|
430
477
|
e.preventDefault()
|
|
431
478
|
const { buildingId, levelId } = useViewer.getState().selection
|
|
@@ -441,8 +488,10 @@ export const useKeyboard = ({
|
|
|
441
488
|
const currentIdx = levelId ? levels.indexOf(levelId as any) : -1
|
|
442
489
|
const nextIdx = currentIdx < levels.length - 1 ? currentIdx + 1 : currentIdx
|
|
443
490
|
if (nextIdx !== -1 && nextIdx !== currentIdx) {
|
|
491
|
+
markPerfAction('level-switch', levels[nextIdx] as string)
|
|
444
492
|
useViewer.getState().setSelection({ levelId: levels[nextIdx] as any })
|
|
445
493
|
} else if (currentIdx === -1) {
|
|
494
|
+
markPerfAction('level-switch', levels[0] as string)
|
|
446
495
|
useViewer.getState().setSelection({ levelId: levels[0] as any })
|
|
447
496
|
}
|
|
448
497
|
}
|
|
@@ -462,8 +511,10 @@ export const useKeyboard = ({
|
|
|
462
511
|
const currentIdx = levelId ? levels.indexOf(levelId as any) : -1
|
|
463
512
|
const prevIdx = currentIdx > 0 ? currentIdx - 1 : currentIdx
|
|
464
513
|
if (prevIdx !== -1 && prevIdx !== currentIdx) {
|
|
514
|
+
markPerfAction('level-switch', levels[prevIdx] as string)
|
|
465
515
|
useViewer.getState().setSelection({ levelId: levels[prevIdx] as any })
|
|
466
516
|
} else if (currentIdx === -1) {
|
|
517
|
+
markPerfAction('level-switch', levels[levels.length - 1] as string)
|
|
467
518
|
useViewer.getState().setSelection({ levelId: levels[levels.length - 1] as any })
|
|
468
519
|
}
|
|
469
520
|
}
|
|
@@ -473,7 +524,8 @@ export const useKeyboard = ({
|
|
|
473
524
|
!e.metaKey &&
|
|
474
525
|
!e.ctrlKey &&
|
|
475
526
|
!isVersionPreviewMode &&
|
|
476
|
-
!
|
|
527
|
+
!isToolOwnedRotation() &&
|
|
528
|
+
canRunGlobalRotationShortcut()
|
|
477
529
|
) {
|
|
478
530
|
// `!metaKey && !ctrlKey` lets Cmd/Ctrl+R reach the browser reload instead
|
|
479
531
|
// of rotating/flipping the selected node.
|
|
@@ -482,10 +534,9 @@ export const useKeyboard = ({
|
|
|
482
534
|
// open/close toggle lives on E. Windows still use R to toggle
|
|
483
535
|
// their open/closed state.
|
|
484
536
|
//
|
|
485
|
-
// Skipped
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
// time — without this guard both would fire (double flip + sfx).
|
|
537
|
+
// Skipped while an item, door, window, or roof placement owns rotation.
|
|
538
|
+
// The user can still have a node selected during placement; without this
|
|
539
|
+
// guard both the draft and the selection would rotate.
|
|
489
540
|
//
|
|
490
541
|
// References (guide/scan) live in `selectedReferenceId`, not the viewer
|
|
491
542
|
// selection — check them first, like the Delete arm below.
|
|
@@ -565,7 +616,12 @@ export const useKeyboard = ({
|
|
|
565
616
|
sfxEmitter.emit('sfx:item-rotate')
|
|
566
617
|
}
|
|
567
618
|
}
|
|
568
|
-
} else if (
|
|
619
|
+
} else if (
|
|
620
|
+
(e.key === 't' || e.key === 'T') &&
|
|
621
|
+
!isVersionPreviewMode &&
|
|
622
|
+
!isToolOwnedRotation() &&
|
|
623
|
+
canRunGlobalRotationShortcut()
|
|
624
|
+
) {
|
|
569
625
|
// Rotate selected node counter-clockwise
|
|
570
626
|
// Multi-selection → group rotate, mirroring the R arm above.
|
|
571
627
|
if (rotateGroupSelection(-1)) {
|
|
@@ -683,16 +739,27 @@ export const useKeyboard = ({
|
|
|
683
739
|
}
|
|
684
740
|
}
|
|
685
741
|
const handleKeyUp = (e: KeyboardEvent) => {
|
|
742
|
+
if (e.key === 'Shift') {
|
|
743
|
+
const wasClean = shiftTapClean
|
|
744
|
+
shiftTapClean = false
|
|
745
|
+
if (!wasClean) return
|
|
746
|
+
if (blocksSnappingShortcut(e.target instanceof HTMLElement ? e.target : null)) {
|
|
747
|
+
return
|
|
748
|
+
}
|
|
749
|
+
if (!canCycleSnappingModeShortcut()) return
|
|
750
|
+
e.preventDefault()
|
|
751
|
+
useEditor.getState().cycleSnappingMode()
|
|
752
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
753
|
+
return
|
|
754
|
+
}
|
|
686
755
|
if (e.key === 'Control' || e.key === 'Meta') {
|
|
687
756
|
const wasClean = ctrlTapClean
|
|
688
757
|
ctrlTapClean = false
|
|
689
758
|
if (!wasClean) return
|
|
690
|
-
|
|
691
|
-
// in an input.
|
|
692
|
-
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
759
|
+
if (blocksSnappingShortcut(e.target instanceof HTMLElement ? e.target : null)) {
|
|
693
760
|
return
|
|
694
761
|
}
|
|
695
|
-
if (!
|
|
762
|
+
if (!canCycleSnappingModeShortcut()) return
|
|
696
763
|
// Cycle the grid / measurement step (0.5 → 0.25 → 0.1 → 0.05).
|
|
697
764
|
useEditor.getState().cycleGridSnapStep()
|
|
698
765
|
sfxEmitter.emit('sfx:grid-snap')
|
|
@@ -706,6 +773,8 @@ export const useKeyboard = ({
|
|
|
706
773
|
// registry move overlay) — safe only because none of them claim Ctrl/Cmd+G.
|
|
707
774
|
// `e.code` keeps it on the physical G key across keyboard layouts.
|
|
708
775
|
const handleSessionGroupKeyDown = (e: KeyboardEvent) => {
|
|
776
|
+
if (e.key !== 'Shift') shiftTapClean = false
|
|
777
|
+
if (e.key !== 'Control' && e.key !== 'Meta') ctrlTapClean = false
|
|
709
778
|
if (
|
|
710
779
|
e.target instanceof HTMLInputElement ||
|
|
711
780
|
e.target instanceof HTMLTextAreaElement ||
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { isNodeKindEnabled, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useEffect } from 'react'
|
|
5
|
+
import useEditor, { type Tool } from '../store/use-editor'
|
|
6
|
+
|
|
7
|
+
export function useRegisteredToolEnabled(tool: Tool | null): boolean {
|
|
8
|
+
const installedPlugins = useScene((state) => state.installedPlugins)
|
|
9
|
+
const enabled = tool === null || isNodeKindEnabled(tool, installedPlugins)
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (enabled || tool === null) return
|
|
13
|
+
|
|
14
|
+
// The render gate has already unmounted the tool, so its own cleanup cancels
|
|
15
|
+
// any draft before clearing the stale selection prevents reinstall remounts.
|
|
16
|
+
if (useEditor.getState().tool === tool) useEditor.getState().setTool(null)
|
|
17
|
+
}, [enabled, tool])
|
|
18
|
+
|
|
19
|
+
return enabled
|
|
20
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Claims Cmd/Ctrl+S for the app's save.
|
|
7
|
+
*
|
|
8
|
+
* Capture phase and ungated on purpose: the browser's "Save page" dialog must
|
|
9
|
+
* never appear anywhere in the editor — including first-person, studio mode and
|
|
10
|
+
* while focus sits in an input, where people still expect the chord to save.
|
|
11
|
+
* `e.code` keeps it on the physical S key across keyboard layouts.
|
|
12
|
+
*/
|
|
13
|
+
export function useSaveShortcut(onSave: () => void) {
|
|
14
|
+
const onSaveRef = useRef(onSave)
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
onSaveRef.current = onSave
|
|
18
|
+
}, [onSave])
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
22
|
+
if (!(e.metaKey || e.ctrlKey) || e.altKey || e.shiftKey) return
|
|
23
|
+
if (e.code !== 'KeyS') return
|
|
24
|
+
|
|
25
|
+
e.preventDefault()
|
|
26
|
+
e.stopPropagation()
|
|
27
|
+
onSaveRef.current()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
window.addEventListener('keydown', handleKeyDown, true)
|
|
31
|
+
return () => window.removeEventListener('keydown', handleKeyDown, true)
|
|
32
|
+
}, [])
|
|
33
|
+
}
|