@gmvjs/core 0.1.0 → 0.1.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.
- package/README.md +41 -35
- package/dist/archives/common.d.ts +9 -9
- package/dist/archives/common.js.map +1 -1
- package/dist/archives/folder.d.ts +1 -1
- package/dist/archives/folder.js +1 -1
- package/dist/archives/folder.js.map +1 -1
- package/dist/archives/zip.js.map +1 -1
- package/dist/binary.d.ts +7 -7
- package/dist/binary.js.map +1 -1
- package/dist/builtin-en.js.map +1 -1
- package/dist/builtin.js +1 -1
- package/dist/builtin.js.map +1 -1
- package/dist/codecs/zlib.d.ts +2 -2
- package/dist/codecs/zlib.js.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/geometry.d.ts +5 -5
- package/dist/geometry.js.map +1 -1
- package/dist/i18n.d.ts +4 -4
- package/dist/i18n.js.map +1 -1
- package/dist/images/bc45.d.ts +1 -1
- package/dist/images/bc45.js.map +1 -1
- package/dist/images/bc7.d.ts +1 -1
- package/dist/images/bc7.js.map +1 -1
- package/dist/images/dds.d.ts +1 -1
- package/dist/images/dds.js.map +1 -1
- package/dist/images/pcx.js.map +1 -1
- package/dist/images/tga.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/levelbuilder.d.ts +3 -3
- package/dist/levelbuilder.js +1 -1
- package/dist/levelbuilder.js.map +1 -1
- package/dist/math.d.ts +6 -6
- package/dist/math.js.map +1 -1
- package/dist/normals.d.ts +5 -5
- package/dist/normals.js.map +1 -1
- package/dist/plugin.d.ts +57 -57
- package/dist/plugin.js.map +1 -1
- package/dist/registry.d.ts +14 -14
- package/dist/registry.js +13 -13
- package/dist/registry.js.map +1 -1
- package/dist/scene.d.ts +14 -14
- package/dist/scene.js.map +1 -1
- package/dist/session.d.ts +34 -34
- package/dist/session.js +33 -33
- package/dist/session.js.map +1 -1
- package/dist/skeleton.d.ts +3 -3
- package/dist/skeleton.js.map +1 -1
- package/dist/surface.d.ts +4 -4
- package/dist/surface.js.map +1 -1
- package/dist/textures.d.ts +12 -12
- package/dist/textures.js +9 -9
- package/dist/textures.js.map +1 -1
- package/dist/types.d.ts +103 -103
- package/dist/types.js.map +1 -1
- package/dist/writers/dds.d.ts +1 -1
- package/dist/writers/dds.js.map +1 -1
- package/dist/writers/obj.js.map +1 -1
- package/dist/writers/zip.js.map +1 -1
- package/package.json +3 -3
package/dist/scene.d.ts
CHANGED
|
@@ -2,24 +2,24 @@ import type { Model } from './types';
|
|
|
2
2
|
export interface SceneNode {
|
|
3
3
|
id: string;
|
|
4
4
|
name: string;
|
|
5
|
-
/** group
|
|
5
|
+
/** group: a folder; mesh: a model mesh; instance: a scene object instance. */
|
|
6
6
|
kind: 'group' | 'mesh' | 'instance';
|
|
7
7
|
mesh?: number;
|
|
8
8
|
instance?: number;
|
|
9
|
-
/**
|
|
9
|
+
/** Right-hand label (triangle count, instance model). */
|
|
10
10
|
note?: string;
|
|
11
11
|
children?: SceneNode[];
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Layer tree: for a scene with instances, "Geometry" (meshes without instances) and "Objects" by group (class);
|
|
15
|
+
* for a regular model, the meshes.
|
|
16
16
|
*/
|
|
17
17
|
export declare function sceneTree(model: Model): SceneNode[];
|
|
18
|
-
/**
|
|
18
|
+
/** All leaf nodes under a node (meshes and instances). */
|
|
19
19
|
export declare function sceneLeaves(n: SceneNode): SceneNode[];
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Rewrite an instance's vertices and indices in the merged meshes from its pose (after rotation).
|
|
22
|
+
* Returns the changed vertex ranges; they must be re-uploaded to the renderer.
|
|
23
23
|
*/
|
|
24
24
|
export declare function applyPlacement(model: Model, index: number): Array<{
|
|
25
25
|
mesh: number;
|
|
@@ -28,22 +28,22 @@ export declare function applyPlacement(model: Model, index: number): Array<{
|
|
|
28
28
|
iStart: number;
|
|
29
29
|
iCount: number;
|
|
30
30
|
}>;
|
|
31
|
-
/**
|
|
31
|
+
/** Rotate an instance about the model's vertical axis (Y) through its origin: R' = R·Ry(angle). */
|
|
32
32
|
export declare function rotatePlacement(model: Model, index: number, angle: number): ReturnType<typeof applyPlacement>;
|
|
33
|
-
/**
|
|
33
|
+
/** Move an instance by (dx, dy, dz) in model coordinates. */
|
|
34
34
|
export declare function translatePlacement(model: Model, index: number, d: ArrayLike<number>): ReturnType<typeof applyPlacement>;
|
|
35
|
-
/**
|
|
35
|
+
/** Mesh indices without hidden instances (for export and picking). */
|
|
36
36
|
export declare function visibleIndices(model: Model, mesh: number): Uint32Array;
|
|
37
|
-
/**
|
|
37
|
+
/** Instance a triangle belongs to (by the triangle's first index), or −1. */
|
|
38
38
|
export declare function instanceAt(model: Model, mesh: number, index: number): number;
|
|
39
39
|
export interface RayHit {
|
|
40
|
-
mesh: number; /**
|
|
40
|
+
mesh: number; /** Index of the triangle's first index. */
|
|
41
41
|
index: number;
|
|
42
42
|
t: number;
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
* skip
|
|
45
|
+
* Nearest triangle along a ray (in model coordinates, without the skeletal pose).
|
|
46
|
+
* skip: meshes that do not take part (hidden); clipY: a cut from above (by height in model coordinates, if set).
|
|
47
47
|
*/
|
|
48
48
|
export declare function pickRay(model: Model, origin: ArrayLike<number>, dir: ArrayLike<number>, opts?: {
|
|
49
49
|
skip?: (mesh: number) => boolean;
|
package/dist/scene.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scene.js","sources":["../src/scene.ts"],"sourcesContent":["// Сцена модели для интерфейса: дерево слоёв, видимость и поворот экземпляров, выбор треугольника лучом.\nimport { tr } from './i18n';\nimport type { InstancePlacement, Model } from './types';\n\nexport interface SceneNode {\n id: string;\n name: string;\n /** group — папка; mesh — меш модели; instance — экземпляр объекта сцены. */\n kind: 'group' | 'mesh' | 'instance';\n mesh?: number;\n instance?: number;\n /** Подпись справа (число треугольников, модель экземпляра). */\n note?: string;\n children?: SceneNode[];\n}\n\nconst tris = (model: Model, i: number) => Math.floor(model.meshes[i]!.indices.length / 3);\n\n/**\n * Дерево слоёв: для сцены с экземплярами — «Геометрия» (меши без экземпляров) и «Объекты» по группам (классам);\n * для обычной модели — меши.\n */\nexport function sceneTree(model: Model): SceneNode[] {\n const inst = model.instances, instMeshes = new Set(inst?.meshes ?? []);\n const meshNode = (i: number): SceneNode => ({ id: `m${i}`, name: model.meshes[i]!.name || tr('scene.mesh', { i }), kind: 'mesh', mesh: i, note: tr('scene.tris', { n: tris(model, i) }) });\n const plain = model.meshes.map((_, i) => i).filter((i) => !instMeshes.has(i));\n if (!inst?.placements.length) return [{ id: 'model', name: tr('scene.model'), kind: 'group', note: tr('scene.meshes', { n: plain.length }), children: plain.map(meshNode) }];\n const groups = new Map<string, number[]>();\n inst.placements.forEach((p, i) => { const g = p.group || tr('scene.otherObjects'); (groups.get(g) ?? groups.set(g, []).get(g)!).push(i); });\n const protoTris = (p: InstancePlacement) => inst.protos[p.proto]!.parts.reduce((a, x) => a + Math.floor(x.indices.length / 3), 0);\n return [\n { id: 'geom', name: tr('scene.geometry'), kind: 'group', note: tr('scene.meshes', { n: plain.length }), children: plain.map(meshNode) },\n {\n id: 'objects', name: tr('scene.objects'), kind: 'group', note: String(inst.placements.length),\n children: [...groups].sort((a, b) => b[1].length - a[1].length).map(([g, list]) => ({\n id: `g:${g}`, name: g, kind: 'group' as const, note: String(list.length),\n children: list.map((i) => {\n const p = inst.placements[i]!, proto = inst.protos[p.proto]!;\n return { id: `i${i}`, name: p.name, kind: 'instance' as const, instance: i, note: `${proto.name.split('/').pop()} · ${tr('scene.tris', { n: protoTris(p) })}` };\n }),\n })),\n },\n ];\n}\n\n/** Все узлы-листья под узлом (меши и экземпляры). */\nexport function sceneLeaves(n: SceneNode): SceneNode[] {\n return n.kind === 'group' ? (n.children ?? []).flatMap(sceneLeaves) : [n];\n}\n\n/**\n * Переписать вершины и индексы экземпляра в слитых мешах по его позе (после поворота).\n * Возвращает изменённые диапазоны вершин — их нужно перезалить в рендерер.\n */\nexport function applyPlacement(model: Model, index: number): Array<{ mesh: number; vStart: number; vCount: number; iStart: number; iCount: number }> {\n const inst = model.instances, p = inst?.placements[index];\n if (!inst || !p?.ranges) return [];\n const M = p.pose, proto = inst.protos[p.proto]!;\n const det = M[0]! * (M[5]! * M[10]! - M[6]! * M[9]!) - M[1]! * (M[4]! * M[10]! - M[6]! * M[8]!) + M[2]! * (M[4]! * M[9]! - M[5]! * M[8]!);\n for (const r of p.ranges) {\n const part = proto.parts[r.part], m = model.meshes[r.mesh];\n if (!part || !m) continue;\n for (let v = 0; v < r.vCount; v++) {\n const x = part.pos[v * 3]!, y = part.pos[v * 3 + 1]!, z = part.pos[v * 3 + 2]!, o = (r.vStart + v) * 3;\n m.pos[o] = x * M[0]! + y * M[4]! + z * M[8]! + M[12]!;\n m.pos[o + 1] = x * M[1]! + y * M[5]! + z * M[9]! + M[13]!;\n m.pos[o + 2] = x * M[2]! + y * M[6]! + z * M[10]! + M[14]!;\n const a = part.nor[v * 3]!, b = part.nor[v * 3 + 1]!, c = part.nor[v * 3 + 2]!;\n const nx = a * M[0]! + b * M[4]! + c * M[8]!, ny = a * M[1]! + b * M[5]! + c * M[9]!, nz = a * M[2]! + b * M[6]! + c * M[10]!, l = Math.hypot(nx, ny, nz) || 1;\n m.nor[o] = nx / l; m.nor[o + 1] = ny / l; m.nor[o + 2] = nz / l;\n }\n for (let t = 0; t + 2 < r.iCount; t += 3) {\n const a = part.indices[t]!, b = part.indices[t + 1]!, c = part.indices[t + 2]!, o = r.iStart + t;\n m.indices[o] = r.vStart + a;\n m.indices[o + 1] = r.vStart + (det < 0 ? c : b);\n m.indices[o + 2] = r.vStart + (det < 0 ? b : c);\n }\n }\n return p.ranges.map((r) => ({ mesh: r.mesh, vStart: r.vStart, vCount: r.vCount, iStart: r.iStart, iCount: r.iCount }));\n}\n\n/** Повернуть экземпляр вокруг вертикали модели (ось Y) через его начало координат: R' = R·Ry(угол). */\nexport function rotatePlacement(model: Model, index: number, angle: number): ReturnType<typeof applyPlacement> {\n const p = model.instances?.placements[index];\n if (!p) return [];\n const c = Math.cos(angle), s = Math.sin(angle), M = p.pose;\n for (let row = 0; row < 3; row++) {\n const x = M[row * 4]!, z = M[row * 4 + 2]!;\n // строка × Ry: x' = x·c − z·s, z' = x·s + z·c\n M[row * 4] = x * c - z * s;\n M[row * 4 + 2] = x * s + z * c;\n }\n return applyPlacement(model, index);\n}\n\n/** Сдвинуть экземпляр на (dx, dy, dz) в координатах модели. */\nexport function translatePlacement(model: Model, index: number, d: ArrayLike<number>): ReturnType<typeof applyPlacement> {\n const p = model.instances?.placements[index];\n if (!p) return [];\n p.pose[12] = p.pose[12]! + d[0]!; p.pose[13] = p.pose[13]! + d[1]!; p.pose[14] = p.pose[14]! + d[2]!;\n return applyPlacement(model, index);\n}\n\n/** Индексы меша без скрытых экземпляров (для экспорта и выбора). */\nexport function visibleIndices(model: Model, mesh: number): Uint32Array {\n const src = model.meshes[mesh]!.indices, hidden = model.instances?.placements.filter((p) => p.hidden && p.ranges?.some((r) => r.mesh === mesh));\n if (!hidden?.length) return Uint32Array.from(src);\n const mask = new Uint8Array(src.length);\n for (const p of hidden) for (const r of p.ranges!) if (r.mesh === mesh) mask.fill(1, r.iStart, r.iStart + r.iCount);\n const out: number[] = [];\n for (let i = 0; i + 2 < src.length; i += 3) if (!mask[i]) out.push(src[i]!, src[i + 1]!, src[i + 2]!);\n return Uint32Array.from(out);\n}\n\n/** Экземпляр, которому принадлежит треугольник (по первому индексу треугольника), или −1. */\nexport function instanceAt(model: Model, mesh: number, index: number): number {\n const pl = model.instances?.placements;\n if (!pl) return -1;\n for (let i = 0; i < pl.length; i++) for (const r of pl[i]!.ranges ?? []) if (r.mesh === mesh && index >= r.iStart && index < r.iStart + r.iCount) return i;\n return -1;\n}\n\nexport interface RayHit { mesh: number; /** Номер первого индекса треугольника. */ index: number; t: number }\n\n/**\n * Ближайший треугольник на луче (в координатах модели, без скелетной позы).\n * skip — меши, которые не участвуют (скрытые); clipY — срез сверху (по высоте в координатах модели, если задано).\n */\nexport function pickRay(model: Model, origin: ArrayLike<number>, dir: ArrayLike<number>, opts: { skip?: (mesh: number) => boolean; indices?: (mesh: number) => ArrayLike<number>; clip?: (x: number, y: number, z: number) => boolean } = {}): RayHit | null {\n const [ox, oy, oz] = [origin[0]!, origin[1]!, origin[2]!], [dx, dy, dz] = [dir[0]!, dir[1]!, dir[2]!];\n let best: RayHit | null = null, bestT = Infinity;\n model.meshes.forEach((m, mi) => {\n if (opts.skip?.(mi)) return;\n const P = m.pos, I = opts.indices?.(mi) ?? m.indices;\n for (let t = 0; t + 2 < I.length; t += 3) {\n const a = I[t]! * 3, b = I[t + 1]! * 3, c = I[t + 2]! * 3;\n if (a === b && b === c) continue; // скрытый (вырожденный) треугольник\n const ax = P[a]!, ay = P[a + 1]!, az = P[a + 2]!;\n const e1x = P[b]! - ax, e1y = P[b + 1]! - ay, e1z = P[b + 2]! - az, e2x = P[c]! - ax, e2y = P[c + 1]! - ay, e2z = P[c + 2]! - az;\n const px = dy * e2z - dz * e2y, py = dz * e2x - dx * e2z, pz = dx * e2y - dy * e2x, det = e1x * px + e1y * py + e1z * pz;\n if (Math.abs(det) < 1e-12) continue;\n const inv = 1 / det, tx = ox - ax, ty = oy - ay, tz = oz - az, u = (tx * px + ty * py + tz * pz) * inv;\n if (u < 0 || u > 1) continue;\n const qx = ty * e1z - tz * e1y, qy = tz * e1x - tx * e1z, qz = tx * e1y - ty * e1x, v = (dx * qx + dy * qy + dz * qz) * inv;\n if (v < 0 || u + v > 1) continue;\n const d = (e2x * qx + e2y * qy + e2z * qz) * inv;\n if (d <= 0 || d >= bestT) continue;\n if (opts.clip && opts.clip(ox + dx * d, oy + dy * d, oz + dz * d)) continue;\n bestT = d; best = { mesh: mi, index: t, t: d };\n }\n });\n return best;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scene.js","sources":["../src/scene.ts"],"sourcesContent":["// Model scene for the UI: layer tree, instance visibility and rotation, ray picking of triangles.\nimport { tr } from './i18n';\nimport type { InstancePlacement, Model } from './types';\n\nexport interface SceneNode {\n id: string;\n name: string;\n /** group: a folder; mesh: a model mesh; instance: a scene object instance. */\n kind: 'group' | 'mesh' | 'instance';\n mesh?: number;\n instance?: number;\n /** Right-hand label (triangle count, instance model). */\n note?: string;\n children?: SceneNode[];\n}\n\nconst tris = (model: Model, i: number) => Math.floor(model.meshes[i]!.indices.length / 3);\n\n/**\n * Layer tree: for a scene with instances, \"Geometry\" (meshes without instances) and \"Objects\" by group (class);\n * for a regular model, the meshes.\n */\nexport function sceneTree(model: Model): SceneNode[] {\n const inst = model.instances, instMeshes = new Set(inst?.meshes ?? []);\n const meshNode = (i: number): SceneNode => ({ id: `m${i}`, name: model.meshes[i]!.name || tr('scene.mesh', { i }), kind: 'mesh', mesh: i, note: tr('scene.tris', { n: tris(model, i) }) });\n const plain = model.meshes.map((_, i) => i).filter((i) => !instMeshes.has(i));\n if (!inst?.placements.length) return [{ id: 'model', name: tr('scene.model'), kind: 'group', note: tr('scene.meshes', { n: plain.length }), children: plain.map(meshNode) }];\n const groups = new Map<string, number[]>();\n inst.placements.forEach((p, i) => { const g = p.group || tr('scene.otherObjects'); (groups.get(g) ?? groups.set(g, []).get(g)!).push(i); });\n const protoTris = (p: InstancePlacement) => inst.protos[p.proto]!.parts.reduce((a, x) => a + Math.floor(x.indices.length / 3), 0);\n return [\n { id: 'geom', name: tr('scene.geometry'), kind: 'group', note: tr('scene.meshes', { n: plain.length }), children: plain.map(meshNode) },\n {\n id: 'objects', name: tr('scene.objects'), kind: 'group', note: String(inst.placements.length),\n children: [...groups].sort((a, b) => b[1].length - a[1].length).map(([g, list]) => ({\n id: `g:${g}`, name: g, kind: 'group' as const, note: String(list.length),\n children: list.map((i) => {\n const p = inst.placements[i]!, proto = inst.protos[p.proto]!;\n return { id: `i${i}`, name: p.name, kind: 'instance' as const, instance: i, note: `${proto.name.split('/').pop()} · ${tr('scene.tris', { n: protoTris(p) })}` };\n }),\n })),\n },\n ];\n}\n\n/** All leaf nodes under a node (meshes and instances). */\nexport function sceneLeaves(n: SceneNode): SceneNode[] {\n return n.kind === 'group' ? (n.children ?? []).flatMap(sceneLeaves) : [n];\n}\n\n/**\n * Rewrite an instance's vertices and indices in the merged meshes from its pose (after rotation).\n * Returns the changed vertex ranges; they must be re-uploaded to the renderer.\n */\nexport function applyPlacement(model: Model, index: number): Array<{ mesh: number; vStart: number; vCount: number; iStart: number; iCount: number }> {\n const inst = model.instances, p = inst?.placements[index];\n if (!inst || !p?.ranges) return [];\n const M = p.pose, proto = inst.protos[p.proto]!;\n const det = M[0]! * (M[5]! * M[10]! - M[6]! * M[9]!) - M[1]! * (M[4]! * M[10]! - M[6]! * M[8]!) + M[2]! * (M[4]! * M[9]! - M[5]! * M[8]!);\n for (const r of p.ranges) {\n const part = proto.parts[r.part], m = model.meshes[r.mesh];\n if (!part || !m) continue;\n for (let v = 0; v < r.vCount; v++) {\n const x = part.pos[v * 3]!, y = part.pos[v * 3 + 1]!, z = part.pos[v * 3 + 2]!, o = (r.vStart + v) * 3;\n m.pos[o] = x * M[0]! + y * M[4]! + z * M[8]! + M[12]!;\n m.pos[o + 1] = x * M[1]! + y * M[5]! + z * M[9]! + M[13]!;\n m.pos[o + 2] = x * M[2]! + y * M[6]! + z * M[10]! + M[14]!;\n const a = part.nor[v * 3]!, b = part.nor[v * 3 + 1]!, c = part.nor[v * 3 + 2]!;\n const nx = a * M[0]! + b * M[4]! + c * M[8]!, ny = a * M[1]! + b * M[5]! + c * M[9]!, nz = a * M[2]! + b * M[6]! + c * M[10]!, l = Math.hypot(nx, ny, nz) || 1;\n m.nor[o] = nx / l; m.nor[o + 1] = ny / l; m.nor[o + 2] = nz / l;\n }\n for (let t = 0; t + 2 < r.iCount; t += 3) {\n const a = part.indices[t]!, b = part.indices[t + 1]!, c = part.indices[t + 2]!, o = r.iStart + t;\n m.indices[o] = r.vStart + a;\n m.indices[o + 1] = r.vStart + (det < 0 ? c : b);\n m.indices[o + 2] = r.vStart + (det < 0 ? b : c);\n }\n }\n return p.ranges.map((r) => ({ mesh: r.mesh, vStart: r.vStart, vCount: r.vCount, iStart: r.iStart, iCount: r.iCount }));\n}\n\n/** Rotate an instance about the model's vertical axis (Y) through its origin: R' = R·Ry(angle). */\nexport function rotatePlacement(model: Model, index: number, angle: number): ReturnType<typeof applyPlacement> {\n const p = model.instances?.placements[index];\n if (!p) return [];\n const c = Math.cos(angle), s = Math.sin(angle), M = p.pose;\n for (let row = 0; row < 3; row++) {\n const x = M[row * 4]!, z = M[row * 4 + 2]!;\n // row × Ry: x' = x·c − z·s, z' = x·s + z·c\n M[row * 4] = x * c - z * s;\n M[row * 4 + 2] = x * s + z * c;\n }\n return applyPlacement(model, index);\n}\n\n/** Move an instance by (dx, dy, dz) in model coordinates. */\nexport function translatePlacement(model: Model, index: number, d: ArrayLike<number>): ReturnType<typeof applyPlacement> {\n const p = model.instances?.placements[index];\n if (!p) return [];\n p.pose[12] = p.pose[12]! + d[0]!; p.pose[13] = p.pose[13]! + d[1]!; p.pose[14] = p.pose[14]! + d[2]!;\n return applyPlacement(model, index);\n}\n\n/** Mesh indices without hidden instances (for export and picking). */\nexport function visibleIndices(model: Model, mesh: number): Uint32Array {\n const src = model.meshes[mesh]!.indices, hidden = model.instances?.placements.filter((p) => p.hidden && p.ranges?.some((r) => r.mesh === mesh));\n if (!hidden?.length) return Uint32Array.from(src);\n const mask = new Uint8Array(src.length);\n for (const p of hidden) for (const r of p.ranges!) if (r.mesh === mesh) mask.fill(1, r.iStart, r.iStart + r.iCount);\n const out: number[] = [];\n for (let i = 0; i + 2 < src.length; i += 3) if (!mask[i]) out.push(src[i]!, src[i + 1]!, src[i + 2]!);\n return Uint32Array.from(out);\n}\n\n/** Instance a triangle belongs to (by the triangle's first index), or −1. */\nexport function instanceAt(model: Model, mesh: number, index: number): number {\n const pl = model.instances?.placements;\n if (!pl) return -1;\n for (let i = 0; i < pl.length; i++) for (const r of pl[i]!.ranges ?? []) if (r.mesh === mesh && index >= r.iStart && index < r.iStart + r.iCount) return i;\n return -1;\n}\n\nexport interface RayHit { mesh: number; /** Index of the triangle's first index. */ index: number; t: number }\n\n/**\n * Nearest triangle along a ray (in model coordinates, without the skeletal pose).\n * skip: meshes that do not take part (hidden); clipY: a cut from above (by height in model coordinates, if set).\n */\nexport function pickRay(model: Model, origin: ArrayLike<number>, dir: ArrayLike<number>, opts: { skip?: (mesh: number) => boolean; indices?: (mesh: number) => ArrayLike<number>; clip?: (x: number, y: number, z: number) => boolean } = {}): RayHit | null {\n const [ox, oy, oz] = [origin[0]!, origin[1]!, origin[2]!], [dx, dy, dz] = [dir[0]!, dir[1]!, dir[2]!];\n let best: RayHit | null = null, bestT = Infinity;\n model.meshes.forEach((m, mi) => {\n if (opts.skip?.(mi)) return;\n const P = m.pos, I = opts.indices?.(mi) ?? m.indices;\n for (let t = 0; t + 2 < I.length; t += 3) {\n const a = I[t]! * 3, b = I[t + 1]! * 3, c = I[t + 2]! * 3;\n if (a === b && b === c) continue; // hidden (degenerate) triangle\n const ax = P[a]!, ay = P[a + 1]!, az = P[a + 2]!;\n const e1x = P[b]! - ax, e1y = P[b + 1]! - ay, e1z = P[b + 2]! - az, e2x = P[c]! - ax, e2y = P[c + 1]! - ay, e2z = P[c + 2]! - az;\n const px = dy * e2z - dz * e2y, py = dz * e2x - dx * e2z, pz = dx * e2y - dy * e2x, det = e1x * px + e1y * py + e1z * pz;\n if (Math.abs(det) < 1e-12) continue;\n const inv = 1 / det, tx = ox - ax, ty = oy - ay, tz = oz - az, u = (tx * px + ty * py + tz * pz) * inv;\n if (u < 0 || u > 1) continue;\n const qx = ty * e1z - tz * e1y, qy = tz * e1x - tx * e1z, qz = tx * e1y - ty * e1x, v = (dx * qx + dy * qy + dz * qz) * inv;\n if (v < 0 || u + v > 1) continue;\n const d = (e2x * qx + e2y * qy + e2z * qz) * inv;\n if (d <= 0 || d >= bestT) continue;\n if (opts.clip && opts.clip(ox + dx * d, oy + dy * d, oz + dz * d)) continue;\n bestT = d; best = { mesh: mi, index: t, t: d };\n }\n });\n return best;\n}\n"],"names":[],"mappings":"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"}
|
package/dist/session.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface SessionEvents {
|
|
|
9
9
|
error: boolean;
|
|
10
10
|
};
|
|
11
11
|
preset: GamePreset;
|
|
12
|
-
/** keepView
|
|
12
|
+
/** keepView: the same model with other Model.options, do not reset the camera. */
|
|
13
13
|
model: {
|
|
14
14
|
model: Model;
|
|
15
15
|
main: VFile;
|
|
@@ -18,9 +18,9 @@ export interface SessionEvents {
|
|
|
18
18
|
};
|
|
19
19
|
motions: Motion[];
|
|
20
20
|
archives: Archive[];
|
|
21
|
-
/**
|
|
21
|
+
/** The set of available textures changed: textures should be applied again. */
|
|
22
22
|
textures: void;
|
|
23
|
-
/**
|
|
23
|
+
/** Long operation: text and, if known, how much of how much is done; null means finished. */
|
|
24
24
|
progress: {
|
|
25
25
|
text: string;
|
|
26
26
|
done?: number;
|
|
@@ -31,15 +31,15 @@ type Listener<T> = (payload: T) => void;
|
|
|
31
31
|
export interface TextureResult {
|
|
32
32
|
meshIndex: number;
|
|
33
33
|
image: DecodedImage | null;
|
|
34
|
-
/**
|
|
34
|
+
/** Label for the mesh list: "512×512", "no file", "error: …". */
|
|
35
35
|
label: string;
|
|
36
36
|
ok: boolean;
|
|
37
|
-
/**
|
|
37
|
+
/** Path of the found file (to look for a normal map next to it). */
|
|
38
38
|
source?: string;
|
|
39
39
|
}
|
|
40
40
|
export interface SurfaceResult {
|
|
41
41
|
meshIndex: number;
|
|
42
|
-
/**
|
|
42
|
+
/** Combined map: R = AO, G = roughness, B = metalness, A = specular. */
|
|
43
43
|
image: DecodedImage | null;
|
|
44
44
|
label: string;
|
|
45
45
|
ok: boolean;
|
|
@@ -58,7 +58,7 @@ export interface ArchiveItem {
|
|
|
58
58
|
export declare class ViewerSession {
|
|
59
59
|
readonly registry: Registry;
|
|
60
60
|
readonly archives: Archive[];
|
|
61
|
-
/**
|
|
61
|
+
/** Archive companion files selected earlier (XR_3DA.exe = SoC archive key, .dir for IMG): passed to archives when they are opened. */
|
|
62
62
|
readonly companions: BlobFile[];
|
|
63
63
|
readonly textures: TextureLibrary;
|
|
64
64
|
preset: GamePreset;
|
|
@@ -66,14 +66,14 @@ export declare class ViewerSession {
|
|
|
66
66
|
format: FormatPlugin | null;
|
|
67
67
|
main: VFile | null;
|
|
68
68
|
readonly motions: Motion[];
|
|
69
|
-
/**
|
|
69
|
+
/** Last failed load (for the "Copy diagnostics" button). */
|
|
70
70
|
lastFailure: {
|
|
71
71
|
file: VFile;
|
|
72
72
|
format: string;
|
|
73
73
|
error: string;
|
|
74
74
|
} | null;
|
|
75
75
|
private readonly overrides;
|
|
76
|
-
/**
|
|
76
|
+
/** Engine maps (resolveMaps) by mesh texture name. */
|
|
77
77
|
private readonly mapsCache;
|
|
78
78
|
private engineMaps;
|
|
79
79
|
private readonly listeners;
|
|
@@ -81,73 +81,73 @@ export declare class ViewerSession {
|
|
|
81
81
|
on<K extends keyof SessionEvents>(type: K, fn: Listener<SessionEvents[K]>): () => void;
|
|
82
82
|
private emit;
|
|
83
83
|
private status;
|
|
84
|
-
/**
|
|
84
|
+
/** Report progress of a long operation (for the progress bar); without arguments the operation is finished. */
|
|
85
85
|
private progress;
|
|
86
|
-
/**
|
|
86
|
+
/** Let the browser repaint the progress bar before a heavy synchronous step. */
|
|
87
87
|
private yieldUI;
|
|
88
88
|
setPreset(id: string): GamePreset;
|
|
89
89
|
get modelExtensions(): string[];
|
|
90
90
|
get motionExtensions(): string[];
|
|
91
|
-
/**
|
|
91
|
+
/** Sort a set of files into archives, texture dictionaries, images, animations and the model. */
|
|
92
92
|
addFiles(input: Iterable<BlobFile>): Promise<void>;
|
|
93
93
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
94
|
+
* Open archives. A file that cannot be read (not an archive, for example the encrypted F.E.A.R. Online a3.Arch01, or damaged)
|
|
95
|
+
* is skipped and the rest are opened;
|
|
96
|
+
* the status line tells how many were opened and what was skipped.
|
|
97
97
|
*/
|
|
98
98
|
addArchives(files: readonly BlobFile[]): Promise<void>;
|
|
99
|
-
/**
|
|
99
|
+
/** "Game folder" with a progress bar: the index is built in chunks so the UI does not freeze (hundreds of thousands of files). */
|
|
100
100
|
addFolderAsync(files: readonly BlobFile[]): Promise<Archive>;
|
|
101
|
-
/**
|
|
101
|
+
/** "Game folder": indexes files without reading them. */
|
|
102
102
|
addFolder(files: readonly BlobFile[]): Archive;
|
|
103
103
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
104
|
+
* Archives inside the "Game folder" that the engine asks to open right away (ArchivePlugin.openFromFolder): F.E.A.R. Online *.arch01.
|
|
105
|
+
* Returns the number of opened archives.
|
|
106
106
|
*/
|
|
107
107
|
openFolderArchives(folder: Archive): Promise<number>;
|
|
108
108
|
private archivesChanged;
|
|
109
109
|
addTextures(files: readonly BlobFile[]): void;
|
|
110
110
|
addTextureDictionary(file: VFile | BlobFile): Promise<void>;
|
|
111
|
-
/**
|
|
112
|
-
/**
|
|
111
|
+
/** Pick the main file (format detection) and load the model. */
|
|
112
|
+
/** Files and choices of the last load, for setModelOption. */
|
|
113
113
|
private lastLoad;
|
|
114
|
-
/**
|
|
114
|
+
/** Change a model choice (Model.options) and rebuild it from the same files. */
|
|
115
115
|
setModelOption(id: string, value: string): Promise<Model | null>;
|
|
116
116
|
loadModel(files: ReadonlyArray<VFile | BlobFile>, options?: Record<string, string>, keepView?: boolean): Promise<Model | null>;
|
|
117
117
|
private loadContext;
|
|
118
118
|
private afterLoadContext;
|
|
119
|
-
/**
|
|
119
|
+
/** Add external animations to the loaded model. */
|
|
120
120
|
loadMotions(file: VFile | BlobFile): Promise<Motion[]>;
|
|
121
121
|
setTextureOverride(meshIndex: number, file: VFile | BlobFile): void;
|
|
122
|
-
/**
|
|
123
|
-
/**
|
|
122
|
+
/** Find and decode a mesh texture. */
|
|
123
|
+
/** Mesh texture file (the same lookup as resolveTexture): for exporting the source bytes, for example DDS. */
|
|
124
124
|
textureSource(i: number): Promise<TextureSource | null>;
|
|
125
125
|
private findTextureSource;
|
|
126
126
|
resolveTexture(i: number): Promise<TextureResult>;
|
|
127
127
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
128
|
+
* Mesh normal map: embedded → named by the model/material → derived from the diffuse name (_co→_nohq, __d→__n_d, _diff→_normal, _d→_n…).
|
|
129
|
+
* A found image is checked for looking like a normal map and for its packing (RGB, RG/BC5, AG/DXT5nm).
|
|
130
130
|
*/
|
|
131
131
|
resolveNormal(i: number, diffuse?: string): Promise<NormalResult>;
|
|
132
132
|
/**
|
|
133
|
-
* PBR
|
|
134
|
-
*
|
|
133
|
+
* Mesh PBR surface: from the material (mesh.surface) or packed maps derived from the diffuse name (_orm, _rma, _smdi…).
|
|
134
|
+
* Maps are combined into one texture (composeSurface); with no maps but with multipliers, a 1×1 texture.
|
|
135
135
|
*/
|
|
136
136
|
resolveSurface(i: number, diffuse?: string): Promise<SurfaceResult>;
|
|
137
137
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
138
|
+
* Last resort: an image in the model folder. Its name matches the material, or "skin.*" (Quake II rerelease: md5/skin.png),
|
|
139
|
+
* or it is the only image in the folder (excluding *_glow/_n/_s/_normal/_spec maps).
|
|
140
140
|
*/
|
|
141
141
|
private sameFolderTexture;
|
|
142
|
-
/**
|
|
142
|
+
/** Models and animations of the current preset in all archives. */
|
|
143
143
|
archiveModels(filter?: string, limit?: number): {
|
|
144
144
|
items: ArchiveItem[];
|
|
145
145
|
total: number;
|
|
146
146
|
};
|
|
147
|
-
/**
|
|
147
|
+
/** Open an archive entry: add an animation, load a model. */
|
|
148
148
|
openEntry(item: ArchiveItem): Promise<void>;
|
|
149
149
|
}
|
|
150
150
|
export declare const errMsg: (e: unknown) => string;
|
|
151
|
-
/**
|
|
151
|
+
/** Animations from the same archive folder as the model (Doom 3: *.md5anim next to *.md5mesh). */
|
|
152
152
|
export declare function loadSameDirMotions(ctx: AfterLoadContext, ext: string): Promise<number>;
|
|
153
153
|
export {};
|
package/dist/session.js
CHANGED
|
@@ -15,7 +15,7 @@ class ViewerSession {
|
|
|
15
15
|
}
|
|
16
16
|
registry;
|
|
17
17
|
archives = [];
|
|
18
|
-
/**
|
|
18
|
+
/** Archive companion files selected earlier (XR_3DA.exe = SoC archive key, .dir for IMG): passed to archives when they are opened. */
|
|
19
19
|
companions = [];
|
|
20
20
|
textures;
|
|
21
21
|
preset;
|
|
@@ -23,10 +23,10 @@ class ViewerSession {
|
|
|
23
23
|
format = null;
|
|
24
24
|
main = null;
|
|
25
25
|
motions = [];
|
|
26
|
-
/**
|
|
26
|
+
/** Last failed load (for the "Copy diagnostics" button). */
|
|
27
27
|
lastFailure = null;
|
|
28
28
|
overrides = /* @__PURE__ */ new Map();
|
|
29
|
-
/**
|
|
29
|
+
/** Engine maps (resolveMaps) by mesh texture name. */
|
|
30
30
|
mapsCache = /* @__PURE__ */ new Map();
|
|
31
31
|
engineMaps(mesh) {
|
|
32
32
|
const fmt = this.format, model = this.model;
|
|
@@ -43,7 +43,7 @@ class ViewerSession {
|
|
|
43
43
|
return p;
|
|
44
44
|
}
|
|
45
45
|
listeners = /* @__PURE__ */ new Map();
|
|
46
|
-
// ----------
|
|
46
|
+
// ---------- events ----------
|
|
47
47
|
on(type, fn) {
|
|
48
48
|
let set = this.listeners.get(type);
|
|
49
49
|
if (!set) this.listeners.set(type, set = /* @__PURE__ */ new Set());
|
|
@@ -56,15 +56,15 @@ class ViewerSession {
|
|
|
56
56
|
status(text, error = false) {
|
|
57
57
|
this.emit("status", { text, error });
|
|
58
58
|
}
|
|
59
|
-
/**
|
|
59
|
+
/** Report progress of a long operation (for the progress bar); without arguments the operation is finished. */
|
|
60
60
|
progress(text, done, total) {
|
|
61
61
|
this.emit("progress", text === void 0 ? null : { text, done, total });
|
|
62
62
|
}
|
|
63
|
-
/**
|
|
63
|
+
/** Let the browser repaint the progress bar before a heavy synchronous step. */
|
|
64
64
|
yieldUI() {
|
|
65
65
|
return new Promise((r) => setTimeout(r, 0));
|
|
66
66
|
}
|
|
67
|
-
// ----------
|
|
67
|
+
// ---------- preset ----------
|
|
68
68
|
setPreset(id) {
|
|
69
69
|
const p = this.registry.game(id) ?? this.registry.games[0];
|
|
70
70
|
this.preset = p;
|
|
@@ -78,8 +78,8 @@ class ViewerSession {
|
|
|
78
78
|
const m = this.registry.presetMotionExtensions(this.preset);
|
|
79
79
|
return this.preset.ui?.motionAny ? [.../* @__PURE__ */ new Set([...m, ...this.modelExtensions])] : m;
|
|
80
80
|
}
|
|
81
|
-
// ----------
|
|
82
|
-
/**
|
|
81
|
+
// ---------- receiving files ----------
|
|
82
|
+
/** Sort a set of files into archives, texture dictionaries, images, animations and the model. */
|
|
83
83
|
async addFiles(input) {
|
|
84
84
|
const files = Array.from(input), reg = this.registry;
|
|
85
85
|
const motionExts = this.registry.motionOnlyExtensions;
|
|
@@ -95,9 +95,9 @@ class ViewerSession {
|
|
|
95
95
|
for (const f of mot) await this.loadMotions(f);
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
98
|
+
* Open archives. A file that cannot be read (not an archive, for example the encrypted F.E.A.R. Online a3.Arch01, or damaged)
|
|
99
|
+
* is skipped and the rest are opened;
|
|
100
|
+
* the status line tells how many were opened and what was skipped.
|
|
101
101
|
*/
|
|
102
102
|
async addArchives(files) {
|
|
103
103
|
const skipped = [];
|
|
@@ -135,7 +135,7 @@ class ViewerSession {
|
|
|
135
135
|
if (!opened) this.status(tr("session.archiveError", { error: skipped.slice(0, 3).join("; ") }), true);
|
|
136
136
|
} else this.archivesChanged(tr("session.archives", { n: this.archives.length }));
|
|
137
137
|
}
|
|
138
|
-
/**
|
|
138
|
+
/** "Game folder" with a progress bar: the index is built in chunks so the UI does not freeze (hundreds of thousands of files). */
|
|
139
139
|
async addFolderAsync(files) {
|
|
140
140
|
try {
|
|
141
141
|
const a = await openFolderAsync(files, (done, total) => this.progress(tr("session.indexing", { n: total }), done, total));
|
|
@@ -148,7 +148,7 @@ class ViewerSession {
|
|
|
148
148
|
this.progress();
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
/**
|
|
151
|
+
/** "Game folder": indexes files without reading them. */
|
|
152
152
|
addFolder(files) {
|
|
153
153
|
const a = openFolder(files);
|
|
154
154
|
this.archives.push(a);
|
|
@@ -156,8 +156,8 @@ class ViewerSession {
|
|
|
156
156
|
return a;
|
|
157
157
|
}
|
|
158
158
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
159
|
+
* Archives inside the "Game folder" that the engine asks to open right away (ArchivePlugin.openFromFolder): F.E.A.R. Online *.arch01.
|
|
160
|
+
* Returns the number of opened archives.
|
|
161
161
|
*/
|
|
162
162
|
async openFolderArchives(folder) {
|
|
163
163
|
if (!folder.blobOf) return 0;
|
|
@@ -212,11 +212,11 @@ class ViewerSession {
|
|
|
212
212
|
this.status(tr("session.dictError", { id: plugin.id.toUpperCase(), error: errMsg(e) }), true);
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
|
-
// ----------
|
|
216
|
-
/**
|
|
217
|
-
/**
|
|
215
|
+
// ---------- model ----------
|
|
216
|
+
/** Pick the main file (format detection) and load the model. */
|
|
217
|
+
/** Files and choices of the last load, for setModelOption. */
|
|
218
218
|
lastLoad = null;
|
|
219
|
-
/**
|
|
219
|
+
/** Change a model choice (Model.options) and rebuild it from the same files. */
|
|
220
220
|
async setModelOption(id, value) {
|
|
221
221
|
if (!this.lastLoad || !this.main) return null;
|
|
222
222
|
const options = { ...this.lastLoad.options, [id]: value };
|
|
@@ -316,7 +316,7 @@ class ViewerSession {
|
|
|
316
316
|
addTextureDictionary: (file) => this.addTextureDictionary(file)
|
|
317
317
|
};
|
|
318
318
|
}
|
|
319
|
-
/**
|
|
319
|
+
/** Add external animations to the loaded model. */
|
|
320
320
|
async loadMotions(file) {
|
|
321
321
|
if (!this.model?.loadMotions) {
|
|
322
322
|
this.status(tr("session.noMotionModel"), true);
|
|
@@ -334,12 +334,12 @@ class ViewerSession {
|
|
|
334
334
|
return [];
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
|
-
// ----------
|
|
337
|
+
// ---------- textures ----------
|
|
338
338
|
setTextureOverride(meshIndex, file) {
|
|
339
339
|
this.overrides.set(meshIndex, file);
|
|
340
340
|
}
|
|
341
|
-
/**
|
|
342
|
-
/**
|
|
341
|
+
/** Find and decode a mesh texture. */
|
|
342
|
+
/** Mesh texture file (the same lookup as resolveTexture): for exporting the source bytes, for example DDS. */
|
|
343
343
|
async textureSource(i) {
|
|
344
344
|
const model = this.model, mesh = model?.meshes[i];
|
|
345
345
|
if (!model || !mesh) return null;
|
|
@@ -380,8 +380,8 @@ class ViewerSession {
|
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
382
|
/**
|
|
383
|
-
*
|
|
384
|
-
*
|
|
383
|
+
* Mesh normal map: embedded → named by the model/material → derived from the diffuse name (_co→_nohq, __d→__n_d, _diff→_normal, _d→_n…).
|
|
384
|
+
* A found image is checked for looking like a normal map and for its packing (RGB, RG/BC5, AG/DXT5nm).
|
|
385
385
|
*/
|
|
386
386
|
async resolveNormal(i, diffuse) {
|
|
387
387
|
const mesh = this.model?.meshes[i], none = { meshIndex: i, image: null, packing: "rgb", label: "", ok: false };
|
|
@@ -410,8 +410,8 @@ class ViewerSession {
|
|
|
410
410
|
return mesh.normalMap ? { ...none, label: tr("tex.normalNoFile") } : none;
|
|
411
411
|
}
|
|
412
412
|
/**
|
|
413
|
-
* PBR
|
|
414
|
-
*
|
|
413
|
+
* Mesh PBR surface: from the material (mesh.surface) or packed maps derived from the diffuse name (_orm, _rma, _smdi…).
|
|
414
|
+
* Maps are combined into one texture (composeSurface); with no maps but with multipliers, a 1×1 texture.
|
|
415
415
|
*/
|
|
416
416
|
async resolveSurface(i, diffuse) {
|
|
417
417
|
const mesh = this.model?.meshes[i], none = { meshIndex: i, image: null, label: "", ok: false };
|
|
@@ -439,8 +439,8 @@ class ViewerSession {
|
|
|
439
439
|
return none;
|
|
440
440
|
}
|
|
441
441
|
/**
|
|
442
|
-
*
|
|
443
|
-
*
|
|
442
|
+
* Last resort: an image in the model folder. Its name matches the material, or "skin.*" (Quake II rerelease: md5/skin.png),
|
|
443
|
+
* or it is the only image in the folder (excluding *_glow/_n/_s/_normal/_spec maps).
|
|
444
444
|
*/
|
|
445
445
|
sameFolderTexture(texture) {
|
|
446
446
|
const main = this.main;
|
|
@@ -451,8 +451,8 @@ class ViewerSession {
|
|
|
451
451
|
const pick = imgs.find((e) => stem(e.path) === want) ?? imgs.find((e) => stem(e.path) === "skin") ?? (imgs.length === 1 ? imgs[0] : void 0);
|
|
452
452
|
return pick ? main.archive.fileOf(pick) : null;
|
|
453
453
|
}
|
|
454
|
-
// ----------
|
|
455
|
-
/**
|
|
454
|
+
// ---------- archive contents ----------
|
|
455
|
+
/** Models and animations of the current preset in all archives. */
|
|
456
456
|
archiveModels(filter = "", limit = 400) {
|
|
457
457
|
const exts = [...this.modelExtensions, ...this.motionExtensions].map((x) => "." + x), q = filter.trim().toLowerCase();
|
|
458
458
|
const items = [];
|
|
@@ -466,7 +466,7 @@ class ViewerSession {
|
|
|
466
466
|
}
|
|
467
467
|
return { items, total };
|
|
468
468
|
}
|
|
469
|
-
/**
|
|
469
|
+
/** Open an archive entry: add an animation, load a model. */
|
|
470
470
|
async openEntry(item) {
|
|
471
471
|
const f = item.archive.fileOf(item.entry), ext = extOf(item.entry.path);
|
|
472
472
|
if (this.registry.presetMotionExtensions(this.preset).includes(ext) && !this.modelExtensions.includes(ext)) await this.loadMotions(f);
|