@oh-just-another/renderer-core 0.57.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +40 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/animation-adapter.d.ts +91 -0
  6. package/dist/animation-adapter.d.ts.map +1 -0
  7. package/dist/animation-adapter.js +90 -0
  8. package/dist/animation-adapter.js.map +1 -0
  9. package/dist/built-in-renderers.d.ts +6 -0
  10. package/dist/built-in-renderers.d.ts.map +1 -0
  11. package/dist/built-in-renderers.js +517 -0
  12. package/dist/built-in-renderers.js.map +1 -0
  13. package/dist/constants.d.ts +97 -0
  14. package/dist/constants.d.ts.map +1 -0
  15. package/dist/constants.js +115 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/edge-cache-bitmap.d.ts +34 -0
  18. package/dist/edge-cache-bitmap.d.ts.map +1 -0
  19. package/dist/edge-cache-bitmap.js +42 -0
  20. package/dist/edge-cache-bitmap.js.map +1 -0
  21. package/dist/edge-cache.d.ts +32 -0
  22. package/dist/edge-cache.d.ts.map +1 -0
  23. package/dist/edge-cache.js +90 -0
  24. package/dist/edge-cache.js.map +1 -0
  25. package/dist/edge-renderer.d.ts +73 -0
  26. package/dist/edge-renderer.d.ts.map +1 -0
  27. package/dist/edge-renderer.js +554 -0
  28. package/dist/edge-renderer.js.map +1 -0
  29. package/dist/grid-renderer.d.ts +55 -0
  30. package/dist/grid-renderer.d.ts.map +1 -0
  31. package/dist/grid-renderer.js +173 -0
  32. package/dist/grid-renderer.js.map +1 -0
  33. package/dist/index.d.ts +37 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +24 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/js-rasterizer.d.ts +8 -0
  38. package/dist/js-rasterizer.d.ts.map +1 -0
  39. package/dist/js-rasterizer.js +86 -0
  40. package/dist/js-rasterizer.js.map +1 -0
  41. package/dist/layer-cache-composite.d.ts +39 -0
  42. package/dist/layer-cache-composite.d.ts.map +1 -0
  43. package/dist/layer-cache-composite.js +47 -0
  44. package/dist/layer-cache-composite.js.map +1 -0
  45. package/dist/layers.d.ts +14 -0
  46. package/dist/layers.d.ts.map +1 -0
  47. package/dist/layers.js +2 -0
  48. package/dist/layers.js.map +1 -0
  49. package/dist/rasterizer.d.ts +27 -0
  50. package/dist/rasterizer.d.ts.map +1 -0
  51. package/dist/rasterizer.js +10 -0
  52. package/dist/rasterizer.js.map +1 -0
  53. package/dist/render-target.d.ts +94 -0
  54. package/dist/render-target.d.ts.map +1 -0
  55. package/dist/render-target.js +2 -0
  56. package/dist/render-target.js.map +1 -0
  57. package/dist/scene-renderer.d.ts +126 -0
  58. package/dist/scene-renderer.d.ts.map +1 -0
  59. package/dist/scene-renderer.js +149 -0
  60. package/dist/scene-renderer.js.map +1 -0
  61. package/dist/shape-cache-bitmap.d.ts +42 -0
  62. package/dist/shape-cache-bitmap.d.ts.map +1 -0
  63. package/dist/shape-cache-bitmap.js +61 -0
  64. package/dist/shape-cache-bitmap.js.map +1 -0
  65. package/dist/shape-cache.d.ts +40 -0
  66. package/dist/shape-cache.d.ts.map +1 -0
  67. package/dist/shape-cache.js +67 -0
  68. package/dist/shape-cache.js.map +1 -0
  69. package/dist/shape-renderer.d.ts +36 -0
  70. package/dist/shape-renderer.d.ts.map +1 -0
  71. package/dist/shape-renderer.js +15 -0
  72. package/dist/shape-renderer.js.map +1 -0
  73. package/dist/text-editing.d.ts +81 -0
  74. package/dist/text-editing.d.ts.map +1 -0
  75. package/dist/text-editing.js +191 -0
  76. package/dist/text-editing.js.map +1 -0
  77. package/dist/text-layout.d.ts +44 -0
  78. package/dist/text-layout.d.ts.map +1 -0
  79. package/dist/text-layout.js +57 -0
  80. package/dist/text-layout.js.map +1 -0
  81. package/dist/text-shaper.d.ts +53 -0
  82. package/dist/text-shaper.d.ts.map +1 -0
  83. package/dist/text-shaper.js +19 -0
  84. package/dist/text-shaper.js.map +1 -0
  85. package/dist/tile-renderer.d.ts +126 -0
  86. package/dist/tile-renderer.d.ts.map +1 -0
  87. package/dist/tile-renderer.js +192 -0
  88. package/dist/tile-renderer.js.map +1 -0
  89. package/dist/worker-render.d.ts +54 -0
  90. package/dist/worker-render.d.ts.map +1 -0
  91. package/dist/worker-render.js +8 -0
  92. package/dist/worker-render.js.map +1 -0
  93. package/package.json +56 -0
@@ -0,0 +1,81 @@
1
+ import type { TextAlign } from "./render-target.js";
2
+ /**
3
+ * Caret-aware text layout. Unlike {@link wrapText} (which collapses
4
+ * whitespace and is only good enough for *drawing*), this keeps every
5
+ * line as an exact substring of the source plus its `[start, end)`
6
+ * character offsets — so a caret index maps unambiguously to a line +
7
+ * column. Measurement is injected as a `measure(s) => width` callback
8
+ * so the same geometry can be computed against either backend's font
9
+ * metrics (Canvas2D `measureText` or WebGL2 MSDF advances).
10
+ *
11
+ * Convention for `\n`: a hard newline at source index `k` ends the
12
+ * current line at `end === k` (caret at `k` = end of line) and the next
13
+ * line starts at `start === k + 1` (caret at `k + 1` = start of next
14
+ * line). The `\n` itself never holds a caret.
15
+ */
16
+ export interface LaidOutLine {
17
+ /** Exact source substring for this visual line (no whitespace collapsing). */
18
+ readonly text: string;
19
+ /** Source offset where the line begins (inclusive). */
20
+ readonly start: number;
21
+ /** Source offset where the line ends (exclusive; excludes a trailing `\n`). */
22
+ readonly end: number;
23
+ /** Measured width of `text` in CSS px. */
24
+ readonly width: number;
25
+ }
26
+ export interface EditableTextLayout {
27
+ readonly lines: readonly LaidOutLine[];
28
+ readonly lineHeight: number;
29
+ /** Width the lines are aligned within (maxWidth, or the widest line). */
30
+ readonly blockWidth: number;
31
+ }
32
+ export type MeasureText = (text: string) => number;
33
+ export interface LayoutTextOptions {
34
+ readonly fontSize: number;
35
+ /** Wrap budget in CSS px. `undefined` → no wrap (split on `\n` only). */
36
+ readonly maxWidth?: number;
37
+ /** Line-height multiplier. Default 1.2 (matches the text renderer). */
38
+ readonly lineHeightFactor?: number;
39
+ }
40
+ /** Default multiplier from font size to line height (matches `drawText`). */
41
+ export declare const DEFAULT_LINE_HEIGHT_FACTOR = 1.2;
42
+ /**
43
+ * Lay out `text` into visual lines with exact source offsets. Always
44
+ * returns at least one (possibly empty) line.
45
+ */
46
+ export declare const layoutText: (text: string, measure: MeasureText, options: LayoutTextOptions) => EditableTextLayout;
47
+ export interface CaretGeometry {
48
+ /** Local x of the caret bar. */
49
+ readonly x: number;
50
+ /** Local y of the caret top (baseline-top line origin). */
51
+ readonly y: number;
52
+ /** Caret height (≈ font size). */
53
+ readonly height: number;
54
+ /** Index of the line the caret sits on. */
55
+ readonly line: number;
56
+ }
57
+ /**
58
+ * Local-space geometry of the caret for a given source `caret` offset.
59
+ * `align` must match the renderer's `textAlign`.
60
+ */
61
+ export declare const caretGeometry: (layout: EditableTextLayout, caret: number, measure: MeasureText, fontSize: number, align: TextAlign) => CaretGeometry;
62
+ /**
63
+ * Map a local-space point to the nearest source caret offset. Used for
64
+ * click-to-place-caret and drag-to-select.
65
+ */
66
+ export declare const pointToCaretIndex: (layout: EditableTextLayout, point: {
67
+ readonly x: number;
68
+ readonly y: number;
69
+ }, measure: MeasureText, align: TextAlign) => number;
70
+ export interface SelectionRect {
71
+ readonly x: number;
72
+ readonly y: number;
73
+ readonly width: number;
74
+ readonly height: number;
75
+ }
76
+ /**
77
+ * Local-space highlight rectangles covering the source range `[from, to)`
78
+ * (order-independent), one per visual line it spans.
79
+ */
80
+ export declare const selectionRects: (layout: EditableTextLayout, from: number, to: number, measure: MeasureText, align: TextAlign) => readonly SelectionRect[];
81
+ //# sourceMappingURL=text-editing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-editing.d.ts","sourceRoot":"","sources":["../src/text-editing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAOpD;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,6EAA6E;AAC7E,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAsE9C;;;GAGG;AACH,eAAO,MAAM,UAAU,GACrB,MAAM,MAAM,EACZ,SAAS,WAAW,EACpB,SAAS,iBAAiB,KACzB,kBAoBF,CAAC;AAuBF,MAAM,WAAW,aAAa;IAC5B,gCAAgC;IAChC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,QAAQ,kBAAkB,EAC1B,OAAO,MAAM,EACb,SAAS,WAAW,EACpB,UAAU,MAAM,EAChB,OAAO,SAAS,KACf,aAOF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,kBAAkB,EAC1B,OAAO;IAAE,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACjD,SAAS,WAAW,EACpB,OAAO,SAAS,KACf,MAiBF,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,GACzB,QAAQ,kBAAkB,EAC1B,MAAM,MAAM,EACZ,IAAI,MAAM,EACV,SAAS,WAAW,EACpB,OAAO,SAAS,KACf,SAAS,aAAa,EA6BxB,CAAC"}
@@ -0,0 +1,191 @@
1
+ const req = (v) => {
2
+ if (v === undefined)
3
+ throw new Error("packages/renderer-core: index out of range");
4
+ return v;
5
+ };
6
+ /** Default multiplier from font size to line height (matches `drawText`). */
7
+ export const DEFAULT_LINE_HEIGHT_FACTOR = 1.2;
8
+ const wrapParagraph = (para, base, maxWidth, measure, out) => {
9
+ if (para === "") {
10
+ out.push({ text: "", start: base, end: base, width: 0 });
11
+ return;
12
+ }
13
+ // Word spans (non-whitespace runs) with offsets relative to `para`.
14
+ const words = [];
15
+ const re = /\S+/g;
16
+ let m;
17
+ while ((m = re.exec(para)) !== null)
18
+ words.push({ s: m.index, e: m.index + m[0].length });
19
+ if (words.length === 0) {
20
+ // Whitespace-only paragraph — keep it as one line so offsets survive.
21
+ out.push({ text: para, start: base, end: base + para.length, width: measure(para) });
22
+ return;
23
+ }
24
+ // `white-space: pre-wrap` + `overflow-wrap: break-word` (standard):
25
+ // preserve whitespace, wrap at word boundaries, and break a word that
26
+ // is itself wider than the line so narrowing the block always reflows
27
+ // (a single long word can't overflow forever). Lines are gapless
28
+ // slices of the source — inter-word whitespace at a soft break stays
29
+ // on the preceding line — so every character maps to exactly one line
30
+ // (clean caret offsets). First line starts at 0 (keeps leading
31
+ // whitespace); the last extends to the paragraph end (trailing ws).
32
+ let lineStart = 0;
33
+ const push = (start, end) => {
34
+ const text = para.slice(start, end);
35
+ out.push({ text, start: base + start, end: base + end, width: measure(text) });
36
+ };
37
+ let i = 0;
38
+ while (i < words.length) {
39
+ const w = req(words[i]);
40
+ if (measure(para.slice(lineStart, w.e)) <= maxWidth) {
41
+ i++; // word fits on the current line — keep it, try the next
42
+ continue;
43
+ }
44
+ if (w.s > lineStart) {
45
+ // Content precedes this word on the line → break before it. The
46
+ // whitespace up to its start stays on the current line (pre-wrap).
47
+ push(lineStart, w.s);
48
+ lineStart = w.s;
49
+ continue; // retry this word on the fresh line
50
+ }
51
+ // The word starts the line and alone overflows → break it by chars,
52
+ // keeping at least one char per line so we always make progress.
53
+ let e = w.s + 1;
54
+ while (e < w.e && measure(para.slice(lineStart, e + 1)) <= maxWidth)
55
+ e++;
56
+ if (e >= w.e) {
57
+ // Whole (remaining) word consumed — leave it on the current line
58
+ // and advance. Guards termination when the measurer is degenerate
59
+ // (e.g. a constant stub that never reports "fits").
60
+ i++;
61
+ continue;
62
+ }
63
+ push(lineStart, e);
64
+ lineStart = e;
65
+ w.s = e; // remainder of the word continues on the next line
66
+ }
67
+ // Last line keeps everything through the end of the paragraph.
68
+ push(lineStart, para.length);
69
+ };
70
+ /**
71
+ * Lay out `text` into visual lines with exact source offsets. Always
72
+ * returns at least one (possibly empty) line.
73
+ */
74
+ export const layoutText = (text, measure, options) => {
75
+ const lineHeight = options.fontSize * (options.lineHeightFactor ?? DEFAULT_LINE_HEIGHT_FACTOR);
76
+ const lines = [];
77
+ let paraStart = 0;
78
+ for (let i = 0; i <= text.length; i++) {
79
+ if (i === text.length || text[i] === "\n") {
80
+ const para = text.slice(paraStart, i);
81
+ if (options.maxWidth === undefined) {
82
+ lines.push({ text: para, start: paraStart, end: i, width: measure(para) });
83
+ }
84
+ else {
85
+ wrapParagraph(para, paraStart, options.maxWidth, measure, lines);
86
+ }
87
+ paraStart = i + 1;
88
+ }
89
+ }
90
+ if (lines.length === 0)
91
+ lines.push({ text: "", start: 0, end: 0, width: 0 });
92
+ let widest = 0;
93
+ for (const l of lines)
94
+ widest = Math.max(widest, l.width);
95
+ const blockWidth = options.maxWidth ?? widest;
96
+ return { lines, lineHeight, blockWidth };
97
+ };
98
+ /** Left edge (local x) where a line's glyphs start, given the align. */
99
+ const lineLeftX = (lineWidth, blockWidth, align) => {
100
+ if (align === "center")
101
+ return blockWidth / 2 - lineWidth / 2;
102
+ if (align === "right")
103
+ return blockWidth - lineWidth;
104
+ return 0;
105
+ };
106
+ /** Index of the line a caret offset falls on (handles boundaries). */
107
+ const lineIndexForCaret = (layout, caret) => {
108
+ const { lines } = layout;
109
+ for (let i = 0; i < lines.length; i++) {
110
+ const l = req(lines[i]);
111
+ // Caret belongs to this line when it's within [start, end]; the
112
+ // upper bound is inclusive so end-of-line resolves here, while
113
+ // start-of-next-line (end + 1 for a hard `\n`) resolves to the
114
+ // next line on the following iteration.
115
+ if (caret <= l.end)
116
+ return i;
117
+ }
118
+ return lines.length - 1;
119
+ };
120
+ /**
121
+ * Local-space geometry of the caret for a given source `caret` offset.
122
+ * `align` must match the renderer's `textAlign`.
123
+ */
124
+ export const caretGeometry = (layout, caret, measure, fontSize, align) => {
125
+ const i = lineIndexForCaret(layout, caret);
126
+ const line = req(layout.lines[i]);
127
+ const col = Math.max(0, Math.min(caret, line.end) - line.start);
128
+ const prefixWidth = col === 0 ? 0 : measure(line.text.slice(0, col));
129
+ const left = lineLeftX(line.width, layout.blockWidth, align);
130
+ return { x: left + prefixWidth, y: i * layout.lineHeight, height: fontSize, line: i };
131
+ };
132
+ /**
133
+ * Map a local-space point to the nearest source caret offset. Used for
134
+ * click-to-place-caret and drag-to-select.
135
+ */
136
+ export const pointToCaretIndex = (layout, point, measure, align) => {
137
+ const { lines, lineHeight } = layout;
138
+ const i = Math.max(0, Math.min(lines.length - 1, Math.floor(point.y / lineHeight)));
139
+ const line = req(lines[i]);
140
+ const left = lineLeftX(line.width, layout.blockWidth, align);
141
+ // Walk columns, picking the boundary whose x is closest to point.x.
142
+ let best = 0;
143
+ let bestDist = Math.abs(left - point.x);
144
+ for (let col = 1; col <= line.text.length; col++) {
145
+ const x = left + measure(line.text.slice(0, col));
146
+ const d = Math.abs(x - point.x);
147
+ if (d < bestDist) {
148
+ bestDist = d;
149
+ best = col;
150
+ }
151
+ }
152
+ return line.start + best;
153
+ };
154
+ /**
155
+ * Local-space highlight rectangles covering the source range `[from, to)`
156
+ * (order-independent), one per visual line it spans.
157
+ */
158
+ export const selectionRects = (layout, from, to, measure, align) => {
159
+ const lo = Math.min(from, to);
160
+ const hi = Math.max(from, to);
161
+ if (lo === hi)
162
+ return [];
163
+ const rects = [];
164
+ for (let i = 0; i < layout.lines.length; i++) {
165
+ const line = req(layout.lines[i]);
166
+ const a = Math.max(lo, line.start);
167
+ const b = Math.min(hi, line.end);
168
+ if (a > b)
169
+ continue;
170
+ if (a === b && !(lo <= line.start && hi > line.end)) {
171
+ // Empty intersection on this line, unless the selection spans the
172
+ // hard break past it (then show a thin trailing marker).
173
+ if (!(hi > line.end && lo <= line.end))
174
+ continue;
175
+ }
176
+ const left = lineLeftX(line.width, layout.blockWidth, align);
177
+ const xa = left + (a === line.start ? 0 : measure(line.text.slice(0, a - line.start)));
178
+ const xb = left + (b === line.start ? 0 : measure(line.text.slice(0, b - line.start)));
179
+ // A line whose break is inside the selection gets a small trailing
180
+ // sliver so multi-line selections read continuously.
181
+ const trailing = hi > line.end ? layout.lineHeight * 0.25 : 0;
182
+ rects.push({
183
+ x: xa,
184
+ y: i * layout.lineHeight,
185
+ width: Math.max(0, xb - xa) + trailing,
186
+ height: layout.lineHeight,
187
+ });
188
+ }
189
+ return rects;
190
+ };
191
+ //# sourceMappingURL=text-editing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-editing.js","sourceRoot":"","sources":["../src/text-editing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import type { RenderTarget } from "./render-target.js";
2
+ import type { TextShaper } from "./text-shaper.js";
3
+ export interface WrapOptions {
4
+ /** Max width in CSS pixels. */
5
+ readonly maxWidth: number;
6
+ /** Font size in CSS pixels — used to derive line height. */
7
+ readonly fontSize: number;
8
+ /** Multiplier applied to font size to derive line height. Default: 1.2. */
9
+ readonly lineHeightFactor?: number;
10
+ /**
11
+ * Optional font family name — paired with `fontSize` to call
12
+ * `shaper.measure(text, { family, size })` when a `shaper` is
13
+ * supplied. Defaults to `"sans-serif"`. Has no effect when no
14
+ * shaper is in play (Canvas2D's `measureText` already knows the
15
+ * font via the host's prior `ctx.font = …` call).
16
+ */
17
+ readonly fontFamily?: string;
18
+ /**
19
+ * Optional `TextShaper` (WASM / harfbuzz / canvaskit) that
20
+ * replaces the default `target.measureText` path. When set,
21
+ * the wrap algorithm queries `shaper.measure(line, font)` per
22
+ * candidate line, so layouts stay deterministic across
23
+ * environments (server / headless / browser).
24
+ */
25
+ readonly shaper?: TextShaper;
26
+ }
27
+ export interface WrappedLine {
28
+ readonly text: string;
29
+ readonly width: number;
30
+ }
31
+ /**
32
+ * Greedy word-wrap by `target.measureText`. Words longer than `maxWidth` are
33
+ * placed on their own line and overflow horizontally — they are not broken.
34
+ * Whitespace runs are collapsed; newlines (`\n`) in the input force a break.
35
+ *
36
+ * The caller is responsible for having set the font on the target before this
37
+ * call. Returns an array of lines plus the effective `lineHeight` so callers
38
+ * can compute total layout height.
39
+ */
40
+ export declare const wrapText: (text: string, target: RenderTarget, options: WrapOptions) => {
41
+ readonly lines: readonly WrappedLine[];
42
+ readonly lineHeight: number;
43
+ };
44
+ //# sourceMappingURL=text-layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-layout.d.ts","sourceRoot":"","sources":["../src/text-layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,+BAA+B;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;CAC9B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,GACnB,MAAM,MAAM,EACZ,QAAQ,YAAY,EACpB,SAAS,WAAW,KACnB;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAiDvE,CAAC"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Greedy word-wrap by `target.measureText`. Words longer than `maxWidth` are
3
+ * placed on their own line and overflow horizontally — they are not broken.
4
+ * Whitespace runs are collapsed; newlines (`\n`) in the input force a break.
5
+ *
6
+ * The caller is responsible for having set the font on the target before this
7
+ * call. Returns an array of lines plus the effective `lineHeight` so callers
8
+ * can compute total layout height.
9
+ */
10
+ export const wrapText = (text, target, options) => {
11
+ const { maxWidth } = options;
12
+ const lineHeightFactor = options.lineHeightFactor ?? 1.2;
13
+ const out = [];
14
+ const fontFamily = options.fontFamily ?? "sans-serif";
15
+ const shaper = options.shaper;
16
+ const measure = shaper
17
+ ? (s) => shaper.measure(s, { family: fontFamily, size: options.fontSize }).width
18
+ : (s) => target.measureText(s).width;
19
+ for (const paragraph of text.split("\n")) {
20
+ if (paragraph === "") {
21
+ out.push({ text: "", width: 0 });
22
+ continue;
23
+ }
24
+ const words = paragraph.split(/\s+/).filter((w) => w.length > 0);
25
+ if (words.length === 0) {
26
+ out.push({ text: "", width: 0 });
27
+ continue;
28
+ }
29
+ const firstWord = words[0];
30
+ if (firstWord === undefined) {
31
+ out.push({ text: "", width: 0 });
32
+ continue;
33
+ }
34
+ let current = firstWord;
35
+ let currentWidth = measure(current);
36
+ for (let i = 1; i < words.length; i++) {
37
+ const word = words[i];
38
+ if (word === undefined)
39
+ continue;
40
+ const candidate = `${current} ${word}`;
41
+ const w = measure(candidate);
42
+ if (w <= maxWidth) {
43
+ current = candidate;
44
+ currentWidth = w;
45
+ }
46
+ else {
47
+ out.push({ text: current, width: currentWidth });
48
+ current = word;
49
+ currentWidth = measure(word);
50
+ }
51
+ }
52
+ out.push({ text: current, width: currentWidth });
53
+ }
54
+ const lineHeight = options.fontSize * lineHeightFactor;
55
+ return { lines: out, lineHeight };
56
+ };
57
+ //# sourceMappingURL=text-layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-layout.js","sourceRoot":"","sources":["../src/text-layout.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import type { TextAlign, TextBaseline } from "./render-target.js";
2
+ /**
3
+ * Text-shaping abstraction. Lets a host swap the default
4
+ * `canvas.measureText`-based path for a richer engine (HarfBuzz /
5
+ * ICU4X / harfbuzzjs / canvaskit) when consistent server-side ↔
6
+ * browser layout matters or when batching many measurements is the
7
+ * hot path.
8
+ *
9
+ * The kernel ships a `Canvas2DTextShaper` implementation that delegates to
10
+ * `ctx.measureText`. Hosts that need deterministic cross-environment layout
11
+ * can implement this interface and pass it via `EditorOptions.textShaper`.
12
+ */
13
+ export interface TextShaper {
14
+ /**
15
+ * Measure a single line in the given font. Returns the advance
16
+ * width in CSS pixels. Hosts may also expose glyph runs for
17
+ * decoration positioning — the kernel only requires width today.
18
+ */
19
+ measure(text: string, font: ShaperFont): {
20
+ width: number;
21
+ };
22
+ /**
23
+ * Returns the glyph layout for richer renderers that want to draw the
24
+ * actual glyphs themselves. Not invoked by the built-in Canvas2D renderer.
25
+ */
26
+ shape?(text: string, font: ShaperFont): readonly ShapedGlyph[];
27
+ }
28
+ export interface ShaperFont {
29
+ readonly family: string;
30
+ readonly size: number;
31
+ readonly weight?: "normal" | "bold" | number;
32
+ readonly style?: "normal" | "italic";
33
+ }
34
+ export interface ShapedGlyph {
35
+ /** Glyph id in the resolved font (renderer-specific). */
36
+ readonly glyphId: number;
37
+ /** Advance width in CSS pixels. */
38
+ readonly advance: number;
39
+ /** X offset from the line origin. */
40
+ readonly x: number;
41
+ /** Y offset from the line baseline. */
42
+ readonly y: number;
43
+ }
44
+ export type { TextAlign, TextBaseline };
45
+ /**
46
+ * Install a process-global text shaper. Subsequent `getActiveTextShaper()`
47
+ * calls (used by the built-in `drawText` renderer) return it; passing `null`
48
+ * reverts to the Canvas2D `target.measureText` path. Idempotent — last write
49
+ * wins.
50
+ */
51
+ export declare const setActiveTextShaper: (shaper: TextShaper | null) => void;
52
+ export declare const getActiveTextShaper: () => TextShaper | null;
53
+ //# sourceMappingURL=text-shaper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-shaper.d.ts","sourceRoot":"","sources":["../src/text-shaper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,UAAU;IACzB;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAE3D;;;OAGG;IACH,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,SAAS,WAAW,EAAE,CAAC;CAChE;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;IAC7C,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;CACtC;AAED,MAAM,WAAW,WAAW;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mCAAmC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,qCAAqC;IACrC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAGD,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;AAYxC;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ,UAAU,GAAG,IAAI,KAAG,IAE/D,CAAC;AAEF,eAAO,MAAM,mBAAmB,QAAO,UAAU,GAAG,IAAoB,CAAC"}
@@ -0,0 +1,19 @@
1
+ // Process-global active shaper. `wrapText` accepts an optional `shaper`
2
+ // parameter, but the built-in `drawText` renderer in `built-in-renderers.ts`
3
+ // is an `ElementRenderer<TextElement>` whose signature is `(shape, target)`
4
+ // with no extra arg. Instead of threading a shaper through every
5
+ // ElementRenderer, a module-level registry that `drawText` consults at call
6
+ // time. Editor sets it via `setActiveTextShaper(options.textShaper)` on
7
+ // construction.
8
+ let activeShaper = null;
9
+ /**
10
+ * Install a process-global text shaper. Subsequent `getActiveTextShaper()`
11
+ * calls (used by the built-in `drawText` renderer) return it; passing `null`
12
+ * reverts to the Canvas2D `target.measureText` path. Idempotent — last write
13
+ * wins.
14
+ */
15
+ export const setActiveTextShaper = (shaper) => {
16
+ activeShaper = shaper;
17
+ };
18
+ export const getActiveTextShaper = () => activeShaper;
19
+ //# sourceMappingURL=text-shaper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-shaper.js","sourceRoot":"","sources":["../src/text-shaper.ts"],"names":[],"mappings":"AAiDA,wEAAwE;AACxE,6EAA6E;AAC7E,4EAA4E;AAC5E,iEAAiE;AACjE,4EAA4E;AAC5E,wEAAwE;AACxE,gBAAgB;AAEhB,IAAI,YAAY,GAAsB,IAAI,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAyB,EAAQ,EAAE;IACrE,YAAY,GAAG,MAAM,CAAC;AACxB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAsB,EAAE,CAAC,YAAY,CAAC"}
@@ -0,0 +1,126 @@
1
+ import type { Bounds, ElementId } from "@oh-just-another/types";
2
+ /**
3
+ * Tile-based rendering scaffold. The scene is split into a fixed-size
4
+ * world-space grid; each tile is rasterised once into an
5
+ * OffscreenCanvas / ImageBitmap and re-composited on pan without
6
+ * re-rendering. Zoom changes invalidate the bucket.
7
+ *
8
+ * This file ships the constants and cache interface so backends can
9
+ * adopt the API.
10
+ */
11
+ /** Side length of one tile in world units. */
12
+ export declare const TILE_SIZE = 2048;
13
+ /**
14
+ * Memory cap for cached tile bitmaps (bytes), adaptive per device.
15
+ *
16
+ * Formula: `min(256 MB, deviceMemoryGB × 64 MB)`.
17
+ * • iPhone Safari (no deviceMemory; A15 = 6 cores) → 2 GiB → 128 MB
18
+ * • Android low-RAM (deviceMemory=2) → 128 MB
19
+ * • Android mid-range (deviceMemory=4) → 256 MB
20
+ * • Desktop (deviceMemory=8 / hardwareConcurrency=8+) → 256 MB (cap)
21
+ * • Very low-end (deviceMemory=0.5) → 32 MB
22
+ *
23
+ * The adaptive cap caps the upper bound proportionally to device
24
+ * memory, leaving headroom for the rest of the JS heap.
25
+ *
26
+ * Evaluated once at module load — navigator state doesn't change during
27
+ * a session. Hosts that need a different cap can pass
28
+ * `new InMemoryTileCache(<custom bytes>)` directly.
29
+ */
30
+ export declare const MAX_TILE_CACHE_BYTES: number;
31
+ /**
32
+ * Below this zoom we drop to LOD: cache tiles render only AABBs
33
+ * (no stroke / text / detail) so 1 M+ shape scenes stay smooth.
34
+ */
35
+ export declare const LOD_THRESHOLD = 0.25;
36
+ export interface TileKey {
37
+ /** Tile column in world-space grid. */
38
+ readonly col: number;
39
+ /** Tile row in world-space grid. */
40
+ readonly row: number;
41
+ /** Zoom bucket (power of two). */
42
+ readonly zoom: number;
43
+ }
44
+ export interface TileCacheEntry<B = unknown> {
45
+ readonly key: TileKey;
46
+ readonly bitmap: B;
47
+ /** World bounds covered by this tile. */
48
+ readonly bounds: Bounds;
49
+ /** Bytes used by this bitmap (for LRU eviction accounting). */
50
+ readonly bytes: number;
51
+ /** Element ids currently visible in this tile (for invalidation). */
52
+ readonly elements: readonly ElementId[];
53
+ }
54
+ export interface TileCache<B = unknown> {
55
+ get(key: TileKey): TileCacheEntry<B> | undefined;
56
+ set(entry: TileCacheEntry<B>): void;
57
+ /**
58
+ * Drop any cached tile that contains the given shape id. Called
59
+ * by the editor when a shape's scene reference changes.
60
+ */
61
+ invalidateForElement(id: ElementId): void;
62
+ /**
63
+ * Drop any cached tile whose `bounds` intersect the given world
64
+ * rectangle. Used by the editor's patch hook when a shape is
65
+ * added (no previous id in the reverse index) or moved (the
66
+ * before/after AABB union covers both old and new tiles).
67
+ */
68
+ invalidateRect(rect: Bounds): void;
69
+ /**
70
+ * Convenience routing used by editor patch pipelines: dispatches
71
+ * to `invalidateForElement` (remove), `invalidateRect` (add), or
72
+ * both (move) based on which of `removedElementId` / `beforeBounds`
73
+ * / `afterBounds` are present.
74
+ */
75
+ invalidateForPatch(options: {
76
+ readonly removedElementId?: ElementId;
77
+ readonly beforeBounds?: Bounds;
78
+ readonly afterBounds?: Bounds;
79
+ }): void;
80
+ /** Total bytes currently held. */
81
+ readonly bytesUsed: number;
82
+ clear(): void;
83
+ }
84
+ /**
85
+ * Reference TileCache backed by a Map with LRU eviction by total
86
+ * bytes. Bitmap-agnostic — the eviction logic tracks the `bytes`
87
+ * field reported by the host; whatever stores ImageBitmap /
88
+ * OffscreenCanvas / SharedArrayBuffer is the host's call.
89
+ *
90
+ * Invalidation is shape-id-indexed: every `set` also updates a
91
+ * reverse index `Map<ElementId, Set<tileKey>>` so the editor's
92
+ * patch hook can call `invalidateForElement(id)` in O(touched tiles)
93
+ * instead of scanning every tile.
94
+ */
95
+ export declare class InMemoryTileCache<B = unknown> implements TileCache<B> {
96
+ private readonly entries;
97
+ private readonly tilesByElement;
98
+ private bytes;
99
+ private readonly cap;
100
+ constructor(byteCap?: number);
101
+ get bytesUsed(): number;
102
+ get(key: TileKey): TileCacheEntry<B> | undefined;
103
+ set(entry: TileCacheEntry<B>): void;
104
+ invalidateForElement(elementId: ElementId): void;
105
+ invalidateRect(rect: Bounds): void;
106
+ clear(): void;
107
+ /**
108
+ * Convenience hook for the editor's patch pipeline. Looks at a
109
+ * scene Patch and invalidates touched tiles via the right
110
+ * cheap path:
111
+ * • shape removed → invalidateForElement (id still in reverse index)
112
+ * • shape added → invalidateRect (no id yet — drop tiles that
113
+ * touch the new shape's bounds)
114
+ * • shape moved → both: removeForElement (old tiles) + rect
115
+ * (new bounds)
116
+ * Caller computes the bounds via `getElementWorldBounds(before|after)`
117
+ * since this package is pure and doesn't know about Element geometry.
118
+ */
119
+ invalidateForPatch(options: {
120
+ readonly removedElementId?: ElementId;
121
+ readonly beforeBounds?: Bounds;
122
+ readonly afterBounds?: Bounds;
123
+ }): void;
124
+ private evictIfOverCap;
125
+ }
126
+ //# sourceMappingURL=tile-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tile-renderer.d.ts","sourceRoot":"","sources":["../src/tile-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEhE;;;;;;;;GAQG;AAEH,8CAA8C;AAC9C,eAAO,MAAM,SAAS,OAAO,CAAC;AAkC9B;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,oBAAoB,QAGhC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,OAAO,CAAC;AAElC,MAAM,WAAW,OAAO;IACtB,uCAAuC;IACvC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,kCAAkC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,OAAO;IACzC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACnB,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAC;CACzC;AAED,MAAM,WAAW,SAAS,CAAC,CAAC,GAAG,OAAO;IACpC,GAAG,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IACjD,GAAG,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IACpC;;;OAGG;IACH,oBAAoB,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI,CAAC;IAC1C;;;;;OAKG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC;;;;;OAKG;IACH,kBAAkB,CAAC,OAAO,EAAE;QAC1B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,CAAC;QACtC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;KAC/B,GAAG,IAAI,CAAC;IACT,kCAAkC;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,KAAK,IAAI,IAAI,CAAC;CACf;AAID;;;;;;;;;;GAUG;AACH,qBAAa,iBAAiB,CAAC,CAAC,GAAG,OAAO,CAAE,YAAW,SAAS,CAAC,CAAC,CAAC;IACjE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwC;IAChE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqC;IACpE,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,OAAO,GAAE,MAA6B;IAIlD,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,GAAG,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAAC,CAAC,CAAC,GAAG,SAAS;IAUhD,GAAG,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,IAAI;IAoBnC,oBAAoB,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAehD,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAelC,KAAK,IAAI,IAAI;IAMb;;;;;;;;;;;OAWG;IACH,kBAAkB,CAChB,OAAO,EAAE;QACP,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,CAAC;QACtC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;KAC/B,GACA,IAAI;IAMP,OAAO,CAAC,cAAc;CAUvB"}