VibeGeometry/tools/verify_lucent_tether_habitat.py
2026-05-05 04:32:10 +01:00

139 lines
5.8 KiB
Python

"""Verify the Lucent tether habitat study scene."""
from pathlib import Path
import bpy
try:
from vibegeometry import assert_objects_exist, assert_render_artifacts, assert_scene_density
except ModuleNotFoundError:
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from vibegeometry import assert_objects_exist, assert_render_artifacts, assert_scene_density
OUT_DIR = Path("experiments/generated/lucent_tether_habitat")
REQUIRED_OBJECTS = [
"lucent_tether_main_cable",
"lucent_tether_spin_reference_core",
"city_bubble_sealed_aquarium",
"city_bubble_pressure_equator",
"city_bubble_flat_ground_rim",
"city_bubble_tether_load_spine",
"city_bubble_elevator_umbilical",
"city_bubble_upper_tether_collar",
"city_bubble_tether_transfer_node",
"city_bubble_elevator_drop_to_anchor_plaza",
"city_bubble_under_keel_counterweight",
"city_bubble_tether_anchor_plaza_core",
"city_bubble_anchor_plaza_ring",
"city_bubble_flat_ground_surface_disk",
"city_bubble_flattening_boolean_cutter",
"city_bubble_solar_panel_ray_florets",
"city_bubble_radiator_ray_florets",
"city_bubble_interspersed_park_cells",
"city_bubble_prestige_anchor_skyscrapers",
"city_bubble_midcity_irregular_blocks",
"city_bubble_faux_rural_cottagecore_edge",
"city_bubble_tensor_field_street_network",
"city_bubble_prestige_plaza_spokes",
"media_eye_transfer_lounge_glass_shell",
"media_eye_public_camera_eye",
"transfer_lounge_curved_floor_slab",
"elevator_gate_threshold",
"waist_high_glass_rail",
"safe_line_white_arc",
"moderation_overlay_wall_public_status",
"feed_ops_prompt_panels",
"sponsor_risk_color_stack",
"audience_heat_meter",
"mara_private_control_rail",
"pippa_delayed_side_panel",
"brant_delayed_side_panel",
"security_lounge_doors",
"GN_Lucent_Feed_Ribbon_Carrier",
"Camera_Lucent_Tether_Overview",
"Camera_Media_Eye_Lounge",
"Camera_City_Bubble_Aquarium",
]
def verify_geometry_script_group():
group = bpy.data.node_groups.get("VG Lucent Feed Ribbon")
if group is None:
raise AssertionError("Missing Geometry Script group: VG Lucent Feed Ribbon")
if len(group.nodes) < 18 or len(group.links) < 25:
raise AssertionError(f"VG Lucent Feed Ribbon looks underbuilt: {len(group.nodes)} nodes, {len(group.links)} links")
carrier = bpy.data.objects["GN_Lucent_Feed_Ribbon_Carrier"]
modifiers = [modifier for modifier in carrier.modifiers if modifier.type == "NODES" and modifier.node_group == group]
if not modifiers:
raise AssertionError("Feed ribbon carrier lacks the VG Lucent Feed Ribbon nodes modifier")
def verify_render_engine_and_shaders():
if bpy.context.scene.render.engine != "CYCLES":
raise AssertionError(f"Expected Cycles render engine, found {bpy.context.scene.render.engine}")
expected_shader_nodes = {
"media-eye blue glass": {"ShaderNodeTexNoise", "ShaderNodeBump"},
"Lucent tether graphite": {"ShaderNodeTexNoise", "ShaderNodeBump"},
"moderation overlay blue": {"ShaderNodeTexNoise", "ShaderNodeValToRGB"},
"sponsor risk magenta": {"ShaderNodeTexNoise", "ShaderNodeValToRGB"},
}
for material_name, node_types in expected_shader_nodes.items():
material = bpy.data.materials.get(material_name)
if material is None:
raise AssertionError(f"Missing material {material_name}")
actual = {node.bl_idname for node in material.node_tree.nodes}
missing = sorted(node_types - actual)
if missing:
raise AssertionError(f"Material {material_name} is missing shader node types {missing}")
def verify_city_coordinate_contract():
bubble = bpy.data.objects["city_bubble_sealed_aquarium"]
plaza = bpy.data.objects["city_bubble_tether_anchor_plaza_core"]
solar = bpy.data.objects["city_bubble_solar_panel_ray_florets"]
radiator = bpy.data.objects["city_bubble_radiator_ray_florets"]
if bubble.dimensions.x >= bubble.dimensions.y * 0.7:
raise AssertionError("City dome is not flattened along the tether axis")
if plaza.dimensions.x >= 0.05:
raise AssertionError("City ground surface should be orthogonal to tether with minimal X depth")
if solar.location.x > 0.001 or radiator.location.x > 0.001:
raise AssertionError("Ray florets should use object-space geometry, not hidden object offsets")
solar_min_x = min(vertex.co.x for vertex in solar.data.vertices)
radiator_min_x = min(vertex.co.x for vertex in radiator.data.vertices)
if solar_min_x < -5.46 or radiator_min_x < -5.46:
raise AssertionError("Ray florets should hinge at the flat dome base, not behind it")
if solar.dimensions.x < 10.5 or radiator.dimensions.x < 10.5:
raise AssertionError("Ray florets should be Citadel-scale arms long enough to fold toward the far tether end")
if solar.dimensions.y < 4.2 or solar.dimensions.z < 4.2 or radiator.dimensions.y < 4.2 or radiator.dimensions.z < 4.2:
raise AssertionError("Alternating solar/radiator ray florets should wrap around most of the dome rim")
def verify_artifacts():
assert_render_artifacts(
[
OUT_DIR / "lucent_tether_habitat.blend",
OUT_DIR / "lucent_tether_overview.png",
OUT_DIR / "lucent_media_eye_lounge.png",
OUT_DIR / "lucent_city_bubble_aquarium.png",
],
min_size=10_000,
)
def main():
assert_objects_exist(bpy, REQUIRED_OBJECTS)
geometry_objects = assert_scene_density(bpy, min_geometry_objects=55)
verify_geometry_script_group()
verify_render_engine_and_shaders()
verify_city_coordinate_contract()
verify_artifacts()
print(f"LUCENT_TETHER_VERIFY ok objects={len(geometry_objects)}")
if __name__ == "__main__":
main()