@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
|
@@ -27,6 +27,11 @@ export type FloorplanAnnotationRole =
|
|
|
27
27
|
| 'structural-grid'
|
|
28
28
|
| 'column-center'
|
|
29
29
|
| 'room-label'
|
|
30
|
+
/** A room's finish / ceiling-height lines — detail under the name, off in the clean plan. */
|
|
31
|
+
| 'room-detail'
|
|
32
|
+
/** The roof plan proper: outline, ridges, hips, slope arrows. Off over a floor plan unless asked. */
|
|
33
|
+
| 'roof-plan'
|
|
34
|
+
| 'roof-pitch'
|
|
30
35
|
| 'stair-annotation'
|
|
31
36
|
|
|
32
37
|
export type FloorplanSchedule = {
|
|
@@ -85,6 +90,8 @@ export type FloorplanNodeExtension<N extends AnyNode = AnyNode> = {
|
|
|
85
90
|
nodes: Readonly<Record<string, AnyNode>>
|
|
86
91
|
levelId: AnyNodeId
|
|
87
92
|
unit: 'metric' | 'imperial'
|
|
93
|
+
/** A drafted sheet's schedule: marks match the sheet's tags. */
|
|
94
|
+
drafting?: boolean
|
|
88
95
|
}) => FloorplanSchedule | null
|
|
89
96
|
linkedLevelIds?: (node: N) => readonly AnyNodeId[]
|
|
90
97
|
resolveForDrawing?: (args: {
|
|
@@ -98,11 +105,30 @@ type FloorplanGeometryMetadata = {
|
|
|
98
105
|
annotationRole?: FloorplanAnnotationRole
|
|
99
106
|
annotationObstacle?: 'bounds' | 'outline'
|
|
100
107
|
renderPass?: 'overlay'
|
|
108
|
+
/**
|
|
109
|
+
* Annotation text only: the size to print at, in paper points, instead of
|
|
110
|
+
* the size its role implies. Lets a sheet set a room tag's name larger than
|
|
111
|
+
* its number without inventing a role per size.
|
|
112
|
+
*/
|
|
113
|
+
textSizePt?: number
|
|
101
114
|
}
|
|
102
115
|
|
|
103
116
|
type FloorplanContextExtension = {
|
|
104
117
|
automaticDimensions: boolean
|
|
105
118
|
purpose: FloorplanRenderPurpose
|
|
119
|
+
/**
|
|
120
|
+
* Sheet drafting (the Sheets plan viewports): kinds draw the permit-set
|
|
121
|
+
* convention — fixtures as labelled linework, furniture as light outlines,
|
|
122
|
+
* no raster sprites. Only `collectSheetGeometry` sets it; the editor and the
|
|
123
|
+
* read-only viewer never do, so their plans keep today's look.
|
|
124
|
+
*/
|
|
125
|
+
drafting: boolean
|
|
126
|
+
/**
|
|
127
|
+
* The editor canvas draws room labels as screen pills over the plan
|
|
128
|
+
* (`RoomLabels2D`), so zones skip their drawn tags and wash there. Exports,
|
|
129
|
+
* sheets and the read-only viewer keep the drawn tags.
|
|
130
|
+
*/
|
|
131
|
+
roomLabelOverlay: boolean
|
|
106
132
|
metricNotation: FloorplanMetricNotation
|
|
107
133
|
wallDimensionReference: FloorplanWallDimensionReference
|
|
108
134
|
}
|
|
@@ -156,6 +182,8 @@ export function createFloorplanContextExtensions(
|
|
|
156
182
|
[FLOORPLAN_CONTEXT_EXTENSION_KEY]: {
|
|
157
183
|
automaticDimensions: values.automaticDimensions !== false,
|
|
158
184
|
purpose: values.purpose === 'document' ? 'document' : 'edit',
|
|
185
|
+
drafting: values.drafting === true,
|
|
186
|
+
roomLabelOverlay: values.roomLabelOverlay === true,
|
|
159
187
|
metricNotation: values.metricNotation === 'millimeters' ? 'millimeters' : 'meters',
|
|
160
188
|
wallDimensionReference: normalizeFloorplanWallDimensionReference(
|
|
161
189
|
values.wallDimensionReference,
|
|
@@ -171,6 +199,8 @@ export function readFloorplanContext(ctx: GeometryContext): FloorplanContextExte
|
|
|
171
199
|
return {
|
|
172
200
|
automaticDimensions: extension.automaticDimensions !== false,
|
|
173
201
|
purpose: extension.purpose === 'document' ? 'document' : 'edit',
|
|
202
|
+
drafting: extension.drafting === true,
|
|
203
|
+
roomLabelOverlay: extension.roomLabelOverlay === true,
|
|
174
204
|
metricNotation: extension.metricNotation === 'millimeters' ? 'millimeters' : 'meters',
|
|
175
205
|
wallDimensionReference: normalizeFloorplanWallDimensionReference(
|
|
176
206
|
extension.wallDimensionReference,
|
|
@@ -180,6 +210,8 @@ export function readFloorplanContext(ctx: GeometryContext): FloorplanContextExte
|
|
|
180
210
|
return {
|
|
181
211
|
automaticDimensions: true,
|
|
182
212
|
purpose: 'edit',
|
|
213
|
+
drafting: false,
|
|
214
|
+
roomLabelOverlay: false,
|
|
183
215
|
metricNotation: 'meters',
|
|
184
216
|
wallDimensionReference: DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE,
|
|
185
217
|
}
|
|
@@ -62,6 +62,8 @@ describe('floor-plan mode', () => {
|
|
|
62
62
|
openingMarks: false,
|
|
63
63
|
structuralGrids: false,
|
|
64
64
|
roomLabels: true,
|
|
65
|
+
roomDetails: false,
|
|
66
|
+
roofPlan: false,
|
|
65
67
|
stairAnnotations: false,
|
|
66
68
|
})
|
|
67
69
|
|
|
@@ -107,6 +109,8 @@ describe('floor-plan mode', () => {
|
|
|
107
109
|
openingMarks: false,
|
|
108
110
|
structuralGrids: false,
|
|
109
111
|
roomLabels: true,
|
|
112
|
+
roomDetails: false,
|
|
113
|
+
roofPlan: false,
|
|
110
114
|
stairAnnotations: false,
|
|
111
115
|
})
|
|
112
116
|
})
|
|
@@ -9,6 +9,124 @@ type PdfTextOptions = {
|
|
|
9
9
|
|
|
10
10
|
type PdfShapeStyle = 'F' | 'S'
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The faces every exported PDF embeds. pdfkit's standard 14 fonts are
|
|
14
|
+
* WinAnsi-only: the ≤ ≥ ≈ → △ Δ ⅛ ′ ″ the sheets print came out as byte
|
|
15
|
+
* pairs of the code point ("≤ 195 mph" printed as `"d 195 mph`), and the
|
|
16
|
+
* fonts were not embedded at all. Liberation Sans is metric-compatible with
|
|
17
|
+
* Helvetica, so every width the sheets were laid out against is unchanged;
|
|
18
|
+
* Geist Mono keeps Courier's 0.6 em advance. Both are SIL OFL 1.1 — the
|
|
19
|
+
* licences sit beside the files.
|
|
20
|
+
*/
|
|
21
|
+
const PDF_FONT_FILES = {
|
|
22
|
+
sans: new URL('./pdf-fonts/LiberationSans-Regular.ttf', import.meta.url),
|
|
23
|
+
'sans-bold': new URL('./pdf-fonts/LiberationSans-Bold.ttf', import.meta.url),
|
|
24
|
+
mono: new URL('./pdf-fonts/GeistMono-Regular.ttf', import.meta.url),
|
|
25
|
+
'mono-bold': new URL('./pdf-fonts/GeistMono-Bold.ttf', import.meta.url),
|
|
26
|
+
} as const
|
|
27
|
+
|
|
28
|
+
export type FloorplanPdfFace = keyof typeof PDF_FONT_FILES
|
|
29
|
+
export type FloorplanPdfFonts = Record<FloorplanPdfFace, ArrayBuffer>
|
|
30
|
+
|
|
31
|
+
/** pdfkit's built-in faces, used when the embedded ones could not be fetched. */
|
|
32
|
+
const STANDARD_FACES: Record<FloorplanPdfFace, string> = {
|
|
33
|
+
sans: 'Helvetica',
|
|
34
|
+
'sans-bold': 'Helvetica-Bold',
|
|
35
|
+
mono: 'Courier',
|
|
36
|
+
'mono-bold': 'Courier-Bold',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A glyph the face lacks is taken from the other family at the same weight. */
|
|
40
|
+
const OTHER_FAMILY: Record<FloorplanPdfFace, FloorplanPdfFace> = {
|
|
41
|
+
sans: 'mono',
|
|
42
|
+
'sans-bold': 'mono-bold',
|
|
43
|
+
mono: 'sans',
|
|
44
|
+
'mono-bold': 'sans-bold',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Symbols the sheets print that neither face carries, drawn as linework in
|
|
49
|
+
* the text's colour, with their advance in em. The warning triangle keeps a
|
|
50
|
+
* real "!" inside it, so the text layer still reads as a warning.
|
|
51
|
+
*/
|
|
52
|
+
const DRAWN_SYMBOLS = {
|
|
53
|
+
'⚠': { symbol: 'warning', advanceEm: 0.9 },
|
|
54
|
+
'▸': { symbol: 'pointer', advanceEm: 0.5 },
|
|
55
|
+
'∠': { symbol: 'angle', advanceEm: 0.8 },
|
|
56
|
+
} as const
|
|
57
|
+
|
|
58
|
+
type DrawnSymbol = (typeof DRAWN_SYMBOLS)[keyof typeof DRAWN_SYMBOLS]['symbol']
|
|
59
|
+
|
|
60
|
+
type TextRun =
|
|
61
|
+
| { kind: 'text'; face: FloorplanPdfFace; text: string }
|
|
62
|
+
| { kind: 'symbol'; symbol: DrawnSymbol; advanceEm: number }
|
|
63
|
+
|
|
64
|
+
/** The SVG `dominant-baseline` values the floor-plan geometry uses. */
|
|
65
|
+
export type FloorplanPdfBaseline = 'auto' | 'alphabetic' | 'middle' | 'central' | 'hanging'
|
|
66
|
+
|
|
67
|
+
export type FloorplanPdfTextStyle = {
|
|
68
|
+
fontFamily?: string
|
|
69
|
+
fontWeight?: number | string
|
|
70
|
+
fontSize: number
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* SVG text's white-space handling (`white-space: normal`; the geometry has no
|
|
75
|
+
* way to ask for `pre`): runs of spaces, tabs and line breaks print as one
|
|
76
|
+
* space and the ends are trimmed. `APN 06075` reads with one space on
|
|
77
|
+
* screen, so it does on paper — and is measured that way for anchoring.
|
|
78
|
+
*/
|
|
79
|
+
function collapseSvgWhitespace(text: string): string {
|
|
80
|
+
return text.replace(/[ \t\r\n]+/g, ' ').trim()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** pdfkit keeps the current face's metrics and its fontkit font on `_font`; neither is public API. */
|
|
84
|
+
type EmbeddedFace = {
|
|
85
|
+
ascender: number
|
|
86
|
+
descender: number
|
|
87
|
+
xHeight: number
|
|
88
|
+
font: { hasGlyphForCodePoint: (codePoint: number) => boolean }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let pdfFonts: Promise<FloorplanPdfFonts> | null = null
|
|
92
|
+
|
|
93
|
+
/** The embedded faces' bytes, fetched once and shared by every export. */
|
|
94
|
+
export function loadFloorplanPdfFonts(): Promise<FloorplanPdfFonts> {
|
|
95
|
+
if (pdfFonts) return pdfFonts
|
|
96
|
+
const loading = Promise.all(
|
|
97
|
+
Object.entries(PDF_FONT_FILES).map(async ([face, url]) => {
|
|
98
|
+
const response = await fetch(url)
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`[floorplan-export] Could not load the PDF font ${face} (HTTP ${response.status})`,
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
return [face, await response.arrayBuffer()] as const
|
|
105
|
+
}),
|
|
106
|
+
).then((entries) => Object.fromEntries(entries) as FloorplanPdfFonts)
|
|
107
|
+
pdfFonts = loading
|
|
108
|
+
// a failed fetch is retried by the next export rather than cached
|
|
109
|
+
loading.catch(() => {
|
|
110
|
+
if (pdfFonts === loading) pdfFonts = null
|
|
111
|
+
})
|
|
112
|
+
return loading
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** SVG font-family / font-weight → the embedded face that sets it. */
|
|
116
|
+
export function resolveFloorplanPdfFace(
|
|
117
|
+
fontFamily: string | undefined,
|
|
118
|
+
fontWeight: number | string | undefined,
|
|
119
|
+
): FloorplanPdfFace {
|
|
120
|
+
const family = fontFamily?.toLocaleLowerCase() ?? ''
|
|
121
|
+
const mono = family.includes('mono') || family.includes('courier')
|
|
122
|
+
const numericWeight = Number.parseInt(String(fontWeight ?? 400), 10)
|
|
123
|
+
const bold =
|
|
124
|
+
String(fontWeight).toLocaleLowerCase() === 'bold' ||
|
|
125
|
+
(Number.isFinite(numericWeight) && numericWeight >= 500)
|
|
126
|
+
if (mono) return bold ? 'mono-bold' : 'mono'
|
|
127
|
+
return bold ? 'sans-bold' : 'sans'
|
|
128
|
+
}
|
|
129
|
+
|
|
12
130
|
export class FloorplanPdfDocument {
|
|
13
131
|
readonly raw: PdfKitDocumentInstance
|
|
14
132
|
readonly internal: {
|
|
@@ -19,11 +137,21 @@ export class FloorplanPdfDocument {
|
|
|
19
137
|
}
|
|
20
138
|
|
|
21
139
|
private currentFontSize = 12
|
|
140
|
+
private currentFace: FloorplanPdfFace = 'sans'
|
|
22
141
|
private readonly defaultPageSize: readonly [number, number]
|
|
142
|
+
private readonly faces = new Map<FloorplanPdfFace, EmbeddedFace>()
|
|
143
|
+
private readonly embedded: boolean
|
|
23
144
|
|
|
24
|
-
|
|
145
|
+
/** `fonts` null sets every face in pdfkit's standard fonts (WinAnsi only). */
|
|
146
|
+
constructor(
|
|
147
|
+
raw: PdfKitDocumentInstance,
|
|
148
|
+
defaultPageSize: readonly [number, number],
|
|
149
|
+
fonts: FloorplanPdfFonts | null,
|
|
150
|
+
) {
|
|
25
151
|
this.raw = raw
|
|
26
152
|
this.defaultPageSize = defaultPageSize
|
|
153
|
+
this.embedded = fonts !== null
|
|
154
|
+
for (const [face, bytes] of Object.entries(fonts ?? {})) raw.registerFont(face, bytes)
|
|
27
155
|
this.internal = {
|
|
28
156
|
pageSize: {
|
|
29
157
|
getWidth: () => this.raw.page?.width ?? this.defaultPageSize[0],
|
|
@@ -61,22 +189,17 @@ export class FloorplanPdfDocument {
|
|
|
61
189
|
}
|
|
62
190
|
|
|
63
191
|
setFont(family: string, weight: string = 'normal'): this {
|
|
64
|
-
|
|
65
|
-
const normalizedWeight = weight.toLocaleLowerCase()
|
|
66
|
-
const bold = normalizedWeight === 'bold' || Number.parseInt(normalizedWeight, 10) >= 500
|
|
67
|
-
const base = normalizedFamily.includes('courier') ? 'Courier' : 'Helvetica'
|
|
68
|
-
this.raw.font(bold ? `${base}-Bold` : base)
|
|
192
|
+
this.currentFace = resolveFloorplanPdfFace(family, weight)
|
|
69
193
|
return this
|
|
70
194
|
}
|
|
71
195
|
|
|
72
196
|
setFontSize(size: number): this {
|
|
73
197
|
this.currentFontSize = size
|
|
74
|
-
this.raw.fontSize(size)
|
|
75
198
|
return this
|
|
76
199
|
}
|
|
77
200
|
|
|
78
201
|
getTextWidth(value: string): number {
|
|
79
|
-
return this.
|
|
202
|
+
return this.measureRuns(this.textRuns(value, this.currentFace), this.currentFontSize)
|
|
80
203
|
}
|
|
81
204
|
|
|
82
205
|
splitTextToSize(value: string, maxWidth: number): string[] {
|
|
@@ -103,20 +226,58 @@ export class FloorplanPdfDocument {
|
|
|
103
226
|
baselineY: number,
|
|
104
227
|
options: PdfTextOptions = {},
|
|
105
228
|
) {
|
|
106
|
-
const
|
|
229
|
+
const given = typeof value === 'string' ? value.split('\n') : value
|
|
230
|
+
const lines =
|
|
231
|
+
options.maxWidth === undefined
|
|
232
|
+
? given
|
|
233
|
+
: given.flatMap((line) => this.splitTextToSize(line, options.maxWidth ?? 0))
|
|
107
234
|
const lineHeight = this.currentFontSize * 1.2
|
|
108
235
|
lines.forEach((line, index) => {
|
|
109
|
-
const
|
|
236
|
+
const runs = this.textRuns(line, this.currentFace)
|
|
237
|
+
const width = this.measureRuns(runs, this.currentFontSize)
|
|
110
238
|
const drawX =
|
|
111
239
|
options.align === 'center' ? x - width / 2 : options.align === 'right' ? x - width : x
|
|
112
|
-
this.
|
|
113
|
-
lineBreak: false,
|
|
114
|
-
width: options.maxWidth,
|
|
115
|
-
})
|
|
240
|
+
this.drawRuns(runs, drawX, baselineY + index * lineHeight, this.currentFontSize)
|
|
116
241
|
})
|
|
117
242
|
return this
|
|
118
243
|
}
|
|
119
244
|
|
|
245
|
+
/** The width `drawText` sets `text` at — the same runs, face by face. */
|
|
246
|
+
measureText(text: string, style: FloorplanPdfTextStyle): number {
|
|
247
|
+
const face = resolveFloorplanPdfFace(style.fontFamily, style.fontWeight)
|
|
248
|
+
return this.measureRuns(this.textRuns(collapseSvgWhitespace(text), face), style.fontSize)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Native (selectable, searchable) text in the current fill colour, anchored
|
|
253
|
+
* at (x, y) the way SVG anchors it: `anchor` is `text-anchor`, `baseline`
|
|
254
|
+
* is `dominant-baseline`, resolved against the embedded face's metrics.
|
|
255
|
+
*/
|
|
256
|
+
drawText(
|
|
257
|
+
text: string,
|
|
258
|
+
x: number,
|
|
259
|
+
y: number,
|
|
260
|
+
style: FloorplanPdfTextStyle,
|
|
261
|
+
placement: { anchor: 'start' | 'middle' | 'end'; baseline: FloorplanPdfBaseline },
|
|
262
|
+
): void {
|
|
263
|
+
const face = resolveFloorplanPdfFace(style.fontFamily, style.fontWeight)
|
|
264
|
+
const runs = this.textRuns(collapseSvgWhitespace(text), face)
|
|
265
|
+
const width = this.measureRuns(runs, style.fontSize)
|
|
266
|
+
const startX =
|
|
267
|
+
placement.anchor === 'middle' ? x - width / 2 : placement.anchor === 'end' ? x - width : x
|
|
268
|
+
const metrics = this.face(face)
|
|
269
|
+
const em = style.fontSize / 1000
|
|
270
|
+
const baselineShift =
|
|
271
|
+
placement.baseline === 'middle'
|
|
272
|
+
? (metrics.xHeight / 2) * em
|
|
273
|
+
: placement.baseline === 'central'
|
|
274
|
+
? ((metrics.ascender + metrics.descender) / 2) * em
|
|
275
|
+
: placement.baseline === 'hanging'
|
|
276
|
+
? 0.8 * metrics.ascender * em
|
|
277
|
+
: 0
|
|
278
|
+
this.drawRuns(runs, startX, y + baselineShift, style.fontSize)
|
|
279
|
+
}
|
|
280
|
+
|
|
120
281
|
line(x1: number, y1: number, x2: number, y2: number): this {
|
|
121
282
|
this.raw.moveTo(x1, y1).lineTo(x2, y2).stroke()
|
|
122
283
|
return this
|
|
@@ -165,17 +326,160 @@ export class FloorplanPdfDocument {
|
|
|
165
326
|
else this.raw.stroke()
|
|
166
327
|
return this
|
|
167
328
|
}
|
|
329
|
+
|
|
330
|
+
private setFace(face: FloorplanPdfFace) {
|
|
331
|
+
return this.raw.font(this.embedded ? face : STANDARD_FACES[face])
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private face(face: FloorplanPdfFace): EmbeddedFace {
|
|
335
|
+
let embedded = this.faces.get(face)
|
|
336
|
+
if (!embedded) {
|
|
337
|
+
this.setFace(face)
|
|
338
|
+
embedded = (this.raw as unknown as { _font: EmbeddedFace })._font
|
|
339
|
+
this.faces.set(face, embedded)
|
|
340
|
+
}
|
|
341
|
+
return embedded
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Splits `text` into runs each face can set, plus the symbols drawn as linework. */
|
|
345
|
+
private textRuns(text: string, face: FloorplanPdfFace): TextRun[] {
|
|
346
|
+
// printable ASCII is in every face; the standard fonts have no glyph table to consult
|
|
347
|
+
if (!this.embedded || /^[\x20-\x7e]*$/.test(text)) return [{ kind: 'text', face, text }]
|
|
348
|
+
const runs: TextRun[] = []
|
|
349
|
+
for (const char of text) {
|
|
350
|
+
const codePoint = char.codePointAt(0) ?? 0
|
|
351
|
+
let runFace = face
|
|
352
|
+
if (!this.face(face).font.hasGlyphForCodePoint(codePoint)) {
|
|
353
|
+
const drawn = DRAWN_SYMBOLS[char as keyof typeof DRAWN_SYMBOLS]
|
|
354
|
+
if (this.face(OTHER_FAMILY[face]).font.hasGlyphForCodePoint(codePoint)) {
|
|
355
|
+
runFace = OTHER_FAMILY[face]
|
|
356
|
+
} else if (drawn) {
|
|
357
|
+
runs.push({ kind: 'symbol', symbol: drawn.symbol, advanceEm: drawn.advanceEm })
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
// anything else prints as the face's .notdef box — visibly missing, never garbage
|
|
361
|
+
}
|
|
362
|
+
const last = runs[runs.length - 1]
|
|
363
|
+
if (last?.kind === 'text' && last.face === runFace) last.text += char
|
|
364
|
+
else runs.push({ kind: 'text', face: runFace, text: char })
|
|
365
|
+
}
|
|
366
|
+
return runs
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private measureRuns(runs: readonly TextRun[], fontSize: number): number {
|
|
370
|
+
let width = 0
|
|
371
|
+
for (const run of runs) {
|
|
372
|
+
width +=
|
|
373
|
+
run.kind === 'text'
|
|
374
|
+
? this.setFace(run.face).fontSize(fontSize).widthOfString(run.text)
|
|
375
|
+
: run.advanceEm * fontSize
|
|
376
|
+
}
|
|
377
|
+
return width
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private drawRuns(runs: readonly TextRun[], x: number, baselineY: number, fontSize: number) {
|
|
381
|
+
let cursor = x
|
|
382
|
+
for (const run of runs) {
|
|
383
|
+
if (run.kind === 'text') {
|
|
384
|
+
this.setFace(run.face).fontSize(fontSize)
|
|
385
|
+
this.raw.text(run.text, cursor, baselineY, { baseline: 'alphabetic', lineBreak: false })
|
|
386
|
+
cursor += this.raw.widthOfString(run.text)
|
|
387
|
+
} else {
|
|
388
|
+
this.drawSymbol(run.symbol, cursor, baselineY, fontSize)
|
|
389
|
+
cursor += run.advanceEm * fontSize
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private drawSymbol(symbol: DrawnSymbol, x: number, baselineY: number, fontSize: number) {
|
|
395
|
+
const raw = this.raw
|
|
396
|
+
if (symbol === 'angle') {
|
|
397
|
+
// two strokes from a vertex on the baseline: the base, and a leg rising
|
|
398
|
+
// to the cap height — filled bands, so the text's own colour paints them
|
|
399
|
+
const vertex: [number, number] = [x + 0.08 * fontSize, baselineY]
|
|
400
|
+
const tip: [number, number] = [x + 0.56 * fontSize, baselineY - 0.7 * fontSize]
|
|
401
|
+
const width = 0.07 * fontSize
|
|
402
|
+
raw
|
|
403
|
+
.save()
|
|
404
|
+
// the base starts a little behind the vertex so the corner is closed
|
|
405
|
+
.polygon(
|
|
406
|
+
...strokeBand([x + 0.05 * fontSize, baselineY], [x + 0.72 * fontSize, baselineY], width),
|
|
407
|
+
)
|
|
408
|
+
.polygon(...strokeBand(vertex, tip, width))
|
|
409
|
+
.fill('non-zero')
|
|
410
|
+
.restore()
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
if (symbol === 'pointer') {
|
|
414
|
+
// a small solid triangle on the x-height, like a list pointer
|
|
415
|
+
const midY = baselineY - 0.26 * fontSize
|
|
416
|
+
const half = 0.17 * fontSize
|
|
417
|
+
raw
|
|
418
|
+
.save()
|
|
419
|
+
.polygon(
|
|
420
|
+
[x + 0.1 * fontSize, midY - half],
|
|
421
|
+
[x + 0.4 * fontSize, midY],
|
|
422
|
+
[x + 0.1 * fontSize, midY + half],
|
|
423
|
+
)
|
|
424
|
+
.fill('non-zero')
|
|
425
|
+
.restore()
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
// warning: an outlined triangle from just under the baseline to the cap
|
|
429
|
+
// height, its "!" standing on the text's own baseline (so text extraction
|
|
430
|
+
// keeps it on the line it warns about)
|
|
431
|
+
const left = x + 0.04 * fontSize
|
|
432
|
+
const width = 0.82 * fontSize
|
|
433
|
+
const base = baselineY + 0.1 * fontSize
|
|
434
|
+
const height = 0.82 * fontSize
|
|
435
|
+
const stroke = 0.07 * fontSize
|
|
436
|
+
const apex: [number, number] = [left + width / 2, base - height]
|
|
437
|
+
const outer: [number, number][] = [[left, base], [left + width, base], apex]
|
|
438
|
+
const leg = Math.hypot(width / 2, height)
|
|
439
|
+
const inradius = (width * height) / (width + 2 * leg)
|
|
440
|
+
const centre: [number, number] = [left + width / 2, base - inradius]
|
|
441
|
+
const k = (inradius - stroke) / inradius
|
|
442
|
+
const inner = outer.map(
|
|
443
|
+
([px, py]) =>
|
|
444
|
+
[centre[0] + (px - centre[0]) * k, centre[1] + (py - centre[1]) * k] as [number, number],
|
|
445
|
+
)
|
|
446
|
+
raw
|
|
447
|
+
.save()
|
|
448
|
+
.polygon(...outer)
|
|
449
|
+
.polygon(...inner)
|
|
450
|
+
.fill('even-odd')
|
|
451
|
+
.restore()
|
|
452
|
+
this.setFace('sans-bold').fontSize(0.55 * fontSize)
|
|
453
|
+
raw.text('!', centre[0] - raw.widthOfString('!') / 2, baselineY, {
|
|
454
|
+
baseline: 'alphabetic',
|
|
455
|
+
lineBreak: false,
|
|
456
|
+
})
|
|
457
|
+
}
|
|
168
458
|
}
|
|
169
459
|
|
|
170
|
-
export async function createFloorplanPdfDocument(
|
|
171
|
-
|
|
460
|
+
export async function createFloorplanPdfDocument(
|
|
461
|
+
defaultPageSize: readonly [number, number],
|
|
462
|
+
options: { title?: string } = {},
|
|
463
|
+
) {
|
|
464
|
+
const [{ default: PDFDocument }, { default: blobStream }, fonts] = await Promise.all([
|
|
172
465
|
import('pdfkit/js/pdfkit.standalone'),
|
|
173
466
|
import('blob-stream'),
|
|
467
|
+
// an export in the standard fonts beats no export when the font files are unreachable
|
|
468
|
+
loadFloorplanPdfFonts().catch((error: unknown) => {
|
|
469
|
+
console.warn('[floorplan-export] Falling back to the standard PDF fonts', error)
|
|
470
|
+
return null
|
|
471
|
+
}),
|
|
174
472
|
])
|
|
175
|
-
const raw = new PDFDocument({
|
|
473
|
+
const raw = new PDFDocument({
|
|
474
|
+
autoFirstPage: false,
|
|
475
|
+
compress: true,
|
|
476
|
+
margin: 0,
|
|
477
|
+
// the viewer's title bar shows the document title, not the file name
|
|
478
|
+
...(options.title ? { info: { Title: options.title }, displayTitle: true } : {}),
|
|
479
|
+
})
|
|
176
480
|
const stream = raw.pipe(blobStream())
|
|
177
481
|
return {
|
|
178
|
-
doc: new FloorplanPdfDocument(raw, defaultPageSize),
|
|
482
|
+
doc: new FloorplanPdfDocument(raw, defaultPageSize, fonts),
|
|
179
483
|
save: async (filename: string) => {
|
|
180
484
|
const blob = await new Promise<Blob>((resolve, reject) => {
|
|
181
485
|
stream.on('finish', () => resolve(stream.toBlob('application/pdf')))
|
|
@@ -192,3 +496,20 @@ export async function createFloorplanPdfDocument(defaultPageSize: readonly [numb
|
|
|
192
496
|
},
|
|
193
497
|
}
|
|
194
498
|
}
|
|
499
|
+
|
|
500
|
+
/** The quad a straight stroke of `width` from `a` to `b` covers (same winding for any direction). */
|
|
501
|
+
function strokeBand(
|
|
502
|
+
a: readonly [number, number],
|
|
503
|
+
b: readonly [number, number],
|
|
504
|
+
width: number,
|
|
505
|
+
): [number, number][] {
|
|
506
|
+
const length = Math.hypot(b[0] - a[0], b[1] - a[1])
|
|
507
|
+
const nx = (-(b[1] - a[1]) / length) * (width / 2)
|
|
508
|
+
const ny = ((b[0] - a[0]) / length) * (width / 2)
|
|
509
|
+
return [
|
|
510
|
+
[a[0] + nx, a[1] + ny],
|
|
511
|
+
[b[0] + nx, b[1] + ny],
|
|
512
|
+
[b[0] - nx, b[1] - ny],
|
|
513
|
+
[a[0] - nx, a[1] - ny],
|
|
514
|
+
]
|
|
515
|
+
}
|