|
- import assert from 'node:assert/strict'
- import { readFile } from 'node:fs/promises'
- import { createRequire } from 'node:module'
- import { pathToFileURL } from 'node:url'
- import test from 'node:test'
- import ts from 'typescript'
-
- const requireWeb = createRequire(new URL('../../unreal_tran_web/package.json', import.meta.url))
- const threeUrl = pathToFileURL(requireWeb.resolve('three')).href
- const THREE = await import(threeUrl)
- const source = await readFile(new URL('../../unreal_tran_web/src/features/scene-legacy/modelPlacement.ts', import.meta.url), 'utf8')
- const output = ts.transpileModule(source, {
- compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
- reportDiagnostics: true,
- })
- assert.deepEqual(output.diagnostics, [])
- const compiled = output.outputText.replace(/from 'three'/g, `from '${threeUrl}'`)
- const { fitSceneModel, refreshSceneModelBounds, parseSceneScale, formatSceneScale } = await import(
- `data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`,
- )
-
- function close(actual, expected, tolerance = 1e-8) {
- assert.ok(Math.abs(actual - expected) <= tolerance, `${actual} != ${expected}`)
- }
-
- function skinFixture() {
- const root = new THREE.Group()
- const bone = new THREE.Bone()
- const geometry = new THREE.BoxGeometry(2, 4, 10)
- geometry.translate(0, 2, 0)
- const count = geometry.getAttribute('position').count
- const indices = new Uint16Array(count * 4)
- const weights = new Float32Array(count * 4)
- for (let i = 0; i < count; i += 1) weights[i * 4] = 1
- geometry.setAttribute('skinIndex', new THREE.Uint16BufferAttribute(indices, 4))
- geometry.setAttribute('skinWeight', new THREE.Float32BufferAttribute(weights, 4))
- const mesh = new THREE.SkinnedMesh(geometry, new THREE.MeshBasicMaterial())
- root.add(bone, mesh)
- root.updateMatrixWorld(true)
- mesh.bind(new THREE.Skeleton([bone]))
- // The published model has already been normalized and may be saved at an
- // animation pose. This is the same stale matrix/cache state as loaded GLTF.
- root.scale.setScalar(0.1)
- root.position.set(1, 2, 3)
- bone.position.y = 5
- return { root, bone, mesh }
- }
-
- test('fresh skinned placement reaches the requested size and floor after authored scale/pose replay', () => {
- const { root, mesh } = skinFixture()
- // Populate the wrong cached skin box just as the previous import path did.
- const stale = new THREE.Box3().setFromObject(root).getSize(new THREE.Vector3())
- assert.ok(stale.z < 1)
- const box = fitSceneModel(root, 6)
- const size = box.getSize(new THREE.Vector3())
- close(size.x, 1.2)
- close(size.y, 2.4)
- close(size.z, 6)
- close(root.scale.x, 0.6)
- close(box.min.y, 0)
- close(box.getCenter(new THREE.Vector3()).x, 0)
- close(box.getCenter(new THREE.Vector3()).z, 0)
- const vertex = new THREE.Vector3()
- for (let i = 0; i < mesh.geometry.getAttribute('position').count; i += 1) {
- mesh.getVertexPosition(i, vertex)
- assert.ok(mesh.boundingSphere.containsPoint(vertex))
- }
- })
-
- test('fresh static placement preserves authored non-uniform proportions', () => {
- const root = new THREE.Group()
- root.add(new THREE.Mesh(new THREE.BoxGeometry(2, 4, 10), new THREE.MeshBasicMaterial()))
- root.scale.set(2, 0.5, 3)
- root.position.set(-4, 8, 6)
- const box = fitSceneModel(root, 6)
- root.scale.toArray().forEach((value, i) => close(value, [0.4, 0.1, 0.6][i]))
- close(box.getSize(new THREE.Vector3()).z, 6)
- close(box.min.y, 0)
- })
-
- test('restoring skin caches keeps existing scene transforms and authored bone poses unchanged', () => {
- const { root, bone, mesh } = skinFixture()
- refreshSceneModelBounds(root)
- root.position.set(-7.5, 1.06, -0.45)
- root.rotation.set(0.1, 0.4, -0.2)
- root.scale.set(6, 4, 2)
- bone.position.set(1, 17, 3)
- const before = { position: root.position.toArray(), rotation: root.rotation.toArray(), scale: root.scale.toArray(), bone: bone.position.toArray() }
- const staleBox = mesh.boundingBox.clone()
- const box = refreshSceneModelBounds(root)
- assert.deepEqual({ position: root.position.toArray(), rotation: root.rotation.toArray(), scale: root.scale.toArray(), bone: bone.position.toArray() }, before)
- assert.notDeepEqual(mesh.boundingBox, staleBox)
- const vertex = new THREE.Vector3()
- const generousBox = box.clone().expandByScalar(1e-8)
- for (let i = 0; i < mesh.geometry.getAttribute('position').count; i += 1) {
- mesh.getVertexPosition(i, vertex)
- assert.ok(vertex.distanceTo(mesh.boundingSphere.center) <= mesh.boundingSphere.radius + 1e-8)
- assert.ok(generousBox.containsPoint(vertex.applyMatrix4(mesh.matrixWorld)))
- }
- })
-
- test('declared terrain ground datum stays on the scene floor', () => {
- const root = new THREE.Group()
- root.add(new THREE.Mesh(new THREE.BoxGeometry(8, 4, 10), new THREE.MeshBasicMaterial()))
- fitSceneModel(root, 20, -1.5)
- close(-1.5 * root.scale.y + root.position.y, 0)
- })
-
- test('scale input accepts real GLTF normalization factors below 0.05', () => {
- close(parseSceneScale('0.0361637593257', 6), 0.0361637593257)
- close(parseSceneScale('0.000001', 6), 0.000001)
- close(parseSceneScale('1e-9', 6), 0.000001)
- for (const value of ['', ' ', 'invalid', 'NaN', 'Infinity', '0', '-2']) {
- close(parseSceneScale(value, 0.036164), 0.036164)
- }
- })
-
- test('scale display preserves small factors and trims unnecessary zeros', () => {
- assert.equal(formatSceneScale(0.0361637593257), '0.03616376')
- assert.equal(formatSceneScale(0.000001), '0.000001')
- assert.equal(formatSceneScale(1), '1')
- assert.equal(formatSceneScale(0.6), '0.6')
- })
|