import React from 'react'; import type { Object3D } from 'three'; import { fourStrokePhaseLabel, crankSliderState, valveLift, windowPulse01, } from '../../../animations/mechanics'; import type { MachineAnimationModule } from '../../../animations/types'; import { FlowArrow, MachinePart, Shaft, SpokedWheel, } from './primitives'; import { EngineeringBaseplate, PartLabels, getPartVisualProps, } from './proceduralSceneHelpers'; import type { ProceduralMachineDefinition, ProceduralMachineModule, ProceduralMachineSceneProps, } from './types'; const definition: ProceduralMachineDefinition = { id: 'four-stroke-petrol-engine', slug: 'four-stroke-petrol-engine', title: 'Four Stroke Petrol Engine', subtitle: 'Spark-ignition piston engine with valve timing', category: 'Engines', difficulty: 'Intermediate', summary: 'A transparent single-cylinder petrol engine demonstrating intake, compression, power, and exhaust strokes.', description: 'This procedural cutaway shows the kinematic relationship between crankshaft, connecting rod, piston, camshaft, poppet valves, spark plug, and gas exchange paths in a four-stroke spark-ignition engine. The crankshaft completes two revolutions per thermodynamic cycle while the camshaft rotates once, opening each valve at the correct stroke.', keywords: ['engine', 'piston', 'crankshaft', 'valves', 'spark ignition', 'otto cycle'], complexity: 6, dateAdded: '2025-02-04', typicalRpm: '800–6,500 RPM', facts: [ { label: 'Cycle', value: '720° crank rotation', detail: 'A complete intake-compression-power-exhaust sequence takes two crank revolutions.', }, { label: 'Valve train', value: 'Camshaft at 1/2 crank speed', detail: 'Four-stroke camshafts rotate once for every two crankshaft turns.', }, { label: 'Common efficiency', value: '25–35%', detail: 'Real brake thermal efficiency depends on compression ratio, load, and friction.', }, { label: 'Invented', value: '1876', detail: 'Nikolaus Otto commercialized the practical four-stroke cycle.', }, ], parts: [ { id: 'engine-block', name: 'Cylinder block', description: 'Rigid structure that supports the cylinder bore, crank bearings, coolant jacket, and head mounting surfaces.', material: 'Cast aluminium / iron', engineeringNote: 'The block must handle combustion pressure while keeping crank and bore alignment stable.', labelPosition: [-1.08, 0.2, 0.85], defaultOpacity: 0.54, }, { id: 'piston', name: 'Piston', description: 'Reciprocating component that seals combustion pressure and transmits gas force through the wrist pin to the connecting rod.', material: 'Forged aluminium alloy', engineeringNote: 'Skirts guide side thrust while rings maintain compression and oil control.', labelPosition: [0.72, 0.45, 0.9], }, { id: 'connecting-rod', name: 'Connecting rod', description: 'Pinned link between piston and crank throw that converts reciprocating piston motion into crank rotation.', material: 'Forged steel', engineeringNote: 'The rod angle creates side thrust on the piston and alternating tensile/compressive loads.', labelPosition: [0.98, -0.65, 0.78], }, { id: 'crankshaft', name: 'Crankshaft', description: 'Rotating shaft with an offset crank throw that converts linear piston force into torque.', material: 'Nitrided steel', engineeringNote: 'Counterweights reduce shaking forces and bearing loads.', labelPosition: [-0.9, -1.12, 0.75], }, { id: 'crank-throw', name: 'Crank throw', description: 'Offset journal defining stroke length and moving the big end of the connecting rod.', material: 'Hardened steel journal', labelPosition: [0.62, -1.18, 0.7], }, { id: 'camshaft', name: 'Camshaft', description: 'Lobed shaft that opens valves with a two-to-one timing ratio relative to the crankshaft.', material: 'Chilled cast iron / steel', engineeringNote: 'Cam phase determines breathing, overlap, idle quality, and power curve.', labelPosition: [-0.92, 1.35, 0.76], }, { id: 'intake-valve', name: 'Intake valve', description: 'Poppet valve admitting fresh air-fuel mixture during the intake stroke.', material: 'Chromium alloy steel', labelPosition: [-0.5, 1.12, 0.85], }, { id: 'exhaust-valve', name: 'Exhaust valve', description: 'Heat-resistant poppet valve allowing combustion products to leave during the exhaust stroke.', material: 'Austenitic stainless steel', labelPosition: [0.5, 1.12, 0.85], }, { id: 'spark-plug', name: 'Spark plug', description: 'Insulated electrode that ignites the compressed mixture near top dead centre.', material: 'Ceramic insulator and nickel electrodes', engineeringNote: 'Ignition timing is advanced before TDC at higher RPM so pressure peaks after TDC.', labelPosition: [0, 1.65, 0.82], }, { id: 'intake-flow', name: 'Intake charge', description: 'Blue flow markers visualize fresh mixture entering the cylinder while the intake valve is open.', labelPosition: [-1.45, 1.08, 0.65], }, { id: 'exhaust-flow', name: 'Exhaust gases', description: 'Warm flow markers visualize hot exhaust leaving through the exhaust valve.', labelPosition: [1.45, 1.08, 0.65], }, { id: 'flywheel', name: 'Flywheel', description: 'Rotational inertia that smooths torque delivery between combustion events.', material: 'Cast steel', labelPosition: [1.35, -1.18, 0.62], }, ], cameraPresets: [ { id: 'iso', label: 'Isometric', position: [3.4, 2.5, 4.2], target: [0, 0, 0], fov: 42 }, { id: 'front', label: 'Front cutaway', position: [0, 0.4, 5.2], target: [0, 0, 0], fov: 38 }, { id: 'side', label: 'Valve timing', position: [4.8, 0.4, 1.2], target: [0, 0, 0.1], fov: 42 }, ], tour: { id: 'tour-four-stroke-petrol-engine', machineId: 'four-stroke-petrol-engine', title: 'Follow one complete four-stroke cycle', description: 'A guided walkthrough of the piston motion, valve timing, ignition event, and crankshaft energy smoothing.', steps: [ { id: 'intake', title: '1. Intake stroke', body: 'The piston descends, the intake valve opens, and fresh mixture is drawn through the port.', durationSeconds: 5, partIds: ['piston', 'intake-valve', 'intake-flow'], phaseRange: [0.02, 0.22], rpm: 900, camera: { position: [2.7, 1.6, 4.3], target: [0, 0.25, 0], duration: 1.2 }, }, { id: 'compression', title: '2. Compression stroke', body: 'Both valves close as the piston rises and compresses the charge before ignition.', durationSeconds: 4.5, partIds: ['piston', 'engine-block', 'spark-plug'], phaseRange: [0.27, 0.46], rpm: 900, camera: { position: [0.8, 1.1, 4.6], target: [0, 0.55, 0], duration: 1.1 }, }, { id: 'power', title: '3. Combustion / power stroke', body: 'The spark plug fires near top dead centre and gas pressure accelerates the piston downward.', durationSeconds: 5, partIds: ['spark-plug', 'piston', 'connecting-rod', 'crankshaft'], phaseRange: [0.5, 0.7], rpm: 780, camera: { position: [3.3, 1.5, 3.4], target: [0, -0.2, 0], duration: 1.15 }, }, { id: 'exhaust', title: '4. Exhaust stroke', body: 'The exhaust valve opens while the piston rises to clear spent gases from the cylinder.', durationSeconds: 4.5, partIds: ['exhaust-valve', 'exhaust-flow', 'piston'], phaseRange: [0.76, 0.96], rpm: 900, camera: { position: [-2.8, 1.6, 4.2], target: [0.15, 0.35, 0], duration: 1.2 }, }, ], }, relatedMachineIds: ['wankel-rotary-engine', 'disc-brake-caliper', 'centrifugal-pump'], }; function partProps( partId: string, props: ProceduralMachineSceneProps, explodeDirection: [number, number, number] = [0, 0, 0], ) { return { partId, registerPart: props.registerPart, explodeDirection, explodedDistance: props.explodedDistance ?? 0, onSelectPart: props.onSelectPart, onHoverPart: props.onHoverPart, ...getPartVisualProps(partId, props), }; } function setPosition(object: Object3D | undefined, x: number, y: number, z: number): void { if (object) object.position.set(x, y, z); } export const fourStrokePetrolEngineAnimation: MachineAnimationModule = { id: 'four-stroke-petrol-engine-cycle', machineId: 'four-stroke-petrol-engine', label: 'Four-stroke crank-valve animation', version: '1.0.0', defaultRpm: 900, minRpm: 120, maxRpm: 6500, cycleRevolutions: 2, cycleSteps: 16, loop: true, supportsReverse: true, reducedMotionStrategy: 'slow', update(context) { const crankRadius = 0.42; const rodLength = 1.24; const crankCenterY = -1.02; const pistonOffsetY = -0.12; const crank = crankSliderState(context.shaftAngle, crankRadius, rodLength); const pistonY = pistonOffsetY + (crank.pistonY - rodLength); const crankPinX = crank.crankPinX; const crankPinY = crankCenterY + crank.crankPinY; const pistonPinY = pistonY - 0.3; const rodMidY = (crankPinY + pistonPinY) / 2; const rodMidX = crankPinX / 2; const rodLengthNow = Math.hypot(crankPinX, pistonPinY - crankPinY); const cycleProgress = context.cycleProgress; const intakeLift = valveLift(cycleProgress, 0.1, 0.28, 0.22); const exhaustLift = valveLift(cycleProgress, 0.88, 0.28, 0.22); const sparkPulse = windowPulse01(cycleProgress, 0.485, 0.525); const intakeFlow = windowPulse01(cycleProgress, 0.02, 0.23); const exhaustFlow = windowPulse01(cycleProgress, 0.78, 0.98); setPosition(context.getPart('piston'), 0, pistonY, 0); setPosition(context.getPart('crank-throw'), crankPinX, crankPinY, 0); setPosition(context.getPart('connecting-rod'), rodMidX, rodMidY, 0); const rod = context.getPart('connecting-rod'); if (rod) { rod.rotation.z = -Math.atan2(crankPinX, pistonPinY - crankPinY); rod.scale.set(1, rodLengthNow, 1); } const crankshaft = context.getPart('crankshaft'); if (crankshaft) crankshaft.rotation.z = -context.shaftAngle; const flywheel = context.getPart('flywheel'); if (flywheel) flywheel.rotation.z = -context.shaftAngle; const camshaft = context.getPart('camshaft'); if (camshaft) camshaft.rotation.z = -context.shaftAngle / 2; const intakeValve = context.getPart('intake-valve'); if (intakeValve) intakeValve.position.y = 1.08 - intakeLift; const exhaustValve = context.getPart('exhaust-valve'); if (exhaustValve) exhaustValve.position.y = 1.08 - exhaustLift; const spark = context.getPart('spark-plug'); if (spark) spark.scale.setScalar(1 + sparkPulse * 0.18); const intake = context.getPart('intake-flow'); if (intake) { intake.visible = intakeFlow > 0.04; intake.position.x = -1.08 + intakeFlow * 0.16; intake.scale.setScalar(0.8 + intakeFlow * 0.45); } const exhaust = context.getPart('exhaust-flow'); if (exhaust) { exhaust.visible = exhaustFlow > 0.04; exhaust.position.x = 1.08 + exhaustFlow * 0.24; exhaust.scale.setScalar(0.8 + exhaustFlow * 0.55); } context.setPartOpacity('intake-flow', intakeFlow); context.setPartOpacity('exhaust-flow', exhaustFlow); context.clearHighlights(); const phaseLabel = fourStrokePhaseLabel(cycleProgress); context.setPhaseLabel(phaseLabel); if (cycleProgress < 0.25) context.highlightParts(['intake-valve', 'intake-flow'], 0.8); else if (cycleProgress < 0.5) context.highlightPart('piston', 0.6); else if (cycleProgress < 0.75) context.highlightParts(['spark-plug', 'connecting-rod', 'crankshaft'], 0.85); else context.highlightParts(['exhaust-valve', 'exhaust-flow'], 0.8); }, }; export function FourStrokePetrolEngineScene(props: ProceduralMachineSceneProps): JSX.Element { return ( ); } export const fourStrokePetrolEngine: ProceduralMachineModule = { definition, animationModule: fourStrokePetrolEngineAnimation, Scene: FourStrokePetrolEngineScene, };