import { Group, MeshStandardMaterial } from 'three'; import type { LoadedMachineAsset, MachineMetadata, ViewerPart } from '../../types/viewer'; import { createBoxPartMesh, createCylinderBetween, createCylinderPartMesh, createPartGroup, createPbrMaterial, createPhysicalGlassMaterial, createSpherePartMesh, createTorusPartMesh, disposeObjectTree } from './geometry'; const PARTS: ViewerPart[] = [ { id: 'engine-block', name: 'Engine Block', description: 'The cast block carries the cylinder bore, coolant jacket, crankcase loads, and the bolted interfaces for the head and oil sump.', material: 'Cast iron alloy', explodeDirection: [-0.55, 0.05, -0.35], annotationPosition: [-1.2, 1.15, 0.86], specs: { 'Primary load': 'Combustion pressure and crank bearing reaction', 'Typical material': 'Cast iron or aluminium alloy', 'Design note': 'Cutaway side exposes the liner and piston travel' } }, { id: 'cylinder-head', name: 'Cylinder Head', description: 'The head seals the combustion chamber and houses the poppet valves, ports, spark plug, and cam-bearing structure.', material: 'Aluminium alloy', explodeDirection: [0, 0.78, 0], annotationPosition: [0.7, 2.72, 0.74], specs: { 'Thermal role': 'Transfers combustion heat into coolant passages', 'Valve layout': 'Single intake and exhaust pair in this teaching model' } }, { id: 'piston', name: 'Piston', description: 'The piston converts expanding combustion gas pressure into linear force while the rings seal the cylinder wall.', material: 'Forged aluminium', explodeDirection: [0.22, 0.42, 0.16], annotationPosition: [0.38, 1.55, 0.2], specs: { Motion: 'Reciprocating', 'Ring pack': 'Compression and oil-control rings', 'Peak acceleration': 'Thousands of g in high-speed engines' } }, { id: 'connecting-rod', name: 'Connecting Rod', description: 'The connecting rod transmits piston force to the crank throw while accommodating angularity through wrist-pin and big-end bearings.', material: 'Forged steel', explodeDirection: [0.52, 0.18, 0], annotationPosition: [0.58, 0.77, 0.08], specs: { Loading: 'Alternating tension and compression', 'Critical region': 'Big-end cap and bolt stretch' } }, { id: 'crankshaft', name: 'Crankshaft', description: 'The crankshaft offsets the crank pin from the main journals, converting reciprocating rod force into smooth rotary output.', material: 'Nitrided steel', explodeDirection: [0, -0.25, 0.64], annotationPosition: [0.1, 0.05, 0.78], specs: { Motion: 'Continuous rotation', 'Balance feature': 'Counterweights reduce shaking force', 'Bearing type': 'Plain hydrodynamic journals' } }, { id: 'camshaft', name: 'Camshaft', description: 'Eccentric cam lobes drive the valve train at half crankshaft speed in a four-stroke cycle.', material: 'Chilled cast iron', explodeDirection: [-0.35, 0.46, -0.35], annotationPosition: [-0.72, 2.52, -0.68], specs: { 'Speed ratio': '0.5 × crankshaft RPM', Function: 'Valve lift timing and duration' } }, { id: 'intake-valve', name: 'Intake Valve', description: 'The intake valve opens during the intake stroke to admit fresh air-fuel mixture from the intake port into the cylinder.', material: 'Stainless valve steel', explodeDirection: [-0.45, 0.25, 0.42], annotationPosition: [-0.45, 2.2, 0.42], specs: { Event: 'Opens before top dead centre, closes after bottom dead centre', Cooling: 'Cooled by incoming charge and seat contact' } }, { id: 'exhaust-valve', name: 'Exhaust Valve', description: 'The exhaust valve opens near the end of the power stroke so burned gas can exit through the exhaust port.', material: 'High-temperature valve steel', explodeDirection: [0.45, 0.25, 0.42], annotationPosition: [0.48, 2.17, 0.42], specs: { Event: 'Opens before bottom dead centre on power stroke', 'Temperature exposure': 'Often above 700 °C at the valve head' } }, { id: 'spark-plug', name: 'Spark Plug', description: 'The spark plug initiates combustion with a timed electrical discharge across the electrode gap.', material: 'Ceramic insulator and nickel alloy electrode', explodeDirection: [0, 0.78, 0.18], annotationPosition: [0, 3.05, 0.18], specs: { Timing: 'Advanced before top dead centre', 'Gap range': 'Typically 0.6–1.1 mm' } }, { id: 'intake-manifold', name: 'Intake Manifold', description: 'The intake manifold guides the air-fuel charge toward the intake port with low pressure loss and controlled runner volume.', material: 'Cast aluminium', explodeDirection: [-0.85, 0.2, 0.12], annotationPosition: [-1.55, 2.13, 0.06], specs: { Purpose: 'Distributes charge to cylinders', 'Design trade-off': 'Runner length tunes torque band' } }, { id: 'exhaust-manifold', name: 'Exhaust Manifold', description: 'The exhaust manifold collects hot exhaust gas and routes it away from the cylinder head.', material: 'Cast iron / stainless steel', explodeDirection: [0.85, 0.16, 0.1], annotationPosition: [1.55, 2.03, 0.04], specs: { Purpose: 'Collects high-temperature exhaust pulses', 'Design trade-off': 'Flow loss versus packaging and durability' } }, { id: 'flywheel', name: 'Flywheel', description: 'The flywheel stores rotational kinetic energy, smoothing torque pulses between combustion events.', material: 'Steel', explodeDirection: [0.9, -0.1, 0], annotationPosition: [1.92, 0.18, 0], specs: { Function: 'Inertia storage and clutch interface', Effect: 'Reduces cyclic speed variation' } }, { id: 'timing-chain', name: 'Timing Chain', description: 'The timing chain synchronizes the camshaft with the crankshaft so the valves open at the correct crank angles.', material: 'Hardened steel links', explodeDirection: [-0.82, 0.05, -0.4], annotationPosition: [-1.58, 1.35, -0.62], specs: { Ratio: '2:1 crank-to-cam', Requirement: 'Maintains phase accuracy under load and heat' } } ]; export const FOUR_STROKE_ENGINE_METADATA: MachineMetadata = { id: 'four-stroke-petrol-engine', slug: 'four-stroke-petrol-engine', title: 'Four Stroke Petrol Engine', subtitle: 'Cutaway single-cylinder teaching model', category: 'Engines', difficulty: 'Beginner', description: 'A premium interactive cutaway of a spark-ignition four-stroke engine, exposing the intake, compression, combustion, and exhaust mechanisms through selectable engineered components.', keywords: ['engine', 'petrol', 'gasoline', 'piston', 'crankshaft', 'valves', 'four stroke'], facts: [ { label: 'Cycle', value: '720° crank rotation', detail: 'A complete intake-compression-power-exhaust cycle requires two crankshaft revolutions.' }, { label: 'Typical RPM', value: '800–6,500 rpm', detail: 'Passenger-car spark ignition engines idle below 1,000 rpm and commonly operate below 6,500 rpm.' }, { label: 'Invented', value: '1876', detail: 'Nikolaus Otto demonstrated the practical four-stroke internal combustion engine.' }, { label: 'Applications', value: 'Cars, motorcycles, generators', detail: 'The architecture is common wherever compact power density and throttle response matter.' } ], parts: PARTS, relatedMachineIds: ['two-stroke-engine', 'diesel-engine', 'v8-engine', 'turbocharger'], approximateBounds: { center: [0, 1.15, 0], radius: 3.6, min: [-2.1, -0.65, -1.25], max: [2.25, 3.25, 1.25] } }; export function createFourStrokeEngineAsset(): LoadedMachineAsset { const root = new Group(); root.name = 'FourStrokePetrolEngine_ProceduralCutaway'; root.userData.assetKind = 'procedural'; root.userData.machineId = FOUR_STROKE_ENGINE_METADATA.id; const castIron = createPbrMaterial('castIron'); const aluminium = createPbrMaterial('aluminium'); const brushedSteel = createPbrMaterial('brushedSteel'); const polishedSteel = createPbrMaterial('polishedSteel'); const darkSteel = createPbrMaterial('darkSteel'); const brass = createPbrMaterial('brass'); const rubber = createPbrMaterial('rubber'); const ceramic = createPbrMaterial('ceramic'); const copper = createPbrMaterial('copper'); const warm = createPbrMaterial('warmHighlight'); const intakeBlue = createPbrMaterial('fluidBlue'); const exhaustOrange = createPbrMaterial('exhaustOrange'); const glass = createPhysicalGlassMaterial('#8fd4ff', 0.22); const black = createPbrMaterial('matteBlack'); addEngineBlock(root, castIron, glass); addCylinderHead(root, aluminium, darkSteel); addPiston(root, aluminium, darkSteel); addConnectingRod(root, polishedSteel, brass); addCrankshaft(root, brushedSteel, darkSteel); addCamshaft(root, darkSteel, polishedSteel); addValve(root, 'intake-valve', polishedSteel, intakeBlue, -0.38); addValve(root, 'exhaust-valve', polishedSteel, exhaustOrange, 0.38); addSparkPlug(root, ceramic, polishedSteel, copper); addIntakeManifold(root, aluminium, intakeBlue); addExhaustManifold(root, darkSteel, exhaustOrange); addFlywheel(root, brushedSteel, black); addTimingChain(root, brushedSteel, warm); root.traverse((object) => { object.frustumCulled = true; }); return { root, metadata: FOUR_STROKE_ENGINE_METADATA, parts: PARTS, source: { kind: 'procedural', generatedAt: new Date(0).toISOString() }, dispose: () => disposeObjectTree(root) }; } function getPart(id: string): ViewerPart { const part = PARTS.find((entry) => entry.id === id); if (!part) { throw new Error(`Unknown four-stroke engine part: ${id}`); } return part; } function addEngineBlock(root: Group, castIron: MeshStandardMaterial, glass: MeshStandardMaterial): void { const group = createPartGroup(getPart('engine-block')); createBoxPartMesh(group, 'Cutaway crankcase casting', [1.72, 1.85, 1.28], castIron, { position: [0, 0.88, 0] }); createBoxPartMesh(group, 'Open cylinder wall section', [1.08, 1.58, 0.16], glass, { position: [0, 1.34, 0.62] }); createCylinderPartMesh(group, 'Cylinder liner', 0.46, 0.46, 1.68, glass, { position: [0, 1.42, 0], radialSegments: 72 }); createBoxPartMesh(group, 'Oil sump flange', [1.94, 0.32, 1.42], castIron, { position: [0, -0.18, 0] }); createBoxPartMesh(group, 'Main bearing bridge', [0.42, 0.34, 1.42], castIron, { position: [-0.52, 0.2, 0] }); createBoxPartMesh(group, 'Main bearing bridge rear', [0.42, 0.34, 1.42], castIron, { position: [0.52, 0.2, 0] }); root.add(group); } function addCylinderHead(root: Group, aluminium: MeshStandardMaterial, darkSteel: MeshStandardMaterial): void { const group = createPartGroup(getPart('cylinder-head')); createBoxPartMesh(group, 'Combustion chamber roof', [1.82, 0.58, 1.2], aluminium, { position: [0, 2.12, 0] }); createBoxPartMesh(group, 'Valve cover', [1.62, 0.34, 1.0], darkSteel, { position: [0, 2.62, 0] }); createCylinderPartMesh(group, 'Left cam bearing cap', 0.14, 0.14, 1.55, aluminium, { position: [-0.56, 2.6, -0.35], rotation: [Math.PI / 2, 0, 0], radialSegments: 32 }); createCylinderPartMesh(group, 'Right cam bearing cap', 0.14, 0.14, 1.55, aluminium, { position: [0.56, 2.6, -0.35], rotation: [Math.PI / 2, 0, 0], radialSegments: 32 }); createBoxPartMesh(group, 'Head gasket plane', [1.9, 0.055, 1.22], darkSteel, { position: [0, 1.78, 0] }); root.add(group); } function addPiston(root: Group, aluminium: MeshStandardMaterial, darkSteel: MeshStandardMaterial): void { const group = createPartGroup(getPart('piston')); createCylinderPartMesh(group, 'Piston crown', 0.42, 0.42, 0.38, aluminium, { position: [0, 1.38, 0], radialSegments: 72 }); createCylinderPartMesh(group, 'Compression ring upper', 0.425, 0.425, 0.025, darkSteel, { position: [0, 1.55, 0], radialSegments: 72 }); createCylinderPartMesh(group, 'Compression ring lower', 0.425, 0.425, 0.025, darkSteel, { position: [0, 1.43, 0], radialSegments: 72 }); createCylinderPartMesh(group, 'Wrist pin', 0.075, 0.075, 0.98, darkSteel, { position: [0, 1.24, 0], rotation: [0, 0, Math.PI / 2], radialSegments: 32 }); root.add(group); } function addConnectingRod(root: Group, steel: MeshStandardMaterial, brass: MeshStandardMaterial): void { const group = createPartGroup(getPart('connecting-rod')); createCylinderBetween(group, 'Rod beam', [0, 1.18, 0], [0.22, 0.36, 0], 0.075, steel, 32); createCylinderPartMesh(group, 'Small-end bushing', 0.17, 0.17, 0.16, brass, { position: [0, 1.18, 0], rotation: [0, 0, Math.PI / 2], radialSegments: 40 }); createCylinderPartMesh(group, 'Big-end bearing', 0.25, 0.25, 0.2, brass, { position: [0.22, 0.36, 0], rotation: [0, 0, Math.PI / 2], radialSegments: 48 }); createBoxPartMesh(group, 'Big-end cap split line', [0.48, 0.035, 0.22], steel, { position: [0.22, 0.2, 0] }); root.add(group); } function addCrankshaft(root: Group, steel: MeshStandardMaterial, darkSteel: MeshStandardMaterial): void { const group = createPartGroup(getPart('crankshaft')); createCylinderPartMesh(group, 'Main crank journal', 0.13, 0.13, 2.38, steel, { position: [0, 0.18, 0], rotation: [0, 0, Math.PI / 2], radialSegments: 48 }); createCylinderPartMesh(group, 'Offset crank pin', 0.12, 0.12, 0.62, steel, { position: [0.22, 0.36, 0], rotation: [0, 0, Math.PI / 2], radialSegments: 48 }); createBoxPartMesh(group, 'Forward crank web', [0.16, 0.52, 0.36], darkSteel, { position: [-0.18, 0.27, 0] }); createBoxPartMesh(group, 'Rear crank web', [0.16, 0.52, 0.36], darkSteel, { position: [0.62, 0.27, 0] }); createCylinderPartMesh(group, 'Counterweight left', 0.36, 0.28, 0.15, darkSteel, { position: [-0.72, 0.1, 0], rotation: [0, 0, Math.PI / 2], radialSegments: 48 }); createCylinderPartMesh(group, 'Counterweight right', 0.36, 0.28, 0.15, darkSteel, { position: [0.94, 0.1, 0], rotation: [0, 0, Math.PI / 2], radialSegments: 48 }); root.add(group); } function addCamshaft(root: Group, darkSteel: MeshStandardMaterial, polished: MeshStandardMaterial): void { const group = createPartGroup(getPart('camshaft')); createCylinderPartMesh(group, 'Camshaft core', 0.095, 0.095, 1.64, darkSteel, { position: [0, 2.55, -0.38], rotation: [0, 0, Math.PI / 2], radialSegments: 40 }); createCylinderPartMesh(group, 'Intake cam lobe', 0.19, 0.11, 0.18, polished, { position: [-0.35, 2.55, -0.38], rotation: [0.35, 0, Math.PI / 2], radialSegments: 36 }); createCylinderPartMesh(group, 'Exhaust cam lobe', 0.19, 0.11, 0.18, polished, { position: [0.35, 2.55, -0.38], rotation: [-0.35, 0, Math.PI / 2], radialSegments: 36 }); createCylinderPartMesh(group, 'Cam sprocket', 0.38, 0.38, 0.1, darkSteel, { position: [-0.92, 2.55, -0.38], rotation: [0, 0, Math.PI / 2], radialSegments: 48 }); root.add(group); } function addValve( root: Group, partId: 'intake-valve' | 'exhaust-valve', steel: MeshStandardMaterial, flowMaterial: MeshStandardMaterial, x: number ): void { const group = createPartGroup(getPart(partId)); createCylinderPartMesh(group, `${partId} stem`, 0.035, 0.035, 0.78, steel, { position: [x, 2.12, 0.18], rotation: [0.18 * Math.sign(x), 0, 0], radialSegments: 24 }); createCylinderPartMesh(group, `${partId} valve head`, 0.18, 0.12, 0.1, steel, { position: [x, 1.73, 0.23], rotation: [Math.PI, 0, 0], radialSegments: 36 }); createSpherePartMesh(group, `${partId} port gas highlight`, 0.13, flowMaterial, { position: [x * 1.22, 1.93, 0.52], scale: [1.6, 0.55, 0.55] }); root.add(group); } function addSparkPlug( root: Group, ceramic: MeshStandardMaterial, steel: MeshStandardMaterial, copper: MeshStandardMaterial ): void { const group = createPartGroup(getPart('spark-plug')); createCylinderPartMesh(group, 'Ceramic insulator', 0.075, 0.095, 0.58, ceramic, { position: [0, 2.78, 0.12], rotation: [0.18, 0, 0], radialSegments: 32 }); createCylinderPartMesh(group, 'Threaded shell', 0.09, 0.09, 0.18, steel, { position: [0, 2.48, 0.18], rotation: [0.18, 0, 0], radialSegments: 32 }); createCylinderPartMesh(group, 'Central electrode', 0.022, 0.022, 0.26, copper, { position: [0, 2.34, 0.22], rotation: [0.18, 0, 0], radialSegments: 16 }); root.add(group); } function addIntakeManifold( root: Group, aluminium: MeshStandardMaterial, flowMaterial: MeshStandardMaterial ): void { const group = createPartGroup(getPart('intake-manifold')); createCylinderPartMesh(group, 'Intake runner', 0.18, 0.24, 1.36, aluminium, { position: [-1.06, 2.08, 0.08], rotation: [0, 0, Math.PI / 2], radialSegments: 40 }); createCylinderPartMesh(group, 'Throttle body', 0.29, 0.29, 0.32, aluminium, { position: [-1.82, 2.08, 0.08], rotation: [0, 0, Math.PI / 2], radialSegments: 48 }); createSpherePartMesh(group, 'Intake charge plume', 0.18, flowMaterial, { position: [-0.72, 2.02, 0.28], scale: [1.9, 0.5, 0.5] }); root.add(group); } function addExhaustManifold( root: Group, steel: MeshStandardMaterial, flowMaterial: MeshStandardMaterial ): void { const group = createPartGroup(getPart('exhaust-manifold')); createCylinderPartMesh(group, 'Exhaust runner', 0.19, 0.26, 1.42, steel, { position: [1.08, 1.96, 0.08], rotation: [0, 0, Math.PI / 2], radialSegments: 40 }); createCylinderPartMesh(group, 'Collector outlet', 0.3, 0.3, 0.38, steel, { position: [1.88, 1.96, 0.08], rotation: [0, 0, Math.PI / 2], radialSegments: 48 }); createSpherePartMesh(group, 'Exhaust pulse glow', 0.2, flowMaterial, { position: [0.76, 1.91, 0.28], scale: [1.8, 0.48, 0.48] }); root.add(group); } function addFlywheel(root: Group, steel: MeshStandardMaterial, black: MeshStandardMaterial): void { const group = createPartGroup(getPart('flywheel')); createCylinderPartMesh(group, 'Flywheel rim', 0.72, 0.72, 0.2, steel, { position: [1.48, 0.18, 0], rotation: [0, 0, Math.PI / 2], radialSegments: 72 }); createCylinderPartMesh(group, 'Flywheel hub', 0.24, 0.24, 0.27, black, { position: [1.48, 0.18, 0], rotation: [0, 0, Math.PI / 2], radialSegments: 48 }); createTorusPartMesh(group, 'Ring gear teeth band', 0.73, 0.028, black, { position: [1.48, 0.18, 0], rotation: [0, Math.PI / 2, 0], radialSegments: 20, tubularSegments: 96 }); root.add(group); } function addTimingChain( root: Group, steel: MeshStandardMaterial, highlight: MeshStandardMaterial ): void { const group = createPartGroup(getPart('timing-chain')); createTorusPartMesh(group, 'Timing chain loop upper', 0.55, 0.035, steel, { position: [-1.03, 1.42, -0.48], rotation: [Math.PI / 2, 0, 0], scale: [0.58, 1.62, 1] }); createCylinderPartMesh(group, 'Crank sprocket', 0.28, 0.28, 0.1, steel, { position: [-0.95, 0.18, -0.48], rotation: [0, 0, Math.PI / 2], radialSegments: 48 }); createCylinderPartMesh(group, 'Highlighted timing mark', 0.055, 0.055, 0.08, highlight, { position: [-0.95, 0.44, -0.48], rotation: [0, 0, Math.PI / 2], radialSegments: 18 }); root.add(group); }