File size: 5,096 Bytes
39ff632
 
 
 
 
 
324b9a7
39ff632
324b9a7
 
 
39ff632
 
 
324b9a7
39ff632
 
324b9a7
 
 
 
 
 
 
 
 
 
39ff632
324b9a7
39ff632
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324b9a7
 
 
 
39ff632
 
 
324b9a7
 
 
 
 
39ff632
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324b9a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39ff632
324b9a7
 
 
39ff632
324b9a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39ff632
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// node_smoke.mjs — headless scene-graph smoke test for a bundled factory.
//
// Asserts the generated artifact actually executes and produces a non-empty
// THREE.Group — without needing WebGL (three's scene graph is DOM-free; the
// generated factory falls back to flat materials when `document` is absent).
//
// Usage: node scripts/node_smoke.mjs <model.bundle.js> [compile-spec.json]
import * as THREE from 'three'; // only used for Box3/Vector3 helpers
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';

const bundlePath = process.argv[2];
if (!bundlePath) {
  console.error('usage: node node_smoke.mjs <model.bundle.js> [compile-spec.json]');
  process.exit(2);
}
const specPath = process.argv[3];
let expectedComponents = null;
if (specPath) {
  const spec = JSON.parse(await readFile(specPath, 'utf8'));
  if (!Array.isArray(spec.componentTree) || spec.componentTree.length === 0) {
    console.error('FAIL: supplied spec has no componentTree');
    process.exit(1);
  }
  expectedComponents = spec.componentTree;
}

const mod = await import(pathToFileURL(resolve(bundlePath)).href);
const makeModel = mod.makeModel
  || Object.values(mod).find((f) => typeof f === 'function' && /Model$/.test(f.name));
if (!makeModel) {
  console.error('FAIL: no create*Model export in bundle');
  process.exit(1);
}

const group = makeModel({ textureSize: 128, qualityPriority: 'balanced' });
// Duck-typing, not instanceof: the bundle carries its own THREE copy, so
// instanceof against node_modules' THREE would always fail.
if (!group || group.isGroup !== true) {
  console.error('FAIL: factory did not return a THREE.Group');
  process.exit(1);
}

let meshes = 0;
group.traverse((node) => { if (node.isMesh) meshes += 1; });
const size = new THREE.Box3().setFromObject(group).getSize(new THREE.Vector3()).toArray();
const runtime = group.userData && group.userData.sculptRuntime;
const geometryTypes = {};
const componentBounds = {};
const inspectedIds = expectedComponents
  ? expectedComponents.map((component) => component.id)
  : ['body', 'handle'];
for (const id of inspectedIds) {
  const mesh = runtime && runtime.meshes && runtime.meshes[id];
  if (!mesh) continue;
  geometryTypes[id] = mesh.geometry && mesh.geometry.type;
  mesh.geometry.computeBoundingBox();
  const localSize = mesh.geometry.boundingBox.getSize(new THREE.Vector3());
  componentBounds[id] = localSize.toArray()
    .map((value, axis) => value * Math.abs(mesh.scale.getComponent(axis)))
    .map((value) => Number(value.toFixed(4)));
}

const report = {
  type: group.type,
  children: group.children.length,
  meshes,
  runtimeNodes: runtime && runtime.nodes ? Object.keys(runtime.nodes).length : 0,
  boundingBox: size.map((v) => Number(v.toFixed(4))),
  geometryTypes,
  componentBounds,
};

const failures = [];
if (group.children.length < 1) failures.push('empty scene graph');
if (meshes < 1) failures.push('no meshes');
if (!size.every(Number.isFinite)) failures.push('non-finite bounding box');
if (!runtime || !runtime.nodes) failures.push('missing userData.sculptRuntime');
const expectedGeometry = {
  box: 'BoxGeometry',
  sphere: 'SphereGeometry',
  ellipsoid: 'SphereGeometry',
  cylinder: 'CylinderGeometry',
  cone: 'ConeGeometry',
  capsule: 'CapsuleGeometry',
  torus: 'TorusGeometry',
  'plane-card': 'PlaneGeometry',
};
const near = (actual, expected) =>
  Math.abs(actual - expected) <= Math.max(0.002, Math.abs(expected) * 0.03);
const assertComponent = (component) => {
  const { id, primitive, dimensions } = component;
  const mesh = runtime && runtime.meshes && runtime.meshes[id];
  if (!mesh) {
    failures.push(`missing runtime mesh for component ${id}`);
    return;
  }
  const geometryName = expectedGeometry[primitive];
  if (geometryName && geometryTypes[id] !== geometryName) {
    failures.push(`${id} is ${geometryTypes[id]}, expected ${geometryName}`);
  }
  const expected = dimensions && [dimensions.width, dimensions.height, dimensions.depth];
  const actual = componentBounds[id];
  if (!expected || !expected.every((value) => Number.isFinite(value) && value > 0)) {
    failures.push(`invalid expected dimensions for ${id}`);
    return;
  }
  const axes = primitive === 'plane-card' ? [0, 1] : [0, 1, 2];
  if (!actual || !axes.every((axis) => near(actual[axis], expected[axis]))) {
    failures.push(
      `${id} dimensions disagree with spec: actual=${JSON.stringify(actual)} expected=${JSON.stringify(expected)}`,
    );
  }
};

if (expectedComponents) {
  expectedComponents.forEach(assertComponent);
} else if (runtime && runtime.meshes && runtime.meshes.body && runtime.meshes.handle) {
  assertComponent({
    id: 'body', primitive: 'cylinder',
    dimensions: { width: 0.08, height: 0.1, depth: 0.08 },
  });
  assertComponent({
    id: 'handle', primitive: 'torus',
    dimensions: { width: 0.04, height: 0.06, depth: 0.015 },
  });
}

console.log(JSON.stringify(report));
if (failures.length) {
  console.error(`FAIL: ${failures.join('; ')}`);
  process.exit(1);
}
console.log('OK');