'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}
* @default null
*/
this.debugStyle = null;
/**
* Style used for text for debug drawing, when `null` the style already
* applied is used.
*
* @type {Object}
* @default null
*/
this.debugTextStyle = null;
/**
* Settings used by the default implementation of `drawable.debug()`.
*
* @property {String} font='monospace'
* Font to use when drawing with `debug()`
* @property {Number} [font=[rac.textFormatDefaults.size]{@link Rac#textFormatDefaults}]
* Font size to use when drawing with `debug()`
* @property {Number} fixedDigits=2
* Number of decimal digits to print when drawing with `debug()`
*
* @type {Object}
*/
this.debugTextOptions = {
font: 'monospace',
// TODO: documentation displays this as being optional
// in order to make the link work it has to be wrapped in [],
// which makes it an optional
size: rac.textFormatDefaults.size,
fixedDigits: 2
};
/**
* Radius of point markers for debug drawing.
* @type {Number}
* @default 22
*/
this.debugPointRadius = 4;
/**
* Radius of the main visual elements for debug drawing.
* @type {Number}
* @default 22
*/
this.debugMarkerRadius = 22;
/**
* Factor applied to stroke weight setting. Stroke weight is set to
* `stroke.weight * strokeWeightFactor` when applicable.
*
* @type {Number}
* @default 1
*/
this.strokeWeightFactor = 1;
this.setupAllDrawFunctions();
this.setupAllDebugFunctions();
this.setupAllApplyFunctions();
// TODO: add a customized function for new classes!
}
/**
* Sets the given `drawFunction` to perform the drawing for instances of
* class `drawableClass`.
*
* `drawFunction` is expected to have the signature:
* ```
* drawFunction(drawer, objectOfClass)
* ```
* + `drawer: P5Drawer` - Instance to use for drawing
* + `objectOfClass: drawableClass` - Instance of `drawableClass` to draw
*
* @param {class} drawableClass - Class of the instances to draw
* @param {function} drawFunction - Function that performs drawing
*/
setDrawFunction(drawableClass, drawFunction) {
let index = this.drawRoutines
.findIndex(routine => routine.classObj === drawableClass);
let routine;
if (index === -1) {
routine = new DrawRoutine(drawableClass, 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;
}
/**
* Sets the given `debugFunction` to perform the debug-drawing for
* instances of class `drawableClass`.
*
* When a drawable class does not have a `debugFunction` setup, calling
* `drawable.debug()` simply calls `draw()` with
* `[debugStyle]{@link Rac.P5Drawer#debugStyle}` applied.
*
* `debugFunction` is expected to have the signature:
* ```
* debugFunction(drawer, objectOfClass, drawsText)
* ```
* + `drawer: P5Drawer` - Instance to use for drawing
* + `objectOfClass: drawableClass` - Instance of `drawableClass` to draw
* + `drawsText: bool` - When `true` text should be drawn with
* additional information.
*
* @param {class} drawableClass - Class of the instances to draw
* @param {function} debugFunction - Function that performs debug-drawing
*/
setDebugFunction(drawableClass, debugFunction) {
let index = this.debugRoutines
.findIndex(routine => routine.classObj === drawableClass);
let routine;
if (index === -1) {
routine = new DebugRoutine(drawableClass, 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);
}
}
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);
});
// `text.format.apply` makes translate and rotation modifications to
// the drawing matrix, this requires a push-pop on every draw
this.setDrawOptions(Rac.Text, {requiresPushPop: true});
} // 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);
}
return this;
};
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 * drawer.strokeWeightFactor);
}
});
// Fill
this.setApplyFunction(Rac.Fill, (drawer, fill) => {
if (fill.color === null) {
drawer.p5.noFill();
return;
}
fill.color.applyFill();
});
// StyleContainer
this.setApplyFunction(Rac.StyleContainer, (drawer, container) => {
container.styles.forEach(item => {
item.apply();
});
});
// Text.Format
// Applies all text properties and translates to the given `point`.
// After the format is applied the text should be drawn at the origin.
//
// Calling this function requires a push-pop to the drawing style
// settings since translate and rotation modifications are made to the
// drawing matrix. Otherwise all other subsequent drawing will be
// impacted.
Rac.Text.Format.prototype.apply = function(point) {
let hAlign;
let hEnum = Rac.Text.Format.horizontalAlign;
switch (this.hAlign) {
case hEnum.left: hAlign = this.rac.drawer.p5.LEFT; break;
case hEnum.center: hAlign = this.rac.drawer.p5.CENTER; break;
case hEnum.right: hAlign = this.rac.drawer.p5.RIGHT; break;
default:
console.trace(`Invalid hAlign configuration - hAlign:${this.hAlign}`);
throw Rac.Error.invalidObjectConfiguration;
}
let vAlign;
let vEnum = Rac.Text.Format.verticalAlign;
switch (this.vAlign) {
case vEnum.top: vAlign = this.rac.drawer.p5.TOP; break;
case vEnum.bottom: vAlign = this.rac.drawer.p5.BOTTOM; break;
case vEnum.center: vAlign = this.rac.drawer.p5.CENTER; break;
case vEnum.baseline: vAlign = this.rac.drawer.p5.BASELINE; break;
default:
console.trace(`Invalid vAlign configuration - vAlign:${this.vAlign}`);
throw Rac.Error.invalidObjectConfiguration;
}
// Align
this.rac.drawer.p5.textAlign(hAlign, vAlign);
// Size
const textSize = this.size ?? this.rac.textFormatDefaults.size;
this.rac.drawer.p5.textSize(textSize);
// Font
const textFont = this.font ?? this.rac.textFormatDefaults.font;
if (textFont !== null) {
this.rac.drawer.p5.textFont(textFont);
}
// 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
} // setupAllApplyFunctions
} // class P5Drawer
module.exports = P5Drawer;
// Contains the drawing function and options for drawing objects of a
// specific class.
//
// An instance is created for each drawable class that the drawer can
// support, which contains all the settings needed for drawing.
class DrawRoutine {
// TODO: Rename to drawableClass
constructor (classObj, drawFunction) {
// Class associated with the contained settings.
this.classObj = classObj;
// Drawing function for objects of type `classObj` with the signature:
// `drawFunction(drawer, objectOfClass)`
// + `drawer: P5Drawer` - Instance to use for drawing
// + `objectOfClass: classObj` - Instance of `classObj` to draw
//
// The function is intended to perform drawing using `drawer.p5`
// functions or calling `draw()` in other drawable objects. All styles
// are pushed beforehand and popped afterwards.
//
// In general it is expected that the `drawFunction` peforms no changes
// to the drawing settings in order for each drawing call to use only a
// single `push/pop` when necessary. For classes that require
// modifications to the drawing settings the `requiresPushPop`
// property can be set to force a `push/pop` with each drawing call
// regardless if styles are applied.
this.drawFunction = drawFunction;
// When set, this style is always applied before each drawing call to
// objects of type `classObj`. This `style` is applied before the
// `style` provided to the drawing call.
this.style = null;
// When set to `true`, a `push/pop` is always peformed before and after
// all the style are applied and drawing is performed. This is intended
// for objects which drawing operations may need to perform
// transformations to the drawing settings.
this.requiresPushPop = false;
} // constructor
} // DrawRoutine
// Contains the debug-drawing function and options for debug-drawing
// objects of a specific class.
//
// An instance is created for each drawable class that the drawer can
// support, which contains all the settings needed for debug-drawing.
//
// When a drawable object does not have a `DebugRoutine` setup, calling
// `debug()` simply calls `draw()` with the debug style applied.
class DebugRoutine {
constructor (classObj, debugFunction) {
// Class associated with the contained settings.
this.classObj = classObj;
// Debug function for objects of type `classObj` with the signature:
// `debugFunction(drawer, objectOfClass, drawsText)`
// + `drawer: P5Drawer` - Instance to use for drawing
// + `objectOfClass: classObj` - Instance of `classObj` to debug
// + `drawsText: bool` - When `true` text should be drawn with
// additional information.
//
// The function is intended to perform debug-drawing using `drawer.p5`
// functions or calling `draw()` in other drawable objects. The debug
// style is pushed beforehand and popped afterwards.
//
// In general it is expected that the `drawFunction` peforms no changes
// to the drawing settings in order for each drawing call to use only a
// single `push/pop` when necessary.
//
this.debugFunction = debugFunction;
} // constructor
}
class ApplyRoutine {
constructor (classObj, applyFunction) {
this.classObj = classObj;
this.applyFunction = applyFunction;
}
}