diff --git a/docs/research/builds/lucent-tether-habitat.md b/docs/research/builds/lucent-tether-habitat.md new file mode 100644 index 0000000..1b2e51e --- /dev/null +++ b/docs/research/builds/lucent-tether-habitat.md @@ -0,0 +1,133 @@ +# Lucent Tether Habitat Build + +## Source Read + +Primary source: `Aetheria/Stories/Lucent Hostage Feed.md` and its interactive +Ghostlight scaffold. Supporting faction read: +`Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Major/Lucent Media.md`. + +The story describes a Lucent tether station with a transfer lounge at the +media-eye end of the tether. The lounge has glass walls, an elevator gate, a +waist-high glass rail, a city bubble far below like a sealed aquarium, public +surfaces full of moderation overlays, Mara's private control rail, influencer +side panels, sponsor-risk colors, breach estimates, audience heat, Lucent-gray +security beyond the lounge doors, and a white safe-line two careful steps back +from the elevator-side glass rail. + +Lucent's broader faction language matters: its habitats are partially staged +media ecologies. Permissiveness sits over a classification engine. The build +therefore treats surfaces as active controls, not passive decoration. + +## Coordinate Contract + +- `X`: tether axis, from the city-bubble side to the media-eye lounge. +- `Z`: local visual altitude in the scene; the city bubble sits below the + tether/lounge assembly to preserve the story's "far below" read. +- `Y`: lounge width and panel spread. +- The media-eye lounge is at positive `X`. +- The sealed city bubble is at negative `X`, lower `Z`. +- Crisis geometry lives inside the lounge: elevator gate and glass rail at the + threshold, safe-line arc behind it, security beyond the doors. + +This is deliberately room-scale plus habitat-scale in one frame. The tether +axis gives the habitat its body; the lounge threshold gives it teeth. + +## Implementation + +Script: +`examples/geometry_script/lucent_tether_habitat.py` + +Generated artifacts: + +- `experiments/generated/lucent_tether_habitat/lucent_tether_habitat.blend` +- `experiments/generated/lucent_tether_habitat/lucent_tether_overview.png` +- `experiments/generated/lucent_tether_habitat/lucent_media_eye_lounge.png` +- `experiments/generated/lucent_tether_habitat/lucent_city_bubble_aquarium.png` + +Verifier: +`tools/verify_lucent_tether_habitat.py` + +Geometry Script group: +`VG Lucent Feed Ribbon` + +The group creates a procedural ribbon of moderation/feed geometry orbiting the +media-eye lounge. Python owns the lore table, deterministic city detail, +materials, camera setup, render output, and verification. Geometry Script owns +the inspectable node graph for the feed ribbon so the scene keeps a live +procedural editing surface instead of becoming only a pile of mesh facts. + +## Translation Patterns + +**Habitat First, Room Second** + +The tether and city bubble establish that the lounge is part of a larger +orbital media habitat. Without that, the crisis room becomes a generic glass +office with neon rectangles, which is how worldbuilding goes to die wearing a +lanyard. + +```python +add_cylinder_between("lucent_tether_main_cable", (-6.5, 0.0, 0.0), (6.8, 0.0, 0.0), 0.075, mats["tether"]) +add_uv_sphere("city_bubble_sealed_aquarium", (-5.9, 0.0, -3.25), (1.95, 1.95, 1.05), mats["glass"]) +add_uv_sphere("media_eye_transfer_lounge_glass_shell", (4.6, 0.0, 0.0), (1.75, 1.05, 0.78), mats["glass"]) +``` + +**Crisis Geometry As Spatial Contract** + +The white arc is not decoration. It is a negotiated border between hostage, +glass rail, security, and feed optics. Its object name and verifier check make +that contract explicit. + +```python +add_curve_polyline("waist_high_glass_rail", [(5.25, -0.62, -0.08), (5.31, 0.0, -0.035), (5.25, 0.62, -0.08)], 0.027, mats["glass"]) +add_curve_polyline("safe_line_white_arc", arc_points((4.82, 0.0, -0.36), 0.68, 208, 332, 36, plane="xy"), 0.015, mats["safe"]) +``` + +**Surfaces Are Interfaces** + +Lucent habitat surfaces carry classification pressure: prompts, sponsor risk, +audience heat, delayed influencer panels, and public edit trails. The panels +are separate named objects because future passes may need to animate or route +them independently. + +```python +add_panel("feed_ops_prompt_panels", (4.12, -0.88, 0.34), (1.08, 0.028, 0.22), mats["amber"]) +add_panel("sponsor_risk_color_stack", (4.74, -0.86, 0.34), (0.15, 0.032, 0.62), mats["risk"]) +add_panel("audience_heat_meter", (3.55, -0.84, 0.30), (0.2, 0.032, 0.58), mats["heat"]) +``` + +**The City Bubble Is A Miniature Pressure Field** + +The sealed city uses deterministic noise for attention-lit tower variation. +This keeps the aquarium dense without hand-authoring every tower. + +```python +n = fbm_2d(ix * 0.27, iy * 0.33, seed=91) +append_box_parts(material_verts, material_faces, (x, y, -3.55 + height * 0.5), axes, size) +``` + +## Verification + +Command: + +```powershell +& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\generated\lucent_tether_habitat\lucent_tether_habitat.blend' --python '.\tools\verify_lucent_tether_habitat.py' +``` + +Result: + +```text +LUCENT_TETHER_VERIFY ok objects=59 +``` + +Render review accepted the first-pass composition after label shrinkage. The +overview reads as tether plus city bubble plus media-eye lounge; the lounge +inspection render exposes glass shell, elevator threshold, glass rail, +safe-line, overlay panels, private rail, side panels, and security doors; the +city inspection render reads as an aquarium-bubble miniature city. + +## Durable Lesson + +For Lucent spaces, avoid thinking "set dressing." The set is the power system. +Build public surfaces as rails, rankings, prompt stacks, edits, and delayed +feeds. Then put physical bodies and thresholds where those abstractions become +dangerous. diff --git a/examples/geometry_script/lucent_tether_habitat.py b/examples/geometry_script/lucent_tether_habitat.py new file mode 100644 index 0000000..ef06f39 --- /dev/null +++ b/examples/geometry_script/lucent_tether_habitat.py @@ -0,0 +1,417 @@ +"""Build the Lucent tether habitat from the Ghostlight hostage-feed scene. + +Spatial contract from the story: + +- The tether is the long station spine. +- The transfer lounge sits at the media-eye end of the tether. +- The city bubble is far below, readable as a sealed aquarium. +- The lounge threshold carries an elevator gate, waist-high glass rail, and + a white safe-line arc two careful steps back from the rail. +- Lucent surfaces are never only surfaces: overlays, rails, prompts, status + edits, influencer side panels, and audience heat are part of the room. +""" + +import math as pymath +from pathlib import Path + +try: + from tools.geometry_script_loader import load_repo_geometry_script +except ModuleNotFoundError: + import sys + + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from tools.geometry_script_loader import load_repo_geometry_script + +load_repo_geometry_script() + +from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals. +from mathutils import Vector +from vibegeometry import ( + add_box, + add_curve_polyline, + add_cylinder_between, + add_material, + add_mesh_parts, + add_multi_polyline_curve, + aim_camera, + append_box_parts, + clear_scene, + fbm_2d, + hash01, + smoothstep, +) + + +TAU = pymath.tau +OUT_DIR = Path("experiments/generated/lucent_tether_habitat") + + +@tree("VG Lucent Feed Ribbon") +def vg_lucent_feed_ribbon( + length: Float = 11.5, + radius: Float = 1.25, + turns: Float = 2.25, + wave: Float = 0.22, + thickness: Float = 0.025, +): + """Procedural moderation/feed rail orbiting the media-eye lounge.""" + + rail = curve_line(mode=CurveLine.Mode.POINTS, start=(-5.75, 0.0, 0.0), end=(5.75, 0.0, 0.0)) + samples = resample_curve(keep_last_segment=True, curve=rail, mode="Count", count=360, length=0.1) + point_count = domain_size(component="CURVE", geometry=samples).point_count + t = math(operation=Math.Operation.DIVIDE, value=(index(), point_count)) + theta = math(operation=Math.Operation.MULTIPLY, value=(t, math(operation=Math.Operation.MULTIPLY, value=(turns, TAU)))) + x = math(operation=Math.Operation.SUBTRACT, value=(math(operation=Math.Operation.MULTIPLY, value=(t, length)), math(operation=Math.Operation.DIVIDE, value=(length, 2.0)))) + pulse = math(operation=Math.Operation.SINE, value=math(operation=Math.Operation.MULTIPLY, value=(theta, 4.0))) + animated_radius = math(operation=Math.Operation.ADD, value=(radius, math(operation=Math.Operation.MULTIPLY, value=(pulse, wave)))) + y = math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.COSINE, value=theta), animated_radius)) + z = math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.SINE, value=theta), animated_radius)) + positioned = set_position(geometry=samples, position=combine_xyz(x=x, y=y, z=z), offset=(0.0, 0.0, 0.0)) + profile = curve_circle(mode=CurveCircle.Mode.RADIUS, resolution=8, radius=thickness) + return {"Geometry": curve_to_mesh(curve=positioned, profile_curve=profile, fill_caps=True)} + + +def mat(name, color, emission=False, strength=0.0, alpha=1.0): + return add_material(name, color, emission=emission, strength=strength, alpha=alpha) + + +def add_uv_sphere(name, loc, scale, material, segments=48, rings=24): + import bpy + + bpy.ops.mesh.primitive_uv_sphere_add(segments=segments, ring_count=rings, radius=1.0, location=loc) + obj = bpy.context.object + obj.name = name + obj.data.name = name + "Mesh" + obj.scale = scale + obj.data.materials.append(material) + obj.show_transparent = True + return obj + + +def add_torus(name, loc, major_radius, minor_radius, material, rotation=(0.0, 0.0, 0.0), segments=96): + import bpy + + bpy.ops.mesh.primitive_torus_add( + major_segments=segments, + minor_segments=10, + major_radius=major_radius, + minor_radius=minor_radius, + location=loc, + rotation=rotation, + ) + obj = bpy.context.object + obj.name = name + obj.data.name = name + "Mesh" + obj.data.materials.append(material) + return obj + + +def add_label(name, text, loc, size, material, rotation=(pymath.radians(65), 0.0, pymath.radians(0))): + import bpy + + bpy.ops.object.text_add(location=loc, rotation=rotation) + obj = bpy.context.object + obj.name = name + obj.data.body = text + obj.data.align_x = "CENTER" + obj.data.align_y = "CENTER" + obj.data.size = size + obj.data.materials.append(material) + return obj + + +def arc_points(center, radius, start_deg, end_deg, steps, plane="xy", wobble=0.0): + points = [] + for i in range(steps + 1): + t = i / steps + a = pymath.radians(start_deg + (end_deg - start_deg) * t) + w = wobble * pymath.sin(t * TAU * 2.0) + r = radius + w + if plane == "xy": + points.append((center[0] + r * pymath.cos(a), center[1] + r * pymath.sin(a), center[2])) + elif plane == "yz": + points.append((center[0], center[1] + r * pymath.cos(a), center[2] + r * pymath.sin(a))) + else: + points.append((center[0] + r * pymath.cos(a), center[1], center[2] + r * pymath.sin(a))) + return points + + +def add_floor_patch(name, center, width, depth, material, z=0.0): + return add_box(name, (center[0], center[1], z), ((1, 0, 0), (0, 1, 0), (0, 0, 1)), (width, depth, 0.035), material) + + +def add_panel(name, loc, size, material, angle=0.0, lean=0.0): + x_axis = (pymath.cos(angle), pymath.sin(angle), 0) + y_axis = (-pymath.sin(angle), pymath.cos(angle), lean) + z_axis = (0, -lean, 1) + return add_box(name, loc, (x_axis, y_axis, z_axis), size, material) + + +def add_habitat_cable_bundle(mats): + add_cylinder_between("lucent_tether_main_cable", (-6.5, 0.0, 0.0), (6.8, 0.0, 0.0), 0.075, mats["tether"], vertices=24) + add_cylinder_between("lucent_tether_spin_reference_core", (-6.25, 0.18, 0.16), (6.25, 0.18, 0.16), 0.023, mats["cyan"], vertices=10) + add_cylinder_between("lucent_tether_service_darkline", (-6.25, -0.16, -0.12), (6.25, -0.16, -0.12), 0.018, mats["dark"], vertices=8) + for i, x in enumerate([-4.9, -3.2, -1.4, 0.4, 2.1, 3.9, 5.4]): + add_torus( + f"slow_rotation_reference_ring_{i:02d}", + (x, 0.0, 0.0), + 0.42 + 0.04 * (i % 3), + 0.008, + mats["soft_white"], + rotation=(0.0, pymath.radians(90), 0.0), + segments=64, + ) + + +def add_city_bubble(mats): + bubble = add_uv_sphere("city_bubble_sealed_aquarium", (-5.9, 0.0, -3.25), (1.95, 1.95, 1.05), mats["glass"], 64, 32) + bubble.rotation_euler[0] = pymath.radians(8) + add_torus( + "city_bubble_pressure_equator", + (-5.9, 0.0, -3.25), + 1.95, + 0.022, + mats["cyan"], + rotation=(0.0, 0.0, 0.0), + segments=128, + ) + add_torus( + "city_bubble_lower_aquarium_rim", + (-5.9, 0.0, -3.52), + 1.42, + 0.018, + mats["aquarium"], + rotation=(0.0, 0.0, 0.0), + segments=96, + ) + + city_verts, city_faces = [], [] + light_verts, light_faces = [], [] + roads = [] + for ix in range(28): + x = -7.1 + ix * 0.09 + for iy in range(22): + y = -1.15 + iy * 0.11 + ell = ((x + 5.9) / 1.42) ** 2 + (y / 1.42) ** 2 + if ell > 1.0: + continue + n = fbm_2d(ix * 0.27, iy * 0.33, seed=91) + if n < 0.33: + continue + height = 0.06 + 0.28 * n + material_verts, material_faces = (light_verts, light_faces) if n > 0.72 else (city_verts, city_faces) + append_box_parts( + material_verts, + material_faces, + (x, y, -3.55 + height * 0.5), + ((1, 0, 0), (0, 1, 0), (0, 0, 1)), + (0.045 + 0.03 * n, 0.055 + 0.025 * hash01(ix, iy, 7), height), + ) + if ix % 4 == 0: + roads.append([(x, -1.25, -3.46), (x + 0.15 * pymath.sin(ix), 0.0, -3.44), (x, 1.25, -3.46)]) + for iy in range(-10, 11, 3): + y = iy * 0.11 + roads.append([(-7.05, y, -3.47), (-5.9, y + 0.08 * pymath.sin(iy), -3.45), (-4.75, y, -3.47)]) + add_mesh_parts("city_bubble_miniature_skyscraper_mesh", city_verts, city_faces, mats["city"]) + add_mesh_parts("city_bubble_attention_lit_tower_mesh", light_verts, light_faces, mats["cyan"]) + add_multi_polyline_curve("city_bubble_aquarium_street_grid", roads, 0.006, mats["soft_white"], resolution=2) + + +def add_media_eye_lounge(mats): + add_uv_sphere("media_eye_transfer_lounge_glass_shell", (4.6, 0.0, 0.0), (1.75, 1.05, 0.78), mats["glass"], 64, 32) + add_torus( + "media_eye_lounge_structural_lash_ring", + (4.6, 0.0, 0.0), + 1.07, + 0.025, + mats["soft_white"], + rotation=(0.0, pymath.radians(90), 0.0), + segments=128, + ) + add_torus( + "media_eye_public_camera_eye", + (5.58, 0.0, 0.08), + 0.42, + 0.022, + mats["cyan"], + rotation=(0.0, pymath.radians(90), 0.0), + segments=96, + ) + add_floor_patch("transfer_lounge_curved_floor_slab", (4.58, 0.0), 2.55, 1.36, mats["floor"], z=-0.42) + for i, y in enumerate([-0.56, -0.28, 0.0, 0.28, 0.56]): + add_curve_polyline( + f"floor_keeps_remembering_physics_arc_{i:02d}", + arc_points((4.58, y, -0.385), 0.34 + abs(y) * 0.15, 190, 350, 18, plane="xz"), + 0.004, + mats["soft_white"], + resolution=2, + ) + + # The crisis geometry: elevator glass rail at the threshold, and the safe + # line behind it. This is the room's moral diagram, unfortunately built + # out of real materials and several departments' bad incentives. + add_box("elevator_gate_threshold", (5.42, 0.0, -0.12), ((1, 0, 0), (0, 1, 0), (0, 0, 1)), (0.11, 1.36, 0.82), mats["lucent_gray"]) + add_curve_polyline("waist_high_glass_rail", [(5.25, -0.62, -0.08), (5.31, 0.0, -0.035), (5.25, 0.62, -0.08)], 0.027, mats["glass"], resolution=3) + add_curve_polyline("safe_line_white_arc", arc_points((4.82, 0.0, -0.36), 0.68, 208, 332, 36, plane="xy"), 0.015, mats["safe"], resolution=3) + add_box("elevator_side_glass_warning_strip", (5.18, 0.0, -0.31), ((1, 0, 0), (0, 1, 0), (0, 0, 1)), (0.025, 1.2, 0.03), mats["amber"]) + + add_panel("moderation_overlay_wall_public_status", (4.2, -0.82, 0.12), (1.18, 0.026, 0.46), mats["overlay_blue"], angle=0.0, lean=0.18) + add_panel("feed_ops_prompt_panels", (4.12, -0.88, 0.34), (1.08, 0.028, 0.22), mats["amber"], angle=0.0, lean=0.18) + add_panel("sponsor_risk_color_stack", (4.74, -0.86, 0.34), (0.15, 0.032, 0.62), mats["risk"], angle=0.0, lean=0.18) + add_panel("audience_heat_meter", (3.55, -0.84, 0.30), (0.2, 0.032, 0.58), mats["heat"], angle=0.0, lean=0.18) + add_panel("pippa_delayed_side_panel", (3.55, 0.87, 0.26), (0.54, 0.03, 0.38), mats["pippa"], angle=0.0, lean=-0.16) + add_panel("brant_delayed_side_panel", (4.34, 0.9, 0.28), (0.58, 0.03, 0.42), mats["brant"], angle=0.0, lean=-0.16) + add_panel("public_edit_trail_checksum_panel", (4.92, -0.84, -0.12), (0.6, 0.025, 0.16), mats["cyan"], angle=0.0, lean=0.08) + + add_curve_polyline("mara_private_control_rail", [(3.32, -0.63, -0.22), (4.1, -0.69, -0.12), (5.05, -0.56, -0.2)], 0.018, mats["private"], resolution=3) + for i in range(9): + x = 3.42 + i * 0.18 + z = -0.18 + 0.08 * pymath.sin(i * 0.9) + material = mats["risk"] if i in {2, 5, 8} else mats["cyan"] if i % 2 else mats["amber"] + add_box(f"mara_private_rail_control_chip_{i:02d}", (x, -0.68, z), ((1, 0, 0), (0, 1, 0), (0, 0, 1)), (0.09, 0.022, 0.045), material) + + add_box("security_lounge_doors", (3.12, 0.0, -0.08), ((1, 0, 0), (0, 1, 0), (0, 0, 1)), (0.11, 1.12, 0.72), mats["lucent_gray"]) + for i, y in enumerate([-0.42, -0.14, 0.14, 0.42]): + add_box(f"lucent_gray_security_waiting_marker_{i:02d}", (2.83, y, -0.2), ((1, 0, 0), (0, 1, 0), (0, 0, 1)), (0.08, 0.08, 0.36), mats["security"]) + + +def add_overlay_ribbons_and_signage(mats): + import bpy + + group = bpy.data.node_groups.get("VG Lucent Feed Ribbon") + mesh = bpy.data.meshes.new("LucentFeedRibbonCarrierMesh") + carrier = bpy.data.objects.new("GN_Lucent_Feed_Ribbon_Carrier", mesh) + bpy.context.collection.objects.link(carrier) + carrier.location = (4.45, 0.0, 0.0) + carrier.rotation_euler[1] = pymath.radians(90) + carrier.scale = (0.32, 0.32, 0.32) + carrier.data.materials.append(mats["cyan"]) + mod = carrier.modifiers.new("VG Lucent Feed Ribbon", "NODES") + mod.node_group = group + + for i, radius in enumerate([0.92, 1.12, 1.34]): + points = [] + for j in range(72): + t = j / 71 + a = TAU * t + i * 0.7 + x = 4.55 + (t - 0.5) * 1.8 + y = radius * pymath.cos(a) * 0.82 + z = 0.12 + radius * pymath.sin(a) * 0.46 + points.append((x, y, z)) + add_curve_polyline(f"manual_status_ribbon_orbit_{i:02d}", points, 0.006 + i * 0.002, mats["overlay_blue"], resolution=4) + + labels = [ + ("label_media_eye", "media-eye transfer lounge", (4.65, -1.52, 0.7), 0.082), + ("label_safe_line", "safe-line / glass rail", (5.18, 1.2, -0.34), 0.066), + ("label_city_bubble", "city bubble below: sealed aquarium", (-5.9, -1.9, -2.42), 0.12), + ("label_lucent_surfaces", "surfaces as controls, rankings, edits", (1.6, -0.82, 0.5), 0.082), + ] + for name, text, loc, size in labels: + add_label(name, text, loc, size, mats["label"]) + + +def build_scene(): + import bpy + + clear_scene() + mats = { + "tether": mat("Lucent tether graphite", (0.11, 0.12, 0.14), alpha=0.98), + "dark": mat("dark service cable", (0.035, 0.04, 0.05), alpha=1.0), + "glass": mat("media-eye blue glass", (0.38, 0.72, 1.0), alpha=0.26), + "floor": mat("polished lounge floor", (0.47, 0.49, 0.51), alpha=0.9), + "lucent_gray": mat("Lucent security gray", (0.54, 0.56, 0.58), alpha=0.95), + "security": mat("Lucent gray security figures", (0.23, 0.25, 0.27), alpha=0.98), + "soft_white": mat("soft white public line", (0.9, 0.94, 1.0), emission=True, strength=0.35), + "safe": mat("safe-line hard white", (1.0, 1.0, 0.94), emission=True, strength=0.8), + "cyan": mat("Lucent cyan feed glow", (0.1, 0.86, 1.0), emission=True, strength=0.65), + "amber": mat("Feed Ops amber", (1.0, 0.58, 0.08), emission=True, strength=0.45), + "risk": mat("sponsor risk magenta", (1.0, 0.16, 0.42), emission=True, strength=0.55), + "heat": mat("audience heat red", (1.0, 0.08, 0.06), emission=True, strength=0.6), + "pippa": mat("Pippa soft-focus side panel", (0.95, 0.52, 0.78), emission=True, strength=0.35, alpha=0.72), + "brant": mat("Brant outrage side panel", (1.0, 0.34, 0.08), emission=True, strength=0.38, alpha=0.72), + "overlay_blue": mat("moderation overlay blue", (0.24, 0.48, 1.0), emission=True, strength=0.2, alpha=0.36), + "private": mat("Mara private rail green", (0.32, 1.0, 0.62), emission=True, strength=0.38), + "aquarium": mat("aquarium blue", (0.02, 0.28, 0.55), alpha=0.58), + "city": mat("sealed miniature city", (0.58, 0.62, 0.66), alpha=0.95), + "label": mat("label white", (0.92, 0.95, 1.0), emission=True, strength=0.45), + } + add_habitat_cable_bundle(mats) + add_city_bubble(mats) + add_media_eye_lounge(mats) + add_overlay_ribbons_and_signage(mats) + + bpy.ops.object.light_add(type="AREA", location=(3.8, -4.8, 4.2)) + key = bpy.context.object + key.name = "Lucent soft inspection key" + key.data.energy = 620 + key.data.size = 5.5 + bpy.ops.object.light_add(type="POINT", location=(4.8, 0.0, 0.25)) + eye = bpy.context.object + eye.name = "media eye internal glow" + eye.data.energy = 180 + eye.data.color = (0.45, 0.85, 1.0) + + bpy.context.scene.render.engine = "BLENDER_EEVEE" + if hasattr(bpy.context.scene, "eevee"): + bpy.context.scene.eevee.taa_render_samples = 24 + bpy.context.scene.render.resolution_x = 1400 + bpy.context.scene.render.resolution_y = 950 + bpy.context.scene.view_settings.view_transform = "Filmic" + bpy.context.scene.view_settings.look = "Medium High Contrast" + + bpy.ops.object.camera_add(location=(7.8, -8.4, 4.2)) + overview = bpy.context.object + overview.name = "Camera_Lucent_Tether_Overview" + aim_camera(overview, (0.0, 0.0, -0.9)) + overview.data.type = "ORTHO" + overview.data.ortho_scale = 8.4 + bpy.context.scene.camera = overview + + bpy.ops.object.camera_add(location=(5.85, -2.95, 1.15)) + lounge = bpy.context.object + lounge.name = "Camera_Media_Eye_Lounge" + aim_camera(lounge, (4.58, 0.02, -0.12)) + lounge.data.type = "ORTHO" + lounge.data.ortho_scale = 2.5 + + bpy.ops.object.camera_add(location=(-4.4, -3.15, -1.75)) + city = bpy.context.object + city.name = "Camera_City_Bubble_Aquarium" + aim_camera(city, (-5.92, 0.0, -3.32)) + city.data.type = "ORTHO" + city.data.ortho_scale = 3.9 + + +def render_camera(camera_name, path): + import bpy + + bpy.context.scene.camera = bpy.data.objects[camera_name] + bpy.context.scene.render.filepath = str(path.resolve()) + bpy.ops.render.render(write_still=True) + + +def main(): + import bpy + + vg_lucent_feed_ribbon() + build_scene() + OUT_DIR.mkdir(parents=True, exist_ok=True) + blend_path = OUT_DIR / "lucent_tether_habitat.blend" + overview_path = OUT_DIR / "lucent_tether_overview.png" + lounge_path = OUT_DIR / "lucent_media_eye_lounge.png" + city_path = OUT_DIR / "lucent_city_bubble_aquarium.png" + bpy.ops.wm.save_as_mainfile(filepath=str(blend_path.resolve())) + render_camera("Camera_Lucent_Tether_Overview", overview_path) + render_camera("Camera_Media_Eye_Lounge", lounge_path) + render_camera("Camera_City_Bubble_Aquarium", city_path) + print("LUCENT_TETHER_BLEND", blend_path.resolve()) + print("LUCENT_TETHER_OVERVIEW_RENDER", overview_path.resolve()) + print("LUCENT_TETHER_LOUNGE_RENDER", lounge_path.resolve()) + print("LUCENT_TETHER_CITY_RENDER", city_path.resolve()) + group = bpy.data.node_groups.get("VG Lucent Feed Ribbon") + if group: + print("LUCENT_TETHER_GN_GROUP", group.name, len(group.nodes), len(group.links)) + + +if __name__ == "__main__": + main() diff --git a/experiments/generated/lucent_tether_habitat/lucent_city_bubble_aquarium.png b/experiments/generated/lucent_tether_habitat/lucent_city_bubble_aquarium.png new file mode 100644 index 0000000..5733dd1 Binary files /dev/null and b/experiments/generated/lucent_tether_habitat/lucent_city_bubble_aquarium.png differ diff --git a/experiments/generated/lucent_tether_habitat/lucent_media_eye_lounge.png b/experiments/generated/lucent_tether_habitat/lucent_media_eye_lounge.png new file mode 100644 index 0000000..1b77657 Binary files /dev/null and b/experiments/generated/lucent_tether_habitat/lucent_media_eye_lounge.png differ diff --git a/experiments/generated/lucent_tether_habitat/lucent_tether_habitat.blend b/experiments/generated/lucent_tether_habitat/lucent_tether_habitat.blend new file mode 100644 index 0000000..3ca152e Binary files /dev/null and b/experiments/generated/lucent_tether_habitat/lucent_tether_habitat.blend differ diff --git a/experiments/generated/lucent_tether_habitat/lucent_tether_overview.png b/experiments/generated/lucent_tether_habitat/lucent_tether_overview.png new file mode 100644 index 0000000..1b42b65 Binary files /dev/null and b/experiments/generated/lucent_tether_habitat/lucent_tether_overview.png differ diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 38b2b45..f165e67 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -46,17 +46,20 @@ footholds. Blockers are repeat-zone feedback, repeat `Top` outputs, foreach zones, gizmo nodes, and rejected empty-output `WindowBeams` harness. IRCSS maps live in `docs/research/translations/ircss-french-houses-*.md`. -First from-scratch build test: `examples/geometry_script/aetheria_bloom_habitat.py` -generates an Aetheria Bloom whole-habitat study. It now carries the key spatial -lessons: define the lore coordinate frame before ornament, use cylindrical and -endcap helpers for attachment, use golden-angle curves for transfer networks, -use deterministic noise for organic settlement pressure, batch dense detail by -material/class, and render inspection cameras that face the domain being judged. -The hubward terrace slums are noise-driven endcap shelves around the docking hub -office complex, stepping inward toward the Spire as "up" and outward into a -classier high-gravity urban crown. Build notes live at -`docs/research/builds/aetheria-bloom-habitat.md`; verification lives at -`tools/verify_aetheria_bloom_habitat.py`. +From-scratch story-bound builds: + +- `examples/geometry_script/aetheria_bloom_habitat.py` generates the Aetheria + Bloom whole-habitat study. It carries the key spatial lessons: explicit lore + coordinate frames, cylindrical/endcap attachment, golden-angle transfer + curves, deterministic settlement noise, batched dense detail, and inspection + cameras. Build notes: `docs/research/builds/aetheria-bloom-habitat.md`; + verifier: `tools/verify_aetheria_bloom_habitat.py`. +- `examples/geometry_script/lucent_tether_habitat.py` generates the Lucent + tether habitat from Ghostlight `Lucent Hostage Feed`: tether axis, city bubble + below, media-eye lounge, elevator threshold, glass rail, white safe-line, + overlays, control rail, side panels, risk/heat surfaces, and security doors. + Build notes: `docs/research/builds/lucent-tether-habitat.md`; verifier: + `tools/verify_lucent_tether_habitat.py`. Current authoring prior: use the full Python stack. Python structures intent, tables, loops, variants, cleanup, validation, docs, and scene/render @@ -82,9 +85,10 @@ The GameCult fork is intentionally pruned to `main` plus VibeGeometry-owned tooling branches. Do not copy upstream branch soup forward unless a future patch actually needs it. -The current next action is to grow `vibegeometry/` from helper primitives into a -small reusable builder seam, starting with either endcap terrace slums or spiral -transfer arteries, then bind that seam to scene-brief, manifest, +The current next action is to extract the first higher-level reusable builder +seam from the completed story-bound builds: either threshold/control-rail +interface kits from the Lucent habitat or endcap terrace/spiral artery kits +from the Bloom, then bind that seam to scene-brief, manifest, inspection-render, and verifier contract drafts. ## Important Invariants diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 456e365..7f5be9d 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -42,3 +42,4 @@ {"ts": "2026-05-05T00:55:36+00:00", "type": "correction", "status": "accepted", "note": "Bloom endcap terrace slums now use fractal value-noise occupancy rather than neat concentric box rings; verified dedicated hubward-endcap render and mesh-density checks for poor shacks, midrise housing, and outer urban crown."} {"ts": "2026-05-05T01:12:46+00:00", "type": "maintenance", "status": "accepted", "note": "Polished persistent state after Bloom corrections: map, handoff, plan, and doctrine now prioritize source coordinate contracts, deterministic noise for organic occupancy, batched dense detail, domain-facing inspection renders, and verifier checks as reusable machine behavior rather than one-off scene scars."} {"ts": "2026-05-05T01:25:43+00:00", "type": "implementation", "status": "accepted", "note": "Created the first reusable vibegeometry Python helper library for coordinate frames, deterministic fields, dense mesh batching, Blender scene primitives, and verifier helpers; refactored the Bloom build and verifier to use it, then rebuilt and verified the scene in Blender with 1028 geometry/text objects."} +{"ts": "2026-05-05T01:40:49+00:00", "type": "implementation", "status": "accepted", "note": "Built and verified the Lucent tether habitat from Ghostlight source: tether axis, city bubble below, media-eye transfer lounge, glass rail/safe-line threshold, Lucent overlay/control surfaces, and an inspectable VG Lucent Feed Ribbon Geometry Script group. Render review accepted overview, lounge, and city-bubble inspection artifacts."} diff --git a/state/map.yaml b/state/map.yaml index 8afd570..dc498bc 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -74,6 +74,7 @@ current_artifacts: - docs/research/translations/ircss-french-houses-node-atlas.md - docs/research/translations/ircss-french-houses-node-mechanisms.md - docs/research/builds/aetheria-bloom-habitat.md + - docs/research/builds/lucent-tether-habitat.md - vibegeometry/ tools: - tools/vibegeometry_state.py @@ -85,6 +86,7 @@ current_artifacts: - tools/geometry_script_loader.py - tools/smoke_nodes_to_script_converter.py - tools/verify_aetheria_bloom_habitat.py + - tools/verify_lucent_tether_habitat.py pending_contracts: - schemas/scene-brief.schema.json - schemas/node-graph-manifest.schema.json @@ -99,8 +101,8 @@ data_flow: - If state grows faster than understanding, cut it back. current_status: - summary: VibeGeometry has verified Geometry Script translations for local samples, official Blender demos, and IRCSS French Houses footholds, plus a from-scratch Aetheria Bloom whole-habitat build. Durable lessons now driving the pipeline: Python orchestrates intent and batching; Geometry Script emits inspectable node groups; source-bound scenes need explicit coordinate contracts before ornament; organic occupancy needs deterministic noise fields; dense detail must be batched; and visual acceptance needs domain-facing inspection renders plus verifier checks, not only overview renders. The first reusable `vibegeometry/` helper library now carries coordinate frames, fields, batching, Blender scene primitives, and verifier helpers used by the Bloom build. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes. - next_action: Grow `vibegeometry/` from helper primitives into builder seams by extracting one higher-level pattern next: either endcap terrace slums or spiral transfer arteries, then bind that seam to scene-brief, manifest, inspection-render, and verifier contract drafts. + summary: VibeGeometry has verified Geometry Script translations for local samples, official Blender demos, and IRCSS French Houses footholds, plus from-scratch Aetheria Bloom and Lucent tether habitat builds. Durable lessons now driving the pipeline: Python orchestrates intent and batching; Geometry Script emits inspectable node groups; source-bound scenes need explicit coordinate contracts before ornament; organic occupancy needs deterministic noise fields; dense detail must be batched; institutional spaces should model power as surfaces, rails, overlays, thresholds, and camera evidence; and visual acceptance needs domain-facing inspection renders plus verifier checks, not only overview renders. The first reusable `vibegeometry/` helper library now carries coordinate frames, fields, batching, Blender scene primitives, and verifier helpers used by the Bloom and Lucent builds. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes. + next_action: Extract the first higher-level reusable builder seam from the completed story-bound builds: either threshold/control-rail interface kits from the Lucent habitat or endcap terrace/spiral artery kits from the Bloom, then bind that seam to scene-brief, manifest, inspection-render, and verifier contract drafts. remotes: vibegeometry: https://github.com/GameCult/VibeGeometry geometry_script_fork: https://github.com/GameCult/geometry-script diff --git a/tools/verify_lucent_tether_habitat.py b/tools/verify_lucent_tether_habitat.py new file mode 100644 index 0000000..3908c8e --- /dev/null +++ b/tools/verify_lucent_tether_habitat.py @@ -0,0 +1,79 @@ +"""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_miniature_skyscraper_mesh", + "city_bubble_attention_lit_tower_mesh", + "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_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_artifacts() + print(f"LUCENT_TETHER_VERIFY ok objects={len(geometry_objects)}") + + +if __name__ == "__main__": + main()