github.com/kintar/etxt@v0.0.9/emask/outliner.go (about)

     1  package emask
     2  
     3  import "math"
     4  
     5  type outliner struct {
     6  	x              float64
     7  	y              float64
     8  	thickness      float64 // can't be modified throughout an outline
     9  	marginFactor   uint8
    10  	Buffer         buffer
    11  	CurveSegmenter curveSegmenter
    12  
    13  	segments         [5]outlineSegment // the 0 and 1 are kept for closing
    14  	openSegmentCount int
    15  }
    16  
    17  // Sets the thickness of the outliner. Thickness can only be
    18  // modified while not drawing. This means it can only be changed
    19  // after MoveTo, ClosePath, CutPath or after initialization but
    20  // before any LineTo, QuadTo or CubeTo commands are issued.
    21  //
    22  // This method will panic if any of the previous conditions are
    23  // violated or if the passed thickness is zero, negative or bigger
    24  // than 1024.
    25  //
    26  // The thickness will be quantized to a multiple of 1/1024 and
    27  // the quantized value will be returned.
    28  func (self *outliner) SetThickness(thickness float64) float64 {
    29  	if self.openSegmentCount > 0 {
    30  		panic("can't change thickness while drawing")
    31  	}
    32  	if thickness <= 0 {
    33  		panic("thickness <= 0 not allowed")
    34  	}
    35  	if thickness > 1024 {
    36  		panic("thickness > 1024 not allowed")
    37  	}
    38  	self.thickness = math.Round(thickness*1024) / 1024
    39  	if self.thickness == 0 {
    40  		self.thickness = 1 / 1024
    41  	}
    42  	return self.thickness
    43  }
    44  
    45  // TODO: probably needs to be quantized too, but I don't think everything
    46  //
    47  //	will fit in the uint64 anyway. say 10 bits thickness, 10 bits
    48  //	margin factor, 8 bits signature, then curve segmenter needs
    49  //	40 bits... so, 68 bits already... and thickness needs actually
    50  //	more like 20 bits. say 20 thick, 20 curve, 8 sig, 10 margin,
    51  //	8 curve splits... that's still 66 bits. margin in 8 bits would
    52  //	be impossible I think. 18 for curve and 18 for thickness may be
    53  //	possible, but really tricky. Well, I can do it in 0.5 parts, up to
    54  //	128. or 0.1 parts up to 25.6. that's not insane. to be seen if
    55  //	curve quantization in 20 bits is enough...
    56  func (self *outliner) SetMarginFactor(factor float64) {
    57  	if factor < 1.0 {
    58  		panic("outliner margin factor must be >= 1.0")
    59  	}
    60  	if factor > 16.0 {
    61  		panic("outliner margin factor must be <= 16.0")
    62  	}
    63  	self.marginFactor = uint8(math.Round((factor - 1.0) * 16))
    64  }
    65  
    66  func (self *outliner) MaxMargin() float64 {
    67  	return self.thickness * (float64(self.marginFactor) + 1.0) / 16
    68  }
    69  
    70  // Moves the current position to the given coordinates.
    71  func (self *outliner) MoveTo(x, y float64) {
    72  	if self.openSegmentCount > 0 {
    73  		self.CutPath() // cut previous path if not closed yet
    74  	}
    75  	self.x = x
    76  	self.y = y
    77  }
    78  
    79  // Creates a straight line from the current position to the given
    80  // target with the current thickness and moves the current position
    81  // to the new one.
    82  func (self *outliner) LineTo(x, y float64) {
    83  	if self.x == x && self.y == y {
    84  		return
    85  	}
    86  	defer func() { self.x, self.y = x, y }()
    87  
    88  	// compute new line ax + by + c = 0 coefficients
    89  	dx := x - self.x
    90  	dy := y - self.y
    91  	c := dx*self.y - dy*self.x
    92  	a, b, c := toLinearFormABC(self.x, self.y, x, y)
    93  
    94  	// if the new line goes in the same direction as the
    95  	// previous one, do not add it as a new line
    96  	if self.openSegmentCount > 0 {
    97  		prevSegment := &self.segments[self.openSegmentCount-1]
    98  		xdiv := prevSegment.a*b - a*prevSegment.b
    99  		if xdiv <= 0.00001 && xdiv >= -0.00001 {
   100  			prevSegment.fx = x
   101  			prevSegment.fy = y
   102  
   103  			start := self.segments[0] // check if closing outline
   104  			if start.ox == x && start.oy == y {
   105  				self.ClosePath()
   106  			}
   107  			return
   108  		}
   109  	}
   110  
   111  	// find parallels at the given distance that will delimit the new segment
   112  	c1, c2 := parallelsAtDist(a, b, c, self.thickness/2)
   113  
   114  	// create the segment
   115  	self.segments[self.openSegmentCount] = outlineSegment{
   116  		ox: self.x, oy: self.y, fx: x, fy: y,
   117  		a: a, b: b, c1: c1, c2: c2,
   118  	}
   119  	self.openSegmentCount += 1
   120  	switch self.openSegmentCount {
   121  	case 3: // fill segment 1
   122  		self.segments[1].Fill(&self.Buffer, &self.segments[0], &self.segments[2])
   123  	case 4: // fill segment 2
   124  		self.segments[2].Fill(&self.Buffer, &self.segments[1], &self.segments[3])
   125  	case 5: // fill one segment and remove another old one
   126  		self.segments[3].Fill(&self.Buffer, &self.segments[2], &self.segments[4])
   127  		self.segments[2] = self.segments[3]
   128  		self.segments[3] = self.segments[4]
   129  		self.openSegmentCount = 4
   130  	}
   131  
   132  	// see if we are closing the outline
   133  	if self.openSegmentCount > 1 {
   134  		start := self.segments[0]
   135  		if start.ox == x && start.oy == y {
   136  			self.ClosePath()
   137  		}
   138  	}
   139  }
   140  
   141  // Creates a boundary from the current position to the given target
   142  // as a quadratic Bézier curve through the given control point and
   143  // moves the current position to the new one.
   144  func (self *outliner) QuadTo(ctrlX, ctrlY, fx, fy float64) {
   145  	self.CurveSegmenter.TraceQuad(self.LineTo, self.x, self.y, ctrlX, ctrlY, fx, fy)
   146  }
   147  
   148  // Creates a boundary from the current position to the given target
   149  // as a cubic Bézier curve through the given control points and
   150  // moves the current position to the new one.
   151  func (self *outliner) CubeTo(cx1, cy1, cx2, cy2, fx, fy float64) {
   152  	self.CurveSegmenter.TraceCube(self.LineTo, self.x, self.y, cx1, cy1, cx2, cy2, fx, fy)
   153  }
   154  
   155  // Closes a path without tying back to the starting point.
   156  func (self *outliner) CutPath() {
   157  	switch self.openSegmentCount {
   158  	case 0:
   159  		return // superfluous call
   160  	case 1: // cut both head and tail
   161  		self.segments[0].Cut(&self.Buffer)
   162  	default: // cut start tail, cut end head
   163  		sc := self.openSegmentCount
   164  		self.segments[0].CutTail(&self.Buffer, &self.segments[1])
   165  		self.segments[sc-1].CutHead(&self.Buffer, &self.segments[sc-2])
   166  	}
   167  	self.openSegmentCount = 0
   168  }
   169  
   170  // Closes a path tying back to the starting point (if possible).
   171  func (self *outliner) ClosePath() {
   172  	sc := self.openSegmentCount
   173  	if sc <= 2 {
   174  		self.CutPath()
   175  	} else {
   176  		self.segments[0].Fill(&self.Buffer, &self.segments[sc-1], &self.segments[1])
   177  		self.segments[sc-1].Fill(&self.Buffer, &self.segments[sc-2], &self.segments[0])
   178  	}
   179  	self.openSegmentCount = 0
   180  }