'use strict';
const Rac = require('../Rac');
const utils = require('../util/utils');
/**
* Drawer that uses a [P5](https://p5js.org/) instance for all drawing
* operations.
*
* @alias Rac.P5Drawer
*/
class P5Drawer {
constructor(rac, p5){
this.rac = rac;
this.p5 = p5;
this.drawRoutines = [];
this.debugRoutines = [];
this.applyRoutines = [];
/**
* Style used for debug drawing, when `null` the style already applied
* is used.
*
* @type {object}
*/
this.debugStyle = null;
/**
* Style used for text for debug drawing, when `null` the style already
* applied is used.
*
* @type {object}
*/
this.debugTextStyle = null;
/**
* Object with options used by the default implementation of
* `drawable.debug()`.
*
* @type {object}
*/
this.debugTextOptions = {
font: 'monospace',
size: Rac.Text.Format.defaultSize,
fixedDigits: 2
};
/**
* Radius of point markers for debug drawing.
* @type {number}
*/
this.debugPointRadius = 4;
/**
* Radius of the main visual elements for debug drawing.
* @type {number}
*/
this.debugMarkerRadius = 22;
this.setupAllDrawFunctions();
this.setupAllDebugFunctions();
this.setupAllApplyFunctions();
}
// Adds a DrawRoutine for the given class.
setDrawFunction(classObj, drawFunction) {
let index = this.drawRoutines
.findIndex(routine => routine.classObj === classObj);
let routine;
if (index === -1) {
routine = new DrawRoutine(classObj, drawFunction);
} else {
routine = this.drawRoutines[index];
routine.drawFunction = drawFunction;
// Delete routine
this.drawRoutines.splice(index, 1);
}
this.drawRoutines.push(routine);
}
setDrawOptions(classObj, options) {
let routine = this.drawRoutines
.find(routine => routine.classObj === classObj);
if (routine === undefined) {
console.log(`Cannot find routine for class - className:${classObj.name}`);
throw Rac.Error.invalidObjectConfiguration
}
if (options.requiresPushPop !== undefined) {
routine.requiresPushPop = options.requiresPushPop;
}
}
setClassDrawStyle(classObj, style) {
let routine = this.drawRoutines
.find(routine => routine.classObj === classObj);
if (routine === undefined) {
console.log(`Cannot find routine for class - className:${classObj.name}`);
throw Rac.Error.invalidObjectConfiguration
}
routine.style = style;
}
// Adds a DebugRoutine for the given class.
setDebugFunction(classObj, debugFunction) {
let index = this.debugRoutines
.findIndex(routine => routine.classObj === classObj);
let routine;
if (index === -1) {
routine = new DebugRoutine(classObj, debugFunction);
} else {
routine = this.debugRoutines[index];
routine.debugFunction = debugFunction;
// Delete routine
this.debugRoutines.splice(index, 1);
}
this.debugRoutines.push(routine);
}
// Adds a ApplyRoutine for the given class.
setApplyFunction(classObj, applyFunction) {
let index = this.applyRoutines
.findIndex(routine => routine.classObj === classObj);
let routine;
if (index === -1) {
routine = new ApplyRoutine(classObj, applyFunction);
} else {
routine = this.applyRoutines[index];
routine.drawFunction = drawFunction;
// Delete routine
this.applyRoutines.splice(index, 1);
}
this.applyRoutines.push(routine);
}
drawObject(object, style = null) {
let routine = this.drawRoutines
.find(routine => object instanceof routine.classObj);
if (routine === undefined) {
console.trace(`Cannot draw object - object-type:${utils.typeName(object)}`);
throw Rac.Error.invalidObjectToDraw;
}
if (routine.requiresPushPop === true
|| style !== null
|| routine.style !== null)
{
this.p5.push();
if (routine.style !== null) {
routine.style.apply();
}
if (style !== null) {
style.apply();
}
routine.drawFunction(this, object);
this.p5.pop();
} else {
// No push-pull
routine.drawFunction(this, object);
}
}
// TODO: use directly number.toFixed
debugNumber(number) {
return number.toFixed(this.debugTextOptions.fixedDigits);
}
debugObject(object, drawsText) {
let routine = this.debugRoutines
.find(routine => object instanceof routine.classObj);
if (routine === undefined) {
// No routine, just draw object with debug style
this.drawObject(object, this.debugStyle);
return;
}
if (this.debugStyle !== null) {
this.p5.push();
this.debugStyle.apply();
routine.debugFunction(this, object, drawsText);
this.p5.pop();
} else {
routine.debugFunction(this, object, drawsText);
}
}
applyObject(object) {
let routine = this.applyRoutines
.find(routine => object instanceof routine.classObj);
if (routine === undefined) {
console.trace(`Cannot apply object - object-type:${utils.typeName(object)}`);
throw Rac.Error.invalidObjectToApply;
}
routine.applyFunction(this, object);
}
// Sets up all drawing routines for rac drawable clases.
// Also attaches additional prototype and static functions in relevant
// classes.
setupAllDrawFunctions() {
let functions = require('./draw.functions');
// Point
this.setDrawFunction(Rac.Point, functions.drawPoint);
require('./Point.functions')(this.rac);
// Ray
this.setDrawFunction(Rac.Ray, functions.drawRay);
require('./Ray.functions')(this.rac);
// Segment
this.setDrawFunction(Rac.Segment, functions.drawSegment);
require('./Segment.functions')(this.rac);
// Arc
this.setDrawFunction(Rac.Arc, functions.drawArc);
Rac.Arc.prototype.vertex = function() {
let angleDistance = this.angleDistance();
let beziersPerTurn = 5;
let divisions = Math.ceil(angleDistance.turnOne() * beziersPerTurn);
this.divideToBeziers(divisions).vertex();
};
// Bezier
this.setDrawFunction(Rac.Bezier, (drawer, bezier) => {
drawer.p5.bezier(
bezier.start.x, bezier.start.y,
bezier.startAnchor.x, bezier.startAnchor.y,
bezier.endAnchor.x, bezier.endAnchor.y,
bezier.end.x, bezier.end.y);
});
Rac.Bezier.prototype.vertex = function() {
this.start.vertex()
this.rac.drawer.p5.bezierVertex(
this.startAnchor.x, this.startAnchor.y,
this.endAnchor.x, this.endAnchor.y,
this.end.x, this.end.y);
};
// Composite
this.setDrawFunction(Rac.Composite, (drawer, composite) => {
composite.sequence.forEach(item => item.draw());
});
Rac.Composite.prototype.vertex = function() {
this.sequence.forEach(item => item.vertex());
};
// Shape
this.setDrawFunction(Rac.Shape, (drawer, shape) => {
drawer.p5.beginShape();
shape.outline.vertex();
if (shape.contour.isNotEmpty()) {
drawer.p5.beginContour();
shape.contour.vertex();
drawer.p5.endContour();
}
drawer.p5.endShape();
});
Rac.Shape.prototype.vertex = function() {
this.outline.vertex();
this.contour.vertex();
};
// Text
this.setDrawFunction(Rac.Text, (drawer, text) => {
text.format.apply(text.point);
drawer.p5.text(text.string, 0, 0);
});
this.setDrawOptions(Rac.Text, {requiresPushPop: true});
// Applies all text properties and translates to the given `point`.
// After the format is applied the text should be drawn at the origin.
Rac.Text.Format.prototype.apply = function(point) {
let hAlign;
let hOptions = Rac.Text.Format.horizontal;
switch (this.horizontal) {
case hOptions.left: hAlign = this.rac.drawer.p5.LEFT; break;
case hOptions.center: hAlign = this.rac.drawer.p5.CENTER; break;
case hOptions.right: hAlign = this.rac.drawer.p5.RIGHT; break;
default:
console.trace(`Invalid horizontal configuration - horizontal:${this.horizontal}`);
throw Rac.Error.invalidObjectConfiguration;
}
let vAlign;
let vOptions = Rac.Text.Format.vertical;
switch (this.vertical) {
case vOptions.top: vAlign = this.rac.drawer.p5.TOP; break;
case vOptions.bottom: vAlign = this.rac.drawer.p5.BOTTOM; break;
case vOptions.center: vAlign = this.rac.drawer.p5.CENTER; break;
case vOptions.baseline: vAlign = this.rac.drawer.p5.BASELINE; break;
default:
console.trace(`Invalid vertical configuration - vertical:${this.vertical}`);
throw Rac.Error.invalidObjectConfiguration;
}
// Text properties
this.rac.drawer.p5.textAlign(hAlign, vAlign);
this.rac.drawer.p5.textSize(this.size);
if (this.font !== null) {
this.rac.drawer.p5.textFont(this.font);
}
// Positioning
this.rac.drawer.p5.translate(point.x, point.y);
if (this.angle.turn != 0) {
this.rac.drawer.p5.rotate(this.angle.radians());
}
} // Rac.Text.Format.prototype.apply
} // setupAllDrawFunctions
// Sets up all debug routines for rac drawable clases.
setupAllDebugFunctions() {
let functions = require('./debug.functions');
this.setDebugFunction(Rac.Point, functions.debugPoint);
this.setDebugFunction(Rac.Ray, functions.debugRay);
this.setDebugFunction(Rac.Segment, functions.debugSegment);
this.setDebugFunction(Rac.Arc, functions.debugArc);
Rac.Angle.prototype.debug = function(point, drawsText = false) {
const drawer = this.rac.drawer;
if (drawer.debugStyle !== null) {
drawer.p5.push();
drawer.debugStyle.apply();
// TODO: could this be a good option to implement splatting arguments
// into the debugFunction?
functions.debugAngle(drawer, this, point, drawsText);
drawer.p5.pop();
} else {
functions.debugAngle(drawer, this, point, drawsText);
}
};
Rac.Point.prototype.debugAngle = function(angle, drawsText = false) {
angle = this.rac.Angle.from(angle);
angle.debug(this, drawsText);
return this;
};
} // setupAllDebugFunctions
// Sets up all applying routines for rac style clases.
// Also attaches additional prototype functions in relevant classes.
setupAllApplyFunctions() {
// Color prototype functions
Rac.Color.prototype.applyBackground = function() {
this.rac.drawer.p5.background(this.r * 255, this.g * 255, this.b * 255);
};
Rac.Color.prototype.applyFill = function() {
this.rac.drawer.p5.fill(this.r * 255, this.g * 255, this.b * 255, this.a * 255);
};
Rac.Color.prototype.applyStroke = function() {
this.rac.drawer.p5.stroke(this.r * 255, this.g * 255, this.b * 255, this.a * 255);
};
// Stroke
this.setApplyFunction(Rac.Stroke, (drawer, stroke) => {
if (stroke.weight === null && stroke.color === null) {
drawer.p5.noStroke();
return;
}
if (stroke.color !== null) {
stroke.color.applyStroke();
}
if (stroke.weight !== null) {
drawer.p5.strokeWeight(stroke.weight);
}
});
// Fill
this.setApplyFunction(Rac.Fill, (drawer, fill) => {
if (fill.color === null) {
drawer.p5.noFill();
return;
}
fill.color.applyFill();
});
// Style
this.setApplyFunction(Rac.Style, (drawer, style) => {
if (style.stroke !== null) {
style.stroke.apply();
}
if (style.fill !== null) {
style.fill.apply();
}
});
Rac.Style.prototype.applyToClass = function(classObj) {
this.rac.drawer.setClassDrawStyle(classObj, this);
}
} // setupAllApplyFunctions
} // class P5Drawer
module.exports = P5Drawer;
// Encapsulates the drawing function and options for a specific class.
// The draw function is called with two parameters: the instance of the
// drawer, and the object to draw.
//
// Optionally a `style` can be asigned to always be applied before
// drawing an instance of the associated class. This style will be
// applied before any styles provided to the `draw` function.
//
// Optionally `requiresPushPop` can be set to `true` to always peform
// a `push` and `pop` before and after all the style and drawing in
// the routine. This is intended for objects which drawing operations
// may need to push transformation to the stack.
class DrawRoutine {
constructor (classObj, drawFunction) {
this.classObj = classObj;
this.drawFunction = drawFunction;
this.style = null;
// Options
this.requiresPushPop = false;
}
} // DrawRoutine
class DebugRoutine {
constructor (classObj, debugFunction) {
this.classObj = classObj;
this.debugFunction = debugFunction;
}
}
class ApplyRoutine {
constructor (classObj, applyFunction) {
this.classObj = classObj;
this.applyFunction = applyFunction;
}
}