testsAndMisc/python_pkg/artgate/routes/blender_scene.py
Krzysztof kuhy Rudnicki e6e0ba5ab3
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
artgate: diffusion + 3D routes, full vector coverage, and the margin gate
Closes the remaining bake-off cells. The phase's real result was not a route
score but a MISSING GATE.

All twelve SDXL icons measured 100% opaque coverage -- square tiles with no
sprite silhouette -- and passed every gate in the harness. Gate-pass was
measuring pipeline hygiene, not usability. MARGIN_TOO_SMALL inverts the table
from a meaningless 12/12/12 tie to:

    diffusion   0/12      (every output full-bleed)
    vector     10/12
    procedural 11/12

The two non-diffusion failures were true positives in MY shapes, not gate
error: the procedural sword pommel ran off the bottom edge and the vector meat
bone clipped the top. Both fixed; both routes back to 12/12.

Diffusion recovers to 12/12 with flood-fill background removal, but visual
inspection shows the fill eats into the art (bone reduced to a fragment, sword
nearly erased). Reported as inadequate rather than shipped -- proper matting is
needed before route (b) is a usable pipeline.

Blender exits 0 even when its embedded script raises: BLENDER_EEVEE_NEXT does
not exist in 5.2, and the failure produced a clean exit and no file. render()
now asserts the output exists, or the route would report phantom successes.

Model gating verified at execution time, never recalled: SDXL base and
nerijs/pixel-art-xl are ungated, FLUX.1-dev is gated=auto, and
runwayml/stable-diffusion-v1-5 now 307-redirects.

The finishing stage now generalises across three continuous-tone sources
(SVG raster, Blender render, SDXL): each is 0/N raw and N/N finished.

Verified: 114 tests, 100% branch coverage on 13 modules, zero suppressions,
all hooks pass. Note: a local pytest run masked a missing branch that the
pre-commit hook caught -- the hook's run is authoritative.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9rQ12yZ7sJxiPQTCS5dUF
2026-08-15 22:51:31 +02:00

147 lines
4.5 KiB
Python

"""Route (d) 3D: a Blender scene script, run headless.
This module is *emitted* and executed by ``blender --background --python``, so
it must not import anything from this package -- the Blender interpreter has
no path to it. It is kept here as a data file rather than a library so the
repo's linters still see it as Python.
The 3D cell exists to answer one question: does a rendered mesh, put through
the same finishing stage as the vector route, clear the pixel gates? The
render is continuous-tone by nature, so the expectation is the same as every
other continuous route -- it needs the finisher.
"""
from __future__ import annotations
from typing import Final
# Rendered square edge length before downscaling. Kept small: the finisher
# reduces to 32px anyway, and Cycles time scales with pixel count.
RENDER_SIZE: Final = 256
# Executed inside Blender's own Python. Written as a template rather than
# imported, because Blender cannot see this package.
SCENE_TEMPLATE: Final = """
import sys
import bpy
import mathutils
SUBJECT = sys.argv[-2]
OUT = sys.argv[-1]
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.render.engine = "BLENDER_EEVEE"
scene.render.resolution_x = {size}
scene.render.resolution_y = {size}
scene.render.film_transparent = True
scene.render.image_settings.file_format = "PNG"
scene.render.image_settings.color_mode = "RGBA"
scene.render.filepath = OUT
def material(name, rgba):
mat = bpy.data.materials.new(name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes["Principled BSDF"]
bsdf.inputs["Base Color"].default_value = rgba
bsdf.inputs["Roughness"].default_value = 0.45
return mat
GOLD = (0.84, 0.64, 0.24, 1.0)
CYAN = (0.36, 0.69, 0.75, 1.0)
RED = (0.69, 0.24, 0.24, 1.0)
BONE = (0.78, 0.73, 0.67, 1.0)
def add(obj, rgba):
obj.data.materials.append(material(obj.name, rgba))
return obj
def build_coin():
bpy.ops.mesh.primitive_cylinder_add(radius=1.1, depth=0.16, vertices=48)
add(bpy.context.object, GOLD)
bpy.ops.mesh.primitive_torus_add(major_radius=1.0, minor_radius=0.08)
add(bpy.context.object, GOLD)
def build_gem():
bpy.ops.mesh.primitive_cone_add(radius1=1.0, depth=1.2, vertices=8)
add(bpy.context.object, CYAN)
bpy.ops.mesh.primitive_cone_add(radius1=1.0, depth=0.7, vertices=8)
bpy.context.object.rotation_euler = (3.14159, 0, 0)
bpy.context.object.location = (0, 0, 0.95)
add(bpy.context.object, CYAN)
def build_ring():
bpy.ops.mesh.primitive_torus_add(major_radius=0.85, minor_radius=0.16)
add(bpy.context.object, GOLD)
bpy.ops.mesh.primitive_ico_sphere_add(radius=0.34, subdivisions=1)
bpy.context.object.location = (0, 0, 0.95)
add(bpy.context.object, CYAN)
def build_bone():
bpy.ops.mesh.primitive_cylinder_add(radius=0.22, depth=1.8, vertices=16)
add(bpy.context.object, BONE)
for z in (0.9, -0.9):
for x in (-0.22, 0.22):
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.34, segments=16)
bpy.context.object.location = (x, 0, z)
add(bpy.context.object, BONE)
def build_bomb():
bpy.ops.mesh.primitive_uv_sphere_add(radius=1.0, segments=32)
add(bpy.context.object, (0.16, 0.15, 0.17, 1.0))
bpy.ops.mesh.primitive_cylinder_add(radius=0.16, depth=0.6, vertices=12)
bpy.context.object.location = (0.25, 0, 1.15)
add(bpy.context.object, RED)
BUILDERS = {{
"coin": build_coin,
"gem": build_gem,
"ring": build_ring,
"bone": build_bone,
"bomb": build_bomb,
}}
BUILDERS[SUBJECT]()
bpy.ops.object.camera_add(location=(0, -4.2, 2.6))
camera = bpy.context.object
camera.rotation_euler = mathutils.Euler((1.02, 0, 0))
camera.data.type = "ORTHO"
camera.data.ortho_scale = 3.0
scene.camera = camera
bpy.ops.object.light_add(type="SUN", location=(2.5, -3.0, 5.0))
bpy.context.object.data.energy = 4.0
bpy.ops.object.light_add(type="AREA", location=(-3.0, -2.0, 2.0))
bpy.context.object.data.energy = 90.0
bpy.ops.render.render(write_still=True)
"""
def scene_script(size: int = RENDER_SIZE) -> str:
"""Return the Blender scene script for a given render size.
Args:
size: Square render edge length in pixels.
Returns:
Python source to hand to ``blender --python``.
"""
return SCENE_TEMPLATE.format(size=size)
# Subjects with a 3D build. Deliberately a subset: a mesh for "scroll" or
# "meat" would be modelling work, not a route comparison, and the cell's
# purpose is to test whether render->finish clears the gates at all.
SUBJECTS_3D: Final = ("coin", "gem", "ring", "bone", "bomb")