@ikijs/editor 0.31.0 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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";
@@ -1948,7 +1972,8 @@ var PROFILE_VALUES = {
1948
1972
  hairFollow: 1,
1949
1973
  fore: 0.2,
1950
1974
  backShare: 0.1,
1951
- earFar: 0.5,
1975
+ earFar: 0,
1976
+ earFore: 0.1,
1952
1977
  nodDown: 0.18,
1953
1978
  nodUp: 0.1,
1954
1979
  nodLead: 0.06,
@@ -2015,7 +2040,7 @@ function deriveProfile(v) {
2015
2040
  earNear: 1,
2016
2041
  /** The far ear's width at full turn, about its outer edge, where the head
2017
2042
  * leaves it room; less where it does not. */
2018
- earFarScale: 1 - v.fore
2043
+ earFarScale: 1 - v.earFore
2019
2044
  };
2020
2045
  const mid = v.nodDown * (1 + v.chinShare) / 2;
2021
2046
  const feature = (depth) => ({
@@ -7058,7 +7083,18 @@ function span2(name, v) {
7058
7083
  }
7059
7084
 
7060
7085
  // src/auto-rig/custom-parts.ts
7061
- var KEYS = ["id", "layer", "anchor", "tie", "after", "deformer", "rigid"];
7086
+ var KEYS = [
7087
+ "id",
7088
+ "layer",
7089
+ "anchor",
7090
+ "tie",
7091
+ "after",
7092
+ "deformer",
7093
+ "rigid",
7094
+ "occlusion"
7095
+ ];
7096
+ var OCCLUSION_KEYS = ["by", "parameter", "stops"];
7097
+ var STOP_KEYS = ["value", "amount"];
7062
7098
  var LAYER_KEYS = ["fileName", "bbox"];
7063
7099
  var finite4 = (v) => typeof v === "number" && Number.isFinite(v);
7064
7100
  var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -7066,7 +7102,7 @@ var inside = (b, [x, y]) => x >= b.x && x < b.x + b.w && y >= b.y && y < b.y + b
7066
7102
  var RESERVED = new Set(
7067
7103
  ROLE_TABLE.flatMap((r) => [r.role, ...partIdsOfRole(r.role)])
7068
7104
  );
7069
- function checkCustomParts(customParts, byRole, canvas) {
7105
+ function checkCustomParts(customParts, byRole, canvas, parameters, physicsOutputs) {
7070
7106
  if (!Array.isArray(customParts)) {
7071
7107
  throw new CustomPartError(
7072
7108
  `customParts must be an array, got ${customParts === null ? "null" : typeof customParts}`
@@ -7089,7 +7125,7 @@ function checkCustomParts(customParts, byRole, canvas) {
7089
7125
  throw new CustomPartError(`${at} must be a plain object`);
7090
7126
  }
7091
7127
  checkedKeys(at, entry, KEYS, CustomPartError);
7092
- const { id, layer, anchor, tie, after, deformer, rigid } = entry;
7128
+ const { id, layer, anchor, tie, after, deformer, rigid, occlusion } = entry;
7093
7129
  if (typeof id !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(id)) {
7094
7130
  throw new CustomPartError(
7095
7131
  `${at}.id must be a plain identifier (letters, digits, underscore; not starting with a digit), got ${JSON.stringify(id)}`
@@ -7140,9 +7176,84 @@ function checkCustomParts(customParts, byRole, canvas) {
7140
7176
  `${at}.rigid must be true or false, got ${JSON.stringify(rigid)}`
7141
7177
  );
7142
7178
  }
7179
+ if (occlusion !== void 0) {
7180
+ checkOcclusion(
7181
+ `${at}.occlusion`,
7182
+ occlusion,
7183
+ present,
7184
+ parameters,
7185
+ physicsOutputs
7186
+ );
7187
+ }
7143
7188
  });
7144
7189
  return customParts;
7145
7190
  }
7191
+ function checkOcclusion(at, occlusion, present, parameters, physicsOutputs) {
7192
+ if (!isObject3(occlusion)) {
7193
+ throw new CustomPartError(`${at} must be a plain object`);
7194
+ }
7195
+ checkedKeys(at, occlusion, OCCLUSION_KEYS, CustomPartError);
7196
+ const { by, parameter, stops } = occlusion;
7197
+ if (!Array.isArray(by) || by.length === 0) {
7198
+ throw new CustomPartError(
7199
+ `${at}.by must be a non-empty array of roles of the set, one of ${present.join(", ")}`
7200
+ );
7201
+ }
7202
+ by.forEach((role, j) => {
7203
+ if (typeof role !== "string" || !present.includes(role)) {
7204
+ throw new CustomPartError(
7205
+ `${at}.by[${j}] must be a role of the set, one of ${present.join(", ")}; got ${JSON.stringify(role)}`
7206
+ );
7207
+ }
7208
+ if (by.indexOf(role) !== j) {
7209
+ throw new CustomPartError(
7210
+ `${at}.by[${j}] "${role}" repeats an earlier role`
7211
+ );
7212
+ }
7213
+ });
7214
+ const descriptor = parameters.find((p) => p.id === parameter);
7215
+ if (typeof parameter !== "string" || descriptor === void 0) {
7216
+ throw new CustomPartError(
7217
+ `${at}.parameter must be a parameter the rig declares, got ${JSON.stringify(parameter)}`
7218
+ );
7219
+ }
7220
+ if (physicsOutputs.has(parameter)) {
7221
+ throw new CustomPartError(
7222
+ `${at}.parameter "${parameter}" is a physics output; an occlusion amount cannot be keyed on one`
7223
+ );
7224
+ }
7225
+ if (!Array.isArray(stops) || stops.length === 0) {
7226
+ throw new CustomPartError(`${at}.stops must be a non-empty array`);
7227
+ }
7228
+ const { min, max } = descriptor;
7229
+ let previous = -Infinity;
7230
+ stops.forEach((stop, k) => {
7231
+ const s = `${at}.stops[${k}]`;
7232
+ if (!isObject3(stop))
7233
+ throw new CustomPartError(`${s} must be a plain object`);
7234
+ checkedKeys(s, stop, STOP_KEYS, CustomPartError);
7235
+ const { value, amount } = stop;
7236
+ if (!finite4(value)) {
7237
+ throw new CustomPartError(
7238
+ `${s}.value must be a finite number, got ${JSON.stringify(value)}`
7239
+ );
7240
+ }
7241
+ if (value < min || value > max) {
7242
+ throw new CustomPartError(
7243
+ `${s}.value ${value} is outside parameter "${parameter}" range [${min},${max}]`
7244
+ );
7245
+ }
7246
+ if (value <= previous) {
7247
+ throw new CustomPartError(`${s}.value must be strictly ascending`);
7248
+ }
7249
+ previous = value;
7250
+ if (!finite4(amount) || amount < 0 || amount > 1) {
7251
+ throw new CustomPartError(
7252
+ `${s}.amount must be a number in 0..1, got ${JSON.stringify(amount)}`
7253
+ );
7254
+ }
7255
+ });
7256
+ }
7146
7257
  var CELL_PX2 = 20;
7147
7258
  function buildCustomParts(customParts, canvas) {
7148
7259
  return customParts.map(({ id, layer }) => {
@@ -7162,6 +7273,32 @@ function buildCustomParts(customParts, canvas) {
7162
7273
  };
7163
7274
  });
7164
7275
  }
7276
+ function writeOcclusion(customParts, built, parts) {
7277
+ const byId = new Map(parts.map((p) => [p.id, p]));
7278
+ customParts.forEach((entry, i) => {
7279
+ if (entry.occlusion === void 0) return;
7280
+ const { by, parameter, stops } = entry.occlusion;
7281
+ const ids = [];
7282
+ by.forEach((role, j) => {
7283
+ for (const id of partIdsOfRole(role)) {
7284
+ const part = byId.get(id);
7285
+ if (part === void 0) continue;
7286
+ 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;
7287
+ if (fault !== void 0) {
7288
+ throw new CustomPartError(
7289
+ `customParts[${i}].occlusion.by[${j}] "${role}" yields part "${id}", which ${fault.is}; ${fault.kind} cannot occlude`
7290
+ );
7291
+ }
7292
+ ids.push(id);
7293
+ }
7294
+ });
7295
+ built[i].occlusion = {
7296
+ by: ids,
7297
+ parameter,
7298
+ stops: stops.map((s) => ({ value: s.value, amount: s.amount }))
7299
+ };
7300
+ });
7301
+ }
7165
7302
 
7166
7303
  // src/auto-rig/generate.ts
7167
7304
  var MESH_CELLS = {
@@ -7204,8 +7341,15 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
7204
7341
  const hasNose = has("nose");
7205
7342
  const plateHalf = byRole.get("face").cropW / 2;
7206
7343
  checkHeadEdges(options.headEdges, byRole);
7207
- const entries = options.customParts !== void 0 && !(Array.isArray(options.customParts) && options.customParts.length === 0) ? checkCustomParts(options.customParts, byRole, canvas) : [];
7344
+ const physics = has("hair_front") ? hairPhysics(has("hair_back")) : void 0;
7208
7345
  const parameters = declareParameters(new Set(byRole.keys()));
7346
+ const entries = options.customParts !== void 0 && !(Array.isArray(options.customParts) && options.customParts.length === 0) ? checkCustomParts(
7347
+ options.customParts,
7348
+ byRole,
7349
+ canvas,
7350
+ parameters,
7351
+ new Set((physics ?? []).map((r) => r.output.parameter))
7352
+ ) : [];
7209
7353
  const bakes = options.bakes === void 0 ? void 0 : parseBakes(options.bakes, entries, parameters);
7210
7354
  checkStrandEdges(
7211
7355
  options.strandEdges,
@@ -7386,6 +7530,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
7386
7530
  parts.forEach((p, i) => {
7387
7531
  p.order = i;
7388
7532
  });
7533
+ writeOcclusion(entries, built, parts);
7389
7534
  if (bakes !== void 0) {
7390
7535
  bakeReports = applyBakes(bakes, built, entries, {
7391
7536
  canvas,
@@ -7402,7 +7547,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
7402
7547
  parameters,
7403
7548
  parts,
7404
7549
  deformers: deformerList,
7405
- ...has("hair_front") ? { physics: hairPhysics(has("hair_back")) } : {},
7550
+ ...physics !== void 0 ? { physics } : {},
7406
7551
  expressions: defaultExpressions(new Set(parameters.map((p) => p.id))),
7407
7552
  motions: defaultMotions(new Set(parameters.map((p) => p.id)))
7408
7553
  };