@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/dist/index.mjs CHANGED
@@ -1023,6 +1023,14 @@ var DeletePart = class {
1023
1023
  `parts."${this.partId}": cannot delete \u2014 used as a clip mask by part "${masker.id}"; remove its clip first`
1024
1024
  );
1025
1025
  }
1026
+ const occluded = parts.find(
1027
+ (p) => p.id !== this.partId && p.occlusion?.by.includes(this.partId)
1028
+ );
1029
+ if (occluded) {
1030
+ throw new Error(
1031
+ `parts."${this.partId}": cannot delete \u2014 used as an occluder by part "${occluded.id}"; remove its occlusion first`
1032
+ );
1033
+ }
1026
1034
  const i = parts.indexOf(part);
1027
1035
  if (!this.captured) {
1028
1036
  this.removed = structuredClone(part);
@@ -1227,6 +1235,14 @@ var SetPartMesh = class {
1227
1235
  `parts."${this.partId}": cannot remove mesh \u2014 used as a clip mask by part "${masker.id}" (masks require a mesh); remove its clip first`
1228
1236
  );
1229
1237
  }
1238
+ const occluded = doc.getModel().parts.find(
1239
+ (p) => p.id !== this.partId && p.occlusion?.by.includes(this.partId)
1240
+ );
1241
+ if (occluded) {
1242
+ throw new IkiFormatError(
1243
+ `parts."${this.partId}": cannot remove mesh \u2014 used as an occluder by part "${occluded.id}" (occluders require a mesh); remove its occlusion first`
1244
+ );
1245
+ }
1230
1246
  } else {
1231
1247
  if ((part.warps?.length ?? 0) > 0) {
1232
1248
  throw new IkiFormatError(
@@ -1306,6 +1322,14 @@ function validatePhysicsCandidate(doc, candidatePhysics) {
1306
1322
  }
1307
1323
  throw e;
1308
1324
  }
1325
+ for (const rig of candidatePhysics) {
1326
+ const part = doc.getModel().parts.find((p) => p.occlusion?.parameter === rig.output.parameter);
1327
+ if (part !== void 0) {
1328
+ throw new IkiFormatError(
1329
+ `physics."${rig.id}".output.parameter "${rig.output.parameter}" keys parts."${part.id}".occlusion; an occlusion amount cannot be keyed on a physics output`
1330
+ );
1331
+ }
1332
+ }
1309
1333
  }
1310
1334
  var AddPhysicsRig = class {
1311
1335
  label = "Add physics rig";
@@ -7058,7 +7082,18 @@ function span2(name, v) {
7058
7082
  }
7059
7083
 
7060
7084
  // src/auto-rig/custom-parts.ts
7061
- var KEYS = ["id", "layer", "anchor", "tie", "after", "deformer", "rigid"];
7085
+ var KEYS = [
7086
+ "id",
7087
+ "layer",
7088
+ "anchor",
7089
+ "tie",
7090
+ "after",
7091
+ "deformer",
7092
+ "rigid",
7093
+ "occlusion"
7094
+ ];
7095
+ var OCCLUSION_KEYS = ["by", "parameter", "stops"];
7096
+ var STOP_KEYS = ["value", "amount"];
7062
7097
  var LAYER_KEYS = ["fileName", "bbox"];
7063
7098
  var finite4 = (v) => typeof v === "number" && Number.isFinite(v);
7064
7099
  var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -7066,7 +7101,7 @@ var inside = (b, [x, y]) => x >= b.x && x < b.x + b.w && y >= b.y && y < b.y + b
7066
7101
  var RESERVED = new Set(
7067
7102
  ROLE_TABLE.flatMap((r) => [r.role, ...partIdsOfRole(r.role)])
7068
7103
  );
7069
- function checkCustomParts(customParts, byRole, canvas) {
7104
+ function checkCustomParts(customParts, byRole, canvas, parameters, physicsOutputs) {
7070
7105
  if (!Array.isArray(customParts)) {
7071
7106
  throw new CustomPartError(
7072
7107
  `customParts must be an array, got ${customParts === null ? "null" : typeof customParts}`
@@ -7089,7 +7124,7 @@ function checkCustomParts(customParts, byRole, canvas) {
7089
7124
  throw new CustomPartError(`${at} must be a plain object`);
7090
7125
  }
7091
7126
  checkedKeys(at, entry, KEYS, CustomPartError);
7092
- const { id, layer, anchor, tie, after, deformer, rigid } = entry;
7127
+ const { id, layer, anchor, tie, after, deformer, rigid, occlusion } = entry;
7093
7128
  if (typeof id !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(id)) {
7094
7129
  throw new CustomPartError(
7095
7130
  `${at}.id must be a plain identifier (letters, digits, underscore; not starting with a digit), got ${JSON.stringify(id)}`
@@ -7140,9 +7175,84 @@ function checkCustomParts(customParts, byRole, canvas) {
7140
7175
  `${at}.rigid must be true or false, got ${JSON.stringify(rigid)}`
7141
7176
  );
7142
7177
  }
7178
+ if (occlusion !== void 0) {
7179
+ checkOcclusion(
7180
+ `${at}.occlusion`,
7181
+ occlusion,
7182
+ present,
7183
+ parameters,
7184
+ physicsOutputs
7185
+ );
7186
+ }
7143
7187
  });
7144
7188
  return customParts;
7145
7189
  }
7190
+ function checkOcclusion(at, occlusion, present, parameters, physicsOutputs) {
7191
+ if (!isObject3(occlusion)) {
7192
+ throw new CustomPartError(`${at} must be a plain object`);
7193
+ }
7194
+ checkedKeys(at, occlusion, OCCLUSION_KEYS, CustomPartError);
7195
+ const { by, parameter, stops } = occlusion;
7196
+ if (!Array.isArray(by) || by.length === 0) {
7197
+ throw new CustomPartError(
7198
+ `${at}.by must be a non-empty array of roles of the set, one of ${present.join(", ")}`
7199
+ );
7200
+ }
7201
+ by.forEach((role, j) => {
7202
+ if (typeof role !== "string" || !present.includes(role)) {
7203
+ throw new CustomPartError(
7204
+ `${at}.by[${j}] must be a role of the set, one of ${present.join(", ")}; got ${JSON.stringify(role)}`
7205
+ );
7206
+ }
7207
+ if (by.indexOf(role) !== j) {
7208
+ throw new CustomPartError(
7209
+ `${at}.by[${j}] "${role}" repeats an earlier role`
7210
+ );
7211
+ }
7212
+ });
7213
+ const descriptor = parameters.find((p) => p.id === parameter);
7214
+ if (typeof parameter !== "string" || descriptor === void 0) {
7215
+ throw new CustomPartError(
7216
+ `${at}.parameter must be a parameter the rig declares, got ${JSON.stringify(parameter)}`
7217
+ );
7218
+ }
7219
+ if (physicsOutputs.has(parameter)) {
7220
+ throw new CustomPartError(
7221
+ `${at}.parameter "${parameter}" is a physics output; an occlusion amount cannot be keyed on one`
7222
+ );
7223
+ }
7224
+ if (!Array.isArray(stops) || stops.length === 0) {
7225
+ throw new CustomPartError(`${at}.stops must be a non-empty array`);
7226
+ }
7227
+ const { min, max } = descriptor;
7228
+ let previous = -Infinity;
7229
+ stops.forEach((stop, k) => {
7230
+ const s = `${at}.stops[${k}]`;
7231
+ if (!isObject3(stop))
7232
+ throw new CustomPartError(`${s} must be a plain object`);
7233
+ checkedKeys(s, stop, STOP_KEYS, CustomPartError);
7234
+ const { value, amount } = stop;
7235
+ if (!finite4(value)) {
7236
+ throw new CustomPartError(
7237
+ `${s}.value must be a finite number, got ${JSON.stringify(value)}`
7238
+ );
7239
+ }
7240
+ if (value < min || value > max) {
7241
+ throw new CustomPartError(
7242
+ `${s}.value ${value} is outside parameter "${parameter}" range [${min},${max}]`
7243
+ );
7244
+ }
7245
+ if (value <= previous) {
7246
+ throw new CustomPartError(`${s}.value must be strictly ascending`);
7247
+ }
7248
+ previous = value;
7249
+ if (!finite4(amount) || amount < 0 || amount > 1) {
7250
+ throw new CustomPartError(
7251
+ `${s}.amount must be a number in 0..1, got ${JSON.stringify(amount)}`
7252
+ );
7253
+ }
7254
+ });
7255
+ }
7146
7256
  var CELL_PX2 = 20;
7147
7257
  function buildCustomParts(customParts, canvas) {
7148
7258
  return customParts.map(({ id, layer }) => {
@@ -7162,6 +7272,32 @@ function buildCustomParts(customParts, canvas) {
7162
7272
  };
7163
7273
  });
7164
7274
  }
7275
+ function writeOcclusion(customParts, built, parts) {
7276
+ const byId = new Map(parts.map((p) => [p.id, p]));
7277
+ customParts.forEach((entry, i) => {
7278
+ if (entry.occlusion === void 0) return;
7279
+ const { by, parameter, stops } = entry.occlusion;
7280
+ const ids = [];
7281
+ by.forEach((role, j) => {
7282
+ for (const id of partIdsOfRole(role)) {
7283
+ const part = byId.get(id);
7284
+ if (part === void 0) continue;
7285
+ 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;
7286
+ if (fault !== void 0) {
7287
+ throw new CustomPartError(
7288
+ `customParts[${i}].occlusion.by[${j}] "${role}" yields part "${id}", which ${fault.is}; ${fault.kind} cannot occlude`
7289
+ );
7290
+ }
7291
+ ids.push(id);
7292
+ }
7293
+ });
7294
+ built[i].occlusion = {
7295
+ by: ids,
7296
+ parameter,
7297
+ stops: stops.map((s) => ({ value: s.value, amount: s.amount }))
7298
+ };
7299
+ });
7300
+ }
7165
7301
 
7166
7302
  // src/auto-rig/generate.ts
7167
7303
  var MESH_CELLS = {
@@ -7204,8 +7340,15 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
7204
7340
  const hasNose = has("nose");
7205
7341
  const plateHalf = byRole.get("face").cropW / 2;
7206
7342
  checkHeadEdges(options.headEdges, byRole);
7207
- const entries = options.customParts !== void 0 && !(Array.isArray(options.customParts) && options.customParts.length === 0) ? checkCustomParts(options.customParts, byRole, canvas) : [];
7343
+ const physics = has("hair_front") ? hairPhysics(has("hair_back")) : void 0;
7208
7344
  const parameters = declareParameters(new Set(byRole.keys()));
7345
+ const entries = options.customParts !== void 0 && !(Array.isArray(options.customParts) && options.customParts.length === 0) ? checkCustomParts(
7346
+ options.customParts,
7347
+ byRole,
7348
+ canvas,
7349
+ parameters,
7350
+ new Set((physics ?? []).map((r) => r.output.parameter))
7351
+ ) : [];
7209
7352
  const bakes = options.bakes === void 0 ? void 0 : parseBakes(options.bakes, entries, parameters);
7210
7353
  checkStrandEdges(
7211
7354
  options.strandEdges,
@@ -7386,6 +7529,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
7386
7529
  parts.forEach((p, i) => {
7387
7530
  p.order = i;
7388
7531
  });
7532
+ writeOcclusion(entries, built, parts);
7389
7533
  if (bakes !== void 0) {
7390
7534
  bakeReports = applyBakes(bakes, built, entries, {
7391
7535
  canvas,
@@ -7402,7 +7546,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
7402
7546
  parameters,
7403
7547
  parts,
7404
7548
  deformers: deformerList,
7405
- ...has("hair_front") ? { physics: hairPhysics(has("hair_back")) } : {},
7549
+ ...physics !== void 0 ? { physics } : {},
7406
7550
  expressions: defaultExpressions(new Set(parameters.map((p) => p.id))),
7407
7551
  motions: defaultMotions(new Set(parameters.map((p) => p.id)))
7408
7552
  };