plotlink-ows 1.0.33 → 1.2.94

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.
Files changed (145) hide show
  1. package/README.md +4 -0
  2. package/app/lib/agent-command.ts +85 -0
  3. package/app/lib/agent-readiness.ts +133 -0
  4. package/app/lib/apply-schema.ts +55 -0
  5. package/app/lib/bubble-text.ts +160 -0
  6. package/app/lib/cartoon-coach.ts +198 -0
  7. package/app/lib/cartoon-markdown.ts +83 -0
  8. package/app/lib/cartoon-prompt.ts +122 -0
  9. package/app/lib/cartoon-readiness.ts +811 -0
  10. package/app/lib/clean-image-sync.ts +245 -0
  11. package/app/lib/codex-images.ts +152 -0
  12. package/app/lib/cut-asset-diagnostics.ts +120 -0
  13. package/app/lib/cuts.ts +302 -0
  14. package/app/lib/fonts.ts +109 -0
  15. package/app/lib/generate-claude-md.ts +8 -1
  16. package/app/lib/generate-story-instructions.ts +731 -0
  17. package/app/lib/image-asset-validate.ts +123 -0
  18. package/app/lib/lettering-status.ts +133 -0
  19. package/app/lib/overlays.ts +637 -0
  20. package/app/lib/paths.ts +10 -0
  21. package/app/lib/public-title.ts +65 -0
  22. package/app/lib/publish.ts +16 -2
  23. package/app/lib/story-progress.ts +243 -0
  24. package/app/lib/terminal-protocol.ts +16 -0
  25. package/app/lib/terminal-redact.ts +50 -0
  26. package/app/prisma/schema.sql +25 -0
  27. package/app/routes/agent.ts +42 -0
  28. package/app/routes/codex-images.ts +67 -0
  29. package/app/routes/publish.ts +203 -22
  30. package/app/routes/stories.ts +961 -5
  31. package/app/routes/terminal.ts +383 -31
  32. package/app/server.ts +47 -12
  33. package/app/vite.config.ts +6 -0
  34. package/app/web/components/CartoonPreview.tsx +267 -0
  35. package/app/web/components/CartoonPublishPage.tsx +407 -0
  36. package/app/web/components/CartoonPublishPreview.tsx +121 -0
  37. package/app/web/components/CartoonStepGuide.tsx +90 -0
  38. package/app/web/components/CartoonWorkflowNav.tsx +68 -0
  39. package/app/web/components/CodexImportPicker.tsx +230 -0
  40. package/app/web/components/CutListPanel.tsx +1299 -0
  41. package/app/web/components/EpisodesPage.tsx +80 -0
  42. package/app/web/components/FinishEpisodePanel.tsx +151 -0
  43. package/app/web/components/Layout.tsx +7 -4
  44. package/app/web/components/LetteringEditor.tsx +1141 -0
  45. package/app/web/components/PreviewPanel.tsx +951 -78
  46. package/app/web/components/Settings.tsx +63 -0
  47. package/app/web/components/StoriesPage.tsx +710 -33
  48. package/app/web/components/StoryBrowser.tsx +22 -14
  49. package/app/web/components/StoryInfoPage.tsx +266 -0
  50. package/app/web/components/StoryProgressPanel.tsx +516 -0
  51. package/app/web/components/TerminalPanel.tsx +233 -11
  52. package/app/web/components/WorkflowCoach.tsx +128 -0
  53. package/app/web/components/asset-image.tsx +114 -0
  54. package/app/web/components/asset-test-utils.ts +44 -0
  55. package/app/web/components/export-cut.ts +320 -0
  56. package/app/web/dist/assets/export-cut-nKQ_n2-J.js +1 -0
  57. package/app/web/dist/assets/index-BAZGwVwj.js +143 -0
  58. package/app/web/dist/assets/index-DoXH2OlP.css +32 -0
  59. package/app/web/dist/index.html +2 -2
  60. package/app/web/lib/cartoon-publish-summary.ts +43 -0
  61. package/app/web/lib/codex-import.ts +94 -0
  62. package/app/web/lib/image-compress.ts +53 -0
  63. package/app/web/lib/import-image.ts +58 -0
  64. package/app/web/lib/publish-helpers.ts +385 -0
  65. package/app/web/lib/upload-retry.ts +130 -0
  66. package/app/web/lib/verify-public-title.ts +105 -0
  67. package/app/web/styles.css +9 -0
  68. package/bin/plotlink-ows.js +53 -16
  69. package/bin/startup-plan.cjs +58 -0
  70. package/lib/genres.ts +92 -0
  71. package/package.json +60 -20
  72. package/scripts/gen-schema-sql.mjs +49 -0
  73. package/scripts/package-hygiene.mjs +116 -0
  74. package/scripts/preflight.mjs +173 -0
  75. package/scripts/start-smoke.mjs +128 -0
  76. package/app/node_modules/.prisma/local-client/client.d.ts +0 -1
  77. package/app/node_modules/.prisma/local-client/client.js +0 -5
  78. package/app/node_modules/.prisma/local-client/default.d.ts +0 -1
  79. package/app/node_modules/.prisma/local-client/default.js +0 -5
  80. package/app/node_modules/.prisma/local-client/edge.d.ts +0 -1
  81. package/app/node_modules/.prisma/local-client/edge.js +0 -184
  82. package/app/node_modules/.prisma/local-client/index-browser.js +0 -173
  83. package/app/node_modules/.prisma/local-client/index.d.ts +0 -3304
  84. package/app/node_modules/.prisma/local-client/index.js +0 -207
  85. package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
  86. package/app/node_modules/.prisma/local-client/package.json +0 -183
  87. package/app/node_modules/.prisma/local-client/query_engine_bg.js +0 -2
  88. package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
  89. package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +0 -35
  90. package/app/node_modules/.prisma/local-client/runtime/edge.js +0 -35
  91. package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +0 -370
  92. package/app/node_modules/.prisma/local-client/runtime/index-browser.js +0 -17
  93. package/app/node_modules/.prisma/local-client/runtime/library.d.ts +0 -3982
  94. package/app/node_modules/.prisma/local-client/runtime/library.js +0 -147
  95. package/app/node_modules/.prisma/local-client/runtime/react-native.js +0 -84
  96. package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +0 -85
  97. package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +0 -38
  98. package/app/node_modules/.prisma/local-client/schema.prisma +0 -21
  99. package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +0 -5
  100. package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +0 -5
  101. package/app/node_modules/.prisma/local-client/wasm.d.ts +0 -1
  102. package/app/node_modules/.prisma/local-client/wasm.js +0 -191
  103. package/app/web/dist/assets/index-B-2Ft7Yv.css +0 -32
  104. package/app/web/dist/assets/index-DxATSk7X.js +0 -134
  105. package/packages/cli/node_modules/commander/LICENSE +0 -22
  106. package/packages/cli/node_modules/commander/Readme.md +0 -1149
  107. package/packages/cli/node_modules/commander/esm.mjs +0 -16
  108. package/packages/cli/node_modules/commander/index.js +0 -24
  109. package/packages/cli/node_modules/commander/lib/argument.js +0 -149
  110. package/packages/cli/node_modules/commander/lib/command.js +0 -2662
  111. package/packages/cli/node_modules/commander/lib/error.js +0 -39
  112. package/packages/cli/node_modules/commander/lib/help.js +0 -709
  113. package/packages/cli/node_modules/commander/lib/option.js +0 -367
  114. package/packages/cli/node_modules/commander/lib/suggestSimilar.js +0 -101
  115. package/packages/cli/node_modules/commander/package-support.json +0 -16
  116. package/packages/cli/node_modules/commander/package.json +0 -82
  117. package/packages/cli/node_modules/commander/typings/esm.d.mts +0 -3
  118. package/packages/cli/node_modules/commander/typings/index.d.ts +0 -1045
  119. package/packages/cli/node_modules/resolve-from/index.d.ts +0 -31
  120. package/packages/cli/node_modules/resolve-from/index.js +0 -47
  121. package/packages/cli/node_modules/resolve-from/license +0 -9
  122. package/packages/cli/node_modules/resolve-from/package.json +0 -36
  123. package/packages/cli/node_modules/resolve-from/readme.md +0 -72
  124. package/packages/cli/node_modules/tsup/LICENSE +0 -21
  125. package/packages/cli/node_modules/tsup/README.md +0 -75
  126. package/packages/cli/node_modules/tsup/assets/cjs_shims.js +0 -13
  127. package/packages/cli/node_modules/tsup/assets/esm_shims.js +0 -9
  128. package/packages/cli/node_modules/tsup/assets/package.json +0 -3
  129. package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +0 -153
  130. package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +0 -42
  131. package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +0 -6
  132. package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +0 -352
  133. package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +0 -203
  134. package/packages/cli/node_modules/tsup/dist/cli-default.js +0 -12
  135. package/packages/cli/node_modules/tsup/dist/cli-main.js +0 -8
  136. package/packages/cli/node_modules/tsup/dist/cli-node.js +0 -14
  137. package/packages/cli/node_modules/tsup/dist/index.d.ts +0 -511
  138. package/packages/cli/node_modules/tsup/dist/index.js +0 -1711
  139. package/packages/cli/node_modules/tsup/dist/rollup.js +0 -6949
  140. package/packages/cli/node_modules/tsup/package.json +0 -99
  141. package/packages/cli/node_modules/tsup/schema.json +0 -362
  142. package/public/screenshot-1.png +0 -0
  143. package/public/screenshot-2.png +0 -0
  144. package/public/screenshot-3.png +0 -0
  145. package/scripts/e2e-verify.ts +0 -1100
@@ -0,0 +1,320 @@
1
+ import {
2
+ speechTailPoints,
3
+ balloonOutline,
4
+ validateOverlaysForExport,
5
+ bubbleLayoutOptionsForOverlay,
6
+ balloonRadiusForOverlay,
7
+ type TailPoints,
8
+ } from "@app-lib/overlays";
9
+ import { textPanelDimensions } from "@app-lib/cuts";
10
+ // Re-exported so existing importers/tests can keep getting it from export-cut.
11
+ export { textPanelDimensions } from "@app-lib/cuts";
12
+ import { layoutBubbleText } from "@app-lib/bubble-text";
13
+ import { compressCanvasToBlob, MAX_IMAGE_BYTES } from "../lib/image-compress";
14
+
15
+ interface Overlay {
16
+ id: string;
17
+ type: "speech" | "narration" | "sfx";
18
+ x: number;
19
+ y: number;
20
+ width: number;
21
+ height: number;
22
+ text: string;
23
+ speaker?: string;
24
+ tailAnchor?: { x: number; y: number };
25
+ textStyle?: {
26
+ mode?: "auto" | "manual";
27
+ fontScale?: number;
28
+ fontWeight?: 400 | 700;
29
+ lineHeightFactor?: number;
30
+ speakerScale?: number;
31
+ };
32
+ bubbleStyle?: {
33
+ paddingX?: number;
34
+ paddingY?: number;
35
+ cornerRadius?: number;
36
+ };
37
+ }
38
+
39
+ // Re-exported for the existing export-size validation + tests; the compression
40
+ // policy now lives in the shared image-compress module so the lettering export
41
+ // and the Codex-image import path (#301) stay in lockstep.
42
+ const MAX_SIZE = MAX_IMAGE_BYTES;
43
+
44
+ export async function ensureFontsReady(families: string[]): Promise<{ ready: boolean; missing: string[] }> {
45
+ if (typeof document === "undefined" || !document.fonts || typeof document.fonts.load !== "function") {
46
+ return { ready: true, missing: [] };
47
+ }
48
+
49
+ const missing: string[] = [];
50
+ for (const family of families) {
51
+ try {
52
+ const loaded = await document.fonts.load(`16px "${family}"`);
53
+ // load() resolves with the FontFace[] that matched. An empty array means
54
+ // the family was never registered (e.g. CDN CSS blocked), so check() may
55
+ // only be matching a system fallback — treat as missing.
56
+ if (!loaded || loaded.length === 0) {
57
+ missing.push(family);
58
+ } else if (!document.fonts.check(`16px "${family}"`)) {
59
+ missing.push(family);
60
+ }
61
+ } catch {
62
+ missing.push(family);
63
+ }
64
+ }
65
+
66
+ return { ready: missing.length === 0, missing };
67
+ }
68
+
69
+ function loadImage(url: string): Promise<HTMLImageElement> {
70
+ return new Promise((resolve, reject) => {
71
+ const img = new Image();
72
+ img.crossOrigin = "anonymous";
73
+ img.onload = () => resolve(img);
74
+ img.onerror = () => reject(new Error("Failed to load image"));
75
+ img.src = url;
76
+ });
77
+ }
78
+
79
+ // Webtoon balloon styling (#363). A near-opaque bubble with a strong, clean
80
+ // near-black outline reads as a comic speech balloon rather than a faint UI box
81
+ // (the old rgba(0,0,0,0.3) hairline). Narration is an intentional parchment
82
+ // card with a softer outline; both stroke weights scale with the panel so the
83
+ // look holds at any export resolution.
84
+ const SPEECH_FILL = "rgba(255, 255, 255, 0.95)";
85
+ const SPEECH_STROKE = "#1a1a1a";
86
+ const NARRATION_FILL = "rgba(244, 239, 230, 0.94)";
87
+ const NARRATION_STROKE = "rgba(26, 26, 26, 0.55)";
88
+
89
+ // Outline weight as a fraction of the rendered panel height, so a balloon keeps
90
+ // the same visual line thickness whether exported small or large (#363).
91
+ function balloonStrokeWidth(renderHeight: number): number {
92
+ return Math.max(2, renderHeight * 0.004);
93
+ }
94
+
95
+ // Trace a speech balloon — rounded-rect body plus its pointer tail — as ONE
96
+ // continuous outline, from the SHARED balloonOutline geometry (#341, formerly a
97
+ // duplicate of #317's tracer). Because the editor-preview SVG path and this
98
+ // export canvas now build from the identical command list, the tail is always a
99
+ // detour in the body's perimeter (never a separate stroked shape) and the
100
+ // exported balloon matches the preview with no internal body/tail seam. `tail`
101
+ // is null for a bubble with no (or inside-the-bubble) tail → a rounded rect.
102
+ function traceBalloonPath(
103
+ ctx: CanvasRenderingContext2D,
104
+ ox: number,
105
+ oy: number,
106
+ ow: number,
107
+ oh: number,
108
+ tail: TailPoints | null,
109
+ radius?: number,
110
+ ) {
111
+ ctx.beginPath();
112
+ for (const c of balloonOutline(ox, oy, ow, oh, tail, radius)) {
113
+ if (c.k === "M") ctx.moveTo(c.x, c.y);
114
+ else if (c.k === "L") ctx.lineTo(c.x, c.y);
115
+ else ctx.arcTo(c.cornerX, c.cornerY, c.x, c.y, c.r);
116
+ }
117
+ ctx.closePath();
118
+ }
119
+
120
+ export function renderOverlays(
121
+ ctx: CanvasRenderingContext2D,
122
+ overlays: Overlay[],
123
+ width: number,
124
+ height: number,
125
+ bodyFont: string,
126
+ displayFont: string,
127
+ ) {
128
+ for (const overlay of overlays) {
129
+ const ox = overlay.x * width;
130
+ const oy = overlay.y * height;
131
+ const ow = overlay.width * width;
132
+ const oh = overlay.height * height;
133
+
134
+ const strokeW = balloonStrokeWidth(height);
135
+ if (overlay.type === "speech") {
136
+ // Trace the body and its tail as a single outline so the exported balloon
137
+ // has no internal seam between them (#317): one fill, one stroke, with the
138
+ // tail forming part of the balloon's outline instead of a shape laid over
139
+ // a fully-stroked body border. A rounded line join keeps the tail/corner
140
+ // junctions soft and organic (#363).
141
+ const radius = balloonRadiusForOverlay(overlay, ow, oh);
142
+ const tail = overlay.tailAnchor ? speechTailPoints(ox, oy, ow, oh, overlay.tailAnchor, radius) : null;
143
+ traceBalloonPath(ctx, ox, oy, ow, oh, tail, radius);
144
+ ctx.fillStyle = SPEECH_FILL;
145
+ ctx.fill();
146
+ ctx.strokeStyle = SPEECH_STROKE;
147
+ ctx.lineWidth = strokeW;
148
+ ctx.lineJoin = "round";
149
+ ctx.stroke();
150
+ } else if (overlay.type === "narration") {
151
+ // Narration stays rectangular but reads as an intentional webtoon caption
152
+ // card: gently rounded corners + a confident (if softer-than-speech)
153
+ // outline, instead of a hairline box (#363).
154
+ const nr = Math.min(ow, oh) * 0.12;
155
+ ctx.beginPath();
156
+ ctx.roundRect(ox, oy, ow, oh, nr);
157
+ ctx.fillStyle = NARRATION_FILL;
158
+ ctx.fill();
159
+ ctx.strokeStyle = NARRATION_STROKE;
160
+ ctx.lineWidth = Math.max(1.5, strokeW * 0.75);
161
+ ctx.lineJoin = "round";
162
+ ctx.stroke();
163
+ }
164
+
165
+ const font = overlay.type === "sfx" ? displayFont : bodyFont;
166
+ const hasSpeaker = overlay.type !== "sfx" && !!overlay.speaker;
167
+ // Measure with the actual draw font so wrapping matches what is rendered.
168
+ const measure = (text: string, fontSize: number, fontWeight: 400 | 700 = 400): number => {
169
+ ctx.font = `${fontWeight} ${fontSize}px ${font}`;
170
+ return ctx.measureText(text).width;
171
+ };
172
+ const layout = layoutBubbleText(
173
+ measure,
174
+ overlay.text,
175
+ ow,
176
+ oh,
177
+ bubbleLayoutOptionsForOverlay(overlay, height, ow, oh),
178
+ );
179
+
180
+ ctx.textAlign = "center";
181
+ ctx.textBaseline = "middle";
182
+ const cx = ox + ow / 2;
183
+ const speakerStrip = hasSpeaker ? layout.speakerFontSize * 1.2 : 0;
184
+
185
+ // Draw the speaker label on its own strip at the top of the bubble.
186
+ if (hasSpeaker) {
187
+ ctx.fillStyle = "#3a3a3a";
188
+ ctx.font = `700 ${layout.speakerFontSize}px ${font}`;
189
+ ctx.fillText(overlay.speaker as string, cx, oy + speakerStrip / 2 + oh * 0.04, ow - 6);
190
+ }
191
+
192
+ // Lay out the wrapped body lines, vertically centered in the remaining box.
193
+ const bodyTop = oy + speakerStrip;
194
+ const bodyH = oh - speakerStrip;
195
+ const totalTextH = layout.lines.length * layout.lineHeight;
196
+ let lineY = bodyTop + bodyH / 2 - totalTextH / 2 + layout.lineHeight / 2;
197
+
198
+ ctx.font = `${overlay.textStyle?.fontWeight ?? 400} ${layout.fontSize}px ${font}`;
199
+ for (const line of layout.lines) {
200
+ if (overlay.type === "sfx") {
201
+ ctx.fillStyle = "#000";
202
+ ctx.strokeStyle = "#fff";
203
+ ctx.lineWidth = 3;
204
+ ctx.strokeText(line, cx, lineY);
205
+ ctx.fillText(line, cx, lineY);
206
+ } else {
207
+ ctx.fillStyle = "#1a1a1a";
208
+ ctx.fillText(line, cx, lineY);
209
+ }
210
+ lineY += layout.lineHeight;
211
+ }
212
+ }
213
+ }
214
+
215
+ interface CutTextContent {
216
+ narration?: string;
217
+ dialogue?: { speaker: string; text: string }[];
218
+ }
219
+
220
+ function renderCutText(
221
+ ctx: CanvasRenderingContext2D,
222
+ content: CutTextContent,
223
+ width: number,
224
+ height: number,
225
+ font: string,
226
+ ) {
227
+ const fontSize = Math.max(14, Math.min(height * 0.05, 28));
228
+ ctx.font = `${fontSize}px ${font}`;
229
+ ctx.fillStyle = "#1a1a1a";
230
+ ctx.textAlign = "center";
231
+ ctx.textBaseline = "middle";
232
+
233
+ const lines: string[] = [];
234
+ if (content.dialogue) {
235
+ for (const d of content.dialogue) {
236
+ lines.push(`${d.speaker}: ${d.text}`);
237
+ }
238
+ }
239
+ if (content.narration) {
240
+ lines.push(content.narration);
241
+ }
242
+
243
+ const lineHeight = fontSize * 1.6;
244
+ const startY = height / 2 - ((lines.length - 1) * lineHeight) / 2;
245
+ for (let i = 0; i < lines.length; i++) {
246
+ ctx.fillText(lines[i], width / 2, startY + i * lineHeight, width - 40);
247
+ }
248
+ }
249
+
250
+ /** Style for a text/interstitial panel exported without a clean image (#351). */
251
+ export interface TextPanelStyle {
252
+ /** CSS background color for the panel canvas. Defaults to white. */
253
+ background?: string;
254
+ /** Aspect ratio "W:H" (e.g. "4:5") sizing the canvas. Defaults to 800×600. */
255
+ aspectRatio?: string;
256
+ }
257
+
258
+ export async function exportCut(
259
+ cleanImageUrl: string | null,
260
+ overlays: Overlay[],
261
+ bodyFontFamily: string,
262
+ displayFontFamily: string,
263
+ cutText?: CutTextContent,
264
+ textPanel?: TextPanelStyle,
265
+ ): Promise<Blob> {
266
+ // Refuse to export an image whose overlays have invalid geometry — otherwise
267
+ // malformed (e.g. semantic-position) overlays render nothing and we silently
268
+ // produce an unlettered final (#309).
269
+ const overlayCheck = validateOverlaysForExport(overlays);
270
+ if (!overlayCheck.valid) {
271
+ throw new Error(overlayCheck.error ?? "Overlay geometry is invalid");
272
+ }
273
+
274
+ let width = 800;
275
+ let height = 600;
276
+ let img: HTMLImageElement | null = null;
277
+
278
+ if (cleanImageUrl) {
279
+ img = await loadImage(cleanImageUrl);
280
+ width = img.naturalWidth;
281
+ height = img.naturalHeight;
282
+ } else if (textPanel) {
283
+ // Text/interstitial panel: no clean image — render text on a styled canvas
284
+ // sized by the panel's aspect ratio (#351).
285
+ const dims = textPanelDimensions(textPanel.aspectRatio);
286
+ if (dims) {
287
+ width = dims.width;
288
+ height = dims.height;
289
+ }
290
+ }
291
+
292
+ const canvas = document.createElement("canvas");
293
+ canvas.width = width;
294
+ canvas.height = height;
295
+ const ctx = canvas.getContext("2d")!;
296
+
297
+ if (img) {
298
+ ctx.drawImage(img, 0, 0, width, height);
299
+ } else {
300
+ ctx.fillStyle = textPanel?.background || "#ffffff";
301
+ ctx.fillRect(0, 0, width, height);
302
+ }
303
+
304
+ renderOverlays(ctx, overlays, width, height, bodyFontFamily, displayFontFamily);
305
+
306
+ if (cutText && overlays.length === 0 && !img) {
307
+ renderCutText(ctx, cutText, width, height, bodyFontFamily);
308
+ }
309
+
310
+ return compressCanvasToBlob(canvas);
311
+ }
312
+
313
+ export function validateExportSize(blob: Blob): { valid: boolean; error?: string } {
314
+ if (blob.size > MAX_SIZE) {
315
+ return { valid: false, error: `Image is ${(blob.size / 1024).toFixed(0)}KB, exceeds 1MB limit` };
316
+ }
317
+ return { valid: true };
318
+ }
319
+
320
+ export { MAX_SIZE };
@@ -0,0 +1 @@
1
+ import{M as w,v as O,t as A,c as M,b as R,s as H,l as I,a as B,d as F}from"./index-BAZGwVwj.js";const z=w;async function G(e){if(typeof document>"u"||!document.fonts||typeof document.fonts.load!="function")return{ready:!0,missing:[]};const n=[];for(const s of e)try{const t=await document.fonts.load(`16px "${s}"`);!t||t.length===0?n.push(s):document.fonts.check(`16px "${s}"`)||n.push(s)}catch{n.push(s)}return{ready:n.length===0,missing:n}}function C(e){return new Promise((n,s)=>{const t=new Image;t.crossOrigin="anonymous",t.onload=()=>n(t),t.onerror=()=>s(new Error("Failed to load image")),t.src=e})}const W="rgba(255, 255, 255, 0.95)",_="#1a1a1a",L="rgba(244, 239, 230, 0.94)",N="rgba(26, 26, 26, 0.55)";function Y(e){return Math.max(2,e*.004)}function K(e,n,s,t,c,d,f){e.beginPath();for(const o of F(n,s,t,c,d,f))o.k==="M"?e.moveTo(o.x,o.y):o.k==="L"?e.lineTo(o.x,o.y):e.arcTo(o.cornerX,o.cornerY,o.x,o.y,o.r);e.closePath()}function P(e,n,s,t,c,d){var f;for(const o of n){const l=o.x*s,i=o.y*t,r=o.width*s,a=o.height*t,y=Y(t);if(o.type==="speech"){const u=R(o,r,a),k=o.tailAnchor?H(l,i,r,a,o.tailAnchor,u):null;K(e,l,i,r,a,k,u),e.fillStyle=W,e.fill(),e.strokeStyle=_,e.lineWidth=y,e.lineJoin="round",e.stroke()}else if(o.type==="narration"){const u=Math.min(r,a)*.12;e.beginPath(),e.roundRect(l,i,r,a,u),e.fillStyle=L,e.fill(),e.strokeStyle=N,e.lineWidth=Math.max(1.5,y*.75),e.lineJoin="round",e.stroke()}const g=o.type==="sfx"?d:c,T=o.type!=="sfx"&&!!o.speaker,h=I((u,k,E=400)=>(e.font=`${E} ${k}px ${g}`,e.measureText(u).width),o.text,r,a,B(o,t,r,a));e.textAlign="center",e.textBaseline="middle";const p=l+r/2,S=T?h.speakerFontSize*1.2:0;T&&(e.fillStyle="#3a3a3a",e.font=`700 ${h.speakerFontSize}px ${g}`,e.fillText(o.speaker,p,i+S/2+a*.04,r-6));const v=i+S,b=a-S,$=h.lines.length*h.lineHeight;let m=v+b/2-$/2+h.lineHeight/2;e.font=`${((f=o.textStyle)==null?void 0:f.fontWeight)??400} ${h.fontSize}px ${g}`;for(const u of h.lines)o.type==="sfx"?(e.fillStyle="#000",e.strokeStyle="#fff",e.lineWidth=3,e.strokeText(u,p,m),e.fillText(u,p,m)):(e.fillStyle="#1a1a1a",e.fillText(u,p,m)),m+=h.lineHeight}}function X(e,n,s,t,c){const d=Math.max(14,Math.min(t*.05,28));e.font=`${d}px ${c}`,e.fillStyle="#1a1a1a",e.textAlign="center",e.textBaseline="middle";const f=[];if(n.dialogue)for(const i of n.dialogue)f.push(`${i.speaker}: ${i.text}`);n.narration&&f.push(n.narration);const o=d*1.6,l=t/2-(f.length-1)*o/2;for(let i=0;i<f.length;i++)e.fillText(f[i],s/2,l+i*o,s-40)}async function Z(e,n,s,t,c,d){const f=O(n);if(!f.valid)throw new Error(f.error??"Overlay geometry is invalid");let o=800,l=600,i=null;if(e)i=await C(e),o=i.naturalWidth,l=i.naturalHeight;else if(d){const y=A(d.aspectRatio);y&&(o=y.width,l=y.height)}const r=document.createElement("canvas");r.width=o,r.height=l;const a=r.getContext("2d");return i?a.drawImage(i,0,0,o,l):(a.fillStyle=(d==null?void 0:d.background)||"#ffffff",a.fillRect(0,0,o,l)),P(a,n,o,l,s,t),c&&n.length===0&&!i&&X(a,c,o,l,s),M(r)}function j(e){return e.size>z?{valid:!1,error:`Image is ${(e.size/1024).toFixed(0)}KB, exceeds 1MB limit`}:{valid:!0}}export{z as MAX_SIZE,G as ensureFontsReady,Z as exportCut,P as renderOverlays,A as textPanelDimensions,j as validateExportSize};