Compare commits
8 Commits
52801b5ddf
...
6e2c3a2785
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e2c3a2785 | |||
| feef7cab62 | |||
| 8f0cf7bc42 | |||
| bf1bb4fd16 | |||
| f1b321731b | |||
| f0a0c216f6 | |||
| e4b0ee0dc3 | |||
| 1b509c3a52 |
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
|||||||
|
package de.blight.editor;
|
||||||
|
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared-Dependencies-Container: wird einmalig in EditorApp erzeugt und
|
||||||
|
* an alle Controller-Implementierungen weitergegeben.
|
||||||
|
*
|
||||||
|
* @param input Gemeinsame JavaFX ↔ JME3 Datenbasis
|
||||||
|
* @param jmeApp JME3-Anwendung (für StateManager-Zugriff)
|
||||||
|
* @param stageSupplier Liefert die primäre Stage (für Dialoge)
|
||||||
|
* @param setStatus Setzt den Text im Statusbalken
|
||||||
|
* @param setCenterView Setzt den Center-Node des Haupt-BorderPane
|
||||||
|
* @param setRightPanel Setzt den Right-Node des Haupt-BorderPane
|
||||||
|
* @param setMainToolbar Ersetzt die aktive Werkzeug-Toolbar in der TopBar
|
||||||
|
* @param viewportSupplier Liefert den JME3-Viewport-StackPane
|
||||||
|
* @param assetRoot Wurzelpfad der blight-assets (für Dateidialoge / IO)
|
||||||
|
* @param switchToWorld Wechselt zurück zum Welteneditor
|
||||||
|
* @param registry ToolRegistry (damit Controller sich selbst aktivieren können)
|
||||||
|
* @param setLeftPanel Setzt den Left-Node des Haupt-BorderPane
|
||||||
|
* @param setBottomPanel Setzt den Bottom-Node des Haupt-BorderPane
|
||||||
|
* @param centerStackSupplier Liefert den persistenten 3D-Viewport-Wrapper
|
||||||
|
* @param setCurrentTool Setzt das aktive Tool-Label in EditorApp (für Keyboard-Routing)
|
||||||
|
*/
|
||||||
|
public record ToolContext(
|
||||||
|
SharedInput input,
|
||||||
|
JmeEditorApp jmeApp,
|
||||||
|
Supplier<Stage> stageSupplier,
|
||||||
|
Consumer<String> setStatus,
|
||||||
|
Consumer<Node> setCenterView,
|
||||||
|
Consumer<Node> setRightPanel,
|
||||||
|
Consumer<Node> setMainToolbar,
|
||||||
|
Supplier<StackPane> viewportSupplier,
|
||||||
|
Path assetRoot,
|
||||||
|
Runnable switchToWorld,
|
||||||
|
ToolRegistry registry,
|
||||||
|
Consumer<Node> setLeftPanel,
|
||||||
|
Consumer<Node> setBottomPanel,
|
||||||
|
Supplier<StackPane> centerStackSupplier,
|
||||||
|
Consumer<String> setCurrentTool
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package de.blight.editor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gemeinsame Schnittstelle für alle Editor-Werkzeug-Controller.
|
||||||
|
*
|
||||||
|
* Jeder Controller verwaltet seinen eigenen UI-Zustand (Felder, Panels),
|
||||||
|
* setzt beim Aktivieren Toolbar + Panel und delegiert sein
|
||||||
|
* AnimationTimer-Polling über {@link #onFrame()}.
|
||||||
|
*
|
||||||
|
* Nicht zu verwechseln mit {@link de.blight.editor.tool.EditorTool}, das
|
||||||
|
* die Terrain-Brush-Parameter-Abstraktion ist.
|
||||||
|
*/
|
||||||
|
public interface ToolController {
|
||||||
|
|
||||||
|
/** Eindeutige Werkzeug-ID (z. B. "vegetation", "object") — für Debugging. */
|
||||||
|
String toolId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktiviert das Werkzeug: setzt Toolbar, rechtes Panel, Center-View
|
||||||
|
* und ggf. {@code SharedInput.activeLayer}.
|
||||||
|
*/
|
||||||
|
void activate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deaktiviert das Werkzeug: räumt temporäre JME-Zustände oder UI-Artefakte auf.
|
||||||
|
* Wird vor jedem {@link #activate()} eines anderen Werkzeugs aufgerufen.
|
||||||
|
*/
|
||||||
|
void deactivate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird jeden JavaFX-AnimationTimer-Frame aufgerufen (nur solange aktiv).
|
||||||
|
* Hier landen alle SharedInput-Polling-Aufgaben, die früher in EditorApps
|
||||||
|
* AnimationTimer standen.
|
||||||
|
*/
|
||||||
|
void onFrame();
|
||||||
|
|
||||||
|
/** F5-Taste: Vorschau/Preview-Aktion (nur wenn dieses Tool aktiv ist). */
|
||||||
|
default void onPreview() {}
|
||||||
|
|
||||||
|
/** F6-Taste: Zufälligen Seed wählen (nur wenn dieses Tool aktiv ist). */
|
||||||
|
default void onRandomSeed() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package de.blight.editor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet das aktive Werkzeug und leitet den AnimationTimer-Frame weiter.
|
||||||
|
*
|
||||||
|
* EditorApp hält eine Instanz, verdrahtet die Toolbar-Buttons damit und ruft
|
||||||
|
* {@link #onFrame()} im AnimationTimer auf.
|
||||||
|
*/
|
||||||
|
public class ToolRegistry {
|
||||||
|
|
||||||
|
private ToolController active = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktiviert {@code tool}: deaktiviert das bisher aktive (falls vorhanden),
|
||||||
|
* setzt {@code tool} als aktiv und ruft {@link ToolController#activate()} auf.
|
||||||
|
* Idempotent wenn dasselbe Tool nochmals übergeben wird.
|
||||||
|
*/
|
||||||
|
public void activate(ToolController tool) {
|
||||||
|
if (tool == active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (active != null) {
|
||||||
|
active.deactivate();
|
||||||
|
}
|
||||||
|
active = tool;
|
||||||
|
tool.activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt das aktuell aktive Werkzeug zurück, oder {@code null}. */
|
||||||
|
public ToolController getActive() {
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt die Tool-ID des aktiven Werkzeugs zurück, oder {@code ""} wenn keines aktiv. */
|
||||||
|
public String activeToolId() {
|
||||||
|
return active != null ? active.toolId() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deaktiviert das aktive Werkzeug und setzt den Zustand auf "kein Tool aktiv". */
|
||||||
|
public void deactivateAll() {
|
||||||
|
if (active != null) {
|
||||||
|
active.deactivate();
|
||||||
|
active = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Muss jeden JavaFX-AnimationTimer-Frame aufgerufen werden.
|
||||||
|
* Delegiert an {@link ToolController#onFrame()} des aktiven Werkzeugs.
|
||||||
|
*/
|
||||||
|
public void onFrame() {
|
||||||
|
if (active != null) {
|
||||||
|
active.onFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,8 +40,8 @@ public class WaterBodyState extends BaseAppState {
|
|||||||
|
|
||||||
private static final float LINE_OFFSET = 0.1f;
|
private static final float LINE_OFFSET = 0.1f;
|
||||||
private static final float POLE_HEIGHT = 5.0f;
|
private static final float POLE_HEIGHT = 5.0f;
|
||||||
private static final float CAP_SIZE = 1.2f;
|
private static final float CAP_SIZE = 2.0f;
|
||||||
private static final float HANDLE_PICK_PX = 24f;
|
private static final float HANDLE_PICK_PX = 32f;
|
||||||
private static final float EDGE_SPLIT_THRESHOLD = 1.5f;
|
private static final float EDGE_SPLIT_THRESHOLD = 1.5f;
|
||||||
|
|
||||||
private static final ColorRGBA COLOR_WATER = new ColorRGBA(0.05f, 0.25f, 0.70f, 0.50f);
|
private static final ColorRGBA COLOR_WATER = new ColorRGBA(0.05f, 0.25f, 0.70f, 0.50f);
|
||||||
@@ -53,8 +53,9 @@ public class WaterBodyState extends BaseAppState {
|
|||||||
private static final ColorRGBA COLOR_ARROW = new ColorRGBA(1f, 0.85f, 0f, 1f);
|
private static final ColorRGBA COLOR_ARROW = new ColorRGBA(1f, 0.85f, 0f, 1f);
|
||||||
private static final ColorRGBA COLOR_POLE_CORNER = new ColorRGBA(0f, 0.9f, 1.0f, 1f);
|
private static final ColorRGBA COLOR_POLE_CORNER = new ColorRGBA(0f, 0.9f, 1.0f, 1f);
|
||||||
private static final ColorRGBA COLOR_POLE_EDGE = new ColorRGBA(0f, 0.55f, 0.75f, 1f);
|
private static final ColorRGBA COLOR_POLE_EDGE = new ColorRGBA(0f, 0.55f, 0.75f, 1f);
|
||||||
private static final ColorRGBA COLOR_POLE_HOVER = new ColorRGBA(1f, 0.9f, 0f, 1f);
|
private static final ColorRGBA COLOR_POLE_HOVER = new ColorRGBA(1f, 0.9f, 0f, 1f);
|
||||||
private static final ColorRGBA COLOR_EDGE_SPLIT = new ColorRGBA(0.1f, 1.0f, 0.4f, 1f);
|
private static final ColorRGBA COLOR_POLE_SELECTED = new ColorRGBA(1f, 0.35f, 0.05f, 1f);
|
||||||
|
private static final ColorRGBA COLOR_EDGE_SPLIT = new ColorRGBA(0.1f, 1.0f, 0.4f, 1f);
|
||||||
|
|
||||||
private static final ColorRGBA PILLAR_SUB = new ColorRGBA(0.2f, 0.85f, 1.0f, 1f);
|
private static final ColorRGBA PILLAR_SUB = new ColorRGBA(0.2f, 0.85f, 1.0f, 1f);
|
||||||
private static final ColorRGBA PILLAR_DRY = new ColorRGBA(1.0f, 0.50f, 0.1f, 1f);
|
private static final ColorRGBA PILLAR_DRY = new ColorRGBA(1.0f, 0.50f, 0.1f, 1f);
|
||||||
@@ -90,8 +91,9 @@ public class WaterBodyState extends BaseAppState {
|
|||||||
private final List<Geometry> handleGeos = new ArrayList<>();
|
private final List<Geometry> handleGeos = new ArrayList<>();
|
||||||
private final List<Material> handleMats = new ArrayList<>();
|
private final List<Material> handleMats = new ArrayList<>();
|
||||||
private final List<Float> handleCapY = new ArrayList<>();
|
private final List<Float> handleCapY = new ArrayList<>();
|
||||||
private int hoveredHandle = -1;
|
private int hoveredHandle = -1;
|
||||||
private int dragHandle = -1;
|
private int dragHandle = -1;
|
||||||
|
private int selectedHandle = -1;
|
||||||
private boolean isDragging = false;
|
private boolean isDragging = false;
|
||||||
private float[] liveXs = new float[0];
|
private float[] liveXs = new float[0];
|
||||||
private float[] liveYs = new float[0];
|
private float[] liveYs = new float[0];
|
||||||
@@ -187,7 +189,11 @@ public class WaterBodyState extends BaseAppState {
|
|||||||
|
|
||||||
if (input.deleteWaterRequested) {
|
if (input.deleteWaterRequested) {
|
||||||
input.deleteWaterRequested = false;
|
input.deleteWaterRequested = false;
|
||||||
if (selectedIdx >= 0) removeBody(selectedIdx);
|
if (selectedHandle >= 0 && selectedIdx >= 0 && selectedHandle < liveXs.length) {
|
||||||
|
deleteCornerPoint(selectedIdx, selectedHandle);
|
||||||
|
} else if (selectedIdx >= 0) {
|
||||||
|
removeBody(selectedIdx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedInput.WaterClick click;
|
SharedInput.WaterClick click;
|
||||||
@@ -235,6 +241,7 @@ public class WaterBodyState extends BaseAppState {
|
|||||||
PlacedWater b = bodies.get(selectedIdx);
|
PlacedWater b = bodies.get(selectedIdx);
|
||||||
int h = findHandleAt(jmeX, jmeY, b.pointsX(), b.pointsZ());
|
int h = findHandleAt(jmeX, jmeY, b.pointsX(), b.pointsZ());
|
||||||
if (h >= 0) {
|
if (h >= 0) {
|
||||||
|
setSelectedHandle(h);
|
||||||
startDrag(h, b.pointsX(), b.pointsY(), b.pointsZ());
|
startDrag(h, b.pointsX(), b.pointsY(), b.pointsZ());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -569,10 +576,50 @@ public class WaterBodyState extends BaseAppState {
|
|||||||
handleMats.clear();
|
handleMats.clear();
|
||||||
handleCapY.clear();
|
handleCapY.clear();
|
||||||
hideSplitIndicator();
|
hideSplitIndicator();
|
||||||
hoveredHandle = -1;
|
hoveredHandle = -1;
|
||||||
|
dragHandle = -1;
|
||||||
|
selectedHandle = -1;
|
||||||
splitHoverEdge = -1;
|
splitHoverEdge = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setSelectedHandle(int h) {
|
||||||
|
if (selectedHandle >= 0 && selectedHandle < handleMats.size() && selectedHandle != hoveredHandle) {
|
||||||
|
int n = liveXs.length;
|
||||||
|
handleMats.get(selectedHandle).setColor("Color", selectedHandle < n ? COLOR_POLE_CORNER : COLOR_POLE_EDGE);
|
||||||
|
}
|
||||||
|
selectedHandle = h;
|
||||||
|
if (h >= 0 && h < handleMats.size() && h != hoveredHandle) {
|
||||||
|
handleMats.get(h).setColor("Color", COLOR_POLE_SELECTED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteCornerPoint(int bodyIdx, int cornerIdx) {
|
||||||
|
int n = liveXs.length;
|
||||||
|
if (cornerIdx >= n) return; // Kantenmittelpunkte nicht löschbar
|
||||||
|
if (n <= 3) {
|
||||||
|
removeBody(bodyIdx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float[] newXs = new float[n - 1];
|
||||||
|
float[] newYs = new float[n - 1];
|
||||||
|
float[] newZs = new float[n - 1];
|
||||||
|
for (int i = 0, j = 0; i < n; i++) {
|
||||||
|
if (i == cornerIdx) continue;
|
||||||
|
newXs[j] = liveXs[i]; newYs[j] = liveYs[i]; newZs[j] = liveZs[i];
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
PlacedWater updated = bodies.get(bodyIdx).withPoints(newXs, newYs, newZs);
|
||||||
|
bodies.set(bodyIdx, updated);
|
||||||
|
rebuildQuadVisual(bodyIdx, newXs, newYs, newZs);
|
||||||
|
rebuildArrow(bodyIdx, updated);
|
||||||
|
liveXs = newXs.clone();
|
||||||
|
liveYs = newYs.clone();
|
||||||
|
liveZs = newZs.clone();
|
||||||
|
removeHandles();
|
||||||
|
buildHandles(newXs, newYs, newZs);
|
||||||
|
publishSelection(bodyIdx);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateHandleHover() {
|
private void updateHandleHover() {
|
||||||
if (selectedIdx < 0) return;
|
if (selectedIdx < 0) return;
|
||||||
PlacedWater b = bodies.get(selectedIdx);
|
PlacedWater b = bodies.get(selectedIdx);
|
||||||
@@ -585,7 +632,8 @@ public class WaterBodyState extends BaseAppState {
|
|||||||
|
|
||||||
if (h != hoveredHandle) {
|
if (h != hoveredHandle) {
|
||||||
if (hoveredHandle >= 0 && hoveredHandle < handleMats.size()) {
|
if (hoveredHandle >= 0 && hoveredHandle < handleMats.size()) {
|
||||||
ColorRGBA c = hoveredHandle < n ? COLOR_POLE_CORNER : COLOR_POLE_EDGE;
|
ColorRGBA c = (hoveredHandle == selectedHandle) ? COLOR_POLE_SELECTED
|
||||||
|
: (hoveredHandle < n ? COLOR_POLE_CORNER : COLOR_POLE_EDGE);
|
||||||
handleMats.get(hoveredHandle).setColor("Color", c);
|
handleMats.get(hoveredHandle).setColor("Color", c);
|
||||||
}
|
}
|
||||||
if (h >= 0 && h < handleMats.size()) {
|
if (h >= 0 && h < handleMats.size()) {
|
||||||
@@ -846,7 +894,7 @@ public class WaterBodyState extends BaseAppState {
|
|||||||
rebuildPoleMesh(mesh, x, z, waterH, terrH);
|
rebuildPoleMesh(mesh, x, z, waterH, terrH);
|
||||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
mat.setColor("Color", color.clone());
|
mat.setColor("Color", color.clone());
|
||||||
mat.getAdditionalRenderState().setLineWidth(2.5f);
|
mat.getAdditionalRenderState().setLineWidth(4f);
|
||||||
Geometry geo = new Geometry(name, mesh);
|
Geometry geo = new Geometry(name, mesh);
|
||||||
geo.setMaterial(mat);
|
geo.setMaterial(mat);
|
||||||
return geo;
|
return geo;
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package de.blight.editor.toolset;
|
||||||
|
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
|
||||||
|
/** Shared static UI helpers used by multiple tool controllers. */
|
||||||
|
public final class EditorUiHelper {
|
||||||
|
|
||||||
|
private EditorUiHelper() {}
|
||||||
|
|
||||||
|
public static Label bold(String text) {
|
||||||
|
Label l = new Label(text);
|
||||||
|
l.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Label styledHint(String text) {
|
||||||
|
Label l = new Label(text);
|
||||||
|
l.setStyle("-fx-text-fill: #333333; -fx-font-size: 11;");
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Label sectionTitle(String text) {
|
||||||
|
Label l = new Label(text);
|
||||||
|
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #1a3a6a;");
|
||||||
|
VBox.setMargin(l, new Insets(8, 0, 0, 0));
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HBox withField(Slider slider, String fmt) {
|
||||||
|
slider.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
TextField tf = new TextField(String.format(fmt, slider.getValue()));
|
||||||
|
tf.setPrefWidth(62);
|
||||||
|
tf.setMaxWidth(62);
|
||||||
|
tf.setStyle("-fx-font-size: 11;");
|
||||||
|
slider.valueProperty().addListener((o, ov, nv) ->
|
||||||
|
tf.setText(String.format(fmt, nv.doubleValue())));
|
||||||
|
Runnable commit = () -> {
|
||||||
|
try {
|
||||||
|
double v = Math.max(slider.getMin(), Math.min(slider.getMax(),
|
||||||
|
Double.parseDouble(tf.getText().replace(',', '.'))));
|
||||||
|
slider.setValue(v);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
tf.setText(String.format(fmt, slider.getValue()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tf.setOnAction(e -> commit.run());
|
||||||
|
tf.focusedProperty().addListener((o, ov, focused) -> { if (!focused) commit.run(); });
|
||||||
|
HBox row = new HBox(5, slider, tf);
|
||||||
|
HBox.setHgrow(slider, Priority.ALWAYS);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VBox paramSlider(String label, double min, double max, double init,
|
||||||
|
java.util.function.Consumer<Float> setter) {
|
||||||
|
Label name = new Label(label);
|
||||||
|
name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
|
||||||
|
Slider slider = new Slider(min, max, init);
|
||||||
|
slider.setShowTickMarks(true);
|
||||||
|
slider.setShowTickLabels(true);
|
||||||
|
slider.setMajorTickUnit((max - min) / 2.0);
|
||||||
|
slider.valueProperty().addListener((o, a, b) -> setter.accept(b.floatValue()));
|
||||||
|
return new VBox(3, name, withField(slider, "%.3f"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VBox ezFloat(String label, double min, double max, double init,
|
||||||
|
java.util.function.Consumer<Float> setter) {
|
||||||
|
Label name = new Label(label);
|
||||||
|
name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
|
||||||
|
Slider slider = new Slider(min, max, Math.max(min, Math.min(max, init)));
|
||||||
|
slider.setShowTickMarks(false);
|
||||||
|
slider.valueProperty().addListener((o, a, b) -> setter.accept(b.floatValue()));
|
||||||
|
return new VBox(2, name, withField(slider, "%.3f"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Spinner<Integer> intSpinner(int min, int max, int init) {
|
||||||
|
Spinner<Integer> sp = new Spinner<>(min, max, Math.max(min, Math.min(max, init)));
|
||||||
|
sp.setEditable(true);
|
||||||
|
sp.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
return sp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String pathToName(String path, String[] paths, String[] names) {
|
||||||
|
if (path == null) return names[0];
|
||||||
|
for (int i = 0; i < paths.length; i++) {
|
||||||
|
if (path.equals(paths[i])) return names[i];
|
||||||
|
}
|
||||||
|
return names[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package de.blight.editor.toolset;
|
||||||
|
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.Slider;
|
||||||
|
import javafx.scene.control.Spinner;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public final class UiHelper {
|
||||||
|
|
||||||
|
private UiHelper() {}
|
||||||
|
|
||||||
|
public static Label bold(String text) {
|
||||||
|
Label l = new Label(text);
|
||||||
|
l.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Label styledHint(String text) {
|
||||||
|
Label l = new Label(text);
|
||||||
|
l.setStyle("-fx-text-fill: #333333; -fx-font-size: 11;");
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Label sectionTitle(String text) {
|
||||||
|
Label l = new Label(text);
|
||||||
|
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #1a3a6a;");
|
||||||
|
VBox.setMargin(l, new Insets(8, 0, 0, 0));
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HBox withField(Slider slider, String fmt) {
|
||||||
|
slider.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
TextField tf = new TextField(String.format(fmt, slider.getValue()));
|
||||||
|
tf.setPrefWidth(62);
|
||||||
|
tf.setMaxWidth(62);
|
||||||
|
tf.setStyle("-fx-font-size: 11;");
|
||||||
|
slider.valueProperty().addListener((o, ov, nv) ->
|
||||||
|
tf.setText(String.format(fmt, nv.doubleValue())));
|
||||||
|
Runnable commit = () -> {
|
||||||
|
try {
|
||||||
|
double v = Math.max(slider.getMin(), Math.min(slider.getMax(),
|
||||||
|
Double.parseDouble(tf.getText().replace(',', '.'))));
|
||||||
|
slider.setValue(v);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
tf.setText(String.format(fmt, slider.getValue()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tf.setOnAction(e -> commit.run());
|
||||||
|
tf.focusedProperty().addListener((o, ov, focused) -> { if (!focused) commit.run(); });
|
||||||
|
HBox row = new HBox(5, slider, tf);
|
||||||
|
HBox.setHgrow(slider, Priority.ALWAYS);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VBox paramSlider(String label, double min, double max, double init,
|
||||||
|
Consumer<Float> setter) {
|
||||||
|
Label name = new Label(label);
|
||||||
|
name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
|
||||||
|
Slider slider = new Slider(min, max, init);
|
||||||
|
slider.setShowTickMarks(true);
|
||||||
|
slider.setShowTickLabels(true);
|
||||||
|
slider.setMajorTickUnit((max - min) / 2.0);
|
||||||
|
slider.valueProperty().addListener((o, a, b) -> setter.accept(b.floatValue()));
|
||||||
|
return new VBox(3, name, withField(slider, "%.3f"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Spinner<Integer> intSpinner(int min, int max, int init) {
|
||||||
|
Spinner<Integer> sp = new Spinner<>(min, max, Math.max(min, Math.min(max, init)));
|
||||||
|
sp.setEditable(true);
|
||||||
|
sp.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
return sp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VBox ezFloat(String label, double min, double max, double init,
|
||||||
|
Consumer<Float> setter) {
|
||||||
|
Label name = new Label(label);
|
||||||
|
name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
|
||||||
|
Slider slider = new Slider(min, max, Math.max(min, Math.min(max, init)));
|
||||||
|
slider.setShowTickMarks(false);
|
||||||
|
slider.valueProperty().addListener((o, a, b) -> setter.accept(b.floatValue()));
|
||||||
|
return new VBox(2, name, withField(slider, "%.3f"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package de.blight.editor.toolset.anim;
|
||||||
|
|
||||||
|
import de.blight.editor.ToolContext;
|
||||||
|
import de.blight.editor.ToolController;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Orientation;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
|
||||||
|
import static de.blight.editor.toolset.UiHelper.*;
|
||||||
|
|
||||||
|
public class AnimPreviewController implements ToolController {
|
||||||
|
|
||||||
|
private final ToolContext ctx;
|
||||||
|
|
||||||
|
private ImageView animPreviewView;
|
||||||
|
private ListView<String> animClipListView;
|
||||||
|
private Label animPreviewStatusLabel;
|
||||||
|
private ComboBox<String> animPreviewModelCombo; // immer null — dead-code-Stub, null-Guards schützen
|
||||||
|
private String animCurrentModelPath = null;
|
||||||
|
private double animPrevDragX, animPrevDragY;
|
||||||
|
|
||||||
|
public AnimPreviewController(ToolContext ctx) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toolId() { return "animpreview"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void activate() {
|
||||||
|
ctx.input().animPreviewSequenceQueue.clear();
|
||||||
|
|
||||||
|
animPreviewView = new ImageView(ctx.input().animPreviewImage);
|
||||||
|
animPreviewView.setPreserveRatio(true);
|
||||||
|
animPreviewView.setSmooth(true);
|
||||||
|
|
||||||
|
StackPane previewPane = new StackPane(animPreviewView);
|
||||||
|
previewPane.setStyle("-fx-background-color: #2e2e38;");
|
||||||
|
|
||||||
|
previewPane.widthProperty().addListener((obs, ov, nv) -> {
|
||||||
|
animPreviewView.setFitWidth(nv.doubleValue());
|
||||||
|
ctx.input().animPreviewW = Math.max(64, nv.intValue());
|
||||||
|
});
|
||||||
|
previewPane.heightProperty().addListener((obs, ov, nv) -> {
|
||||||
|
animPreviewView.setFitHeight(nv.doubleValue());
|
||||||
|
ctx.input().animPreviewH = Math.max(64, nv.intValue());
|
||||||
|
});
|
||||||
|
previewPane.setOnMousePressed(e -> {
|
||||||
|
animPrevDragX = e.getSceneX();
|
||||||
|
animPrevDragY = e.getSceneY();
|
||||||
|
});
|
||||||
|
previewPane.setOnMouseDragged(e -> {
|
||||||
|
double dx = e.getSceneX() - animPrevDragX;
|
||||||
|
double dy = e.getSceneY() - animPrevDragY;
|
||||||
|
animPrevDragX = e.getSceneX();
|
||||||
|
animPrevDragY = e.getSceneY();
|
||||||
|
ctx.input().animPreviewRotY += (float) (dx * 0.5);
|
||||||
|
ctx.input().animPreviewRotX = (float) Math.max(-80, Math.min(80,
|
||||||
|
ctx.input().animPreviewRotX - dy * 0.3));
|
||||||
|
});
|
||||||
|
previewPane.setOnScroll(e -> {
|
||||||
|
double factor = e.getDeltaY() > 0 ? 0.9 : 1.1;
|
||||||
|
ctx.input().animPreviewZoom = (float) Math.max(0.05,
|
||||||
|
Math.min(20.0, ctx.input().animPreviewZoom * factor));
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.setCenterView().accept(previewPane);
|
||||||
|
ctx.setMainToolbar().accept(buildAnimPreviewToolBar());
|
||||||
|
ctx.setRightPanel().accept(buildAnimPreviewPanel());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deactivate() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFrame() {
|
||||||
|
if (ctx.input().animPreviewResized) {
|
||||||
|
ctx.input().animPreviewResized = false;
|
||||||
|
if (animPreviewView != null) {
|
||||||
|
animPreviewView.setImage(ctx.input().animPreviewImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
java.util.List<String> newClips = ctx.input().animPreviewClips.getAndSet(null);
|
||||||
|
if (newClips != null && animClipListView != null) {
|
||||||
|
animClipListView.getItems().setAll(newClips);
|
||||||
|
if (!newClips.isEmpty()) animClipListView.getSelectionModel().selectFirst();
|
||||||
|
}
|
||||||
|
String loadedPath = ctx.input().animPreviewLoadedPath.getAndSet(null);
|
||||||
|
if (loadedPath != null) {
|
||||||
|
animCurrentModelPath = loadedPath;
|
||||||
|
}
|
||||||
|
String animStatus = ctx.input().animPreviewStatus;
|
||||||
|
if (animStatus != null) {
|
||||||
|
ctx.input().animPreviewStatus = null;
|
||||||
|
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText(animStatus);
|
||||||
|
}
|
||||||
|
String animOp = ctx.input().animOpStatus;
|
||||||
|
if (animOp != null) {
|
||||||
|
ctx.input().animOpStatus = null;
|
||||||
|
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText(animOp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wird vom EditorApp-Baum-Handler aufgerufen, wenn ein Modell angeklickt wird. */
|
||||||
|
public void setStatusText(String text) {
|
||||||
|
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wird vom EditorApp-Baum-Handler aufgerufen, bevor ein neues Modell geladen wird. */
|
||||||
|
public void clearClipList() {
|
||||||
|
if (animClipListView != null) animClipListView.getItems().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ToolBar buildAnimPreviewToolBar() {
|
||||||
|
Button backBtn = new Button("← Welteneditor");
|
||||||
|
backBtn.setOnAction(e -> ctx.switchToWorld().run());
|
||||||
|
Label label = new Label("Animations Viewer");
|
||||||
|
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||||
|
ToolBar tb = new ToolBar();
|
||||||
|
tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label);
|
||||||
|
return tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox buildAnimPreviewPanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
// ── Animationen ────────────────────────────────────────────────────────
|
||||||
|
inner.getChildren().addAll(sectionTitle("Animationen"), new Separator());
|
||||||
|
animClipListView = new ListView<>();
|
||||||
|
animClipListView.setPrefHeight(200);
|
||||||
|
animClipListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||||
|
|
||||||
|
Button playBtn = new Button("▶ Abspielen");
|
||||||
|
Button stopBtn = new Button("■ Stop");
|
||||||
|
playBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
stopBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
playBtn.setStyle("-fx-font-weight: bold;");
|
||||||
|
playBtn.setOnAction(e -> {
|
||||||
|
String clip = animClipListView.getSelectionModel().getSelectedItem();
|
||||||
|
if (clip != null) ctx.input().animPreviewPlayClip = clip;
|
||||||
|
});
|
||||||
|
stopBtn.setOnAction(e -> ctx.input().animPreviewPlayClip = "");
|
||||||
|
animClipListView.setOnMouseClicked(e -> {
|
||||||
|
if (e.getClickCount() == 2) {
|
||||||
|
String clip = animClipListView.getSelectionModel().getSelectedItem();
|
||||||
|
if (clip != null) ctx.input().animPreviewPlayClip = clip;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
HBox btnRow = new HBox(6, playBtn, stopBtn);
|
||||||
|
HBox.setHgrow(playBtn, Priority.ALWAYS);
|
||||||
|
HBox.setHgrow(stopBtn, Priority.ALWAYS);
|
||||||
|
inner.getChildren().addAll(animClipListView, btnRow);
|
||||||
|
|
||||||
|
// ── Steuerung ──────────────────────────────────────────────────────────
|
||||||
|
inner.getChildren().addAll(new Separator(), sectionTitle("Steuerung"), new Separator());
|
||||||
|
|
||||||
|
CheckBox loopCB = new CheckBox("Loop");
|
||||||
|
loopCB.setSelected(true);
|
||||||
|
loopCB.setOnAction(e -> ctx.input().animPreviewLoop = loopCB.isSelected());
|
||||||
|
|
||||||
|
Label speedLbl = new Label("Geschwindigkeit");
|
||||||
|
Slider speedSlider = new Slider(0.1, 3.0, 1.0);
|
||||||
|
speedSlider.setShowTickMarks(true);
|
||||||
|
speedSlider.setShowTickLabels(true);
|
||||||
|
speedSlider.setMajorTickUnit(1.0);
|
||||||
|
speedSlider.valueProperty().addListener((obs, ov, nv) ->
|
||||||
|
ctx.input().animPreviewSpeed = nv.floatValue());
|
||||||
|
inner.getChildren().addAll(loopCB, speedLbl, withField(speedSlider, "%.2f"));
|
||||||
|
|
||||||
|
// ── Status ─────────────────────────────────────────────────────────────
|
||||||
|
inner.getChildren().add(new Separator());
|
||||||
|
animPreviewStatusLabel = new Label("");
|
||||||
|
animPreviewStatusLabel.setWrapText(true);
|
||||||
|
animPreviewStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #c00;");
|
||||||
|
inner.getChildren().add(animPreviewStatusLabel);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,364 @@
|
|||||||
|
package de.blight.editor.toolset.eztree;
|
||||||
|
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
import de.blight.editor.ToolContext;
|
||||||
|
import de.blight.editor.ToolController;
|
||||||
|
import de.blight.eztree.Billboard;
|
||||||
|
import de.blight.eztree.TreeOptions;
|
||||||
|
import de.blight.eztree.TreePresets;
|
||||||
|
import de.blight.eztree.TreeType;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Orientation;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ChoiceBox;
|
||||||
|
import javafx.scene.control.ComboBox;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.Separator;
|
||||||
|
import javafx.scene.control.Spinner;
|
||||||
|
import javafx.scene.control.ToolBar;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
import static de.blight.editor.toolset.UiHelper.*;
|
||||||
|
|
||||||
|
public class EzTreeController implements ToolController {
|
||||||
|
|
||||||
|
private final ToolContext ctx;
|
||||||
|
|
||||||
|
// ── Zustand ───────────────────────────────────────────────────────────────
|
||||||
|
private TreeOptions ezTreeOptions = TreePresets.oakMedium();
|
||||||
|
private String ezTreePresetName = "Oak Medium";
|
||||||
|
|
||||||
|
private Runnable onF5 = () -> {};
|
||||||
|
private Runnable onF6 = () -> {};
|
||||||
|
|
||||||
|
// ── Preview-Panel (lazy) ──────────────────────────────────────────────────
|
||||||
|
private ImageView treePreviewView;
|
||||||
|
private StackPane plantPreviewPanel;
|
||||||
|
|
||||||
|
public EzTreeController(ToolContext ctx) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toolId() { return "eztree"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void activate() {
|
||||||
|
onF5 = () -> {
|
||||||
|
ctx.input().ezTreeGenQueue.offer(new SharedInput.EzTreeGenRequest(
|
||||||
|
ezTreeOptions.copy(), ezTreePresetName, false));
|
||||||
|
ctx.setStatus().accept("EZ-Tree: generiere Vorschau…");
|
||||||
|
};
|
||||||
|
ctx.setMainToolbar().accept(buildToolBar());
|
||||||
|
ctx.setCenterView().accept(getPlantPreviewPanel());
|
||||||
|
ctx.setRightPanel().accept(buildEzTreeParamsPanel());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deactivate() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPreview() { onF5.run(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRandomSeed() { onF6.run(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFrame() {
|
||||||
|
SharedInput input = ctx.input();
|
||||||
|
String treeMsg = input.treeGenStatusMsg;
|
||||||
|
if (treeMsg != null) {
|
||||||
|
input.treeGenStatusMsg = null;
|
||||||
|
ctx.setStatus().accept(treeMsg);
|
||||||
|
}
|
||||||
|
if (input.treePreviewResized) {
|
||||||
|
input.treePreviewResized = false;
|
||||||
|
if (treePreviewView != null) {
|
||||||
|
treePreviewView.setImage(input.treePreviewImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Vorschau-Panel ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private StackPane getPlantPreviewPanel() {
|
||||||
|
if (plantPreviewPanel != null) { return plantPreviewPanel; }
|
||||||
|
|
||||||
|
SharedInput input = ctx.input();
|
||||||
|
treePreviewView = new ImageView(input.treePreviewImage);
|
||||||
|
treePreviewView.setPreserveRatio(true);
|
||||||
|
treePreviewView.setSmooth(true);
|
||||||
|
|
||||||
|
StackPane pane = new StackPane(treePreviewView);
|
||||||
|
pane.setStyle("-fx-background-color: #1e2433;");
|
||||||
|
pane.widthProperty().addListener((o, ow, nw) -> {
|
||||||
|
treePreviewView.setFitWidth(nw.doubleValue());
|
||||||
|
input.treePreviewW = Math.max(64, nw.intValue());
|
||||||
|
});
|
||||||
|
pane.heightProperty().addListener((o, oh, nh) -> {
|
||||||
|
treePreviewView.setFitHeight(nh.doubleValue());
|
||||||
|
input.treePreviewH = Math.max(64, nh.intValue());
|
||||||
|
});
|
||||||
|
|
||||||
|
double[] prevMX = {0}, prevMY = {0};
|
||||||
|
pane.setOnMousePressed(e -> {
|
||||||
|
boolean mid = e.getButton() == MouseButton.MIDDLE;
|
||||||
|
boolean both = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
|
||||||
|
if (mid || both) { prevMX[0] = e.getX(); prevMY[0] = e.getY(); }
|
||||||
|
});
|
||||||
|
pane.setOnMouseDragged(e -> {
|
||||||
|
boolean both = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
|
||||||
|
if (e.isMiddleButtonDown() || both) {
|
||||||
|
double dx = e.getX() - prevMX[0];
|
||||||
|
double dy = e.getY() - prevMY[0];
|
||||||
|
input.treePreviewRotY += (float) (dx * 0.4);
|
||||||
|
input.treePreviewRotX = (float) Math.max(5.0, Math.min(80.0,
|
||||||
|
input.treePreviewRotX - dy * 0.3));
|
||||||
|
prevMX[0] = e.getX(); prevMY[0] = e.getY();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pane.setOnScroll(e -> {
|
||||||
|
if (e.getDeltaY() == 0) { return; }
|
||||||
|
float factor = e.getDeltaY() > 0 ? 0.88f : 1.14f;
|
||||||
|
input.treePreviewZoom = (float) Math.max(0.25, Math.min(4.0,
|
||||||
|
input.treePreviewZoom * factor));
|
||||||
|
e.consume();
|
||||||
|
});
|
||||||
|
pane.setFocusTraversable(true);
|
||||||
|
pane.setOnMouseClicked(e -> pane.requestFocus());
|
||||||
|
pane.setOnKeyPressed(e -> {
|
||||||
|
if (e.getCode() == javafx.scene.input.KeyCode.W) {
|
||||||
|
input.treePreviewWireframe = !input.treePreviewWireframe;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
plantPreviewPanel = pane;
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toolbar ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private ToolBar buildToolBar() {
|
||||||
|
Label presetLabel = new Label("Preset:");
|
||||||
|
presetLabel.setStyle("-fx-font-weight: bold;");
|
||||||
|
ComboBox<String> presetBox = new ComboBox<>();
|
||||||
|
presetBox.getItems().addAll(TreePresets.presetNames());
|
||||||
|
presetBox.setValue(ezTreePresetName);
|
||||||
|
presetBox.setOnAction(e -> {
|
||||||
|
ezTreePresetName = presetBox.getValue();
|
||||||
|
ezTreeOptions = TreePresets.byName(ezTreePresetName);
|
||||||
|
ctx.setRightPanel().accept(buildEzTreeParamsPanel());
|
||||||
|
});
|
||||||
|
|
||||||
|
Button previewBtn = new Button("▶ Vorschau [F5]");
|
||||||
|
previewBtn.setStyle("-fx-font-weight: bold;");
|
||||||
|
previewBtn.setOnAction(e -> onF5.run());
|
||||||
|
|
||||||
|
Button exportBtn = new Button("💾 Export .j3o");
|
||||||
|
exportBtn.setOnAction(e -> {
|
||||||
|
ctx.input().ezTreeGenQueue.offer(new SharedInput.EzTreeGenRequest(
|
||||||
|
ezTreeOptions.copy(), ezTreePresetName, true));
|
||||||
|
ctx.setStatus().accept("EZ-Tree: generiere und exportiere…");
|
||||||
|
});
|
||||||
|
|
||||||
|
Button backBtn = new Button("← Welteneditor");
|
||||||
|
backBtn.setOnAction(e -> ctx.switchToWorld().run());
|
||||||
|
|
||||||
|
ToolBar bar = new ToolBar();
|
||||||
|
bar.getItems().addAll(
|
||||||
|
backBtn, new Separator(Orientation.VERTICAL),
|
||||||
|
presetLabel, presetBox,
|
||||||
|
new Separator(Orientation.VERTICAL),
|
||||||
|
previewBtn, exportBtn
|
||||||
|
);
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── EzTree-Parameter-Panel ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private VBox buildEzTreeParamsPanel() {
|
||||||
|
VBox inner = new VBox(6);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
|
||||||
|
|
||||||
|
inner.getChildren().add(bold("Zufallssamen:"));
|
||||||
|
Spinner<Integer> seedSp = intSpinner(0, 999999, ezTreeOptions.seed);
|
||||||
|
seedSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.seed = b);
|
||||||
|
Button ezRndSeed = new Button("🎲");
|
||||||
|
onF6 = () -> {
|
||||||
|
int s = new java.util.Random().nextInt(1000000);
|
||||||
|
ezTreeOptions.seed = s;
|
||||||
|
seedSp.getValueFactory().setValue(s);
|
||||||
|
onF5.run();
|
||||||
|
};
|
||||||
|
ezRndSeed.setOnAction(e -> onF6.run());
|
||||||
|
HBox ezSeedRow = new HBox(4, seedSp, ezRndSeed);
|
||||||
|
HBox.setHgrow(seedSp, Priority.ALWAYS);
|
||||||
|
inner.getChildren().add(ezSeedRow);
|
||||||
|
|
||||||
|
inner.getChildren().add(bold("Typ:"));
|
||||||
|
ChoiceBox<String> typeCB = new ChoiceBox<>();
|
||||||
|
typeCB.getItems().addAll("Laubbaum", "Immergrün");
|
||||||
|
typeCB.setValue(ezTreeOptions.type == TreeType.EVERGREEN ? "Immergrün" : "Laubbaum");
|
||||||
|
typeCB.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
typeCB.setOnAction(e -> ezTreeOptions.type =
|
||||||
|
"Immergrün".equals(typeCB.getValue()) ? TreeType.EVERGREEN : TreeType.DECIDUOUS);
|
||||||
|
inner.getChildren().add(typeCB);
|
||||||
|
|
||||||
|
inner.getChildren().add(bold("Ast-Ebenen:"));
|
||||||
|
Spinner<Integer> levelsSp = intSpinner(1, 5, ezTreeOptions.branch.levels);
|
||||||
|
levelsSp.valueProperty().addListener((o, a, b) -> {
|
||||||
|
ezTreeOptions.branch.levels = b;
|
||||||
|
ctx.setRightPanel().accept(buildEzTreeParamsPanel());
|
||||||
|
});
|
||||||
|
inner.getChildren().add(levelsSp);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Rinde"), new Separator());
|
||||||
|
inner.getChildren().add(ezFloat("UV-Skala X:", 0.1, 5.0, ezTreeOptions.bark.textureScaleX,
|
||||||
|
v -> ezTreeOptions.bark.textureScaleX = v));
|
||||||
|
inner.getChildren().add(ezFloat("UV-Skala Y:", 0.1, 5.0, ezTreeOptions.bark.textureScaleY,
|
||||||
|
v -> ezTreeOptions.bark.textureScaleY = v));
|
||||||
|
inner.getChildren().add(ezFloat("Farbe R:", 0, 1, ezTreeOptions.bark.r,
|
||||||
|
v -> ezTreeOptions.bark.r = v));
|
||||||
|
inner.getChildren().add(ezFloat("Farbe G:", 0, 1, ezTreeOptions.bark.g,
|
||||||
|
v -> ezTreeOptions.bark.g = v));
|
||||||
|
inner.getChildren().add(ezFloat("Farbe B:", 0, 1, ezTreeOptions.bark.b,
|
||||||
|
v -> ezTreeOptions.bark.b = v));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Wachstumskraft"), new Separator());
|
||||||
|
inner.getChildren().add(ezFloat("Richtung X:", -1, 1, ezTreeOptions.branch.force.direction.x,
|
||||||
|
v -> ezTreeOptions.branch.force.direction.x = v));
|
||||||
|
inner.getChildren().add(ezFloat("Richtung Y:", -1, 1, ezTreeOptions.branch.force.direction.y,
|
||||||
|
v -> ezTreeOptions.branch.force.direction.y = v));
|
||||||
|
inner.getChildren().add(ezFloat("Richtung Z:", -1, 1, ezTreeOptions.branch.force.direction.z,
|
||||||
|
v -> ezTreeOptions.branch.force.direction.z = v));
|
||||||
|
inner.getChildren().add(ezFloat("Stärke:", 0, 0.2, ezTreeOptions.branch.force.strength,
|
||||||
|
v -> ezTreeOptions.branch.force.strength = v));
|
||||||
|
|
||||||
|
int maxLevel = ezTreeOptions.branch.levels;
|
||||||
|
for (int lv = 0; lv <= maxLevel; lv++) {
|
||||||
|
buildLevelSection(inner, lv, maxLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Blätter"), new Separator());
|
||||||
|
inner.getChildren().add(bold("Billboard:"));
|
||||||
|
ChoiceBox<String> billCB = new ChoiceBox<>();
|
||||||
|
billCB.getItems().addAll("Einfach", "Kreuz");
|
||||||
|
billCB.setValue(ezTreeOptions.leaves.billboard == Billboard.CROSS ? "Kreuz" : "Einfach");
|
||||||
|
billCB.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
billCB.setOnAction(e -> ezTreeOptions.leaves.billboard =
|
||||||
|
"Kreuz".equals(billCB.getValue()) ? Billboard.CROSS : Billboard.NONE);
|
||||||
|
inner.getChildren().add(billCB);
|
||||||
|
|
||||||
|
inner.getChildren().add(bold("Anzahl:"));
|
||||||
|
Spinner<Integer> leafCountSp = intSpinner(0, 60, ezTreeOptions.leaves.count);
|
||||||
|
leafCountSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.leaves.count = b);
|
||||||
|
inner.getChildren().add(leafCountSp);
|
||||||
|
|
||||||
|
inner.getChildren().add(ezFloat("Start-Position:", 0, 1, ezTreeOptions.leaves.start,
|
||||||
|
v -> ezTreeOptions.leaves.start = v));
|
||||||
|
inner.getChildren().add(ezFloat("Größe:", 0.05, 4, ezTreeOptions.leaves.size,
|
||||||
|
v -> ezTreeOptions.leaves.size = v));
|
||||||
|
inner.getChildren().add(ezFloat("Größen-Varianz:", 0, 1, ezTreeOptions.leaves.sizeVariance,
|
||||||
|
v -> ezTreeOptions.leaves.sizeVariance = v));
|
||||||
|
inner.getChildren().add(ezFloat("Winkel (°):", 0, 90, ezTreeOptions.leaves.angle,
|
||||||
|
v -> ezTreeOptions.leaves.angle = v));
|
||||||
|
inner.getChildren().add(ezFloat("Alpha-Schwellwert:", 0, 1, ezTreeOptions.leaves.alphaTest,
|
||||||
|
v -> ezTreeOptions.leaves.alphaTest = v));
|
||||||
|
inner.getChildren().add(ezFloat("Farbe R:", 0, 1, ezTreeOptions.leaves.r,
|
||||||
|
v -> ezTreeOptions.leaves.r = v));
|
||||||
|
inner.getChildren().add(ezFloat("Farbe G:", 0, 1, ezTreeOptions.leaves.g,
|
||||||
|
v -> ezTreeOptions.leaves.g = v));
|
||||||
|
inner.getChildren().add(ezFloat("Farbe B:", 0, 1, ezTreeOptions.leaves.b,
|
||||||
|
v -> ezTreeOptions.leaves.b = v));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Spalier (Trellis)"), new Separator());
|
||||||
|
javafx.scene.control.CheckBox trellisEnCB = new javafx.scene.control.CheckBox("Aktiviert");
|
||||||
|
trellisEnCB.setSelected(ezTreeOptions.trellis.enabled);
|
||||||
|
trellisEnCB.setOnAction(e -> ezTreeOptions.trellis.enabled = trellisEnCB.isSelected());
|
||||||
|
inner.getChildren().add(trellisEnCB);
|
||||||
|
|
||||||
|
inner.getChildren().add(bold("Streben:"));
|
||||||
|
Spinner<Integer> trellisSecSp = intSpinner(3, 12, ezTreeOptions.trellis.sections);
|
||||||
|
trellisSecSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.trellis.sections = b);
|
||||||
|
inner.getChildren().add(trellisSecSp);
|
||||||
|
|
||||||
|
inner.getChildren().add(ezFloat("Radius:", 0.1, 8, ezTreeOptions.trellis.radius,
|
||||||
|
v -> ezTreeOptions.trellis.radius = v));
|
||||||
|
inner.getChildren().add(ezFloat("Höhe:", 0.5, 15, ezTreeOptions.trellis.length,
|
||||||
|
v -> ezTreeOptions.trellis.length = v));
|
||||||
|
inner.getChildren().add(ezFloat("Streben-Radius:", 0.01, 0.3, ezTreeOptions.trellis.memberRadius,
|
||||||
|
v -> ezTreeOptions.trellis.memberRadius = v));
|
||||||
|
|
||||||
|
inner.getChildren().add(bold("Querstreben:"));
|
||||||
|
Spinner<Integer> crossSp = intSpinner(1, 8, ezTreeOptions.trellis.crossMembers);
|
||||||
|
crossSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.trellis.crossMembers = b);
|
||||||
|
inner.getChildren().add(crossSp);
|
||||||
|
|
||||||
|
ScrollPane scroll = new ScrollPane(inner);
|
||||||
|
scroll.setFitToWidth(true);
|
||||||
|
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||||
|
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
|
||||||
|
VBox panel = new VBox(scroll);
|
||||||
|
VBox.setVgrow(scroll, Priority.ALWAYS);
|
||||||
|
panel.setPrefWidth(270);
|
||||||
|
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildLevelSection(VBox container, int lv, int maxLevel) {
|
||||||
|
String title = lv == 0 ? "Stamm (Ebene 0)" : "Ast-Ebene " + lv;
|
||||||
|
container.getChildren().addAll(sectionTitle(title), new Separator());
|
||||||
|
|
||||||
|
var bo = ezTreeOptions.branch;
|
||||||
|
|
||||||
|
if (lv > 0) {
|
||||||
|
container.getChildren().add(ezFloat("Winkel (°):", 0, 90,
|
||||||
|
bo.getAngle(lv), v -> bo.angle.put(lv, v)));
|
||||||
|
container.getChildren().add(ezFloat("Start-Position:", 0, 1,
|
||||||
|
bo.getStart(lv), v -> bo.start.put(lv, v)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lv < maxLevel) {
|
||||||
|
container.getChildren().add(bold("Kinder:"));
|
||||||
|
Spinner<Integer> childSp = intSpinner(0, 20, bo.getChildren(lv));
|
||||||
|
childSp.valueProperty().addListener((o, a, b) -> bo.children.put(lv, b));
|
||||||
|
container.getChildren().add(childSp);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.getChildren().add(ezFloat("Gnarliness:", 0, 1.5,
|
||||||
|
bo.getGnarliness(lv), v -> bo.gnarliness.put(lv, v)));
|
||||||
|
|
||||||
|
String lenLabel = lv == 0 ? "Länge (absolut):" : "Länge (Faktor):";
|
||||||
|
double lenMax = lv == 0 ? 60 : 2;
|
||||||
|
container.getChildren().add(ezFloat(lenLabel, 0.1, lenMax,
|
||||||
|
bo.getLength(lv), v -> bo.length.put(lv, v)));
|
||||||
|
|
||||||
|
String radLabel = lv == 0 ? "Radius (absolut):" : "Radius (Faktor):";
|
||||||
|
double radMax = lv == 0 ? 3 : 1;
|
||||||
|
container.getChildren().add(ezFloat(radLabel, 0.01, radMax,
|
||||||
|
bo.getRadius(lv), v -> bo.radius.put(lv, v)));
|
||||||
|
|
||||||
|
container.getChildren().add(bold("Sektionen:"));
|
||||||
|
Spinner<Integer> secSp = intSpinner(2, 24, bo.getSections(lv));
|
||||||
|
secSp.valueProperty().addListener((o, a, b) -> bo.sections.put(lv, b));
|
||||||
|
container.getChildren().add(secSp);
|
||||||
|
|
||||||
|
container.getChildren().add(bold("Segmente (Polygon):"));
|
||||||
|
Spinner<Integer> segSp = intSpinner(3, 12, bo.getSegments(lv));
|
||||||
|
segSp.valueProperty().addListener((o, a, b) -> bo.segments.put(lv, b));
|
||||||
|
container.getChildren().add(segSp);
|
||||||
|
|
||||||
|
container.getChildren().add(ezFloat("Verjüngung:", 0, 1,
|
||||||
|
bo.getTaper(lv), v -> bo.taper.put(lv, v)));
|
||||||
|
container.getChildren().add(ezFloat("Drall (°/Sektion):", -180, 180,
|
||||||
|
bo.getTwist(lv), v -> bo.twist.put(lv, v)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
package de.blight.editor.toolset.game;
|
||||||
|
|
||||||
|
import de.blight.editor.ProjectRoot;
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
import de.blight.editor.ToolContext;
|
||||||
|
import de.blight.editor.toolset.EditorUiHelper;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
|
||||||
|
public class GameController {
|
||||||
|
|
||||||
|
private final ToolContext ctx;
|
||||||
|
|
||||||
|
// Game launch state
|
||||||
|
private boolean launchGameAfterSave = false;
|
||||||
|
private boolean pendingNewGame = false;
|
||||||
|
private boolean pendingAutostart = true;
|
||||||
|
|
||||||
|
// Play panel UI
|
||||||
|
private Button gamePlayBtn;
|
||||||
|
private Button gameNewBtn;
|
||||||
|
private Button gameFullBtn;
|
||||||
|
private CheckBox debugConsoleCB;
|
||||||
|
private Label tempSpawnCoordsLabel;
|
||||||
|
private Label permSpawnCoordsLabel;
|
||||||
|
|
||||||
|
// Game console
|
||||||
|
private Stage gameConsoleStage;
|
||||||
|
private TextArea gameConsoleArea;
|
||||||
|
private final ConcurrentLinkedQueue<String> consoleBuffer = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
public GameController(ToolContext ctx) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Play-Tool-Panel ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public VBox buildPlayToolPanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(EditorUiHelper.sectionTitle("Spawnpunkte"), new Separator());
|
||||||
|
|
||||||
|
Button setTempBtn = new Button("Temp. Spawn hier setzen");
|
||||||
|
setTempBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
setTempBtn.setTooltip(new Tooltip(
|
||||||
|
"Aktiviert Klick-Modus im Viewport zum Setzen des temporären Spawnpunkts"));
|
||||||
|
setTempBtn.setOnAction(e -> {
|
||||||
|
ctx.input().playToolMode = SharedInput.PlayToolMode.SET_TEMP;
|
||||||
|
ctx.setStatus().accept("L-Klick im Viewport → temporären Spawnpunkt setzen");
|
||||||
|
});
|
||||||
|
|
||||||
|
Button setPermBtn = new Button("Perm. Spawn hier setzen");
|
||||||
|
setPermBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
setPermBtn.setTooltip(new Tooltip(
|
||||||
|
"Aktiviert Klick-Modus im Viewport zum Setzen des permanenten Spawnpunkts"));
|
||||||
|
setPermBtn.setOnAction(e -> {
|
||||||
|
ctx.input().playToolMode = SharedInput.PlayToolMode.SET_PERM;
|
||||||
|
ctx.setStatus().accept("L-Klick im Viewport → permanenten Spawnpunkt setzen");
|
||||||
|
});
|
||||||
|
|
||||||
|
inner.getChildren().addAll(setTempBtn, setPermBtn, new Separator());
|
||||||
|
|
||||||
|
tempSpawnCoordsLabel = new Label(tempSpawnCoordsText());
|
||||||
|
tempSpawnCoordsLabel.setStyle("-fx-font-family: monospace; -fx-font-size: 11; -fx-text-fill: #333;");
|
||||||
|
permSpawnCoordsLabel = new Label(permSpawnCoordsText());
|
||||||
|
permSpawnCoordsLabel.setStyle("-fx-font-family: monospace; -fx-font-size: 11; -fx-text-fill: #333;");
|
||||||
|
|
||||||
|
Button clearTempBtn = new Button("X Temp. Spawn loeschen");
|
||||||
|
clearTempBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
clearTempBtn.setOnAction(e -> {
|
||||||
|
ctx.input().tempSpawnX = Float.NaN;
|
||||||
|
ctx.input().tempSpawnZ = Float.NaN;
|
||||||
|
if (tempSpawnCoordsLabel != null) tempSpawnCoordsLabel.setText("(nicht gesetzt)");
|
||||||
|
});
|
||||||
|
|
||||||
|
inner.getChildren().addAll(
|
||||||
|
EditorUiHelper.bold("Temp. Spawn:"), tempSpawnCoordsLabel, clearTempBtn,
|
||||||
|
EditorUiHelper.bold("Perm. Spawn:"), permSpawnCoordsLabel,
|
||||||
|
new Separator());
|
||||||
|
|
||||||
|
inner.getChildren().addAll(EditorUiHelper.sectionTitle("Spiel starten"), new Separator());
|
||||||
|
|
||||||
|
Button playBtn = new Button("▶ Spielen");
|
||||||
|
gamePlayBtn = playBtn;
|
||||||
|
playBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
playBtn.setStyle("-fx-background-color: #2d8a3e; -fx-text-fill: white; " +
|
||||||
|
"-fx-font-weight: bold; -fx-padding: 6 12 6 12;");
|
||||||
|
playBtn.setTooltip(new Tooltip(
|
||||||
|
"Startet das Spiel mit dem temp. Spawnpunkt (bzw. gespeicherter Position)"));
|
||||||
|
playBtn.setOnAction(e -> launchGame());
|
||||||
|
|
||||||
|
Button newGameBtn = new Button("Neues Spiel");
|
||||||
|
gameNewBtn = newGameBtn;
|
||||||
|
newGameBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
newGameBtn.setStyle("-fx-background-color: #7b3ea4; -fx-text-fill: white; " +
|
||||||
|
"-fx-font-weight: bold; -fx-padding: 6 12 6 12;");
|
||||||
|
newGameBtn.setTooltip(new Tooltip(
|
||||||
|
"Startet ein neues Spiel am perm. Spawnpunkt mit Intro-Sequenz"));
|
||||||
|
newGameBtn.setOnAction(e -> launchNewGame());
|
||||||
|
|
||||||
|
Button fullStartBtn = new Button("▶ Vollst. Start");
|
||||||
|
gameFullBtn = fullStartBtn;
|
||||||
|
fullStartBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
fullStartBtn.setStyle("-fx-background-color: #1a6080; -fx-text-fill: white; " +
|
||||||
|
"-fx-font-weight: bold; -fx-padding: 6 12 6 12;");
|
||||||
|
fullStartBtn.setTooltip(new Tooltip(
|
||||||
|
"Startet das Spiel wie von außen: mit OAA-Logo, JME-Logo und Hauptmenü"));
|
||||||
|
fullStartBtn.setOnAction(e -> launchFullGame());
|
||||||
|
|
||||||
|
debugConsoleCB = new CheckBox("Debug-Konsole anzeigen");
|
||||||
|
debugConsoleCB.setTooltip(new Tooltip(
|
||||||
|
"Öffnet bei Spielstart die Ausgabekonsole mit der Spielausgabe"));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(playBtn, newGameBtn, fullStartBtn, debugConsoleCB);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Launch ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void launchGame() {
|
||||||
|
if (launchGameAfterSave) return;
|
||||||
|
launchGameAfterSave = true;
|
||||||
|
pendingNewGame = false;
|
||||||
|
pendingAutostart = true;
|
||||||
|
if (gamePlayBtn != null) { gamePlayBtn.setDisable(true); gamePlayBtn.setText("⏳ Startet…"); }
|
||||||
|
if (gameNewBtn != null) gameNewBtn.setDisable(true);
|
||||||
|
if (gameFullBtn != null) gameFullBtn.setDisable(true);
|
||||||
|
ctx.input().saveRequested = true;
|
||||||
|
ctx.setStatus().accept("Karte wird gespeichert, Spiel startet…");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void launchNewGame() {
|
||||||
|
if (launchGameAfterSave) return;
|
||||||
|
launchGameAfterSave = true;
|
||||||
|
pendingNewGame = true;
|
||||||
|
pendingAutostart = true;
|
||||||
|
if (gameNewBtn != null) { gameNewBtn.setDisable(true); gameNewBtn.setText("⏳ Startet…"); }
|
||||||
|
if (gamePlayBtn != null) gamePlayBtn.setDisable(true);
|
||||||
|
if (gameFullBtn != null) gameFullBtn.setDisable(true);
|
||||||
|
ctx.input().saveRequested = true;
|
||||||
|
ctx.setStatus().accept("Karte wird gespeichert, Neues Spiel startet…");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void launchFullGame() {
|
||||||
|
if (launchGameAfterSave) return;
|
||||||
|
launchGameAfterSave = true;
|
||||||
|
pendingNewGame = false;
|
||||||
|
pendingAutostart = false;
|
||||||
|
if (gameFullBtn != null) { gameFullBtn.setDisable(true); gameFullBtn.setText("⏳ Startet…"); }
|
||||||
|
if (gamePlayBtn != null) gamePlayBtn.setDisable(true);
|
||||||
|
if (gameNewBtn != null) gameNewBtn.setDisable(true);
|
||||||
|
ctx.input().saveRequested = true;
|
||||||
|
ctx.setStatus().accept("Karte wird gespeichert, vollständiger Spielstart…");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called from EditorApp AnimationTimer after save completes. */
|
||||||
|
public void onSaveComplete() {
|
||||||
|
if (!launchGameAfterSave) return;
|
||||||
|
launchGameAfterSave = false;
|
||||||
|
startGameProcess();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Console buffer drain — called from AnimationTimer ─────────────────────
|
||||||
|
|
||||||
|
public void flushConsoleBuffer() {
|
||||||
|
if (consoleBuffer.isEmpty()) return;
|
||||||
|
if (gameConsoleArea != null) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
int count = 0;
|
||||||
|
String ln;
|
||||||
|
while (count < 200 && (ln = consoleBuffer.poll()) != null) {
|
||||||
|
sb.append(ln).append('\n');
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
gameConsoleArea.appendText(sb.toString());
|
||||||
|
gameConsoleArea.setScrollTop(Double.MAX_VALUE);
|
||||||
|
} else {
|
||||||
|
consoleBuffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Spawn coords update — called from AnimationTimer ──────────────────────
|
||||||
|
|
||||||
|
public void updateSpawnFields(String info) {
|
||||||
|
if (info == null) return;
|
||||||
|
String[] p = info.split("\\|", -1);
|
||||||
|
if (p.length < 2) return;
|
||||||
|
if (tempSpawnCoordsLabel != null) tempSpawnCoordsLabel.setText(tempSpawnCoordsText());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updatePermSpawnLabel() {
|
||||||
|
if (permSpawnCoordsLabel != null) permSpawnCoordsLabel.setText(permSpawnCoordsText());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Internal ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private String tempSpawnCoordsText() {
|
||||||
|
SharedInput in = ctx.input();
|
||||||
|
if (Float.isNaN(in.tempSpawnX) || Float.isNaN(in.tempSpawnZ)) return "(nicht gesetzt)";
|
||||||
|
return String.format("X=%.1f Z=%.1f Yaw=%.0f°", in.tempSpawnX, in.tempSpawnZ, in.tempSpawnYaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String permSpawnCoordsText() {
|
||||||
|
SharedInput in = ctx.input();
|
||||||
|
if (Float.isNaN(in.permSpawnX) || Float.isNaN(in.permSpawnZ)) return "(nicht gesetzt)";
|
||||||
|
return String.format("X=%.1f Z=%.1f Yaw=%.0f°", in.permSpawnX, in.permSpawnZ, in.permSpawnYaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startGameProcess() {
|
||||||
|
final boolean isNewGame = pendingNewGame;
|
||||||
|
final boolean isAutostart = pendingAutostart;
|
||||||
|
pendingNewGame = false;
|
||||||
|
pendingAutostart = true;
|
||||||
|
Stage primary = ctx.stageSupplier().get();
|
||||||
|
Platform.runLater(() -> { if (primary != null) primary.setIconified(true); });
|
||||||
|
SharedInput in = ctx.input();
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
String javaExe = Paths.get(System.getProperty("java.home"), "bin", "java").toString();
|
||||||
|
String classpath = System.getProperty("java.class.path");
|
||||||
|
String libPath = System.getProperty("java.library.path", "");
|
||||||
|
String projRoot = ProjectRoot.PATH.toString();
|
||||||
|
|
||||||
|
Path srcMapDir = ProjectRoot.PATH.resolve(
|
||||||
|
Paths.get("blight-map", "src", "main", "map"));
|
||||||
|
Path runDir = ProjectRoot.PATH.resolve("run");
|
||||||
|
Path sessionDir = runDir.resolve("session-" + System.currentTimeMillis());
|
||||||
|
copyDirectory(srcMapDir, sessionDir);
|
||||||
|
|
||||||
|
try {
|
||||||
|
java.util.List<Path> sessions;
|
||||||
|
try (java.util.stream.Stream<Path> s = Files.list(runDir)) {
|
||||||
|
sessions = s.filter(p -> p.getFileName().toString().startsWith("session-"))
|
||||||
|
.sorted(java.util.Comparator.comparing(p -> p.getFileName().toString()))
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
|
int keep = 3;
|
||||||
|
for (int i = 0; i < sessions.size() - keep; i++) {
|
||||||
|
deleteDirectoryRecursive(sessions.get(i));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// non-fatal cleanup failure
|
||||||
|
}
|
||||||
|
String sessionMapPath = sessionDir.resolve("blight_map.blm").toString();
|
||||||
|
|
||||||
|
java.util.List<String> cmd = new java.util.ArrayList<>(List.of(
|
||||||
|
javaExe,
|
||||||
|
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
|
||||||
|
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED",
|
||||||
|
"-Djava.library.path=" + libPath,
|
||||||
|
"-Dblight.project.root=" + projRoot,
|
||||||
|
"-D" + de.blight.common.MapIO.PROP_SESSION_MAP + "=" + sessionMapPath,
|
||||||
|
"-Dblight.autostart=" + isAutostart));
|
||||||
|
|
||||||
|
if (isNewGame) {
|
||||||
|
cmd.add("-Dblight.new.game=true");
|
||||||
|
} else if (!Float.isNaN(in.tempSpawnX) && !Float.isNaN(in.tempSpawnZ)) {
|
||||||
|
cmd.add("-Dblight.temp.spawn.x=" + in.tempSpawnX);
|
||||||
|
cmd.add("-Dblight.temp.spawn.z=" + in.tempSpawnZ);
|
||||||
|
cmd.add("-Dblight.temp.spawn.yaw=" + in.tempSpawnYaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.addAll(List.of("-cp", classpath, "de.blight.game.BlightGame"));
|
||||||
|
|
||||||
|
Process proc = new ProcessBuilder(cmd)
|
||||||
|
.directory(ProjectRoot.PATH.toFile())
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start();
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
ctx.setStatus().accept(isNewGame ? "Neues Spiel gestartet" : "Spiel gestartet");
|
||||||
|
if (gamePlayBtn != null) gamePlayBtn.setText("Laeuft…");
|
||||||
|
if (gameNewBtn != null) gameNewBtn.setText("Laeuft…");
|
||||||
|
if (gameFullBtn != null) gameFullBtn.setText("Laeuft…");
|
||||||
|
if (debugConsoleCB != null && debugConsoleCB.isSelected()) openGameConsole();
|
||||||
|
});
|
||||||
|
|
||||||
|
try (java.io.BufferedReader br = new java.io.BufferedReader(
|
||||||
|
new java.io.InputStreamReader(proc.getInputStream(),
|
||||||
|
java.nio.charset.StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
consoleBuffer.offer(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
consoleBuffer.offer("--- Spiel beendet ---");
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (primary != null) primary.setIconified(false);
|
||||||
|
if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); }
|
||||||
|
if (gameNewBtn != null) { gameNewBtn.setText("Neues Spiel"); gameNewBtn.setDisable(false); }
|
||||||
|
if (gameFullBtn != null) { gameFullBtn.setText("▶ Vollst. Start"); gameFullBtn.setDisable(false); }
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
ctx.setStatus().accept("Spielstart fehlgeschlagen: " + ex.getMessage());
|
||||||
|
if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); }
|
||||||
|
if (gameNewBtn != null) { gameNewBtn.setText("Neues Spiel"); gameNewBtn.setDisable(false); }
|
||||||
|
if (gameFullBtn != null) { gameFullBtn.setText("▶ Vollst. Start"); gameFullBtn.setDisable(false); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, "game-launcher").start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openGameConsole() {
|
||||||
|
Stage primary = ctx.stageSupplier().get();
|
||||||
|
if (gameConsoleStage == null) {
|
||||||
|
gameConsoleArea = new TextArea();
|
||||||
|
gameConsoleArea.setEditable(false);
|
||||||
|
gameConsoleArea.setStyle(
|
||||||
|
"-fx-control-inner-background: #1e1e1e; -fx-text-fill: #d4d4d4;" +
|
||||||
|
" -fx-font-family: monospace; -fx-font-size: 11;");
|
||||||
|
|
||||||
|
Button clearBtn = new Button("Löschen");
|
||||||
|
clearBtn.setOnAction(e -> gameConsoleArea.clear());
|
||||||
|
|
||||||
|
HBox toolbar = new HBox(5, clearBtn);
|
||||||
|
toolbar.setPadding(new Insets(4));
|
||||||
|
|
||||||
|
VBox layout = new VBox(toolbar, gameConsoleArea);
|
||||||
|
VBox.setVgrow(gameConsoleArea, Priority.ALWAYS);
|
||||||
|
|
||||||
|
Scene scene = new Scene(layout, 800, 350);
|
||||||
|
gameConsoleStage = new Stage();
|
||||||
|
gameConsoleStage.setTitle("Spiel-Konsole");
|
||||||
|
gameConsoleStage.setScene(scene);
|
||||||
|
gameConsoleStage.initOwner(primary);
|
||||||
|
var screens = javafx.stage.Screen.getScreens();
|
||||||
|
if (screens.size() > 1) {
|
||||||
|
javafx.geometry.Rectangle2D sb = screens.get(1).getVisualBounds();
|
||||||
|
gameConsoleStage.setX(sb.getMinX());
|
||||||
|
gameConsoleStage.setY(sb.getMinY());
|
||||||
|
gameConsoleStage.setWidth(sb.getWidth());
|
||||||
|
gameConsoleStage.setHeight(sb.getHeight());
|
||||||
|
} else if (primary != null) {
|
||||||
|
gameConsoleStage.setX(primary.getX());
|
||||||
|
gameConsoleStage.setY(primary.getY() + primary.getHeight());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gameConsoleArea.clear();
|
||||||
|
}
|
||||||
|
gameConsoleStage.show();
|
||||||
|
gameConsoleStage.toFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void copyDirectory(Path src, Path dst) throws IOException {
|
||||||
|
try (java.util.stream.Stream<Path> walk = java.nio.file.Files.walk(src)) {
|
||||||
|
for (Path s : (Iterable<Path>) walk::iterator) {
|
||||||
|
Path d = dst.resolve(src.relativize(s));
|
||||||
|
if (java.nio.file.Files.isDirectory(s)) {
|
||||||
|
java.nio.file.Files.createDirectories(d);
|
||||||
|
} else {
|
||||||
|
java.nio.file.Files.copy(s, d, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteDirectoryRecursive(Path dir) throws IOException {
|
||||||
|
if (!Files.exists(dir)) return;
|
||||||
|
try (java.util.stream.Stream<Path> walk = Files.walk(dir)) {
|
||||||
|
walk.sorted(java.util.Comparator.reverseOrder())
|
||||||
|
.forEach(p -> { try { Files.delete(p); } catch (IOException ignored) {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,751 @@
|
|||||||
|
package de.blight.editor.toolset.object;
|
||||||
|
|
||||||
|
import de.blight.common.ModelCategory;
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
import de.blight.editor.ToolContext;
|
||||||
|
import de.blight.editor.ToolController;
|
||||||
|
import de.blight.editor.ui.TextureChooser;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static de.blight.editor.toolset.UiHelper.*;
|
||||||
|
|
||||||
|
public class ObjectController implements ToolController {
|
||||||
|
|
||||||
|
private final ToolContext ctx;
|
||||||
|
|
||||||
|
// ── Felder ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Label objModelLabel;
|
||||||
|
private Label objPosLabel;
|
||||||
|
private CheckBox objSolidCB;
|
||||||
|
private CheckBox objCastShadowCB;
|
||||||
|
private CheckBox objReceiveShadowCB;
|
||||||
|
private ComboBox<ModelCategory> objCatCombo;
|
||||||
|
private CheckBox multiPlaceCB;
|
||||||
|
private TextField objXField, objYField, objZField;
|
||||||
|
private TextField objRotXField, objRotYField, objRotZField;
|
||||||
|
private TextField objScaleField;
|
||||||
|
private Label objTexLabel;
|
||||||
|
private Label objNormalMapLabel;
|
||||||
|
private Label objMatLabel;
|
||||||
|
private VBox objDynamicContent;
|
||||||
|
private Button objMergeBtn;
|
||||||
|
private Button objSaveBtn;
|
||||||
|
private Button objDeleteBtn;
|
||||||
|
private ToggleButton selModeObjectBtn;
|
||||||
|
private ToggleGroup meshToggleGroup;
|
||||||
|
private Label randomTreeStatusLabel;
|
||||||
|
private ComboBox<String> treeFolderCB;
|
||||||
|
|
||||||
|
private boolean pendingPlaceMode = true;
|
||||||
|
|
||||||
|
public ObjectController(ToolContext ctx) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ToolController ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toolId() { return "objecteditor"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void activate() {
|
||||||
|
ctx.setCenterView().accept(ctx.viewportSupplier().get());
|
||||||
|
if (pendingPlaceMode) {
|
||||||
|
ctx.input().activeLayer = SharedInput.LAYER_OBJECTS;
|
||||||
|
ctx.setRightPanel().accept(buildObjectPlacePanel());
|
||||||
|
} else {
|
||||||
|
ctx.input().activeLayer = SharedInput.LAYER_OBJECTS_EDIT;
|
||||||
|
ctx.input().pendingModelPath = null;
|
||||||
|
ctx.setRightPanel().accept(buildObjectEditPanel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deactivate() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFrame() {
|
||||||
|
String rts = ctx.input().randomTreeStatus;
|
||||||
|
if (randomTreeStatusLabel != null && rts != null) {
|
||||||
|
randomTreeStatusLabel.setText(rts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.input().refreshTreeFolders) {
|
||||||
|
ctx.input().refreshTreeFolders = false;
|
||||||
|
populateTreeFolderCombo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.input().objectJustPlaced) {
|
||||||
|
ctx.input().objectJustPlaced = false;
|
||||||
|
if (multiPlaceCB == null || !multiPlaceCB.isSelected()) {
|
||||||
|
showEditPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.input().objectClickedWhileInPlaceMode) {
|
||||||
|
ctx.input().objectClickedWhileInPlaceMode = false;
|
||||||
|
showEditPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.input().objectSelectionChanged) {
|
||||||
|
ctx.input().objectSelectionChanged = false;
|
||||||
|
updateObjectPanel(ctx.input().selectedObjectInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Öffentliche API ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void activatePlaceMode() {
|
||||||
|
// Place mode only when something is actually selected; otherwise fall back to edit mode
|
||||||
|
boolean hasSelection = (ctx.input().pendingModelPath != null && !ctx.input().pendingModelPath.isEmpty())
|
||||||
|
|| (ctx.input().treeFolderPath != null && !ctx.input().treeFolderPath.isEmpty());
|
||||||
|
pendingPlaceMode = hasSelection;
|
||||||
|
if (ctx.registry().getActive() == this) {
|
||||||
|
activate();
|
||||||
|
} else {
|
||||||
|
ctx.registry().activate(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void activateEditMode() {
|
||||||
|
pendingPlaceMode = false;
|
||||||
|
if (ctx.registry().getActive() == this) {
|
||||||
|
activate();
|
||||||
|
} else {
|
||||||
|
ctx.registry().activate(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enableMultiPlace() {
|
||||||
|
if (multiPlaceCB != null) multiPlaceCB.setSelected(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zeigt das Platzieren-Panel. */
|
||||||
|
public void showPlacePanel() {
|
||||||
|
pendingPlaceMode = true;
|
||||||
|
ctx.input().activeLayer = SharedInput.LAYER_OBJECTS;
|
||||||
|
ctx.setRightPanel().accept(buildObjectPlacePanel());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zeigt das Bearbeiten-Panel. */
|
||||||
|
public void showEditPanel() {
|
||||||
|
pendingPlaceMode = false;
|
||||||
|
ctx.input().activeLayer = SharedInput.LAYER_OBJECTS_EDIT;
|
||||||
|
ctx.input().pendingModelPath = null;
|
||||||
|
ctx.setRightPanel().accept(buildObjectEditPanel());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hebt die Platzierungs-Auswahl auf und wechselt zum Selektier-Modus. */
|
||||||
|
public void cancelPlacement() {
|
||||||
|
ctx.input().pendingModelPath = null;
|
||||||
|
ctx.input().treeFolderPath = null;
|
||||||
|
showEditPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Platzieren-Panel ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private VBox buildObjectPlacePanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
Button cancelBtn = new Button("✕ Auswahl aufheben [Esc]");
|
||||||
|
cancelBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
cancelBtn.setStyle("-fx-background-color: #e8e8e8;");
|
||||||
|
cancelBtn.setOnAction(e -> cancelPlacement());
|
||||||
|
inner.getChildren().add(cancelBtn);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Baum aus Ordner"), new Separator());
|
||||||
|
|
||||||
|
treeFolderCB = new ComboBox<>();
|
||||||
|
treeFolderCB.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
treeFolderCB.setOnAction(e -> {
|
||||||
|
String sel = treeFolderCB.getValue();
|
||||||
|
if (sel == null || sel.equals("(keiner)")) {
|
||||||
|
ctx.input().treeFolderPath = null;
|
||||||
|
if (randomTreeStatusLabel != null) randomTreeStatusLabel.setText("");
|
||||||
|
} else {
|
||||||
|
ctx.input().treeFolderPath = sel;
|
||||||
|
ctx.input().pendingModelPath = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
populateTreeFolderCombo();
|
||||||
|
|
||||||
|
Button refreshFolderBtn = new Button("↺ Aktualisieren");
|
||||||
|
refreshFolderBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
refreshFolderBtn.setOnAction(e -> populateTreeFolderCombo());
|
||||||
|
|
||||||
|
randomTreeStatusLabel = new Label("");
|
||||||
|
randomTreeStatusLabel.setWrapText(true);
|
||||||
|
randomTreeStatusLabel.setStyle("-fx-text-fill: #555; -fx-font-size: 10;");
|
||||||
|
|
||||||
|
inner.getChildren().add(treeFolderCB);
|
||||||
|
inner.getChildren().add(refreshFolderBtn);
|
||||||
|
inner.getChildren().add(randomTreeStatusLabel);
|
||||||
|
inner.getChildren().add(styledHint("Doppelklick auf Ordner links → Ordner aktivieren"));
|
||||||
|
inner.getChildren().add(styledHint("Linksklick → zufälliges Modell platzieren"));
|
||||||
|
inner.getChildren().add(styledHint("Rechtsklick → neues Zufallsmodell + Rotation"));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Modell"), new Separator());
|
||||||
|
objModelLabel = new Label(ctx.input().pendingModelPath != null
|
||||||
|
&& !ctx.input().pendingModelPath.startsWith("@")
|
||||||
|
? ctx.input().pendingModelPath : "(Doppelklick im Asset-Baum)");
|
||||||
|
objModelLabel.setWrapText(true);
|
||||||
|
objModelLabel.setStyle("-fx-text-fill: #333; -fx-font-size: 11;");
|
||||||
|
inner.getChildren().add(objModelLabel);
|
||||||
|
inner.getChildren().add(styledHint("Doppelklick auf Modell links → auswählen"));
|
||||||
|
inner.getChildren().add(styledHint("Linksklick ins Terrain → platzieren"));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Textur"), new Separator());
|
||||||
|
Label texLabel = new Label(ctx.input().pendingTexturePath != null && !ctx.input().pendingTexturePath.isEmpty()
|
||||||
|
? labelFromPath(ctx.input().pendingTexturePath)
|
||||||
|
: "(keine)");
|
||||||
|
texLabel.setWrapText(true);
|
||||||
|
texLabel.setStyle("-fx-text-fill: #333; -fx-font-size: 11;");
|
||||||
|
Button clearTexBtn = new Button("✕");
|
||||||
|
clearTexBtn.setTooltip(new Tooltip("Textur entfernen"));
|
||||||
|
clearTexBtn.setOnAction(ev -> {
|
||||||
|
ctx.input().pendingTexturePath = "";
|
||||||
|
showPlacePanel();
|
||||||
|
});
|
||||||
|
HBox texRow = new HBox(6, texLabel, clearTexBtn);
|
||||||
|
texRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
HBox.setHgrow(texLabel, Priority.ALWAYS);
|
||||||
|
inner.getChildren().add(texRow);
|
||||||
|
inner.getChildren().add(styledHint("Doppelklick auf Textur links → auswählen"));
|
||||||
|
|
||||||
|
boolean prevMulti = multiPlaceCB != null && multiPlaceCB.isSelected();
|
||||||
|
multiPlaceCB = new CheckBox("Mehrfach platzieren");
|
||||||
|
multiPlaceCB.setSelected(prevMulti);
|
||||||
|
multiPlaceCB.setStyle("-fx-text-fill: #111111;");
|
||||||
|
multiPlaceCB.setTooltip(new Tooltip(
|
||||||
|
"Aktiv: Objekte nacheinander platzieren.\n" +
|
||||||
|
"Inaktiv: Nach dem Platzieren automatisch in den Bearbeiten-Modus wechseln."));
|
||||||
|
inner.getChildren().add(multiPlaceCB);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Meshes"), new Separator());
|
||||||
|
|
||||||
|
meshToggleGroup = new ToggleGroup();
|
||||||
|
String[][] shapes = {
|
||||||
|
{"Box", "@box"},
|
||||||
|
{"Kugel", "@sphere"},
|
||||||
|
{"Zylinder", "@cylinder"},
|
||||||
|
{"Ebene", "@plane"},
|
||||||
|
};
|
||||||
|
|
||||||
|
GridPane grid = new GridPane();
|
||||||
|
grid.setHgap(6);
|
||||||
|
grid.setVgap(6);
|
||||||
|
grid.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
|
for (int i = 0; i < shapes.length; i++) {
|
||||||
|
String lbl = shapes[i][0];
|
||||||
|
String marker = shapes[i][1];
|
||||||
|
ToggleButton btn = new ToggleButton(lbl);
|
||||||
|
btn.setToggleGroup(meshToggleGroup);
|
||||||
|
btn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
btn.setStyle("-fx-font-size: 12;");
|
||||||
|
btn.setOnAction(e -> {
|
||||||
|
if (btn.isSelected()) {
|
||||||
|
ctx.input().pendingModelPath = marker;
|
||||||
|
if (objModelLabel != null)
|
||||||
|
objModelLabel.setText("(Doppelklick im Asset-Baum)");
|
||||||
|
ctx.setStatus().accept("Mesh: " + lbl + " | Linksklick ins Terrain zum Platzieren");
|
||||||
|
} else {
|
||||||
|
ctx.input().pendingModelPath = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (marker.equals(ctx.input().pendingModelPath)) btn.setSelected(true);
|
||||||
|
grid.add(btn, i % 2, i / 2);
|
||||||
|
GridPane.setHgrow(btn, Priority.ALWAYS);
|
||||||
|
}
|
||||||
|
inner.getChildren().add(grid);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Terrain-Ausrichten"), new Separator());
|
||||||
|
CheckBox alignCB = new CheckBox("An Terrain angleichen");
|
||||||
|
alignCB.setSelected(ctx.input().terrainAlignEnabled);
|
||||||
|
alignCB.setStyle("-fx-text-fill: #111111;");
|
||||||
|
alignCB.setOnAction(e -> ctx.input().terrainAlignEnabled = alignCB.isSelected());
|
||||||
|
inner.getChildren().add(alignCB);
|
||||||
|
inner.getChildren().add(styledHint("Passt rotX/rotZ an die Geländesteigung an"));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Vertex-Snap"), new Separator());
|
||||||
|
CheckBox placeSnapCB = new CheckBox("Snap aktiv");
|
||||||
|
placeSnapCB.setSelected(ctx.input().vertexSnapEnabled);
|
||||||
|
placeSnapCB.setStyle("-fx-text-fill: #111111;");
|
||||||
|
placeSnapCB.setOnAction(e -> ctx.input().vertexSnapEnabled = placeSnapCB.isSelected());
|
||||||
|
|
||||||
|
Slider placeSnapSlider = new Slider(0.05, 2.0, ctx.input().vertexSnapRadius);
|
||||||
|
placeSnapSlider.setShowTickLabels(false);
|
||||||
|
placeSnapSlider.setBlockIncrement(0.05);
|
||||||
|
placeSnapSlider.valueProperty().addListener((o, ov, nv) -> ctx.input().vertexSnapRadius = nv.floatValue());
|
||||||
|
HBox placeSnapRow = withField(placeSnapSlider, "%.2f");
|
||||||
|
placeSnapRow.disableProperty().bind(placeSnapCB.selectedProperty().not());
|
||||||
|
inner.getChildren().addAll(new VBox(3, placeSnapCB, new Label("Radius"), placeSnapRow));
|
||||||
|
inner.getChildren().add(styledHint("Snap aktiv: Platzierung rastet am nächsten Vertex ein"));
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bearbeiten-Panel ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private VBox buildObjectEditPanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
CheckBox snapCB = new CheckBox("Snap – Punkte verschmelzen");
|
||||||
|
snapCB.setSelected(ctx.input().vertexSnapEnabled);
|
||||||
|
snapCB.setStyle("-fx-font-size: 11;");
|
||||||
|
snapCB.setOnAction(e -> ctx.input().vertexSnapEnabled = snapCB.isSelected());
|
||||||
|
|
||||||
|
Slider snapRadiusSlider = new Slider(0.05, 2.0, ctx.input().vertexSnapRadius);
|
||||||
|
snapRadiusSlider.setShowTickLabels(false);
|
||||||
|
snapRadiusSlider.setBlockIncrement(0.05);
|
||||||
|
snapRadiusSlider.valueProperty().addListener((o, ov, nv) -> ctx.input().vertexSnapRadius = nv.floatValue());
|
||||||
|
HBox snapRadiusRow = withField(snapRadiusSlider, "%.2f");
|
||||||
|
snapRadiusRow.disableProperty().bind(snapCB.selectedProperty().not());
|
||||||
|
VBox snapBox = new VBox(3, snapCB, new Label("Radius"), snapRadiusRow);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(
|
||||||
|
sectionTitle("Bearbeiten"),
|
||||||
|
buildSelectionModeBar(),
|
||||||
|
buildEditToolBar(),
|
||||||
|
snapBox,
|
||||||
|
new Separator(),
|
||||||
|
sectionTitle("Gewähltes Objekt"),
|
||||||
|
new Separator());
|
||||||
|
|
||||||
|
objDynamicContent = new VBox(6);
|
||||||
|
Label noSel = new Label("Kein Objekt ausgewählt");
|
||||||
|
noSel.setStyle("-fx-text-fill: #888;");
|
||||||
|
objDynamicContent.getChildren().add(noSel);
|
||||||
|
inner.getChildren().add(objDynamicContent);
|
||||||
|
|
||||||
|
inner.getChildren().add(new Separator());
|
||||||
|
|
||||||
|
objMergeBtn = new Button("⛙ Zusammenfassen");
|
||||||
|
objMergeBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
objMergeBtn.setDisable(true);
|
||||||
|
objMergeBtn.setOnAction(e -> ctx.input().mergeSelectedRequested = true);
|
||||||
|
|
||||||
|
objSaveBtn = new Button("💾 Als Vorlage speichern…");
|
||||||
|
objSaveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
objSaveBtn.setDisable(true);
|
||||||
|
objSaveBtn.setOnAction(e -> {
|
||||||
|
TextInputDialog dlg = new TextInputDialog("Vorlage");
|
||||||
|
dlg.initOwner(ctx.stageSupplier().get());
|
||||||
|
dlg.setTitle("Vorlage speichern");
|
||||||
|
dlg.setHeaderText("Name der Vorlage (ohne .j3o):");
|
||||||
|
dlg.setContentText("Name:");
|
||||||
|
dlg.showAndWait().ifPresent(name -> {
|
||||||
|
if (!name.trim().isEmpty()) ctx.input().saveAsTemplateRequest = name.trim();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
objDeleteBtn = new Button("🗑 Löschen");
|
||||||
|
objDeleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
objDeleteBtn.setDisable(true);
|
||||||
|
objDeleteBtn.setStyle("-fx-text-fill: #c0392b;");
|
||||||
|
objDeleteBtn.setOnAction(e -> ctx.input().deleteSelectedRequested = true);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(objMergeBtn, objSaveBtn, objDeleteBtn, new Separator(),
|
||||||
|
styledHint("Linksklick → auswählen"),
|
||||||
|
styledHint("Shift+Klick → zur Selektion hinzufügen"),
|
||||||
|
styledHint("Pfeile ziehen → X/Y/Z bewegen"),
|
||||||
|
styledHint("Ring ziehen → Y-Achse drehen"),
|
||||||
|
styledHint("Entf → ausgewählte 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Selektions-/Bearbeiten-Leisten ───────────────────────────────────────
|
||||||
|
|
||||||
|
private javafx.scene.Node buildSelectionModeBar() {
|
||||||
|
ToggleGroup tg = new ToggleGroup();
|
||||||
|
String[] labels = {"Objekt", "Polygon", "Kante", "Punkt"};
|
||||||
|
int[] modes = {SharedInput.SEL_MODE_OBJECT, SharedInput.SEL_MODE_POLYGON,
|
||||||
|
SharedInput.SEL_MODE_EDGE, SharedInput.SEL_MODE_VERTEX};
|
||||||
|
|
||||||
|
selModeObjectBtn = null;
|
||||||
|
|
||||||
|
HBox bar = new HBox(2);
|
||||||
|
for (int i = 0; i < labels.length; i++) {
|
||||||
|
ToggleButton btn = new ToggleButton(labels[i]);
|
||||||
|
btn.setToggleGroup(tg);
|
||||||
|
btn.setUserData(modes[i]);
|
||||||
|
HBox.setHgrow(btn, Priority.ALWAYS);
|
||||||
|
btn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
if (modes[i] == ctx.input().objectSelectionMode) btn.setSelected(true);
|
||||||
|
int mode = modes[i];
|
||||||
|
btn.setOnAction(e -> ctx.input().objectSelectionMode = mode);
|
||||||
|
bar.getChildren().add(btn);
|
||||||
|
if (modes[i] == SharedInput.SEL_MODE_OBJECT) selModeObjectBtn = btn;
|
||||||
|
}
|
||||||
|
bar.setStyle("-fx-font-size: 11;");
|
||||||
|
|
||||||
|
VBox box = new VBox(3, new Label("Selektion"), bar);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetSelectionModeToObject() {
|
||||||
|
if (selModeObjectBtn != null) selModeObjectBtn.setSelected(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private javafx.scene.Node buildEditToolBar() {
|
||||||
|
ToggleGroup tg = new ToggleGroup();
|
||||||
|
record ToolEntry(String label, String tooltip, int mode) {}
|
||||||
|
var tools = List.of(
|
||||||
|
new ToolEntry("Bewegen", "Objekte entlang X/Y/Z verschieben", SharedInput.EDIT_TOOL_MOVE),
|
||||||
|
new ToolEntry("Rotieren", "Objekte um X/Y/Z rotieren (Ring-Gizmo)", SharedInput.EDIT_TOOL_ROTATE),
|
||||||
|
new ToolEntry("Skalieren", "Objekte skalieren (noch nicht implementiert)", SharedInput.EDIT_TOOL_SCALE)
|
||||||
|
);
|
||||||
|
|
||||||
|
HBox bar = new HBox(2);
|
||||||
|
for (var t : tools) {
|
||||||
|
ToggleButton btn = new ToggleButton(t.label());
|
||||||
|
btn.setToggleGroup(tg);
|
||||||
|
btn.setTooltip(new Tooltip(t.tooltip()));
|
||||||
|
HBox.setHgrow(btn, Priority.ALWAYS);
|
||||||
|
btn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
if (t.mode() == ctx.input().objectEditTool) btn.setSelected(true);
|
||||||
|
int mode = t.mode();
|
||||||
|
btn.setOnAction(e -> ctx.input().objectEditTool = mode);
|
||||||
|
bar.getChildren().add(btn);
|
||||||
|
}
|
||||||
|
bar.setStyle("-fx-font-size: 11;");
|
||||||
|
VBox box = new VBox(3, new Label("Bearbeitung"), bar);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Objekt-Panel Update ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void updateObjectPanel(String info) {
|
||||||
|
if (objDynamicContent == null) return;
|
||||||
|
|
||||||
|
objXField = objYField = objZField = null;
|
||||||
|
objRotXField = objRotYField = objRotZField = null;
|
||||||
|
objScaleField = null;
|
||||||
|
objSolidCB = null;
|
||||||
|
objCastShadowCB = null;
|
||||||
|
objReceiveShadowCB = null;
|
||||||
|
objCatCombo = null;
|
||||||
|
objNormalMapLabel = null;
|
||||||
|
objMatLabel = null;
|
||||||
|
|
||||||
|
objDynamicContent.getChildren().clear();
|
||||||
|
|
||||||
|
if (info == null) {
|
||||||
|
Label noSel = new Label("Kein Objekt ausgewählt");
|
||||||
|
noSel.setStyle("-fx-text-fill: #888;");
|
||||||
|
objDynamicContent.getChildren().add(noSel);
|
||||||
|
if (objMergeBtn != null) objMergeBtn.setDisable(true);
|
||||||
|
if (objSaveBtn != null) objSaveBtn.setDisable(true);
|
||||||
|
if (objDeleteBtn != null) objDeleteBtn.setDisable(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts = info.split("\\|", -1);
|
||||||
|
int count;
|
||||||
|
try { count = Integer.parseInt(parts[0]); } catch (NumberFormatException e) { return; }
|
||||||
|
|
||||||
|
if (count == 1 && parts.length >= 11) {
|
||||||
|
String modelPath = parts[1];
|
||||||
|
boolean isPrefab = !modelPath.startsWith("@") || modelPath.equals("@group");
|
||||||
|
if (isPrefab) resetSelectionModeToObject();
|
||||||
|
boolean solid = Boolean.parseBoolean(parts[2]);
|
||||||
|
float x = parseF(parts[3]);
|
||||||
|
float y = parseF(parts[4]);
|
||||||
|
float z = parseF(parts[5]);
|
||||||
|
float rotXDeg = (float) Math.toDegrees(parseF(parts[6]));
|
||||||
|
float rotYDeg = (float) Math.toDegrees(parseF(parts[7]));
|
||||||
|
float rotZDeg = (float) Math.toDegrees(parseF(parts[8]));
|
||||||
|
float scale = parseF(parts[9]);
|
||||||
|
String texPath = parts[10];
|
||||||
|
String normalPath = parts.length >= 12 ? parts[11] : "";
|
||||||
|
String matPath = parts.length >= 13 ? parts[12] : "";
|
||||||
|
String animClip = parts.length >= 14 ? parts[13] : "";
|
||||||
|
boolean castShadow = parts.length >= 15 ? Boolean.parseBoolean(parts[14]) : true;
|
||||||
|
boolean receiveShadow= parts.length >= 16 ? Boolean.parseBoolean(parts[15]) : true;
|
||||||
|
ModelCategory modelCat = parts.length >= 19
|
||||||
|
? ModelCategory.fromString(parts[18])
|
||||||
|
: ModelCategory.NONE;
|
||||||
|
|
||||||
|
String modelName = modelPath.contains("/")
|
||||||
|
? modelPath.substring(modelPath.lastIndexOf('/') + 1) : modelPath;
|
||||||
|
Label modelLbl = new Label("Modell: " + modelName);
|
||||||
|
modelLbl.setStyle("-fx-text-fill: #555; -fx-font-size: 11;");
|
||||||
|
modelLbl.setWrapText(true);
|
||||||
|
objCatCombo = new ComboBox<>();
|
||||||
|
objCatCombo.getItems().addAll(ModelCategory.values());
|
||||||
|
objCatCombo.setValue(modelCat);
|
||||||
|
objCatCombo.setConverter(new javafx.util.StringConverter<>() {
|
||||||
|
@Override public String toString(ModelCategory c) {
|
||||||
|
return c == null ? "" : c.getLabel();
|
||||||
|
}
|
||||||
|
@Override public ModelCategory fromString(String s) {
|
||||||
|
return ModelCategory.fromString(s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
objCatCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
objCatCombo.setOnAction(ev -> enqueuePropertyChange(null, null, null));
|
||||||
|
HBox catRow = new HBox(6, new Label("Kategorie:"), objCatCombo);
|
||||||
|
catRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
objTexLabel = new Label(texPath.isEmpty() ? "(keine)"
|
||||||
|
: (texPath.contains("/") ? texPath.substring(texPath.lastIndexOf('/') + 1) : texPath));
|
||||||
|
objTexLabel.setStyle("-fx-text-fill: #555; -fx-font-size: 11;");
|
||||||
|
objTexLabel.setWrapText(true);
|
||||||
|
Button chooseTex = new Button("Wählen…");
|
||||||
|
chooseTex.setOnAction(ev -> {
|
||||||
|
TextureChooser chooser = new TextureChooser(ctx.assetRoot(), false);
|
||||||
|
chooser.initOwner(ctx.stageSupplier().get());
|
||||||
|
chooser.showAndWait().ifPresent(p -> {
|
||||||
|
enqueuePropertyChange(p, null, null);
|
||||||
|
objTexLabel.setText(p.contains("/") ? p.substring(p.lastIndexOf('/') + 1) : p);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Button clearTex = new Button("✕");
|
||||||
|
clearTex.setOnAction(ev -> {
|
||||||
|
enqueuePropertyChange("", null, null);
|
||||||
|
objTexLabel.setText("(keine)");
|
||||||
|
});
|
||||||
|
HBox texRow = new HBox(4, objTexLabel, chooseTex, clearTex);
|
||||||
|
texRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
objNormalMapLabel = new Label(normalPath.isEmpty() ? "(keine)"
|
||||||
|
: (normalPath.contains("/") ? normalPath.substring(normalPath.lastIndexOf('/') + 1) : normalPath));
|
||||||
|
objNormalMapLabel.setStyle("-fx-text-fill: #555; -fx-font-size: 11;");
|
||||||
|
objNormalMapLabel.setWrapText(true);
|
||||||
|
Button chooseNorm = new Button("Wählen…");
|
||||||
|
chooseNorm.setOnAction(ev -> {
|
||||||
|
TextureChooser chooser = new TextureChooser(ctx.assetRoot(), true);
|
||||||
|
chooser.initOwner(ctx.stageSupplier().get());
|
||||||
|
chooser.showAndWait().ifPresent(p -> {
|
||||||
|
enqueuePropertyChange(null, p, null);
|
||||||
|
objNormalMapLabel.setText(p.contains("/") ? p.substring(p.lastIndexOf('/') + 1) : p);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Button clearNorm = new Button("✕");
|
||||||
|
clearNorm.setOnAction(ev -> {
|
||||||
|
enqueuePropertyChange(null, "", null);
|
||||||
|
objNormalMapLabel.setText("(keine)");
|
||||||
|
});
|
||||||
|
HBox normRow = new HBox(4, objNormalMapLabel, chooseNorm, clearNorm);
|
||||||
|
normRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
objMatLabel = new Label(matPath.isEmpty() ? "(Standard)"
|
||||||
|
: matPath.substring(matPath.lastIndexOf('/') + 1).replace(".j3md", ""));
|
||||||
|
objMatLabel.setStyle("-fx-text-fill: #555; -fx-font-size: 11;");
|
||||||
|
objMatLabel.setWrapText(true);
|
||||||
|
Button chooseMat = new Button("Wählen…");
|
||||||
|
chooseMat.setOnAction(ev -> {
|
||||||
|
de.blight.editor.ui.MaterialChooser chooser =
|
||||||
|
new de.blight.editor.ui.MaterialChooser(ctx.assetRoot().resolve("MatDefs"));
|
||||||
|
chooser.initOwner(ctx.stageSupplier().get());
|
||||||
|
chooser.showAndWait().ifPresent(p -> {
|
||||||
|
enqueuePropertyChange(null, null, p);
|
||||||
|
objMatLabel.setText(p.substring(p.lastIndexOf('/') + 1).replace(".j3md", ""));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Button clearMat = new Button("✕");
|
||||||
|
clearMat.setOnAction(ev -> {
|
||||||
|
enqueuePropertyChange(null, null, "");
|
||||||
|
objMatLabel.setText("(Standard)");
|
||||||
|
});
|
||||||
|
HBox matRow = new HBox(4, objMatLabel, chooseMat, clearMat);
|
||||||
|
matRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
objXField = makeFloatField(x);
|
||||||
|
objYField = makeFloatField(y);
|
||||||
|
objZField = makeFloatField(z);
|
||||||
|
objRotXField = makeFloatField(rotXDeg);
|
||||||
|
objRotYField = makeFloatField(rotYDeg);
|
||||||
|
objRotZField = makeFloatField(rotZDeg);
|
||||||
|
objScaleField = makeFloatField(scale);
|
||||||
|
|
||||||
|
Button uebernehmenBtn = new Button("✔ Übernehmen");
|
||||||
|
uebernehmenBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
uebernehmenBtn.setDefaultButton(true);
|
||||||
|
uebernehmenBtn.setOnAction(ev -> enqueuePropertyChange(null, null, null));
|
||||||
|
|
||||||
|
objSolidCB = new CheckBox("Solid (Charakter-Kollision)");
|
||||||
|
objSolidCB.setSelected(solid);
|
||||||
|
objSolidCB.setOnAction(ev -> enqueuePropertyChange(null, null, null));
|
||||||
|
|
||||||
|
objCastShadowCB = new CheckBox("Schatten werfen");
|
||||||
|
objCastShadowCB.setSelected(castShadow);
|
||||||
|
objCastShadowCB.setOnAction(ev -> enqueuePropertyChange(null, null, null));
|
||||||
|
|
||||||
|
objReceiveShadowCB = new CheckBox("Schatten empfangen");
|
||||||
|
objReceiveShadowCB.setSelected(receiveShadow);
|
||||||
|
objReceiveShadowCB.setOnAction(ev -> enqueuePropertyChange(null, null, null));
|
||||||
|
|
||||||
|
ComboBox<String> animCombo = new ComboBox<>();
|
||||||
|
animCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
animCombo.getItems().add("(keine)");
|
||||||
|
Path animDir = ctx.assetRoot().resolve("Characters").resolve("clips");
|
||||||
|
if (Files.isDirectory(animDir)) {
|
||||||
|
try (java.util.stream.Stream<Path> walk = Files.walk(animDir)) {
|
||||||
|
walk.filter(p -> p.toString().endsWith(".j3o"))
|
||||||
|
.map(p -> animDir.relativize(p).toString().replace('\\', '/'))
|
||||||
|
.sorted()
|
||||||
|
.forEach(animCombo.getItems()::add);
|
||||||
|
} catch (java.io.IOException ignored) {}
|
||||||
|
}
|
||||||
|
String currentAnim = (animClip == null || animClip.isEmpty()) ? "(keine)" : animClip;
|
||||||
|
animCombo.setValue(animCombo.getItems().contains(currentAnim) ? currentAnim : "(keine)");
|
||||||
|
animCombo.setOnAction(ev -> {
|
||||||
|
String val = animCombo.getValue();
|
||||||
|
ctx.input().pendingAnimClip = "(keine)".equals(val) ? "" : val;
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox scaleRow = new HBox(6, new Label("Skalierung:"), objScaleField);
|
||||||
|
scaleRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
objDynamicContent.getChildren().addAll(
|
||||||
|
modelLbl, catRow,
|
||||||
|
bold("Textur:"), texRow,
|
||||||
|
bold("Normal Map:"), normRow,
|
||||||
|
bold("Material:"), matRow, new Separator(),
|
||||||
|
bold("Position (m)"),
|
||||||
|
labeledRow("X:", objXField, "Y:", objYField, "Z:", objZField),
|
||||||
|
bold("Rotation (°)"),
|
||||||
|
labeledRow("X:", objRotXField, "Y:", objRotYField, "Z:", objRotZField),
|
||||||
|
scaleRow,
|
||||||
|
uebernehmenBtn, new Separator(),
|
||||||
|
objSolidCB, objCastShadowCB, objReceiveShadowCB, new Separator(),
|
||||||
|
bold("Animation:"), animCombo);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Label countLbl = new Label(count + " Objekte ausgewählt");
|
||||||
|
countLbl.setStyle("-fx-font-weight: bold;");
|
||||||
|
objSolidCB = new CheckBox("Solid (alle)");
|
||||||
|
objSolidCB.setIndeterminate(true);
|
||||||
|
objSolidCB.setOnAction(ev -> ctx.input().pendingSolidChange = objSolidCB.isSelected());
|
||||||
|
objDynamicContent.getChildren().addAll(countLbl, objSolidCB);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objMergeBtn != null) objMergeBtn.setDisable(count < 2);
|
||||||
|
if (objSaveBtn != null) objSaveBtn.setDisable(false);
|
||||||
|
if (objDeleteBtn != null) objDeleteBtn.setDisable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enqueuePropertyChange(String texOverride, String normalOverride, String matOverride) {
|
||||||
|
if (objXField == null || objYField == null || objZField == null) return;
|
||||||
|
try {
|
||||||
|
float x = parseFloatField(objXField.getText());
|
||||||
|
float y = parseFloatField(objYField.getText());
|
||||||
|
float z = parseFloatField(objZField.getText());
|
||||||
|
float rotX = (float) Math.toRadians(parseFloatField(objRotXField.getText()));
|
||||||
|
float rotY = (float) Math.toRadians(parseFloatField(objRotYField.getText()));
|
||||||
|
float rotZ = (float) Math.toRadians(parseFloatField(objRotZField.getText()));
|
||||||
|
float scale = objScaleField != null ? parseFloatField(objScaleField.getText()) : 1f;
|
||||||
|
boolean solid = objSolidCB != null && objSolidCB.isSelected();
|
||||||
|
boolean castShadow = objCastShadowCB == null || objCastShadowCB.isSelected();
|
||||||
|
boolean receiveShadow = objReceiveShadowCB == null || objReceiveShadowCB.isSelected();
|
||||||
|
ModelCategory cat = objCatCombo != null ? objCatCombo.getValue() : null;
|
||||||
|
ctx.input().objectPropertyQueue.offer(
|
||||||
|
new SharedInput.ObjectPropertyChange(x, y, z, rotX, rotY, rotZ, scale,
|
||||||
|
solid, castShadow, receiveShadow,
|
||||||
|
texOverride, normalOverride, matOverride, cat));
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Baum-Ordner-Combo ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void populateTreeFolderCombo() {
|
||||||
|
if (treeFolderCB == null) return;
|
||||||
|
String prev = treeFolderCB.getValue();
|
||||||
|
|
||||||
|
Path treeRoot = de.blight.editor.ProjectRoot.resolve(
|
||||||
|
"blight-assets", "src", "main", "resources", "trees");
|
||||||
|
java.util.List<String> items = new java.util.ArrayList<>();
|
||||||
|
items.add("(keiner)");
|
||||||
|
if (Files.isDirectory(treeRoot)) {
|
||||||
|
try (var walk = Files.walk(treeRoot)) {
|
||||||
|
walk.filter(Files::isDirectory)
|
||||||
|
.filter(p -> !p.equals(treeRoot))
|
||||||
|
.map(p -> "trees/" + treeRoot.relativize(p).toString()
|
||||||
|
.replace(java.io.File.separatorChar, '/'))
|
||||||
|
.sorted()
|
||||||
|
.forEach(items::add);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
String cur = ctx.input().treeFolderPath;
|
||||||
|
if (cur != null && !cur.isEmpty() && !items.contains(cur)) {
|
||||||
|
items.add(cur);
|
||||||
|
}
|
||||||
|
|
||||||
|
treeFolderCB.getItems().setAll(items);
|
||||||
|
if (prev != null && items.contains(prev)) {
|
||||||
|
treeFolderCB.setValue(prev);
|
||||||
|
} else {
|
||||||
|
treeFolderCB.setValue(cur != null && items.contains(cur) ? cur : "(keiner)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Statische Hilfs-Methoden ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static float parseF(String s) {
|
||||||
|
try { return Float.parseFloat(s); } catch (NumberFormatException e) { return 0f; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TextField makeFloatField(float val) {
|
||||||
|
TextField tf = new TextField(String.format(java.util.Locale.ROOT, "%.2f", val));
|
||||||
|
tf.setPrefWidth(62);
|
||||||
|
tf.setMaxWidth(62);
|
||||||
|
return tf;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float parseFloatField(String s) {
|
||||||
|
return Float.parseFloat(s.replace(',', '.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HBox labeledRow(String l1, javafx.scene.Node f1,
|
||||||
|
String l2, javafx.scene.Node f2,
|
||||||
|
String l3, javafx.scene.Node f3) {
|
||||||
|
HBox row = new HBox(3,
|
||||||
|
new Label(l1), f1,
|
||||||
|
new Label(l2), f2,
|
||||||
|
new Label(l3), f3);
|
||||||
|
row.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String labelFromPath(String path) {
|
||||||
|
if (path == null || path.isEmpty()) return "(leer)";
|
||||||
|
Path p = ctx.assetRoot().resolve(path.replace('/', java.io.File.separatorChar));
|
||||||
|
Path parent = p.getParent();
|
||||||
|
if (parent != null && 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,335 @@
|
|||||||
|
package de.blight.editor.toolset.topbar;
|
||||||
|
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
import de.blight.editor.ToolContext;
|
||||||
|
import javafx.geometry.Orientation;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
|
||||||
|
public class TopBarController {
|
||||||
|
|
||||||
|
public record Deps(
|
||||||
|
VBox toolPanel,
|
||||||
|
de.blight.editor.toolset.model.ModelEditorController modelEditorController,
|
||||||
|
de.blight.editor.toolset.asset.AssetTreeController assetController,
|
||||||
|
de.blight.editor.toolset.anim.AnimSetController animSetController,
|
||||||
|
de.blight.editor.toolset.content.ContentController contentController,
|
||||||
|
de.blight.editor.toolset.world.WorldPanelController worldPanelController,
|
||||||
|
de.blight.editor.toolset.voxel.VoxelController voxelController,
|
||||||
|
de.blight.editor.toolset.grass.GrassController grassController,
|
||||||
|
de.blight.editor.toolset.atmosphere.AtmosphereController atmosphereController,
|
||||||
|
de.blight.editor.toolset.water.WaterController waterController,
|
||||||
|
de.blight.editor.toolset.game.GameController gameController,
|
||||||
|
de.blight.editor.toolset.terrain.TerrainPanelController terrainController,
|
||||||
|
de.blight.editor.toolset.object.ObjectController objectController,
|
||||||
|
Runnable switchToVegetationGenerator,
|
||||||
|
Runnable switchToEzTree,
|
||||||
|
Runnable switchToTripo,
|
||||||
|
Runnable openCharacterEditor,
|
||||||
|
Runnable switchToThumbnailManager,
|
||||||
|
Runnable takeEditorScreenshot,
|
||||||
|
Runnable openSettings
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private final SharedInput input;
|
||||||
|
|
||||||
|
// ToggleButtons – pre-created so getters work before buildTop() is called
|
||||||
|
private final ToggleButton auswahlBtn = new ToggleButton("[k] Auswahl");
|
||||||
|
private final ToggleButton baseBtn = new ToggleButton("▲▼ Terrain");
|
||||||
|
private final ToggleButton voxelMergedBtn = new ToggleButton("◈ Voxel");
|
||||||
|
private final ToggleButton textureBtn = new ToggleButton(": : Textur");
|
||||||
|
private final ToggleButton grassBtn = new ToggleButton("// Gras");
|
||||||
|
private final ToggleButton stoneBtn = new ToggleButton("o Steine");
|
||||||
|
private final ToggleButton objPlaceBtn = new ToggleButton("+ Objekte");
|
||||||
|
private final ToggleButton atmosphaereBtn = new ToggleButton("* Atmosphare");
|
||||||
|
private final ToggleButton wasserMergedBtn = new ToggleButton("~ Wasser");
|
||||||
|
private final ToggleButton wegeBtn = new ToggleButton("-- Wege");
|
||||||
|
private final ToggleButton zonenBtn = new ToggleButton("[ ] Zonen");
|
||||||
|
private final ToggleButton playToolBtn = new ToggleButton("▶ Spielen");
|
||||||
|
|
||||||
|
// Menu items – pre-created
|
||||||
|
private final RadioMenuItem camOrbitItem = new RadioMenuItem("⊙ Orbit-Kamera (Alt+C)");
|
||||||
|
private final RadioMenuItem camFreeItem = new RadioMenuItem("✈ FreeFly-Kamera (Alt+C)");
|
||||||
|
private final CheckMenuItem dualWindowMenuItem = new CheckMenuItem("2-Fenster-Modus (Alt+W)");
|
||||||
|
private final CheckMenuItem showGridItem = new CheckMenuItem("1m-Gitternetz (Alt+N)");
|
||||||
|
private CheckMenuItem viewTopologyItem;
|
||||||
|
private boolean wireframeActive = false;
|
||||||
|
|
||||||
|
private ToolBar worldToolBar;
|
||||||
|
private Label worldHintLabel;
|
||||||
|
|
||||||
|
public TopBarController(SharedInput input) {
|
||||||
|
this.input = input;
|
||||||
|
|
||||||
|
// Bold style
|
||||||
|
final String BOLD = "-fx-font-weight:bold;";
|
||||||
|
auswahlBtn.setStyle(BOLD);
|
||||||
|
baseBtn.setStyle(BOLD);
|
||||||
|
voxelMergedBtn.setStyle(BOLD);
|
||||||
|
textureBtn.setStyle(BOLD);
|
||||||
|
grassBtn.setStyle(BOLD);
|
||||||
|
stoneBtn.setStyle(BOLD);
|
||||||
|
objPlaceBtn.setStyle(BOLD);
|
||||||
|
atmosphaereBtn.setStyle(BOLD);
|
||||||
|
wasserMergedBtn.setStyle(BOLD);
|
||||||
|
wegeBtn.setStyle(BOLD);
|
||||||
|
zonenBtn.setStyle(BOLD);
|
||||||
|
playToolBtn.setStyle(BOLD);
|
||||||
|
|
||||||
|
// Mutually exclusive toggle group
|
||||||
|
ToggleGroup layerGroup = new ToggleGroup();
|
||||||
|
auswahlBtn.setToggleGroup(layerGroup);
|
||||||
|
baseBtn.setToggleGroup(layerGroup);
|
||||||
|
voxelMergedBtn.setToggleGroup(layerGroup);
|
||||||
|
textureBtn.setToggleGroup(layerGroup);
|
||||||
|
grassBtn.setToggleGroup(layerGroup);
|
||||||
|
stoneBtn.setToggleGroup(layerGroup);
|
||||||
|
objPlaceBtn.setToggleGroup(layerGroup);
|
||||||
|
atmosphaereBtn.setToggleGroup(layerGroup);
|
||||||
|
wasserMergedBtn.setToggleGroup(layerGroup);
|
||||||
|
wegeBtn.setToggleGroup(layerGroup);
|
||||||
|
zonenBtn.setToggleGroup(layerGroup);
|
||||||
|
playToolBtn.setToggleGroup(layerGroup);
|
||||||
|
|
||||||
|
// Cam-Menu toggle group
|
||||||
|
ToggleGroup camMenuGroup = new ToggleGroup();
|
||||||
|
camOrbitItem.setToggleGroup(camMenuGroup);
|
||||||
|
camFreeItem.setToggleGroup(camMenuGroup);
|
||||||
|
camOrbitItem.setSelected(true);
|
||||||
|
|
||||||
|
// Grid default
|
||||||
|
showGridItem.setSelected(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Getters ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public ToggleButton getAuswahlBtn() { return auswahlBtn; }
|
||||||
|
public ToggleButton getBaseBtn() { return baseBtn; }
|
||||||
|
public ToggleButton getVoxelMergedBtn() { return voxelMergedBtn; }
|
||||||
|
public ToggleButton getTextureBtn() { return textureBtn; }
|
||||||
|
public ToggleButton getGrassBtn() { return grassBtn; }
|
||||||
|
public ToggleButton getStoneBtn() { return stoneBtn; }
|
||||||
|
public ToggleButton getObjPlaceBtn() { return objPlaceBtn; }
|
||||||
|
public ToggleButton getAtmosphaereBtn() { return atmosphaereBtn; }
|
||||||
|
public ToggleButton getWasserMergedBtn(){ return wasserMergedBtn; }
|
||||||
|
public ToggleButton getWegeBtn() { return wegeBtn; }
|
||||||
|
public ToggleButton getZonenBtn() { return zonenBtn; }
|
||||||
|
public ToggleButton getPlayToolBtn() { return playToolBtn; }
|
||||||
|
|
||||||
|
public RadioMenuItem getCamOrbitItem() { return camOrbitItem; }
|
||||||
|
public RadioMenuItem getCamFreeItem() { return camFreeItem; }
|
||||||
|
public CheckMenuItem getDualWindowMenuItem() { return dualWindowMenuItem; }
|
||||||
|
public CheckMenuItem getShowGridItem() { return showGridItem; }
|
||||||
|
public CheckMenuItem getViewTopologyItem() { return viewTopologyItem; }
|
||||||
|
|
||||||
|
public ToolBar getWorldToolBar() { return worldToolBar; }
|
||||||
|
public Label getWorldHintLabel(){ return worldHintLabel; }
|
||||||
|
|
||||||
|
public boolean toggleWireframe() {
|
||||||
|
wireframeActive = !wireframeActive;
|
||||||
|
return wireframeActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateCamHintLabel() {
|
||||||
|
if (worldHintLabel != null) { worldHintLabel.setText(camHintText()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyShowGridPref(boolean value) {
|
||||||
|
showGridItem.setSelected(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String camHintText() {
|
||||||
|
String mode = (input.camMode == SharedInput.CAM_FREEFLY) ? "FreeFly" : "Orbit";
|
||||||
|
return "WASD/QE: Kamera | Mitte-Drag: Drehen | Alt+C: Kamera wechseln | Kamera: " + mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public VBox buildTop(ToolContext ctx, Deps deps) {
|
||||||
|
MenuBar menuBar = new MenuBar();
|
||||||
|
|
||||||
|
// ── Datei-Menü ──────────────────────────────────────────────────────
|
||||||
|
Menu fileMenu = new Menu("Datei");
|
||||||
|
MenuItem newItem = new MenuItem("Neue Karte");
|
||||||
|
MenuItem saveItem = new MenuItem("Speichern");
|
||||||
|
saveItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+S"));
|
||||||
|
saveItem.setOnAction(e -> { input.saveRequested = true; ctx.setStatus().accept("Speichern…"); });
|
||||||
|
MenuItem screenshotItem = new MenuItem("Screenshot");
|
||||||
|
screenshotItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+F12"));
|
||||||
|
screenshotItem.setOnAction(e -> deps.takeEditorScreenshot().run());
|
||||||
|
MenuItem settingsItem = new MenuItem("Einstellungen…");
|
||||||
|
settingsItem.setOnAction(e -> deps.openSettings().run());
|
||||||
|
MenuItem quitItem = new MenuItem("Beenden");
|
||||||
|
quitItem.setOnAction(e -> javafx.application.Platform.exit());
|
||||||
|
fileMenu.getItems().addAll(newItem, saveItem, new SeparatorMenuItem(),
|
||||||
|
screenshotItem, new SeparatorMenuItem(), settingsItem,
|
||||||
|
new SeparatorMenuItem(), quitItem);
|
||||||
|
|
||||||
|
// ── Import-Menü ─────────────────────────────────────────────────────
|
||||||
|
Menu importMenu = new Menu("Import");
|
||||||
|
MenuItem importModelLodItem = new MenuItem("Modell mit LOD-Editor…");
|
||||||
|
MenuItem importTexItem = new MenuItem("Texturen…");
|
||||||
|
MenuItem importTexZipItem = new MenuItem("Texturen-ZIP…");
|
||||||
|
MenuItem importTexSetItem = new MenuItem("Textur-Set importieren…");
|
||||||
|
MenuItem importAudioItem = new MenuItem("Audio…");
|
||||||
|
MenuItem importAnimItem = new MenuItem("Animationen…");
|
||||||
|
MenuItem importCharModelItem = new MenuItem("Character-Modell (FBX)…");
|
||||||
|
importModelLodItem.setOnAction(e -> deps.modelEditorController().openModelImport(ctx.stageSupplier().get()));
|
||||||
|
importTexItem.setOnAction(e -> deps.assetController().handleTextureImport(ctx.stageSupplier().get()));
|
||||||
|
importTexZipItem.setOnAction(e -> deps.assetController().handleZipTextureImport(ctx.stageSupplier().get()));
|
||||||
|
importTexSetItem.setOnAction(e -> deps.assetController().openTextureSetImport(ctx.stageSupplier().get()));
|
||||||
|
importAudioItem.setOnAction(e -> deps.assetController().handleAudioImport(ctx.stageSupplier().get()));
|
||||||
|
importAnimItem.setOnAction(e -> deps.animSetController().handleAnimationImport(ctx.stageSupplier().get()));
|
||||||
|
importCharModelItem.setOnAction(e -> deps.animSetController().handleCharacterModelImport(ctx.stageSupplier().get()));
|
||||||
|
importMenu.getItems().addAll(importModelLodItem, importTexItem, importTexZipItem,
|
||||||
|
importTexSetItem, importAudioItem, importAnimItem,
|
||||||
|
new SeparatorMenuItem(), importCharModelItem);
|
||||||
|
|
||||||
|
// ── Werkzeuge-Menü ──────────────────────────────────────────────────
|
||||||
|
Menu toolsMenu = new Menu("Werkzeuge");
|
||||||
|
MenuItem vegetationsItem = new MenuItem("Vegetations Generator");
|
||||||
|
MenuItem ezTreeItem = new MenuItem("Baum Generator (EZ Tree)");
|
||||||
|
MenuItem tripoItem = new MenuItem("AI Modell-Generator (Tripo3D)");
|
||||||
|
MenuItem animPrevItem = new MenuItem("Animationset Editor");
|
||||||
|
MenuItem objEditorItem = new MenuItem("Model Editor");
|
||||||
|
MenuItem thumbItem = new MenuItem("Thumbnail-Verwaltung");
|
||||||
|
vegetationsItem.setOnAction(e -> deps.switchToVegetationGenerator().run());
|
||||||
|
ezTreeItem.setOnAction(e -> deps.switchToEzTree().run());
|
||||||
|
tripoItem.setOnAction(e -> deps.switchToTripo().run());
|
||||||
|
animPrevItem.setOnAction(e -> deps.openCharacterEditor().run());
|
||||||
|
objEditorItem.setOnAction(e -> deps.modelEditorController().openModelEditor(null, null));
|
||||||
|
thumbItem.setOnAction(e -> deps.switchToThumbnailManager().run());
|
||||||
|
toolsMenu.getItems().addAll(vegetationsItem, ezTreeItem, tripoItem,
|
||||||
|
animPrevItem, objEditorItem, new SeparatorMenuItem(), thumbItem);
|
||||||
|
|
||||||
|
// ── Ansicht-Menü ────────────────────────────────────────────────────
|
||||||
|
Menu viewMenu = new Menu("Ansicht");
|
||||||
|
MenuItem resetCam = new MenuItem("Kamera zurücksetzen (Alt+R)");
|
||||||
|
resetCam.setOnAction(e -> input.resetCameraRequested.set(true));
|
||||||
|
MenuItem viewTexture = new MenuItem("Textur (Alt+G)");
|
||||||
|
MenuItem viewWireframe = new MenuItem("Drahtgitter (Alt+G)");
|
||||||
|
viewTopologyItem = new CheckMenuItem("Topologie-Overlay (Alt+T)");
|
||||||
|
viewTexture.setOnAction(e -> {
|
||||||
|
wireframeActive = false;
|
||||||
|
input.wireframeRequest = 2;
|
||||||
|
});
|
||||||
|
viewWireframe.setOnAction(e -> {
|
||||||
|
wireframeActive = true;
|
||||||
|
input.wireframeRequest = 1;
|
||||||
|
});
|
||||||
|
viewTopologyItem.setOnAction(e ->
|
||||||
|
input.topologyRequest = viewTopologyItem.isSelected() ? 1 : 2);
|
||||||
|
|
||||||
|
camOrbitItem.setOnAction(e -> { input.camMode = SharedInput.CAM_ORBIT; updateCamHintLabel(); });
|
||||||
|
camFreeItem.setOnAction(e -> { input.camMode = SharedInput.CAM_FREEFLY; updateCamHintLabel(); });
|
||||||
|
|
||||||
|
CheckMenuItem debugHeightItem = new CheckMenuItem("Höhen-Overlay (Alt+H)");
|
||||||
|
debugHeightItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Alt+H"));
|
||||||
|
debugHeightItem.setOnAction(e ->
|
||||||
|
input.debugHeightOverlayEnabled = debugHeightItem.isSelected());
|
||||||
|
|
||||||
|
dualWindowMenuItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Alt+W"));
|
||||||
|
dualWindowMenuItem.setOnAction(e -> {
|
||||||
|
if (dualWindowMenuItem.isSelected()) { deps.contentController().enableDualWindowMode(); }
|
||||||
|
else { deps.contentController().disableDualWindowMode(); }
|
||||||
|
});
|
||||||
|
deps.contentController().setDualWindowMenuItem(dualWindowMenuItem);
|
||||||
|
|
||||||
|
showGridItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Alt+N"));
|
||||||
|
showGridItem.setOnAction(e -> input.showGrid = showGridItem.isSelected());
|
||||||
|
|
||||||
|
viewMenu.getItems().addAll(resetCam, new SeparatorMenuItem(), viewTexture, viewWireframe,
|
||||||
|
new SeparatorMenuItem(), viewTopologyItem,
|
||||||
|
new SeparatorMenuItem(), debugHeightItem,
|
||||||
|
new SeparatorMenuItem(), showGridItem,
|
||||||
|
new SeparatorMenuItem(), camOrbitItem, camFreeItem);
|
||||||
|
|
||||||
|
// ── Zeit-Menü ───────────────────────────────────────────────────────
|
||||||
|
Menu zeitMenu = new Menu("Zeit");
|
||||||
|
ToggleGroup zeitGroup = new ToggleGroup();
|
||||||
|
int[] hours = {0, 3, 6, 9, 12, 15, 18, 21};
|
||||||
|
for (int h : hours) {
|
||||||
|
RadioMenuItem item = new RadioMenuItem(String.format("%02d:00", h));
|
||||||
|
item.setToggleGroup(zeitGroup);
|
||||||
|
if (h == 12) { item.setSelected(true); }
|
||||||
|
final float t = h / 24f;
|
||||||
|
item.setOnAction(e -> input.pendingTimeOfDay = t);
|
||||||
|
zeitMenu.getItems().add(item);
|
||||||
|
}
|
||||||
|
menuBar.getMenus().addAll(fileMenu, toolsMenu, importMenu, viewMenu, zeitMenu);
|
||||||
|
|
||||||
|
// ── Toolbar-Buttons ─────────────────────────────────────────────────
|
||||||
|
auswahlBtn.setSelected(true);
|
||||||
|
input.activeLayer = SharedInput.LAYER_AUSWAHL;
|
||||||
|
|
||||||
|
auswahlBtn.setOnAction(e -> {
|
||||||
|
input.activeLayer = SharedInput.LAYER_AUSWAHL;
|
||||||
|
input.deselectAllOnAuswahl = true;
|
||||||
|
ctx.setRightPanel().accept(deps.worldPanelController().buildAuswahlPanel());
|
||||||
|
});
|
||||||
|
baseBtn.setOnAction(e -> {
|
||||||
|
input.heightTool.brushRadius.setValue(input.voxelTool.brushRadius.getValue());
|
||||||
|
input.heightTool.brushStrength.setValue(input.voxelTool.brushStrength.getValue());
|
||||||
|
input.heightTool.plateauHeight.setValue(input.voxelTool.plateauTarget.getValue());
|
||||||
|
input.activeLayer = 0; input.activeTool = input.heightTool;
|
||||||
|
ctx.setRightPanel().accept(deps.toolPanel());
|
||||||
|
deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool);
|
||||||
|
});
|
||||||
|
voxelMergedBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.voxelController().buildPanel()));
|
||||||
|
textureBtn.setOnAction(e -> {
|
||||||
|
input.activeLayer = 4; input.activeTool = input.textureTool;
|
||||||
|
ctx.setRightPanel().accept(deps.toolPanel());
|
||||||
|
deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool);
|
||||||
|
});
|
||||||
|
grassBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.grassController().buildPanel()));
|
||||||
|
stoneBtn.setOnAction(e -> {
|
||||||
|
input.activeLayer = SharedInput.LAYER_STONE;
|
||||||
|
input.activeTool = input.stoneTool;
|
||||||
|
ctx.setRightPanel().accept(deps.toolPanel());
|
||||||
|
deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool);
|
||||||
|
});
|
||||||
|
objPlaceBtn.setOnAction(e -> {
|
||||||
|
deps.assetController().expandModelsNode();
|
||||||
|
deps.assetController().openOverlay();
|
||||||
|
deps.objectController().activatePlaceMode();
|
||||||
|
});
|
||||||
|
atmosphaereBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.atmosphereController().buildPanel()));
|
||||||
|
wasserMergedBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.waterController().buildPanel()));
|
||||||
|
wegeBtn.setOnAction(e -> {
|
||||||
|
input.activeLayer = SharedInput.LAYER_PATH_NETWORK;
|
||||||
|
ctx.setRightPanel().accept(deps.worldPanelController().buildWegePanel());
|
||||||
|
});
|
||||||
|
zonenBtn.setOnAction(e -> {
|
||||||
|
input.activeLayer = SharedInput.LAYER_ZONEN;
|
||||||
|
ctx.setRightPanel().accept(deps.worldPanelController().buildZonenPanel());
|
||||||
|
});
|
||||||
|
playToolBtn.setOnAction(e -> {
|
||||||
|
input.activeLayer = SharedInput.LAYER_PLAY_TOOL;
|
||||||
|
input.playToolMode = SharedInput.PlayToolMode.EDIT;
|
||||||
|
ctx.setRightPanel().accept(deps.gameController().buildPlayToolPanel());
|
||||||
|
});
|
||||||
|
|
||||||
|
worldHintLabel = new Label(camHintText());
|
||||||
|
worldHintLabel.setStyle("-fx-text-fill: #555;");
|
||||||
|
|
||||||
|
Region spacer = new Region();
|
||||||
|
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
|
|
||||||
|
ToolBar toolBar = new ToolBar();
|
||||||
|
toolBar.getItems().addAll(
|
||||||
|
auswahlBtn,
|
||||||
|
new Separator(Orientation.VERTICAL), baseBtn, voxelMergedBtn,
|
||||||
|
new Separator(Orientation.VERTICAL), textureBtn, grassBtn, stoneBtn,
|
||||||
|
new Separator(Orientation.VERTICAL), objPlaceBtn,
|
||||||
|
new Separator(Orientation.VERTICAL), atmosphaereBtn,
|
||||||
|
new Separator(Orientation.VERTICAL), wasserMergedBtn, wegeBtn,
|
||||||
|
new Separator(Orientation.VERTICAL), zonenBtn,
|
||||||
|
new Separator(Orientation.VERTICAL), playToolBtn,
|
||||||
|
new Separator(Orientation.VERTICAL), worldHintLabel,
|
||||||
|
new Separator(Orientation.VERTICAL), spacer,
|
||||||
|
deps.contentController().buildContentSwitchBtn());
|
||||||
|
|
||||||
|
worldToolBar = toolBar;
|
||||||
|
return new VBox(menuBar, toolBar);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
package de.blight.editor.toolset.tripo;
|
||||||
|
|
||||||
|
import de.blight.editor.ToolContext;
|
||||||
|
import de.blight.editor.toolset.EditorUiHelper;
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
import de.blight.editor.TripoGenerator;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Orientation;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class TripoController {
|
||||||
|
|
||||||
|
private final ToolContext ctx;
|
||||||
|
|
||||||
|
private ImageView tripoPreviewView;
|
||||||
|
private Label tripoStatusLabel;
|
||||||
|
private ProgressBar tripoProgressBar;
|
||||||
|
private Button tripoImportBtn;
|
||||||
|
private Button tripoImportRigBtn;
|
||||||
|
private boolean tripoGenerating = false;
|
||||||
|
private String tripoLastModelUrl;
|
||||||
|
private String tripoLastTaskId;
|
||||||
|
|
||||||
|
public TripoController(ToolContext ctx) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void activate() {
|
||||||
|
ctx.setCurrentTool().accept("tripo");
|
||||||
|
|
||||||
|
StackPane previewCenter = new StackPane();
|
||||||
|
previewCenter.setStyle("-fx-background-color: #1a1a2e;");
|
||||||
|
tripoPreviewView = new ImageView();
|
||||||
|
tripoPreviewView.setPreserveRatio(true);
|
||||||
|
tripoPreviewView.fitWidthProperty().bind(previewCenter.widthProperty());
|
||||||
|
tripoPreviewView.fitHeightProperty().bind(previewCenter.heightProperty());
|
||||||
|
Label placeholder = new Label("Vorschau erscheint hier nach der Generierung");
|
||||||
|
placeholder.setStyle("-fx-text-fill: #888; -fx-font-size: 14;");
|
||||||
|
previewCenter.getChildren().addAll(placeholder, tripoPreviewView);
|
||||||
|
ctx.setCenterView().accept(previewCenter);
|
||||||
|
ctx.setRightPanel().accept(buildTripoPanel());
|
||||||
|
ctx.setMainToolbar().accept(buildTripoToolBar());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ToolBar buildTripoToolBar() {
|
||||||
|
Button backBtn = new Button("← Welteneditor");
|
||||||
|
backBtn.setOnAction(e -> ctx.switchToWorld().run());
|
||||||
|
Label label = new Label("AI Modell-Generator · Tripo3D");
|
||||||
|
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||||
|
ToolBar tb = new ToolBar();
|
||||||
|
tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label);
|
||||||
|
return tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox buildTripoPanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(EditorUiHelper.sectionTitle("Tripo3D API"), new Separator());
|
||||||
|
Label apiHint = EditorUiHelper.styledHint("Schlüssel unter platform.tripo3d.ai → API Keys");
|
||||||
|
apiHint.setWrapText(true);
|
||||||
|
TextField apiKeyField = new TextField(loadTripoApiKey());
|
||||||
|
apiKeyField.setPromptText("tsk_xxxx…");
|
||||||
|
apiKeyField.setStyle("-fx-font-family: monospace; -fx-font-size: 11;");
|
||||||
|
inner.getChildren().addAll(new Label("API-Schlüssel:"), apiKeyField, apiHint);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(EditorUiHelper.sectionTitle("Modell beschreiben"), new Separator());
|
||||||
|
TextArea promptArea = new TextArea();
|
||||||
|
promptArea.setPromptText("z. B. \"Ein mittelalterlicher Ritter in Ruestung\"\n"
|
||||||
|
+ "Tipp: Englische Prompts liefern meist bessere Ergebnisse.");
|
||||||
|
promptArea.setPrefRowCount(5);
|
||||||
|
promptArea.setWrapText(true);
|
||||||
|
inner.getChildren().addAll(new Label("Prompt:"), promptArea);
|
||||||
|
|
||||||
|
TextField nameField = new TextField();
|
||||||
|
nameField.setPromptText("dateiname (optional)");
|
||||||
|
inner.getChildren().addAll(new Label("Dateiname:"), nameField);
|
||||||
|
|
||||||
|
inner.getChildren().add(new Separator());
|
||||||
|
Button generateBtn = new Button("Modell generieren");
|
||||||
|
generateBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
generateBtn.setStyle("-fx-font-weight: bold;");
|
||||||
|
generateBtn.setDisable(tripoGenerating);
|
||||||
|
|
||||||
|
tripoProgressBar = new ProgressBar(tripoGenerating ? ProgressBar.INDETERMINATE_PROGRESS : 0);
|
||||||
|
tripoProgressBar.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
tripoStatusLabel = new Label(tripoGenerating ? "Generierung läuft…" : "Bereit");
|
||||||
|
tripoStatusLabel.setWrapText(true);
|
||||||
|
tripoStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #444;");
|
||||||
|
inner.getChildren().addAll(generateBtn, tripoProgressBar, tripoStatusLabel);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(EditorUiHelper.sectionTitle("Import"), new Separator());
|
||||||
|
tripoImportBtn = new Button("Importieren (GLB → j3o)");
|
||||||
|
tripoImportRigBtn = new Button("Importieren + Skelett (Humanoid)");
|
||||||
|
tripoImportBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
tripoImportRigBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
boolean hasPrev = tripoLastModelUrl != null && !tripoGenerating;
|
||||||
|
tripoImportBtn.setDisable(!hasPrev);
|
||||||
|
tripoImportRigBtn.setDisable(!hasPrev || tripoLastTaskId == null);
|
||||||
|
inner.getChildren().addAll(tripoImportBtn, tripoImportRigBtn);
|
||||||
|
inner.getChildren().add(EditorUiHelper.styledHint("Das Skelett-Modell eignet sich für Humanoid-Animationen."));
|
||||||
|
|
||||||
|
generateBtn.setOnAction(e -> {
|
||||||
|
if (tripoGenerating) return;
|
||||||
|
String apiKey = apiKeyField.getText().trim();
|
||||||
|
String prompt = promptArea.getText().trim();
|
||||||
|
if (apiKey.isEmpty()) { ctx.setStatus().accept("Bitte API-Schlüssel eingeben"); return; }
|
||||||
|
if (prompt.isEmpty()) { ctx.setStatus().accept("Bitte Prompt eingeben"); return; }
|
||||||
|
|
||||||
|
saveTripoApiKey(apiKey);
|
||||||
|
tripoGenerating = true;
|
||||||
|
generateBtn.setDisable(true);
|
||||||
|
tripoImportBtn.setDisable(true);
|
||||||
|
tripoImportRigBtn.setDisable(true);
|
||||||
|
tripoProgressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
|
||||||
|
tripoStatusLabel.setText("Erstelle Aufgabe…");
|
||||||
|
if (tripoPreviewView != null) tripoPreviewView.setImage(null);
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
String taskId = TripoGenerator.createTextToModelTask(apiKey, prompt);
|
||||||
|
tripoLastTaskId = taskId;
|
||||||
|
Platform.runLater(() -> tripoStatusLabel.setText("Generiere… (ID: " + taskId + ")"));
|
||||||
|
|
||||||
|
pollUntilDone(apiKey, taskId, result -> {
|
||||||
|
tripoLastModelUrl = result.modelUrl();
|
||||||
|
if (result.previewUrl() != null && tripoPreviewView != null) {
|
||||||
|
tripoPreviewView.setImage(
|
||||||
|
new javafx.scene.image.Image(result.previewUrl(), true));
|
||||||
|
}
|
||||||
|
tripoProgressBar.setProgress(1.0);
|
||||||
|
tripoStatusLabel.setText("Fertig! Modell bereit zum Import.");
|
||||||
|
generateBtn.setDisable(false);
|
||||||
|
tripoImportBtn.setDisable(false);
|
||||||
|
tripoImportRigBtn.setDisable(false);
|
||||||
|
tripoGenerating = false;
|
||||||
|
}, err -> {
|
||||||
|
tripoProgressBar.setProgress(0);
|
||||||
|
tripoStatusLabel.setText("Fehler: " + err);
|
||||||
|
generateBtn.setDisable(false);
|
||||||
|
tripoGenerating = false;
|
||||||
|
});
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
tripoProgressBar.setProgress(0);
|
||||||
|
tripoStatusLabel.setText("Fehler: " + ex.getMessage());
|
||||||
|
generateBtn.setDisable(false);
|
||||||
|
tripoGenerating = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
tripoImportBtn.setOnAction(e -> {
|
||||||
|
if (tripoLastModelUrl == null) return;
|
||||||
|
String name = nameField.getText().trim();
|
||||||
|
if (name.isEmpty()) name = "tripo_" + System.currentTimeMillis();
|
||||||
|
importTripoModel(tripoLastModelUrl, name);
|
||||||
|
});
|
||||||
|
|
||||||
|
tripoImportRigBtn.setOnAction(e -> {
|
||||||
|
if (tripoLastTaskId == null || tripoLastModelUrl == null) return;
|
||||||
|
String apiKey = apiKeyField.getText().trim();
|
||||||
|
String baseName = nameField.getText().trim();
|
||||||
|
if (baseName.isEmpty()) baseName = "tripo_" + System.currentTimeMillis();
|
||||||
|
final String rigName = baseName + "_rigged";
|
||||||
|
|
||||||
|
tripoImportRigBtn.setDisable(true);
|
||||||
|
tripoProgressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
|
||||||
|
tripoStatusLabel.setText("Erstelle Skelett…");
|
||||||
|
|
||||||
|
final String taskId = tripoLastTaskId;
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
String rigTaskId = TripoGenerator.createRigTask(apiKey, taskId);
|
||||||
|
Platform.runLater(() -> tripoStatusLabel.setText(
|
||||||
|
"Rigging… (ID: " + rigTaskId + ")"));
|
||||||
|
|
||||||
|
pollUntilDone(apiKey, rigTaskId, result -> {
|
||||||
|
tripoProgressBar.setProgress(1.0);
|
||||||
|
tripoStatusLabel.setText("Skelett fertig! Importiere…");
|
||||||
|
tripoImportRigBtn.setDisable(false);
|
||||||
|
importTripoModel(result.modelUrl(), rigName);
|
||||||
|
}, err -> {
|
||||||
|
tripoProgressBar.setProgress(0);
|
||||||
|
tripoStatusLabel.setText("Skelett-Fehler: " + err);
|
||||||
|
tripoImportRigBtn.setDisable(false);
|
||||||
|
});
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
tripoProgressBar.setProgress(0);
|
||||||
|
tripoStatusLabel.setText("Fehler: " + ex.getMessage());
|
||||||
|
tripoImportRigBtn.setDisable(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
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 pollUntilDone(String apiKey, String taskId,
|
||||||
|
java.util.function.Consumer<TripoGenerator.TaskResult> onSuccess,
|
||||||
|
java.util.function.Consumer<String> onError) {
|
||||||
|
while (true) {
|
||||||
|
try { Thread.sleep(3_000); } catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
Platform.runLater(() -> onError.accept("Unterbrochen"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
TripoGenerator.TaskResult r = TripoGenerator.pollTask(apiKey, taskId);
|
||||||
|
switch (r.status()) {
|
||||||
|
case "success" -> { Platform.runLater(() -> onSuccess.accept(r)); return; }
|
||||||
|
case "failed", "cancelled" -> {
|
||||||
|
Platform.runLater(() -> onError.accept(r.status())); return;
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
int p = r.progress();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (tripoProgressBar != null) tripoProgressBar.setProgress(p / 100.0);
|
||||||
|
if (tripoStatusLabel != null) tripoStatusLabel.setText("Generiere… " + p + "%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (java.io.IOException ex) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (tripoStatusLabel != null)
|
||||||
|
tripoStatusLabel.setText("Prüfe… (" + ex.getMessage() + ")");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importTripoModel(String url, String name) {
|
||||||
|
final String safeName = name.replaceAll("[^a-zA-Z0-9_\\-]", "_");
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
Platform.runLater(() -> ctx.setStatus().accept("Lade Modell herunter: " + safeName + "…"));
|
||||||
|
Path destDir = ctx.assetRoot().resolve("Models");
|
||||||
|
Files.createDirectories(destDir);
|
||||||
|
Path destGlb = destDir.resolve(safeName + ".glb");
|
||||||
|
TripoGenerator.downloadFile(url, destGlb);
|
||||||
|
|
||||||
|
String assetPath = "Models/" + safeName + ".glb";
|
||||||
|
Path destJ3o = destDir.resolve(safeName + ".j3o");
|
||||||
|
ctx.input().modelConvertQueue.offer(
|
||||||
|
new SharedInput.ModelConvertRequest(assetPath, destJ3o, destGlb, true));
|
||||||
|
|
||||||
|
Platform.runLater(() -> ctx.setStatus().accept("Konvertiere " + safeName + ".glb → .j3o…"));
|
||||||
|
} catch (java.io.IOException ex) {
|
||||||
|
Platform.runLater(() -> ctx.setStatus().accept("Import-Fehler: " + ex.getMessage()));
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String loadTripoApiKey() {
|
||||||
|
return java.util.prefs.Preferences
|
||||||
|
.userNodeForPackage(TripoController.class)
|
||||||
|
.get("tripo_api_key", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void saveTripoApiKey(String key) {
|
||||||
|
java.util.prefs.Preferences
|
||||||
|
.userNodeForPackage(TripoController.class)
|
||||||
|
.put("tripo_api_key", key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,832 @@
|
|||||||
|
package de.blight.editor.toolset.vegetation;
|
||||||
|
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
import de.blight.editor.ToolContext;
|
||||||
|
import de.blight.editor.ToolController;
|
||||||
|
import de.blight.editor.tree.FernOptions;
|
||||||
|
import de.blight.editor.tree.FruitBushOptions;
|
||||||
|
import de.blight.editor.tree.GrapevineOptions;
|
||||||
|
import de.blight.editor.tree.PalmOptions;
|
||||||
|
import de.blight.editor.tree.TreeParams;
|
||||||
|
import de.blight.editor.tree.WillowOptions;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Orientation;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.ComboBox;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.Separator;
|
||||||
|
import javafx.scene.control.Spinner;
|
||||||
|
import javafx.scene.control.ToolBar;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
import static de.blight.editor.toolset.UiHelper.*;
|
||||||
|
|
||||||
|
public class VegetationController implements ToolController {
|
||||||
|
|
||||||
|
private final ToolContext ctx;
|
||||||
|
|
||||||
|
// ── Zustand (aus EditorApp migriert) ──────────────────────────────────────
|
||||||
|
private TreeParams treeParams = TreeParams.oak();
|
||||||
|
private String currentTreePreset = "Eiche";
|
||||||
|
private PalmOptions palmOptions = new PalmOptions();
|
||||||
|
private FernOptions fernOptions = new FernOptions();
|
||||||
|
private FruitBushOptions fruitBushOptions = FruitBushOptions.orange();
|
||||||
|
private String fruitBushVariant = "Orange";
|
||||||
|
private GrapevineOptions grapevineOptions = new GrapevineOptions();
|
||||||
|
private WillowOptions willowOptions = new WillowOptions();
|
||||||
|
private String vegetationType = "Baum (Eiche)";
|
||||||
|
|
||||||
|
private Runnable onF5 = () -> {};
|
||||||
|
private Runnable onF6 = () -> {};
|
||||||
|
|
||||||
|
// ── Preview-Panel (lazy) ──────────────────────────────────────────────────
|
||||||
|
private ImageView treePreviewView;
|
||||||
|
private StackPane plantPreviewPanel;
|
||||||
|
|
||||||
|
public VegetationController(ToolContext ctx) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toolId() { return "vegetation"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void activate() {
|
||||||
|
updateOnF5();
|
||||||
|
ctx.setMainToolbar().accept(buildToolBar());
|
||||||
|
ctx.setCenterView().accept(getPlantPreviewPanel());
|
||||||
|
ctx.setRightPanel().accept(buildParamsPanel());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deactivate() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPreview() { onF5.run(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRandomSeed() { onF6.run(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFrame() {
|
||||||
|
SharedInput input = ctx.input();
|
||||||
|
String treeMsg = input.treeGenStatusMsg;
|
||||||
|
if (treeMsg != null) {
|
||||||
|
input.treeGenStatusMsg = null;
|
||||||
|
ctx.setStatus().accept(treeMsg);
|
||||||
|
}
|
||||||
|
if (input.treePreviewResized) {
|
||||||
|
input.treePreviewResized = false;
|
||||||
|
if (treePreviewView != null) {
|
||||||
|
treePreviewView.setImage(input.treePreviewImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Vorschau-Panel ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private StackPane getPlantPreviewPanel() {
|
||||||
|
if (plantPreviewPanel != null) { return plantPreviewPanel; }
|
||||||
|
|
||||||
|
SharedInput input = ctx.input();
|
||||||
|
treePreviewView = new ImageView(input.treePreviewImage);
|
||||||
|
treePreviewView.setPreserveRatio(true);
|
||||||
|
treePreviewView.setSmooth(true);
|
||||||
|
|
||||||
|
StackPane pane = new StackPane(treePreviewView);
|
||||||
|
pane.setStyle("-fx-background-color: #1e2433;");
|
||||||
|
pane.widthProperty().addListener((o, ow, nw) -> {
|
||||||
|
treePreviewView.setFitWidth(nw.doubleValue());
|
||||||
|
input.treePreviewW = Math.max(64, nw.intValue());
|
||||||
|
});
|
||||||
|
pane.heightProperty().addListener((o, oh, nh) -> {
|
||||||
|
treePreviewView.setFitHeight(nh.doubleValue());
|
||||||
|
input.treePreviewH = Math.max(64, nh.intValue());
|
||||||
|
});
|
||||||
|
|
||||||
|
double[] prevMX = {0}, prevMY = {0};
|
||||||
|
pane.setOnMousePressed(e -> {
|
||||||
|
boolean mid = e.getButton() == MouseButton.MIDDLE;
|
||||||
|
boolean both = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
|
||||||
|
if (mid || both) { prevMX[0] = e.getX(); prevMY[0] = e.getY(); }
|
||||||
|
});
|
||||||
|
pane.setOnMouseDragged(e -> {
|
||||||
|
boolean both = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
|
||||||
|
if (e.isMiddleButtonDown() || both) {
|
||||||
|
double dx = e.getX() - prevMX[0];
|
||||||
|
double dy = e.getY() - prevMY[0];
|
||||||
|
input.treePreviewRotY += (float) (dx * 0.4);
|
||||||
|
input.treePreviewRotX = (float) Math.max(5.0, Math.min(80.0,
|
||||||
|
input.treePreviewRotX - dy * 0.3));
|
||||||
|
prevMX[0] = e.getX(); prevMY[0] = e.getY();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pane.setOnScroll(e -> {
|
||||||
|
if (e.getDeltaY() == 0) { return; }
|
||||||
|
float factor = e.getDeltaY() > 0 ? 0.88f : 1.14f;
|
||||||
|
input.treePreviewZoom = (float) Math.max(0.25, Math.min(4.0,
|
||||||
|
input.treePreviewZoom * factor));
|
||||||
|
e.consume();
|
||||||
|
});
|
||||||
|
pane.setFocusTraversable(true);
|
||||||
|
pane.setOnMouseClicked(e -> pane.requestFocus());
|
||||||
|
pane.setOnKeyPressed(e -> {
|
||||||
|
if (e.getCode() == javafx.scene.input.KeyCode.W) {
|
||||||
|
input.treePreviewWireframe = !input.treePreviewWireframe;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
plantPreviewPanel = pane;
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toolbar ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private ToolBar buildToolBar() {
|
||||||
|
Button backBtn = new Button("← Welteneditor");
|
||||||
|
backBtn.setOnAction(e -> ctx.switchToWorld().run());
|
||||||
|
|
||||||
|
Label typeLabel = new Label("Pflanzentyp:");
|
||||||
|
typeLabel.setStyle("-fx-font-weight: bold;");
|
||||||
|
ComboBox<String> typeBox = new ComboBox<>();
|
||||||
|
typeBox.getItems().addAll(
|
||||||
|
"Baum (Eiche)", "Baum (Birke)", "Baum (Kiefer)", "Baum (Weide)", "Baum (Busch)",
|
||||||
|
"Baum (Trauerweide)",
|
||||||
|
"Farn", "Palme", "Fruchtbusch", "Weinpflanze");
|
||||||
|
typeBox.setValue(vegetationType);
|
||||||
|
typeBox.setOnAction(e -> {
|
||||||
|
vegetationType = typeBox.getValue();
|
||||||
|
if (vegetationType.startsWith("Baum (") && !"Baum (Trauerweide)".equals(vegetationType)) {
|
||||||
|
currentTreePreset = vegetationType.substring(6, vegetationType.length() - 1);
|
||||||
|
treeParams = presetFromName(currentTreePreset);
|
||||||
|
}
|
||||||
|
updateOnF5();
|
||||||
|
ctx.setRightPanel().accept(buildParamsPanel());
|
||||||
|
onF5.run();
|
||||||
|
});
|
||||||
|
|
||||||
|
Button previewBtn = new Button("▶ Vorschau [F5]");
|
||||||
|
previewBtn.setStyle("-fx-font-weight: bold;");
|
||||||
|
previewBtn.setOnAction(e -> onF5.run());
|
||||||
|
|
||||||
|
Button exportBtn = new Button("💾 Export .j3o");
|
||||||
|
exportBtn.setOnAction(e -> {
|
||||||
|
SharedInput input = ctx.input();
|
||||||
|
if (vegetationType.startsWith("Baum (") && !"Baum (Trauerweide)".equals(vegetationType)) {
|
||||||
|
input.treeGenQueue.offer(new SharedInput.TreeGenRequest(
|
||||||
|
treeParams.copy(), true, treeTypeFromPreset(currentTreePreset)));
|
||||||
|
} else if ("Farn".equals(vegetationType)) {
|
||||||
|
input.fernGenQueue.offer(new SharedInput.FernGenRequest(fernOptions.copy(), true));
|
||||||
|
} else if ("Fruchtbusch".equals(vegetationType)) {
|
||||||
|
input.fruitBushGenQueue.offer(new SharedInput.FruitBushGenRequest(
|
||||||
|
fruitBushOptions.copy(), fruitBushVariant, true));
|
||||||
|
} else if ("Weinpflanze".equals(vegetationType)) {
|
||||||
|
input.grapevineGenQueue.offer(new SharedInput.GrapevineGenRequest(grapevineOptions.copy(), true));
|
||||||
|
} else if ("Baum (Trauerweide)".equals(vegetationType)) {
|
||||||
|
input.willowGenQueue.offer(new SharedInput.WillowGenRequest(willowOptions.copy(), true));
|
||||||
|
} else {
|
||||||
|
input.palmGenQueue.offer(new SharedInput.PalmGenRequest(palmOptions.copy(), true));
|
||||||
|
}
|
||||||
|
ctx.setStatus().accept("Generiere und exportiere…");
|
||||||
|
});
|
||||||
|
|
||||||
|
Button rndBtn = new Button("🎲 Neuer Seed");
|
||||||
|
rndBtn.setOnAction(e -> onF6.run());
|
||||||
|
|
||||||
|
ToolBar bar = new ToolBar();
|
||||||
|
bar.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL),
|
||||||
|
typeLabel, typeBox,
|
||||||
|
new Separator(Orientation.VERTICAL),
|
||||||
|
previewBtn, exportBtn, rndBtn);
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── F5 / F6-Callbacks ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void updateOnF5() {
|
||||||
|
SharedInput input = ctx.input();
|
||||||
|
if ("Baum (Trauerweide)".equals(vegetationType)) {
|
||||||
|
onF5 = () -> {
|
||||||
|
input.willowGenQueue.offer(new SharedInput.WillowGenRequest(willowOptions.copy(), false));
|
||||||
|
ctx.setStatus().accept("Trauerweide: generiere Vorschau…");
|
||||||
|
};
|
||||||
|
onF6 = () -> {
|
||||||
|
willowOptions.seed = new java.util.Random().nextInt(1000000);
|
||||||
|
ctx.setRightPanel().accept(buildParamsPanel());
|
||||||
|
onF5.run();
|
||||||
|
};
|
||||||
|
} else if (vegetationType.startsWith("Baum (")) {
|
||||||
|
onF5 = () -> {
|
||||||
|
input.treeGenQueue.offer(new SharedInput.TreeGenRequest(
|
||||||
|
treeParams.copy(), false, treeTypeFromPreset(currentTreePreset)));
|
||||||
|
ctx.setStatus().accept("Generiere Vorschau…");
|
||||||
|
};
|
||||||
|
onF6 = () -> {
|
||||||
|
treeParams.seed = new java.util.Random().nextInt(100000);
|
||||||
|
onF5.run();
|
||||||
|
};
|
||||||
|
} else if ("Farn".equals(vegetationType)) {
|
||||||
|
onF5 = () -> {
|
||||||
|
input.fernGenQueue.offer(new SharedInput.FernGenRequest(fernOptions.copy(), false));
|
||||||
|
ctx.setStatus().accept("Generiere Farn-Vorschau…");
|
||||||
|
};
|
||||||
|
onF6 = () -> {
|
||||||
|
fernOptions.seed = new java.util.Random().nextInt(1000000);
|
||||||
|
ctx.setRightPanel().accept(buildParamsPanel());
|
||||||
|
onF5.run();
|
||||||
|
};
|
||||||
|
} else if ("Fruchtbusch".equals(vegetationType)) {
|
||||||
|
onF5 = () -> {
|
||||||
|
input.fruitBushGenQueue.offer(new SharedInput.FruitBushGenRequest(
|
||||||
|
fruitBushOptions.copy(), fruitBushVariant, false));
|
||||||
|
ctx.setStatus().accept("Fruchtbusch: generiere Vorschau…");
|
||||||
|
};
|
||||||
|
onF6 = () -> {
|
||||||
|
fruitBushOptions.seed = new java.util.Random().nextInt(1000000);
|
||||||
|
ctx.setRightPanel().accept(buildParamsPanel());
|
||||||
|
onF5.run();
|
||||||
|
};
|
||||||
|
} else if ("Weinpflanze".equals(vegetationType)) {
|
||||||
|
onF5 = () -> {
|
||||||
|
input.grapevineGenQueue.offer(new SharedInput.GrapevineGenRequest(
|
||||||
|
grapevineOptions.copy(), false));
|
||||||
|
ctx.setStatus().accept("Weinpflanze: generiere Vorschau…");
|
||||||
|
};
|
||||||
|
onF6 = () -> {
|
||||||
|
grapevineOptions.seed = new java.util.Random().nextInt(1000000);
|
||||||
|
ctx.setRightPanel().accept(buildParamsPanel());
|
||||||
|
onF5.run();
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Palme
|
||||||
|
onF5 = () -> {
|
||||||
|
input.palmGenQueue.offer(new SharedInput.PalmGenRequest(palmOptions.copy(), false));
|
||||||
|
ctx.setStatus().accept("Palme: generiere Vorschau…");
|
||||||
|
};
|
||||||
|
onF6 = () -> {
|
||||||
|
palmOptions.seed = new java.util.Random().nextInt(1000000);
|
||||||
|
ctx.setRightPanel().accept(buildParamsPanel());
|
||||||
|
onF5.run();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Parameter-Panel (Dispatcher) ──────────────────────────────────────────
|
||||||
|
|
||||||
|
private javafx.scene.Node buildParamsPanel() {
|
||||||
|
if ("Baum (Trauerweide)".equals(vegetationType)) {
|
||||||
|
return buildWillowParamsPanel();
|
||||||
|
} else if (vegetationType.startsWith("Baum (")) {
|
||||||
|
return buildTreeParamsPanel();
|
||||||
|
} else if ("Farn".equals(vegetationType)) {
|
||||||
|
return buildFernParamsPanel();
|
||||||
|
} else if ("Fruchtbusch".equals(vegetationType)) {
|
||||||
|
return buildFruitBushParamsPanel();
|
||||||
|
} else if ("Weinpflanze".equals(vegetationType)) {
|
||||||
|
return buildGrapevineParamsPanel();
|
||||||
|
} else {
|
||||||
|
return buildPalmParamsPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Baum-Parameter-Panel ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private VBox buildTreeParamsPanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
Label title = new Label("Baum-Parameter");
|
||||||
|
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111111;");
|
||||||
|
inner.getChildren().addAll(title, new Separator());
|
||||||
|
|
||||||
|
inner.getChildren().add(bold("Zufallssamen:"));
|
||||||
|
Spinner<Integer> seedSpinner = new Spinner<>(0, 99999, treeParams.seed);
|
||||||
|
seedSpinner.setEditable(true);
|
||||||
|
seedSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
seedSpinner.valueProperty().addListener((o, a, b) -> treeParams.seed = b);
|
||||||
|
Button treeRndSeed = new Button("🎲");
|
||||||
|
onF6 = () -> {
|
||||||
|
int s = new java.util.Random().nextInt(100000);
|
||||||
|
treeParams.seed = s;
|
||||||
|
seedSpinner.getValueFactory().setValue(s);
|
||||||
|
onF5.run();
|
||||||
|
};
|
||||||
|
treeRndSeed.setOnAction(e -> onF6.run());
|
||||||
|
HBox treeSeedRow = new HBox(4, seedSpinner, treeRndSeed);
|
||||||
|
HBox.setHgrow(seedSpinner, Priority.ALWAYS);
|
||||||
|
inner.getChildren().add(treeSeedRow);
|
||||||
|
|
||||||
|
inner.getChildren().add(bold("Ast-Tiefe:"));
|
||||||
|
Spinner<Integer> levelSpinner = new Spinner<>(1, 4, treeParams.levels);
|
||||||
|
levelSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
levelSpinner.valueProperty().addListener((o, a, b) -> treeParams.levels = b);
|
||||||
|
inner.getChildren().add(levelSpinner);
|
||||||
|
|
||||||
|
inner.getChildren().add(paramSlider("Stamm-Aufrichtung:", 0.0, 0.40, treeParams.trunkUprightness,
|
||||||
|
v -> treeParams.trunkUprightness = v));
|
||||||
|
inner.getChildren().add(new Separator());
|
||||||
|
|
||||||
|
CheckBox leavesCheck = new CheckBox("Blätter generieren");
|
||||||
|
leavesCheck.setSelected(treeParams.generateLeaves);
|
||||||
|
leavesCheck.setOnAction(e -> treeParams.generateLeaves = leavesCheck.isSelected());
|
||||||
|
inner.getChildren().add(leavesCheck);
|
||||||
|
|
||||||
|
inner.getChildren().add(paramSlider("Blattgröße:", 0.3, 3.0, treeParams.leafScale,
|
||||||
|
v -> treeParams.leafScale = v));
|
||||||
|
|
||||||
|
inner.getChildren().add(bold("Blattanzahl:"));
|
||||||
|
Spinner<Integer> leafCountSpinner = new Spinner<>(1, 15, treeParams.leafCount);
|
||||||
|
leafCountSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
leafCountSpinner.valueProperty().addListener((o, a, b) -> treeParams.leafCount = b);
|
||||||
|
inner.getChildren().add(leafCountSpinner);
|
||||||
|
|
||||||
|
inner.getChildren().add(bold("Ast-Verzweigungen:"));
|
||||||
|
Spinner<Integer> leafBranchingsSpinner = new Spinner<>(0, 3, treeParams.leafBranchings);
|
||||||
|
leafBranchingsSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
leafBranchingsSpinner.valueProperty().addListener((o, a, b) -> treeParams.leafBranchings = b);
|
||||||
|
inner.getChildren().add(leafBranchingsSpinner);
|
||||||
|
|
||||||
|
inner.getChildren().add(new Separator());
|
||||||
|
inner.getChildren().add(bold("Wind:"));
|
||||||
|
inner.getChildren().add(paramSlider("Stamm:", 0.0, 0.5, treeParams.trunkFlexibility,
|
||||||
|
v -> treeParams.trunkFlexibility = v));
|
||||||
|
inner.getChildren().add(paramSlider("Spitzen:", 0.2, 1.0, treeParams.branchFlexibility,
|
||||||
|
v -> treeParams.branchFlexibility = v));
|
||||||
|
|
||||||
|
return wrapInPanel(inner, 260);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Farn-Parameter-Panel ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private VBox buildFernParamsPanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
|
||||||
|
inner.getChildren().add(bold("Zufallssamen:"));
|
||||||
|
Spinner<Integer> seedSp = intSpinner(0, 999999, fernOptions.seed);
|
||||||
|
seedSp.valueProperty().addListener((o, a, b) -> fernOptions.seed = b);
|
||||||
|
Button rndSeedBtn = new Button("🎲");
|
||||||
|
rndSeedBtn.setOnAction(e -> {
|
||||||
|
int s = new java.util.Random().nextInt(1000000);
|
||||||
|
fernOptions.seed = s;
|
||||||
|
seedSp.getValueFactory().setValue(s);
|
||||||
|
onF5.run();
|
||||||
|
});
|
||||||
|
HBox seedRow = new HBox(4, seedSp, rndSeedBtn);
|
||||||
|
HBox.setHgrow(seedSp, Priority.ALWAYS);
|
||||||
|
inner.getChildren().add(seedRow);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Fächer"), new Separator());
|
||||||
|
inner.getChildren().add(bold("Anzahl Fächer:"));
|
||||||
|
Spinner<Integer> frondCountSp = intSpinner(3, 30, fernOptions.frondCount);
|
||||||
|
frondCountSp.valueProperty().addListener((o, a, b) -> fernOptions.frondCount = b);
|
||||||
|
inner.getChildren().add(frondCountSp);
|
||||||
|
|
||||||
|
inner.getChildren().add(ezFloat("Länge (m):", 0.2, 2.0, fernOptions.frondLength,
|
||||||
|
v -> fernOptions.frondLength = v));
|
||||||
|
inner.getChildren().add(ezFloat("Neigung min (°):", 0.0, 90.0, fernOptions.tiltMin,
|
||||||
|
v -> fernOptions.tiltMin = v));
|
||||||
|
inner.getChildren().add(ezFloat("Neigung max (°):", 0.0, 90.0, fernOptions.tiltMax,
|
||||||
|
v -> fernOptions.tiltMax = v));
|
||||||
|
inner.getChildren().add(ezFloat("Durchhang:", 0.0, 1.0, fernOptions.droop,
|
||||||
|
v -> fernOptions.droop = v));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Segmente & Wind"), new Separator());
|
||||||
|
inner.getChildren().add(bold("Segmente pro Fächer:"));
|
||||||
|
Spinner<Integer> segSp = intSpinner(2, 12, fernOptions.frondSegments);
|
||||||
|
segSp.valueProperty().addListener((o, a, b) -> fernOptions.frondSegments = b);
|
||||||
|
inner.getChildren().add(segSp);
|
||||||
|
|
||||||
|
inner.getChildren().add(ezFloat("Windstärke:", 0.0, 0.5, fernOptions.windStrength,
|
||||||
|
v -> fernOptions.windStrength = v));
|
||||||
|
inner.getChildren().add(ezFloat("Windgeschwindigkeit:", 0.1, 2.0, fernOptions.windSpeed,
|
||||||
|
v -> fernOptions.windSpeed = v));
|
||||||
|
|
||||||
|
Button previewBtn = new Button("▶ Vorschau");
|
||||||
|
previewBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
previewBtn.setStyle("-fx-background-color:#2d8a3e;-fx-text-fill:white;-fx-font-weight:bold;-fx-padding:6 12 6 12;");
|
||||||
|
previewBtn.setOnAction(e -> onF5.run());
|
||||||
|
|
||||||
|
inner.getChildren().add(new Separator());
|
||||||
|
inner.getChildren().add(previewBtn);
|
||||||
|
|
||||||
|
return wrapInPanel(inner, 270);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fruchtbusch-Parameter-Panel ───────────────────────────────────────────
|
||||||
|
|
||||||
|
private VBox buildFruitBushParamsPanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Fruchtbusch-Typ"), new Separator());
|
||||||
|
ComboBox<String> variantBox = new ComboBox<>();
|
||||||
|
variantBox.getItems().addAll("Orange", "Zitrone", "Aprikose");
|
||||||
|
variantBox.setValue(fruitBushVariant);
|
||||||
|
variantBox.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
variantBox.setOnAction(e -> {
|
||||||
|
fruitBushVariant = variantBox.getValue();
|
||||||
|
int oldSeed = fruitBushOptions.seed;
|
||||||
|
fruitBushOptions = switch (fruitBushVariant) {
|
||||||
|
case "Zitrone" -> FruitBushOptions.lemon();
|
||||||
|
case "Aprikose" -> FruitBushOptions.apricot();
|
||||||
|
default -> FruitBushOptions.orange();
|
||||||
|
};
|
||||||
|
fruitBushOptions.seed = oldSeed;
|
||||||
|
ctx.setRightPanel().accept(buildParamsPanel());
|
||||||
|
onF5.run();
|
||||||
|
});
|
||||||
|
inner.getChildren().add(variantBox);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
|
||||||
|
inner.getChildren().add(bold("Zufallssamen:"));
|
||||||
|
Spinner<Integer> seedSp = intSpinner(0, 999999, fruitBushOptions.seed);
|
||||||
|
seedSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.seed = b);
|
||||||
|
Button rndSeedBtn = new Button("🎲");
|
||||||
|
rndSeedBtn.setOnAction(e -> {
|
||||||
|
int s = new java.util.Random().nextInt(1000000);
|
||||||
|
fruitBushOptions.seed = s;
|
||||||
|
seedSp.getValueFactory().setValue(s);
|
||||||
|
onF5.run();
|
||||||
|
});
|
||||||
|
HBox seedRow = new HBox(4, seedSp, rndSeedBtn);
|
||||||
|
HBox.setHgrow(seedSp, Priority.ALWAYS);
|
||||||
|
inner.getChildren().add(seedRow);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Stamm"), new Separator());
|
||||||
|
inner.getChildren().add(ezFloat("Höhe (m):", 0.2, 1.5, fruitBushOptions.trunkHeight,
|
||||||
|
v -> fruitBushOptions.trunkHeight = v));
|
||||||
|
inner.getChildren().add(ezFloat("Radius (m):", 0.01, 0.15, fruitBushOptions.trunkRadius,
|
||||||
|
v -> fruitBushOptions.trunkRadius = v));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Äste"), new Separator());
|
||||||
|
inner.getChildren().add(bold("Anzahl Primäräste:"));
|
||||||
|
Spinner<Integer> branchCountSp = intSpinner(2, 10, fruitBushOptions.branchCount);
|
||||||
|
branchCountSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.branchCount = b);
|
||||||
|
inner.getChildren().add(branchCountSp);
|
||||||
|
|
||||||
|
inner.getChildren().add(ezFloat("Winkel (°):", 10, 80, fruitBushOptions.branchAngle,
|
||||||
|
v -> fruitBushOptions.branchAngle = v));
|
||||||
|
inner.getChildren().add(ezFloat("Länge (m):", 0.2, 2.0, fruitBushOptions.branchLength,
|
||||||
|
v -> fruitBushOptions.branchLength = v));
|
||||||
|
|
||||||
|
inner.getChildren().add(bold("Reiser pro Ast:"));
|
||||||
|
Spinner<Integer> twigCountSp = intSpinner(1, 8, fruitBushOptions.twigCount);
|
||||||
|
twigCountSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.twigCount = b);
|
||||||
|
inner.getChildren().add(twigCountSp);
|
||||||
|
|
||||||
|
inner.getChildren().add(ezFloat("Reiser-Winkel (°):", 10, 80, fruitBushOptions.twigAngle,
|
||||||
|
v -> fruitBushOptions.twigAngle = v));
|
||||||
|
inner.getChildren().add(ezFloat("Reiser-Länge (m):", 0.1, 1.2, fruitBushOptions.twigLength,
|
||||||
|
v -> fruitBushOptions.twigLength = v));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Blätter & Früchte"), new Separator());
|
||||||
|
inner.getChildren().add(ezFloat("Blattgröße (m):", 0.05, 0.6, fruitBushOptions.leafSize,
|
||||||
|
v -> fruitBushOptions.leafSize = v));
|
||||||
|
inner.getChildren().add(bold("Blätter pro Reiser:"));
|
||||||
|
Spinner<Integer> leafCountSp = intSpinner(2, 20, fruitBushOptions.leafCount);
|
||||||
|
leafCountSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.leafCount = b);
|
||||||
|
inner.getChildren().add(leafCountSp);
|
||||||
|
|
||||||
|
inner.getChildren().add(ezFloat("Frucht-Dichte [0–1]:", 0.0, 1.0, fruitBushOptions.fruitDensity,
|
||||||
|
v -> fruitBushOptions.fruitDensity = v));
|
||||||
|
inner.getChildren().add(ezFloat("Frucht-Radius (m):", 0.02, 0.15, fruitBushOptions.fruitRadius,
|
||||||
|
v -> fruitBushOptions.fruitRadius = v));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Wind"), new Separator());
|
||||||
|
inner.getChildren().add(ezFloat("Windstärke:", 0.0, 0.5, fruitBushOptions.windStrength,
|
||||||
|
v -> fruitBushOptions.windStrength = v));
|
||||||
|
inner.getChildren().add(ezFloat("Windgeschwindigkeit:", 0.1, 2.0, fruitBushOptions.windSpeed,
|
||||||
|
v -> fruitBushOptions.windSpeed = v));
|
||||||
|
|
||||||
|
Button previewBtn = new Button("▶ Vorschau");
|
||||||
|
previewBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
previewBtn.setStyle("-fx-background-color:#2d8a3e;-fx-text-fill:white;-fx-font-weight:bold;-fx-padding:6 12 6 12;");
|
||||||
|
previewBtn.setOnAction(e -> onF5.run());
|
||||||
|
inner.getChildren().addAll(new Separator(), previewBtn);
|
||||||
|
|
||||||
|
return wrapInPanel(inner, 270);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Weinpflanzen-Parameter-Panel ──────────────────────────────────────────
|
||||||
|
|
||||||
|
private VBox buildGrapevineParamsPanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
|
||||||
|
inner.getChildren().add(bold("Zufallssamen:"));
|
||||||
|
Spinner<Integer> seedSp = intSpinner(0, 999999, grapevineOptions.seed);
|
||||||
|
seedSp.valueProperty().addListener((o, a, b) -> grapevineOptions.seed = b);
|
||||||
|
Button rndSeedBtn = new Button("🎲");
|
||||||
|
rndSeedBtn.setOnAction(e -> {
|
||||||
|
int s = new java.util.Random().nextInt(1000000);
|
||||||
|
grapevineOptions.seed = s;
|
||||||
|
seedSp.getValueFactory().setValue(s);
|
||||||
|
onF5.run();
|
||||||
|
});
|
||||||
|
HBox seedRow = new HBox(4, seedSp, rndSeedBtn);
|
||||||
|
HBox.setHgrow(seedSp, Priority.ALWAYS);
|
||||||
|
inner.getChildren().add(seedRow);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Rahmen"), new Separator());
|
||||||
|
inner.getChildren().add(ezFloat("Gesamtbreite (m):", 2.0, 10.0, grapevineOptions.totalWidth,
|
||||||
|
v -> grapevineOptions.totalWidth = v));
|
||||||
|
inner.getChildren().add(ezFloat("Gesamthöhe (m):", 1.5, 4.0, grapevineOptions.totalHeight,
|
||||||
|
v -> grapevineOptions.totalHeight = v));
|
||||||
|
inner.getChildren().add(ezFloat("1. Draht-Höhe (m):", 0.5, 2.0, grapevineOptions.firstWireH,
|
||||||
|
v -> grapevineOptions.firstWireH = v));
|
||||||
|
inner.getChildren().add(bold("Draht-Anzahl:"));
|
||||||
|
Spinner<Integer> wireCountSp = intSpinner(2, 6, grapevineOptions.wireCount);
|
||||||
|
wireCountSp.valueProperty().addListener((o, a, b) -> grapevineOptions.wireCount = b);
|
||||||
|
inner.getChildren().add(wireCountSp);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Triebe & Blätter"), new Separator());
|
||||||
|
inner.getChildren().add(bold("Triebe pro Seite:"));
|
||||||
|
Spinner<Integer> shootCountSp = intSpinner(2, 14, grapevineOptions.shootCount);
|
||||||
|
shootCountSp.valueProperty().addListener((o, a, b) -> grapevineOptions.shootCount = b);
|
||||||
|
inner.getChildren().add(shootCountSp);
|
||||||
|
inner.getChildren().add(ezFloat("Blattgröße (m):", 0.10, 0.60, grapevineOptions.leafSize,
|
||||||
|
v -> grapevineOptions.leafSize = v));
|
||||||
|
inner.getChildren().add(bold("Blätter pro Trieb:"));
|
||||||
|
Spinner<Integer> leafCountSp = intSpinner(2, 128, grapevineOptions.leafCount);
|
||||||
|
leafCountSp.valueProperty().addListener((o, a, b) -> grapevineOptions.leafCount = b);
|
||||||
|
inner.getChildren().add(leafCountSp);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Wind"), new Separator());
|
||||||
|
inner.getChildren().add(ezFloat("Windstärke:", 0.0, 0.5, grapevineOptions.windStrength,
|
||||||
|
v -> grapevineOptions.windStrength = v));
|
||||||
|
inner.getChildren().add(ezFloat("Windgeschwindigkeit:", 0.1, 2.0, grapevineOptions.windSpeed,
|
||||||
|
v -> grapevineOptions.windSpeed = v));
|
||||||
|
|
||||||
|
Button previewBtn = new Button("▶ Vorschau");
|
||||||
|
previewBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
previewBtn.setStyle("-fx-background-color:#2d8a3e;-fx-text-fill:white;-fx-font-weight:bold;-fx-padding:6 12 6 12;");
|
||||||
|
previewBtn.setOnAction(e -> onF5.run());
|
||||||
|
|
||||||
|
inner.getChildren().add(new Separator());
|
||||||
|
inner.getChildren().add(previewBtn);
|
||||||
|
|
||||||
|
return wrapInPanel(inner, 270);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Trauerweide-Parameter-Panel ───────────────────────────────────────────
|
||||||
|
|
||||||
|
private VBox buildWillowParamsPanel() {
|
||||||
|
VBox inner = new VBox(6);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
|
||||||
|
inner.getChildren().add(bold("Zufallssamen:"));
|
||||||
|
Spinner<Integer> seedSp = intSpinner(0, 999999, willowOptions.seed);
|
||||||
|
seedSp.valueProperty().addListener((o, a, b) -> willowOptions.seed = b);
|
||||||
|
Button rndSeed = new Button("🎲");
|
||||||
|
onF6 = () -> {
|
||||||
|
int s = new java.util.Random().nextInt(1000000);
|
||||||
|
willowOptions.seed = s;
|
||||||
|
seedSp.getValueFactory().setValue(s);
|
||||||
|
onF5.run();
|
||||||
|
};
|
||||||
|
rndSeed.setOnAction(e -> onF6.run());
|
||||||
|
HBox seedRow = new HBox(4, seedSp, rndSeed);
|
||||||
|
HBox.setHgrow(seedSp, Priority.ALWAYS);
|
||||||
|
inner.getChildren().add(seedRow);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Stamm"), new Separator());
|
||||||
|
inner.getChildren().add(ezFloat("Höhe:", 4, 25, willowOptions.trunkHeight,
|
||||||
|
v -> willowOptions.trunkHeight = v));
|
||||||
|
inner.getChildren().add(ezFloat("Radius:", 0.1, 1.0, willowOptions.trunkRadius,
|
||||||
|
v -> willowOptions.trunkRadius = v));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Hauptäste"), new Separator());
|
||||||
|
inner.getChildren().add(bold("Anzahl Äste:"));
|
||||||
|
Spinner<Integer> branchCountSp = intSpinner(3, 16, willowOptions.branchCount);
|
||||||
|
branchCountSp.valueProperty().addListener((o, a, b) -> willowOptions.branchCount = b);
|
||||||
|
inner.getChildren().add(branchCountSp);
|
||||||
|
inner.getChildren().add(ezFloat("Startwinkel (°):", 10, 80, willowOptions.branchStartAngle,
|
||||||
|
v -> willowOptions.branchStartAngle = v));
|
||||||
|
inner.getChildren().add(ezFloat("Endwinkel / Hänge (°):", 90, 170, willowOptions.branchEndAngle,
|
||||||
|
v -> willowOptions.branchEndAngle = v));
|
||||||
|
inner.getChildren().add(ezFloat("Winkel-Varianz (°):", 0, 40, willowOptions.branchVariance,
|
||||||
|
v -> willowOptions.branchVariance = v));
|
||||||
|
inner.getChildren().add(ezFloat("Länge:", 3, 18, willowOptions.branchLength,
|
||||||
|
v -> willowOptions.branchLength = v));
|
||||||
|
inner.getChildren().add(ezFloat("Radius:", 0.05, 0.5, willowOptions.branchRadius,
|
||||||
|
v -> willowOptions.branchRadius = v));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Hängeäste"), new Separator());
|
||||||
|
inner.getChildren().add(bold("Anzahl pro Sektion:"));
|
||||||
|
Spinner<Integer> subCountSp = intSpinner(1, 8, willowOptions.subBranchCount);
|
||||||
|
subCountSp.valueProperty().addListener((o, a, b) -> willowOptions.subBranchCount = b);
|
||||||
|
inner.getChildren().add(subCountSp);
|
||||||
|
inner.getChildren().add(ezFloat("Länge:", 1, 10, willowOptions.subBranchLength,
|
||||||
|
v -> willowOptions.subBranchLength = v));
|
||||||
|
inner.getChildren().add(ezFloat("Startwinkel (Grad):", 10, 90, willowOptions.subBranchStartAngle,
|
||||||
|
v -> willowOptions.subBranchStartAngle = v));
|
||||||
|
inner.getChildren().add(ezFloat("Endwinkel (Grad):", 91, 180, willowOptions.subBranchEndAngle,
|
||||||
|
v -> willowOptions.subBranchEndAngle = v));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Blätter"), new Separator());
|
||||||
|
inner.getChildren().add(ezFloat("Größe:", 0.3, 3.0, willowOptions.leafScale,
|
||||||
|
v -> willowOptions.leafScale = v));
|
||||||
|
inner.getChildren().add(bold("Anzahl pro Cluster:"));
|
||||||
|
Spinner<Integer> leafCountSp = intSpinner(1, 12, willowOptions.leafCount);
|
||||||
|
leafCountSp.valueProperty().addListener((o, a, b) -> willowOptions.leafCount = b);
|
||||||
|
inner.getChildren().add(leafCountSp);
|
||||||
|
inner.getChildren().add(ezFloat("Blatt-Hänge:", 0.0, 1.0, willowOptions.leafDroop,
|
||||||
|
v -> willowOptions.leafDroop = v));
|
||||||
|
inner.getChildren().add(ezFloat("Blatt-Startwinkel min (°):", 0, 89, willowOptions.leafAngleMin,
|
||||||
|
v -> willowOptions.leafAngleMin = v));
|
||||||
|
inner.getChildren().add(ezFloat("Blatt-Startwinkel max (°):", 1, 90, willowOptions.leafAngleMax,
|
||||||
|
v -> willowOptions.leafAngleMax = v));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Farben & Texturen"), new Separator());
|
||||||
|
Label leafTexLabel = new Label("Blatt-Textur:");
|
||||||
|
leafTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
|
||||||
|
ComboBox<String> leafTexBox = new ComboBox<>();
|
||||||
|
leafTexBox.getItems().addAll("weeping_willow.png", "ash.png", "oak.png");
|
||||||
|
String curLeaf = willowOptions.leafTexture != null
|
||||||
|
? willowOptions.leafTexture.substring(willowOptions.leafTexture.lastIndexOf('/') + 1)
|
||||||
|
: "weeping_willow.png";
|
||||||
|
leafTexBox.setValue(leafTexBox.getItems().contains(curLeaf) ? curLeaf : "weeping_willow.png");
|
||||||
|
leafTexBox.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
leafTexBox.setOnAction(e -> willowOptions.leafTexture =
|
||||||
|
"Textures/internal/foliage/" + leafTexBox.getValue());
|
||||||
|
inner.getChildren().add(new VBox(2, leafTexLabel, leafTexBox));
|
||||||
|
|
||||||
|
Label barkTexLabel = new Label("Rinden-Textur:");
|
||||||
|
barkTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
|
||||||
|
ComboBox<String> barkTexBox = new ComboBox<>();
|
||||||
|
barkTexBox.getItems().addAll(
|
||||||
|
"Bark001_Color.jpg", "Bark002_Color.jpg", "Bark003_Color.jpg", "Bark008_Color.jpg");
|
||||||
|
String curBark = willowOptions.barkTexture != null
|
||||||
|
? willowOptions.barkTexture.substring(willowOptions.barkTexture.lastIndexOf('/') + 1)
|
||||||
|
: "Bark001_Color.jpg";
|
||||||
|
barkTexBox.setValue(barkTexBox.getItems().contains(curBark) ? curBark : "Bark001_Color.jpg");
|
||||||
|
barkTexBox.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
barkTexBox.setOnAction(e -> {
|
||||||
|
if (barkTexBox.getValue() != null) {
|
||||||
|
willowOptions.barkTexture = "Textures/internal/bark/" + barkTexBox.getValue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
inner.getChildren().add(new VBox(2, barkTexLabel, barkTexBox));
|
||||||
|
|
||||||
|
inner.getChildren().add(ezFloat("Rinde R:", 0, 1, willowOptions.barkR, v -> willowOptions.barkR = v));
|
||||||
|
inner.getChildren().add(ezFloat("Rinde G:", 0, 1, willowOptions.barkG, v -> willowOptions.barkG = v));
|
||||||
|
inner.getChildren().add(ezFloat("Rinde B:", 0, 1, willowOptions.barkB, v -> willowOptions.barkB = v));
|
||||||
|
inner.getChildren().add(ezFloat("Blatt R:", 0, 1, willowOptions.leafR, v -> willowOptions.leafR = v));
|
||||||
|
inner.getChildren().add(ezFloat("Blatt G:", 0, 1, willowOptions.leafG, v -> willowOptions.leafG = v));
|
||||||
|
inner.getChildren().add(ezFloat("Blatt B:", 0, 1, willowOptions.leafB, v -> willowOptions.leafB = v));
|
||||||
|
|
||||||
|
return wrapInPanel(inner, 270);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Palmen-Parameter-Panel ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private VBox buildPalmParamsPanel() {
|
||||||
|
VBox inner = new VBox(6);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
|
||||||
|
inner.getChildren().add(bold("Zufallssamen:"));
|
||||||
|
Spinner<Integer> seedSp = intSpinner(0, 999999, palmOptions.seed);
|
||||||
|
seedSp.valueProperty().addListener((o, a, b) -> palmOptions.seed = b);
|
||||||
|
Button palmRndSeed = new Button("🎲");
|
||||||
|
onF6 = () -> {
|
||||||
|
int s = new java.util.Random().nextInt(1000000);
|
||||||
|
palmOptions.seed = s;
|
||||||
|
seedSp.getValueFactory().setValue(s);
|
||||||
|
onF5.run();
|
||||||
|
};
|
||||||
|
palmRndSeed.setOnAction(e -> onF6.run());
|
||||||
|
HBox palmSeedRow = new HBox(4, seedSp, palmRndSeed);
|
||||||
|
HBox.setHgrow(seedSp, Priority.ALWAYS);
|
||||||
|
inner.getChildren().add(palmSeedRow);
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Stamm"), new Separator());
|
||||||
|
inner.getChildren().add(bold("Anzahl Stämme:"));
|
||||||
|
Spinner<Integer> palmCountSp = intSpinner(1, 3, palmOptions.palmCount);
|
||||||
|
palmCountSp.valueProperty().addListener((o, a, b) -> palmOptions.palmCount = b);
|
||||||
|
inner.getChildren().add(palmCountSp);
|
||||||
|
inner.getChildren().add(ezFloat("Höhe:", 5, 25, palmOptions.trunkHeight,
|
||||||
|
v -> palmOptions.trunkHeight = v));
|
||||||
|
inner.getChildren().add(ezFloat("Radius unten:", 0.1, 1.0, palmOptions.trunkRadiusBottom,
|
||||||
|
v -> palmOptions.trunkRadiusBottom = v));
|
||||||
|
inner.getChildren().add(ezFloat("Radius oben:", 0.05, 0.9, palmOptions.trunkRadiusTop,
|
||||||
|
v -> palmOptions.trunkRadiusTop = v));
|
||||||
|
inner.getChildren().add(ezFloat("Neigung (°):", 0.0, 50.0, palmOptions.lean,
|
||||||
|
v -> palmOptions.lean = v));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Kronenschaft"), new Separator());
|
||||||
|
inner.getChildren().add(ezFloat("Höhe:", 0.2, 4.0, palmOptions.crownHeight,
|
||||||
|
v -> palmOptions.crownHeight = v));
|
||||||
|
inner.getChildren().add(ezFloat("Aufweitung:", 1.0, 3.0, palmOptions.crownFlare,
|
||||||
|
v -> palmOptions.crownFlare = v));
|
||||||
|
inner.getChildren().add(ezFloat("Farbe R:", 0, 1, palmOptions.crownR, v -> palmOptions.crownR = v));
|
||||||
|
inner.getChildren().add(ezFloat("Farbe G:", 0, 1, palmOptions.crownG, v -> palmOptions.crownG = v));
|
||||||
|
inner.getChildren().add(ezFloat("Farbe B:", 0, 1, palmOptions.crownB, v -> palmOptions.crownB = v));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Wedel"), new Separator());
|
||||||
|
inner.getChildren().add(bold("Anzahl:"));
|
||||||
|
Spinner<Integer> frondCountSp = intSpinner(12, 120, palmOptions.frondCount);
|
||||||
|
frondCountSp.valueProperty().addListener((o, a, b) -> palmOptions.frondCount = b);
|
||||||
|
inner.getChildren().add(frondCountSp);
|
||||||
|
inner.getChildren().add(ezFloat("Winkel min (° von oben):", 10, 90, palmOptions.frondAngleMin,
|
||||||
|
v -> palmOptions.frondAngleMin = v));
|
||||||
|
inner.getChildren().add(ezFloat("Winkel max (° von oben):", 90, 170, palmOptions.frondAngleMax,
|
||||||
|
v -> palmOptions.frondAngleMax = v));
|
||||||
|
inner.getChildren().add(ezFloat("Länge:", 2, 12, palmOptions.frondLength,
|
||||||
|
v -> palmOptions.frondLength = v));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Blätter"), new Separator());
|
||||||
|
inner.getChildren().add(ezFloat("Schwerkraft:", 0.0, 2.0, palmOptions.gravity,
|
||||||
|
v -> palmOptions.gravity = v));
|
||||||
|
inner.getChildren().add(ezFloat("Größe oben (relativ):", 0.0, 1.0, palmOptions.frondMinSizeRatio,
|
||||||
|
v -> palmOptions.frondMinSizeRatio = v));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(sectionTitle("Farben"), new Separator());
|
||||||
|
Label leafTexLabel = new Label("Blatt-Textur:");
|
||||||
|
leafTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
|
||||||
|
ComboBox<String> leafTexBox = new ComboBox<>();
|
||||||
|
leafTexBox.getItems().addAll("palm.png", "palm2.png");
|
||||||
|
String currentLeaf = palmOptions.leafTexture != null
|
||||||
|
&& palmOptions.leafTexture.contains("palm2") ? "palm2.png" : "palm.png";
|
||||||
|
leafTexBox.setValue(currentLeaf);
|
||||||
|
leafTexBox.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
leafTexBox.setOnAction(e -> palmOptions.leafTexture =
|
||||||
|
"Textures/internal/foliage/" + leafTexBox.getValue());
|
||||||
|
inner.getChildren().add(new VBox(2, leafTexLabel, leafTexBox));
|
||||||
|
|
||||||
|
Label barkTexLabel = new Label("Rinden-Textur:");
|
||||||
|
barkTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
|
||||||
|
ComboBox<String> barkTexBox = new ComboBox<>();
|
||||||
|
barkTexBox.getItems().addAll(
|
||||||
|
"Bark_Palm.png", "Bark_Palm2.png",
|
||||||
|
"Bark001_Color.jpg", "Bark002_Color.jpg",
|
||||||
|
"Bark003_Color.jpg", "Bark008_Color.jpg"
|
||||||
|
);
|
||||||
|
String currentBark = palmOptions.barkTexture != null
|
||||||
|
? palmOptions.barkTexture.substring(palmOptions.barkTexture.lastIndexOf('/') + 1)
|
||||||
|
: "Bark_Palm.png";
|
||||||
|
barkTexBox.setValue(barkTexBox.getItems().contains(currentBark)
|
||||||
|
? currentBark : barkTexBox.getItems().get(0));
|
||||||
|
barkTexBox.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
barkTexBox.setOnAction(e -> {
|
||||||
|
if (barkTexBox.getValue() != null) {
|
||||||
|
palmOptions.barkTexture = "Textures/internal/bark/" + barkTexBox.getValue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
inner.getChildren().add(new VBox(2, barkTexLabel, barkTexBox));
|
||||||
|
|
||||||
|
inner.getChildren().add(ezFloat("Stamm R:", 0, 1, palmOptions.barkR, v -> palmOptions.barkR = v));
|
||||||
|
inner.getChildren().add(ezFloat("Stamm G:", 0, 1, palmOptions.barkG, v -> palmOptions.barkG = v));
|
||||||
|
inner.getChildren().add(ezFloat("Stamm B:", 0, 1, palmOptions.barkB, v -> palmOptions.barkB = v));
|
||||||
|
inner.getChildren().add(ezFloat("Blatt R:", 0, 1, palmOptions.leafR, v -> palmOptions.leafR = v));
|
||||||
|
inner.getChildren().add(ezFloat("Blatt G:", 0, 1, palmOptions.leafG, v -> palmOptions.leafG = v));
|
||||||
|
inner.getChildren().add(ezFloat("Blatt B:", 0, 1, palmOptions.leafB, v -> palmOptions.leafB = v));
|
||||||
|
|
||||||
|
return wrapInPanel(inner, 270);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static TreeParams presetFromName(String name) {
|
||||||
|
return switch (name) {
|
||||||
|
case "Birke" -> TreeParams.birch();
|
||||||
|
case "Kiefer" -> TreeParams.pine();
|
||||||
|
case "Weide" -> TreeParams.willow();
|
||||||
|
case "Busch" -> TreeParams.bush();
|
||||||
|
default -> TreeParams.oak();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String treeTypeFromPreset(String preset) {
|
||||||
|
return switch (preset) {
|
||||||
|
case "Birke" -> "birch";
|
||||||
|
case "Kiefer" -> "pine";
|
||||||
|
case "Weide" -> "willow";
|
||||||
|
case "Busch" -> "bush";
|
||||||
|
default -> "oak";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VBox wrapInPanel(VBox inner, int prefWidth) {
|
||||||
|
ScrollPane scroll = new ScrollPane(inner);
|
||||||
|
scroll.setFitToWidth(true);
|
||||||
|
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||||
|
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
|
||||||
|
VBox panel = new VBox(scroll);
|
||||||
|
VBox.setVgrow(scroll, Priority.ALWAYS);
|
||||||
|
panel.setPrefWidth(prefWidth);
|
||||||
|
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
package de.blight.editor.toolset.viewport;
|
||||||
|
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
import de.blight.editor.ToolContext;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.image.WritableImage;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
|
public class ViewportController {
|
||||||
|
|
||||||
|
private final SharedInput input;
|
||||||
|
private final WritableImage initialImage;
|
||||||
|
private final Runnable scheduleAutoSave;
|
||||||
|
|
||||||
|
private ImageView viewport;
|
||||||
|
private javafx.scene.canvas.Canvas compassCanvas;
|
||||||
|
|
||||||
|
private double prevDragX, prevDragY;
|
||||||
|
private double objDragPrevX, objDragPrevY;
|
||||||
|
private boolean objDragging = false;
|
||||||
|
private double editPressX, editPressY;
|
||||||
|
private int editPressAction;
|
||||||
|
private javafx.animation.Timeline editTimer;
|
||||||
|
|
||||||
|
public ViewportController(SharedInput input, WritableImage jfxImage, Runnable scheduleAutoSave) {
|
||||||
|
this.input = input;
|
||||||
|
this.initialImage = jfxImage;
|
||||||
|
this.scheduleAutoSave = scheduleAutoSave;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageView getViewport() { return viewport; }
|
||||||
|
|
||||||
|
public void setImage(WritableImage img) {
|
||||||
|
if (viewport != null) { viewport.setImage(img); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public StackPane buildViewport() {
|
||||||
|
viewport = new ImageView(initialImage);
|
||||||
|
viewport.setPreserveRatio(false);
|
||||||
|
viewport.setFocusTraversable(true);
|
||||||
|
|
||||||
|
compassCanvas = new javafx.scene.canvas.Canvas(100, 100);
|
||||||
|
compassCanvas.setMouseTransparent(true);
|
||||||
|
StackPane.setAlignment(compassCanvas, javafx.geometry.Pos.BOTTOM_RIGHT);
|
||||||
|
StackPane.setMargin(compassCanvas, new Insets(0, 10, 10, 0));
|
||||||
|
drawCompass();
|
||||||
|
|
||||||
|
StackPane pane = new StackPane(viewport, compassCanvas);
|
||||||
|
pane.setStyle("-fx-background-color: #1a1a2e;");
|
||||||
|
|
||||||
|
javafx.animation.PauseTransition resizeDebounce =
|
||||||
|
new javafx.animation.PauseTransition(javafx.util.Duration.millis(150));
|
||||||
|
resizeDebounce.setOnFinished(ev -> {
|
||||||
|
int w = (int) pane.getWidth();
|
||||||
|
int h = (int) pane.getHeight();
|
||||||
|
if (w > 0 && h > 0) { input.resizeRequest.set(new int[]{w, h}); }
|
||||||
|
});
|
||||||
|
|
||||||
|
pane.widthProperty().addListener((o, oldW, newW) -> {
|
||||||
|
viewport.setFitWidth(newW.doubleValue());
|
||||||
|
resizeDebounce.playFromStart();
|
||||||
|
});
|
||||||
|
pane.heightProperty().addListener((o, oldH, newH) -> {
|
||||||
|
viewport.setFitHeight(newH.doubleValue());
|
||||||
|
resizeDebounce.playFromStart();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Camera-Rotation: Filter am StackPane (Capture-Phase) → feuert immer,
|
||||||
|
// auch wenn Overlay-Nodes (Tab-Button, Asset-Panel) das Event abfangen würden.
|
||||||
|
pane.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_PRESSED, e -> {
|
||||||
|
if (e.getButton() == MouseButton.MIDDLE
|
||||||
|
|| (e.isPrimaryButtonDown() && e.isSecondaryButtonDown())
|
||||||
|
|| (e.getButton() == MouseButton.PRIMARY && e.isAltDown())) {
|
||||||
|
prevDragX = e.getX(); prevDragY = e.getY();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pane.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_DRAGGED, e -> {
|
||||||
|
boolean bothDown = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
|
||||||
|
boolean altLeft = e.isPrimaryButtonDown() && e.isAltDown();
|
||||||
|
if (e.isMiddleButtonDown() || bothDown || altLeft) {
|
||||||
|
double dx = e.getX() - prevDragX;
|
||||||
|
double dy = e.getY() - prevDragY;
|
||||||
|
input.addMouseDelta((int) dx, (int) dy);
|
||||||
|
prevDragX = e.getX(); prevDragY = e.getY();
|
||||||
|
if (bothDown) { stopEditTimer(); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
viewport.setOnMousePressed(e -> {
|
||||||
|
viewport.requestFocus();
|
||||||
|
boolean bothDown = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
|
||||||
|
|
||||||
|
if (isObjectMode()) {
|
||||||
|
if (e.getButton() == MouseButton.PRIMARY && !bothDown) {
|
||||||
|
input.objectClickQueue.offer(
|
||||||
|
new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), false, e.isShiftDown()));
|
||||||
|
objDragPrevX = e.getX(); objDragPrevY = e.getY();
|
||||||
|
objDragging = true;
|
||||||
|
} else if (e.getButton() == MouseButton.SECONDARY && !bothDown) {
|
||||||
|
input.objectClickQueue.offer(
|
||||||
|
new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), true, false));
|
||||||
|
objDragPrevX = e.getX(); objDragPrevY = e.getY();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.activeLayer == SharedInput.LAYER_ROUTINE_EDITOR) {
|
||||||
|
if (e.getButton() == MouseButton.PRIMARY) {
|
||||||
|
input.routinePointClickQueue.offer(
|
||||||
|
new SharedInput.RoutinePointClick((float) e.getX(), (float) e.getY()));
|
||||||
|
} else if (e.getButton() == MouseButton.SECONDARY) {
|
||||||
|
input.routineYawDragQueue.offer(
|
||||||
|
new SharedInput.RoutineYawDrag((float) e.getX(), (float) e.getY(), false));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.activeLayer == SharedInput.LAYER_WATERFALL) {
|
||||||
|
if (e.getButton() == MouseButton.PRIMARY) {
|
||||||
|
input.waterfallClickQueue.offer(
|
||||||
|
new SharedInput.WaterfallClick((float)e.getX(), (float)e.getY(), false));
|
||||||
|
} else if (e.getButton() == MouseButton.SECONDARY) {
|
||||||
|
input.waterfallClickQueue.offer(
|
||||||
|
new SharedInput.WaterfallClick((float)e.getX(), (float)e.getY(), true));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.activeLayer == SharedInput.LAYER_ITEMS) {
|
||||||
|
if (e.getButton() == MouseButton.PRIMARY && !bothDown) {
|
||||||
|
input.itemClickQueue.offer(
|
||||||
|
new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), false, false));
|
||||||
|
} else if (e.getButton() == MouseButton.SECONDARY) {
|
||||||
|
input.itemClickQueue.offer(
|
||||||
|
new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), true, false));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) {
|
||||||
|
if (e.getButton() == MouseButton.PRIMARY && !bothDown) {
|
||||||
|
input.pathNetworkClickQueue.offer(
|
||||||
|
new SharedInput.PathNetworkClick((float)e.getX(), (float)e.getY(), false));
|
||||||
|
} else if (e.getButton() == MouseButton.SECONDARY && !bothDown) {
|
||||||
|
input.pathNetworkClickQueue.offer(
|
||||||
|
new SharedInput.PathNetworkClick((float)e.getX(), (float)e.getY(), true));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.activeLayer == SharedInput.LAYER_STONE
|
||||||
|
&& input.stoneTool.brushRadius.getValue() == 1.0) {
|
||||||
|
// Single-Stone-Modus: nur Rechtsklick, einmalig ohne Timer
|
||||||
|
if (e.getButton() == MouseButton.SECONDARY && !bothDown) {
|
||||||
|
input.stoneEditQueue.offer(
|
||||||
|
new SharedInput.StoneEdit((float) e.getX(), (float) e.getY(), -1));
|
||||||
|
}
|
||||||
|
} else if (bothDown) {
|
||||||
|
stopEditTimer();
|
||||||
|
} else if (e.getButton() == MouseButton.PRIMARY && !e.isAltDown()) {
|
||||||
|
boolean singleClickLayer = input.activeLayer == SharedInput.LAYER_PLAY_TOOL
|
||||||
|
|| 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_WATER;
|
||||||
|
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) {
|
||||||
|
// Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam)
|
||||||
|
input.playToolClickQueue.offer(
|
||||||
|
new SharedInput.PlayToolClick((float) e.getX(), (float) e.getY()));
|
||||||
|
} else if (singleClickLayer) {
|
||||||
|
submitEdit(e.getX(), e.getY(), +1);
|
||||||
|
} else {
|
||||||
|
editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1;
|
||||||
|
submitEdit(editPressX, editPressY, editPressAction);
|
||||||
|
startEditTimer();
|
||||||
|
}
|
||||||
|
} else if (e.getButton() == MouseButton.SECONDARY) {
|
||||||
|
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_WATER;
|
||||||
|
editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1;
|
||||||
|
submitEdit(editPressX, editPressY, editPressAction);
|
||||||
|
if (!singleClickLayer) { startEditTimer(); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
viewport.setOnMouseDragged(e -> {
|
||||||
|
if (isObjectMode()) {
|
||||||
|
if (objDragging && e.isPrimaryButtonDown()
|
||||||
|
&& !e.isSecondaryButtonDown() && !e.isMiddleButtonDown() && !e.isAltDown()) {
|
||||||
|
float dx = (float)(e.getX() - objDragPrevX);
|
||||||
|
float dy = (float)(e.getY() - objDragPrevY);
|
||||||
|
input.objectDragQueue.offer(new SharedInput.ObjectDrag(dx, dy));
|
||||||
|
objDragPrevX = e.getX(); objDragPrevY = e.getY();
|
||||||
|
} else if (e.isSecondaryButtonDown() && !e.isPrimaryButtonDown()
|
||||||
|
&& !e.isMiddleButtonDown()
|
||||||
|
&& input.activeLayer == SharedInput.LAYER_OBJECTS) {
|
||||||
|
float dx = (float)(e.getX() - objDragPrevX);
|
||||||
|
input.objectRotateDragQueue.offer(new SharedInput.ObjectRotateDrag(dx));
|
||||||
|
objDragPrevX = e.getX(); objDragPrevY = e.getY();
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play-Tool EDIT: Drag-Events an JME3 weiterleiten
|
||||||
|
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL
|
||||||
|
&& input.playToolMode == SharedInput.PlayToolMode.EDIT
|
||||||
|
&& e.isPrimaryButtonDown()) {
|
||||||
|
input.playToolDragQueue.offer(
|
||||||
|
new SharedInput.PlayToolDrag((float) e.getX(), (float) e.getY()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.isPrimaryButtonDown() || e.isSecondaryButtonDown()) {
|
||||||
|
editPressX = e.getX();
|
||||||
|
editPressY = e.getY();
|
||||||
|
input.mouseScreenX = (float) e.getX();
|
||||||
|
input.mouseScreenY = (float) e.getY();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) {
|
||||||
|
input.playToolMouseUp = true;
|
||||||
|
}
|
||||||
|
if (input.vertexSnapEnabled
|
||||||
|
&& input.objectSelectionMode == SharedInput.SEL_MODE_VERTEX) {
|
||||||
|
input.vertexSnapTrigger = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pane.setOnMouseMoved(e -> {
|
||||||
|
input.mouseScreenX = (float) e.getX();
|
||||||
|
input.mouseScreenY = (float) e.getY();
|
||||||
|
});
|
||||||
|
pane.setOnMouseExited(e -> input.mouseScreenX = -1f);
|
||||||
|
|
||||||
|
viewport.setOnScroll(e -> {
|
||||||
|
int steps = (int) Math.signum(e.getDeltaY());
|
||||||
|
if (e.isControlDown()) { steps *= 10; }
|
||||||
|
input.scrollAccum.addAndGet(steps);
|
||||||
|
});
|
||||||
|
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isObjectMode() {
|
||||||
|
return input.activeLayer == SharedInput.LAYER_OBJECTS
|
||||||
|
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT
|
||||||
|
|| input.activeLayer == SharedInput.LAYER_AUSWAHL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startEditTimer() {
|
||||||
|
stopEditTimer();
|
||||||
|
editTimer = new javafx.animation.Timeline(
|
||||||
|
new javafx.animation.KeyFrame(javafx.util.Duration.millis(50),
|
||||||
|
ev -> submitEdit(editPressX, editPressY, editPressAction))
|
||||||
|
);
|
||||||
|
editTimer.setCycleCount(javafx.animation.Timeline.INDEFINITE);
|
||||||
|
editTimer.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopEditTimer() {
|
||||||
|
if (editTimer != null) { editTimer.stop(); editTimer = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void submitEdit(double x, double y, int action) {
|
||||||
|
switch (input.activeLayer) {
|
||||||
|
case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));
|
||||||
|
case 3 -> input.grassEditQueue.offer(new SharedInput.GrassEdit((float) x, (float) y, action));
|
||||||
|
case SharedInput.LAYER_GRASS_VERTEX -> {
|
||||||
|
input.grassVertexEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action));
|
||||||
|
if (action < 0) { input.terrainGrassEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action)); }
|
||||||
|
}
|
||||||
|
case SharedInput.LAYER_TERRAIN_GRASS -> {
|
||||||
|
input.terrainGrassEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action));
|
||||||
|
if (action < 0) { input.grassVertexEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action)); }
|
||||||
|
}
|
||||||
|
case 4 -> {
|
||||||
|
SharedInput.TextureEdit te = new SharedInput.TextureEdit((float) x, (float) y, action);
|
||||||
|
input.textureEditQueue.offer(te);
|
||||||
|
input.voxelTextureEditQueue.offer(te);
|
||||||
|
}
|
||||||
|
case SharedInput.LAYER_LIGHTS ->
|
||||||
|
input.lightClickQueue.offer(new SharedInput.LightClick((float) x, (float) y, action < 0));
|
||||||
|
case SharedInput.LAYER_EMITTERS ->
|
||||||
|
input.emitterClickQueue.offer(new SharedInput.EmitterClick((float) x, (float) y, action < 0));
|
||||||
|
case SharedInput.LAYER_WATER ->
|
||||||
|
input.waterClickQueue.offer(new SharedInput.WaterClick((float) x, (float) y, action < 0));
|
||||||
|
case SharedInput.LAYER_SOUND_AREAS ->
|
||||||
|
input.soundAreaClickQueue.offer(new SharedInput.SoundAreaClick((float) x, (float) y, action < 0));
|
||||||
|
case SharedInput.LAYER_AREAS ->
|
||||||
|
input.areaClickQueue.offer(new SharedInput.AreaClick((float) x, (float) y, action < 0));
|
||||||
|
case SharedInput.LAYER_LOCATION_ZONES ->
|
||||||
|
input.locationZoneClickQueue.offer(new SharedInput.LocationZoneClick((float) x, (float) y, action < 0));
|
||||||
|
case SharedInput.LAYER_ZONEN -> {
|
||||||
|
input.soundAreaClickQueue.offer(new SharedInput.SoundAreaClick((float) x, (float) y, action < 0));
|
||||||
|
input.areaClickQueue.offer(new SharedInput.AreaClick((float) x, (float) y, action < 0));
|
||||||
|
input.locationZoneClickQueue.offer(new SharedInput.LocationZoneClick((float) x, (float) y, action < 0));
|
||||||
|
}
|
||||||
|
case SharedInput.LAYER_VOXEL_CLIFF ->
|
||||||
|
input.voxelCliffClickQueue.offer(new SharedInput.VoxelCliffClick((float) x, (float) y, action < 0));
|
||||||
|
case SharedInput.LAYER_PLAY_TOOL -> {
|
||||||
|
if (action > 0) {
|
||||||
|
input.playToolClickQueue.offer(new SharedInput.PlayToolClick((float) x, (float) y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SharedInput.LAYER_VOXEL ->
|
||||||
|
input.voxelEditQueue.offer(new SharedInput.VoxelEdit((float) x, (float) y, action));
|
||||||
|
case SharedInput.LAYER_STONE ->
|
||||||
|
input.stoneEditQueue.offer(new SharedInput.StoneEdit((float) x, (float) y, action));
|
||||||
|
case SharedInput.LAYER_MODEL_INTERACTABLE -> {
|
||||||
|
if (action > 0) {
|
||||||
|
input.modelInteractableClickQueue.offer(
|
||||||
|
new SharedInput.ModelInteractableClick((float) x, (float) y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduleAutoSave.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void drawCompass() {
|
||||||
|
if (compassCanvas == null) { return; }
|
||||||
|
final double S = compassCanvas.getWidth(); // 100
|
||||||
|
final double cx = S / 2.0;
|
||||||
|
final double cy = S / 2.0;
|
||||||
|
final double R = cx - 3; // Radius bis zum Rand
|
||||||
|
|
||||||
|
javafx.scene.canvas.GraphicsContext gc = compassCanvas.getGraphicsContext2D();
|
||||||
|
gc.clearRect(0, 0, S, S);
|
||||||
|
|
||||||
|
// Hintergrund: dunkler Kreis
|
||||||
|
gc.setFill(javafx.scene.paint.Color.rgb(12, 12, 24, 0.82));
|
||||||
|
gc.fillOval(2, 2, S - 4, S - 4);
|
||||||
|
gc.setStroke(javafx.scene.paint.Color.rgb(100, 100, 160, 0.85));
|
||||||
|
gc.setLineWidth(1.5);
|
||||||
|
gc.strokeOval(2, 2, S - 4, S - 4);
|
||||||
|
|
||||||
|
// Rotierende Rose: -camYaw so dass die aktuelle Blickrichtung oben erscheint
|
||||||
|
gc.save();
|
||||||
|
gc.translate(cx, cy);
|
||||||
|
gc.rotate(-input.camYaw);
|
||||||
|
|
||||||
|
// Tick-Striche an 8 Positionen (45°-Schritte)
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
double a = Math.toRadians(i * 45.0);
|
||||||
|
double len = (i % 2 == 0) ? 9 : 5;
|
||||||
|
gc.setStroke(javafx.scene.paint.Color.rgb(160, 160, 200, 0.65));
|
||||||
|
gc.setLineWidth(i % 2 == 0 ? 1.5 : 1.0);
|
||||||
|
gc.strokeLine(
|
||||||
|
Math.sin(a) * (R - len), -Math.cos(a) * (R - len),
|
||||||
|
Math.sin(a) * R, -Math.cos(a) * R);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Himmelsrichtungen (N/E/S/W), Text durch Gegenrotation immer lesbar
|
||||||
|
String[] dirs = {"N", "E", "S", "W"};
|
||||||
|
double[] angles = {0.0, 90.0, 180.0, 270.0};
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
double a = Math.toRadians(angles[i]);
|
||||||
|
double tx = Math.sin(a) * (R - 16);
|
||||||
|
double ty = -Math.cos(a) * (R - 16);
|
||||||
|
gc.save();
|
||||||
|
gc.translate(tx, ty);
|
||||||
|
gc.rotate(input.camYaw); // Gegenrotation zur Rose → Buchstabe immer aufrecht
|
||||||
|
gc.setFill(i == 0
|
||||||
|
? javafx.scene.paint.Color.rgb(255, 80, 80)
|
||||||
|
: javafx.scene.paint.Color.rgb(210, 210, 230));
|
||||||
|
gc.setFont(javafx.scene.text.Font.font(
|
||||||
|
"System", javafx.scene.text.FontWeight.BOLD, i == 0 ? 13 : 11));
|
||||||
|
gc.fillText(dirs[i], -4.5, 5.0);
|
||||||
|
gc.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
gc.restore(); // Ende rotierende Rose
|
||||||
|
|
||||||
|
// Fixer Richtungszeiger oben (gelbes Dreieck, zeigt immer die Blickrichtung)
|
||||||
|
gc.setFill(javafx.scene.paint.Color.rgb(255, 220, 60, 0.95));
|
||||||
|
gc.fillPolygon(
|
||||||
|
new double[]{cx, cx - 5, cx + 5},
|
||||||
|
new double[]{cy - R + 5, cy - R + 16, cy - R + 16},
|
||||||
|
3);
|
||||||
|
|
||||||
|
// Mittelpunkt
|
||||||
|
gc.setFill(javafx.scene.paint.Color.rgb(200, 200, 240, 0.85));
|
||||||
|
gc.fillOval(cx - 2.5, cy - 2.5, 5, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,511 @@
|
|||||||
|
package de.blight.editor.toolset.world;
|
||||||
|
|
||||||
|
import de.blight.editor.ProjectRoot;
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
import de.blight.editor.ToolContext;
|
||||||
|
import de.blight.editor.toolset.EditorUiHelper;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owns the world-zone side panels:
|
||||||
|
* Auswahl, Wege, Zonen (unified), SoundArea, Area, LocationZone.
|
||||||
|
*/
|
||||||
|
public class WorldPanelController {
|
||||||
|
|
||||||
|
private final ToolContext ctx;
|
||||||
|
private final Runnable scheduleAutoSave;
|
||||||
|
|
||||||
|
// Dynamic VBox areas rebuilt by update*Panel methods
|
||||||
|
private VBox auswahlDynamicContent;
|
||||||
|
private VBox zonenDynamicContent;
|
||||||
|
private VBox soundAreaDynamicContent;
|
||||||
|
private VBox areaDynamicContent;
|
||||||
|
private VBox locationZoneDynamicContent;
|
||||||
|
private Runnable zonenKindStyleUpdater;
|
||||||
|
|
||||||
|
public WorldPanelController(ToolContext ctx, Runnable scheduleAutoSave) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.scheduleAutoSave = scheduleAutoSave;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Public update methods called from EditorApp AnimationTimer ─────────────
|
||||||
|
|
||||||
|
public void updateAuswahlItemPanel(String info) {
|
||||||
|
if (auswahlDynamicContent == null) return;
|
||||||
|
auswahlDynamicContent.getChildren().clear();
|
||||||
|
if (info == null) {
|
||||||
|
Label noSel = new Label("Kein Element ausgewählt");
|
||||||
|
noSel.setStyle("-fx-text-fill: #888;");
|
||||||
|
auswahlDynamicContent.getChildren().add(noSel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String[] parts = info.split("\\|");
|
||||||
|
String itemId = parts.length > 1 ? parts[1] : "?";
|
||||||
|
Label idLabel = new Label("Item: " + itemId);
|
||||||
|
idLabel.setStyle("-fx-font-weight: bold;");
|
||||||
|
Button deleteBtn = new Button("Löschen");
|
||||||
|
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white;");
|
||||||
|
deleteBtn.setOnAction(e -> ctx.input().deleteSelectedItemRequested = true);
|
||||||
|
auswahlDynamicContent.getChildren().addAll(idLabel, deleteBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateZonenPanel(String kind) {
|
||||||
|
if (zonenDynamicContent == null) return;
|
||||||
|
zonenDynamicContent.getChildren().clear();
|
||||||
|
switch (kind != null ? kind : "") {
|
||||||
|
case "area" -> {
|
||||||
|
VBox saved = areaDynamicContent;
|
||||||
|
areaDynamicContent = zonenDynamicContent;
|
||||||
|
updateAreaPanel(ctx.input().selectedAreaInfo);
|
||||||
|
areaDynamicContent = saved;
|
||||||
|
}
|
||||||
|
case "sound" -> {
|
||||||
|
VBox saved = soundAreaDynamicContent;
|
||||||
|
soundAreaDynamicContent = zonenDynamicContent;
|
||||||
|
updateSoundAreaPanel(ctx.input().selectedSoundAreaInfo);
|
||||||
|
soundAreaDynamicContent = saved;
|
||||||
|
}
|
||||||
|
case "location" -> {
|
||||||
|
VBox saved = locationZoneDynamicContent;
|
||||||
|
locationZoneDynamicContent = zonenDynamicContent;
|
||||||
|
updateLocationZonePanel(ctx.input().selectedLocationZoneInfo);
|
||||||
|
locationZoneDynamicContent = saved;
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
Label noSel = new Label("Keine Zone ausgewählt");
|
||||||
|
noSel.setStyle("-fx-text-fill: #888;");
|
||||||
|
zonenDynamicContent.getChildren().add(noSel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void runZonenKindStyleUpdater() {
|
||||||
|
if (zonenKindStyleUpdater != null) zonenKindStyleUpdater.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSoundAreaPanel(String info) {
|
||||||
|
if (soundAreaDynamicContent == null) return;
|
||||||
|
soundAreaDynamicContent.getChildren().clear();
|
||||||
|
if (info == null) {
|
||||||
|
Label noSel = new Label("Kein Bereich ausgewählt");
|
||||||
|
noSel.setStyle("-fx-text-fill: #888;");
|
||||||
|
soundAreaDynamicContent.getChildren().add(noSel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String[] p = info.split("\\|", -1);
|
||||||
|
if (p.length < 4) return;
|
||||||
|
try {
|
||||||
|
int idx = Integer.parseInt(p[0]);
|
||||||
|
float volume = Float.parseFloat(p[2]);
|
||||||
|
boolean xfade = Boolean.parseBoolean(p[3]);
|
||||||
|
final String[] soundPath = {p[1]};
|
||||||
|
final float[] vol = {volume};
|
||||||
|
final boolean[] cf = {xfade};
|
||||||
|
|
||||||
|
Label soundLabel = new Label("Sound: " + (soundPath[0].isEmpty() ? "(keine)" : soundPath[0]));
|
||||||
|
soundLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
|
||||||
|
soundLabel.setWrapText(true);
|
||||||
|
|
||||||
|
Button browseBtn = new Button("Sound waehlen...");
|
||||||
|
browseBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
browseBtn.setOnAction(e -> {
|
||||||
|
java.nio.file.Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||||
|
new de.blight.editor.ui.SoundChooser(assetRoot, de.blight.editor.ui.SoundChooser.Mode.ALL)
|
||||||
|
.showAndWait()
|
||||||
|
.ifPresent(rel -> {
|
||||||
|
soundPath[0] = rel;
|
||||||
|
soundLabel.setText("Sound: " + rel);
|
||||||
|
sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Label volLbl = new Label("Lautstärke");
|
||||||
|
volLbl.setStyle("-fx-font-size: 11;");
|
||||||
|
Slider volSlider = new Slider(0, 1, vol[0]);
|
||||||
|
volSlider.setShowTickLabels(false);
|
||||||
|
volSlider.valueProperty().addListener((o, ov, nv) -> {
|
||||||
|
vol[0] = nv.floatValue();
|
||||||
|
sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
CheckBox xfadeCB = new CheckBox("Loop-Crossfade");
|
||||||
|
xfadeCB.setSelected(cf[0]);
|
||||||
|
xfadeCB.setOnAction(e -> {
|
||||||
|
cf[0] = xfadeCB.isSelected();
|
||||||
|
sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Button delBtn = new Button("Löschen");
|
||||||
|
delBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
delBtn.setStyle("-fx-text-fill: #c0392b;");
|
||||||
|
delBtn.setOnAction(e -> ctx.input().deleteSoundAreaRequested = true);
|
||||||
|
|
||||||
|
soundAreaDynamicContent.getChildren().addAll(
|
||||||
|
soundLabel, browseBtn, new Separator(),
|
||||||
|
volLbl, EditorUiHelper.withField(volSlider, "%.2f"), xfadeCB,
|
||||||
|
new Separator(), delBtn);
|
||||||
|
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateAreaPanel(String info) {
|
||||||
|
if (areaDynamicContent == null) return;
|
||||||
|
areaDynamicContent.getChildren().clear();
|
||||||
|
if (info == null) {
|
||||||
|
Label noSel = new Label("Keine Area ausgewählt");
|
||||||
|
noSel.setStyle("-fx-text-fill: #888;");
|
||||||
|
areaDynamicContent.getChildren().add(noSel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String[] p = info.split("\\|", 3);
|
||||||
|
if (p.length < 1) return;
|
||||||
|
try {
|
||||||
|
int idx = Integer.parseInt(p[0]);
|
||||||
|
String currentAreaId = p.length > 1 ? p[1] : "";
|
||||||
|
java.util.List<de.blight.common.model.trigger.Trigger> existingTriggers =
|
||||||
|
p.length > 2
|
||||||
|
? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2])
|
||||||
|
: new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
java.util.List<String> areaIds = new java.util.ArrayList<>();
|
||||||
|
areaIds.add("");
|
||||||
|
try {
|
||||||
|
de.blight.common.AreaDefinitionIO.load().stream()
|
||||||
|
.map(de.blight.common.AreaDefinition::id)
|
||||||
|
.filter(s -> !s.isBlank())
|
||||||
|
.forEach(areaIds::add);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
Label areaLbl = new Label("Verknüpfte Area:");
|
||||||
|
areaLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
|
||||||
|
|
||||||
|
Label nameKeyLbl = new Label(currentAreaId.isBlank() ? "" : currentAreaId + ".name");
|
||||||
|
nameKeyLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #888; -fx-font-style: italic;");
|
||||||
|
|
||||||
|
de.blight.editor.ui.TriggerListEditor[] editorHolder = {null};
|
||||||
|
|
||||||
|
ComboBox<String> areaCombo = new ComboBox<>();
|
||||||
|
areaCombo.getItems().setAll(areaIds);
|
||||||
|
areaCombo.setValue(areaIds.contains(currentAreaId) ? currentAreaId : "");
|
||||||
|
areaCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
areaCombo.setPromptText("Area wählen…");
|
||||||
|
areaCombo.valueProperty().addListener((obs, o, n) -> {
|
||||||
|
String aid = n != null ? n : "";
|
||||||
|
nameKeyLbl.setText(aid.isBlank() ? "" : aid + ".name");
|
||||||
|
java.util.List<de.blight.common.model.trigger.Trigger> curTriggers =
|
||||||
|
editorHolder[0] != null
|
||||||
|
? new java.util.ArrayList<>(editorHolder[0].getTriggers())
|
||||||
|
: existingTriggers;
|
||||||
|
ctx.input().pendingArea.set(new de.blight.common.PlacedArea(
|
||||||
|
new float[0], new float[0], aid, curTriggers));
|
||||||
|
scheduleAutoSave.run();
|
||||||
|
});
|
||||||
|
|
||||||
|
Label triggerLbl = new Label("Trigger:");
|
||||||
|
triggerLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
|
||||||
|
|
||||||
|
editorHolder[0] = new de.blight.editor.ui.TriggerListEditor(existingTriggers, () -> {
|
||||||
|
String aid = areaCombo.getValue() != null ? areaCombo.getValue() : currentAreaId;
|
||||||
|
ctx.input().pendingArea.set(new de.blight.common.PlacedArea(
|
||||||
|
new float[0], new float[0], aid,
|
||||||
|
new java.util.ArrayList<>(editorHolder[0].getTriggers())));
|
||||||
|
scheduleAutoSave.run();
|
||||||
|
});
|
||||||
|
|
||||||
|
Button delBtn = new Button("Löschen");
|
||||||
|
delBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
delBtn.setStyle("-fx-text-fill: #c0392b;");
|
||||||
|
delBtn.setOnAction(e -> { ctx.input().deleteAreaRequested = true; scheduleAutoSave.run(); });
|
||||||
|
|
||||||
|
areaDynamicContent.getChildren().addAll(
|
||||||
|
areaLbl, areaCombo, nameKeyLbl,
|
||||||
|
new Separator(),
|
||||||
|
triggerLbl, editorHolder[0],
|
||||||
|
new Separator(),
|
||||||
|
delBtn);
|
||||||
|
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLocationZonePanel(String info) {
|
||||||
|
if (locationZoneDynamicContent == null) return;
|
||||||
|
locationZoneDynamicContent.getChildren().clear();
|
||||||
|
if (info == null) {
|
||||||
|
Label noSel = new Label("Keine Location ausgewählt");
|
||||||
|
noSel.setStyle("-fx-text-fill: #888;");
|
||||||
|
locationZoneDynamicContent.getChildren().add(noSel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String[] p = info.split("\\|", 3);
|
||||||
|
if (p.length < 1) return;
|
||||||
|
try {
|
||||||
|
int idx = Integer.parseInt(p[0]);
|
||||||
|
String nameId = p.length > 1 ? p[1] : "";
|
||||||
|
java.util.List<de.blight.common.model.trigger.Trigger> existingTriggers =
|
||||||
|
p.length > 2
|
||||||
|
? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2])
|
||||||
|
: new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
java.util.List<String> locationIds = new java.util.ArrayList<>();
|
||||||
|
locationIds.add("");
|
||||||
|
try {
|
||||||
|
de.blight.common.LocationIO.load().stream()
|
||||||
|
.map(de.blight.common.model.Location::getId)
|
||||||
|
.filter(s -> !s.isBlank())
|
||||||
|
.forEach(locationIds::add);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
Label nameLbl = new Label("Verknüpfte Location:");
|
||||||
|
nameLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
|
||||||
|
|
||||||
|
Label nameKeyLbl = new Label(nameId.isBlank() ? "" : nameId + ".name");
|
||||||
|
nameKeyLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #888; -fx-font-style: italic;");
|
||||||
|
|
||||||
|
ComboBox<String> locationCombo = new ComboBox<>();
|
||||||
|
locationCombo.getItems().setAll(locationIds);
|
||||||
|
locationCombo.setValue(locationIds.contains(nameId) ? nameId : "");
|
||||||
|
locationCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
locationCombo.setPromptText("Location wählen…");
|
||||||
|
locationCombo.valueProperty().addListener((obs, o, n) -> {
|
||||||
|
String lid = n != null ? n : "";
|
||||||
|
nameKeyLbl.setText(lid.isBlank() ? "" : lid + ".name");
|
||||||
|
ctx.input().pendingLocationZone.set(new de.blight.common.PlacedLocationZone(
|
||||||
|
new float[0], new float[0], lid, existingTriggers));
|
||||||
|
scheduleAutoSave.run();
|
||||||
|
});
|
||||||
|
|
||||||
|
Button delBtn = new Button("Löschen");
|
||||||
|
delBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
delBtn.setStyle("-fx-text-fill: #c0392b;");
|
||||||
|
delBtn.setOnAction(e -> { ctx.input().deleteLocationZoneRequested = true; scheduleAutoSave.run(); });
|
||||||
|
|
||||||
|
locationZoneDynamicContent.getChildren().addAll(
|
||||||
|
nameLbl, locationCombo, nameKeyLbl,
|
||||||
|
new Separator(),
|
||||||
|
delBtn);
|
||||||
|
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Panel builders ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public VBox buildAuswahlPanel() {
|
||||||
|
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("Auswahl");
|
||||||
|
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;");
|
||||||
|
panel.getChildren().addAll(title, new Separator());
|
||||||
|
panel.getChildren().addAll(
|
||||||
|
EditorUiHelper.styledHint("L-Klick: Element auswählen"),
|
||||||
|
EditorUiHelper.styledHint(" → wechselt automatisch zum"),
|
||||||
|
EditorUiHelper.styledHint(" passenden Werkzeug"),
|
||||||
|
new Separator(),
|
||||||
|
EditorUiHelper.styledHint("Selektierbar:"),
|
||||||
|
EditorUiHelper.styledHint(" + Platzierte Objekte"),
|
||||||
|
EditorUiHelper.styledHint(" * Lichter / Emitter"),
|
||||||
|
EditorUiHelper.styledHint(" ~ Wasser / Wasserfall"),
|
||||||
|
EditorUiHelper.styledHint(" -- Wege / Knoten"),
|
||||||
|
EditorUiHelper.styledHint(" [ ] Zonen"),
|
||||||
|
EditorUiHelper.styledHint(" o Spawnpunkte (Items)"),
|
||||||
|
new Separator());
|
||||||
|
auswahlDynamicContent = new VBox(6);
|
||||||
|
Label noSel = new Label("Kein Element ausgewählt");
|
||||||
|
noSel.setStyle("-fx-text-fill: #888;");
|
||||||
|
auswahlDynamicContent.getChildren().add(noSel);
|
||||||
|
panel.getChildren().add(auswahlDynamicContent);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VBox buildWegePanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
inner.getChildren().addAll(
|
||||||
|
EditorUiHelper.sectionTitle("Wegnetz"),
|
||||||
|
new Separator(),
|
||||||
|
EditorUiHelper.styledHint("L-Klick: neuen Knoten setzen"),
|
||||||
|
EditorUiHelper.styledHint("L-Klick auf Knoten: auswählen"),
|
||||||
|
EditorUiHelper.styledHint(" (2 gewählte → Kante erzeugen)"),
|
||||||
|
EditorUiHelper.styledHint("L-Klick + gewählter Knoten:"),
|
||||||
|
EditorUiHelper.styledHint(" neuen Knoten + Kante"),
|
||||||
|
EditorUiHelper.styledHint("R-Klick auf Knoten: löschen"),
|
||||||
|
EditorUiHelper.styledHint("R-Klick auf Terrain: abwä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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VBox buildZonenPanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
final String STYLE_AREA_ON = "-fx-background-color: #e05050; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;";
|
||||||
|
final String STYLE_AREA_OFF = "-fx-background-color: #b03030; -fx-text-fill: #ddd; -fx-font-weight: bold;";
|
||||||
|
final String STYLE_SOUND_ON = "-fx-background-color: #e07818; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;";
|
||||||
|
final String STYLE_SOUND_OFF = "-fx-background-color: #a05010; -fx-text-fill: #ddd; -fx-font-weight: bold;";
|
||||||
|
final String STYLE_LOC_ON = "-fx-background-color: #c8a800; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;";
|
||||||
|
final String STYLE_LOC_OFF = "-fx-background-color: #8a7200; -fx-text-fill: #ddd; -fx-font-weight: bold;";
|
||||||
|
|
||||||
|
Button areaKindBtn = new Button("+ Neue Area zeichnen");
|
||||||
|
Button soundKindBtn = new Button("+ Neuen Sound zeichnen");
|
||||||
|
Button locationKindBtn = new Button("+ Neue Location zeichnen");
|
||||||
|
areaKindBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
soundKindBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
locationKindBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
|
Runnable updateKindStyles = () -> {
|
||||||
|
String k = ctx.input().zonenNewKind;
|
||||||
|
areaKindBtn.setStyle("area".equals(k) ? STYLE_AREA_ON : STYLE_AREA_OFF);
|
||||||
|
soundKindBtn.setStyle("sound".equals(k) ? STYLE_SOUND_ON : STYLE_SOUND_OFF);
|
||||||
|
locationKindBtn.setStyle("location".equals(k) ? STYLE_LOC_ON : STYLE_LOC_OFF);
|
||||||
|
};
|
||||||
|
updateKindStyles.run();
|
||||||
|
zonenKindStyleUpdater = updateKindStyles;
|
||||||
|
|
||||||
|
areaKindBtn.setOnAction(e -> { ctx.input().zonenNewKind = "area"; updateKindStyles.run(); });
|
||||||
|
soundKindBtn.setOnAction(e -> { ctx.input().zonenNewKind = "sound"; updateKindStyles.run(); });
|
||||||
|
locationKindBtn.setOnAction(e -> { ctx.input().zonenNewKind = "location"; updateKindStyles.run(); });
|
||||||
|
|
||||||
|
areaKindBtn.setTooltip(new Tooltip(
|
||||||
|
"Area: Musik-Zone mit Tag-, Nacht- und Kampf-Track.\nAreas dürfen sich nicht überschneiden."));
|
||||||
|
soundKindBtn.setTooltip(new Tooltip(
|
||||||
|
"Sound: Ambient-Soundbereich mit wählbarer Audio-Datei,\nLautstärke und Loop-Crossfade."));
|
||||||
|
locationKindBtn.setTooltip(new Tooltip(
|
||||||
|
"Location: Gameplay-Trigger-Zone, z.B. zum Starten\nvon Quests oder Dialogen beim Betreten."));
|
||||||
|
|
||||||
|
inner.getChildren().addAll(
|
||||||
|
EditorUiHelper.sectionTitle("Neue Zone"),
|
||||||
|
areaKindBtn, soundKindBtn, locationKindBtn,
|
||||||
|
new Separator(),
|
||||||
|
EditorUiHelper.styledHint("L-Klick → Polygon-Punkte setzen"),
|
||||||
|
EditorUiHelper.styledHint("R-Klick (beim Zeichnen) → letzten Punkt rückgängig"),
|
||||||
|
EditorUiHelper.styledHint("R-Klick (sonst) → Auswahl aufheben"),
|
||||||
|
EditorUiHelper.styledHint("ESC → Zeichnen abbrechen"),
|
||||||
|
EditorUiHelper.styledHint("Entf → Gewählte Zone löschen"),
|
||||||
|
new Separator(),
|
||||||
|
EditorUiHelper.sectionTitle("Gewählte Zone"),
|
||||||
|
new Separator());
|
||||||
|
|
||||||
|
zonenDynamicContent = new VBox(6);
|
||||||
|
Label noSel = new Label("Keine Zone ausgewählt");
|
||||||
|
noSel.setStyle("-fx-text-fill: #888;");
|
||||||
|
zonenDynamicContent.getChildren().add(noSel);
|
||||||
|
inner.getChildren().add(zonenDynamicContent);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VBox buildSoundAreaPanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
inner.getChildren().addAll(
|
||||||
|
EditorUiHelper.sectionTitle("Sound-Bereiche"),
|
||||||
|
EditorUiHelper.styledHint("L-Klick → Polygon-Punkte setzen"),
|
||||||
|
EditorUiHelper.styledHint("R-Klick → Polygon schließen / Auswahl aufheben"),
|
||||||
|
EditorUiHelper.styledHint("Entf → Bereich löschen"),
|
||||||
|
new Separator(),
|
||||||
|
EditorUiHelper.sectionTitle("Gewählter Bereich"),
|
||||||
|
new Separator());
|
||||||
|
soundAreaDynamicContent = new VBox(6);
|
||||||
|
Label noSel = new Label("Kein Bereich ausgewählt");
|
||||||
|
noSel.setStyle("-fx-text-fill: #888;");
|
||||||
|
soundAreaDynamicContent.getChildren().add(noSel);
|
||||||
|
inner.getChildren().add(soundAreaDynamicContent);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VBox buildAreaPanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
inner.getChildren().addAll(
|
||||||
|
EditorUiHelper.sectionTitle("Areas"),
|
||||||
|
EditorUiHelper.styledHint("L-Klick → Polygon-Punkte setzen"),
|
||||||
|
EditorUiHelper.styledHint("R-Klick → Polygon schließen / Auswahl aufheben"),
|
||||||
|
EditorUiHelper.styledHint("ESC → Zeichnen abbrechen"),
|
||||||
|
EditorUiHelper.styledHint("Entf → Area löschen"),
|
||||||
|
EditorUiHelper.styledHint("Areas dürfen sich nicht überschneiden"),
|
||||||
|
new Separator(),
|
||||||
|
EditorUiHelper.sectionTitle("Gewählte Area"),
|
||||||
|
new Separator());
|
||||||
|
areaDynamicContent = new VBox(6);
|
||||||
|
Label noSel = new Label("Keine Area ausgewählt");
|
||||||
|
noSel.setStyle("-fx-text-fill: #888;");
|
||||||
|
areaDynamicContent.getChildren().add(noSel);
|
||||||
|
inner.getChildren().add(areaDynamicContent);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VBox buildLocationZonePanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
inner.getChildren().addAll(
|
||||||
|
EditorUiHelper.sectionTitle("Locations"),
|
||||||
|
EditorUiHelper.styledHint("L-Klick → Polygon-Punkte setzen"),
|
||||||
|
EditorUiHelper.styledHint("R-Klick → Polygon schließen / Auswahl aufheben"),
|
||||||
|
EditorUiHelper.styledHint("ESC → Zeichnen abbrechen"),
|
||||||
|
EditorUiHelper.styledHint("Entf → Location löschen"),
|
||||||
|
EditorUiHelper.styledHint("Locations dürfen sich überschneiden"),
|
||||||
|
new Separator(),
|
||||||
|
EditorUiHelper.sectionTitle("Gewählte Location"),
|
||||||
|
new Separator());
|
||||||
|
locationZoneDynamicContent = new VBox(6);
|
||||||
|
Label noSel = new Label("Keine Location ausgewählt");
|
||||||
|
noSel.setStyle("-fx-text-fill: #888;");
|
||||||
|
locationZoneDynamicContent.getChildren().add(noSel);
|
||||||
|
inner.getChildren().add(locationZoneDynamicContent);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Internal ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void sendSoundAreaUpdate(int idx, String soundPath, float volume, boolean crossfade) {
|
||||||
|
SharedInput in = ctx.input();
|
||||||
|
if (in.selectedSoundAreaInfo == null) return;
|
||||||
|
in.selectedSoundAreaInfo = idx + "|" + soundPath + "|" + volume + "|" + crossfade;
|
||||||
|
in.pendingSoundArea.set(new de.blight.common.PlacedSoundArea(
|
||||||
|
new float[0], new float[0], soundPath, volume, crossfade));
|
||||||
|
scheduleAutoSave.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,6 +120,15 @@ public class PlayerInputControl {
|
|||||||
/** Autopilot: wenn gesetzt, geht der Charakter automatisch in diese (normalisierte) Richtung. */
|
/** Autopilot: wenn gesetzt, geht der Charakter automatisch in diese (normalisierte) Richtung. */
|
||||||
private Vector3f autopilotDir = null;
|
private Vector3f autopilotDir = null;
|
||||||
|
|
||||||
|
// Pre-allokierte Scratch-Objekte — keine Heap-Allokation pro Frame in update()
|
||||||
|
private final Vector3f scratchCamDir = new Vector3f();
|
||||||
|
private final Vector3f scratchCamLeft = new Vector3f();
|
||||||
|
private final Vector3f scratchMoveDir = new Vector3f();
|
||||||
|
private final Vector3f scratchWalk = new Vector3f();
|
||||||
|
private final Quaternion scratchRot = new Quaternion();
|
||||||
|
private final Quaternion scratchCurRot = new Quaternion();
|
||||||
|
private final Vector3f scratchTransl = new Vector3f();
|
||||||
|
|
||||||
private final ActionListener actionListener = (name, isPressed, tpf) -> {
|
private final ActionListener actionListener = (name, isPressed, tpf) -> {
|
||||||
if (paused) return;
|
if (paused) return;
|
||||||
switch (name) {
|
switch (name) {
|
||||||
@@ -477,7 +486,8 @@ public class PlayerInputControl {
|
|||||||
float t = Math.min(1f, animOffsetElapsed / AnimPlayer.BLEND_DURATION);
|
float t = Math.min(1f, animOffsetElapsed / AnimPlayer.BLEND_DURATION);
|
||||||
animOffsetCurrent.set(animOffsetFrom);
|
animOffsetCurrent.set(animOffsetFrom);
|
||||||
animOffsetCurrent.interpolateLocal(animOffsetTarget, t);
|
animOffsetCurrent.interpolateLocal(animOffsetTarget, t);
|
||||||
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent));
|
scratchTransl.set(visualBaseTranslation).addLocal(animOffsetCurrent);
|
||||||
|
visual.setLocalTranslation(scratchTransl);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputsBlocked) return;
|
if (inputsBlocked) return;
|
||||||
@@ -513,19 +523,17 @@ public class PlayerInputControl {
|
|||||||
turnRemaining -= tpf;
|
turnRemaining -= tpf;
|
||||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
if (visual != null && turnDir != null) {
|
if (visual != null && turnDir != null) {
|
||||||
Quaternion targetRot = new Quaternion();
|
scratchRot.lookAt(turnDir, Vector3f.UNIT_Y);
|
||||||
targetRot.lookAt(turnDir, Vector3f.UNIT_Y);
|
scratchCurRot.set(visual.getLocalRotation());
|
||||||
Quaternion current = visual.getLocalRotation().clone();
|
scratchCurRot.slerp(scratchRot, ROTATE_SPEED * tpf);
|
||||||
current.slerp(targetRot, ROTATE_SPEED * tpf);
|
visual.setLocalRotation(scratchCurRot);
|
||||||
visual.setLocalRotation(current);
|
|
||||||
}
|
}
|
||||||
if (turnRemaining <= 0f) {
|
if (turnRemaining <= 0f) {
|
||||||
turnActive = false;
|
turnActive = false;
|
||||||
// Zielrotation exakt einrasten
|
// Zielrotation exakt einrasten
|
||||||
if (visual != null && turnDir != null) {
|
if (visual != null && turnDir != null) {
|
||||||
Quaternion snap = new Quaternion();
|
scratchRot.lookAt(turnDir, Vector3f.UNIT_Y);
|
||||||
snap.lookAt(turnDir, Vector3f.UNIT_Y);
|
visual.setLocalRotation(scratchRot);
|
||||||
visual.setLocalRotation(snap);
|
|
||||||
}
|
}
|
||||||
Runnable cb = turnCallback;
|
Runnable cb = turnCallback;
|
||||||
turnCallback = null;
|
turnCallback = null;
|
||||||
@@ -557,13 +565,12 @@ public class PlayerInputControl {
|
|||||||
|
|
||||||
// Autopilot: Charakter läuft automatisch in eine vorgegebene Richtung (WALK-Animation)
|
// Autopilot: Charakter läuft automatisch in eine vorgegebene Richtung (WALK-Animation)
|
||||||
if (autopilotDir != null) {
|
if (autopilotDir != null) {
|
||||||
physicsChar.setWalkDirection(autopilotDir.mult(MOVE_SPEED * WALK_MULT));
|
physicsChar.setWalkDirection(scratchWalk.set(autopilotDir).multLocal(MOVE_SPEED * WALK_MULT));
|
||||||
if (visual != null) {
|
if (visual != null) {
|
||||||
Quaternion targetRot = new Quaternion();
|
scratchRot.lookAt(autopilotDir, Vector3f.UNIT_Y);
|
||||||
targetRot.lookAt(autopilotDir, Vector3f.UNIT_Y);
|
scratchCurRot.set(visual.getLocalRotation());
|
||||||
Quaternion current = visual.getLocalRotation().clone();
|
scratchCurRot.slerp(scratchRot, ROTATE_SPEED * tpf);
|
||||||
current.slerp(targetRot, ROTATE_SPEED * tpf);
|
visual.setLocalRotation(scratchCurRot);
|
||||||
visual.setLocalRotation(current);
|
|
||||||
}
|
}
|
||||||
if (AnimationAction.WALK != currentAnim) {
|
if (AnimationAction.WALK != currentAnim) {
|
||||||
playAction(AnimationAction.WALK);
|
playAction(AnimationAction.WALK);
|
||||||
@@ -573,16 +580,16 @@ public class PlayerInputControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Normale Spielereingabe (WASD)
|
// Normale Spielereingabe (WASD)
|
||||||
Vector3f camDir = cam.getDirection().clone().setY(0).normalizeLocal();
|
cam.getDirection(scratchCamDir).setY(0).normalizeLocal();
|
||||||
Vector3f camLeft = cam.getLeft().clone().setY(0).normalizeLocal();
|
cam.getLeft(scratchCamLeft).setY(0).normalizeLocal();
|
||||||
|
|
||||||
Vector3f moveDir = new Vector3f();
|
scratchMoveDir.set(0f, 0f, 0f);
|
||||||
if (forward) moveDir.addLocal(camDir);
|
if (forward) scratchMoveDir.addLocal(scratchCamDir);
|
||||||
if (backward) moveDir.subtractLocal(camDir);
|
if (backward) scratchMoveDir.subtractLocal(scratchCamDir);
|
||||||
if (left) moveDir.addLocal(camLeft);
|
if (left) scratchMoveDir.addLocal(scratchCamLeft);
|
||||||
if (right) moveDir.subtractLocal(camLeft);
|
if (right) scratchMoveDir.subtractLocal(scratchCamLeft);
|
||||||
|
|
||||||
boolean moving = moveDir.lengthSquared() > 0.001f;
|
boolean moving = scratchMoveDir.lengthSquared() > 0.001f;
|
||||||
|
|
||||||
// Wassertiefe bestimmen – einmal pro Frame, auch in pollFootsteps() genutzt
|
// Wassertiefe bestimmen – einmal pro Frame, auch in pollFootsteps() genutzt
|
||||||
Vector3f charPos = physicsChar.getPhysicsLocation();
|
Vector3f charPos = physicsChar.getPhysicsLocation();
|
||||||
@@ -590,18 +597,17 @@ public class PlayerInputControl {
|
|||||||
boolean inDeepWater = currentWaterDepth > WATER_WADING_DEPTH;
|
boolean inDeepWater = currentWaterDepth > WATER_WADING_DEPTH;
|
||||||
|
|
||||||
if (moving) {
|
if (moving) {
|
||||||
moveDir.normalizeLocal();
|
scratchMoveDir.normalizeLocal();
|
||||||
float speed = (inDeepWater || walk) ? MOVE_SPEED * WALK_MULT
|
float speed = (inDeepWater || walk) ? MOVE_SPEED * WALK_MULT
|
||||||
: sprint ? MOVE_SPEED * SPRINT_MULT
|
: sprint ? MOVE_SPEED * SPRINT_MULT
|
||||||
: MOVE_SPEED;
|
: MOVE_SPEED;
|
||||||
physicsChar.setWalkDirection(moveDir.mult(speed));
|
physicsChar.setWalkDirection(scratchWalk.set(scratchMoveDir).multLocal(speed));
|
||||||
|
|
||||||
if (visual != null) {
|
if (visual != null) {
|
||||||
Quaternion targetRot = new Quaternion();
|
scratchRot.lookAt(scratchMoveDir, Vector3f.UNIT_Y);
|
||||||
targetRot.lookAt(moveDir, Vector3f.UNIT_Y);
|
scratchCurRot.set(visual.getLocalRotation());
|
||||||
Quaternion current = visual.getLocalRotation().clone();
|
scratchCurRot.slerp(scratchRot, ROTATE_SPEED * tpf);
|
||||||
current.slerp(targetRot, ROTATE_SPEED * tpf);
|
visual.setLocalRotation(scratchCurRot);
|
||||||
visual.setLocalRotation(current);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ public class ThirdPersonCamera {
|
|||||||
private float introDuration;
|
private float introDuration;
|
||||||
private float introTimer;
|
private float introTimer;
|
||||||
|
|
||||||
|
// Pre-allokierte Scratch-Vektoren — keine Heap-Allokation pro Frame
|
||||||
|
private final Vector3f scratchPivot = new Vector3f();
|
||||||
|
private final Vector3f scratchCamPos = new Vector3f();
|
||||||
|
private final Vector3f scratchDir = new Vector3f();
|
||||||
|
|
||||||
|
|
||||||
public ThirdPersonCamera(Camera cam, InputManager inputManager, KeyBindings keyBindings) {
|
public ThirdPersonCamera(Camera cam, InputManager inputManager, KeyBindings keyBindings) {
|
||||||
this.cam = cam;
|
this.cam = cam;
|
||||||
@@ -111,28 +116,28 @@ public class ThirdPersonCamera {
|
|||||||
|
|
||||||
if (target == null || paused) return;
|
if (target == null || paused) return;
|
||||||
|
|
||||||
Vector3f pivot = target.getWorldTranslation().add(0, TARGET_HEIGHT, 0);
|
scratchPivot.set(target.getWorldTranslation()).addLocal(0f, TARGET_HEIGHT, 0f);
|
||||||
|
|
||||||
// Sphärische Koordinaten → kartesisch
|
// Sphärische Koordinaten → kartesisch
|
||||||
float x = distance * FastMath.cos(pitch) * FastMath.sin(yaw);
|
float x = distance * FastMath.cos(pitch) * FastMath.sin(yaw);
|
||||||
float y = distance * FastMath.sin(pitch);
|
float y = distance * FastMath.sin(pitch);
|
||||||
float z = distance * FastMath.cos(pitch) * FastMath.cos(yaw);
|
float z = distance * FastMath.cos(pitch) * FastMath.cos(yaw);
|
||||||
|
|
||||||
Vector3f camPos = pivot.add(x, y, z);
|
scratchCamPos.set(scratchPivot).addLocal(x, y, z);
|
||||||
|
|
||||||
if (raycastProvider != null) {
|
if (raycastProvider != null) {
|
||||||
float fraction = raycastProvider.apply(pivot, camPos);
|
float fraction = raycastProvider.apply(scratchPivot, scratchCamPos);
|
||||||
if (fraction < 1f) {
|
if (fraction < 1f) {
|
||||||
Vector3f dir = camPos.subtract(pivot);
|
scratchDir.set(scratchCamPos).subtractLocal(scratchPivot);
|
||||||
float rayLen = dir.length();
|
float rayLen = scratchDir.length();
|
||||||
dir.normalizeLocal();
|
scratchDir.normalizeLocal();
|
||||||
float safeLen = Math.max(MIN_DISTANCE * 0.5f, fraction * rayLen - CAM_CLEARANCE);
|
float safeLen = Math.max(MIN_DISTANCE * 0.5f, fraction * rayLen - CAM_CLEARANCE);
|
||||||
camPos = pivot.add(dir.mult(safeLen));
|
scratchCamPos.set(scratchPivot).addLocal(scratchDir.multLocal(safeLen));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cam.setLocation(camPos);
|
cam.setLocation(scratchCamPos);
|
||||||
cam.lookAt(pivot, Vector3f.UNIT_Y);
|
cam.lookAt(scratchPivot, Vector3f.UNIT_Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Aktueller Yaw-Winkel (für CharacterControl nutzbar). */
|
/** Aktueller Yaw-Winkel (für CharacterControl nutzbar). */
|
||||||
|
|||||||
@@ -104,10 +104,18 @@ public class WorldScene extends BaseAppState {
|
|||||||
private de.blight.game.state.RiverSoundState riverSound;
|
private de.blight.game.state.RiverSoundState riverSound;
|
||||||
private de.blight.game.state.AmbientSoundSystem ambientSounds;
|
private de.blight.game.state.AmbientSoundSystem ambientSounds;
|
||||||
private de.blight.game.state.MusicSystem musicSystem;
|
private de.blight.game.state.MusicSystem musicSystem;
|
||||||
|
private de.blight.game.state.EmitterSystem emitterSystem;
|
||||||
private de.blight.game.audio.FootstepSystem footstepSystem;
|
private de.blight.game.audio.FootstepSystem footstepSystem;
|
||||||
private de.blight.game.state.DrownState drownState;
|
private de.blight.game.state.DrownState drownState;
|
||||||
private WaterInteractionState waterInteractionState;
|
private WaterInteractionState waterInteractionState;
|
||||||
|
|
||||||
|
// Pre-allokierte Scratch-Objekte — keine Heap-Allokation pro Frame in update()
|
||||||
|
private final Vector3f scratchLightDir = new Vector3f();
|
||||||
|
private final Vector3f scratchSunColor = new Vector3f();
|
||||||
|
private final Vector3f scratchAmbColor = new Vector3f();
|
||||||
|
private final ColorRGBA scratchEmissive = new ColorRGBA();
|
||||||
|
private final Vector3f scratchSunPos = new Vector3f();
|
||||||
|
|
||||||
public WorldScene(KeyBindings keyBindings, GraphicsSettings graphicsSettings) {
|
public WorldScene(KeyBindings keyBindings, GraphicsSettings graphicsSettings) {
|
||||||
this.keyBindings = keyBindings;
|
this.keyBindings = keyBindings;
|
||||||
this.graphicsSettings = graphicsSettings;
|
this.graphicsSettings = graphicsSettings;
|
||||||
@@ -270,6 +278,8 @@ public class WorldScene extends BaseAppState {
|
|||||||
BlightGame.status("Lade Welt-Objekte...");
|
BlightGame.status("Lade Welt-Objekte...");
|
||||||
app.getStateManager().attach(new RiverState());
|
app.getStateManager().attach(new RiverState());
|
||||||
app.getStateManager().attach(new de.blight.game.state.WaterfallState());
|
app.getStateManager().attach(new de.blight.game.state.WaterfallState());
|
||||||
|
emitterSystem = new de.blight.game.state.EmitterSystem();
|
||||||
|
app.getStateManager().attach(emitterSystem);
|
||||||
WorldObjectsState worldObjects = new WorldObjectsState();
|
WorldObjectsState worldObjects = new WorldObjectsState();
|
||||||
worldObjects.setLodFactor((float) graphicsSettings.viewDistance.lod0Range);
|
worldObjects.setLodFactor((float) graphicsSettings.viewDistance.lod0Range);
|
||||||
app.getStateManager().attach(worldObjects);
|
app.getStateManager().attach(worldObjects);
|
||||||
@@ -460,29 +470,32 @@ public class WorldScene extends BaseAppState {
|
|||||||
if (riverSound != null) riverSound.setPlayerPosition(pos);
|
if (riverSound != null) riverSound.setPlayerPosition(pos);
|
||||||
if (ambientSounds != null) ambientSounds.setPlayerPosition(pos);
|
if (ambientSounds != null) ambientSounds.setPlayerPosition(pos);
|
||||||
if (musicSystem != null) musicSystem.setPlayerPosition(pos);
|
if (musicSystem != null) musicSystem.setPlayerPosition(pos);
|
||||||
|
if (emitterSystem != null) emitterSystem.setPlayerPosition(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (musicSystem != null && dayNight != null)
|
if (musicSystem != null && dayNight != null)
|
||||||
musicSystem.setDaytime(dayNight.getSunElevation() >= 0f);
|
musicSystem.setDaytime(dayNight.getSunElevation() >= 0f);
|
||||||
|
|
||||||
if (terrainMaterial != null && dayNight != null && dayNight.getSunLight() != null) {
|
if (terrainMaterial != null && dayNight != null && dayNight.getSunLight() != null) {
|
||||||
terrainMaterial.setVector3("LightDir", dayNight.getSunDirection().negate());
|
scratchLightDir.set(dayNight.getSunDirection()).negateLocal();
|
||||||
|
terrainMaterial.setVector3("LightDir", scratchLightDir);
|
||||||
ColorRGBA sc = dayNight.getSunLight().getColor();
|
ColorRGBA sc = dayNight.getSunLight().getColor();
|
||||||
ColorRGBA ac = dayNight.getCustomAmbient();
|
ColorRGBA ac = dayNight.getCustomAmbient();
|
||||||
terrainMaterial.setVector3("SunColor", new Vector3f(sc.r, sc.g, sc.b));
|
terrainMaterial.setVector3("SunColor", scratchSunColor.set(sc.r, sc.g, sc.b));
|
||||||
terrainMaterial.setVector3("AmbientColor", new Vector3f(ac.r, ac.g, ac.b));
|
terrainMaterial.setVector3("AmbientColor", scratchAmbColor.set(ac.r, ac.g, ac.b));
|
||||||
de.blight.game.state.RainState rs = app.getStateManager().getState(de.blight.game.state.RainState.class);
|
de.blight.game.state.RainState rs = app.getStateManager().getState(de.blight.game.state.RainState.class);
|
||||||
if (rs != null) terrainMaterial.setFloat("Wetness", rs.getWetness());
|
if (rs != null) terrainMaterial.setFloat("Wetness", rs.getWetness());
|
||||||
// Charakter-PBR: Emissive = customAmbient (IBL-Fallback ohne fertige LightProbe)
|
// Charakter-PBR: Emissive = customAmbient (IBL-Fallback ohne fertige LightProbe)
|
||||||
|
scratchEmissive.set(ac.r, ac.g, ac.b, 1f);
|
||||||
for (Material mat : characterPbrMaterials) {
|
for (Material mat : characterPbrMaterials) {
|
||||||
mat.setColor("Emissive", new ColorRGBA(ac.r, ac.g, ac.b, 1f));
|
mat.setColor("Emissive", scratchEmissive);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lightScatterFilter != null) {
|
if (lightScatterFilter != null) {
|
||||||
Vector3f sunDir = dayNight.getSunDirection();
|
Vector3f sunDir = dayNight.getSunDirection();
|
||||||
// Weltposition der Sonne: von der Kamera aus weit entfernt entgegen der Lichtrichtung
|
// Weltposition der Sonne: von der Kamera aus weit entfernt entgegen der Lichtrichtung
|
||||||
Vector3f sunWorld = app.getCamera().getLocation().add(sunDir.negate().mult(10000f));
|
scratchSunPos.set(sunDir).negateLocal().multLocal(10000f).addLocal(app.getCamera().getLocation());
|
||||||
lightScatterFilter.setLightPosition(sunWorld);
|
lightScatterFilter.setLightPosition(scratchSunPos);
|
||||||
// Deaktivieren wenn Sonne hinter der Kamera (gleiche Richtung wie Kamera schaut)
|
// Deaktivieren wenn Sonne hinter der Kamera (gleiche Richtung wie Kamera schaut)
|
||||||
lightScatterFilter.setEnabled(sunDir.dot(app.getCamera().getDirection()) < 0.2f);
|
lightScatterFilter.setEnabled(sunDir.dot(app.getCamera().getDirection()) < 0.2f);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,12 @@ public class WorldNpcsState extends BaseAppState {
|
|||||||
private float revertTimer = 0f;
|
private float revertTimer = 0f;
|
||||||
private float revertDelay = 0f;
|
private float revertDelay = 0f;
|
||||||
|
|
||||||
|
// Pre-allokierte Scratch-Objekte — keine Heap-Allokation in hot paths
|
||||||
|
private final List<SpawnedNpc> toRemoveBuf = new ArrayList<>();
|
||||||
|
private final Set<String> spawnedIdsBuf = new HashSet<>();
|
||||||
|
private final Quaternion scratchQ = new Quaternion();
|
||||||
|
private final com.jme3.math.ColorRGBA scratchEmissive = new com.jme3.math.ColorRGBA();
|
||||||
|
|
||||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public WorldNpcsState(KeyBindings keyBindings, CharacterControl physicsChar,
|
public WorldNpcsState(KeyBindings keyBindings, CharacterControl physicsChar,
|
||||||
@@ -244,14 +250,14 @@ public class WorldNpcsState extends BaseAppState {
|
|||||||
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
||||||
|
|
||||||
// Bestehende NPCs prüfen: Position aktualisieren oder entladen
|
// Bestehende NPCs prüfen: Position aktualisieren oder entladen
|
||||||
List<SpawnedNpc> toRemove = new ArrayList<>();
|
toRemoveBuf.clear();
|
||||||
for (SpawnedNpc s : spawned) {
|
for (SpawnedNpc s : spawned) {
|
||||||
Vector3f pos = resolveRoutinePosition(s.npc(), currentHour);
|
Vector3f pos = resolveRoutinePosition(s.npc(), currentHour);
|
||||||
float dist = pos != null ? dist2d(playerPos, pos) : Float.MAX_VALUE;
|
float dist = pos != null ? dist2d(playerPos, pos) : Float.MAX_VALUE;
|
||||||
|
|
||||||
if (pos == null || dist > UNLOAD_RADIUS) {
|
if (pos == null || dist > UNLOAD_RADIUS) {
|
||||||
s.visual().removeFromParent();
|
s.visual().removeFromParent();
|
||||||
toRemove.add(s);
|
toRemoveBuf.add(s);
|
||||||
if (rotNpc == s) rotNpc = null;
|
if (rotNpc == s) rotNpc = null;
|
||||||
if (revertNpc == s) revertNpc = null;
|
if (revertNpc == s) revertNpc = null;
|
||||||
} else {
|
} else {
|
||||||
@@ -260,27 +266,28 @@ public class WorldNpcsState extends BaseAppState {
|
|||||||
playNpcAction(s, activityActionFor(s.npc(), currentHour));
|
playNpcAction(s, activityActionFor(s.npc(), currentHour));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
spawned.removeAll(toRemove);
|
spawned.removeAll(toRemoveBuf);
|
||||||
|
|
||||||
// PBR-Emissive der verbleibenden NPCs mit aktuellem Ambient aktualisieren
|
// PBR-Emissive der verbleibenden NPCs mit aktuellem Ambient aktualisieren
|
||||||
if (dayNight != null) {
|
if (dayNight != null) {
|
||||||
com.jme3.math.ColorRGBA ac = dayNight.getCustomAmbient();
|
com.jme3.math.ColorRGBA ac = dayNight.getCustomAmbient();
|
||||||
|
scratchEmissive.set(ac.r, ac.g, ac.b, 1f);
|
||||||
for (SpawnedNpc s : spawned) {
|
for (SpawnedNpc s : spawned) {
|
||||||
for (Material mat : s.pbrMaterials) {
|
for (Material mat : s.pbrMaterials) {
|
||||||
mat.setColor("Emissive", new com.jme3.math.ColorRGBA(ac.r, ac.g, ac.b, 1f));
|
mat.setColor("Emissive", scratchEmissive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDs der bereits gespawnten NPCs für schnelle Prüfung
|
// IDs der bereits gespawnten NPCs für schnelle Prüfung
|
||||||
Set<String> spawnedIds = new HashSet<>();
|
spawnedIdsBuf.clear();
|
||||||
for (SpawnedNpc s : spawned) {
|
for (SpawnedNpc s : spawned) {
|
||||||
spawnedIds.add(s.npc().getCharacterId());
|
spawnedIdsBuf.add(s.npc().getCharacterId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Neue NPCs einladen wenn in Reichweite
|
// Neue NPCs einladen wenn in Reichweite
|
||||||
for (NPC npc : allNpcs) {
|
for (NPC npc : allNpcs) {
|
||||||
if (spawnedIds.contains(npc.getCharacterId())) continue;
|
if (spawnedIdsBuf.contains(npc.getCharacterId())) continue;
|
||||||
Vector3f pos = resolveRoutinePosition(npc, currentHour);
|
Vector3f pos = resolveRoutinePosition(npc, currentHour);
|
||||||
if (pos == null) continue;
|
if (pos == null) continue;
|
||||||
if (dist2d(playerPos, pos) > LOAD_RADIUS) continue;
|
if (dist2d(playerPos, pos) > LOAD_RADIUS) continue;
|
||||||
@@ -292,8 +299,9 @@ public class WorldNpcsState extends BaseAppState {
|
|||||||
de.blight.game.scene.WorldScene.collectPbrMaterials(vis, s.pbrMaterials);
|
de.blight.game.scene.WorldScene.collectPbrMaterials(vis, s.pbrMaterials);
|
||||||
if (dayNight != null && !s.pbrMaterials.isEmpty()) {
|
if (dayNight != null && !s.pbrMaterials.isEmpty()) {
|
||||||
com.jme3.math.ColorRGBA ac = dayNight.getCustomAmbient();
|
com.jme3.math.ColorRGBA ac = dayNight.getCustomAmbient();
|
||||||
|
scratchEmissive.set(ac.r, ac.g, ac.b, 1f);
|
||||||
for (Material mat : s.pbrMaterials) {
|
for (Material mat : s.pbrMaterials) {
|
||||||
mat.setColor("Emissive", new com.jme3.math.ColorRGBA(ac.r, ac.g, ac.b, 1f));
|
mat.setColor("Emissive", scratchEmissive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
spawned.add(s);
|
spawned.add(s);
|
||||||
@@ -482,8 +490,8 @@ public class WorldNpcsState extends BaseAppState {
|
|||||||
if (rotNpc == null) return;
|
if (rotNpc == null) return;
|
||||||
rotElapsed += tpf;
|
rotElapsed += tpf;
|
||||||
float p = FastMath.clamp(rotElapsed / ROT_DURATION, 0f, 1f);
|
float p = FastMath.clamp(rotElapsed / ROT_DURATION, 0f, 1f);
|
||||||
Quaternion q = new Quaternion().slerp(rotFrom, rotTo, p);
|
scratchQ.slerp(rotFrom, rotTo, p);
|
||||||
rotNpc.visual().setLocalRotation(q);
|
rotNpc.visual().setLocalRotation(scratchQ);
|
||||||
if (p >= 1f) rotNpc = null;
|
if (p >= 1f) rotNpc = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,8 +500,8 @@ public class WorldNpcsState extends BaseAppState {
|
|||||||
revertTimer += tpf;
|
revertTimer += tpf;
|
||||||
if (revertTimer < revertDelay) return;
|
if (revertTimer < revertDelay) return;
|
||||||
float p = FastMath.clamp((revertTimer - revertDelay) / REVERT_DURATION, 0f, 1f);
|
float p = FastMath.clamp((revertTimer - revertDelay) / REVERT_DURATION, 0f, 1f);
|
||||||
Quaternion q = new Quaternion().slerp(revertFrom, revertTo, p);
|
scratchQ.slerp(revertFrom, revertTo, p);
|
||||||
revertNpc.visual().setLocalRotation(q);
|
revertNpc.visual().setLocalRotation(scratchQ);
|
||||||
if (p >= 1f) revertNpc = null;
|
if (p >= 1f) revertNpc = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
# x y z activationRadius texturePath imagesX imagesY startR startG startB startA endR endG endB endA startSize endSize velX velY velZ velVariation gravX gravY gravZ lowLife highLife maxParticles emitRate
|
# x y z activationRadius texturePath imagesX imagesY startR startG startB startA endR endG endB endA startSize endSize velX velY velZ velVariation gravX gravY gravZ lowLife highLife maxParticles emitRate
|
||||||
|
0.93604 2.50000 -3.56553 20.00000 Effects/Explosion/flame.png 2 2 1.0000 0.7000 0.1000 1.0000 1.0000 0.1000 0.0000 0.0000 0.5000 1.2000 0.0000 2.5000 0.0000 0.5000 0.0000 -0.1000 0.0000 1.0000 3.0000 60 25.0000
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +1,3 @@
|
|||||||
# x y z r g b intensity radius
|
# x y z r g b intensity radius
|
||||||
|
-2.18687 3.00000 -5.43333 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||||
|
0.42349 3.00000 -10.67357 1.00000 1.00000 1.00000 1.00000 20.00000
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,2 +1,3 @@
|
|||||||
# polygon_points waterHeight flowDegrees speed waveScale waveAmplitude transparency waterR waterG waterB deepR deepG deepB
|
# polygon_points waterHeight flowDegrees speed waveScale waveAmplitude transparency waterR waterG waterB deepR deepG deepB
|
||||||
155.13664,11.00000,-898.11578;120.48705,11.00000,-897.61938;107.05534,11.00000,-890.41797;96.71703,11.00000,-875.01575;98.34164,11.00000,-854.85681;182.64165,11.00000,-829.23749;195.95512,11.00000,-848.04095 11.00000 180.0 0.5000 0.008000 0.10000 0.15000 0.05000 0.25000 0.55000 0.02000 0.12000 0.30000
|
155.13664,11.00000,-898.11578;120.48705,11.00000,-897.61938;107.05534,11.00000,-890.41797;96.71703,11.00000,-875.01575;98.34164,11.00000,-854.85681;182.64165,11.00000,-829.23749;195.95512,11.00000,-848.04095 11.00000 180.0 0.5000 0.008000 0.10000 0.15000 0.05000 0.25000 0.55000 0.02000 0.12000 0.30000
|
||||||
|
219.67131,1.90000,-796.04559;197.17520,1.90000,-800.89313;192.60945,1.90000,-797.65149;190.13742,1.90000,-767.83209;221.88794,1.90000,-767.83209 1.90000 0.0 0.5000 0.008000 0.10000 0.15000 0.05000 0.25000 0.55000 0.02000 0.12000 0.30000
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod2.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod2.j3o
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_0_10.blvc.prebake
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_0_10.blvc.prebake
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod0.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod0.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod1.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod1.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod2.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod2.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_m1_10.blvc.prebake
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_m1_10.blvc.prebake
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod0.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod0.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod1.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod1.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod2.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod2.j3o
Normal file
Binary file not shown.
Reference in New Issue
Block a user