"""Obstbaum: Krone gross genug fuer Fruechte auf der Aussenhaut. Die Frucht-Streuung der Spielseite setzt Fruechte auf die AUSSENHAUT der Krone und schiebt sie radial nach aussen. Das setzt eine Krone voraus, die groesser ist als eine einzelne Blatt-Card - sonst gibt es keine Haut, auf die sich etwas setzen laesst. Am generischen "baum" war der Kronenradius 0.78 m bei 1.28 m Card-Diagonale: die Cards waren groesser als die Krone. Deshalb ist das hier KEIN Silhouetten-Test mit Geschmacksurteil, sondern eine funktionale Bedingung: Kronenradius > Card-Groesse. Vorgaben der Spielseite, die mitgeprueft werden: Kronenansatz 35-40 %, Kronenradius S4 >= 1.8 m, Hoehe 4-5 m. B/H ist bewusst NICHT hart geprueft - siehe Kommentar unten. Aufruf: blender --background --factory-startup --python tests/test_obstbaum.py """ import os import sys import math 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 = [] 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 if "obstbaum" not in tg.PRESETS: print("ERGEBNIS: FEHLER: Preset 'obstbaum' fehlt") sys.exit(1) def inseln(m): nachbarn = {} for e in m.edges: a, b = e.vertices nachbarn.setdefault(a, []).append(b) nachbarn.setdefault(b, []).append(a) gesehen, out = set(), [] for start in range(len(m.vertices)): if start in gesehen: continue stapel, gruppe = [start], [] gesehen.add(start) while stapel: v = stapel.pop() gruppe.append(v) for w in nachbarn.get(v, ()): if w not in gesehen: gesehen.add(w) stapel.append(w) out.append(gruppe) return out # Die Seeds sind die der Spielseite: Apfel 1/15/22, Kirsche 2/16/23, # Birne 3/17/24. Ein Preset, das nur bei einem Seed passt, ist keines. SEEDS = (1, 15, 22, 2, 16, 23, 3, 17, 24) print("%-6s %7s %6s %9s %9s %9s %7s" % ("Seed", "Hoehe", "B/H", "KronR", "Card-Max", "KronR/Card", "Ansatz")) verh = [] for seed in SEEDS: for o in list(bpy.context.scene.objects): if o is not card: bpy.data.objects.remove(o, do_unlink=True) s.preset = "obstbaum" s.art_name = "Obst" s.variant = 1 s.seed = seed s.use_growth = False 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] 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") dg = bpy.context.evaluated_depsgraph_get() ev = baum.evaluated_get(dg) m = ev.to_mesh() vs = [v.co.copy() for v in m.vertices] attr = m.attributes.get("ast_id") 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) hoehe = max(v.z for v in vs) - z0 kron_r = max(math.hypot(v.x, v.y) for v in vs) astz = [v.z for v, i in zip(vs, aid) if i > 0] ansatz = 100.0 * (min(astz) - z0) / hoehe if astz else 0.0 card_max = 0.0 if bl is not None: ev = bl.evaluated_get(dg) m2 = ev.to_mesh() for grp in inseln(m2): pts = [m2.vertices[i].co for i in grp] card_max = max(card_max, max((a - b).length for a in pts for b in pts)) ev.to_mesh_clear() v = kron_r / card_max if card_max else 0.0 verh.append(v) print("%-6d %7.2f %6.2f %9.2f %9.2f %9.2f %6.0f%%" % (seed, hoehe, 2 * kron_r / hoehe, kron_r, card_max, v, ansatz)) # DIE funktionale Bedingung. if v < 1.3: fails.append("Seed %d: Kronenradius %.2f m bei %.2f m Card - die Krone " "hat keine Aussenhaut fuer Fruechte" % (seed, kron_r, card_max)) if kron_r < 1.80: fails.append("Seed %d: Kronenradius %.2f m, Vorgabe >= 1.80" % (seed, kron_r)) if not (4.0 <= hoehe <= 5.2): fails.append("Seed %d: Hoehe %.2f m, Vorgabe 4-5 m (5.2 als Toleranz)" % (seed, hoehe)) if not (33.0 <= ansatz <= 45.0): fails.append("Seed %d: Kronenansatz %.0f %%, Vorgabe 35-40 %% " "(33-45 als Toleranz)" % (seed, ansatz)) # B/H wird BEWUSST nicht hart geprueft. # # Die Spielseite nennt 0.55-0.75 UND Kronenradius >= 1.80 m bei 4-5 m Hoehe. # Beides zusammen laesst bei H = 5.0 nur B zwischen 3.60 und 3.75 zu - ein # Fenster von 4 %, waehrend die Streuung von Seed zu Seed rund 12 % betraegt. # Die Vorgaben sind also nur im Mittel gleichzeitig erfuellbar. Vorrang hat die # funktionale (Kronenradius), weil an ihr die Frucht-Streuung haengt. print("") print("Kronenradius/Card ueber alle Seeds: %.2f - %.2f" % (min(verh), max(verh))) if fails: print("ERGEBNIS: %d FEHLER" % len(fails)) for f in fails: print(" - " + f) sys.exit(1) print("ERGEBNIS: ALLE CHECKS OK")