import bpy, math, os, random
from mathutils import Vector
from math import sin, cos, pi

# Fresh scene
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.render.engine = 'BLENDER_EEVEE_NEXT'
scene.render.resolution_x = 900
scene.render.resolution_y = 900
scene.render.resolution_percentage = 60
scene.render.image_settings.file_format = 'PNG'
scene.render.film_transparent = False
# crisp graphic color management
scene.view_settings.look = 'AgX - Medium High Contrast'
scene.view_settings.exposure = 0.15
scene.view_settings.gamma = 1.0

# World: dark indigo print stock
world = bpy.data.worlds.new('Night_Market_World') if not bpy.data.worlds else bpy.data.worlds[0]
scene.world = world
world.use_nodes = True
wn = world.node_tree.nodes
wl = world.node_tree.links
wn.clear()
out = wn.new('ShaderNodeOutputWorld')
bg = wn.new('ShaderNodeBackground')
bg.inputs['Color'].default_value = (0.002, 0.003, 0.012, 1)
bg.inputs['Strength'].default_value = 0.12
wl.new(bg.outputs['Background'], out.inputs['Surface'])

# Collections

def coll(name):
    c = bpy.data.collections.get(name) or bpy.data.collections.new(name)
    if c.name not in scene.collection.children:
        try: scene.collection.children.link(c)
        except: pass
    return c
C_BG=coll('01_MARKET_ARCHITECTURE')
C_SIGNS=coll('02_MULTICULTURAL_SIGNS')
C_CART=coll('03_RAMEN_CART')
C_HERO=coll('04_RAMEN_SAMURAI')
C_NOODLES=coll('05_NOODLE_CHOREOGRAPHY')
C_FX=coll('06_INK_HALFTONE_MOTION')
C_FRAME=coll('07_PANEL_FRAME')

# Materials: emissive hand-printed flats (no realistic glossy response)
def emat(name, color, strength=1.0):
    m=bpy.data.materials.get(name) or bpy.data.materials.new(name)
    m.use_nodes=True
    n=m.node_tree.nodes; l=m.node_tree.links
    n.clear()
    e=n.new('ShaderNodeEmission')
    e.inputs['Color'].default_value=(*color,1)
    e.inputs['Strength'].default_value=strength
    o=n.new('ShaderNodeOutputMaterial')
    l.new(e.outputs['Emission'],o.inputs['Surface'])
    m.diffuse_color=(*color,1)
    m['graphic_flat']=True
    m['emission_strength']=strength
    return m

INK=emat('INK_outline',(0.003,0.002,0.008),0.25)
BLACK=emat('BROWN_BLACK_print',(0.012,0.006,0.014),0.35)
INDIGO=emat('DARK_indigo',(0.018,0.035,0.11),0.9)
NAVY=emat('NAVY_robes',(0.025,0.07,0.18),1.0)
CONCRETE=emat('MUTED_concrete',(0.09,0.075,0.09),0.8)
CONCRETE2=emat('CONCRETE_shadow',(0.035,0.042,0.065),0.7)
BROWN=emat('AGED_brown',(0.12,0.045,0.025),0.85)
STEEL=emat('DULL_steel',(0.18,0.21,0.24),0.9)
DIRTY=emat('DIRTY_white',(0.76,0.72,0.52),1.05)
CREAM=emat('BONE_highlight',(0.95,0.76,0.42),1.15)
ACID=emat('ACID_green',(0.42,1.0,0.025),1.35)
CHART=emat('CHARTREUSE',(0.78,1.0,0.02),1.3)
LIME=emat('LIME_ink',(0.58,0.95,0.05),1.25)
AMBER=emat('AMBER_orange',(1.0,0.30,0.025),1.35)
SODIUM=emat('SODIUM_yellow',(1.0,0.62,0.035),1.25)
GOLD=emat('BROTH_gold',(1.0,0.78,0.06),1.45)
CRIMSON=emat('DEEP_crimson',(0.68,0.012,0.025),1.2)
VERMILION=emat('VERMILION',(1.0,0.07,0.025),1.25)
CYAN=emat('SELECTIVE_cyan',(0.02,0.72,0.88),1.25)
TURQ=emat('TURQUOISE',(0.01,0.85,0.65),1.2)
VIOLET=emat('ULTRAVIOLET',(0.42,0.05,0.9),1.1)
MAGENTA=emat('MAGENTA_accent',(1.0,0.025,0.42),1.1)
PAPER=emat('PAPER',(0.9,0.82,0.58),1.0)

# Helpers
def link_obj(obj, collection):
    # ensure only desired collection link, unlink scene collection if present
    for c in list(obj.users_collection):
        c.objects.unlink(obj)
    collection.objects.link(obj)
    return obj

def box(name, loc, dims, mat, collection=C_BG, bevel=0.0, rot=(0,0,0)):
    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)
    if mat: o.data.materials.append(mat)
    link_obj(o,collection)
    if bevel:
        mod=o.modifiers.new('tiny_ink_round','BEVEL'); mod.width=bevel; mod.segments=1
    return o

def cyl(name, loc, radius, depth, mat, collection=C_BG, rot=(0,0,0), vertices=24, bevel=0.0):
    bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius, depth=depth, location=loc, rotation=rot)
    o=bpy.context.object; o.name=name
    if mat: o.data.materials.append(mat)
    link_obj(o,collection)
    if bevel:
        mod=o.modifiers.new('printed_edge','BEVEL'); mod.width=bevel; mod.segments=1
    return o

def uv(name, loc, scale, mat, collection=C_BG, segments=24, rings=12):
    bpy.ops.mesh.primitive_uv_sphere_add(segments=segments, ring_count=rings, location=loc)
    o=bpy.context.object; o.name=name; o.scale=scale
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    if mat:o.data.materials.append(mat)
    link_obj(o,collection)
    return o

def poly(name, pts, y, depth, mat, collection=C_BG, bevel=0.0):
    # x,z points -> thin extruded polygon centered at y
    verts=[]
    for x,z in pts: verts.append((x,y-depth/2,z))
    for x,z in pts: verts.append((x,y+depth/2,z))
    n=len(pts)
    faces=[]
    faces.append(tuple(range(n-1,-1,-1)))
    faces.append(tuple(range(n,2*n)))
    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); collection.objects.link(o)
    if mat:o.data.materials.append(mat)
    if bevel:
        mod=o.modifiers.new('ink_edge','BEVEL'); mod.width=bevel; mod.segments=1
    return o

def poly_layer(name, pts, y, mat, collection=C_BG, scale=1.0, outline=0.0, depth=0.12):
    cx=sum(p[0] for p in pts)/len(pts); cz=sum(p[1] for p in pts)/len(pts)
    if outline:
        op=[(cx+(x-cx)*(scale+outline),cz+(z-cz)*(scale+outline)) for x,z in pts]
        poly(name+'_INK',op,y+0.08,depth+0.08,INK,collection)
    ip=[(cx+(x-cx)*scale,cz+(z-cz)*scale) for x,z in pts]
    return poly(name,ip,y,depth,mat,collection)

def curve_obj(name, pts, bevel, mat, collection=C_FX, cyclic=False, resolution=2):
    cu=bpy.data.curves.new(name+'_curve','CURVE'); cu.dimensions='3D'; cu.resolution_u=resolution
    cu.bevel_depth=bevel; cu.bevel_resolution=0; cu.resolution_u=2; cu.fill_mode='FULL'
    sp=cu.splines.new('BEZIER'); sp.bezier_points.add(len(pts)-1)
    for bp,co in zip(sp.bezier_points,pts):
        bp.co=co; bp.handle_left_type='AUTO'; bp.handle_right_type='AUTO'
    sp.use_cyclic_u=cyclic
    o=bpy.data.objects.new(name,cu); collection.objects.link(o)
    cu.materials.append(mat)
    return o

def noodle(name, xzpts, color, width=0.09, y=-0.5, outline=0.14, collection=C_NOODLES, misreg=False):
    pts=[(x,y,z) for x,z in xzpts]
    curve_obj(name+'_black',pts,outline,INK,collection,resolution=3)
    # offset colored registration underlay for print error
    if misreg:
        curve_obj(name+'_cyan_shift',[(x+0.06,y+0.035,z+0.025) for x,z in xzpts],width*0.72,CYAN,collection,resolution=3)
        curve_obj(name+'_red_shift',[(x-0.055,y+0.045,z-0.02) for x,z in xzpts],width*0.70,VERMILION,collection,resolution=3)
    return curve_obj(name,pts,width,color,collection,resolution=3)

def line_between(name,a,b,r,mat,collection=C_FX):
    # cylinder between 3d points
    a=Vector(a); b=Vector(b); d=b-a
    mid=(a+b)/2
    bpy.ops.mesh.primitive_cylinder_add(vertices=12, radius=r, depth=d.length, location=mid)
    o=bpy.context.object; o.name=name
    o.rotation_mode='QUATERNION'; o.rotation_quaternion=d.to_track_quat('Z','Y')
    if mat:o.data.materials.append(mat)
    link_obj(o,collection)
    return o

def text_obj(name, body, loc, size, mat, collection=C_SIGNS, align='CENTER', rot=(pi/2,0,0), extrude=0.012, shadow=True):
    # face toward camera along -Y
    if shadow:
        c=bpy.data.curves.new(name+'_shadow_curve','FONT'); c.body=body; c.align_x=align; c.size=size; c.extrude=extrude*1.2; c.offset=0.01
        so=bpy.data.objects.new(name+'_INK',c); collection.objects.link(so); so.location=(loc[0]+0.045,loc[1]+0.06,loc[2]-0.035); so.rotation_euler=rot; c.materials.append(INK)
    c=bpy.data.curves.new(name+'_curve','FONT'); c.body=body; c.align_x=align; c.align_y='CENTER'; c.size=size; c.extrude=extrude; c.offset=0.0
    o=bpy.data.objects.new(name,c); collection.objects.link(o); o.location=loc; o.rotation_euler=rot; c.materials.append(mat)
    return o

def torus(name, loc, major, minor, mat, collection=C_BG, rot=(0,0,0)):
    bpy.ops.mesh.primitive_torus_add(major_radius=major, minor_radius=minor, major_segments=32, minor_segments=8, location=loc, rotation=rot)
    o=bpy.context.object; o.name=name; o.data.materials.append(mat); link_obj(o,collection); return o

def dots(name, center, w,h, spacing, r, mat, y, collection=C_FX, rot=(pi/2,0,0), jitter=0.0):
    x0,z0=center
    ix=0; x=-w/2
    while x<=w/2+1e-6:
        iz=0; z=-h/2
        while z<=h/2+1e-6:
            jx=(random.random()-0.5)*jitter; jz=(random.random()-0.5)*jitter
            # cylinder disc facing camera
            cyl(f'{name}_{ix}_{iz}',(x0+x+jx,y,z0+z+jz),r,0.035,mat,collection,rot=rot,vertices=12)
            z+=spacing; iz+=1
        x+=spacing; ix+=1

# ---------------- BACKDROP / CITY LAYERS ----------------
# giant inked panel stock and buildings
box('Backdrop_ink',(0,4.8,6.0),(25,0.5,14),BLACK,C_BG)
box('Backdrop_concrete',(0,4.45,6.0),(24.3,0.18,13.3),CONCRETE2,C_BG)
# building masses
box('Tower_left',(-8.1,3.7,7.0),(6.0,0.7,14.0),INDIGO,C_BG)
box('Tower_right',(8.0,3.6,7.0),(6.3,0.7,14.0),BROWN,C_BG)
box('Tower_mid',(1.5,4.0,8.0),(5.2,0.45,11.5),CONCRETE2,C_BG)
# vertical shadow slabs and seams
for x in (-10.5,-6.2,-3.5,4.8,9.8,11.0):
    box('vertical_ink_seam', (x,3.25,6.5),(0.18,0.12,12),BLACK,C_BG)
# rows of windows, hard graphic rectangles
random.seed(7)
win_mats=[AMBER,SODIUM,CRIMSON,ACID,CYAN,DIRTY]
for row,z in enumerate([3.2,4.4,5.6,6.8,8.0,9.2,10.4]):
    for col,x in enumerate([-10.2,-8.9,-7.6,-6.3,-5.0,-3.7,0.0,1.25,2.5,4.9,6.2,7.5,8.8,10.1]):
        if random.random()<0.60:
            w=random.choice([0.35,0.5,0.75]); h=random.choice([0.18,0.28,0.42])
            box('window_%02d_%02d'%(row,col),(x,3.20,z),(w,0.09,h),random.choice(win_mats),C_BG)
            if random.random()<0.27:
                box('window_glitch',(x+0.03,3.13,z+h*0.45),(w*0.5,0.04,0.035),INK,C_FX)
# rooftop silhouettes, water tanks, antennae
box('roofline_left',(-8.4,2.9,11.7),(7.0,0.5,0.45),BLACK,C_BG)
box('roofline_right',(8.3,2.8,11.3),(6.5,0.5,0.55),BLACK,C_BG)
for x,z in [(-10,11.9),(-7.7,12.0),(7.0,11.6),(9.7,11.7)]:
    cyl('water_tank',(x,3.0,z),0.55,0.65,BLACK,C_BG)
    cyl('tank_band',(x,2.92,z),0.58,0.10,CRIMSON if x<0 else ACID,C_BG)
# floor / street slab
box('Street_ground',(0,0.8,-0.18),(28,16,0.35),BLACK,C_BG)
box('Street_surface',(0,0.7,0.02),(27.5,15.0,0.18),CONCRETE2,C_BG)
# painted floor perspective marks
for i,x in enumerate([-10,-6,-2,2,6,10]):
    line_between('floor_perspective_mark',(x,-1.5,0.13),(x*0.48,5.0,0.13),0.035,CRIMSON if i%2 else CYAN,C_FX)
for x in (-8,-4,0,4,8):
    box('floor_dash',(x,-0.8,0.15),(1.5,0.18,0.04),SODIUM,C_FX,rot=(0,0,0.1))

# ---------------- MULTICULTURAL SIGN SYSTEM ----------------
def sign_board(name, loc, dims, boardmat, text, tmat, size, rotz=0, extra=None):
    b=box(name+'_INK',(loc[0]+0.05,loc[1]+0.10,loc[2]-0.05),(dims[0]*1.04,dims[1]+0.14,dims[2]*1.08),INK,C_SIGNS,rot=(0,0,rotz))
    b=box(name,loc,dims,boardmat,C_SIGNS,rot=(0,0,rotz))
    # front text offset toward camera y-; rotate z in local board not perfect but okay
    t=text_obj(name+'_lettering',text,(loc[0],loc[1]-dims[1]*0.56,loc[2]),size,tmat,C_SIGNS,shadow=True)
    t.rotation_euler=(pi/2,0,rotz)
    if extra:
        extra()
    return b

# large signs, with distinct regions
sign_board('Sign_ramen_left',(-7.0,2.85,9.0),(5.1,0.25,1.05),ACID,'RAMEN  /  ラーメン',INK,0.46,rotz=-0.02)
# add red registration ghost on left sign
text_obj('Sign_ramen_redghost','RAMEN',( -6.94,2.62,8.96),0.39,VERMILION,C_FX,shadow=False)
sign_board('Sign_china_right',(7.2,2.82,9.15),(4.6,0.25,1.0),CRIMSON,'中華  CHOW',CREAM,0.42,rotz=0.03)
sign_board('Sign_global_middle',(5.8,2.76,7.55),(5.2,0.22,0.88),CHART,'TACOS  •  CHAI  •  K-FOOD',BLACK,0.27,rotz=-0.04)
sign_board('Sign_arabic',(-8.7,2.78,6.55),(3.7,0.22,0.86),AMBER,'مطبخ 24H',INK,0.42,rotz=0.02)
sign_board('Sign_spanish',(-5.8,2.72,4.75),(3.6,0.22,0.78),VERMILION,'TACOS Y RAMEN',CREAM,0.28,rotz=-0.05)
sign_board('Sign_south_asian',(9.3,2.72,5.55),(3.6,0.22,0.9),VIOLET,'CHAI  चाय',SODIUM,0.35,rotz=0.01)
# vertical sign
box('Vertical_sign_ink',(-3.95,2.48,7.1),(0.95,0.22,4.2),INK,C_SIGNS,rot=(0,0,-0.03))
box('Vertical_sign',(-4.02,2.38,7.1),(0.72,0.18,3.95),SODIUM,C_SIGNS,rot=(0,0,-0.03))
text_obj('Vertical_lettering','夜 食',(-4.02,2.25,7.1),0.52,CRIMSON,C_SIGNS,shadow=True)
# cyan micro-ad
box('Micro_ad_ink',(1.7,2.55,9.5),(2.4,0.16,0.58),INK,C_SIGNS)
box('Micro_ad',(1.7,2.47,9.5),(2.15,0.12,0.40),CYAN,C_SIGNS)
text_obj('Micro_ad_text','OPEN / 24',(1.7,2.38,9.5),0.24,INK,C_SIGNS,shadow=False)
# sign frame bars and halftone texture
for x in (-9.2,-4.7,4.7,9.6): box('sign_hanger',(x,2.55,10.0),(0.10,0.12,1.4),BLACK,C_SIGNS)
dots('halftone_amber',(-8.7,6.55),3.1,0.45,0.27,0.045,CRIMSON,2.63,C_FX,jitter=0.03)
dots('halftone_crimson',(7.2,9.15),4.0,0.55,0.34,0.05,AMBER,2.66,C_FX,jitter=0.04)
dots('halftone_violet',(9.3,5.55),2.7,0.5,0.30,0.04,INK,2.57,C_FX,jitter=0.03)

# ---------------- STREET PROPS ----------------
# patched awnings
box('Awning_left_ink',(-7.0,1.7,4.0),(5.0,1.0,0.40),INK,C_BG,rot=(0.12,0,0.03))
for i in range(6):
    x=-9.1+i*0.85
    box('Awning_left_patch',(x,1.12,3.86),(0.68,0.14,0.32),[CRIMSON,AMBER,ACID,CRIMSON,DIRTY,AMBER][i],C_BG,rot=(0.12,0,0.03))
box('Awning_right_ink',(8.2,1.9,4.0),(4.6,0.85,0.42),INK,C_BG,rot=(-0.08,0,-0.04))
for i in range(5):
    x=6.3+i*0.9
    box('Awning_right_patch',(x,1.45,3.84),(0.72,0.14,0.31),[ACID,BROWN,CRIMSON,SODIUM,CYAN][i],C_BG,rot=(-0.08,0,-0.04))
# vending machine left
box('Vending_ink',(-5.3,0.95,2.9),(2.0,0.9,3.7),INK,C_BG,bevel=0.08)
box('Vending_body',(-5.3,0.88,2.9),(1.72,0.72,3.42),STEEL,C_BG,bevel=0.05)
box('Vending_glow',(-5.3,0.47,3.35),(1.35,0.035,1.75),ACID,C_BG)
for z in (2.75,3.15,3.55,3.95):
    box('vending_slot',(-5.3,0.42,z),(1.15,0.025,0.12),INK,C_FX)
    cyl('vending_can',(-5.75,0.38,z+0.14),0.12,0.04,AMBER,C_FX,rot=(pi/2,0,0),vertices=16)
# hanging wires and cables
cable_paths=[ [(-11,1.5,10.7),(-8,0.8,9.9),(-5,0.9,10.5),(-2,1.1,9.7),(2,1.0,10.2),(6,0.8,9.6),(11,1.2,10.4)],
              [(-10,0.7,8.2),(-7,0.4,7.6),(-3,0.7,8.1),(1,0.6,7.4),(5,0.8,8.2),(10,1.0,7.4)],
              [(-4,0.0,11.3),(-3.4,-0.3,8.8),(-2.9,-0.2,6.2)] ]
for i,p in enumerate(cable_paths):
    curve_obj('hanging_cable_%02d'%i, p, 0.045, BLACK,C_BG,resolution=2)
    if i<2:
        curve_obj('cable_color_echo_%02d'%i,[(x,y-0.035,z+0.02) for x,y,z in p],0.018,CRIMSON if i==0 else CYAN,C_FX,resolution=2)
# lanterns across street
for i,(x,z,ma) in enumerate([(-10.5,7.9,CRIMSON),(-3.0,8.7,SODIUM),(4.2,8.5,VERMILION),(10.0,7.8,AMBER)]):
    line_between('lantern_wire',(x,1.0,z+1.0),(x,1.0,z+0.55),0.035,BLACK,C_BG)
    uv('lantern_ink',(x,0.72,z),(.48,.32,.60),INK,C_BG)
    uv('lantern',(x,0.65,z),(.34,.25,.46),ma,C_BG)
    box('lantern_band',(x,0.35,z),(0.48,0.05,0.08),DIRTY,C_FX)
    # two side tabs
    cyl('lantern_cap',(x,0.6,z+0.48),0.08,0.12,INK,C_BG)
# stools
for x in (-3.7,5.6,7.2):
    cyl('stool_seat',(x,-0.2,1.25),0.52,0.25,INK,C_BG,vertices=16)
    cyl('stool_top',(x,-0.34,1.30),0.40,0.16,CRIMSON if x<0 else SODIUM,C_BG,vertices=16)
    for dx in (-0.28,0.28): line_between('stool_leg',(x+dx,-0.18,1.15),(x+dx*1.12,-0.18,0.3),0.055,BLACK,C_BG)
# delivery bike silhouette at right rear
# wheels face camera
for x in (6.4,8.9):
    torus('bike_wheel_ink',(x,1.45,1.55),0.72,0.10,INK,C_BG,rot=(pi/2,0,0))
    torus('bike_wheel_echo',(x,1.36,1.55),0.57,0.055,CRIMSON,C_BG,rot=(pi/2,0,0))
line_between('bike_frame',(6.4,1.35,1.55),(7.5,1.32,2.55),0.09,CYAN,C_BG)
line_between('bike_frame2',(7.5,1.32,2.55),(8.9,1.35,1.55),0.09,CYAN,C_BG)
line_between('bike_bar',(7.5,1.32,2.55),(8.2,1.35,2.85),0.07,INK,C_BG)
line_between('bike_handle',(8.15,1.34,2.82),(8.72,1.34,2.7),0.07,INK,C_BG)
box('delivery_box',(7.4,1.25,3.0),(1.7,0.65,0.95),AMBER,C_BG,bevel=.06)
text_obj('delivery_text','出前',(7.4,0.88,3.0),0.28,INK,C_BG,shadow=False)

# ---------------- RAMEN CART ----------------
# black silhouette / cart body
box('Cart_body_ink',(2.6,0.15,1.65),(4.8,1.8,2.65),INK,C_CART,bevel=.10)
box('Cart_body',(2.6,0.03,1.72),(4.4,1.55,2.35),BROWN,C_CART,bevel=.06)
# panels and slats
box('Cart_front_panel',(2.6,-0.78,1.75),(3.8,0.07,1.55),INDIGO,C_CART)
box('Cart_front_red',(2.65,-0.83,1.75),(2.5,0.035,0.16),CRIMSON,C_CART)
for x in (1.0,2.0,3.0,4.0): box('cart_slats',(x,-0.86,1.75),(0.10,0.04,1.5),AMBER,C_FX)
# wheels
for x in (0.8,4.4):
    cyl('cart_wheel_ink',(x,-0.83,0.75),0.72,0.30,INK,C_CART,rot=(pi/2,0,0),vertices=24)
    cyl('cart_wheel',(x,-1.00,0.75),0.54,0.12,CRIMSON if x<2 else SODIUM,C_CART,rot=(pi/2,0,0),vertices=24)
    cyl('cart_hub',(x,-1.08,0.75),0.12,0.08,DIRTY,C_FX,rot=(pi/2,0,0),vertices=16)
# counter
box('Cart_counter_ink',(2.6,-0.05,2.85),(5.0,1.95,0.42),INK,C_CART,bevel=.08)
box('Cart_counter',(2.6,-0.18,2.92),(4.65,1.65,0.28),AMBER,C_CART,bevel=.04)
# canopy frame and canopy
for x in (0.3,4.9): line_between('cart_canopy_post',(x,0.3,3.1),(x,0.3,5.0),0.08,INK,C_CART)
box('Cart_canopy_ink',(2.6,0.25,4.95),(5.2,1.55,0.5),INK,C_CART,rot=(0,0,-0.02))
box('Cart_canopy',(2.6,0.12,4.92),(4.85,1.35,0.34),CRIMSON,C_CART,rot=(0,0,-0.02))
for i in range(7):
    x=0.45+i*0.72
    box('canopy_stripe',(x,-0.62,4.89),(0.42,0.06,0.34),[AMBER,DIRTY,ACID,AMBER,DIRTY,ACID,AMBER][i],C_CART,rot=(0,0,-0.02))
# cart marquee
box('Cart_marquee_ink',(2.6,-0.73,5.55),(3.9,0.18,0.88),INK,C_CART,bevel=.03)
box('Cart_marquee',(2.6,-0.85,5.55),(3.55,0.10,0.67),ACID,C_CART,bevel=.02)
text_obj('Cart_marquee_text','RAMEN  力',(2.6,-0.93,5.55),0.38,INK,C_CART,shadow=False)
# bowl, readable and central on counter
# bowl black and inner
bpy.ops.mesh.primitive_cone_add(vertices=32, radius1=1.18, radius2=0.91, depth=0.52, location=(2.55,-1.06,3.42))
bowl=bpy.context.object; bowl.name='BOWL_ink_silhouette'; bowl.data.materials.append(INK); link_obj(bowl,C_CART)
bpy.ops.mesh.primitive_cone_add(vertices=32, radius1=1.00, radius2=0.76, depth=0.42, location=(2.55,-1.15,3.46))
bowl=bpy.context.object; bowl.name='BOWL_vermilion'; bowl.data.materials.append(VERMILION); link_obj(bowl,C_CART)
# broth top disk
cyl('BROTH_surface',(2.55,-1.19,3.69),0.76,0.07,GOLD,C_CART,rot=(0,0,0),vertices=32)
torus('BOWL_rim',(2.55,-1.25,3.69),0.79,0.09,DIRTY,C_CART,rot=(0,0,0))
# toppings / egg / scallions
uv('egg',(2.25,-1.33,3.77),(.24,.10,.17),DIRTY,C_CART)
for x,z in [(2.8,3.78),(2.98,3.62),(2.35,3.58),(2.72,3.87)]:
    cyl('scallion',(x,-1.39,z),0.055,0.05,ACID,C_CART,rot=(pi/2,0,0),vertices=12)
# second sliding bowl on right
cyl('slide_bowl_ink',(4.25,-0.9,3.25),0.72,0.35,INK,C_CART,vertices=24)
cyl('slide_bowl',(4.25,-0.99,3.3),0.58,0.28,AMBER,C_CART,vertices=24)
cyl('slide_broth',(4.25,-1.15,3.47),0.45,0.06,CRIMSON,C_CART,vertices=24)
# cart sign SFX
text_obj('Cart_sfx','ズズッ!',(3.9,-1.02,4.36),0.32,CRIMSON,C_FX,shadow=True)

# ---------------- HERO: RAMEN SAMURAI ----------------
# posed legs and robe silhouette
# back leg/foot
poly_layer('Hero_back_foot',[(-2.55,0.72),(-1.42,0.72),(-0.78,1.12),(-1.00,1.48),(-2.20,1.40),(-2.72,1.05)],-0.35,CRIMSON,C_HERO,scale=1.0,outline=.06,depth=.22)
poly_layer('Hero_front_foot',[(-0.05,0.70),(1.18,0.70),(1.75,1.10),(1.43,1.50),(0.35,1.46),(-0.25,1.10)],-0.37,AMBER,C_HERO,scale=1.0,outline=.06,depth=.22)
# robe lower broad angular silhouette
robe=[(-2.25,4.35),(-1.65,5.0),(-0.55,4.25),(0.15,3.55),(1.42,1.55),(0.55,1.12),(-0.65,1.30),(-1.35,1.02),(-2.85,1.35),(-2.55,2.55),(-3.05,3.35)]
poly_layer('Hero_robe_body',robe,-0.28,NAVY,C_HERO,scale=1.0,outline=.10,depth=.30)
# robe magenta/red interior fold
poly_layer('Hero_robe_fold',[(-1.48,4.32),(-0.65,4.05),(0.18,3.40),(0.92,1.62),(0.4,1.35),(-0.2,2.55),(-1.95,3.55)],-0.48,CRIMSON,C_HERO,scale=1.0,outline=.025,depth=.10)
poly_layer('Hero_robe_highlight',[(-2.42,3.35),(-1.62,3.82),(-1.15,3.35),(-1.48,1.32),(-2.6,1.42)],-0.50,ACID,C_HERO,scale=1.0,outline=.035,depth=.08)
poly_layer('Hero_robe_shadow',[(-0.7,3.95),(0.12,3.32),(1.36,1.6),(0.6,1.16),(-0.02,2.62)],-0.53,INDIGO,C_HERO,scale=1.0,outline=.02,depth=.08)
# torso twisted
body=[(-2.32,4.15),(-2.10,6.85),(-1.25,7.45),(-0.25,7.35),(0.62,6.45),(0.25,4.25),(-0.55,3.65)]
poly_layer('Hero_torso',body,-0.32,NAVY,C_HERO,scale=1.0,outline=.11,depth=.32)
# chest offset print ghost
poly('Hero_torso_red_registration',[(x-0.07,z+0.02) for x,z in body],-0.22,0.10,VERMILION,C_FX,0)
poly('Hero_torso_cyan_registration',[(x+0.08,z-0.02) for x,z in body],-0.24,0.08,CYAN,C_FX,0)
# chest armor plates
poly_layer('Hero_chest_plate',[(-1.72,6.68),(-0.72,7.05),(0.08,6.43),(-0.22,5.42),(-1.45,5.62)],-0.56,STEEL,C_HERO,scale=1.0,outline=.05,depth=.12)
poly_layer('Hero_chest_plate_color',[(-1.52,6.55),(-0.76,6.83),(-0.18,6.35),(-0.38,5.73),(-1.30,5.86)],-0.64,CRIMSON,C_HERO,scale=1.0,outline=.025,depth=.07)
# sash crossing torso
poly_layer('Hero_sash',[(-2.0,5.82),(-1.72,6.18),(0.35,4.55),(0.04,4.10),(-2.2,5.35)],-0.70,AMBER,C_HERO,scale=1.0,outline=.05,depth=.10)
box('Sash_buckle',(-0.56,-0.78,4.94),(0.36,0.10,0.46),ACID,C_HERO,rot=(0,0,-0.2))
# armor shoulder plates
poly_layer('Hero_shoulder_left',[(-2.3,6.62),(-2.85,7.05),(-2.95,7.58),(-2.43,7.78),(-1.75,7.23),(-1.78,6.55)],-0.50,CRIMSON,C_HERO,outline=.08,depth=.18)
poly_layer('Hero_shoulder_right',[(-0.35,6.72),(0.38,7.22),(1.18,7.20),(1.25,6.65),(0.65,6.22),(-0.12,6.20)],-0.53,ACID,C_HERO,outline=.08,depth=.18)
# shoulder armor panel lines
for pts,ma in [([(-2.55,7.02),(-2.15,6.72)],CREAM), ([(0.2,6.8),(0.95,6.82)],INK), ([(0.35,7.1),(0.92,6.48)],INK)]:
    line_between('armor_line',(pts[0][0],-0.72,pts[0][1]),(pts[1][0],-0.72,pts[1][1]),0.035,ma,C_HERO)
# neck
poly_layer('Hero_neck',[(-1.55,7.10),(-0.65,7.10),(-0.62,7.65),(-1.48,7.70)],-0.48,BROWN,C_HERO,outline=.04,depth=.16)
# head angular silhouette and face
head=[(-1.68,8.72),(-1.43,9.25),(-0.72,9.38),(-0.25,8.88),(-0.42,7.90),(-0.92,7.52),(-1.56,7.86)]
poly_layer('Hero_head_ink',head,-0.42,INK,C_HERO,scale=1.0,outline=.06,depth=.30)
face=[(-1.48,8.68),(-1.28,9.04),(-0.74,9.12),(-0.42,8.78),(-0.56,8.03),(-1.0,7.72),(-1.43,7.98)]
poly_layer('Hero_face',face,-0.62,DIRTY,C_HERO,scale=1.0,outline=.02,depth=.10)
# hair mass and topknot
hair=[(-1.7,8.72),(-1.56,9.33),(-1.13,9.62),(-0.57,9.57),(-0.24,9.08),(-0.62,8.86),(-1.10,9.08),(-1.43,8.66)]
poly_layer('Hero_hair',hair,-0.73,BLACK,C_HERO,outline=.06,depth=.10)
poly_layer('Hero_topknot',[(-1.2,9.52),(-1.18,10.12),(-0.78,10.36),(-0.45,10.08),(-0.58,9.60)],-0.57,BLACK,C_HERO,outline=.06,depth=.16)
# colored hair streak
poly('Hero_hair_streak',[(-1.43,9.32),(-1.14,9.60),(-0.92,9.52),(-1.22,9.18)],-0.82,0.05,CRIMSON,C_HERO)
# face graphic mask / cheek shadow
poly('Hero_face_shadow',[(-1.42,8.18),(-0.93,7.73),(-0.58,8.10),(-0.66,8.47),(-1.05,8.36)],-0.80,0.04,VERMILION,C_HERO)
poly('Hero_brow',[(-1.36,8.65),(-0.95,8.73),(-0.68,8.62),(-0.88,8.53),(-1.33,8.55)],-0.84,0.045,INK,C_HERO)
# eyes: controlled gaze to right
poly('Hero_eye_white',[(-1.32,8.45),(-1.00,8.53),(-0.77,8.46),(-1.04,8.37)],-0.86,0.05,PAPER,C_HERO)
poly('Hero_pupil',[(-0.92,8.45),(-0.79,8.48),(-0.82,8.36),(-0.94,8.35)],-0.90,0.04,INK,C_HERO)
# nose and mouth ink
poly('Hero_nose',[(-0.72,8.36),(-0.55,8.18),(-0.77,8.16)],-0.87,0.04,INK,C_HERO)
poly('Hero_mouth',[(-1.04,7.96),(-0.62,7.94),(-0.80,7.87),(-1.14,7.91)],-0.88,0.04,INK,C_HERO)
# ear accent
uv('Hero_ear',(-1.52,-0.86,8.35),(.16,.05,.22),AMBER,C_HERO)
# raised sleeve and arm
poly_layer('Hero_raised_sleeve',[(-0.28,6.76),(0.28,7.18),(1.24,7.62),(1.58,8.16),(1.12,8.43),(0.38,7.95),(-0.38,7.43)],-0.62,NAVY,C_HERO,outline=.08,depth=.18)
poly_layer('Hero_raised_sleeve_stripe',[(0.42,7.34),(1.2,7.82),(1.45,8.13),(1.23,8.3),(0.65,7.91)],-0.75,AMBER,C_HERO,outline=.025,depth=.08)
# raised bracer and hand
poly_layer('Hero_raised_bracer',[(1.18,7.90),(1.73,8.13),(2.08,8.42),(1.75,8.76),(1.25,8.47)],-0.72,STEEL,C_HERO,outline=.05,depth=.12)
poly_layer('Hero_raised_hand',[(1.73,8.38),(2.08,8.44),(2.35,8.73),(2.12,8.96),(1.78,8.82),(1.58,8.56)],-0.82,DIRTY,C_HERO,outline=.035,depth=.10)
# lower redirecting arm across body
poly_layer('Hero_lower_sleeve',[(-2.05,6.18),(-2.55,5.88),(-2.2,5.25),(-1.05,4.86),(-0.35,5.35),(-0.70,5.90),(-1.42,6.38)],-0.66,CRIMSON,C_HERO,outline=.08,depth=.18)
poly_layer('Hero_lower_sleeve_fold',[(-2.2,5.62),(-1.2,5.08),(-0.72,5.35),(-1.45,5.92)],-0.78,ACID,C_HERO,outline=.02,depth=.07)
poly_layer('Hero_lower_bracer',[(-0.9,5.36),(-0.35,5.15),(0.14,5.35),(-0.08,5.75),(-0.62,5.78)],-0.82,STEEL,C_HERO,outline=.045,depth=.12)
poly_layer('Hero_lower_hand',[(-0.38,5.18),(0.10,5.16),(0.42,5.43),(0.18,5.70),(-0.33,5.57),(-0.56,5.34)],-0.90,DIRTY,C_HERO,outline=.03,depth=.10)
# sheathed sword, visibly secondary
line_between('Hero_sword_sheath',(-2.05,-0.10,4.25),(0.18,-0.10,2.05),0.16,INK,C_HERO)
line_between('Hero_sword_color',(-2.05,-0.22,4.25),(0.18,-0.22,2.05),0.085,CRIMSON,C_HERO)
line_between('Hero_sword_edge',(-1.97,-0.28,4.30),(0.26,-0.28,2.12),0.025,DIRTY,C_HERO)
cyl('Hero_sword_guard',(-2.05,-0.38,4.25),0.28,0.08,GOLD,C_HERO,rot=(pi/2,0,0),vertices=12)
# hair tie and robe tabs
line_between('hair_tie',(-0.98,-0.86,9.45),(-0.38,-0.86,9.62),0.045,ACID,C_HERO)
poly_layer('robe_tab',[(-2.25,3.1),(-3.1,2.55),(-2.6,2.1),(-1.9,2.8)],-0.72,VERMILION,C_HERO,outline=.04,depth=.08)

# Chopsticks as paired control batons, highlighted black outlines
# raised pair point dramatically into attack
for i,(a,b,ma) in enumerate([((2.03,-1.02,8.78),(3.9,-1.05,9.62),AMBER),((2.14,-1.08,8.65),(4.05,-1.12,9.45),DIRTY)]):
    line_between('Raised_chopstick_%02d_ink'%i,a,b,0.085,INK,C_HERO)
    line_between('Raised_chopstick_%02d'%i,(a[0],a[1]-0.05,a[2]),(b[0],b[1]-0.05,b[2]),0.033,ma,C_HERO)
# lower pair close to body
for i,(a,b,ma) in enumerate([((0.05,-1.08,5.42),(1.25,-1.10,6.05),ACID),((0.20,-1.13,5.27),(1.42,-1.15,5.92),SODIUM)]):
    line_between('Lower_chopstick_%02d_ink'%i,a,b,0.075,INK,C_HERO)
    line_between('Lower_chopstick_%02d'%i,(a[0],a[1]-0.05,a[2]),(b[0],b[1]-0.05,b[2]),0.027,ma,C_HERO)

# ---------------- NOODLE CHOREOGRAPHY ----------------
# circular shield coil around hero
shield=[]
for i in range(28):
    t=-0.45 + i*(2*pi+0.90)/27
    shield.append((-0.55+2.02*cos(t), -1.34, 6.15+1.62*sin(t)))
noodle('NOODLE_shield_coil',[(x,z) for x,y,z in shield],CHART,width=.105,y=-1.34,outline=.22,misreg=True)
# selective inner red arc, not full ring
arc=[]
for i in range(13):
    t=1.0+i*1.55/12; arc.append((-0.55+2.08*cos(t),6.15+1.68*sin(t)))
noodle('NOODLE_shield_red_arc',arc,VERMILION,width=.075,y=-1.52,outline=.14,misreg=True)
# broad foreground foreshortened ribbon
foreground=[(-10,1.10),(-8.2,1.30),(-6.6,2.2),(-5.1,3.6),(-3.4,5.25),(-1.0,6.18),(1.6,6.0),(4.0,4.65),(6.8,3.15),(10,2.85)]
noodle('NOODLE_foreground_whip',foreground,GOLD,width=.30,y=-3.0,outline=.48,misreg=True)
# inner vermilion streak on foreground
noodle('NOODLE_foreground_hotline',[(-9.0,1.38),(-6.4,2.3),(-4.9,3.6),(-2.7,5.2),(0.0,5.95),(2.7,5.5),(5.4,3.75),(8.9,2.9)],VERMILION,width=.075,y=-3.42,outline=.13,misreg=False)
# raised wide lash from chopsticks to open attack space
raised=[(2.0,8.78),(2.7,9.15),(3.55,9.48),(4.7,9.12),(5.8,8.20),(6.65,6.95),(7.6,7.65),(8.9,8.7),(10.4,8.55)]
noodle('NOODLE_raised_lash',raised,ACID,width=.16,y=-1.55,outline=.29,misreg=True)
# parallel lash echo
noodle('NOODLE_raised_lash_echo',[(x,z+0.22*sin(i/len(raised)*pi)) for i,(x,z) in enumerate(raised)],AMBER,width=.065,y=-1.66,outline=.12,misreg=False)
# sweeping lasso to outside wrist
lasso=[(0.52,6.0),(1.8,6.9),(3.4,7.1),(5.2,6.55),(6.45,5.7),(7.2,5.85),(7.75,6.45),(7.25,7.0),(6.45,6.9),(6.1,6.2),(6.55,5.55),(7.6,5.35),(9.2,5.55)]
noodle('NOODLE_wrist_lasso',lasso,LIME,width=.105,y=-1.45,outline=.21,misreg=True)
# blade from right edge, caught by noodle
blade=[(10.0,7.6),(9.4,7.35),(5.05,6.12),(5.18,6.35)]
poly_layer('Incoming_blade',blade,-1.05,DIRTY,C_FX,outline=.05,depth=.12)
poly_layer('Incoming_blade_red_edge',[(10.0,7.6),(9.4,7.35),(5.05,6.12),(5.18,6.35)],-1.16,CRIMSON,C_FX,scale=.995,outline=0,depth=.035)
# blade highlight
line_between('blade_highlight',(9.6,-1.24,7.37),(5.3,-1.24,6.28),0.035,PAPER,C_FX)
# implied opponent wrist and glove at edge
poly_layer('Opponent_forearm',[(10.0,5.0),(8.55,5.15),(7.55,5.75),(7.7,6.35),(8.9,6.55),(10.0,6.4)],-0.95,INDIGO,C_FX,outline=.10,depth=.20)
uv('Opponent_wrist',(7.55,-1.14,6.0),(.52,.18,.42),CRIMSON,C_FX)
# corkscrew bundle launched from bowl
for j,(phase,ma) in enumerate([(0.0,AMBER),(pi/2,ACID),(pi,CREAM),(3*pi/2,VERMILION)]):
    pts=[]
    for i in range(22):
        t=i/21
        x=2.55+3.9*t
        z=3.65+1.35*t+0.48*sin(t*5*pi+phase)
        pts.append((x,z))
    noodle('NOODLE_corkscrew_%02d'%j,pts,ma,width=.07 if j else .10,y=-1.55-0.03*j,outline=.15,misreg=(j==0))
# high looping strand around sign pole
high=[(1.55,8.7),(0.2,9.65),(-1.8,10.0),(-3.2,9.45),(-2.9,8.55),(-1.6,8.25),(-0.8,8.8),(-1.15,9.6),(-2.25,10.25),(-4.1,10.35),(-5.5,9.55)]
noodle('NOODLE_high_lasso',high,VERMILION,width=.105,y=-1.22,outline=.22,misreg=True)
# loose noodles wrapping cart / poles / stools
wrap1=[(2.55,3.7),(1.25,4.25),(0.35,4.85),(-0.25,4.25),(-0.05,3.35),(0.9,2.92),(1.7,3.25)]
noodle('NOODLE_cart_wrap',wrap1,ACID,width=.075,y=-1.25,outline=.16,misreg=False)
wrap2=[(3.0,4.1),(4.4,4.5),(5.15,4.15),(4.8,3.3),(4.15,2.8),(4.8,2.1),(6.0,1.9)]
noodle('NOODLE_stool_wrap',wrap2,GOLD,width=.08,y=-1.2,outline=.17,misreg=True)
wrap3=[(-2.4,5.6),(-3.2,6.2),(-4.3,6.1),(-4.5,5.25),(-3.8,4.8),(-3.2,5.1)]
noodle('NOODLE_sign_wrap',wrap3,CYAN,width=.07,y=-1.2,outline=.15,misreg=True)
# loose knot near the hero's lower hand, split/reconnect
knot=[(-0.35,5.32),(-0.8,5.0),(-0.4,4.65),(0.3,4.82),(0.1,5.3),(-0.6,5.55),(-0.95,5.1),(-0.5,4.55),(0.25,4.35),(1.2,4.45)]
noodle('NOODLE_knot_split',knot,CHART,width=.065,y=-1.62,outline=.14,misreg=True)
# noodle whip crack across upper right foreground
crack=[(-7.5,7.2),(-5.8,7.55),(-4.1,7.35),(-2.4,7.65),(-0.6,8.0),(1.7,7.55),(3.6,7.05),(5.1,7.5),(6.8,7.2),(8.4,6.75),(9.8,6.9)]
noodle('NOODLE_crack_whip',crack,CRIMSON,width=.105,y=-2.15,outline=.23,misreg=True)
# little end crack
noodle('NOODLE_crack_tip',[(8.2,6.75),(9.4,6.55),(10.2,6.1)],SODIUM,width=.05,y=-2.25,outline=.12,misreg=True)

# ---------------- BROTH + STEAM + DROPLETS ----------------
# painted broth ribbons erupt from bowl
broth1=[(2.42,3.68),(2.05,4.25),(1.65,4.78),(1.83,5.23),(2.45,5.50),(3.0,5.3),(3.1,4.7),(2.78,4.2)]
broth2=[(2.7,3.7),(3.25,4.1),(3.78,4.35),(4.05,4.9),(3.85,5.45)]
noodle('BROTH_ribbon_A',broth1,GOLD,width=.145,y=-1.7,outline=.26,misreg=True)
noodle('BROTH_ribbon_B',broth2,AMBER,width=.11,y=-1.72,outline=.22,misreg=True)
# droplets along action
for i,(x,z,r,ma) in enumerate([(1.35,4.85,.11,GOLD),(1.1,5.4,.08,SODIUM),(3.2,5.95,.12,AMBER),(3.75,5.65,.07,GOLD),(4.35,5.15,.10,VERMILION),(0.55,4.2,.08,SODIUM),(4.75,4.7,.06,GOLD)]):
    uv('broth_drop_%02d'%i,(x,-1.92,z),(r,.065,r*1.15),INK,C_FX)
    uv('broth_drop_color_%02d'%i,(x,-2.0,z),(r*.62,.035,r*.75),ma,C_FX)
# steam bold calligraphic strokes
steam1=[(2.35,3.85),(1.95,4.35),(2.1,4.85),(2.7,5.05),(2.95,5.55),(2.55,6.05),(2.1,6.35)]
steam2=[(2.75,3.9),(3.25,4.45),(3.15,4.95),(2.75,5.35),(3.25,5.82),(3.9,6.0),(4.15,6.55)]
steam3=[(2.25,4.0),(1.6,4.6),(1.65,5.25),(1.25,5.75),(0.75,5.9)]
for i,(p,ma) in enumerate([(steam1,DIRTY),(steam2,CYAN),(steam3,CREAM)]):
    noodle('STEAM_calligraphic_%02d'%i,p,ma,width=.105,y=-1.48,outline=.19,misreg=True)
# steam glyph accents
text_obj('Steam_glyph','湯',(3.65,-1.80,6.55),0.52,DIRTY,C_FX,shadow=True)

# ---------------- GRAPHIC MOTION / IMPACT / HALFTONE ----------------
def star_pts(cx,cz,r1,r2,n=10,phase=0):
    return [ (cx+(r1 if i%2==0 else r2)*cos(phase+i*pi/n), cz+(r1 if i%2==0 else r2)*sin(phase+i*pi/n)) for i in range(2*n) ]
for i,(cx,cz,ma) in enumerate([(6.35,6.15,SODIUM),(5.2,5.0,ACID),(-4.3,7.6,CRIMSON),(4.8,8.9,CYAN)]):
    poly('Impact_%02d_ink'%i,star_pts(cx,cz,.85,.28,9,phase=0.1),-1.85,.10,INK,C_FX)
    poly('Impact_%02d'%i,star_pts(cx,cz,.68,.20,9,phase=0.1),-1.92,.05,ma,C_FX)
# circular motion lines around hero (thin graphic)
for j in range(4):
    pts=[]
    for i in range(13):
        t=0.35+i*1.85/12
        pts.append((-0.7+2.8*cos(t),-1.0-j*.03,6.2+2.1*sin(t)))
    curve_obj('motion_arc_%02d'%j,pts,0.025, [CYAN,AMBER,CRIMSON,ACID][j],C_FX,resolution=2)
# comic speed bars pointing right / open negative space
for i in range(8):
    z=8.0-i*0.42
    x0=-4.5+i*0.25; x1=4.7+i*0.45
    line_between('speed_line_%02d'%i,(x0,-0.15,z),(x1,-0.15,z+0.12*sin(i)),0.018,DIRTY if i%2 else CYAN,C_FX)
# offset background duplicate blocks: stepped animation registration
for i,(x,z,ma) in enumerate([(-3.0,8.9,VERMILION),(4.9,9.4,CYAN),(8.5,7.0,ACID)]):
    box('stepped_echo_%02d'%i,(x+0.17,2.35,z+0.10),(1.1,0.04,.34),ma,C_FX,rot=(0,0,.08))
# large screen-tone dot fields on two flat shadow zones
dots('halftone_shadow_left',(-2.6,3.25),2.8,1.55,0.34,0.052,INK,-0.72,C_FX,jitter=0.03)
dots('halftone_shadow_right',(6.3,3.2),2.6,1.3,0.31,0.045,BLACK,-0.72,C_FX,jitter=0.02)
# printed corner registration blocks
for x,z,ma in [(-10.8,10.9,CRIMSON),(10.8,10.8,CYAN),(-10.8,1.1,ACID),(10.8,1.0,AMBER)]:
    box('registration_block',(x,-2.2,z),(0.45,0.08,0.22),ma,C_FRAME)
    box('registration_block_offset',(x+0.10,-2.25,z+0.08),(0.45,0.06,0.22),CYAN if ma==CRIMSON else VERMILION,C_FRAME)
# SFX glyphs throughout
text_obj('SFX_SLURP','SLURP!',(-5.7,-1.1,8.0),0.45,SODIUM,C_FX,shadow=True)
text_obj('SFX_KRACK','KRAK!',(6.5,-1.95,7.2),0.45,ACID,C_FX,shadow=True)
text_obj('SFX_WHOOSH','ズズズ',(0.9,-1.95,10.15),0.38,CRIMSON,C_FX,shadow=True)
text_obj('SFX_HISS','SHH!',(4.8,-1.92,6.7),0.34,CYAN,C_FX,shadow=True)

# ---------------- PANEL BORDER ----------------
# thick black comic border placed in front, with selective color offset
box('Frame_left',(-11.65,-2.65,6.0),(0.42,0.25,12.7),INK,C_FRAME)
box('Frame_right',(11.65,-2.65,6.0),(0.42,0.25,12.7),INK,C_FRAME)
box('Frame_top',(0,-2.65,12.25),(23.7,0.25,0.42),INK,C_FRAME)
box('Frame_bottom',(0,-3.0,0.25),(23.7,0.25,0.42),INK,C_FRAME)
# colored misregistered slivers on border
box('Frame_left_red',(-11.42,-2.74,6.0),(0.06,0.05,12.3),VERMILION,C_FRAME)
box('Frame_right_cyan',(11.42,-2.74,6.0),(0.06,0.05,12.3),CYAN,C_FRAME)
# title tab
box('Title_tab',(-7.8,-2.8,11.75),(5.2,0.12,0.42),BLACK,C_FRAME)
text_obj('Title_text','THE NOODLE WAY',( -7.8,-2.9,11.75),0.24,ACID,C_FRAME,shadow=False)

# Camera
bpy.ops.object.camera_add(location=(10.6,-29.5,8.0))
cam=bpy.context.object; cam.name='Camera_Presentation_ComicPanel'
def point_camera(obj, target):
    direction=Vector(target)-obj.location
    obj.rotation_euler=direction.to_track_quat('-Z','Y').to_euler()
point_camera(cam,(-0.2,0.0,5.6))
cam.data.lens=54
cam.data.sensor_width=36
scene.camera=cam
link_obj(cam,C_FRAME)
# Lights: restrained, mostly for readable 3D orbit and grounding
# area lights with graphic colors, not bloom-dependent
for name,loc,energy,color,size in [
    ('Key_sodium',(-2,-10,10),900,(1.0,0.24,0.05),8.0),
    ('Rim_acid',(6,-4,10),700,(0.35,1.0,0.05),6.0),
    ('Fill_crimson',(-8,-2,6),500,(1.0,0.02,0.02),5.0)]:
    bpy.ops.object.light_add(type='AREA', location=loc)
    l=bpy.context.object; l.name=name; l.data.energy=energy; l.data.color=color; l.data.shape='DISK'; l.data.size=size; point_camera(l,(0,0,5)); link_obj(l,C_FX)
# add a low point for wheel/counter depth
bpy.ops.object.light_add(type='POINT',location=(2,-4,4)); l=bpy.context.object; l.name='Broth_point'; l.data.energy=80; l.data.color=(1.0,.28,.04); link_obj(l,C_CART)

# scene metadata
scene['title']='THE NOODLE WAY — Ramen Samurai / Multicultural Night Market'
scene['description']='Stylized NPR action panel: improvised ramen martial art, noodle shield, broth ribbons, steam glyphs, multicultural cyberpunk market.'
scene['palette']='acid green / chartreuse / amber / sodium orange / crimson / selective cyan / indigo / brown-black'
scene['graphic_language']='ink outlines, halftone dots, misregistered print layers, hard flats, motion glyphs'
# organize active object
bpy.context.view_layer.objects.active=cam
cam.select_set(True)
# save initial scene
scene.render.filepath='/tmp/ramen_samurai_preview.png'
bpy.ops.wm.save_as_mainfile(filepath='/tmp/ramen_samurai.blend')
# return counts
result={'objects':len(scene.objects),'materials':len(bpy.data.materials),'camera':cam.name,'blend':'/tmp/ramen_samurai.blend'}


Traceback (most recent call last):
  File "<repo>\.runner\blender-mcp-v1.0.0\addon\mcp_to_blender_server.py", line 230, in _execute_code
    exec(code, namespace)
    ~~~~^^^^^^^^^^^^^^^^^
  File "<string>", line 6, in <module>
  File "<repo>\.runner\blender-mcp-v1.0.0\addon\weak_sandbox.py", line 127, in _blocked
    raise RuntimeError(
    ...<3 lines>...
    )
RuntimeError: Operator 'bpy.ops.wm.read_factory_settings(use_empty=True)' is not allowed in LLM-generated code: Resets all user preferences and startup file, use bpy.ops.wm.read_homefile() or bpy.ops.wm.read_homefile(use_empty=True, use_factory_startup=True) instead