skia2/experimental/pathkit/pathkit_wasm_bindings.cpp
Kevin Lubick 641bf8745d [PathKit] Add more Path2D functionality and move some methods to be members
Adds arc, arcTo, rect and Path2D names for quadTo, cubicTo, close.

Adds conic verb support (approximated with 2 quads).

Breaking changes:
Some functions have been moved to be member functions:
PathKit.Simplify(path) -> path.simplify()
PathKit.ToCanvas(path, ctx) -> path.toCanvas(ctx)
PathKit.ToSVGString(path) -> path.toSVGString()
PathKit.ToPath2D(path) -> path.toPath2D()
PathKit.ToCmds(path) -> path.toCmds()
PathKit.ResolveBuilder(builder) -> builder.resolve()
PathKit.GetBoundaryPathFromRegion(region) -> region.getBoundaryPath()

Pathkit.ApplyPathOp(pathOne, pathTwo, op) still exists, but there's
now also pathOne.op(pathTwo, op) for cases when that's easier.

As per custom with version 0.x.y projects, I'm bumping the
minor version (in npm) for these breaking changes instead of the
major version (which will happen when we are version >= 1.0.0).

This also has some small improvements to the output code size.
The biggest jump was from enabling the closure compiler on the
helper JS, which trimmed it down by about 40%. Using the closure
compiler requires the JRE on the bots, which prompted the emsdk-base
image change.

Bug: skia:8216
Change-Id: I40902d23380093c34d1679df0255bcb0eaa77b01
Reviewed-on: https://skia-review.googlesource.com/145420
Reviewed-by: Joe Gregorio <jcgregorio@google.com>
Reviewed-by: Florin Malita <fmalita@chromium.org>
2018-08-06 19:07:12 +00:00

458 lines
18 KiB
C++

/*
* Copyright 2018 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "SkFloatBits.h"
#include "SkFloatingPoint.h"
#include "SkMatrix.h"
#include "SkParsePath.h"
#include "SkPath.h"
#include "SkPathOps.h"
#include "SkRect.h"
#include "SkRegion.h"
#include "SkString.h"
#include <emscripten/emscripten.h>
#include <emscripten/bind.h>
using namespace emscripten;
static const int MOVE = 0;
static const int LINE = 1;
static const int QUAD = 2;
static const int CUBIC = 4;
static const int CLOSE = 5;
// =================================================================================
// Creating/Exporting Paths with cmd arrays
// =================================================================================
template <typename VisitFunc>
void VisitPath(const SkPath& p, VisitFunc&& f) {
SkPath::RawIter iter(p);
SkPoint pts[4];
SkPath::Verb verb;
while ((verb = iter.next(pts)) != SkPath::kDone_Verb) {
f(verb, pts, iter);
}
}
emscripten::val EMSCRIPTEN_KEEPALIVE ToCmds(const SkPath& path) {
val cmds = emscripten::val::array();
VisitPath(path, [&cmds](SkPath::Verb verb, const SkPoint pts[4], SkPath::RawIter iter) {
val cmd = emscripten::val::array();
switch (verb) {
case SkPath::kMove_Verb:
cmd.call<void>("push", MOVE, pts[0].x(), pts[0].y());
break;
case SkPath::kLine_Verb:
cmd.call<void>("push", LINE, pts[1].x(), pts[1].y());
break;
case SkPath::kQuad_Verb:
cmd.call<void>("push", QUAD, pts[1].x(), pts[1].y(), pts[2].x(), pts[2].y());
break;
case SkPath::kConic_Verb:
SkPoint quads[5];
// approximate with 2^1=2 quads.
SkPath::ConvertConicToQuads(pts[0], pts[1], pts[2], iter.conicWeight(), quads, 1);
cmd.call<void>("push", MOVE, quads[0].x(), quads[0].y());
cmds.call<void>("push", cmd);
cmd = emscripten::val::array();
cmd.call<void>("push", QUAD, quads[1].x(), quads[1].y(), quads[2].x(), quads[2].y());
cmds.call<void>("push", cmd);
cmd = emscripten::val::array();
cmd.call<void>("push", QUAD, quads[3].x(), quads[3].y(), quads[4].x(), quads[4].y());
break;
case SkPath::kCubic_Verb:
cmd.call<void>("push", CUBIC,
pts[1].x(), pts[1].y(),
pts[2].x(), pts[2].y(),
pts[3].x(), pts[3].y());
break;
case SkPath::kClose_Verb:
cmd.call<void>("push", CLOSE);
break;
case SkPath::kDone_Verb:
SkASSERT(false);
break;
}
cmds.call<void>("push", cmd);
});
return cmds;
}
// This type signature is a mess, but it's necessary. See, we can't use "bind" (EMSCRIPTEN_BINDINGS)
// and pointers to primitive types (Only bound types like SkPoint). We could if we used
// cwrap (see https://becominghuman.ai/passing-and-returning-webassembly-array-parameters-a0f572c65d97)
// but that requires us to stick to C code and, AFAIK, doesn't allow us to return nice things like
// SkPath or SkOpBuilder.
//
// So, basically, if we are using C++ and EMSCRIPTEN_BINDINGS, we can't have primative pointers
// in our function type signatures. (this gives an error message like "Cannot call foo due to unbound
// types Pi, Pf"). But, we can just pretend they are numbers and cast them to be pointers and
// the compiler is happy.
SkPath EMSCRIPTEN_KEEPALIVE FromCmds(uintptr_t /* float* */ cptr, int numCmds) {
const auto* cmds = reinterpret_cast<const float*>(cptr);
SkPath path;
float x1, y1, x2, y2, x3, y3;
// if there are not enough arguments, bail with the path we've constructed so far.
#define CHECK_NUM_ARGS(n) \
if ((i + n) > numCmds) { \
SkDebugf("Not enough args to match the verbs. Saw %d commands\n", numCmds); \
return path; \
}
for(int i = 0; i < numCmds;){
switch (sk_float_floor2int(cmds[i++])) {
case MOVE:
CHECK_NUM_ARGS(2);
x1 = cmds[i++], y1 = cmds[i++];
path.moveTo(x1, y1);
break;
case LINE:
CHECK_NUM_ARGS(2);
x1 = cmds[i++], y1 = cmds[i++];
path.lineTo(x1, y1);
break;
case QUAD:
CHECK_NUM_ARGS(4);
x1 = cmds[i++], y1 = cmds[i++];
x2 = cmds[i++], y2 = cmds[i++];
path.quadTo(x1, y1, x2, y2);
break;
case CUBIC:
CHECK_NUM_ARGS(6);
x1 = cmds[i++], y1 = cmds[i++];
x2 = cmds[i++], y2 = cmds[i++];
x3 = cmds[i++], y3 = cmds[i++];
path.cubicTo(x1, y1, x2, y2, x3, y3);
break;
case CLOSE:
path.close();
break;
default:
SkDebugf(" path: UNKNOWN command %f, aborting dump...\n", cmds[i-1]);
return path;
}
}
#undef CHECK_NUM_ARGS
return path;
}
SkPath EMSCRIPTEN_KEEPALIVE NewPath() {
return SkPath();
}
//========================================================================================
// SVG things
//========================================================================================
val EMSCRIPTEN_KEEPALIVE ToSVGString(const SkPath& path) {
SkString s;
SkParsePath::ToSVGString(path, &s);
// Wrapping it in val automatically turns it into a JS string.
// Not too sure on performance implications, but is is simpler than
// returning a raw pointer to const char * and then using
// Pointer_stringify() on the calling side.
return val(s.c_str());
}
SkPath EMSCRIPTEN_KEEPALIVE FromSVGString(std::string str) {
SkPath path;
SkParsePath::FromSVGString(str.c_str(), &path);
return path;
}
//========================================================================================
// PATHOP things
//========================================================================================
SkPath EMSCRIPTEN_KEEPALIVE SimplifyPath(const SkPath& path) {
SkPath simple;
Simplify(path, &simple);
return simple;
}
SkPath EMSCRIPTEN_KEEPALIVE ApplyPathOp(const SkPath& pathOne, const SkPath& pathTwo, SkPathOp op) {
SkPath path;
Op(pathOne, pathTwo, op, &path);
return path;
}
SkPath EMSCRIPTEN_KEEPALIVE ResolveBuilder(SkOpBuilder& builder) {
SkPath path;
builder.resolve(&path);
return path;
}
//========================================================================================
// Canvas things
//========================================================================================
void EMSCRIPTEN_KEEPALIVE ToCanvas(const SkPath& path, val/* Path2D or Canvas*/ ctx) {
SkPath::Iter iter(path, false);
SkPoint pts[4];
SkPath::Verb verb;
while ((verb = iter.next(pts, false)) != SkPath::kDone_Verb) {
switch (verb) {
case SkPath::kMove_Verb:
ctx.call<void>("moveTo", pts[0].x(), pts[0].y());
break;
case SkPath::kLine_Verb:
ctx.call<void>("lineTo", pts[1].x(), pts[1].y());
break;
case SkPath::kQuad_Verb:
ctx.call<void>("quadraticCurveTo", pts[1].x(), pts[1].y(), pts[2].x(), pts[2].y());
break;
case SkPath::kConic_Verb:
SkPoint quads[5];
// approximate with 2^1=2 quads.
SkPath::ConvertConicToQuads(pts[0], pts[1], pts[2], iter.conicWeight(), quads, 1);
ctx.call<void>("moveTo", quads[0].x(), quads[0].y());
ctx.call<void>("quadraticCurveTo", quads[1].x(), quads[1].y(), quads[2].x(), quads[2].y());
ctx.call<void>("quadraticCurveTo", quads[3].x(), quads[3].y(), quads[4].x(), quads[4].y());
break;
case SkPath::kCubic_Verb:
ctx.call<void>("bezierCurveTo", pts[1].x(), pts[1].y(), pts[2].x(), pts[2].y(),
pts[3].x(), pts[3].y());
break;
case SkPath::kClose_Verb:
ctx.call<void>("closePath");
break;
case SkPath::kDone_Verb:
break;
}
}
}
emscripten::val JSPath2D = emscripten::val::global("Path2D");
emscripten::val EMSCRIPTEN_KEEPALIVE ToPath2D(const SkPath& path) {
val retVal = JSPath2D.new_();
ToCanvas(path, retVal);
return retVal;
}
// ======================================================================================
// Path2D API things
// ======================================================================================
void Path2DAddRect(SkPath& path, SkScalar x, SkScalar y, SkScalar width, SkScalar height) {
path.addRect(x, y, x+width, y+height);
}
void Path2DAddArc(SkPath& path, SkScalar x, SkScalar y, SkScalar radius,
SkScalar startAngle, SkScalar endAngle, bool ccw) {
SkPath temp;
SkRect bounds = SkRect::MakeLTRB(x-radius, y-radius, x+radius, y+radius);
const auto sweep = SkRadiansToDegrees(endAngle - startAngle) - 360 * ccw;
temp.addArc(bounds, SkRadiansToDegrees(startAngle), sweep);
path.addPath(temp, SkPath::kExtend_AddPathMode);
}
void Path2DAddArc(SkPath& path, SkScalar x, SkScalar y, SkScalar radius,
SkScalar startAngle, SkScalar endAngle) {
Path2DAddArc(path, x, y, radius, startAngle, endAngle, false);
}
void Path2DAddEllipse(SkPath& path, SkScalar x, SkScalar y, SkScalar radiusX, SkScalar radiusY,
SkScalar rotation, SkScalar startAngle, SkScalar endAngle, bool ccw) {
// This is easiest to do by making a new path and then extending the current path
// (this properly catches the cases of if there's a moveTo before this call or not).
SkRect bounds = SkRect::MakeLTRB(x-radiusX, y-radiusY, x+radiusX, y+radiusY);
SkPath temp;
const auto sweep = SkRadiansToDegrees(endAngle - startAngle) - (360 * ccw);
temp.addArc(bounds, SkRadiansToDegrees(startAngle), sweep);
SkMatrix m;
m.setRotate(SkRadiansToDegrees(rotation), x, y);
path.addPath(temp, m, SkPath::kExtend_AddPathMode);
}
void Path2DAddEllipse(SkPath& path, SkScalar x, SkScalar y, SkScalar radiusX, SkScalar radiusY,
SkScalar rotation, SkScalar startAngle, SkScalar endAngle) {
Path2DAddEllipse(path, x, y, radiusX, radiusY, rotation, startAngle, endAngle, false);
}
void Path2DAddPath(SkPath& orig, const SkPath& newPath) {
orig.addPath(newPath);
}
void Path2DAddPath(SkPath& orig, const SkPath& newPath, val /* SVGMatrix*/ t) {
SkMatrix m = SkMatrix::MakeAll(
t["a"].as<SkScalar>(), t["c"].as<SkScalar>(), t["e"].as<SkScalar>(),
t["b"].as<SkScalar>(), t["d"].as<SkScalar>(), t["f"].as<SkScalar>(),
0 , 0 , 1);
orig.addPath(newPath, m);
}
// Mimics the order of SVGMatrix, just w/o the SVG Matrix
// This order is scaleX, skewY, skewX, scaleY, transX, transY
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#Transform_functions
void Path2DAddPath(SkPath& orig, const SkPath& newPath, SkScalar a, SkScalar b, SkScalar c, SkScalar d, SkScalar e, SkScalar f) {
SkMatrix m = SkMatrix::MakeAll(a, c, e,
b, d, f,
0, 0, 1);
orig.addPath(newPath, m);
}
// Allows for full matix control.
void Path2DAddPath(SkPath& orig, const SkPath& newPath,
SkScalar scaleX, SkScalar skewX, SkScalar transX,
SkScalar skewY, SkScalar scaleY, SkScalar transY,
SkScalar pers0, SkScalar pers1, SkScalar pers2) {
SkMatrix m = SkMatrix::MakeAll(scaleX, skewX , transX,
skewY , scaleY, transY,
pers0 , pers1 , pers2);
orig.addPath(newPath, m);
}
//========================================================================================
// Region things
//========================================================================================
#ifdef PATHKIT_TESTING
SkPath GetBoundaryPathFromRegion(SkRegion& region) {
SkPath p;
region.getBoundaryPath(&p);
return p;
}
#endif
// Binds the classes to the JS
//
// See https://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/embind.html#non-member-functions-on-the-javascript-prototype
// for more on binding non-member functions to the JS object, allowing us to rewire
// various functions. That is, we can make the SkPath we expose appear to have methods
// that the original SkPath does not, like rect(x, y, width, height) and toPath2D().
//
// An important detail for binding non-member functions is that the first argument
// must be SkPath& (the reference part is very important).
EMSCRIPTEN_BINDINGS(skia) {
class_<SkPath>("SkPath")
.constructor<>()
// Path2D API
.function("addPath",
select_overload<void(SkPath&, const SkPath&)>(&Path2DAddPath))
.function("addPath",
select_overload<void(SkPath&, const SkPath&, val)>(&Path2DAddPath))
.function("arc",
select_overload<void(SkPath&, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&Path2DAddArc))
.function("arc",
select_overload<void(SkPath&, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, bool)>(&Path2DAddArc))
.function("arcTo",
select_overload<void(SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&SkPath::arcTo))
.function("bezierCurveTo",
select_overload<void(SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&SkPath::cubicTo))
.function("closePath", &SkPath::close)
.function("ellipse",
select_overload<void(SkPath&, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&Path2DAddEllipse))
.function("ellipse",
select_overload<void(SkPath&, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, bool)>(&Path2DAddEllipse))
.function("lineTo",
select_overload<void(SkScalar, SkScalar)>(&SkPath::lineTo))
.function("moveTo",
select_overload<void(SkScalar, SkScalar)>(&SkPath::moveTo))
.function("quadraticCurveTo",
select_overload<void(SkScalar, SkScalar, SkScalar, SkScalar)>(&SkPath::quadTo))
.function("rect", &Path2DAddRect)
// Some shorthand helpers, to mirror SkPath.cpp's API
.function("addPath",
select_overload<void(SkPath&, const SkPath&, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&Path2DAddPath))
.function("addPath",
select_overload<void(SkPath&, const SkPath&, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&Path2DAddPath))
.function("close", &SkPath::close)
.function("cubicTo",
select_overload<void(SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&SkPath::cubicTo))
.function("quadTo",
select_overload<void(SkScalar, SkScalar, SkScalar, SkScalar)>(&SkPath::quadTo))
// PathOps
.function("simplify", &SimplifyPath)
.function("op", &ApplyPathOp)
// Exporting
.function("toCmds", &ToCmds)
.function("toPath2D", &ToPath2D)
.function("toCanvas", &ToCanvas)
.function("toSVGString", &ToSVGString)
#ifdef PATHKIT_TESTING
.function("dump", select_overload<void() const>(&SkPath::dump))
#endif
;
class_<SkOpBuilder>("SkOpBuilder")
.constructor<>()
.function("add", &SkOpBuilder::add)
.function("resolve", &ResolveBuilder);
// Without these function() bindings, the function would be exposed but oblivious to
// our types (e.g. SkPath)
// Import
function("FromSVGString", &FromSVGString);
function("FromCmds", &FromCmds);
function("NewPath", &NewPath);
// Path2D is opaque, so we can't read in from it.
// PathOps
function("ApplyPathOp", &ApplyPathOp);
enum_<SkPathOp>("PathOp")
.value("DIFFERENCE", SkPathOp::kDifference_SkPathOp)
.value("INTERSECT", SkPathOp::kIntersect_SkPathOp)
.value("UNION", SkPathOp::kUnion_SkPathOp)
.value("XOR", SkPathOp::kXOR_SkPathOp)
.value("REVERSE_DIFFERENCE", SkPathOp::kReverseDifference_SkPathOp);
constant("MOVE_VERB", MOVE);
constant("LINE_VERB", LINE);
constant("QUAD_VERB", QUAD);
constant("CUBIC_VERB", CUBIC);
constant("CLOSE_VERB", CLOSE);
// coming soon - Stroke
// coming soon - Matrix
// coming soon - Bounds/Trim
#ifdef PATHKIT_TESTING
function("SkBits2Float", &SkBits2Float);
enum_<SkRegion::Op>("RegionOp")
.value("DIFFERENCE", SkRegion::Op::kDifference_Op)
.value("INTERSECT", SkRegion::Op::kIntersect_Op)
.value("UNION", SkRegion::Op::kUnion_Op)
.value("XOR", SkRegion::Op::kXOR_Op)
.value("REVERSE_DIFFERENCE", SkRegion::Op::kReverseDifference_Op)
.value("REPLACE", SkRegion::Op::kReplace_Op);
class_<SkRegion>("SkRegion")
.constructor<>()
.function("setRect",
select_overload<bool(int32_t, int32_t, int32_t, int32_t)>(&SkRegion::setRect))
.function("setPath", &SkRegion::setPath)
.function("opLTRB",
select_overload<bool(int32_t, int32_t, int32_t, int32_t, SkRegion::Op)>(&SkRegion::op))
.function("opRegion",
select_overload<bool(const SkRegion&, SkRegion::Op)>(&SkRegion::op))
.function("opRegionAB",
select_overload<bool(const SkRegion&, const SkRegion&, SkRegion::Op)>(&SkRegion::op))
.function("getBoundaryPath", &GetBoundaryPathFromRegion);
#endif
}