@flatkit/compiler 0.23.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,84 +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
- 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 (isGroup(it)) walk(it.layers);
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 (isPoseable(it) && it.modifiers) for (const ch of EXPR_CHANNELS) {
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 (isGroup(it)) for (const l of it.layers) collectMods(l.items);
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 (isPoseable(it) && it.expressions) for (const k in it.expressions) {
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 (isGroup(it)) scan(it.layers);
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 (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.`);
325
468
  }
326
- 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);
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 (isPoseable(it) && it.tint?.param) flag(it.tint.param, "a tint", who);
353
- 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);
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.weight && t.weight >= 700 ? 0.56 : 0.52) * t.size;
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
- for (const layer of doc.layers) {
376
- for (const it of layer.items) {
377
- if (!(isText(it) || isImage(it)) || dynamicPos(it) || isText(it) && it.textPath) continue;
378
- const b = itemBBox(doc, it);
379
- if (!b) continue;
380
- const visible = b.maxX > 0 && b.minX < W && b.maxY > 0 && b.minY < H;
381
- const clipped = b.minX < -TOL || b.minY < -TOL || b.maxX > W + TOL || b.maxY > H + TOL;
382
- if (visible && clipped) {
383
- 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}`));
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 of doc.layers.flatMap((l) => l.items)) {
388
- 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;
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 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);
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 (isGroup(it)) walkPathTexts(it.layers);
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 (isGroup(it) && it.hitbox && it.name) {
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 (isGroup(it)) collectZones(it.layers);
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 (isPoseable(it) && it.modifiers) for (const ch of EXPR_CHANNELS) {
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 (isGroup(it)) for (const l of it.layers) visit(l.items);
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,52 @@ 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
+ 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);
688
+ }
689
+
471
690
  export {
472
- localVariables,
473
- lint,
474
- lintReport,
475
691
  splitScopeProgram,
476
692
  scopeRegions,
477
693
  formatObjectBlock,
478
694
  joinScopeProgram,
695
+ localVariables,
696
+ lint,
697
+ lintReport,
698
+ forEachAction,
699
+ forEachActionExpression,
700
+ forEachItemExpression,
479
701
  scopeProgram,
480
702
  docLintContext,
481
703
  allScopeVariables,
@@ -486,6 +708,9 @@ export {
486
708
  lintDoc,
487
709
  docModifierWarnings,
488
710
  lintDocReport,
489
- docHasErrors
711
+ docHasErrors,
712
+ formatDiagnostics,
713
+ programDiagnostics,
714
+ checkProgram
490
715
  };
491
- //# sourceMappingURL=chunk-HRWULWIL.js.map
716
+ //# sourceMappingURL=chunk-7KBWT2PA.js.map