@pascal-app/editor 1.0.3 → 1.0.4
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 +11 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +202 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +36 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +344 -342
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +54 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +294 -169
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +8 -4
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +46 -20
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +325 -14
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -4
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +123 -9
- package/src/hooks/use-auto-save.ts +34 -8
- package/src/hooks/use-keyboard.test.ts +178 -3
- package/src/hooks/use-keyboard.ts +89 -71
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +246 -39
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +59 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +485 -0
- package/src/lib/glb-export.ts +237 -119
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +18 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +1 -1
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test'
|
|
|
2
2
|
import type { FloorplanGeometry } from '@pascal-app/core'
|
|
3
3
|
import PDFDocument from 'pdfkit'
|
|
4
4
|
import { floorplanGeometryMetadata } from './floorplan-extension'
|
|
5
|
-
import { FloorplanPdfDocument } from './floorplan-pdfkit-document'
|
|
5
|
+
import { FloorplanPdfDocument, loadFloorplanPdfFonts } from './floorplan-pdfkit-document'
|
|
6
6
|
import { renderFloorplanGeometryToPdfKit } from './floorplan-pdfkit-renderer'
|
|
7
7
|
|
|
8
8
|
describe('renderFloorplanGeometryToPdfKit', () => {
|
|
@@ -20,7 +20,7 @@ describe('renderFloorplanGeometryToPdfKit', () => {
|
|
|
20
20
|
const pdf = await renderTestPdf(geometry)
|
|
21
21
|
|
|
22
22
|
expect(pdf).toContain('BT')
|
|
23
|
-
expect(pdf).
|
|
23
|
+
expect(extractPdfText(pdf)).toBe('13m')
|
|
24
24
|
expect(pdf).toMatch(/0\.1 w/)
|
|
25
25
|
expect(pdf).toMatch(/0\.15 w/)
|
|
26
26
|
expect(pdf).not.toMatch(/ c\n/)
|
|
@@ -40,7 +40,7 @@ describe('renderFloorplanGeometryToPdfKit', () => {
|
|
|
40
40
|
|
|
41
41
|
const pdf = await renderTestPdf(geometry, 45)
|
|
42
42
|
|
|
43
|
-
expect(pdf).
|
|
43
|
+
expect(extractPdfText(pdf)).toBe('ROOM 101')
|
|
44
44
|
expect(pdf).toContain('BT')
|
|
45
45
|
})
|
|
46
46
|
|
|
@@ -76,13 +76,13 @@ describe('renderFloorplanGeometryToPdfKit', () => {
|
|
|
76
76
|
|
|
77
77
|
const pdfs = await Promise.all(geometries.map((geometry) => renderTestPdf(geometry)))
|
|
78
78
|
const baseFonts = pdfs.flatMap((pdf) =>
|
|
79
|
-
[...pdf.matchAll(/\/BaseFont \/([^\n]+)/g)].map((match) => match[1]),
|
|
79
|
+
[...pdf.matchAll(/\/BaseFont \/(?:[A-Z]{6}\+)?([^\n]+)/g)].map((match) => match[1]),
|
|
80
80
|
)
|
|
81
81
|
const fontSizes = pdfs.flatMap((pdf) =>
|
|
82
82
|
[...pdf.matchAll(/\/F\d+ ([\d.]+) Tf/g)].map((match) => match[1]),
|
|
83
83
|
)
|
|
84
84
|
|
|
85
|
-
expect([...new Set(baseFonts)]).toEqual(['
|
|
85
|
+
expect([...new Set(baseFonts)]).toEqual(['GeistMono-Regular'])
|
|
86
86
|
expect([...new Set(fontSizes)]).toEqual(['1.6'])
|
|
87
87
|
})
|
|
88
88
|
test('uses even-odd fill for compound plugin paths', async () => {
|
|
@@ -113,17 +113,101 @@ describe('renderFloorplanGeometryToPdfKit', () => {
|
|
|
113
113
|
})
|
|
114
114
|
})
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
describe('embedded PDF fonts', () => {
|
|
117
|
+
// what the permit set prints: code limits, dimension strings, schedule
|
|
118
|
+
// callouts, revision deltas — none of them in pdfkit's WinAnsi standard fonts
|
|
119
|
+
const PROBLEM_TEXT =
|
|
120
|
+
'Vult ≤ 195 mph, runs ≥ 24" · VERIFY ≈ 3\'-2" × 6\'-10 ½" — 29.69°N ¼ ¾ ⅛ 5′ 6″ △ ΔT → √0.6'
|
|
121
|
+
|
|
122
|
+
const textAt = (text: string, fontFamily?: string, fontWeight?: number) =>
|
|
123
|
+
({
|
|
124
|
+
kind: 'text',
|
|
125
|
+
x: 1,
|
|
126
|
+
y: 1,
|
|
127
|
+
text,
|
|
128
|
+
fontSize: 0.5,
|
|
129
|
+
fontFamily,
|
|
130
|
+
fontWeight,
|
|
131
|
+
dominantBaseline: 'alphabetic',
|
|
132
|
+
}) satisfies FloorplanGeometry
|
|
133
|
+
|
|
134
|
+
test('the problem glyphs round-trip through the text layer in the sans and mono families', async () => {
|
|
135
|
+
for (const [family, weight] of [
|
|
136
|
+
['"IBM Plex Sans", Helvetica, Arial, sans-serif', 400],
|
|
137
|
+
['"IBM Plex Sans", Helvetica, Arial, sans-serif', 700],
|
|
138
|
+
['"IBM Plex Mono", Menlo, monospace', 400],
|
|
139
|
+
['"IBM Plex Mono", Menlo, monospace', 700],
|
|
140
|
+
] as const) {
|
|
141
|
+
const pdf = await renderTestPdf(textAt(PROBLEM_TEXT, family, weight), 0, false)
|
|
142
|
+
expect(extractPdfText(pdf)).toBe(PROBLEM_TEXT)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('embeds subset TrueType faces, never the unembedded standard 14', async () => {
|
|
147
|
+
const pdf = await renderTestPdf(textAt(PROBLEM_TEXT), 0, false)
|
|
148
|
+
const baseFonts = [...pdf.matchAll(/\/BaseFont \/([^\n]+)/g)].map((match) => match[1])
|
|
149
|
+
expect(baseFonts.length).toBeGreaterThan(0)
|
|
150
|
+
for (const name of baseFonts) expect(name).toMatch(/^[A-Z]{6}\+(LiberationSans|GeistMono)/)
|
|
151
|
+
expect(pdf).toContain('/FontFile2')
|
|
152
|
+
expect(pdf).not.toMatch(/\/BaseFont \/(Helvetica|Courier)/)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('draws the symbols neither face carries — warning, pointer, angle — as linework', async () => {
|
|
156
|
+
const pdf = await renderTestPdf(textAt('∠ 45° ⚠ open porch ▸ note'), 0, false)
|
|
157
|
+
// the warning keeps a real "!" in the text layer; nothing falls to the .notdef glyph
|
|
158
|
+
expect(extractPdfText(pdf)).toBe(' 45° ! open porch note')
|
|
159
|
+
expect(extractPdfText(pdf)).not.toContain('\u0000')
|
|
160
|
+
// the triangle's ring is an even-odd fill; the pointer and the angle's strokes are plain fills
|
|
161
|
+
expect(pdf).toMatch(/f\*/)
|
|
162
|
+
expect(pdf.match(/\nf\n/g)?.length).toBeGreaterThanOrEqual(2)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('collapses runs of white space the way SVG text does', async () => {
|
|
166
|
+
const pdf = await renderTestPdf(
|
|
167
|
+
{ ...textAt(' APN 06075 \t 030\n062 '), textAnchor: 'end' },
|
|
168
|
+
0,
|
|
169
|
+
false,
|
|
170
|
+
)
|
|
171
|
+
expect(extractPdfText(pdf)).toBe('APN 06075 030 062')
|
|
172
|
+
|
|
173
|
+
const raw = new PDFDocument({ autoFirstPage: false, compress: false })
|
|
174
|
+
const doc = new FloorplanPdfDocument(raw, [200, 200], await loadFloorplanPdfFonts())
|
|
175
|
+
const style = { fontFamily: 'sans-serif', fontSize: 10 }
|
|
176
|
+
expect(doc.measureText(`RIDGE 20'-1"`, style)).toBe(doc.measureText(`RIDGE 20'-1"`, style))
|
|
177
|
+
// a no-break space is content, not white space
|
|
178
|
+
expect(doc.measureText('A\u00a0\u00a0B', style)).toBeGreaterThan(doc.measureText('A B', style))
|
|
179
|
+
raw.end()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('measures a mixed-face string as the sum of the runs it draws', async () => {
|
|
183
|
+
const raw = new PDFDocument({ autoFirstPage: false, compress: false })
|
|
184
|
+
const doc = new FloorplanPdfDocument(raw, [200, 200], await loadFloorplanPdfFonts())
|
|
185
|
+
const style = { fontFamily: 'sans-serif', fontSize: 10 }
|
|
186
|
+
const whole = doc.measureText('≤3△', style)
|
|
187
|
+
const parts =
|
|
188
|
+
doc.measureText('≤3', style) + doc.measureText('△', { ...style, fontFamily: 'monospace' })
|
|
189
|
+
expect(whole).toBeCloseTo(parts, 6)
|
|
190
|
+
// Liberation Sans keeps Helvetica's advance widths (the sheets were laid out against them)
|
|
191
|
+
expect(doc.measureText('0', style)).toBeCloseTo(5.56, 2)
|
|
192
|
+
raw.end()
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
async function renderTestPdf(
|
|
197
|
+
geometry: FloorplanGeometry,
|
|
198
|
+
rotationDeg = 0,
|
|
199
|
+
annotationLayer = true,
|
|
200
|
+
): Promise<string> {
|
|
117
201
|
const raw = new PDFDocument({ autoFirstPage: false, compress: false })
|
|
118
202
|
const chunks: Buffer[] = []
|
|
119
203
|
raw.on('data', (chunk: Buffer) => chunks.push(chunk))
|
|
120
204
|
const completed = new Promise<string>((resolve) => {
|
|
121
205
|
raw.on('end', () => resolve(Buffer.concat(chunks).toString('latin1')))
|
|
122
206
|
})
|
|
123
|
-
const doc = new FloorplanPdfDocument(raw, [200, 200])
|
|
207
|
+
const doc = new FloorplanPdfDocument(raw, [200, 200], await loadFloorplanPdfFonts())
|
|
124
208
|
doc.addPage()
|
|
125
209
|
await renderFloorplanGeometryToPdfKit(doc, geometry, {
|
|
126
|
-
annotationLayer
|
|
210
|
+
annotationLayer,
|
|
127
211
|
placement: { x: 20, y: 20, width: 100, height: 100 },
|
|
128
212
|
rotationDeg,
|
|
129
213
|
viewport: { x: 0, y: -2, width: 20, height: 20 },
|
|
@@ -131,3 +215,49 @@ async function renderTestPdf(geometry: FloorplanGeometry, rotationDeg = 0): Prom
|
|
|
131
215
|
raw.end()
|
|
132
216
|
return completed
|
|
133
217
|
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The text an uncompressed pdfkit file carries, in drawing order: every TJ's
|
|
221
|
+
* glyph ids decoded through its font's ToUnicode CMap — what a PDF reader's
|
|
222
|
+
* text layer (search, copy, pdftotext) reads back.
|
|
223
|
+
*/
|
|
224
|
+
function extractPdfText(pdf: string): string {
|
|
225
|
+
const objects = new Map<string, string>()
|
|
226
|
+
for (const match of pdf.matchAll(/(\d+) 0 obj\n([\s\S]*?)\nendobj/g)) {
|
|
227
|
+
objects.set(match[1]!, match[2]!)
|
|
228
|
+
}
|
|
229
|
+
const fontObjects = new Map<string, string>()
|
|
230
|
+
for (const match of pdf.matchAll(/\/(F\d+) (\d+) 0 R/g)) fontObjects.set(match[1]!, match[2]!)
|
|
231
|
+
const cmaps = new Map<string, Map<number, string>>()
|
|
232
|
+
const cmapFor = (resource: string) => {
|
|
233
|
+
let cmap = cmaps.get(resource)
|
|
234
|
+
if (cmap) return cmap
|
|
235
|
+
cmap = new Map()
|
|
236
|
+
const font = objects.get(fontObjects.get(resource) ?? '') ?? ''
|
|
237
|
+
const stream = objects.get(font.match(/\/ToUnicode (\d+) 0 R/)?.[1] ?? '') ?? ''
|
|
238
|
+
for (const range of stream.matchAll(/<([0-9a-f]{4})> <[0-9a-f]{4}> \[([^\]]*)\]/g)) {
|
|
239
|
+
let gid = Number.parseInt(range[1]!, 16)
|
|
240
|
+
for (const entry of range[2]!.matchAll(/<([0-9a-f ]+)>/g)) {
|
|
241
|
+
const units = entry[1]!.split(' ').map((hex) => Number.parseInt(hex, 16))
|
|
242
|
+
cmap.set(gid++, String.fromCharCode(...units))
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
cmaps.set(resource, cmap)
|
|
246
|
+
return cmap
|
|
247
|
+
}
|
|
248
|
+
let text = ''
|
|
249
|
+
let font = ''
|
|
250
|
+
for (const op of pdf.matchAll(/\/(F\d+) [\d.]+ Tf|\[([^\]]*)\] TJ/g)) {
|
|
251
|
+
if (op[1]) {
|
|
252
|
+
font = op[1]
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
const cmap = cmapFor(font)
|
|
256
|
+
for (const hex of op[2]!.matchAll(/<([0-9a-f]+)>/g)) {
|
|
257
|
+
for (let i = 0; i < hex[1]!.length; i += 4) {
|
|
258
|
+
text += cmap.get(Number.parseInt(hex[1]!.slice(i, i + 4), 16)) ?? '\uFFFD'
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return text
|
|
263
|
+
}
|
|
@@ -11,11 +11,11 @@ import {
|
|
|
11
11
|
import { resolveFloorplanLabelAngle } from '../../components/editor-2d/renderers/floorplan-label-angle'
|
|
12
12
|
import type { FloorplanExportBounds } from './floorplan-export'
|
|
13
13
|
import { readFloorplanGeometryMetadata } from './floorplan-extension'
|
|
14
|
-
import type { FloorplanPdfDocument } from './floorplan-pdfkit-document'
|
|
14
|
+
import type { FloorplanPdfBaseline, FloorplanPdfDocument } from './floorplan-pdfkit-document'
|
|
15
15
|
|
|
16
16
|
const DIMENSION_LINE_WIDTH_PT = 0.5
|
|
17
17
|
const DIMENSION_TICK_WIDTH_PT = 0.75
|
|
18
|
-
const DIMENSION_TEXT_FONT_FAMILY = '
|
|
18
|
+
const DIMENSION_TEXT_FONT_FAMILY = 'monospace'
|
|
19
19
|
const DIMENSION_TEXT_FONT_SIZE_PT = 8
|
|
20
20
|
const DIMENSION_TEXT_FONT_WEIGHT = 400
|
|
21
21
|
const DIMENSION_BASELINE_OFFSET_PT = 5
|
|
@@ -35,6 +35,7 @@ type RenderContext = {
|
|
|
35
35
|
annotationLabelShiftIndex: number
|
|
36
36
|
annotationLabelShifts: readonly FloorplanPoint[]
|
|
37
37
|
annotationLayer: boolean
|
|
38
|
+
dimensionTextSizePt: number
|
|
38
39
|
sceneRotationDeg: number
|
|
39
40
|
unitsPerPoint: number
|
|
40
41
|
}
|
|
@@ -52,6 +53,8 @@ export async function renderFloorplanGeometryToPdfKit(
|
|
|
52
53
|
options: {
|
|
53
54
|
annotationLabelShifts?: readonly FloorplanPoint[]
|
|
54
55
|
annotationLayer: boolean
|
|
56
|
+
/** Paper size of dimension strings; a drafted sheet prints them larger. */
|
|
57
|
+
dimensionTextSizePt?: number
|
|
55
58
|
placement: FloorplanPdfKitPlacement
|
|
56
59
|
rotationDeg: number
|
|
57
60
|
viewport: FloorplanExportBounds
|
|
@@ -70,6 +73,7 @@ export async function renderFloorplanGeometryToPdfKit(
|
|
|
70
73
|
annotationLabelShiftIndex: 0,
|
|
71
74
|
annotationLabelShifts: options.annotationLabelShifts ?? [],
|
|
72
75
|
annotationLayer: options.annotationLayer,
|
|
76
|
+
dimensionTextSizePt: options.dimensionTextSizePt ?? DIMENSION_TEXT_FONT_SIZE_PT,
|
|
73
77
|
sceneRotationDeg: options.rotationDeg,
|
|
74
78
|
unitsPerPoint: 1 / pointsPerUnit,
|
|
75
79
|
})
|
|
@@ -186,8 +190,11 @@ function paintStyledGeometry(
|
|
|
186
190
|
geometry: StyledGeometry,
|
|
187
191
|
context: RenderContext,
|
|
188
192
|
): void {
|
|
189
|
-
|
|
190
|
-
|
|
193
|
+
// SVG's `transparent` / `none` mean "no paint" — pdfkit would read either
|
|
194
|
+
// as a colour name it does not know and paint black (the furniture sprites'
|
|
195
|
+
// hit-target polygons printed as black boxes).
|
|
196
|
+
const fill = isPaint(geometry.fill) ? (geometry.fill as string) : null
|
|
197
|
+
const stroke = isPaint(geometry.stroke) ? (geometry.stroke as string) : null
|
|
191
198
|
const opacity = geometry.opacity ?? 1
|
|
192
199
|
const fillOpacity = (geometry.fillOpacity ?? 1) * opacity
|
|
193
200
|
const strokeOpacity = (geometry.strokeOpacity ?? 1) * opacity
|
|
@@ -212,6 +219,18 @@ function paintStyledGeometry(
|
|
|
212
219
|
else if (stroke) raw.stroke(stroke)
|
|
213
220
|
}
|
|
214
221
|
|
|
222
|
+
function isPaint(value: unknown): boolean {
|
|
223
|
+
if (typeof value !== 'string') return false
|
|
224
|
+
const v = value.trim().toLowerCase()
|
|
225
|
+
return (
|
|
226
|
+
v !== '' &&
|
|
227
|
+
v !== 'none' &&
|
|
228
|
+
v !== 'transparent' &&
|
|
229
|
+
!v.startsWith('url(') &&
|
|
230
|
+
!v.startsWith('var(')
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
215
234
|
function resolveStrokeWidth(geometry: StyledGeometry, context: RenderContext): number {
|
|
216
235
|
if (context.annotationLayer) return DIMENSION_LINE_WIDTH_PT * context.unitsPerPoint
|
|
217
236
|
if (geometry.vectorEffect === 'non-scaling-stroke') {
|
|
@@ -247,7 +266,7 @@ function drawGeometryText(
|
|
|
247
266
|
const dimensionValue =
|
|
248
267
|
readFloorplanGeometryMetadata(geometry).annotationRole === 'automatic-dimension'
|
|
249
268
|
const fontSize = context.annotationLayer
|
|
250
|
-
? (dimensionValue ?
|
|
269
|
+
? (dimensionValue ? context.dimensionTextSizePt : annotationTextSizePt(geometry)) *
|
|
251
270
|
context.unitsPerPoint
|
|
252
271
|
: geometry.fontSize
|
|
253
272
|
const outlinedForScreen = geometry.paintOrder === 'stroke' && !!geometry.stroke
|
|
@@ -258,6 +277,7 @@ function drawGeometryText(
|
|
|
258
277
|
drawNativeText(doc, {
|
|
259
278
|
angleDeg: geometry.upright ? -context.sceneRotationDeg : 0,
|
|
260
279
|
anchor: geometry.textAnchor ?? 'start',
|
|
280
|
+
baseline: geometry.dominantBaseline ?? 'middle',
|
|
261
281
|
fill,
|
|
262
282
|
fontFamily: dimensionValue ? DIMENSION_TEXT_FONT_FAMILY : geometry.fontFamily,
|
|
263
283
|
fontSize,
|
|
@@ -270,7 +290,10 @@ function drawGeometryText(
|
|
|
270
290
|
}
|
|
271
291
|
|
|
272
292
|
function annotationTextSizePt(geometry: Extract<FloorplanGeometry, { kind: 'text' }>): number {
|
|
273
|
-
|
|
293
|
+
const metadata = readFloorplanGeometryMetadata(geometry)
|
|
294
|
+
// an explicit paper size wins over the role's (a sheet's room tag sets its own)
|
|
295
|
+
if (typeof metadata.textSizePt === 'number' && metadata.textSizePt > 0) return metadata.textSizePt
|
|
296
|
+
switch (metadata.annotationRole) {
|
|
274
297
|
case 'room-label':
|
|
275
298
|
if (geometry.fontSize >= 0.18) return DEFAULT_ANNOTATION_FONT_SIZE_PT
|
|
276
299
|
if (geometry.fontSize >= 0.145) return ROOM_NUMBER_FONT_SIZE_PT
|
|
@@ -480,7 +503,7 @@ function drawDimensionText(
|
|
|
480
503
|
stroke: string,
|
|
481
504
|
context: RenderContext,
|
|
482
505
|
): void {
|
|
483
|
-
const fontSize =
|
|
506
|
+
const fontSize = context.dimensionTextSizePt * context.unitsPerPoint
|
|
484
507
|
const y =
|
|
485
508
|
geometry.textPosition === 'centered'
|
|
486
509
|
? fontSize * 0.35
|
|
@@ -495,6 +518,7 @@ function drawDimensionText(
|
|
|
495
518
|
.translate(shift[0], shift[1])
|
|
496
519
|
drawNativeText(doc, {
|
|
497
520
|
anchor: 'middle',
|
|
521
|
+
baseline: 'alphabetic',
|
|
498
522
|
fill: stroke,
|
|
499
523
|
fontFamily: DIMENSION_TEXT_FONT_FAMILY,
|
|
500
524
|
fontSize,
|
|
@@ -512,7 +536,7 @@ function drawDimensionLabel(
|
|
|
512
536
|
context: RenderContext,
|
|
513
537
|
): void {
|
|
514
538
|
const unitsPerPoint = context.unitsPerPoint
|
|
515
|
-
const fontSize =
|
|
539
|
+
const fontSize = context.dimensionTextSizePt * unitsPerPoint
|
|
516
540
|
const padX = 6 * unitsPerPoint
|
|
517
541
|
const padY = 3 * unitsPerPoint
|
|
518
542
|
const textWidth = geometry.text.length * 6.2 * unitsPerPoint
|
|
@@ -538,6 +562,7 @@ function drawDimensionLabel(
|
|
|
538
562
|
.fill()
|
|
539
563
|
drawNativeText(doc, {
|
|
540
564
|
anchor: 'middle',
|
|
565
|
+
baseline: 'middle',
|
|
541
566
|
fill: '#111827',
|
|
542
567
|
fontFamily: DIMENSION_TEXT_FONT_FAMILY,
|
|
543
568
|
fontSize,
|
|
@@ -569,8 +594,9 @@ function drawEqualSpacingBadge(
|
|
|
569
594
|
.fill()
|
|
570
595
|
drawNativeText(doc, {
|
|
571
596
|
anchor: 'middle',
|
|
597
|
+
baseline: 'middle',
|
|
572
598
|
fill: '#334155',
|
|
573
|
-
fontFamily:
|
|
599
|
+
fontFamily: DIMENSION_TEXT_FONT_FAMILY,
|
|
574
600
|
fontSize,
|
|
575
601
|
fontWeight: 600,
|
|
576
602
|
text: geometry.text,
|
|
@@ -585,6 +611,7 @@ function drawNativeText(
|
|
|
585
611
|
options: {
|
|
586
612
|
angleDeg?: number
|
|
587
613
|
anchor: 'start' | 'middle' | 'end'
|
|
614
|
+
baseline: FloorplanPdfBaseline
|
|
588
615
|
fill: string
|
|
589
616
|
fontFamily?: string
|
|
590
617
|
fontSize: number
|
|
@@ -596,24 +623,17 @@ function drawNativeText(
|
|
|
596
623
|
},
|
|
597
624
|
): void {
|
|
598
625
|
const raw = doc.raw
|
|
599
|
-
const normalizedFamily = options.fontFamily?.toLocaleLowerCase() ?? ''
|
|
600
|
-
const family =
|
|
601
|
-
normalizedFamily.includes('mono') || normalizedFamily.includes('courier')
|
|
602
|
-
? 'Courier'
|
|
603
|
-
: 'Helvetica'
|
|
604
|
-
const numericWeight = Number.parseInt(String(options.fontWeight ?? 400), 10)
|
|
605
|
-
const bold =
|
|
606
|
-
options.fontWeight === 'bold' || (Number.isFinite(numericWeight) && numericWeight >= 500)
|
|
607
626
|
raw.save().translate(options.x, options.y)
|
|
608
627
|
if (options.angleDeg) raw.rotate(options.angleDeg)
|
|
609
|
-
raw
|
|
610
|
-
.font(bold ? `${family}-Bold` : family)
|
|
611
|
-
.fontSize(options.fontSize)
|
|
612
|
-
.fillColor(options.fill)
|
|
628
|
+
raw.fillColor(options.fill)
|
|
613
629
|
raw.fillOpacity(options.opacity ?? 1)
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
630
|
+
doc.drawText(
|
|
631
|
+
options.text,
|
|
632
|
+
0,
|
|
633
|
+
0,
|
|
634
|
+
{ fontFamily: options.fontFamily, fontSize: options.fontSize, fontWeight: options.fontWeight },
|
|
635
|
+
{ anchor: options.anchor, baseline: options.baseline },
|
|
636
|
+
)
|
|
617
637
|
raw.restore()
|
|
618
638
|
}
|
|
619
639
|
|
|
@@ -653,7 +673,18 @@ async function drawImage(
|
|
|
653
673
|
raw.restore()
|
|
654
674
|
}
|
|
655
675
|
|
|
656
|
-
function blobToDataUrl(blob: Blob): Promise<string> {
|
|
676
|
+
async function blobToDataUrl(blob: Blob): Promise<string> {
|
|
677
|
+
// FileReader exists only in browsers; the headless export (scripts/demo/
|
|
678
|
+
// print-set.ts) runs the same renderer under Bun, where the bytes are
|
|
679
|
+
// base64-encoded directly instead.
|
|
680
|
+
if (typeof FileReader === 'undefined') {
|
|
681
|
+
const bytes = new Uint8Array(await blob.arrayBuffer())
|
|
682
|
+
let binary = ''
|
|
683
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
684
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000))
|
|
685
|
+
}
|
|
686
|
+
return `data:${blob.type || 'image/png'};base64,${btoa(binary)}`
|
|
687
|
+
}
|
|
657
688
|
return new Promise((resolve, reject) => {
|
|
658
689
|
const reader = new FileReader()
|
|
659
690
|
reader.onerror = () => reject(reader.error)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
LevelNode,
|
|
6
|
+
loadPlugin,
|
|
7
|
+
nodeRegistry,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
import { FloorplanPreview } from '../../components/viewer/floorplan-preview'
|
|
12
|
+
import { DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY } from './annotation-visibility'
|
|
13
|
+
import { collectFloorplanGeometry, floorplanExportInstalledPlugins } from './floorplan-export'
|
|
14
|
+
import { FLOORPLAN_NODE_EXTENSION_KEY } from './floorplan-extension'
|
|
15
|
+
|
|
16
|
+
// Plugin API v1, no-install case: a plugin kind's floor-plan output appears in
|
|
17
|
+
// the read-only preview and the PDF export only while the project has the
|
|
18
|
+
// plugin installed.
|
|
19
|
+
|
|
20
|
+
const PLUGIN_ID = 'fixture:floorplan'
|
|
21
|
+
const KIND = 'fixture:bed'
|
|
22
|
+
const LINKED_KIND = 'fixture:shaft'
|
|
23
|
+
const MARKER_FILL = '#a09a09'
|
|
24
|
+
|
|
25
|
+
const level = LevelNode.parse({ id: 'level_plugin_plan', children: ['fxbed_plan'] })
|
|
26
|
+
const bed = {
|
|
27
|
+
object: 'node',
|
|
28
|
+
id: 'fxbed_plan',
|
|
29
|
+
type: KIND,
|
|
30
|
+
parentId: level.id,
|
|
31
|
+
visible: true,
|
|
32
|
+
metadata: {},
|
|
33
|
+
} as unknown as AnyNode
|
|
34
|
+
// Lives on the upper level but draws on this one through `linkedLevelIds`.
|
|
35
|
+
const upper = LevelNode.parse({ id: 'level_plugin_upper', level: 1, children: ['fxshaft_plan'] })
|
|
36
|
+
const shaft = {
|
|
37
|
+
object: 'node',
|
|
38
|
+
id: 'fxshaft_plan',
|
|
39
|
+
type: LINKED_KIND,
|
|
40
|
+
parentId: upper.id,
|
|
41
|
+
visible: true,
|
|
42
|
+
metadata: {},
|
|
43
|
+
} as unknown as AnyNode
|
|
44
|
+
const nodes = { [level.id]: level, [bed.id]: bed, [upper.id]: upper, [shaft.id]: shaft } as Record<
|
|
45
|
+
string,
|
|
46
|
+
AnyNode
|
|
47
|
+
>
|
|
48
|
+
|
|
49
|
+
let restoreRegistry: () => void
|
|
50
|
+
|
|
51
|
+
beforeEach(async () => {
|
|
52
|
+
restoreRegistry = nodeRegistry._snapshot()
|
|
53
|
+
await loadPlugin({
|
|
54
|
+
id: PLUGIN_ID,
|
|
55
|
+
apiVersion: 1,
|
|
56
|
+
nodes: [
|
|
57
|
+
{
|
|
58
|
+
kind: KIND,
|
|
59
|
+
schemaVersion: 1,
|
|
60
|
+
schema: z.looseObject({ id: z.string(), type: z.literal(KIND) }),
|
|
61
|
+
category: 'structure',
|
|
62
|
+
defaults: () => ({}),
|
|
63
|
+
capabilities: {},
|
|
64
|
+
floorplan: () => ({ kind: 'rect', x: 0, y: 0, width: 2, height: 1, fill: MARKER_FILL }),
|
|
65
|
+
} as unknown as AnyNodeDefinition,
|
|
66
|
+
{
|
|
67
|
+
kind: LINKED_KIND,
|
|
68
|
+
schemaVersion: 1,
|
|
69
|
+
schema: z.looseObject({ id: z.string(), type: z.literal(LINKED_KIND) }),
|
|
70
|
+
category: 'structure',
|
|
71
|
+
defaults: () => ({}),
|
|
72
|
+
capabilities: {},
|
|
73
|
+
extensions: { [FLOORPLAN_NODE_EXTENSION_KEY]: { linkedLevelIds: () => [level.id] } },
|
|
74
|
+
floorplan: () => ({ kind: 'rect', x: 3, y: 0, width: 1, height: 1 }),
|
|
75
|
+
} as unknown as AnyNodeDefinition,
|
|
76
|
+
],
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
restoreRegistry()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
function exportedIds(installedPlugins: readonly string[] | undefined) {
|
|
85
|
+
return collectFloorplanGeometry(
|
|
86
|
+
nodes,
|
|
87
|
+
level.id,
|
|
88
|
+
'full',
|
|
89
|
+
'metric',
|
|
90
|
+
'meters',
|
|
91
|
+
DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
|
|
92
|
+
'floor-plan',
|
|
93
|
+
'finished-faces',
|
|
94
|
+
installedPlugins,
|
|
95
|
+
).map(({ id }) => id)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function previewMarkup(installedPlugins: readonly string[]) {
|
|
99
|
+
return renderToStaticMarkup(
|
|
100
|
+
<FloorplanPreview
|
|
101
|
+
levelId={level.id}
|
|
102
|
+
scene={{ nodes, installedPlugins }}
|
|
103
|
+
showCompass={false}
|
|
104
|
+
showLevelSelector={false}
|
|
105
|
+
/>,
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
describe('floor-plan output of a plugin kind', () => {
|
|
110
|
+
test('the read-only preview draws it only while the plugin is installed', () => {
|
|
111
|
+
expect(previewMarkup([PLUGIN_ID])).toContain(MARKER_FILL)
|
|
112
|
+
expect(previewMarkup([])).not.toContain(MARKER_FILL)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('the PDF export draws level and linked-level kinds while the plugin is installed', () => {
|
|
116
|
+
expect(exportedIds([PLUGIN_ID]).sort()).toEqual([bed.id, shaft.id].sort())
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('the PDF export omits level and linked-level kinds while the plugin is not installed', () => {
|
|
120
|
+
expect(exportedIds([])).toEqual([])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('a legacy scene without explicit install state draws every loaded plugin kind', () => {
|
|
124
|
+
const legacy = floorplanExportInstalledPlugins({
|
|
125
|
+
installedPlugins: ['host:default-panel'],
|
|
126
|
+
hasExplicitPluginInstallState: false,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
expect(legacy).toBeUndefined()
|
|
130
|
+
expect(exportedIds(legacy).sort()).toEqual([bed.id, shaft.id].sort())
|
|
131
|
+
expect(
|
|
132
|
+
floorplanExportInstalledPlugins({
|
|
133
|
+
installedPlugins: [],
|
|
134
|
+
hasExplicitPluginInstallState: true,
|
|
135
|
+
}),
|
|
136
|
+
).toEqual([])
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -10,6 +10,10 @@ export type FloorplanViewState = {
|
|
|
10
10
|
unit: 'metric' | 'imperial'
|
|
11
11
|
metricNotation?: 'meters' | 'millimeters'
|
|
12
12
|
purpose?: 'edit' | 'document'
|
|
13
|
+
/** Sheet drafting conventions (see `FloorplanContextExtension.drafting`). */
|
|
14
|
+
drafting?: boolean
|
|
15
|
+
/** Room labels drawn as screen pills (see `FloorplanContextExtension.roomLabelOverlay`). */
|
|
16
|
+
roomLabelOverlay?: boolean
|
|
13
17
|
wallDimensionReference?: FloorplanWallDimensionReference
|
|
14
18
|
highlighted: boolean
|
|
15
19
|
hovered: boolean
|
|
@@ -59,6 +63,8 @@ export function buildFloorplanContext(
|
|
|
59
63
|
automaticDimensions: viewState.automaticDimensions,
|
|
60
64
|
metricNotation: viewState.metricNotation ?? 'meters',
|
|
61
65
|
purpose: viewState.purpose ?? 'edit',
|
|
66
|
+
drafting: viewState.drafting === true,
|
|
67
|
+
roomLabelOverlay: viewState.roomLabelOverlay === true,
|
|
62
68
|
wallDimensionReference: viewState.wallDimensionReference,
|
|
63
69
|
}),
|
|
64
70
|
viewState: viewState.palette
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Point2D } from '@pascal-app/core'
|
|
2
|
+
import { getWallFaceOffsets, type WallJustification } from '@pascal-app/core'
|
|
2
3
|
import type { FloorplanLineSegment, FloorplanSelectionBounds } from './types'
|
|
3
4
|
|
|
4
5
|
// Baseline rotation (deg) that orients the plan-local scene "north up" on
|
|
@@ -107,7 +108,11 @@ export function movePlanPointTowards(start: Point2D, end: Point2D, distance: num
|
|
|
107
108
|
return interpolatePlanPoint(start, end, Math.min(1, distance / totalDistance))
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
export function getThickPlanLinePolygon(
|
|
111
|
+
export function getThickPlanLinePolygon(
|
|
112
|
+
line: FloorplanLineSegment,
|
|
113
|
+
thickness: number,
|
|
114
|
+
justification?: WallJustification,
|
|
115
|
+
): Point2D[] {
|
|
111
116
|
const dx = line.end.x - line.start.x
|
|
112
117
|
const dy = line.end.y - line.start.y
|
|
113
118
|
const length = Math.hypot(dx, dy)
|
|
@@ -116,15 +121,17 @@ export function getThickPlanLinePolygon(line: FloorplanLineSegment, thickness: n
|
|
|
116
121
|
return [line.start, line.end, line.end, line.start]
|
|
117
122
|
}
|
|
118
123
|
|
|
119
|
-
const
|
|
120
|
-
const normalX = (-dy / length) *
|
|
121
|
-
const normalY = (dx / length) *
|
|
124
|
+
const { a, b } = getWallFaceOffsets({ thickness, justification })
|
|
125
|
+
const normalX = (-dy / length) * a
|
|
126
|
+
const normalY = (dx / length) * a
|
|
122
127
|
|
|
128
|
+
const backX = (-dy / length) * -b
|
|
129
|
+
const backY = (dx / length) * -b
|
|
123
130
|
return [
|
|
124
131
|
{ x: line.start.x + normalX, y: line.start.y + normalY },
|
|
125
132
|
{ x: line.end.x + normalX, y: line.end.y + normalY },
|
|
126
|
-
{ x: line.end.x -
|
|
127
|
-
{ x: line.start.x -
|
|
133
|
+
{ x: line.end.x - backX, y: line.end.y - backY },
|
|
134
|
+
{ x: line.start.x - backX, y: line.start.y - backY },
|
|
128
135
|
]
|
|
129
136
|
}
|
|
130
137
|
|
|
@@ -29,20 +29,11 @@ export {
|
|
|
29
29
|
collectLevelDescendants,
|
|
30
30
|
getItemFloorplanTransform,
|
|
31
31
|
} from './items'
|
|
32
|
-
export {
|
|
33
|
-
buildFloorplanStairEntry,
|
|
34
|
-
computeFloorplanStairSegmentTransforms,
|
|
35
|
-
getFloorplanStairSegmentPolygon,
|
|
36
|
-
} from './stairs'
|
|
37
32
|
export type {
|
|
38
33
|
FloorplanItemEntry,
|
|
39
34
|
FloorplanLineSegment,
|
|
40
35
|
FloorplanNodeTransform,
|
|
41
36
|
FloorplanSelectionBounds,
|
|
42
|
-
FloorplanStairArrowEntry,
|
|
43
|
-
FloorplanStairEntry,
|
|
44
|
-
FloorplanStairSegmentEntry,
|
|
45
37
|
LevelDescendantMap,
|
|
46
|
-
StairSegmentTransform,
|
|
47
38
|
} from './types'
|
|
48
39
|
export { getFloorplanWall, getFloorplanWallThickness } from './walls'
|
|
@@ -2,6 +2,8 @@ import {
|
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
4
|
getScaledDimensions,
|
|
5
|
+
getWallBodyCenterOffset,
|
|
6
|
+
getWallLocalFaceZ,
|
|
5
7
|
type ItemNode,
|
|
6
8
|
type LevelNode,
|
|
7
9
|
useLiveTransforms,
|
|
@@ -82,8 +84,9 @@ export function getItemFloorplanTransform(
|
|
|
82
84
|
)
|
|
83
85
|
const wallLocalZ =
|
|
84
86
|
item.asset.attachTo === 'wall-side'
|
|
85
|
-
? (
|
|
86
|
-
: item.position[2]
|
|
87
|
+
? getWallLocalFaceZ(parentNode, item.side === 'front' ? 'a' : 'b')
|
|
88
|
+
: item.position[2] +
|
|
89
|
+
(item.asset.attachTo === 'wall' ? getWallBodyCenterOffset(parentNode) : 0)
|
|
87
90
|
const [offsetX, offsetY] = rotatePlanVector(item.position[0], wallLocalZ, wallRotation)
|
|
88
91
|
|
|
89
92
|
result = {
|