reze-engine 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,13 +43,14 @@ engine.runRenderLoop();
43
43
 
44
44
  ## API
45
45
 
46
- One WebGPU **Engine** per page (singleton after `init()`). Load models via `engine.loadModel(path)` or `engine.loadModel(name, path)`.
46
+ One WebGPU **Engine** per page (singleton after `init()`). Load models via URL **or** from a user-selected folder (see [Local folder uploads](#local-folder-uploads-browser)).
47
47
 
48
48
  ### Engine
49
49
 
50
50
  ```javascript
51
51
  engine.init()
52
52
  engine.loadModel(name, path)
53
+ engine.loadModel(name, { files, pmxFile? }) // folder upload — see below
53
54
  engine.getModel(name)
54
55
  engine.getModelNames()
55
56
  engine.removeModel(name)
@@ -75,6 +76,38 @@ engine.getStats()
75
76
  engine.dispose()
76
77
  ```
77
78
 
79
+ ### Local folder uploads (browser)
80
+
81
+ Use a hidden `<input type="file" webkitdirectory multiple>` (or drag/drop) and pass the resulting `FileList` or `File[]` into the engine. Textures resolve relative to the chosen PMX file inside that tree.
82
+
83
+ **Important:** read `input.files` into a normal array **before** setting `input.value = ""`. The browser’s `FileList` is *live* — clearing the input empties it.
84
+
85
+ 1. **`parsePmxFolderInput(fileList)`** — returns a tagged result (`empty` | `not_directory` | `no_pmx` | `single` | `multiple`). For `single`, you already have `files` and `pmxFile`. For `multiple`, show a picker (dropdown) of `pmxRelativePaths`, then resolve with **`pmxFileAtRelativePath(files, path)`**.
86
+ 2. **`engine.loadModel(name, { files, pmxFile })`** — `pmxFile` selects which `.pmx` when the folder contains several.
87
+
88
+ ```javascript
89
+ import { Engine, parsePmxFolderInput, pmxFileAtRelativePath } from "reze-engine";
90
+
91
+ // In <input onChange>:
92
+ const picked = parsePmxFolderInput(e.target.files);
93
+ e.target.value = "";
94
+
95
+ if (picked.status === "single") {
96
+ const model = await engine.loadModel("myModel", {
97
+ files: picked.files,
98
+ pmxFile: picked.pmxFile,
99
+ });
100
+ }
101
+
102
+ if (picked.status === "multiple") {
103
+ // Let the user choose `chosenPath` from picked.pmxRelativePaths, then:
104
+ const pmxFile = pmxFileAtRelativePath(picked.files, chosenPath);
105
+ const model = await engine.loadModel("myModel", { files: picked.files, pmxFile });
106
+ }
107
+ ```
108
+
109
+ VMD and other assets still load by URL when the path starts with `/` or `http(s):`; relative paths are resolved against the PMX directory inside the upload.
110
+
78
111
  ### Model
79
112
 
80
113
  ```javascript
@@ -108,12 +141,12 @@ model.getBoneWorldPosition(name)
108
141
  `model.exportVmd(name)` serialises a loaded clip back to the VMD binary format and returns an `ArrayBuffer`. Bone and morph names are Shift-JIS encoded for compatibility with standard MMD tools.
109
142
 
110
143
  ```javascript
111
- const buffer = model.exportVmd("idle")
112
- const blob = new Blob([buffer], { type: "application/octet-stream" })
113
- const link = document.createElement("a")
114
- link.href = URL.createObjectURL(blob)
115
- link.download = "idle.vmd"
116
- link.click()
144
+ const buffer = model.exportVmd("idle");
145
+ const blob = new Blob([buffer], { type: "application/octet-stream" });
146
+ const link = document.createElement("a");
147
+ link.href = URL.createObjectURL(blob);
148
+ link.download = "idle.vmd";
149
+ link.click();
117
150
  ```
118
151
 
119
152
  #### Playback
@@ -149,6 +182,7 @@ Call `model.play(name, options?)` to start or switch motion. `loop: true` makes
149
182
 
150
183
  ## Projects Using This Engine
151
184
 
185
+ - **[Reze Studio](https://reze.studio)** - Web-native MMD animation editor
152
186
  - **[MiKaPo](https://mikapo.vercel.app)** — Real-time motion capture for MMD
153
187
  - **[Popo](https://popo.love)** — LLM-generated MMD poses
154
188
  - **[MPL](https://mmd-mpl.vercel.app)** — Motion programming language for MMD
@@ -0,0 +1,16 @@
1
+ /** Unified binary I/O for PMX/VMD/textures: HTTP(s) or user folder (File map). */
2
+ export type AssetReader = {
3
+ readBinary(logicalPath: string): Promise<ArrayBuffer>;
4
+ };
5
+ /** Normalize PMX-style paths: backslashes, trim, strip leading ./ */
6
+ export declare function normalizeAssetPath(p: string): string;
7
+ /** Join PMX directory prefix and texture-relative path (both may be ""). */
8
+ export declare function joinAssetPath(baseDir: string, relative: string): string;
9
+ /** Same rules as the original engine string split: supports absolute site paths like `/models/a/b.pmx`. */
10
+ export declare function deriveBasePathFromPmxPath(pmxPath: string): string;
11
+ export declare function createFetchAssetReader(): AssetReader;
12
+ /** Keys must be normalized paths relative to the selected folder root (see fileListToMap). */
13
+ export declare function createFileMapAssetReader(files: Map<string, File>): AssetReader;
14
+ export declare function fileListToMap(files: FileList | File[]): Map<string, File>;
15
+ export declare function findFirstPmxFileInList(files: FileList | File[]): File | null;
16
+ //# sourceMappingURL=asset-reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-reader.d.ts","sourceRoot":"","sources":["../src/asset-reader.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAElF,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;CACtD,CAAA;AAED,qEAAqE;AACrE,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAIpD;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMvE;AAED,2GAA2G;AAC3G,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIjE;AAED,wBAAgB,sBAAsB,IAAI,WAAW,CAQpD;AAED,8FAA8F;AAC9F,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,WAAW,CAkB9E;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAOzE;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,EAAE,GAAG,IAAI,GAAG,IAAI,CAS5E"}
@@ -0,0 +1,74 @@
1
+ /** Unified binary I/O for PMX/VMD/textures: HTTP(s) or user folder (File map). */
2
+ /** Normalize PMX-style paths: backslashes, trim, strip leading ./ */
3
+ export function normalizeAssetPath(p) {
4
+ let s = p.replace(/\\/g, "/").trim();
5
+ if (s.startsWith("./"))
6
+ s = s.slice(2);
7
+ return s;
8
+ }
9
+ /** Join PMX directory prefix and texture-relative path (both may be ""). */
10
+ export function joinAssetPath(baseDir, relative) {
11
+ const rel = normalizeAssetPath(relative);
12
+ if (!rel)
13
+ return normalizeAssetPath(baseDir);
14
+ const base = baseDir.endsWith("/") ? baseDir.slice(0, -1) : baseDir;
15
+ if (!base)
16
+ return rel;
17
+ return `${base}/${rel}`;
18
+ }
19
+ /** Same rules as the original engine string split: supports absolute site paths like `/models/a/b.pmx`. */
20
+ export function deriveBasePathFromPmxPath(pmxPath) {
21
+ const pathParts = pmxPath.replace(/\\/g, "/").split("/");
22
+ pathParts.pop();
23
+ return pathParts.join("/") + (pathParts.length > 0 ? "/" : "");
24
+ }
25
+ export function createFetchAssetReader() {
26
+ return {
27
+ async readBinary(logicalPath) {
28
+ const r = await fetch(logicalPath);
29
+ if (!r.ok)
30
+ throw new Error(`Failed to fetch ${logicalPath}: ${r.status} ${r.statusText}`);
31
+ return r.arrayBuffer();
32
+ },
33
+ };
34
+ }
35
+ /** Keys must be normalized paths relative to the selected folder root (see fileListToMap). */
36
+ export function createFileMapAssetReader(files) {
37
+ return {
38
+ async readBinary(logicalPath) {
39
+ const key = normalizeAssetPath(logicalPath);
40
+ let file = files.get(key);
41
+ if (!file) {
42
+ const lower = key.toLowerCase();
43
+ for (const [k, f] of files) {
44
+ if (k.toLowerCase() === lower) {
45
+ file = f;
46
+ break;
47
+ }
48
+ }
49
+ }
50
+ if (!file)
51
+ throw new Error(`Missing file in folder: ${key}`);
52
+ return file.arrayBuffer();
53
+ },
54
+ };
55
+ }
56
+ export function fileListToMap(files) {
57
+ const m = new Map();
58
+ for (const f of Array.from(files)) {
59
+ const rel = f.webkitRelativePath ?? f.name;
60
+ m.set(normalizeAssetPath(rel), f);
61
+ }
62
+ return m;
63
+ }
64
+ export function findFirstPmxFileInList(files) {
65
+ const list = Array.from(files).filter((f) => f.name.toLowerCase().endsWith(".pmx"));
66
+ if (list.length === 0)
67
+ return null;
68
+ list.sort((a, b) => {
69
+ const pa = a.webkitRelativePath ?? a.name;
70
+ const pb = b.webkitRelativePath ?? b.name;
71
+ return pa.localeCompare(pb);
72
+ });
73
+ return list[0] ?? null;
74
+ }
package/dist/engine.d.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import { Vec3 } from "./math";
2
2
  import { Model } from "./model";
3
3
  import { type PhysicsOptions } from "./physics";
4
+ import { type AssetReader } from "./asset-reader";
4
5
  export type RaycastCallback = (modelName: string, material: string | null, screenX: number, screenY: number) => void;
6
+ /** Select a folder (webkitdirectory) and pass FileList or File[]; pmxFile picks which .pmx when several exist. */
7
+ export type LoadModelFromFilesOptions = {
8
+ files: FileList | File[];
9
+ pmxFile?: File;
10
+ };
5
11
  export type EngineOptions = {
6
12
  ambientColor?: Vec3;
7
13
  directionalLightIntensity?: number;
@@ -154,7 +160,8 @@ export declare class Engine {
154
160
  dispose(): void;
155
161
  loadModel(path: string): Promise<Model>;
156
162
  loadModel(name: string, path: string): Promise<Model>;
157
- addModel(model: Model, pmxPath: string, name?: string): Promise<string>;
163
+ loadModel(name: string, options: LoadModelFromFilesOptions): Promise<Model>;
164
+ addModel(model: Model, pmxPath: string, name?: string, assetReader?: AssetReader): Promise<string>;
158
165
  removeModel(name: string): void;
159
166
  getModelNames(): string[];
160
167
  getModel(name: string): Model | null;
@@ -178,7 +185,7 @@ export declare class Engine {
178
185
  private createMaterialUniformBuffer;
179
186
  private createUniformBuffer;
180
187
  private shouldRenderDrawCall;
181
- private createTextureFromPath;
188
+ private createTextureFromLogicalPath;
182
189
  private renderGround;
183
190
  private handleCanvasDoubleClick;
184
191
  private handleCanvasTouch;
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
package/dist/engine.js CHANGED
@@ -2,6 +2,7 @@ import { Camera } from "./camera";
2
2
  import { Mat4, Vec3 } from "./math";
3
3
  import { PmxLoader } from "./pmx-loader";
4
4
  import { Physics } from "./physics";
5
+ import { createFetchAssetReader, createFileMapAssetReader, deriveBasePathFromPmxPath, fileListToMap, findFirstPmxFileInList, joinAssetPath, normalizeAssetPath, } from "./asset-reader";
5
6
  export const DEFAULT_ENGINE_OPTIONS = {
6
7
  ambientColor: new Vec3(0.88, 0.88, 0.88),
7
8
  directionalLightIntensity: 0.24,
@@ -965,28 +966,55 @@ export class Engine {
965
966
  this.resizeObserver = null;
966
967
  }
967
968
  }
968
- async loadModel(nameOrPath, path) {
969
- const pmxPath = path === undefined ? nameOrPath : path;
970
- const name = path === undefined ? "model_" + (this._nextDefaultModelId++) : nameOrPath;
969
+ async loadModel(nameOrPath, pathOrOptions) {
970
+ if (pathOrOptions !== undefined && typeof pathOrOptions === "object" && "files" in pathOrOptions) {
971
+ const name = nameOrPath;
972
+ const pmxFile = pathOrOptions.pmxFile ?? findFirstPmxFileInList(pathOrOptions.files);
973
+ if (!pmxFile)
974
+ throw new Error("No .pmx file found in the selected folder");
975
+ const map = fileListToMap(pathOrOptions.files);
976
+ const pmxKey = normalizeAssetPath(pmxFile.webkitRelativePath ?? pmxFile.name);
977
+ const reader = createFileMapAssetReader(map);
978
+ const model = await PmxLoader.loadFromReader(reader, pmxKey);
979
+ model.setName(name);
980
+ await this.addModel(model, pmxKey, name, reader);
981
+ return model;
982
+ }
983
+ const pmxPath = pathOrOptions === undefined ? nameOrPath : pathOrOptions;
984
+ const name = pathOrOptions === undefined ? "model_" + this._nextDefaultModelId++ : nameOrPath;
971
985
  const model = await PmxLoader.load(pmxPath);
972
986
  model.setName(name);
973
987
  await this.addModel(model, pmxPath, name);
974
988
  return model;
975
989
  }
976
- async addModel(model, pmxPath, name) {
990
+ async addModel(model, pmxPath, name, assetReader) {
977
991
  const requested = name ?? model.name;
978
992
  let key = requested;
979
993
  let n = 1;
980
994
  while (this.modelInstances.has(key)) {
981
995
  key = `${requested}_${n++}`;
982
996
  }
983
- const pathParts = pmxPath.split("/");
984
- pathParts.pop();
985
- const basePath = pathParts.join("/") + "/";
986
- await this.setupModelInstance(key, model, basePath);
997
+ const reader = assetReader ?? createFetchAssetReader();
998
+ const basePath = deriveBasePathFromPmxPath(pmxPath);
999
+ model.setAssetContext(reader, basePath);
1000
+ await this.setupModelInstance(key, model, basePath, reader);
987
1001
  return key;
988
1002
  }
989
1003
  removeModel(name) {
1004
+ const inst = this.modelInstances.get(name);
1005
+ if (!inst)
1006
+ return;
1007
+ inst.model.stopAnimation();
1008
+ for (const path of inst.textureCacheKeys) {
1009
+ const tex = this.textureCache.get(path);
1010
+ if (tex) {
1011
+ tex.destroy();
1012
+ this.textureCache.delete(path);
1013
+ }
1014
+ }
1015
+ for (const buf of inst.gpuBuffers) {
1016
+ buf.destroy();
1017
+ }
990
1018
  this.modelInstances.delete(name);
991
1019
  }
992
1020
  getModelNames() {
@@ -1068,7 +1096,7 @@ export class Engine {
1068
1096
  this.device.queue.writeBuffer(inst.vertexBuffer, 0, vertices);
1069
1097
  inst.vertexBufferNeedsUpdate = false;
1070
1098
  }
1071
- async setupModelInstance(name, model, basePath) {
1099
+ async setupModelInstance(name, model, basePath, assetReader) {
1072
1100
  const vertices = model.getVertices();
1073
1101
  const skinning = model.getSkinning();
1074
1102
  const skeleton = model.getSkeleton();
@@ -1130,10 +1158,20 @@ export class Engine {
1130
1158
  { binding: 0, resource: { buffer: skinMatrixBuffer } },
1131
1159
  ],
1132
1160
  });
1161
+ const gpuBuffers = [
1162
+ vertexBuffer,
1163
+ indexBuffer,
1164
+ jointsBuffer,
1165
+ weightsBuffer,
1166
+ skinMatrixBuffer,
1167
+ ];
1133
1168
  const inst = {
1134
1169
  name,
1135
1170
  model,
1136
1171
  basePath,
1172
+ assetReader,
1173
+ gpuBuffers,
1174
+ textureCacheKeys: [],
1137
1175
  vertexBuffer,
1138
1176
  indexBuffer,
1139
1177
  jointsBuffer,
@@ -1285,8 +1323,8 @@ export class Engine {
1285
1323
  const loadTextureByIndex = async (texIndex) => {
1286
1324
  if (texIndex < 0 || texIndex >= textures.length)
1287
1325
  return null;
1288
- const path = inst.basePath + textures[texIndex].path;
1289
- return this.createTextureFromPath(path);
1326
+ const logicalPath = joinAssetPath(inst.basePath, normalizeAssetPath(textures[texIndex].path));
1327
+ return this.createTextureFromLogicalPath(inst, logicalPath);
1290
1328
  };
1291
1329
  let currentIndexOffset = 0;
1292
1330
  let materialId = 0;
@@ -1301,6 +1339,7 @@ export class Engine {
1301
1339
  const materialAlpha = mat.diffuse[3];
1302
1340
  const isTransparent = materialAlpha < 1.0 - 0.001;
1303
1341
  const materialUniformBuffer = this.createMaterialUniformBuffer(prefix + mat.name, materialAlpha, [mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]], mat.ambient, mat.specular, mat.shininess);
1342
+ inst.gpuBuffers.push(materialUniformBuffer);
1304
1343
  const textureView = diffuseTexture.createView();
1305
1344
  const bindGroup = this.device.createBindGroup({
1306
1345
  label: `${prefix}material: ${mat.name}`,
@@ -1318,6 +1357,7 @@ export class Engine {
1318
1357
  mat.edgeSize, 0, 0, 0,
1319
1358
  ]);
1320
1359
  const outlineUniformBuffer = this.createUniformBuffer(`${prefix}outline: ${mat.name}`, materialUniformData);
1360
+ inst.gpuBuffers.push(outlineUniformBuffer);
1321
1361
  const outlineBindGroup = this.device.createBindGroup({
1322
1362
  label: `${prefix}outline: ${mat.name}`,
1323
1363
  layout: this.outlinePerMaterialBindGroupLayout,
@@ -1331,6 +1371,7 @@ export class Engine {
1331
1371
  if (this.onRaycast) {
1332
1372
  const pickIdData = new Float32Array([modelId, materialId, 0, 0]);
1333
1373
  const pickIdBuffer = this.createUniformBuffer(`${prefix}pick: ${mat.name}`, pickIdData);
1374
+ inst.gpuBuffers.push(pickIdBuffer);
1334
1375
  const pickBindGroup = this.device.createBindGroup({
1335
1376
  label: `${prefix}pick: ${mat.name}`,
1336
1377
  layout: this.pickPerMaterialBindGroupLayout,
@@ -1371,22 +1412,20 @@ export class Engine {
1371
1412
  shouldRenderDrawCall(inst, drawCall) {
1372
1413
  return !inst.hiddenMaterials.has(drawCall.materialName);
1373
1414
  }
1374
- async createTextureFromPath(path) {
1375
- const cached = this.textureCache.get(path);
1415
+ async createTextureFromLogicalPath(inst, logicalPath) {
1416
+ const cacheKey = logicalPath;
1417
+ const cached = this.textureCache.get(cacheKey);
1376
1418
  if (cached) {
1377
1419
  return cached;
1378
1420
  }
1379
1421
  try {
1380
- const response = await fetch(path);
1381
- if (!response.ok) {
1382
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1383
- }
1384
- const imageBitmap = await createImageBitmap(await response.blob(), {
1422
+ const buffer = await inst.assetReader.readBinary(logicalPath);
1423
+ const imageBitmap = await createImageBitmap(new Blob([buffer]), {
1385
1424
  premultiplyAlpha: "none",
1386
1425
  colorSpaceConversion: "none",
1387
1426
  });
1388
1427
  const texture = this.device.createTexture({
1389
- label: `texture: ${path}`,
1428
+ label: `texture: ${cacheKey}`,
1390
1429
  size: [imageBitmap.width, imageBitmap.height],
1391
1430
  format: "rgba8unorm",
1392
1431
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
@@ -1395,7 +1434,8 @@ export class Engine {
1395
1434
  imageBitmap.width,
1396
1435
  imageBitmap.height,
1397
1436
  ]);
1398
- this.textureCache.set(path, texture);
1437
+ this.textureCache.set(cacheKey, texture);
1438
+ inst.textureCacheKeys.push(cacheKey);
1399
1439
  return texture;
1400
1440
  }
1401
1441
  catch {
@@ -0,0 +1,24 @@
1
+ /** After choosing a path from `multiple`, get the `File` for `loadModel(..., { files, pmxFile })`. */
2
+ export declare function pmxFileAtRelativePath(files: File[], relativePath: string): File | undefined;
3
+ /** Result of reading a folder input — switch on `status` in your UI. */
4
+ export type PmxFolderInputResult = {
5
+ status: "empty";
6
+ } | {
7
+ status: "not_directory";
8
+ } | {
9
+ status: "no_pmx";
10
+ } | {
11
+ status: "single";
12
+ files: File[];
13
+ pmxFile: File;
14
+ } | {
15
+ status: "multiple";
16
+ files: File[];
17
+ pmxRelativePaths: string[];
18
+ };
19
+ /**
20
+ * One call from `onChange`: snapshots files, validates folder pick, resolves a single PMX or asks you to pick among several.
21
+ * Reset the input after: `e.target.value = ""`.
22
+ */
23
+ export declare function parsePmxFolderInput(fileList: FileList | null | undefined): PmxFolderInputResult;
24
+ //# sourceMappingURL=folder-upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"folder-upload.d.ts","sourceRoot":"","sources":["../src/folder-upload.ts"],"names":[],"mappings":"AAyBA,sGAAsG;AACtG,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAO3F;AAED,wEAAwE;AACxE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GACnB;IAAE,MAAM,EAAE,eAAe,CAAA;CAAE,GAC3B;IAAE,MAAM,EAAE,QAAQ,CAAA;CAAE,GACpB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,IAAI,EAAE,CAAC;IAAC,OAAO,EAAE,IAAI,CAAA;CAAE,GAClD;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,IAAI,EAAE,CAAC;IAAC,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAErE;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,GAAG,oBAAoB,CAW/F"}
@@ -0,0 +1,50 @@
1
+ import { normalizeAssetPath } from "./asset-reader";
2
+ /**
3
+ * Call on `<input type="file" webkitdirectory>` `change` **before** `input.value = ""`.
4
+ * `FileList` is live — clearing the input empties it; this copies to a stable `File[]`.
5
+ */
6
+ function prepareLocalFolderFiles(fileList) {
7
+ const files = fileList?.length ? Array.from(fileList) : [];
8
+ const pmxRelativePaths = [];
9
+ for (const f of files) {
10
+ const wr = f.webkitRelativePath;
11
+ if (!wr || !wr.toLowerCase().endsWith(".pmx"))
12
+ continue;
13
+ pmxRelativePaths.push(normalizeAssetPath(wr));
14
+ }
15
+ pmxRelativePaths.sort((a, b) => a.localeCompare(b));
16
+ return { files, pmxRelativePaths };
17
+ }
18
+ function isDirectoryUpload(files) {
19
+ return files.length > 0 && files.every((f) => !!f.webkitRelativePath);
20
+ }
21
+ /** After choosing a path from `multiple`, get the `File` for `loadModel(..., { files, pmxFile })`. */
22
+ export function pmxFileAtRelativePath(files, relativePath) {
23
+ const norm = normalizeAssetPath(relativePath);
24
+ for (const f of files) {
25
+ const wr = f.webkitRelativePath;
26
+ if (wr && normalizeAssetPath(wr) === norm)
27
+ return f;
28
+ }
29
+ return undefined;
30
+ }
31
+ /**
32
+ * One call from `onChange`: snapshots files, validates folder pick, resolves a single PMX or asks you to pick among several.
33
+ * Reset the input after: `e.target.value = ""`.
34
+ */
35
+ export function parsePmxFolderInput(fileList) {
36
+ const { files, pmxRelativePaths } = prepareLocalFolderFiles(fileList);
37
+ if (files.length === 0)
38
+ return { status: "empty" };
39
+ if (!isDirectoryUpload(files))
40
+ return { status: "not_directory" };
41
+ if (pmxRelativePaths.length === 0)
42
+ return { status: "no_pmx" };
43
+ if (pmxRelativePaths.length === 1) {
44
+ const pmxFile = pmxFileAtRelativePath(files, pmxRelativePaths[0]);
45
+ if (!pmxFile)
46
+ return { status: "no_pmx" };
47
+ return { status: "single", files, pmxFile };
48
+ }
49
+ return { status: "multiple", files, pmxRelativePaths };
50
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- export { Engine, type EngineStats } from "./engine";
1
+ export { Engine, type EngineStats, type LoadModelFromFilesOptions } from "./engine";
2
+ export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload";
2
3
  export { Model } from "./model";
3
4
  export { Vec3, Quat, Mat4 } from "./math";
4
5
  export type { AnimationClip, AnimationPlayOptions, AnimationProgress, BoneKeyframe, MorphKeyframe, BoneInterpolation, ControlPoint, } from "./animation";
5
6
  export { FPS } from "./animation";
6
7
  export { Physics, type PhysicsOptions } from "./physics";
7
- export { VMDWriter } from "./vmd-writer";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,KAAK,yBAAyB,EAAE,MAAM,UAAU,CAAA;AACnF,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AACvG,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { Engine } from "./engine";
2
+ export { parsePmxFolderInput, pmxFileAtRelativePath } from "./folder-upload";
2
3
  export { Model } from "./model";
3
4
  export { Vec3, Quat, Mat4 } from "./math";
4
5
  export { FPS } from "./animation";
5
6
  export { Physics } from "./physics";
6
- export { VMDWriter } from "./vmd-writer";
package/dist/model.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Mat4, Quat, Vec3 } from "./math";
2
+ import { type AssetReader } from "./asset-reader";
2
3
  import { Rigidbody, Joint } from "./physics";
3
4
  import { AnimationClip, AnimationPlayOptions, AnimationProgress } from "./animation";
4
5
  export interface Texture {
@@ -118,6 +119,10 @@ export declare class Model {
118
119
  private boneTrackIndices;
119
120
  private morphTrackIndices;
120
121
  private lastAppliedClip;
122
+ private assetReader;
123
+ private assetBasePath;
124
+ /** Called by Engine when registering the model; enables loadVmd to resolve relative paths for folder uploads. */
125
+ setAssetContext(reader: AssetReader, basePath: string): void;
121
126
  constructor(vertexData: Float32Array<ArrayBuffer>, indexData: Uint32Array<ArrayBuffer>, textures: Texture[], materials: Material[], skeleton: Skeleton, skinning: Skinning, morphing: Morphing, rigidbodies?: Rigidbody[], joints?: Joint[]);
122
127
  private initializeRuntimeSkeleton;
123
128
  private initializeIKRuntime;
@@ -145,7 +150,7 @@ export declare class Model {
145
150
  setMorphWeight(name: string, weight: number, durationMs?: number): void;
146
151
  private applyMorphs;
147
152
  private buildClipFromVmdKeyFrames;
148
- loadVmd(name: string, url: string): Promise<void>;
153
+ loadVmd(name: string, urlOrRelative: string): Promise<void>;
149
154
  loadClip(name: string, clip: AnimationClip): void;
150
155
  resetAllBones(): void;
151
156
  resetAllMorphs(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
package/dist/model.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Mat4, Quat, Vec3 } from "./math";
2
2
  import { Engine } from "./engine";
3
+ import { joinAssetPath } from "./asset-reader";
3
4
  import { IKSolverSystem } from "./ik-solver";
4
5
  import { VMDLoader } from "./vmd-loader";
5
6
  import { VMDWriter } from "./vmd-writer";
@@ -12,6 +13,11 @@ export class Model {
12
13
  setName(value) {
13
14
  this._name = value;
14
15
  }
16
+ /** Called by Engine when registering the model; enables loadVmd to resolve relative paths for folder uploads. */
17
+ setAssetContext(reader, basePath) {
18
+ this.assetReader = reader;
19
+ this.assetBasePath = basePath;
20
+ }
15
21
  constructor(vertexData, indexData, textures, materials, skeleton, skinning, morphing, rigidbodies = [], joints = []) {
16
22
  this._name = "";
17
23
  this.textures = [];
@@ -29,6 +35,8 @@ export class Model {
29
35
  this.boneTrackIndices = new Map();
30
36
  this.morphTrackIndices = new Map();
31
37
  this.lastAppliedClip = null;
38
+ this.assetReader = null;
39
+ this.assetBasePath = "";
32
40
  // Cached set to track which bones are being computed in current IK pass (to avoid infinite recursion)
33
41
  this.ikComputedSet = new Set();
34
42
  // Store base vertex data (original positions before morphing)
@@ -579,8 +587,32 @@ export class Model {
579
587
  }
580
588
  return { boneTracks, morphTracks, frameCount: maxFrame };
581
589
  }
582
- loadVmd(name, url) {
583
- return VMDLoader.load(url).then((vmdKeyFrames) => {
590
+ loadVmd(name, urlOrRelative) {
591
+ const loadBuffer = () => {
592
+ const u = urlOrRelative.trim();
593
+ const useSiteFetch = u.startsWith("http://") ||
594
+ u.startsWith("https://") ||
595
+ u.startsWith("/") ||
596
+ u.startsWith("blob:") ||
597
+ u.startsWith("data:");
598
+ if (useSiteFetch) {
599
+ return fetch(u).then((r) => {
600
+ if (!r.ok)
601
+ throw new Error(`Failed to fetch VMD ${u}: ${r.status}`);
602
+ return r.arrayBuffer();
603
+ });
604
+ }
605
+ if (this.assetReader) {
606
+ return this.assetReader.readBinary(joinAssetPath(this.assetBasePath, u));
607
+ }
608
+ return fetch(u).then((r) => {
609
+ if (!r.ok)
610
+ throw new Error(`Failed to fetch VMD ${u}: ${r.status}`);
611
+ return r.arrayBuffer();
612
+ });
613
+ };
614
+ return loadBuffer().then((buf) => {
615
+ const vmdKeyFrames = VMDLoader.loadFromBuffer(buf);
584
616
  const clip = this.buildClipFromVmdKeyFrames(vmdKeyFrames);
585
617
  this.animationState.loadAnimation(name, clip);
586
618
  });
@@ -1,4 +1,5 @@
1
1
  import { Model } from "./model";
2
+ import { type AssetReader } from "./asset-reader";
2
3
  export declare class PmxLoader {
3
4
  private view;
4
5
  private offset;
@@ -23,6 +24,8 @@ export declare class PmxLoader {
23
24
  private joints;
24
25
  private constructor();
25
26
  static load(url: string): Promise<Model>;
27
+ static loadFromBuffer(buffer: ArrayBuffer): Model;
28
+ static loadFromReader(reader: AssetReader, pmxLogicalPath: string): Promise<Model>;
26
29
  private parse;
27
30
  private parseHeader;
28
31
  private parseVertices;
@@ -1 +1 @@
1
- {"version":3,"file":"pmx-loader.d.ts","sourceRoot":"","sources":["../src/pmx-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EAWN,MAAM,SAAS,CAAA;AAIhB,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,eAAe,CAAI;IAC3B,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,aAAa,CAAI;IACzB,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,kBAAkB,CAAI;IAC9B,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,mBAAmB,CAA4B;IACvD,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,WAAW,CAAY;IAC/B,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,MAAM,CAAc;IAE5B,OAAO;WAIM,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAK9C,OAAO,CAAC,KAAK;IAiBb,OAAO,CAAC,WAAW;IA+CnB,OAAO,CAAC,aAAa;IA+FrB,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,cAAc;IAoEtB,OAAO,CAAC,UAAU;IAsKlB,OAAO,CAAC,WAAW;IA+InB,OAAO,CAAC,iBAAiB;IAgDzB,OAAO,CAAC,gBAAgB;IAyFxB,OAAO,CAAC,WAAW;IAmGnB,OAAO,CAAC,kBAAkB;IAmC1B,OAAO,CAAC,OAAO;IAkLf,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,OAAO;IAmBf,OAAO,CAAC,QAAQ;CAIjB"}
1
+ {"version":3,"file":"pmx-loader.d.ts","sourceRoot":"","sources":["../src/pmx-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EAWN,MAAM,SAAS,CAAA;AAGhB,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEzE,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,eAAe,CAAI;IAC3B,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,aAAa,CAAI;IACzB,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,kBAAkB,CAAI;IAC9B,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,mBAAmB,CAA4B;IACvD,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,WAAW,CAAY;IAC/B,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,MAAM,CAAc;IAE5B,OAAO;WAIM,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAI9C,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK;WAIpC,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAKxF,OAAO,CAAC,KAAK;IAiBb,OAAO,CAAC,WAAW;IA+CnB,OAAO,CAAC,aAAa;IA+FrB,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,cAAc;IAoEtB,OAAO,CAAC,UAAU;IAsKlB,OAAO,CAAC,WAAW;IA+InB,OAAO,CAAC,iBAAiB;IAgDzB,OAAO,CAAC,gBAAgB;IAyFxB,OAAO,CAAC,WAAW;IAmGnB,OAAO,CAAC,kBAAkB;IAmC1B,OAAO,CAAC,OAAO;IAkLf,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,OAAO;IAmBf,OAAO,CAAC,QAAQ;CAIjB"}
@@ -1,5 +1,6 @@
1
1
  import { Model, } from "./model";
2
2
  import { Mat4, Vec3 } from "./math";
3
+ import { createFetchAssetReader } from "./asset-reader";
3
4
  export class PmxLoader {
4
5
  constructor(buffer) {
5
6
  this.offset = 0;
@@ -24,8 +25,14 @@ export class PmxLoader {
24
25
  this.view = new DataView(buffer);
25
26
  }
26
27
  static async load(url) {
27
- const loader = new PmxLoader(await fetch(url).then((r) => r.arrayBuffer()));
28
- return loader.parse();
28
+ return PmxLoader.loadFromReader(createFetchAssetReader(), url);
29
+ }
30
+ static loadFromBuffer(buffer) {
31
+ return new PmxLoader(buffer).parse();
32
+ }
33
+ static async loadFromReader(reader, pmxLogicalPath) {
34
+ const buffer = await reader.readBinary(pmxLogicalPath);
35
+ return PmxLoader.loadFromBuffer(buffer);
29
36
  }
30
37
  parse() {
31
38
  this.parseHeader();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -0,0 +1,79 @@
1
+ /** Unified binary I/O for PMX/VMD/textures: HTTP(s) or user folder (File map). */
2
+
3
+ export type AssetReader = {
4
+ readBinary(logicalPath: string): Promise<ArrayBuffer>
5
+ }
6
+
7
+ /** Normalize PMX-style paths: backslashes, trim, strip leading ./ */
8
+ export function normalizeAssetPath(p: string): string {
9
+ let s = p.replace(/\\/g, "/").trim()
10
+ if (s.startsWith("./")) s = s.slice(2)
11
+ return s
12
+ }
13
+
14
+ /** Join PMX directory prefix and texture-relative path (both may be ""). */
15
+ export function joinAssetPath(baseDir: string, relative: string): string {
16
+ const rel = normalizeAssetPath(relative)
17
+ if (!rel) return normalizeAssetPath(baseDir)
18
+ const base = baseDir.endsWith("/") ? baseDir.slice(0, -1) : baseDir
19
+ if (!base) return rel
20
+ return `${base}/${rel}`
21
+ }
22
+
23
+ /** Same rules as the original engine string split: supports absolute site paths like `/models/a/b.pmx`. */
24
+ export function deriveBasePathFromPmxPath(pmxPath: string): string {
25
+ const pathParts = pmxPath.replace(/\\/g, "/").split("/")
26
+ pathParts.pop()
27
+ return pathParts.join("/") + (pathParts.length > 0 ? "/" : "")
28
+ }
29
+
30
+ export function createFetchAssetReader(): AssetReader {
31
+ return {
32
+ async readBinary(logicalPath: string) {
33
+ const r = await fetch(logicalPath)
34
+ if (!r.ok) throw new Error(`Failed to fetch ${logicalPath}: ${r.status} ${r.statusText}`)
35
+ return r.arrayBuffer()
36
+ },
37
+ }
38
+ }
39
+
40
+ /** Keys must be normalized paths relative to the selected folder root (see fileListToMap). */
41
+ export function createFileMapAssetReader(files: Map<string, File>): AssetReader {
42
+ return {
43
+ async readBinary(logicalPath: string) {
44
+ const key = normalizeAssetPath(logicalPath)
45
+ let file = files.get(key)
46
+ if (!file) {
47
+ const lower = key.toLowerCase()
48
+ for (const [k, f] of files) {
49
+ if (k.toLowerCase() === lower) {
50
+ file = f
51
+ break
52
+ }
53
+ }
54
+ }
55
+ if (!file) throw new Error(`Missing file in folder: ${key}`)
56
+ return file.arrayBuffer()
57
+ },
58
+ }
59
+ }
60
+
61
+ export function fileListToMap(files: FileList | File[]): Map<string, File> {
62
+ const m = new Map<string, File>()
63
+ for (const f of Array.from(files)) {
64
+ const rel = (f as File & { webkitRelativePath?: string }).webkitRelativePath ?? f.name
65
+ m.set(normalizeAssetPath(rel), f)
66
+ }
67
+ return m
68
+ }
69
+
70
+ export function findFirstPmxFileInList(files: FileList | File[]): File | null {
71
+ const list = Array.from(files).filter((f) => f.name.toLowerCase().endsWith(".pmx"))
72
+ if (list.length === 0) return null
73
+ list.sort((a, b) => {
74
+ const pa = (a as File & { webkitRelativePath?: string }).webkitRelativePath ?? a.name
75
+ const pb = (b as File & { webkitRelativePath?: string }).webkitRelativePath ?? b.name
76
+ return pa.localeCompare(pb)
77
+ })
78
+ return list[0] ?? null
79
+ }
package/src/engine.ts CHANGED
@@ -3,9 +3,25 @@ import { Mat4, Vec3 } from "./math"
3
3
  import { Model } from "./model"
4
4
  import { PmxLoader } from "./pmx-loader"
5
5
  import { Physics, type PhysicsOptions } from "./physics"
6
+ import {
7
+ createFetchAssetReader,
8
+ createFileMapAssetReader,
9
+ deriveBasePathFromPmxPath,
10
+ fileListToMap,
11
+ findFirstPmxFileInList,
12
+ joinAssetPath,
13
+ normalizeAssetPath,
14
+ type AssetReader,
15
+ } from "./asset-reader"
6
16
 
7
17
  export type RaycastCallback = (modelName: string, material: string | null, screenX: number, screenY: number) => void
8
18
 
19
+ /** Select a folder (webkitdirectory) and pass FileList or File[]; pmxFile picks which .pmx when several exist. */
20
+ export type LoadModelFromFilesOptions = {
21
+ files: FileList | File[]
22
+ pmxFile?: File
23
+ }
24
+
9
25
  export type EngineOptions = {
10
26
  ambientColor?: Vec3
11
27
  directionalLightIntensity?: number
@@ -62,6 +78,9 @@ interface ModelInstance {
62
78
  name: string
63
79
  model: Model
64
80
  basePath: string
81
+ assetReader: AssetReader
82
+ gpuBuffers: GPUBuffer[]
83
+ textureCacheKeys: string[]
65
84
  vertexBuffer: GPUBuffer
66
85
  indexBuffer: GPUBuffer
67
86
  jointsBuffer: GPUBuffer
@@ -1151,30 +1170,62 @@ export class Engine {
1151
1170
 
1152
1171
  async loadModel(path: string): Promise<Model>
1153
1172
  async loadModel(name: string, path: string): Promise<Model>
1154
- async loadModel(nameOrPath: string, path?: string): Promise<Model> {
1155
- const pmxPath = path === undefined ? nameOrPath : path
1156
- const name = path === undefined ? "model_" + (this._nextDefaultModelId++) : nameOrPath
1173
+ async loadModel(name: string, options: LoadModelFromFilesOptions): Promise<Model>
1174
+ async loadModel(
1175
+ nameOrPath: string,
1176
+ pathOrOptions?: string | LoadModelFromFilesOptions
1177
+ ): Promise<Model> {
1178
+ if (pathOrOptions !== undefined && typeof pathOrOptions === "object" && "files" in pathOrOptions) {
1179
+ const name = nameOrPath
1180
+ const pmxFile = pathOrOptions.pmxFile ?? findFirstPmxFileInList(pathOrOptions.files)
1181
+ if (!pmxFile) throw new Error("No .pmx file found in the selected folder")
1182
+ const map = fileListToMap(pathOrOptions.files)
1183
+ const pmxKey = normalizeAssetPath(
1184
+ (pmxFile as File & { webkitRelativePath?: string }).webkitRelativePath ?? pmxFile.name
1185
+ )
1186
+ const reader = createFileMapAssetReader(map)
1187
+ const model = await PmxLoader.loadFromReader(reader, pmxKey)
1188
+ model.setName(name)
1189
+ await this.addModel(model, pmxKey, name, reader)
1190
+ return model
1191
+ }
1192
+
1193
+ const pmxPath = pathOrOptions === undefined ? nameOrPath : pathOrOptions
1194
+ const name = pathOrOptions === undefined ? "model_" + this._nextDefaultModelId++ : nameOrPath
1157
1195
  const model = await PmxLoader.load(pmxPath)
1158
1196
  model.setName(name)
1159
1197
  await this.addModel(model, pmxPath, name)
1160
1198
  return model
1161
1199
  }
1162
1200
 
1163
- async addModel(model: Model, pmxPath: string, name?: string): Promise<string> {
1201
+ async addModel(model: Model, pmxPath: string, name?: string, assetReader?: AssetReader): Promise<string> {
1164
1202
  const requested = name ?? model.name
1165
1203
  let key = requested
1166
1204
  let n = 1
1167
1205
  while (this.modelInstances.has(key)) {
1168
1206
  key = `${requested}_${n++}`
1169
1207
  }
1170
- const pathParts = pmxPath.split("/")
1171
- pathParts.pop()
1172
- const basePath = pathParts.join("/") + "/"
1173
- await this.setupModelInstance(key, model, basePath)
1208
+ const reader = assetReader ?? createFetchAssetReader()
1209
+ const basePath = deriveBasePathFromPmxPath(pmxPath)
1210
+ model.setAssetContext(reader, basePath)
1211
+ await this.setupModelInstance(key, model, basePath, reader)
1174
1212
  return key
1175
1213
  }
1176
1214
 
1177
1215
  removeModel(name: string): void {
1216
+ const inst = this.modelInstances.get(name)
1217
+ if (!inst) return
1218
+ inst.model.stopAnimation()
1219
+ for (const path of inst.textureCacheKeys) {
1220
+ const tex = this.textureCache.get(path)
1221
+ if (tex) {
1222
+ tex.destroy()
1223
+ this.textureCache.delete(path)
1224
+ }
1225
+ }
1226
+ for (const buf of inst.gpuBuffers) {
1227
+ buf.destroy()
1228
+ }
1178
1229
  this.modelInstances.delete(name)
1179
1230
  }
1180
1231
 
@@ -1262,7 +1313,7 @@ export class Engine {
1262
1313
  inst.vertexBufferNeedsUpdate = false
1263
1314
  }
1264
1315
 
1265
- private async setupModelInstance(name: string, model: Model, basePath: string): Promise<void> {
1316
+ private async setupModelInstance(name: string, model: Model, basePath: string, assetReader: AssetReader): Promise<void> {
1266
1317
  const vertices = model.getVertices()
1267
1318
  const skinning = model.getSkinning()
1268
1319
  const skeleton = model.getSkeleton()
@@ -1345,10 +1396,21 @@ export class Engine {
1345
1396
  ],
1346
1397
  })
1347
1398
 
1399
+ const gpuBuffers: GPUBuffer[] = [
1400
+ vertexBuffer,
1401
+ indexBuffer,
1402
+ jointsBuffer,
1403
+ weightsBuffer,
1404
+ skinMatrixBuffer,
1405
+ ]
1406
+
1348
1407
  const inst: ModelInstance = {
1349
1408
  name,
1350
1409
  model,
1351
1410
  basePath,
1411
+ assetReader,
1412
+ gpuBuffers,
1413
+ textureCacheKeys: [],
1352
1414
  vertexBuffer,
1353
1415
  indexBuffer,
1354
1416
  jointsBuffer,
@@ -1510,8 +1572,8 @@ export class Engine {
1510
1572
 
1511
1573
  const loadTextureByIndex = async (texIndex: number): Promise<GPUTexture | null> => {
1512
1574
  if (texIndex < 0 || texIndex >= textures.length) return null
1513
- const path = inst.basePath + textures[texIndex].path
1514
- return this.createTextureFromPath(path)
1575
+ const logicalPath = joinAssetPath(inst.basePath, normalizeAssetPath(textures[texIndex].path))
1576
+ return this.createTextureFromLogicalPath(inst, logicalPath)
1515
1577
  }
1516
1578
 
1517
1579
  let currentIndexOffset = 0
@@ -1535,6 +1597,7 @@ export class Engine {
1535
1597
  mat.specular,
1536
1598
  mat.shininess
1537
1599
  )
1600
+ inst.gpuBuffers.push(materialUniformBuffer)
1538
1601
 
1539
1602
  const textureView = diffuseTexture.createView()
1540
1603
  const bindGroup = this.device.createBindGroup({
@@ -1555,6 +1618,7 @@ export class Engine {
1555
1618
  mat.edgeSize, 0, 0, 0,
1556
1619
  ])
1557
1620
  const outlineUniformBuffer = this.createUniformBuffer(`${prefix}outline: ${mat.name}`, materialUniformData)
1621
+ inst.gpuBuffers.push(outlineUniformBuffer)
1558
1622
  const outlineBindGroup = this.device.createBindGroup({
1559
1623
  label: `${prefix}outline: ${mat.name}`,
1560
1624
  layout: this.outlinePerMaterialBindGroupLayout,
@@ -1569,6 +1633,7 @@ export class Engine {
1569
1633
  if (this.onRaycast) {
1570
1634
  const pickIdData = new Float32Array([modelId, materialId, 0, 0])
1571
1635
  const pickIdBuffer = this.createUniformBuffer(`${prefix}pick: ${mat.name}`, pickIdData)
1636
+ inst.gpuBuffers.push(pickIdBuffer)
1572
1637
  const pickBindGroup = this.device.createBindGroup({
1573
1638
  label: `${prefix}pick: ${mat.name}`,
1574
1639
  layout: this.pickPerMaterialBindGroupLayout,
@@ -1621,24 +1686,22 @@ export class Engine {
1621
1686
  return !inst.hiddenMaterials.has(drawCall.materialName)
1622
1687
  }
1623
1688
 
1624
- private async createTextureFromPath(path: string): Promise<GPUTexture | null> {
1625
- const cached = this.textureCache.get(path)
1689
+ private async createTextureFromLogicalPath(inst: ModelInstance, logicalPath: string): Promise<GPUTexture | null> {
1690
+ const cacheKey = logicalPath
1691
+ const cached = this.textureCache.get(cacheKey)
1626
1692
  if (cached) {
1627
1693
  return cached
1628
1694
  }
1629
1695
 
1630
1696
  try {
1631
- const response = await fetch(path)
1632
- if (!response.ok) {
1633
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
1634
- }
1635
- const imageBitmap = await createImageBitmap(await response.blob(), {
1697
+ const buffer = await inst.assetReader.readBinary(logicalPath)
1698
+ const imageBitmap = await createImageBitmap(new Blob([buffer]), {
1636
1699
  premultiplyAlpha: "none",
1637
1700
  colorSpaceConversion: "none",
1638
1701
  })
1639
1702
 
1640
1703
  const texture = this.device.createTexture({
1641
- label: `texture: ${path}`,
1704
+ label: `texture: ${cacheKey}`,
1642
1705
  size: [imageBitmap.width, imageBitmap.height],
1643
1706
  format: "rgba8unorm",
1644
1707
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
@@ -1648,7 +1711,8 @@ export class Engine {
1648
1711
  imageBitmap.height,
1649
1712
  ])
1650
1713
 
1651
- this.textureCache.set(path, texture)
1714
+ this.textureCache.set(cacheKey, texture)
1715
+ inst.textureCacheKeys.push(cacheKey)
1652
1716
  return texture
1653
1717
  } catch {
1654
1718
  return null
@@ -0,0 +1,59 @@
1
+ import { normalizeAssetPath } from "./asset-reader"
2
+
3
+ /**
4
+ * Call on `<input type="file" webkitdirectory>` `change` **before** `input.value = ""`.
5
+ * `FileList` is live — clearing the input empties it; this copies to a stable `File[]`.
6
+ */
7
+ function prepareLocalFolderFiles(fileList: FileList | null | undefined): {
8
+ files: File[]
9
+ pmxRelativePaths: string[]
10
+ } {
11
+ const files = fileList?.length ? Array.from(fileList) : []
12
+ const pmxRelativePaths: string[] = []
13
+ for (const f of files) {
14
+ const wr = (f as File & { webkitRelativePath?: string }).webkitRelativePath
15
+ if (!wr || !wr.toLowerCase().endsWith(".pmx")) continue
16
+ pmxRelativePaths.push(normalizeAssetPath(wr))
17
+ }
18
+ pmxRelativePaths.sort((a, b) => a.localeCompare(b))
19
+ return { files, pmxRelativePaths }
20
+ }
21
+
22
+ function isDirectoryUpload(files: File[]): boolean {
23
+ return files.length > 0 && files.every((f) => !!(f as File & { webkitRelativePath?: string }).webkitRelativePath)
24
+ }
25
+
26
+ /** After choosing a path from `multiple`, get the `File` for `loadModel(..., { files, pmxFile })`. */
27
+ export function pmxFileAtRelativePath(files: File[], relativePath: string): File | undefined {
28
+ const norm = normalizeAssetPath(relativePath)
29
+ for (const f of files) {
30
+ const wr = (f as File & { webkitRelativePath?: string }).webkitRelativePath
31
+ if (wr && normalizeAssetPath(wr) === norm) return f
32
+ }
33
+ return undefined
34
+ }
35
+
36
+ /** Result of reading a folder input — switch on `status` in your UI. */
37
+ export type PmxFolderInputResult =
38
+ | { status: "empty" }
39
+ | { status: "not_directory" }
40
+ | { status: "no_pmx" }
41
+ | { status: "single"; files: File[]; pmxFile: File }
42
+ | { status: "multiple"; files: File[]; pmxRelativePaths: string[] }
43
+
44
+ /**
45
+ * One call from `onChange`: snapshots files, validates folder pick, resolves a single PMX or asks you to pick among several.
46
+ * Reset the input after: `e.target.value = ""`.
47
+ */
48
+ export function parsePmxFolderInput(fileList: FileList | null | undefined): PmxFolderInputResult {
49
+ const { files, pmxRelativePaths } = prepareLocalFolderFiles(fileList)
50
+ if (files.length === 0) return { status: "empty" }
51
+ if (!isDirectoryUpload(files)) return { status: "not_directory" }
52
+ if (pmxRelativePaths.length === 0) return { status: "no_pmx" }
53
+ if (pmxRelativePaths.length === 1) {
54
+ const pmxFile = pmxFileAtRelativePath(files, pmxRelativePaths[0]!)
55
+ if (!pmxFile) return { status: "no_pmx" }
56
+ return { status: "single", files, pmxFile }
57
+ }
58
+ return { status: "multiple", files, pmxRelativePaths }
59
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
- export { Engine, type EngineStats } from "./engine"
1
+ export { Engine, type EngineStats, type LoadModelFromFilesOptions } from "./engine"
2
+ export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload"
2
3
  export { Model } from "./model"
3
4
  export { Vec3, Quat, Mat4 } from "./math"
4
5
  export type {
@@ -11,5 +12,4 @@ export type {
11
12
  ControlPoint,
12
13
  } from "./animation"
13
14
  export { FPS } from "./animation"
14
- export { Physics, type PhysicsOptions } from "./physics"
15
- export { VMDWriter } from "./vmd-writer"
15
+ export { Physics, type PhysicsOptions } from "./physics"
package/src/model.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Mat4, Quat, Vec3 } from "./math"
2
2
  import { Engine } from "./engine"
3
+ import { joinAssetPath, type AssetReader } from "./asset-reader"
3
4
  import { Rigidbody, Joint } from "./physics"
4
5
  import { IKSolverSystem } from "./ik-solver"
5
6
  import { VMDLoader, type VMDKeyFrame } from "./vmd-loader"
@@ -206,6 +207,14 @@ export class Model {
206
207
  private morphTrackIndices: Map<string, number> = new Map()
207
208
  private lastAppliedClip: AnimationClip | null = null
208
209
 
210
+ private assetReader: AssetReader | null = null
211
+ private assetBasePath = ""
212
+
213
+ /** Called by Engine when registering the model; enables loadVmd to resolve relative paths for folder uploads. */
214
+ setAssetContext(reader: AssetReader, basePath: string): void {
215
+ this.assetReader = reader
216
+ this.assetBasePath = basePath
217
+ }
209
218
 
210
219
  constructor(
211
220
  vertexData: Float32Array<ArrayBuffer>,
@@ -857,8 +866,31 @@ export class Model {
857
866
  return { boneTracks, morphTracks, frameCount: maxFrame }
858
867
  }
859
868
 
860
- loadVmd(name: string, url: string): Promise<void> {
861
- return VMDLoader.load(url).then((vmdKeyFrames) => {
869
+ loadVmd(name: string, urlOrRelative: string): Promise<void> {
870
+ const loadBuffer = (): Promise<ArrayBuffer> => {
871
+ const u = urlOrRelative.trim()
872
+ const useSiteFetch =
873
+ u.startsWith("http://") ||
874
+ u.startsWith("https://") ||
875
+ u.startsWith("/") ||
876
+ u.startsWith("blob:") ||
877
+ u.startsWith("data:")
878
+ if (useSiteFetch) {
879
+ return fetch(u).then((r) => {
880
+ if (!r.ok) throw new Error(`Failed to fetch VMD ${u}: ${r.status}`)
881
+ return r.arrayBuffer()
882
+ })
883
+ }
884
+ if (this.assetReader) {
885
+ return this.assetReader.readBinary(joinAssetPath(this.assetBasePath, u))
886
+ }
887
+ return fetch(u).then((r) => {
888
+ if (!r.ok) throw new Error(`Failed to fetch VMD ${u}: ${r.status}`)
889
+ return r.arrayBuffer()
890
+ })
891
+ }
892
+ return loadBuffer().then((buf) => {
893
+ const vmdKeyFrames = VMDLoader.loadFromBuffer(buf)
862
894
  const clip = this.buildClipFromVmdKeyFrames(vmdKeyFrames)
863
895
  this.animationState.loadAnimation(name, clip)
864
896
  })
package/src/pmx-loader.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  } from "./model"
14
14
  import { Mat4, Vec3 } from "./math"
15
15
  import { Rigidbody, Joint, RigidbodyShape, RigidbodyType } from "./physics"
16
+ import { createFetchAssetReader, type AssetReader } from "./asset-reader"
16
17
 
17
18
  export class PmxLoader {
18
19
  private view: DataView
@@ -42,8 +43,16 @@ export class PmxLoader {
42
43
  }
43
44
 
44
45
  static async load(url: string): Promise<Model> {
45
- const loader = new PmxLoader(await fetch(url).then((r) => r.arrayBuffer()))
46
- return loader.parse()
46
+ return PmxLoader.loadFromReader(createFetchAssetReader(), url)
47
+ }
48
+
49
+ static loadFromBuffer(buffer: ArrayBuffer): Model {
50
+ return new PmxLoader(buffer).parse()
51
+ }
52
+
53
+ static async loadFromReader(reader: AssetReader, pmxLogicalPath: string): Promise<Model> {
54
+ const buffer = await reader.readBinary(pmxLogicalPath)
55
+ return PmxLoader.loadFromBuffer(buffer)
47
56
  }
48
57
 
49
58
  private parse(): Model {