@gmvjs/core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -35
- package/dist/archives/common.d.ts +9 -9
- package/dist/archives/common.js.map +1 -1
- package/dist/archives/folder.d.ts +1 -1
- package/dist/archives/folder.js +1 -1
- package/dist/archives/folder.js.map +1 -1
- package/dist/archives/zip.js.map +1 -1
- package/dist/binary.d.ts +7 -7
- package/dist/binary.js.map +1 -1
- package/dist/builtin-en.js.map +1 -1
- package/dist/builtin.js +1 -1
- package/dist/builtin.js.map +1 -1
- package/dist/codecs/zlib.d.ts +2 -2
- package/dist/codecs/zlib.js.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/geometry.d.ts +5 -5
- package/dist/geometry.js.map +1 -1
- package/dist/i18n.d.ts +4 -4
- package/dist/i18n.js.map +1 -1
- package/dist/images/bc45.d.ts +1 -1
- package/dist/images/bc45.js.map +1 -1
- package/dist/images/bc7.d.ts +1 -1
- package/dist/images/bc7.js.map +1 -1
- package/dist/images/dds.d.ts +1 -1
- package/dist/images/dds.js.map +1 -1
- package/dist/images/pcx.js.map +1 -1
- package/dist/images/tga.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/levelbuilder.d.ts +3 -3
- package/dist/levelbuilder.js +1 -1
- package/dist/levelbuilder.js.map +1 -1
- package/dist/math.d.ts +6 -6
- package/dist/math.js.map +1 -1
- package/dist/normals.d.ts +5 -5
- package/dist/normals.js.map +1 -1
- package/dist/plugin.d.ts +57 -57
- package/dist/plugin.js.map +1 -1
- package/dist/registry.d.ts +14 -14
- package/dist/registry.js +13 -13
- package/dist/registry.js.map +1 -1
- package/dist/scene.d.ts +14 -14
- package/dist/scene.js.map +1 -1
- package/dist/session.d.ts +34 -34
- package/dist/session.js +33 -33
- package/dist/session.js.map +1 -1
- package/dist/skeleton.d.ts +3 -3
- package/dist/skeleton.js.map +1 -1
- package/dist/surface.d.ts +4 -4
- package/dist/surface.js.map +1 -1
- package/dist/textures.d.ts +12 -12
- package/dist/textures.js +9 -9
- package/dist/textures.js.map +1 -1
- package/dist/types.d.ts +103 -103
- package/dist/types.js.map +1 -1
- package/dist/writers/dds.d.ts +1 -1
- package/dist/writers/dds.js.map +1 -1
- package/dist/writers/obj.js.map +1 -1
- package/dist/writers/zip.js.map +1 -1
- package/package.json +3 -3
package/dist/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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
49
|
+
/** Load companion files after loading (a same-named .txd, animations from the folder…). */
|
|
50
50
|
afterLoad?(ctx: AfterLoadContext): Promise<void>;
|
|
51
51
|
}
|
|
52
|
-
/**
|
|
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
|
-
/**
|
|
69
|
+
/** Extensions of companion files (.dir for IMG, VPK parts) that are not opened on their own. */
|
|
70
70
|
companionExtensions?: string[];
|
|
71
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
79
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
110
|
+
/** The external animation has the same extension as the model (.cast). */
|
|
111
111
|
motionAny?: boolean;
|
|
112
|
-
/**
|
|
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
|
-
/**
|
|
119
|
+
/** Group in the list: "Games", "Formats (assimp)"… */
|
|
120
120
|
group: string;
|
|
121
121
|
title: string;
|
|
122
122
|
engine: string;
|
|
123
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
211
|
+
/** Engine slice by games (pickGames): the full engine it was taken from. */
|
|
212
212
|
base?: EnginePlugin;
|
|
213
213
|
}
|
|
214
|
-
/**
|
|
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
|
-
*
|
|
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;
|
package/dist/plugin.js.map
CHANGED
|
@@ -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;"}
|
package/dist/registry.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface Detection {
|
|
|
5
5
|
format: FormatPlugin;
|
|
6
6
|
/** 0..1 */
|
|
7
7
|
confidence: number;
|
|
8
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
43
|
+
/** Extensions found only on external animations (.omf, .psa, .md5anim), never on models. */
|
|
44
44
|
get motionOnlyExtensions(): string[];
|
|
45
45
|
get modelExtensions(): string[];
|
|
46
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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);
|
package/dist/registry.js.map
CHANGED
|
@@ -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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"}
|