@gmvjs/core 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +41 -35
  2. package/dist/archives/common.d.ts +9 -9
  3. package/dist/archives/common.js.map +1 -1
  4. package/dist/archives/folder.d.ts +1 -1
  5. package/dist/archives/folder.js +1 -1
  6. package/dist/archives/folder.js.map +1 -1
  7. package/dist/archives/zip.js.map +1 -1
  8. package/dist/binary.d.ts +7 -7
  9. package/dist/binary.js.map +1 -1
  10. package/dist/builtin-en.js.map +1 -1
  11. package/dist/builtin.js +1 -1
  12. package/dist/builtin.js.map +1 -1
  13. package/dist/codecs/zlib.d.ts +2 -2
  14. package/dist/codecs/zlib.js.map +1 -1
  15. package/dist/diagnostics.js.map +1 -1
  16. package/dist/geometry.d.ts +5 -5
  17. package/dist/geometry.js.map +1 -1
  18. package/dist/i18n.d.ts +4 -4
  19. package/dist/i18n.js.map +1 -1
  20. package/dist/images/bc45.d.ts +1 -1
  21. package/dist/images/bc45.js.map +1 -1
  22. package/dist/images/bc7.d.ts +1 -1
  23. package/dist/images/bc7.js.map +1 -1
  24. package/dist/images/dds.d.ts +1 -1
  25. package/dist/images/dds.js.map +1 -1
  26. package/dist/images/pcx.js.map +1 -1
  27. package/dist/images/tga.js.map +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/levelbuilder.d.ts +3 -3
  31. package/dist/levelbuilder.js +1 -1
  32. package/dist/levelbuilder.js.map +1 -1
  33. package/dist/math.d.ts +6 -6
  34. package/dist/math.js.map +1 -1
  35. package/dist/normals.d.ts +5 -5
  36. package/dist/normals.js.map +1 -1
  37. package/dist/plugin.d.ts +57 -57
  38. package/dist/plugin.js.map +1 -1
  39. package/dist/registry.d.ts +14 -14
  40. package/dist/registry.js +13 -13
  41. package/dist/registry.js.map +1 -1
  42. package/dist/scene.d.ts +14 -14
  43. package/dist/scene.js.map +1 -1
  44. package/dist/session.d.ts +34 -34
  45. package/dist/session.js +33 -33
  46. package/dist/session.js.map +1 -1
  47. package/dist/skeleton.d.ts +3 -3
  48. package/dist/skeleton.js.map +1 -1
  49. package/dist/surface.d.ts +4 -4
  50. package/dist/surface.js.map +1 -1
  51. package/dist/textures.d.ts +12 -12
  52. package/dist/textures.js +9 -9
  53. package/dist/textures.js.map +1 -1
  54. package/dist/types.d.ts +103 -103
  55. package/dist/types.js.map +1 -1
  56. package/dist/writers/dds.d.ts +1 -1
  57. package/dist/writers/dds.js.map +1 -1
  58. package/dist/writers/obj.js.map +1 -1
  59. package/dist/writers/zip.js.map +1 -1
  60. package/package.json +3 -3
@@ -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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"}
@@ -2,11 +2,11 @@ import type { Motion, SkeletonRuntime } from './types';
2
2
  export interface SkeletonNode {
3
3
  name: string;
4
4
  parent: number;
5
- /** Локальная матрица (column-major). */
5
+ /** Local matrix (column-major). */
6
6
  local: ArrayLike<number>;
7
- /** Обратная bind-матрица кости (offset); null — узел не кость, skin = world. */
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
- /** Служебный узел (меш, bind-смещение): не рисуется линией скелета. */
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 {
@@ -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
- /** Упакованные карты по имени диффузной (если материал их не указал): ORM/RMA/MRA и отдельные _roughness/_metallic/_ao. */
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
- * Свести карты в одну RGBA (AO, шероховатость, металличность, блик). images — декодированные карты по пути.
8
- * Размер — наибольший из карт; меньшие масштабируются ближайшим соседом. Нет ни одной карты — 1×1 из множителей.
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;
@@ -1 +1 @@
1
- {"version":3,"file":"surface.js","sources":["../src/surface.ts"],"sourcesContent":["// PBR-поверхность: карты шероховатости/металличности/AO/блика сводятся в одну текстуру для шейдера:\n// R — AO, G — шероховатость, B — металличность, A — сила блика неметалла (128 ≈ F0 0.04).\nimport { baseName } from './binary';\nimport type { ChannelRef, DecodedImage, SurfaceInfo } from './types';\n\n/** Упакованные карты по имени диффузной (если материал их не указал): ORM/RMA/MRA и отдельные _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/** Все ссылки на карты поверхности (уникальные пути). */\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 * Свести карты в одну RGBA (AO, шероховатость, металличность, блик). images — декодированные карты по пути.\n * Размер — наибольший из карт; меньшие масштабируются ближайшим соседом. Нет ни одной карты — 1×1 из множителей.\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 // множитель glTF умножает карту; без карты — сам множитель (по умолчанию «средняя» шероховатость 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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"}
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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"}
@@ -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
- /** Текстура из словаря (TXD) по имени без расширения. */
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
- * «P:/Doom/base/textures/x/y.tga» (абсолютный путь художника) -> …, «base/textures/x/y», «textures/x/y», «x/y».
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
- /** Найти файл по пути во всех архивах (с запасным вариантом без префикса materials/). */
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
- /** Текстура из словаря (TXD) по имени без расширения. */
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
- * «P:/Doom/base/textures/x/y.tga» (абсолютный путь художника) -> …, «base/textures/x/y», «textures/x/y», «x/y».
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
- /** Найти файл по пути во всех архивах (с запасным вариантом без префикса materials/). */
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();
@@ -1 +1 @@
1
- {"version":3,"file":"textures.js","sources":["../src/textures.ts"],"sourcesContent":["// Библиотека текстур: открытые архивы, отдельные файлы, словари (TXD) и кэш декодированных картинок.\nimport { decodeText, normPath, stripExt } from './binary';\nimport type { Archive, ArchiveEntry, BlobFile, DecodedImage, VFile } from './types';\n\n/** Откуда брать текстуру: файл (декодируется по расширению) или уже декодированная картинка. */\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 /** Живой список архивов сессии. */\n readonly archives: readonly Archive[],\n ) {}\n\n /** Отдельные файлы текстур (ключи — путь без расширения и имя файла). */\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 /** Текстура из словаря (TXD) по имени без расширения. */\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 * Варианты ключа для имени из модели: нормализованный путь без расширения и его «хвосты».\n * «P:/Doom/base/textures/x/y.tga» (абсолютный путь художника) -> …, «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; } // «Data\\Textures\\…» Bethesda — сразу без data/\n out.push(k);\n }\n return [...new Set(out)];\n }\n\n /** Найти текстуру по имени/пути из модели: словари -> архивы -> отдельные файлы; затем — по «хвостам» пути. */\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 // архивы, где имена известны не все (хеши путей): по точному пути из модели\n for (const a of this.archives) { const e = a.findTexture?.(name); if (e) return a.fileOf(e); }\n // последний шанс: по имени файла в любом архиве (ZIP/PAK хранят только полные пути: prebuild/textures/pct/…)\n const hit = this.baseIndex().get(base);\n return hit ? hit[0].fileOf(hit[1]) : null;\n }\n\n /** Имя файла без расширения -> запись архива (первая встреченная), для поиска текстур без пути. */\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 /** Найти файл по пути во всех архивах (с запасным вариантом без префикса materials/). */\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 /** Найти файл по имени (без папки) в индексах «Папки игры». */\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 /** Кэш производных данных (разобранные материалы и т. п.), сбрасывается при смене архивов. */\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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"}
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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"}