From 3c1e1d250a8d013b1af4a273dc0e6827958c9c7e Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Thu, 4 Jun 2026 21:26:25 +0800 Subject: [PATCH 01/52] Accept baseline for NoiseFilter SVG export test. --- test/baseline/version.json | 1 + 1 file changed, 1 insertion(+) diff --git a/test/baseline/version.json b/test/baseline/version.json index fb22ceedbc..d8b0c6b968 100644 --- a/test/baseline/version.json +++ b/test/baseline/version.json @@ -8558,6 +8558,7 @@ }, "PAGXTest": { "LayerBuilderAPIConsistency": "f40e23b6", + "NoiseFilterModes": "fa66a3ae", "PrecomposedTextRender": "9b6dea6a", "html": { "blend_modes_showcase": "f94e413d", From c5a300d65115388b25ecad730278d1042584a963 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Fri, 5 Jun 2026 10:49:07 +0800 Subject: [PATCH 02/52] Add NoiseStyle node definition and SVG export support --- include/pagx/nodes/Node.h | 8 + include/pagx/nodes/NoiseFilter.h | 92 +++++ include/pagx/nodes/NoiseStyle.h | 86 +++++ include/pagx/types/NoiseMode.h | 41 +++ src/pagx/svg/SVGExporter.cpp | 560 ++++++++++++++++++++++++++++++- src/pagx/utils/StringParser.cpp | 4 + src/renderer/LayerBuilder.cpp | 44 +++ test/src/PAGXTest.cpp | 485 ++++++++++++++++---------- 8 files changed, 1130 insertions(+), 190 deletions(-) create mode 100644 include/pagx/nodes/NoiseFilter.h create mode 100644 include/pagx/nodes/NoiseStyle.h create mode 100644 include/pagx/types/NoiseMode.h diff --git a/include/pagx/nodes/Node.h b/include/pagx/nodes/Node.h index dd1362420a..4b5fb06cee 100644 --- a/include/pagx/nodes/Node.h +++ b/include/pagx/nodes/Node.h @@ -117,6 +117,10 @@ enum class NodeType { * A background blur layer style. */ BackgroundBlurStyle, + /** + * A noise layer style. + */ + NoiseStyle, // Layer Filters /** @@ -139,6 +143,10 @@ enum class NodeType { * A color matrix filter. */ ColorMatrixFilter, + /** + * A noise filter. + */ + NoiseFilter, // Elements (geometry, painters, modifiers, containers) /** diff --git a/include/pagx/nodes/NoiseFilter.h b/include/pagx/nodes/NoiseFilter.h new file mode 100644 index 0000000000..3d189c5080 --- /dev/null +++ b/include/pagx/nodes/NoiseFilter.h @@ -0,0 +1,92 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/LayerFilter.h" +#include "pagx/types/BlendMode.h" +#include "pagx/types/Color.h" +#include "pagx/types/NoiseMode.h" + +namespace pagx { + +/** + * A noise filter that overlays procedural Perlin noise on the layer. Three noise modes are + * available: Mono (single color), Duo (two complementary colors), and Multi (preserving original + * noise RGB with enhanced contrast). + */ +class NoiseFilter : public LayerFilter { + public: + /** + * The noise mode. The default value is Mono. + */ + NoiseMode mode = NoiseMode::Mono; + + /** + * The noise grain size. Larger values produce coarser grains. Must be positive. The default value + * is 4.0. + */ + float size = 4.0f; + + /** + * The noise density in [0, 1]. Controls the proportion of visible noise pixels. The default value + * is 0.5. + */ + float density = 0.5f; + + /** + * The random seed for the noise pattern. The default value is 0. + */ + float seed = 0.0f; + + /** + * The blend mode used to composite the noise with the source image. The default value is Normal. + */ + BlendMode blendMode = BlendMode::Normal; + + /** + * The noise color for Mono mode. The alpha component controls the noise opacity. + */ + Color color = {}; + + /** + * The first noise color for Duo mode. The alpha component controls its opacity. + */ + Color firstColor = {}; + + /** + * The second noise color for Duo mode. The alpha component controls its opacity. + */ + Color secondColor = {}; + + /** + * The overall noise opacity for Multi mode, in [0, 1]. The default value is 0.15. + */ + float opacity = 0.15f; + + NodeType nodeType() const override { + return NodeType::NoiseFilter; + } + + private: + NoiseFilter() = default; + + friend class PAGXDocument; +}; + +} // namespace pagx diff --git a/include/pagx/nodes/NoiseStyle.h b/include/pagx/nodes/NoiseStyle.h new file mode 100644 index 0000000000..ad5f37c919 --- /dev/null +++ b/include/pagx/nodes/NoiseStyle.h @@ -0,0 +1,86 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/LayerStyle.h" +#include "pagx/types/Color.h" +#include "pagx/types/NoiseMode.h" + +namespace pagx { + +/** + * A noise layer style that overlays procedural Perlin noise above the layer content. Three noise + * modes are available: Mono (single color), Duo (two complementary colors), and Multi (preserving + * original noise RGB with enhanced contrast). + */ +class NoiseStyle : public LayerStyle { + public: + /** + * The noise mode. The default value is Mono. + */ + NoiseMode mode = NoiseMode::Mono; + + /** + * The noise grain size. Larger values produce coarser grains. Must be positive. The default value + * is 4.0. + */ + float size = 4.0f; + + /** + * The noise density in [0, 1]. Controls the proportion of visible noise pixels. The default value + * is 0.5. + */ + float density = 0.5f; + + /** + * The random seed for the noise pattern. The default value is 0. + */ + float seed = 0.0f; + + /** + * The noise color for Mono mode. The alpha component controls the noise opacity. + */ + Color color = {}; + + /** + * The first noise color for Duo mode. The alpha component controls its opacity. + */ + Color firstColor = {}; + + /** + * The second noise color for Duo mode. The alpha component controls its opacity. + */ + Color secondColor = {}; + + /** + * The overall noise opacity for Multi mode, in [0, 1]. The default value is 0.15. + */ + float opacity = 0.15f; + + NodeType nodeType() const override { + return NodeType::NoiseStyle; + } + + private: + NoiseStyle() = default; + + friend class PAGXDocument; +}; + +} // namespace pagx diff --git a/include/pagx/types/NoiseMode.h b/include/pagx/types/NoiseMode.h new file mode 100644 index 0000000000..b3af31cf43 --- /dev/null +++ b/include/pagx/types/NoiseMode.h @@ -0,0 +1,41 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace pagx { + +/** + * Noise modes for the NoiseFilter. + */ +enum class NoiseMode { + /** + * Single-color noise. Noise pixels are filled with one color. + */ + Mono, + /** + * Dual-color noise. The noise source is split into two complementary regions. + */ + Duo, + /** + * Multi-color noise preserving original Perlin noise RGB with enhanced contrast. + */ + Multi +}; + +} // namespace pagx diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index 86eff3a726..dfcfc6f68a 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -49,6 +49,8 @@ #include "pagx/nodes/InnerShadowStyle.h" #include "pagx/nodes/LayerStyle.h" #include "pagx/nodes/LinearGradient.h" +#include "pagx/nodes/NoiseFilter.h" +#include "pagx/nodes/NoiseStyle.h" #include "pagx/nodes/Path.h" #include "pagx/nodes/PathData.h" #include "pagx/nodes/RadialGradient.h" @@ -101,6 +103,56 @@ static std::string ColorToDisplayP3String(const Color& color) { FloatToString(color.blue) + ")"; } +static Rect ComputeContentBounds(const Layer* layer) { + float minX = std::numeric_limits::max(); + float minY = std::numeric_limits::max(); + float maxX = -std::numeric_limits::max(); + float maxY = -std::numeric_limits::max(); + for (const auto* element : layer->contents) { + switch (element->nodeType()) { + case NodeType::Rectangle: { + auto rect = static_cast(element); + auto pos = rect->renderPosition(); + auto size = rect->renderSize(); + minX = std::min(minX, pos.x - size.width * 0.5f); + minY = std::min(minY, pos.y - size.height * 0.5f); + maxX = std::max(maxX, pos.x + size.width * 0.5f); + maxY = std::max(maxY, pos.y + size.height * 0.5f); + break; + } + case NodeType::Ellipse: { + auto ellipse = static_cast(element); + auto pos = ellipse->renderPosition(); + auto size = ellipse->renderSize(); + minX = std::min(minX, pos.x - size.width * 0.5f); + minY = std::min(minY, pos.y - size.height * 0.5f); + maxX = std::max(maxX, pos.x + size.width * 0.5f); + maxY = std::max(maxY, pos.y + size.height * 0.5f); + break; + } + case NodeType::Path: { + auto pathNode = static_cast(element); + if (pathNode->data == nullptr || pathNode->data->isEmpty()) { + break; + } + auto bounds = pathNode->data->getBounds(); + auto pos = pathNode->renderPosition(); + minX = std::min(minX, pos.x + bounds.x); + minY = std::min(minY, pos.y + bounds.y); + maxX = std::max(maxX, pos.x + bounds.x + bounds.width); + maxY = std::max(maxY, pos.y + bounds.y + bounds.height); + break; + } + default: + break; + } + } + if (minX > maxX) { + return {}; + } + return {minX, minY, maxX - minX, maxY - minY}; +} + // feGaussianBlur stdDeviation string: one value when blurX == blurY, otherwise two. // Compare via the formatted strings so ULP-level differences from upstream transform // scaling don't emit redundant anisotropic stdDeviation that browsers would honour. @@ -403,19 +455,33 @@ class SVGWriter { void writeColorMatrixFilter(const ColorMatrixFilter* cm, int& colorMatrixIndex, std::string& currentSource); void writeBlendFilter(const BlendFilter* blend, int& shadowIndex, std::string& currentSource); + std::string writeNoiseTurbulence(const NoiseFilter* noise, const std::string& resultName, + const Rect& contentBounds); + std::string writeNoiseTurbulence(const NoiseStyle* noise, const std::string& resultName, + const Rect& contentBounds); + std::string writeNoiseBand(const NoiseFilter* noise, bool isDark, const std::string& label, + const Rect& contentBounds); + std::string writeNoiseBand(const NoiseStyle* noise, bool isDark, const std::string& label, + const Rect& contentBounds); + std::string writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, + std::string& currentSource, const Rect& contentBounds); // Collected per-filter state fed into the final feMerge aggregation. struct ShadowAggregate { std::vector dropShadowResults; + std::vector aboveResults; std::vector innerShadowResults; bool needSourceGraphic = false; }; + std::string writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleIndex, + const Rect& contentBounds); void writeFilterList(const std::vector& filters, int& shadowIndex, - ShadowAggregate& agg, std::string& currentSource); + ShadowAggregate& agg, std::string& currentSource, const Rect& contentBounds); void writeStyleList(const std::vector& styles, int& shadowIndex, - ShadowAggregate& agg); + ShadowAggregate& agg, const Rect& contentBounds); void writeShadowMerge(const ShadowAggregate& agg, const std::string& currentSource); std::string writeFilterAndStyleDefs(const std::vector& filters, - const std::vector& styles); + const std::vector& styles, + const Rect& contentBounds = {}); // Mask / clip-path defs using ContentWriter = void (SVGWriter::*)(SVGBuilder&, const Layer*); @@ -971,10 +1037,464 @@ void SVGWriter::writeBlendFilter(const BlendFilter* blend, int& shadowIndex, currentSource = blendOut; } +std::string SVGWriter::writeNoiseTurbulence(const NoiseFilter* noise, const std::string& resultName, + const Rect& contentBounds) { + auto freq = noise->size > 0.0f ? 1.0f / noise->size : 0.25f; + std::string turbResult = resultName; + _defs->openElement("feTurbulence"); + _defs->addAttribute("type", "fractalNoise"); + _defs->addAttribute("baseFrequency", FloatToString(freq)); + _defs->addAttribute("stitchTiles", "noStitch"); + _defs->addAttribute("numOctaves", "3"); + _defs->addAttribute("seed", FloatToString(noise->seed)); + _defs->addAttribute("result", turbResult); + _defs->closeElementSelfClosing(); + + if (!contentBounds.isEmpty()) { + auto shifted = "shift" + resultName; + _defs->openElement("feOffset"); + _defs->addAttribute("in", turbResult); + _defs->addAttribute("dx", FloatToString(contentBounds.width * 0.5f)); + _defs->addAttribute("dy", FloatToString(contentBounds.height * 0.5f)); + _defs->addAttribute("result", shifted); + _defs->closeElementSelfClosing(); + return shifted; + } + return turbResult; +} + +std::string SVGWriter::writeNoiseBand(const NoiseFilter* noise, bool isDark, + const std::string& label, const Rect& contentBounds) { + auto turbResult = writeNoiseTurbulence(noise, "turb" + label, contentBounds); + + _defs->openElement("feColorMatrix"); + _defs->addAttribute("in", turbResult); + _defs->addAttribute("type", "matrix"); + _defs->addAttribute("values", + "0 0 0 0 0 " + "0 0 0 0 0 " + "0 0 0 0 0 " + "0.2126 0.7152 0.0722 0 0"); + _defs->addAttribute("result", "luma" + label); + _defs->closeElementSelfClosing(); + + auto d = std::clamp(noise->density, 0.0f, 1.0f); + int lower = 0; + int upper = 0; + if (isDark) { + lower = std::clamp(static_cast(std::lround(-25.0f * d + 25.0f)), 0, 99); + upper = std::clamp(static_cast(std::lround(24.0f * d + 25.0f)), 0, 99); + } else { + lower = std::clamp(static_cast(std::lround(-24.0f * d + 74.0f)), 0, 99); + upper = std::clamp(static_cast(std::lround(25.0f * d + 74.0f)), 0, 99); + } + std::string table; + table.reserve(300); + for (int i = 0; i < 100; i++) { + table += (i >= lower && i <= upper) ? "1 " : "0 "; + } + table.pop_back(); + + _defs->openElement("feComponentTransfer"); + _defs->addAttribute("in", "luma" + label); + _defs->addAttribute("result", "band" + label); + _defs->closeElementStart(); + _defs->openElement("feFuncA"); + _defs->addAttribute("type", "discrete"); + _defs->addAttribute("tableValues", table); + _defs->closeElementSelfClosing(); + _defs->closeElement(); + return "band" + label; +} + +std::string SVGWriter::writeNoiseTurbulence(const NoiseStyle* noise, const std::string& resultName, + const Rect& contentBounds) { + auto freq = noise->size > 0.0f ? 1.0f / noise->size : 0.25f; + std::string turbResult = resultName; + _defs->openElement("feTurbulence"); + _defs->addAttribute("type", "fractalNoise"); + _defs->addAttribute("baseFrequency", FloatToString(freq)); + _defs->addAttribute("stitchTiles", "noStitch"); + _defs->addAttribute("numOctaves", "3"); + _defs->addAttribute("seed", FloatToString(noise->seed)); + _defs->addAttribute("result", turbResult); + _defs->closeElementSelfClosing(); + + if (!contentBounds.isEmpty()) { + auto shifted = "shift" + resultName; + _defs->openElement("feOffset"); + _defs->addAttribute("in", turbResult); + _defs->addAttribute("dx", FloatToString(contentBounds.width * 0.5f)); + _defs->addAttribute("dy", FloatToString(contentBounds.height * 0.5f)); + _defs->addAttribute("result", shifted); + _defs->closeElementSelfClosing(); + return shifted; + } + return turbResult; +} + +std::string SVGWriter::writeNoiseBand(const NoiseStyle* noise, bool isDark, + const std::string& label, const Rect& contentBounds) { + auto turbResult = writeNoiseTurbulence(noise, "turb" + label, contentBounds); + + _defs->openElement("feColorMatrix"); + _defs->addAttribute("in", turbResult); + _defs->addAttribute("type", "matrix"); + _defs->addAttribute("values", + "0 0 0 0 0 " + "0 0 0 0 0 " + "0 0 0 0 0 " + "0.2126 0.7152 0.0722 0 0"); + _defs->addAttribute("result", "luma" + label); + _defs->closeElementSelfClosing(); + + auto d = std::clamp(noise->density, 0.0f, 1.0f); + int lower = 0; + int upper = 0; + if (isDark) { + lower = std::clamp(static_cast(std::lround(-25.0f * d + 25.0f)), 0, 99); + upper = std::clamp(static_cast(std::lround(24.0f * d + 25.0f)), 0, 99); + } else { + lower = std::clamp(static_cast(std::lround(-24.0f * d + 74.0f)), 0, 99); + upper = std::clamp(static_cast(std::lround(25.0f * d + 74.0f)), 0, 99); + } + std::string table; + table.reserve(300); + for (int i = 0; i < 100; i++) { + table += (i >= lower && i <= upper) ? "1 " : "0 "; + } + table.pop_back(); + + _defs->openElement("feComponentTransfer"); + _defs->addAttribute("in", "luma" + label); + _defs->addAttribute("result", "band" + label); + _defs->closeElementStart(); + _defs->openElement("feFuncA"); + _defs->addAttribute("type", "discrete"); + _defs->addAttribute("tableValues", table); + _defs->closeElementSelfClosing(); + _defs->closeElement(); + return "band" + label; +} + +std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, + std::string& currentSource, const Rect& contentBounds) { + std::string filterId = "noise" + std::to_string(noiseIndex++); + + if (noise->mode == NoiseMode::Mono) { + auto band = writeNoiseBand(noise, true, "Dark" + filterId, contentBounds); + _defs->openElement("feFlood"); + _defs->addAttribute("flood-color", ColorToSVGString(noise->color)); + if (noise->color.alpha < 1.0f) { + _defs->addAttribute("flood-opacity", FloatToString(noise->color.alpha)); + } + _defs->addAttribute("result", "flood" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "flood" + filterId); + _defs->addAttribute("in2", band); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "colored" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "colored" + filterId); + _defs->addAttribute("in2", currentSource); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "clipped" + filterId); + _defs->closeElementSelfClosing(); + + auto resultName = "noiseOut" + filterId; + _defs->openElement("feBlend"); + _defs->addAttribute("in", "clipped" + filterId); + _defs->addAttribute("in2", currentSource); + auto modeStr = BlendModeToFEBlendString(noise->blendMode); + if (modeStr) { + _defs->addAttribute("mode", modeStr); + } + _defs->addAttribute("result", resultName); + _defs->closeElementSelfClosing(); + currentSource = resultName; + return resultName; + } + + if (noise->mode == NoiseMode::Duo) { + auto dark = writeNoiseBand(noise, true, "Dark" + filterId, contentBounds); + auto bright = writeNoiseBand(noise, false, "Bright" + filterId, contentBounds); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", dark); + _defs->addAttribute("in2", bright); + _defs->addAttribute("operator", "out"); + _defs->addAttribute("result", "duoDark" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feFlood"); + _defs->addAttribute("flood-color", ColorToSVGString(noise->firstColor)); + if (noise->firstColor.alpha < 1.0f) { + _defs->addAttribute("flood-opacity", FloatToString(noise->firstColor.alpha)); + } + _defs->addAttribute("result", "floodDark" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "floodDark" + filterId); + _defs->addAttribute("in2", "duoDark" + filterId); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "darkColored" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feFlood"); + _defs->addAttribute("flood-color", ColorToSVGString(noise->secondColor)); + if (noise->secondColor.alpha < 1.0f) { + _defs->addAttribute("flood-opacity", FloatToString(noise->secondColor.alpha)); + } + _defs->addAttribute("result", "floodBright" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "floodBright" + filterId); + _defs->addAttribute("in2", bright); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "brightColored" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feBlend"); + _defs->addAttribute("in", "darkColored" + filterId); + _defs->addAttribute("in2", "brightColored" + filterId); + _defs->addAttribute("mode", "screen"); + _defs->addAttribute("result", "duo" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "duo" + filterId); + _defs->addAttribute("in2", currentSource); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "clipped" + filterId); + _defs->closeElementSelfClosing(); + + auto resultName = "noiseOut" + filterId; + _defs->openElement("feBlend"); + _defs->addAttribute("in", "clipped" + filterId); + _defs->addAttribute("in2", currentSource); + auto modeStr = BlendModeToFEBlendString(noise->blendMode); + if (modeStr) { + _defs->addAttribute("mode", modeStr); + } + _defs->addAttribute("result", resultName); + _defs->closeElementSelfClosing(); + currentSource = resultName; + return resultName; + } + + auto nCtResult = writeNoiseTurbulence(noise, "nCt" + filterId, contentBounds); + + _defs->openElement("feColorMatrix"); + _defs->addAttribute("in", nCtResult); + _defs->addAttribute("type", "matrix"); + _defs->addAttribute("values", + "2 0 0 0 -0.5 " + "0 2 0 0 -0.5 " + "0 0 2 0 -0.5 " + "0 0 0 1 0"); + _defs->addAttribute("result", "nCon" + filterId); + _defs->closeElementSelfClosing(); + + auto darkBand = writeNoiseBand(noise, true, "MultiDark" + filterId, contentBounds); + auto brightBand = writeNoiseBand(noise, false, "MultiBright" + filterId, contentBounds); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", darkBand); + _defs->addAttribute("in2", brightBand); + _defs->addAttribute("operator", "out"); + _defs->addAttribute("result", "mB" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "nCon" + filterId); + _defs->addAttribute("in2", "mB" + filterId); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "mN" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feColorMatrix"); + _defs->addAttribute("in", "mN" + filterId); + _defs->addAttribute("type", "matrix"); + std::string opacityValues = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 "; + opacityValues += FloatToString(noise->opacity); + opacityValues += " 0"; + _defs->addAttribute("values", opacityValues); + _defs->addAttribute("result", "mO" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "mO" + filterId); + _defs->addAttribute("in2", currentSource); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "mC" + filterId); + _defs->closeElementSelfClosing(); + + auto resultName = "noiseOut" + filterId; + _defs->openElement("feBlend"); + _defs->addAttribute("in", "mC" + filterId); + _defs->addAttribute("in2", currentSource); + auto modeStr = BlendModeToFEBlendString(noise->blendMode); + if (modeStr) { + _defs->addAttribute("mode", modeStr); + } + _defs->addAttribute("result", resultName); + _defs->closeElementSelfClosing(); + currentSource = resultName; + return resultName; +} + +std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleIndex, + const Rect& contentBounds) { + std::string styleId = "noiseStyle" + std::to_string(noiseStyleIndex++); + + if (noise->mode == NoiseMode::Mono) { + auto band = writeNoiseBand(noise, true, "Dark" + styleId, contentBounds); + _defs->openElement("feFlood"); + _defs->addAttribute("flood-color", ColorToSVGString(noise->color)); + if (noise->color.alpha < 1.0f) { + _defs->addAttribute("flood-opacity", FloatToString(noise->color.alpha)); + } + _defs->addAttribute("result", "flood" + styleId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "flood" + styleId); + _defs->addAttribute("in2", band); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "colored" + styleId); + _defs->closeElementSelfClosing(); + + auto resultName = "noiseStyleOut" + styleId; + _defs->openElement("feComposite"); + _defs->addAttribute("in", "colored" + styleId); + _defs->addAttribute("in2", "SourceGraphic"); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", resultName); + _defs->closeElementSelfClosing(); + return resultName; + } + + if (noise->mode == NoiseMode::Duo) { + auto dark = writeNoiseBand(noise, true, "Dark" + styleId, contentBounds); + auto bright = writeNoiseBand(noise, false, "Bright" + styleId, contentBounds); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", dark); + _defs->addAttribute("in2", bright); + _defs->addAttribute("operator", "out"); + _defs->addAttribute("result", "duoDark" + styleId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feFlood"); + _defs->addAttribute("flood-color", ColorToSVGString(noise->firstColor)); + if (noise->firstColor.alpha < 1.0f) { + _defs->addAttribute("flood-opacity", FloatToString(noise->firstColor.alpha)); + } + _defs->addAttribute("result", "floodDark" + styleId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "floodDark" + styleId); + _defs->addAttribute("in2", "duoDark" + styleId); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "darkColored" + styleId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feFlood"); + _defs->addAttribute("flood-color", ColorToSVGString(noise->secondColor)); + if (noise->secondColor.alpha < 1.0f) { + _defs->addAttribute("flood-opacity", FloatToString(noise->secondColor.alpha)); + } + _defs->addAttribute("result", "floodBright" + styleId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "floodBright" + styleId); + _defs->addAttribute("in2", bright); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "brightColored" + styleId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feBlend"); + _defs->addAttribute("in", "darkColored" + styleId); + _defs->addAttribute("in2", "brightColored" + styleId); + _defs->addAttribute("mode", "screen"); + _defs->addAttribute("result", "duo" + styleId); + _defs->closeElementSelfClosing(); + + auto resultName = "noiseStyleOut" + styleId; + _defs->openElement("feComposite"); + _defs->addAttribute("in", "duo" + styleId); + _defs->addAttribute("in2", "SourceGraphic"); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", resultName); + _defs->closeElementSelfClosing(); + return resultName; + } + + // Multi mode + auto nCtResult = writeNoiseTurbulence(noise, "nCt" + styleId, contentBounds); + + _defs->openElement("feColorMatrix"); + _defs->addAttribute("in", nCtResult); + _defs->addAttribute("type", "matrix"); + _defs->addAttribute("values", + "2 0 0 0 -0.5 " + "0 2 0 0 -0.5 " + "0 0 2 0 -0.5 " + "0 0 0 1 0"); + _defs->addAttribute("result", "nCon" + styleId); + _defs->closeElementSelfClosing(); + + auto darkBand = writeNoiseBand(noise, true, "MultiDark" + styleId, contentBounds); + auto brightBand = writeNoiseBand(noise, false, "MultiBright" + styleId, contentBounds); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", darkBand); + _defs->addAttribute("in2", brightBand); + _defs->addAttribute("operator", "out"); + _defs->addAttribute("result", "mB" + styleId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "nCon" + styleId); + _defs->addAttribute("in2", "mB" + styleId); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "mN" + styleId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feColorMatrix"); + _defs->addAttribute("in", "mN" + styleId); + _defs->addAttribute("type", "matrix"); + std::string opacityValues = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 "; + opacityValues += FloatToString(noise->opacity); + opacityValues += " 0"; + _defs->addAttribute("values", opacityValues); + _defs->addAttribute("result", "mO" + styleId); + _defs->closeElementSelfClosing(); + + auto resultName = "noiseStyleOut" + styleId; + _defs->openElement("feComposite"); + _defs->addAttribute("in", "mO" + styleId); + _defs->addAttribute("in2", "SourceGraphic"); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", resultName); + _defs->closeElementSelfClosing(); + return resultName; +} + void SVGWriter::writeFilterList(const std::vector& filters, int& shadowIndex, - ShadowAggregate& agg, std::string& currentSource) { + ShadowAggregate& agg, std::string& currentSource, + const Rect& contentBounds) { int colorMatrixIndex = 0; int blurIndex = 0; + int noiseIndex = 0; for (const auto* filter : filters) { switch (filter->nodeType()) { case NodeType::BlurFilter: { @@ -1015,6 +1535,10 @@ void SVGWriter::writeFilterList(const std::vector& filters, int& s case NodeType::BlendFilter: writeBlendFilter(static_cast(filter), shadowIndex, currentSource); break; + case NodeType::NoiseFilter: + writeNoiseFilter(static_cast(filter), noiseIndex, currentSource, + contentBounds); + break; default: break; } @@ -1024,9 +1548,11 @@ void SVGWriter::writeFilterList(const std::vector& filters, int& s // LayerStyle emission mirrors the Filter pass so DropShadowStyle / InnerShadowStyle // share the feMerge aggregation logic. BackgroundBlurStyle is silently skipped because SVG has // no portable backdrop-blur primitive (the deprecated enable-background is not supported by -// modern renderers). +// modern renderers). NoiseStyle emits its SVG primitives and adds the result name to +// agg.aboveResults so it composites above the source in the final feMerge. void SVGWriter::writeStyleList(const std::vector& styles, int& shadowIndex, - ShadowAggregate& agg) { + ShadowAggregate& agg, const Rect& contentBounds) { + int noiseStyleIndex = 0; for (const auto* style : styles) { switch (style->nodeType()) { case NodeType::DropShadowStyle: { @@ -1047,6 +1573,13 @@ void SVGWriter::writeStyleList(const std::vector& styles, int& shad agg.needSourceGraphic = true; break; } + case NodeType::NoiseStyle: { + auto noise = static_cast(style); + auto result = writeNoiseStyle(noise, noiseStyleIndex, contentBounds); + agg.aboveResults.push_back(result); + agg.needSourceGraphic = true; + break; + } default: break; } @@ -1086,6 +1619,11 @@ void SVGWriter::writeShadowMerge(const ShadowAggregate& agg, const std::string& _defs->addAttribute("in", currentSource); _defs->closeElementSelfClosing(); } + for (const auto& result : agg.aboveResults) { + _defs->openElement("feMergeNode"); + _defs->addAttribute("in", result); + _defs->closeElementSelfClosing(); + } for (const auto& result : agg.innerShadowResults) { _defs->openElement("feMergeNode"); _defs->addAttribute("in", result); @@ -1095,7 +1633,8 @@ void SVGWriter::writeShadowMerge(const ShadowAggregate& agg, const std::string& } std::string SVGWriter::writeFilterAndStyleDefs(const std::vector& filters, - const std::vector& styles) { + const std::vector& styles, + const Rect& contentBounds) { if (filters.empty() && styles.empty()) { return {}; } @@ -1187,8 +1726,8 @@ std::string SVGWriter::writeFilterAndStyleDefs(const std::vector& int shadowIndex = 0; ShadowAggregate agg; std::string currentSource = "SourceGraphic"; - writeFilterList(filters, shadowIndex, agg, currentSource); - writeStyleList(styles, shadowIndex, agg); + writeFilterList(filters, shadowIndex, agg, currentSource, contentBounds); + writeStyleList(styles, shadowIndex, agg, contentBounds); writeShadowMerge(agg, currentSource); _defs->closeElement(); // @@ -2567,7 +3106,8 @@ void SVGWriter::writeLayerGroupAttributes(SVGBuilder& out, const Layer* layer, } if (!layer->filters.empty() || !layer->styles.empty()) { - auto filterId = writeFilterAndStyleDefs(layer->filters, layer->styles); + auto contentBounds = ComputeContentBounds(layer); + auto filterId = writeFilterAndStyleDefs(layer->filters, layer->styles, contentBounds); if (!filterId.empty()) { out.addAttribute("filter", "url(#" + filterId + ")"); } diff --git a/src/pagx/utils/StringParser.cpp b/src/pagx/utils/StringParser.cpp index f0576fd733..3a751dff8c 100644 --- a/src/pagx/utils/StringParser.cpp +++ b/src/pagx/utils/StringParser.cpp @@ -88,6 +88,8 @@ const char* NodeTypeName(NodeType type) { return "InnerShadowStyle"; case NodeType::BackgroundBlurStyle: return "BackgroundBlurStyle"; + case NodeType::NoiseStyle: + return "NoiseStyle"; case NodeType::BlurFilter: return "BlurFilter"; case NodeType::DropShadowFilter: @@ -98,6 +100,8 @@ const char* NodeTypeName(NodeType type) { return "BlendFilter"; case NodeType::ColorMatrixFilter: return "ColorMatrixFilter"; + case NodeType::NoiseFilter: + return "NoiseFilter"; case NodeType::Rectangle: return "Rectangle"; case NodeType::Ellipse: diff --git a/src/renderer/LayerBuilder.cpp b/src/renderer/LayerBuilder.cpp index 061fe428c7..eab62df3b1 100644 --- a/src/renderer/LayerBuilder.cpp +++ b/src/renderer/LayerBuilder.cpp @@ -45,6 +45,8 @@ #include "pagx/nodes/LinearGradient.h" #include "pagx/nodes/MergePath.h" #include "pagx/nodes/Node.h" +#include "pagx/nodes/NoiseFilter.h" +#include "pagx/nodes/NoiseStyle.h" #include "pagx/nodes/Path.h" #include "pagx/nodes/Polystar.h" #include "pagx/nodes/RadialGradient.h" @@ -90,9 +92,11 @@ #include "tgfx/layers/filters/DropShadowFilter.h" #include "tgfx/layers/filters/InnerShadowFilter.h" #include "tgfx/layers/filters/LayerFilter.h" +#include "tgfx/layers/filters/NoiseFilter.h" #include "tgfx/layers/layerstyles/BackgroundBlurStyle.h" #include "tgfx/layers/layerstyles/DropShadowStyle.h" #include "tgfx/layers/layerstyles/InnerShadowStyle.h" +#include "tgfx/layers/layerstyles/NoiseStyle.h" #include "tgfx/layers/vectors/Ellipse.h" #include "tgfx/layers/vectors/FillStyle.h" #include "tgfx/layers/vectors/Gradient.h" @@ -1044,6 +1048,29 @@ class LayerBuilderContext { bindBackgroundBlurStyleChannels(style); return tgfxStyle; } + case NodeType::NoiseStyle: { + auto style = static_cast(node); + std::shared_ptr tgfxStyle; + switch (style->mode) { + case NoiseMode::Mono: + tgfxStyle = tgfx::NoiseStyle::MakeMono(style->size, style->density, + ToTGFX(style->color), style->seed); + break; + case NoiseMode::Duo: + tgfxStyle = + tgfx::NoiseStyle::MakeDuo(style->size, style->density, ToTGFX(style->firstColor), + ToTGFX(style->secondColor), style->seed); + break; + case NoiseMode::Multi: + tgfxStyle = tgfx::NoiseStyle::MakeMulti(style->size, style->density, style->opacity, + style->seed); + break; + } + if (tgfxStyle && node->blendMode != BlendMode::Normal) { + tgfxStyle->setBlendMode(ToTGFX(node->blendMode)); + } + return tgfxStyle; + } default: return nullptr; } @@ -1395,6 +1422,23 @@ class LayerBuilderContext { auto filter = static_cast(node); return tgfx::ColorMatrixFilter::Make(filter->matrix); } + case NodeType::NoiseFilter: { + auto filter = static_cast(node); + auto tgfxBlendMode = ToTGFX(filter->blendMode); + switch (filter->mode) { + case NoiseMode::Mono: + return tgfx::NoiseFilter::MakeMono(filter->size, filter->density, ToTGFX(filter->color), + filter->seed, tgfxBlendMode); + case NoiseMode::Duo: + return tgfx::NoiseFilter::MakeDuo( + filter->size, filter->density, ToTGFX(filter->firstColor), + ToTGFX(filter->secondColor), filter->seed, tgfxBlendMode); + case NoiseMode::Multi: + return tgfx::NoiseFilter::MakeMulti(filter->size, filter->density, filter->opacity, + filter->seed, tgfxBlendMode); + } + return nullptr; + } default: return nullptr; } diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 42da624cde..fae557725a 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -36,6 +36,7 @@ #include "pagx/PAGXExporter.h" #include "pagx/PAGXImporter.h" #include "pagx/PAGXOptimizer.h" +#include "pagx/SVGExporter.h" #include "pagx/SVGImporter.h" #include "pagx/TextLayout.h" #include "pagx/TextLayoutParams.h" @@ -58,6 +59,8 @@ #include "pagx/nodes/ImagePattern.h" #include "pagx/nodes/Layer.h" #include "pagx/nodes/LinearGradient.h" +#include "pagx/nodes/NoiseFilter.h" +#include "pagx/nodes/NoiseStyle.h" #include "pagx/nodes/Path.h" #include "pagx/nodes/PathData.h" #include "pagx/nodes/Polystar.h" @@ -1209,10 +1212,8 @@ PAGX_TEST(PAGXTest, CustomDataKeyValidation) { EXPECT_EQ(doc->layers[0]->customData.count("has_underscore"), 0u); } -// ===================================================================================== -// Auto Layout - Container Layout - Basic -// ===================================================================================== - +// ==============================================================================// Auto Layout - Container Layout - Basic +// ============================================================================== PAGX_TEST(PAGXTest, LayoutHorizontalEqualWidth) { auto doc = pagx::PAGXDocument::Make(920, 200); auto parent = doc->makeNode(); @@ -1345,10 +1346,8 @@ PAGX_TEST(PAGXTest, LayoutAlignmentCenter) { EXPECT_EQ(child->renderPosition().y, 125.0f); } -// ===================================================================================== -// Auto Layout - Container Layout - Arrangement End -// ===================================================================================== - +// ==============================================================================// Auto Layout - Container Layout - Arrangement End +// ============================================================================== PAGX_TEST(PAGXTest, LayoutArrangementEnd) { auto doc = pagx::PAGXDocument::Make(400, 100); auto parent = doc->makeNode(); @@ -1375,10 +1374,8 @@ PAGX_TEST(PAGXTest, LayoutArrangementEnd) { EXPECT_EQ(child2->renderPosition().x, 350.0f); } -// ===================================================================================== -// Auto Layout - Container Layout - Alignment End -// ===================================================================================== - +// ==============================================================================// Auto Layout - Container Layout - Alignment End +// ============================================================================== PAGX_TEST(PAGXTest, LayoutAlignmentEnd) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -1400,10 +1397,8 @@ PAGX_TEST(PAGXTest, LayoutAlignmentEnd) { EXPECT_EQ(child->renderPosition().y, 250.0f); } -// ===================================================================================== -// Auto Layout - Container Layout - Padding -// ===================================================================================== - +// ==============================================================================// Auto Layout - Container Layout - Padding +// ============================================================================== PAGX_TEST(PAGXTest, LayoutPadding) { auto doc = pagx::PAGXDocument::Make(400, 200); auto parent = doc->makeNode(); @@ -1454,10 +1449,8 @@ PAGX_TEST(PAGXTest, LayoutPaddingWithFlex) { EXPECT_EQ(child2->renderPosition().x, 200.0f); } -// ===================================================================================== -// Auto Layout - Container Layout - Flex Distribution -// ===================================================================================== - +// ==============================================================================// Auto Layout - Container Layout - Flex Distribution +// ============================================================================== PAGX_TEST(PAGXTest, LayoutFlexEqualDistribution) { auto doc = pagx::PAGXDocument::Make(300, 100); auto parent = doc->makeNode(); @@ -1663,10 +1656,8 @@ PAGX_TEST(PAGXTest, LayoutFlexZeroNotExported) { EXPECT_EQ(xml.find("flex="), std::string::npos); } -// ===================================================================================== -// Auto Layout - Container Layout - Visibility -// ===================================================================================== - +// ==============================================================================// Auto Layout - Container Layout - Visibility +// ============================================================================== PAGX_TEST(PAGXTest, LayoutHiddenChildNotSkipped) { auto doc = pagx::PAGXDocument::Make(300, 100); auto parent = doc->makeNode(); @@ -1698,10 +1689,8 @@ PAGX_TEST(PAGXTest, LayoutHiddenChildNotSkipped) { EXPECT_EQ(child3->renderPosition().x, 200.0f); } -// ===================================================================================== -// Auto Layout - Container Layout - Edge Cases -// ===================================================================================== - +// ==============================================================================// Auto Layout - Container Layout - Edge Cases +// ============================================================================== PAGX_TEST(PAGXTest, LayoutEmptyContainer) { auto doc = pagx::PAGXDocument::Make(400, 200); auto parent = doc->makeNode(); @@ -1775,10 +1764,8 @@ PAGX_TEST(PAGXTest, LayoutOverflow) { EXPECT_EQ(child3->renderPosition().x, 200.0f); } -// ===================================================================================== -// Auto Layout - Container Layout - Nested -// ===================================================================================== - +// ==============================================================================// Auto Layout - Container Layout - Nested +// ============================================================================== PAGX_TEST(PAGXTest, LayoutNested) { auto doc = pagx::PAGXDocument::Make(500, 200); auto outer = doc->makeNode(); @@ -1816,10 +1803,8 @@ PAGX_TEST(PAGXTest, LayoutNested) { EXPECT_EQ(inner2->renderPosition().y, 90.0f); } -// ===================================================================================== -// Auto Layout - Container Layout - Measurement (bottom-up) -// ===================================================================================== - +// ==============================================================================// Auto Layout - Container Layout - Measurement (bottom-up) +// ============================================================================== PAGX_TEST(PAGXTest, LayoutMeasureFromChildren) { auto doc = pagx::PAGXDocument::Make(800, 600); auto parent = doc->makeNode(); @@ -1843,10 +1828,8 @@ PAGX_TEST(PAGXTest, LayoutMeasureFromChildren) { EXPECT_EQ(parent->layoutHeight, 80.0f); } -// ===================================================================================== -// Auto Layout - Container Layout - Pixel Grid Snap -// ===================================================================================== - +// ==============================================================================// Auto Layout - Container Layout - Pixel Grid Snap +// ============================================================================== PAGX_TEST(PAGXTest, LayoutSnapToPixelGrid) { auto doc = pagx::PAGXDocument::Make(100, 100); auto parent = doc->makeNode(); @@ -1881,10 +1864,8 @@ PAGX_TEST(PAGXTest, LayoutSnapToPixelGrid) { EXPECT_EQ(child1->layoutWidth + child2->layoutWidth + child3->layoutWidth, 100.0f); } -// ===================================================================================== -// Auto Layout - Container Layout - Measurement Cache -// ===================================================================================== - +// ==============================================================================// Auto Layout - Container Layout - Measurement Cache +// ============================================================================== PAGX_TEST(PAGXTest, LayoutMeasureCacheIdempotent) { auto doc = pagx::PAGXDocument::Make(400, 200); auto parent = doc->makeNode(); @@ -1917,10 +1898,8 @@ PAGX_TEST(PAGXTest, LayoutMeasureCacheIdempotent) { EXPECT_EQ(child2->renderPosition().x, x2); } -// ===================================================================================== -// Auto Layout - Constraint Positioning - Single Edge -// ===================================================================================== - +// ==============================================================================// Auto Layout - Constraint Positioning - Single Edge +// ============================================================================== PAGX_TEST(PAGXTest, LayoutConstraintLeft) { auto doc = pagx::PAGXDocument::Make(400, 200); auto layer = doc->makeNode(); @@ -2053,10 +2032,8 @@ PAGX_TEST(PAGXTest, LayoutConstraintCenterY) { EXPECT_FLOAT_EQ(bounds.height, 50.0f); } -// ===================================================================================== -// Auto Layout - Constraint Positioning - Stretch (Ellipse) -// ===================================================================================== - +// ==============================================================================// Auto Layout - Constraint Positioning - Stretch (Ellipse) +// ============================================================================== PAGX_TEST(PAGXTest, LayoutConstraintStretchEllipse) { auto doc = pagx::PAGXDocument::Make(400, 200); auto layer = doc->makeNode(); @@ -2103,10 +2080,8 @@ PAGX_TEST(PAGXTest, LayoutConstraintStretchEllipseVertical) { EXPECT_FLOAT_EQ(bounds.y, 15.0f); } -// ===================================================================================== -// Auto Layout - Constraint Positioning - Stretch (Rectangle, TextBox) -// ===================================================================================== - +// ==============================================================================// Auto Layout - Constraint Positioning - Stretch (Rectangle, TextBox) +// ============================================================================== PAGX_TEST(PAGXTest, LayoutConstraintStretchRectangle) { auto doc = pagx::PAGXDocument::Make(400, 200); auto layer = doc->makeNode(); @@ -2159,10 +2134,8 @@ PAGX_TEST(PAGXTest, LayoutConstraintStretchTextBox) { EXPECT_FLOAT_EQ(bounds.y, 20.0f); } -// ===================================================================================== -// Auto Layout - Constraint Positioning - Proportional Scaling -// ===================================================================================== - +// ==============================================================================// Auto Layout - Constraint Positioning - Proportional Scaling +// ============================================================================== PAGX_TEST(PAGXTest, LayoutConstraintScalePolystarHorizontal) { auto doc = pagx::PAGXDocument::Make(400, 200); auto layer = doc->makeNode(); @@ -2455,10 +2428,8 @@ PAGX_TEST(PAGXTest, LayoutConstraintScalePathBothAxes) { EXPECT_FLOAT_EQ(bounds.height, 150.0f); } -// ===================================================================================== -// Auto Layout - Constraint Positioning - Group -// ===================================================================================== - +// ==============================================================================// Auto Layout - Constraint Positioning - Group +// ============================================================================== PAGX_TEST(PAGXTest, LayoutGroupDerivedSize) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -2513,10 +2484,8 @@ PAGX_TEST(PAGXTest, LayoutConstraintGroupRecursive) { EXPECT_FLOAT_EQ(rectBounds.width, 340.0f); } -// ===================================================================================== -// Auto Layout - Constraint Positioning - Multiple Elements -// ===================================================================================== - +// ==============================================================================// Auto Layout - Constraint Positioning - Multiple Elements +// ============================================================================== PAGX_TEST(PAGXTest, LayoutConstraintMultipleElements) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -2554,10 +2523,8 @@ PAGX_TEST(PAGXTest, LayoutConstraintMultipleElements) { EXPECT_FLOAT_EQ(bounds3.y + bounds3.height * 0.5f, 150.0f); } -// ===================================================================================== -// Auto Layout - Constraint Positioning - Validation -// ===================================================================================== - +// ==============================================================================// Auto Layout - Constraint Positioning - Validation +// ============================================================================== PAGX_TEST(PAGXTest, LayoutConstraintConflictCenterXWithLeftRight) { // In the new layout system, conflicting constraints are resolved by priority: // centerX has higher priority than left/right. No error is generated. @@ -2585,10 +2552,8 @@ PAGX_TEST(PAGXTest, LayoutConstraintValidCombination) { EXPECT_TRUE(doc->errors.empty()); } -// ===================================================================================== -// Auto Layout - Container Layout - includeInLayout -// ===================================================================================== - +// ==============================================================================// Auto Layout - Container Layout - includeInLayout +// ============================================================================== PAGX_TEST(PAGXTest, LayoutContainerIncludeInLayout) { auto doc = pagx::PAGXDocument::Make(600, 200); auto parent = doc->makeNode(); @@ -2659,10 +2624,8 @@ PAGX_TEST(PAGXTest, LayoutContainerIncludeInLayoutMeasure) { EXPECT_FLOAT_EQ(parent->layoutWidth, 210.0f); } -// ===================================================================================== -// Auto Layout - Container Layout - Alignment::Stretch -// ===================================================================================== - +// ==============================================================================// Auto Layout - Container Layout - Alignment::Stretch +// ============================================================================== PAGX_TEST(PAGXTest, LayoutContainerStretch) { auto doc = pagx::PAGXDocument::Make(600, 200); auto parent = doc->makeNode(); @@ -2845,10 +2808,8 @@ PAGX_TEST(PAGXTest, LayoutIncludeInLayoutMixedVisibility) { EXPECT_FLOAT_EQ(child4->renderPosition().x, 420.0f); } -// ===================================================================================== -// Auto Layout - Container + Constraint combined -// ===================================================================================== - +// ==============================================================================// Auto Layout - Container + Constraint combined +// ============================================================================== PAGX_TEST(PAGXTest, LayoutContainerWithConstraints) { auto doc = pagx::PAGXDocument::Make(600, 400); auto parent = doc->makeNode(); @@ -2885,10 +2846,8 @@ PAGX_TEST(PAGXTest, LayoutContainerWithConstraints) { EXPECT_FLOAT_EQ(bounds.y + bounds.height * 0.5f, 200.0f); } -// ===================================================================================== -// Auto Layout - Round-trip: Layout Attributes -// ===================================================================================== - +// ==============================================================================// Auto Layout - Round-trip: Layout Attributes +// ============================================================================== PAGX_TEST(PAGXTest, LayoutRoundTripAttributes) { auto doc = pagx::PAGXDocument::Make(500, 400); auto layer = doc->makeNode(); @@ -3043,10 +3002,8 @@ PAGX_TEST(PAGXTest, LayoutRoundTripConstraints) { EXPECT_NE(xml.find("centerY=\"-10\""), std::string::npos); } -// ===================================================================================== -// Auto Layout - Layer Constraint Positioning - Single Edge -// ===================================================================================== - +// ==============================================================================// Auto Layout - Layer Constraint Positioning - Single Edge +// ============================================================================== PAGX_TEST(PAGXTest, LayerConstraintLeft) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3129,10 +3086,8 @@ PAGX_TEST(PAGXTest, LayerConstraintBottom) { EXPECT_FLOAT_EQ(child->renderPosition().y, 215.0f); } -// ===================================================================================== -// Auto Layout - Layer Constraint Positioning - Center -// ===================================================================================== - +// ==============================================================================// Auto Layout - Layer Constraint Positioning - Center +// ============================================================================== PAGX_TEST(PAGXTest, LayerConstraintCenterX) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3199,10 +3154,8 @@ PAGX_TEST(PAGXTest, LayerConstraintCenterXWithOffset) { EXPECT_FLOAT_EQ(child->renderPosition().y, 110.0f); } -// ===================================================================================== -// Auto Layout - Layer Constraint Positioning - Opposite Edges Derive Size -// ===================================================================================== - +// ==============================================================================// Auto Layout - Layer Constraint Positioning - Opposite Edges Derive Size +// ============================================================================== PAGX_TEST(PAGXTest, LayerConstraintLeftRightDeriveWidth) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3299,10 +3252,8 @@ PAGX_TEST(PAGXTest, LayerConstraintLeftRightOverridesExplicitWidth) { 320.0f); // 400 - 30 - 50, left+right overrides explicit width. } -// ===================================================================================== -// Auto Layout - Layer Constraint Positioning - Combined Axes -// ===================================================================================== - +// ==============================================================================// Auto Layout - Layer Constraint Positioning - Combined Axes +// ============================================================================== PAGX_TEST(PAGXTest, LayerConstraintLeftAndTop) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3349,10 +3300,8 @@ PAGX_TEST(PAGXTest, LayerConstraintRightAndBottom) { EXPECT_FLOAT_EQ(child->renderPosition().y, 220.0f); } -// ===================================================================================== -// Auto Layout - Layer Constraint Positioning - includeInLayout=false -// ===================================================================================== - +// ==============================================================================// Auto Layout - Layer Constraint Positioning - includeInLayout=false +// ============================================================================== PAGX_TEST(PAGXTest, LayerConstraintWithIncludeInLayoutFalse) { auto doc = pagx::PAGXDocument::Make(600, 400); auto parent = doc->makeNode(); @@ -3422,10 +3371,8 @@ PAGX_TEST(PAGXTest, LayerConstraintIncludeInLayoutFalseDeriveSize) { EXPECT_FLOAT_EQ(overlay->layoutHeight, 300.0f); // 400 - 40 - 60 } -// ===================================================================================== -// Auto Layout - Layer Constraint Positioning - Priority (container layout wins) -// ===================================================================================== - +// ==============================================================================// Auto Layout - Layer Constraint Positioning - Priority (container layout wins) +// ============================================================================== PAGX_TEST(PAGXTest, LayerConstraintIgnoredInContainerLayout) { auto doc = pagx::PAGXDocument::Make(600, 400); auto parent = doc->makeNode(); @@ -3458,10 +3405,8 @@ PAGX_TEST(PAGXTest, LayerConstraintIgnoredInContainerLayout) { EXPECT_FLOAT_EQ(child2->renderPosition().x, 210.0f); } -// ===================================================================================== -// Auto Layout - Layer Constraint Positioning - No constraint, no change -// ===================================================================================== - +// ==============================================================================// Auto Layout - Layer Constraint Positioning - No constraint, no change +// ============================================================================== PAGX_TEST(PAGXTest, LayerNoConstraintUnchanged) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3485,10 +3430,8 @@ PAGX_TEST(PAGXTest, LayerNoConstraintUnchanged) { EXPECT_FLOAT_EQ(child->renderPosition().y, 77.0f); } -// ===================================================================================== -// Auto Layout - Layer Constraint Positioning - Invisible child still gets layout -// ===================================================================================== - +// ==============================================================================// Auto Layout - Layer Constraint Positioning - Invisible child still gets layout +// ============================================================================== PAGX_TEST(PAGXTest, LayerConstraintInvisibleChildNotSkipped) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3515,10 +3458,8 @@ PAGX_TEST(PAGXTest, LayerConstraintInvisibleChildNotSkipped) { EXPECT_FLOAT_EQ(child->renderPosition().y, 40.0f); } -// ===================================================================================== -// Auto Layout - Layer Constraint Positioning - Multiple children -// ===================================================================================== - +// ==============================================================================// Auto Layout - Layer Constraint Positioning - Multiple children +// ============================================================================== PAGX_TEST(PAGXTest, LayerConstraintMultipleChildren) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3562,10 +3503,8 @@ PAGX_TEST(PAGXTest, LayerConstraintMultipleChildren) { EXPECT_FLOAT_EQ(centered->renderPosition().y, 110.0f); } -// ===================================================================================== -// Auto Layout - Layer Constraint Positioning - Measured child size (no explicit width/height) -// ===================================================================================== - +// ==============================================================================// Auto Layout - Layer Constraint Positioning - Measured child size (no explicit width/height) +// ============================================================================== PAGX_TEST(PAGXTest, LayerConstraintMeasuredChildSize) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3595,10 +3534,8 @@ PAGX_TEST(PAGXTest, LayerConstraintMeasuredChildSize) { EXPECT_FLOAT_EQ(child->renderPosition().y, 220.0f); } -// ===================================================================================== -// Auto Layout - Layer Constraint Positioning - Round-trip XML -// ===================================================================================== - +// ==============================================================================// Auto Layout - Layer Constraint Positioning - Round-trip XML +// ============================================================================== PAGX_TEST(PAGXTest, LayerConstraintRoundTrip) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3849,10 +3786,8 @@ PAGX_TEST(PAGXTest, ResourceCrossReferenceChain) { EXPECT_EQ(font->glyphs[1]->image, image); } -// ===================================================================================== -// Auto Layout - Constraint Priority (centerX/centerY highest priority) -// ===================================================================================== - +// ==============================================================================// Auto Layout - Constraint Priority (centerX/centerY highest priority) +// ============================================================================== PAGX_TEST(PAGXTest, LayerConstraintCenterXOverridesLeft) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3923,10 +3858,8 @@ PAGX_TEST(PAGXTest, LayerConstraintCenterYMeasurementContribution) { << "Parent should measure centerY contribution as |centerY| * 2 + content_height"; } -// ===================================================================================== -// Auto Layout - Priority Combination Tests -// ===================================================================================== - +// ==============================================================================// Auto Layout - Priority Combination Tests +// ============================================================================== PAGX_TEST(PAGXTest, LayoutContainerFlexVsExplicitSize) { // Priority: explicit main-axis size > flex distribution. // When a child has both flex>0 and explicit width, explicit width wins. @@ -4153,10 +4086,8 @@ PAGX_TEST(PAGXTest, LayoutIncludeInLayoutFalseNotAffectingMeasurement) { EXPECT_FLOAT_EQ(child2->renderPosition().x, 200.0f); } -// ===================================================================================== -// Auto Layout - Group Constraint Measurement -// ===================================================================================== - +// ==============================================================================// Auto Layout - Group Constraint Measurement +// ============================================================================== PAGX_TEST(PAGXTest, GroupChildConstraintAffectsMeasurement) { // When a Group has no explicit dimensions, its measured size should include // constraint offsets from child elements (e.g., left, top). @@ -4365,10 +4296,8 @@ PAGX_TEST(PAGXTest, ImagePatternInlineImage) { EXPECT_TRUE(pattern5->image->data != nullptr); } -// ===================================================================================== -// ClipToBounds -// ===================================================================================== - +// ==============================================================================// ClipToBounds +// ============================================================================== /** * Test that clipToBounds sets scrollRect during layout when the layer has resolved dimensions. */ @@ -4492,10 +4421,8 @@ PAGX_TEST(PAGXTest, ClipToBoundsAutoMeasured) { EXPECT_EQ(parent->scrollRect.height, 100); } -// ===================================================================================== -// Auto Layout - Refactoring Correctness (P0) -// ===================================================================================== - +// ==============================================================================// Auto Layout - Refactoring Correctness (P0) +// ============================================================================== PAGX_TEST(PAGXTest, LayoutIdempotent) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -4578,10 +4505,8 @@ PAGX_TEST(PAGXTest, VerifyNestedFlexNoFalsePositive) { VerifyFile(pagxPath, "verify_nested_flex"); } -// ===================================================================================== -// Auto Layout - Edge Case Fixes (P2) -// ===================================================================================== - +// ==============================================================================// Auto Layout - Edge Case Fixes (P2) +// ============================================================================== PAGX_TEST(PAGXTest, DefaultSizeElementWithOppositeEdgeConstraint) { auto doc = pagx::PAGXDocument::Make(200, 200); auto layer = doc->makeNode(); @@ -4633,10 +4558,8 @@ PAGX_TEST(PAGXTest, FlexRoundingErrorPropagation) { EXPECT_FLOAT_EQ(total, 100); } -// ===================================================================================== -// Auto Layout - Architectural Integrity (P3) -// ===================================================================================== - +// ==============================================================================// Auto Layout - Architectural Integrity (P3) +// ============================================================================== PAGX_TEST(PAGXTest, TransformDoesNotAffectLayout) { auto doc = pagx::PAGXDocument::Make(400, 400); auto layer = doc->makeNode(); @@ -4708,10 +4631,8 @@ PAGX_TEST(PAGXTest, DeepNestingConstraintPropagation) { EXPECT_FLOAT_EQ(outerBounds.y, 50); } -// ===================================================================================== -// Auto Layout - New Feature Tests (P1) -// ===================================================================================== - +// ==============================================================================// Auto Layout - New Feature Tests (P1) +// ============================================================================== PAGX_TEST(PAGXTest, LayoutTextIndependentConstraint) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -4780,10 +4701,8 @@ PAGX_TEST(PAGXTest, LayoutTextPathMeasurement) { EXPECT_FLOAT_EQ(group->layoutHeight, 100); } -// ===================================================================================== -// Auto Layout - Edge Case Fixes (P2 continued) -// ===================================================================================== - +// ==============================================================================// Auto Layout - Edge Case Fixes (P2 continued) +// ============================================================================== PAGX_TEST(PAGXTest, LayoutConstraintScalePathSingleAxis) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -4845,10 +4764,8 @@ PAGX_TEST(PAGXTest, LayoutTextScaledPositionAnchor) { EXPECT_NE(text->renderFontSize(), 30); } -// ===================================================================================== -// Auto Layout - Architectural Integrity (P3 continued) -// ===================================================================================== - +// ==============================================================================// Auto Layout - Architectural Integrity (P3 continued) +// ============================================================================== PAGX_TEST(PAGXTest, LayoutTextInTextBoxSkipConstraint) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -4879,10 +4796,8 @@ PAGX_TEST(PAGXTest, LayoutTextInTextBoxSkipConstraint) { EXPECT_FLOAT_EQ(text->fontSize, 20); } -// ===================================================================================== -// Variable naming cleanup -// ===================================================================================== - +// ==============================================================================// Variable naming cleanup +// ============================================================================== /** * Test case: TextLayoutGlyphRun data integrity after layout. * Verifies that layout produces non-empty glyph runs with correct glyph count and positions. @@ -5110,10 +5025,8 @@ PAGX_TEST(PAGXTest, TextBoundsDirectValidation) { EXPECT_GT(boxTextBounds.height, 0); } -// ===================================================================================== -// Padding Unified Semantics — Round-Trip Tests -// ===================================================================================== - +// ==============================================================================// Padding Unified Semantics — Round-Trip Tests +// ============================================================================== // Group padding round-trip through export/import. PAGX_TEST(PAGXTest, GroupPaddingRoundTrip) { auto doc = pagx::PAGXDocument::Make(200, 100); @@ -7209,6 +7122,218 @@ PAGX_TEST(PAGXTest, HitTestGlobalMatrix) { EXPECT_FLOAT_EQ(matrix.d, 2.0f); EXPECT_FLOAT_EQ(matrix.tx, 70.0f); EXPECT_FLOAT_EQ(matrix.ty, 45.0f); + * Test rendering with Mono, Duo, and Multi noise filters side by side. + */ +PAGX_TEST(PAGXTest, NoiseFilterModes) { + constexpr int canvasW = 400; + constexpr int canvasH = 350; + auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); + + auto makeLayer = [&](float x) { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(x, 50); + auto rect = doc->makeNode(); + rect->position = {50, 50}; + rect->size = {100, 100}; + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {0.2f, 0.5f, 0.8f, 1.0f}; + fill->color = solid; + layer->contents.push_back(rect); + layer->contents.push_back(fill); + return layer; + }; + + auto layer1 = makeLayer(20); + auto mono = doc->makeNode(); + mono->mode = pagx::NoiseMode::Mono; + mono->size = 8; + mono->density = 0.5f; + mono->seed = 42; + mono->color = {0.0f, 0.0f, 0.0f, 1.0f}; + layer1->filters.push_back(mono); + doc->layers.push_back(layer1); + + auto layer2 = makeLayer(150); + auto duo = doc->makeNode(); + duo->mode = pagx::NoiseMode::Duo; + duo->size = 8; + duo->density = 0.5f; + duo->seed = 42; + duo->firstColor = {1.0f, 1.0f, 0.0f, 1.0f}; + duo->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; + layer2->filters.push_back(duo); + doc->layers.push_back(layer2); + + auto layer3 = makeLayer(280); + auto multi = doc->makeNode(); + multi->mode = pagx::NoiseMode::Multi; + multi->size = 8; + multi->density = 0.5f; + multi->seed = 42; + multi->opacity = 1.0f; + layer3->filters.push_back(multi); + doc->layers.push_back(layer3); + + auto makePathLayer = [&](float x, float y) { + auto layer = doc->makeNode(); + auto path = doc->makeNode(); + path->data = doc->makeNode(); + path->data->moveTo(0, 0); + path->data->lineTo(100, 0); + path->data->lineTo(100, 100); + path->data->lineTo(0, 100); + path->data->close(); + path->position = {x, y}; + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {0.2f, 0.5f, 0.8f, 1.0f}; + fill->color = solid; + layer->contents.push_back(path); + layer->contents.push_back(fill); + return layer; + }; + + auto layer4 = makePathLayer(20, 170); + auto mono2 = doc->makeNode(); + mono2->mode = pagx::NoiseMode::Mono; + mono2->size = 8; + mono2->density = 0.5f; + mono2->seed = 42; + mono2->color = {0.0f, 0.0f, 0.0f, 1.0f}; + layer4->filters.push_back(mono2); + doc->layers.push_back(layer4); + + auto layer5 = makePathLayer(150, 170); + auto duo2 = doc->makeNode(); + duo2->mode = pagx::NoiseMode::Duo; + duo2->size = 8; + duo2->density = 0.5f; + duo2->seed = 42; + duo2->firstColor = {1.0f, 1.0f, 0.0f, 1.0f}; + duo2->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; + layer5->filters.push_back(duo2); + doc->layers.push_back(layer5); + + auto layer6 = makePathLayer(280, 170); + auto multi2 = doc->makeNode(); + multi2->mode = pagx::NoiseMode::Multi; + multi2->size = 8; + multi2->density = 0.5f; + multi2->seed = 42; + multi2->opacity = 1.0f; + layer6->filters.push_back(multi2); + doc->layers.push_back(layer6); + + doc->applyLayout(); + auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); + ASSERT_TRUE(tgfxLayer != nullptr); + + auto surface = Surface::Make(context, canvasW, canvasH); + ASSERT_TRUE(surface != nullptr); + DisplayList displayList; + displayList.root()->addChild(tgfxLayer); + displayList.render(surface.get(), false); + + EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseFilterModes")); + + auto svg = pagx::SVGExporter::ToSVG(*doc); + EXPECT_FALSE(svg.empty()); + EXPECT_NE(svg.find("(svg.size())); +} + +/** + * Test rendering with NoiseFilter combined with DropShadowFilter and DropShadowStyle. + */ +PAGX_TEST(PAGXTest, NoiseFilterWithShadow) { + constexpr int canvasW = 500; + constexpr int canvasH = 200; + auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); + + auto makeRectLayer = [&]() { + auto layer = doc->makeNode(); + auto rect = doc->makeNode(); + rect->position = {50, 50}; + rect->size = {100, 100}; + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {0.9f, 0.3f, 0.2f, 1.0f}; + fill->color = solid; + layer->contents.push_back(rect); + layer->contents.push_back(fill); + return layer; + }; + + // Left: NoiseFilter + DropShadowFilter + auto layer1 = makeRectLayer(); + layer1->matrix = pagx::Matrix::Translate(60, 50); + auto noise = doc->makeNode(); + noise->mode = pagx::NoiseMode::Mono; + noise->size = 10; + noise->density = 0.5f; + noise->seed = 42; + noise->color = {0.0f, 0.0f, 0.0f, 1.0f}; + auto shadow = doc->makeNode(); + shadow->offsetX = 5; + shadow->offsetY = 5; + shadow->blurX = 3; + shadow->blurY = 3; + shadow->color = {0.0f, 0.0f, 0.0f, 0.5f}; + layer1->filters.push_back(noise); + layer1->filters.push_back(shadow); + doc->layers.push_back(layer1); + + // Right: NoiseStyle + DropShadowFilter + auto layer2 = makeRectLayer(); + layer2->matrix = pagx::Matrix::Translate(290, 50); + auto shadow2 = doc->makeNode(); + shadow2->offsetX = 5; + shadow2->offsetY = 5; + shadow2->blurX = 3; + shadow2->blurY = 3; + shadow2->color = {0.0f, 0.0f, 0.0f, 0.5f}; + auto noiseStyleNode = doc->makeNode(); + noiseStyleNode->mode = pagx::NoiseMode::Mono; + noiseStyleNode->size = 10; + noiseStyleNode->density = 0.5f; + noiseStyleNode->seed = 42; + noiseStyleNode->color = {0.0f, 0.0f, 0.0f, 1.0f}; + layer2->filters.push_back(shadow2); + layer2->styles.push_back(noiseStyleNode); + doc->layers.push_back(layer2); + + doc->applyLayout(); + auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); + ASSERT_TRUE(tgfxLayer != nullptr); + + auto surface = Surface::Make(context, canvasW, canvasH); + ASSERT_TRUE(surface != nullptr); + DisplayList displayList; + displayList.root()->addChild(tgfxLayer); + displayList.render(surface.get(), false); + + EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseFilterWithShadow")); + + auto svg = pagx::SVGExporter::ToSVG(*doc); + EXPECT_FALSE(svg.empty()); + EXPECT_NE(svg.find("feTurbulence"), std::string::npos); + + auto outPath = ProjectPath::Absolute("test/out/PAGXTest/NoiseFilterWithShadow.svg"); + auto dirPath = std::filesystem::path(outPath).parent_path(); + if (!std::filesystem::exists(dirPath)) { + std::filesystem::create_directories(dirPath); + } + std::ofstream file(outPath, std::ios::binary); + file.write(svg.data(), static_cast(svg.size())); } } // namespace pag From dea733cecf7ed59edb9be15065f8f6b9b0de88af Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Fri, 5 Jun 2026 10:53:50 +0800 Subject: [PATCH 03/52] Remove NoiseFilterWithShadow test case --- test/src/PAGXTest.cpp | 82 ------------------------------------------- 1 file changed, 82 deletions(-) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index fae557725a..4b5be1cc8a 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7254,86 +7254,4 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { /** * Test rendering with NoiseFilter combined with DropShadowFilter and DropShadowStyle. */ -PAGX_TEST(PAGXTest, NoiseFilterWithShadow) { - constexpr int canvasW = 500; - constexpr int canvasH = 200; - auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); - - auto makeRectLayer = [&]() { - auto layer = doc->makeNode(); - auto rect = doc->makeNode(); - rect->position = {50, 50}; - rect->size = {100, 100}; - auto fill = doc->makeNode(); - auto solid = doc->makeNode(); - solid->color = {0.9f, 0.3f, 0.2f, 1.0f}; - fill->color = solid; - layer->contents.push_back(rect); - layer->contents.push_back(fill); - return layer; - }; - - // Left: NoiseFilter + DropShadowFilter - auto layer1 = makeRectLayer(); - layer1->matrix = pagx::Matrix::Translate(60, 50); - auto noise = doc->makeNode(); - noise->mode = pagx::NoiseMode::Mono; - noise->size = 10; - noise->density = 0.5f; - noise->seed = 42; - noise->color = {0.0f, 0.0f, 0.0f, 1.0f}; - auto shadow = doc->makeNode(); - shadow->offsetX = 5; - shadow->offsetY = 5; - shadow->blurX = 3; - shadow->blurY = 3; - shadow->color = {0.0f, 0.0f, 0.0f, 0.5f}; - layer1->filters.push_back(noise); - layer1->filters.push_back(shadow); - doc->layers.push_back(layer1); - - // Right: NoiseStyle + DropShadowFilter - auto layer2 = makeRectLayer(); - layer2->matrix = pagx::Matrix::Translate(290, 50); - auto shadow2 = doc->makeNode(); - shadow2->offsetX = 5; - shadow2->offsetY = 5; - shadow2->blurX = 3; - shadow2->blurY = 3; - shadow2->color = {0.0f, 0.0f, 0.0f, 0.5f}; - auto noiseStyleNode = doc->makeNode(); - noiseStyleNode->mode = pagx::NoiseMode::Mono; - noiseStyleNode->size = 10; - noiseStyleNode->density = 0.5f; - noiseStyleNode->seed = 42; - noiseStyleNode->color = {0.0f, 0.0f, 0.0f, 1.0f}; - layer2->filters.push_back(shadow2); - layer2->styles.push_back(noiseStyleNode); - doc->layers.push_back(layer2); - - doc->applyLayout(); - auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); - ASSERT_TRUE(tgfxLayer != nullptr); - - auto surface = Surface::Make(context, canvasW, canvasH); - ASSERT_TRUE(surface != nullptr); - DisplayList displayList; - displayList.root()->addChild(tgfxLayer); - displayList.render(surface.get(), false); - - EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseFilterWithShadow")); - - auto svg = pagx::SVGExporter::ToSVG(*doc); - EXPECT_FALSE(svg.empty()); - EXPECT_NE(svg.find("feTurbulence"), std::string::npos); - - auto outPath = ProjectPath::Absolute("test/out/PAGXTest/NoiseFilterWithShadow.svg"); - auto dirPath = std::filesystem::path(outPath).parent_path(); - if (!std::filesystem::exists(dirPath)) { - std::filesystem::create_directories(dirPath); - } - std::ofstream file(outPath, std::ios::binary); - file.write(svg.data(), static_cast(svg.size())); -} - } // namespace pag From 63cb0eae95bbf0f527df2232e7103eb7c35c4944 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Fri, 5 Jun 2026 16:28:21 +0800 Subject: [PATCH 04/52] Add NoiseFilter test for all element types and update feOffset to use width/2. --- src/pagx/svg/SVGExporter.cpp | 115 +++++++++++-- test/src/PAGXTest.cpp | 319 ++++++++++++++++++++++++++++++++++- 2 files changed, 411 insertions(+), 23 deletions(-) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index dfcfc6f68a..a4c895c2e5 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -53,12 +53,14 @@ #include "pagx/nodes/NoiseStyle.h" #include "pagx/nodes/Path.h" #include "pagx/nodes/PathData.h" +#include "pagx/nodes/Polystar.h" #include "pagx/nodes/RadialGradient.h" #include "pagx/nodes/Rectangle.h" #include "pagx/nodes/SolidColor.h" #include "pagx/nodes/Stroke.h" #include "pagx/nodes/Text.h" #include "pagx/nodes/TextBox.h" +#include "pagx/nodes/TextPath.h" #include "pagx/svg/SVGBlendMode.h" #include "pagx/svg/SVGFeatureProbe.h" #include "pagx/svg/SVGPathParser.h" @@ -103,6 +105,16 @@ static std::string ColorToDisplayP3String(const Color& color) { FloatToString(color.blue) + ")"; } +static void ExpandBounds(float& minX, float& minY, float& maxX, float& maxY, const Rect& bounds) { + if (bounds.isEmpty()) { + return; + } + minX = std::min(minX, bounds.x); + minY = std::min(minY, bounds.y); + maxX = std::max(maxX, bounds.x + bounds.width); + maxY = std::max(maxY, bounds.y + bounds.height); +} + static Rect ComputeContentBounds(const Layer* layer) { float minX = std::numeric_limits::max(); float minY = std::numeric_limits::max(); @@ -114,20 +126,18 @@ static Rect ComputeContentBounds(const Layer* layer) { auto rect = static_cast(element); auto pos = rect->renderPosition(); auto size = rect->renderSize(); - minX = std::min(minX, pos.x - size.width * 0.5f); - minY = std::min(minY, pos.y - size.height * 0.5f); - maxX = std::max(maxX, pos.x + size.width * 0.5f); - maxY = std::max(maxY, pos.y + size.height * 0.5f); + ExpandBounds( + minX, minY, maxX, maxY, + {pos.x - size.width * 0.5f, pos.y - size.height * 0.5f, size.width, size.height}); break; } case NodeType::Ellipse: { auto ellipse = static_cast(element); auto pos = ellipse->renderPosition(); auto size = ellipse->renderSize(); - minX = std::min(minX, pos.x - size.width * 0.5f); - minY = std::min(minY, pos.y - size.height * 0.5f); - maxX = std::max(maxX, pos.x + size.width * 0.5f); - maxY = std::max(maxY, pos.y + size.height * 0.5f); + ExpandBounds( + minX, minY, maxX, maxY, + {pos.x - size.width * 0.5f, pos.y - size.height * 0.5f, size.width, size.height}); break; } case NodeType::Path: { @@ -137,10 +147,37 @@ static Rect ComputeContentBounds(const Layer* layer) { } auto bounds = pathNode->data->getBounds(); auto pos = pathNode->renderPosition(); - minX = std::min(minX, pos.x + bounds.x); - minY = std::min(minY, pos.y + bounds.y); - maxX = std::max(maxX, pos.x + bounds.x + bounds.width); - maxY = std::max(maxY, pos.y + bounds.y + bounds.height); + ExpandBounds(minX, minY, maxX, maxY, + {pos.x + bounds.x, pos.y + bounds.y, bounds.width, bounds.height}); + break; + } + case NodeType::Polystar: { + auto polystar = static_cast(element); + auto pos = polystar->renderPosition(); + auto contentBounds = polystar->getContentBounds(); + auto scale = polystar->renderScale(); + ExpandBounds(minX, minY, maxX, maxY, + {pos.x, pos.y, contentBounds.width * scale, contentBounds.height * scale}); + break; + } + case NodeType::Text: { + auto text = static_cast(element); + ExpandBounds(minX, minY, maxX, maxY, text->layoutBounds()); + break; + } + case NodeType::TextPath: { + auto textPath = static_cast(element); + ExpandBounds(minX, minY, maxX, maxY, textPath->layoutBounds()); + break; + } + case NodeType::Group: { + auto group = static_cast(element); + ExpandBounds(minX, minY, maxX, maxY, group->layoutBounds()); + break; + } + case NodeType::TextBox: { + auto textBox = static_cast(element); + ExpandBounds(minX, minY, maxX, maxY, textBox->layoutBounds()); break; } default: @@ -1706,6 +1743,36 @@ std::string SVGWriter::writeFilterAndStyleDefs(const std::vector& } } + // NoiseFilter/NoiseStyle uses feOffset to shift the turbulence pattern from (0,0) to + // contentBounds center, so the filter region must extend far enough to include both the + // source graphic and the pre-offset noise around the origin. + if (!contentBounds.isEmpty()) { + bool hasNoise = false; + for (const auto* filter : filters) { + if (filter->nodeType() == NodeType::NoiseFilter) { + hasNoise = true; + break; + } + } + if (!hasNoise) { + for (const auto* style : styles) { + if (style->nodeType() == NodeType::NoiseStyle) { + hasNoise = true; + break; + } + } + } + if (hasNoise) { + // feOffset shifts the noise by (width/2, height/2). The filter region must + // extend enough so feTurbulence covers the pre-offset area that maps to content. + // Add padding on all sides so SourceGraphic is not clipped at the filter boundary. + marginLeft = std::max(marginLeft, contentBounds.width * 0.5f); + marginTop = std::max(marginTop, contentBounds.height * 0.5f); + marginRight = std::max(marginRight, contentBounds.width * 0.5f); + marginBottom = std::max(marginBottom, contentBounds.height * 0.5f); + } + } + // Convert pixel margins to percentages, with a minimum of 50% per side to // handle arbitrary element sizes gracefully. float pctLeft = std::max(50.0f, std::ceil(marginLeft)); @@ -1716,10 +1783,26 @@ std::string SVGWriter::writeFilterAndStyleDefs(const std::vector& std::string filterId = generateId("filter"); _defs->openElement("filter"); _defs->addAttribute("id", filterId); - _defs->addAttribute("x", "-" + FloatToString(pctLeft) + "%"); - _defs->addAttribute("y", "-" + FloatToString(pctTop) + "%"); - _defs->addAttribute("width", FloatToString(100.0f + pctLeft + pctRight) + "%"); - _defs->addAttribute("height", FloatToString(100.0f + pctTop + pctBottom) + "%"); + if (!contentBounds.isEmpty()) { + // Use filterUnits="userSpaceOnUse" with absolute pixel coordinates for filters + // containing noise. This decouples the filter region from the bounding box so + // feTurbulence samples at absolute coordinates, producing position-dependent + // noise phase that matches tgfx behavior. + float filterX = contentBounds.x - marginLeft; + float filterY = contentBounds.y - marginTop; + float filterW = contentBounds.width + marginLeft + marginRight; + float filterH = contentBounds.height + marginTop + marginBottom; + _defs->addAttribute("filterUnits", "userSpaceOnUse"); + _defs->addAttribute("x", FloatToString(filterX)); + _defs->addAttribute("y", FloatToString(filterY)); + _defs->addAttribute("width", FloatToString(filterW)); + _defs->addAttribute("height", FloatToString(filterH)); + } else { + _defs->addAttribute("x", "-" + FloatToString(pctLeft) + "%"); + _defs->addAttribute("y", "-" + FloatToString(pctTop) + "%"); + _defs->addAttribute("width", FloatToString(100.0f + pctLeft + pctRight) + "%"); + _defs->addAttribute("height", FloatToString(100.0f + pctTop + pctBottom) + "%"); + } _defs->addAttribute("color-interpolation-filters", "sRGB"); _defs->closeElementStart(); diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 4b5be1cc8a..66978989ec 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7126,12 +7126,12 @@ PAGX_TEST(PAGXTest, HitTestGlobalMatrix) { */ PAGX_TEST(PAGXTest, NoiseFilterModes) { constexpr int canvasW = 400; - constexpr int canvasH = 350; + constexpr int canvasH = 470; auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); - auto makeLayer = [&](float x) { + auto makeLayer = [&](float x, float y) { auto layer = doc->makeNode(); - layer->matrix = pagx::Matrix::Translate(x, 50); + layer->matrix = pagx::Matrix::Translate(x, y); auto rect = doc->makeNode(); rect->position = {50, 50}; rect->size = {100, 100}; @@ -7144,7 +7144,7 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { return layer; }; - auto layer1 = makeLayer(20); + auto layer1 = makeLayer(20, 50); auto mono = doc->makeNode(); mono->mode = pagx::NoiseMode::Mono; mono->size = 8; @@ -7154,7 +7154,7 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { layer1->filters.push_back(mono); doc->layers.push_back(layer1); - auto layer2 = makeLayer(150); + auto layer2 = makeLayer(150, 50); auto duo = doc->makeNode(); duo->mode = pagx::NoiseMode::Duo; duo->size = 8; @@ -7165,7 +7165,7 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { layer2->filters.push_back(duo); doc->layers.push_back(layer2); - auto layer3 = makeLayer(280); + auto layer3 = makeLayer(280, 50); auto multi = doc->makeNode(); multi->mode = pagx::NoiseMode::Multi; multi->size = 8; @@ -7225,6 +7225,39 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { layer6->filters.push_back(multi2); doc->layers.push_back(layer6); + // Row 3: Same rectangle settings as row 1, but use layer matrix for displacement. + // This tests contentBounds in layer local coords starting at (0,0). + auto layer7 = makeLayer(20, 290); + auto mono3 = doc->makeNode(); + mono3->mode = pagx::NoiseMode::Mono; + mono3->size = 8; + mono3->density = 0.5f; + mono3->seed = 42; + mono3->color = {0.0f, 0.0f, 0.0f, 1.0f}; + layer7->filters.push_back(mono3); + doc->layers.push_back(layer7); + + auto layer8 = makeLayer(150, 290); + auto duo3 = doc->makeNode(); + duo3->mode = pagx::NoiseMode::Duo; + duo3->size = 8; + duo3->density = 0.5f; + duo3->seed = 42; + duo3->firstColor = {1.0f, 1.0f, 0.0f, 1.0f}; + duo3->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; + layer8->filters.push_back(duo3); + doc->layers.push_back(layer8); + + auto layer9 = makeLayer(280, 290); + auto multi3 = doc->makeNode(); + multi3->mode = pagx::NoiseMode::Multi; + multi3->size = 8; + multi3->density = 0.5f; + multi3->seed = 42; + multi3->opacity = 1.0f; + layer9->filters.push_back(multi3); + doc->layers.push_back(layer9); + doc->applyLayout(); auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); ASSERT_TRUE(tgfxLayer != nullptr); @@ -7252,6 +7285,278 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { } /** - * Test rendering with NoiseFilter combined with DropShadowFilter and DropShadowStyle. + * Test that NoiseFilter works correctly on Text content. */ +PAGX_TEST(PAGXTest, NoiseFilterOnText) { + constexpr int canvasW = 700; + constexpr int canvasH = 300; + auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); + pagx::FontConfig fontConfig; + fontConfig.addFallbackTypefaces(GetFallbackTypefaces()); + + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(50, 120); + auto text = doc->makeNode(); + text->text = "PAGX"; + text->fontSize = 72; + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {0.2f, 0.6f, 0.9f, 1.0f}; + fill->color = solid; + layer->contents.push_back(text); + layer->contents.push_back(fill); + + auto mono = doc->makeNode(); + mono->mode = pagx::NoiseMode::Mono; + mono->size = 8; + mono->density = 0.5f; + mono->seed = 42; + mono->color = {0.0f, 0.0f, 0.0f, 1.0f}; + layer->filters.push_back(mono); + doc->layers.push_back(layer); + + // Plain text without noise for visual comparison. + auto plainLayer = doc->makeNode(); + plainLayer->matrix = pagx::Matrix::Translate(400, 120); + auto plainText = doc->makeNode(); + plainText->text = "PAGX"; + plainText->fontSize = 72; + auto plainFill = doc->makeNode(); + auto plainSolid = doc->makeNode(); + plainSolid->color = {0.2f, 0.6f, 0.9f, 1.0f}; + plainFill->color = plainSolid; + plainLayer->contents.push_back(plainText); + plainLayer->contents.push_back(plainFill); + doc->layers.push_back(plainLayer); + + doc->applyLayout(&fontConfig); + auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); + ASSERT_TRUE(tgfxLayer != nullptr); + + auto surface = Surface::Make(context, canvasW, canvasH); + ASSERT_TRUE(surface != nullptr); + DisplayList displayList; + displayList.root()->addChild(tgfxLayer); + displayList.render(surface.get(), false); + + EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseFilterOnText")); + + pagx::SVGExportOptions svgOpts; + svgOpts.fontConfig = &fontConfig; + auto svg = pagx::SVGExporter::ToSVG(*doc, svgOpts); + EXPECT_FALSE(svg.empty()); + EXPECT_NE(svg.find("(svg.size())); +} + +/** + * Test NoiseFilter applied to every supported element type (Rectangle, Ellipse, Path, Polystar, + * Text, Group, TextBox) plus Repeater, outputting SVG for visual inspection of contentBounds + * correctness. + */ +PAGX_TEST(PAGXTest, NoiseFilterAllElements) { + constexpr int canvasW = 800; + constexpr int canvasH = 620; + auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); + pagx::FontConfig fontConfig; + fontConfig.addFallbackTypefaces(GetFallbackTypefaces()); + + auto makeNoiseFilter = [&]() { + auto noise = doc->makeNode(); + noise->mode = pagx::NoiseMode::Mono; + noise->size = 10; + noise->density = 0.5f; + noise->seed = 42; + noise->color = {0.0f, 0.0f, 0.0f, 1.0f}; + return noise; + }; + + auto makeFill = [&](float r, float g, float b) { + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {r, g, b, 1.0f}; + fill->color = solid; + return fill; + }; + + // Row 1: Rectangle, Ellipse, Path + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(20, 20); + + auto rect = doc->makeNode(); + rect->position = {60, 60}; + rect->size = {100, 100}; + layer->contents.push_back(rect); + layer->contents.push_back(makeFill(0.2f, 0.5f, 0.8f)); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(150, 20); + + auto ellipse = doc->makeNode(); + ellipse->position = {60, 60}; + ellipse->size = {100, 100}; + layer->contents.push_back(ellipse); + layer->contents.push_back(makeFill(0.8f, 0.3f, 0.3f)); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(280, 20); + + auto path = doc->makeNode(); + path->data = doc->makeNode(); + path->data->moveTo(0, 0); + path->data->lineTo(100, 0); + path->data->lineTo(50, 100); + path->data->close(); + path->position = {10, 10}; + layer->contents.push_back(path); + layer->contents.push_back(makeFill(0.3f, 0.7f, 0.3f)); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + + // Row 2: Polystar, Text, Group + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(20, 160); + + auto polystar = doc->makeNode(); + polystar->position = {60, 60}; + polystar->outerRadius = 50; + polystar->innerRadius = 25; + polystar->pointCount = 5; + layer->contents.push_back(polystar); + layer->contents.push_back(makeFill(0.7f, 0.5f, 0.9f)); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(150, 160); + + auto text = doc->makeNode(); + text->text = "Hi"; + text->fontSize = 60; + layer->contents.push_back(text); + layer->contents.push_back(makeFill(0.2f, 0.6f, 0.9f)); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(280, 160); + + auto group = doc->makeNode(); + group->position = {10, 10}; + auto rect = doc->makeNode(); + rect->position = {40, 40}; + rect->size = {60, 60}; + group->elements.push_back(rect); + group->elements.push_back(makeFill(0.9f, 0.6f, 0.1f)); + layer->contents.push_back(group); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + + // Row 3: TextBox, Repeater + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(20, 320); + + auto textBox = doc->makeNode(); + textBox->width = 120; + textBox->height = 100; + auto text = doc->makeNode(); + text->text = "AB CD"; + text->fontSize = 30; + textBox->elements.push_back(text); + textBox->elements.push_back(makeFill(0.1f, 0.4f, 0.7f)); + layer->contents.push_back(textBox); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(200, 320); + + auto rect = doc->makeNode(); + rect->position = {30, 30}; + rect->size = {40, 40}; + auto repeater = doc->makeNode(); + repeater->copies = 3; + repeater->offset = 0; + repeater->position = {50, 0}; + layer->contents.push_back(rect); + layer->contents.push_back(makeFill(0.3f, 0.8f, 0.6f)); + layer->contents.push_back(repeater); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + + // Row 4: Off-center content (contentBounds origin != 0,0) + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(440, 20); + + auto rect = doc->makeNode(); + rect->position = {80, 40}; + rect->size = {60, 60}; + layer->contents.push_back(rect); + layer->contents.push_back(makeFill(0.6f, 0.2f, 0.8f)); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(580, 20); + + auto ellipse = doc->makeNode(); + ellipse->position = {40, 80}; + ellipse->size = {60, 80}; + layer->contents.push_back(ellipse); + layer->contents.push_back(makeFill(0.8f, 0.7f, 0.2f)); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + + doc->applyLayout(&fontConfig); + auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); + ASSERT_TRUE(tgfxLayer != nullptr); + + auto surface = Surface::Make(context, canvasW, canvasH); + ASSERT_TRUE(surface != nullptr); + DisplayList displayList; + displayList.root()->addChild(tgfxLayer); + displayList.render(surface.get(), false); + + EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseFilterAllElements")); + + pagx::SVGExportOptions svgOpts; + svgOpts.fontConfig = &fontConfig; + auto svg = pagx::SVGExporter::ToSVG(*doc, svgOpts); + EXPECT_FALSE(svg.empty()); + + auto outPath = ProjectPath::Absolute("test/out/PAGXTest/NoiseFilterAllElements.svg"); + auto dirPath = std::filesystem::path(outPath).parent_path(); + if (!std::filesystem::exists(dirPath)) { + std::filesystem::create_directories(dirPath); + } + std::ofstream file(outPath, std::ios::binary); + file.write(svg.data(), static_cast(svg.size())); +} + } // namespace pag From d4fc805689dd3445ae123ab381e3a99df1f8d04a Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Fri, 5 Jun 2026 21:09:23 +0800 Subject: [PATCH 05/52] Fix SVG contentBounds for Repeater and add second test group with layer position displacement. --- src/pagx/svg/SVGExporter.cpp | 196 ++++++++++++++++++++++++----------- test/src/PAGXTest.cpp | 167 ++++++++++++++++++++++++++++- 2 files changed, 302 insertions(+), 61 deletions(-) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index a4c895c2e5..61524e99f8 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -56,6 +56,7 @@ #include "pagx/nodes/Polystar.h" #include "pagx/nodes/RadialGradient.h" #include "pagx/nodes/Rectangle.h" +#include "pagx/nodes/Repeater.h" #include "pagx/nodes/SolidColor.h" #include "pagx/nodes/Stroke.h" #include "pagx/nodes/Text.h" @@ -115,74 +116,91 @@ static void ExpandBounds(float& minX, float& minY, float& maxX, float& maxY, con maxY = std::max(maxY, bounds.y + bounds.height); } -static Rect ComputeContentBounds(const Layer* layer) { +static Rect ComputeElementBounds(const Element* element); + +// std::pow(negative, non-integer) returns NaN. Raise magnitude and reapply sign. +static float SignedPow(float base, float exp) { + float sign = base < 0.0f ? -1.0f : 1.0f; + return sign * std::pow(std::abs(base), exp); +} + +// Apply a 2D affine matrix to a Rect, returning the axis-aligned bounding box +// of the transformed corners. +static Rect MapRect(const Matrix& m, const Rect& r) { + if (r.isEmpty()) { + return {}; + } + float x0 = r.x, y0 = r.y; + float x1 = r.x + r.width, y1 = r.y + r.height; + auto tl = m.mapPoint({x0, y0}); + auto tr = m.mapPoint({x1, y0}); + auto bl = m.mapPoint({x0, y1}); + auto br = m.mapPoint({x1, y1}); + float minX = std::min({tl.x, tr.x, bl.x, br.x}); + float minY = std::min({tl.y, tr.y, bl.y, br.y}); + float maxX = std::max({tl.x, tr.x, bl.x, br.x}); + float maxY = std::max({tl.y, tr.y, bl.y, br.y}); + return {minX, minY, maxX - minX, maxY - minY}; +} + +// Build the per-copy transform matrix for a Repeater copy, mirroring the logic +// in ModifierResolver::MakeCopyGroup and BuildGroupMatrix. +static Matrix BuildRepeaterCopyMatrix(const Repeater* rep, float progress) { + float tx = rep->position.x * progress; + float ty = rep->position.y * progress; + float sx = SignedPow(rep->scale.x, progress); + float sy = SignedPow(rep->scale.y, progress); + float rotation = rep->rotation * progress; + Matrix m = {}; + if (!FloatNearlyZero(rep->anchor.x) || !FloatNearlyZero(rep->anchor.y)) { + m = Matrix::Translate(-rep->anchor.x, -rep->anchor.y); + } + if (!FloatNearlyZero(sx - 1.0f) || !FloatNearlyZero(sy - 1.0f)) { + m = Matrix::Scale(sx, sy) * m; + } + if (!FloatNearlyZero(rotation)) { + m = Matrix::Rotate(rotation) * m; + } + m = Matrix::Translate(tx + rep->anchor.x, ty + rep->anchor.y) * m; + return m; +} + +static Rect ComputeElementsBounds(const std::vector& elements) { float minX = std::numeric_limits::max(); float minY = std::numeric_limits::max(); float maxX = -std::numeric_limits::max(); float maxY = -std::numeric_limits::max(); - for (const auto* element : layer->contents) { - switch (element->nodeType()) { - case NodeType::Rectangle: { - auto rect = static_cast(element); - auto pos = rect->renderPosition(); - auto size = rect->renderSize(); - ExpandBounds( - minX, minY, maxX, maxY, - {pos.x - size.width * 0.5f, pos.y - size.height * 0.5f, size.width, size.height}); - break; - } - case NodeType::Ellipse: { - auto ellipse = static_cast(element); - auto pos = ellipse->renderPosition(); - auto size = ellipse->renderSize(); - ExpandBounds( - minX, minY, maxX, maxY, - {pos.x - size.width * 0.5f, pos.y - size.height * 0.5f, size.width, size.height}); - break; - } - case NodeType::Path: { - auto pathNode = static_cast(element); - if (pathNode->data == nullptr || pathNode->data->isEmpty()) { - break; - } - auto bounds = pathNode->data->getBounds(); - auto pos = pathNode->renderPosition(); - ExpandBounds(minX, minY, maxX, maxY, - {pos.x + bounds.x, pos.y + bounds.y, bounds.width, bounds.height}); - break; - } - case NodeType::Polystar: { - auto polystar = static_cast(element); - auto pos = polystar->renderPosition(); - auto contentBounds = polystar->getContentBounds(); - auto scale = polystar->renderScale(); - ExpandBounds(minX, minY, maxX, maxY, - {pos.x, pos.y, contentBounds.width * scale, contentBounds.height * scale}); - break; - } - case NodeType::Text: { - auto text = static_cast(element); - ExpandBounds(minX, minY, maxX, maxY, text->layoutBounds()); - break; + std::vector preceding; + + for (const auto* element : elements) { + if (element->nodeType() == NodeType::Repeater) { + auto rep = static_cast(element); + if (rep->copies < 0.0f) { + continue; } - case NodeType::TextPath: { - auto textPath = static_cast(element); - ExpandBounds(minX, minY, maxX, maxY, textPath->layoutBounds()); - break; + if (rep->copies == 0.0f) { + return {}; } - case NodeType::Group: { - auto group = static_cast(element); - ExpandBounds(minX, minY, maxX, maxY, group->layoutBounds()); - break; + if (preceding.empty()) { + continue; } - case NodeType::TextBox: { - auto textBox = static_cast(element); - ExpandBounds(minX, minY, maxX, maxY, textBox->layoutBounds()); - break; + auto bodyBounds = ComputeElementsBounds(preceding); + constexpr float MAX_REPEATER_COPIES = 10000.0f; + float copiesF = std::min(rep->copies, MAX_REPEATER_COPIES); + int maxCount = static_cast(std::ceil(copiesF)); + for (int i = 0; i < maxCount; i++) { + float progress = static_cast(i) + rep->offset; + auto copyMatrix = BuildRepeaterCopyMatrix(rep, progress); + auto copyBounds = MapRect(copyMatrix, bodyBounds); + ExpandBounds(minX, minY, maxX, maxY, copyBounds); } - default: - break; + continue; } + auto bounds = ComputeElementBounds(element); + if (bounds.width > 0 || bounds.height > 0) { + ExpandBounds(minX, minY, maxX, maxY, bounds); + } + preceding.push_back(const_cast(element)); } if (minX > maxX) { return {}; @@ -190,6 +208,58 @@ static Rect ComputeContentBounds(const Layer* layer) { return {minX, minY, maxX - minX, maxY - minY}; } +static Rect ComputeElementBounds(const Element* element) { + switch (element->nodeType()) { + case NodeType::Rectangle: { + auto rect = static_cast(element); + auto pos = rect->renderPosition(); + auto size = rect->renderSize(); + return {pos.x - size.width * 0.5f, pos.y - size.height * 0.5f, size.width, size.height}; + } + case NodeType::Ellipse: { + auto ellipse = static_cast(element); + auto pos = ellipse->renderPosition(); + auto size = ellipse->renderSize(); + return {pos.x - size.width * 0.5f, pos.y - size.height * 0.5f, size.width, size.height}; + } + case NodeType::Path: { + auto pathNode = static_cast(element); + if (pathNode->data == nullptr || pathNode->data->isEmpty()) { + return {}; + } + auto bounds = pathNode->data->getBounds(); + auto pos = pathNode->renderPosition(); + return {pos.x + bounds.x, pos.y + bounds.y, bounds.width, bounds.height}; + } + case NodeType::Polystar: { + auto polystar = static_cast(element); + auto pos = polystar->renderPosition(); + auto outerRadius = polystar->renderOuterRadius(); + return {pos.x - outerRadius, pos.y - outerRadius, outerRadius * 2, outerRadius * 2}; + } + case NodeType::Group: { + auto group = static_cast(element); + auto childBounds = ComputeElementsBounds(group->elements); + auto pos = group->renderPosition(); + return {pos.x + childBounds.x, pos.y + childBounds.y, childBounds.width, childBounds.height}; + } + case NodeType::Text: { + auto text = static_cast(element); + return text->layoutBounds(); + } + case NodeType::TextPath: { + auto textPath = static_cast(element); + return textPath->layoutBounds(); + } + case NodeType::TextBox: { + auto textBox = static_cast(element); + return textBox->layoutBounds(); + } + default: + return {}; + } +} + // feGaussianBlur stdDeviation string: one value when blurX == blurY, otherwise two. // Compare via the formatted strings so ULP-level differences from upstream transform // scaling don't emit redundant anisotropic stdDeviation that browsers would honour. @@ -3189,7 +3259,13 @@ void SVGWriter::writeLayerGroupAttributes(SVGBuilder& out, const Layer* layer, } if (!layer->filters.empty() || !layer->styles.empty()) { - auto contentBounds = ComputeContentBounds(layer); + auto contentBounds = ComputeElementsBounds(layer->contents); + auto layerBounds = layer->layoutBounds(); + printf( + "Layer[%s] SVG contentBounds: x=%.2f y=%.2f w=%.2f h=%.2f | layerBounds: x=%.2f y=%.2f " + "w=%.2f h=%.2f\n", + layer->id.c_str(), contentBounds.x, contentBounds.y, contentBounds.width, + contentBounds.height, layerBounds.x, layerBounds.y, layerBounds.width, layerBounds.height); auto filterId = writeFilterAndStyleDefs(layer->filters, layer->styles, contentBounds); if (!filterId.empty()) { out.addAttribute("filter", "url(#" + filterId + ")"); diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 66978989ec..807a19e534 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7364,7 +7364,7 @@ PAGX_TEST(PAGXTest, NoiseFilterOnText) { */ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { constexpr int canvasW = 800; - constexpr int canvasH = 620; + constexpr int canvasH = 900; auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); pagx::FontConfig fontConfig; fontConfig.addFallbackTypefaces(GetFallbackTypefaces()); @@ -7533,10 +7533,175 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { doc->layers.push_back(layer); } + // Second group: same elements, shifted below the first group via layer position (x/y). + constexpr int yShift = 440; + { + auto layer = doc->makeNode(); + layer->x = 20; + layer->y = 20 + yShift; + + auto rect = doc->makeNode(); + rect->position = {60, 60}; + rect->size = {100, 100}; + layer->contents.push_back(rect); + layer->contents.push_back(makeFill(0.2f, 0.5f, 0.8f)); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->x = 150; + layer->y = 20 + yShift; + + auto ellipse = doc->makeNode(); + ellipse->position = {60, 60}; + ellipse->size = {100, 100}; + layer->contents.push_back(ellipse); + layer->contents.push_back(makeFill(0.8f, 0.3f, 0.3f)); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->x = 280; + layer->y = 20 + yShift; + + auto path = doc->makeNode(); + path->data = doc->makeNode(); + path->data->moveTo(0, 0); + path->data->lineTo(100, 0); + path->data->lineTo(50, 100); + path->data->close(); + path->position = {10, 10}; + layer->contents.push_back(path); + layer->contents.push_back(makeFill(0.3f, 0.7f, 0.3f)); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + + // Row 2 shifted: Polystar, Text, Group + { + auto layer = doc->makeNode(); + layer->x = 20; + layer->y = 160 + yShift; + + auto polystar = doc->makeNode(); + polystar->position = {60, 60}; + polystar->outerRadius = 50; + polystar->innerRadius = 25; + polystar->pointCount = 5; + layer->contents.push_back(polystar); + layer->contents.push_back(makeFill(0.7f, 0.5f, 0.9f)); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->x = 150; + layer->y = 160 + yShift; + + auto text = doc->makeNode(); + text->text = "Hi"; + text->fontSize = 60; + layer->contents.push_back(text); + layer->contents.push_back(makeFill(0.2f, 0.6f, 0.9f)); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->x = 280; + layer->y = 160 + yShift; + + auto group = doc->makeNode(); + group->position = {10, 10}; + auto rect = doc->makeNode(); + rect->position = {40, 40}; + rect->size = {60, 60}; + group->elements.push_back(rect); + group->elements.push_back(makeFill(0.9f, 0.6f, 0.1f)); + layer->contents.push_back(group); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + + // Row 3 shifted: TextBox, Repeater + { + auto layer = doc->makeNode(); + layer->x = 20; + layer->y = 320 + yShift; + + auto textBox = doc->makeNode(); + textBox->width = 120; + textBox->height = 100; + auto text = doc->makeNode(); + text->text = "AB CD"; + text->fontSize = 30; + textBox->elements.push_back(text); + textBox->elements.push_back(makeFill(0.1f, 0.4f, 0.7f)); + layer->contents.push_back(textBox); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->x = 200; + layer->y = 320 + yShift; + + auto rect = doc->makeNode(); + rect->position = {30, 30}; + rect->size = {40, 40}; + auto repeater = doc->makeNode(); + repeater->copies = 3; + repeater->offset = 0; + repeater->position = {50, 0}; + layer->contents.push_back(rect); + layer->contents.push_back(makeFill(0.3f, 0.8f, 0.6f)); + layer->contents.push_back(repeater); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + + // Row 4 shifted: Off-center content + { + auto layer = doc->makeNode(); + layer->x = 440; + layer->y = 20 + yShift; + + auto rect = doc->makeNode(); + rect->position = {80, 40}; + rect->size = {60, 60}; + layer->contents.push_back(rect); + layer->contents.push_back(makeFill(0.6f, 0.2f, 0.8f)); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->x = 580; + layer->y = 20 + yShift; + + auto ellipse = doc->makeNode(); + ellipse->position = {40, 80}; + ellipse->size = {60, 80}; + layer->contents.push_back(ellipse); + layer->contents.push_back(makeFill(0.8f, 0.7f, 0.2f)); + layer->filters.push_back(makeNoiseFilter()); + doc->layers.push_back(layer); + } + doc->applyLayout(&fontConfig); auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); ASSERT_TRUE(tgfxLayer != nullptr); + // Log tgfx bounds for each child layer (exclude effects). + for (size_t i = 0; i < tgfxLayer->children().size() && i < doc->layers.size(); i++) { + auto* child = tgfxLayer->children()[i].get(); + auto bounds = child->computeBounds(tgfx::Matrix3D::I(), false, true); + printf("Layer[%zu] tgfx bounds: x=%.2f y=%.2f w=%.2f h=%.2f\n", i, bounds.x(), bounds.y(), + bounds.width(), bounds.height()); + } + auto surface = Surface::Make(context, canvasW, canvasH); ASSERT_TRUE(surface != nullptr); DisplayList displayList; From f034899a19a2fe93f4a341e09c16b31d49ebb14b Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Sun, 7 Jun 2026 14:54:00 +0800 Subject: [PATCH 06/52] Align ComputeElementBounds with tgfx rendering for Path, Group and TextPath --- src/pagx/svg/SVGExporter.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index 61524e99f8..86d30054c9 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -228,6 +228,11 @@ static Rect ComputeElementBounds(const Element* element) { return {}; } auto bounds = pathNode->data->getBounds(); + auto scale = pathNode->renderScale(); + bounds.x *= scale; + bounds.y *= scale; + bounds.width *= scale; + bounds.height *= scale; auto pos = pathNode->renderPosition(); return {pos.x + bounds.x, pos.y + bounds.y, bounds.width, bounds.height}; } @@ -240,8 +245,11 @@ static Rect ComputeElementBounds(const Element* element) { case NodeType::Group: { auto group = static_cast(element); auto childBounds = ComputeElementsBounds(group->elements); - auto pos = group->renderPosition(); - return {pos.x + childBounds.x, pos.y + childBounds.y, childBounds.width, childBounds.height}; + auto groupMatrix = BuildGroupMatrix(group); + if (groupMatrix.isIdentity()) { + return childBounds; + } + return MapRect(groupMatrix, childBounds); } case NodeType::Text: { auto text = static_cast(element); @@ -249,7 +257,17 @@ static Rect ComputeElementBounds(const Element* element) { } case NodeType::TextPath: { auto textPath = static_cast(element); - return textPath->layoutBounds(); + if (textPath->path == nullptr || textPath->path->isEmpty()) { + return textPath->layoutBounds(); + } + auto bounds = textPath->path->getBounds(); + auto scale = textPath->renderScale(); + bounds.x *= scale; + bounds.y *= scale; + bounds.width *= scale; + bounds.height *= scale; + auto pos = textPath->renderPosition(); + return {pos.x + bounds.x, pos.y + bounds.y, bounds.width, bounds.height}; } case NodeType::TextBox: { auto textBox = static_cast(element); From 3222b459fe3bbe2ab91e447b28a1b2e23eddedf7 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Sun, 7 Jun 2026 16:04:22 +0800 Subject: [PATCH 07/52] Add Text::contentBounds and use it in ComputeElementBounds for SVG filter regions --- include/pagx/nodes/Text.h | 7 +++++++ src/pagx/nodes/Text.cpp | 5 +++++ src/pagx/svg/SVGExporter.cpp | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/include/pagx/nodes/Text.h b/include/pagx/nodes/Text.h index db06438324..e3507dee38 100644 --- a/include/pagx/nodes/Text.h +++ b/include/pagx/nodes/Text.h @@ -109,6 +109,13 @@ class Text : public Element, public LayoutNode { /** Returns the effective font size after layout scaling. */ float renderFontSize() const; + /** + * Returns the text content bounds in layer coordinates, computed from the shaped linebox bounds + * offset by renderPosition. This is the bounding rectangle of the rendered text content, + * aligned with how tgfx positions the TextBlob. + */ + Rect contentBounds() const; + NodeType nodeType() const override { return NodeType::Text; } diff --git a/src/pagx/nodes/Text.cpp b/src/pagx/nodes/Text.cpp index 7c04058d5b..757cd94a37 100644 --- a/src/pagx/nodes/Text.cpp +++ b/src/pagx/nodes/Text.cpp @@ -84,4 +84,9 @@ float Text::renderFontSize() const { return fontSize * textScale; } +Rect Text::contentBounds() const { + auto pos = renderPosition(); + return {pos.x + textBounds.x, pos.y + textBounds.y, textBounds.width, textBounds.height}; +} + } // namespace pagx diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index 86d30054c9..50001d5182 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -253,7 +253,7 @@ static Rect ComputeElementBounds(const Element* element) { } case NodeType::Text: { auto text = static_cast(element); - return text->layoutBounds(); + return text->contentBounds(); } case NodeType::TextPath: { auto textPath = static_cast(element); From 1b14e5917d76fdfe8753de3b3e1a8b41ba5321a0 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Sun, 7 Jun 2026 16:07:58 +0800 Subject: [PATCH 08/52] Use TextBlob getTightBounds for precise Text contentBounds aligned with tgfx ink bounds --- src/pagx/nodes/Text.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/pagx/nodes/Text.cpp b/src/pagx/nodes/Text.cpp index 757cd94a37..ef4d962449 100644 --- a/src/pagx/nodes/Text.cpp +++ b/src/pagx/nodes/Text.cpp @@ -21,6 +21,7 @@ #include "pagx/TextLayoutParams.h" #include "pagx/nodes/LayoutNode.h" #include "pagx/utils/TextUtils.h" +#include "renderer/GlyphRunRenderer.h" namespace pagx { @@ -86,6 +87,16 @@ float Text::renderFontSize() const { Rect Text::contentBounds() const { auto pos = renderPosition(); + auto textBlob = glyphData->textBlob; + if (textBlob == nullptr && !glyphData->layoutRuns.empty()) { + textBlob = + GlyphRunRenderer::BuildTextBlobFromLayoutRuns(glyphData->layoutRuns, tgfx::Matrix::I()); + } + if (textBlob) { + auto tightBounds = textBlob->getTightBounds(); + return {pos.x + tightBounds.x(), pos.y + tightBounds.y(), tightBounds.width(), + tightBounds.height()}; + } return {pos.x + textBounds.x, pos.y + textBounds.y, textBounds.width, textBounds.height}; } From d70b2642f569ae7abde85826532896a54d56f69e Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Sun, 7 Jun 2026 16:30:13 +0800 Subject: [PATCH 09/52] Add fontFamily and fontStyle to Text elements in NoiseFilterAllElements test for consistent font rendering --- test/src/PAGXTest.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 807a19e534..dfb64979e7 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7369,6 +7369,14 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { pagx::FontConfig fontConfig; fontConfig.addFallbackTypefaces(GetFallbackTypefaces()); + auto typeface = + Typeface::MakeFromPath(ProjectPath::Absolute("resources/font/NotoSansSC-Regular.otf")); + auto fontFamily = typeface ? typeface->fontFamily() : std::string(); + auto fontStyle = typeface ? typeface->fontStyle() : std::string(); + if (typeface) { + fontConfig.registerTypeface(typeface); + } + auto makeNoiseFilter = [&]() { auto noise = doc->makeNode(); noise->mode = pagx::NoiseMode::Mono; @@ -7450,6 +7458,8 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { auto text = doc->makeNode(); text->text = "Hi"; + text->fontFamily = fontFamily; + text->fontStyle = fontStyle; text->fontSize = 60; layer->contents.push_back(text); layer->contents.push_back(makeFill(0.2f, 0.6f, 0.9f)); @@ -7482,6 +7492,8 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { textBox->height = 100; auto text = doc->makeNode(); text->text = "AB CD"; + text->fontFamily = fontFamily; + text->fontStyle = fontStyle; text->fontSize = 30; textBox->elements.push_back(text); textBox->elements.push_back(makeFill(0.1f, 0.4f, 0.7f)); @@ -7602,6 +7614,8 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { auto text = doc->makeNode(); text->text = "Hi"; + text->fontFamily = fontFamily; + text->fontStyle = fontStyle; text->fontSize = 60; layer->contents.push_back(text); layer->contents.push_back(makeFill(0.2f, 0.6f, 0.9f)); @@ -7636,6 +7650,8 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { textBox->height = 100; auto text = doc->makeNode(); text->text = "AB CD"; + text->fontFamily = fontFamily; + text->fontStyle = fontStyle; text->fontSize = 30; textBox->elements.push_back(text); textBox->elements.push_back(makeFill(0.1f, 0.4f, 0.7f)); From 1b49aacff5b77815529fbff90d8b982f81d0ad74 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Sun, 7 Jun 2026 16:41:24 +0800 Subject: [PATCH 10/52] Use system Helvetica font in NoiseFilterAllElements test for consistent tgfx and SVG rendering --- test/src/PAGXTest.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index dfb64979e7..48024632e7 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7369,8 +7369,7 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { pagx::FontConfig fontConfig; fontConfig.addFallbackTypefaces(GetFallbackTypefaces()); - auto typeface = - Typeface::MakeFromPath(ProjectPath::Absolute("resources/font/NotoSansSC-Regular.otf")); + auto typeface = Typeface::MakeFromPath("/System/Library/Fonts/Helvetica.ttc"); auto fontFamily = typeface ? typeface->fontFamily() : std::string(); auto fontStyle = typeface ? typeface->fontStyle() : std::string(); if (typeface) { From 3eb313d17bce7c9fe4f76e6be3d317b0d82ce80e Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 8 Jun 2026 15:22:11 +0800 Subject: [PATCH 11/52] Remove feOffset from noise filter SVG export and use stitchTiles=stitch to align with Figma. --- include/pagx/nodes/TextBox.h | 2 + src/pagx/nodes/Text.cpp | 5 +- src/pagx/nodes/TextBox.cpp | 44 ++++ src/pagx/svg/SVGExporter.cpp | 173 +++++++-------- test/src/PAGXTest.cpp | 404 ++++++++++++++++++++--------------- 5 files changed, 371 insertions(+), 257 deletions(-) diff --git a/include/pagx/nodes/TextBox.h b/include/pagx/nodes/TextBox.h index a800354569..9949ff556f 100644 --- a/include/pagx/nodes/TextBox.h +++ b/include/pagx/nodes/TextBox.h @@ -90,6 +90,8 @@ class TextBox : public Group { return NodeType::TextBox; } + Rect contentBounds() const; + protected: void onMeasure(LayoutContext* context) override; void setLayoutSize(LayoutContext* context, float targetWidth, float targetHeight) override; diff --git a/src/pagx/nodes/Text.cpp b/src/pagx/nodes/Text.cpp index ef4d962449..bfca519355 100644 --- a/src/pagx/nodes/Text.cpp +++ b/src/pagx/nodes/Text.cpp @@ -93,9 +93,8 @@ Rect Text::contentBounds() const { GlyphRunRenderer::BuildTextBlobFromLayoutRuns(glyphData->layoutRuns, tgfx::Matrix::I()); } if (textBlob) { - auto tightBounds = textBlob->getTightBounds(); - return {pos.x + tightBounds.x(), pos.y + tightBounds.y(), tightBounds.width(), - tightBounds.height()}; + auto bounds = textBlob->getBounds(); + return {pos.x + bounds.x(), pos.y + bounds.y(), bounds.width(), bounds.height()}; } return {pos.x + textBounds.x, pos.y + textBounds.y, textBounds.width, textBounds.height}; } diff --git a/src/pagx/nodes/TextBox.cpp b/src/pagx/nodes/TextBox.cpp index e2fc659fb2..5922fe1dee 100644 --- a/src/pagx/nodes/TextBox.cpp +++ b/src/pagx/nodes/TextBox.cpp @@ -21,6 +21,8 @@ #include "pagx/TextLayout.h" #include "pagx/TextLayoutParams.h" #include "pagx/nodes/LayoutNode.h" +#include "pagx/nodes/Text.h" +#include "renderer/GlyphRunRenderer.h" namespace pagx { @@ -169,4 +171,46 @@ void TextBox::updateLayout(LayoutContext* context) { } } +Rect TextBox::contentBounds() const { + std::vector childText = {}; + TextLayout::CollectTextElements(elements, childText); + float left = 0, top = 0, right = 0, bottom = 0; + bool first = true; + for (auto* text : childText) { + if (text == nullptr) continue; + auto textBlob = text->glyphData->textBlob; + if (textBlob == nullptr && !text->glyphData->layoutRuns.empty()) { + textBlob = GlyphRunRenderer::BuildTextBlobFromLayoutRuns(text->glyphData->layoutRuns, + tgfx::Matrix::I()); + } + auto pos = text->renderPosition(); + float tl, tt, tr, tb; + if (textBlob) { + auto bounds = textBlob->getBounds(); + tl = pos.x + bounds.x(); + tt = pos.y + bounds.y(); + tr = tl + bounds.width(); + tb = tt + bounds.height(); + } else { + tl = pos.x + text->textBounds.x; + tt = pos.y + text->textBounds.y; + tr = tl + text->textBounds.width; + tb = tt + text->textBounds.height; + } + if (first) { + left = tl; + top = tt; + right = tr; + bottom = tb; + first = false; + } else { + if (tl < left) left = tl; + if (tt < top) top = tt; + if (tr > right) right = tr; + if (tb > bottom) bottom = tb; + } + } + return Rect::MakeLTRB(left, top, right, bottom); +} + } // namespace pagx diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index 50001d5182..3572eacf7e 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -116,6 +116,20 @@ static void ExpandBounds(float& minX, float& minY, float& maxX, float& maxY, con maxY = std::max(maxY, bounds.y + bounds.height); } +static Rect IntersectRects(const Rect& a, const Rect& b) { + if (a.isEmpty() || b.isEmpty()) { + return {}; + } + float left = std::max(a.x, b.x); + float top = std::max(a.y, b.y); + float right = std::min(a.x + a.width, b.x + b.width); + float bottom = std::min(a.y + a.height, b.y + b.height); + if (left >= right || top >= bottom) { + return {}; + } + return {left, top, right - left, bottom - top}; +} + static Rect ComputeElementBounds(const Element* element); // std::pow(negative, non-integer) returns NaN. Raise magnitude and reapply sign. @@ -165,6 +179,8 @@ static Matrix BuildRepeaterCopyMatrix(const Repeater* rep, float progress) { return m; } +static Rect ComputeLayerBounds(const Layer* layer); + static Rect ComputeElementsBounds(const std::vector& elements) { float minX = std::numeric_limits::max(); float minY = std::numeric_limits::max(); @@ -271,13 +287,70 @@ static Rect ComputeElementBounds(const Element* element) { } case NodeType::TextBox: { auto textBox = static_cast(element); - return textBox->layoutBounds(); + return textBox->contentBounds(); } default: return {}; } } +static Rect ComputeLayerBounds(const Layer* layer) { + float minX = std::numeric_limits::max(); + float minY = std::numeric_limits::max(); + float maxX = -std::numeric_limits::max(); + float maxY = -std::numeric_limits::max(); + + auto contentsBounds = ComputeElementsBounds(layer->contents); + ExpandBounds(minX, minY, maxX, maxY, contentsBounds); + + if (layer->composition != nullptr) { + for (const auto* compLayer : layer->composition->layers) { + if (!compLayer->visible) continue; + auto compBounds = ComputeLayerBounds(compLayer); + auto compMatrix = BuildLayerMatrix(compLayer); + auto mappedBounds = MapRect(compMatrix, compBounds); + if (compLayer->hasScrollRect) { + auto scrollRect = MapRect(compMatrix, compLayer->scrollRect); + mappedBounds = IntersectRects(mappedBounds, scrollRect); + if (mappedBounds.isEmpty()) continue; + } + if (compLayer->mask != nullptr) { + auto maskBounds = ComputeLayerBounds(compLayer->mask); + auto maskMatrix = BuildLayerMatrix(compLayer->mask); + auto mappedMask = MapRect(maskMatrix, maskBounds); + mappedBounds = IntersectRects(mappedBounds, mappedMask); + if (mappedBounds.isEmpty()) continue; + } + ExpandBounds(minX, minY, maxX, maxY, mappedBounds); + } + } + + for (const auto* child : layer->children) { + if (!child->visible) continue; + auto childBounds = ComputeLayerBounds(child); + auto childMatrix = BuildLayerMatrix(child); + auto mappedBounds = MapRect(childMatrix, childBounds); + if (child->hasScrollRect) { + auto scrollRect = MapRect(childMatrix, child->scrollRect); + mappedBounds = IntersectRects(mappedBounds, scrollRect); + if (mappedBounds.isEmpty()) continue; + } + if (child->mask != nullptr) { + auto maskBounds = ComputeLayerBounds(child->mask); + auto maskMatrix = BuildLayerMatrix(child->mask); + auto mappedMask = MapRect(maskMatrix, maskBounds); + mappedBounds = IntersectRects(mappedBounds, mappedMask); + if (mappedBounds.isEmpty()) continue; + } + ExpandBounds(minX, minY, maxX, maxY, mappedBounds); + } + + if (minX > maxX) { + return {}; + } + return {minX, minY, maxX - minX, maxY - minY}; +} + // feGaussianBlur stdDeviation string: one value when blurX == blurY, otherwise two. // Compare via the formatted strings so ULP-level differences from upstream transform // scaling don't emit redundant anisotropic stdDeviation that browsers would honour. @@ -1163,29 +1236,17 @@ void SVGWriter::writeBlendFilter(const BlendFilter* blend, int& shadowIndex, } std::string SVGWriter::writeNoiseTurbulence(const NoiseFilter* noise, const std::string& resultName, - const Rect& contentBounds) { + const Rect&) { auto freq = noise->size > 0.0f ? 1.0f / noise->size : 0.25f; - std::string turbResult = resultName; _defs->openElement("feTurbulence"); _defs->addAttribute("type", "fractalNoise"); _defs->addAttribute("baseFrequency", FloatToString(freq)); - _defs->addAttribute("stitchTiles", "noStitch"); + _defs->addAttribute("stitchTiles", "stitch"); _defs->addAttribute("numOctaves", "3"); _defs->addAttribute("seed", FloatToString(noise->seed)); - _defs->addAttribute("result", turbResult); + _defs->addAttribute("result", resultName); _defs->closeElementSelfClosing(); - - if (!contentBounds.isEmpty()) { - auto shifted = "shift" + resultName; - _defs->openElement("feOffset"); - _defs->addAttribute("in", turbResult); - _defs->addAttribute("dx", FloatToString(contentBounds.width * 0.5f)); - _defs->addAttribute("dy", FloatToString(contentBounds.height * 0.5f)); - _defs->addAttribute("result", shifted); - _defs->closeElementSelfClosing(); - return shifted; - } - return turbResult; + return resultName; } std::string SVGWriter::writeNoiseBand(const NoiseFilter* noise, bool isDark, @@ -1194,12 +1255,7 @@ std::string SVGWriter::writeNoiseBand(const NoiseFilter* noise, bool isDark, _defs->openElement("feColorMatrix"); _defs->addAttribute("in", turbResult); - _defs->addAttribute("type", "matrix"); - _defs->addAttribute("values", - "0 0 0 0 0 " - "0 0 0 0 0 " - "0 0 0 0 0 " - "0.2126 0.7152 0.0722 0 0"); + _defs->addAttribute("type", "luminanceToAlpha"); _defs->addAttribute("result", "luma" + label); _defs->closeElementSelfClosing(); @@ -1233,29 +1289,17 @@ std::string SVGWriter::writeNoiseBand(const NoiseFilter* noise, bool isDark, } std::string SVGWriter::writeNoiseTurbulence(const NoiseStyle* noise, const std::string& resultName, - const Rect& contentBounds) { + const Rect&) { auto freq = noise->size > 0.0f ? 1.0f / noise->size : 0.25f; - std::string turbResult = resultName; _defs->openElement("feTurbulence"); _defs->addAttribute("type", "fractalNoise"); _defs->addAttribute("baseFrequency", FloatToString(freq)); - _defs->addAttribute("stitchTiles", "noStitch"); + _defs->addAttribute("stitchTiles", "stitch"); _defs->addAttribute("numOctaves", "3"); _defs->addAttribute("seed", FloatToString(noise->seed)); - _defs->addAttribute("result", turbResult); + _defs->addAttribute("result", resultName); _defs->closeElementSelfClosing(); - - if (!contentBounds.isEmpty()) { - auto shifted = "shift" + resultName; - _defs->openElement("feOffset"); - _defs->addAttribute("in", turbResult); - _defs->addAttribute("dx", FloatToString(contentBounds.width * 0.5f)); - _defs->addAttribute("dy", FloatToString(contentBounds.height * 0.5f)); - _defs->addAttribute("result", shifted); - _defs->closeElementSelfClosing(); - return shifted; - } - return turbResult; + return resultName; } std::string SVGWriter::writeNoiseBand(const NoiseStyle* noise, bool isDark, @@ -1264,12 +1308,7 @@ std::string SVGWriter::writeNoiseBand(const NoiseStyle* noise, bool isDark, _defs->openElement("feColorMatrix"); _defs->addAttribute("in", turbResult); - _defs->addAttribute("type", "matrix"); - _defs->addAttribute("values", - "0 0 0 0 0 " - "0 0 0 0 0 " - "0 0 0 0 0 " - "0.2126 0.7152 0.0722 0 0"); + _defs->addAttribute("type", "luminanceToAlpha"); _defs->addAttribute("result", "luma" + label); _defs->closeElementSelfClosing(); @@ -1831,36 +1870,6 @@ std::string SVGWriter::writeFilterAndStyleDefs(const std::vector& } } - // NoiseFilter/NoiseStyle uses feOffset to shift the turbulence pattern from (0,0) to - // contentBounds center, so the filter region must extend far enough to include both the - // source graphic and the pre-offset noise around the origin. - if (!contentBounds.isEmpty()) { - bool hasNoise = false; - for (const auto* filter : filters) { - if (filter->nodeType() == NodeType::NoiseFilter) { - hasNoise = true; - break; - } - } - if (!hasNoise) { - for (const auto* style : styles) { - if (style->nodeType() == NodeType::NoiseStyle) { - hasNoise = true; - break; - } - } - } - if (hasNoise) { - // feOffset shifts the noise by (width/2, height/2). The filter region must - // extend enough so feTurbulence covers the pre-offset area that maps to content. - // Add padding on all sides so SourceGraphic is not clipped at the filter boundary. - marginLeft = std::max(marginLeft, contentBounds.width * 0.5f); - marginTop = std::max(marginTop, contentBounds.height * 0.5f); - marginRight = std::max(marginRight, contentBounds.width * 0.5f); - marginBottom = std::max(marginBottom, contentBounds.height * 0.5f); - } - } - // Convert pixel margins to percentages, with a minimum of 50% per side to // handle arbitrary element sizes gracefully. float pctLeft = std::max(50.0f, std::ceil(marginLeft)); @@ -1873,9 +1882,9 @@ std::string SVGWriter::writeFilterAndStyleDefs(const std::vector& _defs->addAttribute("id", filterId); if (!contentBounds.isEmpty()) { // Use filterUnits="userSpaceOnUse" with absolute pixel coordinates for filters - // containing noise. This decouples the filter region from the bounding box so - // feTurbulence samples at absolute coordinates, producing position-dependent - // noise phase that matches tgfx behavior. + // with known content bounds. This decouples the filter region from the element's + // bounding box so effects like feTurbulence sample at absolute coordinates, + // producing position-dependent noise phase that matches tgfx behavior. float filterX = contentBounds.x - marginLeft; float filterY = contentBounds.y - marginTop; float filterW = contentBounds.width + marginLeft + marginRight; @@ -3277,13 +3286,7 @@ void SVGWriter::writeLayerGroupAttributes(SVGBuilder& out, const Layer* layer, } if (!layer->filters.empty() || !layer->styles.empty()) { - auto contentBounds = ComputeElementsBounds(layer->contents); - auto layerBounds = layer->layoutBounds(); - printf( - "Layer[%s] SVG contentBounds: x=%.2f y=%.2f w=%.2f h=%.2f | layerBounds: x=%.2f y=%.2f " - "w=%.2f h=%.2f\n", - layer->id.c_str(), contentBounds.x, contentBounds.y, contentBounds.width, - contentBounds.height, layerBounds.x, layerBounds.y, layerBounds.width, layerBounds.height); + auto contentBounds = ComputeLayerBounds(layer); auto filterId = writeFilterAndStyleDefs(layer->filters, layer->styles, contentBounds); if (!filterId.empty()) { out.addAttribute("filter", "url(#" + filterId + ")"); diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 48024632e7..d28a4503d4 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7376,15 +7376,34 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { fontConfig.registerTypeface(typeface); } - auto makeNoiseFilter = [&]() { + auto makeMonoNoise = [&](float density) { auto noise = doc->makeNode(); noise->mode = pagx::NoiseMode::Mono; noise->size = 10; - noise->density = 0.5f; + noise->density = density; noise->seed = 42; noise->color = {0.0f, 0.0f, 0.0f, 1.0f}; return noise; }; + auto makeDuoNoise = [&](float density) { + auto noise = doc->makeNode(); + noise->mode = pagx::NoiseMode::Duo; + noise->size = 10; + noise->density = density; + noise->seed = 42; + noise->firstColor = {1.0f, 1.0f, 0.0f, 1.0f}; + noise->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; + return noise; + }; + auto makeMultiNoise = [&](float density) { + auto noise = doc->makeNode(); + noise->mode = pagx::NoiseMode::Multi; + noise->size = 10; + noise->density = density; + noise->seed = 42; + noise->opacity = 1.0f; + return noise; + }; auto makeFill = [&](float r, float g, float b) { auto fill = doc->makeNode(); @@ -7394,7 +7413,7 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { return fill; }; - // Row 1: Rectangle, Ellipse, Path + // Row 1: Rectangle(Mono,0), Ellipse(Duo,0.25), Path(Multi,0.5) { auto layer = doc->makeNode(); layer->matrix = pagx::Matrix::Translate(20, 20); @@ -7404,7 +7423,7 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { rect->size = {100, 100}; layer->contents.push_back(rect); layer->contents.push_back(makeFill(0.2f, 0.5f, 0.8f)); - layer->filters.push_back(makeNoiseFilter()); + layer->filters.push_back(makeMonoNoise(0.0f)); doc->layers.push_back(layer); } { @@ -7416,7 +7435,7 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { ellipse->size = {100, 100}; layer->contents.push_back(ellipse); layer->contents.push_back(makeFill(0.8f, 0.3f, 0.3f)); - layer->filters.push_back(makeNoiseFilter()); + layer->filters.push_back(makeDuoNoise(0.25f)); doc->layers.push_back(layer); } { @@ -7432,11 +7451,11 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { path->position = {10, 10}; layer->contents.push_back(path); layer->contents.push_back(makeFill(0.3f, 0.7f, 0.3f)); - layer->filters.push_back(makeNoiseFilter()); + layer->filters.push_back(makeMultiNoise(0.5f)); doc->layers.push_back(layer); } - // Row 2: Polystar, Text, Group + // Row 2: Polystar(Mono,0.75), Text(Duo,1), Group(Multi,0.5) { auto layer = doc->makeNode(); layer->matrix = pagx::Matrix::Translate(20, 160); @@ -7448,7 +7467,7 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { polystar->pointCount = 5; layer->contents.push_back(polystar); layer->contents.push_back(makeFill(0.7f, 0.5f, 0.9f)); - layer->filters.push_back(makeNoiseFilter()); + layer->filters.push_back(makeMonoNoise(0.75f)); doc->layers.push_back(layer); } { @@ -7462,7 +7481,7 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { text->fontSize = 60; layer->contents.push_back(text); layer->contents.push_back(makeFill(0.2f, 0.6f, 0.9f)); - layer->filters.push_back(makeNoiseFilter()); + layer->filters.push_back(makeDuoNoise(1.0f)); doc->layers.push_back(layer); } { @@ -7477,11 +7496,11 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { group->elements.push_back(rect); group->elements.push_back(makeFill(0.9f, 0.6f, 0.1f)); layer->contents.push_back(group); - layer->filters.push_back(makeNoiseFilter()); + layer->filters.push_back(makeMultiNoise(0.5f)); doc->layers.push_back(layer); } - // Row 3: TextBox, Repeater + // Row 3: TextBox(Duo,0.5), Repeater(Multi,0.5) { auto layer = doc->makeNode(); layer->matrix = pagx::Matrix::Translate(20, 320); @@ -7497,7 +7516,7 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { textBox->elements.push_back(text); textBox->elements.push_back(makeFill(0.1f, 0.4f, 0.7f)); layer->contents.push_back(textBox); - layer->filters.push_back(makeNoiseFilter()); + layer->filters.push_back(makeDuoNoise(0.5f)); doc->layers.push_back(layer); } { @@ -7514,7 +7533,7 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { layer->contents.push_back(rect); layer->contents.push_back(makeFill(0.3f, 0.8f, 0.6f)); layer->contents.push_back(repeater); - layer->filters.push_back(makeNoiseFilter()); + layer->filters.push_back(makeMultiNoise(0.5f)); doc->layers.push_back(layer); } @@ -7528,7 +7547,7 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { rect->size = {60, 60}; layer->contents.push_back(rect); layer->contents.push_back(makeFill(0.6f, 0.2f, 0.8f)); - layer->filters.push_back(makeNoiseFilter()); + layer->filters.push_back(makeMonoNoise(0.5f)); doc->layers.push_back(layer); } { @@ -7540,169 +7559,175 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { ellipse->size = {60, 80}; layer->contents.push_back(ellipse); layer->contents.push_back(makeFill(0.8f, 0.7f, 0.2f)); - layer->filters.push_back(makeNoiseFilter()); - doc->layers.push_back(layer); - } - - // Second group: same elements, shifted below the first group via layer position (x/y). - constexpr int yShift = 440; - { - auto layer = doc->makeNode(); - layer->x = 20; - layer->y = 20 + yShift; - - auto rect = doc->makeNode(); - rect->position = {60, 60}; - rect->size = {100, 100}; - layer->contents.push_back(rect); - layer->contents.push_back(makeFill(0.2f, 0.5f, 0.8f)); - layer->filters.push_back(makeNoiseFilter()); - doc->layers.push_back(layer); - } - { - auto layer = doc->makeNode(); - layer->x = 150; - layer->y = 20 + yShift; - - auto ellipse = doc->makeNode(); - ellipse->position = {60, 60}; - ellipse->size = {100, 100}; - layer->contents.push_back(ellipse); - layer->contents.push_back(makeFill(0.8f, 0.3f, 0.3f)); - layer->filters.push_back(makeNoiseFilter()); - doc->layers.push_back(layer); - } - { - auto layer = doc->makeNode(); - layer->x = 280; - layer->y = 20 + yShift; - - auto path = doc->makeNode(); - path->data = doc->makeNode(); - path->data->moveTo(0, 0); - path->data->lineTo(100, 0); - path->data->lineTo(50, 100); - path->data->close(); - path->position = {10, 10}; - layer->contents.push_back(path); - layer->contents.push_back(makeFill(0.3f, 0.7f, 0.3f)); - layer->filters.push_back(makeNoiseFilter()); - doc->layers.push_back(layer); - } - - // Row 2 shifted: Polystar, Text, Group - { - auto layer = doc->makeNode(); - layer->x = 20; - layer->y = 160 + yShift; - - auto polystar = doc->makeNode(); - polystar->position = {60, 60}; - polystar->outerRadius = 50; - polystar->innerRadius = 25; - polystar->pointCount = 5; - layer->contents.push_back(polystar); - layer->contents.push_back(makeFill(0.7f, 0.5f, 0.9f)); - layer->filters.push_back(makeNoiseFilter()); - doc->layers.push_back(layer); - } - { - auto layer = doc->makeNode(); - layer->x = 150; - layer->y = 160 + yShift; - - auto text = doc->makeNode(); - text->text = "Hi"; - text->fontFamily = fontFamily; - text->fontStyle = fontStyle; - text->fontSize = 60; - layer->contents.push_back(text); - layer->contents.push_back(makeFill(0.2f, 0.6f, 0.9f)); - layer->filters.push_back(makeNoiseFilter()); - doc->layers.push_back(layer); - } - { - auto layer = doc->makeNode(); - layer->x = 280; - layer->y = 160 + yShift; - - auto group = doc->makeNode(); - group->position = {10, 10}; - auto rect = doc->makeNode(); - rect->position = {40, 40}; - rect->size = {60, 60}; - group->elements.push_back(rect); - group->elements.push_back(makeFill(0.9f, 0.6f, 0.1f)); - layer->contents.push_back(group); - layer->filters.push_back(makeNoiseFilter()); + layer->filters.push_back(makeDuoNoise(0.5f)); doc->layers.push_back(layer); } - // Row 3 shifted: TextBox, Repeater + // Second group: same elements inside a parent layer shifted via path. { - auto layer = doc->makeNode(); - layer->x = 20; - layer->y = 320 + yShift; - - auto textBox = doc->makeNode(); - textBox->width = 120; - textBox->height = 100; - auto text = doc->makeNode(); - text->text = "AB CD"; - text->fontFamily = fontFamily; - text->fontStyle = fontStyle; - text->fontSize = 30; - textBox->elements.push_back(text); - textBox->elements.push_back(makeFill(0.1f, 0.4f, 0.7f)); - layer->contents.push_back(textBox); - layer->filters.push_back(makeNoiseFilter()); - doc->layers.push_back(layer); - } - { - auto layer = doc->makeNode(); - layer->x = 200; - layer->y = 320 + yShift; + auto parentLayer = doc->makeNode(); + parentLayer->y = 440; + + { + auto layer = doc->makeNode(); + layer->x = 20; + layer->y = 20; + + auto rect = doc->makeNode(); + rect->position = {60, 60}; + rect->size = {100, 100}; + layer->contents.push_back(rect); + layer->contents.push_back(makeFill(0.2f, 0.5f, 0.8f)); + layer->filters.push_back(makeMonoNoise(0.0f)); + parentLayer->children.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->x = 150; + layer->y = 20; + + auto ellipse = doc->makeNode(); + ellipse->position = {60, 60}; + ellipse->size = {100, 100}; + layer->contents.push_back(ellipse); + layer->contents.push_back(makeFill(0.8f, 0.3f, 0.3f)); + layer->filters.push_back(makeDuoNoise(0.25f)); + parentLayer->children.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->x = 280; + layer->y = 20; + + auto path = doc->makeNode(); + path->data = doc->makeNode(); + path->data->moveTo(0, 0); + path->data->lineTo(100, 0); + path->data->lineTo(50, 100); + path->data->close(); + path->position = {10, 10}; + layer->contents.push_back(path); + layer->contents.push_back(makeFill(0.3f, 0.7f, 0.3f)); + layer->filters.push_back(makeMultiNoise(0.5f)); + parentLayer->children.push_back(layer); + } - auto rect = doc->makeNode(); - rect->position = {30, 30}; - rect->size = {40, 40}; - auto repeater = doc->makeNode(); - repeater->copies = 3; - repeater->offset = 0; - repeater->position = {50, 0}; - layer->contents.push_back(rect); - layer->contents.push_back(makeFill(0.3f, 0.8f, 0.6f)); - layer->contents.push_back(repeater); - layer->filters.push_back(makeNoiseFilter()); - doc->layers.push_back(layer); - } + // Row 2: Polystar, Text, Group + { + auto layer = doc->makeNode(); + layer->x = 20; + layer->y = 160; + + auto polystar = doc->makeNode(); + polystar->position = {60, 60}; + polystar->outerRadius = 50; + polystar->innerRadius = 25; + polystar->pointCount = 5; + layer->contents.push_back(polystar); + layer->contents.push_back(makeFill(0.7f, 0.5f, 0.9f)); + layer->filters.push_back(makeMonoNoise(0.75f)); + parentLayer->children.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->x = 150; + layer->y = 160; + + auto text = doc->makeNode(); + text->text = "Hi"; + text->fontFamily = fontFamily; + text->fontStyle = fontStyle; + text->fontSize = 60; + layer->contents.push_back(text); + layer->contents.push_back(makeFill(0.2f, 0.6f, 0.9f)); + layer->filters.push_back(makeDuoNoise(1.0f)); + parentLayer->children.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->x = 280; + layer->y = 160; + + auto group = doc->makeNode(); + group->position = {10, 10}; + auto rect = doc->makeNode(); + rect->position = {40, 40}; + rect->size = {60, 60}; + group->elements.push_back(rect); + group->elements.push_back(makeFill(0.9f, 0.6f, 0.1f)); + layer->contents.push_back(group); + layer->filters.push_back(makeMultiNoise(0.5f)); + parentLayer->children.push_back(layer); + } - // Row 4 shifted: Off-center content - { - auto layer = doc->makeNode(); - layer->x = 440; - layer->y = 20 + yShift; + // Row 3: TextBox, Repeater + { + auto layer = doc->makeNode(); + layer->x = 20; + layer->y = 320; + + auto textBox = doc->makeNode(); + textBox->width = 120; + textBox->height = 100; + auto text = doc->makeNode(); + text->text = "AB CD"; + text->fontFamily = fontFamily; + text->fontStyle = fontStyle; + text->fontSize = 30; + textBox->elements.push_back(text); + textBox->elements.push_back(makeFill(0.1f, 0.4f, 0.7f)); + layer->contents.push_back(textBox); + layer->filters.push_back(makeDuoNoise(0.5f)); + parentLayer->children.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->x = 200; + layer->y = 320; + + auto rect = doc->makeNode(); + rect->position = {30, 30}; + rect->size = {40, 40}; + auto repeater = doc->makeNode(); + repeater->copies = 3; + repeater->offset = 0; + repeater->position = {50, 0}; + layer->contents.push_back(rect); + layer->contents.push_back(makeFill(0.3f, 0.8f, 0.6f)); + layer->contents.push_back(repeater); + layer->filters.push_back(makeMultiNoise(0.5f)); + parentLayer->children.push_back(layer); + } - auto rect = doc->makeNode(); - rect->position = {80, 40}; - rect->size = {60, 60}; - layer->contents.push_back(rect); - layer->contents.push_back(makeFill(0.6f, 0.2f, 0.8f)); - layer->filters.push_back(makeNoiseFilter()); - doc->layers.push_back(layer); - } - { - auto layer = doc->makeNode(); - layer->x = 580; - layer->y = 20 + yShift; + // Row 4: Off-center content + { + auto layer = doc->makeNode(); + layer->x = 440; + layer->y = 20; + + auto rect = doc->makeNode(); + rect->position = {80, 40}; + rect->size = {60, 60}; + layer->contents.push_back(rect); + layer->contents.push_back(makeFill(0.6f, 0.2f, 0.8f)); + layer->filters.push_back(makeMonoNoise(0.5f)); + parentLayer->children.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->x = 580; + layer->y = 20; + + auto ellipse = doc->makeNode(); + ellipse->position = {40, 80}; + ellipse->size = {60, 80}; + layer->contents.push_back(ellipse); + layer->contents.push_back(makeFill(0.8f, 0.7f, 0.2f)); + layer->filters.push_back(makeDuoNoise(0.5f)); + parentLayer->children.push_back(layer); + } - auto ellipse = doc->makeNode(); - ellipse->position = {40, 80}; - ellipse->size = {60, 80}; - layer->contents.push_back(ellipse); - layer->contents.push_back(makeFill(0.8f, 0.7f, 0.2f)); - layer->filters.push_back(makeNoiseFilter()); - doc->layers.push_back(layer); + doc->layers.push_back(parentLayer); } doc->applyLayout(&fontConfig); @@ -7739,4 +7764,45 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { file.write(svg.data(), static_cast(svg.size())); } +/** + * Test SVG export for a single rectangle with Mono noise filter. + */ +PAGX_TEST(PAGXTest, NoiseFilterSingleRectMono) { + auto doc = pagx::PAGXDocument::Make(200, 200); + + auto layer = doc->makeNode(); + auto rect = doc->makeNode(); + rect->position = {50, 50}; + rect->size = {100, 100}; + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {0.2f, 0.5f, 0.8f, 1.0f}; + fill->color = solid; + layer->contents.push_back(rect); + layer->contents.push_back(fill); + + auto mono = doc->makeNode(); + mono->mode = pagx::NoiseMode::Mono; + mono->size = 8; + mono->density = 0.5f; + mono->seed = 42; + mono->color = {0.0f, 0.0f, 0.0f, 1.0f}; + layer->filters.push_back(mono); + doc->layers.push_back(layer); + + doc->applyLayout(); + auto svg = pagx::SVGExporter::ToSVG(*doc); + EXPECT_FALSE(svg.empty()); + EXPECT_NE(svg.find("(svg.size())); +} + } // namespace pag From 6cf7129ccc1b5eaf6931cd8d547e863f31706206 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 8 Jun 2026 15:41:04 +0800 Subject: [PATCH 12/52] Align Multi noise filter SVG export with Figma using feComponentTransfer for contrast and density banding. --- src/pagx/svg/SVGExporter.cpp | 142 +++++++++++++++++------------------ test/src/PAGXTest.cpp | 20 ++--- 2 files changed, 79 insertions(+), 83 deletions(-) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index 3572eacf7e..cebe1e85ed 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -1452,56 +1452,55 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde return resultName; } - auto nCtResult = writeNoiseTurbulence(noise, "nCt" + filterId, contentBounds); + // Multi mode: single feTurbulence with feComponentTransfer for contrast and density banding, + // matching Figma's approach. feFuncR/G/B apply contrast enhancement, feFuncA applies the + // density threshold band directly on the turbulence alpha channel. + auto turbResult = writeNoiseTurbulence(noise, "n" + filterId, contentBounds); - _defs->openElement("feColorMatrix"); - _defs->addAttribute("in", nCtResult); - _defs->addAttribute("type", "matrix"); - _defs->addAttribute("values", - "2 0 0 0 -0.5 " - "0 2 0 0 -0.5 " - "0 0 2 0 -0.5 " - "0 0 0 1 0"); - _defs->addAttribute("result", "nCon" + filterId); - _defs->closeElementSelfClosing(); - - auto darkBand = writeNoiseBand(noise, true, "MultiDark" + filterId, contentBounds); - auto brightBand = writeNoiseBand(noise, false, "MultiBright" + filterId, contentBounds); + auto d = std::clamp(noise->density, 0.0f, 1.0f); + int threshold = std::clamp(static_cast(std::lround(d * 100.0f)), 0, 100); + std::string table; + table.reserve(300); + for (int i = 0; i < 100; i++) { + table += (i < threshold) ? "1 " : "0 "; + } + table.pop_back(); - _defs->openElement("feComposite"); - _defs->addAttribute("in", darkBand); - _defs->addAttribute("in2", brightBand); - _defs->addAttribute("operator", "out"); - _defs->addAttribute("result", "mB" + filterId); + _defs->openElement("feComponentTransfer"); + _defs->addAttribute("in", turbResult); + _defs->addAttribute("result", "colored" + filterId); + _defs->closeElementStart(); + _defs->openElement("feFuncR"); + _defs->addAttribute("type", "linear"); + _defs->addAttribute("slope", "2"); + _defs->addAttribute("intercept", "-0.5"); _defs->closeElementSelfClosing(); - - _defs->openElement("feComposite"); - _defs->addAttribute("in", "nCon" + filterId); - _defs->addAttribute("in2", "mB" + filterId); - _defs->addAttribute("operator", "in"); - _defs->addAttribute("result", "mN" + filterId); + _defs->openElement("feFuncG"); + _defs->addAttribute("type", "linear"); + _defs->addAttribute("slope", "2"); + _defs->addAttribute("intercept", "-0.5"); _defs->closeElementSelfClosing(); - - _defs->openElement("feColorMatrix"); - _defs->addAttribute("in", "mN" + filterId); - _defs->addAttribute("type", "matrix"); - std::string opacityValues = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 "; - opacityValues += FloatToString(noise->opacity); - opacityValues += " 0"; - _defs->addAttribute("values", opacityValues); - _defs->addAttribute("result", "mO" + filterId); + _defs->openElement("feFuncB"); + _defs->addAttribute("type", "linear"); + _defs->addAttribute("slope", "2"); + _defs->addAttribute("intercept", "-0.5"); + _defs->closeElementSelfClosing(); + _defs->openElement("feFuncA"); + _defs->addAttribute("type", "discrete"); + _defs->addAttribute("tableValues", table); _defs->closeElementSelfClosing(); + _defs->closeElement(); _defs->openElement("feComposite"); - _defs->addAttribute("in", "mO" + filterId); + _defs->addAttribute("in", "colored" + filterId); _defs->addAttribute("in2", currentSource); _defs->addAttribute("operator", "in"); - _defs->addAttribute("result", "mC" + filterId); + _defs->addAttribute("result", "clipped" + filterId); _defs->closeElementSelfClosing(); auto resultName = "noiseOut" + filterId; _defs->openElement("feBlend"); - _defs->addAttribute("in", "mC" + filterId); + _defs->addAttribute("in", "clipped" + filterId); _defs->addAttribute("in2", currentSource); auto modeStr = BlendModeToFEBlendString(noise->blendMode); if (modeStr) { @@ -1602,50 +1601,47 @@ std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleI return resultName; } - // Multi mode - auto nCtResult = writeNoiseTurbulence(noise, "nCt" + styleId, contentBounds); + // Multi mode: single feTurbulence with feComponentTransfer for contrast and density banding, + // matching Figma's approach. + auto turbResult = writeNoiseTurbulence(noise, "n" + styleId, contentBounds); - _defs->openElement("feColorMatrix"); - _defs->addAttribute("in", nCtResult); - _defs->addAttribute("type", "matrix"); - _defs->addAttribute("values", - "2 0 0 0 -0.5 " - "0 2 0 0 -0.5 " - "0 0 2 0 -0.5 " - "0 0 0 1 0"); - _defs->addAttribute("result", "nCon" + styleId); - _defs->closeElementSelfClosing(); - - auto darkBand = writeNoiseBand(noise, true, "MultiDark" + styleId, contentBounds); - auto brightBand = writeNoiseBand(noise, false, "MultiBright" + styleId, contentBounds); + auto d = std::clamp(noise->density, 0.0f, 1.0f); + int threshold = std::clamp(static_cast(std::lround(d * 100.0f)), 0, 100); + std::string table; + table.reserve(300); + for (int i = 0; i < 100; i++) { + table += (i < threshold) ? "1 " : "0 "; + } + table.pop_back(); - _defs->openElement("feComposite"); - _defs->addAttribute("in", darkBand); - _defs->addAttribute("in2", brightBand); - _defs->addAttribute("operator", "out"); - _defs->addAttribute("result", "mB" + styleId); + _defs->openElement("feComponentTransfer"); + _defs->addAttribute("in", turbResult); + _defs->addAttribute("result", "colored" + styleId); + _defs->closeElementStart(); + _defs->openElement("feFuncR"); + _defs->addAttribute("type", "linear"); + _defs->addAttribute("slope", "2"); + _defs->addAttribute("intercept", "-0.5"); _defs->closeElementSelfClosing(); - - _defs->openElement("feComposite"); - _defs->addAttribute("in", "nCon" + styleId); - _defs->addAttribute("in2", "mB" + styleId); - _defs->addAttribute("operator", "in"); - _defs->addAttribute("result", "mN" + styleId); + _defs->openElement("feFuncG"); + _defs->addAttribute("type", "linear"); + _defs->addAttribute("slope", "2"); + _defs->addAttribute("intercept", "-0.5"); _defs->closeElementSelfClosing(); - - _defs->openElement("feColorMatrix"); - _defs->addAttribute("in", "mN" + styleId); - _defs->addAttribute("type", "matrix"); - std::string opacityValues = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 "; - opacityValues += FloatToString(noise->opacity); - opacityValues += " 0"; - _defs->addAttribute("values", opacityValues); - _defs->addAttribute("result", "mO" + styleId); + _defs->openElement("feFuncB"); + _defs->addAttribute("type", "linear"); + _defs->addAttribute("slope", "2"); + _defs->addAttribute("intercept", "-0.5"); + _defs->closeElementSelfClosing(); + _defs->openElement("feFuncA"); + _defs->addAttribute("type", "discrete"); + _defs->addAttribute("tableValues", table); _defs->closeElementSelfClosing(); + _defs->closeElement(); auto resultName = "noiseStyleOut" + styleId; _defs->openElement("feComposite"); - _defs->addAttribute("in", "mO" + styleId); + _defs->addAttribute("in", "colored" + styleId); _defs->addAttribute("in2", "SourceGraphic"); _defs->addAttribute("operator", "in"); _defs->addAttribute("result", resultName); diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index d28a4503d4..5b89a600f2 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7765,9 +7765,9 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { } /** - * Test SVG export for a single rectangle with Mono noise filter. + * Test SVG export for a single rectangle with Multi noise filter. */ -PAGX_TEST(PAGXTest, NoiseFilterSingleRectMono) { +PAGX_TEST(PAGXTest, NoiseFilterSingleRectMulti) { auto doc = pagx::PAGXDocument::Make(200, 200); auto layer = doc->makeNode(); @@ -7781,13 +7781,13 @@ PAGX_TEST(PAGXTest, NoiseFilterSingleRectMono) { layer->contents.push_back(rect); layer->contents.push_back(fill); - auto mono = doc->makeNode(); - mono->mode = pagx::NoiseMode::Mono; - mono->size = 8; - mono->density = 0.5f; - mono->seed = 42; - mono->color = {0.0f, 0.0f, 0.0f, 1.0f}; - layer->filters.push_back(mono); + auto multi = doc->makeNode(); + multi->mode = pagx::NoiseMode::Multi; + multi->size = 8; + multi->density = 0.5f; + multi->seed = 42; + multi->opacity = 1.0f; + layer->filters.push_back(multi); doc->layers.push_back(layer); doc->applyLayout(); @@ -7796,7 +7796,7 @@ PAGX_TEST(PAGXTest, NoiseFilterSingleRectMono) { EXPECT_NE(svg.find(" Date: Mon, 8 Jun 2026 16:03:39 +0800 Subject: [PATCH 13/52] Align Multi noise SVG export with tgfx MakeDarkDensityFilter and remove debug log output. --- src/pagx/svg/SVGExporter.cpp | 122 +++++++++++++++++++++++++++-------- test/src/PAGXTest.cpp | 50 -------------- 2 files changed, 95 insertions(+), 77 deletions(-) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index cebe1e85ed..b5b156e333 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -1452,23 +1452,15 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde return resultName; } - // Multi mode: single feTurbulence with feComponentTransfer for contrast and density banding, - // matching Figma's approach. feFuncR/G/B apply contrast enhancement, feFuncA applies the - // density threshold band directly on the turbulence alpha channel. + // Multi mode: single feTurbulence processed through two branches — contrast (RGB) and + // luminance-based density band (alpha) — then masked and blended, aligned with tgfx's + // MultiNoiseFilter::onBuildBaseShader via MakeDarkDensityFilter. auto turbResult = writeNoiseTurbulence(noise, "n" + filterId, contentBounds); - auto d = std::clamp(noise->density, 0.0f, 1.0f); - int threshold = std::clamp(static_cast(std::lround(d * 100.0f)), 0, 100); - std::string table; - table.reserve(300); - for (int i = 0; i < 100; i++) { - table += (i < threshold) ? "1 " : "0 "; - } - table.pop_back(); - + // Contrast branch: feFuncR/G/B linear applies slope=2 intercept=-0.5, keeping alpha unchanged. _defs->openElement("feComponentTransfer"); _defs->addAttribute("in", turbResult); - _defs->addAttribute("result", "colored" + filterId); + _defs->addAttribute("result", "contrast" + filterId); _defs->closeElementStart(); _defs->openElement("feFuncR"); _defs->addAttribute("type", "linear"); @@ -1485,6 +1477,30 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde _defs->addAttribute("slope", "2"); _defs->addAttribute("intercept", "-0.5"); _defs->closeElementSelfClosing(); + _defs->closeElement(); + + // Density band branch: luminance-to-alpha then discrete band, matching MakeDarkDensityFilter + // which uses ComputeDarkBandBuckets, lumaMatrix + AlphaThreshold, and DstOut(lower, upper). + auto d = std::clamp(noise->density, 0.0f, 1.0f); + int lower = std::clamp(static_cast(std::lround(-25.0f * d + 25.0f)), 0, 99); + int upper = std::clamp(static_cast(std::lround(24.0f * d + 25.0f)), 0, 99); + std::string table; + table.reserve(300); + for (int i = 0; i < 100; i++) { + table += (i >= lower && i <= upper) ? "1 " : "0 "; + } + table.pop_back(); + + _defs->openElement("feColorMatrix"); + _defs->addAttribute("in", turbResult); + _defs->addAttribute("type", "luminanceToAlpha"); + _defs->addAttribute("result", "luma" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComponentTransfer"); + _defs->addAttribute("in", "luma" + filterId); + _defs->addAttribute("result", "band" + filterId); + _defs->closeElementStart(); _defs->openElement("feFuncA"); _defs->addAttribute("type", "discrete"); _defs->addAttribute("tableValues", table); @@ -1492,7 +1508,25 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde _defs->closeElement(); _defs->openElement("feComposite"); - _defs->addAttribute("in", "colored" + filterId); + _defs->addAttribute("in", "contrast" + filterId); + _defs->addAttribute("in2", "band" + filterId); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "masked" + filterId); + _defs->closeElementSelfClosing(); + + // Apply opacity scaling to the masked noise alpha channel. + _defs->openElement("feColorMatrix"); + _defs->addAttribute("in", "masked" + filterId); + _defs->addAttribute("type", "matrix"); + std::string opacityValues = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 "; + opacityValues += FloatToString(noise->opacity); + opacityValues += " 0"; + _defs->addAttribute("values", opacityValues); + _defs->addAttribute("result", "final" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "final" + filterId); _defs->addAttribute("in2", currentSource); _defs->addAttribute("operator", "in"); _defs->addAttribute("result", "clipped" + filterId); @@ -1601,22 +1635,15 @@ std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleI return resultName; } - // Multi mode: single feTurbulence with feComponentTransfer for contrast and density banding, - // matching Figma's approach. + // Multi mode: single feTurbulence processed through two branches — contrast (RGB) and + // luminance-based density band (alpha) — then masked and clipped to SourceGraphic, aligned + // with tgfx's MultiNoiseFilter::onBuildBaseShader via MakeDarkDensityFilter. auto turbResult = writeNoiseTurbulence(noise, "n" + styleId, contentBounds); - auto d = std::clamp(noise->density, 0.0f, 1.0f); - int threshold = std::clamp(static_cast(std::lround(d * 100.0f)), 0, 100); - std::string table; - table.reserve(300); - for (int i = 0; i < 100; i++) { - table += (i < threshold) ? "1 " : "0 "; - } - table.pop_back(); - + // Contrast branch. _defs->openElement("feComponentTransfer"); _defs->addAttribute("in", turbResult); - _defs->addAttribute("result", "colored" + styleId); + _defs->addAttribute("result", "contrast" + styleId); _defs->closeElementStart(); _defs->openElement("feFuncR"); _defs->addAttribute("type", "linear"); @@ -1633,15 +1660,56 @@ std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleI _defs->addAttribute("slope", "2"); _defs->addAttribute("intercept", "-0.5"); _defs->closeElementSelfClosing(); + _defs->closeElement(); + + // Density band branch. + auto d = std::clamp(noise->density, 0.0f, 1.0f); + int lower = std::clamp(static_cast(std::lround(-25.0f * d + 25.0f)), 0, 99); + int upper = std::clamp(static_cast(std::lround(24.0f * d + 25.0f)), 0, 99); + std::string table; + table.reserve(300); + for (int i = 0; i < 100; i++) { + table += (i >= lower && i <= upper) ? "1 " : "0 "; + } + table.pop_back(); + + _defs->openElement("feColorMatrix"); + _defs->addAttribute("in", turbResult); + _defs->addAttribute("type", "luminanceToAlpha"); + _defs->addAttribute("result", "luma" + styleId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComponentTransfer"); + _defs->addAttribute("in", "luma" + styleId); + _defs->addAttribute("result", "band" + styleId); + _defs->closeElementStart(); _defs->openElement("feFuncA"); _defs->addAttribute("type", "discrete"); _defs->addAttribute("tableValues", table); _defs->closeElementSelfClosing(); _defs->closeElement(); + _defs->openElement("feComposite"); + _defs->addAttribute("in", "contrast" + styleId); + _defs->addAttribute("in2", "band" + styleId); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "masked" + styleId); + _defs->closeElementSelfClosing(); + + // Apply opacity scaling. + _defs->openElement("feColorMatrix"); + _defs->addAttribute("in", "masked" + styleId); + _defs->addAttribute("type", "matrix"); + std::string opacityValues = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 "; + opacityValues += FloatToString(noise->opacity); + opacityValues += " 0"; + _defs->addAttribute("values", opacityValues); + _defs->addAttribute("result", "final" + styleId); + _defs->closeElementSelfClosing(); + auto resultName = "noiseStyleOut" + styleId; _defs->openElement("feComposite"); - _defs->addAttribute("in", "colored" + styleId); + _defs->addAttribute("in", "final" + styleId); _defs->addAttribute("in2", "SourceGraphic"); _defs->addAttribute("operator", "in"); _defs->addAttribute("result", resultName); diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 5b89a600f2..1fee53e222 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7734,14 +7734,6 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); ASSERT_TRUE(tgfxLayer != nullptr); - // Log tgfx bounds for each child layer (exclude effects). - for (size_t i = 0; i < tgfxLayer->children().size() && i < doc->layers.size(); i++) { - auto* child = tgfxLayer->children()[i].get(); - auto bounds = child->computeBounds(tgfx::Matrix3D::I(), false, true); - printf("Layer[%zu] tgfx bounds: x=%.2f y=%.2f w=%.2f h=%.2f\n", i, bounds.x(), bounds.y(), - bounds.width(), bounds.height()); - } - auto surface = Surface::Make(context, canvasW, canvasH); ASSERT_TRUE(surface != nullptr); DisplayList displayList; @@ -7763,46 +7755,4 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { std::ofstream file(outPath, std::ios::binary); file.write(svg.data(), static_cast(svg.size())); } - -/** - * Test SVG export for a single rectangle with Multi noise filter. - */ -PAGX_TEST(PAGXTest, NoiseFilterSingleRectMulti) { - auto doc = pagx::PAGXDocument::Make(200, 200); - - auto layer = doc->makeNode(); - auto rect = doc->makeNode(); - rect->position = {50, 50}; - rect->size = {100, 100}; - auto fill = doc->makeNode(); - auto solid = doc->makeNode(); - solid->color = {0.2f, 0.5f, 0.8f, 1.0f}; - fill->color = solid; - layer->contents.push_back(rect); - layer->contents.push_back(fill); - - auto multi = doc->makeNode(); - multi->mode = pagx::NoiseMode::Multi; - multi->size = 8; - multi->density = 0.5f; - multi->seed = 42; - multi->opacity = 1.0f; - layer->filters.push_back(multi); - doc->layers.push_back(layer); - - doc->applyLayout(); - auto svg = pagx::SVGExporter::ToSVG(*doc); - EXPECT_FALSE(svg.empty()); - EXPECT_NE(svg.find("(svg.size())); -} - } // namespace pag From b40f365b1d9e3de2b510b2cbc292669c57683384 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 8 Jun 2026 16:22:24 +0800 Subject: [PATCH 14/52] Fix CR issues: TextBox empty bounds early return, ComputeLayerBounds depth limit, remove unused contentBounds params, add NoiseMode enum conversion. --- src/pagx/nodes/TextBox.cpp | 3 ++ src/pagx/svg/SVGExporter.cpp | 66 +++++++++++++++++---------------- src/pagx/utils/StringParser.cpp | 3 ++ src/pagx/utils/StringParser.h | 8 ++++ 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/pagx/nodes/TextBox.cpp b/src/pagx/nodes/TextBox.cpp index 5922fe1dee..0e1e7f47d2 100644 --- a/src/pagx/nodes/TextBox.cpp +++ b/src/pagx/nodes/TextBox.cpp @@ -210,6 +210,9 @@ Rect TextBox::contentBounds() const { if (tb > bottom) bottom = tb; } } + if (first) { + return {}; + } return Rect::MakeLTRB(left, top, right, bottom); } diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index b5b156e333..41df19d0c9 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -179,7 +179,7 @@ static Matrix BuildRepeaterCopyMatrix(const Repeater* rep, float progress) { return m; } -static Rect ComputeLayerBounds(const Layer* layer); +static Rect ComputeLayerBounds(const Layer* layer, int depth = 0); static Rect ComputeElementsBounds(const std::vector& elements) { float minX = std::numeric_limits::max(); @@ -294,7 +294,13 @@ static Rect ComputeElementBounds(const Element* element) { } } -static Rect ComputeLayerBounds(const Layer* layer) { +static constexpr int MAX_LAYER_BOUNDS_DEPTH = 32; + +static Rect ComputeLayerBounds(const Layer* layer, int depth) { + if (depth >= MAX_LAYER_BOUNDS_DEPTH) { + return {}; + } + float minX = std::numeric_limits::max(); float minY = std::numeric_limits::max(); float maxX = -std::numeric_limits::max(); @@ -306,7 +312,7 @@ static Rect ComputeLayerBounds(const Layer* layer) { if (layer->composition != nullptr) { for (const auto* compLayer : layer->composition->layers) { if (!compLayer->visible) continue; - auto compBounds = ComputeLayerBounds(compLayer); + auto compBounds = ComputeLayerBounds(compLayer, depth + 1); auto compMatrix = BuildLayerMatrix(compLayer); auto mappedBounds = MapRect(compMatrix, compBounds); if (compLayer->hasScrollRect) { @@ -315,7 +321,7 @@ static Rect ComputeLayerBounds(const Layer* layer) { if (mappedBounds.isEmpty()) continue; } if (compLayer->mask != nullptr) { - auto maskBounds = ComputeLayerBounds(compLayer->mask); + auto maskBounds = ComputeLayerBounds(compLayer->mask, depth + 1); auto maskMatrix = BuildLayerMatrix(compLayer->mask); auto mappedMask = MapRect(maskMatrix, maskBounds); mappedBounds = IntersectRects(mappedBounds, mappedMask); @@ -327,7 +333,7 @@ static Rect ComputeLayerBounds(const Layer* layer) { for (const auto* child : layer->children) { if (!child->visible) continue; - auto childBounds = ComputeLayerBounds(child); + auto childBounds = ComputeLayerBounds(child, depth + 1); auto childMatrix = BuildLayerMatrix(child); auto mappedBounds = MapRect(childMatrix, childBounds); if (child->hasScrollRect) { @@ -336,7 +342,7 @@ static Rect ComputeLayerBounds(const Layer* layer) { if (mappedBounds.isEmpty()) continue; } if (child->mask != nullptr) { - auto maskBounds = ComputeLayerBounds(child->mask); + auto maskBounds = ComputeLayerBounds(child->mask, depth + 1); auto maskMatrix = BuildLayerMatrix(child->mask); auto mappedMask = MapRect(maskMatrix, maskBounds); mappedBounds = IntersectRects(mappedBounds, mappedMask); @@ -653,14 +659,10 @@ class SVGWriter { void writeColorMatrixFilter(const ColorMatrixFilter* cm, int& colorMatrixIndex, std::string& currentSource); void writeBlendFilter(const BlendFilter* blend, int& shadowIndex, std::string& currentSource); - std::string writeNoiseTurbulence(const NoiseFilter* noise, const std::string& resultName, - const Rect& contentBounds); - std::string writeNoiseTurbulence(const NoiseStyle* noise, const std::string& resultName, - const Rect& contentBounds); - std::string writeNoiseBand(const NoiseFilter* noise, bool isDark, const std::string& label, - const Rect& contentBounds); - std::string writeNoiseBand(const NoiseStyle* noise, bool isDark, const std::string& label, - const Rect& contentBounds); + std::string writeNoiseTurbulence(const NoiseFilter* noise, const std::string& resultName); + std::string writeNoiseTurbulence(const NoiseStyle* noise, const std::string& resultName); + std::string writeNoiseBand(const NoiseFilter* noise, bool isDark, const std::string& label); + std::string writeNoiseBand(const NoiseStyle* noise, bool isDark, const std::string& label); std::string writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, std::string& currentSource, const Rect& contentBounds); // Collected per-filter state fed into the final feMerge aggregation. @@ -1235,8 +1237,8 @@ void SVGWriter::writeBlendFilter(const BlendFilter* blend, int& shadowIndex, currentSource = blendOut; } -std::string SVGWriter::writeNoiseTurbulence(const NoiseFilter* noise, const std::string& resultName, - const Rect&) { +std::string SVGWriter::writeNoiseTurbulence(const NoiseFilter* noise, + const std::string& resultName) { auto freq = noise->size > 0.0f ? 1.0f / noise->size : 0.25f; _defs->openElement("feTurbulence"); _defs->addAttribute("type", "fractalNoise"); @@ -1250,8 +1252,8 @@ std::string SVGWriter::writeNoiseTurbulence(const NoiseFilter* noise, const std: } std::string SVGWriter::writeNoiseBand(const NoiseFilter* noise, bool isDark, - const std::string& label, const Rect& contentBounds) { - auto turbResult = writeNoiseTurbulence(noise, "turb" + label, contentBounds); + const std::string& label) { + auto turbResult = writeNoiseTurbulence(noise, "turb" + label); _defs->openElement("feColorMatrix"); _defs->addAttribute("in", turbResult); @@ -1288,8 +1290,8 @@ std::string SVGWriter::writeNoiseBand(const NoiseFilter* noise, bool isDark, return "band" + label; } -std::string SVGWriter::writeNoiseTurbulence(const NoiseStyle* noise, const std::string& resultName, - const Rect&) { +std::string SVGWriter::writeNoiseTurbulence(const NoiseStyle* noise, + const std::string& resultName) { auto freq = noise->size > 0.0f ? 1.0f / noise->size : 0.25f; _defs->openElement("feTurbulence"); _defs->addAttribute("type", "fractalNoise"); @@ -1303,8 +1305,8 @@ std::string SVGWriter::writeNoiseTurbulence(const NoiseStyle* noise, const std:: } std::string SVGWriter::writeNoiseBand(const NoiseStyle* noise, bool isDark, - const std::string& label, const Rect& contentBounds) { - auto turbResult = writeNoiseTurbulence(noise, "turb" + label, contentBounds); + const std::string& label) { + auto turbResult = writeNoiseTurbulence(noise, "turb" + label); _defs->openElement("feColorMatrix"); _defs->addAttribute("in", turbResult); @@ -1342,11 +1344,11 @@ std::string SVGWriter::writeNoiseBand(const NoiseStyle* noise, bool isDark, } std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, - std::string& currentSource, const Rect& contentBounds) { + std::string& currentSource, const Rect&) { std::string filterId = "noise" + std::to_string(noiseIndex++); if (noise->mode == NoiseMode::Mono) { - auto band = writeNoiseBand(noise, true, "Dark" + filterId, contentBounds); + auto band = writeNoiseBand(noise, true, "Dark" + filterId); _defs->openElement("feFlood"); _defs->addAttribute("flood-color", ColorToSVGString(noise->color)); if (noise->color.alpha < 1.0f) { @@ -1384,8 +1386,8 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde } if (noise->mode == NoiseMode::Duo) { - auto dark = writeNoiseBand(noise, true, "Dark" + filterId, contentBounds); - auto bright = writeNoiseBand(noise, false, "Bright" + filterId, contentBounds); + auto dark = writeNoiseBand(noise, true, "Dark" + filterId); + auto bright = writeNoiseBand(noise, false, "Bright" + filterId); _defs->openElement("feComposite"); _defs->addAttribute("in", dark); @@ -1455,7 +1457,7 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde // Multi mode: single feTurbulence processed through two branches — contrast (RGB) and // luminance-based density band (alpha) — then masked and blended, aligned with tgfx's // MultiNoiseFilter::onBuildBaseShader via MakeDarkDensityFilter. - auto turbResult = writeNoiseTurbulence(noise, "n" + filterId, contentBounds); + auto turbResult = writeNoiseTurbulence(noise, "n" + filterId); // Contrast branch: feFuncR/G/B linear applies slope=2 intercept=-0.5, keeping alpha unchanged. _defs->openElement("feComponentTransfer"); @@ -1547,11 +1549,11 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde } std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleIndex, - const Rect& contentBounds) { + const Rect&) { std::string styleId = "noiseStyle" + std::to_string(noiseStyleIndex++); if (noise->mode == NoiseMode::Mono) { - auto band = writeNoiseBand(noise, true, "Dark" + styleId, contentBounds); + auto band = writeNoiseBand(noise, true, "Dark" + styleId); _defs->openElement("feFlood"); _defs->addAttribute("flood-color", ColorToSVGString(noise->color)); if (noise->color.alpha < 1.0f) { @@ -1578,8 +1580,8 @@ std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleI } if (noise->mode == NoiseMode::Duo) { - auto dark = writeNoiseBand(noise, true, "Dark" + styleId, contentBounds); - auto bright = writeNoiseBand(noise, false, "Bright" + styleId, contentBounds); + auto dark = writeNoiseBand(noise, true, "Dark" + styleId); + auto bright = writeNoiseBand(noise, false, "Bright" + styleId); _defs->openElement("feComposite"); _defs->addAttribute("in", dark); @@ -1638,7 +1640,7 @@ std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleI // Multi mode: single feTurbulence processed through two branches — contrast (RGB) and // luminance-based density band (alpha) — then masked and clipped to SourceGraphic, aligned // with tgfx's MultiNoiseFilter::onBuildBaseShader via MakeDarkDensityFilter. - auto turbResult = writeNoiseTurbulence(noise, "n" + styleId, contentBounds); + auto turbResult = writeNoiseTurbulence(noise, "n" + styleId); // Contrast branch. _defs->openElement("feComponentTransfer"); diff --git a/src/pagx/utils/StringParser.cpp b/src/pagx/utils/StringParser.cpp index 3a751dff8c..31e81c7bbb 100644 --- a/src/pagx/utils/StringParser.cpp +++ b/src/pagx/utils/StringParser.cpp @@ -247,6 +247,9 @@ DEFINE_ENUM_CONVERSION(Arrangement, Arrangement::Start, {Arrangement::Start, "st {Arrangement::SpaceEvenly, "spaceEvenly"}, {Arrangement::SpaceAround, "spaceAround"}) +DEFINE_ENUM_CONVERSION(NoiseMode, NoiseMode::Mono, {NoiseMode::Mono, "mono"}, + {NoiseMode::Duo, "duo"}, {NoiseMode::Multi, "multi"}) + std::string ColorSpaceToString(ColorSpace space) { switch (space) { case ColorSpace::SRGB: diff --git a/src/pagx/utils/StringParser.h b/src/pagx/utils/StringParser.h index eae70c9b8e..c6035ef22d 100644 --- a/src/pagx/utils/StringParser.h +++ b/src/pagx/utils/StringParser.h @@ -40,6 +40,7 @@ #include "pagx/types/LayoutMode.h" #include "pagx/types/Matrix.h" #include "pagx/types/MipmapMode.h" +#include "pagx/types/NoiseMode.h" #include "pagx/types/Padding.h" #include "pagx/types/ScaleMode.h" #include "pagx/types/TextAnchor.h" @@ -196,6 +197,13 @@ std::string ArrangementToString(Arrangement arr); Arrangement ArrangementFromString(const std::string& str); bool IsValidArrangementString(const std::string& str); +//============================================================================== +// NoiseMode +//============================================================================== +std::string NoiseModeToString(NoiseMode mode); +NoiseMode NoiseModeFromString(const std::string& str); +bool IsValidNoiseModeString(const std::string& str); + //============================================================================== // Padding //============================================================================== From 6341deaa13e0986e1f9c77b9cfd2f13382176761 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 8 Jun 2026 16:28:32 +0800 Subject: [PATCH 15/52] Add NoiseFilterWithDropShadow test case with large shadow offset for SVG and webp output. --- test/src/PAGXTest.cpp | 66 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 1fee53e222..2bdaa49751 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7755,4 +7755,70 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { std::ofstream file(outPath, std::ios::binary); file.write(svg.data(), static_cast(svg.size())); } + +/** + * Test that MonoNoiseFilter and DropShadowFilter can coexist on the same layer + * with a large shadow offset that extends beyond the content bounds. Verifies + * that the filter region is correctly expanded to accommodate the shadow. + */ +PAGX_TEST(PAGXTest, NoiseFilterWithDropShadow) { + constexpr int canvasW = 400; + constexpr int canvasH = 400; + auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); + + auto layer = doc->makeNode(); + auto rect = doc->makeNode(); + rect->position = {100, 100}; + rect->size = {120, 120}; + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {0.2f, 0.5f, 0.9f, 1.0f}; + fill->color = solid; + layer->contents.push_back(rect); + layer->contents.push_back(fill); + + auto noise = doc->makeNode(); + noise->mode = pagx::NoiseMode::Mono; + noise->size = 8; + noise->density = 0.6f; + noise->seed = 42; + noise->color = {0.0f, 0.0f, 0.0f, 0.8f}; + layer->filters.push_back(noise); + + auto shadow = doc->makeNode(); + shadow->offsetX = 30; + shadow->offsetY = 40; + shadow->blurX = 10; + shadow->blurY = 10; + shadow->color = {0.0f, 0.0f, 0.0f, 0.6f}; + layer->filters.push_back(shadow); + + doc->layers.push_back(layer); + + doc->applyLayout(); + auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); + ASSERT_TRUE(tgfxLayer != nullptr); + + auto surface = Surface::Make(context, canvasW, canvasH); + ASSERT_TRUE(surface != nullptr); + DisplayList displayList; + displayList.root()->addChild(tgfxLayer); + displayList.render(surface.get(), false); + + EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseFilterWithDropShadow")); + + auto svg = pagx::SVGExporter::ToSVG(*doc); + EXPECT_FALSE(svg.empty()); + EXPECT_NE(svg.find("(svg.size())); +} } // namespace pag From 352e049290134794e42facb8090f91d12dba4b0b Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 8 Jun 2026 16:34:15 +0800 Subject: [PATCH 16/52] Apply code formatting to SVGExporter.cpp. --- src/pagx/svg/SVGExporter.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index 41df19d0c9..9f6d759f3f 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -1548,8 +1548,7 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde return resultName; } -std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleIndex, - const Rect&) { +std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleIndex, const Rect&) { std::string styleId = "noiseStyle" + std::to_string(noiseStyleIndex++); if (noise->mode == NoiseMode::Mono) { From 49f0c101fd9ac40128dc016c2749338a692ef65b Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 8 Jun 2026 16:46:40 +0800 Subject: [PATCH 17/52] Remove NoiseFilterOnText and NoiseFilterWithDropShadow test cases. --- test/src/PAGXTest.cpp | 140 ------------------------------------------ 1 file changed, 140 deletions(-) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 2bdaa49751..fc10da38d4 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7283,80 +7283,6 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { std::ofstream file(outPath, std::ios::binary); file.write(svg.data(), static_cast(svg.size())); } - -/** - * Test that NoiseFilter works correctly on Text content. - */ -PAGX_TEST(PAGXTest, NoiseFilterOnText) { - constexpr int canvasW = 700; - constexpr int canvasH = 300; - auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); - pagx::FontConfig fontConfig; - fontConfig.addFallbackTypefaces(GetFallbackTypefaces()); - - auto layer = doc->makeNode(); - layer->matrix = pagx::Matrix::Translate(50, 120); - auto text = doc->makeNode(); - text->text = "PAGX"; - text->fontSize = 72; - auto fill = doc->makeNode(); - auto solid = doc->makeNode(); - solid->color = {0.2f, 0.6f, 0.9f, 1.0f}; - fill->color = solid; - layer->contents.push_back(text); - layer->contents.push_back(fill); - - auto mono = doc->makeNode(); - mono->mode = pagx::NoiseMode::Mono; - mono->size = 8; - mono->density = 0.5f; - mono->seed = 42; - mono->color = {0.0f, 0.0f, 0.0f, 1.0f}; - layer->filters.push_back(mono); - doc->layers.push_back(layer); - - // Plain text without noise for visual comparison. - auto plainLayer = doc->makeNode(); - plainLayer->matrix = pagx::Matrix::Translate(400, 120); - auto plainText = doc->makeNode(); - plainText->text = "PAGX"; - plainText->fontSize = 72; - auto plainFill = doc->makeNode(); - auto plainSolid = doc->makeNode(); - plainSolid->color = {0.2f, 0.6f, 0.9f, 1.0f}; - plainFill->color = plainSolid; - plainLayer->contents.push_back(plainText); - plainLayer->contents.push_back(plainFill); - doc->layers.push_back(plainLayer); - - doc->applyLayout(&fontConfig); - auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); - ASSERT_TRUE(tgfxLayer != nullptr); - - auto surface = Surface::Make(context, canvasW, canvasH); - ASSERT_TRUE(surface != nullptr); - DisplayList displayList; - displayList.root()->addChild(tgfxLayer); - displayList.render(surface.get(), false); - - EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseFilterOnText")); - - pagx::SVGExportOptions svgOpts; - svgOpts.fontConfig = &fontConfig; - auto svg = pagx::SVGExporter::ToSVG(*doc, svgOpts); - EXPECT_FALSE(svg.empty()); - EXPECT_NE(svg.find("(svg.size())); -} - /** * Test NoiseFilter applied to every supported element type (Rectangle, Ellipse, Path, Polystar, * Text, Group, TextBox) plus Repeater, outputting SVG for visual inspection of contentBounds @@ -7755,70 +7681,4 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { std::ofstream file(outPath, std::ios::binary); file.write(svg.data(), static_cast(svg.size())); } - -/** - * Test that MonoNoiseFilter and DropShadowFilter can coexist on the same layer - * with a large shadow offset that extends beyond the content bounds. Verifies - * that the filter region is correctly expanded to accommodate the shadow. - */ -PAGX_TEST(PAGXTest, NoiseFilterWithDropShadow) { - constexpr int canvasW = 400; - constexpr int canvasH = 400; - auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); - - auto layer = doc->makeNode(); - auto rect = doc->makeNode(); - rect->position = {100, 100}; - rect->size = {120, 120}; - auto fill = doc->makeNode(); - auto solid = doc->makeNode(); - solid->color = {0.2f, 0.5f, 0.9f, 1.0f}; - fill->color = solid; - layer->contents.push_back(rect); - layer->contents.push_back(fill); - - auto noise = doc->makeNode(); - noise->mode = pagx::NoiseMode::Mono; - noise->size = 8; - noise->density = 0.6f; - noise->seed = 42; - noise->color = {0.0f, 0.0f, 0.0f, 0.8f}; - layer->filters.push_back(noise); - - auto shadow = doc->makeNode(); - shadow->offsetX = 30; - shadow->offsetY = 40; - shadow->blurX = 10; - shadow->blurY = 10; - shadow->color = {0.0f, 0.0f, 0.0f, 0.6f}; - layer->filters.push_back(shadow); - - doc->layers.push_back(layer); - - doc->applyLayout(); - auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); - ASSERT_TRUE(tgfxLayer != nullptr); - - auto surface = Surface::Make(context, canvasW, canvasH); - ASSERT_TRUE(surface != nullptr); - DisplayList displayList; - displayList.root()->addChild(tgfxLayer); - displayList.render(surface.get(), false); - - EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseFilterWithDropShadow")); - - auto svg = pagx::SVGExporter::ToSVG(*doc); - EXPECT_FALSE(svg.empty()); - EXPECT_NE(svg.find("(svg.size())); -} } // namespace pag From c50a3b14fb7314ec73adb7fa42d6ed94643dc0ec Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 8 Jun 2026 16:49:00 +0800 Subject: [PATCH 18/52] Accept screenshot baseline for NoiseFilter tests. --- test/baseline/version.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/baseline/version.json b/test/baseline/version.json index d8b0c6b968..6fab15caa3 100644 --- a/test/baseline/version.json +++ b/test/baseline/version.json @@ -8558,7 +8558,8 @@ }, "PAGXTest": { "LayerBuilderAPIConsistency": "f40e23b6", - "NoiseFilterModes": "fa66a3ae", + "NoiseFilterAllElements": "bc58b86e", + "NoiseFilterModes": "bc58b86e", "PrecomposedTextRender": "9b6dea6a", "html": { "blend_modes_showcase": "f94e413d", From 9937c397f15bbf9ca757f66f2bb43f2ac16feb4d Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 8 Jun 2026 17:21:29 +0800 Subject: [PATCH 19/52] Add NoiseStyleWithDropShadow test case for DuoNoiseStyle plus DropShadowFilter SVG and webp output. --- test/src/PAGXTest.cpp | 67 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index fc10da38d4..00a445df32 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7681,4 +7681,71 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { std::ofstream file(outPath, std::ios::binary); file.write(svg.data(), static_cast(svg.size())); } + +/** + * Test DuoNoiseStyle and DropShadowFilter coexisting on a rectangle layer. + * Verifies that noise style (layer style) and drop shadow filter produce + * correct SVG output and rendering. + */ +PAGX_TEST(PAGXTest, NoiseStyleWithDropShadow) { + constexpr int canvasW = 400; + constexpr int canvasH = 400; + auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); + + auto layer = doc->makeNode(); + auto rect = doc->makeNode(); + rect->position = {100, 100}; + rect->size = {120, 120}; + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {0.2f, 0.5f, 0.9f, 1.0f}; + fill->color = solid; + layer->contents.push_back(rect); + layer->contents.push_back(fill); + + auto noiseStyle = doc->makeNode(); + noiseStyle->mode = pagx::NoiseMode::Duo; + noiseStyle->size = 10; + noiseStyle->density = 0.5f; + noiseStyle->seed = 42; + noiseStyle->firstColor = {1.0f, 0.9f, 0.0f, 1.0f}; + noiseStyle->secondColor = {0.0f, 0.3f, 1.0f, 1.0f}; + layer->styles.push_back(noiseStyle); + + auto shadow = doc->makeNode(); + shadow->offsetX = 20; + shadow->offsetY = 25; + shadow->blurX = 8; + shadow->blurY = 8; + shadow->color = {0.0f, 0.0f, 0.0f, 0.5f}; + layer->filters.push_back(shadow); + + doc->layers.push_back(layer); + + doc->applyLayout(); + auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); + ASSERT_TRUE(tgfxLayer != nullptr); + + auto surface = Surface::Make(context, canvasW, canvasH); + ASSERT_TRUE(surface != nullptr); + DisplayList displayList; + displayList.root()->addChild(tgfxLayer); + displayList.render(surface.get(), false); + + EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseStyleWithDropShadow")); + + auto svg = pagx::SVGExporter::ToSVG(*doc); + EXPECT_FALSE(svg.empty()); + EXPECT_NE(svg.find("(svg.size())); +} } // namespace pag From 329567f668c5bc51084cd72e636a76e21e890c15 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 8 Jun 2026 17:24:46 +0800 Subject: [PATCH 20/52] Change test to DuoNoiseFilter plus DropShadowStyle for cross-testing filter-style combinations. --- test/src/PAGXTest.cpp | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 00a445df32..5daed48a79 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7683,11 +7683,11 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { } /** - * Test DuoNoiseStyle and DropShadowFilter coexisting on a rectangle layer. - * Verifies that noise style (layer style) and drop shadow filter produce + * Test DuoNoiseFilter and DropShadowStyle coexisting on a rectangle layer. + * Verifies that noise filter and drop shadow style (layer style) produce * correct SVG output and rendering. */ -PAGX_TEST(PAGXTest, NoiseStyleWithDropShadow) { +PAGX_TEST(PAGXTest, NoiseFilterWithDropShadowStyle) { constexpr int canvasW = 400; constexpr int canvasH = 400; auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); @@ -7703,22 +7703,22 @@ PAGX_TEST(PAGXTest, NoiseStyleWithDropShadow) { layer->contents.push_back(rect); layer->contents.push_back(fill); - auto noiseStyle = doc->makeNode(); - noiseStyle->mode = pagx::NoiseMode::Duo; - noiseStyle->size = 10; - noiseStyle->density = 0.5f; - noiseStyle->seed = 42; - noiseStyle->firstColor = {1.0f, 0.9f, 0.0f, 1.0f}; - noiseStyle->secondColor = {0.0f, 0.3f, 1.0f, 1.0f}; - layer->styles.push_back(noiseStyle); + auto noise = doc->makeNode(); + noise->mode = pagx::NoiseMode::Duo; + noise->size = 10; + noise->density = 0.5f; + noise->seed = 42; + noise->firstColor = {1.0f, 0.9f, 0.0f, 1.0f}; + noise->secondColor = {0.0f, 0.3f, 1.0f, 1.0f}; + layer->filters.push_back(noise); - auto shadow = doc->makeNode(); + auto shadow = doc->makeNode(); shadow->offsetX = 20; shadow->offsetY = 25; shadow->blurX = 8; shadow->blurY = 8; shadow->color = {0.0f, 0.0f, 0.0f, 0.5f}; - layer->filters.push_back(shadow); + layer->styles.push_back(shadow); doc->layers.push_back(layer); @@ -7732,7 +7732,7 @@ PAGX_TEST(PAGXTest, NoiseStyleWithDropShadow) { displayList.root()->addChild(tgfxLayer); displayList.render(surface.get(), false); - EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseStyleWithDropShadow")); + EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseFilterWithDropShadowStyle")); auto svg = pagx::SVGExporter::ToSVG(*doc); EXPECT_FALSE(svg.empty()); @@ -7740,7 +7740,7 @@ PAGX_TEST(PAGXTest, NoiseStyleWithDropShadow) { EXPECT_NE(svg.find("feTurbulence"), std::string::npos); EXPECT_NE(svg.find("feOffset"), std::string::npos); - auto outPath = ProjectPath::Absolute("test/out/PAGXTest/NoiseStyleWithDropShadow.svg"); + auto outPath = ProjectPath::Absolute("test/out/PAGXTest/NoiseFilterWithDropShadowStyle.svg"); auto dirPath = std::filesystem::path(outPath).parent_path(); if (!std::filesystem::exists(dirPath)) { std::filesystem::create_directories(dirPath); From b8b4bbd96e9056c3a614cf0cf80e6812891afcdc Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 8 Jun 2026 18:51:31 +0800 Subject: [PATCH 21/52] Remove contentBounds parameter from SVG filter and style export pipeline. --- src/pagx/svg/SVGExporter.cpp | 311 +++-------------------------------- 1 file changed, 19 insertions(+), 292 deletions(-) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index 9f6d759f3f..b318369e98 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -106,257 +106,6 @@ static std::string ColorToDisplayP3String(const Color& color) { FloatToString(color.blue) + ")"; } -static void ExpandBounds(float& minX, float& minY, float& maxX, float& maxY, const Rect& bounds) { - if (bounds.isEmpty()) { - return; - } - minX = std::min(minX, bounds.x); - minY = std::min(minY, bounds.y); - maxX = std::max(maxX, bounds.x + bounds.width); - maxY = std::max(maxY, bounds.y + bounds.height); -} - -static Rect IntersectRects(const Rect& a, const Rect& b) { - if (a.isEmpty() || b.isEmpty()) { - return {}; - } - float left = std::max(a.x, b.x); - float top = std::max(a.y, b.y); - float right = std::min(a.x + a.width, b.x + b.width); - float bottom = std::min(a.y + a.height, b.y + b.height); - if (left >= right || top >= bottom) { - return {}; - } - return {left, top, right - left, bottom - top}; -} - -static Rect ComputeElementBounds(const Element* element); - -// std::pow(negative, non-integer) returns NaN. Raise magnitude and reapply sign. -static float SignedPow(float base, float exp) { - float sign = base < 0.0f ? -1.0f : 1.0f; - return sign * std::pow(std::abs(base), exp); -} - -// Apply a 2D affine matrix to a Rect, returning the axis-aligned bounding box -// of the transformed corners. -static Rect MapRect(const Matrix& m, const Rect& r) { - if (r.isEmpty()) { - return {}; - } - float x0 = r.x, y0 = r.y; - float x1 = r.x + r.width, y1 = r.y + r.height; - auto tl = m.mapPoint({x0, y0}); - auto tr = m.mapPoint({x1, y0}); - auto bl = m.mapPoint({x0, y1}); - auto br = m.mapPoint({x1, y1}); - float minX = std::min({tl.x, tr.x, bl.x, br.x}); - float minY = std::min({tl.y, tr.y, bl.y, br.y}); - float maxX = std::max({tl.x, tr.x, bl.x, br.x}); - float maxY = std::max({tl.y, tr.y, bl.y, br.y}); - return {minX, minY, maxX - minX, maxY - minY}; -} - -// Build the per-copy transform matrix for a Repeater copy, mirroring the logic -// in ModifierResolver::MakeCopyGroup and BuildGroupMatrix. -static Matrix BuildRepeaterCopyMatrix(const Repeater* rep, float progress) { - float tx = rep->position.x * progress; - float ty = rep->position.y * progress; - float sx = SignedPow(rep->scale.x, progress); - float sy = SignedPow(rep->scale.y, progress); - float rotation = rep->rotation * progress; - Matrix m = {}; - if (!FloatNearlyZero(rep->anchor.x) || !FloatNearlyZero(rep->anchor.y)) { - m = Matrix::Translate(-rep->anchor.x, -rep->anchor.y); - } - if (!FloatNearlyZero(sx - 1.0f) || !FloatNearlyZero(sy - 1.0f)) { - m = Matrix::Scale(sx, sy) * m; - } - if (!FloatNearlyZero(rotation)) { - m = Matrix::Rotate(rotation) * m; - } - m = Matrix::Translate(tx + rep->anchor.x, ty + rep->anchor.y) * m; - return m; -} - -static Rect ComputeLayerBounds(const Layer* layer, int depth = 0); - -static Rect ComputeElementsBounds(const std::vector& elements) { - float minX = std::numeric_limits::max(); - float minY = std::numeric_limits::max(); - float maxX = -std::numeric_limits::max(); - float maxY = -std::numeric_limits::max(); - std::vector preceding; - - for (const auto* element : elements) { - if (element->nodeType() == NodeType::Repeater) { - auto rep = static_cast(element); - if (rep->copies < 0.0f) { - continue; - } - if (rep->copies == 0.0f) { - return {}; - } - if (preceding.empty()) { - continue; - } - auto bodyBounds = ComputeElementsBounds(preceding); - constexpr float MAX_REPEATER_COPIES = 10000.0f; - float copiesF = std::min(rep->copies, MAX_REPEATER_COPIES); - int maxCount = static_cast(std::ceil(copiesF)); - for (int i = 0; i < maxCount; i++) { - float progress = static_cast(i) + rep->offset; - auto copyMatrix = BuildRepeaterCopyMatrix(rep, progress); - auto copyBounds = MapRect(copyMatrix, bodyBounds); - ExpandBounds(minX, minY, maxX, maxY, copyBounds); - } - continue; - } - auto bounds = ComputeElementBounds(element); - if (bounds.width > 0 || bounds.height > 0) { - ExpandBounds(minX, minY, maxX, maxY, bounds); - } - preceding.push_back(const_cast(element)); - } - if (minX > maxX) { - return {}; - } - return {minX, minY, maxX - minX, maxY - minY}; -} - -static Rect ComputeElementBounds(const Element* element) { - switch (element->nodeType()) { - case NodeType::Rectangle: { - auto rect = static_cast(element); - auto pos = rect->renderPosition(); - auto size = rect->renderSize(); - return {pos.x - size.width * 0.5f, pos.y - size.height * 0.5f, size.width, size.height}; - } - case NodeType::Ellipse: { - auto ellipse = static_cast(element); - auto pos = ellipse->renderPosition(); - auto size = ellipse->renderSize(); - return {pos.x - size.width * 0.5f, pos.y - size.height * 0.5f, size.width, size.height}; - } - case NodeType::Path: { - auto pathNode = static_cast(element); - if (pathNode->data == nullptr || pathNode->data->isEmpty()) { - return {}; - } - auto bounds = pathNode->data->getBounds(); - auto scale = pathNode->renderScale(); - bounds.x *= scale; - bounds.y *= scale; - bounds.width *= scale; - bounds.height *= scale; - auto pos = pathNode->renderPosition(); - return {pos.x + bounds.x, pos.y + bounds.y, bounds.width, bounds.height}; - } - case NodeType::Polystar: { - auto polystar = static_cast(element); - auto pos = polystar->renderPosition(); - auto outerRadius = polystar->renderOuterRadius(); - return {pos.x - outerRadius, pos.y - outerRadius, outerRadius * 2, outerRadius * 2}; - } - case NodeType::Group: { - auto group = static_cast(element); - auto childBounds = ComputeElementsBounds(group->elements); - auto groupMatrix = BuildGroupMatrix(group); - if (groupMatrix.isIdentity()) { - return childBounds; - } - return MapRect(groupMatrix, childBounds); - } - case NodeType::Text: { - auto text = static_cast(element); - return text->contentBounds(); - } - case NodeType::TextPath: { - auto textPath = static_cast(element); - if (textPath->path == nullptr || textPath->path->isEmpty()) { - return textPath->layoutBounds(); - } - auto bounds = textPath->path->getBounds(); - auto scale = textPath->renderScale(); - bounds.x *= scale; - bounds.y *= scale; - bounds.width *= scale; - bounds.height *= scale; - auto pos = textPath->renderPosition(); - return {pos.x + bounds.x, pos.y + bounds.y, bounds.width, bounds.height}; - } - case NodeType::TextBox: { - auto textBox = static_cast(element); - return textBox->contentBounds(); - } - default: - return {}; - } -} - -static constexpr int MAX_LAYER_BOUNDS_DEPTH = 32; - -static Rect ComputeLayerBounds(const Layer* layer, int depth) { - if (depth >= MAX_LAYER_BOUNDS_DEPTH) { - return {}; - } - - float minX = std::numeric_limits::max(); - float minY = std::numeric_limits::max(); - float maxX = -std::numeric_limits::max(); - float maxY = -std::numeric_limits::max(); - - auto contentsBounds = ComputeElementsBounds(layer->contents); - ExpandBounds(minX, minY, maxX, maxY, contentsBounds); - - if (layer->composition != nullptr) { - for (const auto* compLayer : layer->composition->layers) { - if (!compLayer->visible) continue; - auto compBounds = ComputeLayerBounds(compLayer, depth + 1); - auto compMatrix = BuildLayerMatrix(compLayer); - auto mappedBounds = MapRect(compMatrix, compBounds); - if (compLayer->hasScrollRect) { - auto scrollRect = MapRect(compMatrix, compLayer->scrollRect); - mappedBounds = IntersectRects(mappedBounds, scrollRect); - if (mappedBounds.isEmpty()) continue; - } - if (compLayer->mask != nullptr) { - auto maskBounds = ComputeLayerBounds(compLayer->mask, depth + 1); - auto maskMatrix = BuildLayerMatrix(compLayer->mask); - auto mappedMask = MapRect(maskMatrix, maskBounds); - mappedBounds = IntersectRects(mappedBounds, mappedMask); - if (mappedBounds.isEmpty()) continue; - } - ExpandBounds(minX, minY, maxX, maxY, mappedBounds); - } - } - - for (const auto* child : layer->children) { - if (!child->visible) continue; - auto childBounds = ComputeLayerBounds(child, depth + 1); - auto childMatrix = BuildLayerMatrix(child); - auto mappedBounds = MapRect(childMatrix, childBounds); - if (child->hasScrollRect) { - auto scrollRect = MapRect(childMatrix, child->scrollRect); - mappedBounds = IntersectRects(mappedBounds, scrollRect); - if (mappedBounds.isEmpty()) continue; - } - if (child->mask != nullptr) { - auto maskBounds = ComputeLayerBounds(child->mask, depth + 1); - auto maskMatrix = BuildLayerMatrix(child->mask); - auto mappedMask = MapRect(maskMatrix, maskBounds); - mappedBounds = IntersectRects(mappedBounds, mappedMask); - if (mappedBounds.isEmpty()) continue; - } - ExpandBounds(minX, minY, maxX, maxY, mappedBounds); - } - - if (minX > maxX) { - return {}; - } - return {minX, minY, maxX - minX, maxY - minY}; -} - // feGaussianBlur stdDeviation string: one value when blurX == blurY, otherwise two. // Compare via the formatted strings so ULP-level differences from upstream transform // scaling don't emit redundant anisotropic stdDeviation that browsers would honour. @@ -664,7 +413,7 @@ class SVGWriter { std::string writeNoiseBand(const NoiseFilter* noise, bool isDark, const std::string& label); std::string writeNoiseBand(const NoiseStyle* noise, bool isDark, const std::string& label); std::string writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, - std::string& currentSource, const Rect& contentBounds); + std::string& currentSource); // Collected per-filter state fed into the final feMerge aggregation. struct ShadowAggregate { std::vector dropShadowResults; @@ -672,16 +421,14 @@ class SVGWriter { std::vector innerShadowResults; bool needSourceGraphic = false; }; - std::string writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleIndex, - const Rect& contentBounds); + std::string writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleIndex); void writeFilterList(const std::vector& filters, int& shadowIndex, - ShadowAggregate& agg, std::string& currentSource, const Rect& contentBounds); + ShadowAggregate& agg, std::string& currentSource); void writeStyleList(const std::vector& styles, int& shadowIndex, - ShadowAggregate& agg, const Rect& contentBounds); + ShadowAggregate& agg); void writeShadowMerge(const ShadowAggregate& agg, const std::string& currentSource); std::string writeFilterAndStyleDefs(const std::vector& filters, - const std::vector& styles, - const Rect& contentBounds = {}); + const std::vector& styles); // Mask / clip-path defs using ContentWriter = void (SVGWriter::*)(SVGBuilder&, const Layer*); @@ -1344,7 +1091,7 @@ std::string SVGWriter::writeNoiseBand(const NoiseStyle* noise, bool isDark, } std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, - std::string& currentSource, const Rect&) { + std::string& currentSource) { std::string filterId = "noise" + std::to_string(noiseIndex++); if (noise->mode == NoiseMode::Mono) { @@ -1548,7 +1295,7 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde return resultName; } -std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleIndex, const Rect&) { +std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleIndex) { std::string styleId = "noiseStyle" + std::to_string(noiseStyleIndex++); if (noise->mode == NoiseMode::Mono) { @@ -1719,8 +1466,7 @@ std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleI } void SVGWriter::writeFilterList(const std::vector& filters, int& shadowIndex, - ShadowAggregate& agg, std::string& currentSource, - const Rect& contentBounds) { + ShadowAggregate& agg, std::string& currentSource) { int colorMatrixIndex = 0; int blurIndex = 0; int noiseIndex = 0; @@ -1765,8 +1511,7 @@ void SVGWriter::writeFilterList(const std::vector& filters, int& s writeBlendFilter(static_cast(filter), shadowIndex, currentSource); break; case NodeType::NoiseFilter: - writeNoiseFilter(static_cast(filter), noiseIndex, currentSource, - contentBounds); + writeNoiseFilter(static_cast(filter), noiseIndex, currentSource); break; default: break; @@ -1780,7 +1525,7 @@ void SVGWriter::writeFilterList(const std::vector& filters, int& s // modern renderers). NoiseStyle emits its SVG primitives and adds the result name to // agg.aboveResults so it composites above the source in the final feMerge. void SVGWriter::writeStyleList(const std::vector& styles, int& shadowIndex, - ShadowAggregate& agg, const Rect& contentBounds) { + ShadowAggregate& agg) { int noiseStyleIndex = 0; for (const auto* style : styles) { switch (style->nodeType()) { @@ -1804,7 +1549,7 @@ void SVGWriter::writeStyleList(const std::vector& styles, int& shad } case NodeType::NoiseStyle: { auto noise = static_cast(style); - auto result = writeNoiseStyle(noise, noiseStyleIndex, contentBounds); + auto result = writeNoiseStyle(noise, noiseStyleIndex); agg.aboveResults.push_back(result); agg.needSourceGraphic = true; break; @@ -1862,8 +1607,7 @@ void SVGWriter::writeShadowMerge(const ShadowAggregate& agg, const std::string& } std::string SVGWriter::writeFilterAndStyleDefs(const std::vector& filters, - const std::vector& styles, - const Rect& contentBounds) { + const std::vector& styles) { if (filters.empty() && styles.empty()) { return {}; } @@ -1945,34 +1689,18 @@ std::string SVGWriter::writeFilterAndStyleDefs(const std::vector& std::string filterId = generateId("filter"); _defs->openElement("filter"); _defs->addAttribute("id", filterId); - if (!contentBounds.isEmpty()) { - // Use filterUnits="userSpaceOnUse" with absolute pixel coordinates for filters - // with known content bounds. This decouples the filter region from the element's - // bounding box so effects like feTurbulence sample at absolute coordinates, - // producing position-dependent noise phase that matches tgfx behavior. - float filterX = contentBounds.x - marginLeft; - float filterY = contentBounds.y - marginTop; - float filterW = contentBounds.width + marginLeft + marginRight; - float filterH = contentBounds.height + marginTop + marginBottom; - _defs->addAttribute("filterUnits", "userSpaceOnUse"); - _defs->addAttribute("x", FloatToString(filterX)); - _defs->addAttribute("y", FloatToString(filterY)); - _defs->addAttribute("width", FloatToString(filterW)); - _defs->addAttribute("height", FloatToString(filterH)); - } else { - _defs->addAttribute("x", "-" + FloatToString(pctLeft) + "%"); - _defs->addAttribute("y", "-" + FloatToString(pctTop) + "%"); - _defs->addAttribute("width", FloatToString(100.0f + pctLeft + pctRight) + "%"); - _defs->addAttribute("height", FloatToString(100.0f + pctTop + pctBottom) + "%"); - } + _defs->addAttribute("x", "-" + FloatToString(pctLeft) + "%"); + _defs->addAttribute("y", "-" + FloatToString(pctTop) + "%"); + _defs->addAttribute("width", FloatToString(100.0f + pctLeft + pctRight) + "%"); + _defs->addAttribute("height", FloatToString(100.0f + pctTop + pctBottom) + "%"); _defs->addAttribute("color-interpolation-filters", "sRGB"); _defs->closeElementStart(); int shadowIndex = 0; ShadowAggregate agg; std::string currentSource = "SourceGraphic"; - writeFilterList(filters, shadowIndex, agg, currentSource, contentBounds); - writeStyleList(styles, shadowIndex, agg, contentBounds); + writeFilterList(filters, shadowIndex, agg, currentSource); + writeStyleList(styles, shadowIndex, agg); writeShadowMerge(agg, currentSource); _defs->closeElement(); // @@ -3351,8 +3079,7 @@ void SVGWriter::writeLayerGroupAttributes(SVGBuilder& out, const Layer* layer, } if (!layer->filters.empty() || !layer->styles.empty()) { - auto contentBounds = ComputeLayerBounds(layer); - auto filterId = writeFilterAndStyleDefs(layer->filters, layer->styles, contentBounds); + auto filterId = writeFilterAndStyleDefs(layer->filters, layer->styles); if (!filterId.empty()) { out.addAttribute("filter", "url(#" + filterId + ")"); } From fa105e2d7fcfe3fdd0b88f47dc3725451020ac9c Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 8 Jun 2026 20:18:16 +0800 Subject: [PATCH 22/52] Update baseline versions for noise filter tests after content simplification. --- test/baseline/version.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/baseline/version.json b/test/baseline/version.json index 6fab15caa3..d3b581b2c4 100644 --- a/test/baseline/version.json +++ b/test/baseline/version.json @@ -8558,8 +8558,9 @@ }, "PAGXTest": { "LayerBuilderAPIConsistency": "f40e23b6", - "NoiseFilterAllElements": "bc58b86e", - "NoiseFilterModes": "bc58b86e", + "NoiseFilterAllElements": "798f40c4", + "NoiseFilterModes": "798f40c4", + "NoiseFilterWithDropShadowStyle": "798f40c4", "PrecomposedTextRender": "9b6dea6a", "html": { "blend_modes_showcase": "f94e413d", From 8cbf050bfc6402d817a60ee326884d4369ad6f90 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 8 Jun 2026 20:20:35 +0800 Subject: [PATCH 23/52] Remove NoiseFilterWithDropShadowStyle test case and its baseline entry. --- test/baseline/version.json | 1 - test/src/PAGXTest.cpp | 321 +------------------------------------ 2 files changed, 5 insertions(+), 317 deletions(-) diff --git a/test/baseline/version.json b/test/baseline/version.json index d3b581b2c4..8cc3a1b431 100644 --- a/test/baseline/version.json +++ b/test/baseline/version.json @@ -8560,7 +8560,6 @@ "LayerBuilderAPIConsistency": "f40e23b6", "NoiseFilterAllElements": "798f40c4", "NoiseFilterModes": "798f40c4", - "NoiseFilterWithDropShadowStyle": "798f40c4", "PrecomposedTextRender": "9b6dea6a", "html": { "blend_modes_showcase": "f94e413d", diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 5daed48a79..ff1b50b4ba 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7126,7 +7126,7 @@ PAGX_TEST(PAGXTest, HitTestGlobalMatrix) { */ PAGX_TEST(PAGXTest, NoiseFilterModes) { constexpr int canvasW = 400; - constexpr int canvasH = 470; + constexpr int canvasH = 150; auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); auto makeLayer = [&](float x, float y) { @@ -7144,7 +7144,7 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { return layer; }; - auto layer1 = makeLayer(20, 50); + auto layer1 = makeLayer(20, 0); auto mono = doc->makeNode(); mono->mode = pagx::NoiseMode::Mono; mono->size = 8; @@ -7154,7 +7154,7 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { layer1->filters.push_back(mono); doc->layers.push_back(layer1); - auto layer2 = makeLayer(150, 50); + auto layer2 = makeLayer(150, 0); auto duo = doc->makeNode(); duo->mode = pagx::NoiseMode::Duo; duo->size = 8; @@ -7165,7 +7165,7 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { layer2->filters.push_back(duo); doc->layers.push_back(layer2); - auto layer3 = makeLayer(280, 50); + auto layer3 = makeLayer(280, 0); auto multi = doc->makeNode(); multi->mode = pagx::NoiseMode::Multi; multi->size = 8; @@ -7175,89 +7175,6 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { layer3->filters.push_back(multi); doc->layers.push_back(layer3); - auto makePathLayer = [&](float x, float y) { - auto layer = doc->makeNode(); - auto path = doc->makeNode(); - path->data = doc->makeNode(); - path->data->moveTo(0, 0); - path->data->lineTo(100, 0); - path->data->lineTo(100, 100); - path->data->lineTo(0, 100); - path->data->close(); - path->position = {x, y}; - auto fill = doc->makeNode(); - auto solid = doc->makeNode(); - solid->color = {0.2f, 0.5f, 0.8f, 1.0f}; - fill->color = solid; - layer->contents.push_back(path); - layer->contents.push_back(fill); - return layer; - }; - - auto layer4 = makePathLayer(20, 170); - auto mono2 = doc->makeNode(); - mono2->mode = pagx::NoiseMode::Mono; - mono2->size = 8; - mono2->density = 0.5f; - mono2->seed = 42; - mono2->color = {0.0f, 0.0f, 0.0f, 1.0f}; - layer4->filters.push_back(mono2); - doc->layers.push_back(layer4); - - auto layer5 = makePathLayer(150, 170); - auto duo2 = doc->makeNode(); - duo2->mode = pagx::NoiseMode::Duo; - duo2->size = 8; - duo2->density = 0.5f; - duo2->seed = 42; - duo2->firstColor = {1.0f, 1.0f, 0.0f, 1.0f}; - duo2->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; - layer5->filters.push_back(duo2); - doc->layers.push_back(layer5); - - auto layer6 = makePathLayer(280, 170); - auto multi2 = doc->makeNode(); - multi2->mode = pagx::NoiseMode::Multi; - multi2->size = 8; - multi2->density = 0.5f; - multi2->seed = 42; - multi2->opacity = 1.0f; - layer6->filters.push_back(multi2); - doc->layers.push_back(layer6); - - // Row 3: Same rectangle settings as row 1, but use layer matrix for displacement. - // This tests contentBounds in layer local coords starting at (0,0). - auto layer7 = makeLayer(20, 290); - auto mono3 = doc->makeNode(); - mono3->mode = pagx::NoiseMode::Mono; - mono3->size = 8; - mono3->density = 0.5f; - mono3->seed = 42; - mono3->color = {0.0f, 0.0f, 0.0f, 1.0f}; - layer7->filters.push_back(mono3); - doc->layers.push_back(layer7); - - auto layer8 = makeLayer(150, 290); - auto duo3 = doc->makeNode(); - duo3->mode = pagx::NoiseMode::Duo; - duo3->size = 8; - duo3->density = 0.5f; - duo3->seed = 42; - duo3->firstColor = {1.0f, 1.0f, 0.0f, 1.0f}; - duo3->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; - layer8->filters.push_back(duo3); - doc->layers.push_back(layer8); - - auto layer9 = makeLayer(280, 290); - auto multi3 = doc->makeNode(); - multi3->mode = pagx::NoiseMode::Multi; - multi3->size = 8; - multi3->density = 0.5f; - multi3->seed = 42; - multi3->opacity = 1.0f; - layer9->filters.push_back(multi3); - doc->layers.push_back(layer9); - doc->applyLayout(); auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); ASSERT_TRUE(tgfxLayer != nullptr); @@ -7290,7 +7207,7 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { */ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { constexpr int canvasW = 800; - constexpr int canvasH = 900; + constexpr int canvasH = 470; auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); pagx::FontConfig fontConfig; fontConfig.addFallbackTypefaces(GetFallbackTypefaces()); @@ -7489,173 +7406,6 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { doc->layers.push_back(layer); } - // Second group: same elements inside a parent layer shifted via path. - { - auto parentLayer = doc->makeNode(); - parentLayer->y = 440; - - { - auto layer = doc->makeNode(); - layer->x = 20; - layer->y = 20; - - auto rect = doc->makeNode(); - rect->position = {60, 60}; - rect->size = {100, 100}; - layer->contents.push_back(rect); - layer->contents.push_back(makeFill(0.2f, 0.5f, 0.8f)); - layer->filters.push_back(makeMonoNoise(0.0f)); - parentLayer->children.push_back(layer); - } - { - auto layer = doc->makeNode(); - layer->x = 150; - layer->y = 20; - - auto ellipse = doc->makeNode(); - ellipse->position = {60, 60}; - ellipse->size = {100, 100}; - layer->contents.push_back(ellipse); - layer->contents.push_back(makeFill(0.8f, 0.3f, 0.3f)); - layer->filters.push_back(makeDuoNoise(0.25f)); - parentLayer->children.push_back(layer); - } - { - auto layer = doc->makeNode(); - layer->x = 280; - layer->y = 20; - - auto path = doc->makeNode(); - path->data = doc->makeNode(); - path->data->moveTo(0, 0); - path->data->lineTo(100, 0); - path->data->lineTo(50, 100); - path->data->close(); - path->position = {10, 10}; - layer->contents.push_back(path); - layer->contents.push_back(makeFill(0.3f, 0.7f, 0.3f)); - layer->filters.push_back(makeMultiNoise(0.5f)); - parentLayer->children.push_back(layer); - } - - // Row 2: Polystar, Text, Group - { - auto layer = doc->makeNode(); - layer->x = 20; - layer->y = 160; - - auto polystar = doc->makeNode(); - polystar->position = {60, 60}; - polystar->outerRadius = 50; - polystar->innerRadius = 25; - polystar->pointCount = 5; - layer->contents.push_back(polystar); - layer->contents.push_back(makeFill(0.7f, 0.5f, 0.9f)); - layer->filters.push_back(makeMonoNoise(0.75f)); - parentLayer->children.push_back(layer); - } - { - auto layer = doc->makeNode(); - layer->x = 150; - layer->y = 160; - - auto text = doc->makeNode(); - text->text = "Hi"; - text->fontFamily = fontFamily; - text->fontStyle = fontStyle; - text->fontSize = 60; - layer->contents.push_back(text); - layer->contents.push_back(makeFill(0.2f, 0.6f, 0.9f)); - layer->filters.push_back(makeDuoNoise(1.0f)); - parentLayer->children.push_back(layer); - } - { - auto layer = doc->makeNode(); - layer->x = 280; - layer->y = 160; - - auto group = doc->makeNode(); - group->position = {10, 10}; - auto rect = doc->makeNode(); - rect->position = {40, 40}; - rect->size = {60, 60}; - group->elements.push_back(rect); - group->elements.push_back(makeFill(0.9f, 0.6f, 0.1f)); - layer->contents.push_back(group); - layer->filters.push_back(makeMultiNoise(0.5f)); - parentLayer->children.push_back(layer); - } - - // Row 3: TextBox, Repeater - { - auto layer = doc->makeNode(); - layer->x = 20; - layer->y = 320; - - auto textBox = doc->makeNode(); - textBox->width = 120; - textBox->height = 100; - auto text = doc->makeNode(); - text->text = "AB CD"; - text->fontFamily = fontFamily; - text->fontStyle = fontStyle; - text->fontSize = 30; - textBox->elements.push_back(text); - textBox->elements.push_back(makeFill(0.1f, 0.4f, 0.7f)); - layer->contents.push_back(textBox); - layer->filters.push_back(makeDuoNoise(0.5f)); - parentLayer->children.push_back(layer); - } - { - auto layer = doc->makeNode(); - layer->x = 200; - layer->y = 320; - - auto rect = doc->makeNode(); - rect->position = {30, 30}; - rect->size = {40, 40}; - auto repeater = doc->makeNode(); - repeater->copies = 3; - repeater->offset = 0; - repeater->position = {50, 0}; - layer->contents.push_back(rect); - layer->contents.push_back(makeFill(0.3f, 0.8f, 0.6f)); - layer->contents.push_back(repeater); - layer->filters.push_back(makeMultiNoise(0.5f)); - parentLayer->children.push_back(layer); - } - - // Row 4: Off-center content - { - auto layer = doc->makeNode(); - layer->x = 440; - layer->y = 20; - - auto rect = doc->makeNode(); - rect->position = {80, 40}; - rect->size = {60, 60}; - layer->contents.push_back(rect); - layer->contents.push_back(makeFill(0.6f, 0.2f, 0.8f)); - layer->filters.push_back(makeMonoNoise(0.5f)); - parentLayer->children.push_back(layer); - } - { - auto layer = doc->makeNode(); - layer->x = 580; - layer->y = 20; - - auto ellipse = doc->makeNode(); - ellipse->position = {40, 80}; - ellipse->size = {60, 80}; - layer->contents.push_back(ellipse); - layer->contents.push_back(makeFill(0.8f, 0.7f, 0.2f)); - layer->filters.push_back(makeDuoNoise(0.5f)); - parentLayer->children.push_back(layer); - } - - doc->layers.push_back(parentLayer); - } - doc->applyLayout(&fontConfig); auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); ASSERT_TRUE(tgfxLayer != nullptr); @@ -7687,65 +7437,4 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { * Verifies that noise filter and drop shadow style (layer style) produce * correct SVG output and rendering. */ -PAGX_TEST(PAGXTest, NoiseFilterWithDropShadowStyle) { - constexpr int canvasW = 400; - constexpr int canvasH = 400; - auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); - - auto layer = doc->makeNode(); - auto rect = doc->makeNode(); - rect->position = {100, 100}; - rect->size = {120, 120}; - auto fill = doc->makeNode(); - auto solid = doc->makeNode(); - solid->color = {0.2f, 0.5f, 0.9f, 1.0f}; - fill->color = solid; - layer->contents.push_back(rect); - layer->contents.push_back(fill); - - auto noise = doc->makeNode(); - noise->mode = pagx::NoiseMode::Duo; - noise->size = 10; - noise->density = 0.5f; - noise->seed = 42; - noise->firstColor = {1.0f, 0.9f, 0.0f, 1.0f}; - noise->secondColor = {0.0f, 0.3f, 1.0f, 1.0f}; - layer->filters.push_back(noise); - - auto shadow = doc->makeNode(); - shadow->offsetX = 20; - shadow->offsetY = 25; - shadow->blurX = 8; - shadow->blurY = 8; - shadow->color = {0.0f, 0.0f, 0.0f, 0.5f}; - layer->styles.push_back(shadow); - - doc->layers.push_back(layer); - - doc->applyLayout(); - auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); - ASSERT_TRUE(tgfxLayer != nullptr); - - auto surface = Surface::Make(context, canvasW, canvasH); - ASSERT_TRUE(surface != nullptr); - DisplayList displayList; - displayList.root()->addChild(tgfxLayer); - displayList.render(surface.get(), false); - - EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseFilterWithDropShadowStyle")); - - auto svg = pagx::SVGExporter::ToSVG(*doc); - EXPECT_FALSE(svg.empty()); - EXPECT_NE(svg.find("(svg.size())); -} } // namespace pag From b54e98ea0488f4c85a10a519c55286251fb3b62e Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 8 Jun 2026 20:49:36 +0800 Subject: [PATCH 24/52] Fix rebase conflict resolution issues in PAGXTest.cpp. --- test/src/PAGXTest.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index ff1b50b4ba..8127f4d688 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7122,6 +7122,9 @@ PAGX_TEST(PAGXTest, HitTestGlobalMatrix) { EXPECT_FLOAT_EQ(matrix.d, 2.0f); EXPECT_FLOAT_EQ(matrix.tx, 70.0f); EXPECT_FLOAT_EQ(matrix.ty, 45.0f); +} + +/** * Test rendering with Mono, Duo, and Multi noise filters side by side. */ PAGX_TEST(PAGXTest, NoiseFilterModes) { @@ -7432,9 +7435,4 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { file.write(svg.data(), static_cast(svg.size())); } -/** - * Test DuoNoiseFilter and DropShadowStyle coexisting on a rectangle layer. - * Verifies that noise filter and drop shadow style (layer style) produce - * correct SVG output and rendering. - */ } // namespace pag From 4102d81292f831edc64ef277749267e7bd76ed1d Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Tue, 9 Jun 2026 11:15:10 +0800 Subject: [PATCH 25/52] Add NoiseStyleModes test covering Mono, Duo and Multi noise styles for codecov coverage. --- test/src/PAGXTest.cpp | 81 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 8127f4d688..0519a0cd58 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7435,4 +7435,85 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { file.write(svg.data(), static_cast(svg.size())); } +/** + * Test rendering with Mono, Duo, and Multi noise styles side by side. + * Covers writeNoiseStyle for all three modes. + */ +PAGX_TEST(PAGXTest, NoiseStyleModes) { + constexpr int canvasW = 400; + constexpr int canvasH = 180; + auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); + + auto makeLayer = [&](float x, float y) { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(x, y); + auto rect = doc->makeNode(); + rect->position = {0, 0}; + rect->size = {100, 100}; + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {0.8f, 0.8f, 0.8f, 1.0f}; + fill->color = solid; + layer->contents.push_back(rect); + layer->contents.push_back(fill); + return layer; + }; + + auto layer1 = makeLayer(20, 40); + auto mono = doc->makeNode(); + mono->mode = pagx::NoiseMode::Mono; + mono->size = 8; + mono->density = 0.5f; + mono->seed = 42; + mono->color = {0.0f, 0.0f, 0.0f, 1.0f}; + layer1->styles.push_back(mono); + doc->layers.push_back(layer1); + + auto layer2 = makeLayer(150, 40); + auto duo = doc->makeNode(); + duo->mode = pagx::NoiseMode::Duo; + duo->size = 8; + duo->density = 0.5f; + duo->seed = 42; + duo->firstColor = {1.0f, 1.0f, 0.0f, 1.0f}; + duo->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; + layer2->styles.push_back(duo); + doc->layers.push_back(layer2); + + auto layer3 = makeLayer(280, 40); + auto multi = doc->makeNode(); + multi->mode = pagx::NoiseMode::Multi; + multi->size = 8; + multi->density = 0.5f; + multi->seed = 42; + multi->opacity = 1.0f; + layer3->styles.push_back(multi); + doc->layers.push_back(layer3); + + doc->applyLayout(); + auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); + ASSERT_TRUE(tgfxLayer != nullptr); + + auto surface = Surface::Make(context, canvasW, canvasH); + ASSERT_TRUE(surface != nullptr); + DisplayList displayList; + displayList.root()->addChild(tgfxLayer); + displayList.render(surface.get(), false); + + EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseStyleModes")); + + auto svg = pagx::SVGExporter::ToSVG(*doc); + EXPECT_FALSE(svg.empty()); + EXPECT_NE(svg.find("(svg.size())); +} + } // namespace pag From ab4941c5ff472cb718e745bc8aafae12a73eb238 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Tue, 9 Jun 2026 11:23:37 +0800 Subject: [PATCH 26/52] Fix writeShadowMerge skipping feMerge when only NoiseStyle exists without shadows. --- src/pagx/svg/SVGExporter.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index b318369e98..c4bc1f15fc 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -1562,12 +1562,12 @@ void SVGWriter::writeStyleList(const std::vector& styles, int& shad void SVGWriter::writeShadowMerge(const ShadowAggregate& agg, const std::string& currentSource) { bool hasShadows = !agg.dropShadowResults.empty() || !agg.innerShadowResults.empty(); - if (!hasShadows) { - // No shadows but upstream filters may have reshaped the source (Blur, - // ColorMatrix, Blend). In that case the chain already terminates in - // `currentSource`, which is the default implicit output — nothing to - // merge. Only when no image-transforming filter ran does currentSource - // remain "SourceGraphic", and the would have been empty anyway. + bool hasAbove = !agg.aboveResults.empty(); + if (!hasShadows && !hasAbove) { + // No shadows and no above-layer styles (e.g. NoiseStyle). Upstream filters + // may have reshaped the source (Blur, ColorMatrix, Blend). In that case the + // chain already terminates in `currentSource`, which is the default implicit + // output — nothing to merge. return; } bool multipleShadows = (agg.dropShadowResults.size() + agg.innerShadowResults.size()) > 1; From da167f51fd031b892abc9d3abb9dd44ac8091846 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Tue, 9 Jun 2026 11:29:39 +0800 Subject: [PATCH 27/52] Accept screenshot baseline for NoiseStyleModes test. --- test/baseline/version.json | 1 + 1 file changed, 1 insertion(+) diff --git a/test/baseline/version.json b/test/baseline/version.json index 8cc3a1b431..0fb818ec5b 100644 --- a/test/baseline/version.json +++ b/test/baseline/version.json @@ -8560,6 +8560,7 @@ "LayerBuilderAPIConsistency": "f40e23b6", "NoiseFilterAllElements": "798f40c4", "NoiseFilterModes": "798f40c4", + "NoiseStyleModes": "1af07597", "PrecomposedTextRender": "9b6dea6a", "html": { "blend_modes_showcase": "f94e413d", From eacb6e1d7789f0e5f9a7430a570c501e0b6e70fd Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Tue, 9 Jun 2026 11:46:49 +0800 Subject: [PATCH 28/52] Add section separator for noise filter primitives in SVGExporter. --- src/pagx/svg/SVGExporter.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index c4bc1f15fc..4e60c23597 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -984,6 +984,10 @@ void SVGWriter::writeBlendFilter(const BlendFilter* blend, int& shadowIndex, currentSource = blendOut; } +//============================================================================== +// SVGWriter – noise filter primitives +//============================================================================== + std::string SVGWriter::writeNoiseTurbulence(const NoiseFilter* noise, const std::string& resultName) { auto freq = noise->size > 0.0f ? 1.0f / noise->size : 0.25f; From 8a03fbdf6455d1590086163c590febb9e5b45c62 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Tue, 9 Jun 2026 13:04:35 +0800 Subject: [PATCH 29/52] Fix merged separator lines in PAGXTest.cpp to match origin/main format. --- test/src/PAGXTest.cpp | 225 +++++++++++++++++++++++++----------------- 1 file changed, 135 insertions(+), 90 deletions(-) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 0519a0cd58..27b8578e6e 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -1212,8 +1212,9 @@ PAGX_TEST(PAGXTest, CustomDataKeyValidation) { EXPECT_EQ(doc->layers[0]->customData.count("has_underscore"), 0u); } -// ==============================================================================// Auto Layout - Container Layout - Basic -// ============================================================================== +// ===================================================================================== +// Auto Layout - Container Layout - Basic +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutHorizontalEqualWidth) { auto doc = pagx::PAGXDocument::Make(920, 200); auto parent = doc->makeNode(); @@ -1346,8 +1347,9 @@ PAGX_TEST(PAGXTest, LayoutAlignmentCenter) { EXPECT_EQ(child->renderPosition().y, 125.0f); } -// ==============================================================================// Auto Layout - Container Layout - Arrangement End -// ============================================================================== +// ===================================================================================== +// Auto Layout - Container Layout - Arrangement End +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutArrangementEnd) { auto doc = pagx::PAGXDocument::Make(400, 100); auto parent = doc->makeNode(); @@ -1374,8 +1376,9 @@ PAGX_TEST(PAGXTest, LayoutArrangementEnd) { EXPECT_EQ(child2->renderPosition().x, 350.0f); } -// ==============================================================================// Auto Layout - Container Layout - Alignment End -// ============================================================================== +// ===================================================================================== +// Auto Layout - Container Layout - Alignment End +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutAlignmentEnd) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -1397,8 +1400,9 @@ PAGX_TEST(PAGXTest, LayoutAlignmentEnd) { EXPECT_EQ(child->renderPosition().y, 250.0f); } -// ==============================================================================// Auto Layout - Container Layout - Padding -// ============================================================================== +// ===================================================================================== +// Auto Layout - Container Layout - Padding +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutPadding) { auto doc = pagx::PAGXDocument::Make(400, 200); auto parent = doc->makeNode(); @@ -1449,8 +1453,9 @@ PAGX_TEST(PAGXTest, LayoutPaddingWithFlex) { EXPECT_EQ(child2->renderPosition().x, 200.0f); } -// ==============================================================================// Auto Layout - Container Layout - Flex Distribution -// ============================================================================== +// ===================================================================================== +// Auto Layout - Container Layout - Flex Distribution +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutFlexEqualDistribution) { auto doc = pagx::PAGXDocument::Make(300, 100); auto parent = doc->makeNode(); @@ -1656,8 +1661,9 @@ PAGX_TEST(PAGXTest, LayoutFlexZeroNotExported) { EXPECT_EQ(xml.find("flex="), std::string::npos); } -// ==============================================================================// Auto Layout - Container Layout - Visibility -// ============================================================================== +// ===================================================================================== +// Auto Layout - Container Layout - Visibility +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutHiddenChildNotSkipped) { auto doc = pagx::PAGXDocument::Make(300, 100); auto parent = doc->makeNode(); @@ -1689,8 +1695,9 @@ PAGX_TEST(PAGXTest, LayoutHiddenChildNotSkipped) { EXPECT_EQ(child3->renderPosition().x, 200.0f); } -// ==============================================================================// Auto Layout - Container Layout - Edge Cases -// ============================================================================== +// ===================================================================================== +// Auto Layout - Container Layout - Edge Cases +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutEmptyContainer) { auto doc = pagx::PAGXDocument::Make(400, 200); auto parent = doc->makeNode(); @@ -1764,8 +1771,9 @@ PAGX_TEST(PAGXTest, LayoutOverflow) { EXPECT_EQ(child3->renderPosition().x, 200.0f); } -// ==============================================================================// Auto Layout - Container Layout - Nested -// ============================================================================== +// ===================================================================================== +// Auto Layout - Container Layout - Nested +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutNested) { auto doc = pagx::PAGXDocument::Make(500, 200); auto outer = doc->makeNode(); @@ -1803,8 +1811,9 @@ PAGX_TEST(PAGXTest, LayoutNested) { EXPECT_EQ(inner2->renderPosition().y, 90.0f); } -// ==============================================================================// Auto Layout - Container Layout - Measurement (bottom-up) -// ============================================================================== +// ===================================================================================== +// Auto Layout - Container Layout - Measurement (bottom-up) +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutMeasureFromChildren) { auto doc = pagx::PAGXDocument::Make(800, 600); auto parent = doc->makeNode(); @@ -1828,8 +1837,9 @@ PAGX_TEST(PAGXTest, LayoutMeasureFromChildren) { EXPECT_EQ(parent->layoutHeight, 80.0f); } -// ==============================================================================// Auto Layout - Container Layout - Pixel Grid Snap -// ============================================================================== +// ===================================================================================== +// Auto Layout - Container Layout - Pixel Grid Snap +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutSnapToPixelGrid) { auto doc = pagx::PAGXDocument::Make(100, 100); auto parent = doc->makeNode(); @@ -1864,8 +1874,9 @@ PAGX_TEST(PAGXTest, LayoutSnapToPixelGrid) { EXPECT_EQ(child1->layoutWidth + child2->layoutWidth + child3->layoutWidth, 100.0f); } -// ==============================================================================// Auto Layout - Container Layout - Measurement Cache -// ============================================================================== +// ===================================================================================== +// Auto Layout - Container Layout - Measurement Cache +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutMeasureCacheIdempotent) { auto doc = pagx::PAGXDocument::Make(400, 200); auto parent = doc->makeNode(); @@ -1898,8 +1909,9 @@ PAGX_TEST(PAGXTest, LayoutMeasureCacheIdempotent) { EXPECT_EQ(child2->renderPosition().x, x2); } -// ==============================================================================// Auto Layout - Constraint Positioning - Single Edge -// ============================================================================== +// ===================================================================================== +// Auto Layout - Constraint Positioning - Single Edge +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutConstraintLeft) { auto doc = pagx::PAGXDocument::Make(400, 200); auto layer = doc->makeNode(); @@ -2032,8 +2044,9 @@ PAGX_TEST(PAGXTest, LayoutConstraintCenterY) { EXPECT_FLOAT_EQ(bounds.height, 50.0f); } -// ==============================================================================// Auto Layout - Constraint Positioning - Stretch (Ellipse) -// ============================================================================== +// ===================================================================================== +// Auto Layout - Constraint Positioning - Stretch (Ellipse) +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutConstraintStretchEllipse) { auto doc = pagx::PAGXDocument::Make(400, 200); auto layer = doc->makeNode(); @@ -2080,8 +2093,9 @@ PAGX_TEST(PAGXTest, LayoutConstraintStretchEllipseVertical) { EXPECT_FLOAT_EQ(bounds.y, 15.0f); } -// ==============================================================================// Auto Layout - Constraint Positioning - Stretch (Rectangle, TextBox) -// ============================================================================== +// ===================================================================================== +// Auto Layout - Constraint Positioning - Stretch (Rectangle, TextBox) +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutConstraintStretchRectangle) { auto doc = pagx::PAGXDocument::Make(400, 200); auto layer = doc->makeNode(); @@ -2134,8 +2148,9 @@ PAGX_TEST(PAGXTest, LayoutConstraintStretchTextBox) { EXPECT_FLOAT_EQ(bounds.y, 20.0f); } -// ==============================================================================// Auto Layout - Constraint Positioning - Proportional Scaling -// ============================================================================== +// ===================================================================================== +// Auto Layout - Constraint Positioning - Proportional Scaling +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutConstraintScalePolystarHorizontal) { auto doc = pagx::PAGXDocument::Make(400, 200); auto layer = doc->makeNode(); @@ -2428,8 +2443,9 @@ PAGX_TEST(PAGXTest, LayoutConstraintScalePathBothAxes) { EXPECT_FLOAT_EQ(bounds.height, 150.0f); } -// ==============================================================================// Auto Layout - Constraint Positioning - Group -// ============================================================================== +// ===================================================================================== +// Auto Layout - Constraint Positioning - Group +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutGroupDerivedSize) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -2484,8 +2500,9 @@ PAGX_TEST(PAGXTest, LayoutConstraintGroupRecursive) { EXPECT_FLOAT_EQ(rectBounds.width, 340.0f); } -// ==============================================================================// Auto Layout - Constraint Positioning - Multiple Elements -// ============================================================================== +// ===================================================================================== +// Auto Layout - Constraint Positioning - Multiple Elements +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutConstraintMultipleElements) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -2523,8 +2540,9 @@ PAGX_TEST(PAGXTest, LayoutConstraintMultipleElements) { EXPECT_FLOAT_EQ(bounds3.y + bounds3.height * 0.5f, 150.0f); } -// ==============================================================================// Auto Layout - Constraint Positioning - Validation -// ============================================================================== +// ===================================================================================== +// Auto Layout - Constraint Positioning - Validation +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutConstraintConflictCenterXWithLeftRight) { // In the new layout system, conflicting constraints are resolved by priority: // centerX has higher priority than left/right. No error is generated. @@ -2552,8 +2570,9 @@ PAGX_TEST(PAGXTest, LayoutConstraintValidCombination) { EXPECT_TRUE(doc->errors.empty()); } -// ==============================================================================// Auto Layout - Container Layout - includeInLayout -// ============================================================================== +// ===================================================================================== +// Auto Layout - Container Layout - includeInLayout +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutContainerIncludeInLayout) { auto doc = pagx::PAGXDocument::Make(600, 200); auto parent = doc->makeNode(); @@ -2624,8 +2643,9 @@ PAGX_TEST(PAGXTest, LayoutContainerIncludeInLayoutMeasure) { EXPECT_FLOAT_EQ(parent->layoutWidth, 210.0f); } -// ==============================================================================// Auto Layout - Container Layout - Alignment::Stretch -// ============================================================================== +// ===================================================================================== +// Auto Layout - Container Layout - Alignment::Stretch +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutContainerStretch) { auto doc = pagx::PAGXDocument::Make(600, 200); auto parent = doc->makeNode(); @@ -2808,8 +2828,9 @@ PAGX_TEST(PAGXTest, LayoutIncludeInLayoutMixedVisibility) { EXPECT_FLOAT_EQ(child4->renderPosition().x, 420.0f); } -// ==============================================================================// Auto Layout - Container + Constraint combined -// ============================================================================== +// ===================================================================================== +// Auto Layout - Container + Constraint combined +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutContainerWithConstraints) { auto doc = pagx::PAGXDocument::Make(600, 400); auto parent = doc->makeNode(); @@ -2846,8 +2867,9 @@ PAGX_TEST(PAGXTest, LayoutContainerWithConstraints) { EXPECT_FLOAT_EQ(bounds.y + bounds.height * 0.5f, 200.0f); } -// ==============================================================================// Auto Layout - Round-trip: Layout Attributes -// ============================================================================== +// ===================================================================================== +// Auto Layout - Round-trip: Layout Attributes +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutRoundTripAttributes) { auto doc = pagx::PAGXDocument::Make(500, 400); auto layer = doc->makeNode(); @@ -3002,8 +3024,9 @@ PAGX_TEST(PAGXTest, LayoutRoundTripConstraints) { EXPECT_NE(xml.find("centerY=\"-10\""), std::string::npos); } -// ==============================================================================// Auto Layout - Layer Constraint Positioning - Single Edge -// ============================================================================== +// ===================================================================================== +// Auto Layout - Layer Constraint Positioning - Single Edge +// ===================================================================================== PAGX_TEST(PAGXTest, LayerConstraintLeft) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3086,8 +3109,9 @@ PAGX_TEST(PAGXTest, LayerConstraintBottom) { EXPECT_FLOAT_EQ(child->renderPosition().y, 215.0f); } -// ==============================================================================// Auto Layout - Layer Constraint Positioning - Center -// ============================================================================== +// ===================================================================================== +// Auto Layout - Layer Constraint Positioning - Center +// ===================================================================================== PAGX_TEST(PAGXTest, LayerConstraintCenterX) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3154,8 +3178,9 @@ PAGX_TEST(PAGXTest, LayerConstraintCenterXWithOffset) { EXPECT_FLOAT_EQ(child->renderPosition().y, 110.0f); } -// ==============================================================================// Auto Layout - Layer Constraint Positioning - Opposite Edges Derive Size -// ============================================================================== +// ===================================================================================== +// Auto Layout - Layer Constraint Positioning - Opposite Edges Derive Size +// ===================================================================================== PAGX_TEST(PAGXTest, LayerConstraintLeftRightDeriveWidth) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3252,8 +3277,9 @@ PAGX_TEST(PAGXTest, LayerConstraintLeftRightOverridesExplicitWidth) { 320.0f); // 400 - 30 - 50, left+right overrides explicit width. } -// ==============================================================================// Auto Layout - Layer Constraint Positioning - Combined Axes -// ============================================================================== +// ===================================================================================== +// Auto Layout - Layer Constraint Positioning - Combined Axes +// ===================================================================================== PAGX_TEST(PAGXTest, LayerConstraintLeftAndTop) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3300,8 +3326,9 @@ PAGX_TEST(PAGXTest, LayerConstraintRightAndBottom) { EXPECT_FLOAT_EQ(child->renderPosition().y, 220.0f); } -// ==============================================================================// Auto Layout - Layer Constraint Positioning - includeInLayout=false -// ============================================================================== +// ===================================================================================== +// Auto Layout - Layer Constraint Positioning - includeInLayout=false +// ===================================================================================== PAGX_TEST(PAGXTest, LayerConstraintWithIncludeInLayoutFalse) { auto doc = pagx::PAGXDocument::Make(600, 400); auto parent = doc->makeNode(); @@ -3371,8 +3398,9 @@ PAGX_TEST(PAGXTest, LayerConstraintIncludeInLayoutFalseDeriveSize) { EXPECT_FLOAT_EQ(overlay->layoutHeight, 300.0f); // 400 - 40 - 60 } -// ==============================================================================// Auto Layout - Layer Constraint Positioning - Priority (container layout wins) -// ============================================================================== +// ===================================================================================== +// Auto Layout - Layer Constraint Positioning - Priority (container layout wins) +// ===================================================================================== PAGX_TEST(PAGXTest, LayerConstraintIgnoredInContainerLayout) { auto doc = pagx::PAGXDocument::Make(600, 400); auto parent = doc->makeNode(); @@ -3405,8 +3433,9 @@ PAGX_TEST(PAGXTest, LayerConstraintIgnoredInContainerLayout) { EXPECT_FLOAT_EQ(child2->renderPosition().x, 210.0f); } -// ==============================================================================// Auto Layout - Layer Constraint Positioning - No constraint, no change -// ============================================================================== +// ===================================================================================== +// Auto Layout - Layer Constraint Positioning - No constraint, no change +// ===================================================================================== PAGX_TEST(PAGXTest, LayerNoConstraintUnchanged) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3430,8 +3459,9 @@ PAGX_TEST(PAGXTest, LayerNoConstraintUnchanged) { EXPECT_FLOAT_EQ(child->renderPosition().y, 77.0f); } -// ==============================================================================// Auto Layout - Layer Constraint Positioning - Invisible child still gets layout -// ============================================================================== +// ===================================================================================== +// Auto Layout - Layer Constraint Positioning - Invisible child still gets layout +// ===================================================================================== PAGX_TEST(PAGXTest, LayerConstraintInvisibleChildNotSkipped) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3458,8 +3488,9 @@ PAGX_TEST(PAGXTest, LayerConstraintInvisibleChildNotSkipped) { EXPECT_FLOAT_EQ(child->renderPosition().y, 40.0f); } -// ==============================================================================// Auto Layout - Layer Constraint Positioning - Multiple children -// ============================================================================== +// ===================================================================================== +// Auto Layout - Layer Constraint Positioning - Multiple children +// ===================================================================================== PAGX_TEST(PAGXTest, LayerConstraintMultipleChildren) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3503,8 +3534,9 @@ PAGX_TEST(PAGXTest, LayerConstraintMultipleChildren) { EXPECT_FLOAT_EQ(centered->renderPosition().y, 110.0f); } -// ==============================================================================// Auto Layout - Layer Constraint Positioning - Measured child size (no explicit width/height) -// ============================================================================== +// ===================================================================================== +// Auto Layout - Layer Constraint Positioning - Measured child size (no explicit width/height) +// ===================================================================================== PAGX_TEST(PAGXTest, LayerConstraintMeasuredChildSize) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3534,8 +3566,9 @@ PAGX_TEST(PAGXTest, LayerConstraintMeasuredChildSize) { EXPECT_FLOAT_EQ(child->renderPosition().y, 220.0f); } -// ==============================================================================// Auto Layout - Layer Constraint Positioning - Round-trip XML -// ============================================================================== +// ===================================================================================== +// Auto Layout - Layer Constraint Positioning - Round-trip XML +// ===================================================================================== PAGX_TEST(PAGXTest, LayerConstraintRoundTrip) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3786,8 +3819,9 @@ PAGX_TEST(PAGXTest, ResourceCrossReferenceChain) { EXPECT_EQ(font->glyphs[1]->image, image); } -// ==============================================================================// Auto Layout - Constraint Priority (centerX/centerY highest priority) -// ============================================================================== +// ===================================================================================== +// Auto Layout - Constraint Priority (centerX/centerY highest priority) +// ===================================================================================== PAGX_TEST(PAGXTest, LayerConstraintCenterXOverridesLeft) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3858,8 +3892,9 @@ PAGX_TEST(PAGXTest, LayerConstraintCenterYMeasurementContribution) { << "Parent should measure centerY contribution as |centerY| * 2 + content_height"; } -// ==============================================================================// Auto Layout - Priority Combination Tests -// ============================================================================== +// ===================================================================================== +// Auto Layout - Priority Combination Tests +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutContainerFlexVsExplicitSize) { // Priority: explicit main-axis size > flex distribution. // When a child has both flex>0 and explicit width, explicit width wins. @@ -4086,8 +4121,9 @@ PAGX_TEST(PAGXTest, LayoutIncludeInLayoutFalseNotAffectingMeasurement) { EXPECT_FLOAT_EQ(child2->renderPosition().x, 200.0f); } -// ==============================================================================// Auto Layout - Group Constraint Measurement -// ============================================================================== +// ===================================================================================== +// Auto Layout - Group Constraint Measurement +// ===================================================================================== PAGX_TEST(PAGXTest, GroupChildConstraintAffectsMeasurement) { // When a Group has no explicit dimensions, its measured size should include // constraint offsets from child elements (e.g., left, top). @@ -4296,8 +4332,9 @@ PAGX_TEST(PAGXTest, ImagePatternInlineImage) { EXPECT_TRUE(pattern5->image->data != nullptr); } -// ==============================================================================// ClipToBounds -// ============================================================================== +// ===================================================================================== +// ClipToBounds +// ===================================================================================== /** * Test that clipToBounds sets scrollRect during layout when the layer has resolved dimensions. */ @@ -4421,8 +4458,9 @@ PAGX_TEST(PAGXTest, ClipToBoundsAutoMeasured) { EXPECT_EQ(parent->scrollRect.height, 100); } -// ==============================================================================// Auto Layout - Refactoring Correctness (P0) -// ============================================================================== +// ===================================================================================== +// Auto Layout - Refactoring Correctness (P0) +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutIdempotent) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -4505,8 +4543,9 @@ PAGX_TEST(PAGXTest, VerifyNestedFlexNoFalsePositive) { VerifyFile(pagxPath, "verify_nested_flex"); } -// ==============================================================================// Auto Layout - Edge Case Fixes (P2) -// ============================================================================== +// ===================================================================================== +// Auto Layout - Edge Case Fixes (P2) +// ===================================================================================== PAGX_TEST(PAGXTest, DefaultSizeElementWithOppositeEdgeConstraint) { auto doc = pagx::PAGXDocument::Make(200, 200); auto layer = doc->makeNode(); @@ -4558,8 +4597,9 @@ PAGX_TEST(PAGXTest, FlexRoundingErrorPropagation) { EXPECT_FLOAT_EQ(total, 100); } -// ==============================================================================// Auto Layout - Architectural Integrity (P3) -// ============================================================================== +// ===================================================================================== +// Auto Layout - Architectural Integrity (P3) +// ===================================================================================== PAGX_TEST(PAGXTest, TransformDoesNotAffectLayout) { auto doc = pagx::PAGXDocument::Make(400, 400); auto layer = doc->makeNode(); @@ -4631,8 +4671,9 @@ PAGX_TEST(PAGXTest, DeepNestingConstraintPropagation) { EXPECT_FLOAT_EQ(outerBounds.y, 50); } -// ==============================================================================// Auto Layout - New Feature Tests (P1) -// ============================================================================== +// ===================================================================================== +// Auto Layout - New Feature Tests (P1) +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutTextIndependentConstraint) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -4701,8 +4742,9 @@ PAGX_TEST(PAGXTest, LayoutTextPathMeasurement) { EXPECT_FLOAT_EQ(group->layoutHeight, 100); } -// ==============================================================================// Auto Layout - Edge Case Fixes (P2 continued) -// ============================================================================== +// ===================================================================================== +// Auto Layout - Edge Case Fixes (P2 continued) +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutConstraintScalePathSingleAxis) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -4764,8 +4806,9 @@ PAGX_TEST(PAGXTest, LayoutTextScaledPositionAnchor) { EXPECT_NE(text->renderFontSize(), 30); } -// ==============================================================================// Auto Layout - Architectural Integrity (P3 continued) -// ============================================================================== +// ===================================================================================== +// Auto Layout - Architectural Integrity (P3 continued) +// ===================================================================================== PAGX_TEST(PAGXTest, LayoutTextInTextBoxSkipConstraint) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -4796,8 +4839,9 @@ PAGX_TEST(PAGXTest, LayoutTextInTextBoxSkipConstraint) { EXPECT_FLOAT_EQ(text->fontSize, 20); } -// ==============================================================================// Variable naming cleanup -// ============================================================================== +// ===================================================================================== +// Variable naming cleanup +// ===================================================================================== /** * Test case: TextLayoutGlyphRun data integrity after layout. * Verifies that layout produces non-empty glyph runs with correct glyph count and positions. @@ -5025,8 +5069,9 @@ PAGX_TEST(PAGXTest, TextBoundsDirectValidation) { EXPECT_GT(boxTextBounds.height, 0); } -// ==============================================================================// Padding Unified Semantics — Round-Trip Tests -// ============================================================================== +// ===================================================================================== +// Padding Unified Semantics — Round-Trip Tests +// ===================================================================================== // Group padding round-trip through export/import. PAGX_TEST(PAGXTest, GroupPaddingRoundTrip) { auto doc = pagx::PAGXDocument::Make(200, 100); From b7fba47bdd3962651c832c834db2c3b549c8db02 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Tue, 9 Jun 2026 14:16:35 +0800 Subject: [PATCH 30/52] Restore blank lines between section separators and test cases in PAGXTest.cpp. --- test/src/PAGXTest.cpp | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 27b8578e6e..1fe45c92bb 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -1215,6 +1215,7 @@ PAGX_TEST(PAGXTest, CustomDataKeyValidation) { // ===================================================================================== // Auto Layout - Container Layout - Basic // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutHorizontalEqualWidth) { auto doc = pagx::PAGXDocument::Make(920, 200); auto parent = doc->makeNode(); @@ -1350,6 +1351,7 @@ PAGX_TEST(PAGXTest, LayoutAlignmentCenter) { // ===================================================================================== // Auto Layout - Container Layout - Arrangement End // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutArrangementEnd) { auto doc = pagx::PAGXDocument::Make(400, 100); auto parent = doc->makeNode(); @@ -1379,6 +1381,7 @@ PAGX_TEST(PAGXTest, LayoutArrangementEnd) { // ===================================================================================== // Auto Layout - Container Layout - Alignment End // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutAlignmentEnd) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -1403,6 +1406,7 @@ PAGX_TEST(PAGXTest, LayoutAlignmentEnd) { // ===================================================================================== // Auto Layout - Container Layout - Padding // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutPadding) { auto doc = pagx::PAGXDocument::Make(400, 200); auto parent = doc->makeNode(); @@ -1456,6 +1460,7 @@ PAGX_TEST(PAGXTest, LayoutPaddingWithFlex) { // ===================================================================================== // Auto Layout - Container Layout - Flex Distribution // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutFlexEqualDistribution) { auto doc = pagx::PAGXDocument::Make(300, 100); auto parent = doc->makeNode(); @@ -1664,6 +1669,7 @@ PAGX_TEST(PAGXTest, LayoutFlexZeroNotExported) { // ===================================================================================== // Auto Layout - Container Layout - Visibility // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutHiddenChildNotSkipped) { auto doc = pagx::PAGXDocument::Make(300, 100); auto parent = doc->makeNode(); @@ -1698,6 +1704,7 @@ PAGX_TEST(PAGXTest, LayoutHiddenChildNotSkipped) { // ===================================================================================== // Auto Layout - Container Layout - Edge Cases // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutEmptyContainer) { auto doc = pagx::PAGXDocument::Make(400, 200); auto parent = doc->makeNode(); @@ -1774,6 +1781,7 @@ PAGX_TEST(PAGXTest, LayoutOverflow) { // ===================================================================================== // Auto Layout - Container Layout - Nested // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutNested) { auto doc = pagx::PAGXDocument::Make(500, 200); auto outer = doc->makeNode(); @@ -1814,6 +1822,7 @@ PAGX_TEST(PAGXTest, LayoutNested) { // ===================================================================================== // Auto Layout - Container Layout - Measurement (bottom-up) // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutMeasureFromChildren) { auto doc = pagx::PAGXDocument::Make(800, 600); auto parent = doc->makeNode(); @@ -1840,6 +1849,7 @@ PAGX_TEST(PAGXTest, LayoutMeasureFromChildren) { // ===================================================================================== // Auto Layout - Container Layout - Pixel Grid Snap // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutSnapToPixelGrid) { auto doc = pagx::PAGXDocument::Make(100, 100); auto parent = doc->makeNode(); @@ -1877,6 +1887,7 @@ PAGX_TEST(PAGXTest, LayoutSnapToPixelGrid) { // ===================================================================================== // Auto Layout - Container Layout - Measurement Cache // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutMeasureCacheIdempotent) { auto doc = pagx::PAGXDocument::Make(400, 200); auto parent = doc->makeNode(); @@ -1912,6 +1923,7 @@ PAGX_TEST(PAGXTest, LayoutMeasureCacheIdempotent) { // ===================================================================================== // Auto Layout - Constraint Positioning - Single Edge // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutConstraintLeft) { auto doc = pagx::PAGXDocument::Make(400, 200); auto layer = doc->makeNode(); @@ -2047,6 +2059,7 @@ PAGX_TEST(PAGXTest, LayoutConstraintCenterY) { // ===================================================================================== // Auto Layout - Constraint Positioning - Stretch (Ellipse) // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutConstraintStretchEllipse) { auto doc = pagx::PAGXDocument::Make(400, 200); auto layer = doc->makeNode(); @@ -2096,6 +2109,7 @@ PAGX_TEST(PAGXTest, LayoutConstraintStretchEllipseVertical) { // ===================================================================================== // Auto Layout - Constraint Positioning - Stretch (Rectangle, TextBox) // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutConstraintStretchRectangle) { auto doc = pagx::PAGXDocument::Make(400, 200); auto layer = doc->makeNode(); @@ -2151,6 +2165,7 @@ PAGX_TEST(PAGXTest, LayoutConstraintStretchTextBox) { // ===================================================================================== // Auto Layout - Constraint Positioning - Proportional Scaling // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutConstraintScalePolystarHorizontal) { auto doc = pagx::PAGXDocument::Make(400, 200); auto layer = doc->makeNode(); @@ -2446,6 +2461,7 @@ PAGX_TEST(PAGXTest, LayoutConstraintScalePathBothAxes) { // ===================================================================================== // Auto Layout - Constraint Positioning - Group // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutGroupDerivedSize) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -2503,6 +2519,7 @@ PAGX_TEST(PAGXTest, LayoutConstraintGroupRecursive) { // ===================================================================================== // Auto Layout - Constraint Positioning - Multiple Elements // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutConstraintMultipleElements) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -2543,6 +2560,7 @@ PAGX_TEST(PAGXTest, LayoutConstraintMultipleElements) { // ===================================================================================== // Auto Layout - Constraint Positioning - Validation // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutConstraintConflictCenterXWithLeftRight) { // In the new layout system, conflicting constraints are resolved by priority: // centerX has higher priority than left/right. No error is generated. @@ -2573,6 +2591,7 @@ PAGX_TEST(PAGXTest, LayoutConstraintValidCombination) { // ===================================================================================== // Auto Layout - Container Layout - includeInLayout // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutContainerIncludeInLayout) { auto doc = pagx::PAGXDocument::Make(600, 200); auto parent = doc->makeNode(); @@ -2646,6 +2665,7 @@ PAGX_TEST(PAGXTest, LayoutContainerIncludeInLayoutMeasure) { // ===================================================================================== // Auto Layout - Container Layout - Alignment::Stretch // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutContainerStretch) { auto doc = pagx::PAGXDocument::Make(600, 200); auto parent = doc->makeNode(); @@ -2831,6 +2851,7 @@ PAGX_TEST(PAGXTest, LayoutIncludeInLayoutMixedVisibility) { // ===================================================================================== // Auto Layout - Container + Constraint combined // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutContainerWithConstraints) { auto doc = pagx::PAGXDocument::Make(600, 400); auto parent = doc->makeNode(); @@ -2870,6 +2891,7 @@ PAGX_TEST(PAGXTest, LayoutContainerWithConstraints) { // ===================================================================================== // Auto Layout - Round-trip: Layout Attributes // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutRoundTripAttributes) { auto doc = pagx::PAGXDocument::Make(500, 400); auto layer = doc->makeNode(); @@ -3027,6 +3049,7 @@ PAGX_TEST(PAGXTest, LayoutRoundTripConstraints) { // ===================================================================================== // Auto Layout - Layer Constraint Positioning - Single Edge // ===================================================================================== + PAGX_TEST(PAGXTest, LayerConstraintLeft) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3112,6 +3135,7 @@ PAGX_TEST(PAGXTest, LayerConstraintBottom) { // ===================================================================================== // Auto Layout - Layer Constraint Positioning - Center // ===================================================================================== + PAGX_TEST(PAGXTest, LayerConstraintCenterX) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3181,6 +3205,7 @@ PAGX_TEST(PAGXTest, LayerConstraintCenterXWithOffset) { // ===================================================================================== // Auto Layout - Layer Constraint Positioning - Opposite Edges Derive Size // ===================================================================================== + PAGX_TEST(PAGXTest, LayerConstraintLeftRightDeriveWidth) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3280,6 +3305,7 @@ PAGX_TEST(PAGXTest, LayerConstraintLeftRightOverridesExplicitWidth) { // ===================================================================================== // Auto Layout - Layer Constraint Positioning - Combined Axes // ===================================================================================== + PAGX_TEST(PAGXTest, LayerConstraintLeftAndTop) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3329,6 +3355,7 @@ PAGX_TEST(PAGXTest, LayerConstraintRightAndBottom) { // ===================================================================================== // Auto Layout - Layer Constraint Positioning - includeInLayout=false // ===================================================================================== + PAGX_TEST(PAGXTest, LayerConstraintWithIncludeInLayoutFalse) { auto doc = pagx::PAGXDocument::Make(600, 400); auto parent = doc->makeNode(); @@ -3401,6 +3428,7 @@ PAGX_TEST(PAGXTest, LayerConstraintIncludeInLayoutFalseDeriveSize) { // ===================================================================================== // Auto Layout - Layer Constraint Positioning - Priority (container layout wins) // ===================================================================================== + PAGX_TEST(PAGXTest, LayerConstraintIgnoredInContainerLayout) { auto doc = pagx::PAGXDocument::Make(600, 400); auto parent = doc->makeNode(); @@ -3436,6 +3464,7 @@ PAGX_TEST(PAGXTest, LayerConstraintIgnoredInContainerLayout) { // ===================================================================================== // Auto Layout - Layer Constraint Positioning - No constraint, no change // ===================================================================================== + PAGX_TEST(PAGXTest, LayerNoConstraintUnchanged) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3462,6 +3491,7 @@ PAGX_TEST(PAGXTest, LayerNoConstraintUnchanged) { // ===================================================================================== // Auto Layout - Layer Constraint Positioning - Invisible child still gets layout // ===================================================================================== + PAGX_TEST(PAGXTest, LayerConstraintInvisibleChildNotSkipped) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3491,6 +3521,7 @@ PAGX_TEST(PAGXTest, LayerConstraintInvisibleChildNotSkipped) { // ===================================================================================== // Auto Layout - Layer Constraint Positioning - Multiple children // ===================================================================================== + PAGX_TEST(PAGXTest, LayerConstraintMultipleChildren) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3537,6 +3568,7 @@ PAGX_TEST(PAGXTest, LayerConstraintMultipleChildren) { // ===================================================================================== // Auto Layout - Layer Constraint Positioning - Measured child size (no explicit width/height) // ===================================================================================== + PAGX_TEST(PAGXTest, LayerConstraintMeasuredChildSize) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3569,6 +3601,7 @@ PAGX_TEST(PAGXTest, LayerConstraintMeasuredChildSize) { // ===================================================================================== // Auto Layout - Layer Constraint Positioning - Round-trip XML // ===================================================================================== + PAGX_TEST(PAGXTest, LayerConstraintRoundTrip) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3822,6 +3855,7 @@ PAGX_TEST(PAGXTest, ResourceCrossReferenceChain) { // ===================================================================================== // Auto Layout - Constraint Priority (centerX/centerY highest priority) // ===================================================================================== + PAGX_TEST(PAGXTest, LayerConstraintCenterXOverridesLeft) { auto doc = pagx::PAGXDocument::Make(400, 300); auto parent = doc->makeNode(); @@ -3895,6 +3929,7 @@ PAGX_TEST(PAGXTest, LayerConstraintCenterYMeasurementContribution) { // ===================================================================================== // Auto Layout - Priority Combination Tests // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutContainerFlexVsExplicitSize) { // Priority: explicit main-axis size > flex distribution. // When a child has both flex>0 and explicit width, explicit width wins. @@ -4124,6 +4159,7 @@ PAGX_TEST(PAGXTest, LayoutIncludeInLayoutFalseNotAffectingMeasurement) { // ===================================================================================== // Auto Layout - Group Constraint Measurement // ===================================================================================== + PAGX_TEST(PAGXTest, GroupChildConstraintAffectsMeasurement) { // When a Group has no explicit dimensions, its measured size should include // constraint offsets from child elements (e.g., left, top). @@ -4461,6 +4497,7 @@ PAGX_TEST(PAGXTest, ClipToBoundsAutoMeasured) { // ===================================================================================== // Auto Layout - Refactoring Correctness (P0) // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutIdempotent) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -4546,6 +4583,7 @@ PAGX_TEST(PAGXTest, VerifyNestedFlexNoFalsePositive) { // ===================================================================================== // Auto Layout - Edge Case Fixes (P2) // ===================================================================================== + PAGX_TEST(PAGXTest, DefaultSizeElementWithOppositeEdgeConstraint) { auto doc = pagx::PAGXDocument::Make(200, 200); auto layer = doc->makeNode(); @@ -4600,6 +4638,7 @@ PAGX_TEST(PAGXTest, FlexRoundingErrorPropagation) { // ===================================================================================== // Auto Layout - Architectural Integrity (P3) // ===================================================================================== + PAGX_TEST(PAGXTest, TransformDoesNotAffectLayout) { auto doc = pagx::PAGXDocument::Make(400, 400); auto layer = doc->makeNode(); @@ -4674,6 +4713,7 @@ PAGX_TEST(PAGXTest, DeepNestingConstraintPropagation) { // ===================================================================================== // Auto Layout - New Feature Tests (P1) // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutTextIndependentConstraint) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -4745,6 +4785,7 @@ PAGX_TEST(PAGXTest, LayoutTextPathMeasurement) { // ===================================================================================== // Auto Layout - Edge Case Fixes (P2 continued) // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutConstraintScalePathSingleAxis) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); @@ -4809,6 +4850,7 @@ PAGX_TEST(PAGXTest, LayoutTextScaledPositionAnchor) { // ===================================================================================== // Auto Layout - Architectural Integrity (P3 continued) // ===================================================================================== + PAGX_TEST(PAGXTest, LayoutTextInTextBoxSkipConstraint) { auto doc = pagx::PAGXDocument::Make(400, 300); auto layer = doc->makeNode(); From a218045c17aa724ae317a534ab80f16449d08ebc Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Tue, 9 Jun 2026 14:57:54 +0800 Subject: [PATCH 31/52] Remove unused Text::contentBounds and TextBox::contentBounds functions. --- include/pagx/nodes/Text.h | 7 ------ include/pagx/nodes/TextBox.h | 4 +--- src/pagx/nodes/Text.cpp | 14 ----------- src/pagx/nodes/TextBox.cpp | 45 ------------------------------------ 4 files changed, 1 insertion(+), 69 deletions(-) diff --git a/include/pagx/nodes/Text.h b/include/pagx/nodes/Text.h index e3507dee38..db06438324 100644 --- a/include/pagx/nodes/Text.h +++ b/include/pagx/nodes/Text.h @@ -109,13 +109,6 @@ class Text : public Element, public LayoutNode { /** Returns the effective font size after layout scaling. */ float renderFontSize() const; - /** - * Returns the text content bounds in layer coordinates, computed from the shaped linebox bounds - * offset by renderPosition. This is the bounding rectangle of the rendered text content, - * aligned with how tgfx positions the TextBlob. - */ - Rect contentBounds() const; - NodeType nodeType() const override { return NodeType::Text; } diff --git a/include/pagx/nodes/TextBox.h b/include/pagx/nodes/TextBox.h index 9949ff556f..00f16896b7 100644 --- a/include/pagx/nodes/TextBox.h +++ b/include/pagx/nodes/TextBox.h @@ -90,9 +90,7 @@ class TextBox : public Group { return NodeType::TextBox; } - Rect contentBounds() const; - - protected: +protected: void onMeasure(LayoutContext* context) override; void setLayoutSize(LayoutContext* context, float targetWidth, float targetHeight) override; void updateLayout(LayoutContext* context) override; diff --git a/src/pagx/nodes/Text.cpp b/src/pagx/nodes/Text.cpp index bfca519355..f13a7536e9 100644 --- a/src/pagx/nodes/Text.cpp +++ b/src/pagx/nodes/Text.cpp @@ -85,18 +85,4 @@ float Text::renderFontSize() const { return fontSize * textScale; } -Rect Text::contentBounds() const { - auto pos = renderPosition(); - auto textBlob = glyphData->textBlob; - if (textBlob == nullptr && !glyphData->layoutRuns.empty()) { - textBlob = - GlyphRunRenderer::BuildTextBlobFromLayoutRuns(glyphData->layoutRuns, tgfx::Matrix::I()); - } - if (textBlob) { - auto bounds = textBlob->getBounds(); - return {pos.x + bounds.x(), pos.y + bounds.y(), bounds.width(), bounds.height()}; - } - return {pos.x + textBounds.x, pos.y + textBounds.y, textBounds.width, textBounds.height}; -} - } // namespace pagx diff --git a/src/pagx/nodes/TextBox.cpp b/src/pagx/nodes/TextBox.cpp index 0e1e7f47d2..b04b2bba92 100644 --- a/src/pagx/nodes/TextBox.cpp +++ b/src/pagx/nodes/TextBox.cpp @@ -171,49 +171,4 @@ void TextBox::updateLayout(LayoutContext* context) { } } -Rect TextBox::contentBounds() const { - std::vector childText = {}; - TextLayout::CollectTextElements(elements, childText); - float left = 0, top = 0, right = 0, bottom = 0; - bool first = true; - for (auto* text : childText) { - if (text == nullptr) continue; - auto textBlob = text->glyphData->textBlob; - if (textBlob == nullptr && !text->glyphData->layoutRuns.empty()) { - textBlob = GlyphRunRenderer::BuildTextBlobFromLayoutRuns(text->glyphData->layoutRuns, - tgfx::Matrix::I()); - } - auto pos = text->renderPosition(); - float tl, tt, tr, tb; - if (textBlob) { - auto bounds = textBlob->getBounds(); - tl = pos.x + bounds.x(); - tt = pos.y + bounds.y(); - tr = tl + bounds.width(); - tb = tt + bounds.height(); - } else { - tl = pos.x + text->textBounds.x; - tt = pos.y + text->textBounds.y; - tr = tl + text->textBounds.width; - tb = tt + text->textBounds.height; - } - if (first) { - left = tl; - top = tt; - right = tr; - bottom = tb; - first = false; - } else { - if (tl < left) left = tl; - if (tt < top) top = tt; - if (tr > right) right = tr; - if (tb > bottom) bottom = tb; - } - } - if (first) { - return {}; - } - return Rect::MakeLTRB(left, top, right, bottom); -} - } // namespace pagx From 937a0eace039cfcd719bb1166d411e00b01a6f8c Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Tue, 9 Jun 2026 15:00:58 +0800 Subject: [PATCH 32/52] Remove unused includes for GlyphRunRenderer in Text and TextBox. --- src/pagx/nodes/Text.cpp | 1 - src/pagx/nodes/TextBox.cpp | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/pagx/nodes/Text.cpp b/src/pagx/nodes/Text.cpp index f13a7536e9..7c04058d5b 100644 --- a/src/pagx/nodes/Text.cpp +++ b/src/pagx/nodes/Text.cpp @@ -21,7 +21,6 @@ #include "pagx/TextLayoutParams.h" #include "pagx/nodes/LayoutNode.h" #include "pagx/utils/TextUtils.h" -#include "renderer/GlyphRunRenderer.h" namespace pagx { diff --git a/src/pagx/nodes/TextBox.cpp b/src/pagx/nodes/TextBox.cpp index b04b2bba92..e2fc659fb2 100644 --- a/src/pagx/nodes/TextBox.cpp +++ b/src/pagx/nodes/TextBox.cpp @@ -21,8 +21,6 @@ #include "pagx/TextLayout.h" #include "pagx/TextLayoutParams.h" #include "pagx/nodes/LayoutNode.h" -#include "pagx/nodes/Text.h" -#include "renderer/GlyphRunRenderer.h" namespace pagx { From b94d4a132605961ae016209406b7508b44091ded Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Wed, 10 Jun 2026 19:18:45 +0800 Subject: [PATCH 33/52] Add animation binding support for NoiseFilter and NoiseStyle. --- src/renderer/LayerBuilder.cpp | 172 ++++++++++++++++++++++++++++++++-- 1 file changed, 166 insertions(+), 6 deletions(-) diff --git a/src/renderer/LayerBuilder.cpp b/src/renderer/LayerBuilder.cpp index eab62df3b1..8b1205e076 100644 --- a/src/renderer/LayerBuilder.cpp +++ b/src/renderer/LayerBuilder.cpp @@ -1069,6 +1069,8 @@ class LayerBuilderContext { if (tgfxStyle && node->blendMode != BlendMode::Normal) { tgfxStyle->setBlendMode(ToTGFX(node->blendMode)); } + _result.binding.set(style, tgfxStyle); + bindNoiseStyleChannels(style); return tgfxStyle; } default: @@ -1216,6 +1218,82 @@ class LayerBuilderContext { _result.binding.setWriter(node, "blurY", WriteBackgroundBlurStyleBlurY); } + static void WriteNoiseStyleSize(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* style = static_cast(object); + style->setSize(MixFloat(style->size(), *v, mix)); + } + + static void WriteNoiseStyleDensity(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* style = static_cast(object); + style->setDensity(MixFloat(style->density(), *v, mix)); + } + + static void WriteNoiseStyleSeed(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* style = static_cast(object); + style->setSeed(MixFloat(style->seed(), *v, mix)); + } + + static void WriteNoiseStyleColor(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* style = static_cast(object); + auto target = ToTGFX(*v); + style->setColor(MixTGFXColor(style->color(), target, mix)); + } + + static void WriteNoiseStyleFirstColor(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* style = static_cast(object); + auto target = ToTGFX(*v); + style->setFirstColor(MixTGFXColor(style->firstColor(), target, mix)); + } + + static void WriteNoiseStyleSecondColor(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* style = static_cast(object); + auto target = ToTGFX(*v); + style->setSecondColor(MixTGFXColor(style->secondColor(), target, mix)); + } + + static void WriteNoiseStyleOpacity(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* style = static_cast(object); + style->setOpacity(MixFloat(style->opacity(), *v, mix)); + } + + void bindNoiseStyleChannels(const pagx::NoiseStyle* node) { + _result.binding.setWriter(node, "size", WriteNoiseStyleSize); + _result.binding.setWriter(node, "density", WriteNoiseStyleDensity); + _result.binding.setWriter(node, "seed", WriteNoiseStyleSeed); + _result.binding.setWriter(node, "color", WriteNoiseStyleColor); + _result.binding.setWriter(node, "firstColor", WriteNoiseStyleFirstColor); + _result.binding.setWriter(node, "secondColor", WriteNoiseStyleSecondColor); + _result.binding.setWriter(node, "opacity", WriteNoiseStyleOpacity); + } + static void WriteBlurFilterX(void* object, const KeyValue& value, float mix) { auto* v = std::get_if(&value); if (v == nullptr) { @@ -1379,6 +1457,82 @@ class LayerBuilderContext { _result.binding.setWriter(node, "color", WriteBlendFilterColor); } + static void WriteNoiseFilterSize(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* filter = static_cast(object); + filter->setSize(MixFloat(filter->size(), *v, mix)); + } + + static void WriteNoiseFilterDensity(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* filter = static_cast(object); + filter->setDensity(MixFloat(filter->density(), *v, mix)); + } + + static void WriteNoiseFilterSeed(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* filter = static_cast(object); + filter->setSeed(MixFloat(filter->seed(), *v, mix)); + } + + static void WriteNoiseFilterColor(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* filter = static_cast(object); + auto target = ToTGFX(*v); + filter->setColor(MixTGFXColor(filter->color(), target, mix)); + } + + static void WriteNoiseFilterFirstColor(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* filter = static_cast(object); + auto target = ToTGFX(*v); + filter->setFirstColor(MixTGFXColor(filter->firstColor(), target, mix)); + } + + static void WriteNoiseFilterSecondColor(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* filter = static_cast(object); + auto target = ToTGFX(*v); + filter->setSecondColor(MixTGFXColor(filter->secondColor(), target, mix)); + } + + static void WriteNoiseFilterOpacity(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* filter = static_cast(object); + filter->setOpacity(MixFloat(filter->opacity(), *v, mix)); + } + + void bindNoiseFilterChannels(const pagx::NoiseFilter* node) { + _result.binding.setWriter(node, "size", WriteNoiseFilterSize); + _result.binding.setWriter(node, "density", WriteNoiseFilterDensity); + _result.binding.setWriter(node, "seed", WriteNoiseFilterSeed); + _result.binding.setWriter(node, "color", WriteNoiseFilterColor); + _result.binding.setWriter(node, "firstColor", WriteNoiseFilterFirstColor); + _result.binding.setWriter(node, "secondColor", WriteNoiseFilterSecondColor); + _result.binding.setWriter(node, "opacity", WriteNoiseFilterOpacity); + } + std::shared_ptr convertLayerFilter(const LayerFilter* node) { if (!node) { return nullptr; @@ -1425,19 +1579,25 @@ class LayerBuilderContext { case NodeType::NoiseFilter: { auto filter = static_cast(node); auto tgfxBlendMode = ToTGFX(filter->blendMode); + std::shared_ptr tgfxFilter; switch (filter->mode) { case NoiseMode::Mono: - return tgfx::NoiseFilter::MakeMono(filter->size, filter->density, ToTGFX(filter->color), - filter->seed, tgfxBlendMode); + tgfxFilter = tgfx::NoiseFilter::MakeMono( + filter->size, filter->density, ToTGFX(filter->color), filter->seed, tgfxBlendMode); + break; case NoiseMode::Duo: - return tgfx::NoiseFilter::MakeDuo( + tgfxFilter = tgfx::NoiseFilter::MakeDuo( filter->size, filter->density, ToTGFX(filter->firstColor), ToTGFX(filter->secondColor), filter->seed, tgfxBlendMode); + break; case NoiseMode::Multi: - return tgfx::NoiseFilter::MakeMulti(filter->size, filter->density, filter->opacity, - filter->seed, tgfxBlendMode); + tgfxFilter = tgfx::NoiseFilter::MakeMulti(filter->size, filter->density, + filter->opacity, filter->seed, tgfxBlendMode); + break; } - return nullptr; + _result.binding.set(filter, tgfxFilter); + bindNoiseFilterChannels(filter); + return tgfxFilter; } default: return nullptr; From a931e0f1e51b2558a630c036c5893a5396da1ba6 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Wed, 10 Jun 2026 19:24:57 +0800 Subject: [PATCH 34/52] Add animation binding tests for NoiseFilter and NoiseStyle channels. --- test/src/PAGXTest.cpp | 280 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 1fe45c92bb..9e2a6a0315 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -99,7 +99,9 @@ #include "tgfx/layers/filters/BlendFilter.h" #include "tgfx/layers/filters/BlurFilter.h" #include "tgfx/layers/filters/DropShadowFilter.h" +#include "tgfx/layers/filters/NoiseFilter.h" #include "tgfx/layers/layerstyles/DropShadowStyle.h" +#include "tgfx/layers/layerstyles/NoiseStyle.h" #include "tgfx/layers/vectors/Gradient.h" #include "tgfx/layers/vectors/SolidColor.h" #include "tgfx/layers/vectors/Text.h" @@ -4371,6 +4373,7 @@ PAGX_TEST(PAGXTest, ImagePatternInlineImage) { // ===================================================================================== // ClipToBounds // ===================================================================================== + /** * Test that clipToBounds sets scrollRect during layout when the layer has resolved dimensions. */ @@ -4884,6 +4887,7 @@ PAGX_TEST(PAGXTest, LayoutTextInTextBoxSkipConstraint) { // ===================================================================================== // Variable naming cleanup // ===================================================================================== + /** * Test case: TextLayoutGlyphRun data integrity after layout. * Verifies that layout produces non-empty glyph runs with correct glyph count and positions. @@ -5114,6 +5118,7 @@ PAGX_TEST(PAGXTest, TextBoundsDirectValidation) { // ===================================================================================== // Padding Unified Semantics — Round-Trip Tests // ===================================================================================== + // Group padding round-trip through export/import. PAGX_TEST(PAGXTest, GroupPaddingRoundTrip) { auto doc = pagx::PAGXDocument::Make(200, 100); @@ -7603,4 +7608,279 @@ PAGX_TEST(PAGXTest, NoiseStyleModes) { file.write(svg.data(), static_cast(svg.size())); } +/** + * Test animation channel binding for NoiseFilter (Mono/Duo/Multi modes). + */ +PAGX_TEST(PAGXTest, ChannelNoiseFilter) { + auto doc = pagx::PAGXDocument::Make(100, 100); + auto layer = doc->makeNode("L"); + layer->width = 100; + layer->height = 100; + doc->layers.push_back(layer); + + auto monoFilter = doc->makeNode("NF_MONO"); + monoFilter->mode = pagx::NoiseMode::Mono; + monoFilter->size = 4; + monoFilter->density = 0.2f; + monoFilter->seed = 10; + monoFilter->color = {0.0f, 0.0f, 0.0f, 1.0f}; + layer->filters.push_back(monoFilter); + + auto duoFilter = doc->makeNode("NF_DUO"); + duoFilter->mode = pagx::NoiseMode::Duo; + duoFilter->size = 6; + duoFilter->density = 0.3f; + duoFilter->seed = 20; + duoFilter->firstColor = {1.0f, 0.0f, 0.0f, 1.0f}; + duoFilter->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; + layer->filters.push_back(duoFilter); + + auto multiFilter = doc->makeNode("NF_MULTI"); + multiFilter->mode = pagx::NoiseMode::Multi; + multiFilter->size = 8; + multiFilter->density = 0.4f; + multiFilter->seed = 30; + multiFilter->opacity = 0.5f; + layer->filters.push_back(multiFilter); + + auto anim = doc->makeNode("a"); + anim->duration = 60; + anim->frameRate = 60; + doc->animations.push_back(anim); + + // Mono filter channels + auto* monoObj = doc->makeNode(); + monoObj->target = "NF_MONO"; + anim->objects.push_back(monoObj); + auto* monoSize = doc->makeNode>(); + monoSize->name = "size"; + monoSize->keyframes.push_back({0, 20.0f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoSize); + auto* monoDensity = doc->makeNode>(); + monoDensity->name = "density"; + monoDensity->keyframes.push_back({0, 0.8f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoDensity); + auto* monoSeed = doc->makeNode>(); + monoSeed->name = "seed"; + monoSeed->keyframes.push_back({0, 100.0f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoSeed); + auto* monoColor = doc->makeNode>(); + monoColor->name = "color"; + monoColor->keyframes.push_back( + {0, pagx::Color{1.0f, 0.0f, 0.0f, 1.0f}, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoColor); + + // Duo filter channels + auto* duoObj = doc->makeNode(); + duoObj->target = "NF_DUO"; + anim->objects.push_back(duoObj); + auto* duoFirstColor = doc->makeNode>(); + duoFirstColor->name = "firstColor"; + duoFirstColor->keyframes.push_back( + {0, pagx::Color{0.0f, 1.0f, 0.0f, 1.0f}, pagx::KeyframeInterpolationType::Hold, {}, {}}); + duoObj->channels.push_back(duoFirstColor); + auto* duoSecondColor = doc->makeNode>(); + duoSecondColor->name = "secondColor"; + duoSecondColor->keyframes.push_back( + {0, pagx::Color{1.0f, 1.0f, 0.0f, 1.0f}, pagx::KeyframeInterpolationType::Hold, {}, {}}); + duoObj->channels.push_back(duoSecondColor); + + // Multi filter channels + auto* multiObj = doc->makeNode(); + multiObj->target = "NF_MULTI"; + anim->objects.push_back(multiObj); + auto* multiOpacity = doc->makeNode>(); + multiOpacity->name = "opacity"; + multiOpacity->keyframes.push_back({0, 1.0f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + multiObj->channels.push_back(multiOpacity); + + auto file = pagx::PAGScene::Make(doc); + ASSERT_TRUE(file != nullptr); + auto& tree = *file->mutableBinding(); + + auto tgfxMono = tree.get(monoFilter); + ASSERT_TRUE(tgfxMono != nullptr); + auto tgfxDuo = tree.get(duoFilter); + ASSERT_TRUE(tgfxDuo != nullptr); + auto tgfxMulti = tree.get(multiFilter); + ASSERT_TRUE(tgfxMulti != nullptr); + + auto timeline = file->getDefaultTimeline(); + ASSERT_TRUE(timeline != nullptr); + + // Apply at mix=1.0: values fully overwritten by keyframe targets. + timeline->apply(1.0f); + + // Mono: size 4→20, density 0.2→0.8, seed 10→100, color black→red + EXPECT_FLOAT_EQ(tgfxMono->size(), 20.0f); + EXPECT_FLOAT_EQ(tgfxMono->density(), 0.8f); + EXPECT_FLOAT_EQ(tgfxMono->seed(), 100.0f); + auto monoNoise = std::static_pointer_cast(tgfxMono); + EXPECT_FLOAT_EQ(monoNoise->color().red, 1.0f); + EXPECT_FLOAT_EQ(monoNoise->color().green, 0.0f); + + // Duo: firstColor red→green, secondColor blue→yellow + auto duoNoise = std::static_pointer_cast(tgfxDuo); + EXPECT_FLOAT_EQ(duoNoise->firstColor().green, 1.0f); + EXPECT_FLOAT_EQ(duoNoise->firstColor().red, 0.0f); + EXPECT_FLOAT_EQ(duoNoise->secondColor().red, 1.0f); + EXPECT_FLOAT_EQ(duoNoise->secondColor().green, 1.0f); + + // Multi: opacity 0.5→1.0 + auto multiNoise = std::static_pointer_cast(tgfxMulti); + EXPECT_FLOAT_EQ(multiNoise->opacity(), 1.0f); + + // Apply at mix=0.5: interpolate from initial toward keyframe. + // Reset initial values by re-building. + auto file2 = pagx::PAGScene::Make(doc); + auto& tree2 = *file2->mutableBinding(); + auto tgfxMono2 = tree2.get(monoFilter); + auto timeline2 = file2->getDefaultTimeline(); + timeline2->apply(0.5f); + + // size: 4 + (20-4)*0.5 = 12 + EXPECT_FLOAT_EQ(tgfxMono2->size(), 12.0f); + // density: 0.2 + (0.8-0.2)*0.5 = 0.5 + EXPECT_FLOAT_EQ(tgfxMono2->density(), 0.5f); + // seed: 10 + (100-10)*0.5 = 55 + EXPECT_FLOAT_EQ(tgfxMono2->seed(), 55.0f); +} + +/** + * Test animation channel binding for NoiseStyle (Mono/Duo/Multi modes). + */ +PAGX_TEST(PAGXTest, ChannelNoiseStyle) { + auto doc = pagx::PAGXDocument::Make(100, 100); + auto layer = doc->makeNode("L"); + layer->width = 100; + layer->height = 100; + doc->layers.push_back(layer); + + auto monoStyle = doc->makeNode("NS_MONO"); + monoStyle->mode = pagx::NoiseMode::Mono; + monoStyle->size = 4; + monoStyle->density = 0.2f; + monoStyle->seed = 10; + monoStyle->color = {0.0f, 0.0f, 0.0f, 1.0f}; + layer->styles.push_back(monoStyle); + + auto duoStyle = doc->makeNode("NS_DUO"); + duoStyle->mode = pagx::NoiseMode::Duo; + duoStyle->size = 6; + duoStyle->density = 0.3f; + duoStyle->seed = 20; + duoStyle->firstColor = {1.0f, 0.0f, 0.0f, 1.0f}; + duoStyle->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; + layer->styles.push_back(duoStyle); + + auto multiStyle = doc->makeNode("NS_MULTI"); + multiStyle->mode = pagx::NoiseMode::Multi; + multiStyle->size = 8; + multiStyle->density = 0.4f; + multiStyle->seed = 30; + multiStyle->opacity = 0.5f; + layer->styles.push_back(multiStyle); + + auto anim = doc->makeNode("a"); + anim->duration = 60; + anim->frameRate = 60; + doc->animations.push_back(anim); + + // Mono style channels + auto* monoObj = doc->makeNode(); + monoObj->target = "NS_MONO"; + anim->objects.push_back(monoObj); + auto* monoSize = doc->makeNode>(); + monoSize->name = "size"; + monoSize->keyframes.push_back({0, 16.0f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoSize); + auto* monoDensity = doc->makeNode>(); + monoDensity->name = "density"; + monoDensity->keyframes.push_back({0, 0.9f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoDensity); + auto* monoSeed = doc->makeNode>(); + monoSeed->name = "seed"; + monoSeed->keyframes.push_back({0, 80.0f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoSeed); + auto* monoColor = doc->makeNode>(); + monoColor->name = "color"; + monoColor->keyframes.push_back( + {0, pagx::Color{0.0f, 1.0f, 0.0f, 1.0f}, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoColor); + + // Duo style channels + auto* duoObj = doc->makeNode(); + duoObj->target = "NS_DUO"; + anim->objects.push_back(duoObj); + auto* duoFirstColor = doc->makeNode>(); + duoFirstColor->name = "firstColor"; + duoFirstColor->keyframes.push_back( + {0, pagx::Color{0.0f, 0.0f, 1.0f, 1.0f}, pagx::KeyframeInterpolationType::Hold, {}, {}}); + duoObj->channels.push_back(duoFirstColor); + auto* duoSecondColor = doc->makeNode>(); + duoSecondColor->name = "secondColor"; + duoSecondColor->keyframes.push_back( + {0, pagx::Color{0.0f, 1.0f, 0.0f, 1.0f}, pagx::KeyframeInterpolationType::Hold, {}, {}}); + duoObj->channels.push_back(duoSecondColor); + + // Multi style channels + auto* multiObj = doc->makeNode(); + multiObj->target = "NS_MULTI"; + anim->objects.push_back(multiObj); + auto* multiOpacity = doc->makeNode>(); + multiOpacity->name = "opacity"; + multiOpacity->keyframes.push_back({0, 1.0f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + multiObj->channels.push_back(multiOpacity); + + auto file = pagx::PAGScene::Make(doc); + ASSERT_TRUE(file != nullptr); + auto& tree = *file->mutableBinding(); + + auto tgfxMono = tree.get(monoStyle); + ASSERT_TRUE(tgfxMono != nullptr); + auto tgfxDuo = tree.get(duoStyle); + ASSERT_TRUE(tgfxDuo != nullptr); + auto tgfxMulti = tree.get(multiStyle); + ASSERT_TRUE(tgfxMulti != nullptr); + + auto timeline = file->getDefaultTimeline(); + ASSERT_TRUE(timeline != nullptr); + + // Apply at mix=1.0 + timeline->apply(1.0f); + + // Mono: size 4→16, density 0.2→0.9, seed 10→80, color black→green + EXPECT_FLOAT_EQ(tgfxMono->size(), 16.0f); + EXPECT_FLOAT_EQ(tgfxMono->density(), 0.9f); + EXPECT_FLOAT_EQ(tgfxMono->seed(), 80.0f); + auto monoNoise = std::static_pointer_cast(tgfxMono); + EXPECT_FLOAT_EQ(monoNoise->color().green, 1.0f); + EXPECT_FLOAT_EQ(monoNoise->color().red, 0.0f); + + // Duo: firstColor red→blue, secondColor blue→green + auto duoNoise = std::static_pointer_cast(tgfxDuo); + EXPECT_FLOAT_EQ(duoNoise->firstColor().blue, 1.0f); + EXPECT_FLOAT_EQ(duoNoise->firstColor().red, 0.0f); + EXPECT_FLOAT_EQ(duoNoise->secondColor().green, 1.0f); + EXPECT_FLOAT_EQ(duoNoise->secondColor().blue, 0.0f); + + // Multi: opacity 0.5→1.0 + auto multiNoise = std::static_pointer_cast(tgfxMulti); + EXPECT_FLOAT_EQ(multiNoise->opacity(), 1.0f); + + // Apply at mix=0.5: interpolate from initial values. + auto file2 = pagx::PAGScene::Make(doc); + auto& tree2 = *file2->mutableBinding(); + auto tgfxMono2 = tree2.get(monoStyle); + auto timeline2 = file2->getDefaultTimeline(); + timeline2->apply(0.5f); + + // size: 4 + (16-4)*0.5 = 10 + EXPECT_FLOAT_EQ(tgfxMono2->size(), 10.0f); + // density: 0.2 + (0.9-0.2)*0.5 = 0.55 + EXPECT_FLOAT_EQ(tgfxMono2->density(), 0.55f); + // seed: 10 + (80-10)*0.5 = 45 + EXPECT_FLOAT_EQ(tgfxMono2->seed(), 45.0f); +} + } // namespace pag From 73aaa60b533703ab79e4acccbe95741567c7c4f4 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Wed, 10 Jun 2026 20:06:22 +0800 Subject: [PATCH 35/52] Update noise animation test with 40-frame export across 4 modes and combined shadows. --- playground/pagx-playground/package-lock.json | 1978 ++++++++++-------- test/src/PAGXTest.cpp | 155 ++ 2 files changed, 1264 insertions(+), 869 deletions(-) diff --git a/playground/pagx-playground/package-lock.json b/playground/pagx-playground/package-lock.json index ccc324ba38..65a68aa89c 100644 --- a/playground/pagx-playground/package-lock.json +++ b/playground/pagx-playground/package-lock.json @@ -13,31 +13,32 @@ "highlight.js": "^11.9.0", "marked": "^15.0.0", "marked-gfm-heading-id": "^4.1.0", - "marked-highlight": "^2.2.0", - "pagx-viewer": "file:../pagx-viewer" + "marked-highlight": "^2.2.0" }, "devDependencies": { "@rollup/plugin-alias": "~5.1.1", "@rollup/plugin-commonjs": "~28.0.3", "@rollup/plugin-json": "~6.1.0", "@rollup/plugin-node-resolve": "~16.0.1", + "@rollup/plugin-terser": "~0.4.4", "@types/emscripten": "~1.39.6", - "esbuild": "~0.15.14", + "esbuild": "~0.25.0", "rimraf": "~5.0.10", - "rollup": "~2.79.1", - "rollup-plugin-esbuild": "~4.10.3", - "rollup-plugin-terser": "~7.0.2", + "rollup": "~4.18.0", + "rollup-plugin-esbuild": "~6.1.1", "tslib": "~2.4.1", "typescript": "~5.0.3" } }, "../pagx-viewer": { "version": "1.0.0", + "extraneous": true, "license": "Apache-2.0", "devDependencies": { "@rollup/plugin-alias": "~5.1.1", "@rollup/plugin-commonjs": "~28.0.3", "@rollup/plugin-node-resolve": "~16.0.1", + "@rollup/plugin-terser": "~0.4.4", "@rollup/plugin-typescript": "~11.1.6", "@types/emscripten": "~1.39.6", "rimraf": "~5.0.10", @@ -48,37 +49,46 @@ "typescript": "~5.0.3" } }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", - "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", @@ -87,973 +97,1285 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", - "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ - "loong64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14" + "node": ">=18" } }, - "node_modules/@rollup/plugin-alias": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz", - "integrity": "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@rollup/plugin-commonjs": { - "version": "28.0.9", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.9.tgz", - "integrity": "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@rollup/plugin-json": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", - "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.1.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", - "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@types/emscripten": { - "version": "1.39.13", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.13.tgz", - "integrity": "sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~7.19.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">= 0.6" + "node": ">=18" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=0.4.0" + "node": ">=18" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=18" } }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=18" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=18" } }, - "node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.8" + "node": ">=18" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6.0.0" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", + "optional": true, "engines": { - "node": ">= 0.6" + "node": ">=14" } }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/@rollup/plugin-alias": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz", + "integrity": "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.9", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.9.tgz", + "integrity": "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" }, "engines": { - "node": ">= 8" + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "2.0.0" + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", "dev": true, "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, "engines": { - "node": ">= 0.8" + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node_modules/@rollup/plugin-terser/node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz", + "integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz", + "integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz", + "integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz", + "integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.8" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz", + "integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "engines": { - "node": ">= 0.4" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz", + "integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], "license": "MIT", - "engines": { - "node": ">= 0.4" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz", + "integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz", + "integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz", + "integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz", + "integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz", + "integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz", + "integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz", + "integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz", + "integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz", + "integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz", + "integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/emscripten": { + "version": "1.39.13", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.13.tgz", + "integrity": "sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw==", "dev": true, "license": "MIT" }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">= 0.4" + "node": ">=0.4.0" } }, - "node_modules/esbuild": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", - "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { "node": ">=12" }, - "optionalDependencies": { - "@esbuild/android-arm": "0.15.18", - "@esbuild/linux-loong64": "0.15.18", - "esbuild-android-64": "0.15.18", - "esbuild-android-arm64": "0.15.18", - "esbuild-darwin-64": "0.15.18", - "esbuild-darwin-arm64": "0.15.18", - "esbuild-freebsd-64": "0.15.18", - "esbuild-freebsd-arm64": "0.15.18", - "esbuild-linux-32": "0.15.18", - "esbuild-linux-64": "0.15.18", - "esbuild-linux-arm": "0.15.18", - "esbuild-linux-arm64": "0.15.18", - "esbuild-linux-mips64le": "0.15.18", - "esbuild-linux-ppc64le": "0.15.18", - "esbuild-linux-riscv64": "0.15.18", - "esbuild-linux-s390x": "0.15.18", - "esbuild-netbsd-64": "0.15.18", - "esbuild-openbsd-64": "0.15.18", - "esbuild-sunos-64": "0.15.18", - "esbuild-windows-32": "0.15.18", - "esbuild-windows-64": "0.15.18", - "esbuild-windows-arm64": "0.15.18" - } - }, - "node_modules/esbuild-android-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", - "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", - "cpu": [ - "x64" - ], + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/esbuild-android-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", - "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", - "cpu": [ - "arm64" - ], + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, "engines": { - "node": ">=12" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/esbuild-darwin-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", - "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", - "cpu": [ - "x64" - ], + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/esbuild-darwin-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", - "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", - "cpu": [ - "arm64" - ], + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=12" + "node": ">= 0.8" } }, - "node_modules/esbuild-freebsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", - "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" } }, - "node_modules/esbuild-freebsd-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", - "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esbuild-linux-32": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", - "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", - "cpu": [ - "ia32" - ], + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": ">=12" + "node": ">=7.0.0" } }, - "node_modules/esbuild-linux-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", - "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", - "cpu": [ - "x64" - ], + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "safe-buffer": "5.2.1" + }, "engines": { - "node": ">=12" + "node": ">= 0.6" } }, - "node_modules/esbuild-linux-arm": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", - "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", - "cpu": [ - "arm" - ], - "dev": true, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">= 0.6" } }, - "node_modules/esbuild-linux-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", - "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">= 0.6" } }, - "node_modules/esbuild-linux-mips64le": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", - "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", - "cpu": [ - "mips64el" - ], + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" } }, - "node_modules/esbuild-linux-ppc64le": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", - "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", - "cpu": [ - "ppc64" - ], + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/esbuild-linux-riscv64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", - "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", - "cpu": [ - "riscv64" - ], - "dev": true, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">= 0.8" } }, - "node_modules/esbuild-linux-s390x": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", - "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", - "cpu": [ - "s390x" - ], - "dev": true, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/esbuild-netbsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", - "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" } }, - "node_modules/esbuild-openbsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", - "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", - "cpu": [ - "x64" - ], + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], "engines": { - "node": ">=12" + "node": ">= 0.8" } }, - "node_modules/esbuild-sunos-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", - "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], "engines": { - "node": ">=12" + "node": ">= 0.4" } }, - "node_modules/esbuild-windows-32": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", - "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", - "cpu": [ - "ia32" - ], - "dev": true, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=12" + "node": ">= 0.4" } }, - "node_modules/esbuild-windows-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", - "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", - "cpu": [ - "x64" - ], + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "es-errors": "^1.3.0" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" } }, - "node_modules/esbuild-windows-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", - "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", - "cpu": [ - "arm64" - ], + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escape-html": { @@ -1256,6 +1578,19 @@ "node": ">= 0.4" } }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -1296,16 +1631,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1452,45 +1777,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -1568,13 +1854,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -1689,10 +1968,6 @@ "dev": true, "license": "BlueOak-1.0.0" }, - "node_modules/pagx-viewer": { - "resolved": "../pagx-viewer", - "link": true - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1742,13 +2017,6 @@ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -1846,6 +2114,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", @@ -1863,54 +2141,59 @@ } }, "node_modules/rollup": { - "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz", + "integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==", "dev": true, "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.1", + "@rollup/rollup-android-arm64": "4.18.1", + "@rollup/rollup-darwin-arm64": "4.18.1", + "@rollup/rollup-darwin-x64": "4.18.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.1", + "@rollup/rollup-linux-arm-musleabihf": "4.18.1", + "@rollup/rollup-linux-arm64-gnu": "4.18.1", + "@rollup/rollup-linux-arm64-musl": "4.18.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.1", + "@rollup/rollup-linux-riscv64-gnu": "4.18.1", + "@rollup/rollup-linux-s390x-gnu": "4.18.1", + "@rollup/rollup-linux-x64-gnu": "4.18.1", + "@rollup/rollup-linux-x64-musl": "4.18.1", + "@rollup/rollup-win32-arm64-msvc": "4.18.1", + "@rollup/rollup-win32-ia32-msvc": "4.18.1", + "@rollup/rollup-win32-x64-msvc": "4.18.1", "fsevents": "~2.3.2" } }, "node_modules/rollup-plugin-esbuild": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-4.10.3.tgz", - "integrity": "sha512-RILwUCgnCL5vo8vyZ/ZpwcqRuE5KmLizEv6BujBQfgXFZ6ggcS0FiYvQN+gsTJfWCMaU37l0Fosh4eEufyO97Q==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-6.1.1.tgz", + "integrity": "sha512-CehMY9FAqJD5OUaE/Mi1r5z0kNeYxItmRO2zG4Qnv2qWKF09J2lTy5GUzjJR354ZPrLkCj4fiBN41lo8PzBUhw==", "dev": true, "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^4.1.1", - "debug": "^4.3.3", - "es-module-lexer": "^0.9.3", - "joycon": "^3.0.1", - "jsonc-parser": "^3.0.0" + "@rollup/pluginutils": "^5.0.5", + "debug": "^4.3.4", + "es-module-lexer": "^1.3.1", + "get-tsconfig": "^4.7.2" }, "engines": { - "node": ">=12" + "node": ">=14.18.0" }, "peerDependencies": { - "esbuild": ">=0.10.1", - "rollup": "^1.20.0 || ^2.0.0" - } - }, - "node_modules/rollup-plugin-esbuild/node_modules/@rollup/pluginutils": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", - "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "estree-walker": "^2.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" + "esbuild": ">=0.18.0", + "rollup": "^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" } }, "node_modules/rollup-plugin-esbuild/node_modules/debug": { @@ -1938,35 +2221,12 @@ "dev": true, "license": "MIT" }, - "node_modules/rollup-plugin-esbuild/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/rollup-plugin-terser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", - "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "jest-worker": "^26.2.1", - "serialize-javascript": "^4.0.0", - "terser": "^5.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0" - } + "license": "MIT" }, "node_modules/safe-buffer": { "version": "5.2.1", @@ -2024,16 +2284,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/serve-static": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", @@ -2163,6 +2413,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/smob": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", + "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2297,19 +2557,6 @@ "node": ">=8" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -2385,13 +2632,6 @@ "node": ">=12.20" } }, - "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", - "dev": true, - "license": "MIT" - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 9e2a6a0315..08e82a1ba8 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -57,6 +57,7 @@ #include "pagx/nodes/Group.h" #include "pagx/nodes/Image.h" #include "pagx/nodes/ImagePattern.h" +#include "pagx/nodes/InnerShadowStyle.h" #include "pagx/nodes/Layer.h" #include "pagx/nodes/LinearGradient.h" #include "pagx/nodes/NoiseFilter.h" @@ -7883,4 +7884,158 @@ PAGX_TEST(PAGXTest, ChannelNoiseStyle) { EXPECT_FLOAT_EQ(tgfxMono2->seed(), 45.0f); } +/** + * Render NoiseFilter/NoiseStyle animation frames across all modes and combined with shadows. + * Layout: two rectangles side-by-side, left with NoiseFilter, right with NoiseStyle. + * 50 frames total: 10 Mono, 10 Duo, 10 Multi, 10 Multi+DropShadowFilter, 10 Multi+InnerShadowStyle. + * Density animates from 0.1 to 1.0 across all 50 frames. + */ +PAGX_TEST(PAGXTest, ExportNoiseFilterAnimation) { + constexpr int canvasW = 500; + constexpr int canvasH = 260; + constexpr int totalFrames = 40; + constexpr int framesPerSection = 10; + + auto outDir = ProjectPath::Absolute("test/out/PAGXTest/NoiseFilterAnimation"); + auto dirPath = std::filesystem::path(outDir); + if (!std::filesystem::exists(dirPath)) { + std::filesystem::create_directories(dirPath); + } + + auto surface = Surface::Make(context, canvasW, canvasH); + ASSERT_TRUE(surface != nullptr); + + for (int i = 0; i < totalFrames; i++) { + float density = 0.1f + 0.9f * (static_cast(i) / (totalFrames - 1)); + int section = i / framesPerSection; + + auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); + + // Left rectangle with NoiseFilter + auto layerL = doc->makeNode("LL"); + layerL->width = 220; + layerL->height = 220; + layerL->matrix = pagx::Matrix::Translate(10, 20); + auto rectL = doc->makeNode(); + rectL->position = {110, 110}; + rectL->size = {220, 220}; + rectL->roundness = 12; + auto fillL = doc->makeNode(); + auto solidL = doc->makeNode(); + solidL->color = {0.9f, 0.9f, 0.9f, 1.0f}; + fillL->color = solidL; + layerL->contents.push_back(rectL); + layerL->contents.push_back(fillL); + + auto noiseFilter = doc->makeNode("NF"); + noiseFilter->size = 8; + noiseFilter->density = density; + noiseFilter->seed = 42; + switch (section) { + case 0: + noiseFilter->mode = pagx::NoiseMode::Mono; + noiseFilter->color = {0.0f, 0.0f, 0.0f, 1.0f}; + break; + case 1: + noiseFilter->mode = pagx::NoiseMode::Duo; + noiseFilter->firstColor = {1.0f, 0.2f, 0.0f, 1.0f}; + noiseFilter->secondColor = {0.0f, 0.2f, 1.0f, 1.0f}; + break; + default: + noiseFilter->mode = pagx::NoiseMode::Multi; + noiseFilter->opacity = 0.8f; + break; + } + layerL->filters.push_back(noiseFilter); + + if (section == 3) { + auto shadowL = doc->makeNode(); + shadowL->offsetX = 4; + shadowL->offsetY = 4; + shadowL->blurX = 6; + shadowL->blurY = 6; + shadowL->color = {0.0f, 0.0f, 0.0f, 0.6f}; + layerL->filters.push_back(shadowL); + + auto innerShadowL = doc->makeNode(); + innerShadowL->offsetX = 3; + innerShadowL->offsetY = 3; + innerShadowL->blurX = 8; + innerShadowL->blurY = 8; + innerShadowL->color = {0.0f, 0.0f, 0.0f, 0.5f}; + layerL->styles.push_back(innerShadowL); + } + + doc->layers.push_back(layerL); + + // Right rectangle with NoiseStyle + auto layerR = doc->makeNode("LR"); + layerR->width = 220; + layerR->height = 220; + layerR->matrix = pagx::Matrix::Translate(270, 20); + auto rectR = doc->makeNode(); + rectR->position = {110, 110}; + rectR->size = {220, 220}; + rectR->roundness = 12; + auto fillR = doc->makeNode(); + auto solidR = doc->makeNode(); + solidR->color = {0.9f, 0.9f, 0.9f, 1.0f}; + fillR->color = solidR; + layerR->contents.push_back(rectR); + layerR->contents.push_back(fillR); + + auto noiseStyle = doc->makeNode("NS"); + noiseStyle->size = 8; + noiseStyle->density = density; + noiseStyle->seed = 42; + switch (section) { + case 0: + noiseStyle->mode = pagx::NoiseMode::Mono; + noiseStyle->color = {0.0f, 0.0f, 0.0f, 1.0f}; + break; + case 1: + noiseStyle->mode = pagx::NoiseMode::Duo; + noiseStyle->firstColor = {1.0f, 0.2f, 0.0f, 1.0f}; + noiseStyle->secondColor = {0.0f, 0.2f, 1.0f, 1.0f}; + break; + default: + noiseStyle->mode = pagx::NoiseMode::Multi; + noiseStyle->opacity = 0.8f; + break; + } + layerR->styles.push_back(noiseStyle); + + if (section == 3) { + auto shadowR = doc->makeNode(); + shadowR->offsetX = 4; + shadowR->offsetY = 4; + shadowR->blurX = 6; + shadowR->blurY = 6; + shadowR->color = {0.0f, 0.0f, 0.0f, 0.6f}; + layerR->filters.push_back(shadowR); + + auto innerShadowR = doc->makeNode(); + innerShadowR->offsetX = 3; + innerShadowR->offsetY = 3; + innerShadowR->blurX = 8; + innerShadowR->blurY = 8; + innerShadowR->color = {0.0f, 0.0f, 0.0f, 0.5f}; + layerR->styles.push_back(innerShadowR); + } + + doc->layers.push_back(layerR); + + doc->applyLayout(); + auto tgfxRoot = pagx::LayerBuilder::Build(doc.get()); + ASSERT_TRUE(tgfxRoot != nullptr); + + tgfx::DisplayList displayList; + displayList.root()->addChild(tgfxRoot); + displayList.render(surface.get(), false); + + auto key = "PAGXTest/NoiseFilterAnimation/frame_" + std::to_string(i); + EXPECT_TRUE(Baseline::Compare(surface, key)); + } +} + } // namespace pag From d82a742e5e8056a95da4b83700d493ac2e0dcb0d Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Wed, 10 Jun 2026 20:26:55 +0800 Subject: [PATCH 36/52] Accept screenshot baseline for NoiseFilter animation export test. --- test/baseline/version.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/baseline/version.json b/test/baseline/version.json index 0fb818ec5b..1370341721 100644 --- a/test/baseline/version.json +++ b/test/baseline/version.json @@ -8559,6 +8559,20 @@ "PAGXTest": { "LayerBuilderAPIConsistency": "f40e23b6", "NoiseFilterAllElements": "798f40c4", + "NoiseFilterAnimation": { + "frame_0": "8d43445d", + "frame_1": "8d43445d", + "frame_10": "8d43445d", + "frame_11": "8d43445d", + "frame_2": "8d43445d", + "frame_3": "8d43445d", + "frame_4": "8d43445d", + "frame_5": "8d43445d", + "frame_6": "8d43445d", + "frame_7": "8d43445d", + "frame_8": "8d43445d", + "frame_9": "8d43445d" + }, "NoiseFilterModes": "798f40c4", "NoiseStyleModes": "1af07597", "PrecomposedTextRender": "9b6dea6a", From 74d5b8ce4593a22d937b24a230f6ef8046987bb9 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Wed, 10 Jun 2026 20:33:51 +0800 Subject: [PATCH 37/52] Revert package-lock.json to origin/main version. --- playground/pagx-playground/package-lock.json | 1526 ++++++++---------- 1 file changed, 643 insertions(+), 883 deletions(-) diff --git a/playground/pagx-playground/package-lock.json b/playground/pagx-playground/package-lock.json index 65a68aa89c..ccc324ba38 100644 --- a/playground/pagx-playground/package-lock.json +++ b/playground/pagx-playground/package-lock.json @@ -13,32 +13,31 @@ "highlight.js": "^11.9.0", "marked": "^15.0.0", "marked-gfm-heading-id": "^4.1.0", - "marked-highlight": "^2.2.0" + "marked-highlight": "^2.2.0", + "pagx-viewer": "file:../pagx-viewer" }, "devDependencies": { "@rollup/plugin-alias": "~5.1.1", "@rollup/plugin-commonjs": "~28.0.3", "@rollup/plugin-json": "~6.1.0", "@rollup/plugin-node-resolve": "~16.0.1", - "@rollup/plugin-terser": "~0.4.4", "@types/emscripten": "~1.39.6", - "esbuild": "~0.25.0", + "esbuild": "~0.15.14", "rimraf": "~5.0.10", - "rollup": "~4.18.0", - "rollup-plugin-esbuild": "~6.1.1", + "rollup": "~2.79.1", + "rollup-plugin-esbuild": "~4.10.3", + "rollup-plugin-terser": "~7.0.2", "tslib": "~2.4.1", "typescript": "~5.0.3" } }, "../pagx-viewer": { "version": "1.0.0", - "extraneous": true, "license": "Apache-2.0", "devDependencies": { "@rollup/plugin-alias": "~5.1.1", "@rollup/plugin-commonjs": "~28.0.3", "@rollup/plugin-node-resolve": "~16.0.1", - "@rollup/plugin-terser": "~0.4.4", "@rollup/plugin-typescript": "~11.1.6", "@types/emscripten": "~1.39.6", "rimraf": "~5.0.10", @@ -49,146 +48,35 @@ "typescript": "~5.0.3" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", "cpu": [ "arm" ], @@ -196,50 +84,16 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" + "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", "cpu": [ "loong64" ], @@ -250,245 +104,7 @@ "linux" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@isaacs/cliui": { @@ -661,39 +277,6 @@ } } }, - "node_modules/@rollup/plugin-terser": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "serialize-javascript": "^6.0.1", - "smob": "^1.0.0", - "terser": "^5.17.4" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-terser/node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/@rollup/pluginutils": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", @@ -709,264 +292,13 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz", - "integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz", - "integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz", - "integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz", - "integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz", - "integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz", - "integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz", - "integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz", - "integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz", - "integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz", - "integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz", - "integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz", - "integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz", - "integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz", - "integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz", - "integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz", - "integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } }, "node_modules/@types/emscripten": { "version": "1.39.13", @@ -982,6 +314,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -1234,148 +576,484 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=0.10.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.8" + "node": ">=12" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=12" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 0.8" + "node": ">=12" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "node": ">=12" } }, "node_modules/escape-html": { @@ -1578,19 +1256,6 @@ "node": ">= 0.4" } }, - "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -1631,6 +1296,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1777,6 +1452,45 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -1854,6 +1568,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -1968,6 +1689,10 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pagx-viewer": { + "resolved": "../pagx-viewer", + "link": true + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2017,6 +1742,13 @@ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -2114,16 +1846,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", @@ -2141,59 +1863,54 @@ } }, "node_modules/rollup": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz", - "integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==", + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "1.0.5" - }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": ">=10.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.1", - "@rollup/rollup-android-arm64": "4.18.1", - "@rollup/rollup-darwin-arm64": "4.18.1", - "@rollup/rollup-darwin-x64": "4.18.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.1", - "@rollup/rollup-linux-arm-musleabihf": "4.18.1", - "@rollup/rollup-linux-arm64-gnu": "4.18.1", - "@rollup/rollup-linux-arm64-musl": "4.18.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.1", - "@rollup/rollup-linux-riscv64-gnu": "4.18.1", - "@rollup/rollup-linux-s390x-gnu": "4.18.1", - "@rollup/rollup-linux-x64-gnu": "4.18.1", - "@rollup/rollup-linux-x64-musl": "4.18.1", - "@rollup/rollup-win32-arm64-msvc": "4.18.1", - "@rollup/rollup-win32-ia32-msvc": "4.18.1", - "@rollup/rollup-win32-x64-msvc": "4.18.1", "fsevents": "~2.3.2" } }, "node_modules/rollup-plugin-esbuild": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-6.1.1.tgz", - "integrity": "sha512-CehMY9FAqJD5OUaE/Mi1r5z0kNeYxItmRO2zG4Qnv2qWKF09J2lTy5GUzjJR354ZPrLkCj4fiBN41lo8PzBUhw==", + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-4.10.3.tgz", + "integrity": "sha512-RILwUCgnCL5vo8vyZ/ZpwcqRuE5KmLizEv6BujBQfgXFZ6ggcS0FiYvQN+gsTJfWCMaU37l0Fosh4eEufyO97Q==", "dev": true, "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^5.0.5", - "debug": "^4.3.4", - "es-module-lexer": "^1.3.1", - "get-tsconfig": "^4.7.2" + "@rollup/pluginutils": "^4.1.1", + "debug": "^4.3.3", + "es-module-lexer": "^0.9.3", + "joycon": "^3.0.1", + "jsonc-parser": "^3.0.0" }, "engines": { - "node": ">=14.18.0" + "node": ">=12" }, "peerDependencies": { - "esbuild": ">=0.18.0", - "rollup": "^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + "esbuild": ">=0.10.1", + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/rollup-plugin-esbuild/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" } }, "node_modules/rollup-plugin-esbuild/node_modules/debug": { @@ -2221,12 +1938,35 @@ "dev": true, "license": "MIT" }, - "node_modules/rollup/node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "node_modules/rollup-plugin-esbuild/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0" + } }, "node_modules/safe-buffer": { "version": "5.2.1", @@ -2284,6 +2024,16 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serve-static": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", @@ -2413,16 +2163,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/smob": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", - "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2557,6 +2297,19 @@ "node": ">=8" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -2632,6 +2385,13 @@ "node": ">=12.20" } }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", From 126228e099db8ec17bd80b23eae79331f55a8cef Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Wed, 10 Jun 2026 20:51:46 +0800 Subject: [PATCH 38/52] Reduce noise animation export test from 40 frames to 12 frames. --- test/src/PAGXTest.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 08e82a1ba8..bf36f23088 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7887,14 +7887,14 @@ PAGX_TEST(PAGXTest, ChannelNoiseStyle) { /** * Render NoiseFilter/NoiseStyle animation frames across all modes and combined with shadows. * Layout: two rectangles side-by-side, left with NoiseFilter, right with NoiseStyle. - * 50 frames total: 10 Mono, 10 Duo, 10 Multi, 10 Multi+DropShadowFilter, 10 Multi+InnerShadowStyle. - * Density animates from 0.1 to 1.0 across all 50 frames. + * 12 frames total: 3 Mono, 3 Duo, 3 Multi, 3 Multi+DropShadowFilter+InnerShadowStyle. + * Density animates from 0.1 to 1.0 across all 12 frames. */ PAGX_TEST(PAGXTest, ExportNoiseFilterAnimation) { constexpr int canvasW = 500; constexpr int canvasH = 260; - constexpr int totalFrames = 40; - constexpr int framesPerSection = 10; + constexpr int totalFrames = 12; + constexpr int framesPerSection = 3; auto outDir = ProjectPath::Absolute("test/out/PAGXTest/NoiseFilterAnimation"); auto dirPath = std::filesystem::path(outDir); From fdb3cb9aaacf339aedb520f669cc39c11c390a5a Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 15 Jun 2026 13:46:00 +0800 Subject: [PATCH 39/52] Refactor noise SVG export and fix mode-specific channel bindings with blend mode support. --- include/pagx/nodes/NoiseFilter.h | 17 +- include/pagx/nodes/NoiseStyle.h | 17 +- include/pagx/nodes/TextBox.h | 2 +- src/pagx/svg/SVGExporter.cpp | 290 ++++++++++--------------------- src/renderer/LayerBuilder.cpp | 32 +++- test/baseline/version.json | 3 +- test/src/PAGXTest.cpp | 105 ++++++++++- 7 files changed, 242 insertions(+), 224 deletions(-) diff --git a/include/pagx/nodes/NoiseFilter.h b/include/pagx/nodes/NoiseFilter.h index 3d189c5080..d01db85695 100644 --- a/include/pagx/nodes/NoiseFilter.h +++ b/include/pagx/nodes/NoiseFilter.h @@ -28,7 +28,8 @@ namespace pagx { /** * A noise filter that overlays procedural Perlin noise on the layer. Three noise modes are * available: Mono (single color), Duo (two complementary colors), and Multi (preserving original - * noise RGB with enhanced contrast). + * noise RGB with enhanced contrast). Mode-specific fields for inactive modes are ignored; set the + * fields for the new mode after changing mode. */ class NoiseFilter : public LayerFilter { public: @@ -60,22 +61,26 @@ class NoiseFilter : public LayerFilter { BlendMode blendMode = BlendMode::Normal; /** - * The noise color for Mono mode. The alpha component controls the noise opacity. + * The noise color for Mono mode. Ignored by Duo and Multi modes. The alpha component controls the + * noise opacity. The default value is black. */ Color color = {}; /** - * The first noise color for Duo mode. The alpha component controls its opacity. + * The first noise color for Duo mode. Ignored by Mono and Multi modes. The alpha component + * controls its opacity. The default value is black. */ Color firstColor = {}; /** - * The second noise color for Duo mode. The alpha component controls its opacity. + * The second noise color for Duo mode. Ignored by Mono and Multi modes. The alpha component + * controls its opacity. The default value is white. */ - Color secondColor = {}; + Color secondColor = {1.0f, 1.0f, 1.0f, 1.0f}; /** - * The overall noise opacity for Multi mode, in [0, 1]. The default value is 0.15. + * The overall noise opacity for Multi mode, in [0, 1]. Ignored by Mono and Duo modes. The default + * value is 0.15. */ float opacity = 0.15f; diff --git a/include/pagx/nodes/NoiseStyle.h b/include/pagx/nodes/NoiseStyle.h index ad5f37c919..d483a37138 100644 --- a/include/pagx/nodes/NoiseStyle.h +++ b/include/pagx/nodes/NoiseStyle.h @@ -27,7 +27,8 @@ namespace pagx { /** * A noise layer style that overlays procedural Perlin noise above the layer content. Three noise * modes are available: Mono (single color), Duo (two complementary colors), and Multi (preserving - * original noise RGB with enhanced contrast). + * original noise RGB with enhanced contrast). Mode-specific fields for inactive modes are ignored; + * set the fields for the new mode after changing mode. */ class NoiseStyle : public LayerStyle { public: @@ -54,22 +55,26 @@ class NoiseStyle : public LayerStyle { float seed = 0.0f; /** - * The noise color for Mono mode. The alpha component controls the noise opacity. + * The noise color for Mono mode. Ignored by Duo and Multi modes. The alpha component controls the + * noise opacity. The default value is black. */ Color color = {}; /** - * The first noise color for Duo mode. The alpha component controls its opacity. + * The first noise color for Duo mode. Ignored by Mono and Multi modes. The alpha component + * controls its opacity. The default value is black. */ Color firstColor = {}; /** - * The second noise color for Duo mode. The alpha component controls its opacity. + * The second noise color for Duo mode. Ignored by Mono and Multi modes. The alpha component + * controls its opacity. The default value is white. */ - Color secondColor = {}; + Color secondColor = {1.0f, 1.0f, 1.0f, 1.0f}; /** - * The overall noise opacity for Multi mode, in [0, 1]. The default value is 0.15. + * The overall noise opacity for Multi mode, in [0, 1]. Ignored by Mono and Duo modes. The default + * value is 0.15. */ float opacity = 0.15f; diff --git a/include/pagx/nodes/TextBox.h b/include/pagx/nodes/TextBox.h index 00f16896b7..a800354569 100644 --- a/include/pagx/nodes/TextBox.h +++ b/include/pagx/nodes/TextBox.h @@ -90,7 +90,7 @@ class TextBox : public Group { return NodeType::TextBox; } -protected: + protected: void onMeasure(LayoutContext* context) override; void setLayoutSize(LayoutContext* context, float targetWidth, float targetHeight) override; void updateLayout(LayoutContext* context) override; diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index 4e60c23597..1081602b4a 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -408,10 +408,11 @@ class SVGWriter { void writeColorMatrixFilter(const ColorMatrixFilter* cm, int& colorMatrixIndex, std::string& currentSource); void writeBlendFilter(const BlendFilter* blend, int& shadowIndex, std::string& currentSource); - std::string writeNoiseTurbulence(const NoiseFilter* noise, const std::string& resultName); - std::string writeNoiseTurbulence(const NoiseStyle* noise, const std::string& resultName); - std::string writeNoiseBand(const NoiseFilter* noise, bool isDark, const std::string& label); - std::string writeNoiseBand(const NoiseStyle* noise, bool isDark, const std::string& label); + std::string writeNoiseTurbulence(float size, float seed, const std::string& resultName); + std::string writeNoiseBand(float size, float density, float seed, bool isDark, + const std::string& label); + std::string writeNoiseMultiCore(float size, float density, float seed, float opacity, + const std::string& id); std::string writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, std::string& currentSource); // Collected per-filter state fed into the final feMerge aggregation. @@ -988,23 +989,24 @@ void SVGWriter::writeBlendFilter(const BlendFilter* blend, int& shadowIndex, // SVGWriter – noise filter primitives //============================================================================== -std::string SVGWriter::writeNoiseTurbulence(const NoiseFilter* noise, - const std::string& resultName) { - auto freq = noise->size > 0.0f ? 1.0f / noise->size : 0.25f; +std::string SVGWriter::writeNoiseTurbulence(float size, float seed, const std::string& resultName) { + // SVG filter primitives operate in document units and do not receive the runtime content scale + // used by GPU rasterization, so exported noise frequency intentionally derives from authoring size. + auto freq = size > 0.0f ? 1.0f / size : 0.25f; _defs->openElement("feTurbulence"); _defs->addAttribute("type", "fractalNoise"); _defs->addAttribute("baseFrequency", FloatToString(freq)); _defs->addAttribute("stitchTiles", "stitch"); _defs->addAttribute("numOctaves", "3"); - _defs->addAttribute("seed", FloatToString(noise->seed)); + _defs->addAttribute("seed", FloatToString(seed)); _defs->addAttribute("result", resultName); _defs->closeElementSelfClosing(); return resultName; } -std::string SVGWriter::writeNoiseBand(const NoiseFilter* noise, bool isDark, +std::string SVGWriter::writeNoiseBand(float size, float density, float seed, bool isDark, const std::string& label) { - auto turbResult = writeNoiseTurbulence(noise, "turb" + label); + auto turbResult = writeNoiseTurbulence(size, seed, "turb" + label); _defs->openElement("feColorMatrix"); _defs->addAttribute("in", turbResult); @@ -1012,7 +1014,7 @@ std::string SVGWriter::writeNoiseBand(const NoiseFilter* noise, bool isDark, _defs->addAttribute("result", "luma" + label); _defs->closeElementSelfClosing(); - auto d = std::clamp(noise->density, 0.0f, 1.0f); + auto d = std::clamp(density, 0.0f, 1.0f); int lower = 0; int upper = 0; if (isDark) { @@ -1041,40 +1043,34 @@ std::string SVGWriter::writeNoiseBand(const NoiseFilter* noise, bool isDark, return "band" + label; } -std::string SVGWriter::writeNoiseTurbulence(const NoiseStyle* noise, - const std::string& resultName) { - auto freq = noise->size > 0.0f ? 1.0f / noise->size : 0.25f; - _defs->openElement("feTurbulence"); - _defs->addAttribute("type", "fractalNoise"); - _defs->addAttribute("baseFrequency", FloatToString(freq)); - _defs->addAttribute("stitchTiles", "stitch"); - _defs->addAttribute("numOctaves", "3"); - _defs->addAttribute("seed", FloatToString(noise->seed)); - _defs->addAttribute("result", resultName); - _defs->closeElementSelfClosing(); - return resultName; -} - -std::string SVGWriter::writeNoiseBand(const NoiseStyle* noise, bool isDark, - const std::string& label) { - auto turbResult = writeNoiseTurbulence(noise, "turb" + label); +std::string SVGWriter::writeNoiseMultiCore(float size, float density, float seed, float opacity, + const std::string& id) { + auto turbResult = writeNoiseTurbulence(size, seed, "n" + id); - _defs->openElement("feColorMatrix"); + _defs->openElement("feComponentTransfer"); _defs->addAttribute("in", turbResult); - _defs->addAttribute("type", "luminanceToAlpha"); - _defs->addAttribute("result", "luma" + label); + _defs->addAttribute("result", "contrast" + id); + _defs->closeElementStart(); + _defs->openElement("feFuncR"); + _defs->addAttribute("type", "linear"); + _defs->addAttribute("slope", "2"); + _defs->addAttribute("intercept", "-0.5"); _defs->closeElementSelfClosing(); + _defs->openElement("feFuncG"); + _defs->addAttribute("type", "linear"); + _defs->addAttribute("slope", "2"); + _defs->addAttribute("intercept", "-0.5"); + _defs->closeElementSelfClosing(); + _defs->openElement("feFuncB"); + _defs->addAttribute("type", "linear"); + _defs->addAttribute("slope", "2"); + _defs->addAttribute("intercept", "-0.5"); + _defs->closeElementSelfClosing(); + _defs->closeElement(); - auto d = std::clamp(noise->density, 0.0f, 1.0f); - int lower = 0; - int upper = 0; - if (isDark) { - lower = std::clamp(static_cast(std::lround(-25.0f * d + 25.0f)), 0, 99); - upper = std::clamp(static_cast(std::lround(24.0f * d + 25.0f)), 0, 99); - } else { - lower = std::clamp(static_cast(std::lround(-24.0f * d + 74.0f)), 0, 99); - upper = std::clamp(static_cast(std::lround(25.0f * d + 74.0f)), 0, 99); - } + auto d = std::clamp(density, 0.0f, 1.0f); + int lower = std::clamp(static_cast(std::lround(-25.0f * d + 25.0f)), 0, 99); + int upper = std::clamp(static_cast(std::lround(24.0f * d + 25.0f)), 0, 99); std::string table; table.reserve(300); for (int i = 0; i < 100; i++) { @@ -1082,16 +1078,39 @@ std::string SVGWriter::writeNoiseBand(const NoiseStyle* noise, bool isDark, } table.pop_back(); + _defs->openElement("feColorMatrix"); + _defs->addAttribute("in", turbResult); + _defs->addAttribute("type", "luminanceToAlpha"); + _defs->addAttribute("result", "luma" + id); + _defs->closeElementSelfClosing(); + _defs->openElement("feComponentTransfer"); - _defs->addAttribute("in", "luma" + label); - _defs->addAttribute("result", "band" + label); + _defs->addAttribute("in", "luma" + id); + _defs->addAttribute("result", "band" + id); _defs->closeElementStart(); _defs->openElement("feFuncA"); _defs->addAttribute("type", "discrete"); _defs->addAttribute("tableValues", table); _defs->closeElementSelfClosing(); _defs->closeElement(); - return "band" + label; + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "contrast" + id); + _defs->addAttribute("in2", "band" + id); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "masked" + id); + _defs->closeElementSelfClosing(); + + _defs->openElement("feColorMatrix"); + _defs->addAttribute("in", "masked" + id); + _defs->addAttribute("type", "matrix"); + std::string opacityValues = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 "; + opacityValues += FloatToString(opacity); + opacityValues += " 0"; + _defs->addAttribute("values", opacityValues); + _defs->addAttribute("result", "final" + id); + _defs->closeElementSelfClosing(); + return "final" + id; } std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, @@ -1099,7 +1118,7 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde std::string filterId = "noise" + std::to_string(noiseIndex++); if (noise->mode == NoiseMode::Mono) { - auto band = writeNoiseBand(noise, true, "Dark" + filterId); + auto band = writeNoiseBand(noise->size, noise->density, noise->seed, true, "Dark" + filterId); _defs->openElement("feFlood"); _defs->addAttribute("flood-color", ColorToSVGString(noise->color)); if (noise->color.alpha < 1.0f) { @@ -1137,8 +1156,9 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde } if (noise->mode == NoiseMode::Duo) { - auto dark = writeNoiseBand(noise, true, "Dark" + filterId); - auto bright = writeNoiseBand(noise, false, "Bright" + filterId); + auto dark = writeNoiseBand(noise->size, noise->density, noise->seed, true, "Dark" + filterId); + auto bright = + writeNoiseBand(noise->size, noise->density, noise->seed, false, "Bright" + filterId); _defs->openElement("feComposite"); _defs->addAttribute("in", dark); @@ -1205,81 +1225,11 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde return resultName; } - // Multi mode: single feTurbulence processed through two branches — contrast (RGB) and - // luminance-based density band (alpha) — then masked and blended, aligned with tgfx's - // MultiNoiseFilter::onBuildBaseShader via MakeDarkDensityFilter. - auto turbResult = writeNoiseTurbulence(noise, "n" + filterId); - - // Contrast branch: feFuncR/G/B linear applies slope=2 intercept=-0.5, keeping alpha unchanged. - _defs->openElement("feComponentTransfer"); - _defs->addAttribute("in", turbResult); - _defs->addAttribute("result", "contrast" + filterId); - _defs->closeElementStart(); - _defs->openElement("feFuncR"); - _defs->addAttribute("type", "linear"); - _defs->addAttribute("slope", "2"); - _defs->addAttribute("intercept", "-0.5"); - _defs->closeElementSelfClosing(); - _defs->openElement("feFuncG"); - _defs->addAttribute("type", "linear"); - _defs->addAttribute("slope", "2"); - _defs->addAttribute("intercept", "-0.5"); - _defs->closeElementSelfClosing(); - _defs->openElement("feFuncB"); - _defs->addAttribute("type", "linear"); - _defs->addAttribute("slope", "2"); - _defs->addAttribute("intercept", "-0.5"); - _defs->closeElementSelfClosing(); - _defs->closeElement(); - - // Density band branch: luminance-to-alpha then discrete band, matching MakeDarkDensityFilter - // which uses ComputeDarkBandBuckets, lumaMatrix + AlphaThreshold, and DstOut(lower, upper). - auto d = std::clamp(noise->density, 0.0f, 1.0f); - int lower = std::clamp(static_cast(std::lround(-25.0f * d + 25.0f)), 0, 99); - int upper = std::clamp(static_cast(std::lround(24.0f * d + 25.0f)), 0, 99); - std::string table; - table.reserve(300); - for (int i = 0; i < 100; i++) { - table += (i >= lower && i <= upper) ? "1 " : "0 "; - } - table.pop_back(); - - _defs->openElement("feColorMatrix"); - _defs->addAttribute("in", turbResult); - _defs->addAttribute("type", "luminanceToAlpha"); - _defs->addAttribute("result", "luma" + filterId); - _defs->closeElementSelfClosing(); - - _defs->openElement("feComponentTransfer"); - _defs->addAttribute("in", "luma" + filterId); - _defs->addAttribute("result", "band" + filterId); - _defs->closeElementStart(); - _defs->openElement("feFuncA"); - _defs->addAttribute("type", "discrete"); - _defs->addAttribute("tableValues", table); - _defs->closeElementSelfClosing(); - _defs->closeElement(); - - _defs->openElement("feComposite"); - _defs->addAttribute("in", "contrast" + filterId); - _defs->addAttribute("in2", "band" + filterId); - _defs->addAttribute("operator", "in"); - _defs->addAttribute("result", "masked" + filterId); - _defs->closeElementSelfClosing(); - - // Apply opacity scaling to the masked noise alpha channel. - _defs->openElement("feColorMatrix"); - _defs->addAttribute("in", "masked" + filterId); - _defs->addAttribute("type", "matrix"); - std::string opacityValues = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 "; - opacityValues += FloatToString(noise->opacity); - opacityValues += " 0"; - _defs->addAttribute("values", opacityValues); - _defs->addAttribute("result", "final" + filterId); - _defs->closeElementSelfClosing(); + auto final = + writeNoiseMultiCore(noise->size, noise->density, noise->seed, noise->opacity, filterId); _defs->openElement("feComposite"); - _defs->addAttribute("in", "final" + filterId); + _defs->addAttribute("in", final); _defs->addAttribute("in2", currentSource); _defs->addAttribute("operator", "in"); _defs->addAttribute("result", "clipped" + filterId); @@ -1303,7 +1253,7 @@ std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleI std::string styleId = "noiseStyle" + std::to_string(noiseStyleIndex++); if (noise->mode == NoiseMode::Mono) { - auto band = writeNoiseBand(noise, true, "Dark" + styleId); + auto band = writeNoiseBand(noise->size, noise->density, noise->seed, true, "Dark" + styleId); _defs->openElement("feFlood"); _defs->addAttribute("flood-color", ColorToSVGString(noise->color)); if (noise->color.alpha < 1.0f) { @@ -1330,8 +1280,9 @@ std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleI } if (noise->mode == NoiseMode::Duo) { - auto dark = writeNoiseBand(noise, true, "Dark" + styleId); - auto bright = writeNoiseBand(noise, false, "Bright" + styleId); + auto dark = writeNoiseBand(noise->size, noise->density, noise->seed, true, "Dark" + styleId); + auto bright = + writeNoiseBand(noise->size, noise->density, noise->seed, false, "Bright" + styleId); _defs->openElement("feComposite"); _defs->addAttribute("in", dark); @@ -1387,81 +1338,12 @@ std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleI return resultName; } - // Multi mode: single feTurbulence processed through two branches — contrast (RGB) and - // luminance-based density band (alpha) — then masked and clipped to SourceGraphic, aligned - // with tgfx's MultiNoiseFilter::onBuildBaseShader via MakeDarkDensityFilter. - auto turbResult = writeNoiseTurbulence(noise, "n" + styleId); - - // Contrast branch. - _defs->openElement("feComponentTransfer"); - _defs->addAttribute("in", turbResult); - _defs->addAttribute("result", "contrast" + styleId); - _defs->closeElementStart(); - _defs->openElement("feFuncR"); - _defs->addAttribute("type", "linear"); - _defs->addAttribute("slope", "2"); - _defs->addAttribute("intercept", "-0.5"); - _defs->closeElementSelfClosing(); - _defs->openElement("feFuncG"); - _defs->addAttribute("type", "linear"); - _defs->addAttribute("slope", "2"); - _defs->addAttribute("intercept", "-0.5"); - _defs->closeElementSelfClosing(); - _defs->openElement("feFuncB"); - _defs->addAttribute("type", "linear"); - _defs->addAttribute("slope", "2"); - _defs->addAttribute("intercept", "-0.5"); - _defs->closeElementSelfClosing(); - _defs->closeElement(); - - // Density band branch. - auto d = std::clamp(noise->density, 0.0f, 1.0f); - int lower = std::clamp(static_cast(std::lround(-25.0f * d + 25.0f)), 0, 99); - int upper = std::clamp(static_cast(std::lround(24.0f * d + 25.0f)), 0, 99); - std::string table; - table.reserve(300); - for (int i = 0; i < 100; i++) { - table += (i >= lower && i <= upper) ? "1 " : "0 "; - } - table.pop_back(); - - _defs->openElement("feColorMatrix"); - _defs->addAttribute("in", turbResult); - _defs->addAttribute("type", "luminanceToAlpha"); - _defs->addAttribute("result", "luma" + styleId); - _defs->closeElementSelfClosing(); - - _defs->openElement("feComponentTransfer"); - _defs->addAttribute("in", "luma" + styleId); - _defs->addAttribute("result", "band" + styleId); - _defs->closeElementStart(); - _defs->openElement("feFuncA"); - _defs->addAttribute("type", "discrete"); - _defs->addAttribute("tableValues", table); - _defs->closeElementSelfClosing(); - _defs->closeElement(); - - _defs->openElement("feComposite"); - _defs->addAttribute("in", "contrast" + styleId); - _defs->addAttribute("in2", "band" + styleId); - _defs->addAttribute("operator", "in"); - _defs->addAttribute("result", "masked" + styleId); - _defs->closeElementSelfClosing(); - - // Apply opacity scaling. - _defs->openElement("feColorMatrix"); - _defs->addAttribute("in", "masked" + styleId); - _defs->addAttribute("type", "matrix"); - std::string opacityValues = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 "; - opacityValues += FloatToString(noise->opacity); - opacityValues += " 0"; - _defs->addAttribute("values", opacityValues); - _defs->addAttribute("result", "final" + styleId); - _defs->closeElementSelfClosing(); + auto final = + writeNoiseMultiCore(noise->size, noise->density, noise->seed, noise->opacity, styleId); auto resultName = "noiseStyleOut" + styleId; _defs->openElement("feComposite"); - _defs->addAttribute("in", "final" + styleId); + _defs->addAttribute("in", final); _defs->addAttribute("in2", "SourceGraphic"); _defs->addAttribute("operator", "in"); _defs->addAttribute("result", resultName); @@ -1554,7 +1436,23 @@ void SVGWriter::writeStyleList(const std::vector& styles, int& shad case NodeType::NoiseStyle: { auto noise = static_cast(style); auto result = writeNoiseStyle(noise, noiseStyleIndex); - agg.aboveResults.push_back(result); + if (noise->blendMode != BlendMode::Normal) { + auto modeStr = BlendModeToFEBlendString(noise->blendMode); + if (modeStr) { + std::string blendResult = result + "Blended"; + _defs->openElement("feBlend"); + _defs->addAttribute("in", result); + _defs->addAttribute("in2", "SourceGraphic"); + _defs->addAttribute("mode", modeStr); + _defs->addAttribute("result", blendResult); + _defs->closeElementSelfClosing(); + agg.aboveResults.push_back(blendResult); + } else { + agg.aboveResults.push_back(result); + } + } else { + agg.aboveResults.push_back(result); + } agg.needSourceGraphic = true; break; } diff --git a/src/renderer/LayerBuilder.cpp b/src/renderer/LayerBuilder.cpp index 8b1205e076..0a58ced54b 100644 --- a/src/renderer/LayerBuilder.cpp +++ b/src/renderer/LayerBuilder.cpp @@ -1288,10 +1288,18 @@ class LayerBuilderContext { _result.binding.setWriter(node, "size", WriteNoiseStyleSize); _result.binding.setWriter(node, "density", WriteNoiseStyleDensity); _result.binding.setWriter(node, "seed", WriteNoiseStyleSeed); - _result.binding.setWriter(node, "color", WriteNoiseStyleColor); - _result.binding.setWriter(node, "firstColor", WriteNoiseStyleFirstColor); - _result.binding.setWriter(node, "secondColor", WriteNoiseStyleSecondColor); - _result.binding.setWriter(node, "opacity", WriteNoiseStyleOpacity); + switch (node->mode) { + case NoiseMode::Mono: + _result.binding.setWriter(node, "color", WriteNoiseStyleColor); + break; + case NoiseMode::Duo: + _result.binding.setWriter(node, "firstColor", WriteNoiseStyleFirstColor); + _result.binding.setWriter(node, "secondColor", WriteNoiseStyleSecondColor); + break; + case NoiseMode::Multi: + _result.binding.setWriter(node, "opacity", WriteNoiseStyleOpacity); + break; + } } static void WriteBlurFilterX(void* object, const KeyValue& value, float mix) { @@ -1527,10 +1535,18 @@ class LayerBuilderContext { _result.binding.setWriter(node, "size", WriteNoiseFilterSize); _result.binding.setWriter(node, "density", WriteNoiseFilterDensity); _result.binding.setWriter(node, "seed", WriteNoiseFilterSeed); - _result.binding.setWriter(node, "color", WriteNoiseFilterColor); - _result.binding.setWriter(node, "firstColor", WriteNoiseFilterFirstColor); - _result.binding.setWriter(node, "secondColor", WriteNoiseFilterSecondColor); - _result.binding.setWriter(node, "opacity", WriteNoiseFilterOpacity); + switch (node->mode) { + case NoiseMode::Mono: + _result.binding.setWriter(node, "color", WriteNoiseFilterColor); + break; + case NoiseMode::Duo: + _result.binding.setWriter(node, "firstColor", WriteNoiseFilterFirstColor); + _result.binding.setWriter(node, "secondColor", WriteNoiseFilterSecondColor); + break; + case NoiseMode::Multi: + _result.binding.setWriter(node, "opacity", WriteNoiseFilterOpacity); + break; + } } std::shared_ptr convertLayerFilter(const LayerFilter* node) { diff --git a/test/baseline/version.json b/test/baseline/version.json index 1370341721..4769306aa6 100644 --- a/test/baseline/version.json +++ b/test/baseline/version.json @@ -8558,7 +8558,7 @@ }, "PAGXTest": { "LayerBuilderAPIConsistency": "f40e23b6", - "NoiseFilterAllElements": "798f40c4", + "NoiseFilterAllElements": "126228e0", "NoiseFilterAnimation": { "frame_0": "8d43445d", "frame_1": "8d43445d", @@ -8574,6 +8574,7 @@ "frame_9": "8d43445d" }, "NoiseFilterModes": "798f40c4", + "NoiseStyleBlendModeOnImage": "126228e0", "NoiseStyleModes": "1af07597", "PrecomposedTextRender": "9b6dea6a", "html": { diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index bf36f23088..d20a44bece 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7308,12 +7308,12 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { pagx::FontConfig fontConfig; fontConfig.addFallbackTypefaces(GetFallbackTypefaces()); - auto typeface = Typeface::MakeFromPath("/System/Library/Fonts/Helvetica.ttc"); - auto fontFamily = typeface ? typeface->fontFamily() : std::string(); - auto fontStyle = typeface ? typeface->fontStyle() : std::string(); - if (typeface) { - fontConfig.registerTypeface(typeface); - } + auto typeface = + Typeface::MakeFromPath(ProjectPath::Absolute("resources/font/NotoSerifSC-Regular.otf")); + ASSERT_TRUE(typeface != nullptr); + fontConfig.registerTypeface(typeface); + auto fontFamily = typeface->fontFamily(); + auto fontStyle = typeface->fontStyle(); auto makeMonoNoise = [&](float density) { auto noise = doc->makeNode(); @@ -7518,6 +7518,8 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { svgOpts.fontConfig = &fontConfig; auto svg = pagx::SVGExporter::ToSVG(*doc, svgOpts); EXPECT_FALSE(svg.empty()); + EXPECT_NE(svg.find("(svg.size())); } +/** + * Test NoiseStyle with blendMode applied to an image layer, verifying both rendering and SVG + * export. The blendMode is set to Multiply so the noise composites differently from Normal. + * Currently SVG export ignores blendMode, so the SVG output will differ from the GPU rendering. + */ +PAGX_TEST(PAGXTest, NoiseStyleBlendModeOnImage) { + constexpr int canvasW = 200; + constexpr int canvasH = 200; + auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); + + auto* layer = doc->makeNode(); + + auto* rect = doc->makeNode(); + rect->position = {100, 100}; + rect->size = {200, 200}; + + auto* image = doc->makeNode(); + auto imageData = + tgfx::Data::MakeFromFile(ProjectPath::Absolute("resources/apitest/imageReplacement.png")); + ASSERT_TRUE(imageData != nullptr); + image->data = pagx::Data::MakeWithCopy(imageData->bytes(), imageData->size()); + + auto* pattern = doc->makeNode(); + pattern->image = image; + pattern->matrix = {1, 0, 0, 1, 0, 0}; + + auto* fill = doc->makeNode(); + fill->color = pattern; + + auto* noise = doc->makeNode(); + noise->mode = pagx::NoiseMode::Mono; + noise->size = 8; + noise->density = 1.0f; + noise->seed = 7; + noise->color = {0.5f, 0.5f, 0.5f, 1.0f}; + noise->blendMode = pagx::BlendMode::Multiply; + + layer->contents.push_back(rect); + layer->contents.push_back(fill); + layer->styles.push_back(noise); + doc->layers.push_back(layer); + + doc->applyLayout(); + auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); + ASSERT_TRUE(tgfxLayer != nullptr); + + auto surface = Surface::Make(context, canvasW, canvasH); + ASSERT_TRUE(surface != nullptr); + DisplayList displayList; + displayList.root()->addChild(tgfxLayer); + displayList.render(surface.get(), false); + + EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseStyleBlendModeOnImage")); + + pagx::SVGExportOptions svgOpts; + auto svg = pagx::SVGExporter::ToSVG(*doc, svgOpts); + EXPECT_FALSE(svg.empty()); + EXPECT_NE(svg.find("feTurbulence"), std::string::npos); + EXPECT_NE(svg.find("(svg.size())); +} + /** * Test rendering with Mono, Duo, and Multi noise styles side by side. * Covers writeNoiseStyle for all three modes. @@ -7706,6 +7777,17 @@ PAGX_TEST(PAGXTest, ChannelNoiseFilter) { auto tgfxMulti = tree.get(multiFilter); ASSERT_TRUE(tgfxMulti != nullptr); + EXPECT_FALSE(tree.apply(monoFilter, "firstColor", + pagx::KeyValue(pagx::Color{0.0f, 1.0f, 0.0f, 1.0f}), 1.0f)); + EXPECT_FALSE(tree.apply(monoFilter, "opacity", pagx::KeyValue(1.0f), 1.0f)); + EXPECT_FALSE( + tree.apply(duoFilter, "color", pagx::KeyValue(pagx::Color{1.0f, 0.0f, 0.0f, 1.0f}), 1.0f)); + EXPECT_FALSE(tree.apply(duoFilter, "opacity", pagx::KeyValue(1.0f), 1.0f)); + EXPECT_FALSE( + tree.apply(multiFilter, "color", pagx::KeyValue(pagx::Color{1.0f, 0.0f, 0.0f, 1.0f}), 1.0f)); + EXPECT_FALSE(tree.apply(multiFilter, "secondColor", + pagx::KeyValue(pagx::Color{1.0f, 1.0f, 1.0f, 1.0f}), 1.0f)); + auto timeline = file->getDefaultTimeline(); ASSERT_TRUE(timeline != nullptr); @@ -7844,6 +7926,17 @@ PAGX_TEST(PAGXTest, ChannelNoiseStyle) { auto tgfxMulti = tree.get(multiStyle); ASSERT_TRUE(tgfxMulti != nullptr); + EXPECT_FALSE(tree.apply(monoStyle, "firstColor", + pagx::KeyValue(pagx::Color{0.0f, 1.0f, 0.0f, 1.0f}), 1.0f)); + EXPECT_FALSE(tree.apply(monoStyle, "opacity", pagx::KeyValue(1.0f), 1.0f)); + EXPECT_FALSE( + tree.apply(duoStyle, "color", pagx::KeyValue(pagx::Color{1.0f, 0.0f, 0.0f, 1.0f}), 1.0f)); + EXPECT_FALSE(tree.apply(duoStyle, "opacity", pagx::KeyValue(1.0f), 1.0f)); + EXPECT_FALSE( + tree.apply(multiStyle, "color", pagx::KeyValue(pagx::Color{1.0f, 0.0f, 0.0f, 1.0f}), 1.0f)); + EXPECT_FALSE(tree.apply(multiStyle, "secondColor", + pagx::KeyValue(pagx::Color{1.0f, 1.0f, 1.0f, 1.0f}), 1.0f)); + auto timeline = file->getDefaultTimeline(); ASSERT_TRUE(timeline != nullptr); From 49a4cefc7b91e2fc5ac6aaf2bfd3ee5c98eb25ba Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 15 Jun 2026 15:35:26 +0800 Subject: [PATCH 40/52] Fix noise SVG export issues: rename final variable, clamp opacity, skip size<=0, add Display P3 flood-color support, remove stale comment. --- src/pagx/svg/SVGExporter.cpp | 69 +++++++++++++++++++----------------- test/src/PAGXTest.cpp | 1 - 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index 1081602b4a..1b96ca9186 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -457,6 +457,10 @@ class SVGWriter { // needs-group path (inside the ) and for the bare-through path. void writeLayerBody(SVGBuilder& out, const Layer* layer, float perChildAlpha = 1.0f); + // Writes flood-color (and optional flood-opacity) on the current feFlood element, with Display + // P3 override when the color's colorSpace is DisplayP3. + void applyFloodColor(const Color& color); + // Fill / stroke attribute helpers void applyFillAttributes(SVGBuilder& out, const Fill* fill, const Rect& shapeBounds = {}, std::string* p3Style = nullptr, float alphaMultiplier = 1.0f); @@ -946,10 +950,7 @@ void SVGWriter::writeBlendFilter(const BlendFilter* blend, int& shadowIndex, std::string idx = std::to_string(shadowIndex++); std::string floodResult = "blendFlood" + idx; _defs->openElement("feFlood"); - _defs->addAttribute("flood-color", ColorToSVGString(blend->color)); - if (blend->color.alpha < 1.0f) { - _defs->addAttribute("flood-opacity", FloatToString(blend->color.alpha)); - } + applyFloodColor(blend->color); _defs->addAttribute("result", floodResult); _defs->closeElementSelfClosing(); @@ -1104,8 +1105,9 @@ std::string SVGWriter::writeNoiseMultiCore(float size, float density, float seed _defs->openElement("feColorMatrix"); _defs->addAttribute("in", "masked" + id); _defs->addAttribute("type", "matrix"); + auto clampedOpacity = std::clamp(opacity, 0.0f, 1.0f); std::string opacityValues = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 "; - opacityValues += FloatToString(opacity); + opacityValues += FloatToString(clampedOpacity); opacityValues += " 0"; _defs->addAttribute("values", opacityValues); _defs->addAttribute("result", "final" + id); @@ -1115,15 +1117,15 @@ std::string SVGWriter::writeNoiseMultiCore(float size, float density, float seed std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, std::string& currentSource) { + if (noise->size <= 0.0f) { + return {}; + } std::string filterId = "noise" + std::to_string(noiseIndex++); if (noise->mode == NoiseMode::Mono) { auto band = writeNoiseBand(noise->size, noise->density, noise->seed, true, "Dark" + filterId); _defs->openElement("feFlood"); - _defs->addAttribute("flood-color", ColorToSVGString(noise->color)); - if (noise->color.alpha < 1.0f) { - _defs->addAttribute("flood-opacity", FloatToString(noise->color.alpha)); - } + applyFloodColor(noise->color); _defs->addAttribute("result", "flood" + filterId); _defs->closeElementSelfClosing(); @@ -1168,10 +1170,7 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde _defs->closeElementSelfClosing(); _defs->openElement("feFlood"); - _defs->addAttribute("flood-color", ColorToSVGString(noise->firstColor)); - if (noise->firstColor.alpha < 1.0f) { - _defs->addAttribute("flood-opacity", FloatToString(noise->firstColor.alpha)); - } + applyFloodColor(noise->firstColor); _defs->addAttribute("result", "floodDark" + filterId); _defs->closeElementSelfClosing(); @@ -1183,10 +1182,7 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde _defs->closeElementSelfClosing(); _defs->openElement("feFlood"); - _defs->addAttribute("flood-color", ColorToSVGString(noise->secondColor)); - if (noise->secondColor.alpha < 1.0f) { - _defs->addAttribute("flood-opacity", FloatToString(noise->secondColor.alpha)); - } + applyFloodColor(noise->secondColor); _defs->addAttribute("result", "floodBright" + filterId); _defs->closeElementSelfClosing(); @@ -1225,11 +1221,11 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde return resultName; } - auto final = + auto multiResult = writeNoiseMultiCore(noise->size, noise->density, noise->seed, noise->opacity, filterId); _defs->openElement("feComposite"); - _defs->addAttribute("in", final); + _defs->addAttribute("in", multiResult); _defs->addAttribute("in2", currentSource); _defs->addAttribute("operator", "in"); _defs->addAttribute("result", "clipped" + filterId); @@ -1250,15 +1246,15 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde } std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleIndex) { + if (noise->size <= 0.0f) { + return {}; + } std::string styleId = "noiseStyle" + std::to_string(noiseStyleIndex++); if (noise->mode == NoiseMode::Mono) { auto band = writeNoiseBand(noise->size, noise->density, noise->seed, true, "Dark" + styleId); _defs->openElement("feFlood"); - _defs->addAttribute("flood-color", ColorToSVGString(noise->color)); - if (noise->color.alpha < 1.0f) { - _defs->addAttribute("flood-opacity", FloatToString(noise->color.alpha)); - } + applyFloodColor(noise->color); _defs->addAttribute("result", "flood" + styleId); _defs->closeElementSelfClosing(); @@ -1292,10 +1288,7 @@ std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleI _defs->closeElementSelfClosing(); _defs->openElement("feFlood"); - _defs->addAttribute("flood-color", ColorToSVGString(noise->firstColor)); - if (noise->firstColor.alpha < 1.0f) { - _defs->addAttribute("flood-opacity", FloatToString(noise->firstColor.alpha)); - } + applyFloodColor(noise->firstColor); _defs->addAttribute("result", "floodDark" + styleId); _defs->closeElementSelfClosing(); @@ -1307,10 +1300,7 @@ std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleI _defs->closeElementSelfClosing(); _defs->openElement("feFlood"); - _defs->addAttribute("flood-color", ColorToSVGString(noise->secondColor)); - if (noise->secondColor.alpha < 1.0f) { - _defs->addAttribute("flood-opacity", FloatToString(noise->secondColor.alpha)); - } + applyFloodColor(noise->secondColor); _defs->addAttribute("result", "floodBright" + styleId); _defs->closeElementSelfClosing(); @@ -1338,12 +1328,12 @@ std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleI return resultName; } - auto final = + auto multiResult = writeNoiseMultiCore(noise->size, noise->density, noise->seed, noise->opacity, styleId); auto resultName = "noiseStyleOut" + styleId; _defs->openElement("feComposite"); - _defs->addAttribute("in", final); + _defs->addAttribute("in", multiResult); _defs->addAttribute("in2", "SourceGraphic"); _defs->addAttribute("operator", "in"); _defs->addAttribute("result", resultName); @@ -1436,6 +1426,9 @@ void SVGWriter::writeStyleList(const std::vector& styles, int& shad case NodeType::NoiseStyle: { auto noise = static_cast(style); auto result = writeNoiseStyle(noise, noiseStyleIndex); + if (result.empty()) { + break; + } if (noise->blendMode != BlendMode::Normal) { auto modeStr = BlendModeToFEBlendString(noise->blendMode); if (modeStr) { @@ -1749,6 +1742,16 @@ void SVGWriter::applyP3Style(SVGBuilder& out, const std::string& p3Style) { } } +void SVGWriter::applyFloodColor(const Color& color) { + _defs->addAttribute("flood-color", ColorToSVGString(color)); + if (color.alpha < 1.0f) { + _defs->addAttribute("flood-opacity", FloatToString(color.alpha)); + } + if (color.colorSpace == ColorSpace::DisplayP3) { + _defs->addAttribute("style", "flood-color:" + ColorToDisplayP3String(color)); + } +} + void SVGWriter::applyPainters(SVGBuilder& out, const FillStrokeInfo& fs, const Rect& shapeBounds, float alpha) { std::string p3Style; diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index d20a44bece..cdce767037 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7533,7 +7533,6 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { /** * Test NoiseStyle with blendMode applied to an image layer, verifying both rendering and SVG * export. The blendMode is set to Multiply so the noise composites differently from Normal. - * Currently SVG export ignores blendMode, so the SVG output will differ from the GPU rendering. */ PAGX_TEST(PAGXTest, NoiseStyleBlendModeOnImage) { constexpr int canvasW = 200; From 6900e18fc42640727e44398c2f045c66a221de4a Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 15 Jun 2026 16:02:46 +0800 Subject: [PATCH 41/52] Add sRGB fallback to applyFloodColor P3 style for browser compatibility. --- src/pagx/svg/SVGExporter.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index 1b96ca9186..1057e0c571 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -1748,7 +1748,16 @@ void SVGWriter::applyFloodColor(const Color& color) { _defs->addAttribute("flood-opacity", FloatToString(color.alpha)); } if (color.colorSpace == ColorSpace::DisplayP3) { - _defs->addAttribute("style", "flood-color:" + ColorToDisplayP3String(color)); + std::string style; + style.reserve(120); + style += "flood-color:"; + style += ColorToSVGString(color); + style += ";flood-color:"; + style += ColorToDisplayP3String(color); + style += ";flood-opacity:"; + style += FloatToString(color.alpha); + style += ';'; + _defs->addAttribute("style", style); } } From 50bb1571296ca52e16fc1fa4608a14cb0bfabf9e Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 15 Jun 2026 16:02:58 +0800 Subject: [PATCH 42/52] Add size guard for NoiseStyle to avoid null pointer registration when size is non-positive. --- src/renderer/LayerBuilder.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/renderer/LayerBuilder.cpp b/src/renderer/LayerBuilder.cpp index 0a58ced54b..8850c6044f 100644 --- a/src/renderer/LayerBuilder.cpp +++ b/src/renderer/LayerBuilder.cpp @@ -1050,6 +1050,9 @@ class LayerBuilderContext { } case NodeType::NoiseStyle: { auto style = static_cast(node); + if (style->size <= 0.0f) { + return nullptr; + } std::shared_ptr tgfxStyle; switch (style->mode) { case NoiseMode::Mono: @@ -1594,6 +1597,9 @@ class LayerBuilderContext { } case NodeType::NoiseFilter: { auto filter = static_cast(node); + if (filter->size <= 0.0f) { + return nullptr; + } auto tgfxBlendMode = ToTGFX(filter->blendMode); std::shared_ptr tgfxFilter; switch (filter->mode) { From b51200fcfcc701c5d325cc5f29f8ddffcd704a49 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 15 Jun 2026 16:03:29 +0800 Subject: [PATCH 43/52] Update comments to include NoiseFilter and NoiseStyle in subclass lists. --- include/pagx/nodes/LayerFilter.h | 3 ++- include/pagx/nodes/LayerStyle.h | 2 +- include/pagx/types/NoiseMode.h | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/include/pagx/nodes/LayerFilter.h b/include/pagx/nodes/LayerFilter.h index 22dfe67a33..7530b2776e 100644 --- a/include/pagx/nodes/LayerFilter.h +++ b/include/pagx/nodes/LayerFilter.h @@ -23,7 +23,8 @@ namespace pagx { /** - * Base class for layer filters (BlurFilter, DropShadowFilter, InnerShadowFilter, BlendFilter, ColorMatrixFilter). + * Base class for layer filters (BlurFilter, DropShadowFilter, InnerShadowFilter, BlendFilter, + * ColorMatrixFilter, NoiseFilter). */ class LayerFilter : public Node { public: diff --git a/include/pagx/nodes/LayerStyle.h b/include/pagx/nodes/LayerStyle.h index 24c39a2be1..23d328fe64 100644 --- a/include/pagx/nodes/LayerStyle.h +++ b/include/pagx/nodes/LayerStyle.h @@ -24,7 +24,7 @@ namespace pagx { /** - * Base class for layer styles (DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle). + * Base class for layer styles (DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle, NoiseStyle). */ class LayerStyle : public Node { public: diff --git a/include/pagx/types/NoiseMode.h b/include/pagx/types/NoiseMode.h index b3af31cf43..c7e8803f17 100644 --- a/include/pagx/types/NoiseMode.h +++ b/include/pagx/types/NoiseMode.h @@ -21,7 +21,7 @@ namespace pagx { /** - * Noise modes for the NoiseFilter. + * Noise modes for noise filters and noise layer styles. */ enum class NoiseMode { /** From 295d1a615f2036bfd027dd78d3230355720bf322 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 15 Jun 2026 16:04:29 +0800 Subject: [PATCH 44/52] Extract duplicate noise filter blend and style clip logic into helper methods. --- src/pagx/svg/SVGExporter.cpp | 97 ++++++++++++++---------------------- 1 file changed, 38 insertions(+), 59 deletions(-) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index 1057e0c571..75ab91ce20 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -413,8 +413,10 @@ class SVGWriter { const std::string& label); std::string writeNoiseMultiCore(float size, float density, float seed, float opacity, const std::string& id); - std::string writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, - std::string& currentSource); + void writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, std::string& currentSource); + void writeNoiseResultBlend(const std::string& clippedResult, const std::string& filterId, + BlendMode blendMode, std::string& currentSource); + void writeNoiseStyleClip(const std::string& noiseResult, const std::string& styleId); // Collected per-filter state fed into the final feMerge aggregation. struct ShadowAggregate { std::vector dropShadowResults; @@ -1115,10 +1117,26 @@ std::string SVGWriter::writeNoiseMultiCore(float size, float density, float seed return "final" + id; } -std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, - std::string& currentSource) { +void SVGWriter::writeNoiseResultBlend(const std::string& clippedResult, + const std::string& filterId, BlendMode blendMode, + std::string& currentSource) { + auto resultName = "noiseOut" + filterId; + _defs->openElement("feBlend"); + _defs->addAttribute("in", clippedResult); + _defs->addAttribute("in2", currentSource); + auto modeStr = BlendModeToFEBlendString(blendMode); + if (modeStr) { + _defs->addAttribute("mode", modeStr); + } + _defs->addAttribute("result", resultName); + _defs->closeElementSelfClosing(); + currentSource = resultName; +} + +void SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, + std::string& currentSource) { if (noise->size <= 0.0f) { - return {}; + return; } std::string filterId = "noise" + std::to_string(noiseIndex++); @@ -1143,18 +1161,8 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde _defs->addAttribute("result", "clipped" + filterId); _defs->closeElementSelfClosing(); - auto resultName = "noiseOut" + filterId; - _defs->openElement("feBlend"); - _defs->addAttribute("in", "clipped" + filterId); - _defs->addAttribute("in2", currentSource); - auto modeStr = BlendModeToFEBlendString(noise->blendMode); - if (modeStr) { - _defs->addAttribute("mode", modeStr); - } - _defs->addAttribute("result", resultName); - _defs->closeElementSelfClosing(); - currentSource = resultName; - return resultName; + writeNoiseResultBlend("clipped" + filterId, filterId, noise->blendMode, currentSource); + return; } if (noise->mode == NoiseMode::Duo) { @@ -1207,18 +1215,8 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde _defs->addAttribute("result", "clipped" + filterId); _defs->closeElementSelfClosing(); - auto resultName = "noiseOut" + filterId; - _defs->openElement("feBlend"); - _defs->addAttribute("in", "clipped" + filterId); - _defs->addAttribute("in2", currentSource); - auto modeStr = BlendModeToFEBlendString(noise->blendMode); - if (modeStr) { - _defs->addAttribute("mode", modeStr); - } - _defs->addAttribute("result", resultName); - _defs->closeElementSelfClosing(); - currentSource = resultName; - return resultName; + writeNoiseResultBlend("clipped" + filterId, filterId, noise->blendMode, currentSource); + return; } auto multiResult = @@ -1231,18 +1229,7 @@ std::string SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseInde _defs->addAttribute("result", "clipped" + filterId); _defs->closeElementSelfClosing(); - auto resultName = "noiseOut" + filterId; - _defs->openElement("feBlend"); - _defs->addAttribute("in", "clipped" + filterId); - _defs->addAttribute("in2", currentSource); - auto modeStr = BlendModeToFEBlendString(noise->blendMode); - if (modeStr) { - _defs->addAttribute("mode", modeStr); - } - _defs->addAttribute("result", resultName); - _defs->closeElementSelfClosing(); - currentSource = resultName; - return resultName; + writeNoiseResultBlend("clipped" + filterId, filterId, noise->blendMode, currentSource); } std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleIndex) { @@ -1265,14 +1252,8 @@ std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleI _defs->addAttribute("result", "colored" + styleId); _defs->closeElementSelfClosing(); - auto resultName = "noiseStyleOut" + styleId; - _defs->openElement("feComposite"); - _defs->addAttribute("in", "colored" + styleId); - _defs->addAttribute("in2", "SourceGraphic"); - _defs->addAttribute("operator", "in"); - _defs->addAttribute("result", resultName); - _defs->closeElementSelfClosing(); - return resultName; + writeNoiseStyleClip("colored" + styleId, styleId); + return "noiseStyleOut" + styleId; } if (noise->mode == NoiseMode::Duo) { @@ -1318,27 +1299,25 @@ std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleI _defs->addAttribute("result", "duo" + styleId); _defs->closeElementSelfClosing(); - auto resultName = "noiseStyleOut" + styleId; - _defs->openElement("feComposite"); - _defs->addAttribute("in", "duo" + styleId); - _defs->addAttribute("in2", "SourceGraphic"); - _defs->addAttribute("operator", "in"); - _defs->addAttribute("result", resultName); - _defs->closeElementSelfClosing(); - return resultName; + writeNoiseStyleClip("duo" + styleId, styleId); + return "noiseStyleOut" + styleId; } auto multiResult = writeNoiseMultiCore(noise->size, noise->density, noise->seed, noise->opacity, styleId); + writeNoiseStyleClip(multiResult, styleId); + return "noiseStyleOut" + styleId; +} + +void SVGWriter::writeNoiseStyleClip(const std::string& noiseResult, const std::string& styleId) { auto resultName = "noiseStyleOut" + styleId; _defs->openElement("feComposite"); - _defs->addAttribute("in", multiResult); + _defs->addAttribute("in", noiseResult); _defs->addAttribute("in2", "SourceGraphic"); _defs->addAttribute("operator", "in"); _defs->addAttribute("result", resultName); _defs->closeElementSelfClosing(); - return resultName; } void SVGWriter::writeFilterList(const std::vector& filters, int& shadowIndex, From 607f9c434b946bf73aa1e8d1a147cfa3001a22e6 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 15 Jun 2026 16:04:33 +0800 Subject: [PATCH 45/52] Add PAGXImporter parsing for NoiseStyle and NoiseFilter nodes. --- src/pagx/PAGXImporter.cpp | 70 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/src/pagx/PAGXImporter.cpp b/src/pagx/PAGXImporter.cpp index cd0ec5fab4..ec1fc7c5fd 100644 --- a/src/pagx/PAGXImporter.cpp +++ b/src/pagx/PAGXImporter.cpp @@ -52,6 +52,8 @@ #include "pagx/nodes/InnerShadowStyle.h" #include "pagx/nodes/LinearGradient.h" #include "pagx/nodes/MergePath.h" +#include "pagx/nodes/NoiseFilter.h" +#include "pagx/nodes/NoiseStyle.h" #include "pagx/nodes/Path.h" #include "pagx/nodes/PathData.h" #include "pagx/nodes/Polystar.h" @@ -577,9 +579,9 @@ static Layer* ParseLayer(const DOMNode* node, PAGXDocument* doc) { " Expected: Layer, Group, Rectangle, Ellipse, Polystar," " Path, Text, Fill, Stroke, TrimPath, RoundCorner," " MergePath, TextModifier, TextPath, TextBox, Repeater," - " DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle," + " DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle, NoiseStyle," " BlurFilter, DropShadowFilter, InnerShadowFilter," - " BlendFilter, ColorMatrixFilter."); + " BlendFilter, ColorMatrixFilter, NoiseFilter."); } return layer; @@ -623,7 +625,7 @@ static void ParseStyles(const DOMNode* node, Layer* layer, PAGXDocument* doc) { "Element '" + current->name + "' is not allowed in 'styles'." " Expected: DropShadowStyle, InnerShadowStyle," - " BackgroundBlurStyle."); + " BackgroundBlurStyle, NoiseStyle."); } } } @@ -644,7 +646,7 @@ static void ParseFilters(const DOMNode* node, Layer* layer, PAGXDocument* doc) { "Element '" + current->name + "' is not allowed in 'filters'." " Expected: BlurFilter, DropShadowFilter," - " InnerShadowFilter, BlendFilter, ColorMatrixFilter."); + " InnerShadowFilter, BlendFilter, ColorMatrixFilter, NoiseFilter."); } } } @@ -758,6 +760,9 @@ static LayerStyle* ParseLayerStyle(const DOMNode* node, PAGXDocument* doc) { if (node->name == "BackgroundBlurStyle") { return ParseBackgroundBlurStyle(node, doc); } + if (node->name == "NoiseStyle") { + return ParseNoiseStyle(node, doc); + } return nullptr; } @@ -777,6 +782,9 @@ static LayerFilter* ParseLayerFilter(const DOMNode* node, PAGXDocument* doc) { if (node->name == "ColorMatrixFilter") { return ParseColorMatrixFilter(node, doc); } + if (node->name == "NoiseFilter") { + return ParseNoiseFilter(node, doc); + } return nullptr; } @@ -2031,6 +2039,34 @@ static void ParseShadowAttributes(const DOMNode* node, PAGXDocument* doc, float& } } +static NoiseStyle* ParseNoiseStyle(const DOMNode* node, PAGXDocument* doc) { + auto style = makeNodeFromXML(node, doc); + if (!style) { + return nullptr; + } + style->blendMode = GET_ENUM(node, "blendMode", "normal", doc, BlendMode); + style->excludeChildEffects = GetBoolAttribute( + node, "excludeChildEffects", Default().excludeChildEffects, doc); + style->mode = GET_ENUM(node, "mode", "mono", doc, NoiseMode); + style->size = GetFloatAttribute(node, "size", Default().size, doc); + style->density = GetFloatAttribute(node, "density", Default().density, doc); + style->seed = GetFloatAttribute(node, "seed", Default().seed, doc); + auto colorStr = GetAttribute(node, "color"); + if (!colorStr.empty()) { + style->color = GetColorAttribute(node, "color", doc); + } + auto firstColorStr = GetAttribute(node, "firstColor"); + if (!firstColorStr.empty()) { + style->firstColor = GetColorAttribute(node, "firstColor", doc); + } + auto secondColorStr = GetAttribute(node, "secondColor"); + if (!secondColorStr.empty()) { + style->secondColor = GetColorAttribute(node, "secondColor", doc); + } + style->opacity = GetFloatAttribute(node, "opacity", Default().opacity, doc); + return style; +} + static DropShadowStyle* ParseDropShadowStyle(const DOMNode* node, PAGXDocument* doc) { auto style = makeNodeFromXML(node, doc); if (!style) { @@ -2077,6 +2113,32 @@ static BackgroundBlurStyle* ParseBackgroundBlurStyle(const DOMNode* node, PAGXDo // Layer filter parsing //============================================================================== +static NoiseFilter* ParseNoiseFilter(const DOMNode* node, PAGXDocument* doc) { + auto filter = makeNodeFromXML(node, doc); + if (!filter) { + return nullptr; + } + filter->mode = GET_ENUM(node, "mode", "mono", doc, NoiseMode); + filter->size = GetFloatAttribute(node, "size", Default().size, doc); + filter->density = GetFloatAttribute(node, "density", Default().density, doc); + filter->seed = GetFloatAttribute(node, "seed", Default().seed, doc); + filter->blendMode = GET_ENUM(node, "blendMode", "normal", doc, BlendMode); + auto colorStr = GetAttribute(node, "color"); + if (!colorStr.empty()) { + filter->color = GetColorAttribute(node, "color", doc); + } + auto firstColorStr = GetAttribute(node, "firstColor"); + if (!firstColorStr.empty()) { + filter->firstColor = GetColorAttribute(node, "firstColor", doc); + } + auto secondColorStr = GetAttribute(node, "secondColor"); + if (!secondColorStr.empty()) { + filter->secondColor = GetColorAttribute(node, "secondColor", doc); + } + filter->opacity = GetFloatAttribute(node, "opacity", Default().opacity, doc); + return filter; +} + static BlurFilter* ParseBlurFilter(const DOMNode* node, PAGXDocument* doc) { auto filter = makeNodeFromXML(node, doc); if (!filter) { From 021f96ab80d24daeb22b2c9bf49af459a01a360b Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 15 Jun 2026 16:04:47 +0800 Subject: [PATCH 46/52] Add comments explaining writeNoiseBand density-to-band mapping constants. --- src/pagx/svg/SVGExporter.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index 75ab91ce20..9331495206 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -1020,6 +1020,11 @@ std::string SVGWriter::writeNoiseBand(float size, float density, float seed, boo auto d = std::clamp(density, 0.0f, 1.0f); int lower = 0; int upper = 0; + // Maps density to a percentile band [lower, upper] in the luminance histogram: + // dark (isDark=true ): d=0→[25,25], d=1→[ 0,49] + // bright (isDark=false): d=0→[74,74], d=1→[50,99] + // The band narrows at d=0 and widens toward the extremes as density increases, + // producing a continuous noise density gradient. if (isDark) { lower = std::clamp(static_cast(std::lround(-25.0f * d + 25.0f)), 0, 99); upper = std::clamp(static_cast(std::lround(24.0f * d + 25.0f)), 0, 99); From a68731b1fd8a6776f1f2b131e05ed6373ad0afaf Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 15 Jun 2026 16:06:29 +0800 Subject: [PATCH 47/52] Replace lambdas with static helpers and extract duplicated SVG export code in noise tests. --- test/src/PAGXTest.cpp | 233 ++++++++++++++++++++---------------------- 1 file changed, 112 insertions(+), 121 deletions(-) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index cdce767037..60ab80f3fb 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7217,6 +7217,86 @@ PAGX_TEST(PAGXTest, HitTestGlobalMatrix) { EXPECT_FLOAT_EQ(matrix.ty, 45.0f); } +static pagx::Layer* MakeTestLayer(pagx::PAGXDocument* doc, float x, float y, float fillR, + float fillG, float fillB) { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(x, y); + auto rect = doc->makeNode(); + rect->position = {50, 50}; + rect->size = {100, 100}; + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {fillR, fillG, fillB, 1.0f}; + fill->color = solid; + layer->contents.push_back(rect); + layer->contents.push_back(fill); + return layer; +} + +static pagx::Layer* MakeTestLayerSimple(pagx::PAGXDocument* doc, float x, float y) { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(x, y); + auto rect = doc->makeNode(); + rect->position = {0, 0}; + rect->size = {100, 100}; + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {0.8f, 0.8f, 0.8f, 1.0f}; + fill->color = solid; + layer->contents.push_back(rect); + layer->contents.push_back(fill); + return layer; +} + +static pagx::NoiseFilter* MakeMonoNoiseFilter(pagx::PAGXDocument* doc, float density) { + auto noise = doc->makeNode(); + noise->mode = pagx::NoiseMode::Mono; + noise->size = 10; + noise->density = density; + noise->seed = 42; + noise->color = {0.0f, 0.0f, 0.0f, 1.0f}; + return noise; +} + +static pagx::NoiseFilter* MakeDuoNoiseFilter(pagx::PAGXDocument* doc, float density) { + auto noise = doc->makeNode(); + noise->mode = pagx::NoiseMode::Duo; + noise->size = 10; + noise->density = density; + noise->seed = 42; + noise->firstColor = {1.0f, 1.0f, 0.0f, 1.0f}; + noise->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; + return noise; +} + +static pagx::NoiseFilter* MakeMultiNoiseFilter(pagx::PAGXDocument* doc, float density) { + auto noise = doc->makeNode(); + noise->mode = pagx::NoiseMode::Multi; + noise->size = 10; + noise->density = density; + noise->seed = 42; + noise->opacity = 1.0f; + return noise; +} + +static pagx::Fill* MakeSolidFill(pagx::PAGXDocument* doc, float r, float g, float b) { + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {r, g, b, 1.0f}; + fill->color = solid; + return fill; +} + +static void WriteSVGFile(const std::string& svgContent, const std::string& relativePath) { + auto outPath = ProjectPath::Absolute(relativePath); + auto dirPath = std::filesystem::path(outPath).parent_path(); + if (!std::filesystem::exists(dirPath)) { + std::filesystem::create_directories(dirPath); + } + std::ofstream file(outPath, std::ios::binary); + file.write(svgContent.data(), static_cast(svgContent.size())); +} + /** * Test rendering with Mono, Duo, and Multi noise filters side by side. */ @@ -7225,22 +7305,7 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { constexpr int canvasH = 150; auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); - auto makeLayer = [&](float x, float y) { - auto layer = doc->makeNode(); - layer->matrix = pagx::Matrix::Translate(x, y); - auto rect = doc->makeNode(); - rect->position = {50, 50}; - rect->size = {100, 100}; - auto fill = doc->makeNode(); - auto solid = doc->makeNode(); - solid->color = {0.2f, 0.5f, 0.8f, 1.0f}; - fill->color = solid; - layer->contents.push_back(rect); - layer->contents.push_back(fill); - return layer; - }; - - auto layer1 = makeLayer(20, 0); + auto layer1 = MakeTestLayer(doc.get(), 20, 0, 0.2f, 0.5f, 0.8f); auto mono = doc->makeNode(); mono->mode = pagx::NoiseMode::Mono; mono->size = 8; @@ -7250,7 +7315,7 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { layer1->filters.push_back(mono); doc->layers.push_back(layer1); - auto layer2 = makeLayer(150, 0); + auto layer2 = MakeTestLayer(doc.get(), 150, 0, 0.2f, 0.5f, 0.8f); auto duo = doc->makeNode(); duo->mode = pagx::NoiseMode::Duo; duo->size = 8; @@ -7261,7 +7326,7 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { layer2->filters.push_back(duo); doc->layers.push_back(layer2); - auto layer3 = makeLayer(280, 0); + auto layer3 = MakeTestLayer(doc.get(), 280, 0, 0.2f, 0.5f, 0.8f); auto multi = doc->makeNode(); multi->mode = pagx::NoiseMode::Multi; multi->size = 8; @@ -7288,13 +7353,7 @@ PAGX_TEST(PAGXTest, NoiseFilterModes) { EXPECT_NE(svg.find("(svg.size())); + WriteSVGFile(svg, "test/out/PAGXTest/NoiseFilterModes.svg"); } /** * Test NoiseFilter applied to every supported element type (Rectangle, Ellipse, Path, Polystar, @@ -7315,43 +7374,6 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { auto fontFamily = typeface->fontFamily(); auto fontStyle = typeface->fontStyle(); - auto makeMonoNoise = [&](float density) { - auto noise = doc->makeNode(); - noise->mode = pagx::NoiseMode::Mono; - noise->size = 10; - noise->density = density; - noise->seed = 42; - noise->color = {0.0f, 0.0f, 0.0f, 1.0f}; - return noise; - }; - auto makeDuoNoise = [&](float density) { - auto noise = doc->makeNode(); - noise->mode = pagx::NoiseMode::Duo; - noise->size = 10; - noise->density = density; - noise->seed = 42; - noise->firstColor = {1.0f, 1.0f, 0.0f, 1.0f}; - noise->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; - return noise; - }; - auto makeMultiNoise = [&](float density) { - auto noise = doc->makeNode(); - noise->mode = pagx::NoiseMode::Multi; - noise->size = 10; - noise->density = density; - noise->seed = 42; - noise->opacity = 1.0f; - return noise; - }; - - auto makeFill = [&](float r, float g, float b) { - auto fill = doc->makeNode(); - auto solid = doc->makeNode(); - solid->color = {r, g, b, 1.0f}; - fill->color = solid; - return fill; - }; - // Row 1: Rectangle(Mono,0), Ellipse(Duo,0.25), Path(Multi,0.5) { auto layer = doc->makeNode(); @@ -7361,8 +7383,8 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { rect->position = {60, 60}; rect->size = {100, 100}; layer->contents.push_back(rect); - layer->contents.push_back(makeFill(0.2f, 0.5f, 0.8f)); - layer->filters.push_back(makeMonoNoise(0.0f)); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.2f, 0.5f, 0.8f)); + layer->filters.push_back(MakeMonoNoiseFilter(doc.get(), 0.0f)); doc->layers.push_back(layer); } { @@ -7373,8 +7395,8 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { ellipse->position = {60, 60}; ellipse->size = {100, 100}; layer->contents.push_back(ellipse); - layer->contents.push_back(makeFill(0.8f, 0.3f, 0.3f)); - layer->filters.push_back(makeDuoNoise(0.25f)); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.8f, 0.3f, 0.3f)); + layer->filters.push_back(MakeDuoNoiseFilter(doc.get(), 0.25f)); doc->layers.push_back(layer); } { @@ -7389,8 +7411,8 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { path->data->close(); path->position = {10, 10}; layer->contents.push_back(path); - layer->contents.push_back(makeFill(0.3f, 0.7f, 0.3f)); - layer->filters.push_back(makeMultiNoise(0.5f)); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.3f, 0.7f, 0.3f)); + layer->filters.push_back(MakeMultiNoiseFilter(doc.get(), 0.5f)); doc->layers.push_back(layer); } @@ -7405,8 +7427,8 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { polystar->innerRadius = 25; polystar->pointCount = 5; layer->contents.push_back(polystar); - layer->contents.push_back(makeFill(0.7f, 0.5f, 0.9f)); - layer->filters.push_back(makeMonoNoise(0.75f)); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.7f, 0.5f, 0.9f)); + layer->filters.push_back(MakeMonoNoiseFilter(doc.get(), 0.75f)); doc->layers.push_back(layer); } { @@ -7419,8 +7441,8 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { text->fontStyle = fontStyle; text->fontSize = 60; layer->contents.push_back(text); - layer->contents.push_back(makeFill(0.2f, 0.6f, 0.9f)); - layer->filters.push_back(makeDuoNoise(1.0f)); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.2f, 0.6f, 0.9f)); + layer->filters.push_back(MakeDuoNoiseFilter(doc.get(), 1.0f)); doc->layers.push_back(layer); } { @@ -7433,9 +7455,9 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { rect->position = {40, 40}; rect->size = {60, 60}; group->elements.push_back(rect); - group->elements.push_back(makeFill(0.9f, 0.6f, 0.1f)); + group->elements.push_back(MakeSolidFill(doc.get(), 0.9f, 0.6f, 0.1f)); layer->contents.push_back(group); - layer->filters.push_back(makeMultiNoise(0.5f)); + layer->filters.push_back(MakeMultiNoiseFilter(doc.get(), 0.5f)); doc->layers.push_back(layer); } @@ -7453,9 +7475,9 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { text->fontStyle = fontStyle; text->fontSize = 30; textBox->elements.push_back(text); - textBox->elements.push_back(makeFill(0.1f, 0.4f, 0.7f)); + textBox->elements.push_back(MakeSolidFill(doc.get(), 0.1f, 0.4f, 0.7f)); layer->contents.push_back(textBox); - layer->filters.push_back(makeDuoNoise(0.5f)); + layer->filters.push_back(MakeDuoNoiseFilter(doc.get(), 0.5f)); doc->layers.push_back(layer); } { @@ -7470,9 +7492,9 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { repeater->offset = 0; repeater->position = {50, 0}; layer->contents.push_back(rect); - layer->contents.push_back(makeFill(0.3f, 0.8f, 0.6f)); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.3f, 0.8f, 0.6f)); layer->contents.push_back(repeater); - layer->filters.push_back(makeMultiNoise(0.5f)); + layer->filters.push_back(MakeMultiNoiseFilter(doc.get(), 0.5f)); doc->layers.push_back(layer); } @@ -7485,8 +7507,8 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { rect->position = {80, 40}; rect->size = {60, 60}; layer->contents.push_back(rect); - layer->contents.push_back(makeFill(0.6f, 0.2f, 0.8f)); - layer->filters.push_back(makeMonoNoise(0.5f)); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.6f, 0.2f, 0.8f)); + layer->filters.push_back(MakeMonoNoiseFilter(doc.get(), 0.5f)); doc->layers.push_back(layer); } { @@ -7497,8 +7519,8 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { ellipse->position = {40, 80}; ellipse->size = {60, 80}; layer->contents.push_back(ellipse); - layer->contents.push_back(makeFill(0.8f, 0.7f, 0.2f)); - layer->filters.push_back(makeDuoNoise(0.5f)); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.8f, 0.7f, 0.2f)); + layer->filters.push_back(MakeDuoNoiseFilter(doc.get(), 0.5f)); doc->layers.push_back(layer); } @@ -7521,13 +7543,7 @@ PAGX_TEST(PAGXTest, NoiseFilterAllElements) { EXPECT_NE(svg.find("(svg.size())); + WriteSVGFile(svg, "test/out/PAGXTest/NoiseFilterAllElements.svg"); } /** @@ -7589,13 +7605,7 @@ PAGX_TEST(PAGXTest, NoiseStyleBlendModeOnImage) { EXPECT_NE(svg.find("feTurbulence"), std::string::npos); EXPECT_NE(svg.find("(svg.size())); + WriteSVGFile(svg, "test/out/PAGXTest/NoiseStyleBlendModeOnImage.svg"); } /** @@ -7607,22 +7617,7 @@ PAGX_TEST(PAGXTest, NoiseStyleModes) { constexpr int canvasH = 180; auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); - auto makeLayer = [&](float x, float y) { - auto layer = doc->makeNode(); - layer->matrix = pagx::Matrix::Translate(x, y); - auto rect = doc->makeNode(); - rect->position = {0, 0}; - rect->size = {100, 100}; - auto fill = doc->makeNode(); - auto solid = doc->makeNode(); - solid->color = {0.8f, 0.8f, 0.8f, 1.0f}; - fill->color = solid; - layer->contents.push_back(rect); - layer->contents.push_back(fill); - return layer; - }; - - auto layer1 = makeLayer(20, 40); + auto layer1 = MakeTestLayerSimple(doc.get(), 20, 40); auto mono = doc->makeNode(); mono->mode = pagx::NoiseMode::Mono; mono->size = 8; @@ -7632,7 +7627,7 @@ PAGX_TEST(PAGXTest, NoiseStyleModes) { layer1->styles.push_back(mono); doc->layers.push_back(layer1); - auto layer2 = makeLayer(150, 40); + auto layer2 = MakeTestLayerSimple(doc.get(), 150, 40); auto duo = doc->makeNode(); duo->mode = pagx::NoiseMode::Duo; duo->size = 8; @@ -7643,7 +7638,7 @@ PAGX_TEST(PAGXTest, NoiseStyleModes) { layer2->styles.push_back(duo); doc->layers.push_back(layer2); - auto layer3 = makeLayer(280, 40); + auto layer3 = MakeTestLayerSimple(doc.get(), 280, 40); auto multi = doc->makeNode(); multi->mode = pagx::NoiseMode::Multi; multi->size = 8; @@ -7670,13 +7665,7 @@ PAGX_TEST(PAGXTest, NoiseStyleModes) { EXPECT_NE(svg.find("(svg.size())); + WriteSVGFile(svg, "test/out/PAGXTest/NoiseStyleModes.svg"); } /** @@ -7981,6 +7970,8 @@ PAGX_TEST(PAGXTest, ChannelNoiseStyle) { * Layout: two rectangles side-by-side, left with NoiseFilter, right with NoiseStyle. * 12 frames total: 3 Mono, 3 Duo, 3 Multi, 3 Multi+DropShadowFilter+InnerShadowStyle. * Density animates from 0.1 to 1.0 across all 12 frames. + * The baseline key is "NoiseFilterAnimation" (without the "Export" prefix) to avoid + * re-accepting baselines after renaming the test. */ PAGX_TEST(PAGXTest, ExportNoiseFilterAnimation) { constexpr int canvasW = 500; From 25cc6af9af0705b4e35e22f320d562044262adf1 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 15 Jun 2026 16:08:55 +0800 Subject: [PATCH 48/52] Add forward declarations for ParseNoiseStyle and ParseNoiseFilter. --- src/pagx/PAGXImporter.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pagx/PAGXImporter.cpp b/src/pagx/PAGXImporter.cpp index ec1fc7c5fd..512dcc02aa 100644 --- a/src/pagx/PAGXImporter.cpp +++ b/src/pagx/PAGXImporter.cpp @@ -185,6 +185,8 @@ static DropShadowFilter* ParseDropShadowFilter(const DOMNode* node, PAGXDocument static InnerShadowFilter* ParseInnerShadowFilter(const DOMNode* node, PAGXDocument* doc); static BlendFilter* ParseBlendFilter(const DOMNode* node, PAGXDocument* doc); static ColorMatrixFilter* ParseColorMatrixFilter(const DOMNode* node, PAGXDocument* doc); +static NoiseStyle* ParseNoiseStyle(const DOMNode* node, PAGXDocument* doc); +static NoiseFilter* ParseNoiseFilter(const DOMNode* node, PAGXDocument* doc); //============================================================================== // Custom data parsing @@ -2045,8 +2047,8 @@ static NoiseStyle* ParseNoiseStyle(const DOMNode* node, PAGXDocument* doc) { return nullptr; } style->blendMode = GET_ENUM(node, "blendMode", "normal", doc, BlendMode); - style->excludeChildEffects = GetBoolAttribute( - node, "excludeChildEffects", Default().excludeChildEffects, doc); + style->excludeChildEffects = + GetBoolAttribute(node, "excludeChildEffects", Default().excludeChildEffects, doc); style->mode = GET_ENUM(node, "mode", "mono", doc, NoiseMode); style->size = GetFloatAttribute(node, "size", Default().size, doc); style->density = GetFloatAttribute(node, "density", Default().density, doc); From e25fac81b1f4a0a511e12ad83131e27805b733e7 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 15 Jun 2026 16:14:40 +0800 Subject: [PATCH 49/52] Fix ExportNoiseFilterAnimation surface not cleared between frames. --- test/src/PAGXTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 60ab80f3fb..e920c8a400 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -8114,7 +8114,7 @@ PAGX_TEST(PAGXTest, ExportNoiseFilterAnimation) { tgfx::DisplayList displayList; displayList.root()->addChild(tgfxRoot); - displayList.render(surface.get(), false); + displayList.render(surface.get(), true); auto key = "PAGXTest/NoiseFilterAnimation/frame_" + std::to_string(i); EXPECT_TRUE(Baseline::Compare(surface, key)); From e1c63bbfda724ed1e528e0ee6e6f7969be6f4f67 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 15 Jun 2026 16:20:08 +0800 Subject: [PATCH 50/52] Accept updated baselines for ExportNoiseFilterAnimation after autoClear fix. --- test/baseline/version.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/baseline/version.json b/test/baseline/version.json index 4769306aa6..5e233e945a 100644 --- a/test/baseline/version.json +++ b/test/baseline/version.json @@ -8561,17 +8561,17 @@ "NoiseFilterAllElements": "126228e0", "NoiseFilterAnimation": { "frame_0": "8d43445d", - "frame_1": "8d43445d", - "frame_10": "8d43445d", - "frame_11": "8d43445d", - "frame_2": "8d43445d", - "frame_3": "8d43445d", - "frame_4": "8d43445d", - "frame_5": "8d43445d", - "frame_6": "8d43445d", - "frame_7": "8d43445d", - "frame_8": "8d43445d", - "frame_9": "8d43445d" + "frame_1": "e25fac81", + "frame_10": "e25fac81", + "frame_11": "e25fac81", + "frame_2": "e25fac81", + "frame_3": "e25fac81", + "frame_4": "e25fac81", + "frame_5": "e25fac81", + "frame_6": "e25fac81", + "frame_7": "e25fac81", + "frame_8": "e25fac81", + "frame_9": "e25fac81" }, "NoiseFilterModes": "798f40c4", "NoiseStyleBlendModeOnImage": "126228e0", From bf644e5913a0f97e140ae8e5d866b1e529a919bf Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 15 Jun 2026 16:36:56 +0800 Subject: [PATCH 51/52] Apply code formatting to writeNoiseResultBlend parameter alignment. --- src/pagx/svg/SVGExporter.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index 9331495206..d84ab8b6c5 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -1122,9 +1122,8 @@ std::string SVGWriter::writeNoiseMultiCore(float size, float density, float seed return "final" + id; } -void SVGWriter::writeNoiseResultBlend(const std::string& clippedResult, - const std::string& filterId, BlendMode blendMode, - std::string& currentSource) { +void SVGWriter::writeNoiseResultBlend(const std::string& clippedResult, const std::string& filterId, + BlendMode blendMode, std::string& currentSource) { auto resultName = "noiseOut" + filterId; _defs->openElement("feBlend"); _defs->addAttribute("in", clippedResult); From fbe3d73f818179a836497363210d1cdf0acf8346 Mon Sep 17 00:00:00 2001 From: zfw123456 <1559632263@qq.com> Date: Mon, 15 Jun 2026 20:09:36 +0800 Subject: [PATCH 52/52] Add design intent comment for NoiseStyle SourceGraphic blend and strengthen SVG assertions. --- src/pagx/svg/SVGExporter.cpp | 3 +++ test/src/PAGXTest.cpp | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index d84ab8b6c5..f05f515f11 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -1416,6 +1416,9 @@ void SVGWriter::writeStyleList(const std::vector& styles, int& shad auto modeStr = BlendModeToFEBlendString(noise->blendMode); if (modeStr) { std::string blendResult = result + "Blended"; + // NoiseStyle blends against SourceGraphic (not currentSource) because in the tgfx + // runtime, LayerStyles execute before LayerFilters and blend against the original + // layer content, not the filter chain output. _defs->openElement("feBlend"); _defs->addAttribute("in", result); _defs->addAttribute("in2", "SourceGraphic"); diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index e920c8a400..2c5fb92458 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -7604,6 +7604,8 @@ PAGX_TEST(PAGXTest, NoiseStyleBlendModeOnImage) { EXPECT_FALSE(svg.empty()); EXPECT_NE(svg.find("feTurbulence"), std::string::npos); EXPECT_NE(svg.find("