Bug 1934525 - Implement new path data API r=emilio

Differential Revision: https://phabricator.services.mozilla.com/D231736
This commit is contained in:
longsonr
2025-01-10 20:18:12 +00:00
parent 541bba0960
commit 67f4b34001
23 changed files with 980 additions and 87 deletions

View File

@@ -12,6 +12,7 @@
#include "mozilla/SMILValue.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/dom/SVGElement.h"
#include "mozilla/dom/SVGPathSegment.h"
using namespace mozilla::dom;
@@ -26,6 +27,24 @@ nsresult SVGAnimatedPathSegList::SetBaseValueString(const nsAString& aValue) {
return mBaseVal.SetValueFromString(NS_ConvertUTF16toUTF8(aValue));
}
void SVGAnimatedPathSegList::SetBaseValueFromPathSegments(
const Sequence<OwningNonNull<SVGPathSegment>>& aValues) {
AutoTArray<StylePathCommand, 10> pathData;
if (!aValues.IsEmpty() && aValues[0].ref().IsMove()) {
for (const auto& seg : aValues) {
if (!seg.ref().IsValid()) {
break;
}
pathData.AppendElement(seg.ref().ToStylePathCommand());
}
}
if (pathData.IsEmpty()) {
mBaseVal.Clear();
return;
}
Servo_CreatePathDataFromCommands(&pathData, &mBaseVal.RawData());
}
void SVGAnimatedPathSegList::ClearBaseValue() {
mBaseVal.Clear();
// Caller notifies

View File

@@ -20,6 +20,7 @@ class SMILValue;
namespace dom {
class SVGAnimationElement;
class SVGElement;
class SVGPathSegment;
} // namespace dom
/**
@@ -59,6 +60,9 @@ class SVGAnimatedPathSegList final {
nsresult SetBaseValueString(const nsAString& aValue);
void SetBaseValueFromPathSegments(
const dom::Sequence<OwningNonNull<dom::SVGPathSegment>>& aValues);
void ClearBaseValue();
/**

View File

@@ -8,6 +8,7 @@
#include "gfx2DGlue.h"
#include "gfxPlatform.h"
#include "mozilla/dom/SVGPathSegment.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/gfx/Types.h"
#include "mozilla/gfx/Point.h"
@@ -71,6 +72,21 @@ bool SVGPathData::GetDistancesFromOriginToEndsOfVisibleSegments(
return true;
}
/* static */
already_AddRefed<dom::SVGPathSegment> SVGPathData::GetPathSegmentAtLength(
dom::SVGPathElement* aPathElement, Span<const StylePathCommand> aPath,
float aDistance) {
SVGPathTraversalState state;
for (const auto& cmd : aPath) {
SVGPathSegUtils::TraversePathSegment(cmd, state);
if (state.length >= aDistance) {
return do_AddRef(new dom::SVGPathSegment(aPathElement, cmd));
}
}
return nullptr;
}
/**
* The SVG spec says we have to paint stroke caps for zero length subpaths:
*

View File

@@ -26,6 +26,10 @@ namespace mozilla {
struct SVGMark;
enum class StyleStrokeLinecap : uint8_t;
namespace dom {
class SVGPathElement;
class SVGPathSegment;
} // namespace dom
class SVGPathData {
friend class SVGAnimatedPathSegList;
@@ -66,6 +70,10 @@ class SVGPathData {
const StyleSVGPathData& RawData() const { return mData; }
static already_AddRefed<dom::SVGPathSegment> GetPathSegmentAtLength(
dom::SVGPathElement* aPathElement, Span<const StylePathCommand> aPath,
float aDistance);
void GetMarkerPositioningData(float aZoom, nsTArray<SVGMark>* aMarks) const;
static void GetMarkerPositioningData(Span<const StylePathCommand> aPath,

View File

@@ -11,16 +11,19 @@
#include "SVGGeometryProperty.h"
#include "gfx2DGlue.h"
#include "gfxPlatform.h"
#include "mozAutoDocUpdate.h"
#include "nsGkAtoms.h"
#include "nsIFrame.h"
#include "nsStyleConsts.h"
#include "nsStyleStruct.h"
#include "nsWindowSizes.h"
#include "mozilla/dom/SVGPathElementBinding.h"
#include "mozilla/dom/SVGPathSegment.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/RefPtr.h"
#include "SVGPathSegUtils.h"
#include "mozilla/SVGContentUtils.h"
#include "SVGArcConverter.h"
#include "SVGPathSegUtils.h"
NS_IMPL_NS_NEW_SVG_ELEMENT(Path)
@@ -28,6 +31,31 @@ using namespace mozilla::gfx;
namespace mozilla::dom {
//----------------------------------------------------------------------
// Helper class: AutoChangePathSegListNotifier
// Stack-based helper class to pair calls to WillChangePathSegList and
// DidChangePathSegList.
class MOZ_RAII AutoChangePathSegListNotifier : public mozAutoDocUpdate {
public:
explicit AutoChangePathSegListNotifier(SVGPathElement* aSVGPathElement)
: mozAutoDocUpdate(aSVGPathElement->GetComposedDoc(), true),
mSVGElement(aSVGPathElement) {
MOZ_ASSERT(mSVGElement, "Expecting non-null value");
mEmptyOrOldValue = mSVGElement->WillChangePathSegList(*this);
}
~AutoChangePathSegListNotifier() {
mSVGElement->DidChangePathSegList(mEmptyOrOldValue, *this);
if (mSVGElement->GetAnimPathSegList()->IsAnimating()) {
mSVGElement->AnimationNeedsResample();
}
}
private:
SVGPathElement* const mSVGElement;
nsAttrValue mEmptyOrOldValue;
};
JSObject* SVGPathElement::WrapNode(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return SVGPathElement_Binding::Wrap(aCx, this, aGivenProto);
@@ -54,6 +82,106 @@ void SVGPathElement::AddSizeOfExcludingThis(nsWindowSizes& aSizes,
NS_IMPL_ELEMENT_CLONE_WITH_INIT(SVGPathElement)
already_AddRefed<SVGPathSegment> SVGPathElement::GetPathSegmentAtLength(
float aDistance) {
FlushIfNeeded();
RefPtr<SVGPathSegment> segment;
if (SVGGeometryProperty::DoForComputedStyle(
this, [&](const ComputedStyle* s) {
const auto& d = s->StyleSVGReset()->mD;
if (d.IsPath()) {
segment = SVGPathData::GetPathSegmentAtLength(
this, d.AsPath()._0.AsSpan(), aDistance);
}
})) {
return segment.forget();
}
return SVGPathData::GetPathSegmentAtLength(this, mD.GetAnimValue().AsSpan(),
aDistance);
}
static void CreatePathSegments(SVGPathElement* aPathElement,
const StyleSVGPathData& aPathData,
nsTArray<RefPtr<SVGPathSegment>>& aValues,
bool aNormalize) {
if (aNormalize) {
StyleSVGPathData normalizedPathData;
Servo_SVGPathData_NormalizeAndReduce(&aPathData, &normalizedPathData);
Point pathStart(0.0, 0.0);
Point segStart(0.0, 0.0);
Point segEnd(0.0, 0.0);
for (const auto& cmd : normalizedPathData._0.AsSpan()) {
switch (cmd.tag) {
case StylePathCommand::Tag::Close:
segEnd = pathStart;
aValues.AppendElement(new SVGPathSegment(aPathElement, cmd));
break;
case StylePathCommand::Tag::Move:
pathStart = segEnd = cmd.move.point.ToGfxPoint();
aValues.AppendElement(new SVGPathSegment(aPathElement, cmd));
break;
case StylePathCommand::Tag::Line:
segEnd = cmd.line.point.ToGfxPoint();
aValues.AppendElement(new SVGPathSegment(aPathElement, cmd));
break;
case StylePathCommand::Tag::CubicCurve:
segEnd = cmd.cubic_curve.point.ToGfxPoint();
aValues.AppendElement(new SVGPathSegment(aPathElement, cmd));
break;
case StylePathCommand::Tag::Arc: {
const auto& arc = cmd.arc;
segEnd = arc.point.ToGfxPoint();
SVGArcConverter converter(segStart, arc.point.ToGfxPoint(),
arc.radii.ToGfxPoint(), arc.rotate,
arc.arc_size == StyleArcSize::Large,
arc.arc_sweep == StyleArcSweep::Cw);
Point cp1, cp2;
while (converter.GetNextSegment(&cp1, &cp2, &segEnd)) {
auto curve = StylePathCommand::CubicCurve(
StyleByTo::To,
StyleCoordinatePair<StyleCSSFloat>{segEnd.x, segEnd.y},
StyleCoordinatePair<StyleCSSFloat>{cp1.x, cp1.y},
StyleCoordinatePair<StyleCSSFloat>{cp2.x, cp2.y});
aValues.AppendElement(new SVGPathSegment(aPathElement, curve));
}
break;
}
default:
MOZ_ASSERT_UNREACHABLE("Unexpected path command");
break;
}
segStart = segEnd;
}
return;
}
for (const auto& cmd : aPathData._0.AsSpan()) {
aValues.AppendElement(new SVGPathSegment(aPathElement, cmd));
}
}
void SVGPathElement::GetPathData(const SVGPathDataSettings& aOptions,
nsTArray<RefPtr<SVGPathSegment>>& aValues) {
FlushIfNeeded();
if (SVGGeometryProperty::DoForComputedStyle(
this, [&](const ComputedStyle* s) {
const auto& d = s->StyleSVGReset()->mD;
if (d.IsPath()) {
CreatePathSegments(this, d.AsPath(), aValues,
aOptions.mNormalize);
}
})) {
return;
}
CreatePathSegments(this, mD.GetAnimValue().RawData(), aValues,
aOptions.mNormalize);
}
void SVGPathElement::SetPathData(
const Sequence<OwningNonNull<SVGPathSegment>>& aValues) {
AutoChangePathSegListNotifier notifier(this);
mD.SetBaseValueFromPathSegments(aValues);
}
//----------------------------------------------------------------------
// SVGElement methods

View File

@@ -17,6 +17,9 @@ nsresult NS_NewSVGPathElement(
namespace mozilla::dom {
struct SVGPathDataSettings;
class SVGPathSegment;
using SVGPathElementBase = SVGGeometryElement;
class SVGPathElement final : public SVGPathElementBase {
@@ -76,6 +79,14 @@ class SVGPathElement final : public SVGPathElementBase {
nsStaticAtom* GetPathDataAttrName() const override { return nsGkAtoms::d; }
// WebIDL
MOZ_CAN_RUN_SCRIPT
already_AddRefed<SVGPathSegment> GetPathSegmentAtLength(float distance);
MOZ_CAN_RUN_SCRIPT
void GetPathData(const SVGPathDataSettings& aOptions,
nsTArray<RefPtr<SVGPathSegment>>& aValues);
void SetPathData(const Sequence<OwningNonNull<SVGPathSegment>>& aValues);
static bool IsDPropertyChangedViaCSS(const ComputedStyle& aNewStyle,
const ComputedStyle& aOldStyle);

217
dom/svg/SVGPathSegment.cpp Normal file
View File

@@ -0,0 +1,217 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/dom/SVGPathSegment.h"
#include "mozilla/dom/SVGPathElementBinding.h"
#include "SVGPathSegUtils.h"
namespace mozilla::dom {
JSObject* SVGPathSegment::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return SVGPathSegment_Binding::Wrap(aCx, this, aGivenProto);
}
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(SVGPathSegment, mSVGPathElement)
//----------------------------------------------------------------------
// Implementation
SVGPathSegment::SVGPathSegment(SVGPathElement* aSVGPathElement,
const StylePathCommand& aCommand)
: mSVGPathElement(aSVGPathElement) {
switch (aCommand.tag) {
case StylePathCommand::Tag::Close:
mCommand.AssignLiteral("Z");
break;
case StylePathCommand::Tag::Move:
mCommand.AssignLiteral(aCommand.move.by_to == StyleByTo::To ? "M" : "m");
mValues.AppendElement(aCommand.move.point.x);
mValues.AppendElement(aCommand.move.point.y);
break;
case StylePathCommand::Tag::Line:
mCommand.AssignLiteral(aCommand.line.by_to == StyleByTo::To ? "L" : "l");
mValues.AppendElement(aCommand.line.point.x);
mValues.AppendElement(aCommand.line.point.y);
break;
case StylePathCommand::Tag::CubicCurve:
mCommand.AssignLiteral(aCommand.cubic_curve.by_to == StyleByTo::To ? "C"
: "c");
mValues.AppendElement(aCommand.cubic_curve.control1.x);
mValues.AppendElement(aCommand.cubic_curve.control1.y);
mValues.AppendElement(aCommand.cubic_curve.control2.x);
mValues.AppendElement(aCommand.cubic_curve.control2.y);
mValues.AppendElement(aCommand.cubic_curve.point.x);
mValues.AppendElement(aCommand.cubic_curve.point.y);
break;
case StylePathCommand::Tag::QuadCurve:
mCommand.AssignLiteral(aCommand.quad_curve.by_to == StyleByTo::To ? "Q"
: "q");
mValues.AppendElement(aCommand.quad_curve.control1.x);
mValues.AppendElement(aCommand.quad_curve.control1.y);
mValues.AppendElement(aCommand.quad_curve.point.x);
mValues.AppendElement(aCommand.quad_curve.point.y);
break;
case StylePathCommand::Tag::Arc:
mCommand.AssignLiteral(aCommand.arc.by_to == StyleByTo::To ? "A" : "a");
mValues.AppendElement(aCommand.arc.radii.x);
mValues.AppendElement(aCommand.arc.radii.y);
mValues.AppendElement(aCommand.arc.rotate);
mValues.AppendElement(aCommand.arc.arc_size == StyleArcSize::Large);
mValues.AppendElement(aCommand.arc.arc_sweep == StyleArcSweep::Cw);
mValues.AppendElement(aCommand.arc.point.x);
mValues.AppendElement(aCommand.arc.point.y);
break;
case StylePathCommand::Tag::HLine:
mCommand.AssignLiteral(aCommand.h_line.by_to == StyleByTo::To ? "H"
: "h");
mValues.AppendElement(aCommand.h_line.x);
break;
case StylePathCommand::Tag::VLine:
mCommand.AssignLiteral(aCommand.v_line.by_to == StyleByTo::To ? "V"
: "v");
mValues.AppendElement(aCommand.v_line.y);
break;
case StylePathCommand::Tag::SmoothCubic:
mCommand.AssignLiteral(
aCommand.smooth_cubic.by_to == StyleByTo::To ? "S" : "s");
mValues.AppendElement(aCommand.smooth_cubic.control2.x);
mValues.AppendElement(aCommand.smooth_cubic.control2.y);
mValues.AppendElement(aCommand.smooth_cubic.point.x);
mValues.AppendElement(aCommand.smooth_cubic.point.y);
break;
case StylePathCommand::Tag::SmoothQuad:
mCommand.AssignLiteral(aCommand.smooth_quad.by_to == StyleByTo::To ? "T"
: "t");
mValues.AppendElement(aCommand.smooth_quad.point.x);
mValues.AppendElement(aCommand.smooth_quad.point.y);
break;
}
}
static int32_t ArgCountForType(char aType) {
switch (ToLowerCase(aType)) {
case 'z':
return 0;
case 'm':
case 'l':
return 2;
case 'c':
return 6;
case 'q':
return 4;
case 'a':
return 7;
case 'h':
case 'v':
return 1;
case 's':
return 4;
case 't':
return 2;
}
return -1;
}
bool SVGPathSegment::IsMove() const {
return mCommand.EqualsLiteral("M") || mCommand.EqualsLiteral("m");
}
bool SVGPathSegment::IsArc() const {
return mCommand.EqualsLiteral("A") || mCommand.EqualsLiteral("a");
}
bool IsValidFlag(float aFlag) { return aFlag == 0.0f || aFlag == 1.0f; }
bool SVGPathSegment::IsValid() const {
if (mCommand.Length() != 1) {
return false;
}
auto expectedArgCount = ArgCountForType(mCommand.First());
if (expectedArgCount < 0 || mValues.Length() != uint32_t(expectedArgCount)) {
return false;
}
if (IsArc() && !(IsValidFlag(mValues[3]) && IsValidFlag(mValues[4]))) {
return false;
}
return true;
}
StylePathCommand SVGPathSegment::ToStylePathCommand() const {
MOZ_ASSERT(IsValid(), "Trying to convert invalid SVGPathSegment");
switch (mCommand.First()) {
case 'M':
return StylePathCommand::Move(StyleByTo::To, {mValues[0], mValues[1]});
case 'm':
return StylePathCommand::Move(StyleByTo::By, {mValues[0], mValues[1]});
case 'L':
return StylePathCommand::Line(StyleByTo::To, {mValues[0], mValues[1]});
case 'l':
return StylePathCommand::Line(StyleByTo::By, {mValues[0], mValues[1]});
case 'C':
return StylePathCommand::CubicCurve(
StyleByTo::To, {mValues[4], mValues[5]}, {mValues[0], mValues[1]},
{mValues[2], mValues[3]});
case 'c':
return StylePathCommand::CubicCurve(
StyleByTo::By, {mValues[4], mValues[5]}, {mValues[0], mValues[1]},
{mValues[2], mValues[3]});
case 'Q':
return StylePathCommand::QuadCurve(
StyleByTo::To, {mValues[2], mValues[3]}, {mValues[0], mValues[1]});
case 'q':
return StylePathCommand::QuadCurve(
StyleByTo::By, {mValues[2], mValues[3]}, {mValues[0], mValues[1]});
case 'A':
return StylePathCommand::Arc(
StyleByTo::To, {mValues[5], mValues[6]}, {mValues[0], mValues[1]},
mValues[3] ? StyleArcSweep::Cw : StyleArcSweep::Ccw,
mValues[4] ? StyleArcSize::Large : StyleArcSize::Small, mValues[2]);
case 'a':
return StylePathCommand::Arc(
StyleByTo::By, {mValues[5], mValues[6]}, {mValues[0], mValues[1]},
mValues[3] ? StyleArcSweep::Cw : StyleArcSweep::Ccw,
mValues[4] ? StyleArcSize::Large : StyleArcSize::Small, mValues[2]);
case 'H':
return StylePathCommand::HLine(StyleByTo::To, mValues[0]);
case 'h':
return StylePathCommand::HLine(StyleByTo::By, mValues[0]);
case 'V':
return StylePathCommand::VLine(StyleByTo::To, mValues[0]);
case 'v':
return StylePathCommand::VLine(StyleByTo::By, mValues[0]);
case 'S':
return StylePathCommand::SmoothCubic(
StyleByTo::To, {mValues[2], mValues[3]}, {mValues[0], mValues[1]});
case 's':
return StylePathCommand::SmoothCubic(
StyleByTo::By, {mValues[2], mValues[3]}, {mValues[0], mValues[1]});
case 'T':
return StylePathCommand::SmoothQuad(StyleByTo::To,
{mValues[0], mValues[1]});
case 't':
return StylePathCommand::SmoothQuad(StyleByTo::By,
{mValues[0], mValues[1]});
}
return StylePathCommand::Close();
}
void SVGPathSegment::GetType(DOMString& aType) {
aType.SetKnownLiveString(mCommand);
}
void SVGPathSegment::SetType(const nsAString& aType) { mCommand = aType; }
void SVGPathSegment::GetValues(nsTArray<float>& aValues) {
aValues = mValues.Clone();
}
void SVGPathSegment::SetValues(const nsTArray<float>& aValues) {
mValues = aValues.Clone();
}
} // namespace mozilla::dom

54
dom/svg/SVGPathSegment.h Normal file
View File

@@ -0,0 +1,54 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef DOM_SVG_SVGPATHSEGMENT_H_
#define DOM_SVG_SVGPATHSEGMENT_H_
#include "nsWrapperCache.h"
#include "SVGPathSegUtils.h"
#include "mozilla/dom/SVGPathElement.h"
namespace mozilla::dom {
class SVGPathElement;
class SVGPathSegment final : public nsWrapperCache {
public:
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(SVGPathSegment)
NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(SVGPathSegment)
SVGPathSegment(SVGPathElement* aSVGPathElement,
const StylePathCommand& aValue);
protected:
virtual ~SVGPathSegment() = default;
public:
SVGPathElement* GetParentObject() const { return mSVGPathElement; }
JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
bool IsMove() const;
bool IsArc() const;
bool IsValid() const;
StylePathCommand ToStylePathCommand() const;
void GetType(DOMString& aType);
void SetType(const nsAString& aType);
void GetValues(nsTArray<float>& aValues);
void SetValues(const nsTArray<float>& aValues);
private:
RefPtr<SVGPathElement> mSVGPathElement;
nsString mCommand;
nsTArray<float> mValues;
};
} // namespace mozilla::dom
#endif // DOM_SVG_SVGPATHSEGMENT_H_

View File

@@ -1,13 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" onload="boom();">
<script type="text/javascript">
function boom()
{
var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.getPathSegAtLength(1);
var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.getPathSegmentAtLength(1);
}
</script>
</svg>

Before

Width:  |  Height:  |  Size: 258 B

After

Width:  |  Height:  |  Size: 251 B

View File

@@ -8,6 +8,6 @@
0,0 0,0 0,0 0,0 0,0" stroke="black" id="p"/>
<script>
var path = document.getElementById("p");
var segNum = path.getPathSegAtLength(1200);
var segment = path.getPathSegmentAtLength(1200);
</script>
</svg>

Before

Width:  |  Height:  |  Size: 402 B

After

Width:  |  Height:  |  Size: 407 B

View File

@@ -34,7 +34,7 @@ load 413174-1.svg
load 414188-1.svg
load 427325-1.svg
load 428228-1.svg
load 428841-1.svg
pref(dom.svg.pathSegment.enabled,true) load 428841-1.svg
load 436418-mpathRoot-1.svg
load 448244-1.svg
load 466576-1.xhtml
@@ -43,7 +43,7 @@ load 535691-1.svg
load 539167-1.svg
load 573316-1.svg
load 579356-1.svg
load 579356-2.svg
pref(dom.svg.pathSegment.enabled,true) load 579356-2.svg
load 595608-1.svg
load 601251-1.html
load 601406-1.svg

View File

@@ -81,6 +81,7 @@ EXPORTS.mozilla.dom += [
"SVGMPathElement.h",
"SVGPathData.h",
"SVGPathElement.h",
"SVGPathSegment.h",
"SVGPathSegUtils.h",
"SVGPatternElement.h",
"SVGPolygonElement.h",
@@ -220,6 +221,7 @@ UNIFIED_SOURCES += [
"SVGPathData.cpp",
"SVGPathElement.cpp",
"SVGPathSegListSMILType.cpp",
"SVGPathSegment.cpp",
"SVGPathSegUtils.cpp",
"SVGPatternElement.cpp",
"SVGPointList.cpp",

View File

@@ -1,4 +1,7 @@
[DEFAULT]
prefs = [
"dom.svg.pathSegment.enabled=true",
]
support-files = [
"MutationEventChecker.js",
"a_href_destination.svg",
@@ -86,6 +89,7 @@ support-files = [
["test_getElementById.xhtml"]
["test_getPathSegmentAtLength_with_d_property.html"]
["test_getSubStringLength.xhtml"]
@@ -117,7 +121,6 @@ skip-if = ["true"] # disabled-for-intermittent-failures--bug-701060
["test_pairParsing.html"]
["test_pathAnimInterpolation.xhtml"]
skip-if = ["true"] # We need to polyfill the SVG DOM for path data
["test_pointAtLength.xhtml"]

View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<title>Test for getPathSegmentAtLength for d property</title>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style>
svg {
width: 10%;
height: 10%;
background: #eee;
}
svg path {
fill: none;
stroke: #000;
}
</style>
<div id="log"></div>
<svg viewBox="0 0 20 10">
<path id='target1' d="M2,2 L8,8 L12,4"/>
</svg>
<svg viewBox="0 0 20 10">
<path id='target2' style='d: path("M2,2 L8,8 L12,4")' />
</svg>
<script>
/* global test, assert_equals */
'use strict';
test(function() {
let target1 = document.getElementById('target1');
let target2 = document.getElementById('target2');
assert_equals(target1.getPathSegmentAtLength(5).type, "L");
assert_array_equals(target1.getPathSegmentAtLength(5).values, [8, 8]);
assert_equals(target2.getPathSegmentAtLength(5).type, "L");
assert_array_equals(target2.getPathSegmentAtLength(5).values, [8, 8]);
assert_equals(target1.getPathSegmentAtLength(10).type, "L");
assert_array_equals(target1.getPathSegmentAtLength(10).values, [12, 4]);
assert_equals(target2.getPathSegmentAtLength(10).type, "L");
assert_array_equals(target2.getPathSegmentAtLength(10).values, [12, 4]);
}, "getPathSegmentAtLength works properly on both d attribute and d property");
test(function() {
let target = document.getElementById('target1');
target.style.d = 'path("M2,2 h3 v5")';
assert_equals(target.getPathSegmentAtLength(5).type, "v");
assert_array_equals(target.getPathSegmentAtLength(5).values, [5]);
}, "getPathSegmentAtLength works properly after updating d property");
</script>

View File

@@ -20,7 +20,7 @@ var gSVG = document.getElementById("svg");
var gTests = [];
// Array of all path segment types.
var gTypes = "zMmLlCcQqAaHhVvSsTt".split("");
var gTypes = "ZMmLlCcQqAaHhVvSsTt".split("");
// Property names on the SVGPathSeg objects for the given segment type, in the
// order that they would appear in a path data string.
@@ -51,7 +51,7 @@ var gPrefixes = [
[3, "M50,50 H100 v50"],
[2, "M50,50 A10,10,10,0,0,100,100"],
[2, "M50,50 a10,10,10,0,0,50,50"],
[4, "M50,50 l50,50 z m50,50"],
[4, "M50,50 l50,50 Z m50,50"],
// These leave the quadratic implied control point at 125,125.
[2, "M50,50 Q75,75,100,100"],
@@ -129,53 +129,29 @@ var gPrefixes = [
var gSuffixes = {
// Same path segment type, no conversion required.
MM: [[10, 20], [30, 40], [20, 30], [30, 50]],
mm: [[10, 20], [30, 40], [20, 30], [30, 50]],
LL: [[10, 20], [30, 40], [20, 30], [30, 50]],
ll: [[10, 20], [30, 40], [20, 30], [30, 50]],
CC: [[10, 20, 30, 40, 50, 60], [70, 80, 90, 100, 110, 120],
[40, 50, 60, 70, 80, 90], [50, 70, 90, 110, 130, 150]],
cc: [[10, 20, 30, 40, 50, 60], [70, 80, 90, 100, 110, 120],
[40, 50, 60, 70, 80, 90], [50, 70, 90, 110, 130, 150]],
QQ: [[10, 20, 30, 40], [50, 60, 70, 80], [30, 40, 50, 60], [40, 60, 80, 100]],
qq: [[10, 20, 30, 40], [50, 60, 70, 80], [30, 40, 50, 60], [40, 60, 80, 100]],
AA: [[10, 20, 30, 0, 0, 40, 50], [60, 70, 80, 0, 0, 90, 100],
[35, 45, 55, 0, 0, 65, 75], [45, 65, 85, 0, 0, 105, 125]],
aa: [[10, 20, 30, 0, 0, 40, 50], [60, 70, 80, 0, 0, 90, 100],
[35, 45, 55, 0, 0, 65, 75], [45, 65, 85, 0, 0, 105, 125]],
HH: [[10], [20], [15], [25]],
hh: [[10], [20], [15], [25]],
VV: [[10], [20], [15], [25]],
vv: [[10], [20], [15], [25]],
SS: [[10, 20, 30, 40], [50, 60, 70, 80], [30, 40, 50, 60], [40, 60, 80, 100]],
ss: [[10, 20, 30, 40], [50, 60, 70, 80], [30, 40, 50, 60], [40, 60, 80, 100]],
TT: [[10, 20], [30, 40], [20, 30], [30, 50]],
tt: [[10, 20], [30, 40], [20, 30], [30, 50]],
// Relative <-> absolute conversion.
Mm: [[10, 20], [30, 40], [-30, -20], [-120, -100]],
mM: [[10, 20], [30, 40], [70, 80], [180, 200]],
Ll: [[10, 20], [30, 40], [-30, -20], [-120, -100]],
lL: [[10, 20], [30, 40], [70, 80], [180, 200]],
Cc: [[10, 20, 30, 40, 50, 60], [70, 80, 90, 100, 110, 120],
[-10, 0, 10, 20, 30, 40], [-100, -80, -60, -40, -20, 0]],
cC: [[10, 20, 30, 40, 50, 60], [70, 80, 90, 100, 110, 120],
[90, 100, 110, 120, 130, 140], [200, 220, 240, 260, 280, 300]],
Qq: [[10, 20, 30, 40], [50, 60, 70, 80],
[-20, -10, 0, 10], [-110, -90, -70, -50]],
qQ: [[10, 20, 30, 40], [50, 60, 70, 80],
[80, 90, 100, 110], [190, 210, 230, 250]],
Aa: [[10, 20, 30, 0, 0, 40, 50], [60, 70, 80, 0, 0, 90, 100],
[35, 45, 55, 0, 0, 15, 25], [45, 65, 85, 0, 0, -45, -25]],
aA: [[10, 20, 30, 0, 0, 40, 50], [60, 70, 80, 0, 0, 90, 100],
[35, 45, 55, 0, 0, 115, 125], [45, 65, 85, 0, 0, 255, 275]],
Hh: [[10], [20], [-35], [-125]],
hH: [[10], [20], [65], [175]],
Vv: [[10], [20], [-35], [-125]],
vV: [[10], [20], [65], [175]],
Tt: [[10, 20], [30, 40], [-30, -20], [-120, -100]],
tT: [[10, 20], [30, 40], [70, 80], [180, 200]],
Ss: [[10, 20, 30, 40], [50, 60, 70, 80],
[-20, -10, 0, 10], [-110, -90, -70, -50]],
sS: [[10, 20, 30, 40], [50, 60, 70, 80],
[80, 90, 100, 110], [190, 210, 230, 250]],
};
@@ -279,17 +255,6 @@ function run() {
}
}
// Test that differences in arc flag parameters cause the
// interpolation/addition not to occur.
addTest(1, "M100,100",
"A", [10, 20, 30, 0, 0, 40, 50],
"a", [60, 70, 80, 0, 1, 90, 100],
"a", [60, 70, 80, 0, 1, 90, 100], additive);
addTest(1, "M100,100",
"A", [10, 20, 30, 0, 0, 40, 50],
"a", [60, 70, 80, 1, 0, 90, 100],
"a", [60, 70, 80, 1, 0, 90, 100], additive);
// Test all pairs of segment types that cannot be interpolated between.
for (let fromType of gTypes) {
let fromArguments = generatePathSegmentArguments(fromType, 0);
@@ -308,21 +273,20 @@ function run() {
// Inspect the results of each subtest.
for (let test of gTests) {
let list = test.element.animatedPathSegList;
is(list.numberOfItems, test.prefixLength + 1,
let list = test.element.getPathData();
is(list.length, test.prefixLength + 1,
"Length of animatedPathSegList for interpolation " +
(test.usesAddition ? "with addition " : "") +
" from " + test.from + " to " + test.to);
let seg = list.getItem(list.numberOfItems - 1);
let propertyNames = argumentNames(test.expectedType);
let seg = list.at(-1);
let actual = [];
for (let i = 0; i < test.expected.length; i++) {
actual.push(+seg[propertyNames[i]]);
actual.push(seg.values[i]);
}
is(seg.pathSegTypeAsLetter + actual, test.expectedType + test.expected,
is(seg.type + actual, test.expectedType + test.expected,
"Path segment for interpolation " +
(test.usesAddition ? "with addition " : "") +
" from " + test.from + " to " + test.to);

View File

@@ -9,8 +9,28 @@
* Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
* liability, trademark and document use rules apply.
*/
[Exposed=Window]
interface SVGPathElement : SVGGeometryElement {
[LegacyNoInterfaceObject, Exposed=Window]
interface SVGPathSegment {
attribute DOMString type;
[Cached, Pure]
attribute sequence<float> values;
};
// TODO: Implement https://www.w3.org/TR/svg-paths/#InterfaceSVGPathData
dictionary SVGPathDataSettings {
boolean normalize = false;
};
interface mixin SVGPathData {
[Pref="dom.svg.pathSegment.enabled"]
sequence<SVGPathSegment> getPathData(optional SVGPathDataSettings settings = {});
[Pref="dom.svg.pathSegment.enabled"]
undefined setPathData(sequence<SVGPathSegment> pathData);
};
[Exposed=Window]
interface SVGPathElement : SVGGeometryElement {
[Pref="dom.svg.pathSegment.enabled"]
SVGPathSegment? getPathSegmentAtLength(float distance);
};
SVGPathElement includes SVGPathData;

View File

@@ -4508,6 +4508,12 @@
value: 0
mirror: always
# This enables the SVGPathSegment APIs
- name: dom.svg.pathSegment.enabled
type: bool
value: @IS_NIGHTLY_BUILD@
mirror: always
# For area and anchor elements with target=_blank and no rel set to
# opener/noopener.
- name: dom.targetBlankNoOpener.enabled

View File

@@ -13,6 +13,7 @@ use crate::values::CSSFloat;
use cssparser::Parser;
use std::fmt::{self, Write};
use std::iter::{Cloned, Peekable};
use std::ops;
use std::slice;
use style_traits::values::SequenceWriter;
use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
@@ -57,12 +58,14 @@ impl SVGPathData {
/// Create a normalized copy of this path by converting each relative
/// command to an absolute command.
pub fn normalize(&self) -> Self {
pub fn normalize(&self, reduce: bool) -> Self {
let mut state = PathTraversalState {
subpath_start: CoordPair::new(0.0, 0.0),
pos: CoordPair::new(0.0, 0.0),
last_command: PathCommand::Close,
last_control: CoordPair::new(0.0, 0.0),
};
let iter = self.0.iter().map(|seg| seg.normalize(&mut state));
let iter = self.0.iter().map(|seg| seg.normalize(&mut state, reduce));
SVGPathData(crate::ArcSlice::from_iter(iter))
}
@@ -162,8 +165,8 @@ impl Animate for SVGPathData {
// FIXME(emilio): This allocates three copies of the path, that's not
// great! Specially, once we're normalized once, we don't need to
// re-normalize again.
let left = self.normalize();
let right = other.normalize();
let left = self.normalize(false);
let right = other.normalize(false);
let items: Vec<_> = lists::by_computed_value::animate(&left.0, &right.0, procedure)?;
Ok(SVGPathData(crate::ArcSlice::from_iter(items.into_iter())))
@@ -175,8 +178,8 @@ impl ComputeSquaredDistance for SVGPathData {
if self.0.len() != other.0.len() {
return Err(());
}
let left = self.normalize();
let right = other.normalize();
let left = self.normalize(false);
let right = other.normalize(false);
lists::by_computed_value::squared_distance(&left.0, &right.0)
}
}
@@ -194,6 +197,8 @@ pub type PathCommand = GenericShapeCommand<CSSFloat, CSSFloat>;
struct PathTraversalState {
subpath_start: CoordPair,
pos: CoordPair,
last_command: PathCommand,
last_control: CoordPair,
}
impl PathCommand {
@@ -201,11 +206,16 @@ impl PathCommand {
/// for relative commands an equivalent absolute command will be returned.
///
/// See discussion: https://github.com/w3c/svgwg/issues/321
fn normalize(&self, state: &mut PathTraversalState) -> Self {
/// If reduce is true then the path will be restricted to
/// "M", "L", "C", "A" and "Z" commands.
fn normalize(&self, state: &mut PathTraversalState, reduce: bool) -> Self {
use crate::values::generics::basic_shape::GenericShapeCommand::*;
match *self {
Close => {
state.pos = state.subpath_start;
if reduce {
state.last_command = *self;
}
Close
},
Move { by_to, mut point } => {
@@ -214,6 +224,9 @@ impl PathCommand {
}
state.pos = point;
state.subpath_start = point;
if reduce {
state.last_command = *self;
}
Move {
by_to: ByTo::To,
point,
@@ -224,6 +237,9 @@ impl PathCommand {
point += state.pos;
}
state.pos = point;
if reduce {
state.last_command = *self;
}
Line {
by_to: ByTo::To,
point,
@@ -234,14 +250,24 @@ impl PathCommand {
x += state.pos.x;
}
state.pos.x = x;
HLine { by_to: ByTo::To, x }
if reduce {
state.last_command = *self;
PathCommand::Line { by_to: ByTo::To, point: state.pos }
} else {
HLine { by_to: ByTo::To, x }
}
},
VLine { by_to, mut y } => {
if !by_to.is_abs() {
y += state.pos.y;
}
state.pos.y = y;
VLine { by_to: ByTo::To, y }
if reduce {
state.last_command = *self;
PathCommand::Line { by_to: ByTo::To, point: state.pos }
} else {
VLine { by_to: ByTo::To, y }
}
},
CubicCurve {
by_to,
@@ -255,6 +281,10 @@ impl PathCommand {
control2 += state.pos;
}
state.pos = point;
if reduce {
state.last_command = *self;
state.last_control = control2;
}
CubicCurve {
by_to: ByTo::To,
point,
@@ -271,11 +301,25 @@ impl PathCommand {
point += state.pos;
control1 += state.pos;
}
state.pos = point;
QuadCurve {
by_to: ByTo::To,
point,
control1,
if reduce {
let c1 = state.pos + 2. * (control1 - state.pos) / 3.;
let control2 = point + 2. * (control1 - point) / 3.;
state.pos = point;
state.last_command = *self;
state.last_control = control1;
CubicCurve {
by_to: ByTo::To,
point,
control1: c1,
control2,
}
} else {
state.pos = point;
QuadCurve {
by_to: ByTo::To,
point,
control1,
}
}
},
SmoothCubic {
@@ -287,21 +331,57 @@ impl PathCommand {
point += state.pos;
control2 += state.pos;
}
state.pos = point;
SmoothCubic {
by_to: ByTo::To,
point,
control2,
if reduce {
let control1 = match state.last_command {
PathCommand::CubicCurve { by_to: _, point: _, control1: _, control2: _ } | PathCommand::SmoothCubic { by_to: _, point: _, control2: _ } =>
state.pos + state.pos - state.last_control,
_ => state.pos
};
state.pos = point;
state.last_control = control2;
state.last_command = *self;
CubicCurve {
by_to: ByTo::To,
point,
control1,
control2,
}
} else {
state.pos = point;
SmoothCubic {
by_to: ByTo::To,
point,
control2,
}
}
},
SmoothQuad { by_to, mut point } => {
if !by_to.is_abs() {
point += state.pos;
}
state.pos = point;
SmoothQuad {
by_to: ByTo::To,
point,
if reduce {
let control = match state.last_command {
PathCommand::QuadCurve { by_to: _, point: _, control1: _ } | PathCommand::SmoothQuad { by_to: _, point: _ } =>
state.pos + state.pos - state.last_control,
_ => state.pos
};
let control1 = state.pos + 2. * (control - state.pos) / 3.;
let control2 = point + 2. * (control - point) / 3.;
state.pos = point;
state.last_command = *self;
state.last_control = control;
CubicCurve {
by_to: ByTo::To,
point,
control1,
control2,
}
} else {
state.pos = point;
SmoothQuad {
by_to: ByTo::To,
point,
}
}
},
Arc {
@@ -316,13 +396,34 @@ impl PathCommand {
point += state.pos;
}
state.pos = point;
Arc {
by_to: ByTo::To,
point,
radii,
arc_sweep,
arc_size,
rotate,
if reduce {
state.last_command = *self;
if radii.x == 0. && radii.y == 0. {
CubicCurve {
by_to: ByTo::To,
point: state.pos,
control1: point,
control2: point,
}
} else {
Arc {
by_to: ByTo::To,
point,
radii,
arc_sweep,
arc_size,
rotate,
}
}
} else {
Arc {
by_to: ByTo::To,
point,
radii,
arc_sweep,
arc_size,
rotate,
}
}
},
}
@@ -424,6 +525,58 @@ impl PathCommand {
/// The path coord type.
pub type CoordPair = CoordinatePair<CSSFloat>;
impl ops::Add<CoordPair> for CoordPair {
type Output = CoordPair;
fn add(self, rhs: CoordPair) -> CoordPair {
Self {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
impl ops::Sub<CoordPair> for CoordPair {
type Output = CoordPair;
fn sub(self, rhs: CoordPair) -> CoordPair {
Self {
x: self.x - rhs.x,
y: self.y - rhs.y,
}
}
}
impl ops::Mul<CSSFloat> for CoordPair {
type Output = CoordPair;
fn mul(self, f: CSSFloat) -> CoordPair {
Self {
x: self.x * f,
y: self.y * f,
}
}
}
impl ops::Mul<CoordPair> for CSSFloat {
type Output = CoordPair;
fn mul(self, rhs: CoordPair) -> CoordPair {
rhs * self
}
}
impl ops::Div<CSSFloat> for CoordPair {
type Output = CoordPair;
fn div(self, f: CSSFloat) -> CoordPair {
Self {
x: self.x / f,
y: self.y / f,
}
}
}
/// SVG Path parser.
struct PathParser<'a> {
chars: Peekable<Cloned<slice::Iter<'a, u8>>>,

View File

@@ -158,6 +158,7 @@ use style::values::specified::gecko::IntersectionObserverRootMargin;
use style::values::specified::source_size_list::SourceSizeList;
use style::values::specified::{AbsoluteLength, NoCalcLength};
use style::values::{specified, AtomIdent, CustomIdent, KeyframesName};
use style::values::specified::svg_path::PathCommand;
use style_traits::{CssWriter, ParseError, ParsingMode, ToCss};
use thin_vec::ThinVec as nsTArray;
use to_shmem::SharedMemoryBuilder;
@@ -5708,6 +5709,14 @@ pub extern "C" fn Servo_DeclarationBlock_SetPathValue(
})
}
#[no_mangle]
pub extern "C" fn Servo_CreatePathDataFromCommands(
path_commands: &mut nsTArray<PathCommand>,
dest: &mut specified::SVGPathData) {
let path = specified::SVGPathData(style_traits::arc_slice::ArcSlice::from_iter(path_commands.drain(..)));
*dest = path;
}
#[no_mangle]
pub extern "C" fn Servo_SVGPathData_Add(
dest: &mut specified::SVGPathData,
@@ -5730,6 +5739,12 @@ pub extern "C" fn Servo_SVGPathData_Parse(input: &nsACString, dest: &mut specifi
ret
}
#[no_mangle]
pub extern "C" fn Servo_SVGPathData_NormalizeAndReduce(input: &specified::SVGPathData, dest: &mut specified::SVGPathData) {
let path = input.normalize(true);
*dest = path;
}
#[no_mangle]
pub extern "C" fn Servo_SVGPathData_ToString(path: &specified::SVGPathData, dest: &mut nsACString) {
path.to_css(&mut CssWriter::new(dest), /* quote = */ false).unwrap();

View File

@@ -0,0 +1,2 @@
[SVGPathSegment.svg]
prefs: [dom.svg.pathSegment.enabled:true]

View File

@@ -1,4 +1,5 @@
[SVGGeometryElement.getTotalLength-01.svg]
prefs: [dom.svg.pathSegment.enabled:true]
[SVGGeometryElement.prototype.getTotalLength(), getTotalLength - rect in document]
expected:
if (os == "mac") and not debug: [PASS, FAIL]

View File

@@ -0,0 +1,197 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:h="http://www.w3.org/1999/xhtml">
<metadata>
<h:link rel="help" href="https://svgwg.org/specs/paths/#InterfaceSVGPathSegment"/>
</metadata>
<h:script src="/resources/testharness.js"/>
<h:script src="/resources/testharnessreport.js"/>
<path id="track" d="m 10 20 h 30"/>S
<script><![CDATA[
test(function() {
let track = document.getElementById('track');
assert_equals(Object.getPrototypeOf(track), SVGPathElement.prototype);
assert_not_equals(track.getPathData, undefined);
assert_not_equals(track.setPathData, undefined);
assert_not_equals(track.getPathSegmentAtLength, undefined);
}, "SVGPathSegment interface exists");
function checkObjects(actual, expected, epsilon) {
if (Array.isArray(expected)) {
if (actual.length != expected.length) {
return false;
}
for (let i = 0; i < expected.length; i++) {
if (!checkObjects(actual[i], expected[i], epsilon) ) {
return false;
}
}
return true;
}
if (typeof expected == "object") {
for (let key in expected) {
if (!checkObjects(actual[key], expected[key], epsilon)) {
return false;
}
}
return true;
}
if (typeof expected == "number") {
if (epsilon === undefined) {
epsilon = 0;
}
return Math.abs(actual - expected) <= epsilon;
}
return actual == expected;
}
function ToString(value) {
let output = "";
if (Array.isArray(value)) {
let arrayAsString = [];
for (let i = 0; i < value.length; i++) {
arrayAsString.push(ToString(value[i]));
}
return arrayAsString.join(' ');
}
if (typeof value == "object") {
let objectAsArray = [];
for (let key in value) {
objectAsArray.push(ToString(value[key]));
}
return objectAsArray.join(' ');
}
if (typeof value == "number") {
return "" + +value.toFixed(3);
}
return value;
}
var checkPathData = function(actual, expected, epsilon) {
assert_true(checkObjects(actual, expected, epsilon), "actual:" + ToString(actual) + " expected:" + ToString(expected));
};
let getPathDataTests = [
{
description: "straight lines",
input: "M 0 0 H 100 V 100 H 0 Z",
output: [
{ type: "M", values: [0, 0] },
{ type: "H", values: [100] },
{ type: "V", values: [100] },
{ type: "H", values: [0] },
{ type: "Z", values: [] }
]
},
{
description: "cubic",
input: "M 413 140 C 413 140 438 40 302 44",
output: [
{ type: "M", values: [413, 140] },
{ type: "C", values: [413, 140, 438, 40, 302, 44] }
]
}
];
getPathDataTests.forEach(t => {
test(_ => {
let path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", t.input);
let pathData = path.getPathData();
checkPathData(pathData, t.output);
}, "Test path.getPathData() " + t.description)
});
let getNormalizedPathDataTests = [
{
description: "straight lines",
input: "M 0 0 H 100 V 100 H 0 Z",
output: [
{ type: "M", values: [0, 0] },
{ type: "L", values: [100, 0] },
{ type: "L", values: [100, 100] },
{ type: "L", values: [0, 100] },
{ type: "Z", values: [] }
]
},
{
description: "cubic",
input: "M 0 0 C 30,90 25,10 50,10 S 70,90 90,90",
output: [
{ type: "M", values: [0, 0] },
{ type: "C", values: [30, 90, 25, 10, 50, 10] },
{ type: "C", values: [75, 10, 70, 90, 90, 90] },
]
},
{
description: "quadratic",
input: "M 0 0 Q 30 30 30 60 t 30 0 30 0 30 0 30 0 30 0",
output: [
{ type: "M", values: [0, 0] },
{ type: "C", values: [20, 20, 30, 40, 30, 60] },
{ type: "C", values: [30, 80, 40, 80, 60, 60] },
{ type: "C", values: [80, 40, 90, 40, 90, 60] },
{ type: "C", values: [90, 80, 100, 80, 120, 60] },
{ type: "C", values: [140, 40, 150, 40, 150, 60] },
{ type: "C", values: [150, 80, 160, 80, 180, 60] },
]
},
{
description: "elliptical arc",
input: "M 6 10 A 10 10 10 0 0 15 10",
output: [
{ type: "M", values: [6, 10] },
{ type: "C", values: [8.83, 11.426, 12.169, 11.426, 15, 10] },
],
epsilon: 0.01,
},
];
getNormalizedPathDataTests.forEach(t => {
test(_ => {
let path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", t.input);
let pathData = path.getPathData({normalize: true});
checkPathData(pathData, t.output, t.epsilon);
}, "Test path.getPathData({normalize: true}) " + t.description)
});
let setPathDataTests = [
{
description: "lines",
input: [
{ type: "M", values: [0, 0] },
{ type: "L", values: [10, 10] },
{ type: "Z", values: [] }
],
output: "M 0 0 L 10 10 Z"
},
{
description: "empty",
input: [],
output: ""
},
];
setPathDataTests.forEach(t => {
test(_ => {
let path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", t.output);
let segments = path.getPathData();
path.removeAttribute("d");
path.setPathData(segments);
assert_equals(path.getAttribute("d"), t.output);
}, "Test path.setPathData()" + t.description);
});
]]></script>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -3,7 +3,7 @@
<h:link rel="help" href="https://svgwg.org/svg2-draft/types.html#__svg__SVGGeometryElement__getTotalLength"/>
<h:script src="/resources/testharness.js"/>
<h:script src="/resources/testharnessreport.js"/>
<script>
<script><![CDATA[
test(function() {
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M0,0L100,0L100,100');
@@ -69,5 +69,26 @@ test(function() {
document.documentElement.removeChild(g);
}
}, document.title+', getTotalLength - rect in document with display none');
</script>
test(function() {
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
document.documentElement.appendChild(path);
path.setAttribute("d", "M50,100l0,0l0,-50l100,0l86.3325,122.665z");
try {
assert_equals(path.getTotalLength(), 500);
let pathData = path.getPathData();
for (var i = 0; i < 2; i++) {
pathData = pathData.slice(0, -1);
}
path.setPathData(pathData);
assert_equals(path.getTotalLength(), 150);
} finally {
document.documentElement.removeChild(path);
}
}, document.title+', getTotalLength - path modified with setPathData');
]]></script>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB