@zeainc/zea-kinematics
Version:
Kinematics extension for Zea Engine.
1,377 lines (1,365 loc) • 62.3 kB
JavaScript
import { Registry, Operator, NumberParameter, BooleanParameter, TreeItemParameter, ListParameter, XfoOperatorOutput, OperatorOutputMode, StructParameter, Vec3Parameter, Vec3, Vec2Parameter, Vec2, MathFunctions, Quat, MultiChoiceParameter, XfoOperatorInput, Xfo, TreeItem, Lines, LinesMaterial, Color, GeomItem, NumberOperatorInput, EventEmitter, PointsMaterial, Points, Cuboid, libsRegistry } from '@zeainc/zea-engine';
import { UndoRedoManager, Change, HandleMaterial } from '@zeainc/zea-ux';
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 StructParameter {
/**
* Create an explode part parameter.
* @param {string} name - The name value.
*/
constructor(name = '') {
super(name);
this.stageParam = new NumberParameter('Stage', 0);
this.axisParam = new Vec3Parameter('Axis', new Vec3(1, 0, 0));
this.movementParam = new Vec2Parameter('MovementTiming', new Vec2(0, 1), [new Vec2(0, 0), new Vec2(1, 1)]);
this.multiplierParam = new NumberParameter('Multiplier', 1.0);
this.output = new XfoOperatorOutput('output', 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 * 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 * 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 Operator {
/**
* Create an explode parts operator.
* @param {string} name - The name value.
*/
constructor(name) {
super(name);
this.stagesParam = new NumberParameter('Stages', 0);
this.explodeParam = new NumberParameter('Explode', 0.0, [0, 1]);
this.distParam = new NumberParameter('Dist', 1.0);
this.offsetParam = new NumberParameter('Offset', 0);
this.cascadeParam = new BooleanParameter('Cascade', false);
this.centeredParam = new BooleanParameter('Centered', false);
this.parentItemParam = new TreeItemParameter('RelativeTo');
this.itemsParam = new 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 XfoOperatorOutput('Part' + event.index, 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);
}
}
Registry.register('ExplodePartsOperator', ExplodePartsOperator);
/** Class representing a gear parameter.
* @extends StructParameter
*/
class GearParameter extends StructParameter {
/**
* Create a gear parameter.
* @param {string} name - The name value.
*/
constructor(name) {
super(name);
this.__ratioParam = new NumberParameter('Ratio', 1.0);
this.__offsetParam = new NumberParameter('Offset', 0.0);
this.__axisParam = new Vec3Parameter('Axis', new 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 Operator {
/**
* Create a gears operator.
* @param {string} name - The name value.
*/
constructor(name) {
super(name);
this.__revolutionsParam = new NumberParameter('Revolutions', 0.0);
this.rpmParam = new NumberParameter('RPM', 0.0);
this.__gearsParam = new 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 XfoOperatorOutput('Gear' + event.index, 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 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()
}
}
Registry.register('GearsOperator', GearsOperator);
/** An operator for aiming items at targets.
* @extends Operator
*/
class AimOperator extends Operator {
/**
* Create a gears operator.
* @param {string} name - The name value.
*/
constructor(name) {
super(name);
this.weightParam = new NumberParameter('Weight', 1);
this.axisParam = new MultiChoiceParameter('Axis', 0, ['+X Axis', '-X Axis', '+Y Axis', '-Y Axis', '+Z Axis', '-Z Axis']);
this.stretchParam = new NumberParameter('Stretch', 0.0);
this.initialDistParam = new NumberParameter('Initial Dist', 1.0);
this.targetInput = new XfoOperatorInput('Target');
this.xfoInputOutput = new XfoOperatorOutput('InputOutput', 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 Quat();
align.setFrom2Vectors(vec, dir);
align.alignWith(new Quat());
if (weight < 1.0)
align = new 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);
}
}
Registry.register('AimOperator', AimOperator);
/** An operator for aiming items at targets.
* @extends Operator
*/
class RamAndPistonOperator extends Operator {
/**
* Create a gears operator.
* @param {string} name - The name value.
*/
constructor(name) {
super(name);
this.axisParam = new MultiChoiceParameter('Axis', 0, ['+X Axis', '-X Axis', '+Y Axis', '-Y Axis', '+Z Axis', '-Z Axis']);
this.ramXfoOutput = new XfoOperatorOutput('Ram', OperatorOutputMode.OP_READ_WRITE);
this.pistonXfoOutput = new XfoOperatorOutput('Piston', 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 Quat();
const alignPiston = new 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);
}
}
Registry.register('RamAndPistonOperator', RamAndPistonOperator);
/** An operator for aiming items at targets.
* @extends Operator
*/
class TriangleIKSolver extends Operator {
/**
* Create a gears operator.
* @param {string} name - The name value.
*/
constructor(name) {
super(name);
this.targetXfoInput = new XfoOperatorInput('Target');
this.joint0XfoOutput = new XfoOperatorOutput('Joint0', OperatorOutputMode.OP_READ_WRITE);
this.joint1XfoOutput = new XfoOperatorOutput('Joint1', 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 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);
}
}
Registry.register('TriangleIKSolver', TriangleIKSolver);
const X_AXIS = new Vec3(1, 0, 0);
const Y_AXIS = new Vec3(0, 1, 0);
const Z_AXIS = new Vec3(0, 0, 1);
const identityXfo = new Xfo();
const identityQuat = new Quat();
const generateDebugLines = (debugTree, color) => {
const line = new Lines();
const linepositions = line.getVertexAttribute('positions');
const mat = new LinesMaterial('debug');
mat.baseColorParam.setValue(new Color(color));
mat.getParameter('Overlay').setValue(1);
const debugGeomItem = new 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 = [MathFunctions.degToRad(limits[0]), MathFunctions.degToRad(limits[1])];
this.xfo = globalXfoParam.value;
this.backPropagationWeight = 0.0;
this.align = new Quat();
this.debugTree = new 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 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 Operator {
/**
* Create a gears operator.
* @param {string} name - The name value.
*/
constructor(name) {
super(name);
this.iterationsParam = new NumberParameter('Iterations', 5);
this.baseXfoInput = new XfoOperatorInput('Base');
this.targetXfoInput = new XfoOperatorInput('Target');
this.addParameter(this.iterationsParam);
this.addInput(this.baseXfoInput);
this.addInput(this.targetXfoInput);
this.joints = [];
this.enabled = false;
this.debugTree = new 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 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();
}
}
}
Registry.register('IKSolver', IKSolver);
/** An operator for aiming items at targets.
* @extends Operator
*/
class AttachmentConstraint extends Operator {
/**
* Create a gears operator.
* @param {string} name - The name value.
*/
constructor(name) {
super(name);
this.timeInput = new NumberOperatorInput('Time');
this.attachedXfoOutput = new XfoOperatorOutput('Attached', 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 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);
}
}
Registry.register('AttachmentConstraint', AttachmentConstraint);
class IndexEvent {
constructor(index) {
// super()
this.index = index;
}
}
/** Class representing a gear parameter.
* @extends BaseTrack
*/
class BaseTrack extends 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 Vec2(Number.NaN, Number.NaN);
}
const numKeys = this.keys.length;
return new 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 = 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) {
const keyAndLerp = this.findKeyAndLerp(time);
const value0 = this.keys[keyAndLerp.keyIndex].value;
if (keyAndLerp.lerp > 0.0) {
const value1 = this.keys[keyAndLerp.keyIndex + 1].value;
return value0.lerp(value1, keyAndLerp.lerp);
}
else {
return value0;
}
}
}
class XfoTrack extends BaseTrack {
constructor(name) {
super(name);
}
evaluate(time) {
const keyAndLerp = this.findKeyAndLerp(time);
const value0 = this.keys[keyAndLerp.keyIndex].value;
if (keyAndLerp.lerp > 0.0) {
const value1 = this.keys[keyAndLerp.keyIndex + 1].value;
const tr = value0.tr.lerp(value1.tr, keyAndLerp.lerp);
const ori = value0.ori.lerp(value1.ori, keyAndLerp.lerp);
return new Xfo(tr, ori);
}
else {
return value0;
}
}
loadKeyJSON(json) {
const key = {
time: json.time,
value: new Xfo(),
};
key.value.fromJSON(json.value);
return key;
}
}
Registry.register('XfoTrack', XfoTrack);
class AddKeyChange extends Change {
constructor(track, time, value) {
super(`Add Key to ${track ? track.getName() : 'track'}`);
if (track != undefined && time != undefined && value != undefined) {
this.track = track;
this.time = time;
this.value = value;
this.index = track.addKey(this.time, this.value);
}
else {
super();
}
}
update(value) {
this.value = value;
this.track.setKeyValue(this.index, this.value);
this.emit('updated', {
value: this.value,
});
}
emit(arg0, arg1) {
throw new Error('Method not implemented.');
}
undo() {
this.track.removeKey(this.index);
}
redo() {
this.track.addKey(this.time, this.value);
}
/**
* Serializes `Parameter` instance value as a JSON object, allowing persistence/replication.
*
* @param {object} context - The context param.
* @return {object} The return value.
*/
toJSON(context) {
const j = {
name: this.name,
trackPath: this.track.getPath(),
time: this.time,
};
if (this.value != undefined) {
if (this.value.toJSON) {
j.value = this.value.toJSON();
}
else {
j.value = this.value;
}
}
return j;
}
/**
* Restores `Parameter` instance's state with the specified JSON object.
*
* @param {object} j - The j param.
* @param {object} context - The context param.
*/
fromJSON(j, context) {
const track = context.appData.scene.getRoot().resolvePath(j.trackPath, 1);
if (!track || !(track instanceof BaseTrack)) {
console.warn('resolvePath is unable to resolve', j.trackPath);
return;
}
this.name = `Add Key to ${track.getName()}`;
this.track = track;
const key = this.track.loadKeyJSON(j);
this.time = key.time;
this.value = key.value;
this.index = this.track.addKey(key.time, key.value);
}
/**
* Updates the state of an existing identified `Parameter` through replication.
*
* @param {object} j - The j param.
*/
changeFromJSON(j) {
i