postext 1.19.2 → 1.20.0
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/dist/__tests__/comics/art.test.d.ts +2 -0
- package/dist/__tests__/comics/art.test.d.ts.map +1 -0
- package/dist/__tests__/comics/art.test.js +96 -0
- package/dist/__tests__/comics/art.test.js.map +1 -0
- package/dist/__tests__/comics/build.test.d.ts +2 -0
- package/dist/__tests__/comics/build.test.d.ts.map +1 -0
- package/dist/__tests__/comics/build.test.js +215 -0
- package/dist/__tests__/comics/build.test.js.map +1 -0
- package/dist/__tests__/comics/canvas.test.d.ts +2 -0
- package/dist/__tests__/comics/canvas.test.d.ts.map +1 -0
- package/dist/__tests__/comics/canvas.test.js +161 -0
- package/dist/__tests__/comics/canvas.test.js.map +1 -0
- package/dist/__tests__/comics/config.test.d.ts +2 -0
- package/dist/__tests__/comics/config.test.d.ts.map +1 -0
- package/dist/__tests__/comics/config.test.js +97 -0
- package/dist/__tests__/comics/config.test.js.map +1 -0
- package/dist/__tests__/comics/geometry.test.d.ts +2 -0
- package/dist/__tests__/comics/geometry.test.d.ts.map +1 -0
- package/dist/__tests__/comics/geometry.test.js +118 -0
- package/dist/__tests__/comics/geometry.test.js.map +1 -0
- package/dist/__tests__/comics/html.test.d.ts +2 -0
- package/dist/__tests__/comics/html.test.d.ts.map +1 -0
- package/dist/__tests__/comics/html.test.js +253 -0
- package/dist/__tests__/comics/html.test.js.map +1 -0
- package/dist/__tests__/comics/lettering.test.d.ts +2 -0
- package/dist/__tests__/comics/lettering.test.d.ts.map +1 -0
- package/dist/__tests__/comics/lettering.test.js +241 -0
- package/dist/__tests__/comics/lettering.test.js.map +1 -0
- package/dist/__tests__/comics/script.test.d.ts +2 -0
- package/dist/__tests__/comics/script.test.d.ts.map +1 -0
- package/dist/__tests__/comics/script.test.js +147 -0
- package/dist/__tests__/comics/script.test.js.map +1 -0
- package/dist/__tests__/comics/split.test.d.ts +2 -0
- package/dist/__tests__/comics/split.test.d.ts.map +1 -0
- package/dist/__tests__/comics/split.test.js +120 -0
- package/dist/__tests__/comics/split.test.js.map +1 -0
- package/dist/__tests__/comics/spread.test.d.ts +2 -0
- package/dist/__tests__/comics/spread.test.d.ts.map +1 -0
- package/dist/__tests__/comics/spread.test.js +161 -0
- package/dist/__tests__/comics/spread.test.js.map +1 -0
- package/dist/__tests__/comics/strip.test.d.ts +2 -0
- package/dist/__tests__/comics/strip.test.d.ts.map +1 -0
- package/dist/__tests__/comics/strip.test.js +231 -0
- package/dist/__tests__/comics/strip.test.js.map +1 -0
- package/dist/__tests__/exports.test.js +89 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/parse/containers.test.js +1 -1
- package/dist/__tests__/parse/containers.test.js.map +1 -1
- package/dist/canvas-backend/comic.d.ts +32 -0
- package/dist/canvas-backend/comic.d.ts.map +1 -0
- package/dist/canvas-backend/comic.js +216 -0
- package/dist/canvas-backend/comic.js.map +1 -0
- package/dist/canvas-backend/headerFooter.d.ts +4 -1
- package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
- package/dist/canvas-backend/headerFooter.js +3 -1
- package/dist/canvas-backend/headerFooter.js.map +1 -1
- package/dist/canvas-backend/index.d.ts.map +1 -1
- package/dist/canvas-backend/index.js +7 -1
- package/dist/canvas-backend/index.js.map +1 -1
- package/dist/comics/art.d.ts +72 -0
- package/dist/comics/art.d.ts.map +1 -0
- package/dist/comics/art.js +154 -0
- package/dist/comics/art.js.map +1 -0
- package/dist/comics/fonts.d.ts +20 -0
- package/dist/comics/fonts.d.ts.map +1 -0
- package/dist/comics/fonts.js +42 -0
- package/dist/comics/fonts.js.map +1 -0
- package/dist/comics/geometry.d.ts +87 -0
- package/dist/comics/geometry.d.ts.map +1 -0
- package/dist/comics/geometry.js +272 -0
- package/dist/comics/geometry.js.map +1 -0
- package/dist/comics/index.d.ts +29 -0
- package/dist/comics/index.d.ts.map +1 -0
- package/dist/comics/index.js +18 -0
- package/dist/comics/index.js.map +1 -0
- package/dist/comics/layoutPage.d.ts +156 -0
- package/dist/comics/layoutPage.d.ts.map +1 -0
- package/dist/comics/layoutPage.js +697 -0
- package/dist/comics/layoutPage.js.map +1 -0
- package/dist/comics/lettering/__tests__/harness.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/harness.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/harness.test.js +273 -0
- package/dist/comics/lettering/__tests__/harness.test.js.map +1 -0
- package/dist/comics/lettering/__tests__/letterPanel.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/letterPanel.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/letterPanel.test.js +224 -0
- package/dist/comics/lettering/__tests__/letterPanel.test.js.map +1 -0
- package/dist/comics/lettering/__tests__/polish.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/polish.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/polish.test.js +357 -0
- package/dist/comics/lettering/__tests__/polish.test.js.map +1 -0
- package/dist/comics/lettering/__tests__/round2.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/round2.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/round2.test.js +289 -0
- package/dist/comics/lettering/__tests__/round2.test.js.map +1 -0
- package/dist/comics/lettering/__tests__/shapeText.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/shapeText.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/shapeText.test.js +98 -0
- package/dist/comics/lettering/__tests__/shapeText.test.js.map +1 -0
- package/dist/comics/lettering/__tests__/shapes.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/shapes.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/shapes.test.js +137 -0
- package/dist/comics/lettering/__tests__/shapes.test.js.map +1 -0
- package/dist/comics/lettering/__tests__/stubMeasure.d.ts +5 -0
- package/dist/comics/lettering/__tests__/stubMeasure.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/stubMeasure.js +47 -0
- package/dist/comics/lettering/__tests__/stubMeasure.js.map +1 -0
- package/dist/comics/lettering/__tests__/text.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/text.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/text.test.js +86 -0
- package/dist/comics/lettering/__tests__/text.test.js.map +1 -0
- package/dist/comics/lettering/geom.d.ts +78 -0
- package/dist/comics/lettering/geom.d.ts.map +1 -0
- package/dist/comics/lettering/geom.js +376 -0
- package/dist/comics/lettering/geom.js.map +1 -0
- package/dist/comics/lettering/index.d.ts +7 -0
- package/dist/comics/lettering/index.d.ts.map +1 -0
- package/dist/comics/lettering/index.js +8 -0
- package/dist/comics/lettering/index.js.map +1 -0
- package/dist/comics/lettering/letter.d.ts +7 -0
- package/dist/comics/lettering/letter.d.ts.map +1 -0
- package/dist/comics/lettering/letter.js +397 -0
- package/dist/comics/lettering/letter.js.map +1 -0
- package/dist/comics/lettering/placement.d.ts +117 -0
- package/dist/comics/lettering/placement.d.ts.map +1 -0
- package/dist/comics/lettering/placement.js +889 -0
- package/dist/comics/lettering/placement.js.map +1 -0
- package/dist/comics/lettering/presets.d.ts +15 -0
- package/dist/comics/lettering/presets.d.ts.map +1 -0
- package/dist/comics/lettering/presets.js +61 -0
- package/dist/comics/lettering/presets.js.map +1 -0
- package/dist/comics/lettering/shape-text.d.ts +72 -0
- package/dist/comics/lettering/shape-text.d.ts.map +1 -0
- package/dist/comics/lettering/shape-text.js +550 -0
- package/dist/comics/lettering/shape-text.js.map +1 -0
- package/dist/comics/lettering/shapes.d.ts +83 -0
- package/dist/comics/lettering/shapes.d.ts.map +1 -0
- package/dist/comics/lettering/shapes.js +512 -0
- package/dist/comics/lettering/shapes.js.map +1 -0
- package/dist/comics/lettering/text.d.ts +79 -0
- package/dist/comics/lettering/text.d.ts.map +1 -0
- package/dist/comics/lettering/text.js +488 -0
- package/dist/comics/lettering/text.js.map +1 -0
- package/dist/comics/lettering/types.d.ts +270 -0
- package/dist/comics/lettering/types.d.ts.map +1 -0
- package/dist/comics/lettering/types.js +9 -0
- package/dist/comics/lettering/types.js.map +1 -0
- package/dist/comics/page.d.ts +37 -0
- package/dist/comics/page.d.ts.map +1 -0
- package/dist/comics/page.js +285 -0
- package/dist/comics/page.js.map +1 -0
- package/dist/comics/paint.d.ts +65 -0
- package/dist/comics/paint.d.ts.map +1 -0
- package/dist/comics/paint.js +219 -0
- package/dist/comics/paint.js.map +1 -0
- package/dist/comics/script.d.ts +42 -0
- package/dist/comics/script.d.ts.map +1 -0
- package/dist/comics/script.js +302 -0
- package/dist/comics/script.js.map +1 -0
- package/dist/comics/split.d.ts +135 -0
- package/dist/comics/split.d.ts.map +1 -0
- package/dist/comics/split.js +514 -0
- package/dist/comics/split.js.map +1 -0
- package/dist/comics/spread.d.ts +36 -0
- package/dist/comics/spread.d.ts.map +1 -0
- package/dist/comics/spread.js +172 -0
- package/dist/comics/spread.js.map +1 -0
- package/dist/comics/strip.d.ts +77 -0
- package/dist/comics/strip.d.ts.map +1 -0
- package/dist/comics/strip.js +89 -0
- package/dist/comics/strip.js.map +1 -0
- package/dist/comics/transform.d.ts +37 -0
- package/dist/comics/transform.d.ts.map +1 -0
- package/dist/comics/transform.js +161 -0
- package/dist/comics/transform.js.map +1 -0
- package/dist/comics/types.d.ts +143 -0
- package/dist/comics/types.d.ts.map +1 -0
- package/dist/comics/types.js +13 -0
- package/dist/comics/types.js.map +1 -0
- package/dist/comics/warnings.d.ts +14 -0
- package/dist/comics/warnings.d.ts.map +1 -0
- package/dist/comics/warnings.js +94 -0
- package/dist/comics/warnings.js.map +1 -0
- package/dist/configWarnings.d.ts.map +1 -1
- package/dist/configWarnings.js +42 -0
- package/dist/configWarnings.js.map +1 -1
- package/dist/defaults/comics.d.ts +51 -0
- package/dist/defaults/comics.d.ts.map +1 -0
- package/dist/defaults/comics.js +385 -0
- package/dist/defaults/comics.js.map +1 -0
- package/dist/defaults/index.d.ts +1 -0
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +9 -0
- package/dist/defaults/index.js.map +1 -1
- package/dist/design/layout.d.ts +13 -1
- package/dist/design/layout.d.ts.map +1 -1
- package/dist/design/layout.js +17 -0
- package/dist/design/layout.js.map +1 -1
- package/dist/html-backend.d.ts +3 -0
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +44 -8
- package/dist/html-backend.js.map +1 -1
- package/dist/htmlComic.d.ts +69 -0
- package/dist/htmlComic.d.ts.map +1 -0
- package/dist/htmlComic.js +179 -0
- package/dist/htmlComic.js.map +1 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/parse/blockParser.d.ts.map +1 -1
- package/dist/parse/blockParser.js +25 -1
- package/dist/parse/blockParser.js.map +1 -1
- package/dist/parse/indexMarks.d.ts.map +1 -1
- package/dist/parse/indexMarks.js +3 -0
- package/dist/parse/indexMarks.js.map +1 -1
- package/dist/parse/types.d.ts +7 -1
- package/dist/parse/types.d.ts.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +251 -9
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/config.d.ts.map +1 -1
- package/dist/pipeline/config.js +4 -1
- package/dist/pipeline/config.js.map +1 -1
- package/dist/pipeline/contentWarnings.d.ts.map +1 -1
- package/dist/pipeline/contentWarnings.js +58 -0
- package/dist/pipeline/contentWarnings.js.map +1 -1
- package/dist/pipeline/floatSlots.js +1 -1
- package/dist/pipeline/floatSlots.js.map +1 -1
- package/dist/pipeline/headerFooter.d.ts +6 -2
- package/dist/pipeline/headerFooter.d.ts.map +1 -1
- package/dist/pipeline/headerFooter.js +11 -3
- package/dist/pipeline/headerFooter.js.map +1 -1
- package/dist/pipeline/pageRoles.d.ts +1 -0
- package/dist/pipeline/pageRoles.d.ts.map +1 -1
- package/dist/pipeline/pageRoles.js +3 -0
- package/dist/pipeline/pageRoles.js.map +1 -1
- package/dist/pipeline/placement.d.ts +3 -3
- package/dist/pipeline/placement.d.ts.map +1 -1
- package/dist/pipeline/placement.js +4 -4
- package/dist/pipeline/placement.js.map +1 -1
- package/dist/types.d.ts +338 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +271 -2
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js.map +1 -1
- package/dist/worker/fonts.d.ts +2 -1
- package/dist/worker/fonts.d.ts.map +1 -1
- package/dist/worker/fonts.js +11 -1
- package/dist/worker/fonts.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
// Where the balloons of one panel go (SPEC D3.3). Deterministic:
|
|
2
|
+
// captions pinned to a corner or an edge are butted first; the others are
|
|
3
|
+
// placed greedily in reading order, each at the best of a set of candidate
|
|
4
|
+
// centres (a grid over the panel and slots around its speaker), then a few
|
|
5
|
+
// passes move each one to its best candidate given all the others. The
|
|
6
|
+
// cost weighs hard faults (covering a face, an avoid zone, a speaker's
|
|
7
|
+
// mouth or another balloon; running out of the panel) far above the soft
|
|
8
|
+
// preferences (near its speaker, high in the panel, Kurlander's reading
|
|
9
|
+
// order mirrored for right-to-left pages, tails that do not cross). When a
|
|
10
|
+
// balloon cannot be placed cleanly the fallbacks are tried in order: break
|
|
11
|
+
// the border (when allowed), cover avoid zones (never faces), reshape the
|
|
12
|
+
// text block; then it is placed anyway and reported.
|
|
13
|
+
import { boundsOf, clipToRect, dist, insetConvex, nearestOnPolygon, norm, pointInConvex, pointInRect, rayExit, rectsOverlap, segmentHitsRect, segmentDistance, sub, add, scale, } from './geom';
|
|
14
|
+
import { tailLength } from './shapes';
|
|
15
|
+
const W = {
|
|
16
|
+
face: 60,
|
|
17
|
+
avoid: 25,
|
|
18
|
+
anchor: 30,
|
|
19
|
+
balloon: 60,
|
|
20
|
+
outside: 120,
|
|
21
|
+
order: 1.2,
|
|
22
|
+
orderBase: 5,
|
|
23
|
+
distance: 0.7,
|
|
24
|
+
far: 0.18,
|
|
25
|
+
below: 0.3,
|
|
26
|
+
height: 0.1,
|
|
27
|
+
start: 0.03,
|
|
28
|
+
cross: 15,
|
|
29
|
+
tailThrough: 20,
|
|
30
|
+
tangent: 0.6,
|
|
31
|
+
ownTail: 25,
|
|
32
|
+
otherFace: 15,
|
|
33
|
+
tailOverFigure: 5,
|
|
34
|
+
side: 3,
|
|
35
|
+
soft: 0.035,
|
|
36
|
+
longTail: 0.3,
|
|
37
|
+
};
|
|
38
|
+
/** The part of a panel lettering may use: its polygon, less what runs
|
|
39
|
+
* past the page's live area (a bleeding panel's art reaches the edge of
|
|
40
|
+
* the sheet; its lettering stays where the trim cannot cut it). */
|
|
41
|
+
export function livePolygon(panel) {
|
|
42
|
+
if (!panel.limit)
|
|
43
|
+
return panel.polygon.map((p) => ({ ...p }));
|
|
44
|
+
const clipped = clipToRect(panel.polygon, panel.limit);
|
|
45
|
+
return clipped.length >= 3 ? clipped : panel.polygon.map((p) => ({ ...p }));
|
|
46
|
+
}
|
|
47
|
+
function insetRect(r, d) {
|
|
48
|
+
const dx = Math.min(d, r.width / 2);
|
|
49
|
+
const dy = Math.min(d, r.height / 2);
|
|
50
|
+
return { x: r.x + dx, y: r.y + dy, width: r.width - 2 * dx, height: r.height - 2 * dy };
|
|
51
|
+
}
|
|
52
|
+
function sceneOf(panel) {
|
|
53
|
+
const live = livePolygon(panel);
|
|
54
|
+
let visible = insetConvex(live, panel.insetPx);
|
|
55
|
+
if (visible.length < 3)
|
|
56
|
+
visible = live;
|
|
57
|
+
const trim = panel.trim ?? panel.limit;
|
|
58
|
+
return {
|
|
59
|
+
panel,
|
|
60
|
+
visible,
|
|
61
|
+
visBox: boundsOf(visible),
|
|
62
|
+
faces: panel.anchors.filter((a) => a.face).map((a) => a.face),
|
|
63
|
+
avoid: [...(panel.avoid ?? [])],
|
|
64
|
+
soft: [...(panel.softAvoid ?? [])],
|
|
65
|
+
mouths: panel.anchors.filter((a) => a.visible && a.id !== 'sfx').map((a) => a.mouth),
|
|
66
|
+
fwd: panel.direction === 'rtl' ? -1 : 1,
|
|
67
|
+
buttPoly: buttPolygon(panel),
|
|
68
|
+
neighbours: [...(panel.neighbours ?? [])],
|
|
69
|
+
...(panel.limit ? { limit: panel.limit } : {}),
|
|
70
|
+
...(trim ? { edge: insetRect(trim, panel.insetPx), edgeSfx: panel.sheet ?? trim } : {}),
|
|
71
|
+
foreign: [...(panel.foreign ?? [])],
|
|
72
|
+
keepOut: [...(panel.keepOut ?? [])],
|
|
73
|
+
live,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function buttPolygon(panel) {
|
|
77
|
+
const half = (panel.borderPx ?? 0) / 2;
|
|
78
|
+
const live = livePolygon(panel);
|
|
79
|
+
if (half <= 0)
|
|
80
|
+
return live;
|
|
81
|
+
const poly = insetConvex(live, half);
|
|
82
|
+
return poly.length >= 3 ? poly : live;
|
|
83
|
+
}
|
|
84
|
+
const moved = (pts, at) => pts.map((p) => ({ x: p.x + at.x, y: p.y + at.y }));
|
|
85
|
+
const movedRect = (r, at) => ({ x: r.x + at.x, y: r.y + at.y, width: r.width, height: r.height });
|
|
86
|
+
/** Area of `rect` the samples (standing for `area` px²) fall in. */
|
|
87
|
+
function coveredArea(samples, rect, area) {
|
|
88
|
+
let n = 0;
|
|
89
|
+
for (const s of samples)
|
|
90
|
+
if (pointInRect(s, rect))
|
|
91
|
+
n++;
|
|
92
|
+
return (n / Math.max(1, samples.length)) * area;
|
|
93
|
+
}
|
|
94
|
+
function areaOf(v) {
|
|
95
|
+
// The bodies' boxes, as an ellipse fills them.
|
|
96
|
+
return v.bodies.reduce((s, b) => s + b.bbox.width * b.bbox.height * 0.8, 0);
|
|
97
|
+
}
|
|
98
|
+
/** Where a unit's tail goes for the cost: the target point, or none. */
|
|
99
|
+
function tailTargetOf(unit, scene, centre) {
|
|
100
|
+
const t = unit.target;
|
|
101
|
+
if (t.kind === 'point')
|
|
102
|
+
return t.point;
|
|
103
|
+
if (t.kind === 'offPanel')
|
|
104
|
+
return offPanelPoint(scene.panel, centre, t.toward, t.side);
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
/** The border point an off-panel tail runs to: toward a cropped anchor,
|
|
108
|
+
* on a named side, else the nearest border point. */
|
|
109
|
+
export function offPanelPoint(panel, centre, toward, side) {
|
|
110
|
+
const poly = livePolygon(panel);
|
|
111
|
+
if (toward) {
|
|
112
|
+
const u = norm(sub(toward, centre));
|
|
113
|
+
const t = rayExit(centre, u, poly);
|
|
114
|
+
if (t !== undefined)
|
|
115
|
+
return add(centre, scale(u, t));
|
|
116
|
+
}
|
|
117
|
+
if (side) {
|
|
118
|
+
const b = panel.bbox;
|
|
119
|
+
const rtl = panel.direction === 'rtl';
|
|
120
|
+
const phys = side === 'start' ? (rtl ? 'right' : 'left') : side === 'end' ? (rtl ? 'left' : 'right') : side;
|
|
121
|
+
const probe = phys === 'top' ? { x: centre.x, y: b.y - 1e4 }
|
|
122
|
+
: phys === 'bottom' ? { x: centre.x, y: b.y + b.height + 1e4 }
|
|
123
|
+
: phys === 'left' ? { x: b.x - 1e4, y: centre.y } : { x: b.x + b.width + 1e4, y: centre.y };
|
|
124
|
+
const u = norm(sub(probe, centre));
|
|
125
|
+
const t = rayExit(centre, u, poly);
|
|
126
|
+
if (t !== undefined)
|
|
127
|
+
return add(centre, scale(u, t));
|
|
128
|
+
}
|
|
129
|
+
return nearestOnPolygon(centre, poly);
|
|
130
|
+
}
|
|
131
|
+
/** Cost of `unit` (variant `vi`) at `at`, given the other placed units. */
|
|
132
|
+
function evaluate(scene, unit, vi, at, others, level, firstOrder, terms) {
|
|
133
|
+
const v = unit.variants[vi];
|
|
134
|
+
const em = unit.em;
|
|
135
|
+
const em2 = em * em;
|
|
136
|
+
const box = movedRect(v.bbox, at);
|
|
137
|
+
const samples = moved(v.samples, at);
|
|
138
|
+
const area = areaOf(v);
|
|
139
|
+
const reasons = new Set();
|
|
140
|
+
let hard = 0;
|
|
141
|
+
let soft = 0;
|
|
142
|
+
const S = (k, x) => {
|
|
143
|
+
soft += x;
|
|
144
|
+
if (terms)
|
|
145
|
+
terms[k] = (terms[k] ?? 0) + x;
|
|
146
|
+
};
|
|
147
|
+
if (v.cost)
|
|
148
|
+
S('arrangement', v.cost);
|
|
149
|
+
const H = (k, x) => {
|
|
150
|
+
hard += x;
|
|
151
|
+
if (terms)
|
|
152
|
+
terms[`!${k}`] = (terms[`!${k}`] ?? 0) + x;
|
|
153
|
+
};
|
|
154
|
+
// Faces, avoid zones, mouths.
|
|
155
|
+
for (const f of scene.faces) {
|
|
156
|
+
if (!rectsOverlap(box, f))
|
|
157
|
+
continue;
|
|
158
|
+
const a = coveredArea(samples, f, area);
|
|
159
|
+
if (a > 0) {
|
|
160
|
+
H('face', (W.face * a) / em2 + W.face);
|
|
161
|
+
reasons.add('face');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const r of scene.avoid) {
|
|
165
|
+
if (!rectsOverlap(box, r))
|
|
166
|
+
continue;
|
|
167
|
+
const a = coveredArea(samples, r, area);
|
|
168
|
+
if (a > 0) {
|
|
169
|
+
if (level.coverAvoid)
|
|
170
|
+
S('coverAvoid', (2 * a) / em2);
|
|
171
|
+
else {
|
|
172
|
+
H('avoid', (W.avoid * a) / em2 + W.avoid);
|
|
173
|
+
reasons.add('avoid');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const r of scene.soft) {
|
|
178
|
+
if (!rectsOverlap(box, r))
|
|
179
|
+
continue;
|
|
180
|
+
S('softAvoid', ((r.weight ?? 1) * W.soft * coveredArea(samples, r, area)) / em2);
|
|
181
|
+
}
|
|
182
|
+
const bodiesHere = v.bodies.map((b) => moved(b.core, at));
|
|
183
|
+
for (const m of scene.mouths) {
|
|
184
|
+
if (!pointInRect(m, box, 0.4 * em))
|
|
185
|
+
continue;
|
|
186
|
+
if (bodiesHere.some((poly) => pointInConvex(m, poly) || dist(nearestOnPolygon(m, poly), m) < 0.4 * em)) {
|
|
187
|
+
H('anchor', W.anchor);
|
|
188
|
+
reasons.add('anchor');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Outside the panel. A unit that breaks its border on purpose (`break`,
|
|
192
|
+
// at the level that allows it; a pin, where its author put it) may run
|
|
193
|
+
// past the border, the gutter and the live area: a little cost, never a
|
|
194
|
+
// fault — but never off the sheet, and never into another panel's art
|
|
195
|
+
// unless it is a sound effect or a `break` (drawn sound and a balloon
|
|
196
|
+
// breaking into the next panel are both the letterer's call).
|
|
197
|
+
const rim = moved(v.rim, at);
|
|
198
|
+
const intended = unit.pin !== undefined || (level.breakBorder && unit.breakBorder === true);
|
|
199
|
+
const edge = intended ? (unit.kind === 'sfx' ? scene.edgeSfx : scene.edge) ?? scene.limit : scene.limit;
|
|
200
|
+
let out = 0;
|
|
201
|
+
let intrude = 0;
|
|
202
|
+
let offPage = 0;
|
|
203
|
+
// A butted caption sits flush with the border itself.
|
|
204
|
+
const area0 = unit.butt && unit.position ? scene.buttPoly : scene.visible;
|
|
205
|
+
for (const p of rim) {
|
|
206
|
+
if (!pointInConvex(p, area0) && !(unit.butt && unit.position && dist(nearestOnPolygon(p, area0), p) < 1)) {
|
|
207
|
+
out++;
|
|
208
|
+
if (edge && !pointInRect(p, edge))
|
|
209
|
+
offPage++;
|
|
210
|
+
else if (scene.neighbours.some((r) => pointInRect(p, r)))
|
|
211
|
+
intrude++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const mayIntrude = intended && (unit.kind === 'sfx' || unit.breakBorder === true);
|
|
215
|
+
// Off the sheet is the worst; into another panel (its art, its
|
|
216
|
+
// lettering) a fault weighed against the others — a balloon poking into
|
|
217
|
+
// the next panel reads better than one over its speaker's face.
|
|
218
|
+
if (offPage > 0) {
|
|
219
|
+
H('offPage', (12 * W.outside * offPage) / rim.length + 200);
|
|
220
|
+
reasons.add('outside');
|
|
221
|
+
}
|
|
222
|
+
if (intrude > 0 && !mayIntrude) {
|
|
223
|
+
H('neighbour', (4 * W.outside * intrude) / rim.length + 40);
|
|
224
|
+
reasons.add('outside');
|
|
225
|
+
}
|
|
226
|
+
else if (intrude > 0)
|
|
227
|
+
S('neighbour', 4 + (8 * intrude) / rim.length);
|
|
228
|
+
if (intended) {
|
|
229
|
+
if (out > 0) {
|
|
230
|
+
const f = out / rim.length;
|
|
231
|
+
// Breaking the border is a matter of degree: a balloon may stick out
|
|
232
|
+
// of its panel, but one whose middle stands outside it reads as
|
|
233
|
+
// another panel's (drawn sound may; so may a balloon its author
|
|
234
|
+
// pinned). A pinned balloon (no `break`) slides back in when it can.
|
|
235
|
+
S('breakBorder', 3 * f + (unit.kind !== 'sfx' && !unit.breakBorder ? 6 : 0));
|
|
236
|
+
if (unit.kind !== 'sfx' && !unit.pin && !pointInConvex({ x: box.x + box.width / 2, y: box.y + box.height / 2 }, scene.live)) {
|
|
237
|
+
H('outside', W.outside * f + 10);
|
|
238
|
+
reasons.add('outside');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else if (out > 0) {
|
|
243
|
+
H('outside', (W.outside * out) / rim.length + 10);
|
|
244
|
+
reasons.add('outside');
|
|
245
|
+
}
|
|
246
|
+
// The fold of a spread: never, at any level, pinned or not.
|
|
247
|
+
for (const r of scene.keepOut) {
|
|
248
|
+
if (!rectsOverlap(box, r))
|
|
249
|
+
continue;
|
|
250
|
+
const ox = Math.min(box.x + box.width, r.x + r.width) - Math.max(box.x, r.x);
|
|
251
|
+
H('avoid', (W.avoid * 4 * ox) / em + 4 * W.avoid);
|
|
252
|
+
reasons.add('avoid');
|
|
253
|
+
}
|
|
254
|
+
// Balloons of the other panels (a balloon breaking into this one).
|
|
255
|
+
for (const r of scene.foreign) {
|
|
256
|
+
if (!rectsOverlap(box, r, 0.3 * em))
|
|
257
|
+
continue;
|
|
258
|
+
const a = coveredArea(samples, grow(r, 0.3 * em), area);
|
|
259
|
+
if (a > 0) {
|
|
260
|
+
H('balloon', (W.balloon * a) / em2 + W.balloon);
|
|
261
|
+
reasons.add('balloon');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Other balloons: overlap is a fault, a near miss (a tangent) a blemish.
|
|
265
|
+
const centre = at;
|
|
266
|
+
const target = tailTargetOf(unit, scene, centre);
|
|
267
|
+
for (const o of others) {
|
|
268
|
+
const ov = o.unit.variants[o.variant];
|
|
269
|
+
const obox = movedRect(ov.bbox, o.at);
|
|
270
|
+
if (rectsOverlap(box, obox, 0.5 * em)) {
|
|
271
|
+
const opolys = ov.bodies.map((b) => moved(b.core, o.at));
|
|
272
|
+
let inside = 0;
|
|
273
|
+
for (const s of samples)
|
|
274
|
+
if (opolys.some((poly) => pointInConvex(s, poly)))
|
|
275
|
+
inside++;
|
|
276
|
+
if (inside > 0) {
|
|
277
|
+
H('balloon', (W.balloon * (inside / samples.length) * area) / em2 + W.balloon);
|
|
278
|
+
reasons.add('balloon');
|
|
279
|
+
}
|
|
280
|
+
else
|
|
281
|
+
S('tangent', W.tangent);
|
|
282
|
+
}
|
|
283
|
+
// Reading order (Kurlander), for the two in their script order.
|
|
284
|
+
const [eBox, lBox] = o.unit.order < unit.order ? [obox, box] : [box, obox];
|
|
285
|
+
const forward = (lBox.x + lBox.width / 2 - (eBox.x + eBox.width / 2)) * scene.fwd < 0;
|
|
286
|
+
// `forward`: the earlier one lies forward (in reading direction) of the
|
|
287
|
+
// later one: the later may be no higher than its bottom; else no
|
|
288
|
+
// higher than its top.
|
|
289
|
+
const limit = forward ? eBox.y + eBox.height - 0.25 * em : eBox.y - 0.25 * em;
|
|
290
|
+
if (unit.kind !== 'sfx' && o.unit.kind !== 'sfx' && lBox.y < limit) {
|
|
291
|
+
const overlapX = Math.min(lBox.x + lBox.width, eBox.x + eBox.width) - Math.max(lBox.x, eBox.x);
|
|
292
|
+
// Side by side at one height reads in order when the earlier one
|
|
293
|
+
// is behind; a later one above an earlier one never does.
|
|
294
|
+
S('order', W.orderBase + (W.order * (limit - lBox.y)) / em + (overlapX > 0 ? 1 : 0));
|
|
295
|
+
}
|
|
296
|
+
// Tails that cross (as drawn: from the wall to the tip), tails
|
|
297
|
+
// through another balloon.
|
|
298
|
+
const otarget = tailTargetOf(o.unit, scene, o.at);
|
|
299
|
+
const mine = target ? tailSegment(unit, v, at, target) : undefined;
|
|
300
|
+
const theirs = otarget ? tailSegment(o.unit, ov, o.at, otarget) : undefined;
|
|
301
|
+
if (mine && theirs) {
|
|
302
|
+
// Crossing tails; tails that all but touch read as crossed too
|
|
303
|
+
// (unless they run to one speaker).
|
|
304
|
+
const gapTails = segmentDistance(mine[0], mine[1], theirs[0], theirs[1]);
|
|
305
|
+
if (gapTails === 0)
|
|
306
|
+
S('cross', W.cross);
|
|
307
|
+
else if (gapTails < 0.6 * em && dist(target, otarget) > em)
|
|
308
|
+
S('cross', 0.4 * W.cross);
|
|
309
|
+
}
|
|
310
|
+
if (mine && ov.bodies.some((b) => segmentHitsRect(mine[0], mine[1], shrink(movedRect(b.bbox, o.at), 0.12))))
|
|
311
|
+
S('tailThrough', W.tailThrough);
|
|
312
|
+
if (theirs && v.bodies.some((b) => segmentHitsRect(theirs[0], theirs[1], shrink(movedRect(b.bbox, at), 0.12))))
|
|
313
|
+
S('tailThrough', W.tailThrough);
|
|
314
|
+
}
|
|
315
|
+
// The tail leaves the first body clear of the group's other bodies, by
|
|
316
|
+
// more than its own half width: a tail grazing another body of its group
|
|
317
|
+
// leaves a notch between them that the doubled outline fills with ink.
|
|
318
|
+
if (target && v.bodies.length > 1) {
|
|
319
|
+
const edge = edgePoint(v, at, target);
|
|
320
|
+
for (const b of v.bodies.slice(1))
|
|
321
|
+
if (segmentHitsRect(edge, target, grow(movedRect(b.bbox, at), 0.8 * em)))
|
|
322
|
+
S('ownTail', W.ownTail);
|
|
323
|
+
}
|
|
324
|
+
// Near the speaker, a little above; tails not over faces.
|
|
325
|
+
if (target && unit.target.kind === 'point') {
|
|
326
|
+
const edge = edgePoint(v, at, target);
|
|
327
|
+
const gap = dist(edge, target);
|
|
328
|
+
const h = v.bodies[0].bbox.height;
|
|
329
|
+
// Room for a tail of a few ems: too close is worse than too far.
|
|
330
|
+
const ideal = Math.max(2 * em, 0.7 * h);
|
|
331
|
+
S('distance', gap < ideal ? (W.distance * (ideal - gap)) / em : (W.far * (gap - ideal)) / em);
|
|
332
|
+
if (gap < 0.7 * em)
|
|
333
|
+
S('tooClose', 3);
|
|
334
|
+
// A tail longer than a few ems reads as an arrow: the cost of a far
|
|
335
|
+
// balloon grows faster past six ems. (Over a close-up the balloon goes
|
|
336
|
+
// above the head and its tail runs down past the face: that one is
|
|
337
|
+
// long by necessity.)
|
|
338
|
+
if (gap > 6 * em)
|
|
339
|
+
S('longTail', (W.longTail * (gap - 6 * em)) / em);
|
|
340
|
+
if (centre.y > target.y)
|
|
341
|
+
S('below', (W.below * (centre.y - target.y)) / em);
|
|
342
|
+
// A tail over another character's face (or the top of the head)
|
|
343
|
+
// reads as theirs.
|
|
344
|
+
const tip = add(edge, scale(norm(sub(target, edge)), gap * 0.55));
|
|
345
|
+
for (const f of scene.faces) {
|
|
346
|
+
const head = { x: f.x, y: f.y - 0.3 * f.height, width: f.width, height: 1.3 * f.height };
|
|
347
|
+
if (!segmentHitsRect(edge, tip, shrink(head, 0.1)))
|
|
348
|
+
continue;
|
|
349
|
+
S('otherFace', pointInRect(target, head) ? 1 : W.otherFace);
|
|
350
|
+
}
|
|
351
|
+
// A tail drawn across another figure (the body under a marked face):
|
|
352
|
+
// the eye follows it through the wrong character.
|
|
353
|
+
const drawn = tailSegment(unit, v, at, target);
|
|
354
|
+
for (const a of scene.panel.anchors) {
|
|
355
|
+
if (!a.face || a.mouth === target || a.head === target)
|
|
356
|
+
continue;
|
|
357
|
+
const f = a.face;
|
|
358
|
+
const figure = { x: f.x - 0.2 * f.width, y: f.y + f.height, width: 1.4 * f.width, height: 1.6 * f.height };
|
|
359
|
+
if (segmentHitsRect(drawn[0], drawn[1], figure))
|
|
360
|
+
S('tailOverFigure', W.tailOverFigure);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else if (unit.target.kind === 'offPanel' && target) {
|
|
364
|
+
// An off-panel speaker's balloon sits by the border its voice comes
|
|
365
|
+
// from, with a short tail to it (the letterer's practice): a tail
|
|
366
|
+
// across the panel reads as an arrow, so the balloon moves instead.
|
|
367
|
+
const reach = dist(edgePoint(v, at, target), target);
|
|
368
|
+
S('side', (W.side * Math.max(0, reach - 1.2 * em) + 3 * W.side * Math.max(0, reach - 3 * em)) / em);
|
|
369
|
+
}
|
|
370
|
+
if (unit.kind === 'sfx' && unit.near)
|
|
371
|
+
S('sfxNear', (0.5 * dist(centre, unit.near)) / em);
|
|
372
|
+
// High in the panel; the first one in the top start corner.
|
|
373
|
+
if (unit.kind !== 'sfx') {
|
|
374
|
+
const top = box.y - scene.visBox.y;
|
|
375
|
+
S('height', ((scene.heightBias ?? 1) * (unit.kind === 'note' ? W.height * Math.max(0, scene.visBox.y + scene.visBox.height - (box.y + box.height)) : W.height * top)) / em);
|
|
376
|
+
if (unit.order === firstOrder && unit.kind !== 'note') {
|
|
377
|
+
const fromStart = scene.fwd === 1 ? box.x - scene.visBox.x : scene.visBox.x + scene.visBox.width - (box.x + box.width);
|
|
378
|
+
S('start', (W.start * fromStart) / em);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return { hard, soft, reasons };
|
|
382
|
+
}
|
|
383
|
+
/** A rectangle grown by `d` px on every side. */
|
|
384
|
+
function grow(r, d) {
|
|
385
|
+
return { x: r.x - d, y: r.y - d, width: r.width + 2 * d, height: r.height + 2 * d };
|
|
386
|
+
}
|
|
387
|
+
function shrink(r, f) {
|
|
388
|
+
return { x: r.x + r.width * f, y: r.y + r.height * f, width: r.width * (1 - 2 * f), height: r.height * (1 - 2 * f) };
|
|
389
|
+
}
|
|
390
|
+
/** A unit's tail as drawn, for the cost: from where it leaves the first
|
|
391
|
+
* body to its tip ({@link tailLength} of the gap). */
|
|
392
|
+
function tailSegment(unit, v, at, target) {
|
|
393
|
+
const edge = edgePoint(v, at, target);
|
|
394
|
+
const gap = dist(edge, target);
|
|
395
|
+
const len = Math.max(Math.min(gap, 0.8 * unit.em), tailLength(gap, unit.em, {}, unit.target.kind === 'offPanel'));
|
|
396
|
+
return [edge, add(edge, scale(norm(sub(target, edge)), len))];
|
|
397
|
+
}
|
|
398
|
+
/** Where the line from the first body's centre to `target` leaves it. */
|
|
399
|
+
function edgePoint(v, at, target) {
|
|
400
|
+
const b = v.bodies[0];
|
|
401
|
+
const c = { x: b.centre.x + at.x, y: b.centre.y + at.y };
|
|
402
|
+
const u = norm(sub(target, c));
|
|
403
|
+
const t = rayExit(c, u, moved(b.core, at));
|
|
404
|
+
return t === undefined ? c : add(c, scale(u, t));
|
|
405
|
+
}
|
|
406
|
+
/** Candidate anchor positions of a unit (deterministic order). */
|
|
407
|
+
function candidates(scene, unit, vi) {
|
|
408
|
+
const v = unit.variants[vi];
|
|
409
|
+
const out = [];
|
|
410
|
+
const seen = new Set();
|
|
411
|
+
const push = (p) => {
|
|
412
|
+
const k = `${Math.round(p.x * 2)},${Math.round(p.y * 2)}`;
|
|
413
|
+
if (seen.has(k))
|
|
414
|
+
return;
|
|
415
|
+
seen.add(k);
|
|
416
|
+
out.push(p);
|
|
417
|
+
};
|
|
418
|
+
const vb = scene.visBox;
|
|
419
|
+
// Clamp the unit's box into the visible box (a candidate hugging a side).
|
|
420
|
+
const clamp = (p) => {
|
|
421
|
+
const box = movedRect(v.bbox, p);
|
|
422
|
+
let dx = 0;
|
|
423
|
+
let dy = 0;
|
|
424
|
+
if (box.width <= vb.width) {
|
|
425
|
+
if (box.x < vb.x)
|
|
426
|
+
dx = vb.x - box.x;
|
|
427
|
+
else if (box.x + box.width > vb.x + vb.width)
|
|
428
|
+
dx = vb.x + vb.width - (box.x + box.width);
|
|
429
|
+
}
|
|
430
|
+
if (box.height <= vb.height) {
|
|
431
|
+
if (box.y < vb.y)
|
|
432
|
+
dy = vb.y - box.y;
|
|
433
|
+
else if (box.y + box.height > vb.y + vb.height)
|
|
434
|
+
dy = vb.y + vb.height - (box.y + box.height);
|
|
435
|
+
}
|
|
436
|
+
return { x: p.x + dx, y: p.y + dy };
|
|
437
|
+
};
|
|
438
|
+
// Anchor-relative offset: the unit box's centre to its anchor.
|
|
439
|
+
const off = { x: -(v.bbox.x + v.bbox.width / 2), y: -(v.bbox.y + v.bbox.height / 2) };
|
|
440
|
+
const at = (cx, cy) => ({ x: cx + off.x, y: cy + off.y });
|
|
441
|
+
const w = v.bbox.width;
|
|
442
|
+
const h = v.bbox.height;
|
|
443
|
+
const em = unit.em;
|
|
444
|
+
// Around the target (speaker or sound-effect point).
|
|
445
|
+
const target = unit.target.kind === 'point' ? unit.target.point : unit.near;
|
|
446
|
+
if (target) {
|
|
447
|
+
for (const k of [0.6, 1.2, 2, 3]) {
|
|
448
|
+
for (const fx of [0, -0.45, 0.45, -0.85, 0.85]) {
|
|
449
|
+
const p = at(target.x + fx * w, target.y - h / 2 - k * em - 0.4 * h);
|
|
450
|
+
push(p);
|
|
451
|
+
push(clamp(p));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
for (const side of [-1, 1]) {
|
|
455
|
+
for (const k of [0, 0.6, 1.2]) {
|
|
456
|
+
const p = at(target.x + side * (w / 2 + 1.5 * em), target.y - k * h);
|
|
457
|
+
push(p);
|
|
458
|
+
push(clamp(p));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Under the speaker (a last resort the cost weighs: in a cell with no
|
|
462
|
+
// room above the head, below it beats over the face).
|
|
463
|
+
if (unit.kind !== 'sfx') {
|
|
464
|
+
for (const k of [1.2, 2.4]) {
|
|
465
|
+
for (const fx of [0, -0.45, 0.45])
|
|
466
|
+
push(at(target.x + fx * w, target.y + h / 2 + k * em));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (unit.kind === 'sfx')
|
|
470
|
+
push(at(target.x, target.y));
|
|
471
|
+
}
|
|
472
|
+
// A grid over the visible box.
|
|
473
|
+
const step = Math.max(em, Math.min(vb.width, vb.height) / 12);
|
|
474
|
+
for (let y = vb.y + h / 2; y <= vb.y + vb.height - h / 2 + 1e-6; y += step) {
|
|
475
|
+
for (let x = vb.x + w / 2; x <= vb.x + vb.width - w / 2 + 1e-6; x += step)
|
|
476
|
+
push(at(x, y));
|
|
477
|
+
}
|
|
478
|
+
// The corners and edges, butted against the visible box.
|
|
479
|
+
for (const fy of [0, 0.5, 1]) {
|
|
480
|
+
for (const fx of [0, 0.5, 1])
|
|
481
|
+
push(clamp(at(vb.x + fx * vb.width + (0.5 - fx) * w, vb.y + fy * vb.height + (0.5 - fy) * h)));
|
|
482
|
+
}
|
|
483
|
+
if (out.length === 0)
|
|
484
|
+
push(at(vb.x + vb.width / 2, vb.y + vb.height / 2));
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
/** The anchor a corner or edge keyword puts a unit at: its box flush
|
|
488
|
+
* with the panel (`butt`) or with the inset area, at that corner. */
|
|
489
|
+
export function positionAnchor(scene, v, position, butt) {
|
|
490
|
+
const poly = butt ? scene.buttPoly : scene.visible;
|
|
491
|
+
const box = boundsOf(poly);
|
|
492
|
+
const startLeft = scene.fwd === 1;
|
|
493
|
+
const hx = position === 'top' || position === 'bottom' ? 0.5
|
|
494
|
+
: position.endsWith('start') ? (startLeft ? 0 : 1) : (startLeft ? 1 : 0);
|
|
495
|
+
const vy = position.startsWith('top') ? 0 : 1;
|
|
496
|
+
const bx = box.x + hx * (box.width - v.bbox.width);
|
|
497
|
+
const by = box.y + vy * (box.height - v.bbox.height);
|
|
498
|
+
// Move inward until the box's corners sit inside a slanted cell.
|
|
499
|
+
const inward = { x: hx === 0 ? 1 : hx === 1 ? -1 : 0, y: vy === 0 ? 1 : -1 };
|
|
500
|
+
let p = { x: bx - v.bbox.x, y: by - v.bbox.y };
|
|
501
|
+
for (let i = 0; i < 400; i++) {
|
|
502
|
+
const r = movedRect(v.bbox, p);
|
|
503
|
+
const corners = [{ x: r.x, y: r.y }, { x: r.x + r.width, y: r.y }, { x: r.x + r.width, y: r.y + r.height }, { x: r.x, y: r.y + r.height }];
|
|
504
|
+
if (corners.every((c) => pointInConvex(c, poly) || dist(nearestOnPolygon(c, poly), c) < 0.75))
|
|
505
|
+
break;
|
|
506
|
+
p = { x: p.x + inward.x, y: p.y + inward.y };
|
|
507
|
+
}
|
|
508
|
+
return p;
|
|
509
|
+
}
|
|
510
|
+
/** A corner or edge unit's anchors: its own corner first, then — a cost
|
|
511
|
+
* each, so taken only when its own covers a face, an avoid zone or a
|
|
512
|
+
* balloon — the other corner of that edge, the middle of it, and the
|
|
513
|
+
* corners of the opposite edge. */
|
|
514
|
+
function positionAnchors(scene, v, position, butt) {
|
|
515
|
+
const top = position.startsWith('top');
|
|
516
|
+
const across = position.endsWith('start') ? (top ? 'top-end' : 'bottom-end') : position.endsWith('end') ? (top ? 'top-start' : 'bottom-start') : (top ? 'top-start' : 'bottom-start');
|
|
517
|
+
const middle = top ? 'top' : 'bottom';
|
|
518
|
+
const flip = (q) => (q.startsWith('top') ? q.replace('top', 'bottom') : q.replace('bottom', 'top'));
|
|
519
|
+
const list = [[position, 0], [across, 2], [middle, 3], [flip(position), 5], [flip(across), 6]];
|
|
520
|
+
const seen = new Set();
|
|
521
|
+
return list.filter(([q]) => (seen.has(q) ? false : (seen.add(q), true))).map(([q, extra]) => ({ at: positionAnchor(scene, v, q, butt), extra }));
|
|
522
|
+
}
|
|
523
|
+
const LEVELS = [
|
|
524
|
+
{ breakBorder: false, coverAvoid: false },
|
|
525
|
+
{ breakBorder: true, coverAvoid: false },
|
|
526
|
+
{ breakBorder: true, coverAvoid: true },
|
|
527
|
+
];
|
|
528
|
+
function better(a, b) {
|
|
529
|
+
if (a.hard > 0 || b.hard > 0)
|
|
530
|
+
return a.hard * 1000 + a.soft < b.hard * 1000 + b.soft - 1e-9;
|
|
531
|
+
return a.soft < b.soft - 1e-9;
|
|
532
|
+
}
|
|
533
|
+
/** Best spot for one unit given the others, over its variants and the
|
|
534
|
+
* fallback levels (in SPEC order); the first clean choice wins. */
|
|
535
|
+
function bestFor(scene, unit, others, firstOrder, fixed) {
|
|
536
|
+
let fallback;
|
|
537
|
+
const variants = fixed ? [fixed.variant] : unit.variants.map((_, i) => i);
|
|
538
|
+
const levels = fixed ? [fixed.level] : LEVELS.map((_, i) => i).filter((i) => i === 0 || unit.breakBorder || i === 2);
|
|
539
|
+
// Every arrangement of a joined group competes on its cost; a reshaped
|
|
540
|
+
// text block (wider or narrower than its best shape) pays a little for
|
|
541
|
+
// its poorer shape, and is tried before covering an avoid zone (a
|
|
542
|
+
// letterer reshapes the text before covering a hand).
|
|
543
|
+
const penalty = (vi) => (unit.variants[vi].reshaped ? RESHAPE_COST : 0);
|
|
544
|
+
for (const li of levels) {
|
|
545
|
+
const level = LEVELS[li];
|
|
546
|
+
let best;
|
|
547
|
+
for (const vi of variants) {
|
|
548
|
+
const cands = unit.pin
|
|
549
|
+
// (The pin as written wins unless it runs out of the panel.)
|
|
550
|
+
? pinAnchors(scene, unit, vi).map((at, i) => ({ at, extra: i === 0 ? 0 : 4 }))
|
|
551
|
+
: unit.position
|
|
552
|
+
? positionAnchors(scene, unit.variants[vi], unit.position, unit.butt === true)
|
|
553
|
+
: candidates(scene, unit, vi).map((at) => ({ at, extra: 0 }));
|
|
554
|
+
for (const { at, extra } of cands) {
|
|
555
|
+
const ev = evaluate(scene, unit, vi, at, others, level, firstOrder);
|
|
556
|
+
const score = ev.hard * 1000 + ev.soft + penalty(vi) + extra;
|
|
557
|
+
if (!best || score < best.score - 1e-9)
|
|
558
|
+
best = { variant: vi, level: li, at, ev, score };
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (!best)
|
|
562
|
+
continue;
|
|
563
|
+
const choice = { variant: best.variant, level: best.level, at: best.at, ev: best.ev };
|
|
564
|
+
if (choice.ev.hard === 0)
|
|
565
|
+
return choice;
|
|
566
|
+
if (!fallback || better(choice.ev, fallback.ev))
|
|
567
|
+
fallback = choice;
|
|
568
|
+
}
|
|
569
|
+
return fallback;
|
|
570
|
+
}
|
|
571
|
+
/** What choosing a reshaped text block costs (its poorer shape). */
|
|
572
|
+
const RESHAPE_COST = 1.5;
|
|
573
|
+
/** A pinned unit's anchors: on its pin, and moved the least that keeps
|
|
574
|
+
* its box inside the panel (a pinned label too long for the room left
|
|
575
|
+
* at its pin slides in rather than run out of the panel). */
|
|
576
|
+
function pinAnchors(scene, unit, vi) {
|
|
577
|
+
const p = pinAnchor(unit, vi);
|
|
578
|
+
const v = unit.variants[vi];
|
|
579
|
+
const box = movedRect(v.bbox, p);
|
|
580
|
+
// Drawn sound stays where its author put it, over borders and all; it
|
|
581
|
+
// only slides back onto the sheet. A pinned balloon slides into its
|
|
582
|
+
// panel when it would run out of it.
|
|
583
|
+
const vb = unit.kind === 'sfx' ? (scene.edgeSfx ?? scene.limit ?? scene.visBox) : scene.visBox;
|
|
584
|
+
const shift = (lo, size, min, max) => size > max - min ? (min + max) / 2 - (lo + size / 2) : lo < min ? min - lo : lo + size > max ? max - (lo + size) : 0;
|
|
585
|
+
const dx = shift(box.x, box.width, vb.x, vb.x + vb.width);
|
|
586
|
+
const dy = shift(box.y, box.height, vb.y, vb.y + vb.height);
|
|
587
|
+
const out = dx === 0 && dy === 0 ? [p] : [p, { x: p.x + dx, y: p.y + dy }];
|
|
588
|
+
// Off the fold of a spread, to the nearer side (then the other).
|
|
589
|
+
for (const q of [...out]) {
|
|
590
|
+
const b = movedRect(v.bbox, q);
|
|
591
|
+
for (const r of scene.keepOut) {
|
|
592
|
+
if (!rectsOverlap(b, r))
|
|
593
|
+
continue;
|
|
594
|
+
const left = r.x - (b.x + b.width) - 1;
|
|
595
|
+
const right = r.x + r.width - b.x + 1;
|
|
596
|
+
const sides = Math.abs(left) <= Math.abs(right) ? [left, right] : [right, left];
|
|
597
|
+
for (const d of sides)
|
|
598
|
+
out.push({ x: q.x + d, y: q.y });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return out;
|
|
602
|
+
}
|
|
603
|
+
/** The anchor that puts a pinned unit's box centre on its pin; for a
|
|
604
|
+
* joined group, the centre of its first body's box (the pinned line's
|
|
605
|
+
* balloon: the lines joined to it follow wherever the group arranges
|
|
606
|
+
* them). */
|
|
607
|
+
function pinAnchor(unit, vi) {
|
|
608
|
+
const v = unit.variants[vi];
|
|
609
|
+
const box = v.bodies.length > 1 ? v.bodies[0].bbox : v.bbox;
|
|
610
|
+
return { x: unit.pin.x - (box.x + box.width / 2), y: unit.pin.y - (box.y + box.height / 2) };
|
|
611
|
+
}
|
|
612
|
+
function solve(scene, ordered, firstOrder, passes, mode = 'reading') {
|
|
613
|
+
const placed = [];
|
|
614
|
+
// Corner and edge captions first, then pinned units, then the rest in
|
|
615
|
+
// reading order (or the biggest first: in a crowded panel the balloon
|
|
616
|
+
// that needs the most room takes it first); a sound effect with no place
|
|
617
|
+
// of its own (no pin, no `sfx` point in the art) last: the dialogue
|
|
618
|
+
// takes the room it needs first, the sound fills what is left.
|
|
619
|
+
const rank = (u) => (u.position ? 0 : u.pin ? 1 : u.kind === 'sfx' && !u.near ? 3 : 2);
|
|
620
|
+
const size = (u) => u.variants[0].bbox.width * u.variants[0].bbox.height;
|
|
621
|
+
const standing = (u) => (u.target.kind === 'point' ? scene.fwd * u.target.point.x : Number.POSITIVE_INFINITY);
|
|
622
|
+
const sequence = [...ordered].sort((a, b) => rank(a) - rank(b)
|
|
623
|
+
|| (mode === 'big' ? size(b) - size(a) : mode === 'speaker' ? standing(a) - standing(b) : 0)
|
|
624
|
+
|| a.order - b.order);
|
|
625
|
+
for (const unit of sequence) {
|
|
626
|
+
const c = bestFor(scene, unit, placed, firstOrder);
|
|
627
|
+
placed.push({ unit, variant: c.variant, at: c.at, level: c.level, ev: c.ev });
|
|
628
|
+
}
|
|
629
|
+
// Local improvement: each free unit moves to its best spot given the
|
|
630
|
+
// others, keeping its variant and level.
|
|
631
|
+
return solveFrom(scene, placed, firstOrder, passes);
|
|
632
|
+
}
|
|
633
|
+
/** Improvement passes from an existing layout. */
|
|
634
|
+
function solveFrom(scene, start, firstOrder, passes) {
|
|
635
|
+
let placed = [...start];
|
|
636
|
+
for (let pass = 0; pass < passes; pass++) {
|
|
637
|
+
let changed = false;
|
|
638
|
+
// Two balloons whose tails cross trade places (no single move can
|
|
639
|
+
// undo a crossing: each balloon alone is best where it is).
|
|
640
|
+
const swapped = swapCrossing(scene, placed, firstOrder);
|
|
641
|
+
if (swapped) {
|
|
642
|
+
placed = swapped;
|
|
643
|
+
changed = true;
|
|
644
|
+
}
|
|
645
|
+
for (let i = 0; i < placed.length; i++) {
|
|
646
|
+
const p = placed[i];
|
|
647
|
+
if (p.unit.pin || p.unit.position)
|
|
648
|
+
continue;
|
|
649
|
+
const others = placed.filter((_, j) => j !== i);
|
|
650
|
+
const now = evaluate(scene, p.unit, p.variant, p.at, others, LEVELS[p.level], firstOrder);
|
|
651
|
+
const c = bestFor(scene, p.unit, others, firstOrder, { variant: p.variant, level: p.level });
|
|
652
|
+
if (better(c.ev, now) && (c.at.x !== p.at.x || c.at.y !== p.at.y)) {
|
|
653
|
+
placed[i] = { ...p, at: c.at, ev: c.ev };
|
|
654
|
+
changed = true;
|
|
655
|
+
}
|
|
656
|
+
else
|
|
657
|
+
placed[i] = { ...p, ev: now };
|
|
658
|
+
}
|
|
659
|
+
if (!changed)
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
return placed;
|
|
663
|
+
}
|
|
664
|
+
/** The layout with the first pair of free units whose tails cross
|
|
665
|
+
* swapped (each box centred where the other's was, then settled at its
|
|
666
|
+
* best spot), when that lowers the whole layout's cost; else undefined. */
|
|
667
|
+
function swapCrossing(scene, placed, firstOrder) {
|
|
668
|
+
const free = (p) => !p.unit.pin && !p.unit.position && p.unit.kind === 'balloon';
|
|
669
|
+
const centreOf = (p) => {
|
|
670
|
+
const b = p.unit.variants[p.variant].bbox;
|
|
671
|
+
return { x: p.at.x + b.x + b.width / 2, y: p.at.y + b.y + b.height / 2 };
|
|
672
|
+
};
|
|
673
|
+
const base = totalCost(scene, placed, firstOrder);
|
|
674
|
+
for (let i = 0; i < placed.length; i++) {
|
|
675
|
+
for (let j = i + 1; j < placed.length; j++) {
|
|
676
|
+
const a = placed[i];
|
|
677
|
+
const b = placed[j];
|
|
678
|
+
if (!free(a) || !free(b))
|
|
679
|
+
continue;
|
|
680
|
+
const ta = tailTargetOf(a.unit, scene, a.at);
|
|
681
|
+
const tb = tailTargetOf(b.unit, scene, b.at);
|
|
682
|
+
if (!ta || !tb)
|
|
683
|
+
continue;
|
|
684
|
+
const sa = tailSegment(a.unit, a.unit.variants[a.variant], a.at, ta);
|
|
685
|
+
const sb = tailSegment(b.unit, b.unit.variants[b.variant], b.at, tb);
|
|
686
|
+
const em = Math.min(a.unit.em, b.unit.em);
|
|
687
|
+
if (segmentDistance(sa[0], sa[1], sb[0], sb[1]) >= 0.6 * em || dist(ta, tb) <= em)
|
|
688
|
+
continue;
|
|
689
|
+
const ca = centreOf(a);
|
|
690
|
+
const cb = centreOf(b);
|
|
691
|
+
const moveTo = (p, c) => {
|
|
692
|
+
const bb = p.unit.variants[p.variant].bbox;
|
|
693
|
+
return { ...p, at: { x: c.x - (bb.x + bb.width / 2), y: c.y - (bb.y + bb.height / 2) } };
|
|
694
|
+
};
|
|
695
|
+
const raw = placed.map((p, k) => (k === i ? moveTo(a, cb) : k === j ? moveTo(b, ca) : p));
|
|
696
|
+
// Then each settled at its best spot given the other.
|
|
697
|
+
const settled = [...raw];
|
|
698
|
+
for (const k of [i, j]) {
|
|
699
|
+
const p = settled[k];
|
|
700
|
+
const others = settled.filter((_, m) => m !== k);
|
|
701
|
+
const c = bestFor(scene, p.unit, others, firstOrder, { variant: p.variant, level: p.level });
|
|
702
|
+
const now = evaluate(scene, p.unit, p.variant, p.at, others, LEVELS[p.level], firstOrder);
|
|
703
|
+
if (better(c.ev, now))
|
|
704
|
+
settled[k] = { ...p, at: c.at, ev: c.ev };
|
|
705
|
+
}
|
|
706
|
+
// Or both taken out and placed again, each order in turn, the second
|
|
707
|
+
// seeing the first one's tail.
|
|
708
|
+
const replaced = (first, second) => {
|
|
709
|
+
const out = [...placed];
|
|
710
|
+
const rest = () => out.filter((_, m) => m !== i && m !== j);
|
|
711
|
+
const p1 = out[first];
|
|
712
|
+
const c1 = bestFor(scene, p1.unit, rest(), firstOrder);
|
|
713
|
+
out[first] = { ...p1, variant: c1.variant, level: c1.level, at: c1.at, ev: c1.ev };
|
|
714
|
+
const p2 = out[second];
|
|
715
|
+
const c2 = bestFor(scene, p2.unit, out.filter((_, m) => m !== second), firstOrder);
|
|
716
|
+
out[second] = { ...p2, variant: c2.variant, level: c2.level, at: c2.at, ev: c2.ev };
|
|
717
|
+
return out;
|
|
718
|
+
};
|
|
719
|
+
let trial;
|
|
720
|
+
let cost = base;
|
|
721
|
+
for (const t of [raw, settled, replaced(j, i), replaced(i, j)]) {
|
|
722
|
+
const c = totalCost(scene, t, firstOrder);
|
|
723
|
+
if (c < cost - 1e-6) {
|
|
724
|
+
cost = c;
|
|
725
|
+
trial = t;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (trial)
|
|
729
|
+
return trial;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return undefined;
|
|
733
|
+
}
|
|
734
|
+
/** The layout with one pair of free units taken out and placed again
|
|
735
|
+
* (each order in turn, the second seeing the first) when that lowers the
|
|
736
|
+
* whole layout's cost; else undefined. A single move cannot undo a pair
|
|
737
|
+
* that blocks itself: the first speaker's balloon in the corner the second
|
|
738
|
+
* needs, the second pushed onto the figures. */
|
|
739
|
+
function replacePair(scene, placed, firstOrder) {
|
|
740
|
+
const free = (p) => !p.unit.pin && !p.unit.position && p.unit.kind !== 'sfx';
|
|
741
|
+
const base = totalCost(scene, placed, firstOrder);
|
|
742
|
+
let best;
|
|
743
|
+
let cost = base;
|
|
744
|
+
for (let i = 0; i < placed.length; i++) {
|
|
745
|
+
for (let j = i + 1; j < placed.length; j++) {
|
|
746
|
+
if (!free(placed[i]) || !free(placed[j]))
|
|
747
|
+
continue;
|
|
748
|
+
for (const [first, second] of [[i, j], [j, i]]) {
|
|
749
|
+
const out = [...placed];
|
|
750
|
+
const p1 = out[first];
|
|
751
|
+
const c1 = bestFor(scene, p1.unit, out.filter((_, m) => m !== i && m !== j), firstOrder);
|
|
752
|
+
out[first] = { ...p1, variant: c1.variant, level: c1.level, at: c1.at, ev: c1.ev };
|
|
753
|
+
const p2 = out[second];
|
|
754
|
+
const c2 = bestFor(scene, p2.unit, out.filter((_, m) => m !== second), firstOrder);
|
|
755
|
+
out[second] = { ...p2, variant: c2.variant, level: c2.level, at: c2.at, ev: c2.ev };
|
|
756
|
+
const c = totalCost(scene, out, firstOrder);
|
|
757
|
+
if (c < cost - 1e-6) {
|
|
758
|
+
cost = c;
|
|
759
|
+
best = out;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return best;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* A placed layout with its crossed tails undone: pairs of balloons whose
|
|
768
|
+
* tails cross (or all but touch) trade places or are placed again, each
|
|
769
|
+
* seeing the other's tail, as long as the whole layout gets cheaper. What
|
|
770
|
+
* {@link placeUnits} runs between its improvement passes, on its own.
|
|
771
|
+
*/
|
|
772
|
+
export function uncrossTails(panel, placed) {
|
|
773
|
+
const scene = sceneOf(panel);
|
|
774
|
+
const ordered = [...placed].sort((a, b) => a.unit.order - b.unit.order);
|
|
775
|
+
const firstOrder = ordered.find((p) => p.unit.kind !== 'sfx' && p.unit.kind !== 'note')?.unit.order ?? Number.NaN;
|
|
776
|
+
let layout = placed.map((p) => {
|
|
777
|
+
const others = placed.filter((o) => o !== p);
|
|
778
|
+
return { ...p, level: 0, ev: evaluate(scene, p.unit, p.variant, p.at, others, LEVELS[0], firstOrder) };
|
|
779
|
+
});
|
|
780
|
+
for (let k = 0; k < 4; k++) {
|
|
781
|
+
const next = swapCrossing(scene, layout, firstOrder);
|
|
782
|
+
if (!next)
|
|
783
|
+
break;
|
|
784
|
+
layout = next;
|
|
785
|
+
}
|
|
786
|
+
return layout.map(({ ev: _ev, level: _level, ...rest }) => rest);
|
|
787
|
+
}
|
|
788
|
+
/** How many pairs of tails of a layout cross (or all but touch). */
|
|
789
|
+
function crossings(scene, placed) {
|
|
790
|
+
let n = 0;
|
|
791
|
+
for (let i = 0; i < placed.length; i++) {
|
|
792
|
+
for (let j = i + 1; j < placed.length; j++) {
|
|
793
|
+
const a = placed[i];
|
|
794
|
+
const b = placed[j];
|
|
795
|
+
const ta = tailTargetOf(a.unit, scene, a.at);
|
|
796
|
+
const tb = tailTargetOf(b.unit, scene, b.at);
|
|
797
|
+
if (!ta || !tb || dist(ta, tb) <= Math.min(a.unit.em, b.unit.em))
|
|
798
|
+
continue;
|
|
799
|
+
const sa = tailSegment(a.unit, a.unit.variants[a.variant], a.at, ta);
|
|
800
|
+
const sb = tailSegment(b.unit, b.unit.variants[b.variant], b.at, tb);
|
|
801
|
+
if (segmentDistance(sa[0], sa[1], sb[0], sb[1]) < 0.6 * Math.min(a.unit.em, b.unit.em))
|
|
802
|
+
n++;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return n;
|
|
806
|
+
}
|
|
807
|
+
/** The whole layout's cost under the plain weights (hard faults first). */
|
|
808
|
+
function totalCost(scene, placed, firstOrder) {
|
|
809
|
+
let hard = 0;
|
|
810
|
+
let soft = 0;
|
|
811
|
+
for (let i = 0; i < placed.length; i++) {
|
|
812
|
+
const p = placed[i];
|
|
813
|
+
const ev = evaluate(scene, p.unit, p.variant, p.at, placed.filter((_, j) => j !== i), LEVELS[p.level], firstOrder);
|
|
814
|
+
hard += ev.hard;
|
|
815
|
+
soft += ev.soft;
|
|
816
|
+
}
|
|
817
|
+
return hard * 1000 + soft;
|
|
818
|
+
}
|
|
819
|
+
/** Height biases the solver is run with: a greedy pass that lifts every
|
|
820
|
+
* balloon higher first can leave room below for the later ones (the
|
|
821
|
+
* Kurlander order often wants the earlier balloons high); the layout with
|
|
822
|
+
* the lowest plain cost wins. */
|
|
823
|
+
const HEIGHT_BIASES = [1, 3, 7];
|
|
824
|
+
/** Place every unit of a panel (see the module comment). */
|
|
825
|
+
export function placeUnits(panel, units, passes = 3) {
|
|
826
|
+
const scene = sceneOf(panel);
|
|
827
|
+
const ordered = [...units].sort((a, b) => a.order - b.order || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
828
|
+
const firstOrder = ordered.find((u) => u.kind !== 'sfx' && u.kind !== 'note')?.order ?? Number.NaN;
|
|
829
|
+
let placed;
|
|
830
|
+
let bestCost = Number.POSITIVE_INFINITY;
|
|
831
|
+
const tries = (mode) => {
|
|
832
|
+
for (const bias of HEIGHT_BIASES) {
|
|
833
|
+
let run = solve({ ...scene, heightBias: bias }, ordered, firstOrder, passes, mode);
|
|
834
|
+
// Settle under the plain weights.
|
|
835
|
+
if (bias !== 1)
|
|
836
|
+
run = solveFrom(scene, run, firstOrder, passes);
|
|
837
|
+
const cost = totalCost(scene, run, firstOrder);
|
|
838
|
+
if (cost < bestCost - 1e-9) {
|
|
839
|
+
bestCost = cost;
|
|
840
|
+
placed = run;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
tries('reading');
|
|
845
|
+
// A layout with a fault left (a balloon out of the panel, over a face or
|
|
846
|
+
// another balloon): the greedy order may have boxed the last balloons
|
|
847
|
+
// in; try again placing the biggest first.
|
|
848
|
+
if (bestCost >= 1000 && ordered.length > 1)
|
|
849
|
+
tries('big');
|
|
850
|
+
// Tails still crossing (speakers standing in the order opposite to the
|
|
851
|
+
// one they speak in): try again from where the speakers stand, each
|
|
852
|
+
// balloon by its own speaker first.
|
|
853
|
+
if (ordered.length > 1 && crossings(scene, placed) > 0)
|
|
854
|
+
tries('speaker');
|
|
855
|
+
// Pairs placed again together, while that helps (a few rounds).
|
|
856
|
+
for (let round = 0; round < 3 && ordered.length > 1; round++) {
|
|
857
|
+
const next = replacePair(scene, placed, firstOrder);
|
|
858
|
+
if (!next)
|
|
859
|
+
break;
|
|
860
|
+
placed = solveFrom(scene, next, firstOrder, passes);
|
|
861
|
+
}
|
|
862
|
+
const final = placed;
|
|
863
|
+
const diagnostics = [];
|
|
864
|
+
for (let i = 0; i < final.length; i++) {
|
|
865
|
+
const p = final[i];
|
|
866
|
+
const others = final.filter((_, j) => j !== i);
|
|
867
|
+
const ev = evaluate(scene, p.unit, p.variant, p.at, others, LEVELS[p.level], firstOrder);
|
|
868
|
+
const fallbacks = [];
|
|
869
|
+
if (p.level >= 1 && p.unit.breakBorder)
|
|
870
|
+
fallbacks.push('breakBorder');
|
|
871
|
+
if (p.level >= 2)
|
|
872
|
+
fallbacks.push('coverAvoid');
|
|
873
|
+
if (p.unit.variants[p.variant].reshaped)
|
|
874
|
+
fallbacks.push('reshape');
|
|
875
|
+
if (ev.hard > 0) {
|
|
876
|
+
diagnostics.push({
|
|
877
|
+
itemId: p.unit.id,
|
|
878
|
+
panelIndex: panel.index,
|
|
879
|
+
sourceStart: p.unit.sourceStart,
|
|
880
|
+
sourceEnd: p.unit.sourceEnd,
|
|
881
|
+
reasons: [...ev.reasons].sort(),
|
|
882
|
+
fallbacks,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
final.sort((a, b) => a.unit.order - b.unit.order);
|
|
887
|
+
return { placed: final.map(({ ev: _ev, ...rest }) => rest), diagnostics };
|
|
888
|
+
}
|
|
889
|
+
//# sourceMappingURL=placement.js.map
|