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(); 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); } } } } } }