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() })