@glissade/scene 0.58.1 → 0.59.0-pre.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.
@@ -46,6 +46,14 @@ interface DescribedProp {
46
46
  arity?: number;
47
47
  /** Construction-only props: `true` when the constructor REQUIRES it (e.g. Image/Video `assetId`). */
48
48
  required?: boolean;
49
+ /**
50
+ * 0.59 F "manifest conventions": the physical UNIT of the value, when one
51
+ * applies — e.g. `'degrees'` for rotation, `'seconds'` for a Video time offset.
52
+ * ADDITIVE + curated (a small per-prop table, like `positionAnchor`), present
53
+ * ONLY where a unit is meaningful; absent for unitless/px props. Lets a
54
+ * consumer stop guessing whether `rotation` is degrees or radians.
55
+ */
56
+ unit?: string;
49
57
  }
50
58
  interface DescribedNode {
51
59
  props: {
@@ -65,6 +73,17 @@ interface DescribedNode {
65
73
  * shape-vs-Text anchor mismatch by pixel-measuring.
66
74
  */
67
75
  positionAnchor: string;
76
+ /**
77
+ * 0.59 F ride-along "type-level bindable discovery aid": the prop names on this
78
+ * node a Track CAN drive (i.e. the animatable ones) — a flat, at-a-glance list
79
+ * so a consumer sees the bindable surface without filtering `props`. GENERATED
80
+ * from the same `listTargets()` the animatable props are, so it can't drift.
81
+ * This is the TYPE-level "can be animated" aid; the INSTANCE-level "is CURRENTLY
82
+ * bound on THIS node" truth (the anti-false-conclusion guard) is
83
+ * `instanceProps(node).bound` on `@glissade/scene/diagnostics`. Optional so a
84
+ * manifest captured before 0.59 (no `bindable`) still type-checks.
85
+ */
86
+ bindable?: string[];
68
87
  /**
69
88
  * The tree-shakeable subpath this node is imported from, when not the base
70
89
  * `@glissade/scene` index (e.g. the Layout family lives on
@@ -113,6 +132,14 @@ interface DescribedHelper {
113
132
  usage: string;
114
133
  /** Runnable example snippets — see {@link DescribedNode.examples}. */
115
134
  examples?: readonly string[];
135
+ /**
136
+ * 0.59 F/E "manifest conventions": `true` when this helper needs a real text
137
+ * MEASURER for correct geometry (splitText/fitText/…). Without one it degrades
138
+ * to a rough per-character estimate (or, with `{ requireMeasurer: true }`,
139
+ * throws) — so a consumer knows to pass `{ measurer }` / call setTextMeasurer()
140
+ * first. Absent (⇒ not measurer-dependent) for every other helper.
141
+ */
142
+ requiresMeasurer?: boolean;
116
143
  }
117
144
  /**
118
145
  * One entry in the {@link ApiManifest.surface} taxonomy (0.47 "verifiable
package/dist/describe.js CHANGED
@@ -23,7 +23,7 @@ import { easings, listValueTypes } from "@glissade/core";
23
23
  * never pulled onto the base embed path — a scene that never calls `describe()`
24
24
  * pays zero bytes for it.
25
25
  */
26
- const RAW_VERSION = "0.58.1";
26
+ const RAW_VERSION = "0.59.0-pre.0";
27
27
  const PACKAGE_VERSION = RAW_VERSION.includes("GLISSADE_".concat("VERSION")) ? "0.0.0-dev" : RAW_VERSION;
28
28
  /**
29
29
  * Parse the documented positional-arg count from a helper `usage` string — the
@@ -336,6 +336,33 @@ const BASE_CONSTRUCTION_PROP_META = {
336
336
  * coords. Surfaced per node so a consumer aligning a card + its label stops
337
337
  * pixel-measuring the mismatch — and knows `anchor` (above) overrides it.
338
338
  */
339
+ /**
340
+ * 0.59 F "manifest conventions" — the curated per-prop UNIT table (the
341
+ * POSITION_ANCHOR precedent applied to units). Keyed by prop NAME; a prop absent
342
+ * here carries no `unit` (unitless or px). Deliberately minimal: rotation is the
343
+ * classic degrees-vs-radians ambiguity, Video's time offsets are seconds.
344
+ */
345
+ const PROP_UNITS = {
346
+ rotation: "degrees",
347
+ at: "seconds",
348
+ trimStart: "seconds",
349
+ clipDuration: "seconds",
350
+ sourceFps: "fps"
351
+ };
352
+ /**
353
+ * 0.59 F/E — helpers that need a real text measurer for correct geometry (they
354
+ * snapshot part/fit geometry through it). Surfaced as `requiresMeasurer:true`.
355
+ */
356
+ const MEASURER_HELPERS = new Set([
357
+ "measureWrappedText",
358
+ "splitText",
359
+ "fitText",
360
+ "fitTextSize",
361
+ "fitTextGroup",
362
+ "revealWords",
363
+ "revealLines",
364
+ "emphasizeWords"
365
+ ]);
339
366
  const POSITION_ANCHOR = {
340
367
  Rect: "center",
341
368
  Circle: "center",
@@ -358,7 +385,8 @@ function describeNode(node, typeName) {
358
385
  type,
359
386
  animatable: true,
360
387
  target: `<id>/${path}`,
361
- ...arity !== void 0 ? { arity } : {}
388
+ ...arity !== void 0 ? { arity } : {},
389
+ ...PROP_UNITS[path] !== void 0 ? { unit: PROP_UNITS[path] } : {}
362
390
  };
363
391
  }
364
392
  const ownNames = NODE_CONSTRUCTION_PROP_NAMES[typeName] ?? [];
@@ -372,14 +400,21 @@ function describeNode(node, typeName) {
372
400
  props[prop] = {
373
401
  type: spec.type,
374
402
  animatable: false,
375
- ...spec.required ? { required: true } : {}
403
+ ...spec.required ? { required: true } : {},
404
+ ...PROP_UNITS[prop] !== void 0 ? { unit: PROP_UNITS[prop] } : {}
376
405
  };
377
406
  }
378
407
  return {
379
408
  props,
380
- positionAnchor: POSITION_ANCHOR[typeName] ?? "center"
409
+ positionAnchor: POSITION_ANCHOR[typeName] ?? "center",
410
+ bindable: bindableProps(props)
381
411
  };
382
412
  }
413
+ /** The animatable (Track-drivable) prop names of a manifest — the generated
414
+ * 0.59 `DescribedNode.bindable` discovery aid (can't drift from `props`). */
415
+ function bindableProps(props) {
416
+ return Object.entries(props).filter(([, p]) => p.animatable).map(([name]) => name);
417
+ }
383
418
  /**
384
419
  * The Layout family — `Layout` and its `Stack`/`Row`/`Column` ergonomic
385
420
  * factories — lives on the budgeted `@glissade/scene/layout` entry (it ships
@@ -436,6 +471,7 @@ function describeLayoutNode() {
436
471
  return {
437
472
  props,
438
473
  positionAnchor: "top-left",
474
+ bindable: bindableProps(props),
439
475
  subpath: LAYOUT_SUBPATH
440
476
  };
441
477
  }
@@ -844,10 +880,11 @@ function describe(opts = {}) {
844
880
  ...m,
845
881
  ...ex(m.name)
846
882
  })) : BUILDER_METHODS },
847
- helpers: withEx ? HELPERS.map((h) => ({
883
+ helpers: HELPERS.map((h) => ({
848
884
  ...h,
849
- ...ex(h.name)
850
- })) : HELPERS,
885
+ ...MEASURER_HELPERS.has(h.name) ? { requiresMeasurer: true } : {},
886
+ ...withEx ? ex(h.name) : void 0
887
+ })),
851
888
  components: listComponents().map((c) => ({
852
889
  name: c.name,
853
890
  props: mapComponentProps(c.props)
@@ -1,7 +1,8 @@
1
1
  import { i as DrawCommand, m as Resource, n as DisplayList } from "./displayList.js";
2
+ import { B as Node } from "./nodes.js";
2
3
  import { t as collapseReplacer } from "./collapseReplacer.js";
3
- import { r as Scene } from "./scene.js";
4
- import { CoverageReport, FontMode, FontUsage, Timeline } from "@glissade/core";
4
+ import { i as Scene } from "./scene.js";
5
+ import { CoverageReport, FontMode, FontUsage, Timeline, ValueTypeId } from "@glissade/core";
5
6
 
6
7
  //#region src/displayDiff.d.ts
7
8
 
@@ -142,4 +143,110 @@ interface ValidateSceneFontsOptions {
142
143
  */
143
144
  declare function validateSceneFonts(scene: Scene, doc: Timeline, loadBytes: FontByteLoader, options?: ValidateSceneFontsOptions): Promise<CoverageReport>;
144
145
  //#endregion
145
- export { type CacheColdResult, type CommandDelta, DL_SNAPSHOT_VERSION, type DisplayDiff, type DlSnapshot, DlSnapshotError, type FieldChange, type FontByteLoader, type ValidateSceneFontsOptions, auditCacheCold, collapseReplacer, collectLocalizedTextUsages, collectTextUsages, diffDisplayLists, formatDisplayDiff, parseDisplaySnapshot, serializeDisplayList, validateSceneFonts };
146
+ //#region src/validate.d.ts
147
+ /**
148
+ * Bumped ONLY on a breaking change to the diagnostic shape. New CODES and new
149
+ * OPTIONAL fields are additive and do NOT bump it — a consumer keys on `code`
150
+ * and tolerates unknown ones.
151
+ */
152
+ declare const DIAGNOSTIC_SCHEMA_VERSION: 1;
153
+ /** Closed severity ladder. `error` = a build error (unbound target); `warning`
154
+ * = a probable-mistake (position of a flow child); `info` = a valid-but-notable
155
+ * observation (off-canvas, estimating measurer). */
156
+ type DiagnosticSeverity = 'error' | 'warning' | 'info';
157
+ /**
158
+ * Stable, ADDITIVE-ONLY diagnostic codes (never renamed/removed — the wire
159
+ * contract). Chosen with BOTH `validateScene` and the future
160
+ * `gs parity --semantic` surface in mind.
161
+ * - `UNKNOWN_TARGET` — a track targets an id/prop that resolves to no signal.
162
+ * - `ID_COLLISION` — reserved: a duplicate node id (a built Scene rejects these
163
+ * at assembly, so it is unreachable here today; kept for the shared contract).
164
+ * - `OFF_CANVAS` — a node's static position places its box fully outside the
165
+ * viewport (valid, but usually a mistake).
166
+ * - `YOGA_CHILD_POSITION` — a track drives `position`/`position.*` of a FLOWABLE
167
+ * child of a Layout, whose flex slot overrides/confounds that position.
168
+ * - `MEASURER_FALLBACK` — the scene carries Text but no real measurer is
169
+ * injected, so layout uses the rough per-character estimate.
170
+ */
171
+ type DiagnosticCode = 'UNKNOWN_TARGET' | 'ID_COLLISION' | 'OFF_CANVAS' | 'YOGA_CHILD_POSITION' | 'MEASURER_FALLBACK';
172
+ /** One diagnostic. The `{schemaVersion, code, severity, message, node?, track?}`
173
+ * core shape is PINNED; future fields are ADDITIVE only. */
174
+ interface SceneDiagnostic {
175
+ schemaVersion: typeof DIAGNOSTIC_SCHEMA_VERSION;
176
+ code: DiagnosticCode;
177
+ severity: DiagnosticSeverity;
178
+ message: string;
179
+ /** The node id the diagnostic concerns, when applicable. */
180
+ node?: string;
181
+ /** The track target string the diagnostic concerns, when applicable. */
182
+ track?: string;
183
+ }
184
+ /** `validateScene` result — the CLI-lint `{ hasErrors, diagnostics }` shape,
185
+ * plus a top-level `schemaVersion`. */
186
+ interface ValidateSceneResult {
187
+ schemaVersion: typeof DIAGNOSTIC_SCHEMA_VERSION;
188
+ /** true iff any diagnostic has severity `error`. */
189
+ hasErrors: boolean;
190
+ /** Every diagnostic found — AGGREGATED, never throw-on-first, stable order. */
191
+ diagnostics: SceneDiagnostic[];
192
+ }
193
+ /** Classic edit distance (iterative two-row DP). Small strings only (ids). */
194
+ declare function levenshtein(a: string, b: string): number;
195
+ /**
196
+ * The nearest candidate to `name` within a reasonable edit budget (≤ 2, or a
197
+ * third of the length for longer names), or undefined if none is close enough —
198
+ * so a wildly-different typo doesn't get a misleading "did you mean" tail.
199
+ */
200
+ declare function nearestId(name: string, candidates: Iterable<string>): string | undefined;
201
+ /**
202
+ * Eagerly validate a scene (+ optional timeline) and AGGREGATE every problem —
203
+ * the static belt that surfaces at the AUTHORING site what the render-time
204
+ * `UnboundTargetError` backstop only shows one-at-a-time from deep in the render
205
+ * loop. Pure read (see the module header): calling it never changes a subsequent
206
+ * render's bytes.
207
+ *
208
+ * With a `doc`, every track target is walked through the existing
209
+ * `scene.resolveTarget`; an unresolved one becomes an `UNKNOWN_TARGET` error
210
+ * with a Levenshtein nearest-id / nearest-prop suggestion, and a
211
+ * `position`/`position.*` track on a flowable Layout child becomes a
212
+ * `YOGA_CHILD_POSITION` warning. Scene-only checks (off-canvas, measurer
213
+ * fallback) run regardless.
214
+ */
215
+ declare function validateScene(scene: Scene, doc?: Timeline): ValidateSceneResult;
216
+ /**
217
+ * Read a node's RESOLVED prop value at time `t` — the always-truthful read, the
218
+ * anti-false-conclusion primitive for inspection tooling (and load-bearing for
219
+ * 0.60 `critique()`). A BOUND prop returns its REAL bound value at `t` (not the
220
+ * misleading static default); an unbound prop returns its static value at any
221
+ * `t`; an unresolvable target returns `undefined`.
222
+ *
223
+ * Thin wrapper over the existing `scene.resolveTarget` + core's `evaluateAt`
224
+ * (read inside a read phase). NOTE: the scene must be BOUND (`bindScene`/
225
+ * `evaluate` already ran for the doc) for a track-driven value to appear —
226
+ * `resolveAt` reads the live signal, it does not itself bind. Render-neutral:
227
+ * the scene playhead is restored after the read.
228
+ */
229
+ declare function resolveAt(scene: Scene, target: string, t: number): unknown;
230
+ /** One prop's live binding state on a SPECIFIC node instance. */
231
+ interface InstancePropState {
232
+ /** The track-target path (e.g. `position`, `opacity`). */
233
+ path: string;
234
+ /** The §2.2 value type(s) the prop accepts. */
235
+ expects: ValueTypeId | readonly ValueTypeId[] | undefined;
236
+ /**
237
+ * TRUE when THIS instance's signal currently has a bound source (a timeline
238
+ * track OR a computed `() => …` initializer) — so a static read of it is a
239
+ * LIE; use `resolveAt` to read its real value over time. This is the
240
+ * anti-false-conclusion guard (the cursorFill trap): type-level "bindable"
241
+ * says the prop CAN be animated; this says it currently IS.
242
+ */
243
+ bound: boolean;
244
+ }
245
+ /**
246
+ * Announce which props are CURRENTLY bound on THIS node instance (not just
247
+ * type-level bindable). Reads `signal.isBound` per registered target — a pure
248
+ * inspection read. Pair with `resolveAt` to read a bound prop's real value.
249
+ */
250
+ declare function instanceProps(node: Node): InstancePropState[];
251
+ //#endregion
252
+ export { type CacheColdResult, type CommandDelta, DIAGNOSTIC_SCHEMA_VERSION, DL_SNAPSHOT_VERSION, type DiagnosticCode, type DiagnosticSeverity, type DisplayDiff, type DlSnapshot, DlSnapshotError, type FieldChange, type FontByteLoader, type InstancePropState, type SceneDiagnostic, type ValidateSceneFontsOptions, type ValidateSceneResult, auditCacheCold, collapseReplacer, collectLocalizedTextUsages, collectTextUsages, diffDisplayLists, formatDisplayDiff, instanceProps, levenshtein, nearestId, parseDisplaySnapshot, resolveAt, serializeDisplayList, validateScene, validateSceneFonts };
@@ -1,6 +1,6 @@
1
- import { U as createDisplayListBuilder, q as collapseReplacer, r as Group, s as Text } from "./nodes.js";
1
+ import { I as isEstimatingMeasurer, J as collapseReplacer, P as estimatingMeasurer, W as createDisplayListBuilder, r as Group, s as Text } from "./nodes.js";
2
2
  import { a as evaluate } from "./scene.js";
3
- import { buildFontRegistry, parseCmap, validateFonts } from "@glissade/core";
3
+ import { buildFontRegistry, evaluateAt, parseCmap, untracked, validateFonts } from "@glissade/core";
4
4
  //#region src/displayDiff.ts
5
5
  /** A flat, stable JSON value for one command with its resource ids INLINED to content. */
6
6
  function commandView(cmd, resources) {
@@ -272,4 +272,216 @@ async function validateSceneFonts(scene, doc, loadBytes, options = {}) {
272
272
  return validateFonts(usages, registry, cmaps, mode, { ...options.osFamilies !== void 0 ? { osFamilies: options.osFamilies } : {} });
273
273
  }
274
274
  //#endregion
275
- export { DL_SNAPSHOT_VERSION, DlSnapshotError, auditCacheCold, collapseReplacer, collectLocalizedTextUsages, collectTextUsages, diffDisplayLists, formatDisplayDiff, parseDisplaySnapshot, serializeDisplayList, validateSceneFonts };
275
+ //#region src/validate.ts
276
+ /**
277
+ * 0.59 "fail-loud ground floor" — the eager, render-NEUTRAL scene validator plus
278
+ * the truthful read primitive (`resolveAt`) and the instance-level bound
279
+ * indicator (`instanceProps`). All DIAGNOSTIC surface: it lives on the
280
+ * tree-shakeable `@glissade/scene/diagnostics` subpath (re-exported from
281
+ * `diagnostics.ts`), NEVER on the base scene index — the base embed pays zero
282
+ * bytes for it, exactly like the diff/audit/fontUsage cluster.
283
+ *
284
+ * THE THREE INVARIANTS THIS MODULE UPHOLDS:
285
+ * - `validateScene(scene, doc)` is a PURE READ: it walks track targets through
286
+ * the EXISTING `scene.resolveTarget` (no new resolution machinery), reads node
287
+ * bounds/flow-flags, and reports. It NEVER draws RNG, warms a signal memo,
288
+ * populates a measurer/font cache, or mutates a node — so `render(scene)` is
289
+ * byte-identical whether or not `validateScene` ran first. (Flowable-ness is
290
+ * probed with the STATELESS estimating measurer, not the scene's injected one,
291
+ * so no backend font cache is touched.)
292
+ * - It AGGREGATES every failure (never throw-on-first) and returns them.
293
+ * - The schema is PINNED: a closed `severity` enum + stable, additive-only
294
+ * string `code`s + a `schemaVersion` — the shared contract with the CLI lint
295
+ * JSON shape and the future `gs parity --semantic`.
296
+ */
297
+ /**
298
+ * Bumped ONLY on a breaking change to the diagnostic shape. New CODES and new
299
+ * OPTIONAL fields are additive and do NOT bump it — a consumer keys on `code`
300
+ * and tolerates unknown ones.
301
+ */
302
+ const DIAGNOSTIC_SCHEMA_VERSION = 1;
303
+ /** Classic edit distance (iterative two-row DP). Small strings only (ids). */
304
+ function levenshtein(a, b) {
305
+ if (a === b) return 0;
306
+ if (a.length === 0) return b.length;
307
+ if (b.length === 0) return a.length;
308
+ let prev = new Array(b.length + 1);
309
+ let curr = new Array(b.length + 1);
310
+ for (let j = 0; j <= b.length; j++) prev[j] = j;
311
+ for (let i = 1; i <= a.length; i++) {
312
+ curr[0] = i;
313
+ for (let j = 1; j <= b.length; j++) {
314
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
315
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
316
+ }
317
+ [prev, curr] = [curr, prev];
318
+ }
319
+ return prev[b.length];
320
+ }
321
+ /**
322
+ * The nearest candidate to `name` within a reasonable edit budget (≤ 2, or a
323
+ * third of the length for longer names), or undefined if none is close enough —
324
+ * so a wildly-different typo doesn't get a misleading "did you mean" tail.
325
+ */
326
+ function nearestId(name, candidates) {
327
+ const budget = Math.max(2, Math.floor(name.length / 3));
328
+ let best;
329
+ let bestD = Infinity;
330
+ for (const c of candidates) {
331
+ const d = levenshtein(name, c);
332
+ if (d < bestD && d <= budget) {
333
+ bestD = d;
334
+ best = c;
335
+ }
336
+ }
337
+ return best;
338
+ }
339
+ /** Resolve the OWNING node + remaining prop path for a `'<id>/<prop>'` target,
340
+ * by the same longest-registered-id-prefix walk `scene.resolveTarget` uses. */
341
+ function owningNode(scene, target) {
342
+ for (let slash = target.lastIndexOf("/"); slash > 0; slash = target.lastIndexOf("/", slash - 1)) {
343
+ const node = scene.nodes.get(target.slice(0, slash));
344
+ if (node) return {
345
+ node,
346
+ prop: target.slice(slash + 1)
347
+ };
348
+ }
349
+ }
350
+ /** True when `node` is a FLOWABLE child of a Layout — i.e. its parent is a
351
+ * Layout (duck-typed via the `isLayoutNode` static marker, so this stays off
352
+ * the Yoga import) AND it has an intrinsic box (Layout flow-positions exactly
353
+ * these; non-flowable children emit absolutely, untouched). Flowable-ness is
354
+ * probed with the STATELESS estimating measurer — never the scene's injected
355
+ * one — so no backend font cache is warmed (render-neutrality). */
356
+ function isFlowableLayoutChild(node) {
357
+ const parent = node.parent;
358
+ if (!parent) return false;
359
+ if (parent.constructor?.isLayoutNode !== true) return false;
360
+ return node.intrinsicSize(estimatingMeasurer) !== null;
361
+ }
362
+ /**
363
+ * Eagerly validate a scene (+ optional timeline) and AGGREGATE every problem —
364
+ * the static belt that surfaces at the AUTHORING site what the render-time
365
+ * `UnboundTargetError` backstop only shows one-at-a-time from deep in the render
366
+ * loop. Pure read (see the module header): calling it never changes a subsequent
367
+ * render's bytes.
368
+ *
369
+ * With a `doc`, every track target is walked through the existing
370
+ * `scene.resolveTarget`; an unresolved one becomes an `UNKNOWN_TARGET` error
371
+ * with a Levenshtein nearest-id / nearest-prop suggestion, and a
372
+ * `position`/`position.*` track on a flowable Layout child becomes a
373
+ * `YOGA_CHILD_POSITION` warning. Scene-only checks (off-canvas, measurer
374
+ * fallback) run regardless.
375
+ */
376
+ function validateScene(scene, doc) {
377
+ const diagnostics = [];
378
+ const push = (code, severity, message, extra) => {
379
+ diagnostics.push({
380
+ schemaVersion: 1,
381
+ code,
382
+ severity,
383
+ message,
384
+ ...extra?.node !== void 0 ? { node: extra.node } : {},
385
+ ...extra?.track !== void 0 ? { track: extra.track } : {}
386
+ });
387
+ };
388
+ untracked(() => {
389
+ if (doc) for (const tr of doc.tracks) {
390
+ const target = tr.target;
391
+ const owner = owningNode(scene, target);
392
+ if (!scene.resolveTarget(target)) {
393
+ push("UNKNOWN_TARGET", "error", unknownTargetMessage(scene, target, owner), {
394
+ track: target,
395
+ ...owner?.node.id !== void 0 ? { node: owner.node.id } : {}
396
+ });
397
+ continue;
398
+ }
399
+ if (owner && /^position(\.[xy])?$/.test(owner.prop) && isFlowableLayoutChild(owner.node)) push("YOGA_CHILD_POSITION", "warning", `track '${target}' drives the position of a flowable child of a Layout — the flex slot overrides it, so the keyframes are confounded. Animate the Layout (gap/padding/width) or wrap the child in a Group and drive THAT, or make the child absolute (non-flowable).`, {
400
+ track: target,
401
+ ...owner.node.id !== void 0 ? { node: owner.node.id } : {}
402
+ });
403
+ }
404
+ const animatedPos = /* @__PURE__ */ new Set();
405
+ if (doc) for (const tr of doc.tracks) {
406
+ const m = /^(.+)\/position(?:\.[xy])?$/.exec(tr.target);
407
+ if (m) animatedPos.add(m[1]);
408
+ }
409
+ const { w, h } = scene.size;
410
+ let sawText = false;
411
+ const visit = (node) => {
412
+ if (isTextNode(node)) sawText = true;
413
+ const pos = node.position;
414
+ if (node.id !== void 0 && !animatedPos.has(node.id) && typeof pos === "function") {
415
+ const [px, py] = pos();
416
+ if (Number.isFinite(px) && Number.isFinite(py) && (px < 0 || py < 0 || px > w || py > h)) push("OFF_CANVAS", "info", `node '${node.id}' has a static position [${px}, ${py}] outside the ${w}×${h} viewport — it may not be visible (fine if intentional, e.g. an off-screen start).`, { node: node.id });
417
+ }
418
+ const children = node.children;
419
+ if (Array.isArray(children)) for (const c of children) visit(c);
420
+ };
421
+ visit(scene.root);
422
+ if (sawText && isEstimatingMeasurer(scene.textMeasurer)) push("MEASURER_FALLBACK", "info", "the scene contains Text but no real text measurer is injected — line breaking uses a rough per-character estimate. Call setTextMeasurer(backend) / setDefaultMeasurer(...) for exact layout.");
423
+ });
424
+ return {
425
+ schemaVersion: 1,
426
+ hasErrors: diagnostics.some((d) => d.severity === "error"),
427
+ diagnostics
428
+ };
429
+ }
430
+ /** Build the friendliest UNKNOWN_TARGET message: nearest-PROP when the node
431
+ * exists (a typo'd prop), else nearest node-ID (a typo'd id). */
432
+ function unknownTargetMessage(scene, target, owner) {
433
+ if (owner) {
434
+ const props = owner.node.listTargets().map((t) => t.path);
435
+ const near = nearestId(owner.prop, props);
436
+ return `track targets '${target}' but node '${owner.node.id}' has no animatable prop '${owner.prop}'` + (near ? ` — did you mean '${owner.node.id}/${near}'?` : "");
437
+ }
438
+ const slash = target.indexOf("/");
439
+ const idPart = slash >= 0 ? target.slice(0, slash) : target;
440
+ const near = nearestId(idPart, scene.nodes.keys());
441
+ return `track targets '${target}' but no node '${idPart}' exists in the scene` + (near ? ` — did you mean '${near}'?` : "");
442
+ }
443
+ /** Duck-typed Text detection (avoids importing nodes.ts / dragging its
444
+ * construction surface onto the diagnostics bundle): Text overrides
445
+ * `describeType` to `'Text'`. */
446
+ function isTextNode(node) {
447
+ return node.describeType === "Text";
448
+ }
449
+ /**
450
+ * Read a node's RESOLVED prop value at time `t` — the always-truthful read, the
451
+ * anti-false-conclusion primitive for inspection tooling (and load-bearing for
452
+ * 0.60 `critique()`). A BOUND prop returns its REAL bound value at `t` (not the
453
+ * misleading static default); an unbound prop returns its static value at any
454
+ * `t`; an unresolvable target returns `undefined`.
455
+ *
456
+ * Thin wrapper over the existing `scene.resolveTarget` + core's `evaluateAt`
457
+ * (read inside a read phase). NOTE: the scene must be BOUND (`bindScene`/
458
+ * `evaluate` already ran for the doc) for a track-driven value to appear —
459
+ * `resolveAt` reads the live signal, it does not itself bind. Render-neutral:
460
+ * the scene playhead is restored after the read.
461
+ */
462
+ function resolveAt(scene, target, t) {
463
+ const sig = scene.resolveTarget(target);
464
+ if (typeof sig !== "function") return void 0;
465
+ const prev = scene.playhead.peek();
466
+ try {
467
+ return evaluateAt(scene.playhead, t, () => sig());
468
+ } finally {
469
+ scene.playhead.forceSet(prev);
470
+ }
471
+ }
472
+ /**
473
+ * Announce which props are CURRENTLY bound on THIS node instance (not just
474
+ * type-level bindable). Reads `signal.isBound` per registered target — a pure
475
+ * inspection read. Pair with `resolveAt` to read a bound prop's real value.
476
+ */
477
+ function instanceProps(node) {
478
+ return node.listTargets().map(({ path, expects }) => {
479
+ return {
480
+ path,
481
+ expects,
482
+ bound: node.resolveTarget(path)?.isBound === true
483
+ };
484
+ });
485
+ }
486
+ //#endregion
487
+ export { DIAGNOSTIC_SCHEMA_VERSION, DL_SNAPSHOT_VERSION, DlSnapshotError, auditCacheCold, collapseReplacer, collectLocalizedTextUsages, collectTextUsages, diffDisplayLists, formatDisplayDiff, instanceProps, levenshtein, nearestId, parseDisplaySnapshot, resolveAt, serializeDisplayList, validateScene, validateSceneFonts };
@@ -1,5 +1,5 @@
1
1
  import { n as DisplayList } from "./displayList.js";
2
- import { r as Scene } from "./scene.js";
2
+ import { i as Scene } from "./scene.js";
3
3
  import { Timeline } from "@glissade/core";
4
4
 
5
5
  //#region src/identity.d.ts
package/dist/identity.js CHANGED
@@ -1,4 +1,4 @@
1
- import { U as createDisplayListBuilder } from "./nodes.js";
1
+ import { W as createDisplayListBuilder } from "./nodes.js";
2
2
  import { r as bindScene } from "./scene.js";
3
3
  import { evaluateAt } from "@glissade/core";
4
4
  //#region src/identity.ts
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { C as Mat2x3, D as matEquals, E as invert, O as multiply, S as IDENTITY, T as fromTRS, _ as StrokeStyle, a as FilterSpec, b as glow, c as MeshInterpolation, d as Paint, f as PathSeg, g as ShaderRef, h as ResourceId, i as DrawCommand, l as MeshPaint, m as Resource, n as DisplayList, o as FilterValidationError, p as Rect$1, r as DisplayListBuilder, s as FontSpec, t as BlendMode, u as MeshPoint, v as createDisplayListBuilder, w as applyToPoint, x as validateFilters, y as filtersToCanvasFilter } from "./displayList.js";
2
- import { $ as isEstimatingMeasurer, A as hachureLines, B as Node, C as HachureSpec, D as SketchValidationError, E as SketchStyle, F as validateSketch, G as MEASURE_QUANTUM_PX, H as NodeProps, I as AnchorSpec, J as WrappedTextMetrics, K as TextMeasurer, L as BindablePropTarget, M as roughen, N as sketchStrokes, O as arcLength, P as validateHachure, Q as estimatingMeasurer, R as EvalContext, S as roundedRectSegs, T as ResolvedSketch, U as PropInit, V as NodeConstructionError, W as resolveAnchor, X as assertFiniteFontSize, Y as __resetEstimateWarnings, Z as breakLines, _ as VideoProps, a as Group, b as pathFromSegs, c as LineBox, d as Rect, et as measureWrappedText, f as RevealMark, g as Video, h as TextProps, i as GraphemeBox, it as setDefaultMeasurer, j as resolveSketch, k as flatten, l as Path, m as Text, n as ClipRegion, nt as segmentGraphemes, o as ImageNode, p as ShapeProps, q as TextMetricsLite, r as Custom, rt as segmentWords, s as ImageProps, t as Circle, tt as quantize, u as PathProps, v as WordBox, w as Polyline, x as revealSchedule, y as coercePathData, z as HitArea } from "./nodes.js";
2
+ import { $ as estimatingMeasurer, A as hachureLines, B as Node, C as HachureSpec, D as SketchValidationError, E as SketchStyle, F as validateSketch, G as MEASURE_QUANTUM_PX, H as NodeProps, I as AnchorSpec, J as TextMetricsLite, L as BindablePropTarget, M as roughen, N as sketchStrokes, O as arcLength, P as validateHachure, Q as breakLines, R as EvalContext, S as roundedRectSegs, T as ResolvedSketch, U as PropInit, V as NodeConstructionError, W as resolveAnchor, X as __resetEstimateWarnings, Y as WrappedTextMetrics, Z as assertFiniteFontSize, _ as VideoProps, a as Group, at as setDefaultMeasurer, b as pathFromSegs, c as LineBox, d as Rect, et as isEstimatingMeasurer, f as RevealMark, g as Video, h as TextProps, i as GraphemeBox, it as segmentWords, j as resolveSketch, k as flatten, l as Path, m as Text, n as ClipRegion, nt as quantize, o as ImageNode, p as ShapeProps, q as TextMeasurer, r as Custom, rt as segmentGraphemes, s as ImageProps, t as Circle, tt as measureWrappedText, u as PathProps, v as WordBox, w as Polyline, x as revealSchedule, y as coercePathData, z as HitArea } from "./nodes.js";
3
3
  import { t as collapseReplacer } from "./collapseReplacer.js";
4
- import { a as SceneModule, c as evaluate, i as SceneInit, n as ReservedNodeIdError, o as bindScene, r as Scene, s as createScene, t as DuplicateNodeIdError } from "./scene.js";
4
+ import { a as SceneInit, c as createScene, i as Scene, l as evaluate, n as DuplicateNodeIdError, o as SceneModule, r as ReservedNodeIdError, s as bindScene, t as BindSceneOptions } from "./scene.js";
5
5
  import { a as typewriter, c as textCursor, i as TypewriterResult, n as StepMark, o as TextCursor, r as TypeEdit, s as TextCursorProps, t as EditMark } from "./typewriter.js";
6
6
  import { a as EachLayout, c as EachResult, i as EachError, l as Place, n as EachContext, o as EachMotion, r as EachDistribute, s as EachOpts, t as EachBox, u as each } from "./each.js";
7
7
  import { a as LayoutEngineMissingError, c as requireLayoutEngine, i as LayoutEngine, l as setLayoutEngine, n as LayoutChildSpec, r as LayoutContainerSpec, s as getLayoutEngine, t as LayoutBox } from "./layoutEngine.js";
@@ -493,4 +493,4 @@ declare function meshRasterSize(bw: number, bh: number): {
493
493
  h: number;
494
494
  };
495
495
  //#endregion
496
- export { ALL_FILTER_KINDS, type AnchorSpec, type BackendCaps, type BindablePropTarget, type BlendMode, type Bounds, type CanvasLike, Circle, type ClipRegion, ColdAssetError, type Ctx2DLike, Custom, DeterminismViolationError, type DisplayList, type DisplayListBuilder, type DrawCommand, type DrawOnEachOptions, type DrawOnOptions, DuplicateNodeIdError, type EachBox, type EachContext, type EachDistribute, EachError, type EachLayout, type EachMotion, type EachOpts, type EachResult, Echo, type EchoProps, type EditMark, type EvalContext, type FilterKind, type FilterSpec, FilterValidationError, type FontSpec, type GraphemeBox, Group, type GuardMode, type HachureSpec, Highlight, type HighlightProps, type HitArea, IDENTITY, ImageNode as Image, ImageNode, type ImageDataLike, type ImageHandle, type ImageProps, type LayerCacheEntry, type LayerStore, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LineBox, MEASURE_QUANTUM_PX, MESH_DOWNSCALE, MESH_SHEPARD_POWER, MESH_SIGMA, type Mat2x3, type MeshInterpolation, type MeshPaint, type MeshPoint, MotionBlur, type MotionBlurProps, NODE_TAXONOMY, Node, NodeConstructionError, type NodeProps, type NodeTypeName, type Paint, Path, type PathLike, type PathProps, type PathSeg, type Place, type Polyline, type PropInit, Raster2D, type Raster2DHost, Rect, type Rect$1 as RectShape, type RenderBackend, ReservedNodeIdError, type ResolvedSketch, type Resource, type ResourceId, type RevealMark, type Scene, type SceneInit, type SceneModule, type ShaderCaps, ShaderEffect, type ShaderEffectProps, type ShaderRef, type ShapeProps, type SketchStyle, SketchValidationError, type StepMark, type StrokeStyle, Text, TextCursor, type TextCursorProps, type TextMeasurer, type TextMetricsLite, type TextProps, TrackMatte, type TrackMatteProps, type TypeEdit, type TypewriterResult, Video, type VideoFrameSource, type VideoProps, type WordBox, type WrappedTextMetrics, __resetEstimateWarnings, applyToPoint, arcLength, assertFiniteFontSize, bindScene, breakLines, coercePathData, collapseReplacer, createDisplayListBuilder, createScene, drawOn, drawOnEach, each, echo, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, fontString, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, isEstimatingMeasurer, matEquals, measureWrappedText, meshRasterSize, motionBlur, multiply, pathFromSegs, quantize, rasterizeMesh, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, trackMatte, typewriter, validateFilters, validateHachure, validateSketch, withDeterminismGuards };
496
+ export { ALL_FILTER_KINDS, type AnchorSpec, type BackendCaps, type BindSceneOptions, type BindablePropTarget, type BlendMode, type Bounds, type CanvasLike, Circle, type ClipRegion, ColdAssetError, type Ctx2DLike, Custom, DeterminismViolationError, type DisplayList, type DisplayListBuilder, type DrawCommand, type DrawOnEachOptions, type DrawOnOptions, DuplicateNodeIdError, type EachBox, type EachContext, type EachDistribute, EachError, type EachLayout, type EachMotion, type EachOpts, type EachResult, Echo, type EchoProps, type EditMark, type EvalContext, type FilterKind, type FilterSpec, FilterValidationError, type FontSpec, type GraphemeBox, Group, type GuardMode, type HachureSpec, Highlight, type HighlightProps, type HitArea, IDENTITY, ImageNode as Image, ImageNode, type ImageDataLike, type ImageHandle, type ImageProps, type LayerCacheEntry, type LayerStore, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LineBox, MEASURE_QUANTUM_PX, MESH_DOWNSCALE, MESH_SHEPARD_POWER, MESH_SIGMA, type Mat2x3, type MeshInterpolation, type MeshPaint, type MeshPoint, MotionBlur, type MotionBlurProps, NODE_TAXONOMY, Node, NodeConstructionError, type NodeProps, type NodeTypeName, type Paint, Path, type PathLike, type PathProps, type PathSeg, type Place, type Polyline, type PropInit, Raster2D, type Raster2DHost, Rect, type Rect$1 as RectShape, type RenderBackend, ReservedNodeIdError, type ResolvedSketch, type Resource, type ResourceId, type RevealMark, type Scene, type SceneInit, type SceneModule, type ShaderCaps, ShaderEffect, type ShaderEffectProps, type ShaderRef, type ShapeProps, type SketchStyle, SketchValidationError, type StepMark, type StrokeStyle, Text, TextCursor, type TextCursorProps, type TextMeasurer, type TextMetricsLite, type TextProps, TrackMatte, type TrackMatteProps, type TypeEdit, type TypewriterResult, Video, type VideoFrameSource, type VideoProps, type WordBox, type WrappedTextMetrics, __resetEstimateWarnings, applyToPoint, arcLength, assertFiniteFontSize, bindScene, breakLines, coercePathData, collapseReplacer, createDisplayListBuilder, createScene, drawOn, drawOnEach, each, echo, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, fontString, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, isEstimatingMeasurer, matEquals, measureWrappedText, meshRasterSize, motionBlur, multiply, pathFromSegs, quantize, rasterizeMesh, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, trackMatte, typewriter, validateFilters, validateHachure, validateSketch, withDeterminismGuards };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as multiply, A as __resetEstimateWarnings, B as setDefaultMeasurer, C as Node, F as isEstimatingMeasurer, G as glow, H as FilterValidationError, I as measureWrappedText, J as IDENTITY, K as validateFilters, L as quantize, M as breakLines, N as estimatingMeasurer, Q as matEquals, R as segmentGraphemes, S as validateSketch, T as resolveAnchor, U as createDisplayListBuilder, W as filtersToCanvasFilter, X as fromTRS, Y as applyToPoint, Z as invert, a as Path, b as sketchStrokes, c as Video, d as revealSchedule, f as roundedRectSegs, g as hachureLines, h as flatten, i as ImageNode, j as assertFiniteFontSize, k as MEASURE_QUANTUM_PX, l as coercePathData, m as arcLength, n as Custom, o as Rect, p as SketchValidationError, q as collapseReplacer, r as Group, s as Text, t as Circle, u as pathFromSegs, v as resolveSketch, w as NodeConstructionError, x as validateHachure, y as roughen, z as segmentWords } from "./nodes.js";
1
+ import { $ as matEquals, B as segmentWords, C as Node, G as filtersToCanvasFilter, I as isEstimatingMeasurer, J as collapseReplacer, K as glow, L as measureWrappedText, M as assertFiniteFontSize, N as breakLines, P as estimatingMeasurer, Q as invert, R as quantize, S as validateSketch, T as resolveAnchor, U as FilterValidationError, V as setDefaultMeasurer, W as createDisplayListBuilder, X as applyToPoint, Y as IDENTITY, Z as fromTRS, a as Path, b as sketchStrokes, c as Video, d as revealSchedule, et as multiply, f as roundedRectSegs, g as hachureLines, h as flatten, i as ImageNode, j as __resetEstimateWarnings, k as MEASURE_QUANTUM_PX, l as coercePathData, m as arcLength, n as Custom, o as Rect, p as SketchValidationError, q as validateFilters, r as Group, s as Text, t as Circle, u as pathFromSegs, v as resolveSketch, w as NodeConstructionError, x as validateHachure, y as roughen, z as segmentGraphemes } from "./nodes.js";
2
2
  import { a as evaluate, i as createScene, n as ReservedNodeIdError, r as bindScene, t as DuplicateNodeIdError } from "./scene.js";
3
3
  import { i as setLayoutEngine, n as getLayoutEngine, r as requireLayoutEngine, t as LayoutEngineMissingError } from "./layoutEngine.js";
4
4
  import { n as TextCursor, r as textCursor, t as typewriter } from "./typewriter.js";
@@ -1,5 +1,5 @@
1
1
  import { r as DisplayListBuilder } from "./displayList.js";
2
- import { B as Node, H as NodeProps, K as TextMeasurer, R as EvalContext, U as PropInit, a as Group } from "./nodes.js";
2
+ import { B as Node, H as NodeProps, R as EvalContext, U as PropInit, a as Group, q as TextMeasurer } from "./nodes.js";
3
3
  import { r as LayoutContainerSpec } from "./layoutEngine.js";
4
4
  import { BindableSignal } from "@glissade/core";
5
5
 
@@ -1,4 +1,4 @@
1
- import { P as fallbackMeasurer, r as Group } from "./nodes.js";
1
+ import { F as fallbackMeasurer, r as Group } from "./nodes.js";
2
2
  import { r as requireLayoutEngine } from "./layoutEngine.js";
3
3
  import { computed, signal } from "@glissade/core";
4
4
  //#region src/layoutCtors.ts
package/dist/motion.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as multiply, C as Node, X as fromTRS, _ as hashStr, r as Group, s as Text, t as Circle } from "./nodes.js";
1
+ import { C as Node, Z as fromTRS, _ as hashStr, et as multiply, r as Group, s as Text, t as Circle } from "./nodes.js";
2
2
  import { a as FollowPath, c as pathLength, i as orientToPath, l as pointAtLength, n as OrientToPath, o as followPath, r as lookAt, s as motionPath, t as LookAt } from "./orient.js";
3
3
  import { n as each } from "./each.js";
4
4
  import { bake, signal, valueNoise, vec2Signal } from "@glissade/core";
package/dist/nodes.d.ts CHANGED
@@ -51,11 +51,26 @@ declare const estimatingMeasurer: TextMeasurer;
51
51
  * registered measurer never trips it.
52
52
  */
53
53
  declare function isEstimatingMeasurer(m: TextMeasurer): boolean;
54
+ /**
55
+ * Thrown by a measurer-requiring helper (`splitText`/`fitText`) when it resolved
56
+ * to the estimating fallback AND the caller opted into fail-loud
57
+ * (`requireMeasurer: true`, 0.59 measurer fail-loud). The default stays
58
+ * warn-once (below) — this is the opt-in that turns the silent geometry
59
+ * DEGRADATION into a hard error for pipelines that need exact layout.
60
+ */
61
+ declare class MeasurerRequiredError extends Error {
62
+ constructor(site: string);
63
+ }
54
64
  /**
55
65
  * One-shot dev-warning when a build-time geometry getter resolved its measurer
56
66
  * to the rough per-character estimate (no backend, no `setDefaultMeasurer`).
57
67
  * `site` keys the de-dupe so each distinct caller warns at most once. Silent
58
68
  * when a real measurer is in play — the estimate is the only footgun here.
69
+ *
70
+ * 0.59: pass `strict` to make the same fallback FAIL LOUD ({@link
71
+ * MeasurerRequiredError}) instead of warn-once — the `requireMeasurer` opt-in on
72
+ * splitText/fitText. Default (`strict` falsy) preserves the exact prior
73
+ * warn-once behavior (byte/behavior-neutral).
59
74
  */
60
75
 
61
76
  /**
@@ -961,4 +976,4 @@ interface RevealMark {
961
976
  */
962
977
  declare function revealSchedule(text: Text, reveal: Track<number>, measurer?: TextMeasurer): RevealMark[];
963
978
  //#endregion
964
- export { isEstimatingMeasurer as $, hachureLines as A, Node as B, HachureSpec as C, SketchValidationError as D, SketchStyle as E, validateSketch as F, MEASURE_QUANTUM_PX as G, NodeProps as H, AnchorSpec as I, WrappedTextMetrics as J, TextMeasurer as K, BindablePropTarget as L, roughen as M, sketchStrokes as N, arcLength as O, validateHachure as P, estimatingMeasurer as Q, EvalContext as R, roundedRectSegs as S, ResolvedSketch as T, PropInit as U, NodeConstructionError as V, resolveAnchor as W, assertFiniteFontSize as X, __resetEstimateWarnings as Y, breakLines as Z, VideoProps as _, Group as a, pathFromSegs as b, LineBox as c, Rect as d, measureWrappedText as et, RevealMark as f, Video as g, TextProps as h, GraphemeBox as i, setDefaultMeasurer as it, resolveSketch as j, flatten as k, Path as l, Text as m, ClipRegion as n, segmentGraphemes as nt, ImageNode as o, ShapeProps as p, TextMetricsLite as q, Custom as r, segmentWords as rt, ImageProps as s, Circle as t, quantize as tt, PathProps as u, WordBox as v, Polyline as w, revealSchedule as x, coercePathData as y, HitArea as z };
979
+ export { estimatingMeasurer as $, hachureLines as A, Node as B, HachureSpec as C, SketchValidationError as D, SketchStyle as E, validateSketch as F, MEASURE_QUANTUM_PX as G, NodeProps as H, AnchorSpec as I, TextMetricsLite as J, MeasurerRequiredError as K, BindablePropTarget as L, roughen as M, sketchStrokes as N, arcLength as O, validateHachure as P, breakLines as Q, EvalContext as R, roundedRectSegs as S, ResolvedSketch as T, PropInit as U, NodeConstructionError as V, resolveAnchor as W, __resetEstimateWarnings as X, WrappedTextMetrics as Y, assertFiniteFontSize as Z, VideoProps as _, Group as a, setDefaultMeasurer as at, pathFromSegs as b, LineBox as c, Rect as d, isEstimatingMeasurer as et, RevealMark as f, Video as g, TextProps as h, GraphemeBox as i, segmentWords as it, resolveSketch as j, flatten as k, Path as l, Text as m, ClipRegion as n, quantize as nt, ImageNode as o, ShapeProps as p, TextMeasurer as q, Custom as r, segmentGraphemes as rt, ImageProps as s, Circle as t, measureWrappedText as tt, PathProps as u, WordBox as v, Polyline as w, revealSchedule as x, coercePathData as y, HitArea as z };
package/dist/nodes.js CHANGED
@@ -324,13 +324,32 @@ function isEstimatingMeasurer(m) {
324
324
  }
325
325
  const warnedEstimate = /* @__PURE__ */ new Set();
326
326
  /**
327
+ * Thrown by a measurer-requiring helper (`splitText`/`fitText`) when it resolved
328
+ * to the estimating fallback AND the caller opted into fail-loud
329
+ * (`requireMeasurer: true`, 0.59 measurer fail-loud). The default stays
330
+ * warn-once (below) — this is the opt-in that turns the silent geometry
331
+ * DEGRADATION into a hard error for pipelines that need exact layout.
332
+ */
333
+ var MeasurerRequiredError = class extends Error {
334
+ constructor(site) {
335
+ super(`${site}: no real text measurer available and { requireMeasurer: true } was set — part geometry would use a rough per-character estimate. Pass { measurer } or call setTextMeasurer()/setDefaultMeasurer() first.`);
336
+ this.name = "MeasurerRequiredError";
337
+ }
338
+ };
339
+ /**
327
340
  * One-shot dev-warning when a build-time geometry getter resolved its measurer
328
341
  * to the rough per-character estimate (no backend, no `setDefaultMeasurer`).
329
342
  * `site` keys the de-dupe so each distinct caller warns at most once. Silent
330
343
  * when a real measurer is in play — the estimate is the only footgun here.
344
+ *
345
+ * 0.59: pass `strict` to make the same fallback FAIL LOUD ({@link
346
+ * MeasurerRequiredError}) instead of warn-once — the `requireMeasurer` opt-in on
347
+ * splitText/fitText. Default (`strict` falsy) preserves the exact prior
348
+ * warn-once behavior (byte/behavior-neutral).
331
349
  */
332
- function warnIfEstimating(m, site) {
350
+ function warnIfEstimating(m, site, strict = false) {
333
351
  if (!isEstimatingMeasurer(m)) return;
352
+ if (strict) throw new MeasurerRequiredError(site);
334
353
  if (warnedEstimate.has(site)) return;
335
354
  warnedEstimate.add(site);
336
355
  emitDevWarning(`${site}: no text measurer available — using a rough per-character estimate; pass { measurer } or call after setTextMeasurer()/setDefaultMeasurer() for exact layout.`);
@@ -2358,4 +2377,4 @@ function revealSchedule(text, reveal, measurer) {
2358
2377
  return marks;
2359
2378
  }
2360
2379
  //#endregion
2361
- export { multiply as $, __resetEstimateWarnings as A, setDefaultMeasurer as B, Node as C, NODE_CONSTRUCTION_PROP_NAMES as D, BASE_CONSTRUCTION_PROP_NAMES as E, isEstimatingMeasurer as F, glow as G, FilterValidationError as H, measureWrappedText as I, IDENTITY as J, validateFilters as K, quantize as L, breakLines as M, estimatingMeasurer as N, isConstructionProp as O, fallbackMeasurer as P, matEquals as Q, segmentGraphemes as R, validateSketch as S, resolveAnchor as T, createDisplayListBuilder as U, warnIfEstimating as V, filtersToCanvasFilter as W, fromTRS as X, applyToPoint as Y, invert as Z, hashStr as _, Path as a, sketchStrokes as b, Video as c, revealSchedule as d, roundedRectSegs as f, hachureLines as g, flatten as h, ImageNode as i, assertFiniteFontSize as j, MEASURE_QUANTUM_PX as k, coercePathData as l, arcLength as m, Custom as n, Rect as o, SketchValidationError as p, collapseReplacer as q, Group as r, Text as s, Circle as t, pathFromSegs as u, resolveSketch as v, NodeConstructionError as w, validateHachure as x, roughen as y, segmentWords as z };
2380
+ export { matEquals as $, MeasurerRequiredError as A, segmentWords as B, Node as C, NODE_CONSTRUCTION_PROP_NAMES as D, BASE_CONSTRUCTION_PROP_NAMES as E, fallbackMeasurer as F, filtersToCanvasFilter as G, warnIfEstimating as H, isEstimatingMeasurer as I, collapseReplacer as J, glow as K, measureWrappedText as L, assertFiniteFontSize as M, breakLines as N, isConstructionProp as O, estimatingMeasurer as P, invert as Q, quantize as R, validateSketch as S, resolveAnchor as T, FilterValidationError as U, setDefaultMeasurer as V, createDisplayListBuilder as W, applyToPoint as X, IDENTITY as Y, fromTRS as Z, hashStr as _, Path as a, sketchStrokes as b, Video as c, revealSchedule as d, multiply as et, roundedRectSegs as f, hachureLines as g, flatten as h, ImageNode as i, __resetEstimateWarnings as j, MEASURE_QUANTUM_PX as k, coercePathData as l, arcLength as m, Custom as n, Rect as o, SketchValidationError as p, validateFilters as q, Group as r, Text as s, Circle as t, pathFromSegs as u, resolveSketch as v, NodeConstructionError as w, validateHachure as x, roughen as y, segmentGraphemes as z };
package/dist/orient.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as Node, Y as applyToPoint, a as Path } from "./nodes.js";
1
+ import { C as Node, X as applyToPoint, a as Path } from "./nodes.js";
2
2
  import { signal } from "@glissade/core";
3
3
  //#region src/motionPath.ts
4
4
  /**
package/dist/scene.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as DisplayList, s as FontSpec } from "./displayList.js";
2
- import { B as Node, J as WrappedTextMetrics, K as TextMeasurer, L as BindablePropTarget, a as Group } from "./nodes.js";
2
+ import { B as Node, L as BindablePropTarget, Y as WrappedTextMetrics, a as Group, q as TextMeasurer } from "./nodes.js";
3
3
  import { BoundTimeline, CompiledTimeline, Playhead, Timeline } from "@glissade/core";
4
4
 
5
5
  //#region src/scene.d.ts
@@ -55,7 +55,18 @@ interface BindingCacheEntry {
55
55
  compiled: CompiledTimeline;
56
56
  bound: BoundTimeline;
57
57
  }
58
- declare function bindScene(scene: Scene, doc: Timeline): BindingCacheEntry;
58
+ /** Options for {@link bindScene}. */
59
+ interface BindSceneOptions {
60
+ /**
61
+ * 0.59 mode gate (threaded to {@link BindOptions.onUnbound}): `'throw'`
62
+ * (default, loud) vs `'warn'` (prod embeds — downgrade an unresolved target to
63
+ * a dev-warning and skip the track). `mount({ production: true })` passes
64
+ * `'warn'` here on its first (memo-warming) bind, so the whole render loop runs
65
+ * in the chosen mode. Byte-neutral for every valid scene (see BindOptions).
66
+ */
67
+ onUnbound?: 'throw' | 'warn';
68
+ }
69
+ declare function bindScene(scene: Scene, doc: Timeline, opts?: BindSceneOptions): BindingCacheEntry;
59
70
  /**
60
71
  * The non-negotiable contract (§2.5): same (scene, timeline, t) → identical
61
72
  * DisplayList, in any call order. Never awaits; asset readiness is the
@@ -76,4 +87,4 @@ declare function evaluate(scene: Scene, doc: Timeline, t: number): DisplayList;
76
87
  */
77
88
  declare function evaluate(scene: Scene): DisplayList;
78
89
  //#endregion
79
- export { SceneModule as a, evaluate as c, SceneInit as i, ReservedNodeIdError as n, bindScene as o, Scene as r, createScene as s, DuplicateNodeIdError as t };
90
+ export { SceneInit as a, createScene as c, Scene as i, evaluate as l, DuplicateNodeIdError as n, SceneModule as o, ReservedNodeIdError as r, bindScene as s, BindSceneOptions as t };
package/dist/scene.js CHANGED
@@ -1,4 +1,4 @@
1
- import { I as measureWrappedText, O as isConstructionProp, P as fallbackMeasurer, U as createDisplayListBuilder, r as Group } from "./nodes.js";
1
+ import { F as fallbackMeasurer, L as measureWrappedText, O as isConstructionProp, W as createDisplayListBuilder, r as Group } from "./nodes.js";
2
2
  import { bindTimeline, compileTimeline, createPlayhead, evaluateAt, signal } from "@glissade/core";
3
3
  //#region src/scene.ts
4
4
  /**
@@ -75,7 +75,7 @@ function constructionPropMessage(nodes, target) {
75
75
  return;
76
76
  }
77
77
  }
78
- function bindScene(scene, doc) {
78
+ function bindScene(scene, doc, opts = {}) {
79
79
  let perScene = bindings.get(scene);
80
80
  if (!perScene) {
81
81
  perScene = /* @__PURE__ */ new WeakMap();
@@ -86,7 +86,10 @@ function bindScene(scene, doc) {
86
86
  const compiled = compileTimeline(doc);
87
87
  entry = {
88
88
  compiled,
89
- bound: bindTimeline(compiled, scene.resolveTarget, scene.playhead, { unboundMessage: (target) => constructionPropMessage(scene.nodes, target) })
89
+ bound: bindTimeline(compiled, scene.resolveTarget, scene.playhead, {
90
+ unboundMessage: (target) => constructionPropMessage(scene.nodes, target),
91
+ ...opts.onUnbound !== void 0 ? { onUnbound: opts.onUnbound } : {}
92
+ })
90
93
  };
91
94
  perScene.set(doc, entry);
92
95
  }
package/dist/tokens.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as Node, J as IDENTITY, Q as matEquals, f as roundedRectSegs } from "./nodes.js";
1
+ import { $ as matEquals, C as Node, Y as IDENTITY, f as roundedRectSegs } from "./nodes.js";
2
2
  import { signal, vec2Signal } from "@glissade/core";
3
3
  //#region src/tokenHighlight.ts
4
4
  /**
package/dist/type.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { K as TextMeasurer, a as Group, c as LineBox, h as TextProps, i as GraphemeBox, m as Text, v as WordBox } from "./nodes.js";
1
+ import { K as MeasurerRequiredError, a as Group, c as LineBox, h as TextProps, i as GraphemeBox, m as Text, q as TextMeasurer, v as WordBox } from "./nodes.js";
2
2
  import { o as TextCursor, s as TextCursorProps, t as EditMark } from "./typewriter.js";
3
3
  import { EaseSpec, Track } from "@glissade/core";
4
4
 
@@ -20,6 +20,14 @@ interface SplitTextOpts {
20
20
  * Text geometry getters use.
21
21
  */
22
22
  measurer?: TextMeasurer;
23
+ /**
24
+ * 0.59 measurer fail-loud OPT-IN. By default a split with no real measurer
25
+ * warns once and degrades to the rough per-character estimate. Set
26
+ * `requireMeasurer: true` to instead THROW `MeasurerRequiredError` — for a
27
+ * pipeline that must not silently ship estimated geometry. Default false
28
+ * (warn) — behavior-neutral.
29
+ */
30
+ requireMeasurer?: boolean;
23
31
  }
24
32
  /** One part of a split, in the source Text's draw space (group-local coords). */
25
33
  interface SplitPart {
@@ -89,6 +97,11 @@ interface FitTextOpts {
89
97
  /** measurer for exact fit — pass one (or call setTextMeasurer first), else the
90
98
  * estimating fallback is used with a one-time dev warning (the splitText footgun). */
91
99
  measurer?: TextMeasurer;
100
+ /**
101
+ * 0.59 measurer fail-loud OPT-IN (as splitText): `true` THROWs
102
+ * `MeasurerRequiredError` when no real measurer is available instead of
103
+ * warn-once + estimate. Default false. */
104
+ requireMeasurer?: boolean;
92
105
  }
93
106
  /**
94
107
  * The largest integer-px fontSize ≤ the text's current size at which it fits the
@@ -238,4 +251,4 @@ interface EmphasizeOpts {
238
251
  */
239
252
  declare function emphasizeWords(source: Text | TextProps, indices: readonly number[], opts?: EmphasizeOpts): RevealResult;
240
253
  //#endregion
241
- export { EmphasizeOpts, FitTextOpts, type GraphemeBox, KineticTypeError, type LineBox, RevealFrom, RevealOpts, RevealResult, SplitBy, SplitPart, SplitTextError, SplitTextOpts, SplitTextResult, TypeOnOpts, TypeOnResult, type WordBox, emphasizeWords, fitText, fitTextGroup, fitTextSize, revealLines, revealWords, splitText, typeOn };
254
+ export { EmphasizeOpts, FitTextOpts, type GraphemeBox, KineticTypeError, type LineBox, MeasurerRequiredError, RevealFrom, RevealOpts, RevealResult, SplitBy, SplitPart, SplitTextError, SplitTextOpts, SplitTextResult, TypeOnOpts, TypeOnResult, type WordBox, emphasizeWords, fitText, fitTextGroup, fitTextSize, revealLines, revealWords, splitText, typeOn };
package/dist/type.js CHANGED
@@ -1,4 +1,4 @@
1
- import { I as measureWrappedText, L as quantize, P as fallbackMeasurer, R as segmentGraphemes, V as warnIfEstimating, r as Group, s as Text } from "./nodes.js";
1
+ import { A as MeasurerRequiredError, F as fallbackMeasurer, H as warnIfEstimating, L as measureWrappedText, R as quantize, r as Group, s as Text, z as segmentGraphemes } from "./nodes.js";
2
2
  import { r as textCursor, t as typewriter } from "./typewriter.js";
3
3
  import { key, timeline, track } from "@glissade/core";
4
4
  //#region src/type.ts
@@ -54,7 +54,7 @@ function splitText(source, opts = {}) {
54
54
  "grapheme"
55
55
  ].includes(by)) throw new SplitTextError(`splitText() got an unknown { by: ${JSON.stringify(by)} } — valid values are 'word', 'line', 'grapheme'.`);
56
56
  const m = opts.measurer ?? text.measurerSource?.() ?? fallbackMeasurer();
57
- warnIfEstimating(m, "splitText");
57
+ warnIfEstimating(m, "splitText", opts.requireMeasurer === true);
58
58
  const font = {
59
59
  fontFamily: text.fontFamily,
60
60
  fontSize: text.fontSize(),
@@ -160,7 +160,7 @@ function fits(text, size, opts, m) {
160
160
  */
161
161
  function fitTextSize(text, opts) {
162
162
  const m = opts.measurer ?? text.measurerSource?.() ?? fallbackMeasurer();
163
- warnIfEstimating(m, "fitText");
163
+ warnIfEstimating(m, "fitText", opts.requireMeasurer === true);
164
164
  const minPx = opts.minPx ?? 6;
165
165
  const hi = Math.max(minPx, Math.floor(text.fontSize()));
166
166
  if (fits(text, hi, opts, m)) return hi;
@@ -357,4 +357,4 @@ function emphasizeWords(source, indices, opts = {}) {
357
357
  };
358
358
  }
359
359
  //#endregion
360
- export { KineticTypeError, SplitTextError, emphasizeWords, fitText, fitTextGroup, fitTextSize, revealLines, revealWords, splitText, typeOn };
360
+ export { KineticTypeError, MeasurerRequiredError, SplitTextError, emphasizeWords, fitText, fitTextGroup, fitTextSize, revealLines, revealWords, splitText, typeOn };
@@ -1,4 +1,4 @@
1
- import { C as Node, J as IDENTITY, Q as matEquals, R as segmentGraphemes, f as roundedRectSegs } from "./nodes.js";
1
+ import { $ as matEquals, C as Node, Y as IDENTITY, f as roundedRectSegs, z as segmentGraphemes } from "./nodes.js";
2
2
  import { key, signal, track } from "@glissade/core";
3
3
  //#region src/textCursor.ts
4
4
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.58.1",
3
+ "version": "0.59.0-pre.0",
4
4
  "description": "glissade scene graph: nodes, transforms, DisplayList emission. Renderer-agnostic; zero DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -81,7 +81,7 @@
81
81
  ],
82
82
  "dependencies": {
83
83
  "yoga-layout": "^3.2.1",
84
- "@glissade/core": "0.58.1"
84
+ "@glissade/core": "0.59.0-pre.0"
85
85
  },
86
86
  "repository": {
87
87
  "type": "git",