WIP
This commit is contained in:
Generated
+6
-6
@@ -3,11 +3,11 @@
|
||||
"kanjivg": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1754131552,
|
||||
"narHash": "sha256-1h3nT1f1QqXme4rU3HMmEREn74sAASyZ8qzjZ0tPi4I=",
|
||||
"lastModified": 1770109946,
|
||||
"narHash": "sha256-zgkyLJwEJe6YABUNrL27BBrDSWVNcAUk7K6P1mcVHxQ=",
|
||||
"owner": "KanjiVG",
|
||||
"repo": "kanjivg",
|
||||
"rev": "0d08020e69611552a0fbe13d49365d6196431b96",
|
||||
"rev": "2fe6daaba502ee6735f888e5fb4a7d30639a18ee",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -18,11 +18,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1754214453,
|
||||
"narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=",
|
||||
"lastModified": 1771369470,
|
||||
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376",
|
||||
"rev": "0182a361324364ae3f436a63005877674cf45efb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
+252
File diff suppressed because one or more lines are too long
+4
-4
@@ -1,12 +1,12 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' show min, sqrt, pow;
|
||||
|
||||
import 'svg/parser.dart';
|
||||
import 'point.dart';
|
||||
|
||||
import 'bezier.dart' as bezier;
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import 'svg_parser.dart';
|
||||
import 'primitives/point.dart';
|
||||
import 'primitives/bezier.dart' as bezier;
|
||||
|
||||
double _computePathLength(String path) =>
|
||||
parsePath(path).size(error: 1e-8).toDouble();
|
||||
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import 'svg_parser.dart';
|
||||
import 'primitives/path.dart';
|
||||
import 'primitives/point.dart';
|
||||
|
||||
/// Enum set in the kvg:position attribute, used by <g> elements in the KanjiVG SVG files.
|
||||
enum KanjiPathGroupPosition {
|
||||
bottom,
|
||||
kamae,
|
||||
kamaec,
|
||||
left,
|
||||
middle,
|
||||
nyo,
|
||||
nyoc,
|
||||
right,
|
||||
tare,
|
||||
tarec,
|
||||
top;
|
||||
|
||||
factory KanjiPathGroupPosition.fromString(String s) => switch (s) {
|
||||
'bottom' => bottom,
|
||||
'kamae' => kamae,
|
||||
'kamaec' => kamaec,
|
||||
'left' => left,
|
||||
'middle' => middle,
|
||||
'nyo' => nyo,
|
||||
'nyoc' => nyoc,
|
||||
'right' => right,
|
||||
'tare' => tare,
|
||||
'tarec' => tarec,
|
||||
'top' => top,
|
||||
_ => throw ArgumentError('Invalid position string: $s'),
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() => switch (this) {
|
||||
KanjiPathGroupPosition.bottom => 'bottom',
|
||||
KanjiPathGroupPosition.kamae => 'kamae',
|
||||
KanjiPathGroupPosition.kamaec => 'kamaec',
|
||||
KanjiPathGroupPosition.left => 'left',
|
||||
KanjiPathGroupPosition.middle => 'middle',
|
||||
KanjiPathGroupPosition.nyo => 'nyo',
|
||||
KanjiPathGroupPosition.nyoc => 'nyoc',
|
||||
KanjiPathGroupPosition.right => 'right',
|
||||
KanjiPathGroupPosition.tare => 'tare',
|
||||
KanjiPathGroupPosition.tarec => 'tarec',
|
||||
KanjiPathGroupPosition.top => 'top',
|
||||
};
|
||||
}
|
||||
|
||||
/// Contents of a \<g> element in the KanjiVG SVG files.
|
||||
class KanjiPathGroupTreeNode {
|
||||
final String id;
|
||||
final List<KanjiPathGroupTreeNode> children;
|
||||
final String? element;
|
||||
final String? original;
|
||||
final KanjiPathGroupPosition? position;
|
||||
final String? radical;
|
||||
final int? part;
|
||||
|
||||
KanjiPathGroupTreeNode({
|
||||
required this.id,
|
||||
this.children = const [],
|
||||
this.element,
|
||||
this.original,
|
||||
this.position,
|
||||
this.radical,
|
||||
this.part,
|
||||
});
|
||||
}
|
||||
|
||||
/// Contents of a <text> element in the StrokeNumber's group in the KanjiVG SVG files
|
||||
class KanjiStrokeNumber {
|
||||
final int number;
|
||||
final Point position;
|
||||
|
||||
KanjiStrokeNumber(this.number, this.position);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'KanjiStrokeNumber(number: $number, position: $position)';
|
||||
}
|
||||
|
||||
/// Contents of a <path> element in the KanjiVG SVG files
|
||||
class KanjiVGPath {
|
||||
final String id;
|
||||
final String type;
|
||||
final Path svgPath;
|
||||
|
||||
KanjiVGPath({required this.id, required this.type, required this.svgPath});
|
||||
|
||||
@override
|
||||
String toString() => 'KanjiVGPath(id: $id, type: $type, svgPath: $svgPath)';
|
||||
}
|
||||
|
||||
/// Representation of the entire KanjiVG SVG file for a single kanji character
|
||||
class KanjiVGKanji {
|
||||
final String character;
|
||||
final List<Path> paths;
|
||||
final List<KanjiStrokeNumber> strokeNumbers;
|
||||
final KanjiPathGroupTreeNode? pathGroups;
|
||||
|
||||
KanjiVGKanji({
|
||||
required this.character,
|
||||
required this.paths,
|
||||
required this.strokeNumbers,
|
||||
this.pathGroups,
|
||||
});
|
||||
}
|
||||
|
||||
/// Small wrapper returned by the parser. It contains the parsed paths and stroke
|
||||
/// numbers and the kanji character string.
|
||||
class KanjiVGItem {
|
||||
final String character;
|
||||
final List<KanjiVGPath> paths;
|
||||
final List<KanjiStrokeNumber> strokeNumbers;
|
||||
|
||||
KanjiVGItem({
|
||||
required this.character,
|
||||
required this.paths,
|
||||
required this.strokeNumbers,
|
||||
});
|
||||
|
||||
/// Helper method to get attributes that may be in the kvg namespace or as kvg: prefixed attributes.
|
||||
static String? _kvgAttr(XmlElement el, String name) {
|
||||
final fromNs = el.getAttribute(
|
||||
name,
|
||||
namespace: 'http://kanjivg.tagaini.net',
|
||||
);
|
||||
if (fromNs != null) return fromNs;
|
||||
return el.getAttribute('kvg:$name');
|
||||
}
|
||||
|
||||
/// Parse the path data from the provided XML document and return a list of [Path]s.
|
||||
static List<KanjiVGPath> _parsePaths(XmlDocument doc) {
|
||||
final List<KanjiVGPath> paths = [];
|
||||
for (final p in doc.findAllElements('path')) {
|
||||
final d = p.getAttribute('d');
|
||||
if (d == null || d.trim().isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final svgPath = parsePath(d);
|
||||
final id = p.getAttribute('id') ?? '';
|
||||
final type = _kvgAttr(p, 'type') ?? p.getAttribute('kvg:type') ?? '';
|
||||
paths.add(KanjiVGPath(id: id, type: type, svgPath: svgPath));
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
/// Parse the stroke number group from the provided XML document and return a list of [KanjiStrokeNumber]s.
|
||||
static List<KanjiStrokeNumber> _parseStrokeNumbers(XmlDocument doc) {
|
||||
final List<KanjiStrokeNumber> strokeNumbers = [];
|
||||
final strokeNumberGroup = doc.findAllElements('g').firstWhere((g) {
|
||||
final id = g.getAttribute('id') ?? '';
|
||||
return RegExp(r'^kvg:StrokeNumbers_').hasMatch(id);
|
||||
}, orElse: () => XmlElement(XmlName('')));
|
||||
|
||||
if (strokeNumberGroup.name.local != '') {
|
||||
for (final t in strokeNumberGroup.findAllElements('text')) {
|
||||
final rawText = t.innerText.trim();
|
||||
if (rawText.isEmpty) continue;
|
||||
final numVal = int.tryParse(rawText);
|
||||
if (numVal == null) continue;
|
||||
|
||||
final transform = t.getAttribute('transform') ?? '';
|
||||
final numbers = transform
|
||||
.replaceAll('matrix(1 0 0 1', '')
|
||||
.replaceAll(')', '')
|
||||
.trim()
|
||||
.split(' ')
|
||||
.map(num.parse)
|
||||
.toList();
|
||||
|
||||
assert(
|
||||
numbers.length >= 2,
|
||||
'Expected at least 2 numbers in transform for stroke number position',
|
||||
);
|
||||
|
||||
Point pos = Point(
|
||||
numbers[numbers.length - 2],
|
||||
numbers[numbers.length - 1],
|
||||
);
|
||||
|
||||
strokeNumbers.add(KanjiStrokeNumber(numVal, pos));
|
||||
}
|
||||
}
|
||||
|
||||
return strokeNumbers;
|
||||
}
|
||||
|
||||
/// Parse the provided KanjiVG SVG content and return a [KanjiVGItem].
|
||||
factory KanjiVGItem.parseFromXml(String xmlContent) {
|
||||
final XmlDocument doc = XmlDocument.parse(xmlContent);
|
||||
|
||||
XmlElement strokePathsGroup = doc
|
||||
.findElements('svg')
|
||||
.first
|
||||
.findElements('g')
|
||||
.first;
|
||||
XmlElement kanjiGroup = strokePathsGroup.findElements('g').first;
|
||||
final character = _kvgAttr(kanjiGroup, 'element') ?? '';
|
||||
final paths = _parsePaths(doc);
|
||||
final strokeNumbers = _parseStrokeNumbers(doc);
|
||||
|
||||
return KanjiVGItem(
|
||||
character: character,
|
||||
paths: paths,
|
||||
strokeNumbers: strokeNumbers,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import 'dart:math' show sqrt, sin, cos, acos, log, pi;
|
||||
|
||||
import 'package:bisection/extension.dart';
|
||||
|
||||
import '../point.dart';
|
||||
import 'point.dart';
|
||||
|
||||
num radians(num n) => n * pi / 180;
|
||||
num degrees(num n) => n * 180 / pi;
|
||||
@@ -3,8 +3,8 @@
|
||||
/// See https://pypi.org/project/svg.path/ for the original implementation.
|
||||
library;
|
||||
|
||||
import '../point.dart';
|
||||
import 'path.dart';
|
||||
import 'primitives/point.dart';
|
||||
import 'primitives/path.dart';
|
||||
|
||||
const _commands = {
|
||||
'M',
|
||||
@@ -32,7 +32,7 @@ const _commands = {
|
||||
// const _uppercaseCommands = {'M', 'Z', 'L', 'H', 'V', 'C', 'S', 'Q', 'T', 'A'};
|
||||
|
||||
final _commandPattern = RegExp("(?=[${_commands.join('')}])");
|
||||
final _floatPattern = RegExp(r"^[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?");
|
||||
final _floatPattern = RegExp(r"^[-+]?(?:[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)(?:[eE][-+]?[0-9]+)?");
|
||||
|
||||
class ParserResult<T> {
|
||||
final T value;
|
||||
+525
-9
@@ -1,11 +1,527 @@
|
||||
import 'package:flutter/material.dart';
|
||||
// // A Flutter widget that renders an animated single-kanji stroke drawing using
|
||||
// // the project's SVG path parser. This widget loads the Kanji SVG from
|
||||
// // `assets/kanjivg/kanji/<hex>.svg` (the same assets used by the exporter),
|
||||
// // parses the path `d` attributes, converts them into `ui.Path` objects and
|
||||
// // animates the stroke draw using an AnimationController.
|
||||
// //
|
||||
// // Notes:
|
||||
// // - This implementation uses the repository's SVG path parser (`parsePath`)
|
||||
// // and the custom `Path` representation in `lib/svg/path.dart`. We convert
|
||||
// // those into Flutter `ui.Path` by sampling points along the sampled SVG path.
|
||||
// // - The SVGs in KanjiVG typically use a 109x109 viewBox. We detect the
|
||||
// // `viewBox` attribute (if present) and scale paths to fit the widget size.
|
||||
// //
|
||||
// // Public API (Kanimaji widget parameters):
|
||||
// // - `kanji` (required): the single character Kanji to display.
|
||||
// // - `size`: target width/height of the widget (square).
|
||||
// // - `strokeColor`, `strokeUnfilledColor`, `strokeWidth`.
|
||||
// // - `backgroundColor`.
|
||||
// // - `speed`: multiplier to speed up (>1) or slow down (<1) the animation.
|
||||
// // - `loop`: whether to repeat the animation.
|
||||
// // - `showBrush`: whether to show a small circular "brush" tip following the stroke.
|
||||
// // - `durationOverride`: optionally override the computed animation duration.
|
||||
// //
|
||||
// // Example:
|
||||
// // Kanimaji(
|
||||
// // kanji: '水',
|
||||
// // size: 240,
|
||||
// // strokeColor: Colors.black,
|
||||
// // strokeUnfilledColor: Colors.grey.shade300,
|
||||
// // strokeWidth: 3.0,
|
||||
// // backgroundColor: Colors.white,
|
||||
// // speed: 1.0,
|
||||
// // loop: true,
|
||||
// // );
|
||||
|
||||
class Kanimaji extends StatelessWidget {
|
||||
final String kanji;
|
||||
const Kanimaji({super.key, required this.kanji});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
// import 'dart:math' as math;
|
||||
// import 'dart:ui' as ui;
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter/services.dart' show rootBundle;
|
||||
// import 'package:xml/xml.dart';
|
||||
|
||||
// import 'svg/parser.dart' show parsePath;
|
||||
// import 'svg/path.dart' as svgpath;
|
||||
// import 'point.dart' as kpt;
|
||||
// import 'animator.dart' show strokeLengthToDuration;
|
||||
|
||||
// class Kanimaji extends StatefulWidget {
|
||||
// final String kanji;
|
||||
// final double size;
|
||||
// final Color strokeColor;
|
||||
// final Color strokeUnfilledColor;
|
||||
// final double strokeWidth;
|
||||
// final Color backgroundColor;
|
||||
// final double speed;
|
||||
// final bool loop;
|
||||
// final bool showBrush;
|
||||
// final Duration? durationOverride;
|
||||
|
||||
// const Kanimaji({
|
||||
// super.key,
|
||||
// required this.kanji,
|
||||
// this.size = 150,
|
||||
// this.strokeColor = Colors.black,
|
||||
// this.strokeUnfilledColor = const Color(0xFFEEEEEE),
|
||||
// this.strokeWidth = 3.0,
|
||||
// this.backgroundColor = Colors.transparent,
|
||||
// this.speed = 1.0,
|
||||
// this.loop = true,
|
||||
// this.showBrush = true,
|
||||
// this.durationOverride,
|
||||
// });
|
||||
|
||||
// @override
|
||||
// State<Kanimaji> createState() => _KanimajiState();
|
||||
// }
|
||||
|
||||
// class _KanimajiState extends State<Kanimaji> with SingleTickerProviderStateMixin {
|
||||
// bool _loaded = false;
|
||||
// String? _error;
|
||||
// late AnimationController _controller;
|
||||
|
||||
// // Converted UI paths and per-path lengths
|
||||
// List<ui.Path> _paths = [];
|
||||
// List<double> _pathLengths = [];
|
||||
// // per-stroke durations (in seconds) computed using animator.strokeLengthToDuration
|
||||
// List<double> _pathDurations = [];
|
||||
|
||||
// // viewBox width/height from svg for scaling
|
||||
// double _viewBoxWidth = 109.0;
|
||||
// double _viewBoxHeight = 109.0;
|
||||
|
||||
// // total length summation (for drawing metrics)
|
||||
// double _totalLength = 0.0;
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// // Dummy controller; real duration will be set after loading
|
||||
// _controller = AnimationController(vsync: this);
|
||||
// // Add a single listener once to avoid duplicates when reconfiguring the controller.
|
||||
// _controller.addListener(_onTick);
|
||||
// _loadAndParseSvg();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void didUpdateWidget(covariant Kanimaji oldWidget) {
|
||||
// super.didUpdateWidget(oldWidget);
|
||||
// // If kanji changed, re-load
|
||||
// if (oldWidget.kanji != widget.kanji) {
|
||||
// _loadAndParseSvg();
|
||||
// } else if (oldWidget.speed != widget.speed ||
|
||||
// oldWidget.durationOverride != widget.durationOverride ||
|
||||
// oldWidget.loop != widget.loop) {
|
||||
// // Update controller if animation parameters changed
|
||||
// _configureController();
|
||||
// }
|
||||
// }
|
||||
|
||||
// void _onTick() {
|
||||
// if (mounted) setState(() {});
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void dispose() {
|
||||
// _controller.removeListener(_onTick);
|
||||
// _controller.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
// String _codepointHex(String s) {
|
||||
// // Support multi-code-unit characters by taking the first Unicode rune
|
||||
// final runes = s.runes.toList();
|
||||
// if (runes.isEmpty) return '00000';
|
||||
// return runes[0].toRadixString(16).padLeft(5, '0');
|
||||
// }
|
||||
|
||||
// Future<void> _loadAndParseSvg() async {
|
||||
// setState(() {
|
||||
// _loaded = false;
|
||||
// _error = null;
|
||||
// _paths = [];
|
||||
// _pathLengths = [];
|
||||
// _pathDurations = [];
|
||||
// _totalLength = 0.0;
|
||||
// });
|
||||
|
||||
// final hex = _codepointHex(widget.kanji);
|
||||
// final assetPath = 'assets/kanjivg/kanji/$hex.svg';
|
||||
|
||||
// try {
|
||||
// final svgString = await rootBundle.loadString(assetPath);
|
||||
|
||||
// final doc = XmlDocument.parse(svgString);
|
||||
|
||||
// // parse viewBox: "minX minY width height"
|
||||
// final viewBoxAttr = doc.rootElement.getAttribute('viewBox');
|
||||
// if (viewBoxAttr != null) {
|
||||
// final parts = viewBoxAttr.split(RegExp(r'[\s,]+')).where((p) => p.isNotEmpty).toList();
|
||||
// if (parts.length >= 4) {
|
||||
// final w = double.tryParse(parts[2]);
|
||||
// final h = double.tryParse(parts[3]);
|
||||
// if (w != null && h != null && w > 0 && h > 0) {
|
||||
// _viewBoxWidth = w;
|
||||
// _viewBoxHeight = h;
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // fallback to width/height attributes if present (may include "px")
|
||||
// final widthAttr = doc.rootElement.getAttribute('width');
|
||||
// final heightAttr = doc.rootElement.getAttribute('height');
|
||||
// if (widthAttr != null && heightAttr != null) {
|
||||
// final w = double.tryParse(widthAttr.replaceAll('px', '').trim());
|
||||
// final h = double.tryParse(heightAttr.replaceAll('px', '').trim());
|
||||
// if (w != null && h != null && w > 0 && h > 0) {
|
||||
// _viewBoxWidth = w;
|
||||
// _viewBoxHeight = h;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Find all <path ... d="..."> elements inside the SVG. KanjiVG groups strokes in <g>.
|
||||
// final pathElements = doc.findAllElements('path', namespace: doc.rootElement.name.namespaceUri).toList();
|
||||
|
||||
// if (pathElements.isEmpty) {
|
||||
// throw Exception('No <path> elements found in $assetPath');
|
||||
// }
|
||||
|
||||
// final uiPaths = <ui.Path>[];
|
||||
// final lengths = <double>[];
|
||||
|
||||
// for (final p in pathElements) {
|
||||
// final d = p.getAttribute('d');
|
||||
// if (d == null || d.trim().isEmpty) continue;
|
||||
|
||||
// // Parse the SVG path using existing parser
|
||||
// final svgParsed = parsePath(d); // returns svgpath.Path (project type)
|
||||
|
||||
// // Convert to ui.Path using direct segment conversion (no sampling)
|
||||
// final uiPath = _svgPathToUiPath(svgParsed);
|
||||
// if (uiPath == null) continue;
|
||||
|
||||
// // Compute its drawing length using PathMetrics (for extractPath)
|
||||
// final pm = uiPath.computeMetrics();
|
||||
// double pathLen = 0.0;
|
||||
// for (final metric in pm) {
|
||||
// pathLen += metric.length;
|
||||
// }
|
||||
// // If the path metric yields zero (rare), skip
|
||||
// if (pathLen <= 0) continue;
|
||||
|
||||
// // Compute logical stroke length using the parser (precise geometry) and convert to duration
|
||||
// final double logicalLen = svgParsed.size(error: 1e-8).toDouble();
|
||||
// final double durationSec = strokeLengthToDuration(logicalLen);
|
||||
|
||||
// uiPaths.add(uiPath);
|
||||
// lengths.add(pathLen);
|
||||
// _pathDurations.add(durationSec);
|
||||
// _totalLength += pathLen;
|
||||
// }
|
||||
|
||||
// if (uiPaths.isEmpty) {
|
||||
// throw Exception('No drawable paths found in $assetPath');
|
||||
// }
|
||||
|
||||
// setState(() {
|
||||
// _paths = uiPaths;
|
||||
// _pathLengths = lengths;
|
||||
// });
|
||||
|
||||
// _configureController();
|
||||
|
||||
// setState(() {
|
||||
// _loaded = true;
|
||||
// });
|
||||
// } catch (e) {
|
||||
// setState(() {
|
||||
// _error = 'Error loading/ parsing $assetPath: $e';
|
||||
// _loaded = true; // mark loaded so we can show error
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Convert a parsed SVG path (project Path) into a Flutter ui.Path without sampling.
|
||||
// // Uses the concrete segment types (Move, Line, CubicBezier, QuadraticBezier, Arc, Close)
|
||||
// // and maps them to the corresponding ui.Path methods for native, accurate drawing.
|
||||
// ui.Path? _svgPathToUiPath(svgpath.Path svgP) {
|
||||
// try {
|
||||
// final ui.Path p = ui.Path();
|
||||
// bool started = false;
|
||||
|
||||
// for (final seg in svgP) {
|
||||
// if (seg is svgpath.Move) {
|
||||
// p.moveTo(seg.start.x.toDouble(), seg.start.y.toDouble());
|
||||
// started = true;
|
||||
// } else if (seg is svgpath.Line) {
|
||||
// if (!started) {
|
||||
// p.moveTo(seg.start.x.toDouble(), seg.start.y.toDouble());
|
||||
// started = true;
|
||||
// }
|
||||
// p.lineTo(seg.end.x.toDouble(), seg.end.y.toDouble());
|
||||
// } else if (seg is svgpath.Close) {
|
||||
// p.close();
|
||||
// } else if (seg is svgpath.CubicBezier) {
|
||||
// if (!started) {
|
||||
// p.moveTo(seg.start.x.toDouble(), seg.start.y.toDouble());
|
||||
// started = true;
|
||||
// }
|
||||
// p.cubicTo(
|
||||
// seg.control1.x.toDouble(),
|
||||
// seg.control1.y.toDouble(),
|
||||
// seg.control2.x.toDouble(),
|
||||
// seg.control2.y.toDouble(),
|
||||
// seg.end.x.toDouble(),
|
||||
// seg.end.y.toDouble(),
|
||||
// );
|
||||
// } else if (seg is svgpath.QuadraticBezier) {
|
||||
// if (!started) {
|
||||
// p.moveTo(seg.start.x.toDouble(), seg.start.y.toDouble());
|
||||
// started = true;
|
||||
// }
|
||||
// p.quadraticBezierTo(
|
||||
// seg.control.x.toDouble(),
|
||||
// seg.control.y.toDouble(),
|
||||
// seg.end.x.toDouble(),
|
||||
// seg.end.y.toDouble(),
|
||||
// );
|
||||
// } else if (seg is svgpath.Arc) {
|
||||
// if (!started) {
|
||||
// p.moveTo(seg.start.x.toDouble(), seg.start.y.toDouble());
|
||||
// started = true;
|
||||
// }
|
||||
// // Map SVG Arc to Flutter arcToPoint. rotation is in degrees in svg; arcToPoint expects radians.
|
||||
// final radius = Radius.elliptical(seg.radius.x.toDouble(), seg.radius.y.toDouble());
|
||||
// p.arcToPoint(
|
||||
// Offset(seg.end.x.toDouble(), seg.end.y.toDouble()),
|
||||
// radius: radius,
|
||||
// rotation: seg.rotation.toDouble() * (math.pi / 180.0),
|
||||
// largeArc: seg.arc,
|
||||
// clockwise: seg.sweep,
|
||||
// );
|
||||
// } else {
|
||||
// // Unknown segment fallback: try to move to end
|
||||
// try {
|
||||
// p.moveTo(seg.end.x.toDouble(), seg.end.y.toDouble());
|
||||
// started = true;
|
||||
// } catch (_) {}
|
||||
// }
|
||||
// }
|
||||
|
||||
// return p;
|
||||
// } catch (e) {
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
|
||||
// void _configureController() {
|
||||
// // Use per-stroke durations computed from the parsed geometry (strokeLengthToDuration)
|
||||
// // Sum durations and scale by widget.speed. If durationOverride is provided use it.
|
||||
// final double totalSec = _pathDurations.fold(0.0, (a, b) => a + b);
|
||||
// final double scaledTotalSec = (widget.speed <= 0) ? totalSec : (totalSec / widget.speed);
|
||||
// final Duration duration = widget.durationOverride ??
|
||||
// Duration(milliseconds: (math.max(0.001, scaledTotalSec) * 1000.0).round());
|
||||
|
||||
// final bool shouldRepeat = widget.loop;
|
||||
|
||||
// _controller.stop();
|
||||
// _controller.duration = duration;
|
||||
// if (shouldRepeat) {
|
||||
// _controller.repeat();
|
||||
// } else {
|
||||
// _controller.forward(from: 0);
|
||||
// }
|
||||
// // Repaint handled by the single listener added in initState.
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// if (!_loaded) {
|
||||
// return SizedBox(
|
||||
// width: widget.size,
|
||||
// height: widget.size,
|
||||
// child: Center(child: CircularProgressIndicator()),
|
||||
// );
|
||||
// }
|
||||
// if (_error != null) {
|
||||
// return SizedBox(
|
||||
// width: widget.size,
|
||||
// height: widget.size,
|
||||
// child: Container(
|
||||
// color: widget.backgroundColor,
|
||||
// alignment: Alignment.center,
|
||||
// child: Text(
|
||||
// 'Error',
|
||||
// style: TextStyle(color: Colors.red, fontSize: math.max(10, widget.size * 0.08)),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// return SizedBox(
|
||||
// width: widget.size,
|
||||
// height: widget.size,
|
||||
// child: CustomPaint(
|
||||
// painter: _KanimajiPainter(
|
||||
// paths: _paths,
|
||||
// pathLengths: _pathLengths,
|
||||
// pathDurations: _pathDurations,
|
||||
// progress: _controller.value,
|
||||
// strokeColor: widget.strokeColor,
|
||||
// strokeUnfilledColor: widget.strokeUnfilledColor,
|
||||
// strokeWidth: widget.strokeWidth,
|
||||
// backgroundColor: widget.backgroundColor,
|
||||
// viewBoxWidth: _viewBoxWidth,
|
||||
// viewBoxHeight: _viewBoxHeight,
|
||||
// showBrush: widget.showBrush,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// class _KanimajiPainter extends CustomPainter {
|
||||
// final List<ui.Path> paths;
|
||||
// final List<double> pathLengths;
|
||||
// final List<double> pathDurations; // per-stroke durations in seconds
|
||||
// final double progress; // 0..1
|
||||
// final Color strokeColor;
|
||||
// final Color strokeUnfilledColor;
|
||||
// final double strokeWidth;
|
||||
// final Color backgroundColor;
|
||||
// final double viewBoxWidth;
|
||||
// final double viewBoxHeight;
|
||||
// final bool showBrush;
|
||||
|
||||
// _KanimajiPainter({
|
||||
// required this.paths,
|
||||
// required this.pathLengths,
|
||||
// required this.pathDurations,
|
||||
// required this.progress,
|
||||
// required this.strokeColor,
|
||||
// required this.strokeUnfilledColor,
|
||||
// required this.strokeWidth,
|
||||
// required this.backgroundColor,
|
||||
// required this.viewBoxWidth,
|
||||
// required this.viewBoxHeight,
|
||||
// required this.showBrush,
|
||||
// });
|
||||
|
||||
// @override
|
||||
// void paint(Canvas canvas, Size size) {
|
||||
// // Paint background
|
||||
// final bgPaint = Paint()..color = backgroundColor;
|
||||
// canvas.drawRect(Offset.zero & size, bgPaint);
|
||||
|
||||
// if (paths.isEmpty) return;
|
||||
|
||||
// // compute scale to fit viewBox into size, preserving aspect ratio
|
||||
// final double sx = size.width / viewBoxWidth;
|
||||
// final double sy = size.height / viewBoxHeight;
|
||||
// final double scale = math.min(sx, sy);
|
||||
|
||||
// // compute translation to center the drawing
|
||||
// final double dx = (size.width - viewBoxWidth * scale) / 2.0;
|
||||
// final double dy = (size.height - viewBoxHeight * scale) / 2.0;
|
||||
|
||||
// canvas.save();
|
||||
// canvas.translate(dx, dy);
|
||||
// canvas.scale(scale, scale);
|
||||
|
||||
// // paints
|
||||
// final Paint unfilledPaint = Paint()
|
||||
// ..style = PaintingStyle.stroke
|
||||
// ..strokeCap = StrokeCap.round
|
||||
// ..strokeJoin = StrokeJoin.round
|
||||
// ..strokeWidth = strokeWidth / scale // scale stroke width so it looks consistent
|
||||
// ..color = strokeUnfilledColor;
|
||||
|
||||
// final Paint filledPaint = Paint()
|
||||
// ..style = PaintingStyle.stroke
|
||||
// ..strokeCap = StrokeCap.round
|
||||
// ..strokeJoin = StrokeJoin.round
|
||||
// ..strokeWidth = strokeWidth / scale
|
||||
// ..color = strokeColor;
|
||||
|
||||
// // total animation time in seconds computed from durations
|
||||
// final double totalTime = pathDurations.isEmpty ? 1.0 : pathDurations.fold(0.0, (a, b) => a + b);
|
||||
// final double p = progress.clamp(0.0, 1.0);
|
||||
|
||||
// double elapsedTime = 0.0; // seconds accumulated so far
|
||||
|
||||
// for (int i = 0; i < paths.length; i++) {
|
||||
// final ui.Path path = paths[i];
|
||||
// final double len = pathLengths[i];
|
||||
// final double dur = (i < pathDurations.length) ? pathDurations[i] : 0.0;
|
||||
// final double startFraction = totalTime == 0 ? 0.0 : (elapsedTime / totalTime);
|
||||
// final double endFraction = totalTime == 0 ? 1.0 : ((elapsedTime + dur) / totalTime);
|
||||
|
||||
// // Draw background (unfilled) fully for this stroke
|
||||
// canvas.drawPath(path, unfilledPaint);
|
||||
|
||||
// // Determine coverage for this stroke based on global progress (time-based)
|
||||
// double coverage = 0.0;
|
||||
// if (p >= endFraction) {
|
||||
// coverage = 1.0;
|
||||
// } else if (p <= startFraction || dur <= 0.0) {
|
||||
// coverage = 0.0;
|
||||
// } else {
|
||||
// coverage = (p - startFraction) / (endFraction - startFraction);
|
||||
// }
|
||||
|
||||
// if (coverage > 0.0) {
|
||||
// // extract subpath up to coverage * len (length-based extraction for drawing)
|
||||
// final metrics = path.computeMetrics().toList();
|
||||
// final ui.Path drawn = ui.Path();
|
||||
|
||||
// double remain = coverage * len;
|
||||
// for (final metric in metrics) {
|
||||
// if (remain <= 0) break;
|
||||
// final take = math.min(metric.length, remain);
|
||||
// final sub = metric.extractPath(0.0, take);
|
||||
// drawn.addPath(sub, Offset.zero);
|
||||
// remain -= take;
|
||||
// }
|
||||
|
||||
// // draw filled portion
|
||||
// canvas.drawPath(drawn, filledPaint);
|
||||
|
||||
// // brush tip
|
||||
// if (showBrush && coverage > 0.0 && coverage < 1.0) {
|
||||
// // find the tangent at the very end of the drawn part
|
||||
// final metrics2 = path.computeMetrics().toList();
|
||||
// double rem = coverage * len;
|
||||
// for (final metric in metrics2) {
|
||||
// if (rem <= 0) break;
|
||||
// if (rem <= metric.length + 1e-9) {
|
||||
// final tangent = metric.getTangentForOffset(rem.clamp(0.0, metric.length));
|
||||
// if (tangent != null) {
|
||||
// final Offset pos = tangent.position;
|
||||
// final double brushRadius = (strokeWidth * 0.9) / scale;
|
||||
// final Paint brushPaint = Paint()..color = strokeColor;
|
||||
// canvas.drawCircle(pos, brushRadius, brushPaint);
|
||||
// }
|
||||
// break;
|
||||
// } else {
|
||||
// rem -= metric.length;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// elapsedTime += dur;
|
||||
// }
|
||||
|
||||
// canvas.restore();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// bool shouldRepaint(covariant _KanimajiPainter oldDelegate) {
|
||||
// return oldDelegate.progress != progress ||
|
||||
// oldDelegate.paths != paths ||
|
||||
// oldDelegate.strokeColor != strokeColor ||
|
||||
// oldDelegate.strokeUnfilledColor != strokeUnfilledColor ||
|
||||
// oldDelegate.strokeWidth != strokeWidth ||
|
||||
// oldDelegate.backgroundColor != backgroundColor;
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -15,3 +15,4 @@ dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
path: ^1.9.1
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'dart:io';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kanimaji/kanjivg_parser.dart';
|
||||
|
||||
void main() {
|
||||
final kanjivgDirStr = Platform.environment['KANJIVG_PATH'];
|
||||
if (kanjivgDirStr == null) {
|
||||
throw Exception('KANJIVG_PATH environment variable not set');
|
||||
}
|
||||
final kanjivgDir = Directory(kanjivgDirStr);
|
||||
final kanjivgFiles = kanjivgDir
|
||||
.listSync(recursive: true)
|
||||
.whereType<File>()
|
||||
.where((f) => f.path.endsWith('.svg'))
|
||||
.toList();
|
||||
|
||||
for (final file in kanjivgFiles) {
|
||||
test('Test parsing KanjiVG file ${path.basename(file.path)}', () async {
|
||||
final content = await file.readAsString();
|
||||
try {
|
||||
final item = KanjiVGItem.parseFromXml(content);
|
||||
|
||||
expect(item, isNotNull, reason: 'Failed to parse ${file.path}');
|
||||
|
||||
expect(
|
||||
item.strokeNumbers,
|
||||
isNotEmpty,
|
||||
reason: 'No stroke numbers found in ${file.path}',
|
||||
);
|
||||
|
||||
expect(
|
||||
item.strokeNumbers.length,
|
||||
item.paths.length,
|
||||
reason: 'Mismatch between stroke numbers and paths in ${file.path}',
|
||||
);
|
||||
} catch (e) {
|
||||
fail('Error parsing ${file.path}: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kanimaji/svg/parser.dart';
|
||||
import 'package:kanimaji/svg_parser.dart';
|
||||
|
||||
void main() {
|
||||
test('Test generating SVG path strings', () {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kanimaji/point.dart';
|
||||
import 'package:kanimaji/svg/parser.dart';
|
||||
import 'package:kanimaji/svg/path.dart';
|
||||
import 'package:kanimaji/primitives/path.dart';
|
||||
import 'package:kanimaji/primitives/point.dart';
|
||||
import 'package:kanimaji/svg_parser.dart';
|
||||
|
||||
void main() {
|
||||
group("Examples from the SVG spec", () {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import 'dart:math' show sqrt, pi;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kanimaji/point.dart';
|
||||
import 'package:kanimaji/svg/path.dart';
|
||||
import 'package:kanimaji/primitives/point.dart';
|
||||
import 'package:kanimaji/primitives/path.dart';
|
||||
|
||||
// from ..path import CubicBezier, QuadraticBezier, Line, Arc, Move, Close, Path
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// from svg.path import parser
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kanimaji/point.dart';
|
||||
import 'package:kanimaji/svg/parser.dart'
|
||||
import 'package:kanimaji/primitives/point.dart';
|
||||
import 'package:kanimaji/svg_parser.dart'
|
||||
show Command, Token, commandifyPath, parsePath, tokenizePath;
|
||||
|
||||
class TokenizerTest {
|
||||
|
||||
Reference in New Issue
Block a user