export const MACHINE_CATEGORIES = [ 'engines', 'gearboxes', 'pumps', 'mechanisms', 'structural', ] as const; export type MachineCategory = (typeof MACHINE_CATEGORIES)[number]; export type MachineCategoryId = MachineCategory; export type Category = MachineCategory; export const MACHINE_CATEGORY_LABELS: Record = { engines: 'Engines', gearboxes: 'Gearboxes & Drives', pumps: 'Pumps & Fluid Systems', mechanisms: 'Mechanisms', structural: 'Structural / Other', }; export const CATEGORY_LABELS = MACHINE_CATEGORY_LABELS; export const MACHINE_DIFFICULTIES = ['beginner', 'intermediate', 'advanced'] as const; export const MACHINE_DIFFICULTY_LEVELS = MACHINE_DIFFICULTIES; export type MachineDifficulty = (typeof MACHINE_DIFFICULTIES)[number]; export type MachineDifficultyId = MachineDifficulty; export type Difficulty = MachineDifficulty; export const MACHINE_DIFFICULTY_LABELS: Record = { beginner: 'Beginner', intermediate: 'Intermediate', advanced: 'Advanced', }; export const DIFFICULTY_LABELS = MACHINE_DIFFICULTY_LABELS; export const MACHINE_SORT_KEYS = ['name', 'newest', 'complexity'] as const; export type MachineSortKey = (typeof MACHINE_SORT_KEYS)[number]; export const CAMERA_PRESET_IDS = ['front', 'back', 'left', 'right', 'top', 'isometric'] as const; export type CameraPresetId = (typeof CAMERA_PRESET_IDS)[number]; export type CatalogueCategoryFilter = MachineCategory | 'all'; export type CatalogueDifficultyFilter = MachineDifficulty | 'all'; export type Vector3Tuple = [number, number, number]; export type MachineSpecValue = string | number | boolean; export type MachinePartRole = | 'housing' | 'rotating' | 'reciprocating' | 'valve' | 'fastener' | 'fluid' | 'sensor' | 'control' | 'bearing' | 'brake' | 'electrical' | 'annotation'; export interface MachinePartAnnotation { label: string; offset: Vector3Tuple; priority?: number; } export interface MachinePartTransform { position?: Vector3Tuple; rotation?: Vector3Tuple; scale?: Vector3Tuple; } export interface MachinePart { id: string; name: string; description: string; role: MachinePartRole; material: string; color: string; defaultVisible: boolean; defaultOpacity: number; visible?: boolean; opacity?: number; selectable: boolean; parentId?: string; transform?: MachinePartTransform; explode: Vector3Tuple; annotation: MachinePartAnnotation; specs: Record; tags: readonly string[]; } export type ComponentDefinition = MachinePart; export type PartDefinition = MachinePart; export interface MachinePartViewState { partId: string; visible: boolean; opacity: number; selected?: boolean; highlighted?: boolean; } export type PartVisibilityState = MachinePartViewState; export interface EngineeringFact { label: string; value: string; unit?: string; description?: string; } export type MachineSpec = EngineeringFact; export interface GuidedTourStep { id: string; title: string; body: string; focusPartIds: readonly string[]; cameraPreset: CameraPresetId; durationSeconds: number; highlightColor?: string; animationCue?: string; } export type TourStep = GuidedTourStep; export type MachineAssetStrategy = 'procedural' | 'gltf'; export type MachineAssetStatus = | 'procedural-placeholder' | 'source-required' | 'in-production' | 'review-ready' | 'shipped'; export interface MachineAssetVariant { id: string; label: string; quality: 'procedural' | 'low' | 'medium' | 'high'; url?: string; polygonBudget?: number; textureBudgetMb?: number; } export interface MachineAssetDescriptor { strategy: MachineAssetStrategy; status: MachineAssetStatus; url?: string; source: string; license: string; draco: boolean; ktx2: boolean; scale: number; center: Vector3Tuple; boundingRadius: number; variants: readonly MachineAssetVariant[]; } export type AnimationCycleType = | 'rotary' | 'reciprocating' | 'intermittent' | 'fluid' | 'thermal' | 'hybrid'; export type AnimationLoopMode = 'seamless' | 'ping-pong' | 'event-reset'; export interface AnimationParameterDefinition { label: string; min: number; max: number; step: number; default: number; unit?: string; description?: string; } export interface MachineAnimationDescriptor { modulePath: string; defaultRpm: number; minRpm: number; maxRpm: number; cycleSeconds: number; cycleType: AnimationCycleType; loopMode: AnimationLoopMode; stepCount: number; drivenParts: readonly string[]; notes: readonly string[]; parameters?: Record; } export interface MachineSeoMetadata { title: string; description: string; shareImage: string; keywords: readonly string[]; } export type CoreMachineId = | 'four-stroke-petrol-engine' | 'two-stroke-engine' | 'diesel-engine' | 'v8-engine' | 'wankel-rotary-engine' | 'steam-engine' | 'jet-engine-turbojet' | 'turbofan-engine' | 'planetary-gearbox' | 'differential-gear' | 'manual-gearbox-5-speed' | 'cvt' | 'worm-gear-drive' | 'bevel-gear-set' | 'centrifugal-pump' | 'gear-pump' | 'piston-pump' | 'hydraulic-cylinder' | 'scotch-yoke' | 'geneva-drive' | 'cam-and-follower' | 'rack-and-pinion' | 'slider-crank' | 'toggle-clamp' | 'ball-bearing' | 'roller-bearing' | 'disc-brake-caliper' | 'turbocharger'; export type MachineId = CoreMachineId | (string & {}); export interface MachineDefinition { id: MachineId; slug: string; title: string; name: string; subtitle: string; summary: string; description: string; category: MachineCategory; difficulty: MachineDifficulty; complexity: number; phase: 1 | 2; releaseOrder: number; createdAt: string; updatedAt: string; dateAdded: string; thumbnail: string; thumbnailAlt: string; image: string; assetPath?: string; modelPath?: string; tags: readonly string[]; keywords: readonly string[]; facts: readonly EngineeringFact[]; keyFacts: readonly EngineeringFact[]; engineeringFacts: readonly EngineeringFact[]; parts: readonly MachinePart[]; guidedTour: readonly GuidedTourStep[]; tour: readonly GuidedTourStep[]; related: readonly MachineId[]; animation: MachineAnimationDescriptor; asset: MachineAssetDescriptor; seo: MachineSeoMetadata; learningObjectives: readonly string[]; estimatedPartCount?: number; } export type Machine = MachineDefinition; export type DisplayMode = 'solid' | 'wireframe' | 'xray' | 'cross-section'; export type CrossSectionAxis = 'none' | 'x' | 'y' | 'z'; export interface CrossSectionState { enabled: boolean; axis: CrossSectionAxis; offset: number; inverted: boolean; } export interface CameraUrlState { position: Vector3Tuple; target: Vector3Tuple; zoom?: number; preset?: CameraPresetId; } export interface PlaybackUrlState { playing: boolean; rpm: number; timeScale: number; stepIndex?: number; } export interface ShareableViewerState { version: 1; machineId: MachineId; camera?: CameraUrlState; explodeDistance: number; hiddenPartIds: readonly string[]; partOpacity: Record; selectedPartId?: string; displayMode: DisplayMode; crossSection: CrossSectionState; annotationsVisible: boolean; playback: PlaybackUrlState; } export const DEFAULT_PART_OPACITY = 1; const includesString = (values: T, value: unknown): value is T[number] => typeof value === 'string' && (values as readonly string[]).includes(value); export const isMachineCategory = (value: unknown): value is MachineCategory => includesString(MACHINE_CATEGORIES, value); export const isMachineDifficulty = (value: unknown): value is MachineDifficulty => includesString(MACHINE_DIFFICULTIES, value); export const isMachineSortKey = (value: unknown): value is MachineSortKey => includesString(MACHINE_SORT_KEYS, value); export const isCameraPresetId = (value: unknown): value is CameraPresetId => includesString(CAMERA_PRESET_IDS, value); export const isCoreMachineId = (value: unknown): value is CoreMachineId => typeof value === 'string' && [ 'four-stroke-petrol-engine', 'two-stroke-engine', 'diesel-engine', 'v8-engine', 'wankel-rotary-engine', 'steam-engine', 'jet-engine-turbojet', 'turbofan-engine', 'planetary-gearbox', 'differential-gear', 'manual-gearbox-5-speed', 'cvt', 'worm-gear-drive', 'bevel-gear-set', 'centrifugal-pump', 'gear-pump', 'piston-pump', 'hydraulic-cylinder', 'scotch-yoke', 'geneva-drive', 'cam-and-follower', 'rack-and-pinion', 'slider-crank', 'toggle-clamp', 'ball-bearing', 'roller-bearing', 'disc-brake-caliper', 'turbocharger', ].includes(value); export const isMachineId = (value: unknown): value is MachineId => typeof value === 'string' && value.trim().length > 0;