diff --git a/README.md b/README.md index 066d4a6..d17074f 100644 --- a/README.md +++ b/README.md @@ -385,6 +385,51 @@ Zwei Fallen beim Bauen: - `join` loescht das zweite Objekt; jede gehaltene Referenz darauf ist danach tot. Die Aufraeumliste fuehrt deshalb **Namen** statt Objekte. +### Blatt-Defaults: Bueschel, nicht Blaettchen + +Eine Card ist ein ganzer **Zweigbueschel**, kein einzelnes Blatt. Die alten Werte +stammten aus der Blaettchen-Aera und streuten hunderte winziger Cards, wo ein +paar Dutzend grosse hingehoeren. Die neuen Werte sind an einer S4-Eiche in UE +gemessen und abgenommen: + +| | vorher | jetzt | Wirkung | +|---|---|---|---| +| Groesse | 0.22 | **0.85** | Bueschel statt Bonsai | +| Dichte | 500 | **170** | wenige grosse statt viele Fussel | +| **Max Radius** | 0.02 | **0.04** | der Volumen-Hebel | +| Kippung | 0.5 | **0.9** | Rotations-Chaos schliesst Sichtluecken | + +`Max Radius` ist der unscheinbare Held: bei 0.02 trugen nur die aeussersten +Zweigspitzen Laub, die Krone hatte nur Kontur und war innen nackt. Bei 0.04 +wachsen Bueschel auch am mittleren Geaest und die Krone bekommt Tiefe. + +**Bueschelgroesse haengt am Wachstum.** Der Generator merkt sich `growth_t` am +Objekt; der Blattwerk-Operator skaliert die Cards damit (`Setzlings-Anteil`, +Standard 45 %). Gemessen ueber vier Stufen: Card-Kante 0.19 → 0.28 → 0.37 → 0.45. + +> **Falle dabei:** „Dichte" ist Punkte pro m² Zweigflaeche, und ein Setzling hat +> davon fast keine — bei Stufe 1 kamen exakt **null** Cards heraus, der Setzling +> stand kahl da. Da die Bueschel um denselben Faktor kleiner sind, deckt jeder +> nur faktor² der Flaeche; die Dichte entsprechend gegenlaeufig anzuheben haelt +> die Blattdeckung ueber alle Stufen gleich. + +**Tri-Budget pro Stufe** (ASSETS.md Teil 6) statt global: S1 ≤ 800, S2 ≤ 1200, +S3/S4 ≤ 4000. Bewusst Stufen und keine Interpolation. Die Schwellen greifen auch +bei drei Stufen richtig (t = 0 / 0.5 / 1.0). Die 2500 von frueher waren eine +konservative Setzung — bei ISM-Instanzen mit LOD-Kette sind Roh-Dreiecke nicht +der Engpass, sondern **Overdraw** der Cards (die GPU zahlt pro Bildschirmflaeche, +nicht pro Dreieck) und die Schattenpaesse. Genau deshalb sind 170 grosse Bueschel +guenstiger als 500 kleine, bei gleichem Tri-Zaehler. + +> **Die wichtigste Code-Falle in diesem Addon.** `md[socket] = wert` loest +> **keine** Neuauswertung des Geometry-Nodes-Modifiers aus. Nachgestellt: die +> Blattebene blieb bei 118 Faces stehen, obwohl die Dichte von 90 auf 400 +> geschrieben war — auch `view_layer.update()` half nicht. Erst ein +> `show_viewport`-Toggle brachte das Ergebnis. Der Knopf-Weg funktionierte +> bisher nur ZUFAELLIG, weil `make_leaves` jedes Mal auch die Objekt-Zeiger neu +> schreibt und das Aendern eines ID-Zeigers die Depsgraph-Beziehungen umbaut. +> `_modifier_anstossen()` macht das jetzt explizit. + ### LOD-Kette (Stand-Baum) Haken `LOD-Kette erzeugen`: exportiert `_LODGroup.fbx` mit `_LOD0..3` unter @@ -699,6 +744,7 @@ gefiltert `.\run_tests.ps1 -Filter panels`. | Datei | prueft | |---|---| | `tests/test_baum_export.py` | Stand-Baum: ein Mesh, zwei Slots, Ursprung, **Kugel-Normalen im exportierten FBX** | +| `tests/test_blatt_bueschel.py` | Bueschel-Defaults, Wachstums-Kopplung der Card-Groesse, Stufen-Budget 800/1200/4000 | | `tests/test_baum_lod.py` | LOD-Kette: 4 Stufen unter LodGroup, Cards werden nach oben duenner, **Kugel-Normalen auf jeder Stufe**, kein Hoehenversatz | | `tests/test_faell_teile.py` | Faell-Teile: **Blatt-Bilanz 170=170**, Pivot am Massenmittelpunkt, UCX je Teil, Gesamtdatei mit gemeinsamem Ursprung | | `tests/test_blatt_varianten.py` | Card-Sammlung: alle Varianten werden gezogen, seed-stabil, Einzelobjekt weiter moeglich | diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index 081a372..bfa9c34 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.29.0" +version = "1.30.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 f715bdd..5d738c9 100644 --- a/dist/index.html +++ b/dist/index.html @@ -50,17 +50,17 @@ 34.1KB - stylized_tree_generator-1.29.0 + stylized_tree_generator-1.30.0 Stylized Tree Generator Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen link 4.2.0 - ~ all all - 34.4KB + 36.0KB -

Built 2026-08-06, 05:55

+

Built 2026-08-06, 06:22

diff --git a/dist/index.json b/dist/index.json index 4eb5a59..92ad9d3 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.29.0", + "version": "1.30.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -98,9 +98,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.29.0.zip", - "archive_size": 35207, - "archive_hash": "sha256:faa1e629b1ee4cfd465db79246a6d4acfae12af61f8b65aafcabcc64ffdcde72" + "archive_url": "./stylized_tree_generator-1.30.0.zip", + "archive_size": 36830, + "archive_hash": "sha256:d54440c3d577440ff97b0415ef101801b0b7bf50b6df05e3645cc3d1e1eb15e5" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.29.0.zip b/dist/stylized_tree_generator-1.29.0.zip deleted file mode 100644 index 5c2e353..0000000 Binary files a/dist/stylized_tree_generator-1.29.0.zip and /dev/null differ diff --git a/dist/stylized_tree_generator-1.30.0.zip b/dist/stylized_tree_generator-1.30.0.zip new file mode 100644 index 0000000..42f2c20 Binary files /dev/null and b/dist/stylized_tree_generator-1.30.0.zip differ diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index 85b8b30..996435a 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, 29, 0), + "version": (1, 30, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -1171,6 +1171,12 @@ def make_object(ng, preset="baum", name=None, growth=None, seed=None): for k, v in values.items(): if k in ids: md[ids[k]] = v + _modifier_anstossen(md) + + # Wachstums-Fortschritt am Objekt merken. Der Blattwerk-Operator laeuft + # spaeter und getrennt - er kann sonst nicht wissen, ob er einen Setzling + # oder einen Altbaum vor sich hat. Aus dem Namen zu lesen waere bruechig. + ob["growth_t"] = 1.0 if growth is None else max(0.0, min(1.0, float(growth))) bpy.context.view_layer.objects.active = ob return ob @@ -1183,15 +1189,21 @@ def make_object(ng, preset="baum", name=None, growth=None, seed=None): LEAF_GROUP_NAME = "GN_TreeLeaves" +# Eine Card ist ein ganzer ZWEIGBUESCHEL, kein einzelnes Blaettchen. Die alten +# Werte (Size 0.22, Density 300-500) stammen aus der Blaettchen-Aera: damit +# streute der Generator hunderte winziger Cards, wo ein paar Dutzend grosse +# hingehoeren. Mit Bueschel-Atlanten liegen die Groessenordnungen bei +# Size ~0.7 und Density ~90. LEAF_DEFAULTS = { - "Density": 300.0, # Punkte pro m2 Zweigoberflaeche. Gemessen mit der - # 6-Tri-Diamond-Card: 300 -> ~180 Cards / 1092 Tris, - # zusammen mit dem Baum 2228 von 2500 Tris. 900 waere - # mit 4118 Tris deutlich ueber Budget. - "Max Radius": 0.02, # nur wo der Ast duenner ist als das (m) -> Zweige - "Size": 0.35, + "Density": 170.0, # Punkte pro m2 Zweigoberflaeche + # DER Volumen-Hebel. Bei 0.02 trugen nur die aeussersten Zweigspitzen Laub - + # die Krone hatte nur Kontur und war innen nackt. Bei 0.04 wachsen Bueschel + # auch am mittleren Geaest und die Krone bekommt Tiefe. Am fertigen Baum in + # UE gegengeprueft, nicht geschaetzt. + "Max Radius": 0.04, + "Size": 0.85, "Size Var": 0.35, - "Tilt": 0.5, # wie stark die Cards aus der Senkrechten kippen (rad) + "Tilt": 0.9, # mehr Rotations-Chaos schliesst Sichtluecken "Seed": 0, "Use Collection": False, } @@ -1369,6 +1381,34 @@ def leaf_socket_ids(ng): if getattr(so, "in_out", "") == 'INPUT'} +def _stufen_budget(s, growth_t): + """Tri-Budget fuer einen Baum nach seiner Wachstums-Stufe.""" + if growth_t < 0.15: + return s.leaf_budget_s1 + if growth_t <= 0.55: + return s.leaf_budget_s2 + return s.leaf_budget + + +def _modifier_anstossen(md): + """Erzwingt die Neuauswertung nach direktem Schreiben von Socket-Werten. + + FALLE, die von der UE-Seite gemeldet und hier nachgestellt wurde: `md[socket] + = wert` loest KEINE Neuauswertung aus. Gemessen blieb die Blattebene bei 118 + Faces stehen, obwohl die Dichte von 90 auf 400 geschrieben war - auch + `view_layer.update()` half nicht. Erst ein show_viewport-Toggle brachte das + Ergebnis. + + Der Knopf-Weg funktionierte bisher nur ZUFAELLIG: make_leaves schreibt jedes + Mal auch die Objekt-Zeiger neu, und das Aendern eines ID-Zeigers baut die + Depsgraph-Beziehungen um, was den Cache verwirft. Wer das spaeter optimiert + ("nur geaenderte Werte schreiben"), haette den Fehler still zurueck. + """ + sichtbar = md.show_viewport + md.show_viewport = not sichtbar + md.show_viewport = sichtbar + + def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None, **overrides): """Erzeugt/aktualisiert ein _Leaf-Objekt mit dem Blattwerk-Modifier.""" name = name or (tree_obj.name + "_Leaf") @@ -1396,6 +1436,7 @@ def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None, **overri for k, v in vals.items(): if k in ids: md[ids[k]] = v + _modifier_anstossen(md) return ob @@ -1820,23 +1861,50 @@ class TreeGenSettings(PropertyGroup): "damit mischt die Krone 4-6 Varianten statt einer"), ) leaf_density: FloatProperty( - name="Dichte", default=150.0, min=0.0, max=20000.0, + name="Dichte", default=170.0, min=0.0, max=20000.0, description=("Cards pro m2 Zweigoberflaeche. Presets mit viel Geaest (Eiche) " "bekommen dadurch automatisch mehr Blaetter als schlanke. 150 haelt " "auch die Eiche im 2500-Tri-Budget; der Button meldet die Summe"), ) + leaf_budget_s1: IntProperty( + name="Budget S1", default=800, min=100, max=20000, + description="Tri-Budget der Setzlings-Stufe (ASSETS.md Teil 6)", + ) + leaf_budget_s2: IntProperty( + name="Budget S2", default=1200, min=100, max=20000, + description="Tri-Budget der zweiten Stufe (ASSETS.md Teil 6)", + ) leaf_budget: IntProperty( - name="Tri-Budget", default=2500, min=0, max=100000, - description="Bei Ueberschreitung warnt der Blattwerk-Button (0 = keine Pruefung)", + name="Budget S3/S4", default=4000, min=0, max=100000, + description=("Tri-Budget der ausgewachsenen Stufe. Die urspruenglichen " + "2500 waren eine konservative Setzung - bei ISM-Instanzen " + "mit LOD-Kette sind Roh-Dreiecke nicht der Engpass, " + "sondern Overdraw und Schatten der Cards. Bei " + "Ueberschreitung warnt der Blattwerk-Button (0 = aus)"), + ) + leaf_size: FloatProperty( + name="Groesse", default=0.85, min=0.01, max=5.0, + description=("Kantenlaenge einer Card. Eine Card ist ein ganzer " + "Zweigbueschel, kein einzelnes Blatt - deshalb 0.7 und " + "nicht 0.2"), + ) + leaf_size_growth: FloatProperty( + name="Setzlings-Anteil", default=0.45, min=0.05, max=1.0, + description=("Wie gross die Bueschel auf der KLEINSTEN Wachstumsstufe " + "sind, als Anteil der Endgroesse. 1.0 = keine Skalierung. " + "Ein Setzling traegt kleinere Bueschel als der Altbaum"), ) - leaf_size: FloatProperty(name="Groesse", default=0.22, min=0.01, max=5.0) leaf_size_var: FloatProperty(name="Groessen-Streuung", default=0.35, min=0.0, max=1.0) leaf_max_radius: FloatProperty( - name="Nur Zweige duenner als", default=0.02, min=0.001, max=0.5, + name="Nur Zweige duenner als", default=0.04, min=0.001, max=0.5, description=("Blaetter nur dort, wo der Ast duenner als dieser Radius ist (m). " "Verhindert Blaetter am Stamm"), ) - leaf_tilt: FloatProperty(name="Kippung", default=0.5, min=0.0, max=3.14159) + leaf_tilt: FloatProperty( + name="Kippung", default=0.9, min=0.0, max=3.14159, + description=("Wie stark die Cards aus der Senkrechten kippen. Mehr " + "Rotations-Chaos schliesst Sichtluecken in der Krone"), + ) make_frucht: BoolProperty( name="_Frucht-Ebene anlegen", default=False, description="Zusaetzlich _Frucht (gleicher Ursprung) fuer die Frucht-Ebene", @@ -2350,9 +2418,23 @@ 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)) + faktor = s.leaf_size_growth + (1.0 - s.leaf_size_growth) * gt + # Dichte GEGENLAEUFIG mitziehen. "Dichte" ist Punkte pro m2 + # Zweigoberflaeche, und ein Setzling hat davon fast keine - + # gemessen kam bei Stufe 1 exakt NULL Cards heraus, der + # Setzling stand kahl da. Da die Bueschel um `faktor` kleiner + # sind, deckt jeder nur faktor^2 der Flaeche; die Dichte + # entsprechend anzuheben haelt die Blattdeckung ueber alle + # Stufen gleich, statt den Setzling auszuduennen. + dichte = s.leaf_density / max(faktor * faktor, 1e-3) lf = make_leaves(context, t, s.leaf_card, card_coll=s.leaf_cards, - Density=s.leaf_density, Size=s.leaf_size, + Density=dichte, + Size=s.leaf_size * faktor, **{"Size Var": s.leaf_size_var, "Max Radius": s.leaf_max_radius, "Tilt": s.leaf_tilt, "Seed": s.seed}) @@ -2374,7 +2456,14 @@ class TREEGEN_OT_leaves(Operator): total = total_tree + total_leaf msg = ("Blattwerk fuer %d Baum/Baeume: %d Stamm + %d Blatt = %d Tris" % (len(trees), total_tree, total_leaf, total)) - limit = s.leaf_budget * max(len(trees), 1) + # Budget PRO STUFE statt global (ASSETS.md Teil 6): S1 <= 800, + # S2 <= 1200, S3/S4 <= 4000. Bewusst STUFEN und keine Interpolation - + # so steht es in den Unterlagen, und ein Setzling steht nie im + # Vordergrund. Die Schwellen liegen so, dass sie auch bei 3 Stufen + # (t = 0 / 0.5 / 1.0) noch richtig greifen. + limit = 0 + for t in trees: + limit += _stufen_budget(s, float(t.get("growth_t", 1.0))) if total_leaf == 0: # Passiert regulaer beim Kaktus (kein Ast duenner als Max Radius) - # aber auch, wenn Max Radius fuer den Baum zu klein steht. Beides @@ -2570,10 +2659,14 @@ class VIEW3D_PT_tree_leaves(_TreeSub): col.active = s.leaf_card is not None or s.leaf_cards is not None col.prop(s, "leaf_density") col.prop(s, "leaf_size") + col.prop(s, "leaf_size_growth") col.prop(s, "leaf_size_var") col.prop(s, "leaf_max_radius") col.prop(s, "leaf_tilt") - col.prop(s, "leaf_budget") + row = col.row(align=True) + row.prop(s, "leaf_budget_s1", text="S1") + row.prop(s, "leaf_budget_s2", text="S2") + row.prop(s, "leaf_budget", text="S3/S4") layout.separator() layout.operator("object.treegen_leaves", icon='OUTLINER_OB_POINTCLOUD') if s.leaf_cards is not None: diff --git a/tests/test_blatt_bueschel.py b/tests/test_blatt_bueschel.py new file mode 100644 index 0000000..c560361 --- /dev/null +++ b/tests/test_blatt_bueschel.py @@ -0,0 +1,219 @@ +"""Blatt-Defaults fuer BUESCHEL-Atlanten (eine Card = ein ganzer Zweigbueschel). + +Die alten Werte (Size 0.22, Density 150-500) stammen aus der Blaettchen-Aera: +damit streute der Generator hunderte winziger Cards, wo ein paar Dutzend grosse +hingehoeren. Diese Datei nagelt die neuen Groessenordnungen fest und prueft die +Kopplung an die Wachstums-Stufen. + +Aufruf: blender --background --factory-startup --python tests/test_blatt_bueschel.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 + +# --- 1) Auslieferungszustand --------------------------------------------- +print("Defaults: Dichte %.0f, Groesse %.2f, Setzlings-Anteil %.2f" + % (s.leaf_density, s.leaf_size, s.leaf_size_growth)) +if not (0.6 <= s.leaf_size <= 1.1): + fails.append("leaf_size ist %.2f - fuer Bueschel-Atlanten werden ~0.85 " + "gebraucht, 0.2 waere ein einzelnes Blaettchen" % s.leaf_size) +if s.leaf_budget_s1 > 800 or s.leaf_budget_s2 > 1200 or s.leaf_budget > 4000: + fails.append("Stufen-Budgets (%d/%d/%d) ueber ASSETS.md Teil 6 (800/1200/4000)" + % (s.leaf_budget_s1, s.leaf_budget_s2, s.leaf_budget)) +if not (130.0 <= s.leaf_density <= 200.0): + fails.append("leaf_density ist %.0f - der Sweetspot liegt bei 130-170" + % s.leaf_density) +# Der Volumen-Hebel: bei 0.02 trugen nur die aeussersten Zweigspitzen Laub und +# die Krone war innen nackt. +if s.leaf_max_radius < 0.035: + fails.append("leaf_max_radius ist %.3f - unter 0.035 tragen nur die " + "Zweigspitzen Laub und die Krone bleibt innen nackt" + % s.leaf_max_radius) +if s.leaf_tilt < 0.7: + fails.append("leaf_tilt ist %.2f - unter 0.7 fehlt das Rotations-Chaos, " + "das die Sichtluecken schliesst" % s.leaf_tilt) +if abs(tg.LEAF_DEFAULTS["Size"] - s.leaf_size) > 0.2: + fails.append("LEAF_DEFAULTS['Size'] (%.2f) und die UI (%.2f) laufen " + "auseinander" % (tg.LEAF_DEFAULTS["Size"], s.leaf_size)) + + +def bauen(preset, wachstum=False, stufen=4): + for o in list(bpy.context.scene.objects): + if o is not card: + bpy.data.objects.remove(o, do_unlink=True) + s.preset = preset + s.art_name = preset.capitalize() + s.variant = 1 + s.apply_modifier = True + s.use_growth = wachstum + s.stages = stufen + s.leaf_card = card + 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) + 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() + return staemme + + +def messen(stamm): + bl = bpy.data.objects.get(stamm.name + "_Leaf") + if bl is None: + return 0, 0.0, 0 + dg = bpy.context.evaluated_depsgraph_get() + ev = bl.evaluated_get(dg) + m = ev.to_mesh() + cards = len(m.polygons) // 2 + kante = math.sqrt(sum(p.area for p in m.polygons) / cards) if cards else 0.0 + ev.to_mesh_clear() + ev2 = stamm.evaluated_get(dg) + m2 = ev2.to_mesh() + tris = sum(len(p.vertices) - 2 for p in m2.polygons) + cards * 2 + ev2.to_mesh_clear() + return cards, kante, tris + + +# --- 2) Alle Presets: Card-Zahl plausibel, Budget gehalten --------------- +print("") +# BUDGET: ASSETS.md nennt 2500 Tris fuer Baum (Stamm + Krone). Mit +# Bueschel-Atlanten liegt LOD0 darueber - das ist eine bewusste Entscheidung der +# Spielseite: der dichtere LOD0 traegt den Look aus der Naehe, und die LOD-Kette +# dampft ihn auf 16 % ein. Die harte Grenze liegt deshalb hier bei 3000; alles +# zwischen 2500 und 3000 wird als Hinweis ausgegeben, damit es nicht unbemerkt +# weiterwaechst. +# Die urspruenglichen 2500 waren eine konservative Setzung. Bei ISM-Instanzen +# mit LOD-Kette sind Roh-Dreiecke nicht der Engpass, sondern der OVERDRAW der +# Cards (die GPU zahlt pro Bildschirmflaeche, nicht pro Dreieck) und die +# Schattenpaesse. Genau deshalb sind 170 grosse Bueschel guenstiger als 500 +# kleine - bei gleichem Tri-Zaehler. Die Zahl hier ist die Hartgrenze fuer die +# ausgewachsene Stufe; die endgueltige Freigabe kommt aus einer Messung im +# Level (Testwald, stat unit / stat gpu), nicht aus diesem Test. +LOD0_HART = s.leaf_budget +LOD0_HINWEIS = int(s.leaf_budget * 0.8) +ueber = [] +print("%-12s %6s %11s %9s" % ("Preset", "Cards", "Card-Kante", "Tris ges")) +for p in tg.PRESETS: + staemme = bauen(p) + cards, kante, tris = messen(staemme[0]) + marke = "" + if tris > LOD0_HART: + marke = "UEBER HARTGRENZE" + elif tris > LOD0_HINWEIS: + marke = "ueber 2500 (LOD0 ok)" + ueber.append((p, tris)) + print("%-12s %6d %11.2f %9d %s" % (p, cards, kante, tris, marke)) + if tris > LOD0_HART: + fails.append("%s: %d Tris ueber der LOD0-Hartgrenze von %d" + % (p, tris, LOD0_HART)) + if p == "kaktus": + continue # kein Ast duenner als Max Radius - richtig so + if cards == 0: + fails.append("%s: keine Cards gestreut" % p) + # Ob es Bueschel oder Blaettchen sind, entscheidet die GROESSE, nicht die + # Anzahl: ein grosser Baum darf 600 Bueschel tragen. Die Menge deckelt das + # Tri-Budget oben ohnehin. + elif kante < 0.30: + fails.append("%s: Card-Kante nur %.2f m - das sind Blaettchen, keine " + "Bueschel" % (p, kante)) + +if ueber: + print("Hinweis: %s liegen ueber dem Stufenbudget - fuer LOD0 abgesegnet, bei " + "Bedarf ist die Dichte der Feinregler (130-170)." + % ", ".join("%s (%d)" % (p, t) for p, t in ueber)) + +# --- 3) Wachstum: Bueschel wachsen mit, Setzling ist NICHT kahl ---------- +staemme = bauen("baum", wachstum=True, stufen=4) +print("") +print("%-18s %6s %6s %11s" % ("Stufe", "t", "Cards", "Card-Kante")) +kanten, zahlen = [], [] +for st in staemme: + cards, kante, _ = messen(st) + gt = float(st.get("growth_t", 1.0)) + print("%-18s %6.2f %6d %11.2f" % (st.name, gt, cards, kante)) + kanten.append(kante) + zahlen.append(cards) + if cards == 0: + # Genau das war der Fehler: "Dichte" ist Punkte pro m2 Zweigflaeche, + # und ein Setzling hat davon fast keine. + fails.append("%s: keine Cards - ein Setzling darf nicht kahl sein" + % st.name) + +for i in range(1, len(kanten)): + if kanten[i] <= kanten[i - 1]: + fails.append("Stufe %d hat Card-Kante %.2f, Stufe %d hatte %.2f - die " + "Bueschel wachsen nicht mit" + % (i + 1, kanten[i], i, kanten[i - 1])) + +if kanten and kanten[0] > 0: + anteil = kanten[0] / kanten[-1] + print("Setzlings-Bueschel: %.0f%% der Endgroesse (Regler steht auf %.0f%%)" + % (100 * anteil, 100 * s.leaf_size_growth)) + # Der gemessene Anteil darf nicht ueber dem Regler liegen - dann haette die + # Skalierung nicht gegriffen. + if anteil > s.leaf_size_growth + 0.15: + fails.append("Setzlings-Bueschel sind %.0f%% der Endgroesse, der Regler " + "steht auf %.0f%% - die Wachstums-Kopplung greift nicht" + % (100 * anteil, 100 * s.leaf_size_growth)) + +# --- 3b) Budget je Stufe statt global ----------------------------------- +print("") +print("%-18s %6s %8s %8s" % ("Stufe", "t", "Tris", "Budget")) +for st in staemme: + cards, _, tris = messen(st) + gt = float(st.get("growth_t", 1.0)) + budget = tg._stufen_budget(s, gt) + print("%-18s %6.2f %8d %8d %s" + % (st.name, gt, tris, budget, "UEBER" if tris > budget else "")) + if tris > budget: + fails.append("%s: %d Tris ueber dem Stufenbudget %d - ein Setzling mit " + "Altbaum-Dichte ist Verschwendung" % (st.name, tris, budget)) +# Die Budgets muessen nach oben gehen, sonst ist die Staffelung sinnlos. +if not (s.leaf_budget_s1 < s.leaf_budget_s2 <= s.leaf_budget): + fails.append("Stufen-Budgets steigen nicht: %d / %d / %d" + % (s.leaf_budget_s1, s.leaf_budget_s2, s.leaf_budget)) + +# --- 4) Ohne Wachstum bleibt alles unveraendert ------------------------- +staemme = bauen("baum", wachstum=False) +cards_ohne, kante_ohne, _ = messen(staemme[0]) +print("") +print("Ohne Wachstum: %d Cards, Kante %.2f (growth_t=%.2f)" + % (cards_ohne, kante_ohne, float(staemme[0].get("growth_t", 1.0)))) +if abs(kante_ohne - kanten[-1]) > 0.02: + fails.append("Ein Baum ohne Wachstums-Stufen (%.2f) sieht anders aus als " + "die letzte Stufe (%.2f)" % (kante_ohne, kanten[-1])) + +print("") +if fails: + print("ERGEBNIS: %d FEHLER" % len(fails)) + for x in fails: + print(" - " + x) + sys.exit(1) +print("ERGEBNIS: ALLE CHECKS OK") diff --git a/tests/test_blatt_varianten.py b/tests/test_blatt_varianten.py index 79091c9..8cd0650 100644 --- a/tests/test_blatt_varianten.py +++ b/tests/test_blatt_varianten.py @@ -77,7 +77,10 @@ def streuen(collection, einzelcard, seed): dg = bpy.context.evaluated_depsgraph_get() ev = bl.evaluated_get(dg) me = ev.to_mesh() - flaechen = sorted({round(p.area, 5) for p in me.polygons}) + # Auf 4 Stellen runden, nicht auf 5: die Cards werden gedreht, und seit die + # Kippung bei 0.9 steht streuen die Flaechen in der sechsten Stelle. Mit + # 5 Stellen zerfiel eine einzige Card-Sorte in vier Eimer. + flaechen = sorted({round(p.area, 4) for p in me.polygons}) n = len(me.polygons) ev.to_mesh_clear() return n, flaechen