|
- 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 webSource = new URL('../../unreal_tran_web/src/features/', import.meta.url)
- const placementSource = await readFile(new URL('scene-legacy/modelPlacement.ts', webSource), 'utf8')
- const placementJavaScript = ts.transpileModule(placementSource, {
- compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
- }).outputText.replace(/from 'three'/g, `from '${threeUrl}'`)
- const placementUrl = `data:text/javascript;base64,${Buffer.from(placementJavaScript).toString('base64')}`
- const { refreshSceneModelBounds } = await import(placementUrl)
-
- // Exercise the actual Three functions without constructing the browser-only
- // editor UI or opening its storage. Metadata normalization is irrelevant to
- // these geometry checks and remains an identity adapter in this harness.
- async function renderingFunctions(relativePath) {
- const source = await readFile(new URL(relativePath, webSource), 'utf8')
- const ast = ts.createSourceFile(relativePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS)
- const functionNames = new Set(['fitObject', 'applyPublishedSceneTransform', 'buildTrainingContentSnapshot', 'resolveTrainingEnvironmentUrl'])
- const functions = ast.statements
- .filter(node => ts.isFunctionDeclaration(node) && functionNames.has(node.name?.text))
- .map(node => node.getText(ast).replace(/^export\s+/, ''))
- assert.equal(functions.length, functionNames.size)
- const editor = ast.statements.find(node => ts.isClassDeclaration(node) && node.name?.text === 'TrainingSceneEditor')
- const highlight = editor.members.find(node => node.name?.getText(ast) === 'highlightTarget')
- const setProject = editor.members.find(node => node.name?.getText(ast) === 'setProject')
- const body = `
- import * as THREE from '${threeUrl}';
- import { refreshSceneModelBounds } from '${placementUrl}';
- const normalizeTrainingModelPartTargets = item => item.modelPartTargets || [];
- const normalizeTrainingPartOverrides = item => item || {};
- const normalizeSceneRendering = item => item || {};
- ${functions.join('\n')}
- class TrainingSceneEditor { ${highlight.getText(ast)} ${setProject.getText(ast)} }
- export { fitObject, applyPublishedSceneTransform, TrainingSceneEditor };
- `
- return import(`data:text/javascript;base64,${Buffer.from(body).toString('base64')}`)
- }
-
- 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]))
- root.scale.setScalar(0.1)
- root.position.set(1, 2, 3)
- bone.position.y = 5
- return { root, bone, mesh }
- }
-
- function close(actual, expected) {
- assert.ok(Math.abs(actual - expected) < 1e-7, `${actual} != ${expected}`)
- }
-
- function assertSkinFitsCachedBounds(mesh) {
- const vertex = new THREE.Vector3()
- const worldBox = new THREE.Box3().setFromObject(mesh).expandByScalar(1e-7)
- 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-7)
- assert.ok(worldBox.containsPoint(vertex.applyMatrix4(mesh.matrixWorld)))
- }
- }
-
- for (const runtime of ['guide-legacy/demo-src', 'virtual-training-legacy/upstream']) {
- const { fitObject, applyPublishedSceneTransform, TrainingSceneEditor } = await renderingFunctions(`${runtime}/training-scene-editor.js`)
-
- test(`${runtime}: fitting an authored skinned model uses its current pose and real proportions`, () => {
- const { root, mesh } = skinFixture()
- new THREE.Box3().setFromObject(root) // stale cached GLTF bounds
- fitObject(root, { maxSize: 6, center: [0, 0, 0] })
- const box = new THREE.Box3().setFromObject(root)
- const size = box.getSize(new THREE.Vector3())
- close(size.x, 1.2)
- close(size.y, 2.4)
- close(size.z, 6)
- close(box.min.y, 0)
- close(root.scale.x, 0.6)
- assertSkinFitsCachedBounds(mesh)
- })
-
- test(`${runtime}: published scene restore preserves saved scale and refreshes skin culling bounds`, () => {
- const { root, bone, mesh } = skinFixture()
- refreshSceneModelBounds(root)
- const item = {
- position: [-7.5, 1.06, -0.45], rotation: [0.1, 0.4, -0.2],
- scale: [0.0361637593257, 0.06, 0.1], visible: true,
- }
- bone.position.set(1, 17, 3)
- applyPublishedSceneTransform(root, item)
- assert.deepEqual(root.position.toArray(), item.position)
- assert.deepEqual(root.rotation.toArray().slice(0, 3), item.rotation)
- assert.deepEqual(root.scale.toArray(), item.scale)
- assert.deepEqual(bone.position.toArray(), [1, 17, 3])
- assertSkinFitsCachedBounds(mesh)
- })
-
- test(`${runtime}: selecting and clearing a skinned training target keeps bounds consistent`, () => {
- const { root, mesh } = skinFixture()
- refreshSceneModelBounds(root)
- const context = {
- targetMaterials: new Map([['fox', []]]), targetObjects: new Map([['fox', root]]),
- targetScales: new Map([['fox', root.scale.clone()]]), publishedSceneRoot: root,
- }
- TrainingSceneEditor.prototype.highlightTarget.call(context, 'fox')
- close(root.scale.x, 0.104)
- assertSkinFitsCachedBounds(mesh)
- TrainingSceneEditor.prototype.highlightTarget.call(context, '')
- close(root.scale.x, 0.1)
- assertSkinFitsCachedBounds(mesh)
- })
-
- test(`${runtime}: ground datum follows the final authored root scale`, () => {
- const { root } = skinFixture()
- root.position.set(0, 0, 0)
- fitObject(root, { maxSize: 6, center: [0, 0, 0], groundLevelY: -1.5 })
- close(root.position.y - 1.5 * root.scale.y, 0)
- })
-
- if (runtime === 'guide-legacy/demo-src') {
- test('API-backed scene bind reports a failed load and permits a same-snapshot retry', async () => {
- const failure = new Error('controlled asset unavailable')
- let calls = 0
- const context = {
- environmentUrl: '', environmentModel: null,
- workshop: { visible: false }, machine: { visible: false },
- publishedSceneSignature: '', publishedSceneObjects: [],
- applyRendering() {}, async loadEnvironment() {},
- async loadPublishedSceneSnapshot() {
- calls += 1
- if (calls === 1) return { count: 0, objects: [], error: failure }
- this.publishedSceneObjects = [new THREE.Group()]
- return { count: 1, objects: this.publishedSceneObjects }
- },
- }
- const project = { scenario: { publishedSceneId: '160', sceneResource: { snapshot: { objects: [{ sceneId: 'fox' }] } } } }
- const failed = await TrainingSceneEditor.prototype.setProject.call(context, project)
- assert.equal(failed.error, failure)
- assert.equal(failed.snapshotLoaded, false)
- const recovered = await TrainingSceneEditor.prototype.setProject.call(context, project)
- assert.equal(calls, 2)
- assert.equal(recovered.error, undefined)
- assert.equal(recovered.snapshotLoaded, true)
- assert.equal(recovered.objectCount, 1)
- })
- }
- }
-
- test('cached published skins retain independent bones after each scene instance is transformed', async () => {
- const { loadCachedPublishedModel, clearTrainingModelCache } = await import(new URL('guide-legacy/demo-src/training-model-cache.js', webSource))
- clearTrainingModelCache()
- const fixture = skinFixture()
- const canonical = { scene: fixture.root, scenes: [fixture.root], animations: [] }
- const loader = { loadAsync: async () => canonical }
- const first = await loadCachedPublishedModel('fixture://fox.glb', loader)
- const second = await loadCachedPublishedModel('fixture://fox.glb', loader)
- let firstSkin, secondSkin
- first.scene.traverse(node => { if (node.isSkinnedMesh) firstSkin = node })
- second.scene.traverse(node => { if (node.isSkinnedMesh) secondSkin = node })
- assert.notEqual(firstSkin.skeleton.bones[0], secondSkin.skeleton.bones[0])
- assert.notEqual(firstSkin.skeleton.bones[0], fixture.bone)
- firstSkin.skeleton.bones[0].position.y = 17
- first.scene.scale.setScalar(0.0361637593257)
- refreshSceneModelBounds(first.scene)
- refreshSceneModelBounds(second.scene)
- close(secondSkin.skeleton.bones[0].position.y, 5)
- assertSkinFitsCachedBounds(firstSkin)
- assertSkinFitsCachedBounds(secondSkin)
- clearTrainingModelCache()
- })
|