@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.
@@ -1,82 +1,6 @@
1
- // src/lint.ts
2
- import { analyzeExpr, STD_CONSTANTS, STD_FUNCTIONS, STD_IDS, STD_OBJECTS } from "@flatkit/engine/expr";
3
- import { packageFunctionNames } from "@flatkit/engine/stdlib";
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 { itemBBox, dropZoneBounds } from "@flatkit/engine/groups";
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 (isGroup(it)) walk(it.layers);
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 (isPoseable(it) && it.modifiers) for (const ch of EXPR_CHANNELS) {
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 (isGroup(it)) for (const l of it.layers) collectMods(l.items);
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
- let usesTime = false;
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 (isPoseable(it) && it.expressions) {
270
- for (const k in it.expressions) if (/\btime\b/.test(it.expressions[k] ?? "")) usesTime = true;
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 (isGroup(it)) scan(it.layers);
419
+ if (isGroup2(it)) scan(it.layers);
273
420
  }
274
421
  };
275
422
  scan(doc.layers);
276
- if (usesTime) {
423
+ if (culprits.size) {
277
424
  const secs = Math.round(dur / fps * 10) / 10;
278
- out.push(warn(`a channel expression uses \`time\`, but the timeline loops every ${secs}s (durationFrames ${dur}) \u2014 \`time\` resets each loop, so the motion jumps. Use the monotone \`clock\` (never wraps), drive it by \`frame/${dur}\`, or set a longer \`timeline\`.`));
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 (isPoseable(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.`);
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 (isGroup(it)) visit(it.layers);
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 (isPoseable(it) && it.tint?.param) flag(it.tint.param, "a tint", who);
327
- if (isGroup(it)) for (const l of it.layers) for (const c of l.items) visit(c);
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.weight && t.weight >= 700 ? 0.56 : 0.52) * t.size;
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
- for (const layer of doc.layers) {
350
- for (const it of layer.items) {
351
- if (!(isText(it) || isImage(it)) || dynamicPos(it) || isText(it) && it.textPath) continue;
352
- const b = itemBBox(doc, it);
353
- if (!b) continue;
354
- const visible = b.maxX > 0 && b.minX < W && b.maxY > 0 && b.minY < H;
355
- const clipped = b.minX < -TOL || b.minY < -TOL || b.maxX > W + TOL || b.maxY > H + TOL;
356
- if (visible && clipped) {
357
- 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}`));
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 of doc.layers.flatMap((l) => l.items)) {
362
- if (!isText(it) || it.wrap || it.textPath || dynamicPos(it)) continue;
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 wl = it.transform.e + left, wr = wl + estW;
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 (isGroup(it)) walkPathTexts(it.layers);
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 (isGroup(it) && it.hitbox && it.name) {
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 (isGroup(it)) collectZones(it.layers);
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
- 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 } });
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 (isPoseable(it) && it.modifiers) for (const ch of EXPR_CHANNELS) {
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 (isGroup(it)) for (const l of it.layers) visit(l.items);
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-VB3ZWCNP.js.map
716
+ //# sourceMappingURL=chunk-7KBWT2PA.js.map