Panel-Umbau
-----------
Jedes Addon hatte seine Einstellungen als Kette dauerhaft offener Boxen - beim
Rock Generator 15 Stueck, man musste am ganzen Panel vorbeiscrollen, um an den
Erzeugen-Knopf zu kommen. Jetzt steht oben nur, was man bei jedem Asset anfasst
(Vorlage, Name, grosser Erzeugen-Knopf, Zufall), der Rest liegt in nummerierten,
eingeklappten Unter-Panels in der Reihenfolge des Arbeitsablaufs:
Rock Gen 9 Unter-Panels (Benennung ... Textur-Bake)
Tree Gen 4 (Wachstum, Blattwerk, Zusatz-Ebenen, Export)
Grass Gen 4 (Form, Haltung, Batch, Kanal-Belegung)
Asset Utils 5 (war schon so, dient als Vorlage)
Schalter, die ein ganzes Unter-Panel steuern (Vertex-Farben backen,
Wachstums-Stufen), sitzen jetzt als Haekchen im Panel-Kopf - Zustand sichtbar
ohne aufzuklappen.
Gemessen statt geglaubt: test_panels.py registriert alle vier Addons, laesst
jedes Panel zeichnen, prueft Tab-Zugehoerigkeit und Auf-/Zugeklappt-Absicht,
dass jeder angebotene Operator existiert - und vor allem, dass JEDE definierte
Property irgendwo erreichbar bleibt (Zweige wie base_shape='BOX' werden dabei
durchgeschaltet). Ergebnis: 0 von 122 Properties beim Umbau verloren.
Test-Suite
----------
Beim Gegenpruefen kam heraus, dass fuenf Tests still veraltet waren und eine
echte Regression nicht gemeldet haetten:
test_rock_gen suchte Objekte mit Praefix "Rock_" - die heissen seit dem
Benennungssystem "Fels_A_M_01". Fand 0 von 3.
test_base_shapes loeschte am Schleifenanfang ALLE Objekte, auch die eigenen
Kopien aus vorigen Durchlaeufen -> StructRNA removed.
test_ecogame_tree erwartete eine feste Namensliste ohne die spaeter
dazugekommenen _Leaf-Objekte.
test_tree_addon dito, ausserdem Zip-Version 1.22.0 fest verdrahtet.
test_leaves Zip-Version 1.23.0 fest verdrahtet.
Drei Muster vermieden statt nur die Symptome geflickt: neue Objekte ueber die
Differenz vorher/nachher finden statt ueber ein Namens-Praefix, Zips per glob
statt mit fester Version, Zusatz-Ebenen beim Zaehlen herausfiltern.
Neu: run_tests.ps1 faehrt die komplette Suite mit einem Befehl (-Filter fuer
einzelne). Genau das fehlte - deshalb war das Veralten nicht aufgefallen.
13 von 13 Tests bestehen.
Nebenbei
--------
build.ps1 ruft server-generate jetzt mit --html auf (browsbare index.html fuer
statisches Hosting). Nachgemessen und im README festgehalten: zwei Zips
derselben Extension-ID sind KEIN Versions-Archiv - server-generate warnt
("archive found with duplicates ... conflicting blender versions"), mehrere
Versionen einer ID gehen nur bei verschiedenen Blender-Bereichen.
Versionen: Rock 2.13.0, Tree 1.24.0, Grass 1.1.0, Asset Utils 1.1.0
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
93 lines
3.9 KiB
Python
93 lines
3.9 KiB
Python
"""Prueft das gebaute Tree-Addon: registrieren, Operator ausfuehren, Ergebnis messen."""
|
|
import bpy, sys, zipfile, os, tempfile, importlib.util, traceback
|
|
|
|
# Version NICHT fest verdrahten - sonst schlaegt der Test bei jedem Bump fehl,
|
|
# obwohl am Addon nichts kaputt ist.
|
|
import glob
|
|
_d = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "dist")
|
|
_z = sorted(glob.glob(os.path.join(_d, "stylized_tree_generator-*.zip")))
|
|
if not _z:
|
|
print("ERGEBNIS: FEHLER: kein gebautes Tree-Zip in dist/ (erst build.ps1 laufen lassen)")
|
|
sys.exit(1)
|
|
ZIP = _z[-1]
|
|
fails = []
|
|
|
|
tmp = tempfile.mkdtemp()
|
|
with zipfile.ZipFile(ZIP) as z:
|
|
z.extractall(tmp)
|
|
init = None
|
|
for root, _, files in os.walk(tmp):
|
|
if "__init__.py" in files:
|
|
init = os.path.join(root, "__init__.py"); break
|
|
if not init:
|
|
print("FAIL: __init__.py nicht im Zip"); sys.exit(1)
|
|
print("Zip-Inhalt ok:", os.path.relpath(init, tmp))
|
|
|
|
spec = importlib.util.spec_from_file_location("stylized_tree_generator", init)
|
|
m = importlib.util.module_from_spec(spec)
|
|
sys.modules["stylized_tree_generator"] = m
|
|
try:
|
|
spec.loader.exec_module(m)
|
|
m.register()
|
|
print("registriert ok")
|
|
except Exception:
|
|
traceback.print_exc(); print("FAIL: register"); sys.exit(1)
|
|
|
|
s = bpy.context.scene.tree_gen_settings
|
|
|
|
# 1) einzelner Baum je Preset
|
|
for p in ("baum", "palme", "busch", "kaktus"):
|
|
for o in list(bpy.data.objects):
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
|
s.preset = p; s.use_growth = False; s.seed = 3
|
|
r = bpy.ops.object.treegen_create()
|
|
obs = [o for o in bpy.data.objects if o.type == 'MESH']
|
|
bpy.context.view_layer.update()
|
|
dg = bpy.context.evaluated_depsgraph_get()
|
|
v = len(obs[0].evaluated_get(dg).data.vertices) if obs else 0
|
|
ok = ('FINISHED' in r) and v > 0
|
|
print("preset %-7s -> %s verts=%d" % (p, "ok" if ok else "FAIL", v))
|
|
if not ok: fails.append(p)
|
|
|
|
# 2) Wachstums-Stufen
|
|
for o in list(bpy.data.objects):
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
|
s.preset = "baum"; s.use_growth = True; s.stages = 4
|
|
r = bpy.ops.object.treegen_create()
|
|
# Nur die Staemme: seit der Blattwerk-Ebene gibt es zu jeder Stufe noch ein
|
|
# "_Leaf"-Objekt. Mit denen zusammen ist die Vertex-Folge nicht mehr aufsteigend.
|
|
obs = sorted([o for o in bpy.data.objects
|
|
if o.type == 'MESH' and not o.name.endswith(("_Leaf", "_Frucht"))],
|
|
key=lambda o: o.name)
|
|
bpy.context.view_layer.update()
|
|
dg = bpy.context.evaluated_depsgraph_get()
|
|
counts = [len(o.evaluated_get(dg).data.vertices) for o in obs]
|
|
growing = all(counts[i] < counts[i+1] for i in range(len(counts)-1))
|
|
print("stufen: %d Objekte verts=%s aufsteigend=%s" % (len(obs), counts, growing))
|
|
if len(obs) != 4 or not growing: fails.append("stages")
|
|
|
|
# 3) Modifier anwenden (Export-Pfad)
|
|
for o in list(bpy.data.objects):
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
|
s.use_growth = False; s.apply_modifier = True
|
|
s.generate_lightmap_uv = True # Default ist AUS (EcoGame: dynamisches Licht)
|
|
bpy.ops.object.treegen_create()
|
|
obs = [o for o in bpy.data.objects if o.type == 'MESH']
|
|
real = len(obs[0].data.vertices) if obs else 0 # echte Mesh-Daten, nicht evaluated
|
|
uvs = [l.name for l in obs[0].data.uv_layers] if obs else []
|
|
print("apply_modifier -> echte Verts=%d mods=%d uvs=%s" % (real, len(obs[0].modifiers) if obs else -1, uvs))
|
|
if real == 0: fails.append("apply")
|
|
if "UVMap" not in uvs: fails.append("uv0")
|
|
if "Lightmap" not in uvs: fails.append("uv1")
|
|
# UV muss world-space sein: V-Bereich ~ Baumhoehe
|
|
if obs and obs[0].data.uv_layers.get("UVMap"):
|
|
d = obs[0].data.uv_layers["UVMap"].data
|
|
vs = [x.uv[1] for x in d]
|
|
h = max(v.co.z for v in obs[0].data.vertices)
|
|
print("UV world-space check: V-max=%.2f Baumhoehe=%.2f" % (max(vs), h))
|
|
if max(vs) < h * 0.5: fails.append("uv_scale")
|
|
|
|
m.unregister()
|
|
print("ERGEBNIS:", "ALLE TESTS BESTANDEN" if not fails else ("FEHLER: " + ", ".join(fails)))
|
|
sys.exit(1 if fails else 0)
|