class crochetDraw extends crochetLink{
static getType() {return "draw"}
static getDeftLen() {return 10}
static getDesc() {return "no-strength link used for drawing"}
static isPrintable() {return true}
strenght() {return 0}
static tokenizeDrawingCommands(str) {
let allowedCommands = new Map([
["l",1], ["L",1], ["M",1], ["m",1], ["X", 1], ["a", 4], ["q", 2], ["Q", 2], ["c", 3], ["C", 3]
])
let commands = str.split(";").map(e=>e.trim()).filter(e=>e!="")
if (commands.length == 0) this.pathCmds=[]
const command_rgx = /[a-zA-Z\_]+(\ *\:\ *-?[0-9]+\,-?[0-9]+(\%\ *(-?[0-9]+\,-?[0-9]+)?)?)+$/
commands = commands.filter(c=>(c.match(command_rgx)!==null))
let tokenized = commands.map( token=> {
let elements = token.split(":").map(e=>e.trim()).filter(e=>e!="")
let cmd = elements.shift()
return {cmd:cmd, params:elements}
})
.filter( e=>allowedCommands.has(e.cmd) )
.filter( e=>(allowedCommands.get(e.cmd)<=e.params.length) )
return tokenized
}
static parseDrawingCmds(newPath=undefined) {
if (typeof newPath === 'undefined') {newPath=this.getPath()}
// remove invalidr commands, tokenize the rest
// tokens are: command code + array of parameters
let tokenized_commands = this.tokenizeDrawingCommands(newPath)
// convert each fo the parameters into a two-vector
// first vector is %-translaton
// second the abs-translation
let pathCmds = tokenized_commands.map( token=>{
let params_2v = token.params.map( e=>{
// each two-vector is expected to be coded as a string "xx,yy%zz,ww"
// with possible space paddign around each number
let p = e.split("%")
// if no %-separator, assume percentage-vecotr is (0,0)
if (p.length<2) p.unshift("0,0")
// if no valid vector is provided, assume it's (0,0)
if (p[1]=="") p[1]="0,0"
if (p[0]=="") p[0]="0,0"
// below: parseFloat takes care of trimming the string
// %-translation vector
let v_perc = new vec2d(100,0)
try {
v_perc = new vec2d( p[0].split(",").map(e=>parseFloat(e)/100) )
} catch (e) {console.log(e)}
// abs-translation vector
let v_abs = new vec2d(0,0)
try{
v_abs = new vec2d(p[1].split(",").map(e=>parseFloat(e)))
} catch (e) {console.log(e)}
// combined
return {v_perc:v_perc, v_abs:v_abs}
})
return {cmd:token.cmd, params:params_2v}
})
return pathCmds
}
static getPath() { return "l:100,0%"}
getPath() {return this.constructor.getPath()}
static getColor() {return "black"}
constructor(context, fromNode, toNode, length = undefined) {
super(context, fromNode, toNode, length)
this.pathCmds = this.constructor.parseDrawingCmds()
}
// methods to actually generate the SVG path
// resets frame of reference for the link
// i.e. it's base vectors used in drawing path calculation
restetFrameOfReference() {
this.s = this.source.getVector() // where to start drawing
let v = this.target.getVector().sub(this.s)
this.b_phi = v.phi() // normal of the link vector
if (isNaN(this.b_phi)) this.b_phi = 0 // just in case of extremely short links
this.b_l = v.len() // lenght of the link vector
if (this.b_l<1) this.b_l = 1 // just in case of extremely short links
return this
}
// generates SVG path to move pen to links Source point
resetPen(){
return " M "+this.s.getTxt()
}
// assuming the frame of reference is correct, we can transform
// each path parmeter two-vector into a real vector
calcPathPoint(twoVector){
let v = twoVector.v_abs.rot(this.b_phi)
let u = twoVector.v_perc.scale(this.b_l).rot(this.b_phi)
return v.add(u)
}
// having the tokenized path drawing commands and links start and end,
// generate the applicable SVG path to be drawn
getPathTxt(override=false) {
// default path is a simple from-to line
if (override) {
return "M"+this.source.getVector().getTxt()+"L"+this.target.getVector().getTxt()
}
// establish frame of reference
this.restetFrameOfReference()
// parse the command
// start by moving to link source
let path = "M "+this.s.getTxt()
// parse each path command
for (let i = 0; i<this.pathCmds.length; i++){
// get command and control points
let command = this.pathCmds[i].cmd
let params = this.pathCmds[i].params
let vectors = params.map(e=>this.calcPathPoint(e))
// write path string
switch (command) {
case "M":
path += " " + this.resetPen()
case "m":
path += " m " + vectors.map(e=>e.getTxt(true)).join(" ")
break;
case "L":
path += " " + this.resetPen()
case "l":
path += " l " + vectors.map(e=>e.getTxt(true)).join(" ")
break;
case "Q":
path += " " + this.resetPen()
case "q":
path += " q " + vectors.slice(0,3).map(e=>e.getTxt(true)).join(" ")
break;
case "C":
path += " " + this.resetPen()
case "c":
path += " c " + vectors.slice(0,4).map(e=>e.getTxt(true)).join(" ")
break;
case "T":
path += " " + this.resetPen()
case "t":
path += " t " + vectors.slice(0,4).map(e=>e.getTxt(true)).join(" ")
break;
case "a":
path += " a " + vectors[0].rot(-this.b_phi).getTxt(true) + ","
path += vectors[1].phi()/Math.PI*180 + ","
path += params[2].v_abs._x + "," +params[2].v_abs._y+ ","
path += vectors[3].getTxt(true)
break;
}
}
return path
}
}