// Earthquake House Collapse Simulator v3 // Fixed physics + realistic building appearance let scene, camera, renderer; let buildingParts = []; let isSimulating = false; let simulationTime = 0; let capturedImages = []; let rescueMarkers = []; let groundMesh, pathMesh; let cameraBasePos = { x: 0, y: 0, z: 0 }; let quakeDir = 0; // random earthquake primary direction each run let quakeSeed = 0; // random seed per simulation for variation let config = { buildingStyle: 'european', stories: 2, material: 'concrete', foundation: 'rock', magnitude: 7.0, distance: 5, duration: 15, buildingAge: 30 }; const materialProps = { concrete: { density: 2400, strength: 30, brittleness: 0.6, color: 0xd4cfc4, wallColor: 0xe8e4dc }, unreinforcedMasonry: { density: 1800, strength: 8, brittleness: 0.9, color: 0xc4956a, wallColor: 0xd4a574 }, wood: { density: 600, strength: 12, brittleness: 0.3, color: 0x8B6914, wallColor: 0xf5e6d0 }, steel: { density: 7800, strength: 250, brittleness: 0.2, color: 0x708090, wallColor: 0xdcdcdc }, adobe: { density: 1500, strength: 3, brittleness: 0.95, color: 0xc9a86c, wallColor: 0xd4b896 } }; const foundationFactors = { rock: 1.0, stiffSoil: 1.3, softSoil: 1.8, liquefiable: 2.5 }; const styleConfigs = { european: { roofType: 'pitched', roofColor: 0x3d3d3d, hasShutters: true }, chinese: { roofType: 'curved', roofColor: 0x4a4a4a, hasShutters: false }, japanese: { roofType: 'hip', roofColor: 0x2a3a5a, hasShutters: false }, american: { roofType: 'gable', roofColor: 0x4a4a4a, hasShutters: true }, middleEast: { roofType: 'flat', roofColor: 0xc9a86c, hasShutters: false } }; // ==================== SCENE SETUP ==================== function initScene() { const viewport = document.getElementById('viewport'); scene = new THREE.Scene(); scene.background = new THREE.Color(0x87CEEB); scene.fog = new THREE.Fog(0x87CEEB, 60, 120); camera = new THREE.PerspectiveCamera(50, viewport.clientWidth / viewport.clientHeight, 0.1, 500); camera.position.set(18, 14, 18); camera.lookAt(0, 3, 0); renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); renderer.setSize(viewport.clientWidth, viewport.clientHeight); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; viewport.appendChild(renderer.domElement); // Lights scene.add(new THREE.AmbientLight(0x606060, 0.6)); const sun = new THREE.DirectionalLight(0xfff8e0, 1.0); sun.position.set(8, 15, 10); sun.castShadow = true; sun.shadow.mapSize.set(2048, 2048); sun.shadow.camera.left = -20; sun.shadow.camera.right = 20; sun.shadow.camera.top = 20; sun.shadow.camera.bottom = -20; scene.add(sun); scene.add(new THREE.HemisphereLight(0x87CEEB, 0x3a5f0b, 0.3)); // Ground groundMesh = new THREE.Mesh( new THREE.PlaneGeometry(80, 80), new THREE.MeshStandardMaterial({ color: 0x4a7c3f, roughness: 0.95 }) ); groundMesh.rotation.x = -Math.PI / 2; groundMesh.receiveShadow = true; scene.add(groundMesh); // Pathway pathMesh = new THREE.Mesh( new THREE.PlaneGeometry(2.5, 10), new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.9 }) ); pathMesh.rotation.x = -Math.PI / 2; pathMesh.position.set(0, 0.01, 8); scene.add(pathMesh); setupCameraControls(viewport); } function setupCameraControls(el) { let dragging = false; let prev = { x: 0, y: 0 }; let theta = Math.PI / 4; // horizontal angle let phi = Math.PI / 3.5; // vertical angle (0=top, PI/2=horizon) let radius = 25; let target = new THREE.Vector3(0, 3, 0); // orbit center function updateCameraOrbit() { camera.position.set( target.x + radius * Math.sin(phi) * Math.cos(theta), target.y + radius * Math.cos(phi), target.z + radius * Math.sin(phi) * Math.sin(theta) ); camera.lookAt(target); } // Expose for earthquake shaking to read current position window.getCameraOrbitPos = function() { return { x: target.x + radius * Math.sin(phi) * Math.cos(theta), y: target.y + radius * Math.cos(phi), z: target.z + radius * Math.sin(phi) * Math.sin(theta) }; }; window.resetCameraOrbit = function() { updateCameraOrbit(); }; el.addEventListener('mousedown', function(e) { if (e.target.tagName !== 'CANVAS') return; dragging = true; prev = { x: e.clientX, y: e.clientY }; e.preventDefault(); }); el.addEventListener('mousemove', function(e) { if (!dragging) return; var dx = e.clientX - prev.x; var dy = e.clientY - prev.y; prev = { x: e.clientX, y: e.clientY }; if (e.shiftKey || e.buttons === 4) { // Pan (shift+drag or middle mouse) var panSpeed = 0.03 * radius / 25; var right = new THREE.Vector3(); var up = new THREE.Vector3(0, 1, 0); right.crossVectors(camera.getWorldDirection(new THREE.Vector3()), up).normalize(); target.add(right.multiplyScalar(-dx * panSpeed)); target.y += dy * panSpeed; } else { // Orbit: horizontal drag rotates around Y axis theta += dx * 0.008; // Vertical drag: drag up = go higher, drag down = go lower phi = Math.max(0.2, Math.min(Math.PI / 2 - 0.05, phi - dy * 0.006)); } updateCameraOrbit(); }); el.addEventListener('mouseup', function() { dragging = false; }); el.addEventListener('mouseleave', function() { dragging = false; }); el.addEventListener('wheel', function(e) { if (e.target.tagName !== 'CANVAS') return; radius = Math.max(8, Math.min(55, radius + e.deltaY * 0.025)); updateCameraOrbit(); e.preventDefault(); }); // Touch support for mobile var lastTouch = null; el.addEventListener('touchstart', function(e) { if (e.touches.length === 1) { lastTouch = { x: e.touches[0].clientX, y: e.touches[0].clientY }; } }); el.addEventListener('touchmove', function(e) { if (!lastTouch || e.touches.length !== 1) return; var dx = e.touches[0].clientX - lastTouch.x; var dy = e.touches[0].clientY - lastTouch.y; lastTouch = { x: e.touches[0].clientX, y: e.touches[0].clientY }; theta += dx * 0.008; phi = Math.max(0.2, Math.min(Math.PI / 2 - 0.05, phi - dy * 0.006)); updateCameraOrbit(); e.preventDefault(); }); el.addEventListener('touchend', function() { lastTouch = null; }); updateCameraOrbit(); } // ==================== BUILD HOUSE ==================== function buildHouse() { clearAll(); const style = styleConfigs[config.buildingStyle]; const mat = materialProps[config.material]; const stories = parseInt(config.stories); const H = 3.0, W = 10, D = 7; // Foundation (never collapses) addBlock(0, 0.15, 0, W+0.4, 0.3, D+0.4, 0x555555, 'foundation', -1); // Build each story for (let f = 0; f < stories; f++) { const by = f * H + 0.3; buildStory(by, H, W, D, mat, style, f); } // Roof buildRoof(stories * H + 0.3, W, D, style); // Exterior details buildDetails(W, D, stories, H, style); // Room layout for rescue var layout = { groundFloor: [ { name: 'Living Room', x: -2, z: 0, w: 4, d: 5 }, { name: 'Kitchen', x: 3, z: -1, w: 3, d: 3 }, { name: 'Bathroom', x: 3, z: 2, w: 2, d: 2 } ], floors: [] }; for (var fi = 1; fi < stories; fi++) { layout.floors.push([ { name: 'Bedroom ' + fi, x: -2, z: 0, w: 4, d: 4, floor: fi }, { name: (fi === 1 ? 'Master Bedroom' : 'Bedroom ' + (fi + 1)), x: 3, z: 0, w: 3, d: 3, floor: fi }, { name: 'Hallway F' + (fi + 1), x: 0.5, z: 3, w: 8, d: 1.5, floor: fi } ]); } window.roomLayout = layout; } function buildStory(by, H, W, D, mat, style, floorIdx) { if (config.buildingStyle === 'chinese') { buildChineseStory(by, H, W, D, floorIdx); } else if (config.buildingStyle === 'japanese') { buildJapaneseStory(by, H, W, D, floorIdx); } else if (config.buildingStyle === 'american') { buildAmericanStory(by, H, W, D, floorIdx); } else { buildWesternStory(by, H, W, D, mat, style, floorIdx); } } function buildChineseStory(by, H, W, D, floorIdx) { var colColor = 0xaa2222; // vermilion red columns var beamColor = 0x6b2020; // dark red beams var latticeColor = 0x1a5c5c; // teal/green lattice panels var baseColor = 0x888888; // gray stone base var bandColor = 0x1a7a5c; // green decorative band var colR = 0.2; // column radius (using box) var colSpacing = W / 4; // 5 columns across front // Stone platform base (for ground floor only) if (floorIdx === 0) { addBlock(0, by + 0.15, 0, W + 1.0, 0.3, D + 1.0, baseColor, 'foundation', -1); } // Floor slab if (floorIdx > 0) { addBlock(0, by + 0.1, 0, W + 0.5, 0.2, D + 0.5, 0x777777, 'floor', floorIdx); } // ---- COLUMNS (the main structural system) ---- // Front and back rows of columns for (var i = 0; i <= 4; i++) { var cx = -W/2 + i * colSpacing; // Front columns addBlock(cx, by + H/2, D/2, colR*2, H, colR*2, colColor, 'column', floorIdx); // Back columns addBlock(cx, by + H/2, -D/2, colR*2, H, colR*2, colColor, 'column', floorIdx); } // Side columns (middle) addBlock(-W/2, by + H/2, 0, colR*2, H, colR*2, colColor, 'column', floorIdx); addBlock(W/2, by + H/2, 0, colR*2, H, colR*2, colColor, 'column', floorIdx); // ---- BEAMS (horizontal, connecting columns at top) ---- // Front beam addBlock(0, by + H - 0.15, D/2, W, 0.3, colR*2, beamColor, 'beam', floorIdx); // Back beam addBlock(0, by + H - 0.15, -D/2, W, 0.3, colR*2, beamColor, 'beam', floorIdx); // Side beams addBlock(-W/2, by + H - 0.15, 0, colR*2, 0.3, D, beamColor, 'beam', floorIdx); addBlock(W/2, by + H - 0.15, 0, colR*2, 0.3, D, beamColor, 'beam', floorIdx); // ---- DECORATIVE BAND (green/teal) under beams ---- addBlock(0, by + H - 0.45, D/2, W, 0.2, 0.05, bandColor, 'trim', floorIdx); addBlock(0, by + H - 0.45, -D/2, W, 0.2, 0.05, bandColor, 'trim', floorIdx); // ---- LATTICE PANELS between columns (front) ---- for (var i = 0; i < 4; i++) { var px = -W/2 + i * colSpacing + colSpacing/2; var panelH = H * 0.7; var panelY = by + panelH/2 + 0.3; if (floorIdx === 0 && i === 2) { // Door opening in center addBlock(px, by + 1.2, D/2, colSpacing - colR*3, 2.4, 0.08, 0x8b3a1a, 'door', floorIdx); } else { // Lattice window panel addBlock(px, panelY, D/2, colSpacing - colR*3, panelH, 0.06, latticeColor, 'lattice', floorIdx); // Small bottom wall panel (kickplate) addBlock(px, by + 0.15, D/2, colSpacing - colR*3, 0.3, 0.1, baseColor, 'front_wall', floorIdx); } } // ---- LATTICE PANELS (back wall - more solid) ---- for (var i = 0; i < 4; i++) { var px = -W/2 + i * colSpacing + colSpacing/2; addBlock(px, by + H*0.4, -D/2, colSpacing - colR*3, H*0.7, 0.15, 0xd4a574, 'back_wall', floorIdx); } // ---- SIDE WALLS (solid with small windows) ---- addBlock(-W/2, by + H/2, 0, 0.15, H, D - 0.5, 0xd4a574, 'left_wall', floorIdx); addBlock(W/2, by + H/2, 0, 0.15, H, D - 0.5, 0xd4a574, 'right_wall', floorIdx); // Ceiling/floor beams (cross beams visible from below) addBlock(0, by + H - 0.05, 0, W, 0.1, D, 0x8b5a2b, 'ceiling', floorIdx); } function buildJapaneseStory(by, H, W, D, floorIdx) { var frameColor = 0x3d2b1f; // dark brown timber frame var shojiColor = 0xf5e8d0; // warm cream shoji panels var wallColor = 0xe8dcc8; // light plaster walls var balconyColor = 0x6b4c2a; // medium brown wood var colR = 0.15; var colSpacing = W / 5; // 6 columns across // Floor platform if (floorIdx === 0) { addBlock(0, by + 0.1, 0, W + 0.6, 0.2, D + 0.6, 0x5c4030, 'floor', floorIdx); } else { addBlock(0, by + 0.1, 0, W + 0.8, 0.15, D + 0.8, balconyColor, 'floor', floorIdx); } // ---- COLUMNS (dark timber frame) ---- for (var i = 0; i <= 5; i++) { var cx = -W/2 + i * colSpacing; addBlock(cx, by + H/2, D/2, colR*2, H, colR*2, frameColor, 'column', floorIdx); addBlock(cx, by + H/2, -D/2, colR*2, H, colR*2, frameColor, 'column', floorIdx); } // Corner and middle side columns addBlock(-W/2, by + H/2, 0, colR*2, H, colR*2, frameColor, 'column', floorIdx); addBlock(W/2, by + H/2, 0, colR*2, H, colR*2, frameColor, 'column', floorIdx); // ---- HORIZONTAL BEAMS (dark frame) ---- // Top beam (front/back) addBlock(0, by + H - 0.1, D/2, W, 0.2, colR*2, frameColor, 'beam', floorIdx); addBlock(0, by + H - 0.1, -D/2, W, 0.2, colR*2, frameColor, 'beam', floorIdx); // Mid-height beam (front) - nageshi addBlock(0, by + H*0.65, D/2, W, 0.1, colR, frameColor, 'beam', floorIdx); // Bottom rail addBlock(0, by + 0.25, D/2, W, 0.1, colR, frameColor, 'beam', floorIdx); // Side beams addBlock(-W/2, by + H - 0.1, 0, colR*2, 0.2, D, frameColor, 'beam', floorIdx); addBlock(W/2, by + H - 0.1, 0, colR*2, 0.2, D, frameColor, 'beam', floorIdx); // ---- SHOJI / WALL PANELS (front) ---- for (var i = 0; i < 5; i++) { var px = -W/2 + i * colSpacing + colSpacing/2; var panelH = H * 0.62; var panelY = by + 0.3 + panelH/2; if (floorIdx === 0 && (i === 2 || i === 3)) { // Sliding door opening - translucent shoji var shojiMat = new THREE.MeshStandardMaterial({ color: 0xfff8e8, transparent: true, opacity: 0.6, roughness: 0.9, side: THREE.DoubleSide }); var shoji = new THREE.Mesh(new THREE.BoxGeometry(colSpacing - colR*3, panelH, 0.04), shojiMat); shoji.position.set(px, panelY, D/2); shoji.castShadow = true; shoji.receiveShadow = true; scene.add(shoji); buildingParts.push(makePart(shoji, 'shoji', floorIdx)); // Shoji grid lines (horizontal) addBlock(px, panelY, D/2 + 0.025, colSpacing - colR*3, 0.02, 0.01, frameColor, 'shoji_frame', floorIdx); addBlock(px, panelY - panelH*0.3, D/2 + 0.025, colSpacing - colR*3, 0.02, 0.01, frameColor, 'shoji_frame', floorIdx); addBlock(px, panelY + panelH*0.3, D/2 + 0.025, colSpacing - colR*3, 0.02, 0.01, frameColor, 'shoji_frame', floorIdx); } else { // Upper portion: translucent shoji or window var upperH = panelH * 0.55; var upperY = by + H*0.65 - upperH/2 - 0.1; var shojiMat2 = new THREE.MeshStandardMaterial({ color: shojiColor, transparent: true, opacity: 0.7, roughness: 0.95 }); var panel = new THREE.Mesh(new THREE.BoxGeometry(colSpacing - colR*3, upperH, 0.04), shojiMat2); panel.position.set(px, upperY, D/2); panel.castShadow = true; scene.add(panel); buildingParts.push(makePart(panel, 'shoji', floorIdx)); // Lower portion: opaque wall panel var lowerH = panelH * 0.4; addBlock(px, by + 0.3 + lowerH/2, D/2, colSpacing - colR*3, lowerH, 0.08, wallColor, 'front_wall', floorIdx); } } // ---- BACK WALL (more solid) ---- addBlock(0, by + H/2, -D/2, W, H*0.9, 0.12, wallColor, 'back_wall', floorIdx); // ---- SIDE WALLS (solid plaster with dark frame band at top) ---- addBlock(-W/2, by + H*0.45, 0, 0.12, H*0.8, D - 0.5, wallColor, 'left_wall', floorIdx); addBlock(W/2, by + H*0.45, 0, 0.12, H*0.8, D - 0.5, wallColor, 'right_wall', floorIdx); // Dark band at top of side walls addBlock(-W/2, by + H*0.85, 0, 0.14, H*0.12, D - 0.3, frameColor, 'trim', floorIdx); addBlock(W/2, by + H*0.85, 0, 0.14, H*0.12, D - 0.3, frameColor, 'trim', floorIdx); // ---- ENGAWA (veranda/balcony) on upper floors ---- if (floorIdx > 0) { // Balcony floor extending out front addBlock(0, by + 0.05, D/2 + 0.5, W + 0.4, 0.08, 1.0, balconyColor, 'balcony', floorIdx); // Railing addBlock(0, by + 0.7, D/2 + 0.95, W + 0.3, 0.05, 0.05, balconyColor, 'railing', floorIdx); // Railing posts for (var i = 0; i <= 4; i++) { addBlock(-W/2 + i*(W/4), by + 0.35, D/2 + 0.95, 0.06, 0.6, 0.06, balconyColor, 'railing', floorIdx); } } // Ceiling addBlock(0, by + H - 0.05, 0, W, 0.1, D, 0x5c4a3a, 'ceiling', floorIdx); } function buildAmericanStory(by, H, W, D, floorIdx) { var siding = 0xf0ece4; // white/cream clapboard siding var trimColor = 0xffffff; // bright white trim var windowFrame = 0x2a2a2a; // dark/black window frames var doorColor = 0x1a3a4a; // dark teal/navy door var t = 0.2; // wall thickness var winH = 1.6; // window height var winW = 1.1; // window width var winBot = H * 0.3; // window bottom from floor // Floor if (floorIdx > 0) { addBlock(0, by + 0.1, 0, W, 0.2, D, 0x999999, 'floor', floorIdx); } // ---- FRONT WALL (+Z) ---- if (floorIdx === 0) { // Build wall with door opening and 2 window openings var doorW = 1.3, doorH = 2.3; // Left of left window addBlock(-W/2 + 0.7, by + H/2, D/2, 1.4, H, t, siding, 'front_wall', floorIdx); // Between left window and door addBlock(-1.0, by + H/2, D/2, 1.0, H, t, siding, 'front_wall', floorIdx); // Between door and right window addBlock(1.0, by + H/2, D/2, 1.0, H, t, siding, 'front_wall', floorIdx); // Right of right window addBlock(W/2 - 0.7, by + H/2, D/2, 1.4, H, t, siding, 'front_wall', floorIdx); // Above door addBlock(0, by + doorH + 0.35, D/2, doorW + 0.2, H - doorH - 0.2, t, siding, 'front_wall', floorIdx); // Above left window addBlock(-2.5, by + winBot + winH + (H - winBot - winH)/2, D/2, winW + 0.3, H - winBot - winH, t, siding, 'front_wall', floorIdx); // Below left window addBlock(-2.5, by + winBot/2, D/2, winW + 0.3, winBot, t, siding, 'front_wall', floorIdx); // Above right window addBlock(2.5, by + winBot + winH + (H - winBot - winH)/2, D/2, winW + 0.3, H - winBot - winH, t, siding, 'front_wall', floorIdx); // Below right window addBlock(2.5, by + winBot/2, D/2, winW + 0.3, winBot, t, siding, 'front_wall', floorIdx); // Door addBlock(0, by + doorH/2, D/2 + 0.05, doorW, doorH, 0.08, doorColor, 'door', floorIdx); // Windows (visible because wall has openings) addBlock(-2.5, by + winBot + winH/2, D/2 + 0.05, winW, winH, 0.05, 0x6daed0, 'glass', floorIdx); addBlock(2.5, by + winBot + winH/2, D/2 + 0.05, winW, winH, 0.05, 0x6daed0, 'glass', floorIdx); // Window frames (dark) addBlock(-2.5, by + winBot + winH/2, D/2 + 0.1, winW + 0.15, winH + 0.15, 0.03, windowFrame, 'trim', floorIdx); addBlock(2.5, by + winBot + winH/2, D/2 + 0.1, winW + 0.15, winH + 0.15, 0.03, windowFrame, 'trim', floorIdx); // Window sills (white) addBlock(-2.5, by + winBot - 0.05, D/2 + 0.12, winW + 0.3, 0.08, 0.12, trimColor, 'trim', floorIdx); addBlock(2.5, by + winBot - 0.05, D/2 + 0.12, winW + 0.3, 0.08, 0.12, trimColor, 'trim', floorIdx); } else { // Upper floor front wall with 3 windows var winPositions = [-2.5, 0, 2.5]; // Wall segments between windows addBlock(-W/2 + 0.5, by + H/2, D/2, 1.0, H, t, siding, 'front_wall', floorIdx); addBlock(-1.25, by + H/2, D/2, 1.3, H, t, siding, 'front_wall', floorIdx); addBlock(1.25, by + H/2, D/2, 1.3, H, t, siding, 'front_wall', floorIdx); addBlock(W/2 - 0.5, by + H/2, D/2, 1.0, H, t, siding, 'front_wall', floorIdx); // Above and below each window winPositions.forEach(function(wx) { addBlock(wx, by + winBot/2, D/2, winW + 0.1, winBot, t, siding, 'front_wall', floorIdx); addBlock(wx, by + winBot + winH + (H - winBot - winH)/2, D/2, winW + 0.1, H - winBot - winH, t, siding, 'front_wall', floorIdx); // Glass addBlock(wx, by + winBot + winH/2, D/2 + 0.05, winW, winH, 0.05, 0x6daed0, 'glass', floorIdx); // Frame addBlock(wx, by + winBot + winH/2, D/2 + 0.1, winW + 0.12, winH + 0.12, 0.03, windowFrame, 'trim', floorIdx); // Sill addBlock(wx, by + winBot - 0.05, D/2 + 0.12, winW + 0.25, 0.08, 0.1, trimColor, 'trim', floorIdx); // Mullion (cross divider) addBlock(wx, by + winBot + winH/2, D/2 + 0.12, 0.04, winH, 0.02, windowFrame, 'trim', floorIdx); addBlock(wx, by + winBot + winH/2, D/2 + 0.12, winW, 0.04, 0.02, windowFrame, 'trim', floorIdx); }); } // ---- BACK WALL (-Z) with 2 windows ---- addBlock(-W/2 + 1.2, by + H/2, -D/2, 2.4, H, t, siding, 'back_wall', floorIdx); addBlock(0, by + H/2, -D/2, W - 5.6, H, t, siding, 'back_wall', floorIdx); addBlock(W/2 - 1.2, by + H/2, -D/2, 2.4, H, t, siding, 'back_wall', floorIdx); addBlock(-2.2, by + winBot/2, -D/2, winW + 0.1, winBot, t, siding, 'back_wall', floorIdx); addBlock(-2.2, by + winBot + winH + (H-winBot-winH)/2, -D/2, winW+0.1, H-winBot-winH, t, siding, 'back_wall', floorIdx); addBlock(2.2, by + winBot/2, -D/2, winW + 0.1, winBot, t, siding, 'back_wall', floorIdx); addBlock(2.2, by + winBot + winH + (H-winBot-winH)/2, -D/2, winW+0.1, H-winBot-winH, t, siding, 'back_wall', floorIdx); addBlock(-2.2, by + winBot + winH/2, -D/2 - 0.05, winW, winH, 0.05, 0x6daed0, 'glass', floorIdx); addBlock(2.2, by + winBot + winH/2, -D/2 - 0.05, winW, winH, 0.05, 0x6daed0, 'glass', floorIdx); // ---- SIDE WALLS with windows ---- // Left wall: 2 window openings addBlock(-W/2, by + H/2, -D/2 + 1.0, t, H, 2.0, siding, 'left_wall', floorIdx); addBlock(-W/2, by + H/2, 0, t, H, 1.0, siding, 'left_wall', floorIdx); addBlock(-W/2, by + H/2, D/2 - 1.0, t, H, 2.0, siding, 'left_wall', floorIdx); addBlock(-W/2, by + winBot/2, -1.5, t, winBot, 1.2, siding, 'left_wall', floorIdx); addBlock(-W/2, by + winBot+winH+(H-winBot-winH)/2, -1.5, t, H-winBot-winH, 1.2, siding, 'left_wall', floorIdx); addBlock(-W/2, by + winBot/2, 1.5, t, winBot, 1.2, siding, 'left_wall', floorIdx); addBlock(-W/2, by + winBot+winH+(H-winBot-winH)/2, 1.5, t, H-winBot-winH, 1.2, siding, 'left_wall', floorIdx); addBlock(-W/2 - 0.05, by + winBot + winH/2, -1.5, 0.05, winH, 1.0, 0x6daed0, 'glass', floorIdx); addBlock(-W/2 - 0.05, by + winBot + winH/2, 1.5, 0.05, winH, 1.0, 0x6daed0, 'glass', floorIdx); // Right wall: same pattern addBlock(W/2, by + H/2, -D/2 + 1.0, t, H, 2.0, siding, 'right_wall', floorIdx); addBlock(W/2, by + H/2, 0, t, H, 1.0, siding, 'right_wall', floorIdx); addBlock(W/2, by + H/2, D/2 - 1.0, t, H, 2.0, siding, 'right_wall', floorIdx); addBlock(W/2, by + winBot/2, -1.5, t, winBot, 1.2, siding, 'right_wall', floorIdx); addBlock(W/2, by + winBot+winH+(H-winBot-winH)/2, -1.5, t, H-winBot-winH, 1.2, siding, 'right_wall', floorIdx); addBlock(W/2, by + winBot/2, 1.5, t, winBot, 1.2, siding, 'right_wall', floorIdx); addBlock(W/2, by + winBot+winH+(H-winBot-winH)/2, 1.5, t, H-winBot-winH, 1.2, siding, 'right_wall', floorIdx); addBlock(W/2 + 0.05, by + winBot + winH/2, -1.5, 0.05, winH, 1.0, 0x6daed0, 'glass', floorIdx); addBlock(W/2 + 0.05, by + winBot + winH/2, 1.5, 0.05, winH, 1.0, 0x6daed0, 'glass', floorIdx); // ---- WHITE CORNER TRIM ---- addBlock(-W/2 - 0.08, by + H/2, D/2, 0.12, H, 0.12, trimColor, 'trim', floorIdx); addBlock(W/2 + 0.08, by + H/2, D/2, 0.12, H, 0.12, trimColor, 'trim', floorIdx); addBlock(-W/2 - 0.08, by + H/2, -D/2, 0.12, H, 0.12, trimColor, 'trim', floorIdx); addBlock(W/2 + 0.08, by + H/2, -D/2, 0.12, H, 0.12, trimColor, 'trim', floorIdx); // Internal wall addBlock(0, by + H/2, -D/6, 0.15, H, D*0.5, siding, 'int_wall', floorIdx); // Ceiling addBlock(0, by + H - 0.1, 0, W, 0.15, D, 0x999999, 'ceiling', floorIdx); } function buildWesternStory(by, H, W, D, mat, style, floorIdx) { const wc = mat.wallColor; const t = 0.3; if (floorIdx === 0) { addBlock(-W/2 + 1.5, by + H/2, D/2, W/2 - 1.5, H, t, wc, 'front_wall', floorIdx); addBlock(1.5, by + H/2, D/2, W/2 - 1.5, H, t, wc, 'front_wall', floorIdx); addBlock(0, by + 2.6, D/2, 2.0, 0.4, t, wc, 'front_wall', floorIdx); addBlock(0, by + 1.15, D/2 + 0.05, 1.8, 2.3, 0.08, 0x5c3a1e, 'door', floorIdx); } else { buildWallWithWindows(-W/2, by, D/2, W, H, t, wc, 'front_wall', floorIdx, 3, true); } buildWallWithWindows(-W/2, by, -D/2 - t, W, H, t, wc, 'back_wall', floorIdx, 3, true); buildWallWithWindows(-W/2 - t, by, -D/2, t, H, D, wc, 'left_wall', floorIdx, 2, false); buildWallWithWindows(W/2, by, -D/2, t, H, D, wc, 'right_wall', floorIdx, 2, false); addBlock(0, by + H/2, -D/6, 0.2, H, D*0.5, wc, 'int_wall', floorIdx); if (floorIdx > 0) { addBlock(0, by + 0.1, 0, W, 0.2, D, 0x999999, 'floor', floorIdx); } addBlock(0, by + H - 0.1, 0, W, 0.2, D, 0x999999, 'ceiling', floorIdx); } function buildWallWithWindows(x, y, z, w, h, d, color, label, floor, numWin, isXWall) { // Wall runs along X (isXWall=true) or along Z (isXWall=false) const len = isXWall ? w : d; const sections = numWin * 2 + 1; const secLen = len / sections; const winH = 1.3, winBottom = h * 0.35; for (let i = 0; i < sections; i++) { const isWin = (i % 2 === 1); if (isXWall) { const sx = x + i * secLen; const cx = sx + secLen / 2; const cz = z + d / 2; if (isWin) { // Below window addBlock(cx, y + winBottom/2, cz, secLen*0.98, winBottom, d, color, label, floor); // Above window const aboveH = h - winBottom - winH; addBlock(cx, y + winBottom + winH + aboveH/2, cz, secLen*0.98, aboveH, d, color, label, floor); // Window glass addBlock(cx, y + winBottom + winH/2, cz, secLen*0.7, winH*0.9, 0.03, 0x88ccdd, 'glass', floor); } else { addBlock(cx, y + h/2, cz, secLen*0.98, h*0.98, d, color, label, floor); } } else { const sz = z + i * secLen; const cz = sz + secLen / 2; const cx = x + w / 2; if (isWin) { addBlock(cx, y + winBottom/2, cz, w, winBottom, secLen*0.98, color, label, floor); const aboveH = h - winBottom - winH; addBlock(cx, y + winBottom + winH + aboveH/2, cz, w, aboveH, secLen*0.98, color, label, floor); addBlock(cx, y + winBottom + winH/2, cz, 0.03, winH*0.9, secLen*0.7, 0x88ccdd, 'glass', floor); } else { addBlock(cx, y + h/2, cz, w, h*0.98, secLen*0.98, color, label, floor); } } } } function buildRoof(baseY, W, D, style) { const rc = style.roofColor; const oh = 0.8; // overhang if (style.roofType === 'flat') { addBlock(0, baseY + 0.15, 0, W + oh*2, 0.3, D + oh*2, rc, 'roof', 2); } else if (style.roofType === 'pitched' || style.roofType === 'gable') { const rh = 2.5, segs = 8; for (let side = -1; side <= 1; side += 2) { for (let i = 0; i < segs; i++) { const t = (i + 0.5) / segs; const panelD = (D/2 + oh) / segs; const py = baseY + rh * (1 - t); const pz = side * t * (D/2 + oh); const angle = side * Math.atan2(rh, D/2 + oh); const m = createBoxMesh(W + oh*2, 0.12, panelD, rc); m.position.set(0, py, pz); m.rotation.x = angle; m.castShadow = true; m.receiveShadow = true; scene.add(m); buildingParts.push(makePart(m, 'roof', 2)); } } // Ridge addBlock(0, baseY + rh, 0, W + oh*2, 0.15, 0.15, 0x4a3728, 'roof', 2); } else if (style.roofType === 'hip') { // Japanese hip roof (irimoya-yane): deep blue-gray tiles, wide overhang, gentle slope const rh = 2.2; const roofW = W + 2.5; // wide overhang const roofD = D + 2.0; const segs = 8; const tileColor = 0x2a3a5a; // deep blue-gray // Front and back slopes for (let side = -1; side <= 1; side += 2) { for (let i = 0; i < segs; i++) { const t = (i + 0.5) / segs; const panelD = (roofD/2) / segs; // Slight concave curve for elegance const curveT = Math.pow(t, 0.85); const py = baseY + rh * (1 - curveT); const pz = side * t * (roofD/2); const panelW = roofW * (1 - t * 0.25); // narrows toward top const m = createBoxMesh(panelW, 0.1, panelD, tileColor); m.position.set(0, py, pz); m.rotation.x = side * Math.atan2(rh, roofD/2) * 0.85; m.castShadow = true; m.receiveShadow = true; scene.add(m); buildingParts.push(makePart(m, 'roof', 2)); } } // Ridge addBlock(0, baseY + rh + 0.05, 0, roofW * 0.6, 0.15, 0.15, 0x333333, 'roof', 2); // Ridge end ornament (鬼瓦 onigawara) addBlock(0, baseY + rh + 0.2, 0, 0.3, 0.3, 0.3, 0x333333, 'ornament', 2); // Fascia boards (dark trim at eave edges) addBlock(0, baseY + 0.1, roofD/2 - 0.1, roofW, 0.15, 0.08, 0x2a1f14, 'trim', 2); addBlock(0, baseY + 0.1, -roofD/2 + 0.1, roofW, 0.15, 0.08, 0x2a1f14, 'trim', 2); } else if (style.roofType === 'curved') { // Chinese traditional roof: solid curved surface with wide overhang const rh = 2.2; const ovW = W + 3.5; // generous side overhang const ovD = D + 3.0; // generous front/back overhang const slopeSegs = 5; // fewer, wider segments for solid look // Two slope sides (front slope and back slope) for (let side = -1; side <= 1; side += 2) { for (let i = 0; i < slopeSegs; i++) { const t0 = i / slopeSegs; const t1 = (i + 1) / slopeSegs; const tMid = (t0 + t1) / 2; // Concave curve: drops quickly at first, flattens, then kicks up at eave const y0 = rh * (1 - Math.pow(t0, 0.7)); const y1 = rh * (1 - Math.pow(t1, 0.7)); // Eave upturn for last segment const upturn = i === slopeSegs - 1 ? 0.3 : 0; const segDepth = (ovD / 2) / slopeSegs; const segY = baseY + (y0 + y1) / 2 + upturn; const segZ = side * tMid * (ovD / 2); const segW = ovW * (1 - tMid * 0.08); // slightly narrows toward ridge // Angle: slope of the curve at this point const angle = side * Math.atan2(y0 - y1 + upturn * 2, segDepth); const m = createBoxMesh(segW, 0.15, segDepth * 1.05, 0x4a4a4a); m.position.set(0, segY, segZ); m.rotation.x = angle; m.castShadow = true; m.receiveShadow = true; scene.add(m); buildingParts.push(makePart(m, 'roof', 2)); } } // Ridge beam on top (thick, decorative) addBlock(0, baseY + rh + 0.15, 0, ovW * 0.85, 0.25, 0.3, 0x3a3a3a, 'roof', 2); // Ridge end ornaments (chiwen / dragon tail) addBlock(-ovW * 0.42, baseY + rh + 0.35, 0, 0.3, 0.4, 0.3, 0x333333, 'ornament', 2); addBlock(ovW * 0.42, baseY + rh + 0.35, 0, 0.3, 0.4, 0.3, 0x333333, 'ornament', 2); // Red fascia/trim board under the eave edge (front and back) addBlock(0, baseY - 0.1, ovD/2 - 0.2, ovW, 0.2, 0.12, 0xaa2222, 'trim', 2); addBlock(0, baseY - 0.1, -ovD/2 + 0.2, ovW, 0.2, 0.12, 0xaa2222, 'trim', 2); // Side trim addBlock(-ovW/2 + 0.1, baseY - 0.1, 0, 0.12, 0.2, ovD, 0xaa2222, 'trim', 2); addBlock(ovW/2 - 0.1, baseY - 0.1, 0, 0.12, 0.2, ovD, 0xaa2222, 'trim', 2); } } function buildDetails(W, D, stories, H, style) { if (config.buildingStyle === 'chinese') { buildChineseDetails(W, D, stories, H); } else if (config.buildingStyle === 'japanese') { buildJapaneseDetails(W, D, stories, H); } else if (config.buildingStyle === 'american') { buildAmericanDetails(W, D, stories, H); } else { buildWesternDetails(W, D, stories, H, style); } } function buildChineseDetails(W, D, stories, H) { // Raised stone platform steps (wider, grander) for (var i = 0; i < 4; i++) { addBlock(0, 0.3 - i*0.08, D/2 + 0.6 + i*0.35, W*0.4, 0.1, 0.4, 0x888888, 'steps', -1); } // Red lanterns hanging from front beam var lanternColor = 0xff2200; var lanternPositions = [-W/4, 0, W/4]; lanternPositions.forEach(function(lx) { // Lantern body (sphere-ish box) addBlock(lx, stories*H - 0.3, D/2 + 0.3, 0.4, 0.5, 0.4, lanternColor, 'lantern', stories - 1); // Lantern top addBlock(lx, stories*H + 0.0, D/2 + 0.3, 0.15, 0.1, 0.15, 0xffcc00, 'lantern', stories - 1); }); // Stone base wall/fence in front addBlock(0, 0.3, D/2 + 2.5, W + 2, 0.6, 0.25, 0x999999, 'fence', -1); // Gate pillars addBlock(-1.5, 0.6, D/2 + 2.5, 0.4, 1.2, 0.4, 0x777777, 'gate_pillar', -1); addBlock(1.5, 0.6, D/2 + 2.5, 0.4, 1.2, 0.4, 0x777777, 'gate_pillar', -1); // Decorative roof ridge ornaments var roofTop = stories * H + 2.8; addBlock(0, roofTop, 0, 0.3, 0.4, 0.3, 0x444444, 'ornament', stories); addBlock(-W/2 - 0.5, stories*H + 0.3, D/2 + 1.0, 0.25, 0.3, 0.25, 0x444444, 'ornament', stories); addBlock(W/2 + 0.5, stories*H + 0.3, D/2 + 1.0, 0.25, 0.3, 0.25, 0x444444, 'ornament', stories); // Potted plants var bushGeo = new THREE.SphereGeometry(0.35, 8, 6); var bushMat = new THREE.MeshStandardMaterial({ color: 0x2d5a27, roughness: 0.9 }); [[-W/2 - 1, 0.5, D/2 + 1], [W/2 + 1, 0.5, D/2 + 1]].forEach(function(pos) { var b = new THREE.Mesh(bushGeo, bushMat); b.position.set(pos[0], pos[1], pos[2]); b.castShadow = true; scene.add(b); }); } function buildJapaneseDetails(W, D, stories, H) { // Genkan (entry area) - stepping stones for (var i = 0; i < 3; i++) { addBlock(0, 0.05, D/2 + 1.0 + i*0.8, 0.8, 0.1, 0.6, 0x666666, 'steps', -1); } // Stone garden elements addBlock(-W/2 - 1.5, 0.2, 0, 0.5, 0.4, 0.5, 0x777777, 'stone', -1); addBlock(W/2 + 1.2, 0.15, -1, 0.4, 0.3, 0.4, 0x888888, 'stone', -1); // Bamboo/garden plants var bushGeo = new THREE.SphereGeometry(0.3, 8, 6); var bushMat = new THREE.MeshStandardMaterial({ color: 0x3a6b2a, roughness: 0.85 }); [[-W/2 - 1, 0.6, 2], [W/2 + 1.2, 0.5, 1], [-W/2 - 0.8, 0.4, -2]].forEach(function(pos) { var b = new THREE.Mesh(bushGeo, bushMat); b.position.set(pos[0], pos[1], pos[2]); b.scale.set(0.8, 1.5, 0.8); // tall and narrow like bamboo b.castShadow = true; scene.add(b); }); // Small stone lantern (tōrō) addBlock(W/2 + 1.5, 0.25, 2, 0.3, 0.5, 0.3, 0x999999, 'ornament', -1); addBlock(W/2 + 1.5, 0.55, 2, 0.4, 0.1, 0.4, 0x888888, 'ornament', -1); addBlock(W/2 + 1.5, 0.7, 2, 0.25, 0.3, 0.25, 0xaaaa88, 'ornament', -1); // Wooden fence/wall section on one side addBlock(-W/2 - 0.3, 0.5, D/2 + 0.5, 0.08, 1.0, 3, 0x5c4030, 'fence', -1); } function buildAmericanDetails(W, D, stories, H) { var trimColor = 0xffffff; var porchW = W + 1.0; // ---- WRAP-AROUND FRONT PORCH ---- // Porch deck addBlock(0, 0.35, D/2 + 1.2, porchW, 0.12, 2.4, 0x8b7355, 'porch', 0); // Porch roof addBlock(0, H * 0.95 + 0.3, D/2 + 1.2, porchW + 0.3, 0.1, 2.8, 0x3a3a3a, 'porch_roof', 0); // Porch columns (thick white square columns) var colPositions = [-porchW/2 + 0.4, -porchW/4, 0, porchW/4, porchW/2 - 0.4]; colPositions.forEach(function(cx) { addBlock(cx, H*0.5 + 0.3, D/2 + 2.2, 0.25, H*0.9, 0.25, trimColor, 'porch_column', 0); }); // Porch railing between columns addBlock(0, 0.75, D/2 + 2.3, porchW - 1, 0.06, 0.06, trimColor, 'railing', 0); // Railing balusters for (var i = 0; i < 12; i++) { var bx = -porchW/2 + 1.0 + i * (porchW - 2) / 11; addBlock(bx, 0.55, D/2 + 2.3, 0.04, 0.4, 0.04, trimColor, 'railing', 0); } // Front steps (centered, wide) for (var i = 0; i < 3; i++) { addBlock(0, 0.3 - i*0.12, D/2 + 2.5 + i*0.3, 2.0, 0.12, 0.35, 0x888888, 'steps', -1); } // ---- CHIMNEY (stone/brick on the side) ---- var ch = stories * H + 3.5; addBlock(W/2 - 0.5, ch/2, -D/4, 0.8, ch, 0.8, 0xa08060, 'chimney', stories); addBlock(W/2 - 0.5, ch + 0.1, -D/4, 0.9, 0.2, 0.9, 0x666666, 'chimney', stories); // ---- LANDSCAPING ---- // Foundation shrubs (low boxwood hedge) var bushGeo = new THREE.SphereGeometry(0.35, 8, 6); var bushMat = new THREE.MeshStandardMaterial({ color: 0x2a5a22, roughness: 0.9 }); for (var i = 0; i < 6; i++) { var bx = -W/2 + 0.8 + i * (W - 1.6) / 5; var b = new THREE.Mesh(bushGeo, bushMat); b.position.set(bx, 0.3, D/2 + 0.3); b.scale.set(1.0, 0.7, 0.8); b.castShadow = true; scene.add(b); } // Corner accent plants var tallBush = new THREE.Mesh(new THREE.SphereGeometry(0.5, 8, 6), bushMat); tallBush.position.set(-W/2 - 0.5, 0.6, D/2 + 0.5); tallBush.scale.set(0.7, 1.2, 0.7); tallBush.castShadow = true; scene.add(tallBush); // Flower beds (small colored spheres) var flowerColors = [0xcc2233, 0xff6644, 0xee4488]; for (var i = 0; i < 8; i++) { var fx = -2.5 + i * 0.7; var fc = flowerColors[i % 3]; var flower = new THREE.Mesh( new THREE.SphereGeometry(0.15, 6, 6), new THREE.MeshStandardMaterial({ color: fc, roughness: 0.8 }) ); flower.position.set(fx, 0.2, D/2 + 2.8 + Math.sin(i)*0.2); scene.add(flower); } // White trim fascia on gable if (stories >= 2) { addBlock(0, stories*H + 0.3, D/2 + 0.12, W + 0.4, 0.12, 0.06, trimColor, 'trim', stories); } // Gable triangle face (white siding filling the front gable) var gableBaseY = stories * H + 0.3; var gableH = 2.5; // Build gable as stacked narrowing panels for (var g = 0; g < 6; g++) { var gt = g / 6; var gw = (W + 0.8) * (1 - gt); var gy = gableBaseY + gt * gableH + gableH/12; addBlock(0, gy, D/2, gw, gableH/6 * 0.95, 0.12, 0xf0ece4, 'gable', stories); } // Gable window (upper story window in the gable) addBlock(0, gableBaseY + gableH*0.4, D/2 + 0.05, 1.0, 1.2, 0.04, 0x88bbcc, 'glass', stories); addBlock(0, gableBaseY + gableH*0.4, D/2 + 0.07, 1.1, 1.3, 0.02, 0x2a2a2a, 'trim', stories); } function buildWesternDetails(W, D, stories, H, style) { // Front steps for (let i = 0; i < 3; i++) { addBlock(0, 0.3 - i*0.1, D/2 + 0.5 + i*0.3, 2.5, 0.12, 0.35, 0x777777, 'steps', -1); } // Chimney if (style.roofType !== 'flat' && style.roofType !== 'curved') { const ch = stories * H + 4; addBlock(-W/2 + 1.2, ch/2, 0, 0.7, ch, 0.7, 0x8B4513, 'chimney', 2); addBlock(-W/2 + 1.2, ch + 0.1, 0, 0.9, 0.2, 0.9, 0x555555, 'chimney', 2); } // Porch columns if (style.hasShutters) { addBlock(-1.3, 1.6, D/2 + 0.7, 0.18, 2.6, 0.18, 0xf0f0f0, 'porch', 0); addBlock(1.3, 1.6, D/2 + 0.7, 0.18, 2.6, 0.18, 0xf0f0f0, 'porch', 0); addBlock(0, 3.0, D/2 + 0.7, 3.2, 0.1, 1.0, 0x555555, 'porch_roof', 0); } // Bushes const bushGeo = new THREE.SphereGeometry(0.45, 8, 6); const bushMat = new THREE.MeshStandardMaterial({ color: 0x2d5a27, roughness: 0.9 }); [[-3.5, 0.35, D/2+1.5], [3.5, 0.35, D/2+1.5], [-W/2-1, 0.35, 1], [-W/2-1, 0.35, -1.5]].forEach(p => { const b = new THREE.Mesh(bushGeo, bushMat); b.position.set(p[0], p[1], p[2]); b.scale.y = 0.7; b.castShadow = true; scene.add(b); }); } // ==================== BLOCK HELPERS ==================== function createBoxMesh(w, h, d, color) { const geo = new THREE.BoxGeometry(w, h, d); const mat = new THREE.MeshStandardMaterial({ color, roughness: 0.75, metalness: 0.05 }); return new THREE.Mesh(geo, mat); } function addBlock(x, y, z, w, h, d, color, label, floor) { const mesh = createBoxMesh(w, h, d, color); mesh.position.set(x, y, z); mesh.castShadow = true; mesh.receiveShadow = true; scene.add(mesh); buildingParts.push(makePart(mesh, label, floor)); } function makePart(mesh, label, floor) { return { mesh: mesh, label: label, floor: floor, ox: mesh.position.x, oy: mesh.position.y, oz: mesh.position.z, orx: mesh.rotation.x, ory: mesh.rotation.y, orz: mesh.rotation.z, vx: 0, vy: 0, vz: 0, avx: 0, avy: 0, avz: 0, state: 'static' // static | shaking | falling | landed }; } function clearAll() { buildingParts.forEach(p => scene.remove(p.mesh)); buildingParts = []; rescueMarkers.forEach(m => scene.remove(m)); rescueMarkers = []; rescueAnimObjects = []; document.getElementById('analysis-panel').style.display = 'none'; } // ==================== EARTHQUAKE SIMULATION ==================== // Ground motion is THE CAUSE. Parts accumulate stress from ground acceleration. // When stress > strength threshold → part breaks off. function startEarthquake() { if (isSimulating) return; isSimulating = true; simulationTime = 0; // Random earthquake characteristics - unique every run quakeDir = Math.random() * Math.PI * 2; quakeSeed = Math.random() * 1000; // Compute Peak Ground Acceleration (PGA in g's) - properly scaled so magnitude matters var mag = config.magnitude; var dist = config.distance; var foundAmp = foundationFactors[config.foundation]; // Realistic-ish attenuation. Gives clear visible differences across the full M4-M9 range: // M4/dist5: ~0.02g (barely felt), M5.5/dist5: ~0.12g (light damage) // M6.5/dist5: ~0.4g (moderate), M7.5/dist5: ~0.8g (heavy), M9/dist5: ~1.8g (catastrophic) var logPGA = 0.45 * mag - 1.0 * Math.log10(dist + 3) - 1.8; var pga = Math.pow(10, logPGA) * foundAmp; document.getElementById('status').textContent = 'Earthquake! PGA: ' + pga.toFixed(2) + 'g | Shaking...'; updateSimInfo(pga); // Store camera position for shaking var orbitPos = window.getCameraOrbitPos ? window.getCameraOrbitPos() : { x: camera.position.x, y: camera.position.y, z: camera.position.z }; cameraBasePos.x = orbitPos.x; cameraBasePos.y = orbitPos.y; cameraBasePos.z = orbitPos.z; // Initialize each part's stress tracking var weakCorner = { x: (Math.random()-0.5)*10, z: (Math.random()-0.5)*7 }; var weakSide = quakeDir; // the earthquake push direction is the weak side buildingParts.forEach(function(p) { if (p.floor < 0) { p.state = 'static'; return; } p.state = 'shaking'; p.stress = 0; // accumulated stress from ground motion // Each part has a random strength threshold var baseStrength = getPartStrength(p); // Add randomness: ±40% variation p.threshold = baseStrength * (0.6 + Math.random() * 0.8); // Parts closer to weak corner are weaker var dw = Math.sqrt(Math.pow(p.ox - weakCorner.x, 2) + Math.pow(p.oz - weakCorner.z, 2)); p.threshold *= (0.7 + dw / 15 * 0.5); // Random per-part frequency sensitivity (resonance) p.resonanceFreq = 5 + Math.random() * 15; p.resonanceAmp = 0.8 + Math.random() * 0.4; }); // Main simulation interval var simInterval = setInterval(function() { if (!isSimulating) { clearInterval(simInterval); return; } stepSimulation(pga, simInterval); }, 16); } function getPartStrength(p) { var mat = materialProps[config.material]; var matFactor = (mat.strength / 8) * (1.2 - mat.brittleness * 0.7); var ageFactor = Math.max(0.5, 1.0 - config.buildingAge / 200); var base = matFactor * ageFactor; // Structural role determines threshold if (p.label === 'glass') return base * 0.5; if (p.label === 'shoji') return base * 0.4; // paper panels very fragile if (p.label === 'shoji_frame') return base * 0.5; if (p.label === 'lattice') return base * 0.6; // lattice panels are light if (p.label === 'lantern') return base * 0.3; // lanterns fall easily if (p.label === 'railing') return base * 0.8; if (p.label === 'balcony') return base * 2.5; if (p.label === 'chimney') return base * 1.5; if (p.label === 'door') return base * 1.2; if (p.label === 'ornament') return base * 0.8; if (p.label === 'trim') return base * 0.7; if (p.label === 'porch' || p.label === 'porch_roof') return base * 1.8; if (p.label === 'porch_column') return base * 2.5; // porch columns are sturdy if (p.label === 'gable') return base * 3.0; // gable face attached to structure if (p.label === 'column') return base * 8.0; // columns are very strong if (p.label === 'beam') return base * 6.0; // beams are strong if (p.label === 'roof') return base * 4.0; if (p.label === 'int_wall') return base * 6.0; if (p.label.includes('wall')) return base * (7.0 - p.floor * 1.5); if (p.label === 'ceiling' || p.label === 'floor') return base * 5.0; return base * 4.0; } function stepSimulation(pga, interval) { var dt = 0.02; simulationTime += dt; var dur = config.duration; var quakeActive = simulationTime < dur; // ===== COMPUTE GROUND MOTION (this is the cause of everything) ===== var groundAccelX = 0, groundAccelZ = 0, groundAccelY = 0; var intensity = 0; if (quakeActive) { // Envelope: ramp up quickly, sustain, decay var env; var rampUp = Math.min(dur * 0.15, 2.0); var decay = dur * 0.3; if (simulationTime < rampUp) env = simulationTime / rampUp; else if (simulationTime < dur - decay) env = 1.0; else env = Math.max(0, (dur - simulationTime) / decay); intensity = env * pga; // Ground acceleration: sum of multiple frequency components (like real seismogram) var dirX = Math.cos(quakeDir); var dirZ = Math.sin(quakeDir); // Primary motion along earthquake direction var primary = Math.sin(simulationTime * 6.3 + quakeSeed) * 0.5 + Math.sin(simulationTime * 14.1 + quakeSeed * 0.3) * 0.3 + Math.sin(simulationTime * 2.7 + quakeSeed * 1.7) * 0.2; // Secondary (perpendicular) motion var secondary = Math.sin(simulationTime * 8.7 + quakeSeed * 2.1) * 0.3 + Math.cos(simulationTime * 4.5 + quakeSeed * 0.5) * 0.2; // Vertical (smaller) var vertical = Math.sin(simulationTime * 19 + quakeSeed * 3) * 0.15; groundAccelX = intensity * (dirX * primary + (-dirZ) * secondary); groundAccelZ = intensity * (dirZ * primary + dirX * secondary); groundAccelY = intensity * vertical; } // ===== APPLY GROUND MOTION VISUALLY ===== var gDispScale = 0.2; // displacement scale for visual var gx = groundAccelX * gDispScale; var gz = groundAccelZ * gDispScale; var gy = groundAccelY * gDispScale * 0.5; if (quakeActive) { groundMesh.position.set(gx, gy, gz); if (pathMesh) pathMesh.position.set(gx, 0.01 + gy, 8 + gz); // Camera shakes WITH the ground (you're standing on it) camera.position.x = cameraBasePos.x + gx * 0.7; camera.position.y = cameraBasePos.y + gy * 1.5; camera.position.z = cameraBasePos.z + gz * 0.7; camera.lookAt(gx * 0.3, 3 + gy * 0.5, gz * 0.3); } else { groundMesh.position.set(0, 0, 0); if (pathMesh) pathMesh.position.set(0, 0.01, 8); if (window.resetCameraOrbit) window.resetCameraOrbit(); } // ===== BUILDING PARTS: stress from ground motion causes collapse ===== var anyMoving = false; for (var i = 0; i < buildingParts.length; i++) { var p = buildingParts[i]; if (p.state === 'static' || p.state === 'landed') continue; if (p.state === 'shaking') { anyMoving = true; if (quakeActive) { // Part moves with the ground (it's attached to the building on the ground) // But upper parts amplify ground motion (whip effect) var heightAmp = 1.0 + p.oy * 0.15; // higher = more amplification var resonance = 1.0 + 0.3 * Math.sin(simulationTime * p.resonanceFreq) * p.resonanceAmp; var partDispX = gx * heightAmp * resonance; var partDispZ = gz * heightAmp * resonance; p.mesh.position.x = p.ox + partDispX; p.mesh.position.z = p.oz + partDispZ; p.mesh.position.y = p.oy + gy * heightAmp * 0.3; // ===== ACCUMULATE STRESS from ground acceleration ===== // Stress = integral of |acceleration| over time, weighted by height amplification var accelMag = Math.sqrt(groundAccelX * groundAccelX + groundAccelZ * groundAccelZ + groundAccelY * groundAccelY); p.stress += accelMag * heightAmp * resonance * dt; // ===== CHECK FAILURE: stress exceeds threshold → part breaks ===== if (p.stress > p.threshold) { p.state = 'falling'; // Ejection velocity proportional to current ground acceleration var ejectForce = 1.5 + intensity * 2.0; p.vx = groundAccelX * ejectForce * (0.7 + Math.random() * 0.6) + (Math.random()-0.5) * intensity; p.vy = Math.random() * 1.5 + groundAccelY * 2; p.vz = groundAccelZ * ejectForce * (0.7 + Math.random() * 0.6) + (Math.random()-0.5) * intensity; p.avx = (Math.random()-0.5) * 3 * intensity; p.avy = (Math.random()-0.5) * 2; p.avz = (Math.random()-0.5) * 3 * intensity; // Visual damage if (p.mesh.material && p.mesh.material.color) { p.mesh.material = p.mesh.material.clone(); p.mesh.material.color.multiplyScalar(0.5 + Math.random() * 0.2); } } } else { // Earthquake ended, return to rest if not broken p.mesh.position.x = p.ox; p.mesh.position.y = p.oy; p.mesh.position.z = p.oz; } } if (p.state === 'falling') { anyMoving = true; p.vy -= 15.0 * dt; p.vx *= 0.99; p.vz *= 0.99; p.mesh.position.x += p.vx * dt; p.mesh.position.y += p.vy * dt; p.mesh.position.z += p.vz * dt; p.mesh.rotation.x += p.avx * dt; p.mesh.rotation.y += p.avy * dt; p.mesh.rotation.z += p.avz * dt; var gnd = getGroundAt(p); if (p.mesh.position.y <= gnd) { p.mesh.position.y = gnd; if (Math.abs(p.vy) < 1.0) { p.state = 'landed'; p.vx = 0; p.vy = 0; p.vz = 0; p.avx = 0; p.avy = 0; p.avz = 0; } else { p.vy = -p.vy * 0.15; p.vx *= 0.4; p.vz *= 0.4; p.avx *= 0.3; p.avy *= 0.3; p.avz *= 0.3; } } } } // ===== SUPPORT CHECK: slabs/ceilings/roofs fall if walls below are gone ===== for (var i = 0; i < buildingParts.length; i++) { var p = buildingParts[i]; if (p.state !== 'shaking') continue; // Only check slabs, ceilings, floors, roofs, beams var isHorizontal = (p.label === 'ceiling' || p.label === 'floor' || p.label === 'roof' || p.label === 'porch_roof' || p.label === 'beam'); if (!isHorizontal) continue; if (p.floor < 0) continue; // Count supporting parts: must be on SAME floor (or floor below for roof), vertical, and still intact var supportCount = 0; for (var j = 0; j < buildingParts.length; j++) { var s = buildingParts[j]; if (s === p || s.state !== 'shaking') continue; // Must be a vertical structural element var isSupport = (s.label === 'column' || s.label.includes('wall') || s.label === 'porch_column' || s.label === 'int_wall'); if (!isSupport) continue; // Must be on same floor OR one floor below (roof sits on top floor walls) if (s.floor !== p.floor && s.floor !== p.floor - 1) continue; // Must be below this slab if (s.oy >= p.oy) continue; supportCount++; } // Slabs need at least 3 wall segments to stay up (a real building has many) if (supportCount < 3) { p.state = 'falling'; p.vx = (Math.random() - 0.5) * 0.3; p.vy = -1.0; p.vz = (Math.random() - 0.5) * 0.3; p.avx = (Math.random() - 0.5) * 0.3; p.avy = 0; p.avz = (Math.random() - 0.5) * 0.3; if (p.mesh.material && p.mesh.material.color) { p.mesh.material = p.mesh.material.clone(); p.mesh.material.color.multiplyScalar(0.6); } anyMoving = true; } } // Stop when everything settled after quake ends if (!quakeActive && !anyMoving) { isSimulating = false; clearInterval(interval); document.getElementById('status').textContent = 'Simulation complete - Capture images or analyze rescue paths'; } } function getGroundAt(part) { // Simple debris stacking: check if fallen parts are below var ground = 0.1; for (var i = 0; i < buildingParts.length; i++) { var o = buildingParts[i]; if (o === part || o.state !== 'landed') continue; var dx = Math.abs(part.mesh.position.x - o.mesh.position.x); var dz = Math.abs(part.mesh.position.z - o.mesh.position.z); if (dx < 1.2 && dz < 1.2) { var top = o.mesh.position.y + 0.25; if (top > ground) ground = top; } } return ground; } // ==================== INFO / ANALYSIS ==================== function updateSimInfo(pga) { var info = document.getElementById('sim-info'); var level = pga < 0.1 ? 'Light (I-V)' : pga < 0.3 ? 'Moderate (VI-VII)' : pga < 0.6 ? 'Strong (VII-VIII)' : pga < 1.0 ? 'Severe (IX)' : 'Extreme (X+)'; var pattern = pga < 0.1 ? 'Cosmetic damage, glass cracking' : pga < 0.3 ? 'Non-structural damage, chimney fall' : pga < 0.6 ? 'Structural damage, partial collapse' : pga < 1.0 ? 'Heavy damage, story collapse' : 'Catastrophic, total collapse possible'; info.innerHTML = '
PGA: ' + pga.toFixed(2) + 'g — ' + level + '
' + 'Expected: ' + pattern + '
' + 'Style: ' + config.buildingStyle + ' | Material: ' + config.material + '
' + 'Foundation: ' + config.foundation + ' (×' + foundationFactors[config.foundation] + ' amplification)
' + 'Quake: M' + config.magnitude + ' at ' + config.distance + 'km | Duration: ' + config.duration + 's
'; } function analyzeRescuePaths() { var panel = document.getElementById('analysis-panel'); panel.style.display = 'block'; // Estimate damage from how many parts actually fell var totalParts = 0, fallenParts = 0; buildingParts.forEach(function(p) { if (p.floor >= 0) { totalParts++; if (p.state === 'landed') fallenParts++; } }); var damage = totalParts > 0 ? fallenParts / totalParts : 0; var rooms = window.roomLayout; var html = 'No images yet. Run simulation then click Capture.
'; } else { var h = ''; for (var i = 0; i < capturedImages.length; i++) { var img = capturedImages[i]; h += '