@driftengine/assets 3.61.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 (100) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +9 -0
  4. package/dist/assetPath.d.ts +51 -0
  5. package/dist/assetPath.js +59 -0
  6. package/dist/coarseLevel.d.ts +94 -0
  7. package/dist/coarseLevel.js +606 -0
  8. package/dist/dds.d.ts +41 -0
  9. package/dist/dds.js +425 -0
  10. package/dist/drawKey.d.ts +64 -0
  11. package/dist/drawKey.js +42 -0
  12. package/dist/drftLoader.d.ts +420 -0
  13. package/dist/drftLoader.js +996 -0
  14. package/dist/drftTextures.d.ts +87 -0
  15. package/dist/drftTextures.js +138 -0
  16. package/dist/fbx.d.ts +86 -0
  17. package/dist/fbx.js +792 -0
  18. package/dist/fbxHarness.d.ts +68 -0
  19. package/dist/fbxHarness.js +185 -0
  20. package/dist/fbxInflate.d.ts +60 -0
  21. package/dist/fbxInflate.js +204 -0
  22. package/dist/fbxMaterial.d.ts +87 -0
  23. package/dist/fbxMaterial.js +345 -0
  24. package/dist/fbxRig.d.ts +130 -0
  25. package/dist/fbxRig.js +601 -0
  26. package/dist/gltf.d.ts +242 -0
  27. package/dist/gltf.js +904 -0
  28. package/dist/gltfAccessor.d.ts +2 -0
  29. package/dist/gltfAccessor.js +106 -0
  30. package/dist/gltfSkin.d.ts +32 -0
  31. package/dist/gltfSkin.js +144 -0
  32. package/dist/iesPhotometry.d.ts +44 -0
  33. package/dist/iesPhotometry.js +94 -0
  34. package/dist/imageInfo.d.ts +31 -0
  35. package/dist/imageInfo.js +119 -0
  36. package/dist/index.d.ts +58 -0
  37. package/dist/index.js +132 -0
  38. package/dist/kn5.d.ts +158 -0
  39. package/dist/kn5.js +557 -0
  40. package/dist/loadProgress.d.ts +118 -0
  41. package/dist/loadProgress.js +1 -0
  42. package/dist/localise.d.ts +35 -0
  43. package/dist/localise.js +253 -0
  44. package/dist/obj.d.ts +54 -0
  45. package/dist/obj.js +335 -0
  46. package/dist/orient.d.ts +104 -0
  47. package/dist/orient.js +241 -0
  48. package/dist/radianceHdr.d.ts +36 -0
  49. package/dist/radianceHdr.js +172 -0
  50. package/dist/readModel.d.ts +153 -0
  51. package/dist/readModel.js +386 -0
  52. package/dist/recognise.d.ts +38 -0
  53. package/dist/recognise.js +97 -0
  54. package/dist/stl.d.ts +22 -0
  55. package/dist/stl.js +131 -0
  56. package/dist/tangentOrder.d.ts +24 -0
  57. package/dist/tangentOrder.js +31 -0
  58. package/dist/threemf.d.ts +46 -0
  59. package/dist/threemf.js +254 -0
  60. package/dist/uploadBudget.d.ts +82 -0
  61. package/dist/uploadBudget.js +86 -0
  62. package/dist/usd.d.ts +49 -0
  63. package/dist/usd.js +542 -0
  64. package/dist/weld.d.ts +44 -0
  65. package/dist/weld.js +237 -0
  66. package/dist/zip.d.ts +35 -0
  67. package/dist/zip.js +110 -0
  68. package/package.json +61 -0
  69. package/src/assetPath.ts +79 -0
  70. package/src/coarseLevel.ts +720 -0
  71. package/src/dds.ts +507 -0
  72. package/src/drawKey.ts +91 -0
  73. package/src/drftLoader.ts +1197 -0
  74. package/src/drftTextures.ts +153 -0
  75. package/src/fbx.ts +934 -0
  76. package/src/fbxHarness.ts +215 -0
  77. package/src/fbxInflate.ts +258 -0
  78. package/src/fbxMaterial.ts +410 -0
  79. package/src/fbxRig.ts +652 -0
  80. package/src/gltf.ts +1161 -0
  81. package/src/gltfAccessor.ts +127 -0
  82. package/src/gltfSkin.ts +210 -0
  83. package/src/iesPhotometry.ts +136 -0
  84. package/src/imageInfo.ts +130 -0
  85. package/src/index.ts +162 -0
  86. package/src/kn5.ts +705 -0
  87. package/src/loadProgress.ts +131 -0
  88. package/src/localise.ts +275 -0
  89. package/src/obj.ts +386 -0
  90. package/src/orient.ts +262 -0
  91. package/src/radianceHdr.ts +195 -0
  92. package/src/readModel.ts +515 -0
  93. package/src/recognise.ts +124 -0
  94. package/src/stl.ts +147 -0
  95. package/src/tangentOrder.ts +32 -0
  96. package/src/threemf.ts +303 -0
  97. package/src/uploadBudget.ts +90 -0
  98. package/src/usd.ts +642 -0
  99. package/src/weld.ts +264 -0
  100. package/src/zip.ts +130 -0
package/dist/stl.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * STL, binary and ASCII.
3
+ *
4
+ * The simplest format in the pipeline and the least informative: triangles and a facet
5
+ * normal, with no colour, no UVs, no hierarchy and no materials. Everything a `MeshData`
6
+ * carries beyond position and normal has to be supplied by the caller, and this reader
7
+ * says so rather than inventing it.
8
+ *
9
+ * **Shaded flat, deliberately.** An STL has one normal per *face*, so its vertices cannot
10
+ * be shared and there is nothing to average. Welding them and smoothing would make a
11
+ * printed part look like an organic model — a change to the artist's intent made silently
12
+ * by a reader, which is not a reader's decision to make. A smoothing pass belongs in the
13
+ * baker, behind a flag, where somebody chose it.
14
+ */
15
+ import type { MeshData } from '@driftengine/drft';
16
+ /** What a caller may supply, since the format carries none of it. */
17
+ export interface StlAppearance {
18
+ readonly color?: readonly [number, number, number];
19
+ readonly roughness?: number;
20
+ readonly specular?: number;
21
+ }
22
+ export declare function parseStl(buffer: ArrayBuffer, appearance?: StlAppearance): MeshData;
package/dist/stl.js ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * STL, binary and ASCII.
3
+ *
4
+ * The simplest format in the pipeline and the least informative: triangles and a facet
5
+ * normal, with no colour, no UVs, no hierarchy and no materials. Everything a `MeshData`
6
+ * carries beyond position and normal has to be supplied by the caller, and this reader
7
+ * says so rather than inventing it.
8
+ *
9
+ * **Shaded flat, deliberately.** An STL has one normal per *face*, so its vertices cannot
10
+ * be shared and there is nothing to average. Welding them and smoothing would make a
11
+ * printed part look like an organic model — a change to the artist's intent made silently
12
+ * by a reader, which is not a reader's decision to make. A smoothing pass belongs in the
13
+ * baker, behind a flag, where somebody chose it.
14
+ */
15
+ import { DrftError } from '@driftengine/drft';
16
+ /**
17
+ * Binary STL is 80 bytes of header, a count, then 50 bytes per triangle. An ASCII one
18
+ * begins with `solid` — but so do some binary files, so the length is checked as well.
19
+ * That check is the whole of the detection and it is exact rather than a guess.
20
+ */
21
+ function looksBinary(bytes) {
22
+ if (bytes.length < 84)
23
+ return false;
24
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
25
+ const triangles = view.getUint32(80, true);
26
+ return bytes.length === 84 + triangles * 50;
27
+ }
28
+ export function parseStl(buffer, appearance = {}) {
29
+ const bytes = new Uint8Array(buffer);
30
+ const triangles = looksBinary(bytes)
31
+ ? readBinary(bytes)
32
+ : readAscii(new TextDecoder().decode(bytes));
33
+ if (triangles.length === 0)
34
+ throw new DrftError('stl: no triangles');
35
+ const count = triangles.length * 3;
36
+ const positions = new Float32Array(count * 3);
37
+ const normals = new Float32Array(count * 3);
38
+ const colors = new Float32Array(count * 3);
39
+ const indices = new Uint32Array(count);
40
+ const [r, g, b] = appearance.color ?? [0.8, 0.8, 0.82];
41
+ for (let t = 0; t < triangles.length; t++) {
42
+ const triangle = triangles[t];
43
+ for (let c = 0; c < 3; c++) {
44
+ const at = (t * 3 + c) * 3;
45
+ positions[at] = triangle.vertices[c * 3];
46
+ positions[at + 1] = triangle.vertices[c * 3 + 1];
47
+ positions[at + 2] = triangle.vertices[c * 3 + 2];
48
+ normals[at] = triangle.normal[0];
49
+ normals[at + 1] = triangle.normal[1];
50
+ normals[at + 2] = triangle.normal[2];
51
+ colors[at] = r;
52
+ colors[at + 1] = g;
53
+ colors[at + 2] = b;
54
+ indices[t * 3 + c] = t * 3 + c;
55
+ }
56
+ }
57
+ return {
58
+ positions,
59
+ normals,
60
+ colors,
61
+ emissive: new Float32Array(count),
62
+ /* Only what the caller named: an unasked-for attribute is the engine's own default. */
63
+ ...(appearance.specular === undefined
64
+ ? {}
65
+ : { specular: new Float32Array(count).fill(appearance.specular) }),
66
+ ...(appearance.roughness === undefined
67
+ ? {}
68
+ : { roughness: new Float32Array(count).fill(appearance.roughness) }),
69
+ indices,
70
+ };
71
+ }
72
+ /**
73
+ * A facet normal of zero is legal and common — exporters write it to mean "work it out".
74
+ * Computed from the winding rather than passed through, because a zero normal is not a
75
+ * neutral value: every lighting term multiplies by it, so the part would arrive black.
76
+ */
77
+ function withNormal(normal, vertices) {
78
+ if (Math.hypot(normal[0], normal[1], normal[2]) > 1e-8)
79
+ return { normal, vertices };
80
+ const ux = vertices[3] - vertices[0];
81
+ const uy = vertices[4] - vertices[1];
82
+ const uz = vertices[5] - vertices[2];
83
+ const vx = vertices[6] - vertices[0];
84
+ const vy = vertices[7] - vertices[1];
85
+ const vz = vertices[8] - vertices[2];
86
+ const nx = uy * vz - uz * vy;
87
+ const ny = uz * vx - ux * vz;
88
+ const nz = ux * vy - uy * vx;
89
+ const length = Math.hypot(nx, ny, nz) || 1;
90
+ return { normal: [nx / length, ny / length, nz / length], vertices };
91
+ }
92
+ function readBinary(bytes) {
93
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
94
+ const count = view.getUint32(80, true);
95
+ const triangles = [];
96
+ let at = 84;
97
+ for (let i = 0; i < count; i++) {
98
+ const normal = [
99
+ view.getFloat32(at, true),
100
+ view.getFloat32(at + 4, true),
101
+ view.getFloat32(at + 8, true),
102
+ ];
103
+ const vertices = [];
104
+ for (let v = 0; v < 9; v++)
105
+ vertices.push(view.getFloat32(at + 12 + v * 4, true));
106
+ triangles.push(withNormal(normal, vertices));
107
+ at += 50;
108
+ }
109
+ return triangles;
110
+ }
111
+ function readAscii(text) {
112
+ const triangles = [];
113
+ let normal = [0, 0, 0];
114
+ let vertices = [];
115
+ for (const rawLine of text.split('\n')) {
116
+ const parts = rawLine.trim().split(/\s+/);
117
+ if (parts[0] === 'facet' && parts[1] === 'normal') {
118
+ normal = [Number(parts[2]), Number(parts[3]), Number(parts[4])];
119
+ vertices = [];
120
+ }
121
+ else if (parts[0] === 'vertex') {
122
+ vertices.push(Number(parts[1]), Number(parts[2]), Number(parts[3]));
123
+ }
124
+ else if (parts[0] === 'endfacet') {
125
+ if (vertices.length === 9)
126
+ triangles.push(withNormal(normal, vertices));
127
+ vertices = [];
128
+ }
129
+ }
130
+ return triangles;
131
+ }
@@ -0,0 +1,24 @@
1
+ import type { MeshData } from '@driftengine/drft';
2
+ /**
3
+ * Derive a tangent frame for a mesh that has none, **after** it has been welded.
4
+ *
5
+ * **The order is the point of this function existing.** `generateTangents` accumulates a frame per
6
+ * index, so a vertex shared by several triangles gets the sum of all of them and comes out smooth
7
+ * — which is what it was written to do. Run on a mesh as a file stores it, and a great many files
8
+ * store one vertex per triangle corner, every corner is its own index and gets exactly one
9
+ * triangle's frame. Two corners at the same point then disagree, and the weld cannot merge them,
10
+ * because two corners alike in position, normal and UV but opposite in the bitangent's sign are a
11
+ * mirrored UV shell and merging those lights one side of a model inside out. Nothing can tell the
12
+ * two cases apart from the data.
13
+ *
14
+ * **Measured on a rotationally unwrapped corner soup**, which is the shape a CAD conversion
15
+ * arrives in: 9,600 corners weld to 1,681 vertices with no frame present and to 9,482 with a frame
16
+ * derived per corner, with up to six different frames at one point. Deriving afterwards gives
17
+ * 1,681 vertices and one frame each, and the frame is *better* — averaged over every triangle at
18
+ * the vertex, which is the smoothing the derivation exists for and which a soup cannot supply.
19
+ *
20
+ * A mesh that already carries a frame is returned untouched: authored data is the file's to state.
21
+ * A mesh with no texture coordinates is returned untouched too, since there is no texture
22
+ * direction to derive one along.
23
+ */
24
+ export declare function deriveTangentsFor(mesh: MeshData): MeshData;
@@ -0,0 +1,31 @@
1
+ import { generateTangents } from '@driftengine/core';
2
+ /**
3
+ * Derive a tangent frame for a mesh that has none, **after** it has been welded.
4
+ *
5
+ * **The order is the point of this function existing.** `generateTangents` accumulates a frame per
6
+ * index, so a vertex shared by several triangles gets the sum of all of them and comes out smooth
7
+ * — which is what it was written to do. Run on a mesh as a file stores it, and a great many files
8
+ * store one vertex per triangle corner, every corner is its own index and gets exactly one
9
+ * triangle's frame. Two corners at the same point then disagree, and the weld cannot merge them,
10
+ * because two corners alike in position, normal and UV but opposite in the bitangent's sign are a
11
+ * mirrored UV shell and merging those lights one side of a model inside out. Nothing can tell the
12
+ * two cases apart from the data.
13
+ *
14
+ * **Measured on a rotationally unwrapped corner soup**, which is the shape a CAD conversion
15
+ * arrives in: 9,600 corners weld to 1,681 vertices with no frame present and to 9,482 with a frame
16
+ * derived per corner, with up to six different frames at one point. Deriving afterwards gives
17
+ * 1,681 vertices and one frame each, and the frame is *better* — averaged over every triangle at
18
+ * the vertex, which is the smoothing the derivation exists for and which a soup cannot supply.
19
+ *
20
+ * A mesh that already carries a frame is returned untouched: authored data is the file's to state.
21
+ * A mesh with no texture coordinates is returned untouched too, since there is no texture
22
+ * direction to derive one along.
23
+ */
24
+ export function deriveTangentsFor(mesh) {
25
+ if (mesh.tangents !== undefined || mesh.uvs === undefined)
26
+ return mesh;
27
+ return {
28
+ ...mesh,
29
+ tangents: generateTangents(mesh.positions, mesh.normals, mesh.uvs, mesh.indices),
30
+ };
31
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * 3MF, the 3D Manufacturing Format: a zip of XML from the 3MF Consortium.
3
+ *
4
+ * Tier 1. The specification is published and the core is small — a mesh is a vertex list and
5
+ * a triangle list, and that has not changed since the format was introduced.
6
+ *
7
+ * **The same zip reader as `.usdz`, and the reason the two were built together.** The
8
+ * difference is one parameter: USD requires its entries *stored* so a runtime can memory
9
+ * map them, and 3MF uses ordinary deflate, so this one needs the injected inflate and that
10
+ * one does not. Injected rather than imported for the same reason as `fbx.ts`: this module
11
+ * is part of an engine that runs in a browser and must not reach for Node's `zlib`.
12
+ *
13
+ * **A printing format, so what it does not carry matters as much as what it does.** There
14
+ * are no normals: a 3MF mesh is a closed manifold and a slicer derives what it needs, so
15
+ * they are computed here from the triangles. There are no texture coordinates in the core
16
+ * specification either — the materials extension adds them, and until an asset turns up
17
+ * that uses it, claiming support would be claiming something untested.
18
+ */
19
+ import type { MeshData } from '@driftengine/drft';
20
+ import type { DrftMaterial } from '@driftengine/drft';
21
+ import type { Inflate } from './fbx.ts';
22
+ import type { UpAxis } from './orient.ts';
23
+ /** What a `.3mf` yields, in the shape every other reader in this directory returns. */
24
+ export interface ThreeMfResult {
25
+ readonly meshes: MeshData[];
26
+ /** One per mesh, by ordinal. */
27
+ readonly materials: DrftMaterial[];
28
+ readonly warnings: string[];
29
+ /**
30
+ * 3MF is Z-up and says so in its specification rather than per file, so this is a fact
31
+ * about the format rather than something read out of the document.
32
+ */
33
+ readonly declaredUp: UpAxis;
34
+ /** Metres per unit, from the model's `unit` attribute. Millimetres is the default. */
35
+ readonly unitScale: number;
36
+ }
37
+ /** Read the `3dmodel.model` XML out of a 3MF archive. */
38
+ export declare function parseThreeMfModel(xml: string): ThreeMfResult;
39
+ /**
40
+ * Read a `.3mf` archive.
41
+ *
42
+ * The model part is named by the OPC relationships, and is `3D/3dmodel.model` in practice.
43
+ * Both are tried, the relationship first, because "in practice" is how a reader acquires a
44
+ * file it cannot open two years later.
45
+ */
46
+ export declare function threeMfToMeshes(buffer: ArrayBuffer, inflate: Inflate): ThreeMfResult;
@@ -0,0 +1,254 @@
1
+ /**
2
+ * 3MF, the 3D Manufacturing Format: a zip of XML from the 3MF Consortium.
3
+ *
4
+ * Tier 1. The specification is published and the core is small — a mesh is a vertex list and
5
+ * a triangle list, and that has not changed since the format was introduced.
6
+ *
7
+ * **The same zip reader as `.usdz`, and the reason the two were built together.** The
8
+ * difference is one parameter: USD requires its entries *stored* so a runtime can memory
9
+ * map them, and 3MF uses ordinary deflate, so this one needs the injected inflate and that
10
+ * one does not. Injected rather than imported for the same reason as `fbx.ts`: this module
11
+ * is part of an engine that runs in a browser and must not reach for Node's `zlib`.
12
+ *
13
+ * **A printing format, so what it does not carry matters as much as what it does.** There
14
+ * are no normals: a 3MF mesh is a closed manifold and a slicer derives what it needs, so
15
+ * they are computed here from the triangles. There are no texture coordinates in the core
16
+ * specification either — the materials extension adds them, and until an asset turns up
17
+ * that uses it, claiming support would be claiming something untested.
18
+ */
19
+ import { DrftError } from '@driftengine/drft';
20
+ import { readZip } from './zip.js';
21
+ /** `unit` values the specification defines, in metres. */
22
+ const UNITS = {
23
+ micron: 1e-6,
24
+ millimeter: 1e-3,
25
+ centimeter: 1e-2,
26
+ inch: 0.0254,
27
+ foot: 0.3048,
28
+ meter: 1,
29
+ };
30
+ const DEFAULT_MATERIAL = {
31
+ name: '',
32
+ color: [0.8, 0.8, 0.8],
33
+ specular: 0,
34
+ roughness: 0.6,
35
+ emissive: 0,
36
+ emissiveColor: [-1, -1, -1],
37
+ opacity: 1,
38
+ albedo: -1,
39
+ reflectivity: 0,
40
+ normalMap: -1,
41
+ ormMap: -1,
42
+ emissiveMap: -1,
43
+ roughnessScale: 1,
44
+ metallicScale: 1,
45
+ occlusionStrength: 0,
46
+ cutout: 0,
47
+ };
48
+ /** Every attribute of one XML tag, as a map. Values are always quoted in 3MF. */
49
+ function attributes(tag) {
50
+ const out = new Map();
51
+ const pattern = /([\w:]+)\s*=\s*"([^"]*)"/g;
52
+ let match = pattern.exec(tag);
53
+ while (match !== null) {
54
+ out.set(match[1], match[2]);
55
+ match = pattern.exec(tag);
56
+ }
57
+ return out;
58
+ }
59
+ /**
60
+ * `#RRGGBB` or `#RRGGBBAA` to linear colour and opacity.
61
+ *
62
+ * **sRGB to linear, rather than the byte value straight through.** 3MF states its colours
63
+ * as sRGB, which is what a colour picker shows and not what a renderer multiplies: taking
64
+ * the bytes as-is makes every surface too bright, most visibly in the midtones, and it is
65
+ * the kind of wrong that looks like a lighting problem.
66
+ */
67
+ function parseColor(value) {
68
+ const hex = /^#([0-9a-fA-F]{6})([0-9a-fA-F]{2})?$/.exec(value.trim());
69
+ if (hex === null)
70
+ return null;
71
+ const rgb = hex[1];
72
+ const toLinear = (byte) => {
73
+ const s = byte / 255;
74
+ return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
75
+ };
76
+ return {
77
+ color: [
78
+ toLinear(parseInt(rgb.slice(0, 2), 16)),
79
+ toLinear(parseInt(rgb.slice(2, 4), 16)),
80
+ toLinear(parseInt(rgb.slice(4, 6), 16)),
81
+ ],
82
+ opacity: hex[2] === undefined ? 1 : parseInt(hex[2], 16) / 255,
83
+ };
84
+ }
85
+ /**
86
+ * Flat normals, derived per triangle and accumulated per vertex.
87
+ *
88
+ * 3MF carries none, so they are computed rather than invented: a printing format describes a
89
+ * solid and leaves shading to whoever displays it. Accumulating and normalising gives smooth
90
+ * shading across a curved surface, and `weld.ts` keeps a hard edge hard afterwards because
91
+ * two corners with different normals do not merge.
92
+ */
93
+ function deriveNormals(positions, indices) {
94
+ const normals = new Float32Array(positions.length);
95
+ for (let at = 0; at + 2 < indices.length; at += 3) {
96
+ const a = indices[at] * 3;
97
+ const b = indices[at + 1] * 3;
98
+ const c = indices[at + 2] * 3;
99
+ const ux = positions[b] - positions[a];
100
+ const uy = positions[b + 1] - positions[a + 1];
101
+ const uz = positions[b + 2] - positions[a + 2];
102
+ const vx = positions[c] - positions[a];
103
+ const vy = positions[c + 1] - positions[a + 1];
104
+ const vz = positions[c + 2] - positions[a + 2];
105
+ const nx = uy * vz - uz * vy;
106
+ const ny = uz * vx - ux * vz;
107
+ const nz = ux * vy - uy * vx;
108
+ for (const corner of [a, b, c]) {
109
+ normals[corner] = normals[corner] + nx;
110
+ normals[corner + 1] = normals[corner + 1] + ny;
111
+ normals[corner + 2] = normals[corner + 2] + nz;
112
+ }
113
+ }
114
+ for (let at = 0; at + 2 < normals.length; at += 3) {
115
+ const length = Math.hypot(normals[at], normals[at + 1], normals[at + 2]);
116
+ if (length === 0) {
117
+ normals[at + 1] = 1;
118
+ continue;
119
+ }
120
+ normals[at] = normals[at] / length;
121
+ normals[at + 1] = normals[at + 1] / length;
122
+ normals[at + 2] = normals[at + 2] / length;
123
+ }
124
+ return normals;
125
+ }
126
+ /** Read the `3dmodel.model` XML out of a 3MF archive. */
127
+ export function parseThreeMfModel(xml) {
128
+ const warnings = [];
129
+ const modelTag = /<model\b[^>]*>/.exec(xml)?.[0] ?? '';
130
+ const unit = attributes(modelTag).get('unit') ?? 'millimeter';
131
+ const unitScale = UNITS[unit];
132
+ if (unitScale === undefined) {
133
+ warnings.push(`unknown unit "${unit}"; treating the model as millimetres`);
134
+ }
135
+ /*
136
+ * The colour groups a triangle may point at. `basematerials` is the core specification's
137
+ * own; `colorgroup` comes from the materials extension and is what most exporters write.
138
+ * Both are flat lists indexed by the `pid`/`pindex` pair on an object or a triangle.
139
+ */
140
+ const palettes = new Map();
141
+ const groupPattern = /<(basematerials|m:colorgroup|colorgroup)\b([^>]*)>([\s\S]*?)<\/\1>/g;
142
+ let group = groupPattern.exec(xml);
143
+ while (group !== null) {
144
+ const id = attributes(group[2]).get('id');
145
+ if (id !== undefined) {
146
+ const entries = [];
147
+ const itemPattern = /<(?:base|m:color|color)\b([^>]*)\/?>/g;
148
+ let item = itemPattern.exec(group[3]);
149
+ while (item !== null) {
150
+ const fields = attributes(item[1]);
151
+ const parsed = parseColor(fields.get('displaycolor') ?? fields.get('color') ?? '');
152
+ entries.push({
153
+ ...DEFAULT_MATERIAL,
154
+ name: fields.get('name') ?? `material ${entries.length}`,
155
+ ...(parsed === null ? {} : { color: parsed.color, opacity: parsed.opacity }),
156
+ });
157
+ item = itemPattern.exec(group[3]);
158
+ }
159
+ palettes.set(id, entries);
160
+ }
161
+ group = groupPattern.exec(xml);
162
+ }
163
+ const meshes = [];
164
+ const materials = [];
165
+ const objectPattern = /<object\b([^>]*)>([\s\S]*?)<\/object>/g;
166
+ let object = objectPattern.exec(xml);
167
+ while (object !== null) {
168
+ const objectFields = attributes(object[1]);
169
+ const body = object[2];
170
+ const name = objectFields.get('name') ?? `object ${objectFields.get('id') ?? meshes.length}`;
171
+ const vertices = [];
172
+ const vertexPattern = /<vertex\b([^>]*)\/?>/g;
173
+ let vertex = vertexPattern.exec(body);
174
+ while (vertex !== null) {
175
+ const fields = attributes(vertex[1]);
176
+ vertices.push(Number(fields.get('x') ?? 0), Number(fields.get('y') ?? 0), Number(fields.get('z') ?? 0));
177
+ vertex = vertexPattern.exec(body);
178
+ }
179
+ const indices = [];
180
+ const trianglePattern = /<triangle\b([^>]*)\/?>/g;
181
+ let triangle = trianglePattern.exec(body);
182
+ while (triangle !== null) {
183
+ const fields = attributes(triangle[1]);
184
+ const v1 = Number(fields.get('v1'));
185
+ const v2 = Number(fields.get('v2'));
186
+ const v3 = Number(fields.get('v3'));
187
+ const count = vertices.length / 3;
188
+ if (![v1, v2, v3].every((index) => Number.isInteger(index) && index >= 0 && index < count)) {
189
+ throw new DrftError(`3mf: object "${name}" has a triangle indexing ${v1}, ${v2}, ${v3} of ${count} vertices`);
190
+ }
191
+ indices.push(v1, v2, v3);
192
+ triangle = trianglePattern.exec(body);
193
+ }
194
+ if (vertices.length >= 9 && indices.length >= 3) {
195
+ const positions = Float32Array.from(vertices);
196
+ const indexArray = Uint32Array.from(indices);
197
+ const count = positions.length / 3;
198
+ meshes.push({
199
+ positions,
200
+ normals: deriveNormals(positions, indexArray),
201
+ colors: new Float32Array(count * 3).fill(1),
202
+ emissive: new Float32Array(count),
203
+ indices: indexArray,
204
+ });
205
+ const pid = objectFields.get('pid');
206
+ const pindex = Number(objectFields.get('pindex') ?? 0);
207
+ const palette = pid === undefined ? undefined : palettes.get(pid);
208
+ const chosen = palette?.[Number.isFinite(pindex) ? pindex : 0];
209
+ materials.push(chosen === undefined ? { ...DEFAULT_MATERIAL, name } : { ...chosen, name });
210
+ }
211
+ else if (vertices.length > 0 || indices.length > 0) {
212
+ warnings.push(`object "${name}" carries no usable geometry and was skipped`);
213
+ }
214
+ object = objectPattern.exec(xml);
215
+ }
216
+ if (meshes.length === 0)
217
+ throw new DrftError('3mf: the model carries no readable object');
218
+ return {
219
+ meshes,
220
+ materials,
221
+ warnings,
222
+ /*
223
+ * Stated, never applied. 3MF's specification fixes Z as up, which in a Y-up engine means
224
+ * every print lands on its side — so it is reported here and turned once, format
225
+ * agnostically, in `orient.ts`, exactly as the STL note and the FBX fix established.
226
+ */
227
+ declaredUp: '+z',
228
+ unitScale: unitScale ?? 1e-3,
229
+ };
230
+ }
231
+ /**
232
+ * Read a `.3mf` archive.
233
+ *
234
+ * The model part is named by the OPC relationships, and is `3D/3dmodel.model` in practice.
235
+ * Both are tried, the relationship first, because "in practice" is how a reader acquires a
236
+ * file it cannot open two years later.
237
+ */
238
+ export function threeMfToMeshes(buffer, inflate) {
239
+ const entries = readZip(buffer, inflate);
240
+ const decoder = new TextDecoder();
241
+ const relationships = entries.find((entry) => entry.name.toLowerCase() === '_rels/.rels');
242
+ let target = null;
243
+ if (relationships !== undefined) {
244
+ const declared = /Target\s*=\s*"([^"]+)"/i.exec(decoder.decode(relationships.bytes))?.[1];
245
+ if (declared !== undefined)
246
+ target = declared.replace(/^\//, '').toLowerCase();
247
+ }
248
+ const model = (target === null ? undefined : entries.find((entry) => entry.name.toLowerCase() === target)) ??
249
+ entries.find((entry) => entry.name.toLowerCase().endsWith('.model'));
250
+ if (model === undefined) {
251
+ throw new DrftError(`3mf: the archive holds no .model part (it holds ${entries.map((entry) => entry.name).join(', ')})`);
252
+ }
253
+ return parseThreeMfModel(decoder.decode(model.bytes));
254
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * How much uploading fits in one frame, and how to tell bytes from an error page.
3
+ *
4
+ * Pure decisions with no loader state behind them: whether another upload fits the frame's
5
+ * budget, and whether a server answered a request for a model with HTML.
6
+ */
7
+ export declare const DEFAULT_UPLOADS_PER_FRAME = 3;
8
+ export declare const DEFAULT_REVEAL_SEC = 0.35;
9
+ /**
10
+ * How long one `update` may spend *starting* new stream work.
11
+ *
12
+ * A minority of a 60 Hz frame, and under the whole of a 120 Hz one, because the scene has to
13
+ * draw as well as load. It is a budget for beginning work rather than for finishing it: a piece
14
+ * already started is always finished, so a frame overruns by whatever the last piece cost — see
15
+ * `mayBeginMore` for why that residue is a real limit and not a rounding error.
16
+ */
17
+ export declare const DEFAULT_UPLOAD_MS_PER_FRAME = 6;
18
+ /**
19
+ * Whether another piece of stream work may begin in this frame.
20
+ *
21
+ * **The clock decides, because a count cannot.** `uploadsPerFrame` bounded the number of parts a
22
+ * frame took, on the reasoning in its own docstring: that a part is a small buffer upload and
23
+ * that the allocations are the cost. Measured through the demo page on the shipped Audi, that is
24
+ * backwards. `createMesh` costs 0.6 to 3.1 ms; the per-part CPU work around it costs 6 to 254 ms,
25
+ * most of it the consumer's `transform` plus the two further walks over the same vertices this
26
+ * class does itself. A count bounded the cheap term and left the expensive one free, so three
27
+ * large parts made one 318 ms frame.
28
+ *
29
+ * The loader cannot know what a part costs before it pays for it, and it does not have to: a
30
+ * clock read after each one is exact, needs no per-asset tuning, and is as right on a phone as on
31
+ * a workstation.
32
+ *
33
+ * **The first piece always begins.** One part of a heavy model can cost more than the entire
34
+ * budget, and a rule that asked only "is there room left" would then start nothing on any frame
35
+ * and the load would never finish. A hitch is worse than smooth; a load stuck at 40% is worse
36
+ * than a hitch.
37
+ *
38
+ * **`StepBudget` in `@driftengine/core` is the general form of this and is the one to reach for
39
+ * in new code.** It adds what this cannot do: an estimate of the worst stop, so it refuses to
40
+ * *begin* a unit there is no room to finish rather than discovering that afterwards, and a name
41
+ * for the unit that blew the budget. This one stays because the loader's update is a priority
42
+ * ladder rather than a queue of units — an outline, then parts, then one image, then a merge,
43
+ * each with its own reason to go before the next — and the numbers below were measured against
44
+ * that order.
45
+ *
46
+ * **What this does not fix, stated because measuring it is what found it.** A single part is
47
+ * indivisible here, and the worst one in that car costs 254 ms on an RX 9070 XT. This stops the
48
+ * others joining it in the same frame — the measured worst frame falls from 318 ms to 254 — and
49
+ * that is the whole of the improvement. Making that one part fit a frame is a different change
50
+ * to a different place: either its per-part work moves off the main thread, or a part large
51
+ * enough is split across frames. Neither is a budget, which is why neither is here.
52
+ *
53
+ * **The second of those now exists** as `createMeshIncremental`, and it is deliberately not
54
+ * wired up here: measured on that same car, `createMesh` is 0.6 to 3.1 ms of a part and the 254
55
+ * belongs to the per-part CPU work around it. Splitting the upload would divide the small half.
56
+ *
57
+ * @param begunThisFrame How many pieces this frame has already started.
58
+ * @param elapsedMs How long this frame has been spending on the stream.
59
+ * @param budgetMs What it is allowed.
60
+ */
61
+ export declare function mayBeginMore(begunThisFrame: number, elapsedMs: number, budgetMs: number): boolean;
62
+ /**
63
+ * Whether a response carrying 200 is actually a *document* rather than a container.
64
+ *
65
+ * `response.ok` is not enough to tell a missing model from a broken one, and the gap is not
66
+ * exotic: a dev server and most static hosts answer an unknown path with the site's own page
67
+ * under a 200 rather than with a 404. The bytes then reach the reader, the magic does not
68
+ * match, and a model that was simply never deployed is reported as a corrupt file.
69
+ *
70
+ * That is what this exists to prevent, and it was found on a published demo page: the site
71
+ * serves no model, its dev server answered `/car.drft` with 2,947 bytes of HTML under a 200,
72
+ * and the scene read **"the model failed: not a drft file"** at a reader. The deployed site
73
+ * was correct at the same moment, because it answers a real 404 — so the two disagreed, and
74
+ * the honest state was the one nobody could see locally.
75
+ *
76
+ * A declared content type is the right signal rather than sniffing the first bytes: a host
77
+ * answering with a page says so in the header, and it says so *before* a byte of body is
78
+ * read, which is what lets the caller skip downloading a page it has no use for. A genuinely
79
+ * corrupt container still reaches the reader and still fails loudly, which is the distinction
80
+ * worth keeping.
81
+ */
82
+ export declare function isDocumentResponse(contentType: string | null): boolean;