@mark1russell7/layout-contract 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +46 -2
- package/build/index.d.ts +421 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +584 -0
- package/build/index.js.map +1 -0
- package/package.json +57 -3
- package/src/geometry.ts +109 -0
- package/src/index.ts +9 -0
- package/src/input.ts +144 -0
- package/src/measures.ts +53 -0
- package/src/output.ts +58 -0
- package/src/record.ts +83 -0
- package/src/rules.ts +241 -0
- package/src/strategy.ts +20 -0
- package/src/tolerance.ts +25 -0
- package/src/turn.ts +67 -0
package/build/index.js
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
// src/geometry.ts
|
|
2
|
+
/** The sides in clockwise order, from the top. */
|
|
3
|
+
const SIDES = [
|
|
4
|
+
"top",
|
|
5
|
+
"right",
|
|
6
|
+
"bottom",
|
|
7
|
+
"left"
|
|
8
|
+
];
|
|
9
|
+
/** This function gives the value of each side of a margin. No margin gives 0 on each side. */
|
|
10
|
+
function marginSides(margin) {
|
|
11
|
+
if (margin === void 0) return {
|
|
12
|
+
top: 0,
|
|
13
|
+
right: 0,
|
|
14
|
+
bottom: 0,
|
|
15
|
+
left: 0
|
|
16
|
+
};
|
|
17
|
+
if (typeof margin === "number") return {
|
|
18
|
+
top: margin,
|
|
19
|
+
right: margin,
|
|
20
|
+
bottom: margin,
|
|
21
|
+
left: margin
|
|
22
|
+
};
|
|
23
|
+
return margin;
|
|
24
|
+
}
|
|
25
|
+
/** This function gives the rectangle made larger by the margin on each side. */
|
|
26
|
+
function grow(rect, margin) {
|
|
27
|
+
const m = marginSides(margin);
|
|
28
|
+
return {
|
|
29
|
+
x: rect.x - m.left,
|
|
30
|
+
y: rect.y - m.top,
|
|
31
|
+
w: rect.w + m.left + m.right,
|
|
32
|
+
h: rect.h + m.top + m.bottom
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** This function gives the bounding box of a region. */
|
|
36
|
+
function boundsOf(region) {
|
|
37
|
+
switch (region.kind) {
|
|
38
|
+
case "rect": return {
|
|
39
|
+
x: region.x,
|
|
40
|
+
y: region.y,
|
|
41
|
+
w: region.w,
|
|
42
|
+
h: region.h
|
|
43
|
+
};
|
|
44
|
+
case "circle": return {
|
|
45
|
+
x: region.cx - region.r,
|
|
46
|
+
y: region.cy - region.r,
|
|
47
|
+
w: 2 * region.r,
|
|
48
|
+
h: 2 * region.r
|
|
49
|
+
};
|
|
50
|
+
case "ellipse": return {
|
|
51
|
+
x: region.cx - region.rx,
|
|
52
|
+
y: region.cy - region.ry,
|
|
53
|
+
w: 2 * region.rx,
|
|
54
|
+
h: 2 * region.ry
|
|
55
|
+
};
|
|
56
|
+
case "polygon": {
|
|
57
|
+
let x0 = Infinity;
|
|
58
|
+
let y0 = Infinity;
|
|
59
|
+
let x1 = -Infinity;
|
|
60
|
+
let y1 = -Infinity;
|
|
61
|
+
for (const [x, y] of region.points) {
|
|
62
|
+
x0 = Math.min(x0, x);
|
|
63
|
+
y0 = Math.min(y0, y);
|
|
64
|
+
x1 = Math.max(x1, x);
|
|
65
|
+
y1 = Math.max(y1, y);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
x: x0,
|
|
69
|
+
y: y0,
|
|
70
|
+
w: x1 - x0,
|
|
71
|
+
h: y1 - y0
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** This function gives the region moved by `dx` and `dy`. */
|
|
77
|
+
function translate(region, dx, dy) {
|
|
78
|
+
switch (region.kind) {
|
|
79
|
+
case "rect": return {
|
|
80
|
+
...region,
|
|
81
|
+
x: region.x + dx,
|
|
82
|
+
y: region.y + dy
|
|
83
|
+
};
|
|
84
|
+
case "circle":
|
|
85
|
+
case "ellipse": return {
|
|
86
|
+
...region,
|
|
87
|
+
cx: region.cx + dx,
|
|
88
|
+
cy: region.cy + dy
|
|
89
|
+
};
|
|
90
|
+
case "polygon": {
|
|
91
|
+
const move = (ring) => ring.map(([x, y]) => [x + dx, y + dy]);
|
|
92
|
+
return region.holes === void 0 ? {
|
|
93
|
+
kind: "polygon",
|
|
94
|
+
points: move(region.points)
|
|
95
|
+
} : {
|
|
96
|
+
kind: "polygon",
|
|
97
|
+
points: move(region.points),
|
|
98
|
+
holes: region.holes.map(move)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* This function gives the overlap of the insides of two rectangles on each axis.
|
|
105
|
+
* A value of 0 or less on an axis is no overlap. Two rectangles with a common edge have an overlap of 0.
|
|
106
|
+
*/
|
|
107
|
+
function overlapOf(a, b) {
|
|
108
|
+
return {
|
|
109
|
+
x: Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x),
|
|
110
|
+
y: Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// src/tolerance.ts
|
|
114
|
+
/** The epsilon for the float compares in an engine. */
|
|
115
|
+
const EPS = 1e-6;
|
|
116
|
+
/**
|
|
117
|
+
* The epsilon for the float compares on a written file. A file rounds each number to 3 decimals,
|
|
118
|
+
* so a difference of two values can move by up to 0.001.
|
|
119
|
+
*/
|
|
120
|
+
const RECORD_EPS = .001;
|
|
121
|
+
/**
|
|
122
|
+
* The default tolerance of each rule. The packer is exact, so `overlap` is one unit of the rounding.
|
|
123
|
+
* Routes are sampled curves that follow the margin line, so `through` is larger.
|
|
124
|
+
*/
|
|
125
|
+
const DEFAULT_TOLERANCE = Object.freeze({
|
|
126
|
+
overlap: .001,
|
|
127
|
+
through: .5
|
|
128
|
+
});
|
|
129
|
+
// src/input.ts
|
|
130
|
+
/** This function gives the packer input of a file. An input without a container gets `{ fixed: "none" }`, so the packer picks the size. */
|
|
131
|
+
function toPackInput(input) {
|
|
132
|
+
return {
|
|
133
|
+
container: input.container ?? { fixed: "none" },
|
|
134
|
+
items: input.boxes,
|
|
135
|
+
...input.obstacles === void 0 ? {} : { obstacles: input.obstacles },
|
|
136
|
+
...input.gap === void 0 ? {} : { gap: input.gap }
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// src/turn.ts
|
|
140
|
+
/** This function gives the side after a turn of 90 degrees clockwise. */
|
|
141
|
+
function turnSide(side) {
|
|
142
|
+
return SIDES[(SIDES.indexOf(side) + 1) % 4];
|
|
143
|
+
}
|
|
144
|
+
/** This function gives a point of a box of height `h` after the turn. */
|
|
145
|
+
function turnPoint(x, y, h) {
|
|
146
|
+
return [h - y, x];
|
|
147
|
+
}
|
|
148
|
+
/** This function gives a port of a box of height `h` after the turn. Each value of the port turns, also a port with only `x` or only `y`. */
|
|
149
|
+
function turnPort(port, h) {
|
|
150
|
+
const { side, x, y, ...rest } = port;
|
|
151
|
+
return {
|
|
152
|
+
...rest,
|
|
153
|
+
...side === void 0 ? {} : { side: turnSide(side) },
|
|
154
|
+
...y === void 0 ? {} : { x: h - y },
|
|
155
|
+
...x === void 0 ? {} : { y: x }
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** This function gives a margin after the turn: the value of each side moves one side clockwise. */
|
|
159
|
+
function turnMargin(margin) {
|
|
160
|
+
if (typeof margin === "number") return margin;
|
|
161
|
+
const m = marginSides(margin);
|
|
162
|
+
return {
|
|
163
|
+
top: m.left,
|
|
164
|
+
right: m.top,
|
|
165
|
+
bottom: m.right,
|
|
166
|
+
left: m.bottom
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** This function gives a region relative to the origin of a box of height `h` after the turn. */
|
|
170
|
+
function turnRegion(region, h) {
|
|
171
|
+
switch (region.kind) {
|
|
172
|
+
case "rect": return {
|
|
173
|
+
kind: "rect",
|
|
174
|
+
x: h - region.y - region.h,
|
|
175
|
+
y: region.x,
|
|
176
|
+
w: region.h,
|
|
177
|
+
h: region.w
|
|
178
|
+
};
|
|
179
|
+
case "circle": return {
|
|
180
|
+
...region,
|
|
181
|
+
cx: h - region.cy,
|
|
182
|
+
cy: region.cx
|
|
183
|
+
};
|
|
184
|
+
case "ellipse": return {
|
|
185
|
+
kind: "ellipse",
|
|
186
|
+
cx: h - region.cy,
|
|
187
|
+
cy: region.cx,
|
|
188
|
+
rx: region.ry,
|
|
189
|
+
ry: region.rx
|
|
190
|
+
};
|
|
191
|
+
case "polygon": {
|
|
192
|
+
const turn = (ring) => ring.map(([x, y]) => turnPoint(x, y, h));
|
|
193
|
+
return region.holes === void 0 ? {
|
|
194
|
+
kind: "polygon",
|
|
195
|
+
points: turn(region.points)
|
|
196
|
+
} : {
|
|
197
|
+
kind: "polygon",
|
|
198
|
+
points: turn(region.points),
|
|
199
|
+
holes: region.holes.map(turn)
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** This function gives a box after the turn: the size, the ports, the margin and the shape turn. The position stays. */
|
|
205
|
+
function turnBox(box) {
|
|
206
|
+
return {
|
|
207
|
+
...box,
|
|
208
|
+
w: box.h,
|
|
209
|
+
h: box.w,
|
|
210
|
+
...box.ports === void 0 ? {} : { ports: box.ports.map((p) => turnPort(p, box.h)) },
|
|
211
|
+
...box.margin === void 0 ? {} : { margin: turnMargin(box.margin) },
|
|
212
|
+
...box.shape === void 0 ? {} : { shape: turnRegion(box.shape, box.h) }
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// src/output.ts
|
|
216
|
+
/**
|
|
217
|
+
* This function gives the input with the output of a packer applied. Each placed box gets its position.
|
|
218
|
+
* A turned box gets its turned size, ports, margin and shape. Each moved obstacle moves.
|
|
219
|
+
* The container gets the size of the output. The router reads this result. It does not read `turn`.
|
|
220
|
+
*/
|
|
221
|
+
function applyPack(input, output) {
|
|
222
|
+
const at = new Map(output.placements.map((p) => [p.id, p]));
|
|
223
|
+
const moved = new Map(output.moved.map((m) => [m.id, m]));
|
|
224
|
+
const boxes = input.boxes.map((box) => {
|
|
225
|
+
const p = at.get(box.id);
|
|
226
|
+
if (p === void 0) return box;
|
|
227
|
+
return {
|
|
228
|
+
...p.turn === 90 ? turnBox(box) : box,
|
|
229
|
+
x: p.x,
|
|
230
|
+
y: p.y
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
const obstacles = input.obstacles?.map((o) => {
|
|
234
|
+
const m = moved.get(o.id);
|
|
235
|
+
if (m === void 0) return o;
|
|
236
|
+
const b = boundsOf(o.region);
|
|
237
|
+
return {
|
|
238
|
+
...o,
|
|
239
|
+
region: translate(o.region, m.x - b.x, m.y - b.y)
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
...input,
|
|
244
|
+
container: {
|
|
245
|
+
...input.container ?? { fixed: "none" },
|
|
246
|
+
w: output.size.w,
|
|
247
|
+
h: output.size.h
|
|
248
|
+
},
|
|
249
|
+
boxes,
|
|
250
|
+
...obstacles === void 0 ? {} : { obstacles }
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// src/record.ts
|
|
254
|
+
/** This function gives the record of a run. The input gives its id, its hash, its set and the counts of the nodes and the edges. */
|
|
255
|
+
function toLayoutRecord(input, run) {
|
|
256
|
+
return {
|
|
257
|
+
schema: "layout-record/1",
|
|
258
|
+
...run,
|
|
259
|
+
input: input.id,
|
|
260
|
+
inputHash: input.hash ?? "",
|
|
261
|
+
set: input.set ?? "",
|
|
262
|
+
nodes: input.boxes.length,
|
|
263
|
+
edges: input.edges?.length ?? 0
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// src/rules.ts
|
|
267
|
+
/** This function gives the area of a placed box: its shape or its rectangle, at its position. A box without a position gives `undefined`. */
|
|
268
|
+
function placedRect(box) {
|
|
269
|
+
if (box.x === void 0 || box.y === void 0) return void 0;
|
|
270
|
+
if (box.shape === void 0) return {
|
|
271
|
+
x: box.x,
|
|
272
|
+
y: box.y,
|
|
273
|
+
w: box.w,
|
|
274
|
+
h: box.h
|
|
275
|
+
};
|
|
276
|
+
const b = boundsOf(box.shape);
|
|
277
|
+
return {
|
|
278
|
+
x: box.x + b.x,
|
|
279
|
+
y: box.y + b.y,
|
|
280
|
+
w: b.w,
|
|
281
|
+
h: b.h
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* This function counts the pairs of placed boxes whose insides, with their margins, overlap by more than `tolerance` on each axis.
|
|
286
|
+
* A box with a shape counts as its bounding box. That area is larger, thus the count is safe.
|
|
287
|
+
*/
|
|
288
|
+
function countOverlaps(boxes, tolerance = DEFAULT_TOLERANCE.overlap) {
|
|
289
|
+
const rects = [];
|
|
290
|
+
for (const box of boxes) {
|
|
291
|
+
const r = placedRect(box);
|
|
292
|
+
if (r !== void 0) rects.push(grow(r, box.margin));
|
|
293
|
+
}
|
|
294
|
+
rects.sort((a, b) => a.x - b.x);
|
|
295
|
+
let count = 0;
|
|
296
|
+
for (let i = 0; i < rects.length; i++) {
|
|
297
|
+
const a = rects[i];
|
|
298
|
+
for (let j = i + 1; j < rects.length; j++) {
|
|
299
|
+
const b = rects[j];
|
|
300
|
+
if (b.x >= a.x + a.w - tolerance) break;
|
|
301
|
+
const o = overlapOf(a, b);
|
|
302
|
+
if (o.x > tolerance && o.y > tolerance) count++;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return count;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* This function counts the placed boxes that go out of the container.
|
|
309
|
+
* 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.
|
|
310
|
+
* An input without a container gives 0.
|
|
311
|
+
*/
|
|
312
|
+
function countOutside(input, tolerance = DEFAULT_TOLERANCE.overlap) {
|
|
313
|
+
const c = input.container;
|
|
314
|
+
if (c === void 0) return 0;
|
|
315
|
+
const area = c.shape === void 0 ? {
|
|
316
|
+
x: 0,
|
|
317
|
+
y: 0,
|
|
318
|
+
w: c.w ?? Infinity,
|
|
319
|
+
h: c.h ?? Infinity
|
|
320
|
+
} : boundsOf(c.shape);
|
|
321
|
+
const inset = c.inset ?? 0;
|
|
322
|
+
const fixW = c.fixed === "size" || c.fixed === "width";
|
|
323
|
+
const fixH = c.fixed === "size" || c.fixed === "height";
|
|
324
|
+
let count = 0;
|
|
325
|
+
for (const box of input.boxes) {
|
|
326
|
+
const r0 = placedRect(box);
|
|
327
|
+
if (r0 === void 0) continue;
|
|
328
|
+
const r = grow(r0, box.margin);
|
|
329
|
+
const outX = r.x < area.x + inset - tolerance || r.x + r.w > area.x + area.w - inset + tolerance;
|
|
330
|
+
const outY = r.y < area.y + inset - tolerance || r.y + r.h > area.y + area.h - inset + tolerance;
|
|
331
|
+
if (fixW && outX || fixH && outY) count++;
|
|
332
|
+
}
|
|
333
|
+
return count;
|
|
334
|
+
}
|
|
335
|
+
/** This function counts the fixed ports that are not on the border of their box, with the epsilon of a written file. A port without a side does not count. */
|
|
336
|
+
function countPortsOffBorder(boxes, eps = RECORD_EPS) {
|
|
337
|
+
let count = 0;
|
|
338
|
+
for (const box of boxes) for (const p of box.ports ?? []) {
|
|
339
|
+
if (p.x === void 0 || p.y === void 0 || p.side === void 0) continue;
|
|
340
|
+
const along = p.side === "top" || p.side === "bottom" ? p.x : p.y;
|
|
341
|
+
const length = p.side === "top" || p.side === "bottom" ? box.w : box.h;
|
|
342
|
+
const across = {
|
|
343
|
+
top: p.y,
|
|
344
|
+
bottom: p.y - box.h,
|
|
345
|
+
left: p.x,
|
|
346
|
+
right: p.x - box.w
|
|
347
|
+
}[p.side];
|
|
348
|
+
if (Math.abs(across) > eps || along < -eps || along > length + eps) count++;
|
|
349
|
+
}
|
|
350
|
+
return count;
|
|
351
|
+
}
|
|
352
|
+
/** This function tells if the inside of `region` and the inside of `rect` have a common point. */
|
|
353
|
+
function regionHitsRect(region, rect) {
|
|
354
|
+
if (rect.w <= 0 || rect.h <= 0) return false;
|
|
355
|
+
switch (region.kind) {
|
|
356
|
+
case "rect": {
|
|
357
|
+
const o = overlapOf(region, rect);
|
|
358
|
+
return o.x > 0 && o.y > 0;
|
|
359
|
+
}
|
|
360
|
+
case "circle": return circleHitsRect(region.cx, region.cy, region.r, region.r, rect);
|
|
361
|
+
case "ellipse": return circleHitsRect(region.cx, region.cy, region.rx, region.ry, rect);
|
|
362
|
+
case "polygon": {
|
|
363
|
+
const rings = [region.points, ...region.holes ?? []];
|
|
364
|
+
const inside = (x, y) => x > rect.x && x < rect.x + rect.w && y > rect.y && y < rect.y + rect.h;
|
|
365
|
+
if (region.points.some(([x, y]) => inside(x, y))) return true;
|
|
366
|
+
for (const ring of rings) for (let i = 0; i < ring.length; i++) {
|
|
367
|
+
const a = ring[i];
|
|
368
|
+
const b = ring[(i + 1) % ring.length];
|
|
369
|
+
const m = clipMid(a[0], a[1], b[0], b[1], rect);
|
|
370
|
+
if (m !== void 0 && inside(m[0], m[1])) return true;
|
|
371
|
+
}
|
|
372
|
+
return pointInRings(rect.x + rect.w / 2, rect.y + rect.h / 2, rings);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/** An ellipse with the radii `rx` and `ry` scales to a unit circle. The rectangle scales with it. */
|
|
377
|
+
function circleHitsRect(cx, cy, rx, ry, rect) {
|
|
378
|
+
if (rx <= 0 || ry <= 0) return false;
|
|
379
|
+
const x0 = (rect.x - cx) / rx;
|
|
380
|
+
const x1 = (rect.x + rect.w - cx) / rx;
|
|
381
|
+
const y0 = (rect.y - cy) / ry;
|
|
382
|
+
const y1 = (rect.y + rect.h - cy) / ry;
|
|
383
|
+
const dx = Math.max(x0, 0, -x1);
|
|
384
|
+
const dy = Math.max(y0, 0, -y1);
|
|
385
|
+
return dx * dx + dy * dy < 1;
|
|
386
|
+
}
|
|
387
|
+
/** This function gives the middle of the part of a segment inside a rectangle, or `undefined` when that part is a point or empty. */
|
|
388
|
+
function clipMid(ax, ay, bx, by, r) {
|
|
389
|
+
let t0 = 0;
|
|
390
|
+
let t1 = 1;
|
|
391
|
+
const dx = bx - ax;
|
|
392
|
+
const dy = by - ay;
|
|
393
|
+
const edges = [
|
|
394
|
+
[-dx, ax - r.x],
|
|
395
|
+
[dx, r.x + r.w - ax],
|
|
396
|
+
[-dy, ay - r.y],
|
|
397
|
+
[dy, r.y + r.h - ay]
|
|
398
|
+
];
|
|
399
|
+
for (const [p, q] of edges) if (p === 0) {
|
|
400
|
+
if (q < 0) return void 0;
|
|
401
|
+
} else if (p < 0) t0 = Math.max(t0, q / p);
|
|
402
|
+
else t1 = Math.min(t1, q / p);
|
|
403
|
+
if (t1 <= t0) return void 0;
|
|
404
|
+
const t = (t0 + t1) / 2;
|
|
405
|
+
return [ax + t * dx, ay + t * dy];
|
|
406
|
+
}
|
|
407
|
+
/** The even-odd rule over the outer ring and the holes. */
|
|
408
|
+
function pointInRings(x, y, rings) {
|
|
409
|
+
let inside = false;
|
|
410
|
+
for (const ring of rings) for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
411
|
+
const [xi, yi] = ring[i];
|
|
412
|
+
const [xj, yj] = ring[j];
|
|
413
|
+
if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
|
|
414
|
+
}
|
|
415
|
+
return inside;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* This function counts the pairs of a placed box and an obstacle that overlap by more than `tolerance`.
|
|
419
|
+
* The box counts with its margin. A box with a shape counts as its bounding box.
|
|
420
|
+
*/
|
|
421
|
+
function countObstacleHits(boxes, obstacles, tolerance = DEFAULT_TOLERANCE.overlap) {
|
|
422
|
+
if (obstacles === void 0 || obstacles.length === 0) return 0;
|
|
423
|
+
let count = 0;
|
|
424
|
+
for (const box of boxes) {
|
|
425
|
+
const r0 = placedRect(box);
|
|
426
|
+
if (r0 === void 0) continue;
|
|
427
|
+
const r = grow(r0, box.margin);
|
|
428
|
+
const small = {
|
|
429
|
+
x: r.x + tolerance,
|
|
430
|
+
y: r.y + tolerance,
|
|
431
|
+
w: r.w - 2 * tolerance,
|
|
432
|
+
h: r.h - 2 * tolerance
|
|
433
|
+
};
|
|
434
|
+
for (const o of obstacles) if (regionHitsRect(o.region, small)) count++;
|
|
435
|
+
}
|
|
436
|
+
return count;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* This function gives the count of each hard rule of the output of a packer.
|
|
440
|
+
* It applies the output with `applyPack`. Only the placed boxes and the locked boxes count for the overlaps and the container.
|
|
441
|
+
* `no-overlap` counts the pairs of boxes and the pairs of a box and an obstacle.
|
|
442
|
+
* A locked box without a placement, or with a move of more than `RECORD_EPS`, breaks `locked-kept`.
|
|
443
|
+
* A locked box without a position in the input is a bad input, not a break. `checkInput` finds it.
|
|
444
|
+
* Another box without a placement that is not in `overflow` breaks `complete`.
|
|
445
|
+
*/
|
|
446
|
+
function packHard(input, output, tolerance = DEFAULT_TOLERANCE.overlap) {
|
|
447
|
+
const after = applyPack(input, output);
|
|
448
|
+
const placed = new Set(output.placements.map((p) => p.id));
|
|
449
|
+
const out = new Set(output.overflow);
|
|
450
|
+
const was = new Map(input.boxes.map((b) => [b.id, b]));
|
|
451
|
+
let lockedMoved = 0;
|
|
452
|
+
let incomplete = 0;
|
|
453
|
+
const kept = [];
|
|
454
|
+
for (const box of after.boxes) {
|
|
455
|
+
const old = was.get(box.id);
|
|
456
|
+
if (old?.locked === true) {
|
|
457
|
+
if (old.x === void 0 || old.y === void 0) continue;
|
|
458
|
+
const moved = Math.abs(old.x - (box.x ?? old.x)) > .001 || Math.abs(old.y - (box.y ?? old.y)) > .001;
|
|
459
|
+
if (!placed.has(box.id) || moved) lockedMoved++;
|
|
460
|
+
kept.push(box);
|
|
461
|
+
} else if (placed.has(box.id)) kept.push(box);
|
|
462
|
+
else if (!out.has(box.id)) incomplete++;
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
"no-overlap": countOverlaps(kept, tolerance) + countObstacleHits(kept, after.obstacles, tolerance),
|
|
466
|
+
"in-container": countOutside({
|
|
467
|
+
...after,
|
|
468
|
+
boxes: kept
|
|
469
|
+
}, tolerance),
|
|
470
|
+
"locked-kept": lockedMoved,
|
|
471
|
+
complete: incomplete
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* This function gives the problems of an input file. An empty list tells that the file is good.
|
|
476
|
+
* A problem is a bad input, not a break of a hard rule. Thus an engine refuses the input.
|
|
477
|
+
*/
|
|
478
|
+
function checkInput(input) {
|
|
479
|
+
const problems = [];
|
|
480
|
+
const ids = /* @__PURE__ */ new Set();
|
|
481
|
+
for (const box of input.boxes) {
|
|
482
|
+
if (ids.has(box.id)) problems.push(`box ${box.id}: the id is not unique`);
|
|
483
|
+
ids.add(box.id);
|
|
484
|
+
if (!(box.w > 0 && box.h > 0 && Number.isFinite(box.w) && Number.isFinite(box.h))) problems.push(`box ${box.id}: the size is not positive and finite`);
|
|
485
|
+
if (box.locked === true && (box.x === void 0 || box.y === void 0)) problems.push(`box ${box.id}: a locked box has no position`);
|
|
486
|
+
}
|
|
487
|
+
if (countPortsOffBorder(input.boxes) > 0) problems.push("a fixed port is not on the border of its box");
|
|
488
|
+
for (const edge of input.edges ?? []) if (!ids.has(edge.source) || !ids.has(edge.target)) problems.push(`edge ${edge.id}: a box of the edge is not in the input`);
|
|
489
|
+
return problems;
|
|
490
|
+
}
|
|
491
|
+
// src/measures.ts
|
|
492
|
+
/** The specs of the joint measures, in the order of `docs/JOINT-MEASURES.md`. */
|
|
493
|
+
const JOINT_MEASURES = Object.freeze([
|
|
494
|
+
{
|
|
495
|
+
id: "joint.fill",
|
|
496
|
+
unit: "0 to 1",
|
|
497
|
+
direction: "higher",
|
|
498
|
+
min: 0,
|
|
499
|
+
max: 1,
|
|
500
|
+
floor: 1,
|
|
501
|
+
role: "guard"
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
id: "joint.aspectError",
|
|
505
|
+
unit: "natural log",
|
|
506
|
+
direction: "lower",
|
|
507
|
+
min: 0,
|
|
508
|
+
max: Infinity,
|
|
509
|
+
floor: 0,
|
|
510
|
+
tol: .01,
|
|
511
|
+
role: "guard"
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
id: "joint.spill",
|
|
515
|
+
unit: "0 to 1",
|
|
516
|
+
direction: "lower",
|
|
517
|
+
min: 0,
|
|
518
|
+
max: 1,
|
|
519
|
+
floor: 0,
|
|
520
|
+
tol: .001,
|
|
521
|
+
role: "guard"
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
id: "joint.straight.crossings",
|
|
525
|
+
unit: "count",
|
|
526
|
+
direction: "lower",
|
|
527
|
+
min: 0,
|
|
528
|
+
max: Infinity,
|
|
529
|
+
floor: 0,
|
|
530
|
+
tol: 0,
|
|
531
|
+
role: "guard"
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
id: "joint.straight.length",
|
|
535
|
+
unit: "S for each edge",
|
|
536
|
+
direction: "lower",
|
|
537
|
+
min: 0,
|
|
538
|
+
max: Infinity,
|
|
539
|
+
floor: 0,
|
|
540
|
+
tol: .01,
|
|
541
|
+
role: "guard"
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
id: "joint.stability.mean",
|
|
545
|
+
unit: "W",
|
|
546
|
+
direction: "lower",
|
|
547
|
+
min: 0,
|
|
548
|
+
max: Infinity,
|
|
549
|
+
floor: 0,
|
|
550
|
+
tol: .005,
|
|
551
|
+
role: "change-guard"
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
id: "joint.stability.moved",
|
|
555
|
+
unit: "0 to 1",
|
|
556
|
+
direction: "lower",
|
|
557
|
+
min: 0,
|
|
558
|
+
max: 1,
|
|
559
|
+
floor: 0,
|
|
560
|
+
tol: .02,
|
|
561
|
+
role: "change-guard"
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: "joint.proxy.rank",
|
|
565
|
+
unit: "-1 to 1",
|
|
566
|
+
direction: "higher",
|
|
567
|
+
min: -1,
|
|
568
|
+
max: 1,
|
|
569
|
+
role: "diagnostic"
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
id: "joint.ms",
|
|
573
|
+
unit: "milliseconds",
|
|
574
|
+
direction: "lower",
|
|
575
|
+
min: 0,
|
|
576
|
+
max: Infinity,
|
|
577
|
+
role: "time"
|
|
578
|
+
}
|
|
579
|
+
]);
|
|
580
|
+
/** The threshold of `joint.stability.moved`, in widths `W` of the base. */
|
|
581
|
+
const JOINT_MOVED_MIN = .01;
|
|
582
|
+
export { DEFAULT_TOLERANCE, EPS, JOINT_MEASURES, JOINT_MOVED_MIN, RECORD_EPS, SIDES, applyPack, boundsOf, checkInput, countObstacleHits, countOutside, countOverlaps, countPortsOffBorder, grow, marginSides, overlapOf, packHard, placedRect, regionHitsRect, toLayoutRecord, toPackInput, translate, turnBox, turnMargin, turnPoint, turnPort, turnRegion, turnSide };
|
|
583
|
+
|
|
584
|
+
//# sourceMappingURL=index.js.map
|