@vyaz/renderer 0.0.3 → 0.0.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.
@@ -1,284 +0,0 @@
1
- /**
2
- * interactive.ts — hit-testing and position utilities for Canvas editor.
3
- *
4
- * Provides functions to map pixel coordinates to character positions
5
- * within the layout output (Line[]), using Span.glyphAdvances for
6
- * per-character granularity.
7
- *
8
- * @see {@link https://www.w3.org/TR/css-text-3/ | CSS Text Module Level 3}
9
- */
10
-
11
- import type { Line, Span } from '@vyaz/core';
12
-
13
- // ── Types ──────────────────────────────────────────────────────────────────
14
-
15
- /**
16
- * A resolved character position within the layout tree.
17
- *
18
- * Contains enough context for cursor placement, selection start/end,
19
- * and hit-testing.
20
- */
21
- export interface CharPos {
22
- /** Index of the line in the lines array (0-based). */
23
- lineIndex: number;
24
- /** Index of the span within the line (0-based). */
25
- spanIndex: number;
26
- /** Index of the character within the span text (0-based). */
27
- charIndex: number;
28
- /** Paragraph index (from Span.pIdx). */
29
- pIdx: number;
30
- /** Absolute X position of the character's left edge (CSS px). */
31
- x: number;
32
- /** Absolute Y position of the character's baseline (CSS px). */
33
- y: number;
34
- /** Character advance width in px (from glyphAdvances or estimated). */
35
- width: number;
36
- /** Reference to the owning Line. */
37
- line: Line;
38
- /** Reference to the owning Span. */
39
- span: Span;
40
- }
41
-
42
- // ── Internal helpers ───────────────────────────────────────────────────────
43
-
44
- /**
45
- * Get the per-character advance widths for a span.
46
- * Uses `glyphAdvances` when available, otherwise falls back to
47
- * uniform distribution based on `span.width / span.text.length`.
48
- */
49
- function getCharAdvances(span: Span): number[] {
50
- if (span.glyphAdvances && span.glyphAdvances.length > 0) {
51
- return Array.from(span.glyphAdvances);
52
- }
53
- // Fallback: uniform distribution
54
- const len = span.text.length;
55
- if (len === 0) return [];
56
- const avgWidth = span.width / len;
57
- return new Array(len).fill(avgWidth);
58
- }
59
-
60
- /**
61
- * Build a flat list of character segments from all spans in a line.
62
- *
63
- * Each segment records the span index, char index, text char,
64
- * and absolute left-edge X position.
65
- */
66
- interface CharSegment {
67
- lineIndex: number;
68
- spanIndex: number;
69
- charIndex: number;
70
- pIdx: number;
71
- x: number;
72
- y: number;
73
- width: number;
74
- char: string;
75
- line: Line;
76
- span: Span;
77
- }
78
-
79
- function buildCharSegments(lines: Line[]): CharSegment[] {
80
- const segments: CharSegment[] = [];
81
-
82
- for (let li = 0; li < lines.length; li++) {
83
- const line = lines[li];
84
- const baselineY = line.y + line.baseline;
85
-
86
- for (let si = 0; si < line.spans.length; si++) {
87
- const span = line.spans[si];
88
- const advances = getCharAdvances(span);
89
- const text = span.text;
90
-
91
- let charX = line.x + span.x;
92
-
93
- for (let ci = 0; ci < text.length; ci++) {
94
- const advance = ci < advances.length ? advances[ci] : 0;
95
-
96
- segments.push({
97
- lineIndex: li,
98
- spanIndex: si,
99
- charIndex: ci,
100
- pIdx: span.pIdx,
101
- x: charX,
102
- y: baselineY,
103
- width: advance,
104
- char: text[ci],
105
- line,
106
- span,
107
- });
108
-
109
- charX += advance;
110
- }
111
- }
112
- }
113
-
114
- return segments;
115
- }
116
-
117
- // ── Public API ─────────────────────────────────────────────────────────────
118
-
119
- /**
120
- * Find the character position nearest to the given pixel coordinates.
121
- *
122
- * Uses a two-pass approach:
123
- * 1. Find the nearest line by Y distance (closest baseline).
124
- * 2. Within that line, find the nearest character by X distance
125
- * (using glyph advances or fallback uniform widths).
126
- *
127
- * Returns `null` if `lines` is empty.
128
- */
129
- export function charAtPoint(
130
- lines: Line[],
131
- px: number,
132
- py: number,
133
- ): CharPos | null {
134
- if (lines.length === 0) return null;
135
-
136
- // ── Pass 1: find nearest line ─────────────────────────────────────
137
- let nearestLineIdx = 0;
138
- let minYDist = Infinity;
139
-
140
- for (let li = 0; li < lines.length; li++) {
141
- const line = lines[li];
142
- const baselineY = line.y + line.baseline;
143
- const yDist = Math.abs(py - baselineY);
144
-
145
- // Consider line box vertical extent: if py is within [y, y+height],
146
- // it's an exact match (zero distance).
147
- const inLineBox = py >= line.y && py <= line.y + line.height;
148
- const distance = inLineBox ? 0 : yDist;
149
-
150
- if (distance < minYDist) {
151
- minYDist = distance;
152
- nearestLineIdx = li;
153
- }
154
- }
155
-
156
- const line = lines[nearestLineIdx];
157
- const baselineY = line.y + line.baseline;
158
-
159
- // ── Pass 2: find nearest character in the line ────────────────────
160
- // Build char segments for this line only
161
- let nearestSpanIdx = 0;
162
- let nearestCharIdx = 0;
163
- let nearestX = line.x;
164
- let nearestWidth = 0;
165
- let nearestSpan = line.spans[0];
166
- let minXDist = Infinity;
167
-
168
- for (let si = 0; si < line.spans.length; si++) {
169
- const span = line.spans[si];
170
- const advances = getCharAdvances(span);
171
- const text = span.text;
172
-
173
- let charX = line.x + span.x;
174
-
175
- for (let ci = 0; ci < text.length; ci++) {
176
- const advance = ci < advances.length ? advances[ci] : 0;
177
- const charCenter = charX + advance / 2;
178
- const xDist = Math.abs(px - charCenter);
179
-
180
- if (xDist < minXDist) {
181
- minXDist = xDist;
182
- nearestSpanIdx = si;
183
- nearestCharIdx = ci;
184
- nearestX = charX;
185
- nearestWidth = advance;
186
- nearestSpan = span;
187
- }
188
-
189
- charX += advance;
190
- }
191
- }
192
-
193
- return {
194
- lineIndex: nearestLineIdx,
195
- spanIndex: nearestSpanIdx,
196
- charIndex: nearestCharIdx,
197
- pIdx: nearestSpan.pIdx,
198
- x: nearestX,
199
- y: baselineY,
200
- width: nearestWidth,
201
- line,
202
- span: nearestSpan,
203
- };
204
- }
205
-
206
- /**
207
- * Convert a global character index (startIndex / endIndex from Line)
208
- * to a `CharPos`.
209
- *
210
- * Useful for mapping cursor/selection from a text model to layout position.
211
- *
212
- * Returns `null` if the index is out of range.
213
- */
214
- export function charIndexToPos(
215
- lines: Line[],
216
- charIndex: number,
217
- ): CharPos | null {
218
- const segments = buildCharSegments(lines);
219
-
220
- // Find the segment at or after the given index
221
- let globalIdx = 0;
222
- for (const seg of segments) {
223
- if (globalIdx === charIndex) {
224
- return {
225
- lineIndex: seg.lineIndex,
226
- spanIndex: seg.spanIndex,
227
- charIndex: seg.charIndex,
228
- pIdx: seg.pIdx,
229
- x: seg.x,
230
- y: seg.y,
231
- width: seg.width,
232
- line: seg.line,
233
- span: seg.span,
234
- };
235
- }
236
- globalIdx++;
237
- }
238
-
239
- // If charIndex is at the end (after last char), return the last position
240
- if (charIndex >= globalIdx && segments.length > 0) {
241
- const last = segments[segments.length - 1];
242
- return {
243
- lineIndex: last.lineIndex,
244
- spanIndex: last.spanIndex,
245
- charIndex: last.charIndex + 1,
246
- pIdx: last.pIdx,
247
- x: last.x + last.width,
248
- y: last.y,
249
- width: 0,
250
- line: last.line,
251
- span: last.span,
252
- };
253
- }
254
-
255
- return null;
256
- }
257
-
258
- /**
259
- * Compute the global character index from a CharPos.
260
- *
261
- * This walks all preceding lines/spans/characters to compute
262
- * the absolute index in the full text.
263
- */
264
- export function posToCharIndex(lines: Line[], pos: CharPos): number {
265
- let index = 0;
266
-
267
- for (let li = 0; li < pos.lineIndex; li++) {
268
- const line = lines[li];
269
- for (const span of line.spans) {
270
- index += span.text.length;
271
- }
272
- }
273
-
274
- // Add characters in current line up to the target span
275
- const line = lines[pos.lineIndex];
276
- for (let si = 0; si < pos.spanIndex; si++) {
277
- index += line.spans[si].text.length;
278
- }
279
-
280
- // Add characters within the target span
281
- index += pos.charIndex;
282
-
283
- return index;
284
- }
package/src/types.ts DELETED
@@ -1,69 +0,0 @@
1
- /**
2
- * Render types shared across all renderers (SVG, Canvas, etc.).
3
- */
4
-
5
- export interface DebugFlags {
6
- /** Frame container bounding box (frameWidth × frameHeight) */
7
- frameBox?: boolean;
8
- /** Actual content bounding box (BBox of all lines) */
9
- contentBox?: boolean;
10
- /** Paragraph bounding boxes (grouped by paragraphId). */
11
- paragraphBox?: boolean;
12
- /** Stroke width (px) for all debug border lines. Default 1. */
13
- widthBorder?: number;
14
- /** Show filled rect for each line's lineHeight (background fill). */
15
- lineGap?: boolean;
16
- /** Line box outline */
17
- box?: boolean;
18
- /** Baseline line */
19
- baseline?: boolean;
20
- /** Ascent/descent lines */
21
- ascentDescent?: boolean;
22
- /** Coordinate labels */
23
- labels?: boolean;
24
- /** Run rectangles */
25
- runs?: boolean;
26
- /** Column separators (only rendered when multi-column config is set). */
27
- columnBox?: boolean;
28
- /** @deprecated Use frameBox instead */
29
- frame?: boolean;
30
- }
31
-
32
- // ── SVG AST types ────────────────────────────────────────────────────────
33
-
34
- /**
35
- * A generic SVG element node in the AST.
36
- */
37
- export interface SvgElement {
38
- type: 'element';
39
- tag: string;
40
- attrs: Record<string, string | number | undefined>;
41
- children: SvgNode[];
42
- }
43
-
44
- /**
45
- * A text node inside an SVG element (escaped on serialization).
46
- */
47
- export interface SvgTextNode {
48
- type: 'text';
49
- value: string;
50
- }
51
-
52
- /**
53
- * A raw content node — output verbatim without escaping.
54
- * Used for pre-rendered debug overlay markup.
55
- */
56
- export interface SvgRawNode {
57
- type: 'raw';
58
- value: string;
59
- }
60
-
61
- /**
62
- * An SVG comment node.
63
- */
64
- export interface SvgCommentNode {
65
- type: 'comment';
66
- value: string;
67
- }
68
-
69
- export type SvgNode = SvgElement | SvgTextNode | SvgRawNode | SvgCommentNode;
package/src/utils.ts DELETED
@@ -1,28 +0,0 @@
1
- /**
2
- * render/utils.ts — shared renderer utilities.
3
- */
4
-
5
- import type { Line } from '@vyaz/core';
6
-
7
- /**
8
- * Format a number for SVG output with fixed precision.
9
- * Prevents subpixel noise from creating false snapshot diffs.
10
- *
11
- * @param n — number to format
12
- * @param precision — decimal places (default 2)
13
- */
14
- export function fmt(n: number, precision = 2): string {
15
- return (Math.round(n * 10 ** precision) / 10 ** precision).toString();
16
- }
17
-
18
- /**
19
- * Compute the bounding box (content width + height, min x/y) from an array of Line.
20
- */
21
- export function computeBBox(lines: Line[]): { x: number; y: number; width: number; height: number } {
22
- if (lines.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
23
- const minX = Math.min(...lines.map(l => l.x));
24
- const maxX = Math.max(...lines.map(l => l.x + l.width));
25
- const minY = Math.min(...lines.map(l => l.y));
26
- const maxY = lines[lines.length - 1].y + lines[lines.length - 1].height;
27
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
28
- }