@ikijs/editor 0.31.0 → 0.32.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/README.md +2 -1
- package/dist/index.d.mts +23 -8
- package/dist/index.d.ts +23 -8
- package/dist/index.js +149 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +149 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -102,7 +102,8 @@ hair sway), validated against `RIG_PLAN_FIELDS`. `options.patches` adds displace
|
|
|
102
102
|
stops (`RigPatch`), replayed on every rig and reported through
|
|
103
103
|
`options.onPatchesApplied`; a refused op throws `PatchError`. `options.customParts` adds parts the
|
|
104
104
|
auto-rigger has no role for (`CustomPart`: `id`, `layer`, `anchor`, `tie`,
|
|
105
|
-
`after`
|
|
105
|
+
`after`, and `occlusion`: roles whose parts hide it by an amount keyed on a
|
|
106
|
+
parameter), unrigged on `headDeformer` with a plain mesh and drawn after the
|
|
106
107
|
`after` role's last part; the rest of the model is as without them but for
|
|
107
108
|
the `order` of every part after an inserted one (a set rigged without
|
|
108
109
|
`customParts` is byte-identical), and a malformed entry throws
|
package/dist/index.d.mts
CHANGED
|
@@ -251,10 +251,11 @@ declare class AddDeformer implements EditCommand {
|
|
|
251
251
|
* visually predictable.
|
|
252
252
|
*
|
|
253
253
|
* No `parseIkiModel` pre-check is needed: removing an element from an already-
|
|
254
|
-
* valid model cannot introduce a structural violation — EXCEPT for
|
|
255
|
-
* references
|
|
256
|
-
* delete a part still used as another part's `clip.masks`
|
|
257
|
-
* so it can never leave a dangling
|
|
254
|
+
* valid model cannot introduce a structural violation — EXCEPT for the part→part
|
|
255
|
+
* references in the contract: `clip.masks` and `occlusion.by`. `apply` refuses to
|
|
256
|
+
* delete a part still used as another part's `clip.masks` or `occlusion.by`
|
|
257
|
+
* entry (guards below), so it can never leave a dangling ref that would fail
|
|
258
|
+
* `toIkiModel()`.
|
|
258
259
|
*
|
|
259
260
|
* Texture-reference safety (package invariant): `apply` refuses to delete a
|
|
260
261
|
* part that still carries `part.texture`. Texture/atlas state is non-undoable
|
|
@@ -1081,6 +1082,19 @@ interface CustomPart {
|
|
|
1081
1082
|
/** The art does not bend (a hairpin, a ribbon): a bake that bends it more
|
|
1082
1083
|
* than 0.0025 face widths at a stop is refused (`BakeError`). */
|
|
1083
1084
|
rigid?: boolean;
|
|
1085
|
+
/** Hide the part where the parts of these roles cover it, by an amount keyed on a
|
|
1086
|
+
* parameter (`IkiPart.occlusion`). `by` lists roles of the set: the
|
|
1087
|
+
* generator writes the part ids those roles yield. The parameter is one
|
|
1088
|
+
* the model declares and not a physics output; the stops follow the
|
|
1089
|
+
* format's rules. */
|
|
1090
|
+
occlusion?: {
|
|
1091
|
+
by: string[];
|
|
1092
|
+
parameter: string;
|
|
1093
|
+
stops: {
|
|
1094
|
+
value: number;
|
|
1095
|
+
amount: number;
|
|
1096
|
+
}[];
|
|
1097
|
+
};
|
|
1084
1098
|
}
|
|
1085
1099
|
/** A `customParts` entry that is malformed or misses the layer set. Its
|
|
1086
1100
|
* message starts `customParts[i].<field>` (`customParts` alone for a
|
|
@@ -1917,10 +1931,11 @@ declare function validateDeformerReparent(deformers: IkiDeformer[], deformerId:
|
|
|
1917
1931
|
* `toIkiModel()` refuses.
|
|
1918
1932
|
*
|
|
1919
1933
|
* Note: there is no validatePartDelete here, but NOT because parts are
|
|
1920
|
-
* unreferenced — `clip.masks`
|
|
1921
|
-
* closer to the edits that could break it: `DeletePart`
|
|
1922
|
-
* part still used as a mask
|
|
1923
|
-
* one (
|
|
1934
|
+
* unreferenced — `clip.masks` and `occlusion.by` name parts by id. That
|
|
1935
|
+
* invariant is enforced closer to the edits that could break it: `DeletePart`
|
|
1936
|
+
* refuses to remove a part still used as a mask or an occluder, and
|
|
1937
|
+
* `SetPartMesh` refuses to strip the mesh off one (both must be mesh parts).
|
|
1938
|
+
* No command creates or edits `clip` or `occlusion`.
|
|
1924
1939
|
*/
|
|
1925
1940
|
declare function validateDeformerDelete(deformers: IkiDeformer[], parts: IkiPart[], physicsChains: IkiPhysicsChain[], deformerId: string): void;
|
|
1926
1941
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -251,10 +251,11 @@ declare class AddDeformer implements EditCommand {
|
|
|
251
251
|
* visually predictable.
|
|
252
252
|
*
|
|
253
253
|
* No `parseIkiModel` pre-check is needed: removing an element from an already-
|
|
254
|
-
* valid model cannot introduce a structural violation — EXCEPT for
|
|
255
|
-
* references
|
|
256
|
-
* delete a part still used as another part's `clip.masks`
|
|
257
|
-
* so it can never leave a dangling
|
|
254
|
+
* valid model cannot introduce a structural violation — EXCEPT for the part→part
|
|
255
|
+
* references in the contract: `clip.masks` and `occlusion.by`. `apply` refuses to
|
|
256
|
+
* delete a part still used as another part's `clip.masks` or `occlusion.by`
|
|
257
|
+
* entry (guards below), so it can never leave a dangling ref that would fail
|
|
258
|
+
* `toIkiModel()`.
|
|
258
259
|
*
|
|
259
260
|
* Texture-reference safety (package invariant): `apply` refuses to delete a
|
|
260
261
|
* part that still carries `part.texture`. Texture/atlas state is non-undoable
|
|
@@ -1081,6 +1082,19 @@ interface CustomPart {
|
|
|
1081
1082
|
/** The art does not bend (a hairpin, a ribbon): a bake that bends it more
|
|
1082
1083
|
* than 0.0025 face widths at a stop is refused (`BakeError`). */
|
|
1083
1084
|
rigid?: boolean;
|
|
1085
|
+
/** Hide the part where the parts of these roles cover it, by an amount keyed on a
|
|
1086
|
+
* parameter (`IkiPart.occlusion`). `by` lists roles of the set: the
|
|
1087
|
+
* generator writes the part ids those roles yield. The parameter is one
|
|
1088
|
+
* the model declares and not a physics output; the stops follow the
|
|
1089
|
+
* format's rules. */
|
|
1090
|
+
occlusion?: {
|
|
1091
|
+
by: string[];
|
|
1092
|
+
parameter: string;
|
|
1093
|
+
stops: {
|
|
1094
|
+
value: number;
|
|
1095
|
+
amount: number;
|
|
1096
|
+
}[];
|
|
1097
|
+
};
|
|
1084
1098
|
}
|
|
1085
1099
|
/** A `customParts` entry that is malformed or misses the layer set. Its
|
|
1086
1100
|
* message starts `customParts[i].<field>` (`customParts` alone for a
|
|
@@ -1917,10 +1931,11 @@ declare function validateDeformerReparent(deformers: IkiDeformer[], deformerId:
|
|
|
1917
1931
|
* `toIkiModel()` refuses.
|
|
1918
1932
|
*
|
|
1919
1933
|
* Note: there is no validatePartDelete here, but NOT because parts are
|
|
1920
|
-
* unreferenced — `clip.masks`
|
|
1921
|
-
* closer to the edits that could break it: `DeletePart`
|
|
1922
|
-
* part still used as a mask
|
|
1923
|
-
* one (
|
|
1934
|
+
* unreferenced — `clip.masks` and `occlusion.by` name parts by id. That
|
|
1935
|
+
* invariant is enforced closer to the edits that could break it: `DeletePart`
|
|
1936
|
+
* refuses to remove a part still used as a mask or an occluder, and
|
|
1937
|
+
* `SetPartMesh` refuses to strip the mesh off one (both must be mesh parts).
|
|
1938
|
+
* No command creates or edits `clip` or `occlusion`.
|
|
1924
1939
|
*/
|
|
1925
1940
|
declare function validateDeformerDelete(deformers: IkiDeformer[], parts: IkiPart[], physicsChains: IkiPhysicsChain[], deformerId: string): void;
|
|
1926
1941
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1115,6 +1115,14 @@ var DeletePart = class {
|
|
|
1115
1115
|
`parts."${this.partId}": cannot delete \u2014 used as a clip mask by part "${masker.id}"; remove its clip first`
|
|
1116
1116
|
);
|
|
1117
1117
|
}
|
|
1118
|
+
const occluded = parts.find(
|
|
1119
|
+
(p) => p.id !== this.partId && p.occlusion?.by.includes(this.partId)
|
|
1120
|
+
);
|
|
1121
|
+
if (occluded) {
|
|
1122
|
+
throw new Error(
|
|
1123
|
+
`parts."${this.partId}": cannot delete \u2014 used as an occluder by part "${occluded.id}"; remove its occlusion first`
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1118
1126
|
const i = parts.indexOf(part);
|
|
1119
1127
|
if (!this.captured) {
|
|
1120
1128
|
this.removed = structuredClone(part);
|
|
@@ -1319,6 +1327,14 @@ var SetPartMesh = class {
|
|
|
1319
1327
|
`parts."${this.partId}": cannot remove mesh \u2014 used as a clip mask by part "${masker.id}" (masks require a mesh); remove its clip first`
|
|
1320
1328
|
);
|
|
1321
1329
|
}
|
|
1330
|
+
const occluded = doc.getModel().parts.find(
|
|
1331
|
+
(p) => p.id !== this.partId && p.occlusion?.by.includes(this.partId)
|
|
1332
|
+
);
|
|
1333
|
+
if (occluded) {
|
|
1334
|
+
throw new import_format2.IkiFormatError(
|
|
1335
|
+
`parts."${this.partId}": cannot remove mesh \u2014 used as an occluder by part "${occluded.id}" (occluders require a mesh); remove its occlusion first`
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1322
1338
|
} else {
|
|
1323
1339
|
if ((part.warps?.length ?? 0) > 0) {
|
|
1324
1340
|
throw new import_format2.IkiFormatError(
|
|
@@ -1398,6 +1414,14 @@ function validatePhysicsCandidate(doc, candidatePhysics) {
|
|
|
1398
1414
|
}
|
|
1399
1415
|
throw e;
|
|
1400
1416
|
}
|
|
1417
|
+
for (const rig of candidatePhysics) {
|
|
1418
|
+
const part = doc.getModel().parts.find((p) => p.occlusion?.parameter === rig.output.parameter);
|
|
1419
|
+
if (part !== void 0) {
|
|
1420
|
+
throw new import_format2.IkiFormatError(
|
|
1421
|
+
`physics."${rig.id}".output.parameter "${rig.output.parameter}" keys parts."${part.id}".occlusion; an occlusion amount cannot be keyed on a physics output`
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1401
1425
|
}
|
|
1402
1426
|
var AddPhysicsRig = class {
|
|
1403
1427
|
label = "Add physics rig";
|
|
@@ -7132,7 +7156,18 @@ function span2(name, v) {
|
|
|
7132
7156
|
}
|
|
7133
7157
|
|
|
7134
7158
|
// src/auto-rig/custom-parts.ts
|
|
7135
|
-
var KEYS = [
|
|
7159
|
+
var KEYS = [
|
|
7160
|
+
"id",
|
|
7161
|
+
"layer",
|
|
7162
|
+
"anchor",
|
|
7163
|
+
"tie",
|
|
7164
|
+
"after",
|
|
7165
|
+
"deformer",
|
|
7166
|
+
"rigid",
|
|
7167
|
+
"occlusion"
|
|
7168
|
+
];
|
|
7169
|
+
var OCCLUSION_KEYS = ["by", "parameter", "stops"];
|
|
7170
|
+
var STOP_KEYS = ["value", "amount"];
|
|
7136
7171
|
var LAYER_KEYS = ["fileName", "bbox"];
|
|
7137
7172
|
var finite4 = (v) => typeof v === "number" && Number.isFinite(v);
|
|
7138
7173
|
var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -7140,7 +7175,7 @@ var inside = (b, [x, y]) => x >= b.x && x < b.x + b.w && y >= b.y && y < b.y + b
|
|
|
7140
7175
|
var RESERVED = new Set(
|
|
7141
7176
|
ROLE_TABLE.flatMap((r) => [r.role, ...partIdsOfRole(r.role)])
|
|
7142
7177
|
);
|
|
7143
|
-
function checkCustomParts(customParts, byRole, canvas) {
|
|
7178
|
+
function checkCustomParts(customParts, byRole, canvas, parameters, physicsOutputs) {
|
|
7144
7179
|
if (!Array.isArray(customParts)) {
|
|
7145
7180
|
throw new CustomPartError(
|
|
7146
7181
|
`customParts must be an array, got ${customParts === null ? "null" : typeof customParts}`
|
|
@@ -7163,7 +7198,7 @@ function checkCustomParts(customParts, byRole, canvas) {
|
|
|
7163
7198
|
throw new CustomPartError(`${at} must be a plain object`);
|
|
7164
7199
|
}
|
|
7165
7200
|
checkedKeys(at, entry, KEYS, CustomPartError);
|
|
7166
|
-
const { id, layer, anchor, tie, after, deformer, rigid } = entry;
|
|
7201
|
+
const { id, layer, anchor, tie, after, deformer, rigid, occlusion } = entry;
|
|
7167
7202
|
if (typeof id !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(id)) {
|
|
7168
7203
|
throw new CustomPartError(
|
|
7169
7204
|
`${at}.id must be a plain identifier (letters, digits, underscore; not starting with a digit), got ${JSON.stringify(id)}`
|
|
@@ -7214,9 +7249,84 @@ function checkCustomParts(customParts, byRole, canvas) {
|
|
|
7214
7249
|
`${at}.rigid must be true or false, got ${JSON.stringify(rigid)}`
|
|
7215
7250
|
);
|
|
7216
7251
|
}
|
|
7252
|
+
if (occlusion !== void 0) {
|
|
7253
|
+
checkOcclusion(
|
|
7254
|
+
`${at}.occlusion`,
|
|
7255
|
+
occlusion,
|
|
7256
|
+
present,
|
|
7257
|
+
parameters,
|
|
7258
|
+
physicsOutputs
|
|
7259
|
+
);
|
|
7260
|
+
}
|
|
7217
7261
|
});
|
|
7218
7262
|
return customParts;
|
|
7219
7263
|
}
|
|
7264
|
+
function checkOcclusion(at, occlusion, present, parameters, physicsOutputs) {
|
|
7265
|
+
if (!isObject3(occlusion)) {
|
|
7266
|
+
throw new CustomPartError(`${at} must be a plain object`);
|
|
7267
|
+
}
|
|
7268
|
+
checkedKeys(at, occlusion, OCCLUSION_KEYS, CustomPartError);
|
|
7269
|
+
const { by, parameter, stops } = occlusion;
|
|
7270
|
+
if (!Array.isArray(by) || by.length === 0) {
|
|
7271
|
+
throw new CustomPartError(
|
|
7272
|
+
`${at}.by must be a non-empty array of roles of the set, one of ${present.join(", ")}`
|
|
7273
|
+
);
|
|
7274
|
+
}
|
|
7275
|
+
by.forEach((role, j) => {
|
|
7276
|
+
if (typeof role !== "string" || !present.includes(role)) {
|
|
7277
|
+
throw new CustomPartError(
|
|
7278
|
+
`${at}.by[${j}] must be a role of the set, one of ${present.join(", ")}; got ${JSON.stringify(role)}`
|
|
7279
|
+
);
|
|
7280
|
+
}
|
|
7281
|
+
if (by.indexOf(role) !== j) {
|
|
7282
|
+
throw new CustomPartError(
|
|
7283
|
+
`${at}.by[${j}] "${role}" repeats an earlier role`
|
|
7284
|
+
);
|
|
7285
|
+
}
|
|
7286
|
+
});
|
|
7287
|
+
const descriptor = parameters.find((p) => p.id === parameter);
|
|
7288
|
+
if (typeof parameter !== "string" || descriptor === void 0) {
|
|
7289
|
+
throw new CustomPartError(
|
|
7290
|
+
`${at}.parameter must be a parameter the rig declares, got ${JSON.stringify(parameter)}`
|
|
7291
|
+
);
|
|
7292
|
+
}
|
|
7293
|
+
if (physicsOutputs.has(parameter)) {
|
|
7294
|
+
throw new CustomPartError(
|
|
7295
|
+
`${at}.parameter "${parameter}" is a physics output; an occlusion amount cannot be keyed on one`
|
|
7296
|
+
);
|
|
7297
|
+
}
|
|
7298
|
+
if (!Array.isArray(stops) || stops.length === 0) {
|
|
7299
|
+
throw new CustomPartError(`${at}.stops must be a non-empty array`);
|
|
7300
|
+
}
|
|
7301
|
+
const { min, max } = descriptor;
|
|
7302
|
+
let previous = -Infinity;
|
|
7303
|
+
stops.forEach((stop, k) => {
|
|
7304
|
+
const s = `${at}.stops[${k}]`;
|
|
7305
|
+
if (!isObject3(stop))
|
|
7306
|
+
throw new CustomPartError(`${s} must be a plain object`);
|
|
7307
|
+
checkedKeys(s, stop, STOP_KEYS, CustomPartError);
|
|
7308
|
+
const { value, amount } = stop;
|
|
7309
|
+
if (!finite4(value)) {
|
|
7310
|
+
throw new CustomPartError(
|
|
7311
|
+
`${s}.value must be a finite number, got ${JSON.stringify(value)}`
|
|
7312
|
+
);
|
|
7313
|
+
}
|
|
7314
|
+
if (value < min || value > max) {
|
|
7315
|
+
throw new CustomPartError(
|
|
7316
|
+
`${s}.value ${value} is outside parameter "${parameter}" range [${min},${max}]`
|
|
7317
|
+
);
|
|
7318
|
+
}
|
|
7319
|
+
if (value <= previous) {
|
|
7320
|
+
throw new CustomPartError(`${s}.value must be strictly ascending`);
|
|
7321
|
+
}
|
|
7322
|
+
previous = value;
|
|
7323
|
+
if (!finite4(amount) || amount < 0 || amount > 1) {
|
|
7324
|
+
throw new CustomPartError(
|
|
7325
|
+
`${s}.amount must be a number in 0..1, got ${JSON.stringify(amount)}`
|
|
7326
|
+
);
|
|
7327
|
+
}
|
|
7328
|
+
});
|
|
7329
|
+
}
|
|
7220
7330
|
var CELL_PX2 = 20;
|
|
7221
7331
|
function buildCustomParts(customParts, canvas) {
|
|
7222
7332
|
return customParts.map(({ id, layer }) => {
|
|
@@ -7236,6 +7346,32 @@ function buildCustomParts(customParts, canvas) {
|
|
|
7236
7346
|
};
|
|
7237
7347
|
});
|
|
7238
7348
|
}
|
|
7349
|
+
function writeOcclusion(customParts, built, parts) {
|
|
7350
|
+
const byId = new Map(parts.map((p) => [p.id, p]));
|
|
7351
|
+
customParts.forEach((entry, i) => {
|
|
7352
|
+
if (entry.occlusion === void 0) return;
|
|
7353
|
+
const { by, parameter, stops } = entry.occlusion;
|
|
7354
|
+
const ids = [];
|
|
7355
|
+
by.forEach((role, j) => {
|
|
7356
|
+
for (const id of partIdsOfRole(role)) {
|
|
7357
|
+
const part = byId.get(id);
|
|
7358
|
+
if (part === void 0) continue;
|
|
7359
|
+
const fault = part.clip !== void 0 ? { is: "is clipped", kind: "a clipped part" } : part.mesh === void 0 ? { is: "has no mesh", kind: "a part without a mesh" } : void 0;
|
|
7360
|
+
if (fault !== void 0) {
|
|
7361
|
+
throw new CustomPartError(
|
|
7362
|
+
`customParts[${i}].occlusion.by[${j}] "${role}" yields part "${id}", which ${fault.is}; ${fault.kind} cannot occlude`
|
|
7363
|
+
);
|
|
7364
|
+
}
|
|
7365
|
+
ids.push(id);
|
|
7366
|
+
}
|
|
7367
|
+
});
|
|
7368
|
+
built[i].occlusion = {
|
|
7369
|
+
by: ids,
|
|
7370
|
+
parameter,
|
|
7371
|
+
stops: stops.map((s) => ({ value: s.value, amount: s.amount }))
|
|
7372
|
+
};
|
|
7373
|
+
});
|
|
7374
|
+
}
|
|
7239
7375
|
|
|
7240
7376
|
// src/auto-rig/generate.ts
|
|
7241
7377
|
var MESH_CELLS = {
|
|
@@ -7278,8 +7414,15 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
7278
7414
|
const hasNose = has("nose");
|
|
7279
7415
|
const plateHalf = byRole.get("face").cropW / 2;
|
|
7280
7416
|
checkHeadEdges(options.headEdges, byRole);
|
|
7281
|
-
const
|
|
7417
|
+
const physics = has("hair_front") ? hairPhysics(has("hair_back")) : void 0;
|
|
7282
7418
|
const parameters = declareParameters(new Set(byRole.keys()));
|
|
7419
|
+
const entries = options.customParts !== void 0 && !(Array.isArray(options.customParts) && options.customParts.length === 0) ? checkCustomParts(
|
|
7420
|
+
options.customParts,
|
|
7421
|
+
byRole,
|
|
7422
|
+
canvas,
|
|
7423
|
+
parameters,
|
|
7424
|
+
new Set((physics ?? []).map((r) => r.output.parameter))
|
|
7425
|
+
) : [];
|
|
7283
7426
|
const bakes = options.bakes === void 0 ? void 0 : parseBakes(options.bakes, entries, parameters);
|
|
7284
7427
|
checkStrandEdges(
|
|
7285
7428
|
options.strandEdges,
|
|
@@ -7460,6 +7603,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
7460
7603
|
parts.forEach((p, i) => {
|
|
7461
7604
|
p.order = i;
|
|
7462
7605
|
});
|
|
7606
|
+
writeOcclusion(entries, built, parts);
|
|
7463
7607
|
if (bakes !== void 0) {
|
|
7464
7608
|
bakeReports = applyBakes(bakes, built, entries, {
|
|
7465
7609
|
canvas,
|
|
@@ -7476,7 +7620,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
7476
7620
|
parameters,
|
|
7477
7621
|
parts,
|
|
7478
7622
|
deformers: deformerList,
|
|
7479
|
-
...
|
|
7623
|
+
...physics !== void 0 ? { physics } : {},
|
|
7480
7624
|
expressions: defaultExpressions(new Set(parameters.map((p) => p.id))),
|
|
7481
7625
|
motions: defaultMotions(new Set(parameters.map((p) => p.id)))
|
|
7482
7626
|
};
|