Tree Gen 1.36.0: Ursprung an der Stammbasis, Weide gedeckelt, Birke ueberarbeitet
1) Ursprung: Stammbasis statt tiefstem Slot-0-Punkt Zweimal war das schon "behoben". Erst kam der Ursprung aus der Gesamt-Bbox (eine Blatt-Card hob den Baum an), dann aus Material-Slot 0 - aber die AESTE gehoeren ebenfalls zum Rinden-Slot. Gemessen an Tanne_01_S1: tiefster Slot-0-Punkt -5.35 cm, tiefster STAMM-Punkt -0.02 cm. Die Stammbasis schwebte 5.3 cm ueber dem Boden. Die exakte Trennung liefert ast_id (0 = Stamm) - dasselbe Attribut, das der Baum fuer die Faell-Teile ohnehin traegt. Ein Schwellwert auf den Achsabstand faellt beim Kaktus um: dessen Stamm ist dicker als jeder sinnvolle Schwellwert. X/Y kommt jetzt aus dem untersten RING statt aus der Stamm-Bbox; bei Bend 1.5 liegt die Bbox-Mitte in der Kruemmung. Neu: tests/test_ursprung.py, gemessen am reimportierten FBX. Die erste Fassung des Tests suchte den "tiefsten Punkt nahe der Achse" - die Gegenprobe hat sie umgeworfen, weil bei der Tanne eine Astspitze ins Achsband faellt. Ein Schwellwert, der die Bugfassung durchlaesst, prueft nichts. Jetzt wird ein Stammquerschnitt gesucht: Vertices, die die Achse in allen Himmelsrichtungen umschliessen. Gegenprobe bestanden. Nebenbefund: ein Baum ohne Aeste traegt gar kein ast_id (Branch Count 0 schaltet den Ast-Zweig des Graphen ab). Dann ist alles Stamm und der Fallback exakt. 2) Weide S4 auf 350 Cards gedeckelt - Entscheidung des Users Gemessen 5456-5594 Tris ueber drei Seeds gegen 4300 Budget; nach der Deckelung 3890-3962. LEAF_STUFEN_PRESET haelt solche Ausnahmen je Preset. 3) Birke war eine Saeule Gemessen B/H 0.15 - schmaler als der Standardbaum (0.22), nicht die Haelfte der Eiche (0.36). Ursache: Branch Up 1.1, die Aeste klebten am Stamm. Zielband 0.35-0.55 aus Van den Berk (8-12 m bei 15-20 m) und Woodland Trust (6-10 m bei 15-25 m); jetzt 0.51. Haengende Zweigenden ueber Branch Droop 0.8 + Droop Curve 3.5, nicht ueber negatives Sub Up: das wurde von +2.2 bis -2.0 durchgemessen und bewegt nichts (1-6 %), weil die Sub-Aeste zu kurz sind. Mit Droop 26 % bei einem Fall von 0.033 x Hoehe - wie die Tanne (0.030), weit weg von der Weide (0.213). 4) run_tests.ps1 benennt Blender-Abstuerze test_export_robust starb rund in jedem sechsten Lauf mit EXCEPTION_ACCESS_VIOLATION und der Runner meldete "<keine Ergebniszeile>". Der Stack enthaelt keinen einzigen Addon-Frame (RNA_property_int_get -> rna_property_rna_or_id_get -> IDP_GetPropertyFromGroup, Null-Deref), und mit eingebauten Markern verschwindet der Absturz: Heisenbug in Blender 5.1.2. Der Runner wiederholt jetzt einmal und benennt den Absturz. 24 Tests bestehen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -563,6 +563,68 @@ 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
|
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.
|
einer leeren `_Leaf`-Ebene; jetzt sagt der Operator es und nennt den Regler.
|
||||||
|
|
||||||
|
### Der Ursprung sitzt an der Stammbasis, nicht am tiefsten Ast
|
||||||
|
|
||||||
|
Zweimal war dieser Fehler schon "behoben". Erst kam der Ursprung aus der
|
||||||
|
Gesamt-Bbox - eine tief haengende Blatt-Card hob den ganzen Baum an. Dann aus
|
||||||
|
Material-Slot 0, was die Cards ausnahm, aber die AESTE gehoeren ebenfalls zum
|
||||||
|
Rinden-Slot. Gemessen an Tanne_01_S1 (Aeste ab 6 % Stammhoehe, haengend): der
|
||||||
|
tiefste Slot-0-Punkt lag bei **-5.35 cm**, der tiefste STAMM-Punkt bei
|
||||||
|
**-0.02 cm**. Die Stammbasis schwebte 5.3 cm ueber dem Boden, weil eine
|
||||||
|
Astspitze den Nullpunkt bestimmte.
|
||||||
|
|
||||||
|
Die exakte Trennung liefert `ast_id` (0 = Stamm) - dasselbe Attribut, das der
|
||||||
|
Baum ohnehin fuer die Faell-Teile traegt. Ein Schwellwert auf den Achsabstand
|
||||||
|
waere die naheliegende Alternative und faellt beim Kaktus um: dessen Stamm ist
|
||||||
|
dicker als jeder sinnvolle Schwellwert. X/Y kommt aus dem UNTERSTEN RING statt
|
||||||
|
aus der Stamm-Bbox; bei einem krummen Stamm (Bend 1.5 am toten Baum) liegt die
|
||||||
|
Bbox-Mitte irgendwo in der Kruemmung.
|
||||||
|
|
||||||
|
`tests/test_ursprung.py` misst am reimportierten FBX. Die erste Fassung des
|
||||||
|
Tests suchte den "tiefsten Punkt nahe der Achse" - und die Gegenprobe hat sie
|
||||||
|
umgeworfen: bei der Tanne faellt eine Astspitze ins Achsband, der Test meldete
|
||||||
|
Fuss = 0, obwohl der Stamm 5 cm darueber schwebte. Ein Schwellwert, der die
|
||||||
|
Bugfassung durchlaesst, prueft nichts. Jetzt wird ein STAMMQUERSCHNITT gesucht:
|
||||||
|
Vertices, die die Achse in allen Himmelsrichtungen umschliessen. Eine Astspitze
|
||||||
|
tut das nicht.
|
||||||
|
|
||||||
|
Nebenbefund: ein Baum ohne Aeste traegt gar kein `ast_id` - das Attribut
|
||||||
|
entsteht auf dem Ast-Zweig des Graphen, und den schaltet `Branch Count 0`
|
||||||
|
komplett ab (Kaktus-Setzling). Dann ist ohnehin alles Stamm, der Fallback ist
|
||||||
|
also exakt und nicht bloss ausreichend.
|
||||||
|
|
||||||
|
### Weide: Ziel-Cardzahl je Preset
|
||||||
|
|
||||||
|
"Volle Dichte" auf S4 heisst: das Ergebnis haengt an der Zweigflaeche, und die
|
||||||
|
faellt je Art verschieden aus. Die Weide traegt die meiste und landete gemessen
|
||||||
|
bei 5456-5594 Tris ueber drei Seeds, gegen ein Stufenbudget von 4300. Mit der
|
||||||
|
Deckelung auf 350 Cards: 3890-3962 Tris.
|
||||||
|
|
||||||
|
`LEAF_STUFEN_PRESET` haelt solche Ausnahmen. Das macht die Tabelle
|
||||||
|
preset-abhaengig - bewusste Entscheidung der Spielseite, statt das Budget fuer
|
||||||
|
alle anzuheben: die Weide ist der Ausreisser, nicht die Regel.
|
||||||
|
|
||||||
|
### Birke: keine Saeule
|
||||||
|
|
||||||
|
Gemessen war die alte Birke Breite/Hoehe **0.15** - schmaler als der
|
||||||
|
Standardbaum (0.22) und nicht einmal die Haelfte der Eiche (0.36). Ursache war
|
||||||
|
`Branch Up` 1.1: die Aeste zeigten fast senkrecht nach oben und klebten am
|
||||||
|
Stamm.
|
||||||
|
|
||||||
|
Zielband aus zwei Quellen, die sich ueberlappen: Van den Berk nennt fuer
|
||||||
|
*Betula pendula* 15-20 m Hoehe bei 8-12 m Kronenbreite (B/H 0.40-0.80), der
|
||||||
|
Woodland Trust 15-25 m bei 6-10 m (0.24-0.67). Schnittmenge rund 0.35-0.55;
|
||||||
|
gemessen jetzt **0.51**.
|
||||||
|
|
||||||
|
Den Habitus nennt Van den Berk woertlich: "spreading branches have clearly
|
||||||
|
pendulous and thin twigs". Das ist der Palmen-Bogen im Kleinen - der Ast steigt
|
||||||
|
erst und kippt dann ab -, deshalb `Branch Droop` mit spaeter `Droop Curve`.
|
||||||
|
Negatives `Sub Up` waere der naheliegende Griff gewesen und bewegt hier nichts:
|
||||||
|
durchgemessen von +2.2 bis -2.0 blieb der Anteil haengender Vertices bei 1-6 %,
|
||||||
|
weil die Sub-Aeste zu kurz sind, um die Silhouette zu tragen. Mit Droop 0.8 sind
|
||||||
|
es 26 % bei einem Fall von 0.033 x Hoehe - vergleichbar der Tanne (0.030), weit
|
||||||
|
weg von der Weide (0.213). Die Zweige haengen, der Baum nicht.
|
||||||
|
|
||||||
### Die Node-Gruppe wird WIEDERVERWENDET, nicht neu gebaut
|
### Die Node-Gruppe wird WIEDERVERWENDET, nicht neu gebaut
|
||||||
|
|
||||||
`build_group()` loeschte die Gruppe `GN_Tree` bei jedem Erzeugen und legte sie
|
`build_group()` loeschte die Gruppe `GN_Tree` bei jedem Erzeugen und legte sie
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "stylized_tree_generator"
|
id = "stylized_tree_generator"
|
||||||
version = "1.35.0"
|
version = "1.36.0"
|
||||||
name = "Stylized Tree Generator"
|
name = "Stylized Tree Generator"
|
||||||
tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen"
|
tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen"
|
||||||
maintainer = "D4rkst3r"
|
maintainer = "D4rkst3r"
|
||||||
|
|||||||
Vendored
+3
-3
@@ -50,17 +50,17 @@
|
|||||||
<td>34.1KB</td>
|
<td>34.1KB</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><tt><a href="./stylized_tree_generator-1.35.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.35.0</a></tt></td>
|
<td><tt><a href="./stylized_tree_generator-1.36.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.36.0</a></tt></td>
|
||||||
<td>Stylized Tree Generator</td>
|
<td>Stylized Tree Generator</td>
|
||||||
<td>Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen</td>
|
<td>Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen</td>
|
||||||
<td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td>
|
<td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td>
|
||||||
<td>4.2.0 - ~</td>
|
<td>4.2.0 - ~</td>
|
||||||
<td>all</td>
|
<td>all</td>
|
||||||
<td>all</td>
|
<td>all</td>
|
||||||
<td>41.2KB</td>
|
<td>43.0KB</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<center><p>Built 2026-08-06, 09:08</p></center>
|
<center><p>Built 2026-08-06, 09:54</p></center>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Vendored
+4
-4
@@ -82,7 +82,7 @@
|
|||||||
"id": "stylized_tree_generator",
|
"id": "stylized_tree_generator",
|
||||||
"name": "Stylized Tree Generator",
|
"name": "Stylized Tree Generator",
|
||||||
"tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen",
|
"tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen",
|
||||||
"version": "1.35.0",
|
"version": "1.36.0",
|
||||||
"type": "add-on",
|
"type": "add-on",
|
||||||
"maintainer": "D4rkst3r",
|
"maintainer": "D4rkst3r",
|
||||||
"license": [
|
"license": [
|
||||||
@@ -98,9 +98,9 @@
|
|||||||
"Mesh",
|
"Mesh",
|
||||||
"Modeling"
|
"Modeling"
|
||||||
],
|
],
|
||||||
"archive_url": "./stylized_tree_generator-1.35.0.zip",
|
"archive_url": "./stylized_tree_generator-1.36.0.zip",
|
||||||
"archive_size": 42185,
|
"archive_size": 44058,
|
||||||
"archive_hash": "sha256:db4de9904c876c1336879fe4de7a183f1a1f2c7dc52c55e8278e0bf7e43bd323"
|
"archive_hash": "sha256:ef7577536d2ea895b0f601de020c99bd2bf060c15b60ba92ade46f4a625fae0b"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+27
-1
@@ -23,15 +23,38 @@ $tests = Get-ChildItem (Join-Path $root "tests") -Filter "test_$Filter.py" | Sor
|
|||||||
if (-not $tests) { throw "Keine Tests gefunden fuer Filter '$Filter'" }
|
if (-not $tests) { throw "Keine Tests gefunden fuer Filter '$Filter'" }
|
||||||
|
|
||||||
$failed = @()
|
$failed = @()
|
||||||
|
$abgestuerzt = @()
|
||||||
foreach ($t in $tests) {
|
foreach ($t in $tests) {
|
||||||
|
# Ein Lauf kann an einem BLENDER-ABSTURZ sterben statt an einem Testfehler.
|
||||||
|
# Nachgemessen an test_export_robust: rund jeder sechste Lauf endet mit
|
||||||
|
# EXCEPTION_ACCESS_VIOLATION - und zwar ohne einen einzigen Addon-Frame im
|
||||||
|
# Stack (RNA_property_int_get -> rna_property_rna_or_id_get ->
|
||||||
|
# IDP_GetPropertyFromGroup, Null-Deref beim Lesen einer Integer-Property).
|
||||||
|
# Baut man Marker in denselben Test ein, verschwindet der Absturz: ein
|
||||||
|
# Heisenbug, also Blender 5.1.2 und nicht unser Code. Wenn der Lauf
|
||||||
|
# durchlaeuft, bestehen ALLE Checks des Tests.
|
||||||
|
# Deshalb einmal wiederholen und den Absturz benennen, statt ihn als
|
||||||
|
# stillen Testfehler auszugeben - "<keine Ergebniszeile>" hat monatelang
|
||||||
|
# niemandem gesagt, was eigentlich los war.
|
||||||
$out = & $Blender --background --factory-startup --python $t.FullName 2>&1 | Out-String
|
$out = & $Blender --background --factory-startup --python $t.FullName 2>&1 | Out-String
|
||||||
|
$crash = $false
|
||||||
|
if ($out -match "EXCEPTION_ACCESS_VIOLATION") {
|
||||||
|
$crash = $true
|
||||||
|
$out = & $Blender --background --factory-startup --python $t.FullName 2>&1 | Out-String
|
||||||
|
$abgestuerzt += if ($out -match "EXCEPTION_ACCESS_VIOLATION") {
|
||||||
|
$t.BaseName + " (auch im zweiten Lauf)"
|
||||||
|
} else { $t.BaseName }
|
||||||
|
}
|
||||||
|
|
||||||
# Die Tests melden ihr Ergebnis als letzte Zeile mit "ERGEBNIS:" bzw.
|
# Die Tests melden ihr Ergebnis als letzte Zeile mit "ERGEBNIS:" bzw.
|
||||||
# "Alle Checks bestanden." - alles andere ist Blender-Rauschen.
|
# "Alle Checks bestanden." - alles andere ist Blender-Rauschen.
|
||||||
$line = ($out -split "`n" |
|
$line = ($out -split "`n" |
|
||||||
Where-Object { $_ -match "ERGEBNIS|Alle Checks bestanden|FEHLGESCHLAGEN" } |
|
Where-Object { $_ -match "ERGEBNIS|Alle Checks bestanden|FEHLGESCHLAGEN" } |
|
||||||
Select-Object -Last 1)
|
Select-Object -Last 1)
|
||||||
if (-not $line) { $line = "<keine Ergebniszeile>" }
|
if (-not $line) {
|
||||||
|
$line = if ($crash) { "<Blender abgestuerzt, auch im zweiten Lauf>" }
|
||||||
|
else { "<keine Ergebniszeile>" }
|
||||||
|
}
|
||||||
$line = $line.Trim()
|
$line = $line.Trim()
|
||||||
|
|
||||||
$ok = ($line -match "ALLE|OK$|bestanden") -and ($line -notmatch "FEHLER|FEHLGESCHLAGEN")
|
$ok = ($line -match "ALLE|OK$|bestanden") -and ($line -notmatch "FEHLER|FEHLGESCHLAGEN")
|
||||||
@@ -42,6 +65,9 @@ foreach ($t in $tests) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
if ($abgestuerzt) {
|
||||||
|
Write-Host ("Blender-Absturz (wiederholt): {0}" -f ($abgestuerzt -join ", ")) -ForegroundColor Yellow
|
||||||
|
}
|
||||||
if ($failed) {
|
if ($failed) {
|
||||||
Write-Host ("FEHLGESCHLAGEN ({0}): {1}" -f $failed.Count, ($failed -join ", ")) -ForegroundColor Red
|
Write-Host ("FEHLGESCHLAGEN ({0}): {1}" -f $failed.Count, ($failed -join ", ")) -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
+118
-27
@@ -1,7 +1,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Stylized Tree Generator",
|
"name": "Stylized Tree Generator",
|
||||||
"author": "D4rkst3r",
|
"author": "D4rkst3r",
|
||||||
"version": (1, 35, 0),
|
"version": (1, 36, 0),
|
||||||
"blender": (4, 2, 0),
|
"blender": (4, 2, 0),
|
||||||
"location": "View3D > Sidebar > Tree Gen",
|
"location": "View3D > Sidebar > Tree Gen",
|
||||||
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
|
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
|
||||||
@@ -159,13 +159,35 @@ PRESETS = {
|
|||||||
"Branch Bend": 0.9, "Branch Thickness": 0.24, "Sub Count": 3,
|
"Branch Bend": 0.9, "Branch Thickness": 0.24, "Sub Count": 3,
|
||||||
"Sub Length": 0.8, "Root Flare": 1.5, "Root Lobe Depth": 0.4,
|
"Sub Length": 0.8, "Root Flare": 1.5, "Root Lobe Depth": 0.4,
|
||||||
},
|
},
|
||||||
"birke": { # schlank, hoher Ansatz, feine Zweige
|
# Birke: schlanker Stamm, aber KEINE Saeule. Der vorherige Satz war eine -
|
||||||
|
# gemessen Breite/Hoehe 0.15, weniger als der Standardbaum (0.22) und nicht
|
||||||
|
# einmal die Haelfte der Eiche (0.36). Ursache war "Branch Up" 1.1: die
|
||||||
|
# Aeste zeigten fast senkrecht nach oben und klebten am Stamm.
|
||||||
|
#
|
||||||
|
# Zielband aus zwei Baumschul-/Fachquellen, die sich ueberlappen:
|
||||||
|
# Van den Berk nennt fuer Betula pendula 15-20 m Hoehe bei 8-12 m
|
||||||
|
# Kronenbreite (B/H 0.40-0.80), der Woodland Trust 15-25 m bei 6-10 m
|
||||||
|
# (0.24-0.67). Schnittmenge also rund 0.35-0.55; gemessen jetzt 0.45.
|
||||||
|
#
|
||||||
|
# Der Habitus steht bei Van den Berk woertlich: "spreading branches have
|
||||||
|
# clearly pendulous and thin twigs", Krone "irregularly oval to oblong,
|
||||||
|
# half-open". Das ist der Palmen-Bogen im Kleinen - der Ast steigt erst und
|
||||||
|
# kippt dann ab -, deshalb Branch Droop mit spaeter Droop Curve statt
|
||||||
|
# negativem "Sub Up". Sub Up wurde durchgemessen und bewegt hier nichts:
|
||||||
|
# von +2.2 bis -2.0 blieb der Anteil haengender Vertices bei 1-6 %, weil
|
||||||
|
# die Sub-Aeste zu kurz sind, um die Silhouette zu tragen. Mit Droop 0.8
|
||||||
|
# sind es 26 % bei einem Fall von 0.033 x Hoehe - vergleichbar der Tanne
|
||||||
|
# (0.030), weit weg von der Weide (0.213). Zweige haengen, der Baum nicht.
|
||||||
|
"birke": {
|
||||||
"Height": 7.5, "Trunk Radius": 0.09, "Bend": 0.7, "Taper": 0.92,
|
"Height": 7.5, "Trunk Radius": 0.09, "Bend": 0.7, "Taper": 0.92,
|
||||||
"Branch Count": 12, "Branch Length": 1.4, "Branch Up": 1.1,
|
"Branch Count": 16, "Branch Length": 2.1, "Branch Up": 0.55,
|
||||||
"Branch Start": 0.45, "Branch End": 0.98,
|
"Branch Start": 0.38, "Branch End": 0.98,
|
||||||
"Crown Taper": 0.1, "Crown Bulge": 0.25,
|
"Crown Taper": 0.1, "Crown Bulge": 0.25,
|
||||||
|
"Attraction Up": 0.15, # spreizen, nicht hochschwingen
|
||||||
|
"Branch Droop": 0.8, # Zweigenden kippen ab ...
|
||||||
|
"Droop Curve": 3.5, # ... erst im aeusseren Drittel
|
||||||
"Branch Bend": 0.8, "Branch Thickness": 0.11, "Sub Count": 3,
|
"Branch Bend": 0.8, "Branch Thickness": 0.11, "Sub Count": 3,
|
||||||
"Sub Length": 0.5, "Root Flare": 0.6,
|
"Sub Length": 0.7, "Root Flare": 0.6,
|
||||||
},
|
},
|
||||||
"weide": { # haengende Zweige (Droop positiv), breit ausladend
|
"weide": { # haengende Zweige (Droop positiv), breit ausladend
|
||||||
"Height": 5.0, "Trunk Radius": 0.18, "Bend": 0.8, "Taper": 0.8,
|
"Height": 5.0, "Trunk Radius": 0.18, "Bend": 0.8, "Taper": 0.8,
|
||||||
@@ -1245,6 +1267,10 @@ def make_object(ng, preset="baum", name=None, growth=None, seed=None):
|
|||||||
# spaeter und getrennt - er kann sonst nicht wissen, ob er einen Setzling
|
# 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.
|
# 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)))
|
ob["growth_t"] = 1.0 if growth is None else max(0.0, min(1.0, float(growth)))
|
||||||
|
# Das Preset ebenso: einzelne Arten brauchen eine eigene Ziel-Cardzahl
|
||||||
|
# (siehe LEAF_STUFEN_PRESET), und der Blattwerk-Operator sieht spaeter nur
|
||||||
|
# noch das fertige Objekt.
|
||||||
|
ob["preset"] = preset
|
||||||
|
|
||||||
bpy.context.view_layer.objects.active = ob
|
bpy.context.view_layer.objects.active = ob
|
||||||
return ob
|
return ob
|
||||||
@@ -1523,20 +1549,42 @@ LEAF_STUFEN = (
|
|||||||
(None, 1.00, 0.25), # S4 - volle Dichte, keine Regelung
|
(None, 1.00, 0.25), # S4 - volle Dichte, keine Regelung
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# AUSNAHMEN je Preset: {preset: {stufen_index: ziel_cards}}.
|
||||||
|
#
|
||||||
|
# "Volle Dichte" auf S4 heisst: das Ergebnis haengt an der Zweigflaeche, und die
|
||||||
|
# faellt je Art verschieden aus. Die WEIDE traegt die meiste - haengende Zweige
|
||||||
|
# mit Sub Length 0.9 und Sub Count 3 - und landete deshalb gemessen bei
|
||||||
|
# 5456/5558/5594 Tris ueber drei Seeds, gegen ein Stufenbudget von 4300.
|
||||||
|
# Deckelung auf 350 Cards: gemessen 361 Cards / 3818 Tris, damit im Budget.
|
||||||
|
#
|
||||||
|
# Das macht die Tabelle preset-abhaengig - bewusst so entschieden, statt das
|
||||||
|
# Budget fuer alle anzuheben: die Weide ist der Ausreisser, nicht die Regel.
|
||||||
|
LEAF_STUFEN_PRESET = {
|
||||||
|
"weide": {3: 350},
|
||||||
|
}
|
||||||
|
|
||||||
def _stufen_anteile(growth_t):
|
|
||||||
|
def _stufen_anteile(growth_t, preset=None):
|
||||||
"""Ziel-Cardzahl, Groessen- und Kronenansatz-Wert fuer einen Wachstums-Fortschritt.
|
"""Ziel-Cardzahl, Groessen- und Kronenansatz-Wert fuer einen Wachstums-Fortschritt.
|
||||||
|
|
||||||
Die Stufen liegen bei t = 0 / 1/3 / 2/3 / 1 (vier Stufen). Zwischen den
|
Die Stufen liegen bei t = 0 / 1/3 / 2/3 / 1 (vier Stufen). Zwischen den
|
||||||
Stuetzstellen wird linear gemischt, damit auch drei oder fuenf Stufen
|
Stuetzstellen wird linear gemischt, damit auch drei oder fuenf Stufen
|
||||||
sinnvolle Werte bekommen.
|
sinnvolle Werte bekommen.
|
||||||
|
|
||||||
|
`preset` zieht die Ausnahmen aus LEAF_STUFEN_PRESET; ohne Angabe gilt die
|
||||||
|
allgemeine Tabelle.
|
||||||
"""
|
"""
|
||||||
|
aus = LEAF_STUFEN_PRESET.get(preset or "", {})
|
||||||
|
|
||||||
|
def _ziel(i):
|
||||||
|
return aus.get(i, LEAF_STUFEN[i][0])
|
||||||
|
|
||||||
n = len(LEAF_STUFEN)
|
n = len(LEAF_STUFEN)
|
||||||
x = max(0.0, min(1.0, growth_t)) * (n - 1)
|
x = max(0.0, min(1.0, growth_t)) * (n - 1)
|
||||||
i = min(int(x), n - 2)
|
i = min(int(x), n - 2)
|
||||||
f = x - i
|
f = x - i
|
||||||
z0, g0, h0 = LEAF_STUFEN[i]
|
z0, g0, h0 = _ziel(i), LEAF_STUFEN[i][1], LEAF_STUFEN[i][2]
|
||||||
z1, g1, h1 = LEAF_STUFEN[i + 1]
|
z1, g1, h1 = _ziel(i + 1), LEAF_STUFEN[i + 1][1], LEAF_STUFEN[i + 1][2]
|
||||||
# Groesse und Kronenansatz werden gemischt, die Ziel-ANZAHL nicht: sie ist
|
# Groesse und Kronenansatz werden gemischt, die Ziel-ANZAHL nicht: sie ist
|
||||||
# eine Stueckzahl, kein Verlauf. Es gilt die naeher liegende Stuetzstelle.
|
# eine Stueckzahl, kein Verlauf. Es gilt die naeher liegende Stuetzstelle.
|
||||||
ziel = z0 if f < 0.5 else z1
|
ziel = z0 if f < 0.5 else z1
|
||||||
@@ -2369,6 +2417,36 @@ def _deselect(context):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _stamm_koordinaten(me):
|
||||||
|
"""Koordinaten der reinen STAMM-Vertices, mit absteigender Genauigkeit.
|
||||||
|
|
||||||
|
1. "ast_id" == 0 - exakt, kommt aus dem Generator selbst.
|
||||||
|
2. Material-Slot 0 - Rinde inklusive Aeste, immerhin ohne Blatt-Cards.
|
||||||
|
3. alles - wenn das Mesh weder Attribut noch zweiten Slot hat.
|
||||||
|
|
||||||
|
Stufe 2 und 3 sind Notnaegel fuer fremde oder vereinfachte Meshes; jede
|
||||||
|
Stufe weiter unten ist ungenauer, aber keine faellt aus.
|
||||||
|
"""
|
||||||
|
slot0 = None
|
||||||
|
if len(me.materials) > 1:
|
||||||
|
idx = {vi for p in me.polygons if p.material_index == 0
|
||||||
|
for vi in p.vertices}
|
||||||
|
if idx:
|
||||||
|
slot0 = idx
|
||||||
|
|
||||||
|
attr = me.attributes.get("ast_id")
|
||||||
|
if attr is not None and attr.domain == 'POINT' and len(attr.data) == len(me.vertices):
|
||||||
|
idx = {i for i in range(len(me.vertices)) if attr.data[i].value == 0}
|
||||||
|
if slot0 is not None:
|
||||||
|
idx &= slot0
|
||||||
|
if idx:
|
||||||
|
return [me.vertices[i].co for i in idx]
|
||||||
|
|
||||||
|
if slot0:
|
||||||
|
return [me.vertices[i].co for i in slot0]
|
||||||
|
return [v.co for v in me.vertices]
|
||||||
|
|
||||||
|
|
||||||
def _origin_bottom_center(ob):
|
def _origin_bottom_center(ob):
|
||||||
"""Ursprung auf Bbox-Unterseite Mitte - identisch zur Fels-Pipeline.
|
"""Ursprung auf Bbox-Unterseite Mitte - identisch zur Fels-Pipeline.
|
||||||
|
|
||||||
@@ -2383,27 +2461,39 @@ def _origin_bottom_center(ob):
|
|||||||
if not me.vertices:
|
if not me.vertices:
|
||||||
return Matrix.Identity(4)
|
return Matrix.Identity(4)
|
||||||
|
|
||||||
# Der Ursprung kommt aus der STAMM-Geometrie (Slot 0), nicht aus der
|
# Der Ursprung kommt aus der STAMMBASIS, nicht aus einer Bbox.
|
||||||
# Gesamt-Bbox. Sonst hebt eine einzige tief haengende Blatt-Card den ganzen
|
#
|
||||||
# Baum an und er steht in UE in der Luft - gemeldet am S1-Setzling, wo die
|
# Zwei Stufen dieses Fehlers gab es schon. Zuerst die Gesamt-Bbox: eine
|
||||||
# Krone relativ zum kurzen Stamm weit ausladet.
|
# einzige tief haengende Blatt-Card hob den ganzen Baum an. Dann Slot 0 -
|
||||||
stamm_verts = None
|
# aber zum Rinden-Slot gehoeren auch die AESTE. Gemessen an Tanne_01_S1
|
||||||
if len(me.materials) > 1:
|
# (haengende Aeste ab 6 % Stammhoehe): der tiefste Slot-0-Punkt lag bei
|
||||||
idx = {vi for p in me.polygons if p.material_index == 0
|
# -5.35 cm, der tiefste Stammpunkt bei -0.02 cm. Die Stammbasis schwebte
|
||||||
for vi in p.vertices}
|
# also 5.3 cm ueber dem Boden, weil eine Astspitze den Nullpunkt bestimmte.
|
||||||
if idx:
|
#
|
||||||
stamm_verts = [me.vertices[i].co for i in idx]
|
# Die exakte Trennung liefert das Attribut "ast_id" (0 = Stamm), das der
|
||||||
if stamm_verts is None:
|
# Baum ohnehin fuer die Faell-Teile traegt. Ein Schwellwert auf den
|
||||||
stamm_verts = [v.co for v in me.vertices]
|
# Achsabstand waere die naheliegende Alternative und faellt beim Kaktus um:
|
||||||
|
# dessen Stamm ist dicker als jeder sinnvolle Schwellwert.
|
||||||
|
stamm_verts = _stamm_koordinaten(me)
|
||||||
|
|
||||||
|
zs = [c.z for c in stamm_verts]
|
||||||
|
z_fuss = min(zs)
|
||||||
|
# X/Y aus dem UNTERSTEN RING, nicht aus der Bbox des ganzen Stammes: ein
|
||||||
|
# gekruemmter Stamm (Bend 1.5 beim toten Baum) hat seine Bbox-Mitte
|
||||||
|
# irgendwo in der Kruemmung, nicht am Fuss. Der Schwerpunkt des Ringes ist
|
||||||
|
# die Stammachse.
|
||||||
|
hoehe = max(zs) - z_fuss
|
||||||
|
band = [c for c in stamm_verts if c.z <= z_fuss + max(1e-4, hoehe * 0.02)]
|
||||||
|
if band:
|
||||||
|
mx = sum(c.x for c in band) / len(band)
|
||||||
|
my = sum(c.y for c in band) / len(band)
|
||||||
|
else:
|
||||||
xs = [c.x for c in stamm_verts]
|
xs = [c.x for c in stamm_verts]
|
||||||
ys = [c.y for c in stamm_verts]
|
ys = [c.y for c in stamm_verts]
|
||||||
zs = [c.z for c in stamm_verts]
|
mx = (min(xs) + max(xs)) * 0.5
|
||||||
versatz = Matrix.Translation((
|
my = (min(ys) + max(ys)) * 0.5
|
||||||
-(min(xs) + max(xs)) * 0.5,
|
|
||||||
-(min(ys) + max(ys)) * 0.5,
|
versatz = Matrix.Translation((-mx, -my, -z_fuss))
|
||||||
-min(zs),
|
|
||||||
))
|
|
||||||
me.transform(versatz)
|
me.transform(versatz)
|
||||||
return versatz
|
return versatz
|
||||||
|
|
||||||
@@ -2773,7 +2863,8 @@ class TREEGEN_OT_leaves(Operator):
|
|||||||
# traegt kleinere Bueschel. Baeume ohne Wachstums-Stufe haben
|
# traegt kleinere Bueschel. Baeume ohne Wachstums-Stufe haben
|
||||||
# growth_t = 1.0 und bleiben unveraendert.
|
# growth_t = 1.0 und bleiben unveraendert.
|
||||||
gt = float(t.get("growth_t", 1.0))
|
gt = float(t.get("growth_t", 1.0))
|
||||||
ziel_cards, g_anteil, h_anteil = _stufen_anteile(gt)
|
ziel_cards, g_anteil, h_anteil = _stufen_anteile(
|
||||||
|
gt, t.get("preset"))
|
||||||
|
|
||||||
# Der RADIUS-FILTER muss mit dem Baum schrumpfen. Er trennt
|
# Der RADIUS-FILTER muss mit dem Baum schrumpfen. Er trennt
|
||||||
# duenne Zweige (Laub) von dickem Geaest (kein Laub) - aber ein
|
# duenne Zweige (Laub) von dickem Geaest (kein Laub) - aber ein
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
"""Der Ursprung sitzt an der STAMMBASIS, nicht an der tiefsten Astspitze.
|
||||||
|
|
||||||
|
Gemeldet von der Spielseite an Tanne_01_S1: der Baum "liegt am Ast" - die
|
||||||
|
Stammbasis schwebt sichtbar ueber dem Boden. Nachgemessen an der Export-Kopie
|
||||||
|
lag der tiefste Punkt des Rinden-Slots bei -5.35 cm, der tiefste STAMM-Punkt
|
||||||
|
bei -0.02 cm. Die Tanne setzt ihre Aeste ab 6 % Stammhoehe an und laesst sie
|
||||||
|
haengen; eine davon bestimmte den Nullpunkt und hob den ganzen Baum an.
|
||||||
|
|
||||||
|
Der frueher gebaute Fix nahm nur die Blatt-Cards aus der Rechnung. Das reicht
|
||||||
|
nicht: zum Rinden-Slot gehoeren die Aeste mit.
|
||||||
|
|
||||||
|
Warum die Tanne der Pruefling ist: am Standardbaum sitzen die Aeste hoch und
|
||||||
|
haengen nicht - dort waere der Fehler unsichtbar geblieben, genau wie er es
|
||||||
|
monatelang war.
|
||||||
|
|
||||||
|
Gemessen wird im REIMPORTIERTEN FBX, nicht in Blender. Der Schwellwert fuer
|
||||||
|
"nahe der Achse" wird nicht geraten, sondern vor dem Export am echten Stamm
|
||||||
|
abgenommen und ueber die Hoehe skalenfrei gemacht.
|
||||||
|
|
||||||
|
Aufruf: blender --background --factory-startup --python tests/test_ursprung.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)
|
||||||
|
|
||||||
|
|
||||||
|
def stamm_mass(ob):
|
||||||
|
"""Fussradius und Hoehe des reinen Stammes - ueber ast_id, nicht geschaetzt.
|
||||||
|
|
||||||
|
Ein Schwellwert auf den Achsabstand waere die naheliegende Alternative und
|
||||||
|
faellt beim Kaktus um: dessen Stamm ist dicker als jeder sinnvolle
|
||||||
|
Schwellwert. Das Attribut kommt aus dem Generator und ist exakt.
|
||||||
|
"""
|
||||||
|
dg = bpy.context.evaluated_depsgraph_get()
|
||||||
|
ev = ob.evaluated_get(dg)
|
||||||
|
m = ev.to_mesh()
|
||||||
|
attr = m.attributes.get("ast_id")
|
||||||
|
if attr is None:
|
||||||
|
# Nachgemessen: ein Baum OHNE Aeste traegt gar kein ast_id - das
|
||||||
|
# Attribut entsteht auf dem Ast-Zweig des Graphen, und den schaltet
|
||||||
|
# "Branch Count 0" komplett ab (Kaktus-Setzling: keine Arme vor 50-70
|
||||||
|
# Jahren, NPS). Dann ist aber ohnehin alles Stamm, und der Fallback ist
|
||||||
|
# exakt statt bloss ausreichend.
|
||||||
|
co = [v.co.copy() for v in m.vertices]
|
||||||
|
else:
|
||||||
|
co = [m.vertices[i].co.copy() for i in range(len(m.vertices))
|
||||||
|
if attr.data[i].value == 0]
|
||||||
|
alle_z = [v.co.z for v in m.vertices]
|
||||||
|
ev.to_mesh_clear()
|
||||||
|
if not co:
|
||||||
|
return None
|
||||||
|
z0 = min(c.z for c in co)
|
||||||
|
h = max(alle_z) - min(alle_z)
|
||||||
|
band = [c for c in co if c.z <= z0 + h * 0.02]
|
||||||
|
r = max((math.hypot(c.x, c.y) for c in band), default=0.0)
|
||||||
|
return r, h, z0
|
||||||
|
|
||||||
|
|
||||||
|
ergebnisse = []
|
||||||
|
for preset, stufe in (("tanne", 1), ("tanne", 4), ("toter_baum", 1),
|
||||||
|
("weide", 1), ("kaktus", 1)):
|
||||||
|
for o in list(bpy.context.scene.objects):
|
||||||
|
if o is not card:
|
||||||
|
bpy.data.objects.remove(o, do_unlink=True)
|
||||||
|
# Eigenes Verzeichnis je Fall: reimportiert wird erst nach der Schleife,
|
||||||
|
# ein gemeinsamer Ordner wuerde die vorherigen Dateien ueberschreiben.
|
||||||
|
ziel_dir = os.path.join(OUT, "%s_s%d" % (preset, stufe))
|
||||||
|
os.makedirs(ziel_dir, exist_ok=True)
|
||||||
|
|
||||||
|
s = bpy.context.scene.tree_gen_settings
|
||||||
|
s.preset = preset
|
||||||
|
s.art_name = preset.capitalize()
|
||||||
|
s.variant = 1
|
||||||
|
s.apply_modifier = True
|
||||||
|
s.use_growth = True
|
||||||
|
s.stages = 4
|
||||||
|
s.export_dir = ziel_dir
|
||||||
|
s.generate_lods = False
|
||||||
|
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)
|
||||||
|
baum = staemme[stufe - 1]
|
||||||
|
|
||||||
|
# Der Baum steht BEWUSST nicht am Ursprung - eine verschobene Stufe faellt
|
||||||
|
# bei (0,0,0) nicht auf, weil minus null eben null ist.
|
||||||
|
baum.location = (12.0, -4.0, 0.0)
|
||||||
|
mass = stamm_mass(baum)
|
||||||
|
if mass is None:
|
||||||
|
fails.append("%s S%d: kein ast_id am Stamm" % (preset, stufe))
|
||||||
|
continue
|
||||||
|
r_stamm, h_blender, _ = mass
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 S%d: kein FBX geschrieben" % (preset, stufe))
|
||||||
|
continue
|
||||||
|
pfad = os.path.join(ziel_dir, dateien[0])
|
||||||
|
ergebnisse.append((preset, stufe, pfad, r_stamm / h_blender))
|
||||||
|
|
||||||
|
print("%-12s %3s %9s %11s %11s %10s"
|
||||||
|
% ("Preset", "St", "Achs-R", "Stammfuss", "tiefster Pkt", "Achs-Mitte"))
|
||||||
|
|
||||||
|
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']
|
||||||
|
if not meshes:
|
||||||
|
fails.append("%s S%d: kein Mesh im FBX" % (preset, stufe))
|
||||||
|
continue
|
||||||
|
ob = meshes[0]
|
||||||
|
m = ob.data
|
||||||
|
co = [ob.matrix_world @ v.co for v in m.vertices]
|
||||||
|
zlo = min(c.z for c in co)
|
||||||
|
zhi = max(c.z for c in co)
|
||||||
|
h = zhi - zlo
|
||||||
|
|
||||||
|
# Gesucht ist die tiefste Hoehe, auf der ein STAMMQUERSCHNITT liegt.
|
||||||
|
#
|
||||||
|
# Ein blosses "tiefster Punkt nahe der Achse" reicht nicht - das ist die
|
||||||
|
# erste Fassung dieses Tests gewesen, und die Gegenprobe hat sie umgeworfen:
|
||||||
|
# bei der Tanne haengt ein Ast so dicht am Stamm, dass seine Spitze ins
|
||||||
|
# Achsband faellt. Der Test meldete Fuss = 0, obwohl der Stamm 5 cm darueber
|
||||||
|
# schwebte. Ein Threshold, der die Bugfassung durchlaesst, prueft nichts.
|
||||||
|
#
|
||||||
|
# Ein Querschnitt unterscheidet sich von einer Astspitze in zwei messbaren
|
||||||
|
# Punkten: er umschliesst die Achse (Vertices in ALLEN Himmelsrichtungen)
|
||||||
|
# und er ist kompakt (kein Vertex weit draussen). Beides ohne geratene
|
||||||
|
# Konstante: der Vergleichsradius kommt aus dem echten Stamm.
|
||||||
|
r_max = r_rel * h * 2.5
|
||||||
|
SEKTOREN = 8
|
||||||
|
|
||||||
|
def querschnitt(zu, zo):
|
||||||
|
# Erst auf das Achsband einschraenken, DANN auf einen Ring pruefen.
|
||||||
|
# Andersherum (Slab verwerfen, sobald irgendein Punkt weit draussen
|
||||||
|
# liegt) faellt der echte Stammfuss durch: auf seiner Hoehe zieht bei
|
||||||
|
# der Tanne ein haengender Ast vorbei.
|
||||||
|
pts = [c for c in co
|
||||||
|
if zu <= c.z <= zo and math.hypot(c.x, c.y) <= r_max]
|
||||||
|
if len(pts) < SEKTOREN:
|
||||||
|
return None
|
||||||
|
sek = {int((math.atan2(c.y, c.x) + math.pi) / (2 * math.pi) * SEKTOREN)
|
||||||
|
% SEKTOREN for c in pts}
|
||||||
|
if len(sek) < SEKTOREN - 2:
|
||||||
|
return None # einseitig -> Zweig, kein Ring
|
||||||
|
# Schwerpunkt aus einem DICKEREN Band als dem Suchschritt. Nachgemessen
|
||||||
|
# am Kaktus: ein 1-%-Band erwischt nur 11 der 16 Ring-Vertices (die
|
||||||
|
# Rippen legen den Ring in Z leicht schief) und der Schwerpunkt kippt um
|
||||||
|
# 19 % der Hoehe aus der Achse. Mit 4 % ist der Ring vollstaendig und
|
||||||
|
# der Schwerpunkt exakt 0.0000 - geprueft an Kaktus, totem Baum, Tanne.
|
||||||
|
ring = [c for c in co
|
||||||
|
if zu <= c.z <= zu + h * 0.04
|
||||||
|
and math.hypot(c.x, c.y) <= r_max]
|
||||||
|
return (sum(c.x for c in ring) / len(ring),
|
||||||
|
sum(c.y for c in ring) / len(ring))
|
||||||
|
|
||||||
|
dicke = h * 0.01
|
||||||
|
fuss = None
|
||||||
|
mitte = (0.0, 0.0)
|
||||||
|
# Ueberlappende Schritte: mit buendigen Slabs kann eine Slab-Grenze mitten
|
||||||
|
# durch den Ring laufen, dann haelt keine der beiden Haelften die
|
||||||
|
# Sektor-Pruefung und der Fuss wird gar nicht gefunden. Halber Schritt.
|
||||||
|
z = zlo
|
||||||
|
while z < zhi:
|
||||||
|
tr = querschnitt(z, z + dicke)
|
||||||
|
if tr is not None:
|
||||||
|
fuss, mitte = z, tr
|
||||||
|
break
|
||||||
|
z += dicke * 0.5
|
||||||
|
if fuss is None:
|
||||||
|
# Kein Ring innerhalb r_max ueber die ganze Hoehe: der Stamm steht
|
||||||
|
# nicht ueber dem Ursprung. Genau das passiert mit der alten Fassung -
|
||||||
|
# dort bestimmte eine haengende Astspitze den Nullpunkt, und zwar in
|
||||||
|
# allen drei Achsen, nicht nur in Z. Der Baum haengt dann an der
|
||||||
|
# Astspitze statt auf dem Fuss zu stehen.
|
||||||
|
fails.append("%s S%d: auf keiner Hoehe ein Stammquerschnitt im "
|
||||||
|
"Achsband (R %.3f) - der Stamm steht seitlich versetzt "
|
||||||
|
"zum Ursprung" % (preset, stufe, r_max))
|
||||||
|
continue
|
||||||
|
achs = math.hypot(*mitte)
|
||||||
|
|
||||||
|
print("%-12s S%-2d %9.4f %11.4f %11.4f %10.4f"
|
||||||
|
% (preset, stufe, r_max, fuss, zlo, achs))
|
||||||
|
|
||||||
|
# DAS ist der Punkt: die Stammbasis liegt auf Z = 0.
|
||||||
|
if abs(fuss) > h * 0.01:
|
||||||
|
fails.append("%s S%d: Stammfuss bei Z %.4f statt 0 (%.1f %% der Hoehe) "
|
||||||
|
"- der Baum schwebt bzw. steckt im Boden"
|
||||||
|
% (preset, stufe, fuss, 100 * abs(fuss) / h))
|
||||||
|
# Und der Stamm steht ueber dem Ursprung, nicht daneben. Ohne diese Zeile
|
||||||
|
# koennte die Bbox-Mitte den Pivot bei einem einseitigen Baum vom Stamm
|
||||||
|
# wegziehen - derselbe Fehler, nur in X/Y.
|
||||||
|
if achs > h * 0.02:
|
||||||
|
fails.append("%s S%d: Stammachse %.4f neben dem Ursprung (%.1f %% der "
|
||||||
|
"Hoehe)" % (preset, stufe, achs, 100 * achs / h))
|
||||||
|
|
||||||
|
print("")
|
||||||
|
if fails:
|
||||||
|
print("ERGEBNIS: %d FEHLER" % len(fails))
|
||||||
|
for f in fails:
|
||||||
|
print(" - " + f)
|
||||||
|
sys.exit(1)
|
||||||
|
print("ERGEBNIS: ALLE CHECKS OK")
|
||||||
Reference in New Issue
Block a user