239 lines
9.9 KiB
Python
239 lines
9.9 KiB
Python
"""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.
|
||
"""
|
||
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))
|