@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.
@@ -0,0 +1,229 @@
1
+ import {
2
+ buildPageContent,
3
+ computePagination,
4
+ parseColor
5
+ } from "./chunk-TYNGUB4V.js";
6
+
7
+ // src/pdf/writer.ts
8
+ var encoder = new TextEncoder();
9
+ function encodeLatin1(str) {
10
+ const out = new Uint8Array(str.length);
11
+ for (let i = 0; i < str.length; i++) {
12
+ const code = str.charCodeAt(i);
13
+ out[i] = code <= 255 ? code : 63;
14
+ }
15
+ return out;
16
+ }
17
+ function escapePdfText(str) {
18
+ return str.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
19
+ }
20
+ function xrefLine(offset, generation, type) {
21
+ const line = `${String(offset).padStart(10, "0")} ${String(generation).padStart(5, "0")} ${type}
22
+ `;
23
+ return line;
24
+ }
25
+ var PdfBuilder = class {
26
+ constructor() {
27
+ this.parts = [];
28
+ this.length = 0;
29
+ this.offsetsById = /* @__PURE__ */ new Map();
30
+ this.nextId = 1;
31
+ }
32
+ reserveId() {
33
+ return this.nextId++;
34
+ }
35
+ raw(bytes) {
36
+ this.parts.push(bytes);
37
+ this.length += bytes.length;
38
+ }
39
+ text(s) {
40
+ this.raw(encoder.encode(s));
41
+ }
42
+ writeHeader() {
43
+ this.text("%PDF-1.4\n");
44
+ this.raw(new Uint8Array([37, 226, 227, 207, 211, 10]));
45
+ }
46
+ /** Append a simple (non-stream) indirect object, e.g. a dictionary or array. */
47
+ addObject(id, body) {
48
+ this.offsetsById.set(id, this.length);
49
+ this.text(`${id} 0 obj
50
+ ${body}
51
+ endobj
52
+ `);
53
+ }
54
+ /** Append a stream object; `dictExtra` is the dictionary content besides `/Length`. */
55
+ addStreamObject(id, dictExtra, data) {
56
+ this.offsetsById.set(id, this.length);
57
+ this.text(`${id} 0 obj
58
+ << ${dictExtra} /Length ${data.length} >>
59
+ stream
60
+ `);
61
+ this.raw(data);
62
+ this.text("\nendstream\nendobj\n");
63
+ }
64
+ /** Finalize the file: write the xref table, trailer, and startxref/%%EOF footer. */
65
+ build(rootId) {
66
+ const xrefOffset = this.length;
67
+ const maxId = this.nextId - 1;
68
+ let xref = `xref
69
+ 0 ${maxId + 1}
70
+ `;
71
+ xref += xrefLine(0, 65535, "f");
72
+ for (let id = 1; id <= maxId; id++) {
73
+ const off = this.offsetsById.get(id);
74
+ if (off === void 0) {
75
+ throw new Error(`PdfBuilder: object ${id} was reserved but never written`);
76
+ }
77
+ xref += xrefLine(off, 0, "n");
78
+ }
79
+ this.text(xref);
80
+ this.text(`trailer
81
+ << /Size ${maxId + 1} /Root ${rootId} 0 R >>
82
+ startxref
83
+ ${xrefOffset}
84
+ %%EOF`);
85
+ const total = new Uint8Array(this.length);
86
+ let o = 0;
87
+ for (const p of this.parts) {
88
+ total.set(p, o);
89
+ o += p.length;
90
+ }
91
+ return total;
92
+ }
93
+ /** Byte offset an object was (or will be) written at — exposed for tests. */
94
+ getOffset(id) {
95
+ return this.offsetsById.get(id);
96
+ }
97
+ };
98
+
99
+ // src/pdf/content.ts
100
+ function num(n) {
101
+ const r = Math.round(n * 1e3) / 1e3;
102
+ return Object.is(r, -0) ? "0" : String(r);
103
+ }
104
+ function colorOp(color, op) {
105
+ const [r, g, b] = parseColor(color);
106
+ return `${num(r)} ${num(g)} ${num(b)} ${op}
107
+ `;
108
+ }
109
+ function buildContentStream(commands, pageHeightPt) {
110
+ let s = "q\n";
111
+ for (const cmd of commands) {
112
+ switch (cmd.kind) {
113
+ case "rect": {
114
+ const x = cmd.x;
115
+ const yTop = pageHeightPt - (cmd.y + cmd.h);
116
+ if (cmd.fill) s += colorOp(cmd.fill, "rg");
117
+ if (cmd.stroke) s += colorOp(cmd.stroke, "RG");
118
+ s += `${num(x)} ${num(yTop)} ${num(cmd.w)} ${num(cmd.h)} re
119
+ `;
120
+ if (cmd.fill && cmd.stroke) s += "B\n";
121
+ else if (cmd.fill) s += "f\n";
122
+ else if (cmd.stroke) s += "S\n";
123
+ break;
124
+ }
125
+ case "diamond": {
126
+ const pts = [
127
+ { x: cmd.cx, y: cmd.cy - cmd.r },
128
+ { x: cmd.cx + cmd.r, y: cmd.cy },
129
+ { x: cmd.cx, y: cmd.cy + cmd.r },
130
+ { x: cmd.cx - cmd.r, y: cmd.cy }
131
+ ].map((p) => ({ x: p.x, y: pageHeightPt - p.y }));
132
+ if (cmd.fill) s += colorOp(cmd.fill, "rg");
133
+ if (cmd.stroke) s += colorOp(cmd.stroke, "RG");
134
+ s += `${num(pts[0].x)} ${num(pts[0].y)} m
135
+ `;
136
+ for (let i = 1; i < pts.length; i++) s += `${num(pts[i].x)} ${num(pts[i].y)} l
137
+ `;
138
+ s += "h\n";
139
+ if (cmd.fill && cmd.stroke) s += "B\n";
140
+ else if (cmd.fill) s += "f\n";
141
+ else if (cmd.stroke) s += "S\n";
142
+ break;
143
+ }
144
+ case "line": {
145
+ if (cmd.points.length < 2) break;
146
+ s += colorOp(cmd.stroke, "RG");
147
+ const p0 = cmd.points[0];
148
+ s += `${num(p0.x)} ${num(pageHeightPt - p0.y)} m
149
+ `;
150
+ for (let i = 1; i < cmd.points.length; i++) {
151
+ const p = cmd.points[i];
152
+ s += `${num(p.x)} ${num(pageHeightPt - p.y)} l
153
+ `;
154
+ }
155
+ s += "S\n";
156
+ break;
157
+ }
158
+ case "text": {
159
+ if (!cmd.text) break;
160
+ const font = cmd.bold ? "/F2" : "/F1";
161
+ const baselineY = pageHeightPt - (cmd.y + cmd.size * 0.8);
162
+ s += colorOp(cmd.color, "rg");
163
+ s += "BT\n";
164
+ s += `${font} ${num(cmd.size)} Tf
165
+ `;
166
+ s += `${num(cmd.x)} ${num(baselineY)} Td
167
+ `;
168
+ s += `(${escapePdfText(latin1Safe(cmd.text))}) Tj
169
+ `;
170
+ s += "ET\n";
171
+ break;
172
+ }
173
+ }
174
+ }
175
+ s += "Q\n";
176
+ return encodeLatin1(s);
177
+ }
178
+ function latin1Safe(s) {
179
+ let out = "";
180
+ for (const ch of s) {
181
+ const code = ch.codePointAt(0);
182
+ out += code <= 255 ? ch : "?";
183
+ }
184
+ return out;
185
+ }
186
+
187
+ // src/pdf/index.ts
188
+ function renderToPDF(model, tasks, options = {}) {
189
+ const layout = computePagination(model, options);
190
+ const pdf = new PdfBuilder();
191
+ pdf.writeHeader();
192
+ const catalogId = pdf.reserveId();
193
+ const pagesId = pdf.reserveId();
194
+ const helveticaId = pdf.reserveId();
195
+ const helveticaBoldId = pdf.reserveId();
196
+ pdf.addObject(helveticaId, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>");
197
+ pdf.addObject(
198
+ helveticaBoldId,
199
+ "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>"
200
+ );
201
+ const pageIds = [];
202
+ for (const page of layout.pages) {
203
+ const content = buildPageContent(model, tasks, layout, page);
204
+ const streamBytes = buildContentStream(content.commands, layout.pageHeightPt);
205
+ const pageId = pdf.reserveId();
206
+ const contentId = pdf.reserveId();
207
+ pdf.addStreamObject(contentId, "", streamBytes);
208
+ pdf.addObject(
209
+ pageId,
210
+ `<< /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 >>`
211
+ );
212
+ pageIds.push(pageId);
213
+ }
214
+ pdf.addObject(
215
+ pagesId,
216
+ `<< /Type /Pages /Kids [${pageIds.map((id) => `${id} 0 R`).join(" ")}] /Count ${pageIds.length} >>`
217
+ );
218
+ pdf.addObject(catalogId, `<< /Type /Catalog /Pages ${pagesId} 0 R >>`);
219
+ return pdf.build(catalogId);
220
+ }
221
+ function num2(n) {
222
+ const r = Math.round(n * 1e3) / 1e3;
223
+ return Object.is(r, -0) ? "0" : String(r);
224
+ }
225
+
226
+ export {
227
+ renderToPDF
228
+ };
229
+ //# sourceMappingURL=chunk-QPE3ABUG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pdf/writer.ts","../src/pdf/content.ts","../src/pdf/index.ts"],"sourcesContent":["/**\n * Minimal from-scratch PDF object-model writer.\n *\n * Builds a PDF 1.4 file by appending raw bytes to a growing list of chunks\n * while recording the exact byte offset of every indirect object as it is\n * written, then emits a correct cross-reference (xref) table and trailer\n * pointing at those recorded offsets. No external PDF library is used.\n *\n * Object numbers may be reserved ahead of time (`reserveId`) and objects may\n * be appended to the byte stream in any order — the PDF spec only requires\n * the xref table's offsets to be correct, not that objects appear in\n * numeric order in the file body. This lets us, e.g., reserve the `/Pages`\n * object id before we know its `/Kids` array (which depends on how many\n * pages get created) and only serialize it once all pages are known.\n */\n\nconst encoder = new TextEncoder();\n\n/** Encode a JS string as single-byte Latin-1 (WinAnsi-compatible) bytes, replacing anything outside it with \"?\". */\nexport function encodeLatin1(str: string): Uint8Array {\n const out = new Uint8Array(str.length);\n for (let i = 0; i < str.length; i++) {\n const code = str.charCodeAt(i);\n out[i] = code <= 255 ? code : 0x3f; // \"?\"\n }\n return out;\n}\n\n/** Escape a string for use inside a PDF literal string, e.g. `(...)`. */\nexport function escapePdfText(str: string): string {\n return str.replace(/\\\\/g, \"\\\\\\\\\").replace(/\\(/g, \"\\\\(\").replace(/\\)/g, \"\\\\)\");\n}\n\n/** Build one xref subsection entry line — must be exactly 20 bytes. */\nfunction xrefLine(offset: number, generation: number, type: \"n\" | \"f\"): string {\n const line = `${String(offset).padStart(10, \"0\")} ${String(generation).padStart(5, \"0\")} ${type} \\n`;\n return line;\n}\n\nexport class PdfBuilder {\n private parts: Uint8Array[] = [];\n private length = 0;\n private offsetsById = new Map<number, number>();\n private nextId = 1;\n\n reserveId(): number {\n return this.nextId++;\n }\n\n private raw(bytes: Uint8Array): void {\n this.parts.push(bytes);\n this.length += bytes.length;\n }\n\n private text(s: string): void {\n this.raw(encoder.encode(s));\n }\n\n writeHeader(): void {\n this.text(\"%PDF-1.4\\n\");\n // Recommended binary-marker comment (bytes >= 0x80) so tools detect this\n // as a binary file rather than plain text.\n this.raw(new Uint8Array([0x25, 0xe2, 0xe3, 0xcf, 0xd3, 0x0a]));\n }\n\n /** Append a simple (non-stream) indirect object, e.g. a dictionary or array. */\n addObject(id: number, body: string): void {\n this.offsetsById.set(id, this.length);\n this.text(`${id} 0 obj\\n${body}\\nendobj\\n`);\n }\n\n /** Append a stream object; `dictExtra` is the dictionary content besides `/Length`. */\n addStreamObject(id: number, dictExtra: string, data: Uint8Array): void {\n this.offsetsById.set(id, this.length);\n this.text(`${id} 0 obj\\n<< ${dictExtra} /Length ${data.length} >>\\nstream\\n`);\n this.raw(data);\n this.text(\"\\nendstream\\nendobj\\n\");\n }\n\n /** Finalize the file: write the xref table, trailer, and startxref/%%EOF footer. */\n build(rootId: number): Uint8Array {\n const xrefOffset = this.length;\n const maxId = this.nextId - 1;\n let xref = `xref\\n0 ${maxId + 1}\\n`;\n xref += xrefLine(0, 65535, \"f\");\n for (let id = 1; id <= maxId; id++) {\n const off = this.offsetsById.get(id);\n if (off === undefined) {\n throw new Error(`PdfBuilder: object ${id} was reserved but never written`);\n }\n xref += xrefLine(off, 0, \"n\");\n }\n this.text(xref);\n this.text(`trailer\\n<< /Size ${maxId + 1} /Root ${rootId} 0 R >>\\nstartxref\\n${xrefOffset}\\n%%EOF`);\n\n const total = new Uint8Array(this.length);\n let o = 0;\n for (const p of this.parts) {\n total.set(p, o);\n o += p.length;\n }\n return total;\n }\n\n /** Byte offset an object was (or will be) written at — exposed for tests. */\n getOffset(id: number): number | undefined {\n return this.offsetsById.get(id);\n }\n}\n","import type { DrawCommand } from \"../layout.js\";\nimport { parseColor } from \"../color.js\";\nimport { encodeLatin1, escapePdfText } from \"./writer.js\";\n\nfunction num(n: number): string {\n // avoid -0 and excessive decimals in the content stream\n const r = Math.round(n * 1000) / 1000;\n return Object.is(r, -0) ? \"0\" : String(r);\n}\n\nfunction colorOp(color: string, op: \"rg\" | \"RG\"): string {\n const [r, g, b] = parseColor(color);\n return `${num(r)} ${num(g)} ${num(b)} ${op}\\n`;\n}\n\n/**\n * Render a page's format-agnostic draw commands into a PDF content stream.\n * PDF's coordinate system has its origin at the bottom-left with y\n * increasing upward, while our shared layout uses top-down coordinates —\n * so every emitted coordinate is flipped here: pdfY = pageHeightPt - y.\n */\nexport function buildContentStream(commands: DrawCommand[], pageHeightPt: number): Uint8Array {\n let s = \"q\\n\";\n for (const cmd of commands) {\n switch (cmd.kind) {\n case \"rect\": {\n const x = cmd.x;\n const yTop = pageHeightPt - (cmd.y + cmd.h);\n if (cmd.fill) s += colorOp(cmd.fill, \"rg\");\n if (cmd.stroke) s += colorOp(cmd.stroke, \"RG\");\n s += `${num(x)} ${num(yTop)} ${num(cmd.w)} ${num(cmd.h)} re\\n`;\n if (cmd.fill && cmd.stroke) s += \"B\\n\";\n else if (cmd.fill) s += \"f\\n\";\n else if (cmd.stroke) s += \"S\\n\";\n break;\n }\n case \"diamond\": {\n const pts = [\n { x: cmd.cx, y: cmd.cy - cmd.r },\n { x: cmd.cx + cmd.r, y: cmd.cy },\n { x: cmd.cx, y: cmd.cy + cmd.r },\n { x: cmd.cx - cmd.r, y: cmd.cy },\n ].map((p) => ({ x: p.x, y: pageHeightPt - p.y }));\n if (cmd.fill) s += colorOp(cmd.fill, \"rg\");\n if (cmd.stroke) s += colorOp(cmd.stroke, \"RG\");\n s += `${num(pts[0]!.x)} ${num(pts[0]!.y)} m\\n`;\n for (let i = 1; i < pts.length; i++) s += `${num(pts[i]!.x)} ${num(pts[i]!.y)} l\\n`;\n s += \"h\\n\";\n if (cmd.fill && cmd.stroke) s += \"B\\n\";\n else if (cmd.fill) s += \"f\\n\";\n else if (cmd.stroke) s += \"S\\n\";\n break;\n }\n case \"line\": {\n if (cmd.points.length < 2) break;\n s += colorOp(cmd.stroke, \"RG\");\n const p0 = cmd.points[0]!;\n s += `${num(p0.x)} ${num(pageHeightPt - p0.y)} m\\n`;\n for (let i = 1; i < cmd.points.length; i++) {\n const p = cmd.points[i]!;\n s += `${num(p.x)} ${num(pageHeightPt - p.y)} l\\n`;\n }\n s += \"S\\n\";\n break;\n }\n case \"text\": {\n if (!cmd.text) break;\n const font = cmd.bold ? \"/F2\" : \"/F1\";\n const baselineY = pageHeightPt - (cmd.y + cmd.size * 0.8);\n s += colorOp(cmd.color, \"rg\");\n s += \"BT\\n\";\n s += `${font} ${num(cmd.size)} Tf\\n`;\n s += `${num(cmd.x)} ${num(baselineY)} Td\\n`;\n s += `(${escapePdfText(latin1Safe(cmd.text))}) Tj\\n`;\n s += \"ET\\n\";\n break;\n }\n }\n }\n s += \"Q\\n\";\n return encodeLatin1(s);\n}\n\n// Text content (operator names, numbers, parens) is pure ASCII; only the\n// Tj string literal content can contain arbitrary task-label characters,\n// which are already reduced to Latin-1 by latin1Safe()/encodeLatin1 below.\nfunction latin1Safe(s: string): string {\n let out = \"\";\n for (const ch of s) {\n const code = ch.codePointAt(0)!;\n out += code <= 255 ? ch : \"?\";\n }\n return out;\n}\n","import type { GanttRenderModel, GanttTask } from \"@ganttloom/gantt-core\";\nimport { computePagination, type PaginationOptions } from \"../paginate.js\";\nimport { buildPageContent } from \"../layout.js\";\nimport { PdfBuilder } from \"./writer.js\";\nimport { buildContentStream } from \"./content.js\";\n\nexport type PdfExportOptions = PaginationOptions;\n\n/**\n * Render a GanttRenderModel to a native PDF file, built from scratch\n * (no external PDF library). Automatically paginates large charts across\n * multiple pages per `computePagination`.\n */\nexport function renderToPDF(\n model: GanttRenderModel,\n tasks: GanttTask[],\n options: PdfExportOptions = {},\n): Uint8Array {\n const layout = computePagination(model, options);\n const pdf = new PdfBuilder();\n pdf.writeHeader();\n\n const catalogId = pdf.reserveId();\n const pagesId = pdf.reserveId();\n const helveticaId = pdf.reserveId();\n const helveticaBoldId = pdf.reserveId();\n\n pdf.addObject(helveticaId, \"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\");\n pdf.addObject(\n helveticaBoldId,\n \"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>\",\n );\n\n const pageIds: number[] = [];\n\n for (const page of layout.pages) {\n const content = buildPageContent(model, tasks, layout, page);\n const streamBytes = buildContentStream(content.commands, layout.pageHeightPt);\n\n const pageId = pdf.reserveId();\n const contentId = pdf.reserveId();\n\n pdf.addStreamObject(contentId, \"\", streamBytes);\n pdf.addObject(\n pageId,\n `<< /Type /Page /Parent ${pagesId} 0 R ` +\n `/MediaBox [0 0 ${num(layout.pageWidthPt)} ${num(layout.pageHeightPt)}] ` +\n `/Resources << /Font << /F1 ${helveticaId} 0 R /F2 ${helveticaBoldId} 0 R >> >> ` +\n `/Contents ${contentId} 0 R >>`,\n );\n pageIds.push(pageId);\n }\n\n pdf.addObject(\n pagesId,\n `<< /Type /Pages /Kids [${pageIds.map((id) => `${id} 0 R`).join(\" \")}] /Count ${pageIds.length} >>`,\n );\n pdf.addObject(catalogId, `<< /Type /Catalog /Pages ${pagesId} 0 R >>`);\n\n return pdf.build(catalogId);\n}\n\nfunction num(n: number): string {\n const r = Math.round(n * 1000) / 1000;\n return Object.is(r, -0) ? \"0\" : String(r);\n}\n\nexport { computePagination } from \"../paginate.js\";\nexport type { PaginationLayout, PageBand, PageSizeName, Orientation, Margins } from \"../paginate.js\";\n"],"mappings":"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","names":["num"]}
@@ -0,0 +1,434 @@
1
+ // src/paginate.ts
2
+ var PAGE_SIZES_PT = {
3
+ // Values are the portrait (short-edge width x long-edge height) dimensions
4
+ // in PDF/typographic points (72pt = 1in).
5
+ A4: { width: 595.28, height: 841.89 },
6
+ Letter: { width: 612, height: 792 },
7
+ A3: { width: 841.89, height: 1190.55 }
8
+ };
9
+ var DEFAULT_MARGIN_PT = 36;
10
+ var DEFAULT_SCALE = 0.75;
11
+ var MIN_TIMELINE_WIDTH_PT = 50;
12
+ var MIN_ROWS_HEIGHT_PT = 20;
13
+ function resolvePageSize(opt) {
14
+ if (opt?.custom) {
15
+ return { width: opt.custom.widthPt, height: opt.custom.heightPt };
16
+ }
17
+ const name = opt?.name ?? "A4";
18
+ const base = PAGE_SIZES_PT[name];
19
+ const orientation = opt?.orientation ?? "landscape";
20
+ const short = Math.min(base.width, base.height);
21
+ const long = Math.max(base.width, base.height);
22
+ return orientation === "landscape" ? { width: long, height: short } : { width: short, height: long };
23
+ }
24
+ function defaultGridPanelWidthPx(model) {
25
+ const cols = model.columns ?? [];
26
+ if (cols.length === 0) return 160;
27
+ return cols.reduce((sum, c) => sum + (c.width ?? 120), 0);
28
+ }
29
+ function computePagination(model, options = {}) {
30
+ const { width: pageWidthPt, height: pageHeightPt } = resolvePageSize(options.pageSize);
31
+ const margins = {
32
+ top: options.margins?.top ?? DEFAULT_MARGIN_PT,
33
+ right: options.margins?.right ?? DEFAULT_MARGIN_PT,
34
+ bottom: options.margins?.bottom ?? DEFAULT_MARGIN_PT,
35
+ left: options.margins?.left ?? DEFAULT_MARGIN_PT
36
+ };
37
+ const scale = options.scale ?? DEFAULT_SCALE;
38
+ const gridPanelWidthPx = options.gridPanelWidthPx ?? defaultGridPanelWidthPx(model);
39
+ const gridPanelWidthPt = gridPanelWidthPx * scale;
40
+ const headerHeightPx = model.headerHeight;
41
+ const headerHeightPt = headerHeightPx * scale;
42
+ const contentWidthPt = pageWidthPt - margins.left - margins.right;
43
+ const contentHeightPt = pageHeightPt - margins.top - margins.bottom;
44
+ const timelineWidthPtPerPage = Math.max(contentWidthPt - gridPanelWidthPt, MIN_TIMELINE_WIDTH_PT);
45
+ const rowsHeightPtPerPage = Math.max(contentHeightPt - headerHeightPt, MIN_ROWS_HEIGHT_PT);
46
+ const timelineWidthPxPerPage = timelineWidthPtPerPage / scale;
47
+ const rowsHeightPxPerPage = rowsHeightPtPerPage / scale;
48
+ const totalHeightPx = Math.max(model.height, 1);
49
+ const totalWidthPx = Math.max(model.width, 1);
50
+ const rowCount = Math.max(1, Math.ceil(totalHeightPx / rowsHeightPxPerPage));
51
+ const colCount = Math.max(1, Math.ceil(totalWidthPx / timelineWidthPxPerPage));
52
+ const rowBands = [];
53
+ for (let i = 0; i < rowCount; i++) {
54
+ rowBands.push({
55
+ start: i * rowsHeightPxPerPage,
56
+ end: Math.min(totalHeightPx, (i + 1) * rowsHeightPxPerPage)
57
+ });
58
+ }
59
+ const colBands = [];
60
+ for (let i = 0; i < colCount; i++) {
61
+ colBands.push({
62
+ start: i * timelineWidthPxPerPage,
63
+ end: Math.min(totalWidthPx, (i + 1) * timelineWidthPxPerPage)
64
+ });
65
+ }
66
+ const pages = [];
67
+ for (let r = 0; r < rowCount; r++) {
68
+ const rowBand = rowBands[r];
69
+ for (let c = 0; c < colCount; c++) {
70
+ const colBand = colBands[c];
71
+ pages.push({
72
+ rowBand: r,
73
+ colBand: c,
74
+ rowStartY: rowBand.start,
75
+ rowEndY: rowBand.end,
76
+ colStartX: colBand.start,
77
+ colEndX: colBand.end
78
+ });
79
+ }
80
+ }
81
+ return {
82
+ pageWidthPt,
83
+ pageHeightPt,
84
+ margins,
85
+ scale,
86
+ gridPanelWidthPx,
87
+ gridPanelWidthPt,
88
+ headerHeightPx,
89
+ headerHeightPt,
90
+ timelineWidthPxPerPage,
91
+ rowsHeightPxPerPage,
92
+ rowBands,
93
+ colBands,
94
+ pages,
95
+ rowCount,
96
+ colCount
97
+ };
98
+ }
99
+ function clipSegment(x0, y0, x1, y1, rect) {
100
+ let t0 = 0;
101
+ let t1 = 1;
102
+ const dx = x1 - x0;
103
+ const dy = y1 - y0;
104
+ const checks = [
105
+ [-dx, x0 - rect.x0],
106
+ [dx, rect.x1 - x0],
107
+ [-dy, y0 - rect.y0],
108
+ [dy, rect.y1 - y0]
109
+ ];
110
+ for (const [p, q] of checks) {
111
+ if (p === 0) {
112
+ if (q < 0) return null;
113
+ } else {
114
+ const r = q / p;
115
+ if (p < 0) {
116
+ if (r > t1) return null;
117
+ if (r > t0) t0 = r;
118
+ } else {
119
+ if (r < t0) return null;
120
+ if (r < t1) t1 = r;
121
+ }
122
+ }
123
+ }
124
+ return { x0: x0 + t0 * dx, y0: y0 + t0 * dy, x1: x0 + t1 * dx, y1: y0 + t1 * dy };
125
+ }
126
+ function clipRect(x, y, w, h, rect) {
127
+ const x0 = Math.max(x, rect.x0);
128
+ const y0 = Math.max(y, rect.y0);
129
+ const x1 = Math.min(x + w, rect.x1);
130
+ const y1 = Math.min(y + h, rect.y1);
131
+ if (x1 <= x0 || y1 <= y0) return null;
132
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
133
+ }
134
+
135
+ // src/geom.ts
136
+ function computeLinkPolyline(fromBar, toBar) {
137
+ const x0 = fromBar.x + fromBar.width;
138
+ const y0 = fromBar.y + fromBar.height / 2;
139
+ const x1 = toBar.x;
140
+ const y1 = toBar.y + toBar.height / 2;
141
+ if (Math.abs(y1 - y0) < 0.01) {
142
+ return [{ x: x0, y: y0 }, { x: x1, y: y1 }];
143
+ }
144
+ const midX = x0 + (x1 - x0) / 2;
145
+ return [
146
+ { x: x0, y: y0 },
147
+ { x: midX, y: y0 },
148
+ { x: midX, y: y1 },
149
+ { x: x1, y: y1 }
150
+ ];
151
+ }
152
+ function clipPolylineToRect(points, rect) {
153
+ const segments = [];
154
+ let current = [];
155
+ for (let i = 0; i < points.length - 1; i++) {
156
+ const a = points[i];
157
+ const b = points[i + 1];
158
+ const clipped = clipSegment(a.x, a.y, b.x, b.y, rect);
159
+ if (!clipped) {
160
+ if (current.length > 1) segments.push(current);
161
+ current = [];
162
+ continue;
163
+ }
164
+ if (current.length === 0) {
165
+ current.push({ x: clipped.x0, y: clipped.y0 });
166
+ } else {
167
+ const last = current[current.length - 1];
168
+ if (Math.abs(last.x - clipped.x0) > 0.01 || Math.abs(last.y - clipped.y0) > 0.01) {
169
+ if (current.length > 1) segments.push(current);
170
+ current = [{ x: clipped.x0, y: clipped.y0 }];
171
+ }
172
+ }
173
+ current.push({ x: clipped.x1, y: clipped.y1 });
174
+ }
175
+ if (current.length > 1) segments.push(current);
176
+ return segments;
177
+ }
178
+
179
+ // src/layout.ts
180
+ function columnWidthsPt(model, layout) {
181
+ const columns = model.columns.length > 0 ? model.columns : [{ id: "name", title: "Task" }];
182
+ const naturalTotal = columns.reduce((sum, c) => sum + (c.width ?? 120), 0) || 1;
183
+ return columns.map((column) => ({
184
+ column,
185
+ widthPt: (column.width ?? 120) / naturalTotal * layout.gridPanelWidthPt
186
+ }));
187
+ }
188
+ function cellText(column, task, depth) {
189
+ if (!task) return "";
190
+ if (column.accessor) {
191
+ const v = column.accessor(task);
192
+ return v === null || v === void 0 ? "" : String(v);
193
+ }
194
+ const indent = " ".repeat(Math.max(0, depth));
195
+ return `${indent}${task.name}`;
196
+ }
197
+ function buildPageContent(model, tasks, layout, page) {
198
+ const { scale, gridPanelWidthPt, headerHeightPt, pageWidthPt, pageHeightPt, margins } = layout;
199
+ const theme = model.theme;
200
+ const tasksById = new Map(tasks.map((t) => [t.id, t]));
201
+ const barsByTaskId = new Map(model.bars.map((b) => [b.taskId, b]));
202
+ const rowsByTaskId = new Map(model.rows.map((r) => [r.taskId, r]));
203
+ const commands = [];
204
+ const contentX0 = margins.left;
205
+ const contentY0 = margins.top;
206
+ const timelineRect = {
207
+ x0: page.colStartX,
208
+ y0: page.rowStartY,
209
+ x1: page.colEndX,
210
+ y1: page.rowEndY
211
+ };
212
+ commands.push({ kind: "rect", x: 0, y: 0, w: pageWidthPt, h: pageHeightPt, fill: theme.backgroundColor });
213
+ const cols = columnWidthsPt(model, layout);
214
+ let colCursor = contentX0;
215
+ for (const { column, widthPt } of cols) {
216
+ commands.push({
217
+ kind: "text",
218
+ x: colCursor + 4,
219
+ y: contentY0 + headerHeightPt / 2 - 5,
220
+ w: widthPt - 8,
221
+ text: column.title,
222
+ size: 9,
223
+ color: theme.textColor,
224
+ bold: true,
225
+ align: column.align ?? "left"
226
+ });
227
+ colCursor += widthPt;
228
+ }
229
+ commands.push({
230
+ kind: "rect",
231
+ x: contentX0,
232
+ y: contentY0,
233
+ w: gridPanelWidthPt + (page.colEndX - page.colStartX) * scale,
234
+ h: headerHeightPt,
235
+ stroke: theme.gridColor
236
+ });
237
+ const timelineOriginX = contentX0 + gridPanelWidthPt;
238
+ for (const tick of model.ticks) {
239
+ if (tick.x < page.colStartX || tick.x > page.colEndX) continue;
240
+ const px = timelineOriginX + (tick.x - page.colStartX) * scale;
241
+ if (tick.isWeekend) {
242
+ }
243
+ commands.push({
244
+ kind: "line",
245
+ points: [
246
+ { x: px, y: contentY0 },
247
+ { x: px, y: pageHeightPt - margins.bottom }
248
+ ],
249
+ stroke: tick.isToday ? theme.todayColor : theme.gridColor
250
+ });
251
+ commands.push({
252
+ kind: "text",
253
+ x: px + 2,
254
+ y: contentY0 + headerHeightPt / 2 - 5,
255
+ text: tick.label,
256
+ size: 7,
257
+ color: theme.textColor,
258
+ align: "left"
259
+ });
260
+ }
261
+ const rowTop = (y) => contentY0 + headerHeightPt + (y - page.rowStartY) * scale;
262
+ for (const row of model.rows) {
263
+ if (row.y + row.height <= page.rowStartY || row.y >= page.rowEndY) continue;
264
+ const clippedRowY0 = Math.max(row.y, page.rowStartY);
265
+ const clippedRowY1 = Math.min(row.y + row.height, page.rowEndY);
266
+ const rowYTop = rowTop(clippedRowY0);
267
+ const rowHPt = (clippedRowY1 - clippedRowY0) * scale;
268
+ const task = tasksById.get(row.taskId);
269
+ let cx = contentX0;
270
+ for (const { column, widthPt } of cols) {
271
+ commands.push({
272
+ kind: "text",
273
+ x: cx + 4,
274
+ y: rowYTop + rowHPt / 2 - 4,
275
+ w: widthPt - 8,
276
+ text: cellText(column, task, row.depth),
277
+ size: 8,
278
+ color: theme.textColor,
279
+ align: column.align ?? "left"
280
+ });
281
+ cx += widthPt;
282
+ }
283
+ commands.push({
284
+ kind: "rect",
285
+ x: contentX0,
286
+ y: rowYTop,
287
+ w: gridPanelWidthPt + (page.colEndX - page.colStartX) * scale,
288
+ h: rowHPt,
289
+ stroke: theme.gridColor
290
+ });
291
+ const bar = barsByTaskId.get(row.taskId);
292
+ if (bar) {
293
+ const clippedBar = clipRect(bar.x, bar.y, bar.width, bar.height, timelineRect);
294
+ if (clippedBar) {
295
+ const bx = timelineOriginX + (clippedBar.x - page.colStartX) * scale;
296
+ const by = rowTop(clippedBar.y);
297
+ const bw = clippedBar.w * scale;
298
+ const bh = clippedBar.h * scale;
299
+ if (bar.baseline) {
300
+ const clippedBaseline = clipRect(bar.baseline.x, bar.y, bar.baseline.width, bar.height, timelineRect);
301
+ if (clippedBaseline) {
302
+ commands.push({
303
+ kind: "rect",
304
+ x: timelineOriginX + (clippedBaseline.x - page.colStartX) * scale,
305
+ y: rowTop(clippedBaseline.y),
306
+ w: clippedBaseline.w * scale,
307
+ h: clippedBaseline.h * scale,
308
+ fill: theme.baselineColor
309
+ });
310
+ }
311
+ }
312
+ if (bar.isMilestone) {
313
+ const r = bh / 2;
314
+ commands.push({
315
+ kind: "diamond",
316
+ cx: bx + r,
317
+ cy: by + r,
318
+ r,
319
+ fill: bar.isCritical ? theme.criticalColor : bar.color,
320
+ stroke: theme.textColor
321
+ });
322
+ if (bar.label) {
323
+ commands.push({
324
+ kind: "text",
325
+ x: bx + bh + 4,
326
+ y: by + bh / 2 - 4,
327
+ text: bar.label,
328
+ size: 8,
329
+ color: theme.textColor
330
+ });
331
+ }
332
+ } else {
333
+ commands.push({
334
+ kind: "rect",
335
+ x: bx,
336
+ y: by,
337
+ w: bw,
338
+ h: bh,
339
+ fill: bar.isCritical ? theme.criticalColor : bar.color
340
+ });
341
+ if (bar.progressWidth > 0) {
342
+ const clippedProgress = clipRect(bar.x, bar.y, bar.progressWidth, bar.height, timelineRect);
343
+ if (clippedProgress) {
344
+ commands.push({
345
+ kind: "rect",
346
+ x: timelineOriginX + (clippedProgress.x - page.colStartX) * scale,
347
+ y: rowTop(clippedProgress.y),
348
+ w: clippedProgress.w * scale,
349
+ h: clippedProgress.h * scale,
350
+ fill: bar.progressColor
351
+ });
352
+ }
353
+ }
354
+ commands.push({
355
+ kind: "text",
356
+ x: bx + 3,
357
+ y: by + bh / 2 - 4,
358
+ text: bar.label,
359
+ size: 8,
360
+ color: theme.textColor
361
+ });
362
+ }
363
+ }
364
+ }
365
+ }
366
+ for (const link of model.links) {
367
+ const fromBar = barsByTaskId.get(link.fromId);
368
+ const toBar = barsByTaskId.get(link.toId);
369
+ if (!fromBar || !toBar) continue;
370
+ const fromRow = rowsByTaskId.get(link.fromId);
371
+ const toRow = rowsByTaskId.get(link.toId);
372
+ if (!fromRow || !toRow) continue;
373
+ const polyline = computeLinkPolyline(fromBar, toBar);
374
+ const pieces = clipPolylineToRect(polyline, timelineRect);
375
+ for (const piece of pieces) {
376
+ commands.push({
377
+ kind: "line",
378
+ points: piece.map((p) => ({
379
+ x: timelineOriginX + (p.x - page.colStartX) * scale,
380
+ y: rowTop(p.y)
381
+ })),
382
+ stroke: link.isCritical ? theme.criticalColor : theme.linkColor
383
+ });
384
+ }
385
+ }
386
+ return { widthPt: pageWidthPt, heightPt: pageHeightPt, commands };
387
+ }
388
+
389
+ // src/color.ts
390
+ function parseColor(input) {
391
+ if (!input) return [0, 0, 0];
392
+ const s = input.trim();
393
+ const hex = s.match(/^#?([0-9a-fA-F]{6})$/) ?? s.match(/^#?([0-9a-fA-F]{3})$/);
394
+ if (hex) {
395
+ let h = hex[1];
396
+ if (h.length === 3) {
397
+ h = h.split("").map((c) => c + c).join("");
398
+ }
399
+ const r = parseInt(h.slice(0, 2), 16) / 255;
400
+ const g = parseInt(h.slice(2, 4), 16) / 255;
401
+ const b = parseInt(h.slice(4, 6), 16) / 255;
402
+ return [r, g, b];
403
+ }
404
+ const rgb = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i);
405
+ if (rgb) {
406
+ return [Number(rgb[1]) / 255, Number(rgb[2]) / 255, Number(rgb[3]) / 255];
407
+ }
408
+ const named = {
409
+ black: [0, 0, 0],
410
+ white: [1, 1, 1],
411
+ red: [1, 0, 0],
412
+ green: [0, 0.5, 0],
413
+ blue: [0, 0, 1],
414
+ gray: [0.5, 0.5, 0.5],
415
+ grey: [0.5, 0.5, 0.5],
416
+ transparent: [1, 1, 1]
417
+ };
418
+ return named[s.toLowerCase()] ?? [0, 0, 0];
419
+ }
420
+ function toHexByte(v) {
421
+ return Math.round(Math.max(0, Math.min(1, v)) * 255).toString(16).padStart(2, "0");
422
+ }
423
+ function toHexRRGGBB(input) {
424
+ const [r, g, b] = parseColor(input);
425
+ return `${toHexByte(r)}${toHexByte(g)}${toHexByte(b)}`.toUpperCase();
426
+ }
427
+
428
+ export {
429
+ computePagination,
430
+ buildPageContent,
431
+ parseColor,
432
+ toHexRRGGBB
433
+ };
434
+ //# sourceMappingURL=chunk-TYNGUB4V.js.map