Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ jobs:
- name: Showcase helper drift (warn-only report)
run: python3 tests/check_helper_drift.py

- name: Examples and showcase follow the repo's own rules
run: python3 tests/check_example_rules.py

- name: Check asset_sheet.py can render every showcase piece and reference
run: python3 tests/check_asset_sheet_select.py

Expand Down
6 changes: 6 additions & 0 deletions docs/new-example-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ intentional, important geometry is not clipped, highlights are not blown out,
lights or helper objects are not visible unintentionally, and materials, lighting,
background, and camera feel designed rather than left at Blender defaults.

The example must follow the repo's own rules, not just teach them:
`tests/check_example_rules.py` (run by Validate) rejects a per-element write
in a loop over `mesh.polygons` / `mesh.vertices` (use `foreach_set`) and an
`active_object` read with no `None` guard. A loop that really needs per-element
logic carries `# foreach-exempt: <why>` on or just above its `for` line.

Contact-sheet gate (required before shipping the still): build a composite placing
the new hero beside the pinned calibration set—canonical membership is in
`CLAUDE.md` § Quality Gates for Example Runs—and compare stage darkness, wedge
Expand Down
3 changes: 1 addition & 2 deletions examples/armature-bend/armature_bend.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,7 @@ def build_tube():
bm.to_mesh(me)
finally:
bm.free()
for poly in me.polygons:
poly.use_smooth = True
me.polygons.foreach_set("use_smooth", [True] * len(me.polygons))
# collars keep crisp machined steps; the rubber ribs stay smooth
me.set_sharp_from_angle(angle=math.radians(50.0))
obj = bpy.data.objects.new("Tube", me)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,7 @@ def decimate_apply(obj, target_tris):
object=obj, active_object=obj, selected_objects=[obj]
):
bpy.ops.object.modifier_apply(modifier=mod.name)
for poly in obj.data.polygons:
poly.use_smooth = True
obj.data.polygons.foreach_set("use_smooth", [True] * len(obj.data.polygons))
obj.data.update()
return current, evaluated_triangle_count(obj)

Expand Down
9 changes: 3 additions & 6 deletions examples/bmesh-gear/bmesh_gear.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,7 @@ def companion_pitch_r(teeth):

def render_still(obj, path, engine):
scene = bpy.context.scene
for poly in obj.data.polygons:
poly.use_smooth = False # crisp machined facets
obj.data.polygons.foreach_set("use_smooth", [False] * len(obj.data.polygons)) # crisp machined facets
brass = machined_brass()
obj.data.materials.append(brass)
blued = principled("BluedSteel", (0.07, 0.11, 0.22), 0.26, metal=1.0, noise=3.0)
Expand Down Expand Up @@ -329,8 +328,7 @@ def partner(name, teeth, direction, mat, depth, bore=None):
cy = HERO_C[1] + dist * math.sin(direction)
me = ring_gear_mesh(name, teeth, pr - ADDENDUM, pr + ADDENDUM, depth, bore=bore)
me.materials.append(mat)
for p in me.polygons:
p.use_smooth = False
me.polygons.foreach_set("use_smooth", [False] * len(me.polygons))
ob = bpy.data.objects.new(name, me)
scene.collection.objects.link(ob)
# a gap centred on the line of centres, facing the checked gear's tooth
Expand Down Expand Up @@ -368,8 +366,7 @@ def web(bm):
def add(name, build, mat, cx, cy, smooth=False, bevel=0.01):
me = solid_mesh(name, build)
me.materials.append(mat)
for p in me.polygons:
p.use_smooth = smooth
me.polygons.foreach_set("use_smooth", [smooth] * len(me.polygons))
ob = bpy.data.objects.new(name, me)
scene.collection.objects.link(ob)
place(ob, cx, cy)
Expand Down
6 changes: 2 additions & 4 deletions examples/boolean-exact-volume/boolean_exact_volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,7 @@ def tube_mesh(name, segments, radius, sides=10):
bm.to_mesh(me)
finally:
bm.free()
for p in me.polygons:
p.use_smooth = True
me.polygons.foreach_set("use_smooth", [True] * len(me.polygons))
return me


Expand Down Expand Up @@ -357,8 +356,7 @@ def add(name, me, mat, mw=None):
# the boolean output carries the operands' (empty) material slots;
# start clean so every face takes the finish below
shown.materials.clear()
for poly in shown.polygons:
poly.material_index = 0
shown.polygons.foreach_set("material_index", [0] * len(shown.polygons))
ob = add(f"{op.title()}Shown", shown, finishes[op], res.matrix_world.copy())
bev = ob.modifiers.new("MachinedChamfer", 'BEVEL')
bev.width = 0.012
Expand Down
1 change: 1 addition & 0 deletions examples/car-mirror-symmetry/car_mirror_symmetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ def _assign_body_materials(body):
are glass exactly where the cabin factor ramps (windshield, hatch)."""
PAINT, GLASS, TRIM, CHROME = (MATERIALS.index(m)
for m in ("Paint", "Glass", "Trim", "Chrome"))
# foreach-exempt: per-face material from the face's ring/station index
for poly in body.data.polygons:
i, k = divmod(poly.index, RING - 1)
if i >= N_ST - 1: # cap ngons (front/rear fascia)
Expand Down
12 changes: 6 additions & 6 deletions examples/collision-hull-proxy/collision_hull_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ def build_hydrant():
body = lathe_object("Body", BODY_PROFILE, SEGMENTS)
body.data.materials.append(red)
body.data.materials.append(yellow)
# foreach-exempt: conditional per-face material by face height
for p in body.data.polygons: # the bonnet dome is the yellow profile tail
if p.center.z > 1.09:
p.material_index = 1
Expand Down Expand Up @@ -384,8 +385,7 @@ def render_still(groups, pieces, path, engine):
scene = bpy.context.scene
for g in groups:
for o in g["render"]:
for p in o.data.polygons:
p.use_smooth = True
o.data.polygons.foreach_set("use_smooth", [True] * len(o.data.polygons))
for g in groups: # cages exist only to build hulls; never rendered
for o in g["cage"]:
o.hide_render = True
Expand Down Expand Up @@ -415,8 +415,7 @@ def render_still(groups, pieces, path, engine):

for name, hull, _ in pieces:
hull.data.materials.append(shell)
for p in hull.data.polygons:
p.use_smooth = False # facets must read as facets
hull.data.polygons.foreach_set("use_smooth", [False] * len(hull.data.polygons)) # facets must read as facets
hull.data.materials.append(wire_mat)
wire = bpy.data.objects.new(f"{name}Wire", hull.data)
mod = wire.modifiers.new("Wire", 'WIREFRAME')
Expand Down Expand Up @@ -521,8 +520,9 @@ def main():
for g in groups:
hull = build_hull(f"{g['name']}Hull", collect_points(g["cage"]))
if args.shrink_hull:
for v in hull.data.vertices:
v.co *= 0.5
co = [0.0] * (len(hull.data.vertices) * 3)
hull.data.vertices.foreach_get("co", co)
hull.data.vertices.foreach_set("co", [c * 0.5 for c in co])
hull.data.update()
pieces.append((g["name"], hull, collect_points(g["render"])))
code = check(pieces)
Expand Down
3 changes: 1 addition & 2 deletions examples/color-attribute-wheel/color_attribute_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,7 @@ def _mat(name, base, rough, metal=0.0, noise=None, coat=0.0):
def render_still(obj, path, engine):
scene = bpy.context.scene
me = obj.data
for poly in me.polygons:
poly.use_smooth = True
me.polygons.foreach_set("use_smooth", [True] * len(me.polygons))

mat = build_material()
if mat is None:
Expand Down
5 changes: 3 additions & 2 deletions examples/compositor-glare/compositor_glare.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,11 @@ def build_scene():
major_segments=64, minor_segments=16,
location=(x, y, RING_Z), rotation=(math.radians(90), 0.0, 0.0))
ring = bpy.context.active_object
if ring is None:
raise RuntimeError("the add operator left no active object")
ring.name = name
ring.data.materials.append(make_emissive(f"Neon{name[4:]}", color, strength))
for poly in ring.data.polygons:
poly.use_smooth = True
ring.data.polygons.foreach_set("use_smooth", [True] * len(ring.data.polygons))

world = bpy.data.worlds.new("World")
world.use_nodes = True
Expand Down
6 changes: 2 additions & 4 deletions examples/custom-normals-shade/custom_normals_shade.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,8 +478,7 @@ def check_by_angle(objs, mismatch_angle=False):
total_sharp = total_manifold = 0
for obj in objs:
me = obj.data
for p in me.polygons:
p.use_smooth = True
me.polygons.foreach_set("use_smooth", [True] * len(me.polygons))
me.set_sharp_from_angle(angle=mark)
dih, nonmanifold = manifold_dihedrals(me)
if nonmanifold:
Expand Down Expand Up @@ -751,8 +750,7 @@ def shade(parts, mode):
"""The one variable across the three cans."""
for o in parts:
me = o.data
for p in me.polygons:
p.use_smooth = mode != "flat"
me.polygons.foreach_set("use_smooth", [mode != 'flat'] * len(me.polygons))
if mode == "byangle":
me.set_sharp_from_angle(angle=ANGLE)

Expand Down
3 changes: 1 addition & 2 deletions examples/depsgraph-export/depsgraph_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,8 +474,7 @@ def render_still(shell, path, engine):
cage_me.name = "Cage.Shell"
cage_me.materials.clear()
cage_me.materials.append(ghost("Cage.Ghost", (0.02, 0.12, 0.55, 1.0), 0.30))
for p in cage_me.polygons:
p.material_index = 0
cage_me.polygons.foreach_set("material_index", [0] * len(cage_me.polygons))
cage_me.shade_flat()
cage_shell = bpy.data.objects.new("Cage.Shell", cage_me)
wire = bpy.data.objects.new("Cage.Wire", shell.data.copy())
Expand Down
9 changes: 3 additions & 6 deletions examples/driver-wave/driver_wave.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,7 @@ def build_columns(flat_expr=False):
bm.to_mesh(me)
finally:
bm.free()
for poly in me.polygons:
poly.use_smooth = True
me.polygons.foreach_set("use_smooth", [True] * len(me.polygons))
me.set_sharp_from_angle(angle=math.radians(50.0))

objs = []
Expand Down Expand Up @@ -238,8 +237,7 @@ def box(name, dims, loc, mat, bevel=0.02):
bm.to_mesh(fin_me)
finally:
bm.free()
for poly in fin_me.polygons:
poly.use_smooth = True
fin_me.polygons.foreach_set("use_smooth", [True] * len(fin_me.polygons))
fin_me.materials.append(brass)
fin = bpy.data.objects.new(fin_me.name, fin_me)
fin.location = (tx, 0.18, 1.06 + 3.40)
Expand All @@ -258,8 +256,7 @@ def box(name, dims, loc, mat, bevel=0.02):
bm.to_mesh(foot_me)
finally:
bm.free()
for poly in foot_me.polygons:
poly.use_smooth = True
foot_me.polygons.foreach_set("use_smooth", [True] * len(foot_me.polygons))
foot_me.materials.append(brass)
dg = bpy.context.evaluated_depsgraph_get()
for obj in objs:
Expand Down
3 changes: 1 addition & 2 deletions examples/gltf-export-roundtrip/gltf_export_roundtrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,7 @@ def build_crate():
bm.to_mesh(me)
finally:
bm.free() # the ownership contract, as always
for poly in me.polygons:
poly.use_smooth = False # crisp low-poly facets; normals are unambiguous
me.polygons.foreach_set("use_smooth", [False] * len(me.polygons)) # crisp low-poly facets; normals are unambiguous
uv = me.uv_layers.new(name="UVMap")
for poly in me.polygons:
for li in poly.loop_indices:
Expand Down
15 changes: 9 additions & 6 deletions examples/gn-sdf-remesh/gn_sdf_remesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ def build():
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.mesh.primitive_torus_add(location=(0, 0, 0.55), major_radius=1.2, minor_radius=0.5)
obj = bpy.context.active_object
for p in obj.data.polygons:
p.use_smooth = True
if obj is None:
raise RuntimeError("the add operator left no active object")
obj.data.polygons.foreach_set("use_smooth", [True] * len(obj.data.polygons))
mat = bpy.data.materials.new("Ceramic"); mat.use_nodes = True
b = mat.node_tree.nodes.get('Principled BSDF')
b.inputs['Base Color'].default_value = (0.45, 0.025, 0.05, 1) # crimson ceramic
Expand Down Expand Up @@ -82,9 +83,10 @@ def build_vase_kit(name):
def add(op, mat_i, **kw):
op(**kw)
ob = bpy.context.active_object
if ob is None:
raise RuntimeError("the add operator left no active object")
ob.data.materials.append(mats[mat_i])
for poly in ob.data.polygons:
poly.use_smooth = True
ob.data.polygons.foreach_set("use_smooth", [True] * len(ob.data.polygons))
parts.append(ob)
return ob

Expand Down Expand Up @@ -145,8 +147,7 @@ def render_still(obj, path, engine):
cm.inputs['Roughness'].default_value = 0.6
cage = bpy.data.objects.new("VaseKitCage", kit.data.copy())
cage.data.materials.clear(); cage.data.materials.append(cage_mat)
for poly in cage.data.polygons:
poly.material_index = 0
cage.data.polygons.foreach_set("material_index", [0] * len(cage.data.polygons))
cage.location = kit.location
wire = cage.modifiers.new("cage", 'WIREFRAME')
wire.thickness = 0.005; wire.offset = 1.0; wire.use_even_offset = True
Expand All @@ -172,6 +173,8 @@ def render_still(obj, path, engine):
bpy.ops.mesh.primitive_cylinder_add(vertices=64, radius=0.95, depth=0.12,
location=(x, 0, 0.06))
bat = bpy.context.active_object
if bat is None:
raise RuntimeError("the add operator left no active object")
bat.data.materials.append(wood)
bev = bat.modifiers.new("bev", 'BEVEL'); bev.width = 0.025; bev.segments = 3
bats.append(bat)
Expand Down
3 changes: 1 addition & 2 deletions examples/gn-socket-rename/gn_socket_rename.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,8 +467,7 @@ def part(name, build, mat, parent=obj):
bm.free()
me.materials.append(mat)
me.set_sharp_from_angle(angle=math.radians(40))
for p in me.polygons:
p.use_smooth = True
me.polygons.foreach_set("use_smooth", [True] * len(me.polygons))
ob = bpy.data.objects.new(name, me)
scene.collection.objects.link(ob)
ob.parent = parent
Expand Down
3 changes: 1 addition & 2 deletions examples/gp-lineart-contour/gp_lineart_contour.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,7 @@ def build_lighthouse(sc):
bm.to_mesh(me)
finally:
bm.free()
for p in me.polygons:
p.use_smooth = False
me.polygons.foreach_set("use_smooth", [False] * len(me.polygons))
for name, col in (("Lighthouse.Red", (0.72, 0.07, 0.05, 1.0)),
("Lighthouse.White", (0.93, 0.90, 0.82, 1.0)),
("Lighthouse.Iron", (0.10, 0.12, 0.15, 1.0)),
Expand Down
7 changes: 5 additions & 2 deletions examples/grease-pencil-rosette/grease_pencil_rosette.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,7 @@ def _box(name, dims, loc, mat, bevel=0.0, segments=3):
bm.to_mesh(me)
finally:
bm.free()
for poly in me.polygons:
poly.use_smooth = True
me.polygons.foreach_set("use_smooth", [True] * len(me.polygons))
me.materials.append(mat)
ob = bpy.data.objects.new(name, me)
ob.location = loc
Expand Down Expand Up @@ -292,6 +291,8 @@ def render_still(obj, path, engine):
SIGN_Z + sz * (BH / 2 - 0.25)),
rotation=(math.radians(90), 0, 0))
cap = bpy.context.active_object
if cap is None:
raise RuntimeError("the add operator left no active object")
cap.data.materials.append(brass)
caps.append(cap)
# a brass plate under the tubes, a mains cable dropping to the floor
Expand All @@ -300,6 +301,8 @@ def render_still(obj, path, engine):
bpy.ops.mesh.primitive_cylinder_add(vertices=12, radius=0.018, depth=SIGN_Z - BH / 2 - fw,
location=(1.35, 0.28, (SIGN_Z - BH / 2 - fw) / 2))
cable = bpy.context.active_object
if cable is None:
raise RuntimeError("the add operator left no active object")
cable.data.materials.append(cable_mat)
sign = [board, plate, cable] + frame + caps

Expand Down
9 changes: 3 additions & 6 deletions examples/lattice-deform/lattice_deform.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,7 @@ def tube_mesh(name, segments, radius, sides=10):
bm.to_mesh(me)
finally:
bm.free()
for p in me.polygons:
p.use_smooth = True
me.polygons.foreach_set("use_smooth", [True] * len(me.polygons))
return me


Expand All @@ -252,8 +251,7 @@ def sphere_mesh(name, centers, radius):
bm.to_mesh(me)
finally:
bm.free()
for p in me.polygons:
p.use_smooth = True
me.polygons.foreach_set("use_smooth", [True] * len(me.polygons))
return me


Expand Down Expand Up @@ -291,8 +289,7 @@ def render_still(obj, lat_obj, path, engine):
for i in (0, 1) for j in (0, 1) for k in (0, 1)}

obj.data.materials.append(checker_ceramic())
for p in obj.data.polygons:
p.use_smooth = False
obj.data.polygons.foreach_set("use_smooth", [False] * len(obj.data.polygons))

steel = principled("CageSteel", (0.58, 0.60, 0.64), 0.22, metal=1.0)
ghost = principled("RestCage", (0.16, 0.17, 0.19), 0.6, metal=0.3)
Expand Down
6 changes: 2 additions & 4 deletions examples/light-link-studio/light_link_studio.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ def sphere(name, rgb, x, coll):
bm.to_mesh(me)
finally:
bm.free()
for p in me.polygons:
p.use_smooth = True
me.polygons.foreach_set("use_smooth", [True] * len(me.polygons))
mat = bpy.data.materials.new(name + "M")
mat.use_nodes = True
b = mat.node_tree.nodes["Principled BSDF"]
Expand Down Expand Up @@ -103,8 +102,7 @@ def _cyl(sc, name, r1, r2, depth, loc, mat, bevel=0.0):
finally:
bm.free()
me.update()
for p in me.polygons: # smooth the wall, keep the caps flat
p.use_smooth = abs(p.normal.z) < 0.5
me.polygons.foreach_set("use_smooth", [abs(p.normal.z) < 0.5 for p in me.polygons]) # smooth the wall, keep the caps flat
me.materials.append(mat)
ob = bpy.data.objects.new(name, me)
ob.location = loc
Expand Down
1 change: 1 addition & 0 deletions examples/lightmap-uv-channel/lightmap_uv_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ def build_cart_meshes():
top = canopy_z0 + math.sqrt(r_in * r_in - py * py) + post_bite
me = _box(f"Cart.Post.{tag}", (0.09, 0.09, top - post_z0),
(px, py, 0.5 * (post_z0 + top)), 0.015)
# foreach-exempt: conditional per-vertex arc fit of the post tops
for v in me.vertices:
if v.co.z > top - 0.05:
arc = canopy_z0 + math.sqrt(r_in * r_in - v.co.y * v.co.y) + post_bite
Expand Down
Loading
Loading