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
package/src/atlas.ts
CHANGED
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
* be ADDED to the page for the render to agree as well, and `PACK_NO_ROTATE` for
|
|
32
32
|
* the field this deliberately does not use.
|
|
33
33
|
*
|
|
34
|
+
* Under `shape: 'polygon'` (issue #1099) the copy is lossless where a region
|
|
35
|
+
* can be SAMPLED: two rectangles may overlap where neither region draws, so a
|
|
36
|
+
* mesh region's rectangle outside its hull may carry a neighbour's texels, and
|
|
37
|
+
* the claim the selftest holds there is that every texel within a tap of what
|
|
38
|
+
* a region draws is its own (`PK79`). See `packAtlas`, *`shape: 'polygon'`*.
|
|
39
|
+
*
|
|
34
40
|
* ⚠️ **The rendered pictures are equal to within one least significant bit, not
|
|
35
41
|
* bit-for-bit, and the difference is arithmetic rather than texels.** Measured: 0
|
|
36
42
|
* to 480 channel samples of 7 to 21 million on the three public fixtures, worst
|
|
@@ -693,6 +699,32 @@ export const DEFAULT_PAGE_EDGES: PageEdges = 'pot';
|
|
|
693
699
|
*/
|
|
694
700
|
export const FREE_EDGE_STEP = 1;
|
|
695
701
|
|
|
702
|
+
/**
|
|
703
|
+
* What a packed region's rectangle may share with its neighbours' — `--pack-shape`
|
|
704
|
+
* (issue #1099, stage 1 of #1093). See `packAtlas`, *`shape: 'polygon'`*.
|
|
705
|
+
*
|
|
706
|
+
* `rect` is the default and is the packer exactly as it was before the option
|
|
707
|
+
* existed: no two cells overlap. `polygon` packs every region whose every
|
|
708
|
+
* attachment is a mesh by that mesh's emitted hull, so two rectangles may
|
|
709
|
+
* overlap where neither region's footprint is; a region attachment's footprint
|
|
710
|
+
* stays its rectangle, because it draws its whole quad.
|
|
711
|
+
*/
|
|
712
|
+
export const PACK_SHAPES = ['rect', 'polygon'] as const;
|
|
713
|
+
export type PackShape = (typeof PACK_SHAPES)[number];
|
|
714
|
+
export const DEFAULT_PACK_SHAPE: PackShape = 'rect';
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* The part of a region its attachments draw, in the region's own texels — x
|
|
718
|
+
* right and y down from the drawing's top-left corner, so `(u · width, v ·
|
|
719
|
+
* height)` for a mesh UV `(u, v)`. Each polygon is a flat `[x0, y0, x1, y1, …]`
|
|
720
|
+
* loop, closed from its last vertex back to its first: a mesh's hull loop, and
|
|
721
|
+
* each of its triangles. Absent on a `PackInput`, the footprint is the whole
|
|
722
|
+
* rectangle, which is what every region attachment's is.
|
|
723
|
+
*/
|
|
724
|
+
export interface PackFootprint {
|
|
725
|
+
polygons: ReadonlyArray<readonly number[]>;
|
|
726
|
+
}
|
|
727
|
+
|
|
696
728
|
/** The part a page is packed from: its region name and its pixels. */
|
|
697
729
|
export interface PackInput {
|
|
698
730
|
region: string;
|
|
@@ -700,6 +732,11 @@ export interface PackInput {
|
|
|
700
732
|
absPath: string;
|
|
701
733
|
width: number;
|
|
702
734
|
height: number;
|
|
735
|
+
/**
|
|
736
|
+
* What the region's attachments draw (`packFootprints`), read only under
|
|
737
|
+
* `shape: 'polygon'`. Absent means the rectangle.
|
|
738
|
+
*/
|
|
739
|
+
footprint?: PackFootprint;
|
|
703
740
|
}
|
|
704
741
|
|
|
705
742
|
export interface PackOptions {
|
|
@@ -711,6 +748,8 @@ export interface PackOptions {
|
|
|
711
748
|
pageStem?: string;
|
|
712
749
|
/** What the page's edges may be (default `pot`). See `PAGE_EDGES`. */
|
|
713
750
|
pageEdges?: PageEdges;
|
|
751
|
+
/** What two rectangles may share (default `rect`). See `PACK_SHAPES`. */
|
|
752
|
+
shape?: PackShape;
|
|
714
753
|
}
|
|
715
754
|
|
|
716
755
|
/** Where one region landed. `x`/`y` are the REGION's own corner, not its cell's. */
|
|
@@ -741,6 +780,8 @@ export interface PackResult {
|
|
|
741
780
|
/** The atlas text for the packed pages. */
|
|
742
781
|
atlasText: string;
|
|
743
782
|
padding: number;
|
|
783
|
+
/** The shape the pack was made under — what the `pack:` line's last field states. */
|
|
784
|
+
shape: PackShape;
|
|
744
785
|
}
|
|
745
786
|
|
|
746
787
|
/** A free rectangle in the MaxRects free list. */
|
|
@@ -784,15 +825,16 @@ function smallestPageFor(
|
|
|
784
825
|
cells: Array<{ w: number; h: number }>,
|
|
785
826
|
maxEdge: number,
|
|
786
827
|
pageEdges: PageEdges,
|
|
828
|
+
shapes?: readonly CellShape[],
|
|
787
829
|
): { width: number; height: number; rects: Rect[] } | null {
|
|
788
|
-
if (pageEdges === 'free') return smallestFreePageFor(cells, maxEdge);
|
|
830
|
+
if (pageEdges === 'free') return smallestFreePageFor(cells, maxEdge, shapes);
|
|
789
831
|
const edges: number[] = [];
|
|
790
832
|
for (let e = 1; e <= maxEdge; e *= 2) edges.push(e);
|
|
791
833
|
const candidates: Array<{ w: number; h: number }> = [];
|
|
792
834
|
for (const w of edges) for (const h of edges) candidates.push({ w, h });
|
|
793
835
|
candidates.sort((a, b) => a.w * a.h - b.w * b.h || a.w - b.w);
|
|
794
836
|
for (const candidate of candidates) {
|
|
795
|
-
const attempt =
|
|
837
|
+
const attempt = placePass(cells, shapes, candidate.w, candidate.h, Infinity, { wholeOrNothing: true });
|
|
796
838
|
if (attempt.some((r) => r === null)) continue;
|
|
797
839
|
return { width: candidate.w, height: candidate.h, rects: attempt as Rect[] };
|
|
798
840
|
}
|
|
@@ -843,8 +885,9 @@ function smallestPageFor(
|
|
|
843
885
|
function smallestFreePageFor(
|
|
844
886
|
cells: Array<{ w: number; h: number }>,
|
|
845
887
|
maxEdge: number,
|
|
888
|
+
shapes?: readonly CellShape[],
|
|
846
889
|
): { width: number; height: number; rects: Rect[] } | null {
|
|
847
|
-
return freePageSearch(cells, maxEdge).page;
|
|
890
|
+
return freePageSearch(cells, maxEdge, shapes).page;
|
|
848
891
|
}
|
|
849
892
|
|
|
850
893
|
/** What one `free` page search did: the page it chose, and what each width cost. */
|
|
@@ -858,15 +901,37 @@ export interface FreePageSearch {
|
|
|
858
901
|
abandoned: number;
|
|
859
902
|
/** Widths never placed: too narrow for the cells' area, or wide enough that the tallest cell alone loses. */
|
|
860
903
|
skipped: number;
|
|
904
|
+
/** What the passes placed, by kind (`PassTally`). */
|
|
905
|
+
tally: PassTally;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* How `freePageSearch` runs its passes — for the selftest's plants only
|
|
910
|
+
* (`PK94`, `PK95`). Each turns one of issue #1102's savings off: `stopAtMiss:
|
|
911
|
+
* false` runs every footprint pass to its end, `edgeIndex: false` prunes the
|
|
912
|
+
* free list by scanning all of it. The page is the same either way; only what
|
|
913
|
+
* it costs differs, and the plants hold that.
|
|
914
|
+
*/
|
|
915
|
+
export interface FreePageSearchOptions {
|
|
916
|
+
stopAtMiss?: boolean;
|
|
917
|
+
/** `false` scans the whole free list for a piece's containers (`splitFree`), as the prune did before issue #1102. */
|
|
918
|
+
edgeIndex?: boolean;
|
|
861
919
|
}
|
|
862
920
|
|
|
863
921
|
/**
|
|
864
922
|
* `smallestFreePageFor`'s search with its bookkeeping — exported so the
|
|
865
923
|
* selftest can compare its page against an unbounded every-width reference and
|
|
866
924
|
* see that the bounds did prune (`PK75`). The page is the whole of what the
|
|
867
|
-
* packer uses; the counts are for the control.
|
|
925
|
+
* packer uses; the counts are for the control. `shapes`, one per cell, is
|
|
926
|
+
* `shape: 'polygon'`'s placement (`placePass`); absent, the rectangle pass.
|
|
868
927
|
*/
|
|
869
|
-
export function freePageSearch(
|
|
928
|
+
export function freePageSearch(
|
|
929
|
+
cells: Array<{ w: number; h: number }>,
|
|
930
|
+
maxEdge: number,
|
|
931
|
+
shapes?: readonly CellShape[],
|
|
932
|
+
options: FreePageSearchOptions = {},
|
|
933
|
+
): FreePageSearch {
|
|
934
|
+
const wholeOrNothing = options.stopAtMiss ?? true;
|
|
870
935
|
let widest = 0;
|
|
871
936
|
let tallest = 0;
|
|
872
937
|
let cellArea = 0;
|
|
@@ -875,7 +940,15 @@ export function freePageSearch(cells: Array<{ w: number; h: number }>, maxEdge:
|
|
|
875
940
|
tallest = Math.max(tallest, cell.h);
|
|
876
941
|
cellArea += cell.w * cell.h;
|
|
877
942
|
}
|
|
878
|
-
|
|
943
|
+
// 🔒 Under `polygon` cells may overlap, so their area is no bound on the page;
|
|
944
|
+
// what is, is the texels they OWN, which are disjoint (issue #1099). Bounding
|
|
945
|
+
// by the cells' area skipped every width a polygon spill page needed, and a
|
|
946
|
+
// spilled page whose cells overlapped was refused as fitting no page at all.
|
|
947
|
+
if (shapes !== undefined) {
|
|
948
|
+
cellArea = 0;
|
|
949
|
+
for (const shape of shapes) cellArea += shape.owned;
|
|
950
|
+
}
|
|
951
|
+
const out: FreePageSearch = { page: null, widths: 0, completed: 0, abandoned: 0, skipped: 0, tally: { rectPlaced: 0, footprintPlaced: 0, bandSplits: 0, containmentTests: 0 } };
|
|
879
952
|
let best: { width: number; height: number; rects: Rect[] } | null = null;
|
|
880
953
|
for (let width = Math.ceil(widest / FREE_EDGE_STEP) * FREE_EDGE_STEP; width <= maxEdge; width += FREE_EDGE_STEP) {
|
|
881
954
|
out.widths++;
|
|
@@ -885,7 +958,7 @@ export function freePageSearch(cells: Array<{ w: number; h: number }>, maxEdge:
|
|
|
885
958
|
continue;
|
|
886
959
|
}
|
|
887
960
|
const maxBottom = best === null ? Infinity : Math.floor(bestArea / width);
|
|
888
|
-
const attempt =
|
|
961
|
+
const attempt = placePass(cells, shapes, width, maxEdge, maxBottom, { wholeOrNothing, tally: out.tally, edgeIndex: options.edgeIndex ?? true });
|
|
889
962
|
let height = 0;
|
|
890
963
|
for (const r of attempt) if (r !== null) height = Math.max(height, r.y + r.h);
|
|
891
964
|
if (height > maxBottom) {
|
|
@@ -1030,6 +1103,740 @@ function packOnePage(
|
|
|
1030
1103
|
return placed;
|
|
1031
1104
|
}
|
|
1032
1105
|
|
|
1106
|
+
/**
|
|
1107
|
+
* One placement pass: the rectangle pass (`packOnePage`) when `shapes` is
|
|
1108
|
+
* absent, which is every `shape: 'rect'` call and so every pack made before the
|
|
1109
|
+
* option existed, and the footprint pass (`packOnePageByFootprint`) when it is
|
|
1110
|
+
* given.
|
|
1111
|
+
*/
|
|
1112
|
+
function placePass(
|
|
1113
|
+
cells: Array<{ w: number; h: number }>,
|
|
1114
|
+
shapes: readonly CellShape[] | undefined,
|
|
1115
|
+
pageW: number,
|
|
1116
|
+
pageH: number,
|
|
1117
|
+
maxBottom = Infinity,
|
|
1118
|
+
pass: PassOptions = {},
|
|
1119
|
+
): Array<Rect | null> {
|
|
1120
|
+
const wholeOrNothing = pass.wholeOrNothing ?? false;
|
|
1121
|
+
const tally = pass.tally;
|
|
1122
|
+
if (shapes === undefined || shapes.every((s) => s.whole)) {
|
|
1123
|
+
const byRect = packOnePage(cells, pageW, pageH, maxBottom);
|
|
1124
|
+
if (tally !== undefined) tally.rectPlaced += placedCount(byRect);
|
|
1125
|
+
return byRect;
|
|
1126
|
+
}
|
|
1127
|
+
// ⭐ `wholeOrNothing` is the page searches' question (issue #1102): they keep
|
|
1128
|
+
// a pass only when it placed every cell, so a pass that has missed one is
|
|
1129
|
+
// already discarded, whatever it does next. Two savings follow, and neither
|
|
1130
|
+
// can change a page the search keeps:
|
|
1131
|
+
//
|
|
1132
|
+
// * the rectangle pass is not run when the cells' own area exceeds the
|
|
1133
|
+
// page — disjoint rectangles cannot all fit, so it could not place every
|
|
1134
|
+
// cell, and the pass it would have lost or won against is discarded either
|
|
1135
|
+
// way (a footprint pass that placed every cell beats a rectangle pass that
|
|
1136
|
+
// did not, on the first key);
|
|
1137
|
+
// * the footprint pass stops at its first miss (`packOnePageByFootprint`'s
|
|
1138
|
+
// `stopAtMiss`), returning the cells after it as not placed.
|
|
1139
|
+
//
|
|
1140
|
+
// What is returned can then differ from the full answer only in which of two
|
|
1141
|
+
// failed passes it is, or in how many cells a failed pass placed — and the
|
|
1142
|
+
// searches read neither. The spill's assignment pass, which keeps the cells
|
|
1143
|
+
// a pass did place, never asks this (`packAtlas`).
|
|
1144
|
+
//
|
|
1145
|
+
// Measured on a replica of a production rig (31 meshes, a two-page `free`
|
|
1146
|
+
// spill at 2048, padding 2), where the pack took 260–304 s against `rect`'s
|
|
1147
|
+
// 0.14–0.24 s: the single-page search ran a full footprint pass at every one of its
|
|
1148
|
+
// 854 widths (54 % of the time), because nothing fits and so no best page
|
|
1149
|
+
// ever bounds a width, and the first page's own search ran one at every width
|
|
1150
|
+
// it did not abandon (46 %). The largest cell's owned box does not start at
|
|
1151
|
+
// its cell's corner, so on an empty page that cell is every footprint pass's
|
|
1152
|
+
// first miss — all 1,709 of them — and every one of those passes was thrown
|
|
1153
|
+
// away. With the early stop the pack takes 0.15–0.7 s by the load, the
|
|
1154
|
+
// same pages.
|
|
1155
|
+
const rectCanFit = !wholeOrNothing || cellAreaOf(cells) <= pageW * pageH;
|
|
1156
|
+
const byRect = rectCanFit ? packOnePage(cells, pageW, pageH, maxBottom) : null;
|
|
1157
|
+
if (tally !== undefined && byRect !== null) tally.rectPlaced += placedCount(byRect);
|
|
1158
|
+
// ⭐ `polygon` never costs a page anything `rect` would have saved. Both passes
|
|
1159
|
+
// are run on the same page and the better one is kept: more cells placed,
|
|
1160
|
+
// then the higher bottom edge, then — on a tie — the rectangle pass. Two
|
|
1161
|
+
// disjoint cells never share a protected texel, so the rectangle pass is
|
|
1162
|
+
// itself a legal footprint placement, and keeping it where it is better is
|
|
1163
|
+
// choosing between two legal answers, not giving the mode up. Measured
|
|
1164
|
+
// before this rule existed: the footprint pass alone wrote larger pages
|
|
1165
|
+
// than `rect` on two of the five gallery rigs that carry a mesh, because a
|
|
1166
|
+
// greedy pass that places one cell differently places every later one
|
|
1167
|
+
// differently too.
|
|
1168
|
+
const byFootprint = packOnePageByFootprint(shapes, pageW, pageH, maxBottom, wholeOrNothing, pass.edgeIndex ?? true, tally);
|
|
1169
|
+
if (tally !== undefined) tally.footprintPlaced += placedCount(byFootprint);
|
|
1170
|
+
if (byRect === null) return byFootprint;
|
|
1171
|
+
const bottomOf = (placed: Array<Rect | null>): number => placed.reduce((n, r) => (r === null ? n : Math.max(n, r.y + r.h)), 0);
|
|
1172
|
+
const placedRect = placedCount(byRect);
|
|
1173
|
+
const placedFoot = placedCount(byFootprint);
|
|
1174
|
+
if (placedFoot !== placedRect) return placedFoot > placedRect ? byFootprint : byRect;
|
|
1175
|
+
return bottomOf(byFootprint) < bottomOf(byRect) ? byFootprint : byRect;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/** How `placePass` runs — what the page searches ask of it (issue #1102). */
|
|
1179
|
+
interface PassOptions {
|
|
1180
|
+
/** Only a pass that places every cell is kept by the caller. See `placePass`. */
|
|
1181
|
+
wholeOrNothing?: boolean;
|
|
1182
|
+
/** Where to count what the passes placed. */
|
|
1183
|
+
tally?: PassTally;
|
|
1184
|
+
/** `splitFree`'s edge index (default on); off only for the selftest's plant. */
|
|
1185
|
+
edgeIndex?: boolean;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/** How many cells a pass placed. */
|
|
1189
|
+
function placedCount(pass: ReadonlyArray<Rect | null>): number {
|
|
1190
|
+
let n = 0;
|
|
1191
|
+
for (const r of pass) if (r !== null) n++;
|
|
1192
|
+
return n;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/** The cells' own area — what disjoint rectangles need of a page at least. */
|
|
1196
|
+
function cellAreaOf(cells: ReadonlyArray<{ w: number; h: number }>): number {
|
|
1197
|
+
let area = 0;
|
|
1198
|
+
for (const cell of cells) area += cell.w * cell.h;
|
|
1199
|
+
return area;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* What a page search's passes placed, by kind — the operation count a search's
|
|
1204
|
+
* cost is held by (`PK93`), since a footprint placement splits the free list
|
|
1205
|
+
* once per band of what the cell owns and a rectangle placement once.
|
|
1206
|
+
*/
|
|
1207
|
+
export interface PassTally {
|
|
1208
|
+
/** Cells placed by rectangle passes (`packOnePage`). */
|
|
1209
|
+
rectPlaced: number;
|
|
1210
|
+
/** Cells placed by footprint passes (`packOnePageByFootprint`). */
|
|
1211
|
+
footprintPlaced: number;
|
|
1212
|
+
/** Free-list splits the footprint passes made — one per band of every cell they placed (`splitFree`). */
|
|
1213
|
+
bandSplits: number;
|
|
1214
|
+
/** Containment tests the footprint passes' prune made (`splitFree`). */
|
|
1215
|
+
containmentTests: number;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// ---------------------------------------------------------------------------
|
|
1219
|
+
// footprints — `shape: 'polygon'` (issue #1099)
|
|
1220
|
+
// ---------------------------------------------------------------------------
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* A cell as `shape: 'polygon'` places it: the region plus `padding` on every
|
|
1224
|
+
* side, and the cell texels the region OWNS — its protected set.
|
|
1225
|
+
*
|
|
1226
|
+
* ## The protected set, and why it is the footprint test
|
|
1227
|
+
*
|
|
1228
|
+
* A region attachment owns its whole cell, exactly the cell `shape: 'rect'`
|
|
1229
|
+
* keeps apart from every other: it draws its whole quad, and its gutter is what
|
|
1230
|
+
* makes its edge sample as the loose page's does (`extrudeCell`).
|
|
1231
|
+
*
|
|
1232
|
+
* A mesh region owns every cell texel `t` whose unit square, grown by `reach =
|
|
1233
|
+
* max(padding, 1)` texels on every side (a Chebyshev dilation), meets one of
|
|
1234
|
+
* the region's footprint polygons, closed — boundary contact counts. Then:
|
|
1235
|
+
*
|
|
1236
|
+
* * it holds every texel the mesh can sample. A bilinear tap at a point `q`
|
|
1237
|
+
* inside a triangle reads the four texels whose centres are within one
|
|
1238
|
+
* texel of `q`, and each of those squares lies within half a texel of `q`;
|
|
1239
|
+
* `reach` ≥ 1 covers that with room for the float32 page UVs the runtime
|
|
1240
|
+
* stores (`src/atlas.ts`'s header: worst 3.15e-5 texels);
|
|
1241
|
+
* * it keeps the padding the rectangle keeps. A rectangle's cell is the
|
|
1242
|
+
* rectangle grown by `padding`, and this is the hull grown by `padding` —
|
|
1243
|
+
* for a hull that covers its whole drawing, texel for texel the same cell,
|
|
1244
|
+
* which is why such a mesh is placed exactly as a rectangle is;
|
|
1245
|
+
* * it is clipped to the cell, so a region never claims a texel outside the
|
|
1246
|
+
* cell its own pixels are extruded into.
|
|
1247
|
+
*
|
|
1248
|
+
* ⇒ **Two cells may be placed with their rectangles overlapping exactly when
|
|
1249
|
+
* their protected sets share no texel.** For two rectangles that is today's
|
|
1250
|
+
* rule — two cells share no texel — so a pack with no mesh region is placed
|
|
1251
|
+
* by `rect`'s arithmetic, decision for decision. For any two footprints it
|
|
1252
|
+
* means their Chebyshev distance is at least `2 · padding`: if a point of one
|
|
1253
|
+
* were within `2 · padding` of a point of the other, the texel under their
|
|
1254
|
+
* midpoint would be in both protected sets.
|
|
1255
|
+
*
|
|
1256
|
+
* ## Exactness — where it rounds, and in which direction
|
|
1257
|
+
*
|
|
1258
|
+
* The polygons are doubles: a UV as `skeleton.json` states it, times the
|
|
1259
|
+
* region's integer size. The set is computed row by row — the band
|
|
1260
|
+
* `[row − reach, row + 1 + reach]` against each polygon: every edge clipped to
|
|
1261
|
+
* the band, and the polygon's even-odd spans on the band's two lines, which
|
|
1262
|
+
* together are the polygon's exact extent inside the band — and every
|
|
1263
|
+
* comparison that could decide "outside" is made `FOOTPRINT_SLACK` in the
|
|
1264
|
+
* polygon's favour. **The only rounding grows the set**, by at most that slack
|
|
1265
|
+
* (1e-6 texels, against a double's 1e-13 on these magnitudes), so it can make
|
|
1266
|
+
* two footprints keep further apart than they had to and never lets two
|
|
1267
|
+
* touch.
|
|
1268
|
+
*/
|
|
1269
|
+
export interface CellShape {
|
|
1270
|
+
/** The cell: the region plus `padding` on every side. */
|
|
1271
|
+
width: number;
|
|
1272
|
+
height: number;
|
|
1273
|
+
/** 1 on every texel the region owns, row-major over the cell. */
|
|
1274
|
+
mask: Uint8Array;
|
|
1275
|
+
/** `mask` rounded out to bands of `FOOTPRINT_BAND_ROWS` rows, as disjoint rectangles in cell texels, top to bottom — what the free list is split by. */
|
|
1276
|
+
rects: Rect[];
|
|
1277
|
+
/** The bounding box of `mask`, in cell texels. */
|
|
1278
|
+
bbox: Rect;
|
|
1279
|
+
/** The mask is the whole cell: the region is placed exactly as `shape: 'rect'` places it. */
|
|
1280
|
+
whole: boolean;
|
|
1281
|
+
/** How many texels the mask owns — what the `free` search's area bound sums, since owned sets are disjoint and cells may not be. */
|
|
1282
|
+
owned: number;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/** How far in a polygon's favour every footprint comparison is made. See `CellShape`, *Exactness*. */
|
|
1286
|
+
export const FOOTPRINT_SLACK = 1e-6;
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* How many rows of a cell's owned set one free-list rectangle spans — the
|
|
1290
|
+
* resolution the footprint pass's free list keeps, not the resolution of the
|
|
1291
|
+
* footprint test (which is the exact set, texel by texel). Splitting the free
|
|
1292
|
+
* list by one rectangle per row of a curved hull made it thousands long: a
|
|
1293
|
+
* 60-part set of ellipse hulls ran over ten minutes on a `free` page. See
|
|
1294
|
+
* `packOnePageByFootprint` for what the bands cost.
|
|
1295
|
+
*/
|
|
1296
|
+
export const FOOTPRINT_BAND_ROWS = 8;
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* The closed x-extent of one polygon inside the band `y0 ≤ y ≤ y1`, as a list
|
|
1300
|
+
* of closed intervals whose union is exactly that extent: each edge clipped to
|
|
1301
|
+
* the band, and the even-odd spans on the band's two lines (a point inside the
|
|
1302
|
+
* polygon and the band either reaches a band line vertically without leaving
|
|
1303
|
+
* the polygon, or meets an edge inside the band on the way).
|
|
1304
|
+
*/
|
|
1305
|
+
function bandExtent(poly: readonly number[], offset: number, y0: number, y1: number, out: Array<[number, number]>): void {
|
|
1306
|
+
const n = poly.length / 2;
|
|
1307
|
+
for (let i = 0; i < n; i++) {
|
|
1308
|
+
const ax = poly[2 * i] + offset;
|
|
1309
|
+
const ay = poly[2 * i + 1] + offset;
|
|
1310
|
+
const j = (i + 1) % n;
|
|
1311
|
+
const bx = poly[2 * j] + offset;
|
|
1312
|
+
const by = poly[2 * j + 1] + offset;
|
|
1313
|
+
if (Math.max(ay, by) < y0 || Math.min(ay, by) > y1) continue;
|
|
1314
|
+
if (ay === by) {
|
|
1315
|
+
out.push([Math.min(ax, bx), Math.max(ax, bx)]);
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
let t0 = (y0 - ay) / (by - ay);
|
|
1319
|
+
let t1 = (y1 - ay) / (by - ay);
|
|
1320
|
+
if (t0 > t1) [t0, t1] = [t1, t0];
|
|
1321
|
+
t0 = Math.max(0, t0);
|
|
1322
|
+
t1 = Math.min(1, t1);
|
|
1323
|
+
if (t0 > t1) continue;
|
|
1324
|
+
const xa = ax + (bx - ax) * t0;
|
|
1325
|
+
const xb = ax + (bx - ax) * t1;
|
|
1326
|
+
out.push([Math.min(xa, xb), Math.max(xa, xb)]);
|
|
1327
|
+
}
|
|
1328
|
+
for (const y of [y0, y1]) {
|
|
1329
|
+
const crossings: number[] = [];
|
|
1330
|
+
for (let i = 0; i < n; i++) {
|
|
1331
|
+
const ay = poly[2 * i + 1] + offset;
|
|
1332
|
+
const j = (i + 1) % n;
|
|
1333
|
+
const by = poly[2 * j + 1] + offset;
|
|
1334
|
+
if ((ay <= y && y < by) || (by <= y && y < ay)) {
|
|
1335
|
+
const ax = poly[2 * i] + offset;
|
|
1336
|
+
const bx = poly[2 * j] + offset;
|
|
1337
|
+
crossings.push(ax + ((y - ay) * (bx - ax)) / (by - ay));
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
crossings.sort((a, b) => a - b);
|
|
1341
|
+
for (let k = 0; k + 1 < crossings.length; k += 2) out.push([crossings[k], crossings[k + 1]]);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* A region's cell and protected set (`CellShape`) — the rectangle when
|
|
1347
|
+
* `footprint` is absent, the dilated footprint polygons when it is given.
|
|
1348
|
+
* Exported so the selftest can hold the pages to the sets the packer kept
|
|
1349
|
+
* apart (`PK79`) without restating the rule.
|
|
1350
|
+
*/
|
|
1351
|
+
export function footprintCell(width: number, height: number, padding: number, footprint?: PackFootprint): CellShape {
|
|
1352
|
+
const cw = width + 2 * padding;
|
|
1353
|
+
const ch = height + 2 * padding;
|
|
1354
|
+
const whole = (): CellShape => {
|
|
1355
|
+
const cell = { x: 0, y: 0, w: cw, h: ch };
|
|
1356
|
+
return { width: cw, height: ch, mask: new Uint8Array(cw * ch).fill(1), rects: [cell], bbox: { ...cell }, whole: true, owned: cw * ch };
|
|
1357
|
+
};
|
|
1358
|
+
if (footprint === undefined) return whole();
|
|
1359
|
+
const mask = new Uint8Array(cw * ch);
|
|
1360
|
+
const reach = Math.max(padding, 1);
|
|
1361
|
+
const spans: Array<[number, number]> = [];
|
|
1362
|
+
for (let row = 0; row < ch; row++) {
|
|
1363
|
+
spans.length = 0;
|
|
1364
|
+
const y0 = row - reach - FOOTPRINT_SLACK;
|
|
1365
|
+
const y1 = row + 1 + reach + FOOTPRINT_SLACK;
|
|
1366
|
+
for (const poly of footprint.polygons) if (poly.length >= 6) bandExtent(poly, padding, y0, y1, spans);
|
|
1367
|
+
const at = row * cw;
|
|
1368
|
+
for (const [a, b] of spans) {
|
|
1369
|
+
const from = Math.max(0, Math.ceil(a - 1 - reach - FOOTPRINT_SLACK));
|
|
1370
|
+
const to = Math.min(cw - 1, Math.floor(b + reach + FOOTPRINT_SLACK));
|
|
1371
|
+
for (let x = from; x <= to; x++) mask[at + x] = 1;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (mask.every((v) => v === 1)) return whole();
|
|
1375
|
+
// The rectangles the free list is split by: the set rounded OUT to bands of
|
|
1376
|
+
// `FOOTPRINT_BAND_ROWS` rows — each band the columns any of its rows owns,
|
|
1377
|
+
// as runs, merged downwards while a run repeats exactly. Disjoint, a superset
|
|
1378
|
+
// of the set, and in a fixed order (top to bottom, then left to right), so
|
|
1379
|
+
// the free list is split the same way every time. Only the free list reads
|
|
1380
|
+
// them; what a cell owns, what is drawn and what the footprint test compares
|
|
1381
|
+
// is the exact set.
|
|
1382
|
+
const rects: Rect[] = [];
|
|
1383
|
+
let open = new Map<number, Rect>();
|
|
1384
|
+
const columns = new Uint8Array(cw);
|
|
1385
|
+
for (let band = 0; band < ch; band += FOOTPRINT_BAND_ROWS) {
|
|
1386
|
+
const rows = Math.min(FOOTPRINT_BAND_ROWS, ch - band);
|
|
1387
|
+
columns.fill(0);
|
|
1388
|
+
for (let row = band; row < band + rows; row++) for (let x = 0; x < cw; x++) if (mask[row * cw + x] === 1) columns[x] = 1;
|
|
1389
|
+
const next = new Map<number, Rect>();
|
|
1390
|
+
let x = 0;
|
|
1391
|
+
while (x < cw) {
|
|
1392
|
+
if (columns[x] === 0) {
|
|
1393
|
+
x++;
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
const start = x;
|
|
1397
|
+
while (x < cw && columns[x] === 1) x++;
|
|
1398
|
+
const key = start * (cw + 1) + x;
|
|
1399
|
+
const continued = open.get(key);
|
|
1400
|
+
if (continued !== undefined && continued.y + continued.h === band) {
|
|
1401
|
+
continued.h += rows;
|
|
1402
|
+
next.set(key, continued);
|
|
1403
|
+
} else {
|
|
1404
|
+
const rect = { x: start, y: band, w: x - start, h: rows };
|
|
1405
|
+
rects.push(rect);
|
|
1406
|
+
next.set(key, rect);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
open = next;
|
|
1410
|
+
}
|
|
1411
|
+
// The bounding box is the exact set's: a candidate is placed so that box
|
|
1412
|
+
// lies in a free rectangle, which no other cell's bands reach.
|
|
1413
|
+
let minX = cw;
|
|
1414
|
+
let minY = ch;
|
|
1415
|
+
let maxX = 0;
|
|
1416
|
+
let maxY = 0;
|
|
1417
|
+
for (let y = 0; y < ch; y++) {
|
|
1418
|
+
for (let x = 0; x < cw; x++) {
|
|
1419
|
+
if (mask[y * cw + x] === 0) continue;
|
|
1420
|
+
minX = Math.min(minX, x);
|
|
1421
|
+
minY = Math.min(minY, y);
|
|
1422
|
+
maxX = Math.max(maxX, x + 1);
|
|
1423
|
+
maxY = Math.max(maxY, y + 1);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
// A footprint that owns nothing (no polygon of three vertices) still owns a
|
|
1427
|
+
// texel: a cell that claimed no texel could be placed on top of another and
|
|
1428
|
+
// the packer would have nothing to keep apart. Its top-left texel is the
|
|
1429
|
+
// smallest claim, and it is the cell's own.
|
|
1430
|
+
if (rects.length === 0) {
|
|
1431
|
+
mask[0] = 1;
|
|
1432
|
+
rects.push({ x: 0, y: 0, w: 1, h: 1 });
|
|
1433
|
+
minX = 0;
|
|
1434
|
+
minY = 0;
|
|
1435
|
+
maxX = 1;
|
|
1436
|
+
maxY = 1;
|
|
1437
|
+
}
|
|
1438
|
+
let count = 0;
|
|
1439
|
+
for (const v of mask) count += v;
|
|
1440
|
+
return { width: cw, height: ch, mask, rects, bbox: { x: minX, y: minY, w: maxX - minX, h: maxY - minY }, whole: false, owned: count };
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/** Whether two placed cells' protected sets share a texel. */
|
|
1444
|
+
function shapesMeet(a: CellShape, ax: number, ay: number, b: CellShape, bx: number, by: number): boolean {
|
|
1445
|
+
const x0 = Math.max(ax, bx);
|
|
1446
|
+
const y0 = Math.max(ay, by);
|
|
1447
|
+
const x1 = Math.min(ax + a.width, bx + b.width);
|
|
1448
|
+
const y1 = Math.min(ay + a.height, by + b.height);
|
|
1449
|
+
for (let y = y0; y < y1; y++) {
|
|
1450
|
+
const rowA = (y - ay) * a.width - ax;
|
|
1451
|
+
const rowB = (y - by) * b.width - bx;
|
|
1452
|
+
for (let x = x0; x < x1; x++) if (a.mask[rowA + x] === 1 && b.mask[rowB + x] === 1) return true;
|
|
1453
|
+
}
|
|
1454
|
+
return false;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* MaxRects' split of every free rectangle `put` overlaps, then its prune —
|
|
1459
|
+
* `packOnePage`'s, with one saving that changes no rectangle and no order: a
|
|
1460
|
+
* free rectangle `put` did not touch is never tested for containment. Before
|
|
1461
|
+
* the split no free rectangle lay inside another (the prune's own
|
|
1462
|
+
* postcondition), and every new piece lies inside the rectangle it was cut
|
|
1463
|
+
* from, so an untouched rectangle inside a new piece would have lain inside
|
|
1464
|
+
* that one — so the test `packOnePage` makes for it always answers "not
|
|
1465
|
+
* contained", and skipping it keeps the list exactly as that prune leaves it.
|
|
1466
|
+
*
|
|
1467
|
+
* ⭐ And a second (issue #1099, measured on production rigs): a free rectangle
|
|
1468
|
+
* narrower than `minW` or shorter than `minH` is dropped. The caller passes the
|
|
1469
|
+
* least bounding box of every cell the pass places, so such a rectangle can
|
|
1470
|
+
* never be a candidate, and neither can any piece later cut from it (a piece
|
|
1471
|
+
* lies inside the rectangle it was cut from); and a rectangle a dropped one
|
|
1472
|
+
* contains is itself too small. So every rectangle that could ever be a
|
|
1473
|
+
* candidate is kept, in the order the full prune keeps it, and the decisions
|
|
1474
|
+
* are the same. What it removes is the staircase of slivers an irregular hull's
|
|
1475
|
+
* bands cut along its edge: on a production-shaped set (30 regions, 200 to 900
|
|
1476
|
+
* px, hulls near their rectangles, a `free` page about 2023x2046) the pack
|
|
1477
|
+
* took 18.5 s with the slivers kept and spent 95 % of it in this prune.
|
|
1478
|
+
*/
|
|
1479
|
+
function splitFree(free: Rect[], put: Rect, minW = 1, minH = 1, edgeIndex = true, tally?: PassTally): void {
|
|
1480
|
+
const next: Rect[] = [];
|
|
1481
|
+
/** Per entry of `next`: `UNTOUCHED`, or which side of `put` the piece was cut from. */
|
|
1482
|
+
const side: number[] = [];
|
|
1483
|
+
for (const fr of free) {
|
|
1484
|
+
const overlaps = put.x < fr.x + fr.w && put.x + put.w > fr.x && put.y < fr.y + fr.h && put.y + put.h > fr.y;
|
|
1485
|
+
if (!overlaps) {
|
|
1486
|
+
next.push(fr);
|
|
1487
|
+
side.push(UNTOUCHED);
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
if (put.x > fr.x) {
|
|
1491
|
+
next.push({ x: fr.x, y: fr.y, w: put.x - fr.x, h: fr.h });
|
|
1492
|
+
side.push(LEFT_OF_PUT);
|
|
1493
|
+
}
|
|
1494
|
+
if (put.x + put.w < fr.x + fr.w) {
|
|
1495
|
+
next.push({ x: put.x + put.w, y: fr.y, w: fr.x + fr.w - (put.x + put.w), h: fr.h });
|
|
1496
|
+
side.push(RIGHT_OF_PUT);
|
|
1497
|
+
}
|
|
1498
|
+
if (put.y > fr.y) {
|
|
1499
|
+
next.push({ x: fr.x, y: fr.y, w: fr.w, h: put.y - fr.y });
|
|
1500
|
+
side.push(ABOVE_PUT);
|
|
1501
|
+
}
|
|
1502
|
+
if (put.y + put.h < fr.y + fr.h) {
|
|
1503
|
+
next.push({ x: fr.x, y: put.y + put.h, w: fr.w, h: fr.y + fr.h - (put.y + put.h) });
|
|
1504
|
+
side.push(BELOW_PUT);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
const contains = (a: Rect, b: Rect): boolean => b.x >= a.x && b.y >= a.y && b.x + b.w <= a.x + a.w && b.y + b.h <= a.y + a.h;
|
|
1508
|
+
const usable = (r: Rect): boolean => r.w >= minW && r.h >= minH;
|
|
1509
|
+
// ⭐ The third saving (issue #1102): a piece's containers are looked for only
|
|
1510
|
+
// among the entries that share the edge it was cut along. A piece cut from
|
|
1511
|
+
// the left of `put` ends at `put.x` and keeps its parent's rows, which meet
|
|
1512
|
+
// `put`'s rows (the parent overlapped `put`). A rectangle containing it covers
|
|
1513
|
+
// those rows from the piece's left edge to at least `put.x`; if it reached
|
|
1514
|
+
// past `put.x` it would overlap `put` — and no entry of `next` does (an
|
|
1515
|
+
// untouched rectangle by definition, a piece by construction). So every
|
|
1516
|
+
// container of a left piece ENDS at `put.x`; likewise a right piece's starts
|
|
1517
|
+
// at `put.x + put.w`, an upper piece's ends at `put.y` and a lower piece's
|
|
1518
|
+
// starts at `put.y + put.h`. The test is the full prune's test, asked of the
|
|
1519
|
+
// only entries that can answer it yes, so `contained` is the same boolean and
|
|
1520
|
+
// the list the same list in the same order. Measured where the footprint
|
|
1521
|
+
// pass does work (`fixtures/polypack_shapes.ts`, seed 1105, the searches a
|
|
1522
|
+
// `polygon` pack of it runs): 415,916,651 containment tests by the full scan,
|
|
1523
|
+
// 15,407,179 by the index, 3.2 s → 2.3 s; with the early stop off as well,
|
|
1524
|
+
// 11,928,721,590 → 537,215,014 and 101 s → 14.5 s. `edgeIndex: false` is
|
|
1525
|
+
// that scan, kept so the selftest can hold that the index finds what the
|
|
1526
|
+
// scan finds (`PK94`) and for nothing else.
|
|
1527
|
+
const scan: number[][] = [[], [], [], []];
|
|
1528
|
+
const all: number[] = [];
|
|
1529
|
+
for (let j = 0; j < next.length; j++) {
|
|
1530
|
+
const r = next[j];
|
|
1531
|
+
if (!usable(r)) continue;
|
|
1532
|
+
if (!edgeIndex) {
|
|
1533
|
+
all.push(j);
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
if (r.x + r.w === put.x) scan[LEFT_OF_PUT].push(j);
|
|
1537
|
+
if (r.x === put.x + put.w) scan[RIGHT_OF_PUT].push(j);
|
|
1538
|
+
if (r.y + r.h === put.y) scan[ABOVE_PUT].push(j);
|
|
1539
|
+
if (r.y === put.y + put.h) scan[BELOW_PUT].push(j);
|
|
1540
|
+
}
|
|
1541
|
+
free.length = 0;
|
|
1542
|
+
let tests = 0;
|
|
1543
|
+
for (let i = 0; i < next.length; i++) {
|
|
1544
|
+
if (!usable(next[i])) continue;
|
|
1545
|
+
if (side[i] === UNTOUCHED) {
|
|
1546
|
+
free.push(next[i]);
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
let contained = false;
|
|
1550
|
+
for (const j of edgeIndex ? scan[side[i]] : all) {
|
|
1551
|
+
if (i === j) continue;
|
|
1552
|
+
tests++;
|
|
1553
|
+
if (contains(next[j], next[i]) && (j < i || !contains(next[i], next[j]))) {
|
|
1554
|
+
contained = true;
|
|
1555
|
+
break;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
if (!contained) free.push(next[i]);
|
|
1559
|
+
}
|
|
1560
|
+
if (tally !== undefined) {
|
|
1561
|
+
tally.bandSplits++;
|
|
1562
|
+
tally.containmentTests += tests;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
/** Which side of the placed rectangle a free-list piece was cut from (`splitFree`) — an index into its edge lists. */
|
|
1567
|
+
const LEFT_OF_PUT = 0;
|
|
1568
|
+
const RIGHT_OF_PUT = 1;
|
|
1569
|
+
const ABOVE_PUT = 2;
|
|
1570
|
+
const BELOW_PUT = 3;
|
|
1571
|
+
const UNTOUCHED = -1;
|
|
1572
|
+
|
|
1573
|
+
/**
|
|
1574
|
+
* `packOnePage` with the free-space test replaced by the footprint test
|
|
1575
|
+
* (issue #1099) — the placement `shape: 'polygon'` makes.
|
|
1576
|
+
*
|
|
1577
|
+
* What changes is what the free list is the free space OF. `packOnePage`
|
|
1578
|
+
* splits it by each placed cell; this splits it by each placed cell's
|
|
1579
|
+
* protected set rounded out to bands of `FOOTPRINT_BAND_ROWS` rows
|
|
1580
|
+
* (`CellShape.rects`), so a free rectangle may lie inside an earlier cell
|
|
1581
|
+
* wherever that cell's region draws nothing. The bands only make the free list
|
|
1582
|
+
* coarser — a free rectangle is still clear of every owned texel — and they
|
|
1583
|
+
* are what keeps it short: per row, a 60-part set of ellipse hulls packed on a
|
|
1584
|
+
* `free` page in over ten minutes; in bands of 8 rows, in 14.9 s against the
|
|
1585
|
+
* rectangle pass's 0.6 s, the same page — and with `splitFree`'s sliver drop
|
|
1586
|
+
* beside them, in 5.8 s against 2.2 s (a loaded machine). Everything else is
|
|
1587
|
+
* MaxRects as `packOnePage` runs it:
|
|
1588
|
+
*
|
|
1589
|
+
* * a candidate is a free rectangle that holds the cell's protected set's
|
|
1590
|
+
* bounding box, anchored at its top-left corner — the cell placed so that
|
|
1591
|
+
* box's corner lands there, which may put the cell's own unclaimed
|
|
1592
|
+
* texels over a neighbour's — and whose cell stays on the page;
|
|
1593
|
+
* * the score is Best Short Side Fit over that box, with the same total
|
|
1594
|
+
* tie-break (smallest long-side leftover, then topmost, then leftmost);
|
|
1595
|
+
* * the free list is split and pruned by the same code (`splitFree`), once
|
|
1596
|
+
* per rectangle of the protected set.
|
|
1597
|
+
*
|
|
1598
|
+
* ⇒ For a cell whose set is the whole cell (`CellShape.whole`), the box IS the
|
|
1599
|
+
* cell, the anchor IS the free rectangle's corner and the split IS the cell's:
|
|
1600
|
+
* a pack with no mesh region makes `packOnePage`'s decisions one for one
|
|
1601
|
+
* (`PK82`).
|
|
1602
|
+
*
|
|
1603
|
+
* 🔒 **The footprint test is then stated outright rather than trusted to the
|
|
1604
|
+
* bookkeeping.** A candidate inside a free rectangle cannot meet a placed set,
|
|
1605
|
+
* because no free rectangle overlaps one; every placement is nevertheless
|
|
1606
|
+
* checked texel for texel against every set already placed, and a meeting is
|
|
1607
|
+
* an internal error, not a placement.
|
|
1608
|
+
*
|
|
1609
|
+
* It terminates and is deterministic for `packOnePage`'s reasons: one pass
|
|
1610
|
+
* over the cells in the caller's order, each over a finite free list, every
|
|
1611
|
+
* choice made by a total order, and no input but the cells and the page.
|
|
1612
|
+
*/
|
|
1613
|
+
function packOnePageByFootprint(
|
|
1614
|
+
shapes: readonly CellShape[],
|
|
1615
|
+
pageW: number,
|
|
1616
|
+
pageH: number,
|
|
1617
|
+
maxBottom = Infinity,
|
|
1618
|
+
stopAtMiss = false,
|
|
1619
|
+
edgeIndex = true,
|
|
1620
|
+
tally?: PassTally,
|
|
1621
|
+
): Array<Rect | null> {
|
|
1622
|
+
const free: Rect[] = [{ x: 0, y: 0, w: pageW, h: pageH }];
|
|
1623
|
+
const placed: Array<Rect | null> = [];
|
|
1624
|
+
const owned: Array<{ shape: CellShape; x: number; y: number }> = [];
|
|
1625
|
+
// The least box any cell of this pass needs — what a free rectangle must
|
|
1626
|
+
// hold to be a candidate for anything (`splitFree`, the second saving).
|
|
1627
|
+
let minW = Infinity;
|
|
1628
|
+
let minH = Infinity;
|
|
1629
|
+
for (const shape of shapes) {
|
|
1630
|
+
minW = Math.min(minW, shape.bbox.w);
|
|
1631
|
+
minH = Math.min(minH, shape.bbox.h);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
for (const shape of shapes) {
|
|
1635
|
+
const box = shape.bbox;
|
|
1636
|
+
let best: Rect | null = null;
|
|
1637
|
+
let bestShort = Infinity;
|
|
1638
|
+
let bestLong = Infinity;
|
|
1639
|
+
for (const fr of free) {
|
|
1640
|
+
if (fr.w < box.w || fr.h < box.h) continue;
|
|
1641
|
+
const x = fr.x - box.x;
|
|
1642
|
+
const y = fr.y - box.y;
|
|
1643
|
+
if (x < 0 || y < 0 || x + shape.width > pageW || y + shape.height > pageH) continue;
|
|
1644
|
+
const leftoverW = fr.w - box.w;
|
|
1645
|
+
const leftoverH = fr.h - box.h;
|
|
1646
|
+
const short = Math.min(leftoverW, leftoverH);
|
|
1647
|
+
const long = Math.max(leftoverW, leftoverH);
|
|
1648
|
+
if (best !== null) {
|
|
1649
|
+
if (short > bestShort) continue;
|
|
1650
|
+
if (short === bestShort) {
|
|
1651
|
+
if (long > bestLong) continue;
|
|
1652
|
+
if (long === bestLong) {
|
|
1653
|
+
if (fr.y > best.y) continue;
|
|
1654
|
+
if (fr.y === best.y && fr.x >= best.x) continue;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
best = fr;
|
|
1659
|
+
bestShort = short;
|
|
1660
|
+
bestLong = long;
|
|
1661
|
+
}
|
|
1662
|
+
if (best === null) {
|
|
1663
|
+
placed.push(null);
|
|
1664
|
+
// A caller that keeps only a pass placing every cell has its answer
|
|
1665
|
+
// (`placePass`, `wholeOrNothing`): the rest are not placed.
|
|
1666
|
+
if (stopAtMiss) {
|
|
1667
|
+
while (placed.length < shapes.length) placed.push(null);
|
|
1668
|
+
return placed;
|
|
1669
|
+
}
|
|
1670
|
+
continue;
|
|
1671
|
+
}
|
|
1672
|
+
const put: Rect = { x: best.x - box.x, y: best.y - box.y, w: shape.width, h: shape.height };
|
|
1673
|
+
for (const other of owned) {
|
|
1674
|
+
// Two cells that do not overlap cannot share a texel, and that answer
|
|
1675
|
+
// takes no texel to read; only an overlap is scanned, and only over the
|
|
1676
|
+
// overlap's own box (`shapesMeet`).
|
|
1677
|
+
const apart =
|
|
1678
|
+
put.x >= other.x + other.shape.width || other.x >= put.x + put.w || put.y >= other.y + other.shape.height || other.y >= put.y + put.h;
|
|
1679
|
+
if (apart) continue;
|
|
1680
|
+
if (shapesMeet(shape, put.x, put.y, other.shape, other.x, other.y)) {
|
|
1681
|
+
throw new CompileError(
|
|
1682
|
+
`internal: the footprint pass placed a ${shape.width}x${shape.height} cell at ${put.x},${put.y} over ` +
|
|
1683
|
+
`the footprint of the ${other.shape.width}x${other.shape.height} cell at ${other.x},${other.y}`,
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
placed.push(put);
|
|
1688
|
+
owned.push({ shape, x: put.x, y: put.y });
|
|
1689
|
+
if (put.y + put.h > maxBottom) {
|
|
1690
|
+
while (placed.length < shapes.length) placed.push(null);
|
|
1691
|
+
return placed;
|
|
1692
|
+
}
|
|
1693
|
+
for (const r of shape.rects) splitFree(free, { x: put.x + r.x, y: put.y + r.y, w: r.w, h: r.h }, minW, minH, edgeIndex, tally);
|
|
1694
|
+
}
|
|
1695
|
+
return placed;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
const isObject = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
1699
|
+
const isNumberList = (v: unknown): v is number[] => Array.isArray(v) && v.every((n) => typeof n === 'number' && Number.isFinite(n));
|
|
1700
|
+
|
|
1701
|
+
/**
|
|
1702
|
+
* What every packed region's attachments draw, read off the `skeleton.json` the
|
|
1703
|
+
* build emitted — the footprints `shape: 'polygon'` packs by (issue #1099).
|
|
1704
|
+
*
|
|
1705
|
+
* The emitted text and not the model, because the footprint has to be what the
|
|
1706
|
+
* runtime will sample: the mesh's UVs as the file writes them.
|
|
1707
|
+
*
|
|
1708
|
+
* * The region an attachment samples is its `path`, else its `name`, else its
|
|
1709
|
+
* key — the runtime's rule — and a `sequence` samples `that + (start +
|
|
1710
|
+
* frame)` zero-padded to `digits`, for every frame (`start` 1 and `digits` 0
|
|
1711
|
+
* when the file omits them, the parser's defaults).
|
|
1712
|
+
* * A **region** attachment's footprint is its rectangle (`null` here).
|
|
1713
|
+
* * A **mesh**'s is the polygon of its first `hull` vertices, closed from the
|
|
1714
|
+
* last back to the first, and each of its triangles — the hull is the outer
|
|
1715
|
+
* loop of the triangles' union, so for a well-formed mesh the triangles add
|
|
1716
|
+
* nothing, and for one whose triangles reach outside its loop they protect
|
|
1717
|
+
* what is drawn. In the region's own texels: `(u · width, v · height)`.
|
|
1718
|
+
* * A **linked mesh** draws its source's geometry over its own region: the
|
|
1719
|
+
* mesh keyed `source` (`parent` before 4.3) in the slot `slot` names (its
|
|
1720
|
+
* own by default) of the skin `skin` names (`default` by default).
|
|
1721
|
+
* * A region with **any** rectangle use is a rectangle, and so is every
|
|
1722
|
+
* region whose drawing cannot be read as a polygon here — a mesh whose UVs
|
|
1723
|
+
* leave 0..1 (it samples outside its own rectangle, which only the
|
|
1724
|
+
* rectangle keeps the same), a hull or triangle list that does not index its
|
|
1725
|
+
* vertices, a link whose source is not a mesh. Several meshes over one
|
|
1726
|
+
* region give it the union of their polygons.
|
|
1727
|
+
*
|
|
1728
|
+
* Returns, per region name, its footprint, or `null` for a rectangle; a region
|
|
1729
|
+
* no attachment names is absent, and the packer treats it as a rectangle.
|
|
1730
|
+
* Nothing read here can be invented: every fallback is the rectangle, which is
|
|
1731
|
+
* the footprint `shape: 'rect'` gives everything.
|
|
1732
|
+
*/
|
|
1733
|
+
export function packFootprints(
|
|
1734
|
+
skeletonText: string,
|
|
1735
|
+
sizeOf: (region: string) => { width: number; height: number } | undefined,
|
|
1736
|
+
): Map<string, PackFootprint | null> {
|
|
1737
|
+
const doc: unknown = JSON.parse(skeletonText);
|
|
1738
|
+
const skins = isObject(doc) && Array.isArray(doc.skins) ? doc.skins : [];
|
|
1739
|
+
const tableOf = (skin: string, slot: string): Record<string, unknown> | undefined => {
|
|
1740
|
+
for (const s of skins) {
|
|
1741
|
+
if (!isObject(s) || s.name !== skin || !isObject(s.attachments)) continue;
|
|
1742
|
+
const table = s.attachments[slot];
|
|
1743
|
+
return isObject(table) ? table : undefined;
|
|
1744
|
+
}
|
|
1745
|
+
return undefined;
|
|
1746
|
+
};
|
|
1747
|
+
/** A mesh's UV polygons — hull loop, then triangles — or null when they cannot be read as polygons over its own rectangle. */
|
|
1748
|
+
const uvPolygonsOf = (mesh: Record<string, unknown>): number[][] | null => {
|
|
1749
|
+
const { uvs, hull, triangles } = mesh;
|
|
1750
|
+
if (!isNumberList(uvs) || uvs.length % 2 !== 0 || typeof hull !== 'number' || !isNumberList(triangles)) return null;
|
|
1751
|
+
const vertices = uvs.length / 2;
|
|
1752
|
+
if (!Number.isInteger(hull) || hull < 3 || hull > vertices || triangles.length % 3 !== 0) return null;
|
|
1753
|
+
if (uvs.some((n) => n < 0 || n > 1)) return null;
|
|
1754
|
+
if (triangles.some((t) => !Number.isInteger(t) || t < 0 || t >= vertices)) return null;
|
|
1755
|
+
const out: number[][] = [uvs.slice(0, 2 * hull)];
|
|
1756
|
+
for (let t = 0; t < triangles.length; t += 3) {
|
|
1757
|
+
const [a, b, c] = [triangles[t], triangles[t + 1], triangles[t + 2]];
|
|
1758
|
+
out.push([uvs[2 * a], uvs[2 * a + 1], uvs[2 * b], uvs[2 * b + 1], uvs[2 * c], uvs[2 * c + 1]]);
|
|
1759
|
+
}
|
|
1760
|
+
return out;
|
|
1761
|
+
};
|
|
1762
|
+
const uses = new Map<string, number[][][] | null>();
|
|
1763
|
+
const use = (region: string, polygons: number[][] | null): void => {
|
|
1764
|
+
const held = uses.get(region);
|
|
1765
|
+
if (held === null) return;
|
|
1766
|
+
if (polygons === null) uses.set(region, null);
|
|
1767
|
+
else if (held === undefined) uses.set(region, [polygons]);
|
|
1768
|
+
else held.push(polygons);
|
|
1769
|
+
};
|
|
1770
|
+
for (const s of skins) {
|
|
1771
|
+
if (!isObject(s) || !isObject(s.attachments)) continue;
|
|
1772
|
+
for (const [slot, table] of Object.entries(s.attachments)) {
|
|
1773
|
+
if (!isObject(table)) continue;
|
|
1774
|
+
for (const [key, att] of Object.entries(table)) {
|
|
1775
|
+
if (!isObject(att)) continue;
|
|
1776
|
+
const type = att.type ?? 'region';
|
|
1777
|
+
if (type !== 'region' && type !== 'mesh' && type !== 'linkedmesh') continue;
|
|
1778
|
+
const base = typeof att.path === 'string' ? att.path : typeof att.name === 'string' ? att.name : key;
|
|
1779
|
+
const regions: string[] = [];
|
|
1780
|
+
const seq = att.sequence;
|
|
1781
|
+
if (isObject(seq) && typeof seq.count === 'number') {
|
|
1782
|
+
const start = typeof seq.start === 'number' ? seq.start : 1;
|
|
1783
|
+
const digits = typeof seq.digits === 'number' ? seq.digits : 0;
|
|
1784
|
+
for (let f = 0; f < seq.count; f++) regions.push(base + String(start + f).padStart(digits, '0'));
|
|
1785
|
+
} else regions.push(base);
|
|
1786
|
+
let polygons: number[][] | null = null;
|
|
1787
|
+
if (type === 'mesh') polygons = uvPolygonsOf(att);
|
|
1788
|
+
else if (type === 'linkedmesh') {
|
|
1789
|
+
const sourceKey = typeof att.source === 'string' ? att.source : typeof att.parent === 'string' ? att.parent : null;
|
|
1790
|
+
const source =
|
|
1791
|
+
sourceKey === null
|
|
1792
|
+
? undefined
|
|
1793
|
+
: tableOf(typeof att.skin === 'string' ? att.skin : 'default', typeof att.slot === 'string' ? att.slot : slot)?.[sourceKey];
|
|
1794
|
+
polygons = isObject(source) && source.type === 'mesh' ? uvPolygonsOf(source) : null;
|
|
1795
|
+
}
|
|
1796
|
+
for (const region of regions) use(region, polygons);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
const out = new Map<string, PackFootprint | null>();
|
|
1801
|
+
for (const [region, held] of uses) {
|
|
1802
|
+
const size = sizeOf(region);
|
|
1803
|
+
if (held === null || size === undefined) {
|
|
1804
|
+
out.set(region, null);
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
const polygons = held.flat().map((poly) => poly.map((n, i) => n * (i % 2 === 0 ? size.width : size.height)));
|
|
1808
|
+
out.set(region, { polygons });
|
|
1809
|
+
}
|
|
1810
|
+
return out;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* Copy the texels a region owns (`CellShape.mask`) onto a page, with the
|
|
1815
|
+
* values `extrudeCell` gives them — the second of `shape: 'polygon'`'s two
|
|
1816
|
+
* drawing passes (see `packAtlas`).
|
|
1817
|
+
*/
|
|
1818
|
+
function extrudeOwned(page: Plate, source: Plate, cellX: number, cellY: number, padding: number, shape: CellShape): void {
|
|
1819
|
+
const w = source.width;
|
|
1820
|
+
const h = source.height;
|
|
1821
|
+
const dst = page.data;
|
|
1822
|
+
const src = source.data;
|
|
1823
|
+
for (let cy = 0; cy < shape.height; cy++) {
|
|
1824
|
+
const sy = Math.max(0, Math.min(h - 1, cy - padding));
|
|
1825
|
+
const srcRow = sy * w * 4;
|
|
1826
|
+
const dstRow = (cellY + cy) * page.width * 4;
|
|
1827
|
+
for (let cx = 0; cx < shape.width; cx++) {
|
|
1828
|
+
if (shape.mask[cy * shape.width + cx] === 0) continue;
|
|
1829
|
+
const sx = Math.max(0, Math.min(w - 1, cx - padding));
|
|
1830
|
+
const s = srcRow + sx * 4;
|
|
1831
|
+
const d = dstRow + (cellX + cx) * 4;
|
|
1832
|
+
dst[d] = src[s];
|
|
1833
|
+
dst[d + 1] = src[s + 1];
|
|
1834
|
+
dst[d + 2] = src[s + 2];
|
|
1835
|
+
dst[d + 3] = src[s + 3];
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1033
1840
|
/**
|
|
1034
1841
|
* The packing order, and it is stated rather than inherited.
|
|
1035
1842
|
*
|
|
@@ -1144,6 +1951,36 @@ function extrudeCell(page: Plate, source: Plate, cellX: number, cellY: number, p
|
|
|
1144
1951
|
* against the loose build. It does not go past it, and no region's bytes change
|
|
1145
1952
|
* (`PK02`'s lift-back holds under both).
|
|
1146
1953
|
*
|
|
1954
|
+
* ## `shape: 'polygon'` — rectangles that overlap where nothing is drawn (issue #1099)
|
|
1955
|
+
*
|
|
1956
|
+
* Opt-in, and the default stays `rect`, which is this function exactly as it
|
|
1957
|
+
* was before the option: under `rect` no footprint is computed and every pass
|
|
1958
|
+
* is `packOnePage`. Under `polygon` every input carries the footprint its
|
|
1959
|
+
* attachments draw (`packFootprints`: a mesh's emitted hull, a region
|
|
1960
|
+
* attachment's rectangle), and:
|
|
1961
|
+
*
|
|
1962
|
+
* * **what a cell owns** is `footprintCell`'s set — the footprint grown by the
|
|
1963
|
+
* padding, clipped to the cell; two cells may overlap when they own no texel
|
|
1964
|
+
* in common, which between two rectangles is `rect`'s rule and between any
|
|
1965
|
+
* two footprints keeps them `2 · padding` apart (`CellShape`);
|
|
1966
|
+
* * **the placement** is `packOnePageByFootprint`, MaxRects with the free list
|
|
1967
|
+
* split by what each cell owns, run beside the rectangle pass on every page
|
|
1968
|
+
* tried and kept only when it is better (`placePass`) — so a `polygon` page
|
|
1969
|
+
* is never larger than the `rect` page, and a set with no mesh footprint is
|
|
1970
|
+
* placed exactly as under `rect` (`PK82`);
|
|
1971
|
+
* * **the pixels** are drawn in two passes: every cell whole in packing order,
|
|
1972
|
+
* as under `rect`, then every region's owned texels again with its own
|
|
1973
|
+
* values. The owned sets are disjoint, so the second pass's order decides
|
|
1974
|
+
* nothing and every texel a region can sample is its own (`PK79`); a texel
|
|
1975
|
+
* nobody owns carries the last cell drawn over it, which nothing samples.
|
|
1976
|
+
*
|
|
1977
|
+
* What it costs is measured, not conceded: a region that moves samples through
|
|
1978
|
+
* a different `x / pageWidth`, so on the two gallery rigs whose pack moved,
|
|
1979
|
+
* 207 and 36 channel samples differ from the `rect` render, every one by 1 —
|
|
1980
|
+
* the bit two `rect` packs of the same rig differ by when one is `pot` and one
|
|
1981
|
+
* `free` (155 and 44). `--page-edges`, the width search and the spill rule are
|
|
1982
|
+
* unchanged.
|
|
1983
|
+
*
|
|
1147
1984
|
* ⚠️ `pot` is not the smaller answer's poor relation, and its search was never
|
|
1148
1985
|
* "double until it fits": it tries every power-of-two pair in order of area.
|
|
1149
1986
|
* On both rigs above the cells' own area (1,206,755 and 540,793 texels at
|
|
@@ -1158,6 +1995,10 @@ export function packAtlas(inputs: PackInput[], opts: PackOptions = {}): PackResu
|
|
|
1158
1995
|
if (!PAGE_EDGES.includes(pageEdges)) {
|
|
1159
1996
|
throw new CompileError(`--page-edges ${JSON.stringify(String(opts.pageEdges))}; known values: ${PAGE_EDGES.join(', ')}`);
|
|
1160
1997
|
}
|
|
1998
|
+
const shape = opts.shape ?? DEFAULT_PACK_SHAPE;
|
|
1999
|
+
if (!PACK_SHAPES.includes(shape)) {
|
|
2000
|
+
throw new CompileError(`--pack-shape ${JSON.stringify(String(opts.shape))}; known values: ${PACK_SHAPES.join(', ')}`);
|
|
2001
|
+
}
|
|
1161
2002
|
if (!Number.isInteger(pageSize) || pageSize < 1) {
|
|
1162
2003
|
throw new CompileError(`--page-size must be a positive integer, got ${String(opts.pageSize)}`);
|
|
1163
2004
|
}
|
|
@@ -1181,7 +2022,12 @@ export function packAtlas(inputs: PackInput[], opts: PackOptions = {}): PackResu
|
|
|
1181
2022
|
);
|
|
1182
2023
|
}
|
|
1183
2024
|
|
|
1184
|
-
|
|
2025
|
+
// `polygon` only: every cell's protected set. Under `rect` there are none and
|
|
2026
|
+
// every pass below is the rectangle pass, the code it was before #1099.
|
|
2027
|
+
const shapes =
|
|
2028
|
+
shape === 'polygon' ? sorted.map((input) => footprintCell(input.width, input.height, padding, input.footprint)) : undefined;
|
|
2029
|
+
|
|
2030
|
+
const single = smallestPageFor(cells, maxEdge, pageEdges, shapes);
|
|
1185
2031
|
|
|
1186
2032
|
/** page index -> the placements on it, in packing order. */
|
|
1187
2033
|
const perPage: Placement[][] = [];
|
|
@@ -1208,15 +2054,18 @@ export function packAtlas(inputs: PackInput[], opts: PackOptions = {}): PackResu
|
|
|
1208
2054
|
// what makes the boundary deterministic and independent of the shrink below
|
|
1209
2055
|
// — and parts are taken in packing order, whatever will not fit the current
|
|
1210
2056
|
// page opening the next one.
|
|
1211
|
-
let remaining = sorted.map((input, i) => ({ input, cell: cells[i] }));
|
|
2057
|
+
let remaining = sorted.map((input, i) => ({ input, cell: cells[i], shape: shapes?.[i] }));
|
|
2058
|
+
const shapesOf = (list: typeof remaining): CellShape[] | undefined =>
|
|
2059
|
+
shapes === undefined ? undefined : list.map((r) => r.shape ?? footprintCell(r.input.width, r.input.height, padding));
|
|
1212
2060
|
while (remaining.length > 0) {
|
|
1213
2061
|
const pageIndex = perPage.length;
|
|
1214
|
-
const attempt =
|
|
2062
|
+
const attempt = placePass(
|
|
1215
2063
|
remaining.map((r) => r.cell),
|
|
2064
|
+
shapesOf(remaining),
|
|
1216
2065
|
maxEdge,
|
|
1217
2066
|
maxEdge,
|
|
1218
2067
|
);
|
|
1219
|
-
const onPage:
|
|
2068
|
+
const onPage: typeof remaining = [];
|
|
1220
2069
|
const leftOver: typeof remaining = [];
|
|
1221
2070
|
attempt.forEach((rect, i) => {
|
|
1222
2071
|
if (rect === null) leftOver.push(remaining[i]);
|
|
@@ -1241,6 +2090,7 @@ export function packAtlas(inputs: PackInput[], opts: PackOptions = {}): PackResu
|
|
|
1241
2090
|
onPage.map((r) => r.cell),
|
|
1242
2091
|
maxEdge,
|
|
1243
2092
|
pageEdges,
|
|
2093
|
+
shapesOf(onPage),
|
|
1244
2094
|
);
|
|
1245
2095
|
if (shrunk === null) {
|
|
1246
2096
|
// Unreachable for the same reason as the stall above: these cells were
|
|
@@ -1269,12 +2119,16 @@ export function packAtlas(inputs: PackInput[], opts: PackOptions = {}): PackResu
|
|
|
1269
2119
|
// Draw. Reading each source once, in packing order, keeps the decode count at
|
|
1270
2120
|
// one per part whatever the page layout turned out to be.
|
|
1271
2121
|
const byRegion = new Map(sorted.map((input) => [input.region, input]));
|
|
2122
|
+
const shapeByRegion = new Map<string, CellShape>();
|
|
2123
|
+
if (shapes !== undefined) sorted.forEach((input, i) => shapeByRegion.set(input.region, shapes[i]));
|
|
1272
2124
|
const pages: PackedPage[] = [];
|
|
1273
2125
|
const emitPages: EmitPage[] = [];
|
|
1274
2126
|
perPage.forEach((onPage, index) => {
|
|
1275
2127
|
const { width: pageW, height: pageH } = pageSizes[index];
|
|
1276
2128
|
const plate = new Plate(pageW, pageH);
|
|
1277
2129
|
let covered = 0;
|
|
2130
|
+
/** `polygon` only: each region's source and protected set, for the second pass. */
|
|
2131
|
+
const owners: Array<{ source: Plate; place: Placement; shape: CellShape }> = [];
|
|
1278
2132
|
for (const place of onPage) {
|
|
1279
2133
|
const input = byRegion.get(place.region)!;
|
|
1280
2134
|
const source = readPlate(input.absPath);
|
|
@@ -1289,6 +2143,22 @@ export function packAtlas(inputs: PackInput[], opts: PackOptions = {}): PackResu
|
|
|
1289
2143
|
}
|
|
1290
2144
|
extrudeCell(plate, source, place.x - padding, place.y - padding, padding);
|
|
1291
2145
|
covered += place.width * place.height;
|
|
2146
|
+
const owned = shapeByRegion.get(place.region);
|
|
2147
|
+
if (owned !== undefined) owners.push({ source, place, shape: owned });
|
|
2148
|
+
}
|
|
2149
|
+
// `polygon`'s second pass. The first wrote every cell whole, in packing
|
|
2150
|
+
// order, so where two cells overlap the later one's texels lie on top; this
|
|
2151
|
+
// one writes each region's protected set again, with its own values. The
|
|
2152
|
+
// sets are pairwise disjoint (the footprint test), so the order of this pass
|
|
2153
|
+
// decides nothing, and every texel a region owns ends as its own: a texel
|
|
2154
|
+
// under some region's footprint carries that region's value, and a texel
|
|
2155
|
+
// under none carries the last cell drawn over it.
|
|
2156
|
+
// Every region, a whole cell's included: a rectangle's texels are what a
|
|
2157
|
+
// later mesh's cell is most likely to have been drawn over. A page whose
|
|
2158
|
+
// every cell is whole had no overlap to undo and is left as the first pass
|
|
2159
|
+
// drew it, which is `rect`'s page.
|
|
2160
|
+
if (owners.some((o) => !o.shape.whole)) {
|
|
2161
|
+
for (const { source, place, shape: owned } of owners) extrudeOwned(plate, source, place.x - padding, place.y - padding, padding, owned);
|
|
1292
2162
|
}
|
|
1293
2163
|
const name = index === 0 ? `${stem}.png` : `${stem}${index + 1}.png`;
|
|
1294
2164
|
pages.push({ name, width: pageW, height: pageH, plate, occupancy: covered / (pageW * pageH) });
|
|
@@ -1318,7 +2188,7 @@ export function packAtlas(inputs: PackInput[], opts: PackOptions = {}): PackResu
|
|
|
1318
2188
|
});
|
|
1319
2189
|
});
|
|
1320
2190
|
|
|
1321
|
-
return { pages, placements, atlasText: writeAtlasText(emitPages), padding };
|
|
2191
|
+
return { pages, placements, atlasText: writeAtlasText(emitPages), padding, shape };
|
|
1322
2192
|
}
|
|
1323
2193
|
|
|
1324
2194
|
// ---------------------------------------------------------------------------
|