Last active
October 31, 2025 17:36
-
-
Save AloiSama/9bd9db35171ab959ec7f660893f51b5e to your computer and use it in GitHub Desktop.
Plane Sim
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Hand Wood Plane Physics Simulator</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 20px; | |
| } | |
| .container { | |
| background: white; | |
| border-radius: 20px; | |
| box-shadow: 0 20px 60px rgba(0,0,0,0.3); | |
| padding: 30px; | |
| max-width: 1400px; | |
| width: 100%; | |
| } | |
| h1 { | |
| color: #333; | |
| margin-bottom: 10px; | |
| font-size: 2.5em; | |
| text-align: center; | |
| } | |
| .subtitle { | |
| text-align: center; | |
| color: #666; | |
| margin-bottom: 30px; | |
| } | |
| .main-content { | |
| display: grid; | |
| grid-template-columns: 1fr 350px; | |
| gap: 30px; | |
| margin-bottom: 20px; | |
| } | |
| .canvas-container { | |
| position: relative; | |
| background: #f8f9fa; | |
| border-radius: 10px; | |
| padding: 20px; | |
| box-shadow: inset 0 2px 10px rgba(0,0,0,0.1); | |
| } | |
| canvas { | |
| width: 100%; | |
| height: 500px; | |
| background: white; | |
| border-radius: 8px; | |
| cursor: grab; | |
| } | |
| canvas:active { | |
| cursor: grabbing; | |
| } | |
| .controls { | |
| background: #f8f9fa; | |
| border-radius: 10px; | |
| padding: 20px; | |
| } | |
| .control-section { | |
| margin-bottom: 25px; | |
| } | |
| .control-section h3 { | |
| color: #444; | |
| margin-bottom: 15px; | |
| font-size: 1.1em; | |
| font-weight: 600; | |
| } | |
| .control-group { | |
| margin-bottom: 15px; | |
| } | |
| .control-group label { | |
| display: block; | |
| margin-bottom: 5px; | |
| color: #666; | |
| font-size: 0.9em; | |
| font-weight: 500; | |
| } | |
| .slider-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| input[type="range"] { | |
| flex: 1; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: #ddd; | |
| outline: none; | |
| -webkit-appearance: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #667eea; | |
| cursor: pointer; | |
| } | |
| input[type="range"]::-moz-range-thumb { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #667eea; | |
| cursor: pointer; | |
| } | |
| .value-display { | |
| min-width: 60px; | |
| text-align: right; | |
| font-weight: 600; | |
| color: #333; | |
| } | |
| select { | |
| width: 100%; | |
| padding: 8px 12px; | |
| border: 2px solid #e0e0e0; | |
| border-radius: 6px; | |
| background: white; | |
| color: #333; | |
| font-size: 0.95em; | |
| cursor: pointer; | |
| transition: border-color 0.3s; | |
| } | |
| select:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| } | |
| .action-buttons { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| margin-top: 20px; | |
| } | |
| button { | |
| padding: 12px 20px; | |
| border: none; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| font-size: 0.95em; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| button.primary { | |
| background: #667eea; | |
| color: white; | |
| } | |
| button.primary:hover { | |
| background: #5a67d8; | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| button.secondary { | |
| background: #48bb78; | |
| color: white; | |
| } | |
| button.secondary:hover { | |
| background: #38a169; | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(72, 187, 120, 0.4); | |
| } | |
| .info-panel { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin-top: 20px; | |
| } | |
| .metrics { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); | |
| gap: 15px; | |
| } | |
| .metric { | |
| text-align: center; | |
| } | |
| .metric-value { | |
| font-size: 1.8em; | |
| font-weight: bold; | |
| margin-bottom: 5px; | |
| } | |
| .metric-label { | |
| font-size: 0.9em; | |
| opacity: 0.9; | |
| } | |
| .quality-indicator { | |
| display: inline-block; | |
| padding: 4px 12px; | |
| border-radius: 20px; | |
| font-size: 0.9em; | |
| font-weight: 600; | |
| margin-top: 10px; | |
| } | |
| .quality-excellent { | |
| background: #48bb78; | |
| } | |
| .quality-good { | |
| background: #f6ad55; | |
| } | |
| .quality-poor { | |
| background: #fc8181; | |
| } | |
| .instructions { | |
| background: #f7fafc; | |
| border-left: 4px solid #667eea; | |
| padding: 15px; | |
| margin-top: 20px; | |
| border-radius: 0 8px 8px 0; | |
| } | |
| .instructions h4 { | |
| color: #333; | |
| margin-bottom: 10px; | |
| } | |
| .instructions ul { | |
| margin-left: 20px; | |
| color: #666; | |
| } | |
| .instructions li { | |
| margin-bottom: 5px; | |
| } | |
| @media (max-width: 1024px) { | |
| .main-content { | |
| grid-template-columns: 1fr; | |
| } | |
| .action-buttons { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>🪵 Hand Wood Plane Physics Simulator</h1> | |
| <p class="subtitle">Interactive demonstration of wood cutting mechanics and grain interaction</p> | |
| <div class="main-content"> | |
| <div class="canvas-container"> | |
| <canvas id="planeCanvas"></canvas> | |
| <div class="instructions"> | |
| <h4>How to Use:</h4> | |
| <ul> | |
| <li>Click and drag to move the plane across the wood</li> | |
| <li>Observe how shavings form based on grain direction</li> | |
| <li>Watch the force meters to understand cutting resistance</li> | |
| <li>Experiment with different angles and settings</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <div class="control-section"> | |
| <h3>🔧 Plane Configuration</h3> | |
| <div class="control-group"> | |
| <label>Plane Type</label> | |
| <select id="planeType"> | |
| <option value="block">Block Plane (Low Angle)</option> | |
| <option value="smoothing" selected>Smoothing Plane (#4)</option> | |
| <option value="jack">Jack Plane (#5)</option> | |
| <option value="jointer">Jointer Plane (#7)</option> | |
| </select> | |
| </div> | |
| <div class="control-group"> | |
| <label>Bed Angle</label> | |
| <div class="slider-container"> | |
| <input type="range" id="bedAngle" min="12" max="60" value="45"> | |
| <span class="value-display" id="bedAngleValue">45°</span> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label>Bevel Angle</label> | |
| <div class="slider-container"> | |
| <input type="range" id="bevelAngle" min="20" max="35" value="25"> | |
| <span class="value-display" id="bevelAngleValue">25°</span> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label>Cutting Depth</label> | |
| <div class="slider-container"> | |
| <input type="range" id="cuttingDepth" min="0.025" max="1.6" step="0.025" value="0.1"> | |
| <span class="value-display" id="cuttingDepthValue">0.10mm</span> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label>Chipbreaker Distance</label> | |
| <div class="slider-container"> | |
| <input type="range" id="chipbreaker" min="0.1" max="2" step="0.1" value="0.3"> | |
| <span class="value-display" id="chipbreakerValue">0.3mm</span> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label>Blade Sharpness</label> | |
| <div class="slider-container"> | |
| <input type="range" id="sharpness" min="1" max="50" value="5"> | |
| <span class="value-display" id="sharpnessValue">5µm</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="control-section"> | |
| <h3>🌲 Wood Properties</h3> | |
| <div class="control-group"> | |
| <label>Wood Species</label> | |
| <select id="woodSpecies"> | |
| <option value="pine">Pine (Softwood)</option> | |
| <option value="oak" selected>Oak (Hardwood)</option> | |
| <option value="maple">Maple (Hardwood)</option> | |
| <option value="walnut">Walnut (Hardwood)</option> | |
| </select> | |
| </div> | |
| <div class="control-group"> | |
| <label>Grain Direction</label> | |
| <div class="slider-container"> | |
| <input type="range" id="grainAngle" min="0" max="180" value="45"> | |
| <span class="value-display" id="grainAngleValue">45°</span> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label>Moisture Content</label> | |
| <div class="slider-container"> | |
| <input type="range" id="moisture" min="6" max="20" value="8"> | |
| <span class="value-display" id="moistureValue">8%</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="action-buttons"> | |
| <button class="primary" id="startBtn">Start Planing</button> | |
| <button class="secondary" id="resetBtn">Reset Wood</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="info-panel"> | |
| <div class="metrics"> | |
| <div class="metric"> | |
| <div class="metric-value" id="forceValue">0</div> | |
| <div class="metric-label">Cutting Force (N)</div> | |
| </div> | |
| <div class="metric"> | |
| <div class="metric-value" id="qualityValue">-</div> | |
| <div class="metric-label">Surface Quality</div> | |
| </div> | |
| <div class="metric"> | |
| <div class="metric-value" id="chipValue">0.0</div> | |
| <div class="metric-label">Chip Thickness (mm)</div> | |
| </div> | |
| <div class="metric"> | |
| <div class="metric-value" id="tearoutValue">0</div> | |
| <div class="metric-label">Tear-out Risk (%)</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Canvas setup | |
| const canvas = document.getElementById('planeCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // Set canvas resolution | |
| function resizeCanvas() { | |
| const rect = canvas.getBoundingClientRect(); | |
| canvas.width = rect.width * window.devicePixelRatio; | |
| canvas.height = rect.height * window.devicePixelRatio; | |
| ctx.scale(window.devicePixelRatio, window.devicePixelRatio); | |
| } | |
| resizeCanvas(); | |
| window.addEventListener('resize', resizeCanvas); | |
| // Wood species database | |
| const woodDatabase = { | |
| pine: { | |
| name: 'Pine', | |
| density: 450, | |
| elasticModulus: 9000, | |
| shearStrength: 7000, | |
| fractureToughness: 300, | |
| color: '#F4E5C2', | |
| grainColor: '#D4B896' | |
| }, | |
| oak: { | |
| name: 'Oak', | |
| density: 650, | |
| elasticModulus: 12000, | |
| shearStrength: 14000, | |
| fractureToughness: 450, | |
| color: '#C19A6B', | |
| grainColor: '#8B6B3F' | |
| }, | |
| maple: { | |
| name: 'Maple', | |
| density: 630, | |
| elasticModulus: 12500, | |
| shearStrength: 16100, | |
| fractureToughness: 500, | |
| color: '#F2E6D9', | |
| grainColor: '#D4C0A9' | |
| }, | |
| walnut: { | |
| name: 'Walnut', | |
| density: 640, | |
| elasticModulus: 11600, | |
| shearStrength: 13200, | |
| fractureToughness: 480, | |
| color: '#5C4033', | |
| grainColor: '#3E2723' | |
| } | |
| }; | |
| // Plane types database | |
| const planeTypes = { | |
| block: { | |
| name: 'Block Plane', | |
| length: 150, | |
| bladeWidth: 35, | |
| defaultBedAngle: 20, | |
| hasChipbreaker: false | |
| }, | |
| smoothing: { | |
| name: 'Smoothing Plane', | |
| length: 240, | |
| bladeWidth: 45, | |
| defaultBedAngle: 45, | |
| hasChipbreaker: true | |
| }, | |
| jack: { | |
| name: 'Jack Plane', | |
| length: 355, | |
| bladeWidth: 50, | |
| defaultBedAngle: 45, | |
| hasChipbreaker: true | |
| }, | |
| jointer: { | |
| name: 'Jointer Plane', | |
| length: 560, | |
| bladeWidth: 60, | |
| defaultBedAngle: 45, | |
| hasChipbreaker: true | |
| } | |
| }; | |
| // Simulation state | |
| let simulationState = { | |
| isPlaning: false, | |
| planePosition: { x: 50, y: 200 }, | |
| woodSurface: [], | |
| shavings: [], | |
| particles: [], | |
| currentForce: 0, | |
| surfaceQuality: 100, | |
| tearoutRisk: 0, | |
| chipThickness: 0 | |
| }; | |
| // Initialize wood surface | |
| function initializeWoodSurface() { | |
| simulationState.woodSurface = []; | |
| const width = canvas.width / window.devicePixelRatio; | |
| const segments = 200; | |
| for (let i = 0; i <= segments; i++) { | |
| simulationState.woodSurface.push({ | |
| x: (i / segments) * width, | |
| y: 250, | |
| originalY: 250, | |
| cut: false | |
| }); | |
| } | |
| } | |
| // Physics calculations | |
| class CuttingPhysics { | |
| static calculateCuttingForce(params) { | |
| const { | |
| depth, | |
| width, | |
| species, | |
| grainAngle, | |
| sharpness, | |
| moisture, | |
| bedAngle, | |
| bevelAngle | |
| } = params; | |
| const wood = woodDatabase[species]; | |
| const effectiveAngle = bedAngle + bevelAngle; | |
| // Merchant's shear angle calculation | |
| const frictionAngle = 25; // degrees | |
| const rakeAngle = 90 - effectiveAngle; | |
| const shearAngle = 45 + (rakeAngle - frictionAngle) / 2; | |
| // Grain angle effect (forces increase dramatically against grain) | |
| const grainFactor = this.calculateGrainFactor(grainAngle); | |
| // Sharpness effect (dull tools increase force exponentially) | |
| const sharpnessFactor = 1 + Math.pow(sharpness / 10, 1.5); | |
| // Moisture effect (dry wood is harder to cut) | |
| const moistureFactor = 1 + (8 - moisture) * 0.03; | |
| // Fracture mechanics model (Atkins approach) | |
| const plasticWork = wood.shearStrength * depth * width * grainFactor; | |
| const fractureWork = (wood.fractureToughness * width) / depth; | |
| // Total cutting force | |
| const force = (plasticWork + fractureWork) * sharpnessFactor * moistureFactor * 0.001; | |
| return { | |
| force: Math.round(force), | |
| shearAngle: shearAngle, | |
| chipThicknessRatio: Math.sin(shearAngle * Math.PI / 180) / | |
| Math.cos((shearAngle - rakeAngle) * Math.PI / 180) | |
| }; | |
| } | |
| static calculateGrainFactor(angle) { | |
| // Optimal cutting at 20-70°, worst at 90-110° (against grain) | |
| const radians = angle * Math.PI / 180; | |
| if (angle >= 20 && angle <= 70) { | |
| // With grain - minimal resistance | |
| return 1 + 0.5 * Math.sin(radians); | |
| } else if (angle >= 90 && angle <= 110) { | |
| // Against grain - maximum resistance and tear-out | |
| return 3 + 2 * Math.abs(Math.sin(radians)); | |
| } else { | |
| // Cross grain - moderate resistance | |
| return 2 + Math.abs(Math.cos(radians)); | |
| } | |
| } | |
| static calculateTearoutRisk(params) { | |
| const { | |
| grainAngle, | |
| chipbreakerDistance, | |
| effectiveAngle, | |
| depth | |
| } = params; | |
| let risk = 0; | |
| // Against grain dramatically increases tear-out | |
| if (grainAngle >= 80 && grainAngle <= 120) { | |
| risk += 50; | |
| } | |
| // Chipbreaker effectiveness | |
| const chipbreakerRatio = chipbreakerDistance / depth; | |
| if (chipbreakerRatio > 5) { | |
| risk += 30; // Chipbreaker too far | |
| } else if (chipbreakerRatio < 2) { | |
| risk -= 20; // Close chipbreaker prevents tear-out | |
| } | |
| // Higher cutting angles reduce tear-out | |
| if (effectiveAngle > 50) { | |
| risk -= 15; | |
| } else if (effectiveAngle < 40) { | |
| risk += 20; | |
| } | |
| return Math.max(0, Math.min(100, risk)); | |
| } | |
| } | |
| // Shaving particle class | |
| class Shaving { | |
| constructor(x, y, thickness, curl) { | |
| this.points = []; | |
| this.thickness = thickness; | |
| this.curl = curl; | |
| this.opacity = 1; | |
| this.velocity = { x: Math.random() * 2 - 1, y: -Math.random() * 3 }; | |
| // Create curved shaving shape | |
| const segments = 20; | |
| for (let i = 0; i <= segments; i++) { | |
| const t = i / segments; | |
| const angle = t * Math.PI * curl; | |
| this.points.push({ | |
| x: x + t * 30 + Math.sin(angle) * 10, | |
| y: y - Math.cos(angle) * 10 | |
| }); | |
| } | |
| } | |
| update() { | |
| // Apply gravity and air resistance | |
| this.velocity.y += 0.2; | |
| this.velocity.x *= 0.98; | |
| // Update position | |
| for (let point of this.points) { | |
| point.x += this.velocity.x; | |
| point.y += this.velocity.y; | |
| } | |
| // Fade out | |
| this.opacity *= 0.98; | |
| return this.opacity > 0.01; | |
| } | |
| draw(ctx) { | |
| ctx.save(); | |
| ctx.globalAlpha = this.opacity; | |
| ctx.strokeStyle = '#D4A574'; | |
| ctx.lineWidth = this.thickness * 2; | |
| ctx.lineCap = 'round'; | |
| ctx.beginPath(); | |
| ctx.moveTo(this.points[0].x, this.points[0].y); | |
| for (let i = 1; i < this.points.length; i++) { | |
| const prev = this.points[i - 1]; | |
| const curr = this.points[i]; | |
| const cp = { | |
| x: (prev.x + curr.x) / 2, | |
| y: (prev.y + curr.y) / 2 | |
| }; | |
| ctx.quadraticCurveTo(prev.x, prev.y, cp.x, cp.y); | |
| } | |
| ctx.stroke(); | |
| ctx.restore(); | |
| } | |
| } | |
| // Main drawing function | |
| function draw() { | |
| const width = canvas.width / window.devicePixelRatio; | |
| const height = canvas.height / window.devicePixelRatio; | |
| // Clear canvas | |
| ctx.fillStyle = '#FFFFFF'; | |
| ctx.fillRect(0, 0, width, height); | |
| // Get current wood properties | |
| const woodSpecies = document.getElementById('woodSpecies').value; | |
| const wood = woodDatabase[woodSpecies]; | |
| const grainAngle = parseFloat(document.getElementById('grainAngle').value); | |
| // Draw wood piece | |
| drawWood(wood, grainAngle); | |
| // Draw plane | |
| drawPlane(); | |
| // Draw shavings | |
| simulationState.shavings = simulationState.shavings.filter(shaving => { | |
| const alive = shaving.update(); | |
| if (alive) shaving.draw(ctx); | |
| return alive; | |
| }); | |
| // Draw particles | |
| drawParticles(); | |
| // Update metrics | |
| updateMetrics(); | |
| requestAnimationFrame(draw); | |
| } | |
| function drawWood(wood, grainAngle) { | |
| const width = canvas.width / window.devicePixelRatio; | |
| const height = canvas.height / window.devicePixelRatio; | |
| // Wood base | |
| ctx.fillStyle = wood.color; | |
| ctx.fillRect(0, 200, width, height - 200); | |
| // Draw grain lines | |
| ctx.strokeStyle = wood.grainColor; | |
| ctx.lineWidth = 1; | |
| ctx.globalAlpha = 0.3; | |
| const grainSpacing = 8; | |
| const angleRad = grainAngle * Math.PI / 180; | |
| for (let i = -height; i < width + height; i += grainSpacing) { | |
| ctx.beginPath(); | |
| ctx.moveTo(i, 200); | |
| ctx.lineTo(i + Math.cos(angleRad) * height, | |
| 200 + Math.sin(angleRad) * height); | |
| ctx.stroke(); | |
| } | |
| ctx.globalAlpha = 1; | |
| // Draw cut surface | |
| ctx.strokeStyle = wood.color; | |
| ctx.lineWidth = 3; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, simulationState.woodSurface[0].y); | |
| for (let i = 1; i < simulationState.woodSurface.length; i++) { | |
| const point = simulationState.woodSurface[i]; | |
| ctx.lineTo(point.x, point.y); | |
| } | |
| ctx.stroke(); | |
| // Show tear-out areas | |
| const tearoutRisk = simulationState.tearoutRisk; | |
| if (tearoutRisk > 30) { | |
| ctx.fillStyle = 'rgba(255, 0, 0, 0.1)'; | |
| for (let point of simulationState.woodSurface) { | |
| if (point.cut && Math.random() < tearoutRisk / 100) { | |
| ctx.beginPath(); | |
| ctx.arc(point.x, point.y, 3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| } | |
| } | |
| } | |
| function drawPlane() { | |
| const planeType = document.getElementById('planeType').value; | |
| const plane = planeTypes[planeType]; | |
| const pos = simulationState.planePosition; | |
| ctx.save(); | |
| ctx.translate(pos.x, pos.y); | |
| // Plane body | |
| ctx.fillStyle = '#8B4513'; | |
| ctx.fillRect(0, -30, plane.length / 3, 30); | |
| // Plane sole | |
| ctx.fillStyle = '#4A4A4A'; | |
| ctx.fillRect(0, 0, plane.length / 3, 5); | |
| // Blade | |
| const bedAngle = parseFloat(document.getElementById('bedAngle').value); | |
| ctx.save(); | |
| ctx.translate(plane.length / 6, 0); | |
| ctx.rotate(-bedAngle * Math.PI / 180); | |
| ctx.fillStyle = '#C0C0C0'; | |
| ctx.fillRect(-2, 0, 4, -20); | |
| // Cutting edge highlight | |
| ctx.strokeStyle = '#FFD700'; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| ctx.moveTo(-2, 0); | |
| ctx.lineTo(2, 0); | |
| ctx.stroke(); | |
| ctx.restore(); | |
| // Chipbreaker (if present) | |
| if (plane.hasChipbreaker) { | |
| const chipbreakerDist = parseFloat(document.getElementById('chipbreaker').value); | |
| ctx.fillStyle = '#606060'; | |
| ctx.fillRect(plane.length / 6 - chipbreakerDist * 10, -5, 3, 5); | |
| } | |
| // Front knob | |
| ctx.fillStyle = '#8B4513'; | |
| ctx.beginPath(); | |
| ctx.arc(20, -35, 8, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Rear tote | |
| ctx.fillStyle = '#8B4513'; | |
| ctx.beginPath(); | |
| ctx.arc(plane.length / 3 - 20, -35, 8, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| function drawParticles() { | |
| simulationState.particles = simulationState.particles.filter(particle => { | |
| particle.x += particle.vx; | |
| particle.y += particle.vy; | |
| particle.vy += 0.3; // gravity | |
| particle.life -= 0.02; | |
| if (particle.life > 0) { | |
| ctx.save(); | |
| ctx.globalAlpha = particle.life; | |
| ctx.fillStyle = particle.color; | |
| ctx.beginPath(); | |
| ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| return true; | |
| } | |
| return false; | |
| }); | |
| } | |
| function performCut(x) { | |
| const depth = parseFloat(document.getElementById('cuttingDepth').value); | |
| const planeType = document.getElementById('planeType').value; | |
| const plane = planeTypes[planeType]; | |
| const woodSpecies = document.getElementById('woodSpecies').value; | |
| const wood = woodDatabase[woodSpecies]; | |
| // Find surface points near blade | |
| const bladeX = x + plane.length / 6; | |
| const surfaceIndex = Math.floor((bladeX / (canvas.width / window.devicePixelRatio)) * | |
| simulationState.woodSurface.length); | |
| if (surfaceIndex >= 0 && surfaceIndex < simulationState.woodSurface.length) { | |
| const point = simulationState.woodSurface[surfaceIndex]; | |
| if (!point.cut) { | |
| // Calculate physics | |
| const physics = CuttingPhysics.calculateCuttingForce({ | |
| depth: depth, | |
| width: plane.bladeWidth, | |
| species: woodSpecies, | |
| grainAngle: parseFloat(document.getElementById('grainAngle').value), | |
| sharpness: parseFloat(document.getElementById('sharpness').value), | |
| moisture: parseFloat(document.getElementById('moisture').value), | |
| bedAngle: parseFloat(document.getElementById('bedAngle').value), | |
| bevelAngle: parseFloat(document.getElementById('bevelAngle').value) | |
| }); | |
| // Update surface | |
| point.y += depth * 10; // Scale for visibility | |
| point.cut = true; | |
| // Update simulation state | |
| simulationState.currentForce = physics.force; | |
| simulationState.chipThickness = depth * physics.chipThicknessRatio; | |
| // Create shaving | |
| if (Math.random() < 0.3) { | |
| simulationState.shavings.push( | |
| new Shaving(bladeX, point.y, depth, physics.chipThicknessRatio) | |
| ); | |
| } | |
| // Create particles | |
| for (let i = 0; i < 5; i++) { | |
| simulationState.particles.push({ | |
| x: bladeX + Math.random() * 10 - 5, | |
| y: point.y, | |
| vx: Math.random() * 4 - 2, | |
| vy: -Math.random() * 5 - 2, | |
| size: Math.random() * 2 + 1, | |
| color: wood.grainColor, | |
| life: 1 | |
| }); | |
| } | |
| // Calculate surface quality | |
| const tearoutRisk = CuttingPhysics.calculateTearoutRisk({ | |
| grainAngle: parseFloat(document.getElementById('grainAngle').value), | |
| chipbreakerDistance: parseFloat(document.getElementById('chipbreaker').value), | |
| effectiveAngle: parseFloat(document.getElementById('bedAngle').value) + | |
| parseFloat(document.getElementById('bevelAngle').value), | |
| depth: depth | |
| }); | |
| simulationState.tearoutRisk = tearoutRisk; | |
| if (tearoutRisk > 50 && Math.random() < tearoutRisk / 100) { | |
| // Simulate tear-out | |
| for (let j = -2; j <= 2; j++) { | |
| const nearIndex = surfaceIndex + j; | |
| if (nearIndex >= 0 && nearIndex < simulationState.woodSurface.length) { | |
| simulationState.woodSurface[nearIndex].y += Math.random() * depth * 20; | |
| } | |
| } | |
| simulationState.surfaceQuality -= 5; | |
| } | |
| } | |
| } | |
| } | |
| function updateMetrics() { | |
| document.getElementById('forceValue').textContent = simulationState.currentForce; | |
| document.getElementById('chipValue').textContent = simulationState.chipThickness.toFixed(2); | |
| document.getElementById('tearoutValue').textContent = simulationState.tearoutRisk; | |
| // Surface quality indicator | |
| const quality = simulationState.surfaceQuality; | |
| let qualityText = 'Excellent'; | |
| let qualityClass = 'quality-excellent'; | |
| if (quality < 70) { | |
| qualityText = 'Good'; | |
| qualityClass = 'quality-good'; | |
| } | |
| if (quality < 40) { | |
| qualityText = 'Poor'; | |
| qualityClass = 'quality-poor'; | |
| } | |
| const qualityElement = document.getElementById('qualityValue'); | |
| qualityElement.textContent = qualityText; | |
| qualityElement.className = 'metric-value ' + qualityClass; | |
| } | |
| // Event handlers | |
| let isDragging = false; | |
| let lastX = 0; | |
| canvas.addEventListener('mousedown', (e) => { | |
| if (simulationState.isPlaning) { | |
| isDragging = true; | |
| const rect = canvas.getBoundingClientRect(); | |
| lastX = e.clientX - rect.left; | |
| } | |
| }); | |
| canvas.addEventListener('mousemove', (e) => { | |
| if (isDragging && simulationState.isPlaning) { | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const dx = x - lastX; | |
| if (dx > 0) { // Only cut when moving forward | |
| simulationState.planePosition.x = x - 50; | |
| performCut(simulationState.planePosition.x); | |
| } | |
| lastX = x; | |
| } | |
| }); | |
| canvas.addEventListener('mouseup', () => { | |
| isDragging = false; | |
| simulationState.currentForce = 0; | |
| }); | |
| canvas.addEventListener('mouseleave', () => { | |
| isDragging = false; | |
| simulationState.currentForce = 0; | |
| }); | |
| // Control event handlers | |
| document.getElementById('startBtn').addEventListener('click', () => { | |
| simulationState.isPlaning = !simulationState.isPlaning; | |
| document.getElementById('startBtn').textContent = | |
| simulationState.isPlaning ? 'Stop Planing' : 'Start Planing'; | |
| }); | |
| document.getElementById('resetBtn').addEventListener('click', () => { | |
| initializeWoodSurface(); | |
| simulationState.shavings = []; | |
| simulationState.particles = []; | |
| simulationState.surfaceQuality = 100; | |
| simulationState.currentForce = 0; | |
| simulationState.tearoutRisk = 0; | |
| simulationState.chipThickness = 0; | |
| }); | |
| // Update value displays | |
| const sliders = [ | |
| { id: 'bedAngle', suffix: '°' }, | |
| { id: 'bevelAngle', suffix: '°' }, | |
| { id: 'cuttingDepth', suffix: 'mm', decimals: 2 }, | |
| { id: 'chipbreaker', suffix: 'mm', decimals: 1 }, | |
| { id: 'sharpness', suffix: 'µm' }, | |
| { id: 'grainAngle', suffix: '°' }, | |
| { id: 'moisture', suffix: '%' } | |
| ]; | |
| sliders.forEach(slider => { | |
| const element = document.getElementById(slider.id); | |
| const display = document.getElementById(slider.id + 'Value'); | |
| element.addEventListener('input', () => { | |
| const value = parseFloat(element.value); | |
| if (slider.decimals !== undefined) { | |
| display.textContent = value.toFixed(slider.decimals) + slider.suffix; | |
| } else { | |
| display.textContent = value + slider.suffix; | |
| } | |
| }); | |
| }); | |
| // Plane type change handler | |
| document.getElementById('planeType').addEventListener('change', (e) => { | |
| const plane = planeTypes[e.target.value]; | |
| document.getElementById('bedAngle').value = plane.defaultBedAngle; | |
| document.getElementById('bedAngleValue').textContent = plane.defaultBedAngle + '°'; | |
| if (!plane.hasChipbreaker) { | |
| document.getElementById('chipbreaker').disabled = true; | |
| } else { | |
| document.getElementById('chipbreaker').disabled = false; | |
| } | |
| }); | |
| // Initialize | |
| initializeWoodSurface(); | |
| draw(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment