spine-rigc 2.0.3 → 2.1.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/NOTICE.md +2 -2
- package/README.md +6 -6
- package/docs/AUTHORING.md +95 -14
- package/docs/FACE.md +1 -1
- package/docs/INGEST.md +6 -6
- package/package.json +1 -1
- package/src/assertions/bodies/a06.ts +19 -53
- package/src/assertions/bodies/a49.ts +81 -0
- package/src/assertions/emitted/index.ts +1 -1
- package/src/assertions/footprints.ts +367 -0
- package/src/assertions/kinds.ts +6 -3
- package/src/assertions/model/index.ts +3 -1
- package/src/assertions/reasons.ts +2 -0
- package/src/atlas.ts +882 -12
- package/src/cli/shared.ts +55 -22
- package/src/validate.ts +8 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each region of an atlas draws, in page texels, and whether two of those
|
|
3
|
+
* drawings overlap — the reading `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` makes
|
|
4
|
+
* (issue #1099, the gate half of `--pack-shape polygon`).
|
|
5
|
+
*
|
|
6
|
+
* ## The footprint
|
|
7
|
+
*
|
|
8
|
+
* A region's footprint is the part of its page some attachment samples:
|
|
9
|
+
*
|
|
10
|
+
* * a region a **mesh** names (its `path`, else its name, and every frame of
|
|
11
|
+
* its `sequence` — the lookups `../region_lookups.ts` makes, the loader's
|
|
12
|
+
* own) draws the mesh's hull loop — its first `hullLength / 2` UVs, closed
|
|
13
|
+
* last to first — and each of its triangles; a **linked mesh** draws its
|
|
14
|
+
* source's, which is what `MeshEntry.regionUVs` and `triangles` already
|
|
15
|
+
* hold for one. A UV `(u, v)` lands on the page where
|
|
16
|
+
* `MeshAttachment.computeUVs` puts it, times the page size: the default
|
|
17
|
+
* case of `computeUvs` in `src/core/uvs.ts`, which the core suite holds to
|
|
18
|
+
* the runtime — `x = region.x − offsetX + u · originalWidth`,
|
|
19
|
+
* `y = region.y − (originalHeight − offsetY − height) + v · originalHeight`;
|
|
20
|
+
* * a region a **region attachment** names draws its whole rectangle, and so
|
|
21
|
+
* does a region **nothing** names (a frame no attachment reaches, the second
|
|
22
|
+
* of two regions with one name — the runtime's lookup returns the first);
|
|
23
|
+
* * a region is its rectangle, too, wherever the mesh cannot be read as a
|
|
24
|
+
* polygon over it: a mesh whose UVs leave 0..1, a hull or triangle list
|
|
25
|
+
* that does not index its vertices, a region turned by `rotate:` (the hull
|
|
26
|
+
* is not turned here; A06 refuses rotation under the profile this runs
|
|
27
|
+
* in), and every region at all when a skin entry's `sequence` is one the
|
|
28
|
+
* lookup walk will not guess at — then which regions a mesh reaches is not
|
|
29
|
+
* known, and the rectangle is the reading that assumes nothing.
|
|
30
|
+
*
|
|
31
|
+
* Those fallbacks are the packer's (`packFootprints` in `src/atlas.ts`), and
|
|
32
|
+
* every one of them is the rectangle — the footprint A06's tiling clause gave
|
|
33
|
+
* every region before this reading existed.
|
|
34
|
+
*
|
|
35
|
+
* ## The overlap, and why it is exactly A06's statement for rectangles
|
|
36
|
+
*
|
|
37
|
+
* Two regions on one page are refused when their rectangles' interiors overlap
|
|
38
|
+
* **and** their footprints' interiors overlap. Touching is not overlapping, as
|
|
39
|
+
* A06 never refused two rectangles that share an edge. A hull lies inside its
|
|
40
|
+
* rectangle wherever it is read as a hull (UVs in 0..1, offsets 0, which is
|
|
41
|
+
* every region rigc packs), so a footprint overlap implies a rectangle overlap;
|
|
42
|
+
* the rectangle test runs first anyway, so that "nothing A06 accepted is
|
|
43
|
+
* refused" is a property of this code rather than of the art.
|
|
44
|
+
*
|
|
45
|
+
* Each footprint is cut into convex pieces — a rectangle is one, a triangle is
|
|
46
|
+
* one, a hull loop is ear-clipped into triangles — and two footprints overlap
|
|
47
|
+
* when some piece of one and some piece of the other do. Two convex pieces are
|
|
48
|
+
* apart when an edge normal of either separates their projections (the
|
|
49
|
+
* separating-axis theorem, complete for two convex polygons in the plane).
|
|
50
|
+
* Every such comparison is made `FOOTPRINT_SLACK` texels in favour of
|
|
51
|
+
* "apart", the packer's own constant: the coordinates are doubles (a UV as the
|
|
52
|
+
* file writes it times an integer size), so a pair that merely touches may
|
|
53
|
+
* compute as overlapping by a few ulps, and a touch is what A06 accepts.
|
|
54
|
+
* Integer rectangles are exact either way.
|
|
55
|
+
*
|
|
56
|
+
* Links nothing from the runtime.
|
|
57
|
+
*/
|
|
58
|
+
import { FOOTPRINT_SLACK, pageFootprint } from '../atlas.ts';
|
|
59
|
+
import type { AtlasRegionEntry, AtlasRegionFacts } from './facts/atlas_regions.ts';
|
|
60
|
+
import type { MeshEntry, MeshFacts } from './facts/mesh_attachments.ts';
|
|
61
|
+
import type { RegionJoinFacts } from './facts/region_joins.ts';
|
|
62
|
+
|
|
63
|
+
/** A point in page texels, x right and y down from the page's top-left. */
|
|
64
|
+
export type PagePoint = readonly [number, number];
|
|
65
|
+
|
|
66
|
+
/** A region's rectangle on its page, as `pageFootprint` derives it (a turned region's is transposed). */
|
|
67
|
+
export interface PageRect {
|
|
68
|
+
readonly x: number;
|
|
69
|
+
readonly y: number;
|
|
70
|
+
readonly width: number;
|
|
71
|
+
readonly height: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** What one region draws. */
|
|
75
|
+
export interface RegionFootprint {
|
|
76
|
+
readonly region: AtlasRegionEntry;
|
|
77
|
+
readonly rect: PageRect;
|
|
78
|
+
/** `true` when the footprint is the whole rectangle. */
|
|
79
|
+
readonly whole: boolean;
|
|
80
|
+
/** What draws it, as the failure sentence states it. */
|
|
81
|
+
readonly drawn: string;
|
|
82
|
+
/** Its convex pieces, each a loop of page points; one piece, the rectangle, when `whole`. */
|
|
83
|
+
readonly pieces: ReadonlyArray<readonly PagePoint[]>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The rectangle of a region on its page. */
|
|
87
|
+
export function pageRectOf(region: AtlasRegionEntry): PageRect {
|
|
88
|
+
const foot = pageFootprint(region);
|
|
89
|
+
return { x: region.x, y: region.y, width: foot.width, height: foot.height };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function rectPiece(r: PageRect): PagePoint[] {
|
|
93
|
+
return [
|
|
94
|
+
[r.x, r.y],
|
|
95
|
+
[r.x + r.width, r.y],
|
|
96
|
+
[r.x + r.width, r.y + r.height],
|
|
97
|
+
[r.x, r.y + r.height],
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const cross = (o: PagePoint, a: PagePoint, b: PagePoint): number => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
|
102
|
+
|
|
103
|
+
function signedArea(loop: readonly PagePoint[]): number {
|
|
104
|
+
let sum = 0;
|
|
105
|
+
for (let i = 0; i < loop.length; i++) {
|
|
106
|
+
const a = loop[i];
|
|
107
|
+
const b = loop[(i + 1) % loop.length];
|
|
108
|
+
sum += a[0] * b[1] - b[0] * a[1];
|
|
109
|
+
}
|
|
110
|
+
return sum / 2;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A closed loop cut into triangles by ear clipping. A loop with no area gives
|
|
115
|
+
* no piece (it draws nothing a neighbour could sit in). A vertex on a straight
|
|
116
|
+
* run, or the tip of a zero-area spike, is dropped, since it bounds no area.
|
|
117
|
+
* On a loop that is not simple the clipping still ends, and every triangle it
|
|
118
|
+
* gives has three of the loop's own vertices — so it never reaches outside the
|
|
119
|
+
* loop's convex hull, which is inside the region's rectangle.
|
|
120
|
+
*/
|
|
121
|
+
export function earClip(loop: readonly PagePoint[]): PagePoint[][] {
|
|
122
|
+
const points: PagePoint[] = [];
|
|
123
|
+
for (const p of loop) {
|
|
124
|
+
const last = points[points.length - 1];
|
|
125
|
+
if (last === undefined || last[0] !== p[0] || last[1] !== p[1]) points.push(p);
|
|
126
|
+
}
|
|
127
|
+
while (points.length > 1 && points[0][0] === points[points.length - 1][0] && points[0][1] === points[points.length - 1][1]) points.pop();
|
|
128
|
+
if (points.length < 3) return [];
|
|
129
|
+
const area = signedArea(points);
|
|
130
|
+
if (area === 0) return [];
|
|
131
|
+
const ring = area > 0 ? points : points.slice().reverse();
|
|
132
|
+
const idx = ring.map((_, i) => i);
|
|
133
|
+
const out: PagePoint[][] = [];
|
|
134
|
+
let guard = 0;
|
|
135
|
+
while (idx.length > 3 && guard++ < 4 * ring.length * ring.length) {
|
|
136
|
+
let clipped = false;
|
|
137
|
+
for (let k = 0; k < idx.length; k++) {
|
|
138
|
+
const prev = ring[idx[(k + idx.length - 1) % idx.length]];
|
|
139
|
+
const cur = ring[idx[k]];
|
|
140
|
+
const next = ring[idx[(k + 1) % idx.length]];
|
|
141
|
+
const turn = cross(prev, cur, next);
|
|
142
|
+
if (turn === 0) {
|
|
143
|
+
idx.splice(k, 1);
|
|
144
|
+
clipped = true;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
if (turn < 0) continue;
|
|
148
|
+
let blocked = false;
|
|
149
|
+
for (const j of idx) {
|
|
150
|
+
const p = ring[j];
|
|
151
|
+
if (p === prev || p === cur || p === next) continue;
|
|
152
|
+
if (cross(prev, cur, p) >= 0 && cross(cur, next, p) >= 0 && cross(next, prev, p) >= 0) {
|
|
153
|
+
blocked = true;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (blocked) continue;
|
|
158
|
+
out.push([prev, cur, next]);
|
|
159
|
+
idx.splice(k, 1);
|
|
160
|
+
clipped = true;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
// No ear on a loop that is not simple: take the first convex corner anyway,
|
|
164
|
+
// which keeps every triangle on the loop's own vertices (header).
|
|
165
|
+
if (!clipped) {
|
|
166
|
+
const k = idx.findIndex((_, n) => cross(ring[idx[(n + idx.length - 1) % idx.length]], ring[idx[n]], ring[idx[(n + 1) % idx.length]]) > 0);
|
|
167
|
+
const at = k < 0 ? 0 : k;
|
|
168
|
+
out.push([ring[idx[(at + idx.length - 1) % idx.length]], ring[idx[at]], ring[idx[(at + 1) % idx.length]]]);
|
|
169
|
+
idx.splice(at, 1);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (idx.length === 3) {
|
|
173
|
+
const tri: PagePoint[] = [ring[idx[0]], ring[idx[1]], ring[idx[2]]];
|
|
174
|
+
if (cross(tri[0], tri[1], tri[2]) !== 0) out.push(tri);
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A mesh's UV polygons over its own drawing — the hull loop, then each triangle — or why they cannot be read as polygons over its rectangle. */
|
|
180
|
+
function meshPolygons(mesh: MeshEntry): { loops: number[][] } | { why: string } {
|
|
181
|
+
const uvs = mesh.regionUVs;
|
|
182
|
+
const vertices = uvs.length / 2;
|
|
183
|
+
const hull = mesh.hullLength / 2;
|
|
184
|
+
if (uvs.length % 2 !== 0 || !Number.isInteger(hull) || hull < 3 || hull > vertices) return { why: `mesh "${mesh.placeholder}" states a hull of ${hull} over ${vertices} vertex(es)` };
|
|
185
|
+
if (uvs.some((n) => !(n >= 0 && n <= 1))) return { why: `mesh "${mesh.placeholder}"'s UVs leave 0..1, so it samples outside its own rectangle` };
|
|
186
|
+
if (mesh.triangles.length % 3 !== 0 || mesh.triangles.some((t) => !Number.isInteger(t) || t < 0 || t >= vertices)) {
|
|
187
|
+
return { why: `mesh "${mesh.placeholder}"'s triangles do not index its ${vertices} vertex(es)` };
|
|
188
|
+
}
|
|
189
|
+
const loops: number[][] = [uvs.slice(0, 2 * hull)];
|
|
190
|
+
for (let t = 0; t < mesh.triangles.length; t += 3) {
|
|
191
|
+
const [a, b, c] = [mesh.triangles[t], mesh.triangles[t + 1], mesh.triangles[t + 2]];
|
|
192
|
+
loops.push([uvs[2 * a], uvs[2 * a + 1], uvs[2 * b], uvs[2 * b + 1], uvs[2 * c], uvs[2 * c + 1]]);
|
|
193
|
+
}
|
|
194
|
+
return { loops };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const keyOf = (skin: string, slot: string, placeholder: string): string => `${skin}\u0000${slot}\u0000${placeholder}`;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Every region's footprint, in the atlas's region order. `meshes` is `null`
|
|
201
|
+
* where the skeleton did not load (the round trip's own failure is A00's): then
|
|
202
|
+
* which region a mesh draws is not known, and every footprint is its rectangle.
|
|
203
|
+
*/
|
|
204
|
+
export function regionFootprints({ atlas }: AtlasRegionFacts, { joins }: RegionJoinFacts, meshes: MeshFacts | null): RegionFootprint[] {
|
|
205
|
+
if (atlas === null) return [];
|
|
206
|
+
const rectOf = (region: AtlasRegionEntry, drawn: string): RegionFootprint => {
|
|
207
|
+
const rect = pageRectOf(region);
|
|
208
|
+
return { region, rect, whole: true, drawn, pieces: [rectPiece(rect)] };
|
|
209
|
+
};
|
|
210
|
+
if (meshes === null || joins === null) {
|
|
211
|
+
const why = meshes === null ? 'the skeleton did not load, so what draws it is not known' : 'the skeleton did not parse, so what draws it is not known';
|
|
212
|
+
return atlas.regions.map((region) => rectOf(region, `its whole rectangle (${why})`));
|
|
213
|
+
}
|
|
214
|
+
const unread = joins.find((join) => join.lookups === null);
|
|
215
|
+
if (unread !== undefined) {
|
|
216
|
+
const why = `skin "${unread.skin}" slot "${unread.slot}" entry "${unread.placeholder}" states a sequence whose frames cannot be read, so which regions a mesh reaches is not known`;
|
|
217
|
+
return atlas.regions.map((region) => rectOf(region, `its whole rectangle (${why})`));
|
|
218
|
+
}
|
|
219
|
+
const meshAt = new Map<string, MeshEntry>();
|
|
220
|
+
for (const mesh of meshes.meshes) {
|
|
221
|
+
const key = keyOf(mesh.skin, mesh.slot, mesh.placeholder);
|
|
222
|
+
if (!meshAt.has(key)) meshAt.set(key, mesh);
|
|
223
|
+
}
|
|
224
|
+
/** Per region drawn: the meshes over it, or the reason it is a rectangle. */
|
|
225
|
+
const uses = new Map<AtlasRegionEntry, { meshes: MeshEntry[]; rect: string | null }>();
|
|
226
|
+
for (const join of joins) {
|
|
227
|
+
const mesh = meshAt.get(keyOf(join.skin, join.slot, join.placeholder));
|
|
228
|
+
for (const name of join.lookups ?? []) {
|
|
229
|
+
const region = atlas.findRegion(name);
|
|
230
|
+
if (region === null) continue; // A08 names a lookup that finds no region
|
|
231
|
+
const use = uses.get(region) ?? { meshes: [], rect: null };
|
|
232
|
+
uses.set(region, use);
|
|
233
|
+
if (mesh === undefined) use.rect ??= `its whole rectangle, which region attachment "${join.placeholder}" (skin "${join.skin}", slot "${join.slot}") draws`;
|
|
234
|
+
else use.meshes.push(mesh);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return atlas.regions.map((region) => {
|
|
238
|
+
const use = uses.get(region);
|
|
239
|
+
if (use === undefined) return rectOf(region, 'its whole rectangle, which no attachment draws');
|
|
240
|
+
if (use.rect !== null) return rectOf(region, use.rect);
|
|
241
|
+
if (region.degrees !== 0) return rectOf(region, `its whole rectangle (it is turned ${region.degrees} degrees, and a hull is not turned here)`);
|
|
242
|
+
const pieces: PagePoint[][] = [];
|
|
243
|
+
const left = region.x - region.offsetX;
|
|
244
|
+
const top = region.y - (region.originalHeight - region.offsetY - region.height);
|
|
245
|
+
for (const mesh of use.meshes) {
|
|
246
|
+
const read = meshPolygons(mesh);
|
|
247
|
+
if ('why' in read) return rectOf(region, `its whole rectangle (${read.why})`);
|
|
248
|
+
read.loops.forEach((loop, i) => {
|
|
249
|
+
const points: PagePoint[] = [];
|
|
250
|
+
for (let k = 0; k + 1 < loop.length; k += 2) points.push([left + loop[k] * region.originalWidth, top + loop[k + 1] * region.originalHeight]);
|
|
251
|
+
if (i === 0) pieces.push(...earClip(points));
|
|
252
|
+
else if (cross(points[0], points[1], points[2]) !== 0) pieces.push(points);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const named = use.meshes.map((m) => `"${m.placeholder}" (skin "${m.skin}", slot "${m.slot}")`);
|
|
256
|
+
const drawn = named.length === 1 ? `the hull of mesh ${named[0]}` : `the hulls of meshes ${named.join(', ')}`;
|
|
257
|
+
return { region, rect: pageRectOf(region), whole: false, drawn, pieces };
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Whether two convex pieces' interiors overlap by more than `FOOTPRINT_SLACK` along every edge normal of either. */
|
|
262
|
+
export function piecesOverlap(a: readonly PagePoint[], b: readonly PagePoint[]): boolean {
|
|
263
|
+
for (const poly of [a, b]) {
|
|
264
|
+
for (let i = 0; i < poly.length; i++) {
|
|
265
|
+
const p = poly[i];
|
|
266
|
+
const q = poly[(i + 1) % poly.length];
|
|
267
|
+
const nx = q[1] - p[1];
|
|
268
|
+
const ny = p[0] - q[0];
|
|
269
|
+
const len = Math.hypot(nx, ny);
|
|
270
|
+
if (len === 0) continue;
|
|
271
|
+
let minA = Infinity;
|
|
272
|
+
let maxA = -Infinity;
|
|
273
|
+
let minB = Infinity;
|
|
274
|
+
let maxB = -Infinity;
|
|
275
|
+
for (const v of a) {
|
|
276
|
+
const d = (v[0] * nx + v[1] * ny) / len;
|
|
277
|
+
minA = Math.min(minA, d);
|
|
278
|
+
maxA = Math.max(maxA, d);
|
|
279
|
+
}
|
|
280
|
+
for (const v of b) {
|
|
281
|
+
const d = (v[0] * nx + v[1] * ny) / len;
|
|
282
|
+
minB = Math.min(minB, d);
|
|
283
|
+
maxB = Math.max(maxB, d);
|
|
284
|
+
}
|
|
285
|
+
if (maxA <= minB + FOOTPRINT_SLACK || maxB <= minA + FOOTPRINT_SLACK) return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The convex polygon two convex pieces share (Sutherland–Hodgman), for the sentence that names the overlap. */
|
|
292
|
+
function sharedPolygon(subject: readonly PagePoint[], clip: readonly PagePoint[]): PagePoint[] {
|
|
293
|
+
const sign = signedArea(clip) >= 0 ? 1 : -1;
|
|
294
|
+
let out: PagePoint[] = subject.slice();
|
|
295
|
+
for (let i = 0; i < clip.length && out.length > 0; i++) {
|
|
296
|
+
const a = clip[i];
|
|
297
|
+
const b = clip[(i + 1) % clip.length];
|
|
298
|
+
const input = out;
|
|
299
|
+
out = [];
|
|
300
|
+
const inside = (p: PagePoint): boolean => sign * cross(a, b, p) >= 0;
|
|
301
|
+
for (let k = 0; k < input.length; k++) {
|
|
302
|
+
const p = input[k];
|
|
303
|
+
const q = input[(k + 1) % input.length];
|
|
304
|
+
const pin = inside(p);
|
|
305
|
+
const qin = inside(q);
|
|
306
|
+
if (pin) out.push(p);
|
|
307
|
+
if (pin !== qin) {
|
|
308
|
+
const dp = cross(a, b, p);
|
|
309
|
+
const dq = cross(a, b, q);
|
|
310
|
+
const t = dp / (dp - dq);
|
|
311
|
+
out.push([p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t]);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** What two footprints share: the largest area any two pieces share and the box around every shared part — `null` when they do not overlap. */
|
|
319
|
+
export interface FootprintOverlap {
|
|
320
|
+
readonly area: number;
|
|
321
|
+
readonly box: { readonly x0: number; readonly y0: number; readonly x1: number; readonly y1: number };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const overlaps = (a: PageRect, b: PageRect): boolean => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
|
|
325
|
+
|
|
326
|
+
function boxOf(piece: readonly PagePoint[]): { x0: number; y0: number; x1: number; y1: number } {
|
|
327
|
+
let x0 = Infinity;
|
|
328
|
+
let y0 = Infinity;
|
|
329
|
+
let x1 = -Infinity;
|
|
330
|
+
let y1 = -Infinity;
|
|
331
|
+
for (const [x, y] of piece) {
|
|
332
|
+
x0 = Math.min(x0, x);
|
|
333
|
+
y0 = Math.min(y0, y);
|
|
334
|
+
x1 = Math.max(x1, x);
|
|
335
|
+
y1 = Math.max(y1, y);
|
|
336
|
+
}
|
|
337
|
+
return { x0, y0, x1, y1 };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Whether the rectangles' interiors overlap — A06's own test, strict, so two rectangles that share an edge do not. */
|
|
341
|
+
export function rectanglesOverlap(a: RegionFootprint, b: RegionFootprint): boolean {
|
|
342
|
+
return overlaps(a.rect, b.rect);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** What two footprints' interiors share, or `null` when they do not overlap. Asked only of a pair whose rectangles overlap. */
|
|
346
|
+
export function footprintOverlap(a: RegionFootprint, b: RegionFootprint): FootprintOverlap | null {
|
|
347
|
+
let area = 0;
|
|
348
|
+
let box: { x0: number; y0: number; x1: number; y1: number } | null = null;
|
|
349
|
+
const boxesA = a.pieces.map(boxOf);
|
|
350
|
+
const boxesB = b.pieces.map(boxOf);
|
|
351
|
+
for (let i = 0; i < a.pieces.length; i++) {
|
|
352
|
+
for (let j = 0; j < b.pieces.length; j++) {
|
|
353
|
+
const ba = boxesA[i];
|
|
354
|
+
const bb = boxesB[j];
|
|
355
|
+
if (ba.x1 <= bb.x0 || bb.x1 <= ba.x0 || ba.y1 <= bb.y0 || bb.y1 <= ba.y0) continue;
|
|
356
|
+
if (!piecesOverlap(a.pieces[i], b.pieces[j])) continue;
|
|
357
|
+
// The verdict is the separating-axis test's; the clipped polygon only
|
|
358
|
+
// measures it for the sentence, and where rounding leaves it degenerate
|
|
359
|
+
// the two pieces' boxes stand in for where they meet.
|
|
360
|
+
const shared = sharedPolygon(a.pieces[i], b.pieces[j]);
|
|
361
|
+
if (shared.length >= 3) area = Math.max(area, Math.abs(signedArea(shared)));
|
|
362
|
+
const sb = shared.length >= 3 ? boxOf(shared) : { x0: Math.max(ba.x0, bb.x0), y0: Math.max(ba.y0, bb.y0), x1: Math.min(ba.x1, bb.x1), y1: Math.min(ba.y1, bb.y1) };
|
|
363
|
+
box = box === null ? sb : { x0: Math.min(box.x0, sb.x0), y0: Math.min(box.y0, sb.y0), x1: Math.max(box.x1, sb.x1), y1: Math.max(box.y1, sb.y1) };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return box === null ? null : { area, box };
|
|
367
|
+
}
|
package/src/assertions/kinds.ts
CHANGED
|
@@ -32,9 +32,11 @@ export type AssertionKind = 'validity' | 'renderer' | 'archetype';
|
|
|
32
32
|
* validity half must never stop running; their policy clauses are gated inside
|
|
33
33
|
* the assertion body against `profile`, and each such clause says so where it
|
|
34
34
|
* lives. They are A06 (size-vs-PNG and every region's rectangle inside the page
|
|
35
|
-
* it names are validity; pma
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* it names are validity; pma and rotation are policy — the rectangle moved
|
|
36
|
+
* across that line in issue #694, and the reason is stated where it now sits;
|
|
37
|
+
* the policy clause about two regions over the same texels left A06 for
|
|
38
|
+
* `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` in issue #1099, a renderer rule of
|
|
39
|
+
* its own) and A20 (weight coherence is validity; requiring
|
|
38
40
|
* a mesh to be weighted at all is policy).
|
|
39
41
|
*
|
|
40
42
|
* A08 was the third until issue #574 retired its policy clause. It required a
|
|
@@ -94,6 +96,7 @@ export const ASSERTION_KIND: Readonly<Record<string, AssertionKind>> = {
|
|
|
94
96
|
A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES: 'validity',
|
|
95
97
|
A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT: 'validity',
|
|
96
98
|
A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT: 'validity',
|
|
99
|
+
A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP: 'renderer',
|
|
97
100
|
};
|
|
98
101
|
|
|
99
102
|
/** Whether `profile` runs a rule of `kind`: `spine` runs validity alone, `spine-html` every kind. */
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* handed over — now either one given beside the document is refused by name.
|
|
32
32
|
*
|
|
33
33
|
* ⛔ **Not wired into any command.** `build` keeps the round trip and its
|
|
34
|
-
*
|
|
34
|
+
* fifty lines; this entry is what the selftest and the instrument call
|
|
35
35
|
* until every moved assertion is on it (the card's design, point 7).
|
|
36
36
|
*
|
|
37
37
|
* Links nothing from the runtime, directly or through what it imports — held
|
|
@@ -62,6 +62,7 @@ import { a22MeshUvsInUnitRange } from '../bodies/a22.ts';
|
|
|
62
62
|
import { a38SkinMembersAreSkinRequired } from '../bodies/a38.ts';
|
|
63
63
|
import { a06AtlasPageSizeMatchesPng } from '../bodies/a06.ts';
|
|
64
64
|
import { a19OverlayPngsHaveAlpha } from '../bodies/a19.ts';
|
|
65
|
+
import { a49PackedFootprintsDoNotOverlap } from '../bodies/a49.ts';
|
|
65
66
|
import { a27RegionNameMatchesPageFilename } from '../bodies/a27.ts';
|
|
66
67
|
import { modelAnimatedBones } from './animated_bones.ts';
|
|
67
68
|
import { modelSkinMembers } from './skin_members.ts';
|
|
@@ -304,6 +305,7 @@ export const MOVED_ASSERTIONS: readonly MovedAssertion[] = [
|
|
|
304
305
|
{ code: 'A17_ATLAS_PAGE_FILES_EXIST', run: (v, read, input, supply) => a17AtlasPageFilesExist(v, supply.atlasPages(read), input), unread: SKIP_NO_ATLAS },
|
|
305
306
|
{ code: 'A06_ATLAS_PAGE_SIZE_MATCHES_PNG', run: (v, read, input, supply) => a06AtlasPageSizeMatchesPng(v, supply.atlasRegions(read, input), input, input.profile === 'spine-html'), unread: SKIP_NO_ATLAS },
|
|
306
307
|
{ code: 'A19_OVERLAY_PNGS_HAVE_ALPHA', run: (v, read, input, supply) => a19OverlayPngsHaveAlpha(v, supply.atlasRegions(read, input), supply.stage(read, input), supply.skinEntries(read), { atlasDir: input.atlasDir, rig: supply.rigInfo(read, input) }), unread: SKIP_NO_ATLAS },
|
|
308
|
+
{ code: 'A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP', run: (v, read, input, supply) => a49PackedFootprintsDoNotOverlap(v, supply.atlasRegions(read, input), supply.regionJoins(read), supply.meshes(read)), unread: SKIP_NO_ATLAS },
|
|
307
309
|
{ code: 'A24_AXIS_SPACE_STROKE', run: (v, read, input, supply) => a24AxisSpaceStroke(v, supply.boneTimelines(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
308
310
|
{ code: 'A25_DETACHED_BONE_PARENTAGE', run: (v, read, input, supply) => a25DetachedBoneParentage(v, supply.skeletonRoster(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
309
311
|
{ code: 'A26_SLOT_DRAW_ORDER', run: (v, read, input, supply) => a26SlotDrawOrder(v, supply.skeletonRoster(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
@@ -99,6 +99,8 @@ export const SKIP_NO_TIMELINE = 'no animation here carries a timeline';
|
|
|
99
99
|
export const SKIP_NO_PHYSICS_CONSTRAINT = 'the skeleton declares no physics constraint';
|
|
100
100
|
export const SKIP_NO_ATLAS_PAGE = 'the atlas declares no page';
|
|
101
101
|
export const SKIP_NO_ATLAS_REGION = 'the atlas declares no region';
|
|
102
|
+
/** A49's (issue #1099): the subject is a pair of regions on one page, and an atlas of one-region pages carries none. */
|
|
103
|
+
export const SKIP_NO_ATLAS_REGION_PAIR = 'no page of the atlas carries two regions, so there is no pair of footprints to hold apart';
|
|
102
104
|
export const SKIP_NO_ATTACHMENT_REGION_JOIN =
|
|
103
105
|
'no attachment names a region and the atlas declares none, so there is no attachment-to-region join to hold';
|
|
104
106
|
export const SKIP_NO_TWO_COLOR_TINT =
|