@flatkit/compiler 0.1.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/bin/flatc.mjs +23 -0
- package/dist/chunk-EHQSVWOD.js +678 -0
- package/dist/chunk-EHQSVWOD.js.map +1 -0
- package/dist/cli/flatc.d.ts +3 -0
- package/dist/cli/flatc.js +7 -0
- package/dist/cli/flatc.js.map +1 -0
- package/dist/cli/render.d.ts +12 -0
- package/dist/cli/render.js +77 -0
- package/dist/cli/render.js.map +1 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.js +175 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
// src/cli/flatc.ts
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, watch, mkdirSync, copyFileSync } from "fs";
|
|
3
|
+
import { resolve, dirname, basename, extname, join, relative, isAbsolute } from "path";
|
|
4
|
+
|
|
5
|
+
// src/compile.ts
|
|
6
|
+
import { parseFlatLib, parseProgramFull } from "@flatkit/engine/flatFormat";
|
|
7
|
+
import { isGroup, isInstance } from "@flatkit/engine/layers";
|
|
8
|
+
function resolveRefs(layers, byName) {
|
|
9
|
+
const walk = (items) => {
|
|
10
|
+
for (const it of items) {
|
|
11
|
+
if (isInstance(it) && it.symbolId.startsWith("@")) it.symbolId = byName.get(it.symbolId.slice(1)) ?? it.symbolId;
|
|
12
|
+
if (isGroup(it)) it.layers.forEach((l) => walk(l.items));
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
layers.forEach((l) => walk(l.items));
|
|
16
|
+
}
|
|
17
|
+
function compileFlatpack(programSrc, assetSrcs = [], media = {}) {
|
|
18
|
+
const libs = assetSrcs.map((src) => parseFlatLib(src));
|
|
19
|
+
const symbols = libs.flatMap((l) => l.symbols);
|
|
20
|
+
const folders = libs.flatMap((l) => l.folders);
|
|
21
|
+
const prog = parseProgramFull(programSrc);
|
|
22
|
+
const byName = new Map(symbols.map((s) => [s.name, s.id]));
|
|
23
|
+
symbols.forEach((s) => resolveRefs(s.layers, byName));
|
|
24
|
+
resolveRefs(prog.layers, byName);
|
|
25
|
+
const assets = (prog.assets ?? []).map((a) => {
|
|
26
|
+
const m = media[a.data];
|
|
27
|
+
return m ? { ...a, mime: m.mime, data: m.data } : a;
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
width: prog.width,
|
|
31
|
+
height: prog.height,
|
|
32
|
+
...prog.background ? { background: prog.background } : {},
|
|
33
|
+
symbols,
|
|
34
|
+
...folders.length ? { folders } : {},
|
|
35
|
+
layers: prog.layers,
|
|
36
|
+
...prog.variables && Object.keys(prog.variables).length ? { variables: prog.variables } : {},
|
|
37
|
+
...prog.imports?.length ? { imports: prog.imports } : {},
|
|
38
|
+
...prog.functions?.length ? { functions: prog.functions } : {},
|
|
39
|
+
timeline: prog.timeline ?? { fps: 24, durationFrames: 60, tracks: [] },
|
|
40
|
+
...prog.interactions?.length ? { interactions: prog.interactions } : {},
|
|
41
|
+
...prog.interactors?.length ? { interactors: prog.interactors } : {},
|
|
42
|
+
...assets.length ? { assets } : {}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
var packToJSON = (doc) => JSON.stringify(doc);
|
|
46
|
+
|
|
47
|
+
// src/cli/flatc.ts
|
|
48
|
+
import { parseProgramFull as parseProgramFull2 } from "@flatkit/engine/flatFormat";
|
|
49
|
+
import { hasPackage } from "@flatkit/engine/stdlib";
|
|
50
|
+
import { parseUnits as parseUnits3 } from "@flatkit/engine/dsl";
|
|
51
|
+
import { sanitizeDoc } from "@flatkit/engine/validateDoc";
|
|
52
|
+
import { unitsToFunctions } from "@flatkit/engine/scriptDoc";
|
|
53
|
+
|
|
54
|
+
// src/programDoc.ts
|
|
55
|
+
import { printUnits } from "@flatkit/engine/dsl";
|
|
56
|
+
|
|
57
|
+
// src/scopeProgram.ts
|
|
58
|
+
function matchBrace(s, open) {
|
|
59
|
+
let depth = 0;
|
|
60
|
+
for (let i = open; i < s.length; i++) {
|
|
61
|
+
const c = s[i];
|
|
62
|
+
if (c === '"') {
|
|
63
|
+
i++;
|
|
64
|
+
while (i < s.length && s[i] !== '"') {
|
|
65
|
+
if (s[i] === "\\") i++;
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (c === "/" && s[i + 1] === "/") {
|
|
72
|
+
while (i < s.length && s[i] !== "\n") i++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (c === "{") depth++;
|
|
76
|
+
else if (c === "}") {
|
|
77
|
+
depth--;
|
|
78
|
+
if (depth === 0) return i;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return -1;
|
|
82
|
+
}
|
|
83
|
+
function splitScopeProgram(text) {
|
|
84
|
+
let rest = "";
|
|
85
|
+
let i = 0;
|
|
86
|
+
const objects = [];
|
|
87
|
+
while (i < text.length) {
|
|
88
|
+
const m = /object\s+"((?:[^"\\]|\\.)*)"\s*\{/.exec(text.slice(i));
|
|
89
|
+
if (!m) {
|
|
90
|
+
rest += text.slice(i);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
const start = i + m.index;
|
|
94
|
+
rest += text.slice(i, start);
|
|
95
|
+
const ob = start + m[0].length - 1;
|
|
96
|
+
const oc = matchBrace(text, ob);
|
|
97
|
+
if (oc < 0) {
|
|
98
|
+
rest += text.slice(start);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
objects.push({ name: m[1].replace(/\\(.)/g, (_, c) => c === "n" ? "\n" : c), body: text.slice(ob + 1, oc) });
|
|
102
|
+
i = oc + 1;
|
|
103
|
+
}
|
|
104
|
+
return { rest, objects };
|
|
105
|
+
}
|
|
106
|
+
function scopeRegions(text) {
|
|
107
|
+
const lineAt = (off) => text.slice(0, off).split("\n").length;
|
|
108
|
+
const regions = [];
|
|
109
|
+
let i = 0;
|
|
110
|
+
let restStart = 0;
|
|
111
|
+
while (i < text.length) {
|
|
112
|
+
const m = /object\s+"((?:[^"\\]|\\.)*)"\s*\{/.exec(text.slice(i));
|
|
113
|
+
if (!m) break;
|
|
114
|
+
const start = i + m.index;
|
|
115
|
+
const gap = text.slice(restStart, start);
|
|
116
|
+
if (gap.trim()) regions.push({ body: gap, line: lineAt(restStart) });
|
|
117
|
+
const ob = start + m[0].length - 1;
|
|
118
|
+
const oc = matchBrace(text, ob);
|
|
119
|
+
if (oc < 0) break;
|
|
120
|
+
regions.push({ body: text.slice(ob + 1, oc), line: lineAt(ob + 1) });
|
|
121
|
+
i = oc + 1;
|
|
122
|
+
restStart = i;
|
|
123
|
+
}
|
|
124
|
+
const tail = text.slice(restStart);
|
|
125
|
+
if (tail.trim()) regions.push({ body: tail, line: lineAt(restStart) });
|
|
126
|
+
return regions;
|
|
127
|
+
}
|
|
128
|
+
var esc = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
129
|
+
var indent = (body) => body.split("\n").map((l) => l.trim() ? " " + l : l).join("\n");
|
|
130
|
+
function formatObjectBlock(name, body) {
|
|
131
|
+
const inner = body.trim();
|
|
132
|
+
return `object "${esc(name)}" {
|
|
133
|
+
${inner ? indent(inner) + "\n" : ""}}`;
|
|
134
|
+
}
|
|
135
|
+
function joinScopeProgram(rest, objects) {
|
|
136
|
+
const parts = [];
|
|
137
|
+
if (rest.trim()) parts.push(rest.trim());
|
|
138
|
+
for (const o of objects) if (o.body.trim()) parts.push(formatObjectBlock(o.name, o.body));
|
|
139
|
+
return parts.length ? parts.join("\n\n") + "\n" : "";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/programDoc.ts
|
|
143
|
+
import { functionsToUnits, importsToUnits, objectToUnits, timelineToUnits, variablesToUnits } from "@flatkit/engine/scriptDoc";
|
|
144
|
+
import { contextLayers, getScopeTimeline, isContainer, isGroup as isGroup2, isText, isImage } from "@flatkit/engine/layers";
|
|
145
|
+
import { importedFunctions } from "@flatkit/engine/stdlib";
|
|
146
|
+
import { objectNames } from "@flatkit/engine/sceneRefs";
|
|
147
|
+
import { itemBBox, dropZoneBounds } from "@flatkit/engine/groups";
|
|
148
|
+
import { bboxIntersects } from "@flatkit/engine/bbox";
|
|
149
|
+
|
|
150
|
+
// src/lint.ts
|
|
151
|
+
import { analyzeExpr, STD_CONSTANTS, STD_FUNCTIONS, STD_IDS, STD_OBJECTS } from "@flatkit/engine/expr";
|
|
152
|
+
import { packageFunctionNames } from "@flatkit/engine/stdlib";
|
|
153
|
+
import { parseUnits } from "@flatkit/engine/dsl";
|
|
154
|
+
function collectAssigned(actions, into) {
|
|
155
|
+
for (const a of actions) {
|
|
156
|
+
if (a.do === "setVar") into.add(a.name);
|
|
157
|
+
else if (a.do === "if") {
|
|
158
|
+
collectAssigned(a.then, into);
|
|
159
|
+
if (a.else) collectAssigned(a.else, into);
|
|
160
|
+
} else if (a.do === "repeat") collectAssigned(a.body, into);
|
|
161
|
+
else if (a.do === "repeatRange") {
|
|
162
|
+
into.add(a.var);
|
|
163
|
+
collectAssigned(a.body, into);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function localVariables(units) {
|
|
168
|
+
const vars = /* @__PURE__ */ new Set();
|
|
169
|
+
for (const u of units) {
|
|
170
|
+
if (u.kind === "declare") vars.add(u.name);
|
|
171
|
+
else if (u.kind === "event" || u.kind === "frameActions") collectAssigned(u.body, vars);
|
|
172
|
+
else if (u.kind === "each") vars.add(u.as);
|
|
173
|
+
else if (u.kind === "interactor") {
|
|
174
|
+
if (u.varX) vars.add(u.varX);
|
|
175
|
+
if (u.varY) vars.add(u.varY);
|
|
176
|
+
} else if (u.kind === "drop") collectAssigned(u.body, vars);
|
|
177
|
+
else if (u.kind === "func") {
|
|
178
|
+
for (const p of u.func.params) vars.add(p);
|
|
179
|
+
if (u.func.kind === "proc") collectAssigned(u.func.body, vars);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return vars;
|
|
183
|
+
}
|
|
184
|
+
var TEXT_CALL = /\btext\s*\(/;
|
|
185
|
+
function lint(src, ctx = {}) {
|
|
186
|
+
const { units, diagnostics, sites } = parseUnits(src);
|
|
187
|
+
const out = [...diagnostics];
|
|
188
|
+
const variables = localVariables(units);
|
|
189
|
+
for (const v of ctx.variables ?? []) variables.add(v);
|
|
190
|
+
const knownIds = /* @__PURE__ */ new Set([...STD_IDS, ...STD_CONSTANTS, ...variables]);
|
|
191
|
+
const knownFns = new Set(STD_FUNCTIONS);
|
|
192
|
+
for (const u of units) {
|
|
193
|
+
if (u.kind === "func") knownFns.add(u.func.name);
|
|
194
|
+
else if (u.kind === "use") for (const name of packageFunctionNames(u.name)) knownFns.add(name);
|
|
195
|
+
}
|
|
196
|
+
for (const f of ctx.functions ?? []) knownFns.add(f);
|
|
197
|
+
const knownObjs = new Set(STD_OBJECTS);
|
|
198
|
+
for (const o of ctx.objects ?? []) knownObjs.add(o);
|
|
199
|
+
const labels = ctx.labels ? new Set(ctx.labels) : null;
|
|
200
|
+
for (const s of sites) {
|
|
201
|
+
if (s.kind === "expr") {
|
|
202
|
+
if (TEXT_CALL.test(s.text)) {
|
|
203
|
+
out.push({ line: s.line, col: s.col, message: 'text("\u2026") is only allowed as an argument to "send"' });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const a = analyzeExpr(s.text);
|
|
207
|
+
if (!a.ok) {
|
|
208
|
+
out.push({ line: s.line, col: s.col, message: `invalid expression: ${a.error}` });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
for (const fn of a.refs.calls) if (!knownFns.has(fn)) out.push({ line: s.line, col: s.col, message: `unknown function "${fn}"` });
|
|
212
|
+
for (const o of a.refs.members)
|
|
213
|
+
if (!knownObjs.has(o)) out.push({ line: s.line, col: s.col, message: `unknown object "${o}" (expected: ${[...knownObjs].join(", ")})` });
|
|
214
|
+
for (const id of a.refs.ids)
|
|
215
|
+
if (!knownIds.has(id))
|
|
216
|
+
out.push({ line: s.line, col: s.col, message: `unknown variable "${id}"${variables.size ? "" : ' \u2014 declare it with "let"'}` });
|
|
217
|
+
} else if (labels && !labels.has(s.name)) {
|
|
218
|
+
out.push({ line: s.line, col: s.col, message: `unknown label "${s.name}"` });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return out.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
222
|
+
}
|
|
223
|
+
function lintReport(src, ctx = {}) {
|
|
224
|
+
return lint(src, ctx).map((d) => `${d.line}:${d.col}: ${d.message}`).join("\n");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/programDoc.ts
|
|
228
|
+
import { parseUnits as parseUnits2 } from "@flatkit/engine/dsl";
|
|
229
|
+
function scopeProgram(doc, editPath = []) {
|
|
230
|
+
const atRoot = editPath.length === 0;
|
|
231
|
+
const items = contextLayers(doc, editPath).flatMap((l) => l.items);
|
|
232
|
+
const nameCount = /* @__PURE__ */ new Map();
|
|
233
|
+
for (const it of items) if (isContainer(it) && it.name) nameCount.set(it.name, (nameCount.get(it.name) ?? 0) + 1);
|
|
234
|
+
const scripted = (it) => !!doc.interactions?.some((i) => i.targetId === it.id) || !!doc.interactors?.some((i) => i.targetId === it.id) || !!(it.expressions && Object.keys(it.expressions).length);
|
|
235
|
+
const objects = items.filter((it) => isContainer(it) && !!it.name && nameCount.get(it.name) === 1 && scripted(it)).map((it) => ({ name: it.name, body: printUnits(objectToUnits(it.id, doc.interactions, it.expressions, doc.interactors)) }));
|
|
236
|
+
const imports = atRoot ? printUnits(importsToUnits(doc.imports)) : "";
|
|
237
|
+
const funcs = atRoot ? printUnits(functionsToUnits(doc.functions)) : "";
|
|
238
|
+
const globals = atRoot ? printUnits(variablesToUnits(doc.variables)) : "";
|
|
239
|
+
const scene = printUnits(timelineToUnits(getScopeTimeline(doc, editPath)));
|
|
240
|
+
const head = [imports, globals, funcs, scene].filter((t) => t.trim()).join("\n\n");
|
|
241
|
+
return joinScopeProgram(head, objects);
|
|
242
|
+
}
|
|
243
|
+
function docLintContext(doc, editPath = [], extraVars) {
|
|
244
|
+
return {
|
|
245
|
+
variables: [...Object.keys(doc.variables ?? {}), ...extraVars ?? []],
|
|
246
|
+
labels: (getScopeTimeline(doc, editPath)?.labels ?? []).map((l) => l.name),
|
|
247
|
+
functions: [...(doc.functions ?? []).map((f) => f.name), ...importedFunctions(doc.imports).map((f) => f.name)],
|
|
248
|
+
objects: objectNames(contextLayers(doc, editPath))
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function scopes(doc) {
|
|
252
|
+
return [{ label: "scene", editPath: [] }, ...doc.symbols.map((s) => ({ label: s.name, editPath: [{ kind: "symbol", symbolId: s.id, name: s.name }] }))];
|
|
253
|
+
}
|
|
254
|
+
function allScopeVariables(doc) {
|
|
255
|
+
const set = /* @__PURE__ */ new Set();
|
|
256
|
+
for (const { editPath } of scopes(doc))
|
|
257
|
+
for (const r of scopeRegions(scopeProgram(doc, editPath)))
|
|
258
|
+
for (const v of localVariables(parseUnits2(r.body).units)) set.add(v);
|
|
259
|
+
return [...set];
|
|
260
|
+
}
|
|
261
|
+
function itemNameById(doc, id) {
|
|
262
|
+
let found = null;
|
|
263
|
+
const walk = (layers) => {
|
|
264
|
+
for (const l of layers) for (const it of l.items) {
|
|
265
|
+
if (found) return;
|
|
266
|
+
if ("name" in it && it.id === id) {
|
|
267
|
+
found = it.name;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (isGroup2(it)) walk(it.layers);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
walk(doc.layers);
|
|
274
|
+
return found;
|
|
275
|
+
}
|
|
276
|
+
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
|
|
277
|
+
function docStructureWarnings(doc) {
|
|
278
|
+
const out = [];
|
|
279
|
+
const names = new Set(objectNames(doc.layers));
|
|
280
|
+
for (const it of doc.interactions ?? []) {
|
|
281
|
+
if (it.event === "drop" && it.over && !names.has(it.over)) {
|
|
282
|
+
const who = itemNameById(doc, it.targetId) ?? it.targetId;
|
|
283
|
+
out.push({ scope: "scene", diag: { line: 1, col: 1, severity: "warning", message: `unknown drop zone "${it.over}" (object "${who}") \u2014 no item of the scene carries this name` } });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const names0 = Object.keys(doc.variables ?? {});
|
|
287
|
+
if (names0.length) {
|
|
288
|
+
const allText = scopes(doc).map(({ editPath }) => scopeProgram(doc, editPath)).join("\n");
|
|
289
|
+
for (const name of names0) {
|
|
290
|
+
const m = allText.match(new RegExp(`(?<![\\w-])${escapeRe(name)}(?![\\w-])`, "g"));
|
|
291
|
+
if ((m?.length ?? 0) <= 1) out.push({ scope: "scene", diag: { line: 1, col: 1, severity: "warning", message: `global variable "${name}" never used (declared, but neither read nor written)` } });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
out.push(...docLayoutWarnings(doc));
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
var warn = (message) => ({ scope: "scene", diag: { line: 1, col: 1, severity: "warning", message } });
|
|
298
|
+
var itemLabel = (it) => "name" in it && it.name ? it.name : "kind" in it ? it.kind : "path";
|
|
299
|
+
function estTextWidth(t) {
|
|
300
|
+
const advance = (t.weight && t.weight >= 700 ? 0.56 : 0.52) * t.size;
|
|
301
|
+
let longest = 0;
|
|
302
|
+
for (const line of t.content.split("\n")) longest = Math.max(longest, line.length);
|
|
303
|
+
return longest * advance;
|
|
304
|
+
}
|
|
305
|
+
function docLayoutWarnings(doc) {
|
|
306
|
+
const out = [];
|
|
307
|
+
const W = doc.width, H = doc.height;
|
|
308
|
+
const TOL = 8;
|
|
309
|
+
const r0 = (n) => Math.round(n);
|
|
310
|
+
const dynamicPos = (it) => "expressions" in it && it.expressions && (it.expressions.x != null || it.expressions.y != null);
|
|
311
|
+
for (const layer of doc.layers) {
|
|
312
|
+
for (const it of layer.items) {
|
|
313
|
+
if (!(isText(it) || isImage(it)) || dynamicPos(it)) continue;
|
|
314
|
+
const b = itemBBox(doc, it);
|
|
315
|
+
if (!b) continue;
|
|
316
|
+
const visible = b.maxX > 0 && b.minX < W && b.maxY > 0 && b.minY < H;
|
|
317
|
+
const clipped = b.minX < -TOL || b.minY < -TOL || b.maxX > W + TOL || b.maxY > H + TOL;
|
|
318
|
+
if (visible && clipped) {
|
|
319
|
+
out.push(warn(`"${itemLabel(it)}" clipped at the canvas edge: bbox [${r0(b.minX)},${r0(b.minY)} -> ${r0(b.maxX)},${r0(b.maxY)}] outside 0,0 -> ${W},${H}`));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const it of doc.layers.flatMap((l) => l.items)) {
|
|
324
|
+
if (!isText(it) || it.wrap || dynamicPos(it)) continue;
|
|
325
|
+
const estW = estTextWidth(it);
|
|
326
|
+
const left = it.align === "left" ? 0 : it.align === "right" ? it.box.w - estW : it.box.w / 2 - estW / 2;
|
|
327
|
+
const wl = it.transform.e + left, wr = wl + estW;
|
|
328
|
+
if (wr > W + TOL || wl < -TOL) {
|
|
329
|
+
out.push(warn(`text "${it.content.slice(0, 24)}${it.content.length > 24 ? "\u2026" : ""}" overflows the canvas (estimated ~${r0(estW)} px, edge ${r0(wl)}->${r0(wr)} outside 0->${W}) \u2014 add "wrap"`));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const zones = [];
|
|
333
|
+
const collectZones = (layers) => {
|
|
334
|
+
for (const l of layers) for (const it of l.items) {
|
|
335
|
+
if (isGroup2(it) && it.hitbox && it.name) {
|
|
336
|
+
const b = dropZoneBounds(doc, it.name);
|
|
337
|
+
if (b) zones.push({ name: it.name, b });
|
|
338
|
+
}
|
|
339
|
+
if (isGroup2(it)) collectZones(it.layers);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
collectZones(doc.layers);
|
|
343
|
+
for (let i = 0; i < zones.length; i++) for (let j = i + 1; j < zones.length; j++) {
|
|
344
|
+
if (bboxIntersects(zones[i].b, zones[j].b)) out.push(warn(`overlapping hitboxes: "${zones[i].name}" and "${zones[j].name}" -> ambiguous drop`));
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
function lintDoc(doc) {
|
|
349
|
+
const out = [];
|
|
350
|
+
const allVars = allScopeVariables(doc);
|
|
351
|
+
for (const { label, editPath } of scopes(doc)) {
|
|
352
|
+
const ctx = docLintContext(doc, editPath, allVars);
|
|
353
|
+
for (const r of scopeRegions(scopeProgram(doc, editPath))) for (const d of lint(r.body, ctx)) out.push({ scope: label, diag: { ...d, line: d.line + r.line - 1 } });
|
|
354
|
+
}
|
|
355
|
+
out.push(...docStructureWarnings(doc));
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
var sev = (d) => d.severity === "warning" ? "warning" : "error";
|
|
359
|
+
function lintDocReport(doc) {
|
|
360
|
+
return lintDoc(doc).map(({ scope, diag }) => `[${scope}] ${diag.line}:${diag.col}: ${sev(diag)}: ${diag.message}`).join("\n");
|
|
361
|
+
}
|
|
362
|
+
function docHasErrors(doc) {
|
|
363
|
+
return lintDoc(doc).some(({ diag }) => sev(diag) === "error");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/cli/flatc.ts
|
|
367
|
+
import { playHeadless } from "@flatkit/player/debug";
|
|
368
|
+
var MIME = {
|
|
369
|
+
".png": "image/png",
|
|
370
|
+
".jpg": "image/jpeg",
|
|
371
|
+
".jpeg": "image/jpeg",
|
|
372
|
+
".gif": "image/gif",
|
|
373
|
+
".webp": "image/webp",
|
|
374
|
+
".svg": "image/svg+xml",
|
|
375
|
+
".avif": "image/avif",
|
|
376
|
+
".mp3": "audio/mpeg",
|
|
377
|
+
".wav": "audio/wav",
|
|
378
|
+
".ogg": "audio/ogg",
|
|
379
|
+
".m4a": "audio/mp4",
|
|
380
|
+
// Fonts (RFC 8081 media types) — matches the editor's import (`font/woff2` default, FontFace API).
|
|
381
|
+
".woff2": "font/woff2",
|
|
382
|
+
".woff": "font/woff",
|
|
383
|
+
".ttf": "font/ttf",
|
|
384
|
+
".otf": "font/otf"
|
|
385
|
+
// NB: video (.mp4/.webm/…) intentionally omitted — no video runtime in the editor or player yet.
|
|
386
|
+
};
|
|
387
|
+
var mimeFor = (path) => MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
388
|
+
function isWithin(baseDir, target) {
|
|
389
|
+
const rel = relative(baseDir, target);
|
|
390
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
391
|
+
}
|
|
392
|
+
function printHelp() {
|
|
393
|
+
process.stdout.write(`flatc \u2014 compile a FlatInk program into a .flatpack
|
|
394
|
+
|
|
395
|
+
Usage:
|
|
396
|
+
flatc <program.flatink> [assets.flat \u2026] [-o output.flatpack]
|
|
397
|
+
flatc <program.flatink> --watch
|
|
398
|
+
flatc --play <program.flatink | scene.flatpack> --script <gestures.json>
|
|
399
|
+
flatc --render <program.flatink | scene.flatpack> -o out.png [--frame N] [--at k=v[,k2=v2]] [--scale S]
|
|
400
|
+
|
|
401
|
+
program.flatink the program (composition + logic, text DSL)
|
|
402
|
+
assets.flat visual asset libs (default: every .flat in the program's folder)
|
|
403
|
+
-o, --out output file (default: <program>.flatpack \u2014 the CANONICAL name, JSON inside)
|
|
404
|
+
--assets MODE media baking: 'inline' (default, base64 in the .flatpack) or 'external'
|
|
405
|
+
(sidecar <out>.assets/ folder; asset.data = relative key \u2014 serve the folder and
|
|
406
|
+
play with sameOriginAssetResolver(<flatpackUrl>))
|
|
407
|
+
--check semantic lint only (no .flatpack); exits \u22600 on ERROR (warnings do not stop)
|
|
408
|
+
--watch recompile on every change in the folder (agent \u2192 player loop)
|
|
409
|
+
--play run the file WITHOUT a canvas, replay --script and print { sends, vars } (JSON)
|
|
410
|
+
--trace (with --play) HUMAN-READABLE log per gesture: emitted sends + variable diff (debug)
|
|
411
|
+
--script <f> JSON gesture script: [{ "type": "down|move|up|cancel", "x", "y" }, { "type": "set", "name", "value" }, { "type": "wait", "frames": N }]
|
|
412
|
+
semantic (by NAME, the engine resolves coords): { "type": "drag", "source", "target" } \xB7 { "type": "tap", "target" }
|
|
413
|
+
\xB7 { "type": "scratch", "target" } (sweeps a reveal zone) \xB7 { "type": "connect", "source", "target" } (pulls a link wire)
|
|
414
|
+
"wait" lets the simulation run N fixed steps (60 Hz): "every frame" + playhead advance like in real playback
|
|
415
|
+
"expect" self-verifies: { "type": "expect", "sends": ["done"], "vars": { "score": 3 } } \u2192 exits \u22600 on mismatch
|
|
416
|
+
(sends = sequence of names emitted SINCE the last expect; vars = current state). Great in CI.
|
|
417
|
+
--render render a PNG IMAGE (headless skia): see what we draw (positioning)
|
|
418
|
+
--frame N (with --render) target frame (default 0)
|
|
419
|
+
--at k=v[,k2=v2] (with --render) force variables \u2192 capture a given state (e.g. a step of an escape)
|
|
420
|
+
--steps N (with --render) run N fixed sim steps (60 Hz, every-frame) BEFORE capture \u2192 see a
|
|
421
|
+
stateful act unfold without forcing every derived variable by hand in --at
|
|
422
|
+
--scale S (with --render) resolution factor (default 2)
|
|
423
|
+
-h, --help this help
|
|
424
|
+
|
|
425
|
+
Media referenced by 'asset "id" "path" kind' are embedded (paths relative to the program).
|
|
426
|
+
`);
|
|
427
|
+
}
|
|
428
|
+
function buildDocFromProgram(programPath, explicitFlats = [], assetMode = "inline", assetsDir = "") {
|
|
429
|
+
const baseDir = dirname(programPath);
|
|
430
|
+
const programSrc = readFileSync(programPath, "utf8");
|
|
431
|
+
const prog = parseProgramFull2(programSrc);
|
|
432
|
+
const flatPaths = new Set(explicitFlats.map((p) => resolve(p)));
|
|
433
|
+
if (!flatPaths.size) for (const f of readdirSync(baseDir).filter((f2) => f2.endsWith(".flat"))) flatPaths.add(join(baseDir, f));
|
|
434
|
+
const pkgFunctions = [];
|
|
435
|
+
const localResolved = /* @__PURE__ */ new Set();
|
|
436
|
+
for (const name of prog.imports ?? []) {
|
|
437
|
+
if (hasPackage(name)) continue;
|
|
438
|
+
const fink = join(baseDir, name + ".flatink");
|
|
439
|
+
const fflat = join(baseDir, name + ".flat");
|
|
440
|
+
if (!isWithin(baseDir, fink) || !isWithin(baseDir, fflat)) {
|
|
441
|
+
process.stderr.write(`flatc: package outside the program folder (ignored): "${name}"
|
|
442
|
+
`);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
let found = false;
|
|
446
|
+
if (existsSync(fink)) {
|
|
447
|
+
for (const f of unitsToFunctions(parseUnits3(readFileSync(fink, "utf8")).units)) pkgFunctions.push(f, { ...f, name: `${name}.${f.name}` });
|
|
448
|
+
found = true;
|
|
449
|
+
}
|
|
450
|
+
if (existsSync(fflat)) {
|
|
451
|
+
flatPaths.add(fflat);
|
|
452
|
+
found = true;
|
|
453
|
+
}
|
|
454
|
+
if (found) localResolved.add(name);
|
|
455
|
+
else process.stderr.write(`flatc: package not found: "${name}" (neither stdlib, nor ${name}.flatink / ${name}.flat)
|
|
456
|
+
`);
|
|
457
|
+
}
|
|
458
|
+
const assetSrcs = [...flatPaths].map((p) => readFileSync(p, "utf8"));
|
|
459
|
+
const media = {};
|
|
460
|
+
const mediaCopies = [];
|
|
461
|
+
for (const a of prog.assets ?? []) {
|
|
462
|
+
const mp = resolve(baseDir, a.data);
|
|
463
|
+
if (!isWithin(baseDir, mp)) {
|
|
464
|
+
process.stderr.write(`flatc: media outside the program folder (ignored): ${a.data}
|
|
465
|
+
`);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (!existsSync(mp)) {
|
|
469
|
+
process.stderr.write(`flatc: missing media (ignored): ${a.data}
|
|
470
|
+
`);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const mime = mimeFor(mp);
|
|
474
|
+
if (assetMode === "external") {
|
|
475
|
+
const key = `${assetsDir}/${a.data.replace(/\\/g, "/")}`;
|
|
476
|
+
media[a.data] = { mime, data: key };
|
|
477
|
+
mediaCopies.push({ src: mp, key });
|
|
478
|
+
} else {
|
|
479
|
+
media[a.data] = { mime, data: `data:${mime};base64,${readFileSync(mp).toString("base64")}` };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
let doc = compileFlatpack(programSrc, assetSrcs, media);
|
|
483
|
+
if (pkgFunctions.length) doc = { ...doc, functions: [...doc.functions ?? [], ...pkgFunctions] };
|
|
484
|
+
const stdImports = (doc.imports ?? []).filter(hasPackage);
|
|
485
|
+
doc = { ...doc, imports: stdImports.length ? stdImports : void 0 };
|
|
486
|
+
return { doc, flatLibs: flatPaths.size, packages: localResolved.size, media: Object.keys(media).length, mediaCopies };
|
|
487
|
+
}
|
|
488
|
+
function compileOnce(programPath, explicitFlats, out, checkOnly, assetMode = "inline") {
|
|
489
|
+
const outPath = out ? resolve(out) : join(dirname(programPath), basename(programPath, extname(programPath)) + ".flatpack");
|
|
490
|
+
const assetsDir = assetMode === "external" ? basename(outPath, extname(outPath)) + ".assets" : "";
|
|
491
|
+
let built;
|
|
492
|
+
try {
|
|
493
|
+
built = buildDocFromProgram(programPath, explicitFlats, assetMode, assetsDir);
|
|
494
|
+
} catch (e) {
|
|
495
|
+
process.stderr.write(`flatc: compile error: ${e.message}
|
|
496
|
+
`);
|
|
497
|
+
return 1;
|
|
498
|
+
}
|
|
499
|
+
const { doc } = built;
|
|
500
|
+
const report = lintDocReport(doc);
|
|
501
|
+
if (checkOnly) {
|
|
502
|
+
if (report) process.stderr.write(report + "\n");
|
|
503
|
+
if (docHasErrors(doc)) return 1;
|
|
504
|
+
process.stdout.write("flatc: no errors \u2713\n");
|
|
505
|
+
return 0;
|
|
506
|
+
}
|
|
507
|
+
if (report) process.stderr.write(report + "\n");
|
|
508
|
+
writeFileSync(outPath, packToJSON(doc));
|
|
509
|
+
const outDir = dirname(outPath);
|
|
510
|
+
for (const c of built.mediaCopies) {
|
|
511
|
+
const dest = join(outDir, ...c.key.split("/"));
|
|
512
|
+
if (!isWithin(outDir, dest)) continue;
|
|
513
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
514
|
+
copyFileSync(c.src, dest);
|
|
515
|
+
}
|
|
516
|
+
const where = assetMode === "external" ? ` (external \u2192 ${assetsDir}/)` : "";
|
|
517
|
+
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}
|
|
518
|
+
`);
|
|
519
|
+
return 0;
|
|
520
|
+
}
|
|
521
|
+
function loadDoc(filePath) {
|
|
522
|
+
if (filePath.endsWith(".flatink")) return buildDocFromProgram(filePath).doc;
|
|
523
|
+
return sanitizeDoc(JSON.parse(readFileSync(filePath, "utf8")));
|
|
524
|
+
}
|
|
525
|
+
function playOnce(filePath, scriptPath, trace) {
|
|
526
|
+
if (!scriptPath) {
|
|
527
|
+
process.stderr.write("flatc: --play requires --script <gestures.json>\n");
|
|
528
|
+
return 1;
|
|
529
|
+
}
|
|
530
|
+
if (!existsSync(scriptPath)) {
|
|
531
|
+
process.stderr.write(`flatc: script not found: ${scriptPath}
|
|
532
|
+
`);
|
|
533
|
+
return 1;
|
|
534
|
+
}
|
|
535
|
+
let doc, gestures;
|
|
536
|
+
try {
|
|
537
|
+
doc = loadDoc(filePath);
|
|
538
|
+
} catch (e) {
|
|
539
|
+
process.stderr.write(`flatc: cannot read: ${e.message}
|
|
540
|
+
`);
|
|
541
|
+
return 1;
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
gestures = JSON.parse(readFileSync(scriptPath, "utf8"));
|
|
545
|
+
} catch (e) {
|
|
546
|
+
process.stderr.write(`flatc: invalid JSON script: ${e.message}
|
|
547
|
+
`);
|
|
548
|
+
return 1;
|
|
549
|
+
}
|
|
550
|
+
if (!Array.isArray(gestures)) {
|
|
551
|
+
process.stderr.write("flatc: the script must be an array of gestures\n");
|
|
552
|
+
return 1;
|
|
553
|
+
}
|
|
554
|
+
const res = playHeadless(doc, gestures, { trace });
|
|
555
|
+
if (!trace) process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
556
|
+
else
|
|
557
|
+
for (const s of res.steps ?? []) {
|
|
558
|
+
const sends = s.sends.length ? " sends:[" + s.sends.map((e) => e.value !== void 0 ? `${e.name}=${e.value}` : e.name).join(", ") + "]" : "";
|
|
559
|
+
const vars = Object.entries(s.changed).map(([k, [a, b]]) => `${k}:${JSON.stringify(a)}\u2192${JSON.stringify(b)}`);
|
|
560
|
+
process.stdout.write(`${s.gesture.padEnd(28)}${sends}${vars.length ? " vars{" + vars.join(" ") + "}" : ""}
|
|
561
|
+
`);
|
|
562
|
+
}
|
|
563
|
+
if (res.expectFailures?.length) {
|
|
564
|
+
for (const f of res.expectFailures) process.stderr.write(`flatc: \u2717 ${f}
|
|
565
|
+
`);
|
|
566
|
+
return 1;
|
|
567
|
+
}
|
|
568
|
+
return 0;
|
|
569
|
+
}
|
|
570
|
+
function parseVars(spec, into) {
|
|
571
|
+
for (const pair of spec.split(",")) {
|
|
572
|
+
const i = pair.indexOf("=");
|
|
573
|
+
if (i < 0) continue;
|
|
574
|
+
const k = pair.slice(0, i).trim();
|
|
575
|
+
const v = Number(pair.slice(i + 1).trim());
|
|
576
|
+
if (k && Number.isFinite(v)) into[k] = v;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async function renderOnce(filePath, out, frame, vars, scale, steps) {
|
|
580
|
+
let doc;
|
|
581
|
+
try {
|
|
582
|
+
doc = loadDoc(filePath);
|
|
583
|
+
} catch (e) {
|
|
584
|
+
process.stderr.write(`flatc: cannot read: ${e.message}
|
|
585
|
+
`);
|
|
586
|
+
return 1;
|
|
587
|
+
}
|
|
588
|
+
const outPath = out ? resolve(out) : join(dirname(filePath), basename(filePath, extname(filePath)) + ".png");
|
|
589
|
+
try {
|
|
590
|
+
const { renderDocToPng } = await import("./cli/render.js");
|
|
591
|
+
const png = await renderDocToPng(doc, { frame, vars: Object.keys(vars).length ? vars : void 0, scale, steps: steps || void 0 });
|
|
592
|
+
writeFileSync(outPath, png);
|
|
593
|
+
} catch (e) {
|
|
594
|
+
process.stderr.write(`flatc: render failed: ${e.message}
|
|
595
|
+
`);
|
|
596
|
+
return 1;
|
|
597
|
+
}
|
|
598
|
+
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(" ")}` : ""}
|
|
599
|
+
`);
|
|
600
|
+
return 0;
|
|
601
|
+
}
|
|
602
|
+
function run(argv) {
|
|
603
|
+
const args = argv.slice(2);
|
|
604
|
+
let out = "", scriptPath = "";
|
|
605
|
+
let checkOnly = false, doWatch = false, doPlay = false, doRender = false, doTrace = false;
|
|
606
|
+
let frame = 0, scale = 2, steps = 0;
|
|
607
|
+
let assetMode = "inline";
|
|
608
|
+
const vars = {};
|
|
609
|
+
const positional = [];
|
|
610
|
+
for (let i = 0; i < args.length; i++) {
|
|
611
|
+
const a = args[i];
|
|
612
|
+
if (a === "-o" || a === "--out") out = args[++i] ?? "";
|
|
613
|
+
else if (a === "--script") scriptPath = resolve(args[++i] ?? "");
|
|
614
|
+
else if (a === "--assets") assetMode = args[++i] === "external" ? "external" : "inline";
|
|
615
|
+
else if (a === "--check") checkOnly = true;
|
|
616
|
+
else if (a === "--watch") doWatch = true;
|
|
617
|
+
else if (a === "--play") doPlay = true;
|
|
618
|
+
else if (a === "--trace") doTrace = true;
|
|
619
|
+
else if (a === "--render") doRender = true;
|
|
620
|
+
else if (a === "--frame") frame = Number(args[++i] ?? "0") || 0;
|
|
621
|
+
else if (a === "--steps") steps = Math.max(0, Number(args[++i] ?? "0") || 0);
|
|
622
|
+
else if (a === "--scale") scale = Number(args[++i] ?? "2") || 2;
|
|
623
|
+
else if (a === "--at") parseVars(args[++i] ?? "", vars);
|
|
624
|
+
else if (a === "-h" || a === "--help") {
|
|
625
|
+
printHelp();
|
|
626
|
+
return 0;
|
|
627
|
+
} else positional.push(a);
|
|
628
|
+
}
|
|
629
|
+
if (!positional.length) {
|
|
630
|
+
printHelp();
|
|
631
|
+
return 1;
|
|
632
|
+
}
|
|
633
|
+
const filePath = resolve(positional[0]);
|
|
634
|
+
if (!existsSync(filePath)) {
|
|
635
|
+
process.stderr.write(`flatc: not found: ${filePath}
|
|
636
|
+
`);
|
|
637
|
+
return 1;
|
|
638
|
+
}
|
|
639
|
+
const explicitFlats = positional.slice(1);
|
|
640
|
+
if (doRender) return renderOnce(filePath, out, frame, vars, scale, steps);
|
|
641
|
+
if (doPlay) return playOnce(filePath, scriptPath, doTrace);
|
|
642
|
+
if (doWatch) {
|
|
643
|
+
const code = compileOnce(filePath, explicitFlats, out, checkOnly, assetMode);
|
|
644
|
+
const baseDir = dirname(filePath);
|
|
645
|
+
let timer;
|
|
646
|
+
watch(baseDir, { recursive: false }, (_e, filename) => {
|
|
647
|
+
if (filename && (filename.endsWith(".flatpack") || filename.endsWith(".flatpack.json"))) return;
|
|
648
|
+
clearTimeout(timer);
|
|
649
|
+
timer = setTimeout(() => compileOnce(filePath, explicitFlats, out, checkOnly, assetMode), 80);
|
|
650
|
+
});
|
|
651
|
+
process.stdout.write(`flatc: watching ${baseDir} \u2026 (Ctrl+C to stop)
|
|
652
|
+
`);
|
|
653
|
+
return code;
|
|
654
|
+
}
|
|
655
|
+
return compileOnce(filePath, explicitFlats, out, checkOnly, assetMode);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export {
|
|
659
|
+
compileFlatpack,
|
|
660
|
+
packToJSON,
|
|
661
|
+
localVariables,
|
|
662
|
+
lint,
|
|
663
|
+
lintReport,
|
|
664
|
+
splitScopeProgram,
|
|
665
|
+
scopeRegions,
|
|
666
|
+
formatObjectBlock,
|
|
667
|
+
joinScopeProgram,
|
|
668
|
+
scopeProgram,
|
|
669
|
+
docLintContext,
|
|
670
|
+
allScopeVariables,
|
|
671
|
+
docStructureWarnings,
|
|
672
|
+
docLayoutWarnings,
|
|
673
|
+
lintDoc,
|
|
674
|
+
lintDocReport,
|
|
675
|
+
docHasErrors,
|
|
676
|
+
run
|
|
677
|
+
};
|
|
678
|
+
//# sourceMappingURL=chunk-EHQSVWOD.js.map
|