@pascal-app/editor 0.8.0 → 0.9.2
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 +15 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +795 -28
- package/src/components/editor/editor-layout-mobile.tsx +8 -3
- package/src/components/editor/editor-layout-v2.tsx +76 -51
- package/src/components/editor/export-manager.tsx +69 -32
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
- package/src/components/editor/first-person/build-collider-world.ts +228 -83
- package/src/components/editor/first-person-controls.tsx +943 -27
- package/src/components/editor/floating-action-menu.tsx +676 -208
- package/src/components/editor/floating-building-action-menu.tsx +2 -2
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +6916 -12137
- package/src/components/editor/grid.tsx +197 -36
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +420 -0
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +396 -0
- package/src/components/editor/group-transform-shared.ts +489 -0
- package/src/components/editor/handles/handle-arrow.tsx +581 -0
- package/src/components/editor/handles/use-handle-drag.ts +246 -0
- package/src/components/editor/index.tsx +412 -162
- package/src/components/editor/measurement-pill.tsx +115 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +1646 -0
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +1106 -483
- package/src/components/editor/site-edge-labels.tsx +45 -21
- package/src/components/editor/slab-hole-highlights.tsx +474 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +140 -41
- package/src/components/editor/use-floorplan-background-placement.ts +190 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +19 -7
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +14 -19
- package/src/components/editor/wall-move-side-handles.tsx +870 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
- package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +375 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
- package/src/components/tools/fence/fence-drafting.ts +141 -27
- package/src/components/tools/item/move-tool.tsx +45 -105
- package/src/components/tools/item/placement-math.test.ts +22 -0
- package/src/components/tools/item/placement-math.ts +59 -7
- package/src/components/tools/item/placement-strategies.ts +432 -16
- package/src/components/tools/item/placement-types.ts +22 -1
- package/src/components/tools/item/use-draft-node.ts +51 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
- package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
- package/src/components/tools/roof/roof-tool.tsx +350 -28
- package/src/components/tools/select/box-select-state.ts +88 -0
- package/src/components/tools/select/box-select-tool.tsx +442 -520
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +305 -0
- package/src/components/tools/shared/polygon-editor.tsx +866 -175
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +520 -16
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +2 -1
- package/src/components/tools/stair/stair-tool.tsx +342 -37
- package/src/components/tools/tool-manager.tsx +239 -62
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +248 -171
- package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +86 -82
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -170
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +3 -117
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +27 -93
- package/src/components/ui/action-menu/view-toggles.tsx +279 -90
- package/src/components/ui/command-palette/editor-commands.tsx +20 -15
- package/src/components/ui/command-palette/index.tsx +5 -3
- package/src/components/ui/controls/material-paint-panel.tsx +152 -0
- package/src/components/ui/controls/material-picker.tsx +87 -177
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +142 -46
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +235 -23
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +17 -16
- package/src/components/ui/panels/panel-manager.tsx +55 -58
- package/src/components/ui/panels/panel-wrapper.tsx +238 -12
- package/src/components/ui/panels/parametric-inspector.tsx +482 -0
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +75 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +253 -61
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +64 -10
- package/src/hooks/use-ceiling-events.ts +176 -0
- package/src/hooks/use-drag-action.ts +127 -0
- package/src/hooks/use-keyboard.ts +493 -95
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +451 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/constants.ts +6 -2
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +50 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +117 -0
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +10 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +339 -0
- package/src/lib/fresh-planar-placement.ts +130 -0
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +11 -1
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +227 -48
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +100 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +7 -7
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +332 -0
- package/src/lib/scene.ts +40 -8
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +69 -10
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +148 -55
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +231 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +767 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +39 -0
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +34 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { Icon } from '@iconify/react'
|
|
2
|
+
import type { ToolHint } from '@pascal-app/core'
|
|
3
|
+
import { Fragment, useSyncExternalStore } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
CONTINUATION_PROFILES,
|
|
6
|
+
type ContinuationContext,
|
|
7
|
+
} from '../../../lib/continuation'
|
|
8
|
+
import type { ContextualShortcutHint } from '../../../lib/contextual-help'
|
|
9
|
+
import { hasActivePaintMaterial } from '../../../lib/material-paint'
|
|
10
|
+
import { paintScopeLabel, type PaintScope } from '../../../lib/paint-scope'
|
|
11
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
12
|
+
import {
|
|
13
|
+
cycleSnappingModeIn,
|
|
14
|
+
resolveSnapFlags,
|
|
15
|
+
type SnapContext,
|
|
16
|
+
} from '../../../lib/snapping-mode'
|
|
17
|
+
import { cn } from '../../../lib/utils'
|
|
18
|
+
import useEditor, { type GridSnapStep } from '../../../store/use-editor'
|
|
19
|
+
import useFenceCurveDraft from '../../../store/use-fence-curve-draft'
|
|
20
|
+
import { ShortcutToken } from '../primitives/shortcut-token'
|
|
21
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
|
|
22
|
+
|
|
23
|
+
// One muted container holds every row — passive key hints and interactive chips
|
|
24
|
+
// alike — so the HUD reads as a single panel, not a stack of floating pills. The
|
|
25
|
+
// background is near-opaque (`bg-background/95`) with a single backdrop blur so
|
|
26
|
+
// active rows stay readable over the 3D scene even while a modifier is held.
|
|
27
|
+
// A 2-track grid: column 1 sizes to `max-content` (the widest key across ALL
|
|
28
|
+
// rows), column 2 (`1fr`) is the label. Every row is a subgrid sharing those
|
|
29
|
+
// tracks, so labels align even when keys differ in width (⌘ vs Shift) or wrap to
|
|
30
|
+
// two lines. Near-opaque bg + single backdrop blur keeps active rows readable.
|
|
31
|
+
const CONTAINER_CLASS =
|
|
32
|
+
'pointer-events-none fixed top-1/2 right-4 z-40 grid max-w-[260px] -translate-y-1/2 grid-cols-[max-content_1fr] gap-x-2.5 gap-y-1.5 rounded-lg border border-border bg-background/95 px-3 py-2.5 shadow-lg backdrop-blur-md'
|
|
33
|
+
|
|
34
|
+
const TOKEN_CLASS = 'h-5 px-1.5 text-[10px]'
|
|
35
|
+
|
|
36
|
+
// Each row spans both columns as its own subgrid, inheriting the container's
|
|
37
|
+
// tracks so its key/label cells land on the shared column lines.
|
|
38
|
+
const ROW_CLASS = 'col-span-2 grid grid-cols-subgrid'
|
|
39
|
+
|
|
40
|
+
// The key cell (column 1). `items-center` centres the token; the row's
|
|
41
|
+
// `items-start` keeps it on the label's first line when the label wraps.
|
|
42
|
+
const KEY_CELL_CLASS = 'flex items-center gap-1'
|
|
43
|
+
|
|
44
|
+
// Keys pressed together join with "+"; an entry that is itself an array is a
|
|
45
|
+
// group of alternatives and joins with "/" — so [['Cmd/Ctrl', 'Shift'],
|
|
46
|
+
// 'Left click'] reads "⌘ / ⇧ + click".
|
|
47
|
+
function ShortcutSequence({
|
|
48
|
+
active = false,
|
|
49
|
+
keys,
|
|
50
|
+
}: {
|
|
51
|
+
active?: boolean
|
|
52
|
+
keys: Array<string | string[]>
|
|
53
|
+
}) {
|
|
54
|
+
return (
|
|
55
|
+
<div className={KEY_CELL_CLASS}>
|
|
56
|
+
{keys.map((entry, index) => (
|
|
57
|
+
<Fragment key={`${String(entry)}-${index}`}>
|
|
58
|
+
{index > 0 ? (
|
|
59
|
+
<span className="font-medium text-[12px] text-muted-foreground/80 leading-none">
|
|
60
|
+
+
|
|
61
|
+
</span>
|
|
62
|
+
) : null}
|
|
63
|
+
{Array.isArray(entry) ? (
|
|
64
|
+
entry.map((alternative, altIndex) => (
|
|
65
|
+
<Fragment key={`${alternative}-${altIndex}`}>
|
|
66
|
+
{altIndex > 0 ? (
|
|
67
|
+
<span className="text-[9px] text-muted-foreground/70">/</span>
|
|
68
|
+
) : null}
|
|
69
|
+
<ShortcutToken
|
|
70
|
+
className={cn(TOKEN_CLASS, active && 'border-white bg-white text-black shadow-sm')}
|
|
71
|
+
value={alternative}
|
|
72
|
+
/>
|
|
73
|
+
</Fragment>
|
|
74
|
+
))
|
|
75
|
+
) : (
|
|
76
|
+
<ShortcutToken
|
|
77
|
+
className={cn(TOKEN_CLASS, active && 'border-white bg-white text-black shadow-sm')}
|
|
78
|
+
value={entry}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
</Fragment>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Shared single-line chip row (key cell + icon/label cell). Rendered either as a
|
|
88
|
+
// passive row (no `onClick`) or a clickable button. The outer container is
|
|
89
|
+
// `pointer-events-none`, so clickable chips opt back in.
|
|
90
|
+
function ChipRow({
|
|
91
|
+
ariaLabel,
|
|
92
|
+
disabled = false,
|
|
93
|
+
icon,
|
|
94
|
+
label,
|
|
95
|
+
onClick,
|
|
96
|
+
shortcut,
|
|
97
|
+
tooltip,
|
|
98
|
+
}: {
|
|
99
|
+
ariaLabel?: string
|
|
100
|
+
disabled?: boolean
|
|
101
|
+
icon?: string
|
|
102
|
+
label: string
|
|
103
|
+
onClick?: () => void
|
|
104
|
+
shortcut?: string
|
|
105
|
+
tooltip?: string
|
|
106
|
+
}) {
|
|
107
|
+
const body = (
|
|
108
|
+
<>
|
|
109
|
+
<span className={KEY_CELL_CLASS}>
|
|
110
|
+
{shortcut ? <ShortcutToken className={TOKEN_CLASS} value={shortcut} /> : null}
|
|
111
|
+
</span>
|
|
112
|
+
<span className="flex min-w-0 items-center gap-1.5 text-muted-foreground text-xs">
|
|
113
|
+
{icon ? <Icon className="shrink-0" height={13} icon={icon} width={13} /> : null}
|
|
114
|
+
<span className="truncate">{label}</span>
|
|
115
|
+
</span>
|
|
116
|
+
</>
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if (!onClick) {
|
|
120
|
+
return (
|
|
121
|
+
<div className={cn(ROW_CLASS, 'items-center', disabled && 'opacity-45 saturate-0')}>{body}</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const button = (
|
|
126
|
+
<button
|
|
127
|
+
aria-label={ariaLabel ?? label}
|
|
128
|
+
className={cn(
|
|
129
|
+
ROW_CLASS,
|
|
130
|
+
'pointer-events-auto cursor-pointer items-center rounded-md text-left transition-colors hover:bg-muted/60',
|
|
131
|
+
disabled && 'opacity-45 saturate-0',
|
|
132
|
+
)}
|
|
133
|
+
onClick={onClick}
|
|
134
|
+
type="button"
|
|
135
|
+
>
|
|
136
|
+
{body}
|
|
137
|
+
</button>
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if (!tooltip) return button
|
|
141
|
+
return (
|
|
142
|
+
<Tooltip>
|
|
143
|
+
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
144
|
+
<TooltipContent side="left">{tooltip}</TooltipContent>
|
|
145
|
+
</Tooltip>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const SNAPPING_MODE_ICONS = {
|
|
150
|
+
grid: 'lucide:grid-2x2',
|
|
151
|
+
lines: 'lucide:magnet',
|
|
152
|
+
angles: 'lucide:triangle',
|
|
153
|
+
off: 'lucide:ban',
|
|
154
|
+
} as const
|
|
155
|
+
|
|
156
|
+
const SNAPPING_MODE_LABELS = {
|
|
157
|
+
grid: 'Grid',
|
|
158
|
+
lines: 'Lines',
|
|
159
|
+
angles: 'Angles',
|
|
160
|
+
off: 'Off',
|
|
161
|
+
} as const
|
|
162
|
+
|
|
163
|
+
const GRID_SNAP_STEPS: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
|
|
164
|
+
|
|
165
|
+
function nextGridSnapStep(step: GridSnapStep): GridSnapStep {
|
|
166
|
+
const index = GRID_SNAP_STEPS.indexOf(step)
|
|
167
|
+
return GRID_SNAP_STEPS[(index + 1) % GRID_SNAP_STEPS.length] ?? GRID_SNAP_STEPS[0]!
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The active interaction's snapping controls, scoped to its context (wall / item
|
|
171
|
+
// / polygon) so each action shows only the modes that make sense for it.
|
|
172
|
+
function SnappingChips({ context }: { context: SnapContext }) {
|
|
173
|
+
const snappingMode = useEditor((s) => s.snappingModeByContext[context])
|
|
174
|
+
const setSnappingMode = useEditor((s) => s.setSnappingMode)
|
|
175
|
+
const gridSnapStep = useEditor((s) => s.gridSnapStep)
|
|
176
|
+
const setGridSnapStep = useEditor((s) => s.setGridSnapStep)
|
|
177
|
+
|
|
178
|
+
const gridActive = resolveSnapFlags(snappingMode).grid
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<>
|
|
182
|
+
<ChipRow
|
|
183
|
+
ariaLabel={`Snapping: ${SNAPPING_MODE_LABELS[snappingMode]}`}
|
|
184
|
+
icon={SNAPPING_MODE_ICONS[snappingMode]}
|
|
185
|
+
label={`Snapping: ${SNAPPING_MODE_LABELS[snappingMode]}`}
|
|
186
|
+
onClick={() => {
|
|
187
|
+
setSnappingMode(context, cycleSnappingModeIn(context, snappingMode))
|
|
188
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
189
|
+
}}
|
|
190
|
+
shortcut="Shift"
|
|
191
|
+
tooltip="Snapping mode — click or press Shift to cycle"
|
|
192
|
+
/>
|
|
193
|
+
{gridActive ? (
|
|
194
|
+
<ChipRow
|
|
195
|
+
ariaLabel={`Grid step: ${gridSnapStep.toFixed(2)} m`}
|
|
196
|
+
label={`Grid: ${gridSnapStep.toFixed(2)} m`}
|
|
197
|
+
onClick={() => {
|
|
198
|
+
setGridSnapStep(nextGridSnapStep(gridSnapStep))
|
|
199
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
200
|
+
}}
|
|
201
|
+
shortcut="Ctrl"
|
|
202
|
+
tooltip="Grid step — click or tap Ctrl to cycle"
|
|
203
|
+
/>
|
|
204
|
+
) : null}
|
|
205
|
+
</>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// A kind-owned live mode chip declared on a `ToolHint` (`hint.chip`) — the
|
|
210
|
+
// registry counterpart of the snapping / continuation chips above: shows the
|
|
211
|
+
// current value's label, and clicking the row (or the hint's key, handled by
|
|
212
|
+
// the tool itself) cycles it.
|
|
213
|
+
function ToolHintChipRow({ hint }: { hint: ToolHint & { chip: NonNullable<ToolHint['chip']> } }) {
|
|
214
|
+
const { chip } = hint
|
|
215
|
+
const value = useSyncExternalStore(chip.subscribe, chip.value, chip.value)
|
|
216
|
+
const label = chip.labels[value] ?? hint.label
|
|
217
|
+
return (
|
|
218
|
+
<ChipRow
|
|
219
|
+
ariaLabel={label}
|
|
220
|
+
icon={chip.icons?.[value]}
|
|
221
|
+
label={label}
|
|
222
|
+
onClick={chip.cycle}
|
|
223
|
+
shortcut={hint.key}
|
|
224
|
+
tooltip={chip.tooltip}
|
|
225
|
+
/>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function ContinuationChip({ context }: { context: ContinuationContext }) {
|
|
230
|
+
const mode = useEditor((s) => s.getContinuation(context))
|
|
231
|
+
const cycleContinuation = useEditor((s) => s.cycleContinuation)
|
|
232
|
+
const profile = CONTINUATION_PROFILES[context]
|
|
233
|
+
const label = profile.labels[mode] ?? mode
|
|
234
|
+
const icon = profile.icons[mode] ?? 'lucide:repeat'
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<ChipRow
|
|
238
|
+
ariaLabel={`Continuation: ${label}`}
|
|
239
|
+
icon={icon}
|
|
240
|
+
label={label}
|
|
241
|
+
onClick={() => cycleContinuation(context)}
|
|
242
|
+
shortcut="C"
|
|
243
|
+
tooltip="Continuation — click or press C to cycle"
|
|
244
|
+
/>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function FenceContinuationChips() {
|
|
249
|
+
const mode = useEditor((s) => s.getContinuation('fence'))
|
|
250
|
+
const setContinuation = useEditor((s) => s.setContinuation)
|
|
251
|
+
const curveStarted = useFenceCurveDraft((s) => s.pointCount > 0)
|
|
252
|
+
|
|
253
|
+
const isCurved = mode === 'curved'
|
|
254
|
+
const straightMode = isCurved ? 'continuous' : mode
|
|
255
|
+
const straightLabel = straightMode === 'single' ? 'Straight: Single' : 'Straight: Continuous'
|
|
256
|
+
const straightIcon = straightMode === 'single' ? 'lucide:minus' : 'lucide:waypoints'
|
|
257
|
+
const typeLabel = isCurved ? 'Type: Curved' : 'Type: Straight'
|
|
258
|
+
const typeIcon = isCurved ? 'lucide:spline' : 'lucide:minus'
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<>
|
|
262
|
+
<ChipRow
|
|
263
|
+
ariaLabel={`Fence type: ${isCurved ? 'Curved' : 'Straight'}`}
|
|
264
|
+
icon={typeIcon}
|
|
265
|
+
label={typeLabel}
|
|
266
|
+
onClick={() => setContinuation('fence', isCurved ? 'continuous' : 'curved')}
|
|
267
|
+
shortcut="T"
|
|
268
|
+
tooltip="Fence type — click or press T to switch between straight and curved"
|
|
269
|
+
/>
|
|
270
|
+
<ChipRow
|
|
271
|
+
ariaLabel={`Fence continuation: ${straightLabel}`}
|
|
272
|
+
disabled={isCurved}
|
|
273
|
+
icon={straightIcon}
|
|
274
|
+
label={straightLabel}
|
|
275
|
+
onClick={
|
|
276
|
+
isCurved
|
|
277
|
+
? undefined
|
|
278
|
+
: () => setContinuation('fence', straightMode === 'single' ? 'continuous' : 'single')
|
|
279
|
+
}
|
|
280
|
+
shortcut="C"
|
|
281
|
+
tooltip={
|
|
282
|
+
isCurved
|
|
283
|
+
? 'Straight continuation is unavailable while curved fence type is active'
|
|
284
|
+
: 'Straight fence continuation — click or press C to toggle'
|
|
285
|
+
}
|
|
286
|
+
/>
|
|
287
|
+
{/* Curved fences are committed by a closing gesture rather than per-click,
|
|
288
|
+
so the finish keys aren't discoverable on their own — surface them, but
|
|
289
|
+
only once the user has placed a point and a curve is actually in flight. */}
|
|
290
|
+
{isCurved && curveStarted ? (
|
|
291
|
+
<ChipRow
|
|
292
|
+
icon="lucide:circle-check"
|
|
293
|
+
label="Finish curve (or double-click)"
|
|
294
|
+
shortcut="Enter"
|
|
295
|
+
/>
|
|
296
|
+
) : null}
|
|
297
|
+
</>
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const PAINT_SCOPE_ICONS: Record<PaintScope, string> = {
|
|
302
|
+
single: 'lucide:square',
|
|
303
|
+
object: 'lucide:box',
|
|
304
|
+
matching: 'lucide:copy',
|
|
305
|
+
room: 'lucide:scan',
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// The painter's application-scope chip. Driven entirely by the hovered node's
|
|
309
|
+
// derived `paintHover` (scopes + labels), so it works for any kind without a
|
|
310
|
+
// per-target table.
|
|
311
|
+
function PaintScopeChip() {
|
|
312
|
+
// What the cursor is over (that's what the next click paints). `null` when not
|
|
313
|
+
// over a paintable surface — including an item with no slots.
|
|
314
|
+
const paintHover = useEditor((s) => s.paintHover)
|
|
315
|
+
const paintScope = useEditor((s) => s.paintScope)
|
|
316
|
+
const cyclePaintScope = useEditor((s) => s.cyclePaintScope)
|
|
317
|
+
const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
|
|
318
|
+
const paintEraser = useEditor((s) => s.paintEraser)
|
|
319
|
+
|
|
320
|
+
// Nothing to paint with yet (no material picked, not erasing) → the first step
|
|
321
|
+
// is choosing a material, so say that before anything about scope or hovering.
|
|
322
|
+
if (!(paintEraser || hasActivePaintMaterial(activePaintMaterial))) {
|
|
323
|
+
return <ChipRow icon="lucide:palette" label="Select a material to paint" />
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Not over anything paintable → guide the user to hover, still teaching Shift.
|
|
327
|
+
if (!paintHover) {
|
|
328
|
+
return (
|
|
329
|
+
<ChipRow icon="lucide:mouse-pointer-click" label="Hover a surface to paint" shortcut="Shift" />
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const { scopes } = paintHover
|
|
334
|
+
// A scope carried over from another node (the mode is global) falls back to
|
|
335
|
+
// the narrowest for both display and — via the apply-time resolver — behaviour.
|
|
336
|
+
const effective: PaintScope = scopes.includes(paintScope) ? paintScope : 'single'
|
|
337
|
+
|
|
338
|
+
// Paintable but with no scope choice (roof, a one-slot node, …) → a passive
|
|
339
|
+
// row that still names the surface, so the user always sees what they'll paint.
|
|
340
|
+
if (scopes.length <= 1) {
|
|
341
|
+
return (
|
|
342
|
+
<ChipRow
|
|
343
|
+
icon={PAINT_SCOPE_ICONS[effective]}
|
|
344
|
+
label={`Paint: ${paintScopeLabel(effective, paintHover)}`}
|
|
345
|
+
/>
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return (
|
|
350
|
+
<ChipRow
|
|
351
|
+
ariaLabel={`Paint scope: ${paintScopeLabel(effective, paintHover)}`}
|
|
352
|
+
icon={PAINT_SCOPE_ICONS[effective]}
|
|
353
|
+
label={`Paint: ${paintScopeLabel(effective, paintHover)}`}
|
|
354
|
+
onClick={() => cyclePaintScope()}
|
|
355
|
+
shortcut="Shift"
|
|
356
|
+
tooltip="Paint scope — click or press Shift to cycle"
|
|
357
|
+
/>
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function ContextualHelperPanel({
|
|
362
|
+
hints,
|
|
363
|
+
chipHints = [],
|
|
364
|
+
snapContext = null,
|
|
365
|
+
showPaintScope = false,
|
|
366
|
+
continuationContext = null,
|
|
367
|
+
}: {
|
|
368
|
+
hints: ContextualShortcutHint[]
|
|
369
|
+
// Kind-owned live mode chips (`ToolHint.chip`), rendered alongside the
|
|
370
|
+
// snapping / continuation chips.
|
|
371
|
+
chipHints?: ToolHint[]
|
|
372
|
+
// The active snapping context drives the snapping chips (which mode set). Null
|
|
373
|
+
// → no snapping chips for this interaction.
|
|
374
|
+
snapContext?: SnapContext | null
|
|
375
|
+
showPaintScope?: boolean
|
|
376
|
+
continuationContext?: ContinuationContext | null
|
|
377
|
+
}) {
|
|
378
|
+
if (
|
|
379
|
+
hints.length === 0 &&
|
|
380
|
+
chipHints.length === 0 &&
|
|
381
|
+
!snapContext &&
|
|
382
|
+
!showPaintScope &&
|
|
383
|
+
!continuationContext
|
|
384
|
+
)
|
|
385
|
+
return null
|
|
386
|
+
|
|
387
|
+
return (
|
|
388
|
+
<div className={CONTAINER_CLASS}>
|
|
389
|
+
{snapContext ? <SnappingChips context={snapContext} /> : null}
|
|
390
|
+
{continuationContext === 'fence' ? <FenceContinuationChips /> : null}
|
|
391
|
+
{continuationContext && continuationContext !== 'fence' ? (
|
|
392
|
+
<ContinuationChip context={continuationContext} />
|
|
393
|
+
) : null}
|
|
394
|
+
{chipHints.map((hint) =>
|
|
395
|
+
hint.chip ? (
|
|
396
|
+
<ToolHintChipRow
|
|
397
|
+
hint={hint as ToolHint & { chip: NonNullable<ToolHint['chip']> }}
|
|
398
|
+
key={`${hint.key}:${hint.label}`}
|
|
399
|
+
/>
|
|
400
|
+
) : null,
|
|
401
|
+
)}
|
|
402
|
+
{showPaintScope ? <PaintScopeChip /> : null}
|
|
403
|
+
{hints.map((hint) => (
|
|
404
|
+
<div
|
|
405
|
+
className={cn(ROW_CLASS, 'items-start')}
|
|
406
|
+
key={`${hint.keys.join('+')}:${hint.label}`}
|
|
407
|
+
>
|
|
408
|
+
<ShortcutSequence active={hint.active} keys={hint.keys} />
|
|
409
|
+
<div className="min-w-0">
|
|
410
|
+
<div
|
|
411
|
+
className={cn(
|
|
412
|
+
'text-xs leading-5',
|
|
413
|
+
hint.active ? 'font-medium text-white' : 'text-muted-foreground',
|
|
414
|
+
)}
|
|
415
|
+
>
|
|
416
|
+
{hint.label}
|
|
417
|
+
</div>
|
|
418
|
+
{hint.subtitle ? (
|
|
419
|
+
<div className="text-[10px] text-muted-foreground/70 leading-snug">
|
|
420
|
+
{hint.subtitle}
|
|
421
|
+
</div>
|
|
422
|
+
) : null}
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
))}
|
|
426
|
+
</div>
|
|
427
|
+
)
|
|
428
|
+
}
|