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>
69 lines
3.2 KiB
Python
69 lines
3.2 KiB
Python
import bpy, glob, sys, zipfile, os, tempfile, importlib.util, math, bmesh
|
|
# Version NICHT fest verdrahten - sonst schlaegt der Test nach jedem
|
|
# Versions-Bump fehl, obwohl am Addon nichts kaputt ist.
|
|
_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)")
|
|
sys.exit(1)
|
|
ZIP = _z[-1]
|
|
tmp=tempfile.mkdtemp()
|
|
with zipfile.ZipFile(ZIP) as z: z.extractall(tmp)
|
|
init=[os.path.join(r,"__init__.py") for r,_,f in os.walk(tmp) if "__init__.py" in f][0]
|
|
spec=importlib.util.spec_from_file_location("stg",init)
|
|
m=importlib.util.module_from_spec(spec); sys.modules["stg"]=m; spec.loader.exec_module(m)
|
|
try: m.unregister()
|
|
except Exception: pass
|
|
m.register()
|
|
fails=[]
|
|
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
|
|
|
|
# Card wie beim User
|
|
me=bpy.data.meshes.new("DiamondCard"); bm=bmesh.new()
|
|
for ang in (0.0, math.pi/3, 2*math.pi/3):
|
|
c,s_=math.cos(ang), math.sin(ang)
|
|
vs=[bm.verts.new(p) for p in [(-c,-s_,-1.41),(c,s_,-1.41),(c,s_,1.41),(-c,-s_,1.41)]]
|
|
bm.faces.new(vs)
|
|
bm.to_mesh(me); bm.free(); me.uv_layers.new(name="UVMap")
|
|
card=bpy.data.objects.new("Diamon Plane", me); bpy.context.collection.objects.link(card)
|
|
|
|
s=bpy.context.scene.tree_gen_settings
|
|
s.preset='eiche'; s.art_name="Eiche"; s.variant=1
|
|
s.leaf_card=card; s.leaf_size=0.22 # Dichte: Default verwenden
|
|
s.make_leaf=True; s.use_growth=False
|
|
r=bpy.ops.object.treegen_create()
|
|
names=sorted(o.name for o in bpy.context.scene.objects if o.type=='MESH')
|
|
print("OBJEKTE:", [n for n in names if n!="Diamon Plane"])
|
|
if "Eiche_01" not in names or "Eiche_01_Leaf" not in names: fails.append("objekte")
|
|
bpy.context.view_layer.update()
|
|
dg=bpy.context.evaluated_depsgraph_get()
|
|
def tris(n):
|
|
ob=bpy.context.scene.objects[n]
|
|
return sum(len(p.vertices)-2 for p in ob.evaluated_get(dg).data.polygons)
|
|
tt,tl=tris("Eiche_01"), tris("Eiche_01_Leaf")
|
|
print("TRIS Stamm=%d Blatt=%d Summe=%d / 2500 %s (Dichte %.0f)" % (tt,tl,tt+tl,"OK" if tt+tl<=2500 else "UEBER", s.leaf_density))
|
|
if tl==0: fails.append("blattwerk_leer")
|
|
if tt+tl>2500: fails.append("budget")
|
|
# Ursprung deckungsgleich
|
|
a=bpy.context.scene.objects["Eiche_01"]; b=bpy.context.scene.objects["Eiche_01_Leaf"]
|
|
d=(a.location-b.location).length
|
|
print("URSPRUNG-Abstand: %.6f" % d)
|
|
if d>1e-6: fails.append("ursprung")
|
|
# Operator auf Auswahl
|
|
for o in bpy.context.selected_objects: o.select_set(False)
|
|
a.select_set(True); bpy.context.view_layer.objects.active=a
|
|
r2=bpy.ops.object.treegen_leaves()
|
|
print("OPERATOR treegen_leaves:", list(r2))
|
|
if 'FINISHED' not in r2: fails.append("operator")
|
|
# Ohne Card -> leeres _Leaf
|
|
for o in list(bpy.data.objects):
|
|
if o.name.startswith("Eiche"): bpy.data.objects.remove(o,do_unlink=True)
|
|
s.leaf_card=None; s.art_name="Birke"; s.preset='birke'
|
|
bpy.ops.object.treegen_create()
|
|
lf=bpy.context.scene.objects.get("Birke_01_Leaf")
|
|
print("ohne Card -> _Leaf vorhanden:", lf is not None, "| leer:", lf and len(lf.data.polygons)==0)
|
|
if lf is None: fails.append("leer_leaf")
|
|
m.unregister()
|
|
print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))
|
|
sys.exit(1 if fails else 0)
|