Build Lucent tether habitat study

This commit is contained in:
Meta Vens 2026-05-05 02:42:10 +01:00
commit f335dfbb19
10 changed files with 652 additions and 16 deletions

View file

@ -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.

View file

@ -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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,018 KiB

View file

@ -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

View file

@ -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."}

View file

@ -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

View file

@ -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()