@pascal-app/editor 1.0.0-beta.5 → 1.0.0
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 +13 -7
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +1 -0
- 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 +42 -51
- package/src/components/editor/floorplan-panel.tsx +76 -12
- 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/linear-resize-drag.ts +67 -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 +18 -1
- package/src/components/editor/index.tsx +140 -21
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/selection-manager.tsx +104 -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 +284 -237
- 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-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 +180 -32
- 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/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/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 +135 -2
- 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 +6 -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 +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- 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 +45 -8
- 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/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 +9 -12
- 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/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- 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 +22 -5
- 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 +263 -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 +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- 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 +390 -36
- 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 +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
- 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/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- 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 +122 -0
- 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 +129 -62
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +82 -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/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 +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- 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 +1 -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/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- 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-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- 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/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 +366 -0
- package/src/lib/portable-export.ts +969 -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 +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -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 +30 -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.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -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/usdz-export.ts +38 -0
- 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-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/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -0,0 +1,366 @@
|
|
|
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
|
+
DoubleSide,
|
|
9
|
+
Float32BufferAttribute,
|
|
10
|
+
FrontSide,
|
|
11
|
+
Group,
|
|
12
|
+
InstancedBufferAttribute,
|
|
13
|
+
InstancedMesh,
|
|
14
|
+
Matrix4,
|
|
15
|
+
Mesh,
|
|
16
|
+
MeshStandardMaterial,
|
|
17
|
+
Quaternion,
|
|
18
|
+
Vector3,
|
|
19
|
+
} from 'three'
|
|
20
|
+
import { USDZExporter } from 'three/examples/jsm/exporters/USDZExporter.js'
|
|
21
|
+
import {
|
|
22
|
+
createUsdzScene,
|
|
23
|
+
expandInstancedMeshes,
|
|
24
|
+
freezeDeformedMeshes,
|
|
25
|
+
normalizePortableScene,
|
|
26
|
+
} from './portable-export'
|
|
27
|
+
|
|
28
|
+
describe('portable vertex-color boundary', () => {
|
|
29
|
+
test('clips only over-bright channels while preserving ordinary linear colors', () => {
|
|
30
|
+
withCanvasCapture((capture) => {
|
|
31
|
+
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])
|
|
32
|
+
const sourceValues = Array.from(colors)
|
|
33
|
+
const geometry = triangleGeometry(colors)
|
|
34
|
+
const material = new MeshStandardMaterial({ vertexColors: true })
|
|
35
|
+
const mesh = new Mesh(geometry, material)
|
|
36
|
+
const root = new Group()
|
|
37
|
+
root.add(mesh)
|
|
38
|
+
|
|
39
|
+
const warnings = normalizePortableScene(root)
|
|
40
|
+
|
|
41
|
+
expect(warnings).toHaveLength(1)
|
|
42
|
+
expect(warnings[0]).toMatch(/color.*clip/i)
|
|
43
|
+
expect(Array.from(colors)).toEqual(sourceValues)
|
|
44
|
+
expect(portableColorAt(mesh, 0, capture)).toEqual([
|
|
45
|
+
encodeSrgbByte(0.25),
|
|
46
|
+
encodeSrgbByte(0.5),
|
|
47
|
+
encodeSrgbByte(0.75),
|
|
48
|
+
255,
|
|
49
|
+
])
|
|
50
|
+
expect(portableColorAt(mesh, 1, capture)).toEqual([
|
|
51
|
+
255,
|
|
52
|
+
encodeSrgbByte(0.4),
|
|
53
|
+
encodeSrgbByte(0.2),
|
|
54
|
+
128,
|
|
55
|
+
])
|
|
56
|
+
expect(portableColorAt(mesh, 2, capture)).toEqual([
|
|
57
|
+
encodeSrgbByte(0.1),
|
|
58
|
+
encodeSrgbByte(0.2),
|
|
59
|
+
encodeSrgbByte(0.3),
|
|
60
|
+
0,
|
|
61
|
+
])
|
|
62
|
+
const portableMaterial = mesh.material as MeshStandardMaterial
|
|
63
|
+
expect(portableMaterial.emissive.getHex()).toBe(0)
|
|
64
|
+
expect(portableMaterial.emissiveMap).toBeNull()
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('does not warn when every channel is already portable', () => {
|
|
69
|
+
withCanvasCapture(() => {
|
|
70
|
+
const geometry = triangleGeometry(
|
|
71
|
+
new Float32Array([0, 0.25, 0.5, 1, 0.75, 1, 0.125, 1, 0.3, 0.4, 0.6, 1]),
|
|
72
|
+
)
|
|
73
|
+
const mesh = new Mesh(geometry, new MeshStandardMaterial({ vertexColors: true }))
|
|
74
|
+
const root = new Group()
|
|
75
|
+
root.add(mesh)
|
|
76
|
+
|
|
77
|
+
expect(normalizePortableScene(root)).toEqual([])
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('portable geometry normalization', () => {
|
|
83
|
+
test('preserves two transformed instance populations without changing source data', () => {
|
|
84
|
+
const sourceMaterial = new MeshStandardMaterial({ color: '#808080' })
|
|
85
|
+
const sourceGeometry = new BoxGeometry(1, 2, 3)
|
|
86
|
+
const shaderInstancePayload = new InstancedBufferAttribute(new Float32Array(32), 16)
|
|
87
|
+
sourceGeometry.setAttribute('shaderInstancePayload', shaderInstancePayload)
|
|
88
|
+
const instances = new InstancedMesh(sourceGeometry, sourceMaterial, 2)
|
|
89
|
+
instances.name = 'asymmetric-instance'
|
|
90
|
+
instances.position.set(10, 0, -2)
|
|
91
|
+
const firstMatrix = new Matrix4().compose(
|
|
92
|
+
new Vector3(2, 0, 0),
|
|
93
|
+
new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI / 2),
|
|
94
|
+
new Vector3(1, 2, 1),
|
|
95
|
+
)
|
|
96
|
+
const reflectedMatrix = new Matrix4().compose(
|
|
97
|
+
new Vector3(-3, 1, 4),
|
|
98
|
+
new Quaternion(),
|
|
99
|
+
new Vector3(-2, 1, 0.5),
|
|
100
|
+
)
|
|
101
|
+
instances.setMatrixAt(0, firstMatrix)
|
|
102
|
+
instances.setMatrixAt(1, reflectedMatrix)
|
|
103
|
+
instances.setColorAt(0, new Color('#ff0000'))
|
|
104
|
+
instances.setColorAt(1, new Color('#0000ff'))
|
|
105
|
+
const root = new Group()
|
|
106
|
+
root.add(instances)
|
|
107
|
+
|
|
108
|
+
expandInstancedMeshes(root)
|
|
109
|
+
root.updateMatrixWorld(true)
|
|
110
|
+
|
|
111
|
+
const firstPopulation = new Box3().makeEmpty()
|
|
112
|
+
const reflectedPopulation = new Box3().makeEmpty()
|
|
113
|
+
let triangleCount = 0
|
|
114
|
+
let firstOutputTint: Color | null = null
|
|
115
|
+
let reflectedOutputTint: Color | null = null
|
|
116
|
+
root.traverse((object) => {
|
|
117
|
+
const mesh = object as Mesh
|
|
118
|
+
if (!mesh.isMesh) return
|
|
119
|
+
const positions = mesh.geometry.getAttribute('position')
|
|
120
|
+
triangleCount += (mesh.geometry.getIndex()?.count ?? positions.count) / 3
|
|
121
|
+
for (const attribute of Object.values(mesh.geometry.attributes)) {
|
|
122
|
+
expect((attribute as InstancedBufferAttribute).isInstancedBufferAttribute).not.toBe(true)
|
|
123
|
+
expect(attribute.count).toBe(positions.count)
|
|
124
|
+
}
|
|
125
|
+
for (let index = 0; index < positions.count; index += 1) {
|
|
126
|
+
const world = new Vector3()
|
|
127
|
+
.fromBufferAttribute(positions, index)
|
|
128
|
+
.applyMatrix4(mesh.matrixWorld)
|
|
129
|
+
if (world.x < 9) {
|
|
130
|
+
reflectedPopulation.expandByPoint(world)
|
|
131
|
+
reflectedOutputTint ??= portableLinearTintAt(mesh, index)
|
|
132
|
+
} else {
|
|
133
|
+
firstPopulation.expandByPoint(world)
|
|
134
|
+
firstOutputTint ??= portableLinearTintAt(mesh, index)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
expect(triangleCount).toBe(24)
|
|
139
|
+
expectVector(firstPopulation.getCenter(new Vector3()), new Vector3(12, 0, -2))
|
|
140
|
+
expectVector(firstPopulation.getSize(new Vector3()), new Vector3(3, 4, 1))
|
|
141
|
+
expectVector(reflectedPopulation.getCenter(new Vector3()), new Vector3(7, 1, 2))
|
|
142
|
+
expectVector(reflectedPopulation.getSize(new Vector3()), new Vector3(2, 2, 1.5))
|
|
143
|
+
expect(firstOutputTint).not.toBeNull()
|
|
144
|
+
expect(reflectedOutputTint).not.toBeNull()
|
|
145
|
+
expectColor(firstOutputTint!, sourceMaterial.color.clone().multiply(new Color('#ff0000')))
|
|
146
|
+
expectColor(reflectedOutputTint!, sourceMaterial.color.clone().multiply(new Color('#0000ff')))
|
|
147
|
+
const retainedMatrix = new Matrix4()
|
|
148
|
+
instances.getMatrixAt(1, retainedMatrix)
|
|
149
|
+
expect(retainedMatrix.equals(reflectedMatrix)).toBe(true)
|
|
150
|
+
const retainedColor = new Color()
|
|
151
|
+
expect(instances.geometry.getAttribute('shaderInstancePayload')).toBe(shaderInstancePayload)
|
|
152
|
+
instances.getColorAt(1, retainedColor)
|
|
153
|
+
expect(retainedColor.equals(new Color('#0000ff'))).toBe(true)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('freezes a settled morph pose into static vertex positions without mutating its source', () => {
|
|
157
|
+
const geometry = new BufferGeometry()
|
|
158
|
+
const sourcePositions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0])
|
|
159
|
+
const morphDelta = new Float32Array([0, 0, 0, 0, 0, 0, 0, 2, 0])
|
|
160
|
+
geometry.setAttribute('position', new Float32BufferAttribute(sourcePositions, 3))
|
|
161
|
+
geometry.morphAttributes.position = [new Float32BufferAttribute(morphDelta, 3)]
|
|
162
|
+
geometry.morphTargetsRelative = true
|
|
163
|
+
const mesh = new Mesh(geometry, new MeshStandardMaterial())
|
|
164
|
+
mesh.updateMorphTargets()
|
|
165
|
+
mesh.morphTargetInfluences![0] = 0.25
|
|
166
|
+
const root = new Group()
|
|
167
|
+
root.add(mesh)
|
|
168
|
+
|
|
169
|
+
freezeDeformedMeshes(root)
|
|
170
|
+
|
|
171
|
+
const frozen = mesh.geometry.getAttribute('position')
|
|
172
|
+
expect([frozen.getX(2), frozen.getY(2), frozen.getZ(2)]).toEqual([0, 1.5, 0])
|
|
173
|
+
expect(mesh.geometry.morphAttributes.position).toBeUndefined()
|
|
174
|
+
expect(mesh.morphTargetInfluences).toBeUndefined()
|
|
175
|
+
expect(Array.from(sourcePositions)).toEqual([0, 0, 0, 1, 0, 0, 0, 1, 0])
|
|
176
|
+
expect(Array.from(morphDelta)).toEqual([0, 0, 0, 0, 0, 0, 0, 2, 0])
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('bakes reflected USDZ transforms with outward-consistent winding and normals', () => {
|
|
180
|
+
const geometry = new BufferGeometry()
|
|
181
|
+
geometry.setAttribute('position', new Float32BufferAttribute([0, 0, 0, 1, 0, 0, 0, 1, 1], 3))
|
|
182
|
+
geometry.computeVertexNormals()
|
|
183
|
+
const source = new Mesh(geometry, new MeshStandardMaterial())
|
|
184
|
+
source.name = 'reflected-triangle'
|
|
185
|
+
source.position.set(2, -1, 3)
|
|
186
|
+
source.scale.set(-2, 1, 0.5)
|
|
187
|
+
const root = new Group()
|
|
188
|
+
root.add(source)
|
|
189
|
+
|
|
190
|
+
const portable = createUsdzScene(root)
|
|
191
|
+
const reflected = portable.getObjectByName('reflected-triangle') as Mesh
|
|
192
|
+
const positions = reflected.geometry.getAttribute('position')
|
|
193
|
+
const normals = reflected.geometry.getAttribute('normal')
|
|
194
|
+
const first = new Vector3().fromBufferAttribute(positions, 0)
|
|
195
|
+
const second = new Vector3().fromBufferAttribute(positions, 1)
|
|
196
|
+
const third = new Vector3().fromBufferAttribute(positions, 2)
|
|
197
|
+
const faceNormal = second.clone().sub(first).cross(third.clone().sub(first)).normalize()
|
|
198
|
+
const vertexNormal = new Vector3().fromBufferAttribute(normals, 0).normalize()
|
|
199
|
+
|
|
200
|
+
expect(faceNormal.dot(vertexNormal)).toBeGreaterThan(0.999)
|
|
201
|
+
expectVector(reflected.position, new Vector3())
|
|
202
|
+
expectVector(reflected.scale, new Vector3(1, 1, 1))
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('emits every referenced geometry member for a one-element material array', async () => {
|
|
206
|
+
const root = new Group()
|
|
207
|
+
const mesh = new Mesh(new BoxGeometry(1, 1, 1), [new MeshStandardMaterial()])
|
|
208
|
+
root.add(mesh)
|
|
209
|
+
|
|
210
|
+
const archive = await new USDZExporter().parseAsync(createUsdzScene(root))
|
|
211
|
+
const files = unzipSync(archive)
|
|
212
|
+
const modelFile = files['model.usda']
|
|
213
|
+
expect(modelFile).toBeDefined()
|
|
214
|
+
const model = strFromU8(modelFile!)
|
|
215
|
+
const geometryReferences = Array.from(
|
|
216
|
+
model.matchAll(/@\.\/(geometries\/[^@]+)@/g),
|
|
217
|
+
(match) => match[1]!,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
expect(geometryReferences).toHaveLength(1)
|
|
221
|
+
for (const reference of geometryReferences) {
|
|
222
|
+
expect(files[reference]).toBeDefined()
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('retains RGBA and back-face visibility without prescribing GLB representation', () => {
|
|
227
|
+
withCanvasCapture((capture) => {
|
|
228
|
+
const geometry = triangleGeometry(new Float32Array([1, 0, 0, 1, 0, 1, 0, 0.5, 0, 0, 1, 0]))
|
|
229
|
+
geometry.setAttribute('normal', new Float32BufferAttribute([0, 0, 1, 0, 0, 1, 0, 0, 1], 3))
|
|
230
|
+
const mesh = new Mesh(
|
|
231
|
+
geometry,
|
|
232
|
+
new MeshStandardMaterial({ side: DoubleSide, vertexColors: true }),
|
|
233
|
+
)
|
|
234
|
+
const root = new Group()
|
|
235
|
+
root.add(mesh)
|
|
236
|
+
|
|
237
|
+
expect(normalizePortableScene(root)).toEqual([])
|
|
238
|
+
|
|
239
|
+
expect(portableColorAt(mesh, 0, capture)).toEqual([255, 0, 0, 255])
|
|
240
|
+
expect(portableColorAt(mesh, 1, capture)).toEqual([0, 255, 0, 128])
|
|
241
|
+
expect(portableColorAt(mesh, 2, capture)).toEqual([0, 0, 255, 0])
|
|
242
|
+
const material = mesh.material as MeshStandardMaterial
|
|
243
|
+
expect(material.transparent).toBe(true)
|
|
244
|
+
if (material.side === FrontSide) {
|
|
245
|
+
expect(mesh.geometry.getAttribute('position').count).toBe(6)
|
|
246
|
+
expect(mesh.geometry.getAttribute('normal').getZ(0)).toBe(1)
|
|
247
|
+
expect(mesh.geometry.getAttribute('normal').getZ(3)).toBe(-1)
|
|
248
|
+
} else {
|
|
249
|
+
expect(material.side).toBe(DoubleSide)
|
|
250
|
+
expect(mesh.geometry.getAttribute('position').count).toBe(3)
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
function expectVector(actual: Vector3, expected: Vector3): void {
|
|
257
|
+
expect(actual.x).toBeCloseTo(expected.x, 6)
|
|
258
|
+
expect(actual.y).toBeCloseTo(expected.y, 6)
|
|
259
|
+
expect(actual.z).toBeCloseTo(expected.z, 6)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function triangleGeometry(colors: Float32Array): BufferGeometry {
|
|
263
|
+
const geometry = new BufferGeometry()
|
|
264
|
+
geometry.setAttribute('position', new Float32BufferAttribute([0, 0, 0, 1, 0, 0, 0, 1, 0], 3))
|
|
265
|
+
geometry.setAttribute('color', new Float32BufferAttribute(colors, 4))
|
|
266
|
+
return geometry
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function portableLinearTintAt(mesh: Mesh, vertexIndex: number): Color {
|
|
270
|
+
const material = (
|
|
271
|
+
Array.isArray(mesh.material) ? mesh.material[0] : mesh.material
|
|
272
|
+
) as MeshStandardMaterial
|
|
273
|
+
const tint = material.color.clone()
|
|
274
|
+
const color = mesh.geometry.getAttribute('color')
|
|
275
|
+
if (color) {
|
|
276
|
+
tint.multiply(
|
|
277
|
+
new Color(color.getX(vertexIndex), color.getY(vertexIndex), color.getZ(vertexIndex)),
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
return tint
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function expectColor(actual: Color, expected: Color): void {
|
|
284
|
+
expect(actual.r).toBeCloseTo(expected.r, 6)
|
|
285
|
+
expect(actual.g).toBeCloseTo(expected.g, 6)
|
|
286
|
+
expect(actual.b).toBeCloseTo(expected.b, 6)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function portableColorAt(mesh: Mesh, vertexIndex: number, capture: () => ImageData): number[] {
|
|
290
|
+
const colors = mesh.geometry.getAttribute('color')
|
|
291
|
+
if (colors) {
|
|
292
|
+
return [
|
|
293
|
+
encodeSrgbByte(colors.getX(vertexIndex)),
|
|
294
|
+
encodeSrgbByte(colors.getY(vertexIndex)),
|
|
295
|
+
encodeSrgbByte(colors.getZ(vertexIndex)),
|
|
296
|
+
Math.round((colors.itemSize >= 4 ? colors.getW(vertexIndex) : 1) * 255),
|
|
297
|
+
]
|
|
298
|
+
}
|
|
299
|
+
const material = mesh.material as MeshStandardMaterial
|
|
300
|
+
if (!material.map) throw new Error('Portable color has no material-connected carrier')
|
|
301
|
+
const channel = material.map.channel ?? 0
|
|
302
|
+
const uvName = channel === 0 ? 'uv' : `uv${channel}`
|
|
303
|
+
const uvs = mesh.geometry.getAttribute(uvName)
|
|
304
|
+
if (!uvs) throw new Error(`Portable color map has no geometry ${uvName} coordinates`)
|
|
305
|
+
const image = capture()
|
|
306
|
+
const u = Math.max(0, Math.min(1, uvs.getX(vertexIndex)))
|
|
307
|
+
const rawV = Math.max(0, Math.min(1, uvs.getY(vertexIndex)))
|
|
308
|
+
const v = material.map.flipY ? 1 - rawV : rawV
|
|
309
|
+
return pixel(
|
|
310
|
+
image.data,
|
|
311
|
+
image.width,
|
|
312
|
+
Math.min(image.width - 1, Math.floor(u * image.width)),
|
|
313
|
+
Math.min(image.height - 1, Math.floor(v * image.height)),
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function encodeSrgbByte(linear: number): number {
|
|
318
|
+
const clipped = Math.max(0, Math.min(1, linear))
|
|
319
|
+
const encoded = clipped <= 0.0031308 ? clipped * 12.92 : 1.055 * clipped ** (1 / 2.4) - 0.055
|
|
320
|
+
return Math.round(encoded * 255)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function pixel(pixels: Uint8ClampedArray, width: number, x: number, y: number): number[] {
|
|
324
|
+
const offset = (y * width + x) * 4
|
|
325
|
+
return Array.from(pixels.subarray(offset, offset + 4))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function withCanvasCapture(run: (capture: () => ImageData) => void): void {
|
|
329
|
+
const globals = globalThis as unknown as { document?: Document }
|
|
330
|
+
const previousDocument = globals.document
|
|
331
|
+
let captured: ImageData | null = null
|
|
332
|
+
const canvas = {
|
|
333
|
+
width: 0,
|
|
334
|
+
height: 0,
|
|
335
|
+
getContext: () => ({
|
|
336
|
+
createImageData: (width: number, height: number) => {
|
|
337
|
+
captured = {
|
|
338
|
+
colorSpace: 'srgb',
|
|
339
|
+
data: new Uint8ClampedArray(width * height * 4),
|
|
340
|
+
height,
|
|
341
|
+
width,
|
|
342
|
+
} as ImageData
|
|
343
|
+
return captured
|
|
344
|
+
},
|
|
345
|
+
putImageData: (image: ImageData) => {
|
|
346
|
+
captured = image
|
|
347
|
+
},
|
|
348
|
+
}),
|
|
349
|
+
} as unknown as HTMLCanvasElement
|
|
350
|
+
globals.document = {
|
|
351
|
+
createElement: (tagName: string) => {
|
|
352
|
+
if (tagName !== 'canvas') throw new Error(`Unexpected element request: ${tagName}`)
|
|
353
|
+
return canvas
|
|
354
|
+
},
|
|
355
|
+
} as unknown as Document
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
run(() => {
|
|
359
|
+
if (!captured) throw new Error('Portable normalization did not emit atlas pixels')
|
|
360
|
+
return captured
|
|
361
|
+
})
|
|
362
|
+
} finally {
|
|
363
|
+
if (previousDocument) globals.document = previousDocument
|
|
364
|
+
else delete globals.document
|
|
365
|
+
}
|
|
366
|
+
}
|