@flatkit/compiler 0.30.1 → 0.31.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/dist/cli/flatc.js CHANGED
@@ -1,8 +1,566 @@
1
1
  import {
2
- run
3
- } from "../chunk-MRB6DWJA.js";
4
- import "../chunk-LO4PABQX.js";
5
- import "../chunk-KXKX2RMX.js";
2
+ applyFixes,
3
+ docHasErrors,
4
+ formatDiagnostics,
5
+ lintDocReport,
6
+ programDiagnostics,
7
+ repairLoop
8
+ } from "../chunk-UO7ZHNQT.js";
9
+ import {
10
+ compileFlatpack,
11
+ packToJSON
12
+ } from "../chunk-KXKX2RMX.js";
13
+
14
+ // src/cli/flatc.ts
15
+ import { readFileSync, writeFileSync, existsSync, readdirSync, watch, mkdirSync, copyFileSync } from "fs";
16
+ import { resolve, dirname, basename, extname, join, relative, isAbsolute } from "path";
17
+ import { parseProgramFull, parseFlatLib } from "@flatkit/engine/flatFormat";
18
+ import { hasPackage } from "@flatkit/engine/stdlib";
19
+ import { parseUnits } from "@flatkit/engine/dsl";
20
+ import { sanitizeDoc } from "@flatkit/engine/validateDoc";
21
+ import { unitsToFunctions } from "@flatkit/engine/scriptDoc";
22
+ import { containerBBox, containerBBoxUnion } from "@flatkit/engine/groups";
23
+ import { isInstance, isGroup } from "@flatkit/engine/layers";
24
+ import { IDENTITY } from "@flatkit/engine/transform";
25
+ import { FlatSyntaxError } from "@flatkit/engine/flatFormat";
26
+ import { playHeadless } from "@flatkit/player/debug";
27
+ var MIME = {
28
+ ".png": "image/png",
29
+ ".jpg": "image/jpeg",
30
+ ".jpeg": "image/jpeg",
31
+ ".gif": "image/gif",
32
+ ".webp": "image/webp",
33
+ ".svg": "image/svg+xml",
34
+ ".avif": "image/avif",
35
+ ".mp3": "audio/mpeg",
36
+ ".wav": "audio/wav",
37
+ ".ogg": "audio/ogg",
38
+ ".m4a": "audio/mp4",
39
+ // Fonts (RFC 8081 media types) — matches the editor's import (`font/woff2` default, FontFace API).
40
+ ".woff2": "font/woff2",
41
+ ".woff": "font/woff",
42
+ ".ttf": "font/ttf",
43
+ ".otf": "font/otf"
44
+ // NB: video (.mp4/.webm/…) intentionally omitted — no video runtime in the editor or player yet.
45
+ };
46
+ var mimeFor = (path) => MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
47
+ function isWithin(baseDir, target) {
48
+ const rel = relative(baseDir, target);
49
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
50
+ }
51
+ function printHelp() {
52
+ process.stdout.write(`flatc \u2014 compile a FlatInk program into a .flatpack
53
+
54
+ Usage:
55
+ flatc <program.flatink> [assets.flat \u2026] [-o output.flatpack]
56
+ flatc <program.flatink> --watch
57
+ flatc --play <program.flatink | scene.flatpack> --script <gestures.json>
58
+ flatc --render <program.flatink | scene.flatpack> -o out.png [--frame N] [--at k=v[,k2=v2]] [--scale S]
59
+ flatc --preview <library.flat> [--symbol NAME] [-o out.flatpack | --render -o out.png]
60
+ flatc <library.flat> [more.flat \u2026] --check
61
+
62
+ program.flatink the program (composition + logic, text DSL)
63
+ assets.flat visual asset libs (default: every .flat in the program's folder)
64
+ -o, --out output file (default: <program>.flatpack \u2014 the CANONICAL name, JSON inside)
65
+ --assets MODE media baking: 'inline' (default, base64 in the .flatpack) or 'external'
66
+ (sidecar <out>.assets/ folder; asset.data = relative key \u2014 serve the folder and
67
+ play with sameOriginAssetResolver(<flatpackUrl>))
68
+ --check semantic lint only (no .flatpack); exits \u22600 on ERROR (warnings do not stop). Lints a
69
+ program .flatink OR an asset library .flat (per-symbol; several .flat are merged)
70
+ --fix apply the MECHANICAL repairs the diagnostics carry (missing separator, run-on line)
71
+ and re-check, iterating. Writes the file AT MOST ONCE, and never a text it has not
72
+ re-checked; a file it cannot repair is left untouched, mtime included. Implies --check.
73
+ --no-libs do NOT auto-discover the .flat files sitting next to the program (use it in a working
74
+ folder, where a neighbouring scratch file is not a dependency)
75
+ --watch recompile on every change in the folder (agent \u2192 player loop)
76
+ --play run the file WITHOUT a canvas, replay --script and print { sends, vars } (JSON)
77
+ --trace (with --play) HUMAN-READABLE log per gesture: emitted sends + variable diff (debug)
78
+ --script <f> JSON gesture script: [{ "type": "down|move|up|cancel", "x", "y" }, { "type": "set", "name", "value" }, { "type": "wait", "frames": N }, { "type": "wheel", "dy": N }]
79
+ semantic (by NAME, the engine resolves coords): { "type": "drag", "source", "target" } \xB7 { "type": "tap", "target" }
80
+ \xB7 { "type": "scratch", "target" } (sweeps a reveal zone) \xB7 { "type": "connect", "source", "target" } (pulls a link wire)
81
+ { "type": "key", "name": "ArrowRight", "frames": N } holds a key (keys.<name> = 1) N steps, then releases it
82
+ "wait" lets the simulation run N fixed steps (60 Hz): "every frame" + playhead advance like in real playback
83
+ "expect" self-verifies: { "type": "expect", "sends": ["done"], "vars": { "score": 3 } } \u2192 exits \u22600 on mismatch
84
+ (sends = sequence of names emitted SINCE the last expect; vars = current state). Great in CI.
85
+ --render render a PNG IMAGE (headless skia): see what we draw (positioning)
86
+ --preview wrap ONE symbol of a <library.flat> into a playable Doc (a single centered instance on an
87
+ auto-sized stage) \u2192 a .flatpack to drop in the browser player, or a PNG with --render.
88
+ No wrapper .flatink to author by hand. Output defaults to <library>.<symbol>.flatpack/.png
89
+ --symbol NAME (with --preview) which symbol to wrap (default: the first in the lib)
90
+ --bbox all|frame0 (with --preview) auto-size to the UNION over all frames (default 'all', no clipping of
91
+ drifting/rotating/growing motion) or just frame 0 ('frame0', the old behavior)
92
+ --pad N (with --preview) padding in px around the symbol's bounds (default 24)
93
+ --set p=v[,p2=v2] set a SYMBOL's exposed params (works with --preview AND --render); a state param
94
+ takes a state NAME
95
+ or a number (e.g. --set door=open), others take a number \u2192 baked into the preview
96
+ --frame N (with --render) target frame (default 0)
97
+ --at k=v[,k2=v2] (with --render) force variables \u2192 capture a given state (e.g. a step of an escape)
98
+ --steps N (with --render) run N fixed sim steps (60 Hz, every-frame) BEFORE capture \u2192 see a
99
+ stateful act unfold without forcing every derived variable by hand in --at
100
+ --scale S|auto (with --render) resolution factor (default 2); 'auto' picks one from the canvas size
101
+ (enlarges small/thin assets so fine filaments stay legible, large assets stay 1x)
102
+ -h, --help this help
103
+
104
+ Media referenced by 'asset "id" "path" kind' are embedded (paths relative to the program).
105
+ `);
106
+ }
107
+ function buildDocFromProgram(programPath, explicitFlats = [], assetMode = "inline", assetsDir = "", noLibs = false, srcOverride) {
108
+ const baseDir = dirname(programPath);
109
+ const programSrc = srcOverride ?? readFileSync(programPath, "utf8");
110
+ const prog = parseProgramFull(programSrc);
111
+ const flatPaths = new Set(explicitFlats.map((p) => resolve(p)));
112
+ if (!flatPaths.size && !noLibs) for (const f of readdirSync(baseDir).filter((f2) => f2.endsWith(".flat"))) flatPaths.add(join(baseDir, f));
113
+ const pkgFunctions = [];
114
+ const localResolved = /* @__PURE__ */ new Set();
115
+ for (const name of prog.imports ?? []) {
116
+ if (hasPackage(name)) continue;
117
+ const fink = join(baseDir, name + ".flatink");
118
+ const fflat = join(baseDir, name + ".flat");
119
+ if (!isWithin(baseDir, fink) || !isWithin(baseDir, fflat)) {
120
+ process.stderr.write(`flatc: package outside the program folder (ignored): "${name}"
121
+ `);
122
+ continue;
123
+ }
124
+ let found = false;
125
+ if (existsSync(fink)) {
126
+ for (const f of unitsToFunctions(parseUnits(readFileSync(fink, "utf8")).units)) pkgFunctions.push(f, { ...f, name: `${name}.${f.name}` });
127
+ found = true;
128
+ }
129
+ if (existsSync(fflat)) {
130
+ flatPaths.add(fflat);
131
+ found = true;
132
+ }
133
+ if (found) localResolved.add(name);
134
+ else process.stderr.write(`flatc: package not found: "${name}" (neither stdlib, nor ${name}.flatink / ${name}.flat)
135
+ `);
136
+ }
137
+ const assetSrcs = [...flatPaths].map((p) => {
138
+ const src = readFileSync(p, "utf8");
139
+ try {
140
+ parseFlatLib(src);
141
+ } catch (e) {
142
+ throw new Error(`${basename(p)}: ${e.message} (pass --no-libs to ignore the .flat files sitting next to the program)`, { cause: e });
143
+ }
144
+ if (programConstructsIn(src).length) process.stderr.write(`flatc: ${basename(p)}: looks like a PROGRAM saved as .flat \u2014 loaded as a symbol library, so its scene/object blocks are ignored. Rename it to .flatink, or pass --no-libs.
145
+ `);
146
+ return src;
147
+ });
148
+ const media = {};
149
+ const mediaCopies = [];
150
+ for (const a of prog.assets ?? []) {
151
+ const mp = resolve(baseDir, a.data);
152
+ if (!isWithin(baseDir, mp)) {
153
+ process.stderr.write(`flatc: media outside the program folder (ignored): ${a.data}
154
+ `);
155
+ continue;
156
+ }
157
+ if (!existsSync(mp)) {
158
+ process.stderr.write(`flatc: missing media (ignored): ${a.data}
159
+ `);
160
+ continue;
161
+ }
162
+ const mime = mimeFor(mp);
163
+ if (assetMode === "external") {
164
+ const key = `${assetsDir}/${a.data.replace(/\\/g, "/")}`;
165
+ media[a.data] = { mime, data: key };
166
+ mediaCopies.push({ src: mp, key });
167
+ } else {
168
+ media[a.data] = { mime, data: `data:${mime};base64,${readFileSync(mp).toString("base64")}` };
169
+ }
170
+ }
171
+ let doc = compileFlatpack(programSrc, assetSrcs, media);
172
+ if (pkgFunctions.length) doc = { ...doc, functions: [...doc.functions ?? [], ...pkgFunctions] };
173
+ const stdImports = (doc.imports ?? []).filter(hasPackage);
174
+ doc = { ...doc, imports: stdImports.length ? stdImports : void 0 };
175
+ return { doc, flatLibs: flatPaths.size, packages: localResolved.size, media: Object.keys(media).length, mediaCopies, src: programSrc };
176
+ }
177
+ function compileOnce(programPath, explicitFlats, out, checkOnly, assetMode = "inline", noLibs = false, doFix = false) {
178
+ const outPath = out ? resolve(out) : join(dirname(programPath), basename(programPath, extname(programPath)) + ".flatpack");
179
+ const assetsDir = assetMode === "external" ? basename(outPath, extname(outPath)) + ".assets" : "";
180
+ const build = (src) => buildDocFromProgram(programPath, explicitFlats, assetMode, assetsDir, noLibs, src);
181
+ let parseRepaired = "";
182
+ if (doFix) {
183
+ let candidate = readFileSync(programPath, "utf8");
184
+ for (let pass = 0; pass < 10; pass++) {
185
+ try {
186
+ build(candidate);
187
+ break;
188
+ } catch (e) {
189
+ if (!(e instanceof FlatSyntaxError) || !e.fix) break;
190
+ const { text, applied } = applyFixes(candidate, [{ scope: "scene", line: e.line, col: e.col, severity: "error", message: e.message, fix: e.fix }]);
191
+ if (!applied) break;
192
+ candidate = text;
193
+ parseRepaired = candidate;
194
+ process.stderr.write(`flatc: --fix: repaired \`${e.message.split(":")[0]}\` in ${basename(programPath)}
195
+ `);
196
+ }
197
+ }
198
+ }
199
+ let built;
200
+ try {
201
+ built = build(parseRepaired || void 0);
202
+ } catch (e) {
203
+ process.stderr.write(`flatc: compile error: ${e.message}
204
+ `);
205
+ return 1;
206
+ }
207
+ const { doc } = built;
208
+ const diagnostics = programDiagnostics(doc, built.src);
209
+ const report = formatDiagnostics(diagnostics);
210
+ const hasErrors = diagnostics.some((d) => d.severity === "error");
211
+ if (doFix) {
212
+ const original = parseRepaired || readFileSync(programPath, "utf8");
213
+ const { text, applied: total, first, errors: before } = repairLoop(original, diagnostics, (src) => programDiagnostics(buildDocFromProgram(programPath, explicitFlats, assetMode, assetsDir, noLibs, src).doc, src));
214
+ if (!total && !parseRepaired) {
215
+ process.stderr.write(`flatc: --fix: nothing to repair mechanically${hasErrors ? " \u2014 the remaining errors need a decision, not a separator\n" : "\n"}`);
216
+ } else {
217
+ writeFileSync(programPath, text);
218
+ process.stderr.write(`flatc: --fix: ${total} repair(s) applied to ${basename(programPath)} \xB7 errors ${first} \u2192 ${before}
219
+ `);
220
+ return compileOnce(programPath, explicitFlats, out, checkOnly, assetMode, noLibs, false);
221
+ }
222
+ }
223
+ if (checkOnly) {
224
+ if (report) process.stderr.write(report + "\n");
225
+ if (hasErrors) return 1;
226
+ const warnings = diagnostics.length;
227
+ process.stdout.write(`flatc: check passed \u2713${warnings ? ` \xB7 ${warnings} warning(s)` : ""}
228
+ `);
229
+ return 0;
230
+ }
231
+ if (report) process.stderr.write(report + "\n");
232
+ writeFileSync(outPath, packToJSON(doc));
233
+ const outDir = dirname(outPath);
234
+ for (const c of built.mediaCopies) {
235
+ const dest = join(outDir, ...c.key.split("/"));
236
+ if (!isWithin(outDir, dest)) continue;
237
+ mkdirSync(dirname(dest), { recursive: true });
238
+ copyFileSync(c.src, dest);
239
+ }
240
+ const where = assetMode === "external" ? ` (external \u2192 ${assetsDir}/)` : "";
241
+ process.stdout.write(`flatc: ${basename(outPath)} \u2713 ${doc.symbols.length} symbol(s) \xB7 ${built.flatLibs} lib(s) \xB7 ${built.packages} package(s) \xB7 ${built.media} media${where}
242
+ `);
243
+ return 0;
244
+ }
245
+ function programConstructsIn(src) {
246
+ const found = [];
247
+ if (/^[ \t]*scene\s*\{/m.test(src)) found.push("a `scene { \u2026 }` block");
248
+ if (/^[ \t]*object\s+"/m.test(src)) found.push('`object "\u2026" { \u2026 }` blocks');
249
+ return found;
250
+ }
251
+ function checkFlatLibs(flatPaths) {
252
+ const symbols = [];
253
+ for (const p of flatPaths) {
254
+ let src;
255
+ try {
256
+ src = readFileSync(p, "utf8");
257
+ } catch (e) {
258
+ process.stderr.write(`flatc: cannot read: ${e.message}
259
+ `);
260
+ return 1;
261
+ }
262
+ const disguised = programConstructsIn(src);
263
+ if (disguised.length) {
264
+ process.stderr.write(`flatc: ${basename(p)}: this is a PROGRAM, not a symbol library \u2014 it contains ${disguised.join(" and ")}, which a .flat never carries and which --check therefore does NOT verify. Rename it to .flatink.
265
+ `);
266
+ return 1;
267
+ }
268
+ try {
269
+ symbols.push(...parseFlatLib(src).symbols);
270
+ } catch (e) {
271
+ process.stderr.write(`flatc: ${basename(p)}: ${e.message}
272
+ `);
273
+ return 1;
274
+ }
275
+ }
276
+ const doc = { width: 1, height: 1, timeline: { fps: 24, durationFrames: 1, tracks: [] }, variables: {}, layers: [], symbols };
277
+ const report = lintDocReport(doc);
278
+ if (report) process.stderr.write(report + "\n");
279
+ if (docHasErrors(doc)) return 1;
280
+ const warnings = report ? report.split("\n").filter(Boolean).length : 0;
281
+ process.stdout.write(`flatc: check passed \u2713 ${symbols.length} symbol(s)${warnings ? ` \xB7 ${warnings} warning(s)` : ""}
282
+ `);
283
+ return 0;
284
+ }
285
+ function loadDoc(filePath, noLibs = false) {
286
+ if (filePath.endsWith(".flatink")) return buildDocFromProgram(filePath, [], "inline", "", noLibs).doc;
287
+ return sanitizeDoc(JSON.parse(readFileSync(filePath, "utf8")));
288
+ }
289
+ function playOnce(filePath, scriptPath, trace, noLibs = false) {
290
+ if (!scriptPath) {
291
+ process.stderr.write("flatc: --play requires --script <gestures.json>\n");
292
+ return 1;
293
+ }
294
+ if (!existsSync(scriptPath)) {
295
+ process.stderr.write(`flatc: script not found: ${scriptPath}
296
+ `);
297
+ return 1;
298
+ }
299
+ let doc, gestures;
300
+ try {
301
+ doc = loadDoc(filePath, noLibs);
302
+ } catch (e) {
303
+ process.stderr.write(`flatc: cannot read: ${e.message}
304
+ `);
305
+ return 1;
306
+ }
307
+ try {
308
+ gestures = JSON.parse(readFileSync(scriptPath, "utf8"));
309
+ } catch (e) {
310
+ process.stderr.write(`flatc: invalid JSON script: ${e.message}
311
+ `);
312
+ return 1;
313
+ }
314
+ if (!Array.isArray(gestures)) {
315
+ process.stderr.write("flatc: the script must be an array of gestures\n");
316
+ return 1;
317
+ }
318
+ const res = playHeadless(doc, gestures, { trace });
319
+ if (!trace) process.stdout.write(JSON.stringify(res, null, 2) + "\n");
320
+ else
321
+ for (const s of res.steps ?? []) {
322
+ const send = (e) => e.fields ? `${e.name}{${Object.entries(e.fields).map(([k, v]) => `${k}=${v}`).join(", ")}}` : e.value !== void 0 ? `${e.name}=${e.value}` : e.name;
323
+ const sends = s.sends.length ? " sends:[" + s.sends.map(send).join(", ") + "]" : "";
324
+ const vars = Object.entries(s.changed).map(([k, [a, b]]) => `${k}:${JSON.stringify(a)}\u2192${JSON.stringify(b)}`);
325
+ process.stdout.write(`${s.gesture.padEnd(28)}${sends}${vars.length ? " vars{" + vars.join(" ") + "}" : ""}
326
+ `);
327
+ }
328
+ if (res.expectFailures?.length) {
329
+ for (const f of res.expectFailures) process.stderr.write(`flatc: \u2717 ${f}
330
+ `);
331
+ return 1;
332
+ }
333
+ return 0;
334
+ }
335
+ function parseVars(spec, into) {
336
+ for (const pair of spec.split(",")) {
337
+ const i = pair.indexOf("=");
338
+ if (i < 0) continue;
339
+ const k = pair.slice(0, i).trim();
340
+ const v = Number(pair.slice(i + 1).trim());
341
+ if (k && Number.isFinite(v)) into[k] = v;
342
+ }
343
+ }
344
+ function autoScale(w, h) {
345
+ const maxSide = Math.max(1, w, h), minSide = Math.max(1, Math.min(w, h));
346
+ return Math.max(1, Math.min(8, Math.round(Math.max(160 / maxSide, 48 / minSide))));
347
+ }
348
+ async function renderDocToFile(doc, outPath, frame, vars, scale, steps, params = {}) {
349
+ try {
350
+ const { renderDocToPng } = await import("./render.js");
351
+ const png = await renderDocToPng(doc, { frame, vars: Object.keys(vars).length ? vars : void 0, scale, steps: steps || void 0, params: Object.keys(params).length ? params : void 0 });
352
+ writeFileSync(outPath, png);
353
+ } catch (e) {
354
+ process.stderr.write(`flatc: render failed: ${e.message}
355
+ `);
356
+ if (process.env.FLATC_DEBUG && e.stack) process.stderr.write(`${e.stack}
357
+ `);
358
+ return 1;
359
+ }
360
+ process.stdout.write(`flatc: ${basename(outPath)} \u2713 ${doc.width}\xD7${doc.height} \xD7${scale}${frame ? ` \xB7 frame ${frame}` : ""}${steps ? ` \xB7 ${steps} step(s)` : ""}${Object.keys(vars).length ? ` \xB7 ${Object.entries(vars).map(([k, v]) => `${k}=${v}`).join(" ")}` : ""}
361
+ `);
362
+ return 0;
363
+ }
364
+ async function renderOnce(filePath, out, frame, vars, scale, steps, scaleAuto, noLibs = false, params = {}) {
365
+ let doc;
366
+ try {
367
+ doc = loadDoc(filePath, noLibs);
368
+ } catch (e) {
369
+ process.stderr.write(`flatc: cannot read: ${e.message}
370
+ `);
371
+ return 1;
372
+ }
373
+ const dur = doc.timeline?.durationFrames ?? 0;
374
+ if (dur > 0 && frame >= dur) process.stderr.write(`flatc: --frame ${frame} is past the timeline (durationFrames ${dur}); the playhead WRAPS, so this renders frame ${frame % dur}
375
+ `);
376
+ const outPath = out ? resolve(out) : join(dirname(filePath), basename(filePath, extname(filePath)) + ".png");
377
+ return renderDocToFile(doc, outPath, frame, vars, scaleAuto ? autoScale(doc.width, doc.height) : scale, steps, params);
378
+ }
379
+ var gcd = (a, b) => {
380
+ a = Math.abs(a);
381
+ b = Math.abs(b);
382
+ while (b) {
383
+ const t = a % b;
384
+ a = b;
385
+ b = t;
386
+ }
387
+ return a || 1;
388
+ };
389
+ var lcm = (a, b) => Math.max(1, Math.round(a / gcd(a, b) * b));
390
+ function* instancesOf(sym) {
391
+ const stack = [];
392
+ for (const l of sym.layers) stack.push(...l.items);
393
+ for (let it = stack.pop(); it; it = stack.pop()) {
394
+ if (isInstance(it)) yield it;
395
+ else if (isGroup(it)) for (const l of it.layers) stack.push(...l.items);
396
+ }
397
+ }
398
+ function previewDuration(root, symbols) {
399
+ const byId = new Map(symbols.map((s) => [s.id, s]));
400
+ const seen = /* @__PURE__ */ new Set();
401
+ let seam = Math.max(1, root.timeline?.durationFrames ?? 1);
402
+ let longest = seam;
403
+ const walk = (sym) => {
404
+ if (!sym || seen.has(sym.id)) return;
405
+ seen.add(sym.id);
406
+ for (const it of instancesOf(sym)) {
407
+ const child = byId.get(it.symbolId);
408
+ const cdur = Math.max(1, child?.timeline?.durationFrames ?? 1);
409
+ const mode = it.playback?.mode;
410
+ if (mode === "independent") {
411
+ seam = lcm(seam, cdur);
412
+ longest = Math.max(longest, cdur);
413
+ } else if (mode === "once") longest = Math.max(longest, cdur);
414
+ walk(child);
415
+ }
416
+ };
417
+ walk(root);
418
+ let dur = seam;
419
+ while (dur < longest) dur += seam;
420
+ return Math.min(dur, 6e3);
421
+ }
422
+ function buildPreviewDoc(flatPath, symbolName, pad, bboxMode, setSpec = {}) {
423
+ const { symbols } = parseFlatLib(readFileSync(flatPath, "utf8"));
424
+ if (!symbols.length) throw new Error("no symbols found in this .flat library");
425
+ let symbol = symbols[0];
426
+ if (symbolName) {
427
+ const found = symbols.find((s) => s.name === symbolName);
428
+ if (!found) throw new Error(`symbol "${symbolName}" not found \u2014 available: ${symbols.map((s) => s.name).join(", ")}`);
429
+ symbol = found;
430
+ }
431
+ const instance = { id: "preview_instance", kind: "instance", name: symbol.name, transform: { ...IDENTITY }, symbolId: symbol.id };
432
+ const layer = { id: "preview_layer", name: "preview", visible: true, locked: false, opacity: 1, items: [instance] };
433
+ const tl = symbol.timeline;
434
+ const dur = previewDuration(symbol, symbols);
435
+ const timeline = { fps: tl?.fps ?? 24, durationFrames: dur, tracks: [] };
436
+ let doc = { width: 1, height: 1, layers: [layer], symbols, timeline };
437
+ const sampleFrames = (n) => {
438
+ const step = Math.max(1, Math.ceil(n / 240));
439
+ const fs = [];
440
+ for (let f = 0; f < n; f += step) fs.push(f);
441
+ if (fs[fs.length - 1] !== n - 1) fs.push(n - 1);
442
+ return fs;
443
+ };
444
+ const bb = bboxMode === "all" ? containerBBoxUnion(doc, instance, sampleFrames(dur)) : containerBBox(doc, instance, 0);
445
+ if (bb) {
446
+ doc = { ...doc, width: Math.max(1, Math.ceil(bb.maxX - bb.minX) + pad * 2), height: Math.max(1, Math.ceil(bb.maxY - bb.minY) + pad * 2) };
447
+ instance.transform = { ...IDENTITY, e: pad - bb.minX, f: pad - bb.minY };
448
+ } else {
449
+ doc = { ...doc, width: 512, height: 512 };
450
+ instance.transform = { ...IDENTITY, e: 256, f: 256 };
451
+ }
452
+ const entries = Object.entries(setSpec);
453
+ if (entries.length) {
454
+ const known = /* @__PURE__ */ new Set([...(symbol.states ?? []).map((s) => s.param), ...(symbol.params ?? []).map((p) => p.name)]);
455
+ for (const [k, v] of entries) {
456
+ if (!known.has(k)) process.stderr.write(`flatc: --set ${k}: "${symbol.name}" exposes no such param (${[...known].join(", ") || "none"})
457
+ `);
458
+ else (instance.params ??= {})[k] = v;
459
+ }
460
+ }
461
+ return { doc, symbol, others: symbols.filter((s) => s !== symbol).map((s) => s.name) };
462
+ }
463
+ async function previewOnce(flatPath, symbolName, out, frame, vars, scale, steps, doRender, pad, bboxMode, setSpec, scaleAuto) {
464
+ let built;
465
+ try {
466
+ built = buildPreviewDoc(flatPath, symbolName, pad, bboxMode, setSpec);
467
+ } catch (e) {
468
+ process.stderr.write(`flatc: ${e.message}
469
+ `);
470
+ return 1;
471
+ }
472
+ const { doc, symbol, others } = built;
473
+ const stem = `${basename(flatPath, extname(flatPath))}.${symbol.name}`;
474
+ let code;
475
+ if (doRender) {
476
+ const sc = scaleAuto ? autoScale(doc.width - 2 * pad, doc.height - 2 * pad) : scale;
477
+ code = await renderDocToFile(doc, out ? resolve(out) : join(dirname(flatPath), stem + ".png"), frame, vars, sc, steps);
478
+ } else {
479
+ const outPath = out ? resolve(out) : join(dirname(flatPath), stem + ".flatpack");
480
+ writeFileSync(outPath, packToJSON(doc));
481
+ process.stdout.write(`flatc: ${basename(outPath)} \u2713 preview of "${symbol.name}" \xB7 ${doc.width}\xD7${doc.height} \xB7 play it in the browser player
482
+ `);
483
+ code = 0;
484
+ }
485
+ if (!symbolName && others.length) process.stderr.write(`flatc: previewed "${symbol.name}"; other symbols in this lib: ${others.join(", ")} (pick with --symbol <name>)
486
+ `);
487
+ return code;
488
+ }
489
+ function run(argv) {
490
+ const args = argv.slice(2);
491
+ let out = "", scriptPath = "", symbolName = "";
492
+ let checkOnly = false, doFix = false, doWatch = false, doPlay = false, doRender = false, doTrace = false, doPreview = false, noLibs = false;
493
+ let frame = 0, scale = 2, steps = 0, pad = 24;
494
+ let scaleAuto = false;
495
+ let bboxMode = "all";
496
+ let assetMode = "inline";
497
+ const vars = {};
498
+ const setSpec = {};
499
+ const positional = [];
500
+ for (let i = 0; i < args.length; i++) {
501
+ const a = args[i];
502
+ if (a === "-o" || a === "--out") out = args[++i] ?? "";
503
+ else if (a === "--script") scriptPath = resolve(args[++i] ?? "");
504
+ else if (a === "--assets") assetMode = args[++i] === "external" ? "external" : "inline";
505
+ else if (a === "--check") checkOnly = true;
506
+ else if (a === "--fix") {
507
+ doFix = true;
508
+ checkOnly = true;
509
+ } else if (a === "--no-libs") noLibs = true;
510
+ else if (a === "--watch") doWatch = true;
511
+ else if (a === "--play") doPlay = true;
512
+ else if (a === "--trace") doTrace = true;
513
+ else if (a === "--render") doRender = true;
514
+ else if (a === "--preview") doPreview = true;
515
+ else if (a === "--symbol") symbolName = args[++i] ?? "";
516
+ else if (a === "--pad") pad = Math.max(0, Number(args[++i] ?? "24") || 0);
517
+ else if (a === "--bbox") bboxMode = args[++i] === "frame0" ? "frame0" : "all";
518
+ else if (a === "--frame") frame = Number(args[++i] ?? "0") || 0;
519
+ else if (a === "--steps") steps = Math.max(0, Number(args[++i] ?? "0") || 0);
520
+ else if (a === "--scale") {
521
+ const v = args[++i] ?? "2";
522
+ if (v === "auto") scaleAuto = true;
523
+ else scale = Number(v) || 2;
524
+ } else if (a === "--at") parseVars(args[++i] ?? "", vars);
525
+ else if (a === "--set") for (const pair of (args[++i] ?? "").split(",")) {
526
+ const j = pair.indexOf("=");
527
+ if (j > 0) setSpec[pair.slice(0, j).trim()] = pair.slice(j + 1).trim();
528
+ }
529
+ else if (a === "-h" || a === "--help") {
530
+ printHelp();
531
+ return 0;
532
+ } else positional.push(a);
533
+ }
534
+ if (!positional.length) {
535
+ printHelp();
536
+ return 1;
537
+ }
538
+ const filePath = resolve(positional[0]);
539
+ if (!existsSync(filePath)) {
540
+ process.stderr.write(`flatc: not found: ${filePath}
541
+ `);
542
+ return 1;
543
+ }
544
+ const explicitFlats = positional.slice(1);
545
+ if (doPreview) return previewOnce(filePath, symbolName, out, frame, vars, scale, steps, doRender, pad, bboxMode, setSpec, scaleAuto);
546
+ if (doRender) return renderOnce(filePath, out, frame, vars, scale, steps, scaleAuto, noLibs, setSpec);
547
+ if (doPlay) return playOnce(filePath, scriptPath, doTrace, noLibs);
548
+ const action = checkOnly && filePath.endsWith(".flat") ? () => checkFlatLibs([filePath, ...explicitFlats]) : () => compileOnce(filePath, explicitFlats, out, checkOnly, assetMode, noLibs, doFix);
549
+ if (doWatch) {
550
+ const code = action();
551
+ const baseDir = dirname(filePath);
552
+ let timer;
553
+ watch(baseDir, { recursive: false }, (_e, filename) => {
554
+ if (filename && (filename.endsWith(".flatpack") || filename.endsWith(".flatpack.json"))) return;
555
+ clearTimeout(timer);
556
+ timer = setTimeout(action, 80);
557
+ });
558
+ process.stdout.write(`flatc: watching ${baseDir} \u2026 (Ctrl+C to stop)
559
+ `);
560
+ return code;
561
+ }
562
+ return action();
563
+ }
6
564
  export {
7
565
  run
8
566
  };