diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc index 648508dd2..3241735fe 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc @@ -432,6 +432,7 @@ void native_document::append_scene( struct effect_clip_geometry final { layout_rect bounds; float radius{}; + std::string path; }; const auto resolve_effect_clip = [&]() -> std::optional { const auto& effects = node.style.textual().effect_values; @@ -440,6 +441,7 @@ void native_document::append_scene( const std::string_view value = known->second; constexpr std::string_view inset_prefix = "inset("; constexpr std::string_view circle_prefix = "circle("; + constexpr std::string_view polygon_prefix = "polygon("; if (value.empty() || value.back() != ')') return std::nullopt; const auto has_prefix = [&](std::string_view prefix) { if (value.size() < prefix.size() + 1U) return false; @@ -475,6 +477,64 @@ void native_document::append_scene( radius * 2.0F}, radius}; } + if (has_prefix(polygon_prefix)) { + auto arguments = value.substr( + polygon_prefix.size(), value.size() - polygon_prefix.size() - 1U); + const auto trim = [](std::string_view token) { + while (!token.empty() + && std::isspace(static_cast(token.front()))) { + token.remove_prefix(1U); + } + while (!token.empty() + && std::isspace(static_cast(token.back()))) { + token.remove_suffix(1U); + } + return token; + }; + std::ostringstream path; + path.precision(std::numeric_limits::max_digits10); + size_t point_count = 0U; + size_t cursor = 0U; + while (cursor <= arguments.size()) { + const auto comma = arguments.find(',', cursor); + const auto point = trim(arguments.substr( + cursor, + comma == std::string_view::npos + ? arguments.size() - cursor : comma - cursor)); + const auto separator = point.find_first_of(" \t\r\n\f"); + if (separator == std::string_view::npos || point_count == 64U) { + return std::nullopt; + } + const auto x_token = point.substr(0U, separator); + const auto y_token = trim(point.substr(separator + 1U)); + if (y_token.empty() + || y_token.find_first_of(" \t\r\n\f") != std::string_view::npos) { + return std::nullopt; + } + const auto x = css::parse_ascii_inset_length(x_token); + const auto y = css::parse_ascii_inset_length(y_token); + if (!x.has_value() || !y.has_value()) return std::nullopt; + const auto resolved_x = node.layout.x + + (x->percent ? node.layout.width * x->value / 100.0F : x->value); + const auto resolved_y = node.layout.y + + (y->percent ? node.layout.height * y->value / 100.0F : y->value); + path << (point_count == 0U ? "M " : " L ") + << resolved_x << ' ' << resolved_y; + ++point_count; + if (comma == std::string_view::npos) break; + cursor = comma + 1U; + } + if (point_count < 3U) return std::nullopt; + path << " Z"; + return effect_clip_geometry{ + layout_rect{ + node.layout.x, + node.layout.y, + node.layout.width, + node.layout.height}, + 0.0F, + path.str()}; + } if (!has_prefix(inset_prefix)) return std::nullopt; const std::string_view arguments(value.data() + 6U, value.size() - 7U); std::array parsed{}; @@ -516,9 +576,14 @@ void native_document::append_scene( }; const auto effect_clip = resolve_effect_clip(); if (effect_clip.has_value()) { + constexpr uint32_t polygon_clip_resource = 1U << 31U; + const auto flags = effect_clip->path.empty() + ? 0U + : polygon_clip_resource + | append_scene_string(effect_clip->path, strings, string_bytes); commands.push_back(webscene_scene_command{ 12U, - 0U, + flags, effect_clip->bounds.x, effect_clip->bounds.y, effect_clip->bounds.width, diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h index 38a719b7a..6417c6ca7 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h @@ -152,6 +152,9 @@ typedef struct webscene_scene_header { // (background/foreground pairs). stroke_width carries the width in CSS pixels. // Shadow kinds 17/18: flags bit 0 selects an inverse rounded hole; // producers must bracket inverse shadows with clip commands 12/13. +// Clip kind 12 uses flags bit 31 to select an SVG path stored in the indexed +// scene string; the remaining bits are its string index. A zero flag retains +// the rounded-rectangle fields used by existing producers and presenters. typedef struct webscene_scene_command { uint32_t kind; uint32_t flags; diff --git a/src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs b/src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs index b70dca875..4e3adf0b2 100644 --- a/src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs +++ b/src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs @@ -42,6 +42,8 @@ internal sealed unsafe partial class NativeCanvasSceneRenderer private const uint LayerRemove = 2; private const uint LayerUnchangedPrefix = 4; private const uint OffscreenCanvasLayer = 1u << 31; + private const uint DomPolygonClipResource = 1u << 31; + private const uint DomPolygonClipIndexMask = ~DomPolygonClipResource; private readonly Dictionary s_layers = new(); private readonly List s_orderedLayers = []; @@ -799,12 +801,14 @@ private static bool ValidateLayer(NativeSceneView* view, in NativeCanvasLayer la case 12: backdrop.Save(); overlay.Save(); - ClipDomRoundedRect( + ClipDomShape( backdrop, + view, command, ResolveDomCornerRadii(commands, commandIndex)); - ClipDomRoundedRect( + ClipDomShape( overlay, + view, command, ResolveDomCornerRadii(commands, commandIndex)); break; @@ -1771,6 +1775,31 @@ private static void ClipDomRoundedRect( canvas.ClipRoundRect(rounded, SKClipOperation.Intersect, antialias: true); } + private static void ClipDomShape( + SKCanvas canvas, + NativeSceneView* view, + in SceneCommand command, + in DomCornerRadii radii) + { + if ((command.Flags & DomPolygonClipResource) == 0) + { + ClipDomRoundedRect(canvas, command, radii); + return; + } + ClipDomPath(canvas, DomStringAt(view, command.Flags & DomPolygonClipIndexMask)); + } + + private static void ClipDomPath(SKCanvas canvas, string pathData) + { + using var path = SKPath.ParseSvgPathData(pathData); + if (path is null) + { + canvas.ClipRect(SKRect.Empty, SKClipOperation.Intersect, antialias: false); + return; + } + canvas.ClipPath(path, SKClipOperation.Intersect, antialias: true); + } + private void DrawDomText( SKCanvas canvas, NativeSceneView* view, @@ -3612,7 +3641,7 @@ public bool Matches(DomPictureInput other) => Width == other.Width && Height == && Resources.AsSpan().SequenceEqual(other.Resources); } private sealed record OrderedGpuPaint(SceneCommand Command, DomCornerRadii Radii, SKPicture? Picture, - DomPictureInput? Input = null); + DomPictureInput? Input = null, string? ClipPath = null); private bool ValidateOrderedCanvasPlacements(NativeSceneView* view, bool checkpoint) { @@ -3678,7 +3707,11 @@ private List CompileOrderedGpuDom(NativeSceneView* view) var input = new DomPictureInput( MemoryMarshal.AsBytes(commands.Slice(Math.Max(0, start - 1), index - Math.Max(0, start - 1))).ToArray(), commands.Slice(start, index - start).ToArray() - .Select(command => DomStringAt(view, command.Flags)).ToArray(), + .Select(command => command.Kind == 12 + && (command.Flags & DomPolygonClipResource) != 0 + ? DomStringAt(view, command.Flags & DomPolygonClipIndexMask) + : DomStringAt(view, command.Flags)) + .ToArray(), view->Header.ViewportWidth, view->Header.ViewportHeight, _presenterDeviceScaleFactor, NativeTextShaping.FontRegistrationVersion); var previous = _orderedGpuPaint is not null && result.Count < _orderedGpuPaint.Count @@ -3696,7 +3729,19 @@ private List CompileOrderedGpuDom(NativeSceneView* view) } } if (index != commands.Length) - result.Add(new(commands[index], ResolveDomCornerRadii(commands, index), null)); + { + ref readonly var command = ref commands[index]; + var clipPath = command.Kind == 12 + && (command.Flags & DomPolygonClipResource) != 0 + ? DomStringAt(view, command.Flags & DomPolygonClipIndexMask) + : null; + result.Add(new( + command, + ResolveDomCornerRadii(commands, index), + null, + null, + clipPath)); + } start = index + 1; } return result; @@ -3732,7 +3777,12 @@ private void RenderOrderedGpuDom(SKCanvas canvas, Action? drawGpuI switch (command.Kind) { case 12: - canvas.Save(); ClipDomRoundedRect(canvas, command, entry.Radii); break; + canvas.Save(); + if (entry.ClipPath is null) + ClipDomRoundedRect(canvas, command, entry.Radii); + else + ClipDomPath(canvas, entry.ClipPath); + break; case 15: ApplyScale(canvas, command); break; case 19: ApplyRotation(canvas, command); break; case 30: diff --git a/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart b/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart index 3d351abe9..30fc0e45c 100644 --- a/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart +++ b/src/WebScene.Backend.Flutter/lib/src/scene_projector.dart @@ -17,6 +17,8 @@ const int _sceneComponentReady = 4; const int _layerReplace = 1; const int _layerRemove = 2; const int _canvasEvenOdd = 1 << 16; +const int _domPolygonClipResource = 1 << 31; +const int _domPolygonClipIndexMask = _domPolygonClipResource - 1; final class SceneApplyResult { const SceneApplyResult({ @@ -306,9 +308,8 @@ final class WebSceneSceneProjector extends ChangeNotifier { ..color = _rgba(command.rgba), ); case 12: - canvas - ..save() - ..clipRRect(_domRRect(command), doAntiAlias: true); + canvas.save(); + _clipDomShape(canvas, scene, command); case 13: canvas.restore(); } @@ -939,6 +940,27 @@ final class WebSceneSceneProjector extends ChangeNotifier { .map((match) => double.parse(match.group(0)!)) .toList(); + static void _clipDomShape( + ui.Canvas canvas, + WebSceneSceneView scene, + WebSceneSceneCommand command, + ) { + if (command.flags & _domPolygonClipResource == 0) { + canvas.clipRRect(_domRRect(command), doAntiAlias: true); + return; + } + try { + canvas.clipPath( + parseSvgPathData( + _domString(scene, command.flags & _domPolygonClipIndexMask), + ), + doAntiAlias: true, + ); + } catch (_) { + canvas.clipRect(ui.Rect.zero, doAntiAlias: false); + } + } + static String _domString(WebSceneSceneView scene, int index) => _stringAt(scene, index);