UNPKG

@eroscripts/osr-emu

Version:

A web-based graphical emulator for open source strokers.

236 lines (196 loc) 7.72 kB
import type { Material, Object3D, Vector3 } from 'three'; import { MathUtils, Mesh } from 'three'; import { BufferGeometryUtils } from 'three/examples/jsm/Addons.js'; import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'; const objLoader = new OBJLoader(); export const degToRad = MathUtils.degToRad; export const radToDeg = MathUtils.radToDeg; /** * Load a model in .OBJ format from a String and apply the specified material. * * @param {string} text * @param {Material} material * @Param {Function} [processMesh] - optional function to run on each mesh in the object. */ export function loadObj(text: string, material: Material, processMesh?: (mesh: Mesh) => void) { const object = objLoader.parse(text); forEachMesh(object, (mesh) => { if (typeof processMesh === 'function') { processMesh(mesh); } mesh.material = material; }); return object; } /** * Load a model in .OBJ format that is composed of multiple objects (possibly of different * materials and requiring different processing for each object). * * @param {string} text * @param {object} config */ export function loadComplexObj(text: string, config: Record<string, { processMesh?: (mesh: Mesh) => void; material?: Material }>) { const object = objLoader.parse(text); forEachMesh(object, (mesh) => { if (config?.[mesh.name]) { const meshConfig = config[mesh.name]; if (typeof meshConfig?.processMesh === 'function') { meshConfig.processMesh(mesh); } if (meshConfig?.material) { mesh.material = meshConfig.material; } } }); return object; } export function loadObjs(count: number, text: string, material: Material, processMesh?: (mesh: Mesh) => void) { const objects = []; for (let i = 0; i < count; i++) { objects.push(loadObj(text, material, processMesh)); } return objects; } /** * Call a function for each mesh of the specified OBJ. * * @param {object} object * @param {Function} fn */ export function forEachMesh(object: Object3D, fn: (mesh: Mesh) => void) { object.traverse((child) => { if (child instanceof Mesh) { fn(child); } }); } export function recomputeNormals(mesh: Mesh) { mesh.geometry.deleteAttribute('normal'); mesh.geometry = BufferGeometryUtils.mergeVertices(mesh.geometry, 0.000001); mesh.geometry.computeVertexNormals(); }; /** * Calculate the intersection point(s) of a circle and a sphere. * * Uses the algorithm described here: * https://gamedev.stackexchange.com/questions/75756/sphere-sphere-intersection-and-circle-sphere-intersection * * @param {Vector3} circleCenter * @param {number} circleRadius * @param {Vector3} circlePlaneDirection * @param {Vector3} sphereCenter * @param {number} sphereRadius */ export function circleSphereIntersection(circleCenter: Vector3, circleRadius: number, circlePlaneDirection: Vector3, sphereCenter: Vector3, sphereRadius: number) { const circlePlaneNormal = circlePlaneDirection.clone().normalize(); // The plane of the circle cuts the sphere this distance from the sphere's center. const distanceToPlane = circlePlaneNormal.dot(circleCenter.clone().sub(sphereCenter)); if (Math.abs(distanceToPlane) > sphereRadius) { // There is no intersection. return null; } // The center of the circle cut out of the sphere by the plane. const sphereCircleCenter = circlePlaneNormal.clone().multiplyScalar(distanceToPlane).add(sphereCenter); if (Math.abs(distanceToPlane) === sphereRadius) { // This is the sole intersection point with the plane. // If it lies on the circle its the intersection point. if (sphereCircleCenter.distanceTo(circleCenter) === circleRadius) { return [sphereCircleCenter]; } // Does not intersect. return null; } const sphereCircleRadius = Math.sqrt(sphereRadius * sphereRadius - distanceToPlane * distanceToPlane); const intersectionCircle = circleCircleIntersection(sphereCircleCenter, sphereCircleRadius, circleCenter, circleRadius); if (!intersectionCircle) { return null; } const { center, radius } = intersectionCircle; if (radius === 0) { // Single point of intersection.. return [center]; } const tangentVector = sphereCircleCenter.clone().sub(circleCenter).cross(circlePlaneNormal).normalize(); return [ center.clone().sub(tangentVector.clone().multiplyScalar(radius)), center.clone().add(tangentVector.clone().multiplyScalar(radius)), ]; } /** * Compute the intersection between two circles. Returns an object with { center, radius }. * If there is only one intersection point, the radius will be zero. * * https://gamedev.stackexchange.com/questions/75756/sphere-sphere-intersection-and-circle-sphere-intersection * * @param {Vector3} c1 * @param {number} r1 * @param {Vector3} c2 * @param {number} r2 * @returns object with { center, radius } */ export function circleCircleIntersection(c1: Vector3, r1: number, c2: Vector3, r2: number) { const difference = c2.clone().sub(c1); const distance = Math.abs(difference.length()); if (distance === 0 || r1 + r2 < distance || distance + Math.min(r1, r2) < Math.max(r1, r2)) { // Infinitely many intersections or none. return null; } else if (r1 + r2 === distance) { // Exactly one intersection return { center: c1.clone().add(difference).multiplyScalar(r1 / distance), radius: 0, }; } else if (distance + Math.min(r1, r2) === Math.max(r1, r2)) { // Exactly one intersection, but one circle is inside the other. const largerCircle = r1 > r2 ? c1.clone() : c2.clone(); const smallerCircle = r1 < r2 ? c1.clone() : c2.clone(); const largerRadius = Math.max(r1, r2); return { center: largerCircle.add(smallerCircle.sub(largerCircle).multiplyScalar(largerRadius / distance)), radius: 0, }; } const h = 0.5 + (r1 * r1 - r2 * r2) / (2 * distance * distance); const center = c1.clone().add(difference.multiplyScalar(h)); const radius = Math.sqrt(r1 * r1 - h * h * distance * distance); return { center, radius }; } /** * Returns the "direction" of a vector with respect to another using an up vector. * * Inspired by https://forum.unity.com/threads/left-right-test-function.31420/ * * @param {Vector3} a * @param {Vector3} b * @param {Vector3} up */ export function vectorDirection(a: Vector3, b: Vector3, up: Vector3) { const right = up.clone().cross(a); const dir = right.dot(b); return dir > 0 ? 1 : dir < 0 ? -1 : 0; } /** * https://www.arduino.cc/reference/en/language/functions/math/constrain/ */ export function constrain(x: number, a: number, b: number) { return Math.max(a, Math.min(x, b)); } /** * https://www.arduino.cc/reference/en/language/functions/math/map/ */ export function map(x: number, in_min: number, in_max: number, out_min: number, out_max: number) { return Math.floor((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min); } /** * Similar to Arduino's method but allows decimals. * https://www.arduino.cc/reference/en/language/functions/math/map/ */ export function mapDecimal(value: number, inMin: number, inMax: number, outMin = 0, outMax = 1) { return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin; } export function servoToRotation(servoValue: number, scale = 1) { // Map the servo value to an angle in radians. return mapDecimal(servoValue, 0, 65535, degToRad(-180 * scale), degToRad(180 * scale)); };