Skip to content

Instantly share code, notes, and snippets.

@AloiSama
Last active October 31, 2025 17:36
Show Gist options
  • Select an option

  • Save AloiSama/9bd9db35171ab959ec7f660893f51b5e to your computer and use it in GitHub Desktop.

Select an option

Save AloiSama/9bd9db35171ab959ec7f660893f51b5e to your computer and use it in GitHub Desktop.
Plane Sim
<!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