From 8f0cf7bc429f47381f201a181e92c97b48fdef0e Mon Sep 17 00:00:00 2001 From: Mario Date: Fri, 11 Sep 2026 11:01:14 +0200 Subject: [PATCH] Phase 5: Atmosphere, Water, Voxel, Grass aus EditorApp extrahiert (-1.170 Z.) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AtmosphereController: Licht- + Emitter-Panel, updateLight/Emitter, onFrame() - WaterController: Wasser- + Wasserfall-Panel, updateWater/Waterfall, onFrame() - VoxelController: VoxelMerged- + Klippen-Panel; sculptPanelSupplier-Injection für showToolParameters-Delegation; onCliffGenerateKeyPressed() für Keyboard - GrassController: Gras-Panel + GrassTextureUI; viewport/showToolParams/ normalMapLoader injiziert; buildGrassTextureUI() auch von showToolParameters in EditorApp aufgerufen - EditorApp: controller.onFrame()-Aufrufe im AnimationTimer; Toolbar-Buttons delegieren an controller.buildPanel(); Timer-Blöcke für Selection-Changes aus EditorApp entfernt (→ onFrame); Felder lightDynamic, waterDynamic, voxelCliff*, grassSub*, lightBtn/emitterBtn/waterBtn/riverBtn/voxelBtn entfernt Co-Authored-By: Claude Sonnet 4.6 --- .../main/java/de/blight/editor/EditorApp.java | 1276 +---------------- .../atmosphere/AtmosphereController.java | 379 +++++ .../editor/toolset/grass/GrassController.java | 324 +++++ .../editor/toolset/voxel/VoxelController.java | 214 +++ .../editor/toolset/water/WaterController.java | 347 +++++ 5 files changed, 1307 insertions(+), 1233 deletions(-) create mode 100644 blight-editor/src/main/java/de/blight/editor/toolset/atmosphere/AtmosphereController.java create mode 100644 blight-editor/src/main/java/de/blight/editor/toolset/grass/GrassController.java create mode 100644 blight-editor/src/main/java/de/blight/editor/toolset/voxel/VoxelController.java create mode 100644 blight-editor/src/main/java/de/blight/editor/toolset/water/WaterController.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 0a71d49..391f275 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -138,8 +138,12 @@ public class EditorApp extends Application { private de.blight.editor.toolset.eztree.EzTreeController ezTreeController; private de.blight.editor.toolset.anim.AnimPreviewController animPreviewController; private de.blight.editor.toolset.anim.AnimSetController animSetController; - private de.blight.editor.toolset.object.ObjectController objectController; - private de.blight.editor.toolset.model.ModelEditorController modelEditorController; + private de.blight.editor.toolset.object.ObjectController objectController; + private de.blight.editor.toolset.model.ModelEditorController modelEditorController; + private de.blight.editor.toolset.atmosphere.AtmosphereController atmosphereController; + private de.blight.editor.toolset.water.WaterController waterController; + private de.blight.editor.toolset.voxel.VoxelController voxelController; + private de.blight.editor.toolset.grass.GrassController grassController; // Asset-Panel: Pfad-Map + DnD-Zustand private final Map, Path> itemPaths = new HashMap<>(); @@ -185,27 +189,12 @@ public class EditorApp extends Application { private Label charEditorStatusLabel; private javafx.scene.control.ListView charListView; - // Licht-Werkzeug-Zustand - private VBox lightDynamicContent; - - // Emitter-Werkzeug-Zustand - private VBox emitterDynamicContent; - - // Wasser-Werkzeug-Zustand - private VBox waterDynamicContent; - private Label waterCurrentHeightLabel; - private VBox wasserContentBox; // contentBox im Wasser-Merged-Panel für Live-Updates - // Ansichts-Umschalter private boolean wireframeActive = false; private CheckMenuItem viewTopologyItem = null; // Sound-Bereich-Werkzeug-Zustand private VBox soundAreaDynamicContent; - private VBox voxelCliffDynamicContent; - private Label voxelCliffStatusLabel; - private Button voxelCliffGenerateBtn; - private Button voxelCliffDeleteBtn; // Bereich-Werkzeug-Zustand private VBox areaDynamicContent; @@ -251,22 +240,13 @@ public class EditorApp extends Application { // Toolbar-Buttons (müssen vom Status-Poller erreichbar sein) private ToggleButton baseBtn; private ToggleButton grassBtn; // einziger Toolbar-Button für beide Gras-Tools - private ToggleButton grassSubTexBtn; // Sub-Toggle im Panel: Textur-Gras - private ToggleButton grassSubVertBtn; // Sub-Toggle im Panel: Vertex-Gras - private ToggleButton grassSubTerrainBtn; // Sub-Toggle im Panel: Terrain-Farb-Gras private ToggleButton textureBtn; private ToggleButton stoneBtn; private ToggleButton objPlaceBtn; // Unified Objekte-Button (Place + Edit) - private ToggleButton lightBtn; // Sub-Toggle in Atmosphäre-Panel - private ToggleButton emitterBtn; // Sub-Toggle in Atmosphäre-Panel - private ToggleButton waterBtn; // Sub-Toggle in Wasser-Panel - private ToggleButton riverBtn; // Sub-Toggle in Wasser-Panel private ToggleButton wasserMergedBtn; // Haupt-Toolbar: Wasser + Wasserfall private ToggleButton wegeBtn; // Haupt-Toolbar: Wege / Pfadnetz private ToggleButton zonenBtn; private ToggleButton playToolBtn; - private ToggleButton voxelBtn; // Sub-Toggle in Voxel-Panel - private ToggleButton voxelCliffBtn; // Sub-Toggle in Voxel-Panel private ToggleButton auswahlBtn; // Universelles Selektionswerkzeug (F1) private ToggleButton atmosphaereBtn; // Merged: Licht + Emitter private ToggleButton voxelMergedBtn; // Merged: Voxel Sculpting + Klippe @@ -369,6 +349,22 @@ public class EditorApp extends Application { objectController = new de.blight.editor.toolset.object.ObjectController(toolCtx); modelEditorController = new de.blight.editor.toolset.model.ModelEditorController(toolCtx, this::openAssetOverlay); modelEditorController.setObjectController(objectController); + atmosphereController = new de.blight.editor.toolset.atmosphere.AtmosphereController(toolCtx); + waterController = new de.blight.editor.toolset.water.WaterController(toolCtx); + voxelController = new de.blight.editor.toolset.voxel.VoxelController(toolCtx, + () -> { + input.voxelTool.brushRadius.setValue(input.heightTool.brushRadius.getValue()); + input.voxelTool.brushStrength.setValue(input.heightTool.brushStrength.getValue()); + input.voxelTool.plateauTarget.setValue(input.heightTool.plateauHeight.getValue()); + input.activeLayer = SharedInput.LAYER_VOXEL; + input.activeTool = input.voxelTool; + showToolParameters(toolPanel, input.activeTool); + return toolPanel; + }, + () -> { if (voxelMergedBtn != null) voxelMergedBtn.fire(); }); + grassController = new de.blight.editor.toolset.grass.GrassController(toolCtx, + viewport, this::showToolParameters, + colorAbsPath -> { de.blight.editor.TextureMapData d = loadTexMeta(colorAbsPath); return d.normalMap != null ? d.normalMap : ""; }); Scene scene = new Scene(root, 1280, 760); java.net.URL cssUrl = getClass().getResource("/editor.css"); @@ -494,6 +490,9 @@ public class EditorApp extends Application { } registry.onFrame(); + atmosphereController.onFrame(); + waterController.onFrame(); + voxelController.onFrame(); java.util.Set skeletal = input.skeletalPaths.getAndSet(null); if (skeletal != null) { @@ -551,36 +550,11 @@ public class EditorApp extends Application { if (charactersNode != null) refreshCategoryNode(charactersNode, pe); } - if (input.lightSelectionChanged) { - input.lightSelectionChanged = false; - updateLightPanel(input.selectedLightInfo); - } - - if (input.emitterSelectionChanged) { - input.emitterSelectionChanged = false; - updateEmitterPanel(input.selectedEmitterInfo); - } - - if (input.waterSelectionChanged) { - input.waterSelectionChanged = false; - updateWaterPanel(input.selectedWaterInfo); - } - - if (input.waterfallSelectionChanged) { - input.waterfallSelectionChanged = false; - updateWaterfallPanel(input.selectedWaterfallInfo); - } - if (input.refreshWorldTree) { input.refreshWorldTree = false; mapObjectsView.refresh(); } - if (input.waterHeightChanged) { - input.waterHeightChanged = false; - updateWaterHeightDisplay(input.waterCurrentHeight); - } - drawCompass(); // Spiel-Konsole: gepufferte Zeilen gebündelt ausgeben (max 200 auf einmal) @@ -631,8 +605,8 @@ public class EditorApp extends Application { input.auswahlLightSelected = false; if (atmosphaereBtn != null) { atmosphaereBtn.setSelected(true); - root.setRight(buildAtmospherePanel()); - updateLightPanel(input.selectedLightInfo); + root.setRight(atmosphereController.buildPanel()); + atmosphereController.updateLightPanel(input.selectedLightInfo); } } @@ -641,8 +615,8 @@ public class EditorApp extends Application { if (atmosphaereBtn != null) { atmosphaereBtn.setSelected(true); input.activeLayer = SharedInput.LAYER_EMITTERS; - root.setRight(buildAtmospherePanel()); - updateEmitterPanel(input.selectedEmitterInfo); + root.setRight(atmosphereController.buildPanel()); + atmosphereController.updateEmitterPanel(input.selectedEmitterInfo); } } @@ -650,16 +624,16 @@ public class EditorApp extends Application { input.auswahlWaterSelected = false; if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true); input.activeLayer = SharedInput.LAYER_WATER; - root.setRight(buildWasserPanel()); - updateWaterPanel(input.selectedWaterInfo); + root.setRight(waterController.buildPanel()); + waterController.updateWaterPanel(input.selectedWaterInfo); } if (input.auswahlWaterfallSelected) { input.auswahlWaterfallSelected = false; if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true); input.activeLayer = SharedInput.LAYER_WATERFALL; - root.setRight(buildWasserPanel()); - updateWaterfallPanel(input.selectedWaterfallInfo); + root.setRight(waterController.buildPanel()); + waterController.updateWaterfallPanel(input.selectedWaterfallInfo); } if (input.itemSelectionChanged) { @@ -673,23 +647,6 @@ public class EditorApp extends Application { if (zonenKindStyleUpdater != null) zonenKindStyleUpdater.run(); } - if (input.cliffZoneSelectionChanged) { - input.cliffZoneSelectionChanged = false; - updateVoxelCliffPanel(input.selectedCliffZoneIdx >= 0); - } - - String cliffMsg = input.cliffGenStatusMsg; - if (cliffMsg != null) { - input.cliffGenStatusMsg = null; - updateVoxelCliffStatus(cliffMsg); - } - - if (input.cliffGenComplete) { - input.cliffGenComplete = false; - // Zu Voxel-Tool wechseln, damit der User direkt backen kann - Platform.runLater(() -> { if (voxelMergedBtn != null) voxelMergedBtn.fire(); }); - } - if (input.spawnPickChanged) { input.spawnPickChanged = false; updateSpawnFields(input.pickedSpawnInfo); @@ -1003,14 +960,6 @@ public class EditorApp extends Application { wegeBtn = new ToggleButton("-- Wege"); zonenBtn = new ToggleButton("[ ] Zonen"); playToolBtn = new ToggleButton("▶ Spielen"); - // Interne Sub-Toggle-Buttons (nicht in Toolbar, werden in Panel-Buildern erzeugt) - lightBtn = null; - emitterBtn = null; - voxelBtn = null; - voxelCliffBtn = null; - waterBtn = null; - riverBtn = null; - final String BOLD = "-fx-font-weight:bold;"; auswahlBtn.setStyle(BOLD); baseBtn.setStyle(BOLD); @@ -1054,13 +1003,13 @@ public class EditorApp extends Application { root.setRight(toolPanel); showToolParameters(toolPanel, input.activeTool); }); - voxelMergedBtn.setOnAction(e -> root.setRight(buildVoxelMergedPanel())); + voxelMergedBtn.setOnAction(e -> root.setRight(voxelController.buildPanel())); textureBtn.setOnAction(e -> { input.activeLayer = 4; input.activeTool = input.textureTool; root.setRight(toolPanel); showToolParameters(toolPanel, input.activeTool); }); - grassBtn.setOnAction(e -> root.setRight(buildGrassPanel())); + grassBtn.setOnAction(e -> root.setRight(grassController.buildPanel())); stoneBtn.setOnAction(e -> { input.activeLayer = SharedInput.LAYER_STONE; input.activeTool = input.stoneTool; @@ -1072,8 +1021,8 @@ public class EditorApp extends Application { openAssetOverlay(); objectController.activatePlaceMode(); }); - atmosphaereBtn.setOnAction(e -> root.setRight(buildAtmospherePanel())); - wasserMergedBtn.setOnAction(e -> root.setRight(buildWasserPanel())); + atmosphaereBtn.setOnAction(e -> root.setRight(atmosphereController.buildPanel())); + wasserMergedBtn.setOnAction(e -> root.setRight(waterController.buildPanel())); wegeBtn.setOnAction(e -> { input.activeLayer = SharedInput.LAYER_PATH_NETWORK; root.setRight(buildWegePanel()); @@ -1239,607 +1188,13 @@ public class EditorApp extends Application { // ── Objekt-Werkzeug – Platzieren-Panel ────────────────────────────────── - private VBox buildLightPanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - inner.getChildren().addAll( - sectionTitle("Lichtquellen"), - new Separator(), - sectionTitle("Gewähltes Licht"), - new Separator()); - - lightDynamicContent = new VBox(6); - Label noSel = new Label("Kein Licht ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - lightDynamicContent.getChildren().add(noSel); - inner.getChildren().add(lightDynamicContent); - - inner.getChildren().addAll( - new Separator(), - styledHint("Linksklick → platzieren / auswählen"), - styledHint("Rechtsklick → Auswahl aufheben"), - styledHint("Entf → ausgewähltes Licht löschen")); - - 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; - } - - private void updateLightPanel(String info) { - if (lightDynamicContent == null) return; - lightDynamicContent.getChildren().clear(); - - if (info == null) { - Label noSel = new Label("Kein Licht ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - lightDynamicContent.getChildren().add(noSel); - return; - } - - // Format: "idx|x|y|z|r|g|b|intensity|radius" - String[] parts = info.split("\\|", -1); - if (parts.length < 9) return; - float[] vals = new float[9]; - try { - for (int i = 0; i < 9; i++) vals[i] = Float.parseFloat(parts[i]); - } catch (NumberFormatException e) { return; } - - Label posLabel = new Label(String.format("X: %.1f Y: %.1f Z: %.1f", vals[1], vals[2], vals[3])); - posLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); - lightDynamicContent.getChildren().addAll(posLabel, new Separator()); - - // cur[0]=r, cur[1]=g, cur[2]=b, cur[3]=intensity, cur[4]=radius - float[] cur = {vals[4], vals[5], vals[6], vals[7], vals[8]}; - - String[] labels = {"Rot", "Grün", "Blau", "Intensität", "Radius"}; - double[] mins = {0, 0, 0, 0.1, 0.5}; - double[] maxs = {1, 1, 1, 10.0, 200.0}; - double[] steps = {0.01, 0.01, 0.01, 0.1, 1.0}; - - Runnable publish = () -> input.pendingLightProp.set( - new SharedInput.LightPropertyChange(cur[0], cur[1], cur[2], cur[3], cur[4])); - - for (int i = 0; i < 5; i++) { - final int idx = i; - Label lbl = new Label(labels[i]); - lbl.setStyle("-fx-font-size: 11;"); - Slider sl = new Slider(mins[i], maxs[i], cur[i]); - sl.setShowTickLabels(false); - sl.setBlockIncrement(steps[i]); - sl.valueProperty().addListener((o, ov, nv) -> { - cur[idx] = nv.floatValue(); - publish.run(); - }); - lightDynamicContent.getChildren().addAll(lbl, withField(sl, "%.2f")); - } - - Button delBtn = new Button("🗑 Löschen"); - delBtn.setMaxWidth(Double.MAX_VALUE); - delBtn.setStyle("-fx-text-fill: #c0392b;"); - delBtn.setOnAction(e -> input.deleteLightRequested = true); - lightDynamicContent.getChildren().addAll(new Separator(), delBtn); - } - // ── Wasser-Panel ────────────────────────────────────────────────────────── - private VBox buildWaterPanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - - waterCurrentHeightLabel = new Label("Höhe: –"); - waterCurrentHeightLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold;"); - - inner.getChildren().addAll( - sectionTitle("Wasseroberflächen"), - new Separator(), - waterCurrentHeightLabel, - new Separator(), - sectionTitle("Gewählte Fläche"), - new Separator()); - - waterDynamicContent = new VBox(6); - Label noSel = new Label("Keine Fläche ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - waterDynamicContent.getChildren().add(noSel); - inner.getChildren().add(waterDynamicContent); - - inner.getChildren().addAll( - new Separator(), - styledHint("L-Klick (leer) → erste Ecke setzen"), - styledHint("L-Klick (2.) → Quad abschliessen"), - styledHint("Handle ziehen → Ecke / Kante verschieben"), - styledHint("R-Klick → Abbrechen / Auswahl aufheben"), - styledHint("Leertaste → Höhe vom Terrain übernehmen"), - styledHint("ESC → Abbrechen / Auswahl aufheben"), - styledHint("Entf → Quad löschen")); - - 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; - } - - private VBox buildWaterfallPanel() { - VBox inner = new VBox(10); - inner.setPadding(new Insets(10)); - - Button undoBtn = new Button("Letzten Punkt entfernen"); - undoBtn.setMaxWidth(Double.MAX_VALUE); - undoBtn.setOnAction(e -> input.undoWaterfallPointRequested = true); - - inner.getChildren().addAll( - sectionTitle("Wasserfall (4-Klick Quad)"), - new Separator(), - undoBtn, - new Separator(), - styledHint("L-Klick → Punkt setzen (1→2→3→4)"), - styledHint("Punkte auf Klippen, Voxel & Seenkanten möglich"), - styledHint("R-Klick / Backspace → letzten Punkt rückgängig"), - styledHint("ESC → Platzierung abbrechen"), - styledHint("Wasserfall anklicken → Ecken verschiebbar")); - - 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; - } - - private void updateWaterfallPanel(String info) { - if (input.activeLayer != SharedInput.LAYER_WATERFALL) return; - VBox panel = info != null ? buildWaterfallSelectionPanel(info) : buildWaterfallPanel(); - if (wasserContentBox != null) { - wasserContentBox.getChildren().setAll(panel); - } else { - root.setRight(buildWasserPanel()); - } - } - - private VBox buildWaterfallSelectionPanel(String info) { - // info = "idx|numPoints|totalLengthM" - String[] parts = info.split("\\|"); - int numPts = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; - String length = parts.length > 2 ? parts[2] + " m" : "?"; - - VBox inner = new VBox(10); - inner.setPadding(new Insets(10)); - - Label infoLabel = new Label("Punkte: " + numPts + " Länge: ~" + length); - infoLabel.setStyle("-fx-font-size: 11;"); - - Button deleteBtn = new Button("Wasserfall löschen"); - deleteBtn.setMaxWidth(Double.MAX_VALUE); - deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white;"); - deleteBtn.setOnAction(e -> { - javafx.scene.control.Alert confirm = new javafx.scene.control.Alert( - javafx.scene.control.Alert.AlertType.CONFIRMATION, - "Wasserfall unwiderruflich löschen?", - javafx.scene.control.ButtonType.YES, - javafx.scene.control.ButtonType.NO); - confirm.setTitle("Wasserfall löschen"); - confirm.setHeaderText(null); - confirm.initOwner(primaryStage); - confirm.showAndWait().ifPresent(btn -> { - if (btn == javafx.scene.control.ButtonType.YES) input.deleteWaterfallRequested = true; - }); - }); - - inner.getChildren().addAll( - sectionTitle("Wasserfall selektiert"), - new Separator(), - infoLabel, - new Separator(), - deleteBtn, - new Separator(), - styledHint("L-Klick ins Leere → abwählen"), - styledHint("L-Klick → anderen Wasserfall wählen")); - - 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; - } - - private void updateWaterHeightDisplay(float h) { - if (waterCurrentHeightLabel != null) - waterCurrentHeightLabel.setText(String.format("Höhe: %.2f m", h)); - } - - private void updateWaterPanel(String info) { - if (waterDynamicContent == null) return; - waterDynamicContent.getChildren().clear(); - - if (info == null) { - Label noSel = new Label("Keine Fläche ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - waterDynamicContent.getChildren().add(noSel); - return; - } - - // Format: idx|h|n|flow|speed|waveScale|waveAmp|transp|wr|wg|wb|dr|dg|db - String[] p = info.split("\\|", -1); - if (p.length < 3) return; - try { - float waterHeight = Float.parseFloat(p[1]); - int pointCount = Integer.parseInt(p[2]); - float flowDeg = p.length > 3 ? Float.parseFloat(p[3]) : 0f; - float speed = p.length > 4 ? Float.parseFloat(p[4]) : 0.5f; - float waveScale = p.length > 5 ? Float.parseFloat(p[5]) : 0.008f; - float waveAmp = p.length > 6 ? Float.parseFloat(p[6]) : 0.1f; - float transp = p.length > 7 ? Float.parseFloat(p[7]) : 0.15f; - float wr = p.length > 8 ? Float.parseFloat(p[8]) : 0.05f; - float wg = p.length > 9 ? Float.parseFloat(p[9]) : 0.25f; - float wb = p.length > 10 ? Float.parseFloat(p[10]) : 0.55f; - float dr = p.length > 11 ? Float.parseFloat(p[11]) : 0.02f; - float dg = p.length > 12 ? Float.parseFloat(p[12]) : 0.12f; - float db = p.length > 13 ? Float.parseFloat(p[13]) : 0.30f; - - // Live-Array für alle Rendering-Parameter; wird bei jeder Änderung neu gesetzt - float[] params = {speed, waveScale, waveAmp, transp, wr, wg, wb, dr, dg, db}; - - // ── Info ────────────────────────────────────────────────────────── - Label infoLabel = new Label(String.format("Ecken: %d | Höhe: %.2f m", pointCount, waterHeight)); - infoLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); - - // ── Höhe ────────────────────────────────────────────────────────── - Slider heightSlider = new Slider(-50, 500, waterHeight); - heightSlider.setBlockIncrement(0.1); - heightSlider.setShowTickLabels(false); - heightSlider.valueProperty().addListener((o, ov, nv) -> - input.pendingWaterHeight.set(nv.floatValue())); - - // ── Fließrichtung ───────────────────────────────────────────────── - Spinner flowSpinner = new Spinner<>(0, 359, Math.round(flowDeg)); - flowSpinner.setEditable(true); - flowSpinner.setMaxWidth(Double.MAX_VALUE); - flowSpinner.valueProperty().addListener((o, ov, nv) -> - input.pendingWaterFlowDegrees.set(nv.floatValue())); - - // ── Geschwindigkeit ─────────────────────────────────────────────── - Slider speedSlider = new Slider(0.05, 5.0, speed); - speedSlider.setBlockIncrement(0.05); - speedSlider.valueProperty().addListener((o, ov, nv) -> { - params[0] = nv.floatValue(); - input.pendingWaterParams.set(params.clone()); - }); - - // ── Wellengröße ─────────────────────────────────────────────────── - Slider waveScaleSlider = new Slider(0.001, 0.05, waveScale); - waveScaleSlider.setBlockIncrement(0.001); - waveScaleSlider.valueProperty().addListener((o, ov, nv) -> { - params[1] = nv.floatValue(); - input.pendingWaterParams.set(params.clone()); - }); - - // ── Wellenamplitude ─────────────────────────────────────────────── - Slider waveAmpSlider = new Slider(0.0, 2.0, waveAmp); - waveAmpSlider.setBlockIncrement(0.05); - waveAmpSlider.valueProperty().addListener((o, ov, nv) -> { - params[2] = nv.floatValue(); - input.pendingWaterParams.set(params.clone()); - }); - - // ── Transparenz ─────────────────────────────────────────────────── - Slider transpSlider = new Slider(0.0, 1.0, transp); - transpSlider.setBlockIncrement(0.01); - transpSlider.valueProperty().addListener((o, ov, nv) -> { - params[3] = nv.floatValue(); - input.pendingWaterParams.set(params.clone()); - }); - - // ── Wasserfarbe ─────────────────────────────────────────────────── - javafx.scene.control.ColorPicker waterColorPicker = - new javafx.scene.control.ColorPicker(javafx.scene.paint.Color.color( - Math.min(wr, 1.0), Math.min(wg, 1.0), Math.min(wb, 1.0))); - waterColorPicker.setMaxWidth(Double.MAX_VALUE); - waterColorPicker.valueProperty().addListener((o, ov, nv) -> { - params[4] = (float) nv.getRed(); - params[5] = (float) nv.getGreen(); - params[6] = (float) nv.getBlue(); - input.pendingWaterParams.set(params.clone()); - }); - - // ── Tiefwasserfarbe ─────────────────────────────────────────────── - javafx.scene.control.ColorPicker deepColorPicker = - new javafx.scene.control.ColorPicker(javafx.scene.paint.Color.color( - Math.min(dr, 1.0), Math.min(dg, 1.0), Math.min(db, 1.0))); - deepColorPicker.setMaxWidth(Double.MAX_VALUE); - deepColorPicker.valueProperty().addListener((o, ov, nv) -> { - params[7] = (float) nv.getRed(); - params[8] = (float) nv.getGreen(); - params[9] = (float) nv.getBlue(); - input.pendingWaterParams.set(params.clone()); - }); - - // ── Löschen ─────────────────────────────────────────────────────── - Button delBtn = new Button("Quad löschen"); - delBtn.setMaxWidth(Double.MAX_VALUE); - delBtn.setStyle("-fx-text-fill: #c0392b;"); - delBtn.setOnAction(e -> input.deleteWaterRequested = true); - - waterDynamicContent.getChildren().addAll( - infoLabel, - new Separator(), - lbl("Höhe (m)"), withField(heightSlider, "%.2f"), - new Separator(), - lbl("Fließrichtung"), flowSpinner, - lbl("Geschwindigkeit"),withField(speedSlider, "%.2f"), - lbl("Wellengröße"), withField(waveScaleSlider, "%.4f"), - lbl("Wellenamplitude"),withField(waveAmpSlider, "%.2f"), - lbl("Transparenz"), withField(transpSlider, "%.2f"), - new Separator(), - lbl("Wasserfarbe"), waterColorPicker, - lbl("Tiefwasserfarbe"),deepColorPicker, - new Separator(), - delBtn); - - } catch (NumberFormatException ignored) {} - } - - private static Label lbl(String text) { - Label l = new Label(text); - l.setStyle("-fx-font-size: 11;"); - return l; - } - // ── Emitter-Panel ───────────────────────────────────────────────────────── - private VBox buildEmitterPanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - inner.getChildren().addAll( - sectionTitle("Partikel-Emitter"), - new Separator()); - - // Preset-Buttons (setzen emitterPreset; wirken sofort auf selektierten Emitter) - Button fireBtn = new Button("* Feuer"); - Button smokeBtn = new Button("~ Rauch"); - Button sparksBtn = new Button("* Funken"); - fireBtn.setMaxWidth(Double.MAX_VALUE); - smokeBtn.setMaxWidth(Double.MAX_VALUE); - sparksBtn.setMaxWidth(Double.MAX_VALUE); - fireBtn.setOnAction(e -> applyEmitterPreset(0)); - smokeBtn.setOnAction(e -> applyEmitterPreset(1)); - sparksBtn.setOnAction(e -> applyEmitterPreset(2)); - HBox presets = new HBox(4, fireBtn, smokeBtn, sparksBtn); - HBox.setHgrow(fireBtn, Priority.ALWAYS); - HBox.setHgrow(smokeBtn, Priority.ALWAYS); - HBox.setHgrow(sparksBtn, Priority.ALWAYS); - inner.getChildren().addAll(presets, new Separator(), - sectionTitle("Gewählter Emitter"), new Separator()); - - emitterDynamicContent = new VBox(6); - Label noSel = new Label("Kein Emitter ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - emitterDynamicContent.getChildren().add(noSel); - inner.getChildren().add(emitterDynamicContent); - - inner.getChildren().addAll( - new Separator(), - styledHint("Linksklick → platzieren / auswählen"), - styledHint("Rechtsklick → Auswahl aufheben"), - styledHint("Entf → Emitter löschen")); - - 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(280); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - return panel; - } - - private void applyEmitterPreset(int preset) { - input.emitterPreset = preset; - if (input.selectedEmitterInfo == null) return; - // Position aus aktueller Selektion nehmen und Preset-Emitter als Update senden - String[] parts = input.selectedEmitterInfo.split("\\|", -1); - if (parts.length < 4) return; - try { - float x = Float.parseFloat(parts[1]); - float y = Float.parseFloat(parts[2]); - float z = Float.parseFloat(parts[3]); - de.blight.common.PlacedEmitter pe = switch (preset) { - case 1 -> de.blight.common.PlacedEmitter.smoke(x, y, z); - case 2 -> de.blight.common.PlacedEmitter.sparks(x, y, z); - default -> de.blight.common.PlacedEmitter.fire(x, y, z); - }; - input.pendingEmitter.set(pe); - } catch (NumberFormatException ignored) {} - } - - private void updateEmitterPanel(String info) { - if (emitterDynamicContent == null) return; - emitterDynamicContent.getChildren().clear(); - - if (info == null) { - Label noSel = new Label("Kein Emitter ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - emitterDynamicContent.getChildren().add(noSel); - return; - } - - // Format: idx|x|y|z|activationRadius|texturePath|imagesX|imagesY| - // startR|startG|startB|startA|endR|endG|endB|endA| - // startSize|endSize|velX|velY|velZ|velVar|gravX|gravY|gravZ|lowLife|highLife|maxParticles|emitRate - String[] p = info.split("\\|", -1); - if (p.length < 29) return; - try { - float x = Float.parseFloat(p[1]); - float y = Float.parseFloat(p[2]); - float z = Float.parseFloat(p[3]); - float activRadius = Float.parseFloat(p[4]); - String texPath = p[5]; - int imagesX = Integer.parseInt(p[6]); - int imagesY = Integer.parseInt(p[7]); - // 19 float values: startRGBA(4), endRGBA(4), startSize, endSize, velXYZVar(4), gravXYZ(3), lowLife, highLife - // Index map: 0-3=startRGBA, 4-7=endRGBA, 8=startSize, 9=endSize, - // 10-12=velXYZ, 13=velVar, 14-16=gravXYZ, 17=lowLife, 18=highLife - float[] vals = new float[19]; - for (int i = 0; i < 19; i++) vals[i] = Float.parseFloat(p[8 + i]); - int maxParticles = Integer.parseInt(p[27]); - float emitRate = Float.parseFloat(p[28]); - - Label posLabel = new Label(String.format("X:%.1f Y:%.1f Z:%.1f", x, y, z)); - posLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); - Label texLabel = new Label("Textur: " + labelFromPath(texPath)); - texLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #777;"); - texLabel.setWrapText(true); - emitterDynamicContent.getChildren().addAll(posLabel, texLabel, new Separator()); - - float[] actR = {activRadius}; - int[] mxPart = {maxParticles}; - float[] emit = {emitRate}; - int[] imgX = {imagesX}; - int[] imgY = {imagesY}; - final String[] tPath = {texPath}; - final float[] fx = {x}, fy = {y}, fz = {z}; - - Runnable publish = () -> { - de.blight.common.PlacedEmitter pe = new de.blight.common.PlacedEmitter( - fx[0], fy[0], fz[0], actR[0], - tPath[0], imgX[0], imgY[0], - vals[0], vals[1], vals[2], vals[3], - vals[4], vals[5], vals[6], vals[7], - vals[8], vals[9], - vals[10], vals[11], vals[12], vals[13], - vals[14], vals[15], vals[16], - vals[17], vals[18], - mxPart[0], emit[0]); - input.pendingEmitter.set(pe); - }; - - // Aktivierungsradius - emitterDynamicContent.getChildren().add(paramLabel("Aktivierungsradius")); - emitterDynamicContent.getChildren().add( - buildSlider("Radius", actR, 0, 5, 200, 5, publish)); - - // Farben - emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Startfarbe")); - String[] colorLabels = {"Start R","Start G","Start B","Start A", - "Ende R","Ende G","Ende B","Ende A"}; - for (int i = 0; i < 8; i++) { - final int vi = i; - emitterDynamicContent.getChildren().add( - buildSlider(colorLabels[i], vals, vi, 0, 1, 0.01, publish)); - if (i == 3) emitterDynamicContent.getChildren().addAll( - new Separator(), paramLabel("Endfarbe")); - } - - // Größe - emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Partikelgröße")); - emitterDynamicContent.getChildren().add( - buildSlider("Start-Größe", vals, 8, 0.01, 5, 0.05, publish)); - emitterDynamicContent.getChildren().add( - buildSlider("Ende-Größe", vals, 9, 0.01, 5, 0.05, publish)); - - // Geschwindigkeit - emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Geschwindigkeit")); - emitterDynamicContent.getChildren().add( - buildSlider("Vel X", vals, 10, -10, 10, 0.1, publish)); - emitterDynamicContent.getChildren().add( - buildSlider("Vel Y", vals, 11, 0, 15, 0.1, publish)); - emitterDynamicContent.getChildren().add( - buildSlider("Vel Z", vals, 12, -10, 10, 0.1, publish)); - emitterDynamicContent.getChildren().add( - buildSlider("Variation", vals, 13, 0, 1, 0.05, publish)); - - // Gravitation - emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Gravitation")); - emitterDynamicContent.getChildren().add( - buildSlider("Grav Y", vals, 15, -20, 20, 0.5, publish)); - - // Lebensdauer - emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Lebensdauer (s)")); - emitterDynamicContent.getChildren().add( - buildSlider("Min", vals, 17, 0.1, 10, 0.1, publish)); - emitterDynamicContent.getChildren().add( - buildSlider("Max", vals, 18, 0.1, 10, 0.1, publish)); - - // Emission - emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Emission")); - emitterDynamicContent.getChildren().add( - buildSlider("Max Partikel", mxPart, 0, 1, 500, 5, publish)); - emitterDynamicContent.getChildren().add( - buildSlider("Rate/s", emit, 0, 0.5, 100, 0.5, publish)); - - Button delBtn = new Button("🗑 Löschen"); - delBtn.setMaxWidth(Double.MAX_VALUE); - delBtn.setStyle("-fx-text-fill: #c0392b;"); - delBtn.setOnAction(e -> input.deleteEmitterRequested = true); - emitterDynamicContent.getChildren().addAll(new Separator(), delBtn); - - } catch (NumberFormatException ignored) {} - } - /** Baut einen beschrifteten Slider für float[]-Werte. */ - private HBox buildSlider(String name, float[] arr, int idx, - double min, double max, double step, Runnable publish) { - Label lbl = new Label(name); - lbl.setStyle("-fx-font-size: 10;"); - lbl.setMinWidth(90); - Slider sl = new Slider(min, max, arr[idx]); - sl.setShowTickLabels(false); - sl.setBlockIncrement(step); - sl.valueProperty().addListener((o, ov, nv) -> { - arr[idx] = nv.floatValue(); - publish.run(); - }); - HBox slRow = withField(sl, "%.2f"); - HBox.setHgrow(slRow, Priority.ALWAYS); - return new HBox(4, lbl, slRow); - } /** Baut einen beschrifteten Slider für int[]-Werte. */ - private HBox buildSlider(String name, int[] arr, int idx, - double min, double max, double step, Runnable publish) { - Label lbl = new Label(name); - lbl.setStyle("-fx-font-size: 10;"); - lbl.setMinWidth(90); - Slider sl = new Slider(min, max, arr[idx]); - sl.setShowTickLabels(false); - sl.setBlockIncrement(step); - sl.valueProperty().addListener((o, ov, nv) -> { - arr[idx] = nv.intValue(); - publish.run(); - }); - HBox slRow = withField(sl, "%.0f"); - HBox.setHgrow(slRow, Priority.ALWAYS); - return new HBox(4, lbl, slRow); - } - - private static Label paramLabel(String text) { - Label lbl = new Label(text); - lbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;"); - return lbl; - } private VBox buildToolPanel() { VBox panel = new VBox(10); @@ -1852,149 +1207,6 @@ public class EditorApp extends Application { // ── Kombiniertes Gras-Panel ─────────────────────────────────────────────── - private VBox buildGrassPanel() { - VBox panel = new VBox(10); - panel.setPadding(new Insets(10)); - panel.setPrefWidth(260); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - - Label title = new Label("Gras"); - title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;"); - panel.getChildren().addAll(title, new Separator()); - - // Sub-Tool-Auswahl - grassSubTexBtn = new ToggleButton(":: Textur"); - grassSubVertBtn = new ToggleButton("|| Vertex"); - grassSubTerrainBtn = new ToggleButton("~~ Terrain"); - grassSubTexBtn.setMaxWidth(Double.MAX_VALUE); - grassSubVertBtn.setMaxWidth(Double.MAX_VALUE); - grassSubTerrainBtn.setMaxWidth(Double.MAX_VALUE); - ToggleGroup subGroup = new ToggleGroup(); - grassSubTexBtn.setToggleGroup(subGroup); - grassSubVertBtn.setToggleGroup(subGroup); - grassSubTerrainBtn.setToggleGroup(subGroup); - subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); - HBox subRow = new HBox(4, grassSubTexBtn, grassSubVertBtn, grassSubTerrainBtn); - HBox.setHgrow(grassSubTexBtn, Priority.ALWAYS); - HBox.setHgrow(grassSubVertBtn, Priority.ALWAYS); - HBox.setHgrow(grassSubTerrainBtn, Priority.ALWAYS); - panel.getChildren().add(subRow); - - // Parameter-Bereich (wird beim Sub-Tool-Wechsel ersetzt) - VBox paramsBox = new VBox(10); - panel.getChildren().add(paramsBox); - - // Globale Farben – nur für Textur-Gras und Vertex-Gras sichtbar - VBox colorSection = new VBox(6); - colorSection.getChildren().add(new Separator()); - Label colorTitle = new Label("Globale Farben"); - colorTitle.setStyle("-fx-font-weight: bold; -fx-font-size: 11; -fx-text-fill: #333;"); - colorSection.getChildren().add(colorTitle); - - javafx.scene.control.ColorPicker freshPicker = new javafx.scene.control.ColorPicker( - javafx.scene.paint.Color.color(input.grassFreshR, input.grassFreshG, input.grassFreshB)); - freshPicker.setMaxWidth(Double.MAX_VALUE); - freshPicker.setOnAction(e -> { - javafx.scene.paint.Color c = freshPicker.getValue(); - input.grassFreshR = (float) c.getRed(); - input.grassFreshG = (float) c.getGreen(); - input.grassFreshB = (float) c.getBlue(); - input.grassColorsChanged = true; - }); - - javafx.scene.control.ColorPicker dryPicker = new javafx.scene.control.ColorPicker( - javafx.scene.paint.Color.color(input.grassDryR, input.grassDryG, input.grassDryB)); - dryPicker.setMaxWidth(Double.MAX_VALUE); - dryPicker.setOnAction(e -> { - javafx.scene.paint.Color c = dryPicker.getValue(); - input.grassDryR = (float) c.getRed(); - input.grassDryG = (float) c.getGreen(); - input.grassDryB = (float) c.getBlue(); - input.grassColorsChanged = true; - }); - - Button pickFromTerrainBtn = new Button("[ ] Farbe vom Boden picken"); - pickFromTerrainBtn.setMaxWidth(Double.MAX_VALUE); - pickFromTerrainBtn.setOnAction(e -> { - pickFromTerrainBtn.setText("[ ] Boden anklicken..."); - pickFromTerrainBtn.setDisable(true); - input.grassTerrainPickCallback = color -> { - freshPicker.setValue(javafx.scene.paint.Color.color( - Math.min(1.0, color[0]), Math.min(1.0, color[1]), Math.min(1.0, color[2]))); - pickFromTerrainBtn.setText("[ ] Farbe vom Boden picken"); - pickFromTerrainBtn.setDisable(false); - }; - // Nächsten Klick auf die JME-View abfangen - javafx.event.EventHandler handler = - new javafx.event.EventHandler() { - @Override public void handle(javafx.scene.input.MouseEvent ev) { - if (ev.getButton() == javafx.scene.input.MouseButton.PRIMARY) { - input.grassTerrainPickQueue.offer( - new SharedInput.GrassVertexEdit((float) ev.getX(), (float) ev.getY(), +1)); - viewport.removeEventHandler(javafx.scene.input.MouseEvent.MOUSE_PRESSED, this); - ev.consume(); - } - } - }; - viewport.addEventHandler(javafx.scene.input.MouseEvent.MOUSE_PRESSED, handler); - }); - - colorSection.getChildren().addAll(new Label("Unvertrocknet:"), freshPicker, - pickFromTerrainBtn, - new Label("Vertrocknet:"), dryPicker); - panel.getChildren().add(colorSection); - - // Sub-Tool-Handler - Runnable activateTex = () -> { - input.activeLayer = 3; - input.activeTool = input.grassTool; - paramsBox.getChildren().clear(); - VBox inner = new VBox(10); - showToolParameters(inner, input.activeTool); - paramsBox.getChildren().add(inner); - colorSection.setVisible(true); - colorSection.setManaged(true); - }; - Runnable activateVert = () -> { - input.activeLayer = SharedInput.LAYER_GRASS_VERTEX; - input.activeTool = input.grassVertexTool; - paramsBox.getChildren().clear(); - VBox inner = new VBox(10); - showToolParameters(inner, input.activeTool); - paramsBox.getChildren().add(inner); - colorSection.setVisible(true); - colorSection.setManaged(true); - }; - Runnable activateTerrain = () -> { - input.activeLayer = SharedInput.LAYER_TERRAIN_GRASS; - input.activeTool = input.terrainGrassTool; - paramsBox.getChildren().clear(); - VBox inner = new VBox(10); - showToolParameters(inner, input.activeTool); - paramsBox.getChildren().add(inner); - colorSection.setVisible(false); - colorSection.setManaged(false); - }; - - grassSubTexBtn.setOnAction(e -> activateTex.run()); - grassSubVertBtn.setOnAction(e -> activateVert.run()); - grassSubTerrainBtn.setOnAction(e -> activateTerrain.run()); - - // Standard: zuletzt aktives Sub-Tool - if (input.activeLayer == SharedInput.LAYER_GRASS_VERTEX) { - grassSubVertBtn.setSelected(true); - activateVert.run(); - } else if (input.activeLayer == SharedInput.LAYER_TERRAIN_GRASS) { - grassSubTerrainBtn.setSelected(true); - activateTerrain.run(); - } else { - grassSubTexBtn.setSelected(true); - activateTex.run(); - } - - return panel; - } - private VBox buildAuswahlPanel() { VBox panel = new VBox(10); panel.setPadding(new Insets(10)); @@ -2044,107 +1256,6 @@ public class EditorApp extends Application { auswahlDynamicContent.getChildren().addAll(idLabel, deleteBtn); } - private VBox buildAtmospherePanel() { - VBox panel = new VBox(10); - panel.setPadding(new Insets(10)); - panel.setPrefWidth(270); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - Label title = new Label("Atmosphare"); - title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;"); - panel.getChildren().addAll(title, new Separator()); - - ToggleButton subLichtBtn = new ToggleButton("* Licht"); - ToggleButton subEmitterBtn = new ToggleButton("~ Emitter"); - subLichtBtn.setMaxWidth(Double.MAX_VALUE); - subEmitterBtn.setMaxWidth(Double.MAX_VALUE); - ToggleGroup subGroup = new ToggleGroup(); - subLichtBtn.setToggleGroup(subGroup); - subEmitterBtn.setToggleGroup(subGroup); - subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); - HBox subRow = new HBox(4, subLichtBtn, subEmitterBtn); - HBox.setHgrow(subLichtBtn, Priority.ALWAYS); - HBox.setHgrow(subEmitterBtn, Priority.ALWAYS); - panel.getChildren().add(subRow); - - VBox contentBox = new VBox(0); - panel.getChildren().add(contentBox); - - Runnable activateLicht = () -> { - input.activeLayer = SharedInput.LAYER_LIGHTS; - lightBtn = subLichtBtn; - contentBox.getChildren().setAll(buildLightPanel()); - }; - Runnable activateEmitter = () -> { - input.activeLayer = SharedInput.LAYER_EMITTERS; - emitterBtn = subEmitterBtn; - contentBox.getChildren().setAll(buildEmitterPanel()); - }; - - subLichtBtn.setOnAction(e -> activateLicht.run()); - subEmitterBtn.setOnAction(e -> activateEmitter.run()); - - if (input.activeLayer == SharedInput.LAYER_EMITTERS) { - subEmitterBtn.setSelected(true); - activateEmitter.run(); - } else { - subLichtBtn.setSelected(true); - activateLicht.run(); - } - - return panel; - } - - private VBox buildWasserPanel() { - VBox panel = new VBox(10); - panel.setPadding(new Insets(10)); - panel.setPrefWidth(270); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - Label title = new Label("Wasser"); - title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;"); - panel.getChildren().addAll(title, new Separator()); - - ToggleButton subWasserBtn = new ToggleButton("~ Wasser"); - ToggleButton subWasserfallBtn = new ToggleButton("↯ Wasserfall"); - subWasserBtn.setMaxWidth(Double.MAX_VALUE); - subWasserfallBtn.setMaxWidth(Double.MAX_VALUE); - ToggleGroup subGroup = new ToggleGroup(); - subWasserBtn.setToggleGroup(subGroup); - subWasserfallBtn.setToggleGroup(subGroup); - subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); - HBox subRow = new HBox(4, subWasserBtn, subWasserfallBtn); - HBox.setHgrow(subWasserBtn, Priority.ALWAYS); - HBox.setHgrow(subWasserfallBtn, Priority.ALWAYS); - panel.getChildren().add(subRow); - - VBox contentBox = new VBox(0); - wasserContentBox = contentBox; - panel.getChildren().add(contentBox); - - Runnable activateWasser = () -> { - input.activeLayer = SharedInput.LAYER_WATER; - waterBtn = subWasserBtn; - contentBox.getChildren().setAll(buildWaterPanel()); - }; - Runnable activateWasserfall = () -> { - input.activeLayer = SharedInput.LAYER_WATERFALL; - riverBtn = subWasserfallBtn; - contentBox.getChildren().setAll(buildWaterfallPanel()); - }; - - subWasserBtn.setOnAction(e -> activateWasser.run()); - subWasserfallBtn.setOnAction(e -> activateWasserfall.run()); - - if (input.activeLayer == SharedInput.LAYER_WATERFALL) { - subWasserfallBtn.setSelected(true); - activateWasserfall.run(); - } else { - subWasserBtn.setSelected(true); - activateWasser.run(); - } - - return panel; - } - private VBox buildWegePanel() { VBox inner = new VBox(8); inner.setPadding(new Insets(10)); @@ -2172,58 +1283,6 @@ public class EditorApp extends Application { return panel; } - private VBox buildVoxelMergedPanel() { - VBox panel = new VBox(10); - panel.setPadding(new Insets(10)); - panel.setPrefWidth(270); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - - ToggleButton subSculptBtn = new ToggleButton("◈ Sculpting"); - ToggleButton subCliffBtn = new ToggleButton("/\\ Klippe"); - subSculptBtn.setMaxWidth(Double.MAX_VALUE); - subCliffBtn.setMaxWidth(Double.MAX_VALUE); - ToggleGroup subGroup = new ToggleGroup(); - subSculptBtn.setToggleGroup(subGroup); - subCliffBtn.setToggleGroup(subGroup); - subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); - HBox subRow = new HBox(4, subSculptBtn, subCliffBtn); - HBox.setHgrow(subSculptBtn, Priority.ALWAYS); - HBox.setHgrow(subCliffBtn, Priority.ALWAYS); - panel.getChildren().add(subRow); - - VBox contentBox = new VBox(0); - panel.getChildren().add(contentBox); - - Runnable activateSculpt = () -> { - input.voxelTool.brushRadius.setValue(input.heightTool.brushRadius.getValue()); - input.voxelTool.brushStrength.setValue(input.heightTool.brushStrength.getValue()); - input.voxelTool.plateauTarget.setValue(input.heightTool.plateauHeight.getValue()); - input.activeLayer = SharedInput.LAYER_VOXEL; - input.activeTool = input.voxelTool; - voxelBtn = subSculptBtn; - showToolParameters(toolPanel, input.activeTool); - contentBox.getChildren().setAll(toolPanel); - }; - Runnable activateCliff = () -> { - input.activeLayer = SharedInput.LAYER_VOXEL_CLIFF; - voxelCliffBtn = subCliffBtn; - contentBox.getChildren().setAll(buildVoxelCliffPanel()); - }; - - subSculptBtn.setOnAction(e -> activateSculpt.run()); - subCliffBtn.setOnAction(e -> activateCliff.run()); - - if (input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF) { - subCliffBtn.setSelected(true); - activateCliff.run(); - } else { - subSculptBtn.setSelected(true); - activateSculpt.run(); - } - - return panel; - } - private void showToolParameters(VBox panel, EditorTool tool) { panel.getChildren().clear(); @@ -2367,7 +1426,7 @@ public class EditorApp extends Application { // Textur-Picker (nur beim GrassTool) if (tool instanceof GrassTool) { - javafx.scene.Node grassUI = buildGrassTextureUI(); + javafx.scene.Node grassUI = grassController.buildGrassTextureUI(); VBox.setVgrow(grassUI, javafx.scene.layout.Priority.ALWAYS); panel.getChildren().addAll(new Separator(), grassUI); } @@ -2539,145 +1598,6 @@ public class EditorApp extends Application { // ── Gras-Textur-Picker (Multi-Slot) ────────────────────────────────────── - private javafx.scene.Node buildGrassTextureUI() { - Label title = new Label("Textur-Slots"); - title.setStyle("-fx-text-fill: #111111; -fx-font-weight: bold;"); - - // Aspect-Ratios für bereits geladene Slots vorab berechnen - float[] initArs = input.grassAspectRatios.clone(); - for (int i = 0; i < 8; i++) { - String p = getGrassSlotPath(i); - if (p != null && !p.isEmpty()) initArs[i] = readAspectRatio(p); - } - input.grassAspectRatios = initArs; - - ToggleGroup slotGroup = new ToggleGroup(); - VBox slotRows = new VBox(4); - - for (int slotIndex = 0; slotIndex < 8; slotIndex++) { - final int slot = slotIndex; - - RadioButton rb = new RadioButton(); - rb.setToggleGroup(slotGroup); - if (slot == 0) rb.setSelected(true); - rb.setOnAction(e -> input.grassActiveSlot = slot); - - String prefix = slot + " ▸ "; - String currentPath = getGrassSlotPath(slot); - String fname = (currentPath != null && !currentPath.isEmpty()) - ? labelFromPath(currentPath) - : "(Leer)"; - - Label nameLabel = new Label(prefix + fname); - nameLabel.setStyle("-fx-font-size: 11;"); - nameLabel.setWrapText(true); - nameLabel.setMaxWidth(Double.MAX_VALUE); - - ImageView thumb = makeSlotThumb(currentPath); - - Button chooseBtn = new Button("..."); - chooseBtn.setOnAction(e -> { - TextureChooser chooser = new TextureChooser(ASSET_ROOT, false); - chooser.showAndWait().ifPresent(path -> { - // Normal-Map + Seitenverhältnis aus dem Textur-Set auslesen - Path colorAbs = ASSET_ROOT.resolve(path.replace('/', java.io.File.separatorChar)); - TextureMapData grassMeta = loadTexMeta(colorAbs); - String[] nmCopy = input.grassNormalMapPaths.clone(); - nmCopy[slot] = grassMeta.normalMap != null ? grassMeta.normalMap : ""; - input.grassNormalMapPaths = nmCopy; - float[] arCopy = input.grassAspectRatios.clone(); - arCopy[slot] = readAspectRatio(path); - input.grassAspectRatios = arCopy; - - if (slot == 0) { - input.grassTexturePath = path; - input.grassSettingsChanged = true; - } else { - String[] copy = input.grassTextureSlots.clone(); - copy[slot - 1] = path; - input.grassTextureSlots = copy; - input.grassSlotsChanged = true; - } - nameLabel.setText(slot + " ▸ " + labelFromPath(path)); - ImageView newThumb = makeSlotThumb(path); - thumb.setImage(newThumb.getImage()); - }); - }); - - Button clearBtn = new Button("X"); - clearBtn.setOnAction(e -> { - String[] nmCopy = input.grassNormalMapPaths.clone(); - nmCopy[slot] = ""; - input.grassNormalMapPaths = nmCopy; - if (slot == 0) { - input.grassTexturePath = ""; - input.grassSettingsChanged = true; - } else { - String[] copy = input.grassTextureSlots.clone(); - copy[slot - 1] = ""; - input.grassTextureSlots = copy; - input.grassSlotsChanged = true; - } - nameLabel.setText(slot + " ▸ (Leer)"); - thumb.setImage(null); - }); - - HBox btns = new HBox(4, chooseBtn, clearBtn); - VBox info = new VBox(2, nameLabel, btns); - info.setMaxWidth(Double.MAX_VALUE); - HBox.setHgrow(info, javafx.scene.layout.Priority.ALWAYS); - - HBox row = new HBox(6, rb, thumb, info); - row.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - row.setPadding(new Insets(2, 4, 2, 4)); - row.setStyle("-fx-background-color: #f8f8f8; -fx-border-color: #ddd; -fx-border-radius: 3;"); - slotRows.getChildren().add(row); - } - - ScrollPane scroll = new ScrollPane(slotRows); - scroll.setFitToWidth(true); - scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); - scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - scroll.setStyle("-fx-background-color: transparent;"); - VBox.setVgrow(scroll, javafx.scene.layout.Priority.ALWAYS); - - VBox box = new VBox(5, title, scroll); - box.setPadding(new Insets(4, 0, 0, 0)); - VBox.setVgrow(box, javafx.scene.layout.Priority.ALWAYS); - return box; - } - - private float readAspectRatio(String assetRelPath) { - if (assetRelPath == null || assetRelPath.isEmpty()) return 1f; - try { - java.io.File f = ASSET_ROOT.resolve( - assetRelPath.replace('/', java.io.File.separatorChar)).toFile(); - if (!f.exists()) return 1f; - javafx.scene.image.Image img = new javafx.scene.image.Image(f.toURI().toString()); - double w = img.getWidth(), h = img.getHeight(); - return (w > 0 && h > 0) ? (float) (w / h) : 1f; - } catch (Exception e) { return 1f; } - } - - private String getGrassSlotPath(int slot) { - if (slot == 0) return input.grassTexturePath != null ? input.grassTexturePath : ""; - String[] slots = input.grassTextureSlots; - return (slot - 1 < slots.length && slots[slot - 1] != null) ? slots[slot - 1] : ""; - } - - private ImageView makeSlotThumb(String path) { - ImageView iv = new ImageView(); - iv.setFitWidth(48); iv.setFitHeight(48); iv.setPreserveRatio(true); - if (path != null && !path.isEmpty()) { - java.io.File f = ASSET_ROOT.resolve(path).toFile(); - if (f.exists()) { - try { iv.setImage(new Image(f.toURI().toString(), 48, 48, true, true)); } - catch (Exception ignored) {} - } - } - return iv; - } - // ── Textur-Slot-Konfigurator ────────────────────────────────────────────── /** Baut das 8-Slot-Thumbnail-Grid für das TextureTool. */ @@ -3582,13 +2502,13 @@ public class EditorApp extends Application { switch (toolHint) { case "model" -> { objectController.activateEditMode(); } case "item" -> { input.activeLayer = SharedInput.LAYER_ITEMS; root.setRight(buildItemPlacePanel(null)); } - case "light" -> { input.activeLayer = SharedInput.LAYER_LIGHTS; root.setRight(buildLightPanel()); } - case "emitter" -> { input.activeLayer = SharedInput.LAYER_EMITTERS; root.setRight(buildEmitterPanel()); } - case "water" -> { input.activeLayer = SharedInput.LAYER_WATER; if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true); root.setRight(buildWasserPanel()); } + case "light" -> { input.activeLayer = SharedInput.LAYER_LIGHTS; if (atmosphaereBtn != null) atmosphaereBtn.setSelected(true); root.setRight(atmosphereController.buildPanel()); } + case "emitter" -> { input.activeLayer = SharedInput.LAYER_EMITTERS; if (atmosphaereBtn != null) atmosphaereBtn.setSelected(true); root.setRight(atmosphereController.buildPanel()); } + case "water" -> { input.activeLayer = SharedInput.LAYER_WATER; if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true); root.setRight(waterController.buildPanel()); } case "soundarea" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "sound"; root.setRight(buildZonenPanel()); zonenBtn.setSelected(true); } case "area" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "area"; root.setRight(buildZonenPanel()); zonenBtn.setSelected(true); } case "locationzone" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "location"; root.setRight(buildZonenPanel()); zonenBtn.setSelected(true); } - case "waterfall" -> { input.activeLayer = SharedInput.LAYER_WATERFALL; if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true); root.setRight(buildWasserPanel()); } + case "waterfall" -> { input.activeLayer = SharedInput.LAYER_WATERFALL; if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true); root.setRight(waterController.buildPanel()); } } }, (obj, index, toolHint) -> { @@ -6477,8 +5397,7 @@ public class EditorApp extends Application { input.finalizeWaterfallRequested = true; if (pressed && input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF) { input.generateCliffVoxelsRequested = true; - if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(true); - if (voxelCliffStatusLabel != null) voxelCliffStatusLabel.setText("Generiere…"); + voxelController.onCliffGenerateKeyPressed(); } } case ESCAPE -> { @@ -6602,115 +5521,6 @@ public class EditorApp extends Application { // ── Voxel-Klippen-Zone-Panel ────────────────────────────────────────────── - private VBox buildVoxelCliffPanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - inner.getChildren().addAll( - sectionTitle("Voxel-Klippen-Zonen"), - styledHint("L-Klick → Punkt setzen / Startpunkt: Polygon schließen"), - styledHint("R-Klick → Letzten Punkt rückgängig"), - styledHint("ESC → Abbrechen / Zone löschen"), - styledHint("Enter → Generieren + Voxel-Tool öffnen"), - styledHint("Entf → Zone löschen"), - new Separator(), - sectionTitle("Höhen-Parameter")); - - Slider maxHeightSlider = new Slider(0, 30, input.cliffMaxHeight); - maxHeightSlider.valueProperty().addListener((o, ov, nv) -> - input.cliffMaxHeight = nv.floatValue()); - inner.getChildren().addAll(new Label("Max. Höhe über Terrain"), withField(maxHeightSlider, "%.1f")); - - Slider noiseScaleSlider = new Slider(0.01, 0.5, input.cliffNoiseScale); - noiseScaleSlider.valueProperty().addListener((o, ov, nv) -> - input.cliffNoiseScale = nv.floatValue()); - inner.getChildren().addAll(new Label("Höhen-Noise-Frequenz"), withField(noiseScaleSlider, "%.3f")); - - Slider edgeBlendSlider = new Slider(0, 30, input.cliffEdgeBlend); - edgeBlendSlider.valueProperty().addListener((o, ov, nv) -> - input.cliffEdgeBlend = nv.floatValue()); - inner.getChildren().addAll(new Label("Rand-Überblendung"), withField(edgeBlendSlider, "%.1f")); - - inner.getChildren().addAll( - new Separator(), - sectionTitle("Oberflächen-Rauigkeit")); - - Slider roughAmpSlider = new Slider(0, 20, input.cliffRoughnessAmp); - roughAmpSlider.valueProperty().addListener((o, ov, nv) -> - input.cliffRoughnessAmp = nv.floatValue()); - inner.getChildren().addAll(new Label("Oberflächen-Amplitude"), withField(roughAmpSlider, "%.1f")); - - Slider roughFreqSlider = new Slider(0.01, 0.5, input.cliffRoughnessScale); - roughFreqSlider.valueProperty().addListener((o, ov, nv) -> - input.cliffRoughnessScale = nv.floatValue()); - inner.getChildren().addAll(new Label("Oberflächen-Frequenz"), withField(roughFreqSlider, "%.3f")); - - inner.getChildren().addAll( - new Separator(), - sectionTitle("Gewählte Zone"), - new Separator()); - - voxelCliffDynamicContent = new VBox(6); - inner.getChildren().add(voxelCliffDynamicContent); - - voxelCliffStatusLabel = new Label(""); - voxelCliffStatusLabel.setStyle("-fx-text-fill: #555; -fx-font-size: 11;"); - voxelCliffStatusLabel.setWrapText(true); - inner.getChildren().add(voxelCliffStatusLabel); - - // Sofort korrekten Zustand anzeigen (Zone evtl. schon vor Panel-Bau geladen) - updateVoxelCliffPanel(input.selectedCliffZoneIdx >= 0); - - 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 updateVoxelCliffPanel(boolean hasSelection) { - if (voxelCliffDynamicContent == null) return; - voxelCliffDynamicContent.getChildren().clear(); - - if (!hasSelection) { - Label noSel = new Label("Keine Zone ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - voxelCliffDynamicContent.getChildren().add(noSel); - if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(true); - if (voxelCliffDeleteBtn != null) voxelCliffDeleteBtn.setDisable(true); - return; - } - - voxelCliffGenerateBtn = new Button("/\\ Voxel generieren"); - voxelCliffGenerateBtn.setMaxWidth(Double.MAX_VALUE); - voxelCliffGenerateBtn.setStyle("-fx-background-color: #4a7eba; -fx-text-fill: white;"); - voxelCliffGenerateBtn.setOnAction(e -> { - input.generateCliffVoxelsRequested = true; - voxelCliffGenerateBtn.setDisable(true); - if (voxelCliffStatusLabel != null) voxelCliffStatusLabel.setText("Generiere…"); - }); - - voxelCliffDeleteBtn = new Button("🗑 Zone löschen"); - voxelCliffDeleteBtn.setMaxWidth(Double.MAX_VALUE); - voxelCliffDeleteBtn.setStyle("-fx-text-fill: #c0392b;"); - voxelCliffDeleteBtn.setOnAction(e -> input.deleteVoxelCliffZoneRequested = true); - - Label hint = new Label("Hinweis: Generierung legt Voxel über das Terrain. Bestehende Voxel\nim Bereich werden nicht gelöscht (Reset-Brush verwenden)."); - hint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); - hint.setWrapText(true); - - voxelCliffDynamicContent.getChildren().addAll( - voxelCliffGenerateBtn, voxelCliffDeleteBtn, new Separator(), hint); - } - - private void updateVoxelCliffStatus(String msg) { - if (voxelCliffStatusLabel != null) voxelCliffStatusLabel.setText(msg); - if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(false); - } - // ── Zonen-Panel (vereint Sound, Area, Location) ─────────────────────────── private VBox buildZonenPanel() { diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/atmosphere/AtmosphereController.java b/blight-editor/src/main/java/de/blight/editor/toolset/atmosphere/AtmosphereController.java new file mode 100644 index 0000000..84de3b3 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/atmosphere/AtmosphereController.java @@ -0,0 +1,379 @@ +package de.blight.editor.toolset.atmosphere; + +import de.blight.common.PlacedEmitter; +import de.blight.editor.SharedInput; +import de.blight.editor.ToolContext; +import de.blight.editor.toolset.UiHelper; +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.layout.*; + +public class AtmosphereController { + + private final ToolContext ctx; + + private VBox lightDynamicContent; + private VBox emitterDynamicContent; + + public AtmosphereController(ToolContext ctx) { + this.ctx = ctx; + } + + public void onFrame() { + if (ctx.input().lightSelectionChanged) { + ctx.input().lightSelectionChanged = false; + updateLightPanel(ctx.input().selectedLightInfo); + } + if (ctx.input().emitterSelectionChanged) { + ctx.input().emitterSelectionChanged = false; + updateEmitterPanel(ctx.input().selectedEmitterInfo); + } + } + + public VBox buildPanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + Label title = new Label("Atmosphare"); + title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;"); + panel.getChildren().addAll(title, new Separator()); + + ToggleButton subLichtBtn = new ToggleButton("* Licht"); + ToggleButton subEmitterBtn = new ToggleButton("~ Emitter"); + subLichtBtn.setMaxWidth(Double.MAX_VALUE); + subEmitterBtn.setMaxWidth(Double.MAX_VALUE); + ToggleGroup subGroup = new ToggleGroup(); + subLichtBtn.setToggleGroup(subGroup); + subEmitterBtn.setToggleGroup(subGroup); + subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); + HBox subRow = new HBox(4, subLichtBtn, subEmitterBtn); + HBox.setHgrow(subLichtBtn, Priority.ALWAYS); + HBox.setHgrow(subEmitterBtn, Priority.ALWAYS); + panel.getChildren().add(subRow); + + VBox contentBox = new VBox(0); + panel.getChildren().add(contentBox); + + Runnable activateLicht = () -> { + ctx.input().activeLayer = SharedInput.LAYER_LIGHTS; + contentBox.getChildren().setAll(buildLightPanel()); + }; + Runnable activateEmitter = () -> { + ctx.input().activeLayer = SharedInput.LAYER_EMITTERS; + contentBox.getChildren().setAll(buildEmitterPanel()); + }; + + subLichtBtn.setOnAction(e -> activateLicht.run()); + subEmitterBtn.setOnAction(e -> activateEmitter.run()); + + if (ctx.input().activeLayer == SharedInput.LAYER_EMITTERS) { + subEmitterBtn.setSelected(true); + activateEmitter.run(); + } else { + subLichtBtn.setSelected(true); + activateLicht.run(); + } + + return panel; + } + + private VBox buildLightPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + inner.getChildren().addAll( + UiHelper.sectionTitle("Lichtquellen"), + new Separator(), + UiHelper.sectionTitle("Gewähltes Licht"), + new Separator()); + + lightDynamicContent = new VBox(6); + Label noSel = new Label("Kein Licht ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + lightDynamicContent.getChildren().add(noSel); + inner.getChildren().add(lightDynamicContent); + + inner.getChildren().addAll( + new Separator(), + UiHelper.styledHint("Linksklick → platzieren / auswählen"), + UiHelper.styledHint("Rechtsklick → Auswahl aufheben"), + UiHelper.styledHint("Entf → ausgewähltes Licht löschen")); + + 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; + } + + public void updateLightPanel(String info) { + if (lightDynamicContent == null) return; + lightDynamicContent.getChildren().clear(); + + if (info == null) { + Label noSel = new Label("Kein Licht ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + lightDynamicContent.getChildren().add(noSel); + return; + } + + String[] parts = info.split("\\|", -1); + if (parts.length < 9) return; + float[] vals = new float[9]; + try { + for (int i = 0; i < 9; i++) vals[i] = Float.parseFloat(parts[i]); + } catch (NumberFormatException e) { return; } + + Label posLabel = new Label(String.format("X: %.1f Y: %.1f Z: %.1f", vals[1], vals[2], vals[3])); + posLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); + lightDynamicContent.getChildren().addAll(posLabel, new Separator()); + + float[] cur = {vals[4], vals[5], vals[6], vals[7], vals[8]}; + + String[] labels = {"Rot", "Grün", "Blau", "Intensität", "Radius"}; + double[] mins = {0, 0, 0, 0.1, 0.5}; + double[] maxs = {1, 1, 1, 10.0, 200.0}; + double[] steps = {0.01, 0.01, 0.01, 0.1, 1.0}; + + Runnable publish = () -> ctx.input().pendingLightProp.set( + new SharedInput.LightPropertyChange(cur[0], cur[1], cur[2], cur[3], cur[4])); + + for (int i = 0; i < 5; i++) { + final int idx = i; + Label lbl = new Label(labels[i]); + lbl.setStyle("-fx-font-size: 11;"); + Slider sl = new Slider(mins[i], maxs[i], cur[i]); + sl.setShowTickLabels(false); + sl.setBlockIncrement(steps[i]); + sl.valueProperty().addListener((o, ov, nv) -> { + cur[idx] = nv.floatValue(); + publish.run(); + }); + lightDynamicContent.getChildren().addAll(lbl, UiHelper.withField(sl, "%.2f")); + } + + Button delBtn = new Button("Loeschen"); + delBtn.setMaxWidth(Double.MAX_VALUE); + delBtn.setStyle("-fx-text-fill: #c0392b;"); + delBtn.setOnAction(e -> ctx.input().deleteLightRequested = true); + lightDynamicContent.getChildren().addAll(new Separator(), delBtn); + } + + private VBox buildEmitterPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + inner.getChildren().addAll( + UiHelper.sectionTitle("Partikel-Emitter"), + new Separator()); + + Button fireBtn = new Button("* Feuer"); + Button smokeBtn = new Button("~ Rauch"); + Button sparksBtn = new Button("* Funken"); + fireBtn.setMaxWidth(Double.MAX_VALUE); + smokeBtn.setMaxWidth(Double.MAX_VALUE); + sparksBtn.setMaxWidth(Double.MAX_VALUE); + fireBtn.setOnAction(e -> applyEmitterPreset(0)); + smokeBtn.setOnAction(e -> applyEmitterPreset(1)); + sparksBtn.setOnAction(e -> applyEmitterPreset(2)); + HBox presets = new HBox(4, fireBtn, smokeBtn, sparksBtn); + HBox.setHgrow(fireBtn, Priority.ALWAYS); + HBox.setHgrow(smokeBtn, Priority.ALWAYS); + HBox.setHgrow(sparksBtn, Priority.ALWAYS); + inner.getChildren().addAll(presets, new Separator(), + UiHelper.sectionTitle("Gewählter Emitter"), new Separator()); + + emitterDynamicContent = new VBox(6); + Label noSel = new Label("Kein Emitter ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + emitterDynamicContent.getChildren().add(noSel); + inner.getChildren().add(emitterDynamicContent); + + inner.getChildren().addAll( + new Separator(), + UiHelper.styledHint("Linksklick → platzieren / auswählen"), + UiHelper.styledHint("Rechtsklick → Auswahl aufheben"), + UiHelper.styledHint("Entf → Emitter löschen")); + + 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(280); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + private void applyEmitterPreset(int preset) { + ctx.input().emitterPreset = preset; + if (ctx.input().selectedEmitterInfo == null) return; + String[] parts = ctx.input().selectedEmitterInfo.split("\\|", -1); + if (parts.length < 4) return; + try { + float x = Float.parseFloat(parts[1]); + float y = Float.parseFloat(parts[2]); + float z = Float.parseFloat(parts[3]); + PlacedEmitter pe = switch (preset) { + case 1 -> PlacedEmitter.smoke(x, y, z); + case 2 -> PlacedEmitter.sparks(x, y, z); + default -> PlacedEmitter.fire(x, y, z); + }; + ctx.input().pendingEmitter.set(pe); + } catch (NumberFormatException ignored) {} + } + + public void updateEmitterPanel(String info) { + if (emitterDynamicContent == null) return; + emitterDynamicContent.getChildren().clear(); + + if (info == null) { + Label noSel = new Label("Kein Emitter ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + emitterDynamicContent.getChildren().add(noSel); + return; + } + + String[] p = info.split("\\|", -1); + if (p.length < 29) return; + try { + float x = Float.parseFloat(p[1]); + float y = Float.parseFloat(p[2]); + float z = Float.parseFloat(p[3]); + float activRadius = Float.parseFloat(p[4]); + String texPath = p[5]; + int imagesX = Integer.parseInt(p[6]); + int imagesY = Integer.parseInt(p[7]); + float[] vals = new float[19]; + for (int i = 0; i < 19; i++) vals[i] = Float.parseFloat(p[8 + i]); + int maxParticles = Integer.parseInt(p[27]); + float emitRate = Float.parseFloat(p[28]); + + Label posLabel = new Label(String.format("X:%.1f Y:%.1f Z:%.1f", x, y, z)); + posLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); + Label texLabel = new Label("Textur: " + labelFromPath(texPath)); + texLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #777;"); + texLabel.setWrapText(true); + emitterDynamicContent.getChildren().addAll(posLabel, texLabel, new Separator()); + + float[] actR = {activRadius}; + int[] mxPart = {maxParticles}; + float[] emit = {emitRate}; + int[] imgX = {imagesX}; + int[] imgY = {imagesY}; + final String[] tPath = {texPath}; + final float[] fx = {x}, fy = {y}, fz = {z}; + + Runnable publish = () -> { + PlacedEmitter pe = new PlacedEmitter( + fx[0], fy[0], fz[0], actR[0], + tPath[0], imgX[0], imgY[0], + vals[0], vals[1], vals[2], vals[3], + vals[4], vals[5], vals[6], vals[7], + vals[8], vals[9], + vals[10], vals[11], vals[12], vals[13], + vals[14], vals[15], vals[16], + vals[17], vals[18], + mxPart[0], emit[0]); + ctx.input().pendingEmitter.set(pe); + }; + + emitterDynamicContent.getChildren().add(paramLabel("Aktivierungsradius")); + emitterDynamicContent.getChildren().add(buildSlider("Radius", actR, 0, 5, 200, 5, publish)); + + emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Startfarbe")); + String[] colorLabels = {"Start R","Start G","Start B","Start A", + "Ende R","Ende G","Ende B","Ende A"}; + for (int i = 0; i < 8; i++) { + emitterDynamicContent.getChildren().add(buildSlider(colorLabels[i], vals, i, 0, 1, 0.01, publish)); + if (i == 3) emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Endfarbe")); + } + + emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Partikelgröße")); + emitterDynamicContent.getChildren().add(buildSlider("Start-Größe", vals, 8, 0.01, 5, 0.05, publish)); + emitterDynamicContent.getChildren().add(buildSlider("Ende-Größe", vals, 9, 0.01, 5, 0.05, publish)); + + emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Geschwindigkeit")); + emitterDynamicContent.getChildren().add(buildSlider("Vel X", vals, 10, -10, 10, 0.1, publish)); + emitterDynamicContent.getChildren().add(buildSlider("Vel Y", vals, 11, 0, 15, 0.1, publish)); + emitterDynamicContent.getChildren().add(buildSlider("Vel Z", vals, 12, -10, 10, 0.1, publish)); + emitterDynamicContent.getChildren().add(buildSlider("Variation", vals, 13, 0, 1, 0.05, publish)); + + emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Gravitation")); + emitterDynamicContent.getChildren().add(buildSlider("Grav Y", vals, 15, -20, 20, 0.5, publish)); + + emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Lebensdauer (s)")); + emitterDynamicContent.getChildren().add(buildSlider("Min", vals, 17, 0.1, 10, 0.1, publish)); + emitterDynamicContent.getChildren().add(buildSlider("Max", vals, 18, 0.1, 10, 0.1, publish)); + + emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Emission")); + emitterDynamicContent.getChildren().add(buildSlider("Max Partikel", mxPart, 0, 1, 500, 5, publish)); + emitterDynamicContent.getChildren().add(buildSlider("Rate/s", emit, 0, 0.5, 100, 0.5, publish)); + + Button delBtn = new Button("Loeschen"); + delBtn.setMaxWidth(Double.MAX_VALUE); + delBtn.setStyle("-fx-text-fill: #c0392b;"); + delBtn.setOnAction(e -> ctx.input().deleteEmitterRequested = true); + emitterDynamicContent.getChildren().addAll(new Separator(), delBtn); + + } catch (NumberFormatException ignored) {} + } + + private HBox buildSlider(String name, float[] arr, int idx, + double min, double max, double step, Runnable publish) { + Label lbl = new Label(name); + lbl.setStyle("-fx-font-size: 10;"); + lbl.setMinWidth(90); + Slider sl = new Slider(min, max, arr[idx]); + sl.setShowTickLabels(false); + sl.setBlockIncrement(step); + sl.valueProperty().addListener((o, ov, nv) -> { + arr[idx] = nv.floatValue(); + publish.run(); + }); + HBox slRow = UiHelper.withField(sl, "%.2f"); + HBox.setHgrow(slRow, Priority.ALWAYS); + return new HBox(4, lbl, slRow); + } + + private HBox buildSlider(String name, int[] arr, int idx, + double min, double max, double step, Runnable publish) { + Label lbl = new Label(name); + lbl.setStyle("-fx-font-size: 10;"); + lbl.setMinWidth(90); + Slider sl = new Slider(min, max, arr[idx]); + sl.setShowTickLabels(false); + sl.setBlockIncrement(step); + sl.valueProperty().addListener((o, ov, nv) -> { + arr[idx] = nv.intValue(); + publish.run(); + }); + HBox slRow = UiHelper.withField(sl, "%.0f"); + HBox.setHgrow(slRow, Priority.ALWAYS); + return new HBox(4, lbl, slRow); + } + + private static Label paramLabel(String text) { + Label lbl = new Label(text); + lbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;"); + return lbl; + } + + private String labelFromPath(String path) { + if (path == null || path.isEmpty()) return "(leer)"; + java.nio.file.Path p = ctx.assetRoot().resolve(path.replace('/', java.io.File.separatorChar)); + java.nio.file.Path parent = p.getParent(); + if (parent != null && java.nio.file.Files.exists(parent.resolve("textureset.json"))) { + return parent.getFileName().toString(); + } + String name = p.getFileName().toString(); + int dot = name.lastIndexOf('.'); + return dot > 0 ? name.substring(0, dot) : name; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/grass/GrassController.java b/blight-editor/src/main/java/de/blight/editor/toolset/grass/GrassController.java new file mode 100644 index 0000000..50b1142 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/grass/GrassController.java @@ -0,0 +1,324 @@ +package de.blight.editor.toolset.grass; + +import de.blight.editor.SharedInput; +import de.blight.editor.ToolContext; +import de.blight.editor.tool.EditorTool; +import de.blight.editor.toolset.UiHelper; +import de.blight.editor.ui.TextureChooser; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; + +import java.io.File; +import java.nio.file.Path; +import java.util.function.BiConsumer; +import java.util.function.Function; + +public class GrassController { + + private final ToolContext ctx; + private final ImageView viewport; + private final BiConsumer showToolParams; + private final Function normalMapLoader; + + public GrassController(ToolContext ctx, + ImageView viewport, + BiConsumer showToolParams, + Function normalMapLoader) { + this.ctx = ctx; + this.viewport = viewport; + this.showToolParams = showToolParams; + this.normalMapLoader = normalMapLoader; + } + + public void onFrame() { + // no per-frame polling needed + } + + public VBox buildPanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(260); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + + Label title = new Label("Gras"); + title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;"); + panel.getChildren().addAll(title, new Separator()); + + ToggleButton grassSubTexBtn = new ToggleButton(":: Textur"); + ToggleButton grassSubVertBtn = new ToggleButton("|| Vertex"); + ToggleButton grassSubTerrainBtn = new ToggleButton("~~ Terrain"); + grassSubTexBtn.setMaxWidth(Double.MAX_VALUE); + grassSubVertBtn.setMaxWidth(Double.MAX_VALUE); + grassSubTerrainBtn.setMaxWidth(Double.MAX_VALUE); + ToggleGroup subGroup = new ToggleGroup(); + grassSubTexBtn.setToggleGroup(subGroup); + grassSubVertBtn.setToggleGroup(subGroup); + grassSubTerrainBtn.setToggleGroup(subGroup); + subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); + HBox subRow = new HBox(4, grassSubTexBtn, grassSubVertBtn, grassSubTerrainBtn); + HBox.setHgrow(grassSubTexBtn, Priority.ALWAYS); + HBox.setHgrow(grassSubVertBtn, Priority.ALWAYS); + HBox.setHgrow(grassSubTerrainBtn, Priority.ALWAYS); + panel.getChildren().add(subRow); + + VBox paramsBox = new VBox(10); + panel.getChildren().add(paramsBox); + + VBox colorSection = new VBox(6); + colorSection.getChildren().add(new Separator()); + Label colorTitle = new Label("Globale Farben"); + colorTitle.setStyle("-fx-font-weight: bold; -fx-font-size: 11; -fx-text-fill: #333;"); + colorSection.getChildren().add(colorTitle); + + javafx.scene.control.ColorPicker freshPicker = new javafx.scene.control.ColorPicker( + javafx.scene.paint.Color.color(ctx.input().grassFreshR, ctx.input().grassFreshG, ctx.input().grassFreshB)); + freshPicker.setMaxWidth(Double.MAX_VALUE); + freshPicker.setOnAction(e -> { + javafx.scene.paint.Color c = freshPicker.getValue(); + ctx.input().grassFreshR = (float) c.getRed(); + ctx.input().grassFreshG = (float) c.getGreen(); + ctx.input().grassFreshB = (float) c.getBlue(); + ctx.input().grassColorsChanged = true; + }); + + javafx.scene.control.ColorPicker dryPicker = new javafx.scene.control.ColorPicker( + javafx.scene.paint.Color.color(ctx.input().grassDryR, ctx.input().grassDryG, ctx.input().grassDryB)); + dryPicker.setMaxWidth(Double.MAX_VALUE); + dryPicker.setOnAction(e -> { + javafx.scene.paint.Color c = dryPicker.getValue(); + ctx.input().grassDryR = (float) c.getRed(); + ctx.input().grassDryG = (float) c.getGreen(); + ctx.input().grassDryB = (float) c.getBlue(); + ctx.input().grassColorsChanged = true; + }); + + Button pickFromTerrainBtn = new Button("[ ] Farbe vom Boden picken"); + pickFromTerrainBtn.setMaxWidth(Double.MAX_VALUE); + pickFromTerrainBtn.setOnAction(e -> { + pickFromTerrainBtn.setText("[ ] Boden anklicken..."); + pickFromTerrainBtn.setDisable(true); + ctx.input().grassTerrainPickCallback = color -> { + freshPicker.setValue(javafx.scene.paint.Color.color( + Math.min(1.0, color[0]), Math.min(1.0, color[1]), Math.min(1.0, color[2]))); + pickFromTerrainBtn.setText("[ ] Farbe vom Boden picken"); + pickFromTerrainBtn.setDisable(false); + }; + javafx.event.EventHandler handler = + new javafx.event.EventHandler<>() { + @Override public void handle(javafx.scene.input.MouseEvent ev) { + if (ev.getButton() == javafx.scene.input.MouseButton.PRIMARY) { + ctx.input().grassTerrainPickQueue.offer( + new SharedInput.GrassVertexEdit((float) ev.getX(), (float) ev.getY(), +1)); + viewport.removeEventHandler(javafx.scene.input.MouseEvent.MOUSE_PRESSED, this); + ev.consume(); + } + } + }; + viewport.addEventHandler(javafx.scene.input.MouseEvent.MOUSE_PRESSED, handler); + }); + + colorSection.getChildren().addAll(new Label("Unvertrocknet:"), freshPicker, + pickFromTerrainBtn, + new Label("Vertrocknet:"), dryPicker); + panel.getChildren().add(colorSection); + + Runnable activateTex = () -> { + ctx.input().activeLayer = 3; + ctx.input().activeTool = ctx.input().grassTool; + paramsBox.getChildren().clear(); + VBox inner = new VBox(10); + showToolParams.accept(inner, ctx.input().activeTool); + paramsBox.getChildren().add(inner); + colorSection.setVisible(true); + colorSection.setManaged(true); + }; + Runnable activateVert = () -> { + ctx.input().activeLayer = SharedInput.LAYER_GRASS_VERTEX; + ctx.input().activeTool = ctx.input().grassVertexTool; + paramsBox.getChildren().clear(); + VBox inner = new VBox(10); + showToolParams.accept(inner, ctx.input().activeTool); + paramsBox.getChildren().add(inner); + colorSection.setVisible(true); + colorSection.setManaged(true); + }; + Runnable activateTerrain = () -> { + ctx.input().activeLayer = SharedInput.LAYER_TERRAIN_GRASS; + ctx.input().activeTool = ctx.input().terrainGrassTool; + paramsBox.getChildren().clear(); + VBox inner = new VBox(10); + showToolParams.accept(inner, ctx.input().activeTool); + paramsBox.getChildren().add(inner); + colorSection.setVisible(false); + colorSection.setManaged(false); + }; + + grassSubTexBtn.setOnAction(e -> activateTex.run()); + grassSubVertBtn.setOnAction(e -> activateVert.run()); + grassSubTerrainBtn.setOnAction(e -> activateTerrain.run()); + + if (ctx.input().activeLayer == SharedInput.LAYER_GRASS_VERTEX) { + grassSubVertBtn.setSelected(true); + activateVert.run(); + } else if (ctx.input().activeLayer == SharedInput.LAYER_TERRAIN_GRASS) { + grassSubTerrainBtn.setSelected(true); + activateTerrain.run(); + } else { + grassSubTexBtn.setSelected(true); + activateTex.run(); + } + + return panel; + } + + public javafx.scene.Node buildGrassTextureUI() { + Label title = new Label("Textur-Slots"); + title.setStyle("-fx-text-fill: #111111; -fx-font-weight: bold;"); + + float[] initArs = ctx.input().grassAspectRatios.clone(); + for (int i = 0; i < 8; i++) { + String p = getGrassSlotPath(i); + if (p != null && !p.isEmpty()) initArs[i] = readAspectRatio(p); + } + ctx.input().grassAspectRatios = initArs; + + ToggleGroup slotGroup = new ToggleGroup(); + VBox slotRows = new VBox(4); + + for (int slotIndex = 0; slotIndex < 8; slotIndex++) { + final int slot = slotIndex; + + RadioButton rb = new RadioButton(); + rb.setToggleGroup(slotGroup); + if (slot == 0) rb.setSelected(true); + rb.setOnAction(e -> ctx.input().grassActiveSlot = slot); + + String currentPath = getGrassSlotPath(slot); + String fname = (currentPath != null && !currentPath.isEmpty()) + ? labelFromPath(currentPath) : "(Leer)"; + + Label nameLabel = new Label(slot + " > " + fname); + nameLabel.setStyle("-fx-font-size: 11;"); + nameLabel.setWrapText(true); + nameLabel.setMaxWidth(Double.MAX_VALUE); + + ImageView thumb = makeSlotThumb(currentPath); + + Button chooseBtn = new Button("..."); + chooseBtn.setOnAction(e -> { + TextureChooser chooser = new TextureChooser(ctx.assetRoot(), false); + chooser.showAndWait().ifPresent(path -> { + Path colorAbs = ctx.assetRoot().resolve(path.replace('/', File.separatorChar)); + String nm = normalMapLoader.apply(colorAbs); + String[] nmCopy = ctx.input().grassNormalMapPaths.clone(); + nmCopy[slot] = nm; + ctx.input().grassNormalMapPaths = nmCopy; + float[] arCopy = ctx.input().grassAspectRatios.clone(); + arCopy[slot] = readAspectRatio(path); + ctx.input().grassAspectRatios = arCopy; + + if (slot == 0) { + ctx.input().grassTexturePath = path; + ctx.input().grassSettingsChanged = true; + } else { + String[] copy = ctx.input().grassTextureSlots.clone(); + copy[slot - 1] = path; + ctx.input().grassTextureSlots = copy; + ctx.input().grassSlotsChanged = true; + } + nameLabel.setText(slot + " > " + labelFromPath(path)); + ImageView newThumb = makeSlotThumb(path); + thumb.setImage(newThumb.getImage()); + }); + }); + + Button clearBtn = new Button("X"); + clearBtn.setOnAction(e -> { + String[] nmCopy = ctx.input().grassNormalMapPaths.clone(); + nmCopy[slot] = ""; + ctx.input().grassNormalMapPaths = nmCopy; + if (slot == 0) { + ctx.input().grassTexturePath = ""; + ctx.input().grassSettingsChanged = true; + } else { + String[] copy = ctx.input().grassTextureSlots.clone(); + copy[slot - 1] = ""; + ctx.input().grassTextureSlots = copy; + ctx.input().grassSlotsChanged = true; + } + nameLabel.setText(slot + " > (Leer)"); + thumb.setImage(null); + }); + + HBox btns = new HBox(4, chooseBtn, clearBtn); + VBox info = new VBox(2, nameLabel, btns); + info.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(info, Priority.ALWAYS); + + HBox row = new HBox(6, rb, thumb, info); + row.setAlignment(Pos.CENTER_LEFT); + row.setPadding(new Insets(2, 4, 2, 4)); + row.setStyle("-fx-background-color: #f8f8f8; -fx-border-color: #ddd; -fx-border-radius: 3;"); + slotRows.getChildren().add(row); + } + + ScrollPane scroll = new ScrollPane(slotRows); + scroll.setFitToWidth(true); + scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent;"); + VBox.setVgrow(scroll, Priority.ALWAYS); + + VBox box = new VBox(5, title, scroll); + box.setPadding(new Insets(4, 0, 0, 0)); + VBox.setVgrow(box, Priority.ALWAYS); + return box; + } + + private float readAspectRatio(String assetRelPath) { + if (assetRelPath == null || assetRelPath.isEmpty()) return 1f; + try { + File f = ctx.assetRoot().resolve( + assetRelPath.replace('/', File.separatorChar)).toFile(); + if (!f.exists()) return 1f; + Image img = new Image(f.toURI().toString()); + double w = img.getWidth(), h = img.getHeight(); + return (w > 0 && h > 0) ? (float) (w / h) : 1f; + } catch (Exception e) { return 1f; } + } + + private String getGrassSlotPath(int slot) { + if (slot == 0) return ctx.input().grassTexturePath != null ? ctx.input().grassTexturePath : ""; + String[] slots = ctx.input().grassTextureSlots; + return (slot - 1 < slots.length && slots[slot - 1] != null) ? slots[slot - 1] : ""; + } + + private ImageView makeSlotThumb(String path) { + ImageView iv = new ImageView(); + iv.setFitWidth(48); iv.setFitHeight(48); iv.setPreserveRatio(true); + if (path != null && !path.isEmpty()) { + File f = ctx.assetRoot().resolve(path).toFile(); + if (f.exists()) { + try { iv.setImage(new Image(f.toURI().toString(), 48, 48, true, true)); } + catch (Exception ignored) {} + } + } + return iv; + } + + private String labelFromPath(String path) { + if (path == null || path.isEmpty()) return "(leer)"; + Path p = ctx.assetRoot().resolve(path.replace('/', File.separatorChar)); + Path parent = p.getParent(); + if (parent != null && java.nio.file.Files.exists(parent.resolve("textureset.json"))) { + return parent.getFileName().toString(); + } + String name = p.getFileName().toString(); + int dot = name.lastIndexOf('.'); + return dot > 0 ? name.substring(0, dot) : name; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/voxel/VoxelController.java b/blight-editor/src/main/java/de/blight/editor/toolset/voxel/VoxelController.java new file mode 100644 index 0000000..bf51494 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/voxel/VoxelController.java @@ -0,0 +1,214 @@ +package de.blight.editor.toolset.voxel; + +import de.blight.editor.SharedInput; +import de.blight.editor.ToolContext; +import de.blight.editor.toolset.UiHelper; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.layout.*; + +import java.util.function.Supplier; + +public class VoxelController { + + private final ToolContext ctx; + private final Supplier sculptPanelSupplier; + private final Runnable showSculptPanel; + + private VBox voxelCliffDynamicContent; + private Label voxelCliffStatusLabel; + private Button voxelCliffGenerateBtn; + private Button voxelCliffDeleteBtn; + + /** + * @param sculptPanelSupplier EditorApp lambda: sets activeLayer/activeTool, calls + * showToolParameters(toolPanel, voxelTool) and returns toolPanel + * @param showSculptPanel fires voxelMergedBtn so the sculpting sub-tab becomes visible + */ + public VoxelController(ToolContext ctx, + Supplier sculptPanelSupplier, + Runnable showSculptPanel) { + this.ctx = ctx; + this.sculptPanelSupplier = sculptPanelSupplier; + this.showSculptPanel = showSculptPanel; + } + + public void onFrame() { + if (ctx.input().cliffZoneSelectionChanged) { + ctx.input().cliffZoneSelectionChanged = false; + updateVoxelCliffPanel(ctx.input().selectedCliffZoneIdx >= 0); + } + + String cliffMsg = ctx.input().cliffGenStatusMsg; + if (cliffMsg != null) { + ctx.input().cliffGenStatusMsg = null; + updateVoxelCliffStatus(cliffMsg); + } + + if (ctx.input().cliffGenComplete) { + ctx.input().cliffGenComplete = false; + Platform.runLater(showSculptPanel); + } + } + + /** Called from EditorApp keyboard handler on Enter while LAYER_VOXEL_CLIFF is active. */ + public void onCliffGenerateKeyPressed() { + if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(true); + if (voxelCliffStatusLabel != null) voxelCliffStatusLabel.setText("Generiere…"); + } + + public VBox buildPanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + + ToggleButton subSculptBtn = new ToggleButton("◈ Sculpting"); + ToggleButton subCliffBtn = new ToggleButton("/\\ Klippe"); + subSculptBtn.setMaxWidth(Double.MAX_VALUE); + subCliffBtn.setMaxWidth(Double.MAX_VALUE); + ToggleGroup subGroup = new ToggleGroup(); + subSculptBtn.setToggleGroup(subGroup); + subCliffBtn.setToggleGroup(subGroup); + subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); + HBox subRow = new HBox(4, subSculptBtn, subCliffBtn); + HBox.setHgrow(subSculptBtn, Priority.ALWAYS); + HBox.setHgrow(subCliffBtn, Priority.ALWAYS); + panel.getChildren().add(subRow); + + VBox contentBox = new VBox(0); + panel.getChildren().add(contentBox); + + Runnable activateSculpt = () -> { + Node sculptContent = sculptPanelSupplier.get(); + contentBox.getChildren().setAll(sculptContent); + }; + Runnable activateCliff = () -> { + ctx.input().activeLayer = SharedInput.LAYER_VOXEL_CLIFF; + contentBox.getChildren().setAll(buildVoxelCliffPanel()); + }; + + subSculptBtn.setOnAction(e -> activateSculpt.run()); + subCliffBtn.setOnAction(e -> activateCliff.run()); + + if (ctx.input().activeLayer == SharedInput.LAYER_VOXEL_CLIFF) { + subCliffBtn.setSelected(true); + activateCliff.run(); + } else { + subSculptBtn.setSelected(true); + activateSculpt.run(); + } + + return panel; + } + + private VBox buildVoxelCliffPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + inner.getChildren().addAll( + UiHelper.sectionTitle("Voxel-Klippen-Zonen"), + UiHelper.styledHint("L-Klick → Punkt setzen / Startpunkt: Polygon schließen"), + UiHelper.styledHint("R-Klick → Letzten Punkt rückgängig"), + UiHelper.styledHint("ESC → Abbrechen / Zone löschen"), + UiHelper.styledHint("Enter → Generieren + Voxel-Tool öffnen"), + UiHelper.styledHint("Entf → Zone löschen"), + new Separator(), + UiHelper.sectionTitle("Höhen-Parameter")); + + Slider maxHeightSlider = new Slider(0, 30, ctx.input().cliffMaxHeight); + maxHeightSlider.valueProperty().addListener((o, ov, nv) -> + ctx.input().cliffMaxHeight = nv.floatValue()); + inner.getChildren().addAll(new Label("Max. Höhe über Terrain"), UiHelper.withField(maxHeightSlider, "%.1f")); + + Slider noiseScaleSlider = new Slider(0.01, 0.5, ctx.input().cliffNoiseScale); + noiseScaleSlider.valueProperty().addListener((o, ov, nv) -> + ctx.input().cliffNoiseScale = nv.floatValue()); + inner.getChildren().addAll(new Label("Höhen-Noise-Frequenz"), UiHelper.withField(noiseScaleSlider, "%.3f")); + + Slider edgeBlendSlider = new Slider(0, 30, ctx.input().cliffEdgeBlend); + edgeBlendSlider.valueProperty().addListener((o, ov, nv) -> + ctx.input().cliffEdgeBlend = nv.floatValue()); + inner.getChildren().addAll(new Label("Rand-Überblendung"), UiHelper.withField(edgeBlendSlider, "%.1f")); + + inner.getChildren().addAll( + new Separator(), + UiHelper.sectionTitle("Oberflächen-Rauigkeit")); + + Slider roughAmpSlider = new Slider(0, 20, ctx.input().cliffRoughnessAmp); + roughAmpSlider.valueProperty().addListener((o, ov, nv) -> + ctx.input().cliffRoughnessAmp = nv.floatValue()); + inner.getChildren().addAll(new Label("Oberflächen-Amplitude"), UiHelper.withField(roughAmpSlider, "%.1f")); + + Slider roughFreqSlider = new Slider(0.01, 0.5, ctx.input().cliffRoughnessScale); + roughFreqSlider.valueProperty().addListener((o, ov, nv) -> + ctx.input().cliffRoughnessScale = nv.floatValue()); + inner.getChildren().addAll(new Label("Oberflächen-Frequenz"), UiHelper.withField(roughFreqSlider, "%.3f")); + + inner.getChildren().addAll( + new Separator(), + UiHelper.sectionTitle("Gewählte Zone"), + new Separator()); + + voxelCliffDynamicContent = new VBox(6); + inner.getChildren().add(voxelCliffDynamicContent); + + voxelCliffStatusLabel = new Label(""); + voxelCliffStatusLabel.setStyle("-fx-text-fill: #555; -fx-font-size: 11;"); + voxelCliffStatusLabel.setWrapText(true); + inner.getChildren().add(voxelCliffStatusLabel); + + updateVoxelCliffPanel(ctx.input().selectedCliffZoneIdx >= 0); + + 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 updateVoxelCliffPanel(boolean hasSelection) { + if (voxelCliffDynamicContent == null) return; + voxelCliffDynamicContent.getChildren().clear(); + + if (!hasSelection) { + Label noSel = new Label("Keine Zone ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + voxelCliffDynamicContent.getChildren().add(noSel); + if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(true); + if (voxelCliffDeleteBtn != null) voxelCliffDeleteBtn.setDisable(true); + return; + } + + voxelCliffGenerateBtn = new Button("/\\ Voxel generieren"); + voxelCliffGenerateBtn.setMaxWidth(Double.MAX_VALUE); + voxelCliffGenerateBtn.setStyle("-fx-background-color: #4a7eba; -fx-text-fill: white;"); + voxelCliffGenerateBtn.setOnAction(e -> { + ctx.input().generateCliffVoxelsRequested = true; + voxelCliffGenerateBtn.setDisable(true); + if (voxelCliffStatusLabel != null) voxelCliffStatusLabel.setText("Generiere…"); + }); + + voxelCliffDeleteBtn = new Button("Loeschen"); + voxelCliffDeleteBtn.setMaxWidth(Double.MAX_VALUE); + voxelCliffDeleteBtn.setStyle("-fx-text-fill: #c0392b;"); + voxelCliffDeleteBtn.setOnAction(e -> ctx.input().deleteVoxelCliffZoneRequested = true); + + Label hint = new Label("Hinweis: Generierung legt Voxel über das Terrain. Bestehende Voxel\nim Bereich werden nicht gelöscht (Reset-Brush verwenden)."); + hint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); + hint.setWrapText(true); + + voxelCliffDynamicContent.getChildren().addAll( + voxelCliffGenerateBtn, voxelCliffDeleteBtn, new Separator(), hint); + } + + private void updateVoxelCliffStatus(String msg) { + if (voxelCliffStatusLabel != null) voxelCliffStatusLabel.setText(msg); + if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(false); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/water/WaterController.java b/blight-editor/src/main/java/de/blight/editor/toolset/water/WaterController.java new file mode 100644 index 0000000..48d2127 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/water/WaterController.java @@ -0,0 +1,347 @@ +package de.blight.editor.toolset.water; + +import de.blight.editor.SharedInput; +import de.blight.editor.ToolContext; +import de.blight.editor.toolset.UiHelper; +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.layout.*; + +public class WaterController { + + private final ToolContext ctx; + + private VBox waterDynamicContent; + private Label waterCurrentHeightLabel; + private VBox wasserContentBox; + + public WaterController(ToolContext ctx) { + this.ctx = ctx; + } + + public void onFrame() { + if (ctx.input().waterSelectionChanged) { + ctx.input().waterSelectionChanged = false; + updateWaterPanel(ctx.input().selectedWaterInfo); + } + if (ctx.input().waterfallSelectionChanged) { + ctx.input().waterfallSelectionChanged = false; + updateWaterfallPanel(ctx.input().selectedWaterfallInfo); + } + if (ctx.input().waterHeightChanged) { + ctx.input().waterHeightChanged = false; + updateWaterHeightDisplay(ctx.input().waterCurrentHeight); + } + } + + public VBox buildPanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + Label title = new Label("Wasser"); + title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;"); + panel.getChildren().addAll(title, new Separator()); + + ToggleButton subWasserBtn = new ToggleButton("~ Wasser"); + ToggleButton subWasserfallBtn = new ToggleButton("v Wasserfall"); + subWasserBtn.setMaxWidth(Double.MAX_VALUE); + subWasserfallBtn.setMaxWidth(Double.MAX_VALUE); + ToggleGroup subGroup = new ToggleGroup(); + subWasserBtn.setToggleGroup(subGroup); + subWasserfallBtn.setToggleGroup(subGroup); + subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); + HBox subRow = new HBox(4, subWasserBtn, subWasserfallBtn); + HBox.setHgrow(subWasserBtn, Priority.ALWAYS); + HBox.setHgrow(subWasserfallBtn, Priority.ALWAYS); + panel.getChildren().add(subRow); + + VBox contentBox = new VBox(0); + wasserContentBox = contentBox; + panel.getChildren().add(contentBox); + + Runnable activateWasser = () -> { + ctx.input().activeLayer = SharedInput.LAYER_WATER; + contentBox.getChildren().setAll(buildWaterPanel()); + }; + Runnable activateWasserfall = () -> { + ctx.input().activeLayer = SharedInput.LAYER_WATERFALL; + contentBox.getChildren().setAll(buildWaterfallPanel()); + }; + + subWasserBtn.setOnAction(e -> activateWasser.run()); + subWasserfallBtn.setOnAction(e -> activateWasserfall.run()); + + if (ctx.input().activeLayer == SharedInput.LAYER_WATERFALL) { + subWasserfallBtn.setSelected(true); + activateWasserfall.run(); + } else { + subWasserBtn.setSelected(true); + activateWasser.run(); + } + + return panel; + } + + private VBox buildWaterPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + + waterCurrentHeightLabel = new Label("Höhe: –"); + waterCurrentHeightLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold;"); + + inner.getChildren().addAll( + UiHelper.sectionTitle("Wasseroberflächen"), + new Separator(), + waterCurrentHeightLabel, + new Separator(), + UiHelper.sectionTitle("Gewählte Fläche"), + new Separator()); + + waterDynamicContent = new VBox(6); + Label noSel = new Label("Keine Fläche ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + waterDynamicContent.getChildren().add(noSel); + inner.getChildren().add(waterDynamicContent); + + inner.getChildren().addAll( + new Separator(), + UiHelper.styledHint("L-Klick (leer) → erste Ecke setzen"), + UiHelper.styledHint("L-Klick (2.) → Quad abschliessen"), + UiHelper.styledHint("Handle ziehen → Ecke / Kante verschieben"), + UiHelper.styledHint("R-Klick → Abbrechen / Auswahl aufheben"), + UiHelper.styledHint("Leertaste → Höhe vom Terrain übernehmen"), + UiHelper.styledHint("ESC → Abbrechen / Auswahl aufheben"), + UiHelper.styledHint("Entf → Quad löschen")); + + 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; + } + + private VBox buildWaterfallPanel() { + VBox inner = new VBox(10); + inner.setPadding(new Insets(10)); + + Button undoBtn = new Button("Letzten Punkt entfernen"); + undoBtn.setMaxWidth(Double.MAX_VALUE); + undoBtn.setOnAction(e -> ctx.input().undoWaterfallPointRequested = true); + + inner.getChildren().addAll( + UiHelper.sectionTitle("Wasserfall (4-Klick Quad)"), + new Separator(), + undoBtn, + new Separator(), + UiHelper.styledHint("L-Klick → Punkt setzen (1→2→3→4)"), + UiHelper.styledHint("Punkte auf Klippen, Voxel & Seenkanten möglich"), + UiHelper.styledHint("R-Klick / Backspace → letzten Punkt rückgängig"), + UiHelper.styledHint("ESC → Platzierung abbrechen"), + UiHelper.styledHint("Wasserfall anklicken → Ecken verschiebbar")); + + 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; + } + + private VBox buildWaterfallSelectionPanel(String info) { + String[] parts = info.split("\\|"); + int numPts = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; + String length = parts.length > 2 ? parts[2] + " m" : "?"; + + VBox inner = new VBox(10); + inner.setPadding(new Insets(10)); + + Label infoLabel = new Label("Punkte: " + numPts + " Länge: ~" + length); + infoLabel.setStyle("-fx-font-size: 11;"); + + Button deleteBtn = new Button("Wasserfall löschen"); + deleteBtn.setMaxWidth(Double.MAX_VALUE); + deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white;"); + deleteBtn.setOnAction(e -> { + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, + "Wasserfall unwiderruflich löschen?", + ButtonType.YES, ButtonType.NO); + confirm.setTitle("Wasserfall löschen"); + confirm.setHeaderText(null); + confirm.initOwner(ctx.stageSupplier().get()); + confirm.showAndWait().ifPresent(btn -> { + if (btn == ButtonType.YES) ctx.input().deleteWaterfallRequested = true; + }); + }); + + inner.getChildren().addAll( + UiHelper.sectionTitle("Wasserfall selektiert"), + new Separator(), + infoLabel, + new Separator(), + deleteBtn, + new Separator(), + UiHelper.styledHint("L-Klick ins Leere → abwählen"), + UiHelper.styledHint("L-Klick → anderen Wasserfall wählen")); + + 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; + } + + private void updateWaterHeightDisplay(float h) { + if (waterCurrentHeightLabel != null) + waterCurrentHeightLabel.setText(String.format("Höhe: %.2f m", h)); + } + + public void updateWaterPanel(String info) { + if (waterDynamicContent == null) return; + waterDynamicContent.getChildren().clear(); + + if (info == null) { + Label noSel = new Label("Keine Fläche ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + waterDynamicContent.getChildren().add(noSel); + return; + } + + String[] p = info.split("\\|", -1); + if (p.length < 3) return; + try { + float waterHeight = Float.parseFloat(p[1]); + int pointCount = Integer.parseInt(p[2]); + float flowDeg = p.length > 3 ? Float.parseFloat(p[3]) : 0f; + float speed = p.length > 4 ? Float.parseFloat(p[4]) : 0.5f; + float waveScale = p.length > 5 ? Float.parseFloat(p[5]) : 0.008f; + float waveAmp = p.length > 6 ? Float.parseFloat(p[6]) : 0.1f; + float transp = p.length > 7 ? Float.parseFloat(p[7]) : 0.15f; + float wr = p.length > 8 ? Float.parseFloat(p[8]) : 0.05f; + float wg = p.length > 9 ? Float.parseFloat(p[9]) : 0.25f; + float wb = p.length > 10 ? Float.parseFloat(p[10]) : 0.55f; + float dr = p.length > 11 ? Float.parseFloat(p[11]) : 0.02f; + float dg = p.length > 12 ? Float.parseFloat(p[12]) : 0.12f; + float db = p.length > 13 ? Float.parseFloat(p[13]) : 0.30f; + + float[] params = {speed, waveScale, waveAmp, transp, wr, wg, wb, dr, dg, db}; + + Label infoLabel = new Label(String.format("Ecken: %d | Höhe: %.2f m", pointCount, waterHeight)); + infoLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); + + Slider heightSlider = new Slider(-50, 500, waterHeight); + heightSlider.setBlockIncrement(0.1); + heightSlider.setShowTickLabels(false); + heightSlider.valueProperty().addListener((o, ov, nv) -> + ctx.input().pendingWaterHeight.set(nv.floatValue())); + + Spinner flowSpinner = new Spinner<>(0, 359, Math.round(flowDeg)); + flowSpinner.setEditable(true); + flowSpinner.setMaxWidth(Double.MAX_VALUE); + flowSpinner.valueProperty().addListener((o, ov, nv) -> + ctx.input().pendingWaterFlowDegrees.set(nv.floatValue())); + + Slider speedSlider = new Slider(0.05, 5.0, speed); + speedSlider.setBlockIncrement(0.05); + speedSlider.valueProperty().addListener((o, ov, nv) -> { + params[0] = nv.floatValue(); + ctx.input().pendingWaterParams.set(params.clone()); + }); + + Slider waveScaleSlider = new Slider(0.001, 0.05, waveScale); + waveScaleSlider.setBlockIncrement(0.001); + waveScaleSlider.valueProperty().addListener((o, ov, nv) -> { + params[1] = nv.floatValue(); + ctx.input().pendingWaterParams.set(params.clone()); + }); + + Slider waveAmpSlider = new Slider(0.0, 2.0, waveAmp); + waveAmpSlider.setBlockIncrement(0.05); + waveAmpSlider.valueProperty().addListener((o, ov, nv) -> { + params[2] = nv.floatValue(); + ctx.input().pendingWaterParams.set(params.clone()); + }); + + Slider transpSlider = new Slider(0.0, 1.0, transp); + transpSlider.setBlockIncrement(0.01); + transpSlider.valueProperty().addListener((o, ov, nv) -> { + params[3] = nv.floatValue(); + ctx.input().pendingWaterParams.set(params.clone()); + }); + + javafx.scene.control.ColorPicker waterColorPicker = + new javafx.scene.control.ColorPicker(javafx.scene.paint.Color.color( + Math.min(wr, 1.0), Math.min(wg, 1.0), Math.min(wb, 1.0))); + waterColorPicker.setMaxWidth(Double.MAX_VALUE); + waterColorPicker.valueProperty().addListener((o, ov, nv) -> { + params[4] = (float) nv.getRed(); + params[5] = (float) nv.getGreen(); + params[6] = (float) nv.getBlue(); + ctx.input().pendingWaterParams.set(params.clone()); + }); + + javafx.scene.control.ColorPicker deepColorPicker = + new javafx.scene.control.ColorPicker(javafx.scene.paint.Color.color( + Math.min(dr, 1.0), Math.min(dg, 1.0), Math.min(db, 1.0))); + deepColorPicker.setMaxWidth(Double.MAX_VALUE); + deepColorPicker.valueProperty().addListener((o, ov, nv) -> { + params[7] = (float) nv.getRed(); + params[8] = (float) nv.getGreen(); + params[9] = (float) nv.getBlue(); + ctx.input().pendingWaterParams.set(params.clone()); + }); + + Button delBtn = new Button("Quad löschen"); + delBtn.setMaxWidth(Double.MAX_VALUE); + delBtn.setStyle("-fx-text-fill: #c0392b;"); + delBtn.setOnAction(e -> ctx.input().deleteWaterRequested = true); + + waterDynamicContent.getChildren().addAll( + infoLabel, + new Separator(), + lbl("Höhe (m)"), UiHelper.withField(heightSlider, "%.2f"), + new Separator(), + lbl("Fließrichtung"), flowSpinner, + lbl("Geschwindigkeit"), UiHelper.withField(speedSlider, "%.2f"), + lbl("Wellengröße"), UiHelper.withField(waveScaleSlider, "%.4f"), + lbl("Wellenamplitude"), UiHelper.withField(waveAmpSlider, "%.2f"), + lbl("Transparenz"), UiHelper.withField(transpSlider, "%.2f"), + new Separator(), + lbl("Wasserfarbe"), waterColorPicker, + lbl("Tiefwasserfarbe"), deepColorPicker, + new Separator(), + delBtn); + + } catch (NumberFormatException ignored) {} + } + + public void updateWaterfallPanel(String info) { + if (ctx.input().activeLayer != SharedInput.LAYER_WATERFALL) return; + VBox panel = info != null ? buildWaterfallSelectionPanel(info) : buildWaterfallPanel(); + if (wasserContentBox != null) { + wasserContentBox.getChildren().setAll(panel); + } else { + ctx.setRightPanel().accept(buildPanel()); + } + } + + private static Label lbl(String text) { + Label l = new Label(text); + l.setStyle("-fx-font-size: 11;"); + return l; + } +}