167 lines
No EOL
5.8 KiB
Text
167 lines
No EOL
5.8 KiB
Text
define smooth [require './monotonic-interpolate'].createInterpolant
|
|
define tp [require './transform'].transformPoint
|
|
define utp [require './transform'].untransform
|
|
|
|
define [fallback] : for [local j 0] [j < arguments.length] [inc j] : if [arguments`j !== nothing] : return arguments`j
|
|
define [linreg x0 y0 x1 y1 x] : y0 + [x - x0] * [y1 - y0] / [x1 - x0]
|
|
|
|
define [SpiroExpansionContext] : begin {
|
|
set this.gizmo (.xx 1 .yy 1 .xy 0 .yy 0 .x 0 .y 0)
|
|
set this.controlKnots ()
|
|
set this.defaultd1 0
|
|
set this.defaultd2 0
|
|
return nothing
|
|
}
|
|
define [SpiroExpansionContext.prototype.moveTo x y unimportant] : begin {
|
|
if unimportant : return nothing
|
|
# Transform incoming knots using gizmo
|
|
set (.x x .y y) : tp this.gizmo (.x x .y y)
|
|
this.controlKnots.push (.x x .y y .type 'g4' .d1 this.defaultd1 .d2 this.defaultd2)
|
|
}
|
|
define [SpiroExpansionContext.prototype.lineTo x y unimportant] : begin {
|
|
local lastKnot this.controlKnots.[this.controlKnots.length - 1]
|
|
set (.x x .y y) : tp this.gizmo (.x x .y y)
|
|
local thisKnot (.x x .y y .type 'g4' .d1 lastKnot.d1 .d2 lastKnot.d2)
|
|
if lastKnot : begin {
|
|
local normalAngle : Math.PI / 2 + [Math.atan2 [y - lastKnot.y] [x - lastKnot.x]]
|
|
set thisKnot.normalAngle normalAngle
|
|
if [lastKnot.normalAngle === nothing] : set lastKnot.normalAngle normalAngle
|
|
}
|
|
if [not unimportant] : this.controlKnots.push thisKnot
|
|
}
|
|
define [SpiroExpansionContext.prototype.cubicTo x1 y1 x2 y2 x y unimportant] : begin {
|
|
local lastKnot this.controlKnots.[this.controlKnots.length - 1]
|
|
set (.x x1 .y y1) : tp this.gizmo (.x x1 .y y1)
|
|
set (.x x2 .y y2) : tp this.gizmo (.x x2 .y y2)
|
|
set (.x x .y y) : tp this.gizmo (.x x .y y)
|
|
local thisKnot (.x x .y y .type 'g4' .d1 lastKnot.d1 .d2 lastKnot.d2)
|
|
if [lastKnot && lastKnot.normalAngle === nothing] : begin {
|
|
local normalAngle : Math.PI / 2 + [Math.atan2 [y1 - lastKnot.y] [x1 - lastKnot.x]]
|
|
if [lastKnot.normalAngle === nothing] : set lastKnot.normalAngle normalAngle
|
|
}
|
|
if [not unimportant] : begin {
|
|
local normalAngle : Math.PI / 2 + [Math.atan2 [y - y2] [x - x2]]
|
|
set thisKnot.normalAngle normalAngle
|
|
this.controlKnots.push thisKnot
|
|
}
|
|
}
|
|
define [SpiroExpansionContext.prototype.set-width l r] : begin {
|
|
local lastKnot this.controlKnots.[this.controlKnots.length - 1]
|
|
if lastKnot {
|
|
then { lastKnot.d1 = l; lastKnot.d2 = r }
|
|
else { this.defaultd1 = l; this.defaultd2 = r}
|
|
}
|
|
}
|
|
define [SpiroExpansionContext.prototype.heads-to direction] : begin {
|
|
local lastKnot this.controlKnots.[this.controlKnots.length - 1]
|
|
if lastKnot : begin {
|
|
lastKnot.proposedNormal = direction
|
|
}
|
|
}
|
|
define [SpiroExpansionContext.prototype.set-type type] : begin {
|
|
local lastKnot this.controlKnots.[this.controlKnots.length - 1]
|
|
if lastKnot : begin {
|
|
lastKnot.type = type
|
|
}
|
|
}
|
|
|
|
define [shortestAngle end start] : begin {
|
|
local a : [end - start] % [Math.PI * 2]
|
|
if [a > Math.PI / 2] : a = a - Math.PI
|
|
return a
|
|
}
|
|
|
|
define [SpiroExpansionContext.prototype.expand] : begin {
|
|
local lhs ()
|
|
local rhs ()
|
|
|
|
local d1s ()
|
|
local d2s ()
|
|
local dxs ()
|
|
local dys ()
|
|
local js ()
|
|
foreach j [range 0 this.controlKnots.length] : if [not this.controlKnots.(j).unimportant] : begin {
|
|
local knot this.controlKnots.(j)
|
|
js.push j
|
|
d1s.push knot.d1
|
|
d2s.push knot.d2
|
|
if knot.proposedNormal {
|
|
then {
|
|
dxs.push : knot.proposedNormal.x - [Math.cos knot.normalAngle]
|
|
dys.push : knot.proposedNormal.y - [Math.sin knot.normalAngle]
|
|
}
|
|
else { dxs.push 0; dys.push 0 }
|
|
}
|
|
}
|
|
local fd1 : smooth js d1s
|
|
local fd2 : smooth js d2s
|
|
local fdx : smooth js dxs
|
|
local fdy : smooth js dys
|
|
# console.log deltaAngles
|
|
|
|
# interpolate important knots
|
|
foreach j [range 0 this.controlKnots.length] : begin {
|
|
local knot this.controlKnots.(j)
|
|
if [not knot.unimportant] : begin {
|
|
set lhs.(j) : object {
|
|
x : knot.x + [[fdx j] + [Math.cos knot.normalAngle]] * [fd1 j]
|
|
y : knot.y + [[fdy j] + [Math.sin knot.normalAngle]] * [fd1 j]
|
|
type knot.type
|
|
}
|
|
set rhs.(j) : object {
|
|
x : knot.x - [[fdx j] + [Math.cos knot.normalAngle]] * [fd2 j]
|
|
y : knot.y - [[fdy j] + [Math.sin knot.normalAngle]] * [fd2 j]
|
|
type : match knot.type {
|
|
"left" "right"
|
|
"right" "left"
|
|
type type
|
|
}
|
|
}
|
|
}
|
|
}
|
|
# interpolate unimportant knots referencing their original position relationship
|
|
foreach j [range 0 this.controlKnots.length] : begin {
|
|
local knot this.controlKnots.(j)
|
|
if knot.unimportant : begin {
|
|
local jBefore [j - 1]
|
|
while this.controlKnots.(jBefore).unimportant : dec jBefore
|
|
local jAfter [j + 1]
|
|
while this.controlKnots.(jAfter).unimportant : inc jAfter
|
|
|
|
local knotBefore : utp this.gizmo this.controlKnots.(jBefore)
|
|
local knotAfter : utp this.gizmo this.controlKnots.(jAfter)
|
|
local ref : utp this.gizmo knot
|
|
local lhsBefore : utp this.gizmo lhs.(jBefore)
|
|
local lhsAfter : utp this.gizmo lhs.(jAfter)
|
|
local rhsBefore : utp this.gizmo rhs.(jBefore)
|
|
local rhsAfter : utp this.gizmo rhs.(jAfter)
|
|
|
|
local kLHS : tp this.gizmo : object {
|
|
x : linreg knotBefore.x lhsBefore.x knotAfter.x lhsAfter.x ref.x
|
|
y : linreg knotBefore.y lhsBefore.y knotAfter.y lhsAfter.y ref.y
|
|
}
|
|
local kRHS : tp this.gizmo : object {
|
|
x : linreg knotBefore.x rhsBefore.x knotAfter.x rhsAfter.x ref.x
|
|
y : linreg knotBefore.y rhsBefore.y knotAfter.y rhsAfter.y ref.y
|
|
}
|
|
|
|
set lhs.(j) : object {
|
|
x kLHS.x
|
|
y kLHS.y
|
|
type knot.type
|
|
}
|
|
set rhs.(j) : object {
|
|
x kRHS.x
|
|
y kRHS.y
|
|
type : match knot.type {
|
|
"left" "right"
|
|
"right" "left"
|
|
type type
|
|
}
|
|
}
|
|
}
|
|
}
|
|
lhs.0.type = rhs.0.type = lhs.[lhs.length - 1].type = rhs.[rhs.length - 1].type = 'corner'
|
|
return : lhs.concat [rhs.reverse]
|
|
}
|
|
exports.SpiroExpansionContext = SpiroExpansionContext |