292 lines
12 KiB
JavaScript
292 lines
12 KiB
JavaScript
/*
|
|
Copyright (c) 2014-2017, Jan Bösenberg & Jürg Lehni
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
*/
|
|
|
|
var OffsetUtils = {
|
|
offsetPath: function(path, offset, result) {
|
|
var outerPath = new Path({ insert: false }),
|
|
epsilon = Numerical.GEOMETRIC_EPSILON,
|
|
enforeArcs = true;
|
|
for (var i = 0; i < path.curves.length; i++) {
|
|
var curve = path.curves[i];
|
|
if (curve.hasLength(epsilon)) {
|
|
var segments = this.getOffsetSegments(curve, offset),
|
|
start = segments[0];
|
|
if (outerPath.isEmpty()) {
|
|
outerPath.addSegments(segments);
|
|
} else {
|
|
var lastCurve = outerPath.lastCurve;
|
|
if (!lastCurve.point2.isClose(start.point, epsilon)) {
|
|
if (enforeArcs || lastCurve.getTangentAtTime(1).dot(start.point.subtract(curve.point1)) >= 0) {
|
|
this.addRoundJoin(outerPath, start.point, curve.point1, Math.abs(offset));
|
|
} else {
|
|
// Connect points with a line
|
|
outerPath.lineTo(start.point);
|
|
}
|
|
}
|
|
outerPath.lastSegment.handleOut = start.handleOut;
|
|
outerPath.addSegments(segments.slice(1));
|
|
}
|
|
}
|
|
}
|
|
if (path.isClosed()) {
|
|
if (!outerPath.lastSegment.point.isClose(outerPath.firstSegment.point, epsilon) && (enforeArcs ||
|
|
outerPath.lastCurve.getTangentAtTime(1).dot(outerPath.firstSegment.point.subtract(path.firstSegment.point)) >= 0)) {
|
|
this.addRoundJoin(outerPath, outerPath.firstSegment.point, path.firstSegment.point, Math.abs(offset));
|
|
}
|
|
outerPath.closePath();
|
|
}
|
|
return outerPath;
|
|
},
|
|
|
|
/**
|
|
* Creates an offset for the specified curve and returns the segments of
|
|
* that offset path.
|
|
*
|
|
* @param {Curve} curve the curve to be offset
|
|
* @param {Number} offset the offset distance
|
|
* @returns {Segment[]} an array of segments describing the offset path
|
|
*/
|
|
getOffsetSegments: function(curve, offset) {
|
|
if (curve.isStraight()) {
|
|
var n = curve.getNormalAtTime(0.5).multiply(offset),
|
|
p1 = curve.point1.add(n),
|
|
p2 = curve.point2.add(n);
|
|
return [new Segment(p1), new Segment(p2)];
|
|
} else {
|
|
var curves = this.splitCurveForOffseting(curve),
|
|
segments = [];
|
|
for (var i = 0, l = curves.length; i < l; i++) {
|
|
var offsetCurves = this.getOffsetCurves(curves[i], offset, 0),
|
|
prevSegment;
|
|
for (var j = 0, m = offsetCurves.length; j < m; j++) {
|
|
var curve = offsetCurves[j],
|
|
segment = curve.segment1;
|
|
if (prevSegment) {
|
|
prevSegment.handleOut = segment.handleOut;
|
|
} else {
|
|
segments.push(segment);
|
|
}
|
|
segments.push(prevSegment = curve.segment2);
|
|
}
|
|
}
|
|
return segments;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Approach for Curve Offsetting based on:
|
|
* "A New Shape Control and Classification for Cubic Bézier Curves"
|
|
* Shi-Nine Yang and Ming-Liang Huang
|
|
*/
|
|
offsetCurve_middle: function(curve, offset) {
|
|
var v = curve.getValues(),
|
|
p1 = curve.point1.add(Curve.getNormal(v, 0).multiply(offset)),
|
|
p2 = curve.point2.add(Curve.getNormal(v, 1).multiply(offset)),
|
|
pt = Curve.getPoint(v, 0.5).add(
|
|
Curve.getNormal(v, 0.5).multiply(offset)),
|
|
t1 = Curve.getTangent(v, 0),
|
|
t2 = Curve.getTangent(v, 1),
|
|
div = t1.cross(t2) * 3 / 4,
|
|
d = pt.multiply(2).subtract(p1.add(p2)),
|
|
a = d.cross(t2) / div,
|
|
b = d.cross(t1) / div;
|
|
return new Curve(p1, t1.multiply(a), t2.multiply(-b), p2);
|
|
},
|
|
|
|
offsetCurve_average: function(curve, offset) {
|
|
var v = curve.getValues(),
|
|
p1 = curve.point1.add(Curve.getNormal(v, 0).multiply(offset)),
|
|
p2 = curve.point2.add(Curve.getNormal(v, 1).multiply(offset)),
|
|
t = this.getAverageTangentTime(v),
|
|
u = 1 - t,
|
|
pt = Curve.getPoint(v, t).add(
|
|
Curve.getNormal(v, t).multiply(offset)),
|
|
t1 = Curve.getTangent(v, 0),
|
|
t2 = Curve.getTangent(v, 1),
|
|
div = t1.cross(t2) * 3 * t * u,
|
|
v = pt.subtract(
|
|
p1.multiply(u * u * (1 + 2 * t)).add(
|
|
p2.multiply(t * t * (3 - 2 * t)))),
|
|
a = v.cross(t2) / (div * u),
|
|
b = v.cross(t1) / (div * t);
|
|
return new Curve(p1, t1.multiply(a), t2.multiply(-b), p2);
|
|
},
|
|
|
|
/**
|
|
* This algorithm simply scales the curve so its end points are at the
|
|
* calculated offsets of the original end points.
|
|
*/
|
|
offsetCurve_simple: function (crv, dist) {
|
|
// calculate end points of offset curve
|
|
var p1 = crv.point1.add(crv.getNormalAtTime(0).multiply(dist));
|
|
var p4 = crv.point2.add(crv.getNormalAtTime(1).multiply(dist));
|
|
// get scale ratio
|
|
var pointDist = crv.point1.getDistance(crv.point2);
|
|
// TODO: Handle cases when pointDist == 0
|
|
var f = p1.getDistance(p4) / pointDist;
|
|
if (crv.point2.subtract(crv.point1).dot(p4.subtract(p1)) < 0) {
|
|
f = -f; // probably more correct than connecting with line
|
|
}
|
|
// Scale handles and generate offset curve
|
|
return new Curve(p1, crv.handle1.multiply(f), crv.handle2.multiply(f), p4);
|
|
},
|
|
|
|
getOffsetCurves: function(curve, offset, method) {
|
|
var errorThreshold = 0.01,
|
|
radius = Math.abs(offset),
|
|
offsetMethod = this['offsetCurve_' + (method || 'middle')],
|
|
that = this;
|
|
|
|
function offsetCurce(curve, curves, recursion) {
|
|
var offsetCurve = offsetMethod.call(that, curve, offset),
|
|
cv = curve.getValues(),
|
|
ov = offsetCurve.getValues(),
|
|
count = 16,
|
|
error = 0;
|
|
for (var i = 1; i < count; i++) {
|
|
var t = i / count,
|
|
p = Curve.getPoint(cv, t),
|
|
n = Curve.getNormal(cv, t),
|
|
roots = Curve.getCurveLineIntersections(ov, p.x, p.y, n.x, n.y),
|
|
dist = 2 * radius;
|
|
for (var j = 0, l = roots.length; j < l; j++) {
|
|
var d = Curve.getPoint(ov, roots[j]).getDistance(p);
|
|
if (d < dist)
|
|
dist = d;
|
|
}
|
|
var err = Math.abs(radius - dist);
|
|
if (err > error)
|
|
error = err;
|
|
}
|
|
if (error > errorThreshold && recursion++ < 8) {
|
|
if (error === radius) {
|
|
// console.log(cv);
|
|
}
|
|
var curve2 = curve.divideAtTime(that.getAverageTangentTime(cv));
|
|
offsetCurce(curve, curves, recursion);
|
|
offsetCurce(curve2, curves, recursion);
|
|
} else {
|
|
curves.push(offsetCurve);
|
|
}
|
|
return curves;
|
|
}
|
|
|
|
return offsetCurce(curve, [], 0);
|
|
},
|
|
|
|
/**
|
|
* Split curve into sections that can then be treated individually by an
|
|
* offset algorithm.
|
|
*/
|
|
splitCurveForOffseting: function(curve) {
|
|
var curves = [curve.clone()], // Clone so path is not modified.
|
|
that = this;
|
|
if (curve.isStraight())
|
|
return curves;
|
|
|
|
function splitAtRoots(index, roots) {
|
|
for (var i = 0, prevT, l = roots && roots.length; i < l; i++) {
|
|
var t = roots[i],
|
|
curve = curves[index].divideAtTime(
|
|
// Renormalize curve-time for multiple roots:
|
|
i ? (t - prevT) / (1 - prevT) : t);
|
|
prevT = t;
|
|
if (curve)
|
|
curves.splice(++index, 0, curve);
|
|
}
|
|
}
|
|
|
|
// Recursively splits the specified curve if the angle between the two
|
|
// handles is too large (we use 60° as a threshold).
|
|
function splitLargeAngles(index, recursion) {
|
|
var curve = curves[index],
|
|
v = curve.getValues(),
|
|
n1 = Curve.getNormal(v, 0),
|
|
n2 = Curve.getNormal(v, 1).negate(),
|
|
cos = n1.dot(n2);
|
|
if (cos > -0.5 && ++recursion < 4) {
|
|
curves.splice(index + 1, 0,
|
|
curve.divideAtTime(that.getAverageTangentTime(v)));
|
|
splitLargeAngles(index + 1, recursion);
|
|
splitLargeAngles(index, recursion);
|
|
}
|
|
}
|
|
|
|
// Split curves at cusps and inflection points.
|
|
var info = curve.classify();
|
|
if (info.roots && info.type !== 'loop') {
|
|
splitAtRoots(0, info.roots);
|
|
}
|
|
|
|
// Split sub-curves at peaks.
|
|
for (var i = curves.length - 1; i >= 0; i--) {
|
|
splitAtRoots(i, Curve.getPeaks(curves[i].getValues()));
|
|
}
|
|
|
|
// Split sub-curves with too large angle between handles.
|
|
for (var i = curves.length - 1; i >= 0; i--) {
|
|
//splitLargeAngles(i, 0);
|
|
}
|
|
return curves;
|
|
},
|
|
|
|
/**
|
|
* Returns the first curve-time where the curve has its tangent in the same
|
|
* direction as the average of the tangents at its beginning and end.
|
|
*/
|
|
getAverageTangentTime: function(v) {
|
|
var tan = Curve.getTangent(v, 0).add(Curve.getTangent(v, 1)),
|
|
tx = tan.x,
|
|
ty = tan.y,
|
|
abs = Math.abs,
|
|
flip = abs(ty) < abs(tx),
|
|
s = flip ? ty / tx : tx / ty,
|
|
ia = flip ? 1 : 0, // the abscissa index
|
|
io = ia ^ 1, // the ordinate index
|
|
a0 = v[ia + 0], o0 = v[io + 0],
|
|
a1 = v[ia + 2], o1 = v[io + 2],
|
|
a2 = v[ia + 4], o2 = v[io + 4],
|
|
a3 = v[ia + 6], o3 = v[io + 6],
|
|
aA = -a0 + 3 * a1 - 3 * a2 + a3,
|
|
aB = 3 * a0 - 6 * a1 + 3 * a2,
|
|
aC = -3 * a0 + 3 * a1,
|
|
oA = -o0 + 3 * o1 - 3 * o2 + o3,
|
|
oB = 3 * o0 - 6 * o1 + 3 * o2,
|
|
oC = -3 * o0 + 3 * o1,
|
|
roots = [],
|
|
epsilon = Numerical.CURVETIME_EPSILON,
|
|
count = Numerical.solveQuadratic(
|
|
3 * (aA - s * oA),
|
|
2 * (aB - s * oB),
|
|
aC - s * oC, roots,
|
|
epsilon, 1 - epsilon);
|
|
// Fall back to 0.5, so we always have a place to split...
|
|
return count > 0 ? roots[0] : 0.5;
|
|
},
|
|
|
|
addRoundJoin: function(path, dest, center, radius) {
|
|
// return path.lineTo(dest);
|
|
var middle = path.lastSegment.point.add(dest).divide(2),
|
|
through = center.add(middle.subtract(center).normalize(radius));
|
|
path.arcTo(through, dest);
|
|
},
|
|
}; |