"""ARTISAN cartridge geometry, evaluated with build_mezzo.py's modelling helpers.

Centimetres. Upright modules dock at local (0, 0, 0), front toward -Y.
Connector and electronics geometry expresses design intent, not production CAD.
"""
PCB=material('Cartridges / circuit green','#224F40',.46)
CERAMIC=material('Cartridges / ceramic ivory','#D6D7C3',.33)
EPAPER=material('World / unlit e-paper ivory','#D9DECE',.74)
SMOKE=material('Cartridges / smoked inspection glass','#72867B',.16,.12)
SMOKE.node_tree.nodes.get('Principled BSDF').inputs['Alpha'].default_value=.18
SMOKE.diffuse_color=(*SMOKE.diffuse_color[:3],.18)

def face_text(g,name,body,x,z,size=.25,mat=WHITE,y=-1.25,bold=False,align='LEFT'):
    return parent(text(name,body,(x,y,z),size,mat,rotation=(math.pi/2,0,0),bold=bold,align=align),g)

def face_ring(g,name,x,z,outer,inner,y=-.18,depth=.15,mat=ALUMINUM):
    verts=[];n=48
    for yy,r in [(y-depth/2,outer),(y+depth/2,outer),(y+depth/2,inner),(y-depth/2,inner)]:
        verts.extend((x+r*math.cos(i*2*math.pi/n),yy,z+r*math.sin(i*2*math.pi/n)) for i in range(n))
    faces=[]
    for j in range(4):
        for i in range(n):faces.append((j*n+i,j*n+(i+1)%n,((j+1)%4)*n+(i+1)%n,((j+1)%4)*n+i))
    return parent(mesh(name,verts,faces,mat,.015),g)

def round_socket(g,name,x,z,radius=.27,y=-.18,label=None,label_z=None):
    face_ring(g,name+' / metal lip',x,z,radius,radius*.66,y,.18)
    parent(cyl(name+' / dark bore',(x,y+.025,z),radius*.66,.17,BLACK,48,rot=(math.pi/2,0,0),bevel=.006),g)
    face_ring(g,name+' / inner contact',x,z,radius*.54,radius*.44,y-.025,.03,GOLD)
    if label:face_text(g,name+' / silk',label,x,label_z if label_z is not None else z+radius+.24,.21,y=y-.03,align='CENTER')

def usb_c_socket(g,name,x,z,y=-.18,label=None,label_z=None):
    rim=cube(name+' / shell',(x,y,z),(.98,.18,.43),ALUMINUM,.18)
    cut(rim,cube(name+' / aperture',(x,y,z),(.81,.70,.285),None,.13))
    parent(rim,g)
    parent(cube(name+' / cavity',(x,y+.06,z),(.81,.12,.285),BLACK,.12),g)
    parent(cube(name+' / tongue',(x,y-.06,z),(.63,.12,.062),GRAPHITE,.027),g)
    for i in range(10):
        for sign in (-1,1):parent(cube(name+' / contact',(x-.27+i*.06,y-.11,z+sign*.038),(.022,.035,.008),GOLD,.002),g)
    if label:face_text(g,name+' / silk',label,x,label_z if label_z is not None else z+.49,.21,y=y-.03,align='CENTER')

def rect_socket(g,name,x,z,w,h,y=-.18,label=None,label_z=None):
    rim=cube(name+' / rim',(x,y,z),(w,.18,h),ALUMINUM,.07)
    cut(rim,cube(name+' / opening',(x,y,z),(w-.13,.6,h-.13),None,.04))
    parent(rim,g);parent(cube(name+' / interior',(x,y+.03,z),(w-.13,.11,h-.13),BLACK,.04),g)
    if label:face_text(g,name+' / silk',label,x,label_z if label_z is not None else z+h/2+.23,.21,y=y-.03,align='CENTER')

def dsub_socket(g,name,x,z,y=-.18):
    pts=[(x-.80,z+.32),(x+.80,z+.32),(x+.66,z-.32),(x-.66,z-.32)]
    # A real recessed trapezoidal nine-pin controller socket.
    body=mesh(name+' / shell',[(xx,y+dy,zz) for dy in (-.10,.06) for xx,zz in pts],[(0,3,2,1),(4,5,6,7),(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7)],ALUMINUM,.045)
    parent(body,g)
    parent(cube(name+' / dark insert',(x,y-.12,z),(1.25,.05,.42),BLACK,.08),g)
    for row,count in [(0,5),(1,4)]:
        for i in range(count):parent(cyl(name+' / pin',(x+(i-(count-1)/2)*.22,y-.15,z+(.105 if row==0 else -.105)),.034,.055,GOLD,12,rot=(math.pi/2,0,0),bevel=.005),g)
    for dx in (-.99,.99):parent(cyl(name+' / screw post',(x+dx,y-.06,z),.13,.17,STEEL,6,rot=(math.pi/2,0,0),bevel=.02),g)

def cartridge_shoe(g,power=False):
    identity=g.name.removeprefix('cartridge_')
    guard=group(identity+'_connector_guard');parent(guard,g)
    outer=roundrect(11.95,1.72,.14,8);inner=roundrect(11.67,1.44,.065,8);n=len(outer)
    rings=[[(x,y,z) for x,y in points] for points,z in ((outer,-1.50),(outer,.25),(inner,.25),(inner,-1.50))]
    faces=[(j*n+i,j*n+(i+1)%n,((j+1)%4)*n+(i+1)%n,((j+1)%4)*n+i) for j in range(4) for i in range(n)]
    parent(mesh('Module / protective connector sleeve',sum(rings,[]),faces,BLACK,.025),guard)
    parent(cube('Module / seating shoulder',(0,0,.25),(11.95,1.72,.22),BLACK,.10),guard)
    contacts=group(identity+'_connector_contacts');parent(contacts,g)
    board=cube('Module / keyed edge board',(0,0,-.39),(9.24,.16,1.05),PCB,.025)
    cut(board,cube('Module / key notch',(-3.30,0,-.78),(.18,.8,.50),None,.008))
    parent(board,contacts)
    for i in range(82):
        xx=-4.39+i*.106+(.18 if i>=11 else 0)
        for sign in (-1,1):parent(cube('Module / gold finger',(xx,sign*.093,-.47),(.065,.022,.58),GOLD,.009),contacts)
    for xx in (-5.40,5.40):
        parent(cube('Module / dedicated power blade',(xx,0,-.32),(.46,.22,.80),GOLD,.04),contacts)
    if power:face_text(g,'BATT / power interface','MANAGED POWER',0,.32,.17,y=-.91,align='CENTER')

def case_fasteners(g,w,y,z):
    for xx in (-w,w):
        parent(cyl('Module / captive fastener',(xx,y,z),.095,.035,STEEL,24,rot=(math.pi/2,0,0),bevel=.012),g)
        parent(cube('Module / fastener slot',(xx,y-.024,z),(.115,.009,.025),BLACK,.004),g)

def side_panel(g,name,side,w,d,top,mat=GRAPHITE):
    """An outboard control surface; local -Y faces away from the cartridge."""
    panel=group(name);parent(panel,g);panel.location.x=side*w/2;panel.rotation_euler.z=side*math.pi/2
    face=parent(cube(name+' / inset',(0,-.018,(top+.45)/2),(d-.18,.11,top-.75),mat,.12),panel)
    return panel,face

def square_button(g,name,u,z,mat=ORANGE,size=1.04,label=None):
    parent(cube(name+' / recessed surround',(u,-.095,z),(size+.19,.15,size+.19),BLACK,.17),g)
    button=parent(cube(name,(u,-.17,z),(size,.20,size),mat,.15),g)
    if label:face_text(g,name+' / silk',label,u,z-size/2-.28,.23,y=-.09,align='CENTER')
    return button

def family_front(g,title,code,role,feature,detail,w,d,top):
    """Shared two-column instrument face: one light, type, and vertical ribbing."""
    y=-d/2-.025
    parent(cube(title+' / front gasket',(0,y,(top+.38)/2),(w-.36,.095,top-.73),BLACK,.17),g)
    parent(cube(title+' / typography field',(-2.64,y-.06,(top+.50)/2),(6.18,.09,top-1.02),GRAPHITE,.15),g)
    for i in range(13):parent(cube(title+' / vertical rib',(.83+i*.315,y-.095,(top+.49)/2),(.135,.28,top-1.13),BASE,.045),g)
    sy=y-.113
    face_text(g,title+' / title',title,-5.24,top-1.54,.77,y=sy,bold=True)
    face_text(g,title+' / role',role,-5.22,top-2.30,.205,MOD,y=sy-.001)
    face_text(g,title+' / feature',feature,-5.22,2.72,.64,y=sy-.001)
    face_text(g,title+' / detail',detail,-5.22,1.81,.205,MOD,y=sy-.001)
    face_text(g,title+' / family','ARTISAN / '+code,-5.22,.89,.19,y=sy-.001)
    parent(cube('activity_led_'+title.lower(),(-5.14,sy-.025,top-.80),(.38,.05,.12),AMBER,.03),g)
    case_fasteners(g,w/2-.52,sy,.55)

def rear_identity(g,title,code,w,d,top):
    rear=group(title+' / rear');parent(rear,g);rear.location.y=d/2+.012;rear.rotation_euler.z=math.pi
    face_text(rear,title+' / rear identity',title+' / '+code,0,top-.90,.26,MOD,y=-.01,align='CENTER')
    for x in (-w/2+.55,w/2-.55):
        parent(cyl(title+' / rear captive fastener',(x,-.025,.75),.095,.035,STEEL,24,rot=(math.pi/2,0,0),bevel=.012),rear)
    return rear

def make_vector():
    g=group('cartridge_vector');cartridge_shoe(g)
    w,d,top=12.20,2.12,6.40
    parent(cube('Vector / body',(0,0,3.30),(w,d,6.20),BASE,.36),g)
    parent(cube('Vector / polished crown',(0,0,6.27),(12.33,2.23,.58),RED_CAP,.24),g)
    family_front(g,'VECTOR','AT-V01','GRAPHICS + LOCAL AI','24 GB','PCI EXPRESS / x16',w,d,top)
    right,_=side_panel(g,'vector_side_right',1,w,d,top)
    for i,z in enumerate((4.92,3.36,1.80)):usb_c_socket(right,'Vector / display '+str(i+1),0,z,y=-.10,label='DISPLAY 0'+str(i+1),label_z=z+.48)
    left,_=side_panel(g,'vector_side_left',-1,w,d,top)
    face_text(left,'Vector / thermal silk','THERMAL',0,5.50,.21,y=-.09,align='CENTER')
    for i in range(16):parent(cube('Vector / side cooling channel',(0,-.085,1.19+i*.235),(1.49,.035,.095),BLACK,.025),left)
    rear_identity(g,'VECTOR','AT-V01',w,d,top)
    return g


def combo_xlr(g,name,z,index):
    face_ring(g,name+' / flange',0,z,1.08,.83,-.13,.18)
    parent(cyl(name+' / insert',(0,-.145,z),.835,.17,BLACK,48,rot=(math.pi/2,0,0),bevel=.025),g)
    round_socket(g,name+' / TRS',0,z,.29,y=-.25)
    for a in (30,150,270):
        aa=math.radians(a);u=.51*math.cos(aa);zz=z+.51*math.sin(aa)
        face_ring(g,name+' / pin collar',u,zz,.09,.058,-.244,.024,STEEL)
        parent(cyl(name+' / pin cavity',(u,-.238,zz),.058,.018,BLACK,20,rot=(math.pi/2,0,0),bevel=.002),g)
    parent(cube(name+' / latch',(0,-.22,z+.99),(.37,.13,.18),ALUMINUM,.04),g)
    face_text(g,name+' / silk',f'MIC / LINE {index}',0,z+1.27,.195,y=-.105,align='CENTER')
    for i in range(7):parent(cube(name+' / signal indicator',(1.26,-.089,z-.48+i*.16),(.065,.025,.09),AMBER_DIM,.018),g)

def midi_socket(g,name,u,z,label):
    face_ring(g,name+' / lip',u,z,.62,.51,-.12,.15)
    parent(cyl(name+' / insert',(u,-.126,z),.51,.15,BLACK,40,rot=(math.pi/2,0,0),bevel=.015),g)
    for j in range(5):
        a=math.radians(25+j*32.5);xx=u+.34*math.cos(a);zz=z+.34*math.sin(a)
        face_ring(g,name+' / pin socket',xx,zz,.046,.026,-.21,.02,GOLD)
    face_text(g,name+' / silk',label,u,z+.79,.19,y=-.095,align='CENTER')

def make_sound():
    g=group('cartridge_sound');cartridge_shoe(g)
    w,d,top=12.65,3.05,7.93
    body=parent(cube('Sound / ivory housing',(0,0,4.12),(w,d,7.62),CREAM,.32),g)
    parent(cube('Sound / vermilion crown',(0,0,7.75),(12.75,3.14,.56),RED_CAP,.24),g)
    family_front(g,'SOUND','AT-S01','PROFESSIONAL AUDIO','STUDIO','BALANCED / MIDI / OPTICAL',w,d,top)
    left,_=side_panel(g,'sound_side_left',-1,w,d,top)
    combo_xlr(left,'Sound / combo 1',5.89,1);combo_xlr(left,'Sound / combo 2',3.36,2)
    midi_socket(left,'Sound / MIDI in',-.52,1.26,'MIDI IN')
    round_socket(left,'Sound / headphones',.88,1.26,.34,y=-.12,label='PHONES',label_z=2.05)
    right,panel=side_panel(g,'sound_side_right',1,w,d,top)
    # The wheel sits inside the case; only its knurled circumference is exposed.
    bpy.context.view_layer.update()
    for target in (body,panel):
        tool=cube('Sound / thumbwheel opening',right.matrix_world@Vector((0,.21,6.43)),(1.49,.95,1.30),None,.15,rot=(0,0,math.pi/2))
        cut(target,tool)
    parent(cube('Sound / thumbwheel dark pocket',(0,.51,6.43),(1.50,.16,1.32),BLACK,.13),right)
    wheel=group('sound_monitor_wheel');parent(wheel,right);wheel.location=(0,.31,6.43)
    parent(cyl('Sound / recessed wheel',(0,0,0),.65,1.12,ALUMINUM,64,rot=(0,math.pi/2,0),bevel=.05),wheel)
    for i in range(40):
        a=i*math.pi*2/40
        parent(cube('Sound / wheel knurl',(0,.65*math.sin(a),.65*math.cos(a)),(1.14,.05,.045),STEEL,.011,rot=(a,0,0)),wheel)
    face_text(right,'Sound / wheel caption','MONITOR',0,7.32,.23,y=-.10,align='CENTER')
    face_text(right,'Sound / wheel minimum','−',-1.06,6.43,.29,y=-.10,align='CENTER')
    face_text(right,'Sound / wheel maximum','+',1.06,6.43,.26,y=-.10,align='CENTER')
    for u,label in ((-.70,'ADAT IN'),(.70,'ADAT OUT')):rect_socket(right,'Sound / '+label,u,4.73,.86,.80,y=-.11,label=label,label_z=5.32)
    midi_socket(right,'Sound / MIDI out',0,3.17,'MIDI OUT')
    for i,u in enumerate((-.70,.70)):round_socket(right,'Sound / balanced out '+str(i+1),u,1.27,.40,y=-.11,label='OUT '+str(i+1),label_z=1.94)
    rear_identity(g,'SOUND','AT-S01',w,d,top)
    return g

def make_origin():
    g=group('cartridge_origin');cartridge_shoe(g)
    w,d,top=12.35,3.05,7.155
    body=cube('Origin / ivory housing',(0,0,3.72),(w,d,6.87),CREAM,.32)
    cut(body,cube('Origin / rear inspection aperture',(0,d/2-.06,3.65),(7.20,.95,3.54),None,.18))
    parent(body,g)
    parent(cube('Origin / orange crown',(0,0,7.0),(12.47,d+.10,.55),ORANGE,.23),g)
    family_front(g,'ORIGIN','AT-O01','MSX HARDWARE ENGINE','4 ERAS','MSX / MSX2 / 2+ / Turbo R',w,d,top)
    right,panel=side_panel(g,'origin_side_right',1,w,d,top)
    square_button(right,'origin_mode_button',0,5.94,ORANGE,1.04,'MODE')
    for i,label in enumerate(('MSX','MSX2','MSX2+','Turbo R')):
        z=4.58-i*.54
        parent(cube(f'origin_led_{i}',(-.91,-.098,z),(.25,.04,.12),AMBER if i==0 else AMBER_DIM,.027),right)
        face_text(right,'Origin / engine silk',label,-.48,z,.25,y=-.096,bold=True)
    bpy.context.view_layer.update()
    for target in (body,panel):
        cut(target,cube('Origin / side SD aperture',right.matrix_world@Vector((0,.08,1.29)),(2.66,.75,.25),None,.07,rot=(0,0,math.pi/2)))
    parent(cube('Origin / SD throat',(0,-.08,1.29),(2.66,.15,.25),BLACK,.045),right)
    parent(cube('Origin / SD edge',(0,-.18,1.29),(2.39,.25,.10),GRAPHITE,.025),right)
    face_text(right,'Origin / SD silk','SD / ROM + DISK',0,1.83,.21,y=-.095,align='CENTER')
    left,_=side_panel(g,'origin_side_left',-1,w,d,top)
    for i,z in enumerate((5.62,3.95)):
        dsub_socket(left,'Origin / controller '+str(i+1),0,z,y=-.11)
        face_text(left,'Origin / controller silk','CONTROL '+str(i+1),0,z+.60,.23,y=-.10,align='CENTER')
    round_socket(left,'Origin / stereo',0,2.14,.28,y=-.11,label='AUDIO',label_z=2.72)
    face_text(left,'Origin / chips','PSG / FM / SCC',0,.98,.20,y=-.10,align='CENTER')
    rear=rear_identity(g,'ORIGIN','AT-O01',w,d,top)
    parent(cube('Origin / rear PCB',(0,.42,3.65),(7.22,.065,3.55),PCB,.14),rear)
    parent(cube('Origin / FPGA package',(0,.23,3.76),(2.42,.30,1.75),CERAMIC,.09),rear)
    face_text(rear,'Origin / FPGA mark','MSX ENGINE',0,3.96,.31,INK,y=.069,align='CENTER',bold=True)
    face_text(rear,'Origin / FPGA detail','FPGA / COMPLETE SYSTEM',0,3.48,.135,INK,y=.067,align='CENTER')
    for u in (-2.59,2.59):
        for z in (2.76,4.61):
            parent(cube('Origin / memory package',(u,.31,z),(.90,.17,.71),BLACK,.04),rear)
            for j in range(6):parent(cube('Origin / memory pins',(u-.38+j*.15,.30,z+.42),(.06,.08,.16),GOLD,.004),rear)
    for j in range(16):parent(cube('Origin / PCB trace',(-3.16+j*.42,.38,3.02),(.018,.009,1.7),GOLD,.002),rear)
    parent(cube('Origin / inspection glass',(0,-.012,3.65),(7.22,.027,3.54),SMOKE,.14),rear)
    face_text(rear,'Origin / rear caption','CPU / VDP / PSG / FM / SCC',0,1.31,.23,MOD,y=-.01,align='CENTER')
    return g

def make_batt():
    g=group('cartridge_batt');cartridge_shoe(g,True)
    w,d,top=12.40,3.12,6.435
    parent(cube('Batt / red body',(0,0,3.36),(w,d,6.15),RED,.36),g)
    parent(cube('Batt / dark crown',(0,0,6.19),(12.47,3.19,.55),GRAPHITE,.26),g)
    family_front(g,'BATT','AT-B01','PORTABLE POWER','80 Wh','CAPACITY TARGET',w,d,top)
    right,_=side_panel(g,'batt_side_right',1,w,d,top)
    square_button(right,'batt_check_button',0,4.89,MOD,1.00,'CHECK')
    for i in range(5):
        z=1.32+i*.47
        parent(cube('Batt / gauge recess',(-.49,-.079,z),(1.12,.04,.29),BLACK,.06),right)
        parent(cube(f'batt_led_{i}',(-.49,-.113,z),(.90,.035,.14),AMBER_DIM,.032),right)
        face_text(right,'Batt / gauge mark',str((i+1)*20),.48,z,.20,MOD,y=-.096)
    face_text(right,'Batt / gauge caption','CHARGE / %',0,.76,.20,MOD,y=-.096,align='CENTER')
    left,_=side_panel(g,'batt_side_left',-1,w,d,top)
    usb_c_socket(left,'Batt / charge',0,4.41,y=-.11,label='USB-C / CHARGE',label_z=5.0)
    face_text(left,'Batt / interface silk','MANAGED POWER',0,3.49,.21,y=-.10,align='CENTER')
    for i in range(8):parent(cube('Batt / side grip rib',(0,-.12,1.10+i*.22),(2.10,.06,.095),BASE,.025),left)
    rear_identity(g,'BATT','AT-B01',w,d,top)
    return g

def make_world():
    g=group('cartridge_world')
    parent(cube('World / magnesium carrier',(0,0,0),(7.58,5.37,.82),BASE,.18),g)
    parent(cube('World / polished grip',(3.06,0,0),(1.50,5.43,.98),RED_CAP,.21),g)
    parent(cube('World / face gasket',(-.84,0,.429),(5.88,4.87,.04),BLACK,.13),g)
    parent(cube('World / typography field',(-2.10,0,.458),(3.17,4.68,.035),GRAPHITE,.12),g)
    for i in range(12):parent(cube('World / vertical rib',(-.17+i*.184,0,.479),(.083,4.38,.088),BASE,.025),g)
    for name,body,loc,size,mat,bold in [('title','WORLD',(-3.43,.94,.485),.51,WHITE,True),('role','PROJECT STORAGE',(-3.41,.23,.486),.165,MOD,False),('media','NVMe',(-3.41,-.42,.486),.36,WHITE,False),('family','ARTISAN / AT-W01',(-3.41,-2.04,.486),.15,WHITE,False)]:
        parent(text('World / '+name,body,loc,size,mat,bold=bold),g)
    parent(cube('World / writable label',(-2.09,-1.28,.484),(2.68,.55,.022),CREAM,.06),g)
    parent(text('World / project number','PROJECT 01',(-3.27,-1.28,.502),.20,INK),g)
    # An unlit electronic label sits on the exposed grip, readable while docked.
    display=group('world_epaper_label');parent(display,g);display.location=(3.06,0,.508);display.rotation_euler.z=math.pi/2
    parent(cube('World / e-paper gasket',(0,0,0),(4.46,1.15,.045),BLACK,.10),display)
    parent(cube('World / e-paper surface',(0,0,.029),(4.25,.94,.012),EPAPER,.065),display)
    parent(text('World / e-paper project','STUDIO / 01',(-1.91,.175,.039),.24,INK,bold=True),display)
    parent(text('World / e-paper medium','NVMe',(1.91,.19,.039),.135,INK,align='RIGHT'),display)
    parent(text('World / e-paper contents','PROJECTS + TOOLS',(-1.91,-.205,.039),.13,INK),display)
    parent(text('World / e-paper maker','WORLD',(1.91,-.205,.039),.13,INK,align='RIGHT'),display)
    parent(cube('World / e-paper rule',(0,-.015,.038),(3.85,.012,.002),INK,0),display)
    parent(cube('activity_led_world',(3.06,2.46,.511),(.30,.11,.025),AMBER,.025),g)
    for i in range(7):parent(cube('World / grip flute',(3.82,-1.63+i*.55,.02),(.045,.24,.57),RED_DARK,.025),g)
    parent(cube('World / connector board',(-3.94,0,0),(.56,2.12,.13),PCB,.03),g)
    for i in range(22):parent(cube('World / gold contact',(-4.12,-.95+i*.09,.085),(.30,.052,.014),GOLD,.004),g)
    return g

CARTRIDGE_BUILDERS={'vector':make_vector,'sound':make_sound,'origin':make_origin,'batt':make_batt,'world':make_world}
CONTROL_PREFIXES=('origin_led_','batt_led_','activity_led_')
CONTROL_NAMES={'origin_mode_button','batt_check_button'}

def finalize_cartridge(g):
    for ob in list(g.children_recursive):
        if ob.type=='FONT':select(ob);bpy.ops.object.convert(target='MESH')
        if ob.type=='MESH' and ob.modifiers:apply(ob)
    # Keep controls, LEDs and their axes addressable; batch all other surfaces
    # within each assembly by material without losing rear-panel transforms.
    for assembly in [g]+[o for o in g.children_recursive if o.type=='EMPTY']:
        buckets={}
        for ob in list(assembly.children):
            if ob.type!='MESH' or ob.name in CONTROL_NAMES or ob.name.startswith(CONTROL_PREFIXES):continue
            buckets.setdefault(ob.data.materials[0].name,[]).append(ob)
        for mat,obs in buckets.items():
            if len(obs)<2:continue
            bpy.ops.object.select_all(action='DESELECT')
            for ob in obs:ob.select_set(True)
            bpy.context.view_layer.objects.active=obs[0];bpy.ops.object.join()
            bpy.context.object.name=assembly.name+' / '+mat
    return g

exec(compile((ROOT/'modeling/asx_geometry.py').read_text(),str(ROOT/'modeling/asx_geometry.py'),'exec'))
