| /* |
| * Copyright (C) 2004, 2005, 2007 Nikolas Zimmermann <zimmermann@kde.org> |
| * Copyright (C) 2004, 2005, 2008 Rob Buis <buis@kde.org> |
| * Copyright (C) 2005, 2007 Eric Seidel <eric@webkit.org> |
| * Copyright (C) 2009 Google, Inc. |
| * Copyright (C) 2009 Dirk Schulze <krit@webkit.org> |
| * Copyright (C) Research In Motion Limited 2010. All rights reserved. |
| * Copyright (C) 2009 Jeff Schiller <codedread@gmail.com> |
| * Copyright (C) 2011 Renata Hodovan <reni@webkit.org> |
| * Copyright (C) 2011 University of Szeged |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public License |
| * along with this library; see the file COPYING.LIB. If not, write to |
| * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| #include "third_party/blink/renderer/core/layout/svg/layout_svg_shape.h" |
| |
| #include "third_party/blink/renderer/core/layout/hit_test_result.h" |
| #include "third_party/blink/renderer/core/layout/layout_analyzer.h" |
| #include "third_party/blink/renderer/core/layout/pointer_events_hit_rules.h" |
| #include "third_party/blink/renderer/core/layout/svg/layout_svg_resource_paint_server.h" |
| #include "third_party/blink/renderer/core/layout/svg/layout_svg_root.h" |
| #include "third_party/blink/renderer/core/layout/svg/svg_layout_support.h" |
| #include "third_party/blink/renderer/core/layout/svg/svg_resources.h" |
| #include "third_party/blink/renderer/core/layout/svg/transform_helper.h" |
| #include "third_party/blink/renderer/core/layout/svg/transformed_hit_test_location.h" |
| #include "third_party/blink/renderer/core/paint/svg_shape_painter.h" |
| #include "third_party/blink/renderer/core/svg/svg_geometry_element.h" |
| #include "third_party/blink/renderer/core/svg/svg_length_context.h" |
| #include "third_party/blink/renderer/platform/geometry/float_point.h" |
| #include "third_party/blink/renderer/platform/graphics/stroke_data.h" |
| #include "third_party/blink/renderer/platform/wtf/math_extras.h" |
| |
| namespace blink { |
| |
| LayoutSVGShape::LayoutSVGShape(SVGGeometryElement* node, |
| StrokeGeometryClass geometry_class) |
| : LayoutSVGModelObject(node), |
| // Geometry classification - used to compute stroke bounds more |
| // efficiently. |
| geometry_class_(geometry_class), |
| // Default is false, the cached rects are empty from the beginning. |
| needs_boundaries_update_(false), |
| // Default is true, so we grab a Path object once from SVGGeometryElement. |
| needs_shape_update_(true), |
| // Default is true, so we grab a AffineTransform object once from |
| // SVGGeometryElement. |
| needs_transform_update_(true), |
| transform_uses_reference_box_(false) {} |
| |
| LayoutSVGShape::~LayoutSVGShape() = default; |
| |
| void LayoutSVGShape::StyleDidChange(StyleDifference diff, |
| const ComputedStyle* old_style) { |
| NOT_DESTROYED(); |
| LayoutSVGModelObject::StyleDidChange(diff, old_style); |
| |
| transform_uses_reference_box_ = |
| TransformHelper::DependsOnReferenceBox(StyleRef()); |
| SVGResources::UpdatePaints(*GetElement(), old_style, StyleRef()); |
| |
| // Most of the stroke attributes (caps, joins, miters, width, etc.) will cause |
| // a re-layout which will clear the stroke-path cache; however, there are a |
| // couple of additional properties that *won't* cause a layout, but are |
| // significant enough to require invalidating the cache. |
| if (!diff.NeedsFullLayout() && old_style && stroke_path_cache_) { |
| const ComputedStyle& style = StyleRef(); |
| if (old_style->StrokeDashOffset() != style.StrokeDashOffset() || |
| *old_style->StrokeDashArray() != *style.StrokeDashArray()) { |
| stroke_path_cache_.reset(); |
| } |
| } |
| |
| SetTransformAffectsVectorEffect(HasNonScalingStroke()); |
| } |
| |
| void LayoutSVGShape::WillBeDestroyed() { |
| NOT_DESTROYED(); |
| SVGResources::ClearPaints(*GetElement(), Style()); |
| LayoutSVGModelObject::WillBeDestroyed(); |
| } |
| |
| void LayoutSVGShape::ClearPath() { |
| NOT_DESTROYED(); |
| path_.reset(); |
| stroke_path_cache_.reset(); |
| } |
| |
| void LayoutSVGShape::CreatePath() { |
| NOT_DESTROYED(); |
| if (!path_) |
| path_ = std::make_unique<Path>(); |
| *path_ = To<SVGGeometryElement>(GetElement())->AsPath(); |
| |
| // When the path changes, we need to ensure the stale stroke path cache is |
| // cleared. Because this is done in all callsites, we can just DCHECK that it |
| // has been cleared here. |
| DCHECK(!stroke_path_cache_); |
| } |
| |
| float LayoutSVGShape::DashScaleFactor() const { |
| NOT_DESTROYED(); |
| if (!StyleRef().HasDashArray()) |
| return 1; |
| return To<SVGGeometryElement>(*GetElement()).PathLengthScaleFactor(); |
| } |
| |
| void LayoutSVGShape::UpdateShapeFromElement() { |
| NOT_DESTROYED(); |
| CreatePath(); |
| fill_bounding_box_ = GetPath().TightBoundingRect(); |
| |
| if (HasNonScalingStroke()) { |
| // NonScalingStrokeTransform may depend on LocalTransform which in turn may |
| // depend on ObjectBoundingBox, thus we need to call them in this order. |
| local_transform_ = CalculateLocalTransform(); |
| UpdateNonScalingStrokeData(); |
| } |
| |
| stroke_bounding_box_ = CalculateStrokeBoundingBox(); |
| } |
| |
| namespace { |
| |
| bool HasMiterJoinStyle(const ComputedStyle& style) { |
| return style.JoinStyle() == kMiterJoin; |
| } |
| bool HasSquareCapStyle(const ComputedStyle& style) { |
| return style.CapStyle() == kSquareCap; |
| } |
| |
| } // namespace |
| |
| FloatRect LayoutSVGShape::ApproximateStrokeBoundingBox( |
| const FloatRect& shape_bounds) const { |
| NOT_DESTROYED(); |
| FloatRect stroke_box = shape_bounds; |
| |
| // Implementation of |
| // https://drafts.fxtf.org/css-masking/#compute-stroke-bounding-box |
| // except that we ignore whether the stroke is none. |
| |
| const float stroke_width = StrokeWidth(); |
| if (stroke_width <= 0) |
| return stroke_box; |
| |
| float delta = stroke_width / 2; |
| if (geometry_class_ != kSimple) { |
| const ComputedStyle& style = StyleRef(); |
| if (geometry_class_ != kNoMiters && HasMiterJoinStyle(style)) { |
| const float miter = style.StrokeMiterLimit(); |
| if (miter < M_SQRT2 && HasSquareCapStyle(style)) |
| delta *= M_SQRT2; |
| else |
| delta *= std::max(miter, 1.0f); |
| } else if (HasSquareCapStyle(style)) { |
| delta *= M_SQRT2; |
| } |
| } |
| stroke_box.Inflate(delta); |
| return stroke_box; |
| } |
| |
| FloatRect LayoutSVGShape::HitTestStrokeBoundingBox() const { |
| NOT_DESTROYED(); |
| if (StyleRef().HasStroke()) |
| return stroke_bounding_box_; |
| return ApproximateStrokeBoundingBox(fill_bounding_box_); |
| } |
| |
| bool LayoutSVGShape::ShapeDependentStrokeContains( |
| const HitTestLocation& location) { |
| NOT_DESTROYED(); |
| if (!stroke_path_cache_) { |
| // In case the subclass didn't create path during UpdateShapeFromElement() |
| // for optimization but still calls this method. |
| if (!HasPath()) |
| CreatePath(); |
| |
| StrokeData stroke_data; |
| SVGLayoutSupport::ApplyStrokeStyleToStrokeData(stroke_data, StyleRef(), |
| *this, DashScaleFactor()); |
| |
| if (HasNonScalingStroke()) { |
| // The reason is similar to the above code about HasPath(). |
| if (!rare_data_) |
| UpdateNonScalingStrokeData(); |
| |
| // Un-scale to get back to the root-transform (cheaper than re-computing |
| // the root transform from scratch). |
| AffineTransform root_transform; |
| root_transform.Scale(StyleRef().EffectiveZoom()) |
| .Multiply(NonScalingStrokeTransform()); |
| |
| stroke_path_cache_ = std::make_unique<Path>( |
| NonScalingStrokePath().StrokePath(stroke_data, root_transform)); |
| } else { |
| stroke_path_cache_ = std::make_unique<Path>( |
| path_->StrokePath(stroke_data, ComputeRootTransform())); |
| } |
| } |
| |
| DCHECK(stroke_path_cache_); |
| auto point = location.TransformedPoint(); |
| if (HasNonScalingStroke()) |
| point = NonScalingStrokeTransform().MapPoint(point); |
| return stroke_path_cache_->Contains(point); |
| } |
| |
| bool LayoutSVGShape::ShapeDependentFillContains( |
| const HitTestLocation& location, |
| const WindRule fill_rule) const { |
| NOT_DESTROYED(); |
| return GetPath().Contains(location.TransformedPoint(), fill_rule); |
| } |
| |
| static bool HasPaintServer(const LayoutObject& object, const SVGPaint& paint) { |
| if (paint.HasColor()) |
| return true; |
| if (paint.HasUrl()) { |
| SVGResourceClient* client = SVGResources::GetClient(object); |
| if (GetSVGResourceAsType<LayoutSVGResourcePaintServer>(*client, |
| paint.Resource())) |
| return true; |
| } |
| return false; |
| } |
| |
| bool LayoutSVGShape::FillContains(const HitTestLocation& location, |
| bool requires_fill, |
| const WindRule fill_rule) { |
| NOT_DESTROYED(); |
| if (!fill_bounding_box_.Contains(location.TransformedPoint())) |
| return false; |
| |
| if (requires_fill && !HasPaintServer(*this, StyleRef().FillPaint())) |
| return false; |
| |
| return ShapeDependentFillContains(location, fill_rule); |
| } |
| |
| bool LayoutSVGShape::StrokeContains(const HitTestLocation& location, |
| bool requires_stroke) { |
| NOT_DESTROYED(); |
| // "A zero value causes no stroke to be painted." |
| if (StyleRef().StrokeWidth().IsZero()) |
| return false; |
| |
| if (requires_stroke) { |
| if (!StrokeBoundingBox().Contains(location.TransformedPoint())) |
| return false; |
| |
| if (!HasPaintServer(*this, StyleRef().StrokePaint())) |
| return false; |
| } else { |
| if (!HitTestStrokeBoundingBox().Contains(location.TransformedPoint())) |
| return false; |
| } |
| |
| return ShapeDependentStrokeContains(location); |
| } |
| |
| void LayoutSVGShape::UpdateLayout() { |
| NOT_DESTROYED(); |
| LayoutAnalyzer::Scope analyzer(*this); |
| |
| // The cached stroke may be affected by the ancestor transform, and so needs |
| // to be cleared regardless of whether the shape or bounds have changed. |
| stroke_path_cache_.reset(); |
| |
| bool update_parent_boundaries = false; |
| bool bbox_changed = false; |
| // UpdateShapeFromElement() also updates the object & stroke bounds - which |
| // feeds into the visual rect - so we need to call it for both the |
| // shape-update and the bounds-update flag. |
| // We also need to update stroke bounds if HasNonScalingStroke() because the |
| // shape may be affected by ancestor transforms. |
| if (needs_shape_update_ || needs_boundaries_update_ || |
| HasNonScalingStroke()) { |
| FloatRect old_object_bounding_box = ObjectBoundingBox(); |
| UpdateShapeFromElement(); |
| if (old_object_bounding_box != ObjectBoundingBox()) { |
| SetShouldDoFullPaintInvalidation(); |
| bbox_changed = true; |
| } |
| needs_shape_update_ = false; |
| needs_boundaries_update_ = false; |
| update_parent_boundaries = true; |
| } |
| |
| // Invalidate all resources of this client if our reference box changed. |
| if (EverHadLayout() && bbox_changed) { |
| SVGResourceInvalidator resource_invalidator(*this); |
| resource_invalidator.InvalidateEffects(); |
| resource_invalidator.InvalidatePaints(); |
| } |
| |
| if (!needs_transform_update_ && transform_uses_reference_box_) { |
| needs_transform_update_ = CheckForImplicitTransformChange(bbox_changed); |
| if (needs_transform_update_) |
| SetNeedsPaintPropertyUpdate(); |
| } |
| |
| if (needs_transform_update_) { |
| local_transform_ = CalculateLocalTransform(); |
| needs_transform_update_ = false; |
| update_parent_boundaries = true; |
| } |
| |
| // If our bounds changed, notify the parents. |
| if (update_parent_boundaries) |
| LayoutSVGModelObject::SetNeedsBoundariesUpdate(); |
| |
| DCHECK(!needs_shape_update_); |
| DCHECK(!needs_boundaries_update_); |
| DCHECK(!needs_transform_update_); |
| ClearNeedsLayout(); |
| } |
| |
| AffineTransform LayoutSVGShape::ComputeRootTransform() const { |
| NOT_DESTROYED(); |
| const LayoutObject* root = this; |
| while (root && !root->IsSVGRoot()) |
| root = root->Parent(); |
| return LocalToAncestorTransform(To<LayoutSVGRoot>(root)).ToAffineTransform(); |
| } |
| |
| AffineTransform LayoutSVGShape::ComputeNonScalingStrokeTransform() const { |
| NOT_DESTROYED(); |
| // Compute the CTM to the SVG root. This should probably be the CTM all the |
| // way to the "canvas" of the page ("host" coordinate system), but with our |
| // current approach of applying/painting non-scaling-stroke, that can break in |
| // unpleasant ways (see crbug.com/747708 for an example.) Maybe it would be |
| // better to apply this effect during rasterization? |
| AffineTransform host_transform; |
| host_transform.Scale(1 / StyleRef().EffectiveZoom()) |
| .Multiply(ComputeRootTransform()); |
| |
| // Width of non-scaling stroke is independent of translation, so zero it out |
| // here. |
| host_transform.SetE(0); |
| host_transform.SetF(0); |
| return host_transform; |
| } |
| |
| void LayoutSVGShape::UpdateNonScalingStrokeData() { |
| NOT_DESTROYED(); |
| DCHECK(HasNonScalingStroke()); |
| |
| const AffineTransform transform = ComputeNonScalingStrokeTransform(); |
| auto& rare_data = EnsureRareData(); |
| if (rare_data.non_scaling_stroke_transform_ != transform) { |
| SetShouldDoFullPaintInvalidation(PaintInvalidationReason::kStyle); |
| rare_data.non_scaling_stroke_transform_ = transform; |
| } |
| |
| rare_data.non_scaling_stroke_path_ = *path_; |
| rare_data.non_scaling_stroke_path_.Transform(transform); |
| } |
| |
| void LayoutSVGShape::Paint(const PaintInfo& paint_info) const { |
| NOT_DESTROYED(); |
| SVGShapePainter(*this).Paint(paint_info); |
| } |
| |
| bool LayoutSVGShape::NodeAtPoint(HitTestResult& result, |
| const HitTestLocation& hit_test_location, |
| const PhysicalOffset& accumulated_offset, |
| HitTestAction hit_test_action) { |
| NOT_DESTROYED(); |
| DCHECK_EQ(accumulated_offset, PhysicalOffset()); |
| // We only draw in the foreground phase, so we only hit-test then. |
| if (hit_test_action != kHitTestForeground) |
| return false; |
| if (IsShapeEmpty()) |
| return false; |
| const ComputedStyle& style = StyleRef(); |
| const PointerEventsHitRules hit_rules( |
| PointerEventsHitRules::SVG_GEOMETRY_HITTESTING, |
| result.GetHitTestRequest(), style.PointerEvents()); |
| if (hit_rules.require_visible && style.Visibility() != EVisibility::kVisible) |
| return false; |
| |
| TransformedHitTestLocation local_location(hit_test_location, |
| LocalToSVGParentTransform()); |
| if (!local_location) |
| return false; |
| if (!SVGLayoutSupport::IntersectsClipPath(*this, fill_bounding_box_, |
| *local_location)) |
| return false; |
| |
| if (HitTestShape(result.GetHitTestRequest(), *local_location, hit_rules)) { |
| UpdateHitTestResult(result, PhysicalOffset::FromFloatPointRound( |
| local_location->TransformedPoint())); |
| if (result.AddNodeToListBasedTestResult(GetElement(), *local_location) == |
| kStopHitTesting) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool LayoutSVGShape::HitTestShape(const HitTestRequest& request, |
| const HitTestLocation& local_location, |
| PointerEventsHitRules hit_rules) { |
| NOT_DESTROYED(); |
| if (hit_rules.can_hit_bounding_box && |
| local_location.Intersects(ObjectBoundingBox())) |
| return true; |
| |
| // TODO(chrishtr): support rect-based intersections in the cases below. |
| const ComputedStyle& style = StyleRef(); |
| if (hit_rules.can_hit_stroke && |
| (style.HasStroke() || !hit_rules.require_stroke) && |
| StrokeContains(local_location, hit_rules.require_stroke)) |
| return true; |
| WindRule fill_rule = style.FillRule(); |
| if (request.SvgClipContent()) |
| fill_rule = style.ClipRule(); |
| if (hit_rules.can_hit_fill && (style.HasFill() || !hit_rules.require_fill) && |
| FillContains(local_location, hit_rules.require_fill, fill_rule)) |
| return true; |
| return false; |
| } |
| |
| FloatRect LayoutSVGShape::CalculateStrokeBoundingBox() const { |
| NOT_DESTROYED(); |
| if (!StyleRef().HasStroke() || IsShapeEmpty()) |
| return fill_bounding_box_; |
| if (HasNonScalingStroke()) |
| return CalculateNonScalingStrokeBoundingBox(); |
| return ApproximateStrokeBoundingBox(fill_bounding_box_); |
| } |
| |
| FloatRect LayoutSVGShape::CalculateNonScalingStrokeBoundingBox() const { |
| NOT_DESTROYED(); |
| DCHECK(path_); |
| DCHECK(StyleRef().HasStroke()); |
| DCHECK(HasNonScalingStroke()); |
| |
| FloatRect stroke_bounding_box = fill_bounding_box_; |
| const auto& non_scaling_transform = NonScalingStrokeTransform(); |
| if (non_scaling_transform.IsInvertible()) { |
| const auto& non_scaling_stroke = NonScalingStrokePath(); |
| FloatRect stroke_bounding_rect = |
| ApproximateStrokeBoundingBox(non_scaling_stroke.BoundingRect()); |
| stroke_bounding_rect = |
| non_scaling_transform.Inverse().MapRect(stroke_bounding_rect); |
| stroke_bounding_box.Unite(stroke_bounding_rect); |
| } |
| return stroke_bounding_box; |
| } |
| |
| float LayoutSVGShape::StrokeWidth() const { |
| NOT_DESTROYED(); |
| SVGLengthContext length_context(GetElement()); |
| return length_context.ValueForLength(StyleRef().StrokeWidth()); |
| } |
| |
| float LayoutSVGShape::StrokeWidthForMarkerUnits() const { |
| NOT_DESTROYED(); |
| float stroke_width = StrokeWidth(); |
| if (HasNonScalingStroke()) { |
| const auto& non_scaling_transform = NonScalingStrokeTransform(); |
| if (!non_scaling_transform.IsInvertible()) |
| return 0; |
| float scale_factor = |
| clampTo<float>(sqrt((non_scaling_transform.XScaleSquared() + |
| non_scaling_transform.YScaleSquared()) / |
| 2)); |
| stroke_width /= scale_factor; |
| } |
| return stroke_width; |
| } |
| |
| LayoutSVGShapeRareData& LayoutSVGShape::EnsureRareData() const { |
| NOT_DESTROYED(); |
| if (!rare_data_) |
| rare_data_ = std::make_unique<LayoutSVGShapeRareData>(); |
| return *rare_data_.get(); |
| } |
| |
| RasterEffectOutset LayoutSVGShape::VisualRectOutsetForRasterEffects() const { |
| NOT_DESTROYED(); |
| // Account for raster expansions due to SVG stroke hairline raster effects. |
| const ComputedStyle& style = StyleRef(); |
| if (style.HasVisibleStroke()) { |
| if (style.CapStyle() != kButtCap) |
| return RasterEffectOutset::kWholePixel; |
| return RasterEffectOutset::kHalfPixel; |
| } |
| return RasterEffectOutset::kNone; |
| } |
| |
| } // namespace blink |