@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
@@ -0,0 +1,328 @@
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
+
22
+ import type { BlendStruct } from './blendData.ts';
23
+
24
+ /** One keyframed property of one animated datablock. */
25
+ export interface Curve {
26
+ /** Blender's data path: `location`, `rotation_euler`, `pose.bones["Spine"].rotation_quaternion`. */
27
+ readonly path: string;
28
+ readonly index: number;
29
+ evaluate(frame: number): number;
30
+ /** The first and last keyed frames. */
31
+ readonly range: readonly [number, number];
32
+ }
33
+
34
+ const BEZT_IPO_CONST = 0;
35
+ const BEZT_IPO_LIN = 1;
36
+ const FCURVE_EXTRAPOLATE_LINEAR = 1;
37
+ /* F-curve modifier types: generator 1, function 2, envelope 3, cycles 4, noise 5. */
38
+ const FMODIFIER_TYPE_CYCLES = 4;
39
+ const FCM_EXTRAPOLATE_NONE = 0;
40
+ const FCM_EXTRAPOLATE_CYCLIC_OFFSET = 2;
41
+ const FCM_EXTRAPOLATE_MIRROR = 3;
42
+ const NLASTRIP_EXTEND_HOLD = 0;
43
+ const NLASTRIP_EXTEND_HOLD_FORWARD = 1;
44
+ const NLASTRIP_FLAG_MUTED = 1 << 12;
45
+ const NLATRACK_MUTED = 1 << 4;
46
+
47
+ interface Key {
48
+ x: number;
49
+ y: number;
50
+ /** Left handle, then right. */
51
+ lx: number;
52
+ ly: number;
53
+ rx: number;
54
+ ry: number;
55
+ ipo: number;
56
+ }
57
+
58
+ /** The parameter where a cubic's x is `x`, by bisection then Newton: the curve is monotonic. */
59
+ function solveBezier(x0: number, x1: number, x2: number, x3: number, x: number): number {
60
+ let lo = 0;
61
+ let hi = 1;
62
+ let t = (x - x0) / (x3 - x0 || 1);
63
+ for (let i = 0; i < 24; i++) {
64
+ const u = 1 - t;
65
+ const at = u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x3;
66
+ if (Math.abs(at - x) < 1e-7) return t;
67
+ if (at < x) lo = t;
68
+ else hi = t;
69
+ const slope = 3 * u * u * (x1 - x0) + 6 * u * t * (x2 - x1) + 3 * t * t * (x3 - x2);
70
+ const next = slope !== 0 ? t - (at - x) / slope : (lo + hi) / 2;
71
+ t = next > lo && next < hi ? next : (lo + hi) / 2;
72
+ }
73
+ return t;
74
+ }
75
+
76
+ /** Blender's correction of a Bézier segment's handles, so its x never runs backwards. */
77
+ function correct(k0: Key, k1: Key): [number, number, number, number] {
78
+ let h1x = k0.rx - k0.x;
79
+ let h1y = k0.ry - k0.y;
80
+ let h2x = k1.x - k1.lx;
81
+ let h2y = k1.y - k1.ly;
82
+ const span = k1.x - k0.x;
83
+ const total = h1x + h2x;
84
+ if (total > span && total > 0) {
85
+ const f = span / total;
86
+ h1x *= f;
87
+ h1y *= f;
88
+ h2x *= f;
89
+ h2y *= f;
90
+ }
91
+ return [k0.x + h1x, k0.y + h1y, k1.x - h2x, k1.y - h2y];
92
+ }
93
+
94
+ function readKeys(curve: BlendStruct): Key[] {
95
+ const count = curve.int('totvert');
96
+ if (count === 0 || curve.ptr('bezt') === 0n) return [];
97
+ return curve.array('bezt', 'BezTriple', count).map((b) => {
98
+ const v = b.floats('vec');
99
+ return {
100
+ lx: v[0] as number,
101
+ ly: v[1] as number,
102
+ x: v[3] as number,
103
+ y: v[4] as number,
104
+ rx: v[6] as number,
105
+ ry: v[7] as number,
106
+ ipo: b.int('ipo'),
107
+ };
108
+ });
109
+ }
110
+
111
+ /** One F-curve, ready to evaluate. */
112
+ export function readCurve(curve: BlendStruct, unread: Set<string>): Curve | null {
113
+ const keys = readKeys(curve);
114
+ if (keys.length === 0) return null;
115
+ const extend = curve.int('extend');
116
+ let cycles: { before: number; after: number } | null = null;
117
+ for (const modifier of curve.list('modifiers')) {
118
+ if (modifier.int('type') === FMODIFIER_TYPE_CYCLES) {
119
+ const data = modifier.deref('data', 'FMod_Cycles');
120
+ cycles = { before: data?.int('before_mode') ?? 1, after: data?.int('after_mode') ?? 1 };
121
+ } else unread.add(`F-curve modifier ${modifier.int('type')}`);
122
+ }
123
+ for (const key of keys) if (key.ipo > 2) unread.add('easing past Bézier');
124
+ const first = keys[0] as Key;
125
+ const last = keys[keys.length - 1] as Key;
126
+ const span = last.x - first.x;
127
+
128
+ const plain = (frame: number): number => {
129
+ if (frame <= first.x || keys.length === 1) {
130
+ if (extend !== FCURVE_EXTRAPOLATE_LINEAR || keys.length === 1) return first.y;
131
+ const next = keys[1] as Key;
132
+ const slope =
133
+ first.ipo === BEZT_IPO_BEZ_SLOPE
134
+ ? (first.y - first.ly) / (first.x - first.lx || 1)
135
+ : (next.y - first.y) / (next.x - first.x || 1);
136
+ return first.y + (frame - first.x) * slope;
137
+ }
138
+ if (frame >= last.x) {
139
+ if (extend !== FCURVE_EXTRAPOLATE_LINEAR) return last.y;
140
+ const prev = keys[keys.length - 2] as Key;
141
+ const slope =
142
+ last.ipo === BEZT_IPO_BEZ_SLOPE
143
+ ? (last.ry - last.y) / (last.rx - last.x || 1)
144
+ : (last.y - prev.y) / (last.x - prev.x || 1);
145
+ return last.y + (frame - last.x) * slope;
146
+ }
147
+ let lo = 0;
148
+ let hi = keys.length - 1;
149
+ while (hi - lo > 1) {
150
+ const mid = (lo + hi) >> 1;
151
+ if ((keys[mid] as Key).x <= frame) lo = mid;
152
+ else hi = mid;
153
+ }
154
+ const a = keys[lo] as Key;
155
+ const b = keys[hi] as Key;
156
+ if (a.ipo === BEZT_IPO_CONST) return a.y;
157
+ if (a.ipo === BEZT_IPO_LIN) return a.y + ((frame - a.x) / (b.x - a.x || 1)) * (b.y - a.y);
158
+ const [x1, y1, x2, y2] = correct(a, b);
159
+ const t = solveBezier(a.x, x1, x2, b.x, frame);
160
+ const u = 1 - t;
161
+ return u * u * u * a.y + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * b.y;
162
+ };
163
+
164
+ const evaluate =
165
+ cycles === null || span <= 0
166
+ ? plain
167
+ : (frame: number): number => {
168
+ const before = frame < first.x;
169
+ const mode = before ? cycles.before : cycles.after;
170
+ if ((frame >= first.x && frame <= last.x) || mode === FCM_EXTRAPOLATE_NONE)
171
+ return plain(frame);
172
+ const offset = frame - first.x;
173
+ const cycle = Math.floor(offset / span);
174
+ let local = offset - cycle * span;
175
+ if (mode === FCM_EXTRAPOLATE_MIRROR && cycle % 2 !== 0) local = span - local;
176
+ const value = plain(first.x + local);
177
+ return mode === FCM_EXTRAPOLATE_CYCLIC_OFFSET
178
+ ? value + cycle * (last.y - first.y)
179
+ : value;
180
+ };
181
+ return {
182
+ path: curve.text('rna_path'),
183
+ index: curve.int('array_index'),
184
+ evaluate,
185
+ range: [first.x, last.x],
186
+ };
187
+ }
188
+
189
+ /** Bézier keys whose handles give the slope at an end. */
190
+ const BEZT_IPO_BEZ_SLOPE = 2;
191
+
192
+ /** The F-curves an action holds for `slot`: its channel bag in a layered action, every curve otherwise. */
193
+ export function actionCurves(action: BlendStruct, slot: number, unread: Set<string>): Curve[] {
194
+ const out: Curve[] = [];
195
+ const legacy = action.has('curves') ? action.list('curves') : [];
196
+ for (const curve of legacy) {
197
+ const read = readCurve(curve, unread);
198
+ if (read !== null) out.push(read);
199
+ }
200
+ if (!action.has('strip_keyframe_data_array') || action.int('strip_keyframe_data_array_num') === 0)
201
+ return out;
202
+ const scope = action.scope;
203
+ const file = action.file;
204
+ const data = action.pointers(
205
+ 'strip_keyframe_data_array',
206
+ action.int('strip_keyframe_data_array_num'),
207
+ );
208
+ for (const address of data) {
209
+ const keyframes = file.at(address, scope, 'ActionStripKeyframeData');
210
+ if (keyframes === null) continue;
211
+ for (const bagAddress of keyframes.pointers(
212
+ 'channelbag_array',
213
+ keyframes.int('channelbag_array_num'),
214
+ )) {
215
+ const bag = file.at(bagAddress, scope, 'ActionChannelBag');
216
+ if (bag === null || bag.int('slot_handle') !== slot) continue;
217
+ for (const curveAddress of bag.pointers('fcurve_array', bag.int('fcurve_array_num'))) {
218
+ const curve = file.at(curveAddress, scope, 'FCurve');
219
+ const read = curve === null ? null : readCurve(curve, unread);
220
+ if (read !== null) out.push(read);
221
+ }
222
+ }
223
+ }
224
+ return out;
225
+ }
226
+
227
+ /** An NLA strip's or the active action's curves, and how scene time maps into them. */
228
+ interface Source {
229
+ readonly curves: readonly Curve[];
230
+ /** Action time at a scene frame, or null where the strip says nothing then. */
231
+ readonly time: (frame: number) => number | null;
232
+ readonly range: readonly [number, number];
233
+ }
234
+
235
+ function stripSource(strip: BlendStruct, unread: Set<string>): Source | null {
236
+ const action = strip.deref('act');
237
+ if (action === null) return null;
238
+ const curves = actionCurves(
239
+ action,
240
+ strip.has('action_slot_handle') ? strip.int('action_slot_handle') : 0,
241
+ unread,
242
+ );
243
+ if (curves.length === 0) return null;
244
+ const start = strip.float('start');
245
+ const end = strip.float('end');
246
+ const actStart = strip.float('actstart');
247
+ const actEnd = strip.float('actend');
248
+ const scale = Math.abs(strip.float('scale')) || 1;
249
+ const repeat = strip.float('repeat') || 1;
250
+ const length = actEnd - actStart || 1;
251
+ const extend = strip.int('extendmode');
252
+ const time = (frame: number): number | null => {
253
+ if (frame < start) return extend === NLASTRIP_EXTEND_HOLD ? actStart : null;
254
+ if (frame > end)
255
+ return extend === NLASTRIP_EXTEND_HOLD || extend === NLASTRIP_EXTEND_HOLD_FORWARD
256
+ ? actStart + (repeat % 1 === 0 ? length : ((end - start) / scale) % length)
257
+ : null;
258
+ /* At the very end of a whole number of repeats, the end of the action rather than its start. */
259
+ if (frame === end && repeat === Math.floor(repeat)) return actStart + length;
260
+ return (
261
+ actStart +
262
+ ((((frame - start) % (length * scale)) + length * scale) % (length * scale)) / scale
263
+ );
264
+ };
265
+ return { curves, time, range: [start, end] };
266
+ }
267
+
268
+ /** Everything that animates a datablock: its active action, then its NLA tracks bottom to top. */
269
+ export class Animation {
270
+ private readonly sources: Source[] = [];
271
+ readonly unread = new Set<string>();
272
+
273
+ constructor(id: BlendStruct) {
274
+ const adt = id.has('adt') ? id.deref('adt') : null;
275
+ if (adt === null) return;
276
+ for (const track of adt.list('nla_tracks')) {
277
+ if ((track.int('flag') & NLATRACK_MUTED) !== 0) continue;
278
+ for (const strip of track.list('strips')) {
279
+ if ((strip.int('flag') & NLASTRIP_FLAG_MUTED) !== 0) continue;
280
+ const source = stripSource(strip, this.unread);
281
+ if (source !== null) this.sources.push(source);
282
+ }
283
+ }
284
+ const action = adt.deref('action');
285
+ if (action !== null) {
286
+ const curves = actionCurves(
287
+ action,
288
+ adt.has('slot_handle') ? adt.int('slot_handle') : 0,
289
+ this.unread,
290
+ );
291
+ let lo = Infinity;
292
+ let hi = -Infinity;
293
+ for (const c of curves) {
294
+ lo = Math.min(lo, c.range[0]);
295
+ hi = Math.max(hi, c.range[1]);
296
+ }
297
+ if (curves.length > 0) this.sources.push({ curves, time: (frame) => frame, range: [lo, hi] });
298
+ }
299
+ if (adt.list('drivers').length > 0) this.unread.add('drivers');
300
+ }
301
+
302
+ get animated(): boolean {
303
+ return this.sources.length > 0;
304
+ }
305
+
306
+ /** The scene frames anything here animates over. */
307
+ get range(): [number, number] {
308
+ let lo = Infinity;
309
+ let hi = -Infinity;
310
+ for (const s of this.sources) {
311
+ lo = Math.min(lo, s.range[0]);
312
+ hi = Math.max(hi, s.range[1]);
313
+ }
314
+ return [lo, hi];
315
+ }
316
+
317
+ /** Every animated value at `frame`, by `path[index]`; the last source to speak wins. */
318
+ values(frame: number, out: Map<string, number>): Map<string, number> {
319
+ out.clear();
320
+ for (const source of this.sources) {
321
+ const t = source.time(frame);
322
+ if (t === null) continue;
323
+ for (const curve of source.curves)
324
+ out.set(`${curve.path}[${curve.index}]`, curve.evaluate(t));
325
+ }
326
+ return out;
327
+ }
328
+ }
@@ -0,0 +1,375 @@
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
+
16
+ import { DrftError } from '@driftengine/drft';
17
+ import type { BlendBlock, BlendHeader, DnaField, DnaStruct } from './blendFile.ts';
18
+ import { readBlendBlocks, readBlendHeader, readDna } from './blendFile.ts';
19
+
20
+ /** A parsed file: its blocks, its catalogue, and the maps from an address to a block. */
21
+ export class BlendData {
22
+ readonly header: BlendHeader;
23
+ readonly bytes: Uint8Array;
24
+ readonly view: DataView;
25
+ readonly blocks: readonly BlendBlock[];
26
+ readonly structs: readonly DnaStruct[];
27
+ private readonly byType = new Map<string, DnaStruct>();
28
+ /**
29
+ * Which datablock each block's data belongs to: the most recent block that is not `DATA`.
30
+ *
31
+ * **Addresses are only unique inside one datablock, and 5.0 relies on it.** Blender's own reader
32
+ * keeps one map for datablocks and a second, per datablock, for the data blocks that follow it,
33
+ * and 5.0 writes placeholder addresses that repeat between datablocks: measured on a 5.0 file,
34
+ * one address named eight different meshes' attribute arrays. A file-wide map answered every
35
+ * mesh with the last mesh's positions.
36
+ */
37
+ private readonly owner: Int32Array;
38
+ private readonly ids = new Map<bigint, number>();
39
+ private readonly scoped = new Map<number, Map<bigint, number>>();
40
+ private readonly sorted = new Map<number, number[]>();
41
+ /** Before 5.0 every address was a real one, so a data pointer may be resolved file-wide. */
42
+ private readonly fileWide: Map<bigint, number> | null;
43
+
44
+ constructor(bytes: Uint8Array) {
45
+ this.bytes = bytes;
46
+ this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
47
+ this.header = readBlendHeader(bytes);
48
+ this.blocks = readBlendBlocks(bytes, this.header);
49
+ const dna = this.blocks.find((block) => block.code === 'DNA1');
50
+ if (dna === undefined) throw new DrftError('blend: the file carries no DNA1 catalogue');
51
+ this.structs = readDna(bytes, dna, this.header);
52
+ for (const struct of this.structs) this.byType.set(struct.type, struct);
53
+
54
+ this.owner = new Int32Array(this.blocks.length);
55
+ this.fileWide = this.header.version < 500 ? new Map() : null;
56
+ let scope = -1;
57
+ this.blocks.forEach((block, i) => {
58
+ if (block.code !== 'DATA') {
59
+ scope = i;
60
+ if (block.address !== 0n) this.ids.set(block.address, i);
61
+ } else if (block.address !== 0n) {
62
+ let map = this.scoped.get(scope);
63
+ if (map === undefined) this.scoped.set(scope, (map = new Map()));
64
+ map.set(block.address, i);
65
+ let list = this.sorted.get(scope);
66
+ if (list === undefined) this.sorted.set(scope, (list = []));
67
+ list.push(i);
68
+ }
69
+ this.owner[i] = block.code === 'DATA' ? scope : i;
70
+ if (this.fileWide !== null && block.address !== 0n) this.fileWide.set(block.address, i);
71
+ });
72
+ for (const list of this.sorted.values()) {
73
+ list.sort((a, b) => {
74
+ const x = (this.blocks[a] as BlendBlock).address;
75
+ const y = (this.blocks[b] as BlendBlock).address;
76
+ return x < y ? -1 : x > y ? 1 : 0;
77
+ });
78
+ }
79
+ }
80
+
81
+ /** The catalogue entry for a struct type, or undefined where this file's Blender had none. */
82
+ struct(type: string): DnaStruct | undefined {
83
+ return this.byType.get(type);
84
+ }
85
+
86
+ /** The datablock a block's data belongs to, which is what scopes every pointer inside it. */
87
+ scopeOf(block: number): number {
88
+ return this.owner[block] ?? -1;
89
+ }
90
+
91
+ /**
92
+ * The block an address falls in, and how far into it, as seen from inside datablock `scope`.
93
+ *
94
+ * The datablock's own data first, then the datablocks themselves, which is the order Blender's
95
+ * reader resolves in. Null for an address nothing holds.
96
+ */
97
+ find(address: bigint, scope: number): { block: number; offset: number } | null {
98
+ if (address === 0n) return null;
99
+ const own = this.scoped.get(scope)?.get(address) ?? this.ids.get(address);
100
+ if (own !== undefined) return { block: own, offset: 0 };
101
+ const list = this.sorted.get(scope);
102
+ if (list !== undefined) {
103
+ let lo = 0;
104
+ let hi = list.length - 1;
105
+ while (lo <= hi) {
106
+ const mid = (lo + hi) >> 1;
107
+ const index = list[mid] as number;
108
+ const block = this.blocks[index] as BlendBlock;
109
+ if (block.address > address) hi = mid - 1;
110
+ else if (block.address + BigInt(block.length) <= address) lo = mid + 1;
111
+ else return { block: index, offset: Number(address - block.address) };
112
+ }
113
+ }
114
+ const anywhere = this.fileWide?.get(address);
115
+ return anywhere === undefined ? null : { block: anywhere, offset: 0 };
116
+ }
117
+
118
+ /** Every datablock of a two-letter code — `OB`, `ME`, `MA` — as views of their own struct. */
119
+ idsOf(code: string): BlendStruct[] {
120
+ const out: BlendStruct[] = [];
121
+ this.blocks.forEach((block, i) => {
122
+ if (block.code !== code) return;
123
+ const struct = this.structs[block.sdna];
124
+ if (struct !== undefined) out.push(new BlendStruct(this, struct, block.offset, i));
125
+ });
126
+ return out;
127
+ }
128
+
129
+ /** A view of the struct at an address, typed by the block that holds it or by `type`. */
130
+ at(address: bigint, scope: number, type?: string): BlendStruct | null {
131
+ const found = this.find(address, scope);
132
+ if (found === null) return null;
133
+ const block = this.blocks[found.block] as BlendBlock;
134
+ const struct = type === undefined ? this.structs[block.sdna] : this.byType.get(type);
135
+ if (struct === undefined) return null;
136
+ return new BlendStruct(this, struct, block.offset + found.offset, this.scopeOf(found.block));
137
+ }
138
+
139
+ /**
140
+ * The structs of an array a pointer names, indexed *inside* its block.
141
+ *
142
+ * **Never by adding to the address.** From 5.0 an address is a placeholder rather than a
143
+ * location, so `base + 24` is not the second element and can be exactly the address of some
144
+ * unrelated block.
145
+ */
146
+ array(address: bigint, scope: number, type: string, count?: number): BlendStruct[] {
147
+ const found = this.find(address, scope);
148
+ const struct = this.byType.get(type);
149
+ if (found === null || struct === undefined) return [];
150
+ const block = this.blocks[found.block] as BlendBlock;
151
+ const start = block.offset + found.offset;
152
+ const room = Math.floor((block.offset + block.length - start) / struct.size);
153
+ const n = Math.min(count ?? room, room);
154
+ const owner = this.scopeOf(found.block);
155
+ const out: BlendStruct[] = [];
156
+ for (let i = 0; i < n; i++)
157
+ out.push(new BlendStruct(this, struct, start + i * struct.size, owner));
158
+ return out;
159
+ }
160
+
161
+ /** The raw bytes a pointer names, through the end of its block. */
162
+ raw(address: bigint, scope: number): Uint8Array | null {
163
+ const found = this.find(address, scope);
164
+ if (found === null) return null;
165
+ const block = this.blocks[found.block] as BlendBlock;
166
+ return this.bytes.subarray(block.offset + found.offset, block.offset + block.length);
167
+ }
168
+
169
+ /** A zero-terminated string a `char *` names. */
170
+ text(address: bigint, scope: number): string {
171
+ const raw = this.raw(address, scope);
172
+ if (raw === null) return '';
173
+ const stop = raw.indexOf(0);
174
+ return new TextDecoder().decode(stop < 0 ? raw : raw.subarray(0, stop));
175
+ }
176
+
177
+ /** The block a pointer names, for its count and its catalogue type. */
178
+ blockOf(address: bigint, scope: number): BlendBlock | null {
179
+ const found = this.find(address, scope);
180
+ return found === null ? null : (this.blocks[found.block] ?? null);
181
+ }
182
+
183
+ pointer(at: number): bigint {
184
+ const le = this.header.littleEndian;
185
+ return this.header.pointerSize === 8
186
+ ? this.view.getBigUint64(at, le)
187
+ : BigInt(this.view.getUint32(at, le));
188
+ }
189
+ }
190
+
191
+ /** One struct in the file, read by field name. */
192
+ export class BlendStruct {
193
+ constructor(
194
+ readonly file: BlendData,
195
+ readonly struct: DnaStruct,
196
+ readonly offset: number,
197
+ /** The datablock this struct's data belongs to, which every pointer in it is resolved inside. */
198
+ readonly scope: number,
199
+ ) {}
200
+
201
+ get type(): string {
202
+ return this.struct.type;
203
+ }
204
+
205
+ has(name: string): boolean {
206
+ return this.struct.fields.has(name);
207
+ }
208
+
209
+ field(name: string): DnaField {
210
+ const field = this.struct.fields.get(name);
211
+ if (field === undefined) {
212
+ throw new DrftError(
213
+ `blend: ${this.struct.type} has no field "${name}" in a file from Blender ` +
214
+ `${this.file.header.version}, which is a reader bug rather than a bad file`,
215
+ );
216
+ }
217
+ return field;
218
+ }
219
+
220
+ /** An integer field of any width, signed as its type is. */
221
+ int(name: string, index = 0): number {
222
+ const field = this.field(name);
223
+ return readInt(this.file, field.type, this.offset + field.offset + index * elementSize(field));
224
+ }
225
+
226
+ float(name: string, index = 0): number {
227
+ const field = this.field(name);
228
+ const at = this.offset + field.offset + index * elementSize(field);
229
+ const le = this.file.header.littleEndian;
230
+ return field.type === 'double'
231
+ ? this.file.view.getFloat64(at, le)
232
+ : this.file.view.getFloat32(at, le);
233
+ }
234
+
235
+ /** Every element of a float array field, flattened: `co[3]` is three, `mat[4][4]` sixteen. */
236
+ floats(name: string): number[] {
237
+ const field = this.field(name);
238
+ const count = field.dims.reduce((n, d) => n * d, 1);
239
+ const out: number[] = [];
240
+ for (let i = 0; i < count; i++) out.push(this.float(name, i));
241
+ return out;
242
+ }
243
+
244
+ ints(name: string): number[] {
245
+ const field = this.field(name);
246
+ const count = field.dims.reduce((n, d) => n * d, 1);
247
+ const out: number[] = [];
248
+ for (let i = 0; i < count; i++) out.push(this.int(name, i));
249
+ return out;
250
+ }
251
+
252
+ /** A `char[]` field as text, up to its first zero. */
253
+ string(name: string): string {
254
+ const field = this.field(name);
255
+ const start = this.offset + field.offset;
256
+ const bytes = this.file.bytes.subarray(start, start + field.size);
257
+ const stop = bytes.indexOf(0);
258
+ return new TextDecoder().decode(stop < 0 ? bytes : bytes.subarray(0, stop));
259
+ }
260
+
261
+ /** An ID's name without its two-letter code: `OBCube` is `Cube`. */
262
+ idName(): string {
263
+ return this.sub('id').string('name').slice(2);
264
+ }
265
+
266
+ ptr(name: string, index = 0): bigint {
267
+ const field = this.field(name);
268
+ if (!field.pointer) throw new DrftError(`blend: ${this.struct.type}.${name} is not a pointer`);
269
+ return this.file.pointer(this.offset + field.offset + index * this.file.header.pointerSize);
270
+ }
271
+
272
+ /** Follow a pointer field, typed by the block it lands in or by `type` where that is known. */
273
+ deref(name: string, type?: string): BlendStruct | null {
274
+ return this.file.at(this.ptr(name), this.scope, type);
275
+ }
276
+
277
+ /** An array a pointer field names, `count` long or as long as its block. */
278
+ array(name: string, type: string, count?: number): BlendStruct[] {
279
+ return this.file.array(this.ptr(name), this.scope, type, count);
280
+ }
281
+
282
+ /** The bytes a pointer field names, through the end of their block. */
283
+ raw(name: string): Uint8Array | null {
284
+ return this.file.raw(this.ptr(name), this.scope);
285
+ }
286
+
287
+ /** The block a pointer field names. */
288
+ blockOf(name: string): BlendBlock | null {
289
+ return this.file.blockOf(this.ptr(name), this.scope);
290
+ }
291
+
292
+ /** A `char *` field's text. */
293
+ text(name: string): string {
294
+ return this.file.text(this.ptr(name), this.scope);
295
+ }
296
+
297
+ /** An embedded struct field, viewed in place. */
298
+ sub(name: string): BlendStruct {
299
+ const field = this.field(name);
300
+ const struct = this.file.struct(field.type);
301
+ if (struct === undefined)
302
+ throw new DrftError(`blend: no struct ${field.type} in the catalogue`);
303
+ return new BlendStruct(this.file, struct, this.offset + field.offset, this.scope);
304
+ }
305
+
306
+ /** The links of a `ListBase` field, following `next` until it runs out. */
307
+ list(name: string, type?: string): BlendStruct[] {
308
+ const base = this.sub(name);
309
+ const out: BlendStruct[] = [];
310
+ const seen = new Set<bigint>();
311
+ let address = base.ptr('first');
312
+ while (address !== 0n && !seen.has(address)) {
313
+ seen.add(address);
314
+ const link = this.file.at(address, this.scope, type);
315
+ if (link === null) break;
316
+ out.push(link);
317
+ /* `next` is the first pointer of every link, including those that embed it in a header
318
+ struct of their own, as every modifier and constraint does. */
319
+ address = this.file.pointer(link.offset);
320
+ }
321
+ return out;
322
+ }
323
+
324
+ /** An array of pointers that a pointer-to-pointer field names: `Mesh.mat`, `Object.mat`. */
325
+ pointers(name: string, count: number): bigint[] {
326
+ const raw = this.file.find(this.ptr(name), this.scope);
327
+ if (raw === null) return [];
328
+ const out: bigint[] = [];
329
+ const base = (this.file.blocks[raw.block] as BlendBlock).offset + raw.offset;
330
+ for (let i = 0; i < count; i++)
331
+ out.push(this.file.pointer(base + i * this.file.header.pointerSize));
332
+ return out;
333
+ }
334
+ }
335
+
336
+ function elementSize(field: DnaField): number {
337
+ const count = field.dims.reduce((n, d) => n * d, 1);
338
+ return field.size / count;
339
+ }
340
+
341
+ function readInt(file: BlendData, type: string, at: number): number {
342
+ const le = file.header.littleEndian;
343
+ const view = file.view;
344
+ switch (type) {
345
+ case 'char':
346
+ case 'uchar':
347
+ case 'uint8_t':
348
+ case 'bool':
349
+ return view.getUint8(at);
350
+ case 'int8_t':
351
+ return view.getInt8(at);
352
+ case 'short':
353
+ case 'int16_t':
354
+ return view.getInt16(at, le);
355
+ case 'ushort':
356
+ case 'uint16_t':
357
+ return view.getUint16(at, le);
358
+ case 'int':
359
+ case 'int32_t':
360
+ return view.getInt32(at, le);
361
+ case 'uint':
362
+ case 'uint32_t':
363
+ return view.getUint32(at, le);
364
+ case 'int64_t':
365
+ case 'long':
366
+ return Number(view.getBigInt64(at, le));
367
+ case 'uint64_t':
368
+ case 'ulong':
369
+ return Number(view.getBigUint64(at, le));
370
+ case 'float':
371
+ return view.getFloat32(at, le);
372
+ default:
373
+ throw new DrftError(`blend: no integer reading for a field of type ${type}`);
374
+ }
375
+ }