@pascal-app/editor 1.0.0-beta.5 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +5 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +55 -51
- package/src/components/editor/floorplan-panel.tsx +99 -15
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +78 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +23 -3
- package/src/components/editor/index.tsx +151 -24
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +67 -119
- package/src/components/editor/selection-manager.tsx +153 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -239
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +226 -35
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +304 -171
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +10 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +116 -44
- package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +56 -17
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +63 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +44 -21
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +103 -10
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +266 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +35 -164
- package/src/components/ui/panels/reference-panel.tsx +34 -4
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +502 -31
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -7
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/viewer-scene-header.tsx +5 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +144 -0
- package/src/hooks/use-ceiling-events.ts +5 -1
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +132 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +87 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +536 -2
- package/src/lib/floorplan/floorplan-export.tsx +120 -19
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +5 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +1375 -21
- package/src/lib/glb-export.ts +781 -106
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +455 -0
- package/src/lib/portable-export.ts +1042 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +379 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +200 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +38 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +65 -0
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-send-to-app.ts +21 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
DoorNode,
|
|
6
|
+
nodeRegistry,
|
|
7
|
+
registerNode,
|
|
8
|
+
sceneRegistry,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
import * as THREE from 'three'
|
|
11
|
+
import type { ExportTextureUtils } from './export-texture-utils'
|
|
12
|
+
import {
|
|
13
|
+
exportSceneToGlb,
|
|
14
|
+
type GlbExport,
|
|
15
|
+
serializePreparedSceneToGlb,
|
|
16
|
+
withExportDeadline,
|
|
17
|
+
} from './glb-export'
|
|
18
|
+
|
|
19
|
+
// GLTFExporter's binary path runs through document.createElement('canvas'),
|
|
20
|
+
// canvas.toBlob and three FileReader reads (image bufferView, merged buffer,
|
|
21
|
+
// final GLB). Bun has none of those, so a minimal stand-in drives the exporter
|
|
22
|
+
// the way a browser does — including the failure the browser reports only via
|
|
23
|
+
// `result === null` inside `onloadend`.
|
|
24
|
+
type Globals = Record<string, unknown>
|
|
25
|
+
const globals = globalThis as unknown as Globals
|
|
26
|
+
const DOM_KEYS = ['document', 'FileReader', 'ImageData', 'HTMLCanvasElement'] as const
|
|
27
|
+
const previous = new Map<string, unknown>()
|
|
28
|
+
|
|
29
|
+
class FakeImageData {
|
|
30
|
+
constructor(
|
|
31
|
+
public data: Uint8ClampedArray,
|
|
32
|
+
public width: number,
|
|
33
|
+
public height: number,
|
|
34
|
+
) {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class FakeCanvas {
|
|
38
|
+
width = 0
|
|
39
|
+
height = 0
|
|
40
|
+
private pixels: Uint8ClampedArray | null = null
|
|
41
|
+
|
|
42
|
+
getContext() {
|
|
43
|
+
return {
|
|
44
|
+
translate: () => {},
|
|
45
|
+
scale: () => {},
|
|
46
|
+
drawImage: () => {},
|
|
47
|
+
fillRect: () => {},
|
|
48
|
+
createImageData: (width: number, height: number) =>
|
|
49
|
+
new FakeImageData(new Uint8ClampedArray(width * height * 4), width, height),
|
|
50
|
+
putImageData: (image: FakeImageData) => {
|
|
51
|
+
this.pixels = new Uint8ClampedArray(image.data)
|
|
52
|
+
},
|
|
53
|
+
getImageData: (_x: number, _y: number, width: number, height: number) =>
|
|
54
|
+
new FakeImageData(this.pixels ?? new Uint8ClampedArray(width * height * 4), width, height),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
toBlob(callback: (blob: Blob) => void, mimeType: string) {
|
|
59
|
+
callback(new Blob([new Uint8Array([137, 80, 78, 71])], { type: mimeType }))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type FileReaderBehaviour = {
|
|
64
|
+
reads: number
|
|
65
|
+
/** 1-based index of the read whose result is `null`, as after a failed read. */
|
|
66
|
+
failAt: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function installExporterDom(behaviour: FileReaderBehaviour) {
|
|
70
|
+
for (const key of DOM_KEYS) previous.set(key, globals[key])
|
|
71
|
+
|
|
72
|
+
class FakeFileReader {
|
|
73
|
+
result: ArrayBuffer | null = null
|
|
74
|
+
onloadend: (() => void) | null = null
|
|
75
|
+
readAsArrayBuffer(blob: Blob) {
|
|
76
|
+
behaviour.reads += 1
|
|
77
|
+
const read = behaviour.reads
|
|
78
|
+
void blob.arrayBuffer().then((buffer) => {
|
|
79
|
+
this.result = read === behaviour.failAt ? null : buffer
|
|
80
|
+
// Browsers report an exception thrown inside the event handler to
|
|
81
|
+
// window.onerror; it never reaches the exporter's promise chain.
|
|
82
|
+
try {
|
|
83
|
+
this.onloadend?.()
|
|
84
|
+
} catch {}
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
globals.ImageData = FakeImageData
|
|
90
|
+
globals.HTMLCanvasElement = FakeCanvas
|
|
91
|
+
globals.FileReader = FakeFileReader
|
|
92
|
+
globals.document = {
|
|
93
|
+
createElement: (tagName: string) => {
|
|
94
|
+
if (tagName !== 'canvas') throw new Error(`Unexpected element request: ${tagName}`)
|
|
95
|
+
return new FakeCanvas()
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
for (const key of DOM_KEYS) globals[key] = previous.get(key)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
function texturedRoot(texture: THREE.Texture): THREE.Group {
|
|
105
|
+
const root = new THREE.Group()
|
|
106
|
+
root.add(
|
|
107
|
+
new THREE.Mesh(
|
|
108
|
+
new THREE.BoxGeometry(1, 1, 1),
|
|
109
|
+
new THREE.MeshStandardMaterial({ map: texture }),
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
return root
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function texturedScene(texture: THREE.Texture): GlbExport {
|
|
116
|
+
return { scene: texturedRoot(texture), animations: [], warnings: [], dispose: () => {} }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readableTexture(): THREE.DataTexture {
|
|
120
|
+
const texture = new THREE.DataTexture(new Uint8Array([255, 0, 0, 255]), 1, 1)
|
|
121
|
+
texture.needsUpdate = true
|
|
122
|
+
return texture
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function passthroughUtils(): ExportTextureUtils & { calls: THREE.Texture[] } {
|
|
126
|
+
const calls: THREE.Texture[] = []
|
|
127
|
+
return {
|
|
128
|
+
calls,
|
|
129
|
+
decompress: async (texture) => {
|
|
130
|
+
calls.push(texture)
|
|
131
|
+
return readableTexture()
|
|
132
|
+
},
|
|
133
|
+
dispose: async () => {},
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const GLB_MAGIC = 0x46546c67
|
|
138
|
+
|
|
139
|
+
describe('serializePreparedSceneToGlb', () => {
|
|
140
|
+
test('resolves a binary glTF and routes compressed textures through the shared decompressor', async () => {
|
|
141
|
+
installExporterDom({ reads: 0, failAt: Number.POSITIVE_INFINITY })
|
|
142
|
+
const compressed = new THREE.CompressedTexture([], 2, 2)
|
|
143
|
+
compressed.name = 'Color_ktx2'
|
|
144
|
+
const textureUtils = passthroughUtils()
|
|
145
|
+
|
|
146
|
+
const glb = await serializePreparedSceneToGlb(texturedScene(compressed), { textureUtils })
|
|
147
|
+
|
|
148
|
+
expect(glb).toBeInstanceOf(ArrayBuffer)
|
|
149
|
+
expect(new DataView(glb).getUint32(0, true)).toBe(GLB_MAGIC)
|
|
150
|
+
expect(new DataView(glb).getUint32(8, true)).toBe(glb.byteLength)
|
|
151
|
+
expect(textureUtils.calls.map((texture) => texture.name)).toEqual(['Color_ktx2'])
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('rejects instead of resolving null when the final GLB read fails', async () => {
|
|
155
|
+
installExporterDom({ reads: 0, failAt: 3 })
|
|
156
|
+
|
|
157
|
+
await expect(
|
|
158
|
+
serializePreparedSceneToGlb(texturedScene(readableTexture()), {
|
|
159
|
+
textureUtils: passthroughUtils(),
|
|
160
|
+
}),
|
|
161
|
+
).rejects.toThrow('GLB export produced no data')
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('withExportDeadline', () => {
|
|
166
|
+
test('passes a settled value through and clears its timer', async () => {
|
|
167
|
+
await expect(withExportDeadline(Promise.resolve(7), 1_000, 'GLB')).resolves.toBe(7)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('names the format and the wait in the timeout error', async () => {
|
|
171
|
+
const pending = new Promise<never>(() => {})
|
|
172
|
+
await expect(withExportDeadline(pending, 10, 'USDZ')).rejects.toThrow(
|
|
173
|
+
/USDZ export timed out after \d+ s/,
|
|
174
|
+
)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('an infinite deadline returns the original promise', () => {
|
|
178
|
+
const pending = new Promise<never>(() => {})
|
|
179
|
+
expect(withExportDeadline(pending, Number.POSITIVE_INFINITY, 'GLB')).toBe(pending)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('exportSceneToGlb', () => {
|
|
184
|
+
afterEach(() => {
|
|
185
|
+
sceneRegistry.clear()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('rejects at the deadline when a FileReader inside GLTFExporter fails silently', async () => {
|
|
189
|
+
// The image bufferView read is the first one; a null result throws inside
|
|
190
|
+
// onloadend, so GLTFExporter's pending list never drains.
|
|
191
|
+
installExporterDom({ reads: 0, failAt: 1 })
|
|
192
|
+
|
|
193
|
+
await expect(
|
|
194
|
+
exportSceneToGlb(
|
|
195
|
+
texturedRoot(readableTexture()),
|
|
196
|
+
{},
|
|
197
|
+
{
|
|
198
|
+
textureUtils: passthroughUtils(),
|
|
199
|
+
timeoutMs: 50,
|
|
200
|
+
},
|
|
201
|
+
),
|
|
202
|
+
).rejects.toThrow('GLB export timed out')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('exports a live scene through a caller-owned decompressor without disposing it', async () => {
|
|
206
|
+
installExporterDom({ reads: 0, failAt: Number.POSITIVE_INFINITY })
|
|
207
|
+
const root = new THREE.Group()
|
|
208
|
+
root.add(
|
|
209
|
+
new THREE.Mesh(
|
|
210
|
+
new THREE.BoxGeometry(1, 1, 1),
|
|
211
|
+
new THREE.MeshStandardMaterial({ map: readableTexture() }),
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
const textureUtils = passthroughUtils()
|
|
215
|
+
let disposals = 0
|
|
216
|
+
textureUtils.dispose = async () => {
|
|
217
|
+
disposals += 1
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const glb = await exportSceneToGlb(root, {}, { textureUtils, timeoutMs: 5_000 })
|
|
221
|
+
|
|
222
|
+
expect(new DataView(glb).getUint32(0, true)).toBe(GLB_MAGIC)
|
|
223
|
+
expect(disposals).toBe(0)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('rejects at the deadline when a plugin bake hook never settles', async () => {
|
|
227
|
+
installExporterDom({ reads: 0, failAt: Number.POSITIVE_INFINITY })
|
|
228
|
+
const restoreRegistry = nodeRegistry._snapshot()
|
|
229
|
+
try {
|
|
230
|
+
const kind = 'test:bake-never-settles'
|
|
231
|
+
registerNode({
|
|
232
|
+
kind,
|
|
233
|
+
schemaVersion: 1,
|
|
234
|
+
schema: DoorNode,
|
|
235
|
+
category: 'furnish',
|
|
236
|
+
defaults: () => ({}) as never,
|
|
237
|
+
capabilities: {},
|
|
238
|
+
bake: 'replace',
|
|
239
|
+
bakeGeometryAsync: () => new Promise<THREE.Object3D>(() => {}),
|
|
240
|
+
} as AnyNodeDefinition)
|
|
241
|
+
const source = new THREE.Group()
|
|
242
|
+
source.add(new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshStandardMaterial()))
|
|
243
|
+
const root = new THREE.Group()
|
|
244
|
+
root.add(source)
|
|
245
|
+
sceneRegistry.nodes.set('stuck_1', source)
|
|
246
|
+
const nodes = {
|
|
247
|
+
stuck_1: { id: 'stuck_1', type: kind, visible: true },
|
|
248
|
+
} as unknown as Record<string, AnyNode>
|
|
249
|
+
|
|
250
|
+
await expect(
|
|
251
|
+
exportSceneToGlb(root, nodes, { textureUtils: passthroughUtils(), timeoutMs: 50 }),
|
|
252
|
+
).rejects.toThrow('GLB export timed out')
|
|
253
|
+
} finally {
|
|
254
|
+
restoreRegistry()
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
test('preparation and serialisation share one decompressor', async () => {
|
|
259
|
+
installExporterDom({ reads: 0, failAt: Number.POSITIVE_INFINITY })
|
|
260
|
+
const map = new THREE.CompressedTexture([], 2, 2)
|
|
261
|
+
map.name = 'Color_ktx2'
|
|
262
|
+
const normalMap = new THREE.CompressedTexture([], 2, 2)
|
|
263
|
+
normalMap.name = 'NormalGL_ktx2'
|
|
264
|
+
const material = new THREE.MeshStandardMaterial({ map, normalMap })
|
|
265
|
+
// A non-unit normalScale forces the preparation pass to bake (and so
|
|
266
|
+
// decompress) the normal map before the exporter ever sees it.
|
|
267
|
+
material.normalScale.set(0.5, 0.5)
|
|
268
|
+
const root = new THREE.Group()
|
|
269
|
+
root.add(new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), material))
|
|
270
|
+
const textureUtils = passthroughUtils()
|
|
271
|
+
|
|
272
|
+
const glb = await exportSceneToGlb(root, {}, { textureUtils, timeoutMs: 5_000 })
|
|
273
|
+
|
|
274
|
+
expect(new DataView(glb).getUint32(0, true)).toBe(GLB_MAGIC)
|
|
275
|
+
expect(textureUtils.calls.map((texture) => texture.name).sort()).toEqual([
|
|
276
|
+
'Color_ktx2',
|
|
277
|
+
'NormalGL_ktx2',
|
|
278
|
+
])
|
|
279
|
+
})
|
|
280
|
+
})
|