@flatkit/compiler 0.23.0 → 0.25.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 +47 -2
- package/dist/analysis.js +8 -1
- package/dist/{chunk-5PA2W4V3.js → chunk-GAS27LSJ.js} +11 -9
- package/dist/chunk-GAS27LSJ.js.map +1 -0
- package/dist/{chunk-HRWULWIL.js → chunk-MRRRN2CS.js} +356 -118
- package/dist/chunk-MRRRN2CS.js.map +1 -0
- package/dist/cli/flatc.js +2 -2
- package/dist/index.d.ts +40 -6
- package/dist/index.js +188 -5
- 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-5PA2W4V3.js.map +0 -1
- package/dist/chunk-HRWULWIL.js.map +0 -1
|
@@ -1,84 +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
|
-
var SECOND_ACTION = /\b(send|play|pause|sound|set|go|repeat)\b/;
|
|
38
|
-
function lint(src, ctx = {}) {
|
|
39
|
-
const { units, diagnostics, sites } = parseUnits(src);
|
|
40
|
-
const out = [...diagnostics];
|
|
41
|
-
const variables = localVariables(units);
|
|
42
|
-
for (const v of ctx.variables ?? []) variables.add(v);
|
|
43
|
-
const knownIds = /* @__PURE__ */ new Set([...STD_IDS, ...STD_CONSTANTS, ...variables]);
|
|
44
|
-
const knownFns = new Set(STD_FUNCTIONS);
|
|
45
|
-
knownFns.add("velocity");
|
|
46
|
-
for (const u of units) {
|
|
47
|
-
if (u.kind === "func") knownFns.add(u.func.name);
|
|
48
|
-
else if (u.kind === "use") for (const name of packageFunctionNames(u.name)) knownFns.add(name);
|
|
49
|
-
}
|
|
50
|
-
for (const f of ctx.functions ?? []) knownFns.add(f);
|
|
51
|
-
const knownObjs = new Set(STD_OBJECTS);
|
|
52
|
-
for (const o of ctx.objects ?? []) knownObjs.add(o);
|
|
53
|
-
const labels = ctx.labels ? new Set(ctx.labels) : null;
|
|
54
|
-
for (const s of sites) {
|
|
55
|
-
if (s.kind === "expr") {
|
|
56
|
-
if (TEXT_CALL.test(s.text)) {
|
|
57
|
-
out.push({ line: s.line, col: s.col, message: 'text("\u2026") is only allowed as an argument to "send"' });
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const a = analyzeExpr(s.text);
|
|
61
|
-
if (!a.ok) {
|
|
62
|
-
const leftover = SECOND_ASSIGN.test(s.text) ? "an assignment" : SECOND_ACTION.test(s.text) ? `\`${SECOND_ACTION.exec(s.text)[1]} \u2026\`` : "";
|
|
63
|
-
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}`;
|
|
64
|
-
out.push({ line: s.line, col: s.col, message });
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
for (const fn of a.refs.calls) if (!knownFns.has(fn)) out.push({ line: s.line, col: s.col, message: `unknown function "${fn}"` });
|
|
68
|
-
for (const o of a.refs.members)
|
|
69
|
-
if (!knownObjs.has(o)) out.push({ line: s.line, col: s.col, message: `unknown object "${o}" (expected: ${[...knownObjs].join(", ")})` });
|
|
70
|
-
for (const id of a.refs.ids)
|
|
71
|
-
if (!knownIds.has(id))
|
|
72
|
-
out.push({ line: s.line, col: s.col, message: `unknown variable "${id}"${variables.size ? "" : ' \u2014 declare it with "let"'}` });
|
|
73
|
-
} else if (labels && !labels.has(s.name)) {
|
|
74
|
-
out.push({ line: s.line, col: s.col, message: `unknown label "${s.name}"` });
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return out.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
78
|
-
}
|
|
79
|
-
function lintReport(src, ctx = {}) {
|
|
80
|
-
return lint(src, ctx).map((d) => `${d.line}:${d.col}: ${d.message}`).join("\n");
|
|
81
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
compileFlatpack
|
|
3
|
+
} from "./chunk-KXKX2RMX.js";
|
|
82
4
|
|
|
83
5
|
// src/scopeProgram.ts
|
|
84
6
|
function matchBrace(s, open) {
|
|
@@ -165,17 +87,170 @@ function joinScopeProgram(rest, objects) {
|
|
|
165
87
|
return parts.length ? parts.join("\n\n") + "\n" : "";
|
|
166
88
|
}
|
|
167
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
|
+
|
|
168
172
|
// src/programDoc.ts
|
|
169
173
|
import { printUnits } from "@flatkit/engine/dsl";
|
|
170
174
|
import { functionsToUnits, importsToUnits, objectToUnits, timelineToUnits, variablesToUnits } from "@flatkit/engine/scriptDoc";
|
|
171
|
-
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";
|
|
172
176
|
import { importedFunctions } from "@flatkit/engine/stdlib";
|
|
173
|
-
import { EXPR_CHANNELS } from "@flatkit/engine/timeline";
|
|
177
|
+
import { EXPR_CHANNELS as EXPR_CHANNELS2 } from "@flatkit/engine/timeline";
|
|
174
178
|
import { objectNames } from "@flatkit/engine/sceneRefs";
|
|
175
179
|
import { behaviorRegions } from "@flatkit/engine/flatFormat";
|
|
176
|
-
import { itemBBox, dropZoneBounds } from "@flatkit/engine/groups";
|
|
180
|
+
import { itemBBox, dropZoneBounds, transformBBox } from "@flatkit/engine/groups";
|
|
181
|
+
import { IDENTITY, apply, compose } from "@flatkit/engine/transform";
|
|
177
182
|
import { makePathSampler } from "@flatkit/engine/path";
|
|
178
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
|
|
179
254
|
import { parseUnits as parseUnits2 } from "@flatkit/engine/dsl";
|
|
180
255
|
function scopeProgram(doc, editPath = []) {
|
|
181
256
|
const atRoot = editPath.length === 0;
|
|
@@ -227,7 +302,7 @@ function itemNameById(doc, id) {
|
|
|
227
302
|
found = it.name ?? null;
|
|
228
303
|
return;
|
|
229
304
|
}
|
|
230
|
-
if (
|
|
305
|
+
if (isGroup2(it)) walk(it.layers);
|
|
231
306
|
}
|
|
232
307
|
};
|
|
233
308
|
walk(doc.layers);
|
|
@@ -254,6 +329,39 @@ function functionsReadingTime(doc) {
|
|
|
254
329
|
if (reads.size === before) return [...reads];
|
|
255
330
|
}
|
|
256
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
|
+
}
|
|
257
365
|
function docStructureWarnings(doc) {
|
|
258
366
|
const out = [];
|
|
259
367
|
const names = new Set(objectNames(doc.layers));
|
|
@@ -268,11 +376,11 @@ function docStructureWarnings(doc) {
|
|
|
268
376
|
const modTargets = [];
|
|
269
377
|
const collectMods = (items) => {
|
|
270
378
|
for (const it of items) {
|
|
271
|
-
if (
|
|
379
|
+
if (isPoseable2(it) && it.modifiers) for (const ch of EXPR_CHANNELS2) {
|
|
272
380
|
const mm = it.modifiers[ch];
|
|
273
381
|
if (mm) modTargets.push(mm.target);
|
|
274
382
|
}
|
|
275
|
-
if (
|
|
383
|
+
if (isGroup2(it)) for (const l of it.layers) collectMods(l.items);
|
|
276
384
|
}
|
|
277
385
|
};
|
|
278
386
|
for (const l of doc.layers) collectMods(l.items);
|
|
@@ -283,6 +391,19 @@ function docStructureWarnings(doc) {
|
|
|
283
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)` } });
|
|
284
392
|
}
|
|
285
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
|
+
}
|
|
286
407
|
const dur = doc.timeline?.durationFrames ?? 60;
|
|
287
408
|
const fps = doc.timeline?.fps ?? 24;
|
|
288
409
|
if (dur <= 120) {
|
|
@@ -290,12 +411,12 @@ function docStructureWarnings(doc) {
|
|
|
290
411
|
const culprits = /* @__PURE__ */ new Set();
|
|
291
412
|
const scan = (layers) => {
|
|
292
413
|
for (const l of layers) for (const it of l.items) {
|
|
293
|
-
if (
|
|
414
|
+
if (isPoseable2(it) && it.expressions) for (const k in it.expressions) {
|
|
294
415
|
const expr = it.expressions[k] ?? "";
|
|
295
416
|
if (/\btime\b/.test(expr)) culprits.add("`time`");
|
|
296
417
|
for (const fn of viaFn) if (new RegExp(`\\b${escapeRe(fn)}\\s*\\(`).test(expr)) culprits.add(`\`${fn}()\` (which reads \`time\`)`);
|
|
297
418
|
}
|
|
298
|
-
if (
|
|
419
|
+
if (isGroup2(it)) scan(it.layers);
|
|
299
420
|
}
|
|
300
421
|
};
|
|
301
422
|
scan(doc.layers);
|
|
@@ -304,6 +425,28 @@ function docStructureWarnings(doc) {
|
|
|
304
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\`.`));
|
|
305
426
|
}
|
|
306
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\`.`));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
307
450
|
out.push(...docPaintParamWarnings(doc));
|
|
308
451
|
out.push(...docCelLayerWarnings(doc));
|
|
309
452
|
out.push(...docLayoutWarnings(doc));
|
|
@@ -321,9 +464,9 @@ function docCelLayerWarnings(doc) {
|
|
|
321
464
|
const posed = /* @__PURE__ */ new Set();
|
|
322
465
|
for (const c of l.cels) for (const p of c.poses) posed.add(p.id);
|
|
323
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"}).`);
|
|
324
|
-
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.`);
|
|
325
468
|
}
|
|
326
|
-
for (const it of l.items) if (
|
|
469
|
+
for (const it of l.items) if (isGroup2(it)) visit(it.layers);
|
|
327
470
|
}
|
|
328
471
|
};
|
|
329
472
|
visit(layers);
|
|
@@ -349,8 +492,8 @@ function docPaintParamWarnings(doc) {
|
|
|
349
492
|
const stops = it.paint && it.paint.type !== "solid" ? it.paint.stops : [];
|
|
350
493
|
for (const s of stops) if (s.param) flag(s.param, "a gradient stop", who);
|
|
351
494
|
}
|
|
352
|
-
if (
|
|
353
|
-
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);
|
|
354
497
|
};
|
|
355
498
|
for (const l of layers) for (const it of l.items) visit(it);
|
|
356
499
|
};
|
|
@@ -360,44 +503,85 @@ function docPaintParamWarnings(doc) {
|
|
|
360
503
|
}
|
|
361
504
|
var warn = (message) => ({ scope: "scene", diag: { line: 1, col: 1, severity: "warning", message } });
|
|
362
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;
|
|
363
507
|
function estTextWidth(t) {
|
|
364
|
-
const advance = (t
|
|
508
|
+
const advance = glyphAdvance(t);
|
|
365
509
|
let longest = 0;
|
|
366
510
|
for (const line of t.content.split("\n")) longest = Math.max(longest, line.length);
|
|
367
511
|
return longest * advance;
|
|
368
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
|
+
}
|
|
369
532
|
function docLayoutWarnings(doc) {
|
|
370
533
|
const out = [];
|
|
371
534
|
const W = doc.width, H = doc.height;
|
|
372
535
|
const TOL = 8;
|
|
373
536
|
const r0 = (n) => Math.round(n);
|
|
374
|
-
const dynamicPos = (it) => "expressions" in it && it.expressions && (it.expressions.x != null || it.expressions.y != null || it.expressions.dx != null || it.expressions.dy != null);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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}`));
|
|
385
555
|
}
|
|
386
556
|
}
|
|
387
|
-
for (const it
|
|
388
|
-
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;
|
|
389
559
|
const estW = estTextWidth(it);
|
|
390
560
|
const left = it.align === "left" ? 0 : it.align === "right" ? it.box.w - estW : it.box.w / 2 - estW / 2;
|
|
391
|
-
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);
|
|
392
564
|
if (wr > W + TOL || wl < -TOL) {
|
|
393
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"`));
|
|
394
566
|
}
|
|
395
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
|
+
}
|
|
396
580
|
const pathTexts = [];
|
|
397
581
|
const walkPathTexts = (layers) => {
|
|
398
582
|
for (const l of layers) for (const it of l.items) {
|
|
399
583
|
if (isText(it) && it.textPath) pathTexts.push(it);
|
|
400
|
-
else if (
|
|
584
|
+
else if (isGroup2(it)) walkPathTexts(it.layers);
|
|
401
585
|
}
|
|
402
586
|
};
|
|
403
587
|
walkPathTexts(doc.layers);
|
|
@@ -413,11 +597,11 @@ function docLayoutWarnings(doc) {
|
|
|
413
597
|
const zones = [];
|
|
414
598
|
const collectZones = (layers) => {
|
|
415
599
|
for (const l of layers) for (const it of l.items) {
|
|
416
|
-
if (
|
|
600
|
+
if (isGroup2(it) && it.hitbox && it.name) {
|
|
417
601
|
const b = dropZoneBounds(doc, it.name);
|
|
418
602
|
if (b) zones.push({ name: it.name, b });
|
|
419
603
|
}
|
|
420
|
-
if (
|
|
604
|
+
if (isGroup2(it)) collectZones(it.layers);
|
|
421
605
|
}
|
|
422
606
|
};
|
|
423
607
|
collectZones(doc.layers);
|
|
@@ -445,13 +629,13 @@ function docModifierWarnings(doc) {
|
|
|
445
629
|
const ctx = docLintContext(doc, editPath, allVars);
|
|
446
630
|
const visit = (items) => {
|
|
447
631
|
for (const it of items) {
|
|
448
|
-
if (
|
|
632
|
+
if (isPoseable2(it) && it.modifiers) for (const ch of EXPR_CHANNELS2) {
|
|
449
633
|
const m = it.modifiers[ch];
|
|
450
634
|
if (!m) continue;
|
|
451
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}` } });
|
|
452
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` } });
|
|
453
637
|
}
|
|
454
|
-
if (
|
|
638
|
+
if (isGroup2(it)) for (const l of it.layers) visit(l.items);
|
|
455
639
|
}
|
|
456
640
|
};
|
|
457
641
|
visit(contextLayers(doc, editPath).flatMap((l) => l.items));
|
|
@@ -468,14 +652,65 @@ function docHasErrors(doc, src) {
|
|
|
468
652
|
return lintDoc(doc, src).some(({ diag }) => sev(diag) === "error");
|
|
469
653
|
}
|
|
470
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
|
+
var SIZE_HEADER = /^[ \t]*size[ \t]+-?[\d.]+[ \t]+-?[\d.]+/m;
|
|
663
|
+
function missingSizeDiagnostic(src, doc) {
|
|
664
|
+
if (SIZE_HEADER.test(src)) return null;
|
|
665
|
+
return {
|
|
666
|
+
scope: "scene",
|
|
667
|
+
line: 1,
|
|
668
|
+
col: 1,
|
|
669
|
+
severity: "warning",
|
|
670
|
+
message: `no \`size W H\` line \u2014 it is the REQUIRED first statement of a program, and this document silently defaults to ${doc.width}x${doc.height}. Anything laid out past that edge is clipped with nothing to say so. Declare the canvas you drew for.`
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function programDiagnostics(doc, src) {
|
|
674
|
+
const out = [];
|
|
675
|
+
const seen = /* @__PURE__ */ new Set();
|
|
676
|
+
const push = (d) => {
|
|
677
|
+
const k = line1(d);
|
|
678
|
+
if (!seen.has(k)) {
|
|
679
|
+
seen.add(k);
|
|
680
|
+
out.push(d);
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
for (const d of sourceDiagnostics(src)) push(d);
|
|
684
|
+
const noSize = missingSizeDiagnostic(src, doc);
|
|
685
|
+
if (noSize) push(noSize);
|
|
686
|
+
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 });
|
|
687
|
+
return out;
|
|
688
|
+
}
|
|
689
|
+
var tally = (diagnostics, doc) => {
|
|
690
|
+
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
691
|
+
return { ok: errors === 0, errors, warnings: diagnostics.length - errors, diagnostics, report: formatDiagnostics(diagnostics), doc };
|
|
692
|
+
};
|
|
693
|
+
function checkProgram(src, opts = {}) {
|
|
694
|
+
let doc;
|
|
695
|
+
try {
|
|
696
|
+
doc = compileFlatpack(src, opts.assetSrcs ?? [], opts.media ?? {});
|
|
697
|
+
} catch (e) {
|
|
698
|
+
return tally([{ scope: "scene", line: 1, col: 1, severity: "error", message: `compile error: ${e.message}` }], null);
|
|
699
|
+
}
|
|
700
|
+
return tally(programDiagnostics(doc, src), doc);
|
|
701
|
+
}
|
|
702
|
+
|
|
471
703
|
export {
|
|
472
|
-
localVariables,
|
|
473
|
-
lint,
|
|
474
|
-
lintReport,
|
|
475
704
|
splitScopeProgram,
|
|
476
705
|
scopeRegions,
|
|
477
706
|
formatObjectBlock,
|
|
478
707
|
joinScopeProgram,
|
|
708
|
+
localVariables,
|
|
709
|
+
lint,
|
|
710
|
+
lintReport,
|
|
711
|
+
forEachAction,
|
|
712
|
+
forEachActionExpression,
|
|
713
|
+
forEachItemExpression,
|
|
479
714
|
scopeProgram,
|
|
480
715
|
docLintContext,
|
|
481
716
|
allScopeVariables,
|
|
@@ -486,6 +721,9 @@ export {
|
|
|
486
721
|
lintDoc,
|
|
487
722
|
docModifierWarnings,
|
|
488
723
|
lintDocReport,
|
|
489
|
-
docHasErrors
|
|
724
|
+
docHasErrors,
|
|
725
|
+
formatDiagnostics,
|
|
726
|
+
programDiagnostics,
|
|
727
|
+
checkProgram
|
|
490
728
|
};
|
|
491
|
-
//# sourceMappingURL=chunk-
|
|
729
|
+
//# sourceMappingURL=chunk-MRRRN2CS.js.map
|