Refactoring Phase 2: Vegetation & EzTree aus EditorApp extrahiert

- Neue Infrastruktur: ToolController, ToolRegistry, ToolContext (record)
- VegetationController: alle ~1000 Z. Vegetation-State + Panels ausgelagert
  (Baum, Farn, Palme, Fruchtbusch, Weinpflanze, Trauerweide)
- EzTreeController: EzTree-State, Toolbar, Panel + buildLevelSection()
- UiHelper: shared static UI-Helpers (bold, sectionTitle, ezFloat, …)
- EditorApp: 14 222 → 13 187 Zeilen (-1035); switchToVegetationGenerator()
  und switchToEzTree() delegieren an registry.activate()
- F5/F6: hybrides Routing – migrierte Tools über ToolController.onPreview()
  / onRandomSeed(), nicht-migrierte über bestehende Runnable-Felder
- AnimationTimer: treePreviewResized + treeGenStatusMsg → registry.onFrame()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 21:23:46 +02:00
parent 1b509c3a52
commit e4b0ee0dc3
7 changed files with 1443 additions and 1063 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
package de.blight.editor;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import java.nio.file.Path;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* Shared-Dependencies-Container: wird einmalig in EditorApp erzeugt und
* an alle {@link ToolController}-Implementierungen weitergegeben.
*
* Ersetzt das implizite "EditorApp kennt alles" — Controller greifen nur
* über diesen Record auf Infrastruktur zu, nicht auf EditorApp direkt.
*
* @param input Gemeinsame JavaFX ↔ JME3 Datenbasis
* @param jmeApp JME3-Anwendung (für StateManager-Zugriff)
* @param stageSupplier Liefert die primäre Stage (für Dialoge)
* @param setStatus Setzt den Text im Statusbalken
* @param setCenterView Setzt den Center-Node des Haupt-BorderPane
* @param setRightPanel Setzt den Right-Node des Haupt-BorderPane
* @param setMainToolbar Ersetzt die aktive Werkzeug-Toolbar in der TopBar
* @param viewportSupplier Liefert den JME3-Viewport-StackPane
* @param assetRoot Wurzelpfad der blight-assets (für Dateidialoge / IO)
* @param switchToWorld Wechselt zurück zum Welteneditor (für "← Welteneditor"-Buttons)
*/
public record ToolContext(
SharedInput input,
JmeEditorApp jmeApp,
Supplier<Stage> stageSupplier,
Consumer<String> setStatus,
Consumer<Node> setCenterView,
Consumer<Node> setRightPanel,
Consumer<Node> setMainToolbar,
Supplier<StackPane> viewportSupplier,
Path assetRoot,
Runnable switchToWorld
) {}

View File

@@ -0,0 +1,42 @@
package de.blight.editor;
/**
* Gemeinsame Schnittstelle für alle Editor-Werkzeug-Controller.
*
* Jeder Controller verwaltet seinen eigenen UI-Zustand (Felder, Panels),
* setzt beim Aktivieren Toolbar + Panel und delegiert sein
* AnimationTimer-Polling über {@link #onFrame()}.
*
* Nicht zu verwechseln mit {@link de.blight.editor.tool.EditorTool}, das
* die Terrain-Brush-Parameter-Abstraktion ist.
*/
public interface ToolController {
/** Eindeutige Werkzeug-ID (z. B. "vegetation", "object") — für Debugging. */
String toolId();
/**
* Aktiviert das Werkzeug: setzt Toolbar, rechtes Panel, Center-View
* und ggf. {@code SharedInput.activeLayer}.
*/
void activate();
/**
* Deaktiviert das Werkzeug: räumt temporäre JME-Zustände oder UI-Artefakte auf.
* Wird vor jedem {@link #activate()} eines anderen Werkzeugs aufgerufen.
*/
void deactivate();
/**
* Wird jeden JavaFX-AnimationTimer-Frame aufgerufen (nur solange aktiv).
* Hier landen alle SharedInput-Polling-Aufgaben, die früher in EditorApps
* AnimationTimer standen.
*/
void onFrame();
/** F5-Taste: Vorschau/Preview-Aktion (nur wenn dieses Tool aktiv ist). */
default void onPreview() {}
/** F6-Taste: Zufälligen Seed wählen (nur wenn dieses Tool aktiv ist). */
default void onRandomSeed() {}
}

View File

@@ -0,0 +1,48 @@
package de.blight.editor;
/**
* Verwaltet das aktive Werkzeug und leitet den AnimationTimer-Frame weiter.
*
* EditorApp hält eine Instanz, verdrahtet die Toolbar-Buttons damit und ruft
* {@link #onFrame()} im AnimationTimer auf.
*/
public class ToolRegistry {
private ToolController active = null;
/**
* Aktiviert {@code tool}: deaktiviert das bisher aktive (falls vorhanden),
* setzt {@code tool} als aktiv und ruft {@link ToolController#activate()} auf.
* Idempotent wenn dasselbe Tool nochmals übergeben wird.
*/
public void activate(ToolController tool) {
if (tool == active) {
return;
}
if (active != null) {
active.deactivate();
}
active = tool;
tool.activate();
}
/** Gibt das aktuell aktive Werkzeug zurück, oder {@code null}. */
public ToolController getActive() {
return active;
}
/** Gibt die Tool-ID des aktiven Werkzeugs zurück, oder {@code ""} wenn keines aktiv. */
public String activeToolId() {
return active != null ? active.toolId() : "";
}
/**
* Muss jeden JavaFX-AnimationTimer-Frame aufgerufen werden.
* Delegiert an {@link ToolController#onFrame()} des aktiven Werkzeugs.
*/
public void onFrame() {
if (active != null) {
active.onFrame();
}
}
}

View File

@@ -0,0 +1,89 @@
package de.blight.editor.toolset;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.control.Spinner;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import java.util.function.Consumer;
public final class UiHelper {
private UiHelper() {}
public static Label bold(String text) {
Label l = new Label(text);
l.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
return l;
}
public static Label styledHint(String text) {
Label l = new Label(text);
l.setStyle("-fx-text-fill: #333333; -fx-font-size: 11;");
return l;
}
public static Label sectionTitle(String text) {
Label l = new Label(text);
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #1a3a6a;");
VBox.setMargin(l, new Insets(8, 0, 0, 0));
return l;
}
public static HBox withField(Slider slider, String fmt) {
slider.setMaxWidth(Double.MAX_VALUE);
TextField tf = new TextField(String.format(fmt, slider.getValue()));
tf.setPrefWidth(62);
tf.setMaxWidth(62);
tf.setStyle("-fx-font-size: 11;");
slider.valueProperty().addListener((o, ov, nv) ->
tf.setText(String.format(fmt, nv.doubleValue())));
Runnable commit = () -> {
try {
double v = Math.max(slider.getMin(), Math.min(slider.getMax(),
Double.parseDouble(tf.getText().replace(',', '.'))));
slider.setValue(v);
} catch (NumberFormatException ignored) {
tf.setText(String.format(fmt, slider.getValue()));
}
};
tf.setOnAction(e -> commit.run());
tf.focusedProperty().addListener((o, ov, focused) -> { if (!focused) commit.run(); });
HBox row = new HBox(5, slider, tf);
HBox.setHgrow(slider, Priority.ALWAYS);
return row;
}
public static VBox paramSlider(String label, double min, double max, double init,
Consumer<Float> setter) {
Label name = new Label(label);
name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
Slider slider = new Slider(min, max, init);
slider.setShowTickMarks(true);
slider.setShowTickLabels(true);
slider.setMajorTickUnit((max - min) / 2.0);
slider.valueProperty().addListener((o, a, b) -> setter.accept(b.floatValue()));
return new VBox(3, name, withField(slider, "%.3f"));
}
public static Spinner<Integer> intSpinner(int min, int max, int init) {
Spinner<Integer> sp = new Spinner<>(min, max, Math.max(min, Math.min(max, init)));
sp.setEditable(true);
sp.setMaxWidth(Double.MAX_VALUE);
return sp;
}
public static VBox ezFloat(String label, double min, double max, double init,
Consumer<Float> setter) {
Label name = new Label(label);
name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
Slider slider = new Slider(min, max, Math.max(min, Math.min(max, init)));
slider.setShowTickMarks(false);
slider.valueProperty().addListener((o, a, b) -> setter.accept(b.floatValue()));
return new VBox(2, name, withField(slider, "%.3f"));
}
}

View File

@@ -0,0 +1,364 @@
package de.blight.editor.toolset.eztree;
import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext;
import de.blight.editor.ToolController;
import de.blight.eztree.Billboard;
import de.blight.eztree.TreeOptions;
import de.blight.eztree.TreePresets;
import de.blight.eztree.TreeType;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.scene.control.Spinner;
import javafx.scene.control.ToolBar;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import static de.blight.editor.toolset.UiHelper.*;
public class EzTreeController implements ToolController {
private final ToolContext ctx;
// ── Zustand ───────────────────────────────────────────────────────────────
private TreeOptions ezTreeOptions = TreePresets.oakMedium();
private String ezTreePresetName = "Oak Medium";
private Runnable onF5 = () -> {};
private Runnable onF6 = () -> {};
// ── Preview-Panel (lazy) ──────────────────────────────────────────────────
private ImageView treePreviewView;
private StackPane plantPreviewPanel;
public EzTreeController(ToolContext ctx) {
this.ctx = ctx;
}
@Override
public String toolId() { return "eztree"; }
@Override
public void activate() {
onF5 = () -> {
ctx.input().ezTreeGenQueue.offer(new SharedInput.EzTreeGenRequest(
ezTreeOptions.copy(), ezTreePresetName, false));
ctx.setStatus().accept("EZ-Tree: generiere Vorschau…");
};
ctx.setMainToolbar().accept(buildToolBar());
ctx.setCenterView().accept(getPlantPreviewPanel());
ctx.setRightPanel().accept(buildEzTreeParamsPanel());
}
@Override
public void deactivate() {}
@Override
public void onPreview() { onF5.run(); }
@Override
public void onRandomSeed() { onF6.run(); }
@Override
public void onFrame() {
SharedInput input = ctx.input();
String treeMsg = input.treeGenStatusMsg;
if (treeMsg != null) {
input.treeGenStatusMsg = null;
ctx.setStatus().accept(treeMsg);
}
if (input.treePreviewResized) {
input.treePreviewResized = false;
if (treePreviewView != null) {
treePreviewView.setImage(input.treePreviewImage);
}
}
}
// ── Vorschau-Panel ────────────────────────────────────────────────────────
private StackPane getPlantPreviewPanel() {
if (plantPreviewPanel != null) { return plantPreviewPanel; }
SharedInput input = ctx.input();
treePreviewView = new ImageView(input.treePreviewImage);
treePreviewView.setPreserveRatio(true);
treePreviewView.setSmooth(true);
StackPane pane = new StackPane(treePreviewView);
pane.setStyle("-fx-background-color: #1e2433;");
pane.widthProperty().addListener((o, ow, nw) -> {
treePreviewView.setFitWidth(nw.doubleValue());
input.treePreviewW = Math.max(64, nw.intValue());
});
pane.heightProperty().addListener((o, oh, nh) -> {
treePreviewView.setFitHeight(nh.doubleValue());
input.treePreviewH = Math.max(64, nh.intValue());
});
double[] prevMX = {0}, prevMY = {0};
pane.setOnMousePressed(e -> {
boolean mid = e.getButton() == MouseButton.MIDDLE;
boolean both = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
if (mid || both) { prevMX[0] = e.getX(); prevMY[0] = e.getY(); }
});
pane.setOnMouseDragged(e -> {
boolean both = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
if (e.isMiddleButtonDown() || both) {
double dx = e.getX() - prevMX[0];
double dy = e.getY() - prevMY[0];
input.treePreviewRotY += (float) (dx * 0.4);
input.treePreviewRotX = (float) Math.max(5.0, Math.min(80.0,
input.treePreviewRotX - dy * 0.3));
prevMX[0] = e.getX(); prevMY[0] = e.getY();
}
});
pane.setOnScroll(e -> {
if (e.getDeltaY() == 0) { return; }
float factor = e.getDeltaY() > 0 ? 0.88f : 1.14f;
input.treePreviewZoom = (float) Math.max(0.25, Math.min(4.0,
input.treePreviewZoom * factor));
e.consume();
});
pane.setFocusTraversable(true);
pane.setOnMouseClicked(e -> pane.requestFocus());
pane.setOnKeyPressed(e -> {
if (e.getCode() == javafx.scene.input.KeyCode.W) {
input.treePreviewWireframe = !input.treePreviewWireframe;
}
});
plantPreviewPanel = pane;
return pane;
}
// ── Toolbar ───────────────────────────────────────────────────────────────
private ToolBar buildToolBar() {
Label presetLabel = new Label("Preset:");
presetLabel.setStyle("-fx-font-weight: bold;");
ComboBox<String> presetBox = new ComboBox<>();
presetBox.getItems().addAll(TreePresets.presetNames());
presetBox.setValue(ezTreePresetName);
presetBox.setOnAction(e -> {
ezTreePresetName = presetBox.getValue();
ezTreeOptions = TreePresets.byName(ezTreePresetName);
ctx.setRightPanel().accept(buildEzTreeParamsPanel());
});
Button previewBtn = new Button("▶ Vorschau [F5]");
previewBtn.setStyle("-fx-font-weight: bold;");
previewBtn.setOnAction(e -> onF5.run());
Button exportBtn = new Button("💾 Export .j3o");
exportBtn.setOnAction(e -> {
ctx.input().ezTreeGenQueue.offer(new SharedInput.EzTreeGenRequest(
ezTreeOptions.copy(), ezTreePresetName, true));
ctx.setStatus().accept("EZ-Tree: generiere und exportiere…");
});
Button backBtn = new Button("← Welteneditor");
backBtn.setOnAction(e -> ctx.switchToWorld().run());
ToolBar bar = new ToolBar();
bar.getItems().addAll(
backBtn, new Separator(Orientation.VERTICAL),
presetLabel, presetBox,
new Separator(Orientation.VERTICAL),
previewBtn, exportBtn
);
return bar;
}
// ── EzTree-Parameter-Panel ────────────────────────────────────────────────
private VBox buildEzTreeParamsPanel() {
VBox inner = new VBox(6);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
inner.getChildren().add(bold("Zufallssamen:"));
Spinner<Integer> seedSp = intSpinner(0, 999999, ezTreeOptions.seed);
seedSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.seed = b);
Button ezRndSeed = new Button("🎲");
onF6 = () -> {
int s = new java.util.Random().nextInt(1000000);
ezTreeOptions.seed = s;
seedSp.getValueFactory().setValue(s);
onF5.run();
};
ezRndSeed.setOnAction(e -> onF6.run());
HBox ezSeedRow = new HBox(4, seedSp, ezRndSeed);
HBox.setHgrow(seedSp, Priority.ALWAYS);
inner.getChildren().add(ezSeedRow);
inner.getChildren().add(bold("Typ:"));
ChoiceBox<String> typeCB = new ChoiceBox<>();
typeCB.getItems().addAll("Laubbaum", "Immergrün");
typeCB.setValue(ezTreeOptions.type == TreeType.EVERGREEN ? "Immergrün" : "Laubbaum");
typeCB.setMaxWidth(Double.MAX_VALUE);
typeCB.setOnAction(e -> ezTreeOptions.type =
"Immergrün".equals(typeCB.getValue()) ? TreeType.EVERGREEN : TreeType.DECIDUOUS);
inner.getChildren().add(typeCB);
inner.getChildren().add(bold("Ast-Ebenen:"));
Spinner<Integer> levelsSp = intSpinner(1, 5, ezTreeOptions.branch.levels);
levelsSp.valueProperty().addListener((o, a, b) -> {
ezTreeOptions.branch.levels = b;
ctx.setRightPanel().accept(buildEzTreeParamsPanel());
});
inner.getChildren().add(levelsSp);
inner.getChildren().addAll(sectionTitle("Rinde"), new Separator());
inner.getChildren().add(ezFloat("UV-Skala X:", 0.1, 5.0, ezTreeOptions.bark.textureScaleX,
v -> ezTreeOptions.bark.textureScaleX = v));
inner.getChildren().add(ezFloat("UV-Skala Y:", 0.1, 5.0, ezTreeOptions.bark.textureScaleY,
v -> ezTreeOptions.bark.textureScaleY = v));
inner.getChildren().add(ezFloat("Farbe R:", 0, 1, ezTreeOptions.bark.r,
v -> ezTreeOptions.bark.r = v));
inner.getChildren().add(ezFloat("Farbe G:", 0, 1, ezTreeOptions.bark.g,
v -> ezTreeOptions.bark.g = v));
inner.getChildren().add(ezFloat("Farbe B:", 0, 1, ezTreeOptions.bark.b,
v -> ezTreeOptions.bark.b = v));
inner.getChildren().addAll(sectionTitle("Wachstumskraft"), new Separator());
inner.getChildren().add(ezFloat("Richtung X:", -1, 1, ezTreeOptions.branch.force.direction.x,
v -> ezTreeOptions.branch.force.direction.x = v));
inner.getChildren().add(ezFloat("Richtung Y:", -1, 1, ezTreeOptions.branch.force.direction.y,
v -> ezTreeOptions.branch.force.direction.y = v));
inner.getChildren().add(ezFloat("Richtung Z:", -1, 1, ezTreeOptions.branch.force.direction.z,
v -> ezTreeOptions.branch.force.direction.z = v));
inner.getChildren().add(ezFloat("Stärke:", 0, 0.2, ezTreeOptions.branch.force.strength,
v -> ezTreeOptions.branch.force.strength = v));
int maxLevel = ezTreeOptions.branch.levels;
for (int lv = 0; lv <= maxLevel; lv++) {
buildLevelSection(inner, lv, maxLevel);
}
inner.getChildren().addAll(sectionTitle("Blätter"), new Separator());
inner.getChildren().add(bold("Billboard:"));
ChoiceBox<String> billCB = new ChoiceBox<>();
billCB.getItems().addAll("Einfach", "Kreuz");
billCB.setValue(ezTreeOptions.leaves.billboard == Billboard.CROSS ? "Kreuz" : "Einfach");
billCB.setMaxWidth(Double.MAX_VALUE);
billCB.setOnAction(e -> ezTreeOptions.leaves.billboard =
"Kreuz".equals(billCB.getValue()) ? Billboard.CROSS : Billboard.NONE);
inner.getChildren().add(billCB);
inner.getChildren().add(bold("Anzahl:"));
Spinner<Integer> leafCountSp = intSpinner(0, 60, ezTreeOptions.leaves.count);
leafCountSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.leaves.count = b);
inner.getChildren().add(leafCountSp);
inner.getChildren().add(ezFloat("Start-Position:", 0, 1, ezTreeOptions.leaves.start,
v -> ezTreeOptions.leaves.start = v));
inner.getChildren().add(ezFloat("Größe:", 0.05, 4, ezTreeOptions.leaves.size,
v -> ezTreeOptions.leaves.size = v));
inner.getChildren().add(ezFloat("Größen-Varianz:", 0, 1, ezTreeOptions.leaves.sizeVariance,
v -> ezTreeOptions.leaves.sizeVariance = v));
inner.getChildren().add(ezFloat("Winkel (°):", 0, 90, ezTreeOptions.leaves.angle,
v -> ezTreeOptions.leaves.angle = v));
inner.getChildren().add(ezFloat("Alpha-Schwellwert:", 0, 1, ezTreeOptions.leaves.alphaTest,
v -> ezTreeOptions.leaves.alphaTest = v));
inner.getChildren().add(ezFloat("Farbe R:", 0, 1, ezTreeOptions.leaves.r,
v -> ezTreeOptions.leaves.r = v));
inner.getChildren().add(ezFloat("Farbe G:", 0, 1, ezTreeOptions.leaves.g,
v -> ezTreeOptions.leaves.g = v));
inner.getChildren().add(ezFloat("Farbe B:", 0, 1, ezTreeOptions.leaves.b,
v -> ezTreeOptions.leaves.b = v));
inner.getChildren().addAll(sectionTitle("Spalier (Trellis)"), new Separator());
javafx.scene.control.CheckBox trellisEnCB = new javafx.scene.control.CheckBox("Aktiviert");
trellisEnCB.setSelected(ezTreeOptions.trellis.enabled);
trellisEnCB.setOnAction(e -> ezTreeOptions.trellis.enabled = trellisEnCB.isSelected());
inner.getChildren().add(trellisEnCB);
inner.getChildren().add(bold("Streben:"));
Spinner<Integer> trellisSecSp = intSpinner(3, 12, ezTreeOptions.trellis.sections);
trellisSecSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.trellis.sections = b);
inner.getChildren().add(trellisSecSp);
inner.getChildren().add(ezFloat("Radius:", 0.1, 8, ezTreeOptions.trellis.radius,
v -> ezTreeOptions.trellis.radius = v));
inner.getChildren().add(ezFloat("Höhe:", 0.5, 15, ezTreeOptions.trellis.length,
v -> ezTreeOptions.trellis.length = v));
inner.getChildren().add(ezFloat("Streben-Radius:", 0.01, 0.3, ezTreeOptions.trellis.memberRadius,
v -> ezTreeOptions.trellis.memberRadius = v));
inner.getChildren().add(bold("Querstreben:"));
Spinner<Integer> crossSp = intSpinner(1, 8, ezTreeOptions.trellis.crossMembers);
crossSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.trellis.crossMembers = b);
inner.getChildren().add(crossSp);
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(270);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
private void buildLevelSection(VBox container, int lv, int maxLevel) {
String title = lv == 0 ? "Stamm (Ebene 0)" : "Ast-Ebene " + lv;
container.getChildren().addAll(sectionTitle(title), new Separator());
var bo = ezTreeOptions.branch;
if (lv > 0) {
container.getChildren().add(ezFloat("Winkel (°):", 0, 90,
bo.getAngle(lv), v -> bo.angle.put(lv, v)));
container.getChildren().add(ezFloat("Start-Position:", 0, 1,
bo.getStart(lv), v -> bo.start.put(lv, v)));
}
if (lv < maxLevel) {
container.getChildren().add(bold("Kinder:"));
Spinner<Integer> childSp = intSpinner(0, 20, bo.getChildren(lv));
childSp.valueProperty().addListener((o, a, b) -> bo.children.put(lv, b));
container.getChildren().add(childSp);
}
container.getChildren().add(ezFloat("Gnarliness:", 0, 1.5,
bo.getGnarliness(lv), v -> bo.gnarliness.put(lv, v)));
String lenLabel = lv == 0 ? "Länge (absolut):" : "Länge (Faktor):";
double lenMax = lv == 0 ? 60 : 2;
container.getChildren().add(ezFloat(lenLabel, 0.1, lenMax,
bo.getLength(lv), v -> bo.length.put(lv, v)));
String radLabel = lv == 0 ? "Radius (absolut):" : "Radius (Faktor):";
double radMax = lv == 0 ? 3 : 1;
container.getChildren().add(ezFloat(radLabel, 0.01, radMax,
bo.getRadius(lv), v -> bo.radius.put(lv, v)));
container.getChildren().add(bold("Sektionen:"));
Spinner<Integer> secSp = intSpinner(2, 24, bo.getSections(lv));
secSp.valueProperty().addListener((o, a, b) -> bo.sections.put(lv, b));
container.getChildren().add(secSp);
container.getChildren().add(bold("Segmente (Polygon):"));
Spinner<Integer> segSp = intSpinner(3, 12, bo.getSegments(lv));
segSp.valueProperty().addListener((o, a, b) -> bo.segments.put(lv, b));
container.getChildren().add(segSp);
container.getChildren().add(ezFloat("Verjüngung:", 0, 1,
bo.getTaper(lv), v -> bo.taper.put(lv, v)));
container.getChildren().add(ezFloat("Drall (°/Sektion):", -180, 180,
bo.getTwist(lv), v -> bo.twist.put(lv, v)));
}
}

View File

@@ -0,0 +1,832 @@
package de.blight.editor.toolset.vegetation;
import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext;
import de.blight.editor.ToolController;
import de.blight.editor.tree.FernOptions;
import de.blight.editor.tree.FruitBushOptions;
import de.blight.editor.tree.GrapevineOptions;
import de.blight.editor.tree.PalmOptions;
import de.blight.editor.tree.TreeParams;
import de.blight.editor.tree.WillowOptions;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.scene.control.Spinner;
import javafx.scene.control.ToolBar;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import static de.blight.editor.toolset.UiHelper.*;
public class VegetationController implements ToolController {
private final ToolContext ctx;
// ── Zustand (aus EditorApp migriert) ──────────────────────────────────────
private TreeParams treeParams = TreeParams.oak();
private String currentTreePreset = "Eiche";
private PalmOptions palmOptions = new PalmOptions();
private FernOptions fernOptions = new FernOptions();
private FruitBushOptions fruitBushOptions = FruitBushOptions.orange();
private String fruitBushVariant = "Orange";
private GrapevineOptions grapevineOptions = new GrapevineOptions();
private WillowOptions willowOptions = new WillowOptions();
private String vegetationType = "Baum (Eiche)";
private Runnable onF5 = () -> {};
private Runnable onF6 = () -> {};
// ── Preview-Panel (lazy) ──────────────────────────────────────────────────
private ImageView treePreviewView;
private StackPane plantPreviewPanel;
public VegetationController(ToolContext ctx) {
this.ctx = ctx;
}
@Override
public String toolId() { return "vegetation"; }
@Override
public void activate() {
updateOnF5();
ctx.setMainToolbar().accept(buildToolBar());
ctx.setCenterView().accept(getPlantPreviewPanel());
ctx.setRightPanel().accept(buildParamsPanel());
}
@Override
public void deactivate() {}
@Override
public void onPreview() { onF5.run(); }
@Override
public void onRandomSeed() { onF6.run(); }
@Override
public void onFrame() {
SharedInput input = ctx.input();
String treeMsg = input.treeGenStatusMsg;
if (treeMsg != null) {
input.treeGenStatusMsg = null;
ctx.setStatus().accept(treeMsg);
}
if (input.treePreviewResized) {
input.treePreviewResized = false;
if (treePreviewView != null) {
treePreviewView.setImage(input.treePreviewImage);
}
}
}
// ── Vorschau-Panel ────────────────────────────────────────────────────────
private StackPane getPlantPreviewPanel() {
if (plantPreviewPanel != null) { return plantPreviewPanel; }
SharedInput input = ctx.input();
treePreviewView = new ImageView(input.treePreviewImage);
treePreviewView.setPreserveRatio(true);
treePreviewView.setSmooth(true);
StackPane pane = new StackPane(treePreviewView);
pane.setStyle("-fx-background-color: #1e2433;");
pane.widthProperty().addListener((o, ow, nw) -> {
treePreviewView.setFitWidth(nw.doubleValue());
input.treePreviewW = Math.max(64, nw.intValue());
});
pane.heightProperty().addListener((o, oh, nh) -> {
treePreviewView.setFitHeight(nh.doubleValue());
input.treePreviewH = Math.max(64, nh.intValue());
});
double[] prevMX = {0}, prevMY = {0};
pane.setOnMousePressed(e -> {
boolean mid = e.getButton() == MouseButton.MIDDLE;
boolean both = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
if (mid || both) { prevMX[0] = e.getX(); prevMY[0] = e.getY(); }
});
pane.setOnMouseDragged(e -> {
boolean both = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
if (e.isMiddleButtonDown() || both) {
double dx = e.getX() - prevMX[0];
double dy = e.getY() - prevMY[0];
input.treePreviewRotY += (float) (dx * 0.4);
input.treePreviewRotX = (float) Math.max(5.0, Math.min(80.0,
input.treePreviewRotX - dy * 0.3));
prevMX[0] = e.getX(); prevMY[0] = e.getY();
}
});
pane.setOnScroll(e -> {
if (e.getDeltaY() == 0) { return; }
float factor = e.getDeltaY() > 0 ? 0.88f : 1.14f;
input.treePreviewZoom = (float) Math.max(0.25, Math.min(4.0,
input.treePreviewZoom * factor));
e.consume();
});
pane.setFocusTraversable(true);
pane.setOnMouseClicked(e -> pane.requestFocus());
pane.setOnKeyPressed(e -> {
if (e.getCode() == javafx.scene.input.KeyCode.W) {
input.treePreviewWireframe = !input.treePreviewWireframe;
}
});
plantPreviewPanel = pane;
return pane;
}
// ── Toolbar ───────────────────────────────────────────────────────────────
private ToolBar buildToolBar() {
Button backBtn = new Button("← Welteneditor");
backBtn.setOnAction(e -> ctx.switchToWorld().run());
Label typeLabel = new Label("Pflanzentyp:");
typeLabel.setStyle("-fx-font-weight: bold;");
ComboBox<String> typeBox = new ComboBox<>();
typeBox.getItems().addAll(
"Baum (Eiche)", "Baum (Birke)", "Baum (Kiefer)", "Baum (Weide)", "Baum (Busch)",
"Baum (Trauerweide)",
"Farn", "Palme", "Fruchtbusch", "Weinpflanze");
typeBox.setValue(vegetationType);
typeBox.setOnAction(e -> {
vegetationType = typeBox.getValue();
if (vegetationType.startsWith("Baum (") && !"Baum (Trauerweide)".equals(vegetationType)) {
currentTreePreset = vegetationType.substring(6, vegetationType.length() - 1);
treeParams = presetFromName(currentTreePreset);
}
updateOnF5();
ctx.setRightPanel().accept(buildParamsPanel());
onF5.run();
});
Button previewBtn = new Button("▶ Vorschau [F5]");
previewBtn.setStyle("-fx-font-weight: bold;");
previewBtn.setOnAction(e -> onF5.run());
Button exportBtn = new Button("💾 Export .j3o");
exportBtn.setOnAction(e -> {
SharedInput input = ctx.input();
if (vegetationType.startsWith("Baum (") && !"Baum (Trauerweide)".equals(vegetationType)) {
input.treeGenQueue.offer(new SharedInput.TreeGenRequest(
treeParams.copy(), true, treeTypeFromPreset(currentTreePreset)));
} else if ("Farn".equals(vegetationType)) {
input.fernGenQueue.offer(new SharedInput.FernGenRequest(fernOptions.copy(), true));
} else if ("Fruchtbusch".equals(vegetationType)) {
input.fruitBushGenQueue.offer(new SharedInput.FruitBushGenRequest(
fruitBushOptions.copy(), fruitBushVariant, true));
} else if ("Weinpflanze".equals(vegetationType)) {
input.grapevineGenQueue.offer(new SharedInput.GrapevineGenRequest(grapevineOptions.copy(), true));
} else if ("Baum (Trauerweide)".equals(vegetationType)) {
input.willowGenQueue.offer(new SharedInput.WillowGenRequest(willowOptions.copy(), true));
} else {
input.palmGenQueue.offer(new SharedInput.PalmGenRequest(palmOptions.copy(), true));
}
ctx.setStatus().accept("Generiere und exportiere…");
});
Button rndBtn = new Button("🎲 Neuer Seed");
rndBtn.setOnAction(e -> onF6.run());
ToolBar bar = new ToolBar();
bar.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL),
typeLabel, typeBox,
new Separator(Orientation.VERTICAL),
previewBtn, exportBtn, rndBtn);
return bar;
}
// ── F5 / F6-Callbacks ─────────────────────────────────────────────────────
private void updateOnF5() {
SharedInput input = ctx.input();
if ("Baum (Trauerweide)".equals(vegetationType)) {
onF5 = () -> {
input.willowGenQueue.offer(new SharedInput.WillowGenRequest(willowOptions.copy(), false));
ctx.setStatus().accept("Trauerweide: generiere Vorschau…");
};
onF6 = () -> {
willowOptions.seed = new java.util.Random().nextInt(1000000);
ctx.setRightPanel().accept(buildParamsPanel());
onF5.run();
};
} else if (vegetationType.startsWith("Baum (")) {
onF5 = () -> {
input.treeGenQueue.offer(new SharedInput.TreeGenRequest(
treeParams.copy(), false, treeTypeFromPreset(currentTreePreset)));
ctx.setStatus().accept("Generiere Vorschau…");
};
onF6 = () -> {
treeParams.seed = new java.util.Random().nextInt(100000);
onF5.run();
};
} else if ("Farn".equals(vegetationType)) {
onF5 = () -> {
input.fernGenQueue.offer(new SharedInput.FernGenRequest(fernOptions.copy(), false));
ctx.setStatus().accept("Generiere Farn-Vorschau…");
};
onF6 = () -> {
fernOptions.seed = new java.util.Random().nextInt(1000000);
ctx.setRightPanel().accept(buildParamsPanel());
onF5.run();
};
} else if ("Fruchtbusch".equals(vegetationType)) {
onF5 = () -> {
input.fruitBushGenQueue.offer(new SharedInput.FruitBushGenRequest(
fruitBushOptions.copy(), fruitBushVariant, false));
ctx.setStatus().accept("Fruchtbusch: generiere Vorschau…");
};
onF6 = () -> {
fruitBushOptions.seed = new java.util.Random().nextInt(1000000);
ctx.setRightPanel().accept(buildParamsPanel());
onF5.run();
};
} else if ("Weinpflanze".equals(vegetationType)) {
onF5 = () -> {
input.grapevineGenQueue.offer(new SharedInput.GrapevineGenRequest(
grapevineOptions.copy(), false));
ctx.setStatus().accept("Weinpflanze: generiere Vorschau…");
};
onF6 = () -> {
grapevineOptions.seed = new java.util.Random().nextInt(1000000);
ctx.setRightPanel().accept(buildParamsPanel());
onF5.run();
};
} else {
// Palme
onF5 = () -> {
input.palmGenQueue.offer(new SharedInput.PalmGenRequest(palmOptions.copy(), false));
ctx.setStatus().accept("Palme: generiere Vorschau…");
};
onF6 = () -> {
palmOptions.seed = new java.util.Random().nextInt(1000000);
ctx.setRightPanel().accept(buildParamsPanel());
onF5.run();
};
}
}
// ── Parameter-Panel (Dispatcher) ──────────────────────────────────────────
private javafx.scene.Node buildParamsPanel() {
if ("Baum (Trauerweide)".equals(vegetationType)) {
return buildWillowParamsPanel();
} else if (vegetationType.startsWith("Baum (")) {
return buildTreeParamsPanel();
} else if ("Farn".equals(vegetationType)) {
return buildFernParamsPanel();
} else if ("Fruchtbusch".equals(vegetationType)) {
return buildFruitBushParamsPanel();
} else if ("Weinpflanze".equals(vegetationType)) {
return buildGrapevineParamsPanel();
} else {
return buildPalmParamsPanel();
}
}
// ── Baum-Parameter-Panel ──────────────────────────────────────────────────
private VBox buildTreeParamsPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
Label title = new Label("Baum-Parameter");
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111111;");
inner.getChildren().addAll(title, new Separator());
inner.getChildren().add(bold("Zufallssamen:"));
Spinner<Integer> seedSpinner = new Spinner<>(0, 99999, treeParams.seed);
seedSpinner.setEditable(true);
seedSpinner.setMaxWidth(Double.MAX_VALUE);
seedSpinner.valueProperty().addListener((o, a, b) -> treeParams.seed = b);
Button treeRndSeed = new Button("🎲");
onF6 = () -> {
int s = new java.util.Random().nextInt(100000);
treeParams.seed = s;
seedSpinner.getValueFactory().setValue(s);
onF5.run();
};
treeRndSeed.setOnAction(e -> onF6.run());
HBox treeSeedRow = new HBox(4, seedSpinner, treeRndSeed);
HBox.setHgrow(seedSpinner, Priority.ALWAYS);
inner.getChildren().add(treeSeedRow);
inner.getChildren().add(bold("Ast-Tiefe:"));
Spinner<Integer> levelSpinner = new Spinner<>(1, 4, treeParams.levels);
levelSpinner.setMaxWidth(Double.MAX_VALUE);
levelSpinner.valueProperty().addListener((o, a, b) -> treeParams.levels = b);
inner.getChildren().add(levelSpinner);
inner.getChildren().add(paramSlider("Stamm-Aufrichtung:", 0.0, 0.40, treeParams.trunkUprightness,
v -> treeParams.trunkUprightness = v));
inner.getChildren().add(new Separator());
CheckBox leavesCheck = new CheckBox("Blätter generieren");
leavesCheck.setSelected(treeParams.generateLeaves);
leavesCheck.setOnAction(e -> treeParams.generateLeaves = leavesCheck.isSelected());
inner.getChildren().add(leavesCheck);
inner.getChildren().add(paramSlider("Blattgröße:", 0.3, 3.0, treeParams.leafScale,
v -> treeParams.leafScale = v));
inner.getChildren().add(bold("Blattanzahl:"));
Spinner<Integer> leafCountSpinner = new Spinner<>(1, 15, treeParams.leafCount);
leafCountSpinner.setMaxWidth(Double.MAX_VALUE);
leafCountSpinner.valueProperty().addListener((o, a, b) -> treeParams.leafCount = b);
inner.getChildren().add(leafCountSpinner);
inner.getChildren().add(bold("Ast-Verzweigungen:"));
Spinner<Integer> leafBranchingsSpinner = new Spinner<>(0, 3, treeParams.leafBranchings);
leafBranchingsSpinner.setMaxWidth(Double.MAX_VALUE);
leafBranchingsSpinner.valueProperty().addListener((o, a, b) -> treeParams.leafBranchings = b);
inner.getChildren().add(leafBranchingsSpinner);
inner.getChildren().add(new Separator());
inner.getChildren().add(bold("Wind:"));
inner.getChildren().add(paramSlider("Stamm:", 0.0, 0.5, treeParams.trunkFlexibility,
v -> treeParams.trunkFlexibility = v));
inner.getChildren().add(paramSlider("Spitzen:", 0.2, 1.0, treeParams.branchFlexibility,
v -> treeParams.branchFlexibility = v));
return wrapInPanel(inner, 260);
}
// ── Farn-Parameter-Panel ──────────────────────────────────────────────────
private VBox buildFernParamsPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
inner.getChildren().add(bold("Zufallssamen:"));
Spinner<Integer> seedSp = intSpinner(0, 999999, fernOptions.seed);
seedSp.valueProperty().addListener((o, a, b) -> fernOptions.seed = b);
Button rndSeedBtn = new Button("🎲");
rndSeedBtn.setOnAction(e -> {
int s = new java.util.Random().nextInt(1000000);
fernOptions.seed = s;
seedSp.getValueFactory().setValue(s);
onF5.run();
});
HBox seedRow = new HBox(4, seedSp, rndSeedBtn);
HBox.setHgrow(seedSp, Priority.ALWAYS);
inner.getChildren().add(seedRow);
inner.getChildren().addAll(sectionTitle("Fächer"), new Separator());
inner.getChildren().add(bold("Anzahl Fächer:"));
Spinner<Integer> frondCountSp = intSpinner(3, 30, fernOptions.frondCount);
frondCountSp.valueProperty().addListener((o, a, b) -> fernOptions.frondCount = b);
inner.getChildren().add(frondCountSp);
inner.getChildren().add(ezFloat("Länge (m):", 0.2, 2.0, fernOptions.frondLength,
v -> fernOptions.frondLength = v));
inner.getChildren().add(ezFloat("Neigung min (°):", 0.0, 90.0, fernOptions.tiltMin,
v -> fernOptions.tiltMin = v));
inner.getChildren().add(ezFloat("Neigung max (°):", 0.0, 90.0, fernOptions.tiltMax,
v -> fernOptions.tiltMax = v));
inner.getChildren().add(ezFloat("Durchhang:", 0.0, 1.0, fernOptions.droop,
v -> fernOptions.droop = v));
inner.getChildren().addAll(sectionTitle("Segmente & Wind"), new Separator());
inner.getChildren().add(bold("Segmente pro Fächer:"));
Spinner<Integer> segSp = intSpinner(2, 12, fernOptions.frondSegments);
segSp.valueProperty().addListener((o, a, b) -> fernOptions.frondSegments = b);
inner.getChildren().add(segSp);
inner.getChildren().add(ezFloat("Windstärke:", 0.0, 0.5, fernOptions.windStrength,
v -> fernOptions.windStrength = v));
inner.getChildren().add(ezFloat("Windgeschwindigkeit:", 0.1, 2.0, fernOptions.windSpeed,
v -> fernOptions.windSpeed = v));
Button previewBtn = new Button("▶ Vorschau");
previewBtn.setMaxWidth(Double.MAX_VALUE);
previewBtn.setStyle("-fx-background-color:#2d8a3e;-fx-text-fill:white;-fx-font-weight:bold;-fx-padding:6 12 6 12;");
previewBtn.setOnAction(e -> onF5.run());
inner.getChildren().add(new Separator());
inner.getChildren().add(previewBtn);
return wrapInPanel(inner, 270);
}
// ── Fruchtbusch-Parameter-Panel ───────────────────────────────────────────
private VBox buildFruitBushParamsPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(sectionTitle("Fruchtbusch-Typ"), new Separator());
ComboBox<String> variantBox = new ComboBox<>();
variantBox.getItems().addAll("Orange", "Zitrone", "Aprikose");
variantBox.setValue(fruitBushVariant);
variantBox.setMaxWidth(Double.MAX_VALUE);
variantBox.setOnAction(e -> {
fruitBushVariant = variantBox.getValue();
int oldSeed = fruitBushOptions.seed;
fruitBushOptions = switch (fruitBushVariant) {
case "Zitrone" -> FruitBushOptions.lemon();
case "Aprikose" -> FruitBushOptions.apricot();
default -> FruitBushOptions.orange();
};
fruitBushOptions.seed = oldSeed;
ctx.setRightPanel().accept(buildParamsPanel());
onF5.run();
});
inner.getChildren().add(variantBox);
inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
inner.getChildren().add(bold("Zufallssamen:"));
Spinner<Integer> seedSp = intSpinner(0, 999999, fruitBushOptions.seed);
seedSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.seed = b);
Button rndSeedBtn = new Button("🎲");
rndSeedBtn.setOnAction(e -> {
int s = new java.util.Random().nextInt(1000000);
fruitBushOptions.seed = s;
seedSp.getValueFactory().setValue(s);
onF5.run();
});
HBox seedRow = new HBox(4, seedSp, rndSeedBtn);
HBox.setHgrow(seedSp, Priority.ALWAYS);
inner.getChildren().add(seedRow);
inner.getChildren().addAll(sectionTitle("Stamm"), new Separator());
inner.getChildren().add(ezFloat("Höhe (m):", 0.2, 1.5, fruitBushOptions.trunkHeight,
v -> fruitBushOptions.trunkHeight = v));
inner.getChildren().add(ezFloat("Radius (m):", 0.01, 0.15, fruitBushOptions.trunkRadius,
v -> fruitBushOptions.trunkRadius = v));
inner.getChildren().addAll(sectionTitle("Äste"), new Separator());
inner.getChildren().add(bold("Anzahl Primäräste:"));
Spinner<Integer> branchCountSp = intSpinner(2, 10, fruitBushOptions.branchCount);
branchCountSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.branchCount = b);
inner.getChildren().add(branchCountSp);
inner.getChildren().add(ezFloat("Winkel (°):", 10, 80, fruitBushOptions.branchAngle,
v -> fruitBushOptions.branchAngle = v));
inner.getChildren().add(ezFloat("Länge (m):", 0.2, 2.0, fruitBushOptions.branchLength,
v -> fruitBushOptions.branchLength = v));
inner.getChildren().add(bold("Reiser pro Ast:"));
Spinner<Integer> twigCountSp = intSpinner(1, 8, fruitBushOptions.twigCount);
twigCountSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.twigCount = b);
inner.getChildren().add(twigCountSp);
inner.getChildren().add(ezFloat("Reiser-Winkel (°):", 10, 80, fruitBushOptions.twigAngle,
v -> fruitBushOptions.twigAngle = v));
inner.getChildren().add(ezFloat("Reiser-Länge (m):", 0.1, 1.2, fruitBushOptions.twigLength,
v -> fruitBushOptions.twigLength = v));
inner.getChildren().addAll(sectionTitle("Blätter & Früchte"), new Separator());
inner.getChildren().add(ezFloat("Blattgröße (m):", 0.05, 0.6, fruitBushOptions.leafSize,
v -> fruitBushOptions.leafSize = v));
inner.getChildren().add(bold("Blätter pro Reiser:"));
Spinner<Integer> leafCountSp = intSpinner(2, 20, fruitBushOptions.leafCount);
leafCountSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.leafCount = b);
inner.getChildren().add(leafCountSp);
inner.getChildren().add(ezFloat("Frucht-Dichte [0–1]:", 0.0, 1.0, fruitBushOptions.fruitDensity,
v -> fruitBushOptions.fruitDensity = v));
inner.getChildren().add(ezFloat("Frucht-Radius (m):", 0.02, 0.15, fruitBushOptions.fruitRadius,
v -> fruitBushOptions.fruitRadius = v));
inner.getChildren().addAll(sectionTitle("Wind"), new Separator());
inner.getChildren().add(ezFloat("Windstärke:", 0.0, 0.5, fruitBushOptions.windStrength,
v -> fruitBushOptions.windStrength = v));
inner.getChildren().add(ezFloat("Windgeschwindigkeit:", 0.1, 2.0, fruitBushOptions.windSpeed,
v -> fruitBushOptions.windSpeed = v));
Button previewBtn = new Button("▶ Vorschau");
previewBtn.setMaxWidth(Double.MAX_VALUE);
previewBtn.setStyle("-fx-background-color:#2d8a3e;-fx-text-fill:white;-fx-font-weight:bold;-fx-padding:6 12 6 12;");
previewBtn.setOnAction(e -> onF5.run());
inner.getChildren().addAll(new Separator(), previewBtn);
return wrapInPanel(inner, 270);
}
// ── Weinpflanzen-Parameter-Panel ──────────────────────────────────────────
private VBox buildGrapevineParamsPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
inner.getChildren().add(bold("Zufallssamen:"));
Spinner<Integer> seedSp = intSpinner(0, 999999, grapevineOptions.seed);
seedSp.valueProperty().addListener((o, a, b) -> grapevineOptions.seed = b);
Button rndSeedBtn = new Button("🎲");
rndSeedBtn.setOnAction(e -> {
int s = new java.util.Random().nextInt(1000000);
grapevineOptions.seed = s;
seedSp.getValueFactory().setValue(s);
onF5.run();
});
HBox seedRow = new HBox(4, seedSp, rndSeedBtn);
HBox.setHgrow(seedSp, Priority.ALWAYS);
inner.getChildren().add(seedRow);
inner.getChildren().addAll(sectionTitle("Rahmen"), new Separator());
inner.getChildren().add(ezFloat("Gesamtbreite (m):", 2.0, 10.0, grapevineOptions.totalWidth,
v -> grapevineOptions.totalWidth = v));
inner.getChildren().add(ezFloat("Gesamthöhe (m):", 1.5, 4.0, grapevineOptions.totalHeight,
v -> grapevineOptions.totalHeight = v));
inner.getChildren().add(ezFloat("1. Draht-Höhe (m):", 0.5, 2.0, grapevineOptions.firstWireH,
v -> grapevineOptions.firstWireH = v));
inner.getChildren().add(bold("Draht-Anzahl:"));
Spinner<Integer> wireCountSp = intSpinner(2, 6, grapevineOptions.wireCount);
wireCountSp.valueProperty().addListener((o, a, b) -> grapevineOptions.wireCount = b);
inner.getChildren().add(wireCountSp);
inner.getChildren().addAll(sectionTitle("Triebe & Blätter"), new Separator());
inner.getChildren().add(bold("Triebe pro Seite:"));
Spinner<Integer> shootCountSp = intSpinner(2, 14, grapevineOptions.shootCount);
shootCountSp.valueProperty().addListener((o, a, b) -> grapevineOptions.shootCount = b);
inner.getChildren().add(shootCountSp);
inner.getChildren().add(ezFloat("Blattgröße (m):", 0.10, 0.60, grapevineOptions.leafSize,
v -> grapevineOptions.leafSize = v));
inner.getChildren().add(bold("Blätter pro Trieb:"));
Spinner<Integer> leafCountSp = intSpinner(2, 128, grapevineOptions.leafCount);
leafCountSp.valueProperty().addListener((o, a, b) -> grapevineOptions.leafCount = b);
inner.getChildren().add(leafCountSp);
inner.getChildren().addAll(sectionTitle("Wind"), new Separator());
inner.getChildren().add(ezFloat("Windstärke:", 0.0, 0.5, grapevineOptions.windStrength,
v -> grapevineOptions.windStrength = v));
inner.getChildren().add(ezFloat("Windgeschwindigkeit:", 0.1, 2.0, grapevineOptions.windSpeed,
v -> grapevineOptions.windSpeed = v));
Button previewBtn = new Button("▶ Vorschau");
previewBtn.setMaxWidth(Double.MAX_VALUE);
previewBtn.setStyle("-fx-background-color:#2d8a3e;-fx-text-fill:white;-fx-font-weight:bold;-fx-padding:6 12 6 12;");
previewBtn.setOnAction(e -> onF5.run());
inner.getChildren().add(new Separator());
inner.getChildren().add(previewBtn);
return wrapInPanel(inner, 270);
}
// ── Trauerweide-Parameter-Panel ───────────────────────────────────────────
private VBox buildWillowParamsPanel() {
VBox inner = new VBox(6);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
inner.getChildren().add(bold("Zufallssamen:"));
Spinner<Integer> seedSp = intSpinner(0, 999999, willowOptions.seed);
seedSp.valueProperty().addListener((o, a, b) -> willowOptions.seed = b);
Button rndSeed = new Button("🎲");
onF6 = () -> {
int s = new java.util.Random().nextInt(1000000);
willowOptions.seed = s;
seedSp.getValueFactory().setValue(s);
onF5.run();
};
rndSeed.setOnAction(e -> onF6.run());
HBox seedRow = new HBox(4, seedSp, rndSeed);
HBox.setHgrow(seedSp, Priority.ALWAYS);
inner.getChildren().add(seedRow);
inner.getChildren().addAll(sectionTitle("Stamm"), new Separator());
inner.getChildren().add(ezFloat("Höhe:", 4, 25, willowOptions.trunkHeight,
v -> willowOptions.trunkHeight = v));
inner.getChildren().add(ezFloat("Radius:", 0.1, 1.0, willowOptions.trunkRadius,
v -> willowOptions.trunkRadius = v));
inner.getChildren().addAll(sectionTitle("Hauptäste"), new Separator());
inner.getChildren().add(bold("Anzahl Äste:"));
Spinner<Integer> branchCountSp = intSpinner(3, 16, willowOptions.branchCount);
branchCountSp.valueProperty().addListener((o, a, b) -> willowOptions.branchCount = b);
inner.getChildren().add(branchCountSp);
inner.getChildren().add(ezFloat("Startwinkel (°):", 10, 80, willowOptions.branchStartAngle,
v -> willowOptions.branchStartAngle = v));
inner.getChildren().add(ezFloat("Endwinkel / Hänge (°):", 90, 170, willowOptions.branchEndAngle,
v -> willowOptions.branchEndAngle = v));
inner.getChildren().add(ezFloat("Winkel-Varianz (°):", 0, 40, willowOptions.branchVariance,
v -> willowOptions.branchVariance = v));
inner.getChildren().add(ezFloat("Länge:", 3, 18, willowOptions.branchLength,
v -> willowOptions.branchLength = v));
inner.getChildren().add(ezFloat("Radius:", 0.05, 0.5, willowOptions.branchRadius,
v -> willowOptions.branchRadius = v));
inner.getChildren().addAll(sectionTitle("Hängeäste"), new Separator());
inner.getChildren().add(bold("Anzahl pro Sektion:"));
Spinner<Integer> subCountSp = intSpinner(1, 8, willowOptions.subBranchCount);
subCountSp.valueProperty().addListener((o, a, b) -> willowOptions.subBranchCount = b);
inner.getChildren().add(subCountSp);
inner.getChildren().add(ezFloat("Länge:", 1, 10, willowOptions.subBranchLength,
v -> willowOptions.subBranchLength = v));
inner.getChildren().add(ezFloat("Startwinkel (Grad):", 10, 90, willowOptions.subBranchStartAngle,
v -> willowOptions.subBranchStartAngle = v));
inner.getChildren().add(ezFloat("Endwinkel (Grad):", 91, 180, willowOptions.subBranchEndAngle,
v -> willowOptions.subBranchEndAngle = v));
inner.getChildren().addAll(sectionTitle("Blätter"), new Separator());
inner.getChildren().add(ezFloat("Größe:", 0.3, 3.0, willowOptions.leafScale,
v -> willowOptions.leafScale = v));
inner.getChildren().add(bold("Anzahl pro Cluster:"));
Spinner<Integer> leafCountSp = intSpinner(1, 12, willowOptions.leafCount);
leafCountSp.valueProperty().addListener((o, a, b) -> willowOptions.leafCount = b);
inner.getChildren().add(leafCountSp);
inner.getChildren().add(ezFloat("Blatt-Hänge:", 0.0, 1.0, willowOptions.leafDroop,
v -> willowOptions.leafDroop = v));
inner.getChildren().add(ezFloat("Blatt-Startwinkel min (°):", 0, 89, willowOptions.leafAngleMin,
v -> willowOptions.leafAngleMin = v));
inner.getChildren().add(ezFloat("Blatt-Startwinkel max (°):", 1, 90, willowOptions.leafAngleMax,
v -> willowOptions.leafAngleMax = v));
inner.getChildren().addAll(sectionTitle("Farben & Texturen"), new Separator());
Label leafTexLabel = new Label("Blatt-Textur:");
leafTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
ComboBox<String> leafTexBox = new ComboBox<>();
leafTexBox.getItems().addAll("weeping_willow.png", "ash.png", "oak.png");
String curLeaf = willowOptions.leafTexture != null
? willowOptions.leafTexture.substring(willowOptions.leafTexture.lastIndexOf('/') + 1)
: "weeping_willow.png";
leafTexBox.setValue(leafTexBox.getItems().contains(curLeaf) ? curLeaf : "weeping_willow.png");
leafTexBox.setMaxWidth(Double.MAX_VALUE);
leafTexBox.setOnAction(e -> willowOptions.leafTexture =
"Textures/internal/foliage/" + leafTexBox.getValue());
inner.getChildren().add(new VBox(2, leafTexLabel, leafTexBox));
Label barkTexLabel = new Label("Rinden-Textur:");
barkTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
ComboBox<String> barkTexBox = new ComboBox<>();
barkTexBox.getItems().addAll(
"Bark001_Color.jpg", "Bark002_Color.jpg", "Bark003_Color.jpg", "Bark008_Color.jpg");
String curBark = willowOptions.barkTexture != null
? willowOptions.barkTexture.substring(willowOptions.barkTexture.lastIndexOf('/') + 1)
: "Bark001_Color.jpg";
barkTexBox.setValue(barkTexBox.getItems().contains(curBark) ? curBark : "Bark001_Color.jpg");
barkTexBox.setMaxWidth(Double.MAX_VALUE);
barkTexBox.setOnAction(e -> {
if (barkTexBox.getValue() != null) {
willowOptions.barkTexture = "Textures/internal/bark/" + barkTexBox.getValue();
}
});
inner.getChildren().add(new VBox(2, barkTexLabel, barkTexBox));
inner.getChildren().add(ezFloat("Rinde R:", 0, 1, willowOptions.barkR, v -> willowOptions.barkR = v));
inner.getChildren().add(ezFloat("Rinde G:", 0, 1, willowOptions.barkG, v -> willowOptions.barkG = v));
inner.getChildren().add(ezFloat("Rinde B:", 0, 1, willowOptions.barkB, v -> willowOptions.barkB = v));
inner.getChildren().add(ezFloat("Blatt R:", 0, 1, willowOptions.leafR, v -> willowOptions.leafR = v));
inner.getChildren().add(ezFloat("Blatt G:", 0, 1, willowOptions.leafG, v -> willowOptions.leafG = v));
inner.getChildren().add(ezFloat("Blatt B:", 0, 1, willowOptions.leafB, v -> willowOptions.leafB = v));
return wrapInPanel(inner, 270);
}
// ── Palmen-Parameter-Panel ────────────────────────────────────────────────
private VBox buildPalmParamsPanel() {
VBox inner = new VBox(6);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
inner.getChildren().add(bold("Zufallssamen:"));
Spinner<Integer> seedSp = intSpinner(0, 999999, palmOptions.seed);
seedSp.valueProperty().addListener((o, a, b) -> palmOptions.seed = b);
Button palmRndSeed = new Button("🎲");
onF6 = () -> {
int s = new java.util.Random().nextInt(1000000);
palmOptions.seed = s;
seedSp.getValueFactory().setValue(s);
onF5.run();
};
palmRndSeed.setOnAction(e -> onF6.run());
HBox palmSeedRow = new HBox(4, seedSp, palmRndSeed);
HBox.setHgrow(seedSp, Priority.ALWAYS);
inner.getChildren().add(palmSeedRow);
inner.getChildren().addAll(sectionTitle("Stamm"), new Separator());
inner.getChildren().add(bold("Anzahl Stämme:"));
Spinner<Integer> palmCountSp = intSpinner(1, 3, palmOptions.palmCount);
palmCountSp.valueProperty().addListener((o, a, b) -> palmOptions.palmCount = b);
inner.getChildren().add(palmCountSp);
inner.getChildren().add(ezFloat("Höhe:", 5, 25, palmOptions.trunkHeight,
v -> palmOptions.trunkHeight = v));
inner.getChildren().add(ezFloat("Radius unten:", 0.1, 1.0, palmOptions.trunkRadiusBottom,
v -> palmOptions.trunkRadiusBottom = v));
inner.getChildren().add(ezFloat("Radius oben:", 0.05, 0.9, palmOptions.trunkRadiusTop,
v -> palmOptions.trunkRadiusTop = v));
inner.getChildren().add(ezFloat("Neigung (°):", 0.0, 50.0, palmOptions.lean,
v -> palmOptions.lean = v));
inner.getChildren().addAll(sectionTitle("Kronenschaft"), new Separator());
inner.getChildren().add(ezFloat("Höhe:", 0.2, 4.0, palmOptions.crownHeight,
v -> palmOptions.crownHeight = v));
inner.getChildren().add(ezFloat("Aufweitung:", 1.0, 3.0, palmOptions.crownFlare,
v -> palmOptions.crownFlare = v));
inner.getChildren().add(ezFloat("Farbe R:", 0, 1, palmOptions.crownR, v -> palmOptions.crownR = v));
inner.getChildren().add(ezFloat("Farbe G:", 0, 1, palmOptions.crownG, v -> palmOptions.crownG = v));
inner.getChildren().add(ezFloat("Farbe B:", 0, 1, palmOptions.crownB, v -> palmOptions.crownB = v));
inner.getChildren().addAll(sectionTitle("Wedel"), new Separator());
inner.getChildren().add(bold("Anzahl:"));
Spinner<Integer> frondCountSp = intSpinner(12, 120, palmOptions.frondCount);
frondCountSp.valueProperty().addListener((o, a, b) -> palmOptions.frondCount = b);
inner.getChildren().add(frondCountSp);
inner.getChildren().add(ezFloat("Winkel min (° von oben):", 10, 90, palmOptions.frondAngleMin,
v -> palmOptions.frondAngleMin = v));
inner.getChildren().add(ezFloat("Winkel max (° von oben):", 90, 170, palmOptions.frondAngleMax,
v -> palmOptions.frondAngleMax = v));
inner.getChildren().add(ezFloat("Länge:", 2, 12, palmOptions.frondLength,
v -> palmOptions.frondLength = v));
inner.getChildren().addAll(sectionTitle("Blätter"), new Separator());
inner.getChildren().add(ezFloat("Schwerkraft:", 0.0, 2.0, palmOptions.gravity,
v -> palmOptions.gravity = v));
inner.getChildren().add(ezFloat("Größe oben (relativ):", 0.0, 1.0, palmOptions.frondMinSizeRatio,
v -> palmOptions.frondMinSizeRatio = v));
inner.getChildren().addAll(sectionTitle("Farben"), new Separator());
Label leafTexLabel = new Label("Blatt-Textur:");
leafTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
ComboBox<String> leafTexBox = new ComboBox<>();
leafTexBox.getItems().addAll("palm.png", "palm2.png");
String currentLeaf = palmOptions.leafTexture != null
&& palmOptions.leafTexture.contains("palm2") ? "palm2.png" : "palm.png";
leafTexBox.setValue(currentLeaf);
leafTexBox.setMaxWidth(Double.MAX_VALUE);
leafTexBox.setOnAction(e -> palmOptions.leafTexture =
"Textures/internal/foliage/" + leafTexBox.getValue());
inner.getChildren().add(new VBox(2, leafTexLabel, leafTexBox));
Label barkTexLabel = new Label("Rinden-Textur:");
barkTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
ComboBox<String> barkTexBox = new ComboBox<>();
barkTexBox.getItems().addAll(
"Bark_Palm.png", "Bark_Palm2.png",
"Bark001_Color.jpg", "Bark002_Color.jpg",
"Bark003_Color.jpg", "Bark008_Color.jpg"
);
String currentBark = palmOptions.barkTexture != null
? palmOptions.barkTexture.substring(palmOptions.barkTexture.lastIndexOf('/') + 1)
: "Bark_Palm.png";
barkTexBox.setValue(barkTexBox.getItems().contains(currentBark)
? currentBark : barkTexBox.getItems().get(0));
barkTexBox.setMaxWidth(Double.MAX_VALUE);
barkTexBox.setOnAction(e -> {
if (barkTexBox.getValue() != null) {
palmOptions.barkTexture = "Textures/internal/bark/" + barkTexBox.getValue();
}
});
inner.getChildren().add(new VBox(2, barkTexLabel, barkTexBox));
inner.getChildren().add(ezFloat("Stamm R:", 0, 1, palmOptions.barkR, v -> palmOptions.barkR = v));
inner.getChildren().add(ezFloat("Stamm G:", 0, 1, palmOptions.barkG, v -> palmOptions.barkG = v));
inner.getChildren().add(ezFloat("Stamm B:", 0, 1, palmOptions.barkB, v -> palmOptions.barkB = v));
inner.getChildren().add(ezFloat("Blatt R:", 0, 1, palmOptions.leafR, v -> palmOptions.leafR = v));
inner.getChildren().add(ezFloat("Blatt G:", 0, 1, palmOptions.leafG, v -> palmOptions.leafG = v));
inner.getChildren().add(ezFloat("Blatt B:", 0, 1, palmOptions.leafB, v -> palmOptions.leafB = v));
return wrapInPanel(inner, 270);
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
private static TreeParams presetFromName(String name) {
return switch (name) {
case "Birke" -> TreeParams.birch();
case "Kiefer" -> TreeParams.pine();
case "Weide" -> TreeParams.willow();
case "Busch" -> TreeParams.bush();
default -> TreeParams.oak();
};
}
private static String treeTypeFromPreset(String preset) {
return switch (preset) {
case "Birke" -> "birch";
case "Kiefer" -> "pine";
case "Weide" -> "willow";
case "Busch" -> "bush";
default -> "oak";
};
}
private static VBox wrapInPanel(VBox inner, int prefWidth) {
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(prefWidth);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
}