rig-c 2.28.0 → 2.30.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/package.json +1 -1
- package/src/mesh.ts +3 -1
- package/src/meshallocation.ts +18 -4
- package/src/meshcarriers.ts +93 -0
- package/src/meshcompare.ts +60 -73
- package/src/meshquality.ts +143 -13
- package/src/meshskinning.ts +708 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-c",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.30.0",
|
|
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/mesh.ts
CHANGED
|
@@ -50,9 +50,11 @@ import type { ModelBinding, ModelVertices } from './model.ts';
|
|
|
50
50
|
// two form an import cycle: safe only because neither reads the other's
|
|
51
51
|
// bindings while it is being evaluated — every use is inside a function.
|
|
52
52
|
// The reduction (`reduceMesh`, stage B2 of #1224) sits in the same cycle, by
|
|
53
|
-
// the same rule.
|
|
53
|
+
// the same rule. So does the skinning residual and its envelope helper
|
|
54
|
+
// (`src/meshskinning.ts`, issue #1294), which the measurement reads.
|
|
54
55
|
export * from './meshquality.ts';
|
|
55
56
|
export * from './meshreduce.ts';
|
|
57
|
+
export * from './meshskinning.ts';
|
|
56
58
|
|
|
57
59
|
export interface MeshSpecInput {
|
|
58
60
|
/** Polygon in part-local pixels, y down, in manifest order. */
|
package/src/meshallocation.ts
CHANGED
|
@@ -54,6 +54,10 @@ type Weights = ReadonlyArray<ReadonlyArray<{ bone: string; weight: number }>>;
|
|
|
54
54
|
* - `bstar-ignores-art`: B\* without the art test.
|
|
55
55
|
* - `rows-required`: the rows counted as required.
|
|
56
56
|
* - `mutates-input`: the measurement sorts the caller's triangle list in place.
|
|
57
|
+
* - `gradation-defaulted`: an amplitude whose `gradation` is left out or `null` read at 0.75, §8's fixtures' value —
|
|
58
|
+
* the default issue #1291 measured and refused to choose.
|
|
59
|
+
* - `load-needs-gradation`: `MQ_DEFORM_LOAD` left unread for want of a `gradation` it does not read.
|
|
60
|
+
* - `gradation-ignored`: the need relaxed at 0.75 whatever `gradation` declares, so Δ no longer reads the field.
|
|
57
61
|
*/
|
|
58
62
|
export type AllocationPlant =
|
|
59
63
|
| 'omit-rows'
|
|
@@ -64,7 +68,10 @@ export type AllocationPlant =
|
|
|
64
68
|
| 'undeclared-pair-is-rigid'
|
|
65
69
|
| 'bstar-ignores-art'
|
|
66
70
|
| 'rows-required'
|
|
67
|
-
| 'mutates-input'
|
|
71
|
+
| 'mutates-input'
|
|
72
|
+
| 'gradation-defaulted'
|
|
73
|
+
| 'load-needs-gradation'
|
|
74
|
+
| 'gradation-ignored';
|
|
68
75
|
|
|
69
76
|
/** The art as the allocation rows read it: art pixels on the mask's grid, and the scale that grid is at. */
|
|
70
77
|
export interface AllocationArt {
|
|
@@ -493,7 +500,11 @@ export interface AllocationContrast {
|
|
|
493
500
|
worstVertex: number;
|
|
494
501
|
}
|
|
495
502
|
|
|
496
|
-
/**
|
|
503
|
+
/**
|
|
504
|
+
* h\*: the largest local size every primary need allows at a point, each relaxed at gradation G away from its source —
|
|
505
|
+
* G the caller's `motionAmplitude.gradation`, never a constant of this module: §8 measured that it derives from nothing
|
|
506
|
+
* a rig declares and that Δ moves with it (issue #1291). A region relaxes at its own declared `grade` over its band.
|
|
507
|
+
*/
|
|
497
508
|
interface Need {
|
|
498
509
|
at(p: Pt): number;
|
|
499
510
|
}
|
|
@@ -559,7 +570,8 @@ function needField(points: readonly Pt[], triangles: readonly number[], weights:
|
|
|
559
570
|
const twiceSigned = (a: Pt, b: Pt, c: Pt): number => (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
|
|
560
571
|
|
|
561
572
|
/**
|
|
562
|
-
* Δ and E (§8; the stage-A record for #1271, M7)
|
|
573
|
+
* Δ and E (§8; the stage-A record for #1271, M7), with the need relaxing at `gradation` — the caller's declared G,
|
|
574
|
+
* which `measureMeshQuality` hands over only when it is set. For each vertex the
|
|
563
575
|
* cheapest half-edge collapse into a neighbour — a hull vertex only along the
|
|
564
576
|
* hull — that turns no triangle over; ν is the largest, over what that
|
|
565
577
|
* collapse changes, of: the silhouette (the moved outline's chord distance
|
|
@@ -580,13 +592,15 @@ export function allocationContrast(
|
|
|
580
592
|
art: AllocationArt,
|
|
581
593
|
regions: readonly RefinementRegion[],
|
|
582
594
|
declared: MotionAmplitude,
|
|
595
|
+
gradation: number,
|
|
583
596
|
silhouette: number[],
|
|
584
597
|
plant: AllocationPlant | null,
|
|
585
598
|
): Reading<AllocationContrast> {
|
|
586
599
|
const n = points.length;
|
|
587
600
|
const pairs = new PairAmplitudes(declared, plant);
|
|
588
601
|
const amplitudeRead = plant !== 'contrast-without-amplitude';
|
|
589
|
-
const
|
|
602
|
+
const G = plant === 'gradation-ignored' ? 0.75 : gradation;
|
|
603
|
+
const need = needField(points, triangles, weights, silhouette, regions, amplitudeRead ? pairs : new PairAmplitudes({ tracks: [] }, 'undeclared-pair-is-rigid'), G);
|
|
590
604
|
if (need === null) return unmeasured(undeclaredPairReason(pairs.missing!));
|
|
591
605
|
const nb: Array<Set<number>> = Array.from({ length: n }, () => new Set<number>());
|
|
592
606
|
const triOf: number[][] = Array.from({ length: n }, () => []);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a UV sample lies in a mesh's UV triangulation — §3 and correction 4 of
|
|
3
|
+
* docs/MESH_REDUCTION.md, the common domain two triangulations of one
|
|
4
|
+
* attachment are compared on.
|
|
5
|
+
*
|
|
6
|
+
* `uvCarriers` was written for the motion comparison (`src/meshcompare.ts`,
|
|
7
|
+
* issue #1230), which still re-exports it from `rig-c/meshcompare` where it is
|
|
8
|
+
* promised. It lives here, in a module that links no poser, because the
|
|
9
|
+
* skinning residual (`src/meshskinning.ts`, issue #1294) reads the same carriers
|
|
10
|
+
* from `rig-c/mesh`, which is geometry-only (§0, P1): one definition of "the
|
|
11
|
+
* triangle that carries this UV" for both, so the residual and the motion row
|
|
12
|
+
* it is compared with can never disagree about which triangle a sample is in.
|
|
13
|
+
*
|
|
14
|
+
* Pure: no clock, no randomness, no runtime.
|
|
15
|
+
*/
|
|
16
|
+
import { MeshReductionError } from './mesh.ts';
|
|
17
|
+
import { areaBand, triangleAreas } from './areaband.ts';
|
|
18
|
+
|
|
19
|
+
/** `pointInTriangle`'s epsilon in `src/mesh.ts`, applied to each barycentric coordinate. */
|
|
20
|
+
const CONTAINS = 1e-9;
|
|
21
|
+
|
|
22
|
+
function refuse(code: string, message: string): never {
|
|
23
|
+
throw new MeshReductionError(code, message);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A sample's carrier: the UV triangle that contains it, its three vertex indices and the sample's barycentric coordinates there. */
|
|
27
|
+
export interface Carrier {
|
|
28
|
+
triangle: number;
|
|
29
|
+
corners: [number, number, number];
|
|
30
|
+
bary: [number, number, number];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Each sample's carrier in one mesh's UV triangulation — §3 and correction 4.
|
|
35
|
+
* Throws `COMPARE_UV_CARRIER_NOT_UNIQUE` for a sample two triangles carry
|
|
36
|
+
* other than across a vertex or edge they share.
|
|
37
|
+
*/
|
|
38
|
+
export function uvCarriers(uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string): Array<Carrier | null> {
|
|
39
|
+
const areas = triangleAreas(uvs, triangles);
|
|
40
|
+
const band = areaBand(areas, uvs);
|
|
41
|
+
const live: number[] = [];
|
|
42
|
+
const boxes: number[] = [];
|
|
43
|
+
for (let t = 0; t < areas.length; t++) {
|
|
44
|
+
if (Math.abs(areas[t]) <= band) continue;
|
|
45
|
+
live.push(t);
|
|
46
|
+
const xs = [uvs[triangles[t * 3] * 2], uvs[triangles[t * 3 + 1] * 2], uvs[triangles[t * 3 + 2] * 2]];
|
|
47
|
+
const ys = [uvs[triangles[t * 3] * 2 + 1], uvs[triangles[t * 3 + 1] * 2 + 1], uvs[triangles[t * 3 + 2] * 2 + 1]];
|
|
48
|
+
boxes.push(Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys));
|
|
49
|
+
}
|
|
50
|
+
return samples.map((s) => {
|
|
51
|
+
const [px, py] = s.uv;
|
|
52
|
+
const hits: Carrier[] = [];
|
|
53
|
+
live.forEach((t, k) => {
|
|
54
|
+
const slack = 1e-9;
|
|
55
|
+
if (px < boxes[k * 4] - slack || py < boxes[k * 4 + 1] - slack || px > boxes[k * 4 + 2] + slack || py > boxes[k * 4 + 3] + slack) return;
|
|
56
|
+
const i0 = triangles[t * 3];
|
|
57
|
+
const i1 = triangles[t * 3 + 1];
|
|
58
|
+
const i2 = triangles[t * 3 + 2];
|
|
59
|
+
const ax = uvs[i0 * 2];
|
|
60
|
+
const ay = uvs[i0 * 2 + 1];
|
|
61
|
+
const bx = uvs[i1 * 2] - ax;
|
|
62
|
+
const by = uvs[i1 * 2 + 1] - ay;
|
|
63
|
+
const cx = uvs[i2 * 2] - ax;
|
|
64
|
+
const cy = uvs[i2 * 2 + 1] - ay;
|
|
65
|
+
const qx = px - ax;
|
|
66
|
+
const qy = py - ay;
|
|
67
|
+
const det = bx * cy - cx * by;
|
|
68
|
+
const l1 = (qx * cy - cx * qy) / det;
|
|
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] });
|
|
73
|
+
});
|
|
74
|
+
if (hits.length === 0) return null;
|
|
75
|
+
if (hits.length === 1) return hits[0];
|
|
76
|
+
// Several carriers are one hit only across a vertex or an edge they all share: the sample's support — the corners
|
|
77
|
+
// it does not sit at zero weight on — is then the same vertex indices in each, and the carried point is theirs.
|
|
78
|
+
const support = (c: Carrier): string =>
|
|
79
|
+
c.corners
|
|
80
|
+
.filter((_v, k) => c.bary[k] > CONTAINS)
|
|
81
|
+
.sort((a, b) => a - b)
|
|
82
|
+
.join(',');
|
|
83
|
+
const first = support(hits[0]);
|
|
84
|
+
const shared = first.split(',').length <= 2 && hits.every((c) => support(c) === first);
|
|
85
|
+
if (!shared) {
|
|
86
|
+
refuse(
|
|
87
|
+
'COMPARE_UV_CARRIER_NOT_UNIQUE',
|
|
88
|
+
`${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)`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return hits[0];
|
|
92
|
+
});
|
|
93
|
+
}
|
package/src/meshcompare.ts
CHANGED
|
@@ -79,6 +79,7 @@ import { artOf, MeshReductionError, r6 } from './mesh.ts';
|
|
|
79
79
|
import {
|
|
80
80
|
measureMeshQuality,
|
|
81
81
|
MESH_QUALITY_REPORT_SPEC,
|
|
82
|
+
validateMotionAmplitude,
|
|
82
83
|
type ArtFitBounds,
|
|
83
84
|
type ArtInput,
|
|
84
85
|
type AttachmentRef,
|
|
@@ -89,6 +90,7 @@ import {
|
|
|
89
90
|
type MeasureRow,
|
|
90
91
|
type MeshCounts,
|
|
91
92
|
type MeshQualityReport,
|
|
93
|
+
type MotionAmplitude,
|
|
92
94
|
type MotionBounds,
|
|
93
95
|
type MotionReading,
|
|
94
96
|
type MotionRowDetail,
|
|
@@ -98,6 +100,7 @@ import {
|
|
|
98
100
|
type WorstSample,
|
|
99
101
|
} from './meshquality.ts';
|
|
100
102
|
import { areaBand, stretchSingularValues, triangleAreas } from './areaband.ts';
|
|
103
|
+
import { uvCarriers, type Carrier } from './meshcarriers.ts';
|
|
101
104
|
import { CoreInputError, readModel, underNoSkin, underSkin, type CompiledDocument } from './core/index.ts';
|
|
102
105
|
import { poseRawAnimationEach, poseRawSetup, type RawPose } from './core/raw.ts';
|
|
103
106
|
import { sampleTime } from './core/animation.ts';
|
|
@@ -151,15 +154,31 @@ export interface MotionComparisonInput {
|
|
|
151
154
|
bounds: MotionBounds;
|
|
152
155
|
motionRequired: boolean;
|
|
153
156
|
perFrame: boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Issue #1291: the motion amplitude the setup art fit's measurement reads — `MeshMeasureInput.motionAmplitude`, the
|
|
159
|
+
* same shape and the same handling, handed to that measurement of every build and to nothing else (no frame, no
|
|
160
|
+
* motion row and no acceptance reads it). Left out, it is not declared and the setup sections' `MQ_DEFORM_LOAD`
|
|
161
|
+
* and `MQ_ALLOCATION_CONTRAST` are `not-measurable` naming it, as before the field existed; `null` declares it
|
|
162
|
+
* absent; set, `MQ_DEFORM_LOAD` is measured on each build's setup section. `MQ_ALLOCATION_CONTRAST` stays
|
|
163
|
+
* `not-measurable` there either way, naming `targets.maxBoundaryDeviation`: the comparison declares no deviation
|
|
164
|
+
* bound and no reference hull. Anything that is not a `MotionAmplitude` in full is refused `COMPARE_INPUT_MISSING`
|
|
165
|
+
* before any build is read, in the measurement's words. Echoed in `effective` when set, `null` included.
|
|
166
|
+
*/
|
|
167
|
+
motionAmplitude?: MotionAmplitude | null;
|
|
154
168
|
}
|
|
155
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Issue #1291's faults in carrying `motionAmplitude` into the setup measurement, for the `mesh-compare` suite's
|
|
172
|
+
* negative controls: the amplitude not handed over; `null` handed over as the field left out; the field not validated
|
|
173
|
+
* before the builds are read; `null` echoed for a field left out; and the load counted as a required row, so the
|
|
174
|
+
* amplitude moves a verdict. `compareMeshesInMotion` plants none.
|
|
175
|
+
*/
|
|
176
|
+
export type ComparePlant = 'amplitude-not-carried' | 'null-read-as-left-out' | 'amplitude-unvalidated' | 'echo-when-unset' | 'amplitude-gates-acceptance';
|
|
177
|
+
|
|
156
178
|
// ---------------------------------------------------------------------------
|
|
157
179
|
// fixed tolerances — the tree's own
|
|
158
180
|
// ---------------------------------------------------------------------------
|
|
159
181
|
|
|
160
|
-
/** `pointInTriangle`'s epsilon in `src/mesh.ts`, applied to each barycentric coordinate. */
|
|
161
|
-
const CONTAINS = 1e-9;
|
|
162
|
-
|
|
163
182
|
/** §3 *Independent evidence* (a): the rows a reference is refused on — its art fit against its own mask, and only that. */
|
|
164
183
|
const REFERENCE_ART_FIT: readonly string[] = ['MQ_COVERAGE', 'MQ_OVERSHOOT', 'MQ_UNDERCUT'];
|
|
165
184
|
|
|
@@ -204,7 +223,7 @@ function validateFit(field: string, f: unknown): void {
|
|
|
204
223
|
* art itself (mask, threshold, frame) is `measureMeshQuality`'s to refuse, by
|
|
205
224
|
* its own codes, when the setup art fit is taken.
|
|
206
225
|
*/
|
|
207
|
-
function validateInput(input: MotionComparisonInput): void {
|
|
226
|
+
function validateInput(input: MotionComparisonInput, plant: ComparePlant | null): void {
|
|
208
227
|
if (!isObject(input)) missing('the input', input, 'a MotionComparisonInput object');
|
|
209
228
|
const builds = [input.reference, ...(Array.isArray(input.candidates) ? input.candidates : [])];
|
|
210
229
|
if (!Array.isArray(input.candidates) || input.candidates.length === 0) missing('candidates', input.candidates, 'a non-empty list of builds');
|
|
@@ -257,6 +276,16 @@ function validateInput(input: MotionComparisonInput): void {
|
|
|
257
276
|
if (b.minStretch !== undefined && (!isFiniteNumber(b.minStretch) || b.minStretch < 0)) missing('bounds.minStretch', b.minStretch, 'a finite ratio >= 0, or the field left out');
|
|
258
277
|
if (typeof input.motionRequired !== 'boolean') missing('motionRequired', input.motionRequired, 'true or false (P6)');
|
|
259
278
|
if (typeof input.perFrame !== 'boolean') missing('perFrame', input.perFrame, 'true or false (P7)');
|
|
279
|
+
if (plant !== 'amplitude-unvalidated') {
|
|
280
|
+
// Issue #1291: the measurement's own validator, so a comparison and a measurement refuse one value in the same
|
|
281
|
+
// words — under the comparison's code, and here, before any build is read.
|
|
282
|
+
try {
|
|
283
|
+
validateMotionAmplitude('the comparison', input.motionAmplitude);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
if (err instanceof MeshReductionError) refuse('COMPARE_INPUT_MISSING', err.message.slice(err.code.length + 2));
|
|
286
|
+
throw err;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
260
289
|
const s = input.schedule;
|
|
261
290
|
if (s === null) return;
|
|
262
291
|
if (!isObject(s)) missing('schedule', s, 'a MotionSchedule, or null for no motion');
|
|
@@ -554,6 +583,9 @@ function poseBuild(doc: CompiledDocument, refs: readonly AttachmentRef[], frames
|
|
|
554
583
|
// §3 — the samples and their carriers
|
|
555
584
|
// ---------------------------------------------------------------------------
|
|
556
585
|
|
|
586
|
+
/** Promised on `rig-c/meshcompare` since issue #1230; defined in `src/meshcarriers.ts` since issue #1294, which the skinning residual shares. */
|
|
587
|
+
export { uvCarriers };
|
|
588
|
+
|
|
557
589
|
interface Sample {
|
|
558
590
|
uv: [number, number];
|
|
559
591
|
/** Part-local drawing px, y down — where a region's polygon is read. */
|
|
@@ -563,12 +595,6 @@ interface Sample {
|
|
|
563
595
|
vertex: number | null;
|
|
564
596
|
}
|
|
565
597
|
|
|
566
|
-
interface Carrier {
|
|
567
|
-
triangle: number;
|
|
568
|
-
corners: [number, number, number];
|
|
569
|
-
bary: [number, number, number];
|
|
570
|
-
}
|
|
571
|
-
|
|
572
598
|
interface MeshOf {
|
|
573
599
|
uvs: number[];
|
|
574
600
|
triangles: number[];
|
|
@@ -576,68 +602,6 @@ interface MeshOf {
|
|
|
576
602
|
weights: SourceMesh['weights'];
|
|
577
603
|
}
|
|
578
604
|
|
|
579
|
-
/**
|
|
580
|
-
* Each sample's carrier in one mesh's UV triangulation — §3 and correction 4.
|
|
581
|
-
* Throws `COMPARE_UV_CARRIER_NOT_UNIQUE` for a sample two triangles carry
|
|
582
|
-
* other than across a vertex or edge they share.
|
|
583
|
-
*/
|
|
584
|
-
export function uvCarriers(uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string): Array<Carrier | null> {
|
|
585
|
-
const areas = triangleAreas(uvs, triangles);
|
|
586
|
-
const band = areaBand(areas, uvs);
|
|
587
|
-
const live: number[] = [];
|
|
588
|
-
const boxes: number[] = [];
|
|
589
|
-
for (let t = 0; t < areas.length; t++) {
|
|
590
|
-
if (Math.abs(areas[t]) <= band) continue;
|
|
591
|
-
live.push(t);
|
|
592
|
-
const xs = [uvs[triangles[t * 3] * 2], uvs[triangles[t * 3 + 1] * 2], uvs[triangles[t * 3 + 2] * 2]];
|
|
593
|
-
const ys = [uvs[triangles[t * 3] * 2 + 1], uvs[triangles[t * 3 + 1] * 2 + 1], uvs[triangles[t * 3 + 2] * 2 + 1]];
|
|
594
|
-
boxes.push(Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys));
|
|
595
|
-
}
|
|
596
|
-
return samples.map((s) => {
|
|
597
|
-
const [px, py] = s.uv;
|
|
598
|
-
const hits: Carrier[] = [];
|
|
599
|
-
live.forEach((t, k) => {
|
|
600
|
-
const slack = 1e-9;
|
|
601
|
-
if (px < boxes[k * 4] - slack || py < boxes[k * 4 + 1] - slack || px > boxes[k * 4 + 2] + slack || py > boxes[k * 4 + 3] + slack) return;
|
|
602
|
-
const i0 = triangles[t * 3];
|
|
603
|
-
const i1 = triangles[t * 3 + 1];
|
|
604
|
-
const i2 = triangles[t * 3 + 2];
|
|
605
|
-
const ax = uvs[i0 * 2];
|
|
606
|
-
const ay = uvs[i0 * 2 + 1];
|
|
607
|
-
const bx = uvs[i1 * 2] - ax;
|
|
608
|
-
const by = uvs[i1 * 2 + 1] - ay;
|
|
609
|
-
const cx = uvs[i2 * 2] - ax;
|
|
610
|
-
const cy = uvs[i2 * 2 + 1] - ay;
|
|
611
|
-
const qx = px - ax;
|
|
612
|
-
const qy = py - ay;
|
|
613
|
-
const det = bx * cy - cx * by;
|
|
614
|
-
const l1 = (qx * cy - cx * qy) / det;
|
|
615
|
-
const l2 = (bx * qy - qx * by) / det;
|
|
616
|
-
const l0 = 1 - l1 - l2;
|
|
617
|
-
if (l0 < -CONTAINS || l1 < -CONTAINS || l2 < -CONTAINS) return;
|
|
618
|
-
hits.push({ triangle: t, corners: [i0, i1, i2], bary: [l0, l1, l2] });
|
|
619
|
-
});
|
|
620
|
-
if (hits.length === 0) return null;
|
|
621
|
-
if (hits.length === 1) return hits[0];
|
|
622
|
-
// Several carriers are one hit only across a vertex or an edge they all share: the sample's support — the corners
|
|
623
|
-
// it does not sit at zero weight on — is then the same vertex indices in each, and the carried point is theirs.
|
|
624
|
-
const support = (c: Carrier): string =>
|
|
625
|
-
c.corners
|
|
626
|
-
.filter((_v, k) => c.bary[k] > CONTAINS)
|
|
627
|
-
.sort((a, b) => a - b)
|
|
628
|
-
.join(',');
|
|
629
|
-
const first = support(hits[0]);
|
|
630
|
-
const shared = first.split(',').length <= 2 && hits.every((c) => support(c) === first);
|
|
631
|
-
if (!shared) {
|
|
632
|
-
refuse(
|
|
633
|
-
'COMPARE_UV_CARRIER_NOT_UNIQUE',
|
|
634
|
-
`${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)`,
|
|
635
|
-
);
|
|
636
|
-
}
|
|
637
|
-
return hits[0];
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
|
|
641
605
|
/** The point a carrier puts the sample at over one pose's world vertices. */
|
|
642
606
|
function carried(c: Carrier, world: readonly number[]): [number, number] {
|
|
643
607
|
const [i0, i1, i2] = c.corners;
|
|
@@ -852,7 +816,24 @@ function foldingSlots(raw: unknown, who: string): Set<string> {
|
|
|
852
816
|
* undercut at the setup pose fails `referenceArtFit`, and `measureMeshQuality`'s own codes for the art).
|
|
853
817
|
*/
|
|
854
818
|
export function compareMeshesInMotion(input: MotionComparisonInput): MeshQualityReport {
|
|
855
|
-
|
|
819
|
+
return compareMeshesInMotionWith(input, null);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* `compareMeshesInMotion` with a fault planted in how it carries `motionAmplitude` (`ComparePlant`) — the
|
|
824
|
+
* `mesh-compare` suite's negative controls. Internal: merely exported, so not promised (RELEASING.md *The import
|
|
825
|
+
* surface*).
|
|
826
|
+
*/
|
|
827
|
+
export function compareMeshesInMotionPlanted(input: MotionComparisonInput, plant: ComparePlant): MeshQualityReport {
|
|
828
|
+
return compareMeshesInMotionWith(input, plant);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function compareMeshesInMotionWith(input: MotionComparisonInput, plant: ComparePlant | null): MeshQualityReport {
|
|
832
|
+
validateInput(input, plant);
|
|
833
|
+
const amplitude = input.motionAmplitude;
|
|
834
|
+
// What the setup measurement is handed: the field as the caller set it — left out as left out, `null` as `null`.
|
|
835
|
+
const handed: { motionAmplitude?: MotionAmplitude | null } =
|
|
836
|
+
plant === 'amplitude-not-carried' || amplitude === undefined || (plant === 'null-read-as-left-out' && amplitude === null) ? {} : { motionAmplitude: amplitude };
|
|
856
837
|
const refs = input.attachments.map((a) => a.attachment);
|
|
857
838
|
const reference = readBuild(input.reference, 'reference', refs);
|
|
858
839
|
const candidates = input.candidates.map((c, i) => readBuild(c, `candidates[${i}]`, refs));
|
|
@@ -895,9 +876,14 @@ export function compareMeshesInMotion(input: MotionComparisonInput): MeshQuality
|
|
|
895
876
|
influences: null,
|
|
896
877
|
boneOrder: null,
|
|
897
878
|
preset: null,
|
|
879
|
+
...handed,
|
|
898
880
|
});
|
|
899
881
|
const c = measured.candidates[0];
|
|
900
882
|
if (c.geometry === null || c.counts === null) return { geometry: null, counts: null };
|
|
883
|
+
if (plant === 'amplitude-gates-acceptance') {
|
|
884
|
+
const load = c.geometry.rows.find((r) => r.code === 'MQ_DEFORM_LOAD');
|
|
885
|
+
if (load !== undefined && load.state === 'undeclared') c.geometry.verdict = 'fail';
|
|
886
|
+
}
|
|
901
887
|
sections.push(c.geometry);
|
|
902
888
|
counts.push(c.counts);
|
|
903
889
|
}
|
|
@@ -1176,6 +1162,7 @@ export function compareMeshesInMotion(input: MotionComparisonInput): MeshQuality
|
|
|
1176
1162
|
boneOrder: null,
|
|
1177
1163
|
schedule,
|
|
1178
1164
|
budget: null,
|
|
1165
|
+
...(amplitude === undefined ? (plant === 'echo-when-unset' ? { motionAmplitude: null } : {}) : { motionAmplitude: amplitude }),
|
|
1179
1166
|
};
|
|
1180
1167
|
return {
|
|
1181
1168
|
spec: MESH_QUALITY_REPORT_SPEC,
|