From f4eafa288a0bfb88b68cb97434219c697a07c803 Mon Sep 17 00:00:00 2001 From: Pomax Date: Sat, 29 Aug 2020 12:49:23 -0700 Subject: [PATCH] curve/curve intersection --- .../curveintersection/content.en-GB.md | 28 ++-- .../chapters/curveintersection/curve-curve.js | 134 ++++++++++++++++++ docs/chapters/curveintersection/handler.js | 117 --------------- .../a71619a14589851390cf88aa07042d3e.png | Bin 0 -> 30364 bytes docs/index.html | 25 ++-- docs/ja-JP/index.html | 25 ++-- docs/js/custom-element/api/graphics-api.js | 26 +++- docs/js/custom-element/api/types/bezier.js | 14 ++ docs/js/custom-element/lib/bezierjs/utils.js | 4 +- docs/zh-CN/index.html | 25 ++-- 10 files changed, 235 insertions(+), 163 deletions(-) create mode 100644 docs/chapters/curveintersection/curve-curve.js delete mode 100644 docs/chapters/curveintersection/handler.js create mode 100644 docs/images/chapters/curveintersection/a71619a14589851390cf88aa07042d3e.png diff --git a/docs/chapters/curveintersection/content.en-GB.md b/docs/chapters/curveintersection/content.en-GB.md index d176e58f..20902dd4 100644 --- a/docs/chapters/curveintersection/content.en-GB.md +++ b/docs/chapters/curveintersection/content.en-GB.md @@ -2,20 +2,24 @@ Using de Casteljau's algorithm to split the curve we can now implement curve/curve intersection finding using a "divide and conquer" technique: -- Take two curves *C1* and *C2*, and treat them as a pair. -- If their bounding boxes overlap, split up each curve into two sub-curves -- With *C1.1*, *C1.2*, *C2.1* and *C2.2*, form four new pairs (*C1.1*,*C2.1*), (*C1.1*, *C2.2*), (*C1.2*,*C2.1*), and (*C1.2*,*C2.2*). -- For each pair, check whether their bounding boxes overlap. - - If their bounding boxes do not overlap, discard the pair, as there is no intersection between this pair of curves. - - If there is overlap, rerun all steps for this pair. -- Once the sub-curves we form are so small that they effectively occupy sub-pixel areas, we consider an intersection found, noting that we might have a cluster of multiple intersections at the sub-pixel level, out of which we pick one to act as "found" `t` value (we can either throw all but one away, we can average the cluster's `t` values, or you can do something even more creative). +1. Take two curves *C1* and *C2*, and treat them as a pair. +2. If their bounding boxes overlap, split up each curve into two sub-curves +3. With *C1.1*, *C1.2*, *C2.1* and *C2.2*, form four new pairs (*C1.1*,*C2.1*), (*C1.1*, *C2.2*), (*C1.2*,*C2.1*), and (*C1.2*,*C2.2*). +4. For each pair, check whether their bounding boxes overlap. + 1. If their bounding boxes do not overlap, discard the pair, as there is no intersection between this pair of curves. + 2. If there is overlap, rerun all steps for this pair. +5. Once the sub-curves we form are so small that they effectively occupy sub-pixel areas, we consider an intersection found, noting that we might have a cluster of multiple intersections at the sub-pixel level, out of which we pick one to act as "found" `t` value (we can either throw all but one away, we can average the cluster's `t` values, or you can do something even more creative). This algorithm will start with a single pair, "balloon" until it runs in parallel for a large number of potential sub-pairs, and then taper back down as it homes in on intersection coordinates, ending up with as many pairs as there are intersections. -The following graphic applies this algorithm to a pair of cubic curves, one step at a time, so you can see the algorithm in action. Click the button to run a single step in the algorithm, after setting up your curves in some creative arrangement. The algorithm resets once it's found a solution, so you can try this with lots of different curves (can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!) +The following graphic applies this algorithm to a pair of cubic curves, one step at a time, so you can see the algorithm in action. Click the button to run a single step in the algorithm, after setting up your curves in some creative arrangement. You can also change the value that is used in step 5 to determine whether the curves are small enough. Manipulating the curves or changing the threshold will reset the algorithm, so you can try this with lots of different curves. - - - +(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!) -Self-intersection is dealt with in the same way, except we turn a curve into two or more curves first based on the inflection points. We then form all possible curve pairs with the resultant segments, and run exactly the same algorithm. All non-overlapping curve pairs will be removed after the first iteration, and the remaining steps home in on the curve's self-intersection points. + + + + + + +Finding self-intersections is effectively the same procedure, except that we're starting with a single curve, so we need to turn that into two separate curves first. This is trivially achieved by splitting at an inflection point, or if there are none, just splitting at `t=0.5` first, and then running the exact same algorithm as above, with all non-overlapping curve pairs getting removed at each iteration, and each successive step homing in on the curve's self-intersection points. diff --git a/docs/chapters/curveintersection/curve-curve.js b/docs/chapters/curveintersection/curve-curve.js new file mode 100644 index 00000000..42f3f73f --- /dev/null +++ b/docs/chapters/curveintersection/curve-curve.js @@ -0,0 +1,134 @@ +let curves, curve1, curve2, next; + +setup() { + setPanelCount(3); + this.pairReset(); + this.setupEventListening(); + setSlider(`.slide-control`, `epsilon`, 1.0); +} + +pairReset() { + curve1 = new Bezier(this, 50,35, 45,235, 220,235, 220,135); + curve2 = new Bezier(this, 20,150, 120,20, 220,95, 140,240); + curves = [curve1, curve2]; + resetMovable(curve1.points, curve2.points); + this.reset(); +} + +reset() { + if (next && next.disabled) next.disabled = false; + this.pairs = [[curve1, curve2]]; + this.finals = []; + this.step = 0; +} + +setupEventListening() { + next = find(`.next`); + if (next) next.listen([`click`,`touchstart`], evt => { + this.step++; + redraw(); + }); + + let reset = find(`.reset`); + if (reset) reset.listen([`click`,`touchstart`], evt => { + this.pairReset(); + redraw(); + }); +} + +draw() { + resetTransform(); + clear(); + + // panel 1: base curves + curves.forEach(c => { + c.drawSkeleton(); + c.drawCurve(); + c.drawPoints(); + }); + + // panel 2: the current iteration step + nextPanel(); + setStroke(`black`); + line(0,0,0,this.height); + this.drawIteration(); + setFill(`black`); + let information = `Initial curves, threshold = ${this.epsilon}px` + if (this.step) { + information = `Curve collection at iteration ${this.step}`; + } + text(information, this.panelWidth/2, 15, CENTER); + + if (this.finals.length) { + text(`${this.finals.length} intersections found.`, this.panelWidth/2, this.height - 10, CENTER); + } + + // panel 3: intersections + nextPanel(); + setStroke(`black`); + line(0,0,0,this.height); + this.drawIntersections(); +} + +drawIteration() { + if (this.step > 0) { + const pairs = this.pairs; + this.pairs = []; + pairs.forEach(pair => { + if(pair[0].length() < this.epsilon && pair[1].length() < this.epsilon) + return this.finals.push(pair); + + // split two curves into four curves + const s1 = pair[0].split(0.5); + const s2 = pair[1].split(0.5); + + // cross check + if (s1.left.overlaps(s2.left)) { this.pairs.push([ s1.left, s2.left ]); } + if (s1.left.overlaps(s2.right)) { this.pairs.push([ s1.left, s2.right ]); } + if (s1.right.overlaps(s2.left)) { this.pairs.push([ s1.right, s2.left ]); } + if (s1.right.overlaps(s2.right)) { this.pairs.push([ s1.right, s2.right ]); } + }); + } + + if (!this.pairs.length && next) { + next.disabled = true; + } + + this.pairs.forEach(pair => { + pair.forEach(b => { + let curve = new Bezier(this, b.points); + curve.drawCurve(); + curve.drawBoundingBox( randomColor() ); + }) + }); + + setStroke(`red`); + this.finals.forEach(pair => { + let p = pair[0].get(0.5); + circle(p.x, p.y, 3); + }) +} + +drawIntersections() { + curve1.drawCurve(`lightblue`); + curve2.drawCurve(`lightblue`); + noFill(); + setStroke(`blue`); + let intersections = curve1.intersects(curve2); + intersections = intersections.map(v => v.split(`/`).map(t => parseFloat(t))); + intersections.forEach(p => { + p = curve1.get(p[0]); + circle(p.x, p.y, 3); + }); +} + +onMouseMove() { + if (this.currentPoint && this.step !== 0) { + this.reset(); + redraw(); + } +} + +onEpsilon(value) { + this.reset(); +} diff --git a/docs/chapters/curveintersection/handler.js b/docs/chapters/curveintersection/handler.js deleted file mode 100644 index 041db673..00000000 --- a/docs/chapters/curveintersection/handler.js +++ /dev/null @@ -1,117 +0,0 @@ -var abs = Math.abs; - -module.exports = { - setup: function(api) { - this.api = api; - api.setPanelCount(3); - var curve1 = new api.Bezier(10,100,90,30,40,140,220,220); - var curve2 = new api.Bezier(5,150,180,20,80,250,210,190); - api.setCurve(curve1, curve2); - this.pairReset(); - }, - - pairReset: function() { - this.prevstep = 0; - this.step = 0; - }, - - draw: function(api, curves) { - api.reset(); - var offset = {x:0, y:0}; - curves.forEach(curve => { - api.drawSkeleton(curve); - api.drawCurve(curve); - }); - - // next panel: iterations - var w = api.getPanelWidth(); - var h = api.getPanelHeight(); - offset.x += w; - api.drawLine({x:0,y:0}, {x:0,y:h}, offset); - - if (this.step === 0) { - this.pairs = [{c1: curves[0], c2: curves[1]}]; - } - - if(this.step !== this.prevstep) { - var pairs = this.pairs; - this.pairs = []; - this.finals = []; - pairs.forEach(p => { - - if(p.c1.length() < 0.6 && p.c2.length() < 0.6) { - return this.finals.push(p); - } - - var s1 = p.c1.split(0.5); - api.setColor("black"); - api.drawCurve(p.c1, offset); - api.setColor("red"); - api.drawbbox(s1.left.bbox(), offset); - api.drawbbox(s1.right.bbox(), offset); - - var s2 = p.c2.split(0.5); - api.setColor("black"); - api.drawCurve(p.c2, offset); - api.setColor("blue"); - api.drawbbox(s2.left.bbox(), offset); - api.drawbbox(s2.right.bbox(), offset); - - if (s1.left.overlaps(s2.left)) { this.pairs.push({c1: s1.left, c2: s2.left}); } - if (s1.left.overlaps(s2.right)) { this.pairs.push({c1: s1.left, c2: s2.right}); } - if (s1.right.overlaps(s2.left)) { this.pairs.push({c1: s1.right, c2: s2.left}); } - if (s1.right.overlaps(s2.right)) { this.pairs.push({c1: s1.right, c2: s2.right}); } - }); - this.prevstep = this.step; - } else { - this.pairs.forEach(p => { - api.setColor("black"); - api.drawCurve(p.c1, offset); - api.drawCurve(p.c2, offset); - api.setColor("red"); - api.drawbbox(p.c1.bbox(), offset); - api.setColor("blue"); - api.drawbbox(p.c2.bbox(), offset); - }); - } - - if (this.pairs.length === 0) { - this.pairReset(); - this.draw(api, curves); - } - - // next panel: results - offset.x += w; - api.setColor("black"); - api.drawLine({x:0,y:0}, {x:0,y:h}, offset); - - // get intersections as coordinates - var results = curves[0].intersects(curves[1]).map(s => { - var tvals = s.split('/').map(v => parseFloat(v)); - return {t1: tvals[0], t2: tvals[1]}; - }); - - // filter out likely duplicates - var curr = results[0], _, i, same = ((a,b) => abs(a.t1-b.t1) < 0.01 && abs(a.t2-b.t2) < 0.01); - for(i=1; i { - api.drawCircle(curves[0].get(tvals.t1), 3, offset); - }); - }, - - stepUp: function() { - this.step++; - this.api.redraw(); - } -}; diff --git a/docs/images/chapters/curveintersection/a71619a14589851390cf88aa07042d3e.png b/docs/images/chapters/curveintersection/a71619a14589851390cf88aa07042d3e.png new file mode 100644 index 0000000000000000000000000000000000000000..48499e5e3ed5431c9b3174ef20e6da41b90a735d GIT binary patch literal 30364 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 literal 0 HcmV?d00001 diff --git a/docs/index.html b/docs/index.html index 5d0779d9..e38f36f2 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1524,24 +1524,31 @@ lli = function(line1, line2):

Curve/curve intersection

Using de Casteljau's algorithm to split the curve we can now implement curve/curve intersection finding using a "divide and conquer" technique:

-
    +
    1. Take two curves C1 and C2, and treat them as a pair.
    2. If their bounding boxes overlap, split up each curve into two sub-curves
    3. With C1.1, C1.2, C2.1 and C2.2, form four new pairs (C1.1,C2.1), (C1.1, C2.2), (C1.2,C2.1), and (C1.2,C2.2).
    4. -
    5. For each pair, check whether their bounding boxes overlap.
        +
      • For each pair, check whether their bounding boxes overlap.
        1. If their bounding boxes do not overlap, discard the pair, as there is no intersection between this pair of curves.
        2. If there is overlap, rerun all steps for this pair.
        3. -
      +
  • Once the sub-curves we form are so small that they effectively occupy sub-pixel areas, we consider an intersection found, noting that we might have a cluster of multiple intersections at the sub-pixel level, out of which we pick one to act as "found" t value (we can either throw all but one away, we can average the cluster's t values, or you can do something even more creative).
  • -
+

This algorithm will start with a single pair, "balloon" until it runs in parallel for a large number of potential sub-pairs, and then taper back down as it homes in on intersection coordinates, ending up with as many pairs as there are intersections.

-

The following graphic applies this algorithm to a pair of cubic curves, one step at a time, so you can see the algorithm in action. Click the button to run a single step in the algorithm, after setting up your curves in some creative arrangement. The algorithm resets once it's found a solution, so you can try this with lots of different curves (can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

- - - +

The following graphic applies this algorithm to a pair of cubic curves, one step at a time, so you can see the algorithm in action. Click the button to run a single step in the algorithm, after setting up your curves in some creative arrangement. You can also change the value that is used in step 5 to determine whether the curves are small enough. Manipulating the curves or changing the threshold will reset the algorithm, so you can try this with lots of different curves.

+

(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

+ + + + Scripts are disabled. Showing fallback image. + + + + + -

Self-intersection is dealt with in the same way, except we turn a curve into two or more curves first based on the inflection points. We then form all possible curve pairs with the resultant segments, and run exactly the same algorithm. All non-overlapping curve pairs will be removed after the first iteration, and the remaining steps home in on the curve's self-intersection points.

+

Finding self-intersections is effectively the same procedure, except that we're starting with a single curve, so we need to turn that into two separate curves first. This is trivially achieved by splitting at an inflection point, or if there are none, just splitting at t=0.5 first, and then running the exact same algorithm as above, with all non-overlapping curve pairs getting removed at each iteration, and each successive step homing in on the curve's self-intersection points.

diff --git a/docs/ja-JP/index.html b/docs/ja-JP/index.html index 802f7d61..f36b056b 100644 --- a/docs/ja-JP/index.html +++ b/docs/ja-JP/index.html @@ -1521,24 +1521,31 @@ lli = function(line1, line2):

Curve/curve intersection

Using de Casteljau's algorithm to split the curve we can now implement curve/curve intersection finding using a "divide and conquer" technique:

-
    +
    1. Take two curves C1 and C2, and treat them as a pair.
    2. If their bounding boxes overlap, split up each curve into two sub-curves
    3. With C1.1, C1.2, C2.1 and C2.2, form four new pairs (C1.1,C2.1), (C1.1, C2.2), (C1.2,C2.1), and (C1.2,C2.2).
    4. -
    5. For each pair, check whether their bounding boxes overlap.
        +
      • For each pair, check whether their bounding boxes overlap.
        1. If their bounding boxes do not overlap, discard the pair, as there is no intersection between this pair of curves.
        2. If there is overlap, rerun all steps for this pair.
        3. -
      +
  • Once the sub-curves we form are so small that they effectively occupy sub-pixel areas, we consider an intersection found, noting that we might have a cluster of multiple intersections at the sub-pixel level, out of which we pick one to act as "found" t value (we can either throw all but one away, we can average the cluster's t values, or you can do something even more creative).
  • -
+

This algorithm will start with a single pair, "balloon" until it runs in parallel for a large number of potential sub-pairs, and then taper back down as it homes in on intersection coordinates, ending up with as many pairs as there are intersections.

-

The following graphic applies this algorithm to a pair of cubic curves, one step at a time, so you can see the algorithm in action. Click the button to run a single step in the algorithm, after setting up your curves in some creative arrangement. The algorithm resets once it's found a solution, so you can try this with lots of different curves (can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

- - - +

The following graphic applies this algorithm to a pair of cubic curves, one step at a time, so you can see the algorithm in action. Click the button to run a single step in the algorithm, after setting up your curves in some creative arrangement. You can also change the value that is used in step 5 to determine whether the curves are small enough. Manipulating the curves or changing the threshold will reset the algorithm, so you can try this with lots of different curves.

+

(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

+ + + + Scripts are disabled. Showing fallback image. + + + + + -

Self-intersection is dealt with in the same way, except we turn a curve into two or more curves first based on the inflection points. We then form all possible curve pairs with the resultant segments, and run exactly the same algorithm. All non-overlapping curve pairs will be removed after the first iteration, and the remaining steps home in on the curve's self-intersection points.

+

Finding self-intersections is effectively the same procedure, except that we're starting with a single curve, so we need to turn that into two separate curves first. This is trivially achieved by splitting at an inflection point, or if there are none, just splitting at t=0.5 first, and then running the exact same algorithm as above, with all non-overlapping curve pairs getting removed at each iteration, and each successive step homing in on the curve's self-intersection points.

diff --git a/docs/js/custom-element/api/graphics-api.js b/docs/js/custom-element/api/graphics-api.js index 55ea7b39..9f615b13 100644 --- a/docs/js/custom-element/api/graphics-api.js +++ b/docs/js/custom-element/api/graphics-api.js @@ -123,15 +123,29 @@ class GraphicsAPI extends BaseAPI { this.currentPoint = false; } - resetMovable(points) { + resetMovable(...allpoints) { this.movable.splice(0, this.movable.length); - if (points) this.setMovable(points); + if (allpoints) this.setMovable(...allpoints); } setMovable(...allpoints) { allpoints.forEach((points) => points.forEach((p) => this.movable.push(p))); } + /** + * Multi-panel graphics: set panel count + */ + setPanelCount(c) { + this.panelWidth = this.width / c; + } + + /** + * Multi-panel graphics: set up (0,0) to the next panel's start + */ + nextPanel(c) { + this.translate(this.panelWidth, 0); + } + /** * Set up a slider to control a named, numerical property in the sketch. * @@ -156,11 +170,13 @@ class GraphicsAPI extends BaseAPI { slider.value = initial; this[propname] = parseFloat(slider.value); + let handlerName = `on${propname[0].toUpperCase()}${propname + .substring(1) + .toLowerCase()}`; slider.listen(`input`, (evt) => { this[propname] = parseFloat(evt.target.value); - if (redraw && !this.redrawing) { - this.redraw(); - } + if (this[handlerName]) this[handlerName](this[propname]); + if (redraw && !this.redrawing) this.redraw(); }); return slider; diff --git a/docs/js/custom-element/api/types/bezier.js b/docs/js/custom-element/api/types/bezier.js index 4113a38b..a5e0a850 100644 --- a/docs/js/custom-element/api/types/bezier.js +++ b/docs/js/custom-element/api/types/bezier.js @@ -177,6 +177,20 @@ class Bezier extends Original { } ctx.restoreStyle(); } + + drawBoundingBox(color) { + let bbox = this.bbox(), + mx = bbox.x.min, + my = bbox.y.min, + MX = bbox.x.max, + MY = bbox.y.max, + api = this.api; + api.cacheStyle(); + api.noFill(); + api.setStroke(color ? color : `black`); + api.rect(mx, my, MX - mx, MY - my); + api.restoreStyle(); + } } export { Bezier }; diff --git a/docs/js/custom-element/lib/bezierjs/utils.js b/docs/js/custom-element/lib/bezierjs/utils.js index 8dc876c8..6cbbb35a 100644 --- a/docs/js/custom-element/lib/bezierjs/utils.js +++ b/docs/js/custom-element/lib/bezierjs/utils.js @@ -805,7 +805,7 @@ const utils = { ]; } - const cc1 = c1.split(0.5), + let cc1 = c1.split(0.5), cc2 = c2.split(0.5), pairs = [ { left: cc1.left, right: cc2.left }, @@ -818,7 +818,7 @@ const utils = { return utils.bboxoverlap(pair.left.bbox(), pair.right.bbox()); }); - const results = []; + let results = []; if (pairs.length === 0) return results; diff --git a/docs/zh-CN/index.html b/docs/zh-CN/index.html index aaf9ff09..a3f200f5 100644 --- a/docs/zh-CN/index.html +++ b/docs/zh-CN/index.html @@ -1515,24 +1515,31 @@ lli = function(line1, line2):

Curve/curve intersection

Using de Casteljau's algorithm to split the curve we can now implement curve/curve intersection finding using a "divide and conquer" technique:

-
    +
    1. Take two curves C1 and C2, and treat them as a pair.
    2. If their bounding boxes overlap, split up each curve into two sub-curves
    3. With C1.1, C1.2, C2.1 and C2.2, form four new pairs (C1.1,C2.1), (C1.1, C2.2), (C1.2,C2.1), and (C1.2,C2.2).
    4. -
    5. For each pair, check whether their bounding boxes overlap.
        +
      • For each pair, check whether their bounding boxes overlap.
        1. If their bounding boxes do not overlap, discard the pair, as there is no intersection between this pair of curves.
        2. If there is overlap, rerun all steps for this pair.
        3. -
      +
  • Once the sub-curves we form are so small that they effectively occupy sub-pixel areas, we consider an intersection found, noting that we might have a cluster of multiple intersections at the sub-pixel level, out of which we pick one to act as "found" t value (we can either throw all but one away, we can average the cluster's t values, or you can do something even more creative).
  • -
+

This algorithm will start with a single pair, "balloon" until it runs in parallel for a large number of potential sub-pairs, and then taper back down as it homes in on intersection coordinates, ending up with as many pairs as there are intersections.

-

The following graphic applies this algorithm to a pair of cubic curves, one step at a time, so you can see the algorithm in action. Click the button to run a single step in the algorithm, after setting up your curves in some creative arrangement. The algorithm resets once it's found a solution, so you can try this with lots of different curves (can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

- - - +

The following graphic applies this algorithm to a pair of cubic curves, one step at a time, so you can see the algorithm in action. Click the button to run a single step in the algorithm, after setting up your curves in some creative arrangement. You can also change the value that is used in step 5 to determine whether the curves are small enough. Manipulating the curves or changing the threshold will reset the algorithm, so you can try this with lots of different curves.

+

(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

+ + + + Scripts are disabled. Showing fallback image. + + + + + -

Self-intersection is dealt with in the same way, except we turn a curve into two or more curves first based on the inflection points. We then form all possible curve pairs with the resultant segments, and run exactly the same algorithm. All non-overlapping curve pairs will be removed after the first iteration, and the remaining steps home in on the curve's self-intersection points.

+

Finding self-intersections is effectively the same procedure, except that we're starting with a single curve, so we need to turn that into two separate curves first. This is trivially achieved by splitting at an inflection point, or if there are none, just splitting at t=0.5 first, and then running the exact same algorithm as above, with all non-overlapping curve pairs getting removed at each iteration, and each successive step homing in on the curve's self-intersection points.