@ikijs/editor 0.1.0
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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/index.d.mts +916 -0
- package/dist/index.d.ts +916 -0
- package/dist/index.js +2293 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2233 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2233 @@
|
|
|
1
|
+
// src/document.ts
|
|
2
|
+
import { parseIkiModel } from "@ikijs/format";
|
|
3
|
+
|
|
4
|
+
// src/mesh-uv.ts
|
|
5
|
+
function remapMeshUvsToRect(baseUvs, rect) {
|
|
6
|
+
if (baseUvs.length % 2 !== 0) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
`remapMeshUvsToRect: baseUvs must have an even length (u,v pairs), got ${baseUvs.length}`
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
const out = new Array(baseUvs.length);
|
|
12
|
+
for (let i = 0; i < baseUvs.length; i += 2) {
|
|
13
|
+
out[i] = rect.x + baseUvs[i] * rect.width;
|
|
14
|
+
out[i + 1] = rect.y + baseUvs[i + 1] * rect.height;
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/document.ts
|
|
20
|
+
var EditorDocument = class {
|
|
21
|
+
model;
|
|
22
|
+
undoStack = [];
|
|
23
|
+
redoStack = [];
|
|
24
|
+
/** Editor-only session state, never serialized; keyed by stable part id. The
|
|
25
|
+
* unmodified BASE local uvs of every mesh part, captured once at construction
|
|
26
|
+
* so atlas remaps always derive from the original (idempotent). */
|
|
27
|
+
baseMeshUvs = /* @__PURE__ */ new Map();
|
|
28
|
+
constructor(model) {
|
|
29
|
+
this.model = structuredClone(model);
|
|
30
|
+
for (const part of this.model.parts) {
|
|
31
|
+
if (part.mesh) {
|
|
32
|
+
this.baseMeshUvs.set(part.id, part.mesh.uvs.slice());
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Live reference to the working model — for READ access. Mutate it only
|
|
37
|
+
* through {@link execute}/{@link undo}/{@link redo}. */
|
|
38
|
+
getModel() {
|
|
39
|
+
return this.model;
|
|
40
|
+
}
|
|
41
|
+
/** Resolve a part by stable id. Throws a plain `Error` (NOT an
|
|
42
|
+
* `IkiFormatError`) with a path-qualified message if the id is unknown. */
|
|
43
|
+
findPart(id) {
|
|
44
|
+
const part = this.model.parts.find((p) => p.id === id);
|
|
45
|
+
if (!part) {
|
|
46
|
+
throw new Error(`parts: no part with id "${id}"`);
|
|
47
|
+
}
|
|
48
|
+
return part;
|
|
49
|
+
}
|
|
50
|
+
/** Resolve a warp deformer by stable id. Throws a path-qualified plain
|
|
51
|
+
* `Error` if no deformer matches the id or the match is not a warp deformer.
|
|
52
|
+
* READ/mutate-through accessor, consistent with {@link findPart}. */
|
|
53
|
+
findWarpDeformer(id) {
|
|
54
|
+
const deformer = this.model.deformers?.find((d) => d.id === id);
|
|
55
|
+
if (!deformer || deformer.kind !== "warp") {
|
|
56
|
+
throw new Error(`deformers: no warp deformer with id "${id}"`);
|
|
57
|
+
}
|
|
58
|
+
return deformer;
|
|
59
|
+
}
|
|
60
|
+
/** Resolve a matrix deformer by stable id. Throws a path-qualified plain
|
|
61
|
+
* `Error` if no deformer matches the id or the match is a warp deformer
|
|
62
|
+
* (`kind === "warp"`). A `kind` of `"matrix"` or `undefined` is a matrix
|
|
63
|
+
* deformer. READ/mutate-through accessor, consistent with {@link findPart}. */
|
|
64
|
+
findMatrixDeformer(id) {
|
|
65
|
+
const deformer = this.model.deformers?.find((d) => d.id === id);
|
|
66
|
+
if (!deformer || deformer.kind === "warp") {
|
|
67
|
+
throw new Error(`deformers: no matrix deformer with id "${id}"`);
|
|
68
|
+
}
|
|
69
|
+
return deformer;
|
|
70
|
+
}
|
|
71
|
+
/** Resolve any deformer (matrix or warp) by stable id. Throws a
|
|
72
|
+
* path-qualified plain `Error` if no deformer matches the id.
|
|
73
|
+
* READ/mutate-through accessor, consistent with {@link findPart}. */
|
|
74
|
+
findDeformer(id) {
|
|
75
|
+
const deformer = this.model.deformers?.find((d) => d.id === id);
|
|
76
|
+
if (!deformer) {
|
|
77
|
+
throw new Error(`deformers: no deformer with id "${id}"`);
|
|
78
|
+
}
|
|
79
|
+
return deformer;
|
|
80
|
+
}
|
|
81
|
+
/** Resolve a physics rig by stable id. Throws a path-qualified plain `Error`
|
|
82
|
+
* if no rig matches the id. The `physics` array is optional, so guard it.
|
|
83
|
+
* READ/mutate-through accessor, consistent with {@link findDeformer}. */
|
|
84
|
+
findPhysicsRig(id) {
|
|
85
|
+
const rig = this.model.physics?.find((r) => r.id === id);
|
|
86
|
+
if (!rig) {
|
|
87
|
+
throw new Error(`physics: no physics rig with id "${id}"`);
|
|
88
|
+
}
|
|
89
|
+
return rig;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Record the base mesh UVs for a part inserted AFTER construction (e.g. by
|
|
93
|
+
* {@link AddPart}). Returns the PRIOR entry for that id (or `undefined` if
|
|
94
|
+
* none existed) so the caller can restore it on undo. No-op for meshless
|
|
95
|
+
* parts (returns `undefined`). Must be called by any command that pushes a
|
|
96
|
+
* mesh part into the model so the part joins the constructor-captured base-UV
|
|
97
|
+
* side state required by {@link applyAtlas}.
|
|
98
|
+
*
|
|
99
|
+
* The returned prior value is what {@link restoreBaseMeshUvs} expects on
|
|
100
|
+
* undo — pass it verbatim. Hazard guarded: DeletePart(X) → AddPart(X′) →
|
|
101
|
+
* undo(add) → undo(delete); without restore, X's constructor-captured entry
|
|
102
|
+
* would be permanently gone after undo of the add, causing applyAtlas to
|
|
103
|
+
* fail when X is restored.
|
|
104
|
+
*/
|
|
105
|
+
captureBaseMeshUvs(partId) {
|
|
106
|
+
const prev = this.baseMeshUvs.get(partId);
|
|
107
|
+
const part = this.model.parts.find((p) => p.id === partId);
|
|
108
|
+
if (part?.mesh) {
|
|
109
|
+
this.baseMeshUvs.set(partId, part.mesh.uvs.slice());
|
|
110
|
+
}
|
|
111
|
+
return prev;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Restore the base-UV side state to exactly what it was before a
|
|
115
|
+
* {@link captureBaseMeshUvs} call. Called from {@link AddPart.invert}:
|
|
116
|
+
* pass the value returned by captureBaseMeshUvs on first apply.
|
|
117
|
+
* - `prev` is `number[]` → sets the entry (restores a prior mesh's base).
|
|
118
|
+
* - `prev` is `undefined` → deletes the entry (no entry existed before the
|
|
119
|
+
* add, so a later different-mesh part reusing the id must not inherit this
|
|
120
|
+
* one's base).
|
|
121
|
+
* DeletePart does NOT call this — the entry persists across delete/undo so
|
|
122
|
+
* the restored part still has its base available for applyAtlas.
|
|
123
|
+
*/
|
|
124
|
+
restoreBaseMeshUvs(partId, prev) {
|
|
125
|
+
if (prev !== void 0) {
|
|
126
|
+
this.baseMeshUvs.set(partId, prev);
|
|
127
|
+
} else {
|
|
128
|
+
this.baseMeshUvs.delete(partId);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** The construction-captured base UVs for a mesh part. Throws a path-qualified
|
|
132
|
+
* plain `Error` if absent. SINGLE accessor for both apply branches — never
|
|
133
|
+
* read `baseMeshUvs` with a bare `!` elsewhere. */
|
|
134
|
+
requireBaseUvs(partId) {
|
|
135
|
+
const base = this.baseMeshUvs.get(partId);
|
|
136
|
+
if (!base) {
|
|
137
|
+
throw new Error(`parts: no base mesh uvs captured for part "${partId}"`);
|
|
138
|
+
}
|
|
139
|
+
return base;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Replace the atlas table and rewrite every part's texture reference in a
|
|
143
|
+
* single atomic step. For every part in `partTextureAssignments` set
|
|
144
|
+
* `texture = { index: 0, uv }`; CLEAR `texture` (delete the key) on every
|
|
145
|
+
* other part.
|
|
146
|
+
*
|
|
147
|
+
* Mesh parts are textured as a MATCHED PAIR: an assigned mesh part also has
|
|
148
|
+
* its per-vertex `mesh.uvs` remapped (from the construction-captured base)
|
|
149
|
+
* into the same `uv` rect; an unassigned mesh part has its `mesh.uvs` restored
|
|
150
|
+
* to that base. Quad parts carry `texture.uv` only and are untouched here.
|
|
151
|
+
*
|
|
152
|
+
* Deliberately NON-undoable: it does NOT push to or clear the undo/redo
|
|
153
|
+
* stacks (texture/atlas state is not undoable in 5b — the unified
|
|
154
|
+
* editor-state superset is deferred to 5d). `canUndo()`/`canRedo()` are
|
|
155
|
+
* unchanged after a call.
|
|
156
|
+
*
|
|
157
|
+
* Validate-all-then-apply: structural input validation, per-partId
|
|
158
|
+
* resolution, and a base-UV preflight over every mesh part all run BEFORE any
|
|
159
|
+
* mutation, so a bad input (wrong shape, duplicate partId, unknown partId, or
|
|
160
|
+
* a mesh part with no captured base) throws a plain `Error` and leaves the
|
|
161
|
+
* model exactly as it was — never a partial application.
|
|
162
|
+
*/
|
|
163
|
+
applyAtlas(input) {
|
|
164
|
+
const { texture, assignmentsByPart } = this.normalizeAtlasInput(input);
|
|
165
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
166
|
+
for (const [partId, uv] of assignmentsByPart) {
|
|
167
|
+
resolved.set(this.findPart(partId), uv);
|
|
168
|
+
}
|
|
169
|
+
for (const part of this.model.parts) {
|
|
170
|
+
if (part.mesh) {
|
|
171
|
+
this.requireBaseUvs(part.id);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
this.model.textures = texture === void 0 ? void 0 : [{ source: texture.source }];
|
|
175
|
+
for (const part of this.model.parts) {
|
|
176
|
+
const uv = resolved.get(part);
|
|
177
|
+
if (uv) {
|
|
178
|
+
part.texture = {
|
|
179
|
+
index: 0,
|
|
180
|
+
uv: { x: uv.x, y: uv.y, width: uv.width, height: uv.height }
|
|
181
|
+
};
|
|
182
|
+
if (part.mesh) {
|
|
183
|
+
part.mesh = {
|
|
184
|
+
...part.mesh,
|
|
185
|
+
uvs: remapMeshUvsToRect(this.requireBaseUvs(part.id), uv)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
delete part.texture;
|
|
190
|
+
if (part.mesh) {
|
|
191
|
+
part.mesh = {
|
|
192
|
+
...part.mesh,
|
|
193
|
+
uvs: this.requireBaseUvs(part.id).slice()
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Clear a model-committed texture reference from a single part. Deliberately
|
|
201
|
+
* NON-undoable, matching {@link applyAtlas} — texture/atlas state is not in
|
|
202
|
+
* the undo model (unified editor-state is deferred). Does NOT push to or
|
|
203
|
+
* clear the undo/redo stacks. Does NOT touch `mesh.uvs` — atlas-space UVs on
|
|
204
|
+
* an untextured mesh are inert for color rendering; a later atlas import
|
|
205
|
+
* remaps from the base UVs anyway.
|
|
206
|
+
*
|
|
207
|
+
* This is the single consistent texture/atlas undo boundary: by the time a
|
|
208
|
+
* part is deletable (no texture), its DeletePart snapshot carries no texture
|
|
209
|
+
* reference, so no stale index can resurface on undo after a later atlas
|
|
210
|
+
* repack.
|
|
211
|
+
*
|
|
212
|
+
* Throws a path-qualified plain `Error` (NOT `IkiFormatError`) if the part
|
|
213
|
+
* id is unknown — same contract as {@link findPart}.
|
|
214
|
+
*/
|
|
215
|
+
clearPartTextureRef(partId) {
|
|
216
|
+
const part = this.findPart(partId);
|
|
217
|
+
delete part.texture;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Validate the structural shape of an {@link ApplyAtlasInput} without
|
|
221
|
+
* touching the model, returning a known-good `{ texture?, assignmentsByPart }`
|
|
222
|
+
* for {@link applyAtlas} to resolve and apply.
|
|
223
|
+
*/
|
|
224
|
+
normalizeAtlasInput(input) {
|
|
225
|
+
if (input.textures.length > 1) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`applyAtlas: textures must be a single atlas page (got ${input.textures.length})`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (input.partTextureAssignments.length > 0 && input.textures.length !== 1) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
"applyAtlas: partTextureAssignments require exactly one texture"
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const assignmentsByPart = /* @__PURE__ */ new Map();
|
|
236
|
+
for (const { partId, uv } of input.partTextureAssignments) {
|
|
237
|
+
if (assignmentsByPart.has(partId)) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`applyAtlas: duplicate partId "${partId}" in partTextureAssignments`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
assignmentsByPart.set(partId, uv);
|
|
243
|
+
}
|
|
244
|
+
return { texture: input.textures[0], assignmentsByPart };
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Overwrite a part's whole transform with a fresh copy of `transform`.
|
|
248
|
+
* Replacing the whole object (rather than individual channels) preserves any
|
|
249
|
+
* optional keys already absent from the incoming value.
|
|
250
|
+
*
|
|
251
|
+
* Deliberately NON-undoable — used ONLY for an editor app's transient capture
|
|
252
|
+
* pose. Does NOT push to or clear undoStack/redoStack (sibling to
|
|
253
|
+
* {@link applyAtlas}'s non-undoable boundary). The caller is responsible for
|
|
254
|
+
* restoring the exact prior snapshot when the capture pose ends.
|
|
255
|
+
*
|
|
256
|
+
* `IkiTransform` is a flat number map, so a shallow spread is a sufficient
|
|
257
|
+
* deep copy — no aliasing of the caller's object remains.
|
|
258
|
+
*/
|
|
259
|
+
setPartTransformEphemeral(partId, transform) {
|
|
260
|
+
const part = this.findPart(partId);
|
|
261
|
+
part.transform = { ...transform };
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Overwrite a matrix deformer's optional transform with a fresh copy, or
|
|
265
|
+
* delete it when `transform` is `undefined`.
|
|
266
|
+
* `undefined` deletes the key, restoring the deformer to the same state as
|
|
267
|
+
* one that never had a transform (absent-vs-present matters for downstream
|
|
268
|
+
* renderers).
|
|
269
|
+
*
|
|
270
|
+
* Deliberately NON-undoable — used ONLY for an editor app's transient capture
|
|
271
|
+
* pose. Does NOT push to or clear undoStack/redoStack (sibling to
|
|
272
|
+
* {@link applyAtlas}'s non-undoable boundary). The caller is responsible for
|
|
273
|
+
* restoring the exact prior snapshot when the capture pose ends.
|
|
274
|
+
*
|
|
275
|
+
* `IkiDeformerTransform` is a flat number map, so a shallow spread is a
|
|
276
|
+
* sufficient deep copy — no aliasing of the caller's object remains.
|
|
277
|
+
*/
|
|
278
|
+
setDeformerTransformEphemeral(deformerId, transform) {
|
|
279
|
+
const deformer = this.findMatrixDeformer(deformerId);
|
|
280
|
+
if (transform === void 0) {
|
|
281
|
+
delete deformer.transform;
|
|
282
|
+
} else {
|
|
283
|
+
deformer.transform = { ...transform };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Overwrite a part's whole bindings array with a fresh deep copy, or delete
|
|
288
|
+
* the key when `bindings` is empty.
|
|
289
|
+
*
|
|
290
|
+
* Deliberately NON-undoable — used ONLY for an editor app's transient capture
|
|
291
|
+
* neutralization (zeroing the row being recaptured so the preview reflects
|
|
292
|
+
* base-only during posing). Does NOT push to or clear undoStack/redoStack.
|
|
293
|
+
* The caller is responsible for restoring the original bindings when the
|
|
294
|
+
* capture session ends.
|
|
295
|
+
*/
|
|
296
|
+
setPartBindingsEphemeral(partId, bindings) {
|
|
297
|
+
const part = this.findPart(partId);
|
|
298
|
+
if (bindings.length > 0) {
|
|
299
|
+
part.bindings = bindings.map((b) => ({ ...b }));
|
|
300
|
+
} else {
|
|
301
|
+
delete part.bindings;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Overwrite a matrix deformer's whole bindings array with a fresh deep copy,
|
|
306
|
+
* or delete the key when `bindings` is empty.
|
|
307
|
+
*
|
|
308
|
+
* Deliberately NON-undoable — used ONLY for an editor app's transient capture
|
|
309
|
+
* neutralization (zeroing the row being recaptured so the preview reflects
|
|
310
|
+
* base-only during posing). Does NOT push to or clear undoStack/redoStack.
|
|
311
|
+
* The caller is responsible for restoring the original bindings when the
|
|
312
|
+
* capture session ends.
|
|
313
|
+
*/
|
|
314
|
+
setDeformerBindingsEphemeral(deformerId, bindings) {
|
|
315
|
+
const deformer = this.findMatrixDeformer(deformerId);
|
|
316
|
+
if (bindings.length > 0) {
|
|
317
|
+
deformer.bindings = bindings.map((b) => ({ ...b }));
|
|
318
|
+
} else {
|
|
319
|
+
delete deformer.bindings;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/** Apply a command and record it as one undo step. Clears the redo stack. */
|
|
323
|
+
execute(cmd) {
|
|
324
|
+
cmd.apply(this);
|
|
325
|
+
this.undoStack.push(cmd);
|
|
326
|
+
this.redoStack.length = 0;
|
|
327
|
+
}
|
|
328
|
+
/** Invert the most recent command and move it onto the redo stack. */
|
|
329
|
+
undo() {
|
|
330
|
+
const cmd = this.undoStack.pop();
|
|
331
|
+
if (!cmd) return;
|
|
332
|
+
cmd.invert(this);
|
|
333
|
+
this.redoStack.push(cmd);
|
|
334
|
+
}
|
|
335
|
+
/** Re-apply the most recently undone command and move it back onto undo. */
|
|
336
|
+
redo() {
|
|
337
|
+
const cmd = this.redoStack.pop();
|
|
338
|
+
if (!cmd) return;
|
|
339
|
+
cmd.apply(this);
|
|
340
|
+
this.undoStack.push(cmd);
|
|
341
|
+
}
|
|
342
|
+
canUndo() {
|
|
343
|
+
return this.undoStack.length > 0;
|
|
344
|
+
}
|
|
345
|
+
canRedo() {
|
|
346
|
+
return this.redoStack.length > 0;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Validate and export the current working model by running it through
|
|
350
|
+
* {@link parseIkiModel}. Uses `structuredClone` so the validator's
|
|
351
|
+
* normalized output cannot alias the working model. Propagates
|
|
352
|
+
* `IkiFormatError` unchanged on failure — callers surface `.message`.
|
|
353
|
+
*/
|
|
354
|
+
toIkiModel() {
|
|
355
|
+
return parseIkiModel(structuredClone(this.model));
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Pretty-print the validated model as a `.iki` JSON string. Always
|
|
359
|
+
* validates first — invalid documents never reach a file.
|
|
360
|
+
*/
|
|
361
|
+
serialize() {
|
|
362
|
+
return JSON.stringify(this.toIkiModel(), null, 2);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// src/commands.ts
|
|
367
|
+
import {
|
|
368
|
+
IKI_FORMAT_VERSION,
|
|
369
|
+
IkiFormatError,
|
|
370
|
+
parseIkiModel as parseIkiModel2
|
|
371
|
+
} from "@ikijs/format";
|
|
372
|
+
|
|
373
|
+
// src/grid-keyform.ts
|
|
374
|
+
function interpolateGridOffsets(keyforms, value) {
|
|
375
|
+
if (keyforms.length === 0) {
|
|
376
|
+
throw new Error("interpolateGridOffsets: keyforms must be non-empty");
|
|
377
|
+
}
|
|
378
|
+
if (value <= keyforms[0].value) {
|
|
379
|
+
return [...keyforms[0].offsets];
|
|
380
|
+
}
|
|
381
|
+
const last = keyforms[keyforms.length - 1];
|
|
382
|
+
if (value >= last.value) {
|
|
383
|
+
return [...last.offsets];
|
|
384
|
+
}
|
|
385
|
+
let lo = keyforms[0];
|
|
386
|
+
let hi = keyforms[1];
|
|
387
|
+
for (let k = 1; k < keyforms.length - 1; k++) {
|
|
388
|
+
if (keyforms[k].value <= value) {
|
|
389
|
+
lo = keyforms[k];
|
|
390
|
+
hi = keyforms[k + 1];
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const t = (value - lo.value) / (hi.value - lo.value);
|
|
394
|
+
return lo.offsets.map((loOff, i) => loOff + (hi.offsets[i] - loOff) * t);
|
|
395
|
+
}
|
|
396
|
+
function computeGridOffsets(restPoints, restFrameDraggedPoints) {
|
|
397
|
+
if (restPoints.length !== restFrameDraggedPoints.length) {
|
|
398
|
+
throw new Error(
|
|
399
|
+
`computeGridOffsets: restFrameDraggedPoints length ${restFrameDraggedPoints.length} must equal restPoints length ${restPoints.length}`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
if (restPoints.length % 2 !== 0) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`computeGridOffsets: restPoints length ${restPoints.length} must be even (x,y pairs)`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return restPoints.map((rest, i) => restFrameDraggedPoints[i] - rest);
|
|
408
|
+
}
|
|
409
|
+
function upsertGridKeyform(keyforms, value, offsets) {
|
|
410
|
+
const result = keyforms.map((kf) => ({
|
|
411
|
+
value: kf.value,
|
|
412
|
+
offsets: [...kf.offsets]
|
|
413
|
+
}));
|
|
414
|
+
const existing = result.findIndex((kf) => kf.value === value);
|
|
415
|
+
if (existing !== -1) {
|
|
416
|
+
result[existing] = { value, offsets: [...offsets] };
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
const insertAt = result.findIndex((kf) => kf.value > value);
|
|
420
|
+
const entry = { value, offsets: [...offsets] };
|
|
421
|
+
if (insertAt === -1) {
|
|
422
|
+
result.push(entry);
|
|
423
|
+
} else {
|
|
424
|
+
result.splice(insertAt, 0, entry);
|
|
425
|
+
}
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/reparent.ts
|
|
430
|
+
function kindOf(d) {
|
|
431
|
+
return d.kind === "warp" ? "warp" : "matrix";
|
|
432
|
+
}
|
|
433
|
+
function validateDeformerReparent(deformers, deformerId, newParentId) {
|
|
434
|
+
const target = deformers.find((d) => d.id === deformerId);
|
|
435
|
+
if (target === void 0) {
|
|
436
|
+
throw new Error(`deformers: no deformer with id "${deformerId}"`);
|
|
437
|
+
}
|
|
438
|
+
if (newParentId === void 0) return;
|
|
439
|
+
if (newParentId === deformerId) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`deformers."${deformerId}".parent "${newParentId}" is a self-reference`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
const parent = deformers.find((d) => d.id === newParentId);
|
|
445
|
+
if (parent === void 0) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`deformers."${deformerId}".parent "${newParentId}" is not a declared deformer`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
if (kindOf(parent) === "warp") {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`deformers."${deformerId}".parent "${newParentId}" must be a matrix deformer (warp deformers cannot be parents)`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
456
|
+
for (const d of deformers) {
|
|
457
|
+
if (d.parent !== void 0) parentOf.set(d.id, d.parent);
|
|
458
|
+
}
|
|
459
|
+
parentOf.set(deformerId, newParentId);
|
|
460
|
+
const visited = /* @__PURE__ */ new Set();
|
|
461
|
+
let cur = deformerId;
|
|
462
|
+
while (cur !== void 0) {
|
|
463
|
+
if (visited.has(cur)) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`deformers: reparenting "${deformerId}" under "${newParentId}" would create a cycle`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
visited.add(cur);
|
|
469
|
+
cur = parentOf.get(cur);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function validateDeformerDelete(deformers, parts, physicsChains, deformerId) {
|
|
473
|
+
const target = deformers.find((d) => d.id === deformerId);
|
|
474
|
+
if (target === void 0) {
|
|
475
|
+
throw new Error(`deformers: no deformer with id "${deformerId}"`);
|
|
476
|
+
}
|
|
477
|
+
const childDeformer = deformers.find((d) => d.parent === deformerId);
|
|
478
|
+
if (childDeformer !== void 0) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
`deformers."${deformerId}": cannot delete \u2014 deformer "${childDeformer.id}" is parented to it; reparent or detach it first`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
const attachedPart = parts.find((p) => p.deformer === deformerId);
|
|
484
|
+
if (attachedPart !== void 0) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
`deformers."${deformerId}": cannot delete \u2014 part "${attachedPart.id}" is attached to it; detach it first`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
const anchoredChain = physicsChains.find(
|
|
490
|
+
(c) => c.anchorDeformer === deformerId
|
|
491
|
+
);
|
|
492
|
+
if (anchoredChain !== void 0) {
|
|
493
|
+
throw new Error(
|
|
494
|
+
`deformers."${deformerId}": cannot delete \u2014 physics chain "${anchoredChain.id}" anchors to it; re-anchor or delete the chain first`
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function validatePartAttach(deformers, partId, parts, newDeformerId) {
|
|
499
|
+
const part = parts.find((p) => p.id === partId);
|
|
500
|
+
if (part === void 0) {
|
|
501
|
+
throw new Error(`parts: no part with id "${partId}"`);
|
|
502
|
+
}
|
|
503
|
+
if (newDeformerId === void 0) return;
|
|
504
|
+
const deformer = deformers.find((d) => d.id === newDeformerId);
|
|
505
|
+
if (deformer === void 0) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`parts."${partId}".deformer "${newDeformerId}" is not a declared deformer`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
if (kindOf(deformer) === "warp" && part.mesh === void 0) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`parts."${partId}".deformer "${newDeformerId}" is a warp deformer and requires a mesh`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/commands.ts
|
|
518
|
+
function findIdCollision(model, id) {
|
|
519
|
+
for (const p of model.parts) {
|
|
520
|
+
if (p.id === id) return "part";
|
|
521
|
+
}
|
|
522
|
+
for (const d of model.deformers ?? []) {
|
|
523
|
+
if (d.id === id) return "deformer";
|
|
524
|
+
}
|
|
525
|
+
return void 0;
|
|
526
|
+
}
|
|
527
|
+
var FieldCommand = class {
|
|
528
|
+
constructor(partId, newValue, label, get, set) {
|
|
529
|
+
this.partId = partId;
|
|
530
|
+
this.newValue = newValue;
|
|
531
|
+
this.get = get;
|
|
532
|
+
this.set = set;
|
|
533
|
+
this.label = label;
|
|
534
|
+
}
|
|
535
|
+
partId;
|
|
536
|
+
newValue;
|
|
537
|
+
get;
|
|
538
|
+
set;
|
|
539
|
+
label;
|
|
540
|
+
captured = false;
|
|
541
|
+
prevValue;
|
|
542
|
+
apply(doc) {
|
|
543
|
+
const part = doc.findPart(this.partId);
|
|
544
|
+
if (!this.captured) {
|
|
545
|
+
this.prevValue = this.get(part);
|
|
546
|
+
this.captured = true;
|
|
547
|
+
}
|
|
548
|
+
this.set(part, this.newValue);
|
|
549
|
+
}
|
|
550
|
+
invert(doc) {
|
|
551
|
+
const part = doc.findPart(this.partId);
|
|
552
|
+
this.set(part, this.prevValue);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
var SetPartColor = class extends FieldCommand {
|
|
556
|
+
constructor(partId, rgba) {
|
|
557
|
+
super(
|
|
558
|
+
partId,
|
|
559
|
+
[...rgba],
|
|
560
|
+
"Set color",
|
|
561
|
+
(part) => [...part.color],
|
|
562
|
+
(part, value) => {
|
|
563
|
+
part.color = [...value];
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
var SetPartWidth = class extends FieldCommand {
|
|
569
|
+
constructor(partId, value) {
|
|
570
|
+
super(
|
|
571
|
+
partId,
|
|
572
|
+
value,
|
|
573
|
+
"Set width",
|
|
574
|
+
(part) => part.width,
|
|
575
|
+
(part, v) => {
|
|
576
|
+
part.width = v;
|
|
577
|
+
}
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
var SetPartHeight = class extends FieldCommand {
|
|
582
|
+
constructor(partId, value) {
|
|
583
|
+
super(
|
|
584
|
+
partId,
|
|
585
|
+
value,
|
|
586
|
+
"Set height",
|
|
587
|
+
(part) => part.height,
|
|
588
|
+
(part, v) => {
|
|
589
|
+
part.height = v;
|
|
590
|
+
}
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
var SetPartOrder = class extends FieldCommand {
|
|
595
|
+
constructor(partId, value) {
|
|
596
|
+
super(
|
|
597
|
+
partId,
|
|
598
|
+
value,
|
|
599
|
+
"Set order",
|
|
600
|
+
(part) => part.order,
|
|
601
|
+
(part, v) => {
|
|
602
|
+
part.order = v;
|
|
603
|
+
}
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
var SetPartTransform = class extends FieldCommand {
|
|
608
|
+
constructor(partId, channel, value) {
|
|
609
|
+
super(
|
|
610
|
+
partId,
|
|
611
|
+
value,
|
|
612
|
+
"Set transform",
|
|
613
|
+
(part) => part.transform[channel],
|
|
614
|
+
(part, v) => {
|
|
615
|
+
if (v === void 0) {
|
|
616
|
+
delete part.transform[channel];
|
|
617
|
+
} else {
|
|
618
|
+
part.transform[channel] = v;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
var CaptureGridKeyform = class {
|
|
625
|
+
constructor(deformerId, value, offsets) {
|
|
626
|
+
this.deformerId = deformerId;
|
|
627
|
+
this.value = value;
|
|
628
|
+
this.offsets = [...offsets];
|
|
629
|
+
}
|
|
630
|
+
deformerId;
|
|
631
|
+
value;
|
|
632
|
+
label = "Capture grid keyform";
|
|
633
|
+
offsets;
|
|
634
|
+
captured = false;
|
|
635
|
+
prevKeyforms;
|
|
636
|
+
apply(doc) {
|
|
637
|
+
const deformer = doc.findWarpDeformer(this.deformerId);
|
|
638
|
+
const warp = deformer.warps?.[0];
|
|
639
|
+
if (!warp) {
|
|
640
|
+
throw new Error(
|
|
641
|
+
`deformers."${this.deformerId}".warps: no grid warp to capture into`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
if (this.offsets.length !== deformer.grid.points.length) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
`deformers."${this.deformerId}".warps[0].keyforms.offsets length ${this.offsets.length} must equal grid.points length ${deformer.grid.points.length}`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
const param = doc.getModel().parameters.find((p) => p.id === warp.parameter);
|
|
650
|
+
if (!param) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
`deformers."${this.deformerId}".warps[0].parameter "${warp.parameter}" is not a declared parameter`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
if (this.value < param.min || this.value > param.max) {
|
|
656
|
+
throw new Error(
|
|
657
|
+
`deformers."${this.deformerId}".warps[0].keyforms.value ${this.value} is outside parameter "${warp.parameter}" range [${param.min},${param.max}]`
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
if (!this.captured) {
|
|
661
|
+
this.prevKeyforms = structuredClone(warp.keyforms);
|
|
662
|
+
this.captured = true;
|
|
663
|
+
}
|
|
664
|
+
warp.keyforms = upsertGridKeyform(warp.keyforms, this.value, [
|
|
665
|
+
...this.offsets
|
|
666
|
+
]);
|
|
667
|
+
}
|
|
668
|
+
invert(doc) {
|
|
669
|
+
const warp = doc.findWarpDeformer(this.deformerId).warps?.[0];
|
|
670
|
+
if (!warp) {
|
|
671
|
+
throw new Error(
|
|
672
|
+
`deformers."${this.deformerId}".warps: no grid warp to restore into`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
warp.keyforms = structuredClone(this.prevKeyforms);
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
var DeformerFieldCommand = class {
|
|
679
|
+
constructor(deformerId, newValue, label, get, set) {
|
|
680
|
+
this.deformerId = deformerId;
|
|
681
|
+
this.newValue = newValue;
|
|
682
|
+
this.get = get;
|
|
683
|
+
this.set = set;
|
|
684
|
+
this.label = label;
|
|
685
|
+
}
|
|
686
|
+
deformerId;
|
|
687
|
+
newValue;
|
|
688
|
+
get;
|
|
689
|
+
set;
|
|
690
|
+
label;
|
|
691
|
+
captured = false;
|
|
692
|
+
prevValue;
|
|
693
|
+
apply(doc) {
|
|
694
|
+
const deformer = doc.findMatrixDeformer(this.deformerId);
|
|
695
|
+
if (!this.captured) {
|
|
696
|
+
this.prevValue = this.get(deformer);
|
|
697
|
+
this.captured = true;
|
|
698
|
+
}
|
|
699
|
+
this.set(deformer, this.newValue);
|
|
700
|
+
}
|
|
701
|
+
invert(doc) {
|
|
702
|
+
const deformer = doc.findMatrixDeformer(this.deformerId);
|
|
703
|
+
this.set(deformer, this.prevValue);
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
var SetDeformerPivotX = class extends DeformerFieldCommand {
|
|
707
|
+
constructor(deformerId, value) {
|
|
708
|
+
super(
|
|
709
|
+
deformerId,
|
|
710
|
+
value,
|
|
711
|
+
"Set pivot x",
|
|
712
|
+
(d) => d.pivot.x,
|
|
713
|
+
(d, v) => {
|
|
714
|
+
d.pivot.x = v;
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
var SetDeformerPivotY = class extends DeformerFieldCommand {
|
|
720
|
+
constructor(deformerId, value) {
|
|
721
|
+
super(
|
|
722
|
+
deformerId,
|
|
723
|
+
value,
|
|
724
|
+
"Set pivot y",
|
|
725
|
+
(d) => d.pivot.y,
|
|
726
|
+
(d, v) => {
|
|
727
|
+
d.pivot.y = v;
|
|
728
|
+
}
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
var SetDeformerPivot = class {
|
|
733
|
+
constructor(deformerId, pivot) {
|
|
734
|
+
this.deformerId = deformerId;
|
|
735
|
+
this.pivot = { x: pivot.x, y: pivot.y };
|
|
736
|
+
}
|
|
737
|
+
deformerId;
|
|
738
|
+
label = "Set pivot";
|
|
739
|
+
captured = false;
|
|
740
|
+
prevPivot;
|
|
741
|
+
pivot;
|
|
742
|
+
apply(doc) {
|
|
743
|
+
const deformer = doc.findMatrixDeformer(this.deformerId);
|
|
744
|
+
if (!this.captured) {
|
|
745
|
+
this.prevPivot = { x: deformer.pivot.x, y: deformer.pivot.y };
|
|
746
|
+
this.captured = true;
|
|
747
|
+
}
|
|
748
|
+
deformer.pivot = { x: this.pivot.x, y: this.pivot.y };
|
|
749
|
+
}
|
|
750
|
+
invert(doc) {
|
|
751
|
+
const deformer = doc.findMatrixDeformer(this.deformerId);
|
|
752
|
+
deformer.pivot = { x: this.prevPivot.x, y: this.prevPivot.y };
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
var SetDeformerTransform = class {
|
|
756
|
+
constructor(deformerId, channel, value) {
|
|
757
|
+
this.deformerId = deformerId;
|
|
758
|
+
this.channel = channel;
|
|
759
|
+
this.value = value;
|
|
760
|
+
}
|
|
761
|
+
deformerId;
|
|
762
|
+
channel;
|
|
763
|
+
value;
|
|
764
|
+
label = "Set deformer transform";
|
|
765
|
+
captured = false;
|
|
766
|
+
prevTransform;
|
|
767
|
+
apply(doc) {
|
|
768
|
+
const deformer = doc.findMatrixDeformer(this.deformerId);
|
|
769
|
+
if (!this.captured) {
|
|
770
|
+
this.prevTransform = deformer.transform === void 0 ? void 0 : { ...deformer.transform };
|
|
771
|
+
this.captured = true;
|
|
772
|
+
}
|
|
773
|
+
const next = {
|
|
774
|
+
...deformer.transform ?? { x: 0, y: 0 }
|
|
775
|
+
};
|
|
776
|
+
next[this.channel] = this.value;
|
|
777
|
+
deformer.transform = next;
|
|
778
|
+
}
|
|
779
|
+
invert(doc) {
|
|
780
|
+
const deformer = doc.findMatrixDeformer(this.deformerId);
|
|
781
|
+
if (this.prevTransform === void 0) {
|
|
782
|
+
delete deformer.transform;
|
|
783
|
+
} else {
|
|
784
|
+
deformer.transform = { ...this.prevTransform };
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
var SetDeformerBindings = class {
|
|
789
|
+
constructor(deformerId, bindings) {
|
|
790
|
+
this.deformerId = deformerId;
|
|
791
|
+
this.bindings = bindings.map((b) => ({ ...b }));
|
|
792
|
+
}
|
|
793
|
+
deformerId;
|
|
794
|
+
label = "Set deformer bindings";
|
|
795
|
+
bindings;
|
|
796
|
+
captured = false;
|
|
797
|
+
prevBindings;
|
|
798
|
+
apply(doc) {
|
|
799
|
+
const deformer = doc.findMatrixDeformer(this.deformerId);
|
|
800
|
+
const candidateDeformer = {
|
|
801
|
+
kind: "matrix",
|
|
802
|
+
id: "_d",
|
|
803
|
+
pivot: { x: 0, y: 0 }
|
|
804
|
+
};
|
|
805
|
+
if (this.bindings.length > 0) {
|
|
806
|
+
candidateDeformer.bindings = this.bindings.map((b) => ({ ...b }));
|
|
807
|
+
}
|
|
808
|
+
const candidate = {
|
|
809
|
+
version: IKI_FORMAT_VERSION,
|
|
810
|
+
name: "_",
|
|
811
|
+
canvas: { width: 1, height: 1 },
|
|
812
|
+
parameters: doc.getModel().parameters,
|
|
813
|
+
parts: [
|
|
814
|
+
{
|
|
815
|
+
id: "_",
|
|
816
|
+
color: [0, 0, 0, 1],
|
|
817
|
+
width: 1,
|
|
818
|
+
height: 1,
|
|
819
|
+
transform: { x: 0, y: 0 },
|
|
820
|
+
order: 0
|
|
821
|
+
}
|
|
822
|
+
],
|
|
823
|
+
deformers: [candidateDeformer]
|
|
824
|
+
};
|
|
825
|
+
try {
|
|
826
|
+
parseIkiModel2(candidate);
|
|
827
|
+
} catch (e) {
|
|
828
|
+
if (e instanceof IkiFormatError) {
|
|
829
|
+
throw new IkiFormatError(
|
|
830
|
+
e.message.replace(
|
|
831
|
+
/^deformers\[0\]/,
|
|
832
|
+
`deformers."${this.deformerId}"`
|
|
833
|
+
)
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
throw e;
|
|
837
|
+
}
|
|
838
|
+
if (!this.captured) {
|
|
839
|
+
this.prevBindings = deformer.bindings === void 0 ? void 0 : deformer.bindings.map((b) => ({ ...b }));
|
|
840
|
+
this.captured = true;
|
|
841
|
+
}
|
|
842
|
+
if (this.bindings.length > 0) {
|
|
843
|
+
deformer.bindings = this.bindings.map((b) => ({ ...b }));
|
|
844
|
+
} else {
|
|
845
|
+
delete deformer.bindings;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
invert(doc) {
|
|
849
|
+
const deformer = doc.findMatrixDeformer(this.deformerId);
|
|
850
|
+
if (this.prevBindings === void 0) {
|
|
851
|
+
delete deformer.bindings;
|
|
852
|
+
} else {
|
|
853
|
+
deformer.bindings = this.prevBindings.map((b) => ({ ...b }));
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
var SetDeformerParent = class {
|
|
858
|
+
constructor(deformerId, newParentId) {
|
|
859
|
+
this.deformerId = deformerId;
|
|
860
|
+
this.newParentId = newParentId;
|
|
861
|
+
}
|
|
862
|
+
deformerId;
|
|
863
|
+
newParentId;
|
|
864
|
+
label = "Set deformer parent";
|
|
865
|
+
captured = false;
|
|
866
|
+
prevParent = void 0;
|
|
867
|
+
prevHadParent = false;
|
|
868
|
+
apply(doc) {
|
|
869
|
+
validateDeformerReparent(
|
|
870
|
+
doc.getModel().deformers ?? [],
|
|
871
|
+
this.deformerId,
|
|
872
|
+
this.newParentId
|
|
873
|
+
);
|
|
874
|
+
const deformer = doc.findDeformer(this.deformerId);
|
|
875
|
+
if (!this.captured) {
|
|
876
|
+
this.prevHadParent = Object.prototype.hasOwnProperty.call(
|
|
877
|
+
deformer,
|
|
878
|
+
"parent"
|
|
879
|
+
);
|
|
880
|
+
this.prevParent = deformer.parent;
|
|
881
|
+
this.captured = true;
|
|
882
|
+
}
|
|
883
|
+
if (this.newParentId !== void 0) {
|
|
884
|
+
deformer.parent = this.newParentId;
|
|
885
|
+
} else {
|
|
886
|
+
delete deformer.parent;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
invert(doc) {
|
|
890
|
+
const deformer = doc.findDeformer(this.deformerId);
|
|
891
|
+
if (this.prevHadParent) {
|
|
892
|
+
deformer.parent = this.prevParent;
|
|
893
|
+
} else {
|
|
894
|
+
delete deformer.parent;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
var AddPart = class {
|
|
899
|
+
label = "Add part";
|
|
900
|
+
part;
|
|
901
|
+
captured = false;
|
|
902
|
+
prevBaseMeshUvs = void 0;
|
|
903
|
+
constructor(part) {
|
|
904
|
+
this.part = structuredClone(part);
|
|
905
|
+
}
|
|
906
|
+
apply(doc) {
|
|
907
|
+
const hit = findIdCollision(doc.getModel(), this.part.id);
|
|
908
|
+
if (hit === "part") {
|
|
909
|
+
throw new Error(
|
|
910
|
+
`parts: id "${this.part.id}" collides with an existing part id`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
if (hit === "deformer") {
|
|
914
|
+
throw new Error(
|
|
915
|
+
`parts: id "${this.part.id}" collides with an existing deformer id`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
const candidate = structuredClone(doc.getModel());
|
|
919
|
+
candidate.parts.push(structuredClone(this.part));
|
|
920
|
+
parseIkiModel2(candidate);
|
|
921
|
+
doc.getModel().parts.push(structuredClone(this.part));
|
|
922
|
+
const prev = doc.captureBaseMeshUvs(this.part.id);
|
|
923
|
+
if (!this.captured) {
|
|
924
|
+
this.prevBaseMeshUvs = prev;
|
|
925
|
+
this.captured = true;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
invert(doc) {
|
|
929
|
+
const parts = doc.getModel().parts;
|
|
930
|
+
const i = parts.findIndex((p) => p.id === this.part.id);
|
|
931
|
+
if (i !== -1) parts.splice(i, 1);
|
|
932
|
+
doc.restoreBaseMeshUvs(this.part.id, this.prevBaseMeshUvs);
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
var AddDeformer = class {
|
|
936
|
+
label = "Add deformer";
|
|
937
|
+
deformer;
|
|
938
|
+
captured = false;
|
|
939
|
+
prevDeformersAbsent = false;
|
|
940
|
+
constructor(deformer) {
|
|
941
|
+
this.deformer = structuredClone(deformer);
|
|
942
|
+
}
|
|
943
|
+
apply(doc) {
|
|
944
|
+
const model = doc.getModel();
|
|
945
|
+
const hit = findIdCollision(model, this.deformer.id);
|
|
946
|
+
if (hit === "deformer") {
|
|
947
|
+
throw new Error(
|
|
948
|
+
`deformers: id "${this.deformer.id}" collides with an existing deformer id`
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
if (hit === "part") {
|
|
952
|
+
throw new Error(
|
|
953
|
+
`deformers: id "${this.deformer.id}" collides with an existing part id`
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
const candidate = structuredClone(model);
|
|
957
|
+
candidate.deformers = [
|
|
958
|
+
...candidate.deformers ?? [],
|
|
959
|
+
structuredClone(this.deformer)
|
|
960
|
+
];
|
|
961
|
+
parseIkiModel2(candidate);
|
|
962
|
+
if (!this.captured) {
|
|
963
|
+
this.prevDeformersAbsent = model.deformers === void 0;
|
|
964
|
+
this.captured = true;
|
|
965
|
+
}
|
|
966
|
+
if (model.deformers === void 0) {
|
|
967
|
+
model.deformers = [];
|
|
968
|
+
}
|
|
969
|
+
model.deformers.push(structuredClone(this.deformer));
|
|
970
|
+
}
|
|
971
|
+
invert(doc) {
|
|
972
|
+
const model = doc.getModel();
|
|
973
|
+
const arr = model.deformers;
|
|
974
|
+
if (!arr) return;
|
|
975
|
+
const i = arr.findIndex((d) => d.id === this.deformer.id);
|
|
976
|
+
if (i !== -1) arr.splice(i, 1);
|
|
977
|
+
if (this.prevDeformersAbsent && arr.length === 0) {
|
|
978
|
+
delete model.deformers;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
var DeletePart = class {
|
|
983
|
+
constructor(partId) {
|
|
984
|
+
this.partId = partId;
|
|
985
|
+
}
|
|
986
|
+
partId;
|
|
987
|
+
label = "Delete part";
|
|
988
|
+
captured = false;
|
|
989
|
+
removed;
|
|
990
|
+
index;
|
|
991
|
+
apply(doc) {
|
|
992
|
+
const part = doc.findPart(this.partId);
|
|
993
|
+
if (part.texture !== void 0) {
|
|
994
|
+
throw new Error(
|
|
995
|
+
`parts."${this.partId}": cannot delete \u2014 part has a texture reference; clear its texture first`
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
const parts = doc.getModel().parts;
|
|
999
|
+
const masker = parts.find(
|
|
1000
|
+
(p) => p.id !== this.partId && p.clip?.masks.includes(this.partId)
|
|
1001
|
+
);
|
|
1002
|
+
if (masker) {
|
|
1003
|
+
throw new Error(
|
|
1004
|
+
`parts."${this.partId}": cannot delete \u2014 used as a clip mask by part "${masker.id}"; remove its clip first`
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
const i = parts.indexOf(part);
|
|
1008
|
+
if (!this.captured) {
|
|
1009
|
+
this.removed = structuredClone(part);
|
|
1010
|
+
this.index = i;
|
|
1011
|
+
this.captured = true;
|
|
1012
|
+
}
|
|
1013
|
+
parts.splice(i, 1);
|
|
1014
|
+
}
|
|
1015
|
+
invert(doc) {
|
|
1016
|
+
doc.getModel().parts.splice(this.index, 0, structuredClone(this.removed));
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
var DeleteDeformer = class {
|
|
1020
|
+
constructor(deformerId) {
|
|
1021
|
+
this.deformerId = deformerId;
|
|
1022
|
+
}
|
|
1023
|
+
deformerId;
|
|
1024
|
+
label = "Delete deformer";
|
|
1025
|
+
captured = false;
|
|
1026
|
+
removed;
|
|
1027
|
+
index;
|
|
1028
|
+
apply(doc) {
|
|
1029
|
+
const model = doc.getModel();
|
|
1030
|
+
validateDeformerDelete(
|
|
1031
|
+
model.deformers ?? [],
|
|
1032
|
+
model.parts,
|
|
1033
|
+
model.physicsChains ?? [],
|
|
1034
|
+
this.deformerId
|
|
1035
|
+
);
|
|
1036
|
+
const arr = model.deformers;
|
|
1037
|
+
const i = arr.findIndex((d) => d.id === this.deformerId);
|
|
1038
|
+
if (!this.captured) {
|
|
1039
|
+
this.removed = structuredClone(arr[i]);
|
|
1040
|
+
this.index = i;
|
|
1041
|
+
this.captured = true;
|
|
1042
|
+
}
|
|
1043
|
+
arr.splice(i, 1);
|
|
1044
|
+
}
|
|
1045
|
+
invert(doc) {
|
|
1046
|
+
doc.getModel().deformers.splice(this.index, 0, structuredClone(this.removed));
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
var SetPartBindings = class {
|
|
1050
|
+
constructor(partId, bindings) {
|
|
1051
|
+
this.partId = partId;
|
|
1052
|
+
this.bindings = bindings.map((b) => ({ ...b }));
|
|
1053
|
+
}
|
|
1054
|
+
partId;
|
|
1055
|
+
label = "Set part bindings";
|
|
1056
|
+
bindings;
|
|
1057
|
+
captured = false;
|
|
1058
|
+
prevBindings;
|
|
1059
|
+
apply(doc) {
|
|
1060
|
+
const candidatePart = {
|
|
1061
|
+
id: "_",
|
|
1062
|
+
color: [0, 0, 0, 1],
|
|
1063
|
+
width: 1,
|
|
1064
|
+
height: 1,
|
|
1065
|
+
transform: { x: 0, y: 0 },
|
|
1066
|
+
order: 0
|
|
1067
|
+
};
|
|
1068
|
+
if (this.bindings.length > 0) {
|
|
1069
|
+
candidatePart.bindings = this.bindings.map((b) => ({ ...b }));
|
|
1070
|
+
}
|
|
1071
|
+
const candidate = {
|
|
1072
|
+
version: IKI_FORMAT_VERSION,
|
|
1073
|
+
name: "_",
|
|
1074
|
+
canvas: { width: 1, height: 1 },
|
|
1075
|
+
parameters: doc.getModel().parameters,
|
|
1076
|
+
parts: [candidatePart]
|
|
1077
|
+
};
|
|
1078
|
+
try {
|
|
1079
|
+
parseIkiModel2(candidate);
|
|
1080
|
+
} catch (e) {
|
|
1081
|
+
if (e instanceof IkiFormatError) {
|
|
1082
|
+
throw new IkiFormatError(
|
|
1083
|
+
e.message.replace(/^parts\[0\]/, `parts."${this.partId}"`)
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
throw e;
|
|
1087
|
+
}
|
|
1088
|
+
const part = doc.findPart(this.partId);
|
|
1089
|
+
if (!this.captured) {
|
|
1090
|
+
this.prevBindings = part.bindings === void 0 ? void 0 : part.bindings.map((b) => ({ ...b }));
|
|
1091
|
+
this.captured = true;
|
|
1092
|
+
}
|
|
1093
|
+
if (this.bindings.length > 0) {
|
|
1094
|
+
part.bindings = this.bindings.map((b) => ({ ...b }));
|
|
1095
|
+
} else {
|
|
1096
|
+
delete part.bindings;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
invert(doc) {
|
|
1100
|
+
const part = doc.findPart(this.partId);
|
|
1101
|
+
if (this.prevBindings === void 0) {
|
|
1102
|
+
delete part.bindings;
|
|
1103
|
+
} else {
|
|
1104
|
+
part.bindings = this.prevBindings.map((b) => ({ ...b }));
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
var SetPartDeformer = class {
|
|
1109
|
+
constructor(partId, newDeformerId) {
|
|
1110
|
+
this.partId = partId;
|
|
1111
|
+
this.newDeformerId = newDeformerId;
|
|
1112
|
+
}
|
|
1113
|
+
partId;
|
|
1114
|
+
newDeformerId;
|
|
1115
|
+
label = "Set part deformer";
|
|
1116
|
+
captured = false;
|
|
1117
|
+
prevDeformer = void 0;
|
|
1118
|
+
prevHadDeformer = false;
|
|
1119
|
+
apply(doc) {
|
|
1120
|
+
validatePartAttach(
|
|
1121
|
+
doc.getModel().deformers ?? [],
|
|
1122
|
+
this.partId,
|
|
1123
|
+
doc.getModel().parts,
|
|
1124
|
+
this.newDeformerId
|
|
1125
|
+
);
|
|
1126
|
+
const part = doc.findPart(this.partId);
|
|
1127
|
+
if (!this.captured) {
|
|
1128
|
+
this.prevHadDeformer = Object.prototype.hasOwnProperty.call(
|
|
1129
|
+
part,
|
|
1130
|
+
"deformer"
|
|
1131
|
+
);
|
|
1132
|
+
this.prevDeformer = part.deformer;
|
|
1133
|
+
this.captured = true;
|
|
1134
|
+
}
|
|
1135
|
+
if (this.newDeformerId !== void 0) {
|
|
1136
|
+
part.deformer = this.newDeformerId;
|
|
1137
|
+
} else {
|
|
1138
|
+
delete part.deformer;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
invert(doc) {
|
|
1142
|
+
const part = doc.findPart(this.partId);
|
|
1143
|
+
if (this.prevHadDeformer) {
|
|
1144
|
+
part.deformer = this.prevDeformer;
|
|
1145
|
+
} else {
|
|
1146
|
+
delete part.deformer;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
function isWarpDeformer(model, deformerId) {
|
|
1151
|
+
const d = (model.deformers ?? []).find((x) => x.id === deformerId);
|
|
1152
|
+
return d?.kind === "warp";
|
|
1153
|
+
}
|
|
1154
|
+
var SetPartMesh = class {
|
|
1155
|
+
constructor(partId, mesh) {
|
|
1156
|
+
this.partId = partId;
|
|
1157
|
+
this.mesh = mesh === void 0 ? void 0 : structuredClone(mesh);
|
|
1158
|
+
}
|
|
1159
|
+
partId;
|
|
1160
|
+
label = "Set part mesh";
|
|
1161
|
+
mesh;
|
|
1162
|
+
captured = false;
|
|
1163
|
+
prevHadMesh = false;
|
|
1164
|
+
prevMesh;
|
|
1165
|
+
prevBaseMeshUvs;
|
|
1166
|
+
apply(doc) {
|
|
1167
|
+
const part = doc.findPart(this.partId);
|
|
1168
|
+
if (this.mesh !== void 0) {
|
|
1169
|
+
const candidatePart = {
|
|
1170
|
+
id: "_",
|
|
1171
|
+
color: [0, 0, 0, 1],
|
|
1172
|
+
width: 1,
|
|
1173
|
+
height: 1,
|
|
1174
|
+
transform: { x: 0, y: 0 },
|
|
1175
|
+
order: 0,
|
|
1176
|
+
mesh: structuredClone(this.mesh)
|
|
1177
|
+
};
|
|
1178
|
+
const candidate = {
|
|
1179
|
+
version: IKI_FORMAT_VERSION,
|
|
1180
|
+
name: "_",
|
|
1181
|
+
canvas: { width: 1, height: 1 },
|
|
1182
|
+
parameters: doc.getModel().parameters,
|
|
1183
|
+
parts: [candidatePart]
|
|
1184
|
+
};
|
|
1185
|
+
try {
|
|
1186
|
+
parseIkiModel2(candidate);
|
|
1187
|
+
} catch (e) {
|
|
1188
|
+
if (e instanceof IkiFormatError) {
|
|
1189
|
+
throw new IkiFormatError(
|
|
1190
|
+
e.message.replace(/^parts\[0\]/, `parts."${this.partId}"`)
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
throw e;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const attachedToWarp = part.deformer !== void 0 && isWarpDeformer(doc.getModel(), part.deformer);
|
|
1197
|
+
if (this.mesh === void 0) {
|
|
1198
|
+
if (part.warps !== void 0 || attachedToWarp) {
|
|
1199
|
+
throw new IkiFormatError(
|
|
1200
|
+
`parts."${this.partId}": cannot remove mesh \u2014 part has warps or is attached to a warp deformer; remove its warps / detach from the warp deformer first`
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
const masker = doc.getModel().parts.find(
|
|
1204
|
+
(p) => p.id !== this.partId && p.clip?.masks.includes(this.partId)
|
|
1205
|
+
);
|
|
1206
|
+
if (masker) {
|
|
1207
|
+
throw new IkiFormatError(
|
|
1208
|
+
`parts."${this.partId}": cannot remove mesh \u2014 used as a clip mask by part "${masker.id}" (masks require a mesh); remove its clip first`
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
} else {
|
|
1212
|
+
if ((part.warps?.length ?? 0) > 0) {
|
|
1213
|
+
throw new IkiFormatError(
|
|
1214
|
+
`parts."${this.partId}": cannot regenerate mesh \u2014 part has warps whose offsets are bound to the current rest mesh; remove its warps first`
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (!this.captured) {
|
|
1219
|
+
this.prevHadMesh = part.mesh !== void 0;
|
|
1220
|
+
this.prevMesh = part.mesh ? structuredClone(part.mesh) : void 0;
|
|
1221
|
+
this.prevBaseMeshUvs = doc.captureBaseMeshUvs(this.partId);
|
|
1222
|
+
this.captured = true;
|
|
1223
|
+
}
|
|
1224
|
+
if (this.mesh !== void 0) {
|
|
1225
|
+
const storedUvs = part.texture !== void 0 ? remapMeshUvsToRect(this.mesh.uvs, part.texture.uv) : this.mesh.uvs.slice();
|
|
1226
|
+
part.mesh = {
|
|
1227
|
+
vertices: this.mesh.vertices.slice(),
|
|
1228
|
+
uvs: storedUvs,
|
|
1229
|
+
indices: this.mesh.indices.slice()
|
|
1230
|
+
};
|
|
1231
|
+
} else {
|
|
1232
|
+
delete part.mesh;
|
|
1233
|
+
}
|
|
1234
|
+
if (this.mesh !== void 0) {
|
|
1235
|
+
doc.restoreBaseMeshUvs(this.partId, this.mesh.uvs.slice());
|
|
1236
|
+
} else {
|
|
1237
|
+
doc.restoreBaseMeshUvs(this.partId, void 0);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
invert(doc) {
|
|
1241
|
+
const part = doc.findPart(this.partId);
|
|
1242
|
+
if (this.prevHadMesh) {
|
|
1243
|
+
if (this.prevBaseMeshUvs === void 0) {
|
|
1244
|
+
throw new Error(
|
|
1245
|
+
`parts."${this.partId}": cannot invert SetPartMesh \u2014 no base mesh uvs captured for a mesh part (broken side-table invariant)`
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
const restored = structuredClone(this.prevMesh);
|
|
1249
|
+
restored.uvs = part.texture !== void 0 ? remapMeshUvsToRect(this.prevBaseMeshUvs, part.texture.uv) : this.prevBaseMeshUvs.slice();
|
|
1250
|
+
part.mesh = restored;
|
|
1251
|
+
} else {
|
|
1252
|
+
delete part.mesh;
|
|
1253
|
+
}
|
|
1254
|
+
doc.restoreBaseMeshUvs(this.partId, this.prevBaseMeshUvs);
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
function validatePhysicsCandidate(doc, candidatePhysics) {
|
|
1258
|
+
const candidatePart = {
|
|
1259
|
+
id: "_",
|
|
1260
|
+
color: [0, 0, 0, 1],
|
|
1261
|
+
width: 1,
|
|
1262
|
+
height: 1,
|
|
1263
|
+
transform: { x: 0, y: 0 },
|
|
1264
|
+
order: 0
|
|
1265
|
+
};
|
|
1266
|
+
const candidate = {
|
|
1267
|
+
version: IKI_FORMAT_VERSION,
|
|
1268
|
+
name: "_",
|
|
1269
|
+
canvas: { width: 1, height: 1 },
|
|
1270
|
+
parameters: doc.getModel().parameters,
|
|
1271
|
+
parts: [candidatePart],
|
|
1272
|
+
deformers: doc.getModel().deformers,
|
|
1273
|
+
physics: candidatePhysics,
|
|
1274
|
+
physicsChains: doc.getModel().physicsChains
|
|
1275
|
+
};
|
|
1276
|
+
try {
|
|
1277
|
+
parseIkiModel2(candidate);
|
|
1278
|
+
} catch (e) {
|
|
1279
|
+
if (e instanceof IkiFormatError) {
|
|
1280
|
+
const m = e.message.match(/^physics\[(\d+)\]/);
|
|
1281
|
+
const rigId = m ? candidatePhysics[Number(m[1])]?.id : void 0;
|
|
1282
|
+
if (rigId !== void 0) {
|
|
1283
|
+
throw new IkiFormatError(
|
|
1284
|
+
e.message.replace(/^physics\[\d+\]/, `physics."${rigId}"`)
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
throw e;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
var AddPhysicsRig = class {
|
|
1292
|
+
label = "Add physics rig";
|
|
1293
|
+
rig;
|
|
1294
|
+
captured = false;
|
|
1295
|
+
prevPhysicsAbsent = false;
|
|
1296
|
+
constructor(rig) {
|
|
1297
|
+
this.rig = structuredClone(rig);
|
|
1298
|
+
}
|
|
1299
|
+
apply(doc) {
|
|
1300
|
+
const model = doc.getModel();
|
|
1301
|
+
if ((model.physics ?? []).some((r) => r.id === this.rig.id)) {
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
`physics: id "${this.rig.id}" collides with an existing physics rig id`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
validatePhysicsCandidate(doc, [
|
|
1307
|
+
...model.physics ?? [],
|
|
1308
|
+
structuredClone(this.rig)
|
|
1309
|
+
]);
|
|
1310
|
+
if (!this.captured) {
|
|
1311
|
+
this.prevPhysicsAbsent = model.physics === void 0;
|
|
1312
|
+
this.captured = true;
|
|
1313
|
+
}
|
|
1314
|
+
if (model.physics === void 0) {
|
|
1315
|
+
model.physics = [];
|
|
1316
|
+
}
|
|
1317
|
+
model.physics.push(structuredClone(this.rig));
|
|
1318
|
+
}
|
|
1319
|
+
invert(doc) {
|
|
1320
|
+
const model = doc.getModel();
|
|
1321
|
+
const arr = model.physics;
|
|
1322
|
+
if (!arr) return;
|
|
1323
|
+
const i = arr.findIndex((r) => r.id === this.rig.id);
|
|
1324
|
+
if (i !== -1) arr.splice(i, 1);
|
|
1325
|
+
if (this.prevPhysicsAbsent && arr.length === 0) {
|
|
1326
|
+
delete model.physics;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
var DeletePhysicsRig = class {
|
|
1331
|
+
constructor(rigId) {
|
|
1332
|
+
this.rigId = rigId;
|
|
1333
|
+
}
|
|
1334
|
+
rigId;
|
|
1335
|
+
label = "Delete physics rig";
|
|
1336
|
+
captured = false;
|
|
1337
|
+
removed;
|
|
1338
|
+
index;
|
|
1339
|
+
apply(doc) {
|
|
1340
|
+
const rig = doc.findPhysicsRig(this.rigId);
|
|
1341
|
+
const arr = doc.getModel().physics;
|
|
1342
|
+
const i = arr.indexOf(rig);
|
|
1343
|
+
if (!this.captured) {
|
|
1344
|
+
this.removed = structuredClone(rig);
|
|
1345
|
+
this.index = i;
|
|
1346
|
+
this.captured = true;
|
|
1347
|
+
}
|
|
1348
|
+
arr.splice(i, 1);
|
|
1349
|
+
if (arr.length === 0) {
|
|
1350
|
+
delete doc.getModel().physics;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
invert(doc) {
|
|
1354
|
+
const model = doc.getModel();
|
|
1355
|
+
(model.physics ??= []).splice(this.index, 0, structuredClone(this.removed));
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
var SetPhysicsRig = class {
|
|
1359
|
+
constructor(rigId, rig) {
|
|
1360
|
+
this.rigId = rigId;
|
|
1361
|
+
this.rig = structuredClone(rig);
|
|
1362
|
+
}
|
|
1363
|
+
rigId;
|
|
1364
|
+
label = "Set physics rig";
|
|
1365
|
+
rig;
|
|
1366
|
+
captured = false;
|
|
1367
|
+
prevRig;
|
|
1368
|
+
apply(doc) {
|
|
1369
|
+
if (this.rig.id !== this.rigId) {
|
|
1370
|
+
throw new Error(
|
|
1371
|
+
`physics."${this.rigId}": cannot change rig id to "${this.rig.id}" (rename unsupported)`
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
const model = doc.getModel();
|
|
1375
|
+
const i = (model.physics ?? []).findIndex((r) => r.id === this.rigId);
|
|
1376
|
+
if (i === -1) {
|
|
1377
|
+
throw new Error(`physics: no physics rig with id "${this.rigId}"`);
|
|
1378
|
+
}
|
|
1379
|
+
const candidate = model.physics.map(
|
|
1380
|
+
(r, idx) => idx === i ? structuredClone(this.rig) : r
|
|
1381
|
+
);
|
|
1382
|
+
validatePhysicsCandidate(doc, candidate);
|
|
1383
|
+
if (!this.captured) {
|
|
1384
|
+
this.prevRig = structuredClone(model.physics[i]);
|
|
1385
|
+
this.captured = true;
|
|
1386
|
+
}
|
|
1387
|
+
model.physics[i] = structuredClone(this.rig);
|
|
1388
|
+
}
|
|
1389
|
+
invert(doc) {
|
|
1390
|
+
const arr = doc.getModel().physics;
|
|
1391
|
+
if (!arr) return;
|
|
1392
|
+
const i = arr.findIndex((r) => r.id === this.rigId);
|
|
1393
|
+
if (i !== -1) arr[i] = structuredClone(this.prevRig);
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
// src/alpha-bbox.ts
|
|
1398
|
+
var ALPHA_BBOX_THRESHOLD = 8;
|
|
1399
|
+
function detectAlphaBbox(rgba, width, height) {
|
|
1400
|
+
let minX = width;
|
|
1401
|
+
let maxX = -1;
|
|
1402
|
+
let minY = height;
|
|
1403
|
+
let maxY = -1;
|
|
1404
|
+
for (let y3 = 0; y3 < height; y3++) {
|
|
1405
|
+
for (let x3 = 0; x3 < width; x3++) {
|
|
1406
|
+
const alpha = rgba[(y3 * width + x3) * 4 + 3];
|
|
1407
|
+
if (alpha >= ALPHA_BBOX_THRESHOLD) {
|
|
1408
|
+
if (x3 < minX) minX = x3;
|
|
1409
|
+
if (x3 > maxX) maxX = x3;
|
|
1410
|
+
if (y3 < minY) minY = y3;
|
|
1411
|
+
if (y3 > maxY) maxY = y3;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (maxX === -1) return null;
|
|
1416
|
+
const x = Math.max(0, minX - 1);
|
|
1417
|
+
const y = Math.max(0, minY - 1);
|
|
1418
|
+
const x2 = Math.min(width - 1, maxX + 1);
|
|
1419
|
+
const y2 = Math.min(height - 1, maxY + 1);
|
|
1420
|
+
return { x, y, w: x2 - x + 1, h: y2 - y + 1 };
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/binding-capture.ts
|
|
1424
|
+
function captureBindingEndpoint(channel, restValue, posedValue) {
|
|
1425
|
+
if (channel === "opacity") {
|
|
1426
|
+
return restValue === 0 ? 0 : posedValue / restValue;
|
|
1427
|
+
}
|
|
1428
|
+
return posedValue - restValue;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// src/atlas.ts
|
|
1432
|
+
var ATLAS_PADDING = 2;
|
|
1433
|
+
var UV_INSET_PX = 0.5;
|
|
1434
|
+
function packAtlas(sources, padding = ATLAS_PADDING) {
|
|
1435
|
+
for (const src of sources) {
|
|
1436
|
+
if (!isFinite(src.width) || src.width <= 0) {
|
|
1437
|
+
throw new Error(
|
|
1438
|
+
`packAtlas: source "${src.id}" has invalid width ${src.width}`
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
if (!isFinite(src.height) || src.height <= 0) {
|
|
1442
|
+
throw new Error(
|
|
1443
|
+
`packAtlas: source "${src.id}" has invalid height ${src.height}`
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
if (sources.length === 0) {
|
|
1448
|
+
return { pageWidth: 0, pageHeight: 0, placements: [], padding };
|
|
1449
|
+
}
|
|
1450
|
+
const sorted = sources.slice().sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
1451
|
+
const totalArea = sorted.reduce(
|
|
1452
|
+
(sum, s) => sum + (s.width + padding) * (s.height + padding),
|
|
1453
|
+
0
|
|
1454
|
+
);
|
|
1455
|
+
const targetWidth = Math.max(
|
|
1456
|
+
Math.ceil(Math.sqrt(totalArea)),
|
|
1457
|
+
// Ensure at least the widest single source fits.
|
|
1458
|
+
Math.max(...sorted.map((s) => s.width + padding))
|
|
1459
|
+
);
|
|
1460
|
+
const placements = [];
|
|
1461
|
+
let shelfX = 0;
|
|
1462
|
+
let shelfY = 0;
|
|
1463
|
+
let shelfHeight = 0;
|
|
1464
|
+
for (const src of sorted) {
|
|
1465
|
+
const paddedW = src.width + padding;
|
|
1466
|
+
const paddedH = src.height + padding;
|
|
1467
|
+
if (shelfX > 0 && shelfX + paddedW > targetWidth) {
|
|
1468
|
+
shelfY += shelfHeight;
|
|
1469
|
+
shelfX = 0;
|
|
1470
|
+
shelfHeight = 0;
|
|
1471
|
+
}
|
|
1472
|
+
placements.push({
|
|
1473
|
+
id: src.id,
|
|
1474
|
+
x: shelfX,
|
|
1475
|
+
y: shelfY,
|
|
1476
|
+
width: src.width,
|
|
1477
|
+
height: src.height
|
|
1478
|
+
});
|
|
1479
|
+
shelfX += paddedW;
|
|
1480
|
+
if (paddedH > shelfHeight) shelfHeight = paddedH;
|
|
1481
|
+
}
|
|
1482
|
+
let pageWidth = 0;
|
|
1483
|
+
let pageHeight = 0;
|
|
1484
|
+
for (const p of placements) {
|
|
1485
|
+
const right = p.x + p.width + padding;
|
|
1486
|
+
const bottom = p.y + p.height + padding;
|
|
1487
|
+
if (right > pageWidth) pageWidth = right;
|
|
1488
|
+
if (bottom > pageHeight) pageHeight = bottom;
|
|
1489
|
+
}
|
|
1490
|
+
return { pageWidth, pageHeight, placements, padding };
|
|
1491
|
+
}
|
|
1492
|
+
function uvRectFor(placement, page, insetPx = UV_INSET_PX) {
|
|
1493
|
+
const px = placement.x + insetPx;
|
|
1494
|
+
const py = placement.y + insetPx;
|
|
1495
|
+
const pw = placement.width - insetPx * 2;
|
|
1496
|
+
const ph = placement.height - insetPx * 2;
|
|
1497
|
+
const x = Math.max(0, px / page.width);
|
|
1498
|
+
const y = Math.max(0, py / page.height);
|
|
1499
|
+
const width = Math.min(1 - x, Math.max(0, pw / page.width));
|
|
1500
|
+
const height = Math.min(1 - y, Math.max(0, ph / page.height));
|
|
1501
|
+
return { x, y, width, height };
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// src/factories.ts
|
|
1505
|
+
function generateUniqueId(model, base) {
|
|
1506
|
+
const used = /* @__PURE__ */ new Set();
|
|
1507
|
+
for (const p of model.parts) {
|
|
1508
|
+
used.add(p.id);
|
|
1509
|
+
}
|
|
1510
|
+
for (const d of model.deformers ?? []) {
|
|
1511
|
+
used.add(d.id);
|
|
1512
|
+
}
|
|
1513
|
+
if (!used.has(base)) return base;
|
|
1514
|
+
let n = 2;
|
|
1515
|
+
while (true) {
|
|
1516
|
+
const candidate = `${base}_${n}`;
|
|
1517
|
+
if (!used.has(candidate)) return candidate;
|
|
1518
|
+
n++;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
function generateRegularGridPoints(cols, rows, minX, maxX, minY, maxY) {
|
|
1522
|
+
const pts = [];
|
|
1523
|
+
for (let row = 0; row <= rows; row++) {
|
|
1524
|
+
const t = row / rows;
|
|
1525
|
+
const y = maxY - t * (maxY - minY);
|
|
1526
|
+
for (let col = 0; col <= cols; col++) {
|
|
1527
|
+
const s = col / cols;
|
|
1528
|
+
const x = minX + s * (maxX - minX);
|
|
1529
|
+
pts.push(x, y);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return pts;
|
|
1533
|
+
}
|
|
1534
|
+
function createDefaultPart(model) {
|
|
1535
|
+
const order = model.parts.length ? Math.max(...model.parts.map((p) => p.order)) + 1 : 0;
|
|
1536
|
+
return {
|
|
1537
|
+
id: generateUniqueId(model, "part"),
|
|
1538
|
+
color: [0.45, 0.6, 0.85, 1],
|
|
1539
|
+
width: 150,
|
|
1540
|
+
height: 150,
|
|
1541
|
+
transform: { x: 0, y: 0 },
|
|
1542
|
+
order
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
function createDefaultMatrixDeformer(model) {
|
|
1546
|
+
return {
|
|
1547
|
+
id: generateUniqueId(model, "deformer"),
|
|
1548
|
+
pivot: { x: 0, y: 0 }
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
function createGridMesh(cols, rows) {
|
|
1552
|
+
if (!Number.isInteger(cols) || cols < 1 || !Number.isInteger(rows) || rows < 1 || (cols + 1) * (rows + 1) > 65536) {
|
|
1553
|
+
throw new Error(
|
|
1554
|
+
"createGridMesh: cols and rows must be integers >= 1 with (cols+1)*(rows+1) <= 65536"
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
const colVerts = cols + 1;
|
|
1558
|
+
const rowVerts = rows + 1;
|
|
1559
|
+
const vertices = [];
|
|
1560
|
+
const uvs = [];
|
|
1561
|
+
for (let row = 0; row < rowVerts; row++) {
|
|
1562
|
+
const t = row / rows;
|
|
1563
|
+
const y = 0.5 - t;
|
|
1564
|
+
const v = t;
|
|
1565
|
+
for (let col = 0; col < colVerts; col++) {
|
|
1566
|
+
const s = col / cols;
|
|
1567
|
+
const x = -0.5 + s;
|
|
1568
|
+
const u = s;
|
|
1569
|
+
vertices.push(x, y);
|
|
1570
|
+
uvs.push(u, v);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
const indices = [];
|
|
1574
|
+
for (let row = 0; row < rows; row++) {
|
|
1575
|
+
for (let col = 0; col < cols; col++) {
|
|
1576
|
+
const tl = row * colVerts + col;
|
|
1577
|
+
const tr = row * colVerts + col + 1;
|
|
1578
|
+
const bl = (row + 1) * colVerts + col;
|
|
1579
|
+
const br = (row + 1) * colVerts + col + 1;
|
|
1580
|
+
indices.push(bl, br, tl);
|
|
1581
|
+
indices.push(tl, br, tr);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return { vertices, uvs, indices };
|
|
1585
|
+
}
|
|
1586
|
+
function createDefaultWarpDeformer(model) {
|
|
1587
|
+
const hx = model.canvas.width / 4;
|
|
1588
|
+
const hy = model.canvas.height / 4;
|
|
1589
|
+
return {
|
|
1590
|
+
kind: "warp",
|
|
1591
|
+
id: generateUniqueId(model, "warp"),
|
|
1592
|
+
grid: {
|
|
1593
|
+
cols: 4,
|
|
1594
|
+
rows: 4,
|
|
1595
|
+
points: generateRegularGridPoints(4, 4, -hx, hx, -hy, hy)
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// src/auto-rig.ts
|
|
1601
|
+
import {
|
|
1602
|
+
IKI_FORMAT_VERSION as IKI_FORMAT_VERSION2,
|
|
1603
|
+
StandardParameter,
|
|
1604
|
+
parseIkiModel as parseIkiModel3
|
|
1605
|
+
} from "@ikijs/format";
|
|
1606
|
+
var ROLE_TABLE = {
|
|
1607
|
+
// hair_back stays rigid (silhouette behind the face); per-layer front/back
|
|
1608
|
+
// depth parallax is a later slice.
|
|
1609
|
+
hair_back: { deformer: "headDeformer", order: 0, mesh: false },
|
|
1610
|
+
face: { deformer: "faceWarp", order: 10, mesh: true },
|
|
1611
|
+
nose: { deformer: "faceWarp", order: 15, mesh: true },
|
|
1612
|
+
blush_L: { deformer: "faceWarp", order: 20, mesh: true },
|
|
1613
|
+
blush_R: { deformer: "faceWarp", order: 20, mesh: true },
|
|
1614
|
+
mouth: { deformer: "faceWarp", order: 25, mesh: true },
|
|
1615
|
+
eye_L: { deformer: "faceWarp", order: 30, mesh: true, eyeSide: "L" },
|
|
1616
|
+
eye_R: { deformer: "faceWarp", order: 30, mesh: true, eyeSide: "R" },
|
|
1617
|
+
iris_L: { deformer: "faceWarp", order: 31, mesh: true, eyeSide: "L" },
|
|
1618
|
+
iris_R: { deformer: "faceWarp", order: 31, mesh: true, eyeSide: "R" },
|
|
1619
|
+
pupil_L: { deformer: "faceWarp", order: 32, mesh: true, eyeSide: "L" },
|
|
1620
|
+
pupil_R: { deformer: "faceWarp", order: 32, mesh: true, eyeSide: "R" },
|
|
1621
|
+
highlight_L: { deformer: "faceWarp", order: 33, mesh: true, eyeSide: "L" },
|
|
1622
|
+
highlight_R: { deformer: "faceWarp", order: 33, mesh: true, eyeSide: "R" },
|
|
1623
|
+
// Upper lashes: an OPTIONAL separate layer ABOVE the iris that folds down to
|
|
1624
|
+
// the closed-eye seam (the same crease the white folds to), covering the cut
|
|
1625
|
+
// eyeball cleanly. When absent, the white's own fold is the only closed line.
|
|
1626
|
+
lash_L: { deformer: "faceWarp", order: 34, mesh: true, eyeSide: "L" },
|
|
1627
|
+
lash_R: { deformer: "faceWarp", order: 34, mesh: true, eyeSide: "R" },
|
|
1628
|
+
brow_L: { deformer: "faceWarp", order: 40, mesh: true },
|
|
1629
|
+
brow_R: { deformer: "faceWarp", order: 40, mesh: true },
|
|
1630
|
+
// Front hair rides faceWarp (mesh) so it follows the head-turn curvature with
|
|
1631
|
+
// the face instead of detaching as a rigid blob; its bbox joins the faceWarp
|
|
1632
|
+
// grid union so the grid covers it.
|
|
1633
|
+
hair_front: { deformer: "faceWarp", order: 50, mesh: true }
|
|
1634
|
+
};
|
|
1635
|
+
var REQUIRED_ROLES = ["face", "eye_L", "eye_R", "mouth"];
|
|
1636
|
+
var ALIAS_MAP = {
|
|
1637
|
+
eyebrow_L: "brow_L",
|
|
1638
|
+
eyebrow_R: "brow_R",
|
|
1639
|
+
eye_white_L: "eye_L",
|
|
1640
|
+
eye_white_R: "eye_R"
|
|
1641
|
+
};
|
|
1642
|
+
function normalizeRole(raw) {
|
|
1643
|
+
const noExt = raw.replace(/\.[^.]+$/, "");
|
|
1644
|
+
const collapsed = noExt.toLowerCase().replace(/[-\s]+/g, "_");
|
|
1645
|
+
const sided = collapsed.replace(
|
|
1646
|
+
/_([lr])$/,
|
|
1647
|
+
(_, s) => `_${s.toUpperCase()}`
|
|
1648
|
+
);
|
|
1649
|
+
return ALIAS_MAP[sided] ?? sided;
|
|
1650
|
+
}
|
|
1651
|
+
function assertRoleSet(roles) {
|
|
1652
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1653
|
+
for (const role of roles) {
|
|
1654
|
+
if (!(role in ROLE_TABLE)) {
|
|
1655
|
+
throw new Error(`auto-rig: unknown role "${role}"`);
|
|
1656
|
+
}
|
|
1657
|
+
if (seen.has(role)) {
|
|
1658
|
+
throw new Error(`auto-rig: duplicate role "${role}"`);
|
|
1659
|
+
}
|
|
1660
|
+
seen.add(role);
|
|
1661
|
+
}
|
|
1662
|
+
for (const required of REQUIRED_ROLES) {
|
|
1663
|
+
if (!seen.has(required)) {
|
|
1664
|
+
throw new Error(`auto-rig: missing required role "${required}"`);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
function parseLayerRoles(fileNames) {
|
|
1669
|
+
const pairs = fileNames.map((fileName) => {
|
|
1670
|
+
const role = normalizeRole(fileName);
|
|
1671
|
+
if (!(role in ROLE_TABLE)) {
|
|
1672
|
+
throw new Error(
|
|
1673
|
+
`auto-rig: unknown role "${role}" from file "${fileName}"`
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
return { role, fileName };
|
|
1677
|
+
});
|
|
1678
|
+
assertRoleSet(pairs.map((p) => p.role));
|
|
1679
|
+
return pairs;
|
|
1680
|
+
}
|
|
1681
|
+
function bboxToTransform(bbox, canvasW, canvasH, partLabel) {
|
|
1682
|
+
if (bbox.w <= 0 || bbox.h <= 0) {
|
|
1683
|
+
throw new Error(`auto-rig: empty bbox for ${partLabel ?? "layer"}`);
|
|
1684
|
+
}
|
|
1685
|
+
const x = bbox.x + bbox.w / 2 - canvasW / 2;
|
|
1686
|
+
const y = canvasH / 2 - (bbox.y + bbox.h / 2);
|
|
1687
|
+
return { x, y };
|
|
1688
|
+
}
|
|
1689
|
+
function validateLayerInputs(layers, canvas) {
|
|
1690
|
+
if (layers.length === 0) {
|
|
1691
|
+
throw new Error("auto-rig: validateLayerInputs: layers must not be empty");
|
|
1692
|
+
}
|
|
1693
|
+
assertRoleSet(layers.map((l) => l.role));
|
|
1694
|
+
for (const layer of layers) {
|
|
1695
|
+
const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;
|
|
1696
|
+
if (bbox.w <= 0) {
|
|
1697
|
+
throw new Error(
|
|
1698
|
+
`auto-rig: validateLayerInputs: role "${role}" has non-positive bbox.w (${bbox.w})`
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
if (bbox.h <= 0) {
|
|
1702
|
+
throw new Error(
|
|
1703
|
+
`auto-rig: validateLayerInputs: role "${role}" has non-positive bbox.h (${bbox.h})`
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
if (cropW <= 0) {
|
|
1707
|
+
throw new Error(
|
|
1708
|
+
`auto-rig: validateLayerInputs: role "${role}" has non-positive cropW (${cropW})`
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
if (cropH <= 0) {
|
|
1712
|
+
throw new Error(
|
|
1713
|
+
`auto-rig: validateLayerInputs: role "${role}" has non-positive cropH (${cropH})`
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
if (canvasW !== canvas.width || canvasH !== canvas.height) {
|
|
1717
|
+
throw new Error(
|
|
1718
|
+
`auto-rig: validateLayerInputs: role "${role}" canvas size (${canvasW}\xD7${canvasH}) does not match canvas arg (${canvas.width}\xD7${canvas.height})`
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
function generateGridPoints(cols, rows, minX, maxX, minY, maxY) {
|
|
1724
|
+
const pts = [];
|
|
1725
|
+
for (let row = 0; row <= rows; row++) {
|
|
1726
|
+
const t = row / rows;
|
|
1727
|
+
const y = maxY - t * (maxY - minY);
|
|
1728
|
+
for (let col = 0; col <= cols; col++) {
|
|
1729
|
+
const s = col / cols;
|
|
1730
|
+
const x = minX + s * (maxX - minX);
|
|
1731
|
+
pts.push(x, y);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return pts;
|
|
1735
|
+
}
|
|
1736
|
+
function createPixelGridMesh(cols, rows, w, h) {
|
|
1737
|
+
const colVerts = cols + 1;
|
|
1738
|
+
const rowVerts = rows + 1;
|
|
1739
|
+
const vertices = [];
|
|
1740
|
+
const uvs = [];
|
|
1741
|
+
for (let row = 0; row < rowVerts; row++) {
|
|
1742
|
+
const t = row / rows;
|
|
1743
|
+
const y = h / 2 - t * h;
|
|
1744
|
+
const v = t;
|
|
1745
|
+
for (let col = 0; col < colVerts; col++) {
|
|
1746
|
+
const s = col / cols;
|
|
1747
|
+
const x = -w / 2 + s * w;
|
|
1748
|
+
const u = s;
|
|
1749
|
+
vertices.push(x, y);
|
|
1750
|
+
uvs.push(u, v);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
const indices = [];
|
|
1754
|
+
for (let row = 0; row < rows; row++) {
|
|
1755
|
+
for (let col = 0; col < cols; col++) {
|
|
1756
|
+
const tl = row * colVerts + col;
|
|
1757
|
+
const tr = row * colVerts + col + 1;
|
|
1758
|
+
const bl = (row + 1) * colVerts + col;
|
|
1759
|
+
const br = (row + 1) * colVerts + col + 1;
|
|
1760
|
+
indices.push(bl, br, tl);
|
|
1761
|
+
indices.push(tl, br, tr);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
return { vertices, uvs, indices };
|
|
1765
|
+
}
|
|
1766
|
+
function bakeHeadTurnGridWarpCentered(grid, parameter, centerX) {
|
|
1767
|
+
const ANGLES = [-30, 0, 30];
|
|
1768
|
+
const halfWidth = (grid.points[grid.cols * 2] - grid.points[0]) / 2;
|
|
1769
|
+
const RADIUS = halfWidth * (0.6 / 0.5);
|
|
1770
|
+
const pointCount = grid.points.length / 2;
|
|
1771
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
1772
|
+
const keyforms = ANGLES.map((angleDeg) => {
|
|
1773
|
+
const theta = angleDeg * DEG_TO_RAD;
|
|
1774
|
+
const offsets = [];
|
|
1775
|
+
for (let i = 0; i < pointCount; i++) {
|
|
1776
|
+
const x = grid.points[i * 2];
|
|
1777
|
+
const localX = x - centerX;
|
|
1778
|
+
const alpha = Math.asin(Math.max(-1, Math.min(1, localX / RADIUS)));
|
|
1779
|
+
const xPrime = centerX + RADIUS * Math.sin(alpha + theta);
|
|
1780
|
+
const dx = xPrime - x;
|
|
1781
|
+
offsets.push(dx, 0);
|
|
1782
|
+
}
|
|
1783
|
+
return { value: angleDeg, offsets };
|
|
1784
|
+
});
|
|
1785
|
+
return { parameter, keyforms };
|
|
1786
|
+
}
|
|
1787
|
+
var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
|
|
1788
|
+
function bindingsForRole(spec, role, cropW, cropH) {
|
|
1789
|
+
const isEyeStack = EYE_STACK_PREFIXES.some((p) => role.startsWith(p));
|
|
1790
|
+
if (isEyeStack && spec.eyeSide !== void 0) {
|
|
1791
|
+
const isGazeRole = role.startsWith("iris_") || role.startsWith("pupil_") || role.startsWith("highlight_");
|
|
1792
|
+
if (!isGazeRole) return [];
|
|
1793
|
+
const gx = Math.min(cropW * 0.18, 22);
|
|
1794
|
+
const gy = Math.min(cropH * 0.18, 16);
|
|
1795
|
+
return [
|
|
1796
|
+
{
|
|
1797
|
+
parameter: StandardParameter.EyeballX,
|
|
1798
|
+
channel: "translateX",
|
|
1799
|
+
from: -gx,
|
|
1800
|
+
to: gx
|
|
1801
|
+
},
|
|
1802
|
+
{
|
|
1803
|
+
parameter: StandardParameter.EyeballY,
|
|
1804
|
+
channel: "translateY",
|
|
1805
|
+
from: -gy,
|
|
1806
|
+
to: gy
|
|
1807
|
+
}
|
|
1808
|
+
];
|
|
1809
|
+
}
|
|
1810
|
+
if (role === "mouth") {
|
|
1811
|
+
return [
|
|
1812
|
+
// Mouth open: scaleY from 0 (closed, param=0) to 3 (wide open, param=1).
|
|
1813
|
+
{
|
|
1814
|
+
parameter: StandardParameter.MouthOpen,
|
|
1815
|
+
channel: "scaleY",
|
|
1816
|
+
from: 0,
|
|
1817
|
+
to: 3
|
|
1818
|
+
},
|
|
1819
|
+
// Mouth form: scaleX from -0.2 (pursed, param=-1) to 0.4 (wide, param=1).
|
|
1820
|
+
{
|
|
1821
|
+
parameter: StandardParameter.MouthForm,
|
|
1822
|
+
channel: "scaleX",
|
|
1823
|
+
from: -0.2,
|
|
1824
|
+
to: 0.4
|
|
1825
|
+
}
|
|
1826
|
+
];
|
|
1827
|
+
}
|
|
1828
|
+
if (role === "brow_L" || role === "brow_R") {
|
|
1829
|
+
const ty = Math.min(cropH * 0.8, 18);
|
|
1830
|
+
const deg = 12;
|
|
1831
|
+
if (role === "brow_L") {
|
|
1832
|
+
return [
|
|
1833
|
+
{
|
|
1834
|
+
parameter: StandardParameter.BrowLeftY,
|
|
1835
|
+
channel: "translateY",
|
|
1836
|
+
from: -ty,
|
|
1837
|
+
to: ty
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
parameter: StandardParameter.BrowLeftAngle,
|
|
1841
|
+
channel: "rotate",
|
|
1842
|
+
from: -deg,
|
|
1843
|
+
to: deg
|
|
1844
|
+
}
|
|
1845
|
+
];
|
|
1846
|
+
} else {
|
|
1847
|
+
return [
|
|
1848
|
+
{
|
|
1849
|
+
parameter: StandardParameter.BrowRightY,
|
|
1850
|
+
channel: "translateY",
|
|
1851
|
+
from: -ty,
|
|
1852
|
+
to: ty
|
|
1853
|
+
},
|
|
1854
|
+
{
|
|
1855
|
+
parameter: StandardParameter.BrowRightAngle,
|
|
1856
|
+
channel: "rotate",
|
|
1857
|
+
from: -deg,
|
|
1858
|
+
to: deg
|
|
1859
|
+
}
|
|
1860
|
+
];
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
if (role === "hair_front") {
|
|
1864
|
+
return [
|
|
1865
|
+
{
|
|
1866
|
+
parameter: StandardParameter.HairSwayX,
|
|
1867
|
+
channel: "rotate",
|
|
1868
|
+
from: -8,
|
|
1869
|
+
to: 8
|
|
1870
|
+
},
|
|
1871
|
+
{
|
|
1872
|
+
parameter: StandardParameter.HairSwayX,
|
|
1873
|
+
channel: "translateX",
|
|
1874
|
+
from: -10,
|
|
1875
|
+
to: 10
|
|
1876
|
+
}
|
|
1877
|
+
];
|
|
1878
|
+
}
|
|
1879
|
+
return [];
|
|
1880
|
+
}
|
|
1881
|
+
var EYELID_FOLD_CREASE = 0.15;
|
|
1882
|
+
var EYELID_FOLD_K = 0.04;
|
|
1883
|
+
var LASH_FOLD_K = 0.2;
|
|
1884
|
+
function bakeEyelidFoldWarp(mesh, parameter, creaseOffsetY, k) {
|
|
1885
|
+
const closed = [];
|
|
1886
|
+
const zeros = [];
|
|
1887
|
+
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
1888
|
+
const vy = mesh.vertices[i + 1];
|
|
1889
|
+
closed.push(0, creaseOffsetY - (1 - k) * vy);
|
|
1890
|
+
zeros.push(0, 0);
|
|
1891
|
+
}
|
|
1892
|
+
return {
|
|
1893
|
+
parameter,
|
|
1894
|
+
keyforms: [
|
|
1895
|
+
{ value: 0, offsets: closed },
|
|
1896
|
+
{ value: 1, offsets: zeros }
|
|
1897
|
+
]
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
function generateIkiFromLayerSet(layers, canvas) {
|
|
1901
|
+
validateLayerInputs(layers, canvas);
|
|
1902
|
+
const hasHair = layers.some((l) => l.role === "hair_front");
|
|
1903
|
+
const parameters = [
|
|
1904
|
+
{
|
|
1905
|
+
id: StandardParameter.MouthOpen,
|
|
1906
|
+
name: "Mouth Open",
|
|
1907
|
+
min: 0,
|
|
1908
|
+
max: 1,
|
|
1909
|
+
default: 0
|
|
1910
|
+
},
|
|
1911
|
+
{
|
|
1912
|
+
id: StandardParameter.MouthForm,
|
|
1913
|
+
name: "Mouth Form",
|
|
1914
|
+
min: -1,
|
|
1915
|
+
max: 1,
|
|
1916
|
+
default: 0
|
|
1917
|
+
},
|
|
1918
|
+
{
|
|
1919
|
+
id: StandardParameter.EyeOpenLeft,
|
|
1920
|
+
name: "Eye L",
|
|
1921
|
+
min: 0,
|
|
1922
|
+
max: 1,
|
|
1923
|
+
default: 1
|
|
1924
|
+
},
|
|
1925
|
+
{
|
|
1926
|
+
id: StandardParameter.EyeOpenRight,
|
|
1927
|
+
name: "Eye R",
|
|
1928
|
+
min: 0,
|
|
1929
|
+
max: 1,
|
|
1930
|
+
default: 1
|
|
1931
|
+
},
|
|
1932
|
+
{
|
|
1933
|
+
id: StandardParameter.EyeballX,
|
|
1934
|
+
name: "Gaze X",
|
|
1935
|
+
min: -1,
|
|
1936
|
+
max: 1,
|
|
1937
|
+
default: 0
|
|
1938
|
+
},
|
|
1939
|
+
{
|
|
1940
|
+
id: StandardParameter.EyeballY,
|
|
1941
|
+
name: "Gaze Y",
|
|
1942
|
+
min: -1,
|
|
1943
|
+
max: 1,
|
|
1944
|
+
default: 0
|
|
1945
|
+
},
|
|
1946
|
+
{
|
|
1947
|
+
id: StandardParameter.AngleX,
|
|
1948
|
+
name: "Head Angle",
|
|
1949
|
+
min: -30,
|
|
1950
|
+
max: 30,
|
|
1951
|
+
default: 0
|
|
1952
|
+
},
|
|
1953
|
+
{
|
|
1954
|
+
id: StandardParameter.Breath,
|
|
1955
|
+
name: "Breath",
|
|
1956
|
+
min: 0,
|
|
1957
|
+
max: 1,
|
|
1958
|
+
default: 0
|
|
1959
|
+
},
|
|
1960
|
+
{
|
|
1961
|
+
id: StandardParameter.BrowLeftY,
|
|
1962
|
+
name: "Brow L Y",
|
|
1963
|
+
min: -1,
|
|
1964
|
+
max: 1,
|
|
1965
|
+
default: 0
|
|
1966
|
+
},
|
|
1967
|
+
{
|
|
1968
|
+
id: StandardParameter.BrowRightY,
|
|
1969
|
+
name: "Brow R Y",
|
|
1970
|
+
min: -1,
|
|
1971
|
+
max: 1,
|
|
1972
|
+
default: 0
|
|
1973
|
+
},
|
|
1974
|
+
{
|
|
1975
|
+
id: StandardParameter.BrowLeftAngle,
|
|
1976
|
+
name: "Brow L Angle",
|
|
1977
|
+
min: -1,
|
|
1978
|
+
max: 1,
|
|
1979
|
+
default: 0
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
id: StandardParameter.BrowRightAngle,
|
|
1983
|
+
name: "Brow R Angle",
|
|
1984
|
+
min: -1,
|
|
1985
|
+
max: 1,
|
|
1986
|
+
default: 0
|
|
1987
|
+
}
|
|
1988
|
+
];
|
|
1989
|
+
if (hasHair) {
|
|
1990
|
+
parameters.push({
|
|
1991
|
+
id: StandardParameter.HairSwayX,
|
|
1992
|
+
name: "Hair Sway X",
|
|
1993
|
+
min: -20,
|
|
1994
|
+
max: 20,
|
|
1995
|
+
default: 0
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
const faceLayers = layers.filter((l) => l.role === "face");
|
|
1999
|
+
const faceLayer = faceLayers[0];
|
|
2000
|
+
const faceTransform = bboxToTransform(
|
|
2001
|
+
faceLayer.bbox,
|
|
2002
|
+
faceLayer.canvasW,
|
|
2003
|
+
faceLayer.canvasH,
|
|
2004
|
+
"face"
|
|
2005
|
+
);
|
|
2006
|
+
const faceCenterX = faceTransform.x;
|
|
2007
|
+
const faceCropH = faceLayer.cropH;
|
|
2008
|
+
const faceWarpLayers = layers.filter(
|
|
2009
|
+
(l) => ROLE_TABLE[l.role].deformer === "faceWarp"
|
|
2010
|
+
);
|
|
2011
|
+
let unionMinX = -canvas.width / 2;
|
|
2012
|
+
let unionMaxX = canvas.width / 2;
|
|
2013
|
+
let unionMinY = -canvas.height / 2;
|
|
2014
|
+
let unionMaxY = canvas.height / 2;
|
|
2015
|
+
if (faceWarpLayers.length > 0) {
|
|
2016
|
+
const transforms = faceWarpLayers.map(
|
|
2017
|
+
(l) => bboxToTransform(l.bbox, l.canvasW, l.canvasH, l.role)
|
|
2018
|
+
);
|
|
2019
|
+
unionMinX = Math.min(
|
|
2020
|
+
...transforms.map((t, i) => t.x - faceWarpLayers[i].cropW / 2)
|
|
2021
|
+
);
|
|
2022
|
+
unionMaxX = Math.max(
|
|
2023
|
+
...transforms.map((t, i) => t.x + faceWarpLayers[i].cropW / 2)
|
|
2024
|
+
);
|
|
2025
|
+
unionMinY = Math.min(
|
|
2026
|
+
...transforms.map((t, i) => t.y - faceWarpLayers[i].cropH / 2)
|
|
2027
|
+
);
|
|
2028
|
+
unionMaxY = Math.max(
|
|
2029
|
+
...transforms.map((t, i) => t.y + faceWarpLayers[i].cropH / 2)
|
|
2030
|
+
);
|
|
2031
|
+
const spanX = unionMaxX - unionMinX;
|
|
2032
|
+
const spanY = unionMaxY - unionMinY;
|
|
2033
|
+
const MARGIN = 0.12;
|
|
2034
|
+
unionMinX -= spanX * MARGIN;
|
|
2035
|
+
unionMaxX += spanX * MARGIN;
|
|
2036
|
+
unionMinY -= spanY * MARGIN;
|
|
2037
|
+
unionMaxY += spanY * MARGIN;
|
|
2038
|
+
}
|
|
2039
|
+
const halfW = Math.max(faceCenterX - unionMinX, unionMaxX - faceCenterX);
|
|
2040
|
+
const faceGridMinX = faceCenterX - halfW;
|
|
2041
|
+
const faceGridMaxX = faceCenterX + halfW;
|
|
2042
|
+
const faceGrid = {
|
|
2043
|
+
cols: 4,
|
|
2044
|
+
rows: 4,
|
|
2045
|
+
points: generateGridPoints(
|
|
2046
|
+
4,
|
|
2047
|
+
4,
|
|
2048
|
+
faceGridMinX,
|
|
2049
|
+
faceGridMaxX,
|
|
2050
|
+
unionMinY,
|
|
2051
|
+
unionMaxY
|
|
2052
|
+
)
|
|
2053
|
+
};
|
|
2054
|
+
const faceBottom = faceTransform.y - faceCropH / 2;
|
|
2055
|
+
const neckPivot = {
|
|
2056
|
+
x: faceCenterX,
|
|
2057
|
+
y: faceBottom - faceCropH * 0.15
|
|
2058
|
+
// 15% below face bottom = neck
|
|
2059
|
+
};
|
|
2060
|
+
const faceWarpBake = bakeHeadTurnGridWarpCentered(
|
|
2061
|
+
faceGrid,
|
|
2062
|
+
StandardParameter.AngleX,
|
|
2063
|
+
faceCenterX
|
|
2064
|
+
);
|
|
2065
|
+
const deformers = [
|
|
2066
|
+
// headDeformer: rigid matrix rotating/translating the whole head about the
|
|
2067
|
+
// neck pivot; bindings mirror sample-model.ts exactly.
|
|
2068
|
+
{
|
|
2069
|
+
id: "headDeformer",
|
|
2070
|
+
pivot: neckPivot,
|
|
2071
|
+
bindings: [
|
|
2072
|
+
{
|
|
2073
|
+
parameter: StandardParameter.AngleX,
|
|
2074
|
+
channel: "rotate",
|
|
2075
|
+
from: 6,
|
|
2076
|
+
to: -6
|
|
2077
|
+
},
|
|
2078
|
+
{
|
|
2079
|
+
parameter: StandardParameter.AngleX,
|
|
2080
|
+
channel: "translateX",
|
|
2081
|
+
from: -50,
|
|
2082
|
+
to: 50
|
|
2083
|
+
},
|
|
2084
|
+
{
|
|
2085
|
+
parameter: StandardParameter.Breath,
|
|
2086
|
+
channel: "translateY",
|
|
2087
|
+
from: 0,
|
|
2088
|
+
to: -12
|
|
2089
|
+
}
|
|
2090
|
+
]
|
|
2091
|
+
},
|
|
2092
|
+
// faceWarp: cylinder-bend warp parented to headDeformer; grid is symmetric
|
|
2093
|
+
// about faceCenterX so the bake's cylinder axis aligns with the face center.
|
|
2094
|
+
{
|
|
2095
|
+
kind: "warp",
|
|
2096
|
+
id: "faceWarp",
|
|
2097
|
+
parent: "headDeformer",
|
|
2098
|
+
grid: faceGrid,
|
|
2099
|
+
warps: [faceWarpBake]
|
|
2100
|
+
}
|
|
2101
|
+
];
|
|
2102
|
+
const eyeCreaseBySide = {};
|
|
2103
|
+
for (const layer of layers) {
|
|
2104
|
+
const side = ROLE_TABLE[layer.role].eyeSide;
|
|
2105
|
+
if ((layer.role === "eye_L" || layer.role === "eye_R") && side) {
|
|
2106
|
+
const ey = bboxToTransform(
|
|
2107
|
+
layer.bbox,
|
|
2108
|
+
layer.canvasW,
|
|
2109
|
+
layer.canvasH,
|
|
2110
|
+
layer.role
|
|
2111
|
+
).y;
|
|
2112
|
+
eyeCreaseBySide[side] = ey - EYELID_FOLD_CREASE * layer.cropH;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
const parts = layers.map((layer) => {
|
|
2116
|
+
const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;
|
|
2117
|
+
const spec = ROLE_TABLE[role];
|
|
2118
|
+
const t = bboxToTransform(bbox, canvasW, canvasH, role);
|
|
2119
|
+
const roleBindings = bindingsForRole(spec, role, cropW, cropH);
|
|
2120
|
+
if (spec.mesh) {
|
|
2121
|
+
const mesh = createPixelGridMesh(4, 4, cropW, cropH);
|
|
2122
|
+
const part = {
|
|
2123
|
+
id: role,
|
|
2124
|
+
color: [1, 1, 1, 1],
|
|
2125
|
+
width: 1,
|
|
2126
|
+
height: 1,
|
|
2127
|
+
order: spec.order,
|
|
2128
|
+
transform: t,
|
|
2129
|
+
deformer: spec.deformer,
|
|
2130
|
+
mesh
|
|
2131
|
+
};
|
|
2132
|
+
if (roleBindings.length > 0) {
|
|
2133
|
+
part.bindings = roleBindings;
|
|
2134
|
+
}
|
|
2135
|
+
if (spec.eyeSide !== void 0) {
|
|
2136
|
+
const isLash = role.startsWith("lash_");
|
|
2137
|
+
if (role.startsWith("eye_") || isLash) {
|
|
2138
|
+
const openParam = spec.eyeSide === "L" ? StandardParameter.EyeOpenLeft : StandardParameter.EyeOpenRight;
|
|
2139
|
+
const creaseWorldY = eyeCreaseBySide[spec.eyeSide] ?? t.y - EYELID_FOLD_CREASE * cropH;
|
|
2140
|
+
part.warps = [
|
|
2141
|
+
bakeEyelidFoldWarp(
|
|
2142
|
+
mesh,
|
|
2143
|
+
openParam,
|
|
2144
|
+
creaseWorldY - t.y,
|
|
2145
|
+
isLash ? LASH_FOLD_K : EYELID_FOLD_K
|
|
2146
|
+
)
|
|
2147
|
+
];
|
|
2148
|
+
} else {
|
|
2149
|
+
part.clip = { masks: [`eye_${spec.eyeSide}`] };
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
return part;
|
|
2153
|
+
} else {
|
|
2154
|
+
return {
|
|
2155
|
+
id: role,
|
|
2156
|
+
color: [1, 1, 1, 1],
|
|
2157
|
+
width: cropW,
|
|
2158
|
+
height: cropH,
|
|
2159
|
+
order: spec.order,
|
|
2160
|
+
transform: t,
|
|
2161
|
+
deformer: spec.deformer
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
const model = {
|
|
2166
|
+
version: IKI_FORMAT_VERSION2,
|
|
2167
|
+
name: "Auto-Rigged Model",
|
|
2168
|
+
canvas: { width: canvas.width, height: canvas.height },
|
|
2169
|
+
textures: [],
|
|
2170
|
+
parameters,
|
|
2171
|
+
deformers,
|
|
2172
|
+
parts,
|
|
2173
|
+
// Secondary motion: a spring lags AngleX onto HairSwayX so front hair sways
|
|
2174
|
+
// behind the head turn (same constants as the hand-authored sample). Omitted
|
|
2175
|
+
// when there is no front hair to drive.
|
|
2176
|
+
physics: hasHair ? [
|
|
2177
|
+
{
|
|
2178
|
+
id: "hairSway",
|
|
2179
|
+
input: { parameter: StandardParameter.AngleX, weight: 1 },
|
|
2180
|
+
output: { parameter: StandardParameter.HairSwayX, scale: -10 },
|
|
2181
|
+
mass: 1,
|
|
2182
|
+
stiffness: 80,
|
|
2183
|
+
damping: 10
|
|
2184
|
+
}
|
|
2185
|
+
] : void 0
|
|
2186
|
+
};
|
|
2187
|
+
return parseIkiModel3(structuredClone(model));
|
|
2188
|
+
}
|
|
2189
|
+
export {
|
|
2190
|
+
ALPHA_BBOX_THRESHOLD,
|
|
2191
|
+
ATLAS_PADDING,
|
|
2192
|
+
AddDeformer,
|
|
2193
|
+
AddPart,
|
|
2194
|
+
AddPhysicsRig,
|
|
2195
|
+
CaptureGridKeyform,
|
|
2196
|
+
DeleteDeformer,
|
|
2197
|
+
DeletePart,
|
|
2198
|
+
DeletePhysicsRig,
|
|
2199
|
+
EditorDocument,
|
|
2200
|
+
SetDeformerBindings,
|
|
2201
|
+
SetDeformerParent,
|
|
2202
|
+
SetDeformerPivot,
|
|
2203
|
+
SetDeformerPivotX,
|
|
2204
|
+
SetDeformerPivotY,
|
|
2205
|
+
SetDeformerTransform,
|
|
2206
|
+
SetPartBindings,
|
|
2207
|
+
SetPartColor,
|
|
2208
|
+
SetPartDeformer,
|
|
2209
|
+
SetPartHeight,
|
|
2210
|
+
SetPartMesh,
|
|
2211
|
+
SetPartOrder,
|
|
2212
|
+
SetPartTransform,
|
|
2213
|
+
SetPartWidth,
|
|
2214
|
+
SetPhysicsRig,
|
|
2215
|
+
UV_INSET_PX,
|
|
2216
|
+
captureBindingEndpoint,
|
|
2217
|
+
computeGridOffsets,
|
|
2218
|
+
createDefaultMatrixDeformer,
|
|
2219
|
+
createDefaultPart,
|
|
2220
|
+
createDefaultWarpDeformer,
|
|
2221
|
+
createGridMesh,
|
|
2222
|
+
detectAlphaBbox,
|
|
2223
|
+
generateIkiFromLayerSet,
|
|
2224
|
+
interpolateGridOffsets,
|
|
2225
|
+
packAtlas,
|
|
2226
|
+
parseLayerRoles,
|
|
2227
|
+
upsertGridKeyform,
|
|
2228
|
+
uvRectFor,
|
|
2229
|
+
validateDeformerDelete,
|
|
2230
|
+
validateDeformerReparent,
|
|
2231
|
+
validatePartAttach
|
|
2232
|
+
};
|
|
2233
|
+
//# sourceMappingURL=index.mjs.map
|