github.com/Kintar/etxt@v0.0.0-20221224033739-2fc69f000137/emask/helper_curve_segmenter.go (about) 1 package emask 2 3 // A small struct to handle Bézier curve segmentation into straight 4 // lines. It has a configurable curveThreshold and a maxCurveSplits 5 // limit. Used by edge_marker and outliner. 6 type curveSegmenter struct { 7 curveThreshold float32 // threshold to decide if a segment approximates 8 // a bézier curve well enough or we should split 9 maxCurveSplits uint8 // a cutoff for curve segmentation 10 } 11 12 // Sets the threshold distance to use when splitting Bézier curves into 13 // linear segments. If a linear segment misses the curve by more than 14 // the threshold value, the curve will be split. Otherwise, the linear 15 // segment will be used to approximate it. 16 // 17 // Values very close to zero could prevent algorithms from converging 18 // due to floating point instability, but MaxCurveSplits will still 19 // prevent infinite loops. 20 func (self *curveSegmenter) SetThreshold(dist float32) { 21 self.curveThreshold = dist 22 } 23 24 // Sets the maximum amount of times a curve can be recursively split 25 // into subsegments while trying to approximate it with TraceQuad or 26 // TraceCube. 27 // 28 // The maximum number of segments that will approximate a curve is 29 // 2^maxCurveSplits. 30 // 31 // This value is typically used as a cutoff to prevent low curve thresholds 32 // from making the curve splitting process too slow, but it can also be used 33 // creatively to get jaggy results instead of smooth curves. 34 // 35 // Values outside the [0, 255] range will be silently clamped. 36 func (self *curveSegmenter) SetMaxSplits(maxCurveSplits int) { 37 if maxCurveSplits < 0 { 38 self.maxCurveSplits = 0 39 } else if maxCurveSplits > 255 { 40 self.maxCurveSplits = 255 41 } else { 42 self.maxCurveSplits = uint8(maxCurveSplits) 43 } 44 } 45 46 type traceFunc = func(x, y float64) // called for each segment during curve segmentation 47 func (self *curveSegmenter) TraceQuad(lineTo traceFunc, x, y, ctrlX, ctrlY, fx, fy float64) { 48 self.recursiveTraceQuad(lineTo, x, y, ctrlX, ctrlY, fx, fy, 0) 49 } 50 51 func (self *curveSegmenter) recursiveTraceQuad(lineTo traceFunc, x, y, ctrlX, ctrlY, fx, fy float64, depth uint8) (float64, float64) { 52 if depth >= self.maxCurveSplits || self.withinThreshold(x, y, fx, fy, ctrlX, ctrlY) { 53 lineTo(fx, fy) 54 return fx, fy 55 } 56 57 ocx, ocy := lerp(x, y, ctrlX, ctrlY, 0.5) // origin to control 58 cfx, cfy := lerp(ctrlX, ctrlY, fx, fy, 0.5) // control to end 59 ix, iy := lerp(ocx, ocy, cfx, cfy, 0.5) // interpolated point 60 x, y = self.recursiveTraceQuad(lineTo, x, y, ocx, ocy, ix, iy, depth+1) 61 return self.recursiveTraceQuad(lineTo, x, y, cfx, cfy, fx, fy, depth+1) 62 } 63 64 func (self *curveSegmenter) TraceCube(lineTo traceFunc, x, y, cx1, cy1, cx2, cy2, fx, fy float64) { 65 self.recursiveTraceCube(lineTo, x, y, cx1, cy1, cx2, cy2, fx, fy, 0) 66 } 67 68 func (self *curveSegmenter) recursiveTraceCube(lineTo traceFunc, x, y, cx1, cy1, cx2, cy2, fx, fy float64, depth uint8) (float64, float64) { 69 if depth >= self.maxCurveSplits || (self.withinThreshold(x, y, cx2, cy2, cx1, cy1) && self.withinThreshold(cx1, cy1, fx, fy, cx2, cy2)) { 70 lineTo(fx, fy) 71 return fx, fy 72 } 73 74 oc1x, oc1y := lerp(x, y, cx1, cy1, 0.5) // origin to control 1 75 c1c2x, c1c2y := lerp(cx1, cy1, cx2, cy2, 0.5) // control 1 to control 2 76 c2fx, c2fy := lerp(cx2, cy2, fx, fy, 0.5) // control 2 to end 77 iox, ioy := lerp(oc1x, oc1y, c1c2x, c1c2y, 0.5) // first interpolation from origin 78 ifx, ify := lerp(c1c2x, c1c2y, c2fx, c2fy, 0.5) // second interpolation to end 79 ix, iy := lerp(iox, ioy, ifx, ify, 0.5) // cubic interpolation 80 x, y = self.recursiveTraceCube(lineTo, x, y, oc1x, oc1y, iox, ioy, ix, iy, depth+1) 81 return self.recursiveTraceCube(lineTo, x, y, ifx, ify, c2fx, c2fy, fx, fy, depth+1) 82 } 83 84 func (self *curveSegmenter) withinThreshold(ox, oy, fx, fy, px, py float64) bool { 85 // https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation 86 // dist = |a*x + b*y + c| / sqrt(a^2 + b^2) 87 a, b, c := toLinearFormABC(ox, oy, fx, fy) 88 n := a*px + b*py + c 89 return n*n <= float64(self.curveThreshold)*float64(self.curveThreshold)*(a*a+b*b) 90 }