@glissade/scene 0.60.0-pre.0 → 0.60.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.
@@ -153,8 +153,16 @@ interface DescribedHelper {
153
153
  interface SurfaceEntry {
154
154
  /** The export name — also the `window.glissade.<name>` global on the IIFE. */
155
155
  name: string;
156
- /** `'value'` = a runtime binding on the bundle (a class / function / object); `'type'` = a TS type-only name that erases at runtime (opaque, referenced by signatures). */
157
- kind: 'value' | 'type';
156
+ /**
157
+ * `'value'` = a runtime binding on the bundle (a class / function / object);
158
+ * `'type'` = a TS type-only name that erases at runtime (opaque, referenced by
159
+ * signatures); `'diagnostic'` = a runtime AUTHORING-DIAGNOSTIC function
160
+ * (`critique`/`validateScene`/`resolveAt`/`instanceProps`, 0.60) — a real
161
+ * `window.glissade.<name>` callable that is PERCEPTION/self-check tooling, not
162
+ * scene-building surface. An agent BUILDING a scene filters `kind !== 'diagnostic'`;
163
+ * an agent CRITIQUING a rendered scene filters `kind === 'diagnostic'`.
164
+ */
165
+ kind: 'value' | 'type' | 'diagnostic';
158
166
  /** `true` when it is reachable as `window.glissade.<name>` on the single-file IIFE bundle. */
159
167
  iife: boolean;
160
168
  /** How to consume it: `'constructor'` needs `new`, `'function'` is a plain call, `'object'` is a value namespace (e.g. `easings`), `'type'` is type-only. */
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.60.0-pre.0";
26
+ const RAW_VERSION = "0.60.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
@@ -182,6 +182,34 @@ const SURFACE_EXTRA = [
182
182
  /** Value exports that are runtime OBJECTS (not callable): the easing registry. */
183
183
  const SURFACE_VALUE_OBJECTS = ["easings"];
184
184
  /**
185
+ * The 0.60 machine-readable AUTHORING-DIAGNOSTIC functions on `window.glissade`
186
+ * (the `@glissade/scene/diagnostics` subpath, IIFE-re-exported off the base embed):
187
+ * `critique` (rendered geometry), `validateScene` (static structure), `resolveAt`
188
+ * (the truthful read primitive), `instanceProps` (instance-bound state). Marked
189
+ * `kind: 'diagnostic'` so a consumer can PARTITION the surface — build-a-scene
190
+ * tooling filters them OUT, render-critique/perception tooling filters them IN —
191
+ * instead of them being invisible (previously exempt-internal, discoverable only by
192
+ * reading the bundle). `arity` = the documented required positional-arg count.
193
+ */
194
+ const SURFACE_DIAGNOSTICS = [
195
+ {
196
+ name: "critique",
197
+ arity: 2
198
+ },
199
+ {
200
+ name: "validateScene",
201
+ arity: 2
202
+ },
203
+ {
204
+ name: "resolveAt",
205
+ arity: 3
206
+ },
207
+ {
208
+ name: "instanceProps",
209
+ arity: 1
210
+ }
211
+ ];
212
+ /**
185
213
  * The opaque, type-ONLY names the API surface references (they erase at runtime —
186
214
  * `window.glissade.Paint` is `undefined`). `gs types --global` emits a best-effort
187
215
  * alias per name; `gs describe --lint` guards they stay type-only (a type surfaced
@@ -235,6 +263,13 @@ function buildSurface() {
235
263
  iife: true,
236
264
  form: "object"
237
265
  });
266
+ for (const d of SURFACE_DIAGNOSTICS) out.push({
267
+ name: d.name,
268
+ kind: "diagnostic",
269
+ iife: true,
270
+ form: "function",
271
+ arity: d.arity
272
+ });
238
273
  for (const name of SURFACE_TYPE_ONLY) out.push({
239
274
  name,
240
275
  kind: "type",
@@ -282,6 +282,20 @@ interface CritiqueOptions {
282
282
  * override for tooling; a fixed INTEGER-frame grid is the determinism contract.
283
283
  */
284
284
  fps?: number;
285
+ /**
286
+ * Author-declared INTENTIONALLY off-stage node ids — the OFF_CANVAS opt-out.
287
+ * A node is exempt from OFF_CANVAS iff its id is in this list OR ANY of its
288
+ * ancestors' ids is (SUBTREE match): list the parked GROUP id
289
+ * (`'sd1-drawer'`) and its whole subtree — current children AND any it later
290
+ * gains — is suppressed, while sibling groups stay fully checked. This lets an
291
+ * author silence the true-positive-but-intentional off-stage art (wing-parked
292
+ * drawers, hidden placeholder cards) without muting OFF_CANVAS wholesale. A
293
+ * PURE emission filter — determinism-neutral (it never changes the sampled
294
+ * geometry, only which off-frame nodes are reported). Same param-seam shape as
295
+ * a future `safeAreas`; a per-node `offstage:true` marker is a planned
296
+ * fast-follow, not this mechanism.
297
+ */
298
+ offstage?: readonly string[];
285
299
  }
286
300
  interface CritiqueResult {
287
301
  schemaVersion: typeof DIAGNOSTIC_SCHEMA_VERSION;
@@ -1,4 +1,4 @@
1
- import { I as isEstimatingMeasurer, J as collapseReplacer, P as estimatingMeasurer, W as createDisplayListBuilder, Y as IDENTITY, et as multiply, r as Group, s as Text } from "./nodes.js";
1
+ import { I as isEstimatingMeasurer, J as collapseReplacer, P as estimatingMeasurer, R as quantize, W as createDisplayListBuilder, Y as IDENTITY, et as multiply, r as Group, s as Text } from "./nodes.js";
2
2
  import { a as evaluate, r as bindScene } from "./scene.js";
3
3
  import { emitWithIds } from "./identity.js";
4
4
  import { buildFontRegistry, compileTimeline, evaluateAt, parseCmap, parseColor, untracked, validateFonts } from "@glissade/core";
@@ -737,8 +737,10 @@ function critique(scene, timeline, opts = {}) {
737
737
  }
738
738
  }
739
739
  const rendered = [];
740
+ const offstageSet = new Set(opts.offstage ?? []);
741
+ const isOffstage = (id) => offstageSet.size > 0 && (offstageSet.has(id) || hasFlaggedAncestor(scene, id, offstageSet));
740
742
  const offCanvasIds = /* @__PURE__ */ new Set();
741
- for (const [id, a] of agg) if (a.onStage > 0 && a.offCanvas === a.onStage) offCanvasIds.add(id);
743
+ for (const [id, a] of agg) if (a.onStage > 0 && a.offCanvas === a.onStage && !isOffstage(id)) offCanvasIds.add(id);
742
744
  const reportedOffCanvas = /* @__PURE__ */ new Set();
743
745
  for (const id of offCanvasIds) {
744
746
  if (hasFlaggedAncestor(scene, id, offCanvasIds)) continue;
@@ -749,22 +751,29 @@ function critique(scene, timeline, opts = {}) {
749
751
  for (const [id, a] of agg) {
750
752
  if (a.lastTexts.length === 0) continue;
751
753
  const node = scene.nodes.get(id);
752
- if (!node || node.describeType !== "Text") continue;
754
+ if (!(node instanceof Text)) continue;
753
755
  const width = numberAt(scene, `${id}/width`, a.lastTextFrameT);
754
- if (width === void 0 || width <= 0) continue;
755
- let widest = 0;
756
- for (const run of a.lastTexts) {
757
- let mw = 0;
758
- try {
759
- mw = measurer.measureText(run.text, run.font).width;
760
- } catch {
761
- mw = 0;
756
+ if (width !== void 0 && width > 0) {
757
+ let widest = 0;
758
+ for (const run of a.lastTexts) {
759
+ let mw = 0;
760
+ try {
761
+ mw = measurer.measureText(run.text, run.font).width;
762
+ } catch {
763
+ mw = 0;
764
+ }
765
+ if (mw > widest) widest = mw;
762
766
  }
763
- if (mw > widest) widest = mw;
767
+ const over = widest - width;
768
+ if (over > .5) rendered.push(textOverflowDiagnostic(id, "width", widest, width, over, estimating));
769
+ }
770
+ const boxH = node.box?.h;
771
+ if (boxH !== void 0 && boxH > 0) {
772
+ const fontSize = a.lastTexts[0].font.size;
773
+ const blockH = quantize(fontSize * node.lineHeight) * a.lastTexts.length;
774
+ const overH = blockH - boxH;
775
+ if (overH > .5) rendered.push(textOverflowDiagnostic(id, "height", blockH, boxH, overH, estimating));
764
776
  }
765
- const over = widest - width;
766
- if (over <= .5) continue;
767
- rendered.push(textOverflowDiagnostic(id, widest, width, over, estimating));
768
777
  }
769
778
  for (const [id, a] of agg) {
770
779
  if (a.onStage === 0 || a.occluded !== a.onStage) continue;
@@ -824,8 +833,8 @@ function offCanvasDiagnostic(scene, id, a, w, h) {
824
833
  }
825
834
  };
826
835
  }
827
- function textOverflowDiagnostic(id, measured, threshold, over, estimating) {
828
- const base = `text of node '${id}' overflows its box by ${round(over)}px (needs ${round(measured)}px, box width ${round(threshold)}px). Reduce fontSize, widen width, or wrap it with fitText({ maxW: ${round(threshold)} }).`;
836
+ function textOverflowDiagnostic(id, dimension, measured, threshold, over, estimating) {
837
+ const base = dimension === "width" ? `text of node '${id}' overflows its box WIDTH by ${round(over)}px (needs ${round(measured)}px, box width ${round(threshold)}px). Reduce fontSize, widen width, or wrap it with fitText({ maxW: ${round(threshold)} }).` : `text of node '${id}' overflows its box HEIGHT by ${round(over)}px (wrapped block ${round(measured)}px tall, box height ${round(threshold)}px). Reduce fontSize, increase the box height, or shorten the text.`;
829
838
  return {
830
839
  schemaVersion: 1,
831
840
  code: "TEXT_OVERFLOW",
@@ -834,6 +843,7 @@ function textOverflowDiagnostic(id, measured, threshold, over, estimating) {
834
843
  node: id,
835
844
  message: estimating ? `${base} (metrics ESTIMATED — no real text measurer injected; verify with the real backend measurer.)` : base,
836
845
  detail: {
846
+ dimension,
837
847
  measured: round(measured),
838
848
  threshold: round(threshold),
839
849
  overflowPx: round(over),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.60.0-pre.0",
3
+ "version": "0.60.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.60.0-pre.0"
84
+ "@glissade/core": "0.60.0"
85
85
  },
86
86
  "repository": {
87
87
  "type": "git",