Wasserfall System eingebaut

This commit is contained in:
2026-08-22 11:37:51 +02:00
parent 528b2ea576
commit 6ee404fc2c
58 changed files with 2548 additions and 534 deletions

View File

@@ -814,6 +814,22 @@ public class EditorApp extends Application {
}
}
if (input.auswahlWaterSelected) {
input.auswahlWaterSelected = false;
if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true);
input.activeLayer = SharedInput.LAYER_WATER;
root.setRight(buildWasserPanel());
updateWaterPanel(input.selectedWaterInfo);
}
if (input.auswahlWaterfallSelected) {
input.auswahlWaterfallSelected = false;
if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true);
input.activeLayer = SharedInput.LAYER_WATERFALL;
root.setRight(buildWasserPanel());
updateWaterfallPanel(input.selectedWaterfallInfo);
}
if (input.itemSelectionChanged) {
input.itemSelectionChanged = false;
updateAuswahlItemPanel(input.selectedItemInfo);
@@ -2877,12 +2893,13 @@ public class EditorApp extends Application {
inner.getChildren().addAll(
new Separator(),
styledHint("L-Klick → Polygon-Punkt setzen"),
styledHint("R-Klick → letzten Punkt entfernen"),
styledHint("L-Klick (leer) → erste Ecke setzen"),
styledHint("L-Klick (2.) → Quad abschliessen"),
styledHint("Handle ziehen → Ecke / Kante verschieben"),
styledHint("R-Klick → Abbrechen / Auswahl aufheben"),
styledHint("Leertaste → Höhe vom Terrain übernehmen"),
styledHint("Erster Punkt bestimmt die Höhe"),
styledHint("ESC → Polygon abbrechen"),
styledHint("Entf → Fläche löschen"));
styledHint("ESC → Abbrechen / Auswahl aufheben"),
styledHint("Entf → Quad löschen"));
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
@@ -2900,27 +2917,20 @@ public class EditorApp extends Application {
VBox inner = new VBox(10);
inner.setPadding(new Insets(10));
Label widthTitle = sectionTitle("Breite (nächster Punkt)");
Slider widthSlider = new Slider(8.0, 40.0, input.waterfallNewWidth);
widthSlider.setBlockIncrement(0.5);
widthSlider.setShowTickLabels(true);
widthSlider.setMajorTickUnit(5);
widthSlider.valueProperty().addListener((o, ov, nv) -> input.waterfallNewWidth = nv.floatValue());
Button undoBtn = new Button("Letzten Punkt entfernen");
undoBtn.setMaxWidth(Double.MAX_VALUE);
undoBtn.setOnAction(e -> input.undoWaterfallPointRequested = true);
inner.getChildren().addAll(
sectionTitle("Wasserfall"),
new Separator(),
widthTitle, withField(widthSlider, "%.1f"),
sectionTitle("Wasserfall (4-Klick Quad)"),
new Separator(),
undoBtn,
new Separator(),
styledHint("L-Klick → Punkt setzen"),
styledHint("R-Klick → Wasserfall abschließen"),
styledHint("Backspace → letzten Punkt löschen"));
styledHint("L-Klick → Punkt setzen (1→2→3→4)"),
styledHint("Punkte auf Klippen, Voxel & Seenkanten möglich"),
styledHint("R-Klick / Backspace → letzten Punkt rückgängig"),
styledHint("ESC → Platzierung abbrechen"),
styledHint("Wasserfall anklicken → Ecken verschiebbar"));
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
@@ -2959,7 +2969,19 @@ public class EditorApp extends Application {
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 -> input.deleteWaterfallRequested = true);
deleteBtn.setOnAction(e -> {
javafx.scene.control.Alert confirm = new javafx.scene.control.Alert(
javafx.scene.control.Alert.AlertType.CONFIRMATION,
"Wasserfall unwiderruflich löschen?",
javafx.scene.control.ButtonType.YES,
javafx.scene.control.ButtonType.NO);
confirm.setTitle("Wasserfall löschen");
confirm.setHeaderText(null);
confirm.initOwner(primaryStage);
confirm.showAndWait().ifPresent(btn -> {
if (btn == javafx.scene.control.ButtonType.YES) input.deleteWaterfallRequested = true;
});
});
inner.getChildren().addAll(
sectionTitle("Wasserfall selektiert"),
@@ -2999,54 +3021,132 @@ public class EditorApp extends Application {
return;
}
// Format: "idx|waterHeight|pointCount|flowDegrees"
// Format: idx|h|n|flow|speed|waveScale|waveAmp|transp|wr|wg|wb|dr|dg|db
String[] p = info.split("\\|", -1);
if (p.length < 3) return;
try {
float waterHeight = Float.parseFloat(p[1]);
int pointCount = Integer.parseInt(p[2]);
float flowDeg = p.length >= 4 ? Float.parseFloat(p[3]) : 0f;
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;
Label infoLabel = new Label(String.format("Punkte: %d | Höhe: %.2f m", pointCount, waterHeight));
// Live-Array für alle Rendering-Parameter; wird bei jeder Änderung neu gesetzt
float[] params = {speed, waveScale, waveAmp, transp, wr, wg, wb, dr, dg, db};
// ── Info ──────────────────────────────────────────────────────────
Label infoLabel = new Label(String.format("Ecken: %d | Höhe: %.2f m", pointCount, waterHeight));
infoLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
Label heightLabel = new Label("Höhe (m)");
heightLabel.setStyle("-fx-font-size: 11;");
// ── Höhe ──────────────────────────────────────────────────────────
Slider heightSlider = new Slider(-50, 500, waterHeight);
heightSlider.setBlockIncrement(0.1);
heightSlider.setShowTickLabels(false);
heightSlider.valueProperty().addListener((o, ov, nv) -> input.pendingWaterHeight.set(nv.floatValue()));
Label flowLabel = new Label("Fließrichtung");
flowLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
Label flowDegLabel = new Label(String.format("%.0f°", flowDeg));
flowDegLabel.setStyle("-fx-font-size: 11;");
heightSlider.valueProperty().addListener((o, ov, nv) ->
input.pendingWaterHeight.set(nv.floatValue()));
// ── Fließrichtung ─────────────────────────────────────────────────
Spinner<Integer> flowSpinner = new Spinner<>(0, 359, Math.round(flowDeg));
flowSpinner.setEditable(true);
flowSpinner.setMaxWidth(Double.MAX_VALUE);
flowSpinner.valueProperty().addListener((o, ov, nv) -> {
flowDegLabel.setText(nv + "°");
input.pendingWaterFlowDegrees.set(nv.floatValue());
flowSpinner.valueProperty().addListener((o, ov, nv) ->
input.pendingWaterFlowDegrees.set(nv.floatValue()));
// ── Geschwindigkeit ───────────────────────────────────────────────
Slider speedSlider = new Slider(0.05, 5.0, speed);
speedSlider.setBlockIncrement(0.05);
speedSlider.valueProperty().addListener((o, ov, nv) -> {
params[0] = nv.floatValue();
input.pendingWaterParams.set(params.clone());
});
Button delBtn = new Button("Löschen");
// ── Wellengröße ───────────────────────────────────────────────────
Slider waveScaleSlider = new Slider(0.001, 0.05, waveScale);
waveScaleSlider.setBlockIncrement(0.001);
waveScaleSlider.valueProperty().addListener((o, ov, nv) -> {
params[1] = nv.floatValue();
input.pendingWaterParams.set(params.clone());
});
// ── Wellenamplitude ───────────────────────────────────────────────
Slider waveAmpSlider = new Slider(0.0, 2.0, waveAmp);
waveAmpSlider.setBlockIncrement(0.05);
waveAmpSlider.valueProperty().addListener((o, ov, nv) -> {
params[2] = nv.floatValue();
input.pendingWaterParams.set(params.clone());
});
// ── Transparenz ───────────────────────────────────────────────────
Slider transpSlider = new Slider(0.0, 1.0, transp);
transpSlider.setBlockIncrement(0.01);
transpSlider.valueProperty().addListener((o, ov, nv) -> {
params[3] = nv.floatValue();
input.pendingWaterParams.set(params.clone());
});
// ── Wasserfarbe ───────────────────────────────────────────────────
javafx.scene.control.ColorPicker waterColorPicker =
new javafx.scene.control.ColorPicker(javafx.scene.paint.Color.color(
Math.min(wr, 1.0), Math.min(wg, 1.0), Math.min(wb, 1.0)));
waterColorPicker.setMaxWidth(Double.MAX_VALUE);
waterColorPicker.valueProperty().addListener((o, ov, nv) -> {
params[4] = (float) nv.getRed();
params[5] = (float) nv.getGreen();
params[6] = (float) nv.getBlue();
input.pendingWaterParams.set(params.clone());
});
// ── Tiefwasserfarbe ───────────────────────────────────────────────
javafx.scene.control.ColorPicker deepColorPicker =
new javafx.scene.control.ColorPicker(javafx.scene.paint.Color.color(
Math.min(dr, 1.0), Math.min(dg, 1.0), Math.min(db, 1.0)));
deepColorPicker.setMaxWidth(Double.MAX_VALUE);
deepColorPicker.valueProperty().addListener((o, ov, nv) -> {
params[7] = (float) nv.getRed();
params[8] = (float) nv.getGreen();
params[9] = (float) nv.getBlue();
input.pendingWaterParams.set(params.clone());
});
// ── Löschen ───────────────────────────────────────────────────────
Button delBtn = new Button("Quad löschen");
delBtn.setMaxWidth(Double.MAX_VALUE);
delBtn.setStyle("-fx-text-fill: #c0392b;");
delBtn.setOnAction(e -> input.deleteWaterRequested = true);
waterDynamicContent.getChildren().addAll(
infoLabel, new Separator(),
heightLabel, withField(heightSlider, "%.2f"),
infoLabel,
new Separator(),
flowLabel, flowSpinner,
lbl("Höhe (m)"), withField(heightSlider, "%.2f"),
new Separator(),
lbl("Fließrichtung"), flowSpinner,
lbl("Geschwindigkeit"),withField(speedSlider, "%.2f"),
lbl("Wellengröße"), withField(waveScaleSlider, "%.4f"),
lbl("Wellenamplitude"),withField(waveAmpSlider, "%.2f"),
lbl("Transparenz"), withField(transpSlider, "%.2f"),
new Separator(),
lbl("Wasserfarbe"), waterColorPicker,
lbl("Tiefwasserfarbe"),deepColorPicker,
new Separator(),
delBtn);
} catch (NumberFormatException ignored) {}
}
private static Label lbl(String text) {
Label l = new Label(text);
l.setStyle("-fx-font-size: 11;");
return l;
}
// ── Emitter-Panel ─────────────────────────────────────────────────────────
private VBox buildEmitterPanel() {
@@ -5354,6 +5454,12 @@ public class EditorApp extends Application {
input.reloadPlacedOther = true;
}
case "waterfall" -> {
java.util.List<de.blight.common.PlacedWaterfall> list = new java.util.ArrayList<>(de.blight.common.WaterfallIO.load());
if (index >= 0 && index < list.size()) list.remove(index);
de.blight.common.WaterfallIO.save(list);
input.reloadPlacedOther = true;
}
case "river" -> {
java.util.List<java.util.List<de.blight.common.RiverPoint>> list = new java.util.ArrayList<>(de.blight.common.RiverIO.load());
if (index >= 0 && index < list.size()) list.remove(index);
de.blight.common.RiverIO.save(list);
@@ -8748,7 +8854,8 @@ public class EditorApp extends Application {
|| input.activeLayer == SharedInput.LAYER_ZONEN
|| input.activeLayer == SharedInput.LAYER_SOUND_AREAS
|| input.activeLayer == SharedInput.LAYER_AREAS
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES;
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES
|| input.activeLayer == SharedInput.LAYER_WATER;
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) {
// Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam)
input.playToolClickQueue.offer(
@@ -8764,7 +8871,8 @@ public class EditorApp extends Application {
boolean singleClickLayer = input.activeLayer == SharedInput.LAYER_ZONEN
|| input.activeLayer == SharedInput.LAYER_SOUND_AREAS
|| input.activeLayer == SharedInput.LAYER_AREAS
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES;
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES
|| input.activeLayer == SharedInput.LAYER_WATER;
editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1;
submitEdit(editPressX, editPressY, editPressAction);
if (!singleClickLayer) startEditTimer();
@@ -8789,6 +8897,18 @@ public class EditorApp extends Application {
return;
}
if (input.activeLayer == SharedInput.LAYER_WATER && e.isPrimaryButtonDown()) {
input.waterDragQueue.offer(new SharedInput.WaterDrag((float) e.getX(), (float) e.getY()));
input.mouseScreenX = (float) e.getX();
input.mouseScreenY = (float) e.getY();
return;
}
if (input.activeLayer == SharedInput.LAYER_WATERFALL && e.isPrimaryButtonDown()) {
input.waterfallDragQueue.offer(new SharedInput.WaterfallDrag((float) e.getX(), (float) e.getY()));
return;
}
if (input.activeLayer == SharedInput.LAYER_ROUTINE_EDITOR && e.isSecondaryButtonDown()) {
input.routineYawDragQueue.offer(
new SharedInput.RoutineYawDrag((float) e.getX(), (float) e.getY(), false));
@@ -8815,6 +8935,12 @@ public class EditorApp extends Application {
viewport.setOnMouseReleased(e -> {
objDragging = false;
if (!isObjectMode()) stopEditTimer();
if (input.activeLayer == SharedInput.LAYER_WATER) {
input.waterDragEnd = true;
}
if (input.activeLayer == SharedInput.LAYER_WATERFALL) {
input.waterfallDragEnd = true;
}
if (input.activeLayer == SharedInput.LAYER_ROUTINE_EDITOR) {
input.routineYawDragQueue.offer(
new SharedInput.RoutineYawDrag((float) e.getX(), (float) e.getY(), true));
@@ -9404,6 +9530,8 @@ public class EditorApp extends Application {
case CONTROL -> input.ctrlHeld = pressed;
case ALT -> input.altHeld = pressed;
case ENTER -> {
if (pressed && input.activeLayer == SharedInput.LAYER_WATERFALL)
input.finalizeWaterfallRequested = true;
if (pressed && input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF) {
input.generateCliffVoxelsRequested = true;
if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(true);
@@ -9416,6 +9544,7 @@ public class EditorApp extends Application {
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES
|| input.activeLayer == SharedInput.LAYER_ZONEN
|| input.activeLayer == SharedInput.LAYER_WATER
|| input.activeLayer == SharedInput.LAYER_WATERFALL
|| input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF))
input.cancelZoneDrawing = true;
}
@@ -9439,6 +9568,23 @@ public class EditorApp extends Application {
input.deleteVoxelCliffZoneRequested = true;
else if (input.activeLayer == SharedInput.LAYER_WATER)
input.deleteWaterRequested = true;
else if (input.activeLayer == SharedInput.LAYER_WATERFALL
&& input.selectedWaterfallInfo != null) {
Platform.runLater(() -> {
javafx.scene.control.Alert confirm = new javafx.scene.control.Alert(
javafx.scene.control.Alert.AlertType.CONFIRMATION,
"Wasserfall unwiderruflich löschen?",
javafx.scene.control.ButtonType.YES,
javafx.scene.control.ButtonType.NO);
confirm.setTitle("Wasserfall löschen");
confirm.setHeaderText(null);
confirm.initOwner(primaryStage);
confirm.showAndWait().ifPresent(btn -> {
if (btn == javafx.scene.control.ButtonType.YES)
input.deleteWaterfallRequested = true;
});
});
}
}
}
case BACK_SPACE -> {

View File

@@ -24,6 +24,7 @@ import de.blight.editor.state.ModelEditorState;
import de.blight.editor.state.EmitterState;
import de.blight.editor.state.LocationZoneState;
import de.blight.editor.state.RiverEditorState;
import de.blight.editor.state.WaterfallEditorState;
import de.blight.editor.state.PlayToolState;
import de.blight.editor.state.SoundAreaState;
import de.blight.editor.state.WaterBodyState;
@@ -206,6 +207,7 @@ public class JmeEditorApp extends SimpleApplication {
stateManager.attach(new AreaState(input));
stateManager.attach(new LocationZoneState(input));
stateManager.attach(new RiverEditorState(input));
stateManager.attach(new WaterfallEditorState(input));
stateManager.attach(new PlayToolState(input));
stateManager.attach(new RoutineMapState(input));
stateManager.attach(new PathNetworkEditorState(input));

View File

@@ -472,14 +472,20 @@ public class SharedInput {
/** activeLayer==12 → Temporären Spawnpunkt setzen und Spiel starten */
public static final int LAYER_PLAY_TOOL = 12;
/** activeLayer==13 → Wasserfälle platzieren */
/** activeLayer==13 → Wasserfälle platzieren (4-Klick-Quad) */
public static final int LAYER_WATERFALL = 13;
/** activeLayer==40 → Flüsse/Bäche (RiverEditorState) */
public static final int LAYER_RIVER = 40;
// ── Wasserfall-Werkzeug ────────────────────────────────────────────────────
public record WaterfallClick(float screenX, float screenY, boolean rightButton) {}
public record WaterfallDrag(float screenX, float screenY) {}
public final ConcurrentLinkedQueue<WaterfallClick> waterfallClickQueue = new ConcurrentLinkedQueue<>();
public volatile float waterfallNewWidth = 8.0f;
public volatile boolean undoWaterfallPointRequested = false;
public final ConcurrentLinkedQueue<WaterfallDrag> waterfallDragQueue = new ConcurrentLinkedQueue<>();
public volatile boolean waterfallDragEnd = false;
public volatile boolean undoWaterfallPointRequested = false;
public volatile boolean finalizeWaterfallRequested = false;
public volatile float waterfallNewWidth = 8.0f;
/** JME → JavaFX: Info des selektierten Wasserfalls. Format: "idx|numPoints|totalLengthM" oder null. */
public volatile String selectedWaterfallInfo = null;
@@ -487,11 +493,18 @@ public class SharedInput {
/** JavaFX → JME: Selektierten Wasserfall löschen. */
public volatile boolean deleteWaterfallRequested = false;
// ── Wasser-Werkzeug (Polygon) ─────────────────────────────────────────────
/** Klick im Viewport im Wasser-Modus: Punkt setzen oder selektieren. */
// ── Wasser-Werkzeug (Quad) ────────────────────────────────────────────────
/** Klick im Viewport im Wasser-Modus: Ecke setzen, Quad abschließen oder selektieren. */
public record WaterClick(float screenX, float screenY, boolean rightButton) {}
public final ConcurrentLinkedQueue<WaterClick> waterClickQueue = new ConcurrentLinkedQueue<>();
/** Maus-Drag während Handle-Bearbeitung: absolute Bildschirmposition. */
public record WaterDrag(float screenX, float screenY) {}
public final ConcurrentLinkedQueue<WaterDrag> waterDragQueue = new ConcurrentLinkedQueue<>();
/** Maus-Release im Wasser-Modus: beendet einen laufenden Handle-Drag. */
public volatile boolean waterDragEnd = false;
/**
* JME → JavaFX: Info der selektierten Wasserfläche.
* Format: "idx|waterHeight|pointCount" oder null.
@@ -512,6 +525,13 @@ public class SharedInput {
/** JavaFX → JME: neue Fließrichtung (Grad 0359) für selektierte Wasserfläche. null = kein Auftrag. */
public final AtomicReference<Float> pendingWaterFlowDegrees = new AtomicReference<>();
/**
* JavaFX → JME: neue Rendering-Parameter für selektierte Wasserfläche.
* float[10]: [speed, waveScale, waveAmplitude, transparency,
* waterR, waterG, waterB, deepR, deepG, deepB]
*/
public final AtomicReference<float[]> pendingWaterParams = new AtomicReference<>();
/** JavaFX → JME: Selektierte Wasserfläche löschen. */
public volatile boolean deleteWaterRequested = false;
@@ -1146,15 +1166,23 @@ public class SharedInput {
/** JavaFX → JME3: Selektierten Item-Spawn löschen. */
public volatile boolean deleteSelectedItemRequested = false;
/** JME3-intern: Auswahl-Klick für ItemPlacementState (kein Objekt/keine Zone getroffen). */
public final ConcurrentLinkedQueue<ObjectClick> auswahlClickQueue = new ConcurrentLinkedQueue<>();
public final ConcurrentLinkedQueue<ObjectClick> auswahlClickQueue = new ConcurrentLinkedQueue<>();
/** JME3-intern: Auswahl-Klick für LightState (kein Objekt/keine Zone getroffen). */
public final ConcurrentLinkedQueue<ObjectClick> auswahlLightClickQueue = new ConcurrentLinkedQueue<>();
public final ConcurrentLinkedQueue<ObjectClick> auswahlLightClickQueue = new ConcurrentLinkedQueue<>();
/** JME3-intern: Auswahl-Klick für EmitterState (kein Objekt/keine Zone getroffen). */
public final ConcurrentLinkedQueue<ObjectClick> auswahlEmitterClickQueue = new ConcurrentLinkedQueue<>();
public final ConcurrentLinkedQueue<ObjectClick> auswahlEmitterClickQueue = new ConcurrentLinkedQueue<>();
/** JME3-intern: Auswahl-Klick für WaterBodyState. */
public final ConcurrentLinkedQueue<ObjectClick> auswahlWaterClickQueue = new ConcurrentLinkedQueue<>();
/** JME3-intern: Auswahl-Klick für WaterfallEditorState. */
public final ConcurrentLinkedQueue<ObjectClick> auswahlWaterfallClickQueue = new ConcurrentLinkedQueue<>();
/** JME3 → JavaFX: Licht im Auswahl-Modus selektiert → Atmosphäre/Licht-Werkzeug aktivieren. */
public volatile boolean auswahlLightSelected = false;
public volatile boolean auswahlLightSelected = false;
/** JME3 → JavaFX: Emitter im Auswahl-Modus selektiert → Atmosphäre/Emitter-Werkzeug aktivieren. */
public volatile boolean auswahlEmitterSelected = false;
public volatile boolean auswahlEmitterSelected = false;
/** JME3 → JavaFX: Wasserfläche im Auswahl-Modus selektiert → Wasser-Werkzeug aktivieren. */
public volatile boolean auswahlWaterSelected = false;
/** JME3 → JavaFX: Wasserfall im Auswahl-Modus selektiert → Wasserfall-Werkzeug aktivieren. */
public volatile boolean auswahlWaterfallSelected = false;
// ── Voxel-Textur-Malen ────────────────────────────────────────────────────
/** Parallel-Queue zu textureEditQueue wird von submitEdit(layer=4) mitbefüllt,

View File

@@ -89,13 +89,27 @@ public class RiverEditorState extends BaseAppState {
@Override
public void update(float tpf) {
if (input.activeLayer != SharedInput.LAYER_WATERFALL) return;
if (input.activeLayer != SharedInput.LAYER_RIVER) return;
if (input.undoWaterfallPointRequested) {
input.undoWaterfallPointRequested = false;
undoLastPoint();
}
if (input.finalizeWaterfallRequested) {
input.finalizeWaterfallRequested = false;
if (activeRiver >= 0) finalizeActiveRiver();
}
if (input.cancelZoneDrawing) {
input.cancelZoneDrawing = false;
if (activeRiver >= 0) {
cancelActiveRiver();
} else if (selectedRiver >= 0) {
selectRiver(-1);
}
}
if (input.deleteWaterfallRequested) {
input.deleteWaterfallRequested = false;
if (selectedRiver >= 0) {
@@ -114,7 +128,7 @@ public class RiverEditorState extends BaseAppState {
private void handleClick(SharedInput.WaterfallClick click) {
if (click.rightButton()) {
finalizeActiveRiver();
undoLastPoint();
return;
}
@@ -137,6 +151,7 @@ public class RiverEditorState extends BaseAppState {
int nearby = findNearestRiver(hit, 8f);
if (nearby >= 0) {
selectRiver(nearby);
activeRiver = nearby; // bestehenden Wasserfall bearbeiten (Punkte anhängen)
return;
}
selectRiver(-1);
@@ -163,6 +178,13 @@ public class RiverEditorState extends BaseAppState {
rebuildActiveRibbon();
}
private void cancelActiveRiver() {
if (activeRiver < 0 || activeRiver >= rivers.size()) return;
removeRiver(activeRiver);
activeRiver = -1;
selectRiver(-1);
}
private void finalizeActiveRiver() {
if (activeRiver >= 0 && activeRiver < rivers.size()) {
if (rivers.get(activeRiver).size() < 2) {

View File

@@ -729,6 +729,8 @@ public class SceneObjectState extends BaseAppState {
input.auswahlClickQueue.offer(click);
input.auswahlLightClickQueue.offer(click);
input.auswahlEmitterClickQueue.offer(click);
input.auswahlWaterClickQueue.offer(click);
input.auswahlWaterfallClickQueue.offer(click);
return;
}
if (input.activeLayer != SharedInput.LAYER_OBJECTS) { deselectAll(); return; }

View File

@@ -390,6 +390,11 @@ public class TerrainEditorState extends BaseAppState {
riverEditorState.setTerrain(terrain);
}
WaterfallEditorState waterfallEditorState = app.getStateManager().getState(WaterfallEditorState.class);
if (waterfallEditorState != null) {
waterfallEditorState.setTerrain(terrain);
}
input.loadingStatus = "Baue Szene...";
VoxelEditorState ves = app.getStateManager().getState(VoxelEditorState.class);
if (ves != null) ves.setTerrainNode(terrain);
@@ -1811,10 +1816,8 @@ public class TerrainEditorState extends BaseAppState {
int mode = input.heightTool.mode.getSelectedIndex();
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() == 0) continue;
Vector3f contact = hits.getClosestCollision().getContactPoint();
Vector3f contact = raycastSurface(ray);
if (contact == null) continue;
// Plateau-RMB: Höhe von der sichtbaren Oberfläche samplen (Basis + Voxel)
if (mode == HeightTool.MODE_PLATEAU && edit.action() < 0) {
@@ -1871,6 +1874,15 @@ public class TerrainEditorState extends BaseAppState {
}
}
}
// 4. Gebackene Voxel-Meshes (SculptedMeshEditorState) direkt raycasten
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) {
float vh = sp.y;
if (Float.isFinite(vh)) h = Float.isFinite(h) ? Math.max(h, vh) : vh;
}
}
if (Float.isFinite(h)) {
input.heightTool.plateauHeight.setValue(h);
input.heightTool.plateauHeightChanged = true;

View File

@@ -0,0 +1,739 @@
package de.blight.editor.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.collision.CollisionResults;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.*;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.shape.Sphere;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.util.BufferUtils;
import de.blight.common.PlacedWater;
import de.blight.common.PlacedWaterfall;
import de.blight.common.WaterfallIO;
import de.blight.editor.SharedInput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Editor-State für das 4-Klick-Wasserfall-Werkzeug.
*
* Workflow:
* 14 Linksklicks setzen Ecken (A=oben-links, B=oben-rechts, C=unten-rechts, D=unten-links).
* Raycast trifft Basis-Terrain, Voxel-Geometrie, SculptedMesh und Seenkanten.
* Selektierter Wasserfall: Ecken per Drag verschiebbar.
* Rechtsklick / Backspace: letzten Punkt rückgängig oder Selektion aufheben.
* Escape: Platzierung abbrechen oder Selektion aufheben.
* Delete: selektierten Wasserfall löschen.
*/
public class WaterfallEditorState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(WaterfallEditorState.class);
private enum Mode { IDLE, PLACING, EDITING }
private static final ColorRGBA COL_PLACING = new ColorRGBA(0.2f, 0.6f, 1.0f, 1f);
private static final ColorRGBA COL_SELECTED = new ColorRGBA(1.0f, 0.8f, 0.1f, 1f);
private static final ColorRGBA COL_NORMAL = new ColorRGBA(0.2f, 0.4f, 0.8f, 1f);
private static final float HANDLE_RADIUS = 0.25f;
private static final float HANDLE_PICK_PX = 20f;
private static final float WATER_SNAP_PX = 30f; // screen-Pixel Snap-Radius für Seenkanten
private final SharedInput input;
private SimpleApplication app;
private Camera cam;
private Node rootNode;
private TerrainQuad terrain;
private Mode mode = Mode.IDLE;
private final List<PlacedWaterfall> waterfalls = new ArrayList<>();
private final List<Node> wfNodes = new ArrayList<>();
// Platzierungs-State
private final List<Vector3f> placingCorners = new ArrayList<>();
private Node placingNode;
// Editier-State
private int selectedIdx = -1;
private Vector3f[] liveCorners = null; // 4 Ecken A,B,C,D live während Drag
private List<Geometry> handleGeos = new ArrayList<>();
private Geometry selOutline = null; // live Umriss des selektierten Wf
private boolean isDragging = false;
private int dragHandle = -1;
public WaterfallEditorState(SharedInput input) {
this.input = input;
}
public void setTerrain(TerrainQuad t) { this.terrain = t; }
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.cam = app.getCamera();
this.rootNode = this.app.getRootNode();
try {
for (PlacedWaterfall wf : WaterfallIO.load()) {
waterfalls.add(wf);
wfNodes.add(buildWfNode(wf, false));
}
} catch (Exception e) {
log.error("Wasserfälle nicht ladbar", e);
}
}
@Override
protected void cleanup(Application app) {
clearPlacing();
clearEditHandles();
for (Node n : wfNodes) n.removeFromParent();
wfNodes.clear();
}
@Override protected void onEnable() { setAllCull(Spatial.CullHint.Inherit); }
@Override protected void onDisable() { setAllCull(Spatial.CullHint.Always); }
@Override
public void update(float tpf) {
if (input.activeLayer == SharedInput.LAYER_AUSWAHL) {
SharedInput.ObjectClick ac;
while ((ac = input.auswahlWaterfallClickQueue.poll()) != null) handleAuswahlClick(ac);
return;
}
if (input.activeLayer != SharedInput.LAYER_WATERFALL) {
if (mode == Mode.PLACING) clearPlacing();
return;
}
if (input.cancelZoneDrawing) {
input.cancelZoneDrawing = false;
if (mode == Mode.PLACING) clearPlacing();
else deselect();
return;
}
if (input.undoWaterfallPointRequested) {
input.undoWaterfallPointRequested = false;
if (mode == Mode.PLACING && !placingCorners.isEmpty()) {
placingCorners.remove(placingCorners.size() - 1);
if (placingCorners.isEmpty()) clearPlacing();
else updatePlacingVisual();
}
}
if (input.deleteWaterfallRequested) {
input.deleteWaterfallRequested = false;
if (selectedIdx >= 0) { removeWf(selectedIdx); deselect(); }
}
SharedInput.WaterfallClick click;
while ((click = input.waterfallClickQueue.poll()) != null) handleClick(click);
SharedInput.WaterfallDrag drag;
while ((drag = input.waterfallDragQueue.poll()) != null) {
if (isDragging) processDragMove(drag.screenX(), drag.screenY());
}
if (input.waterfallDragEnd) {
input.waterfallDragEnd = false;
if (isDragging) finalizeDrag();
}
}
// ── Klick ─────────────────────────────────────────────────────────────────
private void handleClick(SharedInput.WaterfallClick click) {
float jmeX = (float) (click.screenX() * input.viewportScaleX);
float jmeY = cam.getHeight() - (float) (click.screenY() * input.viewportScaleY);
if (click.rightButton()) {
if (mode == Mode.PLACING && !placingCorners.isEmpty()) {
placingCorners.remove(placingCorners.size() - 1);
if (placingCorners.isEmpty()) clearPlacing();
else updatePlacingVisual();
} else {
deselect();
}
return;
}
// Selektierter Wasserfall: Handle-Treffer → Drag starten
if (mode == Mode.EDITING && selectedIdx >= 0) {
int h = findHandleAt(jmeX, jmeY);
if (h >= 0) { startDrag(h); return; }
}
// Raycast + See-Snapping
Ray ray = buildRay(jmeX, jmeY);
Vector3f pt = raycastAll(ray);
Vector3f snapped = snapToWaterVertex(jmeX, jmeY);
if (snapped != null) pt = snapped;
if (pt == null) return;
if (mode == Mode.PLACING) {
placingCorners.add(pt.clone());
updatePlacingVisual();
if (placingCorners.size() == 4) finalizeWaterfall();
return;
}
// Vorhandenen Wasserfall selektieren
int nearby = findNearestWf(pt, 3f);
if (nearby >= 0) { selectWf(nearby); return; }
// Leere Fläche: Platzierung starten
deselect();
mode = Mode.PLACING;
placingCorners.add(pt.clone());
updatePlacingVisual();
}
// ── Finalisieren / Entfernen ──────────────────────────────────────────────
private void finalizeWaterfall() {
Vector3f a = placingCorners.get(0), b = placingCorners.get(1);
Vector3f c = placingCorners.get(2), d = placingCorners.get(3);
PlacedWaterfall wf = new PlacedWaterfall(
a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z, d.x, d.y, d.z,
PlacedWaterfall.DEF_SPEED, PlacedWaterfall.DEF_TRANSPARENCY,
PlacedWaterfall.DEF_R, PlacedWaterfall.DEF_G, PlacedWaterfall.DEF_B);
clearPlacing();
waterfalls.add(wf);
wfNodes.add(buildWfNode(wf, false));
save();
selectWf(waterfalls.size() - 1);
}
private void removeWf(int idx) {
if (idx < 0 || idx >= waterfalls.size()) return;
wfNodes.remove(idx).removeFromParent();
waterfalls.remove(idx);
save();
}
// ── Selektion ─────────────────────────────────────────────────────────────
private void selectWf(int idx) {
// Vorherige Selektion aufheben
if (selectedIdx >= 0 && selectedIdx < wfNodes.size()) {
wfNodes.get(selectedIdx).setCullHint(Spatial.CullHint.Inherit);
}
clearEditHandles();
selectedIdx = idx;
mode = (idx >= 0) ? Mode.EDITING : Mode.IDLE;
if (idx >= 0 && idx < waterfalls.size()) {
// wfNode ausblenden — Handles + selOutline übernehmen die Darstellung
wfNodes.get(idx).setCullHint(Spatial.CullHint.Always);
PlacedWaterfall wf = waterfalls.get(idx);
liveCorners = new Vector3f[]{
new Vector3f(wf.ax(), wf.ay(), wf.az()),
new Vector3f(wf.bx(), wf.by(), wf.bz()),
new Vector3f(wf.cx(), wf.cy(), wf.cz()),
new Vector3f(wf.dx(), wf.dy(), wf.dz())
};
buildHandles();
buildSelOutline();
}
publishSelection(idx);
}
private void deselect() {
if (selectedIdx >= 0 && selectedIdx < wfNodes.size()) {
wfNodes.get(selectedIdx).setCullHint(Spatial.CullHint.Inherit);
}
selectedIdx = -1;
mode = Mode.IDLE;
liveCorners = null;
isDragging = false;
dragHandle = -1;
clearEditHandles();
publishSelection(-1);
}
private void publishSelection(int idx) {
input.selectedWaterfallInfo = (idx >= 0 && idx < waterfalls.size())
? (idx + "|4|" + String.format(java.util.Locale.ROOT, "%.1f", quadWidth(waterfalls.get(idx))))
: null;
input.waterfallSelectionChanged = true;
}
// ── Drag ──────────────────────────────────────────────────────────────────
private void startDrag(int h) {
isDragging = true;
dragHandle = h;
}
private void processDragMove(float screenX, float screenY) {
if (!isDragging || selectedIdx < 0 || liveCorners == null) return;
float jmeX = screenX * (float) input.viewportScaleX;
float jmeY = cam.getHeight() - screenY * (float) input.viewportScaleY;
Vector3f pt = raycastAll(buildRay(jmeX, jmeY));
Vector3f snapped = snapToWaterVertex(jmeX, jmeY);
if (snapped != null) pt = snapped;
if (pt == null) return;
liveCorners[dragHandle].set(pt);
rebuildHandlePositions();
rebuildSelOutline();
}
private void finalizeDrag() {
isDragging = false;
dragHandle = -1;
if (selectedIdx < 0 || liveCorners == null) return;
Vector3f a = liveCorners[0], b = liveCorners[1], c = liveCorners[2], d = liveCorners[3];
PlacedWaterfall old = waterfalls.get(selectedIdx);
PlacedWaterfall updated = new PlacedWaterfall(
a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z, d.x, d.y, d.z,
old.speed(), old.transparency(), old.colorR(), old.colorG(), old.colorB());
waterfalls.set(selectedIdx, updated);
// wfNode (verborgen) auf neue Positionen updaten, damit er beim Abwählen stimmt
Node old2 = wfNodes.get(selectedIdx);
old2.removeFromParent();
Node fresh = buildWfNode(updated, false);
fresh.setCullHint(Spatial.CullHint.Always); // bleibt verborgen, solange selektiert
wfNodes.set(selectedIdx, fresh);
save();
publishSelection(selectedIdx);
}
// ── Handles ───────────────────────────────────────────────────────────────
private void buildHandles() {
if (liveCorners == null) return;
for (int i = 0; i < 4; i++) {
Geometry g = buildSphere("wf_h" + i, liveCorners[i], COL_SELECTED);
handleGeos.add(g);
rootNode.attachChild(g);
}
}
private void rebuildHandlePositions() {
for (int i = 0; i < 4 && i < handleGeos.size(); i++) {
handleGeos.get(i).setLocalTranslation(liveCorners[i]);
}
}
private void clearEditHandles() {
for (Geometry g : handleGeos) g.removeFromParent();
handleGeos.clear();
if (selOutline != null) { selOutline.removeFromParent(); selOutline = null; }
liveCorners = null;
}
private int findHandleAt(float jmeX, float jmeY) {
if (liveCorners == null) return -1;
float bestSq = HANDLE_PICK_PX * HANDLE_PICK_PX;
int best = -1;
for (int i = 0; i < 4; i++) {
Vector3f sc = cam.getScreenCoordinates(liveCorners[i]);
if (sc.z <= 0 || sc.z >= 1) continue;
float dx = sc.x - jmeX, dy = sc.y - jmeY;
float d = dx * dx + dy * dy;
if (d < bestSq) { bestSq = d; best = i; }
}
return best;
}
private void buildSelOutline() {
if (selOutline != null) { selOutline.removeFromParent(); selOutline = null; }
if (liveCorners == null) return;
selOutline = buildLineLoop(toFlatArray(liveCorners), COL_SELECTED);
rootNode.attachChild(selOutline);
}
private void rebuildSelOutline() {
if (selOutline == null || liveCorners == null) return;
float[] pts = toFlatArray(liveCorners);
selOutline.getMesh().setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pts));
selOutline.getMesh().updateBound();
}
// ── See-Kanten-Snapping ────────────────────────────────────────────────────
/**
* Prüft ob ein Seekanten-Vertex näher als WATER_SNAP_PX Pixel am Cursor liegt.
* Gibt den 3D-Vertex zurück oder null.
*/
private Vector3f snapToWaterVertex(float jmeX, float jmeY) {
WaterBodyState wbs = getStateManager().getState(WaterBodyState.class);
if (wbs == null) return null;
List<PlacedWater> bodies = wbs.getPlacedBodies();
if (bodies.isEmpty()) return null;
float bestSq = WATER_SNAP_PX * WATER_SNAP_PX;
Vector3f best = null;
for (PlacedWater body : bodies) {
float[] xs = body.pointsX(), ys = body.pointsY(), zs = body.pointsZ();
for (int i = 0; i < xs.length; i++) {
Vector3f sc = cam.getScreenCoordinates(new Vector3f(xs[i], ys[i], zs[i]));
if (sc.z <= 0 || sc.z >= 1) continue;
float dx = sc.x - jmeX, dy = sc.y - jmeY;
float d = dx * dx + dy * dy;
if (d < bestSq) { bestSq = d; best = new Vector3f(xs[i], ys[i], zs[i]); }
}
}
return best;
}
// ── Raycast ───────────────────────────────────────────────────────────────
private Ray buildRay(float jmeX, float jmeY) {
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
return new Ray(near, far.subtract(near).normalizeLocal());
}
private Vector3f raycastAll(Ray ray) {
Vector3f best = null;
float bestDistSq = Float.MAX_VALUE;
if (terrain != null) {
CollisionResults res = new CollisionResults();
terrain.collideWith(ray, res);
if (res.size() > 0) {
Vector3f p = res.getClosestCollision().getContactPoint();
float d = ray.origin.distanceSquared(p);
if (d < bestDistSq) { best = p; bestDistSq = d; }
}
}
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
Vector3f vp = ves.raycastVoxelGeometry(ray);
if (vp != null) {
float d = ray.origin.distanceSquared(vp);
if (d < bestDistSq) { best = vp; bestDistSq = d; }
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) {
float d = ray.origin.distanceSquared(sp);
if (d < bestDistSq) { best = sp; }
}
}
return best;
}
// ── Visuals ───────────────────────────────────────────────────────────────
private int findNearestWf(Vector3f pos, float maxDist) {
int best = -1;
float bestD = maxDist * maxDist;
for (int i = 0; i < waterfalls.size(); i++) {
PlacedWaterfall wf = waterfalls.get(i);
float[] pts = { wf.ax(), wf.ay(), wf.az(), wf.bx(), wf.by(), wf.bz(),
wf.cx(), wf.cy(), wf.cz(), wf.dx(), wf.dy(), wf.dz() };
for (int k = 0; k < 4; k++) {
float dx = pos.x - pts[k*3], dy = pos.y - pts[k*3+1], dz = pos.z - pts[k*3+2];
float d2 = dx*dx + dy*dy + dz*dz;
if (d2 < bestD) { bestD = d2; best = i; }
}
}
return best;
}
private Node buildWfNode(PlacedWaterfall wf, boolean selected) {
Node n = new Node("wf_node");
ColorRGBA col = selected ? COL_SELECTED : COL_NORMAL;
float[] pts = { wf.ax(), wf.ay(), wf.az(), wf.bx(), wf.by(), wf.bz(),
wf.cx(), wf.cy(), wf.cz(), wf.dx(), wf.dy(), wf.dz() };
n.attachChild(buildFillQuad(wf));
n.attachChild(buildLineLoop(pts, col));
rootNode.attachChild(n);
return n;
}
// Muss mit WaterfallState (game) synchron gehalten werden
private static final int PREVIEW_ROWS = 24;
private static final int PREVIEW_COLS = 6;
private static final float PREVIEW_BULGE_MAX = 1.2f;
private static final float PREVIEW_V_RANGE = 0.30f;
private Geometry buildFillQuad(PlacedWaterfall wf) {
Mesh mesh = buildBulgeMesh(wf);
Geometry g = new Geometry("wf_fill", mesh);
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(1f, 1f, 1f, 0.25f));
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
mat.getAdditionalRenderState().setDepthWrite(false);
g.setMaterial(mat);
g.setQueueBucket(RenderQueue.Bucket.Transparent);
return g;
}
private static Mesh buildBulgeMesh(PlacedWaterfall wf) {
Vector3f a = new Vector3f(wf.ax(), wf.ay(), wf.az());
Vector3f b = new Vector3f(wf.bx(), wf.by(), wf.bz());
Vector3f c = new Vector3f(wf.cx(), wf.cy(), wf.cz());
Vector3f d = new Vector3f(wf.dx(), wf.dy(), wf.dz());
Vector3f n1 = d.subtract(a).cross(b.subtract(a)).normalizeLocal();
Vector3f n2 = b.subtract(c).cross(d.subtract(c)).normalizeLocal();
Vector3f avgNorm = n1.add(n2).normalizeLocal();
if (avgNorm.lengthSquared() < 1e-4f) avgNorm.set(0f, 0f, 1f);
Vector3f outward = new Vector3f(avgNorm.x, 0f, avgNorm.z);
if (outward.lengthSquared() < 1e-4f) outward.set(avgNorm);
else outward.normalizeLocal();
int vRows = PREVIEW_ROWS + 1, vCols = PREVIEW_COLS + 1;
FloatBuffer pos = BufferUtils.createFloatBuffer(vRows * vCols * 3);
IntBuffer idx = BufferUtils.createIntBuffer(PREVIEW_ROWS * PREVIEW_COLS * 6);
for (int row = 0; row < vRows; row++) {
float v = (float) row / PREVIEW_ROWS;
float bulge = (v < PREVIEW_V_RANGE)
? PREVIEW_BULGE_MAX * (float) Math.sin(Math.PI / 2f * v / PREVIEW_V_RANGE)
: PREVIEW_BULGE_MAX;
for (int col = 0; col < vCols; col++) {
float u = (float) col / PREVIEW_COLS;
Vector3f p = a.mult(1f - u).add(b.mult(u))
.mult(1f - v)
.add(d.mult(1f - u).add(c.mult(u)).mult(v));
p.addLocal(outward.mult(bulge));
pos.put(p.x).put(p.y).put(p.z);
}
}
for (int row = 0; row < PREVIEW_ROWS; row++) {
for (int col = 0; col < PREVIEW_COLS; col++) {
int i0 = row * vCols + col, i1 = i0 + 1, i2 = i0 + vCols, i3 = i2 + 1;
idx.put(i0).put(i2).put(i1);
idx.put(i1).put(i2).put(i3);
}
}
pos.rewind(); idx.rewind();
Mesh mesh = new Mesh();
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
mesh.updateBound();
mesh.updateCounts();
return mesh;
}
private void updatePlacingVisual() {
if (placingNode == null) {
placingNode = new Node("wf_placing");
rootNode.attachChild(placingNode);
}
placingNode.detachAllChildren();
for (Vector3f p : placingCorners) {
placingNode.attachChild(buildSphere("wf_ph", p, COL_PLACING));
}
if (placingCorners.size() >= 2) {
float[] pts = new float[placingCorners.size() * 3];
for (int i = 0; i < placingCorners.size(); i++) {
pts[i*3] = placingCorners.get(i).x;
pts[i*3+1] = placingCorners.get(i).y;
pts[i*3+2] = placingCorners.get(i).z;
}
placingNode.attachChild(buildLineStrip(pts, COL_PLACING));
}
}
private void clearPlacing() {
placingCorners.clear();
if (placingNode != null) { placingNode.removeFromParent(); placingNode = null; }
if (mode == Mode.PLACING) mode = Mode.IDLE;
}
private void setAllCull(Spatial.CullHint hint) {
for (int i = 0; i < wfNodes.size(); i++) {
// Selektierter bleibt Always wenn hint=Always, sonst Inherit
if (hint == Spatial.CullHint.Always || i != selectedIdx) {
wfNodes.get(i).setCullHint(hint);
}
}
if (placingNode != null) placingNode.setCullHint(hint);
for (Geometry g : handleGeos) g.setCullHint(hint);
if (selOutline != null) selOutline.setCullHint(hint);
}
// ── Geometry-Helfer ───────────────────────────────────────────────────────
private Geometry buildSphere(String name, Vector3f pos, ColorRGBA col) {
Sphere s = new Sphere(8, 8, HANDLE_RADIUS);
Geometry g = new Geometry(name, s);
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", col);
g.setMaterial(mat);
g.setLocalTranslation(pos);
g.setQueueBucket(RenderQueue.Bucket.Transparent);
return g;
}
private Geometry buildLineLoop(float[] pts, ColorRGBA col) {
int n = pts.length / 3;
IntBuffer idx = BufferUtils.createIntBuffer(n * 2);
for (int i = 0; i < n; i++) { idx.put(i).put((i + 1) % n); }
idx.rewind();
Mesh mesh = new Mesh();
mesh.setMode(Mesh.Mode.Lines);
mesh.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pts));
mesh.setBuffer(VertexBuffer.Type.Index, 2, idx);
mesh.updateBound(); mesh.updateCounts();
Geometry g = new Geometry("wf_loop", mesh);
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", col);
mat.getAdditionalRenderState().setLineWidth(2f);
g.setMaterial(mat);
return g;
}
private Geometry buildLineStrip(float[] pts, ColorRGBA col) {
int n = pts.length / 3;
IntBuffer idx = BufferUtils.createIntBuffer((n - 1) * 2);
for (int i = 0; i < n - 1; i++) { idx.put(i).put(i + 1); }
idx.rewind();
Mesh mesh = new Mesh();
mesh.setMode(Mesh.Mode.Lines);
mesh.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pts));
mesh.setBuffer(VertexBuffer.Type.Index, 2, idx);
mesh.updateBound(); mesh.updateCounts();
Geometry g = new Geometry("wf_strip", mesh);
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", col);
mat.getAdditionalRenderState().setLineWidth(2f);
g.setMaterial(mat);
return g;
}
private static float[] toFlatArray(Vector3f[] corners) {
float[] pts = new float[corners.length * 3];
for (int i = 0; i < corners.length; i++) {
pts[i*3] = corners[i].x; pts[i*3+1] = corners[i].y; pts[i*3+2] = corners[i].z;
}
return pts;
}
// ── Auswahl-Modus ─────────────────────────────────────────────────────────
private void handleAuswahlClick(SharedInput.ObjectClick click) {
if (click.rightButton()) return;
float jmeX = click.screenX() * (float) input.viewportScaleX;
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
Ray ray = buildRay(jmeX, jmeY);
for (int i = 0; i < waterfalls.size(); i++) {
if (rayHitsWaterfall(ray, waterfalls.get(i))) {
selectWf(i);
input.auswahlWaterfallSelected = true;
return;
}
}
}
private boolean rayHitsWaterfall(Ray ray, PlacedWaterfall wf) {
Vector3f a = new Vector3f(wf.ax(), wf.ay(), wf.az());
Vector3f b = new Vector3f(wf.bx(), wf.by(), wf.bz());
Vector3f c = new Vector3f(wf.cx(), wf.cy(), wf.cz());
Vector3f d = new Vector3f(wf.dx(), wf.dy(), wf.dz());
return rayHitsTriangle(ray, a, d, b) || rayHitsTriangle(ray, b, d, c);
}
/** MöllerTrumbore raytriangle intersection, beide Seiten (kein Backface-Culling). */
private static boolean rayHitsTriangle(Ray ray, Vector3f v0, Vector3f v1, Vector3f v2) {
Vector3f e1 = v1.subtract(v0);
Vector3f e2 = v2.subtract(v0);
Vector3f h = ray.direction.cross(e2);
float det = e1.dot(h);
if (Math.abs(det) < 1e-6f) return false;
float inv = 1f / det;
Vector3f s = ray.origin.subtract(v0);
float u = inv * s.dot(h);
if (u < 0f || u > 1f) return false;
Vector3f q = s.cross(e1);
float v = inv * ray.direction.dot(q);
if (v < 0f || u + v > 1f) return false;
float t = inv * e2.dot(q);
return t > 1e-4f;
}
// ── Externaler Sync (von WaterBodyState) ──────────────────────────────────
/**
* Verschiebt alle Wasserfall-Ecken die sich näher als {@code threshold} Meter
* an (ox,oy,oz) befinden auf die neue Position (nx,ny,nz).
* Gibt true zurück wenn mindestens eine Ecke geändert wurde (zum Batching des save()-Aufrufs).
* Wird von WaterBodyState.finalizeDrag() aufgerufen.
*/
public boolean syncCornersFromVertex(float ox, float oy, float oz,
float nx, float ny, float nz, float threshold) {
float t2 = threshold * threshold;
boolean anyChanged = false;
for (int i = 0; i < waterfalls.size(); i++) {
PlacedWaterfall wf = waterfalls.get(i);
float[] cx = { wf.ax(), wf.bx(), wf.cx(), wf.dx() };
float[] cy = { wf.ay(), wf.by(), wf.cy(), wf.dy() };
float[] cz = { wf.az(), wf.bz(), wf.cz(), wf.dz() };
boolean wfChanged = false;
for (int c = 0; c < 4; c++) {
float ddx = cx[c] - ox, ddy = cy[c] - oy, ddz = cz[c] - oz;
if (ddx*ddx + ddy*ddy + ddz*ddz < t2) {
cx[c] = nx; cy[c] = ny; cz[c] = nz;
wfChanged = true;
}
}
if (wfChanged) {
PlacedWaterfall upd = new PlacedWaterfall(
cx[0], cy[0], cz[0], cx[1], cy[1], cz[1],
cx[2], cy[2], cz[2], cx[3], cy[3], cz[3],
wf.speed(), wf.transparency(), wf.colorR(), wf.colorG(), wf.colorB());
waterfalls.set(i, upd);
boolean isSelected = (i == selectedIdx);
wfNodes.remove(i).removeFromParent();
Node fresh = buildWfNode(upd, false);
wfNodes.add(i, fresh);
if (isSelected) {
fresh.setCullHint(Spatial.CullHint.Always);
if (liveCorners != null) {
liveCorners[0].set(upd.ax(), upd.ay(), upd.az());
liveCorners[1].set(upd.bx(), upd.by(), upd.bz());
liveCorners[2].set(upd.cx(), upd.cy(), upd.cz());
liveCorners[3].set(upd.dx(), upd.dy(), upd.dz());
rebuildHandlePositions();
rebuildSelOutline();
}
}
anyChanged = true;
}
}
return anyChanged;
}
public void save() {
try {
WaterfallIO.save(waterfalls);
} catch (Exception e) {
log.error("Wasserfall-Speichern fehlgeschlagen", e);
}
}
private static float quadWidth(PlacedWaterfall wf) {
float dx = wf.bx() - wf.ax(), dy = wf.by() - wf.ay(), dz = wf.bz() - wf.az();
return (float) Math.sqrt(dx*dx + dy*dy + dz*dz);
}
}

View File

@@ -106,6 +106,7 @@ public class MapObjectsView extends VBox {
loadLights();
loadEmitters();
loadWaterBodies();
loadWaterfalls();
loadRivers();
loadSoundAreas();
loadAreas();
@@ -203,17 +204,35 @@ public class MapObjectsView extends VBox {
} catch (Exception ignored) {}
}
private void loadWaterfalls() {
try {
List<PlacedWaterfall> list = WaterfallIO.load();
TreeItem<String> group = group("Wasserfälle", list.size());
for (int idx = 0; idx < list.size(); idx++) {
PlacedWaterfall wf = list.get(idx);
float cx = (wf.ax() + wf.bx() + wf.cx() + wf.dx()) / 4f;
float cy = (wf.ay() + wf.by() + wf.cy() + wf.dy()) / 4f;
float cz = (wf.az() + wf.bz() + wf.cz() + wf.dz()) / 4f;
TreeItem<String> item = leaf("Wasserfall #" + (idx + 1) + " " + pos(cx, cy, cz));
entryMap.put(item, new Entry(cx, cy + 5f, cz, "waterfall", wf, idx));
group.getChildren().add(item);
}
treeRoot.getChildren().add(group);
} catch (Exception ignored) {}
}
private void loadRivers() {
try {
List<List<RiverPoint>> list = RiverIO.load();
TreeItem<String> group = group("Wasserfälle", list.size());
if (list.isEmpty()) return;
TreeItem<String> group = group("Flüsse (veraltet)", list.size());
for (int idx = 0; idx < list.size(); idx++) {
List<RiverPoint> river = list.get(idx);
float cx = 0f, cy = 0f, cz = 0f;
for (RiverPoint pt : river) { cx += pt.x(); cy += pt.y(); cz += pt.z(); }
if (!river.isEmpty()) { cx /= river.size(); cy /= river.size(); cz /= river.size(); }
TreeItem<String> item = leaf("Wasserfall #" + (idx + 1) + " " + pos(cx, cy, cz));
entryMap.put(item, new Entry(cx, cy + 5f, cz, "waterfall", river, idx));
TreeItem<String> item = leaf("Fluss #" + (idx + 1) + " " + pos(cx, cy, cz));
entryMap.put(item, new Entry(cx, cy + 5f, cz, "river", river, idx));
group.getChildren().add(item);
}
treeRoot.getChildren().add(group);