optigami_ / lib /physics.ts
sissississi's picture
Deploy full Next.js origami simulator to HF Space
b40f1ec
import * as THREE from 'three';
import { Pattern, CreaseType } from './patterns';
export class Vertex {
pos: THREE.Vector3;
oldPos: THREE.Vector3;
originalPos: THREE.Vector2;
fixed: boolean = false;
constructor(x: number, y: number) {
this.pos = new THREE.Vector3(x, y, 0);
this.oldPos = new THREE.Vector3(x, y, 0);
this.originalPos = new THREE.Vector2(x, y);
}
}
export class DistanceConstraint {
v1: Vertex;
v2: Vertex;
restLength: number;
constructor(v1: Vertex, v2: Vertex) {
this.v1 = v1;
this.v2 = v2;
this.restLength = v1.originalPos.distanceTo(v2.originalPos);
}
solve() {
const delta = new THREE.Vector3().subVectors(this.v2.pos, this.v1.pos);
const dist = delta.length();
if (dist === 0) return;
const diff = (dist - this.restLength) / dist;
const correction = delta.multiplyScalar(diff * 0.5);
if (!this.v1.fixed && !this.v2.fixed) {
this.v1.pos.add(correction);
this.v2.pos.sub(correction);
} else if (!this.v1.fixed) {
this.v1.pos.add(correction.multiplyScalar(2));
} else if (!this.v2.fixed) {
this.v2.pos.sub(correction.multiplyScalar(2));
}
}
}
export class CreaseConstraint {
vA: Vertex;
vB: Vertex;
vC: Vertex; // hinge
vD: Vertex; // hinge
type: CreaseType;
d: number;
vA_len: number;
vB_len: number;
constructor(vA: Vertex, vB: Vertex, vC: Vertex, vD: Vertex, type: CreaseType) {
this.vA = vA;
this.vB = vB;
this.vC = vC;
this.vD = vD;
this.type = type;
const C = vC.originalPos;
const D = vD.originalPos;
const A = vA.originalPos;
const B = vB.originalPos;
const CD = new THREE.Vector2().subVectors(D, C);
const hingeLen = CD.length();
const hingeDir = CD.clone().divideScalar(hingeLen);
const CA = new THREE.Vector2().subVectors(A, C);
const CB = new THREE.Vector2().subVectors(B, C);
const projA_len = CA.dot(hingeDir);
const projB_len = CB.dot(hingeDir);
this.d = Math.abs(projA_len - projB_len);
this.vA_len = Math.sqrt(Math.max(0, CA.lengthSq() - projA_len * projA_len));
this.vB_len = Math.sqrt(Math.max(0, CB.lengthSq() - projB_len * projB_len));
}
solve(foldPercent: number) {
let theta = 0;
const maxAngle = Math.PI * 0.85; // Prevent folding completely flat to stop self-intersection
if (this.type === 'mountain') theta = foldPercent * maxAngle;
if (this.type === 'valley') theta = -foldPercent * maxAngle;
// Target distance
const L_sq = this.d * this.d + this.vA_len * this.vA_len + this.vB_len * this.vB_len + 2 * this.vA_len * this.vB_len * Math.cos(theta);
const targetDist = Math.sqrt(Math.max(0, L_sq));
// Apply distance constraint between A and B
const delta = new THREE.Vector3().subVectors(this.vB.pos, this.vA.pos);
const dist = delta.length();
if (dist === 0) return;
const diff = (dist - targetDist) / dist;
const correction = delta.multiplyScalar(diff * 0.5);
// We can use a slightly lower stiffness for creases to allow the structural springs to dominate
const stiffness = 0.5;
correction.multiplyScalar(stiffness);
if (!this.vA.fixed) this.vA.pos.add(correction);
if (!this.vB.fixed) this.vB.pos.sub(correction);
// Apply bias force if needed
if (this.type !== 'flat' && foldPercent > 0.001) {
const hingeDir = new THREE.Vector3().subVectors(this.vD.pos, this.vC.pos).normalize();
const n1 = new THREE.Vector3().subVectors(this.vA.pos, this.vC.pos).cross(hingeDir).normalize();
const n2 = hingeDir.clone().cross(new THREE.Vector3().subVectors(this.vB.pos, this.vC.pos)).normalize();
const dot = n1.dot(n2);
if (dot > 0.99 || isNaN(dot)) { // almost flat
const forceDir = this.type === 'mountain' ? 1 : -1;
const push = new THREE.Vector3(0, 0, forceDir * 0.02); // Global Z push
if (!this.vA.fixed) this.vA.pos.sub(push);
if (!this.vB.fixed) this.vB.pos.sub(push);
if (!this.vC.fixed) this.vC.pos.add(push);
if (!this.vD.fixed) this.vD.pos.add(push);
}
}
}
}
export class OrigamiSimulation {
vertices: Vertex[] = [];
structuralSprings: DistanceConstraint[] = [];
creases: CreaseConstraint[] = [];
faces: number[][] = [];
constructor(pattern: Pattern) {
this.vertices = pattern.vertices.map(v => new Vertex(v[0], v[1]));
this.faces = pattern.faces;
// Fix the center vertex to prevent floating away
let centerIdx = 0;
let minDist = Infinity;
this.vertices.forEach((v, i) => {
const dist = v.originalPos.lengthSq();
if (dist < minDist) {
minDist = dist;
centerIdx = i;
}
});
this.vertices[centerIdx].fixed = true;
// Fix another vertex connected to the center vertex to prevent rotation
// Find an edge connected to centerIdx
const connectedEdge = pattern.faces.find(f => f.includes(centerIdx));
if (connectedEdge) {
const otherIdx = connectedEdge.find(v => v !== centerIdx);
if (otherIdx !== undefined) {
this.vertices[otherIdx].fixed = true;
}
// Fix a third vertex to completely prevent spinning in 3D
const thirdIdx = connectedEdge.find(v => v !== centerIdx && v !== otherIdx);
if (thirdIdx !== undefined) {
this.vertices[thirdIdx].fixed = true;
}
}
// Build structural springs (edges of faces)
const edgeMap = new Map<string, { faces: number[], v1: number, v2: number }>();
const addEdge = (v1: number, v2: number, faceIdx: number) => {
const key = v1 < v2 ? `${v1}-${v2}` : `${v2}-${v1}`;
if (!edgeMap.has(key)) {
edgeMap.set(key, { faces: [], v1, v2 });
this.structuralSprings.push(new DistanceConstraint(this.vertices[v1], this.vertices[v2]));
}
edgeMap.get(key)!.faces.push(faceIdx);
};
this.faces.forEach((face, i) => {
addEdge(face[0], face[1], i);
addEdge(face[1], face[2], i);
addEdge(face[2], face[0], i);
});
// Build creases
edgeMap.forEach((edge, key) => {
if (edge.faces.length === 2) {
const f1 = this.faces[edge.faces[0]];
const f2 = this.faces[edge.faces[1]];
// Find opposite vertices
const vA_idx = f1.find(v => v !== edge.v1 && v !== edge.v2)!;
const vB_idx = f2.find(v => v !== edge.v1 && v !== edge.v2)!;
// Check if this edge is in pattern.creases
let type: CreaseType = 'flat';
const creaseDef = pattern.creases.find(c =>
(c.edge[0] === edge.v1 && c.edge[1] === edge.v2) ||
(c.edge[0] === edge.v2 && c.edge[1] === edge.v1)
);
if (creaseDef) {
type = creaseDef.type;
}
this.creases.push(new CreaseConstraint(
this.vertices[vA_idx],
this.vertices[vB_idx],
this.vertices[edge.v1],
this.vertices[edge.v2],
type
));
}
});
// Add initial noise to break symmetry
this.vertices.forEach(v => {
v.pos.z += (Math.random() - 0.5) * 0.01;
v.oldPos.copy(v.pos);
});
}
step(foldPercent: number) {
const damping = 0.8; // Increased damping for stability
for (const v of this.vertices) {
if (v.fixed) continue;
const velocity = new THREE.Vector3().subVectors(v.pos, v.oldPos).multiplyScalar(damping);
v.oldPos.copy(v.pos);
v.pos.add(velocity);
}
const _tri = new THREE.Triangle();
const _closest = new THREE.Vector3();
const _delta = new THREE.Vector3();
const _faceCorrection = new THREE.Vector3();
const thickness = 0.06; // Increased thickness for impenetrable paper
const thicknessSq = thickness * thickness;
// Solve constraints iteratively
const iterations = 60; // More iterations for rigid collisions
for (let i = 0; i < iterations; i++) {
for (const crease of this.creases) {
crease.solve(foldPercent);
}
for (const spring of this.structuralSprings) {
spring.solve();
}
// Self-collision (Vertex-Face repulsion)
for (let j = 0; j < this.vertices.length; j++) {
const v = this.vertices[j];
for (let f = 0; f < this.faces.length; f++) {
const face = this.faces[f];
if (face.includes(j)) continue;
const vA = this.vertices[face[0]];
const vB = this.vertices[face[1]];
const vC = this.vertices[face[2]];
_tri.set(vA.pos, vB.pos, vC.pos);
_tri.closestPointToPoint(v.pos, _closest);
_delta.subVectors(v.pos, _closest); // Vector FROM face TO vertex
const distSq = _delta.lengthSq();
if (distSq < thicknessSq) {
let dist = Math.sqrt(distSq);
let overlap = thickness - dist;
if (dist < 0.0001) {
_tri.getNormal(_delta); // Use normal if exactly touching
} else {
_delta.normalize();
}
// Apply strong repulsion force
_delta.multiplyScalar(overlap * 0.9);
if (!v.fixed) v.pos.addScaledVector(_delta, 0.5);
_faceCorrection.copy(_delta).multiplyScalar(-0.5 / 3);
if (!vA.fixed) vA.pos.add(_faceCorrection);
if (!vB.fixed) vB.pos.add(_faceCorrection);
if (!vC.fixed) vC.pos.add(_faceCorrection);
}
}
}
}
}
}