@mark1russell7/layout-contract 0.2.3 → 0.2.5
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/README.md +7 -2
- package/build/index.d.ts +83 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +314 -16
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/hash.ts +198 -0
- package/src/index.ts +1 -0
- package/src/random.ts +2 -13
- package/src/rules.ts +86 -4
- package/src/utf8.ts +12 -0
package/src/rules.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { boundsOf, grow, overlapOf, type Rect, type Region } from "./geometry.ts";
|
|
2
2
|
import type { LayoutBox, LayoutInput, Obstacle } from "./input.ts";
|
|
3
3
|
import { applyPack, type PackOutput } from "./output.ts";
|
|
4
|
-
import { DEFAULT_TOLERANCE, RECORD_EPS } from "./tolerance.ts";
|
|
4
|
+
import { DEFAULT_TOLERANCE, EPS, RECORD_EPS } from "./tolerance.ts";
|
|
5
5
|
|
|
6
6
|
/** This function gives the area of a placed box: its shape or its rectangle, at its position. A box without a position gives `undefined`. */
|
|
7
7
|
export function placedRect(box: LayoutBox): Rect | undefined {
|
|
@@ -35,14 +35,27 @@ export function countOverlaps(boxes: readonly LayoutBox[], tolerance: number = D
|
|
|
35
35
|
return count;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** The options of the container test of `countOutside` and `packHard`. */
|
|
39
|
+
export interface ContainerTest {
|
|
40
|
+
/**
|
|
41
|
+
* With `true`, a box of a shaped container with the size `fixed` must stay inside the shape, not only inside its bounding box.
|
|
42
|
+
* The default is `false`. The default changes to `true` at the next breaking version.
|
|
43
|
+
*/
|
|
44
|
+
readonly exactContainer?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
/**
|
|
39
48
|
* This function counts the placed boxes that go out of the container.
|
|
40
49
|
* Each box with its margin must stay inside the container made smaller by `inset`. Only the fixed sides of the size count. A shaped container counts as its bounding box.
|
|
50
|
+
*
|
|
51
|
+
* With `exactContainer`, a box of a shaped container with the size `fixed` also counts when `rectInRegion` tells that it is not inside the shape.
|
|
52
|
+
* For that test, the box grows by `inset` and becomes smaller by `tolerance` on each side. A shaped container with a free side keeps the bounding test only.
|
|
41
53
|
* An input without a container gives 0.
|
|
42
54
|
*/
|
|
43
|
-
export function countOutside(input: LayoutInput, tolerance: number = DEFAULT_TOLERANCE.overlap): number {
|
|
55
|
+
export function countOutside(input: LayoutInput, tolerance: number = DEFAULT_TOLERANCE.overlap, options: ContainerTest = {}): number {
|
|
44
56
|
const c = input.container;
|
|
45
57
|
if (c === undefined) return 0;
|
|
58
|
+
const shape = options.exactContainer === true && c.fixed === "size" ? c.shape : undefined;
|
|
46
59
|
const area = c.shape === undefined ? { x: 0, y: 0, w: c.w ?? Infinity, h: c.h ?? Infinity } : boundsOf(c.shape);
|
|
47
60
|
const inset = c.inset ?? 0;
|
|
48
61
|
const fixW = c.fixed === "size" || c.fixed === "width";
|
|
@@ -55,6 +68,7 @@ export function countOutside(input: LayoutInput, tolerance: number = DEFAULT_TOL
|
|
|
55
68
|
const outX = r.x < area.x + inset - tolerance || r.x + r.w > area.x + area.w - inset + tolerance;
|
|
56
69
|
const outY = r.y < area.y + inset - tolerance || r.y + r.h > area.y + area.h - inset + tolerance;
|
|
57
70
|
if ((fixW && outX) || (fixH && outY)) count++;
|
|
71
|
+
else if (shape !== undefined && !rectInRegion(shape, shrink(grow(r, inset), tolerance))) count++;
|
|
58
72
|
}
|
|
59
73
|
return count;
|
|
60
74
|
}
|
|
@@ -111,6 +125,72 @@ export function regionHitsRect(region: Region, rect: Rect): boolean {
|
|
|
111
125
|
}
|
|
112
126
|
}
|
|
113
127
|
|
|
128
|
+
/**
|
|
129
|
+
* This function tells if all of `rect` is inside `region`. A point on the border counts as inside. No part of `rect` can overlap a hole.
|
|
130
|
+
* The test of a circle, an ellipse or a polygon accepts an error of `EPS` on the border. A polygon can be concave.
|
|
131
|
+
*/
|
|
132
|
+
export function rectInRegion(region: Region, rect: Rect): boolean {
|
|
133
|
+
const x1 = rect.x + rect.w;
|
|
134
|
+
const y1 = rect.y + rect.h;
|
|
135
|
+
const corners: readonly (readonly [number, number])[] = [
|
|
136
|
+
[rect.x, rect.y],
|
|
137
|
+
[x1, rect.y],
|
|
138
|
+
[x1, y1],
|
|
139
|
+
[rect.x, y1],
|
|
140
|
+
];
|
|
141
|
+
switch (region.kind) {
|
|
142
|
+
case "rect":
|
|
143
|
+
return rect.x >= region.x && rect.y >= region.y && x1 <= region.x + region.w && y1 <= region.y + region.h;
|
|
144
|
+
case "circle":
|
|
145
|
+
return corners.every(([x, y]) => inEllipse(region.cx, region.cy, region.r, region.r, x, y));
|
|
146
|
+
case "ellipse":
|
|
147
|
+
return corners.every(([x, y]) => inEllipse(region.cx, region.cy, region.rx, region.ry, x, y));
|
|
148
|
+
case "polygon": {
|
|
149
|
+
const ring = region.points;
|
|
150
|
+
if (ring.length < 3) return false;
|
|
151
|
+
const points = [...corners, [rect.x + rect.w / 2, rect.y + rect.h / 2] as const];
|
|
152
|
+
if (!points.every(([x, y]) => onRing(x, y, ring) || pointInRings(x, y, [ring]))) return false;
|
|
153
|
+
const deep = (x: number, y: number): boolean => x > rect.x + EPS && x < x1 - EPS && y > rect.y + EPS && y < y1 - EPS;
|
|
154
|
+
for (let i = 0; i < ring.length; i++) {
|
|
155
|
+
const a = ring[i]!;
|
|
156
|
+
const b = ring[(i + 1) % ring.length]!;
|
|
157
|
+
const m = clipMid(a[0], a[1], b[0], b[1], rect);
|
|
158
|
+
if (m !== undefined && deep(m[0], m[1])) return false;
|
|
159
|
+
}
|
|
160
|
+
return (region.holes ?? []).every((hole) => !regionHitsRect({ kind: "polygon", points: hole }, rect));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** This function gives the rectangle made smaller by `d` on each side. A side does not become smaller than 0. */
|
|
166
|
+
function shrink(r: Rect, d: number): Rect {
|
|
167
|
+
const dx = Math.min(d, r.w / 2);
|
|
168
|
+
const dy = Math.min(d, r.h / 2);
|
|
169
|
+
return { x: r.x + dx, y: r.y + dy, w: r.w - 2 * dx, h: r.h - 2 * dy };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** This function tells if a point is inside an ellipse or on its border, with an error of `EPS`. */
|
|
173
|
+
function inEllipse(cx: number, cy: number, rx: number, ry: number, x: number, y: number): boolean {
|
|
174
|
+
if (rx <= 0 || ry <= 0) return false;
|
|
175
|
+
const u = (x - cx) / rx;
|
|
176
|
+
const v = (y - cy) / ry;
|
|
177
|
+
return u * u + v * v <= 1 + EPS;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** This function tells if a point is on a ring of a polygon, with an error of `EPS`. */
|
|
181
|
+
function onRing(x: number, y: number, ring: readonly (readonly [number, number])[]): boolean {
|
|
182
|
+
for (let i = 0; i < ring.length; i++) {
|
|
183
|
+
const [ax, ay] = ring[i]!;
|
|
184
|
+
const [bx, by] = ring[(i + 1) % ring.length]!;
|
|
185
|
+
const dx = bx - ax;
|
|
186
|
+
const dy = by - ay;
|
|
187
|
+
const len2 = dx * dx + dy * dy;
|
|
188
|
+
const t = len2 === 0 ? 0 : Math.max(0, Math.min(1, ((x - ax) * dx + (y - ay) * dy) / len2));
|
|
189
|
+
if (Math.hypot(x - (ax + t * dx), y - (ay + t * dy)) <= EPS) return true;
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
114
194
|
/** An ellipse with the radii `rx` and `ry` scales to a unit circle. The rectangle scales with it. */
|
|
115
195
|
function circleHitsRect(cx: number, cy: number, rx: number, ry: number, rect: Rect): boolean {
|
|
116
196
|
if (rx <= 0 || ry <= 0) return false;
|
|
@@ -190,8 +270,10 @@ export function countObstacleHits(
|
|
|
190
270
|
* A locked box without a placement, or with a move of more than `RECORD_EPS`, breaks `locked-kept`.
|
|
191
271
|
* A locked box without a position in the input is a bad input, not a break. `checkInput` finds it.
|
|
192
272
|
* Another box without a placement that is not in `overflow` breaks `complete`.
|
|
273
|
+
*
|
|
274
|
+
* `options` goes to `countOutside` for `in-container`.
|
|
193
275
|
*/
|
|
194
|
-
export function packHard(input: LayoutInput, output: PackOutput, tolerance: number = DEFAULT_TOLERANCE.overlap): PackHard {
|
|
276
|
+
export function packHard(input: LayoutInput, output: PackOutput, tolerance: number = DEFAULT_TOLERANCE.overlap, options: ContainerTest = {}): PackHard {
|
|
195
277
|
const after = applyPack(input, output);
|
|
196
278
|
const placed = new Set(output.placements.map((p) => p.id));
|
|
197
279
|
const out = new Set(output.overflow);
|
|
@@ -214,7 +296,7 @@ export function packHard(input: LayoutInput, output: PackOutput, tolerance: numb
|
|
|
214
296
|
}
|
|
215
297
|
return {
|
|
216
298
|
"no-overlap": countOverlaps(kept, tolerance) + countObstacleHits(kept, after.obstacles, tolerance),
|
|
217
|
-
"in-container": countOutside({ ...after, boxes: kept }, tolerance),
|
|
299
|
+
"in-container": countOutside({ ...after, boxes: kept }, tolerance, options),
|
|
218
300
|
"locked-kept": lockedMoved,
|
|
219
301
|
complete: incomplete,
|
|
220
302
|
};
|
package/src/utf8.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** This function gives the UTF-8 bytes of a string. The package has no runtime dependencies, thus it does not use `TextEncoder`. */
|
|
2
|
+
export function utf8(text: string): number[] {
|
|
3
|
+
const bytes: number[] = [];
|
|
4
|
+
for (const ch of text) {
|
|
5
|
+
const c = ch.codePointAt(0)!;
|
|
6
|
+
if (c < 0x80) bytes.push(c);
|
|
7
|
+
else if (c < 0x800) bytes.push(0xc0 | (c >> 6), 0x80 | (c & 63));
|
|
8
|
+
else if (c < 0x10000) bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
|
|
9
|
+
else bytes.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
|
|
10
|
+
}
|
|
11
|
+
return bytes;
|
|
12
|
+
}
|