PathDirect2D.cpp   [plain text]


/*
 * Copyright (C) 2016 Apple Inc.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#include "config.h"
#include "Path.h"

#if USE(DIRECT2D)

#include "AffineTransform.h"
#include "COMPtr.h"
#include "FloatRect.h"
#include "GraphicsContext.h"
#include "IntRect.h"
#include "NotImplemented.h"
#include "StrokeStyleApplier.h"
#include <d2d1.h>
#include <wtf/MathExtras.h>
#include <wtf/RetainPtr.h>
#include <wtf/text/WTFString.h>

namespace WebCore {

static inline ID2D1RenderTarget* scratchRenderTarget()
{
    static COMPtr<ID2D1RenderTarget> renderTarget = adoptCOM(GraphicsContext::defaultRenderTarget());
    return renderTarget.get();
}

Path Path::polygonPathFromPoints(const Vector<FloatPoint>& points)
{
    Path path;
    if (points.size() < 2)
        return path;

    Vector<D2D1_POINT_2F, 32> d2dPoints;
    d2dPoints.reserveInitialCapacity(points.size() - 1);
    for (auto point : points)
        d2dPoints.uncheckedAppend(point);

    path.moveTo(points.first());

    ASSERT(path.activePath());

    path.activePath()->AddLines(d2dPoints.data(), d2dPoints.size());
    path.closeSubpath();

    return path;
}

Path::Path() = default;

Path::~Path() = default;

PlatformPathPtr Path::ensurePlatformPath()
{
    if (!m_path) {
        HRESULT hr = GraphicsContext::systemFactory()->CreateGeometryGroup(D2D1_FILL_MODE_WINDING, nullptr, 0, &m_path);
        ASSERT(SUCCEEDED(hr));
        if (FAILED(hr))
            return nullptr;
    }

    return m_path.get();
}

void Path::appendGeometry(ID2D1Geometry* geometry)
{
    unsigned geometryCount = m_path ? m_path->GetSourceGeometryCount() : 0;
    Vector<ID2D1Geometry*> geometries(geometryCount, nullptr);

    // Note: 'GetSourceGeometries' returns geometries that have a +1 ref count.
    // so they must be released before we return.
    if (geometryCount)
        m_path->GetSourceGeometries(geometries.data(), geometryCount);

    geometry->AddRef();
    geometries.append(geometry);

    auto fillMode = m_path ? m_path->GetFillMode() : D2D1_FILL_MODE_WINDING;

    COMPtr<ID2D1GeometryGroup> protectedPath = m_path;
    m_path = nullptr;

    HRESULT hr = GraphicsContext::systemFactory()->CreateGeometryGroup(fillMode, geometries.data(), geometries.size(), &m_path);
    RELEASE_ASSERT(SUCCEEDED(hr));

    for (auto entry : geometries)
        entry->Release();
}

void Path::createGeometryWithFillMode(WindRule webkitFillMode, COMPtr<ID2D1GeometryGroup>& path) const
{
    RELEASE_ASSERT(m_path);

    auto fillMode = (webkitFillMode == WindRule::EvenOdd) ? D2D1_FILL_MODE_ALTERNATE : D2D1_FILL_MODE_WINDING;

    if (fillMode == m_path->GetFillMode()) {
        path = m_path;
        return;
    }

    unsigned geometryCount = m_path->GetSourceGeometryCount();

    Vector<ID2D1Geometry*> geometries(geometryCount, nullptr);
    ASSERT(geometryCount);

    // Note: 'GetSourceGeometries' returns geometries that have a +1 ref count.
    // so they must be released before we return.
    m_path->GetSourceGeometries(geometries.data(), geometryCount);

    HRESULT hr = GraphicsContext::systemFactory()->CreateGeometryGroup(fillMode, geometries.data(), geometries.size(), &path);
    RELEASE_ASSERT(SUCCEEDED(hr));

    for (auto entry : geometries)
        entry->Release();
}

Path::Path(const Path& other)
{
    if (other.platformPath() && other.activePath()) {
        auto otherPath = other.platformPath();

        unsigned geometryCount = otherPath->GetSourceGeometryCount();

        Vector<ID2D1Geometry*> geometries(geometryCount, nullptr);
        ASSERT(geometryCount);

        // Note: 'GetSourceGeometries' returns geometries that have a +1 ref count.
        // so they must be released before we return.
        otherPath->GetSourceGeometries(geometries.data(), geometryCount);

        HRESULT hr = GraphicsContext::systemFactory()->CreateGeometryGroup(other.m_path->GetFillMode(), geometries.data(), geometryCount, &m_path);
        RELEASE_ASSERT(SUCCEEDED(hr));

        for (auto entry : geometries)
            entry->Release();
    }
}
    
Path::Path(Path&& other)
{
    m_path = other.m_path;
    m_activePath = other.m_activePath;
    m_activePathGeometry = other.m_activePathGeometry;
    other.m_path = nullptr;
    other.m_activePath = nullptr;
    other.m_activePathGeometry = nullptr;
}

Path& Path::operator=(const Path& other)
{
    m_path = other.m_path;
    m_activePath = other.m_activePath;
    m_activePathGeometry = other.m_activePathGeometry;
    return *this;
}

Path& Path::operator=(Path&& other)
{
    if (this == &other)
        return *this;
    m_path = other.m_path;
    m_activePath = other.m_activePath;
    m_activePathGeometry = other.m_activePathGeometry;
    other.m_path = nullptr;
    other.m_activePath = nullptr;
    other.m_activePathGeometry = nullptr;
    return *this;
}

HRESULT Path::initializePathState()
{
    m_path = nullptr;
    m_activePath = nullptr;
    m_activePathGeometry = nullptr;

    GraphicsContext::systemFactory()->CreatePathGeometry(&m_activePathGeometry);

    Vector<ID2D1Geometry*> geometries;
    geometries.append(m_activePathGeometry.get());

    HRESULT hr = GraphicsContext::systemFactory()->CreateGeometryGroup(D2D1_FILL_MODE_WINDING, geometries.data(), geometries.size(), &m_path);
    if (FAILED(hr))
        return hr;

    return m_activePathGeometry->Open(&m_activePath);
}

void Path::drawDidComplete() const
{
    FloatPoint currentPoint = this->currentPoint();

    // To maintain proper semantics with CG, we need to clear our Direct2D
    // path objects when a draw has finished.
    HRESULT hr = const_cast<Path*>(this)->initializePathState();

    if (!SUCCEEDED(hr))
        return;

    m_activePath->SetFillMode(D2D1_FILL_MODE_WINDING);

    m_activePath->BeginFigure(currentPoint, D2D1_FIGURE_BEGIN_FILLED);
}

bool Path::contains(const FloatPoint& point, WindRule rule) const
{
    if (isNull())
        return false;

    if (!fastBoundingRect().contains(point))
        return false;

    BOOL contains;
    if (!SUCCEEDED(m_path->FillContainsPoint(D2D1::Point2F(point.x(), point.y()), nullptr, &contains)))
        return false;

    return contains;
}

bool Path::strokeContains(StrokeStyleApplier* applier, const FloatPoint& point) const
{
    if (isNull())
        return false;

    ASSERT(applier);

    GraphicsContext scratchContext(scratchRenderTarget());
    applier->strokeStyle(&scratchContext);

    BOOL containsPoint = false;
    HRESULT hr = m_path->StrokeContainsPoint(point, scratchContext.strokeThickness(), scratchContext.platformStrokeStyle(), nullptr, 1.0f, &containsPoint);
    if (!SUCCEEDED(hr))
        return false;

    return containsPoint;
}

void Path::translate(const FloatSize& size)
{
    transform(AffineTransform(1, 0, 0, 1, size.width(), size.height()));
}

void Path::transform(const AffineTransform& transform)
{
    if (transform.isIdentity() || isEmpty())
        return;

    bool pathIsActive = false;
    if (m_activePath) {
        m_activePath->Close();
        m_activePath = nullptr;
        m_activePathGeometry = nullptr;
        pathIsActive = true;
    }

    const D2D1_MATRIX_3X2_F& d2dTransform = static_cast<const D2D1_MATRIX_3X2_F>(transform);
    COMPtr<ID2D1TransformedGeometry> transformedPath;
    if (!SUCCEEDED(GraphicsContext::systemFactory()->CreateTransformedGeometry(m_path.get(), d2dTransform, &transformedPath)))
        return;

    Vector<ID2D1Geometry*> geometries;
    geometries.append(transformedPath.get());

    if (pathIsActive) {
        GraphicsContext::systemFactory()->CreatePathGeometry(&m_activePathGeometry);
        m_activePathGeometry->Open(&m_activePath);
        geometries.append(m_activePathGeometry.get());
    }

    auto fillMode = m_path->GetFillMode();

    m_path = nullptr;

    HRESULT hr = GraphicsContext::systemFactory()->CreateGeometryGroup(fillMode, geometries.data(), geometries.size(), &m_path);
    RELEASE_ASSERT(SUCCEEDED(hr));
}

FloatRect Path::boundingRect() const
{
    if (isNull())
        return FloatRect();

    D2D1_RECT_F bounds = { };
    if (!SUCCEEDED(m_path->GetBounds(nullptr, &bounds)))
        return FloatRect();

    return bounds;
}

FloatRect Path::fastBoundingRect() const
{
    if (isNull())
        return FloatRect();

    D2D1_RECT_F bounds = { };
    if (!SUCCEEDED(m_path->GetBounds(nullptr, &bounds)))
        return FloatRect();

    return bounds;
}

FloatRect Path::strokeBoundingRect(StrokeStyleApplier* applier) const
{
    if (isNull())
        return FloatRect();

    if (!applier)
        return boundingRect();

    UNUSED_PARAM(applier);
    notImplemented();

    // Just return regular bounding rect for now.
    return boundingRect();
}

void Path::moveTo(const FloatPoint& point)
{
    if (!m_activePath) {
        GraphicsContext::systemFactory()->CreatePathGeometry(&m_activePathGeometry);

        appendGeometry(m_activePathGeometry.get());

        if (!SUCCEEDED(m_activePathGeometry->Open(&m_activePath)))
            return;

        m_activePath->SetFillMode(D2D1_FILL_MODE_WINDING);
    }

    m_activePath->BeginFigure(point, D2D1_FIGURE_BEGIN_FILLED);
}

void Path::addLineTo(const FloatPoint& point)
{
    ASSERT(m_activePath.get());

    m_activePath->AddLine(point);
}

void Path::addQuadCurveTo(const FloatPoint& cp, const FloatPoint& p)
{
    ASSERT(m_activePath.get());

    m_activePath->AddQuadraticBezier(D2D1::QuadraticBezierSegment(cp, p));
}

void Path::addBezierCurveTo(const FloatPoint& cp1, const FloatPoint& cp2, const FloatPoint& p)
{
    ASSERT(m_activePath.get());

    FloatPoint beforePoint = currentPoint();

    m_activePath->AddBezier(D2D1::BezierSegment(cp1, cp2, p));
}

void Path::addArcTo(const FloatPoint& p1, const FloatPoint& p2, float radius)
{
    UNUSED_PARAM(p1);
    UNUSED_PARAM(p2);
    UNUSED_PARAM(radius);
    notImplemented();
}

static bool equalRadiusWidths(const FloatSize& topLeftRadius, const FloatSize& topRightRadius, const FloatSize& bottomLeftRadius, const FloatSize& bottomRightRadius)
{
    return topLeftRadius.width() == topRightRadius.width()
        && topRightRadius.width() == bottomLeftRadius.width()
        && bottomLeftRadius.width() == bottomRightRadius.width();
}

static bool equalRadiusHeights(const FloatSize& topLeftRadius, const FloatSize& topRightRadius, const FloatSize& bottomLeftRadius, const FloatSize& bottomRightRadius)
{
    return topLeftRadius.height() == bottomLeftRadius.height()
        && bottomLeftRadius.height() == topRightRadius.height()
        && topRightRadius.height() == bottomRightRadius.height();
}

void Path::platformAddPathForRoundedRect(const FloatRect& rect, const FloatSize& topLeftRadius, const FloatSize& topRightRadius, const FloatSize& bottomLeftRadius, const FloatSize& bottomRightRadius)
{
    bool equalWidths = equalRadiusWidths(topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius);
    bool equalHeights = equalRadiusHeights(topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius);

    if (equalWidths && equalHeights) {
        // Ensure that CG can render the rounded rect.
        float radiusWidth = topLeftRadius.width();
        float radiusHeight = topLeftRadius.height();
        auto rectToDraw = D2D1::RectF(rect.x(), rect.y(), rect.maxX(), rect.maxY());

        COMPtr<ID2D1RoundedRectangleGeometry> roundRect;
        HRESULT hr = GraphicsContext::systemFactory()->CreateRoundedRectangleGeometry(D2D1::RoundedRect(rectToDraw, radiusWidth, radiusHeight), &roundRect);
        RELEASE_ASSERT(SUCCEEDED(hr));
        appendGeometry(roundRect.get());
        return;
    }

    addBeziersForRoundedRect(rect, topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius);
}

void Path::closeSubpath()
{
    if (isNull())
        return;

    ASSERT(m_activePath.get());
    m_activePath->EndFigure(D2D1_FIGURE_END_OPEN);
}

static FloatPoint arcStart(const FloatPoint& center, float radius, float startAngle)
{
    FloatPoint startingPoint = center;
    float startX = radius * std::cos(startAngle);
    float startY = radius * std::sin(startAngle);
    startingPoint.move(startX, startY);
    return startingPoint;
}

static void drawArcSection(ID2D1GeometrySink* sink, const FloatPoint& center, float radius, float startAngle, float endAngle, bool clockwise)
{
    // Direct2D wants us to specify the end point of the arc, not the center. It will be drawn from
    // whatever the current point in the 'm_activePath' is.
    FloatPoint p = center;
    float endX = radius * std::cos(endAngle);
    float endY = radius * std::sin(endAngle);
    p.move(endX, endY);

    float arcDeg = rad2deg(endAngle - startAngle);
    D2D1_SWEEP_DIRECTION direction = clockwise ? D2D1_SWEEP_DIRECTION_CLOCKWISE : D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE;
    sink->AddArc(D2D1::ArcSegment(p, D2D1::SizeF(radius, radius), arcDeg, direction, D2D1_ARC_SIZE_SMALL));
}

void Path::addArc(const FloatPoint& center, float radius, float startAngle, float endAngle, bool clockwise)
{
    auto arcStartPoint = arcStart(center, radius, startAngle);
    if (!m_activePath)
        moveTo(arcStartPoint);
    else if (!areEssentiallyEqual(currentPoint(), arcStartPoint)) {
        // If the arc defined by the center and radius does not intersect the current position,
        // we need to draw a line from the current position to the starting point of the arc.
        addLineTo(arcStartPoint);
    }

    // Direct2D has problems drawing large arcs. It gets confused if drawing a complete (or
    // nearly complete) circle in the counter-clockwise direction. So, draw any arcs larger
    // than 180 degrees in two pieces.
    float fullSweep = endAngle - startAngle;
    float negate = fullSweep < 0 ? -1.0f : 1.0f;
    float maxSweep = negate * std::min(std::abs(fullSweep), piFloat);
    float firstArcEnd = startAngle + maxSweep;
    drawArcSection(m_activePath.get(), center, radius, startAngle, firstArcEnd, clockwise);

    if (WTF::areEssentiallyEqual(firstArcEnd, endAngle))
        return;

    drawArcSection(m_activePath.get(), center, radius, firstArcEnd, endAngle, clockwise);
}

void Path::addRect(const FloatRect& r)
{
    COMPtr<ID2D1RectangleGeometry> rectangle;
    HRESULT hr = GraphicsContext::systemFactory()->CreateRectangleGeometry(r, &rectangle);
    RELEASE_ASSERT(SUCCEEDED(hr));
    appendGeometry(rectangle.get());
}

void Path::addEllipse(FloatPoint p, float radiusX, float radiusY, float rotation, float startAngle, float endAngle, bool anticlockwise)
{
    AffineTransform transform;
    transform.translate(p.x(), p.y()).rotate(rad2deg(rotation)).scale(radiusX, radiusY);

    notImplemented();
}

void Path::addEllipse(const FloatRect& r)
{
    COMPtr<ID2D1EllipseGeometry> ellipse;
    HRESULT hr = GraphicsContext::systemFactory()->CreateEllipseGeometry(D2D1::Ellipse(r.center(), r.width(), r.height()), &ellipse);
    RELEASE_ASSERT(SUCCEEDED(hr));
    appendGeometry(ellipse.get());
}

void Path::addPath(const Path& path, const AffineTransform& transform)
{
    if (!path.platformPath())
        return;

    if (!transform.isInvertible())
        return;

    notImplemented();
}


void Path::clear()
{
    if (isNull())
        return;

    m_path = nullptr;
    m_activePath = nullptr;
    m_activePathGeometry = nullptr;
}

bool Path::isEmpty() const
{
    if (isNull())
        return true;

    if (!m_path->GetSourceGeometryCount())
        return true;

    return false;
}

bool Path::hasCurrentPoint() const
{
    return !isEmpty();
}
    
FloatPoint Path::currentPoint() const 
{
    if (isNull())
        return FloatPoint();

    float length = 0;
    HRESULT hr = m_path->ComputeLength(nullptr, &length);
    if (!SUCCEEDED(hr))
        return FloatPoint();

    D2D1_POINT_2F point = { };
    D2D1_POINT_2F tangent = { };
    hr = m_path->ComputePointAtLength(length, nullptr, &point, &tangent);
    if (!SUCCEEDED(hr))
        return FloatPoint();

    return point;
}

float Path::length() const
{
    float length = 0;
    HRESULT hr = m_path->ComputeLength(nullptr, &length);
    if (!SUCCEEDED(hr))
        return 0;

    return length;
}

void Path::apply(const PathApplierFunction& function) const
{
    if (isNull())
        return;

    notImplemented();
}

}

#endif // USE(DIRECT2D)