"""Original HUMMBL illustration. Run in a fresh background Blender process."""
from pathlib import Path
import bpy
import math
from mathutils import Vector
import json

OUT = Path(__file__).resolve().parent
OUT.mkdir(parents=True, exist_ok=True)

# This process is launched with --factory-startup; no existing user scene is edited.
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)

def linear_channel(v):
    return v / 12.92 if v <= .04045 else ((v + .055) / 1.055) ** 2.4

def color(hex_value):
    value = hex_value.lstrip('#')
    return tuple(linear_channel(int(value[i:i+2], 16) / 255) for i in (0, 2, 4)) + (1,)

def material(name, hex_value, metallic=0, roughness=.4, transmission=0):
    m = bpy.data.materials.new(name)
    m.use_nodes = True
    shader = m.node_tree.nodes.get('Principled BSDF')
    shader.inputs['Base Color'].default_value = color(hex_value)
    shader.inputs['Metallic'].default_value = metallic
    shader.inputs['Roughness'].default_value = roughness
    shader.inputs['Transmission Weight'].default_value = transmission
    shader.inputs['IOR'].default_value = 1.46
    return m

paper = material('Paper | canvas', '#f7f8f4', roughness=.67)
ceramic = material('Warm mineral | structure', '#e6eee5', roughness=.32)
forest = material('Forest anodised | authority', '#173b2d', metallic=.22, roughness=.42)
green = material('Verdant enamel | permitted path', '#26754b', metallic=.18, roughness=.34)
mint = material('Soft green | edge', '#a8c4b6', metallic=.25, roughness=.35)
brass = material('Muted brass | reference', '#ab9d72', metallic=.55, roughness=.32)
glass = material('Frosted boundary | containment', '#c1dbc9', roughness=.08, transmission=.92)
glass.node_tree.nodes.get('Principled BSDF').inputs['IOR'].default_value = 1.15
ink = material('Forest ink | receipt marks', '#102f24', roughness=.46)
evidence = material('Uncoated paper | evidence tiles', '#f7f8f4', roughness=.78)
plinth = material('Mineral base | plinth', '#e6eee5', roughness=.58)

def cube(name, location, dimensions, mat, bevel=.06):
    bpy.ops.mesh.primitive_cube_add(size=1, location=location)
    obj = bpy.context.object
    obj.name = name
    obj.dimensions = dimensions
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    if mat:
        obj.data.materials.append(mat)
    if bevel:
        modifier = obj.modifiers.new('Machined edge radius', 'BEVEL')
        modifier.width = bevel
        modifier.segments = 5
        modifier.affect = 'EDGES'
        modifier = obj.modifiers.new('Face normals', 'WEIGHTED_NORMAL')
        modifier.keep_sharp = True
    return obj

def portal(name, x, mat):
    obj = cube(name, (x, .28, 1.88), (.36, 2.74, 3.14), mat, 0)
    cutter = cube(name + ' aperture cutter', (x, .28, 1.91), (1.4, 1.95, 2.35), None, .19)
    bpy.context.view_layer.objects.active = cutter
    bpy.ops.object.modifier_apply(modifier='Machined edge radius')
    bpy.ops.object.modifier_apply(modifier='Face normals')
    boolean = obj.modifiers.new('Bounded aperture', 'BOOLEAN')
    boolean.operation = 'DIFFERENCE'
    boolean.solver = 'EXACT'
    boolean.object = cutter
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.modifier_apply(modifier=boolean.name)
    bpy.data.objects.remove(cutter, do_unlink=True)
    bevel = obj.modifiers.new('Soft machined edges', 'BEVEL')
    bevel.width = .085
    bevel.segments = 5
    obj.modifiers.new('Weighted face normals', 'WEIGHTED_NORMAL')
    return obj

cube('Ground', (0, 0, -.3), (200, 200, .2), paper, 0)
cube('Precision plinth', (0, .1, -.06), (8.1, 5.35, .25), plinth, .2)
portal('01 — scoped authority', -1.75, forest)
portal('02 — execution boundary', 0, ceramic)
portal('03 — evidence boundary', 1.75, forest)

# One narrow permitted path crosses all three aligned apertures.
cube('Permitted path | continuous rail', (-.05, .27, 1.1), (6.65, .15, .13), green, .055)
cube('Rail underside', (-.05, .27, 1.04), (6.3, .24, .06), brass, .025)
cube('Execution plane | frosted insert', (0, .28, 1.89), (.075, 1.78, 2.13), glass, .035)

action = cube('Bounded action | carrier', (.65, .27, 1.39), (.57, .56, .44), green, .12)
cube('Carrier cap', (.65, .27, 1.622), (.35, .35, .025), mint, .065)
for i, x in enumerate((-2.92, -2.64, -2.36)):
    cube(f'Input marker {i+1}', (x, .27, 1.205), (.07, .19, .065), mint, .027)

# Paper-like evidence tiles: abstract marks only, no invented data or verification claims.
for index in range(3):
    z = .16 + index * .105
    tile = cube(f'Evidence tile {index+1}', (2.55 + index*.035, -1.16 - index*.035, z), (1.75, 1.19, .08), evidence, .065)
top = .41
for index, length in enumerate((.92, 1.13, .68)):
    cube(f'Receipt line {index+1}', (2.42, -1.44 + index*.19, top), (length, .022, .008), ink, .008)
cube('Receipt abstract seal', (3.14, -.91, top), (.2, .2, .009), green, .044)
cube('Evidence rail elbow', (2.8, -.29, .52), (.1, .9, .12), green, .045)

for x in (-1.75, 0, 1.75):
    for y in (-1.04, 1.6):
        cube('Portal foot', (x, y, .23), (.72, .38, .17), forest if x else ceramic, .075)

def area(name, location, energy, size, rgb, target=(0, 0, 1)):
    bpy.ops.object.light_add(type='AREA', location=location)
    light = bpy.context.object
    light.name = name
    light.data.energy = energy
    light.data.shape = 'DISK'
    light.data.size = size
    light.data.color = rgb
    light.rotation_euler = (Vector(target) - light.location).to_track_quat('-Z', 'Y').to_euler()

area('Large window | key', (-3, -5, 9), 1700, 7, (1, .95, .86))
area('Soft fill', (4, -1, 5), 850, 5, (.85, .96, 1))
area('Fine rim', (0, 5, 7), 1850, 5, (.95, 1, .94))

bpy.ops.object.camera_add(location=(10.4, -14.5, 10.3))
camera = bpy.context.object
camera.name = 'Editorial isometric camera'
camera.rotation_euler = (Vector((.05, .0, 1.25)) - camera.location).to_track_quat('-Z', 'Y').to_euler()
camera.data.type = 'ORTHO'
camera.data.ortho_scale = 11.2
scene = bpy.context.scene
scene.camera = camera
scene.render.engine = 'CYCLES'
scene.cycles.samples = 64
scene.cycles.use_denoising = True
scene.cycles.max_bounces = 8
scene.render.resolution_x = 1600
scene.render.resolution_y = 1200
scene.render.resolution_percentage = 100
scene.render.image_settings.file_format = 'PNG'
scene.render.image_settings.color_mode = 'RGBA'
scene.render.film_transparent = False
scene.world.use_nodes = True
world_shader = scene.world.node_tree.nodes.get('Background')
world_shader.inputs['Color'].default_value = color('#f7f8f4')
world_shader.inputs['Strength'].default_value = .35
scene.view_settings.view_transform = 'AgX'

device_names = []
try:
    prefs = bpy.context.preferences.addons['cycles'].preferences
    prefs.compute_device_type = 'CUDA'
    prefs.get_devices()
    for device in prefs.devices:
        device.use = device.type == 'CUDA'
        if device.use:
            device_names.append(device.name)
    if device_names:
        scene.cycles.device = 'GPU'
except Exception:
    scene.cycles.device = 'CPU'

scene['asset_purpose'] = 'Explanatory illustration of bounded action and evidence; not operational proof.'
scene['authoring'] = 'Original procedural geometry for HUMMBL, 2026-09-26.'
scene['palette_source'] = 'hummbl-production/web/tokens.css'
scene['renderer_device_names'] = ', '.join(device_names) or 'CPU'
scene.render.filepath = str(OUT / 'boundary-study-light.png')
bpy.ops.wm.save_as_mainfile(filepath=str(OUT / 'hummbl-boundary-study.blend'))
bpy.ops.render.render(write_still=True)
scene.render.image_settings.file_format = 'JPEG'
scene.render.image_settings.color_mode = 'RGB'
scene.render.image_settings.quality = 90
bpy.data.images['Render Result'].save_render(str(OUT / 'boundary-study-light.jpg'), scene=scene)
scene.render.image_settings.file_format = 'PNG'
scene.render.image_settings.color_mode = 'RGBA'

paper.node_tree.nodes.get('Principled BSDF').inputs['Base Color'].default_value = color('#0b1a14')
ceramic.node_tree.nodes.get('Principled BSDF').inputs['Base Color'].default_value = color('#365d49')
plinth.node_tree.nodes.get('Principled BSDF').inputs['Base Color'].default_value = color('#122b20')
world_shader.inputs['Color'].default_value = color('#163b2c')
world_shader.inputs['Strength'].default_value = .25
scene.render.filepath = str(OUT / 'boundary-study-dark.png')
bpy.ops.render.render(write_still=True)
scene.render.image_settings.file_format = 'JPEG'
scene.render.image_settings.color_mode = 'RGB'
scene.render.image_settings.quality = 90
bpy.data.images['Render Result'].save_render(str(OUT / 'boundary-study-dark.jpg'), scene=scene)

(OUT / 'blender-render-receipt.json').write_text(json.dumps({
    'blender': bpy.app.version_string, 'engine': scene.render.engine,
    'device': device_names or ['CPU'], 'samples': scene.cycles.samples,
    'dimensions': [1600, 1200], 'source': 'build_boundary_scene.py',
    'editable': 'hummbl-boundary-study.blend',
    'outputs': ['boundary-study-light.png', 'boundary-study-dark.png'],
    'scope': 'Original explanatory artwork; no measured operational assertions.'
}, indent=2) + '\n', encoding='utf-8')
print('HUMMBL_RENDER_COMPLETE')
