import React, { useMemo } from 'react'; import { ExtrudeGeometry, Shape, Vector2, } from 'three'; import { wankelState } from '../../../animations/mechanics'; import type { MachineAnimationModule } from '../../../animations/types'; import { FlowArrow, MachinePart, Shaft, } from './primitives'; import { EngineeringBaseplate, PartLabels, getPartVisualProps, } from './proceduralSceneHelpers'; import type { ProceduralMachineDefinition, ProceduralMachineModule, ProceduralMachineSceneProps, } from './types'; const APEX_RADIUS = 0.95; const definition: ProceduralMachineDefinition = { id: 'wankel-rotary-engine', slug: 'wankel-rotary-engine', title: 'Wankel Rotary Engine', subtitle: 'Eccentric triangular rotor in epitrochoid housing', category: 'Engines', difficulty: 'Advanced', summary: 'A rotary engine cutaway showing eccentric shaft motion, three apex seals, port timing, and combustion chamber pulse.', description: 'The Wankel engine replaces reciprocating pistons with a rounded triangular rotor orbiting inside an epitrochoid housing. The rotor turns at one third of eccentric shaft speed while its chambers pass intake, compression, combustion, and exhaust regions.', keywords: ['wankel', 'rotary engine', 'epitrochoid', 'apex seal', 'eccentric shaft'], complexity: 8, dateAdded: '2025-02-04', typicalRpm: '1,000–9,000 RPM', facts: [ { label: 'Rotor speed', value: '1/3 eccentric shaft speed', detail: 'The output shaft turns three times for one rotor revolution.', }, { label: 'Chambers', value: 'Three working chambers', detail: 'Each rotor face forms a moving chamber with the epitrochoid housing.', }, { label: 'Strength', value: 'High power density', detail: 'Smooth rotary motion and compact packaging are major advantages.', }, { label: 'Challenge', value: 'Apex sealing', detail: 'Apex seals must maintain compression across high sliding speed and temperature.', }, ], parts: [ { id: 'housing', name: 'Epitrochoid housing', description: 'Two-lobed chamber profile that guides the rotor apex seals.', material: 'Aluminium housing with hard-coated running surface', labelPosition: [-1.55, 1.12, 0.78], defaultOpacity: 0.46, }, { id: 'rotor', name: 'Triangular rotor', description: 'Rounded triangular rotor creating three moving combustion chambers.', material: 'Cast iron / steel rotor', labelPosition: [0.72, 0.18, 0.86], }, { id: 'eccentric-shaft', name: 'Eccentric shaft', description: 'Output shaft with offset journal driving the rotor orbit.', material: 'Forged steel', labelPosition: [1.45, -0.78, 0.7], }, { id: 'apex-seal-a', name: 'Apex seal A', description: 'Spring-loaded seal maintaining contact at one rotor tip.', labelPosition: [1.35, 0.75, 0.9], }, { id: 'apex-seal-b', name: 'Apex seal B', description: 'Second rotor-tip seal separating adjacent chambers.', labelPosition: [-1.2, 0.95, 0.9], }, { id: 'apex-seal-c', name: 'Apex seal C', description: 'Third apex seal completing chamber sealing around the rotor.', labelPosition: [0, -1.4, 0.9], }, { id: 'intake-port', name: 'Intake port', description: 'Port opened by rotor motion to admit fresh charge.', labelPosition: [-1.78, -0.48, 0.72], }, { id: 'exhaust-port', name: 'Exhaust port', description: 'Port uncovered by rotor motion to release exhaust gas.', labelPosition: [1.78, 0.5, 0.72], }, { id: 'spark-plug', name: 'Spark plug', description: 'Ignition source positioned near the compressed chamber.', labelPosition: [0.2, 1.55, 0.82], }, { id: 'combustion-pocket', name: 'Combustion pulse', description: 'Warm glow representing the chamber firing event.', labelPosition: [0.78, 1.1, 0.92], }, { id: 'intake-flow', name: 'Intake flow', description: 'Blue flow marker showing port induction.', labelPosition: [-2.05, -0.85, 0.65], }, { id: 'exhaust-flow', name: 'Exhaust flow', description: 'Warm flow marker showing exhaust discharge.', labelPosition: [2.0, 0.85, 0.65], }, ], cameraPresets: [ { id: 'iso', label: 'Isometric', position: [3.5, 2.4, 4.1], target: [0, 0, 0], fov: 40 }, { id: 'front', label: 'Housing profile', position: [0, 0.3, 5.1], target: [0, 0, 0], fov: 36 }, { id: 'ports', label: 'Port timing', position: [3.7, 1.5, 3.3], target: [0.15, 0.15, 0], fov: 42 }, ], tour: { id: 'tour-wankel-rotary-engine', machineId: 'wankel-rotary-engine', title: 'Inside a Wankel rotary cycle', description: 'Follow the rotor orbit, apex seals, port timing, and combustion pulse.', steps: [ { id: 'eccentric', title: 'Eccentric shaft drives orbit', body: 'The eccentric journal makes the rotor center orbit while the rotor turns more slowly.', durationSeconds: 4.5, partIds: ['eccentric-shaft', 'rotor'], camera: { position: [2.8, 1.5, 4.0], target: [0, 0, 0], duration: 1.1 }, rpm: 720, }, { id: 'seals', title: 'Apex seals define chambers', body: 'Three apex seals sweep the housing and keep adjacent gas chambers separated.', durationSeconds: 5, partIds: ['apex-seal-a', 'apex-seal-b', 'apex-seal-c', 'housing'], camera: { position: [0.5, 1.5, 4.7], target: [0, 0.05, 0], duration: 1.1 }, rpm: 620, }, { id: 'ports', title: 'Ports open by geometry', body: 'The rotor itself uncovers intake and exhaust ports rather than using poppet valves.', durationSeconds: 5, partIds: ['intake-port', 'exhaust-port', 'intake-flow', 'exhaust-flow'], phaseRange: [0.05, 0.32], camera: { position: [3.3, 1.4, 3.5], target: [0.15, 0.05, 0], duration: 1.1 }, }, { id: 'combustion', title: 'Combustion expands one chamber', body: 'A spark ignites the compressed charge and pressure drives the rotor around the eccentric.', durationSeconds: 4.5, partIds: ['spark-plug', 'combustion-pocket', 'rotor'], phaseRange: [0.5, 0.66], camera: { position: [-2.8, 1.8, 3.7], target: [0, 0.3, 0], duration: 1.1 }, }, ], }, relatedMachineIds: ['four-stroke-petrol-engine', 'centrifugal-pump', 'ball-bearing'], }; 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 useEpitrochoidGeometry(): ExtrudeGeometry { return useMemo(() => { const points: Vector2[] = []; const samples = 180; for (let index = 0; index < samples; index += 1) { const t = (index / samples) * Math.PI * 2; const r = 1.22 + 0.28 * Math.cos(2 * t); points.push(new Vector2(Math.cos(t) * r, Math.sin(t) * (1.0 + 0.22 * Math.cos(2 * t)))); } const shape = new Shape(points); const geometry = new ExtrudeGeometry(shape, { depth: 0.22, bevelEnabled: true, bevelSegments: 3, bevelSize: 0.025, bevelThickness: 0.025, }); geometry.center(); return geometry; }, []); } function useRotorGeometry(): ExtrudeGeometry { return useMemo(() => { const points: Vector2[] = []; const samples = 150; for (let index = 0; index < samples; index += 1) { const t = (index / samples) * Math.PI * 2; const radius = 0.72 + 0.14 * Math.cos(3 * t); points.push(new Vector2(Math.cos(t) * radius, Math.sin(t) * radius)); } const shape = new Shape(points); const geometry = new ExtrudeGeometry(shape, { depth: 0.24, bevelEnabled: true, bevelSegments: 4, bevelSize: 0.035, bevelThickness: 0.035, }); geometry.center(); return geometry; }, []); } export const wankelRotaryEngineAnimation: MachineAnimationModule = { id: 'wankel-eccentric-rotor-cycle', machineId: 'wankel-rotary-engine', label: 'Eccentric rotor, apex seals, and port timing', version: '1.0.0', defaultRpm: 720, minRpm: 80, maxRpm: 9000, cycleRevolutions: 3, cycleSteps: 18, loop: true, supportsReverse: true, reducedMotionStrategy: 'slow', update(context) { const state = wankelState(context.shaftAngle); const centerX = state.eccentricCenterX; const centerY = state.eccentricCenterY; const rotor = context.getPart('rotor'); if (rotor) { rotor.position.set(centerX, centerY, 0.08); rotor.rotation.z = state.rotorAngle; } const shaft = context.getPart('eccentric-shaft'); if (shaft) shaft.rotation.z = -state.shaftAngle; const apexIds = ['apex-seal-a', 'apex-seal-b', 'apex-seal-c']; apexIds.forEach((id, index) => { const apex = context.getPart(id); if (!apex) return; const localAngle = state.rotorAngle + (index / 3) * Math.PI * 2; apex.position.set( centerX + Math.cos(localAngle) * APEX_RADIUS, centerY + Math.sin(localAngle) * APEX_RADIUS, 0.22, ); apex.rotation.z = localAngle + Math.PI / 2; }); const combustion = context.getPart('combustion-pocket'); if (combustion) { combustion.visible = state.combustionPulse > 0.03; combustion.position.set(centerX + 0.46, centerY + 0.62, 0.18); combustion.scale.setScalar(0.42 + state.combustionPulse * 0.62); } context.setPartOpacity('combustion-pocket', state.combustionPulse); const intake = context.getPart('intake-flow'); const intakePulse = Math.max(0, Math.sin(state.chamberPhase * Math.PI * 2 + 0.9)); if (intake) { intake.visible = intakePulse > 0.05; intake.position.x = -1.55 + intakePulse * 0.18; intake.scale.setScalar(0.84 + intakePulse * 0.32); } context.setPartOpacity('intake-flow', intakePulse); const exhaust = context.getPart('exhaust-flow'); const exhaustPulse = Math.max(0, Math.sin(state.chamberPhase * Math.PI * 2 - 1.7)); if (exhaust) { exhaust.visible = exhaustPulse > 0.05; exhaust.position.x = 1.55 + exhaustPulse * 0.22; exhaust.scale.setScalar(0.84 + exhaustPulse * 0.38); } context.setPartOpacity('exhaust-flow', exhaustPulse); context.setPhaseLabel( state.combustionPulse > 0.3 ? 'Combustion chamber expanding — rotor drives eccentric shaft' : 'Rotor orbiting — ports open and close by housing geometry', ); context.clearHighlights(); if (state.combustionPulse > 0.3) context.highlightParts(['spark-plug', 'combustion-pocket', 'rotor'], 0.9); else if (intakePulse > exhaustPulse) context.highlightParts(['intake-port', 'intake-flow'], 0.72); else context.highlightParts(['apex-seal-a', 'apex-seal-b', 'apex-seal-c'], 0.7); }, }; export function WankelRotaryEngineScene(props: ProceduralMachineSceneProps): JSX.Element { const housingGeometry = useEpitrochoidGeometry(); const rotorGeometry = useRotorGeometry(); return ( {['apex-seal-a', 'apex-seal-b', 'apex-seal-c'].map((id, index) => { const angle = (index / 3) * Math.PI * 2; return ( ); })} ); } export const wankelRotaryEngine: ProceduralMachineModule = { definition, animationModule: wankelRotaryEngineAnimation, Scene: WankelRotaryEngineScene, };