rig-c 2.23.0 → 2.24.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/mesh.ts +1 -1
- package/src/meshquality.ts +39 -6
- package/src/meshreduce.ts +68 -16
- package/src/transform.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-c",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.24.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/mesh.ts
CHANGED
|
@@ -2036,7 +2036,7 @@ export function meshEdges(vertexCount: number, triangles: readonly number[], hul
|
|
|
2036
2036
|
// that used to re-implement distance weighting outside the compiler states the
|
|
2037
2037
|
// segment list and nothing else.
|
|
2038
2038
|
//
|
|
2039
|
-
// The algorithm is a port of `
|
|
2039
|
+
// The algorithm is a port of `rig-parts`' lattice mesh and segment weights
|
|
2040
2040
|
// (MIT, same owner). What it does, in order:
|
|
2041
2041
|
//
|
|
2042
2042
|
// 1. **Cells.** A square lattice of `cell`-pixel squares over the part; the
|
package/src/meshquality.ts
CHANGED
|
@@ -235,6 +235,14 @@ export interface MeshReductionInput {
|
|
|
235
235
|
deform: DeformTimelineInput[];
|
|
236
236
|
/** Every linked mesh of the source (they inherit the new topology); empty when none. */
|
|
237
237
|
linkedMeshes: AttachmentRef[];
|
|
238
|
+
/**
|
|
239
|
+
* Replay (issue #1268, §7 mechanism 2): stop after the n-th accepted step — a refinement insertion or a removal
|
|
240
|
+
* taken, counted as `ReductionChanges.acceptedAt` counts them — and return the mesh the same call without this field
|
|
241
|
+
* had at that step, byte for byte, terminated `replayed-to-accepted-step`. Left out = today's behaviour. A whole
|
|
242
|
+
* number, 0 or more: 0 takes no step. A number the run never reaches leaves the run to end as it would have, and
|
|
243
|
+
* that termination says so (`stopAfterAccepted` on it).
|
|
244
|
+
*/
|
|
245
|
+
stopAfterAccepted?: number;
|
|
238
246
|
}
|
|
239
247
|
|
|
240
248
|
/**
|
|
@@ -402,6 +410,12 @@ export interface ReductionChanges {
|
|
|
402
410
|
deformReevaluated: Array<{ animation: string; attachment: AttachmentRef; key: number }>;
|
|
403
411
|
/** Every linked mesh of the source: each inherits the new topology. */
|
|
404
412
|
linkedMeshes: AttachmentRef[];
|
|
413
|
+
/**
|
|
414
|
+
* Issue #1268: the attempt number — `candidatesTried` as it stood when the step was taken, 1-based — of every
|
|
415
|
+
* accepted step, ascending: each refinement insertion and each removal, so its length is
|
|
416
|
+
* `insertedVertices + removedVertices`. `stopAfterAccepted: k` replays to the step at `acceptedAt[k - 1]`.
|
|
417
|
+
*/
|
|
418
|
+
acceptedAt: number[];
|
|
405
419
|
}
|
|
406
420
|
|
|
407
421
|
export interface MeshQualityReport {
|
|
@@ -445,6 +459,8 @@ export interface EffectiveSettings {
|
|
|
445
459
|
boneOrder: string[] | null;
|
|
446
460
|
schedule: MotionSchedule | null;
|
|
447
461
|
budget: { maxCandidates: number } | null;
|
|
462
|
+
/** A reduction's `stopAfterAccepted`, echoed when the input set it (issue #1268); absent otherwise. */
|
|
463
|
+
stopAfterAccepted?: number;
|
|
448
464
|
}
|
|
449
465
|
|
|
450
466
|
export interface MeshCounts {
|
|
@@ -457,10 +473,20 @@ export interface MeshCounts {
|
|
|
457
473
|
maxInfluences: number;
|
|
458
474
|
}
|
|
459
475
|
|
|
476
|
+
/**
|
|
477
|
+
* Issue #1268: carried by a run's own termination when the input asked for `stopAfterAccepted` and the run ended
|
|
478
|
+
* before that many steps were accepted — `requested` the input's n, `acceptedSteps` how many the run took.
|
|
479
|
+
*/
|
|
480
|
+
export interface StopNotReached {
|
|
481
|
+
requested: number;
|
|
482
|
+
acceptedSteps: number;
|
|
483
|
+
}
|
|
484
|
+
|
|
460
485
|
/** *Termination reasons*. Every `reduce` report carries exactly one; a `measure` carries one only when it could not read the mesh. */
|
|
461
486
|
export type Termination =
|
|
462
|
-
| { reason: 'no-further-valid-reduction'; candidatesTried: number; blockingConstraint: string }
|
|
463
|
-
| { reason: 'budget-exhausted'; candidatesTried: number; budget: number; result: 'best-meeting-every-bound' | 'none-met-the-targets' }
|
|
487
|
+
| { reason: 'no-further-valid-reduction'; candidatesTried: number; blockingConstraint: string; stopAfterAccepted?: StopNotReached }
|
|
488
|
+
| { reason: 'budget-exhausted'; candidatesTried: number; budget: number; result: 'best-meeting-every-bound' | 'none-met-the-targets'; stopAfterAccepted?: StopNotReached }
|
|
489
|
+
| { reason: 'replayed-to-accepted-step'; acceptedSteps: number; candidatesTried: number }
|
|
464
490
|
| { reason: 'invalid-input'; code: string; detail: string }
|
|
465
491
|
| { reason: 'unsupported-topology'; code: string; detail: string };
|
|
466
492
|
|
|
@@ -908,7 +934,7 @@ function runsAlongOuterBoundary(a: Pt, b: Pt, poly: readonly Pt[], transition: n
|
|
|
908
934
|
}
|
|
909
935
|
|
|
910
936
|
/**
|
|
911
|
-
* [agreed,
|
|
937
|
+
* [agreed, rig-parts#126] Is the edge `a`–`b` held by `region` — does the
|
|
912
938
|
* region's density bound (`MQ_MAX_EDGE` or `MQ_TRANSITION`) apply to it? The
|
|
913
939
|
* one definition both `measureMeshQuality` and `reduceMesh`'s refinement read.
|
|
914
940
|
*
|
|
@@ -1648,7 +1674,7 @@ interface Edge {
|
|
|
1648
1674
|
*
|
|
1649
1675
|
* Which edges a region holds is `edgeIsHeldByRegion`'s answer and nothing
|
|
1650
1676
|
* else's: an edge that only touches the band's outer boundary at one point,
|
|
1651
|
-
* from outside, is not held (
|
|
1677
|
+
* from outside, is not held (rig-parts#126). One bound per edge, the smallest
|
|
1652
1678
|
* applicable anywhere on it (P16): `L0` of every region whose closed polygon it
|
|
1653
1679
|
* meets, and `L0 + grade·d` of every other region that holds it, `d` its
|
|
1654
1680
|
* distance from that region. A row's value is the edge whose
|
|
@@ -1963,6 +1989,7 @@ function effectiveJson(e: EffectiveSettings): Json {
|
|
|
1963
1989
|
boneOrder: e.boneOrder === null ? null : [...e.boneOrder],
|
|
1964
1990
|
schedule: scheduleJson(e.schedule),
|
|
1965
1991
|
budget: e.budget === null ? null : { maxCandidates: e.budget.maxCandidates },
|
|
1992
|
+
...(e.stopAfterAccepted === undefined ? {} : { stopAfterAccepted: e.stopAfterAccepted }),
|
|
1966
1993
|
};
|
|
1967
1994
|
}
|
|
1968
1995
|
|
|
@@ -2040,18 +2067,24 @@ function candidateJson(c: CandidateReport): Json {
|
|
|
2040
2067
|
deformRemapped: k.deformRemapped.map((d) => ({ animation: d.animation, attachment: attachmentJson(d.attachment), key: d.key, droppedVertices: [...d.droppedVertices] })),
|
|
2041
2068
|
deformReevaluated: k.deformReevaluated.map((d) => ({ animation: d.animation, attachment: attachmentJson(d.attachment), key: d.key })),
|
|
2042
2069
|
linkedMeshes: k.linkedMeshes.map(attachmentJson),
|
|
2070
|
+
acceptedAt: [...k.acceptedAt],
|
|
2043
2071
|
};
|
|
2044
2072
|
}
|
|
2045
2073
|
return out;
|
|
2046
2074
|
}
|
|
2047
2075
|
|
|
2076
|
+
const notReachedJson = (s: StopNotReached | undefined): { [key: string]: Json } =>
|
|
2077
|
+
s === undefined ? {} : { stopAfterAccepted: { requested: s.requested, acceptedSteps: s.acceptedSteps } };
|
|
2078
|
+
|
|
2048
2079
|
function terminationJson(t: Termination | null): Json {
|
|
2049
2080
|
if (t === null) return null;
|
|
2050
2081
|
switch (t.reason) {
|
|
2051
2082
|
case 'no-further-valid-reduction':
|
|
2052
|
-
return { reason: t.reason, candidatesTried: t.candidatesTried, blockingConstraint: t.blockingConstraint };
|
|
2083
|
+
return { reason: t.reason, candidatesTried: t.candidatesTried, blockingConstraint: t.blockingConstraint, ...notReachedJson(t.stopAfterAccepted) };
|
|
2053
2084
|
case 'budget-exhausted':
|
|
2054
|
-
return { reason: t.reason, candidatesTried: t.candidatesTried, budget: t.budget, result: t.result };
|
|
2085
|
+
return { reason: t.reason, candidatesTried: t.candidatesTried, budget: t.budget, result: t.result, ...notReachedJson(t.stopAfterAccepted) };
|
|
2086
|
+
case 'replayed-to-accepted-step':
|
|
2087
|
+
return { reason: t.reason, acceptedSteps: t.acceptedSteps, candidatesTried: t.candidatesTried };
|
|
2055
2088
|
case 'invalid-input':
|
|
2056
2089
|
case 'unsupported-topology':
|
|
2057
2090
|
return { reason: t.reason, code: t.code, detail: t.detail };
|
package/src/meshreduce.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Each is its own function with its own entry and exit, so a control can run
|
|
19
19
|
* one with the other idle — regions empty, or every source vertex protected —
|
|
20
|
-
* which is the shape the consumer asked for (
|
|
20
|
+
* which is the shape the consumer asked for (rig-parts#126, comment
|
|
21
21
|
* 6042150608: "separate composed operations … with independently passing
|
|
22
22
|
* controls for each").
|
|
23
23
|
*
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
* the edge lies outside the region and its band, the split is where the edge
|
|
54
54
|
* leaves the band, so the piece to that end only touches the band's outer
|
|
55
55
|
* boundary and `edgeIsHeldByRegion` — the one definition the rows read —
|
|
56
|
-
* exempts it (
|
|
56
|
+
* exempts it (rig-parts#126). Otherwise the split is at the midpoint when
|
|
57
57
|
* the midpoint lies in the region or its band, else at the point of the edge
|
|
58
58
|
* inside them nearest the midpoint. A region no edge meets — one
|
|
59
59
|
* inside a single triangle — gets the first vertex of its polygon inserted into
|
|
@@ -105,6 +105,7 @@ import {
|
|
|
105
105
|
type RefinementRegion,
|
|
106
106
|
type ReductionChanges,
|
|
107
107
|
type SourceMesh,
|
|
108
|
+
type StopNotReached,
|
|
108
109
|
type Termination,
|
|
109
110
|
} from './meshquality.ts';
|
|
110
111
|
import { artRastersOf, stepRastersOf, type ArtRasters, type StepRasters } from './meshrasters.ts';
|
|
@@ -260,6 +261,9 @@ function validateReduction(input: MeshReductionInput): void {
|
|
|
260
261
|
if (!isObject(b) || !Number.isInteger(b.maxCandidates) || (b.maxCandidates as number) < 0) {
|
|
261
262
|
refuse('REDUCE_INPUT_MISSING', `${who}: budget is ${JSON.stringify(b)}; required { maxCandidates: a whole number, 0 or more } — the work bound has no default`);
|
|
262
263
|
}
|
|
264
|
+
if ('stopAfterAccepted' in input && input.stopAfterAccepted !== undefined && (!Number.isInteger(input.stopAfterAccepted) || input.stopAfterAccepted < 0)) {
|
|
265
|
+
refuse('REDUCE_INPUT_MISSING', `${who}: stopAfterAccepted is ${JSON.stringify(input.stopAfterAccepted)}; required a whole number of accepted steps, 0 or more, or the field left out (issue #1268: replay to that step)`);
|
|
266
|
+
}
|
|
263
267
|
const src = input.source;
|
|
264
268
|
if (!isObject(src) || !Array.isArray(src.points)) refuse('REDUCE_INPUT_MISSING', `${who}: source is not { points, uvs, triangles, hull, weights }`);
|
|
265
269
|
const n = src.points.length;
|
|
@@ -729,6 +733,10 @@ interface Run {
|
|
|
729
733
|
sourceHull: Array<[number, number]>;
|
|
730
734
|
/** Steps tried so far, refinement insertions and removal attempts alike. */
|
|
731
735
|
steps: number;
|
|
736
|
+
/** Issue #1268: `steps` as it stood at each accepted step — every insertion made and every removal taken — in order. */
|
|
737
|
+
acceptedAt: number[];
|
|
738
|
+
/** The replay fault planted for a control, or null. */
|
|
739
|
+
plant: ReplayPlant | null;
|
|
732
740
|
tally: Tally;
|
|
733
741
|
keyed: Map<number, { animation: string; ref: AttachmentRef; key: number; time: number }>;
|
|
734
742
|
protectedVertices: Set<number>;
|
|
@@ -875,7 +883,7 @@ function offsetExit(inner: Pt, outer: Pt, poly: readonly Pt[], level: number): n
|
|
|
875
883
|
}
|
|
876
884
|
|
|
877
885
|
/**
|
|
878
|
-
* [agreed,
|
|
886
|
+
* [agreed, rig-parts#126] A split of `inner`–`outer` where it leaves
|
|
879
887
|
* `region`'s band, so that the piece to `outer` only touches the band's outer
|
|
880
888
|
* boundary and is exempt by `edgeIsHeldByRegion` — the one definition the
|
|
881
889
|
* measurement reads. The split is put half of `BAND_CONTACT_TOLERANCE` inside
|
|
@@ -959,7 +967,27 @@ function insertPoint(run: Run, p: Pt): boolean {
|
|
|
959
967
|
type PhaseEnd =
|
|
960
968
|
| { kind: 'done' }
|
|
961
969
|
| { kind: 'budget' }
|
|
962
|
-
| { kind: 'stuck'; constraint: string }
|
|
970
|
+
| { kind: 'stuck'; constraint: string }
|
|
971
|
+
| { kind: 'replayed' };
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Record an accepted step (issue #1268) and say whether the call's `stopAfterAccepted` is now reached — checked
|
|
975
|
+
* straight after the step, before the budget is read again, so the replay stops in exactly the state the
|
|
976
|
+
* unreplayed run held after that step.
|
|
977
|
+
*/
|
|
978
|
+
function accept(run: Run): boolean {
|
|
979
|
+
run.acceptedAt.push(run.steps);
|
|
980
|
+
const stop = run.input.stopAfterAccepted;
|
|
981
|
+
if (stop === undefined) return false;
|
|
982
|
+
const at = run.plant === 'stop-one-early' ? stop - 1 : run.plant === 'stop-one-late' ? stop + 1 : stop;
|
|
983
|
+
return run.acceptedAt.length >= at;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* A fault planted on purpose in the replay, for the mesh-quality suite's controls (issue #1268): the stop taken
|
|
988
|
+
* one accepted step before or after the one asked for. `reduceMesh` plants none.
|
|
989
|
+
*/
|
|
990
|
+
export type ReplayPlant = 'stop-one-early' | 'stop-one-late';
|
|
963
991
|
|
|
964
992
|
/**
|
|
965
993
|
* Refine the working mesh until every region's `MQ_MAX_EDGE` and
|
|
@@ -983,13 +1011,14 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
983
1011
|
if (target.state === 'not-measurable') {
|
|
984
1012
|
const p: Pt = [r6(region.polygon[0][0]), r6(region.polygon[0][1])];
|
|
985
1013
|
if (!insertPoint(run, p)) return { kind: 'stuck', constraint: `${rowName(target)} (the refinement found no triangle holding the region's first vertex)` };
|
|
1014
|
+
if (accept(run)) return { kind: 'replayed' };
|
|
986
1015
|
continue;
|
|
987
1016
|
}
|
|
988
1017
|
const [ra, rb] = target.worst!.at.edge!;
|
|
989
1018
|
const a = canon.order[ra];
|
|
990
1019
|
const b = canon.order[rb];
|
|
991
1020
|
// An end outside the region and its band: split where the edge leaves the band, so the piece to that
|
|
992
|
-
// end only touches the band's outer boundary and is exempt (
|
|
1021
|
+
// end only touches the band's outer boundary and is exempt (rig-parts#126) — the same predicate the
|
|
993
1022
|
// measurement reads decides it, so the next measurement agrees.
|
|
994
1023
|
let exited = false;
|
|
995
1024
|
for (const [inner, outer] of [[b, a], [a, b]] as const) {
|
|
@@ -1000,7 +1029,10 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1000
1029
|
exited = true;
|
|
1001
1030
|
break;
|
|
1002
1031
|
}
|
|
1003
|
-
if (exited)
|
|
1032
|
+
if (exited) {
|
|
1033
|
+
if (accept(run)) return { kind: 'replayed' };
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1004
1036
|
// An end outside the region and its band further beyond it than the edge's own bound, with no split on
|
|
1005
1037
|
// the band's outer boundary that frees the piece to it — always so with transition 0, where a contact
|
|
1006
1038
|
// with the authored boundary stays held: every split keeps a held edge from a vertex inside the region
|
|
@@ -1013,7 +1045,7 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1013
1045
|
const why =
|
|
1014
1046
|
region.transition > 0
|
|
1015
1047
|
? 'no point of the edge on the band\'s outer boundary leaves the piece to it exempt, so an edge from inside the region and its band to it stays over the bound'
|
|
1016
|
-
: 'with no band, an edge from the region to it touches the authored boundary and stays held (
|
|
1048
|
+
: 'with no band, an edge from the region to it touches the authored boundary and stays held (rig-parts#126), so it stays over the bound';
|
|
1017
1049
|
return {
|
|
1018
1050
|
kind: 'stuck',
|
|
1019
1051
|
constraint: `${rowName(target)} (vertex ${end} of edge ${ra}–${rb} lies ${r6(beyond)} px beyond region "${region.name}"'s ${region.transition} px band, further than the edge's bound ${target.bound!.value}: ${why}, and the refinement inserts only inside them — P16)`,
|
|
@@ -1023,6 +1055,7 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1023
1055
|
const p = splitPoint(run.work.pos[a], run.work.pos[b], region);
|
|
1024
1056
|
if (p === null) return { kind: 'stuck', constraint: `${rowName(target)} (the refinement found no point of edge ${ra}–${rb} strictly between its ends inside region "${region.name}" or its band)` };
|
|
1025
1057
|
splitEdge(run, a, b, p);
|
|
1058
|
+
if (accept(run)) return { kind: 'replayed' };
|
|
1026
1059
|
}
|
|
1027
1060
|
}
|
|
1028
1061
|
|
|
@@ -1115,8 +1148,10 @@ function removeVertices(run: Run): PhaseEnd {
|
|
|
1115
1148
|
run.steps++;
|
|
1116
1149
|
tried++;
|
|
1117
1150
|
const block = tryRemoval(run, v);
|
|
1118
|
-
if (block === null)
|
|
1119
|
-
|
|
1151
|
+
if (block === null) {
|
|
1152
|
+
taken++;
|
|
1153
|
+
if (accept(run)) return { kind: 'replayed' };
|
|
1154
|
+
} else lastBlock = `${block}, removing source vertex ${v}`;
|
|
1120
1155
|
}
|
|
1121
1156
|
if (taken === 0) {
|
|
1122
1157
|
if (tried === 0) lastBlock = 'protect: every surviving source vertex is protected (protect.hull, protect.vertices, protect.edges, protect.regionBoundaries or a weightJump edge)';
|
|
@@ -1193,6 +1228,11 @@ function tryRemoval(run: Run, v: number): string | null {
|
|
|
1193
1228
|
* returned when it meets every required bound, and none is when it does not;
|
|
1194
1229
|
* - `no-further-valid-reduction`: a pass took no step, naming what blocked the
|
|
1195
1230
|
* last one. A local stop: nothing here says the result is the smallest.
|
|
1231
|
+
* - `replayed-to-accepted-step`: the input's `stopAfterAccepted` was reached —
|
|
1232
|
+
* the mesh is the one the same call without it held after that many accepted
|
|
1233
|
+
* steps, byte for byte (issue #1268). Never reported as an exhausted budget; a
|
|
1234
|
+
* stop the run does not reach leaves the run's own termination, which then
|
|
1235
|
+
* carries `stopAfterAccepted: { requested, acceptedSteps }`.
|
|
1196
1236
|
*/
|
|
1197
1237
|
export function reduceMesh(input: MeshReductionInput): MeshReductionResult {
|
|
1198
1238
|
validateReduction(input);
|
|
@@ -1215,19 +1255,19 @@ export function reduceMesh(input: MeshReductionInput): MeshReductionResult {
|
|
|
1215
1255
|
* module with `export *`, and a symbol that is merely exported is not promised
|
|
1216
1256
|
* (RELEASING.md, *The import surface*).
|
|
1217
1257
|
*/
|
|
1218
|
-
export function reduceMeshWith(input: MeshReductionInput, rasters: ArtRasters, steps: StepRasters | null = stepRastersOf(rasters)): MeshReductionResult {
|
|
1258
|
+
export function reduceMeshWith(input: MeshReductionInput, rasters: ArtRasters, steps: StepRasters | null = stepRastersOf(rasters), plant: ReplayPlant | null = null): MeshReductionResult {
|
|
1219
1259
|
validateReduction(input);
|
|
1220
1260
|
if (steps !== null && steps.rasters !== rasters) {
|
|
1221
1261
|
refuse('REDUCE_ART_RASTERS_MISMATCH', `attachment ${nameOf(input.attachment)}: the step rasters were made over another rasters object; required step rasters made over the rasters passed beside them (stepRastersOf(rasters))`);
|
|
1222
1262
|
}
|
|
1223
|
-
return reduceValidated(input, rasters, steps);
|
|
1263
|
+
return reduceValidated(input, rasters, steps, plant);
|
|
1224
1264
|
}
|
|
1225
1265
|
|
|
1226
1266
|
/**
|
|
1227
1267
|
* The operation, over an input `validateReduction` accepted; the art's rasters are computed at most once, in
|
|
1228
1268
|
* `rasters`, and every refinement and removal step is measured through `steps` when it is given (issue #1246).
|
|
1229
1269
|
*/
|
|
1230
|
-
function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps: StepRasters | null): MeshReductionResult {
|
|
1270
|
+
function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps: StepRasters | null, plant: ReplayPlant | null = null): MeshReductionResult {
|
|
1231
1271
|
const who = `attachment ${nameOf(input.attachment)}`;
|
|
1232
1272
|
const src = input.source;
|
|
1233
1273
|
const sourceHull: Array<[number, number]> = src.points.slice(0, Math.max(0, src.hull)).map(([x, y]): [number, number] => [x, y]);
|
|
@@ -1302,6 +1342,8 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1302
1342
|
sourceTurn: Math.sign(signedArea(sourceHull)),
|
|
1303
1343
|
sourceHull,
|
|
1304
1344
|
steps: 0,
|
|
1345
|
+
acceptedAt: [],
|
|
1346
|
+
plant,
|
|
1305
1347
|
tally: { sharesDroppedOnGrid: 0, sharesPruned: 0 },
|
|
1306
1348
|
keyed: keyedSourceVertices(input),
|
|
1307
1349
|
...protectionOf(input, work, sourceEdges),
|
|
@@ -1310,20 +1352,28 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1310
1352
|
stepRasters: steps,
|
|
1311
1353
|
};
|
|
1312
1354
|
|
|
1355
|
+
// Issue #1268: a replay ends in its own termination, never as an exhausted budget; a stop the run never reaches
|
|
1356
|
+
// leaves the run's own termination, which then carries `stopAfterAccepted` saying so.
|
|
1357
|
+
const replayed = (): Termination => ({ reason: 'replayed-to-accepted-step', acceptedSteps: run.acceptedAt.length, candidatesTried: run.steps });
|
|
1358
|
+
const stop = input.stopAfterAccepted;
|
|
1359
|
+
const notReached = (): { stopAfterAccepted?: StopNotReached } => (stop === undefined ? {} : { stopAfterAccepted: { requested: stop, acceptedSteps: run.acceptedAt.length } });
|
|
1360
|
+
if (stop === 0) return finish(run, effective, sourceCounts, replayed());
|
|
1313
1361
|
const refined = refineRegions(run);
|
|
1314
|
-
if (refined.kind === '
|
|
1362
|
+
if (refined.kind === 'replayed') return finish(run, effective, sourceCounts, replayed());
|
|
1363
|
+
if (refined.kind === 'budget') return noMesh({ reason: 'budget-exhausted', candidatesTried: run.steps, budget: input.budget.maxCandidates, result: 'none-met-the-targets', ...notReached() });
|
|
1315
1364
|
const startCanon = canonicalise(work, run.sourceTurn, boneRank);
|
|
1316
1365
|
const startBlock = firstBlockingRow(measureAgainstTargets(run, startCanon.mesh, 'start'), input.targets.artFit);
|
|
1317
1366
|
let termination: Termination;
|
|
1318
1367
|
if (startBlock !== null) {
|
|
1319
1368
|
const constraint = refined.kind === 'stuck' ? refined.constraint : rowName(startBlock);
|
|
1320
|
-
termination = { reason: 'no-further-valid-reduction', candidatesTried: run.steps, blockingConstraint: `${constraint} — before any removal: the refined source does not meet its targets, so no step from it can
|
|
1369
|
+
termination = { reason: 'no-further-valid-reduction', candidatesTried: run.steps, blockingConstraint: `${constraint} — before any removal: the refined source does not meet its targets, so no step from it can`, ...notReached() };
|
|
1321
1370
|
} else {
|
|
1322
1371
|
const reduced = removeVertices(run);
|
|
1372
|
+
if (reduced.kind === 'replayed') return finish(run, effective, sourceCounts, replayed());
|
|
1323
1373
|
termination =
|
|
1324
1374
|
reduced.kind === 'budget'
|
|
1325
|
-
? { reason: 'budget-exhausted', candidatesTried: run.steps, budget: input.budget.maxCandidates, result: 'best-meeting-every-bound' }
|
|
1326
|
-
: { reason: 'no-further-valid-reduction', candidatesTried: run.steps, blockingConstraint: reduced.kind === 'stuck' ? reduced.constraint : '' };
|
|
1375
|
+
? { reason: 'budget-exhausted', candidatesTried: run.steps, budget: input.budget.maxCandidates, result: 'best-meeting-every-bound', ...notReached() }
|
|
1376
|
+
: { reason: 'no-further-valid-reduction', candidatesTried: run.steps, blockingConstraint: reduced.kind === 'stuck' ? reduced.constraint : '', ...notReached() };
|
|
1327
1377
|
}
|
|
1328
1378
|
return finish(run, effective, sourceCounts, termination);
|
|
1329
1379
|
} catch (err) {
|
|
@@ -1403,6 +1453,7 @@ function finish(run: Run, effective: EffectiveSettings, sourceCounts: MeshCounts
|
|
|
1403
1453
|
deformRemapped: deform.remapped.map((d) => ({ animation: d.animation, attachment: d.attachment, key: d.key, droppedVertices: d.droppedVertices })),
|
|
1404
1454
|
deformReevaluated: deform.reevaluated.map((d) => ({ animation: d.animation, attachment: d.attachment, key: d.key })),
|
|
1405
1455
|
linkedMeshes: input.linkedMeshes,
|
|
1456
|
+
acceptedAt: [...run.acceptedAt],
|
|
1406
1457
|
};
|
|
1407
1458
|
const counts = candidate.counts!;
|
|
1408
1459
|
const mesh: ReducedMesh = {
|
|
@@ -1440,5 +1491,6 @@ function effectiveOf(input: MeshReductionInput, measured: EffectiveSettings, sou
|
|
|
1440
1491
|
targets: input.targets,
|
|
1441
1492
|
referenceHull: sourceHull,
|
|
1442
1493
|
budget: { maxCandidates: input.budget.maxCandidates },
|
|
1494
|
+
...(input.stopAfterAccepted === undefined ? {} : { stopAfterAccepted: input.stopAfterAccepted }),
|
|
1443
1495
|
};
|
|
1444
1496
|
}
|
package/src/transform.ts
CHANGED
|
@@ -68,7 +68,7 @@ import { modeMatrix, RUNTIME_ARITHMETIC, worldTransforms, type CoreWorld, type W
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* What `computeWorldTransforms` reads off a bone — a structural type, so both a
|
|
71
|
-
* Spine bone (`SpineBone`, what
|
|
71
|
+
* Spine bone (`SpineBone`, what rig-parts and every skeleton reader hold) and
|
|
72
72
|
* a compiled-model bone (`ModelBone`, what `compile` holds) satisfy it as they
|
|
73
73
|
* are, without a cast.
|
|
74
74
|
*
|