@ganttloom/gantt-export 0.2.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 +109 -0
- package/dist/chunk-JB7HLLAI.js +113 -0
- package/dist/chunk-JB7HLLAI.js.map +1 -0
- package/dist/chunk-PXURNZ6Y.js +272 -0
- package/dist/chunk-PXURNZ6Y.js.map +1 -0
- package/dist/chunk-QPE3ABUG.js +229 -0
- package/dist/chunk-QPE3ABUG.js.map +1 -0
- package/dist/chunk-TYNGUB4V.js +434 -0
- package/dist/chunk-TYNGUB4V.js.map +1 -0
- package/dist/chunk-X5RULSWH.js +125 -0
- package/dist/chunk-X5RULSWH.js.map +1 -0
- package/dist/index.cjs +1167 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/paginate-Z2RycLzz.d.cts +73 -0
- package/dist/paginate-Z2RycLzz.d.ts +73 -0
- package/dist/pdf.cjs +671 -0
- package/dist/pdf.cjs.map +1 -0
- package/dist/pdf.d.cts +13 -0
- package/dist/pdf.d.ts +13 -0
- package/dist/pdf.js +11 -0
- package/dist/pdf.js.map +1 -0
- package/dist/pptx.cjs +827 -0
- package/dist/pptx.cjs.map +1 -0
- package/dist/pptx.d.cts +15 -0
- package/dist/pptx.d.ts +15 -0
- package/dist/pptx.js +12 -0
- package/dist/pptx.js.map +1 -0
- package/dist/xlsx.cjs +256 -0
- package/dist/xlsx.cjs.map +1 -0
- package/dist/xlsx.d.cts +22 -0
- package/dist/xlsx.d.ts +22 -0
- package/dist/xlsx.js +8 -0
- package/dist/xlsx.js.map +1 -0
- package/package.json +69 -0
package/dist/pdf.cjs
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/pdf.ts
|
|
21
|
+
var pdf_exports = {};
|
|
22
|
+
__export(pdf_exports, {
|
|
23
|
+
computePagination: () => computePagination,
|
|
24
|
+
renderToPDF: () => renderToPDF
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(pdf_exports);
|
|
27
|
+
|
|
28
|
+
// src/paginate.ts
|
|
29
|
+
var PAGE_SIZES_PT = {
|
|
30
|
+
// Values are the portrait (short-edge width x long-edge height) dimensions
|
|
31
|
+
// in PDF/typographic points (72pt = 1in).
|
|
32
|
+
A4: { width: 595.28, height: 841.89 },
|
|
33
|
+
Letter: { width: 612, height: 792 },
|
|
34
|
+
A3: { width: 841.89, height: 1190.55 }
|
|
35
|
+
};
|
|
36
|
+
var DEFAULT_MARGIN_PT = 36;
|
|
37
|
+
var DEFAULT_SCALE = 0.75;
|
|
38
|
+
var MIN_TIMELINE_WIDTH_PT = 50;
|
|
39
|
+
var MIN_ROWS_HEIGHT_PT = 20;
|
|
40
|
+
function resolvePageSize(opt) {
|
|
41
|
+
if (opt?.custom) {
|
|
42
|
+
return { width: opt.custom.widthPt, height: opt.custom.heightPt };
|
|
43
|
+
}
|
|
44
|
+
const name = opt?.name ?? "A4";
|
|
45
|
+
const base = PAGE_SIZES_PT[name];
|
|
46
|
+
const orientation = opt?.orientation ?? "landscape";
|
|
47
|
+
const short = Math.min(base.width, base.height);
|
|
48
|
+
const long = Math.max(base.width, base.height);
|
|
49
|
+
return orientation === "landscape" ? { width: long, height: short } : { width: short, height: long };
|
|
50
|
+
}
|
|
51
|
+
function defaultGridPanelWidthPx(model) {
|
|
52
|
+
const cols = model.columns ?? [];
|
|
53
|
+
if (cols.length === 0) return 160;
|
|
54
|
+
return cols.reduce((sum, c) => sum + (c.width ?? 120), 0);
|
|
55
|
+
}
|
|
56
|
+
function computePagination(model, options = {}) {
|
|
57
|
+
const { width: pageWidthPt, height: pageHeightPt } = resolvePageSize(options.pageSize);
|
|
58
|
+
const margins = {
|
|
59
|
+
top: options.margins?.top ?? DEFAULT_MARGIN_PT,
|
|
60
|
+
right: options.margins?.right ?? DEFAULT_MARGIN_PT,
|
|
61
|
+
bottom: options.margins?.bottom ?? DEFAULT_MARGIN_PT,
|
|
62
|
+
left: options.margins?.left ?? DEFAULT_MARGIN_PT
|
|
63
|
+
};
|
|
64
|
+
const scale = options.scale ?? DEFAULT_SCALE;
|
|
65
|
+
const gridPanelWidthPx = options.gridPanelWidthPx ?? defaultGridPanelWidthPx(model);
|
|
66
|
+
const gridPanelWidthPt = gridPanelWidthPx * scale;
|
|
67
|
+
const headerHeightPx = model.headerHeight;
|
|
68
|
+
const headerHeightPt = headerHeightPx * scale;
|
|
69
|
+
const contentWidthPt = pageWidthPt - margins.left - margins.right;
|
|
70
|
+
const contentHeightPt = pageHeightPt - margins.top - margins.bottom;
|
|
71
|
+
const timelineWidthPtPerPage = Math.max(contentWidthPt - gridPanelWidthPt, MIN_TIMELINE_WIDTH_PT);
|
|
72
|
+
const rowsHeightPtPerPage = Math.max(contentHeightPt - headerHeightPt, MIN_ROWS_HEIGHT_PT);
|
|
73
|
+
const timelineWidthPxPerPage = timelineWidthPtPerPage / scale;
|
|
74
|
+
const rowsHeightPxPerPage = rowsHeightPtPerPage / scale;
|
|
75
|
+
const totalHeightPx = Math.max(model.height, 1);
|
|
76
|
+
const totalWidthPx = Math.max(model.width, 1);
|
|
77
|
+
const rowCount = Math.max(1, Math.ceil(totalHeightPx / rowsHeightPxPerPage));
|
|
78
|
+
const colCount = Math.max(1, Math.ceil(totalWidthPx / timelineWidthPxPerPage));
|
|
79
|
+
const rowBands = [];
|
|
80
|
+
for (let i = 0; i < rowCount; i++) {
|
|
81
|
+
rowBands.push({
|
|
82
|
+
start: i * rowsHeightPxPerPage,
|
|
83
|
+
end: Math.min(totalHeightPx, (i + 1) * rowsHeightPxPerPage)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const colBands = [];
|
|
87
|
+
for (let i = 0; i < colCount; i++) {
|
|
88
|
+
colBands.push({
|
|
89
|
+
start: i * timelineWidthPxPerPage,
|
|
90
|
+
end: Math.min(totalWidthPx, (i + 1) * timelineWidthPxPerPage)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const pages = [];
|
|
94
|
+
for (let r = 0; r < rowCount; r++) {
|
|
95
|
+
const rowBand = rowBands[r];
|
|
96
|
+
for (let c = 0; c < colCount; c++) {
|
|
97
|
+
const colBand = colBands[c];
|
|
98
|
+
pages.push({
|
|
99
|
+
rowBand: r,
|
|
100
|
+
colBand: c,
|
|
101
|
+
rowStartY: rowBand.start,
|
|
102
|
+
rowEndY: rowBand.end,
|
|
103
|
+
colStartX: colBand.start,
|
|
104
|
+
colEndX: colBand.end
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
pageWidthPt,
|
|
110
|
+
pageHeightPt,
|
|
111
|
+
margins,
|
|
112
|
+
scale,
|
|
113
|
+
gridPanelWidthPx,
|
|
114
|
+
gridPanelWidthPt,
|
|
115
|
+
headerHeightPx,
|
|
116
|
+
headerHeightPt,
|
|
117
|
+
timelineWidthPxPerPage,
|
|
118
|
+
rowsHeightPxPerPage,
|
|
119
|
+
rowBands,
|
|
120
|
+
colBands,
|
|
121
|
+
pages,
|
|
122
|
+
rowCount,
|
|
123
|
+
colCount
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function clipSegment(x0, y0, x1, y1, rect) {
|
|
127
|
+
let t0 = 0;
|
|
128
|
+
let t1 = 1;
|
|
129
|
+
const dx = x1 - x0;
|
|
130
|
+
const dy = y1 - y0;
|
|
131
|
+
const checks = [
|
|
132
|
+
[-dx, x0 - rect.x0],
|
|
133
|
+
[dx, rect.x1 - x0],
|
|
134
|
+
[-dy, y0 - rect.y0],
|
|
135
|
+
[dy, rect.y1 - y0]
|
|
136
|
+
];
|
|
137
|
+
for (const [p, q] of checks) {
|
|
138
|
+
if (p === 0) {
|
|
139
|
+
if (q < 0) return null;
|
|
140
|
+
} else {
|
|
141
|
+
const r = q / p;
|
|
142
|
+
if (p < 0) {
|
|
143
|
+
if (r > t1) return null;
|
|
144
|
+
if (r > t0) t0 = r;
|
|
145
|
+
} else {
|
|
146
|
+
if (r < t0) return null;
|
|
147
|
+
if (r < t1) t1 = r;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { x0: x0 + t0 * dx, y0: y0 + t0 * dy, x1: x0 + t1 * dx, y1: y0 + t1 * dy };
|
|
152
|
+
}
|
|
153
|
+
function clipRect(x, y, w, h, rect) {
|
|
154
|
+
const x0 = Math.max(x, rect.x0);
|
|
155
|
+
const y0 = Math.max(y, rect.y0);
|
|
156
|
+
const x1 = Math.min(x + w, rect.x1);
|
|
157
|
+
const y1 = Math.min(y + h, rect.y1);
|
|
158
|
+
if (x1 <= x0 || y1 <= y0) return null;
|
|
159
|
+
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/geom.ts
|
|
163
|
+
function computeLinkPolyline(fromBar, toBar) {
|
|
164
|
+
const x0 = fromBar.x + fromBar.width;
|
|
165
|
+
const y0 = fromBar.y + fromBar.height / 2;
|
|
166
|
+
const x1 = toBar.x;
|
|
167
|
+
const y1 = toBar.y + toBar.height / 2;
|
|
168
|
+
if (Math.abs(y1 - y0) < 0.01) {
|
|
169
|
+
return [{ x: x0, y: y0 }, { x: x1, y: y1 }];
|
|
170
|
+
}
|
|
171
|
+
const midX = x0 + (x1 - x0) / 2;
|
|
172
|
+
return [
|
|
173
|
+
{ x: x0, y: y0 },
|
|
174
|
+
{ x: midX, y: y0 },
|
|
175
|
+
{ x: midX, y: y1 },
|
|
176
|
+
{ x: x1, y: y1 }
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
function clipPolylineToRect(points, rect) {
|
|
180
|
+
const segments = [];
|
|
181
|
+
let current = [];
|
|
182
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
183
|
+
const a = points[i];
|
|
184
|
+
const b = points[i + 1];
|
|
185
|
+
const clipped = clipSegment(a.x, a.y, b.x, b.y, rect);
|
|
186
|
+
if (!clipped) {
|
|
187
|
+
if (current.length > 1) segments.push(current);
|
|
188
|
+
current = [];
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (current.length === 0) {
|
|
192
|
+
current.push({ x: clipped.x0, y: clipped.y0 });
|
|
193
|
+
} else {
|
|
194
|
+
const last = current[current.length - 1];
|
|
195
|
+
if (Math.abs(last.x - clipped.x0) > 0.01 || Math.abs(last.y - clipped.y0) > 0.01) {
|
|
196
|
+
if (current.length > 1) segments.push(current);
|
|
197
|
+
current = [{ x: clipped.x0, y: clipped.y0 }];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
current.push({ x: clipped.x1, y: clipped.y1 });
|
|
201
|
+
}
|
|
202
|
+
if (current.length > 1) segments.push(current);
|
|
203
|
+
return segments;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/layout.ts
|
|
207
|
+
function columnWidthsPt(model, layout) {
|
|
208
|
+
const columns = model.columns.length > 0 ? model.columns : [{ id: "name", title: "Task" }];
|
|
209
|
+
const naturalTotal = columns.reduce((sum, c) => sum + (c.width ?? 120), 0) || 1;
|
|
210
|
+
return columns.map((column) => ({
|
|
211
|
+
column,
|
|
212
|
+
widthPt: (column.width ?? 120) / naturalTotal * layout.gridPanelWidthPt
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
function cellText(column, task, depth) {
|
|
216
|
+
if (!task) return "";
|
|
217
|
+
if (column.accessor) {
|
|
218
|
+
const v = column.accessor(task);
|
|
219
|
+
return v === null || v === void 0 ? "" : String(v);
|
|
220
|
+
}
|
|
221
|
+
const indent = " ".repeat(Math.max(0, depth));
|
|
222
|
+
return `${indent}${task.name}`;
|
|
223
|
+
}
|
|
224
|
+
function buildPageContent(model, tasks, layout, page) {
|
|
225
|
+
const { scale, gridPanelWidthPt, headerHeightPt, pageWidthPt, pageHeightPt, margins } = layout;
|
|
226
|
+
const theme = model.theme;
|
|
227
|
+
const tasksById = new Map(tasks.map((t) => [t.id, t]));
|
|
228
|
+
const barsByTaskId = new Map(model.bars.map((b) => [b.taskId, b]));
|
|
229
|
+
const rowsByTaskId = new Map(model.rows.map((r) => [r.taskId, r]));
|
|
230
|
+
const commands = [];
|
|
231
|
+
const contentX0 = margins.left;
|
|
232
|
+
const contentY0 = margins.top;
|
|
233
|
+
const timelineRect = {
|
|
234
|
+
x0: page.colStartX,
|
|
235
|
+
y0: page.rowStartY,
|
|
236
|
+
x1: page.colEndX,
|
|
237
|
+
y1: page.rowEndY
|
|
238
|
+
};
|
|
239
|
+
commands.push({ kind: "rect", x: 0, y: 0, w: pageWidthPt, h: pageHeightPt, fill: theme.backgroundColor });
|
|
240
|
+
const cols = columnWidthsPt(model, layout);
|
|
241
|
+
let colCursor = contentX0;
|
|
242
|
+
for (const { column, widthPt } of cols) {
|
|
243
|
+
commands.push({
|
|
244
|
+
kind: "text",
|
|
245
|
+
x: colCursor + 4,
|
|
246
|
+
y: contentY0 + headerHeightPt / 2 - 5,
|
|
247
|
+
w: widthPt - 8,
|
|
248
|
+
text: column.title,
|
|
249
|
+
size: 9,
|
|
250
|
+
color: theme.textColor,
|
|
251
|
+
bold: true,
|
|
252
|
+
align: column.align ?? "left"
|
|
253
|
+
});
|
|
254
|
+
colCursor += widthPt;
|
|
255
|
+
}
|
|
256
|
+
commands.push({
|
|
257
|
+
kind: "rect",
|
|
258
|
+
x: contentX0,
|
|
259
|
+
y: contentY0,
|
|
260
|
+
w: gridPanelWidthPt + (page.colEndX - page.colStartX) * scale,
|
|
261
|
+
h: headerHeightPt,
|
|
262
|
+
stroke: theme.gridColor
|
|
263
|
+
});
|
|
264
|
+
const timelineOriginX = contentX0 + gridPanelWidthPt;
|
|
265
|
+
for (const tick of model.ticks) {
|
|
266
|
+
if (tick.x < page.colStartX || tick.x > page.colEndX) continue;
|
|
267
|
+
const px = timelineOriginX + (tick.x - page.colStartX) * scale;
|
|
268
|
+
if (tick.isWeekend) {
|
|
269
|
+
}
|
|
270
|
+
commands.push({
|
|
271
|
+
kind: "line",
|
|
272
|
+
points: [
|
|
273
|
+
{ x: px, y: contentY0 },
|
|
274
|
+
{ x: px, y: pageHeightPt - margins.bottom }
|
|
275
|
+
],
|
|
276
|
+
stroke: tick.isToday ? theme.todayColor : theme.gridColor
|
|
277
|
+
});
|
|
278
|
+
commands.push({
|
|
279
|
+
kind: "text",
|
|
280
|
+
x: px + 2,
|
|
281
|
+
y: contentY0 + headerHeightPt / 2 - 5,
|
|
282
|
+
text: tick.label,
|
|
283
|
+
size: 7,
|
|
284
|
+
color: theme.textColor,
|
|
285
|
+
align: "left"
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const rowTop = (y) => contentY0 + headerHeightPt + (y - page.rowStartY) * scale;
|
|
289
|
+
for (const row of model.rows) {
|
|
290
|
+
if (row.y + row.height <= page.rowStartY || row.y >= page.rowEndY) continue;
|
|
291
|
+
const clippedRowY0 = Math.max(row.y, page.rowStartY);
|
|
292
|
+
const clippedRowY1 = Math.min(row.y + row.height, page.rowEndY);
|
|
293
|
+
const rowYTop = rowTop(clippedRowY0);
|
|
294
|
+
const rowHPt = (clippedRowY1 - clippedRowY0) * scale;
|
|
295
|
+
const task = tasksById.get(row.taskId);
|
|
296
|
+
let cx = contentX0;
|
|
297
|
+
for (const { column, widthPt } of cols) {
|
|
298
|
+
commands.push({
|
|
299
|
+
kind: "text",
|
|
300
|
+
x: cx + 4,
|
|
301
|
+
y: rowYTop + rowHPt / 2 - 4,
|
|
302
|
+
w: widthPt - 8,
|
|
303
|
+
text: cellText(column, task, row.depth),
|
|
304
|
+
size: 8,
|
|
305
|
+
color: theme.textColor,
|
|
306
|
+
align: column.align ?? "left"
|
|
307
|
+
});
|
|
308
|
+
cx += widthPt;
|
|
309
|
+
}
|
|
310
|
+
commands.push({
|
|
311
|
+
kind: "rect",
|
|
312
|
+
x: contentX0,
|
|
313
|
+
y: rowYTop,
|
|
314
|
+
w: gridPanelWidthPt + (page.colEndX - page.colStartX) * scale,
|
|
315
|
+
h: rowHPt,
|
|
316
|
+
stroke: theme.gridColor
|
|
317
|
+
});
|
|
318
|
+
const bar = barsByTaskId.get(row.taskId);
|
|
319
|
+
if (bar) {
|
|
320
|
+
const clippedBar = clipRect(bar.x, bar.y, bar.width, bar.height, timelineRect);
|
|
321
|
+
if (clippedBar) {
|
|
322
|
+
const bx = timelineOriginX + (clippedBar.x - page.colStartX) * scale;
|
|
323
|
+
const by = rowTop(clippedBar.y);
|
|
324
|
+
const bw = clippedBar.w * scale;
|
|
325
|
+
const bh = clippedBar.h * scale;
|
|
326
|
+
if (bar.baseline) {
|
|
327
|
+
const clippedBaseline = clipRect(bar.baseline.x, bar.y, bar.baseline.width, bar.height, timelineRect);
|
|
328
|
+
if (clippedBaseline) {
|
|
329
|
+
commands.push({
|
|
330
|
+
kind: "rect",
|
|
331
|
+
x: timelineOriginX + (clippedBaseline.x - page.colStartX) * scale,
|
|
332
|
+
y: rowTop(clippedBaseline.y),
|
|
333
|
+
w: clippedBaseline.w * scale,
|
|
334
|
+
h: clippedBaseline.h * scale,
|
|
335
|
+
fill: theme.baselineColor
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (bar.isMilestone) {
|
|
340
|
+
const r = bh / 2;
|
|
341
|
+
commands.push({
|
|
342
|
+
kind: "diamond",
|
|
343
|
+
cx: bx + r,
|
|
344
|
+
cy: by + r,
|
|
345
|
+
r,
|
|
346
|
+
fill: bar.isCritical ? theme.criticalColor : bar.color,
|
|
347
|
+
stroke: theme.textColor
|
|
348
|
+
});
|
|
349
|
+
if (bar.label) {
|
|
350
|
+
commands.push({
|
|
351
|
+
kind: "text",
|
|
352
|
+
x: bx + bh + 4,
|
|
353
|
+
y: by + bh / 2 - 4,
|
|
354
|
+
text: bar.label,
|
|
355
|
+
size: 8,
|
|
356
|
+
color: theme.textColor
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
commands.push({
|
|
361
|
+
kind: "rect",
|
|
362
|
+
x: bx,
|
|
363
|
+
y: by,
|
|
364
|
+
w: bw,
|
|
365
|
+
h: bh,
|
|
366
|
+
fill: bar.isCritical ? theme.criticalColor : bar.color
|
|
367
|
+
});
|
|
368
|
+
if (bar.progressWidth > 0) {
|
|
369
|
+
const clippedProgress = clipRect(bar.x, bar.y, bar.progressWidth, bar.height, timelineRect);
|
|
370
|
+
if (clippedProgress) {
|
|
371
|
+
commands.push({
|
|
372
|
+
kind: "rect",
|
|
373
|
+
x: timelineOriginX + (clippedProgress.x - page.colStartX) * scale,
|
|
374
|
+
y: rowTop(clippedProgress.y),
|
|
375
|
+
w: clippedProgress.w * scale,
|
|
376
|
+
h: clippedProgress.h * scale,
|
|
377
|
+
fill: bar.progressColor
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
commands.push({
|
|
382
|
+
kind: "text",
|
|
383
|
+
x: bx + 3,
|
|
384
|
+
y: by + bh / 2 - 4,
|
|
385
|
+
text: bar.label,
|
|
386
|
+
size: 8,
|
|
387
|
+
color: theme.textColor
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
for (const link of model.links) {
|
|
394
|
+
const fromBar = barsByTaskId.get(link.fromId);
|
|
395
|
+
const toBar = barsByTaskId.get(link.toId);
|
|
396
|
+
if (!fromBar || !toBar) continue;
|
|
397
|
+
const fromRow = rowsByTaskId.get(link.fromId);
|
|
398
|
+
const toRow = rowsByTaskId.get(link.toId);
|
|
399
|
+
if (!fromRow || !toRow) continue;
|
|
400
|
+
const polyline = computeLinkPolyline(fromBar, toBar);
|
|
401
|
+
const pieces = clipPolylineToRect(polyline, timelineRect);
|
|
402
|
+
for (const piece of pieces) {
|
|
403
|
+
commands.push({
|
|
404
|
+
kind: "line",
|
|
405
|
+
points: piece.map((p) => ({
|
|
406
|
+
x: timelineOriginX + (p.x - page.colStartX) * scale,
|
|
407
|
+
y: rowTop(p.y)
|
|
408
|
+
})),
|
|
409
|
+
stroke: link.isCritical ? theme.criticalColor : theme.linkColor
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return { widthPt: pageWidthPt, heightPt: pageHeightPt, commands };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/pdf/writer.ts
|
|
417
|
+
var encoder = new TextEncoder();
|
|
418
|
+
function encodeLatin1(str) {
|
|
419
|
+
const out = new Uint8Array(str.length);
|
|
420
|
+
for (let i = 0; i < str.length; i++) {
|
|
421
|
+
const code = str.charCodeAt(i);
|
|
422
|
+
out[i] = code <= 255 ? code : 63;
|
|
423
|
+
}
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
function escapePdfText(str) {
|
|
427
|
+
return str.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
|
|
428
|
+
}
|
|
429
|
+
function xrefLine(offset, generation, type) {
|
|
430
|
+
const line = `${String(offset).padStart(10, "0")} ${String(generation).padStart(5, "0")} ${type}
|
|
431
|
+
`;
|
|
432
|
+
return line;
|
|
433
|
+
}
|
|
434
|
+
var PdfBuilder = class {
|
|
435
|
+
constructor() {
|
|
436
|
+
this.parts = [];
|
|
437
|
+
this.length = 0;
|
|
438
|
+
this.offsetsById = /* @__PURE__ */ new Map();
|
|
439
|
+
this.nextId = 1;
|
|
440
|
+
}
|
|
441
|
+
reserveId() {
|
|
442
|
+
return this.nextId++;
|
|
443
|
+
}
|
|
444
|
+
raw(bytes) {
|
|
445
|
+
this.parts.push(bytes);
|
|
446
|
+
this.length += bytes.length;
|
|
447
|
+
}
|
|
448
|
+
text(s) {
|
|
449
|
+
this.raw(encoder.encode(s));
|
|
450
|
+
}
|
|
451
|
+
writeHeader() {
|
|
452
|
+
this.text("%PDF-1.4\n");
|
|
453
|
+
this.raw(new Uint8Array([37, 226, 227, 207, 211, 10]));
|
|
454
|
+
}
|
|
455
|
+
/** Append a simple (non-stream) indirect object, e.g. a dictionary or array. */
|
|
456
|
+
addObject(id, body) {
|
|
457
|
+
this.offsetsById.set(id, this.length);
|
|
458
|
+
this.text(`${id} 0 obj
|
|
459
|
+
${body}
|
|
460
|
+
endobj
|
|
461
|
+
`);
|
|
462
|
+
}
|
|
463
|
+
/** Append a stream object; `dictExtra` is the dictionary content besides `/Length`. */
|
|
464
|
+
addStreamObject(id, dictExtra, data) {
|
|
465
|
+
this.offsetsById.set(id, this.length);
|
|
466
|
+
this.text(`${id} 0 obj
|
|
467
|
+
<< ${dictExtra} /Length ${data.length} >>
|
|
468
|
+
stream
|
|
469
|
+
`);
|
|
470
|
+
this.raw(data);
|
|
471
|
+
this.text("\nendstream\nendobj\n");
|
|
472
|
+
}
|
|
473
|
+
/** Finalize the file: write the xref table, trailer, and startxref/%%EOF footer. */
|
|
474
|
+
build(rootId) {
|
|
475
|
+
const xrefOffset = this.length;
|
|
476
|
+
const maxId = this.nextId - 1;
|
|
477
|
+
let xref = `xref
|
|
478
|
+
0 ${maxId + 1}
|
|
479
|
+
`;
|
|
480
|
+
xref += xrefLine(0, 65535, "f");
|
|
481
|
+
for (let id = 1; id <= maxId; id++) {
|
|
482
|
+
const off = this.offsetsById.get(id);
|
|
483
|
+
if (off === void 0) {
|
|
484
|
+
throw new Error(`PdfBuilder: object ${id} was reserved but never written`);
|
|
485
|
+
}
|
|
486
|
+
xref += xrefLine(off, 0, "n");
|
|
487
|
+
}
|
|
488
|
+
this.text(xref);
|
|
489
|
+
this.text(`trailer
|
|
490
|
+
<< /Size ${maxId + 1} /Root ${rootId} 0 R >>
|
|
491
|
+
startxref
|
|
492
|
+
${xrefOffset}
|
|
493
|
+
%%EOF`);
|
|
494
|
+
const total = new Uint8Array(this.length);
|
|
495
|
+
let o = 0;
|
|
496
|
+
for (const p of this.parts) {
|
|
497
|
+
total.set(p, o);
|
|
498
|
+
o += p.length;
|
|
499
|
+
}
|
|
500
|
+
return total;
|
|
501
|
+
}
|
|
502
|
+
/** Byte offset an object was (or will be) written at — exposed for tests. */
|
|
503
|
+
getOffset(id) {
|
|
504
|
+
return this.offsetsById.get(id);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// src/color.ts
|
|
509
|
+
function parseColor(input) {
|
|
510
|
+
if (!input) return [0, 0, 0];
|
|
511
|
+
const s = input.trim();
|
|
512
|
+
const hex = s.match(/^#?([0-9a-fA-F]{6})$/) ?? s.match(/^#?([0-9a-fA-F]{3})$/);
|
|
513
|
+
if (hex) {
|
|
514
|
+
let h = hex[1];
|
|
515
|
+
if (h.length === 3) {
|
|
516
|
+
h = h.split("").map((c) => c + c).join("");
|
|
517
|
+
}
|
|
518
|
+
const r = parseInt(h.slice(0, 2), 16) / 255;
|
|
519
|
+
const g = parseInt(h.slice(2, 4), 16) / 255;
|
|
520
|
+
const b = parseInt(h.slice(4, 6), 16) / 255;
|
|
521
|
+
return [r, g, b];
|
|
522
|
+
}
|
|
523
|
+
const rgb = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i);
|
|
524
|
+
if (rgb) {
|
|
525
|
+
return [Number(rgb[1]) / 255, Number(rgb[2]) / 255, Number(rgb[3]) / 255];
|
|
526
|
+
}
|
|
527
|
+
const named = {
|
|
528
|
+
black: [0, 0, 0],
|
|
529
|
+
white: [1, 1, 1],
|
|
530
|
+
red: [1, 0, 0],
|
|
531
|
+
green: [0, 0.5, 0],
|
|
532
|
+
blue: [0, 0, 1],
|
|
533
|
+
gray: [0.5, 0.5, 0.5],
|
|
534
|
+
grey: [0.5, 0.5, 0.5],
|
|
535
|
+
transparent: [1, 1, 1]
|
|
536
|
+
};
|
|
537
|
+
return named[s.toLowerCase()] ?? [0, 0, 0];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/pdf/content.ts
|
|
541
|
+
function num(n) {
|
|
542
|
+
const r = Math.round(n * 1e3) / 1e3;
|
|
543
|
+
return Object.is(r, -0) ? "0" : String(r);
|
|
544
|
+
}
|
|
545
|
+
function colorOp(color, op) {
|
|
546
|
+
const [r, g, b] = parseColor(color);
|
|
547
|
+
return `${num(r)} ${num(g)} ${num(b)} ${op}
|
|
548
|
+
`;
|
|
549
|
+
}
|
|
550
|
+
function buildContentStream(commands, pageHeightPt) {
|
|
551
|
+
let s = "q\n";
|
|
552
|
+
for (const cmd of commands) {
|
|
553
|
+
switch (cmd.kind) {
|
|
554
|
+
case "rect": {
|
|
555
|
+
const x = cmd.x;
|
|
556
|
+
const yTop = pageHeightPt - (cmd.y + cmd.h);
|
|
557
|
+
if (cmd.fill) s += colorOp(cmd.fill, "rg");
|
|
558
|
+
if (cmd.stroke) s += colorOp(cmd.stroke, "RG");
|
|
559
|
+
s += `${num(x)} ${num(yTop)} ${num(cmd.w)} ${num(cmd.h)} re
|
|
560
|
+
`;
|
|
561
|
+
if (cmd.fill && cmd.stroke) s += "B\n";
|
|
562
|
+
else if (cmd.fill) s += "f\n";
|
|
563
|
+
else if (cmd.stroke) s += "S\n";
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case "diamond": {
|
|
567
|
+
const pts = [
|
|
568
|
+
{ x: cmd.cx, y: cmd.cy - cmd.r },
|
|
569
|
+
{ x: cmd.cx + cmd.r, y: cmd.cy },
|
|
570
|
+
{ x: cmd.cx, y: cmd.cy + cmd.r },
|
|
571
|
+
{ x: cmd.cx - cmd.r, y: cmd.cy }
|
|
572
|
+
].map((p) => ({ x: p.x, y: pageHeightPt - p.y }));
|
|
573
|
+
if (cmd.fill) s += colorOp(cmd.fill, "rg");
|
|
574
|
+
if (cmd.stroke) s += colorOp(cmd.stroke, "RG");
|
|
575
|
+
s += `${num(pts[0].x)} ${num(pts[0].y)} m
|
|
576
|
+
`;
|
|
577
|
+
for (let i = 1; i < pts.length; i++) s += `${num(pts[i].x)} ${num(pts[i].y)} l
|
|
578
|
+
`;
|
|
579
|
+
s += "h\n";
|
|
580
|
+
if (cmd.fill && cmd.stroke) s += "B\n";
|
|
581
|
+
else if (cmd.fill) s += "f\n";
|
|
582
|
+
else if (cmd.stroke) s += "S\n";
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
case "line": {
|
|
586
|
+
if (cmd.points.length < 2) break;
|
|
587
|
+
s += colorOp(cmd.stroke, "RG");
|
|
588
|
+
const p0 = cmd.points[0];
|
|
589
|
+
s += `${num(p0.x)} ${num(pageHeightPt - p0.y)} m
|
|
590
|
+
`;
|
|
591
|
+
for (let i = 1; i < cmd.points.length; i++) {
|
|
592
|
+
const p = cmd.points[i];
|
|
593
|
+
s += `${num(p.x)} ${num(pageHeightPt - p.y)} l
|
|
594
|
+
`;
|
|
595
|
+
}
|
|
596
|
+
s += "S\n";
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
case "text": {
|
|
600
|
+
if (!cmd.text) break;
|
|
601
|
+
const font = cmd.bold ? "/F2" : "/F1";
|
|
602
|
+
const baselineY = pageHeightPt - (cmd.y + cmd.size * 0.8);
|
|
603
|
+
s += colorOp(cmd.color, "rg");
|
|
604
|
+
s += "BT\n";
|
|
605
|
+
s += `${font} ${num(cmd.size)} Tf
|
|
606
|
+
`;
|
|
607
|
+
s += `${num(cmd.x)} ${num(baselineY)} Td
|
|
608
|
+
`;
|
|
609
|
+
s += `(${escapePdfText(latin1Safe(cmd.text))}) Tj
|
|
610
|
+
`;
|
|
611
|
+
s += "ET\n";
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
s += "Q\n";
|
|
617
|
+
return encodeLatin1(s);
|
|
618
|
+
}
|
|
619
|
+
function latin1Safe(s) {
|
|
620
|
+
let out = "";
|
|
621
|
+
for (const ch of s) {
|
|
622
|
+
const code = ch.codePointAt(0);
|
|
623
|
+
out += code <= 255 ? ch : "?";
|
|
624
|
+
}
|
|
625
|
+
return out;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/pdf/index.ts
|
|
629
|
+
function renderToPDF(model, tasks, options = {}) {
|
|
630
|
+
const layout = computePagination(model, options);
|
|
631
|
+
const pdf = new PdfBuilder();
|
|
632
|
+
pdf.writeHeader();
|
|
633
|
+
const catalogId = pdf.reserveId();
|
|
634
|
+
const pagesId = pdf.reserveId();
|
|
635
|
+
const helveticaId = pdf.reserveId();
|
|
636
|
+
const helveticaBoldId = pdf.reserveId();
|
|
637
|
+
pdf.addObject(helveticaId, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>");
|
|
638
|
+
pdf.addObject(
|
|
639
|
+
helveticaBoldId,
|
|
640
|
+
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>"
|
|
641
|
+
);
|
|
642
|
+
const pageIds = [];
|
|
643
|
+
for (const page of layout.pages) {
|
|
644
|
+
const content = buildPageContent(model, tasks, layout, page);
|
|
645
|
+
const streamBytes = buildContentStream(content.commands, layout.pageHeightPt);
|
|
646
|
+
const pageId = pdf.reserveId();
|
|
647
|
+
const contentId = pdf.reserveId();
|
|
648
|
+
pdf.addStreamObject(contentId, "", streamBytes);
|
|
649
|
+
pdf.addObject(
|
|
650
|
+
pageId,
|
|
651
|
+
`<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 ${num2(layout.pageWidthPt)} ${num2(layout.pageHeightPt)}] /Resources << /Font << /F1 ${helveticaId} 0 R /F2 ${helveticaBoldId} 0 R >> >> /Contents ${contentId} 0 R >>`
|
|
652
|
+
);
|
|
653
|
+
pageIds.push(pageId);
|
|
654
|
+
}
|
|
655
|
+
pdf.addObject(
|
|
656
|
+
pagesId,
|
|
657
|
+
`<< /Type /Pages /Kids [${pageIds.map((id) => `${id} 0 R`).join(" ")}] /Count ${pageIds.length} >>`
|
|
658
|
+
);
|
|
659
|
+
pdf.addObject(catalogId, `<< /Type /Catalog /Pages ${pagesId} 0 R >>`);
|
|
660
|
+
return pdf.build(catalogId);
|
|
661
|
+
}
|
|
662
|
+
function num2(n) {
|
|
663
|
+
const r = Math.round(n * 1e3) / 1e3;
|
|
664
|
+
return Object.is(r, -0) ? "0" : String(r);
|
|
665
|
+
}
|
|
666
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
667
|
+
0 && (module.exports = {
|
|
668
|
+
computePagination,
|
|
669
|
+
renderToPDF
|
|
670
|
+
});
|
|
671
|
+
//# sourceMappingURL=pdf.cjs.map
|