@glissade/scene 0.9.1-pre.0 → 0.10.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.
- package/dist/index.d.ts +32 -3
- package/dist/index.js +233 -151
- package/dist/layout.d.ts +1 -1
- package/dist/layout.js +28 -19
- package/dist/layoutEngine.d.ts +39 -2
- package/dist/layoutEngine.js +96 -12
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as estimatingMeasurer, A as ResolvedSketch, B as validateSketch, C as VideoProps, Ct as Mat2x3, D as roundedRectSegs, Dt as matEquals, E as revealSchedule, Et as invert, F as hachureLines, G as Node, H as BindablePropTarget, I as resolveSketch, J as resolveAnchor, K as NodeProps, L as roughen, M as SketchValidationError, N as arcLength, O as HachureSpec, Ot as multiply, P as flatten, Q as breakLines, R as sketchStrokes, S as Video, St as IDENTITY, T as pathFromSegs, Tt as fromTRS, U as EvalContext, V as AnchorSpec, W as HitArea, X as TextMeasurer, Y as MEASURE_QUANTUM_PX, Z as TextMetricsLite, _ as Rect, _t as StrokeStyle, a as LayoutEngineMissingError, at as DisplayList, b as Text, bt as glow, c as requireLayoutEngine, ct as FilterSpec, d as Group, dt as Paint, et as quantize, f as ImageNode, ft as PathSeg, g as PathProps, gt as ShaderRef, h as Path, ht as ResourceId, i as LayoutEngine, it as BlendMode, j as SketchStyle, k as Polyline, l as setLayoutEngine, lt as FilterValidationError, m as LineBox, mt as Resource, n as LayoutChildSpec, nt as segmentWords, ot as DisplayListBuilder, p as ImageProps, pt as Rect$1, q as PropInit, r as LayoutContainerSpec, rt as setDefaultMeasurer, s as getLayoutEngine, st as DrawCommand, t as LayoutBox, tt as segmentGraphemes, u as Circle, ut as FontSpec, v as RevealMark, vt as createDisplayListBuilder, w as WordBox, wt as applyToPoint, x as TextProps, xt as validateFilters, y as ShapeProps, yt as filtersToCanvasFilter, z as validateHachure } from "./layoutEngine.js";
|
|
2
2
|
import { BindableSignal, BoundTimeline, CompiledTimeline, CoverageReport, EaseSpec, FontMode, FontUsage, PathValue, Playhead, Timeline, Track, Vec2 } from "@glissade/core";
|
|
3
3
|
|
|
4
4
|
//#region src/highlight.d.ts
|
|
@@ -512,8 +512,22 @@ declare class Raster2D<TCanvas extends CanvasLike, TPath extends PathLike, TDraw
|
|
|
512
512
|
private readonly images;
|
|
513
513
|
private readonly videos;
|
|
514
514
|
private warnedShaders;
|
|
515
|
+
/**
|
|
516
|
+
* §3.5 bitmap LRU: device-transform-qualified cacheKey → rasterized layer.
|
|
517
|
+
* A Map preserves insertion order, so the oldest key is `keys().next()` —
|
|
518
|
+
* touch-on-hit by delete+set keeps it a true LRU. Disabled (stays empty) when
|
|
519
|
+
* `cacheEnabled` is false, so cache-cold === cache-warm is testable directly.
|
|
520
|
+
*/
|
|
521
|
+
private readonly rasterCache;
|
|
522
|
+
private readonly cacheEnabled;
|
|
515
523
|
constructor(host: Raster2DHost<TCanvas, TPath, TDrawable>, /** caps.shaders (§3.7): what happens when a shader can't run here. */
|
|
516
|
-
shaderCaps?: ShaderCaps
|
|
524
|
+
shaderCaps?: ShaderCaps,
|
|
525
|
+
/**
|
|
526
|
+
* §3.5: opt-OUT switch for the bitmap LRU. Defaults on, but the env var
|
|
527
|
+
* RASTER_CACHE=0 force-disables it (the equality test renders both ways).
|
|
528
|
+
* A disabled cache is byte-identical — it just always takes the miss path.
|
|
529
|
+
*/
|
|
530
|
+
cacheEnabled?: boolean);
|
|
517
531
|
/** Register a decoded still (kind 'image' assets). */
|
|
518
532
|
setImageAsset(assetId: string, image: TDrawable): void;
|
|
519
533
|
/** Register a warmed-on-demand video source (kind 'video' assets, §3.8). */
|
|
@@ -525,8 +539,23 @@ declare class Raster2D<TCanvas extends CanvasLike, TPath extends PathLike, TDraw
|
|
|
525
539
|
private buildPath;
|
|
526
540
|
private acquire;
|
|
527
541
|
private release;
|
|
542
|
+
/**
|
|
543
|
+
* §3.5 LRU insert with touch-on-hit + eviction-to-pool. Storing under a key
|
|
544
|
+
* that already holds a (different) canvas releases the old one first.
|
|
545
|
+
*/
|
|
546
|
+
private cacheStore;
|
|
547
|
+
private cacheTouch;
|
|
548
|
+
/**
|
|
549
|
+
* §3.4/§3.5 composite of a finished group layer onto its parent — the EXACT
|
|
550
|
+
* same save/resetTransform/clip/globalAlpha/filter/blend/drawImage sequence
|
|
551
|
+
* for both the freshly-rasterized miss path and a cache-blit hit, so a HIT is
|
|
552
|
+
* byte-identical to a MISS. `bounds`/`unbounded` come from the layer (miss) or
|
|
553
|
+
* the cache entry (hit); the composite params (opacity/blend/filters) always
|
|
554
|
+
* come from the LIVE pushGroup command, never the cache.
|
|
555
|
+
*/
|
|
556
|
+
private composite;
|
|
528
557
|
/** The command walk — order and operations identical to the pre-extraction twins. */
|
|
529
558
|
render(target: TCanvas, list: DisplayList): void;
|
|
530
559
|
}
|
|
531
560
|
//#endregion
|
|
532
|
-
export { ALL_FILTER_KINDS, type AnchorSpec, type BackendCaps, type BindablePropTarget, type BlendMode, type CacheColdResult, type CanvasLike, Circle, ColdAssetError, type Ctx2DLike, DeterminismViolationError, type DisplayList, type DisplayListBuilder, type DrawCommand, type DrawOnEachOptions, type DrawOnOptions, DuplicateNodeIdError, type EditMark, type EvalContext, type FilterKind, type FilterSpec, FilterValidationError, FollowPath, type FollowPathProps, type FontByteLoader, type FontSpec, Group, type GuardMode, type HachureSpec, Highlight, type HighlightProps, type HitArea, IDENTITY, type ImageHandle, ImageNode, type ImageProps, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LineBox, type Mat2x3, Node, type NodeProps, type Paint, Path, type PathLike, type PathProps, type PathSampler, type PathSeg, 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, TokenHighlight, type TokenHighlightProps, TokenMatchError, type TokenRange, type TypeEdit, type TypewriterResult, type ValidateSceneFontsOptions, Video, type VideoFrameSource, type VideoProps, type WordBox, applyToPoint, arcLength, auditCacheCold, bindScene, breakLines, collectTextUsages, createDisplayListBuilder, createScene, drawOn, drawOnEach, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, followPath, fontString, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, matEquals, matchTokenRun, motionPath, multiply, pathFromSegs, pathLength, pointAtLength, quantize, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, tokenHighlight, typewriter, validateFilters, validateHachure, validateSceneFonts, validateSketch, withDeterminismGuards };
|
|
561
|
+
export { ALL_FILTER_KINDS, type AnchorSpec, type BackendCaps, type BindablePropTarget, type BlendMode, type CacheColdResult, type CanvasLike, Circle, ColdAssetError, type Ctx2DLike, DeterminismViolationError, type DisplayList, type DisplayListBuilder, type DrawCommand, type DrawOnEachOptions, type DrawOnOptions, DuplicateNodeIdError, type EditMark, type EvalContext, type FilterKind, type FilterSpec, FilterValidationError, FollowPath, type FollowPathProps, type FontByteLoader, type FontSpec, Group, type GuardMode, type HachureSpec, Highlight, type HighlightProps, type HitArea, IDENTITY, type ImageHandle, ImageNode, type ImageProps, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LineBox, MEASURE_QUANTUM_PX, type Mat2x3, Node, type NodeProps, type Paint, Path, type PathLike, type PathProps, type PathSampler, type PathSeg, 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, TokenHighlight, type TokenHighlightProps, TokenMatchError, type TokenRange, type TypeEdit, type TypewriterResult, type ValidateSceneFontsOptions, Video, type VideoFrameSource, type VideoProps, type WordBox, applyToPoint, arcLength, auditCacheCold, bindScene, breakLines, collectTextUsages, createDisplayListBuilder, createScene, drawOn, drawOnEach, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, followPath, fontString, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, matEquals, matchTokenRun, motionPath, multiply, pathFromSegs, pathLength, pointAtLength, quantize, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, tokenHighlight, typewriter, validateFilters, validateHachure, validateSceneFonts, validateSketch, withDeterminismGuards };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as quantize, B as applyToPoint, C as validateSketch, D as breakLines, E as MEASURE_QUANTUM_PX, F as createDisplayListBuilder, H as invert, I as filtersToCanvasFilter, L as glow, M as segmentWords, N as setDefaultMeasurer, O as estimatingMeasurer, P as FilterValidationError, R as validateFilters, S as validateHachure, T as resolveAnchor, U as matEquals, V as fromTRS, W as multiply, _ as flatten, a as Circle, b as roughen, c as Path, d as Video, f as pathFromSegs, g as arcLength, h as SketchValidationError, i as setLayoutEngine, j as segmentGraphemes, k as fallbackMeasurer, l as Rect, m as roundedRectSegs, n as getLayoutEngine, o as Group, p as revealSchedule, r as requireLayoutEngine, s as ImageNode, t as LayoutEngineMissingError, u as Text, v as hachureLines, w as Node, x as sketchStrokes, y as resolveSketch, z as IDENTITY } from "./layoutEngine.js";
|
|
2
2
|
import { bindTimeline, buildFontRegistry, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, key, parseCmap, signal, stagger, track, validateFonts, vec2Signal } from "@glissade/core";
|
|
3
3
|
//#region src/highlight.ts
|
|
4
4
|
/**
|
|
@@ -970,6 +970,21 @@ var ShaderEffect = class extends Group {
|
|
|
970
970
|
function fontString(font) {
|
|
971
971
|
return `${font.style === "italic" ? "italic " : ""}${font.weight !== void 0 && font.weight !== 400 ? `${font.weight} ` : ""}${font.size}px ${font.family}`;
|
|
972
972
|
}
|
|
973
|
+
/** Hardcoded LRU cap (§3.5). Evicted canvases return to the raster pool. */
|
|
974
|
+
const RASTER_CACHE_CAP = 16;
|
|
975
|
+
/**
|
|
976
|
+
* Round a device-transform component into the cache key. The layer bakes the
|
|
977
|
+
* parent CTM into its pixels, so two frames that share a cacheKey but differ in
|
|
978
|
+
* device transform are NOT interchangeable — they must key separately. Rounding
|
|
979
|
+
* to 1e-4 collapses float jitter from matrix re-multiplication of an unchanged
|
|
980
|
+
* transform (so a genuinely static parent HITs) while keeping any visible move
|
|
981
|
+
* to a distinct key (so a stale-CTM bitmap can never blit). The float is taken
|
|
982
|
+
* verbatim into the string — no lossy truncation that could alias two CTMs.
|
|
983
|
+
*/
|
|
984
|
+
function transformKey(m) {
|
|
985
|
+
const q = (v) => Object.is(v, -0) ? "0" : String(Math.round(v * 1e4) / 1e4);
|
|
986
|
+
return `${q(m[0])},${q(m[1])},${q(m[2])},${q(m[3])},${q(m[4])},${q(m[5])}`;
|
|
987
|
+
}
|
|
973
988
|
/**
|
|
974
989
|
* How far a filter chain can paint beyond its input (device px). Each stage
|
|
975
990
|
* feeds the next, so outsets ADD. Gaussian reach: Skia truncates at 3σ and
|
|
@@ -1042,9 +1057,18 @@ var Raster2D = class {
|
|
|
1042
1057
|
images = /* @__PURE__ */ new Map();
|
|
1043
1058
|
videos = /* @__PURE__ */ new Map();
|
|
1044
1059
|
warnedShaders = false;
|
|
1045
|
-
|
|
1060
|
+
/**
|
|
1061
|
+
* §3.5 bitmap LRU: device-transform-qualified cacheKey → rasterized layer.
|
|
1062
|
+
* A Map preserves insertion order, so the oldest key is `keys().next()` —
|
|
1063
|
+
* touch-on-hit by delete+set keeps it a true LRU. Disabled (stays empty) when
|
|
1064
|
+
* `cacheEnabled` is false, so cache-cold === cache-warm is testable directly.
|
|
1065
|
+
*/
|
|
1066
|
+
rasterCache = /* @__PURE__ */ new Map();
|
|
1067
|
+
cacheEnabled;
|
|
1068
|
+
constructor(host, shaderCaps = "warn", cacheEnabled = (globalThis.process?.env?.["RASTER_CACHE"] ?? "1") !== "0") {
|
|
1046
1069
|
this.host = host;
|
|
1047
1070
|
this.shaderCaps = shaderCaps;
|
|
1071
|
+
this.cacheEnabled = cacheEnabled;
|
|
1048
1072
|
}
|
|
1049
1073
|
/** Register a decoded still (kind 'image' assets). */
|
|
1050
1074
|
setImageAsset(assetId, image) {
|
|
@@ -1056,6 +1080,7 @@ var Raster2D = class {
|
|
|
1056
1080
|
}
|
|
1057
1081
|
dispose() {
|
|
1058
1082
|
this.pool.length = 0;
|
|
1083
|
+
this.rasterCache.clear();
|
|
1059
1084
|
}
|
|
1060
1085
|
resolveDrawable(res, id) {
|
|
1061
1086
|
if (res.kind === "image") {
|
|
@@ -1127,6 +1152,73 @@ var Raster2D = class {
|
|
|
1127
1152
|
release(canvas) {
|
|
1128
1153
|
if (this.pool.length < 8) this.pool.push(canvas);
|
|
1129
1154
|
}
|
|
1155
|
+
/**
|
|
1156
|
+
* §3.5 LRU insert with touch-on-hit + eviction-to-pool. Storing under a key
|
|
1157
|
+
* that already holds a (different) canvas releases the old one first.
|
|
1158
|
+
*/
|
|
1159
|
+
cacheStore(key, entry) {
|
|
1160
|
+
const prior = this.rasterCache.get(key);
|
|
1161
|
+
if (prior) {
|
|
1162
|
+
this.rasterCache.delete(key);
|
|
1163
|
+
if (prior.canvas !== entry.canvas) this.release(prior.canvas);
|
|
1164
|
+
}
|
|
1165
|
+
this.rasterCache.set(key, entry);
|
|
1166
|
+
while (this.rasterCache.size > RASTER_CACHE_CAP) {
|
|
1167
|
+
const oldest = this.rasterCache.keys().next().value;
|
|
1168
|
+
if (oldest === void 0) break;
|
|
1169
|
+
const evicted = this.rasterCache.get(oldest);
|
|
1170
|
+
this.rasterCache.delete(oldest);
|
|
1171
|
+
this.release(evicted.canvas);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
cacheTouch(key) {
|
|
1175
|
+
const hit = this.rasterCache.get(key);
|
|
1176
|
+
if (hit) {
|
|
1177
|
+
this.rasterCache.delete(key);
|
|
1178
|
+
this.rasterCache.set(key, hit);
|
|
1179
|
+
}
|
|
1180
|
+
return hit;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* §3.4/§3.5 composite of a finished group layer onto its parent — the EXACT
|
|
1184
|
+
* same save/resetTransform/clip/globalAlpha/filter/blend/drawImage sequence
|
|
1185
|
+
* for both the freshly-rasterized miss path and a cache-blit hit, so a HIT is
|
|
1186
|
+
* byte-identical to a MISS. `bounds`/`unbounded` come from the layer (miss) or
|
|
1187
|
+
* the cache entry (hit); the composite params (opacity/blend/filters) always
|
|
1188
|
+
* come from the LIVE pushGroup command, never the cache.
|
|
1189
|
+
*/
|
|
1190
|
+
composite(parent, parentLayer, drawable, bounds, unbounded, shaderReplaced, opacity, blend, filter, filters, w, h) {
|
|
1191
|
+
const hasFilter = filter !== void 0 && filter !== "none";
|
|
1192
|
+
const outset = hasFilter ? filterOutset(filters) : 0;
|
|
1193
|
+
if (unbounded || blend !== "source-over") parentLayer.unbounded = true;
|
|
1194
|
+
else if (bounds) accumulateRect(parentLayer, IDENTITY, bounds.minX - outset, bounds.minY - outset, bounds.maxX + outset, bounds.maxY + outset);
|
|
1195
|
+
const clippable = hasFilter && !shaderReplaced && !unbounded && blend === "source-over";
|
|
1196
|
+
if (clippable && bounds === null) return;
|
|
1197
|
+
parent.save();
|
|
1198
|
+
parent.resetTransform();
|
|
1199
|
+
if (clippable && bounds) {
|
|
1200
|
+
const x0 = Math.max(0, Math.floor(bounds.minX - outset));
|
|
1201
|
+
const y0 = Math.max(0, Math.floor(bounds.minY - outset));
|
|
1202
|
+
const x1 = Math.min(w, Math.ceil(bounds.maxX + outset));
|
|
1203
|
+
const y1 = Math.min(h, Math.ceil(bounds.maxY + outset));
|
|
1204
|
+
if (x0 >= x1 || y0 >= y1) {
|
|
1205
|
+
parent.restore();
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
const clip = this.host.newPath();
|
|
1209
|
+
clip.moveTo(x0, y0);
|
|
1210
|
+
clip.lineTo(x1, y0);
|
|
1211
|
+
clip.lineTo(x1, y1);
|
|
1212
|
+
clip.lineTo(x0, y1);
|
|
1213
|
+
clip.closePath();
|
|
1214
|
+
parent.clip(clip, "nonzero");
|
|
1215
|
+
}
|
|
1216
|
+
parent.globalAlpha = opacity;
|
|
1217
|
+
if (hasFilter) parent.filter = filter;
|
|
1218
|
+
parent.globalCompositeOperation = blend;
|
|
1219
|
+
parent.drawImage(drawable, 0, 0);
|
|
1220
|
+
parent.restore();
|
|
1221
|
+
}
|
|
1130
1222
|
/** The command walk — order and operations identical to the pre-extraction twins. */
|
|
1131
1223
|
render(target, list) {
|
|
1132
1224
|
const { w, h } = list.size;
|
|
@@ -1147,167 +1239,157 @@ var Raster2D = class {
|
|
|
1147
1239
|
const top = () => layers[layers.length - 1];
|
|
1148
1240
|
let mat = IDENTITY;
|
|
1149
1241
|
const matStack = [];
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
ctx.strokeStyle = cmd.paint.color;
|
|
1177
|
-
ctx.lineWidth = cmd.stroke.width;
|
|
1178
|
-
ctx.lineCap = cmd.stroke.cap ?? "butt";
|
|
1179
|
-
ctx.lineJoin = cmd.stroke.join ?? "miter";
|
|
1180
|
-
if (cmd.stroke.dash) {
|
|
1181
|
-
ctx.setLineDash(cmd.stroke.dash);
|
|
1182
|
-
ctx.lineDashOffset = cmd.stroke.dashOffset ?? 0;
|
|
1242
|
+
const commands = list.commands;
|
|
1243
|
+
for (let ci = 0; ci < commands.length; ci++) {
|
|
1244
|
+
const cmd = commands[ci];
|
|
1245
|
+
switch (cmd.op) {
|
|
1246
|
+
case "save":
|
|
1247
|
+
matStack.push(mat);
|
|
1248
|
+
ctxOf().save();
|
|
1249
|
+
break;
|
|
1250
|
+
case "restore":
|
|
1251
|
+
mat = matStack.pop() ?? mat;
|
|
1252
|
+
ctxOf().restore();
|
|
1253
|
+
break;
|
|
1254
|
+
case "transform":
|
|
1255
|
+
mat = multiply(mat, cmd.m);
|
|
1256
|
+
ctxOf().transform(cmd.m[0], cmd.m[1], cmd.m[2], cmd.m[3], cmd.m[4], cmd.m[5]);
|
|
1257
|
+
break;
|
|
1258
|
+
case "clip":
|
|
1259
|
+
ctxOf().clip(this.path(list.resources, cmd.path), cmd.rule ?? "nonzero");
|
|
1260
|
+
break;
|
|
1261
|
+
case "fillPath": {
|
|
1262
|
+
const ctx = ctxOf();
|
|
1263
|
+
ctx.fillStyle = cmd.paint.color;
|
|
1264
|
+
ctx.fill(this.path(list.resources, cmd.path));
|
|
1265
|
+
const b = this.pathBounds(list.resources, cmd.path);
|
|
1266
|
+
if (b) accumulateRect(top(), mat, b.minX, b.minY, b.maxX, b.maxY);
|
|
1267
|
+
break;
|
|
1183
1268
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
ctx.
|
|
1187
|
-
ctx.
|
|
1269
|
+
case "strokePath": {
|
|
1270
|
+
const ctx = ctxOf();
|
|
1271
|
+
ctx.strokeStyle = cmd.paint.color;
|
|
1272
|
+
ctx.lineWidth = cmd.stroke.width;
|
|
1273
|
+
ctx.lineCap = cmd.stroke.cap ?? "butt";
|
|
1274
|
+
ctx.lineJoin = cmd.stroke.join ?? "miter";
|
|
1275
|
+
if (cmd.stroke.dash) {
|
|
1276
|
+
ctx.setLineDash(cmd.stroke.dash);
|
|
1277
|
+
ctx.lineDashOffset = cmd.stroke.dashOffset ?? 0;
|
|
1278
|
+
}
|
|
1279
|
+
ctx.stroke(this.path(list.resources, cmd.path));
|
|
1280
|
+
if (cmd.stroke.dash) {
|
|
1281
|
+
ctx.setLineDash([]);
|
|
1282
|
+
ctx.lineDashOffset = 0;
|
|
1283
|
+
}
|
|
1284
|
+
const b = this.pathBounds(list.resources, cmd.path);
|
|
1285
|
+
if (b) {
|
|
1286
|
+
const o = cmd.stroke.width * ((cmd.stroke.join ?? "miter") === "miter" ? 5 : 1);
|
|
1287
|
+
accumulateRect(top(), mat, b.minX - o, b.minY - o, b.maxX + o, b.maxY + o);
|
|
1288
|
+
}
|
|
1289
|
+
break;
|
|
1188
1290
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1291
|
+
case "fillText": {
|
|
1292
|
+
const ctx = ctxOf();
|
|
1293
|
+
ctx.font = fontString(cmd.font);
|
|
1294
|
+
ctx.fillStyle = cmd.paint.color;
|
|
1295
|
+
ctx.textBaseline = "alphabetic";
|
|
1296
|
+
ctx.textAlign = cmd.align ?? "left";
|
|
1297
|
+
ctx.fillText(cmd.text, cmd.x, cmd.y);
|
|
1298
|
+
try {
|
|
1299
|
+
const width = ctx.measureText(cmd.text).width;
|
|
1300
|
+
const align = cmd.align ?? "left";
|
|
1301
|
+
const x0 = align === "center" ? cmd.x - width / 2 : align === "right" ? cmd.x - width : cmd.x;
|
|
1302
|
+
const m = cmd.font.size;
|
|
1303
|
+
accumulateRect(top(), mat, x0 - m, cmd.y - 1.5 * m, x0 + width + m, cmd.y + .75 * m);
|
|
1304
|
+
} catch {
|
|
1305
|
+
top().unbounded = true;
|
|
1306
|
+
}
|
|
1307
|
+
break;
|
|
1193
1308
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
const align = cmd.align ?? "left";
|
|
1206
|
-
const x0 = align === "center" ? cmd.x - width / 2 : align === "right" ? cmd.x - width : cmd.x;
|
|
1207
|
-
const m = cmd.font.size;
|
|
1208
|
-
accumulateRect(top(), mat, x0 - m, cmd.y - 1.5 * m, x0 + width + m, cmd.y + .75 * m);
|
|
1209
|
-
} catch {
|
|
1210
|
-
top().unbounded = true;
|
|
1309
|
+
case "drawImage": {
|
|
1310
|
+
const res = list.resources[cmd.image];
|
|
1311
|
+
if (!res) throw new Error(`drawImage references missing resource ${cmd.image}`);
|
|
1312
|
+
const drawable = this.resolveDrawable(res, cmd.image);
|
|
1313
|
+
const ctx = ctxOf();
|
|
1314
|
+
if (cmd.smoothing !== void 0) ctx.imageSmoothingEnabled = cmd.smoothing;
|
|
1315
|
+
const { x, y, w: dw, h: dh } = cmd.dst;
|
|
1316
|
+
if (cmd.src) ctx.drawImage(drawable, cmd.src.x, cmd.src.y, cmd.src.w, cmd.src.h, x, y, dw, dh);
|
|
1317
|
+
else ctx.drawImage(drawable, x, y, dw, dh);
|
|
1318
|
+
accumulateRect(top(), mat, x, y, x + dw, y + dh);
|
|
1319
|
+
break;
|
|
1211
1320
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
const parent = ctxOf();
|
|
1228
|
-
const layerCanvas = this.acquire(w, h);
|
|
1229
|
-
const layerCtx = this.host.context(layerCanvas);
|
|
1230
|
-
layerCtx.resetTransform();
|
|
1231
|
-
layerCtx.clearRect(0, 0, w, h);
|
|
1232
|
-
layerCtx.setTransform(parent.getTransform());
|
|
1233
|
-
layers.push({
|
|
1234
|
-
ctx: layerCtx,
|
|
1235
|
-
canvas: layerCanvas,
|
|
1236
|
-
opacity: cmd.opacity,
|
|
1237
|
-
blend: cmd.blend,
|
|
1238
|
-
filter: filtersToCanvasFilter(cmd.filters),
|
|
1239
|
-
filters: cmd.filters,
|
|
1240
|
-
...cmd.shader !== void 0 ? { shader: cmd.shader } : {},
|
|
1241
|
-
bounds: null,
|
|
1242
|
-
unbounded: false
|
|
1243
|
-
});
|
|
1244
|
-
break;
|
|
1245
|
-
}
|
|
1246
|
-
case "popGroup": {
|
|
1247
|
-
const layer = layers.pop();
|
|
1248
|
-
if (!layer || layer.canvas === null) throw new Error("popGroup without matching pushGroup");
|
|
1249
|
-
const parent = ctxOf();
|
|
1250
|
-
let drawable = layer.canvas;
|
|
1251
|
-
let shaderReplaced = false;
|
|
1252
|
-
if (layer.shader !== void 0) {
|
|
1253
|
-
const replaced = this.host.applyShader?.(layer.canvas, layer.shader, w, h) ?? null;
|
|
1254
|
-
if (replaced !== null) {
|
|
1255
|
-
drawable = replaced;
|
|
1256
|
-
shaderReplaced = true;
|
|
1257
|
-
layer.bounds = {
|
|
1258
|
-
minX: 0,
|
|
1259
|
-
minY: 0,
|
|
1260
|
-
maxX: w,
|
|
1261
|
-
maxY: h
|
|
1262
|
-
};
|
|
1263
|
-
layer.unbounded = false;
|
|
1264
|
-
} else if (this.shaderCaps === "error") throw new Error("a ShaderEffect reached a backend without a shader runner (§3.7) — load @glissade/effects-webgpu in the browser, or accept passthrough with caps.shaders: warn");
|
|
1265
|
-
else if (!this.warnedShaders) {
|
|
1266
|
-
this.warnedShaders = true;
|
|
1267
|
-
emitDevWarning("ShaderEffect pass skipped: no shader runner here (headless or webgpu-less browser) — subtree composites unfiltered (§3.7 caps.shaders)");
|
|
1321
|
+
case "pushGroup": {
|
|
1322
|
+
const parent = ctxOf();
|
|
1323
|
+
const lruKey = this.cacheEnabled && cmd.cacheKey !== void 0 && cmd.shader === void 0 ? `${cmd.cacheKey}@${transformKey(mat)}` : void 0;
|
|
1324
|
+
if (lruKey !== void 0) {
|
|
1325
|
+
const hit = this.cacheTouch(lruKey);
|
|
1326
|
+
if (hit) {
|
|
1327
|
+
this.composite(parent, top(), hit.canvas, hit.bounds, hit.unbounded, false, cmd.opacity, cmd.blend, filtersToCanvasFilter(cmd.filters), cmd.filters, w, h);
|
|
1328
|
+
let depth = 1;
|
|
1329
|
+
while (depth > 0 && ++ci < commands.length) {
|
|
1330
|
+
const c = commands[ci];
|
|
1331
|
+
if (c.op === "pushGroup") depth++;
|
|
1332
|
+
else if (c.op === "popGroup") depth--;
|
|
1333
|
+
}
|
|
1334
|
+
break;
|
|
1335
|
+
}
|
|
1268
1336
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1337
|
+
const layerCanvas = this.acquire(w, h);
|
|
1338
|
+
const layerCtx = this.host.context(layerCanvas);
|
|
1339
|
+
layerCtx.resetTransform();
|
|
1340
|
+
layerCtx.clearRect(0, 0, w, h);
|
|
1341
|
+
layerCtx.setTransform(parent.getTransform());
|
|
1342
|
+
layers.push({
|
|
1343
|
+
ctx: layerCtx,
|
|
1344
|
+
canvas: layerCanvas,
|
|
1345
|
+
opacity: cmd.opacity,
|
|
1346
|
+
blend: cmd.blend,
|
|
1347
|
+
filter: filtersToCanvasFilter(cmd.filters),
|
|
1348
|
+
filters: cmd.filters,
|
|
1349
|
+
...cmd.shader !== void 0 ? { shader: cmd.shader } : {},
|
|
1350
|
+
bounds: null,
|
|
1351
|
+
unbounded: false,
|
|
1352
|
+
...lruKey !== void 0 ? { cacheStoreKey: lruKey } : {}
|
|
1353
|
+
});
|
|
1278
1354
|
break;
|
|
1279
1355
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1356
|
+
case "popGroup": {
|
|
1357
|
+
const layer = layers.pop();
|
|
1358
|
+
if (!layer || layer.canvas === null) throw new Error("popGroup without matching pushGroup");
|
|
1359
|
+
const parent = ctxOf();
|
|
1360
|
+
let drawable = layer.canvas;
|
|
1361
|
+
let shaderReplaced = false;
|
|
1362
|
+
if (layer.shader !== void 0) {
|
|
1363
|
+
const replaced = this.host.applyShader?.(layer.canvas, layer.shader, w, h) ?? null;
|
|
1364
|
+
if (replaced !== null) {
|
|
1365
|
+
drawable = replaced;
|
|
1366
|
+
shaderReplaced = true;
|
|
1367
|
+
layer.bounds = {
|
|
1368
|
+
minX: 0,
|
|
1369
|
+
minY: 0,
|
|
1370
|
+
maxX: w,
|
|
1371
|
+
maxY: h
|
|
1372
|
+
};
|
|
1373
|
+
layer.unbounded = false;
|
|
1374
|
+
} else if (this.shaderCaps === "error") throw new Error("a ShaderEffect reached a backend without a shader runner (§3.7) — load @glissade/effects-webgpu in the browser, or accept passthrough with caps.shaders: warn");
|
|
1375
|
+
else if (!this.warnedShaders) {
|
|
1376
|
+
this.warnedShaders = true;
|
|
1377
|
+
emitDevWarning("ShaderEffect pass skipped: no shader runner here (headless or webgpu-less browser) — subtree composites unfiltered (§3.7 caps.shaders)");
|
|
1378
|
+
}
|
|
1291
1379
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1380
|
+
this.composite(parent, top(), drawable, layer.bounds, layer.unbounded, shaderReplaced, layer.opacity, layer.blend, layer.filter, layer.filters, w, h);
|
|
1381
|
+
if (layer.cacheStoreKey !== void 0 && !shaderReplaced) this.cacheStore(layer.cacheStoreKey, {
|
|
1382
|
+
canvas: layer.canvas,
|
|
1383
|
+
bounds: layer.bounds,
|
|
1384
|
+
unbounded: layer.unbounded
|
|
1385
|
+
});
|
|
1386
|
+
else this.release(layer.canvas);
|
|
1387
|
+
break;
|
|
1299
1388
|
}
|
|
1300
|
-
parent.globalAlpha = layer.opacity;
|
|
1301
|
-
if (hasFilter) parent.filter = layer.filter;
|
|
1302
|
-
parent.globalCompositeOperation = layer.blend;
|
|
1303
|
-
parent.drawImage(drawable, 0, 0);
|
|
1304
|
-
parent.restore();
|
|
1305
|
-
this.release(layer.canvas);
|
|
1306
|
-
break;
|
|
1307
1389
|
}
|
|
1308
1390
|
}
|
|
1309
1391
|
if (layers.length !== 1) throw new Error("unbalanced pushGroup/popGroup in DisplayList");
|
|
1310
1392
|
}
|
|
1311
1393
|
};
|
|
1312
1394
|
//#endregion
|
|
1313
|
-
export { ALL_FILTER_KINDS, Circle, ColdAssetError, DeterminismViolationError, DuplicateNodeIdError, FilterValidationError, FollowPath, Group, Highlight, IDENTITY, ImageNode, LayoutEngineMissingError, Node, Path, Raster2D, Rect, ReservedNodeIdError, ShaderEffect, SketchValidationError, Text, TextCursor, TokenHighlight, TokenMatchError, Video, applyToPoint, arcLength, auditCacheCold, bindScene, breakLines, collectTextUsages, createDisplayListBuilder, createScene, drawOn, drawOnEach, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, followPath, fontString, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, matEquals, matchTokenRun, motionPath, multiply, pathFromSegs, pathLength, pointAtLength, quantize, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, tokenHighlight, typewriter, validateFilters, validateHachure, validateSceneFonts, validateSketch, withDeterminismGuards };
|
|
1395
|
+
export { ALL_FILTER_KINDS, Circle, ColdAssetError, DeterminismViolationError, DuplicateNodeIdError, FilterValidationError, FollowPath, Group, Highlight, IDENTITY, ImageNode, LayoutEngineMissingError, MEASURE_QUANTUM_PX, Node, Path, Raster2D, Rect, ReservedNodeIdError, ShaderEffect, SketchValidationError, Text, TextCursor, TokenHighlight, TokenMatchError, Video, applyToPoint, arcLength, auditCacheCold, bindScene, breakLines, collectTextUsages, createDisplayListBuilder, createScene, drawOn, drawOnEach, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, followPath, fontString, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, matEquals, matchTokenRun, motionPath, multiply, pathFromSegs, pathLength, pointAtLength, quantize, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, tokenHighlight, typewriter, validateFilters, validateHachure, validateSceneFonts, validateSketch, withDeterminismGuards };
|
package/dist/layout.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { G as Node, K as NodeProps, U as EvalContext,
|
|
1
|
+
import { G as Node, K as NodeProps, U as EvalContext, X as TextMeasurer, a as LayoutEngineMissingError, d as Group, i as LayoutEngine, l as setLayoutEngine, n as LayoutChildSpec, o as LayoutResult, ot as DisplayListBuilder, q as PropInit, r as LayoutContainerSpec, s as getLayoutEngine, t as LayoutBox } from "./layoutEngine.js";
|
|
2
2
|
import { BindableSignal } from "@glissade/core";
|
|
3
3
|
|
|
4
4
|
//#region src/layout.d.ts
|
package/dist/layout.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { signal } from "@glissade/core";
|
|
1
|
+
import { i as setLayoutEngine, k as fallbackMeasurer, n as getLayoutEngine, o as Group, r as requireLayoutEngine, t as LayoutEngineMissingError } from "./layoutEngine.js";
|
|
2
|
+
import { computed, signal } from "@glissade/core";
|
|
3
3
|
//#region src/layout.ts
|
|
4
4
|
/**
|
|
5
5
|
* '@glissade/scene/layout' (DESIGN.md §3.2): the flexbox Layout node and the
|
|
@@ -32,12 +32,16 @@ var Layout = class extends Group {
|
|
|
32
32
|
/** Content-sized axes ('auto'): the size signal is ignored, Yoga computes it. */
|
|
33
33
|
autoWidth;
|
|
34
34
|
autoHeight;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Sanctioned memoization (§2.1), core-`computed()`-backed: a pure function of
|
|
37
|
+
* the PARTICIPATING signals. The compute reads exactly the container props
|
|
38
|
+
* and child intrinsic-size signals it consumes, so the signal graph records
|
|
39
|
+
* those as deps and re-invokes Yoga only when one of THEM changes — a sibling
|
|
40
|
+
* mutating a non-participating signal does not invalidate the layout. Pulls
|
|
41
|
+
* the scene-injected measurer (the same one draw() uses via ctx.measurer);
|
|
42
|
+
* a caller-supplied non-default measurer bypasses this cache (see #compute).
|
|
43
|
+
*/
|
|
44
|
+
#memo = computed(() => this.#computeUncached(this.measurerSource?.() ?? fallbackMeasurer()));
|
|
41
45
|
constructor(props = {}) {
|
|
42
46
|
super(props);
|
|
43
47
|
this.autoWidth = props.width === "auto";
|
|
@@ -71,7 +75,17 @@ var Layout = class extends Group {
|
|
|
71
75
|
const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
|
|
72
76
|
return this.#compute(m).size;
|
|
73
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Route through the #memo (the dependency-tracked computed) when `measurer`
|
|
80
|
+
* is the default the memo itself pulls; otherwise compute fresh & UNCACHED —
|
|
81
|
+
* a caller-supplied non-default measurer must never read (or poison) a cache
|
|
82
|
+
* keyed on the scene-singleton measurer (the `computedSize(customMeasurer)`
|
|
83
|
+
* escape hatch).
|
|
84
|
+
*/
|
|
74
85
|
#compute(measurer) {
|
|
86
|
+
return measurer === (this.measurerSource?.() ?? fallbackMeasurer()) ? this.#memo() : this.#computeUncached(measurer);
|
|
87
|
+
}
|
|
88
|
+
#computeUncached(measurer) {
|
|
75
89
|
const container = {
|
|
76
90
|
width: this.autoWidth ? "auto" : this.width(),
|
|
77
91
|
height: this.autoHeight ? "auto" : this.height(),
|
|
@@ -95,18 +109,13 @@ var Layout = class extends Group {
|
|
|
95
109
|
});
|
|
96
110
|
else absolute.push(child);
|
|
97
111
|
});
|
|
98
|
-
const
|
|
99
|
-
if (key !== this.#memoKey) {
|
|
100
|
-
this.#memoResult = requireLayoutEngine().compute(container, flowable.map((f) => f.spec));
|
|
101
|
-
this.#memoKey = key;
|
|
102
|
-
}
|
|
103
|
-
const size = {
|
|
104
|
-
w: this.autoWidth ? this.#memoResult.width : container.width,
|
|
105
|
-
h: this.autoHeight ? this.#memoResult.height : container.height
|
|
106
|
-
};
|
|
112
|
+
const result = requireLayoutEngine().compute(container, flowable.map((f) => f.spec));
|
|
107
113
|
return {
|
|
108
|
-
result
|
|
109
|
-
size
|
|
114
|
+
result,
|
|
115
|
+
size: {
|
|
116
|
+
w: this.autoWidth ? result.width : container.width,
|
|
117
|
+
h: this.autoHeight ? result.height : container.height
|
|
118
|
+
},
|
|
110
119
|
flowable,
|
|
111
120
|
absolute
|
|
112
121
|
};
|
package/dist/layoutEngine.d.ts
CHANGED
|
@@ -171,6 +171,23 @@ interface DisplayList {
|
|
|
171
171
|
interface DisplayListBuilder {
|
|
172
172
|
push(cmd: DrawCommand): void;
|
|
173
173
|
resource(res: Resource): ResourceId;
|
|
174
|
+
/**
|
|
175
|
+
* §3.5 cacheKey seam — OPTIONAL so non-cache emits and lightweight mock
|
|
176
|
+
* builders never need them. `createDisplayListBuilder` supplies all three;
|
|
177
|
+
* `Node.emit` calls them only for `cache:true` nodes when present.
|
|
178
|
+
*/
|
|
179
|
+
/** Count of commands emitted so far — a markpoint for cacheKey ranges. */
|
|
180
|
+
mark?(): number;
|
|
181
|
+
/**
|
|
182
|
+
* A stable hash of the command slice [start, end) plus the FULL content of
|
|
183
|
+
* every resource those commands reference (not just ids — interned ids are a
|
|
184
|
+
* per-list detail). Pure function of the slice; identical slices at two times
|
|
185
|
+
* hash equal, so a static subtree caches. Opaque buffers collapse to a length
|
|
186
|
+
* marker (mirrors cacheColdAudit's serializer). Undefined for an empty slice.
|
|
187
|
+
*/
|
|
188
|
+
cacheKey?(start: number, end: number): string | undefined;
|
|
189
|
+
/** Stamp a cacheKey onto the pushGroup already emitted at index `i`. */
|
|
190
|
+
patchCacheKey?(i: number, key: string): void;
|
|
174
191
|
}
|
|
175
192
|
declare function createDisplayListBuilder(size: {
|
|
176
193
|
w: number;
|
|
@@ -188,7 +205,15 @@ interface TextMetricsLite {
|
|
|
188
205
|
interface TextMeasurer {
|
|
189
206
|
measureText(text: string, font: FontSpec): TextMetricsLite;
|
|
190
207
|
}
|
|
191
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* §3.6 measurement quantum (px). Scene-owned pre-measure quantizes every
|
|
210
|
+
* layout-feeding advance to this grid ONCE, then hands Yoga frozen integers —
|
|
211
|
+
* so sub-pixel measureText drift between Skia/HarfBuzz versions cannot move a
|
|
212
|
+
* whole layout. The single source of truth for the grid; `quantize` rounds to
|
|
213
|
+
* it. (Yoga's `setMeasureFunc` was considered and rejected — see DESIGN.md §3.6.)
|
|
214
|
+
*/
|
|
215
|
+
declare const MEASURE_QUANTUM_PX = 0.5;
|
|
216
|
+
/** §3.6 measurement quantum — round to the MEASURE_QUANTUM_PX grid. */
|
|
192
217
|
declare function quantize(v: number): number;
|
|
193
218
|
/**
|
|
194
219
|
* Process-wide fallback measurer for FACTORY-TIME measurement — component
|
|
@@ -254,6 +279,16 @@ interface NodeProps {
|
|
|
254
279
|
filters?: PropInit<FilterSpec[]>;
|
|
255
280
|
/** Placement point + transform pivot on the intrinsic box; default 'center'. */
|
|
256
281
|
anchor?: AnchorSpec;
|
|
282
|
+
/**
|
|
283
|
+
* §3.5 cross-frame raster cache: FORCE this subtree into a group and stamp a
|
|
284
|
+
* cacheKey on its pushGroup, so a backend with the bitmap LRU re-blits an
|
|
285
|
+
* unchanged subtree under a moving parent instead of re-rasterizing it. A
|
|
286
|
+
* pure performance hint — semantics are byte-identical with the cache off
|
|
287
|
+
* (the cache key folds in the inherited device transform, so a stale CTM can
|
|
288
|
+
* never blit). OFF by default: a scene that never sets it emits ZERO extra
|
|
289
|
+
* groups and is byte-identical to before. Best for expensive STATIC subtrees.
|
|
290
|
+
*/
|
|
291
|
+
cache?: boolean;
|
|
257
292
|
}
|
|
258
293
|
interface BindablePropTarget {
|
|
259
294
|
bindSource(fn: () => unknown): void;
|
|
@@ -286,6 +321,8 @@ declare abstract class Node {
|
|
|
286
321
|
readonly anchor: Vec2;
|
|
287
322
|
/** True only when the author SET an anchor — unset keeps the legacy origin. */
|
|
288
323
|
readonly hasAnchor: boolean;
|
|
324
|
+
/** §3.5: opt-in cross-frame raster cache. Forces a group + a stamped cacheKey. */
|
|
325
|
+
readonly cache: boolean;
|
|
289
326
|
parent: Node | null;
|
|
290
327
|
/** v2 §C.3: participates in hit testing; set implicitly by attaching a listener. */
|
|
291
328
|
interactive: boolean;
|
|
@@ -787,4 +824,4 @@ declare function setLayoutEngine(e: LayoutEngine): void;
|
|
|
787
824
|
declare function getLayoutEngine(): LayoutEngine | null;
|
|
788
825
|
declare function requireLayoutEngine(): LayoutEngine;
|
|
789
826
|
//#endregion
|
|
790
|
-
export {
|
|
827
|
+
export { estimatingMeasurer as $, ResolvedSketch as A, validateSketch as B, VideoProps as C, Mat2x3 as Ct, roundedRectSegs as D, matEquals as Dt, revealSchedule as E, invert as Et, hachureLines as F, Node as G, BindablePropTarget as H, resolveSketch as I, resolveAnchor as J, NodeProps as K, roughen as L, SketchValidationError as M, arcLength as N, HachureSpec as O, multiply as Ot, flatten as P, breakLines as Q, sketchStrokes as R, Video as S, IDENTITY as St, pathFromSegs as T, fromTRS as Tt, EvalContext as U, AnchorSpec as V, HitArea as W, TextMeasurer as X, MEASURE_QUANTUM_PX as Y, TextMetricsLite as Z, Rect as _, StrokeStyle as _t, LayoutEngineMissingError as a, DisplayList as at, Text as b, glow as bt, requireLayoutEngine as c, FilterSpec as ct, Group as d, Paint as dt, quantize as et, ImageNode as f, PathSeg as ft, PathProps as g, ShaderRef as gt, Path as h, ResourceId as ht, LayoutEngine as i, BlendMode as it, SketchStyle as j, Polyline as k, setLayoutEngine as l, FilterValidationError as lt, LineBox as m, Resource as mt, LayoutChildSpec as n, segmentWords as nt, LayoutResult as o, DisplayListBuilder as ot, ImageProps as p, Rect$1 as pt, PropInit as q, LayoutContainerSpec as r, setDefaultMeasurer as rt, getLayoutEngine as s, DrawCommand as st, LayoutBox as t, segmentGraphemes as tt, Circle as u, FontSpec as ut, RevealMark as v, createDisplayListBuilder as vt, WordBox as w, applyToPoint as wt, TextProps as x, validateFilters as xt, ShapeProps as y, filtersToCanvasFilter as yt, validateHachure as z };
|
package/dist/layoutEngine.js
CHANGED
|
@@ -122,6 +122,28 @@ function glow(color, radius = 16, intensity = 2) {
|
|
|
122
122
|
});
|
|
123
123
|
return layers;
|
|
124
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Resource ids that appear in a draw command — only path/image/video draws
|
|
127
|
+
* carry one. Keeps the cacheKey serializer in lockstep with DrawCommand.
|
|
128
|
+
*/
|
|
129
|
+
function commandResourceIds(cmd) {
|
|
130
|
+
switch (cmd.op) {
|
|
131
|
+
case "clip":
|
|
132
|
+
case "fillPath":
|
|
133
|
+
case "strokePath": return [cmd.path];
|
|
134
|
+
case "drawImage": return [cmd.image];
|
|
135
|
+
default: return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** FNV-1a over a string → an 8-hex-digit stable token (no crypto dep, ESM-safe). */
|
|
139
|
+
function fnv1a(s) {
|
|
140
|
+
let h = 2166136261;
|
|
141
|
+
for (let i = 0; i < s.length; i++) {
|
|
142
|
+
h ^= s.charCodeAt(i);
|
|
143
|
+
h = Math.imul(h, 16777619);
|
|
144
|
+
}
|
|
145
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
146
|
+
}
|
|
125
147
|
function createDisplayListBuilder(size) {
|
|
126
148
|
const commands = [];
|
|
127
149
|
const resources = [];
|
|
@@ -140,6 +162,45 @@ function createDisplayListBuilder(size) {
|
|
|
140
162
|
interned.set(k, id);
|
|
141
163
|
return id;
|
|
142
164
|
},
|
|
165
|
+
mark: () => commands.length,
|
|
166
|
+
cacheKey: (start, end) => {
|
|
167
|
+
if (end <= start) return void 0;
|
|
168
|
+
const localIds = /* @__PURE__ */ new Map();
|
|
169
|
+
const usedResources = [];
|
|
170
|
+
const remap = (id) => {
|
|
171
|
+
let local = localIds.get(id);
|
|
172
|
+
if (local === void 0) {
|
|
173
|
+
local = localIds.size;
|
|
174
|
+
localIds.set(id, local);
|
|
175
|
+
usedResources.push(resources[id]);
|
|
176
|
+
}
|
|
177
|
+
return local;
|
|
178
|
+
};
|
|
179
|
+
const sliceCmds = commands.slice(start, end).map((cmd) => {
|
|
180
|
+
if (commandResourceIds(cmd).length === 0) return cmd;
|
|
181
|
+
if (cmd.op === "drawImage") return {
|
|
182
|
+
...cmd,
|
|
183
|
+
image: remap(cmd.image)
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
...cmd,
|
|
187
|
+
path: remap(cmd.path)
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
return fnv1a(JSON.stringify({
|
|
191
|
+
c: sliceCmds,
|
|
192
|
+
r: usedResources
|
|
193
|
+
}, (_k, value) => {
|
|
194
|
+
if (value instanceof ArrayBuffer) return `ab:${value.byteLength}`;
|
|
195
|
+
if (ArrayBuffer.isView(value)) return `view:${value.byteLength}`;
|
|
196
|
+
if (typeof value === "function") return void 0;
|
|
197
|
+
return value;
|
|
198
|
+
}));
|
|
199
|
+
},
|
|
200
|
+
patchCacheKey: (i, key) => {
|
|
201
|
+
const cmd = commands[i];
|
|
202
|
+
if (cmd && cmd.op === "pushGroup") cmd.cacheKey = key;
|
|
203
|
+
},
|
|
143
204
|
finish: () => ({
|
|
144
205
|
commands,
|
|
145
206
|
resources,
|
|
@@ -149,9 +210,17 @@ function createDisplayListBuilder(size) {
|
|
|
149
210
|
}
|
|
150
211
|
//#endregion
|
|
151
212
|
//#region src/text.ts
|
|
152
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* §3.6 measurement quantum (px). Scene-owned pre-measure quantizes every
|
|
215
|
+
* layout-feeding advance to this grid ONCE, then hands Yoga frozen integers —
|
|
216
|
+
* so sub-pixel measureText drift between Skia/HarfBuzz versions cannot move a
|
|
217
|
+
* whole layout. The single source of truth for the grid; `quantize` rounds to
|
|
218
|
+
* it. (Yoga's `setMeasureFunc` was considered and rejected — see DESIGN.md §3.6.)
|
|
219
|
+
*/
|
|
220
|
+
const MEASURE_QUANTUM_PX = .5;
|
|
221
|
+
/** §3.6 measurement quantum — round to the MEASURE_QUANTUM_PX grid. */
|
|
153
222
|
function quantize(v) {
|
|
154
|
-
return Math.round(v
|
|
223
|
+
return Math.round(v / MEASURE_QUANTUM_PX) * MEASURE_QUANTUM_PX;
|
|
155
224
|
}
|
|
156
225
|
/**
|
|
157
226
|
* Estimating fallback measurer — used only when no backend has been injected
|
|
@@ -282,6 +351,8 @@ var Node = class {
|
|
|
282
351
|
anchor;
|
|
283
352
|
/** True only when the author SET an anchor — unset keeps the legacy origin. */
|
|
284
353
|
hasAnchor;
|
|
354
|
+
/** §3.5: opt-in cross-frame raster cache. Forces a group + a stamped cacheKey. */
|
|
355
|
+
cache;
|
|
285
356
|
parent = null;
|
|
286
357
|
#warnedAnchor = false;
|
|
287
358
|
/** v2 §C.3: participates in hit testing; set implicitly by attaching a listener. */
|
|
@@ -311,6 +382,7 @@ var Node = class {
|
|
|
311
382
|
this.filters = initScalar(signal([]), props.filters);
|
|
312
383
|
this.hasAnchor = props.anchor !== void 0;
|
|
313
384
|
this.anchor = resolveAnchor(props.anchor ?? "center");
|
|
385
|
+
this.cache = props.cache === true;
|
|
314
386
|
this.localMatrix = computed(() => {
|
|
315
387
|
const trs = fromTRS(this.position(), this.rotation(), this.scale());
|
|
316
388
|
const [sx, sy] = this.anchorShift();
|
|
@@ -418,21 +490,33 @@ var Node = class {
|
|
|
418
490
|
const local = this.localMatrix();
|
|
419
491
|
const isIdentity = matEquals(local, IDENTITY);
|
|
420
492
|
const shader = this.groupShader();
|
|
421
|
-
const group = this.requiresGroup() || shader !== void 0;
|
|
493
|
+
const group = this.requiresGroup() || shader !== void 0 || this.cache;
|
|
494
|
+
const wantsKey = this.cache && out.mark !== void 0;
|
|
422
495
|
out.push({ op: "save" });
|
|
423
496
|
if (!isIdentity) out.push({
|
|
424
497
|
op: "transform",
|
|
425
498
|
m: local
|
|
426
499
|
});
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
500
|
+
let pushIdx = -1;
|
|
501
|
+
if (group) {
|
|
502
|
+
if (wantsKey) pushIdx = out.mark();
|
|
503
|
+
out.push({
|
|
504
|
+
op: "pushGroup",
|
|
505
|
+
opacity,
|
|
506
|
+
blend: this.blend(),
|
|
507
|
+
filters: this.filters(),
|
|
508
|
+
...shader !== void 0 ? { shader } : {}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
const drawStart = wantsKey ? out.mark() : -1;
|
|
434
512
|
this.draw(out, ctx);
|
|
435
|
-
if (group)
|
|
513
|
+
if (group) {
|
|
514
|
+
if (wantsKey) {
|
|
515
|
+
const key = out.cacheKey(drawStart, out.mark());
|
|
516
|
+
if (key !== void 0) out.patchCacheKey(pushIdx, key);
|
|
517
|
+
}
|
|
518
|
+
out.push({ op: "popGroup" });
|
|
519
|
+
}
|
|
436
520
|
out.push({ op: "restore" });
|
|
437
521
|
}
|
|
438
522
|
};
|
|
@@ -1740,4 +1824,4 @@ function requireLayoutEngine() {
|
|
|
1740
1824
|
return engine;
|
|
1741
1825
|
}
|
|
1742
1826
|
//#endregion
|
|
1743
|
-
export {
|
|
1827
|
+
export { quantize as A, applyToPoint as B, validateSketch as C, breakLines as D, MEASURE_QUANTUM_PX as E, createDisplayListBuilder as F, invert as H, filtersToCanvasFilter as I, glow as L, segmentWords as M, setDefaultMeasurer as N, estimatingMeasurer as O, FilterValidationError as P, validateFilters as R, validateHachure as S, resolveAnchor as T, matEquals as U, fromTRS as V, multiply as W, flatten as _, Circle as a, roughen as b, Path as c, Video as d, pathFromSegs as f, arcLength as g, SketchValidationError as h, setLayoutEngine as i, segmentGraphemes as j, fallbackMeasurer as k, Rect as l, roundedRectSegs as m, getLayoutEngine as n, Group as o, revealSchedule as p, requireLayoutEngine as r, ImageNode as s, LayoutEngineMissingError as t, Text as u, hachureLines as v, Node as w, sketchStrokes as x, resolveSketch as y, IDENTITY as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/scene",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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
|
"type": "module",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"yoga-layout": "^3.2.1",
|
|
23
|
-
"@glissade/core": "0.
|
|
23
|
+
"@glissade/core": "0.10.0-pre.0"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|