microjam
Version:
Minimalistic JAMStack authoring and publishing environment
1,280 lines (1,232 loc) • 189 kB
JavaScript
/**
* mec (c) 2018-19 Stefan Goessner
* @license MIT License
*/
"use strict";
/**
* @namespace mec namespace for the mec library.
* It includes mainly constants and some general purpose functions.
*/
const mec = {
/**
* user language shortcut (for messages)
* @const
* @type {string}
*/
lang: 'en',
/**
* namespace for user language neutral messages
* @const
* @type {object}
*/
msg: {},
/**
* minimal float difference to 1.0
* @const
* @type {number}
*/
EPS: 1.19209e-07,
/**
* Medium length tolerance for position correction.
* @const
* @type {number}
*/
lenTol: 0.001,
/**
* Angular tolerance for orientation correction.
* @const
* @type {number}
*/
angTol: 1 / 180 * Math.PI,
/**
* Velocity tolerance.
* @const
* @type {number}
*/
velTol: 0.01,
/**
* Force tolerance.
* @const
* @type {number}
*/
forceTol: 0.1,
/**
* Moment tolerance.
* @const
* @type {number}
*/
momentTol: 0.01,
/**
* Tolerances (new concept)
* accepting ['high','medium','low'].
* @const
* @type {number}
*/
tol: {
len: {
low: 0.00001,
medium: 0.001,
high: 0.1
}
},
maxLinCorrect: 20,
/**
* fixed limit of assembly iteration steps.
*/
asmItrMax: 512, // 512,
/**
* itrMax: fixed limit of simulation iteration steps.
*/
itrMax: 256,
/**
* corrMax: fixed number of position correction steps.
*/
corrMax: 64,
/**
* graphics options
* @const
* @type {object}
*/
show: {
/**
* flag for darkmode.
* @const
* @type {boolean}
*/
darkmode: false,
/**
* flag for showing labels of nodes.
* @const
* @type {boolean}
*/
nodeLabels: false,
/**
* flag for showing labels of constraints.
* @const
* @type {boolean}
*/
constraintLabels: true,
/**
* flag for showing labels of loads.
* @const
* @type {boolean}
*/
loadLabels: true,
/**
* flag for showing nodes.
* @const
* @type {boolean}
*/
nodes: true,
/**
* flag for showing constraints.
* @const
* @type {boolean}
*/
constraints: true,
colors: {
invalidConstraintColor: '#b11',
validConstraintColor: { dark: '#ffffff99', light: '#777' },
forceColor: { dark: 'orangered', light: 'orange' },
springColor: { dark: '#ccc', light: '#aaa' },
constraintVectorColor: { dark: 'orange', light: 'green' },
hoveredElmColor: { dark: 'white', light: 'gray' },
selectedElmColor: { dark: 'yellow', light: 'blue' },
txtColor: { dark: 'white', light: 'black' },
velVecColor: { dark: 'lightsteelblue', light: 'steelblue' },
accVecColor: { dark: 'lightsalmon', light: 'firebrick' },
forceVecColor: { dark: 'wheat', light: 'saddlebrown' }
},
/**
* color for drawing valid constraints.
* @return {string}
*/
get validConstraintColor() { return this.darkmode ? this.colors.validConstraintColor.dark : this.colors.validConstraintColor.light },
/**
* color for drawing forces.
* @return {string}
*/
get forceColor() { return this.darkmode ? this.colors.forceColor.dark : this.colors.forceColor.light },
/**
* color for drawing springs.
* @return {string}
*/
get springColor() { return this.darkmode ? this.colors.springColor.dark : this.colors.springColor.light },
/**
* color for vectortypes of constraints.
* @return {string}
*/
get constraintVectorColor() { return this.darkmode ? this.colors.constraintVectorColor.dark : this.colors.constraintVectorColor.light },
/**
* hovered element shading color.
* @return {string}
*/
get hoveredElmColor() { return this.darkmode ? this.colors.hoveredElmColor.dark : this.colors.hoveredElmColor.light },
/**
* selected element shading color.
* @return {string}
*/
get selectedElmColor() { return this.darkmode ? this.colors.selectedElmColor.dark : this.colors.selectedElmColor.light },
/**
* color for g2.txt (ls).
* @return {string}
*/
get txtColor() { return this.darkmode ? this.colors.txtColor.dark : this.colors.txtColor.light },
/**
* color for velocity arrow (ls).
* @const
* @type {string}
*/
get velVecColor() { return this.darkmode ? this.colors.velVecColor.dark : this.colors.velVecColor.light },
/**
* color for acceleration arrow (ls).
* @const
* @type {string}
*/
get accVecColor() { return this.darkmode ? this.colors.accVecColor.dark : this.colors.accVecColor.light },
/**
* color for acceleration arrow (ls).
* @const
* @type {string}
*/
get forceVecColor() { return this.darkmode ? this.colors.forceVecColor.dark : this.colors.forceVecColor.light }
},
/**
* default gravity.
* @const
* @type {object}
*/
gravity: {x:0,y:-10,active:false},
/*
* analysing values
*/
aly: {
m: { get scl() { return 1}, type:'num', name:'m', unit:'kg' },
pos: { type:'pnt', name:'p', unit:'m' },
vel: { get scl() {return mec.m_u}, type:'vec', name:'v', unit:'m/s', get drwscl() {return 40*mec.m_u} },
acc: { get scl() {return mec.m_u}, type:'vec', name:'a', unit:'m/s^2', get drwscl() {return 10*mec.m_u} },
w: { get scl() { return 180/Math.PI}, type:'num', name:'φ', unit:'°' },
wt: { get scl() { return 1}, type:'num', name:'ω', unit:'rad/s' },
wtt: { get scl() { return 1}, type:'num', name:'α', unit:'rad/s^2' },
r: { get scl() { return mec.m_u}, type:'num', name:'r', unit:'m' },
rt: { get scl() { return mec.m_u}, type:'num', name:'rt', unit:'m/s' },
rtt: { get scl() { return mec.m_u}, type:'num', name:'rtt', unit:'m/s^2' },
force: { get scl() {return mec.m_u}, type:'vec', name:'F', unit:'N', get drwscl() {return 5*mec.m_u} },
velAbs: { get scl() {return mec.m_u}, type:'num', name:'v', unit:'m/s' },
accAbs: { get scl() {return mec.m_u}, type:'num', name:'a', unit:'m/s' },
forceAbs: { get scl() {return mec.m_u}, type:'num', name:'F', unit:'N' },
moment: { get scl() {return mec.m_u**2}, type:'num', name:'M', unit:'Nm' },
energy: { get scl() {return mec.to_J}, type:'num', name:'E', unit:'J' },
pole: { type:'pnt', name:'P', unit:'m' },
polAcc: { get scl() {return mec.m_u}, type:'vec', name:'a_P', unit:'m/s^2', get drwscl() {return 10*mec.m_u} },
polChgVel: { get scl() {return mec.m_u}, type:'vec', name:'u_P', unit:'m/s', get drwscl() {return 40*mec.m_u} },
accPole: { type:'pnt', name:'Q', unit:'m' },
inflPole: { type:'pnt', name:'I', unit:'m' },
t: { get scl() { return 1 }, type:'num', name:'t', unit:'sec' }
},
/**
* unit specifiers and relations
*/
/**
* default length scale factor (meter per unit) [m/u].
* @const
* @type {number}
*/
m_u: 0.01,
/**
* convert [u] => [m]
* @return {number} Value in [m]
*/
to_m(x) { return x*mec.m_u; },
/**
* convert [m] = [u]
* @return {number} Value in [u]
*/
from_m(x) { return x/mec.m_u; },
/**
* convert [kgu/m^2] => [kgm/s^2] = [N]
* @return {number} Value in [N]
*/
to_N(x) { return x*mec.m_u; },
/**
* convert [N] = [kgm/s^2] => [kgu/s^2]
* @return {number} Value in [kgu/s^2]
*/
from_N(x) { return x/mec.m_u; },
/**
* convert [kgu^2/m^2] => [kgm^2/s^2] = [Nm]
* @return {number} Value in [Nm]
*/
to_Nm(x) { return x*mec.m_u*mec.m_u; },
/**
* convert [Nm] = [kgm^2/s^2] => [kgu^2/s^2]
* @return {number} Value in [kgu^2/s^2]
*/
from_Nm(x) { return x/mec.m_u/mec.m_u; },
/**
* convert [N/m] => [kg/s^2] = [N/m] (spring rate)
* @return {number} Value in [N/m]
*/
to_N_m(x) { return x; },
/**
* convert [N/m] = [kg/s^2] => [kg/s^2]
* @return {number} Value in [kg/s^2]
*/
from_N_m(x) { return x; },
/**
* convert [kgu/m^2] => [kgm^2/s^2] = [J]
* @return {number} Value in [N]
*/
to_J(x) { return mec.to_Nm(x) },
/**
* convert [J] = [kgm^2/s^2] => [kgu^2/s^2]
* @return {number} Value in [kgu^2/s^2]
*/
from_J(x) { return mec.from_Nm(x) },
/**
* convert [kgu^2] => [kgm^2]
* @return {number} Value in [kgm^2]
*/
to_kgm2(x) { return x*mec.m_u*mec.m_u; },
/**
* convert [kgm^2] => [kgu^2]
* @return {number} Value in [kgu^2]
*/
from_kgm2(x) { return x/mec.m_u/mec.m_u; },
/**
* Helper functions
*/
/**
* Test, if the absolute value of a number `a` is smaller than eps.
* @param {number} a Value to test.
* @param {number} [eps=mec.EPS] used epsilon.
* @returns {boolean} test result.
*/
isEps(a,eps) {
return a < (eps || mec.EPS) && a > -(eps || mec.EPS);
},
/**
* If the absolute value of a number `a` is smaller than eps, it is set to zero.
* @param {number} a Value to test.
* @param {number} [eps=mec.EPS] used epsilon.
* @returns {number} original value or zero.
*/
toZero(a,eps) {
return a < (eps || mec.EPS) && a > -(eps || mec.EPS) ? 0 : a;
},
/**
* Clamps a numerical value linearly within the provided bounds.
* @param {number} val Value to clamp.
* @param {number} lo Lower bound.
* @param {number} hi Upper bound.
* @returns {number} Value within the bounds.
*/
clamp(val,lo,hi) { return Math.min(Math.max(val, lo), hi); },
/**
* Clamps a numerical value asymptotically within the provided bounds.
* @param {number} val Value to clamp.
* @param {number} lo Lower bound.
* @param {number} hi Upper bound.
* @returns {number} Value within the bounds.
*/
asympClamp(val,lo,hi) {
const dq = hi - lo;
return dq ? lo + 0.5*dq + Math.tanh(((Math.min(Math.max(val, lo), hi) - lo)/dq - 0.5)*5)*0.5*dq : lo;
},
/**
* Convert angle from degrees to radians.
* @param {number} deg Angle in degrees.
* @returns {number} Angle in radians.
*/
toRad(deg) { return deg*Math.PI/180; },
/**
* Convert angle from radians to degrees.
* @param {number} rad Angle in radians.
* @returns {number} Angle in degrees.
*/
toDeg(rad) { return rad/Math.PI*180; },
/**
* Continuously rotating objects require infinite angles, both positives and negatives.
* Setting an angle `winf` to a new angle `w` does this with respect to the
* shortest angular distance from `winf` to `w`.
* @param {number} winf infinite extensible angle in radians.
* @param {number} w Destination angle in radians [-pi,pi].
* @returns {number} Extended angle in radians.
*/
infAngle(winf, w) {
let pi = Math.PI, pi2 = 2*pi, d = w - winf % pi2;
if (d > pi) d -= pi2;
else if (d < -pi) d += pi2;
return winf + d;
},
/**
* Mixin a set of prototypes into a primary object.
* @param {object} obj Primary object.
* @param {objects} ...protos Set of prototype objects.
*/
mixin(obj, ...protos) {
protos.forEach(proto => {
obj = Object.defineProperties(obj, Object.getOwnPropertyDescriptors(proto))
})
return obj;
},
/**
* Assign getters to an objects prototype.
* @param {object} obj Primary object.
* @param {objects} ...protos Set of prototype objects.
*/
assignGetters(obj,getters) {
for (const key in getters)
Object.defineProperty(obj, key, { get: getters[key], enumerable:true, configurable:true });
},
/**
* Create message string from message object.
* @param {object} msg message/warning/error object.
* @returns {string} message string.
*/
messageString(msg) {
const entry = mec.msg[mec.lang][msg.mid];
return entry ? msg.mid[0]+': '+entry(msg) : '';
}
}
/**
* mec.node (c) 2018-19 Stefan Goessner
* @license MIT License
* @requires mec.core.js
* @requires mec.model.js
* @requires g2.js
*/
"use strict";
/**
* Wrapper class for extending plain node objects, usually coming from JSON strings.
* @method
* @returns {object} load object.
* @param {object} - plain javascript load object.
* @property {string} id - node id.
* @property {number} x - x-coordinate.
* @property {number} y - y-coordinate.
* @property {number} [m=1] - mass.
* @property {boolean} [base=false] - specify node as base node.
*/
mec.node = {
extend(node) { Object.setPrototypeOf(node, this.prototype); node.constructor(); return node; },
prototype: {
constructor() { // always parameterless .. !
this.x = this.x || 0;
this.y = this.y || 0;
this.x0 = this.x;
this.y0 = this.y;
this.xt = this.yt = 0;
this.xtt = this.ytt = 0;
this.dxt = this.dyt = 0;
this.Qx = this.Qy = 0; // sum of external loads ...
},
/**
* Check node properties for validity.
* @method
* @param {number} idx - index in node array.
* @returns {boolean | object} false - if no error was detected, error object otherwise.
*/
validate(idx) {
if (!this.id)
return { mid:'E_ELEM_ID_MISSING',elemtype:'node',idx };
if (this.model.elementById(this.id) !== this)
return { mid:'E_ELEM_ID_AMBIGIOUS', id:this.id };
if (typeof this.m === 'number' && mec.isEps(this.m) )
return { mid:'E_NODE_MASS_TOO_SMALL', id:this.id, m:this.m };
return false;
},
/**
* Initialize node. Multiple initialization allowed.
* @method
* @param {object} model - model parent.
* @param {number} idx - index in node array.
*/
init(model, idx) {
this.model = model;
if (!this.model.notifyValid(this.validate(idx))) return;
// make inverse mass to first class citizen ...
this.im = typeof this.m === 'number' ? 1/this.m
: this.base === true ? 0
: 1;
// ... and mass / base to getter/setter
Object.defineProperty(this,'m',{ get: () => 1/this.im,
set: (m) => this.im = 1/m,
enumerable:true, configurable:true });
Object.defineProperty(this,'base',{ get: () => this.im === 0,
set: (q) => this.im = q ? 0 : 1,
enumerable:true, configurable:true });
this.g2cache = false;
},
// kinematics
// current velocity state .. only used during iteration.
get xtcur() { return this.xt + this.dxt },
get ytcur() { return this.yt + this.dyt },
// inverse mass
get type() { return 'node' }, // needed for ... what .. ?
get dof() { return this.m === Number.POSITIVE_INFINITY ? 0 : 2 },
/**
* Test, if node is not resting
* @const
* @type {boolean}
*/
get isSleeping() {
return this.base
|| mec.isEps(this.xt,mec.velTol)
&& mec.isEps(this.yt,mec.velTol)
&& mec.isEps(this.xtt,mec.velTol/this.model.timer.dt)
&& mec.isEps(this.ytt,mec.velTol/this.model.timer.dt);
},
/**
* Energy [kgu^2/s^2]
*/
get energy() {
var e = 0;
if (!this.base) {
if (this.model.hasGravity)
e += this.m*(-(this.x-this.x0)*mec.from_m(this.model.gravity.x) - (this.y-this.y0)*mec.from_m(this.model.gravity.y));
e += 0.5*this.m*(this.xt**2 + this.yt**2);
}
return e;
},
/**
* Check node for dependencies on another element.
* @method
* @param {object} elem - element to test dependency for.
* @returns {boolean} always false.
*/
dependsOn(elem) { return false; },
/**
* Check node for deep (indirect) dependencies on another element.
* @method
* @param {object} elem - element to test dependency for.
* @returns {boolean} dependency exists.
*/
deepDependsOn(elem) {
return elem === this;
},
reset() {
if (!this.base) {
this.x = this.x0;
this.y = this.y0;
}
// resetting kinematic values ...
this.xt = this.yt = 0;
this.xtt = this.ytt = 0;
this.dxt = this.dyt = 0;
},
/**
* First step of node pre-processing.
* Zeroing out node forces and differential velocities.
* @method
*/
pre_0() {
this.Qx = this.Qy = 0;
this.dxt = this.dyt = 0;
},
/**
* Second step of node pre-processing.
* @method
* @param {number} dt - time increment [s].
* @returns {boolean} dependency exists.
*/
pre(dt) {
// apply optional gravitational force
if (!this.base && this.model.hasGravity) {
this.Qx += this.m*mec.from_m(this.model.gravity.x);
this.Qy += this.m*mec.from_m(this.model.gravity.y);
}
// semi-implicite Euler step ... !
this.dxt += this.Qx*this.im * dt;
this.dyt += this.Qy*this.im * dt;
// increasing velocity is done dynamically and implicitly by using `xtcur, ytcur` during iteration ...
// increase positions using previously incremented velocities ... !
// x = x0 + (dx/dt)*dt + 1/2*(dv/dt)*dt^2
this.x += (this.xt + 1.5*this.dxt)*dt;
this.y += (this.yt + 1.5*this.dyt)*dt;
},
/**
* Node post-processing.
* @method
* @param {number} dt - time increment [s].
* @returns {boolean} dependency exists.
*/
post(dt) {
// update velocity from `xtcur, ytcur`
this.xt += this.dxt;
this.yt += this.dyt;
// get accelerations from velocity differences...
this.xtt = this.dxt/dt;
this.ytt = this.dyt/dt;
},
asJSON() {
return '{ "id":"'+this.id+'","x":'+this.x0+',"y":'+this.y0
+ (this.base ? ',"base":true' : '')
+ (this.idloc ? ',"idloc":"'+this.idloc+'"' : '')
+ ' }';
},
// analysis getters
get force() { return {x:this.Qx,y:this.Qy}; },
get pos() { return {x:this.x,y:this.y}; },
get vel() { return {x:this.xt,y:this.yt}; },
get acc() { return {x:this.xtt,y:this.ytt}; },
get forceAbs() { return Math.hypot(this.Qx,this.Qy); },
get velAbs() { return Math.hypot(this.xt,this.yt); },
get accAbs() { return Math.hypot(this.xtt,this.ytt); },
// interaction
get showInfo() {
return this.state & g2.OVER;
},
get infos() {
return {
'id': () => `'${this.id}'`,
'pos': () => `p=(${this.x.toFixed(0)},${this.y.toFixed(0)})`,
'vel': () => `v=(${mec.to_m(this.xt).toFixed(2)},${mec.to_m(this.yt).toFixed(2)})m/s`,
'm': () => `m=${this.m}`
}
},
info(q) { const i = this.infos[q]; return i ? i() : '?'; },
// _info() { return `x:${this.x.toFixed(1)}<br>y:${this.y.toFixed(1)}` },
hitInner({x,y,eps}) {
return g2.isPntInCir({x,y},this,eps);
},
selectBeg({x,y,t}) { },
selectEnd({x,y,t}) {
if (!this.base) {
this.xt = this.yt = this.xtt = this.ytt = 0;
}
},
drag({x,y,mode}) {
if (mode === 'edit' && !this.base) { this.x0 = x; this.y0 = y; }
else { this.x = x; this.y = y; }
},
// graphics ...
get isSolid() { return true },
get sh() { return this.state & g2.OVER ? [0, 0, 10, this.model.env.show.hoveredElmColor]
: this.state & g2.EDIT ? [0, 0, 10, this.model.env.show.selectedElmColor]
: false; },
get r() { return mec.node.radius; },
g2() {
const g = g2().use({grp: this.base ? mec.node.g2BaseNode
: mec.node.g2Node, x:this.x, y:this.y, sh:this.sh});
if (this.model.env.show.nodeLabels) {
const loc = mec.node.locdir[this.idloc || 'n'];
g.txt({str:this.id||'?',
x: this.x + 3*this.r*loc[0],
y: this.y + 3*this.r*loc[1],
thal:'center',tval:'middle',
ls:this.model.env.show.txtColor});
}
return g;
},
draw(g) {
if (this.model.env.show.nodes)
g.ins(this);
}
},
radius: 5,
locdir: { e:[ 1,0],ne:[ Math.SQRT2/2, Math.SQRT2/2],n:[0, 1],nw:[-Math.SQRT2/2, Math.SQRT2/2],
w:[-1,0],sw:[-Math.SQRT2/2,-Math.SQRT2/2],s:[0,-1],se:[ Math.SQRT2/2,-Math.SQRT2/2] },
g2BaseNode: g2().cir({x:0,y:0,r:5,ls:"@nodcolor",fs:"@nodfill"})
.p().m({x:0,y:5}).a({dw:Math.PI/2,x:-5,y:0}).l({x:5,y:0})
.a({dw:-Math.PI/2,x:0,y:-5}).z().fill({fs:"@nodcolor"}),
g2Node: g2().cir({x:0,y:0,r:5,ls:"@nodcolor",fs:"@nodfill"})
}
/**
* mec.constraint (c) 2018 Stefan Goessner
* @license MIT License
* @requires mec.core.js
* @requires mec.node.js
* @requires mec.drive.js
* @requires mec.model.js
* @requires g2.js
*/
"use strict";
/**
* Wrapper class for extending plain constraint objects, usually coming from JSON objects.
* @method
* @returns {object} constraint object.
* @param {object} - plain javascript constraint object.
* @property {string} id - constraint id.
* @property {string|number} [idloc='left'] - label location ['left','right',-1..1]
* @property {string} p1 - first point id.
* @property {string} p2 - second point id.
* @property {object} [ori] - orientation object.
* @property {string} [ori.type] - type of orientation constraint ['free'|'const'|'drive'].
* @property {number} [ori.w0] - initial angle [rad].
* @property {string} [ori.ref] - referenced constraint id.
* @property {string} [ori.passive] - no impulses back to referenced value (default: `false`).
* @property {string} [ori.reftype] - referencing other orientation or length value ['ori'|'len'].
* @property {number} [ori.ratio] - ratio to referencing value.
* @property {string} [ori.func] - drive function name from `mec.drive` object ['linear'|'quadratic', ...].
* If the name points to a function in `mec.drive` (not an object as usual)
* it will be called with `ori.arg` as an argument.
* @property {string} [ori.arg] - drive function argument.
* @property {number} [ori.t0] - drive parameter start value.
* @property {number} [ori.Dt] - drive parameter value range.
* @property {number} [ori.Dw] - drive angular range [rad].
* @property {boolean} [ori.bounce=false] - drive oscillate between drivestart and driveend.
* @property {number} [ori.repeat] - drive parameter scaling Dt.
* @property {boolean} [ori.input=false] - drive flags for actuation via an existing range-input with the same id.
* @property {object} [len] - length object.
* @property {string} [len.type] - type of length constraint ['free'|'const'|'ref'|'drive'].
* @property {number} [len.r0] - initial length.
* @property {string} [len.ref] - referenced constraint id.
* @property {string} [len.passive] - no impulses back to referenced value (default: `false`).
* @property {string} [len.reftype] - referencing other orientation or length value ['ori'|'len'].
* @property {number} [len.ratio] - ratio to referencing value.
* @property {string} [len.func] - drive function name ['linear'|'quadratic', ...].
* @property {string} [len.arg] - drive function argument.
* @property {number} [len.t0] - drive parameter start value.
* @property {number} [len.Dt] - drive parameter value range.
* @property {number} [len.Dr] - drive linear range.
* @property {boolean} [len.bounce=false] - drive oscillate between drivestart and driveend.
* @property {number} [len.repeat] - drive parameter scaling Dt.
* @property {boolean} [len.input=false] - drive flags for actuation via an existing range-input with the same id.
*/
mec.constraint = {
extend(c) { Object.setPrototypeOf(c, this.prototype); c.constructor(); return c; },
prototype: {
constructor() {}, // always parameterless .. !
/**
* Check constraint properties for validity.
* @method
* @param {number} idx - index in constraint array.
* @returns {boolean | object} true - if no error was detected, error object otherwise.
*/
validate(idx) {
let tmp, warn = false;
if (!this.id)
return { mid:'E_ELEM_ID_MISSING',elemtype:'constraint',idx };
if (this.model.elementById(this.id) !== this)
return { mid:'E_ELEM_ID_AMBIGIOUS', id:this.id };
if (!this.p1)
return { mid:'E_CSTR_NODE_MISSING', id:this.id, loc:'start', p:'p1' };
if (!this.p2)
return { mid:'E_CSTR_NODE_MISSING', id:this.id, loc:'end', p:'p2' };
if (typeof this.p1 === 'string') {
if (!(tmp=this.model.nodeById(this.p1)))
return { mid:'E_CSTR_NODE_NOT_EXISTS', id:this.id, loc:'start', p:'p1', nodeId:this.p1 };
else
this.p1 = tmp;
}
if (typeof this.p2 === 'string') {
if (!(tmp=this.model.nodeById(this.p2)))
return { mid:'E_CSTR_NODE_NOT_EXISTS', id:this.id, loc:'end', p:'p2', nodeId:this.p2 };
else
this.p2 = tmp;
}
if (mec.isEps(this.p1.x - this.p2.x) && mec.isEps(this.p1.y - this.p2.y))
warn = { mid:'W_CSTR_NODES_COINCIDE', id:this.id, p1:this.p1.id, p2:this.p2.id };
if (!this.hasOwnProperty('ori'))
this.ori = { type:'free' };
if (!this.hasOwnProperty('len'))
this.len = { type:'free' };
if (!this.ori.hasOwnProperty('type'))
this.ori.type = 'free';
if (!this.len.hasOwnProperty('type'))
this.len.type = 'free';
if (typeof this.ori.ref === 'string') {
if (!(tmp=this.model.constraintById(this.ori.ref)))
return { mid:'E_CSTR_REF_NOT_EXISTS', id:this.id, sub:'ori', ref:this.ori.ref };
else
this.ori.ref = tmp;
if (this.ori.type === 'drive') {
if (this.ori.ref[this.ori.reftype || 'ori'].type === 'free')
return { mid:'E_CSTR_DRIVEN_REF_TO_FREE', id:this.id, sub:'ori', ref:this.ori.ref.id, reftype:this.ori.reftype || 'ori' };
if (this.ratio !== undefined && this.ratio !== 1)
return { mid:'E_CSTR_RATIO_IGNORED', id:this.id, sub:'ori', ref:this.ori.ref.id, reftype:this.ori.reftype || 'ori' };
}
}
if (typeof this.len.ref === 'string') {
if (!(tmp=this.model.constraintById(this.len.ref)))
return { mid:'E_CSTR_REF_NOT_EXISTS', id:this.id, sub:'len', ref:this.len.ref };
else
this.len.ref = tmp;
if (this.len.type === 'drive') {
if (this.len.ref[this.len.reftype || 'len'].type === 'free')
return { mid:'E_CSTR_DRIVEN_REF_TO_FREE', id:this.id, sub:'len', ref:this.len.ref.id, reftype:this.len.reftype || 'len' };
if (this.ratio !== undefined && this.ratio !== 1)
return { mid:'E_CSTR_RATIO_IGNORED', id:this.id, sub:'len', ref:this.ori.ref.id, reftype:this.ori.reftype || 'len' };
}
}
return warn;
},
/**
* Initialize constraint. Multiple initialization allowed.
* @method
* @param {object} model - model parent.
* @param {number} idx - index in constraint array.
*/
init(model, idx) {
this.model = model;
if (!this.model.notifyValid(this.validate(idx))) return;
const ori = this.ori, len = this.len;
// initialize absolute magnitude and orientation
this.initVector();
this._angle = 0; // infinite extensible angle
if (ori.type === 'free') this.init_ori_free(ori);
else if (ori.type === 'const') this.init_ori_const(ori);
else if (ori.type === 'drive') this.init_ori_drive(ori);
if (len.type === 'free') this.init_len_free(len);
else if (len.type === 'const') this.init_len_const(len);
else if (len.type === 'drive') this.init_len_drive(len);
// trigonometric cache
this._angle = this.w0;
this.sw = Math.sin(this.w); this.cw = Math.cos(this.w);
// lagrange identifiers
this.lambda_r = this.dlambda_r = 0;
this.lambda_w = this.dlambda_w = 0;
},
/**
* Init vector magnitude and orientation.
* Referenced constraints can be assumed to be already initialized here.
*/
/*
initVector() {
const correctLen = this.len.hasOwnProperty('r0') && !this.len.hasOwnProperty('ref'),
correctOri = this.ori.hasOwnProperty('w0') && !this.ori.hasOwnProperty('ref');
this.r0 = correctLen ? this.len.r0 : Math.hypot(this.ay,this.ax);
this.w0 = correctOri ? this.ori.w0 : Math.atan2(this.ay,this.ax);
if (correctLen || correctOri) {
this.p2.x = this.p1.x + this.r0*Math.cos(this.w0);
this.p2.y = this.p1.y + this.r0*Math.sin(this.w0);
}
},
*/
initVector() {
let correctLen = false, correctOri = false;
if (this.len.hasOwnProperty('r0')) {
this.r0 = this.len.r0; // presume absolute value ...
correctLen = true;
if (this.len.hasOwnProperty('ref')) { // relative ...
if (this.len.reftype !== 'ori') // reftype === 'len'
this.r0 += (this.len.ratio||1)*this.len.ref.r0; // interprete as relative delta len ...
// else // todo ...
}
}
else
this.r0 = Math.hypot(this.ay,this.ax);
if (this.ori.hasOwnProperty('w0')) {
this.w0 = this.ori.w0; // presume absolute value ...
correctOri = true;
if (this.ori.hasOwnProperty('ref')) { // relative ...
if (this.ori.reftype !== 'len') // reftype === 'ori'
this.w0 += (this.ori.ratio||1)*this.ori.ref.w0; // interprete as relative delta angle ...
// else // todo ...
}
}
else
this.w0 = Math.atan2(this.ay,this.ax);
if (correctLen || correctOri) {
this.p2.x = this.p1.x + this.r0*Math.cos(this.w0);
this.p2.y = this.p1.y + this.r0*Math.sin(this.w0);
}
},
/**
* Track unlimited angle
*/
angle(w) {
return this._angle = mec.infAngle(this._angle,w);
},
/**
* Reset constraint
*/
reset() {
this.initVector();
this._angle = this.w0;
this.lambda_r = this.dlambda_r = 0;
this.lambda_w = this.dlambda_w = 0;
},
get type() {
const ori = this.ori, len = this.len;
return ori.type === 'free' && len.type === 'free' ? 'free'
: ori.type === 'free' && len.type !== 'free' ? 'rot'
: ori.type !== 'free' && len.type === 'free' ? 'tran'
: ori.type !== 'free' && len.type !== 'free' ? 'ctrl'
: 'invalid';
},
get initialized() { return this.model !== undefined },
get dof() {
return (this.ori.type === 'free' ? 1 : 0) +
(this.len.type === 'free' ? 1 : 0);
},
get lenTol() { return mec.tol.len[this.model.tolerance]; },
// analysis getters
/**
* Force value in [N]
*/
get force() { return -this.lambda_r; },
get forceAbs() { return -this.lambda_r; }, // deprecated !
/**
* Moment value in [N*u]
*/
get moment() { return -this.lambda_w*this.r; },
/**
* Instantaneous centre of velocity
*/
get pole() {
return { x:this.p1.x-this.p1.yt/this.wt, y:this.p1.y+this.p1.xt/this.wt };
},
get velPole() { return this.pole; },
/**
* Inflection pole
*/
get inflPole() {
return {
x:this.p1.x + this.p1.xtt/this.wt**2-this.wtt/this.wt**3*this.p1.xt,
y:this.p1.y + this.p1.ytt/this.wt**2-this.wtt/this.wt**3*this.p1.yt
};
},
/**
* Acceleration pole
*/
get accPole() {
const wt2 = this.wt**2,
wtt = this.wtt,
den = wtt**2 + wt2**2;
return {
x:this.p1.x + (wt2*this.p1.xtt - wtt*this.p1.ytt)/den,
y:this.p1.y + (wt2*this.p1.ytt + wtt*this.p1.xtt)/den
};
},
/**
* Number of active drives.
* @method
* @param {number} t - current time.
* @returns {int} Number of active drives.
*/
activeDriveCount(t) {
let ori = this.ori, len = this.len, drvCnt = 0;
if (ori.type === 'drive' && (ori.input || t <= ori.t0 + ori.Dt*(ori.bounce ? 2 : 1)*(ori.repeat || 1) + 0.5*this.model.timer.dt))
++drvCnt;
if (len.type === 'drive' && (len.input || t <= len.t0 + len.Dt*(len.bounce ? 2 : 1)*(len.repeat || 1) + 0.5*this.model.timer.dt))
++drvCnt;
return drvCnt;
},
/**
* Check constraint for dependencies on another element.
* @method
* @param {object} elem - element to test dependency for.
* @returns {boolean} dependency exists.
*/
dependsOn(elem) {
return this.p1 === elem
|| this.p2 === elem
|| this.ori && this.ori.ref === elem
|| this.len && this.len.ref === elem;
},
/**
* Check constraint for deep (indirect) dependency on another element.
* @method
* @param {object} elem - element to test deep dependency for.
* @returns {boolean} dependency exists.
*/
/*
deepDependsOn(target) {
return this === target
|| this.dependsOn(target)
|| this.model.deepDependsOn(this.p1,target)
|| this.model.deepDependsOn(this.p2,target)
|| this.ori && this.model.deepDependsOn(this.ori.ref,target)
|| this.len && this.model.deepDependsOn(this.len.ref,target);
},
*/
// privates
get ax() { return this.p2.x - this.p1.x },
get ay() { return this.p2.y - this.p1.y },
get axt() { return this.p2.xtcur - this.p1.xtcur },
get ayt() { return this.p2.ytcur - this.p1.ytcur },
get axtt() { return this.p2.xtt - this.p1.xtt },
get aytt() { return this.p2.ytt - this.p1.ytt },
// default orientational constraint equations
get ori_C() { return this.ay*this.cw - this.ax*this.sw; },
get ori_Ct() { return this.ayt*this.cw - this.axt*this.sw - this.wt*this.r; },
get ori_mc() {
const imc = mec.toZero(this.p1.im + this.p2.im);
return imc ? 1/imc : 0;
},
// default magnitude constraint equations
get len_C() { return this.ax*this.cw + this.ay*this.sw - this.r; },
get len_Ct() { return this.axt*this.cw + this.ayt*this.sw - this.rt; },
get len_mc() {
let imc = mec.toZero(this.p1.im + this.p2.im);
return (imc ? 1/imc : 0);
},
/**
* Perform preprocess step.
* @param {number} dt - time increment.
*/
pre(dt) {
let w = this.w;
// perfect location to update trig. cache
this.cw = Math.cos(w);
this.sw = Math.sin(w);
// apply angular impulse (warmstarting)
this.ori_impulse_vel(this.lambda_w * dt);
// apply axial impulse (warmstarting)
this.len_impulse_vel(this.lambda_r * dt);
this.dlambda_r = this.dlambda_w = 0; // important !!
},
post(dt) {
// apply angular impulse Q = J^T * lambda
this.lambda_w += this.dlambda_w;
this.ori_apply_Q(this.lambda_w)
// apply radial impulse Q = J^T * lambda
this.lambda_r += this.dlambda_r;
this.len_apply_Q(this.lambda_r)
},
/**
* Perform position step.
*/
posStep() {
let res, w = this.w;
// perfect location to update trig. cache
this.cw = Math.cos(w);
this.sw = Math.sin(w);
return this.type === 'free' ? true
: this.type === 'rot' ? this.len_pos()
: this.type === 'tran' ? this.ori_pos()
: this.type === 'ctrl' ? (res = this.ori_pos(), (this.len_pos() && res))
: false;
},
/**
* Perform velocity step.
*/
velStep(dt) {
let res;
return this.type === 'free' ? true
: this.type === 'rot' ? this.len_vel(dt)
: this.type === 'tran' ? this.ori_vel(dt)
: this.type === 'ctrl' ? (res = this.ori_vel(dt), (this.len_vel(dt) && res))
: false;
},
/**
* Calculate orientation.
*/
ori_pos() {
const C = this.ori_C, impulse = -this.ori_mc * C;
this.ori_impulse_pos(impulse);
if (this.ori.ref && !this.ori.passive) {
const ref = this.ori.ref;
if (this.ori.reftype === 'len')
ref.len_impulse_pos(-(this.ori.ratio||1)*impulse);
else
ref.ori_impulse_pos(-(this.ori.ratio||1)*this.r/ref.r*impulse);
}
return mec.isEps(C, mec.lenTol); // orientation constraint satisfied .. !
},
/**
* Calculate orientational velocity.
* @param {dt} - time increment.
*/
ori_vel(dt) {
const Ct = this.ori_Ct, impulse = -this.ori_mc * Ct;
this.ori_impulse_vel(impulse);
this.dlambda_w += impulse/dt;
if (this.ori.ref && !this.ori.passive) {
const ref = this.ori.ref,
ratioimp = impulse*(this.ori.ratio || 1);
if (this.ori.reftype === 'len') {
ref.len_impulse_vel(-ratioimp);
ref.dlambda_r -= ratioimp/dt;
}
else {
const refimp = this.r/this.ori.ref.r*ratioimp;
ref.ori_impulse_vel(-refimp);
ref.dlambda_w -= refimp/dt;
}
}
// return Math.abs(impulse/dt) < mec.forceTol; // orientation constraint satisfied .. !
return mec.isEps(Ct*dt, mec.lenTol); // orientation constraint satisfied .. !
},
/**
* Apply pseudo impulse `impulse` from ori constraint to its node positions.
* 'delta q = -W * J^T * m_c * C'
* @param {number} impulse - pseudo impulse.
*/
ori_impulse_pos(impulse) {
this.p1.x += this.p1.im * this.sw * impulse;
this.p1.y += -this.p1.im * this.cw * impulse;
this.p2.x += -this.p2.im * this.sw * impulse;
this.p2.y += this.p2.im * this.cw * impulse;
},
/**
* Apply impulse `impulse` from ori constraint to its node displacements.
* 'delta dot q = -W * J^T * m_c * dot C'
* @param {number} impulse - impulse.
*/
ori_impulse_vel(impulse) {
this.p1.dxt += this.p1.im * this.sw * impulse;
this.p1.dyt += -this.p1.im * this.cw * impulse;
this.p2.dxt += -this.p2.im * this.sw * impulse;
this.p2.dyt += this.p2.im * this.cw * impulse;
},
/**
* Apply constraint force `lambda` from ori constraint to its nodes.
* 'Q_c = J^T * lambda'
* @param {number} lambda - moment.
*/
ori_apply_Q(lambda) {
this.p1.Qx += this.sw * lambda;
this.p1.Qy += -this.cw * lambda;
this.p2.Qx += -this.sw * lambda;
this.p2.Qy += this.cw * lambda;
},
/**
* Calculate length.
*/
len_pos() {
const C = this.len_C, impulse = -this.len_mc * C;
this.len_impulse_pos(impulse);
if (this.len.ref && !this.len.passive) {
if (this.len.reftype === 'ori')
this.len.ref.ori_impulse_pos(-(this.len.ratio||1)*impulse);
else
this.len.ref.len_impulse_pos(-(this.len.ratio||1)*impulse);
}
return mec.isEps(C, mec.lenTol); // length constraint satisfied .. !
},
/**
* Calculate length velocity.
* @param {number} dt - time increment.
*/
len_vel(dt) {
const Ct = this.len_Ct, impulse = -this.len_mc * Ct;
this.len_impulse_vel(impulse);
this.dlambda_r += impulse/dt;
if (this.len.ref && !this.len.passive) {
const ref = this.len.ref,
ratioimp = impulse*(this.ori.ratio || 1);
if (this.len.reftype === 'ori') {
ref.ori_impulse_vel(-ratioimp);
ref.dlambda_w -= ratioimp/dt;
}
else {
ref.len_impulse_vel(-ratioimp);
ref.dlambda_r -= ratioimp/dt;
}
}
return mec.isEps(Ct*dt, mec.lenTol); // velocity constraint satisfied .. !
},
/**
* Apply pseudo impulse `impulse` from len constraint to its node positions.
* 'delta q = -W * J^T * m_c * C'
* @param {number} impulse - pseudo impulse.
*/
len_impulse_pos(impulse) {
this.p1.x += -this.p1.im * this.cw * impulse;
this.p1.y += -this.p1.im * this.sw * impulse;
this.p2.x += this.p2.im * this.cw * impulse;
this.p2.y += this.p2.im * this.sw * impulse;
},
/**
* Apply impulse `impulse` from len constraint to its node displacements.
* 'delta dot q = -W * J^T * m_c * dot C'
* @param {number} impulse - impulse.
*/
len_impulse_vel(impulse) {
this.p1.dxt += -this.p1.im * this.cw * impulse;
this.p1.dyt += -this.p1.im * this.sw * impulse;
this.p2.dxt += this.p2.im * this.cw * impulse;
this.p2.dyt += this.p2.im * this.sw * impulse;
},
/**
* Apply force `lambda` from len constraint to its node forces.
* 'Q_c = J^T * lambda'
* @param {number} lambda - force.
*/
len_apply_Q(lambda) {
this.p1.Qx += -this.cw * lambda;
this.p1.Qy += -this.sw * lambda;
this.p2.Qx += this.cw * lambda;
this.p2.Qy += this.sw * lambda;
},
/**
* Initialize a free orientation constraint.
* @param {object} ori - orientational sub-object.
*/
init_ori_free(ori) {
this.w0 = ori.hasOwnProperty('w0') ? ori.w0 : this.angle(Math.atan2(this.ay,this.ax));
mec.assignGetters(this, {
w: () => this.angle(Math.atan2(this.ay,this.ax)),
wt: () => (this.ayt*this.cw - this.axt*this.sw)/this.r,
wtt:() => (this.aytt*this.cw - this.axtt*this.sw)/this.r
});
},
/**
* Initialize a const orientation constraint.
* @param {object} ori - orientational sub-object.
*/
init_ori_const(ori) {
if (!!ori.ref) {
const ref = ori.ref = this.model.constraintById(ori.ref) || ori.ref,
reftype = ori.reftype || 'ori',
ratio = ori.ratio || 1;
if (!ref.initialized)
ref.init(this.model); // 'idx' argument not necessary here !
if (reftype === 'ori') {
const w0 = ori.hasOwnProperty('w0') ? ref.w0 + ori.w0 : Math.atan2(this.ay,this.ax);
mec.assignGetters(this, {
w: () => w0 + ratio*(ref.w - ref.w0),
wt: () => ratio*ref.wt,
wtt:() => ratio*ref.wtt,
ori_C: () => this.ay*this.cw - this.ax*this.sw - ratio*this.r/(ref.r||1)*(ref.ay*ref.cw - ref.ax*ref.sw),
ori_Ct: () => this.ayt*this.cw - this.axt*this.sw - ratio*this.r/(ref.r||1)*(ref.ayt*ref.cw - ref.axt*ref.sw),
ori_mc: () => {
let imc = mec.toZero(this.p1.im + this.p2.im) + ratio**2*this.r**2/(ref.r||1)**2*mec.toZero(ref.p1.im + ref.p2.im);
return imc ? 1/imc : 0;
}
});
}
else { // reftype === 'len'
const w0 = ori.hasOwnProperty('w0') ? ref.w0 + ori.w0 : Math.atan2(this.ay,this.ax);
mec.assignGetters(this, {
w: () => w0 + ratio*(ref.r - ref.r0)/this.r,
wt: () => ratio*ref.rt,
wtt:() => ratio*ref.rtt,
ori_C: () => this.r*(this.angle(Math.atan2(this.ay,this.ax)) - w0) - ratio*(ref.ax*ref.cw + ref.ay*ref.sw - ref.r0),
ori_Ct: () => this.ayt*this.cw - this.axt*this.sw - ratio*(ref.axt*ref.cw + ref.ayt*ref.sw),
ori_mc: () => {
let imc = mec.toZero(this.p1.im + this.p2.im) + ratio**2*mec.toZero(ref.p1.im + ref.p2.im);
return imc ? 1/imc : 0;
}
});
}
}
else {
mec.assignGetters(this, {
w: () => this.w0,
wt: () => 0,
wtt:() => 0,
});
}
},
/**
* Initialize a driven orientation constraint.
* @param {object} ori - orientational sub-object.
*/
init_ori_drive(ori) {
this.w0 = ori.hasOwnProperty('w0') ? ori.w0 : this.angle(Math.atan2(this.ay,this.ax));
ori.Dw = ori.Dw || 2*Math.PI;
ori.t0 = ori.t0 || 0;
ori.Dt = ori.Dt || 1;
if (ori.input) {
// maintain a local input controlled time 'local_t'.
ori.local_t = 0;
ori.t = () => !this.model.state.preview ? ori.local_t : this.model.timer.t;
ori.inputCallbk = (w) => { ori.local_t = w*Math.PI/180*ori.Dt/ori.Dw; };
}
else
ori.t = () => this.model.timer.t;
ori.drive = mec.drive.create({ func: ori.func || ori.input && 'static' || 'linear',
z0: ori.ref ? 0 : this.w0,
Dz: ori.Dw,
t0: ori.t0,
Dt: ori.Dt,
t: ori.t,
bounce: ori.bounce,
repeat: ori.repeat,
args: ori.args
});
if (!!ori.ref) {