import type { MachineDefinition } from '../types'; import { groupNode, hierarchy, makeMachine, makePart, primitive, standardCameraPresets, thumbnail, tourStep, } from './catalogueHelpers'; const scotchYokeParts = [ makePart({ id: 'rotating-crank', name: 'Rotating crank disk', group: 'Input', description: 'Input disk carrying an offset crank pin that drives the yoke slot.', primitive: primitive('cylinder', { radius: 0.36, depth: 0.08, axis: 'z' }), position: [-0.55, 0, 0], material: 'warm-highlight', explodedDirection: [-0.55, 0.35, 0], }), makePart({ id: 'crank-pin', name: 'Crank pin', group: 'Input', description: 'Offset pin sliding inside the yoke slot; its circular path creates sinusoidal yoke motion.', primitive: primitive('cylinder', { radius: 0.055, depth: 0.16, axis: 'z' }), position: [-0.25, 0, 0.08], material: 'machined-steel', explodedDirection: [-0.35, 0.55, 0.25], }), makePart({ id: 'yoke-slot', name: 'Yoke slot', group: 'Output', description: 'Vertical slot constrains the pin while the whole yoke slides horizontally.', primitive: primitive('box', { width: 0.16, height: 0.88, depth: 0.08, rounded: true, slot: true }), position: [0, 0, 0.06], material: 'blue-accent', explodedDirection: [0, 0.7, 0.2], }), makePart({ id: 'slider-block', name: 'Slider block', group: 'Output', description: 'Linear block attached to the yoke and guided by rails.', primitive: primitive('box', { width: 0.78, height: 0.28, depth: 0.24, bevel: 0.04 }), position: [0.15, 0, 0], material: 'dark-anodized', explodedDirection: [0.3, 0.45, 0], }), makePart({ id: 'guide-rails', name: 'Guide rails', group: 'Support', description: 'Parallel rails constrain the slider to pure linear motion.', primitive: primitive('cylinder', { radius: 0.035, depth: 1.85, axis: 'x', count: 2, spacing: 0.36 }), position: [0.18, 0, -0.12], rotation: [0, 0, 1.5708], material: 'brushed-steel', explodedDirection: [0, -0.55, -0.25], }), makePart({ id: 'output-rod', name: 'Output rod', group: 'Output', description: 'Rod delivering the yoke linear motion to an external load.', primitive: primitive('cylinder', { radius: 0.055, depth: 0.9, axis: 'x' }), position: [0.84, 0, 0], rotation: [0, 0, 1.5708], material: 'machined-steel', explodedDirection: [0.95, 0, 0], }), makePart({ id: 'flywheel', name: 'Flywheel', group: 'Input', description: 'Optional inertia wheel smoothing the input crank speed.', primitive: primitive('torus', { majorRadius: 0.44, tubeRadius: 0.045, spokes: 4, axis: 'z' }), position: [-0.55, 0, -0.12], material: 'machined-steel', explodedDirection: [-0.55, 0, -0.55], }), makePart({ id: 'base-plate', name: 'Base plate', group: 'Support', description: 'Reference plate carrying bearings and rail supports.', primitive: primitive('box', { width: 2.25, height: 0.08, depth: 0.72, bevel: 0.03 }), position: [0.18, -0.38, -0.12], material: 'cast-iron', explodedDirection: [0, -0.75, 0], }), makePart({ id: 'dead-center-markers', name: 'Dead-centre markers', group: 'Learning overlays', description: 'Markers show the two crank positions where output velocity crosses zero.', primitive: primitive('annotation-plane', { width: 0.72, height: 0.16, text: 'dead centres' }), position: [0.18, 0.52, 0], material: 'warm-highlight', explodedDirection: [0, 0.95, 0], crossSectionBehavior: 'hidden-when-clipped', }), ]; const genevaParts = [ makePart({ id: 'drive-wheel', name: 'Drive wheel', group: 'Input', description: 'Continuously rotating wheel carrying a drive pin and locking circular surface.', primitive: primitive('cylinder', { radius: 0.42, depth: 0.09, axis: 'z' }), position: [-0.48, 0, 0], material: 'warm-highlight', explodedDirection: [-0.7, 0.3, 0], }), makePart({ id: 'drive-pin', name: 'Drive pin', group: 'Input', description: 'Pin enters one Geneva slot per revolution to index the output wheel.', primitive: primitive('cylinder', { radius: 0.05, depth: 0.16, axis: 'z' }), position: [-0.18, 0.22, 0.08], material: 'machined-steel', explodedDirection: [-0.45, 0.6, 0.25], }), makePart({ id: 'locking-disc', name: 'Locking disc', group: 'Input', description: 'Raised circular section fits the Geneva wheel between slots to hold it stationary during dwell.', primitive: primitive('cylinder', { radius: 0.2, depth: 0.12, axis: 'z', cutout: true }), position: [-0.48, 0, 0.08], material: 'dark-anodized', explodedDirection: [-0.7, -0.3, 0.25], }), makePart({ id: 'geneva-wheel', name: 'Geneva wheel', group: 'Output', description: 'Slotted output wheel that advances intermittently one index at a time.', primitive: primitive('custom', { profile: 'geneva-wheel', slots: 4, radius: 0.52 }), position: [0.38, 0, 0], material: 'machined-steel', explodedDirection: [0.7, 0.3, 0], }), makePart({ id: 'radial-slots', name: 'Radial slots', group: 'Output', description: 'Precision slots receive the drive pin; slot count determines index angle.', primitive: primitive('custom', { profile: 'geneva-slots-highlight', slots: 4, radius: 0.44 }), position: [0.38, 0, 0.08], material: 'blue-accent', explodedDirection: [0.7, 0.6, 0.25], }), makePart({ id: 'output-shaft', name: 'Output shaft', group: 'Output', description: 'Shaft attached to the Geneva wheel, moving only during the indexing portion.', primitive: primitive('cylinder', { radius: 0.07, depth: 0.75, axis: 'z' }), position: [0.38, 0, -0.2], material: 'brushed-steel', explodedDirection: [0.55, 0, -0.75], }), makePart({ id: 'input-shaft', name: 'Input shaft', group: 'Input', description: 'Continuously rotating shaft driving the wheel.', primitive: primitive('cylinder', { radius: 0.07, depth: 0.75, axis: 'z' }), position: [-0.48, 0, -0.2], material: 'brushed-steel', explodedDirection: [-0.55, 0, -0.75], }), makePart({ id: 'dwell-indicator', name: 'Dwell indicator', group: 'Learning overlays', description: 'Arc overlay highlights the part of each input revolution where the output is locked.', primitive: primitive('annotation-plane', { width: 0.78, height: 0.18, text: 'dwell' }), position: [0, 0.72, 0], material: 'warm-highlight', explodedDirection: [0, 0.95, 0], crossSectionBehavior: 'hidden-when-clipped', }), makePart({ id: 'base-plate', name: 'Base plate', group: 'Support', description: 'Plate fixing shaft centre distance and providing visual reference.', primitive: primitive('box', { width: 1.9, height: 0.08, depth: 1.25, bevel: 0.03 }), position: [-0.05, -0.5, -0.12], material: 'cast-iron', explodedDirection: [0, -0.75, 0], }), ]; const camFollowerParts = [ makePart({ id: 'camshaft', name: 'Camshaft', group: 'Input', description: 'Rotating shaft carrying interchangeable cam profiles.', primitive: primitive('cylinder', { radius: 0.07, depth: 1.15, axis: 'z' }), position: [0, 0, 0], material: 'machined-steel', explodedDirection: [0, -0.65, 0], }), makePart({ id: 'eccentric-cam', name: 'Eccentric cam', group: 'Cam profiles', description: 'Simple circular cam with offset centre, producing near-sinusoidal follower lift.', primitive: primitive('cam', { profile: 'eccentric', radius: 0.32, eccentricity: 0.09, axis: 'z' }), position: [-0.42, 0, 0], material: 'warm-highlight', explodedDirection: [-0.7, 0.3, 0], }), makePart({ id: 'heart-cam', name: 'Heart cam', group: 'Cam profiles', description: 'Constant-velocity return profile commonly used in indexing and timing applications.', primitive: primitive('cam', { profile: 'heart', radius: 0.31, axis: 'z' }), position: [0, 0, 0], material: 'blue-accent', explodedDirection: [0, 0.6, 0], }), makePart({ id: 'snail-cam', name: 'Snail cam', group: 'Cam profiles', description: 'Ramp-and-drop cam that slowly lifts a follower then releases it rapidly.', primitive: primitive('cam', { profile: 'snail', radius: 0.34, dropAngleDegrees: 28, axis: 'z' }), position: [0.42, 0, 0], material: 'paint-yellow', explodedDirection: [0.7, 0.3, 0], }), makePart({ id: 'roller-follower', name: 'Roller follower', group: 'Follower', description: 'Low-friction follower roller tracks the active cam profile.', primitive: primitive('cylinder', { radius: 0.12, depth: 0.16, axis: 'z' }), position: [0, 0.52, 0.06], material: 'bearing-steel', explodedDirection: [0, 0.85, 0.25], }), makePart({ id: 'follower-stem', name: 'Follower stem', group: 'Follower', description: 'Linear output translating cam profile geometry into lift.', primitive: primitive('cylinder', { radius: 0.045, depth: 0.72, axis: 'y' }), position: [0, 0.88, 0.06], material: 'brushed-steel', explodedDirection: [0, 1.05, 0.25], }), makePart({ id: 'return-spring', name: 'Return spring', group: 'Follower', description: 'Keeps the follower in contact with the cam when geometry would otherwise unload it.', primitive: primitive('spring', { radius: 0.11, length: 0.48, turns: 6, axis: 'y' }), position: [0, 1.15, 0.06], material: 'machined-steel', explodedDirection: [0.32, 1.05, 0.25], }), makePart({ id: 'guide-block', name: 'Guide block', group: 'Support', description: 'Linear guide constraining the follower to vertical motion.', primitive: primitive('box', { width: 0.32, height: 0.42, depth: 0.28, bevel: 0.04 }), position: [0, 0.92, -0.06], material: 'dark-anodized', explodedDirection: [0, 0.5, -0.55], }), makePart({ id: 'lift-indicator', name: 'Lift indicator', group: 'Learning overlays', description: 'Scale overlay showing instantaneous follower displacement and profile-dependent motion.', primitive: primitive('annotation-plane', { width: 0.24, height: 0.78, text: 'lift' }), position: [0.58, 0.82, 0], material: 'warm-highlight', explodedDirection: [0.85, 0.65, 0], crossSectionBehavior: 'hidden-when-clipped', }), makePart({ id: 'profile-selector', name: 'Profile selector', group: 'Learning overlays', description: 'Selector overlay for eccentric, heart, and snail cam demonstrations.', primitive: primitive('annotation-plane', { width: 0.95, height: 0.22, text: 'eccentric | heart | snail' }), position: [0, -0.52, 0], material: 'blue-accent', explodedDirection: [0, -0.8, 0], crossSectionBehavior: 'hidden-when-clipped', }), ]; const rackPinionParts = [ makePart({ id: 'pinion-gear', name: 'Pinion gear', group: 'Rotary input', description: 'Small gear converts rotation to linear motion by meshing with straight rack teeth.', primitive: primitive('gear', { radius: 0.32, thickness: 0.16, teeth: 20, axis: 'z' }), position: [0, 0.3, 0], material: 'warm-highlight', explodedDirection: [0, 0.75, 0], }), makePart({ id: 'rack-bar', name: 'Rack bar', group: 'Linear output', description: 'Straight toothed bar whose pitch matches the pinion circular pitch.', primitive: primitive('rack', { length: 1.75, toothCount: 28, module: 0.06 }), position: [0, -0.1, 0], material: 'machined-steel', explodedDirection: [0, -0.65, 0], }), makePart({ id: 'guide-rail', name: 'Guide rail', group: 'Support', description: 'Linear bearing surface preventing rack rotation and controlling backlash.', primitive: primitive('box', { width: 1.95, height: 0.08, depth: 0.24, bevel: 0.03 }), position: [0, -0.32, -0.06], material: 'dark-anodized', explodedDirection: [0, -0.9, -0.2], }), makePart({ id: 'input-shaft', name: 'Input shaft', group: 'Rotary input', description: 'Shaft applying torque to the pinion gear.', primitive: primitive('cylinder', { radius: 0.07, depth: 0.72, axis: 'z' }), position: [0, 0.3, -0.24], material: 'brushed-steel', explodedDirection: [0, 0.35, -0.75], }), makePart({ id: 'linear-carriage', name: 'Linear carriage', group: 'Linear output', description: 'Carriage attached to the rack; used in steering racks and positioning actuators.', primitive: primitive('box', { width: 0.42, height: 0.18, depth: 0.34, bevel: 0.04 }), position: [0, -0.18, 0.12], material: 'blue-accent', explodedDirection: [0, -0.45, 0.55], }), makePart({ id: 'end-stops', name: 'End stops', group: 'Support', description: 'Stops define safe travel limits and make the reciprocating demonstration readable.', primitive: primitive('box', { width: 0.08, height: 0.36, depth: 0.36, count: 2, spacing: 1.86 }), position: [0, -0.12, 0], material: 'paint-red', explodedDirection: [0, -0.35, 0.65], }), makePart({ id: 'backlash-indicator', name: 'Backlash indicator', group: 'Learning overlays', description: 'Small overlay showing clearance between mating teeth and its effect on reversal response.', primitive: primitive('annotation-plane', { width: 0.68, height: 0.18, text: 'backlash' }), position: [0.62, 0.64, 0], material: 'warm-highlight', explodedDirection: [0.8, 0.8, 0], crossSectionBehavior: 'hidden-when-clipped', }), makePart({ id: 'position-scale', name: 'Position scale', group: 'Learning overlays', description: 'Scale relates pinion angle to rack displacement using pitch radius.', primitive: primitive('annotation-plane', { width: 1.55, height: 0.12, text: 'linear displacement' }), position: [0, -0.58, 0], material: 'blue-accent', explodedDirection: [0, -1, 0], crossSectionBehavior: 'hidden-when-clipped', }), ]; const sliderCrankParts = [ makePart({ id: 'crank-disc', name: 'Crank disc', group: 'Input', description: 'Rotating input disc that establishes crank radius.', primitive: primitive('cylinder', { radius: 0.34, depth: 0.08, axis: 'z' }), position: [-0.65, 0, 0], material: 'warm-highlight', explodedDirection: [-0.65, 0.35, 0], }), makePart({ id: 'crank-pin', name: 'Crank pin', group: 'Input', description: 'Offset joint between crank and connecting rod.', primitive: primitive('cylinder', { radius: 0.05, depth: 0.16, axis: 'z' }), position: [-0.38, 0, 0.08], material: 'machined-steel', explodedDirection: [-0.45, 0.6, 0.25], }), makePart({ id: 'connecting-rod', name: 'Connecting rod', group: 'Linkage', description: 'Rigid link whose angle creates non-sinusoidal piston motion for finite rod length.', primitive: primitive('box', { width: 0.1, height: 0.08, depth: 0.92, rounded: true }), position: [-0.02, 0, 0.08], rotation: [0, 1.5708, 0], material: 'brushed-steel', explodedDirection: [0, 0.5, 0.35], }), makePart({ id: 'slider-piston', name: 'Slider piston', group: 'Output', description: 'Constrained block moving linearly in the cylinder guide.', primitive: primitive('cylinder', { radius: 0.18, depth: 0.28, axis: 'x' }), position: [0.52, 0, 0.08], rotation: [0, 0, 1.5708], material: 'machined-steel', explodedDirection: [0.7, 0.45, 0], }), makePart({ id: 'cylinder-guide', name: 'Cylinder guide', group: 'Support', description: 'Linear guide representing the engine cylinder or machine slide.', primitive: primitive('cylinder', { radius: 0.22, depth: 0.9, axis: 'x', cutaway: true }), position: [0.62, 0, 0], rotation: [0, 0, 1.5708], material: 'transparent-shell', explodedDirection: [0.75, 0, -0.45], crossSectionBehavior: 'shell', }), makePart({ id: 'crankshaft', name: 'Crankshaft', group: 'Input', description: 'Shaft about which the crank disc rotates.', primitive: primitive('cylinder', { radius: 0.07, depth: 0.7, axis: 'z' }), position: [-0.65, 0, -0.18], material: 'brushed-steel', explodedDirection: [-0.65, 0, -0.7], }), makePart({ id: 'flywheel', name: 'Flywheel', group: 'Input', description: 'Inertia wheel carries the crank through top and bottom dead centre.', primitive: primitive('torus', { majorRadius: 0.43, tubeRadius: 0.045, spokes: 5, axis: 'z' }), position: [-0.65, 0, -0.28], material: 'machined-steel', explodedDirection: [-0.65, 0, -0.9], }), makePart({ id: 'top-dead-center-marker', name: 'Top dead centre marker', group: 'Learning overlays', description: 'Marker for minimum cylinder volume and one dead-centre position.', primitive: primitive('annotation-plane', { width: 0.42, height: 0.16, text: 'TDC' }), position: [0.28, 0.45, 0], material: 'warm-highlight', explodedDirection: [0.3, 0.85, 0], crossSectionBehavior: 'hidden-when-clipped', }), makePart({ id: 'bottom-dead-center-marker', name: 'Bottom dead centre marker', group: 'Learning overlays', description: 'Marker for maximum cylinder volume and the opposite dead-centre position.', primitive: primitive('annotation-plane', { width: 0.42, height: 0.16, text: 'BDC' }), position: [0.88, 0.45, 0], material: 'blue-accent', explodedDirection: [0.9, 0.85, 0], crossSectionBehavior: 'hidden-when-clipped', }), ]; const toggleClampParts = [ makePart({ id: 'handle-lever', name: 'Handle lever', group: 'Input', description: 'Operator handle that rotates the linkage through the over-centre position.', primitive: primitive('box', { width: 0.12, height: 0.82, depth: 0.12, rounded: true }), position: [-0.38, 0.26, 0], rotation: [0, 0, -0.55], material: 'paint-red', explodedDirection: [-0.65, 0.55, 0], }), makePart({ id: 'base-frame', name: 'Base frame', group: 'Support', description: 'Fixed bracket carrying pivots and mounting holes.', primitive: primitive('box', { width: 1.35, height: 0.12, depth: 0.46, bevel: 0.04 }), position: [0, -0.34, 0], material: 'cast-iron', explodedDirection: [0, -0.75, 0], }), makePart({ id: 'pivot-link', name: 'Pivot link', group: 'Linkage', description: 'Short link connecting the handle to the clamp arm and toggle link.', primitive: primitive('box', { width: 0.1, height: 0.48, depth: 0.1, rounded: true }), position: [-0.05, 0.06, 0], rotation: [0, 0, 0.45], material: 'brushed-steel', explodedDirection: [-0.2, 0.5, 0.25], }), makePart({ id: 'toggle-link', name: 'Toggle link', group: 'Linkage', description: 'Link passes slightly over centre to lock the clamp under load.', primitive: primitive('box', { width: 0.1, height: 0.58, depth: 0.1, rounded: true }), position: [0.24, 0.02, 0], rotation: [0, 0, -0.62], material: 'machined-steel', explodedDirection: [0.3, 0.52, 0.25], }), makePart({ id: 'clamp-arm', name: 'Clamp arm', group: 'Output', description: 'Output arm transmitting linkage force to the pressure pad.', primitive: primitive('box', { width: 0.92, height: 0.12, depth: 0.12, rounded: true }), position: [0.42, 0.1, 0], rotation: [0, 0, -0.12], material: 'warm-highlight', explodedDirection: [0.65, 0.5, 0], }), makePart({ id: 'pressure-pad', name: 'Pressure pad', group: 'Output', description: 'Swivelling pad applies clamp force to the workpiece.', primitive: primitive('cylinder', { radius: 0.16, depth: 0.08, axis: 'y' }), position: [0.86, -0.12, 0], material: 'rubber', explodedDirection: [0.95, 0.2, 0], }), makePart({ id: 'workpiece', name: 'Workpiece', group: 'Load', description: 'Reference block being clamped; used to visualise applied force.', primitive: primitive('box', { width: 0.52, height: 0.18, depth: 0.5, bevel: 0.02 }), position: [0.86, -0.32, 0], material: 'dark-anodized', explodedDirection: [0.95, -0.55, 0], }), makePart({ id: 'over-center-stop', name: 'Over-centre stop', group: 'Locking', description: 'Mechanical stop prevents excessive travel after the linkage passes centre.', primitive: primitive('box', { width: 0.12, height: 0.3, depth: 0.16, bevel: 0.02 }), position: [0.18, -0.14, 0], material: 'paint-yellow', explodedDirection: [0.25, -0.45, 0.25], }), makePart({ id: 'force-vector', name: 'Force vector', group: 'Learning overlays', description: 'Arrow overlay grows near locked position to illustrate mechanical advantage.', primitive: primitive('annotation-plane', { width: 0.7, height: 0.18, text: 'clamp force' }), position: [0.82, 0.3, 0], material: 'blue-accent', explodedDirection: [0.9, 0.65, 0], crossSectionBehavior: 'hidden-when-clipped', }), makePart({ id: 'spring-return', name: 'Return spring', group: 'Return', description: 'Optional spring opens the clamp when the handle is released.', primitive: primitive('spring', { radius: 0.07, length: 0.48, turns: 5, axis: 'x' }), position: [-0.18, -0.12, 0.18], material: 'machined-steel', explodedDirection: [-0.4, -0.35, 0.55], }), ]; export const MECHANISM_MACHINES: readonly MachineDefinition[] = [ makeMachine({ id: 'scotch-yoke', slug: 'scotch-yoke', title: 'Scotch Yoke', category: 'mechanisms', difficulty: 'Beginner', complexityScore: 35, sortOrder: 19, releasedAt: '2025-02-01', summary: 'Crank-to-linear mechanism with sinusoidal output and sliding yoke slot.', description: 'A Scotch yoke demonstration showing a crank disk, offset pin, slotted yoke, slider block, guide rails, output rod, flywheel, and dead-centre markers. The clean geometry is ideal for comparing pure sinusoidal displacement with a slider-crank mechanism.', keywords: ['scotch yoke', 'crank', 'linear motion', 'sinusoidal', 'dead centre'], facts: [ { label: 'Motion conversion', value: 'Continuous rotation to reciprocating linear motion', kind: 'note' }, { label: 'Output displacement', value: 'Approximately sinusoidal with crank angle', kind: 'note' }, { label: 'Efficiency considerations', value: 'Sliding slot contact can create friction and wear', kind: 'efficiency' }, { label: 'Invention milestone', value: 'Historic machine linkage used in pumps and actuators', kind: 'date' }, { label: 'Common applications', value: 'Valve actuators, pumps, demonstration mechanisms', kind: 'application' }, ], relatedMachineIds: ['slider-crank', 'piston-pump', 'steam-engine'], thumbnail: thumbnail('Scotch Yoke', ['rotating-crank', 'yoke-slot', 'slider-block'], 'dead-center-markers'), parts: scotchYokeParts, hierarchy: hierarchy([ groupNode('input', 'Rotary input', ['rotating-crank', 'crank-pin', 'flywheel']), groupNode('output', 'Linear output', ['yoke-slot', 'slider-block', 'output-rod']), groupNode('support', 'Support and guides', ['guide-rails', 'base-plate']), groupNode('learning', 'Learning overlays', ['dead-center-markers']), ]), cameraPresets: standardCameraPresets(4.3), guidedTour: [ tourStep('overview', 'Crank pin in a slot', 'The offset pin follows a circle while the slot converts horizontal projection into slider travel.', 'isometric', ['rotating-crank', 'crank-pin', 'yoke-slot']), tourStep('sinusoidal', 'Sinusoidal displacement', 'The yoke displacement follows crank radius times cosine of crank angle.', 'front', ['slider-block', 'output-rod', 'dead-center-markers'], { playback: 'play', rpm: 90 }), tourStep('wear', 'Sliding contact', 'The pin slides up and down in the slot, a major friction and wear consideration.', 'top', ['crank-pin', 'yoke-slot'], { explodeDistance: 0.22 }), ], animationModuleId: 'anim-scotch-yoke', labelsEnabledByDefault: true, }), makeMachine({ id: 'geneva-drive', slug: 'geneva-drive', title: 'Geneva Drive', category: 'mechanisms', difficulty: 'Intermediate', complexityScore: 46, sortOrder: 20, releasedAt: '2025-02-01', summary: 'Intermittent indexing mechanism with drive pin, radial slots, and dwell period.', description: 'A Geneva drive assembly exposing a continuously rotating driver, drive pin, locking disc, slotted Geneva wheel, shafts, dwell indicator, and base plate. The step-index animation holds the output during dwell and snaps it through each indexing phase.', keywords: ['geneva drive', 'intermittent motion', 'indexing', 'dwell', 'slots'], facts: [ { label: 'Indexing', value: 'One output step for each driver revolution', kind: 'note' }, { label: 'Typical slot counts', value: '4 to 12 slots', kind: 'ratio' }, { label: 'Efficiency considerations', value: 'Shock and acceleration limit high-speed operation', kind: 'efficiency' }, { label: 'Invention milestone', value: 'Used in film projectors and indexing machines from the 19th-20th century', kind: 'date' }, { label: 'Common applications', value: 'Index tables, packaging machines, film transport, watches', kind: 'application' }, ], relatedMachineIds: ['cam-and-follower', 'scotch-yoke', 'rack-and-pinion'], thumbnail: thumbnail('Geneva Drive', ['drive-wheel', 'geneva-wheel', 'drive-pin'], 'dwell-indicator'), parts: genevaParts, hierarchy: hierarchy([ groupNode('input', 'Input driver', ['drive-wheel', 'drive-pin', 'locking-disc', 'input-shaft']), groupNode('output', 'Indexed output', ['geneva-wheel', 'radial-slots', 'output-shaft']), groupNode('support', 'Support and overlays', ['base-plate', 'dwell-indicator']), ]), cameraPresets: standardCameraPresets(4.2), guidedTour: [ tourStep('overview', 'Intermittent output', 'The driver rotates continuously but the Geneva wheel moves only when the pin enters a slot.', 'isometric', ['drive-wheel', 'drive-pin', 'geneva-wheel']), tourStep('index', 'Indexing phase', 'The drive pin pushes a slot and rotates the output one quarter turn in this four-slot example.', 'front', ['drive-pin', 'radial-slots', 'geneva-wheel'], { playback: 'play', rpm: 60 }), tourStep('dwell', 'Dwell and locking', 'Between slots, the locking disc holds the output stationary against disturbance.', 'top', ['locking-disc', 'dwell-indicator'], { playback: 'pause', explodeDistance: 0.18 }), ], animationModuleId: 'anim-geneva-drive', labelsEnabledByDefault: true, }), makeMachine({ id: 'cam-and-follower', slug: 'cam-and-follower', title: 'Cam and Follower', category: 'mechanisms', difficulty: 'Intermediate', complexityScore: 52, sortOrder: 21, releasedAt: '2025-02-01', summary: 'Rotating cam profiles driving a spring-loaded roller follower with lift diagram.', description: 'A cam-and-follower trainer with eccentric, heart, and snail cam profiles, roller follower, stem, return spring, guide block, lift indicator, and profile selector overlay. The animation can morph between profile behaviours while keeping a standard rotation/follower-lift interface.', keywords: ['cam', 'follower', 'lift', 'eccentric cam', 'heart cam', 'snail cam', 'spring'], facts: [ { label: 'Motion conversion', value: 'Rotary profile to programmed linear displacement', kind: 'note' }, { label: 'Profile effect', value: 'Cam contour defines lift, velocity, acceleration, and jerk', kind: 'note' }, { label: 'Efficiency considerations', value: 'Rolling followers reduce sliding friction', kind: 'efficiency' }, { label: 'Invention milestone', value: 'Ancient cams; modern valve trains and automation cams', kind: 'date' }, { label: 'Common applications', value: 'Engine valve trains, automation, pumps, timing devices', kind: 'application' }, ], relatedMachineIds: ['four-stroke-petrol-engine', 'geneva-drive', 'toggle-clamp'], thumbnail: thumbnail('Cam and Follower', ['heart-cam', 'roller-follower', 'lift-indicator'], 'profile-selector'), parts: camFollowerParts, hierarchy: hierarchy([ groupNode('input', 'Input shaft', ['camshaft']), groupNode('profiles', 'Cam profiles', ['eccentric-cam', 'heart-cam', 'snail-cam']), groupNode('follower', 'Follower assembly', ['roller-follower', 'follower-stem', 'return-spring', 'guide-block']), groupNode('learning', 'Learning overlays', ['lift-indicator', 'profile-selector']), ]), cameraPresets: standardCameraPresets(4.1), guidedTour: [ tourStep('overview', 'Profile controls motion', 'The cam shape directly programs follower lift over one revolution.', 'isometric', ['heart-cam', 'roller-follower', 'follower-stem']), tourStep('profiles', 'Compare profiles', 'Eccentric, heart, and snail profiles show smooth lift, constant-velocity return, and ramp-drop behaviour.', 'front', ['eccentric-cam', 'heart-cam', 'snail-cam'], { playback: 'step' }), tourStep('follower', 'Follower and spring', 'The roller reduces friction while the spring maintains contact as lift changes.', 'right', ['roller-follower', 'return-spring', 'guide-block'], { playback: 'play', rpm: 120 }), tourStep('lift', 'Lift diagram', 'The indicator maps cam angle to follower displacement.', 'front', ['lift-indicator', 'follower-stem'], { explodeDistance: 0.2 }), ], animationModuleId: 'anim-cam-and-follower', labelsEnabledByDefault: true, }), makeMachine({ id: 'rack-and-pinion', slug: 'rack-and-pinion', title: 'Rack and Pinion', category: 'mechanisms', difficulty: 'Beginner', complexityScore: 32, sortOrder: 22, releasedAt: '2025-02-01', summary: 'Straight rack and circular pinion converting rotary motion to linear travel.', description: 'A rack-and-pinion trainer showing pinion gear, toothed rack, guide rail, input shaft, carriage, end stops, backlash indicator, and position scale. The animation is ideal for relating angular displacement, pitch radius, and linear travel.', keywords: ['rack and pinion', 'linear motion', 'gear', 'steering', 'pitch radius', 'backlash'], facts: [ { label: 'Displacement relation', value: 'Linear travel = pitch radius × pinion angle', kind: 'note' }, { label: 'Typical efficiency', value: '90-98', unit: '% depending on lubrication and guide friction', kind: 'efficiency' }, { label: 'Typical ratios', value: 'Chosen by pinion pitch radius and tooth module', kind: 'ratio' }, { label: 'Invention milestone', value: 'Common gear mechanism for centuries', kind: 'date' }, { label: 'Common applications', value: 'Steering systems, CNC axes, gates, elevators, actuators', kind: 'application' }, ], relatedMachineIds: ['bevel-gear-set', 'hydraulic-cylinder', 'slider-crank'], thumbnail: thumbnail('Rack and Pinion', ['pinion-gear', 'rack-bar', 'linear-carriage'], 'position-scale'), parts: rackPinionParts, hierarchy: hierarchy([ groupNode('input', 'Rotary input', ['pinion-gear', 'input-shaft']), groupNode('output', 'Linear output', ['rack-bar', 'linear-carriage']), groupNode('support', 'Support', ['guide-rail', 'end-stops']), groupNode('learning', 'Learning overlays', ['backlash-indicator', 'position-scale']), ]), cameraPresets: standardCameraPresets(4.1), guidedTour: [ tourStep('overview', 'Rotary to linear', 'Pinion rotation advances rack teeth in a straight line.', 'isometric', ['pinion-gear', 'rack-bar']), tourStep('pitch', 'Pitch radius relationship', 'One radian of pinion rotation moves the rack by one pitch radius.', 'front', ['pinion-gear', 'position-scale', 'rack-bar'], { playback: 'play', rpm: 80 }), tourStep('backlash', 'Clearance and reversal', 'Backlash appears as small lost motion when direction reverses.', 'top', ['backlash-indicator', 'pinion-gear'], { explodeDistance: 0.18 }), ], animationModuleId: 'anim-rack-and-pinion', labelsEnabledByDefault: true, }), makeMachine({ id: 'slider-crank', slug: 'slider-crank', title: 'Slider Crank', category: 'mechanisms', difficulty: 'Beginner', complexityScore: 37, sortOrder: 23, releasedAt: '2025-02-01', summary: 'Crank, connecting rod, and slider geometry with dead-centre markers.', description: 'A slider-crank mechanism with crank disc, crank pin, connecting rod, piston slider, guide, crankshaft, flywheel, and top/bottom dead-centre overlays. It is the kinematic foundation for piston engines, pumps, and compressors.', keywords: ['slider crank', 'connecting rod', 'piston', 'dead centre', 'kinematics'], facts: [ { label: 'Motion conversion', value: 'Rotary crank to constrained reciprocating slider', kind: 'note' }, { label: 'Kinematic note', value: 'Finite rod length creates non-sinusoidal piston motion', kind: 'note' }, { label: 'Typical efficiency', value: 'High with rolling bearings and lubrication', kind: 'efficiency' }, { label: 'Invention milestone', value: 'Ancient crank concepts; steam and internal combustion engines popularised usage', kind: 'date' }, { label: 'Common applications', value: 'Engines, compressors, pumps, presses', kind: 'application' }, ], relatedMachineIds: ['four-stroke-petrol-engine', 'piston-pump', 'steam-engine', 'scotch-yoke'], thumbnail: thumbnail('Slider Crank', ['crank-disc', 'connecting-rod', 'slider-piston'], 'top-dead-center-marker'), parts: sliderCrankParts, hierarchy: hierarchy([ groupNode('input', 'Rotary input', ['crank-disc', 'crank-pin', 'crankshaft', 'flywheel']), groupNode('linkage', 'Linkage', ['connecting-rod']), groupNode('output', 'Slider output', ['slider-piston', 'cylinder-guide']), groupNode('learning', 'Learning overlays', ['top-dead-center-marker', 'bottom-dead-center-marker']), ]), cameraPresets: standardCameraPresets(4.3), guidedTour: [ tourStep('overview', 'Engine kinematic core', 'A crank and rod convert shaft rotation to piston translation.', 'isometric', ['crank-disc', 'connecting-rod', 'slider-piston']), tourStep('rod-angle', 'Rod angle matters', 'Finite rod length changes piston speed and side load through the stroke.', 'front', ['connecting-rod', 'slider-piston'], { playback: 'play', rpm: 120 }), tourStep('dead-centres', 'Top and bottom dead centre', 'At dead centres the crank and rod align, and instantaneous torque arm approaches zero.', 'front', ['top-dead-center-marker', 'bottom-dead-center-marker', 'crank-pin'], { playback: 'step' }), ], animationModuleId: 'anim-slider-crank', labelsEnabledByDefault: true, }), makeMachine({ id: 'toggle-clamp', slug: 'toggle-clamp', title: 'Toggle Clamp', category: 'mechanisms', difficulty: 'Intermediate', complexityScore: 45, sortOrder: 24, releasedAt: '2025-02-01', summary: 'Over-centre linkage demonstrating mechanical advantage and self-locking clamp force.', description: 'A toggle clamp trainer with handle lever, base frame, pivot link, toggle link, clamp arm, pressure pad, workpiece, over-centre stop, force vector, and return spring. The guided tour explains why a linkage can multiply force and remain locked after crossing centre.', keywords: ['toggle clamp', 'over centre', 'mechanical advantage', 'linkage', 'clamping force'], facts: [ { label: 'Principle', value: 'Over-centre linkage locks under load', kind: 'note' }, { label: 'Mechanical advantage', value: 'Rises sharply near toggle alignment', kind: 'ratio' }, { label: 'Efficiency considerations', value: 'Pivot friction affects feel and holding force', kind: 'efficiency' }, { label: 'Invention milestone', value: 'Industrial fixture clamps standardised in modern manufacturing', kind: 'date' }, { label: 'Common applications', value: 'Welding fixtures, woodworking jigs, quick-release latches', kind: 'application' }, ], relatedMachineIds: ['cam-and-follower', 'hydraulic-cylinder', 'worm-gear-drive'], thumbnail: thumbnail('Toggle Clamp', ['handle-lever', 'toggle-link', 'pressure-pad'], 'force-vector'), parts: toggleClampParts, hierarchy: hierarchy([ groupNode('input', 'Input handle', ['handle-lever']), groupNode('support', 'Base and stop', ['base-frame', 'over-center-stop']), groupNode('linkage', 'Toggle linkage', ['pivot-link', 'toggle-link', 'spring-return']), groupNode('output', 'Clamp output', ['clamp-arm', 'pressure-pad', 'workpiece']), groupNode('learning', 'Learning overlays', ['force-vector']), ]), cameraPresets: standardCameraPresets(4.0), guidedTour: [ tourStep('overview', 'Compact locking linkage', 'A hand lever drives links that multiply force at the clamp pad.', 'isometric', ['handle-lever', 'pivot-link', 'clamp-arm']), tourStep('approach', 'Approaching centre', 'As the links straighten, output force increases rapidly while motion slows.', 'front', ['toggle-link', 'pressure-pad', 'force-vector'], { playback: 'play', rpm: 45 }), tourStep('lock', 'Over-centre lock', 'Once the toggle passes centre and rests on the stop, load tends to keep it closed.', 'front', ['toggle-link', 'over-center-stop', 'workpiece'], { playback: 'pause' }), tourStep('release', 'Return spring', 'A spring can bias the clamp open when the operator releases the handle.', 'right', ['spring-return', 'handle-lever'], { explodeDistance: 0.22 }), ], animationModuleId: 'anim-toggle-clamp', labelsEnabledByDefault: true, }), ];