From e4b0ee0dc3898871156a5ee76efa1f688694effd Mon Sep 17 00:00:00 2001 From: Mario Date: Thu, 10 Sep 2026 21:23:46 +0200 Subject: [PATCH] Refactoring Phase 2: Vegetation & EzTree aus EditorApp extrahiert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../main/java/de/blight/editor/EditorApp.java | 1091 +---------------- .../java/de/blight/editor/ToolContext.java | 40 + .../java/de/blight/editor/ToolController.java | 42 + .../java/de/blight/editor/ToolRegistry.java | 48 + .../de/blight/editor/toolset/UiHelper.java | 89 ++ .../toolset/eztree/EzTreeController.java | 364 ++++++ .../vegetation/VegetationController.java | 832 +++++++++++++ 7 files changed, 1443 insertions(+), 1063 deletions(-) create mode 100644 blight-editor/src/main/java/de/blight/editor/ToolContext.java create mode 100644 blight-editor/src/main/java/de/blight/editor/ToolController.java create mode 100644 blight-editor/src/main/java/de/blight/editor/ToolRegistry.java create mode 100644 blight-editor/src/main/java/de/blight/editor/toolset/UiHelper.java create mode 100644 blight-editor/src/main/java/de/blight/editor/toolset/eztree/EzTreeController.java create mode 100644 blight-editor/src/main/java/de/blight/editor/toolset/vegetation/VegetationController.java diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 9b409f9..1e29256 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -5,15 +5,9 @@ import de.blight.editor.tool.ChoiceToolParameter; import de.blight.editor.tool.EditorTool; import de.blight.editor.tool.GrassTool; import de.blight.editor.tool.ToolParameter; -import de.blight.editor.tree.PalmOptions; -import de.blight.editor.tree.TreeParams; import de.blight.editor.ui.MapObjectsView; import de.blight.editor.ui.TextureChooser; import de.blight.editor.ui.TextureDetailDialog; -import de.blight.eztree.Billboard; -import de.blight.eztree.TreeOptions; -import de.blight.eztree.TreePresets; -import de.blight.eztree.TreeType; import javafx.embed.swing.SwingFXUtils; import javafx.application.Application; import javafx.application.Platform; @@ -129,43 +123,19 @@ public class EditorApp extends Application { private javafx.scene.Node worldBottomBar; private boolean contentTreeUpdating = false; - private ImageView treePreviewView; // aktualisiert wenn JME3 Bild neu erstellt - private StackPane plantPreviewPanel; // einmaliges Vorschau-Panel für alle Generatoren private Stage primaryStage; private JmeEditorApp jmeApp; - // Baum-Generator-Zustand (wird beim Preset-Wechsel neu gesetzt) - private TreeParams treeParams = TreeParams.oak(); - private String currentTreePreset = "Eiche"; - - // EZ-Tree-Zustand - private TreeOptions ezTreeOptions = TreePresets.oakMedium(); - private String ezTreePresetName = "Oak Medium"; - - // Palmen-Generator-Zustand - private PalmOptions palmOptions = new PalmOptions(); - - // Farn-Generator-Zustand - private de.blight.editor.tree.FernOptions fernOptions = new de.blight.editor.tree.FernOptions(); - - // Fruchtbusch-Generator-Zustand - private de.blight.editor.tree.FruitBushOptions fruitBushOptions = - de.blight.editor.tree.FruitBushOptions.orange(); - private String fruitBushVariant = "Orange"; - - // Weinpflanzen-Generator-Zustand - private de.blight.editor.tree.GrapevineOptions grapevineOptions = new de.blight.editor.tree.GrapevineOptions(); - - // Trauerweide-Generator-Zustand - private de.blight.editor.tree.WillowOptions willowOptions = new de.blight.editor.tree.WillowOptions(); - - // Vegetations-Generator-Zustand - private String vegetationType = "Baum (Eiche)"; // Aktives Tool + Tastenkürzel-Callbacks private String currentTool = "world"; - private Runnable onF5 = () -> {}; // Vorschau - private Runnable onF6 = () -> {}; // Zufälligen Seed wählen + private Runnable onF5 = () -> {}; // Vorschau (nicht-migrierte Tools) + private Runnable onF6 = () -> {}; // Zufälligen Seed (nicht-migrierte Tools) + + // ── Refactored Tool-Controller (Phase 2+) ───────────────────────────────── + private final ToolRegistry registry = new ToolRegistry(); + private de.blight.editor.toolset.vegetation.VegetationController vegController; + private de.blight.editor.toolset.eztree.EzTreeController ezTreeController; // Asset-Panel: Pfad-Map + DnD-Zustand private final Map, Path> itemPaths = new HashMap<>(); @@ -483,6 +453,16 @@ public class EditorApp extends Application { worldBottomBar = buildBottomBox(); root.setBottom(worldBottomBar); + // ── Tool-Controller (Phase 2-Extraktion) ────────────────────────────── + ToolContext toolCtx = new ToolContext( + input, jmeApp, () -> stage, + this::setStatus, this::setCenterView, + root::setRight, node -> topBar.getChildren().set(1, node), + () -> centerStack, ASSET_ROOT, this::switchToWorldEditor + ); + vegController = new de.blight.editor.toolset.vegetation.VegetationController(toolCtx); + ezTreeController = new de.blight.editor.toolset.eztree.EzTreeController(toolCtx); + Scene scene = new Scene(root, 1280, 760); java.net.URL cssUrl = getClass().getResource("/editor.css"); if (cssUrl != null) scene.getStylesheets().add(cssUrl.toExternalForm()); @@ -606,14 +586,7 @@ public class EditorApp extends Application { } } - String treeMsg = input.treeGenStatusMsg; - if (treeMsg != null) { input.treeGenStatusMsg = null; setStatus(treeMsg); } - - if (input.treePreviewResized) { - input.treePreviewResized = false; - if (treePreviewView != null) - treePreviewView.setImage(input.treePreviewImage); - } + registry.onFrame(); if (input.animPreviewResized) { input.animPreviewResized = false; @@ -1052,9 +1025,7 @@ public class EditorApp extends Application { private void switchToVegetationGenerator() { ensureNotContentEditor(); currentTool = "vegetation"; - topBar.getChildren().set(1, buildVegetationToolBar()); - setCenterView(getPlantPreviewPanel()); - root.setRight(buildVegetationParamsPanel()); + registry.activate(vegController); } private void switchToWorldEditor() { @@ -1216,324 +1187,9 @@ public class EditorApp extends Application { private void switchToEzTree() { ensureNotContentEditor(); currentTool = "eztree"; - topBar.getChildren().set(1, buildEzTreeToolBar()); - setCenterView(getPlantPreviewPanel()); - root.setRight(buildEzTreeParamsPanel()); + registry.activate(ezTreeController); } - private ToolBar buildVegetationToolBar() { - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - - Label typeLabel = new Label("Pflanzentyp:"); - typeLabel.setStyle("-fx-font-weight: bold;"); - ComboBox 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); - } - updateVegetationOnF5(); - root.setRight(buildVegetationParamsPanel()); - 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 -> { - 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)); - } - setStatus("Generiere und exportiere…"); - }); - - Button rndBtn = new Button("🎲 Neuer Seed"); - rndBtn.setOnAction(e -> onF6.run()); - - updateVegetationOnF5(); - - ToolBar bar = new ToolBar(); - bar.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), - typeLabel, typeBox, - new Separator(Orientation.VERTICAL), - previewBtn, exportBtn, rndBtn); - return bar; - } - - private void updateVegetationOnF5() { - if ("Baum (Trauerweide)".equals(vegetationType)) { - onF5 = () -> { - input.willowGenQueue.offer( - new SharedInput.WillowGenRequest(willowOptions.copy(), false)); - setStatus("Trauerweide: generiere Vorschau…"); - }; - onF6 = () -> { - willowOptions.seed = new java.util.Random().nextInt(1000000); - root.setRight(buildVegetationParamsPanel()); - onF5.run(); - }; - } else if (vegetationType.startsWith("Baum (")) { - onF5 = () -> { - input.treeGenQueue.offer(new SharedInput.TreeGenRequest( - treeParams.copy(), false, treeTypeFromPreset(currentTreePreset))); - setStatus("Generiere Vorschau…"); - }; - onF6 = () -> { - int s = new java.util.Random().nextInt(100000); - treeParams.seed = s; - onF5.run(); - }; - } else if ("Farn".equals(vegetationType)) { - onF5 = () -> { - input.fernGenQueue.offer(new SharedInput.FernGenRequest(fernOptions.copy(), false)); - setStatus("Generiere Farn-Vorschau…"); - }; - onF6 = () -> { - fernOptions.seed = new java.util.Random().nextInt(1000000); - root.setRight(buildVegetationParamsPanel()); - onF5.run(); - }; - } else if ("Fruchtbusch".equals(vegetationType)) { - onF5 = () -> { - input.fruitBushGenQueue.offer( - new SharedInput.FruitBushGenRequest(fruitBushOptions.copy(), fruitBushVariant, false)); - setStatus("Fruchtbusch: generiere Vorschau…"); - }; - onF6 = () -> { - fruitBushOptions.seed = new java.util.Random().nextInt(1000000); - root.setRight(buildVegetationParamsPanel()); - onF5.run(); - }; - } else if ("Weinpflanze".equals(vegetationType)) { - onF5 = () -> { - input.grapevineGenQueue.offer( - new SharedInput.GrapevineGenRequest(grapevineOptions.copy(), false)); - setStatus("Weinpflanze: generiere Vorschau…"); - }; - onF6 = () -> { - grapevineOptions.seed = new java.util.Random().nextInt(1000000); - root.setRight(buildVegetationParamsPanel()); - onF5.run(); - }; - } else { - onF5 = () -> { - input.palmGenQueue.offer(new SharedInput.PalmGenRequest(palmOptions.copy(), false)); - setStatus("Palme: generiere Vorschau…"); - }; - onF6 = () -> { - palmOptions.seed = new java.util.Random().nextInt(1000000); - root.setRight(buildVegetationParamsPanel()); - onF5.run(); - }; - } - } - - private javafx.scene.Node buildVegetationParamsPanel() { - 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(); - } - } - - 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 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); - if (onF5 != null) 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 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 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 -> { if (onF5 != null) onF5.run(); }); - - inner.getChildren().add(new Separator()); - inner.getChildren().add(previewBtn); - - 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 VBox buildFruitBushParamsPanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - - // ── Variante ────────────────────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Fruchtbusch-Typ"), new Separator()); - ComboBox 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" -> de.blight.editor.tree.FruitBushOptions.lemon(); - case "Aprikose" -> de.blight.editor.tree.FruitBushOptions.apricot(); - default -> de.blight.editor.tree.FruitBushOptions.orange(); - }; - fruitBushOptions.seed = oldSeed; - root.setRight(buildVegetationParamsPanel()); - if (onF5 != null) onF5.run(); - }); - inner.getChildren().add(variantBox); - - // ── Allgemein ───────────────────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator()); - inner.getChildren().add(bold("Zufallssamen:")); - Spinner 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); - if (onF5 != null) onF5.run(); - }); - HBox seedRow = new HBox(4, seedSp, rndSeedBtn); - HBox.setHgrow(seedSp, Priority.ALWAYS); - inner.getChildren().add(seedRow); - - // ── Stamm ───────────────────────────────────────────────────────────── - 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)); - - // ── Äste ────────────────────────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Äste"), new Separator()); - inner.getChildren().add(bold("Anzahl Primäräste:")); - Spinner 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 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)); - - // ── Blätter & Früchte ───────────────────────────────────────────────── - 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 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)); - - // ── Wind ────────────────────────────────────────────────────────────── - 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 -> { if (onF5 != null) onF5.run(); }); - inner.getChildren().addAll(new Separator(), previewBtn); - - 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 switchToTripo() { ensureNotContentEditor(); @@ -1795,216 +1451,6 @@ public class EditorApp extends Application { return new VBox(menuBar, toolBar); } - // ── Baum-Generator – Toolbar (ersetzt die Welt-Toolbar) ────────────────── - - // ── Baum-Generator – zentrales Vorschau-Panel (ersetzt den Welt-Viewport) ── - - private StackPane getPlantPreviewPanel() { - if (plantPreviewPanel != null) return plantPreviewPanel; - - 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 plantPreviewPanel; - } - - // ── Weinpflanzen-Generator – rechtes 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 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); - if (onF5 != null) 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 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 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 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 -> { if (onF5 != null) onF5.run(); }); - - inner.getChildren().add(new Separator()); - inner.getChildren().add(previewBtn); - - 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; - } - - // ── Baum-Generator – rechtes 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()); - - // Zufallssamen - inner.getChildren().add(bold("Zufallssamen:")); - Spinner 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); - - // Ast-Tiefe - inner.getChildren().add(bold("Ast-Tiefe:")); - Spinner 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); - - // Stamm-Aufrichtung - inner.getChildren().add(paramSlider("Stamm-Aufrichtung:", 0.0, 0.40, treeParams.trunkUprightness, - v -> treeParams.trunkUprightness = v)); - - inner.getChildren().add(new Separator()); - - // Blätter - 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 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 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()); - - // Wind - 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)); - - 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(260); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - return panel; - } // ── Kleine Helpers für Baum-Parameter-Panel ────────────────────────────── @@ -2066,475 +1512,7 @@ public class EditorApp extends Application { return new VBox(3, name, withField(slider, "%.3f")); } - // ── EZ-Tree – Toolbar ──────────────────────────────────────────────────── - private ToolBar buildEzTreeToolBar() { - Label presetLabel = new Label("Preset:"); - presetLabel.setStyle("-fx-font-weight: bold;"); - ComboBox presetBox = new ComboBox<>(); - presetBox.getItems().addAll(TreePresets.presetNames()); - presetBox.setValue("Oak Medium"); - presetBox.setOnAction(e -> { - ezTreePresetName = presetBox.getValue(); - ezTreeOptions = TreePresets.byName(ezTreePresetName); - root.setRight(buildEzTreeParamsPanel()); - }); - - onF5 = () -> { - input.ezTreeGenQueue.offer(new SharedInput.EzTreeGenRequest( - ezTreeOptions.copy(), ezTreePresetName, false)); - setStatus("EZ-Tree: generiere Vorschau…"); - }; - 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 -> { - input.ezTreeGenQueue.offer(new SharedInput.EzTreeGenRequest( - ezTreeOptions.copy(), ezTreePresetName, true)); - setStatus("EZ-Tree: generiere und exportiere…"); - }); - - ToolBar bar = new ToolBar(); - bar.getItems().addAll( - presetLabel, presetBox, - new Separator(Orientation.VERTICAL), - previewBtn, exportBtn - ); - return bar; - } - - // ── EZ-Tree – Parameter-Panel (alle Optionen) ──────────────────────────── - - private VBox buildEzTreeParamsPanel() { - VBox inner = new VBox(6); - inner.setPadding(new Insets(10)); - - // ── Allgemein ──────────────────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator()); - - inner.getChildren().add(bold("Zufallssamen:")); - Spinner 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 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 levelsSp = intSpinner(1, 5, ezTreeOptions.branch.levels); - levelsSp.valueProperty().addListener((o, a, b) -> { - ezTreeOptions.branch.levels = b; - root.setRight(buildEzTreeParamsPanel()); // rebuild für neue Level-Sektionen - }); - inner.getChildren().add(levelsSp); - - // ── Rinde ──────────────────────────────────────────────────────────── - 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)); - - // ── Wachstumskraft ─────────────────────────────────────────────────── - 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)); - - // ── Pro-Ebene-Sektionen ────────────────────────────────────────────── - int maxLevel = ezTreeOptions.branch.levels; - for (int lv = 0; lv <= maxLevel; lv++) { - buildLevelSection(inner, lv, maxLevel); - } - - // ── Blätter ────────────────────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Blätter"), new Separator()); - - inner.getChildren().add(bold("Billboard:")); - ChoiceBox 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 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)); - - // ── Spalier ────────────────────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Spalier (Trellis)"), new Separator()); - - CheckBox trellisEnCB = new 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 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 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; - } - - // ── Trauerweide-Generator – 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 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 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 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 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 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 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)); - - 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; - } - - // ── Palmen-Generator – 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 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 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 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()); - // Blatt-Textur-Auswahl - Label leafTexLabel = new Label("Blatt-Textur:"); - leafTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); - ComboBox 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)); - - // Rinden-Textur-Auswahl (alle Texturen aus dem bark-Ordner) - Label barkTexLabel = new Label("Rinden-Textur:"); - barkTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); - ComboBox 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)); - - 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; - - // Winkel + Start nur ab Ebene 1 (da Kind-Eigenschaften) - 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))); - } - - // Kinder nur wenn noch weitere Ebenen folgen - if (lv < maxLevel) { - container.getChildren().add(bold("Kinder:")); - Spinner 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 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 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))); - } // ── EZ-Tree UI-Helpers ─────────────────────────────────────────────────── @@ -2570,25 +1548,6 @@ public class EditorApp extends Application { return names[0]; } - 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"; - }; - } // ── Item-Platzierung ───────────────────────────────────────────────────── @@ -10015,13 +8974,19 @@ public class EditorApp extends Application { case F5 -> { if (pressed) { if ("world".equals(currentTool)) Platform.runLater(() -> { if (grassBtn != null) grassBtn.fire(); }); - else onF5.run(); + else { + ToolController active = registry.getActive(); + if (active != null) { active.onPreview(); } else if (onF5 != null) { onF5.run(); } + } } } case F6 -> { if (pressed) { if ("world".equals(currentTool)) Platform.runLater(() -> { if (stoneBtn != null) stoneBtn.fire(); }); - else onF6.run(); + else { + ToolController active = registry.getActive(); + if (active != null) { active.onRandomSeed(); } else if (onF6 != null) { onF6.run(); } + } } } case F7 -> { diff --git a/blight-editor/src/main/java/de/blight/editor/ToolContext.java b/blight-editor/src/main/java/de/blight/editor/ToolContext.java new file mode 100644 index 0000000..081f91c --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/ToolContext.java @@ -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 stageSupplier, + Consumer setStatus, + Consumer setCenterView, + Consumer setRightPanel, + Consumer setMainToolbar, + Supplier viewportSupplier, + Path assetRoot, + Runnable switchToWorld +) {} diff --git a/blight-editor/src/main/java/de/blight/editor/ToolController.java b/blight-editor/src/main/java/de/blight/editor/ToolController.java new file mode 100644 index 0000000..73bc425 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/ToolController.java @@ -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() {} +} diff --git a/blight-editor/src/main/java/de/blight/editor/ToolRegistry.java b/blight-editor/src/main/java/de/blight/editor/ToolRegistry.java new file mode 100644 index 0000000..ab12846 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/ToolRegistry.java @@ -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(); + } + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/UiHelper.java b/blight-editor/src/main/java/de/blight/editor/toolset/UiHelper.java new file mode 100644 index 0000000..348ae4b --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/UiHelper.java @@ -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 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 intSpinner(int min, int max, int init) { + Spinner 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 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")); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/eztree/EzTreeController.java b/blight-editor/src/main/java/de/blight/editor/toolset/eztree/EzTreeController.java new file mode 100644 index 0000000..67a3870 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/eztree/EzTreeController.java @@ -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 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 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 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 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 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 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 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 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 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 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 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))); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/vegetation/VegetationController.java b/blight-editor/src/main/java/de/blight/editor/toolset/vegetation/VegetationController.java new file mode 100644 index 0000000..e24ddb2 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/vegetation/VegetationController.java @@ -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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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; + } +}