@driftengine/assets 4.6.1 → 4.7.1

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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/blend.d.ts +25 -0
  3. package/dist/blend.js +43 -0
  4. package/dist/blendAnimation.d.ts +45 -0
  5. package/dist/blendAnimation.js +294 -0
  6. package/dist/blendData.d.ts +114 -0
  7. package/dist/blendData.js +366 -0
  8. package/dist/blendFile.d.ts +69 -0
  9. package/dist/blendFile.js +182 -0
  10. package/dist/blendGeometry.d.ts +45 -0
  11. package/dist/blendGeometry.js +210 -0
  12. package/dist/blendGltf.d.ts +65 -0
  13. package/dist/blendGltf.js +545 -0
  14. package/dist/blendMaterial.d.ts +68 -0
  15. package/dist/blendMaterial.js +545 -0
  16. package/dist/blendMatrix.d.ts +28 -0
  17. package/dist/blendMatrix.js +245 -0
  18. package/dist/blendMesh.d.ts +70 -0
  19. package/dist/blendMesh.js +409 -0
  20. package/dist/blendModifiers.d.ts +25 -0
  21. package/dist/blendModifiers.js +83 -0
  22. package/dist/blendNormals.d.ts +44 -0
  23. package/dist/blendNormals.js +458 -0
  24. package/dist/blendPacked.d.ts +35 -0
  25. package/dist/blendPacked.js +82 -0
  26. package/dist/blendScene.d.ts +50 -0
  27. package/dist/blendScene.js +280 -0
  28. package/dist/gltf.js +8 -2
  29. package/dist/index.d.ts +21 -0
  30. package/dist/index.js +17 -0
  31. package/dist/readModel.d.ts +13 -1
  32. package/dist/readModel.js +43 -13
  33. package/package.json +4 -4
  34. package/src/blend.ts +57 -0
  35. package/src/blendAnimation.ts +328 -0
  36. package/src/blendData.ts +375 -0
  37. package/src/blendFile.ts +233 -0
  38. package/src/blendGeometry.ts +263 -0
  39. package/src/blendGltf.ts +621 -0
  40. package/src/blendMaterial.ts +676 -0
  41. package/src/blendMatrix.ts +257 -0
  42. package/src/blendMesh.ts +496 -0
  43. package/src/blendModifiers.ts +96 -0
  44. package/src/blendNormals.ts +481 -0
  45. package/src/blendPacked.ts +93 -0
  46. package/src/blendScene.ts +307 -0
  47. package/src/gltf.ts +14 -2
  48. package/src/index.ts +41 -0
  49. package/src/readModel.ts +58 -13
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @driftengine/assets
2
2
 
3
- Model readers for seven formats, and the streaming loader that uploads them.
3
+ Model readers for nine formats — `.blend` among them, read from the file's own DNA — and the streaming loader that uploads them.
4
4
 
5
5
  **Cost: 22.5 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
6
6
  3% — the number is a fact about the build rather than a claim in a document.
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Reading a `.blend` directly: Blender's own file, with no Blender installed.
3
+ *
4
+ * The route is container → catalogue → scene → glTF document → the glTF reader, each step in its
5
+ * own module, and this one only chains them and decides what a refusal looks like. See
6
+ * `blendGltf.ts` for why the document in the middle is glTF.
7
+ *
8
+ * **A refusal is an error with its reasons attached**, `BlendNeedsBlender`, because the caller
9
+ * that can act on it — the baker, with Blender on the machine — needs the list rather than a
10
+ * sentence to parse: it hands the file to Blender and says why. A browser has no Blender, and shows
11
+ * the message.
12
+ */
13
+ import { DrftError } from '@driftengine/drft';
14
+ import { BlendData } from './blendData.ts';
15
+ import type { BlendGltf } from './blendGltf.ts';
16
+ import type { BlendDecompress } from './blendPacked.ts';
17
+ /** Thrown when a `.blend` holds something only Blender can evaluate, with each thing named. */
18
+ export declare class BlendNeedsBlender extends DrftError {
19
+ readonly reasons: readonly string[];
20
+ constructor(reasons: readonly string[]);
21
+ }
22
+ /** Open a `.blend`'s bytes, decompressing them first where the file was saved compressed. */
23
+ export declare function openBlend(bytes: Uint8Array, decompress?: BlendDecompress): Promise<BlendData>;
24
+ /** A `.blend` as a glTF document, refusing it whole when any part of it needs evaluating. */
25
+ export declare function readBlend(bytes: Uint8Array, decompress?: BlendDecompress): Promise<BlendGltf>;
package/dist/blend.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Reading a `.blend` directly: Blender's own file, with no Blender installed.
3
+ *
4
+ * The route is container → catalogue → scene → glTF document → the glTF reader, each step in its
5
+ * own module, and this one only chains them and decides what a refusal looks like. See
6
+ * `blendGltf.ts` for why the document in the middle is glTF.
7
+ *
8
+ * **A refusal is an error with its reasons attached**, `BlendNeedsBlender`, because the caller
9
+ * that can act on it — the baker, with Blender on the machine — needs the list rather than a
10
+ * sentence to parse: it hands the file to Blender and says why. A browser has no Blender, and shows
11
+ * the message.
12
+ */
13
+ import { DrftError } from '@driftengine/drft';
14
+ import { BlendData } from './blendData.js';
15
+ import { blendToGltf } from './blendGltf.js';
16
+ import { unpackBlend } from './blendPacked.js';
17
+ /** Thrown when a `.blend` holds something only Blender can evaluate, with each thing named. */
18
+ export class BlendNeedsBlender extends DrftError {
19
+ reasons;
20
+ constructor(reasons) {
21
+ const shown = reasons
22
+ .slice(0, 6)
23
+ .map((reason) => ` ${reason}`)
24
+ .join('\n');
25
+ const more = reasons.length > 6 ? `\n …and ${reasons.length - 6} more` : '';
26
+ super(`blend: this file needs Blender to evaluate it, and the direct reader draws nothing rather than ` +
27
+ `the wrong shape:\n${shown}${more}\nBake it with \`npm run bake\` on a machine with Blender ` +
28
+ 'installed, which hands it to Blender, or apply these in Blender and save again.');
29
+ this.name = 'BlendNeedsBlender';
30
+ this.reasons = reasons;
31
+ }
32
+ }
33
+ /** Open a `.blend`'s bytes, decompressing them first where the file was saved compressed. */
34
+ export async function openBlend(bytes, decompress) {
35
+ return new BlendData(await unpackBlend(bytes, decompress));
36
+ }
37
+ /** A `.blend` as a glTF document, refusing it whole when any part of it needs evaluating. */
38
+ export async function readBlend(bytes, decompress) {
39
+ const result = blendToGltf(await openBlend(bytes, decompress));
40
+ if (result.refusals.length > 0)
41
+ throw new BlendNeedsBlender(result.refusals);
42
+ return result;
43
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * What an object's animation says at a frame: its F-curves read from its action or its NLA strips,
3
+ * and evaluated as Blender evaluates them.
4
+ *
5
+ * **Two ways an action stores its curves.** Before 4.4 an action was a list of F-curves; from 4.4 it
6
+ * is layered — layers of strips, each strip's keyframes in channel bags, one bag a slot — so one
7
+ * action can animate several objects, each reading the bag its slot names. Both are read here into
8
+ * the same `Curve`, and nothing above this module knows which a file used.
9
+ *
10
+ * **What is evaluated, Blender's way**: constant, linear and Bézier keys, the Bézier handles
11
+ * corrected so a segment cannot loop back in time (as Blender corrects them before it draws one),
12
+ * constant or linear extrapolation past the ends, and the Cycles modifier, which is how a looping
13
+ * action is made. **NLA strips** map scene time into each strip's action time by its start, scale
14
+ * and repeats, and hold their ends as their extend mode says.
15
+ *
16
+ * What it gives up, and says through `unread`: the easing modes past Bézier (back, bounce, elastic
17
+ * and the rest, drawn as Bézier), every F-curve modifier but Cycles, drivers, blending between NLA
18
+ * strips other than replacing, and the influence of a strip below one. A file using them animates
19
+ * here as the nearest of what is read, which a bake should check against Blender's own export.
20
+ */
21
+ import type { BlendStruct } from './blendData.ts';
22
+ /** One keyframed property of one animated datablock. */
23
+ export interface Curve {
24
+ /** Blender's data path: `location`, `rotation_euler`, `pose.bones["Spine"].rotation_quaternion`. */
25
+ readonly path: string;
26
+ readonly index: number;
27
+ evaluate(frame: number): number;
28
+ /** The first and last keyed frames. */
29
+ readonly range: readonly [number, number];
30
+ }
31
+ /** One F-curve, ready to evaluate. */
32
+ export declare function readCurve(curve: BlendStruct, unread: Set<string>): Curve | null;
33
+ /** The F-curves an action holds for `slot`: its channel bag in a layered action, every curve otherwise. */
34
+ export declare function actionCurves(action: BlendStruct, slot: number, unread: Set<string>): Curve[];
35
+ /** Everything that animates a datablock: its active action, then its NLA tracks bottom to top. */
36
+ export declare class Animation {
37
+ private readonly sources;
38
+ readonly unread: Set<string>;
39
+ constructor(id: BlendStruct);
40
+ get animated(): boolean;
41
+ /** The scene frames anything here animates over. */
42
+ get range(): [number, number];
43
+ /** Every animated value at `frame`, by `path[index]`; the last source to speak wins. */
44
+ values(frame: number, out: Map<string, number>): Map<string, number>;
45
+ }
@@ -0,0 +1,294 @@
1
+ /**
2
+ * What an object's animation says at a frame: its F-curves read from its action or its NLA strips,
3
+ * and evaluated as Blender evaluates them.
4
+ *
5
+ * **Two ways an action stores its curves.** Before 4.4 an action was a list of F-curves; from 4.4 it
6
+ * is layered — layers of strips, each strip's keyframes in channel bags, one bag a slot — so one
7
+ * action can animate several objects, each reading the bag its slot names. Both are read here into
8
+ * the same `Curve`, and nothing above this module knows which a file used.
9
+ *
10
+ * **What is evaluated, Blender's way**: constant, linear and Bézier keys, the Bézier handles
11
+ * corrected so a segment cannot loop back in time (as Blender corrects them before it draws one),
12
+ * constant or linear extrapolation past the ends, and the Cycles modifier, which is how a looping
13
+ * action is made. **NLA strips** map scene time into each strip's action time by its start, scale
14
+ * and repeats, and hold their ends as their extend mode says.
15
+ *
16
+ * What it gives up, and says through `unread`: the easing modes past Bézier (back, bounce, elastic
17
+ * and the rest, drawn as Bézier), every F-curve modifier but Cycles, drivers, blending between NLA
18
+ * strips other than replacing, and the influence of a strip below one. A file using them animates
19
+ * here as the nearest of what is read, which a bake should check against Blender's own export.
20
+ */
21
+ const BEZT_IPO_CONST = 0;
22
+ const BEZT_IPO_LIN = 1;
23
+ const FCURVE_EXTRAPOLATE_LINEAR = 1;
24
+ /* F-curve modifier types: generator 1, function 2, envelope 3, cycles 4, noise 5. */
25
+ const FMODIFIER_TYPE_CYCLES = 4;
26
+ const FCM_EXTRAPOLATE_NONE = 0;
27
+ const FCM_EXTRAPOLATE_CYCLIC_OFFSET = 2;
28
+ const FCM_EXTRAPOLATE_MIRROR = 3;
29
+ const NLASTRIP_EXTEND_HOLD = 0;
30
+ const NLASTRIP_EXTEND_HOLD_FORWARD = 1;
31
+ const NLASTRIP_FLAG_MUTED = 1 << 12;
32
+ const NLATRACK_MUTED = 1 << 4;
33
+ /** The parameter where a cubic's x is `x`, by bisection then Newton: the curve is monotonic. */
34
+ function solveBezier(x0, x1, x2, x3, x) {
35
+ let lo = 0;
36
+ let hi = 1;
37
+ let t = (x - x0) / (x3 - x0 || 1);
38
+ for (let i = 0; i < 24; i++) {
39
+ const u = 1 - t;
40
+ const at = u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x3;
41
+ if (Math.abs(at - x) < 1e-7)
42
+ return t;
43
+ if (at < x)
44
+ lo = t;
45
+ else
46
+ hi = t;
47
+ const slope = 3 * u * u * (x1 - x0) + 6 * u * t * (x2 - x1) + 3 * t * t * (x3 - x2);
48
+ const next = slope !== 0 ? t - (at - x) / slope : (lo + hi) / 2;
49
+ t = next > lo && next < hi ? next : (lo + hi) / 2;
50
+ }
51
+ return t;
52
+ }
53
+ /** Blender's correction of a Bézier segment's handles, so its x never runs backwards. */
54
+ function correct(k0, k1) {
55
+ let h1x = k0.rx - k0.x;
56
+ let h1y = k0.ry - k0.y;
57
+ let h2x = k1.x - k1.lx;
58
+ let h2y = k1.y - k1.ly;
59
+ const span = k1.x - k0.x;
60
+ const total = h1x + h2x;
61
+ if (total > span && total > 0) {
62
+ const f = span / total;
63
+ h1x *= f;
64
+ h1y *= f;
65
+ h2x *= f;
66
+ h2y *= f;
67
+ }
68
+ return [k0.x + h1x, k0.y + h1y, k1.x - h2x, k1.y - h2y];
69
+ }
70
+ function readKeys(curve) {
71
+ const count = curve.int('totvert');
72
+ if (count === 0 || curve.ptr('bezt') === 0n)
73
+ return [];
74
+ return curve.array('bezt', 'BezTriple', count).map((b) => {
75
+ const v = b.floats('vec');
76
+ return {
77
+ lx: v[0],
78
+ ly: v[1],
79
+ x: v[3],
80
+ y: v[4],
81
+ rx: v[6],
82
+ ry: v[7],
83
+ ipo: b.int('ipo'),
84
+ };
85
+ });
86
+ }
87
+ /** One F-curve, ready to evaluate. */
88
+ export function readCurve(curve, unread) {
89
+ const keys = readKeys(curve);
90
+ if (keys.length === 0)
91
+ return null;
92
+ const extend = curve.int('extend');
93
+ let cycles = null;
94
+ for (const modifier of curve.list('modifiers')) {
95
+ if (modifier.int('type') === FMODIFIER_TYPE_CYCLES) {
96
+ const data = modifier.deref('data', 'FMod_Cycles');
97
+ cycles = { before: data?.int('before_mode') ?? 1, after: data?.int('after_mode') ?? 1 };
98
+ }
99
+ else
100
+ unread.add(`F-curve modifier ${modifier.int('type')}`);
101
+ }
102
+ for (const key of keys)
103
+ if (key.ipo > 2)
104
+ unread.add('easing past Bézier');
105
+ const first = keys[0];
106
+ const last = keys[keys.length - 1];
107
+ const span = last.x - first.x;
108
+ const plain = (frame) => {
109
+ if (frame <= first.x || keys.length === 1) {
110
+ if (extend !== FCURVE_EXTRAPOLATE_LINEAR || keys.length === 1)
111
+ return first.y;
112
+ const next = keys[1];
113
+ const slope = first.ipo === BEZT_IPO_BEZ_SLOPE
114
+ ? (first.y - first.ly) / (first.x - first.lx || 1)
115
+ : (next.y - first.y) / (next.x - first.x || 1);
116
+ return first.y + (frame - first.x) * slope;
117
+ }
118
+ if (frame >= last.x) {
119
+ if (extend !== FCURVE_EXTRAPOLATE_LINEAR)
120
+ return last.y;
121
+ const prev = keys[keys.length - 2];
122
+ const slope = last.ipo === BEZT_IPO_BEZ_SLOPE
123
+ ? (last.ry - last.y) / (last.rx - last.x || 1)
124
+ : (last.y - prev.y) / (last.x - prev.x || 1);
125
+ return last.y + (frame - last.x) * slope;
126
+ }
127
+ let lo = 0;
128
+ let hi = keys.length - 1;
129
+ while (hi - lo > 1) {
130
+ const mid = (lo + hi) >> 1;
131
+ if (keys[mid].x <= frame)
132
+ lo = mid;
133
+ else
134
+ hi = mid;
135
+ }
136
+ const a = keys[lo];
137
+ const b = keys[hi];
138
+ if (a.ipo === BEZT_IPO_CONST)
139
+ return a.y;
140
+ if (a.ipo === BEZT_IPO_LIN)
141
+ return a.y + ((frame - a.x) / (b.x - a.x || 1)) * (b.y - a.y);
142
+ const [x1, y1, x2, y2] = correct(a, b);
143
+ const t = solveBezier(a.x, x1, x2, b.x, frame);
144
+ const u = 1 - t;
145
+ return u * u * u * a.y + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * b.y;
146
+ };
147
+ const evaluate = cycles === null || span <= 0
148
+ ? plain
149
+ : (frame) => {
150
+ const before = frame < first.x;
151
+ const mode = before ? cycles.before : cycles.after;
152
+ if ((frame >= first.x && frame <= last.x) || mode === FCM_EXTRAPOLATE_NONE)
153
+ return plain(frame);
154
+ const offset = frame - first.x;
155
+ const cycle = Math.floor(offset / span);
156
+ let local = offset - cycle * span;
157
+ if (mode === FCM_EXTRAPOLATE_MIRROR && cycle % 2 !== 0)
158
+ local = span - local;
159
+ const value = plain(first.x + local);
160
+ return mode === FCM_EXTRAPOLATE_CYCLIC_OFFSET
161
+ ? value + cycle * (last.y - first.y)
162
+ : value;
163
+ };
164
+ return {
165
+ path: curve.text('rna_path'),
166
+ index: curve.int('array_index'),
167
+ evaluate,
168
+ range: [first.x, last.x],
169
+ };
170
+ }
171
+ /** Bézier keys whose handles give the slope at an end. */
172
+ const BEZT_IPO_BEZ_SLOPE = 2;
173
+ /** The F-curves an action holds for `slot`: its channel bag in a layered action, every curve otherwise. */
174
+ export function actionCurves(action, slot, unread) {
175
+ const out = [];
176
+ const legacy = action.has('curves') ? action.list('curves') : [];
177
+ for (const curve of legacy) {
178
+ const read = readCurve(curve, unread);
179
+ if (read !== null)
180
+ out.push(read);
181
+ }
182
+ if (!action.has('strip_keyframe_data_array') || action.int('strip_keyframe_data_array_num') === 0)
183
+ return out;
184
+ const scope = action.scope;
185
+ const file = action.file;
186
+ const data = action.pointers('strip_keyframe_data_array', action.int('strip_keyframe_data_array_num'));
187
+ for (const address of data) {
188
+ const keyframes = file.at(address, scope, 'ActionStripKeyframeData');
189
+ if (keyframes === null)
190
+ continue;
191
+ for (const bagAddress of keyframes.pointers('channelbag_array', keyframes.int('channelbag_array_num'))) {
192
+ const bag = file.at(bagAddress, scope, 'ActionChannelBag');
193
+ if (bag === null || bag.int('slot_handle') !== slot)
194
+ continue;
195
+ for (const curveAddress of bag.pointers('fcurve_array', bag.int('fcurve_array_num'))) {
196
+ const curve = file.at(curveAddress, scope, 'FCurve');
197
+ const read = curve === null ? null : readCurve(curve, unread);
198
+ if (read !== null)
199
+ out.push(read);
200
+ }
201
+ }
202
+ }
203
+ return out;
204
+ }
205
+ function stripSource(strip, unread) {
206
+ const action = strip.deref('act');
207
+ if (action === null)
208
+ return null;
209
+ const curves = actionCurves(action, strip.has('action_slot_handle') ? strip.int('action_slot_handle') : 0, unread);
210
+ if (curves.length === 0)
211
+ return null;
212
+ const start = strip.float('start');
213
+ const end = strip.float('end');
214
+ const actStart = strip.float('actstart');
215
+ const actEnd = strip.float('actend');
216
+ const scale = Math.abs(strip.float('scale')) || 1;
217
+ const repeat = strip.float('repeat') || 1;
218
+ const length = actEnd - actStart || 1;
219
+ const extend = strip.int('extendmode');
220
+ const time = (frame) => {
221
+ if (frame < start)
222
+ return extend === NLASTRIP_EXTEND_HOLD ? actStart : null;
223
+ if (frame > end)
224
+ return extend === NLASTRIP_EXTEND_HOLD || extend === NLASTRIP_EXTEND_HOLD_FORWARD
225
+ ? actStart + (repeat % 1 === 0 ? length : ((end - start) / scale) % length)
226
+ : null;
227
+ /* At the very end of a whole number of repeats, the end of the action rather than its start. */
228
+ if (frame === end && repeat === Math.floor(repeat))
229
+ return actStart + length;
230
+ return (actStart +
231
+ ((((frame - start) % (length * scale)) + length * scale) % (length * scale)) / scale);
232
+ };
233
+ return { curves, time, range: [start, end] };
234
+ }
235
+ /** Everything that animates a datablock: its active action, then its NLA tracks bottom to top. */
236
+ export class Animation {
237
+ sources = [];
238
+ unread = new Set();
239
+ constructor(id) {
240
+ const adt = id.has('adt') ? id.deref('adt') : null;
241
+ if (adt === null)
242
+ return;
243
+ for (const track of adt.list('nla_tracks')) {
244
+ if ((track.int('flag') & NLATRACK_MUTED) !== 0)
245
+ continue;
246
+ for (const strip of track.list('strips')) {
247
+ if ((strip.int('flag') & NLASTRIP_FLAG_MUTED) !== 0)
248
+ continue;
249
+ const source = stripSource(strip, this.unread);
250
+ if (source !== null)
251
+ this.sources.push(source);
252
+ }
253
+ }
254
+ const action = adt.deref('action');
255
+ if (action !== null) {
256
+ const curves = actionCurves(action, adt.has('slot_handle') ? adt.int('slot_handle') : 0, this.unread);
257
+ let lo = Infinity;
258
+ let hi = -Infinity;
259
+ for (const c of curves) {
260
+ lo = Math.min(lo, c.range[0]);
261
+ hi = Math.max(hi, c.range[1]);
262
+ }
263
+ if (curves.length > 0)
264
+ this.sources.push({ curves, time: (frame) => frame, range: [lo, hi] });
265
+ }
266
+ if (adt.list('drivers').length > 0)
267
+ this.unread.add('drivers');
268
+ }
269
+ get animated() {
270
+ return this.sources.length > 0;
271
+ }
272
+ /** The scene frames anything here animates over. */
273
+ get range() {
274
+ let lo = Infinity;
275
+ let hi = -Infinity;
276
+ for (const s of this.sources) {
277
+ lo = Math.min(lo, s.range[0]);
278
+ hi = Math.max(hi, s.range[1]);
279
+ }
280
+ return [lo, hi];
281
+ }
282
+ /** Every animated value at `frame`, by `path[index]`; the last source to speak wins. */
283
+ values(frame, out) {
284
+ out.clear();
285
+ for (const source of this.sources) {
286
+ const t = source.time(frame);
287
+ if (t === null)
288
+ continue;
289
+ for (const curve of source.curves)
290
+ out.set(`${curve.path}[${curve.index}]`, curve.evaluate(t));
291
+ }
292
+ return out;
293
+ }
294
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Reading a `.blend`'s structs by field name, and following its pointers between blocks.
3
+ *
4
+ * `blendFile.ts` says where every block is and what every struct looks like; this is what turns
5
+ * those two tables into `mesh.int('totvert')` and `object.deref('data')`. Every accessor names a
6
+ * field rather than an offset, so a field that moved between Blender releases is found where the
7
+ * file says it is, and a field that does not exist in this file answers `has` false rather than
8
+ * reading whatever sits at an offset some other release used.
9
+ *
10
+ * **A missing field is a question, not an error.** Most of the reading above this module is
11
+ * "the new name if this file has it, the old one otherwise", so `has` is cheap and every typed
12
+ * accessor throws only when asked for a field the struct does not carry — which is a reader bug,
13
+ * and is said with the struct's name and the Blender version beside it.
14
+ */
15
+ import type { BlendBlock, BlendHeader, DnaField, DnaStruct } from './blendFile.ts';
16
+ /** A parsed file: its blocks, its catalogue, and the maps from an address to a block. */
17
+ export declare class BlendData {
18
+ readonly header: BlendHeader;
19
+ readonly bytes: Uint8Array;
20
+ readonly view: DataView;
21
+ readonly blocks: readonly BlendBlock[];
22
+ readonly structs: readonly DnaStruct[];
23
+ private readonly byType;
24
+ /**
25
+ * Which datablock each block's data belongs to: the most recent block that is not `DATA`.
26
+ *
27
+ * **Addresses are only unique inside one datablock, and 5.0 relies on it.** Blender's own reader
28
+ * keeps one map for datablocks and a second, per datablock, for the data blocks that follow it,
29
+ * and 5.0 writes placeholder addresses that repeat between datablocks: measured on a 5.0 file,
30
+ * one address named eight different meshes' attribute arrays. A file-wide map answered every
31
+ * mesh with the last mesh's positions.
32
+ */
33
+ private readonly owner;
34
+ private readonly ids;
35
+ private readonly scoped;
36
+ private readonly sorted;
37
+ /** Before 5.0 every address was a real one, so a data pointer may be resolved file-wide. */
38
+ private readonly fileWide;
39
+ constructor(bytes: Uint8Array);
40
+ /** The catalogue entry for a struct type, or undefined where this file's Blender had none. */
41
+ struct(type: string): DnaStruct | undefined;
42
+ /** The datablock a block's data belongs to, which is what scopes every pointer inside it. */
43
+ scopeOf(block: number): number;
44
+ /**
45
+ * The block an address falls in, and how far into it, as seen from inside datablock `scope`.
46
+ *
47
+ * The datablock's own data first, then the datablocks themselves, which is the order Blender's
48
+ * reader resolves in. Null for an address nothing holds.
49
+ */
50
+ find(address: bigint, scope: number): {
51
+ block: number;
52
+ offset: number;
53
+ } | null;
54
+ /** Every datablock of a two-letter code — `OB`, `ME`, `MA` — as views of their own struct. */
55
+ idsOf(code: string): BlendStruct[];
56
+ /** A view of the struct at an address, typed by the block that holds it or by `type`. */
57
+ at(address: bigint, scope: number, type?: string): BlendStruct | null;
58
+ /**
59
+ * The structs of an array a pointer names, indexed *inside* its block.
60
+ *
61
+ * **Never by adding to the address.** From 5.0 an address is a placeholder rather than a
62
+ * location, so `base + 24` is not the second element and can be exactly the address of some
63
+ * unrelated block.
64
+ */
65
+ array(address: bigint, scope: number, type: string, count?: number): BlendStruct[];
66
+ /** The raw bytes a pointer names, through the end of its block. */
67
+ raw(address: bigint, scope: number): Uint8Array | null;
68
+ /** A zero-terminated string a `char *` names. */
69
+ text(address: bigint, scope: number): string;
70
+ /** The block a pointer names, for its count and its catalogue type. */
71
+ blockOf(address: bigint, scope: number): BlendBlock | null;
72
+ pointer(at: number): bigint;
73
+ }
74
+ /** One struct in the file, read by field name. */
75
+ export declare class BlendStruct {
76
+ readonly file: BlendData;
77
+ readonly struct: DnaStruct;
78
+ readonly offset: number;
79
+ /** The datablock this struct's data belongs to, which every pointer in it is resolved inside. */
80
+ readonly scope: number;
81
+ constructor(file: BlendData, struct: DnaStruct, offset: number,
82
+ /** The datablock this struct's data belongs to, which every pointer in it is resolved inside. */
83
+ scope: number);
84
+ get type(): string;
85
+ has(name: string): boolean;
86
+ field(name: string): DnaField;
87
+ /** An integer field of any width, signed as its type is. */
88
+ int(name: string, index?: number): number;
89
+ float(name: string, index?: number): number;
90
+ /** Every element of a float array field, flattened: `co[3]` is three, `mat[4][4]` sixteen. */
91
+ floats(name: string): number[];
92
+ ints(name: string): number[];
93
+ /** A `char[]` field as text, up to its first zero. */
94
+ string(name: string): string;
95
+ /** An ID's name without its two-letter code: `OBCube` is `Cube`. */
96
+ idName(): string;
97
+ ptr(name: string, index?: number): bigint;
98
+ /** Follow a pointer field, typed by the block it lands in or by `type` where that is known. */
99
+ deref(name: string, type?: string): BlendStruct | null;
100
+ /** An array a pointer field names, `count` long or as long as its block. */
101
+ array(name: string, type: string, count?: number): BlendStruct[];
102
+ /** The bytes a pointer field names, through the end of their block. */
103
+ raw(name: string): Uint8Array | null;
104
+ /** The block a pointer field names. */
105
+ blockOf(name: string): BlendBlock | null;
106
+ /** A `char *` field's text. */
107
+ text(name: string): string;
108
+ /** An embedded struct field, viewed in place. */
109
+ sub(name: string): BlendStruct;
110
+ /** The links of a `ListBase` field, following `next` until it runs out. */
111
+ list(name: string, type?: string): BlendStruct[];
112
+ /** An array of pointers that a pointer-to-pointer field names: `Mesh.mat`, `Object.mat`. */
113
+ pointers(name: string, count: number): bigint[];
114
+ }