Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<effect_clip_geometry> {
const auto& effects = node.style.textual().effect_values;
Expand All @@ -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;
Expand Down Expand Up @@ -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<unsigned char>(token.front()))) {
token.remove_prefix(1U);
}
while (!token.empty()
&& std::isspace(static_cast<unsigned char>(token.back()))) {
token.remove_suffix(1U);
}
return token;
};
std::ostringstream path;
path.precision(std::numeric_limits<float>::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<css::ascii_inset_length, 4> parsed{};
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
62 changes: 56 additions & 6 deletions src/WebScene.Backend.Avalonia/NativeCanvasSceneRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint, RetainedLayer> s_layers = new();
private readonly List<RetainedLayer> s_orderedLayers = [];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -3678,7 +3707,11 @@ private List<OrderedGpuPaint> 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
Expand All @@ -3696,7 +3729,19 @@ private List<OrderedGpuPaint> 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;
Expand Down Expand Up @@ -3732,7 +3777,12 @@ private void RenderOrderedGpuDom(SKCanvas canvas, Action<uint, SKRect>? 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:
Expand Down
28 changes: 25 additions & 3 deletions src/WebScene.Backend.Flutter/lib/src/scene_projector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);

Expand Down
Loading