From 73b4dd09ac0d3401b0f24d6488d726f28f5d656c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Tue, 1 Sep 2026 10:11:26 +0200 Subject: [PATCH] webapi: SVGGraphicsElement.getCTM and getScreenCTM Composes transform attributes up to the nearest (getCTM) or through every enclosing , offset by the outermost one's client rect (getScreenCTM). Null when detached. d3-zoom, d3-brush and svg-pan-zoom call these at init and threw. --- src/browser/tests/element/svg/geometry.html | 26 +++++++++++ src/browser/webapi/element/svg/Graphics.zig | 49 +++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/browser/tests/element/svg/geometry.html b/src/browser/tests/element/svg/geometry.html index e3d67b71f..2e57db8d0 100644 --- a/src/browser/tests/element/svg/geometry.html +++ b/src/browser/tests/element/svg/geometry.html @@ -210,3 +210,29 @@ testing.expectEqual(true, rootBox.height >= 100); } + + diff --git a/src/browser/webapi/element/svg/Graphics.zig b/src/browser/webapi/element/svg/Graphics.zig index e4b4f9d05..d88c7e9d4 100644 --- a/src/browser/webapi/element/svg/Graphics.zig +++ b/src/browser/webapi/element/svg/Graphics.zig @@ -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 . 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 , 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");