@developerz.ai/ts23d 0.1.0 → 0.2.0

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.
Files changed (162) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/CLAUDE.md +1 -1
  3. package/README.md +10 -5
  4. package/dist/{chunk-a8fxakcj.js → chunk-6b05mnmx.js} +2 -2
  5. package/dist/{chunk-kgvbpqqw.js → chunk-7rtfjh9f.js} +6 -5
  6. package/dist/chunk-7rtfjh9f.js.map +10 -0
  7. package/dist/{chunk-1ypfs7ga.js → chunk-8j4c28jt.js} +790 -276
  8. package/dist/chunk-8j4c28jt.js.map +46 -0
  9. package/dist/{chunk-wtqhq529.js → chunk-d1fv6hrm.js} +6 -6
  10. package/dist/{chunk-wtqhq529.js.map → chunk-d1fv6hrm.js.map} +1 -1
  11. package/dist/{chunk-gcyp9b4d.js → chunk-dbq4qf52.js} +362 -215
  12. package/dist/chunk-dbq4qf52.js.map +25 -0
  13. package/dist/{chunk-xnt2sdhf.js → chunk-drna2rny.js} +2 -2
  14. package/dist/{chunk-ravbb06q.js → chunk-dxvypr7y.js} +2 -2
  15. package/dist/{chunk-11v28977.js → chunk-f6yme003.js} +85 -71
  16. package/dist/{chunk-11v28977.js.map → chunk-f6yme003.js.map} +6 -6
  17. package/dist/{chunk-4ddzsqtc.js → chunk-hg7pf34j.js} +4 -3
  18. package/dist/{chunk-4ddzsqtc.js.map → chunk-hg7pf34j.js.map} +5 -5
  19. package/dist/{chunk-w1zyhj40.js → chunk-jq4e17tj.js} +2 -2
  20. package/dist/{chunk-kvcep7ft.js → chunk-y63c6zcv.js} +967 -218
  21. package/dist/chunk-y63c6zcv.js.map +53 -0
  22. package/dist/cli/args.d.ts +7 -1
  23. package/dist/cli/bin.js +66 -25
  24. package/dist/cli/bin.js.map +6 -5
  25. package/dist/cli/manifest.d.ts +5 -1
  26. package/dist/cli/texture-cache.d.ts +4 -0
  27. package/dist/core/definition.d.ts +7 -0
  28. package/dist/core/errors.d.ts +1 -1
  29. package/dist/core/index.js +1 -1
  30. package/dist/export/credits.d.ts +4 -0
  31. package/dist/export/glb-options.d.ts +48 -0
  32. package/dist/export/glb.d.ts +5 -35
  33. package/dist/export/hierarchy.d.ts +23 -0
  34. package/dist/export/index.d.ts +2 -1
  35. package/dist/export/index.js +21 -7
  36. package/dist/export/index.js.map +1 -1
  37. package/dist/export/lod-coverage.d.ts +12 -0
  38. package/dist/export/metadata.d.ts +2 -0
  39. package/dist/export/version.d.ts +2 -2
  40. package/dist/grammar/index.js +2 -2
  41. package/dist/index.js +63 -17
  42. package/dist/index.js.map +1 -1
  43. package/dist/kinds/head-armour.d.ts +3 -7
  44. package/dist/kinds/head-bust.d.ts +3 -2
  45. package/dist/kinds/head-face.d.ts +3 -1
  46. package/dist/kinds/head-garb.d.ts +4 -0
  47. package/dist/kinds/head-mesh.d.ts +4 -0
  48. package/dist/kinds/head-mouth.d.ts +4 -0
  49. package/dist/kinds/head-params.d.ts +9 -1
  50. package/dist/kinds/head-tone.d.ts +10 -0
  51. package/dist/kinds/head-torso.d.ts +35 -0
  52. package/dist/kinds/index.js +6 -6
  53. package/dist/material/cc0.d.ts +4 -0
  54. package/dist/material/define.d.ts +34 -0
  55. package/dist/material/generator.d.ts +1 -1
  56. package/dist/material/index.d.ts +8 -3
  57. package/dist/material/index.js +39 -7
  58. package/dist/material/index.js.map +1 -1
  59. package/dist/material/library.d.ts +7 -0
  60. package/dist/material/material-def.d.ts +30 -0
  61. package/dist/material/paint/canvas.d.ts +7 -0
  62. package/dist/material/paint/default-sheet.d.ts +3 -1
  63. package/dist/material/paint/fabric.d.ts +4 -0
  64. package/dist/material/paint/image-strip.d.ts +4 -0
  65. package/dist/material/paint/kit.d.ts +5 -0
  66. package/dist/material/paint/metal.d.ts +3 -0
  67. package/dist/material/paint/orm.d.ts +1 -1
  68. package/dist/material/paint/strip.d.ts +1 -1
  69. package/dist/material/paint/types.d.ts +32 -2
  70. package/dist/material/palette.d.ts +2 -0
  71. package/dist/material/slots.d.ts +5 -0
  72. package/dist/material/texture-source.d.ts +34 -0
  73. package/dist/material/trim-sheet.d.ts +20 -4
  74. package/dist/neural/index.js +2 -2
  75. package/dist/passes/index.js +2 -2
  76. package/dist/primitives/index.js +3 -3
  77. package/dist/qa/index.js +2 -2
  78. package/dist/render/beauty.d.ts +1 -1
  79. package/dist/render/index.js +3 -3
  80. package/dist/render/lighting.d.ts +7 -0
  81. package/dist/render/shade.d.ts +1 -1
  82. package/dist/testing/index.js +5 -5
  83. package/docs/README.md +4 -0
  84. package/docs/architecture/gltf-capabilities.md +38 -1
  85. package/docs/architecture/materials.md +151 -0
  86. package/docs/idea/10-sculpting.md +166 -0
  87. package/docs/idea/11-materials-authoring.md +132 -0
  88. package/docs/idea/12-blender-bridge.md +136 -0
  89. package/docs/idea/13-asset-pipeline-research.md +89 -0
  90. package/docs/plans/roadmap.md +12 -0
  91. package/package.json +2 -1
  92. package/recipes/boxcar/README.md +7 -0
  93. package/recipes/boxcar/boxcar.ts +118 -34
  94. package/recipes/goblin_head/README.md +27 -14
  95. package/recipes/goblin_head/goblin_head.ts +11 -9
  96. package/recipes/human_head/README.md +8 -4
  97. package/recipes/human_head/human_head.ts +4 -3
  98. package/recipes/material_swatches/material_swatches.ts +11 -5
  99. package/recipes/orc_head/README.md +32 -20
  100. package/recipes/orc_head/orc_head.ts +9 -7
  101. package/src/cli/args.ts +16 -3
  102. package/src/cli/manifest.ts +6 -0
  103. package/src/cli/run.ts +33 -9
  104. package/src/cli/texture-cache.ts +24 -0
  105. package/src/core/asset.ts +1 -0
  106. package/src/core/definition.ts +7 -0
  107. package/src/core/errors.ts +6 -1
  108. package/src/export/credits.ts +25 -0
  109. package/src/export/glb-options.ts +53 -0
  110. package/src/export/glb.ts +100 -113
  111. package/src/export/hierarchy.ts +122 -0
  112. package/src/export/index.ts +17 -1
  113. package/src/export/lod-coverage.ts +71 -0
  114. package/src/export/metadata.ts +3 -0
  115. package/src/export/version.ts +1 -1
  116. package/src/kinds/head-armour.ts +103 -73
  117. package/src/kinds/head-bust.ts +49 -86
  118. package/src/kinds/head-face.ts +94 -74
  119. package/src/kinds/head-garb.ts +181 -0
  120. package/src/kinds/head-hair.ts +74 -33
  121. package/src/kinds/head-mesh.ts +43 -5
  122. package/src/kinds/head-mouth.ts +111 -0
  123. package/src/kinds/head-params.ts +11 -2
  124. package/src/kinds/head-presets.ts +30 -27
  125. package/src/kinds/head-tone.ts +52 -0
  126. package/src/kinds/head-torso.ts +230 -0
  127. package/src/kinds/head.ts +8 -3
  128. package/src/material/cc0.ts +41 -0
  129. package/src/material/define.ts +130 -0
  130. package/src/material/generator.ts +15 -8
  131. package/src/material/index.ts +34 -3
  132. package/src/material/library.ts +196 -0
  133. package/src/material/material-def.ts +84 -0
  134. package/src/material/paint/canvas.ts +11 -1
  135. package/src/material/paint/compose.ts +1 -1
  136. package/src/material/paint/decode-png.ts +19 -13
  137. package/src/material/paint/default-sheet.ts +40 -28
  138. package/src/material/paint/fabric.ts +63 -0
  139. package/src/material/paint/image-strip.ts +88 -0
  140. package/src/material/paint/kit.ts +37 -0
  141. package/src/material/paint/masks.ts +3 -1
  142. package/src/material/paint/metal.ts +109 -0
  143. package/src/material/paint/orm.ts +14 -1
  144. package/src/material/paint/planks.ts +49 -4
  145. package/src/material/paint/strip.ts +35 -29
  146. package/src/material/paint/types.ts +45 -2
  147. package/src/material/paint/validate.ts +19 -3
  148. package/src/material/palette.ts +3 -0
  149. package/src/material/slots.ts +18 -0
  150. package/src/material/texture-source.ts +86 -0
  151. package/src/material/trim-sheet.ts +79 -13
  152. package/src/render/beauty.ts +2 -2
  153. package/src/render/lighting.ts +26 -0
  154. package/src/render/shade.ts +10 -5
  155. package/dist/chunk-1ypfs7ga.js.map +0 -42
  156. package/dist/chunk-gcyp9b4d.js.map +0 -22
  157. package/dist/chunk-kgvbpqqw.js.map +0 -10
  158. package/dist/chunk-kvcep7ft.js.map +0 -43
  159. /package/dist/{chunk-a8fxakcj.js.map → chunk-6b05mnmx.js.map} +0 -0
  160. /package/dist/{chunk-xnt2sdhf.js.map → chunk-drna2rny.js.map} +0 -0
  161. /package/dist/{chunk-ravbb06q.js.map → chunk-dxvypr7y.js.map} +0 -0
  162. /package/dist/{chunk-w1zyhj40.js.map → chunk-jq4e17tj.js.map} +0 -0
@@ -5,14 +5,16 @@ import {
5
5
  quatFromAxisAngle2,
6
6
  worldTransforms2,
7
7
  toWorld2,
8
- rotateZ2
9
- } from "./chunk-4ddzsqtc.js";
8
+ rotateZ2,
9
+ worldMeshes2
10
+ } from "./chunk-hg7pf34j.js";
10
11
  import {
11
- hex2
12
- } from "./chunk-kvcep7ft.js";
12
+ hex2,
13
+ stripCredits2
14
+ } from "./chunk-y63c6zcv.js";
13
15
  import {
14
16
  optimize2
15
- } from "./chunk-xnt2sdhf.js";
17
+ } from "./chunk-drna2rny.js";
16
18
 
17
19
  // src/export/bin-writer.ts
18
20
  var ARRAY_BUFFER = 34962;
@@ -111,6 +113,28 @@ function concat(a, b) {
111
113
  return out;
112
114
  }
113
115
 
116
+ // src/export/credits.ts
117
+ function creditsOf(asset) {
118
+ const out = new Set;
119
+ for (const p of asset.parts) {
120
+ const uv = p.mesh?.uvs;
121
+ if (!uv)
122
+ continue;
123
+ const v = new Float32Array(uv.length >> 1);
124
+ for (let i = 0;i < v.length; i++)
125
+ v[i] = uv[i * 2 + 1] ?? 0;
126
+ for (const c of stripCredits2(v))
127
+ out.add(c);
128
+ }
129
+ return [...out].sort();
130
+ }
131
+ function withCredits(asset, meta) {
132
+ const credits = creditsOf(asset);
133
+ if (!credits.length)
134
+ return meta;
135
+ return { ...meta, credits: [...meta?.credits ?? [], ...credits] };
136
+ }
137
+
114
138
  // src/export/lights.ts
115
139
  var LIGHTS = "KHR_lights_punctual";
116
140
  function between(a, b) {
@@ -148,6 +172,273 @@ function lightOf(part) {
148
172
  };
149
173
  }
150
174
 
175
+ // src/export/motion.ts
176
+ var LOOP_SECONDS = 4;
177
+ var cyclesOf = (rate) => Math.max(1, Math.round(Math.abs(rate) * LOOP_SECONDS)) * Math.sign(rate || 1);
178
+ var timeAccessors = new WeakMap;
179
+ function times(bin, keys) {
180
+ const t = new Float32Array(keys + 1).map((_, i) => i / keys * LOOP_SECONDS);
181
+ const byBin = timeAccessors.get(bin) ?? new Map;
182
+ timeAccessors.set(bin, byBin);
183
+ let input = byBin.get(keys);
184
+ if (input === undefined) {
185
+ input = bin.accessor(t, "SCALAR", undefined, true);
186
+ byBin.set(keys, input);
187
+ }
188
+ return { input, t };
189
+ }
190
+ function motionChannel(bin, motion, node, rest) {
191
+ const rate = motion.kind === "spin" ? motion.rps : motion.hz;
192
+ const cycles = cyclesOf(rate);
193
+ const keys = Math.max(24, Math.abs(cycles) * (motion.kind === "spin" ? 12 : 16));
194
+ const { input, t } = times(bin, keys);
195
+ const phase = (time) => time / LOOP_SECONDS * cycles * Math.PI * 2;
196
+ const wave = (time) => motion.kind !== "spin" && motion.oneWay ? (1 - Math.cos(phase(time))) / 2 : Math.sin(phase(time));
197
+ if (motion.kind === "slide") {
198
+ const values = new Float32Array((keys + 1) * 3);
199
+ t.forEach((time, i) => {
200
+ const s = wave(time);
201
+ values.set([0, 1, 2].map((k) => (rest[k] ?? 0) + (motion.offset[k] ?? 0) * s), i * 3);
202
+ });
203
+ return {
204
+ sampler: { input, output: bin.accessor(values, "VEC3"), interpolation: "LINEAR" },
205
+ channel: { target: { node, path: "translation" } }
206
+ };
207
+ }
208
+ const values = new Float32Array((keys + 1) * 4);
209
+ const peak = motion.kind === "sway" ? motion.degrees * Math.PI / 180 : 0;
210
+ t.forEach((time, i) => {
211
+ const angle = motion.kind === "spin" ? phase(time) : peak * wave(time);
212
+ values.set(quatFromAxisAngle2(motion.axis, angle), i * 4);
213
+ });
214
+ return {
215
+ sampler: { input, output: bin.accessor(values, "VEC4"), interpolation: "LINEAR" },
216
+ channel: { target: { node, path: "rotation" } }
217
+ };
218
+ }
219
+
220
+ // src/export/tangents.ts
221
+ function tangentsOf(m) {
222
+ const n = m.positions.length / 3;
223
+ const tan = new Float32Array(n * 3);
224
+ const bit = new Float32Array(n * 3);
225
+ const P = m.positions;
226
+ const U = m.uvs;
227
+ for (let t = 0;t < m.indices.length; t += 3) {
228
+ const a = m.indices[t] ?? 0;
229
+ const b = m.indices[t + 1] ?? 0;
230
+ const c = m.indices[t + 2] ?? 0;
231
+ const e1x = (P[b * 3] ?? 0) - (P[a * 3] ?? 0);
232
+ const e1y = (P[b * 3 + 1] ?? 0) - (P[a * 3 + 1] ?? 0);
233
+ const e1z = (P[b * 3 + 2] ?? 0) - (P[a * 3 + 2] ?? 0);
234
+ const e2x = (P[c * 3] ?? 0) - (P[a * 3] ?? 0);
235
+ const e2y = (P[c * 3 + 1] ?? 0) - (P[a * 3 + 1] ?? 0);
236
+ const e2z = (P[c * 3 + 2] ?? 0) - (P[a * 3 + 2] ?? 0);
237
+ const du1 = (U[b * 2] ?? 0) - (U[a * 2] ?? 0);
238
+ const dv1 = -((U[b * 2 + 1] ?? 0) - (U[a * 2 + 1] ?? 0));
239
+ const du2 = (U[c * 2] ?? 0) - (U[a * 2] ?? 0);
240
+ const dv2 = -((U[c * 2 + 1] ?? 0) - (U[a * 2 + 1] ?? 0));
241
+ const det = du1 * dv2 - du2 * dv1;
242
+ if (Math.abs(det) < 0.000000000001)
243
+ continue;
244
+ const r = 1 / det;
245
+ const tx = (e1x * dv2 - e2x * dv1) * r;
246
+ const ty = (e1y * dv2 - e2y * dv1) * r;
247
+ const tz = (e1z * dv2 - e2z * dv1) * r;
248
+ const bx = (e2x * du1 - e1x * du2) * r;
249
+ const by = (e2y * du1 - e1y * du2) * r;
250
+ const bz = (e2z * du1 - e1z * du2) * r;
251
+ for (const v of [a, b, c]) {
252
+ tan[v * 3] = (tan[v * 3] ?? 0) + tx;
253
+ tan[v * 3 + 1] = (tan[v * 3 + 1] ?? 0) + ty;
254
+ tan[v * 3 + 2] = (tan[v * 3 + 2] ?? 0) + tz;
255
+ bit[v * 3] = (bit[v * 3] ?? 0) + bx;
256
+ bit[v * 3 + 1] = (bit[v * 3 + 1] ?? 0) + by;
257
+ bit[v * 3 + 2] = (bit[v * 3 + 2] ?? 0) + bz;
258
+ }
259
+ }
260
+ const out = new Float32Array(n * 4);
261
+ for (let v = 0;v < n; v++) {
262
+ const nx = m.normals[v * 3] ?? 0;
263
+ const ny = m.normals[v * 3 + 1] ?? 0;
264
+ const nz = m.normals[v * 3 + 2] ?? 1;
265
+ let tx = tan[v * 3] ?? 0;
266
+ let ty = tan[v * 3 + 1] ?? 0;
267
+ let tz = tan[v * 3 + 2] ?? 0;
268
+ const d = nx * tx + ny * ty + nz * tz;
269
+ tx -= nx * d;
270
+ ty -= ny * d;
271
+ tz -= nz * d;
272
+ let len = Math.hypot(tx, ty, tz);
273
+ if (len < 0.00000001) {
274
+ [tx, ty, tz] = Math.abs(nx) < 0.9 ? [0, -nz, ny] : [nz, 0, -nx];
275
+ len = Math.hypot(tx, ty, tz) || 1;
276
+ }
277
+ tx /= len;
278
+ ty /= len;
279
+ tz /= len;
280
+ const cx = ny * tz - nz * ty;
281
+ const cy = nz * tx - nx * tz;
282
+ const cz = nx * ty - ny * tx;
283
+ const w = cx * (bit[v * 3] ?? 0) + cy * (bit[v * 3 + 1] ?? 0) + cz * (bit[v * 3 + 2] ?? 0);
284
+ out.set([tx, ty, tz, w < 0 ? -1 : 1], v * 4);
285
+ }
286
+ return out;
287
+ }
288
+
289
+ // src/export/primitive.ts
290
+ function writePrimitive(bin, m, material, tangents) {
291
+ const colors = new Uint16Array(m.colors.length / 3 * 4);
292
+ const q = (v) => Math.round(Math.min(1, Math.max(0, v)) * 65535);
293
+ for (let i = 0;i < m.colors.length / 3; i++)
294
+ colors.set([q(m.colors[i * 3] ?? 1), q(m.colors[i * 3 + 1] ?? 1), q(m.colors[i * 3 + 2] ?? 1), 65535], i * 4);
295
+ return {
296
+ attributes: {
297
+ POSITION: bin.accessor(m.positions, "VEC3", ARRAY_BUFFER, true),
298
+ NORMAL: bin.accessor(m.normals, "VEC3", ARRAY_BUFFER),
299
+ TEXCOORD_0: bin.accessor(m.uvs, "VEC2", ARRAY_BUFFER),
300
+ COLOR_0: bin.accessor(colors, "VEC4", ARRAY_BUFFER, false, true),
301
+ ...tangents ? { TANGENT: bin.accessor(tangentsOf(m), "VEC4", ARRAY_BUFFER) } : {}
302
+ },
303
+ indices: bin.accessor(m.positions.length / 3 <= 65535 ? Uint16Array.from(m.indices) : m.indices, "SCALAR", ELEMENT_ARRAY_BUFFER),
304
+ material
305
+ };
306
+ }
307
+
308
+ // src/export/hierarchy.ts
309
+ var newDoc = (bin) => ({
310
+ bin,
311
+ nodes: [],
312
+ meshes: [],
313
+ lights: [],
314
+ samplers: [],
315
+ channels: [],
316
+ motionSamplers: new Map,
317
+ lightIndex: new Map
318
+ });
319
+ function attach(doc, parentId, id) {
320
+ const parent = doc.nodes[parentId];
321
+ if (!parent)
322
+ return;
323
+ parent["children"] = [...parent["children"] ?? [], id];
324
+ }
325
+ function lightIndexOf(doc, light) {
326
+ const key = JSON.stringify(light);
327
+ const known = doc.lightIndex.get(key);
328
+ if (known !== undefined)
329
+ return known;
330
+ doc.lights.push(light);
331
+ doc.lightIndex.set(key, doc.lights.length - 1);
332
+ return doc.lights.length - 1;
333
+ }
334
+ function addMotion(doc, part, id) {
335
+ if (!part.motion)
336
+ return;
337
+ const key = JSON.stringify([part.motion, part.at]);
338
+ let sampler = doc.motionSamplers.get(key);
339
+ let channel;
340
+ if (sampler === undefined) {
341
+ const written = motionChannel(doc.bin, part.motion, id, part.at);
342
+ doc.samplers.push(written.sampler);
343
+ sampler = doc.samplers.length - 1;
344
+ doc.motionSamplers.set(key, sampler);
345
+ channel = written.channel;
346
+ } else {
347
+ channel = {
348
+ target: { node: id, path: part.motion.kind === "slide" ? "translation" : "rotation" }
349
+ };
350
+ }
351
+ doc.channels.push({ ...channel, sampler });
352
+ }
353
+ function writeHierarchy(doc, asset, rootName, o) {
354
+ doc.nodes.push({ name: rootName, children: [] });
355
+ const root = doc.nodes.length - 1;
356
+ const index = new Map;
357
+ asset.parts.forEach((part, i) => {
358
+ const node = { name: part.name, translation: [...part.at] };
359
+ if (part.yaw)
360
+ node["rotation"] = [...quatFromAxisAngle2([0, 0, 1], part.yaw)];
361
+ if (part.emitter || part.socket) {
362
+ node["extras"] = {
363
+ ts23d: {
364
+ ...part.emitter ? { emitter: part.emitter } : {},
365
+ ...part.socket ? { socket: part.socket } : {}
366
+ }
367
+ };
368
+ }
369
+ if (part.mesh && part.mesh.indices.length > 0) {
370
+ doc.meshes.push({
371
+ name: part.name,
372
+ primitives: [writePrimitive(doc.bin, part.mesh, o.materialOf(part, i), o.withSheet)]
373
+ });
374
+ node["mesh"] = doc.meshes.length - 1;
375
+ }
376
+ doc.nodes.push(node);
377
+ const id = doc.nodes.length - 1;
378
+ index.set(part.name, id);
379
+ attach(doc, part.parent === null ? root : index.get(part.parent) ?? root, id);
380
+ const lit = lightOf(part);
381
+ if (lit) {
382
+ doc.nodes.push({
383
+ name: `${part.name}_light`,
384
+ rotation: [...lit.rotation],
385
+ extensions: { [LIGHTS]: { light: lightIndexOf(doc, lit.light) } }
386
+ });
387
+ attach(doc, id, doc.nodes.length - 1);
388
+ }
389
+ if (o.animations)
390
+ addMotion(doc, part, id);
391
+ });
392
+ return root;
393
+ }
394
+
395
+ // src/export/lod-coverage.ts
396
+ var MSFT_LOD2 = "MSFT_lod";
397
+ var SCREEN_COVERAGE2 = "MSFT_screencoverage";
398
+ var tris = (a) => a.parts.reduce((s, p) => s + (p.mesh ? p.mesh.indices.length / 3 : 0), 0);
399
+ function extent(a) {
400
+ if (a.real)
401
+ return Math.max(a.real.length, a.real.width, a.real.height);
402
+ const lo = [Infinity, Infinity, Infinity];
403
+ const hi = [-Infinity, -Infinity, -Infinity];
404
+ for (const mesh of worldMeshes2(a.parts).values()) {
405
+ const p = mesh.positions;
406
+ for (let i = 0;i < p.length; i++) {
407
+ const k = i % 3;
408
+ const v = p[i] ?? 0;
409
+ lo[k] = Math.min(lo[k] ?? 0, v);
410
+ hi[k] = Math.max(hi[k] ?? 0, v);
411
+ }
412
+ }
413
+ return Math.max(0, ...hi.map((h, k) => h - (lo[k] ?? 0)));
414
+ }
415
+ var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
416
+ var round = (v) => Math.round(v * 1000) / 1000;
417
+ function defaultCoverage2(lods) {
418
+ const byLod = new Map(lods.map((a) => [a.lod, tris(a)]));
419
+ const top = lods[0];
420
+ const t0 = Math.max(1, top ? tris(top) : 1);
421
+ const ratio = (lod) => (byLod.get(lod) ?? t0) / t0;
422
+ const c1 = round(clamp(Math.sqrt(ratio(1)), 0.1, 0.6));
423
+ const c2 = round(clamp(Math.sqrt(ratio(2)), 0.02, c1 / 2));
424
+ const cull = top && extent(top) >= 10 ? 0.002 : 0.01;
425
+ return [c1, c2, Math.min(cull, round(c2 / 2))];
426
+ }
427
+ function checkCoverage2(c, asset) {
428
+ const [a = -1, b = -1, cull = -1] = c;
429
+ if (c.length !== 3 || !(a < 1 && a > b && b > cull && cull >= 0))
430
+ throw new Ts23dError2("T3D_RECIPE_INVALID", {
431
+ cause: `lodCoverage ${JSON.stringify(c)} on '${asset}' is not strictly descending in [0, 1)`,
432
+ fix: "give [LOD0→LOD1, LOD1→LOD2, cull] screen-height fractions, e.g. [0.5, 0.2, 0.01]; 0 = never cull"
433
+ });
434
+ }
435
+ function coverageFor2(levels, c) {
436
+ return levels.map((_, j) => {
437
+ const next = levels[j + 1];
438
+ return next === undefined ? c[2] : c[next - 1] ?? c[2];
439
+ });
440
+ }
441
+
151
442
  // src/export/materials.ts
152
443
  var MATERIALS = {
153
444
  opaque: {
@@ -341,7 +632,7 @@ function mergeToPivots2(asset, glow = []) {
341
632
  }
342
633
 
343
634
  // src/export/version.ts
344
- var VERSION = "0.1.0";
635
+ var VERSION = "0.2.0";
345
636
  var GENERATOR = `@developerz.ai/ts23d ${VERSION}`;
346
637
 
347
638
  // src/export/metadata.ts
@@ -371,6 +662,7 @@ function metadata(name, meta) {
371
662
  ...meta.copyright ? { "dc:rights": alt(meta.copyright) } : {},
372
663
  ...meta.license ? { "xmpRights:UsageTerms": alt(meta.license) } : {},
373
664
  ...meta.sources?.length ? { "dc:relation": { "@set": [...meta.sources] } } : {},
665
+ ...meta.credits?.length ? { "dc:contributor": { "@set": [...meta.credits] } } : {},
374
666
  "xmp:CreatorTool": GENERATOR
375
667
  };
376
668
  return {
@@ -380,51 +672,6 @@ function metadata(name, meta) {
380
672
  };
381
673
  }
382
674
 
383
- // src/export/motion.ts
384
- var LOOP_SECONDS = 4;
385
- var cyclesOf = (rate) => Math.max(1, Math.round(Math.abs(rate) * LOOP_SECONDS)) * Math.sign(rate || 1);
386
- var timeAccessors = new WeakMap;
387
- function times(bin, keys) {
388
- const t = new Float32Array(keys + 1).map((_, i) => i / keys * LOOP_SECONDS);
389
- const byBin = timeAccessors.get(bin) ?? new Map;
390
- timeAccessors.set(bin, byBin);
391
- let input = byBin.get(keys);
392
- if (input === undefined) {
393
- input = bin.accessor(t, "SCALAR", undefined, true);
394
- byBin.set(keys, input);
395
- }
396
- return { input, t };
397
- }
398
- function motionChannel(bin, motion, node, rest) {
399
- const rate = motion.kind === "spin" ? motion.rps : motion.hz;
400
- const cycles = cyclesOf(rate);
401
- const keys = Math.max(24, Math.abs(cycles) * (motion.kind === "spin" ? 12 : 16));
402
- const { input, t } = times(bin, keys);
403
- const phase = (time) => time / LOOP_SECONDS * cycles * Math.PI * 2;
404
- const wave = (time) => motion.kind !== "spin" && motion.oneWay ? (1 - Math.cos(phase(time))) / 2 : Math.sin(phase(time));
405
- if (motion.kind === "slide") {
406
- const values = new Float32Array((keys + 1) * 3);
407
- t.forEach((time, i) => {
408
- const s = wave(time);
409
- values.set([0, 1, 2].map((k) => (rest[k] ?? 0) + (motion.offset[k] ?? 0) * s), i * 3);
410
- });
411
- return {
412
- sampler: { input, output: bin.accessor(values, "VEC3"), interpolation: "LINEAR" },
413
- channel: { target: { node, path: "translation" } }
414
- };
415
- }
416
- const values = new Float32Array((keys + 1) * 4);
417
- const peak = motion.kind === "sway" ? motion.degrees * Math.PI / 180 : 0;
418
- t.forEach((time, i) => {
419
- const angle = motion.kind === "spin" ? phase(time) : peak * wave(time);
420
- values.set(quatFromAxisAngle2(motion.axis, angle), i * 4);
421
- });
422
- return {
423
- sampler: { input, output: bin.accessor(values, "VEC4"), interpolation: "LINEAR" },
424
- channel: { target: { node, path: "rotation" } }
425
- };
426
- }
427
-
428
675
  // src/export/optimize-parts.ts
429
676
  function summarize2(parts, optimized) {
430
677
  const sum = (f) => parts.reduce((s, r) => s + f(r), 0);
@@ -476,94 +723,6 @@ function optimizeParts2(asset, o) {
476
723
  return { asset: { ...asset, parts }, report: summarize2(reports, o !== false) };
477
724
  }
478
725
 
479
- // src/export/tangents.ts
480
- function tangentsOf(m) {
481
- const n = m.positions.length / 3;
482
- const tan = new Float32Array(n * 3);
483
- const bit = new Float32Array(n * 3);
484
- const P = m.positions;
485
- const U = m.uvs;
486
- for (let t = 0;t < m.indices.length; t += 3) {
487
- const a = m.indices[t] ?? 0;
488
- const b = m.indices[t + 1] ?? 0;
489
- const c = m.indices[t + 2] ?? 0;
490
- const e1x = (P[b * 3] ?? 0) - (P[a * 3] ?? 0);
491
- const e1y = (P[b * 3 + 1] ?? 0) - (P[a * 3 + 1] ?? 0);
492
- const e1z = (P[b * 3 + 2] ?? 0) - (P[a * 3 + 2] ?? 0);
493
- const e2x = (P[c * 3] ?? 0) - (P[a * 3] ?? 0);
494
- const e2y = (P[c * 3 + 1] ?? 0) - (P[a * 3 + 1] ?? 0);
495
- const e2z = (P[c * 3 + 2] ?? 0) - (P[a * 3 + 2] ?? 0);
496
- const du1 = (U[b * 2] ?? 0) - (U[a * 2] ?? 0);
497
- const dv1 = -((U[b * 2 + 1] ?? 0) - (U[a * 2 + 1] ?? 0));
498
- const du2 = (U[c * 2] ?? 0) - (U[a * 2] ?? 0);
499
- const dv2 = -((U[c * 2 + 1] ?? 0) - (U[a * 2 + 1] ?? 0));
500
- const det = du1 * dv2 - du2 * dv1;
501
- if (Math.abs(det) < 0.000000000001)
502
- continue;
503
- const r = 1 / det;
504
- const tx = (e1x * dv2 - e2x * dv1) * r;
505
- const ty = (e1y * dv2 - e2y * dv1) * r;
506
- const tz = (e1z * dv2 - e2z * dv1) * r;
507
- const bx = (e2x * du1 - e1x * du2) * r;
508
- const by = (e2y * du1 - e1y * du2) * r;
509
- const bz = (e2z * du1 - e1z * du2) * r;
510
- for (const v of [a, b, c]) {
511
- tan[v * 3] = (tan[v * 3] ?? 0) + tx;
512
- tan[v * 3 + 1] = (tan[v * 3 + 1] ?? 0) + ty;
513
- tan[v * 3 + 2] = (tan[v * 3 + 2] ?? 0) + tz;
514
- bit[v * 3] = (bit[v * 3] ?? 0) + bx;
515
- bit[v * 3 + 1] = (bit[v * 3 + 1] ?? 0) + by;
516
- bit[v * 3 + 2] = (bit[v * 3 + 2] ?? 0) + bz;
517
- }
518
- }
519
- const out = new Float32Array(n * 4);
520
- for (let v = 0;v < n; v++) {
521
- const nx = m.normals[v * 3] ?? 0;
522
- const ny = m.normals[v * 3 + 1] ?? 0;
523
- const nz = m.normals[v * 3 + 2] ?? 1;
524
- let tx = tan[v * 3] ?? 0;
525
- let ty = tan[v * 3 + 1] ?? 0;
526
- let tz = tan[v * 3 + 2] ?? 0;
527
- const d = nx * tx + ny * ty + nz * tz;
528
- tx -= nx * d;
529
- ty -= ny * d;
530
- tz -= nz * d;
531
- let len = Math.hypot(tx, ty, tz);
532
- if (len < 0.00000001) {
533
- [tx, ty, tz] = Math.abs(nx) < 0.9 ? [0, -nz, ny] : [nz, 0, -nx];
534
- len = Math.hypot(tx, ty, tz) || 1;
535
- }
536
- tx /= len;
537
- ty /= len;
538
- tz /= len;
539
- const cx = ny * tz - nz * ty;
540
- const cy = nz * tx - nx * tz;
541
- const cz = nx * ty - ny * tx;
542
- const w = cx * (bit[v * 3] ?? 0) + cy * (bit[v * 3 + 1] ?? 0) + cz * (bit[v * 3 + 2] ?? 0);
543
- out.set([tx, ty, tz, w < 0 ? -1 : 1], v * 4);
544
- }
545
- return out;
546
- }
547
-
548
- // src/export/primitive.ts
549
- function writePrimitive(bin, m, material, tangents) {
550
- const colors = new Uint16Array(m.colors.length / 3 * 4);
551
- const q = (v) => Math.round(Math.min(1, Math.max(0, v)) * 65535);
552
- for (let i = 0;i < m.colors.length / 3; i++)
553
- colors.set([q(m.colors[i * 3] ?? 1), q(m.colors[i * 3 + 1] ?? 1), q(m.colors[i * 3 + 2] ?? 1), 65535], i * 4);
554
- return {
555
- attributes: {
556
- POSITION: bin.accessor(m.positions, "VEC3", ARRAY_BUFFER, true),
557
- NORMAL: bin.accessor(m.normals, "VEC3", ARRAY_BUFFER),
558
- TEXCOORD_0: bin.accessor(m.uvs, "VEC2", ARRAY_BUFFER),
559
- COLOR_0: bin.accessor(colors, "VEC4", ARRAY_BUFFER, false, true),
560
- ...tangents ? { TANGENT: bin.accessor(tangentsOf(m), "VEC4", ARRAY_BUFFER) } : {}
561
- },
562
- indices: bin.accessor(m.positions.length / 3 <= 65535 ? Uint16Array.from(m.indices) : m.indices, "SCALAR", ELEMENT_ARRAY_BUFFER),
563
- material
564
- };
565
- }
566
-
567
726
  // src/export/textures.ts
568
727
  var SHEET_MAPS2 = ["albedo", "normal", "orm"];
569
728
  var sheetFileName2 = (map, prefix = "trim_sheet") => `${prefix}_${map}.png`;
@@ -620,18 +779,25 @@ function sheetGltf(sheet, views, offset) {
620
779
 
621
780
  // src/export/glb.ts
622
781
  var Z_UP_TO_Y_UP = [-0.5, -0.5, -0.5, 0.5];
623
- function restClip(bin) {
782
+ function restClip(bin, roots) {
624
783
  const input = bin.accessor(new Float32Array([0, 1]), "SCALAR", undefined, true);
625
784
  const output = bin.accessor(new Float32Array([...Z_UP_TO_Y_UP, ...Z_UP_TO_Y_UP]), "VEC4");
626
785
  return {
627
786
  name: "rest",
628
787
  samplers: [{ input, output, interpolation: "STEP" }],
629
- channels: [{ sampler: 0, target: { node: 0, path: "rotation" } }]
788
+ channels: roots.map((node) => ({ sampler: 0, target: { node, path: "rotation" } }))
630
789
  };
631
790
  }
632
791
  function toGlb2(input, o = {}) {
633
792
  return exportGlb2(input, o).glb;
634
793
  }
794
+ function exportGlb2(input, o = {}) {
795
+ const { glb, reports } = exportLodGlb2([input], o);
796
+ return { glb, report: reports[0] ?? summarize2([], false) };
797
+ }
798
+ function toLodGlb2(lods, o = {}) {
799
+ return exportLodGlb2(lods, o).glb;
800
+ }
635
801
  function checkGlow(rules) {
636
802
  for (const r of rules)
637
803
  if ((r.strength ?? 4) < 0 || (r.color ?? [0, 0, 0]).some((c) => c < 0 || c > 1))
@@ -640,106 +806,87 @@ function checkGlow(rules) {
640
806
  fix: "colour is linear 0..1 (hex() converts sRGB); brightness goes in `strength`"
641
807
  });
642
808
  }
643
- function exportGlb2(input, o = {}) {
644
- const glow = o.glow ?? input.glow ?? [];
645
- checkGlow(glow);
809
+ function checkLods(lods) {
810
+ const names = new Set(lods.map((l) => l.name));
811
+ const levels = lods.map((l) => l.lod);
812
+ if (lods.length === 0 || names.size !== 1 || levels.some((l, i) => i > 0 && l <= (levels[i - 1] ?? 0)))
813
+ throw new Ts23dError2("T3D_RECIPE_INVALID", {
814
+ cause: `exportLodGlb got LODs [${levels.join(", ")}] of [${[...names].join(", ")}]`,
815
+ fix: "pass compile(asset, { lod }) results of ONE asset, highest detail first: LOD0, LOD1, LOD2"
816
+ });
817
+ }
818
+ function prepare(input, o, glow) {
646
819
  const opt = o.optimize === false ? false : o.optimize === true ? {} : o.optimize ?? {};
647
820
  const merged = o.keepParts ? input : mergeToPivots2(input, glow.map((g) => g.parts));
648
821
  const { asset, report } = optimizeParts2(merged, opt);
649
822
  const glowRes = glow.map((g) => g.parts.map(globRe));
650
823
  const glowOf = (p, i) => glowLayer.get(merged.parts[i] ?? p) ?? (o.keepParts ? glowRes.findIndex((rs) => rs.some((r) => r.test(p.name))) : -1);
651
- const mats = materialsFor(new Set(asset.parts.flatMap((p) => p.surface ? [p.surface] : [])), o.sheet !== undefined);
824
+ return { asset, report, glowOf };
825
+ }
826
+ function exportLodGlb2(lods, o = {}) {
827
+ checkLods(lods);
828
+ const top = lods[0];
829
+ const glow = o.glow ?? top.glow ?? [];
830
+ checkGlow(glow);
831
+ const preps = lods.map((l) => prepare(l, o, glow));
832
+ const withSheet = o.sheet !== undefined;
833
+ const mats = materialsFor(new Set(preps.flatMap((p) => p.asset.parts.flatMap((q) => q.surface ? [q.surface] : []))), withSheet);
652
834
  const glowAt = mats.materials.length;
653
- const materials = [
654
- ...mats.materials,
655
- ...glow.map((g, i) => glowMaterial(g, i, o.sheet !== undefined))
656
- ];
657
- const bin = new BinWriter;
658
- const meshes = [];
659
- const nodes = [{ name: asset.name, children: [] }];
660
- const index = new Map;
661
- const samplers = [];
662
- const channels = [];
663
- const lights = [];
664
- const attach = (parentId, id) => {
665
- const parent = nodes[parentId];
666
- if (!parent)
667
- return;
668
- parent["children"] = [...parent["children"] ?? [], id];
669
- };
670
- asset.parts.forEach((part, i) => {
671
- const node = { name: part.name, translation: [...part.at] };
672
- if (part.yaw)
673
- node["rotation"] = [...quatFromAxisAngle2([0, 0, 1], part.yaw)];
674
- if (part.emitter || part.socket) {
675
- node["extras"] = {
676
- ts23d: {
677
- ...part.emitter ? { emitter: part.emitter } : {},
678
- ...part.socket ? { socket: part.socket } : {}
679
- }
680
- };
681
- }
682
- if (part.mesh && part.mesh.indices.length > 0) {
835
+ const materials = [...mats.materials, ...glow.map((g, i) => glowMaterial(g, i, withSheet))];
836
+ const doc = newDoc(new BinWriter);
837
+ const roots = preps.map(({ asset, glowOf }, k) => writeHierarchy(doc, asset, k === 0 ? asset.name : `${asset.name}_LOD${asset.lod}`, {
838
+ withSheet,
839
+ animations: o.animations === true,
840
+ materialOf: (part, i) => {
683
841
  const g = glowOf(part, i);
684
- const material = g >= 0 ? glowAt + g : mats.index.get(part.surface ?? "opaque") ?? 0;
685
- meshes.push({
686
- name: part.name,
687
- primitives: [writePrimitive(bin, part.mesh, material, o.sheet !== undefined)]
688
- });
689
- node["mesh"] = meshes.length - 1;
842
+ return g >= 0 ? glowAt + g : mats.index.get(part.surface ?? "opaque") ?? 0;
690
843
  }
691
- nodes.push(node);
692
- const id = nodes.length - 1;
693
- index.set(part.name, id);
694
- attach(part.parent === null ? 0 : index.get(part.parent) ?? 0, id);
695
- const lit = lightOf(part);
696
- if (lit) {
697
- lights.push(lit.light);
698
- nodes.push({
699
- name: `${part.name}_light`,
700
- rotation: [...lit.rotation],
701
- extensions: { [LIGHTS]: { light: lights.length - 1 } }
702
- });
703
- attach(id, nodes.length - 1);
704
- }
705
- if (part.motion && o.animations) {
706
- const { sampler, channel } = motionChannel(bin, part.motion, id, part.at);
707
- samplers.push(sampler);
708
- channels.push({ ...channel, sampler: samplers.length - 1 });
709
- }
710
- });
711
- const animations = channels.length ? [restClip(bin), { name: "run", samplers, channels }] : [];
844
+ }));
845
+ for (const r of roots) {
846
+ const node = doc.nodes[r];
847
+ if (node)
848
+ node["rotation"] = Z_UP_TO_Y_UP;
849
+ }
850
+ const coverage = [];
851
+ const rootNode = doc.nodes[0];
852
+ if (roots.length > 1 && rootNode) {
853
+ const c = o.lodCoverage ?? top.lodCoverage ?? defaultCoverage2(lods);
854
+ checkCoverage2(c, top.name);
855
+ coverage.push(...coverageFor2(lods.map((l) => l.lod), c));
856
+ rootNode["extensions"] = { [MSFT_LOD2]: { ids: roots.slice(1) } };
857
+ rootNode["extras"] = { [SCREEN_COVERAGE2]: coverage };
858
+ }
859
+ const { bin, lights, samplers, channels } = doc;
860
+ const animations = channels.length ? [restClip(bin, roots), { name: "run", samplers, channels }] : [];
712
861
  const geometry = bin.bytes();
713
862
  const tex = o.sheet ? sheetGltf(o.sheet, bin.views, geometry.byteLength) : null;
714
863
  const body = tex?.tail.byteLength ? concat(geometry, tex.tail) : geometry;
715
- const meta = metadata(asset.name, o.meta ?? input.meta);
864
+ const meta = metadata(top.name, withCredits(top, o.meta ?? top.meta));
716
865
  const used = [
717
866
  ...mats.extensionsUsed,
718
867
  ...glow.some((g) => (g.strength ?? 4) !== 1) ? [EMISSIVE_STRENGTH] : [],
719
868
  ...lights.length ? [LIGHTS] : [],
869
+ ...coverage.length ? [MSFT_LOD2] : [],
720
870
  ...meta.used
721
871
  ].sort();
722
872
  const extensions = {
723
873
  ...lights.length ? { [LIGHTS]: { lights } } : {},
724
874
  ...meta.extensions ?? {}
725
875
  };
726
- const root = nodes[0];
727
- if (root)
728
- root["rotation"] = Z_UP_TO_Y_UP;
729
876
  const gltf = {
730
877
  asset: meta.asset,
731
878
  extras: {
732
- kind: asset.kind,
733
- lod: asset.lod,
879
+ kind: top.kind,
880
+ lod: coverage.length ? lods.map((l) => l.lod) : top.lod,
734
881
  units: "metres",
735
882
  up: "Z (rotated to glTF Y-up at the root)"
736
883
  },
737
884
  ...used.length ? { extensionsUsed: used } : {},
738
885
  ...Object.keys(extensions).length ? { extensions } : {},
739
886
  scene: 0,
740
- scenes: [{ name: asset.name, nodes: [0] }],
741
- nodes,
742
- meshes,
887
+ scenes: [{ name: top.name, nodes: [0] }],
888
+ nodes: doc.nodes,
889
+ meshes: doc.meshes,
743
890
  materials,
744
891
  ...tex ? tex.json : {},
745
892
  buffers: [{ byteLength: body.byteLength }],
@@ -747,9 +894,9 @@ function exportGlb2(input, o = {}) {
747
894
  accessors: bin.accessors,
748
895
  ...animations.length ? { animations } : {}
749
896
  };
750
- return { glb: packGlb(gltf, body), report };
897
+ return { glb: packGlb(gltf, body), reports: preps.map((p) => p.report), coverage };
751
898
  }
752
- export { mergeToPivots2, summarize2, optimizeParts2, SHEET_MAPS2, sheetFileName2, sheetPngs2, toGlb2, exportGlb2 };
899
+ export { MSFT_LOD2, SCREEN_COVERAGE2, defaultCoverage2, checkCoverage2, coverageFor2, mergeToPivots2, summarize2, optimizeParts2, SHEET_MAPS2, sheetFileName2, sheetPngs2, toGlb2, exportGlb2, toLodGlb2, exportLodGlb2 };
753
900
 
754
- //# debugId=635FF0E967FF13E664756E2164756E21
755
- //# sourceMappingURL=chunk-gcyp9b4d.js.map
901
+ //# debugId=48DF0BBA03847F0964756E2164756E21
902
+ //# sourceMappingURL=chunk-dbq4qf52.js.map