Implement correct CurveLocation#isCrossing() check.

And improve curve caching. Still needs work.
This commit is contained in:
Jürg Lehni 2015-09-15 19:38:28 +02:00
parent 3ce7d88347
commit 60a725b527

View file

@ -57,19 +57,24 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{
// since this is only required to be unique at runtime among other // since this is only required to be unique at runtime among other
// CurveLocation objects. // CurveLocation objects.
this._id = UID.get(CurveLocation); this._id = UID.get(CurveLocation);
var path = curve._path; this._setCurve(curve);
this._version = path ? path._version : 0;
this._curve = curve;
this._parameter = parameter; this._parameter = parameter;
this._point = point || curve.getPointAt(parameter, true); this._point = point || curve.getPointAt(parameter, true);
this._distance = _distance; this._distance = _distance;
this._overlap = _overlap; this._overlap = _overlap;
this._crossing = null;
this._intersection = _intersection; this._intersection = _intersection;
if (_intersection) { if (_intersection) {
_intersection._intersection = this; _intersection._intersection = this;
// TODO: Remove this once debug logging is removed. // TODO: Remove this once debug logging is removed.
_intersection._other = true; _intersection._other = true;
} }
},
_setCurve: function(curve) {
var path = curve._path;
this._version = path ? path._version : 0;
this._curve = curve;
this._segment = null; // To be determined, see #getSegment() this._segment = null; // To be determined, see #getSegment()
// Also store references to segment1 and segment2, in case path // Also store references to segment1 and segment2, in case path
// splitting / dividing is going to happen, in which case the segments // splitting / dividing is going to happen, in which case the segments
@ -84,25 +89,26 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{
* @type Segment * @type Segment
* @bean * @bean
*/ */
getSegment: function(_preferFirst) { getSegment: function() {
if (!this._segment) { // Request curve first, so _segment gets invalidated if it's out of sync
var curve = this.getCurve(), var curve = this.getCurve(),
parameter = this.getParameter(); segment = this._segment;
if (parameter === 1) { if (!segment) {
this._segment = curve._segment2; var parameter = this.getParameter();
} else if (parameter === 0 || _preferFirst) { if (parameter === 0) {
this._segment = curve._segment1; segment = curve._segment1;
} else if (parameter == null) { } else if (parameter === 1) {
return null; segment = curve._segment2;
} else { } else if (parameter != null) {
// Determine the closest segment by comparing curve lengths // Determine the closest segment by comparing curve lengths
this._segment = curve.getPartLength(0, parameter) segment = curve.getPartLength(0, parameter)
< curve.getPartLength(parameter, 1) < curve.getPartLength(parameter, 1)
? curve._segment1 ? curve._segment1
: curve._segment2; : curve._segment2;
} }
this._segment = segment;
} }
return this._segment; return segment;
}, },
/** /**
@ -113,29 +119,34 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{
*/ */
getCurve: function() { getCurve: function() {
var curve = this._curve, var curve = this._curve,
path = curve && curve._path; path = curve && curve._path,
that = this;
if (path && path._version !== this._version) { if (path && path._version !== this._version) {
// If the path's segments have changed in the meantime, clear the // If the path's segments have changed in the meantime, clear the
// internal _parameter value and force refetching of the correct // internal _parameter value and force refetching of the correct
// curve again here. // curve again here.
curve = null; curve = this._parameter = this._curve = null;
this._parameter = null;
} }
if (!curve) {
// If we're asked to get the curve uncached, access current curve // If path is out of sync, access current curve objects through segment1
// objects through segment1 / segment2. Since path splitting or // / segment2. Since path splitting or dividing might have happened in
// dividing might have happened in the meantime, try segment1's // the meantime, try segment1's curve, and see if _point lies on it
// curve, and see if _point lies on it still, otherwise assume it's // still, otherwise assume it's the curve before segment2.
// the curve before segment2. function trySegment(segment) {
curve = this._segment1.getCurve(); var curve = segment && segment.getCurve();
if (curve.getParameterOf(this._point) == null) if (curve && (that._parameter = curve.getParameterOf(that._point))
curve = this._segment2.getPrevious().getCurve(); != null) {
this._curve = curve;
// Fetch path again as it could be on a new one through split() // Fetch path again as it could be on a new one through split()
path = curve._path; that._setCurve(curve);
this._version = path ? path._version : 0; that._segment = segment;
}
return curve; return curve;
}
}
return curve
|| trySegment(this._segment)
|| trySegment(this._segment1)
|| trySegment(this._segment2.getPrevious());
}, },
/** /**
@ -271,6 +282,70 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{
return curve && curve.split(this.getParameter(), true); return curve && curve.split(this.getParameter(), true);
}, },
isCrossing: function(report) {
// Implementation based on work by Andy Finnell:
// http://losingfight.com/blog/2011/07/09/how-to-implement-boolean-operations-on-bezier-paths-part-3/
// https://bitbucket.org/andyfinnell/vectorboolean
var intersection = this._intersection,
crossing = this._crossing;
if (crossing != null || !intersection)
return crossing || false;
// TODO: isTangent() ?
// TODO: isAtEndPoint() ?
// -> Return if it's a tangent, or if not at an end point, only end
// point intersections need more checking!
var tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin,
PI = Math.PI,
PI_2 = PI * 2,
// TODO: Make getCurve() sync work in boolean ops after splitting!!!
c2 = this._curve,
c1 = c2.getPrevious(),
c4 = intersection._curve,
c3 = c4.getPrevious();
if (!c1 || !c3)
return this._crossing = false;
if (report) {
new Path.Circle({
center: this.getPoint(),
radius: 10,
strokeColor: 'red'
});
new Path({
segments: [c1.getSegment1(), c1.getSegment2(), c2.getSegment2()],
strokeColor: 'red'
});
new Path({
segments: [c3.getSegment1(), c3.getSegment2(), c4.getSegment2()],
strokeColor: 'orange'
});
console.log(c1.getValues(), c2.getValues(), c3.getValues(), c4.getValues());
}
function getAngle(tangent) {
var a = tangent.getAngleInRadians();
return a < -PI ? a + PI_2 : a >= PI ? a - PI_2 : a;
}
function isInRange(angle, min, max) {
return min < max
? angle > min && angle < max
// The range wraps around 0:
: angle > min && angle <= PI || angle >= -PI && angle < max;
}
// Calculate angles for all four tangents at the intersection point
var a1 = getAngle(c1.getTangentAt(tMax, true).negate()),
a2 = getAngle(c2.getTangentAt(tMin, true)),
a3 = getAngle(c3.getTangentAt(tMax, true).negate()),
a4 = getAngle(c4.getTangentAt(tMin, true));
// Count how many times curve2 angles appear between the curve1 angles
// If each pair of angles split the other two, then the edges cross.
return (isInRange(a3, a1, a2) ^ isInRange(a4, a1, a2))
&& (isInRange(a3, a2, a1) ^ isInRange(a4, a2, a1));
},
/** /**
* Checks whether tow CurveLocation objects are describing the same location * Checks whether tow CurveLocation objects are describing the same location
* on a path, by applying the same tolerances as elsewhere when dealing with * on a path, by applying the same tolerances as elsewhere when dealing with