| // This code has been "translated" largely from the Chromium/blink source |
| // for SVG path parsing. |
| // The following files can be cross referenced to the classes and methods here: |
| // * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_parser_utilities.cc |
| // * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_parser_utilities.h |
| // * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_path_string_source.cc |
| // * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_path_string_source.h |
| // * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_path_parser.cc |
| // * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_path_parser.h |
| // * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/html/parser/html_parser_idioms.h (IsHTMLSpace) |
| // * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_path_parser_test.cc |
| |
| import 'dart:math' as math show sqrt, max, pi, tan, sin, cos, pow, atan2; |
| |
| import 'package:meta/meta.dart'; |
| import 'package:vector_math/vector_math.dart' show Matrix4; |
| |
| import './path_segment_type.dart'; |
| |
| /// Parse `svg`, emitting the segment data to `path`. |
| void writeSvgPathDataToPath(String svg, PathProxy path) { |
| if (svg == null || svg == '') { |
| return; |
| } |
| |
| final SvgPathStringSource parser = new SvgPathStringSource(svg); |
| final SvgPathNormalizer normalizer = new SvgPathNormalizer(); |
| for (PathSegmentData seg in parser.parseSegments()) { |
| normalizer.emitSegment(seg, path); |
| } |
| } |
| |
| /// A receiver for normalized [PathSegmentData]. |
| abstract class PathProxy { |
| void moveTo(double x, double y); |
| void lineTo(double x, double y); |
| void cubicTo( |
| double x1, |
| double y1, |
| double x2, |
| double y2, |
| double x3, |
| double y3, |
| ); |
| void close(); |
| } |
| |
| /// Provides a minimal implementation of a [Point] or [Offset]. |
| // Takes care of a few things Point doesn't, without requiring Flutter as dependency |
| @immutable |
| class _PathOffset { |
| const _PathOffset(this.dx, this.dy) |
| : assert(dx != null), |
| assert(dy != null); |
| |
| static _PathOffset get zero => const _PathOffset(0.0, 0.0); |
| final double dx; |
| final double dy; |
| |
| double get direction => math.atan2(dy, dx); |
| |
| _PathOffset translate(double translateX, double translateY) => |
| new _PathOffset(dx + translateX, dy + translateY); |
| |
| _PathOffset operator +(_PathOffset other) => |
| new _PathOffset(dx + other.dx, dy + other.dy); |
| _PathOffset operator -(_PathOffset other) => |
| new _PathOffset(dx - other.dx, dy - other.dy); |
| |
| _PathOffset operator *(double operand) => |
| new _PathOffset(dx * operand, dy * operand); |
| |
| @override |
| String toString() { |
| return 'PathOffset{$dx,$dy}'; |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| return other is _PathOffset && other.dx == dx && other.dy == dy; |
| } |
| |
| // TODO(dnfield): Use a real hashing function - but this should at least be better than the default. |
| @override |
| int get hashCode => (((17 * 23) ^ dx.hashCode) * 23) ^ dy.hashCode; |
| } |
| |
| const double _twoPiFloat = math.pi * 2.0; |
| const double _piOverTwoFloat = math.pi / 2.0; |
| |
| class SvgPathStringSource { |
| SvgPathStringSource(String string) : assert(string != null) { |
| _previousCommand = SvgPathSegType.unknown; |
| _codePoints = string.codeUnits; |
| _idx = 0; |
| _skipOptionalSvgSpaces(); |
| } |
| |
| SvgPathSegType _previousCommand; |
| List<int> _codePoints; |
| int _idx; |
| |
| bool _isHtmlSpace(int character) { |
| // Histogram from Apple's page load test combined with some ad hoc browsing |
| // some other test suites. |
| // |
| // 82%: 216330 non-space characters, all > U+0020 |
| // 11%: 30017 plain space characters, U+0020 |
| // 5%: 12099 newline characters, U+000A |
| // 2%: 5346 tab characters, U+0009 |
| // |
| // No other characters seen. No U+000C or U+000D, and no other control |
| // characters. Accordingly, we check for non-spaces first, then space, then |
| // newline, then tab, then the other characters. |
| |
| return character <= AsciiConstants.space && |
| (character == AsciiConstants.space || |
| character == AsciiConstants.slashN || |
| character == AsciiConstants.slashT || |
| character == AsciiConstants.slashR || |
| character == AsciiConstants.slashF); |
| } |
| |
| bool _skipOptionalSvgSpaces() { |
| while (_idx < _codePoints.length && _isHtmlSpace(_codePoints[_idx])) { |
| _idx++; |
| } |
| return _idx < _codePoints.length; |
| } |
| |
| bool _skipOptionalSvgSpacesOrDelimiter( |
| [int delimiter = AsciiConstants.comma]) { |
| if (_idx < _codePoints.length && |
| !_isHtmlSpace(_codePoints[_idx]) && |
| _codePoints[_idx] != delimiter) { |
| return false; |
| } |
| if (_skipOptionalSvgSpaces()) { |
| if (_idx < _codePoints.length && _codePoints[_idx] == delimiter) { |
| _idx++; |
| _skipOptionalSvgSpaces(); |
| } |
| } |
| return _idx < _codePoints.length; |
| } |
| |
| bool _isNumberStart(int lookahead) { |
| return (lookahead >= AsciiConstants.number0 && |
| lookahead <= AsciiConstants.number9) || |
| lookahead == AsciiConstants.plus || |
| lookahead == AsciiConstants.minus || |
| lookahead == AsciiConstants.period; |
| } |
| |
| SvgPathSegType _maybeImplicitCommand( |
| int lookahead, SvgPathSegType nextCommand) { |
| // Check if the current lookahead may start a number - in which case it |
| // could be the start of an implicit command. The 'close' command does not |
| // have any parameters though and hence can't have an implicit |
| // 'continuation'. |
| if (!_isNumberStart(lookahead) || _previousCommand == SvgPathSegType.close) |
| return nextCommand; |
| // Implicit continuations of moveto command translate to linetos. |
| if (_previousCommand == SvgPathSegType.moveToAbs) { |
| return SvgPathSegType.lineToAbs; |
| } |
| if (_previousCommand == SvgPathSegType.moveToRel) { |
| return SvgPathSegType.lineToRel; |
| } |
| return _previousCommand; |
| } |
| |
| bool _isValidRange(double x) { |
| return x >= -double.maxFinite && x <= double.maxFinite; |
| } |
| |
| bool _isValidExponent(double x) { |
| return x >= -37 && x <= 38; |
| } |
| |
| // We use this generic parseNumber function to allow the Path parsing code to |
| // work at a higher precision internally, without any unnecessary runtime cost |
| // or code complexity. |
| double _parseNumber() { |
| _skipOptionalSvgSpaces(); |
| |
| // read the sign |
| int sign = 1; |
| final int end = _codePoints.length; |
| if (_idx < end && _codePoints[_idx] == AsciiConstants.plus) |
| _idx++; |
| else if (_idx < end && _codePoints[_idx] == AsciiConstants.minus) { |
| _idx++; |
| sign = -1; |
| } |
| |
| if (_idx == end || |
| ((_codePoints[_idx] < AsciiConstants.number0 || |
| _codePoints[_idx] > AsciiConstants.number9) && |
| _codePoints[_idx] != AsciiConstants.period)) |
| // The first character of a number must be one of [0-9+-.] |
| throw new StateError( |
| 'First character of a number must be one of [0-9+-.]'); |
| |
| // read the integer part, build right-to-left |
| final int digitsStart = _idx; |
| while (_idx < end && |
| _codePoints[_idx] >= AsciiConstants.number0 && |
| _codePoints[_idx] <= AsciiConstants.number9) |
| ++_idx; // Advance to first non-digit. |
| |
| double integer = 0.0; |
| if (_idx != digitsStart) { |
| int ptrScanIntPart = _idx - 1; |
| int multiplier = 1; |
| while (ptrScanIntPart >= digitsStart) { |
| integer += multiplier * |
| (_codePoints[ptrScanIntPart--] - AsciiConstants.number0); |
| |
| multiplier *= 10; |
| } |
| // Bail out early if this overflows. |
| if (!_isValidRange(integer)) { |
| throw new StateError('Numeric overflow'); |
| } |
| } |
| |
| double decimal = 0.0; |
| if (_idx < end && _codePoints[_idx] == AsciiConstants.period) { |
| // read the decimals |
| _idx++; |
| |
| // There must be a least one digit following the . |
| if (_idx >= end || |
| _codePoints[_idx] < AsciiConstants.number0 || |
| _codePoints[_idx] > AsciiConstants.number9) |
| throw new StateError( |
| 'There must be at least one digit following the .'); |
| |
| double frac = 1.0; |
| while (_idx < end && |
| _codePoints[_idx] >= AsciiConstants.number0 && |
| _codePoints[_idx] <= AsciiConstants.number9) { |
| frac *= 0.1; |
| decimal += (_codePoints[_idx++] - AsciiConstants.number0) * frac; |
| } |
| } |
| |
| // When we get here we should have consumed either a digit for the integer |
| // part or a fractional part (with at least one digit after the '.'.) |
| assert(digitsStart != _idx); |
| |
| double number = integer + decimal; |
| number *= sign; |
| |
| // read the exponent part |
| if (_idx + 1 < end && |
| (_codePoints[_idx] == AsciiConstants.lowerE || |
| _codePoints[_idx] == AsciiConstants.upperE) && |
| (_codePoints[_idx + 1] != AsciiConstants.lowerX && |
| _codePoints[_idx + 1] != AsciiConstants.lowerM)) { |
| _idx++; |
| |
| // read the sign of the exponent |
| bool exponentIsNegative = false; |
| if (_codePoints[_idx] == AsciiConstants.plus) |
| _idx++; |
| else if (_codePoints[_idx] == AsciiConstants.minus) { |
| _idx++; |
| exponentIsNegative = true; |
| } |
| |
| // There must be an exponent |
| if (_idx >= end || |
| _codePoints[_idx] < AsciiConstants.number0 || |
| _codePoints[_idx] > AsciiConstants.number9) |
| throw new StateError('Missing exponent'); |
| |
| double exponent = 0.0; |
| while (_idx < end && |
| _codePoints[_idx] >= AsciiConstants.number0 && |
| _codePoints[_idx] <= AsciiConstants.number9) { |
| exponent *= 10.0; |
| exponent += _codePoints[_idx] - AsciiConstants.number0; |
| _idx++; |
| } |
| if (exponentIsNegative) { |
| exponent = -exponent; |
| } |
| // Make sure exponent is valid. |
| if (!_isValidExponent(exponent)) { |
| throw new StateError('Invalid exponent $exponent'); |
| } |
| if (exponent != 0) { |
| number *= math.pow(10.0, exponent); |
| } |
| } |
| |
| // Don't return Infinity() or NaN(). |
| if (!_isValidRange(number)) { |
| throw new StateError('Numeric overflow'); |
| } |
| |
| // if (mode & kAllowTrailingWhitespace) |
| _skipOptionalSvgSpacesOrDelimiter(); |
| |
| return number; |
| } |
| |
| bool _parseArcFlag() { |
| if (!hasMoreData) { |
| throw new StateError('Expected more data'); |
| } |
| final int flagChar = _codePoints[_idx]; |
| _idx++; |
| _skipOptionalSvgSpacesOrDelimiter(); |
| |
| if (flagChar == AsciiConstants.number0) |
| return false; |
| else if (flagChar == AsciiConstants.number1) |
| return true; |
| else |
| throw new StateError('Invalid flag value'); |
| } |
| |
| bool get hasMoreData => _idx < _codePoints.length; |
| |
| Iterable<PathSegmentData> parseSegments() sync* { |
| while (hasMoreData) { |
| yield parseSegment(); |
| } |
| } |
| |
| PathSegmentData parseSegment() { |
| assert(hasMoreData); |
| final PathSegmentData segment = new PathSegmentData(); |
| final int lookahead = _codePoints[_idx]; |
| SvgPathSegType command = AsciiConstants.mapLetterToSegmentType(lookahead); |
| if (_previousCommand == SvgPathSegType.unknown) { |
| // First command has to be a moveto. |
| if (command != SvgPathSegType.moveToRel && |
| command != SvgPathSegType.moveToAbs) { |
| throw new StateError('Expected to find moveTo command'); |
| // SetErrorMark(SVGParseStatus::kExpectedMoveToCommand); |
| // return segment; |
| } |
| // Consume command letter. |
| _idx++; |
| } else if (command == SvgPathSegType.unknown) { |
| // Possibly an implicit command. |
| assert(_previousCommand != SvgPathSegType.unknown); |
| command = _maybeImplicitCommand(lookahead, command); |
| if (command == null || command == SvgPathSegType.unknown) { |
| throw new StateError('Expected a path command'); |
| } |
| } else { |
| // Valid explicit command. |
| _idx++; |
| } |
| |
| segment.command = _previousCommand = command; |
| |
| switch (segment.command) { |
| case SvgPathSegType.cubicToRel: |
| case SvgPathSegType.cubicToAbs: |
| segment.point1 = new _PathOffset(_parseNumber(), _parseNumber()); |
| continue cubic_smooth; |
| case SvgPathSegType.smoothCubicToRel: |
| cubic_smooth: |
| case SvgPathSegType.smoothCubicToAbs: |
| segment.point2 = new _PathOffset(_parseNumber(), _parseNumber()); |
| continue quad_smooth; |
| case SvgPathSegType.moveToRel: |
| case SvgPathSegType.moveToAbs: |
| case SvgPathSegType.lineToRel: |
| case SvgPathSegType.lineToAbs: |
| case SvgPathSegType.smoothQuadToRel: |
| quad_smooth: |
| case SvgPathSegType.smoothQuadToAbs: |
| segment.targetPoint = new _PathOffset(_parseNumber(), _parseNumber()); |
| break; |
| case SvgPathSegType.lineToHorizontalRel: |
| case SvgPathSegType.lineToHorizontalAbs: |
| segment.targetPoint = |
| new _PathOffset(_parseNumber(), segment.targetPoint?.dy ?? 0.0); |
| break; |
| case SvgPathSegType.lineToVerticalRel: |
| case SvgPathSegType.lineToVerticalAbs: |
| segment.targetPoint = |
| new _PathOffset(segment.targetPoint?.dx ?? 0.0, _parseNumber()); |
| break; |
| case SvgPathSegType.close: |
| _skipOptionalSvgSpaces(); |
| break; |
| case SvgPathSegType.quadToRel: |
| case SvgPathSegType.quadToAbs: |
| segment.point1 = new _PathOffset(_parseNumber(), _parseNumber()); |
| segment.targetPoint = new _PathOffset(_parseNumber(), _parseNumber()); |
| break; |
| case SvgPathSegType.arcToRel: |
| case SvgPathSegType.arcToAbs: |
| segment.point1 = new _PathOffset(_parseNumber(), _parseNumber()); |
| segment.arcAngle = _parseNumber(); |
| segment.arcLarge = _parseArcFlag(); |
| segment.arcSweep = _parseArcFlag(); |
| segment.targetPoint = new _PathOffset(_parseNumber(), _parseNumber()); |
| break; |
| case SvgPathSegType.unknown: |
| throw new StateError('Unknown segment command'); |
| } |
| |
| return segment; |
| } |
| } |
| |
| class OffsetHelper { |
| static _PathOffset reflectedPoint( |
| _PathOffset reflectedIn, _PathOffset pointToReflect) { |
| return new _PathOffset(2 * reflectedIn.dx - pointToReflect.dx, |
| 2 * reflectedIn.dy - pointToReflect.dy); |
| } |
| |
| static const double _kOneOverThree = 1.0 / 3.0; |
| |
| /// Blend the points with a ratio (1/3):(2/3). |
| static _PathOffset blendPoints(_PathOffset p1, _PathOffset p2) { |
| return new _PathOffset((p1.dx + 2 * p2.dx) * _kOneOverThree, |
| (p1.dy + 2 * p2.dy) * _kOneOverThree); |
| } |
| } |
| |
| bool isCubicCommand(SvgPathSegType command) { |
| return command == SvgPathSegType.cubicToAbs || |
| command == SvgPathSegType.cubicToRel || |
| command == SvgPathSegType.smoothCubicToAbs || |
| command == SvgPathSegType.smoothCubicToRel; |
| } |
| |
| bool isQuadraticCommand(SvgPathSegType command) { |
| return command == SvgPathSegType.quadToAbs || |
| command == SvgPathSegType.quadToRel || |
| command == SvgPathSegType.smoothQuadToAbs || |
| command == SvgPathSegType.smoothQuadToRel; |
| } |
| |
| // TODO(dnfield): This can probably be cleaned up a bit. Some of this was designed in such a way to pack data/optimize for C++ |
| // There are probably better/clearer ways to do it for Dart. |
| class PathSegmentData { |
| PathSegmentData() |
| : command = SvgPathSegType.unknown, |
| arcSweep = false, |
| arcLarge = false; |
| |
| _PathOffset get arcRadii => point1; |
| |
| double get arcAngle => point2.dx; |
| set arcAngle(double angle) => |
| point2 = new _PathOffset(angle, point2?.dy ?? 0.0); |
| |
| double get r1 => arcRadii.dx; |
| double get r2 => arcRadii.dy; |
| |
| bool get largeArcFlag => arcLarge; |
| bool get sweepFlag => arcSweep; |
| |
| double get x => targetPoint.dx; |
| double get y => targetPoint.dy; |
| |
| double get x1 => point1.dx; |
| double get y1 => point1.dy; |
| |
| double get x2 => point2.dx; |
| double get y2 => point2.dy; |
| |
| SvgPathSegType command; |
| _PathOffset targetPoint; |
| _PathOffset point1; |
| _PathOffset point2; |
| bool arcSweep; |
| bool arcLarge; |
| |
| @override |
| String toString() { |
| return 'PathSegmentData{$command $targetPoint $point1 $point2 $arcSweep $arcLarge}'; |
| } |
| } |
| |
| class SvgPathNormalizer { |
| _PathOffset _currentPoint = _PathOffset.zero; |
| _PathOffset _subPathPoint = _PathOffset.zero; |
| _PathOffset _controlPoint = _PathOffset.zero; |
| SvgPathSegType _lastCommand = SvgPathSegType.unknown; |
| |
| void emitSegment(PathSegmentData segment, PathProxy path) { |
| final PathSegmentData normSeg = segment; |
| assert( |
| normSeg.command == SvgPathSegType.close || normSeg.targetPoint != null); |
| assert(_currentPoint != null); |
| // Convert relative points to absolute points. |
| switch (segment.command) { |
| case SvgPathSegType.quadToRel: |
| normSeg.point1 += _currentPoint; |
| normSeg.targetPoint += _currentPoint; |
| break; |
| case SvgPathSegType.cubicToRel: |
| normSeg.point1 += _currentPoint; |
| continue smooth_rel; |
| smooth_rel: |
| case SvgPathSegType.smoothCubicToRel: |
| normSeg.point2 += _currentPoint; |
| continue arc_rel; |
| case SvgPathSegType.moveToRel: |
| case SvgPathSegType.lineToRel: |
| case SvgPathSegType.lineToHorizontalRel: |
| case SvgPathSegType.lineToVerticalRel: |
| case SvgPathSegType.smoothQuadToRel: |
| arc_rel: |
| case SvgPathSegType.arcToRel: |
| normSeg.targetPoint += _currentPoint; |
| break; |
| case SvgPathSegType.lineToHorizontalAbs: |
| normSeg.targetPoint = |
| new _PathOffset(normSeg.targetPoint.dx, _currentPoint.dy); |
| break; |
| case SvgPathSegType.lineToVerticalAbs: |
| normSeg.targetPoint = |
| new _PathOffset(_currentPoint.dx, normSeg.targetPoint.dy); |
| break; |
| case SvgPathSegType.close: |
| // Reset m_currentPoint for the next path. |
| normSeg.targetPoint = _subPathPoint; |
| break; |
| default: |
| break; |
| } |
| |
| // Update command verb, handle smooth segments and convert quadratic curve |
| // segments to cubics. |
| switch (segment.command) { |
| case SvgPathSegType.moveToRel: |
| case SvgPathSegType.moveToAbs: |
| _subPathPoint = normSeg.targetPoint; |
| // normSeg.command = SvgPathSegType.moveToAbs; |
| path.moveTo(normSeg.targetPoint.dx, normSeg.targetPoint.dy); |
| break; |
| case SvgPathSegType.lineToRel: |
| case SvgPathSegType.lineToAbs: |
| case SvgPathSegType.lineToHorizontalRel: |
| case SvgPathSegType.lineToHorizontalAbs: |
| case SvgPathSegType.lineToVerticalRel: |
| case SvgPathSegType.lineToVerticalAbs: |
| // normSeg.command = SvgPathSegType.lineToAbs; |
| path.lineTo(normSeg.targetPoint.dx, normSeg.targetPoint.dy); |
| break; |
| case SvgPathSegType.close: |
| // normSeg.command = SvgPathSegType.close; |
| path.close(); |
| break; |
| case SvgPathSegType.smoothCubicToRel: |
| case SvgPathSegType.smoothCubicToAbs: |
| if (!isCubicCommand(_lastCommand)) { |
| normSeg.point1 = _currentPoint; |
| } else { |
| normSeg.point1 = OffsetHelper.reflectedPoint( |
| _currentPoint, |
| _controlPoint, |
| ); |
| } |
| continue cubic_abs2; |
| case SvgPathSegType.cubicToRel: |
| cubic_abs2: |
| case SvgPathSegType.cubicToAbs: |
| _controlPoint = normSeg.point2; |
| // normSeg.command = SvgPathSegType.cubicToAbs; |
| path.cubicTo( |
| normSeg.point1.dx, |
| normSeg.point1.dy, |
| normSeg.point2.dx, |
| normSeg.point2.dy, |
| normSeg.targetPoint.dx, |
| normSeg.targetPoint.dy, |
| ); |
| break; |
| case SvgPathSegType.smoothQuadToRel: |
| case SvgPathSegType.smoothQuadToAbs: |
| if (!isQuadraticCommand(_lastCommand)) { |
| normSeg.point1 = _currentPoint; |
| } else { |
| normSeg.point1 = OffsetHelper.reflectedPoint( |
| _currentPoint, |
| _controlPoint, |
| ); |
| } |
| continue quad_abs2; |
| case SvgPathSegType.quadToRel: |
| quad_abs2: |
| case SvgPathSegType.quadToAbs: |
| // Save the unmodified control point. |
| _controlPoint = normSeg.point1; |
| normSeg.point1 = OffsetHelper.blendPoints(_currentPoint, _controlPoint); |
| normSeg.point2 = OffsetHelper.blendPoints( |
| normSeg.targetPoint, |
| _controlPoint, |
| ); |
| // normSeg.command = SvgPathSegType.cubicToAbs; |
| path.cubicTo( |
| normSeg.point1.dx, |
| normSeg.point1.dy, |
| normSeg.point2.dx, |
| normSeg.point2.dy, |
| normSeg.targetPoint.dx, |
| normSeg.targetPoint.dy, |
| ); |
| break; |
| case SvgPathSegType.arcToRel: |
| case SvgPathSegType.arcToAbs: |
| if (!_decomposeArcToCubic(_currentPoint, normSeg, path)) { |
| // On failure, emit a line segment to the target point. |
| // normSeg.command = SvgPathSegType.lineToAbs; |
| path.lineTo(normSeg.targetPoint.dx, normSeg.targetPoint.dy); |
| // } else { |
| // // decomposeArcToCubic() has already emitted the normalized |
| // // segments, so set command to PathSegArcAbs, to skip any further |
| // // emit. |
| // // normSeg.command = SvgPathSegType.arcToAbs; |
| } |
| break; |
| default: |
| throw new StateError('Invalid command type in path'); |
| } |
| |
| _currentPoint = normSeg.targetPoint; |
| |
| if (!isCubicCommand(segment.command) && |
| !isQuadraticCommand(segment.command)) { |
| _controlPoint = _currentPoint; |
| } |
| |
| _lastCommand = segment.command; |
| } |
| |
| // This works by converting the SVG arc to "simple" beziers. |
| // Partly adapted from Niko's code in kdelibs/kdecore/svgicons. |
| // See also SVG implementation notes: |
| // http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter |
| bool _decomposeArcToCubic( |
| _PathOffset currentPoint, |
| PathSegmentData arcSegment, |
| PathProxy path, |
| ) { |
| // If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a |
| // "lineto") joining the endpoints. |
| // http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters |
| double rx = arcSegment.arcRadii.dx.abs(); |
| double ry = arcSegment.arcRadii.dy.abs(); |
| if (rx == 0 || ry == 0) { |
| return false; |
| } |
| |
| // If the current point and target point for the arc are identical, it should |
| // be treated as a zero length path. This ensures continuity in animations. |
| if (arcSegment.targetPoint == currentPoint) { |
| return false; |
| } |
| |
| final double angle = arcSegment.arcAngle; |
| |
| final _PathOffset midPointDistance = |
| (currentPoint - arcSegment.targetPoint) * 0.5; |
| |
| final Matrix4 pointTransform = new Matrix4.identity(); |
| pointTransform.rotateZ(-angle); |
| |
| final _PathOffset transformedMidPoint = _mapPoint( |
| pointTransform, |
| new _PathOffset( |
| midPointDistance.dx, |
| midPointDistance.dy, |
| ), |
| ); |
| |
| final double squareRx = rx * rx; |
| final double squareRy = ry * ry; |
| final double squareX = transformedMidPoint.dx * transformedMidPoint.dx; |
| final double squareY = transformedMidPoint.dy * transformedMidPoint.dy; |
| |
| // Check if the radii are big enough to draw the arc, scale radii if not. |
| // http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii |
| final double radiiScale = squareX / squareRx + squareY / squareRy; |
| if (radiiScale > 1.0) { |
| rx *= math.sqrt(radiiScale); |
| ry *= math.sqrt(radiiScale); |
| } |
| pointTransform.setIdentity(); |
| |
| pointTransform.scale(1.0 / rx, 1.0 / ry); |
| pointTransform.rotateZ(-angle); |
| |
| _PathOffset point1 = _mapPoint(pointTransform, currentPoint); |
| _PathOffset point2 = _mapPoint(pointTransform, arcSegment.targetPoint); |
| _PathOffset delta = point2 - point1; |
| |
| final double d = delta.dx * delta.dx + delta.dy * delta.dy; |
| final double scaleFactorSquared = math.max(1.0 / d - 0.25, 0.0); |
| double scaleFactor = math.sqrt(scaleFactorSquared); |
| if (!scaleFactor.isFinite) { |
| scaleFactor = 0.0; |
| } |
| |
| if (arcSegment.arcSweep == arcSegment.arcLarge) { |
| scaleFactor = -scaleFactor; |
| } |
| |
| delta = delta * scaleFactor; |
| final _PathOffset centerPoint = |
| ((point1 + point2) * 0.5).translate(-delta.dy, delta.dx); |
| |
| final double theta1 = (point1 - centerPoint).direction; |
| final double theta2 = (point2 - centerPoint).direction; |
| |
| double thetaArc = theta2 - theta1; |
| |
| if (thetaArc < 0.0 && arcSegment.arcSweep) { |
| thetaArc += _twoPiFloat; |
| } else if (thetaArc > 0.0 && !arcSegment.arcSweep) { |
| thetaArc -= _twoPiFloat; |
| } |
| |
| pointTransform.setIdentity(); |
| pointTransform.rotateZ(angle); |
| pointTransform.scale(rx, ry); |
| |
| // Some results of atan2 on some platform implementations are not exact |
| // enough. So that we get more cubic curves than expected here. Adding 0.001f |
| // reduces the count of segments to the correct count. |
| final int segments = (thetaArc / (_piOverTwoFloat + 0.001)).abs().ceil(); |
| for (int i = 0; i < segments; ++i) { |
| final double startTheta = theta1 + i * thetaArc / segments; |
| final double endTheta = theta1 + (i + 1) * thetaArc / segments; |
| |
| final double t = (8.0 / 6.0) * math.tan(0.25 * (endTheta - startTheta)); |
| if (!t.isFinite) { |
| return false; |
| } |
| final double sinStartTheta = math.sin(startTheta); |
| final double cosStartTheta = math.cos(startTheta); |
| final double sinEndTheta = math.sin(endTheta); |
| final double cosEndTheta = math.cos(endTheta); |
| |
| point1 = new _PathOffset( |
| cosStartTheta - t * sinStartTheta, |
| sinStartTheta + t * cosStartTheta, |
| ).translate(centerPoint.dx, centerPoint.dy); |
| final _PathOffset targetPoint = new _PathOffset( |
| cosEndTheta, |
| sinEndTheta, |
| ).translate(centerPoint.dx, centerPoint.dy); |
| point2 = targetPoint.translate(t * sinEndTheta, -t * cosEndTheta); |
| |
| final PathSegmentData cubicSegment = new PathSegmentData(); |
| cubicSegment.command = SvgPathSegType.cubicToAbs; |
| cubicSegment.point1 = _mapPoint(pointTransform, point1); |
| cubicSegment.point2 = _mapPoint(pointTransform, point2); |
| cubicSegment.targetPoint = _mapPoint(pointTransform, targetPoint); |
| |
| path.cubicTo(cubicSegment.x1, cubicSegment.y1, cubicSegment.x2, |
| cubicSegment.y2, cubicSegment.x, cubicSegment.y); |
| //consumer_->EmitSegment(cubicSegment); |
| } |
| return true; |
| } |
| |
| _PathOffset _mapPoint(Matrix4 transform, _PathOffset point) { |
| // a, b, 0.0, 0.0, c, d, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, e, f, 0.0, 1.0 |
| return new _PathOffset( |
| transform.storage[0] * point.dx + |
| transform.storage[4] * point.dy + |
| transform.storage[12], |
| transform.storage[1] * point.dx + |
| transform.storage[5] * point.dy + |
| transform.storage[13], |
| ); |
| } |
| } |