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:
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user