@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/plugin.d.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  import type { Archive, BlobFile, DecodedImage, Mesh, Model, Motion, VFile } from './types';
2
2
  import type { TextureLibrary, TextureSource } from './textures';
3
- /** Степень готовности (для значка в интерфейсе). */
3
+ /** Readiness level (for the badge in the UI). */
4
4
  export type SupportStatus = 'stable' | 'beta' | 'bridge';
5
5
  export interface LoadContext {
6
- /** Все файлы, выбранные вместе с основным (.mtl, .bin, .vvd…). */
6
+ /** All files selected together with the main one (.mtl, .bin, .vvd…). */
7
7
  readonly files: readonly VFile[];
8
- /** Открытые архивы и папки. */
8
+ /** Open archives and folders. */
9
9
  readonly archives: readonly Archive[];
10
- /** Найти соседний файл: среди выбранных, затем в той же папке архива. */
10
+ /** Find a neighbouring file: among the selected ones, then in the same archive folder. */
11
11
  sibling(name: string): Promise<ArrayBuffer | null>;
12
- /** Прочитать файл архива по полному пути (первый найденный). */
12
+ /** Read an archive file by full path (first match). */
13
13
  readPath(path: string): Promise<Uint8Array | null>;
14
- /** Значения Model.options, выбранные пользователем (повторная загрузка с другими дисками, винилом…). */
14
+ /** Model.options values chosen by the user (reloading with other wheels, vinyl…). */
15
15
  readonly options?: Readonly<Record<string, string>>;
16
16
  }
17
17
  export interface AfterLoadContext {
@@ -22,34 +22,34 @@ export interface AfterLoadContext {
22
22
  addTextureDictionary(file: VFile): Promise<void>;
23
23
  }
24
24
  export interface FormatPlugin {
25
- /** Уникальный id: '<движок>-<формат>', например 'xray-ogf'. */
25
+ /** Unique id: '<engine>-<format>', for example 'xray-ogf'. */
26
26
  id: string;
27
27
  title: string;
28
28
  engine: string;
29
- /** Расширения без точки, нижний регистр. */
29
+ /** Extensions without the dot, lower case. */
30
30
  extensions: string[];
31
- /** Файлы без расширения, которые открывает формат (по пути в архиве/папке), например «media/meshcache/…». */
31
+ /** Extensionless files the format opens (by path in an archive/folder), for example "media/meshcache/…". */
32
32
  pathPattern?: RegExp;
33
- /** Расширения внешних анимаций, которые принимает модель этого формата. */
33
+ /** Extensions of external animations a model of this format accepts. */
34
34
  motionExtensions?: string[];
35
35
  status?: SupportStatus;
36
36
  /**
37
- * Уверенность 0..1 по первым 256 байтам файла. 0 — точно не этот формат.
38
- * Не задано — формат определяется только по расширению.
37
+ * Confidence 0..1 from the first 256 bytes of the file. 0 means definitely not this format.
38
+ * Not set: the format is detected by extension only.
39
39
  */
40
40
  sniff?(head: Uint8Array, name: string): number;
41
41
  load(main: VFile, ctx: LoadContext): Promise<Model>;
42
- /** Поиск текстуры меша по правилам движка (материалы .vmt/.mtr/.mtl…). */
42
+ /** Find a mesh texture using the engine's rules (.vmt/.mtr/.mtl materials…). */
43
43
  resolveTexture?(mesh: Mesh, model: Model, lib: TextureLibrary): Promise<TextureSource | null>;
44
44
  /**
45
- * Карты меша по правилам движка, если они не лежат отдельными файлами нормалей/PBR (Metro: «_bump» — высота и блик,
46
- * нормали считаются из высоты). Возвращает готовые картинки.
45
+ * Mesh maps by the engine's rules when they are not separate normal/PBR files (Metro: "_bump" holds height and specular,
46
+ * normals are computed from height). Returns ready images.
47
47
  */
48
48
  resolveMaps?(mesh: Mesh, model: Model, lib: TextureLibrary): Promise<EngineMaps | null>;
49
- /** Подгрузка сопутствующих файлов после загрузки (одноимённый .txd, анимации из папки…). */
49
+ /** Load companion files after loading (a same-named .txd, animations from the folder…). */
50
50
  afterLoad?(ctx: AfterLoadContext): Promise<void>;
51
51
  }
52
- /** Карты, собранные движком: нормали (касательное пространство, Y как в DirectX) и PBR-поверхность (R AO, G шероховатость, B металл, A блик). */
52
+ /** Maps assembled by the engine: normals (tangent space, Y as in DirectX) and the PBR surface (R AO, G roughness, B metal, A specular). */
53
53
  export interface EngineMaps {
54
54
  normal?: {
55
55
  image: DecodedImage;
@@ -66,17 +66,17 @@ export interface ArchivePlugin {
66
66
  title: string;
67
67
  engine: string;
68
68
  extensions: string[];
69
- /** Расширения файлов-спутников (.dir для IMG, части VPK), которые не открываются сами по себе. */
69
+ /** Extensions of companion files (.dir for IMG, VPK parts) that are not opened on their own. */
70
70
  companionExtensions?: string[];
71
- /** Открывать такие архивы автоматически, когда выбрана «Папка игры» (F.E.A.R. Online: десятки *.arch01 рядом с Game/). */
71
+ /** Open such archives automatically when a "Game folder" is chosen (F.E.A.R. Online: dozens of *.arch01 next to Game/). */
72
72
  openFromFolder?: boolean | ((head: Uint8Array, name: string) => boolean);
73
73
  sniff?(head: Uint8Array, name: string): number;
74
- /** null — файл является частью другого архива (например, *_001.vpk) и пропускается. */
74
+ /** null: the file is part of another archive (for example *_001.vpk) and is skipped. */
75
75
  open(file: BlobFile, siblings: readonly BlobFile[]): Promise<Archive | null>;
76
76
  }
77
77
  /**
78
- * Поиск текстуры по имени материала через файлы материалов движка (.mtr, .mat…), независимо от формата модели.
79
- * Срабатывает, когда собственный поиск формата ничего не нашёл: например, .lwo/.ase из Doom 3 через assimp + .mtr из pk4.
78
+ * Texture lookup by material name through the engine's material files (.mtr, .mat…), regardless of the model format.
79
+ * Used when the format's own lookup found nothing: for example .lwo/.ase from Doom 3 via assimp + .mtr from pk4.
80
80
  */
81
81
  export interface MaterialResolver {
82
82
  id: string;
@@ -93,49 +93,49 @@ export interface TextureDictionaryResult {
93
93
  }>;
94
94
  errors: string[];
95
95
  }
96
- /** Файл-словарь текстур (RenderWare .txd). */
96
+ /** Texture dictionary file (RenderWare .txd). */
97
97
  export interface TextureDictionaryPlugin {
98
98
  id: string;
99
99
  extensions: string[];
100
100
  parse(buf: ArrayBuffer): TextureDictionaryResult | Promise<TextureDictionaryResult>;
101
101
  }
102
- /** Настройки интерфейса для пресета игры. */
102
+ /** UI settings for a game preset. */
103
103
  export interface PresetUi {
104
- /** Показать кнопку «Открыть архив…». */
104
+ /** Show the "Open archive…" button. */
105
105
  archives?: boolean;
106
106
  archiveExts?: string[];
107
107
  archiveLabel?: string;
108
- /** Кнопка внешней анимации. */
108
+ /** External animation button. */
109
109
  motionLabel?: string;
110
- /** Внешняя анимация имеет то же расширение, что и модель (.cast). */
110
+ /** The external animation has the same extension as the model (.cast). */
111
111
  motionAny?: boolean;
112
- /** Переключатель «Z вверх». */
112
+ /** "Z up" toggle. */
113
113
  zUp?: boolean;
114
- /** Переключатель «инвертировать вращение». */
114
+ /** "Invert rotation" toggle. */
115
115
  invertQuat?: boolean;
116
116
  }
117
117
  export interface GamePreset {
118
118
  id: string;
119
- /** Группа в списке: «Игры», «Форматы (assimp)»… */
119
+ /** Group in the list: "Games", "Formats (assimp)"… */
120
120
  group: string;
121
121
  title: string;
122
122
  engine: string;
123
- /** id форматов, которые открывает пресет. */
123
+ /** Ids of the formats the preset opens. */
124
124
  formats: string[];
125
- /** Ограничить расширения моделей (по умолчанию — все расширения форматов). */
125
+ /** Restrict model extensions (default: all format extensions). */
126
126
  extensions?: string[];
127
127
  hint?: string;
128
128
  status?: SupportStatus;
129
129
  ui?: PresetUi;
130
- /** Файлы-маркеры игры для автоопределения по папке. */
130
+ /** Game marker files for detection by folder. */
131
131
  markers?: string[];
132
132
  }
133
- /** Поле настроек экспорта (рисуется в окне экспорта). */
133
+ /** Export settings field (drawn in the export dialog). */
134
134
  export interface ExportField {
135
135
  id: string;
136
136
  label: string;
137
137
  type: 'text' | 'number' | 'checkbox' | 'select';
138
- /** Значение по умолчанию; функция — по модели и имени файла. */
138
+ /** Default value; a function receives the model and file name. */
139
139
  default?: string | number | boolean | ((model: Model, baseName: string) => string | number | boolean);
140
140
  placeholder?: string;
141
141
  hint?: string;
@@ -145,47 +145,47 @@ export interface ExportField {
145
145
  }>;
146
146
  min?: number;
147
147
  step?: number;
148
- /** Показывать поле только для таких моделей (например, «объекты ссылками» — у сцен с экземплярами). */
148
+ /** Show the field only for such models (for example "objects as references" for scenes with instances). */
149
149
  when?(model: Model): boolean;
150
150
  }
151
151
  export type ExportValues = Record<string, string | number | boolean>;
152
- /** Текстура меша для писателя: исходный DDS (копируется как есть) или декодированная картинка. */
152
+ /** Mesh texture for a writer: the source DDS (copied as is) or a decoded image. */
153
153
  export interface ExportTexture {
154
154
  name: string;
155
155
  dds?: Uint8Array;
156
156
  image?: DecodedImage | null;
157
157
  }
158
158
  export interface ExportContext {
159
- /** Пресет и движок, из которых открыта модель. */
159
+ /** Preset and engine the model was opened from. */
160
160
  preset: GamePreset;
161
161
  sourceEngine: string;
162
- /** Имя исходного файла без расширения. */
162
+ /** Source file name without extension. */
163
163
  baseName: string;
164
- /** Скрытые пользователем меши. */
164
+ /** Meshes hidden by the user. */
165
165
  hidden: ReadonlySet<number>;
166
166
  texture(meshIndex: number): Promise<ExportTexture | null>;
167
167
  }
168
168
  export interface ExportResult {
169
169
  files: Map<string, Uint8Array>;
170
170
  report: string[];
171
- /** Имя архива для скачивания (без .zip). */
171
+ /** Name of the archive to download (without .zip). */
172
172
  name: string;
173
173
  }
174
- /** Куда можно выгрузить загруженную модель: целевой движок/формат и его настройки. */
174
+ /** Where a loaded model can be exported: the target engine/format and its settings. */
175
175
  export interface ExporterPlugin {
176
176
  id: string;
177
- /** Короткое название («X-Ray SDK 0.7 (ЗП)»). */
177
+ /** Short name ("X-Ray SDK 0.7 (CoP)"). */
178
178
  title: string;
179
- /** Что получится и как этим пользоваться. */
179
+ /** What you get and how to use it. */
180
180
  description: string;
181
- /** Целевой движок (id) или формат («obj»). */
181
+ /** Target engine (id) or format ("obj"). */
182
182
  target: string;
183
183
  /**
184
- * Движки-источники, для которых выгрузка предназначена; не задано — подходит любая модель.
185
- * Совпадение с движком открытой модели поднимает вариант в начало списка.
184
+ * Source engines the export is intended for; not set: any model fits.
185
+ * A match with the open model's engine moves the option to the top of the list.
186
186
  */
187
187
  from?: string[];
188
- /** Только из перечисленных движков (иначе вариант не показывается). */
188
+ /** Only from the listed engines (otherwise the option is not shown). */
189
189
  onlyFrom?: boolean;
190
190
  fields: ExportField[];
191
191
  accepts?(model: Model): boolean;
@@ -195,23 +195,23 @@ export interface EnginePlugin {
195
195
  id: string;
196
196
  title: string;
197
197
  formats?: FormatPlugin[];
198
- /** Выгрузка в формат этого движка. */
198
+ /** Export to this engine's format. */
199
199
  exporters?: ExporterPlugin[];
200
200
  archives?: ArchivePlugin[];
201
201
  codecs?: ImageCodec[];
202
202
  dictionaries?: TextureDictionaryPlugin[];
203
203
  materials?: MaterialResolver[];
204
204
  games?: GamePreset[];
205
- /** Направление зелёного канала карт нормалей движка: 'gl' (Y вверх: id Tech, OpenGL-игры) или 'dx' (по умолчанию). */
205
+ /** Green channel direction of the engine's normal maps: 'gl' (Y up: id Tech, OpenGL games) or 'dx' (default). */
206
206
  normalY?: 'gl' | 'dx';
207
- /** Тексты на других языках (русские — в полях самих плагинов). Реестр подключает их вместе с движком. */
207
+ /** Texts in other languages (Russian ones are in the plugins' own fields). The registry attaches them with the engine. */
208
208
  i18n?: {
209
209
  en?: EngineTexts;
210
210
  };
211
- /** Срез движка по играм (pickGames): полный движок, из которого он получен. */
211
+ /** Engine slice by games (pickGames): the full engine it was taken from. */
212
212
  base?: EnginePlugin;
213
213
  }
214
- /** Тексты движка на одном языке: название, группы пресетов, пресеты и варианты выгрузки по id. */
214
+ /** Engine texts in one language: name, preset groups, presets and export options by id. */
215
215
  export interface EngineTexts {
216
216
  title?: string;
217
217
  groups?: Record<string, string>;
@@ -231,11 +231,11 @@ export interface EngineTexts {
231
231
  }>;
232
232
  }>;
233
233
  }
234
- /** Помощник для объявления движка с проверкой типов. */
234
+ /** Helper for declaring an engine with type checking. */
235
235
  export declare const defineEngine: (e: EnginePlugin) => EnginePlugin;
236
236
  /**
237
- * Движок, ограниченный выбранными играми: их пресеты и форматы, на которые они ссылаются (архивы, кодеки, словари,
238
- * материалы и выгрузка — все). Так устроены подпути пакетов движков (`@gmvjs/xray-v1/cop`). Два среза одного движка
239
- * в одном реестре объединяются.
237
+ * An engine limited to the chosen games: their presets and the formats they reference (archives, codecs, dictionaries,
238
+ * materials and export: all of them). This is how engine package subpaths work (`@gmvjs/xray-v1/cop`). Two slices of one engine
239
+ * in the same registry are merged.
240
240
  */
241
241
  export declare function pickGames(engine: EnginePlugin, ...ids: string[]): EnginePlugin;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sources":["../src/plugin.ts"],"sourcesContent":["// Интерфейс расширения: движок = набор плагинов форматов, архивов, кодеков текстур и пресетов игр.\nimport type { Archive, BlobFile, DecodedImage, Mesh, Model, Motion, VFile } from './types';\nimport type { TextureLibrary, TextureSource } from './textures';\n\n/** Степень готовности (для значка в интерфейсе). */\nexport type SupportStatus = 'stable' | 'beta' | 'bridge';\n\nexport interface LoadContext {\n /** Все файлы, выбранные вместе с основным (.mtl, .bin, .vvd…). */\n readonly files: readonly VFile[];\n /** Открытые архивы и папки. */\n readonly archives: readonly Archive[];\n /** Найти соседний файл: среди выбранных, затем в той же папке архива. */\n sibling(name: string): Promise<ArrayBuffer | null>;\n /** Прочитать файл архива по полному пути (первый найденный). */\n readPath(path: string): Promise<Uint8Array | null>;\n /** Значения Model.options, выбранные пользователем (повторная загрузка с другими дисками, винилом…). */\n readonly options?: Readonly<Record<string, string>>;\n}\n\nexport interface AfterLoadContext {\n readonly main: VFile;\n readonly model: Model;\n readonly textures: TextureLibrary;\n addMotions(list: Motion[], source: string): void;\n addTextureDictionary(file: VFile): Promise<void>;\n}\n\nexport interface FormatPlugin {\n /** Уникальный id: '<движок>-<формат>', например 'xray-ogf'. */\n id: string;\n title: string;\n engine: string;\n /** Расширения без точки, нижний регистр. */\n extensions: string[];\n /** Файлы без расширения, которые открывает формат (по пути в архиве/папке), например «media/meshcache/…». */\n pathPattern?: RegExp;\n /** Расширения внешних анимаций, которые принимает модель этого формата. */\n motionExtensions?: string[];\n status?: SupportStatus;\n /**\n * Уверенность 0..1 по первым 256 байтам файла. 0 — точно не этот формат.\n * Не задано — формат определяется только по расширению.\n */\n sniff?(head: Uint8Array, name: string): number;\n load(main: VFile, ctx: LoadContext): Promise<Model>;\n /** Поиск текстуры меша по правилам движка (материалы .vmt/.mtr/.mtl…). */\n resolveTexture?(mesh: Mesh, model: Model, lib: TextureLibrary): Promise<TextureSource | null>;\n /**\n * Карты меша по правилам движка, если они не лежат отдельными файлами нормалей/PBR (Metro: «_bump» — высота и блик,\n * нормали считаются из высоты). Возвращает готовые картинки.\n */\n resolveMaps?(mesh: Mesh, model: Model, lib: TextureLibrary): Promise<EngineMaps | null>;\n /** Подгрузка сопутствующих файлов после загрузки (одноимённый .txd, анимации из папки…). */\n afterLoad?(ctx: AfterLoadContext): Promise<void>;\n}\n\n/** Карты, собранные движком: нормали (касательное пространство, Y как в DirectX) и PBR-поверхность (R AO, G шероховатость, B металл, A блик). */\nexport interface EngineMaps {\n normal?: { image: DecodedImage; packing?: 'rgb' | 'rg' | 'ag'; label?: string };\n surface?: { image: DecodedImage; label?: string };\n}\n\nexport interface ArchivePlugin {\n id: string;\n title: string;\n engine: string;\n extensions: string[];\n /** Расширения файлов-спутников (.dir для IMG, части VPK), которые не открываются сами по себе. */\n companionExtensions?: string[];\n /** Открывать такие архивы автоматически, когда выбрана «Папка игры» (F.E.A.R. Online: десятки *.arch01 рядом с Game/). */\n openFromFolder?: boolean | ((head: Uint8Array, name: string) => boolean);\n sniff?(head: Uint8Array, name: string): number;\n /** null — файл является частью другого архива (например, *_001.vpk) и пропускается. */\n open(file: BlobFile, siblings: readonly BlobFile[]): Promise<Archive | null>;\n}\n\n/**\n * Поиск текстуры по имени материала через файлы материалов движка (.mtr, .mat…), независимо от формата модели.\n * Срабатывает, когда собственный поиск формата ничего не нашёл: например, .lwo/.ase из Doom 3 через assimp + .mtr из pk4.\n */\nexport interface MaterialResolver {\n id: string;\n resolve(mesh: Mesh, lib: TextureLibrary): Promise<TextureSource | null>;\n}\n\nexport interface ImageCodec {\n id: string;\n extensions: string[];\n decode(buf: ArrayBuffer, name: string): DecodedImage | Promise<DecodedImage>;\n}\n\nexport interface TextureDictionaryResult {\n textures: Array<DecodedImage & { name: string }>;\n errors: string[];\n}\n/** Файл-словарь текстур (RenderWare .txd). */\nexport interface TextureDictionaryPlugin {\n id: string;\n extensions: string[];\n parse(buf: ArrayBuffer): TextureDictionaryResult | Promise<TextureDictionaryResult>;\n}\n\n/** Настройки интерфейса для пресета игры. */\nexport interface PresetUi {\n /** Показать кнопку «Открыть архив…». */\n archives?: boolean;\n archiveExts?: string[];\n archiveLabel?: string;\n /** Кнопка внешней анимации. */\n motionLabel?: string;\n /** Внешняя анимация имеет то же расширение, что и модель (.cast). */\n motionAny?: boolean;\n /** Переключатель «Z вверх». */\n zUp?: boolean;\n /** Переключатель «инвертировать вращение». */\n invertQuat?: boolean;\n}\n\nexport interface GamePreset {\n id: string;\n /** Группа в списке: «Игры», «Форматы (assimp)»… */\n group: string;\n title: string;\n engine: string;\n /** id форматов, которые открывает пресет. */\n formats: string[];\n /** Ограничить расширения моделей (по умолчанию — все расширения форматов). */\n extensions?: string[];\n hint?: string;\n status?: SupportStatus;\n ui?: PresetUi;\n /** Файлы-маркеры игры для автоопределения по папке. */\n markers?: string[];\n}\n\n/** Поле настроек экспорта (рисуется в окне экспорта). */\nexport interface ExportField {\n id: string;\n label: string;\n type: 'text' | 'number' | 'checkbox' | 'select';\n /** Значение по умолчанию; функция — по модели и имени файла. */\n default?: string | number | boolean | ((model: Model, baseName: string) => string | number | boolean);\n placeholder?: string;\n hint?: string;\n options?: Array<{ value: string; label: string }>;\n min?: number;\n step?: number;\n /** Показывать поле только для таких моделей (например, «объекты ссылками» — у сцен с экземплярами). */\n when?(model: Model): boolean;\n}\nexport type ExportValues = Record<string, string | number | boolean>;\n\n/** Текстура меша для писателя: исходный DDS (копируется как есть) или декодированная картинка. */\nexport interface ExportTexture { name: string; dds?: Uint8Array; image?: DecodedImage | null }\n\nexport interface ExportContext {\n /** Пресет и движок, из которых открыта модель. */\n preset: GamePreset;\n sourceEngine: string;\n /** Имя исходного файла без расширения. */\n baseName: string;\n /** Скрытые пользователем меши. */\n hidden: ReadonlySet<number>;\n texture(meshIndex: number): Promise<ExportTexture | null>;\n}\nexport interface ExportResult {\n files: Map<string, Uint8Array>;\n report: string[];\n /** Имя архива для скачивания (без .zip). */\n name: string;\n}\n\n/** Куда можно выгрузить загруженную модель: целевой движок/формат и его настройки. */\nexport interface ExporterPlugin {\n id: string;\n /** Короткое название («X-Ray SDK 0.7 (ЗП)»). */\n title: string;\n /** Что получится и как этим пользоваться. */\n description: string;\n /** Целевой движок (id) или формат («obj»). */\n target: string;\n /**\n * Движки-источники, для которых выгрузка предназначена; не задано — подходит любая модель.\n * Совпадение с движком открытой модели поднимает вариант в начало списка.\n */\n from?: string[];\n /** Только из перечисленных движков (иначе вариант не показывается). */\n onlyFrom?: boolean;\n fields: ExportField[];\n accepts?(model: Model): boolean;\n run(model: Model, values: ExportValues, ctx: ExportContext): Promise<ExportResult>;\n}\n\nexport interface EnginePlugin {\n id: string;\n title: string;\n formats?: FormatPlugin[];\n /** Выгрузка в формат этого движка. */\n exporters?: ExporterPlugin[];\n archives?: ArchivePlugin[];\n codecs?: ImageCodec[];\n dictionaries?: TextureDictionaryPlugin[];\n materials?: MaterialResolver[];\n games?: GamePreset[];\n /** Направление зелёного канала карт нормалей движка: 'gl' (Y вверх: id Tech, OpenGL-игры) или 'dx' (по умолчанию). */\n normalY?: 'gl' | 'dx';\n /** Тексты на других языках (русские — в полях самих плагинов). Реестр подключает их вместе с движком. */\n i18n?: { en?: EngineTexts };\n /** Срез движка по играм (pickGames): полный движок, из которого он получен. */\n base?: EnginePlugin;\n}\n\n/** Тексты движка на одном языке: название, группы пресетов, пресеты и варианты выгрузки по id. */\nexport interface EngineTexts {\n title?: string;\n groups?: Record<string, string>;\n presets?: Record<string, { title?: string; hint?: string; archiveLabel?: string; motionLabel?: string }>;\n exporters?: Record<string, { title?: string; description?: string; fields?: Record<string, { label?: string; hint?: string; placeholder?: string }> }>;\n}\n\n/** Помощник для объявления движка с проверкой типов. */\nexport const defineEngine = (e: EnginePlugin): EnginePlugin => e;\n\n/**\n * Движок, ограниченный выбранными играми: их пресеты и форматы, на которые они ссылаются (архивы, кодеки, словари,\n * материалы и выгрузка — все). Так устроены подпути пакетов движков (`@gmvjs/xray-v1/cop`). Два среза одного движка\n * в одном реестре объединяются.\n */\nexport function pickGames(engine: EnginePlugin, ...ids: string[]): EnginePlugin {\n const base = engine.base ?? engine;\n const games = (base.games ?? []).filter((g) => ids.includes(g.id));\n const missing = ids.filter((id) => !games.some((g) => g.id === id));\n if (missing.length) throw new Error(`gmv: в движке ${base.id} нет пресетов ${missing.join(', ')}`);\n const used = new Set(games.flatMap((g) => g.formats));\n return { ...base, base, games, formats: (base.formats ?? []).filter((f) => used.has(f.id)) };\n}\n"],"names":[],"mappings":"AA8NO,MAAM,eAAe,CAAC,MAAkC;AAOxD,SAAS,UAAU,WAAyB,KAA6B;AAC9E,QAAM,OAAO,OAAO,QAAQ;AAC5B,QAAM,SAAS,KAAK,SAAS,CAAA,GAAI,OAAO,CAAC,MAAM,IAAI,SAAS,EAAE,EAAE,CAAC;AACjE,QAAM,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;AAClE,MAAI,QAAQ,OAAQ,OAAM,IAAI,MAAM,iBAAiB,KAAK,EAAE,iBAAiB,QAAQ,KAAK,IAAI,CAAC,EAAE;AACjG,QAAM,OAAO,IAAI,IAAI,MAAM,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;AACpD,SAAO,EAAE,GAAG,MAAM,MAAM,OAAO,UAAU,KAAK,WAAW,CAAA,GAAI,OAAO,CAAC,MAAM,KAAK,IAAI,EAAE,EAAE,CAAC,EAAA;AAC3F;"}
1
+ {"version":3,"file":"plugin.js","sources":["../src/plugin.ts"],"sourcesContent":["// Extension interface: an engine is a set of plugins for formats, archives, texture codecs and game presets.\nimport type { Archive, BlobFile, DecodedImage, Mesh, Model, Motion, VFile } from './types';\nimport type { TextureLibrary, TextureSource } from './textures';\n\n/** Readiness level (for the badge in the UI). */\nexport type SupportStatus = 'stable' | 'beta' | 'bridge';\n\nexport interface LoadContext {\n /** All files selected together with the main one (.mtl, .bin, .vvd…). */\n readonly files: readonly VFile[];\n /** Open archives and folders. */\n readonly archives: readonly Archive[];\n /** Find a neighbouring file: among the selected ones, then in the same archive folder. */\n sibling(name: string): Promise<ArrayBuffer | null>;\n /** Read an archive file by full path (first match). */\n readPath(path: string): Promise<Uint8Array | null>;\n /** Model.options values chosen by the user (reloading with other wheels, vinyl…). */\n readonly options?: Readonly<Record<string, string>>;\n}\n\nexport interface AfterLoadContext {\n readonly main: VFile;\n readonly model: Model;\n readonly textures: TextureLibrary;\n addMotions(list: Motion[], source: string): void;\n addTextureDictionary(file: VFile): Promise<void>;\n}\n\nexport interface FormatPlugin {\n /** Unique id: '<engine>-<format>', for example 'xray-ogf'. */\n id: string;\n title: string;\n engine: string;\n /** Extensions without the dot, lower case. */\n extensions: string[];\n /** Extensionless files the format opens (by path in an archive/folder), for example \"media/meshcache/…\". */\n pathPattern?: RegExp;\n /** Extensions of external animations a model of this format accepts. */\n motionExtensions?: string[];\n status?: SupportStatus;\n /**\n * Confidence 0..1 from the first 256 bytes of the file. 0 means definitely not this format.\n * Not set: the format is detected by extension only.\n */\n sniff?(head: Uint8Array, name: string): number;\n load(main: VFile, ctx: LoadContext): Promise<Model>;\n /** Find a mesh texture using the engine's rules (.vmt/.mtr/.mtl materials…). */\n resolveTexture?(mesh: Mesh, model: Model, lib: TextureLibrary): Promise<TextureSource | null>;\n /**\n * Mesh maps by the engine's rules when they are not separate normal/PBR files (Metro: \"_bump\" holds height and specular,\n * normals are computed from height). Returns ready images.\n */\n resolveMaps?(mesh: Mesh, model: Model, lib: TextureLibrary): Promise<EngineMaps | null>;\n /** Load companion files after loading (a same-named .txd, animations from the folder…). */\n afterLoad?(ctx: AfterLoadContext): Promise<void>;\n}\n\n/** Maps assembled by the engine: normals (tangent space, Y as in DirectX) and the PBR surface (R AO, G roughness, B metal, A specular). */\nexport interface EngineMaps {\n normal?: { image: DecodedImage; packing?: 'rgb' | 'rg' | 'ag'; label?: string };\n surface?: { image: DecodedImage; label?: string };\n}\n\nexport interface ArchivePlugin {\n id: string;\n title: string;\n engine: string;\n extensions: string[];\n /** Extensions of companion files (.dir for IMG, VPK parts) that are not opened on their own. */\n companionExtensions?: string[];\n /** Open such archives automatically when a \"Game folder\" is chosen (F.E.A.R. Online: dozens of *.arch01 next to Game/). */\n openFromFolder?: boolean | ((head: Uint8Array, name: string) => boolean);\n sniff?(head: Uint8Array, name: string): number;\n /** null: the file is part of another archive (for example *_001.vpk) and is skipped. */\n open(file: BlobFile, siblings: readonly BlobFile[]): Promise<Archive | null>;\n}\n\n/**\n * Texture lookup by material name through the engine's material files (.mtr, .mat…), regardless of the model format.\n * Used when the format's own lookup found nothing: for example .lwo/.ase from Doom 3 via assimp + .mtr from pk4.\n */\nexport interface MaterialResolver {\n id: string;\n resolve(mesh: Mesh, lib: TextureLibrary): Promise<TextureSource | null>;\n}\n\nexport interface ImageCodec {\n id: string;\n extensions: string[];\n decode(buf: ArrayBuffer, name: string): DecodedImage | Promise<DecodedImage>;\n}\n\nexport interface TextureDictionaryResult {\n textures: Array<DecodedImage & { name: string }>;\n errors: string[];\n}\n/** Texture dictionary file (RenderWare .txd). */\nexport interface TextureDictionaryPlugin {\n id: string;\n extensions: string[];\n parse(buf: ArrayBuffer): TextureDictionaryResult | Promise<TextureDictionaryResult>;\n}\n\n/** UI settings for a game preset. */\nexport interface PresetUi {\n /** Show the \"Open archive…\" button. */\n archives?: boolean;\n archiveExts?: string[];\n archiveLabel?: string;\n /** External animation button. */\n motionLabel?: string;\n /** The external animation has the same extension as the model (.cast). */\n motionAny?: boolean;\n /** \"Z up\" toggle. */\n zUp?: boolean;\n /** \"Invert rotation\" toggle. */\n invertQuat?: boolean;\n}\n\nexport interface GamePreset {\n id: string;\n /** Group in the list: \"Games\", \"Formats (assimp)\"… */\n group: string;\n title: string;\n engine: string;\n /** Ids of the formats the preset opens. */\n formats: string[];\n /** Restrict model extensions (default: all format extensions). */\n extensions?: string[];\n hint?: string;\n status?: SupportStatus;\n ui?: PresetUi;\n /** Game marker files for detection by folder. */\n markers?: string[];\n}\n\n/** Export settings field (drawn in the export dialog). */\nexport interface ExportField {\n id: string;\n label: string;\n type: 'text' | 'number' | 'checkbox' | 'select';\n /** Default value; a function receives the model and file name. */\n default?: string | number | boolean | ((model: Model, baseName: string) => string | number | boolean);\n placeholder?: string;\n hint?: string;\n options?: Array<{ value: string; label: string }>;\n min?: number;\n step?: number;\n /** Show the field only for such models (for example \"objects as references\" for scenes with instances). */\n when?(model: Model): boolean;\n}\nexport type ExportValues = Record<string, string | number | boolean>;\n\n/** Mesh texture for a writer: the source DDS (copied as is) or a decoded image. */\nexport interface ExportTexture { name: string; dds?: Uint8Array; image?: DecodedImage | null }\n\nexport interface ExportContext {\n /** Preset and engine the model was opened from. */\n preset: GamePreset;\n sourceEngine: string;\n /** Source file name without extension. */\n baseName: string;\n /** Meshes hidden by the user. */\n hidden: ReadonlySet<number>;\n texture(meshIndex: number): Promise<ExportTexture | null>;\n}\nexport interface ExportResult {\n files: Map<string, Uint8Array>;\n report: string[];\n /** Name of the archive to download (without .zip). */\n name: string;\n}\n\n/** Where a loaded model can be exported: the target engine/format and its settings. */\nexport interface ExporterPlugin {\n id: string;\n /** Short name (\"X-Ray SDK 0.7 (CoP)\"). */\n title: string;\n /** What you get and how to use it. */\n description: string;\n /** Target engine (id) or format (\"obj\"). */\n target: string;\n /**\n * Source engines the export is intended for; not set: any model fits.\n * A match with the open model's engine moves the option to the top of the list.\n */\n from?: string[];\n /** Only from the listed engines (otherwise the option is not shown). */\n onlyFrom?: boolean;\n fields: ExportField[];\n accepts?(model: Model): boolean;\n run(model: Model, values: ExportValues, ctx: ExportContext): Promise<ExportResult>;\n}\n\nexport interface EnginePlugin {\n id: string;\n title: string;\n formats?: FormatPlugin[];\n /** Export to this engine's format. */\n exporters?: ExporterPlugin[];\n archives?: ArchivePlugin[];\n codecs?: ImageCodec[];\n dictionaries?: TextureDictionaryPlugin[];\n materials?: MaterialResolver[];\n games?: GamePreset[];\n /** Green channel direction of the engine's normal maps: 'gl' (Y up: id Tech, OpenGL games) or 'dx' (default). */\n normalY?: 'gl' | 'dx';\n /** Texts in other languages (Russian ones are in the plugins' own fields). The registry attaches them with the engine. */\n i18n?: { en?: EngineTexts };\n /** Engine slice by games (pickGames): the full engine it was taken from. */\n base?: EnginePlugin;\n}\n\n/** Engine texts in one language: name, preset groups, presets and export options by id. */\nexport interface EngineTexts {\n title?: string;\n groups?: Record<string, string>;\n presets?: Record<string, { title?: string; hint?: string; archiveLabel?: string; motionLabel?: string }>;\n exporters?: Record<string, { title?: string; description?: string; fields?: Record<string, { label?: string; hint?: string; placeholder?: string }> }>;\n}\n\n/** Helper for declaring an engine with type checking. */\nexport const defineEngine = (e: EnginePlugin): EnginePlugin => e;\n\n/**\n * An engine limited to the chosen games: their presets and the formats they reference (archives, codecs, dictionaries,\n * materials and export: all of them). This is how engine package subpaths work (`@gmvjs/xray-v1/cop`). Two slices of one engine\n * in the same registry are merged.\n */\nexport function pickGames(engine: EnginePlugin, ...ids: string[]): EnginePlugin {\n const base = engine.base ?? engine;\n const games = (base.games ?? []).filter((g) => ids.includes(g.id));\n const missing = ids.filter((id) => !games.some((g) => g.id === id));\n if (missing.length) throw new Error(`gmv: в движке ${base.id} нет пресетов ${missing.join(', ')}`);\n const used = new Set(games.flatMap((g) => g.formats));\n return { ...base, base, games, formats: (base.formats ?? []).filter((f) => used.has(f.id)) };\n}\n"],"names":[],"mappings":"AA8NO,MAAM,eAAe,CAAC,MAAkC;AAOxD,SAAS,UAAU,WAAyB,KAA6B;AAC9E,QAAM,OAAO,OAAO,QAAQ;AAC5B,QAAM,SAAS,KAAK,SAAS,CAAA,GAAI,OAAO,CAAC,MAAM,IAAI,SAAS,EAAE,EAAE,CAAC;AACjE,QAAM,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;AAClE,MAAI,QAAQ,OAAQ,OAAM,IAAI,MAAM,iBAAiB,KAAK,EAAE,iBAAiB,QAAQ,KAAK,IAAI,CAAC,EAAE;AACjG,QAAM,OAAO,IAAI,IAAI,MAAM,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;AACpD,SAAO,EAAE,GAAG,MAAM,MAAM,OAAO,UAAU,KAAK,WAAW,CAAA,GAAI,OAAO,CAAC,MAAM,KAAK,IAAI,EAAE,EAAE,CAAC,EAAA;AAC3F;"}
@@ -5,7 +5,7 @@ export interface Detection {
5
5
  format: FormatPlugin;
6
6
  /** 0..1 */
7
7
  confidence: number;
8
- /** Все кандидаты по убыванию уверенности (для выбора из 2–3 вариантов). */
8
+ /** All candidates by descending confidence (for choosing among 2–3 options). */
9
9
  candidates: Array<{
10
10
  format: FormatPlugin;
11
11
  confidence: number;
@@ -21,10 +21,10 @@ export declare class Registry implements ImageDecoder {
21
21
  readonly materials: MaterialResolver[];
22
22
  readonly exporters: ExporterPlugin[];
23
23
  constructor(engines?: readonly EnginePlugin[]);
24
- /** Подключить движок. Повторное подключение с тем же id заменяет плагины. */
24
+ /** Attach an engine. Attaching again with the same id replaces its plugins. */
25
25
  use(engine: EnginePlugin): this;
26
26
  remove(engineId: string): void;
27
- /** Варианты выгрузки модели, открытой из движка sourceEngine: сначала предназначенные для него, затем общие. */
27
+ /** Export options for a model opened from engine sourceEngine: those intended for it first, then general ones. */
28
28
  exportersFor(model: Model, sourceEngine: string): Array<{
29
29
  exporter: ExporterPlugin;
30
30
  recommended: boolean;
@@ -32,32 +32,32 @@ export declare class Registry implements ImageDecoder {
32
32
  format(id: string): FormatPlugin | undefined;
33
33
  game(id: string): GamePreset | undefined;
34
34
  engine(id: string): EnginePlugin | undefined;
35
- /** Есть ли в реестре хотя бы один формат пресета (мост через assimp без @gmvjs/assimp — нет). */
35
+ /** Whether the registry has at least one of the preset's formats (an assimp bridge without @gmvjs/assimp does not). */
36
36
  presetAvailable(p: GamePreset): boolean;
37
- /** Расширения моделей пресета. */
37
+ /** Model extensions of the preset. */
38
38
  presetExtensions(p: GamePreset): string[];
39
- /** Расширения внешних анимаций пресета. */
39
+ /** External animation extensions of the preset. */
40
40
  presetMotionExtensions(p: GamePreset): string[];
41
- /** Первый пресет, открывающий формат. */
41
+ /** First preset that opens the format. */
42
42
  presetForFormat(formatId: string, fileName?: string): GamePreset | undefined;
43
- /** Расширения, которые бывают только у внешних анимаций (.omf, .psa, .md5anim), а не у моделей. */
43
+ /** Extensions found only on external animations (.omf, .psa, .md5anim), never on models. */
44
44
  get motionOnlyExtensions(): string[];
45
45
  get modelExtensions(): string[];
46
- /** Модель по шаблону пути (MeshCache без расширения, classes/trucks/*.xml); formats — ограничить форматами пресета. */
46
+ /** Model by path pattern (extensionless MeshCache, classes/trucks/*.xml); formats limits it to the preset's formats. */
47
47
  isPathModel(path: string, formats?: readonly string[]): boolean;
48
48
  archiveExtensions(): string[];
49
49
  isArchive(name: string): boolean;
50
50
  dictionaryFor(name: string): TextureDictionaryPlugin | undefined;
51
51
  codecFor(name: string): ImageCodec | undefined;
52
- /** Картинка ли это (по расширению): кодеки плагинов + форматы браузера. */
52
+ /** Whether this is an image (by extension): plugin codecs + browser formats. */
53
53
  isImage(name: string): boolean;
54
- /** Плагин архива для файла: по расширению, при неоднозначности — по сигнатуре. */
54
+ /** Archive plugin for a file: by extension, by signature when ambiguous. */
55
55
  archiveFor(file: BlobFile): Promise<ArchivePlugin | null>;
56
56
  /**
57
- * Определить формат модели: кандидаты по расширению, уточнение по сигнатуре,
58
- * бонус форматам предпочитаемого пресета. Без расширения — перебор всех сигнатур.
57
+ * Detect the model format: candidates by extension, refined by signature,
58
+ * with a bonus for the preferred preset's formats. Without an extension all signatures are tried.
59
59
  */
60
60
  detect(file: VFile | BlobFile, prefer?: readonly string[]): Promise<Detection | null>;
61
- /** Декодировать картинку: кодеки плагинов, затем createImageBitmap браузера. */
61
+ /** Decode an image: plugin codecs, then the browser's createImageBitmap. */
62
62
  decodeImage(file: VFile): Promise<DecodedImage>;
63
63
  }
package/dist/registry.js CHANGED
@@ -13,7 +13,7 @@ class Registry {
13
13
  constructor(engines = []) {
14
14
  for (const e of engines) this.use(e);
15
15
  }
16
- /** Подключить движок. Повторное подключение с тем же id заменяет плагины. */
16
+ /** Attach an engine. Attaching again with the same id replaces its plugins. */
17
17
  use(engine) {
18
18
  const old = this.engines.findIndex((e) => e.id === engine.id);
19
19
  if (old >= 0) {
@@ -57,7 +57,7 @@ class Registry {
57
57
  drop(this.materials, e.materials);
58
58
  drop(this.exporters, e.exporters);
59
59
  }
60
- /** Варианты выгрузки модели, открытой из движка sourceEngine: сначала предназначенные для него, затем общие. */
60
+ /** Export options for a model opened from engine sourceEngine: those intended for it first, then general ones. */
61
61
  exportersFor(model, sourceEngine) {
62
62
  return this.exporters.filter((x) => (!x.onlyFrom || x.from?.includes(sourceEngine)) && (!x.accepts || x.accepts(model))).map((exporter) => ({ exporter, recommended: !!exporter.from?.includes(sourceEngine) })).sort((a, b) => Number(b.recommended) - Number(a.recommended));
63
63
  }
@@ -70,25 +70,25 @@ class Registry {
70
70
  engine(id) {
71
71
  return this.engines.find((e) => e.id === id);
72
72
  }
73
- /** Есть ли в реестре хотя бы один формат пресета (мост через assimp без @gmvjs/assimp — нет). */
73
+ /** Whether the registry has at least one of the preset's formats (an assimp bridge without @gmvjs/assimp does not). */
74
74
  presetAvailable(p) {
75
75
  return p.formats.some((id) => !!this.format(id));
76
76
  }
77
- /** Расширения моделей пресета. */
77
+ /** Model extensions of the preset. */
78
78
  presetExtensions(p) {
79
79
  if (p.extensions) return p.extensions;
80
80
  return [...new Set(p.formats.flatMap((id) => this.format(id)?.extensions ?? []))];
81
81
  }
82
- /** Расширения внешних анимаций пресета. */
82
+ /** External animation extensions of the preset. */
83
83
  presetMotionExtensions(p) {
84
84
  return [...new Set(p.formats.flatMap((id) => this.format(id)?.motionExtensions ?? []))];
85
85
  }
86
- /** Первый пресет, открывающий формат. */
86
+ /** First preset that opens the format. */
87
87
  presetForFormat(formatId, fileName) {
88
88
  const ext = fileName ? extOf(fileName) : "";
89
89
  return ext && this.games.find((g) => g.formats.includes(formatId) && this.presetExtensions(g).includes(ext)) || this.games.find((g) => g.formats.includes(formatId));
90
90
  }
91
- /** Расширения, которые бывают только у внешних анимаций (.omf, .psa, .md5anim), а не у моделей. */
91
+ /** Extensions found only on external animations (.omf, .psa, .md5anim), never on models. */
92
92
  get motionOnlyExtensions() {
93
93
  const models = new Set(this.formats.flatMap((f) => f.extensions));
94
94
  return [...new Set(this.formats.flatMap((f) => f.motionExtensions ?? []))].filter((e) => !models.has(e));
@@ -96,7 +96,7 @@ class Registry {
96
96
  get modelExtensions() {
97
97
  return [...new Set(this.formats.flatMap((f) => [...f.extensions, ...f.motionExtensions ?? []]))];
98
98
  }
99
- /** Модель по шаблону пути (MeshCache без расширения, classes/trucks/*.xml); formats — ограничить форматами пресета. */
99
+ /** Model by path pattern (extensionless MeshCache, classes/trucks/*.xml); formats limits it to the preset's formats. */
100
100
  isPathModel(path, formats) {
101
101
  return this.formats.some((f) => f.pathPattern?.test(path) && (!formats || formats.includes(f.id)));
102
102
  }
@@ -114,11 +114,11 @@ class Registry {
114
114
  const ext = extOf(name);
115
115
  return this.codecs.find((c) => c.extensions.includes(ext));
116
116
  }
117
- /** Картинка ли это (по расширению): кодеки плагинов + форматы браузера. */
117
+ /** Whether this is an image (by extension): plugin codecs + browser formats. */
118
118
  isImage(name) {
119
119
  return !!this.codecFor(name) || /\.(png|jpe?g|bmp|webp|gif)$/i.test(name);
120
120
  }
121
- /** Плагин архива для файла: по расширению, при неоднозначности — по сигнатуре. */
121
+ /** Archive plugin for a file: by extension, by signature when ambiguous. */
122
122
  async archiveFor(file) {
123
123
  const ext = extOf(file.name), cands = this.archives.filter((a) => a.extensions.includes(ext));
124
124
  if (cands.length <= 1) return cands[0] ?? null;
@@ -134,8 +134,8 @@ class Registry {
134
134
  return score > 0 ? best : cands.find((a) => a.id === "zip") ?? null;
135
135
  }
136
136
  /**
137
- * Определить формат модели: кандидаты по расширению, уточнение по сигнатуре,
138
- * бонус форматам предпочитаемого пресета. Без расширения — перебор всех сигнатур.
137
+ * Detect the model format: candidates by extension, refined by signature,
138
+ * with a bonus for the preferred preset's formats. Without an extension all signatures are tried.
139
139
  */
140
140
  async detect(file, prefer = []) {
141
141
  const ext = extOf(file.name);
@@ -158,7 +158,7 @@ class Registry {
158
158
  if (!scored.length) return null;
159
159
  return { format: scored[0].format, confidence: scored[0].confidence, candidates: scored };
160
160
  }
161
- /** Декодировать картинку: кодеки плагинов, затем createImageBitmap браузера. */
161
+ /** Decode an image: plugin codecs, then the browser's createImageBitmap. */
162
162
  async decodeImage(file) {
163
163
  if (file.decoded) return file.decoded;
164
164
  const buf = await file.arrayBuffer(), codec = this.codecFor(file.name);
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sources":["../src/registry.ts"],"sourcesContent":["// Реестр плагинов: движки -> форматы, архивы, кодеки, словари текстур, пресеты игр. Автоопределение формата.\nimport { extOf, readHead } from './binary';\nimport { pickGames, type ArchivePlugin, type EnginePlugin, type ExporterPlugin, type FormatPlugin, type GamePreset, type ImageCodec, type MaterialResolver, type TextureDictionaryPlugin } from './plugin';\nimport { registerTexts } from './i18n';\nimport type { BlobFile, DecodedImage, Model, VFile } from './types';\nimport type { ImageDecoder } from './textures';\n\nexport interface Detection {\n format: FormatPlugin;\n /** 0..1 */\n confidence: number;\n /** Все кандидаты по убыванию уверенности (для выбора из 2–3 вариантов). */\n candidates: Array<{ format: FormatPlugin; confidence: number }>;\n}\n\nexport class Registry implements ImageDecoder {\n readonly engines: EnginePlugin[] = [];\n readonly formats: FormatPlugin[] = [];\n readonly archives: ArchivePlugin[] = [];\n readonly codecs: ImageCodec[] = [];\n readonly dictionaries: TextureDictionaryPlugin[] = [];\n readonly games: GamePreset[] = [];\n readonly materials: MaterialResolver[] = [];\n readonly exporters: ExporterPlugin[] = [];\n\n constructor(engines: readonly EnginePlugin[] = []) {\n for (const e of engines) this.use(e);\n }\n\n /** Подключить движок. Повторное подключение с тем же id заменяет плагины. */\n use(engine: EnginePlugin): this {\n const old = this.engines.findIndex((e) => e.id === engine.id);\n if (old >= 0) {\n // второй срез того же движка (подпуть другой игры): игры объединяются\n const prev = this.engines[old]!, base = engine.base ?? engine;\n if (prev.base && engine.base && prev.base === engine.base) engine = pickGames(base, ...new Set([...(prev.games ?? []), ...(engine.games ?? [])].map((g) => g.id)));\n this.remove(engine.id);\n }\n registerTexts(engine);\n this.engines.push(engine);\n const add = <T extends { id: string }>(list: T[], items: readonly T[] | undefined, kind: string) => {\n for (const it of items ?? []) {\n if (list.some((x) => x.id === it.id)) throw new Error(`gmv: ${kind} с id «${it.id}» уже зарегистрирован`);\n list.push(it);\n }\n };\n add(this.formats, engine.formats, 'формат');\n add(this.archives, engine.archives, 'архив');\n add(this.codecs, engine.codecs, 'кодек');\n add(this.dictionaries, engine.dictionaries, 'словарь');\n add(this.games, engine.games, 'пресет');\n add(this.materials, engine.materials, 'материалы');\n add(this.exporters, engine.exporters, 'экспорт');\n return this;\n }\n\n remove(engineId: string): void {\n const i = this.engines.findIndex((e) => e.id === engineId);\n if (i < 0) return;\n const e = this.engines[i]!;\n this.engines.splice(i, 1);\n const drop = <T>(list: T[], items: readonly T[] | undefined) => {\n for (const it of items ?? []) { const k = list.indexOf(it); if (k >= 0) list.splice(k, 1); }\n };\n drop(this.formats, e.formats);\n drop(this.archives, e.archives);\n drop(this.codecs, e.codecs);\n drop(this.dictionaries, e.dictionaries);\n drop(this.games, e.games);\n drop(this.materials, e.materials);\n drop(this.exporters, e.exporters);\n }\n\n /** Варианты выгрузки модели, открытой из движка sourceEngine: сначала предназначенные для него, затем общие. */\n exportersFor(model: Model, sourceEngine: string): Array<{ exporter: ExporterPlugin; recommended: boolean }> {\n return this.exporters\n .filter((x) => (!x.onlyFrom || x.from?.includes(sourceEngine)) && (!x.accepts || x.accepts(model)))\n .map((exporter) => ({ exporter, recommended: !!exporter.from?.includes(sourceEngine) }))\n .sort((a, b) => Number(b.recommended) - Number(a.recommended));\n }\n\n format(id: string): FormatPlugin | undefined { return this.formats.find((f) => f.id === id); }\n game(id: string): GamePreset | undefined { return this.games.find((g) => g.id === id); }\n engine(id: string): EnginePlugin | undefined { return this.engines.find((e) => e.id === id); }\n\n /** Есть ли в реестре хотя бы один формат пресета (мост через assimp без @gmvjs/assimp — нет). */\n presetAvailable(p: GamePreset): boolean { return p.formats.some((id) => !!this.format(id)); }\n /** Расширения моделей пресета. */\n presetExtensions(p: GamePreset): string[] {\n if (p.extensions) return p.extensions;\n return [...new Set(p.formats.flatMap((id) => this.format(id)?.extensions ?? []))];\n }\n /** Расширения внешних анимаций пресета. */\n presetMotionExtensions(p: GamePreset): string[] {\n return [...new Set(p.formats.flatMap((id) => this.format(id)?.motionExtensions ?? []))];\n }\n /** Первый пресет, открывающий формат. */\n presetForFormat(formatId: string, fileName?: string): GamePreset | undefined {\n const ext = fileName ? extOf(fileName) : '';\n return (ext && this.games.find((g) => g.formats.includes(formatId) && this.presetExtensions(g).includes(ext))) || this.games.find((g) => g.formats.includes(formatId));\n }\n /** Расширения, которые бывают только у внешних анимаций (.omf, .psa, .md5anim), а не у моделей. */\n get motionOnlyExtensions(): string[] {\n const models = new Set(this.formats.flatMap((f) => f.extensions));\n return [...new Set(this.formats.flatMap((f) => f.motionExtensions ?? []))].filter((e) => !models.has(e));\n }\n get modelExtensions(): string[] {\n return [...new Set(this.formats.flatMap((f) => [...f.extensions, ...(f.motionExtensions ?? [])]))];\n }\n\n /** Модель по шаблону пути (MeshCache без расширения, classes/trucks/*.xml); formats — ограничить форматами пресета. */\n isPathModel(path: string, formats?: readonly string[]): boolean {\n return this.formats.some((f) => f.pathPattern?.test(path) && (!formats || formats.includes(f.id)));\n }\n\n archiveExtensions(): string[] {\n return [...new Set(this.archives.flatMap((a) => [...a.extensions, ...(a.companionExtensions ?? [])]))];\n }\n isArchive(name: string): boolean { return this.archiveExtensions().includes(extOf(name)); }\n dictionaryFor(name: string): TextureDictionaryPlugin | undefined {\n const ext = extOf(name);\n return this.dictionaries.find((d) => d.extensions.includes(ext));\n }\n codecFor(name: string): ImageCodec | undefined {\n const ext = extOf(name);\n return this.codecs.find((c) => c.extensions.includes(ext));\n }\n /** Картинка ли это (по расширению): кодеки плагинов + форматы браузера. */\n isImage(name: string): boolean {\n return !!this.codecFor(name) || /\\.(png|jpe?g|bmp|webp|gif)$/i.test(name);\n }\n\n /** Плагин архива для файла: по расширению, при неоднозначности — по сигнатуре. */\n async archiveFor(file: BlobFile): Promise<ArchivePlugin | null> {\n const ext = extOf(file.name), cands = this.archives.filter((a) => a.extensions.includes(ext));\n if (cands.length <= 1) return cands[0] ?? null;\n const head = await readHead(file, 64);\n let best: ArchivePlugin | null = null, score = -1;\n for (const a of cands) {\n const s = a.sniff ? a.sniff(head, file.name) : 0.1;\n if (s > score) { score = s; best = a; }\n }\n // сигнатура не узнана: ZIP читается с конца файла (каталог), поэтому он — запасной вариант, как в v11\n return score > 0 ? best : cands.find((a) => a.id === 'zip') ?? null;\n }\n\n /**\n * Определить формат модели: кандидаты по расширению, уточнение по сигнатуре,\n * бонус форматам предпочитаемого пресета. Без расширения — перебор всех сигнатур.\n */\n async detect(file: VFile | BlobFile, prefer: readonly string[] = []): Promise<Detection | null> {\n const ext = extOf(file.name);\n let cands = this.formats.filter((f) => f.extensions.includes(ext));\n const byMagicOnly = !cands.length;\n if (byMagicOnly) cands = this.formats.filter((f) => f.sniff);\n if (!cands.length) return null;\n // сигнатуру проверяем всегда, когда кандидат её умеет: иначе текстовый Doom 3 .skin уходит в CryEngine .skin\n const needHead = byMagicOnly || cands.some((c) => c.sniff);\n const head = needHead ? await readHead(file, 256) : null;\n const scored = cands\n .map((format) => {\n let c = byMagicOnly ? 0 : 0.5;\n if (head && format.sniff) {\n const s = format.sniff(head, file.name);\n if (s <= 0) return { format, confidence: 0 };\n c += (byMagicOnly ? 1 : 0.5) * s;\n }\n if (prefer.includes(format.id)) c += 0.25;\n return { format, confidence: Math.min(1, c), rank: c };\n })\n .filter((x) => x.confidence > (byMagicOnly ? 0.6 : 0))\n // порядок — по оценке без потолка 1: при равной сигнатуре решает пресет (PSK одинаков у Unreal 1–5)\n .sort((a, b) => (b.rank ?? 0) - (a.rank ?? 0))\n .map(({ format, confidence }) => ({ format, confidence }));\n if (!scored.length) return null;\n return { format: scored[0]!.format, confidence: scored[0]!.confidence, candidates: scored };\n }\n\n /** Декодировать картинку: кодеки плагинов, затем createImageBitmap браузера. */\n async decodeImage(file: VFile): Promise<DecodedImage> {\n if (file.decoded) return file.decoded;\n const buf = await file.arrayBuffer(), codec = this.codecFor(file.name);\n if (codec) return codec.decode(buf, file.name);\n if (typeof createImageBitmap !== 'function') throw new Error(`нет декодера для ${file.name}`);\n const bitmap = await createImageBitmap(new Blob([buf]), { premultiplyAlpha: 'none', colorSpaceConversion: 'none' });\n return { bitmap, width: bitmap.width, height: bitmap.height };\n }\n}\n"],"names":["e"],"mappings":"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"}
1
+ {"version":3,"file":"registry.js","sources":["../src/registry.ts"],"sourcesContent":["// Plugin registry: engines -> formats, archives, codecs, texture dictionaries, game presets. Format detection.\nimport { extOf, readHead } from './binary';\nimport { pickGames, type ArchivePlugin, type EnginePlugin, type ExporterPlugin, type FormatPlugin, type GamePreset, type ImageCodec, type MaterialResolver, type TextureDictionaryPlugin } from './plugin';\nimport { registerTexts } from './i18n';\nimport type { BlobFile, DecodedImage, Model, VFile } from './types';\nimport type { ImageDecoder } from './textures';\n\nexport interface Detection {\n format: FormatPlugin;\n /** 0..1 */\n confidence: number;\n /** All candidates by descending confidence (for choosing among 2–3 options). */\n candidates: Array<{ format: FormatPlugin; confidence: number }>;\n}\n\nexport class Registry implements ImageDecoder {\n readonly engines: EnginePlugin[] = [];\n readonly formats: FormatPlugin[] = [];\n readonly archives: ArchivePlugin[] = [];\n readonly codecs: ImageCodec[] = [];\n readonly dictionaries: TextureDictionaryPlugin[] = [];\n readonly games: GamePreset[] = [];\n readonly materials: MaterialResolver[] = [];\n readonly exporters: ExporterPlugin[] = [];\n\n constructor(engines: readonly EnginePlugin[] = []) {\n for (const e of engines) this.use(e);\n }\n\n /** Attach an engine. Attaching again with the same id replaces its plugins. */\n use(engine: EnginePlugin): this {\n const old = this.engines.findIndex((e) => e.id === engine.id);\n if (old >= 0) {\n // a second slice of the same engine (another game's subpath): games are merged\n const prev = this.engines[old]!, base = engine.base ?? engine;\n if (prev.base && engine.base && prev.base === engine.base) engine = pickGames(base, ...new Set([...(prev.games ?? []), ...(engine.games ?? [])].map((g) => g.id)));\n this.remove(engine.id);\n }\n registerTexts(engine);\n this.engines.push(engine);\n const add = <T extends { id: string }>(list: T[], items: readonly T[] | undefined, kind: string) => {\n for (const it of items ?? []) {\n if (list.some((x) => x.id === it.id)) throw new Error(`gmv: ${kind} с id «${it.id}» уже зарегистрирован`);\n list.push(it);\n }\n };\n add(this.formats, engine.formats, 'формат');\n add(this.archives, engine.archives, 'архив');\n add(this.codecs, engine.codecs, 'кодек');\n add(this.dictionaries, engine.dictionaries, 'словарь');\n add(this.games, engine.games, 'пресет');\n add(this.materials, engine.materials, 'материалы');\n add(this.exporters, engine.exporters, 'экспорт');\n return this;\n }\n\n remove(engineId: string): void {\n const i = this.engines.findIndex((e) => e.id === engineId);\n if (i < 0) return;\n const e = this.engines[i]!;\n this.engines.splice(i, 1);\n const drop = <T>(list: T[], items: readonly T[] | undefined) => {\n for (const it of items ?? []) { const k = list.indexOf(it); if (k >= 0) list.splice(k, 1); }\n };\n drop(this.formats, e.formats);\n drop(this.archives, e.archives);\n drop(this.codecs, e.codecs);\n drop(this.dictionaries, e.dictionaries);\n drop(this.games, e.games);\n drop(this.materials, e.materials);\n drop(this.exporters, e.exporters);\n }\n\n /** Export options for a model opened from engine sourceEngine: those intended for it first, then general ones. */\n exportersFor(model: Model, sourceEngine: string): Array<{ exporter: ExporterPlugin; recommended: boolean }> {\n return this.exporters\n .filter((x) => (!x.onlyFrom || x.from?.includes(sourceEngine)) && (!x.accepts || x.accepts(model)))\n .map((exporter) => ({ exporter, recommended: !!exporter.from?.includes(sourceEngine) }))\n .sort((a, b) => Number(b.recommended) - Number(a.recommended));\n }\n\n format(id: string): FormatPlugin | undefined { return this.formats.find((f) => f.id === id); }\n game(id: string): GamePreset | undefined { return this.games.find((g) => g.id === id); }\n engine(id: string): EnginePlugin | undefined { return this.engines.find((e) => e.id === id); }\n\n /** Whether the registry has at least one of the preset's formats (an assimp bridge without @gmvjs/assimp does not). */\n presetAvailable(p: GamePreset): boolean { return p.formats.some((id) => !!this.format(id)); }\n /** Model extensions of the preset. */\n presetExtensions(p: GamePreset): string[] {\n if (p.extensions) return p.extensions;\n return [...new Set(p.formats.flatMap((id) => this.format(id)?.extensions ?? []))];\n }\n /** External animation extensions of the preset. */\n presetMotionExtensions(p: GamePreset): string[] {\n return [...new Set(p.formats.flatMap((id) => this.format(id)?.motionExtensions ?? []))];\n }\n /** First preset that opens the format. */\n presetForFormat(formatId: string, fileName?: string): GamePreset | undefined {\n const ext = fileName ? extOf(fileName) : '';\n return (ext && this.games.find((g) => g.formats.includes(formatId) && this.presetExtensions(g).includes(ext))) || this.games.find((g) => g.formats.includes(formatId));\n }\n /** Extensions found only on external animations (.omf, .psa, .md5anim), never on models. */\n get motionOnlyExtensions(): string[] {\n const models = new Set(this.formats.flatMap((f) => f.extensions));\n return [...new Set(this.formats.flatMap((f) => f.motionExtensions ?? []))].filter((e) => !models.has(e));\n }\n get modelExtensions(): string[] {\n return [...new Set(this.formats.flatMap((f) => [...f.extensions, ...(f.motionExtensions ?? [])]))];\n }\n\n /** Model by path pattern (extensionless MeshCache, classes/trucks/*.xml); formats limits it to the preset's formats. */\n isPathModel(path: string, formats?: readonly string[]): boolean {\n return this.formats.some((f) => f.pathPattern?.test(path) && (!formats || formats.includes(f.id)));\n }\n\n archiveExtensions(): string[] {\n return [...new Set(this.archives.flatMap((a) => [...a.extensions, ...(a.companionExtensions ?? [])]))];\n }\n isArchive(name: string): boolean { return this.archiveExtensions().includes(extOf(name)); }\n dictionaryFor(name: string): TextureDictionaryPlugin | undefined {\n const ext = extOf(name);\n return this.dictionaries.find((d) => d.extensions.includes(ext));\n }\n codecFor(name: string): ImageCodec | undefined {\n const ext = extOf(name);\n return this.codecs.find((c) => c.extensions.includes(ext));\n }\n /** Whether this is an image (by extension): plugin codecs + browser formats. */\n isImage(name: string): boolean {\n return !!this.codecFor(name) || /\\.(png|jpe?g|bmp|webp|gif)$/i.test(name);\n }\n\n /** Archive plugin for a file: by extension, by signature when ambiguous. */\n async archiveFor(file: BlobFile): Promise<ArchivePlugin | null> {\n const ext = extOf(file.name), cands = this.archives.filter((a) => a.extensions.includes(ext));\n if (cands.length <= 1) return cands[0] ?? null;\n const head = await readHead(file, 64);\n let best: ArchivePlugin | null = null, score = -1;\n for (const a of cands) {\n const s = a.sniff ? a.sniff(head, file.name) : 0.1;\n if (s > score) { score = s; best = a; }\n }\n // signature not recognized: ZIP is read from the end of the file (directory), so it is the fallback, as in v11\n return score > 0 ? best : cands.find((a) => a.id === 'zip') ?? null;\n }\n\n /**\n * Detect the model format: candidates by extension, refined by signature,\n * with a bonus for the preferred preset's formats. Without an extension all signatures are tried.\n */\n async detect(file: VFile | BlobFile, prefer: readonly string[] = []): Promise<Detection | null> {\n const ext = extOf(file.name);\n let cands = this.formats.filter((f) => f.extensions.includes(ext));\n const byMagicOnly = !cands.length;\n if (byMagicOnly) cands = this.formats.filter((f) => f.sniff);\n if (!cands.length) return null;\n // always check the signature when a candidate supports it: otherwise a text Doom 3 .skin goes to CryEngine .skin\n const needHead = byMagicOnly || cands.some((c) => c.sniff);\n const head = needHead ? await readHead(file, 256) : null;\n const scored = cands\n .map((format) => {\n let c = byMagicOnly ? 0 : 0.5;\n if (head && format.sniff) {\n const s = format.sniff(head, file.name);\n if (s <= 0) return { format, confidence: 0 };\n c += (byMagicOnly ? 1 : 0.5) * s;\n }\n if (prefer.includes(format.id)) c += 0.25;\n return { format, confidence: Math.min(1, c), rank: c };\n })\n .filter((x) => x.confidence > (byMagicOnly ? 0.6 : 0))\n // order by the uncapped score: with an equal signature the preset decides (PSK is the same in Unreal 1–5)\n .sort((a, b) => (b.rank ?? 0) - (a.rank ?? 0))\n .map(({ format, confidence }) => ({ format, confidence }));\n if (!scored.length) return null;\n return { format: scored[0]!.format, confidence: scored[0]!.confidence, candidates: scored };\n }\n\n /** Decode an image: plugin codecs, then the browser's createImageBitmap. */\n async decodeImage(file: VFile): Promise<DecodedImage> {\n if (file.decoded) return file.decoded;\n const buf = await file.arrayBuffer(), codec = this.codecFor(file.name);\n if (codec) return codec.decode(buf, file.name);\n if (typeof createImageBitmap !== 'function') throw new Error(`нет декодера для ${file.name}`);\n const bitmap = await createImageBitmap(new Blob([buf]), { premultiplyAlpha: 'none', colorSpaceConversion: 'none' });\n return { bitmap, width: bitmap.width, height: bitmap.height };\n }\n}\n"],"names":["e"],"mappings":"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"}