Add some tests and fix several bugs
This commit is contained in:
@@ -13,6 +13,13 @@ class Point {
|
|||||||
operator *(covariant Point p) => Point(x * p.x, y * p.y);
|
operator *(covariant Point p) => Point(x * p.x, y * p.y);
|
||||||
operator /(covariant Point p) => Point(x / p.x, y / p.y);
|
operator /(covariant Point p) => Point(x / p.x, y / p.y);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
other is Point && x == other.x && y == other.y;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => x.hashCode ^ y.hashCode;
|
||||||
|
|
||||||
Point addX(num n) => Point(x + n, y);
|
Point addX(num n) => Point(x + n, y);
|
||||||
Point addY(num n) => Point(x, y + n);
|
Point addY(num n) => Point(x, y + n);
|
||||||
Point add(num n) => Point(x + n, y + n);
|
Point add(num n) => Point(x + n, y + n);
|
||||||
@@ -38,4 +45,4 @@ class Point {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '($x,$y)';
|
String toString() => '($x,$y)';
|
||||||
}
|
}
|
||||||
|
@@ -73,13 +73,13 @@ String _stripArray(String stringToParse) {
|
|||||||
return stringToParse;
|
return stringToParse;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ParserResult<double> _parseNumber(String stringToParse) {
|
_ParserResult<num> _parseNumber(String stringToParse) {
|
||||||
final res = _floatPattern.firstMatch(stringToParse);
|
final res = _floatPattern.firstMatch(stringToParse);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
throw InvalidPathError("Expected a number, got '$stringToParse'.");
|
throw InvalidPathError("Expected a number, got '$stringToParse'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
final number = double.parse(res.group(0)!);
|
final number = num.parse(res.group(0)!);
|
||||||
final start = res.start;
|
final start = res.start;
|
||||||
final end = res.end;
|
final end = res.end;
|
||||||
stringToParse =
|
stringToParse =
|
||||||
@@ -89,7 +89,7 @@ _ParserResult<double> _parseNumber(String stringToParse) {
|
|||||||
return _ParserResult(value: number, remaining: stringToParse);
|
return _ParserResult(value: number, remaining: stringToParse);
|
||||||
}
|
}
|
||||||
|
|
||||||
_ParserResult<double> _parseUnsignedNumber(String stringToParse) {
|
_ParserResult<num> _parseUnsignedNumber(String stringToParse) {
|
||||||
final number = _parseNumber(stringToParse);
|
final number = _parseNumber(stringToParse);
|
||||||
if (number.value < 0) {
|
if (number.value < 0) {
|
||||||
throw InvalidPathError("Expected a non-negative number, got '$number'.");
|
throw InvalidPathError("Expected a non-negative number, got '$number'.");
|
||||||
@@ -137,6 +137,7 @@ List<_Command> _commandifyPath(String pathdef) {
|
|||||||
for (String c in pathdef.split(_commandPattern)) {
|
for (String c in pathdef.split(_commandPattern)) {
|
||||||
String x = c[0];
|
String x = c[0];
|
||||||
String? y = (c.length > 1) ? c.substring(1).trim() : null;
|
String? y = (c.length > 1) ? c.substring(1).trim() : null;
|
||||||
|
|
||||||
if (!_commands.contains(x)) {
|
if (!_commands.contains(x)) {
|
||||||
throw InvalidPathError("Path does not start with a command: $pathdef");
|
throw InvalidPathError("Path does not start with a command: $pathdef");
|
||||||
}
|
}
|
||||||
@@ -146,9 +147,8 @@ List<_Command> _commandifyPath(String pathdef) {
|
|||||||
}
|
}
|
||||||
if (x == "z" || x == "Z") {
|
if (x == "z" || x == "Z") {
|
||||||
// The end command takes no arguments, so add a blank one
|
// The end command takes no arguments, so add a blank one
|
||||||
token.addAll([x, ""]);
|
token = [x, ""];
|
||||||
} else {
|
} else {
|
||||||
// token = [x, x.substring(1).trim()];
|
|
||||||
token = [x];
|
token = [x];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ Path parsePath(String pathdef) {
|
|||||||
segments.add(Line(start: currentPos, end: pos));
|
segments.add(Line(start: currentPos, end: pos));
|
||||||
currentPos = pos;
|
currentPos = pos;
|
||||||
} else if (command == "H") {
|
} else if (command == "H") {
|
||||||
double hpos = token.args[0] as double;
|
num hpos = token.args[0] as num;
|
||||||
if (!absolute) {
|
if (!absolute) {
|
||||||
hpos += currentPos.x;
|
hpos += currentPos.x;
|
||||||
}
|
}
|
||||||
@@ -250,7 +250,7 @@ Path parsePath(String pathdef) {
|
|||||||
segments.add(Line(start: currentPos, end: pos));
|
segments.add(Line(start: currentPos, end: pos));
|
||||||
currentPos = pos;
|
currentPos = pos;
|
||||||
} else if (command == "V") {
|
} else if (command == "V") {
|
||||||
double vpos = token.args[0] as double;
|
num vpos = token.args[0] as num;
|
||||||
if (!absolute) {
|
if (!absolute) {
|
||||||
vpos += currentPos.y;
|
vpos += currentPos.y;
|
||||||
}
|
}
|
||||||
@@ -304,10 +304,11 @@ Path parsePath(String pathdef) {
|
|||||||
}
|
}
|
||||||
segments.add(
|
segments.add(
|
||||||
CubicBezier(
|
CubicBezier(
|
||||||
start: currentPos,
|
start: currentPos,
|
||||||
control1: control1,
|
control1: control1,
|
||||||
control2: control2,
|
control2: control2,
|
||||||
end: end),
|
end: end,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
currentPos = end;
|
currentPos = end;
|
||||||
} else if (command == "Q") {
|
} else if (command == "Q") {
|
||||||
@@ -354,8 +355,8 @@ Path parsePath(String pathdef) {
|
|||||||
} else if (command == "A") {
|
} else if (command == "A") {
|
||||||
// For some reason I implemented the Arc with a complex radius.
|
// For some reason I implemented the Arc with a complex radius.
|
||||||
// That doesn't really make much sense, but... *shrugs*
|
// That doesn't really make much sense, but... *shrugs*
|
||||||
final radius = Point(token.args[0] as double, token.args[1] as double);
|
final radius = Point(token.args[0] as num, token.args[1] as num);
|
||||||
final rotation = token.args[2] as double;
|
final rotation = token.args[2] as num;
|
||||||
final arc = token.args[3] as bool;
|
final arc = token.args[3] as bool;
|
||||||
final sweep = token.args[4] as bool;
|
final sweep = token.args[4] as bool;
|
||||||
Point end = token.args[5] as Point;
|
Point end = token.args[5] as Point;
|
||||||
|
@@ -9,30 +9,34 @@ import 'package:bisection/extension.dart';
|
|||||||
|
|
||||||
import '../common/Point.dart';
|
import '../common/Point.dart';
|
||||||
|
|
||||||
double radians(num n) => n * pi / 180;
|
num radians(num n) => n * pi / 180;
|
||||||
double degrees(num n) => n * 180 / pi;
|
num degrees(num n) => n * 180 / pi;
|
||||||
|
|
||||||
const defaultMinDepth = 5;
|
const defaultMinDepth = 5;
|
||||||
const defaultError = 1e-12;
|
const defaultError = 1e-12;
|
||||||
|
|
||||||
|
extension _RemovePointIfInt on num {
|
||||||
|
num get removePointIfInt => truncate() == this ? truncate() : this;
|
||||||
|
}
|
||||||
|
|
||||||
/// Recursively approximates the length by straight lines
|
/// Recursively approximates the length by straight lines
|
||||||
double segmentLength({
|
num segmentLength({
|
||||||
required SvgPath curve,
|
required SvgPath curve,
|
||||||
required num start,
|
required num start,
|
||||||
required num end,
|
required num end,
|
||||||
required Point startPoint,
|
required Point startPoint,
|
||||||
required Point endPoint,
|
required Point endPoint,
|
||||||
required double error,
|
required num error,
|
||||||
required int minDepth,
|
required int minDepth,
|
||||||
required double depth,
|
required num depth,
|
||||||
}) {
|
}) {
|
||||||
num mid = (start + end) / 2;
|
num mid = (start + end) / 2;
|
||||||
Point midPoint = curve.point(mid);
|
Point midPoint = curve.point(mid);
|
||||||
double length = (endPoint - startPoint).abs();
|
num length = (endPoint - startPoint).abs();
|
||||||
double firstHalf = (midPoint - startPoint).abs();
|
num firstHalf = (midPoint - startPoint).abs();
|
||||||
double secondHalf = (endPoint - midPoint).abs();
|
num secondHalf = (endPoint - midPoint).abs();
|
||||||
|
|
||||||
double length2 = firstHalf + secondHalf;
|
num length2 = firstHalf + secondHalf;
|
||||||
if ((length2 - length > error) || (depth < minDepth)) {
|
if ((length2 - length > error) || (depth < minDepth)) {
|
||||||
// Calculate the length of each segment:
|
// Calculate the length of each segment:
|
||||||
depth += 1;
|
depth += 1;
|
||||||
@@ -70,11 +74,18 @@ abstract class SvgPath {
|
|||||||
required this.end,
|
required this.end,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
other is SvgPath && start == other.start && end == other.end;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => start.hashCode ^ end.hashCode;
|
||||||
|
|
||||||
/// Calculate the x,y position at a certain position of the path
|
/// Calculate the x,y position at a certain position of the path
|
||||||
Point point(num pos);
|
Point point(num pos);
|
||||||
|
|
||||||
/// Calculate the length of the path up to a certain position
|
/// Calculate the length of the path up to a certain position
|
||||||
double size({double error = defaultError, int minDepth = defaultMinDepth});
|
num size({num error = defaultError, int minDepth = defaultMinDepth});
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class Bezier extends SvgPath {
|
abstract class Bezier extends SvgPath {
|
||||||
@@ -83,6 +94,12 @@ abstract class Bezier extends SvgPath {
|
|||||||
required Point end,
|
required Point end,
|
||||||
}) : super(start: start, end: end);
|
}) : super(start: start, end: end);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => other is Bezier && super == other;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => super.hashCode + 0;
|
||||||
|
|
||||||
/// Checks if this segment would be a smooth segment following the previous
|
/// Checks if this segment would be a smooth segment following the previous
|
||||||
bool isSmoothFrom(Object? previous);
|
bool isSmoothFrom(Object? previous);
|
||||||
}
|
}
|
||||||
@@ -95,11 +112,17 @@ class Linear extends SvgPath {
|
|||||||
required Point end,
|
required Point end,
|
||||||
}) : super(start: start, end: end);
|
}) : super(start: start, end: end);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => other is Linear && super == other;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => super.hashCode + 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Point point(num pos) => start + (end - start).times(pos);
|
Point point(num pos) => start + (end - start).times(pos);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
double size({double error = defaultError, int minDepth = defaultMinDepth}) {
|
num size({num error = defaultError, int minDepth = defaultMinDepth}) {
|
||||||
final distance = end - start;
|
final distance = end - start;
|
||||||
return sqrt(distance.x * distance.x + distance.y * distance.y);
|
return sqrt(distance.x * distance.x + distance.y * distance.y);
|
||||||
}
|
}
|
||||||
@@ -111,6 +134,12 @@ class Line extends Linear {
|
|||||||
required Point end,
|
required Point end,
|
||||||
}) : super(start: start, end: end);
|
}) : super(start: start, end: end);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => other is Line && super == other;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => super.hashCode + 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return "Line(start=$start, end=$end)";
|
return "Line(start=$start, end=$end)";
|
||||||
@@ -128,6 +157,16 @@ class CubicBezier extends Bezier {
|
|||||||
required Point end,
|
required Point end,
|
||||||
}) : super(start: start, end: end);
|
}) : super(start: start, end: end);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
other is CubicBezier &&
|
||||||
|
control1 == other.control1 &&
|
||||||
|
control2 == other.control2 &&
|
||||||
|
super == other;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => super.hashCode ^ control1.hashCode ^ control2.hashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => "CubicBezier(start=$start, control1=$control1, "
|
String toString() => "CubicBezier(start=$start, control1=$control1, "
|
||||||
"control2=$control2, end=$end)";
|
"control2=$control2, end=$end)";
|
||||||
@@ -146,18 +185,19 @@ class CubicBezier extends Bezier {
|
|||||||
end.times(math.pow(pos, 3));
|
end.times(math.pow(pos, 3));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
double size({double error = defaultError, int minDepth = defaultMinDepth}) {
|
num size({num error = defaultError, int minDepth = defaultMinDepth}) {
|
||||||
final startPoint = point(0);
|
final startPoint = point(0);
|
||||||
final endPoint = point(1);
|
final endPoint = point(1);
|
||||||
return segmentLength(
|
return segmentLength(
|
||||||
curve: this,
|
curve: this,
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 1,
|
end: 1,
|
||||||
startPoint: startPoint,
|
startPoint: startPoint,
|
||||||
endPoint: endPoint,
|
endPoint: endPoint,
|
||||||
error: error,
|
error: error,
|
||||||
minDepth: minDepth,
|
minDepth: minDepth,
|
||||||
depth: 0);
|
depth: 0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,10 +208,14 @@ class QuadraticBezier extends Bezier {
|
|||||||
required Point start,
|
required Point start,
|
||||||
required Point end,
|
required Point end,
|
||||||
required this.control,
|
required this.control,
|
||||||
}) : super(
|
}) : super(start: start, end: end);
|
||||||
start: start,
|
|
||||||
end: end,
|
@override
|
||||||
);
|
bool operator ==(Object other) =>
|
||||||
|
other is QuadraticBezier && control == other.control && super == other;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => super.hashCode ^ control.hashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
@@ -190,12 +234,12 @@ class QuadraticBezier extends Bezier {
|
|||||||
end.times(math.pow(pos, 2));
|
end.times(math.pow(pos, 2));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
double size({double error = defaultError, int minDepth = defaultMinDepth}) {
|
num size({num error = defaultError, int minDepth = defaultMinDepth}) {
|
||||||
final Point a = start - control.times(2) + end;
|
final Point a = start - control.times(2) + end;
|
||||||
final Point b = (control - start).times(2);
|
final Point b = (control - start).times(2);
|
||||||
final num aDotB = a.x * b.x + a.y * b.y;
|
final num aDotB = a.x * b.x + a.y * b.y;
|
||||||
|
|
||||||
late final double s;
|
late final num s;
|
||||||
if (a.abs() < 1e-12) {
|
if (a.abs() < 1e-12) {
|
||||||
s = b.abs();
|
s = b.abs();
|
||||||
} else if ((aDotB + a.abs() * b.abs()).abs() < 1e-12) {
|
} else if ((aDotB + a.abs() * b.abs()).abs() < 1e-12) {
|
||||||
@@ -208,11 +252,11 @@ class QuadraticBezier extends Bezier {
|
|||||||
final num B = 4 * (a.x * b.x + a.y * b.y);
|
final num B = 4 * (a.x * b.x + a.y * b.y);
|
||||||
final num C = b.x * b.x + b.y * b.y;
|
final num C = b.x * b.x + b.y * b.y;
|
||||||
|
|
||||||
final double sabc = 2 * sqrt(A + B + C);
|
final num sabc = 2 * sqrt(A + B + C);
|
||||||
final double a2 = sqrt(A);
|
final num a2 = sqrt(A);
|
||||||
final double a32 = 2 * A * a2;
|
final num a32 = 2 * A * a2;
|
||||||
final double c2 = 2 * sqrt(C);
|
final num c2 = 2 * sqrt(C);
|
||||||
final double bA = B / a2;
|
final num bA = B / a2;
|
||||||
|
|
||||||
s = (a32 * sabc +
|
s = (a32 * sabc +
|
||||||
a2 * B * (sabc - c2) +
|
a2 * B * (sabc - c2) +
|
||||||
@@ -227,7 +271,7 @@ class QuadraticBezier extends Bezier {
|
|||||||
/// large and sweep are 1 or 0 (True/False also work)
|
/// large and sweep are 1 or 0 (True/False also work)
|
||||||
class Arc extends SvgPath {
|
class Arc extends SvgPath {
|
||||||
final Point radius;
|
final Point radius;
|
||||||
final double rotation;
|
final num rotation;
|
||||||
final bool arc;
|
final bool arc;
|
||||||
final bool sweep;
|
final bool sweep;
|
||||||
late final num radiusScale;
|
late final num radiusScale;
|
||||||
@@ -246,6 +290,23 @@ class Arc extends SvgPath {
|
|||||||
_parameterize();
|
_parameterize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
other is Arc &&
|
||||||
|
radius == other.radius &&
|
||||||
|
rotation == other.rotation &&
|
||||||
|
arc == other.arc &&
|
||||||
|
sweep == other.sweep &&
|
||||||
|
super == other;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
super.hashCode ^
|
||||||
|
radius.hashCode ^
|
||||||
|
rotation.hashCode ^
|
||||||
|
arc.hashCode ^
|
||||||
|
sweep.hashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'Arc(start=$start, radius=$radius, rotation=$rotation, '
|
String toString() => 'Arc(start=$start, radius=$radius, rotation=$rotation, '
|
||||||
'arc=$arc, sweep=$sweep, end=$end)';
|
'arc=$arc, sweep=$sweep, end=$end)';
|
||||||
@@ -289,7 +350,7 @@ class Arc extends SvgPath {
|
|||||||
|
|
||||||
final t1 = rxSq * y1primSq;
|
final t1 = rxSq * y1primSq;
|
||||||
final t2 = rySq * x1primSq;
|
final t2 = rySq * x1primSq;
|
||||||
double c = sqrt(((rxSq * rySq - t1 - t2) / (t1 + t2)).abs());
|
num c = sqrt(((rxSq * rySq - t1 - t2) / (t1 + t2)).abs());
|
||||||
|
|
||||||
if (arc == sweep) {
|
if (arc == sweep) {
|
||||||
c = -c;
|
c = -c;
|
||||||
@@ -352,7 +413,7 @@ class Arc extends SvgPath {
|
|||||||
/// integration, and in that case it's simpler to just do a geometric
|
/// integration, and in that case it's simpler to just do a geometric
|
||||||
/// approximation, as for cubic bezier curves.
|
/// approximation, as for cubic bezier curves.
|
||||||
@override
|
@override
|
||||||
double size({double error = defaultError, minDepth = defaultMinDepth}) {
|
num size({num error = defaultError, minDepth = defaultMinDepth}) {
|
||||||
// This is equivalent of omitting the segment
|
// This is equivalent of omitting the segment
|
||||||
if (start == end) return 0;
|
if (start == end) return 0;
|
||||||
|
|
||||||
@@ -387,6 +448,13 @@ class Arc extends SvgPath {
|
|||||||
class Move extends SvgPath {
|
class Move extends SvgPath {
|
||||||
const Move({required Point to}) : super(start: to, end: to);
|
const Move({required Point to}) : super(start: to, end: to);
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => other is Move && super == other;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => super.hashCode + 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => "Move(to=$start)";
|
String toString() => "Move(to=$start)";
|
||||||
|
|
||||||
@@ -394,8 +462,7 @@ class Move extends SvgPath {
|
|||||||
Point point(num pos) => start;
|
Point point(num pos) => start;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
double size({double error = defaultError, int minDepth = defaultMinDepth}) =>
|
num size({num error = defaultError, int minDepth = defaultMinDepth}) => 0;
|
||||||
0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the closepath command
|
/// Represents the closepath command
|
||||||
@@ -405,10 +472,12 @@ class Close extends Linear {
|
|||||||
required Point end,
|
required Point end,
|
||||||
}) : super(start: start, end: end);
|
}) : super(start: start, end: end);
|
||||||
|
|
||||||
// def __eq__(self, other):
|
|
||||||
// if not isinstance(other, Close):
|
@override
|
||||||
// return NotImplemented
|
bool operator ==(Object other) => other is Close && super == other;
|
||||||
// return self.start == other.start and self.end == other.end
|
|
||||||
|
@override
|
||||||
|
int get hashCode => super.hashCode + 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => "Close(start=$start, end=$end)";
|
String toString() => "Close(start=$start, end=$end)";
|
||||||
@@ -425,6 +494,14 @@ class Path extends ListBase<SvgPath> {
|
|||||||
segments = [];
|
segments = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Path.fromSegments(this.segments);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => other is Path && segments == other.segments;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => segments.hashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
SvgPath operator [](int index) => segments[index]!;
|
SvgPath operator [](int index) => segments[index]!;
|
||||||
|
|
||||||
@@ -445,7 +522,7 @@ class Path extends ListBase<SvgPath> {
|
|||||||
'Path(${[for (final s in segments) s.toString()].join(", ")})';
|
'Path(${[for (final s in segments) s.toString()].join(", ")})';
|
||||||
|
|
||||||
void _calcLengths(
|
void _calcLengths(
|
||||||
{double error = defaultError, int minDepth = defaultMinDepth}) {
|
{num error = defaultError, int minDepth = defaultMinDepth}) {
|
||||||
if (_memoizedLength != null) return;
|
if (_memoizedLength != null) return;
|
||||||
|
|
||||||
final lengths = [
|
final lengths = [
|
||||||
@@ -466,7 +543,7 @@ class Path extends ListBase<SvgPath> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Point point({required num pos, double error = defaultError}) {
|
Point point({required num pos, num error = defaultError}) {
|
||||||
// Shortcuts
|
// Shortcuts
|
||||||
if (pos == 0.0) {
|
if (pos == 0.0) {
|
||||||
return segments[0]!.point(pos);
|
return segments[0]!.point(pos);
|
||||||
@@ -505,7 +582,7 @@ class Path extends ListBase<SvgPath> {
|
|||||||
SvgPath? previousSegment;
|
SvgPath? previousSegment;
|
||||||
final end = last.end;
|
final end = last.end;
|
||||||
|
|
||||||
String formatNumber(num n) => n.toString();
|
String formatNumber(num n) => n.removePointIfInt.toString();
|
||||||
String coord(Point p) => '${formatNumber(p.x)},${formatNumber(p.y)}';
|
String coord(Point p) => '${formatNumber(p.x)},${formatNumber(p.y)}';
|
||||||
|
|
||||||
for (final segment in this) {
|
for (final segment in this) {
|
||||||
@@ -518,7 +595,7 @@ class Path extends ListBase<SvgPath> {
|
|||||||
} else if (segment is Move ||
|
} else if (segment is Move ||
|
||||||
(currentPos != start) ||
|
(currentPos != start) ||
|
||||||
(start == end && previousSegment is! Move)) {
|
(start == end && previousSegment is! Move)) {
|
||||||
parts.add("M ${coord(start)}");
|
parts.add("M ${coord(segment.start)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (segment is Line) {
|
if (segment is Line) {
|
||||||
@@ -540,7 +617,7 @@ class Path extends ListBase<SvgPath> {
|
|||||||
} else if (segment is Arc) {
|
} else if (segment is Arc) {
|
||||||
parts.add(
|
parts.add(
|
||||||
"A ${coord(segment.radius)} ${formatNumber(segment.rotation)} "
|
"A ${coord(segment.radius)} ${formatNumber(segment.rotation)} "
|
||||||
"${(segment.arc ? 1 : 0).toDouble},${(segment.sweep ? 1 : 0).toDouble} ${coord(end)}",
|
"${segment.arc ? 1 : 0},${segment.sweep ? 1 : 0} ${coord(segment.end)}",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,6 +625,6 @@ class Path extends ListBase<SvgPath> {
|
|||||||
previousSegment = segment;
|
previousSegment = segment;
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.join(" ");
|
return parts.join(" ").toUpperCase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +0,0 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import 'package:kanimaji/kanimaji.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
test('adds one to input values', () {
|
|
||||||
final calculator = Calculator();
|
|
||||||
expect(calculator.addOne(2), 3);
|
|
||||||
expect(calculator.addOne(-7), -6);
|
|
||||||
expect(calculator.addOne(0), 1);
|
|
||||||
});
|
|
||||||
}
|
|
279
test/svg/parser_test.dart
Normal file
279
test/svg/parser_test.dart
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:kanimaji/common/Point.dart';
|
||||||
|
import 'package:kanimaji/svg/parser.dart';
|
||||||
|
import 'package:kanimaji/svg/path.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
/// """Examples from the SVG spec"""
|
||||||
|
test("svg_examples", () {
|
||||||
|
Path path1 = parsePath("M 100 100 L 300 100 L 200 300 z");
|
||||||
|
expect(
|
||||||
|
path1,
|
||||||
|
Path.fromSegments([
|
||||||
|
const Move(to: Point(100, 100)),
|
||||||
|
const Line(start: Point(100, 100), end: Point(300, 100)),
|
||||||
|
const Line(start: Point(300, 100), end: Point(200, 300)),
|
||||||
|
const Close(start: Point(200, 300), end: Point(100, 100)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// for Z command behavior when there is multiple subpaths
|
||||||
|
path1 = parsePath("M 0 0 L 50 20 M 100 100 L 300 100 L 200 300 z");
|
||||||
|
expect(
|
||||||
|
path1,
|
||||||
|
Path.fromSegments(const [
|
||||||
|
Move(to: Point.zero),
|
||||||
|
Line(start: Point.zero, end: Point(50, 20)),
|
||||||
|
Move(to: Point(100, 100)),
|
||||||
|
Line(start: Point(100, 100), end: Point(300, 100)),
|
||||||
|
Line(start: Point(300, 100), end: Point(200, 300)),
|
||||||
|
Close(start: Point(200, 300), end: Point(100, 100)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
path1 = parsePath("M 100 100 L 200 200");
|
||||||
|
Path path2 = parsePath("M100 100L200 200");
|
||||||
|
expect(path1, path2);
|
||||||
|
|
||||||
|
path1 = parsePath("M 100 200 L 200 100 L -100 -200");
|
||||||
|
path2 = parsePath("M 100 200 L 200 100 -100 -200");
|
||||||
|
expect(path1, path2);
|
||||||
|
|
||||||
|
path1 = parsePath("""M100,200 C100,100 250,100 250,200
|
||||||
|
S400,300 400,200""");
|
||||||
|
expect(
|
||||||
|
path1,
|
||||||
|
Path.fromSegments(const [
|
||||||
|
Move(to: Point(100, 200)),
|
||||||
|
CubicBezier(
|
||||||
|
start: Point(100, 200),
|
||||||
|
control1: Point(100, 100),
|
||||||
|
control2: Point(250, 100),
|
||||||
|
end: Point(250, 200),
|
||||||
|
),
|
||||||
|
CubicBezier(
|
||||||
|
start: Point(250, 200),
|
||||||
|
control1: Point(250, 300),
|
||||||
|
control2: Point(400, 300),
|
||||||
|
end: Point(400, 200),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
path1 = parsePath("M100,200 C100,100 400,100 400,200");
|
||||||
|
expect(
|
||||||
|
path1,
|
||||||
|
Path.fromSegments(const [
|
||||||
|
Move(to: Point(100, 200)),
|
||||||
|
CubicBezier(
|
||||||
|
start: Point(100, 200),
|
||||||
|
control1: Point(100, 100),
|
||||||
|
control2: Point(400, 100),
|
||||||
|
end: Point(400, 200),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
path1 = parsePath("M100,500 C25,400 475,400 400,500");
|
||||||
|
expect(
|
||||||
|
path1,
|
||||||
|
Path.fromSegments(const [
|
||||||
|
Move(to: Point(100, 500)),
|
||||||
|
CubicBezier(
|
||||||
|
start: Point(100, 500),
|
||||||
|
control1: Point(25, 400),
|
||||||
|
control2: Point(475, 400),
|
||||||
|
end: Point(400, 500),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// path1 = parse_path("M100,800 C175,700 325,700 400,800")
|
||||||
|
// self.assertEqual(
|
||||||
|
// path1,
|
||||||
|
// Path(
|
||||||
|
// Move(100 + 800j),
|
||||||
|
// CubicBezier(100 + 800j, 175 + 700j, 325 + 700j, 400 + 800j),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
|
||||||
|
// path1 = parse_path("M600,200 C675,100 975,100 900,200")
|
||||||
|
// self.assertEqual(
|
||||||
|
// path1,
|
||||||
|
// Path(
|
||||||
|
// Move(600 + 200j),
|
||||||
|
// CubicBezier(600 + 200j, 675 + 100j, 975 + 100j, 900 + 200j),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
|
||||||
|
// path1 = parse_path("M600,500 C600,350 900,650 900,500")
|
||||||
|
// self.assertEqual(
|
||||||
|
// path1,
|
||||||
|
// Path(
|
||||||
|
// Move(600 + 500j),
|
||||||
|
// CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
|
||||||
|
// path1 = parse_path(
|
||||||
|
// """M600,800 C625,700 725,700 750,800
|
||||||
|
// S875,900 900,800"""
|
||||||
|
// )
|
||||||
|
// self.assertEqual(
|
||||||
|
// path1,
|
||||||
|
// Path(
|
||||||
|
// Move(600 + 800j),
|
||||||
|
// CubicBezier(600 + 800j, 625 + 700j, 725 + 700j, 750 + 800j),
|
||||||
|
// CubicBezier(750 + 800j, 775 + 900j, 875 + 900j, 900 + 800j),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
|
||||||
|
// path1 = parse_path("M200,300 Q400,50 600,300 T1000,300")
|
||||||
|
// self.assertEqual(
|
||||||
|
// path1,
|
||||||
|
// Path(
|
||||||
|
// Move(200 + 300j),
|
||||||
|
// QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j),
|
||||||
|
// QuadraticBezier(600 + 300j, 800 + 550j, 1000 + 300j),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
|
||||||
|
// path1 = parse_path("M300,200 h-150 a150,150 0 1,0 150,-150 z")
|
||||||
|
// self.assertEqual(
|
||||||
|
// path1,
|
||||||
|
// Path(
|
||||||
|
// Move(300 + 200j),
|
||||||
|
// Line(300 + 200j, 150 + 200j),
|
||||||
|
// Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j),
|
||||||
|
// Close(300 + 50j, 300 + 200j),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
|
||||||
|
// path1 = parse_path("M275,175 v-150 a150,150 0 0,0 -150,150 z")
|
||||||
|
// self.assertEqual(
|
||||||
|
// path1,
|
||||||
|
// Path(
|
||||||
|
// Move(275 + 175j),
|
||||||
|
// Line(275 + 175j, 275 + 25j),
|
||||||
|
// Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j),
|
||||||
|
// Close(125 + 175j, 275 + 175j),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
|
||||||
|
// path1 = parse_path("M275,175 v-150 a150,150 0 0,0 -150,150 L 275,175 z")
|
||||||
|
// self.assertEqual(
|
||||||
|
// path1,
|
||||||
|
// Path(
|
||||||
|
// Move(275 + 175j),
|
||||||
|
// Line(275 + 175j, 275 + 25j),
|
||||||
|
// Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j),
|
||||||
|
// Line(125 + 175j, 275 + 175j),
|
||||||
|
// Close(275 + 175j, 275 + 175j),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
|
||||||
|
// path1 = parse_path(
|
||||||
|
// """M600,350 l 50,-25
|
||||||
|
// a25,25 -30 0,1 50,-25 l 50,-25
|
||||||
|
// a25,50 -30 0,1 50,-25 l 50,-25
|
||||||
|
// a25,75 -30 0,1 50,-25 l 50,-25
|
||||||
|
// a25,100 -30 0,1 50,-25 l 50,-25"""
|
||||||
|
// )
|
||||||
|
// self.assertEqual(
|
||||||
|
// path1,
|
||||||
|
// Path(
|
||||||
|
// Move(600 + 350j),
|
||||||
|
// Line(600 + 350j, 650 + 325j),
|
||||||
|
// Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j),
|
||||||
|
// Line(700 + 300j, 750 + 275j),
|
||||||
|
// Arc(750 + 275j, 25 + 50j, -30, 0, 1, 800 + 250j),
|
||||||
|
// Line(800 + 250j, 850 + 225j),
|
||||||
|
// Arc(850 + 225j, 25 + 75j, -30, 0, 1, 900 + 200j),
|
||||||
|
// Line(900 + 200j, 950 + 175j),
|
||||||
|
// Arc(950 + 175j, 25 + 100j, -30, 0, 1, 1000 + 150j),
|
||||||
|
// Line(1000 + 150j, 1050 + 125j),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
});
|
||||||
|
|
||||||
|
// def test_others(self):
|
||||||
|
// # Other paths that need testing:
|
||||||
|
|
||||||
|
// # Relative moveto:
|
||||||
|
// path1 = parse_path("M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z")
|
||||||
|
// self.assertEqual(
|
||||||
|
// path1,
|
||||||
|
// Path(
|
||||||
|
// Move(0j),
|
||||||
|
// Line(0 + 0j, 50 + 20j),
|
||||||
|
// Move(100 + 100j),
|
||||||
|
// Line(100 + 100j, 300 + 100j),
|
||||||
|
// Line(300 + 100j, 200 + 300j),
|
||||||
|
// Close(200 + 300j, 100 + 100j),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
|
||||||
|
// # Initial smooth and relative CubicBezier
|
||||||
|
// path1 = parse_path("""M100,200 s 150,-100 150,0""")
|
||||||
|
// self.assertEqual(
|
||||||
|
// path1,
|
||||||
|
// Path(
|
||||||
|
// Move(100 + 200j),
|
||||||
|
// CubicBezier(100 + 200j, 100 + 200j, 250 + 100j, 250 + 200j),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
|
||||||
|
// # Initial smooth and relative QuadraticBezier
|
||||||
|
// path1 = parse_path("""M100,200 t 150,0""")
|
||||||
|
// self.assertEqual(
|
||||||
|
// path1,
|
||||||
|
// Path(Move(100 + 200j), QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j)),
|
||||||
|
// )
|
||||||
|
|
||||||
|
// # Relative QuadraticBezier
|
||||||
|
// path1 = parse_path("""M100,200 q 0,0 150,0""")
|
||||||
|
// self.assertEqual(
|
||||||
|
// path1,
|
||||||
|
// Path(Move(100 + 200j), QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j)),
|
||||||
|
// )
|
||||||
|
|
||||||
|
// def test_negative(self):
|
||||||
|
// """You don't need spaces before a minus-sign"""
|
||||||
|
// path1 = parse_path("M100,200c10-5,20-10,30-20")
|
||||||
|
// path2 = parse_path("M 100 200 c 10 -5 20 -10 30 -20")
|
||||||
|
// self.assertEqual(path1, path2)
|
||||||
|
|
||||||
|
// def test_numbers(self):
|
||||||
|
// """Exponents and other number format cases"""
|
||||||
|
// # It can be e or E, the plus is optional, and a minimum of +/-3.4e38 must be supported.
|
||||||
|
// path1 = parse_path("M-3.4e38 3.4E+38L-3.4E-38,3.4e-38")
|
||||||
|
// path2 = Path(
|
||||||
|
// Move(-3.4e38 + 3.4e38j), Line(-3.4e38 + 3.4e38j, -3.4e-38 + 3.4e-38j)
|
||||||
|
// )
|
||||||
|
// self.assertEqual(path1, path2)
|
||||||
|
|
||||||
|
// def test_errors(self):
|
||||||
|
// self.assertRaises(ValueError, parse_path, "M 100 100 L 200 200 Z 100 200")
|
||||||
|
|
||||||
|
// def test_non_path(self):
|
||||||
|
// # It's possible in SVG to create paths that has zero length,
|
||||||
|
// # we need to handle that.
|
||||||
|
|
||||||
|
// path = parse_path("M10.236,100.184")
|
||||||
|
// self.assertEqual(path.d(), "M 10.236,100.184")
|
||||||
|
|
||||||
|
// def test_issue_45(self):
|
||||||
|
// path = parse_path(
|
||||||
|
// "m 1672.2372,-54.8161 "
|
||||||
|
// "a 14.5445,14.5445 0 0 0 -11.3152,23.6652 "
|
||||||
|
// "l 27.2573,27.2572 27.2572,-27.2572 "
|
||||||
|
// "a 14.5445,14.5445 0 0 0 -11.3012,-23.634 "
|
||||||
|
// "a 14.5445,14.5445 0 0 0 -11.414,5.4625 "
|
||||||
|
// "l -4.542,4.5420 "
|
||||||
|
// "l -4.5437,-4.5420 "
|
||||||
|
// "a 14.5445,14.5445 0 0 0 -11.3984,-5.4937 "
|
||||||
|
// "z"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// self.assertIn("A 14.5445,14.5445 0 0,0 1672.24,-54.8161 Z", path.d())
|
||||||
|
}
|
28
test/svg/svg_test.dart
Normal file
28
test/svg/svg_test.dart
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:kanimaji/svg/parser.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('Test SVG Paths', () {
|
||||||
|
final paths = [
|
||||||
|
"M 100,100 L 300,100 L 200,300 Z",
|
||||||
|
"M 0,0 L 50,20 M 100,100 L 300,100 L 200,300 Z",
|
||||||
|
"M 100,100 L 200,200",
|
||||||
|
"M 100,200 L 200,100 L -100,-200",
|
||||||
|
"M 100,200 C 100,100 250,100 250,200 S 400,300 400,200",
|
||||||
|
"M 100,200 C 100,100 400,100 400,200",
|
||||||
|
"M 100,500 C 25,400 475,400 400,500",
|
||||||
|
"M 100,800 C 175,700 325,700 400,800",
|
||||||
|
"M 600,200 C 675,100 975,100 900,200",
|
||||||
|
"M 600,500 C 600,350 900,650 900,500",
|
||||||
|
"M 600,800 C 625,700 725,700 750,800 S 875,900 900,800",
|
||||||
|
"M 200,300 Q 400,50 600,300 T 1000,300",
|
||||||
|
"M -3.4E+38,3.4E+38 L -3.4E-38,3.4E-38",
|
||||||
|
"M 0,0 L 50,20 M 50,20 L 200,100 Z",
|
||||||
|
"M 600,350 L 650,325 A 25,25 -30 0,1 700,300 L 750,275",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final path in paths) {
|
||||||
|
expect(parsePath(path).d(), path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Reference in New Issue
Block a user