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:
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user