slotdeck 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +486 -2
- package/dist/deck/numbers.d.ts +14 -0
- package/dist/deck/numbers.js +48 -0
- package/dist/deck/numbers.js.map +1 -0
- package/dist/deck/spec.d.ts +73 -0
- package/dist/deck/spec.js +21 -0
- package/dist/deck/spec.js.map +1 -0
- package/dist/deck/validate.d.ts +49 -0
- package/dist/deck/validate.js +189 -0
- package/dist/deck/validate.js.map +1 -0
- package/dist/generate.d.ts +53 -0
- package/dist/generate.js +44 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/gemini.d.ts +39 -0
- package/dist/llm/gemini.js +147 -0
- package/dist/llm/gemini.js.map +1 -0
- package/dist/llm/provider.d.ts +38 -0
- package/dist/llm/provider.js +2 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/node.d.ts +3 -0
- package/dist/node.js +31 -0
- package/dist/node.js.map +1 -0
- package/dist/plan/assets.d.ts +28 -0
- package/dist/plan/assets.js +80 -0
- package/dist/plan/assets.js.map +1 -0
- package/dist/plan/catalog.d.ts +106 -0
- package/dist/plan/catalog.js +302 -0
- package/dist/plan/catalog.js.map +1 -0
- package/dist/plan/edit.d.ts +31 -0
- package/dist/plan/edit.js +364 -0
- package/dist/plan/edit.js.map +1 -0
- package/dist/plan/plan.d.ts +106 -0
- package/dist/plan/plan.js +243 -0
- package/dist/plan/plan.js.map +1 -0
- package/dist/plan/write.d.ts +39 -0
- package/dist/plan/write.js +179 -0
- package/dist/plan/write.js.map +1 -0
- package/dist/pptx/package.d.ts +48 -0
- package/dist/pptx/package.js +202 -0
- package/dist/pptx/package.js.map +1 -0
- package/dist/pptx/presentation.d.ts +20 -0
- package/dist/pptx/presentation.js +31 -0
- package/dist/pptx/presentation.js.map +1 -0
- package/dist/pptx/xml.d.ts +23 -0
- package/dist/pptx/xml.js +67 -0
- package/dist/pptx/xml.js.map +1 -0
- package/dist/render/chart.d.ts +18 -0
- package/dist/render/chart.js +158 -0
- package/dist/render/chart.js.map +1 -0
- package/dist/render/extract.d.ts +11 -0
- package/dist/render/extract.js +65 -0
- package/dist/render/extract.js.map +1 -0
- package/dist/render/picture.d.ts +20 -0
- package/dist/render/picture.js +114 -0
- package/dist/render/picture.js.map +1 -0
- package/dist/render/render.d.ts +32 -0
- package/dist/render/render.js +247 -0
- package/dist/render/render.js.map +1 -0
- package/dist/render/repeat.d.ts +15 -0
- package/dist/render/repeat.js +118 -0
- package/dist/render/repeat.js.map +1 -0
- package/dist/render/table.d.ts +12 -0
- package/dist/render/table.js +84 -0
- package/dist/render/table.js.map +1 -0
- package/dist/render/text.d.ts +31 -0
- package/dist/render/text.js +210 -0
- package/dist/render/text.js.map +1 -0
- package/dist/store.d.ts +18 -0
- package/dist/store.js +23 -0
- package/dist/store.js.map +1 -0
- package/dist/template/change.d.ts +138 -0
- package/dist/template/change.js +406 -0
- package/dist/template/change.js.map +1 -0
- package/dist/template/check.d.ts +15 -0
- package/dist/template/check.js +97 -0
- package/dist/template/check.js.map +1 -0
- package/dist/template/draft.d.ts +47 -0
- package/dist/template/draft.js +276 -0
- package/dist/template/draft.js.map +1 -0
- package/dist/template/inspect.d.ts +53 -0
- package/dist/template/inspect.js +157 -0
- package/dist/template/inspect.js.map +1 -0
- package/dist/template/instruct.d.ts +26 -0
- package/dist/template/instruct.js +278 -0
- package/dist/template/instruct.js.map +1 -0
- package/dist/template/manifest.d.ts +226 -0
- package/dist/template/manifest.js +116 -0
- package/dist/template/manifest.js.map +1 -0
- package/dist/template/measure.d.ts +80 -0
- package/dist/template/measure.js +212 -0
- package/dist/template/measure.js.map +1 -0
- package/dist/template/sample.d.ts +17 -0
- package/dist/template/sample.js +96 -0
- package/dist/template/sample.js.map +1 -0
- package/dist/template/view.d.ts +46 -0
- package/dist/template/view.js +38 -0
- package/dist/template/view.js.map +1 -0
- package/dist/tools.d.ts +68 -0
- package/dist/tools.js +266 -0
- package/dist/tools.js.map +1 -0
- package/dist/util/image.d.ts +11 -0
- package/dist/util/image.js +58 -0
- package/dist/util/image.js.map +1 -0
- package/dist/util/png.d.ts +2 -0
- package/dist/util/png.js +50 -0
- package/dist/util/png.js.map +1 -0
- package/package.json +63 -4
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
export type Direction = 'rtl' | 'ltr';
|
|
2
|
+
export type AlignPolicy = 'flip' | 'keep';
|
|
3
|
+
/**
|
|
4
|
+
* Describes how a pptx can be reused as a template: which of its slides are
|
|
5
|
+
* reusable patterns, and which shapes on them are slots to fill.
|
|
6
|
+
* Shapes that are not listed as slots are copied untouched (logo, rules, decorations).
|
|
7
|
+
*/
|
|
8
|
+
export interface TemplateManifest {
|
|
9
|
+
version: 1;
|
|
10
|
+
name: string;
|
|
11
|
+
/** BCP 47 tag of the template's sample text, e.g. `he`. */
|
|
12
|
+
language?: string;
|
|
13
|
+
/** Reading direction the template was designed for. */
|
|
14
|
+
direction: Direction;
|
|
15
|
+
/**
|
|
16
|
+
* What happens to left/right paragraph alignment when a deck reads the other way from the
|
|
17
|
+
* template. `flip` (default) mirrors it. `keep` leaves it as designed, which suits templates
|
|
18
|
+
* whose text boxes share one edge but not the other. Slots can override this.
|
|
19
|
+
*/
|
|
20
|
+
alignOnDirectionChange?: AlignPolicy;
|
|
21
|
+
patterns: PatternDef[];
|
|
22
|
+
}
|
|
23
|
+
export interface PatternDef {
|
|
24
|
+
id: string;
|
|
25
|
+
/** 1-based slide number in the template pptx that this pattern is cloned from. */
|
|
26
|
+
sourceSlide: number;
|
|
27
|
+
/** What the pattern is for. Written for the model that picks patterns. */
|
|
28
|
+
description: string;
|
|
29
|
+
slots: SlotDef[];
|
|
30
|
+
/**
|
|
31
|
+
* The pattern is built around numbers. The planner offers it only when the brief contains
|
|
32
|
+
* figures, so the model is not led into inventing statistics to fill it.
|
|
33
|
+
*/
|
|
34
|
+
figures?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Rows of items (cards, steps, figures). Each row lists its items, first item first in the
|
|
37
|
+
* template's reading direction, and each item lists its slot names in the same order.
|
|
38
|
+
* When a deck reads the other way, values are placed mirror-wise within each row, so the
|
|
39
|
+
* deck's first item still comes first for its readers. A grid is one row per line of cards.
|
|
40
|
+
* Items of a row should have matching limits.
|
|
41
|
+
*/
|
|
42
|
+
sequences?: string[][][];
|
|
43
|
+
/** A row of like items (cards, steps, figures) whose number a deck may choose. */
|
|
44
|
+
repeat?: RepeatDef;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A row of items that can hold fewer or more than the template slide shows. Items are removed
|
|
48
|
+
* or copied and the row is spaced out again; nothing is redrawn.
|
|
49
|
+
*/
|
|
50
|
+
export interface RepeatDef {
|
|
51
|
+
/** What one item is called in prompts, e.g. `card`. */
|
|
52
|
+
item: string;
|
|
53
|
+
/** Slot name templates of one item; `{n}` stands for the item number, from 1. */
|
|
54
|
+
slots: string[];
|
|
55
|
+
/**
|
|
56
|
+
* For each item on the template slide, first in reading order first: the shapes that move
|
|
57
|
+
* together as that item (its group, and any text box that sits outside the group).
|
|
58
|
+
*/
|
|
59
|
+
shapes: string[][];
|
|
60
|
+
/** Fewest and most items the row may hold. Both default to the number on the template slide. */
|
|
61
|
+
min?: number;
|
|
62
|
+
max?: number;
|
|
63
|
+
/** Left and right limits of the row, as fractions of the slide width. Defaults to the row as drawn. */
|
|
64
|
+
span?: {
|
|
65
|
+
left: number;
|
|
66
|
+
right: number;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* For a grid: how many items one row holds on the template slide. Items then fill rows in
|
|
70
|
+
* reading order, spread evenly over as few rows as they need. Leave it out for a single row.
|
|
71
|
+
*/
|
|
72
|
+
columns?: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* How many items each row of a repeat holds for a given count. A single row takes them all.
|
|
76
|
+
* A grid uses as few rows as the count needs and spreads the items evenly: four items in a
|
|
77
|
+
* three-column grid make two rows of two, not three and one.
|
|
78
|
+
*/
|
|
79
|
+
export declare function rowLayout(repeat: RepeatDef, count: number): number[];
|
|
80
|
+
/**
|
|
81
|
+
* For each slot of a pattern, the deck slot name its value is taken from: the slot's own name,
|
|
82
|
+
* except across a row of `sequences` when the deck reads against the template.
|
|
83
|
+
*/
|
|
84
|
+
export declare function slotSources(pattern: PatternDef, mirrored: boolean, items?: number): Map<string, string>;
|
|
85
|
+
/**
|
|
86
|
+
* What to do when a deck gives no value for a slot:
|
|
87
|
+
* - `error`: the slot is required (default)
|
|
88
|
+
* - `remove`: optional content; the shape is deleted, which needs `layout: 'adaptive'`
|
|
89
|
+
* - `keep`: leave the template's own content in place (decoration that suits any content)
|
|
90
|
+
* - `drop`: delete the shape in any layout (decoration drawn for the template's sample
|
|
91
|
+
* content, such as a sticker placed on a particular photo)
|
|
92
|
+
*
|
|
93
|
+
* Slots with `keep` or `drop` are never offered to the planning model.
|
|
94
|
+
*/
|
|
95
|
+
export type EmptyPolicy = 'error' | 'remove' | 'keep' | 'drop';
|
|
96
|
+
interface SlotBase {
|
|
97
|
+
/** Name the deck uses to address the slot; unique within the pattern. */
|
|
98
|
+
name: string;
|
|
99
|
+
/** `p:cNvPr/@id` of the shape on the source slide. */
|
|
100
|
+
shapeId: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
empty?: EmptyPolicy;
|
|
103
|
+
}
|
|
104
|
+
export interface TextSlotDef extends SlotBase {
|
|
105
|
+
kind: 'text';
|
|
106
|
+
/** Soft limit on total characters; exceeding it produces a warning. */
|
|
107
|
+
maxChars?: number;
|
|
108
|
+
/** Soft limit on the number of paragraphs. */
|
|
109
|
+
maxParagraphs?: number;
|
|
110
|
+
/** Take the value from `deck.fields[deckField]` when the slide does not set it. */
|
|
111
|
+
deckField?: string;
|
|
112
|
+
/** Overrides the manifest's `alignOnDirectionChange` for this slot. */
|
|
113
|
+
alignOnDirectionChange?: AlignPolicy;
|
|
114
|
+
/**
|
|
115
|
+
* Which run of the template paragraph supplies the formatting, for paragraphs that mix
|
|
116
|
+
* styles (e.g. a coloured label followed by plain text). Defaults to `first`.
|
|
117
|
+
*/
|
|
118
|
+
styleRun?: 'first' | 'last';
|
|
119
|
+
/** Font size of the slot's text on the template slide, in points. Decides whether a deck's text size applies. */
|
|
120
|
+
size?: number;
|
|
121
|
+
/** Sets the font size outright, in points: for a pattern derived from a slide that uses the shape differently. */
|
|
122
|
+
fontSize?: number;
|
|
123
|
+
/** Moves and resizes the text box: for a derived pattern. Only for a shape that is not inside a group. */
|
|
124
|
+
box?: SlideBox;
|
|
125
|
+
/** Space after each paragraph, in points. */
|
|
126
|
+
paragraphGap?: number;
|
|
127
|
+
/** Whether a deck's text size applies. Defaults to body text: 12 pt and under, and not a deck field. */
|
|
128
|
+
scales?: boolean;
|
|
129
|
+
}
|
|
130
|
+
export type ImageKind = 'photo' | 'icon' | 'graphic';
|
|
131
|
+
/** A rectangle as fractions of the slide (0..1). */
|
|
132
|
+
export interface SlideBox {
|
|
133
|
+
x: number;
|
|
134
|
+
y: number;
|
|
135
|
+
w: number;
|
|
136
|
+
h: number;
|
|
137
|
+
}
|
|
138
|
+
export interface ImageSlotDef extends SlotBase {
|
|
139
|
+
kind: 'image';
|
|
140
|
+
imageKind: ImageKind;
|
|
141
|
+
/** `cover` crops the image to fill the frame; `contain` shrinks the frame to the image. Defaults by kind. */
|
|
142
|
+
fit?: 'cover' | 'contain';
|
|
143
|
+
/**
|
|
144
|
+
* The area the picture may use, when that is larger or other than the sample picture's own
|
|
145
|
+
* frame (a chart drawn small in the middle of a slide that has room for a wide diagram).
|
|
146
|
+
* Only for pictures that are not inside a group.
|
|
147
|
+
*/
|
|
148
|
+
box?: SlideBox;
|
|
149
|
+
/** Set on slots of copied items: the template shape the copy was made from. */
|
|
150
|
+
copyOf?: string;
|
|
151
|
+
}
|
|
152
|
+
/** Formatting for table text, for templates whose sample table leaves it undefined. */
|
|
153
|
+
export interface TableTextStyle {
|
|
154
|
+
/** Font size in points. */
|
|
155
|
+
size?: number;
|
|
156
|
+
bold?: boolean;
|
|
157
|
+
/** Hex colour without `#`, e.g. `FFFFFF`. */
|
|
158
|
+
color?: string;
|
|
159
|
+
/** Typeface name. */
|
|
160
|
+
font?: string;
|
|
161
|
+
}
|
|
162
|
+
export interface TableSlotDef extends SlotBase {
|
|
163
|
+
kind: 'table';
|
|
164
|
+
/** Soft limit on columns. Defaults to the sample table's. */
|
|
165
|
+
maxColumns?: number;
|
|
166
|
+
/** Soft limit on body rows (the header row not counted). Defaults to the sample table's. */
|
|
167
|
+
maxRows?: number;
|
|
168
|
+
/** Soft limit on the characters in one cell. */
|
|
169
|
+
maxCellChars?: number;
|
|
170
|
+
headerText?: TableTextStyle;
|
|
171
|
+
bodyText?: TableTextStyle;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A native, editable chart. The slot points at the sample picture of a chart; the picture is
|
|
175
|
+
* replaced by a real chart with its data in an embedded workbook.
|
|
176
|
+
*/
|
|
177
|
+
export interface ChartSlotDef extends SlotBase {
|
|
178
|
+
kind: 'chart';
|
|
179
|
+
/** The area the chart may use, when other than the sample picture's frame. */
|
|
180
|
+
box?: SlideBox;
|
|
181
|
+
/** Soft limit on the number of categories (points along the axis, or pie slices). */
|
|
182
|
+
maxCategories?: number;
|
|
183
|
+
/** Soft limit on the number of series. */
|
|
184
|
+
maxSeries?: number;
|
|
185
|
+
/** Series colours in order, as hex without `#`. They repeat when there are more series. */
|
|
186
|
+
colors?: string[];
|
|
187
|
+
/** Formatting of axis labels, data labels and the legend. */
|
|
188
|
+
text?: TableTextStyle;
|
|
189
|
+
/** Colour of the gridlines and axis lines, as hex without `#`. */
|
|
190
|
+
lineColor?: string;
|
|
191
|
+
}
|
|
192
|
+
export type SlotDef = TextSlotDef | ImageSlotDef | TableSlotDef | ChartSlotDef;
|
|
193
|
+
export declare function defaultFit(slot: ImageSlotDef): 'cover' | 'contain';
|
|
194
|
+
/** Copied shapes get the id of their original plus a multiple of this, so the id is known without the file. */
|
|
195
|
+
export declare const COPY_ID_STEP = 10000;
|
|
196
|
+
/** How many items a slide's row holds: what the deck asks for, or the number on the template slide. */
|
|
197
|
+
export declare function itemCount(pattern: PatternDef, items?: number): number;
|
|
198
|
+
/** The range of item counts a pattern accepts, or `undefined` when it has no row to vary. */
|
|
199
|
+
export declare function itemRange(pattern: PatternDef): {
|
|
200
|
+
min: number;
|
|
201
|
+
max: number;
|
|
202
|
+
drawn: number;
|
|
203
|
+
} | undefined;
|
|
204
|
+
/**
|
|
205
|
+
* The slots of a pattern for a given number of items. Slots of items beyond the count are left
|
|
206
|
+
* out; items beyond those drawn on the template get copies of the last item's slots, pointing
|
|
207
|
+
* at the shapes the renderer will copy.
|
|
208
|
+
*/
|
|
209
|
+
export declare function effectiveSlots(pattern: PatternDef, items?: number): SlotDef[];
|
|
210
|
+
/** Deck text sizes by name, as factors on the template's body text. */
|
|
211
|
+
export declare const TEXT_SIZES: {
|
|
212
|
+
readonly normal: 1;
|
|
213
|
+
readonly large: 1.25;
|
|
214
|
+
readonly 'x-large': 1.5;
|
|
215
|
+
};
|
|
216
|
+
export type TextSize = keyof typeof TEXT_SIZES;
|
|
217
|
+
export declare function scalesWithText(slot: TextSlotDef): boolean;
|
|
218
|
+
/**
|
|
219
|
+
* A text slot's limits at a given text scale. Larger text fits fewer characters per line and
|
|
220
|
+
* fewer lines in the same box, so the character budget falls with the square of the scale.
|
|
221
|
+
*/
|
|
222
|
+
export declare function textBudget(slot: TextSlotDef, scale?: number): {
|
|
223
|
+
maxChars?: number;
|
|
224
|
+
maxParagraphs?: number;
|
|
225
|
+
};
|
|
226
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many items each row of a repeat holds for a given count. A single row takes them all.
|
|
3
|
+
* A grid uses as few rows as the count needs and spreads the items evenly: four items in a
|
|
4
|
+
* three-column grid make two rows of two, not three and one.
|
|
5
|
+
*/
|
|
6
|
+
export function rowLayout(repeat, count) {
|
|
7
|
+
if (!repeat.columns || repeat.columns >= count)
|
|
8
|
+
return count > 0 ? [count] : [];
|
|
9
|
+
const rows = Math.ceil(count / repeat.columns);
|
|
10
|
+
const layout = [];
|
|
11
|
+
for (let row = 0, left = count; row < rows; row++) {
|
|
12
|
+
const here = Math.ceil(left / (rows - row));
|
|
13
|
+
layout.push(here);
|
|
14
|
+
left -= here;
|
|
15
|
+
}
|
|
16
|
+
return layout;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* For each slot of a pattern, the deck slot name its value is taken from: the slot's own name,
|
|
20
|
+
* except across a row of `sequences` when the deck reads against the template.
|
|
21
|
+
*/
|
|
22
|
+
export function slotSources(pattern, mirrored, items) {
|
|
23
|
+
const sources = new Map(effectiveSlots(pattern, items).map((slot) => [slot.name, slot.name]));
|
|
24
|
+
if (!mirrored)
|
|
25
|
+
return sources;
|
|
26
|
+
const rows = [...(pattern.sequences ?? [])];
|
|
27
|
+
if (pattern.repeat) {
|
|
28
|
+
// Each row of items is mirrored on its own, so a grid keeps its rows.
|
|
29
|
+
const repeat = pattern.repeat;
|
|
30
|
+
let next = 1;
|
|
31
|
+
for (const size of rowLayout(repeat, itemCount(pattern, items))) {
|
|
32
|
+
rows.push(Array.from({ length: size }, () => {
|
|
33
|
+
const n = next++;
|
|
34
|
+
return repeat.slots.map((name) => name.replace('{n}', String(n)));
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const items of rows) {
|
|
39
|
+
items.forEach((item, index) => {
|
|
40
|
+
const opposite = items[items.length - 1 - index] ?? [];
|
|
41
|
+
item.forEach((name, position) => {
|
|
42
|
+
const source = opposite[position];
|
|
43
|
+
if (source !== undefined)
|
|
44
|
+
sources.set(name, source);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return sources;
|
|
49
|
+
}
|
|
50
|
+
export function defaultFit(slot) {
|
|
51
|
+
return slot.fit ?? (slot.imageKind === 'photo' ? 'cover' : 'contain');
|
|
52
|
+
}
|
|
53
|
+
/** Copied shapes get the id of their original plus a multiple of this, so the id is known without the file. */
|
|
54
|
+
export const COPY_ID_STEP = 10000;
|
|
55
|
+
/** How many items a slide's row holds: what the deck asks for, or the number on the template slide. */
|
|
56
|
+
export function itemCount(pattern, items) {
|
|
57
|
+
if (!pattern.repeat)
|
|
58
|
+
return 0;
|
|
59
|
+
return items ?? pattern.repeat.shapes.length;
|
|
60
|
+
}
|
|
61
|
+
/** The range of item counts a pattern accepts, or `undefined` when it has no row to vary. */
|
|
62
|
+
export function itemRange(pattern) {
|
|
63
|
+
if (!pattern.repeat)
|
|
64
|
+
return undefined;
|
|
65
|
+
const drawn = pattern.repeat.shapes.length;
|
|
66
|
+
return { min: pattern.repeat.min ?? drawn, max: pattern.repeat.max ?? drawn, drawn };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The slots of a pattern for a given number of items. Slots of items beyond the count are left
|
|
70
|
+
* out; items beyond those drawn on the template get copies of the last item's slots, pointing
|
|
71
|
+
* at the shapes the renderer will copy.
|
|
72
|
+
*/
|
|
73
|
+
export function effectiveSlots(pattern, items) {
|
|
74
|
+
const repeat = pattern.repeat;
|
|
75
|
+
if (!repeat)
|
|
76
|
+
return pattern.slots;
|
|
77
|
+
const drawn = repeat.shapes.length;
|
|
78
|
+
const count = itemCount(pattern, items);
|
|
79
|
+
const itemOf = new Map();
|
|
80
|
+
for (let n = 1; n <= drawn; n++)
|
|
81
|
+
for (const name of repeat.slots)
|
|
82
|
+
itemOf.set(name.replace('{n}', String(n)), n);
|
|
83
|
+
const slots = pattern.slots.filter((slot) => (itemOf.get(slot.name) ?? 0) <= count);
|
|
84
|
+
for (let n = drawn + 1; n <= count; n++) {
|
|
85
|
+
for (const name of repeat.slots) {
|
|
86
|
+
const original = pattern.slots.find((slot) => slot.name === name.replace('{n}', String(drawn)));
|
|
87
|
+
if (!original)
|
|
88
|
+
continue;
|
|
89
|
+
const copy = { ...original, name: name.replace('{n}', String(n)), shapeId: String(Number(original.shapeId) + (n - drawn) * COPY_ID_STEP) };
|
|
90
|
+
slots.push(copy.kind === 'image' ? { ...copy, copyOf: original.shapeId } : copy);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return slots;
|
|
94
|
+
}
|
|
95
|
+
/** Deck text sizes by name, as factors on the template's body text. */
|
|
96
|
+
export const TEXT_SIZES = { normal: 1, large: 1.25, 'x-large': 1.5 };
|
|
97
|
+
export function scalesWithText(slot) {
|
|
98
|
+
if (slot.scales !== undefined)
|
|
99
|
+
return slot.scales;
|
|
100
|
+
const size = slot.fontSize ?? slot.size;
|
|
101
|
+
return !slot.deckField && size !== undefined && size <= 12;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A text slot's limits at a given text scale. Larger text fits fewer characters per line and
|
|
105
|
+
* fewer lines in the same box, so the character budget falls with the square of the scale.
|
|
106
|
+
*/
|
|
107
|
+
export function textBudget(slot, scale = 1) {
|
|
108
|
+
const factor = scalesWithText(slot) ? scale : 1;
|
|
109
|
+
const budget = {};
|
|
110
|
+
if (slot.maxChars !== undefined)
|
|
111
|
+
budget.maxChars = Math.max(1, Math.floor(slot.maxChars / (factor * factor)));
|
|
112
|
+
if (slot.maxParagraphs !== undefined)
|
|
113
|
+
budget.maxParagraphs = Math.max(1, Math.floor(slot.maxParagraphs / factor));
|
|
114
|
+
return budget;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/template/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Box, ShapeInfo, SlideInfo, TemplateInfo } from './inspect.ts';
|
|
2
|
+
import type { SlotDef } from './manifest.ts';
|
|
3
|
+
/** What a shape is for. `fixed` means it is part of the design and not a slot. */
|
|
4
|
+
export declare const ROLES: readonly ['title', 'subtitle', 'text', 'list', 'number', 'caption', 'photo', 'icon', 'graphic', 'chart', 'table', 'decoration', 'fixed'];
|
|
5
|
+
export type Role = (typeof ROLES)[number];
|
|
6
|
+
export declare const pct: (value: number) => number;
|
|
7
|
+
export declare const normalize: (text: string) => string;
|
|
8
|
+
export declare const shapeText: (shape: ShapeInfo) => string;
|
|
9
|
+
/** A name the manifest can use as a slot name: letters and digits, starting with a lowercase letter. */
|
|
10
|
+
export declare function slotName(raw: string, fallback: string): string;
|
|
11
|
+
/** How much text a box holds, estimated from its size and font, and never less than its sample holds. */
|
|
12
|
+
export declare function textLimits(shape: ShapeInfo, info: TemplateInfo, role: Role): {
|
|
13
|
+
size: number;
|
|
14
|
+
maxChars: number;
|
|
15
|
+
maxParagraphs?: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* How many characters an area of the slide holds at a font size, when the text is in several
|
|
19
|
+
* paragraphs with a gap between them. Each paragraph after the first ends half a line short on average.
|
|
20
|
+
*/
|
|
21
|
+
export declare function boxCapacity(box: Box, info: TemplateInfo, size: number, paragraphs?: number, gap?: number): number;
|
|
22
|
+
/**
|
|
23
|
+
* What a table holds: its columns and rows as drawn, and the characters that fit in a cell
|
|
24
|
+
* before the rows grow so tall that the table leaves the slide. The room is measured from the
|
|
25
|
+
* table's top down to the slide's bottom margin, or to the next shape under the table.
|
|
26
|
+
* `fontSize` and `columns` give the limits for a table set differently from the sample.
|
|
27
|
+
*/
|
|
28
|
+
export declare function tableLimits(shape: ShapeInfo, info: TemplateInfo, options?: {
|
|
29
|
+
slide?: SlideInfo;
|
|
30
|
+
fontSize?: number;
|
|
31
|
+
columns?: number;
|
|
32
|
+
rows?: number;
|
|
33
|
+
}): {
|
|
34
|
+
maxColumns: number;
|
|
35
|
+
maxRows: number;
|
|
36
|
+
maxCellChars: number;
|
|
37
|
+
};
|
|
38
|
+
/** The roles a shape can take, by what kind of shape it is. `fixed` is always possible and not listed. */
|
|
39
|
+
export declare function rolesFor(shape: ShapeInfo): Role[];
|
|
40
|
+
/**
|
|
41
|
+
* The slot a shape becomes in a given role, with limits measured from the shape. `undefined`
|
|
42
|
+
* when the shape cannot take the role (a picture cannot be a title).
|
|
43
|
+
*/
|
|
44
|
+
export declare function slotFromShape(shape: ShapeInfo, info: TemplateInfo, role: Role, name: string, optional?: boolean, slide?: SlideInfo): SlotDef | undefined;
|
|
45
|
+
/** Whether a shape of a role's kind carries text. */
|
|
46
|
+
export declare function isTextRole(role: Role): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* A page number, date, footer or header placeholder: part of every slide, never content. A title
|
|
49
|
+
* or body placeholder is content like any other text box.
|
|
50
|
+
*/
|
|
51
|
+
export declare function isFramePlaceholder(shape: ShapeInfo): boolean;
|
|
52
|
+
export interface FrameFacts {
|
|
53
|
+
/** On how many slides something must sit in the same place to count as part of the frame. */
|
|
54
|
+
often: number;
|
|
55
|
+
/** A logo, rule, page number or other element that repeats across slides. */
|
|
56
|
+
isFrame(shape: ShapeInfo): boolean;
|
|
57
|
+
/** Texts that appear on many slides: one value for the whole deck. */
|
|
58
|
+
repeatedTexts: string[];
|
|
59
|
+
/** Every distinct sample text, in slide order. */
|
|
60
|
+
texts: string[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whatever sits in the same place on many slides is part of the frame, not content. A short deck
|
|
64
|
+
* gives too little evidence of what repeats, so nothing is treated as frame there.
|
|
65
|
+
*/
|
|
66
|
+
export declare function frameFacts(info: TemplateInfo): FrameFacts;
|
|
67
|
+
/** One line of measured facts about a shape, as the model reads it. */
|
|
68
|
+
export declare function describeShape(shape: ShapeInfo, info: TemplateInfo): string;
|
|
69
|
+
/** The smallest box around several shapes, as slide fractions. */
|
|
70
|
+
export declare function unionBox(boxes: Box[]): Box;
|
|
71
|
+
/**
|
|
72
|
+
* How far a row of items may spread on its slide: from the row as drawn outwards, until the
|
|
73
|
+
* slide's margin or another shape that sits beside the row. Fractions of the slide width.
|
|
74
|
+
*/
|
|
75
|
+
export declare function rowRoom(slide: SlideInfo, itemShapes: string[][]): {
|
|
76
|
+
area: Box;
|
|
77
|
+
itemWidth: number;
|
|
78
|
+
left: number;
|
|
79
|
+
right: number;
|
|
80
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/** What a shape is for. `fixed` means it is part of the design and not a slot. */
|
|
2
|
+
export const ROLES = ['title', 'subtitle', 'text', 'list', 'number', 'caption', 'photo', 'icon', 'graphic', 'chart', 'table', 'decoration', 'fixed'];
|
|
3
|
+
const TEXT_ROLES = new Set(['title', 'subtitle', 'text', 'list', 'number', 'caption']);
|
|
4
|
+
export const pct = (value) => Math.round(value * 100);
|
|
5
|
+
export const normalize = (text) => text.replace(/\s+/g, ' ').trim();
|
|
6
|
+
export const shapeText = (shape) => normalize((shape.paragraphs ?? []).join(' '));
|
|
7
|
+
/** A name the manifest can use as a slot name: letters and digits, starting with a lowercase letter. */
|
|
8
|
+
export function slotName(raw, fallback) {
|
|
9
|
+
const words = raw.replace(/[^A-Za-z0-9]+/g, ' ').trim().split(' ').filter(Boolean);
|
|
10
|
+
const name = words.map((word, index) => (index === 0 ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1)).join('');
|
|
11
|
+
return /^[a-z]/.test(name) ? name : fallback;
|
|
12
|
+
}
|
|
13
|
+
/** How much text a box holds, estimated from its size and font, and never less than its sample holds. */
|
|
14
|
+
export function textLimits(shape, info, role) {
|
|
15
|
+
const size = shape.fontSizes?.[0] ?? 14;
|
|
16
|
+
const paragraphs = (shape.paragraphs ?? []).filter((paragraph) => paragraph.trim() !== '');
|
|
17
|
+
const sample = paragraphs.reduce((sum, paragraph) => sum + paragraph.length, 0);
|
|
18
|
+
const perLine = Math.max(1, Math.floor((shape.box.w * info.slideWidth) / 12700 / (size * 0.5)));
|
|
19
|
+
// A text box often shrinks to its sample, so the lines the sample takes count as well as the box height.
|
|
20
|
+
const sampleLines = paragraphs.reduce((sum, paragraph) => sum + Math.max(1, Math.ceil(paragraph.length / perLine)), 0);
|
|
21
|
+
const boxLines = Math.max(1, Math.round((shape.box.h * info.slideHeight) / 12700 / (size * 1.25)));
|
|
22
|
+
const estimate = Math.floor(perLine * Math.max(sampleLines, boxLines) * 0.8);
|
|
23
|
+
const limits = {
|
|
24
|
+
size,
|
|
25
|
+
// A big figure fits what the sample shows and little more; other text can use the room there is.
|
|
26
|
+
maxChars: Math.max(1, role === 'number' ? Math.max(sample, 2) : Math.max(sample, estimate)),
|
|
27
|
+
};
|
|
28
|
+
if (role === 'list')
|
|
29
|
+
limits.maxParagraphs = Math.max(2, paragraphs.length);
|
|
30
|
+
return limits;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* How many characters an area of the slide holds at a font size, when the text is in several
|
|
34
|
+
* paragraphs with a gap between them. Each paragraph after the first ends half a line short on average.
|
|
35
|
+
*/
|
|
36
|
+
export function boxCapacity(box, info, size, paragraphs = 1, gap = 0) {
|
|
37
|
+
const extra = Math.max(0, paragraphs - 1);
|
|
38
|
+
const perLine = Math.max(1, Math.floor((box.w * info.slideWidth) / 12700 / (size * 0.5)));
|
|
39
|
+
const height = (box.h * info.slideHeight) / 12700 - extra * gap;
|
|
40
|
+
const lines = Math.max(1, Math.round(height / (size * 1.25)) - extra * 0.5);
|
|
41
|
+
return Math.max(1, Math.floor(perLine * lines * 0.8));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* What a table holds: its columns and rows as drawn, and the characters that fit in a cell
|
|
45
|
+
* before the rows grow so tall that the table leaves the slide. The room is measured from the
|
|
46
|
+
* table's top down to the slide's bottom margin, or to the next shape under the table.
|
|
47
|
+
* `fontSize` and `columns` give the limits for a table set differently from the sample.
|
|
48
|
+
*/
|
|
49
|
+
export function tableLimits(shape, info, options = {}) {
|
|
50
|
+
const columns = options.columns ?? shape.table?.columns ?? 4;
|
|
51
|
+
const bodyRows = options.rows ?? Math.max(1, (shape.table?.rows ?? 4) - 1);
|
|
52
|
+
const size = options.fontSize ?? shape.fontSizes?.[0] ?? info.defaultFontSize;
|
|
53
|
+
const margins = 14.4; // a cell's two opposite margins, in points
|
|
54
|
+
const points = (fraction, of) => (fraction * of) / 12700;
|
|
55
|
+
let bottom = 0.93;
|
|
56
|
+
for (const other of options.slide?.shapes ?? []) {
|
|
57
|
+
if (other.id === shape.id || other.kind === 'group' || isFramePlaceholder(other))
|
|
58
|
+
continue;
|
|
59
|
+
const beside = other.box.x < shape.box.x + shape.box.w && other.box.x + other.box.w > shape.box.x;
|
|
60
|
+
if (beside && other.box.y > shape.box.y + 0.05)
|
|
61
|
+
bottom = Math.min(bottom, other.box.y - 0.02);
|
|
62
|
+
}
|
|
63
|
+
const width = points(shape.box.w, info.slideWidth) / columns - margins;
|
|
64
|
+
const height = points(Math.max(bottom - shape.box.y, 0), info.slideHeight) / (bodyRows + 1) - margins;
|
|
65
|
+
const perLine = Math.max(1, Math.floor(width / (size * 0.5)));
|
|
66
|
+
const lines = Math.max(1, Math.floor(height / (size * 1.2) + 0.05));
|
|
67
|
+
return { maxColumns: columns, maxRows: bodyRows, maxCellChars: Math.max(4, Math.floor(perLine * lines * 0.9)) };
|
|
68
|
+
}
|
|
69
|
+
/** The roles a shape can take, by what kind of shape it is. `fixed` is always possible and not listed. */
|
|
70
|
+
export function rolesFor(shape) {
|
|
71
|
+
if (shape.kind === 'text')
|
|
72
|
+
return ['title', 'subtitle', 'text', 'list', 'number', 'caption', 'decoration'];
|
|
73
|
+
if (shape.kind === 'picture')
|
|
74
|
+
return ['photo', 'icon', 'graphic', 'chart', 'decoration'];
|
|
75
|
+
if (shape.kind === 'table')
|
|
76
|
+
return ['table'];
|
|
77
|
+
// A drawn ornament, a line or a group can only be taken away.
|
|
78
|
+
return ['decoration'];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The slot a shape becomes in a given role, with limits measured from the shape. `undefined`
|
|
82
|
+
* when the shape cannot take the role (a picture cannot be a title).
|
|
83
|
+
*/
|
|
84
|
+
export function slotFromShape(shape, info, role, name, optional = false, slide) {
|
|
85
|
+
if (!rolesFor(shape).includes(role))
|
|
86
|
+
return undefined;
|
|
87
|
+
let slot;
|
|
88
|
+
if (shape.kind !== 'text' && shape.kind !== 'picture' && shape.kind !== 'table') {
|
|
89
|
+
// Nothing fills such a shape; the slot exists so that the shape is removed.
|
|
90
|
+
return { name, kind: 'text', shapeId: shape.id, empty: 'drop' };
|
|
91
|
+
}
|
|
92
|
+
if (shape.kind === 'text') {
|
|
93
|
+
slot = { name, kind: 'text', shapeId: shape.id, ...textLimits(shape, info, role) };
|
|
94
|
+
}
|
|
95
|
+
else if (shape.kind === 'table') {
|
|
96
|
+
return { name, kind: 'table', shapeId: shape.id, ...tableLimits(shape, info, slide ? { slide } : {}) };
|
|
97
|
+
}
|
|
98
|
+
else if (role === 'chart') {
|
|
99
|
+
// A chart replaces the sample picture and may not sit in a group, which a native chart cannot.
|
|
100
|
+
if (shape.groupId !== undefined)
|
|
101
|
+
return undefined;
|
|
102
|
+
return { name, kind: 'chart', shapeId: shape.id, maxCategories: 8, maxSeries: 3 };
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
slot = { name, kind: 'image', imageKind: role === 'photo' || role === 'icon' ? role : 'graphic', shapeId: shape.id };
|
|
106
|
+
}
|
|
107
|
+
if (role === 'decoration')
|
|
108
|
+
slot.empty = 'drop';
|
|
109
|
+
else if (optional)
|
|
110
|
+
slot.empty = 'remove';
|
|
111
|
+
return slot;
|
|
112
|
+
}
|
|
113
|
+
/** Whether a shape of a role's kind carries text. */
|
|
114
|
+
export function isTextRole(role) {
|
|
115
|
+
return TEXT_ROLES.has(role);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A page number, date, footer or header placeholder: part of every slide, never content. A title
|
|
119
|
+
* or body placeholder is content like any other text box.
|
|
120
|
+
*/
|
|
121
|
+
export function isFramePlaceholder(shape) {
|
|
122
|
+
return shape.placeholder === 'sldNum' || shape.placeholder === 'dt' || shape.placeholder === 'ftr' || shape.placeholder === 'hdr';
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Whatever sits in the same place on many slides is part of the frame, not content. A short deck
|
|
126
|
+
* gives too little evidence of what repeats, so nothing is treated as frame there.
|
|
127
|
+
*/
|
|
128
|
+
export function frameFacts(info) {
|
|
129
|
+
const total = info.slides.length;
|
|
130
|
+
const often = total >= 6 ? Math.max(3, Math.ceil(total * 0.3)) : Infinity;
|
|
131
|
+
const placeOf = (shape) => `${shape.kind}|${pct(shape.box.x)}|${pct(shape.box.y)}|${pct(shape.box.w)}|${pct(shape.box.h)}|${shape.image?.part ?? ''}`;
|
|
132
|
+
const places = new Map();
|
|
133
|
+
const texts = new Map();
|
|
134
|
+
for (const slide of info.slides) {
|
|
135
|
+
for (const key of new Set(slide.shapes.filter((shape) => shape.kind !== 'text').map(placeOf)))
|
|
136
|
+
places.set(key, (places.get(key) ?? 0) + 1);
|
|
137
|
+
for (const text of new Set(slide.shapes.filter((shape) => shape.kind === 'text' && !isFramePlaceholder(shape)).map(shapeText))) {
|
|
138
|
+
if (text)
|
|
139
|
+
texts.set(text, (texts.get(text) ?? 0) + 1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
often,
|
|
144
|
+
isFrame: (shape) => isFramePlaceholder(shape) || (shape.kind !== 'text' && (places.get(placeOf(shape)) ?? 0) >= often),
|
|
145
|
+
repeatedTexts: [...texts].filter(([, count]) => count >= often).map(([text]) => text),
|
|
146
|
+
texts: [...texts.keys()],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** One line of measured facts about a shape, as the model reads it. */
|
|
150
|
+
export function describeShape(shape, info) {
|
|
151
|
+
const where = `at x${pct(shape.box.x)} y${pct(shape.box.y)}, ${pct(shape.box.w)} wide ${pct(shape.box.h)} high${shape.groupId ? `, in group #${shape.groupId}` : ''}`;
|
|
152
|
+
if (shape.kind === 'text') {
|
|
153
|
+
const text = shapeText(shape);
|
|
154
|
+
const count = shape.paragraphs?.length ?? 1;
|
|
155
|
+
return `#${shape.id} text ${where} | ${shape.fontSizes?.[0] ?? 14} pt | ${count} paragraph${count === 1 ? '' : 's'}, ${text.length} characters: ${JSON.stringify(text.slice(0, 60))}`;
|
|
156
|
+
}
|
|
157
|
+
if (shape.kind === 'table')
|
|
158
|
+
return `#${shape.id} table ${where} | ${shape.table?.columns ?? '?'} columns, ${shape.table?.rows ?? '?'} rows`;
|
|
159
|
+
if (shape.kind !== 'picture')
|
|
160
|
+
return `#${shape.id} ${shape.kind} ${where}`;
|
|
161
|
+
const image = shape.image;
|
|
162
|
+
const uses = info.slides.filter((other) => other.shapes.some((candidate) => candidate.image?.part === image?.part)).length;
|
|
163
|
+
return `#${shape.id} picture ${where} | ${image?.width ?? '?'}x${image?.height ?? '?'} px ${image?.part.split('.').pop() ?? ''}${image?.cropped ? ', cropped to its frame' : ''}${image?.geometry && image.geometry !== 'rect' && image.geometry !== 'custom' ? `, ${image.geometry} frame` : ''}${uses > 1 ? `, same picture on ${uses} slides` : ''}`;
|
|
164
|
+
}
|
|
165
|
+
/** The smallest box around several shapes, as slide fractions. */
|
|
166
|
+
export function unionBox(boxes) {
|
|
167
|
+
const x = Math.min(...boxes.map((box) => box.x));
|
|
168
|
+
const y = Math.min(...boxes.map((box) => box.y));
|
|
169
|
+
return { x, y, w: Math.max(...boxes.map((box) => box.x + box.w)) - x, h: Math.max(...boxes.map((box) => box.y + box.h)) - y };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* How far a row of items may spread on its slide: from the row as drawn outwards, until the
|
|
173
|
+
* slide's margin or another shape that sits beside the row. Fractions of the slide width.
|
|
174
|
+
*/
|
|
175
|
+
export function rowRoom(slide, itemShapes) {
|
|
176
|
+
const byId = new Map(slide.shapes.map((shape) => [shape.id, shape]));
|
|
177
|
+
const inRow = new Set();
|
|
178
|
+
const mark = (id) => {
|
|
179
|
+
inRow.add(id);
|
|
180
|
+
for (const shape of slide.shapes)
|
|
181
|
+
if (shape.groupId === id)
|
|
182
|
+
mark(shape.id);
|
|
183
|
+
};
|
|
184
|
+
for (const id of itemShapes.flat()) {
|
|
185
|
+
mark(id);
|
|
186
|
+
// A group that holds the whole row is not beside it.
|
|
187
|
+
for (let parent = byId.get(id)?.groupId; parent; parent = byId.get(parent)?.groupId)
|
|
188
|
+
inRow.add(parent);
|
|
189
|
+
}
|
|
190
|
+
const boxes = itemShapes.map((ids) => unionBox(ids.flatMap((id) => (byId.get(id) ? [byId.get(id).box] : []))));
|
|
191
|
+
const area = unionBox(boxes);
|
|
192
|
+
const itemWidth = Math.max(...boxes.map((box) => box.w));
|
|
193
|
+
// The margin the slide's own content keeps from its sides.
|
|
194
|
+
const content = slide.shapes.filter((shape) => !isFramePlaceholder(shape) && shape.kind !== 'group' && shape.box.w < 0.9 && shape.box.x >= 0 && shape.box.x + shape.box.w <= 1);
|
|
195
|
+
const margin = Math.max(0.03, Math.min(0.08, ...content.map((shape) => Math.min(shape.box.x, 1 - shape.box.x - shape.box.w)).filter((edge) => edge > 0.005)));
|
|
196
|
+
const gap = 0.02;
|
|
197
|
+
let left = Math.min(area.x, margin);
|
|
198
|
+
let right = Math.max(area.x + area.w, 1 - margin);
|
|
199
|
+
for (const shape of slide.shapes) {
|
|
200
|
+
if (inRow.has(shape.id) || isFramePlaceholder(shape) || shape.kind === 'group' || shape.box.w >= 0.9)
|
|
201
|
+
continue;
|
|
202
|
+
const overlaps = shape.box.y < area.y + area.h && shape.box.y + shape.box.h > area.y;
|
|
203
|
+
if (!overlaps)
|
|
204
|
+
continue;
|
|
205
|
+
if (shape.box.x + shape.box.w <= area.x + 0.005)
|
|
206
|
+
left = Math.max(left, Math.min(area.x, shape.box.x + shape.box.w + gap));
|
|
207
|
+
else if (shape.box.x >= area.x + area.w - 0.005)
|
|
208
|
+
right = Math.min(right, Math.max(area.x + area.w, shape.box.x - gap));
|
|
209
|
+
}
|
|
210
|
+
return { area, itemWidth, left, right };
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=measure.js.map
|