function robotModel(layout, {
w=width, labels=true, buffers=true, labelSizeMM=1.5, labelColor = (id) => "cyan"
}={}) {
const {min,max,round,cos,sin,PI} = Math;
const deg2rad = PI/180;
const {xlo,xhi,ylo,yhi} = layout.extent;
const h = round(w*(yhi-ylo)/(xhi-xlo));
const X = d3.scaleLinear().domain([xlo,xhi]).range([0,w-1]);
const Y = d3.scaleLinear().domain([ylo,yhi]).range([h-1,0]);
const X0 = X(0), Y0 = Y(0);
const labelSize = X(xlo + labelSizeMM) - X(xlo);
function drawPoly(P) {
const p = d3.path();
d3.transpose([P.x,P.y]).forEach(([x,y],i) => {
if(i) p.lineTo(X(x),Y(y));
else p.moveTo(X(x),Y(y));
});
p.closePath();
return p;
}
const armLineWidth = max(1, min(4, X(0.2)-X0));
const goalRadius = min(w / 150, X(0.5)-X0);
console.log("initializing svg...");
const svg = d3.create("svg")
.attr("width", w)
.attr("height", h)
.attr("viewBox", [0, 0, w, h]);
const g = svg.append("g")
.attr("fill", "lightgray")
.attr("stroke", "black");
const gg = svg.append("g")
.attr("fill", "none")
.attr("stroke", "lightseagreen")
.attr("stroke-width", 1);
//.attr("stroke-dasharray", [2,2]);
// Draw the robots.
g.selectAll("g.robot")
.data(layout.robots, d=>d.id)
.join("g")
.classed("robot", true)
// Translate to each robots (x,y) and theta origins.
.attr("transform", d => `translate(${X(d.x)-X0},${Y(d.y)-Y0}) rotate(${-d.T0},${X0},${Y0})`)
.call(R => {
R.append("g")
.classed("theta", true)
// Rotate to this robot's current internal theta.
.attr("transform", d => `rotate(${-d.T},${X0},${Y0})`)
.call(S => {
S.append("path")
.classed("theta", true)
.attr("d", drawPoly(physicalKeepouts.theta));
if(buffers) {
S.append("path")
.attr("fill", "none")
.attr("stroke-dasharray", [1,2])
.attr("d", drawPoly(bufferedKeepouts.theta));
}
const phi=S.append("g")
// Translate to this robot's phi arm axis.
.attr("transform", d => `translate(${X(d.r1)-X0},0)`)
.append("g")
.classed("phi", true)
// Rotate to this robot's current external phi.
.attr("transform", d => `rotate(${-(d.P+d.P0)},${X0},${Y0})`);
phi.append("path")
.classed("phi", true)
.attr("d", drawPoly(physicalKeepouts.phi));
if(buffers) {
phi.append("path")
.attr("fill", "none")
.attr("stroke-dasharray", [1,2])
.attr("d", drawPoly(bufferedKeepouts.phi));
}
phi.append("line")
.attr("stroke-width", armLineWidth)
.attr("stroke-linecap", "round")
.attr("x1", X0)
.attr("y1", Y0)
.attr("x2", d => X(d.r2))
.attr("y2", d => Y0);
S.append("line")
.attr("stroke-width", armLineWidth)
.attr("stroke-linecap", "round")
.attr("x1", X0)
.attr("y1", Y0)
.attr("x2", d => X(d.r1))
.attr("y2", d => Y0);
});
// Draw theta stop region relative to robot's (x,y) and theta origin.
R.append("path")
.attr("stroke", "blue")
.attr("fill", "none")
.attr("d", d => {
const p = d3.path();
p.moveTo(X(-2*d.cosTstop), Y(2*d.sinTstop));
p.lineTo(X0, Y0);
p.lineTo(X(-2*d.cosTstop), Y(-2*d.sinTstop));
return p;
});
if(labels) {
// Display an ID label for this robot.
R.append("text")
.attr("transform", d => `rotate(${d.T0},${X0},${Y0})`)
.attr("x", X0)
.attr("y", Y0)
.attr("stroke", "black")
.attr("fill", d => labelColor(d.id))
.attr("font-size", labelSize)
.attr("font-weight", "bold")
.attr("opacity", 0.3)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.text(d => d.id);
}
});
// Update the angles with minimal changes to the DOM.
function _setAngles(angles) {
g.selectAll("g.theta")
.data(angles, d=>d.id)
.join(
enter => {
if(enter.size()) {
const newIDs = enter.nodes().map(n=>n.__data__.id);
console.log("Ignoring angles for new robots:", newIDs);
}
},
update => {
update.attr("transform", d => `rotate(${-d.T},${X0},${Y0})`);
},
exit => { }
);
g.selectAll("g.phi")
.data(angles, d=>d.id)
.join(
enter => { },
update => {
update.attr("transform", d => `rotate(${-(d.P+d.P0)},${X0},${Y0})`);
},
exit => { }
);
}
// Update the keepout polygon colors based on their proximity values.
function _setProximityColors(proximities) {
proximities = [...proximities.values()];
g.selectAll("path.theta")
.data(proximities, d=>d.id)
.join(
enter => { },
update => { update.attr("fill", d => proximityColor(d.proximity.theta)); },
exit => { }
);
g.selectAll("path.phi")
.data(proximities, d=>d.id)
.join(
enter => { },
update => { update.attr("fill", d => proximityColor(d.proximity.phi)); },
exit => { }
);
}
const proximities = layout.getProximity();
_setProximityColors(proximities);
// Update the goal angles.
function _setGoals(goals) {
goals = goals.filter(d => d.goalT !== null && d.goalP !== null);
const goalT = goals.map(R => (R.goalT+R.T0)*deg2rad);
const goalTP = goals.map(R => (R.goalT+R.T0+R.goalP+R.P0)*deg2rad);
const cosT = goalT.map(T => cos(T)), sinT = goalT.map(T => sin(T));
const cosTP = goalTP.map(TP => cos(TP)), sinTP = goalTP.map(TP => sin(TP));
gg.selectAll("circle")
.data(goals, d=>d.id)
.join("circle")
.attr("cx", (d,i) => X(d.x + d.r1 * cosT[i] + d.r2 * cosTP[i]))
.attr("cy", (d,i) => Y(d.y + d.r1 * sinT[i] + d.r2 * sinTP[i]))
.attr("r", goalRadius);
gg.selectAll("path")
.data(goals, d=>d.id)
.join("path")
.attr("d", (d,i) => {
const p = d3.path();
p.moveTo(X(d.x), Y(d.y));
p.lineTo(X(d.x + d.r1 * cosT[i]), Y(d.y + d.r1 * sinT[i]));
p.lineTo(X(d.x + d.r1 * cosT[i] + d.r2 * cosTP[i]),
Y(d.y + d.r1 * sinT[i] + d.r2 * sinTP[i]));
return p;
});
}
_setGoals(layout.robots);
const model = Object.assign(svg.node(), {
value: layout,
setAngles(angles) {
angles.forEach(({id,T,P}) => {
if(this.value.index.has(id)) {
const robot = this.value.index.get(id);
robot.T = T;
robot.P = P;
//console.log(`robotModel: set T=${T} P=${P} for ${id}`);
}
});
_setAngles(this.value.robots);
const proximities = this.value.getProximity();
_setProximityColors(proximities);
model.dispatchEvent(new Event("input", {bubble:true}));
},
setGoals(goals) {
goals.forEach(({id,T,P}) => {
if(this.value.index.has(id)) {
const robot = this.value.index.get(id);
robot.goalT = T;
robot.goalP = P;
}
});
_setGoals(this.value.robots);
model.dispatchEvent(new Event("input", {bubble:true}));
}
});
return model;
}