@glissade/scene 0.4.2 → 0.4.4
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 +52 -2
- package/dist/index.js +186 -7
- package/dist/layout.d.ts +1 -1
- package/dist/layout.js +2 -2
- package/dist/layoutEngine.d.ts +10 -4
- package/dist/layoutEngine.js +31 -12
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as StrokeStyle, A as NodeProps, B as BlendMode, C as WordBox, D as EvalContext, E as BindablePropTarget, F as breakLines, G as FilterValidationError, H as DisplayListBuilder, I as estimatingMeasurer, J as PathSeg, K as FontSpec, L as quantize, M as resolveAnchor, N as TextMeasurer, O as HitArea, P as TextMetricsLite, Q as ShaderRef, R as segmentWords, S as VideoProps, T as AnchorSpec, U as DrawCommand, V as DisplayList, W as FilterSpec, X as Resource, Y as Rect$1, Z as ResourceId, _ as Rect, a as LayoutEngineMissingError, at as Mat2x3, b as TextProps, c as requireLayoutEngine, ct as invert, d as Group, et as createDisplayListBuilder, f as ImageNode, g as PathProps, h as Path, i as LayoutEngine, it as IDENTITY, j as PropInit, k as Node, l as setLayoutEngine, lt as matEquals, m as LineBox, n as LayoutChildSpec, nt as glow, ot as applyToPoint, p as ImageProps, q as Paint, r as LayoutContainerSpec, rt as validateFilters, s as getLayoutEngine, st as fromTRS, t as LayoutBox, tt as filtersToCanvasFilter, u as Circle, ut as multiply, v as ShapeProps, w as roundedRectSegs, x as Video, y as Text, z as setDefaultMeasurer } from "./layoutEngine.js";
|
|
2
2
|
import { BindableSignal, BoundTimeline, CompiledTimeline, Playhead, Timeline } from "@glissade/core";
|
|
3
3
|
|
|
4
4
|
//#region src/highlight.d.ts
|
|
@@ -28,6 +28,56 @@ declare class Highlight extends Node {
|
|
|
28
28
|
/** `children: [highlight(title, { color: '#ffe066' }), title]` — marker behind the text. */
|
|
29
29
|
declare function highlight(text: Text, props?: Omit<HighlightProps, 'text'>): Highlight;
|
|
30
30
|
//#endregion
|
|
31
|
+
//#region src/tokenHighlight.d.ts
|
|
32
|
+
interface TokenRange {
|
|
33
|
+
/** token text (whitespace-insensitive run match) or inclusive [from, to] wordBoxes indices */
|
|
34
|
+
match: string | readonly [number, number];
|
|
35
|
+
/** which occurrence of a string match; default 1 (the first) */
|
|
36
|
+
occurrence?: number;
|
|
37
|
+
/** range id for track targets ('<nodeId>/<rangeId>/fill' …); default 'r<index>' */
|
|
38
|
+
id?: string;
|
|
39
|
+
fill?: PropInit<string>;
|
|
40
|
+
opacity?: PropInit<number>;
|
|
41
|
+
/** 0→1 left-to-right reveal across the range; default 1 */
|
|
42
|
+
progress?: PropInit<number>;
|
|
43
|
+
/** scale about the range rect's center; default 1 */
|
|
44
|
+
scale?: PropInit<number>;
|
|
45
|
+
}
|
|
46
|
+
interface TokenHighlightProps extends NodeProps {
|
|
47
|
+
/** the Text whose tokens get highlighted; place this node as an EARLIER sibling */
|
|
48
|
+
text: Text;
|
|
49
|
+
ranges: TokenRange[];
|
|
50
|
+
/** marker overhang beyond the ink box, [x, y] px; default [4, 2] */
|
|
51
|
+
padding?: [number, number];
|
|
52
|
+
cornerRadius?: number;
|
|
53
|
+
/** re-resolve string matches every frame (animated text); default false — drift throws */
|
|
54
|
+
rematch?: boolean;
|
|
55
|
+
}
|
|
56
|
+
declare class TokenMatchError extends Error {
|
|
57
|
+
constructor(message: string);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Find the Nth whitespace-stripped consecutive run of boxes equal to token.
|
|
61
|
+
* Boundary-exact: a run that diverges mid-segment is not a match; ending
|
|
62
|
+
* mid-segment throws (with the real segment list) rather than half-boxing.
|
|
63
|
+
*/
|
|
64
|
+
declare function matchTokenRun(boxes: WordBox[], token: string, occurrence?: number): [number, number];
|
|
65
|
+
declare class TokenHighlight extends Node {
|
|
66
|
+
readonly target: Text;
|
|
67
|
+
readonly padding: [number, number];
|
|
68
|
+
readonly cornerRadius: number;
|
|
69
|
+
readonly rematch: boolean;
|
|
70
|
+
private readonly ranges;
|
|
71
|
+
constructor(props: TokenHighlightProps);
|
|
72
|
+
private resolveRun;
|
|
73
|
+
protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* `children: [tokenHighlight(para, { ranges: [{ match: '$48,200', fill: cat.money }] }), para]`
|
|
77
|
+
* — each range animates independently via '<id>/<rangeId>/fill|opacity|progress|scale'.
|
|
78
|
+
*/
|
|
79
|
+
declare function tokenHighlight(text: Text, props: Omit<TokenHighlightProps, 'text'>): TokenHighlight;
|
|
80
|
+
//#endregion
|
|
31
81
|
//#region src/assets.d.ts
|
|
32
82
|
/**
|
|
33
83
|
* Asset contracts (DESIGN.md §3.8): evaluate() never awaits — callers warm
|
|
@@ -218,4 +268,4 @@ declare function bindScene(scene: Scene, doc: Timeline): BindingCacheEntry;
|
|
|
218
268
|
*/
|
|
219
269
|
declare function evaluate(scene: Scene, doc: Timeline, t: number): DisplayList;
|
|
220
270
|
//#endregion
|
|
221
|
-
export { type AnchorSpec, type BindablePropTarget, type BlendMode, type CanvasLike, Circle, ColdAssetError, type Ctx2DLike, type DisplayList, type DisplayListBuilder, type DrawCommand, DuplicateNodeIdError, type EvalContext, type FilterSpec, FilterValidationError, type FontSpec, Group, 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 PathSeg, type PropInit, Raster2D, type Raster2DHost, Rect, type Rect$1 as RectShape, type Resource, type ResourceId, type Scene, type SceneInit, type SceneModule, type ShaderCaps, ShaderEffect, type ShaderEffectProps, type ShaderRef, type ShapeProps, type StrokeStyle, Text, type TextMeasurer, type TextMetricsLite, type TextProps, Video, type VideoFrameSource, type VideoProps, type WordBox, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, multiply, quantize, requireLayoutEngine, resolveAnchor, roundedRectSegs, segmentWords, setLayoutEngine, validateFilters };
|
|
271
|
+
export { type AnchorSpec, type BindablePropTarget, type BlendMode, type CanvasLike, Circle, ColdAssetError, type Ctx2DLike, type DisplayList, type DisplayListBuilder, type DrawCommand, DuplicateNodeIdError, type EvalContext, type FilterSpec, FilterValidationError, type FontSpec, Group, 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 PathSeg, type PropInit, Raster2D, type Raster2DHost, Rect, type Rect$1 as RectShape, type Resource, type ResourceId, type Scene, type SceneInit, type SceneModule, type ShaderCaps, ShaderEffect, type ShaderEffectProps, type ShaderRef, type ShapeProps, type StrokeStyle, Text, type TextMeasurer, type TextMetricsLite, type TextProps, TokenHighlight, type TokenHighlightProps, TokenMatchError, type TokenRange, Video, type VideoFrameSource, type VideoProps, type WordBox, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, multiply, quantize, requireLayoutEngine, resolveAnchor, roundedRectSegs, segmentWords, setDefaultMeasurer, setLayoutEngine, tokenHighlight, validateFilters };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { A as matEquals, C as filtersToCanvasFilter, D as applyToPoint, E as IDENTITY, O as fromTRS, S as createDisplayListBuilder, T as validateFilters, _ as fallbackMeasurer, a as Circle, b as setDefaultMeasurer, c as Path, d as Video, f as roundedRectSegs, g as estimatingMeasurer, h as breakLines, i as setLayoutEngine, j as multiply, k as invert, l as Rect, m as resolveAnchor, n as getLayoutEngine, o as Group, p as Node, r as requireLayoutEngine, s as ImageNode, t as LayoutEngineMissingError, u as Text, v as quantize, w as glow, x as FilterValidationError, y as segmentWords } from "./layoutEngine.js";
|
|
2
2
|
import { bindTimeline, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, signal } from "@glissade/core";
|
|
3
3
|
//#region src/highlight.ts
|
|
4
4
|
/**
|
|
@@ -19,8 +19,8 @@ var Highlight = class extends Node {
|
|
|
19
19
|
constructor(props) {
|
|
20
20
|
super(props);
|
|
21
21
|
this.target = props.text;
|
|
22
|
-
this.color = init(signal("#ffe066"), props.color);
|
|
23
|
-
this.progress = init(signal(1), props.progress);
|
|
22
|
+
this.color = init$1(signal("#ffe066"), props.color);
|
|
23
|
+
this.progress = init$1(signal(1), props.progress);
|
|
24
24
|
this.padding = props.padding ?? [4, 2];
|
|
25
25
|
this.cornerRadius = props.cornerRadius ?? 4;
|
|
26
26
|
this.registerTarget("progress", this.progress);
|
|
@@ -73,6 +73,185 @@ function highlight(text, props = {}) {
|
|
|
73
73
|
text
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
+
function init$1(sig, v) {
|
|
77
|
+
if (typeof v === "function") sig.bindSource(v);
|
|
78
|
+
else if (v !== void 0) sig.set(v);
|
|
79
|
+
return sig;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/tokenHighlight.ts
|
|
83
|
+
/**
|
|
84
|
+
* Multi-range token highlight: sub-line ranges over a Text node's wordBoxes,
|
|
85
|
+
* each with its OWN animatable fill/opacity/progress/scale — four-color
|
|
86
|
+
* category passes, per-token flips, karaoke-with-color. Design answers from
|
|
87
|
+
* downstream production (the NNDL verification ritual):
|
|
88
|
+
* - ranges VALIDATE at construction and THROW on copy drift at draw — the
|
|
89
|
+
* throw is load-bearing (it catches edited copy that no longer matches);
|
|
90
|
+
* `rematch: true` opts animated text into per-frame re-resolution.
|
|
91
|
+
* - a range spanning a wrap produces one rect per line segment.
|
|
92
|
+
* - string matches are whitespace-stripped consecutive box runs and must end
|
|
93
|
+
* boundary-exact (mid-segment end = error listing the actual segments);
|
|
94
|
+
* `[wordIndex, wordIndex]` ranges sidestep matching entirely.
|
|
95
|
+
*/
|
|
96
|
+
var TokenMatchError = class extends Error {
|
|
97
|
+
constructor(message) {
|
|
98
|
+
super(message);
|
|
99
|
+
this.name = "TokenMatchError";
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const stripWs = (s) => s.replace(/\s+/g, "");
|
|
103
|
+
/**
|
|
104
|
+
* Find the Nth whitespace-stripped consecutive run of boxes equal to token.
|
|
105
|
+
* Boundary-exact: a run that diverges mid-segment is not a match; ending
|
|
106
|
+
* mid-segment throws (with the real segment list) rather than half-boxing.
|
|
107
|
+
*/
|
|
108
|
+
function matchTokenRun(boxes, token, occurrence = 1) {
|
|
109
|
+
const want = stripWs(token);
|
|
110
|
+
if (want.length === 0) throw new TokenMatchError("empty token");
|
|
111
|
+
let seen = 0;
|
|
112
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
113
|
+
let acc = "";
|
|
114
|
+
for (let j = i; j < boxes.length; j++) {
|
|
115
|
+
acc += stripWs(boxes[j].text);
|
|
116
|
+
if (acc === want) {
|
|
117
|
+
seen++;
|
|
118
|
+
if (seen === occurrence) return [i, j];
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
if (!want.startsWith(acc)) break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
throw new TokenMatchError(`no match for token '${token}'${occurrence > 1 ? ` (occurrence ${occurrence})` : ""} — segments are [${boxes.map((b) => `'${b.text}'`).join(", ")}]`);
|
|
125
|
+
}
|
|
126
|
+
var TokenHighlight = class extends Node {
|
|
127
|
+
target;
|
|
128
|
+
padding;
|
|
129
|
+
cornerRadius;
|
|
130
|
+
rematch;
|
|
131
|
+
ranges;
|
|
132
|
+
constructor(props) {
|
|
133
|
+
super(props);
|
|
134
|
+
this.target = props.text;
|
|
135
|
+
this.padding = props.padding ?? [4, 2];
|
|
136
|
+
this.cornerRadius = props.cornerRadius ?? 4;
|
|
137
|
+
this.rematch = props.rematch ?? false;
|
|
138
|
+
const boxes = this.target.wordBoxes();
|
|
139
|
+
this.ranges = props.ranges.map((spec, index) => {
|
|
140
|
+
const run = this.resolveRun(boxes, spec);
|
|
141
|
+
const id = spec.id ?? `r${index}`;
|
|
142
|
+
const r = {
|
|
143
|
+
spec,
|
|
144
|
+
fill: init(signal("#ffe066"), spec.fill),
|
|
145
|
+
opacity: init(signal(1), spec.opacity),
|
|
146
|
+
progress: init(signal(1), spec.progress),
|
|
147
|
+
scale: init(signal(1), spec.scale),
|
|
148
|
+
run,
|
|
149
|
+
bound: runText(boxes, run)
|
|
150
|
+
};
|
|
151
|
+
this.registerTarget(`${id}/fill`, r.fill);
|
|
152
|
+
this.registerTarget(`${id}/opacity`, r.opacity);
|
|
153
|
+
this.registerTarget(`${id}/progress`, r.progress);
|
|
154
|
+
this.registerTarget(`${id}/scale`, r.scale);
|
|
155
|
+
return r;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
resolveRun(boxes, spec) {
|
|
159
|
+
if (typeof spec.match !== "string") {
|
|
160
|
+
const [from, to] = spec.match;
|
|
161
|
+
if (from < 0 || to >= boxes.length || from > to) throw new TokenMatchError(`word index range [${from}, ${to}] out of bounds (${boxes.length} boxes)`);
|
|
162
|
+
return [from, to];
|
|
163
|
+
}
|
|
164
|
+
return matchTokenRun(boxes, spec.match, spec.occurrence ?? 1);
|
|
165
|
+
}
|
|
166
|
+
draw(out, ctx) {
|
|
167
|
+
const boxes = this.target.wordBoxes(ctx.measurer);
|
|
168
|
+
if (boxes.length === 0) return;
|
|
169
|
+
const tm = this.target.localMatrix();
|
|
170
|
+
const [px, py] = this.padding;
|
|
171
|
+
let pushedTransform = false;
|
|
172
|
+
for (const r of this.ranges) {
|
|
173
|
+
const opacity = r.opacity();
|
|
174
|
+
if (opacity <= 0) continue;
|
|
175
|
+
const progress = Math.min(1, Math.max(0, r.progress()));
|
|
176
|
+
if (progress <= 0) continue;
|
|
177
|
+
let run = r.run;
|
|
178
|
+
if (this.rematch && typeof r.spec.match === "string") run = matchTokenRun(boxes, r.spec.match, r.spec.occurrence ?? 1);
|
|
179
|
+
else if (runText(boxes, run) !== r.bound) throw new TokenMatchError(`bound token '${r.bound}' no longer matches boxes [${run[0]}, ${run[1]}] — the text changed (segments now [${boxes.slice(run[0], run[1] + 1).map((b) => `'${b.text}'`).join(", ")}]); pass rematch: true for animated text`);
|
|
180
|
+
const lineRects = [];
|
|
181
|
+
for (let i = run[0]; i <= run[1]; i++) {
|
|
182
|
+
const b = boxes[i];
|
|
183
|
+
const last = lineRects[lineRects.length - 1];
|
|
184
|
+
const cur = i > run[0] && boxes[i - 1].line === b.line ? last : void 0;
|
|
185
|
+
if (cur) {
|
|
186
|
+
cur.w = b.x + b.w + px - cur.x;
|
|
187
|
+
cur.y = Math.min(cur.y, b.y - py);
|
|
188
|
+
cur.h = Math.max(cur.h, b.y + b.h + py - cur.y);
|
|
189
|
+
} else lineRects.push({
|
|
190
|
+
x: b.x - px,
|
|
191
|
+
y: b.y - py,
|
|
192
|
+
w: b.w + 2 * px,
|
|
193
|
+
h: b.h + 2 * py
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (!pushedTransform && !matEquals(tm, IDENTITY)) {
|
|
197
|
+
out.push({
|
|
198
|
+
op: "transform",
|
|
199
|
+
m: tm
|
|
200
|
+
});
|
|
201
|
+
pushedTransform = true;
|
|
202
|
+
}
|
|
203
|
+
const grouped = opacity < 1;
|
|
204
|
+
if (grouped) out.push({
|
|
205
|
+
op: "pushGroup",
|
|
206
|
+
opacity,
|
|
207
|
+
blend: "source-over",
|
|
208
|
+
filters: []
|
|
209
|
+
});
|
|
210
|
+
const fill = r.fill();
|
|
211
|
+
const scale = r.scale();
|
|
212
|
+
let remaining = progress * lineRects.reduce((sum, q) => sum + q.w, 0);
|
|
213
|
+
for (const q of lineRects) {
|
|
214
|
+
const fillW = Math.min(q.w, remaining);
|
|
215
|
+
remaining -= fillW;
|
|
216
|
+
if (fillW <= 0) break;
|
|
217
|
+
const cx = q.x + q.w / 2;
|
|
218
|
+
const cy = q.y + q.h / 2;
|
|
219
|
+
const w = fillW * scale;
|
|
220
|
+
const h = q.h * scale;
|
|
221
|
+
const x = cx - q.w / 2 * scale;
|
|
222
|
+
const y = cy - h / 2;
|
|
223
|
+
const rad = Math.min(this.cornerRadius, w / 2, h / 2);
|
|
224
|
+
const path = out.resource({
|
|
225
|
+
kind: "path",
|
|
226
|
+
segs: roundedRectSegs(x, y, w, h, rad)
|
|
227
|
+
});
|
|
228
|
+
out.push({
|
|
229
|
+
op: "fillPath",
|
|
230
|
+
path,
|
|
231
|
+
paint: {
|
|
232
|
+
kind: "color",
|
|
233
|
+
color: fill
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
if (remaining <= 0) break;
|
|
237
|
+
}
|
|
238
|
+
if (grouped) out.push({ op: "popGroup" });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
function runText(boxes, run) {
|
|
243
|
+
return boxes.slice(run[0], run[1] + 1).map((b) => stripWs(b.text)).join("");
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* `children: [tokenHighlight(para, { ranges: [{ match: '$48,200', fill: cat.money }] }), para]`
|
|
247
|
+
* — each range animates independently via '<id>/<rangeId>/fill|opacity|progress|scale'.
|
|
248
|
+
*/
|
|
249
|
+
function tokenHighlight(text, props) {
|
|
250
|
+
return new TokenHighlight({
|
|
251
|
+
...props,
|
|
252
|
+
text
|
|
253
|
+
});
|
|
254
|
+
}
|
|
76
255
|
function init(sig, v) {
|
|
77
256
|
if (typeof v === "function") sig.bindSource(v);
|
|
78
257
|
else if (v !== void 0) sig.set(v);
|
|
@@ -505,8 +684,8 @@ function createScene(init) {
|
|
|
505
684
|
});
|
|
506
685
|
const nodes = /* @__PURE__ */ new Map();
|
|
507
686
|
const playhead = createPlayhead();
|
|
508
|
-
let measurer =
|
|
509
|
-
indexNodes(root, nodes, () => measurer);
|
|
687
|
+
let measurer = null;
|
|
688
|
+
indexNodes(root, nodes, () => measurer ?? fallbackMeasurer());
|
|
510
689
|
return {
|
|
511
690
|
root,
|
|
512
691
|
nodes,
|
|
@@ -521,7 +700,7 @@ function createScene(init) {
|
|
|
521
700
|
measurer = m;
|
|
522
701
|
},
|
|
523
702
|
get textMeasurer() {
|
|
524
|
-
return measurer;
|
|
703
|
+
return measurer ?? fallbackMeasurer();
|
|
525
704
|
}
|
|
526
705
|
};
|
|
527
706
|
}
|
|
@@ -563,4 +742,4 @@ function evaluate(scene, doc, t) {
|
|
|
563
742
|
});
|
|
564
743
|
}
|
|
565
744
|
//#endregion
|
|
566
|
-
export { Circle, ColdAssetError, DuplicateNodeIdError, FilterValidationError, Group, Highlight, IDENTITY, ImageNode, LayoutEngineMissingError, Node, Path, Raster2D, Rect, ShaderEffect, Text, Video, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, multiply, quantize, requireLayoutEngine, resolveAnchor, roundedRectSegs, segmentWords, setLayoutEngine, validateFilters };
|
|
745
|
+
export { Circle, ColdAssetError, DuplicateNodeIdError, FilterValidationError, Group, Highlight, IDENTITY, ImageNode, LayoutEngineMissingError, Node, Path, Raster2D, Rect, ShaderEffect, Text, TokenHighlight, TokenMatchError, Video, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, multiply, quantize, requireLayoutEngine, resolveAnchor, roundedRectSegs, segmentWords, setDefaultMeasurer, setLayoutEngine, tokenHighlight, validateFilters };
|
package/dist/layout.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as NodeProps, D as EvalContext,
|
|
1
|
+
import { A as NodeProps, D as EvalContext, H as DisplayListBuilder, N as TextMeasurer, a as LayoutEngineMissingError, d as Group, i as LayoutEngine, j as PropInit, k as Node, l as setLayoutEngine, n as LayoutChildSpec, o as LayoutResult, 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,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as fallbackMeasurer, i as setLayoutEngine, n as getLayoutEngine, o as Group, r as requireLayoutEngine, t as LayoutEngineMissingError } from "./layoutEngine.js";
|
|
2
2
|
import { signal } from "@glissade/core";
|
|
3
3
|
//#region src/layout.ts
|
|
4
4
|
/**
|
|
@@ -68,7 +68,7 @@ var Layout = class extends Group {
|
|
|
68
68
|
* The measurer defaults to the scene-injected one (estimating pre-scene).
|
|
69
69
|
*/
|
|
70
70
|
computedSize(measurer) {
|
|
71
|
-
const m = measurer ?? this.measurerSource?.() ??
|
|
71
|
+
const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
|
|
72
72
|
return this.#compute(m).size;
|
|
73
73
|
}
|
|
74
74
|
#compute(measurer) {
|
package/dist/layoutEngine.d.ts
CHANGED
|
@@ -189,10 +189,16 @@ interface TextMeasurer {
|
|
|
189
189
|
/** §3.6 measurement quantum. */
|
|
190
190
|
declare function quantize(v: number): number;
|
|
191
191
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
192
|
+
* Process-wide fallback measurer for FACTORY-TIME measurement — component
|
|
193
|
+
* factories run before any scene exists, so Text pulls (measuredSize,
|
|
194
|
+
* lineBoxes, wordBoxes) and createScene fall back here before the estimator.
|
|
195
|
+
* Node consumers: `setDefaultMeasurer(createMeasurer({ fonts }))` from
|
|
196
|
+
* @glissade/backend-skia gives factory code the rasterizer's real metrics.
|
|
197
|
+
* Scene-injected measurers (mount/CLI/golden harness) always win.
|
|
195
198
|
*/
|
|
199
|
+
declare function setDefaultMeasurer(m: TextMeasurer | null): void;
|
|
200
|
+
/** The default-or-estimating chain end; internal fallback for measurer pulls. */
|
|
201
|
+
|
|
196
202
|
declare const estimatingMeasurer: TextMeasurer;
|
|
197
203
|
/**
|
|
198
204
|
* The draw-path word segmentation (Intl.Segmenter boundaries, punctuation
|
|
@@ -602,4 +608,4 @@ declare function setLayoutEngine(e: LayoutEngine): void;
|
|
|
602
608
|
declare function getLayoutEngine(): LayoutEngine | null;
|
|
603
609
|
declare function requireLayoutEngine(): LayoutEngine;
|
|
604
610
|
//#endregion
|
|
605
|
-
export {
|
|
611
|
+
export { StrokeStyle as $, NodeProps as A, BlendMode as B, WordBox as C, EvalContext as D, BindablePropTarget as E, breakLines as F, FilterValidationError as G, DisplayListBuilder as H, estimatingMeasurer as I, PathSeg as J, FontSpec as K, quantize as L, resolveAnchor as M, TextMeasurer as N, HitArea as O, TextMetricsLite as P, ShaderRef as Q, segmentWords as R, VideoProps as S, AnchorSpec as T, DrawCommand as U, DisplayList as V, FilterSpec as W, Resource as X, Rect$1 as Y, ResourceId as Z, Rect as _, LayoutEngineMissingError as a, Mat2x3 as at, TextProps as b, requireLayoutEngine as c, invert as ct, Group as d, createDisplayListBuilder as et, ImageNode as f, PathProps as g, Path as h, LayoutEngine as i, IDENTITY as it, PropInit as j, Node as k, setLayoutEngine as l, matEquals as lt, LineBox as m, LayoutChildSpec as n, glow as nt, LayoutResult as o, applyToPoint as ot, ImageProps as p, Paint as q, LayoutContainerSpec as r, validateFilters as rt, getLayoutEngine as s, fromTRS as st, LayoutBox as t, filtersToCanvasFilter as tt, Circle as u, multiply as ut, ShapeProps as v, roundedRectSegs as w, Video as x, Text as y, setDefaultMeasurer as z };
|
package/dist/layoutEngine.js
CHANGED
|
@@ -158,6 +158,22 @@ function quantize(v) {
|
|
|
158
158
|
* (e.g. evaluating for IR-level tests). Deterministic but not metrically
|
|
159
159
|
* faithful; mount(), the CLI, and exporters always inject the real one.
|
|
160
160
|
*/
|
|
161
|
+
let defaultMeasurer = null;
|
|
162
|
+
/**
|
|
163
|
+
* Process-wide fallback measurer for FACTORY-TIME measurement — component
|
|
164
|
+
* factories run before any scene exists, so Text pulls (measuredSize,
|
|
165
|
+
* lineBoxes, wordBoxes) and createScene fall back here before the estimator.
|
|
166
|
+
* Node consumers: `setDefaultMeasurer(createMeasurer({ fonts }))` from
|
|
167
|
+
* @glissade/backend-skia gives factory code the rasterizer's real metrics.
|
|
168
|
+
* Scene-injected measurers (mount/CLI/golden harness) always win.
|
|
169
|
+
*/
|
|
170
|
+
function setDefaultMeasurer(m) {
|
|
171
|
+
defaultMeasurer = m;
|
|
172
|
+
}
|
|
173
|
+
/** The default-or-estimating chain end; internal fallback for measurer pulls. */
|
|
174
|
+
function fallbackMeasurer() {
|
|
175
|
+
return defaultMeasurer ?? estimatingMeasurer;
|
|
176
|
+
}
|
|
161
177
|
const estimatingMeasurer = { measureText(text, font) {
|
|
162
178
|
return {
|
|
163
179
|
width: text.length * font.size * .52,
|
|
@@ -329,7 +345,7 @@ var Node = class {
|
|
|
329
345
|
* left/center/right baseline origin; Path from author-positioned bounds.
|
|
330
346
|
*/
|
|
331
347
|
drawOffset(measurer) {
|
|
332
|
-
const m = measurer ?? this.measurerSource?.() ??
|
|
348
|
+
const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
|
|
333
349
|
const size = this.intrinsicSize(m) ?? {
|
|
334
350
|
w: 0,
|
|
335
351
|
h: 0
|
|
@@ -345,7 +361,7 @@ var Node = class {
|
|
|
345
361
|
* (−ax·w, −ay·h); the center default reproduces (−w/2, −h/2).
|
|
346
362
|
*/
|
|
347
363
|
flowOffset(measurer) {
|
|
348
|
-
const m = measurer ?? this.measurerSource?.() ??
|
|
364
|
+
const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
|
|
349
365
|
const d = this.drawOffset(m);
|
|
350
366
|
const [sx, sy] = this.anchorShift(m);
|
|
351
367
|
return {
|
|
@@ -365,7 +381,7 @@ var Node = class {
|
|
|
365
381
|
anchorShift(measurer) {
|
|
366
382
|
if (!this.hasAnchor) return [0, 0];
|
|
367
383
|
const [ax, ay] = this.anchor;
|
|
368
|
-
const m = measurer ?? this.measurerSource?.() ??
|
|
384
|
+
const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
|
|
369
385
|
const size = this.intrinsicSize(m);
|
|
370
386
|
if (!size) {
|
|
371
387
|
if (!this.#warnedAnchor) {
|
|
@@ -869,7 +885,7 @@ var Text = class extends Node {
|
|
|
869
885
|
}
|
|
870
886
|
/** Text draws from a baseline origin at its align edge, not a center (§3.6). */
|
|
871
887
|
drawOffset(measurer) {
|
|
872
|
-
const m = measurer ?? this.measurerSource?.() ??
|
|
888
|
+
const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
|
|
873
889
|
const size = this.intrinsicSize(m);
|
|
874
890
|
const font = {
|
|
875
891
|
family: this.fontFamily,
|
|
@@ -889,7 +905,7 @@ var Text = class extends Node {
|
|
|
889
905
|
* text dimensions (e.g. underline width = () => title.measuredSize().w).
|
|
890
906
|
*/
|
|
891
907
|
measuredSize(measurer) {
|
|
892
|
-
return this.intrinsicSize(measurer ?? this.measurerSource?.() ??
|
|
908
|
+
return this.intrinsicSize(measurer ?? this.measurerSource?.() ?? fallbackMeasurer());
|
|
893
909
|
}
|
|
894
910
|
/**
|
|
895
911
|
* Per-line ink boxes in this node's DRAW space (origin = first baseline at
|
|
@@ -899,7 +915,7 @@ var Text = class extends Node {
|
|
|
899
915
|
* reveals, selections.
|
|
900
916
|
*/
|
|
901
917
|
lineBoxes(measurer) {
|
|
902
|
-
const m = measurer ?? this.measurerSource?.() ??
|
|
918
|
+
const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
|
|
903
919
|
const text = this.text();
|
|
904
920
|
if (!text) return [];
|
|
905
921
|
const font = {
|
|
@@ -936,7 +952,7 @@ var Text = class extends Node {
|
|
|
936
952
|
* karaoke; draw your own rects for sub-line multi-color token work.
|
|
937
953
|
*/
|
|
938
954
|
wordBoxes(measurer) {
|
|
939
|
-
const m = measurer ?? this.measurerSource?.() ??
|
|
955
|
+
const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
|
|
940
956
|
const text = this.text();
|
|
941
957
|
if (!text) return [];
|
|
942
958
|
const font = {
|
|
@@ -958,12 +974,15 @@ var Text = class extends Node {
|
|
|
958
974
|
const h = met.ascent + met.descent;
|
|
959
975
|
let prefix = "";
|
|
960
976
|
for (const seg of segmentWords(line)) {
|
|
961
|
-
const
|
|
977
|
+
const start = prefix;
|
|
962
978
|
prefix += seg;
|
|
963
|
-
|
|
964
|
-
|
|
979
|
+
const word = seg.trim();
|
|
980
|
+
if (word === "") continue;
|
|
981
|
+
const lead = seg.length - seg.trimStart().length;
|
|
982
|
+
const before = m.measureText(start + seg.slice(0, lead), font).width;
|
|
983
|
+
const after = m.measureText(start + seg.trimEnd(), font).width;
|
|
965
984
|
boxes.push({
|
|
966
|
-
text:
|
|
985
|
+
text: word,
|
|
967
986
|
line: i,
|
|
968
987
|
x: lineX + before,
|
|
969
988
|
y,
|
|
@@ -1022,4 +1041,4 @@ function requireLayoutEngine() {
|
|
|
1022
1041
|
return engine;
|
|
1023
1042
|
}
|
|
1024
1043
|
//#endregion
|
|
1025
|
-
export {
|
|
1044
|
+
export { matEquals as A, filtersToCanvasFilter as C, applyToPoint as D, IDENTITY as E, fromTRS as O, createDisplayListBuilder as S, validateFilters as T, fallbackMeasurer as _, Circle as a, setDefaultMeasurer as b, Path as c, Video as d, roundedRectSegs as f, estimatingMeasurer as g, breakLines as h, setLayoutEngine as i, multiply as j, invert as k, Rect as l, resolveAnchor as m, getLayoutEngine as n, Group as o, Node as p, requireLayoutEngine as r, ImageNode as s, LayoutEngineMissingError as t, Text as u, quantize as v, glow as w, FilterValidationError as x, segmentWords as y };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/scene",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
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.4.
|
|
23
|
+
"@glissade/core": "0.4.4"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|