rig-c 2.30.0 → 2.31.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/package.json +1 -1
- package/src/meshcarriers.ts +116 -42
- package/src/meshquality.ts +16 -1
- package/src/meshreduce.ts +462 -59
- package/src/meshskinning.ts +735 -44
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-c",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.31.1",
|
|
4
4
|
"description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/meshcarriers.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { MeshReductionError } from './mesh.ts';
|
|
|
17
17
|
import { areaBand, triangleAreas } from './areaband.ts';
|
|
18
18
|
|
|
19
19
|
/** `pointInTriangle`'s epsilon in `src/mesh.ts`, applied to each barycentric coordinate. */
|
|
20
|
-
const CONTAINS = 1e-9;
|
|
20
|
+
export const CONTAINS = 1e-9;
|
|
21
21
|
|
|
22
22
|
function refuse(code: string, message: string): never {
|
|
23
23
|
throw new MeshReductionError(code, message);
|
|
@@ -47,47 +47,121 @@ export function uvCarriers(uvs: readonly number[], triangles: readonly number[],
|
|
|
47
47
|
const ys = [uvs[triangles[t * 3] * 2 + 1], uvs[triangles[t * 3 + 1] * 2 + 1], uvs[triangles[t * 3 + 2] * 2 + 1]];
|
|
48
48
|
boxes.push(Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys));
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
const u = (v: number): number => uvs[v * 2];
|
|
51
|
+
const w = (v: number): number => uvs[v * 2 + 1];
|
|
52
|
+
// Issue #1295: the samples bucketed on a uniform grid, so each live triangle is tested only against the samples in
|
|
53
|
+
// the cells its box (with the containment slack) overlaps — every (sample, triangle) pair the box test could pass is
|
|
54
|
+
// still tested, by the same `carrierIn`, and each sample's hits are gathered in ascending triangle index, the order
|
|
55
|
+
// a scan of every live triangle per sample gathered them in. The carriers are that scan's, value for value; what
|
|
56
|
+
// changes is that the work no longer grows with samples × triangles.
|
|
57
|
+
const n = samples.length;
|
|
58
|
+
const hits: Array<Carrier[] | null> = new Array(n).fill(null);
|
|
59
|
+
if (n > 0 && live.length > 0) {
|
|
60
|
+
let minX = Infinity;
|
|
61
|
+
let minY = Infinity;
|
|
62
|
+
let maxX = -Infinity;
|
|
63
|
+
let maxY = -Infinity;
|
|
64
|
+
for (const s of samples) {
|
|
65
|
+
minX = Math.min(minX, s.uv[0]);
|
|
66
|
+
minY = Math.min(minY, s.uv[1]);
|
|
67
|
+
maxX = Math.max(maxX, s.uv[0]);
|
|
68
|
+
maxY = Math.max(maxY, s.uv[1]);
|
|
69
|
+
}
|
|
70
|
+
const G = Math.max(1, Math.min(1024, Math.ceil(Math.sqrt(n / 4))));
|
|
71
|
+
const cw = (maxX - minX) / G || 1;
|
|
72
|
+
const ch = (maxY - minY) / G || 1;
|
|
73
|
+
const cellX = (x: number): number => Math.min(G - 1, Math.max(0, Math.floor((x - minX) / cw)));
|
|
74
|
+
const cellY = (y: number): number => Math.min(G - 1, Math.max(0, Math.floor((y - minY) / ch)));
|
|
75
|
+
const start = new Int32Array(G * G + 1);
|
|
76
|
+
const cellOf = new Int32Array(n);
|
|
77
|
+
samples.forEach((s, j) => {
|
|
78
|
+
cellOf[j] = cellY(s.uv[1]) * G + cellX(s.uv[0]);
|
|
79
|
+
start[cellOf[j] + 1]++;
|
|
80
|
+
});
|
|
81
|
+
for (let c = 0; c < G * G; c++) start[c + 1] += start[c];
|
|
82
|
+
const order = new Int32Array(n);
|
|
83
|
+
const fill = start.slice(0, G * G);
|
|
84
|
+
for (let j = 0; j < n; j++) order[fill[cellOf[j]]++] = j;
|
|
85
|
+
const slack = 1e-9;
|
|
53
86
|
live.forEach((t, k) => {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const l2 = (bx * qy - qx * by) / det;
|
|
70
|
-
const l0 = 1 - l1 - l2;
|
|
71
|
-
if (l0 < -CONTAINS || l1 < -CONTAINS || l2 < -CONTAINS) return;
|
|
72
|
-
hits.push({ triangle: t, corners: [i0, i1, i2], bary: [l0, l1, l2] });
|
|
87
|
+
const [x0, y0, x1, y1] = [boxes[k * 4], boxes[k * 4 + 1], boxes[k * 4 + 2], boxes[k * 4 + 3]];
|
|
88
|
+
const cy0 = cellY(y0 - slack);
|
|
89
|
+
const cy1 = cellY(y1 + slack);
|
|
90
|
+
const cx0 = cellX(x0 - slack);
|
|
91
|
+
const cx1 = cellX(x1 + slack);
|
|
92
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
93
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
94
|
+
const c = cy * G + cx;
|
|
95
|
+
for (let q = start[c]; q < start[c + 1]; q++) {
|
|
96
|
+
const j = order[q];
|
|
97
|
+
const hit = carrierIn(samples[j].uv[0], samples[j].uv[1], x0, y0, x1, y1, triangles[t * 3], triangles[t * 3 + 1], triangles[t * 3 + 2], u, w, t);
|
|
98
|
+
if (hit !== null) (hits[j] ??= []).push(hit);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
73
102
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
}
|
|
104
|
+
return samples.map((s, j) => resolveCarriers(s.uv[0], s.uv[1], hits[j] ?? [], who));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether the UV triangle i0, i1, i2 (`t`, its box x0..x1, y0..y1) carries the sample at (px, py), and where: the
|
|
109
|
+
* carrier, or null. The one containment test — `uvCarriers` and the reduction's carried residual (issue #1295) both
|
|
110
|
+
* read it, so a sample is in a triangle by one arithmetic.
|
|
111
|
+
*/
|
|
112
|
+
export function carrierIn(
|
|
113
|
+
px: number,
|
|
114
|
+
py: number,
|
|
115
|
+
x0: number,
|
|
116
|
+
y0: number,
|
|
117
|
+
x1: number,
|
|
118
|
+
y1: number,
|
|
119
|
+
i0: number,
|
|
120
|
+
i1: number,
|
|
121
|
+
i2: number,
|
|
122
|
+
u: (v: number) => number,
|
|
123
|
+
v: (v: number) => number,
|
|
124
|
+
t: number,
|
|
125
|
+
): Carrier | null {
|
|
126
|
+
const slack = 1e-9;
|
|
127
|
+
if (px < x0 - slack || py < y0 - slack || px > x1 + slack || py > y1 + slack) return null;
|
|
128
|
+
const ax = u(i0);
|
|
129
|
+
const ay = v(i0);
|
|
130
|
+
const bx = u(i1) - ax;
|
|
131
|
+
const by = v(i1) - ay;
|
|
132
|
+
const cx = u(i2) - ax;
|
|
133
|
+
const cy = v(i2) - ay;
|
|
134
|
+
const qx = px - ax;
|
|
135
|
+
const qy = py - ay;
|
|
136
|
+
const det = bx * cy - cx * by;
|
|
137
|
+
const l1 = (qx * cy - cx * qy) / det;
|
|
138
|
+
const l2 = (bx * qy - qx * by) / det;
|
|
139
|
+
const l0 = 1 - l1 - l2;
|
|
140
|
+
if (l0 < -CONTAINS || l1 < -CONTAINS || l2 < -CONTAINS) return null;
|
|
141
|
+
return { triangle: t, corners: [i0, i1, i2], bary: [l0, l1, l2] };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The carrier of a sample from every triangle that contains it: none, the one, or — several — the first when they all
|
|
146
|
+
* meet there at a shared vertex or edge; else `COMPARE_UV_CARRIER_NOT_UNIQUE` (correction 4).
|
|
147
|
+
*/
|
|
148
|
+
export function resolveCarriers(px: number, py: number, hits: readonly Carrier[], who: string): Carrier | null {
|
|
149
|
+
if (hits.length === 0) return null;
|
|
150
|
+
if (hits.length === 1) return hits[0];
|
|
151
|
+
// Several carriers are one hit only across a vertex or an edge they all share: the sample's support — the corners
|
|
152
|
+
// it does not sit at zero weight on — is then the same vertex indices in each, and the carried point is theirs.
|
|
153
|
+
const support = (c: Carrier): string =>
|
|
154
|
+
c.corners
|
|
155
|
+
.filter((_v, k) => c.bary[k] > CONTAINS)
|
|
156
|
+
.sort((a, b) => a - b)
|
|
157
|
+
.join(',');
|
|
158
|
+
const first = support(hits[0]);
|
|
159
|
+
const shared = first.split(',').length <= 2 && hits.every((c) => support(c) === first);
|
|
160
|
+
if (!shared) {
|
|
161
|
+
refuse(
|
|
162
|
+
'COMPARE_UV_CARRIER_NOT_UNIQUE',
|
|
163
|
+
`${who}: the sample at uv (${px}, ${py}) lies in ${hits.length} UV triangles — ${hits.map((c) => `triangle ${c.triangle} (vertices ${c.corners.join(', ')})`).join(' and ')} — that do not meet there at a shared vertex or edge; required one carrier per sample (correction 4: overlapping or folded UV triangles would carry it by an arbitrary one)`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return hits[0];
|
|
93
167
|
}
|
package/src/meshquality.ts
CHANGED
|
@@ -71,7 +71,7 @@ import {
|
|
|
71
71
|
} from './mesh.ts';
|
|
72
72
|
import { areaBand, triangleAreas } from './areaband.ts';
|
|
73
73
|
import { allocationContrast, boundaryNecessityOnce, deformLoad, gradeMax, minAngleP10, type AllocationArt, type AllocationPlant } from './meshallocation.ts';
|
|
74
|
-
import { skinningEchoOf, skinningResidual, validateSkinning, type SkinningDetail, type SkinningEcho, type SkinningPlant, type SkinningResidualInput } from './meshskinning.ts';
|
|
74
|
+
import { skinningEchoOf, skinningResidual, validateSkinning, type ReductionSkinning, type SkinningDetail, type SkinningEcho, type SkinningPlant, type SkinningResidualInput } from './meshskinning.ts';
|
|
75
75
|
import { artRastersOf, type ArtRasters, type CoverageReading, type OutlineMemo, type RegionEdgeReading, type SilhouetteReading, type StepRasters } from './meshrasters.ts';
|
|
76
76
|
import { cropToSpineY } from './transform.ts';
|
|
77
77
|
|
|
@@ -147,6 +147,16 @@ export interface ReductionTargets {
|
|
|
147
147
|
minAngle?: number;
|
|
148
148
|
/** Local density requirements — §5. */
|
|
149
149
|
regions: RefinementRegion[];
|
|
150
|
+
/**
|
|
151
|
+
* Issue #1295 (§7 mechanism 1, the reducer half) — opt-in. Left out = no step reads a residual and the call is the
|
|
152
|
+
* one it was before the field existed, byte for byte. Set, every removal, boundary run and triangulation post-pass is
|
|
153
|
+
* taken only when `MQ_SKINNING_RESIDUAL` of its result against the call's own `source` — the original, never a
|
|
154
|
+
* previous step — is within `maxResidual` and every other required row passes; refinement insertions are not vetoed
|
|
155
|
+
* one by one, and the refined mesh is held to it before any removal. The result's measurement carries the row.
|
|
156
|
+
* `null` declares it absent (no row read by a step, the result's row `not-measurable` saying so); anything else that
|
|
157
|
+
* is not `{ envelope, maxResidual }` in full is refused `REDUCE_INPUT_MISSING` naming its path, before any work.
|
|
158
|
+
*/
|
|
159
|
+
skinning?: ReductionSkinning | null;
|
|
150
160
|
}
|
|
151
161
|
|
|
152
162
|
/** P16/P17: a local density requirement over a closed polygon. */
|
|
@@ -2359,6 +2369,11 @@ function targetsJson(t: ReductionTargets | MeasureTargets | null): Json {
|
|
|
2359
2369
|
const out: { [key: string]: Json } = { artFit: fitJson(t.artFit), maxBoundaryDeviation: t.maxBoundaryDeviation };
|
|
2360
2370
|
if (t.minAngle !== undefined) out.minAngle = t.minAngle;
|
|
2361
2371
|
out.regions = t.regions.map(regionJson);
|
|
2372
|
+
// Issue #1295: a reduction's `targets.skinning`, echoed when the input set it — `null` included — and never otherwise.
|
|
2373
|
+
if ('skinning' in t && t.skinning !== undefined) {
|
|
2374
|
+
const k = t.skinning;
|
|
2375
|
+
out.skinning = k === null ? null : { envelope: { reference: k.envelope.reference, bones: k.envelope.bones.map((b) => ({ bone: b.bone, linear: b.linear, pivot: pair(b.pivot), translation: b.translation })) }, maxResidual: k.maxResidual };
|
|
2376
|
+
}
|
|
2362
2377
|
return out;
|
|
2363
2378
|
}
|
|
2364
2379
|
|