import bpy, math, mathutils, random
from mathutils import Vector
from math import radians, sin, cos, atan2, sqrt

# clean scene
if bpy.ops.object.mode_set.poll():
    try: bpy.ops.object.mode_set(mode='OBJECT')
    except: pass
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
scene=bpy.context.scene
scene.render.engine='BLENDER_EEVEE'
scene.render.resolution_x=768; scene.render.resolution_y=768; scene.render.resolution_percentage=100
scene.render.image_settings.file_format='PNG'; scene.render.film_transparent=False
try: scene.view_settings.view_transform='AgX'
except: pass
try: scene.view_settings.look='AgX - Medium High Contrast'
except: pass
scene.view_settings.exposure=0.0; scene.view_settings.gamma=1.0
# collections
root=scene.collection
collections={}
for nm in ['ARCHITECTURE','CHARACTER','GROUND','LIGHTING','DETAILS']:
    c=bpy.data.collections.new(nm); root.children.link(c); collections[nm]=c

def move_to(obj,coll):
    for c in list(obj.users_collection): c.objects.unlink(obj)
    collections[coll].objects.link(obj); return obj

def make_mat(name,color,metallic=0.0,rough=0.5,alpha=1.0,transmission=0.0,emission=None,emission_strength=0.0):
    m=bpy.data.materials.get(name) or bpy.data.materials.new(name); m.use_nodes=True
    bs=m.node_tree.nodes.get('Principled BSDF')
    if bs:
        if bs.inputs.get('Base Color'): bs.inputs['Base Color'].default_value=(*color,alpha)
        if bs.inputs.get('Metallic'): bs.inputs['Metallic'].default_value=metallic
        if bs.inputs.get('Roughness'): bs.inputs['Roughness'].default_value=rough
        if bs.inputs.get('Alpha'): bs.inputs['Alpha'].default_value=alpha
        if bs.inputs.get('Transmission Weight'): bs.inputs['Transmission Weight'].default_value=transmission
        elif bs.inputs.get('Transmission'): bs.inputs['Transmission'].default_value=transmission
        if emission is not None:
            if bs.inputs.get('Emission Color'): bs.inputs['Emission Color'].default_value=(*emission,1)
            elif bs.inputs.get('Emission'): bs.inputs['Emission'].default_value=(*emission,1)
            if bs.inputs.get('Emission Strength'): bs.inputs['Emission Strength'].default_value=emission_strength
    m.diffuse_color=(*color,alpha)
    if alpha<.999:
        try: m.surface_render_method='DITHERED'
        except: pass
        try: m.use_transparency_overlap=False
        except: pass
    return m

def make_noise_mat(name,c1,c2,scale=4.0,rough=.7,metallic=0.0,bump=.15):
    m=bpy.data.materials.new(name); m.use_nodes=True; nt=m.node_tree; nt.nodes.clear()
    out=nt.nodes.new('ShaderNodeOutputMaterial'); bs=nt.nodes.new('ShaderNodeBsdfPrincipled')
    tex=nt.nodes.new('ShaderNodeTexNoise'); tex.inputs['Scale'].default_value=scale; tex.inputs['Detail'].default_value=3.0; tex.inputs['Roughness'].default_value=rough
    ramp=nt.nodes.new('ShaderNodeValToRGB'); ramp.color_ramp.elements[0].color=(*c1,1); ramp.color_ramp.elements[1].color=(*c2,1)
    bumpn=nt.nodes.new('ShaderNodeBump'); bumpn.inputs['Strength'].default_value=bump; bumpn.inputs['Distance'].default_value=.12
    nt.links.new(tex.outputs['Fac'],ramp.inputs['Fac']); nt.links.new(ramp.outputs['Color'],bs.inputs['Base Color']); nt.links.new(tex.outputs['Fac'],bumpn.inputs['Height']); nt.links.new(bumpn.outputs['Normal'],bs.inputs['Normal']); nt.links.new(bs.outputs['BSDF'],out.inputs['Surface'])
    bs.inputs['Roughness'].default_value=.78; bs.inputs['Metallic'].default_value=metallic; m.diffuse_color=(*c1,1); return m
# materials
ground_mat=make_noise_mat('Dust_ochre_grit',(0.095,.046,.018),(.24,.135,.055),scale=3.2,rough=.82,bump=.28)
wall_mat=make_noise_mat('Weathered_colony_planks',(.17,.075,.027),(.38,.20,.07),scale=5.5,rough=.82,bump=.2)
wall_dark=make_mat('Board_shadow',(.055,.022,.010),rough=.88); wood=make_mat('Porch_wood',(.20,.075,.025),rough=.72); wood_light=make_mat('Worn_edge_wood',(.34,.14,.045),rough=.68)
coat=make_noise_mat('Duster_brown_canvas',(.075,.018,.008),(.25,.075,.018),scale=7,rough=.78,bump=.16); coat_dark=make_mat('Duster_fold_shadow',(.045,.010,.004),rough=.88)
leather=make_noise_mat('Belt_and_holster_leather',(.025,.006,.003),(.14,.028,.008),scale=11,rough=.68,bump=.2); leather_edge=make_mat('Leather_edges',(.22,.055,.012),rough=.55)
suit=make_mat('Vac_suit_navy',(.012,.027,.065),metallic=.15,rough=.34); suit_high=make_mat('Vac_suit_seams',(.035,.095,.14),metallic=.25,rough=.27)
polymer=make_mat('Hard_polymer',(.018,.032,.044),metallic=.18,rough=.24); polymer_light=make_mat('Polymer_highlights',(.10,.16,.19),metallic=.35,rough=.22)
metal=make_mat('Brushed_steel',(.20,.25,.28),metallic=.88,rough=.25); metal_dark=make_mat('Oxidized_steel',(.055,.072,.075),metallic=.78,rough=.42); brass=make_mat('Aged_brass',(.42,.17,.035),metallic=.78,rough=.28); copper=make_mat('Copper_conduit',(.32,.07,.018),metallic=.82,rough=.30)
red=make_mat('Warning_red',(.30,.018,.008),metallic=.15,rough=.30); cyan=make_mat('Energy_cyan',(.015,.18,.32),metallic=.25,rough=.18,emission=(.01,.3,.8),emission_strength=5.0); orange=make_mat('Interior_amber',(.42,.075,.012),rough=.34,emission=(1.0,.12,.015),emission_strength=4.0)
window_mat=make_mat('Dusty_window_glass',(.045,.09,.085),metallic=.05,rough=.24,alpha=.44,transmission=.25); glass=make_mat('Helmet_dome_glass',(.05,.19,.27),metallic=.05,rough=.10,alpha=.24,transmission=.68)
skin=make_mat('Warm_skin',(.46,.20,.105),rough=.48); skin_light=make_mat('Face_highlight',(.60,.30,.16),rough=.43); hair=make_mat('Hair_and_beard',(.022,.009,.004),rough=.63)
eye_white=make_mat('Eye_white',(.72,.70,.60),rough=.3); iris=make_mat('Iris',(.015,.06,.075),metallic=.1,rough=.2); glove=make_mat('Sealed_glove',(.055,.10,.13),metallic=.25,rough=.30); dust_dark=make_mat('Footprint_dust',(.035,.016,.007),rough=.95)
sign_mat=make_mat('Sign_paint_cream',(.78,.49,.16),metallic=.1,rough=.48); sign_dark=make_mat('Sign_backing',(.075,.018,.006),rough=.60)
# helpers
def assign_mat(obj,material):
    if material: obj.data.materials.append(material)
def bevel(obj,width=.04,segments=2):
    if width<=0:return obj
    mod=obj.modifiers.new('Softened manufactured edges','BEVEL'); mod.width=width; mod.segments=segments
    try: mod.limit_method='ANGLE'
    except: pass
    return obj
def cube(name,loc,dims,material,rot=(0,0,0),bev=.04,coll='DETAILS'):
    bpy.ops.mesh.primitive_cube_add(location=loc,rotation=rot); o=bpy.context.object; o.name=name; o.dimensions=dims; bpy.ops.object.transform_apply(location=False,rotation=False,scale=True); assign_mat(o,material); bevel(o,bev,3 if bev>.05 else 2); move_to(o,coll); return o
def uv(name,loc,scale,material,rot=(0,0,0),coll='DETAILS',seg=32,rings=20):
    bpy.ops.mesh.primitive_uv_sphere_add(segments=seg,ring_count=rings,location=loc,rotation=rot); o=bpy.context.object; o.name=name; o.scale=scale; bpy.ops.object.transform_apply(location=False,rotation=False,scale=True); assign_mat(o,material); bpy.ops.object.shade_smooth(); move_to(o,coll); return o
def ico(name,loc,scale,material,coll='GROUND',subdivisions=2):
    bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=subdivisions,radius=1,location=loc); o=bpy.context.object; o.name=name; o.scale=scale; bpy.ops.object.transform_apply(location=False,rotation=False,scale=True); assign_mat(o,material); bpy.ops.object.shade_smooth(); move_to(o,coll); return o
def cyl_between(name,p1,p2,radius,material,coll='DETAILS',verts=24,bev=.015):
    p1,p2=Vector(p1),Vector(p2); d=p2-p1; L=d.length
    if L<1e-5:return None
    bpy.ops.mesh.primitive_cylinder_add(vertices=verts,radius=radius,depth=L,location=(p1+p2)/2); o=bpy.context.object; o.name=name; o.rotation_mode='QUATERNION'; o.rotation_quaternion=d.to_track_quat('Z','Y'); o.rotation_mode='XYZ'; assign_mat(o,material); bevel(o,bev,2); move_to(o,coll); return o
def cone_between(name,p1,p2,r1,r2,material,coll='DETAILS',verts=24,bev=.012):
    p1,p2=Vector(p1),Vector(p2); d=p2-p1; L=d.length
    bpy.ops.mesh.primitive_cone_add(vertices=verts,radius1=r1,radius2=r2,depth=L,location=(p1+p2)/2); o=bpy.context.object; o.name=name; o.rotation_mode='QUATERNION'; o.rotation_quaternion=d.to_track_quat('Z','Y'); o.rotation_mode='XYZ'; assign_mat(o,material); bevel(o,bev,2); move_to(o,coll); return o
def torus(name,loc,major,minor,material,scale=(1,1,1),rot=(0,0,0),coll='DETAILS'):
    bpy.ops.mesh.primitive_torus_add(major_radius=major,minor_radius=minor,major_segments=48,minor_segments=12,location=loc,rotation=rot); o=bpy.context.object; o.name=name; o.scale=scale; bpy.ops.object.transform_apply(location=False,rotation=False,scale=True); assign_mat(o,material); bpy.ops.object.shade_smooth(); move_to(o,coll); return o
def curve_tube(name,points,radius,material,coll='DETAILS',resolution=2):
    cu=bpy.data.curves.new(name+'_Curve','CURVE'); cu.dimensions='3D'; cu.resolution_u=resolution; cu.bevel_depth=radius; cu.bevel_resolution=3
    spl=cu.splines.new('BEZIER'); spl.bezier_points.add(len(points)-1)
    for bp,co in zip(spl.bezier_points,points): bp.co=co; bp.handle_left_type='AUTO'; bp.handle_right_type='AUTO'
    o=bpy.data.objects.new(name,cu); collections[coll].objects.link(o); assign_mat(o,material); bpy.context.view_layer.objects.active=o; o.select_set(True)
    try: bpy.ops.object.convert(target='MESH')
    except: pass
    o=bpy.context.object; o.name=name
    for c in list(o.users_collection):
        if c!=collections[coll]: c.objects.unlink(o)
    if collections[coll] not in o.users_collection: collections[coll].objects.link(o)
    o.select_set(False); return o
def poly_extrude(name,xz_points,yfront,depth,material,coll='DETAILS',bev=.025):
    n=len(xz_points); verts=[(x,yfront,z) for x,z in xz_points]+[(x,yfront+depth,z) for x,z in xz_points]; faces=[tuple(range(n)),tuple(range(2*n-1,n-1,-1))]
    for i in range(n): j=(i+1)%n; faces.append((i,j,n+j,n+i))
    me=bpy.data.meshes.new(name+'_Mesh'); me.from_pydata(verts,[],faces); me.update(); o=bpy.data.objects.new(name,me); collections[coll].objects.link(o); assign_mat(o,material); bevel(o,bev,2); return o
def annular_ring(name,loc,outer,inner,z0,z1,material,coll='DETAILS',segments=48):
    verts=[]
    for z in [z0,z1]:
        for rx,ry in [outer,inner]:
            for i in range(segments): a=2*math.pi*i/segments; verts.append((loc[0]+rx*cos(a),loc[1]+ry*sin(a),z))
    S=segments; faces=[]
    for i in range(S):
        j=(i+1)%S; faces += [((i),(j),(2*S+j),(2*S+i)),((S+i),(3*S+i),(3*S+j),(S+j)),((2*S+i),(2*S+j),(3*S+j),(3*S+i)),((i),(S+i),(S+j),(j))]
    me=bpy.data.meshes.new(name+'_Mesh'); me.from_pydata(verts,[],faces); me.update(); o=bpy.data.objects.new(name,me); collections[coll].objects.link(o); assign_mat(o,material); bevel(o,.018,2); return o
def text_obj(name,text,loc,size,material,extrude=.025,rot=(radians(90),0,0),coll='ARCHITECTURE'):
    cu=bpy.data.curves.new(name+'_TextCurve','FONT'); cu.body=text; cu.align_x='CENTER'; cu.align_y='CENTER'; cu.size=size; cu.extrude=extrude; cu.bevel_depth=.008; cu.bevel_resolution=2; cu.resolution_u=10
    o=bpy.data.objects.new(name,cu); collections[coll].objects.link(o); o.location=loc; o.rotation_euler=rot; assign_mat(o,material); bpy.context.view_layer.objects.active=o; o.select_set(True)
    try:bpy.ops.object.convert(target='MESH')
    except:pass
    o=bpy.context.object; o.name=name; o.select_set(False); return o
result={'stage':'setup','materials':len(bpy.data.materials)}