@segnavia/format 0.15.0 → 0.17.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.
@@ -0,0 +1,10 @@
1
+ //#region src/board-section-constants.d.ts
2
+ export declare const SECTION_PADDING = 24;
3
+ export declare const SECTION_TITLE_ROOM = 32;
4
+ export declare const SECTION_MIN_SIZE: {
5
+ readonly width: 240;
6
+ readonly height: 160;
7
+ };
8
+ export declare const SECTION_GAP = 16;
9
+ export declare const MAKE_ROOM_LIMIT = 50;
10
+ //#endregion
@@ -0,0 +1,11 @@
1
+ //#region src/board-section-constants.ts
2
+ const SECTION_PADDING = 24;
3
+ const SECTION_TITLE_ROOM = 32;
4
+ const SECTION_MIN_SIZE = {
5
+ width: 240,
6
+ height: 160
7
+ };
8
+ const SECTION_GAP = 16;
9
+ const MAKE_ROOM_LIMIT = 50;
10
+ //#endregion
11
+ export { MAKE_ROOM_LIMIT, SECTION_GAP, SECTION_MIN_SIZE, SECTION_PADDING, SECTION_TITLE_ROOM };
@@ -0,0 +1,20 @@
1
+ import { Board, GridCell } from "./board.js";
2
+ import { Box, Point } from "./deck.js";
3
+ import { BoardShapes } from "./board-geometry.js";
4
+ //#region src/board-section-grid.d.ts
5
+ export type GridDropTarget = {
6
+ anchor: string;
7
+ side: 'before' | 'after';
8
+ axis: 'row' | 'column';
9
+ } | {
10
+ row: number;
11
+ column: number;
12
+ axis: 'cell';
13
+ };
14
+ export declare function deriveSectionCells(board: Board, shapes: BoardShapes, id: string): GridCell[];
15
+ export declare function layoutSection(board: Board, shapes: BoardShapes, id: string): Board;
16
+ export declare function gridDropTarget(board: Board, shapes: BoardShapes, id: string, point: Point): GridDropTarget | undefined;
17
+ export declare function gridDropMarkerBox(board: Board, shapes: BoardShapes, id: string, target: GridDropTarget): Box | undefined;
18
+ export declare function removeFromGrid(board: Board, shapes: BoardShapes, ids: readonly string[]): Board;
19
+ export declare function insertIntoGrid(board: Board, shapes: BoardShapes, id: string, ids: readonly string[], target: GridDropTarget): Board;
20
+ //#endregion
@@ -0,0 +1,353 @@
1
+ import { itemHeight } from "./board-geometry.js";
2
+ import { SECTION_MIN_SIZE } from "./board-section-constants.js";
3
+ //#region src/board-section-grid.ts
4
+ function sectionOf(board, id) {
5
+ return board.sections.find((section) => section.id === id);
6
+ }
7
+ function dimensions(board, shapes, id) {
8
+ const item = board.items.find((entry) => entry.id === id);
9
+ const shape = shapes.items[id];
10
+ if (!item || !shape) throw new Error(`item "${id}" has no supplied shape; supply shapes.items["${id}"]`);
11
+ return {
12
+ width: item.width,
13
+ height: itemHeight(item, shape)
14
+ };
15
+ }
16
+ function deriveSectionCells(board, shapes, id) {
17
+ const section = sectionOf(board, id);
18
+ if (!section) return [];
19
+ const direction = board.direction === "rtl" ? -1 : 1;
20
+ const members = section.items.map((member) => {
21
+ const item = board.items.find((entry) => entry.id === member);
22
+ if (!item) return void 0;
23
+ return {
24
+ id: member,
25
+ x: item.x,
26
+ y: item.y,
27
+ bottom: item.y + dimensions(board, shapes, member).height
28
+ };
29
+ }).filter((item) => item !== void 0).sort((a, b) => a.y - b.y || direction * (a.x - b.x));
30
+ const cells = [];
31
+ let row = -1;
32
+ let bottom = Number.NEGATIVE_INFINITY;
33
+ let column = 0;
34
+ for (const member of members) {
35
+ if (row < 0 || member.y > bottom) {
36
+ row += 1;
37
+ column = 0;
38
+ bottom = member.bottom;
39
+ } else bottom = Math.max(bottom, member.bottom);
40
+ cells.push({
41
+ id: member.id,
42
+ row,
43
+ column
44
+ });
45
+ column += 1;
46
+ }
47
+ return cells;
48
+ }
49
+ function cellsFor(board, shapes, section) {
50
+ const members = new Set(section.items);
51
+ return section.cells?.filter((cell) => members.has(cell.id)).map((cell) => ({ ...cell })) ?? deriveSectionCells(board, shapes, section.id);
52
+ }
53
+ function normalized(cells) {
54
+ const rows = [...new Set(cells.map((cell) => cell.row))].sort((a, b) => a - b);
55
+ const columns = [...new Set(cells.map((cell) => cell.column))].sort((a, b) => a - b);
56
+ const rowIndex = new Map(rows.map((value, index) => [value, index]));
57
+ const columnIndex = new Map(columns.map((value, index) => [value, index]));
58
+ return cells.map((cell) => ({
59
+ ...cell,
60
+ row: rowIndex.get(cell.row) ?? 0,
61
+ column: columnIndex.get(cell.column) ?? 0
62
+ })).sort((a, b) => a.row - b.row || a.column - b.column);
63
+ }
64
+ function trackSizes(board, shapes, cells) {
65
+ const widths = Array.from({ length: Math.max(0, ...cells.map((cell) => cell.column + 1)) }, () => 0);
66
+ const heights = Array.from({ length: Math.max(0, ...cells.map((cell) => cell.row + 1)) }, () => 0);
67
+ for (const cell of cells) {
68
+ const size = dimensions(board, shapes, cell.id);
69
+ widths[cell.column] = Math.max(widths[cell.column] ?? 0, size.width);
70
+ heights[cell.row] = Math.max(heights[cell.row] ?? 0, size.height);
71
+ }
72
+ return {
73
+ widths,
74
+ heights
75
+ };
76
+ }
77
+ function starts(sizes) {
78
+ const result = [];
79
+ let value = 0;
80
+ for (const size of sizes) {
81
+ result.push(value);
82
+ value += size + 16;
83
+ }
84
+ return result;
85
+ }
86
+ function span(sizes) {
87
+ return sizes.reduce((sum, size) => sum + size, 0) + 16 * Math.max(0, sizes.length - 1);
88
+ }
89
+ function sectionGrid(board, shapes, section) {
90
+ const cells = normalized(cellsFor(board, shapes, section));
91
+ const { widths, heights } = trackSizes(board, shapes, cells);
92
+ return {
93
+ cells,
94
+ widths,
95
+ heights,
96
+ columnStarts: starts(widths),
97
+ rowStarts: starts(heights),
98
+ width: span(widths),
99
+ height: span(heights),
100
+ innerX: section.rectangle.x + 24,
101
+ innerY: section.rectangle.y + 24 + 32
102
+ };
103
+ }
104
+ function physicalX(board, grid, logicalStart, width) {
105
+ return board.direction === "rtl" ? grid.innerX + grid.width - logicalStart - width : grid.innerX + logicalStart;
106
+ }
107
+ function layoutSection(board, shapes, id) {
108
+ const section = sectionOf(board, id);
109
+ if (!section || section.layout === "manual") return board;
110
+ if (section.items.length === 0) return {
111
+ ...board,
112
+ sections: board.sections.map((entry) => entry.id === id ? {
113
+ ...entry,
114
+ cells: [],
115
+ rectangle: {
116
+ ...entry.rectangle,
117
+ ...SECTION_MIN_SIZE
118
+ }
119
+ } : entry)
120
+ };
121
+ const grid = sectionGrid(board, shapes, section);
122
+ const { cells } = grid;
123
+ const positions = /* @__PURE__ */ new Map();
124
+ for (const cell of cells) {
125
+ const width = dimensions(board, shapes, cell.id).width;
126
+ positions.set(cell.id, {
127
+ x: physicalX(board, grid, grid.columnStarts[cell.column] ?? 0, width),
128
+ y: grid.innerY + (grid.rowStarts[cell.row] ?? 0)
129
+ });
130
+ }
131
+ return {
132
+ ...board,
133
+ items: board.items.map((item) => {
134
+ const position = positions.get(item.id);
135
+ return position ? {
136
+ ...item,
137
+ ...position
138
+ } : item;
139
+ }),
140
+ sections: board.sections.map((entry) => entry.id === id ? {
141
+ ...entry,
142
+ cells,
143
+ items: cells.map((cell) => cell.id),
144
+ rectangle: {
145
+ ...entry.rectangle,
146
+ width: grid.width + 48,
147
+ height: grid.height + 48 + 32
148
+ }
149
+ } : entry)
150
+ };
151
+ }
152
+ function trackAt(value, sizes, offsets) {
153
+ if (value <= 0) return {
154
+ index: 0,
155
+ gap: false
156
+ };
157
+ for (let index = 0; index < sizes.length; index += 1) {
158
+ const end = (offsets[index] ?? 0) + (sizes[index] ?? 0);
159
+ if (value <= end) return {
160
+ index,
161
+ gap: false
162
+ };
163
+ if (index < sizes.length - 1 && value < end + 16) return {
164
+ index: index + 1,
165
+ gap: true
166
+ };
167
+ }
168
+ return {
169
+ index: Math.max(0, sizes.length - 1),
170
+ gap: false
171
+ };
172
+ }
173
+ function gridDropTarget(board, shapes, id, point) {
174
+ const section = sectionOf(board, id);
175
+ if (!section || section.layout === "manual") return void 0;
176
+ const box = section.rectangle;
177
+ if (point.x < box.x || point.x > box.x + box.width || point.y < box.y || point.y > box.y + box.height) return void 0;
178
+ const grid = sectionGrid(board, shapes, section);
179
+ const { cells } = grid;
180
+ if (cells.length === 0) return {
181
+ row: 0,
182
+ column: 0,
183
+ axis: "cell"
184
+ };
185
+ const xOffsets = grid.columnStarts;
186
+ const yOffsets = grid.rowStarts;
187
+ const fromStart = point.x - grid.innerX;
188
+ const x = board.direction === "rtl" ? grid.width - fromStart : fromStart;
189
+ const y = point.y - grid.innerY;
190
+ const columnTrack = trackAt(x, grid.widths, xOffsets);
191
+ const rowTrack = trackAt(y, grid.heights, yOffsets);
192
+ if (columnTrack.gap) return targetAtGap(cells, rowTrack.index, columnTrack.index, "row");
193
+ if (rowTrack.gap) return targetAtGap(cells, rowTrack.index, columnTrack.index, "column");
194
+ const cell = cells.find((entry) => entry.row === rowTrack.index && entry.column === columnTrack.index);
195
+ if (!cell) return {
196
+ row: rowTrack.index,
197
+ column: columnTrack.index,
198
+ axis: "cell"
199
+ };
200
+ const size = dimensions(board, shapes, cell.id);
201
+ const localX = x - (xOffsets[cell.column] ?? 0);
202
+ const localY = y - (yOffsets[cell.row] ?? 0);
203
+ return [
204
+ {
205
+ distance: Math.abs(localX),
206
+ target: {
207
+ anchor: cell.id,
208
+ side: "before",
209
+ axis: "row"
210
+ }
211
+ },
212
+ {
213
+ distance: Math.abs(size.width - localX),
214
+ target: {
215
+ anchor: cell.id,
216
+ side: "after",
217
+ axis: "row"
218
+ }
219
+ },
220
+ {
221
+ distance: Math.abs(localY),
222
+ target: {
223
+ anchor: cell.id,
224
+ side: "before",
225
+ axis: "column"
226
+ }
227
+ },
228
+ {
229
+ distance: Math.abs(size.height - localY),
230
+ target: {
231
+ anchor: cell.id,
232
+ side: "after",
233
+ axis: "column"
234
+ }
235
+ }
236
+ ].reduce((best, edge) => edge.distance < best.distance ? edge : best).target;
237
+ }
238
+ function targetAtGap(cells, row, column, axis) {
239
+ const track = cells.filter((cell) => axis === "row" ? cell.row === row : cell.column === column).sort((a, b) => axis === "row" ? a.column - b.column : a.row - b.row);
240
+ const coordinate = axis === "row" ? column : row;
241
+ const after = track.find((cell) => (axis === "row" ? cell.column : cell.row) >= coordinate);
242
+ if (after) return {
243
+ anchor: after.id,
244
+ side: "before",
245
+ axis
246
+ };
247
+ const before = track.at(-1);
248
+ return before ? {
249
+ anchor: before.id,
250
+ side: "after",
251
+ axis
252
+ } : void 0;
253
+ }
254
+ function gridDropMarkerBox(board, shapes, id, target) {
255
+ const section = sectionOf(board, id);
256
+ if (!section || section.layout === "manual") return void 0;
257
+ const grid = sectionGrid(board, shapes, section);
258
+ const { innerX, innerY } = grid;
259
+ if (grid.cells.length === 0) return {
260
+ x: innerX,
261
+ y: innerY,
262
+ width: Math.max(0, section.rectangle.width - 48),
263
+ height: Math.max(0, section.rectangle.height - 48 - 32)
264
+ };
265
+ const cell = target.axis === "cell" ? target : grid.cells.find((entry) => entry.id === target.anchor);
266
+ if (!cell) return void 0;
267
+ const columnStart = grid.columnStarts[cell.column] ?? grid.width;
268
+ const rowStart = grid.rowStarts[cell.row] ?? grid.height;
269
+ const width = grid.widths[cell.column] ?? 0;
270
+ const height = grid.heights[cell.row] ?? 0;
271
+ const x = physicalX(board, grid, columnStart, width);
272
+ const y = innerY + rowStart;
273
+ if (target.axis === "cell") return {
274
+ x,
275
+ y,
276
+ width,
277
+ height
278
+ };
279
+ if (target.axis === "row") return {
280
+ x: physicalX(board, grid, columnStart + (target.side === "after" ? width : 0), 0),
281
+ y,
282
+ width: 2,
283
+ height
284
+ };
285
+ return {
286
+ x,
287
+ y: y + (target.side === "after" ? height : 0),
288
+ width,
289
+ height: 2
290
+ };
291
+ }
292
+ function updateSection(section, cells) {
293
+ const next = normalized(cells);
294
+ return {
295
+ ...section,
296
+ cells: next,
297
+ items: next.map((cell) => cell.id)
298
+ };
299
+ }
300
+ function removeFromGrid(board, shapes, ids) {
301
+ const removed = new Set(ids);
302
+ return {
303
+ ...board,
304
+ sections: board.sections.map((section) => {
305
+ if (section.layout === "manual") return {
306
+ ...section,
307
+ items: section.items.filter((id) => !removed.has(id))
308
+ };
309
+ const cells = cellsFor(board, shapes, section);
310
+ const removedCells = cells.filter((cell) => removed.has(cell.id));
311
+ return updateSection(section, cells.filter((cell) => !removed.has(cell.id)).map((cell) => ({
312
+ ...cell,
313
+ column: cell.column - removedCells.filter((gone) => gone.row === cell.row && gone.column < cell.column).length
314
+ })));
315
+ })
316
+ };
317
+ }
318
+ function insertIntoGrid(board, shapes, id, ids, target) {
319
+ const unique = ids.filter((member, index) => ids.indexOf(member) === index);
320
+ const moved = new Set(unique);
321
+ if (target.axis !== "cell" && moved.has(target.anchor)) return board;
322
+ const removed = removeFromGrid(board, shapes, unique);
323
+ const section = sectionOf(removed, id);
324
+ if (!section || section.layout === "manual") return removed;
325
+ const cells = [...section.cells ?? []];
326
+ const anchor = target.axis === "cell" ? void 0 : cells.find((cell) => cell.id === target.anchor);
327
+ if (target.axis !== "cell" && !anchor) return removed;
328
+ const axis = target.axis === "cell" ? "row" : target.axis;
329
+ const row = target.axis === "cell" ? target.row : (anchor?.row ?? 0) + (axis === "column" && target.side === "after" ? 1 : 0);
330
+ const column = target.axis === "cell" ? target.column : (anchor?.column ?? 0) + (axis === "row" && target.side === "after" ? 1 : 0);
331
+ const inserted = unique.map((member, index) => ({
332
+ id: member,
333
+ row: row + (axis === "column" ? index : 0),
334
+ column: column + (axis === "row" ? index : 0)
335
+ }));
336
+ const shifted = inserted.some((placement) => cells.some((cell) => cell.row === placement.row && cell.column === placement.column)) ? cells.map((cell) => {
337
+ if (axis === "row" && cell.row === row && cell.column >= column) return {
338
+ ...cell,
339
+ column: cell.column + unique.length
340
+ };
341
+ if (axis === "column" && cell.column === column && cell.row >= row) return {
342
+ ...cell,
343
+ row: cell.row + unique.length
344
+ };
345
+ return cell;
346
+ }) : cells;
347
+ return {
348
+ ...removed,
349
+ sections: removed.sections.map((entry) => entry.id === id ? updateSection(entry, [...shifted, ...inserted]) : entry)
350
+ };
351
+ }
352
+ //#endregion
353
+ export { deriveSectionCells, gridDropMarkerBox, gridDropTarget, insertIntoGrid, layoutSection, removeFromGrid };
@@ -1,18 +1,12 @@
1
1
  import { Board, BoardSection } from "./board.js";
2
2
  import { Point } from "./deck.js";
3
3
  import { BoardShapes } from "./board-geometry.js";
4
+ import { MAKE_ROOM_LIMIT, SECTION_GAP, SECTION_MIN_SIZE, SECTION_PADDING, SECTION_TITLE_ROOM } from "./board-section-constants.js";
4
5
  //#region src/board-sections.d.ts
5
- export declare const SECTION_PADDING = 24;
6
- export declare const SECTION_TITLE_ROOM = 32;
7
- export declare const SECTION_MIN_SIZE: {
8
- readonly width: 240;
9
- readonly height: 160;
10
- };
11
- export declare const SECTION_GAP = 16;
12
- export declare const MAKE_ROOM_LIMIT = 50;
13
6
  export declare function fitSection(board: Board, shapes: BoardShapes, sectionId: string): Board;
14
7
  export declare function assignMembership(board: Board, shapes: BoardShapes, ids: readonly string[], point: Point, startingSections?: readonly BoardSection[]): Board;
15
8
  export declare function makeRoom(board: Board, shapes: BoardShapes, changed: readonly string[]): Board;
16
9
  export declare function fitTouchedSections(previous: Board, board: Board, shapes: BoardShapes, touched: readonly string[]): Board;
17
10
  export declare function settleBoard(previous: Board, board: Board, shapes: BoardShapes, touched: readonly string[]): Board;
18
- //#endregion
11
+ //#endregion
12
+ export { MAKE_ROOM_LIMIT, SECTION_GAP, SECTION_MIN_SIZE, SECTION_PADDING, SECTION_TITLE_ROOM };
@@ -1,14 +1,8 @@
1
1
  import { moveItems, moveSection } from "./board-edit.js";
2
2
  import { itemHeight } from "./board-geometry.js";
3
+ import { MAKE_ROOM_LIMIT, SECTION_GAP, SECTION_MIN_SIZE, SECTION_PADDING, SECTION_TITLE_ROOM } from "./board-section-constants.js";
4
+ import { layoutSection } from "./board-section-grid.js";
3
5
  //#region src/board-sections.ts
4
- const SECTION_PADDING = 24;
5
- const SECTION_TITLE_ROOM = 32;
6
- const SECTION_MIN_SIZE = {
7
- width: 240,
8
- height: 160
9
- };
10
- const SECTION_GAP = 16;
11
- const MAKE_ROOM_LIMIT = 50;
12
6
  function itemRectangle(board, shapes, id) {
13
7
  const item = board.items.find((entry) => entry.id === id);
14
8
  if (!item) return void 0;
@@ -197,10 +191,14 @@ function touchedSections(previous, board, touched) {
197
191
  return [...previous.sections, ...board.sections].filter((section) => touchedIds.has(section.id) || section.items.some((id) => touchedIds.has(id))).map((section) => section.id).filter((id, index, ids) => ids.indexOf(id) === index);
198
192
  }
199
193
  function fitSections(board, shapes, sectionIds) {
200
- return sectionIds.reduce((value, id) => fitSection(value, shapes, id), board);
194
+ return sectionIds.reduce((value, id) => {
195
+ return value.sections.find((entry) => entry.id === id)?.layout === "manual" ? fitSection(value, shapes, id) : layoutSection(value, shapes, id);
196
+ }, board);
201
197
  }
202
198
  function fitTouchedSections(previous, board, shapes, touched) {
203
- return fitSections(board, shapes, touchedSections(previous, board, touched));
199
+ return touchedSections(previous, board, touched).reduce((value, id) => {
200
+ return value.sections.find((entry) => entry.id === id)?.layout === "manual" ? fitSection(value, shapes, id) : value;
201
+ }, board);
204
202
  }
205
203
  function settleBoard(previous, board, shapes, touched) {
206
204
  const sectionIds = touchedSections(previous, board, touched);
package/dist/board.d.ts CHANGED
@@ -23,6 +23,12 @@ export declare const BoardLabel: z.ZodObject<{
23
23
  y: z.ZodNumber;
24
24
  }, z.core.$strip>;
25
25
  export type BoardLabel = z.infer<typeof BoardLabel>;
26
+ export declare const GridCell: z.ZodObject<{
27
+ id: z.ZodString;
28
+ row: z.ZodNumber;
29
+ column: z.ZodNumber;
30
+ }, z.core.$strip>;
31
+ export type GridCell = z.infer<typeof GridCell>;
26
32
  export declare const BoardSection: z.ZodObject<{
27
33
  id: z.ZodString;
28
34
  title: z.ZodString;
@@ -33,6 +39,15 @@ export declare const BoardSection: z.ZodObject<{
33
39
  height: z.ZodNumber;
34
40
  }, z.core.$strip>;
35
41
  items: z.ZodArray<z.ZodString>;
42
+ layout: z.ZodOptional<z.ZodEnum<{
43
+ auto: "auto";
44
+ manual: "manual";
45
+ }>>;
46
+ cells: z.ZodOptional<z.ZodArray<z.ZodObject<{
47
+ id: z.ZodString;
48
+ row: z.ZodNumber;
49
+ column: z.ZodNumber;
50
+ }, z.core.$strip>>>;
36
51
  }, z.core.$strip>;
37
52
  export type BoardSection = z.infer<typeof BoardSection>;
38
53
  export declare const Board: z.ZodObject<{
@@ -72,6 +87,15 @@ export declare const Board: z.ZodObject<{
72
87
  height: z.ZodNumber;
73
88
  }, z.core.$strip>;
74
89
  items: z.ZodArray<z.ZodString>;
90
+ layout: z.ZodOptional<z.ZodEnum<{
91
+ auto: "auto";
92
+ manual: "manual";
93
+ }>>;
94
+ cells: z.ZodOptional<z.ZodArray<z.ZodObject<{
95
+ id: z.ZodString;
96
+ row: z.ZodNumber;
97
+ column: z.ZodNumber;
98
+ }, z.core.$strip>>>;
75
99
  }, z.core.$strip>>;
76
100
  }, z.core.$strip>;
77
101
  export type Board = z.infer<typeof Board>;
package/dist/board.js CHANGED
@@ -24,11 +24,18 @@ const BoardLabel = z.object({
24
24
  x: z.number(),
25
25
  y: z.number()
26
26
  });
27
+ const GridCell = z.object({
28
+ id: z.string(),
29
+ row: z.number(),
30
+ column: z.number()
31
+ });
27
32
  const BoardSection = z.object({
28
33
  id: z.string(),
29
34
  title: z.string(),
30
35
  rectangle: Box,
31
- items: z.array(z.string())
36
+ items: z.array(z.string()),
37
+ layout: z.enum(["auto", "manual"]).optional(),
38
+ cells: z.array(GridCell).optional()
32
39
  });
33
40
  const Board = z.object({
34
41
  title: z.string(),
@@ -69,7 +76,51 @@ const Board = z.object({
69
76
  message: `section member "${id}" is not a board item id; use the id of an existing item`
70
77
  });
71
78
  });
79
+ if (section.layout === "manual" || !section.cells) return;
80
+ const members = new Set(section.items);
81
+ const positions = /* @__PURE__ */ new Set();
82
+ const cellIds = /* @__PURE__ */ new Set();
83
+ section.cells.forEach((cell, cellIndex) => {
84
+ const path = [
85
+ "sections",
86
+ sectionIndex,
87
+ "cells",
88
+ cellIndex
89
+ ];
90
+ if (!members.has(cell.id)) ctx.addIssue({
91
+ code: "custom",
92
+ path,
93
+ message: `section "${section.id}" cell "${cell.id}" is not a member; use the id of a section member`
94
+ });
95
+ if (!Number.isInteger(cell.row) || cell.row < 0 || !Number.isInteger(cell.column) || cell.column < 0) ctx.addIssue({
96
+ code: "custom",
97
+ path,
98
+ message: `section "${section.id}" cell "${cell.id}" has an invalid track index; use non-negative integer row and column indexes`
99
+ });
100
+ const position = `${cell.row}:${cell.column}`;
101
+ if (positions.has(position)) ctx.addIssue({
102
+ code: "custom",
103
+ path,
104
+ message: `section "${section.id}" cell "${cell.id}" shares position (${cell.row}, ${cell.column}); give every cell a unique position`
105
+ });
106
+ positions.add(position);
107
+ if (cellIds.has(cell.id)) ctx.addIssue({
108
+ code: "custom",
109
+ path,
110
+ message: `section "${section.id}" cell "${cell.id}" is duplicated; provide exactly one cell for each member`
111
+ });
112
+ cellIds.add(cell.id);
113
+ });
114
+ for (const id of section.items) if (!cellIds.has(id)) ctx.addIssue({
115
+ code: "custom",
116
+ path: [
117
+ "sections",
118
+ sectionIndex,
119
+ "cells"
120
+ ],
121
+ message: `section "${section.id}" member "${id}" has no cell; add one cell for every member`
122
+ });
72
123
  });
73
124
  });
74
125
  //#endregion
75
- export { Board, BoardItem, BoardLabel, BoardSection };
126
+ export { Board, BoardItem, BoardLabel, BoardSection, GridCell };
@@ -119,7 +119,7 @@ export declare function deckFromSlides(rows: SlideRow[], options: DeckFromSlides
119
119
  kind: "none" | "fade" | "slide";
120
120
  duration: number;
121
121
  } | undefined;
122
- playback?: "continuous" | "step" | undefined;
122
+ playback?: "continuous" | "segmented" | undefined;
123
123
  captions?: "on" | "off" | "viewer" | undefined;
124
124
  };
125
125
  //#endregion
package/dist/deck.d.ts CHANGED
@@ -385,6 +385,13 @@ export declare const DECK_SIZE: {
385
385
  readonly width: 1280;
386
386
  readonly height: 720;
387
387
  };
388
+ export declare const PLAYBACK_MODES: readonly ["segmented", "continuous"];
389
+ export type PlaybackMode = (typeof PLAYBACK_MODES)[number];
390
+ export declare const Playback: z.ZodUnion<readonly [z.ZodEnum<{
391
+ continuous: "continuous";
392
+ segmented: "segmented";
393
+ }>, z.ZodPipe<z.ZodLiteral<"step">, z.ZodTransform<"continuous" | "segmented", "step">>]>;
394
+ export declare function parsePlayback(value: unknown): PlaybackMode | undefined;
388
395
  export declare function annotationCueIssues(narration: Slide['narration'], annotations: number): string[];
389
396
  export declare const Deck: z.ZodObject<{
390
397
  title: z.ZodString;
@@ -421,10 +428,10 @@ export declare const Deck: z.ZodObject<{
421
428
  }>;
422
429
  duration: z.ZodDefault<z.ZodNumber>;
423
430
  }, z.core.$strip>>;
424
- playback: z.ZodOptional<z.ZodEnum<{
431
+ playback: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
425
432
  continuous: "continuous";
426
- step: "step";
427
- }>>;
433
+ segmented: "segmented";
434
+ }>, z.ZodPipe<z.ZodLiteral<"step">, z.ZodTransform<"continuous" | "segmented", "step">>]>>;
428
435
  captions: z.ZodOptional<z.ZodEnum<{
429
436
  on: "on";
430
437
  off: "off";
package/dist/deck.js CHANGED
@@ -119,6 +119,12 @@ const DECK_SIZE = {
119
119
  width: 1280,
120
120
  height: 720
121
121
  };
122
+ const PLAYBACK_MODES = ["segmented", "continuous"];
123
+ const Playback = z.union([z.enum(PLAYBACK_MODES), z.literal("step").transform(() => "segmented")]);
124
+ function parsePlayback(value) {
125
+ const parsed = Playback.safeParse(value);
126
+ return parsed.success ? parsed.data : void 0;
127
+ }
122
128
  const DeckShape = z.object({
123
129
  title: z.string(),
124
130
  size: Size.default(DECK_SIZE),
@@ -129,7 +135,7 @@ const DeckShape = z.object({
129
135
  dir: Direction.optional(),
130
136
  lang: z.string().optional(),
131
137
  transition: Transition.optional(),
132
- playback: z.enum(["step", "continuous"]).optional(),
138
+ playback: Playback.optional(),
133
139
  captions: z.enum([
134
140
  "on",
135
141
  "off",
@@ -208,4 +214,4 @@ function exceedsLimits(deck, limits) {
208
214
  return reasons;
209
215
  }
210
216
  //#endregion
211
- export { Annotation, Box, Cue, DECK_SIZE, Deck, DeckShape, ImageFit, Manifest, Narration, Point, PublishedShot, SHOT_KEY, Shot, Size, Slide, SlideContent, Word, annotationCueIssues, exceedsLimits };
217
+ export { Annotation, Box, Cue, DECK_SIZE, Deck, DeckShape, ImageFit, Manifest, Narration, PLAYBACK_MODES, Playback, Point, PublishedShot, SHOT_KEY, Shot, Size, Slide, SlideContent, Word, annotationCueIssues, exceedsLimits, parsePlayback };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
- import { Board, BoardInput, BoardItem, BoardLabel, BoardSection } from "./board.js";
2
- import { Annotation, Box, Cue, DECK_SIZE, Deck, DeckInput, DeckLimits, ImageFit, Manifest, Narration, Point, PublishedShot, SHOT_KEY, Shot, Size, Slide, SlideContent, Word, annotationCueIssues, exceedsLimits } from "./deck.js";
1
+ import { Board, BoardInput, BoardItem, BoardLabel, BoardSection, GridCell } from "./board.js";
2
+ import { Annotation, Box, Cue, DECK_SIZE, Deck, DeckInput, DeckLimits, ImageFit, Manifest, Narration, PLAYBACK_MODES, Playback, PlaybackMode, Point, PublishedShot, SHOT_KEY, Shot, Size, Slide, SlideContent, Word, annotationCueIssues, exceedsLimits, parsePlayback } from "./deck.js";
3
3
  import { BoardItemAnchor, MIN_BOARD_ITEM_WIDTH, addItem, moveItems, moveSection, removeItems, resizeItem } from "./board-edit.js";
4
4
  import { ArrangeOptions, BoardShapes, BoardView, MAX_BOARD_ZOOM, MIN_BOARD_ZOOM, ZoomBounds, arrange, boardBounds, boardToScreen, itemAt, itemHeight, revealView, screenToBoard, visibleItems, zoomAt, zoomToFit } from "./board-geometry.js";
5
5
  import { BoardListingGroup, boardListing, boardListingIsEmpty, foldForSearch, nameMatches } from "./board-listing.js";
6
- import { MAKE_ROOM_LIMIT, SECTION_GAP, SECTION_MIN_SIZE, SECTION_PADDING, SECTION_TITLE_ROOM, assignMembership, fitSection, fitTouchedSections, makeRoom, settleBoard } from "./board-sections.js";
6
+ import { GridDropTarget, deriveSectionCells, gridDropMarkerBox, gridDropTarget, insertIntoGrid, layoutSection, removeFromGrid } from "./board-section-grid.js";
7
+ import { MAKE_ROOM_LIMIT, SECTION_GAP, SECTION_MIN_SIZE, SECTION_PADDING, SECTION_TITLE_ROOM } from "./board-section-constants.js";
8
+ import { assignMembership, fitSection, fitTouchedSections, makeRoom, settleBoard } from "./board-sections.js";
7
9
  import { Book, BookContents, BookFrontMatterEntry, BookInput, BookMode, BookPage, BookSection, BookView, BookViewInput, Chapter, isBookContents, isHebrew, noteMarkerIssues } from "./book.js";
8
10
  import { DeckFromSlidesOptions, SlideRow, deckFromSlides } from "./deck-from-slides.js";
9
11
  import { Direction, TextDirection, firstStrongDirection, languageDirection, resolveDirection } from "./direction.js";
@@ -462,10 +464,10 @@ export declare function jsonSchemas(): {
462
464
  }>;
463
465
  duration: z.ZodDefault<z.ZodNumber>;
464
466
  }, z.core.$strip>>;
465
- playback: z.ZodOptional<z.ZodEnum<{
467
+ playback: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
466
468
  continuous: "continuous";
467
- step: "step";
468
- }>>;
469
+ segmented: "segmented";
470
+ }>, z.ZodPipe<z.ZodLiteral<"step">, z.ZodTransform<"continuous" | "segmented", "step">>]>>;
469
471
  captions: z.ZodOptional<z.ZodEnum<{
470
472
  on: "on";
471
473
  off: "off";
@@ -718,8 +720,17 @@ export declare function jsonSchemas(): {
718
720
  height: z.ZodNumber;
719
721
  }, z.core.$strip>;
720
722
  items: z.ZodArray<z.ZodString>;
723
+ layout: z.ZodOptional<z.ZodEnum<{
724
+ auto: "auto";
725
+ manual: "manual";
726
+ }>>;
727
+ cells: z.ZodOptional<z.ZodArray<z.ZodObject<{
728
+ id: z.ZodString;
729
+ row: z.ZodNumber;
730
+ column: z.ZodNumber;
731
+ }, z.core.$strip>>>;
721
732
  }, z.core.$strip>>;
722
733
  }, z.core.$strip>>;
723
734
  };
724
735
  //#endregion
725
- export { Annotation, type ArrangeOptions, Board, type BoardInput, BoardItem, type BoardItemAnchor, BoardLabel, type BoardListingGroup, BoardSection, type BoardShapes, type BoardView, Book, BookContents, BookFrontMatterEntry, type BookInput, BookMode, BookPage, BookSection, BookView, type BookViewInput, Box, Chapter, Cue, DECK_SIZE, DEFAULT_TRANSITION_DURATION_MS, Deck, type DeckFromSlidesOptions, type DeckInput, type DeckLimits, Direction, ImageFit, type Imposed, Lexicon, MAKE_ROOM_LIMIT, MARK_GAP_MM, MARK_LENGTH_MM, MAX_BOARD_ZOOM, MAX_SLIDE_TIMING_MS, MAX_TRANSITION_DURATION_MS, MIN_BOARD_ITEM_WIDTH, MIN_BOARD_ZOOM, MIN_TRANSITION_DURATION_MS, MM_PER_PT, Manifest, type MillimeterEdges, NAMED_PAPERS, Narration, NoteMarker, NumberSystem, PAPER_MM, PX_PER_MM, type PageFit, type PagePlacementKind, type PagePlacementLayout, Paper, type PaperBox, type Placement, Point, PronunciationEntry, PublishedShot, SECTION_GAP, SECTION_MIN_SIZE, SECTION_PADDING, SECTION_TITLE_ROOM, SHOT_KEY, Scripts, Shot, type ShotUse, Size, Slide, SlideContent, type SlideRow, TextDirection, Transition, TransitionInput, Word, type ZoomBounds, addItem, annotationCueIssues, arrange, assignMembership, boardBounds, boardListing, boardListingIsEmpty, boardToScreen, deckFromSlides, exceedsLimits, firstStrongDirection, fitPage, fitSection, fitTouchedSections, foldForSearch, formatNumber, impose, isBookContents, isHebrew, itemAt, itemHeight, languageDirection, lexiconVersion, makeRoom, mirrorPlacement, moveItems, moveSection, nameMatches, noteMarker, noteMarkerIssues, pagePlacementLayout, printBoxes, printPadding, printProperties, removeItems, resizeItem, resolveDirection, revealView, roundMillimeters, screenToBoard, settleBoard, sheetMm, shotsUsed, toMillimeters, visibleItems, zoomAt, zoomToFit };
736
+ export { Annotation, type ArrangeOptions, Board, type BoardInput, BoardItem, type BoardItemAnchor, BoardLabel, type BoardListingGroup, BoardSection, type BoardShapes, type BoardView, Book, BookContents, BookFrontMatterEntry, type BookInput, BookMode, BookPage, BookSection, BookView, type BookViewInput, Box, Chapter, Cue, DECK_SIZE, DEFAULT_TRANSITION_DURATION_MS, Deck, type DeckFromSlidesOptions, type DeckInput, type DeckLimits, Direction, GridCell, type GridDropTarget, ImageFit, type Imposed, Lexicon, MAKE_ROOM_LIMIT, MARK_GAP_MM, MARK_LENGTH_MM, MAX_BOARD_ZOOM, MAX_SLIDE_TIMING_MS, MAX_TRANSITION_DURATION_MS, MIN_BOARD_ITEM_WIDTH, MIN_BOARD_ZOOM, MIN_TRANSITION_DURATION_MS, MM_PER_PT, Manifest, type MillimeterEdges, NAMED_PAPERS, Narration, NoteMarker, NumberSystem, PAPER_MM, PLAYBACK_MODES, PX_PER_MM, type PageFit, type PagePlacementKind, type PagePlacementLayout, Paper, type PaperBox, type Placement, Playback, type PlaybackMode, Point, PronunciationEntry, PublishedShot, SECTION_GAP, SECTION_MIN_SIZE, SECTION_PADDING, SECTION_TITLE_ROOM, SHOT_KEY, Scripts, Shot, type ShotUse, Size, Slide, SlideContent, type SlideRow, TextDirection, Transition, TransitionInput, Word, type ZoomBounds, addItem, annotationCueIssues, arrange, assignMembership, boardBounds, boardListing, boardListingIsEmpty, boardToScreen, deckFromSlides, deriveSectionCells, exceedsLimits, firstStrongDirection, fitPage, fitSection, fitTouchedSections, foldForSearch, formatNumber, gridDropMarkerBox, gridDropTarget, impose, insertIntoGrid, isBookContents, isHebrew, itemAt, itemHeight, languageDirection, layoutSection, lexiconVersion, makeRoom, mirrorPlacement, moveItems, moveSection, nameMatches, noteMarker, noteMarkerIssues, pagePlacementLayout, parsePlayback, printBoxes, printPadding, printProperties, removeFromGrid, removeItems, resizeItem, resolveDirection, revealView, roundMillimeters, screenToBoard, settleBoard, sheetMm, shotsUsed, toMillimeters, visibleItems, zoomAt, zoomToFit };
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@ import { Direction, TextDirection, firstStrongDirection, languageDirection, reso
2
2
  import { Scripts } from "./scripts.js";
3
3
  import { MAX_SLIDE_TIMING_MS } from "./timing.js";
4
4
  import { DEFAULT_TRANSITION_DURATION_MS, MAX_TRANSITION_DURATION_MS, MIN_TRANSITION_DURATION_MS, Transition } from "./transition.js";
5
- import { Annotation, Box, Cue, DECK_SIZE, Deck, DeckShape, ImageFit, Manifest, Narration, Point, PublishedShot, SHOT_KEY, Shot, Size, Slide, SlideContent, Word, annotationCueIssues, exceedsLimits } from "./deck.js";
6
- import { Board, BoardItem, BoardLabel, BoardSection } from "./board.js";
5
+ import { Annotation, Box, Cue, DECK_SIZE, Deck, DeckShape, ImageFit, Manifest, Narration, PLAYBACK_MODES, Playback, Point, PublishedShot, SHOT_KEY, Shot, Size, Slide, SlideContent, Word, annotationCueIssues, exceedsLimits, parsePlayback } from "./deck.js";
6
+ import { Board, BoardItem, BoardLabel, BoardSection, GridCell } from "./board.js";
7
7
  import { NoteMarker, NumberSystem, formatNumber, noteMarker } from "./numbering.js";
8
8
  import { MARK_GAP_MM, MARK_LENGTH_MM, MM_PER_PT, NAMED_PAPERS, PAPER_MM, PX_PER_MM, Paper, printBoxes, printPadding, printProperties, roundMillimeters, sheetMm, toMillimeters } from "./paper.js";
9
9
  import { Book, BookContents, BookFrontMatterEntry, BookMode, BookPage, BookSection, BookShape, BookView, Chapter, isBookContents, isHebrew, noteMarkerIssues } from "./book.js";
@@ -11,7 +11,9 @@ import { Lexicon, PronunciationEntry, lexiconVersion } from "./lexicon.js";
11
11
  import { MIN_BOARD_ITEM_WIDTH, addItem, moveItems, moveSection, removeItems, resizeItem } from "./board-edit.js";
12
12
  import { MAX_BOARD_ZOOM, MIN_BOARD_ZOOM, arrange, boardBounds, boardToScreen, itemAt, itemHeight, revealView, screenToBoard, visibleItems, zoomAt, zoomToFit } from "./board-geometry.js";
13
13
  import { boardListing, boardListingIsEmpty, foldForSearch, nameMatches } from "./board-listing.js";
14
- import { MAKE_ROOM_LIMIT, SECTION_GAP, SECTION_MIN_SIZE, SECTION_PADDING, SECTION_TITLE_ROOM, assignMembership, fitSection, fitTouchedSections, makeRoom, settleBoard } from "./board-sections.js";
14
+ import { MAKE_ROOM_LIMIT, SECTION_GAP, SECTION_MIN_SIZE, SECTION_PADDING, SECTION_TITLE_ROOM } from "./board-section-constants.js";
15
+ import { deriveSectionCells, gridDropMarkerBox, gridDropTarget, insertIntoGrid, layoutSection, removeFromGrid } from "./board-section-grid.js";
16
+ import { assignMembership, fitSection, fitTouchedSections, makeRoom, settleBoard } from "./board-sections.js";
15
17
  import { deckFromSlides } from "./deck-from-slides.js";
16
18
  import { fitPage, impose, mirrorPlacement, pagePlacementLayout } from "./imposition.js";
17
19
  import { shotsUsed } from "./shots-used.js";
@@ -157,4 +159,4 @@ function jsonSchemas() {
157
159
  };
158
160
  }
159
161
  //#endregion
160
- export { Annotation, Board, BoardItem, BoardLabel, BoardSection, Book, BookContents, BookFrontMatterEntry, BookMode, BookPage, BookSection, BookView, Box, Chapter, Cue, DECK_SIZE, DEFAULT_TRANSITION_DURATION_MS, Deck, Direction, ImageFit, Imposition, Lexicon, MAKE_ROOM_LIMIT, MARK_GAP_MM, MARK_LENGTH_MM, MAX_BOARD_ZOOM, MAX_IMPOSITION_CELLS, MAX_SLIDE_TIMING_MS, MAX_TRANSITION_DURATION_MS, MIN_BOARD_ITEM_WIDTH, MIN_BOARD_ZOOM, MIN_TRANSITION_DURATION_MS, MM_PER_PT, Manifest, Margin, NAMED_PAPERS, Narration, NoteMarker, NumberSystem, PAPER_MM, PX_PER_MM, Page, PageSize, Paper, Point, Print, PronunciationEntry, PublishedShot, SECTION_GAP, SECTION_MIN_SIZE, SECTION_PADDING, SECTION_TITLE_ROOM, SHOT_KEY, Scripts, Shot, Size, Slide, SlideContent, TextDirection, Transition, Word, addItem, annotationCueIssues, arrange, assignMembership, boardBounds, boardListing, boardListingIsEmpty, boardToScreen, deckFromSlides, exceedsLimits, firstStrongDirection, fitPage, fitSection, fitTouchedSections, foldForSearch, formatNumber, impose, isBookContents, isHebrew, itemAt, itemHeight, jsonSchemas, languageDirection, lexiconVersion, makeRoom, mirrorPlacement, moveItems, moveSection, nameMatches, noteMarker, noteMarkerIssues, pagePlacementLayout, printBoxes, printPadding, printProperties, removeItems, resizeItem, resolveDirection, revealView, roundMillimeters, screenToBoard, settleBoard, sheetMm, shotsUsed, toMillimeters, visibleItems, zoomAt, zoomToFit };
162
+ export { Annotation, Board, BoardItem, BoardLabel, BoardSection, Book, BookContents, BookFrontMatterEntry, BookMode, BookPage, BookSection, BookView, Box, Chapter, Cue, DECK_SIZE, DEFAULT_TRANSITION_DURATION_MS, Deck, Direction, GridCell, ImageFit, Imposition, Lexicon, MAKE_ROOM_LIMIT, MARK_GAP_MM, MARK_LENGTH_MM, MAX_BOARD_ZOOM, MAX_IMPOSITION_CELLS, MAX_SLIDE_TIMING_MS, MAX_TRANSITION_DURATION_MS, MIN_BOARD_ITEM_WIDTH, MIN_BOARD_ZOOM, MIN_TRANSITION_DURATION_MS, MM_PER_PT, Manifest, Margin, NAMED_PAPERS, Narration, NoteMarker, NumberSystem, PAPER_MM, PLAYBACK_MODES, PX_PER_MM, Page, PageSize, Paper, Playback, Point, Print, PronunciationEntry, PublishedShot, SECTION_GAP, SECTION_MIN_SIZE, SECTION_PADDING, SECTION_TITLE_ROOM, SHOT_KEY, Scripts, Shot, Size, Slide, SlideContent, TextDirection, Transition, Word, addItem, annotationCueIssues, arrange, assignMembership, boardBounds, boardListing, boardListingIsEmpty, boardToScreen, deckFromSlides, deriveSectionCells, exceedsLimits, firstStrongDirection, fitPage, fitSection, fitTouchedSections, foldForSearch, formatNumber, gridDropMarkerBox, gridDropTarget, impose, insertIntoGrid, isBookContents, isHebrew, itemAt, itemHeight, jsonSchemas, languageDirection, layoutSection, lexiconVersion, makeRoom, mirrorPlacement, moveItems, moveSection, nameMatches, noteMarker, noteMarkerIssues, pagePlacementLayout, parsePlayback, printBoxes, printPadding, printProperties, removeFromGrid, removeItems, resizeItem, resolveDirection, revealView, roundMillimeters, screenToBoard, settleBoard, sheetMm, shotsUsed, toMillimeters, visibleItems, zoomAt, zoomToFit };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segnavia/format",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {