@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/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sources":["../src/session.ts"],"sourcesContent":["// Сессия просмотра без DOM: выбранный пресет, архивы, текстуры, модель и анимации.\n// Интерфейс (расширение, страница, другое приложение) подписывается на события и рисует результат.\nimport { tr } from './i18n';\nimport { baseName, extOf, toArrayBuffer } from './binary';\nimport { analyzeNormalMap, normalCandidates, type NormalPacking } from './normals';\nimport { composeSurface, surfaceCandidates, surfaceMaps } from './surface';\nimport { openFolder, openFolderAsync } from './archives/folder';\nimport type { AfterLoadContext, EngineMaps, FormatPlugin, GamePreset, LoadContext } from './plugin';\nimport type { Registry } from './registry';\nimport { TextureLibrary, type TextureSource } from './textures';\nimport { MotionOnlyError, type Archive, type ArchiveEntry, type BlobFile, type DecodedImage, type Mesh, type Model, type Motion, type VFile } from './types';\n\nexport interface SessionEvents {\n status: { text: string; error: boolean };\n preset: GamePreset;\n /** keepView — та же модель с другими Model.options: камеру не сбрасывать. */\n model: { model: Model; main: VFile; format: FormatPlugin; keepView?: boolean };\n motions: Motion[];\n archives: Archive[];\n /** Набор доступных текстур изменился — стоит заново применить текстуры. */\n textures: void;\n /** Долгая операция: текст и, если известно, сколько сделано из скольких; null — закончена. */\n progress: { text: string; done?: number; total?: number } | null;\n}\ntype Listener<T> = (payload: T) => void;\n\nexport interface TextureResult {\n meshIndex: number;\n image: DecodedImage | null;\n /** Подпись для списка мешей: «512×512», «нет файла», «ошибка: …». */\n label: string;\n ok: boolean;\n /** Путь найденного файла (для поиска карты нормалей рядом). */\n source?: string;\n}\n\nexport interface SurfaceResult {\n meshIndex: number;\n /** Сведённая карта: R — AO, G — шероховатость, B — металличность, A — блик. */\n image: DecodedImage | null;\n label: string;\n ok: boolean;\n}\n\nexport interface NormalResult {\n meshIndex: number;\n image: DecodedImage | null;\n packing: NormalPacking;\n label: string;\n ok: boolean;\n}\n\nexport interface ArchiveItem { archive: Archive; entry: ArchiveEntry }\n\nconst MAX_SAME_DIR_MOTIONS = 80;\n\nexport class ViewerSession {\n readonly archives: Archive[] = [];\n /** Файлы-спутники архивов, выбранные раньше (XR_3DA.exe — ключ архивов ТЧ, .dir к IMG): передаются архивам при открытии. */\n readonly companions: BlobFile[] = [];\n readonly textures: TextureLibrary;\n preset: GamePreset;\n model: Model | null = null;\n format: FormatPlugin | null = null;\n main: VFile | null = null;\n readonly motions: Motion[] = [];\n /** Последняя неудачная загрузка (для кнопки «Скопировать диагностику»). */\n lastFailure: { file: VFile; format: string; error: string } | null = null;\n private readonly overrides = new Map<number, TextureSource>();\n /** Карты движка (resolveMaps) по имени текстуры меша. */\n private readonly mapsCache = new Map<string, Promise<EngineMaps | null>>();\n private engineMaps(mesh: Mesh): Promise<EngineMaps | null> {\n const fmt = this.format, model = this.model;\n if (!fmt?.resolveMaps || !model) return Promise.resolve(null);\n const key = `${mesh.texture}|${mesh.shader ?? ''}`;\n let p = this.mapsCache.get(key);\n if (!p) { p = fmt.resolveMaps(mesh, model, this.textures).catch((e) => { console.warn(e); return null; }); this.mapsCache.set(key, p); }\n return p;\n }\n private readonly listeners = new Map<keyof SessionEvents, Set<Listener<never>>>();\n\n constructor(readonly registry: Registry, presetId?: string | null) {\n if (!registry.games.length) throw new Error('gmv: в реестре нет ни одного пресета');\n this.preset = (presetId && registry.game(presetId)) || registry.games[0]!;\n this.textures = new TextureLibrary(registry, this.archives);\n }\n\n // ---------- события ----------\n on<K extends keyof SessionEvents>(type: K, fn: Listener<SessionEvents[K]>): () => void {\n let set = this.listeners.get(type);\n if (!set) this.listeners.set(type, (set = new Set()));\n set.add(fn as Listener<never>);\n return () => set!.delete(fn as Listener<never>);\n }\n private emit<K extends keyof SessionEvents>(type: K, payload: SessionEvents[K]): void {\n for (const fn of this.listeners.get(type) ?? []) (fn as Listener<SessionEvents[K]>)(payload);\n }\n private status(text: string, error = false): void { this.emit('status', { text, error }); }\n /** Сообщить о ходе долгой операции (для полосы прогресса); без аргументов — операция закончена. */\n private progress(text?: string, done?: number, total?: number): void { this.emit('progress', text === undefined ? null : { text, done, total }); }\n /** Дать браузеру перерисовать полосу прогресса перед тяжёлым синхронным шагом. */\n private yieldUI(): Promise<void> { return new Promise((r) => setTimeout(r, 0)); }\n\n // ---------- пресет ----------\n setPreset(id: string): GamePreset {\n const p = this.registry.game(id) ?? this.registry.games[0]!;\n this.preset = p;\n this.emit('preset', p);\n return p;\n }\n get modelExtensions(): string[] { return this.registry.presetExtensions(this.preset); }\n get motionExtensions(): string[] {\n const m = this.registry.presetMotionExtensions(this.preset);\n return this.preset.ui?.motionAny ? [...new Set([...m, ...this.modelExtensions])] : m;\n }\n\n // ---------- приём файлов ----------\n /** Разложить набор файлов: архивы, словари текстур, картинки, анимации, модель. */\n async addFiles(input: Iterable<BlobFile>): Promise<void> {\n const files = Array.from(input), reg = this.registry;\n const motionExts = this.registry.motionOnlyExtensions;\n const arc = files.filter((f) => reg.isArchive(f.name));\n const dict = files.filter((f) => !reg.isArchive(f.name) && reg.dictionaryFor(f.name));\n const img = files.filter((f) => !reg.isArchive(f.name) && reg.isImage(f.name));\n const mot = files.filter((f) => motionExts.includes(extOf(f.name)));\n const rest = files.filter((f) => !arc.includes(f) && !dict.includes(f) && !img.includes(f) && !mot.includes(f));\n if (arc.length) await this.addArchives(arc);\n for (const f of dict) await this.addTextureDictionary(f);\n if (img.length) this.addTextures(img);\n if (rest.length) await this.loadModel(rest);\n for (const f of mot) await this.loadMotions(f);\n }\n\n /**\n * Открыть архивы. Файл, который не читается (не архив — например, зашифрованные a3.Arch01 F.E.A.R. Online, — повреждён),\n * пропускается — остальные открываются;\n * в строке состояния — сколько открыто и что пропущено.\n */\n async addArchives(files: readonly BlobFile[]): Promise<void> {\n const skipped: string[] = [];\n let opened = 0;\n // спутники (XR_3DA.exe ТЧ, .dir) запоминаются: их можно выбрать отдельно от архивов (они лежат в другой папке)\n const isCompanion = (f: BlobFile) => this.registry.archives.some((a) => a.companionExtensions?.includes(extOf(f.name)));\n for (const f of files) if (isCompanion(f) && !this.companions.some((c) => c.name === f.name && c.size === f.size)) this.companions.push(f);\n const all = [...files, ...this.companions.filter((c) => !files.includes(c))];\n if (files.every(isCompanion)) { this.status(tr('session.companions', { list: files.map((f) => f.name).join(', ') })); return; }\n try {\n for (const [i, f] of files.entries()) {\n try {\n const plugin = await this.registry.archiveFor(f);\n if (!plugin) continue; // спутник (.dir) или неизвестный формат\n this.status(tr('session.readingArchive', { name: f.name }));\n this.progress(tr('session.readingArchive', { name: f.name }), i, files.length);\n await this.yieldUI();\n const a = await plugin.open(f, all);\n if (a) { this.archives.push(a); opened++; }\n } catch (e) {\n console.error(e);\n skipped.push(`${f.name}: ${errMsg(e)}`);\n }\n }\n } finally { this.progress(); }\n if (skipped.length) {\n this.archivesChanged(tr('session.archivesSkipped', { n: this.archives.length, k: skipped.length, list: skipped.slice(0, 3).join('; ') + (skipped.length > 3 ? '…' : '') }));\n if (!opened) this.status(tr('session.archiveError', { error: skipped.slice(0, 3).join('; ') }), true);\n } else this.archivesChanged(tr('session.archives', { n: this.archives.length }));\n }\n\n /** «Папка игры» с полосой прогресса: индекс строится кусками, интерфейс не замирает (сотни тысяч файлов). */\n async addFolderAsync(files: readonly BlobFile[]): Promise<Archive> {\n try {\n const a = await openFolderAsync(files, (done, total) => this.progress(tr('session.indexing', { n: total }), done, total));\n this.archives.push(a);\n this.progress(tr('session.folderTextures'));\n await this.yieldUI();\n this.archivesChanged(tr('session.folder', { name: a.name, n: a.entries.length }));\n return a;\n } finally { this.progress(); }\n }\n\n /** «Папка игры»: индексирует файлы без чтения. */\n addFolder(files: readonly BlobFile[]): Archive {\n const a = openFolder(files);\n this.archives.push(a);\n this.archivesChanged(tr('session.folder', { name: a.name, n: a.entries.length }));\n return a;\n }\n\n /**\n * Архивы внутри «Папки игры», которые движок просит открывать сразу (ArchivePlugin.openFromFolder): F.E.A.R. Online — *.arch01.\n * Возвращает число открытых архивов.\n */\n async openFolderArchives(folder: Archive): Promise<number> {\n if (!folder.blobOf) return 0;\n const exts = new Set(this.registry.archives.filter((a) => a.openFromFolder).flatMap((a) => a.extensions));\n // файлы-спутники (.dat к .fat Far Cry 2) — передаются вместе с архивами, сами не открываются\n const companions = new Set(this.registry.archives.filter((a) => a.openFromFolder).flatMap((a) => a.companionExtensions ?? []));\n const files: BlobFile[] = [], extra: BlobFile[] = [];\n this.progress(tr('session.findArchives'));\n for (const e of folder.entries) {\n if (companions.has(extOf(e.path)) && !exts.has(extOf(e.path))) { const f = folder.blobOf(e); if (f) extra.push(f); continue; }\n if (!exts.has(extOf(e.path))) continue;\n const f = folder.blobOf(e);\n // то же расширение может быть у архивов другого движка (.db X-Ray) — их не трогаем\n const plugin = f ? await this.registry.archiveFor(f) : null;\n if (!f || !plugin?.openFromFolder) continue;\n // файлы с тем же расширением, но не архивы (F.E.A.R. Online: зашифрованные a3.Arch01…) — пропускаем\n const head = new Uint8Array(await f.slice(0, 64).arrayBuffer());\n if (plugin.sniff && !plugin.sniff(head, f.name)) continue;\n if (typeof plugin.openFromFolder === 'function' && !plugin.openFromFolder(head, f.name)) continue;\n files.push(f);\n }\n this.progress();\n if (!files.length) return 0;\n const before = this.archives.length;\n await this.addArchives([...files, ...extra]);\n return this.archives.length - before;\n }\n\n private archivesChanged(msg: string): void {\n this.textures.invalidate();\n this.status(msg);\n this.emit('archives', this.archives);\n this.emit('textures', undefined);\n }\n\n addTextures(files: readonly BlobFile[]): void {\n this.textures.addFiles(files.filter((f) => this.registry.isImage(f.name)));\n this.emit('textures', undefined);\n }\n\n async addTextureDictionary(file: VFile | BlobFile): Promise<void> {\n const plugin = this.registry.dictionaryFor(file.name);\n if (!plugin) return;\n try {\n const { textures, errors } = await plugin.parse(await file.arrayBuffer());\n for (const t of textures) this.textures.addDecoded(t.name, t, plugin.id);\n this.status(\n tr('session.dict', { id: plugin.id.toUpperCase(), name: file.name, n: textures.length }) + (errors.length ? tr('session.dictErrors', { n: errors.length, first: errors[0]! }) : ''),\n errors.length > 0 && !textures.length,\n );\n this.emit('textures', undefined);\n } catch (e) {\n console.error(e);\n this.status(tr('session.dictError', { id: plugin.id.toUpperCase(), error: errMsg(e) }), true);\n }\n }\n\n // ---------- модель ----------\n /** Выбрать основной файл (автоопределение) и загрузить модель. */\n /** Файлы и выборы последней загрузки — для setModelOption. */\n private lastLoad: { files: ReadonlyArray<VFile | BlobFile>; options: Record<string, string> } | null = null;\n\n /** Сменить выбор модели (Model.options) и пересобрать её из тех же файлов. */\n async setModelOption(id: string, value: string): Promise<Model | null> {\n if (!this.lastLoad || !this.main) return null;\n const options = { ...this.lastLoad.options, [id]: value };\n return this.loadModel([this.main, ...this.lastLoad.files.filter((f) => f !== this.main)], options, true);\n }\n\n async loadModel(files: ReadonlyArray<VFile | BlobFile>, options: Record<string, string> = {}, keepView = false): Promise<Model | null> {\n let main: VFile | null = null, format: FormatPlugin | null = null, fallback: { file: VFile; format: FormatPlugin } | null = null;\n for (const f of files) {\n const d = await this.registry.detect(f, this.preset.formats);\n if (!d) continue;\n if (this.preset.formats.includes(d.format.id)) { main = f; format = d.format; break; }\n if (!fallback) fallback = { file: f, format: d.format };\n }\n if (!main && fallback) {\n main = fallback.file; format = fallback.format;\n const p = this.registry.presetForFormat(format.id, main.name);\n if (p) this.setPreset(p.id);\n }\n if (!main || !format) { this.status(tr('session.noModel'), true); return null; }\n this.status(tr('session.loading'));\n this.progress(tr('session.loadingName', { name: main.name }));\n await this.yieldUI();\n try {\n const model = await format.load(main, this.loadContext(files, main, options));\n this.lastLoad = { files, options };\n model.normalY ??= this.registry.engine(format.engine)?.normalY ?? 'dx';\n this.model = model; this.format = format; this.main = main; this.lastFailure = null;\n this.overrides.clear();\n this.mapsCache.clear();\n this.motions.length = 0;\n this.motions.push(...model.motions.map((m) => ({ ...m, source: m.source ?? 'model' })));\n this.emit('model', { model, main, format, keepView });\n if (format.afterLoad) await format.afterLoad(this.afterLoadContext(main, model));\n this.emit('motions', this.motions);\n this.status(tr('session.loaded', { name: main.name }));\n return model;\n } catch (e) {\n if (e instanceof MotionOnlyError && this.model?.loadMotions) { await this.loadMotions(main); return this.model; }\n console.error(e);\n this.lastFailure = { file: main, format: format.id, error: errMsg(e) };\n this.status(tr('session.error', { error: errMsg(e) }), true);\n return null;\n } finally { this.progress(); }\n }\n\n private loadContext(files: ReadonlyArray<VFile | BlobFile>, main: VFile, options: Record<string, string> = {}): LoadContext {\n const archives = this.archives;\n return {\n files,\n archives,\n options,\n async sibling(name) {\n const lower = name.toLowerCase();\n const f = files.find((x) => x.name.toLowerCase() === lower);\n if (f) return f.arrayBuffer();\n if (main.path) {\n const dir = main.path.replace(/[^/]*$/, '');\n for (const a of archives) { const e = a.lookup(dir + lower); if (e) return toArrayBuffer(await a.read(e)); }\n }\n return null;\n },\n async readPath(path) {\n for (const a of archives) { const e = a.lookup(path); if (e) return a.read(e); }\n return null;\n },\n };\n }\n\n private afterLoadContext(main: VFile, model: Model): AfterLoadContext {\n return {\n main,\n model,\n textures: this.textures,\n addMotions: (list, source) => { this.motions.push(...list.map((m) => ({ ...m, source }))); },\n addTextureDictionary: (file) => this.addTextureDictionary(file),\n };\n }\n\n /** Добавить внешние анимации к загруженной модели. */\n async loadMotions(file: VFile | BlobFile): Promise<Motion[]> {\n if (!this.model?.loadMotions) {\n this.status(tr('session.noMotionModel'), true);\n return [];\n }\n try {\n const list = await this.model.loadMotions(file);\n this.motions.push(...list.map((m) => ({ ...m, source: file.name })));\n this.emit('motions', this.motions);\n this.status(tr('session.motionsAdded', { n: list.length, name: file.name }));\n return list;\n } catch (e) {\n console.error(e);\n this.status(tr('session.motionError', { error: errMsg(e) }), true);\n return [];\n }\n }\n\n // ---------- текстуры ----------\n setTextureOverride(meshIndex: number, file: VFile | BlobFile): void { this.overrides.set(meshIndex, file); }\n\n /** Найти и декодировать текстуру меша. */\n /** Файл текстуры меша (тот же поиск, что у resolveTexture): для выгрузки исходных байтов, например DDS. */\n async textureSource(i: number): Promise<TextureSource | null> {\n const model = this.model, mesh = model?.meshes[i];\n if (!model || !mesh) return null;\n return this.findTextureSource(model, mesh, i);\n }\n\n private async findTextureSource(model: Model, mesh: Mesh, i: number): Promise<TextureSource | null> {\n let src: TextureSource | null = this.overrides.get(i) ?? null;\n if (!src && mesh.image) src = { name: 'embedded', decoded: mesh.image, arrayBuffer: async () => new ArrayBuffer(0) };\n if (!src) src = this.textures.find(mesh.texture);\n if (!src && this.format?.resolveTexture) {\n try { src = await this.format.resolveTexture(mesh, model, this.textures); } catch (e) { console.warn(e); }\n }\n // общие файлы материалов движков (.mtr, .mat) — для моделей любого формата\n for (const r of this.registry.materials) {\n if (src) break;\n try { src = await r.resolve(mesh, this.textures); } catch (e) { console.warn(e); }\n }\n if (!src && mesh.texture) src = this.sameFolderTexture(mesh.texture);\n return src;\n }\n\n async resolveTexture(i: number): Promise<TextureResult> {\n const model = this.model, mesh = model?.meshes[i];\n if (!model || !mesh) return { meshIndex: i, image: null, label: '', ok: false };\n const src = await this.findTextureSource(model, mesh, i);\n if (!src) return { meshIndex: i, image: null, label: mesh.texture ? tr('tex.noFile') : '', ok: false };\n try {\n const image = await this.textures.decode(src);\n return { meshIndex: i, image, label: `${image.width}×${image.height}`, ok: true, source: src.path ?? src.name };\n } catch (e) {\n return { meshIndex: i, image: null, label: tr('tex.error', { error: errMsg(e) }), ok: false };\n }\n }\n\n /**\n * Карта нормалей меша: встроенная → указанная моделью/материалом → по имени диффузной (_co→_nohq, __d→__n_d, _diff→_normal, _d→_n…).\n * Найденная картинка проверяется на «похожесть» на нормали и упаковку (RGB, RG/BC5, AG/DXT5nm).\n */\n async resolveNormal(i: number, diffuse?: string): Promise<NormalResult> {\n const mesh = this.model?.meshes[i], none: NormalResult = { meshIndex: i, image: null, packing: 'rgb', label: '', ok: false };\n if (!mesh) return none;\n const em = (await this.engineMaps(mesh))?.normal;\n if (em) return { meshIndex: i, image: em.image, packing: em.packing ?? 'rgb', label: em.label ?? `N ${em.image.width}×${em.image.height}`, ok: true };\n // явно заданные (встроенная, из материала) принимаются без проверки «похожести»; найденные по имени — проверяются\n const tried: Array<[() => TextureSource | null, boolean]> = [];\n if (mesh.normalImage) tried.push([() => ({ name: 'embedded', decoded: mesh.normalImage, arrayBuffer: async () => new ArrayBuffer(0) }), true]);\n if (mesh.normalMap) tried.push([() => this.textures.find(mesh.normalMap), true]);\n for (const base of [diffuse, mesh.texture]) for (const c of normalCandidates(base ?? '')) tried.push([() => this.textures.find(c), false]);\n const seen = new Set<string>();\n for (const [get, explicit] of tried) {\n const src = get();\n if (!src) continue;\n const key = src.path ?? src.name;\n if (seen.has(key)) continue;\n seen.add(key);\n try {\n const image = await this.textures.decode(src);\n const a = analyzeNormalMap(image);\n if (!a.normal && !explicit) continue; // по имени нашлось не то\n return { meshIndex: i, image, packing: a.packing, label: `N ${image.width}×${image.height}${a.packing !== 'rgb' ? ' ' + a.packing.toUpperCase() : ''}`, ok: true };\n } catch { /* битый файл — пробуем следующий */ }\n }\n return mesh.normalMap ? { ...none, label: tr('tex.normalNoFile') } : none;\n }\n\n /**\n * PBR-поверхность меша: из материала (mesh.surface) или упакованные карты по имени диффузной (_orm, _rma, _smdi…).\n * Карты сводятся в одну текстуру (composeSurface); нет карт, но есть множители — текстура 1×1.\n */\n async resolveSurface(i: number, diffuse?: string): Promise<SurfaceResult> {\n const mesh = this.model?.meshes[i], none: SurfaceResult = { meshIndex: i, image: null, label: '', ok: false };\n if (!mesh) return none;\n const es = (await this.engineMaps(mesh))?.surface;\n if (es) return { meshIndex: i, image: es.image, label: es.label ?? 'PBR', ok: true };\n const variants = [...(mesh.surface ? [mesh.surface] : []), ...surfaceCandidates(diffuse ?? '').concat(diffuse !== mesh.texture ? surfaceCandidates(mesh.texture) : [])];\n for (const [k, s] of variants.entries()) {\n const explicit = k === 0 && !!mesh.surface;\n const images = new Map<string, DecodedImage>();\n for (const map of surfaceMaps(s)) {\n const src = this.textures.find(map);\n if (!src) continue;\n try { images.set(map, await this.textures.decode(src)); } catch { /* битая карта — без неё */ }\n }\n const factors = s.roughnessFactor !== undefined || s.metallicFactor !== undefined || s.specularFactor !== undefined;\n if (!images.size && !(explicit && factors)) continue;\n const image = composeSurface(s, images);\n const parts = [s.metallic && 'M', (s.roughness || s.gloss) && 'R', s.ao && 'AO', s.specular && 'S'].filter(Boolean).join('·');\n return { meshIndex: i, image, label: `PBR${parts ? ' ' + parts : ''}${images.size ? '' : tr('tex.pbrFactors')}`, ok: true };\n }\n return none;\n }\n\n /**\n * Последняя попытка: картинка в папке модели. Имя совпадает с материалом, или «skin.*» (Quake II rerelease: md5/skin.png),\n * или это единственная картинка в папке (без карт *_glow/_n/_s/_normal/_spec).\n */\n private sameFolderTexture(texture: string): TextureSource | null {\n const main = this.main;\n if (!main?.archive || !main.path) return null;\n const dir = main.path.replace(/[^/]*$/, ''), want = baseName(texture).toLowerCase().replace(/\\.[^.]+$/, '');\n const imgs = main.archive.entries.filter((e) => e.path.startsWith(dir) && !e.path.slice(dir.length).includes('/') && this.registry.isImage(e.path)\n && !/_(glow|n|s|normal|spec|local|bump|gloss|h)\\.[^.]+$/.test(e.path));\n const stem = (p: string) => baseName(p).replace(/\\.[^.]+$/, '');\n const pick = imgs.find((e) => stem(e.path) === want) ?? imgs.find((e) => stem(e.path) === 'skin') ?? (imgs.length === 1 ? imgs[0] : undefined);\n return pick ? main.archive.fileOf(pick) : null;\n }\n\n // ---------- содержимое архивов ----------\n /** Модели и анимации текущего пресета во всех архивах. */\n archiveModels(filter = '', limit = 400): { items: ArchiveItem[]; total: number } {\n const exts = [...this.modelExtensions, ...this.motionExtensions].map((x) => '.' + x), q = filter.trim().toLowerCase();\n const items: ArchiveItem[] = [];\n let total = 0;\n for (const archive of this.archives)\n for (const entry of archive.entries) {\n if (!exts.some((x) => entry.path.endsWith(x)) && !this.registry.isPathModel(entry.path, this.preset.formats)) continue;\n total++;\n if (items.length >= limit || (q && !entry.path.includes(q))) continue;\n items.push({ archive, entry });\n }\n return { items, total };\n }\n\n /** Открыть запись архива: анимацию — добавить, модель — загрузить. */\n async openEntry(item: ArchiveItem): Promise<void> {\n const f = item.archive.fileOf(item.entry), ext = extOf(item.entry.path);\n if (this.registry.presetMotionExtensions(this.preset).includes(ext) && !this.modelExtensions.includes(ext)) await this.loadMotions(f);\n else await this.loadModel([f]);\n }\n}\n\nexport const errMsg = (e: unknown): string => (e instanceof Error ? e.message : String(e));\n\n/** Анимации из той же папки архива, что и модель (Doom 3: *.md5anim рядом с *.md5mesh). */\nexport async function loadSameDirMotions(ctx: AfterLoadContext, ext: string): Promise<number> {\n const { main, model } = ctx;\n if (!main.archive || !main.path || !model.loadMotions) return 0;\n const dir = main.path.replace(/[^/]*$/, '');\n let n = 0;\n for (const e of main.archive.entries) {\n if (n >= MAX_SAME_DIR_MOTIONS) break;\n if (!e.path.startsWith(dir) || !e.path.endsWith('.' + ext) || e.path.slice(dir.length).includes('/')) continue;\n try {\n const f = main.archive.fileOf(e);\n ctx.addMotions(await model.loadMotions(f), f.name);\n n++;\n } catch (err) { console.warn(err); }\n }\n return n;\n}\n"],"names":["f"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session.js","sources":["../src/session.ts"],"sourcesContent":["// DOM-free viewing session: the selected preset, archives, textures, model and animations.\n// The UI (extension, web page, another app) subscribes to events and draws the result.\nimport { tr } from './i18n';\nimport { baseName, extOf, toArrayBuffer } from './binary';\nimport { analyzeNormalMap, normalCandidates, type NormalPacking } from './normals';\nimport { composeSurface, surfaceCandidates, surfaceMaps } from './surface';\nimport { openFolder, openFolderAsync } from './archives/folder';\nimport type { AfterLoadContext, EngineMaps, FormatPlugin, GamePreset, LoadContext } from './plugin';\nimport type { Registry } from './registry';\nimport { TextureLibrary, type TextureSource } from './textures';\nimport { MotionOnlyError, type Archive, type ArchiveEntry, type BlobFile, type DecodedImage, type Mesh, type Model, type Motion, type VFile } from './types';\n\nexport interface SessionEvents {\n status: { text: string; error: boolean };\n preset: GamePreset;\n /** keepView: the same model with other Model.options, do not reset the camera. */\n model: { model: Model; main: VFile; format: FormatPlugin; keepView?: boolean };\n motions: Motion[];\n archives: Archive[];\n /** The set of available textures changed: textures should be applied again. */\n textures: void;\n /** Long operation: text and, if known, how much of how much is done; null means finished. */\n progress: { text: string; done?: number; total?: number } | null;\n}\ntype Listener<T> = (payload: T) => void;\n\nexport interface TextureResult {\n meshIndex: number;\n image: DecodedImage | null;\n /** Label for the mesh list: \"512×512\", \"no file\", \"error: …\". */\n label: string;\n ok: boolean;\n /** Path of the found file (to look for a normal map next to it). */\n source?: string;\n}\n\nexport interface SurfaceResult {\n meshIndex: number;\n /** Combined map: R = AO, G = roughness, B = metalness, A = specular. */\n image: DecodedImage | null;\n label: string;\n ok: boolean;\n}\n\nexport interface NormalResult {\n meshIndex: number;\n image: DecodedImage | null;\n packing: NormalPacking;\n label: string;\n ok: boolean;\n}\n\nexport interface ArchiveItem { archive: Archive; entry: ArchiveEntry }\n\nconst MAX_SAME_DIR_MOTIONS = 80;\n\nexport class ViewerSession {\n readonly archives: Archive[] = [];\n /** Archive companion files selected earlier (XR_3DA.exe = SoC archive key, .dir for IMG): passed to archives when they are opened. */\n readonly companions: BlobFile[] = [];\n readonly textures: TextureLibrary;\n preset: GamePreset;\n model: Model | null = null;\n format: FormatPlugin | null = null;\n main: VFile | null = null;\n readonly motions: Motion[] = [];\n /** Last failed load (for the \"Copy diagnostics\" button). */\n lastFailure: { file: VFile; format: string; error: string } | null = null;\n private readonly overrides = new Map<number, TextureSource>();\n /** Engine maps (resolveMaps) by mesh texture name. */\n private readonly mapsCache = new Map<string, Promise<EngineMaps | null>>();\n private engineMaps(mesh: Mesh): Promise<EngineMaps | null> {\n const fmt = this.format, model = this.model;\n if (!fmt?.resolveMaps || !model) return Promise.resolve(null);\n const key = `${mesh.texture}|${mesh.shader ?? ''}`;\n let p = this.mapsCache.get(key);\n if (!p) { p = fmt.resolveMaps(mesh, model, this.textures).catch((e) => { console.warn(e); return null; }); this.mapsCache.set(key, p); }\n return p;\n }\n private readonly listeners = new Map<keyof SessionEvents, Set<Listener<never>>>();\n\n constructor(readonly registry: Registry, presetId?: string | null) {\n if (!registry.games.length) throw new Error('gmv: в реестре нет ни одного пресета');\n this.preset = (presetId && registry.game(presetId)) || registry.games[0]!;\n this.textures = new TextureLibrary(registry, this.archives);\n }\n\n // ---------- events ----------\n on<K extends keyof SessionEvents>(type: K, fn: Listener<SessionEvents[K]>): () => void {\n let set = this.listeners.get(type);\n if (!set) this.listeners.set(type, (set = new Set()));\n set.add(fn as Listener<never>);\n return () => set!.delete(fn as Listener<never>);\n }\n private emit<K extends keyof SessionEvents>(type: K, payload: SessionEvents[K]): void {\n for (const fn of this.listeners.get(type) ?? []) (fn as Listener<SessionEvents[K]>)(payload);\n }\n private status(text: string, error = false): void { this.emit('status', { text, error }); }\n /** Report progress of a long operation (for the progress bar); without arguments the operation is finished. */\n private progress(text?: string, done?: number, total?: number): void { this.emit('progress', text === undefined ? null : { text, done, total }); }\n /** Let the browser repaint the progress bar before a heavy synchronous step. */\n private yieldUI(): Promise<void> { return new Promise((r) => setTimeout(r, 0)); }\n\n // ---------- preset ----------\n setPreset(id: string): GamePreset {\n const p = this.registry.game(id) ?? this.registry.games[0]!;\n this.preset = p;\n this.emit('preset', p);\n return p;\n }\n get modelExtensions(): string[] { return this.registry.presetExtensions(this.preset); }\n get motionExtensions(): string[] {\n const m = this.registry.presetMotionExtensions(this.preset);\n return this.preset.ui?.motionAny ? [...new Set([...m, ...this.modelExtensions])] : m;\n }\n\n // ---------- receiving files ----------\n /** Sort a set of files into archives, texture dictionaries, images, animations and the model. */\n async addFiles(input: Iterable<BlobFile>): Promise<void> {\n const files = Array.from(input), reg = this.registry;\n const motionExts = this.registry.motionOnlyExtensions;\n const arc = files.filter((f) => reg.isArchive(f.name));\n const dict = files.filter((f) => !reg.isArchive(f.name) && reg.dictionaryFor(f.name));\n const img = files.filter((f) => !reg.isArchive(f.name) && reg.isImage(f.name));\n const mot = files.filter((f) => motionExts.includes(extOf(f.name)));\n const rest = files.filter((f) => !arc.includes(f) && !dict.includes(f) && !img.includes(f) && !mot.includes(f));\n if (arc.length) await this.addArchives(arc);\n for (const f of dict) await this.addTextureDictionary(f);\n if (img.length) this.addTextures(img);\n if (rest.length) await this.loadModel(rest);\n for (const f of mot) await this.loadMotions(f);\n }\n\n /**\n * Open archives. A file that cannot be read (not an archive, for example the encrypted F.E.A.R. Online a3.Arch01, or damaged)\n * is skipped and the rest are opened;\n * the status line tells how many were opened and what was skipped.\n */\n async addArchives(files: readonly BlobFile[]): Promise<void> {\n const skipped: string[] = [];\n let opened = 0;\n // companions (SoC XR_3DA.exe, .dir) are remembered: they can be selected separately from archives (they live in another folder)\n const isCompanion = (f: BlobFile) => this.registry.archives.some((a) => a.companionExtensions?.includes(extOf(f.name)));\n for (const f of files) if (isCompanion(f) && !this.companions.some((c) => c.name === f.name && c.size === f.size)) this.companions.push(f);\n const all = [...files, ...this.companions.filter((c) => !files.includes(c))];\n if (files.every(isCompanion)) { this.status(tr('session.companions', { list: files.map((f) => f.name).join(', ') })); return; }\n try {\n for (const [i, f] of files.entries()) {\n try {\n const plugin = await this.registry.archiveFor(f);\n if (!plugin) continue; // a companion (.dir) or an unknown format\n this.status(tr('session.readingArchive', { name: f.name }));\n this.progress(tr('session.readingArchive', { name: f.name }), i, files.length);\n await this.yieldUI();\n const a = await plugin.open(f, all);\n if (a) { this.archives.push(a); opened++; }\n } catch (e) {\n console.error(e);\n skipped.push(`${f.name}: ${errMsg(e)}`);\n }\n }\n } finally { this.progress(); }\n if (skipped.length) {\n this.archivesChanged(tr('session.archivesSkipped', { n: this.archives.length, k: skipped.length, list: skipped.slice(0, 3).join('; ') + (skipped.length > 3 ? '…' : '') }));\n if (!opened) this.status(tr('session.archiveError', { error: skipped.slice(0, 3).join('; ') }), true);\n } else this.archivesChanged(tr('session.archives', { n: this.archives.length }));\n }\n\n /** \"Game folder\" with a progress bar: the index is built in chunks so the UI does not freeze (hundreds of thousands of files). */\n async addFolderAsync(files: readonly BlobFile[]): Promise<Archive> {\n try {\n const a = await openFolderAsync(files, (done, total) => this.progress(tr('session.indexing', { n: total }), done, total));\n this.archives.push(a);\n this.progress(tr('session.folderTextures'));\n await this.yieldUI();\n this.archivesChanged(tr('session.folder', { name: a.name, n: a.entries.length }));\n return a;\n } finally { this.progress(); }\n }\n\n /** \"Game folder\": indexes files without reading them. */\n addFolder(files: readonly BlobFile[]): Archive {\n const a = openFolder(files);\n this.archives.push(a);\n this.archivesChanged(tr('session.folder', { name: a.name, n: a.entries.length }));\n return a;\n }\n\n /**\n * Archives inside the \"Game folder\" that the engine asks to open right away (ArchivePlugin.openFromFolder): F.E.A.R. Online *.arch01.\n * Returns the number of opened archives.\n */\n async openFolderArchives(folder: Archive): Promise<number> {\n if (!folder.blobOf) return 0;\n const exts = new Set(this.registry.archives.filter((a) => a.openFromFolder).flatMap((a) => a.extensions));\n // companion files (.dat for Far Cry 2 .fat) are passed along with archives and are not opened on their own\n const companions = new Set(this.registry.archives.filter((a) => a.openFromFolder).flatMap((a) => a.companionExtensions ?? []));\n const files: BlobFile[] = [], extra: BlobFile[] = [];\n this.progress(tr('session.findArchives'));\n for (const e of folder.entries) {\n if (companions.has(extOf(e.path)) && !exts.has(extOf(e.path))) { const f = folder.blobOf(e); if (f) extra.push(f); continue; }\n if (!exts.has(extOf(e.path))) continue;\n const f = folder.blobOf(e);\n // another engine's archives may have the same extension (X-Ray .db): leave them alone\n const plugin = f ? await this.registry.archiveFor(f) : null;\n if (!f || !plugin?.openFromFolder) continue;\n // files with the same extension that are not archives (F.E.A.R. Online: encrypted a3.Arch01…) are skipped\n const head = new Uint8Array(await f.slice(0, 64).arrayBuffer());\n if (plugin.sniff && !plugin.sniff(head, f.name)) continue;\n if (typeof plugin.openFromFolder === 'function' && !plugin.openFromFolder(head, f.name)) continue;\n files.push(f);\n }\n this.progress();\n if (!files.length) return 0;\n const before = this.archives.length;\n await this.addArchives([...files, ...extra]);\n return this.archives.length - before;\n }\n\n private archivesChanged(msg: string): void {\n this.textures.invalidate();\n this.status(msg);\n this.emit('archives', this.archives);\n this.emit('textures', undefined);\n }\n\n addTextures(files: readonly BlobFile[]): void {\n this.textures.addFiles(files.filter((f) => this.registry.isImage(f.name)));\n this.emit('textures', undefined);\n }\n\n async addTextureDictionary(file: VFile | BlobFile): Promise<void> {\n const plugin = this.registry.dictionaryFor(file.name);\n if (!plugin) return;\n try {\n const { textures, errors } = await plugin.parse(await file.arrayBuffer());\n for (const t of textures) this.textures.addDecoded(t.name, t, plugin.id);\n this.status(\n tr('session.dict', { id: plugin.id.toUpperCase(), name: file.name, n: textures.length }) + (errors.length ? tr('session.dictErrors', { n: errors.length, first: errors[0]! }) : ''),\n errors.length > 0 && !textures.length,\n );\n this.emit('textures', undefined);\n } catch (e) {\n console.error(e);\n this.status(tr('session.dictError', { id: plugin.id.toUpperCase(), error: errMsg(e) }), true);\n }\n }\n\n // ---------- model ----------\n /** Pick the main file (format detection) and load the model. */\n /** Files and choices of the last load, for setModelOption. */\n private lastLoad: { files: ReadonlyArray<VFile | BlobFile>; options: Record<string, string> } | null = null;\n\n /** Change a model choice (Model.options) and rebuild it from the same files. */\n async setModelOption(id: string, value: string): Promise<Model | null> {\n if (!this.lastLoad || !this.main) return null;\n const options = { ...this.lastLoad.options, [id]: value };\n return this.loadModel([this.main, ...this.lastLoad.files.filter((f) => f !== this.main)], options, true);\n }\n\n async loadModel(files: ReadonlyArray<VFile | BlobFile>, options: Record<string, string> = {}, keepView = false): Promise<Model | null> {\n let main: VFile | null = null, format: FormatPlugin | null = null, fallback: { file: VFile; format: FormatPlugin } | null = null;\n for (const f of files) {\n const d = await this.registry.detect(f, this.preset.formats);\n if (!d) continue;\n if (this.preset.formats.includes(d.format.id)) { main = f; format = d.format; break; }\n if (!fallback) fallback = { file: f, format: d.format };\n }\n if (!main && fallback) {\n main = fallback.file; format = fallback.format;\n const p = this.registry.presetForFormat(format.id, main.name);\n if (p) this.setPreset(p.id);\n }\n if (!main || !format) { this.status(tr('session.noModel'), true); return null; }\n this.status(tr('session.loading'));\n this.progress(tr('session.loadingName', { name: main.name }));\n await this.yieldUI();\n try {\n const model = await format.load(main, this.loadContext(files, main, options));\n this.lastLoad = { files, options };\n model.normalY ??= this.registry.engine(format.engine)?.normalY ?? 'dx';\n this.model = model; this.format = format; this.main = main; this.lastFailure = null;\n this.overrides.clear();\n this.mapsCache.clear();\n this.motions.length = 0;\n this.motions.push(...model.motions.map((m) => ({ ...m, source: m.source ?? 'model' })));\n this.emit('model', { model, main, format, keepView });\n if (format.afterLoad) await format.afterLoad(this.afterLoadContext(main, model));\n this.emit('motions', this.motions);\n this.status(tr('session.loaded', { name: main.name }));\n return model;\n } catch (e) {\n if (e instanceof MotionOnlyError && this.model?.loadMotions) { await this.loadMotions(main); return this.model; }\n console.error(e);\n this.lastFailure = { file: main, format: format.id, error: errMsg(e) };\n this.status(tr('session.error', { error: errMsg(e) }), true);\n return null;\n } finally { this.progress(); }\n }\n\n private loadContext(files: ReadonlyArray<VFile | BlobFile>, main: VFile, options: Record<string, string> = {}): LoadContext {\n const archives = this.archives;\n return {\n files,\n archives,\n options,\n async sibling(name) {\n const lower = name.toLowerCase();\n const f = files.find((x) => x.name.toLowerCase() === lower);\n if (f) return f.arrayBuffer();\n if (main.path) {\n const dir = main.path.replace(/[^/]*$/, '');\n for (const a of archives) { const e = a.lookup(dir + lower); if (e) return toArrayBuffer(await a.read(e)); }\n }\n return null;\n },\n async readPath(path) {\n for (const a of archives) { const e = a.lookup(path); if (e) return a.read(e); }\n return null;\n },\n };\n }\n\n private afterLoadContext(main: VFile, model: Model): AfterLoadContext {\n return {\n main,\n model,\n textures: this.textures,\n addMotions: (list, source) => { this.motions.push(...list.map((m) => ({ ...m, source }))); },\n addTextureDictionary: (file) => this.addTextureDictionary(file),\n };\n }\n\n /** Add external animations to the loaded model. */\n async loadMotions(file: VFile | BlobFile): Promise<Motion[]> {\n if (!this.model?.loadMotions) {\n this.status(tr('session.noMotionModel'), true);\n return [];\n }\n try {\n const list = await this.model.loadMotions(file);\n this.motions.push(...list.map((m) => ({ ...m, source: file.name })));\n this.emit('motions', this.motions);\n this.status(tr('session.motionsAdded', { n: list.length, name: file.name }));\n return list;\n } catch (e) {\n console.error(e);\n this.status(tr('session.motionError', { error: errMsg(e) }), true);\n return [];\n }\n }\n\n // ---------- textures ----------\n setTextureOverride(meshIndex: number, file: VFile | BlobFile): void { this.overrides.set(meshIndex, file); }\n\n /** Find and decode a mesh texture. */\n /** Mesh texture file (the same lookup as resolveTexture): for exporting the source bytes, for example DDS. */\n async textureSource(i: number): Promise<TextureSource | null> {\n const model = this.model, mesh = model?.meshes[i];\n if (!model || !mesh) return null;\n return this.findTextureSource(model, mesh, i);\n }\n\n private async findTextureSource(model: Model, mesh: Mesh, i: number): Promise<TextureSource | null> {\n let src: TextureSource | null = this.overrides.get(i) ?? null;\n if (!src && mesh.image) src = { name: 'embedded', decoded: mesh.image, arrayBuffer: async () => new ArrayBuffer(0) };\n if (!src) src = this.textures.find(mesh.texture);\n if (!src && this.format?.resolveTexture) {\n try { src = await this.format.resolveTexture(mesh, model, this.textures); } catch (e) { console.warn(e); }\n }\n // shared engine material files (.mtr, .mat): for models of any format\n for (const r of this.registry.materials) {\n if (src) break;\n try { src = await r.resolve(mesh, this.textures); } catch (e) { console.warn(e); }\n }\n if (!src && mesh.texture) src = this.sameFolderTexture(mesh.texture);\n return src;\n }\n\n async resolveTexture(i: number): Promise<TextureResult> {\n const model = this.model, mesh = model?.meshes[i];\n if (!model || !mesh) return { meshIndex: i, image: null, label: '', ok: false };\n const src = await this.findTextureSource(model, mesh, i);\n if (!src) return { meshIndex: i, image: null, label: mesh.texture ? tr('tex.noFile') : '', ok: false };\n try {\n const image = await this.textures.decode(src);\n return { meshIndex: i, image, label: `${image.width}×${image.height}`, ok: true, source: src.path ?? src.name };\n } catch (e) {\n return { meshIndex: i, image: null, label: tr('tex.error', { error: errMsg(e) }), ok: false };\n }\n }\n\n /**\n * Mesh normal map: embedded → named by the model/material → derived from the diffuse name (_co→_nohq, __d→__n_d, _diff→_normal, _d→_n…).\n * A found image is checked for looking like a normal map and for its packing (RGB, RG/BC5, AG/DXT5nm).\n */\n async resolveNormal(i: number, diffuse?: string): Promise<NormalResult> {\n const mesh = this.model?.meshes[i], none: NormalResult = { meshIndex: i, image: null, packing: 'rgb', label: '', ok: false };\n if (!mesh) return none;\n const em = (await this.engineMaps(mesh))?.normal;\n if (em) return { meshIndex: i, image: em.image, packing: em.packing ?? 'rgb', label: em.label ?? `N ${em.image.width}×${em.image.height}`, ok: true };\n // explicitly given ones (embedded, from the material) are accepted without the \"looks like\" check; ones found by name are checked\n const tried: Array<[() => TextureSource | null, boolean]> = [];\n if (mesh.normalImage) tried.push([() => ({ name: 'embedded', decoded: mesh.normalImage, arrayBuffer: async () => new ArrayBuffer(0) }), true]);\n if (mesh.normalMap) tried.push([() => this.textures.find(mesh.normalMap), true]);\n for (const base of [diffuse, mesh.texture]) for (const c of normalCandidates(base ?? '')) tried.push([() => this.textures.find(c), false]);\n const seen = new Set<string>();\n for (const [get, explicit] of tried) {\n const src = get();\n if (!src) continue;\n const key = src.path ?? src.name;\n if (seen.has(key)) continue;\n seen.add(key);\n try {\n const image = await this.textures.decode(src);\n const a = analyzeNormalMap(image);\n if (!a.normal && !explicit) continue; // the name matched something else\n return { meshIndex: i, image, packing: a.packing, label: `N ${image.width}×${image.height}${a.packing !== 'rgb' ? ' ' + a.packing.toUpperCase() : ''}`, ok: true };\n } catch { /* broken file: try the next one */ }\n }\n return mesh.normalMap ? { ...none, label: tr('tex.normalNoFile') } : none;\n }\n\n /**\n * Mesh PBR surface: from the material (mesh.surface) or packed maps derived from the diffuse name (_orm, _rma, _smdi…).\n * Maps are combined into one texture (composeSurface); with no maps but with multipliers, a 1×1 texture.\n */\n async resolveSurface(i: number, diffuse?: string): Promise<SurfaceResult> {\n const mesh = this.model?.meshes[i], none: SurfaceResult = { meshIndex: i, image: null, label: '', ok: false };\n if (!mesh) return none;\n const es = (await this.engineMaps(mesh))?.surface;\n if (es) return { meshIndex: i, image: es.image, label: es.label ?? 'PBR', ok: true };\n const variants = [...(mesh.surface ? [mesh.surface] : []), ...surfaceCandidates(diffuse ?? '').concat(diffuse !== mesh.texture ? surfaceCandidates(mesh.texture) : [])];\n for (const [k, s] of variants.entries()) {\n const explicit = k === 0 && !!mesh.surface;\n const images = new Map<string, DecodedImage>();\n for (const map of surfaceMaps(s)) {\n const src = this.textures.find(map);\n if (!src) continue;\n try { images.set(map, await this.textures.decode(src)); } catch { /* broken map: go without it */ }\n }\n const factors = s.roughnessFactor !== undefined || s.metallicFactor !== undefined || s.specularFactor !== undefined;\n if (!images.size && !(explicit && factors)) continue;\n const image = composeSurface(s, images);\n const parts = [s.metallic && 'M', (s.roughness || s.gloss) && 'R', s.ao && 'AO', s.specular && 'S'].filter(Boolean).join('·');\n return { meshIndex: i, image, label: `PBR${parts ? ' ' + parts : ''}${images.size ? '' : tr('tex.pbrFactors')}`, ok: true };\n }\n return none;\n }\n\n /**\n * Last resort: an image in the model folder. Its name matches the material, or \"skin.*\" (Quake II rerelease: md5/skin.png),\n * or it is the only image in the folder (excluding *_glow/_n/_s/_normal/_spec maps).\n */\n private sameFolderTexture(texture: string): TextureSource | null {\n const main = this.main;\n if (!main?.archive || !main.path) return null;\n const dir = main.path.replace(/[^/]*$/, ''), want = baseName(texture).toLowerCase().replace(/\\.[^.]+$/, '');\n const imgs = main.archive.entries.filter((e) => e.path.startsWith(dir) && !e.path.slice(dir.length).includes('/') && this.registry.isImage(e.path)\n && !/_(glow|n|s|normal|spec|local|bump|gloss|h)\\.[^.]+$/.test(e.path));\n const stem = (p: string) => baseName(p).replace(/\\.[^.]+$/, '');\n const pick = imgs.find((e) => stem(e.path) === want) ?? imgs.find((e) => stem(e.path) === 'skin') ?? (imgs.length === 1 ? imgs[0] : undefined);\n return pick ? main.archive.fileOf(pick) : null;\n }\n\n // ---------- archive contents ----------\n /** Models and animations of the current preset in all archives. */\n archiveModels(filter = '', limit = 400): { items: ArchiveItem[]; total: number } {\n const exts = [...this.modelExtensions, ...this.motionExtensions].map((x) => '.' + x), q = filter.trim().toLowerCase();\n const items: ArchiveItem[] = [];\n let total = 0;\n for (const archive of this.archives)\n for (const entry of archive.entries) {\n if (!exts.some((x) => entry.path.endsWith(x)) && !this.registry.isPathModel(entry.path, this.preset.formats)) continue;\n total++;\n if (items.length >= limit || (q && !entry.path.includes(q))) continue;\n items.push({ archive, entry });\n }\n return { items, total };\n }\n\n /** Open an archive entry: add an animation, load a model. */\n async openEntry(item: ArchiveItem): Promise<void> {\n const f = item.archive.fileOf(item.entry), ext = extOf(item.entry.path);\n if (this.registry.presetMotionExtensions(this.preset).includes(ext) && !this.modelExtensions.includes(ext)) await this.loadMotions(f);\n else await this.loadModel([f]);\n }\n}\n\nexport const errMsg = (e: unknown): string => (e instanceof Error ? e.message : String(e));\n\n/** Animations from the same archive folder as the model (Doom 3: *.md5anim next to *.md5mesh). */\nexport async function loadSameDirMotions(ctx: AfterLoadContext, ext: string): Promise<number> {\n const { main, model } = ctx;\n if (!main.archive || !main.path || !model.loadMotions) return 0;\n const dir = main.path.replace(/[^/]*$/, '');\n let n = 0;\n for (const e of main.archive.entries) {\n if (n >= MAX_SAME_DIR_MOTIONS) break;\n if (!e.path.startsWith(dir) || !e.path.endsWith('.' + ext) || e.path.slice(dir.length).includes('/')) continue;\n try {\n const f = main.archive.fileOf(e);\n ctx.addMotions(await model.loadMotions(f), f.name);\n n++;\n } catch (err) { console.warn(err); }\n }\n return n;\n}\n"],"names":["f"],"mappings":"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"}
|
package/dist/skeleton.d.ts
CHANGED
|
@@ -2,11 +2,11 @@ import type { Motion, SkeletonRuntime } from './types';
|
|
|
2
2
|
export interface SkeletonNode {
|
|
3
3
|
name: string;
|
|
4
4
|
parent: number;
|
|
5
|
-
/**
|
|
5
|
+
/** Local matrix (column-major). */
|
|
6
6
|
local: ArrayLike<number>;
|
|
7
|
-
/**
|
|
7
|
+
/** Inverse bind matrix of the bone (offset); null means the node is not a bone, skin = world. */
|
|
8
8
|
offset: ArrayLike<number> | null;
|
|
9
|
-
/**
|
|
9
|
+
/** Helper node (mesh, bind offset): not drawn as a skeleton line. */
|
|
10
10
|
helper?: boolean;
|
|
11
11
|
}
|
|
12
12
|
export declare class NodeSkeleton implements SkeletonRuntime {
|
package/dist/skeleton.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skeleton.js","sources":["../src/skeleton.ts"],"sourcesContent":["// Скелет из иерархии узлов (assimp, NIF, RenderWare, MD5, PSK, Cast, GoldSrc).\nimport { compose, matToTRS, mulColInto, sampleQuat, sampleVec, type TRS } from './math';\nimport type { Keys, Motion, NodeMotion, SkeletonRuntime } from './types';\n\nexport interface SkeletonNode {\n name: string;\n parent: number;\n /** Локальная матрица (column-major). */\n local: ArrayLike<number>;\n /** Обратная bind-матрица кости (offset); null — узел не кость, skin = world. */\n offset: ArrayLike<number> | null;\n /** Служебный узел (меш, bind-смещение): не рисуется линией скелета. */\n helper?: boolean;\n}\n\n/** q·r (кватернионы [w, x, y, z]). */\nfunction qmul(q: ArrayLike<number>, r: ArrayLike<number>): number[] {\n return [\n q[0]! * r[0]! - q[1]! * r[1]! - q[2]! * r[2]! - q[3]! * r[3]!,\n q[0]! * r[1]! + q[1]! * r[0]! + q[2]! * r[3]! - q[3]! * r[2]!,\n q[0]! * r[2]! - q[1]! * r[3]! + q[2]! * r[0]! + q[3]! * r[1]!,\n q[0]! * r[3]! + q[1]! * r[2]! - q[2]! * r[1]! + q[3]! * r[0]!,\n ];\n}\n\n/** Канал в точке сетки поз: билинейно по 4 соседним кадрам (перенос — линейно, поворот — нормированная сумма). */\nfunction gridSample(c: { pos: Keys<number[]> | null; rot: Keys<number[]> | null }, g: { cols: number; rows: number; x: number; y: number }): { t: number[] | null; q: number[] | null } {\n const cx = Math.max(0, Math.min(g.cols - 1, g.x)), cy = Math.max(0, Math.min(g.rows - 1, g.y));\n const x0 = Math.floor(cx), y0 = Math.floor(cy), x1 = Math.min(g.cols - 1, x0 + 1), y1 = Math.min(g.rows - 1, y0 + 1), fx = cx - x0, fy = cy - y0;\n const taps: Array<[number, number]> = [[y0 * g.cols + x0, (1 - fx) * (1 - fy)], [y0 * g.cols + x1, fx * (1 - fy)], [y1 * g.cols + x0, (1 - fx) * fy], [y1 * g.cols + x1, fx * fy]];\n let t: number[] | null = null, q: number[] | null = null;\n if (c.pos) { t = [0, 0, 0]; for (const [f, w] of taps) { if (!w) continue; const v = sampleVec(c.pos, f); for (let k = 0; k < 3; k++) t[k] += w * v[k]!; } }\n if (c.rot) {\n q = [0, 0, 0, 0];\n let ref: number[] | null = null;\n for (const [f, w] of taps) {\n if (!w) continue;\n const v = sampleQuat(c.rot, f);\n if (!ref) ref = v;\n const sgn = ref[0]! * v[0]! + ref[1]! * v[1]! + ref[2]! * v[2]! + ref[3]! * v[3]! < 0 ? -w : w;\n for (let k = 0; k < 4; k++) q[k] += sgn * v[k]!;\n }\n const n = Math.hypot(q[0]!, q[1]!, q[2]!, q[3]!) || 1;\n q = q.map((v) => v / n);\n }\n return { t, q };\n}\n\nexport class NodeSkeleton implements SkeletonRuntime {\n readonly n: number;\n readonly nodes: SkeletonNode[];\n readonly parent: number[];\n readonly world: Float32Array;\n readonly skin: Float32Array;\n readonly worldV: Float32Array[];\n readonly skinV: Float32Array[];\n private readonly rest: TRS[];\n private readonly w64: Float64Array[];\n private readonly tmp = new Float64Array(16);\n private readonly tmp2 = new Float64Array(16);\n\n constructor(nodes: SkeletonNode[]) {\n const n = (this.n = nodes.length);\n this.nodes = nodes;\n this.parent = nodes.map((x) => (x.helper ? -1 : x.parent));\n this.rest = nodes.map((nd) => matToTRS(nd.local));\n this.world = new Float32Array(16 * n);\n this.skin = new Float32Array(16 * n);\n const view = (a: Float32Array) => Array.from({ length: n }, (_, i) => a.subarray(16 * i, 16 * i + 16));\n this.worldV = view(this.world);\n this.skinV = view(this.skin);\n this.w64 = Array.from({ length: n }, () => new Float64Array(16));\n }\n\n pose(motion: Motion | null, time: number, loop: boolean): void {\n const m = motion && motion.kind === 'node' ? motion : null;\n const tickOf = (x: { duration: number; tps: number }): number => {\n const d = x.duration, tt = loop ? (d > 0 ? ((time % d) + d) % d : 0) : Math.min(Math.max(time, 0), d);\n return tt * x.tps;\n };\n const tick = m ? tickOf(m) : 0;\n const layers = m?.layers?.length ? m.layers.map((l) => ({ l, tick: l.frame ?? tickOf(l.motion) })) : null;\n // смешивание: до 4 соседних анимаций сетки с общей фазой\n let taps: Array<{ mo: NodeMotion; w: number; tick: number }> | null = null;\n if (m?.blend?.motions.length) {\n const b = m.blend, cx = Math.max(0, Math.min(b.cols - 1, b.x)), cy = Math.max(0, Math.min(b.rows - 1, b.y));\n const x0 = Math.floor(cx), y0 = Math.floor(cy), x1 = Math.min(b.cols - 1, x0 + 1), y1 = Math.min(b.rows - 1, y0 + 1), fx = cx - x0, fy = cy - y0;\n const d = m.duration, phase = d > 0 ? (loop ? (((time % d) + d) % d) / d : Math.min(Math.max(time / d, 0), 1)) : 0;\n taps = [];\n for (const [i, w] of [[y0 * b.cols + x0, (1 - fx) * (1 - fy)], [y0 * b.cols + x1, fx * (1 - fy)], [y1 * b.cols + x0, (1 - fx) * fy], [y1 * b.cols + x1, fx * fy]] as Array<[number, number]>) {\n const mo = b.motions[i];\n if (!mo || w < 1e-6) continue;\n const prev = taps.find((x) => x.mo === mo);\n if (prev) prev.w += w; else taps.push({ mo, w, tick: phase * mo.duration * mo.tps });\n }\n }\n for (let i = 0; i < this.n; i++) {\n const nd = this.nodes[i]!, ch = m ? m.chan.get(i) : undefined, rest = this.rest[i]!;\n let L: ArrayLike<number> = nd.local;\n if (ch || layers || taps) {\n let t: ArrayLike<number> = ch?.pos ? sampleVec(ch.pos, tick) : rest.t, q: ArrayLike<number> = ch?.rot ? sampleQuat(ch.rot, tick) : rest.q;\n const s = ch?.scl ? sampleVec(ch.scl, tick) : rest.s;\n let touched = !!ch;\n if (taps) {\n const tt = [0, 0, 0], qq = [0, 0, 0, 0];\n let any = false, ref: ArrayLike<number> | null = null;\n for (const { mo, w, tick: bt } of taps) {\n const c = mo.chan.get(i);\n if (c) any = true;\n const p = c?.pos ? sampleVec(c.pos, bt) : rest.t, r = c?.rot ? sampleQuat(c.rot, bt) : rest.q;\n for (let k = 0; k < 3; k++) tt[k] += p[k]! * w;\n ref ??= r;\n const sg = ref[0]! * r[0]! + ref[1]! * r[1]! + ref[2]! * r[2]! + ref[3]! * r[3]! < 0 ? -w : w;\n for (let k = 0; k < 4; k++) qq[k] += r[k]! * sg;\n }\n if (any) { const n = Math.hypot(qq[0]!, qq[1]!, qq[2]!, qq[3]!) || 1; t = tt; q = qq.map((v) => v / n); touched = true; }\n }\n if (layers) for (const { l, tick: lt } of layers) {\n const c = l.motion.chan.get(i);\n if (!c || (l.mask && !l.mask.has(i)) || (l.fill && ch)) continue;\n touched = true;\n if (l.grid && !c.add) { const g = gridSample(c, l.grid); if (g.t) t = g.t; if (g.q) q = g.q; continue; }\n if (c.add) {\n if (c.add.pos) { const d = sampleVec(c.add.pos, lt); t = [t[0]! + d[0]!, t[1]! + d[1]!, t[2]! + d[2]!]; }\n if (c.add.rot) q = qmul(q, sampleQuat(c.add.rot, lt));\n } else {\n if (c.pos) t = sampleVec(c.pos, lt);\n if (c.rot) q = sampleQuat(c.rot, lt);\n }\n }\n if (touched) { compose(t, q, s, this.tmp); L = this.tmp; }\n }\n const W = this.w64[i]!;\n if (nd.parent < 0) W.set(L); else mulColInto(this.w64[nd.parent]!, L, W);\n this.worldV[i]!.set(W);\n if (nd.offset) { mulColInto(W, nd.offset, this.tmp2); this.skinV[i]!.set(this.tmp2); }\n else this.skinV[i]!.set(W);\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"skeleton.js","sources":["../src/skeleton.ts"],"sourcesContent":["// Skeleton from a node hierarchy (assimp, NIF, RenderWare, MD5, PSK, Cast, GoldSrc).\nimport { compose, matToTRS, mulColInto, sampleQuat, sampleVec, type TRS } from './math';\nimport type { Keys, Motion, NodeMotion, SkeletonRuntime } from './types';\n\nexport interface SkeletonNode {\n name: string;\n parent: number;\n /** Local matrix (column-major). */\n local: ArrayLike<number>;\n /** Inverse bind matrix of the bone (offset); null means the node is not a bone, skin = world. */\n offset: ArrayLike<number> | null;\n /** Helper node (mesh, bind offset): not drawn as a skeleton line. */\n helper?: boolean;\n}\n\n/** q·r (quaternions [w, x, y, z]). */\nfunction qmul(q: ArrayLike<number>, r: ArrayLike<number>): number[] {\n return [\n q[0]! * r[0]! - q[1]! * r[1]! - q[2]! * r[2]! - q[3]! * r[3]!,\n q[0]! * r[1]! + q[1]! * r[0]! + q[2]! * r[3]! - q[3]! * r[2]!,\n q[0]! * r[2]! - q[1]! * r[3]! + q[2]! * r[0]! + q[3]! * r[1]!,\n q[0]! * r[3]! + q[1]! * r[2]! - q[2]! * r[1]! + q[3]! * r[0]!,\n ];\n}\n\n/** Channel at a point of the pose grid: bilinear over 4 neighbouring frames (translation linear, rotation normalized sum). */\nfunction gridSample(c: { pos: Keys<number[]> | null; rot: Keys<number[]> | null }, g: { cols: number; rows: number; x: number; y: number }): { t: number[] | null; q: number[] | null } {\n const cx = Math.max(0, Math.min(g.cols - 1, g.x)), cy = Math.max(0, Math.min(g.rows - 1, g.y));\n const x0 = Math.floor(cx), y0 = Math.floor(cy), x1 = Math.min(g.cols - 1, x0 + 1), y1 = Math.min(g.rows - 1, y0 + 1), fx = cx - x0, fy = cy - y0;\n const taps: Array<[number, number]> = [[y0 * g.cols + x0, (1 - fx) * (1 - fy)], [y0 * g.cols + x1, fx * (1 - fy)], [y1 * g.cols + x0, (1 - fx) * fy], [y1 * g.cols + x1, fx * fy]];\n let t: number[] | null = null, q: number[] | null = null;\n if (c.pos) { t = [0, 0, 0]; for (const [f, w] of taps) { if (!w) continue; const v = sampleVec(c.pos, f); for (let k = 0; k < 3; k++) t[k] += w * v[k]!; } }\n if (c.rot) {\n q = [0, 0, 0, 0];\n let ref: number[] | null = null;\n for (const [f, w] of taps) {\n if (!w) continue;\n const v = sampleQuat(c.rot, f);\n if (!ref) ref = v;\n const sgn = ref[0]! * v[0]! + ref[1]! * v[1]! + ref[2]! * v[2]! + ref[3]! * v[3]! < 0 ? -w : w;\n for (let k = 0; k < 4; k++) q[k] += sgn * v[k]!;\n }\n const n = Math.hypot(q[0]!, q[1]!, q[2]!, q[3]!) || 1;\n q = q.map((v) => v / n);\n }\n return { t, q };\n}\n\nexport class NodeSkeleton implements SkeletonRuntime {\n readonly n: number;\n readonly nodes: SkeletonNode[];\n readonly parent: number[];\n readonly world: Float32Array;\n readonly skin: Float32Array;\n readonly worldV: Float32Array[];\n readonly skinV: Float32Array[];\n private readonly rest: TRS[];\n private readonly w64: Float64Array[];\n private readonly tmp = new Float64Array(16);\n private readonly tmp2 = new Float64Array(16);\n\n constructor(nodes: SkeletonNode[]) {\n const n = (this.n = nodes.length);\n this.nodes = nodes;\n this.parent = nodes.map((x) => (x.helper ? -1 : x.parent));\n this.rest = nodes.map((nd) => matToTRS(nd.local));\n this.world = new Float32Array(16 * n);\n this.skin = new Float32Array(16 * n);\n const view = (a: Float32Array) => Array.from({ length: n }, (_, i) => a.subarray(16 * i, 16 * i + 16));\n this.worldV = view(this.world);\n this.skinV = view(this.skin);\n this.w64 = Array.from({ length: n }, () => new Float64Array(16));\n }\n\n pose(motion: Motion | null, time: number, loop: boolean): void {\n const m = motion && motion.kind === 'node' ? motion : null;\n const tickOf = (x: { duration: number; tps: number }): number => {\n const d = x.duration, tt = loop ? (d > 0 ? ((time % d) + d) % d : 0) : Math.min(Math.max(time, 0), d);\n return tt * x.tps;\n };\n const tick = m ? tickOf(m) : 0;\n const layers = m?.layers?.length ? m.layers.map((l) => ({ l, tick: l.frame ?? tickOf(l.motion) })) : null;\n // blending: up to 4 neighbouring grid animations with a shared phase\n let taps: Array<{ mo: NodeMotion; w: number; tick: number }> | null = null;\n if (m?.blend?.motions.length) {\n const b = m.blend, cx = Math.max(0, Math.min(b.cols - 1, b.x)), cy = Math.max(0, Math.min(b.rows - 1, b.y));\n const x0 = Math.floor(cx), y0 = Math.floor(cy), x1 = Math.min(b.cols - 1, x0 + 1), y1 = Math.min(b.rows - 1, y0 + 1), fx = cx - x0, fy = cy - y0;\n const d = m.duration, phase = d > 0 ? (loop ? (((time % d) + d) % d) / d : Math.min(Math.max(time / d, 0), 1)) : 0;\n taps = [];\n for (const [i, w] of [[y0 * b.cols + x0, (1 - fx) * (1 - fy)], [y0 * b.cols + x1, fx * (1 - fy)], [y1 * b.cols + x0, (1 - fx) * fy], [y1 * b.cols + x1, fx * fy]] as Array<[number, number]>) {\n const mo = b.motions[i];\n if (!mo || w < 1e-6) continue;\n const prev = taps.find((x) => x.mo === mo);\n if (prev) prev.w += w; else taps.push({ mo, w, tick: phase * mo.duration * mo.tps });\n }\n }\n for (let i = 0; i < this.n; i++) {\n const nd = this.nodes[i]!, ch = m ? m.chan.get(i) : undefined, rest = this.rest[i]!;\n let L: ArrayLike<number> = nd.local;\n if (ch || layers || taps) {\n let t: ArrayLike<number> = ch?.pos ? sampleVec(ch.pos, tick) : rest.t, q: ArrayLike<number> = ch?.rot ? sampleQuat(ch.rot, tick) : rest.q;\n const s = ch?.scl ? sampleVec(ch.scl, tick) : rest.s;\n let touched = !!ch;\n if (taps) {\n const tt = [0, 0, 0], qq = [0, 0, 0, 0];\n let any = false, ref: ArrayLike<number> | null = null;\n for (const { mo, w, tick: bt } of taps) {\n const c = mo.chan.get(i);\n if (c) any = true;\n const p = c?.pos ? sampleVec(c.pos, bt) : rest.t, r = c?.rot ? sampleQuat(c.rot, bt) : rest.q;\n for (let k = 0; k < 3; k++) tt[k] += p[k]! * w;\n ref ??= r;\n const sg = ref[0]! * r[0]! + ref[1]! * r[1]! + ref[2]! * r[2]! + ref[3]! * r[3]! < 0 ? -w : w;\n for (let k = 0; k < 4; k++) qq[k] += r[k]! * sg;\n }\n if (any) { const n = Math.hypot(qq[0]!, qq[1]!, qq[2]!, qq[3]!) || 1; t = tt; q = qq.map((v) => v / n); touched = true; }\n }\n if (layers) for (const { l, tick: lt } of layers) {\n const c = l.motion.chan.get(i);\n if (!c || (l.mask && !l.mask.has(i)) || (l.fill && ch)) continue;\n touched = true;\n if (l.grid && !c.add) { const g = gridSample(c, l.grid); if (g.t) t = g.t; if (g.q) q = g.q; continue; }\n if (c.add) {\n if (c.add.pos) { const d = sampleVec(c.add.pos, lt); t = [t[0]! + d[0]!, t[1]! + d[1]!, t[2]! + d[2]!]; }\n if (c.add.rot) q = qmul(q, sampleQuat(c.add.rot, lt));\n } else {\n if (c.pos) t = sampleVec(c.pos, lt);\n if (c.rot) q = sampleQuat(c.rot, lt);\n }\n }\n if (touched) { compose(t, q, s, this.tmp); L = this.tmp; }\n }\n const W = this.w64[i]!;\n if (nd.parent < 0) W.set(L); else mulColInto(this.w64[nd.parent]!, L, W);\n this.worldV[i]!.set(W);\n if (nd.offset) { mulColInto(W, nd.offset, this.tmp2); this.skinV[i]!.set(this.tmp2); }\n else this.skinV[i]!.set(W);\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/surface.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { DecodedImage, SurfaceInfo } from './types';
|
|
2
|
-
/**
|
|
2
|
+
/** Packed maps derived from the diffuse name (if the material does not name them): ORM/RMA/MRA and separate _roughness/_metallic/_ao. */
|
|
3
3
|
export declare function surfaceCandidates(diffuse: string): SurfaceInfo[];
|
|
4
|
-
/**
|
|
4
|
+
/** All surface map references (unique paths). */
|
|
5
5
|
export declare function surfaceMaps(s: SurfaceInfo): string[];
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Combine maps into one RGBA (AO, roughness, metalness, specular). images: decoded maps by path.
|
|
8
|
+
* The size is the largest map's; smaller ones are scaled with nearest neighbour. With no maps at all, 1×1 from the multipliers.
|
|
9
9
|
*/
|
|
10
10
|
export declare function composeSurface(s: SurfaceInfo, images: Map<string, DecodedImage>): DecodedImage;
|
package/dist/surface.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"surface.js","sources":["../src/surface.ts"],"sourcesContent":["// PBR
|
|
1
|
+
{"version":3,"file":"surface.js","sources":["../src/surface.ts"],"sourcesContent":["// PBR surface: roughness/metalness/AO/specular maps are combined into one texture for the shader:\n// R = AO, G = roughness, B = metalness, A = non-metal specular strength (128 ≈ F0 0.04).\nimport { baseName } from './binary';\nimport type { ChannelRef, DecodedImage, SurfaceInfo } from './types';\n\n/** Packed maps derived from the diffuse name (if the material does not name them): ORM/RMA/MRA and separate _roughness/_metallic/_ao. */\nexport function surfaceCandidates(diffuse: string): SurfaceInfo[] {\n if (!diffuse) return [];\n const p = diffuse.replace(/\\\\/g, '/'), dir = p.slice(0, p.lastIndexOf('/') + 1);\n const stem = baseName(p).replace(/\\.(dds|tga|png|jpe?g|paa|pac|pct|vtf|bmp|pcx|gif|webp|wal|xbt)$/i, '').toLowerCase().replace(/_(d|diff|diffuse|albedo|basecolor|base_color|color|col|c|co|b\\.color)$/, '');\n const at = (s: string) => dir + stem + s;\n const ch = (map: string, channel: ChannelRef['channel']): ChannelRef => ({ map, channel });\n return [\n { ao: ch(at('_orm'), 0), roughness: ch(at('_orm'), 1), metallic: ch(at('_orm'), 2), source: '_orm' }, // glTF / UE\n { roughness: ch(at('_rma'), 0), metallic: ch(at('_rma'), 1), ao: ch(at('_rma'), 2), source: '_rma' },\n { metallic: ch(at('_mra'), 0), roughness: ch(at('_mra'), 1), ao: ch(at('_mra'), 2), source: '_mra' },\n { roughness: ch(at('_roughness'), 0), metallic: ch(at('_metallic'), 0), ao: ch(at('_ao'), 0), source: '_roughness' },\n { roughness: ch(at('_rough'), 0), metallic: ch(at('_metal'), 0), source: '_rough' },\n { specular: ch(at('_smdi'), 1), gloss: ch(at('_smdi'), 2), source: 'Arma _smdi' },\n ];\n}\n\nconst KEYS = ['ao', 'roughness', 'gloss', 'metallic', 'specular'] as const;\n\n/** All surface map references (unique paths). */\nexport function surfaceMaps(s: SurfaceInfo): string[] {\n return [...new Set(KEYS.map((k) => s[k]?.map).filter((m): m is string => !!m && !m.startsWith('#')))];\n}\n\n/**\n * Combine maps into one RGBA (AO, roughness, metalness, specular). images: decoded maps by path.\n * The size is the largest map's; smaller ones are scaled with nearest neighbour. With no maps at all, 1×1 from the multipliers.\n */\nexport function composeSurface(s: SurfaceInfo, images: Map<string, DecodedImage>): DecodedImage {\n let w = 1, h = 1;\n for (const img of images.values()) if (img.width * img.height > w * h) { w = img.width; h = img.height; }\n const out = new Uint8Array(w * h * 4);\n const rf = Math.round(255 * Math.min(1, Math.max(0, s.roughnessFactor ?? 1))), mf = Math.round(255 * Math.min(1, Math.max(0, s.metallicFactor ?? 1)));\n const sample = (ref: ChannelRef | undefined, x: number, y: number): number | null => {\n const img = ref ? (ref.image ?? images.get(ref.map)) : undefined;\n if (!ref || !img?.rgba) return null;\n const sx = Math.min(img.width - 1, ((x * img.width) / w) | 0), sy = Math.min(img.height - 1, ((y * img.height) / h) | 0);\n return img.rgba[(sy * img.width + sx) * 4 + ref.channel]!;\n };\n const hasRough = !!(s.roughness || s.gloss), hasMetal = !!s.metallic;\n for (let y = 0; y < h; y++)\n for (let x = 0; x < w; x++) {\n const o = (y * w + x) * 4;\n const ao = sample(s.ao, x, y), r = sample(s.roughness, x, y), g = sample(s.gloss, x, y), m = sample(s.metallic, x, y), sp = sample(s.specular, x, y);\n out[o] = ao ?? 255;\n // a glTF multiplier multiplies the map; without a map, the multiplier itself (default \"medium\" roughness 0.6)\n out[o + 1] = r !== null ? (r * rf) / 255 : g !== null ? 255 - g : hasRough ? rf : s.roughnessFactor !== undefined ? rf : 153;\n out[o + 2] = m !== null ? (m * mf) / 255 : hasMetal ? mf : s.metallicFactor !== undefined ? mf : 0;\n out[o + 3] = sp ?? (s.specularFactor !== undefined ? Math.round(255 * Math.min(1, Math.max(0, s.specularFactor))) : 128);\n }\n return { width: w, height: h, rgba: out };\n}\n"],"names":[],"mappings":"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"}
|
package/dist/textures.d.ts
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
import type { Archive, ArchiveEntry, BlobFile, DecodedImage, VFile } from './types';
|
|
2
|
-
/**
|
|
2
|
+
/** Where to take a texture from: a file (decoded by extension) or an already decoded image. */
|
|
3
3
|
export type TextureSource = VFile;
|
|
4
4
|
export interface ImageDecoder {
|
|
5
5
|
decodeImage(file: VFile): Promise<DecodedImage>;
|
|
6
6
|
}
|
|
7
7
|
export declare class TextureLibrary {
|
|
8
8
|
private readonly decoder;
|
|
9
|
-
/**
|
|
9
|
+
/** Live list of the session's archives. */
|
|
10
10
|
readonly archives: readonly Archive[];
|
|
11
11
|
private readonly loose;
|
|
12
12
|
private readonly dict;
|
|
13
13
|
private readonly cache;
|
|
14
14
|
private memoMap;
|
|
15
15
|
constructor(decoder: ImageDecoder,
|
|
16
|
-
/**
|
|
16
|
+
/** Live list of the session's archives. */
|
|
17
17
|
archives: readonly Archive[]);
|
|
18
|
-
/**
|
|
18
|
+
/** Separate texture files (keys: path without extension and file name). */
|
|
19
19
|
addFiles(files: readonly BlobFile[]): void;
|
|
20
|
-
/**
|
|
20
|
+
/** Texture from a dictionary (TXD) by name without extension. */
|
|
21
21
|
addDecoded(name: string, image: DecodedImage, origin?: string): void;
|
|
22
22
|
get looseCount(): number;
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* Key variants for a name from a model: the normalized path without extension and its "tails".
|
|
25
|
+
* "P:/Doom/base/textures/x/y.tga" (an artist's absolute path) -> …, "base/textures/x/y", "textures/x/y", "x/y".
|
|
26
26
|
*/
|
|
27
27
|
static candidates(name: string): string[];
|
|
28
|
-
/**
|
|
28
|
+
/** Find a texture by a name/path from the model: dictionaries -> archives -> separate files; then by path "tails". */
|
|
29
29
|
find(name: string | undefined | null): TextureSource | null;
|
|
30
|
-
/**
|
|
30
|
+
/** File name without extension -> archive entry (first seen), for finding textures without a path. */
|
|
31
31
|
private baseIndex;
|
|
32
|
-
/**
|
|
32
|
+
/** Find a file by path in all archives (falling back to no materials/ prefix). */
|
|
33
33
|
findEntry(path: string): [Archive, ArchiveEntry] | null;
|
|
34
|
-
/**
|
|
34
|
+
/** Find a file by name (without folder) in the "Game folder" indexes. */
|
|
35
35
|
findByBase(base: string): [Archive, ArchiveEntry] | null;
|
|
36
36
|
readText(hit: [Archive, ArchiveEntry]): Promise<string>;
|
|
37
|
-
/**
|
|
37
|
+
/** Cache of derived data (parsed materials and the like), cleared when archives change. */
|
|
38
38
|
memo<T>(key: string, make: () => Promise<T>): Promise<T>;
|
|
39
39
|
invalidate(): void;
|
|
40
40
|
decode(src: TextureSource): Promise<DecodedImage>;
|
package/dist/textures.js
CHANGED
|
@@ -10,14 +10,14 @@ class TextureLibrary {
|
|
|
10
10
|
dict = /* @__PURE__ */ new Map();
|
|
11
11
|
cache = /* @__PURE__ */ new WeakMap();
|
|
12
12
|
memoMap = /* @__PURE__ */ new Map();
|
|
13
|
-
/**
|
|
13
|
+
/** Separate texture files (keys: path without extension and file name). */
|
|
14
14
|
addFiles(files) {
|
|
15
15
|
for (const f of files) {
|
|
16
16
|
const key = stripExt(normPath(f.webkitRelativePath || f.name));
|
|
17
17
|
this.loose.push({ file: f, key, base: key.split("/").pop() });
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/** Texture from a dictionary (TXD) by name without extension. */
|
|
21
21
|
addDecoded(name, image, origin = "dict") {
|
|
22
22
|
this.dict.set(name.toLowerCase(), { name: `${name}.${origin}`, decoded: image, arrayBuffer: async () => new ArrayBuffer(0) });
|
|
23
23
|
}
|
|
@@ -25,8 +25,8 @@ class TextureLibrary {
|
|
|
25
25
|
return this.loose.length;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* Key variants for a name from a model: the normalized path without extension and its "tails".
|
|
29
|
+
* "P:/Doom/base/textures/x/y.tga" (an artist's absolute path) -> …, "base/textures/x/y", "textures/x/y", "x/y".
|
|
30
30
|
*/
|
|
31
31
|
static candidates(name) {
|
|
32
32
|
const n = stripExt(normPath(name)).replace(/^[a-z]:\//, "").replace(/^(\.\/)+/, "").replace(/^\/+/, "");
|
|
@@ -41,7 +41,7 @@ class TextureLibrary {
|
|
|
41
41
|
}
|
|
42
42
|
return [...new Set(out)];
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
44
|
+
/** Find a texture by a name/path from the model: dictionaries -> archives -> separate files; then by path "tails". */
|
|
45
45
|
find(name) {
|
|
46
46
|
if (!name) return null;
|
|
47
47
|
const cands = TextureLibrary.candidates(name), full = cands.includes(stripExt(normPath(name))) ? stripExt(normPath(name)) : cands[0];
|
|
@@ -67,7 +67,7 @@ class TextureLibrary {
|
|
|
67
67
|
const hit = this.baseIndex().get(base);
|
|
68
68
|
return hit ? hit[0].fileOf(hit[1]) : null;
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
70
|
+
/** File name without extension -> archive entry (first seen), for finding textures without a path. */
|
|
71
71
|
baseIndex() {
|
|
72
72
|
const key = "\0baseIndex:" + this.archives.length;
|
|
73
73
|
let m = this.memoMap.get(key);
|
|
@@ -82,7 +82,7 @@ class TextureLibrary {
|
|
|
82
82
|
}
|
|
83
83
|
return m;
|
|
84
84
|
}
|
|
85
|
-
/**
|
|
85
|
+
/** Find a file by path in all archives (falling back to no materials/ prefix). */
|
|
86
86
|
findEntry(path) {
|
|
87
87
|
for (const p of [path, path.replace(/^materials\//, "")])
|
|
88
88
|
for (const a of this.archives) {
|
|
@@ -91,7 +91,7 @@ class TextureLibrary {
|
|
|
91
91
|
}
|
|
92
92
|
return null;
|
|
93
93
|
}
|
|
94
|
-
/**
|
|
94
|
+
/** Find a file by name (without folder) in the "Game folder" indexes. */
|
|
95
95
|
findByBase(base) {
|
|
96
96
|
for (const a of this.archives) {
|
|
97
97
|
const e = a.byBase?.get(base);
|
|
@@ -102,7 +102,7 @@ class TextureLibrary {
|
|
|
102
102
|
async readText(hit) {
|
|
103
103
|
return decodeText(await hit[0].read(hit[1]));
|
|
104
104
|
}
|
|
105
|
-
/**
|
|
105
|
+
/** Cache of derived data (parsed materials and the like), cleared when archives change. */
|
|
106
106
|
async memo(key, make) {
|
|
107
107
|
if (this.memoMap.has(key)) return this.memoMap.get(key);
|
|
108
108
|
const v = await make();
|
package/dist/textures.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textures.js","sources":["../src/textures.ts"],"sourcesContent":["//
|
|
1
|
+
{"version":3,"file":"textures.js","sources":["../src/textures.ts"],"sourcesContent":["// Texture library: open archives, separate files, dictionaries (TXD) and a cache of decoded images.\nimport { decodeText, normPath, stripExt } from './binary';\nimport type { Archive, ArchiveEntry, BlobFile, DecodedImage, VFile } from './types';\n\n/** Where to take a texture from: a file (decoded by extension) or an already decoded image. */\nexport type TextureSource = VFile;\n\nexport interface ImageDecoder {\n decodeImage(file: VFile): Promise<DecodedImage>;\n}\n\ninterface LooseTexture { file: BlobFile; key: string; base: string }\n\nexport class TextureLibrary {\n private readonly loose: LooseTexture[] = [];\n private readonly dict = new Map<string, VFile>();\n private readonly cache = new WeakMap<object, DecodedImage>();\n private memoMap = new Map<string, unknown>();\n\n constructor(\n private readonly decoder: ImageDecoder,\n /** Live list of the session's archives. */\n readonly archives: readonly Archive[],\n ) {}\n\n /** Separate texture files (keys: path without extension and file name). */\n addFiles(files: readonly BlobFile[]): void {\n for (const f of files) {\n const key = stripExt(normPath(f.webkitRelativePath || f.name));\n this.loose.push({ file: f, key, base: key.split('/').pop()! });\n }\n }\n\n /** Texture from a dictionary (TXD) by name without extension. */\n addDecoded(name: string, image: DecodedImage, origin = 'dict'): void {\n this.dict.set(name.toLowerCase(), { name: `${name}.${origin}`, decoded: image, arrayBuffer: async () => new ArrayBuffer(0) });\n }\n\n get looseCount(): number { return this.loose.length; }\n\n /**\n * Key variants for a name from a model: the normalized path without extension and its \"tails\".\n * \"P:/Doom/base/textures/x/y.tga\" (an artist's absolute path) -> …, \"base/textures/x/y\", \"textures/x/y\", \"x/y\".\n */\n static candidates(name: string): string[] {\n const n = stripExt(normPath(name)).replace(/^[a-z]:\\//, '').replace(/^(\\.\\/)+/, '').replace(/^\\/+/, '');\n const parts = n.split('/'), out: string[] = [];\n for (let i = 0; i < parts.length; i++) {\n const k = parts.slice(i).join('/');\n if (i > 0 && parts[i - 1] === 'data') { out.unshift(k); continue; } // Bethesda \"Data\\Textures\\…\": drop data/ right away\n out.push(k);\n }\n return [...new Set(out)];\n }\n\n /** Find a texture by a name/path from the model: dictionaries -> archives -> separate files; then by path \"tails\". */\n find(name: string | undefined | null): TextureSource | null {\n if (!name) return null;\n const cands = TextureLibrary.candidates(name), full = cands.includes(stripExt(normPath(name))) ? stripExt(normPath(name)) : cands[0]!;\n const base = full.split('/').pop()!;\n const d = this.dict.get(base);\n if (d) return d;\n for (const n of cands) {\n for (const a of this.archives) {\n const e = a.byKey.get(n) ?? a.byKey.get('textures/' + n) ?? a.byKey.get('models/' + n);\n if (e) return a.fileOf(e);\n }\n if (n.includes('/') || n === base) {\n const t = this.loose.find((x) => x.key === n) ?? this.loose.find((x) => x.key.endsWith('/' + n));\n if (t) return t.file;\n }\n }\n const t = this.loose.find((x) => x.base === base);\n if (t) return t.file;\n // archives where not all names are known (path hashes): by the exact path from the model\n for (const a of this.archives) { const e = a.findTexture?.(name); if (e) return a.fileOf(e); }\n // last chance: by file name in any archive (ZIP/PAK store only full paths: prebuild/textures/pct/…)\n const hit = this.baseIndex().get(base);\n return hit ? hit[0].fileOf(hit[1]) : null;\n }\n\n /** File name without extension -> archive entry (first seen), for finding textures without a path. */\n private baseIndex(): Map<string, [Archive, ArchiveEntry]> {\n const key = '\\0baseIndex:' + this.archives.length;\n let m = this.memoMap.get(key) as Map<string, [Archive, ArchiveEntry]> | undefined;\n if (!m) {\n m = new Map();\n for (const a of this.archives)\n for (const [k, e] of a.byKey) { const b = k.slice(k.lastIndexOf('/') + 1); if (!m.has(b)) m.set(b, [a, e]); }\n this.memoMap.set(key, m);\n }\n return m;\n }\n\n /** Find a file by path in all archives (falling back to no materials/ prefix). */\n findEntry(path: string): [Archive, ArchiveEntry] | null {\n for (const p of [path, path.replace(/^materials\\//, '')])\n for (const a of this.archives) {\n const e = a.lookup(p);\n if (e) return [a, e];\n }\n return null;\n }\n\n /** Find a file by name (without folder) in the \"Game folder\" indexes. */\n findByBase(base: string): [Archive, ArchiveEntry] | null {\n for (const a of this.archives) {\n const e = a.byBase?.get(base);\n if (e) return [a, e];\n }\n return null;\n }\n\n async readText(hit: [Archive, ArchiveEntry]): Promise<string> {\n return decodeText(await hit[0].read(hit[1]));\n }\n\n /** Cache of derived data (parsed materials and the like), cleared when archives change. */\n async memo<T>(key: string, make: () => Promise<T>): Promise<T> {\n if (this.memoMap.has(key)) return this.memoMap.get(key) as T;\n const v = await make();\n this.memoMap.set(key, v);\n return v;\n }\n\n invalidate(): void { this.memoMap = new Map(); }\n\n async decode(src: TextureSource): Promise<DecodedImage> {\n if (src.decoded) return src.decoded;\n let img = this.cache.get(src);\n if (!img) { img = await this.decoder.decodeImage(src); this.cache.set(src, img); }\n return img;\n }\n}\n"],"names":["t"],"mappings":"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"}
|