Bug 1934525 - Implement new path data API r=emilio
Differential Revision: https://phabricator.services.mozilla.com/D231736
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
/**
|
||||
|
||||
@@ -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:
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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_
|
||||
@@ -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 |
@@ -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 |
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>>>,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[SVGPathSegment.svg]
|
||||
prefs: [dom.svg.pathSegment.enabled:true]
|
||||
@@ -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]
|
||||
|
||||
@@ -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 |
@@ -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 |