@guildai/cli 0.7.1 → 0.8.1

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,858 +0,0 @@
1
- // Copyright 2026 Guild.ai
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * SVG to Braille Canvas Renderer
5
- *
6
- * Renders SVG paths to terminal using braille characters.
7
- * Each braille character represents a 2x4 pixel grid.
8
- *
9
- * Braille dot positions:
10
- * ┌───┬───┐
11
- * │ 1 │ 4 │
12
- * │ 2 │ 5 │
13
- * │ 3 │ 6 │
14
- * │ 7 │ 8 │
15
- * └───┴───┘
16
- */
17
- import { SVGPathData } from 'svg-pathdata';
18
- import * as fs from 'fs';
19
- import * as path from 'path';
20
- import { fileURLToPath } from 'url';
21
- // ESM equivalent of __dirname
22
- const __filename = fileURLToPath(import.meta.url);
23
- const __dirname = path.dirname(__filename);
24
- // Braille Unicode block starts at U+2800
25
- const BRAILLE_BASE = 0x2800;
26
- // Dot bit positions in the braille character
27
- // Left column: bits 0,1,2,6 (positions 1,2,3,7)
28
- // Right column: bits 3,4,5,7 (positions 4,5,6,8)
29
- const DOT_BITS = [
30
- [0x01, 0x08], // Row 0: dots 1,4
31
- [0x02, 0x10], // Row 1: dots 2,5
32
- [0x04, 0x20], // Row 2: dots 3,6
33
- [0x40, 0x80], // Row 3: dots 7,8
34
- ];
35
- /**
36
- * Create an identity transform matrix
37
- */
38
- function identityMatrix() {
39
- return { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
40
- }
41
- /**
42
- * Multiply two transform matrices
43
- */
44
- function multiplyMatrices(m1, m2) {
45
- return {
46
- a: m1.a * m2.a + m1.c * m2.b,
47
- b: m1.b * m2.a + m1.d * m2.b,
48
- c: m1.a * m2.c + m1.c * m2.d,
49
- d: m1.b * m2.c + m1.d * m2.d,
50
- e: m1.a * m2.e + m1.c * m2.f + m1.e,
51
- f: m1.b * m2.e + m1.d * m2.f + m1.f,
52
- };
53
- }
54
- /**
55
- * Apply transform matrix to a point
56
- */
57
- export function transformPoint(x, y, m) {
58
- return [m.a * x + m.c * y + m.e, m.b * x + m.d * y + m.f];
59
- }
60
- /**
61
- * Parse SVG transform string into a matrix
62
- * Handles: translate, scale, rotate, matrix
63
- */
64
- function parseTransform(transform) {
65
- let result = identityMatrix();
66
- // Match each transform function
67
- const regex = /(translate|scale|rotate|matrix)\s*\(([^)]+)\)/gi;
68
- let match;
69
- while ((match = regex.exec(transform)) !== null) {
70
- const type = match[1].toLowerCase();
71
- const args = match[2]
72
- .split(/[\s,]+/)
73
- .filter((s) => s.length > 0)
74
- .map(Number);
75
- let m;
76
- switch (type) {
77
- case 'translate':
78
- m = { a: 1, b: 0, c: 0, d: 1, e: args[0] || 0, f: args[1] || 0 };
79
- break;
80
- case 'scale':
81
- m = { a: args[0] || 1, b: 0, c: 0, d: args[1] ?? args[0] ?? 1, e: 0, f: 0 };
82
- break;
83
- case 'rotate': {
84
- const angle = ((args[0] || 0) * Math.PI) / 180;
85
- const cos = Math.cos(angle);
86
- const sin = Math.sin(angle);
87
- m = { a: cos, b: sin, c: -sin, d: cos, e: 0, f: 0 };
88
- break;
89
- }
90
- case 'matrix':
91
- m = {
92
- a: args[0] || 1,
93
- b: args[1] || 0,
94
- c: args[2] || 0,
95
- d: args[3] || 1,
96
- e: args[4] || 0,
97
- f: args[5] || 0,
98
- };
99
- break;
100
- default:
101
- continue;
102
- }
103
- result = multiplyMatrices(result, m);
104
- }
105
- return result;
106
- }
107
- /**
108
- * Create an empty braille canvas
109
- */
110
- export function createCanvas(widthChars, heightChars) {
111
- const pixelWidth = widthChars * 2;
112
- const pixelHeight = heightChars * 4;
113
- const pixels = [];
114
- for (let y = 0; y < pixelHeight; y++) {
115
- pixels.push(new Array(pixelWidth).fill(false));
116
- }
117
- return { width: widthChars, height: heightChars, pixels };
118
- }
119
- /**
120
- * Set a pixel in the canvas
121
- */
122
- export function setPixel(canvas, x, y, value = true) {
123
- const px = Math.floor(x);
124
- const py = Math.floor(y);
125
- if (px >= 0 && px < canvas.width * 2 && py >= 0 && py < canvas.height * 4) {
126
- canvas.pixels[py][px] = value;
127
- }
128
- }
129
- /**
130
- * Draw a line using Bresenham's algorithm
131
- */
132
- export function drawLine(canvas, x0, y0, x1, y1) {
133
- x0 = Math.round(x0);
134
- y0 = Math.round(y0);
135
- x1 = Math.round(x1);
136
- y1 = Math.round(y1);
137
- const dx = Math.abs(x1 - x0);
138
- const dy = Math.abs(y1 - y0);
139
- const sx = x0 < x1 ? 1 : -1;
140
- const sy = y0 < y1 ? 1 : -1;
141
- let err = dx - dy;
142
- let x = x0;
143
- let y = y0;
144
- while (true) {
145
- setPixel(canvas, x, y);
146
- if (x === x1 && y === y1)
147
- break;
148
- const e2 = 2 * err;
149
- if (e2 > -dy) {
150
- err -= dy;
151
- x += sx;
152
- }
153
- if (e2 < dx) {
154
- err += dx;
155
- y += sy;
156
- }
157
- }
158
- }
159
- /**
160
- * Draw a cubic bezier curve
161
- */
162
- export function drawCubicBezier(canvas, x0, y0, x1, y1, x2, y2, x3, y3, steps = 20) {
163
- let lastX = x0;
164
- let lastY = y0;
165
- for (let i = 1; i <= steps; i++) {
166
- const t = i / steps;
167
- const mt = 1 - t;
168
- const mt2 = mt * mt;
169
- const mt3 = mt2 * mt;
170
- const t2 = t * t;
171
- const t3 = t2 * t;
172
- const x = mt3 * x0 + 3 * mt2 * t * x1 + 3 * mt * t2 * x2 + t3 * x3;
173
- const y = mt3 * y0 + 3 * mt2 * t * y1 + 3 * mt * t2 * y2 + t3 * y3;
174
- drawLine(canvas, lastX, lastY, x, y);
175
- lastX = x;
176
- lastY = y;
177
- }
178
- }
179
- /**
180
- * Draw a quadratic bezier curve
181
- */
182
- export function drawQuadraticBezier(canvas, x0, y0, x1, y1, x2, y2, steps = 20) {
183
- let lastX = x0;
184
- let lastY = y0;
185
- for (let i = 1; i <= steps; i++) {
186
- const t = i / steps;
187
- const mt = 1 - t;
188
- const mt2 = mt * mt;
189
- const t2 = t * t;
190
- const x = mt2 * x0 + 2 * mt * t * x1 + t2 * x2;
191
- const y = mt2 * y0 + 2 * mt * t * y1 + t2 * y2;
192
- drawLine(canvas, lastX, lastY, x, y);
193
- lastX = x;
194
- lastY = y;
195
- }
196
- }
197
- /**
198
- * Fill a closed path using scanline algorithm
199
- */
200
- export function fillPath(canvas, points, density = 1.0) {
201
- if (points.length < 3)
202
- return;
203
- // Find bounding box
204
- let minY = Infinity;
205
- let maxY = -Infinity;
206
- for (const [, y] of points) {
207
- minY = Math.min(minY, y);
208
- maxY = Math.max(maxY, y);
209
- }
210
- minY = Math.max(0, Math.floor(minY));
211
- maxY = Math.min(canvas.height * 4 - 1, Math.ceil(maxY));
212
- // Scanline fill
213
- for (let y = minY; y <= maxY; y++) {
214
- const intersections = [];
215
- // Find intersections with all edges
216
- for (let i = 0; i < points.length; i++) {
217
- const [x1, y1] = points[i];
218
- const [x2, y2] = points[(i + 1) % points.length];
219
- if ((y1 <= y && y2 > y) || (y2 <= y && y1 > y)) {
220
- const x = x1 + ((y - y1) / (y2 - y1)) * (x2 - x1);
221
- intersections.push(x);
222
- }
223
- }
224
- // Sort and fill between pairs
225
- intersections.sort((a, b) => a - b);
226
- for (let i = 0; i < intersections.length; i += 2) {
227
- if (i + 1 < intersections.length) {
228
- const startX = Math.max(0, Math.floor(intersections[i]));
229
- const endX = Math.min(canvas.width * 2 - 1, Math.ceil(intersections[i + 1]));
230
- // Apply density-based sparse sampling
231
- if (density >= 1.0) {
232
- // Full density - render every pixel
233
- for (let x = startX; x <= endX; x++) {
234
- setPixel(canvas, x, y);
235
- }
236
- }
237
- else {
238
- // Sparse sampling using ordered dithering (Bayer-style pattern)
239
- // This creates a more uniform distribution than random sampling
240
- // and preserves detail better than simple checkerboard
241
- for (let x = startX; x <= endX; x++) {
242
- // 2x2 Bayer matrix pattern for ordered dithering
243
- // Values: 0.0, 0.5, 0.75, 0.25 in a 2x2 pattern
244
- const bx = x % 2;
245
- const by = y % 2;
246
- let threshold;
247
- if (bx === 0 && by === 0)
248
- threshold = 0.0;
249
- else if (bx === 1 && by === 0)
250
- threshold = 0.5;
251
- else if (bx === 0 && by === 1)
252
- threshold = 0.75;
253
- else
254
- threshold = 0.25;
255
- // Render pixel if density is higher than threshold
256
- // density=1.0 renders all (all thresholds < 1.0)
257
- // density=0.75 renders 75% (thresholds 0.0, 0.5, 0.25 < 0.75)
258
- // density=0.5 renders 50% (thresholds 0.0, 0.25 < 0.5)
259
- if (density > threshold) {
260
- setPixel(canvas, x, y);
261
- }
262
- }
263
- }
264
- }
265
- }
266
- }
267
- }
268
- /**
269
- * Extract clipPath rectangles from SVG defs
270
- * Lottie mask animations become clipPaths with path geometry
271
- * We extract their bounding rectangles for clipping
272
- */
273
- function extractClipPaths(svgContent) {
274
- const clipPaths = new Map();
275
- // Find all clipPath definitions
276
- const clipPathRegex = /<clipPath id="([^"]+)"[^>]*>([\s\S]*?)<\/clipPath>/g;
277
- let match;
278
- while ((match = clipPathRegex.exec(svgContent)) !== null) {
279
- const id = match[1];
280
- const content = match[2];
281
- // Extract path data
282
- const pathMatch = content.match(/d="([^"]+)"/);
283
- if (pathMatch) {
284
- const d = pathMatch[1];
285
- // Extract all coordinates from the path data
286
- // Match patterns like: 123.45 or -123.45
287
- const coords = [];
288
- const coordRegex = /-?\d+(?:\.\d+)?/g;
289
- let coordMatch;
290
- while ((coordMatch = coordRegex.exec(d)) !== null) {
291
- coords.push(parseFloat(coordMatch[0]));
292
- }
293
- if (coords.length >= 4) {
294
- // Get bounding box from all coordinates
295
- const xCoords = coords.filter((_, i) => i % 2 === 0); // Even indices are X
296
- const yCoords = coords.filter((_, i) => i % 2 === 1); // Odd indices are Y
297
- clipPaths.set(id, {
298
- minX: Math.min(...xCoords),
299
- minY: Math.min(...yCoords),
300
- maxX: Math.max(...xCoords),
301
- maxY: Math.max(...yCoords),
302
- });
303
- }
304
- }
305
- }
306
- return clipPaths;
307
- }
308
- /**
309
- * Parse SVG hierarchically to match each path with its correct transform chain
310
- */
311
- export function parseHierarchicalPaths(svgContent) {
312
- const paths = [];
313
- const transformStack = [identityMatrix()];
314
- const visibilityStack = [true]; // Track visibility state
315
- // Extract all clipPath rectangles
316
- const clipPathRects = extractClipPaths(svgContent);
317
- // Track current clipPath (stack for nested groups)
318
- const clipPathStack = [undefined];
319
- // Track if we're inside <defs> to skip those paths
320
- let inDefs = false;
321
- // Use regex to find all <defs>, </defs>, <g>, </g>, and <path> tags in order
322
- const tagRegex = /<(\/?)(?:defs|g|path)([^>]*)>/g;
323
- let match;
324
- while ((match = tagRegex.exec(svgContent)) !== null) {
325
- const isClosing = match[1] === '/';
326
- const fullTag = match[0];
327
- let tagName = 'unknown';
328
- if (fullTag.startsWith('<defs'))
329
- tagName = 'defs';
330
- else if (fullTag.startsWith('</defs'))
331
- tagName = 'defs';
332
- else if (fullTag.startsWith('<g'))
333
- tagName = 'g';
334
- else if (fullTag.startsWith('</g'))
335
- tagName = 'g';
336
- else if (fullTag.startsWith('<path'))
337
- tagName = 'path';
338
- const attributes = match[2];
339
- // Track <defs> sections
340
- if (tagName === 'defs') {
341
- inDefs = !isClosing;
342
- continue;
343
- }
344
- // Skip everything inside <defs>
345
- if (inDefs)
346
- continue;
347
- if (tagName === 'g' && !isClosing) {
348
- // Opening <g> tag - check visibility and transform
349
- const hasDisplayNone = attributes.includes('display: none');
350
- // Check for opacity="0" which also hides elements
351
- const opacityMatch = attributes.match(/opacity="([^"]+)"/);
352
- const hasZeroOpacity = opacityMatch && parseFloat(opacityMatch[1]) === 0;
353
- const isVisible = !hasDisplayNone && !hasZeroOpacity;
354
- const currentVisibility = visibilityStack[visibilityStack.length - 1];
355
- // Group is visible only if parent is visible AND this group isn't hidden
356
- visibilityStack.push(currentVisibility && isVisible);
357
- // Check for clip-path attribute
358
- const clipPathMatch = attributes.match(/clip-path="url\(#([^)]+)\)"/);
359
- let groupClipRect;
360
- if (clipPathMatch) {
361
- const clipPathId = clipPathMatch[1];
362
- groupClipRect = clipPathRects.get(clipPathId);
363
- }
364
- // Inherit parent's clipRect if we don't have our own
365
- if (!groupClipRect) {
366
- groupClipRect = clipPathStack[clipPathStack.length - 1];
367
- }
368
- clipPathStack.push(groupClipRect);
369
- // Extract transform and push onto stack
370
- const transformMatch = attributes.match(/transform="([^"]+)"/);
371
- if (transformMatch) {
372
- const transform = parseTransform(transformMatch[1]);
373
- const currentTransform = transformStack[transformStack.length - 1];
374
- const newTransform = multiplyMatrices(currentTransform, transform);
375
- transformStack.push(newTransform);
376
- }
377
- else {
378
- // No transform attribute - duplicate current transform
379
- transformStack.push(transformStack[transformStack.length - 1]);
380
- }
381
- }
382
- else if (tagName === 'g' && isClosing) {
383
- // Closing </g> tag - pop from stacks
384
- if (transformStack.length > 1) {
385
- transformStack.pop();
386
- visibilityStack.pop();
387
- clipPathStack.pop();
388
- }
389
- }
390
- else if (tagName === 'path') {
391
- // <path> tag - only record if currently visible
392
- const currentlyVisible = visibilityStack[visibilityStack.length - 1];
393
- if (currentlyVisible) {
394
- const pathDataMatch = attributes.match(/d="([^"]+)"/);
395
- if (pathDataMatch) {
396
- // Extract fill color
397
- const fillMatch = attributes.match(/fill="([^"]+)"/);
398
- paths.push({
399
- pathData: pathDataMatch[1],
400
- transform: transformStack[transformStack.length - 1],
401
- clipRect: clipPathStack[clipPathStack.length - 1],
402
- fill: fillMatch ? fillMatch[1] : undefined,
403
- });
404
- }
405
- }
406
- }
407
- }
408
- return paths;
409
- }
410
- /**
411
- * Parse SVG content and extract path points with transforms applied
412
- */
413
- export function extractPathPoints(svgContent, targetWidth, targetHeight, rotation = 0, autoFit = false, explicitBounds) {
414
- // Extract viewBox
415
- const viewBoxMatch = svgContent.match(/viewBox="([^"]+)"/);
416
- let viewBoxX = 0;
417
- let viewBoxY = 0;
418
- let viewBoxWidth = 100;
419
- let viewBoxHeight = 100;
420
- if (viewBoxMatch) {
421
- const parts = viewBoxMatch[1].split(/\s+/).map(Number);
422
- viewBoxX = parts[0] || 0;
423
- viewBoxY = parts[1] || 0;
424
- viewBoxWidth = parts[2] || 100;
425
- viewBoxHeight = parts[3] || 100;
426
- }
427
- // Parse SVG hierarchically to get paths with their correct transforms
428
- const parsedPaths = parseHierarchicalPaths(svgContent);
429
- const pixelWidth = targetWidth * 2;
430
- const pixelHeight = targetHeight * 4;
431
- // Determine bounds to use (explicit > autoFit > viewBox)
432
- let boundsMinX;
433
- let boundsMinY;
434
- let boundsMaxX;
435
- let boundsMaxY;
436
- if (explicitBounds) {
437
- // Use explicit bounds provided by caller
438
- boundsMinX = explicitBounds.minX;
439
- boundsMinY = explicitBounds.minY;
440
- boundsMaxX = explicitBounds.maxX;
441
- boundsMaxY = explicitBounds.maxY;
442
- }
443
- else if (autoFit) {
444
- // Calculate bounds from actual path content WITH CORRECT TRANSFORMS
445
- boundsMinX = viewBoxX;
446
- boundsMinY = viewBoxY;
447
- boundsMaxX = viewBoxX + viewBoxWidth;
448
- boundsMaxY = viewBoxY + viewBoxHeight;
449
- let hasPoints = false;
450
- for (const parsedPath of parsedPaths) {
451
- try {
452
- const pathData = new SVGPathData(parsedPath.pathData).toAbs();
453
- const commands = pathData.commands;
454
- for (const cmd of commands) {
455
- // Check all coordinate types
456
- const coords = [];
457
- if ('x' in cmd && 'y' in cmd) {
458
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
459
- coords.push([cmd.x, cmd.y]);
460
- }
461
- if ('x1' in cmd && 'y1' in cmd) {
462
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
463
- coords.push([cmd.x1, cmd.y1]);
464
- }
465
- if ('x2' in cmd && 'y2' in cmd) {
466
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
467
- coords.push([cmd.x2, cmd.y2]);
468
- }
469
- // Apply THIS path's specific transform (not all transforms!)
470
- for (const [x, y] of coords) {
471
- const [tx, ty] = transformPoint(x, y, parsedPath.transform);
472
- if (!hasPoints) {
473
- boundsMinX = boundsMaxX = tx;
474
- boundsMinY = boundsMaxY = ty;
475
- hasPoints = true;
476
- }
477
- else {
478
- boundsMinX = Math.min(boundsMinX, tx);
479
- boundsMinY = Math.min(boundsMinY, ty);
480
- boundsMaxX = Math.max(boundsMaxX, tx);
481
- boundsMaxY = Math.max(boundsMaxY, ty);
482
- }
483
- }
484
- }
485
- }
486
- catch {
487
- // Skip invalid paths
488
- }
489
- }
490
- }
491
- else {
492
- // Use viewBox bounds
493
- boundsMinX = viewBoxX;
494
- boundsMinY = viewBoxY;
495
- boundsMaxX = viewBoxX + viewBoxWidth;
496
- boundsMaxY = viewBoxY + viewBoxHeight;
497
- }
498
- // Calculate dimensions based on bounds
499
- const boundsWidth = boundsMaxX - boundsMinX;
500
- const boundsHeight = boundsMaxY - boundsMinY;
501
- // Calculate scale to fit into target pixel dimensions
502
- const scaleX = pixelWidth / boundsWidth;
503
- const scaleY = pixelHeight / boundsHeight;
504
- const scale = Math.min(scaleX, scaleY) * 0.9;
505
- // Center offset (accounting for bounds offset)
506
- const offsetX = (pixelWidth - boundsWidth * scale) / 2 - boundsMinX * scale;
507
- const offsetY = (pixelHeight - boundsHeight * scale) / 2 - boundsMinY * scale;
508
- // Apply rotation if specified (rotate around center of bounds)
509
- let rotationTransform = identityMatrix();
510
- if (rotation !== 0) {
511
- const angle = (rotation * Math.PI) / 180;
512
- const cos = Math.cos(angle);
513
- const sin = Math.sin(angle);
514
- const centerX = (boundsMinX + boundsMaxX) / 2;
515
- const centerY = (boundsMinY + boundsMaxY) / 2;
516
- // Translate to origin, rotate, translate back
517
- rotationTransform = multiplyMatrices({ a: 1, b: 0, c: 0, d: 1, e: centerX, f: centerY }, multiplyMatrices({ a: cos, b: sin, c: -sin, d: cos, e: 0, f: 0 }, { a: 1, b: 0, c: 0, d: 1, e: -centerX, f: -centerY }));
518
- }
519
- // Viewport transform: rotation -> scale to fit -> center
520
- const viewportTransform = multiplyMatrices({ a: scale, b: 0, c: 0, d: scale, e: offsetX, f: offsetY }, rotationTransform);
521
- // Render each path with its individual transform chain
522
- const allPaths = [];
523
- // SVG viewBox bounds for clipping (lottie uses clip-path to enforce this)
524
- const clipMinX = viewBoxX;
525
- const clipMinY = viewBoxY;
526
- const clipMaxX = viewBoxX + viewBoxWidth;
527
- const clipMaxY = viewBoxY + viewBoxHeight;
528
- for (const parsedPath of parsedPaths) {
529
- try {
530
- // Skip white fills (these are used for negative space in logos)
531
- // Check for common white color formats: "#ffffff", "#fff", "rgb(255,255,255)", "white"
532
- const fill = parsedPath.fill?.toLowerCase();
533
- if (fill === '#ffffff' ||
534
- fill === '#fff' ||
535
- fill === 'rgb(255,255,255)' ||
536
- fill === 'white' ||
537
- fill === 'rgba(255,255,255,1)') {
538
- continue; // Skip white fills
539
- }
540
- const pathData = new SVGPathData(parsedPath.pathData).toAbs();
541
- // First, check if this path is within the viewBox clip bounds
542
- // Apply the path's transform to check bounds before viewport scaling
543
- let pathBoundsMinX = Infinity;
544
- let pathBoundsMinY = Infinity;
545
- let pathBoundsMaxX = -Infinity;
546
- let pathBoundsMaxY = -Infinity;
547
- pathData.commands.forEach((cmd) => {
548
- if ('x' in cmd && 'y' in cmd) {
549
- const [tx, ty] = transformPoint(
550
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
551
- cmd.x,
552
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
553
- cmd.y, parsedPath.transform);
554
- pathBoundsMinX = Math.min(pathBoundsMinX, tx);
555
- pathBoundsMinY = Math.min(pathBoundsMinY, ty);
556
- pathBoundsMaxX = Math.max(pathBoundsMaxX, tx);
557
- pathBoundsMaxY = Math.max(pathBoundsMaxY, ty);
558
- }
559
- });
560
- // Skip paths that are completely outside the clip bounds
561
- if (pathBoundsMaxX < clipMinX ||
562
- pathBoundsMinX > clipMaxX ||
563
- pathBoundsMaxY < clipMinY ||
564
- pathBoundsMinY > clipMaxY) {
565
- continue; // Path is completely clipped out
566
- }
567
- // Final transform: path's SVG transform -> viewport transform
568
- const finalTransform = multiplyMatrices(viewportTransform, parsedPath.transform);
569
- const commands = pathData.commands;
570
- const pathPoints = [];
571
- let currentX = 0;
572
- let currentY = 0;
573
- let startX = 0;
574
- let startY = 0;
575
- for (const cmd of commands) {
576
- switch (cmd.type) {
577
- case SVGPathData.MOVE_TO: {
578
- const [tx, ty] = transformPoint(cmd.x, cmd.y, finalTransform);
579
- currentX = tx;
580
- currentY = ty;
581
- startX = tx;
582
- startY = ty;
583
- pathPoints.push([tx, ty]);
584
- break;
585
- }
586
- case SVGPathData.LINE_TO: {
587
- const [tx, ty] = transformPoint(cmd.x, cmd.y, finalTransform);
588
- // Add intermediate points for better rendering
589
- const steps = Math.max(1, Math.ceil(Math.hypot(tx - currentX, ty - currentY) / 2));
590
- for (let i = 1; i <= steps; i++) {
591
- const t = i / steps;
592
- pathPoints.push([
593
- currentX + (tx - currentX) * t,
594
- currentY + (ty - currentY) * t,
595
- ]);
596
- }
597
- currentX = tx;
598
- currentY = ty;
599
- break;
600
- }
601
- case SVGPathData.HORIZ_LINE_TO: {
602
- const [tx] = transformPoint(cmd.x, currentY / finalTransform.d, finalTransform);
603
- const steps = Math.max(1, Math.ceil(Math.abs(tx - currentX) / 2));
604
- for (let i = 1; i <= steps; i++) {
605
- const t = i / steps;
606
- pathPoints.push([currentX + (tx - currentX) * t, currentY]);
607
- }
608
- currentX = tx;
609
- break;
610
- }
611
- case SVGPathData.VERT_LINE_TO: {
612
- const [, ty] = transformPoint(currentX / finalTransform.a, cmd.y, finalTransform);
613
- const steps = Math.max(1, Math.ceil(Math.abs(ty - currentY) / 2));
614
- for (let i = 1; i <= steps; i++) {
615
- const t = i / steps;
616
- pathPoints.push([currentX, currentY + (ty - currentY) * t]);
617
- }
618
- currentY = ty;
619
- break;
620
- }
621
- case SVGPathData.CURVE_TO: {
622
- const [x1t, y1t] = transformPoint(cmd.x1, cmd.y1, finalTransform);
623
- const [x2t, y2t] = transformPoint(cmd.x2, cmd.y2, finalTransform);
624
- const [xt, yt] = transformPoint(cmd.x, cmd.y, finalTransform);
625
- // Sample bezier curve
626
- const steps = 20;
627
- for (let i = 1; i <= steps; i++) {
628
- const t = i / steps;
629
- const mt = 1 - t;
630
- const mt2 = mt * mt;
631
- const mt3 = mt2 * mt;
632
- const t2 = t * t;
633
- const t3 = t2 * t;
634
- const x = mt3 * currentX + 3 * mt2 * t * x1t + 3 * mt * t2 * x2t + t3 * xt;
635
- const y = mt3 * currentY + 3 * mt2 * t * y1t + 3 * mt * t2 * y2t + t3 * yt;
636
- pathPoints.push([x, y]);
637
- }
638
- currentX = xt;
639
- currentY = yt;
640
- break;
641
- }
642
- case SVGPathData.SMOOTH_CURVE_TO: {
643
- const [x2t, y2t] = transformPoint(cmd.x2, cmd.y2, finalTransform);
644
- const [xt, yt] = transformPoint(cmd.x, cmd.y, finalTransform);
645
- const steps = 20;
646
- for (let i = 1; i <= steps; i++) {
647
- const t = i / steps;
648
- const mt = 1 - t;
649
- const mt2 = mt * mt;
650
- const mt3 = mt2 * mt;
651
- const t2 = t * t;
652
- const t3 = t2 * t;
653
- const x = mt3 * currentX + 3 * mt2 * t * currentX + 3 * mt * t2 * x2t + t3 * xt;
654
- const y = mt3 * currentY + 3 * mt2 * t * currentY + 3 * mt * t2 * y2t + t3 * yt;
655
- pathPoints.push([x, y]);
656
- }
657
- currentX = xt;
658
- currentY = yt;
659
- break;
660
- }
661
- case SVGPathData.QUAD_TO: {
662
- const [x1t, y1t] = transformPoint(cmd.x1, cmd.y1, finalTransform);
663
- const [xt, yt] = transformPoint(cmd.x, cmd.y, finalTransform);
664
- const steps = 20;
665
- for (let i = 1; i <= steps; i++) {
666
- const t = i / steps;
667
- const mt = 1 - t;
668
- const mt2 = mt * mt;
669
- const t2 = t * t;
670
- const x = mt2 * currentX + 2 * mt * t * x1t + t2 * xt;
671
- const y = mt2 * currentY + 2 * mt * t * y1t + t2 * yt;
672
- pathPoints.push([x, y]);
673
- }
674
- currentX = xt;
675
- currentY = yt;
676
- break;
677
- }
678
- case SVGPathData.CLOSE_PATH:
679
- // Add line back to start
680
- const steps = Math.max(1, Math.ceil(Math.hypot(startX - currentX, startY - currentY) / 2));
681
- for (let i = 1; i <= steps; i++) {
682
- const t = i / steps;
683
- pathPoints.push([
684
- currentX + (startX - currentX) * t,
685
- currentY + (startY - currentY) * t,
686
- ]);
687
- }
688
- currentX = startX;
689
- currentY = startY;
690
- break;
691
- }
692
- }
693
- // Apply clipRect filtering if this path has a mask
694
- if (parsedPath.clipRect && pathPoints.length > 0) {
695
- const clip = parsedPath.clipRect;
696
- // Transform clip bounds to pixel space (once per path, not per point)
697
- const [clipMinX, clipMinY] = transformPoint(clip.minX, clip.minY, viewportTransform);
698
- const [clipMaxX, clipMaxY] = transformPoint(clip.maxX, clip.maxY, viewportTransform);
699
- // Normalize bounds in case transform flipped them
700
- const pixelClipMinX = Math.min(clipMinX, clipMaxX);
701
- const pixelClipMaxX = Math.max(clipMinX, clipMaxX);
702
- const pixelClipMinY = Math.min(clipMinY, clipMaxY);
703
- const pixelClipMaxY = Math.max(clipMinY, clipMaxY);
704
- // Filter points to only those within clip rectangle
705
- const clippedPoints = pathPoints.filter(([x, y]) => {
706
- return (x >= pixelClipMinX &&
707
- x <= pixelClipMaxX &&
708
- y >= pixelClipMinY &&
709
- y <= pixelClipMaxY);
710
- });
711
- if (clippedPoints.length > 0) {
712
- allPaths.push(clippedPoints);
713
- }
714
- }
715
- else if (pathPoints.length > 0) {
716
- // No clipping needed
717
- allPaths.push(pathPoints);
718
- }
719
- }
720
- catch {
721
- // Skip invalid paths
722
- }
723
- }
724
- return allPaths;
725
- }
726
- /**
727
- * Render SVG content to a braille canvas
728
- */
729
- export function renderSVGToCanvas(svgContent, options = {}) {
730
- const targetWidth = options.width || 20;
731
- const targetHeight = options.height || 10;
732
- const canvas = createCanvas(targetWidth, targetHeight);
733
- const allPaths = extractPathPoints(svgContent, targetWidth, targetHeight, options.rotation || 0, options.autoFit || false, options.explicitBounds);
734
- // Draw all paths
735
- for (const pathPoints of allPaths) {
736
- // Draw edges (stroke) if requested
737
- if (options.stroke !== false) {
738
- for (let i = 0; i < pathPoints.length - 1; i++) {
739
- drawLine(canvas, pathPoints[i][0], pathPoints[i][1], pathPoints[i + 1][0], pathPoints[i + 1][1]);
740
- }
741
- }
742
- // Fill if requested
743
- if (options.fill !== false && pathPoints.length > 2) {
744
- fillPath(canvas, pathPoints, options.fillDensity || 1.0);
745
- }
746
- }
747
- // Invert pixels if requested
748
- if (options.invert) {
749
- for (let y = 0; y < canvas.pixels.length; y++) {
750
- for (let x = 0; x < canvas.pixels[y].length; x++) {
751
- canvas.pixels[y][x] = !canvas.pixels[y][x];
752
- }
753
- }
754
- }
755
- return canvas;
756
- }
757
- /**
758
- * Convert braille canvas to string array
759
- */
760
- export function canvasToStrings(canvas) {
761
- const lines = [];
762
- for (let charY = 0; charY < canvas.height; charY++) {
763
- let line = '';
764
- for (let charX = 0; charX < canvas.width; charX++) {
765
- let code = BRAILLE_BASE;
766
- // Each braille character is 2x4 pixels
767
- for (let dy = 0; dy < 4; dy++) {
768
- for (let dx = 0; dx < 2; dx++) {
769
- const px = charX * 2 + dx;
770
- const py = charY * 4 + dy;
771
- if (canvas.pixels[py]?.[px]) {
772
- code += DOT_BITS[dy][dx];
773
- }
774
- }
775
- }
776
- line += String.fromCharCode(code);
777
- }
778
- lines.push(line);
779
- }
780
- return lines;
781
- }
782
- /**
783
- * Render SVG content string to braille strings
784
- */
785
- export function renderSVGContent(content, options = {}) {
786
- const canvas = renderSVGToCanvas(content, options);
787
- return canvasToStrings(canvas);
788
- }
789
- /**
790
- * Load and render the Guild logo from local assets
791
- */
792
- export function renderGuildLogo(widthChars = 20, heightChars = 10, invert = true, rotation = 0) {
793
- const content = loadLogoSVG();
794
- if (!content) {
795
- // Fallback: return empty lines if logo not found
796
- return Array(heightChars).fill('⠀'.repeat(widthChars));
797
- }
798
- return renderSVGContent(content, {
799
- width: widthChars,
800
- height: heightChars,
801
- fill: true,
802
- invert,
803
- rotation,
804
- });
805
- }
806
- /**
807
- * Get raw path points for animation purposes
808
- * Returns points in pixel coordinate space for the given dimensions
809
- */
810
- export function getLogoPathPoints(widthChars = 20, heightChars = 10) {
811
- const content = loadLogoSVG();
812
- if (!content) {
813
- return [];
814
- }
815
- return extractPathPoints(content, widthChars, heightChars);
816
- }
817
- /**
818
- * Get pixel positions from rendered logo (useful for inverted logos)
819
- * Returns array of [x, y] pixel coordinates that are "on" in the rendered logo
820
- */
821
- export function getLogoPixelPositions(widthChars = 20, heightChars = 10, invert = true) {
822
- const content = loadLogoSVG();
823
- if (!content) {
824
- return [];
825
- }
826
- const canvas = renderSVGToCanvas(content, {
827
- width: widthChars,
828
- height: heightChars,
829
- fill: true,
830
- invert,
831
- });
832
- const positions = [];
833
- for (let y = 0; y < canvas.pixels.length; y++) {
834
- for (let x = 0; x < canvas.pixels[y].length; x++) {
835
- if (canvas.pixels[y][x]) {
836
- positions.push([x, y]);
837
- }
838
- }
839
- }
840
- return positions;
841
- }
842
- /**
843
- * Load the Guild logo SVG from local assets
844
- */
845
- function loadLogoSVG() {
846
- // Primary: CLI's own copy in assets directory
847
- const possiblePaths = [
848
- path.resolve(__dirname, '../assets/logo.svg'), // From dist/lib/
849
- path.resolve(__dirname, '../../src/assets/logo.svg'), // Dev fallback
850
- ];
851
- for (const logoPath of possiblePaths) {
852
- if (fs.existsSync(logoPath)) {
853
- return fs.readFileSync(logoPath, 'utf-8');
854
- }
855
- }
856
- return null;
857
- }
858
- //# sourceMappingURL=svg-renderer.js.map