@jasy/pdf 1.0.0-beta.3 → 1.0.0-beta.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/dist/api/color.d.ts +1 -24
- package/dist/api/color.js +2 -278
- package/dist/api/content.d.ts +10 -5
- package/dist/api/content.js +29 -13
- package/dist/api/descriptor.js +30 -3
- package/dist/api/flow.d.ts +55 -0
- package/dist/api/flow.js +71 -0
- package/dist/api/font-url.d.ts +1 -2
- package/dist/api/font-url.js +8 -74
- package/dist/api/forms.d.ts +3 -0
- package/dist/api/forms.js +3 -0
- package/dist/api/gradient.d.ts +1 -53
- package/dist/api/gradient.js +2 -97
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.js +3 -0
- package/dist/api/layout.d.ts +6 -0
- package/dist/api/layout.js +9 -3
- package/dist/api/page-builder.d.ts +4 -0
- package/dist/api/page-builder.js +4 -0
- package/dist/api/structure.d.ts +10 -2
- package/dist/api/structure.js +9 -6
- package/dist/api/table.d.ts +5 -3
- package/dist/api/table.js +101 -22
- package/dist/api/text.d.ts +8 -8
- package/dist/api/text.js +6 -0
- package/dist/canvas/painter.d.ts +2 -2
- package/dist/canvas/painter.js +2 -2
- package/dist/common/color-input.d.ts +24 -0
- package/dist/common/color-input.js +278 -0
- package/dist/common/diagnostics.d.ts +61 -0
- package/dist/common/diagnostics.js +77 -0
- package/dist/common/gradient.d.ts +53 -0
- package/dist/common/gradient.js +97 -0
- package/dist/edit/acroform-reader.js +7 -7
- package/dist/edit/appearance.js +3 -3
- package/dist/edit/attachments.d.ts +52 -0
- package/dist/edit/attachments.js +178 -0
- package/dist/edit/document.d.ts +38 -8
- package/dist/edit/document.js +208 -51
- package/dist/edit/fill.js +8 -23
- package/dist/edit/filters.d.ts +15 -0
- package/dist/edit/filters.js +157 -0
- package/dist/edit/index.d.ts +4 -1
- package/dist/edit/index.js +3 -1
- package/dist/edit/objects.d.ts +3 -4
- package/dist/edit/objects.js +5 -18
- package/dist/elements/canvas-element.d.ts +3 -0
- package/dist/elements/canvas-element.js +14 -3
- package/dist/elements/container-element.d.ts +8 -1
- package/dist/elements/container-element.js +49 -17
- package/dist/elements/forms/checkbox-element.d.ts +2 -0
- package/dist/elements/forms/checkbox-element.js +12 -3
- package/dist/elements/forms/choice-element.d.ts +6 -1
- package/dist/elements/forms/choice-element.js +17 -6
- package/dist/elements/forms/field-font.d.ts +8 -0
- package/dist/elements/forms/field-font.js +5 -0
- package/dist/elements/forms/push-button-element.d.ts +6 -1
- package/dist/elements/forms/push-button-element.js +16 -4
- package/dist/elements/forms/radio-element.d.ts +2 -0
- package/dist/elements/forms/radio-element.js +12 -3
- package/dist/elements/forms/signature-element.d.ts +6 -1
- package/dist/elements/forms/signature-element.js +16 -4
- package/dist/elements/forms/text-field-element.d.ts +6 -1
- package/dist/elements/forms/text-field-element.js +16 -4
- package/dist/elements/image-element.d.ts +70 -21
- package/dist/elements/image-element.js +173 -52
- package/dist/elements/index.d.ts +3 -0
- package/dist/elements/index.js +3 -0
- package/dist/elements/layout/anchor-element.d.ts +8 -10
- package/dist/elements/layout/anchor-element.js +10 -18
- package/dist/elements/layout/bookmark-element.d.ts +9 -11
- package/dist/elements/layout/bookmark-element.js +11 -20
- package/dist/elements/layout/column-break-element.d.ts +17 -0
- package/dist/elements/layout/column-break-element.js +26 -0
- package/dist/elements/layout/default-text-style-element.d.ts +11 -13
- package/dist/elements/layout/default-text-style-element.js +14 -31
- package/dist/elements/layout/deferred-element.d.ts +20 -4
- package/dist/elements/layout/deferred-element.js +41 -9
- package/dist/elements/layout/expanded-element.d.ts +11 -0
- package/dist/elements/layout/expanded-element.js +53 -18
- package/dist/elements/layout/flow-element.d.ts +109 -0
- package/dist/elements/layout/flow-element.js +317 -0
- package/dist/elements/layout/keep-together-element.d.ts +8 -18
- package/dist/elements/layout/keep-together-element.js +33 -45
- package/dist/elements/layout/link-element.d.ts +8 -13
- package/dist/elements/layout/link-element.js +9 -19
- package/dist/elements/layout/padding-element.d.ts +18 -1
- package/dist/elements/layout/padding-element.js +63 -28
- package/dist/elements/layout/page-break-element.d.ts +4 -1
- package/dist/elements/layout/page-break-element.js +6 -1
- package/dist/elements/layout/page-builder-element.d.ts +5 -0
- package/dist/elements/layout/page-builder-element.js +13 -2
- package/dist/elements/layout/positioned-element.d.ts +23 -4
- package/dist/elements/layout/positioned-element.js +42 -12
- package/dist/elements/layout/proxy-element.d.ts +47 -0
- package/dist/elements/layout/proxy-element.js +106 -0
- package/dist/elements/layout/region-element.d.ts +53 -0
- package/dist/elements/layout/region-element.js +108 -0
- package/dist/elements/layout/repeating-header-element.d.ts +9 -0
- package/dist/elements/layout/repeating-header-element.js +55 -13
- package/dist/elements/layout/rotated-box-element.d.ts +6 -0
- package/dist/elements/layout/rotated-box-element.js +27 -14
- package/dist/elements/layout/rotated-element.d.ts +9 -0
- package/dist/elements/layout/rotated-element.js +33 -2
- package/dist/elements/layout/struct-group.d.ts +9 -17
- package/dist/elements/layout/struct-group.js +10 -35
- package/dist/elements/line-element.d.ts +1 -0
- package/dist/elements/line-element.js +6 -0
- package/dist/elements/page-element.d.ts +10 -3
- package/dist/elements/page-element.js +39 -31
- package/dist/elements/pdf-document-element.d.ts +3 -0
- package/dist/elements/pdf-document-element.js +7 -0
- package/dist/elements/pdf-element.d.ts +105 -16
- package/dist/elements/pdf-element.js +118 -19
- package/dist/elements/rectangle-element.d.ts +22 -2
- package/dist/elements/rectangle-element.js +119 -55
- package/dist/elements/row-element.d.ts +17 -12
- package/dist/elements/row-element.js +89 -9
- package/dist/elements/svg-element.d.ts +30 -6
- package/dist/elements/svg-element.js +91 -26
- package/dist/elements/text-element.d.ts +48 -13
- package/dist/elements/text-element.js +161 -116
- package/dist/forms/acroform.d.ts +9 -1
- package/dist/forms/acroform.js +83 -34
- package/dist/forms/appearance.d.ts +17 -6
- package/dist/forms/appearance.js +18 -16
- package/dist/ir/display-list.d.ts +18 -3
- package/dist/layout/box-constraints.d.ts +17 -0
- package/dist/layout/box-constraints.js +31 -0
- package/dist/layout/collect-images.d.ts +6 -4
- package/dist/layout/collect-images.js +14 -8
- package/dist/layout/flex-item.d.ts +11 -0
- package/dist/layout/flex-item.js +51 -0
- package/dist/layout/fragmentation.d.ts +23 -9
- package/dist/layout/fragmentation.js +62 -19
- package/dist/layout/layout-cache.d.ts +53 -0
- package/dist/layout/layout-cache.js +110 -0
- package/dist/platform/browser-fs.d.ts +1 -2
- package/dist/platform/browser-fs.js +3 -6
- package/dist/platform/node-fs.d.ts +0 -1
- package/dist/platform/node-fs.js +0 -4
- package/dist/renderer/anchor-renderer.d.ts +2 -1
- package/dist/renderer/anchor-renderer.js +5 -4
- package/dist/renderer/bookmark-renderer.d.ts +2 -1
- package/dist/renderer/bookmark-renderer.js +5 -4
- package/dist/renderer/canvas-renderer.d.ts +2 -1
- package/dist/renderer/canvas-renderer.js +5 -2
- package/dist/renderer/column-break-renderer.d.ts +8 -0
- package/dist/renderer/column-break-renderer.js +15 -0
- package/dist/renderer/container-renderer.d.ts +2 -1
- package/dist/renderer/container-renderer.js +6 -5
- package/dist/renderer/default-text-style-renderer.d.ts +2 -1
- package/dist/renderer/default-text-style-renderer.js +2 -3
- package/dist/renderer/deferred-renderer.d.ts +2 -1
- package/dist/renderer/deferred-renderer.js +2 -3
- package/dist/renderer/emoji-image.js +3 -1
- package/dist/renderer/expanded-renderer.d.ts +2 -1
- package/dist/renderer/expanded-renderer.js +2 -3
- package/dist/renderer/flow-renderer.d.ts +16 -0
- package/dist/renderer/flow-renderer.js +21 -0
- package/dist/renderer/form-field-renderer.d.ts +2 -1
- package/dist/renderer/form-field-renderer.js +16 -3
- package/dist/renderer/image-renderer.d.ts +2 -1
- package/dist/renderer/image-renderer.js +33 -53
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/keep-together-renderer.d.ts +2 -1
- package/dist/renderer/keep-together-renderer.js +2 -3
- package/dist/renderer/line-renderer.d.ts +2 -1
- package/dist/renderer/line-renderer.js +7 -2
- package/dist/renderer/link-renderer.d.ts +2 -1
- package/dist/renderer/link-renderer.js +36 -5
- package/dist/renderer/padding-renderer.d.ts +2 -1
- package/dist/renderer/padding-renderer.js +5 -5
- package/dist/renderer/page-break-renderer.d.ts +6 -5
- package/dist/renderer/page-break-renderer.js +14 -7
- package/dist/renderer/page-builder-renderer.d.ts +2 -1
- package/dist/renderer/page-builder-renderer.js +2 -3
- package/dist/renderer/page-renderer.js +30 -10
- package/dist/renderer/pdf-backend.d.ts +4 -3
- package/dist/renderer/pdf-backend.js +14 -7
- package/dist/renderer/pdf-document-renderer.d.ts +1 -8
- package/dist/renderer/pdf-document-renderer.js +17 -17
- package/dist/renderer/pdf-renderer.js +20 -11
- package/dist/renderer/positioned-renderer.d.ts +18 -1
- package/dist/renderer/positioned-renderer.js +43 -6
- package/dist/renderer/rectangle-renderer.d.ts +2 -1
- package/dist/renderer/rectangle-renderer.js +18 -8
- package/dist/renderer/repeating-header-renderer.d.ts +2 -1
- package/dist/renderer/repeating-header-renderer.js +6 -5
- package/dist/renderer/rotated-renderer.d.ts +2 -1
- package/dist/renderer/rotated-renderer.js +9 -5
- package/dist/renderer/row-renderer.d.ts +2 -1
- package/dist/renderer/row-renderer.js +6 -5
- package/dist/renderer/struct-group-renderer.d.ts +2 -1
- package/dist/renderer/struct-group-renderer.js +2 -3
- package/dist/renderer/svg-renderer.d.ts +2 -1
- package/dist/renderer/svg-renderer.js +8 -2
- package/dist/renderer/text-renderer.d.ts +4 -6
- package/dist/renderer/text-renderer.js +114 -127
- package/dist/svg/gradients.js +5 -0
- package/dist/svg/index.d.ts +1 -1
- package/dist/svg/index.js +36 -6
- package/dist/svg/style.d.ts +2 -0
- package/dist/svg/style.js +12 -1
- package/dist/text/advance.d.ts +4 -0
- package/dist/text/advance.js +17 -4
- package/dist/text/bidi.d.ts +0 -2
- package/dist/text/bidi.js +0 -10
- package/dist/text/glyph-coverage.d.ts +1 -1
- package/dist/text/glyph-coverage.js +7 -3
- package/dist/text/hyphen-breaks.d.ts +17 -0
- package/dist/text/hyphen-breaks.js +54 -0
- package/dist/text/line-breaker.d.ts +55 -29
- package/dist/text/line-breaker.js +453 -188
- package/dist/text/line-metrics.d.ts +8 -3
- package/dist/text/line-metrics.js +11 -2
- package/dist/text/pdf-text-string.d.ts +21 -0
- package/dist/text/pdf-text-string.js +114 -0
- package/dist/text/text-decoration.d.ts +5 -5
- package/dist/text/text-decoration.js +5 -5
- package/dist/text/word-splitting.d.ts +15 -2
- package/dist/text/word-splitting.js +45 -13
- package/dist/utils/afm-parser.d.ts +2 -0
- package/dist/utils/afm-parser.js +10 -1
- package/dist/utils/dest-registry.d.ts +1 -1
- package/dist/utils/dest-registry.js +3 -2
- package/dist/utils/fetch-bytes.d.ts +19 -0
- package/dist/utils/fetch-bytes.js +79 -0
- package/dist/utils/flex-layout.d.ts +16 -14
- package/dist/utils/flex-layout.js +427 -217
- package/dist/utils/font-metrics.d.ts +4 -3
- package/dist/utils/image-helper.d.ts +59 -11
- package/dist/utils/image-helper.js +209 -81
- package/dist/utils/pdf-object-manager.d.ts +17 -6
- package/dist/utils/pdf-object-manager.js +70 -20
- package/dist/utils/renderer-registry.d.ts +49 -2
- package/dist/utils/renderer-registry.js +40 -0
- package/dist/utils/struct-tree.d.ts +24 -0
- package/dist/utils/struct-tree.js +61 -9
- package/dist/utils/ttf-parser.d.ts +4 -0
- package/dist/utils/ttf-parser.js +30 -6
- package/dist/validators/element-validator.js +2 -1
- package/package.json +1 -1
- package/dist/elements/layout/sized-container-element.d.ts +0 -14
- package/dist/elements/layout/sized-container-element.js +0 -32
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { BoxConstraints } from "../../layout/box-constraints.js";
|
|
2
|
+
import { reportClipped } from "../../layout/fragmentation.js";
|
|
3
|
+
import { Diagnostics, pageOf } from "../../common/diagnostics.js";
|
|
4
|
+
import { SizedPDFElement } from "../pdf-element.js";
|
|
5
|
+
import { PageBuilderElement } from "./page-builder-element.js";
|
|
6
|
+
import { RegionElement } from "./region-element.js";
|
|
7
|
+
/** A template with no `Region` would place nothing on every page, forever. */
|
|
8
|
+
export class FlowWithoutRegionError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("@jasy/pdf: a Flow's template holds no Region, so the content has nowhere to go. Put at least " +
|
|
11
|
+
"one Region in the template (in @jasy/vue: the #page slot).");
|
|
12
|
+
this.name = "FlowWithoutRegionError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** A template that leaves every Region without room, where the Flow cannot move on to find more. */
|
|
16
|
+
export class FlowRegionsEmptyError extends Error {
|
|
17
|
+
constructor(width, height) {
|
|
18
|
+
super(`@jasy/pdf: a Flow's template leaves its Regions no room in ${Math.round(width)} x ` +
|
|
19
|
+
`${Math.round(height)}pt - every Region is 0 wide or 0 tall (too many columns or gaps for the ` +
|
|
20
|
+
"width, or the rest of the template is taller than the space). Give the Regions room." +
|
|
21
|
+
(height <= 0
|
|
22
|
+
? " In a wrapping Row a line is only as tall as its content - give the Row around the " +
|
|
23
|
+
"Flow a height."
|
|
24
|
+
: ""));
|
|
25
|
+
this.name = "FlowRegionsEmptyError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Laid out with no height to fill: every line would land in the first Region. */
|
|
29
|
+
export class FlowUnboundedError extends Error {
|
|
30
|
+
constructor() {
|
|
31
|
+
super("@jasy/pdf: a Flow takes its height from where it sits - the page, or a box with a height. " +
|
|
32
|
+
"Here it has none (in a Row without a height, for instance). Give the Row or a Box around " +
|
|
33
|
+
"the Flow a height.");
|
|
34
|
+
this.name = "FlowUnboundedError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** A PageBuilder builds again for the page it is drawn on, so Regions inside it lose their content. */
|
|
38
|
+
export class RegionInPageBuilderError extends Error {
|
|
39
|
+
constructor() {
|
|
40
|
+
super("@jasy/pdf: a Region inside a PageBuilder - it builds again for the page it is drawn on, and " +
|
|
41
|
+
"its Regions would come back empty. A Flow's template is built for every page already and " +
|
|
42
|
+
"gets { index, pageNumber }: build the Regions there.");
|
|
43
|
+
this.name = "RegionInPageBuilderError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** The Regions of a laid-out template, in tree order - the order the content flows through them. */
|
|
47
|
+
function regionsIn(root) {
|
|
48
|
+
const out = [];
|
|
49
|
+
const visit = (el, built) => {
|
|
50
|
+
if (el instanceof RegionElement) {
|
|
51
|
+
if (built)
|
|
52
|
+
throw new RegionInPageBuilderError();
|
|
53
|
+
out.push(el);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const inBuilder = built || el instanceof PageBuilderElement;
|
|
57
|
+
for (const child of el.layoutChildren())
|
|
58
|
+
visit(child, inBuilder);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
visit(root, false);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
const room = (r) => r.getProps().width > 0 && r.getProps().height > 0;
|
|
65
|
+
const SLACK_EPSILON = 1e-6;
|
|
66
|
+
// How close the last page's height gets to the tightest one that holds the content, in points.
|
|
67
|
+
const SHRINK_PRECISION = 0.01;
|
|
68
|
+
/**
|
|
69
|
+
* One page of a Flow: its template, with each Region holding its content. It is drawn laid out at the
|
|
70
|
+
* height it was filled at - not at its own size, which a template that does not fill (`50%`) differs from.
|
|
71
|
+
*/
|
|
72
|
+
export class FlowPieceElement extends SizedPDFElement {
|
|
73
|
+
constructor(template, size, bound, page) {
|
|
74
|
+
super({ x: 0, y: 0, ...size });
|
|
75
|
+
this.template = template;
|
|
76
|
+
this.size = size;
|
|
77
|
+
this.bound = bound;
|
|
78
|
+
this.page = page;
|
|
79
|
+
}
|
|
80
|
+
calculateLayout(_constraints, offset, ctx) {
|
|
81
|
+
this.x = offset.x;
|
|
82
|
+
this.y = offset.y;
|
|
83
|
+
this.template.calculateLayout(BoxConstraints.loose(this.size.width, this.bound), { x: 0, y: 0 }, { ...ctx, flowPage: this.page });
|
|
84
|
+
return this.size;
|
|
85
|
+
}
|
|
86
|
+
computeSize() {
|
|
87
|
+
return this.size;
|
|
88
|
+
}
|
|
89
|
+
layoutChildren() {
|
|
90
|
+
return [this.template];
|
|
91
|
+
}
|
|
92
|
+
getProps() {
|
|
93
|
+
return { x: this.x, y: this.y, ...this.size, child: this.template };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Content flowing through the Regions of a template, page after page. It has no height of its own -
|
|
98
|
+
* like an `Expanded`, it takes what it is given: the rest of the page when it is fragmented, the rest
|
|
99
|
+
* of a box with a height otherwise. On its last page it shrinks to what the content used.
|
|
100
|
+
*/
|
|
101
|
+
export class FlowElement extends SizedPDFElement {
|
|
102
|
+
constructor({ content, template, index = 0, balance = false, }) {
|
|
103
|
+
super({ x: 0, y: 0 });
|
|
104
|
+
this.content = content;
|
|
105
|
+
this.template = template;
|
|
106
|
+
this.index = index;
|
|
107
|
+
this.balance = balance;
|
|
108
|
+
}
|
|
109
|
+
needsRegion() {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
/** Its height comes from the parent, as an `Expanded`'s does. */
|
|
113
|
+
needsBoundedMain(horizontal) {
|
|
114
|
+
return !horizontal;
|
|
115
|
+
}
|
|
116
|
+
/** The template is built from the page it lands on. */
|
|
117
|
+
dependsOnPage() {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
fragment(maxHeight, width, ctx) {
|
|
121
|
+
// Below other content, a template with no room here may find it on the next page.
|
|
122
|
+
const filled = this.fill(maxHeight, width, ctx, !ctx.belowContent);
|
|
123
|
+
// Nothing fit: move on whole. On a fresh page the packer forces it on, so this always advances.
|
|
124
|
+
if (!filled.placed)
|
|
125
|
+
return { fitted: null, remainder: this };
|
|
126
|
+
return {
|
|
127
|
+
fitted: filled.piece,
|
|
128
|
+
remainder: filled.rest
|
|
129
|
+
? new FlowElement({
|
|
130
|
+
content: filled.rest,
|
|
131
|
+
template: this.template,
|
|
132
|
+
index: this.index + 1,
|
|
133
|
+
balance: this.balance,
|
|
134
|
+
})
|
|
135
|
+
: null,
|
|
136
|
+
forceBreak: filled.forceBreak,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
calculateLayout(constraints, offset, ctx) {
|
|
140
|
+
if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
|
|
141
|
+
throw new FlowUnboundedError();
|
|
142
|
+
}
|
|
143
|
+
this.x = offset.x;
|
|
144
|
+
this.y = offset.y;
|
|
145
|
+
const filled = this.fill(constraints.maxHeight, constraints.maxWidth, ctx, true);
|
|
146
|
+
// Not paginated here (a box with a height): what does not fit is cut, as any overflow is.
|
|
147
|
+
if (filled.rest) {
|
|
148
|
+
reportClipped(this, "a Flow has more content than its Regions hold here, and it cannot continue on a next " +
|
|
149
|
+
"page (it sits in a box with a height) - give the box more height, or let the Flow " +
|
|
150
|
+
"continue on the page.", ctx);
|
|
151
|
+
}
|
|
152
|
+
this.piece = filled.piece;
|
|
153
|
+
const size = this.piece.calculateLayout(constraints, { x: 0, y: 0 }, ctx);
|
|
154
|
+
this.width = size.width;
|
|
155
|
+
this.height = size.height;
|
|
156
|
+
return size;
|
|
157
|
+
}
|
|
158
|
+
/** Unbounded, it has no height of its own (a parent that bounds it asks again); bounded, it fills. */
|
|
159
|
+
computeSize(constraints, ctx) {
|
|
160
|
+
if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
|
|
161
|
+
return { width: constraints.hasBoundedWidth ? constraints.maxWidth : 0, height: 0 };
|
|
162
|
+
}
|
|
163
|
+
return this.fill(constraints.maxHeight, constraints.maxWidth, ctx, false).piece.measure(constraints, ctx);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The one place a page of the Flow is made: the template at `width` x `maxHeight`, the content
|
|
167
|
+
* fragmented into its Regions in turn, and - when the content ends here - the template shrunk to
|
|
168
|
+
* what it used, or balanced.
|
|
169
|
+
*/
|
|
170
|
+
fill(maxHeight, width, ctx, final) {
|
|
171
|
+
const template = this.template({ index: this.index, pageNumber: pageOf(ctx) ?? 1 });
|
|
172
|
+
// Laid out as this page of the Flow, its Regions claim themselves - wherever the template made
|
|
173
|
+
// them, a PageBuilder or a Table included - and are collected from the laid-out tree.
|
|
174
|
+
const pageCtx = { ...ctx, flowPage: this };
|
|
175
|
+
const layOut = (height) => template.calculateLayout(BoxConstraints.loose(width, height), { x: 0, y: 0 }, pageCtx);
|
|
176
|
+
const size = layOut(maxHeight);
|
|
177
|
+
const regions = regionsIn(template);
|
|
178
|
+
if (regions.length === 0)
|
|
179
|
+
throw new FlowWithoutRegionError();
|
|
180
|
+
// No room here: every Region is empty, or the rest of the template (its height with the Regions
|
|
181
|
+
// empty) is taller than the space.
|
|
182
|
+
const own = template.measure(BoxConstraints.loose(width, Infinity), ctx).height;
|
|
183
|
+
if (own > maxHeight + SLACK_EPSILON || !regions.some(room)) {
|
|
184
|
+
if (final)
|
|
185
|
+
throw new FlowRegionsEmptyError(width, maxHeight);
|
|
186
|
+
const piece = new FlowPieceElement(template, size, maxHeight, this);
|
|
187
|
+
return { piece, rest: this.content, placed: false, forceBreak: false };
|
|
188
|
+
}
|
|
189
|
+
const { rest, placed, forceBreak } = this.pour(regions, ctx);
|
|
190
|
+
const ends = !rest && placed;
|
|
191
|
+
const last = !ends
|
|
192
|
+
? null
|
|
193
|
+
: this.balance && regions.filter(room).length > 1
|
|
194
|
+
? this.balanced(regions, own, maxHeight, layOut, ctx)
|
|
195
|
+
: this.shrink(regions, own, maxHeight, layOut, ctx);
|
|
196
|
+
const piece = last
|
|
197
|
+
? new FlowPieceElement(template, last.size, last.bound, this)
|
|
198
|
+
: new FlowPieceElement(template, size, maxHeight, this);
|
|
199
|
+
return { piece, rest, placed, forceBreak };
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The content fragmented into the laid-out Regions in turn; each holds its piece. `fresh` is how tall
|
|
203
|
+
* each Region is on a full page - what a block that does not fit here may wait for.
|
|
204
|
+
*/
|
|
205
|
+
pour(regions, ctx, fresh) {
|
|
206
|
+
let rest = this.content;
|
|
207
|
+
let placed = false;
|
|
208
|
+
let forceBreak = false;
|
|
209
|
+
for (const region of regions)
|
|
210
|
+
region.hold(null);
|
|
211
|
+
for (const region of regions) {
|
|
212
|
+
if (!rest)
|
|
213
|
+
break;
|
|
214
|
+
if (!room(region))
|
|
215
|
+
continue;
|
|
216
|
+
const { width: w, height: h } = region.getProps();
|
|
217
|
+
// Each Region is a fragmentation region of its own: a page break inside it is measured by it.
|
|
218
|
+
const split = rest.fragment(h, w, {
|
|
219
|
+
...ctx,
|
|
220
|
+
pageBodyHeight: fresh?.get(region) ?? h,
|
|
221
|
+
inFlowRegion: true,
|
|
222
|
+
flowPage: undefined,
|
|
223
|
+
});
|
|
224
|
+
region.hold(split.fitted);
|
|
225
|
+
if (split.fitted)
|
|
226
|
+
placed = true;
|
|
227
|
+
rest = split.remainder;
|
|
228
|
+
// A column break is this Flow's: on to the next Region (or the next page's first).
|
|
229
|
+
if (split.forceBreak === "column")
|
|
230
|
+
continue;
|
|
231
|
+
if (split.forceBreak) {
|
|
232
|
+
forceBreak = true;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { rest, placed, forceBreak };
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The last page, balanced: the smallest height at which the content, poured again, still ends here,
|
|
240
|
+
* so the Regions come out about equally full. Every candidate is a real pour - breaks,
|
|
241
|
+
* `keepTogether` and orphans hold as on any page - and the last one poured is the one drawn. A
|
|
242
|
+
* candidate is silent, and one that had to clip a block it could not break is too low.
|
|
243
|
+
*/
|
|
244
|
+
balanced(regions, own, maxHeight, layOut, ctx) {
|
|
245
|
+
const fresh = new Map(regions.map((r) => [r, r.getProps().height]));
|
|
246
|
+
const endsAt = (bound) => {
|
|
247
|
+
layOut(bound);
|
|
248
|
+
let clipped = false;
|
|
249
|
+
const trial = new Diagnostics();
|
|
250
|
+
trial.setHandler((d) => (clipped || (clipped = d.code === "overflow-clipped")));
|
|
251
|
+
const poured = this.pour(regions, { ...ctx, onOverflow: "ignore", diagnostics: trial }, fresh);
|
|
252
|
+
return !poured.rest && poured.placed && !clipped;
|
|
253
|
+
};
|
|
254
|
+
let low = Math.min(maxHeight, own);
|
|
255
|
+
let high = maxHeight;
|
|
256
|
+
while (high - low > SHRINK_PRECISION) {
|
|
257
|
+
const mid = (low + high) / 2;
|
|
258
|
+
if (endsAt(mid))
|
|
259
|
+
high = mid;
|
|
260
|
+
else
|
|
261
|
+
low = mid;
|
|
262
|
+
}
|
|
263
|
+
const size = layOut(high);
|
|
264
|
+
this.pour(regions, ctx, fresh);
|
|
265
|
+
return { size, bound: high };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* The last page: the smallest height at which every filled Region still holds what it was given.
|
|
269
|
+
* Only a layout knows how a template's Regions answer a height (`%`, stacked, `minHeight`, fixed), so
|
|
270
|
+
* each candidate is laid out and checked. The first is exact for Regions side by side (the full bound
|
|
271
|
+
* less the smallest slack); below it the bound is searched, down to the template's own height.
|
|
272
|
+
* Content that overflowed its Region at full height asks for no more than it had there.
|
|
273
|
+
*/
|
|
274
|
+
shrink(regions, own, maxHeight, layOut, ctx) {
|
|
275
|
+
const filled = regions.filter((r) => r.held);
|
|
276
|
+
const needed = new Map(filled.map((r) => {
|
|
277
|
+
const { width, height } = r.getProps();
|
|
278
|
+
const used = r.held.measure(BoxConstraints.loose(width, Infinity), ctx).height;
|
|
279
|
+
return [r, Math.min(used, height)];
|
|
280
|
+
}));
|
|
281
|
+
const slack = Math.min(...filled.map((r) => r.getProps().height - needed.get(r)));
|
|
282
|
+
const fitsAt = (bound) => {
|
|
283
|
+
const size = layOut(bound);
|
|
284
|
+
const fits = filled.every((r) => r.getProps().height + SLACK_EPSILON >= needed.get(r));
|
|
285
|
+
return fits ? { size, bound } : null;
|
|
286
|
+
};
|
|
287
|
+
let low = Math.min(maxHeight, own);
|
|
288
|
+
let high = maxHeight;
|
|
289
|
+
const exact = fitsAt(Math.max(low, maxHeight - slack));
|
|
290
|
+
if (exact) {
|
|
291
|
+
if (exact.bound === low)
|
|
292
|
+
return exact;
|
|
293
|
+
high = exact.bound;
|
|
294
|
+
}
|
|
295
|
+
while (high - low > SHRINK_PRECISION) {
|
|
296
|
+
const mid = (low + high) / 2;
|
|
297
|
+
if (fitsAt(mid))
|
|
298
|
+
high = mid;
|
|
299
|
+
else
|
|
300
|
+
low = mid;
|
|
301
|
+
}
|
|
302
|
+
return { size: layOut(high), bound: high };
|
|
303
|
+
}
|
|
304
|
+
layoutChildren() {
|
|
305
|
+
return [this.content];
|
|
306
|
+
}
|
|
307
|
+
getProps() {
|
|
308
|
+
return {
|
|
309
|
+
x: this.x,
|
|
310
|
+
y: this.y,
|
|
311
|
+
width: this.width,
|
|
312
|
+
height: this.height,
|
|
313
|
+
child: this.content,
|
|
314
|
+
piece: this.piece,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Fragmentable, FragmentResult } from "../../layout/fragmentation.js";
|
|
1
|
+
import { FragmentResult } from "../../layout/fragmentation.js";
|
|
3
2
|
import { LayoutContext, PDFElement } from "../pdf-element.js";
|
|
3
|
+
import { ProxyElement } from "./proxy-element.js";
|
|
4
4
|
/**
|
|
5
|
-
* A layout-
|
|
6
|
-
*
|
|
7
|
-
* unwrapped content; only pagination behaves differently.
|
|
5
|
+
* A layout-transparent wrapper (a `ProxyElement`) that tries to keep its content on ONE page (CSS
|
|
6
|
+
* `break-inside: avoid`). Only pagination behaves differently from the unwrapped content.
|
|
8
7
|
*
|
|
9
8
|
* The rule, decided in `fragment`:
|
|
10
9
|
* 1. Fits in the space offered here -> place it whole.
|
|
@@ -20,23 +19,14 @@ import { LayoutContext, PDFElement } from "../pdf-element.js";
|
|
|
20
19
|
* keeping it together: the break wins (CSS behaves the same), and we WARN once rather than silently
|
|
21
20
|
* swallowing either intent.
|
|
22
21
|
*/
|
|
23
|
-
export declare class KeepTogetherElement extends
|
|
24
|
-
private child;
|
|
25
|
-
private warnedBreakConflict;
|
|
22
|
+
export declare class KeepTogetherElement extends ProxyElement {
|
|
26
23
|
constructor({ child }: {
|
|
27
24
|
child: PDFElement;
|
|
28
25
|
});
|
|
29
|
-
|
|
30
|
-
minIntrinsicMain(horizontal: boolean, ctx: LayoutContext): number;
|
|
31
|
-
calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
|
|
32
|
-
relativeSizeFactor(horizontal: boolean): number | undefined;
|
|
33
|
-
needsBoundedMain(horizontal: boolean): boolean;
|
|
34
|
-
hasForcedBreak(): boolean;
|
|
35
|
-
breaksBefore(): boolean;
|
|
36
|
-
breaksAfter(): boolean;
|
|
26
|
+
protected rewrap(child: PDFElement): PDFElement;
|
|
37
27
|
fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
|
|
38
|
-
/**
|
|
39
|
-
*
|
|
28
|
+
/** Split like any wrapper - once we have decided to break the group, the pieces flow bare but keep
|
|
29
|
+
* the group's place in the flex line (any INNER keepTogether lives in the child and still vetoes). */
|
|
40
30
|
private splitChild;
|
|
41
31
|
getProps(): {
|
|
42
32
|
child: PDFElement;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { BoxConstraints } from "../../layout/box-constraints.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { ProxyElement } from "./proxy-element.js";
|
|
3
|
+
import { diagnose, pageOf } from "../../common/diagnostics.js";
|
|
4
4
|
/**
|
|
5
|
-
* A layout-
|
|
6
|
-
*
|
|
7
|
-
* unwrapped content; only pagination behaves differently.
|
|
5
|
+
* A layout-transparent wrapper (a `ProxyElement`) that tries to keep its content on ONE page (CSS
|
|
6
|
+
* `break-inside: avoid`). Only pagination behaves differently from the unwrapped content.
|
|
8
7
|
*
|
|
9
8
|
* The rule, decided in `fragment`:
|
|
10
9
|
* 1. Fits in the space offered here -> place it whole.
|
|
@@ -20,50 +19,41 @@ import { PDFElement } from "../pdf-element.js";
|
|
|
20
19
|
* keeping it together: the break wins (CSS behaves the same), and we WARN once rather than silently
|
|
21
20
|
* swallowing either intent.
|
|
22
21
|
*/
|
|
23
|
-
export class KeepTogetherElement extends
|
|
22
|
+
export class KeepTogetherElement extends ProxyElement {
|
|
24
23
|
constructor({ child }) {
|
|
25
|
-
super();
|
|
26
|
-
this.warnedBreakConflict = false;
|
|
27
|
-
this.child = child;
|
|
24
|
+
super(child);
|
|
28
25
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
calculateLayout(constraints, offset, ctx) {
|
|
34
|
-
return this.child.calculateLayout(constraints, offset, ctx);
|
|
35
|
-
}
|
|
36
|
-
// Transparent: forward the sizing hints the flex parent reads, so a %-sized or Spacer-holding child
|
|
37
|
-
// keeps working when it is wrapped (the wrapper must not swallow these).
|
|
38
|
-
relativeSizeFactor(horizontal) {
|
|
39
|
-
return this.child.relativeSizeFactor(horizontal);
|
|
40
|
-
}
|
|
41
|
-
needsBoundedMain(horizontal) {
|
|
42
|
-
return this.child.needsBoundedMain(horizontal);
|
|
43
|
-
}
|
|
44
|
-
// Transparent to break control: a break-before/after on the wrapped element still applies to the group
|
|
45
|
-
// as a whole, and an inner forced break still forces the parent to fragment us (so `fragment` runs).
|
|
46
|
-
hasForcedBreak() {
|
|
47
|
-
return this.child.hasForcedBreak();
|
|
48
|
-
}
|
|
49
|
-
breaksBefore() {
|
|
50
|
-
return this.child.breaksBefore();
|
|
51
|
-
}
|
|
52
|
-
breaksAfter() {
|
|
53
|
-
return this.child.breaksAfter();
|
|
26
|
+
// Once the group is broken, its pieces flow bare (see `splitChild`).
|
|
27
|
+
rewrap(child) {
|
|
28
|
+
return child;
|
|
54
29
|
}
|
|
55
30
|
fragment(maxHeight, width, ctx) {
|
|
56
31
|
// Contradiction: a forced break inside the group beats keeping it together. Split at the break by
|
|
57
32
|
// delegating to the child (which honours it), and say so once - never keep such a group whole.
|
|
58
33
|
if (this.child.hasForcedBreak()) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
34
|
+
diagnose(ctx.diagnostics, {
|
|
35
|
+
code: "keep-together-split",
|
|
36
|
+
severity: "warning",
|
|
37
|
+
message: "keepTogether contains a forced break (PageBreak, ColumnBreak, breakBefore / breakAfter). " +
|
|
38
|
+
"The break wins and the group is split. Remove the break, or the keepTogether, to resolve this.",
|
|
39
|
+
about: this,
|
|
40
|
+
page: pageOf(ctx),
|
|
41
|
+
});
|
|
64
42
|
return this.splitChild(maxHeight, width, ctx);
|
|
65
43
|
}
|
|
66
|
-
|
|
44
|
+
// A region-sized child (a Flow) has no height of its own to compare, so the same three rules ask
|
|
45
|
+
// whether it ENDS here, or on a fresh page.
|
|
46
|
+
if (this.child.needsRegion()) {
|
|
47
|
+
const here = this.splitChild(maxHeight, width, ctx);
|
|
48
|
+
if (!here.remainder)
|
|
49
|
+
return here;
|
|
50
|
+
const pageBody = ctx.pageBodyHeight ?? maxHeight;
|
|
51
|
+
if (maxHeight < pageBody && !this.splitChild(pageBody, width, ctx).remainder) {
|
|
52
|
+
return { fitted: null, remainder: this };
|
|
53
|
+
}
|
|
54
|
+
return here;
|
|
55
|
+
}
|
|
56
|
+
const natural = this.child.measure(BoxConstraints.loose(width, Infinity), ctx).height;
|
|
67
57
|
// 1. Fits in the offered space: keep whole.
|
|
68
58
|
if (natural <= maxHeight)
|
|
69
59
|
return { fitted: this, remainder: null };
|
|
@@ -76,12 +66,10 @@ export class KeepTogetherElement extends PDFElement {
|
|
|
76
66
|
// 3. Bigger than a whole page: it can never be kept together, so split it.
|
|
77
67
|
return this.splitChild(maxHeight, width, ctx);
|
|
78
68
|
}
|
|
79
|
-
/**
|
|
80
|
-
*
|
|
69
|
+
/** Split like any wrapper - once we have decided to break the group, the pieces flow bare but keep
|
|
70
|
+
* the group's place in the flex line (any INNER keepTogether lives in the child and still vetoes). */
|
|
81
71
|
splitChild(maxHeight, width, ctx) {
|
|
82
|
-
|
|
83
|
-
return { fitted: null, remainder: this };
|
|
84
|
-
return this.child.fragment(maxHeight, width, ctx);
|
|
72
|
+
return super.fragment(maxHeight, width, ctx);
|
|
85
73
|
}
|
|
86
74
|
getProps() {
|
|
87
75
|
return { child: this.child };
|
|
@@ -1,26 +1,21 @@
|
|
|
1
|
-
import { PDFElement
|
|
2
|
-
import {
|
|
1
|
+
import { PDFElement } from "../pdf-element.js";
|
|
2
|
+
import { ProxyElement } from "./proxy-element.js";
|
|
3
3
|
/**
|
|
4
4
|
* Makes its child a clickable hyperlink. `href` is an external URL; `dest` is an internal named
|
|
5
|
-
* destination (an `Anchor` elsewhere in the document) - exactly one is set. Layout-transparent
|
|
6
|
-
*
|
|
7
|
-
* a
|
|
8
|
-
*
|
|
5
|
+
* destination (an `Anchor` elsewhere in the document) - exactly one is set. Layout-transparent (a
|
|
6
|
+
* `ProxyElement`); the renderer turns the child's box into a `Link` IR node (and the page renderer into
|
|
7
|
+
* a /Link annotation). The link draws nothing itself - style the child (e.g. a blue Text) if you want it
|
|
8
|
+
* to look like a link. Split across pages, every piece stays clickable.
|
|
9
9
|
*/
|
|
10
|
-
export declare class LinkElement extends
|
|
10
|
+
export declare class LinkElement extends ProxyElement {
|
|
11
11
|
private href?;
|
|
12
12
|
private dest?;
|
|
13
|
-
private child;
|
|
14
|
-
private x;
|
|
15
|
-
private y;
|
|
16
|
-
private width;
|
|
17
|
-
private height;
|
|
18
13
|
constructor({ href, dest, child }: {
|
|
19
14
|
href?: string;
|
|
20
15
|
dest?: string;
|
|
21
16
|
child: PDFElement;
|
|
22
17
|
});
|
|
23
|
-
|
|
18
|
+
protected rewrap(child: PDFElement): PDFElement;
|
|
24
19
|
getProps(): {
|
|
25
20
|
href: string | undefined;
|
|
26
21
|
dest: string | undefined;
|
|
@@ -1,29 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ProxyElement } from "./proxy-element.js";
|
|
2
2
|
/**
|
|
3
3
|
* Makes its child a clickable hyperlink. `href` is an external URL; `dest` is an internal named
|
|
4
|
-
* destination (an `Anchor` elsewhere in the document) - exactly one is set. Layout-transparent
|
|
5
|
-
*
|
|
6
|
-
* a
|
|
7
|
-
*
|
|
4
|
+
* destination (an `Anchor` elsewhere in the document) - exactly one is set. Layout-transparent (a
|
|
5
|
+
* `ProxyElement`); the renderer turns the child's box into a `Link` IR node (and the page renderer into
|
|
6
|
+
* a /Link annotation). The link draws nothing itself - style the child (e.g. a blue Text) if you want it
|
|
7
|
+
* to look like a link. Split across pages, every piece stays clickable.
|
|
8
8
|
*/
|
|
9
|
-
export class LinkElement extends
|
|
9
|
+
export class LinkElement extends ProxyElement {
|
|
10
10
|
constructor({ href, dest, child }) {
|
|
11
|
-
super();
|
|
12
|
-
this.x = 0;
|
|
13
|
-
this.y = 0;
|
|
14
|
-
this.width = 0;
|
|
15
|
-
this.height = 0;
|
|
11
|
+
super(child);
|
|
16
12
|
this.href = href;
|
|
17
13
|
this.dest = dest;
|
|
18
|
-
this.child = child;
|
|
19
14
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.x = offset.x;
|
|
23
|
-
this.y = offset.y;
|
|
24
|
-
this.width = size.width;
|
|
25
|
-
this.height = size.height;
|
|
26
|
-
return size;
|
|
15
|
+
rewrap(child) {
|
|
16
|
+
return new LinkElement({ href: this.href, dest: this.dest, child });
|
|
27
17
|
}
|
|
28
18
|
getProps() {
|
|
29
19
|
return {
|
|
@@ -5,11 +5,21 @@ import { type EdgeSpecs } from "../../layout/insets.js";
|
|
|
5
5
|
interface PaddingElementParams extends WithChild {
|
|
6
6
|
/** Still unresolved: a side may be a percentage of the offered WIDTH (see `Insets`). */
|
|
7
7
|
margin: EdgeSpecs;
|
|
8
|
+
/** This is a `Box`'s own padding: a percentage resolves against what the Box was offered
|
|
9
|
+
* (`ctx.boxPaddingBase`), as CSS resolves it against the containing block. */
|
|
10
|
+
ofBox?: boolean;
|
|
8
11
|
}
|
|
9
12
|
export declare class PaddingElement extends SizedPDFElement implements Fragmentable {
|
|
10
13
|
private child;
|
|
11
14
|
private margin;
|
|
12
|
-
|
|
15
|
+
readonly ofBox: boolean;
|
|
16
|
+
constructor({ margin, child, ofBox }: PaddingElementParams);
|
|
17
|
+
/** Some side is a percentage. */
|
|
18
|
+
get hasPercentSide(): boolean;
|
|
19
|
+
/** The width a percentage side resolves against, and the context the child sees: a Box's base is
|
|
20
|
+
* for this padding alone, so it does not travel further down. */
|
|
21
|
+
private percentBase;
|
|
22
|
+
private childContext;
|
|
13
23
|
/**
|
|
14
24
|
* Splits the padded box across pages (box-decoration-break: clone - every fragment
|
|
15
25
|
* keeps its full top/bottom inset). The child is fragmented into the space left after
|
|
@@ -17,6 +27,9 @@ export declare class PaddingElement extends SizedPDFElement implements Fragmenta
|
|
|
17
27
|
* child can't be split, the whole padding moves on as the remainder.
|
|
18
28
|
*/
|
|
19
29
|
fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
|
|
30
|
+
/** A wrapper: it needs a bound exactly when its child does, and passes the bound on. */
|
|
31
|
+
needsBoundedMain(horizontal: boolean): boolean;
|
|
32
|
+
needsRegion(): boolean;
|
|
20
33
|
hasForcedBreak(): boolean;
|
|
21
34
|
breaksBefore(): boolean;
|
|
22
35
|
breaksAfter(): boolean;
|
|
@@ -27,6 +40,10 @@ export declare class PaddingElement extends SizedPDFElement implements Fragmenta
|
|
|
27
40
|
*/
|
|
28
41
|
minIntrinsicMain(horizontal: boolean, ctx: LayoutContext): number;
|
|
29
42
|
calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
|
|
43
|
+
protected computeSize(constraints: BoxConstraints, ctx: LayoutContext): Size;
|
|
44
|
+
/** The one sizing path: `layout` places the child (relative to this box), `measure` only sizes it. */
|
|
45
|
+
private sizeWith;
|
|
46
|
+
layoutChildren(): readonly PDFElement[];
|
|
30
47
|
getProps(): {
|
|
31
48
|
x: number;
|
|
32
49
|
y: number;
|