@ikijs/editor 0.30.1 → 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 CHANGED
@@ -40,19 +40,19 @@ leaves the document untouched.
40
40
 
41
41
  ## API
42
42
 
43
- | Area | Exports |
44
- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
- | Document | `EditorDocument`, `EditCommand` |
46
- | Part edits | `AddPart`, `DeletePart`, `SetPartColor`, `SetPartWidth`, `SetPartHeight`, `SetPartOrder`, `SetPartTransform`, `SetPartBindings`, `SetPartMesh`, `SetPartDeformer` |
47
- | Deformer edits | `AddDeformer`, `DeleteDeformer`, `SetDeformerParent`, `SetDeformerTransform`, `SetDeformerBindings`, `SetDeformerPivot` (+ `X`/`Y`), `CaptureGridKeyform` |
48
- | Physics edits | `AddPhysicsRig`, `SetPhysicsRig`, `DeletePhysicsRig` |
49
- | Referential guards | `validateDeformerReparent`, `validateDeformerDelete`, `validatePartAttach` |
50
- | Atlas | `packAtlas`, `uvRectFor`, `ATLAS_PADDING`, `UV_INSET_PX` |
51
- | Grid keyforms | `computeGridOffsets`, `interpolateGridOffsets`, `upsertGridKeyform` |
52
- | Factories | `createDefaultPart`, `createDefaultMatrixDeformer`, `createDefaultWarpDeformer`, `createGridMesh` |
53
- | Pixels | `detectAlphaBbox`, `ALPHA_BBOX_THRESHOLD`, `AlphaBbox`, `ALPHA_OPAQUE`, `HEAD_BAND`, `SPECK_CORE_FRACTION`, `denseCoreOf`, `isSpeckCore`, `foregroundSpan`, `headHalfOf` |
54
- | Auto-rig | `generateIkiFromLayerSet`, `parseLayerRoles`, `partIdsOfRole`, `createLayerSetMeasurer`, `armGeometry`, `ArmGeometry`, `forearmPoseGeometry`, `ForearmPoseGeometry`, `mouthOpening`, `mouthFold`, `mouthRestShift`, `columnRuns`, `LIP_ROLES`, `LipRole`, `Opening`, `OpeningColumn`, `TurnTargets`, `DEFAULT_TURN_TARGETS`, `TurnSolveReport`, `HairSideRoom`, `RigPlan`, `RigPlanError`, `RIG_PLAN_FIELDS`, `RigPlanReport`, `RigPatch`, `PatchReport`, `PatchError`, `SHUT_BY`, `LayerGeometryError` |
55
- | Bindings | `captureBindingEndpoint` |
43
+ | Area | Exports |
44
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
+ | Document | `EditorDocument`, `EditCommand` |
46
+ | Part edits | `AddPart`, `DeletePart`, `SetPartColor`, `SetPartWidth`, `SetPartHeight`, `SetPartOrder`, `SetPartTransform`, `SetPartBindings`, `SetPartMesh`, `SetPartDeformer` |
47
+ | Deformer edits | `AddDeformer`, `DeleteDeformer`, `SetDeformerParent`, `SetDeformerTransform`, `SetDeformerBindings`, `SetDeformerPivot` (+ `X`/`Y`), `CaptureGridKeyform` |
48
+ | Physics edits | `AddPhysicsRig`, `SetPhysicsRig`, `DeletePhysicsRig` |
49
+ | Referential guards | `validateDeformerReparent`, `validateDeformerDelete`, `validatePartAttach` |
50
+ | Atlas | `packAtlas`, `uvRectFor`, `ATLAS_PADDING`, `UV_INSET_PX` |
51
+ | Grid keyforms | `computeGridOffsets`, `interpolateGridOffsets`, `upsertGridKeyform` |
52
+ | Factories | `createDefaultPart`, `createDefaultMatrixDeformer`, `createDefaultWarpDeformer`, `createGridMesh` |
53
+ | Pixels | `detectAlphaBbox`, `ALPHA_BBOX_THRESHOLD`, `AlphaBbox`, `ALPHA_OPAQUE`, `HEAD_BAND`, `SPECK_CORE_FRACTION`, `denseCoreOf`, `isSpeckCore`, `foregroundSpan`, `headHalfOf` |
54
+ | Auto-rig | `generateIkiFromLayerSet`, `parseLayerRoles`, `partIdsOfRole`, `createLayerSetMeasurer`, `armGeometry`, `ArmGeometry`, `forearmPoseGeometry`, `ForearmPoseGeometry`, `mouthOpening`, `mouthFold`, `mouthRestShift`, `columnRuns`, `LIP_ROLES`, `LipRole`, `Opening`, `OpeningColumn`, `TurnTargets`, `DEFAULT_TURN_TARGETS`, `TurnSolveReport`, `HairSideRoom`, `RigPlan`, `RigPlanError`, `RIG_PLAN_FIELDS`, `RigPlanReport`, `RigPatch`, `PatchReport`, `PatchError`, `RigBakes`, `BakeTarget`, `BakeReport`, `BakeError`, `rigidResidual`, `SHUT_BY`, `LayerGeometryError` |
55
+ | Bindings | `captureBindingEndpoint` |
56
56
 
57
57
  ## Auto-rig
58
58
 
@@ -100,7 +100,19 @@ profile in three groups (`turn`, `hair`, `eyes`: the whole turn, the
100
100
  features' lead, the front hair's follow and its outline's, the blink and the
101
101
  hair sway), validated against `RIG_PLAN_FIELDS`. `options.patches` adds displacements to a generated rig's keyform
102
102
  stops (`RigPatch`), replayed on every rig and reported through
103
- `options.onPatchesApplied`; a refused op throws `PatchError`. The turn
103
+ `options.onPatchesApplied`; a refused op throws `PatchError`. `options.customParts` adds parts the
104
+ auto-rigger has no role for (`CustomPart`: `id`, `layer`, `anchor`, `tie`,
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
107
+ `after` role's last part; the rest of the model is as without them but for
108
+ the `order` of every part after an inserted one (a set rigged without
109
+ `customParts` is byte-identical), and a malformed entry throws
110
+ `CustomPartError`. `options.bakes` (`RigBakes`: control lattices per
111
+ parameter stop, `meshes` to re-mesh) writes warps on those parts that
112
+ land them where the lattice asks, solved in closed form through the
113
+ head's frame; a `rigid` custom part refuses a bend over 0.0025 face
114
+ widths (`rigidResidual`), a refused input throws `BakeError`, and
115
+ `options.onBakesApplied` gets what each part was asked for. The turn
104
116
  is bounded by the art: the far eye stays inside the plate's outline and, given
105
117
  `options.strandEdges`, the far iris may not slide under the bangs' side strand
106
118
  any further than it is painted — best effort, never a refusal. The report's
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 clip-mask
255
- * references, the one part→part reference in the contract. `apply` refuses to
256
- * delete a part still used as another part's `clip.masks` entry (guard below),
257
- * so it can never leave a dangling mask ref that would fail `toIkiModel()`.
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
@@ -1052,6 +1053,145 @@ interface PatchReport {
1052
1053
  declare class PatchError extends Error {
1053
1054
  constructor(message: string);
1054
1055
  }
1056
+ /**
1057
+ * A part the auto-rigger has no role for (a hairpin, a twin tail): drawn from
1058
+ * its own canvas-sized layer, hung on the head with plain warps.
1059
+ */
1060
+ interface CustomPart {
1061
+ /** A plain identifier; never a role's name or a part id a role yields. */
1062
+ id: string;
1063
+ layer: {
1064
+ fileName: string;
1065
+ /** The layer's alpha box in canvas px, as `LayerInput.bbox`. */
1066
+ bbox: {
1067
+ x: number;
1068
+ y: number;
1069
+ w: number;
1070
+ h: number;
1071
+ };
1072
+ };
1073
+ /** The role whose layer the part is fixed against. */
1074
+ anchor: string;
1075
+ /** Where the part meets the anchor, canvas px: inside both layers' boxes. */
1076
+ tie: [number, number];
1077
+ /** A role of the set: the part is drawn right after that role's last part. */
1078
+ after: string;
1079
+ /** The matrix deformer it hangs on; only `headDeformer` (the default) is
1080
+ * accepted. */
1081
+ deformer?: string;
1082
+ /** The art does not bend (a hairpin, a ribbon): a bake that bends it more
1083
+ * than 0.0025 face widths at a stop is refused (`BakeError`). */
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
+ };
1098
+ }
1099
+ /** A `customParts` entry that is malformed or misses the layer set. Its
1100
+ * message starts `customParts[i].<field>` (`customParts` alone for a
1101
+ * non-array). */
1102
+ declare class CustomPartError extends Error {
1103
+ constructor(message: string);
1104
+ }
1105
+ /**
1106
+ * One lattice a bake spreads over the custom parts it names: `(cols + 1) x
1107
+ * (rows + 1)` control points laid evenly over `box`, listed row-major from the
1108
+ * top-left (the top row first, each row left to right). Every position and
1109
+ * displacement is in canvas px, origin top-left, y DOWN.
1110
+ *
1111
+ * A stop is exact where the bake writes it, and so is the end key a side's
1112
+ * outermost stop gets at the parameter's range end; between keys the motion
1113
+ * is interpolated, and under a parent that rotates with the parameter (the
1114
+ * head's roll) it bows off the straight line, so put a stop where the path
1115
+ * matters. A side with no stop is not moved by this target. Targets that name the same part and
1116
+ * parameter add.
1117
+ */
1118
+ interface BakeTarget {
1119
+ /** A label for reports; the rig ignores it. */
1120
+ id?: string;
1121
+ /** A parameter the model declares. */
1122
+ param: string;
1123
+ /** Custom part ids only: a bake moves nothing else. */
1124
+ parts: string[];
1125
+ /** Canvas px; absent, the union of the parts' rest boxes grown by 2 px. */
1126
+ box?: {
1127
+ x: number;
1128
+ y: number;
1129
+ w: number;
1130
+ h: number;
1131
+ };
1132
+ /** Integers 1 to 32. */
1133
+ grid: {
1134
+ cols: number;
1135
+ rows: number;
1136
+ };
1137
+ /** Keyed by a value of `param` inside its range, never its default (rest is
1138
+ * implicit). A stop is the part's TOTAL landed displacement from rest when
1139
+ * `param` alone is at that value and every other parameter is at its
1140
+ * default: the head's own carry is included, so a neighbour's measured
1141
+ * displacement at that pose is written as it is. */
1142
+ stops: Record<string, [number, number][]>;
1143
+ /** Fill a missing stop from the one at the value mirrored about the rest
1144
+ * value: the lattice reflected about the box's vertical centre line, `dx`
1145
+ * negated. A written stop is never overwritten. */
1146
+ mirror?: boolean;
1147
+ /** Why the target exists, for whoever edits the bakes next; ignored. */
1148
+ why?: string;
1149
+ }
1150
+ /**
1151
+ * Lattice bakes for custom parts: what moves them when the head turns or the
1152
+ * hair sways, which an unrigged custom part otherwise does not. A bake moves
1153
+ * custom parts only.
1154
+ */
1155
+ interface RigBakes {
1156
+ targets: BakeTarget[];
1157
+ /** Re-mesh a custom part before baking: `rows` (canvas-px y, strictly
1158
+ * increasing, first the part's top and last its bottom within 0.5 px) with
1159
+ * `cols`, or `cellPx` for a uniform grid. "The box" here is the part's own
1160
+ * rest box (its layer's bbox), not a target's `box`. */
1161
+ meshes?: Record<string, {
1162
+ rows: number[];
1163
+ cols: number;
1164
+ } | {
1165
+ cellPx: number;
1166
+ }>;
1167
+ }
1168
+ /** What a bake asked of one custom part on one parameter, for a caller that
1169
+ * checks the landing. */
1170
+ interface BakeReport {
1171
+ part: string;
1172
+ param: string;
1173
+ /** The stops written, ascending (rest, implicit, not among them). */
1174
+ stops: number[];
1175
+ vertexCount: number;
1176
+ /** Per stop, in `stops` order: each vertex's wanted displacement from rest,
1177
+ * `[dx0, dy0, dx1, dy1, ...]` in canvas px (y down), before the offsets
1178
+ * were rounded. */
1179
+ wanted: number[][];
1180
+ /** The end keys the bake wrote at the parameter's range ends, ascending,
1181
+ * each with the displacement it was solved for (`wanted` as above): its
1182
+ * side's outermost stop's. A side with no stop, or whose outermost stop is
1183
+ * at the end, has none. */
1184
+ ends: {
1185
+ value: number;
1186
+ wanted: number[];
1187
+ }[];
1188
+ }
1189
+ /** A `bakes` input that is malformed or misses the model. Its message starts
1190
+ * with the path of the field (`bakes.targets[2].stops["30"][7]`,
1191
+ * `bakes.meshes.tail_R.rows[0]`). */
1192
+ declare class BakeError extends Error {
1193
+ constructor(message: string);
1194
+ }
1055
1195
  interface HeadEdges {
1056
1196
  left: {
1057
1197
  role: string;
@@ -1076,6 +1216,17 @@ interface GenerateOptions {
1076
1216
  /** Displacements added to the generated rig's stops (`RigPatch`), replayed
1077
1217
  * on every rig; a refused op throws `PatchError` and rigs nothing. */
1078
1218
  patches?: RigPatch[];
1219
+ /** Parts the auto-rigger has no role for, put in the model unrigged: drawn
1220
+ * from their own layers, hung on the head. Every entry is checked first
1221
+ * (`CustomPartError`). */
1222
+ customParts?: CustomPart[];
1223
+ /** Lattices that move the custom parts when a parameter does (`RigBakes`):
1224
+ * written as warps on the parts, after the patches. A refused input throws
1225
+ * `BakeError` and rigs nothing. */
1226
+ bakes?: RigBakes;
1227
+ /** What `bakes` asked of each custom part and parameter — called only when
1228
+ * `bakes` is given, and only for a model that validates. */
1229
+ onBakesApplied?: (report: BakeReport[]) => void;
1079
1230
  /** What each of `patches` moved, in order — called only when `patches` is
1080
1231
  * given, and only for a model that validates. */
1081
1232
  onPatchesApplied?: (report: PatchReport[]) => void;
@@ -1130,17 +1281,17 @@ interface GenerateOptions {
1130
1281
  * the shoulder's +: it turns CW on the +x side, CCW on the −x side.
1131
1282
  */
1132
1283
 
1133
- type Point = {
1284
+ type Point$1 = {
1134
1285
  x: number;
1135
1286
  y: number;
1136
1287
  };
1137
1288
  interface ArmGeometry {
1138
1289
  /** The shoulder pivot, model space: the deltoid cap's centre. */
1139
- shoulder: Point;
1290
+ shoulder: Point$1;
1140
1291
  /** r_u: the upper arm's half-width under the cap, px. */
1141
1292
  shoulderRadius: number;
1142
1293
  /** The elbow pivot, model space. Its y is the seam the two meshes meet on. */
1143
- elbow: Point;
1294
+ elbow: Point$1;
1144
1295
  /** The elbow cap's largest radius, px. */
1145
1296
  capRadius: number;
1146
1297
  /** The cap's radius on each of its `CAP_SEGMENTS + 1` rays, from the elbow
@@ -1395,6 +1546,26 @@ declare function mouthFold(role: LipRole | LipInsideRole, opening: Opening): (co
1395
1546
  * clamps differently, and the knot grid starts at the union's edge. */
1396
1547
  declare function mouthRestShift(byRole: Map<string, LayerInput>): (role: "lip_upper" | "lip_lower", col: number, row: number) => number | undefined;
1397
1548
 
1549
+ /**
1550
+ * The `bakes` generator input, in two steps. `parseBakes` checks the whole
1551
+ * input before anything is applied, so a refusal changes nothing; each
1552
+ * message starts at the path of the field (`bakes.targets[2].stops["30"][7]`).
1553
+ * `applyBakes` then re-meshes the named parts and writes their warps, solved
1554
+ * in closed form through the head's chain.
1555
+ */
1556
+
1557
+ type Point = [number, number];
1558
+ /**
1559
+ * The largest distance, over the point set, from where `moved` puts a point
1560
+ * to where the least-squares rigid fit of `rest` puts it: 0 for a rigid
1561
+ * motion, the bend of anything else. The fit is a rotation and a translation,
1562
+ * no scale and no reflection, and it minimises the summed squares, not the
1563
+ * largest distance (so the result can exceed the minimax one). Both lists
1564
+ * hold the same points in the same order, in one frame, so their lengths must
1565
+ * be equal.
1566
+ */
1567
+ declare function rigidResidual(rest: readonly Point[], moved: readonly Point[]): number;
1568
+
1398
1569
  /**
1399
1570
  * `generateIkiFromLayerSet`: role layers in, a rigged `.iki` out.
1400
1571
  *
@@ -1760,10 +1931,11 @@ declare function validateDeformerReparent(deformers: IkiDeformer[], deformerId:
1760
1931
  * `toIkiModel()` refuses.
1761
1932
  *
1762
1933
  * Note: there is no validatePartDelete here, but NOT because parts are
1763
- * unreferenced — `clip.masks` names parts by id. That invariant is enforced
1764
- * closer to the edits that could break it: `DeletePart` refuses to remove a
1765
- * part still used as a mask, and `SetPartMesh` refuses to strip the mesh off
1766
- * one (masks must be mesh parts). No command creates or edits `clip`.
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`.
1767
1939
  */
1768
1940
  declare function validateDeformerDelete(deformers: IkiDeformer[], parts: IkiPart[], physicsChains: IkiPhysicsChain[], deformerId: string): void;
1769
1941
  /**
@@ -1820,4 +1992,4 @@ declare function createGridMesh(cols: number, rows: number): IkiMesh;
1820
1992
  */
1821
1993
  declare function createDefaultWarpDeformer(model: IkiModel): IkiWarpDeformer;
1822
1994
 
1823
- export { ALPHA_BBOX_THRESHOLD, ALPHA_OPAQUE, ATLAS_PADDING, AddDeformer, AddPart, AddPhysicsRig, type AlphaBbox, type ApplyAtlasInput, type ArmGeometry, type AtlasAssignment, type AtlasLayout, type AtlasPlacement, type AtlasSource, CaptureGridKeyform, DEFAULT_TURN_TARGETS, type DeformerTransformChannel, DeleteDeformer, DeletePart, DeletePhysicsRig, type EditCommand, type EditTransformChannel, EditorDocument, type ForearmPoseGeometry, HEAD_BAND, type HairSideRoom, type IrisStrand, LIP_INSIDE_ROLES, LIP_ROLES, LayerGeometryError, type LayerInput, type LayerSetMeasurement, type LayerSetMeasurer, type LipInsideRole, type LipRole, type Opening, type OpeningColumn, PatchError, type PatchReport, RIG_PLAN_FIELDS, type RgbaLayer, type RigPatch, type RigPlan, RigPlanError, type RigPlanField, type RigPlanReport, SHUT_BY, SPECK_CORE_FRACTION, SetDeformerBindings, SetDeformerParent, SetDeformerPivot, SetDeformerPivotX, SetDeformerPivotY, SetDeformerTransform, SetPartBindings, SetPartColor, SetPartDeformer, SetPartHeight, SetPartMesh, SetPartOrder, SetPartTransform, SetPartWidth, SetPhysicsRig, type StrandOverlap, type TurnDepths, type TurnSolveReport, TurnTargetError, type TurnTargets, UV_INSET_PX, armGeometry, captureBindingEndpoint, columnRuns, computeGridOffsets, createDefaultMatrixDeformer, createDefaultPart, createDefaultWarpDeformer, createGridMesh, createLayerSetMeasurer, denseCoreOf, detectAlphaBbox, forearmPoseGeometry, foregroundSpan, generateIkiFromLayerSet, headHalfOf, interpolateGridOffsets, isLipInsideRole, isSpeckCore, mouthFold, mouthOpening, mouthRestShift, packAtlas, parseLayerRoles, partIdsOfRole, styleRemovedMessage, upsertGridKeyform, uvRectFor, validateDeformerDelete, validateDeformerReparent, validatePartAttach };
1995
+ export { ALPHA_BBOX_THRESHOLD, ALPHA_OPAQUE, ATLAS_PADDING, AddDeformer, AddPart, AddPhysicsRig, type AlphaBbox, type ApplyAtlasInput, type ArmGeometry, type AtlasAssignment, type AtlasLayout, type AtlasPlacement, type AtlasSource, BakeError, type BakeReport, type BakeTarget, CaptureGridKeyform, type CustomPart, CustomPartError, DEFAULT_TURN_TARGETS, type DeformerTransformChannel, DeleteDeformer, DeletePart, DeletePhysicsRig, type EditCommand, type EditTransformChannel, EditorDocument, type ForearmPoseGeometry, HEAD_BAND, type HairSideRoom, type IrisStrand, LIP_INSIDE_ROLES, LIP_ROLES, LayerGeometryError, type LayerInput, type LayerSetMeasurement, type LayerSetMeasurer, type LipInsideRole, type LipRole, type Opening, type OpeningColumn, PatchError, type PatchReport, RIG_PLAN_FIELDS, type RgbaLayer, type RigBakes, type RigPatch, type RigPlan, RigPlanError, type RigPlanField, type RigPlanReport, SHUT_BY, SPECK_CORE_FRACTION, SetDeformerBindings, SetDeformerParent, SetDeformerPivot, SetDeformerPivotX, SetDeformerPivotY, SetDeformerTransform, SetPartBindings, SetPartColor, SetPartDeformer, SetPartHeight, SetPartMesh, SetPartOrder, SetPartTransform, SetPartWidth, SetPhysicsRig, type StrandOverlap, type TurnDepths, type TurnSolveReport, TurnTargetError, type TurnTargets, UV_INSET_PX, armGeometry, captureBindingEndpoint, columnRuns, computeGridOffsets, createDefaultMatrixDeformer, createDefaultPart, createDefaultWarpDeformer, createGridMesh, createLayerSetMeasurer, denseCoreOf, detectAlphaBbox, forearmPoseGeometry, foregroundSpan, generateIkiFromLayerSet, headHalfOf, interpolateGridOffsets, isLipInsideRole, isSpeckCore, mouthFold, mouthOpening, mouthRestShift, packAtlas, parseLayerRoles, partIdsOfRole, rigidResidual, styleRemovedMessage, upsertGridKeyform, uvRectFor, validateDeformerDelete, validateDeformerReparent, validatePartAttach };
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 clip-mask
255
- * references, the one part→part reference in the contract. `apply` refuses to
256
- * delete a part still used as another part's `clip.masks` entry (guard below),
257
- * so it can never leave a dangling mask ref that would fail `toIkiModel()`.
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
@@ -1052,6 +1053,145 @@ interface PatchReport {
1052
1053
  declare class PatchError extends Error {
1053
1054
  constructor(message: string);
1054
1055
  }
1056
+ /**
1057
+ * A part the auto-rigger has no role for (a hairpin, a twin tail): drawn from
1058
+ * its own canvas-sized layer, hung on the head with plain warps.
1059
+ */
1060
+ interface CustomPart {
1061
+ /** A plain identifier; never a role's name or a part id a role yields. */
1062
+ id: string;
1063
+ layer: {
1064
+ fileName: string;
1065
+ /** The layer's alpha box in canvas px, as `LayerInput.bbox`. */
1066
+ bbox: {
1067
+ x: number;
1068
+ y: number;
1069
+ w: number;
1070
+ h: number;
1071
+ };
1072
+ };
1073
+ /** The role whose layer the part is fixed against. */
1074
+ anchor: string;
1075
+ /** Where the part meets the anchor, canvas px: inside both layers' boxes. */
1076
+ tie: [number, number];
1077
+ /** A role of the set: the part is drawn right after that role's last part. */
1078
+ after: string;
1079
+ /** The matrix deformer it hangs on; only `headDeformer` (the default) is
1080
+ * accepted. */
1081
+ deformer?: string;
1082
+ /** The art does not bend (a hairpin, a ribbon): a bake that bends it more
1083
+ * than 0.0025 face widths at a stop is refused (`BakeError`). */
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
+ };
1098
+ }
1099
+ /** A `customParts` entry that is malformed or misses the layer set. Its
1100
+ * message starts `customParts[i].<field>` (`customParts` alone for a
1101
+ * non-array). */
1102
+ declare class CustomPartError extends Error {
1103
+ constructor(message: string);
1104
+ }
1105
+ /**
1106
+ * One lattice a bake spreads over the custom parts it names: `(cols + 1) x
1107
+ * (rows + 1)` control points laid evenly over `box`, listed row-major from the
1108
+ * top-left (the top row first, each row left to right). Every position and
1109
+ * displacement is in canvas px, origin top-left, y DOWN.
1110
+ *
1111
+ * A stop is exact where the bake writes it, and so is the end key a side's
1112
+ * outermost stop gets at the parameter's range end; between keys the motion
1113
+ * is interpolated, and under a parent that rotates with the parameter (the
1114
+ * head's roll) it bows off the straight line, so put a stop where the path
1115
+ * matters. A side with no stop is not moved by this target. Targets that name the same part and
1116
+ * parameter add.
1117
+ */
1118
+ interface BakeTarget {
1119
+ /** A label for reports; the rig ignores it. */
1120
+ id?: string;
1121
+ /** A parameter the model declares. */
1122
+ param: string;
1123
+ /** Custom part ids only: a bake moves nothing else. */
1124
+ parts: string[];
1125
+ /** Canvas px; absent, the union of the parts' rest boxes grown by 2 px. */
1126
+ box?: {
1127
+ x: number;
1128
+ y: number;
1129
+ w: number;
1130
+ h: number;
1131
+ };
1132
+ /** Integers 1 to 32. */
1133
+ grid: {
1134
+ cols: number;
1135
+ rows: number;
1136
+ };
1137
+ /** Keyed by a value of `param` inside its range, never its default (rest is
1138
+ * implicit). A stop is the part's TOTAL landed displacement from rest when
1139
+ * `param` alone is at that value and every other parameter is at its
1140
+ * default: the head's own carry is included, so a neighbour's measured
1141
+ * displacement at that pose is written as it is. */
1142
+ stops: Record<string, [number, number][]>;
1143
+ /** Fill a missing stop from the one at the value mirrored about the rest
1144
+ * value: the lattice reflected about the box's vertical centre line, `dx`
1145
+ * negated. A written stop is never overwritten. */
1146
+ mirror?: boolean;
1147
+ /** Why the target exists, for whoever edits the bakes next; ignored. */
1148
+ why?: string;
1149
+ }
1150
+ /**
1151
+ * Lattice bakes for custom parts: what moves them when the head turns or the
1152
+ * hair sways, which an unrigged custom part otherwise does not. A bake moves
1153
+ * custom parts only.
1154
+ */
1155
+ interface RigBakes {
1156
+ targets: BakeTarget[];
1157
+ /** Re-mesh a custom part before baking: `rows` (canvas-px y, strictly
1158
+ * increasing, first the part's top and last its bottom within 0.5 px) with
1159
+ * `cols`, or `cellPx` for a uniform grid. "The box" here is the part's own
1160
+ * rest box (its layer's bbox), not a target's `box`. */
1161
+ meshes?: Record<string, {
1162
+ rows: number[];
1163
+ cols: number;
1164
+ } | {
1165
+ cellPx: number;
1166
+ }>;
1167
+ }
1168
+ /** What a bake asked of one custom part on one parameter, for a caller that
1169
+ * checks the landing. */
1170
+ interface BakeReport {
1171
+ part: string;
1172
+ param: string;
1173
+ /** The stops written, ascending (rest, implicit, not among them). */
1174
+ stops: number[];
1175
+ vertexCount: number;
1176
+ /** Per stop, in `stops` order: each vertex's wanted displacement from rest,
1177
+ * `[dx0, dy0, dx1, dy1, ...]` in canvas px (y down), before the offsets
1178
+ * were rounded. */
1179
+ wanted: number[][];
1180
+ /** The end keys the bake wrote at the parameter's range ends, ascending,
1181
+ * each with the displacement it was solved for (`wanted` as above): its
1182
+ * side's outermost stop's. A side with no stop, or whose outermost stop is
1183
+ * at the end, has none. */
1184
+ ends: {
1185
+ value: number;
1186
+ wanted: number[];
1187
+ }[];
1188
+ }
1189
+ /** A `bakes` input that is malformed or misses the model. Its message starts
1190
+ * with the path of the field (`bakes.targets[2].stops["30"][7]`,
1191
+ * `bakes.meshes.tail_R.rows[0]`). */
1192
+ declare class BakeError extends Error {
1193
+ constructor(message: string);
1194
+ }
1055
1195
  interface HeadEdges {
1056
1196
  left: {
1057
1197
  role: string;
@@ -1076,6 +1216,17 @@ interface GenerateOptions {
1076
1216
  /** Displacements added to the generated rig's stops (`RigPatch`), replayed
1077
1217
  * on every rig; a refused op throws `PatchError` and rigs nothing. */
1078
1218
  patches?: RigPatch[];
1219
+ /** Parts the auto-rigger has no role for, put in the model unrigged: drawn
1220
+ * from their own layers, hung on the head. Every entry is checked first
1221
+ * (`CustomPartError`). */
1222
+ customParts?: CustomPart[];
1223
+ /** Lattices that move the custom parts when a parameter does (`RigBakes`):
1224
+ * written as warps on the parts, after the patches. A refused input throws
1225
+ * `BakeError` and rigs nothing. */
1226
+ bakes?: RigBakes;
1227
+ /** What `bakes` asked of each custom part and parameter — called only when
1228
+ * `bakes` is given, and only for a model that validates. */
1229
+ onBakesApplied?: (report: BakeReport[]) => void;
1079
1230
  /** What each of `patches` moved, in order — called only when `patches` is
1080
1231
  * given, and only for a model that validates. */
1081
1232
  onPatchesApplied?: (report: PatchReport[]) => void;
@@ -1130,17 +1281,17 @@ interface GenerateOptions {
1130
1281
  * the shoulder's +: it turns CW on the +x side, CCW on the −x side.
1131
1282
  */
1132
1283
 
1133
- type Point = {
1284
+ type Point$1 = {
1134
1285
  x: number;
1135
1286
  y: number;
1136
1287
  };
1137
1288
  interface ArmGeometry {
1138
1289
  /** The shoulder pivot, model space: the deltoid cap's centre. */
1139
- shoulder: Point;
1290
+ shoulder: Point$1;
1140
1291
  /** r_u: the upper arm's half-width under the cap, px. */
1141
1292
  shoulderRadius: number;
1142
1293
  /** The elbow pivot, model space. Its y is the seam the two meshes meet on. */
1143
- elbow: Point;
1294
+ elbow: Point$1;
1144
1295
  /** The elbow cap's largest radius, px. */
1145
1296
  capRadius: number;
1146
1297
  /** The cap's radius on each of its `CAP_SEGMENTS + 1` rays, from the elbow
@@ -1395,6 +1546,26 @@ declare function mouthFold(role: LipRole | LipInsideRole, opening: Opening): (co
1395
1546
  * clamps differently, and the knot grid starts at the union's edge. */
1396
1547
  declare function mouthRestShift(byRole: Map<string, LayerInput>): (role: "lip_upper" | "lip_lower", col: number, row: number) => number | undefined;
1397
1548
 
1549
+ /**
1550
+ * The `bakes` generator input, in two steps. `parseBakes` checks the whole
1551
+ * input before anything is applied, so a refusal changes nothing; each
1552
+ * message starts at the path of the field (`bakes.targets[2].stops["30"][7]`).
1553
+ * `applyBakes` then re-meshes the named parts and writes their warps, solved
1554
+ * in closed form through the head's chain.
1555
+ */
1556
+
1557
+ type Point = [number, number];
1558
+ /**
1559
+ * The largest distance, over the point set, from where `moved` puts a point
1560
+ * to where the least-squares rigid fit of `rest` puts it: 0 for a rigid
1561
+ * motion, the bend of anything else. The fit is a rotation and a translation,
1562
+ * no scale and no reflection, and it minimises the summed squares, not the
1563
+ * largest distance (so the result can exceed the minimax one). Both lists
1564
+ * hold the same points in the same order, in one frame, so their lengths must
1565
+ * be equal.
1566
+ */
1567
+ declare function rigidResidual(rest: readonly Point[], moved: readonly Point[]): number;
1568
+
1398
1569
  /**
1399
1570
  * `generateIkiFromLayerSet`: role layers in, a rigged `.iki` out.
1400
1571
  *
@@ -1760,10 +1931,11 @@ declare function validateDeformerReparent(deformers: IkiDeformer[], deformerId:
1760
1931
  * `toIkiModel()` refuses.
1761
1932
  *
1762
1933
  * Note: there is no validatePartDelete here, but NOT because parts are
1763
- * unreferenced — `clip.masks` names parts by id. That invariant is enforced
1764
- * closer to the edits that could break it: `DeletePart` refuses to remove a
1765
- * part still used as a mask, and `SetPartMesh` refuses to strip the mesh off
1766
- * one (masks must be mesh parts). No command creates or edits `clip`.
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`.
1767
1939
  */
1768
1940
  declare function validateDeformerDelete(deformers: IkiDeformer[], parts: IkiPart[], physicsChains: IkiPhysicsChain[], deformerId: string): void;
1769
1941
  /**
@@ -1820,4 +1992,4 @@ declare function createGridMesh(cols: number, rows: number): IkiMesh;
1820
1992
  */
1821
1993
  declare function createDefaultWarpDeformer(model: IkiModel): IkiWarpDeformer;
1822
1994
 
1823
- export { ALPHA_BBOX_THRESHOLD, ALPHA_OPAQUE, ATLAS_PADDING, AddDeformer, AddPart, AddPhysicsRig, type AlphaBbox, type ApplyAtlasInput, type ArmGeometry, type AtlasAssignment, type AtlasLayout, type AtlasPlacement, type AtlasSource, CaptureGridKeyform, DEFAULT_TURN_TARGETS, type DeformerTransformChannel, DeleteDeformer, DeletePart, DeletePhysicsRig, type EditCommand, type EditTransformChannel, EditorDocument, type ForearmPoseGeometry, HEAD_BAND, type HairSideRoom, type IrisStrand, LIP_INSIDE_ROLES, LIP_ROLES, LayerGeometryError, type LayerInput, type LayerSetMeasurement, type LayerSetMeasurer, type LipInsideRole, type LipRole, type Opening, type OpeningColumn, PatchError, type PatchReport, RIG_PLAN_FIELDS, type RgbaLayer, type RigPatch, type RigPlan, RigPlanError, type RigPlanField, type RigPlanReport, SHUT_BY, SPECK_CORE_FRACTION, SetDeformerBindings, SetDeformerParent, SetDeformerPivot, SetDeformerPivotX, SetDeformerPivotY, SetDeformerTransform, SetPartBindings, SetPartColor, SetPartDeformer, SetPartHeight, SetPartMesh, SetPartOrder, SetPartTransform, SetPartWidth, SetPhysicsRig, type StrandOverlap, type TurnDepths, type TurnSolveReport, TurnTargetError, type TurnTargets, UV_INSET_PX, armGeometry, captureBindingEndpoint, columnRuns, computeGridOffsets, createDefaultMatrixDeformer, createDefaultPart, createDefaultWarpDeformer, createGridMesh, createLayerSetMeasurer, denseCoreOf, detectAlphaBbox, forearmPoseGeometry, foregroundSpan, generateIkiFromLayerSet, headHalfOf, interpolateGridOffsets, isLipInsideRole, isSpeckCore, mouthFold, mouthOpening, mouthRestShift, packAtlas, parseLayerRoles, partIdsOfRole, styleRemovedMessage, upsertGridKeyform, uvRectFor, validateDeformerDelete, validateDeformerReparent, validatePartAttach };
1995
+ export { ALPHA_BBOX_THRESHOLD, ALPHA_OPAQUE, ATLAS_PADDING, AddDeformer, AddPart, AddPhysicsRig, type AlphaBbox, type ApplyAtlasInput, type ArmGeometry, type AtlasAssignment, type AtlasLayout, type AtlasPlacement, type AtlasSource, BakeError, type BakeReport, type BakeTarget, CaptureGridKeyform, type CustomPart, CustomPartError, DEFAULT_TURN_TARGETS, type DeformerTransformChannel, DeleteDeformer, DeletePart, DeletePhysicsRig, type EditCommand, type EditTransformChannel, EditorDocument, type ForearmPoseGeometry, HEAD_BAND, type HairSideRoom, type IrisStrand, LIP_INSIDE_ROLES, LIP_ROLES, LayerGeometryError, type LayerInput, type LayerSetMeasurement, type LayerSetMeasurer, type LipInsideRole, type LipRole, type Opening, type OpeningColumn, PatchError, type PatchReport, RIG_PLAN_FIELDS, type RgbaLayer, type RigBakes, type RigPatch, type RigPlan, RigPlanError, type RigPlanField, type RigPlanReport, SHUT_BY, SPECK_CORE_FRACTION, SetDeformerBindings, SetDeformerParent, SetDeformerPivot, SetDeformerPivotX, SetDeformerPivotY, SetDeformerTransform, SetPartBindings, SetPartColor, SetPartDeformer, SetPartHeight, SetPartMesh, SetPartOrder, SetPartTransform, SetPartWidth, SetPhysicsRig, type StrandOverlap, type TurnDepths, type TurnSolveReport, TurnTargetError, type TurnTargets, UV_INSET_PX, armGeometry, captureBindingEndpoint, columnRuns, computeGridOffsets, createDefaultMatrixDeformer, createDefaultPart, createDefaultWarpDeformer, createGridMesh, createLayerSetMeasurer, denseCoreOf, detectAlphaBbox, forearmPoseGeometry, foregroundSpan, generateIkiFromLayerSet, headHalfOf, interpolateGridOffsets, isLipInsideRole, isSpeckCore, mouthFold, mouthOpening, mouthRestShift, packAtlas, parseLayerRoles, partIdsOfRole, rigidResidual, styleRemovedMessage, upsertGridKeyform, uvRectFor, validateDeformerDelete, validateDeformerReparent, validatePartAttach };