UNPKG

@zeainc/zea-kinematics

Version:

Kinematics extension for Zea Engine.

1,264 lines (1,253 loc) 67.4 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@zeainc/zea-engine'), require('@zeainc/zea-ux')) : typeof define === 'function' && define.amd ? define(['exports', '@zeainc/zea-engine', '@zeainc/zea-ux'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.zeaKinematics = {}, global.zeaEngine, global.zeaUx)); })(this, (function (exports, zeaEngine, zeaUx) { 'use strict'; var name$1 = "@zeainc/zea-kinematics"; var libraryName = "ZeaKinematics"; var fileName = "zea-kinematics"; var author = "Philip Taylor"; var description = "Kinematics extension for Zea Engine."; var version = "4.0.4"; var license = "MIT"; var main = "dist/index.cjs.js"; var browser = "dist/index.esm.js"; var umd = "dist/index.umd.js"; var types = "dist/zea-kinematics.d.ts"; var files = [ "dist/" ]; var keywords = [ "WebGL", "ES6", "Zea" ]; var repository = { type: "git", url: "git+ssh://git@github.com:ZeaInc/zea-kinematics.git" }; var scripts = { build: "rollup -c", "build:watch": "rollup -c -w", dev: "npm-run-all --parallel build:watch start:watch", dist: "yarn publish --access=public", docs: "cp CHANGELOG.md docs/ && adg --config adg.config.json", "docs-w": "cp CHANGELOG.md docs/ && adg -w --config=adg.config.json", "docs:serve": "docsify serve docs/", generate: "plop", prepare: "yarn run build", release: "standard-version", start: "es-dev-server --cors", "start:watch": "es-dev-server --app-index testing-e2e/index.html --cors --open --watch", "test:e2e": "percy exec cypress run --browser chrome --headless", "test:e2e:watch": "percy exec cypress open" }; var devDependencies = { "@percy/cypress": "^2.3.2", "@rollup/plugin-commonjs": "^21.0.1", "@rollup/plugin-json": "^4.1.0", "@zeainc/jsdocs2md": "^0.0.7", "@zeainc/zea-engine": "^4", "@zeainc/zea-ux": "4.6.0-caa887e", cypress: "^5.4.0", "docsify-cli": "^4.4.1", "es-dev-server": "^1.57.8", eslint: "^6.5.1", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^6.3.0", "eslint-plugin-prettier": "^3.1.1", "npm-run-all": "^4.1.5", plop: "^2.7.4", prettier: "^2.1.1", rollup: "^2.60.1", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-dts": "^4.1.0", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-typescript": "^1.0.1", "standard-version": "^9.0.0", "ts-node": "^10.4.0", typescript: "^4.5.4", "worker-loader": "^2.0.0", yargs: "12.0.2" }; var dependencies = { }; var pkg = { name: name$1, libraryName: libraryName, fileName: fileName, author: author, description: description, version: version, license: license, main: main, browser: browser, umd: umd, "umd.min": "dist/index.umd.min.js", types: types, files: files, keywords: keywords, repository: repository, scripts: scripts, devDependencies: devDependencies, dependencies: dependencies }; /** Class representing an explode part parameter. * @extends StructParameter * @private */ class ExplodePartParameter extends zeaEngine.StructParameter { /** * Create an explode part parameter. * @param {string} name - The name value. */ constructor(name = '') { super(name); this.stageParam = new zeaEngine.NumberParameter('Stage', 0); this.axisParam = new zeaEngine.Vec3Parameter('Axis', new zeaEngine.Vec3(1, 0, 0)); this.movementParam = new zeaEngine.Vec2Parameter('MovementTiming', new zeaEngine.Vec2(0, 1), [new zeaEngine.Vec2(0, 0), new zeaEngine.Vec2(1, 1)]); this.multiplierParam = new zeaEngine.NumberParameter('Multiplier', 1.0); this.output = new zeaEngine.XfoOperatorOutput('output', zeaEngine.OperatorOutputMode.OP_READ_WRITE); this.addMember(this.stageParam); this.addMember(this.axisParam); // The Movement param enables fine level timing to be set per part. this.addMember(this.movementParam); this.addMember(this.multiplierParam); } /** * The getStage method. * @return {any} - The return value. */ getStage() { return this.stageParam.getValue(); } /** * The setStage method. * @param {any} stage - The stage value. */ setStage(stage) { this.stageParam.setValue(stage); } /** * The getOutput method. * @return {XfoOperatorOutput} - The return value. */ getOutput() { return this.output; } /** * The evaluate method. * @param {any} explode - The explode value. * @param {any} explodeDist - The distance that the parts explode to. * @param {any} offset - The offset value. * @param {any} stages - The stages value. * @param {any} cascade - In "cascade" mode, the parts move in a cascade. * @param {any} centered - The centered value. * @param {Xfo} parentXfo - The parentXfo value. * @param {any} parentDelta - The parentDelta value. */ evaluate(explode, explodeDist, offset, stages, cascade, centered, parentXfo, parentDelta) { // Note: during interactive setup of the operator we // can have evaluations before anhthing is connected. if (!this.output.isConnected()) return; const stage = this.stageParam.getValue(); const movement = this.movementParam.getValue(); let dist; if (cascade) { // In 'cascade' mode, the parts move in a cascade, // starting with stage 0. then 1 ... let t = stage / stages; if (centered) t -= 0.5; dist = explodeDist * zeaEngine.MathFunctions.linStep(movement.x, movement.y, Math.max(0, explode - t)); } else { // Else all the parts are spread out across the explode distance. let t = 1.0 - stage / stages; if (centered) t -= 0.5; dist = explodeDist * zeaEngine.MathFunctions.linStep(movement.x, movement.y, explode) * t; } dist += offset; let explodeDir = this.axisParam.getValue(); const multiplier = this.multiplierParam.getValue(); let xfo = this.output.getValue(); if (parentXfo) { xfo = parentDelta.multiply(xfo); explodeDir = parentXfo.ori.rotateVec3(explodeDir); } xfo.tr.addInPlace(explodeDir.scale(dist * multiplier)); this.output.setClean(xfo); } // //////////////////////////////////////// // Persistence /** * The toJSON method encodes this type as a json object for persistence. * @param {object} context - The context value. * @return {object} - Returns the json object. */ toJSON(context) { const j = super.toJSON(context); if (j) { j.output = this.output.toJSON(context); } return j; } /** * The fromJSON method decodes a json object for this type. * @param {object} j - The json object this item must decode. * @param {object} context - The context value. */ fromJSON(j, context) { super.fromJSON(j, context); if (j.output) { this.output.fromJSON(j.output, context); } } } /** Class representing an explode parts operator. * @extends ParameterOwner */ class ExplodePartsOperator extends zeaEngine.Operator { /** * Create an explode parts operator. * @param {string} name - The name value. */ constructor(name) { super(name); this.stagesParam = new zeaEngine.NumberParameter('Stages', 0); this.explodeParam = new zeaEngine.NumberParameter('Explode', 0.0, [0, 1]); this.distParam = new zeaEngine.NumberParameter('Dist', 1.0); this.offsetParam = new zeaEngine.NumberParameter('Offset', 0); this.cascadeParam = new zeaEngine.BooleanParameter('Cascade', false); this.centeredParam = new zeaEngine.BooleanParameter('Centered', false); this.parentItemParam = new zeaEngine.TreeItemParameter('RelativeTo'); this.itemsParam = new zeaEngine.ListParameter('Parts', typeof ExplodePartParameter); this.addParameter(this.stagesParam); this.addParameter(this.explodeParam); this.addParameter(this.distParam); this.addParameter(this.offsetParam); this.addParameter(this.cascadeParam); this.addParameter(this.centeredParam); this.addParameter(this.parentItemParam); this.parentItemParam.on('valueChanged', () => { // compute the local xfos const parentItem = this.parentItemParam.getValue(); if (parentItem) this.invParentSpace = parentItem.getParameter('GlobalXfo').getValue().inverse(); else this.invParentSpace = undefined; }); this.parentItemParam.on('treeItemGlobalXfoChanged', () => { this.setDirty(); }); this.addParameter(this.itemsParam); this.itemsParam.on('elementAdded', (event) => { if (event.index > 0) { const explodeParam = this.itemsParam.getElement(event.index - 1); const prevStage = explodeParam.getStage(); event.elem.setStage(prevStage + 1); this.stagesParam.setValue(prevStage + 2); } else { this.stagesParam.setValue(1); } event.elem.output = new zeaEngine.XfoOperatorOutput('Part' + event.index, zeaEngine.OperatorOutputMode.OP_READ_WRITE); this.addOutput(event.elem.getOutput()); this.setDirty(); }); this.itemsParam.on('elementRemoved', (event) => { this.removeOutput(event.elem.getOutput()); }); this.addParameter(this.itemsParam); this.localXfos = []; } addPart() { const part = new ExplodePartParameter(); this.itemsParam.addElement(part); return part; } /** * The evaluate method. */ evaluate() { // console.log(`Operator: evaluate: ${this.getName()}`) const stages = this.stagesParam.getValue(); const explode = this.explodeParam.getValue(); // const explodeDir = this.axisParam.getValue(); const explodeDist = this.distParam.getValue(); const offset = this.offsetParam.getValue(); const cascade = this.cascadeParam.getValue(); const centered = this.centeredParam.getValue(); const parentItem = this.parentItemParam.getValue(); let parentXfo; let parentDelta; if (parentItem) { parentXfo = parentItem.getParameter('GlobalXfo').getValue(); parentDelta = this.invParentSpace.multiply(parentXfo); } const items = this.itemsParam.getValue(); for (let i = 0; i < items.length; i++) { const part = items[i]; part.evaluate(explode, explodeDist, offset, stages, cascade, centered, parentXfo, parentDelta); } } // //////////////////////////////////////// // Persistence /** * The toJSON method encodes this type as a json object for persistence. * * @param {object} context - The context value. * @return {object} - Returns the json object. */ toJSON(context) { return super.toJSON(context); } /** * The fromJSON method decodes a json object for this type. * @param {object} j - The json object this item must decode. * @param {object} context - The context value. */ fromJSON(j, context) { super.fromJSON(j, context); } } zeaEngine.Registry.register('ExplodePartsOperator', ExplodePartsOperator); /** Class representing a gear parameter. * @extends StructParameter */ class GearParameter extends zeaEngine.StructParameter { /** * Create a gear parameter. * @param {string} name - The name value. */ constructor(name) { super(name); this.__ratioParam = new zeaEngine.NumberParameter('Ratio', 1.0); this.__offsetParam = new zeaEngine.NumberParameter('Offset', 0.0); this.__axisParam = new zeaEngine.Vec3Parameter('Axis', new zeaEngine.Vec3(1, 0, 0)); this.addMember(this.__ratioParam); this.addMember(this.__offsetParam); this.addMember(this.__axisParam); } /** * The getOutput method. * @return {any} - The return value. */ getOutput() { return this.__output; } /** * Getter for the gear ratio. * @return {number} - Returns the ratio. */ getRatio() { return this.__ratioParam.getValue(); } /** * getter for the gear offset. * @return {number} - Returns the offset. */ getOffset() { return this.__offsetParam.getValue(); } /** * The getAxis method. * @return {any} - The return value. */ getAxis() { return this.__axisParam.getValue(); } // //////////////////////////////////////// // Persistence /** * The toJSON method encodes this type as a json object for persistence. * @param {object} context - The context value. * @return {object} - Returns the json object. */ toJSON(context) { const j = super.toJSON(context); if (j) { j.output = this.__output.toJSON(context); } return j; } /** * The fromJSON method decodes a json object for this type. * @param {object} j - The json object this item must decode. * @param {object} context - The context value. */ fromJSON(j, context) { super.fromJSON(j, context); if (j.output) { this.__output.fromJSON(j.output, context); } } } /** * Class representing a gears operator. * * @extends Operator */ class GearsOperator extends zeaEngine.Operator { /** * Create a gears operator. * @param {string} name - The name value. */ constructor(name) { super(name); this.__revolutionsParam = new zeaEngine.NumberParameter('Revolutions', 0.0); this.rpmParam = new zeaEngine.NumberParameter('RPM', 0.0); this.__gearsParam = new zeaEngine.ListParameter('Gears', typeof GearParameter); this.addParameter(this.__revolutionsParam); this.addParameter(this.rpmParam); // revolutions per minute this.rpmParam.on('valueChanged', () => { const rpm = this.rpmParam.getValue(); if (Math.abs(rpm) > 0.0) { if (!this.__timeoutId) { const timerCallback = () => { const rpm = this.rpmParam.getValue(); const revolutions = this.__revolutionsParam.getValue(); this.__revolutionsParam.setValue(revolutions + rpm * (1 / (50 * 60))); this.__timeoutId = setTimeout(timerCallback, 20); // Sample at 50fps. }; timerCallback(); } } else { clearTimeout(this.__timeoutId); this.__timeoutId = undefined; } }); this.addParameter(this.__gearsParam); this.__gearsParam.on('elementAdded', (event) => { event.elem.__output = new zeaEngine.XfoOperatorOutput('Gear' + event.index, zeaEngine.OperatorOutputMode.OP_READ_WRITE); this.addOutput(event.elem.getOutput()); }); this.__gearsParam.on('elementRemoved', (event) => { this.removeOutput(event.index); }); this.__gears = []; } addGear() { const part = new GearParameter(); this.__gearsParam.addElement(part); return part; } /** * The evaluate method. */ evaluate() { // console.log(`Operator: evaluate: ${this.getName()}`) const revolutions = this.__revolutionsParam.getValue(); const gears = this.__gearsParam.getValue(); gears.forEach((gear, index) => { const output = this.getOutputByIndex(index); // Note: we have cases where we have interdependencies. // Operator A Writes to [A, B, C] // Operator B Writes to [A, B, C]. // During the load of operator B.C, we trigger an evaluation // of Operator A, which causes B to evaluate (due to B.A already connected) // Now operator B is evaluating will partially setup. // See SmartLoc: Exploded Parts and Gears read/write the same set of // params. if (!output.isConnected()) return; const rot = revolutions * gear.getRatio() + gear.getOffset(); const quat = new zeaEngine.Quat(); quat.setFromAxisAndAngle(gear.getAxis(), rot * Math.PI * 2.0); const xfo = output.getValue(); xfo.ori = quat.multiply(xfo.ori); output.setClean(xfo); }); } /** * The detach method. */ detach() { super.detach(); if (this.__timeoutId) { clearTimeout(this.__timeoutId); this.__timeoutId = null; } } /** * The reattach method. */ reattach() { super.reattach(); // Restart the operator. this.getParameter('RPM').emit('valueChanged', {}); } /** * The destroy is called by the system to cause explicit resources cleanup. * Users should never need to call this method directly. */ destroy() { if (this.__timeoutId) { clearTimeout(this.__timeoutId); this.__timeoutId = null; } //super.destroy() } } zeaEngine.Registry.register('GearsOperator', GearsOperator); /** An operator for aiming items at targets. * @extends Operator */ class AimOperator extends zeaEngine.Operator { /** * Create a gears operator. * @param {string} name - The name value. */ constructor(name) { super(name); this.weightParam = new zeaEngine.NumberParameter('Weight', 1); this.axisParam = new zeaEngine.MultiChoiceParameter('Axis', 0, ['+X Axis', '-X Axis', '+Y Axis', '-Y Axis', '+Z Axis', '-Z Axis']); this.stretchParam = new zeaEngine.NumberParameter('Stretch', 0.0); this.initialDistParam = new zeaEngine.NumberParameter('Initial Dist', 1.0); this.targetInput = new zeaEngine.XfoOperatorInput('Target'); this.xfoInputOutput = new zeaEngine.XfoOperatorOutput('InputOutput', zeaEngine.OperatorOutputMode.OP_READ_WRITE); this.addParameter(this.weightParam); this.addParameter(this.axisParam); this.addParameter(this.stretchParam); this.addParameter(this.initialDistParam); // this.addParameter(new XfoParameter('Target')) this.addInput(this.targetInput); this.addOutput(this.xfoInputOutput); } /** * The resetStretchRefDist method. */ resetStretchRefDist() { const target = this.targetInput.getValue(); const output = this.getOutputByIndex(0); const xfo = output.getValue(); const dist = target.tr.subtract(xfo.tr).length(); this.initialDistParam.value = dist; } /** * The evaluate method. */ evaluate() { const weight = this.weightParam.value; const axis = this.axisParam.value; const target = this.targetInput.getValue(); const output = this.getOutputByIndex(0); const xfo = output.getValue(); const dir = target.tr.subtract(xfo.tr); const dist = dir.length(); if (dist < 0.000001) return; dir.scaleInPlace(1 / dist); let vec; switch (axis) { case 0: vec = xfo.ori.getXaxis(); break; case 1: vec = xfo.ori.getXaxis().negate(); break; case 2: vec = xfo.ori.getYaxis(); break; case 3: vec = xfo.ori.getYaxis().negate(); break; case 4: vec = xfo.ori.getZaxis(); break; case 5: vec = xfo.ori.getZaxis().negate(); break; } let align = new zeaEngine.Quat(); align.setFrom2Vectors(vec, dir); align.alignWith(new zeaEngine.Quat()); if (weight < 1.0) align = new zeaEngine.Quat().lerp(align, weight); xfo.ori = align.multiply(xfo.ori); const stretch = this.stretchParam.value; if (stretch > 0.0) { const initialDist = this.initialDistParam.value; // Scale the output to reach towards the target. // Note: once the base xfo is re-calculated, then // we can make this scale relative. (e.g. *= sc) // This will happen once GalcGlibalXfo is the base // operator applied to GlobalXfo param. // Until then, we must reset scale manually here. const sc = 1.0 + (dist / initialDist - 1.0) * stretch; switch (axis) { case 0: case 1: xfo.sc.x = sc; break; case 2: case 3: xfo.sc.y = sc; break; case 4: case 5: xfo.sc.z = sc; break; } // console.log("AimOperator.evaluate:", xfo.sc.toString()) } output.setClean(xfo); } } zeaEngine.Registry.register('AimOperator', AimOperator); /** An operator for aiming items at targets. * @extends Operator */ class RamAndPistonOperator extends zeaEngine.Operator { /** * Create a gears operator. * @param {string} name - The name value. */ constructor(name) { super(name); this.axisParam = new zeaEngine.MultiChoiceParameter('Axis', 0, ['+X Axis', '-X Axis', '+Y Axis', '-Y Axis', '+Z Axis', '-Z Axis']); this.ramXfoOutput = new zeaEngine.XfoOperatorOutput('Ram', zeaEngine.OperatorOutputMode.OP_READ_WRITE); this.pistonXfoOutput = new zeaEngine.XfoOperatorOutput('Piston', zeaEngine.OperatorOutputMode.OP_READ_WRITE); this.addParameter(this.axisParam); this.addOutput(this.ramXfoOutput); this.addOutput(this.pistonXfoOutput); } /** * The evaluate method. */ evaluate() { const ramOutput = this.getOutputByIndex(0); const pistonOutput = this.getOutputByIndex(1); const ramXfo = ramOutput.getValue(); const pistonXfo = pistonOutput.getValue(); const axis = this.axisParam.value; const dir = pistonXfo.tr.subtract(ramXfo.tr); dir.normalizeInPlace(); const alignRam = new zeaEngine.Quat(); const alignPiston = new zeaEngine.Quat(); switch (axis) { case 0: alignRam.setFrom2Vectors(ramXfo.ori.getXaxis(), dir); alignPiston.setFrom2Vectors(pistonXfo.ori.getXaxis().negate(), dir); break; case 1: alignRam.setFrom2Vectors(ramXfo.ori.getXaxis().negate(), dir); alignPiston.setFrom2Vectors(pistonXfo.ori.getXaxis(), dir); break; case 2: alignRam.setFrom2Vectors(ramXfo.ori.getYaxis(), dir); alignPiston.setFrom2Vectors(pistonXfo.ori.getYaxis().negate(), dir); break; case 3: alignRam.setFrom2Vectors(ramXfo.ori.getYaxis().negate(), dir); alignPiston.setFrom2Vectors(pistonXfo.ori.getYaxis(), dir); break; case 4: alignRam.setFrom2Vectors(ramXfo.ori.getZaxis(), dir); alignPiston.setFrom2Vectors(pistonXfo.ori.getZaxis().negate(), dir); break; case 5: alignRam.setFrom2Vectors(ramXfo.ori.getZaxis().negate(), dir); alignPiston.setFrom2Vectors(pistonXfo.ori.getZaxis(), dir); break; } ramXfo.ori = alignRam.multiply(ramXfo.ori); pistonXfo.ori = alignPiston.multiply(pistonXfo.ori); ramOutput.setClean(ramXfo); pistonOutput.setClean(pistonXfo); } } zeaEngine.Registry.register('RamAndPistonOperator', RamAndPistonOperator); /** An operator for aiming items at targets. * @extends Operator */ class TriangleIKSolver extends zeaEngine.Operator { /** * Create a gears operator. * @param {string} name - The name value. */ constructor(name) { super(name); this.targetXfoInput = new zeaEngine.XfoOperatorInput('Target'); this.joint0XfoOutput = new zeaEngine.XfoOperatorOutput('Joint0', zeaEngine.OperatorOutputMode.OP_READ_WRITE); this.joint1XfoOutput = new zeaEngine.XfoOperatorOutput('Joint1', zeaEngine.OperatorOutputMode.OP_READ_WRITE); // this.addParameter( // new MultiChoiceParameter('Axis', 0, ['+X Axis', '-X Axis', '+Y Axis', '-Y Axis', '+Z Axis', '-Z Axis']) // ) this.addInput(this.targetXfoInput); this.addOutput(this.joint0XfoOutput); this.addOutput(this.joint1XfoOutput); this.align = new zeaEngine.Quat(); this.enabled = false; } enable() { const targetXfo = this.targetXfoInput.getValue(); const joint0Xfo = this.joint0XfoOutput.getValue(); const joint1Xfo = this.joint1XfoOutput.getValue(); this.joint1Offset = joint0Xfo.inverse().multiply(joint1Xfo).tr; this.joint1TargetOffset = joint1Xfo.inverse().multiply(targetXfo).tr; this.joint1TargetOffset.normalizeInPlace(); this.joint0Length = joint1Xfo.tr.distanceTo(joint0Xfo.tr); this.joint1Length = targetXfo.tr.distanceTo(joint1Xfo.tr); this.setDirty(); this.enabled = true; } /** * The evaluate method. */ evaluate() { const targetXfo = this.getInput('Target').getValue(); const joint0Output = this.joint0XfoOutput; const joint1Output = this.joint1XfoOutput; const joint0Xfo = joint0Output.getValue(); const joint1Xfo = joint1Output.getValue(); /////////////////////////////// // Calc joint0Xfo const joint0TargetVec = targetXfo.tr.subtract(joint0Xfo.tr); const joint0TargetDist = joint0TargetVec.length(); const joint01Vec = joint0Xfo.ori.rotateVec3(this.joint1Offset); // Calculate the angle using the rule of cosines. // cos C = (a2 + b2 − c2)/2ab const a = this.joint0Length; const b = joint0TargetDist; const c = this.joint1Length; const angle = Math.acos((a * a + b * b - c * c) / (2 * a * b)); // console.log(currAngle, angle) joint01Vec.normalizeInPlace(); joint0TargetVec.normalizeInPlace(); const Joint0Axis = joint0TargetVec.cross(joint01Vec); const currAngle = joint0TargetVec.angleTo(joint01Vec); Joint0Axis.normalizeInPlace(); this.align.setFromAxisAndAngle(Joint0Axis, angle - currAngle); joint0Xfo.ori = this.align.multiply(joint0Xfo.ori); /////////////////////////////// // Calc joint1Xfo joint1Xfo.tr = joint0Xfo.transformVec3(this.joint1Offset); const joint1TargetVec = targetXfo.tr.subtract(joint1Xfo.tr); joint1TargetVec.normalizeInPlace(); this.align.setFrom2Vectors(joint1Xfo.ori.rotateVec3(this.joint1TargetOffset), joint1TargetVec); joint1Xfo.ori = this.align.multiply(joint1Xfo.ori); /////////////////////////////// // Done joint0Output.setClean(joint0Xfo); joint1Output.setClean(joint1Xfo); } } zeaEngine.Registry.register('TriangleIKSolver', TriangleIKSolver); const X_AXIS = new zeaEngine.Vec3(1, 0, 0); const Y_AXIS = new zeaEngine.Vec3(0, 1, 0); const Z_AXIS = new zeaEngine.Vec3(0, 0, 1); const identityXfo = new zeaEngine.Xfo(); const identityQuat = new zeaEngine.Quat(); const generateDebugLines = (debugTree, color) => { const line = new zeaEngine.Lines(); const linepositions = line.getVertexAttribute('positions'); const mat = new zeaEngine.LinesMaterial('debug'); mat.baseColorParam.setValue(new zeaEngine.Color(color)); mat.getParameter('Overlay').setValue(1); const debugGeomItem = new zeaEngine.GeomItem('Pointer', line, mat); debugTree.addChild(debugGeomItem); let numDebugSegments = 0; let numDebugPoints = 0; return { addDebugSegment: (p0, p1) => { const pid0 = numDebugPoints; const pid1 = numDebugPoints + 1; numDebugSegments++; numDebugPoints += 2; if (line.getNumVertices() < numDebugPoints) line.setNumVertices(numDebugPoints); if (line.getNumSegments() < numDebugSegments) line.setNumSegments(numDebugSegments); line.setSegmentVertexIndices(numDebugSegments - 1, pid0, pid1); linepositions.setValue(pid0, p0); linepositions.setValue(pid1, p1); }, doneFrame: () => { line.emit('geomDataTopologyChanged'); numDebugSegments = 0; numDebugPoints = 0; }, }; }; class IKJoint { constructor(index, axisId = 0, limits, globalXfoParam, solverDebugTree) { this.index = index; this.axisId = axisId; this.limits = [zeaEngine.MathFunctions.degToRad(limits[0]), zeaEngine.MathFunctions.degToRad(limits[1])]; this.xfo = globalXfoParam.value; this.backPropagationWeight = 0.0; this.align = new zeaEngine.Quat(); this.debugTree = new zeaEngine.TreeItem('IKJoint' + index); solverDebugTree.addChild(this.debugTree); this.debugLines = {}; } addDebugSegment(color, p0, p1) { if (!this.debugLines[color]) { this.debugLines[color] = generateDebugLines(this.debugTree, color); } this.debugLines[color].addDebugSegment(p0, p1); } init(parentXfo, index, numJoints) { this.localXfo = parentXfo.inverse().multiply(this.xfo); this.bindLocalXfo = this.localXfo.clone(); this.backPropagationWeight = index / (numJoints - 1); switch (this.axisId) { case 0: this.axis = X_AXIS; break; case 1: this.axis = Y_AXIS; break; case 2: this.axis = Z_AXIS; break; } } backPropagateOrientation(baseXfo, targetXfo, index, joints) { if (index == joints.length - 1) { this.xfo.ori = targetXfo.ori.clone(); } /////////////////////// // Apply joint constraint. // const backPropagationWeight = Math.max(0, index / (joints.length - 1) - 0.5) if (this.backPropagationWeight > 0) { const parentJoint = joints[index - 1]; const globalAxis = this.xfo.ori.rotateVec3(this.axis); const parentGlobalAxis = (index > 0 ? parentJoint.xfo : baseXfo).ori.rotateVec3(this.axis); // this.addDebugSegment('#FF0000', this.xfo.tr, this.xfo.tr.add(globalAxis.scale(-0.2))) // this.addDebugSegment('#FFFF00', this.xfo.tr, this.xfo.tr.add(parentGlobalAxis.scale(-0.2))) this.align.setFrom2Vectors(globalAxis, parentGlobalAxis); if (this.backPropagationWeight == 1.0) { parentJoint.xfo.ori = this.align.conjugate().multiply(parentJoint.xfo.ori); parentJoint.xfo.ori.normalizeInPlace(); } else { // We propagate the alignment up the chain by rotating our parent. const parentAlign = this.align.lerp(identityQuat, this.backPropagationWeight).conjugate(); parentJoint.xfo.ori = parentAlign.multiply(parentJoint.xfo.ori); this.xfo.ori = this.align.lerp(identityQuat, 1 - this.backPropagationWeight).multiply(this.xfo.ori); } } else { const globalAxis = this.xfo.ori.rotateVec3(this.axis); const parentGlobalAxis = (index > 0 ? joints[index - 1].xfo : baseXfo).ori.rotateVec3(this.axis); this.align.setFrom2Vectors(globalAxis, parentGlobalAxis); this.xfo.ori = this.align.multiply(this.xfo.ori); } } forwardPropagateAlignment(baseXfo, targetXfo, index, joints) { /////////////////////// // Aim sub-chain at target { const targetVec = targetXfo.tr.subtract(this.xfo.tr); const jointToTip = joints[joints.length - 1].xfo.tr.subtract(this.xfo.tr); if (jointToTip.length() > 0.0001 && targetVec.length() > 0.0001) { jointToTip.normalizeInPlace(); targetVec.normalizeInPlace(); this.align.setFrom2Vectors(jointToTip, targetVec); this.xfo.ori = this.align.multiply(this.xfo.ori); // this.addDebugSegment('#FF0000', this.xfo.tr, this.xfo.tr.add(jointToTip)) // this.addDebugSegment('#FFFF00', this.xfo.tr, this.xfo.tr.add(targetVec)) } } this.xfo.ori.normalizeInPlace(); /////////////////////// // Apply joint constraint. { const globalAxis = this.xfo.ori.rotateVec3(this.axis); const parentGlobalAxis = (index > 0 ? joints[index - 1].xfo : baseXfo).ori.rotateVec3(this.axis); this.align.setFrom2Vectors(globalAxis, parentGlobalAxis); this.xfo.ori = this.align.multiply(this.xfo.ori); } /////////////////////// // Apply angle Limits. { const parentXfo = index > 0 ? joints[index - 1].xfo : baseXfo; const deltaQuat = parentXfo.ori.inverse().multiply(this.xfo.ori); let currAngle = deltaQuat.w < 1.0 ? deltaQuat.getAngle() : 0.0; const deltaAxis = new zeaEngine.Vec3(deltaQuat.x, deltaQuat.y, deltaQuat.x); if (this.axis.dot(deltaAxis) > 0.0) currAngle = -currAngle; if (currAngle < this.limits[0] || currAngle > this.limits[1]) { const deltaAngle = currAngle < this.limits[0] ? this.limits[0] - currAngle : this.limits[1] - currAngle; this.align.setFromAxisAndAngle(this.axis, -deltaAngle); this.xfo.ori = this.xfo.ori.multiply(this.align); } } { let parentXfo = this.xfo; for (let i = index + 1; i < joints.length; i++) { const joint = joints[i]; joint.xfo.ori = parentXfo.ori.multiply(joint.localXfo.ori); joint.xfo.tr = parentXfo.transformVec3(joint.localXfo.tr); parentXfo = joint.xfo; } } } setClean() { for (let key in this.debugLines) this.debugLines[key].doneFrame(); this.output.setClean(this.xfo); } } /** An operator for aiming items at targets. * @extends Operator */ class IKSolver extends zeaEngine.Operator { /** * Create a gears operator. * @param {string} name - The name value. */ constructor(name) { super(name); this.iterationsParam = new zeaEngine.NumberParameter('Iterations', 5); this.baseXfoInput = new zeaEngine.XfoOperatorInput('Base'); this.targetXfoInput = new zeaEngine.XfoOperatorInput('Target'); this.addParameter(this.iterationsParam); this.addInput(this.baseXfoInput); this.addInput(this.targetXfoInput); this.joints = []; this.enabled = false; this.debugTree = new zeaEngine.TreeItem('IKSolver-debug'); } addJoint(globalXfoParam, axisId = 0, limits = [-180, 180]) { const joint = new IKJoint(this.joints.length, axisId, limits, globalXfoParam, this.debugTree); const output = this.addOutput(new zeaEngine.XfoOperatorOutput('Joint' + this.joints.length)); output.setParam(globalXfoParam); joint.output = output; this.joints.push(joint); return joint; } enable() { const baseXfo = this.getInput('Base').isConnected() ? this.getInput('Base').getValue() : identityXfo; this.joints.forEach((joint, index) => { const parentXfo = index > 0 ? this.joints[index - 1].xfo : baseXfo; joint.init(parentXfo, index, this.joints.length); }); this.enabled = true; this.setDirty(); } /** * The evaluate method. */ evaluate() { const numJoints = this.joints.length; if (this.enabled) { const targetXfo = this.getInput('Target').getValue(); const baseXfo = this.getInput('Base').isConnected() ? this.getInput('Base').getValue() : identityXfo; const iterations = this.getParameter('Iterations').getValue(); for (let i = 0; i < iterations; i++) { for (let j = numJoints - 1; j >= 0; j--) { const joint = this.joints[j]; joint.backPropagateOrientation(baseXfo, targetXfo, j, this.joints); } { let parentXfo = this.joints[0].xfo; for (let i = 1; i < numJoints; i++) { const joint = this.joints[i]; joint.localXfo.ori = parentXfo.ori.inverse().multiply(joint.xfo.ori); joint.xfo.ori = parentXfo.ori.multiply(joint.localXfo.ori); joint.xfo.tr = parentXfo.transformVec3(joint.localXfo.tr); parentXfo = joint.xfo; } } { for (let j = 0; j < numJoints; j++) { const joint = this.joints[j]; joint.forwardPropagateAlignment(baseXfo, targetXfo, j, this.joints); } } } } // Now store the value to the connected Xfo parameter. for (let i = 0; i < numJoints; i++) { this.joints[i].setClean(); } } } zeaEngine.Registry.register('IKSolver', IKSolver); /** An operator for aiming items at targets. * @extends Operator */ class AttachmentConstraint extends zeaEngine.Operator { /** * Create a gears operator. * @param {string} name - The name value. */ constructor(name) { super(name); this.timeInput = new zeaEngine.NumberOperatorInput('Time'); this.attachedXfoOutput = new zeaEngine.XfoOperatorOutput('Attached', zeaEngine.OperatorOutputMode.OP_READ_WRITE); this.addInput(this.timeInput); this.addOutput(this.attachedXfoOutput); this.attachTargets = []; this.attachId = -1; } addAttachTarget(target, time) { const input = this.addInput(new zeaEngine.XfoOperatorInput('Target' + this.getNumInputs())); input.setParam(target); this.attachTargets.push({ input, time, offsetXfo: undefined, }); } getAttachTarget(attachId) { return this.getInputByIndex(attachId + 1); } findTarget(time) { if (this.attachTargets.length == 0 || time <= this.attachTargets[0].time) { return -1; } const numKeys = this.attachTargets.length; if (time >= this.attachTargets[numKeys - 1].time) { return numKeys - 1; } // Find the first key after the specified time value for (let i = 1; i < numKeys; i++) { const key = this.attachTargets[i]; if (key.time > time) { return i - 1; } } } /** * The evaluate method. */ evaluate() { const time = this.getInput('Time').getValue(); const output = this.getOutputByIndex(0); let xfo = output.getValue(); const attachId = this.findTarget(time); if (attachId != -1) { const currXfo = this.getAttachTarget(attachId).getValue(); const attachment = this.attachTargets[attachId]; if (attachId != this.attachId) { if (!attachment.offsetXfo) { if (this.attachId == -1) { attachment.offsetXfo = currXfo.inverse().multiply(xfo); } else { const prevXfo = this.getAttachTarget(this.attachId).getValue(); const prevOffset = this.attachTargets[this.attachId].offsetXfo; const offsetXfo = currXfo.inverse().multiply(prevXfo.multiply(prevOffset)); attachment.offsetXfo = offsetXfo; } } this.attachId = attachId; } xfo = currXfo.multiply(attachment.offsetXfo); } output.setClean(xfo); } } zeaEngine.Registry.register('AttachmentConstraint', AttachmentConstraint); class IndexEvent { constructor(index) { // super() this.index = index; } } /** Class representing a gear parameter. * @extends BaseTrack */ class BaseTrack extends zeaEngine.EventEmitter { constructor(name, owner) { super(); this.name = name; this.owner = owner; this.keys = []; this.__sampleCache = {}; this.__currChange = null; this.__secondaryChange = null; this.__secondaryChangeTime = -1; } getName() { return this.name; } getOwner() { return this.owner; } setOwner(owner) { this.owner = owner; } getPath() { return [...this.owner.getPath(), name]; } getNumKeys() { return this.keys.length; } getKeyTime(index) { return this.keys[index].time; } getKeyValue(index) { return this.keys[index].value; } setKeyValue(index, value) { this.keys[index].value = value; this.emit('keyChanged', new IndexEvent(index)); } setKeyTimeAndValue(index, time, value) { this.keys[index].time = time; this.keys[index].value = value; this.emit('keyChanged', new IndexEvent(index)); } getTimeRange() { if (this.keys.length == 0) { return new zeaEngine.Vec2(Number.NaN, Number.NaN); } const numKeys = this.keys.length; return new zeaEngine.Vec2(this.keys[0].time, this.keys[numKeys - 1].time); } addKey(time, value) { let index; const numKeys = this.keys.length; if (this.keys.length == 0 || time < this.keys[0].time) { this.keys.splice(0, 0, { time, value }); index = 0; } else { if (time > this.keys[numKeys - 1].time) { this.keys.push({ time, value }); index = numKeys; } else { // Find the first key after the specified time value for (let i = 1; i < numKeys; i++) { const key = this.keys[i]; if (key.time > time) { this.keys.splice(i, 0, { time, value }); index = i; break; } } } } this.emit('keyAdded', new IndexEvent(index)); return index; } updateKey(index, value) { this.keys[index].value = value; this.emit('keyUpdated', new IndexEvent(index)); } removeKey(index) { // const undoRedoManager = UndoRedoManager.getInstance() // const change = undoRedoManager.getCurrentChange() // if (change) { // if (this.__currChange != change || this.__secondaryChangeTime != time) { // this.__currChange = change // this.__secondaryChangeTime = time // } // } this.keys.splice(index, 1); this.emit('keyRemoved', new IndexEvent(index)); } findKeyAndLerp(time) { if (this.keys.length == 0) { return { keyIndex: -1, lerp: 0, }; } if (time <= this.keys[0].time) { return { keyIndex: 0, lerp: 0, }; } const numKeys = this.keys.length; if (time >= this.keys[numKeys - 1].time) { return { keyIndex: numKeys - 1, lerp: 0, }; } // Find the first key after the given time value for (let i = 1; i < numKeys; i++) { const key = this.keys[i]; if (time < key.time) { const prevKey = this.keys[i - 1]; const delta = key.time - prevKey.time; return { keyIndex: i - 1, lerp: (time - prevKey.time) / delta, }; } } } evaluate(time) { this.findKeyAndLerp(time); } setValue(time, value) { // const undoRedoManager = UndoRedoManager.getInstance() // const change = undoRedoManager.getCurrentChange() // if (change) { // if (this.__currChange != change || this.__secondaryChangeTime != time) { // this.__currChange = change // this.__secondaryChangeTime = time // const keyAndLerp = this.findKeyAndLerp(time) // if (keyAndLerp.lerp > 0.0) { // this.__secondaryChange = new AddKeyChange(this, time, value) // this.__currChange.secondaryChanges.push(this.__secondaryChange) // } else { // this.__secondaryChange = new KeyChange(this, keyAndLerp.keyIndex, value) // this.__currChange.secondaryChanges.push(this.__secondaryChange) // } // } else { // this.__secondaryChange.update(value) // } // } const keyAndLerp = this.findKeyAndLerp(time); if (keyAndLerp.lerp > 0.0) { this.addKey(time, value); } else { this.setKeyValue(keyAndLerp.keyIndex, value); } } // //////////////////////////////////////// // Persistence /** * Encodes the current object as a json object. * * @param {object} context - The context value. * @return {object} - Returns the json object. */ toJSON(context) { const j = {}; j.name = this.name; j.type = zeaEngine.Registry.getClassName(Object.getPrototypeOf(this).constructor); j.keys = this.keys.map((key) => { return { time: key.time, value: key.value.toJSON ? key.value.toJSON() : key.value }; }); return j; } /** * Decodes a json object for this type. * * @param {object} j - The json object this item must decode. * @param {object} context - The context value. */ fromJSON(j, context) { this.__name = j.name; this.keys = j.keys.map((keyJson) => this.loadKeyJSON(keyJson)); this.emit('loaded'); } loadKeyJSON(json) { const key = { time: json.time, value: json.value, }; return key; } } class ColorTrack extends BaseTrack { constructor(name) { super(name); } evaluate(time) {