@instruktlabs/kiln 1.2.2 → 1.2.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +74 -0
- package/README.md +7 -0
- package/dist/agent-run.mjs +6392 -1477
- package/dist/build.json +23 -23
- package/dist/cli.mjs +8453 -3407
- package/dist/evaluator-worker.mjs +16608 -12387
- package/dist/mcp-engine.mjs +6746 -1725
- package/dist/mcp-server.mjs +4 -4
- package/dist/viewer/app.js +203 -199
- package/dist/viewer/chat.html +209 -205
- package/dist/viewer/style.css +10 -0
- package/docs/atlas-provenance.md +23 -0
- package/docs/community-exporter.md +27 -0
- package/docs/export-profiles.md +20 -0
- package/docs/material-library.md +9 -5
- package/docs/procedural-surfaces-v3.md +276 -0
- package/docs/projects-and-live-review.md +1 -1
- package/docs/rendering.md +41 -0
- package/docs/sdk.md +53 -0
- package/docs/tools-operations.md +2 -2
- package/docs/tools.md +3 -3
- package/lib/asset-materials-node.d.ts +3 -3
- package/lib/asset-materials-node.js +29 -33
- package/lib/assets-node.d.ts +1 -1
- package/lib/assets-node.js +11 -4
- package/lib/assets.d.ts +4 -2
- package/lib/assets.js +61 -16
- package/lib/atlas-provenance.d.ts +86 -0
- package/lib/atlas-provenance.js +448 -0
- package/lib/character.d.ts +11 -2
- package/lib/character.js +90 -2
- package/lib/community-exporter.d.ts +3 -2
- package/lib/community-exporter.js +139 -3
- package/lib/discovery/helper-contracts.js +161 -7
- package/lib/discovery/helper-specs.js +49 -4
- package/lib/discovery/shapes.js +20 -7
- package/lib/engine-identity.d.ts +1 -1
- package/lib/engine-identity.js +1 -1
- package/lib/evaluator/authoring-diagnostic.d.ts +1 -1
- package/lib/evaluator/authoring-diagnostic.js +12 -0
- package/lib/evaluator/protocol.d.ts +2 -2
- package/lib/evaluator/protocol.js +16 -6
- package/lib/evaluator/subprocess.js +2 -2
- package/lib/exporter-node.js +9 -1
- package/lib/geometry-budget.d.ts +7 -0
- package/lib/geometry-budget.js +8 -0
- package/lib/gltf-texture-binding.d.ts +6 -0
- package/lib/gltf-texture-binding.js +54 -0
- package/lib/material-authoring-node.d.ts +6 -0
- package/lib/material-authoring-node.js +49 -0
- package/lib/material-authoring-schema.d.ts +871 -0
- package/lib/material-authoring-schema.js +310 -0
- package/lib/material-export.d.ts +5 -0
- package/lib/material-export.js +21 -0
- package/lib/material-library-node.d.ts +34 -8
- package/lib/material-library-node.js +691 -11
- package/lib/material-library.d.ts +3704 -4
- package/lib/material-library.js +272 -0
- package/lib/mcp-core.d.ts +1 -1
- package/lib/pinned-material-map.d.ts +33 -0
- package/lib/pinned-material-map.js +1 -0
- package/lib/portable-material-runtime.d.ts +5 -0
- package/lib/portable-material-runtime.js +160 -0
- package/lib/portable-sampler-runtime.d.ts +4 -0
- package/lib/portable-sampler-runtime.js +26 -0
- package/lib/portable-sampler.d.ts +11 -0
- package/lib/portable-sampler.js +42 -0
- package/lib/primitives.d.ts +15 -3
- package/lib/primitives.js +24 -4
- package/lib/procedural-material-v2.d.ts +2 -0
- package/lib/procedural-material-v2.js +2 -1
- package/lib/procedural-material-v3.d.ts +72 -0
- package/lib/procedural-material-v3.js +162 -0
- package/lib/procedural-texture-v2-compiler.d.ts +31 -0
- package/lib/procedural-texture-v2-compiler.js +172 -0
- package/lib/procedural-texture-v3-contract.d.ts +125 -0
- package/lib/procedural-texture-v3-contract.js +333 -0
- package/lib/procedural-texture-v3.d.ts +12 -0
- package/lib/procedural-texture-v3.js +165 -0
- package/lib/procedural-texture.d.ts +9 -31
- package/lib/procedural-texture.js +43 -170
- package/lib/project-bundle-node.d.ts +1 -1
- package/lib/project-bundle-node.js +70 -13
- package/lib/project-bundle.d.ts +91 -3
- package/lib/project-bundle.js +48 -7
- package/lib/qa/atlas.d.ts +21 -0
- package/lib/qa/atlas.js +474 -0
- package/lib/qa/index.d.ts +1 -0
- package/lib/qa/index.js +1 -0
- package/lib/qa/material.js +2 -0
- package/lib/qa/skin-integrity.d.ts +18 -0
- package/lib/qa/skin-integrity.js +221 -0
- package/lib/render-service-host.js +4 -7
- package/lib/render.d.ts +3 -3
- package/lib/render.js +108 -16
- package/lib/skin-binding.d.ts +20 -0
- package/lib/skin-binding.js +144 -0
- package/lib/texture-atlas-contract.d.ts +97 -0
- package/lib/texture-atlas-contract.js +414 -0
- package/lib/texture-atlas-pixels.d.ts +73 -0
- package/lib/texture-atlas-pixels.js +290 -0
- package/lib/texture-atlas.d.ts +10 -0
- package/lib/texture-atlas.js +143 -0
- package/lib/texture-bake.d.ts +3 -0
- package/lib/texture-bake.js +66 -11
- package/lib/texture-resolver.d.ts +22 -0
- package/lib/texture-resolver.js +24 -0
- package/lib/tools/materials.js +8 -7
- package/lib/tools/registry.js +5 -16
- package/lib/tools/review.js +1 -1
- package/lib/uv-data.d.ts +32 -0
- package/lib/uv-data.js +172 -0
- package/lib/uv-shapes.js +3 -0
- package/lib/uv.d.ts +9 -3
- package/lib/uv.js +53 -82
- package/lib/views/camera.d.ts +3 -3
- package/lib/views/camera.js +32 -28
- package/lib/views/face-policy.d.ts +9 -0
- package/lib/views/face-policy.js +16 -0
- package/lib/views/glb.js +218 -24
- package/lib/views/pose.d.ts +7 -1
- package/lib/views/pose.js +63 -12
- package/lib/views/raster.d.ts +6 -2
- package/lib/views/raster.js +49 -37
- package/lib/views/scene-copy.d.ts +6 -0
- package/lib/views/scene-copy.js +73 -0
- package/lib/views/scene-raster.js +2 -1
- package/lib/views/triangle-coverage.d.ts +8 -0
- package/lib/views/triangle-coverage.js +58 -0
- package/lib/workspace-node.js +7 -3
- package/lib/workspace.d.ts +2 -2
- package/package.json +3 -1
- package/plugin.json +1 -1
- package/plugins/kiln-engine/.claude-plugin/plugin.json +1 -1
- package/plugins/kiln-engine/README.md +3 -3
- package/plugins/kiln-engine/package-provenance.json +6 -6
- package/plugins/kiln-engine/plugin.json +1 -1
- package/plugins/kiln-engine/runtime.json +1 -1
- package/skills/kiln-author-asset/references/geometry-recipes.md +124 -0
- package/skills/kiln-author-asset/references/projects-and-materials.md +32 -2
- package/skills/kiln-compose-scene/references/runtime-scenes.md +19 -0
- package/skills/kiln-qa-asset/references/integration-checks.md +37 -2
- package/src/asset-materials-node.ts +38 -45
- package/src/assets-node.ts +22 -5
- package/src/assets.ts +61 -20
- package/src/atlas-provenance.ts +522 -0
- package/src/character.ts +107 -2
- package/src/community-exporter.ts +143 -3
- package/src/discovery/helper-contracts.ts +188 -7
- package/src/discovery/helper-specs.ts +69 -4
- package/src/discovery/shapes.ts +25 -9
- package/src/engine-identity.ts +1 -1
- package/src/evaluator/authoring-diagnostic.ts +19 -1
- package/src/evaluator/protocol.ts +19 -9
- package/src/evaluator/subprocess.ts +2 -2
- package/src/exporter-node.ts +12 -1
- package/src/generated/mcp-manifest.json +3 -3
- package/src/geometry-budget.ts +8 -0
- package/src/gltf-texture-binding.ts +60 -0
- package/src/material-authoring-node.ts +68 -0
- package/src/material-authoring-schema.ts +343 -0
- package/src/material-export.ts +25 -0
- package/src/material-library-node.ts +840 -16
- package/src/material-library.ts +348 -5
- package/src/pinned-material-map.ts +43 -0
- package/src/portable-material-runtime.ts +200 -0
- package/src/portable-sampler-runtime.ts +40 -0
- package/src/portable-sampler.ts +54 -0
- package/src/primitives.ts +98 -4
- package/src/procedural-material-v2.ts +2 -1
- package/src/procedural-material-v3.ts +224 -0
- package/src/procedural-texture-v2-compiler.ts +234 -0
- package/src/procedural-texture-v3-contract.ts +437 -0
- package/src/procedural-texture-v3.ts +220 -0
- package/src/procedural-texture.ts +61 -229
- package/src/project-bundle-node.ts +106 -14
- package/src/project-bundle.ts +61 -18
- package/src/project-cli.ts +29 -17
- package/src/qa/atlas.ts +597 -0
- package/src/qa/index.ts +1 -0
- package/src/qa/material.ts +2 -0
- package/src/qa/skin-integrity.ts +291 -0
- package/src/render-service-host.ts +4 -6
- package/src/render.ts +133 -15
- package/src/skin-binding.ts +225 -0
- package/src/texture-atlas-contract.ts +558 -0
- package/src/texture-atlas-pixels.ts +456 -0
- package/src/texture-atlas.ts +185 -0
- package/src/texture-bake.ts +88 -10
- package/src/texture-resolver.ts +55 -0
- package/src/tools/materials.ts +15 -11
- package/src/tools/registry.ts +5 -15
- package/src/tools/review.ts +1 -1
- package/src/uv-data.ts +238 -0
- package/src/uv-shapes.ts +3 -0
- package/src/uv.ts +73 -128
- package/src/viewer/app.ts +37 -0
- package/src/viewer/live-panel.ts +57 -1
- package/src/viewer/material-preview-panel.ts +140 -0
- package/src/viewer/material-preview.ts +138 -0
- package/src/viewer/materials-panel.ts +34 -5
- package/src/viewer/navigation.ts +2 -0
- package/src/views/camera.ts +37 -40
- package/src/views/face-policy.ts +20 -0
- package/src/views/glb.ts +294 -17
- package/src/views/pose.ts +71 -13
- package/src/views/raster.ts +73 -46
- package/src/views/scene-copy.ts +86 -0
- package/src/views/scene-raster.ts +2 -1
- package/src/views/triangle-coverage.ts +78 -0
- package/src/workspace-node.ts +11 -3
- package/src/workspace.ts +2 -2
package/src/views/raster.ts
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* kiln-studio agent-runtime container ships without a browser
|
|
7
7
|
* (PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1) and Playwright is unsupported under
|
|
8
8
|
* Bun on Windows (see imposter/bake.ts). headless-gl is WebGL1-only while
|
|
9
|
-
* three >= r163 requires WebGL2. So: a
|
|
9
|
+
* three >= r163 requires WebGL2. So: a CPU scanline z-buffer
|
|
10
10
|
* rasterizer over the executed scene. Flat lambert, base color only — enough
|
|
11
11
|
* fidelity for silhouette / proportion / orientation / contact QA, which is
|
|
12
12
|
* what the vision loop checks.
|
|
13
13
|
*
|
|
14
14
|
* Scene access is strictly duck-typed (`.isMesh`, `matrixWorld.elements`,
|
|
15
|
-
* position
|
|
15
|
+
* vertex readers and position attributes) because sandbox-created objects live in a
|
|
16
16
|
* different module realm — see executeKilnCode in ../render.ts.
|
|
17
17
|
*
|
|
18
18
|
* Lighting matches scripts/visual-audit.ts: key directional light from
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
* the offline audit grid) so inverted winding shows up as missing faces.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { Vector3 } from 'three';
|
|
23
24
|
import { resolveBackdrop, type BackdropId } from './background';
|
|
25
|
+
import { visibleNormalSign, worldWindingParity, type SurfaceSide } from './face-policy';
|
|
26
|
+
import { prepareTriangleCoverage } from './triangle-coverage';
|
|
24
27
|
|
|
25
28
|
export interface ViewSpec {
|
|
26
29
|
name: string;
|
|
@@ -118,7 +121,10 @@ export interface RasterOptions {
|
|
|
118
121
|
* (a sideways/forward-sliding walk would look stationary). The scene's own
|
|
119
122
|
* geometry is still drawn; only the camera center/zoom come from this box.
|
|
120
123
|
*/
|
|
121
|
-
frameBounds?: {
|
|
124
|
+
frameBounds?: {
|
|
125
|
+
min: [number, number, number];
|
|
126
|
+
max: [number, number, number];
|
|
127
|
+
};
|
|
122
128
|
/** Named backdrop to clear to. Omit for the default; see `views/background.ts`. */
|
|
123
129
|
backdrop?: BackdropId;
|
|
124
130
|
}
|
|
@@ -146,6 +152,9 @@ interface DuckAttribute {
|
|
|
146
152
|
array: ArrayLike<number>;
|
|
147
153
|
itemSize: number;
|
|
148
154
|
count: number;
|
|
155
|
+
getX?(index: number): number;
|
|
156
|
+
getY?(index: number): number;
|
|
157
|
+
getZ?(index: number): number;
|
|
149
158
|
}
|
|
150
159
|
interface DuckGeometry {
|
|
151
160
|
getAttribute(name: string): DuckAttribute | undefined;
|
|
@@ -159,6 +168,7 @@ interface DuckMaterial {
|
|
|
159
168
|
/** Effective glTF base-color alpha after OPAQUE/MASK/BLEND handling. */
|
|
160
169
|
opacity?: number;
|
|
161
170
|
doubleSided?: boolean;
|
|
171
|
+
side?: number;
|
|
162
172
|
visible?: boolean;
|
|
163
173
|
}
|
|
164
174
|
interface DuckMesh {
|
|
@@ -167,20 +177,50 @@ interface DuckMesh {
|
|
|
167
177
|
geometry?: DuckGeometry;
|
|
168
178
|
material?: DuckMaterial | DuckMaterial[];
|
|
169
179
|
matrixWorld?: { elements: ArrayLike<number> };
|
|
180
|
+
getVertexPosition?(index: number, target: Vector3): Vector3;
|
|
181
|
+
skeleton?: { bones: DuckObject3D[] };
|
|
170
182
|
}
|
|
171
183
|
interface DuckObject3D {
|
|
172
184
|
visible?: boolean;
|
|
173
185
|
children?: DuckObject3D[];
|
|
174
186
|
updateMatrixWorld?(force?: boolean): void;
|
|
187
|
+
updateWorldMatrix?(updateParents: boolean, updateChildren: boolean): void;
|
|
175
188
|
traverse?(cb: (obj: unknown) => void): void;
|
|
176
189
|
}
|
|
177
190
|
|
|
191
|
+
function updateSurfaceMatrices(root: DuckObject3D): void {
|
|
192
|
+
// Isolated part measurements may start below a changed parent. Keep the normal
|
|
193
|
+
// updateMatrixWorld dispatch too: SkinnedMesh refreshes its bind inverse there.
|
|
194
|
+
root.updateWorldMatrix?.(true, false);
|
|
195
|
+
root.updateMatrixWorld?.(true);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Three's own vertex reader applies morphs, then skinning, without changing the
|
|
199
|
+
* source buffers. A local Vector3 works across realms; no class identity test is
|
|
200
|
+
* needed. Plain GLB duck geometry retains its deterministic array fallback. */
|
|
201
|
+
function surfaceVertexReader(mesh: DuckMesh, position: DuckAttribute): (index: number) => Vector3 {
|
|
202
|
+
for (const bone of mesh.skeleton?.bones ?? []) bone.updateWorldMatrix?.(true, false);
|
|
203
|
+
const vertex = new Vector3();
|
|
204
|
+
if (mesh.getVertexPosition) return (index) => mesh.getVertexPosition!(index, vertex);
|
|
205
|
+
if (position.getX && position.getY && position.getZ)
|
|
206
|
+
return (index) =>
|
|
207
|
+
vertex.set(position.getX!(index), position.getY!(index), position.getZ!(index));
|
|
208
|
+
return (index) =>
|
|
209
|
+
vertex.set(
|
|
210
|
+
position.array[index * 3]!,
|
|
211
|
+
position.array[index * 3 + 1]!,
|
|
212
|
+
position.array[index * 3 + 2]!,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
178
216
|
interface Tri {
|
|
179
217
|
// World-space vertices, flattened [x0,y0,z0, x1,y1,z1, x2,y2,z2]
|
|
180
218
|
v: Float64Array;
|
|
181
219
|
color: [number, number, number];
|
|
182
220
|
alpha: number;
|
|
183
221
|
doubleSided: boolean;
|
|
222
|
+
side: SurfaceSide;
|
|
223
|
+
windingParity: 1 | -1;
|
|
184
224
|
}
|
|
185
225
|
|
|
186
226
|
/**
|
|
@@ -241,7 +281,7 @@ export function collectTriangles(root: DuckObject3D): {
|
|
|
241
281
|
tris: Tri[];
|
|
242
282
|
bbox: { min: Vec3; max: Vec3 };
|
|
243
283
|
} {
|
|
244
|
-
root
|
|
284
|
+
updateSurfaceMatrices(root);
|
|
245
285
|
|
|
246
286
|
const tris: Tri[] = [];
|
|
247
287
|
const min: Vec3 = [Infinity, Infinity, Infinity];
|
|
@@ -277,13 +317,12 @@ export function collectTriangles(root: DuckObject3D): {
|
|
|
277
317
|
|
|
278
318
|
const m = mesh.matrixWorld?.elements;
|
|
279
319
|
if (!m) return;
|
|
320
|
+
const windingParity = worldWindingParity(m);
|
|
280
321
|
|
|
281
|
-
const
|
|
322
|
+
const readVertex = surfaceVertexReader(mesh, pos);
|
|
282
323
|
const world = new Float64Array(pos.count * 3);
|
|
283
324
|
for (let i = 0; i < pos.count; i++) {
|
|
284
|
-
const x =
|
|
285
|
-
const y = arr[i * 3 + 1]!;
|
|
286
|
-
const z = arr[i * 3 + 2]!;
|
|
325
|
+
const { x, y, z } = readVertex(i);
|
|
287
326
|
const wx = m[0]! * x + m[4]! * y + m[8]! * z + m[12]!;
|
|
288
327
|
const wy = m[1]! * x + m[5]! * y + m[9]! * z + m[13]!;
|
|
289
328
|
const wz = m[2]! * x + m[6]! * y + m[10]! * z + m[14]!;
|
|
@@ -309,11 +348,15 @@ export function collectTriangles(root: DuckObject3D): {
|
|
|
309
348
|
v[6] = world[c2 * 3]!;
|
|
310
349
|
v[7] = world[c2 * 3 + 1]!;
|
|
311
350
|
v[8] = world[c2 * 3 + 2]!;
|
|
351
|
+
const side =
|
|
352
|
+
material?.doubleSided === true || material?.side === 2 ? 2 : material?.side === 1 ? 1 : 0;
|
|
312
353
|
tris.push({
|
|
313
354
|
v,
|
|
314
355
|
color: colorOf(material),
|
|
315
356
|
alpha: clamp01(material?.opacity ?? 1),
|
|
316
|
-
doubleSided:
|
|
357
|
+
doubleSided: side === 2,
|
|
358
|
+
side,
|
|
359
|
+
windingParity,
|
|
317
360
|
});
|
|
318
361
|
};
|
|
319
362
|
|
|
@@ -410,6 +453,7 @@ export function rasterizeView(
|
|
|
410
453
|
const sx = new Float64Array(3);
|
|
411
454
|
const sy = new Float64Array(3);
|
|
412
455
|
const sz = new Float64Array(3);
|
|
456
|
+
const weights = new Float64Array(3);
|
|
413
457
|
|
|
414
458
|
// Orthographic: distance from the viewer runs against the view axis `z`.
|
|
415
459
|
const ordered = compositingOrder(
|
|
@@ -423,10 +467,15 @@ export function rasterizeView(
|
|
|
423
467
|
const n = cross(e1, e2);
|
|
424
468
|
const nLen = Math.hypot(n[0], n[1], n[2]);
|
|
425
469
|
if (nLen < 1e-12) continue;
|
|
426
|
-
const N: Vec3 = [
|
|
470
|
+
const N: Vec3 = [
|
|
471
|
+
(n[0] / nLen) * tri.windingParity,
|
|
472
|
+
(n[1] / nLen) * tri.windingParity,
|
|
473
|
+
(n[2] / nLen) * tri.windingParity,
|
|
474
|
+
];
|
|
427
475
|
|
|
428
476
|
const facing = dot(N, z);
|
|
429
|
-
|
|
477
|
+
const normalSign = visibleNormalSign(facing, tri.side, cull);
|
|
478
|
+
if (normalSign === 0) continue;
|
|
430
479
|
if (tri.alpha <= 0) continue;
|
|
431
480
|
|
|
432
481
|
// Project to screen space.
|
|
@@ -442,43 +491,20 @@ export function rasterizeView(
|
|
|
442
491
|
|
|
443
492
|
// Flat lambert with the audit-grid light rig, in linear space, then the
|
|
444
493
|
// sRGB output transform (matching what a Three.js renderer would write).
|
|
445
|
-
const lambert = Math.max(0, dot(N, KEY_DIR));
|
|
494
|
+
const lambert = Math.max(0, dot(N, KEY_DIR) * normalSign);
|
|
446
495
|
const lit = Math.min(1, AMBIENT + KEY_INTENSITY * lambert);
|
|
447
496
|
const r = Math.round(linearToSrgb(tri.color[0] * lit) * 255);
|
|
448
497
|
const g = Math.round(linearToSrgb(tri.color[1] * lit) * 255);
|
|
449
498
|
const b = Math.round(linearToSrgb(tri.color[2] * lit) * 255);
|
|
450
499
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
const minY = Math.max(0, Math.floor(Math.min(sy[0]!, sy[1]!, sy[2]!)));
|
|
455
|
-
const maxY = Math.min(size - 1, Math.ceil(Math.max(sy[0]!, sy[1]!, sy[2]!)));
|
|
456
|
-
if (minX > maxX || minY > maxY) continue;
|
|
457
|
-
|
|
458
|
-
const ax = sx[0]!,
|
|
459
|
-
ay = sy[0]!,
|
|
460
|
-
bx2 = sx[1]!,
|
|
461
|
-
by = sy[1]!,
|
|
462
|
-
cx = sx[2]!,
|
|
463
|
-
cy = sy[2]!;
|
|
464
|
-
const area = (bx2 - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
465
|
-
if (Math.abs(area) < 1e-9) continue;
|
|
466
|
-
const invArea = 1 / area;
|
|
500
|
+
const triangle = prepareTriangleCoverage(sx[0]!, sy[0]!, sx[1]!, sy[1]!, sx[2]!, sy[2]!, size);
|
|
501
|
+
if (!triangle) continue;
|
|
502
|
+
const { minX, maxX, minY, maxY } = triangle;
|
|
467
503
|
|
|
468
504
|
for (let py = minY; py <= maxY; py++) {
|
|
469
505
|
for (let px = minX; px <= maxX; px++) {
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
const w0 = ((bx2 - ax) * (cyp - ay) - (by - ay) * (cxp - ax)) * invArea;
|
|
473
|
-
const w1 = ((cx - bx2) * (cyp - by) - (cy - by) * (cxp - bx2)) * invArea;
|
|
474
|
-
const w2 = 1 - w0 - w1;
|
|
475
|
-
// Accept either winding on screen (back-face culling already happened
|
|
476
|
-
// in world space) — check all barycentrics share the area's sign space.
|
|
477
|
-
if (w0 < 0 || w1 < 0 || w2 < 0) continue;
|
|
478
|
-
// Barycentric weights map: w0 → vertex C, w1 → vertex A, w2 → vertex B
|
|
479
|
-
// for this edge-function arrangement; depth interpolation just needs a
|
|
480
|
-
// consistent convex combination, so order doesn't matter for QA views.
|
|
481
|
-
const depth = w1 * sz[0]! + w2 * sz[1]! + w0 * sz[2]!;
|
|
506
|
+
if (!triangle.sample(px + 0.5, py + 0.5, weights)) continue;
|
|
507
|
+
const depth = weights[0]! * sz[0]! + weights[1]! * sz[1]! + weights[2]! * sz[2]!;
|
|
482
508
|
const pi = py * size + px;
|
|
483
509
|
if (depth <= zbuf[pi]!) continue;
|
|
484
510
|
zbuf[pi] = depth;
|
|
@@ -525,7 +551,7 @@ export function measurePartBounds(root: unknown): ReturnType<typeof measureBound
|
|
|
525
551
|
export function createPartBoundsReader(
|
|
526
552
|
root: unknown,
|
|
527
553
|
): (node: unknown) => ReturnType<typeof measureBounds> | null {
|
|
528
|
-
(root as DuckObject3D)
|
|
554
|
+
updateSurfaceMatrices(root as DuckObject3D);
|
|
529
555
|
const own = new Map<unknown, { min: Vec3; max: Vec3; tris: number } | null>();
|
|
530
556
|
const meshBounds = (obj: unknown) => {
|
|
531
557
|
if (own.has(obj)) return own.get(obj)!;
|
|
@@ -536,11 +562,9 @@ export function createPartBoundsReader(
|
|
|
536
562
|
if (pos && pos.itemSize === 3 && m) {
|
|
537
563
|
const min: Vec3 = [Infinity, Infinity, Infinity];
|
|
538
564
|
const max: Vec3 = [-Infinity, -Infinity, -Infinity];
|
|
539
|
-
const
|
|
565
|
+
const readVertex = surfaceVertexReader(mesh, pos);
|
|
540
566
|
for (let i = 0; i < pos.count; i++) {
|
|
541
|
-
const x =
|
|
542
|
-
y = arr[i * 3 + 1]!,
|
|
543
|
-
z = arr[i * 3 + 2]!;
|
|
567
|
+
const { x, y, z } = readVertex(i);
|
|
544
568
|
const w: Vec3 = [
|
|
545
569
|
m[0]! * x + m[4]! * y + m[8]! * z + m[12]!,
|
|
546
570
|
m[1]! * x + m[5]! * y + m[9]! * z + m[13]!,
|
|
@@ -614,7 +638,10 @@ export function hideNodeInScene(
|
|
|
614
638
|
let hidden = 0;
|
|
615
639
|
const r = root as DuckObject3D;
|
|
616
640
|
r.traverse?.((obj) => {
|
|
617
|
-
const node = obj as {
|
|
641
|
+
const node = obj as {
|
|
642
|
+
name?: string;
|
|
643
|
+
traverse?(cb: (o: unknown) => void): void;
|
|
644
|
+
};
|
|
618
645
|
if (!node.name || !test(node.name)) return;
|
|
619
646
|
hidden++;
|
|
620
647
|
// Flip the matched node and its whole subtree (the per-mesh cull means hiding
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DoubleSide,
|
|
3
|
+
Group,
|
|
4
|
+
Skeleton,
|
|
5
|
+
type Bone,
|
|
6
|
+
type Material,
|
|
7
|
+
type Mesh,
|
|
8
|
+
type Object3D,
|
|
9
|
+
type SkinnedMesh,
|
|
10
|
+
} from 'three';
|
|
11
|
+
|
|
12
|
+
/** Owned review hierarchy and skeletons; immutable geometry/textures stay shared. */
|
|
13
|
+
export function cloneReviewScene(
|
|
14
|
+
root: Object3D,
|
|
15
|
+
options: { omitHidden?: boolean; doubleSided?: boolean } = {},
|
|
16
|
+
): Object3D {
|
|
17
|
+
const visible = new Set<Object3D>();
|
|
18
|
+
if (options.omitHidden) root.traverseVisible((node) => visible.add(node));
|
|
19
|
+
else root.traverse((node) => visible.add(node));
|
|
20
|
+
const retained = new Set(visible);
|
|
21
|
+
const hierarchy = new Set<Object3D>();
|
|
22
|
+
root.traverse((node) => hierarchy.add(node));
|
|
23
|
+
|
|
24
|
+
// Visibility hides drawing, not the transforms used by another mesh's skeleton.
|
|
25
|
+
for (const node of visible) {
|
|
26
|
+
const mesh = node as SkinnedMesh;
|
|
27
|
+
if (!mesh.isSkinnedMesh) continue;
|
|
28
|
+
if (!mesh.skeleton) throw new Error(`Review skin ${mesh.name} has no skeleton.`);
|
|
29
|
+
for (const bone of mesh.skeleton.bones) {
|
|
30
|
+
if (!hierarchy.has(bone)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Skin bone ${bone?.name ?? '(missing)'} is outside the review hierarchy. Include its ancestors in the review root.`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
for (let ancestor: Object3D | null = bone; ancestor; ancestor = ancestor.parent) {
|
|
36
|
+
retained.add(ancestor);
|
|
37
|
+
if (ancestor === root) break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const copies = new Map<Object3D, Object3D>();
|
|
43
|
+
const copyNode = (node: Object3D): Object3D => {
|
|
44
|
+
// A hidden mesh/light can be a joint ancestor. Keep its transform as a group,
|
|
45
|
+
// not its drawing payload. Referenced bones must retain their Bone identity.
|
|
46
|
+
const copy =
|
|
47
|
+
!visible.has(node) && !(node as Bone).isBone
|
|
48
|
+
? new Group().copy(node, false)
|
|
49
|
+
: node.clone(false);
|
|
50
|
+
copies.set(node, copy);
|
|
51
|
+
if (options.omitHidden) copy.visible = true;
|
|
52
|
+
for (const child of node.children) if (retained.has(child)) copy.add(copyNode(child));
|
|
53
|
+
return copy;
|
|
54
|
+
};
|
|
55
|
+
if (!retained.has(root)) {
|
|
56
|
+
const empty = new Group();
|
|
57
|
+
empty.name = root.name;
|
|
58
|
+
return empty;
|
|
59
|
+
}
|
|
60
|
+
const copy = copyNode(root);
|
|
61
|
+
for (const [source, target] of copies) {
|
|
62
|
+
const sourceSkin = source as SkinnedMesh;
|
|
63
|
+
const targetSkin = target as SkinnedMesh;
|
|
64
|
+
if (targetSkin.isSkinnedMesh) {
|
|
65
|
+
// SkinnedMesh.copy already preserves bind mode/matrices and morph weights.
|
|
66
|
+
// Reconnect only to copied bones, including detached skeletons. Keep inverse
|
|
67
|
+
// matrices independent too, so later edits cannot mutate the retained source.
|
|
68
|
+
targetSkin.skeleton = new Skeleton(
|
|
69
|
+
sourceSkin.skeleton.bones.map((bone) => copies.get(bone) as Bone),
|
|
70
|
+
sourceSkin.skeleton.boneInverses.map((matrix) => matrix.clone()),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const mesh = target as Mesh;
|
|
74
|
+
if (options.doubleSided && mesh.isMesh) {
|
|
75
|
+
const prepare = (material: Material) => {
|
|
76
|
+
const owned = material.clone();
|
|
77
|
+
owned.side = DoubleSide;
|
|
78
|
+
return owned;
|
|
79
|
+
};
|
|
80
|
+
mesh.material = Array.isArray(mesh.material)
|
|
81
|
+
? mesh.material.map(prepare)
|
|
82
|
+
: prepare(mesh.material);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return copy;
|
|
86
|
+
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { loadGlbReviewScene } from './glb';
|
|
24
24
|
import { encodePng } from './png';
|
|
25
25
|
import { rasterizeView, type ViewSpec } from './raster';
|
|
26
|
+
import { cloneReviewScene } from './scene-copy';
|
|
26
27
|
|
|
27
28
|
const DEG = Math.PI / 180;
|
|
28
29
|
const PAD = 4;
|
|
@@ -103,7 +104,7 @@ export async function rasterizeComposedScene(
|
|
|
103
104
|
base = (await loadGlbReviewScene(glb)).root;
|
|
104
105
|
baseByInput.set(key, base);
|
|
105
106
|
}
|
|
106
|
-
const inst = base
|
|
107
|
+
const inst = cloneReviewScene(base);
|
|
107
108
|
inst.position.set(part.pos[0], part.pos[1], part.pos[2]);
|
|
108
109
|
inst.rotation.set(0, part.rotYDeg * DEG, 0);
|
|
109
110
|
inst.scale.setScalar(part.scale);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Pixel-centre coverage shared by the flat CPU renderers. No shading/culling policy. */
|
|
2
|
+
interface Edge {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
dx: number;
|
|
6
|
+
dy: number;
|
|
7
|
+
sign: number;
|
|
8
|
+
inclusive: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function edge(ax: number, ay: number, bx: number, by: number, orientation: number): Edge {
|
|
12
|
+
const dx = (bx - ax) * orientation;
|
|
13
|
+
const dy = (by - ay) * orientation;
|
|
14
|
+
// Screen Y grows downward. For positive area, top/left edges own exact ties.
|
|
15
|
+
const inclusive = dy < 0 || (dy === 0 && dx > 0);
|
|
16
|
+
// Both triangles evaluate a shared edge from the SAME endpoint. Merely reversing
|
|
17
|
+
// (B-A)x(P-A) can round differently from (A-B)x(P-B), causing cracks/overlap.
|
|
18
|
+
const forward = ax < bx || (ax === bx && ay <= by);
|
|
19
|
+
return forward
|
|
20
|
+
? { x: ax, y: ay, dx: bx - ax, dy: by - ay, sign: orientation, inclusive }
|
|
21
|
+
: { x: bx, y: by, dx: ax - bx, dy: ay - by, sign: -orientation, inclusive };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function value(e: Edge, x: number, y: number): number {
|
|
25
|
+
return (e.dx * (y - e.y) - e.dy * (x - e.x)) * e.sign;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function prepareTriangleCoverage(
|
|
29
|
+
ax: number,
|
|
30
|
+
ay: number,
|
|
31
|
+
bx: number,
|
|
32
|
+
by: number,
|
|
33
|
+
cx: number,
|
|
34
|
+
cy: number,
|
|
35
|
+
size: number,
|
|
36
|
+
) {
|
|
37
|
+
if (![ax, ay, bx, by, cx, cy].every(Number.isFinite)) return null;
|
|
38
|
+
const area = value(edge(ax, ay, bx, by, 1), cx, cy);
|
|
39
|
+
if (!Number.isFinite(area) || area === 0) return null;
|
|
40
|
+
const orientation = area < 0 ? -1 : 1;
|
|
41
|
+
const a = edge(bx, by, cx, cy, orientation);
|
|
42
|
+
const b = edge(cx, cy, ax, ay, orientation);
|
|
43
|
+
const c = edge(ax, ay, bx, by, orientation);
|
|
44
|
+
const minX = Math.max(0, Math.floor(Math.min(ax, bx, cx)));
|
|
45
|
+
const maxX = Math.min(size - 1, Math.ceil(Math.max(ax, bx, cx)));
|
|
46
|
+
const minY = Math.max(0, Math.floor(Math.min(ay, by, cy)));
|
|
47
|
+
const maxY = Math.min(size - 1, Math.ceil(Math.max(ay, by, cy)));
|
|
48
|
+
if (minX > maxX || minY > maxY) return null;
|
|
49
|
+
return {
|
|
50
|
+
minX,
|
|
51
|
+
maxX,
|
|
52
|
+
minY,
|
|
53
|
+
maxY,
|
|
54
|
+
/** Writes A/B/C weights into caller-owned storage; no per-pixel allocations. */
|
|
55
|
+
sample(x: number, y: number, weights: Float64Array): boolean {
|
|
56
|
+
const wa = value(a, x, y),
|
|
57
|
+
wb = value(b, x, y),
|
|
58
|
+
wc = value(c, x, y);
|
|
59
|
+
if (
|
|
60
|
+
wa < 0 ||
|
|
61
|
+
wb < 0 ||
|
|
62
|
+
wc < 0 ||
|
|
63
|
+
(wa === 0 && !a.inclusive) ||
|
|
64
|
+
(wb === 0 && !b.inclusive) ||
|
|
65
|
+
(wc === 0 && !c.inclusive)
|
|
66
|
+
)
|
|
67
|
+
return false;
|
|
68
|
+
// Evaluate all three edges directly. Deriving one as 1-wa-wb was the source
|
|
69
|
+
// of false negative coverage. Tie ownership never biases interpolation.
|
|
70
|
+
const sum = wa + wb + wc;
|
|
71
|
+
if (!(sum > 0) || !Number.isFinite(sum)) return false;
|
|
72
|
+
weights[0] = wa / sum;
|
|
73
|
+
weights[1] = wb / sum;
|
|
74
|
+
weights[2] = wc / sum;
|
|
75
|
+
return true;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
package/src/workspace-node.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { FileProjectStore } from './projects-node';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
FileMaterialLibrary,
|
|
6
|
+
createMaterialLibraryPayload,
|
|
7
|
+
resolveMaterialRecordClosure,
|
|
8
|
+
} from './material-library-node';
|
|
5
9
|
import {
|
|
6
10
|
workspaceSelectionSchema,
|
|
7
11
|
type WorkspaceBinding,
|
|
@@ -68,12 +72,12 @@ export class FileWorkspace implements WorkspacePort {
|
|
|
68
72
|
throw new Error(
|
|
69
73
|
`An evaluation may resolve at most ${MATERIAL_LIBRARY_LIMITS.maxPayloadRecords} material resources; select a smaller dependency closure.`,
|
|
70
74
|
);
|
|
71
|
-
const
|
|
75
|
+
const manifests = await Promise.all(
|
|
72
76
|
materialDependencies.map(async (dependency) => {
|
|
73
77
|
if (dependency.sha256 !== dependency.revisionId)
|
|
74
78
|
throw new Error(`Material lock hash mismatch: ${dependency.resourceId}`);
|
|
75
79
|
try {
|
|
76
|
-
return await this.materials.
|
|
80
|
+
return await this.materials.readManifest(dependency.resourceId, dependency.revisionId);
|
|
77
81
|
} catch (error) {
|
|
78
82
|
throw new Error(
|
|
79
83
|
`Locked material unavailable: ${dependency.resourceId} at ${dependency.revisionId}`,
|
|
@@ -82,6 +86,10 @@ export class FileWorkspace implements WorkspacePort {
|
|
|
82
86
|
}
|
|
83
87
|
}),
|
|
84
88
|
);
|
|
89
|
+
const materials = await resolveMaterialRecordClosure(manifests, {
|
|
90
|
+
readManifest: (id, revision) => this.materials.readManifest(id, revision),
|
|
91
|
+
read: (id, revision) => this.materials.readRecordFiles(id, revision),
|
|
92
|
+
});
|
|
85
93
|
const materialResources = await createMaterialLibraryPayload(materials);
|
|
86
94
|
return this.active.run(
|
|
87
95
|
{ ...(project ? { project } : {}), materialDependencies, materialResources },
|
package/src/workspace.ts
CHANGED
|
@@ -6,12 +6,12 @@ import {
|
|
|
6
6
|
type MaterialDependency,
|
|
7
7
|
type ProjectRevision,
|
|
8
8
|
} from './projects';
|
|
9
|
-
import type {
|
|
9
|
+
import type { MaterialLibraryPayload } from './material-library';
|
|
10
10
|
|
|
11
11
|
export interface WorkspaceBinding {
|
|
12
12
|
project?: ProjectRevision;
|
|
13
13
|
materialDependencies: MaterialDependency[];
|
|
14
|
-
materialResources:
|
|
14
|
+
materialResources: MaterialLibraryPayload;
|
|
15
15
|
}
|
|
16
16
|
export const workspaceSelectionSchema = z
|
|
17
17
|
.object({
|