@rig-c/parts 0.0.0-stage → 1.5.4

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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +134 -0
  3. package/README.md +433 -2
  4. package/bin/rig-parts.cjs +43 -0
  5. package/cli.ts +1240 -0
  6. package/docs/AUTHORING.md +2387 -0
  7. package/docs/MIGRATION.md +150 -0
  8. package/docs/STABILITY.md +262 -0
  9. package/package.json +70 -4
  10. package/skills/rig-parts/SKILL.md +167 -0
  11. package/src/apng.ts +381 -0
  12. package/src/assemble.ts +2259 -0
  13. package/src/autoamplitude.ts +194 -0
  14. package/src/autoenvelope.ts +404 -0
  15. package/src/automesh.ts +585 -0
  16. package/src/automotion.ts +447 -0
  17. package/src/autoreplay.ts +516 -0
  18. package/src/build.ts +1240 -0
  19. package/src/check.ts +2942 -0
  20. package/src/comfy/client.ts +245 -0
  21. package/src/comfy/index.ts +19 -0
  22. package/src/comfy/painting.ts +136 -0
  23. package/src/comfy/seethrough.ts +171 -0
  24. package/src/config.ts +2283 -0
  25. package/src/contour.ts +1901 -0
  26. package/src/coords.ts +26 -0
  27. package/src/diagnostics.ts +198 -0
  28. package/src/errors.ts +39 -0
  29. package/src/gif.ts +213 -0
  30. package/src/graphs.ts +302 -0
  31. package/src/headbox.ts +173 -0
  32. package/src/heat.ts +313 -0
  33. package/src/inputs.ts +100 -0
  34. package/src/instruments.ts +1145 -0
  35. package/src/keypoints.ts +296 -0
  36. package/src/layers.ts +598 -0
  37. package/src/localweights.ts +217 -0
  38. package/src/mesh.ts +362 -0
  39. package/src/motion.ts +311 -0
  40. package/src/palette.ts +208 -0
  41. package/src/parts.ts +410 -0
  42. package/src/propose.ts +2540 -0
  43. package/src/pyfmt.ts +135 -0
  44. package/src/raster/blur.ts +80 -0
  45. package/src/raster/components.ts +165 -0
  46. package/src/raster/composite.ts +81 -0
  47. package/src/raster/index.ts +9 -0
  48. package/src/raster/morph.ts +116 -0
  49. package/src/raster/png.ts +82 -0
  50. package/src/raster/poly.ts +166 -0
  51. package/src/raster/resize.ts +301 -0
  52. package/src/raster/types.ts +89 -0
  53. package/src/raster/warp.ts +121 -0
  54. package/src/requirements.ts +1240 -0
  55. package/src/rig.ts +1440 -0
  56. package/src/round.ts +62 -0
  57. package/src/scene.ts +1414 -0
  58. package/src/sheet.ts +128 -0
  59. package/src/skeleton.ts +400 -0
  60. package/src/structure.ts +858 -0
  61. package/src/tags.ts +121 -0
  62. package/src/weights.ts +124 -0
  63. package/workflows/seethrough.json +68 -0
package/src/tags.ts ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * See-through's layer vocabulary, as the tags a layer arrives named by.
3
+ *
4
+ * ⭐ These are the **v3** tags — the vocabulary `pipeline.unet.get_tag_version()`
5
+ * reports on the `seethroughv0.0.2_layerdiff3d` weights, read out of the
6
+ * upstream code: 13 body tags and 11 head tags, 24 in all. The older v2
7
+ * vocabulary has 19 tags and a single `hair` tag; it is not what the published
8
+ * v0.0.2 weights emit, and a layer named by it is refused as unknown rather
9
+ * than mapped, because a mapping would be a guess about which v3 tag it meant.
10
+ *
11
+ * Six tags are split left/right by upstream (`_tag_lr_split`, which needs the
12
+ * tag's mask to be two separate blobs): `handwear`, `eyewhite`, `irides`,
13
+ * `eyelash`, `eyebrow`, `ears`. A split layer is named `<tag>-r` / `<tag>-l`.
14
+ * **[observed]** on wrapper output: `-r` is the half at the smaller image x —
15
+ * the character's right as they face the viewer — and the wrapper emits both
16
+ * halves even when one is a few pixels (a 6x6 `handwear-l` beside a 275x352
17
+ * `handwear-r`), so "both halves present" says nothing about the split having
18
+ * found two real blobs.
19
+ *
20
+ * `head` is in the v3 list but is not emitted as a layer: upstream's README
21
+ * says the pipeline "stratifies the character into up to 23 semantic layers"
22
+ * (24 tags less `head`, whose pass arrives as its parts), and **[observed]**
23
+ * no wrapper output read so far carries one. It stays in the vocabulary
24
+ * because it is upstream's word.
25
+ *
26
+ * Nothing downstream classifies a part by its NAME. The role of a part — what
27
+ * a proposer may do with it — is read from the tag it came from, which is why
28
+ * `parts.json` records `from` for every part.
29
+ */
30
+
31
+ export const BODY_TAGS = [
32
+ 'front hair',
33
+ 'back hair',
34
+ 'head',
35
+ 'neck',
36
+ 'neckwear',
37
+ 'topwear',
38
+ 'handwear',
39
+ 'bottomwear',
40
+ 'legwear',
41
+ 'footwear',
42
+ 'tail',
43
+ 'wings',
44
+ 'objects',
45
+ ] as const;
46
+
47
+ export const HEAD_TAGS = [
48
+ 'headwear',
49
+ 'face',
50
+ 'irides',
51
+ 'eyebrow',
52
+ 'eyewhite',
53
+ 'eyelash',
54
+ 'eyewear',
55
+ 'ears',
56
+ 'earwear',
57
+ 'nose',
58
+ 'mouth',
59
+ ] as const;
60
+
61
+ export type BaseTag = (typeof BODY_TAGS)[number] | (typeof HEAD_TAGS)[number];
62
+
63
+ /** The tags upstream splits into `-r` / `-l` halves. */
64
+ export const SPLIT_TAGS = ['handwear', 'eyewhite', 'irides', 'eyelash', 'eyebrow', 'ears'] as const;
65
+
66
+ export type SplitTag = (typeof SPLIT_TAGS)[number];
67
+ export type Side = 'r' | 'l';
68
+
69
+ /** Every v3 tag, in the order upstream lists them. */
70
+ export const V3_TAGS: readonly BaseTag[] = [...BODY_TAGS, ...HEAD_TAGS];
71
+
72
+ export interface TagReading {
73
+ /** The name as the layer carries it: `eyebrow-l`, `front hair`. */
74
+ name: string;
75
+ base: BaseTag;
76
+ /** `null` for an unsplit tag or an unsplit form of a split tag. */
77
+ side: Side | null;
78
+ group: 'body' | 'head';
79
+ }
80
+
81
+ /**
82
+ * Read a layer name as a v3 tag, or `null` when it is not one.
83
+ *
84
+ * Accepted: any of the 24 base tags, and `<tag>-r` / `<tag>-l` for the six
85
+ * split tags. A suffix on a tag upstream never splits (`topwear-l`) is not a
86
+ * tag — upstream cannot have produced it, so accepting it would be accepting a
87
+ * layer somebody renamed.
88
+ */
89
+ export function readTag(name: string): TagReading | null {
90
+ const split = /^(.+)-([rl])$/.exec(name);
91
+ if (split !== null && (SPLIT_TAGS as readonly string[]).includes(split[1])) {
92
+ const base = split[1] as SplitTag;
93
+ return { name, base, side: split[2] as Side, group: groupOf(base) };
94
+ }
95
+ if ((V3_TAGS as readonly string[]).includes(name)) {
96
+ const base = name as BaseTag;
97
+ return { name, base, side: null, group: groupOf(base) };
98
+ }
99
+ return null;
100
+ }
101
+
102
+ function groupOf(tag: BaseTag): 'body' | 'head' {
103
+ return (HEAD_TAGS as readonly string[]).includes(tag) ? 'head' : 'body';
104
+ }
105
+
106
+ /** Every name `readTag` accepts, sorted — what a refusal quotes as "required". */
107
+ export function acceptedTagNames(): string[] {
108
+ const names = new Set<string>(V3_TAGS);
109
+ for (const t of SPLIT_TAGS) {
110
+ names.add(`${t}-r`);
111
+ names.add(`${t}-l`);
112
+ }
113
+ return [...names].sort();
114
+ }
115
+
116
+ /** The mirror of a split layer name (`eyebrow-l` <-> `eyebrow-r`), or `null` for an unsided one. */
117
+ export function pairOf(name: string): string | null {
118
+ const t = readTag(name);
119
+ if (t === null || t.side === null) return null;
120
+ return `${t.base}-${t.side === 'r' ? 'l' : 'r'}`;
121
+ }
package/src/weights.ts ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Vertex weights by distance to bone SEGMENTS.
3
+ *
4
+ * Each mesh names its candidate segments in the config — a chain (every link,
5
+ * origin to the next link, the last to the chain's tip), a single bone (origin
6
+ * to its tip), or an explicit `[bone, [x0, y0], [x1, y1]]`. That list is the
7
+ * one authored decision about a layer: which bones may pull it. The weights
8
+ * then follow from the geometry and nothing else:
9
+ *
10
+ * w = 1 / (d + r)^e d = distance from the vertex to the segment
11
+ *
12
+ * per candidate, with `e` the mesh's declared `exponent` (issue #161) and 2 —
13
+ * {@link DISTANCE_EXPONENT}, the rule as it always was — where none is
14
+ * declared; a bone named by two segments keeps the larger; the top
15
+ * {@link MAX_INFLUENCES} are kept, normalised, any below {@link MIN_WEIGHT}
16
+ * dropped, the rest normalised again, rounded to 5 decimals, and the last
17
+ * weight written as `1 - sum(others)` so every vertex sums to exactly 1 after
18
+ * rounding. Ties in the sort keep the order the bones first appeared among the
19
+ * segments (Python's `sorted` is stable, and so is `Array.prototype.sort`).
20
+ *
21
+ * Distances are the reference's arithmetic, operation for operation — the
22
+ * projection `t = clip(((p - a) . ab) / |ab|^2, 0, 1)` and `sqrt(dx^2 + dy^2)`,
23
+ * not `Math.hypot`, whose last bit differs — because a weight rounded at the
24
+ * fifth decimal is where a one-ulp difference becomes a visible one.
25
+ *
26
+ * ⛔ **No invented segment.** The reference gave a single bone with no tip and
27
+ * no chain child the segment `(x, y) -> (x, y + 1)`: one pixel, straight down,
28
+ * a direction nothing in the config says. Here that is `RIG_SEGMENT_DEFINED`,
29
+ * naming the bone and the two fields that would define it. A chain link whose
30
+ * next point is its own origin is refused the same way (`RIG_CHAIN_POINTS`).
31
+ */
32
+ import type { Point } from './config.ts';
33
+
34
+ /** At most this many bones pull one vertex — Spine's usual budget and the reference's `[:4]`. */
35
+ export const MAX_INFLUENCES = 4;
36
+
37
+ /** A normalised weight under this is dropped before the final normalisation — the reference's `>= 0.03`. */
38
+ export const MIN_WEIGHT = 0.03;
39
+
40
+ /**
41
+ * The exponent of the distance rule where a mesh declares none: `1/(d + r)²`,
42
+ * the reference's rule. At this value the weight is computed as
43
+ * `(d + r) * (d + r)`, the reference's operations, and never through
44
+ * `Math.pow`, which the language does not promise to round as one multiply
45
+ * does — so a mesh that declares nothing, or declares 2, writes the bytes it
46
+ * always wrote.
47
+ */
48
+ export const DISTANCE_EXPONENT = 2;
49
+
50
+ export interface Segment {
51
+ bone: string;
52
+ a: Point;
53
+ b: Point;
54
+ }
55
+
56
+ /** Distance from `p` to the segment `a -> b`; a zero-length segment is its point. */
57
+ export function segmentDistance(p: Point, a: Point, b: Point): number {
58
+ const abx = b[0] - a[0];
59
+ const aby = b[1] - a[1];
60
+ const len2 = abx * abx + aby * aby;
61
+ let t = 0;
62
+ if (len2 > 0) {
63
+ t = ((p[0] - a[0]) * abx + (p[1] - a[1]) * aby) / len2;
64
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
65
+ }
66
+ const dx = p[0] - (a[0] + t * abx);
67
+ const dy = p[1] - (a[1] + t * aby);
68
+ return Math.sqrt(dx * dx + dy * dy);
69
+ }
70
+
71
+ export interface Influence {
72
+ bone: string;
73
+ /** Unrounded, normalised over the kept influences. */
74
+ weight: number;
75
+ }
76
+
77
+ /**
78
+ * The cap and the floor `influences()` applies. The lattice and the contour
79
+ * mode always run at {@link DEFAULT_LIMITS} (the reference's `[:4]` and
80
+ * `>= 0.03`); the automatic mode (`src/automesh.ts`, issue #126) passes the
81
+ * author's numbers instead, because rig-c's reduction is handed explicit
82
+ * influence limits on every weighted call and the 0.03 floor is not one the
83
+ * author wrote (P19). The arithmetic is the same either way, so with the
84
+ * defaults every byte the two older modes write is unchanged.
85
+ */
86
+ export interface InfluenceLimits {
87
+ maxInfluences: number;
88
+ minWeight: number;
89
+ }
90
+
91
+ export const DEFAULT_LIMITS: InfluenceLimits = { maxInfluences: MAX_INFLUENCES, minWeight: MIN_WEIGHT };
92
+
93
+ /**
94
+ * The influences on one vertex at `p` (rig pixels), before rounding, under
95
+ * `w = 1/(d + r)^exponent`. The rig stage passes every mesh's exponent
96
+ * explicitly; the default is for readers of today's rule (the tools, the
97
+ * selftest's older controls).
98
+ */
99
+ export function influences(p: Point, segments: readonly Segment[], r: number, limits: InfluenceLimits = DEFAULT_LIMITS, exponent: number = DISTANCE_EXPONENT): Influence[] {
100
+ const agg = new Map<string, number>();
101
+ for (const s of segments) {
102
+ const d = segmentDistance(p, s.a, s.b);
103
+ // 2 is the reference's multiply, bit for bit (DISTANCE_EXPONENT); any other declared exponent is Math.pow.
104
+ const w = 1.0 / (exponent === DISTANCE_EXPONENT ? (d + r) * (d + r) : Math.pow(d + r, exponent));
105
+ const was = agg.get(s.bone);
106
+ agg.set(s.bone, was === undefined ? w : Math.max(was, w));
107
+ }
108
+ return capAndFloor([...agg.entries()], limits);
109
+ }
110
+
111
+ /**
112
+ * The cap and the floor, on raw per-bone values in first-appearance order: sorted (stable), the top
113
+ * `maxInfluences` normalised, those under `minWeight` dropped, the rest normalised again. The distance rule
114
+ * above and bone heat (`src/heat.ts`, issue #161) both end here, so the two rules share one cap and one floor.
115
+ */
116
+ export function capAndFloor(raw: ReadonlyArray<readonly [string, number]>, limits: InfluenceLimits): Influence[] {
117
+ const top = [...raw].sort((x, y) => y[1] - x[1]).slice(0, limits.maxInfluences);
118
+ let s = 0;
119
+ for (const [, v] of top) s += v;
120
+ const kept = top.map(([bone, v]) => ({ bone, weight: v / s })).filter((e) => e.weight >= limits.minWeight);
121
+ let s2 = 0;
122
+ for (const e of kept) s2 += e.weight;
123
+ return kept.map((e) => ({ bone: e.bone, weight: e.weight / s2 }));
124
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "_comment": "API-format graph for jtydhr88/ComfyUI-See-through (its node classes and inputs as a box running the wrapper lists them on /object_info; the adapter checks every node and input against the box's own /object_info before it submits). Every %NAME% string is filled by src/graphs.ts fillSeeThrough, and a placeholder nothing fills is refused before submission. The LoadImage 'upload' widget of the UI format is left out: it is not an input /object_info lists. Top-level keys starting with _ are annotations and are dropped before /prompt. SeeThrough_SavePSD writes one PNG per layer and a <prefix>_<timestamp>_layers.json manifest into the box's output directory, and records that manifest's file name in output/seethrough_psd_info.log; the two SaveImage nodes are previews and are what /history lists.",
3
+ "1": {
4
+ "class_type": "LoadImage",
5
+ "inputs": { "image": "%IMAGE%" }
6
+ },
7
+ "2": {
8
+ "class_type": "SeeThrough_LoadLayerDiffModel",
9
+ "inputs": {
10
+ "model": "layerdifforg/seethroughv0.0.2_layerdiff3d",
11
+ "vae_ckpt": "",
12
+ "unet_ckpt": "",
13
+ "quant_mode": "%QUANT%",
14
+ "cache_tag_embeds": true,
15
+ "group_offload": "%OFFLOAD%",
16
+ "auto_download": true
17
+ }
18
+ },
19
+ "3": {
20
+ "class_type": "SeeThrough_GenerateLayers",
21
+ "inputs": {
22
+ "image": ["1", 0],
23
+ "layerdiff_model": ["2", 0],
24
+ "seed": "%SEED%",
25
+ "resolution": "%RESOLUTION%",
26
+ "num_inference_steps": "%STEPS%"
27
+ }
28
+ },
29
+ "4": {
30
+ "class_type": "SeeThrough_LoadDepthModel",
31
+ "inputs": {
32
+ "model": "layerdifforg/seethroughv0.0.1_marigold",
33
+ "quant_mode": "%QUANT%",
34
+ "cache_tag_embeds": true,
35
+ "group_offload": "%OFFLOAD%",
36
+ "auto_download": true
37
+ }
38
+ },
39
+ "5": {
40
+ "class_type": "SeeThrough_GenerateDepth",
41
+ "inputs": {
42
+ "layers": ["3", 0],
43
+ "depth_model": ["4", 0],
44
+ "seed": "%SEED%",
45
+ "resolution_depth": -1
46
+ }
47
+ },
48
+ "6": {
49
+ "class_type": "SeeThrough_PostProcess",
50
+ "inputs": {
51
+ "layers_depth": ["5", 0],
52
+ "tblr_split": true,
53
+ "use_lama": "%LAMA%"
54
+ }
55
+ },
56
+ "7": {
57
+ "class_type": "SeeThrough_SavePSD",
58
+ "inputs": { "parts": ["6", 0], "filename_prefix": "%PREFIX%" }
59
+ },
60
+ "8": {
61
+ "class_type": "SaveImage",
62
+ "inputs": { "images": ["3", 1], "filename_prefix": "%PREFIX%_preview_layers" }
63
+ },
64
+ "9": {
65
+ "class_type": "SaveImage",
66
+ "inputs": { "images": ["6", 1], "filename_prefix": "%PREFIX%_preview_parts" }
67
+ }
68
+ }