Phase 5: Atmosphere, Water, Voxel, Grass aus EditorApp extrahiert (-1.170 Z.)

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 11:01:14 +02:00
parent bf1bb4fd16
commit 8f0cf7bc42
5 changed files with 1307 additions and 1233 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}
}

View File

@@ -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<VBox, EditorTool> showToolParams;
private final Function<Path, String> normalMapLoader;
public GrassController(ToolContext ctx,
ImageView viewport,
BiConsumer<VBox, EditorTool> showToolParams,
Function<Path, String> 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<javafx.scene.input.MouseEvent> 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;
}
}

View File

@@ -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<Node> 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<Node> 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);
}
}

View File

@@ -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<Integer> 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;
}
}