diff --git a/README.md b/README.md index 9431536..3516918 100644 --- a/README.md +++ b/README.md @@ -563,6 +563,38 @@ 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. +### Zwei Knoepfe, ein Weg + +Blattwerk entsteht an zwei Stellen: ueber das Haekchen "_Leaf anlegen" beim +Erzeugen und ueber den Knopf "Blattwerk streuen". Nur der zweite rechnete die +Wachstums-Staffel. Von der Spielseite am Modifier nachgemessen trug der +S1-Setzling aus dem Create-Pfad **Size 0.85, Max Radius 0.04, Min Height 0.25** - +exakt die Altbaum-Werte. + +Das ist zum dritten Mal dieselbe Fehlerklasse (nach der Stufen-Deckung und +`build_group`): **zwei Quellen fuer dieselbe Frage**. Die Staffel liegt deshalb +jetzt in `make_leaves` selbst, die sie aus `growth_t` und `preset` am +Baum-Objekt zieht - ein Aufrufer KANN sie nicht mehr vergessen. Darueber sitzt +`blattwerk_streuen()` als der eine Einstieg, den beide Knoepfe benutzen; dort +haengt auch die Dichte-Regelung, die vorher ebenfalls nur der Operator hatte. + +`tests/test_zwei_pfade.py` prueft nicht die absoluten Werte, sondern die +DIFFERENZ zwischen beiden Wegen - ein Test, der nur einen Weg anschaut, findet +so etwas grundsaetzlich nie. Zusaetzlich prueft er, dass sich S1 und S4 +ueberhaupt unterscheiden: waeren beide Wege gleich falsch, waere der Vergleich +allein zufrieden. + +### Der generische Baum war auch eine Saeule + +Gemessen Breite/Hoehe **0.18** ueber fuenf Seeds - schmaler als jedes andere +Laubgehoelz im Satz. Ursache dieselbe wie bei der Birke: `Branch Up` 1.3 aus den +DEFAULTS. Zielband der Spielseite 0.45-0.60, gemessen jetzt **0.55** +(0.52-0.56). + +Die DEFAULTS bleiben unangetastet - sie sind die Socket-Vorgaben der +Node-Gruppe, an denen jeder haendische Regler haengt. Nur `baum` bekommt eigene +Werte; alle anderen Presets ueberschreiben `Branch Up` ohnehin. + ### Obstbaum: die Krone muss groesser sein als eine Card Das Preset ist nicht nur eine Silhouette, es ist eine FUNKTIONALE Bedingung. diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index 95d95f0..9897898 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.37.0" +version = "1.38.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 d24a382..6f7f08e 100644 --- a/dist/index.html +++ b/dist/index.html @@ -50,17 +50,17 @@ 34.1KB - stylized_tree_generator-1.37.0 + stylized_tree_generator-1.38.0 Stylized Tree Generator Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen link 4.2.0 - ~ all all - 44.2KB + 45.0KB -

Built 2026-08-06, 10:59

+

Built 2026-08-06, 11:22

diff --git a/dist/index.json b/dist/index.json index b8898de..02212d0 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.37.0", + "version": "1.38.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -98,9 +98,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.37.0.zip", - "archive_size": 45213, - "archive_hash": "sha256:189494bb426bb757671a8da62baf1fb341e6147532a1d0a7eb7d9a7a745d15f3" + "archive_url": "./stylized_tree_generator-1.38.0.zip", + "archive_size": 46032, + "archive_hash": "sha256:0d65140f04d1ba8289a72c69c44c95b8e7b7300681695b43c7cba7a8f2ec6c9b" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.37.0.zip b/dist/stylized_tree_generator-1.37.0.zip deleted file mode 100644 index c26c4ea..0000000 Binary files a/dist/stylized_tree_generator-1.37.0.zip and /dev/null differ diff --git a/dist/stylized_tree_generator-1.38.0.zip b/dist/stylized_tree_generator-1.38.0.zip new file mode 100644 index 0000000..3902d39 Binary files /dev/null and b/dist/stylized_tree_generator-1.38.0.zip differ diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index 7ad744b..9681929 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, 37, 0), + "version": (1, 38, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -86,7 +86,21 @@ DEFAULTS = { # Presets: nur die Abweichungen von DEFAULTS. PRESETS = { - "baum": {}, + # Der generische Laubbaum war eine SAEULE: gemessen Breite/Hoehe 0.18 ueber + # fuenf Seeds, schmaler als jedes andere Laubgehoelz hier. Ursache war + # dieselbe wie bei der Birke - "Branch Up" 1.3 aus den DEFAULTS liess die + # Aeste fast senkrecht nach oben zeigen und am Stamm kleben. + # + # Zielband der Spielseite: 0.45-0.60, "zwischen Birke (0.51) und einem + # klassischen Parkbaum". + # + # Die DEFAULTS bleiben unangetastet - sie sind die Socket-Vorgaben der + # Node-Gruppe, an denen jeder haendische Regler haengt. Nur "baum" bekommt + # eigene Werte; alle anderen Presets ueberschreiben Branch Up ohnehin. + "baum": { + "Branch Up": 0.5, "Branch Length": 2.1, + "Attraction Up": 0.15, # spreizen statt hochschwingen + }, # Palme - am TreeIt-Referenzbaum ausgemessen (palm07_0 + frond stem_1): # Stamm 4.33 m, Radius 0.133 unten -> ~0.086 Mitte -> 0.106 OBEN # (Kronenschaft!), 11 Seiten, nur 150 Tris. @@ -1753,8 +1767,46 @@ def _dichte_einregeln(context, ob, md, ids, ziel, start_dichte, pro_stueck, return bestes[1], bestes[2] -def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None, **overrides): - """Erzeugt/aktualisiert ein _Leaf-Objekt mit dem Blattwerk-Modifier.""" +def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None, + stufen=True, **overrides): + """Erzeugt/aktualisiert ein _Leaf-Objekt mit dem Blattwerk-Modifier. + + Die WACHSTUMS-STAFFEL sitzt hier drin, nicht beim Aufrufer. Uebergeben + werden die Grundwerte (Size, Max Radius); welche Stufe daraus wird, zieht + diese Funktion selbst aus `growth_t` und `preset` am Baum-Objekt. + + Warum hier und nicht beim Aufrufer: es gab zwei Wege zum Blattwerk - den + Knopf "Blattwerk streuen" und das Haekchen "_Leaf anlegen" beim Erzeugen - + und nur der erste rechnete die Staffel. Gemessen am S1-Modifier des + Create-Pfads: Size 0.85, Max Radius 0.04, Min Height 0.25, also exakt die + Altbaum-Werte. Das ist dieselbe Fehlerklasse wie bei der Stufen-Deckung und + bei build_group: zwei Quellen fuer dieselbe Frage. Solange die Staffel hier + liegt, KANN ein Aufrufer sie nicht vergessen. + + `stufen=False` schaltet sie ab - fuer Aufrufer, die bewusst rohe Werte + setzen. + """ + if stufen: + # Stufe aus dem OBJEKT lesen, nicht aus dem Kontext: der Blattwerk-Lauf + # kommt spaeter und getrennt vom Erzeugen. + gt = float(tree_obj.get("growth_t", 1.0)) + _ziel, g_anteil, h_anteil = _stufen_anteile(gt, tree_obj.get("preset")) + + # Der RADIUS-FILTER muss mit dem Baum schrumpfen. Er trennt duenne + # Zweige (Laub) von dickem Geaest (kein Laub) - aber ein Setzling ist + # KOMPLETT duenner als die Schwelle. Gemessen mit festem 0.04 m: bei S1 + # sassen 92 % der Cards am Stamm, bei S2 noch 81 %, das Laub klebte also + # am Stiel statt in der Krone. Boden bei LEAF_RADIUS_YOUNG, weil der + # reine Stammradius-Faktor (0.064 bei t = 0) unter die duennsten Zweige + # fiel und der Setzling wieder kahl stand (gemessen 0 Cards). + r_faktor = LEAF_RADIUS_YOUNG + (1.0 - LEAF_RADIUS_YOUNG) * gt + + basis = dict(LEAF_DEFAULTS) + basis.update(overrides) + overrides["Size"] = basis["Size"] * g_anteil + overrides["Max Radius"] = basis["Max Radius"] * r_faktor + overrides["Min Height"] = h_anteil + name = name or (tree_obj.name + "_Leaf") ob = bpy.data.objects.get(name) if ob is None: @@ -1784,6 +1836,41 @@ def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None, **overri return ob +def blattwerk_streuen(context, tree_obj, s): + """DER Weg zum Blattwerk - fuer beide Knoepfe derselbe. + + Streut die Cards und regelt die Dichte auf die Ziel-Cardzahl der Stufe ein. + Wer Blattwerk erzeugt, ruft das hier und sonst nichts: die frueheren zwei + Wege liefen genau so lange auseinander, bis es jemandem im Spiel auffiel. + + Gibt (Blatt-Objekt, gezaehlte Cards, Ziel-Cards) zurueck; ohne Regelung + sind die letzten beiden None. + """ + lf = make_leaves(context, tree_obj, s.leaf_card, + card_coll=s.leaf_cards, + Density=s.leaf_density, + Size=s.leaf_size, + **{"Size Var": s.leaf_size_var, + "Max Radius": s.leaf_max_radius, + "Tilt": s.leaf_tilt, "Seed": s.seed}) + + # Dichte auf die ZIEL-CARDZAHL einregeln. Muss NACH dem Anlegen passieren, + # weil dafuer gemessen werden muss, was tatsaechlich herauskommt - die + # Zweigflaeche faellt je Preset voellig anders aus. + gt = float(tree_obj.get("growth_t", 1.0)) + ziel_cards, _g, _h = _stufen_anteile(gt, tree_obj.get("preset")) + if not ziel_cards: + return lf, None, None + md = lf.modifiers.get("GN_Leaves") + ng = md.node_group if md is not None else None + if ng is None: + return lf, None, None + pro = _cards_pro_stueck(s.leaf_card, s.leaf_cards) + dichte, ist = _dichte_einregeln(context, lf, md, leaf_socket_ids(ng), + ziel_cards, s.leaf_density, pro) + print("[TreeGen] %s: %d Cards (Ziel %d) bei Dichte %.0f" + % (tree_obj.name, ist, ziel_cards, dichte)) + return lf, ist, ziel_cards # --------------------------------------------------------------------------- @@ -2909,48 +2996,10 @@ class TREEGEN_OT_leaves(Operator): total_tree = total_leaf = 0 for t in trees: try: - # Bueschelgroesse mit dem Wachstum skalieren: ein Setzling - # traegt kleinere Bueschel. Baeume ohne Wachstums-Stufe haben - # growth_t = 1.0 und bleiben unveraendert. - gt = float(t.get("growth_t", 1.0)) - ziel_cards, g_anteil, h_anteil = _stufen_anteile( - gt, t.get("preset")) - - # Der RADIUS-FILTER muss mit dem Baum schrumpfen. Er trennt - # duenne Zweige (Laub) von dickem Geaest (kein Laub) - aber ein - # Setzling ist KOMPLETT duenner als die Schwelle. Gemessen mit - # festem 0.04 m: bei S1 sassen 92 % der Cards am Stamm, bei S2 - # noch 81 %, das Laub klebte also am Stiel statt in der Krone. - # Skaliert wird mit dem Wachstum, aber mit BODEN bei 0.20. - # Der reine Stammradius-Faktor (0.064 bei t=0) war zu scharf: - # damit fiel die Schwelle unter die duennsten Zweige und der - # Setzling stand wieder kahl (gemessen 0 Cards). Bei 0.20 - # bekommt er 12 Cards, davon nur 12 % am Stamm - vorher waren - # es 92 %. - r_faktor = LEAF_RADIUS_YOUNG + (1.0 - LEAF_RADIUS_YOUNG) * gt - - lf = make_leaves(context, t, s.leaf_card, - card_coll=s.leaf_cards, - Density=s.leaf_density, - Size=s.leaf_size * g_anteil, - **{"Size Var": s.leaf_size_var, - "Max Radius": s.leaf_max_radius * r_faktor, - "Min Height": h_anteil, - "Tilt": s.leaf_tilt, "Seed": s.seed}) - # Dichte auf die ZIEL-CARDZAHL einregeln. Muss nach dem Anlegen - # passieren, weil dafuer gemessen werden muss, was tatsaechlich - # herauskommt - die Zweigflaeche faellt je Preset voellig - # anders aus. - if ziel_cards: - md = lf.modifiers.get("GN_Leaves") - ng = md.node_group if md is not None else None - if ng is not None: - pro = _cards_pro_stueck(s.leaf_card, s.leaf_cards) - dichte, ist = _dichte_einregeln( - context, lf, md, leaf_socket_ids(ng), - ziel_cards, s.leaf_density, pro) - print("[TreeGen] %s: %d Cards (Ziel %d) bei Dichte %.0f" - % (t.name, ist, ziel_cards, dichte)) + # Staffel und Dichte-Regelung stecken in blattwerk_streuen - + # damit laeuft dieser Knopf durch dieselbe Stelle wie das + # Haekchen beim Erzeugen. + lf, _ist, _ziel = blattwerk_streuen(context, t, s) except Exception as exc: # noqa: BLE001 self.report({'WARNING'}, "%s: %s" % (t.name, exc)) continue @@ -3071,13 +3120,13 @@ class TREEGEN_OT_create(Operator): for ob in created: if s.make_leaf: if s.leaf_card is not None and s.leaf_card.type == 'MESH': - # Card gesetzt -> Blattwerk direkt streuen - extra.append(make_leaves( - context, ob, s.leaf_card, - Density=s.leaf_density, Size=s.leaf_size, - **{"Size Var": s.leaf_size_var, - "Max Radius": s.leaf_max_radius, - "Tilt": s.leaf_tilt, "Seed": s.seed})) + # Card gesetzt -> Blattwerk direkt streuen, ueber + # DIESELBE Stelle wie der Knopf "Blattwerk streuen". + # Vorher stand hier ein eigener make_leaves-Aufruf mit + # rohem s.leaf_size - die Wachstums-Staffel fehlte + # komplett, gemessen trug der S1-Setzling + # Altbaum-Bueschel. + extra.append(blattwerk_streuen(context, ob, s)[0]) else: # keine Card -> leeres _Leaf zum Selbermodellieren extra.append(_add_layer(context, ob, "_Leaf")) diff --git a/tests/test_zwei_pfade.py b/tests/test_zwei_pfade.py new file mode 100644 index 0000000..987429d --- /dev/null +++ b/tests/test_zwei_pfade.py @@ -0,0 +1,127 @@ +"""Beide Wege zum Blattwerk muessen dasselbe ergeben. + +Es gibt zwei Knoepfe, die Blattwerk erzeugen: + * "_Leaf anlegen" beim Erzeugen des Baums (TREEGEN_OT_create) + * "Blattwerk streuen" auf einer Auswahl (TREEGEN_OT_leaves) + +Nur der zweite rechnete die Wachstums-Staffel. Von der Spielseite am Modifier +nachgemessen: der S1-Setzling aus dem Create-Pfad trug Size 0.85, Max Radius +0.04, Min Height 0.25 - exakt die Altbaum-Werte. + +Das ist dieselbe Fehlerklasse, die vorher schon die Stufen-Deckung und +build_group erwischt hat: ZWEI QUELLEN fuer dieselbe Frage. Ein Test, der nur +einen der beiden Wege prueft, findet so etwas nie - deshalb prueft dieser hier +die DIFFERENZ zwischen ihnen und nicht die absoluten Werte. + +Aufruf: blender --background --factory-startup --python tests/test_zwei_pfade.py +""" +import os +import sys +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.leaf_card = card +s.apply_modifier = True +s.preset = "birke" +s.variant = 1 +s.use_growth = True +s.stages = 4 + +# Die Werte, die die Staffel anfasst. "Density" steht bewusst NICHT dabei: sie +# wird nach dem Streuen per Bisektion eingeregelt und darf sich zwischen zwei +# Laeufen minimal unterscheiden. +GESTAFFELT = ("Size", "Max Radius", "Min Height") + + +def werte(leaf_obj): + md = leaf_obj.modifiers.get("GN_Leaves") + ids = tg.leaf_socket_ids(md.node_group) + return {k: round(float(md[ids[k]]), 6) for k in GESTAFFELT if k in ids} + + +def bauen(mit_haken): + for o in list(bpy.context.scene.objects): + if o is not card: + bpy.data.objects.remove(o, do_unlink=True) + s.art_name = "P" if mit_haken else "Q" + s.make_leaf = mit_haken + bpy.ops.object.treegen_create() + staemme = sorted((o for o in bpy.context.scene.objects + if o.type == 'MESH' and o is not card + and not o.name.endswith(("_Leaf", "_Frucht"))), + key=lambda o: o.name) + if not mit_haken: + for o in bpy.context.scene.objects: + o.select_set(o in staemme) + bpy.context.view_layer.objects.active = staemme[0] + bpy.ops.object.treegen_leaves() + out = [] + for st in staemme: + bl = bpy.data.objects.get(st.name + "_Leaf") + out.append((st.name, float(st.get("growth_t", 1.0)), + werte(bl) if bl else None)) + return out + + +erzeugt = bauen(True) # Haekchen "_Leaf anlegen" +operator = bauen(False) # Knopf "Blattwerk streuen" + +print("%-5s %7s | %-34s | %-34s" + % ("Stufe", "growth", "beim Erzeugen", "per Operator")) +for (n1, gt, w1), (n2, _gt, w2) in zip(erzeugt, operator): + def zeig(w): + if w is None: + return "keine Blattebene" + return " ".join("%s %.3f" % (k[:3], w[k]) for k in GESTAFFELT if k in w) + gleich = "" if w1 == w2 else " <-- WEICHT AB" + print("%-5s %7.2f | %-34s | %-34s%s" + % (n1.split("_")[-1], gt, zeig(w1), zeig(w2), gleich)) + if w1 is None or w2 is None: + fails.append("%s: eine der beiden Ebenen fehlt" % n1) + continue + for k in GESTAFFELT: + if k not in w1 or k not in w2: + continue + if abs(w1[k] - w2[k]) > 1e-6: + fails.append("%s: '%s' ist beim Erzeugen %.4f, per Operator %.4f - " + "die Wachstums-Staffel greift nur auf einem Weg" + % (n1, k, w1[k], w2[k])) + +# Und die Staffel muss ueberhaupt greifen: waeren beide Wege gleich FALSCH, +# waere der Vergleich oben zufrieden. Also zusaetzlich, dass sich S1 von S4 +# unterscheidet. +if len(erzeugt) >= 2 and erzeugt[0][2] and erzeugt[-1][2]: + s1, s4 = erzeugt[0][2], erzeugt[-1][2] + if s1.get("Size") is not None and abs(s1["Size"] - s4["Size"]) < 1e-6: + fails.append("S1 und S4 tragen dieselbe Card-Groesse (%.3f) - die " + "Staffel greift auf KEINEM der beiden Wege" % s1["Size"]) + +print("") +if fails: + print("ERGEBNIS: %d FEHLER" % len(fails)) + for f in fails: + print(" - " + f) + sys.exit(1) +print("ERGEBNIS: ALLE CHECKS OK")