@flatkit/compiler 0.22.0 → 0.24.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/analysis.d.ts +57 -9
- package/dist/analysis.js +8 -1
- package/dist/{chunk-AVUH5CVJ.js → chunk-5WQFNGZQ.js} +43 -16
- package/dist/chunk-5WQFNGZQ.js.map +1 -0
- package/dist/{chunk-VB3ZWCNP.js → chunk-7KBWT2PA.js} +378 -126
- package/dist/chunk-7KBWT2PA.js.map +1 -0
- package/dist/cli/flatc.js +2 -2
- package/dist/index.d.ts +40 -6
- package/dist/index.js +198 -7
- package/dist/index.js.map +1 -1
- package/package.json +12 -8
- package/prompts/README.md +45 -0
- package/prompts/flatink-core.md +273 -0
- package/prompts/flatink-lite.md +132 -0
- package/prompts/role-asset-creator.md +97 -0
- package/prompts/role-coder.md +180 -0
- package/prompts/role-motion-designer.md +171 -0
- package/dist/chunk-AVUH5CVJ.js.map +0 -1
- package/dist/chunk-VB3ZWCNP.js.map +0 -1
|
@@ -1,82 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { parseUnits } from "@flatkit/engine/dsl";
|
|
5
|
-
function collectAssigned(actions, into) {
|
|
6
|
-
for (const a of actions) {
|
|
7
|
-
if (a.do === "setVar") into.add(a.name);
|
|
8
|
-
else if (a.do === "if") {
|
|
9
|
-
collectAssigned(a.then, into);
|
|
10
|
-
if (a.else) collectAssigned(a.else, into);
|
|
11
|
-
} else if (a.do === "repeat") collectAssigned(a.body, into);
|
|
12
|
-
else if (a.do === "repeatRange") {
|
|
13
|
-
into.add(a.var);
|
|
14
|
-
collectAssigned(a.body, into);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
function localVariables(units) {
|
|
19
|
-
const vars = /* @__PURE__ */ new Set();
|
|
20
|
-
for (const u of units) {
|
|
21
|
-
if (u.kind === "declare") vars.add(u.name);
|
|
22
|
-
else if (u.kind === "event" || u.kind === "frameActions") collectAssigned(u.body, vars);
|
|
23
|
-
else if (u.kind === "each") vars.add(u.as);
|
|
24
|
-
else if (u.kind === "interactor") {
|
|
25
|
-
if (u.varX) vars.add(u.varX);
|
|
26
|
-
if (u.varY) vars.add(u.varY);
|
|
27
|
-
} else if (u.kind === "drop") collectAssigned(u.body, vars);
|
|
28
|
-
else if (u.kind === "func") {
|
|
29
|
-
for (const p of u.func.params) vars.add(p);
|
|
30
|
-
if (u.func.kind === "proc") collectAssigned(u.func.body, vars);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return vars;
|
|
34
|
-
}
|
|
35
|
-
var TEXT_CALL = /\btext\s*\(/;
|
|
36
|
-
var SECOND_ASSIGN = /[\w\]]\s*=(?![=])/;
|
|
37
|
-
function lint(src, ctx = {}) {
|
|
38
|
-
const { units, diagnostics, sites } = parseUnits(src);
|
|
39
|
-
const out = [...diagnostics];
|
|
40
|
-
const variables = localVariables(units);
|
|
41
|
-
for (const v of ctx.variables ?? []) variables.add(v);
|
|
42
|
-
const knownIds = /* @__PURE__ */ new Set([...STD_IDS, ...STD_CONSTANTS, ...variables]);
|
|
43
|
-
const knownFns = new Set(STD_FUNCTIONS);
|
|
44
|
-
knownFns.add("velocity");
|
|
45
|
-
for (const u of units) {
|
|
46
|
-
if (u.kind === "func") knownFns.add(u.func.name);
|
|
47
|
-
else if (u.kind === "use") for (const name of packageFunctionNames(u.name)) knownFns.add(name);
|
|
48
|
-
}
|
|
49
|
-
for (const f of ctx.functions ?? []) knownFns.add(f);
|
|
50
|
-
const knownObjs = new Set(STD_OBJECTS);
|
|
51
|
-
for (const o of ctx.objects ?? []) knownObjs.add(o);
|
|
52
|
-
const labels = ctx.labels ? new Set(ctx.labels) : null;
|
|
53
|
-
for (const s of sites) {
|
|
54
|
-
if (s.kind === "expr") {
|
|
55
|
-
if (TEXT_CALL.test(s.text)) {
|
|
56
|
-
out.push({ line: s.line, col: s.col, message: 'text("\u2026") is only allowed as an argument to "send"' });
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
const a = analyzeExpr(s.text);
|
|
60
|
-
if (!a.ok) {
|
|
61
|
-
const message = SECOND_ASSIGN.test(s.text) ? "two statements on one line \u2014 put each on its own line (FlatInk ends a statement at the newline)" : `invalid expression: ${a.error}`;
|
|
62
|
-
out.push({ line: s.line, col: s.col, message });
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
for (const fn of a.refs.calls) if (!knownFns.has(fn)) out.push({ line: s.line, col: s.col, message: `unknown function "${fn}"` });
|
|
66
|
-
for (const o of a.refs.members)
|
|
67
|
-
if (!knownObjs.has(o)) out.push({ line: s.line, col: s.col, message: `unknown object "${o}" (expected: ${[...knownObjs].join(", ")})` });
|
|
68
|
-
for (const id of a.refs.ids)
|
|
69
|
-
if (!knownIds.has(id))
|
|
70
|
-
out.push({ line: s.line, col: s.col, message: `unknown variable "${id}"${variables.size ? "" : ' \u2014 declare it with "let"'}` });
|
|
71
|
-
} else if (labels && !labels.has(s.name)) {
|
|
72
|
-
out.push({ line: s.line, col: s.col, message: `unknown label "${s.name}"` });
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return out.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
76
|
-
}
|
|
77
|
-
function lintReport(src, ctx = {}) {
|
|
78
|
-
return lint(src, ctx).map((d) => `${d.line}:${d.col}: ${d.message}`).join("\n");
|
|
79
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
compileFlatpack
|
|
3
|
+
} from "./chunk-KXKX2RMX.js";
|
|
80
4
|
|
|
81
5
|
// src/scopeProgram.ts
|
|
82
6
|
function matchBrace(s, open) {
|
|
@@ -163,16 +87,170 @@ function joinScopeProgram(rest, objects) {
|
|
|
163
87
|
return parts.length ? parts.join("\n\n") + "\n" : "";
|
|
164
88
|
}
|
|
165
89
|
|
|
90
|
+
// src/lint.ts
|
|
91
|
+
import { analyzeExpr, STD_CONSTANTS, STD_FUNCTIONS, STD_IDS, STD_OBJECTS } from "@flatkit/engine/expr";
|
|
92
|
+
import { packageFunctionNames } from "@flatkit/engine/stdlib";
|
|
93
|
+
import { parseUnits } from "@flatkit/engine/dsl";
|
|
94
|
+
function collectAssigned(actions, into) {
|
|
95
|
+
for (const a of actions) {
|
|
96
|
+
if (a.do === "setVar") into.add(a.name);
|
|
97
|
+
else if (a.do === "if") {
|
|
98
|
+
collectAssigned(a.then, into);
|
|
99
|
+
if (a.else) collectAssigned(a.else, into);
|
|
100
|
+
} else if (a.do === "repeat") collectAssigned(a.body, into);
|
|
101
|
+
else if (a.do === "repeatRange") {
|
|
102
|
+
into.add(a.var);
|
|
103
|
+
collectAssigned(a.body, into);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function localVariables(units) {
|
|
108
|
+
const vars = /* @__PURE__ */ new Set();
|
|
109
|
+
for (const u of units) {
|
|
110
|
+
if (u.kind === "declare") vars.add(u.name);
|
|
111
|
+
else if (u.kind === "event" || u.kind === "frameActions") collectAssigned(u.body, vars);
|
|
112
|
+
else if (u.kind === "each") vars.add(u.as);
|
|
113
|
+
else if (u.kind === "interactor") {
|
|
114
|
+
if (u.varX) vars.add(u.varX);
|
|
115
|
+
if (u.varY) vars.add(u.varY);
|
|
116
|
+
} else if (u.kind === "drop") collectAssigned(u.body, vars);
|
|
117
|
+
else if (u.kind === "func") {
|
|
118
|
+
for (const p of u.func.params) vars.add(p);
|
|
119
|
+
if (u.func.kind === "proc") collectAssigned(u.func.body, vars);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return vars;
|
|
123
|
+
}
|
|
124
|
+
var TEXT_CALL = /\btext\s*\(/;
|
|
125
|
+
var SECOND_ASSIGN = /[\w\]]\s*=(?![=])/;
|
|
126
|
+
var SECOND_ACTION = /\b(send|play|pause|sound|set|go|repeat)\b/;
|
|
127
|
+
function lint(src, ctx = {}) {
|
|
128
|
+
const { units, diagnostics, sites } = parseUnits(src);
|
|
129
|
+
const out = [...diagnostics];
|
|
130
|
+
const variables = localVariables(units);
|
|
131
|
+
for (const v of ctx.variables ?? []) variables.add(v);
|
|
132
|
+
const knownIds = /* @__PURE__ */ new Set([...STD_IDS, ...STD_CONSTANTS, ...variables]);
|
|
133
|
+
const knownFns = new Set(STD_FUNCTIONS);
|
|
134
|
+
knownFns.add("velocity");
|
|
135
|
+
for (const u of units) {
|
|
136
|
+
if (u.kind === "func") knownFns.add(u.func.name);
|
|
137
|
+
else if (u.kind === "use") for (const name of packageFunctionNames(u.name)) knownFns.add(name);
|
|
138
|
+
}
|
|
139
|
+
for (const f of ctx.functions ?? []) knownFns.add(f);
|
|
140
|
+
const knownObjs = new Set(STD_OBJECTS);
|
|
141
|
+
for (const o of ctx.objects ?? []) knownObjs.add(o);
|
|
142
|
+
const labels = ctx.labels ? new Set(ctx.labels) : null;
|
|
143
|
+
for (const s of sites) {
|
|
144
|
+
if (s.kind === "expr") {
|
|
145
|
+
if (TEXT_CALL.test(s.text)) {
|
|
146
|
+
out.push({ line: s.line, col: s.col, message: 'text("\u2026") is only allowed as an argument to "send"' });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const a = analyzeExpr(s.text);
|
|
150
|
+
if (!a.ok) {
|
|
151
|
+
const leftover = SECOND_ASSIGN.test(s.text) ? "an assignment" : SECOND_ACTION.test(s.text) ? `\`${SECOND_ACTION.exec(s.text)[1]} \u2026\`` : "";
|
|
152
|
+
const message = leftover ? `two statements on one line \u2014 ${leftover} was swallowed into the expression before it. FlatInk ends a statement at the NEWLINE: put each action on its own line.` : `invalid expression: ${a.error}`;
|
|
153
|
+
out.push({ line: s.line, col: s.col, message });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
for (const fn of a.refs.calls) if (!knownFns.has(fn)) out.push({ line: s.line, col: s.col, message: `unknown function "${fn}"` });
|
|
157
|
+
for (const o of a.refs.members)
|
|
158
|
+
if (!knownObjs.has(o)) out.push({ line: s.line, col: s.col, message: `unknown object "${o}" (expected: ${[...knownObjs].join(", ")})` });
|
|
159
|
+
for (const id of a.refs.ids)
|
|
160
|
+
if (!knownIds.has(id))
|
|
161
|
+
out.push({ line: s.line, col: s.col, message: `unknown variable "${id}"${variables.size ? "" : ' \u2014 declare it with "let"'}` });
|
|
162
|
+
} else if (labels && !labels.has(s.name)) {
|
|
163
|
+
out.push({ line: s.line, col: s.col, message: `unknown label "${s.name}"` });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
167
|
+
}
|
|
168
|
+
function lintReport(src, ctx = {}) {
|
|
169
|
+
return lint(src, ctx).map((d) => `${d.line}:${d.col}: ${d.message}`).join("\n");
|
|
170
|
+
}
|
|
171
|
+
|
|
166
172
|
// src/programDoc.ts
|
|
167
173
|
import { printUnits } from "@flatkit/engine/dsl";
|
|
168
174
|
import { functionsToUnits, importsToUnits, objectToUnits, timelineToUnits, variablesToUnits } from "@flatkit/engine/scriptDoc";
|
|
169
|
-
import { contextLayers, getScopeTimeline, isContainer, isGroup, isText, isImage, isPoseable, isRegion } from "@flatkit/engine/layers";
|
|
175
|
+
import { contextLayers, getScopeTimeline, isContainer, isGroup as isGroup2, isInstance, isText, isImage, isPoseable as isPoseable2, isRegion } from "@flatkit/engine/layers";
|
|
170
176
|
import { importedFunctions } from "@flatkit/engine/stdlib";
|
|
171
|
-
import { EXPR_CHANNELS } from "@flatkit/engine/timeline";
|
|
177
|
+
import { EXPR_CHANNELS as EXPR_CHANNELS2 } from "@flatkit/engine/timeline";
|
|
172
178
|
import { objectNames } from "@flatkit/engine/sceneRefs";
|
|
173
|
-
import {
|
|
179
|
+
import { behaviorRegions } from "@flatkit/engine/flatFormat";
|
|
180
|
+
import { itemBBox, dropZoneBounds, transformBBox } from "@flatkit/engine/groups";
|
|
181
|
+
import { IDENTITY, apply, compose } from "@flatkit/engine/transform";
|
|
174
182
|
import { makePathSampler } from "@flatkit/engine/path";
|
|
175
183
|
import { bboxIntersects } from "@flatkit/engine/bbox";
|
|
184
|
+
|
|
185
|
+
// src/docWalk.ts
|
|
186
|
+
import { isGroup, isPoseable } from "@flatkit/engine/layers";
|
|
187
|
+
import { EXPR_CHANNELS } from "@flatkit/engine/timeline";
|
|
188
|
+
function forEachActionIn(actions, fn) {
|
|
189
|
+
for (const a of actions ?? []) {
|
|
190
|
+
fn(a);
|
|
191
|
+
if (a.do === "if") {
|
|
192
|
+
forEachActionIn(a.then, fn);
|
|
193
|
+
forEachActionIn(a.else, fn);
|
|
194
|
+
} else if (a.do === "repeat" || a.do === "repeatRange") forEachActionIn(a.body, fn);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function forEachAction(doc, fn) {
|
|
198
|
+
for (const i of doc.interactions ?? []) forEachActionIn(i.actions, fn);
|
|
199
|
+
for (const f of doc.functions ?? []) if (f.kind === "proc") forEachActionIn(f.body, fn);
|
|
200
|
+
for (const t of [doc.timeline, ...(doc.symbols ?? []).map((s) => s.timeline)]) {
|
|
201
|
+
if (!t) continue;
|
|
202
|
+
forEachActionIn(t.onLoad, fn);
|
|
203
|
+
forEachActionIn(t.onEnterFrame, fn);
|
|
204
|
+
for (const fa of t.frameActions ?? []) forEachActionIn(fa.actions, fn);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function forEachActionExpression(actions, fn) {
|
|
208
|
+
forEachActionIn(actions, (a) => {
|
|
209
|
+
if (a.do === "setVar" || a.do === "setParam") fn(a.value);
|
|
210
|
+
else if (a.do === "setIndex") {
|
|
211
|
+
fn(a.value);
|
|
212
|
+
fn(a.index);
|
|
213
|
+
} else if (a.do === "if") fn(a.cond);
|
|
214
|
+
else if (a.do === "repeat") fn(a.count);
|
|
215
|
+
else if (a.do === "repeatRange") {
|
|
216
|
+
fn(a.from);
|
|
217
|
+
fn(a.to);
|
|
218
|
+
} else if (a.do === "call") for (const arg of a.args) fn(arg);
|
|
219
|
+
else if (a.do === "send" && a.payload?.kind === "expr") fn(a.payload.expr);
|
|
220
|
+
else if (a.do === "send" && a.payload?.kind === "record") for (const f of a.payload.fields) fn(f.expr);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function forEachItemExpression(it, fn) {
|
|
224
|
+
if (!isPoseable(it)) return;
|
|
225
|
+
if (it.expressions) for (const k of Object.keys(it.expressions)) {
|
|
226
|
+
const e = it.expressions[k];
|
|
227
|
+
if (e) fn(e, k);
|
|
228
|
+
}
|
|
229
|
+
if (it.modifiers) for (const ch of EXPR_CHANNELS) {
|
|
230
|
+
const m = it.modifiers[ch];
|
|
231
|
+
if (m) fn(m.target, ch);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function forEachExpression(doc, fn) {
|
|
235
|
+
const visit = (items) => {
|
|
236
|
+
for (const it of items) {
|
|
237
|
+
forEachItemExpression(it, (e) => fn(e));
|
|
238
|
+
if (isGroup(it)) for (const l of it.layers) visit(l.items);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
for (const l of doc.layers) visit(l.items);
|
|
242
|
+
for (const s of doc.symbols ?? []) for (const l of s.layers) visit(l.items);
|
|
243
|
+
for (const i of doc.interactions ?? []) forEachActionExpression(i.actions, fn);
|
|
244
|
+
for (const f of doc.functions ?? []) if (f.kind === "proc") forEachActionExpression(f.body, fn);
|
|
245
|
+
for (const t of [doc.timeline, ...(doc.symbols ?? []).map((s) => s.timeline)]) {
|
|
246
|
+
if (!t) continue;
|
|
247
|
+
forEachActionExpression(t.onLoad, fn);
|
|
248
|
+
forEachActionExpression(t.onEnterFrame, fn);
|
|
249
|
+
for (const fa of t.frameActions ?? []) forEachActionExpression(fa.actions, fn);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/programDoc.ts
|
|
176
254
|
import { parseUnits as parseUnits2 } from "@flatkit/engine/dsl";
|
|
177
255
|
function scopeProgram(doc, editPath = []) {
|
|
178
256
|
const atRoot = editPath.length === 0;
|
|
@@ -224,13 +302,66 @@ function itemNameById(doc, id) {
|
|
|
224
302
|
found = it.name ?? null;
|
|
225
303
|
return;
|
|
226
304
|
}
|
|
227
|
-
if (
|
|
305
|
+
if (isGroup2(it)) walk(it.layers);
|
|
228
306
|
}
|
|
229
307
|
};
|
|
230
308
|
walk(doc.layers);
|
|
231
309
|
return found;
|
|
232
310
|
}
|
|
233
311
|
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
|
|
312
|
+
function functionsReadingTime(doc) {
|
|
313
|
+
const defs = [...doc.functions ?? [], ...importedFunctions(doc.imports)].filter((f) => f.kind === "value");
|
|
314
|
+
const reads = /* @__PURE__ */ new Set();
|
|
315
|
+
for (; ; ) {
|
|
316
|
+
const before = reads.size;
|
|
317
|
+
for (const f of defs) {
|
|
318
|
+
if (reads.has(f.name)) continue;
|
|
319
|
+
const body = f.expr;
|
|
320
|
+
if (!f.params.includes("time") && /\btime\b/.test(body)) {
|
|
321
|
+
reads.add(f.name);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
for (const other of reads) if (new RegExp(`\\b${escapeRe(other)}\\s*\\(`).test(body)) {
|
|
325
|
+
reads.add(f.name);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (reads.size === before) return [...reads];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function callArguments(src, name) {
|
|
333
|
+
const out = [];
|
|
334
|
+
const re = new RegExp(`(?<![\\w.])${escapeRe(name)}\\s*\\(`, "g");
|
|
335
|
+
let m;
|
|
336
|
+
while (m = re.exec(src)) {
|
|
337
|
+
const args = [];
|
|
338
|
+
let depth = 1;
|
|
339
|
+
let start = m.index + m[0].length;
|
|
340
|
+
let i = start;
|
|
341
|
+
for (; i < src.length; i++) {
|
|
342
|
+
const c = src[i];
|
|
343
|
+
if (c === "(") depth++;
|
|
344
|
+
else if (c === ")") {
|
|
345
|
+
depth--;
|
|
346
|
+
if (depth === 0) break;
|
|
347
|
+
} else if (c === "," && depth === 1) {
|
|
348
|
+
args.push(src.slice(start, i));
|
|
349
|
+
start = i + 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
args.push(src.slice(start, i));
|
|
353
|
+
out.push(args.map((a) => a.trim()));
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
var INSTANT_ARG = { pulse: 0, shake: 1 };
|
|
358
|
+
function timeCapturedVars(doc) {
|
|
359
|
+
const out = /* @__PURE__ */ new Set();
|
|
360
|
+
forEachAction(doc, (a) => {
|
|
361
|
+
if (a.do === "setVar" && /\btime\b/.test(a.value) && !/\bclock\b/.test(a.value)) out.add(a.name);
|
|
362
|
+
});
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
234
365
|
function docStructureWarnings(doc) {
|
|
235
366
|
const out = [];
|
|
236
367
|
const names = new Set(objectNames(doc.layers));
|
|
@@ -245,11 +376,11 @@ function docStructureWarnings(doc) {
|
|
|
245
376
|
const modTargets = [];
|
|
246
377
|
const collectMods = (items) => {
|
|
247
378
|
for (const it of items) {
|
|
248
|
-
if (
|
|
379
|
+
if (isPoseable2(it) && it.modifiers) for (const ch of EXPR_CHANNELS2) {
|
|
249
380
|
const mm = it.modifiers[ch];
|
|
250
381
|
if (mm) modTargets.push(mm.target);
|
|
251
382
|
}
|
|
252
|
-
if (
|
|
383
|
+
if (isGroup2(it)) for (const l of it.layers) collectMods(l.items);
|
|
253
384
|
}
|
|
254
385
|
};
|
|
255
386
|
for (const l of doc.layers) collectMods(l.items);
|
|
@@ -260,22 +391,60 @@ function docStructureWarnings(doc) {
|
|
|
260
391
|
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)` } });
|
|
261
392
|
}
|
|
262
393
|
}
|
|
394
|
+
const missing = /* @__PURE__ */ new Set();
|
|
395
|
+
const scanRefs = (items) => {
|
|
396
|
+
for (const it of items) {
|
|
397
|
+
if (isInstance(it) && it.symbolId.startsWith("@")) missing.add(it.symbolId.slice(1));
|
|
398
|
+
if (isGroup2(it)) for (const l of it.layers) scanRefs(l.items);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
for (const l of doc.layers) scanRefs(l.items);
|
|
402
|
+
for (const s of doc.symbols ?? []) for (const l of s.layers) scanRefs(l.items);
|
|
403
|
+
if (missing.size) {
|
|
404
|
+
const names2 = [...missing].map((n) => `"${n}"`).join(", ");
|
|
405
|
+
out.push(warn(`instance resolves to no symbol: ${names2} \u2014 nothing is drawn where it stands. Pass the .flat library that defines it (\`flatc <program> <lib.flat>\`, or place it beside the program), or fix the name.`));
|
|
406
|
+
}
|
|
263
407
|
const dur = doc.timeline?.durationFrames ?? 60;
|
|
264
408
|
const fps = doc.timeline?.fps ?? 24;
|
|
265
409
|
if (dur <= 120) {
|
|
266
|
-
|
|
410
|
+
const viaFn = functionsReadingTime(doc);
|
|
411
|
+
const culprits = /* @__PURE__ */ new Set();
|
|
267
412
|
const scan = (layers) => {
|
|
268
413
|
for (const l of layers) for (const it of l.items) {
|
|
269
|
-
if (
|
|
270
|
-
|
|
414
|
+
if (isPoseable2(it) && it.expressions) for (const k in it.expressions) {
|
|
415
|
+
const expr = it.expressions[k] ?? "";
|
|
416
|
+
if (/\btime\b/.test(expr)) culprits.add("`time`");
|
|
417
|
+
for (const fn of viaFn) if (new RegExp(`\\b${escapeRe(fn)}\\s*\\(`).test(expr)) culprits.add(`\`${fn}()\` (which reads \`time\`)`);
|
|
271
418
|
}
|
|
272
|
-
if (
|
|
419
|
+
if (isGroup2(it)) scan(it.layers);
|
|
273
420
|
}
|
|
274
421
|
};
|
|
275
422
|
scan(doc.layers);
|
|
276
|
-
if (
|
|
423
|
+
if (culprits.size) {
|
|
277
424
|
const secs = Math.round(dur / fps * 10) / 10;
|
|
278
|
-
out.push(warn(`a channel expression uses
|
|
425
|
+
out.push(warn(`a channel expression uses ${[...culprits].join(" and ")}, but the timeline loops every ${secs}s (durationFrames ${dur}) \u2014 \`time\` resets each loop, so the motion jumps (a one-shot ramp REPLAYS forever). Use the monotone \`clock\` (never wraps), drive it by \`frame/${dur}\`, or set a longer \`timeline\`.`));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const captured = timeCapturedVars(doc);
|
|
429
|
+
if (captured.size) {
|
|
430
|
+
const readers = /* @__PURE__ */ new Map();
|
|
431
|
+
forEachExpression(doc, (expr) => {
|
|
432
|
+
for (const fnName of Object.keys(INSTANT_ARG)) {
|
|
433
|
+
for (const args of callArguments(expr, fnName)) {
|
|
434
|
+
const arg = args[INSTANT_ARG[fnName]];
|
|
435
|
+
if (!arg) continue;
|
|
436
|
+
for (const v of captured) {
|
|
437
|
+
if (!new RegExp(`(?<![\\w-])${escapeRe(v)}(?![\\w-])`).test(arg)) continue;
|
|
438
|
+
const seen = readers.get(v) ?? /* @__PURE__ */ new Set();
|
|
439
|
+
seen.add(fnName);
|
|
440
|
+
readers.set(v, seen);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
for (const [v, fns] of readers) {
|
|
446
|
+
const who = [...fns].map((f) => `\`${f}()\``).join(" and ");
|
|
447
|
+
out.push(warn(`"${v}" is captured on \`time\` but read as an INSTANT by ${who}, which compares it against the monotone \`clock\` \u2014 the two axes never meet, so the ramp never fires and nothing on screen says so. Capture it with \`clock\` instead: \`${v} = clock\`.`));
|
|
279
448
|
}
|
|
280
449
|
}
|
|
281
450
|
out.push(...docPaintParamWarnings(doc));
|
|
@@ -295,9 +464,9 @@ function docCelLayerWarnings(doc) {
|
|
|
295
464
|
const posed = /* @__PURE__ */ new Set();
|
|
296
465
|
for (const c of l.cels) for (const p of c.poses) posed.add(p.id);
|
|
297
466
|
for (const id of posed) if (id.startsWith("@")) push(`layer "${l.name}": \`pose "${id.slice(1)}"\` matches no item of this layer \u2014 the pose draws nothing (roster: ${l.items.map(itemLabel).join(", ") || "empty"}).`);
|
|
298
|
-
for (const it of l.items) if (
|
|
467
|
+
for (const it of l.items) if (isPoseable2(it) && !posed.has(it.id)) push(`layer "${l.name}": "${itemLabel(it)}" is never posed by a cel \u2014 it is never drawn. Add \`pose "${itemLabel(it)}"\` to the cels where it should show.`);
|
|
299
468
|
}
|
|
300
|
-
for (const it of l.items) if (
|
|
469
|
+
for (const it of l.items) if (isGroup2(it)) visit(it.layers);
|
|
301
470
|
}
|
|
302
471
|
};
|
|
303
472
|
visit(layers);
|
|
@@ -323,8 +492,8 @@ function docPaintParamWarnings(doc) {
|
|
|
323
492
|
const stops = it.paint && it.paint.type !== "solid" ? it.paint.stops : [];
|
|
324
493
|
for (const s of stops) if (s.param) flag(s.param, "a gradient stop", who);
|
|
325
494
|
}
|
|
326
|
-
if (
|
|
327
|
-
if (
|
|
495
|
+
if (isPoseable2(it) && it.tint?.param) flag(it.tint.param, "a tint", who);
|
|
496
|
+
if (isGroup2(it)) for (const l of it.layers) for (const c of l.items) visit(c);
|
|
328
497
|
};
|
|
329
498
|
for (const l of layers) for (const it of l.items) visit(it);
|
|
330
499
|
};
|
|
@@ -334,44 +503,85 @@ function docPaintParamWarnings(doc) {
|
|
|
334
503
|
}
|
|
335
504
|
var warn = (message) => ({ scope: "scene", diag: { line: 1, col: 1, severity: "warning", message } });
|
|
336
505
|
var itemLabel = (it) => "name" in it && it.name ? it.name : "kind" in it ? it.kind : "path";
|
|
506
|
+
var glyphAdvance = (t) => (t.weight && t.weight >= 700 ? 0.56 : 0.52) * t.size;
|
|
337
507
|
function estTextWidth(t) {
|
|
338
|
-
const advance = (t
|
|
508
|
+
const advance = glyphAdvance(t);
|
|
339
509
|
let longest = 0;
|
|
340
510
|
for (const line of t.content.split("\n")) longest = Math.max(longest, line.length);
|
|
341
511
|
return longest * advance;
|
|
342
512
|
}
|
|
513
|
+
function wrapMetrics(t) {
|
|
514
|
+
const advance = glyphAdvance(t);
|
|
515
|
+
const width = (s) => s.length * advance;
|
|
516
|
+
let lines = 0;
|
|
517
|
+
let widestWord = 0;
|
|
518
|
+
for (const paragraph of t.content.split("\n")) {
|
|
519
|
+
lines++;
|
|
520
|
+
let current = "";
|
|
521
|
+
for (const word of paragraph.split(/\s+/).filter(Boolean)) {
|
|
522
|
+
widestWord = Math.max(widestWord, width(word));
|
|
523
|
+
const joined = current ? `${current} ${word}` : word;
|
|
524
|
+
if (current && width(joined) > t.box.w) {
|
|
525
|
+
lines++;
|
|
526
|
+
current = word;
|
|
527
|
+
} else current = joined;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return { lines, widestWord };
|
|
531
|
+
}
|
|
343
532
|
function docLayoutWarnings(doc) {
|
|
344
533
|
const out = [];
|
|
345
534
|
const W = doc.width, H = doc.height;
|
|
346
535
|
const TOL = 8;
|
|
347
536
|
const r0 = (n) => Math.round(n);
|
|
348
|
-
const dynamicPos = (it) => "expressions" in it && it.expressions && (it.expressions.x != null || it.expressions.y != null || it.expressions.dx != null || it.expressions.dy != null);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
537
|
+
const dynamicPos = (it) => "expressions" in it && it.expressions && (it.expressions.x != null || it.expressions.y != null || it.expressions.dx != null || it.expressions.dy != null) || !!doc.interactors?.some((i) => i.targetId === it.id);
|
|
538
|
+
const placed = [];
|
|
539
|
+
const collect = (layers, matrix, dynamic) => {
|
|
540
|
+
for (const l of layers) for (const it of l.items) {
|
|
541
|
+
if (isText(it) || isImage(it)) placed.push({ it, parent: matrix, dynamic: dynamic || dynamicPos(it) });
|
|
542
|
+
if (isGroup2(it)) collect(it.layers, compose(matrix, it.transform), dynamic || dynamicPos(it));
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
collect(doc.layers, IDENTITY, false);
|
|
546
|
+
for (const { it, parent, dynamic } of placed) {
|
|
547
|
+
if (dynamic || isText(it) && it.textPath) continue;
|
|
548
|
+
const local = itemBBox(doc, it);
|
|
549
|
+
if (!local) continue;
|
|
550
|
+
const b = transformBBox(local, parent);
|
|
551
|
+
const visible = b.maxX > 0 && b.minX < W && b.maxY > 0 && b.minY < H;
|
|
552
|
+
const clipped = b.minX < -TOL || b.minY < -TOL || b.maxX > W + TOL || b.maxY > H + TOL;
|
|
553
|
+
if (visible && clipped) {
|
|
554
|
+
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}`));
|
|
359
555
|
}
|
|
360
556
|
}
|
|
361
|
-
for (const it
|
|
362
|
-
if (!isText(it) || it.wrap || it.textPath ||
|
|
557
|
+
for (const { it, parent, dynamic } of placed) {
|
|
558
|
+
if (!isText(it) || it.wrap || it.textPath || dynamic) continue;
|
|
363
559
|
const estW = estTextWidth(it);
|
|
364
560
|
const left = it.align === "left" ? 0 : it.align === "right" ? it.box.w - estW : it.box.w / 2 - estW / 2;
|
|
365
|
-
const
|
|
561
|
+
const m = compose(parent, it.transform);
|
|
562
|
+
const p0 = apply(m, { x: left, y: 0 }), p1 = apply(m, { x: left + estW, y: 0 });
|
|
563
|
+
const wl = Math.min(p0.x, p1.x), wr = Math.max(p0.x, p1.x);
|
|
366
564
|
if (wr > W + TOL || wl < -TOL) {
|
|
367
565
|
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"`));
|
|
368
566
|
}
|
|
369
567
|
}
|
|
568
|
+
for (const { it } of placed) {
|
|
569
|
+
if (!isText(it) || !it.wrap || it.textPath) continue;
|
|
570
|
+
const { lines, widestWord } = wrapMetrics(it);
|
|
571
|
+
const label = `${it.content.slice(0, 24).replace(/\n/g, " ")}${it.content.length > 24 ? "\u2026" : ""}`;
|
|
572
|
+
const tall = lines * it.size * it.lineHeight;
|
|
573
|
+
if (tall > it.box.h + TOL) {
|
|
574
|
+
out.push(warn(`text "${label}" overflows its box: ~${lines} wrapped line(s) \u2248 ${r0(tall)} px for a box ${r0(it.box.h)} px tall \u2014 raise the box height, shorten the text, or reduce the size`));
|
|
575
|
+
}
|
|
576
|
+
if (widestWord > it.box.w + TOL) {
|
|
577
|
+
out.push(warn(`text "${label}" has a word too wide for its box (~${r0(widestWord)} px > ${r0(it.box.w)} px) \u2014 a single word cannot be broken, so it spills past the frame whatever "wrap" says`));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
370
580
|
const pathTexts = [];
|
|
371
581
|
const walkPathTexts = (layers) => {
|
|
372
582
|
for (const l of layers) for (const it of l.items) {
|
|
373
583
|
if (isText(it) && it.textPath) pathTexts.push(it);
|
|
374
|
-
else if (
|
|
584
|
+
else if (isGroup2(it)) walkPathTexts(it.layers);
|
|
375
585
|
}
|
|
376
586
|
};
|
|
377
587
|
walkPathTexts(doc.layers);
|
|
@@ -387,11 +597,11 @@ function docLayoutWarnings(doc) {
|
|
|
387
597
|
const zones = [];
|
|
388
598
|
const collectZones = (layers) => {
|
|
389
599
|
for (const l of layers) for (const it of l.items) {
|
|
390
|
-
if (
|
|
600
|
+
if (isGroup2(it) && it.hitbox && it.name) {
|
|
391
601
|
const b = dropZoneBounds(doc, it.name);
|
|
392
602
|
if (b) zones.push({ name: it.name, b });
|
|
393
603
|
}
|
|
394
|
-
if (
|
|
604
|
+
if (isGroup2(it)) collectZones(it.layers);
|
|
395
605
|
}
|
|
396
606
|
};
|
|
397
607
|
collectZones(doc.layers);
|
|
@@ -400,12 +610,13 @@ function docLayoutWarnings(doc) {
|
|
|
400
610
|
}
|
|
401
611
|
return out;
|
|
402
612
|
}
|
|
403
|
-
function lintDoc(doc) {
|
|
613
|
+
function lintDoc(doc, src) {
|
|
404
614
|
const out = [];
|
|
405
615
|
const allVars = allScopeVariables(doc);
|
|
406
616
|
for (const { label, editPath } of scopes(doc)) {
|
|
407
617
|
const ctx = docLintContext(doc, editPath, allVars);
|
|
408
|
-
|
|
618
|
+
const regions = src && editPath.length === 0 ? behaviorRegions(src) : scopeRegions(scopeProgram(doc, editPath)).map((r) => ({ scope: label, body: r.body, line: r.line }));
|
|
619
|
+
for (const r of regions) for (const d of lint(r.body, ctx)) out.push({ scope: r.scope, diag: { ...d, line: d.line + r.line - 1 } });
|
|
409
620
|
}
|
|
410
621
|
out.push(...docStructureWarnings(doc));
|
|
411
622
|
out.push(...docModifierWarnings(doc));
|
|
@@ -418,13 +629,13 @@ function docModifierWarnings(doc) {
|
|
|
418
629
|
const ctx = docLintContext(doc, editPath, allVars);
|
|
419
630
|
const visit = (items) => {
|
|
420
631
|
for (const it of items) {
|
|
421
|
-
if (
|
|
632
|
+
if (isPoseable2(it) && it.modifiers) for (const ch of EXPR_CHANNELS2) {
|
|
422
633
|
const m = it.modifiers[ch];
|
|
423
634
|
if (!m) continue;
|
|
424
635
|
for (const d of lint(`${ch} = ${m.target}`, ctx)) out.push({ scope: label, diag: { ...d, line: 1, col: 1, message: `${m.kind} ${ch}: ${d.message}` } });
|
|
425
636
|
if (m.kind === "spring" && !(m.damping > 0 && m.damping < 1)) out.push({ scope: label, diag: { line: 1, col: 1, severity: "warning", message: `spring ${ch}: damping ${m.damping} should be in (0,1) \u2014 clamped at runtime` } });
|
|
426
637
|
}
|
|
427
|
-
if (
|
|
638
|
+
if (isGroup2(it)) for (const l of it.layers) visit(l.items);
|
|
428
639
|
}
|
|
429
640
|
};
|
|
430
641
|
visit(contextLayers(doc, editPath).flatMap((l) => l.items));
|
|
@@ -434,21 +645,59 @@ function docModifierWarnings(doc) {
|
|
|
434
645
|
return out;
|
|
435
646
|
}
|
|
436
647
|
var sev = (d) => d.severity === "warning" ? "warning" : "error";
|
|
437
|
-
function lintDocReport(doc) {
|
|
438
|
-
return lintDoc(doc).map(({ scope, diag }) => `[${scope}] ${diag.line}:${diag.col}: ${sev(diag)}: ${diag.message}`).join("\n");
|
|
648
|
+
function lintDocReport(doc, src) {
|
|
649
|
+
return lintDoc(doc, src).map(({ scope, diag }) => `[${scope}] ${diag.line}:${diag.col}: ${sev(diag)}: ${diag.message}`).join("\n");
|
|
439
650
|
}
|
|
440
|
-
function docHasErrors(doc) {
|
|
441
|
-
return lintDoc(doc).some(({ diag }) => sev(diag) === "error");
|
|
651
|
+
function docHasErrors(doc, src) {
|
|
652
|
+
return lintDoc(doc, src).some(({ diag }) => sev(diag) === "error");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/check.ts
|
|
656
|
+
import { behaviorDiagnostics, objectTargetDiagnostics } from "@flatkit/engine/flatFormat";
|
|
657
|
+
var line1 = (d) => `[${d.scope}] ${d.line}:${d.col}: ${d.severity}: ${d.message}`;
|
|
658
|
+
var formatDiagnostics = (diagnostics) => diagnostics.map(line1).join("\n");
|
|
659
|
+
function sourceDiagnostics(src) {
|
|
660
|
+
return [...behaviorDiagnostics(src), ...objectTargetDiagnostics(src)].map(({ scope, diag }) => ({ scope, line: diag.line, col: diag.col, severity: "error", message: diag.message }));
|
|
661
|
+
}
|
|
662
|
+
function programDiagnostics(doc, src) {
|
|
663
|
+
const out = [];
|
|
664
|
+
const seen = /* @__PURE__ */ new Set();
|
|
665
|
+
const push = (d) => {
|
|
666
|
+
const k = line1(d);
|
|
667
|
+
if (!seen.has(k)) {
|
|
668
|
+
seen.add(k);
|
|
669
|
+
out.push(d);
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
for (const d of sourceDiagnostics(src)) push(d);
|
|
673
|
+
for (const { scope, diag } of lintDoc(doc, src)) push({ scope, line: diag.line, col: diag.col, severity: diag.severity === "warning" ? "warning" : "error", message: diag.message });
|
|
674
|
+
return out;
|
|
675
|
+
}
|
|
676
|
+
var tally = (diagnostics, doc) => {
|
|
677
|
+
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
678
|
+
return { ok: errors === 0, errors, warnings: diagnostics.length - errors, diagnostics, report: formatDiagnostics(diagnostics), doc };
|
|
679
|
+
};
|
|
680
|
+
function checkProgram(src, opts = {}) {
|
|
681
|
+
let doc;
|
|
682
|
+
try {
|
|
683
|
+
doc = compileFlatpack(src, opts.assetSrcs ?? [], opts.media ?? {});
|
|
684
|
+
} catch (e) {
|
|
685
|
+
return tally([{ scope: "scene", line: 1, col: 1, severity: "error", message: `compile error: ${e.message}` }], null);
|
|
686
|
+
}
|
|
687
|
+
return tally(programDiagnostics(doc, src), doc);
|
|
442
688
|
}
|
|
443
689
|
|
|
444
690
|
export {
|
|
445
|
-
localVariables,
|
|
446
|
-
lint,
|
|
447
|
-
lintReport,
|
|
448
691
|
splitScopeProgram,
|
|
449
692
|
scopeRegions,
|
|
450
693
|
formatObjectBlock,
|
|
451
694
|
joinScopeProgram,
|
|
695
|
+
localVariables,
|
|
696
|
+
lint,
|
|
697
|
+
lintReport,
|
|
698
|
+
forEachAction,
|
|
699
|
+
forEachActionExpression,
|
|
700
|
+
forEachItemExpression,
|
|
452
701
|
scopeProgram,
|
|
453
702
|
docLintContext,
|
|
454
703
|
allScopeVariables,
|
|
@@ -459,6 +708,9 @@ export {
|
|
|
459
708
|
lintDoc,
|
|
460
709
|
docModifierWarnings,
|
|
461
710
|
lintDocReport,
|
|
462
|
-
docHasErrors
|
|
711
|
+
docHasErrors,
|
|
712
|
+
formatDiagnostics,
|
|
713
|
+
programDiagnostics,
|
|
714
|
+
checkProgram
|
|
463
715
|
};
|
|
464
|
-
//# sourceMappingURL=chunk-
|
|
716
|
+
//# sourceMappingURL=chunk-7KBWT2PA.js.map
|