@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,455 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { strFromU8, unzipSync } from 'fflate'
|
|
3
|
+
import {
|
|
4
|
+
Box3,
|
|
5
|
+
BoxGeometry,
|
|
6
|
+
BufferGeometry,
|
|
7
|
+
Color,
|
|
8
|
+
CompressedTexture,
|
|
9
|
+
DataTexture,
|
|
10
|
+
DoubleSide,
|
|
11
|
+
Float32BufferAttribute,
|
|
12
|
+
FrontSide,
|
|
13
|
+
Group,
|
|
14
|
+
InstancedBufferAttribute,
|
|
15
|
+
InstancedMesh,
|
|
16
|
+
Matrix4,
|
|
17
|
+
Mesh,
|
|
18
|
+
MeshStandardMaterial,
|
|
19
|
+
Quaternion,
|
|
20
|
+
Texture,
|
|
21
|
+
Vector3,
|
|
22
|
+
} from 'three'
|
|
23
|
+
import { USDZExporter } from 'three/examples/jsm/exporters/USDZExporter.js'
|
|
24
|
+
import {
|
|
25
|
+
createUsdzScene,
|
|
26
|
+
decompressCanonicalNormalMaps,
|
|
27
|
+
expandInstancedMeshes,
|
|
28
|
+
freezeDeformedMeshes,
|
|
29
|
+
normalizePortableScene,
|
|
30
|
+
} from './portable-export'
|
|
31
|
+
|
|
32
|
+
describe('portable vertex-color boundary', () => {
|
|
33
|
+
test('clips only over-bright channels while preserving ordinary linear colors', () => {
|
|
34
|
+
withCanvasCapture((capture) => {
|
|
35
|
+
const colors = new Float32Array([0.25, 0.5, 0.75, 1, 1.5, 0.4, 0.2, 0.5, 0.1, 0.2, 0.3, 0])
|
|
36
|
+
const sourceValues = Array.from(colors)
|
|
37
|
+
const geometry = triangleGeometry(colors)
|
|
38
|
+
const material = new MeshStandardMaterial({ vertexColors: true })
|
|
39
|
+
const mesh = new Mesh(geometry, material)
|
|
40
|
+
const root = new Group()
|
|
41
|
+
root.add(mesh)
|
|
42
|
+
|
|
43
|
+
const warnings = normalizePortableScene(root)
|
|
44
|
+
|
|
45
|
+
expect(warnings).toHaveLength(1)
|
|
46
|
+
expect(warnings[0]).toMatch(/color.*clip/i)
|
|
47
|
+
expect(Array.from(colors)).toEqual(sourceValues)
|
|
48
|
+
expect(portableColorAt(mesh, 0, capture)).toEqual([
|
|
49
|
+
encodeSrgbByte(0.25),
|
|
50
|
+
encodeSrgbByte(0.5),
|
|
51
|
+
encodeSrgbByte(0.75),
|
|
52
|
+
255,
|
|
53
|
+
])
|
|
54
|
+
expect(portableColorAt(mesh, 1, capture)).toEqual([
|
|
55
|
+
255,
|
|
56
|
+
encodeSrgbByte(0.4),
|
|
57
|
+
encodeSrgbByte(0.2),
|
|
58
|
+
128,
|
|
59
|
+
])
|
|
60
|
+
expect(portableColorAt(mesh, 2, capture)).toEqual([
|
|
61
|
+
encodeSrgbByte(0.1),
|
|
62
|
+
encodeSrgbByte(0.2),
|
|
63
|
+
encodeSrgbByte(0.3),
|
|
64
|
+
0,
|
|
65
|
+
])
|
|
66
|
+
const portableMaterial = mesh.material as MeshStandardMaterial
|
|
67
|
+
expect(portableMaterial.emissive.getHex()).toBe(0)
|
|
68
|
+
expect(portableMaterial.emissiveMap).toBeNull()
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('does not warn when every channel is already portable', () => {
|
|
73
|
+
withCanvasCapture(() => {
|
|
74
|
+
const geometry = triangleGeometry(
|
|
75
|
+
new Float32Array([0, 0.25, 0.5, 1, 0.75, 1, 0.125, 1, 0.3, 0.4, 0.6, 1]),
|
|
76
|
+
)
|
|
77
|
+
const mesh = new Mesh(geometry, new MeshStandardMaterial({ vertexColors: true }))
|
|
78
|
+
const root = new Group()
|
|
79
|
+
root.add(mesh)
|
|
80
|
+
|
|
81
|
+
expect(normalizePortableScene(root)).toEqual([])
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('portable geometry normalization', () => {
|
|
87
|
+
test('preserves two transformed instance populations without changing source data', () => {
|
|
88
|
+
const sourceMaterial = new MeshStandardMaterial({ color: '#808080' })
|
|
89
|
+
const sourceGeometry = new BoxGeometry(1, 2, 3)
|
|
90
|
+
const shaderInstancePayload = new InstancedBufferAttribute(new Float32Array(32), 16)
|
|
91
|
+
sourceGeometry.setAttribute('shaderInstancePayload', shaderInstancePayload)
|
|
92
|
+
const instances = new InstancedMesh(sourceGeometry, sourceMaterial, 2)
|
|
93
|
+
instances.name = 'asymmetric-instance'
|
|
94
|
+
instances.position.set(10, 0, -2)
|
|
95
|
+
const firstMatrix = new Matrix4().compose(
|
|
96
|
+
new Vector3(2, 0, 0),
|
|
97
|
+
new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI / 2),
|
|
98
|
+
new Vector3(1, 2, 1),
|
|
99
|
+
)
|
|
100
|
+
const reflectedMatrix = new Matrix4().compose(
|
|
101
|
+
new Vector3(-3, 1, 4),
|
|
102
|
+
new Quaternion(),
|
|
103
|
+
new Vector3(-2, 1, 0.5),
|
|
104
|
+
)
|
|
105
|
+
instances.setMatrixAt(0, firstMatrix)
|
|
106
|
+
instances.setMatrixAt(1, reflectedMatrix)
|
|
107
|
+
instances.setColorAt(0, new Color('#ff0000'))
|
|
108
|
+
instances.setColorAt(1, new Color('#0000ff'))
|
|
109
|
+
const root = new Group()
|
|
110
|
+
root.add(instances)
|
|
111
|
+
|
|
112
|
+
expandInstancedMeshes(root)
|
|
113
|
+
root.updateMatrixWorld(true)
|
|
114
|
+
|
|
115
|
+
const firstPopulation = new Box3().makeEmpty()
|
|
116
|
+
const reflectedPopulation = new Box3().makeEmpty()
|
|
117
|
+
let triangleCount = 0
|
|
118
|
+
let firstOutputTint: Color | null = null
|
|
119
|
+
let reflectedOutputTint: Color | null = null
|
|
120
|
+
root.traverse((object) => {
|
|
121
|
+
const mesh = object as Mesh
|
|
122
|
+
if (!mesh.isMesh) return
|
|
123
|
+
const positions = mesh.geometry.getAttribute('position')
|
|
124
|
+
triangleCount += (mesh.geometry.getIndex()?.count ?? positions.count) / 3
|
|
125
|
+
for (const attribute of Object.values(mesh.geometry.attributes)) {
|
|
126
|
+
expect((attribute as InstancedBufferAttribute).isInstancedBufferAttribute).not.toBe(true)
|
|
127
|
+
expect(attribute.count).toBe(positions.count)
|
|
128
|
+
}
|
|
129
|
+
for (let index = 0; index < positions.count; index += 1) {
|
|
130
|
+
const world = new Vector3()
|
|
131
|
+
.fromBufferAttribute(positions, index)
|
|
132
|
+
.applyMatrix4(mesh.matrixWorld)
|
|
133
|
+
if (world.x < 9) {
|
|
134
|
+
reflectedPopulation.expandByPoint(world)
|
|
135
|
+
reflectedOutputTint ??= portableLinearTintAt(mesh, index)
|
|
136
|
+
} else {
|
|
137
|
+
firstPopulation.expandByPoint(world)
|
|
138
|
+
firstOutputTint ??= portableLinearTintAt(mesh, index)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
expect(triangleCount).toBe(24)
|
|
143
|
+
expectVector(firstPopulation.getCenter(new Vector3()), new Vector3(12, 0, -2))
|
|
144
|
+
expectVector(firstPopulation.getSize(new Vector3()), new Vector3(3, 4, 1))
|
|
145
|
+
expectVector(reflectedPopulation.getCenter(new Vector3()), new Vector3(7, 1, 2))
|
|
146
|
+
expectVector(reflectedPopulation.getSize(new Vector3()), new Vector3(2, 2, 1.5))
|
|
147
|
+
expect(firstOutputTint).not.toBeNull()
|
|
148
|
+
expect(reflectedOutputTint).not.toBeNull()
|
|
149
|
+
expectColor(firstOutputTint!, sourceMaterial.color.clone().multiply(new Color('#ff0000')))
|
|
150
|
+
expectColor(reflectedOutputTint!, sourceMaterial.color.clone().multiply(new Color('#0000ff')))
|
|
151
|
+
const retainedMatrix = new Matrix4()
|
|
152
|
+
instances.getMatrixAt(1, retainedMatrix)
|
|
153
|
+
expect(retainedMatrix.equals(reflectedMatrix)).toBe(true)
|
|
154
|
+
const retainedColor = new Color()
|
|
155
|
+
expect(instances.geometry.getAttribute('shaderInstancePayload')).toBe(shaderInstancePayload)
|
|
156
|
+
instances.getColorAt(1, retainedColor)
|
|
157
|
+
expect(retainedColor.equals(new Color('#0000ff'))).toBe(true)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('freezes a settled morph pose into static vertex positions without mutating its source', () => {
|
|
161
|
+
const geometry = new BufferGeometry()
|
|
162
|
+
const sourcePositions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0])
|
|
163
|
+
const morphDelta = new Float32Array([0, 0, 0, 0, 0, 0, 0, 2, 0])
|
|
164
|
+
geometry.setAttribute('position', new Float32BufferAttribute(sourcePositions, 3))
|
|
165
|
+
geometry.morphAttributes.position = [new Float32BufferAttribute(morphDelta, 3)]
|
|
166
|
+
geometry.morphTargetsRelative = true
|
|
167
|
+
const mesh = new Mesh(geometry, new MeshStandardMaterial())
|
|
168
|
+
mesh.updateMorphTargets()
|
|
169
|
+
mesh.morphTargetInfluences![0] = 0.25
|
|
170
|
+
const root = new Group()
|
|
171
|
+
root.add(mesh)
|
|
172
|
+
|
|
173
|
+
freezeDeformedMeshes(root)
|
|
174
|
+
|
|
175
|
+
const frozen = mesh.geometry.getAttribute('position')
|
|
176
|
+
expect([frozen.getX(2), frozen.getY(2), frozen.getZ(2)]).toEqual([0, 1.5, 0])
|
|
177
|
+
expect(mesh.geometry.morphAttributes.position).toBeUndefined()
|
|
178
|
+
expect(mesh.morphTargetInfluences).toBeUndefined()
|
|
179
|
+
expect(Array.from(sourcePositions)).toEqual([0, 0, 0, 1, 0, 0, 0, 1, 0])
|
|
180
|
+
expect(Array.from(morphDelta)).toEqual([0, 0, 0, 0, 0, 0, 0, 2, 0])
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('bakes reflected USDZ transforms with outward-consistent winding and normals', () => {
|
|
184
|
+
const geometry = new BufferGeometry()
|
|
185
|
+
geometry.setAttribute('position', new Float32BufferAttribute([0, 0, 0, 1, 0, 0, 0, 1, 1], 3))
|
|
186
|
+
geometry.computeVertexNormals()
|
|
187
|
+
const source = new Mesh(geometry, new MeshStandardMaterial())
|
|
188
|
+
source.name = 'reflected-triangle'
|
|
189
|
+
source.position.set(2, -1, 3)
|
|
190
|
+
source.scale.set(-2, 1, 0.5)
|
|
191
|
+
const root = new Group()
|
|
192
|
+
root.add(source)
|
|
193
|
+
|
|
194
|
+
const portable = createUsdzScene(root)
|
|
195
|
+
const reflected = portable.getObjectByName('reflected-triangle') as Mesh
|
|
196
|
+
const positions = reflected.geometry.getAttribute('position')
|
|
197
|
+
const normals = reflected.geometry.getAttribute('normal')
|
|
198
|
+
const first = new Vector3().fromBufferAttribute(positions, 0)
|
|
199
|
+
const second = new Vector3().fromBufferAttribute(positions, 1)
|
|
200
|
+
const third = new Vector3().fromBufferAttribute(positions, 2)
|
|
201
|
+
const faceNormal = second.clone().sub(first).cross(third.clone().sub(first)).normalize()
|
|
202
|
+
const vertexNormal = new Vector3().fromBufferAttribute(normals, 0).normalize()
|
|
203
|
+
|
|
204
|
+
expect(faceNormal.dot(vertexNormal)).toBeGreaterThan(0.999)
|
|
205
|
+
expectVector(reflected.position, new Vector3())
|
|
206
|
+
expectVector(reflected.scale, new Vector3(1, 1, 1))
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('emits every referenced geometry member for a one-element material array', async () => {
|
|
210
|
+
const root = new Group()
|
|
211
|
+
const mesh = new Mesh(new BoxGeometry(1, 1, 1), [new MeshStandardMaterial()])
|
|
212
|
+
root.add(mesh)
|
|
213
|
+
|
|
214
|
+
const archive = await new USDZExporter().parseAsync(createUsdzScene(root))
|
|
215
|
+
const files = unzipSync(archive)
|
|
216
|
+
const modelFile = files['model.usda']
|
|
217
|
+
expect(modelFile).toBeDefined()
|
|
218
|
+
const model = strFromU8(modelFile!)
|
|
219
|
+
const geometryReferences = Array.from(
|
|
220
|
+
model.matchAll(/@\.\/(geometries\/[^@]+)@/g),
|
|
221
|
+
(match) => match[1]!,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
expect(geometryReferences).toHaveLength(1)
|
|
225
|
+
for (const reference of geometryReferences) {
|
|
226
|
+
expect(files[reference]).toBeDefined()
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('retains RGBA and back-face visibility without prescribing GLB representation', () => {
|
|
231
|
+
withCanvasCapture((capture) => {
|
|
232
|
+
const geometry = triangleGeometry(new Float32Array([1, 0, 0, 1, 0, 1, 0, 0.5, 0, 0, 1, 0]))
|
|
233
|
+
geometry.setAttribute('normal', new Float32BufferAttribute([0, 0, 1, 0, 0, 1, 0, 0, 1], 3))
|
|
234
|
+
const mesh = new Mesh(
|
|
235
|
+
geometry,
|
|
236
|
+
new MeshStandardMaterial({ side: DoubleSide, vertexColors: true }),
|
|
237
|
+
)
|
|
238
|
+
const root = new Group()
|
|
239
|
+
root.add(mesh)
|
|
240
|
+
|
|
241
|
+
expect(normalizePortableScene(root)).toEqual([])
|
|
242
|
+
|
|
243
|
+
expect(portableColorAt(mesh, 0, capture)).toEqual([255, 0, 0, 255])
|
|
244
|
+
expect(portableColorAt(mesh, 1, capture)).toEqual([0, 255, 0, 128])
|
|
245
|
+
expect(portableColorAt(mesh, 2, capture)).toEqual([0, 0, 255, 0])
|
|
246
|
+
const material = mesh.material as MeshStandardMaterial
|
|
247
|
+
expect(material.transparent).toBe(true)
|
|
248
|
+
if (material.side === FrontSide) {
|
|
249
|
+
expect(mesh.geometry.getAttribute('position').count).toBe(6)
|
|
250
|
+
expect(mesh.geometry.getAttribute('normal').getZ(0)).toBe(1)
|
|
251
|
+
expect(mesh.geometry.getAttribute('normal').getZ(3)).toBe(-1)
|
|
252
|
+
} else {
|
|
253
|
+
expect(material.side).toBe(DoubleSide)
|
|
254
|
+
expect(mesh.geometry.getAttribute('position').count).toBe(3)
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
function expectVector(actual: Vector3, expected: Vector3): void {
|
|
261
|
+
expect(actual.x).toBeCloseTo(expected.x, 6)
|
|
262
|
+
expect(actual.y).toBeCloseTo(expected.y, 6)
|
|
263
|
+
expect(actual.z).toBeCloseTo(expected.z, 6)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function triangleGeometry(colors: Float32Array): BufferGeometry {
|
|
267
|
+
const geometry = new BufferGeometry()
|
|
268
|
+
geometry.setAttribute('position', new Float32BufferAttribute([0, 0, 0, 1, 0, 0, 0, 1, 0], 3))
|
|
269
|
+
geometry.setAttribute('color', new Float32BufferAttribute(colors, 4))
|
|
270
|
+
return geometry
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function portableLinearTintAt(mesh: Mesh, vertexIndex: number): Color {
|
|
274
|
+
const material = (
|
|
275
|
+
Array.isArray(mesh.material) ? mesh.material[0] : mesh.material
|
|
276
|
+
) as MeshStandardMaterial
|
|
277
|
+
const tint = material.color.clone()
|
|
278
|
+
const color = mesh.geometry.getAttribute('color')
|
|
279
|
+
if (color) {
|
|
280
|
+
tint.multiply(
|
|
281
|
+
new Color(color.getX(vertexIndex), color.getY(vertexIndex), color.getZ(vertexIndex)),
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
return tint
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function expectColor(actual: Color, expected: Color): void {
|
|
288
|
+
expect(actual.r).toBeCloseTo(expected.r, 6)
|
|
289
|
+
expect(actual.g).toBeCloseTo(expected.g, 6)
|
|
290
|
+
expect(actual.b).toBeCloseTo(expected.b, 6)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function portableColorAt(mesh: Mesh, vertexIndex: number, capture: () => ImageData): number[] {
|
|
294
|
+
const colors = mesh.geometry.getAttribute('color')
|
|
295
|
+
if (colors) {
|
|
296
|
+
return [
|
|
297
|
+
encodeSrgbByte(colors.getX(vertexIndex)),
|
|
298
|
+
encodeSrgbByte(colors.getY(vertexIndex)),
|
|
299
|
+
encodeSrgbByte(colors.getZ(vertexIndex)),
|
|
300
|
+
Math.round((colors.itemSize >= 4 ? colors.getW(vertexIndex) : 1) * 255),
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
const material = mesh.material as MeshStandardMaterial
|
|
304
|
+
if (!material.map) throw new Error('Portable color has no material-connected carrier')
|
|
305
|
+
const channel = material.map.channel ?? 0
|
|
306
|
+
const uvName = channel === 0 ? 'uv' : `uv${channel}`
|
|
307
|
+
const uvs = mesh.geometry.getAttribute(uvName)
|
|
308
|
+
if (!uvs) throw new Error(`Portable color map has no geometry ${uvName} coordinates`)
|
|
309
|
+
const image = capture()
|
|
310
|
+
const u = Math.max(0, Math.min(1, uvs.getX(vertexIndex)))
|
|
311
|
+
const rawV = Math.max(0, Math.min(1, uvs.getY(vertexIndex)))
|
|
312
|
+
const v = material.map.flipY ? 1 - rawV : rawV
|
|
313
|
+
return pixel(
|
|
314
|
+
image.data,
|
|
315
|
+
image.width,
|
|
316
|
+
Math.min(image.width - 1, Math.floor(u * image.width)),
|
|
317
|
+
Math.min(image.height - 1, Math.floor(v * image.height)),
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function encodeSrgbByte(linear: number): number {
|
|
322
|
+
const clipped = Math.max(0, Math.min(1, linear))
|
|
323
|
+
const encoded = clipped <= 0.0031308 ? clipped * 12.92 : 1.055 * clipped ** (1 / 2.4) - 0.055
|
|
324
|
+
return Math.round(encoded * 255)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function pixel(pixels: Uint8ClampedArray, width: number, x: number, y: number): number[] {
|
|
328
|
+
const offset = (y * width + x) * 4
|
|
329
|
+
return Array.from(pixels.subarray(offset, offset + 4))
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function withCanvasCapture(run: (capture: () => ImageData) => void): void {
|
|
333
|
+
const globals = globalThis as unknown as { document?: Document }
|
|
334
|
+
const previousDocument = globals.document
|
|
335
|
+
let captured: ImageData | null = null
|
|
336
|
+
const canvas = {
|
|
337
|
+
width: 0,
|
|
338
|
+
height: 0,
|
|
339
|
+
getContext: () => ({
|
|
340
|
+
createImageData: (width: number, height: number) => {
|
|
341
|
+
captured = {
|
|
342
|
+
colorSpace: 'srgb',
|
|
343
|
+
data: new Uint8ClampedArray(width * height * 4),
|
|
344
|
+
height,
|
|
345
|
+
width,
|
|
346
|
+
} as ImageData
|
|
347
|
+
return captured
|
|
348
|
+
},
|
|
349
|
+
putImageData: (image: ImageData) => {
|
|
350
|
+
captured = image
|
|
351
|
+
},
|
|
352
|
+
drawImage: () => {
|
|
353
|
+
if (!captured) throw new Error('Source canvas has no captured pixels')
|
|
354
|
+
},
|
|
355
|
+
getImageData: (_x: number, _y: number, width: number, height: number) =>
|
|
356
|
+
captured ??
|
|
357
|
+
({
|
|
358
|
+
colorSpace: 'srgb',
|
|
359
|
+
data: new Uint8ClampedArray(width * height * 4),
|
|
360
|
+
height,
|
|
361
|
+
width,
|
|
362
|
+
} as ImageData),
|
|
363
|
+
}),
|
|
364
|
+
} as unknown as HTMLCanvasElement
|
|
365
|
+
globals.document = {
|
|
366
|
+
createElement: (tagName: string) => {
|
|
367
|
+
if (tagName !== 'canvas') throw new Error(`Unexpected element request: ${tagName}`)
|
|
368
|
+
return canvas
|
|
369
|
+
},
|
|
370
|
+
} as unknown as Document
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
run(() => {
|
|
374
|
+
if (!captured) throw new Error('Portable normalization did not emit atlas pixels')
|
|
375
|
+
return captured
|
|
376
|
+
})
|
|
377
|
+
} finally {
|
|
378
|
+
if (previousDocument) globals.document = previousDocument
|
|
379
|
+
else delete globals.document
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
describe('portable normal maps', () => {
|
|
384
|
+
function flatNormalPixels(size: number): Uint8Array {
|
|
385
|
+
const pixels = new Uint8Array(size * size * 4)
|
|
386
|
+
for (let index = 0; index < pixels.length; index += 4) pixels.set([128, 128, 255, 255], index)
|
|
387
|
+
return pixels
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
test('decompresses a shared compressed normal map once before canonicalising it', async () => {
|
|
391
|
+
const compressed = new CompressedTexture([], 2, 2)
|
|
392
|
+
compressed.name = 'NormalGL_test'
|
|
393
|
+
compressed.repeat.set(2, 2)
|
|
394
|
+
const first = new MeshStandardMaterial({ normalMap: compressed })
|
|
395
|
+
first.normalScale.set(0.5, 0.5)
|
|
396
|
+
const second = new MeshStandardMaterial({ normalMap: compressed })
|
|
397
|
+
second.normalScale.set(0.5, -0.5)
|
|
398
|
+
const root = new Group()
|
|
399
|
+
root.add(new Mesh(new BoxGeometry(), first), new Mesh(new BoxGeometry(), second))
|
|
400
|
+
|
|
401
|
+
let calls = 0
|
|
402
|
+
await decompressCanonicalNormalMaps(root, async () => {
|
|
403
|
+
calls += 1
|
|
404
|
+
return new DataTexture(flatNormalPixels(2), 2, 2)
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
expect(calls).toBe(1)
|
|
408
|
+
expect(first.normalMap).toBe(second.normalMap)
|
|
409
|
+
expect(first.normalMap?.repeat.toArray()).toEqual([2, 2])
|
|
410
|
+
expect(first.normalMap?.name).toBe('NormalGL_test')
|
|
411
|
+
|
|
412
|
+
withCanvasCapture(() => {
|
|
413
|
+
normalizePortableScene(root)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
expect((first.normalMap as { isCanvasTexture?: boolean }).isCanvasTexture).toBe(true)
|
|
417
|
+
expect(first.normalMap).not.toBe(second.normalMap)
|
|
418
|
+
expect(first.normalScale.toArray()).toEqual([1, 1])
|
|
419
|
+
expect(second.normalScale.toArray()).toEqual([1, 1])
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
test('a compressed normal map that was not decompressed still fails loudly', () => {
|
|
423
|
+
const material = new MeshStandardMaterial({ normalMap: new CompressedTexture([], 2, 2) })
|
|
424
|
+
material.normalScale.set(0.5, 0.5)
|
|
425
|
+
const root = new Group()
|
|
426
|
+
root.add(new Mesh(new BoxGeometry(), material))
|
|
427
|
+
withCanvasCapture(() => {
|
|
428
|
+
expect(() => normalizePortableScene(root)).toThrow('must be baked before portable export')
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
test('preserved normal maps keep their texture and normal scale', async () => {
|
|
433
|
+
const placeholder = new Texture()
|
|
434
|
+
placeholder.image = { width: 1, height: 1 }
|
|
435
|
+
const material = new MeshStandardMaterial({ normalMap: placeholder })
|
|
436
|
+
material.normalScale.set(1, -1)
|
|
437
|
+
const root = new Group()
|
|
438
|
+
root.add(new Mesh(new BoxGeometry(), material))
|
|
439
|
+
const options = { preserveNormalMap: (texture: Texture) => texture === placeholder }
|
|
440
|
+
|
|
441
|
+
await decompressCanonicalNormalMaps(
|
|
442
|
+
root,
|
|
443
|
+
async () => {
|
|
444
|
+
throw new Error('preserved maps must not be decompressed')
|
|
445
|
+
},
|
|
446
|
+
options,
|
|
447
|
+
)
|
|
448
|
+
withCanvasCapture(() => {
|
|
449
|
+
normalizePortableScene(root, options)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
expect(material.normalMap).toBe(placeholder)
|
|
453
|
+
expect(material.normalScale.toArray()).toEqual([1, -1])
|
|
454
|
+
})
|
|
455
|
+
})
|