UNPKG

microjam

Version:

Minimalistic JAMStack authoring and publishing environment

1,280 lines (1,232 loc) 189 kB
/** * 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) {