@pascal-app/editor 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -6
- package/src/components/editor/bake-exporter.tsx +4 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/custom-camera-controls.tsx +22 -74
- package/src/components/editor/editor-layout-mobile.tsx +1 -0
- package/src/components/editor/floating-action-menu.tsx +14 -1
- package/src/components/editor/floorplan-panel.tsx +233 -166
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +5 -9
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- 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 +13 -2
- package/src/components/editor/handles/use-handle-drag.ts +80 -8
- package/src/components/editor/index.tsx +36 -13
- package/src/components/editor/node-action-menu.tsx +2 -0
- package/src/components/editor/node-arrow-handles.tsx +162 -69
- package/src/components/editor/registry-action-contributions.tsx +53 -0
- package/src/components/editor/selection-manager.tsx +49 -0
- package/src/components/editor/thumbnail-generator.tsx +11 -2
- package/src/components/editor/wall-move-side-handles.tsx +71 -21
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +154 -49
- package/src/components/editor-2d/renderers/floorplan-draft-wall-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +49 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +113 -24
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/zone/zone-system.tsx +8 -4
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +169 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +204 -0
- package/src/components/tools/item/placement-strategies.ts +212 -200
- package/src/components/tools/item/placement-types.ts +4 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/use-draft-node.ts +136 -47
- package/src/components/tools/item/use-placement-coordinator.tsx +391 -157
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +509 -0
- package/src/components/tools/registry/item-surface-move.ts +463 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +237 -78
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +27 -13
- 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 +107 -0
- package/src/components/tools/shared/surface-rejection.tsx +43 -0
- package/src/components/tools/tool-manager.tsx +36 -3
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/command-palette/editor-commands.tsx +54 -1
- package/src/components/ui/controls/material-paint-panel.tsx +6 -67
- package/src/components/ui/controls/material-picker.tsx +5 -67
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/slider-control.tsx +87 -35
- package/src/components/ui/floating-level-selector.tsx +81 -5
- package/src/components/ui/helpers/helper-manager.tsx +24 -6
- package/src/components/ui/helpers/registered-tool-helper.tsx +2 -32
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +6 -3
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +9 -17
- package/src/components/ui/panels/reference-panel.tsx +3 -3
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +669 -336
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +30 -11
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +106 -4
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +6 -1
- 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/snap-target-badge.tsx +2 -0
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +1 -12
- package/src/components/viewer/viewer-scene-header.tsx +3 -0
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.integration.test.tsx +197 -0
- package/src/hooks/use-auto-save.ts +21 -6
- package/src/hooks/use-ceiling-events.test.ts +25 -2
- package/src/hooks/use-ceiling-events.ts +7 -1
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +7 -0
- package/src/hooks/use-keyboard.ts +43 -28
- package/src/index.tsx +68 -3
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- 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/floorplan/floorplan-export.test.ts +142 -2
- package/src/lib/floorplan/floorplan-export.tsx +9 -3
- package/src/lib/floorplan/floorplan-extension.ts +16 -0
- package/src/lib/floorplan/floorplan-readonly.ts +4 -0
- package/src/lib/fresh-planar-placement.ts +173 -33
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +265 -16
- package/src/lib/glb-export.ts +197 -37
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/interaction/scope.ts +9 -1
- package/src/lib/keyboard-pan.test.ts +62 -0
- package/src/lib/keyboard-pan.ts +89 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- 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/material-catalog-model.ts +72 -0
- package/src/lib/material-paint-panel-model.ts +76 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/panel-rows.ts +75 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/portable-export.test.ts +89 -0
- package/src/lib/portable-export.ts +84 -11
- package/src/lib/print-shell-compiler-manifold-core.ts +59 -10
- package/src/lib/print-shell-compiler-manifold-worker.ts +14 -1
- package/src/lib/print-shell-compiler-manifold.worker.ts +1 -1
- package/src/lib/print-shell-compiler-protocol.ts +8 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.test.ts +99 -0
- package/src/lib/scene.ts +21 -3
- 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/snapping-mode.test.ts +13 -2
- package/src/lib/snapping-mode.ts +11 -6
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +43 -16
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/use-editor.tsx +10 -2
- package/src/store/use-floorplan-draft-preview.ts +10 -1
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-send-to-app.ts +21 -0
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
prepareSceneForExportAsync,
|
|
28
28
|
writeTextureReferenceExtras,
|
|
29
29
|
} from './glb-export'
|
|
30
|
+
import { createUsdzScene } from './portable-export'
|
|
30
31
|
|
|
31
32
|
// The reference module reads the storage origin lazily on first use, so
|
|
32
33
|
// setting the env here (before any validation call) pins it for the file.
|
|
@@ -99,6 +100,29 @@ describe('prepareSceneForExport', () => {
|
|
|
99
100
|
expect(material.color.getHexString()).toBe('cc3300')
|
|
100
101
|
})
|
|
101
102
|
|
|
103
|
+
test('clones userData that holds runtime resources and functions', () => {
|
|
104
|
+
const root = new THREE.Group()
|
|
105
|
+
root.name = 'scene-renderer'
|
|
106
|
+
const effectMaterial = new THREE.MeshStandardMaterial()
|
|
107
|
+
effectMaterial.addEventListener('dispose', () => {})
|
|
108
|
+
const pool = meshWithNodeMaterial(nodeMaterial())
|
|
109
|
+
pool.userData = { waterEffect: { material: effectMaterial, onFrame: () => {} } }
|
|
110
|
+
const overlay = meshWithNodeMaterial(nodeMaterial())
|
|
111
|
+
overlay.userData = { pascalExport: 'strip', mesh: pool }
|
|
112
|
+
root.add(pool, overlay)
|
|
113
|
+
expect(() => structuredClone(pool.userData)).toThrow()
|
|
114
|
+
|
|
115
|
+
const { scene } = prepareSceneForExport(root, {})
|
|
116
|
+
|
|
117
|
+
const meshes: THREE.Mesh[] = []
|
|
118
|
+
scene.traverse((object) => {
|
|
119
|
+
if ((object as THREE.Mesh).isMesh) meshes.push(object as THREE.Mesh)
|
|
120
|
+
})
|
|
121
|
+
expect(meshes).toHaveLength(1)
|
|
122
|
+
expect(meshes[0]?.userData.waterEffect).toBeUndefined()
|
|
123
|
+
expect(pool.userData.waterEffect.material).toBe(effectMaterial)
|
|
124
|
+
})
|
|
125
|
+
|
|
102
126
|
test('replaces only the cloned registered subtree with bake-only geometry', () => {
|
|
103
127
|
const restoreRegistry = nodeRegistry._snapshot()
|
|
104
128
|
try {
|
|
@@ -566,19 +590,64 @@ describe('prepareSceneForExport', () => {
|
|
|
566
590
|
expect(animations).toHaveLength(0)
|
|
567
591
|
})
|
|
568
592
|
|
|
569
|
-
test('
|
|
593
|
+
test('keeps the buildings on a hidden Site and drops only the Site ground', () => {
|
|
594
|
+
// A layout authored outside the editor can hide the root Site (the
|
|
595
|
+
// renderer ignores that flag) while every node on it stays visible. The
|
|
596
|
+
// Site's own flag must stop at the Site: it is the parcel reference, not
|
|
597
|
+
// a container the building inherits visibility from.
|
|
598
|
+
const root = new THREE.Group()
|
|
599
|
+
const siteGroup = new THREE.Group()
|
|
600
|
+
const siteGround = meshWithNodeMaterial(nodeMaterial())
|
|
601
|
+
const buildingGroup = new THREE.Group()
|
|
602
|
+
const levelGroup = new THREE.Group()
|
|
603
|
+
const itemGroup = new THREE.Group()
|
|
604
|
+
itemGroup.add(meshWithNodeMaterial(nodeMaterial()))
|
|
605
|
+
levelGroup.add(itemGroup)
|
|
606
|
+
buildingGroup.add(levelGroup)
|
|
607
|
+
siteGroup.add(buildingGroup, siteGround)
|
|
608
|
+
root.add(siteGroup)
|
|
609
|
+
|
|
610
|
+
const siteId = 'site_hidden'
|
|
611
|
+
const buildingId = 'building_on_hidden_site'
|
|
612
|
+
const levelId = 'level_on_hidden_site'
|
|
613
|
+
const itemId = 'item_on_hidden_site'
|
|
614
|
+
sceneRegistry.nodes.set(siteId, siteGroup)
|
|
615
|
+
sceneRegistry.nodes.set(buildingId, buildingGroup)
|
|
616
|
+
sceneRegistry.nodes.set(levelId, levelGroup)
|
|
617
|
+
sceneRegistry.nodes.set(itemId, itemGroup)
|
|
618
|
+
const node = (id: string, type: string, parentId: string | null, visible: boolean) =>
|
|
619
|
+
({ object: 'node', id, type, parentId, visible }) as unknown as AnyNode
|
|
620
|
+
const nodes: Record<string, AnyNode> = {
|
|
621
|
+
[siteId]: node(siteId, 'site', null, false),
|
|
622
|
+
[buildingId]: node(buildingId, 'building', siteId, true),
|
|
623
|
+
[levelId]: node(levelId, 'level', buildingId, true),
|
|
624
|
+
[itemId]: node(itemId, 'item', levelId, true),
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const { scene } = prepareSceneForExport(root, nodes)
|
|
628
|
+
|
|
629
|
+
const meshes: THREE.Mesh[] = []
|
|
630
|
+
scene.traverse((object) => {
|
|
631
|
+
if ((object as THREE.Mesh).isMesh) meshes.push(object as THREE.Mesh)
|
|
632
|
+
})
|
|
633
|
+
expect(scene.getObjectByName(buildingId)).toBeDefined()
|
|
634
|
+
expect(scene.getObjectByName(itemId)).toBeDefined()
|
|
635
|
+
expect(meshes).toHaveLength(1)
|
|
636
|
+
expect(meshes[0]?.parent?.name).toBe(itemId)
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
test('keeps the nodes a hidden Site hosts, declared or detached, unless hidden themselves', async () => {
|
|
570
640
|
const restoreRegistry = nodeRegistry._snapshot()
|
|
571
641
|
try {
|
|
572
|
-
const kind = 'test:
|
|
642
|
+
const kind = 'test:hidden-site-host'
|
|
573
643
|
const childId = 'detached_site_child'
|
|
574
644
|
const descendantId = 'detached_site_descendant'
|
|
575
645
|
const explicitId = 'explicit_site_child'
|
|
576
|
-
const
|
|
646
|
+
const hiddenChildId = 'hidden_site_child'
|
|
577
647
|
const hiddenSite = SiteNode.parse({
|
|
578
648
|
visible: false,
|
|
579
|
-
children: [childId, explicitId],
|
|
649
|
+
children: [childId, explicitId, hiddenChildId],
|
|
580
650
|
})
|
|
581
|
-
const visibleSite = SiteNode.parse({ visible: true })
|
|
582
651
|
registerNode({
|
|
583
652
|
kind,
|
|
584
653
|
schemaVersion: 1,
|
|
@@ -591,17 +660,19 @@ describe('prepareSceneForExport', () => {
|
|
|
591
660
|
} as AnyNodeDefinition)
|
|
592
661
|
const nodes = {
|
|
593
662
|
[hiddenSite.id]: hiddenSite,
|
|
594
|
-
[visibleSite.id]: visibleSite,
|
|
595
663
|
[childId]: { id: childId, type: kind, parentId: null, visible: true },
|
|
596
664
|
[descendantId]: { id: descendantId, type: kind, parentId: childId, visible: true },
|
|
597
|
-
[explicitId]: { id: explicitId, type: kind, parentId:
|
|
598
|
-
[
|
|
665
|
+
[explicitId]: { id: explicitId, type: kind, parentId: hiddenSite.id, visible: true },
|
|
666
|
+
[hiddenChildId]: { id: hiddenChildId, type: kind, parentId: hiddenSite.id, visible: false },
|
|
599
667
|
} as unknown as Record<string, AnyNode>
|
|
600
|
-
const allIds = [childId, descendantId, explicitId,
|
|
668
|
+
const allIds = [childId, descendantId, explicitId, hiddenChildId]
|
|
601
669
|
const root = new THREE.Group()
|
|
602
|
-
|
|
670
|
+
const siteObject = new THREE.Group()
|
|
671
|
+
root.add(siteObject)
|
|
672
|
+
sceneRegistry.nodes.set(hiddenSite.id, siteObject)
|
|
673
|
+
for (const id of allIds) {
|
|
603
674
|
const object = new THREE.Group()
|
|
604
|
-
|
|
675
|
+
siteObject.add(object)
|
|
605
676
|
sceneRegistry.nodes.set(id, object)
|
|
606
677
|
}
|
|
607
678
|
const exportedIds = async (onlyVisible?: boolean) => {
|
|
@@ -613,12 +684,8 @@ describe('prepareSceneForExport', () => {
|
|
|
613
684
|
}
|
|
614
685
|
}
|
|
615
686
|
|
|
616
|
-
expect(await exportedIds()).toEqual([
|
|
687
|
+
expect(await exportedIds()).toEqual([childId, descendantId, explicitId])
|
|
617
688
|
expect(await exportedIds(false)).toEqual(allIds)
|
|
618
|
-
nodes[hiddenSite.id] = { ...hiddenSite, visible: true }
|
|
619
|
-
expect(await exportedIds()).toEqual(allIds)
|
|
620
|
-
nodes[hiddenSite.id] = hiddenSite
|
|
621
|
-
expect(await exportedIds()).toEqual([explicitId, unownedId])
|
|
622
689
|
} finally {
|
|
623
690
|
restoreRegistry()
|
|
624
691
|
}
|
|
@@ -1685,6 +1752,21 @@ async function withCanvasCapture(
|
|
|
1685
1752
|
putImageData: (image: ImageData) => {
|
|
1686
1753
|
pixelsByCanvas.set(canvas, new Uint8ClampedArray(image.data))
|
|
1687
1754
|
},
|
|
1755
|
+
fillRect: (_x: number, _y: number, width: number, height: number) => {
|
|
1756
|
+
pixelsByCanvas.set(canvas, new Uint8ClampedArray(width * height * 4).fill(255))
|
|
1757
|
+
},
|
|
1758
|
+
drawImage: (image: HTMLCanvasElement) => {
|
|
1759
|
+
const pixels = pixelsByCanvas.get(image)
|
|
1760
|
+
if (!pixels) throw new Error('Source canvas has no captured pixels')
|
|
1761
|
+
pixelsByCanvas.set(canvas, new Uint8ClampedArray(pixels))
|
|
1762
|
+
},
|
|
1763
|
+
getImageData: (_x: number, _y: number, width: number, height: number) =>
|
|
1764
|
+
({
|
|
1765
|
+
colorSpace: 'srgb',
|
|
1766
|
+
data: pixelsByCanvas.get(canvas) ?? new Uint8ClampedArray(width * height * 4),
|
|
1767
|
+
height,
|
|
1768
|
+
width,
|
|
1769
|
+
}) as ImageData,
|
|
1688
1770
|
}),
|
|
1689
1771
|
} as unknown as HTMLCanvasElement
|
|
1690
1772
|
return canvas
|
|
@@ -1702,3 +1784,170 @@ async function withCanvasCapture(
|
|
|
1702
1784
|
else delete globals.document
|
|
1703
1785
|
}
|
|
1704
1786
|
}
|
|
1787
|
+
|
|
1788
|
+
describe('normal maps in async export preparation', () => {
|
|
1789
|
+
test('decompresses a compressed normal map through the provided decompressor before baking its scale', async () => {
|
|
1790
|
+
await withCanvasCapture(async () => {
|
|
1791
|
+
const root = new THREE.Group()
|
|
1792
|
+
const compressed = new THREE.CompressedTexture([], 2, 2)
|
|
1793
|
+
compressed.name = 'NormalGL_3a132c5f'
|
|
1794
|
+
const material = nodeMaterial({ normalMap: compressed }) as THREE.MeshStandardMaterial
|
|
1795
|
+
material.normalScale.set(0.5, 0.5)
|
|
1796
|
+
root.add(meshWithNodeMaterial(material))
|
|
1797
|
+
let calls = 0
|
|
1798
|
+
|
|
1799
|
+
const prepared = await prepareSceneForExportAsync(
|
|
1800
|
+
root,
|
|
1801
|
+
{},
|
|
1802
|
+
{
|
|
1803
|
+
decompressTexture: async (texture) => {
|
|
1804
|
+
calls += 1
|
|
1805
|
+
expect(texture.name).toBe('NormalGL_3a132c5f')
|
|
1806
|
+
return new THREE.DataTexture(
|
|
1807
|
+
new Uint8Array(
|
|
1808
|
+
[128, 128, 255, 255].concat(
|
|
1809
|
+
[128, 128, 255, 255],
|
|
1810
|
+
[128, 128, 255, 255],
|
|
1811
|
+
[128, 128, 255, 255],
|
|
1812
|
+
),
|
|
1813
|
+
),
|
|
1814
|
+
2,
|
|
1815
|
+
2,
|
|
1816
|
+
)
|
|
1817
|
+
},
|
|
1818
|
+
},
|
|
1819
|
+
)
|
|
1820
|
+
|
|
1821
|
+
const exported = (prepared.scene.children[0] as THREE.Mesh)
|
|
1822
|
+
.material as THREE.MeshStandardMaterial
|
|
1823
|
+
expect(calls).toBe(1)
|
|
1824
|
+
expect((exported.normalMap as { isCanvasTexture?: boolean }).isCanvasTexture).toBe(true)
|
|
1825
|
+
expect(exported.normalScale.toArray()).toEqual([1, 1])
|
|
1826
|
+
expect(material.normalMap).toBe(compressed)
|
|
1827
|
+
expect(material.normalScale.toArray()).toEqual([0.5, 0.5])
|
|
1828
|
+
})
|
|
1829
|
+
})
|
|
1830
|
+
|
|
1831
|
+
test('reference mode keeps a stamped normal-map placeholder and its scale for the packer', async () => {
|
|
1832
|
+
await withCanvasCapture(async () => {
|
|
1833
|
+
const root = new THREE.Group()
|
|
1834
|
+
const stamped = new THREE.CompressedTexture([], 4, 4)
|
|
1835
|
+
stamped.userData.pascalTextureRef = {
|
|
1836
|
+
v: 1,
|
|
1837
|
+
kind: 'library-material',
|
|
1838
|
+
src: `${STORAGE_ORIGIN}/storage/v1/object/public/materials/user/material/oak_normal_512.ktx2`,
|
|
1839
|
+
map: 'normal',
|
|
1840
|
+
colorSpace: 'linear',
|
|
1841
|
+
}
|
|
1842
|
+
const material = nodeMaterial({ normalMap: stamped }) as THREE.MeshStandardMaterial
|
|
1843
|
+
material.normalScale.set(1, -1)
|
|
1844
|
+
root.add(meshWithNodeMaterial(material))
|
|
1845
|
+
|
|
1846
|
+
const prepared = await prepareSceneForExportAsync(
|
|
1847
|
+
root,
|
|
1848
|
+
{},
|
|
1849
|
+
{
|
|
1850
|
+
textures: 'reference',
|
|
1851
|
+
purpose: 'viewer',
|
|
1852
|
+
decompressTexture: async () => {
|
|
1853
|
+
throw new Error('by-reference placeholders must not be decompressed')
|
|
1854
|
+
},
|
|
1855
|
+
},
|
|
1856
|
+
)
|
|
1857
|
+
|
|
1858
|
+
const exported = (prepared.scene.children[0] as THREE.Mesh)
|
|
1859
|
+
.material as THREE.MeshStandardMaterial
|
|
1860
|
+
expect(exported.normalMap?.userData.pascalTextureRef).toEqual(
|
|
1861
|
+
stamped.userData.pascalTextureRef,
|
|
1862
|
+
)
|
|
1863
|
+
expect(exported.normalScale.toArray()).toEqual([1, -1])
|
|
1864
|
+
})
|
|
1865
|
+
})
|
|
1866
|
+
})
|
|
1867
|
+
|
|
1868
|
+
describe('portable glass', () => {
|
|
1869
|
+
test('see-through untextured surfaces become transmission glass in portable exports only', async () => {
|
|
1870
|
+
const root = new THREE.Group()
|
|
1871
|
+
const glass = new MeshStandardNodeMaterial({
|
|
1872
|
+
color: '#3d9ed4',
|
|
1873
|
+
transparent: true,
|
|
1874
|
+
opacity: 0.3,
|
|
1875
|
+
roughness: 0.1,
|
|
1876
|
+
})
|
|
1877
|
+
const tintedPlastic = new MeshStandardNodeMaterial({
|
|
1878
|
+
color: '#3d9ed4',
|
|
1879
|
+
transparent: true,
|
|
1880
|
+
opacity: 0.8,
|
|
1881
|
+
})
|
|
1882
|
+
root.add(meshWithNodeMaterial(glass), meshWithNodeMaterial(tintedPlastic))
|
|
1883
|
+
|
|
1884
|
+
const portable = await prepareSceneForExportAsync(root, {})
|
|
1885
|
+
const [exportedGlass, exportedPlastic] = portable.scene.children.map(
|
|
1886
|
+
(child) => (child as THREE.Mesh).material as THREE.MeshPhysicalMaterial,
|
|
1887
|
+
)
|
|
1888
|
+
expect(exportedGlass!.isMeshPhysicalMaterial).toBe(true)
|
|
1889
|
+
expect(exportedGlass!.transmission).toBe(1)
|
|
1890
|
+
expect(exportedGlass!.transparent).toBe(false)
|
|
1891
|
+
expect(exportedGlass!.opacity).toBe(1)
|
|
1892
|
+
expect(exportedGlass!.roughness).toBeCloseTo(0.1)
|
|
1893
|
+
// 30% authored opacity keeps 30% of the (linear) tint.
|
|
1894
|
+
const tint = new THREE.Color('#3d9ed4')
|
|
1895
|
+
expect(exportedGlass!.color.r).toBeCloseTo(tint.r + (1 - tint.r) * 0.7, 4)
|
|
1896
|
+
expect(exportedGlass!.color.b).toBeCloseTo(tint.b + (1 - tint.b) * 0.7, 4)
|
|
1897
|
+
expect(exportedPlastic!.isMeshPhysicalMaterial).toBeUndefined()
|
|
1898
|
+
expect(exportedPlastic!.transparent).toBe(true)
|
|
1899
|
+
|
|
1900
|
+
const viewer = prepareSceneForExport(root, {}, { purpose: 'viewer' })
|
|
1901
|
+
const kept = (viewer.scene.children[0] as THREE.Mesh).material as THREE.MeshStandardMaterial
|
|
1902
|
+
expect((kept as { isMeshPhysicalMaterial?: boolean }).isMeshPhysicalMaterial).toBeUndefined()
|
|
1903
|
+
expect(kept.transparent).toBe(true)
|
|
1904
|
+
expect(kept.opacity).toBeCloseTo(0.3)
|
|
1905
|
+
|
|
1906
|
+
// USDZ has no transmission: the same prep hands glass its opacity back.
|
|
1907
|
+
const usdz = createUsdzScene(portable.scene)
|
|
1908
|
+
const usdzGlass = (usdz.children[0] as THREE.Mesh).material as THREE.MeshPhysicalMaterial
|
|
1909
|
+
expect(usdzGlass.transmission).toBe(0)
|
|
1910
|
+
expect(usdzGlass.transparent).toBe(true)
|
|
1911
|
+
expect(usdzGlass.opacity).toBeCloseTo(0.3)
|
|
1912
|
+
expect(exportedGlass!.transmission).toBe(1)
|
|
1913
|
+
})
|
|
1914
|
+
})
|
|
1915
|
+
|
|
1916
|
+
describe('portable clips', () => {
|
|
1917
|
+
function swingDoorScene() {
|
|
1918
|
+
const root = new THREE.Group()
|
|
1919
|
+
const doorGroup = new THREE.Group()
|
|
1920
|
+
const leaf = new THREE.Group()
|
|
1921
|
+
leaf.userData.pascalSwingLeaf = { axis: 'y', openRotationY: Math.PI / 2 }
|
|
1922
|
+
leaf.add(meshWithNodeMaterial(nodeMaterial()))
|
|
1923
|
+
doorGroup.add(leaf)
|
|
1924
|
+
root.add(doorGroup)
|
|
1925
|
+
const doorId = 'door_portable'
|
|
1926
|
+
sceneRegistry.nodes.set(doorId, doorGroup)
|
|
1927
|
+
const nodes: Record<string, AnyNode> = {
|
|
1928
|
+
[doorId]: { object: 'node', id: doorId, type: 'door', name: 'Door' } as unknown as AnyNode,
|
|
1929
|
+
}
|
|
1930
|
+
return { root, nodes, doorId }
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
test('GLB downloads keep door clips when asked, USDZ passes none', async () => {
|
|
1934
|
+
const { root, nodes, doorId } = swingDoorScene()
|
|
1935
|
+
|
|
1936
|
+
const glb = await prepareSceneForExportAsync(root, nodes, { animations: 'keep' })
|
|
1937
|
+
expect(glb.animations).toHaveLength(1)
|
|
1938
|
+
expect(glb.animations[0]!.name).toBe(`${doorId}: open`)
|
|
1939
|
+
const track = glb.animations[0]!.tracks[0]!
|
|
1940
|
+
const targetUuid = track.name.split('.')[0]
|
|
1941
|
+
expect(glb.scene.getObjectByProperty('uuid', targetUuid)).toBeDefined()
|
|
1942
|
+
expect(glb.scene.getObjectByProperty('name', doorId)?.userData.clips).toEqual([
|
|
1943
|
+
`${doorId}: open`,
|
|
1944
|
+
])
|
|
1945
|
+
|
|
1946
|
+
const usdz = await prepareSceneForExportAsync(root, nodes, { animations: 'none' })
|
|
1947
|
+
expect(usdz.animations).toEqual([])
|
|
1948
|
+
expect(usdz.scene.getObjectByProperty('name', doorId)?.userData.clips).toBeUndefined()
|
|
1949
|
+
|
|
1950
|
+
const legacy = await prepareSceneForExportAsync(root, nodes)
|
|
1951
|
+
expect(legacy.animations).toEqual([])
|
|
1952
|
+
})
|
|
1953
|
+
})
|