feat(svg): SVG colour + transform parsers for the drawing card (T-320)

parseSvgColor → packed ARGB (#rgb/#rgba/#rrggbb/#rrggbbaa, rgb()/rgba()
integer or %, common names, none/transparent → 0x0); null on unrecognised
so the caller can inherit. parseTransform → a composed 2-D Affine
(translate/scale/rotate[/about-point]/matrix/skew), applied left-to-right.
Both tolerant and Flutter-free, covered by dart test (22 cases). These feed
the typed node model next. No user-visible behaviour yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 22:18:53 +02:00
co-authored by Claude Opus 4.8
parent fb96e5238f
commit ac387796c8
4 changed files with 318 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
import 'package:clide/src/svg/svg_color.dart';
import 'package:test/test.dart';
void main() {
group('parseSvgColor', () {
test('#rrggbb', () {
expect(parseSvgColor('#0D32B2'), 0xFF0D32B2);
});
test('#rgb shorthand expands each nibble', () {
expect(parseSvgColor('#abc'), 0xFFAABBCC);
});
test('#rrggbbaa carries alpha', () {
expect(parseSvgColor('#11223344'), 0x44112233);
});
test('#rgba shorthand', () {
expect(parseSvgColor('#1234'), 0x44112233);
});
test('rgb() integer channels', () {
expect(parseSvgColor('rgb(13, 50, 178)'), 0xFF0D32B2);
});
test('rgba() with fractional alpha', () {
expect(parseSvgColor('rgba(0,0,0,0.5)'), 0x80000000);
});
test('rgb() percentage channels', () {
expect(parseSvgColor('rgb(100%, 0%, 0%)'), 0xFFFF0000);
});
test('named colours', () {
expect(parseSvgColor('red'), 0xFFFF0000);
expect(parseSvgColor('white'), 0xFFFFFFFF);
expect(parseSvgColor('grey'), parseSvgColor('gray'));
});
test('none and transparent are fully transparent', () {
expect(parseSvgColor('none'), 0x00000000);
expect(parseSvgColor('transparent'), 0x00000000);
});
test('case-insensitive and whitespace-tolerant', () {
expect(parseSvgColor(' #0d32b2 '), 0xFF0D32B2);
expect(parseSvgColor('RED'), 0xFFFF0000);
});
test('unrecognised input is null, never throws', () {
expect(parseSvgColor(''), isNull);
expect(parseSvgColor('bogus'), isNull);
expect(parseSvgColor('#xyz'), isNull);
expect(parseSvgColor('#12'), isNull);
});
});
}
+65
View File
@@ -0,0 +1,65 @@
import 'package:clide/src/svg/svg_transform.dart';
import 'package:test/test.dart';
void main() {
group('Affine', () {
test('apply maps a point', () {
final (x, y) = const Affine(2, 0, 0, 3, 5, 7).apply(1, 1);
expect(x, 7); // 2*1 + 0 + 5
expect(y, 10); // 3*1 + 0 + 7
});
test('multiply composes (this after other)', () {
// translate(10,0) · scale(2): scale first, then translate.
const t = Affine(1, 0, 0, 1, 10, 0);
const s = Affine(2, 0, 0, 2, 0, 0);
final (x, y) = t.multiply(s).apply(1, 1);
expect(x, 12);
expect(y, 2);
});
});
group('parseTransform', () {
test('empty / garbage → identity', () {
expect(parseTransform(''), Affine.identity);
expect(parseTransform('not a transform'), Affine.identity);
});
test('translate with one and two args', () {
expect(parseTransform('translate(5,10)'), const Affine(1, 0, 0, 1, 5, 10));
expect(parseTransform('translate(5)'), const Affine(1, 0, 0, 1, 5, 0));
});
test('scale uniform and non-uniform', () {
expect(parseTransform('scale(2)'), const Affine(2, 0, 0, 2, 0, 0));
expect(parseTransform('scale(2,3)'), const Affine(2, 0, 0, 3, 0, 0));
});
test('matrix is taken verbatim', () {
expect(parseTransform('matrix(1,2,3,4,5,6)'), const Affine(1, 2, 3, 4, 5, 6));
});
test('rotate(90) turns +x into +y', () {
final (x, y) = parseTransform('rotate(90)').apply(1, 0);
expect(x, closeTo(0, 1e-9));
expect(y, closeTo(1, 1e-9));
});
test('rotate about a point leaves that point fixed', () {
final (x, y) = parseTransform('rotate(90, 1, 1)').apply(1, 1);
expect(x, closeTo(1, 1e-9));
expect(y, closeTo(1, 1e-9));
});
test('composes a list left-to-right, leftmost outermost', () {
// translate(10,0) scale(2): point (1,1) → scale → (2,2) → translate → (12,2)
final (x, y) = parseTransform('translate(10,0) scale(2)').apply(1, 1);
expect(x, closeTo(12, 1e-9));
expect(y, closeTo(2, 1e-9));
});
test('skips unknown functions but keeps the rest', () {
expect(parseTransform('frobnicate(9) translate(3,4)'), const Affine(1, 0, 0, 1, 3, 4));
});
});
}