mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 09:46:05 -04:00
webapi: implement SVG geometry
This commit is contained in:
committed by
Karl Seguin
parent
8a256f59e0
commit
2e89b805fc
@@ -65,6 +65,7 @@ const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
|
||||
const PointList = @import("webapi/svg/PointList.zig");
|
||||
const StringList = @import("webapi/svg/StringList.zig");
|
||||
const AnimatedLength = @import("webapi/svg/AnimatedLength.zig");
|
||||
const AnimatedNumber = @import("webapi/svg/AnimatedNumber.zig");
|
||||
const AnimatedString = @import("webapi/svg/AnimatedString.zig");
|
||||
const AnimatedTransformList = @import("webapi/svg/AnimatedTransformList.zig");
|
||||
const AnimatedPreserveAspectRatio = @import("webapi/svg/AnimatedPreserveAspectRatio.zig");
|
||||
@@ -150,6 +151,7 @@ _node_owner_documents: Node.OwnerDocumentLookup = .empty,
|
||||
_element_scroll_positions: Element.ScrollPositionLookup = .empty,
|
||||
_element_namespace_uris: Element.NamespaceUriLookup = .empty,
|
||||
_svg_animated_lengths: AnimatedLength.Lookup = .empty,
|
||||
_svg_animated_numbers: AnimatedNumber.Lookup = .empty,
|
||||
_svg_animated_preserve_aspect_ratios: AnimatedPreserveAspectRatio.Lookup = .empty,
|
||||
_svg_animated_strings: AnimatedString.Lookup = .empty,
|
||||
_svg_animated_transform_lists: AnimatedTransformList.Lookup = .empty,
|
||||
|
||||
@@ -1074,6 +1074,7 @@ pub const PageJsApis = flattenTypes(&.{
|
||||
@import("../webapi/svg/Angle.zig"),
|
||||
@import("../webapi/svg/Transform.zig"),
|
||||
@import("../webapi/svg/AnimatedLength.zig"),
|
||||
@import("../webapi/svg/AnimatedNumber.zig"),
|
||||
@import("../webapi/svg/PreserveAspectRatio.zig"),
|
||||
@import("../webapi/svg/AnimatedPreserveAspectRatio.zig"),
|
||||
@import("../webapi/svg/PointList.zig"),
|
||||
|
||||
173
src/browser/tests/element/svg/geometry.html
Normal file
173
src/browser/tests/element/svg/geometry.html
Normal file
@@ -0,0 +1,173 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<svg id=root width="200" height="100">
|
||||
<rect id=rect x="10" y="20" width="30" height="40" rx="20"></rect>
|
||||
<circle id=circle cx="50" cy="30" r="10"></circle>
|
||||
<ellipse id=ellipse cx="70" cy="40" rx="20" ry="5"></ellipse>
|
||||
<line id=line x1="1" y1="2" x2="4" y2="6"></line>
|
||||
<polygon id=polygon points="0,0 10,0 10,10"></polygon>
|
||||
<polyline id=polyline points="-5,-2 3,7 12,1"></polyline>
|
||||
<path id=cubic d="M0 0 C0 100 100 100 100 0"></path>
|
||||
<path id=arc d="M0 0 A80 20 45 1 1 100 100"></path>
|
||||
<path id=partial d="M10 10 L20 20 30"></path>
|
||||
<path id=moves d="M0 0 10 10 20 0"></path>
|
||||
<g id=transformed transform="translate(10 20)">
|
||||
<rect x="0" y="0" width="10" height="5"></rect>
|
||||
<g transform="scale(2)"><circle cx="10" cy="10" r="2"></circle></g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<script id=typedShapeProperties>
|
||||
{
|
||||
const rect = $('#rect');
|
||||
testing.expectEqual(true, rect.x instanceof SVGAnimatedLength);
|
||||
testing.expectEqual(true, rect.x === rect.x);
|
||||
testing.expectEqual(true, rect.width.baseVal === rect.width.baseVal);
|
||||
testing.expectEqual(false, rect.width.baseVal === rect.width.animVal);
|
||||
testing.expectEqual(30, rect.width.baseVal.value);
|
||||
rect.width.baseVal.value = 35;
|
||||
testing.expectEqual('35', rect.getAttribute('width'));
|
||||
testing.expectEqual(35, rect.width.animVal.value);
|
||||
testing.expectError('NoModificationAllowedError', () => { rect.width.animVal.value = 5; });
|
||||
|
||||
testing.expectEqual(true, $('#circle').r instanceof SVGAnimatedLength);
|
||||
testing.expectEqual(true, $('#ellipse').rx instanceof SVGAnimatedLength);
|
||||
testing.expectEqual(true, $('#line').x2 instanceof SVGAnimatedLength);
|
||||
|
||||
const pathLength = rect.pathLength;
|
||||
testing.expectEqual(true, pathLength instanceof SVGAnimatedNumber);
|
||||
testing.expectEqual(true, pathLength === rect.pathLength);
|
||||
testing.expectEqual('number', typeof pathLength.baseVal);
|
||||
testing.expectEqual(0, pathLength.baseVal);
|
||||
pathLength.baseVal = 75;
|
||||
testing.expectEqual('75', rect.getAttribute('pathLength'));
|
||||
testing.expectEqual(75, pathLength.animVal);
|
||||
testing.expectError('TypeError', () => { pathLength.baseVal = 'invalid'; });
|
||||
testing.expectError('TypeError', () => { pathLength.baseVal = NaN; });
|
||||
testing.expectError('TypeError', () => { pathLength.baseVal = Infinity; });
|
||||
testing.expectEqual(75, pathLength.baseVal);
|
||||
testing.expectEqual('75', rect.getAttribute('pathLength'));
|
||||
testing.expectEqual(false, Reflect.set(pathLength, 'animVal', 4));
|
||||
testing.expectEqual(75, pathLength.animVal);
|
||||
rect.setAttribute('pathLength', 'invalid');
|
||||
testing.expectEqual(0, pathLength.baseVal);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=prototypePlacementAndShapeBounds>
|
||||
{
|
||||
testing.expectEqual(true, Object.prototype.hasOwnProperty.call(SVGGraphicsElement.prototype, 'getBBox'));
|
||||
testing.expectEqual(false, Object.prototype.hasOwnProperty.call(SVGGeometryElement.prototype, 'getBBox'));
|
||||
testing.expectEqual(false, Object.prototype.hasOwnProperty.call(SVGRectElement.prototype, 'getBBox'));
|
||||
|
||||
let box = $('#rect').getBBox();
|
||||
testing.expectEqual(10, box.x);
|
||||
testing.expectEqual(20, box.y);
|
||||
testing.expectEqual(35, box.width);
|
||||
testing.expectEqual(40, box.height);
|
||||
testing.expectEqual(true, box instanceof DOMRect);
|
||||
|
||||
box = $('#circle').getBBox();
|
||||
testing.expectEqual(40, box.x);
|
||||
testing.expectEqual(20, box.y);
|
||||
testing.expectEqual(20, box.width);
|
||||
testing.expectEqual(20, box.height);
|
||||
|
||||
box = $('#ellipse').getBBox();
|
||||
testing.expectEqual(50, box.x);
|
||||
testing.expectEqual(35, box.y);
|
||||
testing.expectEqual(40, box.width);
|
||||
testing.expectEqual(10, box.height);
|
||||
|
||||
box = $('#line').getBBox();
|
||||
testing.expectEqual(1, box.x);
|
||||
testing.expectEqual(2, box.y);
|
||||
testing.expectEqual(3, box.width);
|
||||
testing.expectEqual(4, box.height);
|
||||
|
||||
box = $('#polyline').getBBox();
|
||||
testing.expectEqual(-5, box.x);
|
||||
testing.expectEqual(-2, box.y);
|
||||
testing.expectEqual(17, box.width);
|
||||
testing.expectEqual(9, box.height);
|
||||
|
||||
box = $('#circle').getBBox({fill: false});
|
||||
testing.expectEqual(0, box.width);
|
||||
testing.expectError('NotSupportedError', () => $('#circle').getBBox({stroke: true}));
|
||||
|
||||
$('#circle').setAttribute('r', '-2');
|
||||
testing.expectEqual(0, $('#circle').getBBox().width);
|
||||
$('#circle').setAttribute('r', '10');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=pathParsingAndBounds>
|
||||
{
|
||||
let box = $('#cubic').getBBox();
|
||||
testing.expectEqual(0, box.x);
|
||||
testing.expectEqual(0, box.y);
|
||||
testing.expectEqual(100, box.width);
|
||||
testing.expectEqual(75, box.height);
|
||||
|
||||
box = $('#partial').getBBox();
|
||||
testing.expectEqual(10, box.x);
|
||||
testing.expectEqual(10, box.y);
|
||||
testing.expectEqual(10, box.width);
|
||||
testing.expectEqual(10, box.height);
|
||||
|
||||
box = $('#moves').getBBox();
|
||||
testing.expectEqual(0, box.x);
|
||||
testing.expectEqual(0, box.y);
|
||||
testing.expectEqual(20, box.width);
|
||||
testing.expectEqual(10, box.height);
|
||||
|
||||
box = $('#arc').getBBox();
|
||||
testing.expectEqual(true, box.x < 0);
|
||||
testing.expectEqual(true, box.width > 100);
|
||||
testing.expectEqual(true, box.height >= 100);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=lengthAndPointAtLength>
|
||||
{
|
||||
testing.expectEqual(5, $('#line').getTotalLength());
|
||||
let point = $('#line').getPointAtLength(2.5);
|
||||
testing.expectEqual(true, point instanceof DOMPoint);
|
||||
testing.expectEqual(2.5, point.x);
|
||||
testing.expectEqual(4, point.y);
|
||||
point = $('#line').getPointAtLength(-100);
|
||||
testing.expectEqual(1, point.x);
|
||||
testing.expectEqual(2, point.y);
|
||||
point = $('#line').getPointAtLength(100);
|
||||
testing.expectEqual(4, point.x);
|
||||
testing.expectEqual(6, point.y);
|
||||
|
||||
const cubic = $('#cubic');
|
||||
testing.expectEqual(true, Math.abs(cubic.getTotalLength() - 200) < 0.01);
|
||||
point = cubic.getPointAtLength(cubic.getTotalLength() / 2);
|
||||
testing.expectEqual(true, Math.abs(point.x - 50) < 0.01);
|
||||
testing.expectEqual(true, Math.abs(point.y - 75) < 0.01);
|
||||
|
||||
// pathLength calibrates author distance, not these native geometry methods.
|
||||
cubic.pathLength.baseVal.value = 10;
|
||||
testing.expectEqual(true, Math.abs(cubic.getTotalLength() - 200) < 0.01);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=containerTransforms>
|
||||
{
|
||||
const box = $('#transformed').getBBox();
|
||||
// The group's own translate is excluded. Its direct rect occupies 0..10,
|
||||
// while the scaled child circle occupies 16..24 on both axes.
|
||||
testing.expectEqual(0, box.x);
|
||||
testing.expectEqual(0, box.y);
|
||||
testing.expectEqual(24, box.width);
|
||||
testing.expectEqual(24, box.height);
|
||||
|
||||
const rootBox = $('#root').getBBox();
|
||||
// From the root, the group's transform is included.
|
||||
testing.expectEqual(true, rootBox.width >= 100);
|
||||
testing.expectEqual(true, rootBox.height >= 100);
|
||||
}
|
||||
</script>
|
||||
@@ -38,6 +38,7 @@
|
||||
testing.expectEqual('function', typeof SVGPolylineElement);
|
||||
testing.expectEqual('function', typeof SVGAnimatedString);
|
||||
testing.expectEqual('function', typeof SVGNumber);
|
||||
testing.expectEqual('function', typeof SVGAnimatedNumber);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
|
||||
const Geometry = @import("Geometry.zig");
|
||||
const AnimatedLength = @import("../../svg/AnimatedLength.zig");
|
||||
|
||||
const Circle = @This();
|
||||
_proto: *Geometry,
|
||||
@@ -41,4 +43,18 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const cx = bridge.accessor(Circle.getCx, null, .{});
|
||||
pub const cy = bridge.accessor(Circle.getCy, null, .{});
|
||||
pub const r = bridge.accessor(Circle.getR, null, .{});
|
||||
};
|
||||
|
||||
pub fn getCx(self: *Circle, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .cx, frame);
|
||||
}
|
||||
pub fn getCy(self: *Circle, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .cy, frame);
|
||||
}
|
||||
pub fn getR(self: *Circle, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .r, frame);
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
|
||||
const Geometry = @import("Geometry.zig");
|
||||
const AnimatedLength = @import("../../svg/AnimatedLength.zig");
|
||||
|
||||
const Ellipse = @This();
|
||||
_proto: *Geometry,
|
||||
@@ -41,4 +43,22 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const cx = bridge.accessor(Ellipse.getCx, null, .{});
|
||||
pub const cy = bridge.accessor(Ellipse.getCy, null, .{});
|
||||
pub const rx = bridge.accessor(Ellipse.getRx, null, .{});
|
||||
pub const ry = bridge.accessor(Ellipse.getRy, null, .{});
|
||||
};
|
||||
|
||||
pub fn getCx(self: *Ellipse, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .cx, frame);
|
||||
}
|
||||
pub fn getCy(self: *Ellipse, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .cy, frame);
|
||||
}
|
||||
pub fn getRx(self: *Ellipse, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .rx, frame);
|
||||
}
|
||||
pub fn getRy(self: *Ellipse, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .ry, frame);
|
||||
}
|
||||
|
||||
@@ -16,12 +16,19 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const DOMPoint = @import("../../DOMPoint.zig");
|
||||
|
||||
const Graphics = @import("Graphics.zig");
|
||||
const AnimatedNumber = @import("../../svg/AnimatedNumber.zig");
|
||||
const AnimatedLength = @import("../../svg/AnimatedLength.zig");
|
||||
const PathData = @import("../../svg/PathData.zig");
|
||||
pub const Rect = @import("Rect.zig");
|
||||
pub const Circle = @import("Circle.zig");
|
||||
pub const Ellipse = @import("Ellipse.zig");
|
||||
@@ -70,4 +77,174 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const pathLength = bridge.accessor(Geometry.getPathLength, null, .{});
|
||||
pub const getTotalLength = bridge.function(Geometry.getTotalLength, .{});
|
||||
pub const getPointAtLength = bridge.function(Geometry.getPointAtLength, .{});
|
||||
};
|
||||
|
||||
pub fn getPathLength(self: *Geometry, frame: *Frame) !*AnimatedNumber {
|
||||
return AnimatedNumber.getOrCreate(self.asElement(), .path_length, frame);
|
||||
}
|
||||
|
||||
pub fn getTotalLength(self: *Geometry, frame: *Frame) !f64 {
|
||||
var path = try self.buildPath(frame);
|
||||
defer path.deinit(frame.local_arena);
|
||||
return path.totalLength(frame.local_arena);
|
||||
}
|
||||
|
||||
pub fn getPointAtLength(self: *Geometry, distance: f64, frame: *Frame) !*DOMPoint {
|
||||
if (!std.math.isFinite(distance)) return error.TypeError;
|
||||
var path = try self.buildPath(frame);
|
||||
defer path.deinit(frame.local_arena);
|
||||
const point = try path.pointAtLength(distance, frame.local_arena);
|
||||
return DOMPoint.create(point.x, point.y, 0, 1, frame._page);
|
||||
}
|
||||
|
||||
pub fn buildPath(self: *Geometry, frame: *Frame) !PathData.Path {
|
||||
return switch (self._type) {
|
||||
.rect => |rect| buildRect(rect, frame),
|
||||
.circle => |circle| buildCircle(circle, frame),
|
||||
.ellipse => |ellipse| buildEllipse(ellipse, frame),
|
||||
.line => |line| buildLine(line, frame),
|
||||
.path => |path| PathData.parse(
|
||||
path.asElement().getAttributeSafe(comptime .wrap("d")) orelse "",
|
||||
frame.local_arena,
|
||||
),
|
||||
.polygon => |polygon| buildPoints(try polygon.getPoints(frame), true, frame),
|
||||
.polyline => |polyline| buildPoints(try polyline.getPoints(frame), false, frame),
|
||||
};
|
||||
}
|
||||
|
||||
fn value(length: *AnimatedLength, frame: *Frame) ?f64 {
|
||||
const base = length.getBaseVal();
|
||||
if (base.getUnitType() == 0) return null;
|
||||
const result = base.getValue(frame);
|
||||
return if (std.math.isFinite(result)) result else null;
|
||||
}
|
||||
|
||||
fn buildRect(rect: *Rect, frame: *Frame) !PathData.Path {
|
||||
var path: PathData.Path = .{};
|
||||
errdefer path.deinit(frame.local_arena);
|
||||
|
||||
const x = value(try rect.getX(frame), frame) orelse 0;
|
||||
const y = value(try rect.getY(frame), frame) orelse 0;
|
||||
const width = value(try rect.getWidth(frame), frame) orelse 0;
|
||||
const height = value(try rect.getHeight(frame), frame) orelse 0;
|
||||
if (width <= 0 or height <= 0) return path;
|
||||
|
||||
const rx_value = optionalRadius(rect.asElement(), "rx", try rect.getRx(frame), frame);
|
||||
const ry_value = optionalRadius(rect.asElement(), "ry", try rect.getRy(frame), frame);
|
||||
var rx = rx_value orelse ry_value orelse 0;
|
||||
var ry = ry_value orelse rx_value orelse 0;
|
||||
rx = @min(rx, width / 2.0);
|
||||
ry = @min(ry, height / 2.0);
|
||||
|
||||
const top_left = PathData.Point{ .x = x, .y = y };
|
||||
if (rx == 0 or ry == 0) {
|
||||
const top_right = PathData.Point{ .x = x + width, .y = y };
|
||||
const bottom_right = PathData.Point{ .x = x + width, .y = y + height };
|
||||
const bottom_left = PathData.Point{ .x = x, .y = y + height };
|
||||
try path.appendLine(top_left, top_right, frame.local_arena);
|
||||
try path.appendLine(top_right, bottom_right, frame.local_arena);
|
||||
try path.appendLine(bottom_right, bottom_left, frame.local_arena);
|
||||
try path.appendLine(bottom_left, top_left, frame.local_arena);
|
||||
return path;
|
||||
}
|
||||
|
||||
const start = PathData.Point{ .x = x + rx, .y = y };
|
||||
const top_end = PathData.Point{ .x = x + width - rx, .y = y };
|
||||
const right_start = PathData.Point{ .x = x + width, .y = y + ry };
|
||||
const right_end = PathData.Point{ .x = x + width, .y = y + height - ry };
|
||||
const bottom_start = PathData.Point{ .x = x + width - rx, .y = y + height };
|
||||
const bottom_end = PathData.Point{ .x = x + rx, .y = y + height };
|
||||
const left_start = PathData.Point{ .x = x, .y = y + height - ry };
|
||||
const left_end = PathData.Point{ .x = x, .y = y + ry };
|
||||
|
||||
try path.appendLine(start, top_end, frame.local_arena);
|
||||
try path.appendArc(top_end, rx, ry, 0, false, true, right_start, frame.local_arena);
|
||||
try path.appendLine(right_start, right_end, frame.local_arena);
|
||||
try path.appendArc(right_end, rx, ry, 0, false, true, bottom_start, frame.local_arena);
|
||||
try path.appendLine(bottom_start, bottom_end, frame.local_arena);
|
||||
try path.appendArc(bottom_end, rx, ry, 0, false, true, left_start, frame.local_arena);
|
||||
try path.appendLine(left_start, left_end, frame.local_arena);
|
||||
try path.appendArc(left_end, rx, ry, 0, false, true, start, frame.local_arena);
|
||||
return path;
|
||||
}
|
||||
|
||||
fn optionalRadius(element: *Element, comptime name: []const u8, length: *AnimatedLength, frame: *Frame) ?f64 {
|
||||
const raw = element.getAttributeSafe(comptime .wrap(name)) orelse return null;
|
||||
if (std.ascii.eqlIgnoreCase(std.mem.trim(u8, raw, " \t\r\n\x0c"), "auto")) return null;
|
||||
const result = value(length, frame) orelse return null;
|
||||
return if (result < 0) null else result;
|
||||
}
|
||||
|
||||
fn buildCircle(circle: *Circle, frame: *Frame) !PathData.Path {
|
||||
const center = PathData.Point{
|
||||
.x = value(try circle.getCx(frame), frame) orelse 0,
|
||||
.y = value(try circle.getCy(frame), frame) orelse 0,
|
||||
};
|
||||
const radius = value(try circle.getR(frame), frame) orelse 0;
|
||||
return buildEllipsePath(center, radius, radius, frame);
|
||||
}
|
||||
|
||||
fn buildEllipse(ellipse: *Ellipse, frame: *Frame) !PathData.Path {
|
||||
const center = PathData.Point{
|
||||
.x = value(try ellipse.getCx(frame), frame) orelse 0,
|
||||
.y = value(try ellipse.getCy(frame), frame) orelse 0,
|
||||
};
|
||||
const rx = optionalRadius(ellipse.asElement(), "rx", try ellipse.getRx(frame), frame);
|
||||
const ry = optionalRadius(ellipse.asElement(), "ry", try ellipse.getRy(frame), frame);
|
||||
return buildEllipsePath(center, rx orelse ry orelse 0, ry orelse rx orelse 0, frame);
|
||||
}
|
||||
|
||||
fn buildEllipsePath(center: PathData.Point, rx: f64, ry: f64, frame: *Frame) !PathData.Path {
|
||||
var path: PathData.Path = .{};
|
||||
errdefer path.deinit(frame.local_arena);
|
||||
if (rx <= 0 or ry <= 0) return path;
|
||||
|
||||
const right = PathData.Point{ .x = center.x + rx, .y = center.y };
|
||||
const bottom = PathData.Point{ .x = center.x, .y = center.y + ry };
|
||||
const left = PathData.Point{ .x = center.x - rx, .y = center.y };
|
||||
const top = PathData.Point{ .x = center.x, .y = center.y - ry };
|
||||
try path.appendArc(right, rx, ry, 0, false, true, bottom, frame.local_arena);
|
||||
try path.appendArc(bottom, rx, ry, 0, false, true, left, frame.local_arena);
|
||||
try path.appendArc(left, rx, ry, 0, false, true, top, frame.local_arena);
|
||||
try path.appendArc(top, rx, ry, 0, false, true, right, frame.local_arena);
|
||||
return path;
|
||||
}
|
||||
|
||||
fn buildLine(line: *Line, frame: *Frame) !PathData.Path {
|
||||
var path: PathData.Path = .{};
|
||||
errdefer path.deinit(frame.local_arena);
|
||||
const start = PathData.Point{
|
||||
.x = value(try line.getX1(frame), frame) orelse 0,
|
||||
.y = value(try line.getY1(frame), frame) orelse 0,
|
||||
};
|
||||
const end = PathData.Point{
|
||||
.x = value(try line.getX2(frame), frame) orelse 0,
|
||||
.y = value(try line.getY2(frame), frame) orelse 0,
|
||||
};
|
||||
try path.appendLine(start, end, frame.local_arena);
|
||||
return path;
|
||||
}
|
||||
|
||||
fn buildPoints(list: anytype, close: bool, frame: *Frame) !PathData.Path {
|
||||
var path: PathData.Path = .{};
|
||||
errdefer path.deinit(frame.local_arena);
|
||||
const count = try list.getNumberOfItems(frame);
|
||||
if (count == 0) return path;
|
||||
|
||||
const first_item = try list.getItem(0, frame);
|
||||
const first = PathData.Point{ .x = first_item.getX(), .y = first_item.getY() };
|
||||
path.first_point = first;
|
||||
var previous = first;
|
||||
for (1..count) |index| {
|
||||
const item = try list.getItem(@intCast(index), frame);
|
||||
const next = PathData.Point{ .x = item.getX(), .y = item.getY() };
|
||||
try path.appendLine(previous, next, frame.local_arena);
|
||||
previous = next;
|
||||
}
|
||||
if (close and count > 1) try path.appendLine(previous, first, frame.local_arena);
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -16,12 +16,17 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const DOMRect = @import("../../DOMRect.zig");
|
||||
const DOMMatrixReadOnly = @import("../../DOMMatrixReadOnly.zig");
|
||||
const SvgElement = @import("../Svg.zig");
|
||||
const AnimatedTransformList = @import("../../svg/AnimatedTransformList.zig");
|
||||
const PathData = @import("../../svg/PathData.zig");
|
||||
const StringList = @import("../../svg/StringList.zig");
|
||||
|
||||
pub const Svg = @import("Svg.zig");
|
||||
@@ -79,8 +84,83 @@ pub const JsApi = struct {
|
||||
pub const transform = bridge.accessor(Graphics.getTransform, null, .{});
|
||||
pub const requiredExtensions = bridge.accessor(Graphics.getRequiredExtensions, null, .{});
|
||||
pub const systemLanguage = bridge.accessor(Graphics.getSystemLanguage, null, .{});
|
||||
pub const getBBox = bridge.function(Graphics.getBBox, .{});
|
||||
};
|
||||
|
||||
const BoundingBoxOptions = struct {
|
||||
fill: bool = true,
|
||||
stroke: bool = false,
|
||||
markers: bool = false,
|
||||
clipped: bool = false,
|
||||
};
|
||||
|
||||
pub fn getBBox(self: *Graphics, options_: ?BoundingBoxOptions, frame: *Frame) !*DOMRect {
|
||||
const options = options_ orelse BoundingBoxOptions{};
|
||||
if (options.stroke or options.markers or options.clipped) return error.NotSupported;
|
||||
if (!options.fill) return DOMRect.create(.{}, frame._factory);
|
||||
|
||||
var bounds: PathData.Bounds = .{};
|
||||
switch (self._type) {
|
||||
.geometry => |geometry| {
|
||||
var path = try geometry.buildPath(frame);
|
||||
defer path.deinit(frame.local_arena);
|
||||
bounds = path.bounds(.{});
|
||||
},
|
||||
.g, .a, .svg => try accumulateChildren(self, .{}, &bounds, frame),
|
||||
.defs, .use, .image => return error.InvalidStateError,
|
||||
}
|
||||
if (bounds.isEmpty()) return DOMRect.create(.{}, frame._factory);
|
||||
return DOMRect.create(.{
|
||||
.x = bounds.min_x,
|
||||
.y = bounds.min_y,
|
||||
.width = bounds.width(),
|
||||
.height = bounds.height(),
|
||||
}, frame._factory);
|
||||
}
|
||||
|
||||
fn accumulateChildren(parent: *Graphics, matrix: PathData.Matrix, bounds: *PathData.Bounds, frame: *Frame) !void {
|
||||
var child = parent.asNode().firstChild();
|
||||
while (child) |node| : (child = node.nextSibling()) {
|
||||
const element = node.is(Element) orelse continue;
|
||||
if (element._namespace != .svg) continue;
|
||||
const svg = element.as(SvgElement);
|
||||
const graphics = svg.is(Graphics) orelse continue;
|
||||
const child_matrix = matrix.multiply(transformMatrix(element));
|
||||
|
||||
switch (graphics._type) {
|
||||
.geometry => |geometry| {
|
||||
var path = try geometry.buildPath(frame);
|
||||
defer path.deinit(frame.local_arena);
|
||||
bounds.merge(path.bounds(child_matrix));
|
||||
},
|
||||
.g, .a => try accumulateChildren(graphics, child_matrix, bounds, frame),
|
||||
.defs => {},
|
||||
.svg, .use, .image => return error.InvalidStateError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn transformMatrix(element: *Element) PathData.Matrix {
|
||||
const raw = element.getAttributeSafe(comptime .wrap("transform")) orelse return .{};
|
||||
const trimmed = std.mem.trim(u8, raw, " \t\r\n");
|
||||
if (trimmed.len == 0 or std.mem.eql(u8, trimmed, "none")) return .{};
|
||||
|
||||
var matrix = DOMMatrixReadOnly.identity();
|
||||
var iterator = DOMMatrixReadOnly.TransformFunctionIterator{ .input = trimmed, .allow_comma = true };
|
||||
while (iterator.next() catch return .{}) |function| {
|
||||
const parsed = DOMMatrixReadOnly.parseTransformFunction(function, .svg) catch return .{};
|
||||
matrix = DOMMatrixReadOnly.multiplyMatrix(matrix, parsed.matrix);
|
||||
}
|
||||
return .{
|
||||
.a = matrix[0],
|
||||
.b = matrix[1],
|
||||
.c = matrix[4],
|
||||
.d = matrix[5],
|
||||
.e = matrix[12],
|
||||
.f = matrix[13],
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getTransform(self: *Graphics, frame: *Frame) !*AnimatedTransformList {
|
||||
return AnimatedTransformList.getOrCreate(self.asElement(), frame);
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
|
||||
const Geometry = @import("Geometry.zig");
|
||||
const AnimatedLength = @import("../../svg/AnimatedLength.zig");
|
||||
|
||||
const Line = @This();
|
||||
_proto: *Geometry,
|
||||
@@ -41,4 +43,22 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const x1 = bridge.accessor(Line.getX1, null, .{});
|
||||
pub const y1 = bridge.accessor(Line.getY1, null, .{});
|
||||
pub const x2 = bridge.accessor(Line.getX2, null, .{});
|
||||
pub const y2 = bridge.accessor(Line.getY2, null, .{});
|
||||
};
|
||||
|
||||
pub fn getX1(self: *Line, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .x1, frame);
|
||||
}
|
||||
pub fn getY1(self: *Line, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .y1, frame);
|
||||
}
|
||||
pub fn getX2(self: *Line, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .x2, frame);
|
||||
}
|
||||
pub fn getY2(self: *Line, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .y2, frame);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const Geometry = @import("Geometry.zig");
|
||||
const AnimatedLength = @import("../../svg/AnimatedLength.zig");
|
||||
|
||||
const Rect = @This();
|
||||
_proto: *Geometry,
|
||||
@@ -39,4 +41,30 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const x = bridge.accessor(Rect.getX, null, .{});
|
||||
pub const y = bridge.accessor(Rect.getY, null, .{});
|
||||
pub const width = bridge.accessor(Rect.getWidth, null, .{});
|
||||
pub const height = bridge.accessor(Rect.getHeight, null, .{});
|
||||
pub const rx = bridge.accessor(Rect.getRx, null, .{});
|
||||
pub const ry = bridge.accessor(Rect.getRy, null, .{});
|
||||
};
|
||||
|
||||
pub fn getX(self: *Rect, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .x, frame);
|
||||
}
|
||||
pub fn getY(self: *Rect, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .y, frame);
|
||||
}
|
||||
pub fn getWidth(self: *Rect, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .width, frame);
|
||||
}
|
||||
pub fn getHeight(self: *Rect, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .height, frame);
|
||||
}
|
||||
pub fn getRx(self: *Rect, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .rx, frame);
|
||||
}
|
||||
pub fn getRy(self: *Rect, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .ry, frame);
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ pub fn createSVGRect(_: *Svg, frame: *Frame) !*DOMRect {
|
||||
}
|
||||
|
||||
pub fn createSVGNumber(_: *Svg, frame: *Frame) !*SvgNumber {
|
||||
return frame._factory.create(SvgNumber{});
|
||||
return SvgNumber.detached(frame);
|
||||
}
|
||||
|
||||
pub fn createSVGLength(_: *Svg, frame: *Frame) !*SvgLength {
|
||||
|
||||
@@ -34,6 +34,15 @@ pub const Kind = enum {
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
cx,
|
||||
cy,
|
||||
r,
|
||||
rx,
|
||||
ry,
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2,
|
||||
|
||||
fn attributeName(self: Kind) lp.String {
|
||||
return switch (self) {
|
||||
@@ -41,13 +50,23 @@ pub const Kind = enum {
|
||||
.y => comptime .wrap("y"),
|
||||
.width => comptime .wrap("width"),
|
||||
.height => comptime .wrap("height"),
|
||||
.cx => comptime .wrap("cx"),
|
||||
.cy => comptime .wrap("cy"),
|
||||
.r => comptime .wrap("r"),
|
||||
.rx => comptime .wrap("rx"),
|
||||
.ry => comptime .wrap("ry"),
|
||||
.x1 => comptime .wrap("x1"),
|
||||
.y1 => comptime .wrap("y1"),
|
||||
.x2 => comptime .wrap("x2"),
|
||||
.y2 => comptime .wrap("y2"),
|
||||
};
|
||||
}
|
||||
|
||||
fn direction(self: Kind) Length.Direction {
|
||||
return switch (self) {
|
||||
.x, .width => .horizontal,
|
||||
.y, .height => .vertical,
|
||||
.x, .width, .cx, .rx, .x1, .x2 => .horizontal,
|
||||
.y, .height, .cy, .ry, .y1, .y2 => .vertical,
|
||||
.r => .unspecified,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
96
src/browser/webapi/svg/AnimatedNumber.zig
Normal file
96
src/browser/webapi/svg/AnimatedNumber.zig
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Element = @import("../Element.zig");
|
||||
|
||||
const AnimatedNumber = @This();
|
||||
|
||||
_element: *Element,
|
||||
_attr_name: lp.String,
|
||||
_allow_percentage: bool,
|
||||
|
||||
pub const Kind = enum {
|
||||
path_length,
|
||||
offset,
|
||||
|
||||
fn attributeName(self: Kind) lp.String {
|
||||
return switch (self) {
|
||||
.path_length => comptime .wrap("pathLength"),
|
||||
.offset => comptime .wrap("offset"),
|
||||
};
|
||||
}
|
||||
|
||||
fn allowsPercentage(self: Kind) bool {
|
||||
return switch (self) {
|
||||
.path_length => false,
|
||||
.offset => true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Key = struct {
|
||||
element: *Element,
|
||||
kind: Kind,
|
||||
};
|
||||
|
||||
pub const Lookup = std.AutoHashMapUnmanaged(Key, *AnimatedNumber);
|
||||
|
||||
pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*AnimatedNumber {
|
||||
const key: Key = .{ .element = element, .kind = kind };
|
||||
const gop = try frame._svg_animated_numbers.getOrPut(frame.arena, key);
|
||||
if (!gop.found_existing) {
|
||||
errdefer _ = frame._svg_animated_numbers.remove(key);
|
||||
gop.value_ptr.* = try frame._factory.create(AnimatedNumber{
|
||||
._element = element,
|
||||
._attr_name = kind.attributeName(),
|
||||
._allow_percentage = kind.allowsPercentage(),
|
||||
});
|
||||
}
|
||||
return gop.value_ptr.*;
|
||||
}
|
||||
|
||||
pub fn getBaseVal(self: *const AnimatedNumber) f32 {
|
||||
return self.currentValue();
|
||||
}
|
||||
|
||||
pub fn setBaseVal(self: *AnimatedNumber, value: f32, frame: *Frame) !void {
|
||||
if (!std.math.isFinite(value)) return error.TypeError;
|
||||
const serialized = try std.fmt.allocPrint(frame.call_arena, "{d}", .{value});
|
||||
try self._element.setAttributeSafe(self._attr_name, lp.String.wrap(serialized), frame);
|
||||
}
|
||||
|
||||
pub fn getAnimVal(self: *const AnimatedNumber) f32 {
|
||||
return self.currentValue();
|
||||
}
|
||||
|
||||
fn currentValue(self: *const AnimatedNumber) f32 {
|
||||
const raw = self._element.getAttributeSafe(self._attr_name) orelse return 0;
|
||||
const trimmed = std.mem.trim(u8, raw, " \t\r\n\x0c");
|
||||
const value = if (self._allow_percentage and std.mem.endsWith(u8, trimmed, "%"))
|
||||
(std.fmt.parseFloat(f32, trimmed[0 .. trimmed.len - 1]) catch return 0) / 100
|
||||
else
|
||||
std.fmt.parseFloat(f32, trimmed) catch return 0;
|
||||
return if (std.math.isFinite(value)) value else 0;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(AnimatedNumber);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "SVGAnimatedNumber";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const baseVal = bridge.accessor(AnimatedNumber.getBaseVal, AnimatedNumber.setBaseVal, .{});
|
||||
pub const animVal = bridge.accessor(AnimatedNumber.getAnimVal, null, .{});
|
||||
};
|
||||
@@ -16,17 +16,54 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Element = @import("../Element.zig");
|
||||
|
||||
const Number = @This();
|
||||
_value: f32 = 0,
|
||||
_element: ?*Element = null,
|
||||
_attr_name: lp.String = .empty,
|
||||
_read_only: bool = false,
|
||||
|
||||
pub fn getValue(self: *const Number) f32 {
|
||||
pub fn detached(frame: *Frame) !*Number {
|
||||
return frame._factory.create(Number{});
|
||||
}
|
||||
|
||||
pub fn reflected(element: *Element, attr_name: lp.String, read_only: bool, frame: *Frame) !*Number {
|
||||
return frame._factory.create(Number{
|
||||
._element = element,
|
||||
._attr_name = attr_name,
|
||||
._read_only = read_only,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn getValue(self: *Number) f32 {
|
||||
self.syncFromAttribute();
|
||||
return self._value;
|
||||
}
|
||||
|
||||
pub fn setValue(self: *Number, value: f32) void {
|
||||
pub fn setValue(self: *Number, value: f32, frame: *Frame) !void {
|
||||
if (self._read_only) return error.NoModificationAllowed;
|
||||
if (!std.math.isFinite(value)) return error.TypeError;
|
||||
self._value = value;
|
||||
const element = self._element orelse return;
|
||||
const serialized = try std.fmt.allocPrint(frame.call_arena, "{d}", .{value});
|
||||
try element.setAttributeSafe(self._attr_name, lp.String.wrap(serialized), frame);
|
||||
}
|
||||
|
||||
fn syncFromAttribute(self: *Number) void {
|
||||
const element = self._element orelse return;
|
||||
const raw = element.getAttributeSafe(self._attr_name) orelse {
|
||||
self._value = 0;
|
||||
return;
|
||||
};
|
||||
const trimmed = std.mem.trim(u8, raw, " \t\r\n\x0c");
|
||||
self._value = std.fmt.parseFloat(f32, trimmed) catch 0;
|
||||
if (!std.math.isFinite(self._value)) self._value = 0;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
733
src/browser/webapi/svg/PathData.zig
Normal file
733
src/browser/webapi/svg/PathData.zig
Normal file
@@ -0,0 +1,733 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const tau = 2.0 * std.math.pi;
|
||||
|
||||
pub const Point = struct {
|
||||
x: f64,
|
||||
y: f64,
|
||||
|
||||
fn add(a: Point, b: Point) Point {
|
||||
return .{ .x = a.x + b.x, .y = a.y + b.y };
|
||||
}
|
||||
|
||||
fn midpoint(a: Point, b: Point) Point {
|
||||
return .{ .x = (a.x + b.x) / 2.0, .y = (a.y + b.y) / 2.0 };
|
||||
}
|
||||
};
|
||||
|
||||
pub const Matrix = struct {
|
||||
a: f64 = 1,
|
||||
b: f64 = 0,
|
||||
c: f64 = 0,
|
||||
d: f64 = 1,
|
||||
e: f64 = 0,
|
||||
f: f64 = 0,
|
||||
|
||||
pub fn apply(self: Matrix, point: Point) Point {
|
||||
return .{
|
||||
.x = self.a * point.x + self.c * point.y + self.e,
|
||||
.y = self.b * point.x + self.d * point.y + self.f,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn multiply(self: Matrix, child: Matrix) Matrix {
|
||||
return .{
|
||||
.a = self.a * child.a + self.c * child.b,
|
||||
.b = self.b * child.a + self.d * child.b,
|
||||
.c = self.a * child.c + self.c * child.d,
|
||||
.d = self.b * child.c + self.d * child.d,
|
||||
.e = self.a * child.e + self.c * child.f + self.e,
|
||||
.f = self.b * child.e + self.d * child.f + self.f,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Bounds = struct {
|
||||
min_x: f64 = std.math.inf(f64),
|
||||
min_y: f64 = std.math.inf(f64),
|
||||
max_x: f64 = -std.math.inf(f64),
|
||||
max_y: f64 = -std.math.inf(f64),
|
||||
|
||||
pub fn include(self: *Bounds, point: Point) void {
|
||||
self.min_x = @min(self.min_x, point.x);
|
||||
self.min_y = @min(self.min_y, point.y);
|
||||
self.max_x = @max(self.max_x, point.x);
|
||||
self.max_y = @max(self.max_y, point.y);
|
||||
}
|
||||
|
||||
pub fn merge(self: *Bounds, other: Bounds) void {
|
||||
if (other.isEmpty()) return;
|
||||
self.include(.{ .x = other.min_x, .y = other.min_y });
|
||||
self.include(.{ .x = other.max_x, .y = other.max_y });
|
||||
}
|
||||
|
||||
pub fn isEmpty(self: Bounds) bool {
|
||||
return self.min_x > self.max_x or self.min_y > self.max_y;
|
||||
}
|
||||
|
||||
pub fn width(self: Bounds) f64 {
|
||||
return if (self.isEmpty()) 0 else self.max_x - self.min_x;
|
||||
}
|
||||
|
||||
pub fn height(self: Bounds) f64 {
|
||||
return if (self.isEmpty()) 0 else self.max_y - self.min_y;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Line = struct { start: Point, end: Point };
|
||||
pub const Quadratic = struct { start: Point, control: Point, end: Point };
|
||||
pub const Cubic = struct { start: Point, control1: Point, control2: Point, end: Point };
|
||||
pub const Arc = struct {
|
||||
start: Point,
|
||||
end: Point,
|
||||
center: Point,
|
||||
rx: f64,
|
||||
ry: f64,
|
||||
rotation: f64,
|
||||
theta: f64,
|
||||
delta: f64,
|
||||
};
|
||||
|
||||
pub const Segment = union(enum) {
|
||||
line: Line,
|
||||
quadratic: Quadratic,
|
||||
cubic: Cubic,
|
||||
arc: Arc,
|
||||
|
||||
fn start(self: Segment) Point {
|
||||
return switch (self) {
|
||||
inline else => |segment| segment.start,
|
||||
};
|
||||
}
|
||||
|
||||
fn end(self: Segment) Point {
|
||||
return switch (self) {
|
||||
inline else => |segment| segment.end,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Path = struct {
|
||||
segments: std.ArrayList(Segment) = .empty,
|
||||
first_point: ?Point = null,
|
||||
|
||||
pub fn deinit(self: *Path, allocator: Allocator) void {
|
||||
self.segments.deinit(allocator);
|
||||
}
|
||||
|
||||
pub fn appendLine(self: *Path, start: Point, end: Point, allocator: Allocator) !void {
|
||||
if (self.first_point == null) self.first_point = start;
|
||||
try self.segments.append(allocator, .{ .line = .{ .start = start, .end = end } });
|
||||
}
|
||||
|
||||
pub fn appendQuadratic(self: *Path, segment: Quadratic, allocator: Allocator) !void {
|
||||
if (self.first_point == null) self.first_point = segment.start;
|
||||
try self.segments.append(allocator, .{ .quadratic = segment });
|
||||
}
|
||||
|
||||
pub fn appendCubic(self: *Path, segment: Cubic, allocator: Allocator) !void {
|
||||
if (self.first_point == null) self.first_point = segment.start;
|
||||
try self.segments.append(allocator, .{ .cubic = segment });
|
||||
}
|
||||
|
||||
pub fn appendArc(self: *Path, start: Point, rx: f64, ry: f64, rotation: f64, large: bool, sweep: bool, end: Point, allocator: Allocator) !void {
|
||||
if (self.first_point == null) self.first_point = start;
|
||||
if (pointsEqual(start, end)) return;
|
||||
if (rx == 0 or ry == 0) return self.appendLine(start, end, allocator);
|
||||
const arc = endpointArc(start, rx, ry, rotation, large, sweep, end) orelse
|
||||
return self.appendLine(start, end, allocator);
|
||||
try self.segments.append(allocator, .{ .arc = arc });
|
||||
}
|
||||
|
||||
pub fn bounds(self: *const Path, matrix: Matrix) Bounds {
|
||||
var result: Bounds = .{};
|
||||
for (self.segments.items) |segment| includeSegmentBounds(&result, segment, matrix);
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn totalLength(self: *const Path, allocator: Allocator) !f64 {
|
||||
var total: f64 = 0;
|
||||
for (self.segments.items) |segment| total += try segmentLength(segment, allocator);
|
||||
return total;
|
||||
}
|
||||
|
||||
pub fn pointAtLength(self: *const Path, requested: f64, allocator: Allocator) !Point {
|
||||
if (self.segments.items.len == 0) return self.first_point orelse .{ .x = 0, .y = 0 };
|
||||
|
||||
var remaining = @max(requested, 0);
|
||||
for (self.segments.items, 0..) |segment, segment_index| {
|
||||
var points: std.ArrayList(Point) = .empty;
|
||||
defer points.deinit(allocator);
|
||||
try flatten(segment, &points, allocator);
|
||||
|
||||
for (points.items[0 .. points.items.len - 1], points.items[1..]) |a, b| {
|
||||
const length = distance(a, b);
|
||||
if (remaining <= length) {
|
||||
if (length == 0) return b;
|
||||
const ratio = remaining / length;
|
||||
return .{
|
||||
.x = a.x + (b.x - a.x) * ratio,
|
||||
.y = a.y + (b.y - a.y) * ratio,
|
||||
};
|
||||
}
|
||||
remaining -= length;
|
||||
}
|
||||
|
||||
if (segment_index + 1 == self.segments.items.len) return segment.end();
|
||||
}
|
||||
unreachable;
|
||||
}
|
||||
};
|
||||
|
||||
const Previous = enum { other, cubic, quadratic };
|
||||
|
||||
pub fn parse(input: []const u8, allocator: Allocator) !Path {
|
||||
var path: Path = .{};
|
||||
errdefer path.deinit(allocator);
|
||||
|
||||
var parser = Parser{ .input = input };
|
||||
var command: ?u8 = null;
|
||||
var command_fresh = false;
|
||||
var current = Point{ .x = 0, .y = 0 };
|
||||
var subpath_start = current;
|
||||
var last_control = current;
|
||||
var previous: Previous = .other;
|
||||
var saw_moveto = false;
|
||||
|
||||
while (true) {
|
||||
parser.skipWhitespace();
|
||||
if (parser.atEnd()) break;
|
||||
|
||||
const byte = parser.peek();
|
||||
if (isCommand(byte)) {
|
||||
parser.index += 1;
|
||||
command = byte;
|
||||
command_fresh = true;
|
||||
|
||||
if (upper(byte) == 'Z') {
|
||||
if (!saw_moveto) break;
|
||||
try path.appendLine(current, subpath_start, allocator);
|
||||
current = subpath_start;
|
||||
previous = .other;
|
||||
command = null;
|
||||
continue;
|
||||
}
|
||||
} else if (std.ascii.isAlphabetic(byte) or command == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
const cmd = command.?;
|
||||
const kind = upper(cmd);
|
||||
if (!saw_moveto and kind != 'M') break;
|
||||
const relative = std.ascii.isLower(cmd);
|
||||
const allow_first_comma = !command_fresh;
|
||||
|
||||
switch (kind) {
|
||||
'M', 'L' => {
|
||||
const x = parser.number(allow_first_comma) orelse break;
|
||||
const y = parser.number(true) orelse break;
|
||||
const end = absolutePoint(current, x, y, relative);
|
||||
if (kind == 'M') {
|
||||
current = end;
|
||||
subpath_start = end;
|
||||
if (path.first_point == null) path.first_point = end;
|
||||
saw_moveto = true;
|
||||
command = if (relative) 'l' else 'L';
|
||||
} else {
|
||||
try path.appendLine(current, end, allocator);
|
||||
current = end;
|
||||
}
|
||||
previous = .other;
|
||||
},
|
||||
'H' => {
|
||||
const x = parser.number(allow_first_comma) orelse break;
|
||||
const end = Point{ .x = if (relative) current.x + x else x, .y = current.y };
|
||||
try path.appendLine(current, end, allocator);
|
||||
current = end;
|
||||
previous = .other;
|
||||
},
|
||||
'V' => {
|
||||
const y = parser.number(allow_first_comma) orelse break;
|
||||
const end = Point{ .x = current.x, .y = if (relative) current.y + y else y };
|
||||
try path.appendLine(current, end, allocator);
|
||||
current = end;
|
||||
previous = .other;
|
||||
},
|
||||
'C' => {
|
||||
const x1 = parser.number(allow_first_comma) orelse break;
|
||||
const y1 = parser.number(true) orelse break;
|
||||
const x2 = parser.number(true) orelse break;
|
||||
const y2 = parser.number(true) orelse break;
|
||||
const x = parser.number(true) orelse break;
|
||||
const y = parser.number(true) orelse break;
|
||||
const control1 = absolutePoint(current, x1, y1, relative);
|
||||
const control2 = absolutePoint(current, x2, y2, relative);
|
||||
const end = absolutePoint(current, x, y, relative);
|
||||
try path.appendCubic(.{ .start = current, .control1 = control1, .control2 = control2, .end = end }, allocator);
|
||||
current = end;
|
||||
last_control = control2;
|
||||
previous = .cubic;
|
||||
},
|
||||
'S' => {
|
||||
const x2 = parser.number(allow_first_comma) orelse break;
|
||||
const y2 = parser.number(true) orelse break;
|
||||
const x = parser.number(true) orelse break;
|
||||
const y = parser.number(true) orelse break;
|
||||
const control1 = if (previous == .cubic) reflect(last_control, current) else current;
|
||||
const control2 = absolutePoint(current, x2, y2, relative);
|
||||
const end = absolutePoint(current, x, y, relative);
|
||||
try path.appendCubic(.{ .start = current, .control1 = control1, .control2 = control2, .end = end }, allocator);
|
||||
current = end;
|
||||
last_control = control2;
|
||||
previous = .cubic;
|
||||
},
|
||||
'Q' => {
|
||||
const x1 = parser.number(allow_first_comma) orelse break;
|
||||
const y1 = parser.number(true) orelse break;
|
||||
const x = parser.number(true) orelse break;
|
||||
const y = parser.number(true) orelse break;
|
||||
const control = absolutePoint(current, x1, y1, relative);
|
||||
const end = absolutePoint(current, x, y, relative);
|
||||
try path.appendQuadratic(.{ .start = current, .control = control, .end = end }, allocator);
|
||||
current = end;
|
||||
last_control = control;
|
||||
previous = .quadratic;
|
||||
},
|
||||
'T' => {
|
||||
const x = parser.number(allow_first_comma) orelse break;
|
||||
const y = parser.number(true) orelse break;
|
||||
const control = if (previous == .quadratic) reflect(last_control, current) else current;
|
||||
const end = absolutePoint(current, x, y, relative);
|
||||
try path.appendQuadratic(.{ .start = current, .control = control, .end = end }, allocator);
|
||||
current = end;
|
||||
last_control = control;
|
||||
previous = .quadratic;
|
||||
},
|
||||
'A' => {
|
||||
const rx = parser.number(allow_first_comma) orelse break;
|
||||
const ry = parser.number(true) orelse break;
|
||||
const rotation = parser.number(true) orelse break;
|
||||
const large = parser.flag(true) orelse break;
|
||||
const sweep = parser.flag(true) orelse break;
|
||||
const x = parser.number(true) orelse break;
|
||||
const y = parser.number(true) orelse break;
|
||||
const end = absolutePoint(current, x, y, relative);
|
||||
try path.appendArc(current, rx, ry, rotation, large, sweep, end, allocator);
|
||||
current = end;
|
||||
previous = .other;
|
||||
},
|
||||
else => break,
|
||||
}
|
||||
command_fresh = false;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
const Parser = struct {
|
||||
input: []const u8,
|
||||
index: usize = 0,
|
||||
|
||||
fn atEnd(self: Parser) bool {
|
||||
return self.index >= self.input.len;
|
||||
}
|
||||
|
||||
fn peek(self: Parser) u8 {
|
||||
return self.input[self.index];
|
||||
}
|
||||
|
||||
fn skipWhitespace(self: *Parser) void {
|
||||
while (!self.atEnd() and isWhitespace(self.peek())) self.index += 1;
|
||||
}
|
||||
|
||||
fn separator(self: *Parser, allow_comma: bool) bool {
|
||||
self.skipWhitespace();
|
||||
if (!self.atEnd() and self.peek() == ',') {
|
||||
if (!allow_comma) return false;
|
||||
self.index += 1;
|
||||
self.skipWhitespace();
|
||||
if (self.atEnd() or self.peek() == ',') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn number(self: *Parser, allow_comma: bool) ?f64 {
|
||||
if (!self.separator(allow_comma)) return null;
|
||||
const start = self.index;
|
||||
if (self.atEnd()) return null;
|
||||
|
||||
if (self.peek() == '+' or self.peek() == '-') self.index += 1;
|
||||
var digits: usize = 0;
|
||||
while (!self.atEnd() and std.ascii.isDigit(self.peek())) : (self.index += 1) digits += 1;
|
||||
if (!self.atEnd() and self.peek() == '.') {
|
||||
self.index += 1;
|
||||
while (!self.atEnd() and std.ascii.isDigit(self.peek())) : (self.index += 1) digits += 1;
|
||||
}
|
||||
if (digits == 0) {
|
||||
self.index = start;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!self.atEnd() and (self.peek() == 'e' or self.peek() == 'E')) {
|
||||
self.index += 1;
|
||||
if (!self.atEnd() and (self.peek() == '+' or self.peek() == '-')) self.index += 1;
|
||||
const exponent_start = self.index;
|
||||
while (!self.atEnd() and std.ascii.isDigit(self.peek())) self.index += 1;
|
||||
if (self.index == exponent_start) {
|
||||
self.index = start;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const value = std.fmt.parseFloat(f64, self.input[start..self.index]) catch {
|
||||
self.index = start;
|
||||
return null;
|
||||
};
|
||||
if (!std.math.isFinite(value)) {
|
||||
self.index = start;
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
fn flag(self: *Parser, allow_comma: bool) ?bool {
|
||||
if (!self.separator(allow_comma) or self.atEnd()) return null;
|
||||
return switch (self.peek()) {
|
||||
'0' => blk: {
|
||||
self.index += 1;
|
||||
break :blk false;
|
||||
},
|
||||
'1' => blk: {
|
||||
self.index += 1;
|
||||
break :blk true;
|
||||
},
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
fn isWhitespace(byte: u8) bool {
|
||||
return byte == ' ' or byte == '\t' or byte == '\r' or byte == '\n' or byte == '\x0c';
|
||||
}
|
||||
|
||||
fn isCommand(byte: u8) bool {
|
||||
return switch (upper(byte)) {
|
||||
'M', 'Z', 'L', 'H', 'V', 'C', 'S', 'Q', 'T', 'A' => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn upper(byte: u8) u8 {
|
||||
return if (byte >= 'a' and byte <= 'z') byte - ('a' - 'A') else byte;
|
||||
}
|
||||
|
||||
fn absolutePoint(origin: Point, x: f64, y: f64, relative: bool) Point {
|
||||
return if (relative) Point.add(origin, .{ .x = x, .y = y }) else .{ .x = x, .y = y };
|
||||
}
|
||||
|
||||
fn reflect(control: Point, around: Point) Point {
|
||||
return .{ .x = 2.0 * around.x - control.x, .y = 2.0 * around.y - control.y };
|
||||
}
|
||||
|
||||
fn pointsEqual(a: Point, b: Point) bool {
|
||||
return a.x == b.x and a.y == b.y;
|
||||
}
|
||||
|
||||
fn distance(a: Point, b: Point) f64 {
|
||||
return std.math.hypot(b.x - a.x, b.y - a.y);
|
||||
}
|
||||
|
||||
fn endpointArc(start: Point, rx_input: f64, ry_input: f64, rotation_degrees: f64, large: bool, sweep: bool, end: Point) ?Arc {
|
||||
var rx = @abs(rx_input);
|
||||
var ry = @abs(ry_input);
|
||||
if (rx == 0 or ry == 0 or pointsEqual(start, end)) return null;
|
||||
|
||||
const rotation = @mod(rotation_degrees, 360.0) * std.math.pi / 180.0;
|
||||
const cosine = @cos(rotation);
|
||||
const sine = @sin(rotation);
|
||||
const dx = (start.x - end.x) / 2.0;
|
||||
const dy = (start.y - end.y) / 2.0;
|
||||
const x_prime = cosine * dx + sine * dy;
|
||||
const y_prime = -sine * dx + cosine * dy;
|
||||
|
||||
const radii_scale = x_prime * x_prime / (rx * rx) + y_prime * y_prime / (ry * ry);
|
||||
if (radii_scale > 1) {
|
||||
const scale = @sqrt(radii_scale);
|
||||
rx *= scale;
|
||||
ry *= scale;
|
||||
}
|
||||
|
||||
const rx2 = rx * rx;
|
||||
const ry2 = ry * ry;
|
||||
const numerator = @max(0, rx2 * ry2 - rx2 * y_prime * y_prime - ry2 * x_prime * x_prime);
|
||||
const denominator = rx2 * y_prime * y_prime + ry2 * x_prime * x_prime;
|
||||
if (denominator == 0) return null;
|
||||
var coefficient = @sqrt(numerator / denominator);
|
||||
if (large == sweep) coefficient = -coefficient;
|
||||
|
||||
const center_prime = Point{
|
||||
.x = coefficient * rx * y_prime / ry,
|
||||
.y = -coefficient * ry * x_prime / rx,
|
||||
};
|
||||
const center = Point{
|
||||
.x = cosine * center_prime.x - sine * center_prime.y + (start.x + end.x) / 2.0,
|
||||
.y = sine * center_prime.x + cosine * center_prime.y + (start.y + end.y) / 2.0,
|
||||
};
|
||||
|
||||
const first = Point{
|
||||
.x = (x_prime - center_prime.x) / rx,
|
||||
.y = (y_prime - center_prime.y) / ry,
|
||||
};
|
||||
const second = Point{
|
||||
.x = (-x_prime - center_prime.x) / rx,
|
||||
.y = (-y_prime - center_prime.y) / ry,
|
||||
};
|
||||
const theta = std.math.atan2(first.y, first.x);
|
||||
var delta = std.math.atan2(first.x * second.y - first.y * second.x, first.x * second.x + first.y * second.y);
|
||||
if (sweep and delta < 0) delta += tau;
|
||||
if (!sweep and delta > 0) delta -= tau;
|
||||
|
||||
return .{
|
||||
.start = start,
|
||||
.end = end,
|
||||
.center = center,
|
||||
.rx = rx,
|
||||
.ry = ry,
|
||||
.rotation = rotation,
|
||||
.theta = theta,
|
||||
.delta = delta,
|
||||
};
|
||||
}
|
||||
|
||||
fn includeSegmentBounds(bounds: *Bounds, segment: Segment, matrix: Matrix) void {
|
||||
bounds.include(matrix.apply(segment.start()));
|
||||
bounds.include(matrix.apply(segment.end()));
|
||||
|
||||
switch (segment) {
|
||||
.line => {},
|
||||
.quadratic => |quadratic| {
|
||||
includeQuadraticExtremum(bounds, quadratic.start.x, quadratic.control.x, quadratic.end.x, quadratic, matrix);
|
||||
includeQuadraticExtremum(bounds, quadratic.start.y, quadratic.control.y, quadratic.end.y, quadratic, matrix);
|
||||
includeTransformedQuadraticExtrema(bounds, quadratic, matrix);
|
||||
},
|
||||
.cubic => |cubic| includeTransformedCubicExtrema(bounds, cubic, matrix),
|
||||
.arc => |arc| includeArcExtrema(bounds, arc, matrix),
|
||||
}
|
||||
}
|
||||
|
||||
// The direct-axis roots help the identity case and are harmless duplicates for
|
||||
// transformed paths. The transformed roots below are authoritative.
|
||||
fn includeQuadraticExtremum(bounds: *Bounds, p0: f64, p1: f64, p2: f64, quadratic: Quadratic, matrix: Matrix) void {
|
||||
const denominator = p0 - 2.0 * p1 + p2;
|
||||
if (@abs(denominator) < 1e-14) return;
|
||||
const t = (p0 - p1) / denominator;
|
||||
if (t > 0 and t < 1) bounds.include(matrix.apply(evalQuadratic(quadratic, t)));
|
||||
}
|
||||
|
||||
fn includeTransformedQuadraticExtrema(bounds: *Bounds, quadratic: Quadratic, matrix: Matrix) void {
|
||||
const p0 = matrix.apply(quadratic.start);
|
||||
const p1 = matrix.apply(quadratic.control);
|
||||
const p2 = matrix.apply(quadratic.end);
|
||||
for ([_][3]f64{ .{ p0.x, p1.x, p2.x }, .{ p0.y, p1.y, p2.y } }) |values| {
|
||||
const denominator = values[0] - 2.0 * values[1] + values[2];
|
||||
if (@abs(denominator) < 1e-14) continue;
|
||||
const t = (values[0] - values[1]) / denominator;
|
||||
if (t > 0 and t < 1) bounds.include(matrix.apply(evalQuadratic(quadratic, t)));
|
||||
}
|
||||
}
|
||||
|
||||
fn includeTransformedCubicExtrema(bounds: *Bounds, cubic: Cubic, matrix: Matrix) void {
|
||||
const p0 = matrix.apply(cubic.start);
|
||||
const p1 = matrix.apply(cubic.control1);
|
||||
const p2 = matrix.apply(cubic.control2);
|
||||
const p3 = matrix.apply(cubic.end);
|
||||
for ([_][4]f64{ .{ p0.x, p1.x, p2.x, p3.x }, .{ p0.y, p1.y, p2.y, p3.y } }) |values| {
|
||||
const a = -values[0] + 3.0 * values[1] - 3.0 * values[2] + values[3];
|
||||
const b = 2.0 * (values[0] - 2.0 * values[1] + values[2]);
|
||||
const c = values[1] - values[0];
|
||||
for (quadraticRoots(a, b, c)) |root| {
|
||||
const t = root orelse continue;
|
||||
if (t > 0 and t < 1) bounds.include(matrix.apply(evalCubic(cubic, t)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn includeArcExtrema(bounds: *Bounds, arc: Arc, matrix: Matrix) void {
|
||||
const cosine = @cos(arc.rotation);
|
||||
const sine = @sin(arc.rotation);
|
||||
|
||||
const x_cos = arc.rx * cosine;
|
||||
const x_sin = -arc.ry * sine;
|
||||
const y_cos = arc.rx * sine;
|
||||
const y_sin = arc.ry * cosine;
|
||||
|
||||
const transformed = [_][2]f64{
|
||||
.{ matrix.a * x_cos + matrix.c * y_cos, matrix.a * x_sin + matrix.c * y_sin },
|
||||
.{ matrix.b * x_cos + matrix.d * y_cos, matrix.b * x_sin + matrix.d * y_sin },
|
||||
};
|
||||
for (transformed) |coefficients| {
|
||||
const candidate = std.math.atan2(coefficients[1], coefficients[0]);
|
||||
for ([_]f64{ candidate, candidate + std.math.pi }) |angle| {
|
||||
if (angleInSweep(angle, arc.theta, arc.delta)) bounds.include(matrix.apply(evalArc(arc, angle)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn quadraticRoots(a: f64, b: f64, c: f64) [2]?f64 {
|
||||
if (@abs(a) < 1e-14) {
|
||||
if (@abs(b) < 1e-14) return .{ null, null };
|
||||
return .{ -c / b, null };
|
||||
}
|
||||
const discriminant = b * b - 4.0 * a * c;
|
||||
if (discriminant < 0) return .{ null, null };
|
||||
const root = @sqrt(discriminant);
|
||||
return .{ (-b + root) / (2.0 * a), (-b - root) / (2.0 * a) };
|
||||
}
|
||||
|
||||
fn angleInSweep(angle: f64, start: f64, delta: f64) bool {
|
||||
if (delta >= 0) return positiveAngle(angle - start) <= delta + 1e-12;
|
||||
return positiveAngle(start - angle) <= -delta + 1e-12;
|
||||
}
|
||||
|
||||
fn positiveAngle(angle: f64) f64 {
|
||||
var result = @mod(angle, tau);
|
||||
if (result < 0) result += tau;
|
||||
return result;
|
||||
}
|
||||
|
||||
fn evalQuadratic(segment: Quadratic, t: f64) Point {
|
||||
const inverse = 1.0 - t;
|
||||
return .{
|
||||
.x = inverse * inverse * segment.start.x + 2.0 * inverse * t * segment.control.x + t * t * segment.end.x,
|
||||
.y = inverse * inverse * segment.start.y + 2.0 * inverse * t * segment.control.y + t * t * segment.end.y,
|
||||
};
|
||||
}
|
||||
|
||||
fn evalCubic(segment: Cubic, t: f64) Point {
|
||||
const inverse = 1.0 - t;
|
||||
return .{
|
||||
.x = inverse * inverse * inverse * segment.start.x + 3.0 * inverse * inverse * t * segment.control1.x + 3.0 * inverse * t * t * segment.control2.x + t * t * t * segment.end.x,
|
||||
.y = inverse * inverse * inverse * segment.start.y + 3.0 * inverse * inverse * t * segment.control1.y + 3.0 * inverse * t * t * segment.control2.y + t * t * t * segment.end.y,
|
||||
};
|
||||
}
|
||||
|
||||
fn evalArc(arc: Arc, angle: f64) Point {
|
||||
const cosine = @cos(arc.rotation);
|
||||
const sine = @sin(arc.rotation);
|
||||
const x = arc.rx * @cos(angle);
|
||||
const y = arc.ry * @sin(angle);
|
||||
return .{
|
||||
.x = arc.center.x + cosine * x - sine * y,
|
||||
.y = arc.center.y + sine * x + cosine * y,
|
||||
};
|
||||
}
|
||||
|
||||
fn segmentLength(segment: Segment, allocator: Allocator) !f64 {
|
||||
var points: std.ArrayList(Point) = .empty;
|
||||
defer points.deinit(allocator);
|
||||
try flatten(segment, &points, allocator);
|
||||
|
||||
var total: f64 = 0;
|
||||
for (points.items[0 .. points.items.len - 1], points.items[1..]) |a, b| total += distance(a, b);
|
||||
return total;
|
||||
}
|
||||
|
||||
const flatten_tolerance = 0.001;
|
||||
const flatten_depth = 18;
|
||||
|
||||
fn flatten(segment: Segment, points: *std.ArrayList(Point), allocator: Allocator) !void {
|
||||
try points.append(allocator, segment.start());
|
||||
switch (segment) {
|
||||
.line => |line| try points.append(allocator, line.end),
|
||||
.quadratic => |quadratic| try flattenQuadratic(quadratic, points, allocator, 0),
|
||||
.cubic => |cubic| try flattenCubic(cubic, points, allocator, 0),
|
||||
.arc => |arc| try flattenArc(arc, arc.theta, arc.theta + arc.delta, arc.start, arc.end, points, allocator, 0),
|
||||
}
|
||||
}
|
||||
|
||||
fn flattenQuadratic(segment: Quadratic, points: *std.ArrayList(Point), allocator: Allocator, depth: usize) !void {
|
||||
if (depth >= flatten_depth or pointLineDistance(segment.control, segment.start, segment.end) <= flatten_tolerance) {
|
||||
return points.append(allocator, segment.end);
|
||||
}
|
||||
const a = Point.midpoint(segment.start, segment.control);
|
||||
const b = Point.midpoint(segment.control, segment.end);
|
||||
const middle = Point.midpoint(a, b);
|
||||
try flattenQuadratic(.{ .start = segment.start, .control = a, .end = middle }, points, allocator, depth + 1);
|
||||
try flattenQuadratic(.{ .start = middle, .control = b, .end = segment.end }, points, allocator, depth + 1);
|
||||
}
|
||||
|
||||
fn flattenCubic(segment: Cubic, points: *std.ArrayList(Point), allocator: Allocator, depth: usize) !void {
|
||||
const flatness = @max(
|
||||
pointLineDistance(segment.control1, segment.start, segment.end),
|
||||
pointLineDistance(segment.control2, segment.start, segment.end),
|
||||
);
|
||||
if (depth >= flatten_depth or flatness <= flatten_tolerance) return points.append(allocator, segment.end);
|
||||
|
||||
const a = Point.midpoint(segment.start, segment.control1);
|
||||
const b = Point.midpoint(segment.control1, segment.control2);
|
||||
const c = Point.midpoint(segment.control2, segment.end);
|
||||
const d = Point.midpoint(a, b);
|
||||
const e = Point.midpoint(b, c);
|
||||
const middle = Point.midpoint(d, e);
|
||||
try flattenCubic(.{ .start = segment.start, .control1 = a, .control2 = d, .end = middle }, points, allocator, depth + 1);
|
||||
try flattenCubic(.{ .start = middle, .control1 = e, .control2 = c, .end = segment.end }, points, allocator, depth + 1);
|
||||
}
|
||||
|
||||
fn flattenArc(arc: Arc, start_angle: f64, end_angle: f64, start: Point, end: Point, points: *std.ArrayList(Point), allocator: Allocator, depth: usize) !void {
|
||||
const middle_angle = (start_angle + end_angle) / 2.0;
|
||||
const middle = evalArc(arc, middle_angle);
|
||||
if (depth >= flatten_depth or pointLineDistance(middle, start, end) <= flatten_tolerance) {
|
||||
return points.append(allocator, end);
|
||||
}
|
||||
try flattenArc(arc, start_angle, middle_angle, start, middle, points, allocator, depth + 1);
|
||||
try flattenArc(arc, middle_angle, end_angle, middle, end, points, allocator, depth + 1);
|
||||
}
|
||||
|
||||
fn pointLineDistance(point: Point, start: Point, end: Point) f64 {
|
||||
const dx = end.x - start.x;
|
||||
const dy = end.y - start.y;
|
||||
const denominator = std.math.hypot(dx, dy);
|
||||
if (denominator == 0) return distance(point, start);
|
||||
return @abs(dy * point.x - dx * point.y + end.x * start.y - end.y * start.x) / denominator;
|
||||
}
|
||||
|
||||
test "path parser preserves complete packs before an error" {
|
||||
var path = try parse("M10 10 L20 20 30", std.testing.allocator);
|
||||
defer path.deinit(std.testing.allocator);
|
||||
try std.testing.expectEqual(@as(usize, 1), path.segments.items.len);
|
||||
const bounds = path.bounds(.{});
|
||||
try std.testing.expectEqual(@as(f64, 10), bounds.min_x);
|
||||
try std.testing.expectEqual(@as(f64, 20), bounds.max_x);
|
||||
}
|
||||
|
||||
test "moveto coordinate pairs become lines and malformed exponents stop" {
|
||||
var path = try parse("M0 0 10 10 20 1e L99 99", std.testing.allocator);
|
||||
defer path.deinit(std.testing.allocator);
|
||||
try std.testing.expectEqual(@as(usize, 1), path.segments.items.len);
|
||||
try std.testing.expectEqual(Point{ .x = 10, .y = 10 }, path.segments.items[0].end());
|
||||
}
|
||||
|
||||
test "rotated arc bounds use ellipse derivative extrema" {
|
||||
var path = try parse("M0 0 A80 20 45 1 1 100 100", std.testing.allocator);
|
||||
defer path.deinit(std.testing.allocator);
|
||||
const bounds = path.bounds(.{});
|
||||
try std.testing.expect(bounds.min_x < 0);
|
||||
try std.testing.expect(bounds.max_y >= 100);
|
||||
}
|
||||
|
||||
test "length and point lookup share adaptive geometry" {
|
||||
var path = try parse("M0 0 C0 100 100 100 100 0", std.testing.allocator);
|
||||
defer path.deinit(std.testing.allocator);
|
||||
const length = try path.totalLength(std.testing.allocator);
|
||||
const middle = try path.pointAtLength(length / 2.0, std.testing.allocator);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 50), middle.x, 0.01);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 75), middle.y, 0.01);
|
||||
}
|
||||
Reference in New Issue
Block a user