@eroscripts/osr-emu
Version:
A web-based graphical emulator for open source strokers.
236 lines (196 loc) • 7.72 kB
text/typescript
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));
};