export const CATALOGUE_TARGET_COUNT = 28; export type CatalogueCategory = | "engine" | "gearbox" | "pump" | "mechanism" | "structural"; export type ImplementationTier = 1 | 2 | 3 | 4; export type MachineComplexity = | "introductory" | "intermediate" | "advanced" | "expert"; export type AssetStrategy = | "procedural-first" | "hybrid-glb" | "cad-derived-glb"; export type ViewerModeBlueprint = | "solid" | "wireframe" | "exploded" | "section" | "ghosted" | "fluid-flow" | "load-path" | "motion-trails"; export type AnimationDriverBlueprint = | "rotary" | "linear" | "oscillating" | "reciprocating" | "gear-train" | "cam-profile" | "belt" | "fluid-flow" | "load-path" | "spring" | "hydraulic" | "thermal-flow"; export type MaterialHint = | "brushed-steel" | "cast-metal" | "painted-metal" | "rubber" | "plastic" | "glass" | "composite" | "copper" | "fluid" | "oil-film" | "cutaway-highlight" | "concrete" | "cable"; export type Vector3Tuple = readonly [number, number, number]; export interface ComponentBlueprint { readonly key: string; readonly label: string; readonly role: string; readonly defaultMaterial: MaterialHint; readonly explodeAxis: Vector3Tuple; readonly sectionCritical?: boolean; } export interface AnimationChannelBlueprint { readonly key: string; readonly label: string; readonly driver: AnimationDriverBlueprint; readonly relation: string; readonly notes: string; } export interface CameraPresetBlueprint { readonly key: string; readonly label: string; readonly position: Vector3Tuple; readonly target: Vector3Tuple; readonly notes: string; } export interface MachineBlueprint { readonly id: string; readonly title: string; readonly category: CatalogueCategory; readonly implementationTier: ImplementationTier; readonly complexity: MachineComplexity; readonly assetStrategy: AssetStrategy; readonly engineeringFocus: readonly string[]; readonly learningOutcomes: readonly string[]; readonly primaryMotion: string; readonly components: readonly ComponentBlueprint[]; readonly animationChannels: readonly AnimationChannelBlueprint[]; readonly requiredViewerModes: readonly ViewerModeBlueprint[]; readonly explodedViewPlan: string; readonly crossSectionPlan: string; readonly cameraPresets: readonly CameraPresetBlueprint[]; readonly validationChecks: readonly string[]; readonly sourceAssetNotes: string; readonly riskNotes?: string; } export interface BlueprintValidationIssue { readonly machineId?: string; readonly message: string; } /** * Canonical implementation blueprint for the full 28-machine Mechanica catalogue. * * This is intentionally richer than the public machine registry metadata. It gives * asset authors, animation authors, and future QA automation a single typed source * for the component decomposition and motion contracts each catalogue entry must * satisfy before it graduates from placeholder to production viewer content. */ export const catalogueBlueprints: readonly MachineBlueprint[] = [ { id: "inline-four-engine", title: "Inline-Four Combustion Engine", category: "engine", implementationTier: 1, complexity: "advanced", assetStrategy: "hybrid-glb", engineeringFocus: [ "four-stroke cycle", "crankshaft phasing", "overhead valvetrain timing", ], learningOutcomes: [ "Explain why piston pairs move together while firing events are staggered.", "Trace intake, compression, power, and exhaust strokes through 720 degrees.", ], primaryMotion: "Continuous crankshaft rotation drives four reciprocating piston assemblies and phase-offset camshaft events.", components: [ { key: "engine-block", label: "Engine block and cylinders", role: "Locates the cylinder bores, coolant jackets, crank journals, and main bearing saddles.", defaultMaterial: "cast-metal", explodeAxis: [0, -1, 0], sectionCritical: true, }, { key: "crankshaft", label: "Crankshaft", role: "Converts piston force into output torque through four offset crank throws.", defaultMaterial: "brushed-steel", explodeAxis: [0, -0.4, 0], sectionCritical: true, }, { key: "pistons-rods", label: "Pistons and connecting rods", role: "Transmit gas pressure to the crankshaft while constraining combustion chamber volume.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.7, 0], sectionCritical: true, }, { key: "camshafts-valves", label: "Camshafts, tappets, and valves", role: "Open and close intake and exhaust paths at half crankshaft speed.", defaultMaterial: "brushed-steel", explodeAxis: [0, 1.1, 0], sectionCritical: true, }, { key: "timing-drive", label: "Timing chain and sprockets", role: "Maintains the 2:1 crank-to-cam speed relationship.", defaultMaterial: "painted-metal", explodeAxis: [1, 0, 0], }, ], animationChannels: [ { key: "crankshaft-rotation", label: "Crankshaft rotation", driver: "rotary", relation: "theta = inputAngle", notes: "Primary clock; all piston and cam transforms derive from this channel.", }, { key: "piston-reciprocation", label: "Piston reciprocation", driver: "reciprocating", relation: "y = crankRadius*cos(theta + throwPhase) + rodLengthCorrection(theta)", notes: "Use the connecting-rod correction rather than a pure sine wave to make top/bottom dwell visibly accurate.", }, { key: "valvetrain-timing", label: "Valvetrain timing", driver: "cam-profile", relation: "camAngle = theta / 2; firingOrder = 1-3-4-2", notes: "Valve lift curves should include short overlap around exhaust/intake transition.", }, { key: "combustion-pulses", label: "Combustion pulse highlights", driver: "thermal-flow", relation: "pulse every 180 crank degrees in firing order", notes: "Non-physical glow helper used only to teach stroke sequence; disabled in solid CAD mode.", }, ], requiredViewerModes: ["solid", "wireframe", "exploded", "section", "ghosted"], explodedViewPlan: "Separate head/valvetrain upward, timing cover forward, block downward, and crank/piston assemblies slightly along cylinder axes so the kinematic chain remains readable.", crossSectionPlan: "Default section plane cuts through cylinders 2 and 3, revealing piston crown, combustion chamber, valves, crank throw, oil gallery, and water jacket.", cameraPresets: [ { key: "front-cutaway", label: "Front cutaway", position: [4.6, 2.4, 5.2], target: [0, 0.6, 0], notes: "Best saved view for explaining four-stroke sequencing.", }, { key: "crankcase-low", label: "Crankcase low view", position: [3.6, -1.2, 2.8], target: [0, -0.45, 0], notes: "Highlights crank throws, rods, and main journals.", }, { key: "valvetrain-top", label: "Valvetrain top view", position: [1.8, 5.1, 2.4], target: [0, 1.35, 0], notes: "Shows cam lobes and valve event offsets.", }, ], validationChecks: [ "Pistons 1 and 4 share position phase; pistons 2 and 3 share opposite phase.", "Camshafts rotate exactly one revolution per two crank revolutions.", "Combustion highlights follow 1-3-4-2 order at 180 degree crank intervals.", ], sourceAssetNotes: "Start with procedural internals for exact animation pivots, then swap block, head, covers, and manifolds with optimized GLB shells.", riskNotes: "Naive sinusoidal piston motion will be visibly wrong in slow-motion educational mode.", }, { id: "v6-engine", title: "V6 Combustion Engine", category: "engine", implementationTier: 3, complexity: "expert", assetStrategy: "hybrid-glb", engineeringFocus: [ "bank angle packaging", "shared crank pins", "split exhaust routing", ], learningOutcomes: [ "Compare banked cylinder packaging with an inline engine layout.", "Identify why crank pin layout affects vibration and firing smoothness.", ], primaryMotion: "A single crankshaft drives two angled cylinder banks with phase-offset reciprocating assemblies and bank-specific valvetrains.", components: [ { key: "v-block", label: "V engine block", role: "Supports two cylinder banks and a compact central crankcase.", defaultMaterial: "cast-metal", explodeAxis: [0, -1, 0], sectionCritical: true, }, { key: "crankshaft-shared-pins", label: "Crankshaft with shared pins", role: "Coordinates bank-to-bank piston phase and firing spacing.", defaultMaterial: "brushed-steel", explodeAxis: [0, -0.5, 0], sectionCritical: true, }, { key: "bank-piston-sets", label: "Left and right piston banks", role: "Reciprocate on two cylinder axes angled from the crank centerline.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.8, 0], sectionCritical: true, }, { key: "dual-heads-valvetrain", label: "Cylinder heads and valvetrain", role: "Hosts valves, camshafts, and ports for each bank.", defaultMaterial: "cast-metal", explodeAxis: [0, 1.1, 0], sectionCritical: true, }, { key: "intake-exhaust-manifolds", label: "Intake and exhaust manifolds", role: "Routes air and exhaust around the compact V package.", defaultMaterial: "painted-metal", explodeAxis: [1, 0.35, 0], }, ], animationChannels: [ { key: "crankshaft-rotation", label: "Crankshaft rotation", driver: "rotary", relation: "theta = inputAngle", notes: "Primary clock for both cylinder banks.", }, { key: "banked-piston-motion", label: "Banked piston motion", driver: "reciprocating", relation: "pistonAxis = rotateY(bankAngle/2) for each bank; stroke follows crank phase", notes: "Transforms must run along cylinder axes rather than global vertical axes.", }, { key: "dual-cam-timing", label: "Dual-bank cam timing", driver: "cam-profile", relation: "camAngle = theta / 2 with bank-specific phase offsets", notes: "Useful for comparing SOHC/DOHC variants in later catalogue expansions.", }, { key: "exhaust-pulse-routing", label: "Exhaust pulse routing", driver: "thermal-flow", relation: "pulse path follows firing order across alternating banks", notes: "Render as subtle emissive traces only in educational overlay mode.", }, ], requiredViewerModes: ["solid", "wireframe", "exploded", "section", "ghosted"], explodedViewPlan: "Pull cylinder heads outward along bank normals, lift intake plenum upward, separate manifolds laterally, and drop crankcase elements downward.", crossSectionPlan: "Use an oblique section plane through one cylinder on each bank and the crank centerline to expose bank angle geometry.", cameraPresets: [ { key: "bank-angle", label: "Bank angle overview", position: [5.4, 3.1, 5.2], target: [0, 0.6, 0], notes: "Shows the V package and shared crankcase.", }, { key: "crank-centerline", label: "Crank centerline", position: [4.8, 0.4, 0.2], target: [0, 0, 0], notes: "Highlights shared crank pins and bank offsets.", }, { key: "manifold-routing", label: "Manifold routing", position: [-4.4, 2.8, 4.4], target: [0, 1, 0], notes: "Explains intake and exhaust packaging tradeoffs.", }, ], validationChecks: [ "Piston travel vectors align with left/right bank axes.", "Camshaft channels remain locked at half crankshaft speed.", "Exploded heads move along bank normals, not world up.", ], sourceAssetNotes: "Author banked procedural skeleton first; GLB cosmetic shells can then bind to named pivots for reliable animation.", riskNotes: "Complex firing orders vary by real-world engine; metadata should state the chosen reference configuration.", }, { id: "two-stroke-single-engine", title: "Two-Stroke Single-Cylinder Engine", category: "engine", implementationTier: 2, complexity: "intermediate", assetStrategy: "procedural-first", engineeringFocus: [ "crankcase scavenging", "port timing", "power stroke every revolution", ], learningOutcomes: [ "Explain how intake and exhaust ports replace poppet valves in a simple two-stroke.", "Identify why port exposure is controlled by piston position.", ], primaryMotion: "One crank revolution completes compression, combustion, exhaust, transfer, and intake events.", components: [ { key: "cylinder-barrel", label: "Cylinder barrel and head", role: "Contains the bore, cooling fins, combustion chamber, and port windows.", defaultMaterial: "cast-metal", explodeAxis: [0, 0.8, 0], sectionCritical: true, }, { key: "piston-rod-crank", label: "Piston, rod, and crank", role: "Forms the single reciprocating power mechanism.", defaultMaterial: "brushed-steel", explodeAxis: [0, -0.3, 0], sectionCritical: true, }, { key: "crankcase", label: "Sealed crankcase", role: "Pre-compresses mixture before transfer into the cylinder.", defaultMaterial: "cast-metal", explodeAxis: [0, -0.9, 0], sectionCritical: true, }, { key: "reed-intake", label: "Reed valve and intake tract", role: "Allows mixture into the crankcase while limiting reverse flow.", defaultMaterial: "composite", explodeAxis: [-1, 0, 0], }, { key: "transfer-exhaust-ports", label: "Transfer and exhaust ports", role: "Timed openings exposed and covered by the piston skirt.", defaultMaterial: "cutaway-highlight", explodeAxis: [1, 0, 0], sectionCritical: true, }, ], animationChannels: [ { key: "crank-and-piston", label: "Crank and piston", driver: "reciprocating", relation: "one full engine cycle per crank revolution", notes: "Unlike a four-stroke, the educational overlay repeats every 360 degrees.", }, { key: "port-exposure", label: "Port exposure", driver: "linear", relation: "visibleArea = portWindow ∩ pistonSkirtClearance(y)", notes: "Mask transfer and exhaust ports as the piston uncovers them.", }, { key: "scavenging-flow", label: "Scavenging flow", driver: "fluid-flow", relation: "flow arrows activate when transfer and exhaust ports overlap", notes: "Use color-coded fresh mixture and exhaust arrows to teach short-circuiting risk.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "fluid-flow", ], explodedViewPlan: "Lift cylinder barrel and head from crankcase, pull intake tract sideways, and keep piston/rod/crank together for motion continuity.", crossSectionPlan: "Default vertical cut passes through intake, transfer port, exhaust port, piston crown, and crankcase volume.", cameraPresets: [ { key: "port-cutaway", label: "Port cutaway", position: [3.2, 1.8, 4.6], target: [0, 0.3, 0], notes: "Best for showing port exposure timing.", }, { key: "crankcase-flow", label: "Crankcase flow", position: [3.8, -0.5, 3.2], target: [0, -0.35, 0], notes: "Focuses on pre-compression and reed valve behavior.", }, { key: "finned-cylinder", label: "Cooling fins", position: [-3.6, 2.2, 3.8], target: [0, 0.8, 0], notes: "Shows air-cooled packaging details.", }, ], validationChecks: [ "Cycle overlay repeats every 360 degrees, not every 720 degrees.", "Transfer flow only appears when piston skirt uncovers the transfer ports.", "Reed valve opens during crankcase intake and closes during compression.", ], sourceAssetNotes: "Can be built procedurally with cylinders, ports, and fin arrays before commissioning detailed GLB castings.", }, { id: "radial-aircraft-engine", title: "Radial Aircraft Engine", category: "engine", implementationTier: 4, complexity: "expert", assetStrategy: "hybrid-glb", engineeringFocus: [ "master-and-articulating rod geometry", "radial cylinder packaging", "air-cooled cylinder repetition", ], learningOutcomes: [ "Describe how one master rod coordinates multiple articulating rods.", "Explain why radial engines package cylinders around the crankcase.", ], primaryMotion: "A central crank and master rod assembly drives multiple pistons arranged radially around the crankcase.", components: [ { key: "central-crankcase", label: "Central crankcase", role: "Houses crankshaft, master rod big end, bearings, and oil distribution.", defaultMaterial: "cast-metal", explodeAxis: [0, 0, -1], sectionCritical: true, }, { key: "master-rod-assembly", label: "Master and articulating rods", role: "Transmits crank motion to all radial pistons from a shared master rod.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0, 0.8], sectionCritical: true, }, { key: "cylinder-bank", label: "Radial cylinder bank", role: "Repeating air-cooled cylinders arranged around the crankcase.", defaultMaterial: "cast-metal", explodeAxis: [0, 0.8, 0], sectionCritical: true, }, { key: "pushrod-ring", label: "Pushrods and rocker housings", role: "Transfers cam ring motion to overhead valves on each cylinder.", defaultMaterial: "brushed-steel", explodeAxis: [0.7, 0.7, 0], }, { key: "propeller-hub", label: "Propeller hub", role: "Visible output load that makes engine speed and torque legible.", defaultMaterial: "painted-metal", explodeAxis: [0, 0, 1.2], }, ], animationChannels: [ { key: "crank-propeller", label: "Crankshaft and propeller", driver: "rotary", relation: "propellerAngle = crankAngle", notes: "Can be slowed independently for learning mode blur control.", }, { key: "master-rod-kinematics", label: "Master rod kinematics", driver: "reciprocating", relation: "each piston follows its cylinder axis from shared crank throw", notes: "The master rod must orbit; articulating rods should pivot around their link pins.", }, { key: "pushrod-valve-events", label: "Pushrod valve events", driver: "cam-profile", relation: "cam ring drives paired pushrods per cylinder at four-stroke timing", notes: "Use simplified lift timing but keep pushrod phase tied to cylinder firing sequence.", }, ], requiredViewerModes: ["solid", "wireframe", "exploded", "section", "ghosted"], explodedViewPlan: "Fan cylinders slightly outward from crankcase, pull propeller forward, and expose master rod assembly without disconnecting link pivots.", crossSectionPlan: "Radial section removes one cylinder wedge to reveal piston, rod link, crankcase, and cam ring.", cameraPresets: [ { key: "front-radial", label: "Front radial layout", position: [0, 1.8, 7.2], target: [0, 0.2, 0], notes: "Shows radial symmetry and propeller output.", }, { key: "master-rod", label: "Master rod detail", position: [3.2, 1.4, 3.8], target: [0, 0, 0], notes: "Explains shared crank throw geometry.", }, { key: "cylinder-fins", label: "Cylinder fin detail", position: [-4.8, 2.3, 4.1], target: [-1, 0.8, 0], notes: "Highlights air-cooling and repeated cylinder modules.", }, ], validationChecks: [ "Every piston motion is constrained to its own radial cylinder axis.", "Articulating rods pivot from master rod pins rather than sharing the crank pin.", "Exploded cylinder offsets preserve radial ordering and do not overlap adjacent cylinders.", ], sourceAssetNotes: "Use instanced cylinder modules for performance; keep rods as individually pivoted meshes for accurate kinematics.", riskNotes: "This entry has high mesh count and should be delayed until instancing and LOD policy are proven.", }, { id: "wankel-rotary-engine", title: "Wankel Rotary Engine", category: "engine", implementationTier: 2, complexity: "advanced", assetStrategy: "procedural-first", engineeringFocus: [ "epitrochoid housing geometry", "eccentric shaft output", "three moving combustion chambers", ], learningOutcomes: [ "Describe how the triangular rotor creates changing chamber volumes.", "Relate rotor orbital motion to eccentric shaft speed.", ], primaryMotion: "A triangular rotor orbits inside an epitrochoid housing while spinning at one third eccentric shaft speed.", components: [ { key: "epitrochoid-housing", label: "Epitrochoid housing", role: "Creates the sealed two-lobed path followed by the rotor apex seals.", defaultMaterial: "cast-metal", explodeAxis: [0, 0, -0.8], sectionCritical: true, }, { key: "triangular-rotor", label: "Triangular rotor", role: "Separates three chambers that expand and contract during rotation.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0, 0.7], sectionCritical: true, }, { key: "eccentric-shaft", label: "Eccentric shaft", role: "Receives output torque from the rotor through an offset journal.", defaultMaterial: "brushed-steel", explodeAxis: [0, -0.6, 0], sectionCritical: true, }, { key: "apex-seals", label: "Apex seals", role: "Maintain chamber separation at each rotor tip.", defaultMaterial: "cutaway-highlight", explodeAxis: [0.4, 0.4, 0], sectionCritical: true, }, { key: "intake-exhaust-ports", label: "Intake and exhaust ports", role: "Fixed housing openings uncovered by rotor motion.", defaultMaterial: "painted-metal", explodeAxis: [-0.7, 0, 0], }, ], animationChannels: [ { key: "eccentric-shaft", label: "Eccentric shaft rotation", driver: "rotary", relation: "shaftAngle = inputAngle", notes: "Primary output clock.", }, { key: "rotor-orbit", label: "Rotor orbit and spin", driver: "rotary", relation: "rotorCenterAngle = shaftAngle; rotorSpin = -shaftAngle / 3", notes: "The negative one-third relation is the key educational motion contract.", }, { key: "chamber-volume", label: "Chamber volume overlays", driver: "thermal-flow", relation: "three chamber phases offset by 120 rotor degrees", notes: "Use translucent volume fills for intake, compression, combustion, and exhaust.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "motion-trails", ], explodedViewPlan: "Separate front housing plate, rotor, eccentric shaft, rear plate, and seals along the shaft axis like a service manual stack.", crossSectionPlan: "Use an axial cutaway through the housing face so the rotor path, ports, and chamber volumes remain visible.", cameraPresets: [ { key: "front-housing", label: "Front housing", position: [0, 0.6, 5.6], target: [0, 0, 0], notes: "Best for explaining rotor-to-housing geometry.", }, { key: "shaft-offset", label: "Eccentric shaft offset", position: [3.2, 1.6, 3.8], target: [0, 0, 0], notes: "Shows output shaft and eccentric journal relationship.", }, { key: "port-timing", label: "Port timing", position: [-3.4, 1.2, 4.2], target: [-0.4, 0, 0], notes: "Frames intake and exhaust port exposure.", }, ], validationChecks: [ "Rotor spin remains exactly opposite one third of eccentric shaft angle.", "Apex seal markers stay near the epitrochoid housing boundary.", "Three chamber overlays remain phase-offset by 120 degrees.", ], sourceAssetNotes: "Procedural epitrochoid and rotor meshes are preferable to static CAD imports because the geometry must align exactly with animation math.", }, { id: "steam-locomotive-cylinder", title: "Steam Locomotive Cylinder and Valve Gear", category: "engine", implementationTier: 3, complexity: "advanced", assetStrategy: "hybrid-glb", engineeringFocus: [ "double-acting piston", "crosshead guidance", "valve lead and cutoff", ], learningOutcomes: [ "Trace how reciprocating piston motion becomes wheel rotation.", "Explain how valve timing admits steam alternately to each side of the piston.", ], primaryMotion: "Driving wheel rotation moves a side rod and crosshead while valve gear phase controls steam admission.", components: [ { key: "cylinder-chest", label: "Cylinder and steam chest", role: "Contains the power piston and valve ports for both piston sides.", defaultMaterial: "cast-metal", explodeAxis: [0, 0.5, 0], sectionCritical: true, }, { key: "piston-crosshead", label: "Piston rod and crosshead", role: "Transfers straight-line piston force to the connecting rod while resisting side loads.", defaultMaterial: "brushed-steel", explodeAxis: [1, 0, 0], sectionCritical: true, }, { key: "connecting-side-rods", label: "Connecting and side rods", role: "Link the crosshead to the driving wheel crank pin.", defaultMaterial: "brushed-steel", explodeAxis: [0, -0.4, 0], sectionCritical: true, }, { key: "slide-valve", label: "Slide valve", role: "Times steam admission and exhaust through port openings.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.9, 0], sectionCritical: true, }, { key: "driving-wheel", label: "Driving wheel", role: "Receives torque and provides the visible output rotation.", defaultMaterial: "painted-metal", explodeAxis: [0, 0, -1], }, ], animationChannels: [ { key: "wheel-rotation", label: "Driving wheel rotation", driver: "rotary", relation: "wheelAngle = inputAngle", notes: "Primary clock for rods and valve gear.", }, { key: "crosshead-reciprocation", label: "Crosshead reciprocation", driver: "reciprocating", relation: "x = crankRadius*cos(theta) + rodLengthCorrection(theta)", notes: "Crosshead must remain constrained to the guide rails without side drift.", }, { key: "valve-lap-lead", label: "Valve lap, lead, and cutoff", driver: "oscillating", relation: "valveTravel = eccentricPhase(theta + advanceAngle)", notes: "Overlay port opening state so cutoff is understandable at slow speeds.", }, { key: "steam-flow", label: "Steam flow", driver: "fluid-flow", relation: "flow side alternates based on slide valve port exposure", notes: "Color live steam and exhaust steam differently.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "fluid-flow", "motion-trails", ], explodedViewPlan: "Pull cylinder cover and steam chest outward, offset rods away from wheel plane, and separate wheel slightly to expose crank pin relationships.", crossSectionPlan: "Longitudinal cut through cylinder, steam chest, and ports with the crosshead guide left intact.", cameraPresets: [ { key: "side-motion", label: "Side rod motion", position: [0, 1.8, 7.4], target: [0, 0.25, 0], notes: "Classic locomotive side elevation for rod kinematics.", }, { key: "steam-chest", label: "Steam chest cutaway", position: [4.2, 2.6, 4.6], target: [0.8, 0.6, 0], notes: "Focuses on valve timing and port exposure.", }, { key: "crosshead-guide", label: "Crosshead guide", position: [-4.0, 1.5, 3.8], target: [-0.5, 0.1, 0], notes: "Shows why guidance prevents rod side load on the piston.", }, ], validationChecks: [ "Crosshead translation remains perfectly collinear with the cylinder axis.", "Valve flow overlay switches sides only when corresponding ports are open.", "Connecting rod big end remains attached to driving wheel crank pin.", ], sourceAssetNotes: "Model a representative single-cylinder side assembly; mirrored opposite-side expansion can be a later variant.", }, { id: "gas-turbine-core", title: "Gas Turbine Core", category: "engine", implementationTier: 4, complexity: "expert", assetStrategy: "cad-derived-glb", engineeringFocus: [ "Brayton cycle", "compressor and turbine stage coupling", "hot-section cooling", ], learningOutcomes: [ "Follow air through compression, combustion, expansion, and exhaust.", "Distinguish compressor work extraction from turbine power generation.", ], primaryMotion: "One or more concentric spools rotate compressor and turbine stages while fluid overlays show pressure and temperature changes.", components: [ { key: "compressor-stages", label: "Axial compressor stages", role: "Progressively raise inlet air pressure through alternating rotor and stator rows.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0, -1], sectionCritical: true, }, { key: "combustor-can-annulus", label: "Combustor", role: "Adds heat energy to compressed air before turbine expansion.", defaultMaterial: "cutaway-highlight", explodeAxis: [0, 0.8, 0], sectionCritical: true, }, { key: "turbine-stages", label: "Turbine stages", role: "Extract work from hot gas to drive the compressor and output shaft.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0, 1], sectionCritical: true, }, { key: "spools-bearings", label: "Spools and bearings", role: "Connect rotating compressor and turbine assemblies through the engine centerline.", defaultMaterial: "brushed-steel", explodeAxis: [0, -0.4, 0], sectionCritical: true, }, { key: "casing-nozzles", label: "Casing, stators, and nozzles", role: "Directs flow, carries loads, and controls expansion angles.", defaultMaterial: "composite", explodeAxis: [0, 1, 0], }, ], animationChannels: [ { key: "spool-rotation", label: "Spool rotation", driver: "rotary", relation: "lowSpoolAngle = input; highSpoolAngle = input * speedRatio", notes: "Support at least two independently colored spool groups for future turbofan variants.", }, { key: "pressure-flow", label: "Pressure flow overlay", driver: "fluid-flow", relation: "pressure rises across compressor and drops across turbine", notes: "Use arrow density and blue-to-orange gradient to teach the Brayton cycle.", }, { key: "combustion-heat", label: "Combustion heat overlay", driver: "thermal-flow", relation: "temperature peaks in combustor and decays through turbine stages", notes: "Educational overlay only; do not imply real flame shape at every operating point.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "fluid-flow", "ghosted", ], explodedViewPlan: "Separate compressor, combustor, turbine, and casing modules along the engine axis with enough spacing to compare stage order.", crossSectionPlan: "Long axial half-section exposes rotor rows, stators, combustor liner, turbine nozzles, and concentric shafts.", cameraPresets: [ { key: "axial-cutaway", label: "Axial cutaway", position: [6.2, 2.8, 6.8], target: [0, 0, 0], notes: "Primary teaching view for stage order and flow direction.", }, { key: "compressor-front", label: "Compressor front", position: [0, 1.2, -7.2], target: [0, 0, -1.4], notes: "Shows inlet guide vanes and early compressor stages.", }, { key: "hot-section", label: "Hot section", position: [-5.2, 2.5, 5.2], target: [0.7, 0, 1.5], notes: "Focuses on combustor and turbine handoff.", }, ], validationChecks: [ "Flow overlay direction is always compressor to combustor to turbine.", "Rotating rows and static rows are visually distinguishable.", "Section mode keeps centerline shafts visible rather than clipping them away entirely.", ], sourceAssetNotes: "Use aggressive mesh decimation and instancing for blade rows; raw CAD blade counts will exceed the viewer performance budget.", riskNotes: "Thermodynamic overlays are qualitative in milestone scope unless backed by a separate simulation dataset.", }, { id: "manual-synchromesh-transmission", title: "Manual Synchromesh Transmission", category: "gearbox", implementationTier: 1, complexity: "advanced", assetStrategy: "hybrid-glb", engineeringFocus: [ "constant-mesh gears", "synchronizer cones", "selector fork actuation", ], learningOutcomes: [ "Trace torque through the selected gear pair.", "Explain why synchronizers match speeds before dog teeth engage.", ], primaryMotion: "Input shaft drives a layshaft cluster continuously while selector sleeves lock one output gear at a time.", components: [ { key: "input-shaft", label: "Input shaft and clutch gear", role: "Receives engine torque and drives the layshaft cluster.", defaultMaterial: "brushed-steel", explodeAxis: [-1, 0, 0], sectionCritical: true, }, { key: "layshaft-cluster", label: "Layshaft gear cluster", role: "Carries fixed gears that remain in constant mesh with output gears.", defaultMaterial: "brushed-steel", explodeAxis: [0, -0.7, 0], sectionCritical: true, }, { key: "output-gears", label: "Output shaft free gears", role: "Spin freely until locked to the output shaft by a synchronizer sleeve.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.7, 0], sectionCritical: true, }, { key: "synchronizer-sleeves", label: "Synchronizer hubs and sleeves", role: "Friction-match gear speed and then mechanically lock dog teeth.", defaultMaterial: "cutaway-highlight", explodeAxis: [0, 1.1, 0], sectionCritical: true, }, { key: "selector-forks", label: "Selector forks and rails", role: "Translate driver shift input into sleeve movement.", defaultMaterial: "painted-metal", explodeAxis: [0, 1.5, 0], }, ], animationChannels: [ { key: "constant-mesh-rotation", label: "Constant-mesh gear rotation", driver: "gear-train", relation: "gearOmega = inputOmega * toothCountRatio with alternating signs", notes: "All meshed gears rotate even when not selected; only torque highlight changes.", }, { key: "synchronizer-shift", label: "Synchronizer sleeve shift", driver: "linear", relation: "sleeveX = neutral | selectedGearOffset", notes: "Add a short cone-friction phase before dog-teeth lock in the animation timeline.", }, { key: "torque-path", label: "Selected torque path", driver: "load-path", relation: "highlight input -> layshaft -> selected free gear -> sleeve -> output", notes: "Highlight must update instantly when URL state or control panel selected gear changes.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "load-path", ], explodedViewPlan: "Separate casing halves, lift selector rail assembly, offset shafts vertically, and leave meshed gear pairs close enough to understand tooth engagement.", crossSectionPlan: "Longitudinal section through all shafts with casing shell clipped and synchronizer cones exposed.", cameraPresets: [ { key: "torque-flow", label: "Torque flow", position: [5.8, 3.1, 5.4], target: [0, 0.2, 0], notes: "Best for showing selected gear path.", }, { key: "synchronizer-detail", label: "Synchronizer detail", position: [3.1, 2.2, 2.8], target: [0.8, 0.3, 0], notes: "Frames sleeve, hub, cone, and dog teeth.", }, { key: "shift-rails", label: "Shift rails", position: [0.4, 5.1, 3.8], target: [0, 0.9, 0], notes: "Shows fork-to-sleeve actuation.", }, ], validationChecks: [ "Meshed gears counter-rotate according to tooth ratio.", "Only one synchronizer sleeve can be engaged at a time.", "Torque path highlight changes when selected gear changes.", ], sourceAssetNotes: "Gear teeth can be simplified with normal maps for distant views, but pitch circles and pivots must be exact.", }, { id: "planetary-automatic-gearset", title: "Planetary Automatic Gearset", category: "gearbox", implementationTier: 1, complexity: "advanced", assetStrategy: "procedural-first", engineeringFocus: [ "epicyclic speed relationships", "locked-member ratios", "compact coaxial packaging", ], learningOutcomes: [ "Predict output direction when the sun, carrier, or ring is held.", "Explain why planet gears spin and orbit simultaneously.", ], primaryMotion: "Sun, planet carrier, and ring gear exchange speed depending on which member is driven, held, or output.", components: [ { key: "sun-gear", label: "Sun gear", role: "Central gear that meshes with all planet gears.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0, -0.8], sectionCritical: true, }, { key: "planet-gears", label: "Planet gears", role: "Spin on their own axes while orbiting with the carrier.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.7, 0], sectionCritical: true, }, { key: "planet-carrier", label: "Planet carrier", role: "Holds planet pins and can act as input or output member.", defaultMaterial: "painted-metal", explodeAxis: [0, -0.5, 0], sectionCritical: true, }, { key: "ring-gear", label: "Internal ring gear", role: "Outer internal-tooth member for reduction, overdrive, or reverse relationships.", defaultMaterial: "cast-metal", explodeAxis: [0, 0, 0.8], sectionCritical: true, }, { key: "clutch-brake-elements", label: "Clutch and brake elements", role: "Select which member is driven, held, or connected to output.", defaultMaterial: "cutaway-highlight", explodeAxis: [0.9, 0, 0], }, ], animationChannels: [ { key: "epicyclic-solver", label: "Epicyclic speed solver", driver: "gear-train", relation: "(Nr + Ns) * carrierOmega = Nr * ringOmega + Ns * sunOmega", notes: "This formula should be the source of truth for all mode animations.", }, { key: "planet-spin-orbit", label: "Planet spin and orbit", driver: "rotary", relation: "planetCenterAngle = carrierAngle; spin from sun/ring mesh speed", notes: "Planet transforms need nested groups for carrier orbit and local spin.", }, { key: "member-locking", label: "Member locking", driver: "load-path", relation: "mode selects heldMember, inputMember, outputMember", notes: "Render held member with brake highlight and torque path with distinct color.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "load-path", ], explodedViewPlan: "Spread the coaxial stack along the shaft axis: ring gear, planet carrier, planet gears, sun gear, and clutch/brake pack.", crossSectionPlan: "Radial cut removes part of ring gear and carrier plate so planet mesh and internal teeth are visible.", cameraPresets: [ { key: "front-epicyclic", label: "Epicyclic front view", position: [0, 1.2, 5.4], target: [0, 0, 0], notes: "Shows sun, planets, carrier, and ring relationship.", }, { key: "member-stack", label: "Member stack", position: [4.2, 2.1, 3.2], target: [0, 0, 0], notes: "Good for exploded coaxial layout.", }, { key: "clutch-brake", label: "Clutch and brake selection", position: [-3.8, 2.4, 3.8], target: [0.7, 0, 0], notes: "Frames held/input/output member overlays.", }, ], validationChecks: [ "Speed relationship satisfies the epicyclic equation for every preset mode.", "Planet gears maintain mesh contact while orbiting.", "Held member angle remains constant in locked-member demonstrations.", ], sourceAssetNotes: "Procedural gear pitch geometry is required so tooth counts match the speed solver.", }, { id: "open-differential", title: "Open Automotive Differential", category: "gearbox", implementationTier: 1, complexity: "intermediate", assetStrategy: "hybrid-glb", engineeringFocus: [ "bevel gear torque split", "carrier rotation", "wheel speed differentiation", ], learningOutcomes: [ "Trace torque from pinion to ring gear to axle side gears.", "Explain how left and right wheels can rotate at different speeds while cornering.", ], primaryMotion: "The ring gear rotates a carrier that drives spider gears, allowing side gears to average left and right axle speeds.", components: [ { key: "drive-pinion", label: "Drive pinion", role: "Inputs torque from the driveshaft into the ring gear.", defaultMaterial: "brushed-steel", explodeAxis: [-1, 0, 0], sectionCritical: true, }, { key: "ring-gear-carrier", label: "Ring gear and carrier", role: "Rotates the differential case and spider gear cross shaft.", defaultMaterial: "painted-metal", explodeAxis: [0, 0, -0.7], sectionCritical: true, }, { key: "spider-gears", label: "Spider gears", role: "Permit relative speed between left and right side gears.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.6, 0], sectionCritical: true, }, { key: "side-gears-axles", label: "Side gears and axle stubs", role: "Deliver torque to left and right wheels.", defaultMaterial: "brushed-steel", explodeAxis: [1, 0, 0], sectionCritical: true, }, { key: "differential-housing", label: "Differential housing", role: "Supports bearings and contains lubricant.", defaultMaterial: "cast-metal", explodeAxis: [0, -0.9, 0], }, ], animationChannels: [ { key: "pinion-ring-drive", label: "Pinion to ring drive", driver: "gear-train", relation: "ringOmega = -pinionOmega * pinionTeeth / ringTeeth", notes: "Use bevel gear axes rather than parallel spur gear assumptions.", }, { key: "carrier-side-average", label: "Carrier and side gear average", driver: "gear-train", relation: "carrierOmega = (leftAxleOmega + rightAxleOmega) / 2", notes: "This averaging relation should drive straight, cornering, and one-wheel-held presets.", }, { key: "spider-spin", label: "Spider gear spin", driver: "rotary", relation: "spiderSpin proportional to leftAxleOmega - rightAxleOmega", notes: "Spider gears do not spin relative to carrier during equal wheel speed mode.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "load-path", ], explodedViewPlan: "Split housing shells, pull axle stubs outward, offset carrier forward, and separate pinion along driveshaft axis.", crossSectionPlan: "Cut housing and carrier to reveal bevel gear contact, spider cross shaft, and side gear mesh.", cameraPresets: [ { key: "torque-input", label: "Torque input", position: [4.6, 2.4, 4.6], target: [0, 0, 0], notes: "Shows pinion entering ring gear.", }, { key: "spider-gears", label: "Spider gear detail", position: [2.8, 1.6, 3.1], target: [0, 0, 0], notes: "Focuses on differential action.", }, { key: "axle-output", label: "Axle outputs", position: [0, 2.0, 5.2], target: [0, 0, 0], notes: "Frames left/right side gear speed difference.", }, ], validationChecks: [ "Carrier speed equals the average of left and right axle speeds.", "Spider spin is zero in straight-line equal-wheel-speed mode.", "Pinion and ring gear rotate with correct bevel ratio and opposite sense.", ], sourceAssetNotes: "GLB housing can be detailed, but gears should keep procedural pivots and named axes.", }, { id: "worm-reduction-gearbox", title: "Worm Reduction Gearbox", category: "gearbox", implementationTier: 2, complexity: "intermediate", assetStrategy: "procedural-first", engineeringFocus: [ "right-angle power transfer", "high reduction ratio", "sliding tooth contact", ], learningOutcomes: [ "Explain how worm lead angle determines reduction ratio.", "Identify why many worm drives are difficult to back-drive.", ], primaryMotion: "A screw-like worm shaft drives a perpendicular worm wheel at a large speed reduction.", components: [ { key: "worm-shaft", label: "Worm shaft", role: "Input screw gear with helical thread form.", defaultMaterial: "brushed-steel", explodeAxis: [-1, 0, 0], sectionCritical: true, }, { key: "worm-wheel", label: "Worm wheel", role: "Output gear with teeth shaped for sliding contact with the worm.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.8, 0], sectionCritical: true, }, { key: "bearings", label: "Shaft bearings", role: "Support axial and radial loads created by the sliding mesh.", defaultMaterial: "brushed-steel", explodeAxis: [0, -0.6, 0], }, { key: "gearbox-housing", label: "Gearbox housing", role: "Maintains shaft spacing and contains lubrication.", defaultMaterial: "cast-metal", explodeAxis: [0, -1, 0], }, { key: "lubricant-bath", label: "Lubricant bath", role: "Reduces heat and wear at the sliding tooth contact.", defaultMaterial: "oil-film", explodeAxis: [0, -1.2, 0], sectionCritical: true, }, ], animationChannels: [ { key: "worm-drive", label: "Worm drive", driver: "gear-train", relation: "wheelOmega = wormOmega * wormStarts / wheelTeeth", notes: "Animate thread travel so users can see screw action, not only shaft rotation.", }, { key: "contact-patch", label: "Sliding contact patch", driver: "load-path", relation: "contact highlight advances along worm thread and wheel tooth face", notes: "Important for explaining friction and lubrication demand.", }, { key: "lubricant-splash", label: "Lubricant splash", driver: "fluid-flow", relation: "splash intensity proportional to wormOmega", notes: "Keep subtle; this is an educational overlay, not a CFD simulation.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "load-path", ], explodedViewPlan: "Lift top housing cover, slide worm shaft out along input axis, raise worm wheel along output axis, and drop lubricant volume.", crossSectionPlan: "Cut through both shaft centerlines to show right-angle mesh and lubricant fill level.", cameraPresets: [ { key: "right-angle", label: "Right-angle drive", position: [4.4, 3.0, 4.4], target: [0, 0, 0], notes: "Shows perpendicular input and output axes.", }, { key: "tooth-contact", label: "Tooth contact", position: [2.6, 1.5, 2.3], target: [0, 0.2, 0], notes: "Focuses on sliding mesh patch.", }, { key: "housing-section", label: "Housing section", position: [-4.0, 2.4, 3.6], target: [0, -0.2, 0], notes: "Shows lubrication and bearing support.", }, ], validationChecks: [ "Output wheel speed equals worm starts divided by wheel tooth count.", "Input and output axes remain perpendicular.", "Contact highlight follows the worm lead direction.", ], sourceAssetNotes: "Use procedural worm/wheel pitch geometry for educational accuracy; decorative housing can be GLB.", }, { id: "cycloidal-reducer", title: "Cycloidal Speed Reducer", category: "gearbox", implementationTier: 3, complexity: "expert", assetStrategy: "procedural-first", engineeringFocus: [ "eccentric orbit", "rolling pin engagement", "high-ratio compact reduction", ], learningOutcomes: [ "Explain how an eccentric input produces slow counter-rotation of the cycloidal disc.", "Trace how output pins average disc motion into smooth shaft rotation.", ], primaryMotion: "An eccentric input bearing makes a cycloidal disc orbit inside fixed ring pins while output pins extract reduced rotation.", components: [ { key: "eccentric-input", label: "Eccentric input shaft and bearing", role: "Offsets the cycloidal disc center from the output axis.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0, -0.8], sectionCritical: true, }, { key: "cycloidal-disc", label: "Cycloidal disc", role: "Lobed disc that rolls against fixed ring pins.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.8, 0], sectionCritical: true, }, { key: "ring-pins", label: "Fixed ring pins", role: "Stationary pins defining the rolling reduction geometry.", defaultMaterial: "brushed-steel", explodeAxis: [0, -0.8, 0], sectionCritical: true, }, { key: "output-pins", label: "Output pins and carrier", role: "Transmit the disc's slow rotation to the output shaft.", defaultMaterial: "painted-metal", explodeAxis: [0, 0, 0.9], sectionCritical: true, }, { key: "reducer-housing", label: "Reducer housing", role: "Holds ring pins, bearings, and seals in alignment.", defaultMaterial: "cast-metal", explodeAxis: [0, -1.2, 0], }, ], animationChannels: [ { key: "eccentric-orbit", label: "Eccentric orbit", driver: "rotary", relation: "discCenter = eccentricRadius * [cos(theta), sin(theta)]", notes: "Represent orbit as translation of the disc group before local counter-rotation.", }, { key: "disc-counter-rotation", label: "Disc counter-rotation", driver: "gear-train", relation: "discAngle = -theta / lobeCount for one-pin-difference design", notes: "Expose lobe/pin counts in facts panel because ratio derives from them.", }, { key: "output-pin-averaging", label: "Output pin averaging", driver: "rotary", relation: "outputAngle = reduced disc rotation extracted through pin holes", notes: "Output pins should appear to roll inside oversized disc holes.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "motion-trails", ], explodedViewPlan: "Separate along shaft axis: input eccentric, first disc, ring pin plate, output carrier, and housing cover.", crossSectionPlan: "Face cutaway removes front cover and a sector of the disc to expose ring-pin engagement and output pin holes.", cameraPresets: [ { key: "face-reduction", label: "Face reduction view", position: [0, 1.1, 5.5], target: [0, 0, 0], notes: "Best for seeing orbit and counter-rotation.", }, { key: "eccentric-bearing", label: "Eccentric bearing", position: [3.2, 1.8, 3.2], target: [0, 0, 0], notes: "Explains input offset.", }, { key: "output-pins", label: "Output pins", position: [-3.3, 2.0, 3.4], target: [0, 0, 0], notes: "Shows pin-hole averaging.", }, ], validationChecks: [ "Disc center orbits once per input revolution.", "Disc local rotation is opposite input direction for one-pin-difference reducer.", "Output carrier rotates at the documented reduction ratio.", ], sourceAssetNotes: "Generate disc lobes and pin circles procedurally from ratio parameters; imported geometry risks mismatch with solver math.", }, { id: "belt-cvt", title: "Belt Continuously Variable Transmission", category: "gearbox", implementationTier: 2, complexity: "intermediate", assetStrategy: "hybrid-glb", engineeringFocus: [ "variable effective pulley radius", "belt tension", "continuous ratio change", ], learningOutcomes: [ "Explain how moving pulley sheaves changes the effective drive radius.", "Relate pulley radius ratio to input/output speed ratio.", ], primaryMotion: "Adjustable conical pulley sheaves change belt contact radius to vary speed ratio continuously.", components: [ { key: "drive-pulley", label: "Drive pulley sheaves", role: "Input pulley whose sheave spacing controls belt ride radius.", defaultMaterial: "brushed-steel", explodeAxis: [-0.9, 0, 0], sectionCritical: true, }, { key: "driven-pulley", label: "Driven pulley sheaves", role: "Output pulley that changes radius opposite the drive pulley.", defaultMaterial: "brushed-steel", explodeAxis: [0.9, 0, 0], sectionCritical: true, }, { key: "steel-belt", label: "Steel belt or chain", role: "Transfers torque through tension and compression elements.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.7, 0], sectionCritical: true, }, { key: "sheave-actuators", label: "Hydraulic sheave actuators", role: "Move pulley halves to command target ratio.", defaultMaterial: "painted-metal", explodeAxis: [0, -0.6, 0], }, { key: "transmission-case", label: "Transmission case", role: "Contains pulleys, belt, bearings, and oil passages.", defaultMaterial: "cast-metal", explodeAxis: [0, -1, 0], }, ], animationChannels: [ { key: "pulley-rotation", label: "Pulley rotation", driver: "belt", relation: "outputOmega = inputOmega * driveRadius / drivenRadius", notes: "Speed ratio must update continuously during ratio sweep.", }, { key: "sheave-translation", label: "Sheave translation", driver: "linear", relation: "driveGap closes as drivenGap opens", notes: "Animate both pulley halves symmetrically around their center planes.", }, { key: "belt-loop", label: "Belt loop travel", driver: "belt", relation: "belt path tangent to current effective pulley radii", notes: "Rebuild or morph belt curve as ratio changes; avoid belt clipping into sheaves.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "motion-trails", ], explodedViewPlan: "Remove case cover, separate pulley halves slightly, lift belt loop upward, and expose actuator pistons below.", crossSectionPlan: "Section through both pulley axes to show conical sheaves and changing belt ride height.", cameraPresets: [ { key: "ratio-overview", label: "Ratio overview", position: [4.8, 2.8, 4.8], target: [0, 0, 0], notes: "Shows both pulley radii and belt loop.", }, { key: "sheave-section", label: "Sheave section", position: [3.5, 1.8, 2.8], target: [-0.8, 0, 0], notes: "Explains conical pulley gap change.", }, { key: "belt-path", label: "Belt path", position: [0, 4.6, 3.4], target: [0, 0, 0], notes: "Top-down view for path tangent validation.", }, ], validationChecks: [ "Drive and driven effective radii change inversely.", "Output speed follows current driveRadius/drivenRadius relation.", "Belt curve remains tangent to both pulleys across full ratio sweep.", ], sourceAssetNotes: "Use procedural belt path and sheave positions; GLB can supply case and actuator details.", }, { id: "centrifugal-pump", title: "Centrifugal Pump", category: "pump", implementationTier: 1, complexity: "intermediate", assetStrategy: "hybrid-glb", engineeringFocus: [ "radial impeller acceleration", "volute pressure recovery", "shaft seal support", ], learningOutcomes: [ "Follow fluid from axial inlet through radial impeller discharge.", "Explain why the volute area increases around the casing.", ], primaryMotion: "A rotating impeller accelerates fluid outward into a volute that converts velocity into pressure.", components: [ { key: "impeller", label: "Impeller", role: "Adds kinetic energy to fluid through rotating vanes.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0, 0.8], sectionCritical: true, }, { key: "volute-casing", label: "Volute casing", role: "Collects impeller discharge and recovers pressure as area expands.", defaultMaterial: "cast-metal", explodeAxis: [0, -0.8, 0], sectionCritical: true, }, { key: "suction-nozzle", label: "Suction nozzle", role: "Routes inlet flow axially into the impeller eye.", defaultMaterial: "painted-metal", explodeAxis: [-1, 0, 0], }, { key: "discharge-nozzle", label: "Discharge nozzle", role: "Delivers pressurized flow tangentially from the volute.", defaultMaterial: "painted-metal", explodeAxis: [1, 0, 0], }, { key: "shaft-seal-bearing", label: "Shaft, seal, and bearing housing", role: "Supports the rotating impeller and prevents leakage along the shaft.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0, -0.9], sectionCritical: true, }, ], animationChannels: [ { key: "impeller-rotation", label: "Impeller rotation", driver: "rotary", relation: "impellerAngle = inputAngle", notes: "Primary clock for flow arrows and blade motion blur.", }, { key: "radial-flow", label: "Radial flow", driver: "fluid-flow", relation: "fluid vectors move axial inlet -> impeller eye -> radial vane exit", notes: "Use arrow curvature to show fluid leaving tangentially, not straight out.", }, { key: "pressure-gradient", label: "Pressure gradient", driver: "fluid-flow", relation: "pressureColor increases through volute toward discharge", notes: "Qualitative gradient is sufficient for catalogue viewer; avoid claiming pump curve accuracy.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "fluid-flow", ], explodedViewPlan: "Remove front casing cover, pull impeller along shaft axis, offset seal/bearing housing backward, and separate inlet/outlet nozzles.", crossSectionPlan: "Half-section through shaft centerline and volute tongue to reveal impeller eye, vane passages, and casing expansion.", cameraPresets: [ { key: "volute-cutaway", label: "Volute cutaway", position: [4.2, 2.8, 4.6], target: [0, 0, 0], notes: "Best for flow path and pressure recovery.", }, { key: "impeller-eye", label: "Impeller eye", position: [0, 1.2, 5.2], target: [0, 0, 0], notes: "Shows axial inlet and vane entry.", }, { key: "seal-bearing", label: "Seal and bearing", position: [-3.7, 2.0, 3.4], target: [0, 0, -0.8], notes: "Explains shaft support and leakage control.", }, ], validationChecks: [ "Fluid arrows enter axially and leave radially/tangentially.", "Pressure color increases toward discharge nozzle.", "Impeller rotates without intersecting the cutaway casing.", ], sourceAssetNotes: "Impeller and flow arrows should be procedural for accurate pivots; casing can be optimized GLB.", }, { id: "external-gear-pump", title: "External Gear Pump", category: "pump", implementationTier: 2, complexity: "intermediate", assetStrategy: "procedural-first", engineeringFocus: [ "positive displacement", "trapped tooth volume", "pressure ripple", ], learningOutcomes: [ "Explain how fluid is carried around the outside of meshing gears.", "Identify why fluid does not pass through the gear mesh centerline.", ], primaryMotion: "Two meshing gears trap fluid at the inlet and carry it around the housing perimeter to the outlet.", components: [ { key: "drive-gear", label: "Drive gear", role: "Input gear that drives the idler and traps inlet fluid between teeth.", defaultMaterial: "brushed-steel", explodeAxis: [-0.5, 0.7, 0], sectionCritical: true, }, { key: "idler-gear", label: "Idler gear", role: "Counter-rotating gear completing the positive-displacement chambers.", defaultMaterial: "brushed-steel", explodeAxis: [0.5, 0.7, 0], sectionCritical: true, }, { key: "pump-housing", label: "Close-clearance housing", role: "Seals tooth chambers against the gear outside diameters.", defaultMaterial: "cast-metal", explodeAxis: [0, -0.9, 0], sectionCritical: true, }, { key: "inlet-outlet-ports", label: "Inlet and outlet ports", role: "Admit low-pressure fluid and discharge high-pressure fluid.", defaultMaterial: "painted-metal", explodeAxis: [0, 0, 0.9], }, { key: "shafts-bushings", label: "Shafts and bushings", role: "Support gears while maintaining small clearances.", defaultMaterial: "brushed-steel", explodeAxis: [0, -0.4, 0], }, ], animationChannels: [ { key: "gear-mesh", label: "Gear mesh rotation", driver: "gear-train", relation: "idlerOmega = -driveOmega * driveTeeth / idlerTeeth", notes: "Usually equal tooth counts; retain formula for variants.", }, { key: "trapped-volume", label: "Trapped volume transport", driver: "fluid-flow", relation: "fluid packets follow outer tooth spaces from inlet to outlet", notes: "Do not draw primary flow through gear mesh; that is the central misconception to prevent.", }, { key: "pressure-ripple", label: "Pressure ripple", driver: "fluid-flow", relation: "outlet pulse frequency = gearToothCount * shaftFrequency", notes: "Small pulsing opacity communicates positive displacement without detailed hydraulics.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "fluid-flow", ], explodedViewPlan: "Remove top cover, lift gears on their shafts, offset port plate, and leave housing cavity visible.", crossSectionPlan: "Planar section through gear faces and port centerline to show close clearances and trapped chambers.", cameraPresets: [ { key: "gear-face", label: "Gear face", position: [0, 1.4, 5.3], target: [0, 0, 0], notes: "Best for outer-path fluid transport.", }, { key: "clearance-section", label: "Clearance section", position: [3.6, 2.2, 3.6], target: [0, 0, 0], notes: "Shows housing-to-gear sealing clearance.", }, { key: "port-flow", label: "Port flow", position: [-3.3, 2.0, 3.8], target: [0, 0, 0], notes: "Frames inlet/outlet pressure arrows.", }, ], validationChecks: [ "Meshing gears counter-rotate and maintain tooth phase.", "Fluid packets travel around the outside, not through the gear mesh.", "Outlet pulse frequency follows tooth count times shaft frequency.", ], sourceAssetNotes: "Procedural involute-like teeth are sufficient; priority is correct tooth chamber path and mesh phasing.", }, { id: "axial-piston-pump", title: "Axial Piston Pump", category: "pump", implementationTier: 3, complexity: "expert", assetStrategy: "hybrid-glb", engineeringFocus: [ "swash plate displacement", "rotating cylinder barrel", "valve plate timing", ], learningOutcomes: [ "Explain how swash plate angle controls piston stroke and pump displacement.", "Trace cylinder ports across inlet and outlet kidney slots.", ], primaryMotion: "A rotating cylinder barrel carries pistons over an angled swash plate, producing reciprocation and timed port flow.", components: [ { key: "drive-shaft-barrel", label: "Drive shaft and cylinder barrel", role: "Rotates the piston bores past valve plate ports.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0, -0.8], sectionCritical: true, }, { key: "pistons-slippers", label: "Pistons and slipper shoes", role: "Reciprocate axially as slippers ride on the swash plate.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.8, 0], sectionCritical: true, }, { key: "swash-plate", label: "Swash plate", role: "Angled reaction surface that sets piston stroke.", defaultMaterial: "painted-metal", explodeAxis: [0, -0.7, 0], sectionCritical: true, }, { key: "valve-plate", label: "Valve plate", role: "Connects each cylinder alternately to inlet and outlet ports.", defaultMaterial: "cutaway-highlight", explodeAxis: [0, 0, 0.9], sectionCritical: true, }, { key: "pump-case", label: "Pump case", role: "Supports rotating group, ports, and case drain passages.", defaultMaterial: "cast-metal", explodeAxis: [0, -1, 0], }, ], animationChannels: [ { key: "barrel-rotation", label: "Cylinder barrel rotation", driver: "rotary", relation: "barrelAngle = shaftAngle", notes: "Piston bores orbit around the pump axis.", }, { key: "swash-reciprocation", label: "Swash-plate piston reciprocation", driver: "reciprocating", relation: "pistonStroke = tan(swashAngle) * radius * cos(barrelAngle + phase)", notes: "Expose swash angle as a future control because it directly changes displacement.", }, { key: "valve-port-flow", label: "Valve plate port flow", driver: "fluid-flow", relation: "cylinder connects to inlet/outlet based on angular kidney slot overlap", notes: "Highlight active inlet and outlet cylinders as the barrel rotates.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "fluid-flow", ], explodedViewPlan: "Separate rotating group along shaft axis, fan pistons slightly outward, offset swash plate, and expose valve plate ports.", crossSectionPlan: "Axial cut through shaft, one piston bore, swash plate contact, and valve plate kidney slots.", cameraPresets: [ { key: "rotating-group", label: "Rotating group", position: [4.4, 2.6, 4.8], target: [0, 0, 0], notes: "Shows barrel, pistons, and swash plate together.", }, { key: "valve-plate", label: "Valve plate timing", position: [0, 1.2, 5.2], target: [0, 0, 0.6], notes: "Best for inlet/outlet kidney slot explanation.", }, { key: "swash-angle", label: "Swash angle", position: [-3.8, 2.4, 3.8], target: [0, -0.1, 0], notes: "Frames displacement control.", }, ], validationChecks: [ "Piston stroke amplitude scales with swash plate angle.", "Each cylinder alternates between inlet and outlet slots over one revolution.", "Slipper shoes remain visually constrained to the swash plate plane.", ], sourceAssetNotes: "Keep piston groups procedural with named bore phases; case and port block can be GLB shells.", riskNotes: "A physically convincing slipper/swash contact requires nested transforms and should be reviewed early.", }, { id: "reciprocating-piston-pump", title: "Reciprocating Piston Pump", category: "pump", implementationTier: 2, complexity: "intermediate", assetStrategy: "procedural-first", engineeringFocus: [ "positive displacement", "check valve timing", "pulsating flow smoothing", ], learningOutcomes: [ "Explain why inlet and outlet check valves open at opposite stroke phases.", "Relate piston motion to pulsating discharge flow.", ], primaryMotion: "A crank drives a piston in a cylinder while check valves route fluid during suction and discharge strokes.", components: [ { key: "pump-cylinder", label: "Pump cylinder", role: "Contains the piston and working fluid volume.", defaultMaterial: "cast-metal", explodeAxis: [0, 0.6, 0], sectionCritical: true, }, { key: "piston-plunger", label: "Piston or plunger", role: "Changes chamber volume to draw in and discharge fluid.", defaultMaterial: "brushed-steel", explodeAxis: [1, 0, 0], sectionCritical: true, }, { key: "crank-rod", label: "Crank and connecting rod", role: "Converts rotary input into piston reciprocation.", defaultMaterial: "brushed-steel", explodeAxis: [0, -0.5, 0], sectionCritical: true, }, { key: "check-valves", label: "Inlet and outlet check valves", role: "Automatically open based on pressure differential.", defaultMaterial: "cutaway-highlight", explodeAxis: [0, 1, 0], sectionCritical: true, }, { key: "air-chamber", label: "Discharge air chamber", role: "Damps pulsating flow in many reciprocating pump installations.", defaultMaterial: "painted-metal", explodeAxis: [-0.8, 0, 0], }, ], animationChannels: [ { key: "crank-piston", label: "Crank-driven piston", driver: "reciprocating", relation: "pistonX = crankRadius*cos(theta) + rodLengthCorrection(theta)", notes: "Use exact slider-crank math shared with mechanism module.", }, { key: "check-valve-state", label: "Check valve state", driver: "fluid-flow", relation: "inlet opens during suction; outlet opens during discharge", notes: "Valve animation should be pressure-state driven, not merely time toggled.", }, { key: "flow-pulses", label: "Flow pulses", driver: "fluid-flow", relation: "discharge pulse peaks near maximum piston velocity on discharge stroke", notes: "Air chamber can smooth pulse opacity to demonstrate damping.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "fluid-flow", ], explodedViewPlan: "Open cylinder cover, separate crankcase, lift check valves, and keep piston/rod attached to illustrate kinematic linkage.", crossSectionPlan: "Longitudinal section through cylinder, piston, both check valves, and discharge air chamber.", cameraPresets: [ { key: "pump-cycle", label: "Pump cycle", position: [4.2, 2.4, 4.2], target: [0, 0, 0], notes: "Shows piston and valves together.", }, { key: "valve-detail", label: "Check valve detail", position: [2.6, 2.0, 2.8], target: [0.4, 0.5, 0], notes: "Frames inlet/outlet valve state.", }, { key: "crank-drive", label: "Crank drive", position: [-3.8, 1.8, 3.6], target: [-0.5, -0.1, 0], notes: "Focuses on rotary-to-linear conversion.", }, ], validationChecks: [ "Inlet and outlet check valves are never open as primary flow paths at the same time.", "Discharge flow occurs during decreasing chamber volume.", "Piston remains constrained to cylinder axis.", ], sourceAssetNotes: "Strong candidate for fully procedural geometry and shared slider-crank animation utilities.", }, { id: "peristaltic-pump", title: "Peristaltic Pump", category: "pump", implementationTier: 2, complexity: "introductory", assetStrategy: "procedural-first", engineeringFocus: [ "occlusion-driven displacement", "sterile tube isolation", "roller compression sequence", ], learningOutcomes: [ "Explain why fluid contacts only the flexible tube interior.", "Trace a fluid slug as rollers squeeze it from inlet to outlet.", ], primaryMotion: "Rotating rollers progressively occlude a flexible tube, pushing trapped fluid pockets along the tube.", components: [ { key: "rotor-hub", label: "Rotor hub", role: "Carries multiple rollers around the pump center.", defaultMaterial: "painted-metal", explodeAxis: [0, 0, 0.7], sectionCritical: true, }, { key: "rollers", label: "Compression rollers", role: "Pinch the tube to create moving occlusions.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.8, 0], sectionCritical: true, }, { key: "flexible-tube", label: "Flexible tube", role: "Contains the fluid and deforms under each roller.", defaultMaterial: "rubber", explodeAxis: [0, -0.7, 0], sectionCritical: true, }, { key: "pump-raceway", label: "Curved raceway", role: "Supports the tube against roller compression.", defaultMaterial: "cast-metal", explodeAxis: [0, -1, 0], }, { key: "inlet-outlet-tails", label: "Inlet and outlet tube tails", role: "Connect the pump head to the external fluid path.", defaultMaterial: "rubber", explodeAxis: [1, 0, 0], }, ], animationChannels: [ { key: "roller-rotation", label: "Roller rotation", driver: "rotary", relation: "rollerCenterAngle = rotorAngle + rollerIndexOffset", notes: "Rollers orbit with rotor and may spin locally for contact realism.", }, { key: "tube-occlusion", label: "Tube occlusion", driver: "linear", relation: "tubeRadiusAtAngle = baseRadius - compression near roller contact", notes: "Use shader or morph targets to visibly flatten the tube at contact patches.", }, { key: "fluid-slug", label: "Fluid slug transport", driver: "fluid-flow", relation: "slug position follows occlusion wave from inlet to outlet", notes: "Fluid overlay should never pass through an occluded tube segment.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "fluid-flow", ], explodedViewPlan: "Lift rotor and rollers forward, offset flexible tube from raceway, and leave inlet/outlet tails connected for path clarity.", crossSectionPlan: "Face cutaway removes pump cover and optionally slices the tube at one occluded segment.", cameraPresets: [ { key: "front-occlusion", label: "Front occlusion view", position: [0, 1.0, 5.2], target: [0, 0, 0], notes: "Best for roller sequence and tube compression.", }, { key: "tube-section", label: "Tube section", position: [3.2, 2.0, 3.4], target: [0.6, 0, 0], notes: "Shows fluid isolation in the tube.", }, { key: "slug-path", label: "Fluid slug path", position: [-3.4, 2.1, 3.6], target: [0, 0, 0], notes: "Frames inlet-to-outlet motion.", }, ], validationChecks: [ "Tube occlusion follows roller contact locations.", "Fluid slug movement stops at occluded segments and advances with the roller wave.", "Roller spacing remains equal around the rotor.", ], sourceAssetNotes: "Use procedural curves and tube deformation; static GLB tubing will not communicate the mechanism.", }, { id: "slider-crank-mechanism", title: "Slider-Crank Mechanism", category: "mechanism", implementationTier: 1, complexity: "introductory", assetStrategy: "procedural-first", engineeringFocus: [ "rotary-to-linear conversion", "connecting rod angularity", "non-sinusoidal slider dwell", ], learningOutcomes: [ "Convert crank angle into slider displacement.", "Identify where slider velocity is maximum and zero.", ], primaryMotion: "A rotating crank pin drives a connecting rod that constrains a slider to linear motion.", components: [ { key: "ground-frame", label: "Ground frame and guide", role: "Defines fixed pivots and slider path.", defaultMaterial: "painted-metal", explodeAxis: [0, -0.8, 0], }, { key: "crank", label: "Crank", role: "Input link rotating about the ground pivot.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.6, 0], sectionCritical: true, }, { key: "connecting-rod", label: "Connecting rod", role: "Links crank pin to slider pin while changing angle over the cycle.", defaultMaterial: "brushed-steel", explodeAxis: [0, 1, 0], sectionCritical: true, }, { key: "slider", label: "Slider block", role: "Output element constrained to translate along the guide.", defaultMaterial: "cutaway-highlight", explodeAxis: [1, 0, 0], sectionCritical: true, }, { key: "joint-pins", label: "Joint pins", role: "Make revolute connections visible for learners.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0, 0.5], }, ], animationChannels: [ { key: "crank-input", label: "Crank input", driver: "rotary", relation: "theta = inputAngle", notes: "Shared reference motion for other crank-driven machines.", }, { key: "rod-angle", label: "Connecting rod angle", driver: "oscillating", relation: "rodAngle = atan2(crankY, sliderX - crankX)", notes: "Rod should be solved from joint positions, not separately keyed.", }, { key: "slider-displacement", label: "Slider displacement", driver: "linear", relation: "x = r*cos(theta) + sqrt(rodLength^2 - (r*sin(theta))^2)", notes: "Use exact displacement equation so velocity and dwell overlays are correct.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "motion-trails", "load-path", ], explodedViewPlan: "Offset links vertically while retaining ghosted joint lines from original pivot positions.", crossSectionPlan: "No heavy section required; use a guide rail slice to show the slider constraint.", cameraPresets: [ { key: "planar-overview", label: "Planar overview", position: [0, 5.4, 4.2], target: [0.5, 0, 0], notes: "Orthographic-like view for mechanism geometry.", }, { key: "slider-guide", label: "Slider guide", position: [3.4, 2.2, 3.4], target: [1.2, 0, 0], notes: "Shows linear constraint.", }, { key: "crank-pin", label: "Crank pin", position: [-2.8, 2.0, 2.8], target: [-0.4, 0, 0], notes: "Frames rotating input joint.", }, ], validationChecks: [ "Distance between crank pin and slider pin equals connecting rod length at all angles.", "Slider has zero displacement off the guide axis.", "Motion-trail extrema match top and bottom dead-center positions.", ], sourceAssetNotes: "Implement first as a pure procedural reference mechanism; reuse math in engines and pumps.", }, { id: "scotch-yoke-mechanism", title: "Scotch Yoke Mechanism", category: "mechanism", implementationTier: 2, complexity: "introductory", assetStrategy: "procedural-first", engineeringFocus: [ "pure sinusoidal linear motion", "slot contact", "compact reciprocating drive", ], learningOutcomes: [ "Compare Scotch yoke displacement with slider-crank displacement.", "Explain how the crank pin slides inside the yoke slot.", ], primaryMotion: "A crank pin running in a slotted yoke produces sinusoidal linear reciprocation.", components: [ { key: "fixed-frame", label: "Fixed frame and guides", role: "Constrains the yoke to linear travel.", defaultMaterial: "painted-metal", explodeAxis: [0, -0.8, 0], }, { key: "drive-crank", label: "Drive crank", role: "Rotates the pin that drives the slot.", defaultMaterial: "brushed-steel", explodeAxis: [-0.5, 0.6, 0], sectionCritical: true, }, { key: "crank-pin", label: "Crank pin roller", role: "Slides vertically within the yoke slot while pushing horizontally.", defaultMaterial: "brushed-steel", explodeAxis: [0, 1, 0], sectionCritical: true, }, { key: "slotted-yoke", label: "Slotted yoke", role: "Output member with a slot that converts rotation to linear motion.", defaultMaterial: "cutaway-highlight", explodeAxis: [0.8, 0, 0], sectionCritical: true, }, { key: "output-rod", label: "Output rod", role: "Transfers yoke reciprocation to an external load.", defaultMaterial: "brushed-steel", explodeAxis: [1.2, 0, 0], }, ], animationChannels: [ { key: "crank-rotation", label: "Crank rotation", driver: "rotary", relation: "theta = inputAngle", notes: "Constant angular input.", }, { key: "yoke-translation", label: "Yoke translation", driver: "linear", relation: "x = crankRadius * cos(theta)", notes: "Unlike slider-crank, displacement is exactly sinusoidal.", }, { key: "slot-slide", label: "Slot sliding contact", driver: "linear", relation: "pinSlotY = crankRadius * sin(theta)", notes: "Show contact patch moving up and down the slot.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "motion-trails", "load-path", ], explodedViewPlan: "Separate crank, pin roller, yoke, and output rod slightly in depth while drawing ghosted contact normals.", crossSectionPlan: "Use a shallow slot cutaway to expose the pin roller inside the yoke.", cameraPresets: [ { key: "sinusoidal-overview", label: "Sinusoidal overview", position: [0, 5.2, 4.0], target: [0.3, 0, 0], notes: "Best for displacement comparison with slider-crank.", }, { key: "slot-contact", label: "Slot contact", position: [2.8, 2.4, 2.9], target: [0.2, 0, 0], notes: "Shows sliding contact point.", }, { key: "output-stroke", label: "Output stroke", position: [-3.2, 2.0, 3.2], target: [0.8, 0, 0], notes: "Frames yoke and output rod travel.", }, ], validationChecks: [ "Yoke displacement follows r*cos(theta) exactly.", "Crank pin remains inside slot boundaries at all times.", "Motion trail is symmetric about the guide centerline.", ], sourceAssetNotes: "Fully procedural model is sufficient and useful as a comparison reference for slider-crank math.", }, { id: "geneva-drive", title: "Geneva Drive", category: "mechanism", implementationTier: 1, complexity: "intermediate", assetStrategy: "procedural-first", engineeringFocus: [ "intermittent indexing", "dwell period", "locking surface geometry", ], learningOutcomes: [ "Explain how continuous rotation becomes intermittent output indexing.", "Identify the dwell interval where the Geneva wheel is locked.", ], primaryMotion: "A rotating drive pin enters Geneva wheel slots to index the output wheel, then a locking disc holds position during dwell.", components: [ { key: "drive-wheel", label: "Drive wheel", role: "Rotates continuously and carries the indexing pin.", defaultMaterial: "brushed-steel", explodeAxis: [-0.8, 0, 0], sectionCritical: true, }, { key: "drive-pin", label: "Drive pin", role: "Engages each Geneva slot to rotate the output wheel.", defaultMaterial: "cutaway-highlight", explodeAxis: [0, 0.8, 0], sectionCritical: true, }, { key: "geneva-wheel", label: "Geneva wheel", role: "Output wheel with radial slots for intermittent indexing.", defaultMaterial: "painted-metal", explodeAxis: [0.8, 0, 0], sectionCritical: true, }, { key: "locking-disc", label: "Locking disc", role: "Fits into concave surfaces to hold the Geneva wheel during dwell.", defaultMaterial: "brushed-steel", explodeAxis: [0, -0.7, 0], sectionCritical: true, }, { key: "index-markers", label: "Index markers", role: "Visual reference for step angle and dwell positions.", defaultMaterial: "cutaway-highlight", explodeAxis: [0, 0, 0.5], }, ], animationChannels: [ { key: "driver-rotation", label: "Driver rotation", driver: "rotary", relation: "driverAngle = inputAngle", notes: "Continuous input clock.", }, { key: "geneva-index", label: "Geneva index motion", driver: "oscillating", relation: "outputAngle steps by 360 / slotCount during pin engagement", notes: "Output is piecewise: stationary during dwell, rapidly indexed during engagement.", }, { key: "dwell-lock", label: "Dwell lock", driver: "load-path", relation: "lock active when pin is outside any slot", notes: "Highlight locking surfaces during dwell to show why output cannot drift.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "motion-trails", "load-path", ], explodedViewPlan: "Offset driver, pin, Geneva wheel, and locking disc along depth while keeping ghosted outlines at operating plane.", crossSectionPlan: "No full section required; use slot edge highlighting and optional face clipping for engagement detail.", cameraPresets: [ { key: "indexing-front", label: "Indexing front view", position: [0, 1.1, 5.4], target: [0, 0, 0], notes: "Primary view for intermittent motion.", }, { key: "pin-entry", label: "Pin entry detail", position: [2.8, 2.1, 3.0], target: [0.25, 0.2, 0], notes: "Shows slot engagement geometry.", }, { key: "dwell-lock", label: "Dwell lock view", position: [-3.2, 2.0, 3.2], target: [-0.2, 0, 0], notes: "Frames locking disc during dwell.", }, ], validationChecks: [ "Output wheel remains stationary during dwell intervals.", "Each index step equals 360 degrees divided by slot count.", "Drive pin enters and exits slots without geometry intersection.", ], sourceAssetNotes: "Generate wheel slots from slot count so step angle, geometry, and labels remain consistent.", }, { id: "cam-and-follower", title: "Cam and Follower", category: "mechanism", implementationTier: 1, complexity: "intermediate", assetStrategy: "procedural-first", engineeringFocus: [ "lift profile", "rise-dwell-return", "spring preload", ], learningOutcomes: [ "Read a cam lift curve from follower motion.", "Distinguish rise, dwell, and return segments in one cam revolution.", ], primaryMotion: "A rotating cam profile drives a constrained follower through a prescribed lift curve.", components: [ { key: "camshaft", label: "Camshaft", role: "Rotates the cam profile around a fixed axis.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0, -0.7], sectionCritical: true, }, { key: "cam-lobe", label: "Cam lobe", role: "Encodes follower displacement as a radial profile.", defaultMaterial: "cutaway-highlight", explodeAxis: [0, 0.7, 0], sectionCritical: true, }, { key: "roller-follower", label: "Roller follower", role: "Maintains rolling contact with the cam profile.", defaultMaterial: "brushed-steel", explodeAxis: [0.8, 0, 0], sectionCritical: true, }, { key: "follower-guide", label: "Follower guide", role: "Constrains follower motion to a straight line.", defaultMaterial: "painted-metal", explodeAxis: [0, -0.7, 0], }, { key: "return-spring", label: "Return spring", role: "Maintains contact force between follower and cam.", defaultMaterial: "brushed-steel", explodeAxis: [-0.8, 0, 0], sectionCritical: true, }, ], animationChannels: [ { key: "cam-rotation", label: "Cam rotation", driver: "rotary", relation: "camAngle = inputAngle", notes: "Primary clock for lift curve.", }, { key: "follower-lift", label: "Follower lift", driver: "cam-profile", relation: "lift = camProfile(camAngle) with rise/dwell/return segments", notes: "Lift curve should be data-defined, not hard-coded to a circle.", }, { key: "spring-compression", label: "Spring compression", driver: "spring", relation: "springLength = freeLength - lift", notes: "Spring coils should scale/compress visibly but not invert.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "motion-trails", "load-path", ], explodedViewPlan: "Separate camshaft, lobe, follower, guide, and spring along depth with a ghosted operating profile left behind.", crossSectionPlan: "Section follower guide and spring housing to expose follower travel.", cameraPresets: [ { key: "lift-profile", label: "Lift profile", position: [0, 1.2, 5.0], target: [0, 0, 0], notes: "Best for cam profile and follower motion.", }, { key: "roller-contact", label: "Roller contact", position: [2.9, 2.0, 3.1], target: [0.2, 0.2, 0], notes: "Shows contact point and normal force.", }, { key: "spring-return", label: "Spring return", position: [-3.1, 2.1, 3.2], target: [-0.2, 0.4, 0], notes: "Explains preload and return force.", }, ], validationChecks: [ "Follower displacement matches the configured cam profile.", "Dwell segments keep follower height constant.", "Spring compression remains positive and tied to follower lift.", ], sourceAssetNotes: "Represent cam profile from a sampled polar curve so alternate profiles can be added without remodeling.", }, { id: "four-bar-linkage", title: "Four-Bar Linkage", category: "mechanism", implementationTier: 2, complexity: "intermediate", assetStrategy: "procedural-first", engineeringFocus: [ "closed-chain kinematics", "coupler curves", "Grashof condition", ], learningOutcomes: [ "Identify ground, crank, coupler, and rocker links.", "Explain how link lengths determine whether a full crank rotation is possible.", ], primaryMotion: "A rotating input crank drives a closed chain of links, producing rocker oscillation and coupler point paths.", components: [ { key: "ground-link", label: "Ground link", role: "Fixed distance between input and output pivots.", defaultMaterial: "painted-metal", explodeAxis: [0, -0.8, 0], }, { key: "input-crank", label: "Input crank", role: "Driven link rotating about the ground pivot.", defaultMaterial: "brushed-steel", explodeAxis: [-0.6, 0.6, 0], sectionCritical: true, }, { key: "coupler-link", label: "Coupler link", role: "Floating link connecting crank and rocker joints.", defaultMaterial: "brushed-steel", explodeAxis: [0, 1, 0], sectionCritical: true, }, { key: "output-rocker", label: "Output rocker", role: "Oscillating output link about the second ground pivot.", defaultMaterial: "cutaway-highlight", explodeAxis: [0.6, 0.6, 0], sectionCritical: true, }, { key: "coupler-point", label: "Coupler point marker", role: "Traces a useful path for mechanism synthesis examples.", defaultMaterial: "cutaway-highlight", explodeAxis: [0, 0, 0.5], }, ], animationChannels: [ { key: "input-crank-angle", label: "Input crank angle", driver: "rotary", relation: "theta = inputAngle when linkage satisfies Grashof crank condition", notes: "For non-Grashof examples, future controls should restrict invalid rotation ranges.", }, { key: "closed-chain-solve", label: "Closed-chain solve", driver: "oscillating", relation: "solve coupler-rocker joint from circle-circle intersection", notes: "Use geometric constraint solving so link lengths remain constant.", }, { key: "coupler-curve", label: "Coupler curve", driver: "motion-trails", relation: "trace selected point on coupler over input cycle", notes: "Persist trail in educational mode to teach path generation.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "motion-trails", "load-path", ], explodedViewPlan: "Offset each planar link along the view depth with dashed constraint lines to original pivots.", crossSectionPlan: "No volumetric section required; pin stack cutaways can expose revolute joints.", cameraPresets: [ { key: "linkage-plan", label: "Linkage plan", position: [0, 5.5, 4.1], target: [0, 0, 0], notes: "Orthographic-style view for link lengths.", }, { key: "coupler-path", label: "Coupler path", position: [3.2, 2.3, 3.3], target: [0.2, 0.2, 0], notes: "Shows tracer point and path.", }, { key: "rocker-output", label: "Rocker output", position: [-3.2, 2.2, 3.4], target: [0.8, 0, 0], notes: "Frames output oscillation.", }, ], validationChecks: [ "All link lengths remain constant during animation.", "Closed chain remains connected at every joint.", "Coupler path uses the selected coupler point transform, not approximated screen-space drawing.", ], sourceAssetNotes: "Procedural link lengths enable future interactive mechanism synthesis controls.", }, { id: "rack-and-pinion-steering", title: "Rack and Pinion Steering", category: "mechanism", implementationTier: 2, complexity: "intermediate", assetStrategy: "hybrid-glb", engineeringFocus: [ "rotary-to-linear gear mesh", "tie rod linkage", "Ackermann steering geometry", ], learningOutcomes: [ "Trace steering wheel rotation through pinion and rack translation.", "Explain why inner and outer wheels steer at different angles in a turn.", ], primaryMotion: "A steering pinion translates a rack that pushes tie rods and rotates wheel knuckles.", components: [ { key: "steering-pinion", label: "Steering pinion", role: "Small gear driven by the steering column.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.7, 0], sectionCritical: true, }, { key: "steering-rack", label: "Linear rack", role: "Translates left/right as the pinion rotates.", defaultMaterial: "brushed-steel", explodeAxis: [0, -0.7, 0], sectionCritical: true, }, { key: "tie-rods", label: "Tie rods", role: "Connect rack ends to steering arms.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0, 0.8], sectionCritical: true, }, { key: "knuckles-wheels", label: "Steering knuckles and wheels", role: "Rotate about kingpin axes to steer the vehicle.", defaultMaterial: "rubber", explodeAxis: [1, 0, 0], sectionCritical: true, }, { key: "rack-housing", label: "Rack housing", role: "Supports rack bushings and protects the gear mesh.", defaultMaterial: "cast-metal", explodeAxis: [0, -1, 0], }, ], animationChannels: [ { key: "pinion-rack", label: "Pinion to rack", driver: "gear-train", relation: "rackX = pinionAngle * pinionPitchRadius", notes: "No slip at pitch circle; rack direction depends on pinion orientation.", }, { key: "tie-rod-kinematics", label: "Tie rod kinematics", driver: "oscillating", relation: "tie rod endpoints solve from rack end and steering arm pivot", notes: "Tie rods must retain length during wheel steering.", }, { key: "ackermann-angle", label: "Ackermann steering angle", driver: "oscillating", relation: "innerWheelAngle > outerWheelAngle for nonzero turn radius", notes: "Use simplified geometry but show the inner/outer angle difference.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "motion-trails", ], explodedViewPlan: "Open rack housing, lift pinion, offset rack forward, and spread tie rods/wheel knuckles outward symmetrically.", crossSectionPlan: "Section the rack housing at the pinion mesh and optionally ghost wheel assemblies.", cameraPresets: [ { key: "steering-overview", label: "Steering overview", position: [0, 4.4, 6.0], target: [0, 0, 0], notes: "Shows full rack, tie rods, and wheels.", }, { key: "gear-mesh", label: "Gear mesh", position: [2.8, 2.0, 2.8], target: [0, 0, 0], notes: "Focuses on pinion and rack teeth.", }, { key: "ackermann", label: "Ackermann geometry", position: [-4.8, 3.2, 5.0], target: [0, 0, 0], notes: "Frames inner/outer wheel angle difference.", }, ], validationChecks: [ "Rack displacement equals pinion pitch radius times pinion angle.", "Tie rod lengths remain constant through steering travel.", "Inner wheel angle exceeds outer wheel angle in a turn.", ], sourceAssetNotes: "Use simplified tire/knuckle geometry first; accurate linkage pivots matter more than cosmetic suspension detail.", }, { id: "universal-joint", title: "Universal Joint", category: "mechanism", implementationTier: 2, complexity: "intermediate", assetStrategy: "procedural-first", engineeringFocus: [ "misaligned shaft coupling", "nonuniform output velocity", "cross spider articulation", ], learningOutcomes: [ "Explain how torque crosses between shafts at an angle.", "Identify why a single universal joint creates output speed variation.", ], primaryMotion: "Two yokes connected by a cross spider transmit rotation between angled shafts with nonuniform output speed.", components: [ { key: "input-shaft-yoke", label: "Input shaft and yoke", role: "Driven fork attached to the input shaft.", defaultMaterial: "brushed-steel", explodeAxis: [-0.9, 0, 0], sectionCritical: true, }, { key: "cross-spider", label: "Cross spider", role: "Orthogonal trunnions allowing both yokes to articulate.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.8, 0], sectionCritical: true, }, { key: "output-shaft-yoke", label: "Output shaft and yoke", role: "Receives torque at an angle from the input yoke.", defaultMaterial: "brushed-steel", explodeAxis: [0.9, 0, 0], sectionCritical: true, }, { key: "needle-bearings", label: "Needle bearing caps", role: "Support spider trunnions in the yoke ears.", defaultMaterial: "brushed-steel", explodeAxis: [0, -0.6, 0], }, { key: "angle-fixture", label: "Angle fixture", role: "Adjustable reference bracket showing shaft misalignment angle.", defaultMaterial: "painted-metal", explodeAxis: [0, -1, 0], }, ], animationChannels: [ { key: "input-rotation", label: "Input rotation", driver: "rotary", relation: "inputAngle = theta", notes: "Constant-speed reference.", }, { key: "output-nonuniformity", label: "Output nonuniform speed", driver: "rotary", relation: "tan(outputAngle) = tan(inputAngle) * cos(jointAngle)", notes: "Plot instantaneous output speed to show why paired joints are used in driveshafts.", }, { key: "spider-articulation", label: "Spider articulation", driver: "oscillating", relation: "spider trunnions remain aligned with yoke bearing axes", notes: "Nested rotations should preserve trunnion contact with bearing caps.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "motion-trails", "load-path", ], explodedViewPlan: "Separate yokes along shaft axes, lift bearing caps, and pull spider forward while keeping ghosted articulation axes.", crossSectionPlan: "Partial cut through bearing caps and yoke ears to show trunnion support.", cameraPresets: [ { key: "joint-angle", label: "Joint angle", position: [4.0, 2.8, 4.2], target: [0, 0, 0], notes: "Shows shaft misalignment clearly.", }, { key: "spider-detail", label: "Spider detail", position: [2.5, 2.0, 2.5], target: [0, 0, 0], notes: "Frames cross spider and bearings.", }, { key: "velocity-variation", label: "Velocity variation", position: [-4.2, 2.4, 4.0], target: [0, 0, 0], notes: "Best when paired with motion trail or graph overlay.", }, ], validationChecks: [ "Output angle follows the universal joint tangent relation for selected joint angle.", "Input and output yokes remain connected through spider trunnions.", "Speed variation disappears when joint angle is zero.", ], sourceAssetNotes: "Procedural yoke and spider pivots are preferable so future angle control remains reliable.", }, { id: "pratt-truss-bridge", title: "Pratt Truss Bridge Load Path", category: "structural", implementationTier: 3, complexity: "intermediate", assetStrategy: "procedural-first", engineeringFocus: [ "tension and compression members", "panel point loading", "support reactions", ], learningOutcomes: [ "Identify which truss members are in tension or compression under a central load.", "Explain why loads should enter a truss at panel points.", ], primaryMotion: "A movable load applies force to panel points while member colors and thickness show load path and axial force sign.", components: [ { key: "top-bottom-chords", label: "Top and bottom chords", role: "Primary longitudinal members resisting global bending through axial forces.", defaultMaterial: "painted-metal", explodeAxis: [0, 0.8, 0], sectionCritical: true, }, { key: "vertical-members", label: "Vertical members", role: "Transfer deck loads between chords at panel points.", defaultMaterial: "painted-metal", explodeAxis: [0, 0.5, 0], sectionCritical: true, }, { key: "diagonal-members", label: "Diagonal members", role: "Carry tension-dominant shear path in a Pratt arrangement.", defaultMaterial: "painted-metal", explodeAxis: [0, 0.3, 0], sectionCritical: true, }, { key: "deck-floor-beams", label: "Deck and floor beams", role: "Distribute vehicle loads into truss panel points.", defaultMaterial: "concrete", explodeAxis: [0, -0.6, 0], }, { key: "supports-loads", label: "Supports and movable load", role: "Defines boundary conditions and external force application.", defaultMaterial: "cutaway-highlight", explodeAxis: [0, -1, 0], sectionCritical: true, }, ], animationChannels: [ { key: "moving-load", label: "Moving load", driver: "linear", relation: "loadX snaps or interpolates between panel points", notes: "Panel-point snapping prevents implying arbitrary member bending behavior.", }, { key: "member-force-colors", label: "Member force colors", driver: "load-path", relation: "blue = tension; orange = compression; opacity = normalized axial force", notes: "Qualitative force solver can be precomputed per load position for milestone catalogue content.", }, { key: "support-reactions", label: "Support reactions", driver: "load-path", relation: "reaction arrows update from static equilibrium", notes: "Show vertical reactions at pin and roller supports.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "load-path", "ghosted", ], explodedViewPlan: "Separate deck downward, offset chords and web members slightly by member family, and keep node markers in place as reference.", crossSectionPlan: "Use member cross-section callouts rather than global clipping; optional cut on one joint gusset plate.", cameraPresets: [ { key: "elevation-load-path", label: "Elevation load path", position: [0, 4.6, 8.4], target: [0, 0.5, 0], notes: "Primary structural teaching view.", }, { key: "panel-point", label: "Panel point detail", position: [3.2, 2.8, 3.2], target: [1, 0.4, 0], notes: "Shows load introduction and member joints.", }, { key: "support-reactions", label: "Support reactions", position: [-5.0, 3.0, 4.2], target: [-2.5, 0, 0], notes: "Frames boundary conditions.", }, ], validationChecks: [ "Moving load positions align with panel point coordinates.", "Member force colors always include a legend for tension and compression.", "Support reaction arrows balance the applied vertical load.", ], sourceAssetNotes: "Procedural truss generation allows member forces, labels, and node IDs to remain synchronized.", }, { id: "tower-crane-slewing-jib", title: "Tower Crane Slewing Jib", category: "structural", implementationTier: 4, complexity: "advanced", assetStrategy: "hybrid-glb", engineeringFocus: [ "slewing ring rotation", "counterweight balance", "trolley and hoist motion", ], learningOutcomes: [ "Trace load path from hook through trolley, jib, slewing ring, mast, and foundation.", "Explain how counterweights reduce overturning moment.", ], primaryMotion: "A slewing ring rotates the jib, a trolley changes load radius, and a hoist raises the load while load-path overlays show moments.", components: [ { key: "mast-sections", label: "Mast sections", role: "Vertical tower carrying compressive load and overturning moment.", defaultMaterial: "painted-metal", explodeAxis: [0, -0.8, 0], sectionCritical: true, }, { key: "slewing-ring", label: "Slewing ring and turntable", role: "Allows upper crane structure to rotate relative to the mast.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0.8, 0], sectionCritical: true, }, { key: "jib-counter-jib", label: "Jib and counter-jib", role: "Horizontal truss arms supporting trolley load and counterweights.", defaultMaterial: "painted-metal", explodeAxis: [1, 0, 0], sectionCritical: true, }, { key: "trolley-hoist", label: "Trolley, hoist rope, and hook", role: "Moves load radius along the jib and lifts the payload.", defaultMaterial: "cable", explodeAxis: [0, 0, 0.8], sectionCritical: true, }, { key: "counterweights-load", label: "Counterweights and payload", role: "Visible masses used to explain moment balance.", defaultMaterial: "concrete", explodeAxis: [-1, 0, 0], sectionCritical: true, }, ], animationChannels: [ { key: "slewing-rotation", label: "Slewing rotation", driver: "rotary", relation: "upperStructureAngle = inputAngle", notes: "Rotate all upper assemblies around mast centerline.", }, { key: "trolley-travel", label: "Trolley travel", driver: "linear", relation: "trolleyX constrained along jib chord between stops", notes: "Load radius drives moment overlay.", }, { key: "hoist-lift", label: "Hoist lift", driver: "linear", relation: "hookY = commanded cable payout length", notes: "Cable length and hook position must stay connected.", }, { key: "moment-load-path", label: "Moment load path", driver: "load-path", relation: "overturningMoment = payloadWeight * trolleyRadius - counterweightMoment", notes: "Qualitative display with arrows and moment bars; not a crane certification calculator.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "load-path", "motion-trails", ], explodedViewPlan: "Separate mast modules vertically, lift slewing ring, offset jib/counter-jib, and drop trolley/hoist assembly for inspection.", crossSectionPlan: "Section slewing ring and mast base nodes; use callouts for lattice member force flow.", cameraPresets: [ { key: "full-crane", label: "Full crane overview", position: [8.0, 6.5, 8.0], target: [0, 2.5, 0], notes: "Frames whole system and load radius.", }, { key: "slewing-ring", label: "Slewing ring detail", position: [3.4, 4.2, 3.4], target: [0, 2.5, 0], notes: "Shows rotation bearing and load transfer into mast.", }, { key: "trolley-load", label: "Trolley and load", position: [5.4, 4.0, 4.2], target: [2.2, 2.0, 0], notes: "Explains radius and overturning moment.", }, ], validationChecks: [ "Upper crane components rotate together about mast centerline.", "Trolley remains constrained to jib path.", "Moment overlay increases with payload radius and decreases with counterweight contribution.", ], sourceAssetNotes: "Use instanced lattice members for mast and jib; detailed bolts are unnecessary at catalogue scale.", riskNotes: "Large scene extents require tuned camera near/far planes and label scaling.", }, { id: "hydraulic-excavator-arm", title: "Hydraulic Excavator Arm", category: "structural", implementationTier: 2, complexity: "advanced", assetStrategy: "hybrid-glb", engineeringFocus: [ "hydraulic cylinder actuation", "pin-jointed linkage", "bucket breakout force path", ], learningOutcomes: [ "Trace how cylinder extension changes boom, stick, and bucket angles.", "Identify pin joints and load paths through the arm structure.", ], primaryMotion: "Hydraulic cylinders extend and retract to rotate the boom, stick, and bucket around heavy pin joints.", components: [ { key: "upper-frame", label: "Upper frame and boom foot", role: "Fixed reference structure carrying boom pivot and cylinder mounts.", defaultMaterial: "painted-metal", explodeAxis: [0, -0.8, 0], sectionCritical: true, }, { key: "boom", label: "Boom", role: "Primary arm member lifting the stick and bucket assembly.", defaultMaterial: "painted-metal", explodeAxis: [0, 0.8, 0], sectionCritical: true, }, { key: "stick", label: "Stick", role: "Secondary arm member extending reach and carrying bucket linkage.", defaultMaterial: "painted-metal", explodeAxis: [0.8, 0, 0], sectionCritical: true, }, { key: "bucket-linkage", label: "Bucket and bucket linkage", role: "End effector and four-bar-like linkage for curl motion.", defaultMaterial: "cast-metal", explodeAxis: [1.1, 0, 0], sectionCritical: true, }, { key: "hydraulic-cylinders", label: "Hydraulic cylinders and hoses", role: "Linear actuators that command joint rotation through pin mounts.", defaultMaterial: "brushed-steel", explodeAxis: [0, 0, 0.9], sectionCritical: true, }, ], animationChannels: [ { key: "boom-cylinder", label: "Boom cylinder extension", driver: "hydraulic", relation: "boomAngle solved from cylinder length between frame and boom mounts", notes: "Use inverse linkage solve; do not directly key boom angle only.", }, { key: "stick-cylinder", label: "Stick cylinder extension", driver: "hydraulic", relation: "stickAngle solved from cylinder length between boom and stick mounts", notes: "Cylinder rod and barrel must remain collinear.", }, { key: "bucket-linkage", label: "Bucket linkage curl", driver: "hydraulic", relation: "bucketAngle solved through bucket cylinder and bellcrank links", notes: "Critical for demonstrating force multiplication at the bucket.", }, { key: "load-path", label: "Digging load path", driver: "load-path", relation: "bucket force resolves through bucket pins, stick, boom, cylinders, and frame", notes: "Show qualitative compression/tension arrows through links and cylinders.", }, ], requiredViewerModes: [ "solid", "wireframe", "exploded", "section", "load-path", "motion-trails", ], explodedViewPlan: "Offset boom, stick, bucket linkage, and cylinders along depth while retaining ghosted pin axes and cylinder mount lines.", crossSectionPlan: "Section one hydraulic cylinder to reveal barrel, rod, piston, and oil chambers; use pin cutaways at boom foot and bucket linkage.", cameraPresets: [ { key: "arm-overview", label: "Arm overview", position: [6.5, 4.2, 6.0], target: [1.2, 1.4, 0], notes: "Frames full excavator working envelope.", }, { key: "cylinder-cutaway", label: "Cylinder cutaway", position: [3.8, 3.0, 3.4], target: [0.5, 1.4, 0], notes: "Explains hydraulic actuation.", }, { key: "bucket-linkage-detail", label: "Bucket linkage detail", position: [5.0, 2.8, 3.2], target: [2.6, 0.6, 0], notes: "Shows curl linkage and force path.", }, ], validationChecks: [ "Cylinder rods and barrels remain collinear between their mount pins.", "Boom, stick, and bucket links rotate around fixed pin axes.", "Bucket load-path overlay follows linkage pins back to the upper frame.", ], sourceAssetNotes: "Use simplified excavator body context only; spend geometry budget on pivots, cylinders, and bucket linkage clarity.", }, ]; const createCategoryCounts = (): Record => ({ engine: 0, gearbox: 0, pump: 0, mechanism: 0, structural: 0, }); const findDuplicates = (values: readonly string[]): string[] => { const seen = new Set(); const duplicates = new Set(); for (const value of values) { if (seen.has(value)) { duplicates.add(value); } seen.add(value); } return [...duplicates]; }; export const validateCatalogueBlueprints = ( blueprints: readonly MachineBlueprint[] = catalogueBlueprints, ): BlueprintValidationIssue[] => { const issues: BlueprintValidationIssue[] = []; if (blueprints.length !== CATALOGUE_TARGET_COUNT) { issues.push({ message: `Expected ${CATALOGUE_TARGET_COUNT} machine blueprints, found ${blueprints.length}.`, }); } const duplicateIds = findDuplicates( blueprints.map((blueprint) => blueprint.id), ); for (const duplicateId of duplicateIds) { issues.push({ machineId: duplicateId, message: "Machine blueprint IDs must be unique.", }); } for (const blueprint of blueprints) { if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(blueprint.id)) { issues.push({ machineId: blueprint.id, message: "Machine blueprint ID must be kebab-case.", }); } if (blueprint.components.length < 4) { issues.push({ machineId: blueprint.id, message: "Machine blueprint must define at least four components.", }); } const duplicateComponentKeys = findDuplicates( blueprint.components.map((component) => component.key), ); for (const duplicateComponentKey of duplicateComponentKeys) { issues.push({ machineId: blueprint.id, message: `Duplicate component key "${duplicateComponentKey}".`, }); } if (blueprint.animationChannels.length < 2) { issues.push({ machineId: blueprint.id, message: "Machine blueprint must define at least two animation channels.", }); } const duplicateAnimationKeys = findDuplicates( blueprint.animationChannels.map((channel) => channel.key), ); for (const duplicateAnimationKey of duplicateAnimationKeys) { issues.push({ machineId: blueprint.id, message: `Duplicate animation channel key "${duplicateAnimationKey}".`, }); } if (blueprint.cameraPresets.length < 2) { issues.push({ machineId: blueprint.id, message: "Machine blueprint must define at least two camera presets.", }); } if (blueprint.learningOutcomes.length < 2) { issues.push({ machineId: blueprint.id, message: "Machine blueprint must define at least two learning outcomes.", }); } if (blueprint.requiredViewerModes.length === 0) { issues.push({ machineId: blueprint.id, message: "Machine blueprint must define required viewer modes.", }); } if (blueprint.validationChecks.length < 3) { issues.push({ machineId: blueprint.id, message: "Machine blueprint must define at least three validation checks.", }); } } return issues; }; const catalogueBlueprintIssues = validateCatalogueBlueprints(catalogueBlueprints); if (catalogueBlueprintIssues.length > 0) { throw new Error( `Catalogue blueprint validation failed:\n${catalogueBlueprintIssues .map((issue) => issue.machineId ? `- ${issue.machineId}: ${issue.message}` : `- ${issue.message}`, ) .join("\n")}`, ); } export const catalogueBlueprintById: ReadonlyMap = new Map( catalogueBlueprints.map((blueprint) => [blueprint.id, blueprint] as const), ); export const catalogueBlueprintIds = catalogueBlueprints.map( (blueprint) => blueprint.id, ); export const getCatalogueBlueprintById = ( id: string, ): MachineBlueprint | undefined => catalogueBlueprintById.get(id); export const getCatalogueBlueprintsByCategory = ( category: CatalogueCategory, ): readonly MachineBlueprint[] => catalogueBlueprints.filter((blueprint) => blueprint.category === category); export const catalogueBlueprintSummary = { targetCount: CATALOGUE_TARGET_COUNT, actualCount: catalogueBlueprints.length, byCategory: catalogueBlueprints.reduce((counts, blueprint) => { counts[blueprint.category] += 1; return counts; }, createCategoryCounts()), tierOneIds: catalogueBlueprints .filter((blueprint) => blueprint.implementationTier === 1) .map((blueprint) => blueprint.id), };