//! Reusable procedural Foundation institution kit shared by the asset crate //! and the Bevy frontend. Geometry stays deliberately cheap: one pooled cube //! mesh scaled into quiet porcelain/chrome/gunmetal volumes. use std::f32::consts::TAU; use bevy::prelude::*; use crate::palette::{ AMBER, BONE, CRIMSON, DIM, GUNMETAL, GUNMETAL_DARK, PORCELAIN, SIGNAL, scaled, }; /// Neutral building-side prop families used by the Foundation kit. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum InstitutionPropKind { Camera, Ups, Breaker, PatchPanel, Hvac, FloorFixture, RackPlinth, CableTray, CableBridge, ServiceDrop, FloorTrench, MaintenanceBay, ServiceCart, } impl InstitutionPropKind { pub const ALL: [Self; 13] = [ Self::Camera, Self::Ups, Self::Breaker, Self::PatchPanel, Self::Hvac, Self::FloorFixture, Self::RackPlinth, Self::CableTray, Self::CableBridge, Self::ServiceDrop, Self::FloorTrench, Self::MaintenanceBay, Self::ServiceCart, ]; pub const fn label(self) -> &'static str { match self { Self::Camera => "camera", Self::Ups => "ups", Self::Breaker => "breaker", Self::PatchPanel => "patch_panel", Self::Hvac => "hvac", Self::FloorFixture => "floor_fixture", Self::RackPlinth => "rack_plinth", Self::CableTray => "cable_tray", Self::CableBridge => "cable_bridge", Self::ServiceDrop => "service_drop", Self::FloorTrench => "floor_trench", Self::MaintenanceBay => "maintenance_bay", Self::ServiceCart => "service_cart", } } } /// Optional signal state. The neutral kit uses none of these colors unless the /// caller opts in explicitly. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum InstitutionSemanticState { #[default] Neutral, Powered, Owned, Alert, } /// Spawn-time placement shared by props and people. The root entity carries /// the transform and visibility so the caller can move or hide the whole /// hierarchy cheaply. #[derive(Debug, Clone, Default)] pub struct InstitutionInstanceOptions { pub transform: Transform, pub parent: Option, } /// Authored gait offsets for the simplified walking person. #[derive(Debug, Clone, Copy, PartialEq)] pub struct InstitutionHumanGait { pub left_leg_forward: f32, pub right_leg_forward: f32, pub left_arm_forward: f32, pub right_arm_forward: f32, pub torso_lean: f32, } impl Default for InstitutionHumanGait { fn default() -> Self { Self::STANDING } } impl InstitutionHumanGait { pub const STANDING: Self = Self { left_leg_forward: 0.0, right_leg_forward: 0.0, left_arm_forward: 0.0, right_arm_forward: 0.0, torso_lean: 0.0, }; /// Convenience helper for the common "same authored walk cycle, different /// phase" case. `phase` is normalized to a full 0..1 step cycle. pub fn from_phase(phase: f32) -> Self { let swing = (phase * TAU).sin(); Self { left_leg_forward: swing, right_leg_forward: -swing, left_arm_forward: -0.82 * swing, right_arm_forward: 0.82 * swing, torso_lean: 0.08 * swing, } } } /// Pooled primitive + material handles. Callers can `init_resource` this once /// and reuse the handles across every spawned institution prop/person. #[derive(Resource, Clone)] pub struct InstitutionAssets { cube: Handle, porcelain: Handle, porcelain_trim: Handle, chrome: Handle, gunmetal: Handle, structure: Handle, signal_amber: Handle, signal_cold: Handle, signal_crimson: Handle, } impl InstitutionAssets { pub fn new(meshes: &mut Assets, materials: &mut Assets) -> Self { Self { cube: meshes.add(Cuboid::from_size(Vec3::ONE)), porcelain: materials.add(porcelain_material()), porcelain_trim: materials.add(porcelain_trim_material()), chrome: materials.add(chrome_material()), gunmetal: materials.add(gunmetal_material()), structure: materials.add(structure_material()), signal_amber: materials.add(signal_material(AMBER, 0.16)), signal_cold: materials.add(signal_material(SIGNAL, 0.20)), signal_crimson: materials.add(signal_material(CRIMSON, 0.12)), } } } impl FromWorld for InstitutionAssets { fn from_world(world: &mut World) -> Self { let cube = { let mut meshes = world.resource_mut::>(); meshes.add(Cuboid::from_size(Vec3::ONE)) }; let ( porcelain, porcelain_trim, chrome, gunmetal, structure, signal_amber, signal_cold, signal_crimson, ) = { let mut materials = world.resource_mut::>(); ( materials.add(porcelain_material()), materials.add(porcelain_trim_material()), materials.add(chrome_material()), materials.add(gunmetal_material()), materials.add(structure_material()), materials.add(signal_material(AMBER, 0.16)), materials.add(signal_material(SIGNAL, 0.20)), materials.add(signal_material(CRIMSON, 0.12)), ) }; Self { cube, porcelain, porcelain_trim, chrome, gunmetal, structure, signal_amber, signal_cold, signal_crimson, } } } /// Spawn one volumetric institution prop with a movable/hideable root. pub fn spawn_institution_prop( commands: &mut Commands, assets: &InstitutionAssets, kind: InstitutionPropKind, semantic_state: InstitutionSemanticState, options: InstitutionInstanceOptions, ) -> Entity { let root = spawn_root( commands, format!("institution_prop_{}", kind.label()), options.transform, options.parent, ); build_institution_prop(commands, assets, root, kind, semantic_state); root } /// Build one volumetric institution prop under an existing root entity. pub fn build_institution_prop( commands: &mut Commands, assets: &InstitutionAssets, root: Entity, kind: InstitutionPropKind, semantic_state: InstitutionSemanticState, ) { match kind { InstitutionPropKind::Camera => build_camera(commands, assets, root, semantic_state), InstitutionPropKind::Ups => build_ups(commands, assets, root, semantic_state), InstitutionPropKind::Breaker => build_breaker(commands, assets, root, semantic_state), InstitutionPropKind::PatchPanel => { build_patch_panel(commands, assets, root, semantic_state) } InstitutionPropKind::Hvac => build_hvac(commands, assets, root, semantic_state), InstitutionPropKind::FloorFixture => { build_floor_fixture(commands, assets, root, semantic_state) } InstitutionPropKind::RackPlinth => build_rack_plinth(commands, assets, root), InstitutionPropKind::CableTray => build_cable_tray(commands, assets, root), InstitutionPropKind::CableBridge => build_cable_bridge(commands, assets, root), InstitutionPropKind::ServiceDrop => build_service_drop(commands, assets, root), InstitutionPropKind::FloorTrench => build_floor_trench(commands, assets, root), InstitutionPropKind::MaintenanceBay => build_maintenance_bay(commands, assets, root), InstitutionPropKind::ServiceCart => { build_service_cart(commands, assets, root, semantic_state) } } } /// Spawn a small walking human silhouette with a movable/hideable root. pub fn spawn_institution_person( commands: &mut Commands, assets: &InstitutionAssets, gait: InstitutionHumanGait, options: InstitutionInstanceOptions, ) -> Entity { let root = spawn_root( commands, "institution_person".to_string(), options.transform, options.parent, ); build_institution_person(commands, assets, root, gait); root } /// Build the simplified volumetric person under an existing root entity. pub fn build_institution_person( commands: &mut Commands, assets: &InstitutionAssets, root: Entity, gait: InstitutionHumanGait, ) { let left_leg = gait.left_leg_forward.clamp(-1.0, 1.0); let right_leg = gait.right_leg_forward.clamp(-1.0, 1.0); let left_arm = gait.left_arm_forward.clamp(-1.0, 1.0); let right_arm = gait.right_arm_forward.clamp(-1.0, 1.0); let torso_lean = gait.torso_lean.clamp(-1.0, 1.0) * 0.12; const HIP_Y: f32 = 0.88; const SHOULDER_Y: f32 = 1.44; const LEG_LEN: f32 = 0.88; const ARM_LEN: f32 = 0.58; const FOOT_Z: f32 = 0.08; let spine = spawn_pivot( commands, root, Vec3::new(0.0, HIP_Y + 0.08, 0.0), Quat::from_rotation_x(torso_lean), "person_spine", ); spawn_part( commands, assets, assets.porcelain_trim.clone(), root, Vec3::new(0.30, 0.16, 0.18), Vec3::new(0.0, HIP_Y + 0.08, 0.0), Quat::IDENTITY, "pelvis", ); spawn_part( commands, assets, assets.porcelain.clone(), spine, Vec3::new(0.34, 0.54, 0.20), Vec3::new(0.0, 0.27, 0.0), Quat::IDENTITY, "torso", ); spawn_part( commands, assets, assets.chrome.clone(), spine, Vec3::new(0.20, 0.07, 0.13), Vec3::new(0.0, 0.57, 0.0), Quat::IDENTITY, "neck", ); spawn_part( commands, assets, assets.porcelain_trim.clone(), spine, Vec3::new(0.22, 0.22, 0.19), Vec3::new(0.0, 0.69, 0.02), Quat::IDENTITY, "head", ); let left_arm_root = spawn_pivot( commands, spine, Vec3::new(-0.23, SHOULDER_Y - (HIP_Y + 0.08), 0.0), Quat::from_rotation_x(left_arm * 0.42), "left_arm_root", ); spawn_part( commands, assets, assets.chrome.clone(), left_arm_root, Vec3::new(0.11, ARM_LEN, 0.12), Vec3::new(0.0, -ARM_LEN * 0.5, 0.02), Quat::IDENTITY, "left_arm", ); let right_arm_root = spawn_pivot( commands, spine, Vec3::new(0.23, SHOULDER_Y - (HIP_Y + 0.08), 0.0), Quat::from_rotation_x(right_arm * 0.42), "right_arm_root", ); spawn_part( commands, assets, assets.chrome.clone(), right_arm_root, Vec3::new(0.11, ARM_LEN, 0.12), Vec3::new(0.0, -ARM_LEN * 0.5, 0.02), Quat::IDENTITY, "right_arm", ); let left_leg_root = spawn_pivot( commands, root, Vec3::new(-0.10, HIP_Y, 0.0), Quat::from_rotation_x(left_leg * 0.52), "left_leg_root", ); spawn_part( commands, assets, assets.gunmetal.clone(), left_leg_root, Vec3::new(0.12, LEG_LEN, 0.15), Vec3::new(0.0, -LEG_LEN * 0.5, 0.0), Quat::IDENTITY, "left_leg", ); spawn_part( commands, assets, assets.structure.clone(), left_leg_root, Vec3::new(0.16, 0.07, 0.30), Vec3::new(0.0, -LEG_LEN + 0.035, FOOT_Z), Quat::IDENTITY, "left_foot", ); let right_leg_root = spawn_pivot( commands, root, Vec3::new(0.10, HIP_Y, 0.0), Quat::from_rotation_x(right_leg * 0.52), "right_leg_root", ); spawn_part( commands, assets, assets.gunmetal.clone(), right_leg_root, Vec3::new(0.12, LEG_LEN, 0.15), Vec3::new(0.0, -LEG_LEN * 0.5, 0.0), Quat::IDENTITY, "right_leg", ); spawn_part( commands, assets, assets.structure.clone(), right_leg_root, Vec3::new(0.16, 0.07, 0.30), Vec3::new(0.0, -LEG_LEN + 0.035, FOOT_Z), Quat::IDENTITY, "right_foot", ); } fn build_camera( commands: &mut Commands, assets: &InstitutionAssets, root: Entity, semantic_state: InstitutionSemanticState, ) { spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.24, 0.16, 0.05), Vec3::new(0.0, 1.76, -0.38), Quat::IDENTITY, "mount_plate", ); spawn_part( commands, assets, assets.gunmetal.clone(), root, Vec3::new(0.08, 0.08, 0.24), Vec3::new(0.0, 1.72, -0.24), Quat::from_rotation_x(-0.22), "mount_arm", ); spawn_part( commands, assets, assets.chrome.clone(), root, Vec3::new(0.18, 0.12, 0.22), Vec3::new(0.0, 1.66, -0.08), Quat::from_rotation_x(-0.14), "housing", ); spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.20, 0.03, 0.08), Vec3::new(0.0, 1.71, 0.01), Quat::IDENTITY, "visor", ); if let Some(material) = accent_handle(assets, semantic_state) { spawn_part( commands, assets, material, root, Vec3::new(0.09, 0.05, 0.04), Vec3::new(0.0, 1.64, 0.04), Quat::IDENTITY, "lens", ); } } fn build_ups( commands: &mut Commands, assets: &InstitutionAssets, root: Entity, semantic_state: InstitutionSemanticState, ) { spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.48, 0.08, 0.34), Vec3::new(0.0, 0.04, 0.0), Quat::IDENTITY, "plinth", ); spawn_part( commands, assets, assets.porcelain.clone(), root, Vec3::new(0.42, 0.62, 0.28), Vec3::new(0.0, 0.39, 0.0), Quat::IDENTITY, "cabinet", ); spawn_part( commands, assets, assets.porcelain_trim.clone(), root, Vec3::new(0.46, 0.04, 0.32), Vec3::new(0.0, 0.72, 0.0), Quat::IDENTITY, "cap", ); spawn_part( commands, assets, assets.gunmetal.clone(), root, Vec3::new(0.34, 0.46, 0.04), Vec3::new(0.0, 0.39, 0.16), Quat::IDENTITY, "front_inset", ); spawn_part( commands, assets, assets.chrome.clone(), root, Vec3::new(0.12, 0.04, 0.02), Vec3::new(0.0, 0.56, 0.18), Quat::IDENTITY, "pull", ); if let Some(material) = accent_handle(assets, semantic_state) { spawn_part( commands, assets, material, root, Vec3::new(0.14, 0.05, 0.02), Vec3::new(0.0, 0.67, 0.18), Quat::IDENTITY, "state_strip", ); } } fn build_breaker( commands: &mut Commands, assets: &InstitutionAssets, root: Entity, semantic_state: InstitutionSemanticState, ) { spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.64, 1.04, 0.04), Vec3::new(0.0, 0.86, -0.39), Quat::IDENTITY, "backplate", ); spawn_part( commands, assets, assets.porcelain.clone(), root, Vec3::new(0.56, 0.88, 0.10), Vec3::new(0.0, 0.86, -0.31), Quat::IDENTITY, "cabinet", ); spawn_part( commands, assets, assets.gunmetal.clone(), root, Vec3::new(0.50, 0.78, 0.03), Vec3::new(0.0, 0.86, -0.25), Quat::IDENTITY, "door_inset", ); for (column, x) in [(-0.16, 0usize), (0.16, 1usize)] { for row in 0..4 { let y = 1.13 - row as f32 * 0.18; spawn_part( commands, assets, if (row + x) % 2 == 0 { assets.chrome.clone() } else { assets.gunmetal.clone() }, root, Vec3::new(0.11, 0.05, 0.04), Vec3::new(column, y, -0.22), Quat::IDENTITY, "breaker_toggle", ); } } if let Some(material) = accent_handle(assets, semantic_state) { spawn_part( commands, assets, material, root, Vec3::new(0.10, 0.04, 0.02), Vec3::new(0.0, 1.24, -0.23), Quat::IDENTITY, "panel_state", ); } } fn build_patch_panel( commands: &mut Commands, assets: &InstitutionAssets, root: Entity, semantic_state: InstitutionSemanticState, ) { spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.88, 0.34, 0.04), Vec3::new(0.0, 1.28, -0.39), Quat::IDENTITY, "backplate", ); spawn_part( commands, assets, assets.gunmetal.clone(), root, Vec3::new(0.80, 0.22, 0.10), Vec3::new(0.0, 1.28, -0.31), Quat::IDENTITY, "panel", ); spawn_part( commands, assets, assets.porcelain_trim.clone(), root, Vec3::new(0.78, 0.05, 0.04), Vec3::new(0.0, 1.17, -0.23), Quat::IDENTITY, "label_rail", ); for i in 0..8 { let x = -0.31 + i as f32 * 0.09; spawn_part( commands, assets, assets.chrome.clone(), root, Vec3::new(0.05, 0.05, 0.04), Vec3::new(x, 1.30, -0.22), Quat::IDENTITY, "port", ); } if let Some(material) = accent_handle(assets, semantic_state) { spawn_part( commands, assets, material, root, Vec3::new(0.70, 0.02, 0.02), Vec3::new(0.0, 1.38, -0.22), Quat::IDENTITY, "live_line", ); } } fn build_hvac( commands: &mut Commands, assets: &InstitutionAssets, root: Entity, semantic_state: InstitutionSemanticState, ) { spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.90, 0.06, 0.54), Vec3::new(0.0, 0.03, 0.0), Quat::IDENTITY, "base", ); spawn_part( commands, assets, assets.gunmetal.clone(), root, Vec3::new(0.82, 0.78, 0.46), Vec3::new(0.0, 0.45, 0.0), Quat::IDENTITY, "body", ); spawn_part( commands, assets, assets.porcelain.clone(), root, Vec3::new(0.70, 0.58, 0.04), Vec3::new(0.0, 0.46, 0.25), Quat::IDENTITY, "front_panel", ); spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.38, 0.24, 0.28), Vec3::new(0.0, 0.96, -0.03), Quat::IDENTITY, "top_duct", ); for i in 0..4 { let y = 0.26 + i as f32 * 0.12; spawn_part( commands, assets, assets.chrome.clone(), root, Vec3::new(0.58, 0.02, 0.02), Vec3::new(0.0, y, 0.27), Quat::IDENTITY, "vent_slit", ); } if let Some(material) = accent_handle(assets, semantic_state) { spawn_part( commands, assets, material, root, Vec3::new(0.16, 0.04, 0.02), Vec3::new(0.0, 0.77, 0.27), Quat::IDENTITY, "service_tag", ); } } fn build_floor_fixture( commands: &mut Commands, assets: &InstitutionAssets, root: Entity, semantic_state: InstitutionSemanticState, ) { spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.42, 0.04, 0.88), Vec3::new(0.0, 0.02, 0.0), Quat::IDENTITY, "frame", ); spawn_part( commands, assets, assets.gunmetal.clone(), root, Vec3::new(0.30, 0.02, 0.74), Vec3::new(0.0, 0.018, 0.0), Quat::IDENTITY, "well", ); let lens = accent_handle(assets, semantic_state).unwrap_or_else(|| assets.chrome.clone()); spawn_part( commands, assets, lens, root, Vec3::new(0.22, 0.01, 0.64), Vec3::new(0.0, 0.03, 0.0), Quat::IDENTITY, "lens", ); spawn_part( commands, assets, assets.chrome.clone(), root, Vec3::new(0.02, 0.01, 0.58), Vec3::new(-0.07, 0.031, 0.0), Quat::IDENTITY, "brace_left", ); spawn_part( commands, assets, assets.chrome.clone(), root, Vec3::new(0.02, 0.01, 0.58), Vec3::new(0.07, 0.031, 0.0), Quat::IDENTITY, "brace_right", ); } /// Low service footing that binds one chassis into a repeated rack bank. It /// stays below ankle height and carries no signal color; ownership still /// belongs to the chassis above it. fn build_rack_plinth(commands: &mut Commands, assets: &InstitutionAssets, root: Entity) { spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.78, 0.07, 0.66), Vec3::new(0.0, 0.035, 0.0), Quat::IDENTITY, "service_plinth", ); for x in [-0.29_f32, 0.29] { spawn_part( commands, assets, assets.chrome.clone(), root, Vec3::new(0.06, 0.018, 0.58), Vec3::new(x, 0.076, 0.0), Quat::IDENTITY, "mounting_rail", ); } } /// One tile of the high service spine running behind a rack row. Repeating /// quiet gunmetal rails and porcelain hangers make the row read as maintained /// infrastructure without adding decorative emissive detail. fn build_cable_tray(commands: &mut Commands, assets: &InstitutionAssets, root: Entity) { // The tray hugs the rear service side of the row rather than bridging // over the chassis. From the wide camera it should read as one sparse // spine, never a wire cage laid across the whole hall. for z in [-0.40_f32, -0.28] { spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.96, 0.045, 0.035), Vec3::new(0.0, 1.46, z), Quat::IDENTITY, "tray_rail", ); } for x in [-0.32_f32, 0.32] { spawn_part( commands, assets, assets.chrome.clone(), root, Vec3::new(0.022, 0.028, 0.15), Vec3::new(x, 1.445, -0.34), Quat::IDENTITY, "tray_rung", ); } spawn_part( commands, assets, assets.porcelain_trim.clone(), root, Vec3::new(0.045, 0.26, 0.045), Vec3::new(0.0, 1.59, -0.40), Quat::IDENTITY, "tray_hanger", ); } /// One quiet bridge between the rack-local tray spans. Rack sites are two /// tiles apart; these odd-tile spans make the row read as one maintained run /// without adding another hanger or any signal color. fn build_cable_bridge(commands: &mut Commands, assets: &InstitutionAssets, root: Entity) { for z in [-0.40_f32, -0.28] { spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(1.08, 0.045, 0.035), Vec3::new(0.0, 1.46, z), Quat::IDENTITY, "bridge_rail", ); } for x in [-0.36_f32, 0.0, 0.36] { spawn_part( commands, assets, assets.chrome.clone(), root, Vec3::new(0.020, 0.026, 0.15), Vec3::new(x, 1.445, -0.34), Quat::IDENTITY, "bridge_rung", ); } } /// Row C's service spine terminates beside the real hall patch panel. The /// paired verticals and floor elbow make the endpoint physically legible; the /// reach graph and material trace still own whether anything can flow. fn build_service_drop(commands: &mut Commands, assets: &InstitutionAssets, root: Entity) { for x in [-0.075_f32, 0.075] { spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.035, 1.40, 0.035), Vec3::new(x, 0.73, -0.40), Quat::IDENTITY, "drop_vertical", ); spawn_part( commands, assets, assets.gunmetal.clone(), root, Vec3::new(0.035, 0.035, 0.34), Vec3::new(x, 0.045, -0.25), Quat::IDENTITY, "drop_floor_elbow", ); } spawn_part( commands, assets, assets.porcelain_trim.clone(), root, Vec3::new(0.28, 0.07, 0.06), Vec3::new(0.0, 1.46, -0.36), Quat::IDENTITY, "drop_header", ); } /// A dark recessed service trench through a cold aisle. The interrupted /// chrome edge provides scale and direction; it is structure, not a signal /// route and therefore never glows. fn build_floor_trench(commands: &mut Commands, assets: &InstitutionAssets, root: Entity) { spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.96, 0.022, 0.22), Vec3::new(0.0, 0.012, 0.0), Quat::IDENTITY, "trench_well", ); for z in [-0.125_f32, 0.125] { spawn_part( commands, assets, assets.gunmetal.clone(), root, Vec3::new(0.96, 0.014, 0.018), Vec3::new(0.0, 0.026, z), Quat::IDENTITY, "trench_edge", ); } for x in [-0.32_f32, 0.0, 0.32] { spawn_part( commands, assets, assets.gunmetal.clone(), root, Vec3::new(0.035, 0.015, 0.20), Vec3::new(x, 0.028, 0.0), Quat::IDENTITY, "trench_bridge", ); } } /// Sparse authored maintenance history at a real dead rack: one pulled /// porcelain access panel, one dark tool tray, and one neutral indexed tag. /// Nothing glows and nothing is randomized into generic facility clutter. fn build_maintenance_bay(commands: &mut Commands, assets: &InstitutionAssets, root: Entity) { spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.46, 0.07, 0.30), Vec3::new(-0.34, 0.055, 0.36), Quat::IDENTITY, "tool_tray", ); spawn_part( commands, assets, assets.porcelain_trim.clone(), root, Vec3::new(0.30, 0.50, 0.035), Vec3::new(0.43, 0.28, 0.20), Quat::from_rotation_z(-0.24), "pulled_access_panel", ); spawn_part( commands, assets, assets.chrome.clone(), root, Vec3::new(0.16, 0.055, 0.018), Vec3::new(0.48, 0.40, 0.17), Quat::from_rotation_z(-0.24), "maintenance_index_tag", ); for x in [-0.47_f32, -0.21] { spawn_part( commands, assets, assets.gunmetal.clone(), root, Vec3::new(0.08, 0.04, 0.08), Vec3::new(x, 0.11, 0.36), Quat::IDENTITY, "tool_case", ); } } /// Mobile service cart attached by the frontend to one visible person while /// the renderer-neutral `PersonCarrier` says they carry work. The cold lamp /// identifies a live work packet; it is not a second task queue. fn build_service_cart( commands: &mut Commands, assets: &InstitutionAssets, root: Entity, semantic_state: InstitutionSemanticState, ) { for y in [0.26_f32, 0.58] { spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.50, 0.055, 0.34), Vec3::new(0.0, y, 0.0), Quat::IDENTITY, "cart_shelf", ); } for x in [-0.21_f32, 0.21] { for z in [-0.13_f32, 0.13] { spawn_part( commands, assets, assets.gunmetal.clone(), root, Vec3::new(0.10, 0.10, 0.08), Vec3::new(x, 0.10, z), Quat::IDENTITY, "cart_wheel", ); } } for x in [-0.22_f32, 0.22] { spawn_part( commands, assets, assets.chrome.clone(), root, Vec3::new(0.035, 0.72, 0.035), Vec3::new(x, 0.48, -0.13), Quat::IDENTITY, "cart_upright", ); } spawn_part( commands, assets, assets.chrome.clone(), root, Vec3::new(0.50, 0.035, 0.035), Vec3::new(0.0, 0.84, -0.13), Quat::IDENTITY, "cart_handle", ); let packet = accent_handle(assets, semantic_state).unwrap_or_else(|| assets.chrome.clone()); spawn_part( commands, assets, packet, root, Vec3::new(0.22, 0.16, 0.16), Vec3::new(0.0, 0.69, 0.0), Quat::IDENTITY, "carried_work_packet", ); spawn_part( commands, assets, assets.structure.clone(), root, Vec3::new(0.28, 0.035, 0.20), Vec3::new(0.0, 0.61, 0.0), Quat::IDENTITY, "packet_custody_frame", ); } fn accent_handle( assets: &InstitutionAssets, semantic_state: InstitutionSemanticState, ) -> Option> { match semantic_state { InstitutionSemanticState::Neutral => None, InstitutionSemanticState::Powered => Some(assets.signal_cold.clone()), InstitutionSemanticState::Owned => Some(assets.signal_amber.clone()), InstitutionSemanticState::Alert => Some(assets.signal_crimson.clone()), } } fn spawn_root( commands: &mut Commands, name: String, transform: Transform, parent: Option, ) -> Entity { let mut entity = commands.spawn((transform, Visibility::default(), Name::new(name))); if let Some(parent) = parent { entity.insert(ChildOf(parent)); } entity.id() } fn spawn_pivot( commands: &mut Commands, parent: Entity, translation: Vec3, rotation: Quat, name: &'static str, ) -> Entity { commands .spawn(( Transform::from_translation(translation).with_rotation(rotation), Visibility::default(), ChildOf(parent), Name::new(name), )) .id() } #[allow(clippy::too_many_arguments)] // A primitive part's material, parent, pose, and name are independent. fn spawn_part( commands: &mut Commands, assets: &InstitutionAssets, material: Handle, parent: Entity, size: Vec3, translation: Vec3, rotation: Quat, name: &'static str, ) -> Entity { commands .spawn(( Mesh3d(assets.cube.clone()), MeshMaterial3d(material), Transform::from_translation(translation) .with_rotation(rotation) .with_scale(size), ChildOf(parent), Name::new(name), )) .id() } fn porcelain_material() -> StandardMaterial { StandardMaterial { base_color: PORCELAIN, perceptual_roughness: 0.97, metallic: 0.0, reflectance: 0.03, ..default() } } fn porcelain_trim_material() -> StandardMaterial { StandardMaterial { base_color: BONE, perceptual_roughness: 0.94, metallic: 0.0, reflectance: 0.04, ..default() } } fn chrome_material() -> StandardMaterial { StandardMaterial { base_color: DIM, perceptual_roughness: 0.34, metallic: 0.58, reflectance: 0.28, ..default() } } fn gunmetal_material() -> StandardMaterial { StandardMaterial { base_color: GUNMETAL, perceptual_roughness: 0.82, metallic: 0.18, reflectance: 0.08, ..default() } } fn structure_material() -> StandardMaterial { StandardMaterial { base_color: GUNMETAL_DARK, perceptual_roughness: 0.94, metallic: 0.08, reflectance: 0.03, ..default() } } fn signal_material(color: Color, emissive_scale: f32) -> StandardMaterial { StandardMaterial { base_color: scaled(color, 0.55), emissive: color.to_linear() * emissive_scale, perceptual_roughness: 0.68, metallic: 0.04, reflectance: 0.12, ..default() } } #[cfg(test)] mod tests { use std::collections::HashSet; use super::*; fn warm_color(c: Color) -> bool { let s = c.to_srgba(); s.red > s.blue * 1.45 } fn crimsonish(c: Color) -> bool { let s = c.to_srgba(); s.red > 0.65 && s.red > s.green * 2.2 && s.red > s.blue * 2.2 } #[test] fn prop_kind_coverage_is_pinned() { let labels: HashSet<_> = InstitutionPropKind::ALL .into_iter() .map(InstitutionPropKind::label) .collect(); assert_eq!(InstitutionPropKind::ALL.len(), 13); assert_eq!(labels.len(), 13); assert!(labels.contains("camera")); assert!(labels.contains("ups")); assert!(labels.contains("breaker")); assert!(labels.contains("patch_panel")); assert!(labels.contains("hvac")); assert!(labels.contains("floor_fixture")); assert!(labels.contains("rack_plinth")); assert!(labels.contains("cable_tray")); assert!(labels.contains("cable_bridge")); assert!(labels.contains("service_drop")); assert!(labels.contains("floor_trench")); assert!(labels.contains("maintenance_bay")); assert!(labels.contains("service_cart")); } #[test] fn neutral_materials_stay_out_of_signal_space() { for material in [ porcelain_material(), porcelain_trim_material(), chrome_material(), gunmetal_material(), structure_material(), ] { assert!( !warm_color(material.base_color), "neutral base color drifted warm: {:?}", material.base_color ); assert!( !crimsonish(material.base_color), "neutral base color drifted crimson: {:?}", material.base_color ); assert_eq!( material.emissive, LinearRgba::BLACK, "neutral kit should not glow by default" ); } assert!(accent_handle_fixture(InstitutionSemanticState::Neutral).is_none()); } #[test] #[allow(clippy::assertions_on_constants)] // Pins authored kit proportions at compile time. fn shape_proportions_stay_disciplined() { const PERSON_HEIGHT: f32 = 1.84; assert!(PERSON_HEIGHT > 1.65 && PERSON_HEIGHT < 1.95); assert!(0.48 < 0.82, "UPS should stay taller than it is wide"); assert!(0.90 > 0.78, "HVAC should stay broader than it is tall"); assert!( 0.42 < 0.08 * 8.0, "camera stays compact against human scale" ); assert!( 0.88 < PERSON_HEIGHT * 0.6, "floor fixture stays recess-like" ); let gait = InstitutionHumanGait::from_phase(0.25); assert!(gait.left_leg_forward > 0.9); assert!(gait.right_leg_forward < -0.9); assert!(gait.left_arm_forward < -0.7); assert!(gait.right_arm_forward > 0.7); } fn accent_handle_fixture(state: InstitutionSemanticState) -> Option { match state { InstitutionSemanticState::Neutral => None, other => Some(other), } } }