Files

241 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests de Slice Wrap en arrière-plan (sans installer l'add-on).
"C:\\Program Files\\Blender Foundation\\Blender 5.2\\blender.exe" -b -P tests/run_tests.py -- [dossier_sortie] [--head]
- rapide (~40 s) : sphère trouée et bruitée → nom / numérotation / collection / Regenerate, Envelope fermée,
Wrap + Freeze (réparation), Despike (global, filtré, refus groupe absent, Protect Detail), Analyze.
- --head : chaîne complète sur la tête brute Pixal3D (11,8 M faces, ~2 min) + rendus de contrôle.
Termine par « RESULTAT : TOUT OK » ou la liste des échecs ; code de retour 0 si tout est bon, 1 sinon
(convention de la chaîne a2m : `tests/run_tests.py`, lancé avant chaque release).
"""
import importlib
import math
import os
import sys
import time
import bmesh
import bpy
import numpy as np
from mathutils import Vector
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.dirname(ROOT))
for k in [k for k in sys.modules if k.startswith(os.path.basename(ROOT))]:
del sys.modules[k]
mod = importlib.import_module(os.path.basename(ROOT))
ARGS = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
OUT = next((a for a in ARGS if not a.startswith("--")), ROOT)
HEAD = r"C:\Users\a-mma\Desktop\CharacterDesign\Work3D\refs\head\3D\head_raw_pixal3d_front_s56.glb"
bpy.ops.wm.read_factory_settings(use_empty=True)
mod.register()
sc = bpy.context.scene
s = sc.slice_wrap
fails = []
def check(name, cond, msg=""):
print(("OK " if cond else "FAIL ") + f"{name} : {msg}")
if not cond:
fails.append(name)
def sel(o):
for x in bpy.context.selected_objects:
x.select_set(False)
o.hide_set(False)
o.select_set(True)
bpy.context.view_layer.objects.active = o
def topo(o):
bm = bmesh.new()
bm.from_mesh(o.data)
seen, pieces = set(), 0
for v in bm.verts:
if v in seen:
continue
pieces += 1
st = [v]
seen.add(v)
while st:
x = st.pop()
for e in x.link_edges:
w = e.other_vert(x)
if w not in seen:
seen.add(w)
st.append(w)
r = dict(faces=len(bm.faces), boundary=sum(e.is_boundary for e in bm.edges),
nonmanifold=sum(not e.is_manifold for e in bm.edges), pieces=pieces)
bm.free()
return r
def safe(op, **kw):
try:
return op(**kw)
except RuntimeError:
return {"CANCELLED"}
# ----------------------------------------------------------------------------- source : sphère trouée + bruit
bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=5, radius=1.0)
src = bpy.context.active_object
src.name = "Tete"
bm = bmesh.new()
bm.from_mesh(src.data)
bmesh.ops.delete(bm, geom=[f for f in bm.faces if f.calc_center_median().z > 0.85
or abs(f.calc_center_median().x - 0.6) < 0.05], context="FACES") # trous
rng = np.random.default_rng(0)
for v in bm.verts:
v.co *= 1.0 + 0.004 * rng.standard_normal() # bruit
bm.to_mesh(src.data)
bm.free()
check("source trouée", topo(src)["boundary"] > 0, str(topo(src)))
# ----------------------------------------------------------------------------- nom / collection / Regenerate
s.pitch_rel = 1.5 # voxel grossier : test rapide (le défaut 0,6 % → ~100 k faces ×16)
pre = bpy.data.collections.new("SliceWrapCollection")
sc.collection.children.link(pre)
keep = bpy.data.objects.new("DejaLa", None)
pre.objects.link(keep)
sel(src)
t = time.time()
r1 = bpy.ops.slice_wrap.envelope()
e1 = bpy.context.active_object
check("envelope", r1 == {"FINISHED"}, f"{e1.name} en {time.time() - t:.1f} s")
check("nom suffixe", e1.name == "Tete_slicewrapped", e1.name)
tp = topo(e1)
check("enveloppe fermée", tp["boundary"] == 0 and tp["nonmanifold"] == 0 and tp["pieces"] == 1, str(tp))
check("source cachée", src.hide_get())
sel(src)
bpy.ops.slice_wrap.envelope()
e2 = bpy.context.active_object
check("numérotation", e2.name == "Tete_slicewrapped.001" and e1.name in bpy.data.objects, e2.name)
check("collection réutilisée", [c.name for c in e1.users_collection] == ["SliceWrapCollection"]
and "DejaLa" in pre.objects and len([c for c in bpy.data.collections if c.name.startswith("SliceWrapCollection")]) == 1)
nb = len(e2.data.polygons)
s.pitch_rel = 1.0
sel(e2)
r = bpy.ops.slice_wrap.regenerate()
check("regenerate sur place", r == {"FINISHED"} and bpy.context.active_object == e2 and len(e2.data.polygons) != nb,
f"{nb} → {len(e2.data.polygons)} faces")
s.pitch_rel = 1.5
s.use_collection = False
s.name_suffix = ""
sel(src)
bpy.ops.slice_wrap.envelope()
e4 = bpy.context.active_object
check("suffixe vide sans écraser la source", e4.name == "Tete.001" and src.name == "Tete"
and [c.name for c in e4.users_collection] == ["Scene Collection"], e4.name)
s.use_collection = True
s.name_suffix = "_slicewrapped"
# ----------------------------------------------------------------------------- objet sélectionné / annulation
check("plus de réglage Target", "target" not in s.bl_rna.properties)
check("opérateurs longs : propriété modal", "modal_run" in bpy.ops.slice_wrap.run.get_rna_type().properties)
sel(src)
src.select_set(False)
check("rien de sélectionné → Slice Wrap grisé", not bpy.ops.slice_wrap.run.poll())
sel(src)
check("objet sélectionné → Slice Wrap actif", bpy.ops.slice_wrap.run.poll())
n_obj, n_me, n_col = len(bpy.data.objects), len(bpy.data.meshes), len(bpy.data.collections)
g = mod.step_run(src) # chaîne complète pour compter les tranches, puis on la supprime
total = 0
try:
while True:
next(g)
total += 1
except StopIteration as e:
mod._remove_object(e.value[0])
sel(src)
check("chaîne découpée en tranches", total > 20, f"{total} tranches")
for stop in (3, total // 2, total): # annulation (Échap) : enveloppe, projection, juste avant le Reshape final
g = mod.step_run(src)
steps = 0
try:
for _ in range(stop):
next(g)
steps += 1
except StopIteration:
pass
g.close()
check(f"annulation après {steps} tranches : rien de créé",
(len(bpy.data.objects), len(bpy.data.meshes)) == (n_obj, n_me) and len(bpy.data.collections) == n_col,
f"objets {len(bpy.data.objects)}/{n_obj}, maillages {len(bpy.data.meshes)}/{n_me}")
sel(src)
# ----------------------------------------------------------------------------- Wrap + Freeze
sel(e1)
r = bpy.ops.slice_wrap.wrap_setup()
check("wrap setup", r == {"FINISHED"} and [m.type for m in e1.modifiers] == ["MULTIRES", "SHRINKWRAP"],
str([m.type for m in e1.modifiers]))
r = bpy.ops.slice_wrap.freeze()
check("freeze", r == {"FINISHED"} and [m.type for m in e1.modifiers] == ["MULTIRES"], str([m.type for m in e1.modifiers]))
check("regenerate refusé si wrappé", safe(bpy.ops.slice_wrap.regenerate) == {"CANCELLED"})
# ----------------------------------------------------------------------------- Despike
co0 = [v.co.copy() for v in e1.data.vertices]
g = mod.step_despike(e1)
next(g), next(g), next(g)
g.close()
check("despike annulé : maillage intact", [m.type for m in e1.modifiers] == ["MULTIRES"]
and all((v.co - c).length == 0 for v, c in zip(e1.data.vertices, co0))
and not any(o.name.endswith("_tmp") for o in bpy.data.objects))
vg = e1.vertex_groups.new(name="Zone")
vg.add([v.index for v in e1.data.vertices if v.co.z < 0], 1.0, "REPLACE")
e1.slice_wrap_despike_groups.add().group = "Zone"
r = bpy.ops.slice_wrap.despike()
check("despike filtré", r == {"FINISHED"})
e1.slice_wrap_despike_groups.clear()
e1.slice_wrap_despike_groups.add().group = "Absent"
check("despike refus groupe absent", safe(bpy.ops.slice_wrap.despike) == {"CANCELLED"})
e1.slice_wrap_despike_groups.clear()
s.despike_protect = True
check("despike global + Protect Detail", bpy.ops.slice_wrap.despike() == {"FINISHED"})
s.despike_protect = False
# ----------------------------------------------------------------------------- Analyze
r = bpy.ops.slice_wrap.analyze()
me = e1.data
check("analyze", r == {"FINISHED"} and {"sw_detail", "sw_shape", "sw_flow"} <= {a.name for a in me.attributes}
and {"SW_Detail_Color", "SW_Shape_Color"} <= {c.name for c in me.color_attributes}
and {"SW_Detail", "SW_Fold", "SW_Bulge"} <= {g.name for g in e1.vertex_groups})
check("pas de collision couleur / groupe", not ({c.name for c in me.color_attributes} & {g.name for g in e1.vertex_groups}))
r = bpy.ops.slice_wrap.show(mode="SW_Detail_Color")
check("show", r == {"FINISHED"} and me.color_attributes.render_color_index == me.color_attributes.active_color_index)
# ----------------------------------------------------------------------------- tête réelle (optionnel)
if "--head" in ARGS and os.path.isfile(HEAD):
bpy.ops.import_scene.gltf(filepath=HEAD)
coque = bpy.context.selected_objects[0]
coque.name = "Coque"
sel(coque)
t = time.time()
r = bpy.ops.slice_wrap.run()
env = bpy.context.active_object
check("tête : chaîne complète", r == {"FINISHED"} and [m.type for m in env.modifiers] == ["MULTIRES"],
f"{env.name}, base {len(env.data.polygons)} faces, {time.time() - t:.0f} s")
bpy.ops.slice_wrap.analyze()
sc.render.engine = "BLENDER_WORKBENCH"
sc.render.resolution_x = sc.render.resolution_y = 800
sc.display.shading.light = "MATCAP"
cam = bpy.data.objects.new("CAM", bpy.data.cameras.new("CAM"))
sc.collection.objects.link(cam)
sc.camera = cam
cam.data.type = "ORTHO"
for o in sc.objects:
if o.type == "MESH":
o.hide_render = o != env
for tag, loc, rot, scl in (("face", (0, -2, 0), (90, 0, 0), 0.75), ("cote", (2, 0.05, 0.08), (90, 0, 90), 0.45)):
cam.location = Vector(loc)
cam.rotation_euler = tuple(math.radians(a) for a in rot)
cam.data.ortho_scale = scl
sc.render.filepath = os.path.join(OUT, f"slice_wrap_head_{tag}.png")
bpy.ops.render.render(write_still=True)
print("IMAGE", sc.render.filepath)
mod.unregister()
print("RESULTAT :", "TOUT OK" if not fails else "ECHECS " + ", ".join(fails))
sys.exit(1 if fails else 0) # code de retour lu par « a2m validate --tests » et « a2m release »