react-pebble 0.1.1 → 0.2.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.
@@ -12,9 +12,10 @@
12
12
  * named color / font exactly once.
13
13
  * - **No native line, circle, or stroked rectangle.** Poco only has
14
14
  * `fillRectangle`, `drawText`, and bitmap draws. Outlines (stroke) are
15
- * emulated as four thin fillRectangles. Axis-aligned lines likewise.
16
- * Circles and diagonal lines would need the `commodetto/outline` extension
17
- * they're currently stubbed (TODO for a later pass).
15
+ * emulated as four thin fillRectangles. Axis-aligned lines use
16
+ * fillRectangles. Circles use a midpoint circle algorithm and diagonal
17
+ * lines use Bresenham's algorithm both decomposed into fillRectangle
18
+ * calls so they work on mock and real Poco without requiring extensions.
18
19
  * - **Text alignment is manual.** `drawText(text, font, color, x, y)` only
19
20
  * draws at a point. For center/right alignment we measure with
20
21
  * `getTextWidth` and compute the origin ourselves.
@@ -53,10 +54,9 @@ export const COLOR_PALETTE: Readonly<Record<string, RGB>> = {
53
54
  // ---------------------------------------------------------------------------
54
55
  // Named font shortcuts — mapped to (family, size) pairs.
55
56
  //
56
- // The family names here need to match fonts available in the Moddable
57
- // manifest. The Alloy scaffold ships "Bitham-Black" as a default we use
58
- // it for every logical font for now. Revisit once we add custom font
59
- // resources to the library manifest.
57
+ // The family names must match fonts available in the Moddable manifest.
58
+ // These correspond to the Pebble system font families available in Alloy.
59
+ // The piu compiler uses the same families via FONT_TO_PIU in compile-to-piu.ts.
60
60
  // ---------------------------------------------------------------------------
61
61
 
62
62
  export interface FontSpec {
@@ -65,19 +65,38 @@ export interface FontSpec {
65
65
  }
66
66
 
67
67
  export const FONT_PALETTE: Readonly<Record<string, FontSpec>> = {
68
- gothic14: { family: 'Bitham-Black', size: 14 },
69
- gothic14Bold: { family: 'Bitham-Black', size: 14 },
70
- gothic18: { family: 'Bitham-Black', size: 18 },
71
- gothic18Bold: { family: 'Bitham-Black', size: 18 },
72
- gothic24: { family: 'Bitham-Black', size: 24 },
73
- gothic24Bold: { family: 'Bitham-Black', size: 24 },
74
- gothic28: { family: 'Bitham-Black', size: 28 },
75
- gothic28Bold: { family: 'Bitham-Black', size: 28 },
76
- bitham30Black: { family: 'Bitham-Black', size: 30 },
77
- bitham42Bold: { family: 'Bitham-Black', size: 42 },
78
- bitham42Light: { family: 'Bitham-Black', size: 42 },
79
- bitham34MediumNumbers: { family: 'Bitham-Black', size: 34 },
80
- bitham42MediumNumbers: { family: 'Bitham-Black', size: 42 },
68
+ // Gothic family standard UI text
69
+ gothic14: { family: 'Gothic', size: 14 },
70
+ gothic14Bold: { family: 'Gothic-Bold', size: 14 },
71
+ gothic18: { family: 'Gothic', size: 18 },
72
+ gothic18Bold: { family: 'Gothic-Bold', size: 18 },
73
+ gothic24: { family: 'Gothic', size: 24 },
74
+ gothic24Bold: { family: 'Gothic-Bold', size: 24 },
75
+ gothic28: { family: 'Gothic', size: 28 },
76
+ gothic28Bold: { family: 'Gothic-Bold', size: 28 },
77
+
78
+ // Bitham family large display fonts
79
+ bitham30Black: { family: 'Bitham-Black', size: 30 },
80
+ bitham42Bold: { family: 'Bitham-Bold', size: 42 },
81
+ bitham42Light: { family: 'Bitham-Light', size: 42 },
82
+ bitham34MediumNumbers: { family: 'Bitham', size: 34 },
83
+ bitham42MediumNumbers: { family: 'Bitham', size: 42 },
84
+
85
+ // Roboto family
86
+ robotoCondensed21: { family: 'Roboto-Condensed', size: 21 },
87
+ roboto21: { family: 'Roboto', size: 21 },
88
+
89
+ // Droid Serif
90
+ droid28: { family: 'Droid-Serif', size: 28 },
91
+
92
+ // LECO family — LED-style numeric fonts
93
+ leco20: { family: 'LECO', size: 20 },
94
+ leco26: { family: 'LECO', size: 26 },
95
+ leco28: { family: 'LECO', size: 28 },
96
+ leco32: { family: 'LECO', size: 32 },
97
+ leco36: { family: 'LECO', size: 36 },
98
+ leco38: { family: 'LECO', size: 38 },
99
+ leco42: { family: 'LECO', size: 42 },
81
100
  };
82
101
 
83
102
  const DEFAULT_FONT_KEY = 'gothic18';
@@ -195,18 +214,30 @@ export class PocoRenderer {
195
214
  const h = num(p, 'h') || num(p, 'height');
196
215
  const fill = str(p, 'fill');
197
216
  const stroke = str(p, 'stroke');
217
+ const br = num(p, 'borderRadius');
198
218
 
199
- if (fill) {
200
- this.poco.fillRectangle(this.getColor(fill), x, y, w, h);
201
- }
202
- if (stroke) {
203
- // Emulate outline with four thin fill rects.
204
- const sw = num(p, 'strokeWidth') || 1;
205
- const c = this.getColor(stroke);
206
- this.poco.fillRectangle(c, x, y, w, sw); // top
207
- this.poco.fillRectangle(c, x, y + h - sw, w, sw); // bottom
208
- this.poco.fillRectangle(c, x, y, sw, h); // left
209
- this.poco.fillRectangle(c, x + w - sw, y, sw, h); // right
219
+ if (br > 0) {
220
+ // Rounded rectangle
221
+ if (fill) {
222
+ this.fillRoundRect(this.getColor(fill), x, y, w, h, br);
223
+ }
224
+ if (stroke) {
225
+ const sw = num(p, 'strokeWidth') || 1;
226
+ this.strokeRoundRect(this.getColor(stroke), x, y, w, h, br, sw);
227
+ }
228
+ } else {
229
+ // Sharp rectangle
230
+ if (fill) {
231
+ this.poco.fillRectangle(this.getColor(fill), x, y, w, h);
232
+ }
233
+ if (stroke) {
234
+ const sw = num(p, 'strokeWidth') || 1;
235
+ const c = this.getColor(stroke);
236
+ this.poco.fillRectangle(c, x, y, w, sw); // top
237
+ this.poco.fillRectangle(c, x, y + h - sw, w, sw); // bottom
238
+ this.poco.fillRectangle(c, x, y, sw, h); // left
239
+ this.poco.fillRectangle(c, x + w - sw, y, sw, h); // right
240
+ }
210
241
  }
211
242
 
212
243
  this.renderChildren(node, x, y);
@@ -218,21 +249,33 @@ export class PocoRenderer {
218
249
  if (!text) break;
219
250
 
220
251
  const boxW = num(p, 'w') || num(p, 'width') || this.poco.width - x;
252
+ const boxH = num(p, 'h') || num(p, 'height') || 0;
221
253
  const font = this.getFont(str(p, 'font'));
222
254
  const color = this.getColor(str(p, 'color') ?? 'white');
223
255
  const align = str(p, 'align') ?? 'left';
224
-
225
- let tx = x;
226
- if (align === 'center' || align === 'right') {
227
- const tw = this.poco.getTextWidth(text, font);
228
- if (align === 'center') {
229
- tx = x + Math.floor((boxW - tw) / 2);
230
- } else {
231
- tx = x + boxW - tw;
256
+ const lineHeight = (font as unknown as { height: number }).height || 16;
257
+
258
+ // Word-wrap text into lines that fit within boxW
259
+ const lines = this.wrapText(text, font, boxW);
260
+
261
+ let ty = y;
262
+ for (const line of lines) {
263
+ // Stop if we'd exceed the box height (when specified)
264
+ if (boxH > 0 && ty - y + lineHeight > boxH) break;
265
+
266
+ let tx = x;
267
+ if (align === 'center' || align === 'right') {
268
+ const tw = this.poco.getTextWidth(line, font);
269
+ if (align === 'center') {
270
+ tx = x + Math.floor((boxW - tw) / 2);
271
+ } else {
272
+ tx = x + boxW - tw;
273
+ }
232
274
  }
233
- }
234
275
 
235
- this.poco.drawText(text, font, color, tx, y);
276
+ this.poco.drawText(line, font, color, tx, ty);
277
+ ty += lineHeight;
278
+ }
236
279
  break;
237
280
  }
238
281
 
@@ -255,20 +298,56 @@ export class PocoRenderer {
255
298
  const w = Math.abs(x2 - x) || 1;
256
299
  this.poco.fillRectangle(c, left, y, w, sw);
257
300
  }
258
- // else: diagonal line — silently skipped for now
301
+ else {
302
+ // Diagonal line via Bresenham's algorithm
303
+ this.drawDiagonalLine(c, x, y, x2, y2, sw);
304
+ }
259
305
  break;
260
306
  }
261
307
 
262
308
  case 'pbl-circle': {
263
- // TODO: implement via commodetto/outline extension (blendOutline)
264
- // or a Bresenham-style approximation. Stubbed for now.
309
+ const r = num(p, 'r') || num(p, 'radius');
310
+ if (r <= 0) break;
311
+ const fill = str(p, 'fill');
312
+ const stroke = str(p, 'stroke');
313
+ const sw = num(p, 'strokeWidth') || 1;
314
+
315
+ // Midpoint circle algorithm — works on any Poco (real or mock)
316
+ // without requiring the commodetto/outline extension.
317
+ if (fill) {
318
+ const fc = this.getColor(fill);
319
+ this.fillCircle(fc, x + r, y + r, r);
320
+ }
321
+ if (stroke) {
322
+ const sc = this.getColor(stroke);
323
+ this.strokeCircle(sc, x + r, y + r, r, sw);
324
+ }
265
325
  break;
266
326
  }
267
327
 
268
328
  case 'pbl-image': {
269
329
  const bitmap = p.bitmap;
270
330
  if (bitmap) {
271
- this.poco.drawBitmap(bitmap as never, x, y);
331
+ const rotation = num(p, 'rotation');
332
+ const scale = num(p, 'scale');
333
+ if (rotation || (scale && scale !== 1)) {
334
+ // Use Poco's extended drawBitmap with rotation/scale if available.
335
+ // The mock Poco records the intent; the real Poco handles transforms.
336
+ const bmp = bitmap as never;
337
+ const poco = this.poco as Poco & {
338
+ drawBitmapWithTransform?: (
339
+ bmp: never, x: number, y: number, rotation: number, scale: number,
340
+ ) => void;
341
+ };
342
+ if (poco.drawBitmapWithTransform) {
343
+ poco.drawBitmapWithTransform(bmp, x, y, rotation, scale || 1);
344
+ } else {
345
+ // Fallback: draw without transform
346
+ this.poco.drawBitmap(bmp, x, y);
347
+ }
348
+ } else {
349
+ this.poco.drawBitmap(bitmap as never, x, y);
350
+ }
272
351
  }
273
352
  break;
274
353
  }
@@ -278,10 +357,53 @@ export class PocoRenderer {
278
357
  break;
279
358
  }
280
359
 
281
- case 'pbl-statusbar':
360
+ case 'pbl-statusbar': {
361
+ // Render a simple status bar: background + time text
362
+ const sbBg = str(p, 'backgroundColor') ?? 'black';
363
+ const sbColor = str(p, 'color') ?? 'white';
364
+ const sbH = 16;
365
+ const sbW = this.poco.width;
366
+
367
+ this.poco.fillRectangle(this.getColor(sbBg), 0, 0, sbW, sbH);
368
+
369
+ // Draw current time in the center
370
+ const sbFont = this.getFont('gothic14');
371
+ const now = new Date();
372
+ const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
373
+ const tw = this.poco.getTextWidth(timeStr, sbFont);
374
+ this.poco.drawText(timeStr, sbFont, this.getColor(sbColor), Math.floor((sbW - tw) / 2), 1);
375
+
376
+ // Separator line
377
+ const sep = str(p, 'separator') ?? 'none';
378
+ if (sep === 'line') {
379
+ this.poco.fillRectangle(this.getColor(sbColor), 0, sbH - 1, sbW, 1);
380
+ } else if (sep === 'dotted') {
381
+ for (let dx = 0; dx < sbW; dx += 3) {
382
+ this.poco.fillRectangle(this.getColor(sbColor), dx, sbH - 1, 1, 1);
383
+ }
384
+ }
385
+ break;
386
+ }
387
+
282
388
  case 'pbl-actionbar': {
283
- // Alloy has no built-in status/action bar UI; these are no-ops for now.
284
- // An app that wants a status bar should draw its own.
389
+ // Render an action bar on the right edge: background column + icon placeholders
390
+ const abBg = str(p, 'backgroundColor') ?? 'darkGray';
391
+ const abW = 30;
392
+ const abX = this.poco.width - abW;
393
+ const abH = this.poco.height;
394
+
395
+ this.poco.fillRectangle(this.getColor(abBg), abX, 0, abW, abH);
396
+
397
+ // Draw dot placeholders for up/select/down icons
398
+ const dotColor = this.getColor('white');
399
+ const dotR = 3;
400
+ const centerX = abX + Math.floor(abW / 2);
401
+ // Up icon area (top third)
402
+ this.poco.fillRectangle(dotColor, centerX - dotR, Math.floor(abH / 6) - dotR, dotR * 2, dotR * 2);
403
+ // Select icon area (middle)
404
+ this.poco.fillRectangle(dotColor, centerX - dotR, Math.floor(abH / 2) - dotR, dotR * 2, dotR * 2);
405
+ // Down icon area (bottom third)
406
+ this.poco.fillRectangle(dotColor, centerX - dotR, Math.floor(abH * 5 / 6) - dotR, dotR * 2, dotR * 2);
285
407
  break;
286
408
  }
287
409
 
@@ -291,6 +413,244 @@ export class PocoRenderer {
291
413
  }
292
414
  }
293
415
  }
416
+
417
+ // -------------------------------------------------------------------------
418
+ // Circle rendering — midpoint circle algorithm
419
+ // -------------------------------------------------------------------------
420
+
421
+ /** Fill a circle by drawing horizontal spans for each row. */
422
+ private fillCircle(color: PocoColor, cx: number, cy: number, r: number): void {
423
+ const { poco } = this;
424
+ let x0 = r;
425
+ let y0 = 0;
426
+ let err = 1 - r;
427
+
428
+ // Track the last drawn y to avoid duplicate spans
429
+ let lastY1 = -1;
430
+ let lastY2 = -1;
431
+
432
+ while (x0 >= y0) {
433
+ // Draw horizontal spans for each octant pair
434
+ if (cy + y0 !== lastY1) {
435
+ poco.fillRectangle(color, cx - x0, cy + y0, x0 * 2 + 1, 1);
436
+ lastY1 = cy + y0;
437
+ }
438
+ if (cy - y0 !== lastY2 && y0 !== 0) {
439
+ poco.fillRectangle(color, cx - x0, cy - y0, x0 * 2 + 1, 1);
440
+ lastY2 = cy - y0;
441
+ }
442
+ if (cy + x0 !== lastY1) {
443
+ poco.fillRectangle(color, cx - y0, cy + x0, y0 * 2 + 1, 1);
444
+ lastY1 = cy + x0;
445
+ }
446
+ if (cy - x0 !== lastY2) {
447
+ poco.fillRectangle(color, cx - y0, cy - x0, y0 * 2 + 1, 1);
448
+ lastY2 = cy - x0;
449
+ }
450
+
451
+ y0++;
452
+ if (err < 0) {
453
+ err += 2 * y0 + 1;
454
+ } else {
455
+ x0--;
456
+ err += 2 * (y0 - x0) + 1;
457
+ }
458
+ }
459
+ }
460
+
461
+ /** Stroke a circle outline by drawing small rects at each perimeter point. */
462
+ private strokeCircle(color: PocoColor, cx: number, cy: number, r: number, sw: number): void {
463
+ const { poco } = this;
464
+ let x0 = r;
465
+ let y0 = 0;
466
+ let err = 1 - r;
467
+
468
+ while (x0 >= y0) {
469
+ // 8 octant points
470
+ poco.fillRectangle(color, cx + x0, cy + y0, sw, sw);
471
+ poco.fillRectangle(color, cx - x0, cy + y0, sw, sw);
472
+ poco.fillRectangle(color, cx + x0, cy - y0, sw, sw);
473
+ poco.fillRectangle(color, cx - x0, cy - y0, sw, sw);
474
+ poco.fillRectangle(color, cx + y0, cy + x0, sw, sw);
475
+ poco.fillRectangle(color, cx - y0, cy + x0, sw, sw);
476
+ poco.fillRectangle(color, cx + y0, cy - x0, sw, sw);
477
+ poco.fillRectangle(color, cx - y0, cy - x0, sw, sw);
478
+
479
+ y0++;
480
+ if (err < 0) {
481
+ err += 2 * y0 + 1;
482
+ } else {
483
+ x0--;
484
+ err += 2 * (y0 - x0) + 1;
485
+ }
486
+ }
487
+ }
488
+
489
+ // -------------------------------------------------------------------------
490
+ // Diagonal line rendering — Bresenham's line algorithm
491
+ // -------------------------------------------------------------------------
492
+
493
+ private drawDiagonalLine(
494
+ color: PocoColor, x1: number, y1: number, x2: number, y2: number, sw: number,
495
+ ): void {
496
+ const { poco } = this;
497
+ let dx = Math.abs(x2 - x1);
498
+ let dy = Math.abs(y2 - y1);
499
+ const sx = x1 < x2 ? 1 : -1;
500
+ const sy = y1 < y2 ? 1 : -1;
501
+ let err = dx - dy;
502
+ let cx = x1;
503
+ let cy = y1;
504
+
505
+ for (;;) {
506
+ poco.fillRectangle(color, cx, cy, sw, sw);
507
+ if (cx === x2 && cy === y2) break;
508
+ const e2 = 2 * err;
509
+ if (e2 > -dy) {
510
+ err -= dy;
511
+ cx += sx;
512
+ }
513
+ if (e2 < dx) {
514
+ err += dx;
515
+ cy += sy;
516
+ }
517
+ }
518
+ }
519
+
520
+ // -------------------------------------------------------------------------
521
+ // Rounded rectangle rendering
522
+ // -------------------------------------------------------------------------
523
+
524
+ /** Fill a rounded rectangle by combining rects and quarter-circle corners. */
525
+ private fillRoundRect(
526
+ color: PocoColor, x: number, y: number, w: number, h: number, r: number,
527
+ ): void {
528
+ const { poco } = this;
529
+ const cr = Math.min(r, Math.floor(w / 2), Math.floor(h / 2));
530
+
531
+ // Center body
532
+ poco.fillRectangle(color, x, y + cr, w, h - cr * 2);
533
+ // Top strip (between corners)
534
+ poco.fillRectangle(color, x + cr, y, w - cr * 2, cr);
535
+ // Bottom strip (between corners)
536
+ poco.fillRectangle(color, x + cr, y + h - cr, w - cr * 2, cr);
537
+
538
+ // Four quarter-circle corners via midpoint algorithm
539
+ this.fillQuarterCircles(color, x + cr, y + cr, x + w - cr - 1, y + h - cr - 1, cr);
540
+ }
541
+
542
+ /** Stroke a rounded rectangle outline. */
543
+ private strokeRoundRect(
544
+ color: PocoColor, x: number, y: number, w: number, h: number, r: number, sw: number,
545
+ ): void {
546
+ const { poco } = this;
547
+ const cr = Math.min(r, Math.floor(w / 2), Math.floor(h / 2));
548
+
549
+ // Straight edges
550
+ poco.fillRectangle(color, x + cr, y, w - cr * 2, sw); // top
551
+ poco.fillRectangle(color, x + cr, y + h - sw, w - cr * 2, sw); // bottom
552
+ poco.fillRectangle(color, x, y + cr, sw, h - cr * 2); // left
553
+ poco.fillRectangle(color, x + w - sw, y + cr, sw, h - cr * 2); // right
554
+
555
+ // Corner arcs
556
+ this.strokeQuarterCircles(color, x + cr, y + cr, x + w - cr - 1, y + h - cr - 1, cr, sw);
557
+ }
558
+
559
+ /** Fill four quarter-circles at the corners of a rounded rect. */
560
+ private fillQuarterCircles(
561
+ color: PocoColor, cx1: number, cy1: number, cx2: number, cy2: number, r: number,
562
+ ): void {
563
+ const { poco } = this;
564
+ let x0 = r;
565
+ let y0 = 0;
566
+ let err = 1 - r;
567
+
568
+ while (x0 >= y0) {
569
+ // Top-left corner
570
+ poco.fillRectangle(color, cx1 - x0, cy1 - y0, x0, 1);
571
+ poco.fillRectangle(color, cx1 - y0, cy1 - x0, y0, 1);
572
+ // Top-right corner
573
+ poco.fillRectangle(color, cx2 + 1, cy1 - y0, x0, 1);
574
+ poco.fillRectangle(color, cx2 + 1, cy1 - x0, y0, 1);
575
+ // Bottom-left corner
576
+ poco.fillRectangle(color, cx1 - x0, cy2 + y0, x0, 1);
577
+ poco.fillRectangle(color, cx1 - y0, cy2 + x0, y0, 1);
578
+ // Bottom-right corner
579
+ poco.fillRectangle(color, cx2 + 1, cy2 + y0, x0, 1);
580
+ poco.fillRectangle(color, cx2 + 1, cy2 + x0, y0, 1);
581
+
582
+ y0++;
583
+ if (err < 0) {
584
+ err += 2 * y0 + 1;
585
+ } else {
586
+ x0--;
587
+ err += 2 * (y0 - x0) + 1;
588
+ }
589
+ }
590
+ }
591
+
592
+ /** Stroke four quarter-circle arcs at the corners of a rounded rect. */
593
+ private strokeQuarterCircles(
594
+ color: PocoColor, cx1: number, cy1: number, cx2: number, cy2: number, r: number, sw: number,
595
+ ): void {
596
+ const { poco } = this;
597
+ let x0 = r;
598
+ let y0 = 0;
599
+ let err = 1 - r;
600
+
601
+ while (x0 >= y0) {
602
+ // Top-left
603
+ poco.fillRectangle(color, cx1 - x0, cy1 - y0, sw, sw);
604
+ poco.fillRectangle(color, cx1 - y0, cy1 - x0, sw, sw);
605
+ // Top-right
606
+ poco.fillRectangle(color, cx2 + x0, cy1 - y0, sw, sw);
607
+ poco.fillRectangle(color, cx2 + y0, cy1 - x0, sw, sw);
608
+ // Bottom-left
609
+ poco.fillRectangle(color, cx1 - x0, cy2 + y0, sw, sw);
610
+ poco.fillRectangle(color, cx1 - y0, cy2 + x0, sw, sw);
611
+ // Bottom-right
612
+ poco.fillRectangle(color, cx2 + x0, cy2 + y0, sw, sw);
613
+ poco.fillRectangle(color, cx2 + y0, cy2 + x0, sw, sw);
614
+
615
+ y0++;
616
+ if (err < 0) {
617
+ err += 2 * y0 + 1;
618
+ } else {
619
+ x0--;
620
+ err += 2 * (y0 - x0) + 1;
621
+ }
622
+ }
623
+ }
624
+
625
+ // -------------------------------------------------------------------------
626
+ // Text wrapping
627
+ // -------------------------------------------------------------------------
628
+
629
+ /** Break text into lines that fit within maxWidth. */
630
+ private wrapText(text: string, font: PocoFont, maxWidth: number): string[] {
631
+ // If the full text fits, skip wrapping
632
+ if (this.poco.getTextWidth(text, font) <= maxWidth) {
633
+ return [text];
634
+ }
635
+
636
+ const words = text.split(' ');
637
+ const lines: string[] = [];
638
+ let currentLine = '';
639
+
640
+ for (const word of words) {
641
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
642
+ if (this.poco.getTextWidth(testLine, font) <= maxWidth) {
643
+ currentLine = testLine;
644
+ } else {
645
+ if (currentLine) lines.push(currentLine);
646
+ // If a single word exceeds maxWidth, it goes on its own line (truncated visually)
647
+ currentLine = word;
648
+ }
649
+ }
650
+ if (currentLine) lines.push(currentLine);
651
+
652
+ return lines;
653
+ }
294
654
  }
295
655
 
296
656
  // ---------------------------------------------------------------------------