import { CORE_MACHINE_IDS, type AssetReplacementPoint, type CameraPresetId, type CatalogueThumbnailStrategy, type CoreMachineId, type ProceduralLabel, type ProceduralMachineBlueprint, type ProceduralMotionBinding, } from './blueprintTypes'; type Axis = 'x' | 'y' | 'z'; const label = ( id: string, partId: string, text: string, anchorNodeId: string, position: readonly [number, number, number], screenPriority = 1, ): ProceduralLabel => ({ id, partId, text, anchorNodeId, position, screenPriority, }); const slot = ( machineId: CoreMachineId, id: string, partId: string, labelText: string, anchorNodeId: string, notes: string, ): AssetReplacementPoint => ({ id, partId, label: labelText, anchorNodeId, suggestedAssetPath: `src/assets/machines/${machineId}/${id}.glb`, preferredFormat: 'glb', units: 'meters', origin: 'component-local-center', upAxis: 'Y', notes, }); const thumb = ( accentPartIds: readonly string[], cameraPreset: CameraPresetId = 'isometric', notes = 'Render the procedural assembly as the default catalogue thumbnail; replace with the matching hero GLB render when available.', ): CatalogueThumbnailStrategy => ({ strategy: 'procedural-hero', cameraPreset, accentPartIds, cropping: 'wide', background: 'dark-radial', notes, }); const rotate = ( channel: string, axis: Axis = 'z', speedMultiplier = 1, phase = 0, notes?: string, ): ProceduralMotionBinding => ({ channel, kind: 'continuous-rotation', axis, speedMultiplier, phase, ...(notes === undefined ? {} : { notes }), }); const oscillate = ( channel: string, axis: Axis, amplitude: number, phase = 0, notes?: string, ): ProceduralMotionBinding => ({ channel, kind: 'reciprocating-translation', axis, amplitude, phase, ...(notes === undefined ? {} : { notes }), }); const timedLift = ( channel: string, axis: Axis, amplitude: number, phase: number, notes: string, ): ProceduralMotionBinding => ({ channel, kind: 'timed-lift', axis, amplitude, phase, notes, }); const custom = ( channel: string, kind: ProceduralMotionBinding['kind'], notes: string, phase = 0, ): ProceduralMotionBinding => ({ channel, kind, phase, notes, }); const flowPulse = (channel: string, phase: number, notes: string): ProceduralMotionBinding => ({ channel, kind: 'flow-pulse', phase, notes, }); export const CORE_MACHINE_BLUEPRINTS = { 'four-stroke-petrol-engine': { machineId: 'four-stroke-petrol-engine', title: 'Four Stroke Petrol Engine', category: 'engines', summary: 'A sectioned single-cylinder petrol engine showing intake, compression, combustion, and exhaust strokes with the crank train, camshaft, valves, spark plug, and gas exchange paths exposed.', layout: 'cutaway', scaleMeters: 1, anchorPartId: 'engine-block', animationProfileId: 'four-stroke-petrol-engine', guidedTourStageIds: ['overview', 'intake', 'compression', 'combustion', 'exhaust', 'valvetrain'], nodes: [ { id: 'four-stroke-root', kind: 'group', label: 'Single cylinder four-stroke assembly', children: [ { id: 'engine-block-cutaway', kind: 'rounded-box', partId: 'engine-block', label: 'Cutaway engine block', material: 'dark-cast-iron', transform: { position: [0, 0.05, 0], scale: [1.05, 1.35, 0.75] }, dimensions: { radius: 0.06, sectionRemoved: true }, explodedOffset: [-0.35, -0.05, 0], }, { id: 'cylinder-liner', kind: 'hollow-cylinder', partId: 'cylinder', label: 'Cylinder liner', material: 'brushed-steel', transform: { position: [0, 0.2, 0], rotation: [0, 0, 0], scale: [0.36, 0.9, 0.36] }, dimensions: { bore: 0.34, wall: 0.025 }, }, { id: 'piston-crown', kind: 'cylinder', partId: 'piston', label: 'Piston and rings', material: 'aluminium', transform: { position: [0, 0.42, 0], scale: [0.32, 0.2, 0.32] }, motionBindings: [oscillate('crank-cycle', 'y', 0.36, 0, 'Piston follows sinusoidal crank/rod approximation.')], explodedOffset: [0, 0.25, 0], }, { id: 'connecting-rod', kind: 'link', partId: 'connecting-rod', label: 'Connecting rod', material: 'brushed-steel', transform: { position: [0, -0.12, 0], scale: [0.12, 0.62, 0.1] }, motionBindings: [custom('crank-cycle', 'custom-linkage', 'Small-end tracks piston while big-end orbits crank pin.')], }, { id: 'crankshaft', kind: 'shaft', partId: 'crankshaft', label: 'Crankshaft with counterweight', material: 'polished-steel', transform: { position: [0, -0.62, 0], rotation: [1.5708, 0, 0], scale: [0.16, 0.82, 0.16] }, motionBindings: [rotate('crank-cycle', 'z', 1, 0)], }, { id: 'flywheel', kind: 'disc', partId: 'flywheel', label: 'Flywheel', material: 'brushed-steel', transform: { position: [0, -0.62, -0.55], rotation: [1.5708, 0, 0], scale: [0.58, 0.09, 0.58] }, motionBindings: [rotate('crank-cycle', 'z', 1, 0)], explodedOffset: [0, 0, -0.4], }, { id: 'camshaft', kind: 'shaft', partId: 'camshaft', label: 'Camshaft', material: 'polished-steel', transform: { position: [0.34, 0.82, 0], rotation: [1.5708, 0, 0], scale: [0.09, 0.72, 0.09] }, motionBindings: [rotate('crank-cycle', 'z', 0.5, 0, 'Camshaft rotates at half crank speed.')], }, { id: 'intake-valve', kind: 'valve', partId: 'intake-valve', label: 'Intake valve', material: 'brushed-steel', transform: { position: [-0.14, 0.94, 0], scale: [0.08, 0.42, 0.08] }, motionBindings: [timedLift('crank-cycle', 'y', 0.12, 0.05, 'Opens during the intake stroke.')], }, { id: 'exhaust-valve', kind: 'valve', partId: 'exhaust-valve', label: 'Exhaust valve', material: 'brushed-steel', transform: { position: [0.14, 0.94, 0], scale: [0.08, 0.42, 0.08] }, motionBindings: [timedLift('crank-cycle', 'y', 0.12, 0.78, 'Opens during the exhaust stroke.')], }, { id: 'spark-plug', kind: 'spark-pulse', partId: 'spark-plug', label: 'Spark plug', material: 'ceramic', transform: { position: [0, 1.15, 0], scale: [0.09, 0.24, 0.09] }, motionBindings: [custom('crank-cycle', 'visibility-pulse', 'Ignition flash appears near top dead centre of compression.', 0.48)], }, { id: 'intake-flow', kind: 'flow-arrow', partId: 'intake-manifold', label: 'Intake charge', material: 'intake-air', transform: { position: [-0.62, 0.78, 0], rotation: [0, 0, -0.35], scale: [0.55, 0.08, 0.08] }, motionBindings: [flowPulse('crank-cycle', 0.12, 'Air/fuel charge pulses while the intake valve is open.')], }, { id: 'exhaust-flow', kind: 'flow-arrow', partId: 'exhaust-manifold', label: 'Exhaust stream', material: 'exhaust-gas', transform: { position: [0.62, 0.78, 0], rotation: [0, 0, 0.35], scale: [0.55, 0.08, 0.08] }, motionBindings: [flowPulse('crank-cycle', 0.84, 'Combustion products leave during exhaust stroke.')], }, ], }, ], labels: [ label('four-stroke-piston-label', 'piston', 'Piston changes cylinder volume', 'piston-crown', [0.42, 0.44, 0.12], 5), label('four-stroke-crank-label', 'crankshaft', 'Crankshaft converts reciprocation to rotation', 'crankshaft', [0.38, -0.62, 0.18], 5), label('four-stroke-cam-label', 'camshaft', 'Camshaft times valve events at 1/2 speed', 'camshaft', [0.62, 0.9, 0.18], 4), label('four-stroke-spark-label', 'spark-plug', 'Spark near top dead centre ignites charge', 'spark-plug', [0.22, 1.24, 0.1], 4), ], replacementPoints: [ slot('four-stroke-petrol-engine', 'engine-block', 'engine-block', 'Engine block GLB', 'engine-block-cutaway', 'Use a cutaway block with cylinder bore centered on the machine origin.'), slot('four-stroke-petrol-engine', 'crank-train', 'crankshaft', 'Crankshaft, rod, piston GLB', 'crankshaft', 'Author pivots so crankshaft rotation is around local Z.'), slot('four-stroke-petrol-engine', 'valvetrain', 'camshaft', 'Camshaft and valves GLB', 'camshaft', 'Valve stems should align to local Y for the timed-lift channel.'), slot('four-stroke-petrol-engine', 'manifolds', 'intake-manifold', 'Intake and exhaust manifolds GLB', 'intake-flow', 'Keep ports named intake-manifold and exhaust-manifold for opacity controls.'), ], thumbnail: thumb(['piston', 'crankshaft', 'camshaft', 'spark-plug']), performanceNotes: [ 'The block is a transparent cutaway shell; no runtime boolean CSG is required.', 'Crank, cam, piston, and valve channels share one normalized engine-cycle clock for deterministic step-through.', ], }, 'two-stroke-engine': { machineId: 'two-stroke-engine', title: 'Two Stroke Engine', category: 'engines', summary: 'A compact port-controlled two-stroke engine showing crankcase compression, transfer flow, exhaust blowdown, piston port timing, and a simplified spark ignition event.', layout: 'cutaway', scaleMeters: 0.85, anchorPartId: 'crankcase', animationProfileId: 'two-stroke-engine', guidedTourStageIds: ['overview', 'upstroke', 'ignition', 'blowdown', 'transfer-flow'], nodes: [ { id: 'two-stroke-root', kind: 'group', children: [ { id: 'two-stroke-cylinder', kind: 'hollow-cylinder', partId: 'cylinder-barrel', label: 'Cylinder barrel with ports', material: 'dark-cast-iron', transform: { position: [0, 0.32, 0], scale: [0.38, 0.92, 0.38] }, dimensions: { exhaustPortAngle: 75, transferPortAngle: -75, intakePort: true }, explodedOffset: [-0.32, 0, 0], }, { id: 'two-stroke-crankcase', kind: 'rounded-box', partId: 'crankcase', label: 'Sealed crankcase', material: 'aluminium', transform: { position: [0, -0.42, 0], scale: [0.82, 0.5, 0.62] }, dimensions: { radius: 0.08, crankcaseCompression: true }, }, { id: 'two-stroke-piston', kind: 'cylinder', partId: 'piston', label: 'Piston controls ports', material: 'aluminium', transform: { position: [0, 0.38, 0], scale: [0.33, 0.2, 0.33] }, motionBindings: [oscillate('two-stroke-cycle', 'y', 0.36, 0)], }, { id: 'two-stroke-rod', kind: 'link', partId: 'connecting-rod', label: 'Connecting rod', material: 'brushed-steel', transform: { position: [0, -0.08, 0], scale: [0.1, 0.55, 0.1] }, motionBindings: [custom('two-stroke-cycle', 'custom-linkage', 'Rod angularity follows crank pin and piston pin.')], }, { id: 'two-stroke-crankshaft', kind: 'shaft', partId: 'crankshaft', label: 'Crankshaft', material: 'polished-steel', transform: { position: [0, -0.52, 0], rotation: [1.5708, 0, 0], scale: [0.15, 0.62, 0.15] }, motionBindings: [rotate('two-stroke-cycle', 'z')], }, { id: 'two-stroke-reed', kind: 'valve', partId: 'reed-valve', label: 'Reed inlet valve', material: 'bronze', transform: { position: [-0.55, -0.28, 0], rotation: [0, 0, -0.75], scale: [0.16, 0.18, 0.05] }, motionBindings: [custom('two-stroke-cycle', 'reciprocating-rotation', 'Reed flexes open when crankcase pressure drops.', 0.2)], }, { id: 'two-stroke-transfer', kind: 'flow-arrow', partId: 'transfer-port', label: 'Transfer port flow', material: 'intake-air', transform: { position: [-0.31, 0.12, 0], rotation: [0, 0, 0.95], scale: [0.42, 0.07, 0.07] }, motionBindings: [flowPulse('two-stroke-cycle', 0.72, 'Fresh charge sweeps from crankcase to cylinder.')], }, { id: 'two-stroke-exhaust', kind: 'flow-arrow', partId: 'exhaust-port', label: 'Exhaust blowdown', material: 'exhaust-gas', transform: { position: [0.55, 0.2, 0], rotation: [0, 0, 0.15], scale: [0.5, 0.08, 0.08] }, motionBindings: [flowPulse('two-stroke-cycle', 0.62, 'Exhaust port uncovers before transfer ports.')], }, { id: 'two-stroke-spark', kind: 'spark-pulse', partId: 'spark-plug', label: 'Spark plug', material: 'ceramic', transform: { position: [0, 0.98, 0], scale: [0.08, 0.22, 0.08] }, motionBindings: [custom('two-stroke-cycle', 'visibility-pulse', 'Spark fires every crank revolution.', 0.45)], }, ], }, ], labels: [ label('two-stroke-port-label', 'transfer-port', 'Transfer ports are opened by piston skirt position', 'two-stroke-transfer', [-0.58, 0.22, 0.16], 5), label('two-stroke-crankcase-label', 'crankcase', 'Crankcase pre-compresses fresh charge', 'two-stroke-crankcase', [0.42, -0.42, 0.18], 4), label('two-stroke-exhaust-label', 'exhaust-port', 'Exhaust port opens before transfer flow', 'two-stroke-exhaust', [0.78, 0.28, 0.12], 4), ], replacementPoints: [ slot('two-stroke-engine', 'cylinder-barrel', 'cylinder-barrel', 'Ported cylinder barrel GLB', 'two-stroke-cylinder', 'Model exhaust and transfer ports as open geometry so cross-section remains readable.'), slot('two-stroke-engine', 'crankcase', 'crankcase', 'Crankcase GLB', 'two-stroke-crankcase', 'Origin should remain at crankshaft centerline.'), slot('two-stroke-engine', 'piston-rod-crank', 'piston', 'Piston, rod, and crank GLB', 'two-stroke-piston', 'Piston should expose skirt edges that cover and uncover ports.'), slot('two-stroke-engine', 'ports-and-flow', 'transfer-port', 'Transfer and exhaust ducts GLB', 'two-stroke-transfer', 'Keep port identifiers stable for guided tour highlighting.'), ], thumbnail: thumb(['piston', 'transfer-port', 'exhaust-port']), performanceNotes: [ 'Flow arrows are lightweight shader billboards that can be disabled on low-power devices.', 'Port timing is derived from piston travel rather than separate keyed timelines, keeping loop closure exact.', ], }, 'diesel-engine': { machineId: 'diesel-engine', title: 'Diesel Engine', category: 'engines', summary: 'A compression-ignition cylinder with high compression chamber, injector spray plume, heavier crank train, intake/exhaust valves, and heat/pressure visualization.', layout: 'cutaway', scaleMeters: 1.1, anchorPartId: 'engine-block', animationProfileId: 'diesel-engine', guidedTourStageIds: ['overview', 'air-intake', 'compression-ignition', 'fuel-injection', 'exhaust'], nodes: [ { id: 'diesel-root', kind: 'group', children: [ { id: 'diesel-block', kind: 'rounded-box', partId: 'engine-block', label: 'Heavy engine block', material: 'dark-cast-iron', transform: { position: [0, 0.02, 0], scale: [1.12, 1.42, 0.78] }, dimensions: { radius: 0.05, highCompression: true }, explodedOffset: [-0.38, 0, 0], }, { id: 'diesel-cylinder', kind: 'hollow-cylinder', partId: 'cylinder', label: 'High-compression cylinder', material: 'brushed-steel', transform: { position: [0, 0.22, 0], scale: [0.34, 0.95, 0.34] }, }, { id: 'diesel-piston', kind: 'cylinder', partId: 'piston', label: 'Bowl piston', material: 'aluminium', transform: { position: [0, 0.4, 0], scale: [0.32, 0.22, 0.32] }, dimensions: { crownBowl: true, compressionRatio: 18 }, motionBindings: [oscillate('diesel-cycle', 'y', 0.37, 0)], }, { id: 'diesel-rod', kind: 'link', partId: 'connecting-rod', label: 'Robust connecting rod', material: 'brushed-steel', transform: { position: [0, -0.1, 0], scale: [0.12, 0.65, 0.12] }, motionBindings: [custom('diesel-cycle', 'custom-linkage', 'Rod follows the heavier diesel crank throw.')], }, { id: 'diesel-crankshaft', kind: 'shaft', partId: 'crankshaft', label: 'Crankshaft', material: 'polished-steel', transform: { position: [0, -0.66, 0], rotation: [1.5708, 0, 0], scale: [0.18, 0.88, 0.18] }, motionBindings: [rotate('diesel-cycle', 'z')], }, { id: 'diesel-injector', kind: 'valve', partId: 'fuel-injector', label: 'Common-rail injector', material: 'brass', transform: { position: [0, 1.16, 0], scale: [0.09, 0.34, 0.09] }, motionBindings: [custom('diesel-cycle', 'pressure-pulse', 'Injector opens near the end of compression stroke.', 0.47)], }, { id: 'diesel-spray', kind: 'fluid-volume', partId: 'fuel-spray', label: 'Atomized diesel spray', material: 'diesel-spray', transform: { position: [0, 0.92, 0], scale: [0.28, 0.32, 0.28] }, dimensions: { coneAngleDegrees: 72, droplets: 64 }, motionBindings: [custom('diesel-cycle', 'visibility-pulse', 'Spray cone appears during injection.', 0.48)], }, { id: 'diesel-heat', kind: 'heat-glow', partId: 'combustion-chamber', label: 'Compression ignition heat', material: 'hot-gas', transform: { position: [0, 0.78, 0], scale: [0.3, 0.18, 0.3] }, motionBindings: [custom('diesel-cycle', 'heat-pulse', 'Heat pulse follows injected fuel ignition.', 0.52)], }, { id: 'diesel-intake-valve', kind: 'valve', partId: 'intake-valve', label: 'Intake valve', material: 'brushed-steel', transform: { position: [-0.14, 0.98, 0], scale: [0.08, 0.42, 0.08] }, motionBindings: [timedLift('diesel-cycle', 'y', 0.11, 0.08, 'Draws in air only; no fuel premix.')], }, { id: 'diesel-exhaust-valve', kind: 'valve', partId: 'exhaust-valve', label: 'Exhaust valve', material: 'brushed-steel', transform: { position: [0.14, 0.98, 0], scale: [0.08, 0.42, 0.08] }, motionBindings: [timedLift('diesel-cycle', 'y', 0.11, 0.78, 'Releases high-pressure exhaust gas.')], }, ], }, ], labels: [ label('diesel-injector-label', 'fuel-injector', 'Injector meters fuel into hot compressed air', 'diesel-injector', [0.28, 1.2, 0.12], 5), label('diesel-piston-label', 'piston', 'Bowl shapes swirl and mixing', 'diesel-piston', [0.42, 0.42, 0.14], 4), label('diesel-heat-label', 'combustion-chamber', 'Ignition comes from compression temperature', 'diesel-heat', [-0.44, 0.82, 0.15], 5), ], replacementPoints: [ slot('diesel-engine', 'diesel-block', 'engine-block', 'Diesel block GLB', 'diesel-block', 'Use heavier wall thickness than petrol engine model.'), slot('diesel-engine', 'injector', 'fuel-injector', 'Fuel injector GLB', 'diesel-injector', 'Nozzle tip should remain centered above cylinder axis.'), slot('diesel-engine', 'piston-cranktrain', 'piston', 'Diesel piston and crank train GLB', 'diesel-piston', 'Local pivots follow the same engine-cycle linkage channels.'), slot('diesel-engine', 'valvetrain', 'intake-valve', 'Diesel valvetrain GLB', 'diesel-intake-valve', 'Valve stems align with local Y lift.'), ], thumbnail: thumb(['fuel-injector', 'fuel-spray', 'piston']), performanceNotes: [ 'Spray and heat effects use pooled translucent particles to avoid allocations during RPM changes.', 'The heavier diesel block uses the same cutaway renderer path as other piston engines for consistency.', ], }, 'v8-engine': { machineId: 'v8-engine', title: 'V8 Engine', category: 'engines', summary: 'A cross-plane V8 layout with two banks of four cylinders, shared crankshaft, intake valley, exhaust headers, firing-order highlights, and synchronized piston phasing.', layout: 'hybrid', scaleMeters: 1.8, anchorPartId: 'engine-block', animationProfileId: 'v8-engine', guidedTourStageIds: ['overview', 'cylinder-banks', 'crankshaft', 'firing-order', 'intake-exhaust'], nodes: [ { id: 'v8-root', kind: 'group', children: [ { id: 'v8-block', kind: 'rounded-box', partId: 'engine-block', label: 'V block and crankcase', material: 'dark-cast-iron', transform: { position: [0, 0, 0], scale: [1.42, 0.72, 0.9] }, dimensions: { bankAngleDegrees: 90, sectioned: true }, explodedOffset: [0, -0.25, 0], }, { id: 'v8-left-bank', kind: 'group', partId: 'left-cylinder-bank', label: 'Left cylinder bank', transform: { position: [-0.42, 0.38, 0], rotation: [0, 0, 0.55] }, children: [ { id: 'v8-l-cylinders', kind: 'hollow-cylinder', partId: 'left-cylinders', label: 'Four left-bank cylinders', material: 'brushed-steel', transform: { position: [0, 0, 0], scale: [0.24, 0.78, 1.18] }, dimensions: { count: 4, spacing: 0.32 }, }, { id: 'v8-l-pistons', kind: 'cylinder', partId: 'left-pistons', label: 'Left-bank pistons', material: 'aluminium', transform: { position: [0, -0.02, 0], scale: [0.22, 0.16, 1.12] }, dimensions: { count: 4, phases: [0, 0.25, 0.75, 0.5] }, motionBindings: [custom('v8-cycle', 'custom-linkage', 'Four pistons follow cross-plane firing-order phase offsets.')], }, ], }, { id: 'v8-right-bank', kind: 'group', partId: 'right-cylinder-bank', label: 'Right cylinder bank', transform: { position: [0.42, 0.38, 0], rotation: [0, 0, -0.55] }, children: [ { id: 'v8-r-cylinders', kind: 'hollow-cylinder', partId: 'right-cylinders', label: 'Four right-bank cylinders', material: 'brushed-steel', transform: { position: [0, 0, 0], scale: [0.24, 0.78, 1.18] }, dimensions: { count: 4, spacing: 0.32 }, }, { id: 'v8-r-pistons', kind: 'cylinder', partId: 'right-pistons', label: 'Right-bank pistons', material: 'aluminium', transform: { position: [0, -0.02, 0], scale: [0.22, 0.16, 1.12] }, dimensions: { count: 4, phases: [0.125, 0.625, 0.375, 0.875] }, motionBindings: [custom('v8-cycle', 'custom-linkage', 'Right bank pistons are phase-offset according to the firing order.')], }, ], }, { id: 'v8-crankshaft', kind: 'shaft', partId: 'crankshaft', label: 'Cross-plane crankshaft', material: 'polished-steel', transform: { position: [0, -0.35, 0], rotation: [1.5708, 0, 0], scale: [0.18, 1.55, 0.18] }, dimensions: { throws: 4, crossPlane: true }, motionBindings: [rotate('v8-cycle', 'z')], }, { id: 'v8-intake-manifold', kind: 'manifold', partId: 'intake-manifold', label: 'Intake manifold', material: 'aluminium', transform: { position: [0, 0.77, 0], scale: [0.72, 0.2, 1.12] }, explodedOffset: [0, 0.35, 0], }, { id: 'v8-exhaust-headers', kind: 'pipe', partId: 'exhaust-headers', label: 'Exhaust headers', material: 'brushed-steel', transform: { position: [0, 0.22, 0], scale: [1.72, 0.18, 1.22] }, dimensions: { branchesPerBank: 4 }, motionBindings: [flowPulse('v8-cycle', 0.4, 'Header glow and pulses follow cylinder firing order.')], }, { id: 'v8-firing-order-ribbon', kind: 'polyline', partId: 'firing-order', label: 'Firing order highlight', material: 'accent-warm', transform: { position: [0, 1.02, 0], scale: [1, 1, 1] }, dimensions: { order: ['1', '8', '4', '3', '6', '5', '7', '2'] }, motionBindings: [custom('v8-cycle', 'visibility-pulse', 'Cylinder highlight advances through a typical cross-plane firing order.')], }, ], }, ], labels: [ label('v8-bank-label', 'left-cylinder-bank', 'Two banks form the V layout', 'v8-left-bank', [-0.9, 0.72, 0.24], 5), label('v8-crank-label', 'crankshaft', 'Cross-plane crank smooths torque delivery', 'v8-crankshaft', [0.34, -0.38, 0.62], 5), label('v8-order-label', 'firing-order', 'Animated firing order shows ignition sequence', 'v8-firing-order-ribbon', [0.18, 1.12, 0.64], 4), ], replacementPoints: [ slot('v8-engine', 'v8-block', 'engine-block', 'V8 block GLB', 'v8-block', 'Bank angle and crank centerline should match procedural origin for animation reuse.'), slot('v8-engine', 'v8-crankshaft', 'crankshaft', 'Cross-plane crankshaft GLB', 'v8-crankshaft', 'Use four crankpin pivots and expose throw metadata for piston rods.'), slot('v8-engine', 'v8-bank-left', 'left-cylinder-bank', 'Left cylinder bank GLB', 'v8-left-bank', 'Keep cylinder indices 1,3,5,7 addressable for firing highlights.'), slot('v8-engine', 'v8-bank-right', 'right-cylinder-bank', 'Right cylinder bank GLB', 'v8-right-bank', 'Keep cylinder indices 2,4,6,8 addressable for firing highlights.'), slot('v8-engine', 'v8-intake-exhaust', 'intake-manifold', 'Intake and exhaust system GLB', 'v8-intake-manifold', 'Manifold can be replaced independently of the animated crank train.'), ], thumbnail: thumb(['left-cylinder-bank', 'right-cylinder-bank', 'crankshaft'], 'exploded'), performanceNotes: [ 'Cylinder/piston repeats are suitable for instancing with per-instance phase uniforms.', 'Firing order visualization is data-driven and does not require unique geometry per cylinder.', ], }, 'wankel-rotary-engine': { machineId: 'wankel-rotary-engine', title: 'Wankel Rotary Engine', category: 'engines', summary: 'A rotary engine diagram with epitrochoid housing, triangular rotor, eccentric shaft, apex seals, spark plugs, ports, and chamber-volume colour cycling.', layout: 'sectioned', scaleMeters: 0.9, anchorPartId: 'epitrochoid-housing', animationProfileId: 'wankel-rotary-engine', guidedTourStageIds: ['overview', 'rotor-motion', 'intake-port', 'compression-ignition', 'exhaust-port'], nodes: [ { id: 'wankel-root', kind: 'group', children: [ { id: 'wankel-housing', kind: 'epitrochoid-housing', partId: 'epitrochoid-housing', label: 'Epitrochoid housing', material: 'dark-cast-iron', transform: { position: [0, 0, 0], scale: [0.95, 0.58, 0.22] }, dimensions: { lobeCount: 2, sectionFace: true }, explodedOffset: [-0.28, 0, 0], }, { id: 'wankel-rotor', kind: 'rotor-triangle', partId: 'rotor', label: 'Triangular rotor', material: 'aluminium', transform: { position: [0.08, 0, 0.02], scale: [0.58, 0.58, 0.18] }, dimensions: { sides: 3, crownedFaces: true }, motionBindings: [custom('wankel-cycle', 'custom-linkage', 'Rotor turns at one third eccentric shaft speed while orbiting.')], }, { id: 'wankel-eccentric-shaft', kind: 'shaft', partId: 'eccentric-shaft', label: 'Eccentric shaft', material: 'polished-steel', transform: { position: [0, 0, -0.1], rotation: [1.5708, 0, 0], scale: [0.15, 0.5, 0.15] }, motionBindings: [rotate('wankel-cycle', 'z', 1)], }, { id: 'wankel-apex-seals', kind: 'polyline', partId: 'apex-seals', label: 'Apex seals', material: 'brushed-steel', transform: { position: [0.08, 0, 0.12], scale: [0.6, 0.6, 0.04] }, dimensions: { count: 3 }, motionBindings: [custom('wankel-cycle', 'custom-linkage', 'Apex seal anchors follow rotor corners.')], }, { id: 'wankel-intake-port', kind: 'flow-arrow', partId: 'intake-port', label: 'Intake port', material: 'intake-air', transform: { position: [-0.58, -0.18, 0.02], rotation: [0, 0, 0.15], scale: [0.42, 0.07, 0.07] }, motionBindings: [flowPulse('wankel-cycle', 0.1, 'Port opens as a chamber expands.')], }, { id: 'wankel-exhaust-port', kind: 'flow-arrow', partId: 'exhaust-port', label: 'Exhaust port', material: 'exhaust-gas', transform: { position: [0.6, -0.16, 0.02], rotation: [0, 0, -0.15], scale: [0.42, 0.07, 0.07] }, motionBindings: [flowPulse('wankel-cycle', 0.82, 'Burnt gas exits through side/exhaust port.')], }, { id: 'wankel-spark-plugs', kind: 'spark-pulse', partId: 'spark-plugs', label: 'Twin spark plugs', material: 'ceramic', transform: { position: [0.4, 0.25, 0.08], scale: [0.08, 0.22, 0.08] }, dimensions: { count: 2 }, motionBindings: [custom('wankel-cycle', 'visibility-pulse', 'Spark pulses as compressed chamber reaches plug side.', 0.48)], }, { id: 'wankel-chambers', kind: 'fluid-volume', partId: 'working-chambers', label: 'Three working chambers', material: 'hot-gas', transform: { position: [0, 0, 0.04], scale: [0.78, 0.48, 0.05] }, dimensions: { chamberCount: 3, colorCycle: true }, motionBindings: [custom('wankel-cycle', 'heat-pulse', 'Chambers cycle through intake, compression, combustion, and exhaust states.')], }, ], }, ], labels: [ label('wankel-housing-label', 'epitrochoid-housing', 'Epitrochoid housing creates variable chambers', 'wankel-housing', [-0.7, 0.42, 0.14], 5), label('wankel-rotor-label', 'rotor', 'Rotor orbits while turning inside housing', 'wankel-rotor', [0.18, 0.36, 0.18], 5), label('wankel-apex-label', 'apex-seals', 'Apex seals maintain chamber separation', 'wankel-apex-seals', [-0.18, -0.42, 0.16], 4), ], replacementPoints: [ slot('wankel-rotary-engine', 'housing', 'epitrochoid-housing', 'Epitrochoid housing GLB', 'wankel-housing', 'Use a sectioned front cover or provide a separate cover part for exploded views.'), slot('wankel-rotary-engine', 'rotor', 'rotor', 'Rotor GLB', 'wankel-rotor', 'Rotor pivot should be at rotor centroid; eccentric orbit is supplied by animation channel.'), slot('wankel-rotary-engine', 'eccentric-shaft', 'eccentric-shaft', 'Eccentric shaft GLB', 'wankel-eccentric-shaft', 'Shaft axis should align with local Z rotation channel.'), slot('wankel-rotary-engine', 'ports-plugs', 'spark-plugs', 'Ports and spark plugs GLB', 'wankel-spark-plugs', 'Maintain named port submeshes for tour highlights.'), ], thumbnail: thumb(['rotor', 'epitrochoid-housing', 'apex-seals'], 'section'), performanceNotes: [ 'The epitrochoid is a parametric 2D profile extruded once, then cached per material variant.', 'Chamber colour states are shader uniforms bound to the normalized rotary cycle.', ], }, 'steam-engine': { machineId: 'steam-engine', title: 'Steam Engine', category: 'engines', summary: 'A horizontal reciprocating steam engine with cylinder, piston rod, crosshead, connecting rod, flywheel, slide valve, eccentric linkage, steam pipes, and governor.', layout: 'hybrid', scaleMeters: 1.6, anchorPartId: 'bedplate', animationProfileId: 'steam-engine', guidedTourStageIds: ['overview', 'steam-admission', 'piston-stroke', 'flywheel', 'slide-valve', 'governor'], nodes: [ { id: 'steam-root', kind: 'group', children: [ { id: 'steam-bedplate', kind: 'rounded-box', partId: 'bedplate', label: 'Bedplate', material: 'dark-cast-iron', transform: { position: [0, -0.35, 0], scale: [1.75, 0.16, 0.58] }, }, { id: 'steam-cylinder', kind: 'hollow-cylinder', partId: 'steam-cylinder', label: 'Steam cylinder', material: 'brushed-steel', transform: { position: [-0.62, 0, 0], rotation: [0, 0, 1.5708], scale: [0.3, 0.62, 0.3] }, explodedOffset: [-0.38, 0, 0], }, { id: 'steam-piston-rod', kind: 'rod', partId: 'piston-rod', label: 'Piston rod', material: 'polished-steel', transform: { position: [-0.22, 0, 0], rotation: [0, 0, 1.5708], scale: [0.06, 0.8, 0.06] }, motionBindings: [oscillate('steam-cycle', 'x', 0.36, 0)], }, { id: 'steam-crosshead', kind: 'box', partId: 'crosshead', label: 'Crosshead guide', material: 'aluminium', transform: { position: [-0.08, 0, 0], scale: [0.16, 0.22, 0.22] }, motionBindings: [oscillate('steam-cycle', 'x', 0.36, 0)], }, { id: 'steam-connecting-rod', kind: 'link', partId: 'connecting-rod', label: 'Connecting rod', material: 'brushed-steel', transform: { position: [0.35, 0, 0], scale: [0.1, 0.9, 0.08] }, motionBindings: [custom('steam-cycle', 'custom-linkage', 'Rod connects crosshead to crank pin on flywheel shaft.')], }, { id: 'steam-flywheel', kind: 'torus', partId: 'flywheel', label: 'Flywheel', material: 'brushed-steel', transform: { position: [0.82, 0, 0], rotation: [1.5708, 0, 0], scale: [0.52, 0.52, 0.08] }, dimensions: { spokes: 8 }, motionBindings: [rotate('steam-cycle', 'z')], }, { id: 'steam-slide-valve', kind: 'valve', partId: 'slide-valve', label: 'Slide valve', material: 'bronze', transform: { position: [-0.62, 0.34, 0], scale: [0.34, 0.12, 0.16] }, motionBindings: [oscillate('steam-cycle', 'x', 0.18, 0.25, 'Eccentric rod shifts valve timing ahead of piston.')], }, { id: 'steam-eccentric-rod', kind: 'link', partId: 'eccentric-rod', label: 'Eccentric valve rod', material: 'polished-steel', transform: { position: [0.1, 0.28, 0], scale: [0.08, 1.05, 0.06] }, motionBindings: [custom('steam-cycle', 'custom-linkage', 'Eccentric on crankshaft drives slide valve.')], }, { id: 'steam-governor', kind: 'vane-wheel', partId: 'centrifugal-governor', label: 'Centrifugal governor', material: 'brass', transform: { position: [0.28, 0.62, 0], scale: [0.28, 0.45, 0.28] }, motionBindings: [rotate('steam-cycle', 'y', 2.2, 0, 'Governor spins faster than flywheel to visualize speed regulation.')], }, { id: 'steam-inlet', kind: 'flow-arrow', partId: 'steam-inlet', label: 'Live steam inlet', material: 'steam', transform: { position: [-1.02, 0.36, 0], rotation: [0, 0, -0.2], scale: [0.42, 0.08, 0.08] }, motionBindings: [flowPulse('steam-cycle', 0.18, 'Steam admitted alternately to either side of piston.')], }, ], }, ], labels: [ label('steam-valve-label', 'slide-valve', 'Slide valve alternates steam admission', 'steam-slide-valve', [-0.86, 0.52, 0.16], 5), label('steam-crosshead-label', 'crosshead', 'Crosshead constrains linear motion', 'steam-crosshead', [-0.04, 0.24, 0.16], 4), label('steam-flywheel-label', 'flywheel', 'Flywheel stores rotational energy', 'steam-flywheel', [0.98, 0.46, 0.16], 5), label('steam-governor-label', 'centrifugal-governor', 'Governor throttles steam as speed rises', 'steam-governor', [0.48, 0.88, 0.18], 4), ], replacementPoints: [ slot('steam-engine', 'cylinder-valve-chest', 'steam-cylinder', 'Cylinder and valve chest GLB', 'steam-cylinder', 'Keep cylinder axis aligned with local X for piston stroke.'), slot('steam-engine', 'flywheel-crank', 'flywheel', 'Flywheel and crank GLB', 'steam-flywheel', 'Flywheel rotation channel expects local Z axis.'), slot('steam-engine', 'linkage', 'connecting-rod', 'Crosshead and linkage GLB', 'steam-connecting-rod', 'Expose crosshead, rod, and eccentric rod as selectable child nodes.'), slot('steam-engine', 'governor', 'centrifugal-governor', 'Centrifugal governor GLB', 'steam-governor', 'Governor balls should be independently addressable for speed demonstrations.'), ], thumbnail: thumb(['flywheel', 'slide-valve', 'centrifugal-governor']), performanceNotes: [ 'Linkages are analytical transforms driven from flywheel phase to guarantee clean looping.', 'Steam flow opacity is animated separately from metal geometry for reduced overdraw on mobile.', ], }, 'jet-engine-turbojet': { machineId: 'jet-engine-turbojet', title: 'Jet Engine (Turbojet)', category: 'engines', summary: 'A cutaway axial turbojet showing inlet, compressor stages, combustor cans, turbine stages, shared shaft, nozzle acceleration, and core airflow temperature rise.', layout: 'sectioned', scaleMeters: 2.2, anchorPartId: 'engine-casing', animationProfileId: 'jet-engine-turbojet', guidedTourStageIds: ['overview', 'intake', 'compression', 'combustion', 'turbine', 'nozzle'], nodes: [ { id: 'turbojet-root', kind: 'group', children: [ { id: 'turbojet-casing', kind: 'hollow-cylinder', partId: 'engine-casing', label: 'Cutaway engine casing', material: 'transparent-shell', transform: { position: [0, 0, 0], rotation: [0, 0, 1.5708], scale: [0.55, 2.0, 0.55] }, dimensions: { sectionRemovedDegrees: 105 }, explodedOffset: [0, 0.42, 0], }, { id: 'turbojet-inlet', kind: 'hollow-cylinder', partId: 'intake', label: 'Intake diffuser', material: 'aluminium', transform: { position: [-0.95, 0, 0], rotation: [0, 0, 1.5708], scale: [0.48, 0.35, 0.48] }, }, { id: 'turbojet-compressor', kind: 'compressor-stage', partId: 'compressor-stages', label: 'Axial compressor stages', material: 'brushed-steel', transform: { position: [-0.35, 0, 0], rotation: [0, 0, 1.5708], scale: [0.42, 0.75, 0.42] }, dimensions: { stages: 7, bladeTwist: true }, motionBindings: [rotate('jet-spool', 'x', 1.8, 0, 'Compressor rotates on main shaft.')], }, { id: 'turbojet-combustor', kind: 'fluid-volume', partId: 'combustion-chamber', label: 'Combustion chamber', material: 'hot-gas', transform: { position: [0.35, 0, 0], rotation: [0, 0, 1.5708], scale: [0.44, 0.48, 0.44] }, dimensions: { cans: 8, flameCore: true }, motionBindings: [custom('jet-spool', 'heat-pulse', 'Combustion glow is steady with a subtle pressure shimmer.')], }, { id: 'turbojet-turbine', kind: 'turbine-stage', partId: 'turbine-stages', label: 'Turbine stages', material: 'polished-steel', transform: { position: [0.82, 0, 0], rotation: [0, 0, 1.5708], scale: [0.38, 0.45, 0.38] }, dimensions: { stages: 2, highTemperature: true }, motionBindings: [rotate('jet-spool', 'x', 1.8)], }, { id: 'turbojet-shaft', kind: 'shaft', partId: 'main-shaft', label: 'Main shaft', material: 'polished-steel', transform: { position: [0, 0, 0], rotation: [0, 0, 1.5708], scale: [0.08, 1.75, 0.08] }, motionBindings: [rotate('jet-spool', 'x', 1.8)], }, { id: 'turbojet-nozzle', kind: 'hollow-cylinder', partId: 'exhaust-nozzle', label: 'Converging nozzle', material: 'brushed-steel', transform: { position: [1.2, 0, 0], rotation: [0, 0, 1.5708], scale: [0.34, 0.45, 0.34] }, }, { id: 'turbojet-flow', kind: 'flow-arrow', partId: 'core-flow', label: 'Core airflow', material: 'hot-gas', transform: { position: [0.08, 0, 0], rotation: [0, 0, 1.5708], scale: [1.85, 0.08, 0.08] }, motionBindings: [flowPulse('jet-spool', 0, 'Flow accelerates from intake through exhaust nozzle.')], }, ], }, ], labels: [ label('turbojet-compressor-label', 'compressor-stages', 'Compressor raises air pressure in stages', 'turbojet-compressor', [-0.45, 0.48, 0.18], 5), label('turbojet-combustor-label', 'combustion-chamber', 'Fuel burns at near constant pressure', 'turbojet-combustor', [0.38, 0.52, 0.18], 5), label('turbojet-turbine-label', 'turbine-stages', 'Turbine extracts work for compressor', 'turbojet-turbine', [0.86, 0.42, 0.18], 4), label('turbojet-nozzle-label', 'exhaust-nozzle', 'Nozzle converts pressure to jet velocity', 'turbojet-nozzle', [1.34, 0.36, 0.16], 4), ], replacementPoints: [ slot('jet-engine-turbojet', 'casing', 'engine-casing', 'Cutaway casing GLB', 'turbojet-casing', 'Use a removable upper quadrant to preserve educational cutaway visibility.'), slot('jet-engine-turbojet', 'compressor', 'compressor-stages', 'Compressor stages GLB', 'turbojet-compressor', 'Blade rows may be instanced; axis must align with local X.'), slot('jet-engine-turbojet', 'combustor', 'combustion-chamber', 'Combustor cans GLB', 'turbojet-combustor', 'Expose each combustor can for guided tour highlighting.'), slot('jet-engine-turbojet', 'turbine-nozzle', 'turbine-stages', 'Turbine and nozzle GLB', 'turbojet-turbine', 'High-temperature material assignments should remain separate from casing.'), ], thumbnail: thumb(['compressor-stages', 'combustion-chamber', 'turbine-stages'], 'section'), performanceNotes: [ 'Compressor/turbine blade rows are intended for instanced rendering with a single angular phase uniform.', 'Hot gas glow is a single translucent core volume to limit overdraw.', ], }, 'turbofan-engine': { machineId: 'turbofan-engine', title: 'Turbofan Engine', category: 'engines', summary: 'A high-bypass turbofan cutaway with large fan, bypass duct, splitter, core compressor, combustor, turbines, nozzle, and separate bypass/core flow visualization.', layout: 'sectioned', scaleMeters: 2.8, anchorPartId: 'fan-casing', animationProfileId: 'turbofan-engine', guidedTourStageIds: ['overview', 'fan', 'bypass-flow', 'core-flow', 'combustor', 'turbines'], nodes: [ { id: 'turbofan-root', kind: 'group', children: [ { id: 'turbofan-nacelle', kind: 'hollow-cylinder', partId: 'fan-casing', label: 'Fan nacelle and casing', material: 'transparent-shell', transform: { position: [-0.25, 0, 0], rotation: [0, 0, 1.5708], scale: [0.92, 2.35, 0.92] }, dimensions: { bypassDuct: true, sectionRemovedDegrees: 110 }, explodedOffset: [0, 0.5, 0], }, { id: 'turbofan-fan', kind: 'fan-stage', partId: 'fan', label: 'Large bypass fan', material: 'polished-steel', transform: { position: [-1.12, 0, 0], rotation: [0, 0, 1.5708], scale: [0.78, 0.18, 0.78] }, dimensions: { blades: 24, wideChord: true }, motionBindings: [rotate('fan-spool', 'x', 1.15, 0, 'Fan spool rotates slower than core spool.')], }, { id: 'turbofan-splitter', kind: 'hollow-cylinder', partId: 'flow-splitter', label: 'Bypass/core splitter', material: 'aluminium', transform: { position: [-0.78, 0, 0], rotation: [0, 0, 1.5708], scale: [0.52, 0.22, 0.52] }, }, { id: 'turbofan-core-compressor', kind: 'compressor-stage', partId: 'core-compressor', label: 'Core compressor', material: 'brushed-steel', transform: { position: [-0.2, 0, 0], rotation: [0, 0, 1.5708], scale: [0.34, 0.68, 0.34] }, dimensions: { stages: 9 }, motionBindings: [rotate('core-spool', 'x', 2.4, 0, 'Core spool rotates faster than fan spool.')], }, { id: 'turbofan-combustor', kind: 'fluid-volume', partId: 'combustion-chamber', label: 'Annular combustor', material: 'hot-gas', transform: { position: [0.45, 0, 0], rotation: [0, 0, 1.5708], scale: [0.36, 0.44, 0.36] }, motionBindings: [custom('core-spool', 'heat-pulse', 'Combustor glow is almost continuous at operating speed.')], }, { id: 'turbofan-turbine', kind: 'turbine-stage', partId: 'turbine-stages', label: 'High/low pressure turbines', material: 'polished-steel', transform: { position: [0.98, 0, 0], rotation: [0, 0, 1.5708], scale: [0.34, 0.55, 0.34] }, dimensions: { stages: 4, nestedSpools: true }, motionBindings: [rotate('core-spool', 'x', 2.4)], }, { id: 'turbofan-bypass-flow', kind: 'flow-arrow', partId: 'bypass-flow', label: 'Bypass airflow', material: 'intake-air', transform: { position: [-0.05, 0.58, 0], rotation: [0, 0, 1.5708], scale: [1.82, 0.08, 0.08] }, dimensions: { annular: true }, motionBindings: [flowPulse('fan-spool', 0, 'Most thrust comes from bypass air in a high-bypass turbofan.')], }, { id: 'turbofan-core-flow', kind: 'flow-arrow', partId: 'core-flow', label: 'Hot core flow', material: 'hot-gas', transform: { position: [0.22, 0, 0], rotation: [0, 0, 1.5708], scale: [1.45, 0.07, 0.07] }, motionBindings: [flowPulse('core-spool', 0.2, 'Core flow powers the turbines and exits through the core nozzle.')], }, ], }, ], labels: [ label('turbofan-fan-label', 'fan', 'Large fan accelerates bypass air', 'turbofan-fan', [-1.2, 0.82, 0.2], 5), label('turbofan-bypass-label', 'bypass-flow', 'Bypass duct carries most mass flow', 'turbofan-bypass-flow', [0.12, 0.88, 0.18], 5), label('turbofan-core-label', 'core-compressor', 'Core behaves like a compact turbojet', 'turbofan-core-compressor', [-0.12, 0.42, 0.16], 4), ], replacementPoints: [ slot('turbofan-engine', 'nacelle', 'fan-casing', 'Nacelle and bypass duct GLB', 'turbofan-nacelle', 'Provide separate outer nacelle and inner duct meshes for exploded views.'), slot('turbofan-engine', 'fan', 'fan', 'Fan stage GLB', 'turbofan-fan', 'Fan rotates around local X; blades may be a single mesh or instanced.'), slot('turbofan-engine', 'core', 'core-compressor', 'Core compressor and combustor GLB', 'turbofan-core-compressor', 'Keep core spool axis coincident with fan axis.'), slot('turbofan-engine', 'turbines', 'turbine-stages', 'Turbine stages GLB', 'turbofan-turbine', 'Separate HP and LP turbine groups if available.'), ], thumbnail: thumb(['fan', 'bypass-flow', 'core-compressor'], 'section'), performanceNotes: [ 'Fan and core spools use separate time multipliers to explain multi-spool operation.', 'Bypass flow is represented as sparse annular ribbons instead of volumetric fog for mobile readability.', ], }, 'planetary-gearbox': { machineId: 'planetary-gearbox', title: 'Planetary Gearbox', category: 'gearboxes-drives', summary: 'A selectable epicyclic gear train with sun gear, planet gears, carrier, ring gear, input/output shafts, brake band, and mode-dependent rotation ratios.', layout: 'exploded', scaleMeters: 1, anchorPartId: 'ring-gear', animationProfileId: 'planetary-gearbox', guidedTourStageIds: ['overview', 'sun-gear', 'planet-carrier', 'ring-gear', 'ratio-modes'], nodes: [ { id: 'planetary-root', kind: 'group', children: [ { id: 'planetary-ring', kind: 'ring-gear', partId: 'ring-gear', label: 'Internal ring gear', material: 'dark-cast-iron', transform: { position: [0, 0, 0], rotation: [1.5708, 0, 0], scale: [0.82, 0.14, 0.82] }, dimensions: { teeth: 72, internalTeeth: true }, motionBindings: [custom('planetary-mode', 'ratio-change', 'Ring may be held, driven, or used as output depending on selected mode.')], explodedOffset: [0, 0, -0.32], }, { id: 'planetary-sun', kind: 'gear', partId: 'sun-gear', label: 'Sun gear', material: 'brass', transform: { position: [0, 0, 0], rotation: [1.5708, 0, 0], scale: [0.28, 0.16, 0.28] }, dimensions: { teeth: 24 }, motionBindings: [rotate('planetary-mode', 'z', 1)], }, { id: 'planetary-planets', kind: 'gear', partId: 'planet-gears', label: 'Planet gears', material: 'brushed-steel', transform: { position: [0, 0, 0], rotation: [1.5708, 0, 0], scale: [0.2, 0.14, 0.2] }, dimensions: { count: 3, teeth: 24, orbitRadius: 0.46 }, motionBindings: [ custom('planetary-mode', 'planetary-orbit', 'Planet gears spin on their pins while orbiting with the carrier.'), ], }, { id: 'planetary-carrier', kind: 'disc', partId: 'planet-carrier', label: 'Planet carrier', material: 'aluminium', transform: { position: [0, 0, -0.08], rotation: [1.5708, 0, 0], scale: [0.62, 0.08, 0.62] }, dimensions: { arms: 3 }, motionBindings: [custom('planetary-mode', 'ratio-change', 'Carrier speed is computed from selected input/output constraints.')], }, { id: 'planetary-input-shaft', kind: 'shaft', partId: 'input-shaft', label: 'Input shaft', material: 'polished-steel', transform: { position: [0, 0, -0.55], rotation: [1.5708, 0, 0], scale: [0.1, 0.58, 0.1] }, motionBindings: [rotate('planetary-mode', 'z', 1)], }, { id: 'planetary-output-shaft', kind: 'shaft', partId: 'output-shaft', label: 'Output shaft', material: 'polished-steel', transform: { position: [0, 0, 0.55], rotation: [1.5708, 0, 0], scale: [0.1, 0.58, 0.1] }, motionBindings: [custom('planetary-mode', 'ratio-change', 'Output speed follows the active mode selection.')], }, { id: 'planetary-brake-band', kind: 'belt', partId: 'brake-band', label: 'Brake band / held member', material: 'carbon-friction', transform: { position: [0, 0.05, 0], rotation: [1.5708, 0, 0], scale: [0.9, 0.05, 0.9] }, motionBindings: [custom('planetary-mode', 'visibility-pulse', 'Highlights when the ring is held stationary.')], }, ], }, ], labels: [ label('planetary-sun-label', 'sun-gear', 'Sun gear commonly acts as input', 'planetary-sun', [0.18, 0.38, 0.18], 5), label('planetary-planets-label', 'planet-gears', 'Planets both spin and orbit', 'planetary-planets', [0.58, 0.34, 0.18], 5), label('planetary-ring-label', 'ring-gear', 'Internal ring gear sets ratio', 'planetary-ring', [-0.62, 0.5, 0.18], 4), label('planetary-carrier-label', 'planet-carrier', 'Carrier combines planet pin motion', 'planetary-carrier', [0.0, -0.52, 0.2], 4), ], replacementPoints: [ slot('planetary-gearbox', 'ring-gear', 'ring-gear', 'Ring gear GLB', 'planetary-ring', 'Internal teeth face inward; origin centered on gearbox axis.'), slot('planetary-gearbox', 'sun-gear', 'sun-gear', 'Sun gear GLB', 'planetary-sun', 'Use local Z as rotational axis for consistency with procedural gear renderer.'), slot('planetary-gearbox', 'planet-set', 'planet-gears', 'Planet gears GLB', 'planetary-planets', 'Provide one planet mesh plus instance transforms, or three named planet children.'), slot('planetary-gearbox', 'carrier', 'planet-carrier', 'Planet carrier GLB', 'planetary-carrier', 'Carrier pin origins should match orbit radius metadata.'), ], thumbnail: thumb(['sun-gear', 'planet-gears', 'ring-gear'], 'exploded'), performanceNotes: [ 'Gear teeth are generated parametrically and can be simplified to cylinders below catalogue-card size.', 'Selectable input/output modes only alter transform equations, not geometry.', ], }, 'differential-gear': { machineId: 'differential-gear', title: 'Differential Gear', category: 'gearboxes-drives', summary: 'An open bevel-gear differential showing pinion and crown gear input, rotating carrier, spider gears, side gears, axle shafts, and cornering speed split.', layout: 'cutaway', scaleMeters: 1.15, anchorPartId: 'differential-carrier', animationProfileId: 'differential-gear', guidedTourStageIds: ['overview', 'input-pinion', 'carrier', 'spider-gears', 'cornering'], nodes: [ { id: 'diff-root', kind: 'group', children: [ { id: 'diff-housing', kind: 'rounded-box', partId: 'housing', label: 'Cutaway axle housing', material: 'transparent-shell', transform: { position: [0, 0, 0], scale: [1.25, 0.68, 0.72] }, dimensions: { sectionRemoved: true }, explodedOffset: [0, 0.38, 0], }, { id: 'diff-pinion', kind: 'bevel-gear', partId: 'drive-pinion', label: 'Drive pinion', material: 'polished-steel', transform: { position: [0, -0.48, 0], rotation: [0, 0, 0], scale: [0.26, 0.24, 0.26] }, dimensions: { teeth: 12, spiral: true }, motionBindings: [rotate('differential-cycle', 'y', 3.2)], }, { id: 'diff-ring', kind: 'bevel-gear', partId: 'ring-gear', label: 'Crown / ring gear', material: 'brass', transform: { position: [0, 0, 0], rotation: [1.5708, 0, 0], scale: [0.58, 0.16, 0.58] }, dimensions: { teeth: 41, spiral: true }, motionBindings: [rotate('differential-cycle', 'x', 1)], }, { id: 'diff-carrier', kind: 'disc', partId: 'differential-carrier', label: 'Differential carrier', material: 'aluminium', transform: { position: [0, 0, 0], rotation: [1.5708, 0, 0], scale: [0.48, 0.34, 0.48] }, motionBindings: [rotate('differential-cycle', 'x', 1)], }, { id: 'diff-spider-gears', kind: 'bevel-gear', partId: 'spider-gears', label: 'Spider gears', material: 'brushed-steel', transform: { position: [0, 0, 0], scale: [0.22, 0.22, 0.22] }, dimensions: { count: 2, onCrossPin: true }, motionBindings: [custom('cornering-demo', 'ratio-change', 'Spider gears spin when left and right axle speeds differ.')], }, { id: 'diff-side-gears', kind: 'bevel-gear', partId: 'side-gears', label: 'Side gears', material: 'brushed-steel', transform: { position: [0, 0, 0], rotation: [0, 1.5708, 0], scale: [0.25, 0.18, 0.25] }, dimensions: { count: 2 }, motionBindings: [custom('cornering-demo', 'ratio-change', 'Side gears counter-rotate relative to carrier during cornering.')], }, { id: 'diff-left-axle', kind: 'shaft', partId: 'left-axle', label: 'Left axle shaft', material: 'polished-steel', transform: { position: [-0.72, 0, 0], rotation: [0, 0, 1.5708], scale: [0.09, 0.72, 0.09] }, motionBindings: [custom('cornering-demo', 'ratio-change', 'Outer wheel can rotate faster in cornering mode.')], }, { id: 'diff-right-axle', kind: 'shaft', partId: 'right-axle', label: 'Right axle shaft', material: 'polished-steel', transform: { position: [0.72, 0, 0], rotation: [0, 0, 1.5708], scale: [0.09, 0.72, 0.09] }, motionBindings: [custom('cornering-demo', 'ratio-change', 'Inner wheel can rotate slower in cornering mode.')], }, ], }, ], labels: [ label('diff-pinion-label', 'drive-pinion', 'Pinion turns the ring gear at 90°', 'diff-pinion', [0.18, -0.72, 0.18], 5), label('diff-spider-label', 'spider-gears', 'Spider gears allow wheel speed difference', 'diff-spider-gears', [0.2, 0.28, 0.24], 5), label('diff-axle-label', 'left-axle', 'Axle shafts receive averaged torque', 'diff-left-axle', [-0.92, 0.22, 0.16], 4), ], replacementPoints: [ slot('differential-gear', 'housing', 'housing', 'Differential housing GLB', 'diff-housing', 'Provide cutaway cover as separate mesh for opacity/explosion.'), slot('differential-gear', 'ring-pinion', 'ring-gear', 'Ring and pinion GLB', 'diff-ring', 'Pinion axis must be perpendicular to axle axis.'), slot('differential-gear', 'carrier-spiders', 'differential-carrier', 'Carrier and spider gear GLB', 'diff-carrier', 'Spider gear pivots should be addressable for cornering animation.'), slot('differential-gear', 'axles', 'left-axle', 'Axle shafts and side gears GLB', 'diff-left-axle', 'Left/right axle groups must remain individually selectable.'), ], thumbnail: thumb(['ring-gear', 'spider-gears', 'left-axle'], 'section'), performanceNotes: [ 'Cornering demonstration changes left/right axle angular velocity from a single slider state.', 'Gear meshes are low-tooth-count enough to remain readable in a cutaway scene.', ], }, 'manual-gearbox-5-speed': { machineId: 'manual-gearbox-5-speed', title: 'Manual Gearbox (5-speed)', category: 'gearboxes-drives', summary: 'A five-speed constant-mesh manual transmission showing input shaft, layshaft, output shaft, gear pairs, synchromesh sleeves, selector forks, shift rails, and clutch input.', layout: 'exploded', scaleMeters: 1.45, anchorPartId: 'gearbox-casing', animationProfileId: 'manual-gearbox-5-speed', guidedTourStageIds: ['overview', 'constant-mesh', 'synchromesh', 'selector-forks', 'gear-ratios'], nodes: [ { id: 'manual-root', kind: 'group', children: [ { id: 'manual-casing', kind: 'rounded-box', partId: 'gearbox-casing', label: 'Sectioned gearbox casing', material: 'transparent-shell', transform: { position: [0, 0, 0], scale: [1.55, 0.72, 0.62] }, dimensions: { removableTop: true }, explodedOffset: [0, 0.45, 0], }, { id: 'manual-input-shaft', kind: 'shaft', partId: 'input-shaft', label: 'Input shaft', material: 'polished-steel', transform: { position: [-0.62, 0.15, 0], rotation: [0, 0, 1.5708], scale: [0.08, 0.5, 0.08] }, motionBindings: [rotate('gearbox-input', 'x', 1)], }, { id: 'manual-layshaft', kind: 'shaft', partId: 'layshaft', label: 'Layshaft', material: 'polished-steel', transform: { position: [0, -0.18, 0], rotation: [0, 0, 1.5708], scale: [0.08, 1.25, 0.08] }, motionBindings: [custom('selected-gear', 'ratio-change', 'Layshaft speed is always tied to input through the primary gear pair.')], }, { id: 'manual-output-shaft', kind: 'shaft', partId: 'output-shaft', label: 'Output shaft', material: 'polished-steel', transform: { position: [0.2, 0.15, 0], rotation: [0, 0, 1.5708], scale: [0.08, 1.25, 0.08] }, motionBindings: [custom('selected-gear', 'ratio-change', 'Output speed depends on engaged gear pair.')], }, { id: 'manual-gearsets', kind: 'gear', partId: 'gear-pairs', label: 'Five forward gear pairs', material: 'brushed-steel', transform: { position: [0.05, 0, 0], rotation: [1.5708, 0, 0], scale: [0.2, 0.12, 0.2] }, dimensions: { count: 5, ratios: [3.4, 2.1, 1.4, 1, 0.82], spacing: 0.22 }, motionBindings: [custom('selected-gear', 'ratio-change', 'All gears mesh continuously; only selected gear locks to output shaft.')], }, { id: 'manual-synchros', kind: 'disc', partId: 'synchromesh-sleeves', label: 'Synchromesh sleeves', material: 'brass', transform: { position: [0.18, 0.16, 0], rotation: [1.5708, 0, 0], scale: [0.24, 0.08, 0.24] }, dimensions: { count: 3, dogTeeth: true }, motionBindings: [custom('shift-action', 'reciprocating-translation', 'Selected sleeve slides along output shaft to lock a gear.')], }, { id: 'manual-selector-forks', kind: 'link', partId: 'selector-forks', label: 'Selector forks', material: 'aluminium', transform: { position: [0.18, 0.42, 0], scale: [1.1, 0.12, 0.12] }, dimensions: { forks: 3 }, motionBindings: [custom('shift-action', 'reciprocating-translation', 'Forks move one synchronizer sleeve at a time.')], }, { id: 'manual-clutch', kind: 'disc', partId: 'clutch', label: 'Clutch input', material: 'carbon-friction', transform: { position: [-0.9, 0.15, 0], rotation: [1.5708, 0, 0], scale: [0.32, 0.08, 0.32] }, motionBindings: [rotate('gearbox-input', 'x', 1)], }, ], }, ], labels: [ label('manual-constant-label', 'gear-pairs', 'All gear pairs remain in mesh', 'manual-gearsets', [0.06, -0.5, 0.22], 5), label('manual-synchro-label', 'synchromesh-sleeves', 'Synchronizer locks one free gear to shaft', 'manual-synchros', [0.42, 0.38, 0.2], 5), label('manual-fork-label', 'selector-forks', 'Selector forks slide sleeves', 'manual-selector-forks', [0.5, 0.62, 0.18], 4), ], replacementPoints: [ slot('manual-gearbox-5-speed', 'casing', 'gearbox-casing', 'Gearbox casing GLB', 'manual-casing', 'Top cover should remain separate for cutaway and exploded modes.'), slot('manual-gearbox-5-speed', 'shafts-gears', 'gear-pairs', 'Shafts and gearsets GLB', 'manual-gearsets', 'Provide gear pair IDs gear-1 through gear-5 for ratio highlighting.'), slot('manual-gearbox-5-speed', 'synchros', 'synchromesh-sleeves', 'Synchronizer sleeves GLB', 'manual-synchros', 'Sleeve pivots should align along output shaft axis.'), slot('manual-gearbox-5-speed', 'selector', 'selector-forks', 'Selector rail and forks GLB', 'manual-selector-forks', 'Forks must be independently visible for guided tour.'), ], thumbnail: thumb(['gear-pairs', 'synchromesh-sleeves', 'selector-forks'], 'exploded'), performanceNotes: [ 'Five gear pairs share parametric gear generation with different radii and ratios.', 'Only the active gear pair receives accent material, minimizing animated material changes.', ], }, cvt: { machineId: 'cvt', title: 'CVT (Continuously Variable Transmission)', category: 'gearboxes-drives', summary: 'A belt-and-variable-pulley continuously variable transmission showing movable sheaves, steel belt, input/output shafts, ratio change positions, and actuator pressure.', layout: 'hybrid', scaleMeters: 1.25, anchorPartId: 'belt', animationProfileId: 'cvt', guidedTourStageIds: ['overview', 'drive-pulley', 'driven-pulley', 'belt-radius', 'ratio-change'], nodes: [ { id: 'cvt-root', kind: 'group', children: [ { id: 'cvt-input-shaft', kind: 'shaft', partId: 'input-shaft', label: 'Input shaft', material: 'polished-steel', transform: { position: [-0.48, 0, 0], rotation: [1.5708, 0, 0], scale: [0.08, 0.72, 0.08] }, motionBindings: [rotate('cvt-cycle', 'z', 1.4)], }, { id: 'cvt-output-shaft', kind: 'shaft', partId: 'output-shaft', label: 'Output shaft', material: 'polished-steel', transform: { position: [0.48, 0, 0], rotation: [1.5708, 0, 0], scale: [0.08, 0.72, 0.08] }, motionBindings: [custom('cvt-ratio', 'ratio-change', 'Output speed follows effective pulley radius ratio.')], }, { id: 'cvt-drive-pulley', kind: 'disc', partId: 'drive-pulley', label: 'Variable drive pulley', material: 'aluminium', transform: { position: [-0.48, 0, 0], rotation: [1.5708, 0, 0], scale: [0.36, 0.18, 0.36] }, dimensions: { splitSheave: true, minRadius: 0.18, maxRadius: 0.36 }, motionBindings: [custom('cvt-ratio', 'ratio-change', 'Movable sheave changes belt running radius.')], }, { id: 'cvt-driven-pulley', kind: 'disc', partId: 'driven-pulley', label: 'Variable driven pulley', material: 'aluminium', transform: { position: [0.48, 0, 0], rotation: [1.5708, 0, 0], scale: [0.36, 0.18, 0.36] }, dimensions: { splitSheave: true, inverseRadius: true }, motionBindings: [custom('cvt-ratio', 'ratio-change', 'Driven pulley changes opposite to drive pulley to maintain belt length.')], }, { id: 'cvt-belt', kind: 'belt', partId: 'belt', label: 'Steel push belt', material: 'brushed-steel', transform: { position: [0, 0, 0], scale: [1.18, 0.08, 0.52] }, dimensions: { path: 'variable-ellipse', segmentCount: 96 }, motionBindings: [custom('cvt-cycle', 'ratio-change', 'Belt segment speed remains continuous as pitch radius changes.')], }, { id: 'cvt-actuators', kind: 'pressure-gauge', partId: 'hydraulic-actuators', label: 'Hydraulic ratio actuators', material: 'hydraulic-fluid', transform: { position: [0, 0.42, 0], scale: [1.05, 0.12, 0.12] }, motionBindings: [custom('cvt-ratio', 'pressure-pulse', 'Hydraulic pressure clamps belt and controls sheave position.')], }, ], }, ], labels: [ label('cvt-belt-label', 'belt', 'Belt rides at changing effective radius', 'cvt-belt', [0, -0.38, 0.22], 5), label('cvt-drive-label', 'drive-pulley', 'Drive sheaves squeeze inward for higher radius', 'cvt-drive-pulley', [-0.72, 0.34, 0.18], 5), label('cvt-actuator-label', 'hydraulic-actuators', 'Actuator pressure controls ratio', 'cvt-actuators', [0.18, 0.58, 0.16], 4), ], replacementPoints: [ slot('cvt', 'pulleys', 'drive-pulley', 'Variable pulleys GLB', 'cvt-drive-pulley', 'Model each pulley as fixed and sliding sheave groups.'), slot('cvt', 'belt', 'belt', 'Segmented steel belt GLB', 'cvt-belt', 'A static belt mesh can replace procedural path; animated belt segments remain optional.'), slot('cvt', 'actuators', 'hydraulic-actuators', 'Hydraulic actuator GLB', 'cvt-actuators', 'Actuators should align along sheave sliding axes.'), ], thumbnail: thumb(['belt', 'drive-pulley', 'driven-pulley']), performanceNotes: [ 'The belt path is spline-based and recalculated only when ratio changes, not every frame at constant ratio.', 'Sheave positions are simple transforms driven by a normalized ratio parameter.', ], }, 'worm-gear-drive': { machineId: 'worm-gear-drive', title: 'Worm Gear Drive', category: 'gearboxes-drives', summary: 'A right-angle worm reducer showing screw worm, worm wheel, input/output shafts, bearing supports, high reduction ratio, and self-locking demonstration.', layout: 'cutaway', scaleMeters: 1, anchorPartId: 'worm-wheel', animationProfileId: 'worm-gear-drive', guidedTourStageIds: ['overview', 'worm-screw', 'worm-wheel', 'ratio', 'self-locking'], nodes: [ { id: 'worm-root', kind: 'group', children: [ { id: 'worm-screw', kind: 'worm', partId: 'worm-screw', label: 'Worm screw', material: 'polished-steel', transform: { position: [0, 0.28, 0], rotation: [0, 0, 1.5708], scale: [0.16, 0.9, 0.16] }, dimensions: { starts: 1, leadAngleDegrees: 6 }, motionBindings: [rotate('worm-cycle', 'x', 6, 0, 'Many worm revolutions produce one wheel revolution.')], }, { id: 'worm-wheel', kind: 'gear', partId: 'worm-wheel', label: 'Worm wheel', material: 'bronze', transform: { position: [0, -0.05, 0], rotation: [1.5708, 0, 0], scale: [0.52, 0.16, 0.52] }, dimensions: { teeth: 40, concaveTeeth: true }, motionBindings: [rotate('worm-cycle', 'z', 0.15)], }, { id: 'worm-input-shaft', kind: 'shaft', partId: 'input-shaft', label: 'Input shaft', material: 'brushed-steel', transform: { position: [0, 0.28, 0], rotation: [0, 0, 1.5708], scale: [0.08, 1.25, 0.08] }, motionBindings: [rotate('worm-cycle', 'x', 6)], }, { id: 'worm-output-shaft', kind: 'shaft', partId: 'output-shaft', label: 'Output shaft', material: 'brushed-steel', transform: { position: [0, -0.05, 0], rotation: [1.5708, 0, 0], scale: [0.1, 0.82, 0.1] }, motionBindings: [rotate('worm-cycle', 'z', 0.15)], }, { id: 'worm-bearings', kind: 'hollow-cylinder', partId: 'bearings', label: 'Bearing supports', material: 'aluminium', transform: { position: [0, 0.28, 0], rotation: [0, 0, 1.5708], scale: [0.24, 1.08, 0.24] }, dimensions: { count: 2 }, }, { id: 'worm-lock-indicator', kind: 'load-arrow', partId: 'self-locking-load', label: 'Self-locking load', material: 'warning-amber', transform: { position: [0.52, -0.05, 0], rotation: [0, 0, -1.5708], scale: [0.32, 0.08, 0.08] }, motionBindings: [custom('self-lock-demo', 'visibility-pulse', 'Reverse torque is shown as blocked when lead angle/friction make drive self-locking.')], }, ], }, ], labels: [ label('worm-screw-label', 'worm-screw', 'Single-start worm acts like one-tooth gear', 'worm-screw', [-0.44, 0.52, 0.16], 5), label('worm-wheel-label', 'worm-wheel', 'Wheel advances one tooth per worm turn', 'worm-wheel', [0.52, -0.2, 0.18], 5), label('worm-lock-label', 'self-locking-load', 'High friction angle can prevent back-driving', 'worm-lock-indicator', [0.78, 0.08, 0.16], 4), ], replacementPoints: [ slot('worm-gear-drive', 'worm-screw', 'worm-screw', 'Worm screw GLB', 'worm-screw', 'Worm axis should align with local X.'), slot('worm-gear-drive', 'worm-wheel', 'worm-wheel', 'Worm wheel GLB', 'worm-wheel', 'Wheel axis should align with local Z and mesh at tangent contact.'), slot('worm-gear-drive', 'housing-bearings', 'bearings', 'Housing and bearing supports GLB', 'worm-bearings', 'Supports are non-animated and may be combined.'), ], thumbnail: thumb(['worm-screw', 'worm-wheel', 'self-locking-load']), performanceNotes: [ 'Helical worm geometry can be reduced to a normal-mapped cylinder for catalogue thumbnails.', 'Self-locking mode is a state overlay, not a separate physical simulation.', ], }, 'bevel-gear-set': { machineId: 'bevel-gear-set', title: 'Bevel Gear Set', category: 'gearboxes-drives', summary: 'A right-angle bevel gear pair with pinion and crown gears, intersecting shafts, bearing blocks, spiral tooth hinting, and direction-of-power annotations.', layout: 'schematic', scaleMeters: 0.9, anchorPartId: 'crown-gear', animationProfileId: 'bevel-gear-set', guidedTourStageIds: ['overview', 'pinion', 'crown-gear', 'right-angle-drive', 'spiral-teeth'], nodes: [ { id: 'bevel-root', kind: 'group', children: [ { id: 'bevel-pinion', kind: 'bevel-gear', partId: 'pinion-gear', label: 'Bevel pinion', material: 'polished-steel', transform: { position: [-0.25, 0, 0], rotation: [0, 0, 1.5708], scale: [0.28, 0.22, 0.28] }, dimensions: { teeth: 18, spiral: true }, motionBindings: [rotate('bevel-cycle', 'x', 1.8)], }, { id: 'bevel-crown', kind: 'bevel-gear', partId: 'crown-gear', label: 'Driven bevel gear', material: 'brass', transform: { position: [0.1, 0, 0], rotation: [1.5708, 0, 0], scale: [0.42, 0.22, 0.42] }, dimensions: { teeth: 36, spiral: true }, motionBindings: [rotate('bevel-cycle', 'z', -0.9)], }, { id: 'bevel-input-shaft', kind: 'shaft', partId: 'input-shaft', label: 'Input shaft', material: 'brushed-steel', transform: { position: [-0.62, 0, 0], rotation: [0, 0, 1.5708], scale: [0.08, 0.72, 0.08] }, motionBindings: [rotate('bevel-cycle', 'x', 1.8)], }, { id: 'bevel-output-shaft', kind: 'shaft', partId: 'output-shaft', label: 'Output shaft', material: 'brushed-steel', transform: { position: [0.1, 0.42, 0], rotation: [1.5708, 0, 0], scale: [0.08, 0.72, 0.08] }, motionBindings: [rotate('bevel-cycle', 'z', -0.9)], }, { id: 'bevel-bearings', kind: 'rounded-box', partId: 'bearing-blocks', label: 'Bearing blocks', material: 'aluminium', transform: { position: [-0.24, -0.34, 0], scale: [0.95, 0.12, 0.28] }, dimensions: { count: 2 }, }, { id: 'bevel-power-arrow', kind: 'flow-arrow', partId: 'power-flow', label: 'Power turns through 90°', material: 'accent-blue', transform: { position: [-0.22, 0.22, 0], rotation: [0, 0, 0.72], scale: [0.52, 0.07, 0.07] }, motionBindings: [flowPulse('bevel-cycle', 0, 'Direction marker follows torque path through the mesh.')], }, ], }, ], labels: [ label('bevel-pinion-label', 'pinion-gear', 'Pinion drives intersecting gear axes', 'bevel-pinion', [-0.56, 0.26, 0.16], 5), label('bevel-crown-label', 'crown-gear', 'Larger gear halves output speed here', 'bevel-crown', [0.42, 0.18, 0.18], 5), label('bevel-power-label', 'power-flow', 'Torque changes direction by 90°', 'bevel-power-arrow', [0.02, 0.58, 0.14], 4), ], replacementPoints: [ slot('bevel-gear-set', 'pinion', 'pinion-gear', 'Bevel pinion GLB', 'bevel-pinion', 'Pitch cone apex should intersect driven gear pitch cone at machine origin.'), slot('bevel-gear-set', 'crown', 'crown-gear', 'Driven bevel gear GLB', 'bevel-crown', 'Local Z axis is output rotation axis.'), slot('bevel-gear-set', 'shafts-bearings', 'bearing-blocks', 'Shafts and bearing blocks GLB', 'bevel-bearings', 'Non-animated support geometry can be merged by material.'), ], thumbnail: thumb(['pinion-gear', 'crown-gear', 'power-flow']), performanceNotes: [ 'Spiral bevel tooth detail can be represented with normal maps when zoomed out.', 'The pair uses fixed ratio transforms, so no contact solver is required.', ], }, 'centrifugal-pump': { machineId: 'centrifugal-pump', title: 'Centrifugal Pump', category: 'pumps-fluid', summary: 'A volute centrifugal pump cutaway with impeller, inlet eye, outlet diffuser, shaft seal, velocity arrows, pressure gradient, and cavitation indicator.', layout: 'cutaway', scaleMeters: 1.15, anchorPartId: 'volute-casing', animationProfileId: 'centrifugal-pump', guidedTourStageIds: ['overview', 'inlet-eye', 'impeller', 'volute', 'cavitation'], nodes: [ { id: 'centrifugal-root', kind: 'group', children: [ { id: 'centrifugal-volute', kind: 'vane-wheel', partId: 'volute-casing', label: 'Volute casing', material: 'transparent-shell', transform: { position: [0, 0, 0], scale: [0.86, 0.66, 0.32] }, dimensions: { spiralVolute: true, sectioned: true }, explodedOffset: [0, 0.35, 0], }, { id: 'centrifugal-impeller', kind: 'impeller', partId: 'impeller', label: 'Curved-vane impeller', material: 'brushed-steel', transform: { position: [0, 0, 0], rotation: [1.5708, 0, 0], scale: [0.46, 0.12, 0.46] }, dimensions: { vanes: 7, backwardCurved: true }, motionBindings: [rotate('pump-cycle', 'z', 1.8)], }, { id: 'centrifugal-inlet', kind: 'pipe', partId: 'inlet', label: 'Axial inlet', material: 'aluminium', transform: { position: [0, 0, -0.45], rotation: [1.5708, 0, 0], scale: [0.24, 0.62, 0.24] }, }, { id: 'centrifugal-outlet', kind: 'pipe', partId: 'outlet', label: 'Tangential outlet', material: 'aluminium', transform: { position: [0.68, 0.28, 0], rotation: [0, 0, 0.2], scale: [0.22, 0.66, 0.22] }, }, { id: 'centrifugal-flow-in', kind: 'flow-arrow', partId: 'inlet-flow', label: 'Low-pressure inlet flow', material: 'hydraulic-fluid', transform: { position: [0, 0, -0.78], rotation: [1.5708, 0, 0], scale: [0.48, 0.06, 0.06] }, motionBindings: [flowPulse('pump-cycle', 0, 'Fluid enters through impeller eye.')], }, { id: 'centrifugal-flow-out', kind: 'flow-arrow', partId: 'outlet-flow', label: 'High-velocity outlet flow', material: 'accent-blue', transform: { position: [0.95, 0.38, 0], rotation: [0, 0, 0.2], scale: [0.52, 0.07, 0.07] }, motionBindings: [flowPulse('pump-cycle', 0.3, 'Volute converts velocity to pressure.')], }, { id: 'centrifugal-cavitation', kind: 'fluid-volume', partId: 'cavitation-indicator', label: 'Cavitation bubbles', material: 'warning-amber', transform: { position: [-0.12, 0, -0.12], scale: [0.28, 0.18, 0.28] }, dimensions: { bubbles: 32 }, motionBindings: [custom('cavitation-demo', 'visibility-pulse', 'Bubbles appear when inlet pressure falls below vapour pressure.')], }, ], }, ], labels: [ label('pump-impeller-label', 'impeller', 'Impeller adds kinetic energy to fluid', 'centrifugal-impeller', [0.28, 0.38, 0.16], 5), label('pump-volute-label', 'volute-casing', 'Volute gradually diffuses flow', 'centrifugal-volute', [-0.58, 0.42, 0.16], 4), label('pump-cavitation-label', 'cavitation-indicator', 'Cavitation warns of low inlet pressure', 'centrifugal-cavitation', [-0.42, -0.28, 0.16], 4), ], replacementPoints: [ slot('centrifugal-pump', 'volute-casing', 'volute-casing', 'Volute casing GLB', 'centrifugal-volute', 'Provide a removable front cover for cutaway mode.'), slot('centrifugal-pump', 'impeller', 'impeller', 'Impeller GLB', 'centrifugal-impeller', 'Impeller local Z is rotation axis; eye centered on inlet.'), slot('centrifugal-pump', 'pipes-seal', 'inlet', 'Inlet/outlet pipes and shaft seal GLB', 'centrifugal-inlet', 'Keep inlet and outlet as separate part IDs for flow highlighting.'), ], thumbnail: thumb(['impeller', 'volute-casing', 'cavitation-indicator'], 'section'), performanceNotes: [ 'Impeller can be a single mesh with shader streaks rather than fully animated individual fluid particles.', 'Cavitation indicator is optional and disabled unless the demo state requests it.', ], }, 'gear-pump': { machineId: 'gear-pump', title: 'Gear Pump', category: 'pumps-fluid', summary: 'A positive-displacement external gear pump showing meshing gears, trapped fluid pockets, inlet/outlet ports, housing clearances, and displacement per revolution.', layout: 'cutaway', scaleMeters: 0.95, anchorPartId: 'pump-housing', animationProfileId: 'gear-pump', guidedTourStageIds: ['overview', 'gear-mesh', 'inlet', 'trapped-volume', 'outlet'], nodes: [ { id: 'gear-pump-root', kind: 'group', children: [ { id: 'gear-pump-housing', kind: 'rounded-box', partId: 'pump-housing', label: 'Precision pump housing', material: 'transparent-shell', transform: { position: [0, 0, 0], scale: [1.08, 0.68, 0.32] }, dimensions: { closeClearance: true, sectioned: true }, explodedOffset: [0, 0.35, 0], }, { id: 'gear-pump-drive-gear', kind: 'gear', partId: 'drive-gear', label: 'Drive gear', material: 'polished-steel', transform: { position: [-0.24, 0, 0], rotation: [1.5708, 0, 0], scale: [0.28, 0.12, 0.28] }, dimensions: { teeth: 14 }, motionBindings: [rotate('gear-pump-cycle', 'z', 1)], }, { id: 'gear-pump-idler-gear', kind: 'gear', partId: 'idler-gear', label: 'Idler gear', material: 'brushed-steel', transform: { position: [0.24, 0, 0], rotation: [1.5708, 0, 0], scale: [0.28, 0.12, 0.28] }, dimensions: { teeth: 14 }, motionBindings: [rotate('gear-pump-cycle', 'z', -1)], }, { id: 'gear-pump-inlet', kind: 'pipe', partId: 'inlet-port', label: 'Inlet port', material: 'aluminium', transform: { position: [0, -0.42, 0], scale: [0.22, 0.42, 0.22] }, }, { id: 'gear-pump-outlet', kind: 'pipe', partId: 'outlet-port', label: 'Outlet port', material: 'aluminium', transform: { position: [0, 0.42, 0], scale: [0.22, 0.42, 0.22] }, }, { id: 'gear-pump-pockets', kind: 'fluid-volume', partId: 'fluid-pockets', label: 'Trapped fluid pockets', material: 'hydraulic-fluid', transform: { position: [0, 0, 0.08], scale: [0.86, 0.5, 0.08] }, dimensions: { pocketsPerGear: 7 }, motionBindings: [custom('gear-pump-cycle', 'custom-linkage', 'Fluid pockets move around outside of gears, not through the mesh centre.')], }, { id: 'gear-pump-displacement', kind: 'pressure-gauge', partId: 'displacement-meter', label: 'Displacement per revolution', material: 'accent-blue', transform: { position: [0.62, 0, 0], scale: [0.22, 0.22, 0.08] }, motionBindings: [custom('gear-pump-cycle', 'pressure-pulse', 'Meter increments once per complete pocket delivery set.')], }, ], }, ], labels: [ label('gear-pump-mesh-label', 'drive-gear', 'Meshing teeth seal the high side from low side', 'gear-pump-drive-gear', [-0.42, 0.32, 0.16], 5), label('gear-pump-pocket-label', 'fluid-pockets', 'Fluid is carried around outside gear teeth', 'gear-pump-pockets', [0, -0.55, 0.16], 5), label('gear-pump-outlet-label', 'outlet-port', 'Outlet receives positive displacement flow', 'gear-pump-outlet', [0.28, 0.62, 0.16], 4), ], replacementPoints: [ slot('gear-pump', 'housing', 'pump-housing', 'Gear pump housing GLB', 'gear-pump-housing', 'Include close-clearance cavity and removable cover.'), slot('gear-pump', 'gear-pair', 'drive-gear', 'Meshing gear pair GLB', 'gear-pump-drive-gear', 'Drive and idler gear origins should be at their shaft centres.'), slot('gear-pump', 'ports', 'inlet-port', 'Inlet and outlet port GLB', 'gear-pump-inlet', 'Keep port part IDs separate for flow direction overlays.'), ], thumbnail: thumb(['drive-gear', 'idler-gear', 'fluid-pockets'], 'section'), performanceNotes: [ 'Fluid pockets are small instanced capsule/arc meshes keyed to gear phase.', 'The housing cutaway is static and can share material with piston pump housings.', ], }, 'piston-pump': { machineId: 'piston-pump', title: 'Piston Pump', category: 'pumps-fluid', summary: 'A reciprocating piston pump with plunger, crank drive, inlet and outlet check valves, suction/discharge manifolds, and pressure pulsation display.', layout: 'cutaway', scaleMeters: 1.05, anchorPartId: 'pump-cylinder', animationProfileId: 'piston-pump', guidedTourStageIds: ['overview', 'suction-stroke', 'discharge-stroke', 'check-valves', 'pressure-pulse'], nodes: [ { id: 'piston-pump-root', kind: 'group', children: [ { id: 'piston-pump-cylinder', kind: 'hollow-cylinder', partId: 'pump-cylinder', label: 'Pump cylinder', material: 'transparent-shell', transform: { position: [-0.25, 0, 0], rotation: [0, 0, 1.5708], scale: [0.28, 0.68, 0.28] }, explodedOffset: [-0.3, 0, 0], }, { id: 'piston-pump-plunger', kind: 'cylinder', partId: 'plunger', label: 'Reciprocating plunger', material: 'polished-steel', transform: { position: [-0.2, 0, 0], rotation: [0, 0, 1.5708], scale: [0.2, 0.32, 0.2] }, motionBindings: [oscillate('piston-pump-cycle', 'x', 0.28, 0)], }, { id: 'piston-pump-rod', kind: 'link', partId: 'connecting-rod', label: 'Crank connecting rod', material: 'brushed-steel', transform: { position: [0.28, 0, 0], scale: [0.08, 0.68, 0.08] }, motionBindings: [custom('piston-pump-cycle', 'custom-linkage', 'Rod converts crank rotation to plunger reciprocation.')], }, { id: 'piston-pump-crank', kind: 'disc', partId: 'crank', label: 'Crank wheel', material: 'brushed-steel', transform: { position: [0.62, 0, 0], rotation: [1.5708, 0, 0], scale: [0.28, 0.08, 0.28] }, motionBindings: [rotate('piston-pump-cycle', 'z')], }, { id: 'piston-pump-inlet-valve', kind: 'valve', partId: 'inlet-check-valve', label: 'Inlet check valve', material: 'bronze', transform: { position: [-0.55, -0.26, 0], scale: [0.12, 0.16, 0.12] }, motionBindings: [timedLift('piston-pump-cycle', 'y', 0.07, 0.08, 'Opens during suction stroke.')], }, { id: 'piston-pump-outlet-valve', kind: 'valve', partId: 'outlet-check-valve', label: 'Outlet check valve', material: 'bronze', transform: { position: [-0.55, 0.26, 0], scale: [0.12, 0.16, 0.12] }, motionBindings: [timedLift('piston-pump-cycle', 'y', 0.07, 0.58, 'Opens during discharge stroke.')], }, { id: 'piston-pump-fluid', kind: 'fluid-volume', partId: 'pump-chamber-fluid', label: 'Working chamber fluid', material: 'hydraulic-fluid', transform: { position: [-0.43, 0, 0], scale: [0.22, 0.28, 0.22] }, motionBindings: [custom('piston-pump-cycle', 'pressure-pulse', 'Chamber volume and pressure alternate each half-cycle.')], }, { id: 'piston-pump-pressure', kind: 'pressure-gauge', partId: 'pressure-indicator', label: 'Pressure pulsation', material: 'accent-blue', transform: { position: [-0.92, 0.1, 0], scale: [0.22, 0.22, 0.08] }, motionBindings: [custom('piston-pump-cycle', 'pressure-pulse', 'Gauge needle rises on discharge stroke.')], }, ], }, ], labels: [ label('piston-pump-plunger-label', 'plunger', 'Plunger changes chamber volume', 'piston-pump-plunger', [-0.14, 0.32, 0.16], 5), label('piston-pump-valves-label', 'inlet-check-valve', 'Check valves enforce one-way flow', 'piston-pump-inlet-valve', [-0.78, -0.38, 0.16], 5), label('piston-pump-pressure-label', 'pressure-indicator', 'Output pressure pulsates each stroke', 'piston-pump-pressure', [-1.08, 0.32, 0.16], 4), ], replacementPoints: [ slot('piston-pump', 'cylinder', 'pump-cylinder', 'Pump cylinder GLB', 'piston-pump-cylinder', 'Cylinder axis should align local X for plunger stroke.'), slot('piston-pump', 'crank-drive', 'crank', 'Crank drive GLB', 'piston-pump-crank', 'Crank pivot local Z; connecting rod may remain procedural if desired.'), slot('piston-pump', 'check-valves', 'inlet-check-valve', 'Check valve assembly GLB', 'piston-pump-inlet-valve', 'Inlet and outlet valves must be distinct selectable groups.'), ], thumbnail: thumb(['plunger', 'inlet-check-valve', 'outlet-check-valve']), performanceNotes: [ 'Valve lift is phase-gated from pressure differential, represented analytically for clean stepping.', 'Fluid chamber is a single scalable volume mesh to avoid particle churn.', ], }, 'hydraulic-cylinder': { machineId: 'hydraulic-cylinder', title: 'Hydraulic Cylinder', category: 'pumps-fluid', summary: 'A double-acting hydraulic cylinder with barrel, piston, rod, seals, ports A/B, extend/retract fluid volumes, pressure indicator, and clevis mounts.', layout: 'sectioned', scaleMeters: 1.35, anchorPartId: 'cylinder-barrel', animationProfileId: 'hydraulic-cylinder', guidedTourStageIds: ['overview', 'extend', 'retract', 'seals', 'pressure'], nodes: [ { id: 'hydraulic-root', kind: 'group', children: [ { id: 'hydraulic-barrel', kind: 'hollow-cylinder', partId: 'cylinder-barrel', label: 'Sectioned cylinder barrel', material: 'transparent-shell', transform: { position: [0, 0, 0], rotation: [0, 0, 1.5708], scale: [0.26, 1.05, 0.26] }, explodedOffset: [0, 0.34, 0], }, { id: 'hydraulic-piston', kind: 'cylinder', partId: 'piston', label: 'Internal piston', material: 'aluminium', transform: { position: [0.08, 0, 0], rotation: [0, 0, 1.5708], scale: [0.23, 0.12, 0.23] }, motionBindings: [oscillate('hydraulic-stroke', 'x', 0.42, 0)], }, { id: 'hydraulic-rod', kind: 'shaft', partId: 'rod', label: 'Chrome piston rod', material: 'polished-steel', transform: { position: [0.45, 0, 0], rotation: [0, 0, 1.5708], scale: [0.1, 0.88, 0.1] }, motionBindings: [oscillate('hydraulic-stroke', 'x', 0.42, 0)], }, { id: 'hydraulic-seals', kind: 'torus', partId: 'seals', label: 'Rod and piston seals', material: 'rubber', transform: { position: [0.12, 0, 0], rotation: [1.5708, 0, 0], scale: [0.24, 0.04, 0.24] }, dimensions: { count: 3 }, motionBindings: [oscillate('hydraulic-stroke', 'x', 0.42, 0)], }, { id: 'hydraulic-port-a', kind: 'pipe', partId: 'port-a', label: 'Cap-end port A', material: 'aluminium', transform: { position: [-0.42, 0.28, 0], scale: [0.12, 0.34, 0.12] }, }, { id: 'hydraulic-port-b', kind: 'pipe', partId: 'port-b', label: 'Rod-end port B', material: 'aluminium', transform: { position: [0.46, 0.28, 0], scale: [0.12, 0.34, 0.12] }, }, { id: 'hydraulic-fluid-a', kind: 'fluid-volume', partId: 'cap-end-fluid', label: 'Cap-end fluid volume', material: 'hydraulic-fluid', transform: { position: [-0.22, 0, 0], rotation: [0, 0, 1.5708], scale: [0.22, 0.42, 0.22] }, motionBindings: [custom('hydraulic-stroke', 'pressure-pulse', 'High pressure here extends the rod.')], }, { id: 'hydraulic-fluid-b', kind: 'fluid-volume', partId: 'rod-end-fluid', label: 'Rod-end fluid volume', material: 'coolant', transform: { position: [0.42, 0, 0], rotation: [0, 0, 1.5708], scale: [0.2, 0.36, 0.2] }, motionBindings: [custom('hydraulic-stroke', 'pressure-pulse', 'High pressure here retracts the rod.', 0.5)], }, { id: 'hydraulic-pressure', kind: 'pressure-gauge', partId: 'pressure-indicator', label: 'Pressure indicator', material: 'accent-blue', transform: { position: [0, 0.58, 0], scale: [0.22, 0.22, 0.08] }, motionBindings: [custom('hydraulic-stroke', 'pressure-pulse', 'Gauge follows selected extend/retract side.')], }, ], }, ], labels: [ label('hydraulic-rod-label', 'rod', 'Rod transmits linear force', 'hydraulic-rod', [0.82, 0.24, 0.16], 5), label('hydraulic-seal-label', 'seals', 'Seals prevent leakage under pressure', 'hydraulic-seals', [0.18, -0.34, 0.16], 4), label('hydraulic-port-label', 'port-a', 'Ports switch pressure between sides', 'hydraulic-port-a', [-0.58, 0.5, 0.16], 5), ], replacementPoints: [ slot('hydraulic-cylinder', 'barrel', 'cylinder-barrel', 'Cylinder barrel GLB', 'hydraulic-barrel', 'Use sectioned barrel or separate transparent cutaway material.'), slot('hydraulic-cylinder', 'piston-rod', 'rod', 'Piston and rod GLB', 'hydraulic-rod', 'Rod stroke axis must align local X.'), slot('hydraulic-cylinder', 'ports-seals', 'seals', 'Ports and seals GLB', 'hydraulic-seals', 'Seal groups should remain individually visible for detail panel.'), ], thumbnail: thumb(['rod', 'cap-end-fluid', 'pressure-indicator'], 'section'), performanceNotes: [ 'Stroke is a single normalized parameter that drives piston, rod, seal, and fluid volume transforms.', 'Fluid volumes are clipped against the piston plane rather than regenerated.', ], }, 'scotch-yoke': { machineId: 'scotch-yoke', title: 'Scotch Yoke', category: 'mechanisms', summary: 'A crank-to-linear mechanism with rotating crank disk, sliding pin in a slotted yoke, linear rails, output rod, and sinusoidal displacement reference.', layout: 'schematic', scaleMeters: 0.9, anchorPartId: 'yoke', animationProfileId: 'scotch-yoke', guidedTourStageIds: ['overview', 'crank-pin', 'slot', 'linear-output', 'motion-curve'], nodes: [ { id: 'scotch-root', kind: 'group', children: [ { id: 'scotch-crank-disc', kind: 'disc', partId: 'crank-disc', label: 'Crank disk', material: 'brushed-steel', transform: { position: [-0.35, 0, 0], rotation: [1.5708, 0, 0], scale: [0.28, 0.08, 0.28] }, motionBindings: [rotate('scotch-cycle', 'z')], }, { id: 'scotch-crank-pin', kind: 'sphere', partId: 'crank-pin', label: 'Crank pin', material: 'accent-warm', transform: { position: [-0.2, 0, 0.08], scale: [0.06, 0.06, 0.06] }, motionBindings: [custom('scotch-cycle', 'custom-linkage', 'Pin orbits crank center and slides in yoke slot.')], }, { id: 'scotch-yoke', kind: 'rounded-box', partId: 'yoke', label: 'Slotted yoke', material: 'aluminium', transform: { position: [0, 0, 0], scale: [0.48, 0.28, 0.12] }, dimensions: { verticalSlot: true }, motionBindings: [oscillate('scotch-cycle', 'x', 0.3, 0)], }, { id: 'scotch-rails', kind: 'rod', partId: 'guide-rails', label: 'Linear guide rails', material: 'polished-steel', transform: { position: [0.05, 0, -0.08], rotation: [0, 0, 1.5708], scale: [0.04, 1.15, 0.04] }, dimensions: { count: 2 }, }, { id: 'scotch-output-rod', kind: 'rod', partId: 'output-rod', label: 'Linear output rod', material: 'brushed-steel', transform: { position: [0.48, 0, 0], rotation: [0, 0, 1.5708], scale: [0.07, 0.72, 0.07] }, motionBindings: [oscillate('scotch-cycle', 'x', 0.3, 0)], }, { id: 'scotch-motion-curve', kind: 'polyline', partId: 'displacement-curve', label: 'Sinusoidal displacement curve', material: 'accent-blue', transform: { position: [0.05, -0.48, 0], scale: [0.9, 0.22, 0.02] }, dimensions: { curve: 'sine' }, }, ], }, ], labels: [ label('scotch-pin-label', 'crank-pin', 'Crank pin drives the slot', 'scotch-crank-pin', [-0.18, 0.28, 0.16], 5), label('scotch-yoke-label', 'yoke', 'Yoke converts orbit to linear motion', 'scotch-yoke', [0.12, 0.36, 0.16], 5), label('scotch-curve-label', 'displacement-curve', 'Output displacement is sinusoidal', 'scotch-motion-curve', [0.28, -0.66, 0.12], 4), ], replacementPoints: [ slot('scotch-yoke', 'crank', 'crank-disc', 'Crank disk and pin GLB', 'scotch-crank-disc', 'Crank pin should be named so custom linkage can animate it independently.'), slot('scotch-yoke', 'yoke-slider', 'yoke', 'Yoke and slider GLB', 'scotch-yoke', 'Slider stroke axis aligns local X.'), slot('scotch-yoke', 'rails-output', 'guide-rails', 'Guide rails and output rod GLB', 'scotch-rails', 'Rails are static; output rod can share yoke transform.'), ], thumbnail: thumb(['crank-pin', 'yoke', 'displacement-curve']), performanceNotes: [ 'All transforms derive from crank angle, so the yoke path is deterministic and drift-free.', 'Motion curve is a static line with an animated cursor rather than redrawn geometry.', ], }, 'geneva-drive': { machineId: 'geneva-drive', title: 'Geneva Drive', category: 'mechanisms', summary: 'An intermittent-motion Geneva mechanism with drive wheel, indexing pin, locking disk, four-slot Geneva wheel, dwell arcs, and output index markers.', layout: 'schematic', scaleMeters: 0.9, anchorPartId: 'geneva-wheel', animationProfileId: 'geneva-drive', guidedTourStageIds: ['overview', 'drive-pin', 'indexing', 'dwell', 'output-steps'], nodes: [ { id: 'geneva-root', kind: 'group', children: [ { id: 'geneva-drive-wheel', kind: 'disc', partId: 'drive-wheel', label: 'Drive wheel', material: 'brushed-steel', transform: { position: [-0.34, 0, 0], rotation: [1.5708, 0, 0], scale: [0.28, 0.08, 0.28] }, motionBindings: [rotate('geneva-cycle', 'z')], }, { id: 'geneva-drive-pin', kind: 'sphere', partId: 'drive-pin', label: 'Indexing pin', material: 'accent-warm', transform: { position: [-0.16, 0, 0.08], scale: [0.055, 0.055, 0.055] }, motionBindings: [custom('geneva-cycle', 'custom-linkage', 'Pin engages one slot each revolution.')], }, { id: 'geneva-locking-disk', kind: 'disc', partId: 'locking-disk', label: 'Locking disk', material: 'carbon-friction', transform: { position: [-0.34, 0, 0.04], rotation: [1.5708, 0, 0], scale: [0.2, 0.05, 0.2] }, motionBindings: [rotate('geneva-cycle', 'z')], }, { id: 'geneva-wheel', kind: 'disc', partId: 'geneva-wheel', label: 'Four-slot Geneva wheel', material: 'aluminium', transform: { position: [0.28, 0, 0], rotation: [1.5708, 0, 0], scale: [0.38, 0.08, 0.38] }, dimensions: { slots: 4, dwellArc: true }, motionBindings: [custom('geneva-cycle', 'geneva-index', 'Output wheel advances one quarter turn, then dwells.')], }, { id: 'geneva-output-shaft', kind: 'shaft', partId: 'output-shaft', label: 'Intermittent output shaft', material: 'polished-steel', transform: { position: [0.28, 0, -0.28], rotation: [1.5708, 0, 0], scale: [0.08, 0.46, 0.08] }, motionBindings: [custom('geneva-cycle', 'geneva-index', 'Shaft moves only during index portion of cycle.')], }, { id: 'geneva-index-markers', kind: 'polyline', partId: 'index-markers', label: 'Index positions', material: 'accent-blue', transform: { position: [0.28, 0, 0.1], scale: [0.52, 0.52, 0.02] }, dimensions: { count: 4 }, }, ], }, ], labels: [ label('geneva-pin-label', 'drive-pin', 'Pin enters slot to index wheel', 'geneva-drive-pin', [-0.08, 0.28, 0.16], 5), label('geneva-dwell-label', 'locking-disk', 'Locking disk holds wheel during dwell', 'geneva-locking-disk', [-0.54, -0.24, 0.16], 4), label('geneva-wheel-label', 'geneva-wheel', 'Output jumps to discrete positions', 'geneva-wheel', [0.52, 0.34, 0.16], 5), ], replacementPoints: [ slot('geneva-drive', 'drive-wheel-pin', 'drive-wheel', 'Drive wheel and pin GLB', 'geneva-drive-wheel', 'Pin must remain separate from wheel for engagement highlighting.'), slot('geneva-drive', 'geneva-wheel', 'geneva-wheel', 'Geneva wheel GLB', 'geneva-wheel', 'Slots should be modeled as visible cutouts.'), slot('geneva-drive', 'output-shaft', 'output-shaft', 'Output shaft GLB', 'geneva-output-shaft', 'Output rotates with the indexed wheel only.'), ], thumbnail: thumb(['drive-pin', 'geneva-wheel', 'index-markers']), performanceNotes: [ 'Geneva output angle is piecewise-eased to show dwell and rapid index without contact simulation.', 'Slot geometry is static; only transform phases change per frame.', ], }, 'cam-and-follower': { machineId: 'cam-and-follower', title: 'Cam and Follower', category: 'mechanisms', summary: 'A cam mechanism with selectable eccentric, heart, and snail profiles, roller follower, pushrod, return spring, guide, lift curve, and dwell/rise/fall annotations.', layout: 'schematic', scaleMeters: 0.85, anchorPartId: 'camshaft', animationProfileId: 'cam-and-follower', guidedTourStageIds: ['overview', 'cam-profile', 'rise', 'dwell', 'return-spring'], nodes: [ { id: 'cam-root', kind: 'group', children: [ { id: 'cam-shaft', kind: 'shaft', partId: 'camshaft', label: 'Camshaft', material: 'polished-steel', transform: { position: [0, -0.18, 0], rotation: [1.5708, 0, 0], scale: [0.08, 0.55, 0.08] }, motionBindings: [rotate('cam-cycle', 'z')], }, { id: 'cam-profile', kind: 'cam', partId: 'cam-profile', label: 'Selected cam profile', material: 'brass', transform: { position: [0, -0.18, 0], rotation: [1.5708, 0, 0], scale: [0.32, 0.08, 0.32] }, dimensions: { profiles: ['eccentric', 'heart', 'snail'], active: 'eccentric' }, motionBindings: [rotate('cam-cycle', 'z')], }, { id: 'cam-follower-roller', kind: 'roller', partId: 'follower-roller', label: 'Roller follower', material: 'brushed-steel', transform: { position: [0, 0.22, 0], rotation: [1.5708, 0, 0], scale: [0.12, 0.08, 0.12] }, motionBindings: [oscillate('cam-cycle', 'y', 0.2, 0, 'Follower lift depends on selected cam radius at contact angle.')], }, { id: 'cam-pushrod', kind: 'rod', partId: 'pushrod', label: 'Pushrod', material: 'polished-steel', transform: { position: [0, 0.48, 0], scale: [0.06, 0.52, 0.06] }, motionBindings: [oscillate('cam-cycle', 'y', 0.2, 0)], }, { id: 'cam-return-spring', kind: 'spring', partId: 'return-spring', label: 'Return spring', material: 'brushed-steel', transform: { position: [0.18, 0.48, 0], scale: [0.1, 0.46, 0.1] }, motionBindings: [custom('cam-cycle', 'custom-linkage', 'Spring compresses as follower rises.')], }, { id: 'cam-guide', kind: 'rounded-box', partId: 'follower-guide', label: 'Follower guide', material: 'aluminium', transform: { position: [0, 0.5, -0.08], scale: [0.28, 0.52, 0.12] }, }, { id: 'cam-lift-curve', kind: 'polyline', partId: 'lift-curve', label: 'Lift curve', material: 'accent-blue', transform: { position: [0.58, 0.1, 0], scale: [0.5, 0.36, 0.02] }, dimensions: { curve: 'profile-dependent' }, }, ], }, ], labels: [ label('cam-profile-label', 'cam-profile', 'Cam profile defines follower lift law', 'cam-profile', [-0.36, -0.32, 0.16], 5), label('cam-follower-label', 'follower-roller', 'Follower tracks cam radius', 'cam-follower-roller', [0.22, 0.22, 0.16], 5), label('cam-curve-label', 'lift-curve', 'Lift curve shows rise, dwell, and fall', 'cam-lift-curve', [0.82, 0.34, 0.14], 4), ], replacementPoints: [ slot('cam-and-follower', 'cam-profiles', 'cam-profile', 'Cam profile set GLB', 'cam-profile', 'Provide eccentric, heart, and snail meshes or keep procedural profile selection.'), slot('cam-and-follower', 'follower', 'follower-roller', 'Follower and pushrod GLB', 'cam-follower-roller', 'Follower lift axis must align local Y.'), slot('cam-and-follower', 'spring-guide', 'return-spring', 'Spring and guide GLB', 'cam-return-spring', 'Spring can be procedural helix if no asset is supplied.'), ], thumbnail: thumb(['cam-profile', 'follower-roller', 'lift-curve']), performanceNotes: [ 'Profile radius is sampled from a small lookup table for deterministic step-through.', 'Return spring deformation is shader/scale based, not simulated coil physics.', ], }, 'rack-and-pinion': { machineId: 'rack-and-pinion', title: 'Rack and Pinion', category: 'mechanisms', summary: 'A linear-to-rotary conversion with pinion gear, toothed rack, guide rail, input shaft, linear displacement scale, and reversible motion arrows.', layout: 'schematic', scaleMeters: 0.95, anchorPartId: 'rack', animationProfileId: 'rack-and-pinion', guidedTourStageIds: ['overview', 'pinion', 'rack', 'linear-conversion', 'backdriving'], nodes: [ { id: 'rack-root', kind: 'group', children: [ { id: 'rack-pinion', kind: 'gear', partId: 'pinion', label: 'Pinion gear', material: 'brass', transform: { position: [0, 0.18, 0], rotation: [1.5708, 0, 0], scale: [0.24, 0.1, 0.24] }, dimensions: { teeth: 18 }, motionBindings: [rotate('rack-cycle', 'z')], }, { id: 'rack-bar', kind: 'gear', partId: 'rack', label: 'Linear rack', material: 'brushed-steel', transform: { position: [0, -0.12, 0], scale: [0.92, 0.12, 0.1] }, dimensions: { linearTeeth: 28, pitchMatchesPinion: true }, motionBindings: [oscillate('rack-cycle', 'x', 0.38, 0)], }, { id: 'rack-guide', kind: 'rounded-box', partId: 'guide-rail', label: 'Guide rail', material: 'aluminium', transform: { position: [0, -0.32, 0], scale: [1.05, 0.1, 0.16] }, }, { id: 'rack-input-shaft', kind: 'shaft', partId: 'input-shaft', label: 'Rotary shaft', material: 'polished-steel', transform: { position: [0, 0.18, -0.3], rotation: [1.5708, 0, 0], scale: [0.08, 0.45, 0.08] }, motionBindings: [rotate('rack-cycle', 'z')], }, { id: 'rack-displacement-scale', kind: 'polyline', partId: 'displacement-scale', label: 'Linear displacement scale', material: 'accent-blue', transform: { position: [0, -0.5, 0], scale: [1.0, 0.08, 0.02] }, dimensions: { ticks: 9 }, }, { id: 'rack-motion-arrows', kind: 'flow-arrow', partId: 'motion-arrows', label: 'Reversible motion', material: 'accent-warm', transform: { position: [0.56, -0.12, 0], scale: [0.28, 0.06, 0.06] }, motionBindings: [flowPulse('rack-cycle', 0, 'Arrows flip direction when the pinion reverses.')], }, ], }, ], labels: [ label('rack-pinion-label', 'pinion', 'Pinion rotation sets rack travel', 'rack-pinion', [0.28, 0.42, 0.16], 5), label('rack-bar-label', 'rack', 'Rack translates one tooth pitch per tooth engagement', 'rack-bar', [0.18, -0.02, 0.16], 5), label('rack-scale-label', 'displacement-scale', 'Linear output is measured along rack axis', 'rack-displacement-scale', [0.22, -0.62, 0.12], 4), ], replacementPoints: [ slot('rack-and-pinion', 'pinion', 'pinion', 'Pinion GLB', 'rack-pinion', 'Pinion local Z is rotational axis.'), slot('rack-and-pinion', 'rack', 'rack', 'Rack GLB', 'rack-bar', 'Rack teeth pitch should match pinion tooth pitch metadata.'), slot('rack-and-pinion', 'guide', 'guide-rail', 'Guide rail and shaft GLB', 'rack-guide', 'Guide is static and can be merged with housing if desired.'), ], thumbnail: thumb(['pinion', 'rack', 'motion-arrows']), performanceNotes: [ 'Rack translation is computed from pinion angular phase and pitch radius.', 'Tooth contact is visual rather than collision-driven, avoiding jitter.', ], }, 'slider-crank': { machineId: 'slider-crank', title: 'Slider Crank', category: 'mechanisms', summary: 'A fundamental crank-rocker style mechanism showing crank, connecting rod, slider/piston, guide cylinder, dead-centre markers, and rod angularity.', layout: 'schematic', scaleMeters: 0.95, anchorPartId: 'slider-guide', animationProfileId: 'slider-crank', guidedTourStageIds: ['overview', 'crank', 'connecting-rod', 'slider', 'dead-centres'], nodes: [ { id: 'slider-root', kind: 'group', children: [ { id: 'slider-crank-disc', kind: 'disc', partId: 'crank', label: 'Crank', material: 'brushed-steel', transform: { position: [-0.42, 0, 0], rotation: [1.5708, 0, 0], scale: [0.25, 0.08, 0.25] }, motionBindings: [rotate('slider-crank-cycle', 'z')], }, { id: 'slider-crank-pin', kind: 'sphere', partId: 'crank-pin', label: 'Crank pin', material: 'accent-warm', transform: { position: [-0.22, 0, 0.08], scale: [0.055, 0.055, 0.055] }, motionBindings: [custom('slider-crank-cycle', 'custom-linkage', 'Pin orbits crank center.')], }, { id: 'slider-rod', kind: 'link', partId: 'connecting-rod', label: 'Connecting rod', material: 'polished-steel', transform: { position: [0.1, 0, 0], scale: [0.08, 0.68, 0.08] }, motionBindings: [custom('slider-crank-cycle', 'custom-linkage', 'Rod angle and length determine non-sinusoidal piston travel.')], }, { id: 'slider-block', kind: 'rounded-box', partId: 'slider', label: 'Slider / piston', material: 'aluminium', transform: { position: [0.44, 0, 0], scale: [0.22, 0.2, 0.18] }, motionBindings: [oscillate('slider-crank-cycle', 'x', 0.34, 0)], }, { id: 'slider-guide', kind: 'hollow-cylinder', partId: 'slider-guide', label: 'Linear guide', material: 'transparent-shell', transform: { position: [0.44, 0, 0], rotation: [0, 0, 1.5708], scale: [0.18, 0.78, 0.18] }, }, { id: 'slider-dead-centres', kind: 'polyline', partId: 'dead-centre-markers', label: 'Top/bottom dead centre markers', material: 'warning-amber', transform: { position: [0.44, -0.28, 0], scale: [0.72, 0.08, 0.02] }, dimensions: { markers: ['TDC', 'BDC'] }, }, ], }, ], labels: [ label('slider-crank-label', 'crank', 'Crank sets rotation input', 'slider-crank-disc', [-0.62, 0.28, 0.16], 5), label('slider-rod-label', 'connecting-rod', 'Rod length shapes piston motion', 'slider-rod', [0.1, 0.28, 0.16], 5), label('slider-dead-label', 'dead-centre-markers', 'Dead centres occur when crank and rod align', 'slider-dead-centres', [0.62, -0.42, 0.12], 4), ], replacementPoints: [ slot('slider-crank', 'crank-pin', 'crank', 'Crank and pin GLB', 'slider-crank-disc', 'Pin child must orbit around crank local Z axis.'), slot('slider-crank', 'rod-slider', 'connecting-rod', 'Connecting rod and slider GLB', 'slider-rod', 'Rod endpoints should be documented for analytical transform binding.'), slot('slider-crank', 'guide', 'slider-guide', 'Slider guide GLB', 'slider-guide', 'Guide is static with local X as stroke direction.'), ], thumbnail: thumb(['crank', 'connecting-rod', 'dead-centre-markers']), performanceNotes: [ 'The linkage uses closed-form slider-crank equations rather than a physics solver.', 'Dead-centre markers are static annotations toggled by the learning layer.', ], }, 'toggle-clamp': { machineId: 'toggle-clamp', title: 'Toggle Clamp', category: 'mechanisms', summary: 'A quick-action toggle clamp with handle, over-centre links, clamp arm, pivot base, adjustable spindle pad, workpiece, and mechanical advantage indicator.', layout: 'schematic', scaleMeters: 0.9, anchorPartId: 'base', animationProfileId: 'toggle-clamp', guidedTourStageIds: ['overview', 'handle', 'toggle-linkage', 'over-centre-lock', 'clamping-force'], nodes: [ { id: 'toggle-root', kind: 'group', children: [ { id: 'toggle-base', kind: 'rounded-box', partId: 'base', label: 'Mounting base', material: 'dark-cast-iron', transform: { position: [0, -0.28, 0], scale: [0.86, 0.14, 0.36] }, }, { id: 'toggle-handle', kind: 'link', partId: 'handle', label: 'Operating handle', material: 'brushed-steel', transform: { position: [-0.28, 0.18, 0], rotation: [0, 0, -0.75], scale: [0.1, 0.72, 0.08] }, motionBindings: [custom('toggle-cycle', 'reciprocating-rotation', 'Handle rotates through the clamp stroke.')], }, { id: 'toggle-link-upper', kind: 'link', partId: 'toggle-links', label: 'Toggle links', material: 'polished-steel', transform: { position: [0.02, 0.04, 0], rotation: [0, 0, 0.4], scale: [0.08, 0.38, 0.06] }, dimensions: { count: 2 }, motionBindings: [custom('toggle-cycle', 'custom-linkage', 'Links pass just over centre to lock.')], }, { id: 'toggle-clamp-arm', kind: 'link', partId: 'clamp-arm', label: 'Clamp arm', material: 'aluminium', transform: { position: [0.34, 0.04, 0], rotation: [0, 0, -0.18], scale: [0.1, 0.72, 0.08] }, motionBindings: [custom('toggle-cycle', 'custom-linkage', 'Clamp arm moves downward as handle closes.')], }, { id: 'toggle-spindle', kind: 'shaft', partId: 'spindle-pad', label: 'Adjustable spindle pad', material: 'rubber', transform: { position: [0.62, -0.08, 0], scale: [0.11, 0.24, 0.11] }, motionBindings: [custom('toggle-cycle', 'custom-linkage', 'Pad contacts workpiece near the over-centre position.')], }, { id: 'toggle-workpiece', kind: 'rounded-box', partId: 'workpiece', label: 'Workpiece', material: 'brass', transform: { position: [0.62, -0.36, 0], scale: [0.36, 0.12, 0.26] }, }, { id: 'toggle-force-gauge', kind: 'pressure-gauge', partId: 'mechanical-advantage', label: 'Mechanical advantage indicator', material: 'accent-blue', transform: { position: [-0.55, 0.42, 0], scale: [0.22, 0.22, 0.08] }, motionBindings: [custom('toggle-cycle', 'pressure-pulse', 'Mechanical advantage rises sharply near the locked position.')], }, ], }, ], labels: [ label('toggle-handle-label', 'handle', 'Handle supplies input motion', 'toggle-handle', [-0.52, 0.42, 0.16], 5), label('toggle-overcentre-label', 'toggle-links', 'Links cross over centre to lock', 'toggle-link-upper', [0.08, 0.28, 0.16], 5), label('toggle-pad-label', 'spindle-pad', 'Pad applies clamping force to workpiece', 'toggle-spindle', [0.82, 0.08, 0.16], 4), ], replacementPoints: [ slot('toggle-clamp', 'base', 'base', 'Clamp base GLB', 'toggle-base', 'Base is static and can include mounting holes.'), slot('toggle-clamp', 'handle-links-arm', 'toggle-links', 'Handle and toggle linkage GLB', 'toggle-link-upper', 'Each pivoted link must be a separate child mesh for analytical animation.'), slot('toggle-clamp', 'spindle-pad', 'spindle-pad', 'Spindle pad GLB', 'toggle-spindle', 'Pad remains adjustable along clamp arm local axis.'), ], thumbnail: thumb(['handle', 'toggle-links', 'mechanical-advantage']), performanceNotes: [ 'Link pivots are solved geometrically from handle angle, not simulated with constraints.', 'Mechanical advantage is a scalar overlay driven by distance from over-centre lock.', ], }, 'ball-bearing': { machineId: 'ball-bearing', title: 'Ball Bearing', category: 'structural-other', summary: 'A deep-groove ball bearing cutaway with inner and outer races, balls, cage, lubricant film, load distribution arrows, and rolling contact paths.', layout: 'sectioned', scaleMeters: 0.75, anchorPartId: 'outer-race', animationProfileId: 'ball-bearing', guidedTourStageIds: ['overview', 'races', 'balls', 'cage', 'load-distribution'], nodes: [ { id: 'ball-bearing-root', kind: 'group', children: [ { id: 'ball-outer-race', kind: 'torus', partId: 'outer-race', label: 'Outer race', material: 'brushed-steel', transform: { position: [0, 0, 0], rotation: [1.5708, 0, 0], scale: [0.52, 0.52, 0.09] }, dimensions: { groove: 'deep', sectionRemovedDegrees: 70 }, explodedOffset: [0, 0, -0.22], }, { id: 'ball-inner-race', kind: 'torus', partId: 'inner-race', label: 'Inner race', material: 'polished-steel', transform: { position: [0, 0, 0], rotation: [1.5708, 0, 0], scale: [0.28, 0.28, 0.08] }, dimensions: { groove: 'deep' }, motionBindings: [rotate('bearing-cycle', 'z', 1)], }, { id: 'bearing-balls', kind: 'sphere', partId: 'balls', label: 'Rolling balls', material: 'polished-steel', transform: { position: [0, 0, 0], scale: [0.08, 0.08, 0.08] }, dimensions: { count: 10, orbitRadius: 0.39 }, motionBindings: [custom('bearing-cycle', 'planetary-orbit', 'Balls roll and orbit at cage speed between races.')], }, { id: 'bearing-cage', kind: 'torus', partId: 'cage', label: 'Ball cage', material: 'brass', transform: { position: [0, 0, 0], rotation: [1.5708, 0, 0], scale: [0.39, 0.39, 0.04] }, dimensions: { pockets: 10 }, motionBindings: [rotate('bearing-cycle', 'z', 0.42)], }, { id: 'bearing-lubricant', kind: 'fluid-volume', partId: 'lubricant-film', label: 'Lubricant film', material: 'lubricant', transform: { position: [0, 0, 0], rotation: [1.5708, 0, 0], scale: [0.44, 0.44, 0.03] }, motionBindings: [custom('bearing-cycle', 'visibility-pulse', 'Thin lubricant film separates rolling contacts.')], }, { id: 'bearing-load', kind: 'load-arrow', partId: 'load-distribution', label: 'Load distribution', material: 'warning-amber', transform: { position: [0, 0.62, 0], rotation: [0, 0, -1.5708], scale: [0.42, 0.07, 0.07] }, dimensions: { lowerBallLoadHigher: true }, }, ], }, ], labels: [ label('ball-race-label', 'outer-race', 'Races provide hardened rolling grooves', 'ball-outer-race', [-0.46, 0.48, 0.16], 5), label('ball-balls-label', 'balls', 'Balls roll with very low friction', 'bearing-balls', [0.42, 0.32, 0.16], 5), label('ball-load-label', 'load-distribution', 'Load concentrates across a few balls', 'bearing-load', [0.28, 0.78, 0.16], 4), ], replacementPoints: [ slot('ball-bearing', 'races', 'outer-race', 'Inner and outer races GLB', 'ball-outer-race', 'Races should remain separate meshes for opacity and cutaway control.'), slot('ball-bearing', 'balls', 'balls', 'Bearing balls GLB', 'bearing-balls', 'Provide one ball mesh for instancing or ten named children.'), slot('ball-bearing', 'cage', 'cage', 'Ball cage GLB', 'bearing-cage', 'Cage pockets should align with ball orbit radius.'), ], thumbnail: thumb(['balls', 'outer-race', 'load-distribution'], 'section'), performanceNotes: [ 'Balls are ideal instancing candidates with per-instance angular offsets.', 'Race cutaway is a static clipped mesh, avoiding runtime clipping for catalogue thumbnails.', ], }, 'roller-bearing': { machineId: 'roller-bearing', title: 'Roller Bearing', category: 'structural-other', summary: 'A tapered roller bearing variant with cone, cup, tapered rollers, cage, projected load lines, and combined radial/axial load visualization.', layout: 'sectioned', scaleMeters: 0.82, anchorPartId: 'outer-cup', animationProfileId: 'roller-bearing', guidedTourStageIds: ['overview', 'tapered-rollers', 'cone-cup', 'load-line', 'cage'], nodes: [ { id: 'roller-bearing-root', kind: 'group', children: [ { id: 'roller-outer-cup', kind: 'torus', partId: 'outer-cup', label: 'Outer cup', material: 'brushed-steel', transform: { position: [0, 0, 0], rotation: [1.5708, 0, 0], scale: [0.56, 0.56, 0.1] }, dimensions: { taperDegrees: 12, sectionRemovedDegrees: 70 }, explodedOffset: [0, 0, -0.24], }, { id: 'roller-inner-cone', kind: 'torus', partId: 'inner-cone', label: 'Inner cone', material: 'polished-steel', transform: { position: [0, 0, 0], rotation: [1.5708, 0, 0], scale: [0.32, 0.32, 0.1] }, dimensions: { taperDegrees: 12 }, motionBindings: [rotate('roller-bearing-cycle', 'z', 1)], }, { id: 'tapered-rollers', kind: 'roller', partId: 'tapered-rollers', label: 'Tapered rollers', material: 'polished-steel', transform: { position: [0, 0, 0], scale: [0.08, 0.18, 0.08] }, dimensions: { count: 12, orbitRadius: 0.43, taperDegrees: 12 }, motionBindings: [custom('roller-bearing-cycle', 'planetary-orbit', 'Rollers spin and orbit while their axes point toward a common apex.')], }, { id: 'roller-cage', kind: 'torus', partId: 'cage', label: 'Roller cage', material: 'brass', transform: { position: [0, 0, 0], rotation: [1.5708, 0, 0], scale: [0.43, 0.43, 0.04] }, dimensions: { pockets: 12, tapered: true }, motionBindings: [rotate('roller-bearing-cycle', 'z', 0.38)], }, { id: 'roller-load-line', kind: 'load-arrow', partId: 'load-line', label: 'Projected load line', material: 'warning-amber', transform: { position: [0.32, 0.48, 0], rotation: [0, 0, -0.85], scale: [0.48, 0.07, 0.07] }, dimensions: { combinesRadialAndAxial: true }, }, ], }, ], labels: [ label('roller-taper-label', 'tapered-rollers', 'Tapered rollers support axial and radial loads', 'tapered-rollers', [0.48, 0.26, 0.16], 5), label('roller-cup-label', 'outer-cup', 'Cup and cone raceways share a common apex', 'roller-outer-cup', [-0.54, 0.44, 0.16], 5), label('roller-load-label', 'load-line', 'Load line projects through the bearing apex', 'roller-load-line', [0.58, 0.68, 0.16], 4), ], replacementPoints: [ slot('roller-bearing', 'cup-cone', 'outer-cup', 'Cup and cone GLB', 'roller-outer-cup', 'Taper angle should be documented so roller axes align.'), slot('roller-bearing', 'rollers', 'tapered-rollers', 'Tapered rollers GLB', 'tapered-rollers', 'One roller mesh can be instanced around the orbit.'), slot('roller-bearing', 'cage', 'cage', 'Roller cage GLB', 'roller-cage', 'Cage pockets should match roller count.'), ], thumbnail: thumb(['tapered-rollers', 'outer-cup', 'load-line'], 'section'), performanceNotes: [ 'Tapered rollers are instanced with precomputed orientation matrices.', 'Load-line annotations are static overlays that remain legible in wireframe mode.', ], }, 'disc-brake-caliper': { machineId: 'disc-brake-caliper', title: 'Disc Brake Caliper', category: 'structural-other', summary: 'A hydraulic disc brake assembly with rotor, floating caliper body, brake pads, pistons, hydraulic line, dust boots, heat glow, and clamping force visualization.', layout: 'cutaway', scaleMeters: 1, anchorPartId: 'brake-rotor', animationProfileId: 'disc-brake-caliper', guidedTourStageIds: ['overview', 'hydraulic-pressure', 'piston-actuation', 'friction-pads', 'heat'], nodes: [ { id: 'brake-root', kind: 'group', children: [ { id: 'brake-rotor', kind: 'disc', partId: 'brake-rotor', label: 'Ventilated rotor', material: 'brushed-steel', transform: { position: [0, 0, 0], rotation: [1.5708, 0, 0], scale: [0.52, 0.08, 0.52] }, dimensions: { vents: 24, drilled: false }, motionBindings: [rotate('wheel-speed', 'z', 1.5)], }, { id: 'brake-caliper', kind: 'caliper-body', partId: 'caliper-body', label: 'Caliper body', material: 'aluminium', transform: { position: [0.42, 0, 0], scale: [0.34, 0.68, 0.28] }, dimensions: { floating: true, sectioned: true }, explodedOffset: [0.32, 0, 0], }, { id: 'brake-inner-pad', kind: 'box', partId: 'inner-pad', label: 'Inner brake pad', material: 'carbon-friction', transform: { position: [0.2, 0, 0.08], scale: [0.14, 0.42, 0.06] }, motionBindings: [oscillate('brake-apply', 'x', 0.05, 0)], }, { id: 'brake-outer-pad', kind: 'box', partId: 'outer-pad', label: 'Outer brake pad', material: 'carbon-friction', transform: { position: [0.58, 0, -0.08], scale: [0.14, 0.42, 0.06] }, motionBindings: [oscillate('brake-apply', 'x', 0.05, 0.5)], }, { id: 'brake-pistons', kind: 'cylinder', partId: 'pistons', label: 'Hydraulic pistons', material: 'polished-steel', transform: { position: [0.52, 0, 0.12], rotation: [0, 0, 1.5708], scale: [0.1, 0.18, 0.1] }, dimensions: { count: 2 }, motionBindings: [oscillate('brake-apply', 'x', 0.06, 0, 'Pistons push pads against rotor when pressure rises.')], }, { id: 'brake-hydraulic-line', kind: 'pipe', partId: 'hydraulic-line', label: 'Hydraulic line', material: 'rubber', transform: { position: [0.72, 0.42, 0], scale: [0.08, 0.46, 0.08] }, motionBindings: [custom('brake-apply', 'pressure-pulse', 'Line pressure rises as brake is applied.')], }, { id: 'brake-heat', kind: 'heat-glow', partId: 'heat-zone', label: 'Friction heat', material: 'hot-gas', transform: { position: [0.38, 0, 0], rotation: [1.5708, 0, 0], scale: [0.52, 0.03, 0.52] }, motionBindings: [custom('brake-apply', 'heat-pulse', 'Rotor glow increases during sustained braking.')], }, { id: 'brake-force-arrows', kind: 'load-arrow', partId: 'clamping-force', label: 'Clamping force', material: 'warning-amber', transform: { position: [0.4, 0, 0.24], scale: [0.32, 0.07, 0.07] }, motionBindings: [custom('brake-apply', 'pressure-pulse', 'Opposing arrows show pad clamping force.')], }, ], }, ], labels: [ label('brake-rotor-label', 'brake-rotor', 'Rotor converts wheel kinetic energy into heat', 'brake-rotor', [-0.38, 0.42, 0.16], 5), label('brake-piston-label', 'pistons', 'Hydraulic pistons press pads inward', 'brake-pistons', [0.75, 0.18, 0.18], 5), label('brake-pad-label', 'inner-pad', 'Friction pads clamp both sides of rotor', 'brake-inner-pad', [0.16, -0.42, 0.16], 4), ], replacementPoints: [ slot('disc-brake-caliper', 'rotor', 'brake-rotor', 'Brake rotor GLB', 'brake-rotor', 'Rotor local Z is rotation axis; vents can be real geometry or normal mapped.'), slot('disc-brake-caliper', 'caliper', 'caliper-body', 'Caliper body GLB', 'brake-caliper', 'Caliper body should be sectioned or have removable outer face.'), slot('disc-brake-caliper', 'pads-pistons', 'pistons', 'Pads and pistons GLB', 'brake-pistons', 'Pads and pistons must remain separately transformable along clamp axis.'), slot('disc-brake-caliper', 'hydraulic-line', 'hydraulic-line', 'Hydraulic hose GLB', 'brake-hydraulic-line', 'Flexible hose can be static for the educational scene.'), ], thumbnail: thumb(['brake-rotor', 'caliper-body', 'clamping-force'], 'section'), performanceNotes: [ 'Rotor rotation and brake application are independent channels so users can pause one while stepping the other.', 'Heat glow is a thin disc overlay with additive material, disabled under reduced-motion settings.', ], }, turbocharger: { machineId: 'turbocharger', title: 'Turbocharger', category: 'structural-other', summary: 'A turbocharger cutaway with turbine housing/wheel, compressor housing/wheel, shared shaft, bearings, center cartridge, wastegate, exhaust flow, intake boost flow, and oil/coolant lines.', layout: 'cutaway', scaleMeters: 1.05, anchorPartId: 'center-housing', animationProfileId: 'turbocharger', guidedTourStageIds: ['overview', 'exhaust-turbine', 'compressor', 'shaft-bearings', 'wastegate', 'boost'], nodes: [ { id: 'turbo-root', kind: 'group', children: [ { id: 'turbo-center', kind: 'rounded-box', partId: 'center-housing', label: 'Center bearing housing', material: 'dark-cast-iron', transform: { position: [0, 0, 0], scale: [0.32, 0.32, 0.32] }, dimensions: { oilPassages: true }, }, { id: 'turbo-shaft', kind: 'shaft', partId: 'shaft', label: 'Common shaft', material: 'polished-steel', transform: { position: [0, 0, 0], rotation: [0, 0, 1.5708], scale: [0.07, 1.2, 0.07] }, motionBindings: [rotate('turbo-spool', 'x', 3.2, 0, 'Turbo shaft spins very fast relative to engine speed.')], }, { id: 'turbo-turbine-housing', kind: 'vane-wheel', partId: 'turbine-housing', label: 'Turbine scroll housing', material: 'dark-cast-iron', transform: { position: [-0.48, 0, 0], scale: [0.42, 0.42, 0.28] }, dimensions: { scroll: true, sectioned: true }, explodedOffset: [-0.28, 0, 0], }, { id: 'turbo-turbine-wheel', kind: 'turbine-stage', partId: 'turbine-wheel', label: 'Turbine wheel', material: 'polished-steel', transform: { position: [-0.48, 0, 0], rotation: [0, 0, 1.5708], scale: [0.3, 0.12, 0.3] }, dimensions: { blades: 11, radialInflow: true }, motionBindings: [rotate('turbo-spool', 'x', 3.2)], }, { id: 'turbo-compressor-housing', kind: 'vane-wheel', partId: 'compressor-housing', label: 'Compressor volute', material: 'aluminium', transform: { position: [0.48, 0, 0], scale: [0.42, 0.42, 0.28] }, dimensions: { scroll: true, sectioned: true }, explodedOffset: [0.28, 0, 0], }, { id: 'turbo-compressor-wheel', kind: 'impeller', partId: 'compressor-wheel', label: 'Compressor wheel', material: 'brushed-steel', transform: { position: [0.48, 0, 0], rotation: [0, 0, 1.5708], scale: [0.3, 0.12, 0.3] }, dimensions: { blades: 12, backswept: true }, motionBindings: [rotate('turbo-spool', 'x', 3.2)], }, { id: 'turbo-wastegate', kind: 'valve', partId: 'wastegate', label: 'Wastegate bypass valve', material: 'brass', transform: { position: [-0.72, 0.36, 0], scale: [0.14, 0.22, 0.12] }, motionBindings: [custom('boost-control', 'reciprocating-rotation', 'Wastegate opens to bypass turbine and limit boost.')], }, { id: 'turbo-exhaust-flow', kind: 'flow-arrow', partId: 'exhaust-flow', label: 'Exhaust energy', material: 'exhaust-gas', transform: { position: [-0.82, -0.2, 0], rotation: [0, 0, 0.25], scale: [0.52, 0.07, 0.07] }, motionBindings: [flowPulse('turbo-spool', 0.1, 'Exhaust gas drives turbine wheel.')], }, { id: 'turbo-boost-flow', kind: 'flow-arrow', partId: 'boost-flow', label: 'Compressed intake air', material: 'accent-blue', transform: { position: [0.82, 0.2, 0], rotation: [0, 0, -0.25], scale: [0.52, 0.07, 0.07] }, motionBindings: [flowPulse('turbo-spool', 0.35, 'Compressor raises intake manifold pressure.')], }, { id: 'turbo-oil-lines', kind: 'pipe', partId: 'oil-coolant-lines', label: 'Oil and coolant lines', material: 'lubricant', transform: { position: [0, -0.42, 0], scale: [0.58, 0.08, 0.08] }, }, ], }, ], labels: [ label('turbo-turbine-label', 'turbine-wheel', 'Exhaust turbine extracts gas energy', 'turbo-turbine-wheel', [-0.72, 0.26, 0.16], 5), label('turbo-compressor-label', 'compressor-wheel', 'Compressor wheel boosts intake air', 'turbo-compressor-wheel', [0.74, 0.3, 0.16], 5), label('turbo-shaft-label', 'shaft', 'Shared shaft couples turbine and compressor', 'turbo-shaft', [0.08, 0.28, 0.16], 5), label('turbo-wastegate-label', 'wastegate', 'Wastegate controls turbine power and boost', 'turbo-wastegate', [-0.92, 0.56, 0.16], 4), ], replacementPoints: [ slot('turbocharger', 'center-housing-shaft', 'center-housing', 'Center housing and shaft GLB', 'turbo-center', 'Shaft axis should align local X and be shared by both wheels.'), slot('turbocharger', 'turbine-side', 'turbine-housing', 'Turbine housing and wheel GLB', 'turbo-turbine-housing', 'Keep turbine wheel separate from scroll housing for spool rotation.'), slot('turbocharger', 'compressor-side', 'compressor-housing', 'Compressor housing and wheel GLB', 'turbo-compressor-housing', 'Compressor wheel local axis must match shaft axis.'), slot('turbocharger', 'wastegate-lines', 'wastegate', 'Wastegate and oil/coolant lines GLB', 'turbo-wastegate', 'Wastegate flap should be a pivoting child mesh.'), ], thumbnail: thumb(['turbine-wheel', 'compressor-wheel', 'wastegate'], 'section'), performanceNotes: [ 'The turbine and compressor wheels share a single high-speed spool channel with motion blur-friendly material hints.', 'Wastegate and boost-flow overlays are independent so boost control can be demonstrated at constant shaft speed.', ], }, } as const satisfies Record; export const CORE_MACHINE_BLUEPRINT_LIST: readonly ProceduralMachineBlueprint[] = CORE_MACHINE_IDS.map( (machineId) => CORE_MACHINE_BLUEPRINTS[machineId], ); export const hasCoreMachineBlueprint = (machineId: string): machineId is CoreMachineId => Object.prototype.hasOwnProperty.call(CORE_MACHINE_BLUEPRINTS, machineId); export const getCoreMachineBlueprint = (machineId: string): ProceduralMachineBlueprint | undefined => { if (!hasCoreMachineBlueprint(machineId)) { return undefined; } return CORE_MACHINE_BLUEPRINTS[machineId]; }; export interface BlueprintValidationIssue { readonly machineId: string; readonly severity: 'error' | 'warning'; readonly message: string; } export const validateCoreMachineBlueprintCoverage = ( expectedIds: readonly string[] = CORE_MACHINE_IDS, ): readonly BlueprintValidationIssue[] => { const issues: BlueprintValidationIssue[] = []; for (const machineId of expectedIds) { if (!hasCoreMachineBlueprint(machineId)) { issues.push({ machineId, severity: 'error', message: 'Missing procedural blueprint for registered core machine.', }); continue; } const blueprint = CORE_MACHINE_BLUEPRINTS[machineId]; const nodeIds = new Set(); const partIds = new Set(); const visit = (nodes: ProceduralMachineBlueprint['nodes']): void => { for (const node of nodes) { if (nodeIds.has(node.id)) { issues.push({ machineId, severity: 'error', message: `Duplicate procedural node id "${node.id}".`, }); } nodeIds.add(node.id); if (node.partId !== undefined) { partIds.add(node.partId); } if (node.children !== undefined) { visit(node.children); } } }; visit(blueprint.nodes); if (!partIds.has(blueprint.anchorPartId)) { issues.push({ machineId, severity: 'warning', message: `Anchor part "${blueprint.anchorPartId}" is not represented by a procedural node.`, }); } for (const replacementPoint of blueprint.replacementPoints) { if (!nodeIds.has(replacementPoint.anchorNodeId)) { issues.push({ machineId, severity: 'error', message: `Replacement point "${replacementPoint.id}" references missing node "${replacementPoint.anchorNodeId}".`, }); } if (!partIds.has(replacementPoint.partId)) { issues.push({ machineId, severity: 'warning', message: `Replacement point "${replacementPoint.id}" references part "${replacementPoint.partId}" that is not present in the procedural node tree.`, }); } } for (const blueprintLabel of blueprint.labels) { if (!nodeIds.has(blueprintLabel.anchorNodeId)) { issues.push({ machineId, severity: 'error', message: `Label "${blueprintLabel.id}" references missing node "${blueprintLabel.anchorNodeId}".`, }); } if (!partIds.has(blueprintLabel.partId)) { issues.push({ machineId, severity: 'warning', message: `Label "${blueprintLabel.id}" references part "${blueprintLabel.partId}" that is not present in the procedural node tree.`, }); } } } return issues; };