UNPKG

@eroscripts/osr-emu

Version:

A web-based graphical emulator for open source strokers.

236 lines (193 loc) 9.82 kB
import type { Group, Object3DEventMap } from 'three'; import type { axisId } from '../../../osr-emu.js'; import { MeshPhongMaterial, MeshStandardMaterial, Quaternion, Vector3 } from 'three'; import { circleSphereIntersection, degToRad, loadObj, mapDecimal, recomputeNormals, servoToRotation, vectorDirection } from '../../util.js'; import caseGeometry from '../common/case.js'; import bearingGeometry from '../common/rod-end-bearing.js'; import baseGeometry from './geometry/base.js'; import mainArmGeometry from './geometry/bearing-arm.js'; import bearingJointGeometry from './geometry/bearing-joint.js'; import lidGeometry from './geometry/lid.js'; import pitcherArmGeometry from './geometry/pitcher-arm.js'; import pitcherLinkArmGeometry from './geometry/pitcher-link-arm.js'; import pitcherGeometry from './geometry/pitcher.js'; import receiverGeometry from './geometry/receiver.js'; /** * 3D Model of the OSR2 */ export default class OSR2Model { _leftArmPosition = [70, 5, 16.5]; _rightArmPosition = [-70, 5, 16.5]; _pitcherArmPosition = [77, 5, 17]; _orientation = -Math.PI / 2; // Rotation around the x-axis. objects!: Record<string, Group<Object3DEventMap>>; load() { const receiverMaterial = new MeshPhongMaterial({ color: 0x3F5173 }); const baseMaterial = new MeshPhongMaterial({ color: 0x1E1E1E }); const bearingMaterial = new MeshStandardMaterial({ color: 0xC0C0C0, metalness: 1, roughness: 0.5, }); const modCase = loadObj(caseGeometry, baseMaterial); const base = loadObj(baseGeometry, baseMaterial); const receiver = loadObj(receiverGeometry, receiverMaterial); const rightArm = loadObj(mainArmGeometry, baseMaterial); const leftArm = loadObj(mainArmGeometry, baseMaterial); const lid = loadObj(lidGeometry, receiverMaterial); const pitcher = loadObj(pitcherGeometry, baseMaterial); const pitcherArm = loadObj(pitcherArmGeometry, baseMaterial); const pitcherLinkArm = loadObj(pitcherLinkArmGeometry, baseMaterial); const leftBearing = loadObj(bearingGeometry, bearingMaterial, recomputeNormals); const rightBearing = loadObj(bearingGeometry, bearingMaterial, recomputeNormals); const linkStartBearing = loadObj(bearingGeometry, bearingMaterial, recomputeNormals); const linkEndBearing = loadObj(bearingGeometry, bearingMaterial, recomputeNormals); const leftJoint = loadObj(bearingJointGeometry, bearingMaterial, recomputeNormals); const rightJoint = loadObj(bearingJointGeometry, bearingMaterial, recomputeNormals); const linkJoint = loadObj(bearingJointGeometry, bearingMaterial, recomputeNormals); pitcherArm.position.fromArray(this._pitcherArmPosition); rightArm.position.fromArray(this._rightArmPosition); leftArm.position.fromArray(this._leftArmPosition); this.objects = { base, receiver, leftArm, rightArm, lid, pitcher, pitcherArm, pitcherLinkArm, modCase, leftBearing, rightBearing, linkStartBearing, linkEndBearing, leftJoint, rightJoint, linkJoint, }; return { objects: this.objects, orientation: this._orientation }; } preRender(axes: Record<axisId, number>, scale: Record<axisId, number>) { this._forwardKinematics(axes, scale); } _forwardKinematics(axes: Record<axisId, number>, scale: Record<axisId, number>) { const R0_SCALE = scale.R0; const leftBearingPosition = new Vector3(0, 0, 140); const rightBearingPosition = leftBearingPosition.clone(); const armBearingOffset = 3.5; const { leftServoAngle, rightServoAngle, pitcherServoAngle, armBend } = this._computeServoAngles(axes, scale); const { leftArm, rightArm, pitcherArm, pitcherLinkArm, leftBearing, rightBearing, linkStartBearing, linkEndBearing, receiver, modCase, leftJoint, rightJoint, linkJoint, } = this.objects; leftArm.rotation.set(leftServoAngle, -armBend, degToRad(180)); rightArm.rotation.set(rightServoAngle, armBend, 0); pitcherArm.rotation.set(pitcherServoAngle, 0, 0); this._updateMainArmBearingValues(leftBearing, leftBearingPosition, leftArm, -armBearingOffset, -armBend); this._updateMainArmBearingValues(rightBearing, rightBearingPosition, rightArm, armBearingOffset, armBend); leftJoint.position.copy(leftBearing.position); rightJoint.position.copy(rightBearing.position); pitcherLinkArm.position.fromArray([-3, -40, 0]) .applyAxisAngle(new Vector3(1, 0, 0), pitcherServoAngle) .add(pitcherArm.position); linkStartBearing.position.fromArray([0, 0, 0]).add(pitcherLinkArm.position); const receiverPosition = leftBearingPosition.clone() .lerp(rightBearingPosition, 0.5); receiver.position.fromArray(receiverPosition.toArray()); const receiverDirection = new Vector3(-1, 0, 0); const receiverBearingAxis = rightBearingPosition.clone() .sub(leftBearingPosition) .normalize(); const intersectionPoints = circleSphereIntersection( leftBearingPosition, 40, // Distance between pitcher hole and arm hole on the receiver... receiverBearingAxis, linkStartBearing.position, 140, // Length of pitcher link arm to the hole... ); if (intersectionPoints) { const linkEndBearingPosition = intersectionPoints.length === 1 ? intersectionPoints[0] : intersectionPoints[0].y < intersectionPoints[1].y ? intersectionPoints[0] : intersectionPoints[1]; linkEndBearing.position.copy(linkEndBearingPosition); linkEndBearing.up.fromArray([0, 0, 1]); linkEndBearing.lookAt(this._toWorldCoordinates(linkEndBearingPosition.clone().sub(linkStartBearing.position).add(linkEndBearingPosition))); linkStartBearing.up.fromArray([0, 0, 1]); linkStartBearing.lookAt(this._toWorldCoordinates(linkStartBearing.position.clone().sub(linkEndBearing.position).add(linkStartBearing.position))); pitcherLinkArm.up.fromArray([0, 0, 1]); pitcherLinkArm.lookAt(this._toWorldCoordinates(linkEndBearing.position)); } else { console.warn('Invalid model arrangement. No intersection found for positioning pitcher link.'); } linkJoint.position.copy(linkEndBearing.position); const rotationAxis = receiverDirection.clone().cross(receiverBearingAxis).normalize(); const rotationAngle = Math.acos(receiverDirection.dot(receiverBearingAxis)); const rollQuaternion = new Quaternion().setFromAxisAngle(rotationAxis, rotationAngle); const pitchUpVector = new Vector3(0, -1, 0) .applyQuaternion(rollQuaternion) .normalize(); const pitchBearingVector = linkEndBearing.position.clone().sub(leftBearing.position); const pitchAngleDirection = vectorDirection(pitchUpVector, pitchBearingVector, new Vector3(1, 0, 0).applyQuaternion(rollQuaternion)); const pitchAngle = pitchBearingVector.angleTo(pitchUpVector) * pitchAngleDirection; const pitchQuaternion = new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), pitchAngle); const twistQuaternion = new Quaternion().setFromAxisAngle(new Vector3(0, -1, 0), axes.R0 * degToRad(240 * R0_SCALE) - degToRad(120 * R0_SCALE)); const receiverQuaternion = rollQuaternion.multiply(pitchQuaternion); receiver.setRotationFromQuaternion(receiverQuaternion); modCase.position.fromArray(receiverPosition.toArray()); modCase.setRotationFromQuaternion(receiverQuaternion.clone().multiply(twistQuaternion)); leftJoint.setRotationFromQuaternion(receiverQuaternion); rightJoint.setRotationFromQuaternion(receiverQuaternion); } _updateMainArmBearingValues(bearing: Group<Object3DEventMap>, bearingPosition: Vector3, arm: Group<Object3DEventMap>, bearingOffset: number, armBend: number) { const bearingAxis = new Vector3(1, 0, 0); const upAxis = new Vector3(0, 1, 0); bearingPosition.x += bearingOffset; bearingPosition.applyAxisAngle(upAxis, armBend); bearingPosition.applyAxisAngle(bearingAxis, arm.rotation.x); bearingPosition.add(arm.position); bearing.position.fromArray(bearingPosition.toArray()); bearing.rotation.x = arm.rotation.x; bearing.rotation.y = arm.rotation.y; } _toWorldCoordinates(vector: Vector3) { return vector.clone().applyAxisAngle(new Vector3(1, 0, 0), this._orientation); } /** * Compute the angles for the servos based on the algorithm from the actual OSR2 Firmware. */ _computeServoAngles(axes: Record<axisId, number>, scale: Record<axisId, number>) { const L0_SCALE = scale.L0; const R1_SCALE = scale.R1; const R2_SCALE = scale.R2; const stroke = mapDecimal(axes.L0, 0, 0.9999, -350 * L0_SCALE, 350 * L0_SCALE); const roll = mapDecimal(axes.R1, 0, 0.9999, -132 * R1_SCALE, 132 * R1_SCALE); const pitch = mapDecimal(axes.R2, 0, 0.9999, -350 * R2_SCALE, 350 * R2_SCALE); const servoZero = 1515.105; const servoFrequency = 330; const servoInterval = 1000000 / servoFrequency; const leftServo = mapDecimal(servoZero + stroke + roll, 0, servoInterval, 0, 65535); const rightServo = mapDecimal(servoZero - stroke + roll, 0, servoInterval, 0, 65535); const pitchServo = mapDecimal(servoZero - pitch, 0, servoInterval, 0, 65535); return { leftServoAngle: servoToRotation(leftServo), rightServoAngle: servoToRotation(rightServo, -1), pitcherServoAngle: servoToRotation(pitchServo, -1), armBend: degToRad(mapDecimal(Math.abs(roll), 0, 132, 0, 4)), }; } }