@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.
Files changed (60) hide show
  1. package/README.md +41 -35
  2. package/dist/archives/common.d.ts +9 -9
  3. package/dist/archives/common.js.map +1 -1
  4. package/dist/archives/folder.d.ts +1 -1
  5. package/dist/archives/folder.js +1 -1
  6. package/dist/archives/folder.js.map +1 -1
  7. package/dist/archives/zip.js.map +1 -1
  8. package/dist/binary.d.ts +7 -7
  9. package/dist/binary.js.map +1 -1
  10. package/dist/builtin-en.js.map +1 -1
  11. package/dist/builtin.js +1 -1
  12. package/dist/builtin.js.map +1 -1
  13. package/dist/codecs/zlib.d.ts +2 -2
  14. package/dist/codecs/zlib.js.map +1 -1
  15. package/dist/diagnostics.js.map +1 -1
  16. package/dist/geometry.d.ts +5 -5
  17. package/dist/geometry.js.map +1 -1
  18. package/dist/i18n.d.ts +4 -4
  19. package/dist/i18n.js.map +1 -1
  20. package/dist/images/bc45.d.ts +1 -1
  21. package/dist/images/bc45.js.map +1 -1
  22. package/dist/images/bc7.d.ts +1 -1
  23. package/dist/images/bc7.js.map +1 -1
  24. package/dist/images/dds.d.ts +1 -1
  25. package/dist/images/dds.js.map +1 -1
  26. package/dist/images/pcx.js.map +1 -1
  27. package/dist/images/tga.js.map +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/levelbuilder.d.ts +3 -3
  31. package/dist/levelbuilder.js +1 -1
  32. package/dist/levelbuilder.js.map +1 -1
  33. package/dist/math.d.ts +6 -6
  34. package/dist/math.js.map +1 -1
  35. package/dist/normals.d.ts +5 -5
  36. package/dist/normals.js.map +1 -1
  37. package/dist/plugin.d.ts +57 -57
  38. package/dist/plugin.js.map +1 -1
  39. package/dist/registry.d.ts +14 -14
  40. package/dist/registry.js +13 -13
  41. package/dist/registry.js.map +1 -1
  42. package/dist/scene.d.ts +14 -14
  43. package/dist/scene.js.map +1 -1
  44. package/dist/session.d.ts +34 -34
  45. package/dist/session.js +33 -33
  46. package/dist/session.js.map +1 -1
  47. package/dist/skeleton.d.ts +3 -3
  48. package/dist/skeleton.js.map +1 -1
  49. package/dist/surface.d.ts +4 -4
  50. package/dist/surface.js.map +1 -1
  51. package/dist/textures.d.ts +12 -12
  52. package/dist/textures.js +9 -9
  53. package/dist/textures.js.map +1 -1
  54. package/dist/types.d.ts +103 -103
  55. package/dist/types.js.map +1 -1
  56. package/dist/writers/dds.d.ts +1 -1
  57. package/dist/writers/dds.js.map +1 -1
  58. package/dist/writers/obj.js.map +1 -1
  59. package/dist/writers/zip.js.map +1 -1
  60. 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 — папка; mesh — меш модели; instance — экземпляр объекта сцены. */
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
- /** Повернуть экземпляр вокруг вертикали модели (ось Y) через его начало координат: R' = R·Ry(угол). */
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
- /** Сдвинуть экземпляр на (dx, dy, dz) в координатах модели. */
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
- /** Экземпляр, которому принадлежит треугольник (по первому индексу треугольника), или −1. */
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 — меши, которые не участвуют (скрытые); clipY — срез сверху (по высоте в координатах модели, если задано).
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 — та же модель с другими Model.options: камеру не сбрасывать. */
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
- /** Долгая операция: текст и, если известно, сколько сделано из скольких; null — закончена. */
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
- /** Подпись для списка мешей: «512×512», «нет файла», «ошибка: …». */
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
- /** Сведённая карта: R — AO, G — шероховатость, B — металличность, A — блик. */
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
- /** Файлы-спутники архивов, выбранные раньше (XR_3DA.exe — ключ архивов ТЧ, .dir к IMG): передаются архивам при открытии. */
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
- /** Карты движка (resolveMaps) по имени текстуры меша. */
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
- * Открыть архивы. Файл, который не читается (не архив — например, зашифрованные a3.Arch01 F.E.A.R. Online, — повреждён),
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
- * Архивы внутри «Папки игры», которые движок просит открывать сразу (ArchivePlugin.openFromFolder): F.E.A.R. Online — *.arch01.
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
- /** Файлы и выборы последней загрузки — для setModelOption. */
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
- /** Сменить выбор модели (Model.options) и пересобрать её из тех же файлов. */
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
- /** Файл текстуры меша (тот же поиск, что у resolveTexture): для выгрузки исходных байтов, например DDS. */
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
- * Карта нормалей меша: встроенная → указанная моделью/материалом → по имени диффузной (_co→_nohq, __d→__n_d, _diff→_normal, _d→_n…).
129
- * Найденная картинка проверяется на «похожесть» на нормали и упаковку (RGB, RG/BC5, AG/DXT5nm).
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-поверхность меша: из материала (mesh.surface) или упакованные карты по имени диффузной (_orm, _rma, _smdi…).
134
- * Карты сводятся в одну текстуру (composeSurface); нет карт, но есть множители — текстура 1×1.
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
- * Последняя попытка: картинка в папке модели. Имя совпадает с материалом, или «skin.*» (Quake II rerelease: md5/skin.png),
139
- * или это единственная картинка в папке (без карт *_glow/_n/_s/_normal/_spec).
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
- /** Анимации из той же папки архива, что и модель (Doom 3: *.md5anim рядом с *.md5mesh). */
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
- /** Файлы-спутники архивов, выбранные раньше (XR_3DA.exe — ключ архивов ТЧ, .dir к IMG): передаются архивам при открытии. */
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
- /** Карты движка (resolveMaps) по имени текстуры меша. */
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
- * Открыть архивы. Файл, который не читается (не архив — например, зашифрованные a3.Arch01 F.E.A.R. Online, — повреждён),
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
- * Архивы внутри «Папки игры», которые движок просит открывать сразу (ArchivePlugin.openFromFolder): F.E.A.R. Online — *.arch01.
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
- /** Файлы и выборы последней загрузки — для setModelOption. */
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
- /** Сменить выбор модели (Model.options) и пересобрать её из тех же файлов. */
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
- /** Файл текстуры меша (тот же поиск, что у resolveTexture): для выгрузки исходных байтов, например DDS. */
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
- * Карта нормалей меша: встроенная → указанная моделью/материалом → по имени диффузной (_co→_nohq, __d→__n_d, _diff→_normal, _d→_n…).
384
- * Найденная картинка проверяется на «похожесть» на нормали и упаковку (RGB, RG/BC5, AG/DXT5nm).
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-поверхность меша: из материала (mesh.surface) или упакованные карты по имени диффузной (_orm, _rma, _smdi…).
414
- * Карты сводятся в одну текстуру (composeSurface); нет карт, но есть множители — текстура 1×1.
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
- * Последняя попытка: картинка в папке модели. Имя совпадает с материалом, или «skin.*» (Quake II rerelease: md5/skin.png),
443
- * или это единственная картинка в папке (без карт *_glow/_n/_s/_normal/_spec).
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);