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>
140 lines
4.3 KiB
Python
140 lines
4.3 KiB
Python
"""Headless-Test fuer den Stylized Rock Generator.
|
|
|
|
Aufruf (aus dem Repo-Root oder beliebig):
|
|
|
|
blender --background --python tests/test_rock_gen.py
|
|
|
|
Exit-Code 0 = alle Checks bestanden, 1 = mindestens ein Check fehlgeschlagen.
|
|
|
|
Der Test:
|
|
* registriert das Addon
|
|
* erzeugt einen Batch von 3 Rocks mit Standard-Settings (Subsurf reduziert
|
|
fuer Speed)
|
|
* prueft je Rock: keine losen Vertices, manifold, Normalen nach aussen
|
|
(signed volume > 0), UV0 + UV1 vorhanden
|
|
* gibt am Ende eine Zusammenfassung (Face-/Vertex-Count) aus
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import importlib.util
|
|
|
|
import bpy
|
|
import bmesh
|
|
|
|
|
|
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
ADDON_FILE = os.path.normpath(os.path.join(THIS_DIR, "..", "stylized_rock_generator.py"))
|
|
|
|
BATCH = 3
|
|
|
|
|
|
def load_addon():
|
|
spec = importlib.util.spec_from_file_location("stylized_rock_generator", ADDON_FILE)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
sys.modules["stylized_rock_generator"] = mod
|
|
spec.loader.exec_module(mod)
|
|
try:
|
|
mod.unregister()
|
|
except Exception:
|
|
pass
|
|
mod.register()
|
|
return mod
|
|
|
|
|
|
def mesh_stats(obj):
|
|
bm = bmesh.new()
|
|
bm.from_mesh(obj.data)
|
|
bm.normal_update()
|
|
loose = sum(1 for v in bm.verts if not v.link_faces)
|
|
nonmanifold = sum(1 for e in bm.edges if not e.is_manifold)
|
|
volume = bm.calc_volume(signed=True)
|
|
stats = dict(
|
|
name=obj.name,
|
|
verts=len(bm.verts),
|
|
faces=len(bm.faces),
|
|
loose=loose,
|
|
nonmanifold=nonmanifold,
|
|
volume=volume,
|
|
uv_layers=[layer.name for layer in obj.data.uv_layers],
|
|
)
|
|
bm.free()
|
|
return stats
|
|
|
|
|
|
def main():
|
|
failures = []
|
|
|
|
# 1. Saubere, leere Szene
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
|
|
# 2. Addon registrieren (Scene-Property lebt am Typ -> nach Reset ok)
|
|
load_addon()
|
|
|
|
settings = bpy.context.scene.rock_gen_settings
|
|
settings.batch_count = BATCH
|
|
settings.subsurf_levels = 4 # Test-Speed
|
|
settings.use_modal = False # synchron -> deterministisch
|
|
settings.generate_uv0 = True
|
|
settings.generate_lightmap_uv = True
|
|
settings.generate_lods = False
|
|
settings.assign_material = False
|
|
|
|
# 3. Batch erzeugen
|
|
# Vorher merken, was schon da ist: das Benennungsschema kommt aus den
|
|
# Einstellungen (Fels_A_M_01 ...), ein fest verdrahtetes Praefix wie "Rock_"
|
|
# geht schief, sobald jemand die Benennung aendert. Differenz statt Ratename.
|
|
before = set(bpy.context.scene.objects)
|
|
result = bpy.ops.object.generate_rocks_batch()
|
|
if 'FINISHED' not in result:
|
|
failures.append("Operator lieferte nicht {'FINISHED'}: %s" % (result,))
|
|
|
|
rocks = [o for o in bpy.context.scene.objects
|
|
if o.type == 'MESH' and o not in before]
|
|
|
|
if len(rocks) != BATCH:
|
|
failures.append("Erwartet %d Rocks, gefunden %d" % (BATCH, len(rocks)))
|
|
|
|
# 4. Validierung + Zusammenfassung
|
|
print("\n" + "=" * 64)
|
|
print(" Rock-Generator Test - Zusammenfassung")
|
|
print("=" * 64)
|
|
print(" %-16s %7s %7s %6s %6s %9s %s" %
|
|
("Name", "Verts", "Faces", "Loose", "NonMf", "Volume", "UVs"))
|
|
print("-" * 64)
|
|
|
|
for obj in sorted(rocks, key=lambda o: o.name):
|
|
st = mesh_stats(obj)
|
|
print(" %-16s %7d %7d %6d %6d %9.3f %s" % (
|
|
st["name"], st["verts"], st["faces"], st["loose"],
|
|
st["nonmanifold"], st["volume"], ",".join(st["uv_layers"])))
|
|
|
|
if st["faces"] == 0:
|
|
failures.append("%s: keine Faces" % st["name"])
|
|
if st["loose"] != 0:
|
|
failures.append("%s: %d lose Vertices" % (st["name"], st["loose"]))
|
|
if st["nonmanifold"] != 0:
|
|
failures.append("%s: %d non-manifold Kanten" % (st["name"], st["nonmanifold"]))
|
|
if st["volume"] <= 0.0:
|
|
failures.append("%s: signed volume <= 0 (Normalen invertiert?)" % st["name"])
|
|
if "Lightmap" not in st["uv_layers"]:
|
|
failures.append("%s: kein Lightmap-UV (UV1)" % st["name"])
|
|
if len(st["uv_layers"]) < 2:
|
|
failures.append("%s: weniger als 2 UV-Layer" % st["name"])
|
|
|
|
print("=" * 64)
|
|
|
|
if failures:
|
|
print("\nFEHLGESCHLAGEN (%d):" % len(failures))
|
|
for f in failures:
|
|
print(" - " + f)
|
|
print("")
|
|
sys.exit(1)
|
|
|
|
print("\nAlle Checks bestanden.\n")
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|