Merge pull request #3370 from lightpanda-io/svg-ctm

webapi: SVGGraphicsElement.getCTM and getScreenCTM
This commit is contained in:
Karl Seguin authored and GitHub committed 2026-09-01 18:13:54 +08:00
commit 40fb080509
2 files changed
+75

No files matched your search

@@ -210,3 +210,29 @@
testing.expectEqual(true, rootBox.height >= 100);
}
</script>
<script id=ctm>
{
const circle = document.querySelector('#transformed > g > circle');
const m = circle.getCTM();
testing.expectEqual(true, m instanceof DOMMatrix);
testing.expectEqual(2, m.a);
testing.expectEqual(0, m.b);
testing.expectEqual(0, m.c);
testing.expectEqual(2, m.d);
testing.expectEqual(10, m.e);
testing.expectEqual(20, m.f);
const root = $('#root').getCTM();
testing.expectEqual(1, root.a);
testing.expectEqual(0, root.e);
const rect = $('#root').getBoundingClientRect();
const s = circle.getScreenCTM();
testing.expectEqual(2, s.a);
testing.expectEqual(rect.x + 10, s.e);
testing.expectEqual(rect.y + 20, s.f);
testing.expectEqual(null, document.createElementNS('http://www.w3.org/2000/svg', 'rect').getCTM());
}
</script>
@@ -25,6 +25,7 @@ const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const Factory = @import("../../../Factory.zig");
const DOMRect = @import("../../DOMRect.zig");
const DOMMatrix = @import("../../DOMMatrix.zig");
const DOMMatrixReadOnly = @import("../../DOMMatrixReadOnly.zig");
const SvgElement = @import("../Svg.zig");
const AnimatedTransformList = @import("../../svg/AnimatedTransformList.zig");
@@ -128,6 +129,8 @@ pub const JsApi = struct {
pub const requiredExtensions = bridge.accessor(Graphics.getRequiredExtensions, null, .{});
pub const systemLanguage = bridge.accessor(Graphics.getSystemLanguage, null, .{});
pub const getBBox = bridge.function(Graphics.getBBox, .{});
pub const getCTM = bridge.function(Graphics.getCTM, .{});
pub const getScreenCTM = bridge.function(Graphics.getScreenCTM, .{});
};
// SVGBoundingBoxOptions is not modelled: Chrome declares no parameter at all
@@ -218,6 +221,52 @@ fn accumulateChildren(parent: *Graphics, matrix: PathData.Matrix, bounds: *PathD
}
}
/// Composes `transform` attributes from the element up to, excluding, its
/// nearest <svg>. Null when the element isn't in a document.
pub fn getCTM(self: *Graphics, frame: *Frame) !?*DOMMatrix {
return currentTransformMatrix(self, .viewport, frame);
}
/// Like getCTM through every enclosing <svg>, offset by where the outermost
/// one sits on the page. Nested viewport x/y are not applied.
pub fn getScreenCTM(self: *Graphics, frame: *Frame) !?*DOMMatrix {
return currentTransformMatrix(self, .screen, frame);
}
fn currentTransformMatrix(self: *Graphics, space: enum { viewport, screen }, frame: *Frame) !?*DOMMatrix {
if (!self.asNode().isConnected()) {
return null;
}
var matrix = transformMatrix(self.asElement());
var outermost: ?*Element = if (self._type == .svg) self.asElement() else null;
var node = self.asNode().parentNode();
while (node) |n| : (node = n.parentNode()) {
const element = n.is(Element) orelse break;
if (element._namespace != .svg) break;
const graphics = element.as(SvgElement).is(Graphics) orelse continue;
if (graphics._type == .svg) {
if (space == .viewport) break;
outermost = element;
}
matrix = transformMatrix(element).multiply(matrix);
}
if (space == .screen) {
if (outermost) |svg| {
const rect = svg.boundingClientRectValues(frame);
matrix = (PathData.Matrix{ .e = rect.x, .f = rect.y }).multiply(matrix);
}
}
return try DOMMatrix.create(.{
matrix.a, matrix.b, 0, 0,
matrix.c, matrix.d, 0, 0,
0, 0, 1, 0,
matrix.e, matrix.f, 0, 1,
}, true, frame._page);
}
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");