diff --git a/README.md b/README.md index 9d0110b..3e645d8 100644 --- a/README.md +++ b/README.md @@ -563,6 +563,54 @@ Der Kaktus erzeugt regulaer **0 Cards** — kein Ast ist duenner als `Max Radius und das ist botanisch richtig. Vorher lief das still durch und man stand vor einer leeren `_Leaf`-Ebene; jetzt sagt der Operator es und nennt den Regler. +### Stamm-UCX fuer den Stand-Baum + +Die Stand-Baeume bekommen eine konvexe Kollisionshuelle um den **Stamm**, vom +Fuss bis zum Kronenansatz. Bewusst keine Kronen-Huelle: gegen die liefe der +Spieler, bevor er den Baum ueberhaupt beruehrt. + +Die Trennung kommt aus `ast_id` (0 = Stamm) - dasselbe Attribut wie beim +Ursprung und bei den Faell-Teilen. Kronenansatz ist der tiefste Punkt mit +ast_id > 0, plus ein Zehntel Stammhoehe Luft, weil der tiefste Astpunkt eine +herabgebogene Spitze sein kann. Ohne Aeste (Kaktus-Setzling) ist die ganze +Pflanze Stamm - und genau die soll dann die Kollision sein. + +**Name und Hierarchie sind nicht geraten, sondern vom Fels-Export abgeschaut**, +der in der Spiel-Pipeline schon abgenommen ist: dort exportiert eine LOD-Gruppe +als `UCX_`, und zwar als GESCHWISTER des LodGroup-Empty, nicht +als Kind - Unreal liest jedes Kind der Gruppe als LOD-Stufe. Nachgemessen im +Fels-FBX, bevor eine Zeile Baum-Code entstand. + +Ergebnis im reimportierten Baum-FBX: + +``` +Eiche_01_LOD0..3 MESH parent = Eiche_01_LODGroup +Eiche_01_LODGroup EMPTY parent = - +UCX_Eiche_01_LODGroup MESH parent = - 12 Verts +``` + +Zwei Dinge, die erst die Messung gezeigt hat: + +* Die Huelle rutschte **4-6 cm unter den Boden**. Das Vereinfachen auf 12 Verts + schneidet Ecken ab, und das anschliessende Aufblasen (damit wieder alles + hineinpasst) skaliert um die Huellenmitte - dabei wandert die Unterseite nach + unten. Im Spiel waere das eine Kollision, die aus dem Terrain herausragt. + Jetzt wird der Boden auf Z = 0 geklemmt. +* Der erste Anlauf des Tests rechnete die FBX-Skala aus der **Gesamthoehe** + zurueck - die Blatt-Cards ragen aber ueber die Rinde hinaus, die Skala kam zu + gross heraus und die Huelle sah zu klein aus. Jetzt kommt der Massstab aus + Material-Slot 0. Ein Massstab, der etwas anderes misst als die Referenz, + verschiebt jede Aussage. + +`tests/test_stamm_ucx.py` prueft an vier Faellen (gerader Stamm, krummer Stamm +mit Bend 1.5, Obstbaum, und einmal ohne LODs): genau eine Huelle, richtiger +Name, kein Elternteil, 8-24 Verts, Radius zwischen Stammradius und dem +2.5-fachen davon, Fuss auf Z = 0, Hoehe nicht ueber den Kronenansatz hinaus. +Gegenprobe ohne die Boden-Klemmung: vier Fehler. + +Drei bestehende Tests zaehlten die Huelle als Render-Mesh mit und schlugen zu +Recht an - sie filtern jetzt `UCX_`. + ### Vom Trichter ueber die Haube zur Kugel Zielbild ist der **Lollipop** der stilisierten Vektor-Illustration: kurzer diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index 192fc24..9008dab 100644 --- a/blender_manifest_tree.toml +++ b/blender_manifest_tree.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "stylized_tree_generator" -version = "1.41.0" +version = "1.42.0" name = "Stylized Tree Generator" tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen" maintainer = "D4rkst3r" diff --git a/dist/index.html b/dist/index.html index 81e469e..dc287cb 100644 --- a/dist/index.html +++ b/dist/index.html @@ -50,17 +50,17 @@ 34.1KB - stylized_tree_generator-1.41.0 + stylized_tree_generator-1.42.0 Stylized Tree Generator Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen link 4.2.0 - ~ all all - 46.3KB + 47.6KB -

Built 2026-08-06, 13:04

+

Built 2026-08-07, 04:18

diff --git a/dist/index.json b/dist/index.json index 24ec862..1ff78fc 100644 --- a/dist/index.json +++ b/dist/index.json @@ -82,7 +82,7 @@ "id": "stylized_tree_generator", "name": "Stylized Tree Generator", "tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen", - "version": "1.41.0", + "version": "1.42.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -98,9 +98,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.41.0.zip", - "archive_size": 47402, - "archive_hash": "sha256:c5d5c612fd723af6c1e226ce0ca42cf6d28298641ec5af77bb12223ebd6935c6" + "archive_url": "./stylized_tree_generator-1.42.0.zip", + "archive_size": 48711, + "archive_hash": "sha256:e1b5eb8cb83ecabb60d24ce9c6be75be4c3d168aee86b9a3baa2856bdf440e80" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.41.0.zip b/dist/stylized_tree_generator-1.41.0.zip deleted file mode 100644 index a5bde76..0000000 Binary files a/dist/stylized_tree_generator-1.41.0.zip and /dev/null differ diff --git a/dist/stylized_tree_generator-1.42.0.zip b/dist/stylized_tree_generator-1.42.0.zip new file mode 100644 index 0000000..23215e2 Binary files /dev/null and b/dist/stylized_tree_generator-1.42.0.zip differ diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index 266490e..c1858f5 100644 --- a/stylized_tree_generator.py +++ b/stylized_tree_generator.py @@ -1,7 +1,7 @@ bl_info = { "name": "Stylized Tree Generator", "author": "D4rkst3r", - "version": (1, 41, 0), + "version": (1, 42, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -2177,17 +2177,25 @@ def _merge_for_export(context, tree_ob, leaf_ob, name, bark_decimate=1.0): # Faell-Teile: Stammsegmente und Aeste als eigene Physik-Assets # --------------------------------------------------------------------------- -def _tree_ucx(quell_objs, name, max_verts=32): +def _tree_ucx(quell_objs, name, max_verts=32, punkte=None): """Konvexe Huelle als `UCX_` - gleiches Muster wie bei den Felsen. Nur die PUNKTE einlesen: mit `from_mesh` kaemen die Originalflaechen mit und die Huelle laege obendrauf statt darum herum. + + `punkte` uebergeht `quell_objs` und huellt genau die gegebenen Koordinaten - + dafuer, dass die Stand-Baum-Huelle nur den STAMM umschliesst und nicht die + Krone. """ bm = bmesh.new() - for o in quell_objs: - if o.type == 'MESH': - for v in o.data.vertices: - bm.verts.new(o.matrix_world @ v.co) + if punkte is not None: + for co in punkte: + bm.verts.new(co) + else: + for o in quell_objs: + if o.type == 'MESH': + for v in o.data.vertices: + bm.verts.new(o.matrix_world @ v.co) if len(bm.verts) < 4: bm.free() return None @@ -2249,6 +2257,49 @@ def _tree_ucx(quell_objs, name, max_verts=32): return ob +def _stamm_punkte(tree_obj, matrix): + """Stamm-Koordinaten vom Fuss bis zum KRONENANSATZ, in Weltkoordinaten. + + Fuer die Kollision des Stand-Baums. Bewusst nur der Stamm: eine Huelle um + die ganze Krone waere ein Zylinder von mehreren Metern Durchmesser, gegen + den der Spieler laeuft, bevor er den Baum ueberhaupt beruehrt. + + Die Trennung kommt aus `ast_id` (0 = Stamm) - dasselbe Attribut wie beim + Ursprung und bei den Faell-Teilen. Der Kronenansatz ist der tiefste Punkt + mit ast_id > 0; oberhalb davon steckt der Stamm ohnehin in der Krone. + + Ohne Aeste (Kaktus-Setzling: `Branch Count 0` schaltet den Ast-Zweig des + Graphen ab, dann gibt es gar kein ast_id) ist die ganze Pflanze Stamm - und + genau die soll dann auch die Kollision sein. + """ + dg = bpy.context.evaluated_depsgraph_get() + ev = tree_obj.evaluated_get(dg) + me = ev.to_mesh() + attr = me.attributes.get("ast_id") + hat_id = (attr is not None and attr.domain == 'POINT' + and len(attr.data) == len(me.vertices)) + stamm, aeste = [], [] + for i, v in enumerate(me.vertices): + co = matrix @ v.co + if hat_id and attr.data[i].value > 0: + aeste.append(co) + else: + stamm.append(co) + ev.to_mesh_clear() + if not stamm: + return [] + if aeste: + # Etwas Luft ueber den Ansatz: der tiefste Astpunkt kann eine + # herabgebogene Spitze sein, dann waere die Huelle zu kurz. Ein Zehntel + # der Stammhoehe reicht und bleibt weit unter der Krone. + zs = [c.z for c in stamm] + grenze = min(aeste, key=lambda c: c.z).z + (max(zs) - min(zs)) * 0.1 + gekuerzt = [c for c in stamm if c.z <= grenze] + if len(gekuerzt) >= 8: + return gekuerzt + return stamm + + def _mass_center(me): """Massenmittelpunkt als flaechengewichteter Schwerpunkt der Faces. @@ -2485,6 +2536,17 @@ class TreeGenSettings(PropertyGroup): name="_Frucht-Ebene anlegen", default=False, description="Zusaetzlich _Frucht (gleicher Ursprung) fuer die Frucht-Ebene", ) + stand_ucx: BoolProperty( + name="UCX-Stammhuelle", default=True, + description=("Legt dem spielfertigen Baum eine konvexe Huelle um den " + "STAMM (Fuss bis Kronenansatz) ins FBX. Bewusst keine " + "Kronen-Huelle: gegen die liefe der Spieler, bevor er den " + "Baum beruehrt"), + ) + stand_ucx_verts: IntProperty( + name="Stammhuelle max. Verts", default=12, min=8, max=32, + description="Ein Stamm braucht keine feine Huelle - 8-12 reichen", + ) piece_trunk_segments: IntProperty( name="Stamm-Segmente", default=3, min=2, max=3, description="In wie viele Stuecke der Stamm beim Faellen zerfaellt", @@ -2790,6 +2852,33 @@ class TREEGEN_OT_export_game(Operator): weitere.matrix_parent_inverse = Matrix.Identity(4) weitere.data.transform(versatz) + # UCX-Stammhuelle. Die Punkte kommen aus dem QUELLBAUM, weil nur + # der noch `ast_id` traegt - die Export-Kopie ist verschmolzen + # und bei LOD1-3 zusaetzlich dezimiert. Sie durchlaufen dieselbe + # Kette wie die Stufen: eigene Weltmatrix, dann die gemeinsame + # Zentrierung `versatz`. + stamm_ucx_name = (basis_name + "_LODGroup") if s.generate_lods \ + else basis_name + hull = None + if s.stand_ucx: + pts = _stamm_punkte(baum, baum.matrix_world) + pts = [versatz @ c for c in pts] + hull = _tree_ucx(None, stamm_ucx_name, + max_verts=s.stand_ucx_verts, punkte=pts) + if hull is not None: + # Der Boden der Huelle MUSS auf Z = 0 liegen. Das + # Vereinfachen schneidet Ecken ab, und das anschliessende + # Aufblasen (damit wieder alles hineinpasst) skaliert um + # die Huellenmitte - dabei rutschte die Unterseite + # gemessen 4-6 cm unter den Boden. Im Spiel waere das + # eine Kollision, die aus dem Terrain herausragt. + for v in hull.data.vertices: + if v.co.z < 0.0: + v.co.z = 0.0 + aufraeumen.append(hull.name) + print("[TreeGen] %s: Stamm-UCX mit %d Verts" + % (hull.name, len(hull.data.vertices))) + _deselect(context) if s.generate_lods: gruppe = bpy.data.objects.new(basis_name + "_LODGroup", None) @@ -2807,6 +2896,11 @@ class TREEGEN_OT_export_game(Operator): else: lods[0].select_set(True) context.view_layer.objects.active = lods[0] + # Die Huelle wird MITEXPORTIERT, aber NICHT unter das + # LodGroup-Empty gehaengt: Unreal liest jedes Kind der Gruppe + # als LOD-Stufe. Geschwister, so wie es der Fels-Export macht. + if hull is not None: + hull.select_set(True) bpy.ops.export_scene.fbx( filepath=ziel, @@ -3339,6 +3433,10 @@ class VIEW3D_PT_tree_export(_TreeSub): layout.separator() layout.prop(s, "sphere_normals") layout.prop(s, "generate_lods") + layout.prop(s, "stand_ucx") + row = layout.row() + row.active = s.stand_ucx + row.prop(s, "stand_ucx_verts") layout.prop(s, "export_dir") layout.operator("treegen.export_game", icon='EXPORT') diff --git a/tests/test_baum_export.py b/tests/test_baum_export.py index cabf858..fd31dfa 100644 --- a/tests/test_baum_export.py +++ b/tests/test_baum_export.py @@ -94,7 +94,9 @@ if not dateien: bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.fbx(filepath=os.path.join(OUT, dateien[0])) -meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH'] +# Die UCX-Stammhuelle ist KEIN Render-Mesh - sie gehoert nicht in diese Zaehlung. +meshes = [o for o in bpy.context.scene.objects + if o.type == 'MESH' and not o.name.startswith("UCX_")] print("") print("FBX %s: %d Mesh(es)" % (dateien[0], len(meshes))) if len(meshes) != 1: diff --git a/tests/test_baum_lod.py b/tests/test_baum_lod.py index 03cbc88..83275eb 100644 --- a/tests/test_baum_lod.py +++ b/tests/test_baum_lod.py @@ -96,7 +96,9 @@ if not lodfile: bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.fbx(filepath=os.path.join(OUT, lodfile[0])) -meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH'] +# Die UCX-Stammhuelle ist KEIN Render-Mesh - sie gehoert nicht in diese Zaehlung. +meshes = [o for o in bpy.context.scene.objects + if o.type == 'MESH' and not o.name.startswith("UCX_")] empties = [o for o in bpy.context.scene.objects if o.type == 'EMPTY'] stufen = sorted(meshes, key=lambda o: o.name) print("") diff --git a/tests/test_stamm_ucx.py b/tests/test_stamm_ucx.py new file mode 100644 index 0000000..a82cf60 --- /dev/null +++ b/tests/test_stamm_ucx.py @@ -0,0 +1,200 @@ +"""Stamm-UCX fuer den spielfertigen Stand-Baum. + +Die Stand-Baeume brauchen Kollision. Gefordert ist EIN konvexer Stamm-Zylinder +vom Fuss bis zum Kronenansatz - KEINE Kronen-Huelle: gegen die liefe der +Spieler, bevor er den Baum ueberhaupt beruehrt. + +Die Namens- und Hierarchie-Konvention ist nicht geraten, sondern vom +Fels-Export abgeschaut, der in der Spiel-Pipeline bereits abgenommen ist: +`UCX_`, und zwar als GESCHWISTER der LodGroup, nicht als Kind - +Unreal liest jedes Kind der Gruppe als LOD-Stufe. + +Gemessen wird im reimportierten FBX. + +Aufruf: blender --background --factory-startup --python tests/test_stamm_ucx.py +""" +import os +import sys +import math +import tempfile +import importlib.util + +import bpy + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +spec = importlib.util.spec_from_file_location( + "tg", os.path.join(ROOT, "stylized_tree_generator.py")) +tg = importlib.util.module_from_spec(spec) +sys.modules["tg"] = tg +spec.loader.exec_module(tg) +tg.register() + +fails = [] +OUT = tempfile.mkdtemp() + +for o in list(bpy.data.objects): + bpy.data.objects.remove(o, do_unlink=True) + +me = bpy.data.meshes.new("Leaf_Card") +me.from_pydata([(-0.5, 0, 0), (0, 0, 0.7), (0.5, 0, 0), (0, 0, -0.1)], + [], [(0, 1, 2), (0, 2, 3)]) +me.update() +card = bpy.data.objects.new("Leaf_Card", me) +bpy.context.scene.collection.objects.link(card) + +s = bpy.context.scene.tree_gen_settings +s.apply_modifier = True +s.leaf_card = card +s.use_growth = False +s.stand_ucx = True + +# Presets mit verschiedenen Stamm-Formen: gerade, krumm (Bend 1.5), ohne Aeste +# am Setzling, und der Obstbaum als Produktionsfall. +FAELLE = (("eiche", True), ("toter_baum", True), ("obstbaum", True), + ("eiche", False)) + +# Erst ALLE Faelle exportieren, dann reimportieren. `read_factory_settings` +# raeumt die Szene komplett aus - Blatt-Card und Einstellungen inklusive -, +# ein Reimport mitten in der Schleife killt also den naechsten Durchlauf. +ergebnisse = [] +for preset, mit_lods in FAELLE: + for o in list(bpy.context.scene.objects): + if o is not card: + bpy.data.objects.remove(o, do_unlink=True) + ziel_dir = os.path.join(OUT, "%s_%d" % (preset, int(mit_lods))) + os.makedirs(ziel_dir, exist_ok=True) + + s.preset = preset + s.art_name = preset.capitalize() + s.variant = 1 + s.seed = 1 + s.generate_lods = mit_lods + s.export_dir = ziel_dir + bpy.ops.object.treegen_create() + baum = [o for o in bpy.context.scene.objects + if o.type == 'MESH' and o is not card + and not o.name.endswith(("_Leaf", "_Frucht"))][0] + # BEWUSST nicht am Ursprung - eine Huelle, die die Weltmatrix mitschleppt, + # faellt bei (0,0,0) nicht auf. + baum.location = (9.0, -5.0, 0.0) + for o in bpy.context.scene.objects: + o.select_set(o is baum) + bpy.context.view_layer.objects.active = baum + bpy.ops.object.treegen_leaves() + bl = bpy.data.objects.get(baum.name + "_Leaf") + if bl is not None: + bl.location = baum.location + + # Kronenansatz und Stammradius am Quellbaum, als Sollwerte. + dg = bpy.context.evaluated_depsgraph_get() + ev = baum.evaluated_get(dg) + m = ev.to_mesh() + attr = m.attributes.get("ast_id") + vs = [v.co.copy() for v in m.vertices] + aid = [attr.data[i].value for i in range(len(vs))] if attr else [0] * len(vs) + ev.to_mesh_clear() + z0 = min(v.z for v in vs) + astz = [v.z for v, i in zip(vs, aid) if i > 0] + ansatz = (min(astz) - z0) if astz else (max(v.z for v in vs) - z0) + stamm_r = max((math.hypot(v.x, v.y) for v, i in zip(vs, aid) + if i == 0 and v.z - z0 <= ansatz), default=0.0) + + for o in bpy.context.scene.objects: + o.select_set(o is baum) + bpy.context.view_layer.objects.active = baum + bpy.ops.treegen.export_game() + + dateien = sorted(f for f in os.listdir(ziel_dir) + if f.lower().endswith(".fbx")) + if not dateien: + fails.append("%s: kein FBX geschrieben" % preset) + continue + ergebnisse.append((preset, mit_lods, os.path.join(ziel_dir, dateien[0]), + stamm_r, ansatz, max(v.z for v in vs) - z0, + s.stand_ucx_verts)) + +print("%-12s %-5s %6s %8s %8s %8s %8s" + % ("Preset", "LODs", "Verts", "UCX-R", "Stamm-R", "UCX-Z", "Ansatz-Z")) + +for preset, mit_lods, pfad, stamm_r, ansatz, blender_h, max_verts in ergebnisse: + bpy.ops.wm.read_factory_settings(use_empty=True) + bpy.ops.import_scene.fbx(filepath=pfad) + + ucx = [o for o in bpy.context.scene.objects + if o.type == 'MESH' and o.name.startswith("UCX_")] + rest = [o for o in bpy.context.scene.objects + if o.type == 'MESH' and not o.name.startswith("UCX_")] + gruppe = [o for o in bpy.context.scene.objects if o.type == 'EMPTY'] + + if len(ucx) != 1: + fails.append("%s (LODs %s): %d UCX-Meshes im FBX, erwartet genau eines" + % (preset, mit_lods, len(ucx))) + continue + u = ucx[0] + + # Name: exakt die Konvention des Fels-Exports. + erwartet = ("UCX_" + gruppe[0].name) if gruppe else ("UCX_" + rest[0].name) + if u.name != erwartet: + fails.append("%s: Huelle heisst '%s', erwartet '%s' - Unreal ordnet " + "die Kollision ueber den Namen zu" + % (preset, u.name, erwartet)) + + # Hierarchie: NICHT unter der LodGroup, sonst waere sie eine LOD-Stufe. + if u.parent is not None: + fails.append("%s: Huelle haengt unter '%s' - Unreal liest jedes Kind " + "der LodGroup als LOD-Stufe" % (preset, u.parent.name)) + + co = [u.matrix_world @ v.co for v in u.data.vertices] + u_r = max(math.hypot(c.x, c.y) for c in co) + u_z0 = min(c.z for c in co) + u_z1 = max(c.z for c in co) + n = len(u.data.vertices) + + # Skala aus dem FBX zurueckrechnen, damit die Vergleiche unabhaengig von + # FBX_SCALE_ALL sind. NUR ueber die RINDE (Material-Slot 0): der erste + # Anlauf nahm die Gesamthoehe inklusive Blatt-Cards, und die ragen ueber die + # Rinde hinaus - die Skala kam dadurch zu gross heraus und die Huelle sah + # zu klein aus. Ein Massstab, der etwas anderes misst als die Referenz, + # verschiebt jede Aussage. + lod0 = max(rest, key=lambda o: len(o.data.vertices)) + rinde = [lod0.matrix_world @ lod0.data.vertices[vi].co + for p in lod0.data.polygons if p.material_index == 0 + for vi in p.vertices] + fbx_h = (max(c.z for c in rinde) - min(c.z for c in rinde)) if rinde else 1.0 + skala = fbx_h / blender_h if blender_h else 1.0 + + print("%-12s %-5s %6d %8.3f %8.3f %8.3f %8.3f" + % (preset, mit_lods, n, u_r / skala, stamm_r, + (u_z1 - u_z0) / skala, ansatz)) + + if not (8 <= n <= max_verts * 2): + fails.append("%s: Huelle hat %d Verts, erwartet 8-%d" + % (preset, n, max_verts * 2)) + + # DIE Bedingung: Stamm umschlossen, Krone NICHT. Der Stammradius liegt bei + # wenigen Zentimetern, die Krone bei ein bis zwei Metern - dazwischen ist + # so viel Luft, dass die Schranke nicht knapp sein muss. + if u_r / skala < stamm_r * 0.95: + fails.append("%s: Huellenradius %.3f m unter dem Stammradius %.3f - " + "der Spieler wuerde in den Stamm laufen" + % (preset, u_r / skala, stamm_r)) + if u_r / skala > max(stamm_r * 2.5, stamm_r + 0.15): + fails.append("%s: Huellenradius %.3f m gegen Stammradius %.3f - das " + "ist keine Stammhuelle mehr" + % (preset, u_r / skala, stamm_r)) + + # Fuss auf Z = 0 und nicht in die Krone hinaufreichend. + if abs(u_z0 / skala) > 0.02: + fails.append("%s: Huelle beginnt bei Z %.3f statt 0" % (preset, u_z0 / skala)) + if (u_z1 - u_z0) / skala > ansatz * 1.35 + 0.05: + fails.append("%s: Huelle ist %.2f m hoch, Kronenansatz liegt bei " + "%.2f m - sie reicht in die Krone" + % (preset, (u_z1 - u_z0) / skala, ansatz)) + +print("") +if fails: + print("ERGEBNIS: %d FEHLER" % len(fails)) + for f in fails: + print(" - " + f) + sys.exit(1) +print("ERGEBNIS: ALLE CHECKS OK") diff --git a/tests/test_ursprung.py b/tests/test_ursprung.py index ac473dd..05a304b 100644 --- a/tests/test_ursprung.py +++ b/tests/test_ursprung.py @@ -146,7 +146,9 @@ print("%-12s %3s %9s %11s %11s %10s" for preset, stufe, pfad, r_rel in ergebnisse: bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.fbx(filepath=pfad) - meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH'] + # Die UCX-Stammhuelle ist KEIN Render-Mesh - sie gehoert nicht in diese Zaehlung. + meshes = [o for o in bpy.context.scene.objects + if o.type == 'MESH' and not o.name.startswith("UCX_")] if not meshes: fails.append("%s S%d: kein Mesh im FBX" % (preset, stufe)) continue