# Blender Arch

> Skill avanzata per modellazione 3D in Blender — architettura, oggetti, prodotti, mobili, veicoli. Tecnica professionale: bevel, SubSurf, Boolean, bmesh, Array, Curve, Solidify, smooth shading. Visual loop con MCP connector (porta 9876): esegui → render → analizza → itera.

- Skill: `max-786/blender-arch` (Agent Skill)
- Install (CLI): `npx skillmds@latest add max-786/blender-arch`
- Raw SKILL.md: https://api.skillmd.com/api/skills/max-786/blender-arch/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: MAX-786 (https://skillmd.com/u/max-786)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/max-786/blender-arch

---


# Skill: Blender 3D Modeling (Advanced)

Sei un esperto di modellazione 3D in Blender con Python (`bpy` + `bmesh`).
Ricevi una richiesta (`$ARGUMENTS`) e produci geometria di qualità professionale.

---

## Connessione — MCP

### ✅ Metodo 1 — MCP Tool (PREFERITO, porta 9876)
Usa direttamente il tool `mcp__Blender__execute_blender_code`:
```python
# Esegui codice in Blender — assegna sempre result={...} per ricevere dati
mcp__Blender__execute_blender_code(code="""
import bpy
# ... il tuo codice ...
result = {"ok": True, "verts": len(me.vertices)}
""")

# Screenshot viewport (senza render)
mcp__Blender__get_screenshot_of_window_as_image()

# Render su file e visualizza
mcp__Blender__render_viewport_to_path(output_path="<JOB_DIR>/out.png")

# Lista oggetti in scena
mcp__Blender__get_objects_summary()
```

---

## Visual Loop — MCP (esegui → screenshot → analizza → itera)

```
FLUSSO PREFERITO con MCP:
1. mcp__Blender__execute_blender_code(code=build_code)
2. mcp__Blender__render_viewport_to_path(output_path="...preview.png")
   oppure mcp__Blender__get_screenshot_of_window_as_image()  ← più veloce, no render
3. Read("...preview.png")  → analisi visiva
4. mcp__Blender__execute_blender_code(code=fix_code)  → itera

RENDER COMPLETO (EEVEE) — da usare per risultato finale:
```
```python
# Via MCP — esegui questo codice poi leggi il file con Read
render_code = """
import bpy
sc = bpy.context.scene
try:    sc.render.engine = "BLENDER_EEVEE_NEXT"
except: sc.render.engine = "BLENDER_EEVEE"
sc.render.resolution_x = 1280
sc.render.resolution_y = 720
sc.render.filepath = "<JOB_DIR>/render_final.png"
sc.render.use_compositing = False
sc.view_settings.view_transform = "Filmic"
sc.view_settings.look = "Medium High Contrast"
bpy.ops.render.render(write_still=True)
result = {"saved": sc.render.filepath}
"""
# mcp__Blender__execute_blender_code(code=render_code)
# poi: Read("<JOB_DIR>/render_final.png")
```

---

## MODELLAZIONE — Funzioni Base

### Helper universali
```python
import bpy, math, bmesh
from mathutils import Vector, Matrix

def new_obj(name, mesh):
    """Crea e linka oggetto con mesh."""
    obj = bpy.data.objects.new(name, mesh)
    bpy.context.collection.objects.link(obj)
    bpy.context.view_layer.objects.active = obj
    obj.select_set(True)
    return obj

def box(name, x, y, z, sx, sy, sz, mat=None):
    """Box con transform apply. Dimensioni reali (non half)."""
    bpy.ops.mesh.primitive_cube_add(size=1, location=(x, y, z))
    o = bpy.context.active_object
    o.name = name; o.scale = (sx, sy, sz)
    bpy.ops.object.transform_apply(scale=True)
    if mat: assign_mat(o, mat)
    return o

def cyl(name, x, y, z, r, h, verts=32, cap_fill='NGON', mat=None):
    """Cilindro: r=raggio, h=altezza, centrato in z."""
    bpy.ops.mesh.primitive_cylinder_add(
        radius=r, depth=h, vertices=verts,
        cap_fill_type=cap_fill, location=(x, y, z))
    o = bpy.context.active_object; o.name = name
    if mat: assign_mat(o, mat)
    return o

def sphere(name, x, y, z, r, subdiv=3, mat=None):
    """Icosfera: più uniforme della UV sphere."""
    bpy.ops.mesh.primitive_ico_sphere_add(radius=r, subdivisions=subdiv,
                                           location=(x, y, z))
    o = bpy.context.active_object; o.name = name
    smooth_shade(o)
    if mat: assign_mat(o, mat)
    return o

def plane(name, x, y, z, sx, sy, mat=None):
    bpy.ops.mesh.primitive_plane_add(size=1, location=(x, y, z))
    o = bpy.context.active_object; o.name = name
    o.scale = (sx, sy, 1)
    bpy.ops.object.transform_apply(scale=True)
    if mat: assign_mat(o, mat)
    return o

def assign_mat(obj, mat):
    if obj.data.materials: obj.data.materials[0] = mat
    else: obj.data.materials.append(mat)
```

---

## MODELLAZIONE — Tecniche Avanzate

### Smooth shading + Auto Smooth

> ⚠️ **BUG CRITICO — `bpy.ops.object.shade_smooth()` NON funziona su mesh bmesh**
>
> L'operatore `bpy.ops.object.shade_smooth()` **non rimuove l'attributo `sharp_face`**
> su mesh create con bmesh. Risultato: tutte le facce rimangono piatte (`normals_domain=FACE`)
> e si vedono striature verticali pronunciate su cilindri, coni e oggetti lathe.
>
> **Diagnosi:**
> ```python
> ob.data.normals_domain  # → 'FACE' (sbagliato), deve essere 'POINT'
> 'sharp_face' in ob.data.attributes  # → True dopo ops.shade_smooth() → piatto!
> ```
>
> **Fix: usa il metodo diretto sul mesh, non l'operatore:**
> ```python
> # SBAGLIATO (non funziona su bmesh):
> bpy.ops.object.shade_smooth()   # → sharp_face rimane True, striature!
>
> # CORRETTO — metodo diretto sul mesh (Blender 4.x+):
> ob.data.shade_smooth()          # → rimuove sharp_face, normals_domain='POINT' ✓
>
> # CORRETTO con soglia angolo (marca edge acuti come sharp):
> ob.data.shade_smooth()                          # prima: abilita smooth su tutto
> ob.data.set_sharp_from_angle(angle=math.radians(30))  # poi: marca edge > 30° come sharp
> ```

```python
def smooth_shade(obj, angle_deg=30):
    """
    Smooth shading con soglia angolo. ESSENZIALE per oggetti organici e curvi.
    Senza questo: facce piatte visibili su cilindri e sfere.

    NOTA: usa ob.data.shade_smooth() (metodo mesh), NON bpy.ops.object.shade_smooth()
    che non funziona correttamente su mesh create con bmesh.
    """
    # CORRETTO: metodo diretto sul mesh data-block
    obj.data.shade_smooth()
    # Marca edge acuti come sharp (angolo > soglia)
    try:
        obj.data.set_sharp_from_angle(angle=math.radians(angle_deg))
    except AttributeError:
        # Blender < 4.1: fallback via modifier
        try:
            mod = obj.modifiers.new("SmoothAngle", "SMOOTH_BY_ANGLE")
            mod.angle = math.radians(angle_deg)
        except:
            pass
    obj.data.update()
```

### Bevel modifier — spigoli realistici
```python
def add_bevel(obj, amount=0.02, segments=2, limit='ANGLE', angle_deg=30):
    """
    REGOLA D'ORO: ogni oggetto reale ha spigoli smussati.
    amount  = 0.005–0.02  per oggetti piccoli (telefono, sedia)
    amount  = 0.05–0.15   per architettura (davanzali, cornici)
    segments = 2  → smussatura morbida con riflessi netti (product design)
    segments = 3+ → ultra morbido (auto, gadget)
    """
    mod = obj.modifiers.new("Bevel", "BEVEL")
    mod.width        = amount
    mod.segments     = segments
    mod.limit_method = limit
    if limit == 'ANGLE':
        mod.angle_limit = math.radians(angle_deg)
    mod.profile = 0.5   # profilo circolare
    return mod

# Esempio uso: tavolo con spigoli realistici
# t = box("Table", 0, 0, 0.75, 1.6, 0.8, 0.05)
# add_bevel(t, amount=0.008, segments=2)
# smooth_shade(t, 60)
```

### Subdivision Surface — forme organiche
```python
def add_subsurf(obj, levels=2, render_levels=3, simple=False):
    """
    Leviga la mesh per forme organiche.
    levels=1: leggera levigatura (mobili morbidi)
    levels=2: media (cuscini, corpi)
    levels=3: alta (personaggi, auto)
    ATTENZIONE: applicare DOPO il bevel. Mai su mesh con N-gon complessi.
    """
    mod = obj.modifiers.new("Subdivision", "SUBSURF")
    mod.levels          = levels
    mod.render_levels   = render_levels
    mod.subdivision_type = 'SIMPLE' if simple else 'CATMULL_CLARK'
    smooth_shade(obj)
    return mod
```

### Solidify — spessore a superfici piatte
```python
def add_solidify(obj, thickness=0.05, offset=-1.0):
    """
    Aggiunge spessore a mesh piatte: vetrate, pareti sottili, pannelli.
    offset=-1.0 → spessore verso l'interno
    offset= 0.0 → simmetrico
    offset=+1.0 → verso l'esterno
    """
    mod = obj.modifiers.new("Solidify", "SOLIDIFY")
    mod.thickness         = thickness
    mod.offset            = offset
    mod.use_even_offset   = True
    return mod
```

### Array modifier — elementi ripetuti
```python
def add_array(obj, count=5, offset_x=0, offset_y=0, offset_z=0,
              relative=True):
    """
    Moltiplica oggetto lungo un asse.
    relative=True: offset come multiplo della dimensione oggetto
    relative=False: offset assoluto in metri

    Usi tipici:
    - Ringhiera: count=20, offset_x=1.0 (relativo)
    - Finestre ripetute: count=5, offset_x=3.0 (assoluto)
    - Listelli solaio: count=15, offset_y=1.0 (relativo)
    """
    mod = obj.modifiers.new("Array", "ARRAY")
    mod.count = count
    if relative:
        mod.use_relative_offset = True
        mod.relative_offset_displace = (offset_x, offset_y, offset_z)
    else:
        mod.use_relative_offset  = False
        mod.use_constant_offset  = True
        mod.constant_offset_displace = (offset_x, offset_y, offset_z)
    return mod

# Esempio: ringhiera balcone
# palo = cyl("Palo", 0, 0, 0.5, 0.02, 1.0, verts=8)
# add_array(palo, count=20, offset_x=1.0)   # 20 pali spaziati di 1 dim
# barra = box("Barra", 0, 0, 1.0, 0.02, 0.04, 0.04)
# add_array(barra, count=1, relative=False, offset_x=19*spazio)
```

### Mirror modifier
```python
def add_mirror(obj, axis_x=True, axis_y=False, axis_z=False,
               merge=True, threshold=0.001):
    """Specchia oggetto. Modella solo metà → risparmio tempo."""
    mod = obj.modifiers.new("Mirror", "MIRROR")
    mod.use_axis         = (axis_x, axis_y, axis_z)
    mod.use_merge_center = merge
    mod.merge_threshold  = threshold
    return mod
```

### Boolean — tagli e unioni
```python
def boolean_cut(target, cutter, apply=True):
    """
    Taglia target con forma del cutter (finestre in muri, buchi, nicchie).
    IMPORTANTE: entrambi devono avere manifold mesh (no fori, no face invertite).
    """
    mod = target.modifiers.new("Boolean", "BOOLEAN")
    mod.operation = 'DIFFERENCE'
    mod.object    = cutter
    mod.solver    = 'FAST'   # EXACT più preciso ma più lento
    if apply:
        bpy.context.view_layer.objects.active = target
        bpy.ops.object.modifier_apply(modifier="Boolean")
        bpy.data.objects.remove(cutter, do_unlink=True)
    return mod

# Esempio: finestra nel muro
# muro  = box("Muro", 0, 0, 1.5, 6, 0.3, 3.0)
# taglio = box("Cut_Finestra", 1.0, 0, 1.5, 1.4, 1.0, 1.2)
# boolean_cut(muro, taglio)
# vetro = box("Vetro", 1.0, 0.02, 1.5, 1.4, 0.04, 1.2, mat_glass)
```

### bmesh — geometria personalizzata
```python
def make_mesh_from_data(name, verts, faces, edges=[], smooth=False, mat=None):
    """
    Crea mesh da liste verts/faces. Massimo controllo sulla topologia.
    verts: [(x,y,z), ...]
    faces: [(i,j,k,...), ...] — CCW per normali verso l'esterno
    """
    mesh = bpy.data.meshes.new(name + "_mesh")
    obj  = bpy.data.objects.new(name, mesh)
    bpy.context.collection.objects.link(obj)
    mesh.from_pydata(verts, edges, faces)
    mesh.update()
    if smooth: smooth_shade(obj)
    if mat: assign_mat(obj, mat)
    # Normalizza normali
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.mesh.normals_make_consistent(inside=False)
    bpy.ops.object.mode_set(mode='OBJECT')
    return obj

def bmesh_extrude(name, profile_verts_2d, depth, axis='Y', mat=None):
    """
    Estrude un profilo 2D lungo un asse.
    Utile per: cornici, modanature, profili architettonici.
    profile_verts_2d: [(x, z), ...] nel piano XZ
    """
    bm = bmesh.new()
    v0_list = [bm.verts.new((x, 0, z)) for x, z in profile_verts_2d]
    v1_list = [bm.verts.new((x, depth, z)) for x, z in profile_verts_2d]
    bm.verts.ensure_lookup_table()
    n = len(profile_verts_2d)
    # Facce laterali
    for i in range(n - 1):
        bm.faces.new([v0_list[i], v0_list[i+1], v1_list[i+1], v1_list[i]])
    # Cap start e end
    bm.faces.new(list(reversed(v0_list)))
    bm.faces.new(v1_list)
    bm.normal_update()
    mesh = bpy.data.meshes.new(name + "_mesh")
    bm.to_mesh(mesh); bm.free()
    obj = bpy.data.objects.new(name, mesh)
    bpy.context.collection.objects.link(obj)
    if mat: assign_mat(obj, mat)
    return obj
```

### Curve — tubi, cavi, ringhiere su percorso
```python
def pipe_along_points(name, points, radius=0.02, resolution=12, mat=None):
    """
    Crea un tubo/cavo che segue una serie di punti 3D.
    Utile per: tubature, cavi elettrici, ringhiere curve, scale.
    points: [(x,y,z), ...]
    """
    curve_data = bpy.data.curves.new(name + "_curve", type='CURVE')
    curve_data.dimensions = '3D'
    curve_data.fill_mode  = 'FULL'
    curve_data.bevel_depth    = radius
    curve_data.bevel_resolution = resolution
    spline = curve_data.splines.new('NURBS')
    spline.points.add(len(points) - 1)
    for i, (x, y, z) in enumerate(points):
        spline.points[i].co = (x, y, z, 1)
    spline.use_endpoint_u = True
    obj = bpy.data.objects.new(name, curve_data)
    bpy.context.collection.objects.link(obj)
    if mat: assign_mat(obj, mat)
    return obj
```

---

## POSIZIONAMENTO PRECISO — Attach Point System

> **Teoria:** ogni oggetto Blender ha una **base ortonormale** propria incorporata nella
> `matrix_world` (4×4). Per far toccare due oggetti con precisione si calcolano i punti
> di contatto nel frame locale di ciascun oggetto e si trasformano nel frame world comune.
>
> ```
> p_world = obj.matrix_world @ p_local        # locale → world
> p_local = obj.matrix_world.inverted() @ p_world  # world → locale
> ```
> Precisione verificata: errore residuo < 0.15 μm anche con oggetti ruotati.

```python
from mathutils import Vector
import bpy

# ── Conversioni frame ─────────────────────────────────────────────────

def local_to_world(obj, p_local):
    """Trasforma un punto dal frame locale dell'oggetto al world frame."""
    bpy.context.view_layer.update()
    return obj.matrix_world @ Vector(p_local)

def world_to_local(obj, p_world):
    """Trasforma un punto dal world frame al frame locale dell'oggetto."""
    bpy.context.view_layer.update()
    return obj.matrix_world.inverted() @ Vector(p_world)

def world_bounds(obj):
    """
    Restituisce il bounding box world-space dell'oggetto.
    Returns: dict con 'min', 'max', 'center', 'size' (tutti Vector)
    """
    bpy.context.view_layer.update()
    verts = [obj.matrix_world @ v.co for v in obj.data.vertices]
    mn = Vector((min(v.x for v in verts), min(v.y for v in verts), min(v.z for v in verts)))
    mx = Vector((max(v.x for v in verts), max(v.y for v in verts), max(v.z for v in verts)))
    return {'min': mn, 'max': mx, 'center': (mn + mx) / 2, 'size': mx - mn}

# ── Attach point — posizionamento preciso ─────────────────────────────

def attach_to(obj_b, pt_b_local, obj_a, pt_a_local,
              align=False, gap=0.0, gap_axis=None):
    """
    Posiziona obj_b in modo che il suo punto locale `pt_b_local`
    coincida esattamente con il punto locale `pt_a_local` di obj_a.

    Args:
        obj_b      : oggetto da spostare
        pt_b_local : punto di attacco su obj_b in coordinate LOCALI  (tuple o Vector)
        obj_a      : oggetto di riferimento (fisso)
        pt_a_local : punto di attacco su obj_a in coordinate LOCALI  (tuple o Vector)
        align      : se True, copia anche la rotazione di obj_a su obj_b
        gap        : offset aggiuntivo dopo il contatto (in metri)
        gap_axis   : direzione world del gap (Vector); se None usa la direzione delta

    Returns:
        float: distanza residua tra i punti di attacco (< 1e-7 m = precisione macchina)

    Esempi:
        # Piattino: il suo top (z_local = saucer_h) tocca il bottom della tazza (z_local = 0)
        attach_to(saucer, (0, 0, 0.016), cup, (0, 0, 0))

        # Manico: il suo start (z_local = ATZ_TOP) tocca la parete destra della tazza
        attach_to(handle, (0.031, 0, 0.048), cup, (0.031, 0, 0.048))

        # Coperchio 2mm sopra il bordo
        attach_to(lid, (0, 0, -0.01), mug, (0, 0, 0.09), gap=0.002, gap_axis=(0,0,1))
    """
    bpy.context.view_layer.update()

    # 1. Punto target nel world frame
    p_target = obj_a.matrix_world @ Vector(pt_a_local)

    # 2. (Opzionale) allinea rotazione di B a quella di A
    if align:
        obj_b.rotation_mode = 'QUATERNION'
        obj_b.rotation_quaternion = obj_a.matrix_world.to_quaternion()
        bpy.context.view_layer.update()

    # 3. Posizione attuale del punto di attacco di B nel world
    p_now = obj_b.matrix_world @ Vector(pt_b_local)

    # 4. Delta world-space da applicare
    delta_world = p_target - p_now

    # 5. Applica gap
    if gap != 0.0:
        if gap_axis is not None:
            delta_world += Vector(gap_axis).normalized() * gap
        elif delta_world.length > 1e-10:
            delta_world += delta_world.normalized() * gap

    # 6. Converti in parent space se necessario, poi aggiorna location
    if obj_b.parent:
        delta = obj_b.parent.matrix_world.inverted().to_3x3() @ delta_world
    else:
        delta = delta_world
    obj_b.location = obj_b.location + delta
    bpy.context.view_layer.update()

    # 7. Calcola residuo
    p_a_f = obj_a.matrix_world @ Vector(pt_a_local)
    p_b_f = obj_b.matrix_world @ Vector(pt_b_local)
    return (p_b_f - p_a_f).length


def attach_bounds(obj_b, face_b, obj_a, face_a, gap=0.0):
    """
    Posiziona obj_b in modo che la faccia `face_b` del suo bounding box
    tocchi la faccia `face_a` del bounding box di obj_a nel world space.

    face: 'top' | 'bottom' | 'front' | 'back' | 'right' | 'left'

    Esempi:
        attach_bounds(saucer, 'top', cup, 'bottom')
        # → top del piattino tocca il bottom della tazza

        attach_bounds(lid, 'bottom', mug, 'top', gap=0.002)
        # → coperchio 2mm sopra il bordo
    """
    AXIS   = {'top': 2, 'bottom': 2, 'front': 1, 'back': 1, 'right': 0, 'left': 0}
    IS_MAX = {'top': True, 'right': True, 'back': True,
              'bottom': False, 'left': False, 'front': False}
    bpy.context.view_layer.update()

    ax    = AXIS[face_a]
    va    = [obj_a.matrix_world @ v.co for v in obj_a.data.vertices]
    vb    = [obj_b.matrix_world @ v.co for v in obj_b.data.vertices]
    ext_a = max(v[ax] for v in va) if IS_MAX[face_a] else min(v[ax] for v in va)
    ext_b = max(v[ax] for v in vb) if IS_MAX[face_b] else min(v[ax] for v in vb)
    delta_ax = ext_a - ext_b + gap * (1 if IS_MAX[face_a] else -1)

    if obj_b.parent:
        world_d = [0, 0, 0]; world_d[ax] = delta_ax
        obj_b.location += obj_b.parent.matrix_world.inverted().to_3x3() @ Vector(world_d)
    else:
        obj_b.location[ax] += delta_ax

    bpy.context.view_layer.update()
    vb2   = [obj_b.matrix_world @ v.co for v in obj_b.data.vertices]
    ext_b2 = max(v[ax] for v in vb2) if IS_MAX[face_b] else min(v[ax] for v in vb2)
    return abs(ext_b2 - ext_a)   # residuo (< 1e-7 = ok)
```

### Pattern tipici di attacco

```python
# ── Stack: B sopra A (via bounds) ─────────────────────────────
attach_bounds(obj_b, 'bottom', obj_a, 'top')

# ── Stack con gap ─────────────────────────────────────────────
attach_bounds(lid, 'bottom', mug, 'top', gap=0.002)

# ── Punto preciso (local frame): piattino sotto tazza ─────────
# Il top del piattino (z_local=saucer_height) tocca il bottom della tazza (z_local=0)
attach_to(saucer, (0, 0, 0.016), cup, (0, 0, 0.0))

# ── Verifica dopo posizionamento ──────────────────────────────
b = world_bounds(obj)
print(f"bottom z = {b['min'].z:.6f}")
print(f"top    z = {b['max'].z:.6f}")
print(f"center   = {b['center']}")

# ── Trasformazioni frame ──────────────────────────────────────
# "Dove si trova il bordo del rim della tazza nel world space?"
rim_world = local_to_world(cup, (0.031, 0, 0.058))

# "Dato un punto world, dove è nelle coordinate locali del piattino?"
p_local = world_to_local(saucer, rim_world)
```

---

## OGGETTI GENERICI — Pattern Riutilizzabili

### Sedia moderna
```python
def make_chair(name, x, y, z, mat_seat=None, mat_legs=None):
    """Sedia scandinava con 4 gambe, schienale, seduta."""
    parts = []

    # Seduta
    seat = box(f"{name}_Seat", x, y, z+0.45, 0.50, 0.50, 0.04, mat_seat)
    add_bevel(seat, 0.005, 2); parts.append(seat)

    # Schienale
    back = box(f"{name}_Back", x, y+0.23, z+0.72, 0.46, 0.04, 0.55, mat_seat)
    add_bevel(back, 0.005, 2); parts.append(back)

    # 4 gambe
    for lx, ly in [(-0.20,-0.20), (0.20,-0.20), (-0.20,0.20), (0.20,0.20)]:
        leg = cyl(f"{name}_Leg_{lx}{ly}", x+lx, y+ly, z+0.225, 0.018, 0.45, verts=8, mat=mat_legs)
        smooth_shade(leg); parts.append(leg)

    return parts

# sedia_mat = mat_wood("Legno_Chiaro", (0.65, 0.42, 0.20))
# make_chair("Sedia1", 0, 0, 0, sedia_mat, sedia_mat)
```

### Tavolo
```python
def make_table(name, x, y, z, w=1.6, d=0.85, h=0.74,
               mat_top=None, mat_legs=None):
    """Tavolo rettangolare con 4 gambe."""
    top = box(f"{name}_Top", x, y, z+h, w, d, 0.04, mat_top)
    add_bevel(top, 0.006, 2)
    for lx, ly in [(-w/2+0.06, -d/2+0.06), (w/2-0.06, -d/2+0.06),
                   (-w/2+0.06,  d/2-0.06), (w/2-0.06,  d/2-0.06)]:
        leg = box(f"{name}_Leg_{lx:.2f}", x+lx, y+ly, z+h/2-0.02, 0.06, 0.06, h-0.04, mat_legs)
        add_bevel(leg, 0.004, 2)
```

### Lampada da pavimento
```python
def make_floor_lamp(name, x, y, z=0, mat_pole=None, mat_shade=None):
    """Lampada da pavimento: base, asta, paralume conico."""
    # Base
    base = cyl(f"{name}_Base", x, y, z+0.04, 0.15, 0.08, verts=24, mat=mat_pole)
    smooth_shade(base); add_bevel(base, 0.01, 2)
    # Asta
    pole = cyl(f"{name}_Pole", x, y, z+0.8, 0.015, 1.5, verts=16, mat=mat_pole)
    smooth_shade(pole)
    # Paralume (cono)
    bpy.ops.mesh.primitive_cone_add(radius1=0.25, radius2=0.08, depth=0.3,
                                    location=(x, y, z+1.65))
    shade = bpy.context.active_object; shade.name = f"{name}_Shade"
    smooth_shade(shade)
    if mat_shade: assign_mat(shade, mat_shade)
    # Luce interna
    bpy.ops.object.light_add(type='POINT', location=(x, y, z+1.58))
    bulb = bpy.context.active_object; bulb.name = f"{name}_Light"
    bulb.data.energy = 150; bulb.data.color = (1.0, 0.92, 0.75)
    bulb.data.shadow_soft_size = 0.05
```

### Oggetto cilindrico (bottiglia, vaso, tazza)
```python
def make_lathe_object(name, x, y, z, profile_rz, mat=None, smooth=True):
    """
    Crea oggetto di rivoluzione da profilo [(r, z), ...].
    Usa bmesh per massima precisione.
    profile_rz: lista di (raggio, altezza_z) dal basso verso l'alto.

    Esempio tazza:
    profile = [(0,0),(0.04,0),(0.045,0.02),(0.045,0.08),
               (0.04,0.09),(0.035,0.1),(0.035,0.09),(0,0.09)]
    make_lathe_object("Tazza", 0,0,0, profile, mat_ceramic)
    """
    segments = 32
    bm = bmesh.new()
    verts_rings = []
    for r, zz in profile_rz:
        ring = []
        for i in range(segments):
            angle = 2 * math.pi * i / segments
            vx = r * math.cos(angle)
            vy = r * math.sin(angle)
            ring.append(bm.verts.new((x + vx, y + vy, z + zz)))
        verts_rings.append(ring)
    bm.verts.ensure_lookup_table()
    for ri in range(len(verts_rings) - 1):
        r0, r1 = verts_rings[ri], verts_rings[ri + 1]
        for j in range(segments):
            nj = (j + 1) % segments
            bm.faces.new([r0[j], r0[nj], r1[nj], r1[j]])
    bm.normal_update()
    mesh = bpy.data.meshes.new(name + "_mesh")
    bm.to_mesh(mesh); bm.free()
    obj = bpy.data.objects.new(name, mesh)
    bpy.context.collection.objects.link(obj)
    if smooth: smooth_shade(obj)
    if mat: assign_mat(obj, mat)
    return obj
```

---

## ARCHITETTURA — Pattern Aggiornati

### Muro con apertura (Boolean corretto)
```python
def wall_with_openings(name, x, y, z_base, width, thickness, height,
                       openings, mat_wall=None):
    """
    Muro con finestre/porte ricavate per Boolean.
    openings: [{'x': offset, 'z': bottom, 'w': width, 'h': height}, ...]

    Più realistico dei box separati perché il muro è un solido continuo.
    """
    wall = box(name, x, y, z_base + height/2, width, thickness, height, mat_wall)

    for i, op in enumerate(openings):
        cut = box(f"{name}_Cut_{i}",
                  x + op['x'] - width/2, y,
                  z_base + op['z'] + op['h']/2,
                  op['w'], thickness * 2, op['h'])
        bpy.context.view_layer.objects.active = wall
        mod = wall.modifiers.new(f"Cut_{i}", "BOOLEAN")
        mod.operation = 'DIFFERENCE'
        mod.object    = cut
        mod.solver    = 'FAST'
        bpy.ops.object.modifier_apply(modifier=f"Cut_{i}")
        bpy.data.objects.remove(cut, do_unlink=True)
    return wall

# Esempio:
# wall_with_openings("Facciata", 0, -4, 0, 10, 0.3, 3.0, [
#     {'x': 2.0, 'z': 0.8, 'w': 1.4, 'h': 1.2},   # finestra sinistra
#     {'x': 5.0, 'z': 0.8, 'w': 1.4, 'h': 1.2},   # finestra destra
#     {'x': 0.5, 'z': 0.0, 'w': 1.0, 'h': 2.1},   # porta
# ], mat_concrete)
```

### Cornice finestra
```python
def window_frame(name, x, y, z, w, h, depth=0.15,
                 frame_w=0.06, mat_frame=None, mat_glass=None):
    """
    Finestra completa: telaio (4 profili) + vetro.
    frame_w: larghezza profilo del telaio
    """
    # Montanti verticali
    for side, ox in [("L", -(w/2 - frame_w/2)), ("R", w/2 - frame_w/2)]:
        b = box(f"{name}_Frame_{side}", x+ox, y, z, frame_w, depth, h, mat_frame)
        add_bevel(b, 0.004, 2)
    # Traversi orizzontali
    inner_w = w - 2 * frame_w
    for side, oz in [("B", -(h/2 - frame_w/2)), ("T", h/2 - frame_w/2)]:
        b = box(f"{name}_Frame_{side}", x, y, z+oz, inner_w, depth, frame_w, mat_frame)
        add_bevel(b, 0.004, 2)
    # Vetro
    glass = box(f"{name}_Glass", x, y, z, inner_w, depth*0.1, h - 2*frame_w, mat_glass)
    add_solidify(glass, 0.006)
    return glass
```

### Ringhiera parametrica
```python
def railing(name, x_start, x_end, y, z_base, height=1.0,
            post_spacing=1.0, post_r=0.025, bar_r=0.015, mat=None):
    """
    Ringhiera con pali e barra orizzontale.
    Usa Array modifier per i pali → efficiente e modificabile.
    """
    length = x_end - x_start
    n_posts = max(2, int(length / post_spacing) + 1)
    spacing = length / (n_posts - 1)

    # Palo singolo + array
    post = cyl(f"{name}_Post", x_start, y, z_base + height/2,
               post_r, height, verts=8, mat=mat)
    smooth_shade(post)
    if n_posts > 1:
        add_array(post, count=n_posts, relative=False,
                  offset_x=spacing, offset_y=0, offset_z=0)

    # Barra orizzontale
    bar = pipe_along_points(f"{name}_Bar",
        [(x_start, y, z_base + height),
         (x_end,   y, z_base + height)],
        radius=bar_r, mat=mat)

    # Barra inferiore
    bar_low = pipe_along_points(f"{name}_Bar_Low",
        [(x_start, y, z_base + 0.08),
         (x_end,   y, z_base + 0.08)],
        radius=bar_r, mat=mat)
    return post, bar, bar_low
```

### Scala a rampa
```python
def staircase(name, x, y, z_bottom, z_top, width, depth_total,
              mat_step=None, mat_riser=None):
    """
    Scala lineare con pedate e alzate.
    Scende da z_top a z_bottom su profondità depth_total.
    """
    n_steps = max(3, round((z_top - z_bottom) / 0.175))
    step_h  = (z_top - z_bottom) / n_steps
    step_d  = depth_total / n_steps
    parts   = []
    for i in range(n_steps):
        # Pedata
        pz = z_bottom + (i + 1) * step_h
        py = y + depth_total - (i + 0.5) * step_d
        tread = box(f"{name}_Tread_{i}", x, py, pz - step_h/2 + 0.02,
                    width, step_d, 0.04, mat_step)
        add_bevel(tread, 0.005, 2); parts.append(tread)
        # Alzata (opzionale, per scale chiuse)
        if mat_riser:
            riser = box(f"{name}_Riser_{i}", x, py + step_d/2 - 0.02,
                        pz - step_h/2, width, 0.04, step_h, mat_riser)
            parts.append(riser)
    return parts
```

### Tetto a padiglione (hip roof)
```python
def hip_roof(name, cx, cy, z_eave, W, D, rise, overhang=0.5, mat=None):
    hw = W/2 + overhang;  hd = D/2 + overhang
    rl = max((W - D)/2, 0.8);  zr = z_eave + rise
    v = [(cx-hw,cy-hd,z_eave),(cx+hw,cy-hd,z_eave),
         (cx+hw,cy+hd,z_eave),(cx-hw,cy+hd,z_eave),
         (cx-rl,cy,zr),(cx+rl,cy,zr)]
    f = [(0,1,5,4),(2,3,4,5),(4,3,0),(1,2,5)]
    obj = make_mesh_from_data(name, v, f)
    if mat: assign_mat(obj, mat)
    return obj
```

### Barra diagonale XZ (X-frame, croce di Sant'Andrea)
```python
def diag_bar_xz(name, x1, z1, x2, z2, y_wall,
                thickness=0.048, depth=0.07, mat=None):
    """
    Barra diagonale piatta su parete frontale (piano XZ).
    NON usare box ruotati — proiettano la lunghezza in Y.
    """
    dx=x2-x1; dz=z2-z1; ln=math.sqrt(dx*dx+dz*dz)
    if ln < 1e-6: return None
    dx/=ln; dz/=ln; px=-dz; pz=dx
    mx=(x1+x2)/2; mz=(z1+z2)/2
    hl=ln/2; ht=thickness/2; hd=depth/2
    verts = []
    for sl in (-hl, hl):
        for st in (-ht, ht):
            for sd in (-hd, hd):
                verts.append((mx+sl*dx+st*px, y_wall+sd, mz+sl*dz+st*pz))
    faces = [(0,2,3,1),(4,5,7,6),(0,1,5,4),(2,6,7,3),(0,4,6,2),(1,3,7,5)]
    obj = make_mesh_from_data(name, verts, faces)
    if mat: assign_mat(obj, mat)
    return obj
```

---

## MATERIALI

> **Blender 5.x API notes:**
> - `blend_method` è **DEPRECATO** → usa `surface_render_method = "BLENDED"` (trasparenza colorata) o `"DITHERED"` (compatibile con passes)
> - `use_nodes` setter è deprecated (5.0+), ma la proprietà esiste ancora
> - Principled BSDF ora usa modello **OpenPBR**: ha layer Coat (clearcoat), Sheen, Subsurface migliorato
> - Input sicuro: controlla `if 'Nome' in [i.name for i in bsdf.inputs]` per versione-safety
> - **`ShaderNodeMixRGB` è DEPRECATO** in Blender 5.x → usa `ShaderNodeMix` con `node.data_type = 'RGBA'`. Gli input cambiano: `inputs[0]` = Factor, `inputs[6]` = Color A, `inputs[7]` = Color B, `outputs[2]` = Color. Con MixRGB il nodo esiste ma restituisce valori di default (giallo) invece del mix corretto — bug silenzioso!
>
> ```python
> # SBAGLIATO (Blender 5.x):
> mix = nt.nodes.new('ShaderNodeMixRGB')
> mix.inputs[1].default_value = (1,0,0,1)  # → restituisce giallo default
>
> # CORRETTO (Blender 4.x+):
> mix = nt.nodes.new('ShaderNodeMix')
> mix.data_type = 'RGBA'
> mix.blend_type = 'MIX'
> mix.inputs[6].default_value = (1,0,0,1)   # Color A
> mix.inputs[7].default_value = (0,1,0,1)   # Color B
> nt.links.new(factor_socket, mix.inputs[0])
> nt.links.new(mix.outputs[2], bsdf.inputs['Base Color'])
> ```

### Helper: accesso input sicuro
```python
def bsdf_set(bsdf, input_name, value):
    """Setta input BSDF solo se esiste (version-safe)."""
    input_names = [i.name for i in bsdf.inputs]
    if input_name in input_names:
        bsdf.inputs[input_name].default_value = value
```

### PBR base
```python
def mat_pbr(name, color, roughness=0.5, metallic=0.0, alpha=1.0):
    m = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    m.use_nodes = True; m.node_tree.nodes.clear()
    bsdf = m.node_tree.nodes.new('ShaderNodeBsdfPrincipled')
    out  = m.node_tree.nodes.new('ShaderNodeOutputMaterial')
    m.node_tree.links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])
    bsdf.inputs['Base Color'].default_value = (*color, 1.0)
    bsdf.inputs['Roughness'].default_value  = roughness
    bsdf.inputs['Metallic'].default_value   = metallic
    if alpha < 1.0:
        # Blender 5.x: surface_render_method sostituisce blend_method
        try: m.surface_render_method = "BLENDED"
        except: m.blend_method = 'BLEND'   # fallback < 4.x
        bsdf.inputs['Alpha'].default_value = alpha
    return m
```

### Noise procedurale (calcestruzzo, marmo, roccia)
```python
def mat_noise(name, c1, c2, roughness=0.85, scale=8, distortion=0.1,
              noise_type='MULTIFRACTAL'):
    """
    Materiale con variazione procedurale.
    c1/c2: colori min/max della variazione.
    scale: densità rumore (6=grosso, 15=fine come marmo)

    noise_type (Blender 5.x ShaderNodeTexNoise):
      'MULTIFRACTAL'       → default, variazione organica naturale
      'FBM'                → Fractional Brownian Motion, simile ma più uniforme
      'RIDGED_MULTIFRACTAL'→ creste nette, ottimo per rocce e terrain
      'HYBRID_MULTIFRACTAL'→ via di mezzo
      'HETERO_TERRAIN'     → dettagli eterogeni, terrain realistico
    """
    m = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    m.use_nodes = True; tree = m.node_tree
    tree.nodes.clear()
    out   = tree.nodes.new('ShaderNodeOutputMaterial')
    bsdf  = tree.nodes.new('ShaderNodeBsdfPrincipled')
    noise = tree.nodes.new('ShaderNodeTexNoise')
    cramp = tree.nodes.new('ShaderNodeValToRGB')
    # Blender 5.x: noise_type è un enum sull'nodo
    try: noise.noise_type = noise_type
    except: pass
    noise.inputs['Scale'].default_value      = scale
    noise.inputs['Detail'].default_value     = 6.0
    noise.inputs['Roughness'].default_value  = 0.6
    noise.inputs['Distortion'].default_value = distortion
    cramp.color_ramp.elements[0].color = (*c1, 1.0)
    cramp.color_ramp.elements[1].color = (*c2, 1.0)
    tree.links.new(noise.outputs['Fac'],   cramp.inputs['Fac'])
    tree.links.new(cramp.outputs['Color'], bsdf.inputs['Base Color'])
    bsdf.inputs['Roughness'].default_value = roughness
    tree.links.new(bsdf.outputs['BSDF'],   out.inputs['Surface'])
    return m

# Esempi noise_type per materiali specifici:
# Calcestruzzo:  mat_noise("Concrete", (0.55,0.52,0.50), (0.38,0.35,0.32), roughness=0.90)
# Marmo bianco:  mat_noise("Marble", (0.95,0.94,0.92), (0.70,0.68,0.65), scale=14, distortion=0.6)
# Roccia:        mat_noise("Rock", (0.25,0.22,0.18), (0.12,0.10,0.08), noise_type='RIDGED_MULTIFRACTAL')
# Terreno:       mat_noise("Terrain", (0.18,0.14,0.08), (0.08,0.06,0.04), noise_type='HETERO_TERRAIN')
```

### Vetro
```python
def mat_glass(name="Glass", color=(0.8, 0.9, 1.0), roughness=0.02, ior=1.52):
    """
    Blender 5.x: Transmission Weight sostituisce Transmission.
    use_raytrace_refraction abilita rifrazione raytracing in EEVEE Next.
    surface_render_method = "BLENDED" per trasparenza corretta.
    """
    m = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    m.use_nodes = True; tree = m.node_tree; tree.nodes.clear()
    bsdf = tree.nodes.new('ShaderNodeBsdfPrincipled')
    out  = tree.nodes.new('ShaderNodeOutputMaterial')
    bsdf.inputs['Base Color'].default_value = (*color, 1.0)
    bsdf.inputs['Roughness'].default_value  = roughness
    # Transmission: Blender 4.x → "Transmission", 5.x → "Transmission Weight"
    for tname in ['Transmission Weight', 'Transmission']:
        if tname in [i.name for i in bsdf.inputs]:
            bsdf.inputs[tname].default_value = 1.0
            break
    if 'IOR' in [i.name for i in bsdf.inputs]:
        bsdf.inputs['IOR'].default_value = ior
    tree.links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])
    # Blender 5.x: surface_render_method sostituisce blend_method
    try:
        m.surface_render_method = "BLENDED"
        m.use_raytrace_refraction = True   # rifrazione EEVEE Next
    except:
        m.blend_method = 'BLEND'
    return m

# Varianti vetro:
# Vetro chiaro:     mat_glass("VetroChiaro", (0.92,0.97,1.0), roughness=0.01, ior=1.52)
# Vetro verde:      mat_glass("VetroVerde",  (0.65,0.88,0.72), roughness=0.02, ior=1.52)
# Vetro smerigliato:mat_glass("VetroFrost",  (0.90,0.92,0.95), roughness=0.35, ior=1.47)
# Bottiglia vino:   mat_glass("BottleDark",  (0.04,0.18,0.06), roughness=0.03, ior=1.52)
```

### Metallo (acciaio, alluminio, rame, clearcoat)
```python
def mat_metal(name, color=(0.8, 0.8, 0.8), roughness=0.15, anisotropic=0.3,
              clearcoat=0.0):
    """
    roughness: 0.05=specchio, 0.15=spazzolato, 0.4=opaco
    clearcoat: 0.0=nessuno, 1.0=vernice lucida (auto, lacca)
    color:
      acciaio:  (0.80, 0.80, 0.82)
      alluminio:(0.91, 0.92, 0.93)
      rame:     (0.95, 0.64, 0.54)
      oro:      (1.00, 0.78, 0.34)
      cromo:    (0.95, 0.95, 0.96)

    Blender 5.x OpenPBR: Coat layer sostituisce il vecchio Clearcoat.
    Per metalli ad alta roughness usa distribution='MULTI_GGX' (più realistico).
    """
    m = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    m.use_nodes = True; tree = m.node_tree; tree.nodes.clear()
    bsdf = tree.nodes.new('ShaderNodeBsdfPrincipled')
    out  = tree.nodes.new('ShaderNodeOutputMaterial')
    inp  = [i.name for i in bsdf.inputs]
    bsdf.inputs['Base Color'].default_value  = (*color, 1.0)
    bsdf.inputs['Metallic'].default_value    = 1.0
    bsdf.inputs['Roughness'].default_value   = roughness
    if 'Anisotropic' in inp:
        bsdf.inputs['Anisotropic'].default_value = anisotropic
    # Coat layer (Blender 5.x OpenPBR — clearcoat)
    if clearcoat > 0:
        if 'Coat Weight' in inp:        # Blender 5.x
            bsdf.inputs['Coat Weight'].default_value    = clearcoat
            bsdf.inputs['Coat Roughness'].default_value = 0.05
            bsdf.inputs['Coat IOR'].default_value       = 1.50
        elif 'Clearcoat' in inp:        # Blender 4.x
            bsdf.inputs['Clearcoat'].default_value          = clearcoat
            bsdf.inputs['Clearcoat Roughness'].default_value = 0.05
    tree.links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])
    return m

# Esempi:
# mat_metal("Acciaio",    (0.80,0.80,0.82), roughness=0.15)
# mat_metal("Cromo",      (0.95,0.95,0.96), roughness=0.04)
# mat_metal("CarPaint",   (0.05,0.08,0.65), roughness=0.20, clearcoat=1.0)
# mat_metal("Oro",        (1.00,0.78,0.34), roughness=0.08, anisotropic=0.5)
```

### Subsurface scattering (pelle, cera, cibo, marmo)
```python
def mat_subsurface(name, color, subsurface_color=None, roughness=0.6,
                   radius=(1.0, 0.2, 0.1), scale=0.01, method='RANDOM_WALK'):
    """
    Subsurface scattering per materiali traslucenti.
    Blender 5.x OpenPBR: 'Subsurface Weight' + 'subsurface_method'.

    method:
      'RANDOM_WALK'      → pelle, cera, marmo (più preciso)
      'RANDOM_WALK_SKIN' → pelle umana con epidermide
      'BURLEY'           → veloce, meno preciso

    radius: (R, G, B) scattering — sangue/pelle: (1.0, 0.2, 0.1)
    scale:  0.005=pelle sottile, 0.02=cera, 0.05=marmo
    """
    m = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    m.use_nodes = True; tree = m.node_tree; tree.nodes.clear()
    bsdf = tree.nodes.new('ShaderNodeBsdfPrincipled')
    out  = tree.nodes.new('ShaderNodeOutputMaterial')
    inp  = [i.name for i in bsdf.inputs]
    bsdf.inputs['Base Color'].default_value = (*color, 1.0)
    bsdf.inputs['Roughness'].default_value  = roughness
    # Subsurface Weight (Blender 5.x) o Subsurface (4.x)
    for sname in ['Subsurface Weight', 'Subsurface']:
        if sname in inp:
            bsdf.inputs[sname].default_value = 0.8
            break
    if 'Subsurface Radius' in inp:
        bsdf.inputs['Subsurface Radius'].default_value = radius
    if 'Subsurface Scale' in inp:
        bsdf.inputs['Subsurface Scale'].default_value  = scale
    if subsurface_color and 'Subsurface Color' in inp:
        bsdf.inputs['Subsurface Color'].default_value  = (*subsurface_color, 1.0)
    # Metodo subsurface
    try: bsdf.subsurface_method = method
    except: pass
    tree.links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])
    return m

# Esempi:
# mat_subsurface("Skin",  (0.84,0.61,0.50), method='RANDOM_WALK_SKIN', scale=0.006)
# mat_subsurface("Wax",   (0.98,0.94,0.82), method='RANDOM_WALK', scale=0.025)
# mat_subsurface("Marble",(0.94,0.92,0.90), method='RANDOM_WALK', radius=(0.8,0.6,0.5), scale=0.04)
```

### Tessuto / velluto (Sheen layer)
```python
def mat_fabric(name, color, roughness=0.85, sheen=0.8, sheen_tint=(1,1,1)):
    """
    Materiale tessuto con Sheen layer (Blender 5.x OpenPBR).
    Sheen dà l'effetto vellutato caratteristico dei tessuti.

    Blender 5.x: 'Sheen Weight' + 'Sheen Roughness' + 'Sheen Tint'
    Blender 4.x: 'Sheen' + 'Sheen Tint'
    """
    m = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    m.use_nodes = True; tree = m.node_tree; tree.nodes.clear()
    bsdf = tree.nodes.new('ShaderNodeBsdfPrincipled')
    out  = tree.nodes.new('ShaderNodeOutputMaterial')
    inp  = [i.name for i in bsdf.inputs]
    bsdf.inputs['Base Color'].default_value = (*color, 1.0)
   

…(truncated)
