Compare commits

...

8 Commits

Author SHA1 Message Date
6e2c3a2785 Abschluss des großen Editor Refactorings 2026-09-13 12:20:53 +02:00
feef7cab62 Bugfixes: ObjectController-Toolbar, Emitter ingame, Esc-Shortcut
ObjectController: Welt-Toolbar bleibt beim F7-Öffnen sichtbar (kein
eigener Toolbar mehr). Modus wechselt automatisch: Platzieren wenn
Modell/Ordner ausgewählt, sonst Bearbeiten. Neuer Button + Esc-Shortcut
zum Aufheben der Platzierungs-Auswahl.

WorldScene: EmitterSystem eingebunden — platzierte Emitter werden jetzt
auch im Spiel geladen und proximity-basiert aktiviert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-12 21:47:32 +02:00
8f0cf7bc42 Phase 5: Atmosphere, Water, Voxel, Grass aus EditorApp extrahiert (-1.170 Z.)
- AtmosphereController: Licht- + Emitter-Panel, updateLight/Emitter, onFrame()
- WaterController: Wasser- + Wasserfall-Panel, updateWater/Waterfall, onFrame()
- VoxelController: VoxelMerged- + Klippen-Panel; sculptPanelSupplier-Injection
  für showToolParameters-Delegation; onCliffGenerateKeyPressed() für Keyboard
- GrassController: Gras-Panel + GrassTextureUI; viewport/showToolParams/
  normalMapLoader injiziert; buildGrassTextureUI() auch von showToolParameters
  in EditorApp aufgerufen
- EditorApp: controller.onFrame()-Aufrufe im AnimationTimer; Toolbar-Buttons
  delegieren an controller.buildPanel(); Timer-Blöcke für Selection-Changes
  aus EditorApp entfernt (→ onFrame); Felder lightDynamic, waterDynamic,
  voxelCliff*, grassSub*, lightBtn/emitterBtn/waterBtn/riverBtn/voxelBtn entfernt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-11 11:01:14 +02:00
bf1bb4fd16 Phase 4 Bugfixes: viewportSupplier, registry-State, currentTool-Checks
- ToolContext: viewportSupplier liefert worldViewport statt centerStack
  (setCenterView(centerStack) wäre self-parent JavaFX-Exception)
- switchToWorldEditor: registry.deactivateAll() + currentTool-Checks
  durch registry.activeToolId() ersetzt (currentTool wurde nie auf
  "objecteditor"/"modeleditor" gesetzt)
- ToolRegistry: deactivateAll() hinzugefügt
- ModelEditorController.openModelEditor: Idempotenz-Fix wie activatePlaceMode
  (bei bereits aktivem Controller activate() direkt aufrufen)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-11 10:45:57 +02:00
f1b321731b Phase 4: ObjectController + ModelEditorController aus EditorApp extrahiert
EditorApp: 11.519 → 9.293 Zeilen (-2.226)
- de.blight.editor.toolset.object.ObjectController (Objekt-Platzieren/Bearbeiten)
- de.blight.editor.toolset.model.ModelEditorController (Modell-Editor + Modell-Import)
- Timer-Blöcke für Objekt/Modell aus EditorApp entfernt; registry.onFrame() delegiert
- Alle Aufruf-Sites auf die neuen Controller umgestellt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-11 10:03:46 +02:00
f0a0c216f6 Refactoring Phase 3: AnimPreview + AnimSet aus EditorApp extrahiert
AnimPreviewController und AnimSetController in de.blight.editor.toolset.anim/
extrahiert (~1500 Zeilen raus). EditorApp: 13.187 → 11.650 Zeilen.
ToolContext um registry-Parameter erweitert, damit AnimSetController sich
nach Picker-Dialog selbst aktivieren kann.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-10 22:28:09 +02:00
e4b0ee0dc3 Refactoring Phase 2: Vegetation & EzTree aus EditorApp extrahiert
- Neue Infrastruktur: ToolController, ToolRegistry, ToolContext (record)
- VegetationController: alle ~1000 Z. Vegetation-State + Panels ausgelagert
  (Baum, Farn, Palme, Fruchtbusch, Weinpflanze, Trauerweide)
- EzTreeController: EzTree-State, Toolbar, Panel + buildLevelSection()
- UiHelper: shared static UI-Helpers (bold, sectionTitle, ezFloat, …)
- EditorApp: 14 222 → 13 187 Zeilen (-1035); switchToVegetationGenerator()
  und switchToEzTree() delegieren an registry.activate()
- F5/F6: hybrides Routing – migrierte Tools über ToolController.onPreview()
  / onRandomSeed(), nicht-migrierte über bestehende Runnable-Felder
- AnimationTimer: treePreviewResized + treeGenStatusMsg → registry.onFrame()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-10 21:23:46 +02:00
1b509c3a52 Performance Tweaks 2026-09-10 20:54:36 +02:00
62 changed files with 14272 additions and 13482 deletions

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

File diff suppressed because it is too large Load Diff

View File

@@ -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
) {}

View File

@@ -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() {}
}

View File

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

View File

@@ -40,8 +40,8 @@ public class WaterBodyState extends BaseAppState {
private static final float LINE_OFFSET = 0.1f;
private static final float POLE_HEIGHT = 5.0f;
private static final float CAP_SIZE = 1.2f;
private static final float HANDLE_PICK_PX = 24f;
private static final float CAP_SIZE = 2.0f;
private static final float HANDLE_PICK_PX = 32f;
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);
@@ -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_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_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_HOVER = new ColorRGBA(1f, 0.9f, 0f, 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_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<Material> handleMats = new ArrayList<>();
private final List<Float> handleCapY = new ArrayList<>();
private int hoveredHandle = -1;
private int dragHandle = -1;
private int hoveredHandle = -1;
private int dragHandle = -1;
private int selectedHandle = -1;
private boolean isDragging = false;
private float[] liveXs = new float[0];
private float[] liveYs = new float[0];
@@ -187,7 +189,11 @@ public class WaterBodyState extends BaseAppState {
if (input.deleteWaterRequested) {
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;
@@ -235,6 +241,7 @@ public class WaterBodyState extends BaseAppState {
PlacedWater b = bodies.get(selectedIdx);
int h = findHandleAt(jmeX, jmeY, b.pointsX(), b.pointsZ());
if (h >= 0) {
setSelectedHandle(h);
startDrag(h, b.pointsX(), b.pointsY(), b.pointsZ());
return;
}
@@ -569,10 +576,50 @@ public class WaterBodyState extends BaseAppState {
handleMats.clear();
handleCapY.clear();
hideSplitIndicator();
hoveredHandle = -1;
hoveredHandle = -1;
dragHandle = -1;
selectedHandle = -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() {
if (selectedIdx < 0) return;
PlacedWater b = bodies.get(selectedIdx);
@@ -585,7 +632,8 @@ public class WaterBodyState extends BaseAppState {
if (h != hoveredHandle) {
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);
}
if (h >= 0 && h < handleMats.size()) {
@@ -846,7 +894,7 @@ public class WaterBodyState extends BaseAppState {
rebuildPoleMesh(mesh, x, z, waterH, terrH);
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color.clone());
mat.getAdditionalRenderState().setLineWidth(2.5f);
mat.getAdditionalRenderState().setLineWidth(4f);
Geometry geo = new Geometry(name, mesh);
geo.setMaterial(mat);
return geo;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,379 @@
package de.blight.editor.toolset.atmosphere;
import de.blight.common.PlacedEmitter;
import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext;
import de.blight.editor.toolset.UiHelper;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.*;
public class AtmosphereController {
private final ToolContext ctx;
private VBox lightDynamicContent;
private VBox emitterDynamicContent;
public AtmosphereController(ToolContext ctx) {
this.ctx = ctx;
}
public void onFrame() {
if (ctx.input().lightSelectionChanged) {
ctx.input().lightSelectionChanged = false;
updateLightPanel(ctx.input().selectedLightInfo);
}
if (ctx.input().emitterSelectionChanged) {
ctx.input().emitterSelectionChanged = false;
updateEmitterPanel(ctx.input().selectedEmitterInfo);
}
}
public VBox buildPanel() {
VBox panel = new VBox(10);
panel.setPadding(new Insets(10));
panel.setPrefWidth(270);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
Label title = new Label("Atmosphare");
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;");
panel.getChildren().addAll(title, new Separator());
ToggleButton subLichtBtn = new ToggleButton("* Licht");
ToggleButton subEmitterBtn = new ToggleButton("~ Emitter");
subLichtBtn.setMaxWidth(Double.MAX_VALUE);
subEmitterBtn.setMaxWidth(Double.MAX_VALUE);
ToggleGroup subGroup = new ToggleGroup();
subLichtBtn.setToggleGroup(subGroup);
subEmitterBtn.setToggleGroup(subGroup);
subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); });
HBox subRow = new HBox(4, subLichtBtn, subEmitterBtn);
HBox.setHgrow(subLichtBtn, Priority.ALWAYS);
HBox.setHgrow(subEmitterBtn, Priority.ALWAYS);
panel.getChildren().add(subRow);
VBox contentBox = new VBox(0);
panel.getChildren().add(contentBox);
Runnable activateLicht = () -> {
ctx.input().activeLayer = SharedInput.LAYER_LIGHTS;
contentBox.getChildren().setAll(buildLightPanel());
};
Runnable activateEmitter = () -> {
ctx.input().activeLayer = SharedInput.LAYER_EMITTERS;
contentBox.getChildren().setAll(buildEmitterPanel());
};
subLichtBtn.setOnAction(e -> activateLicht.run());
subEmitterBtn.setOnAction(e -> activateEmitter.run());
if (ctx.input().activeLayer == SharedInput.LAYER_EMITTERS) {
subEmitterBtn.setSelected(true);
activateEmitter.run();
} else {
subLichtBtn.setSelected(true);
activateLicht.run();
}
return panel;
}
private VBox buildLightPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(
UiHelper.sectionTitle("Lichtquellen"),
new Separator(),
UiHelper.sectionTitle("Gewähltes Licht"),
new Separator());
lightDynamicContent = new VBox(6);
Label noSel = new Label("Kein Licht ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
lightDynamicContent.getChildren().add(noSel);
inner.getChildren().add(lightDynamicContent);
inner.getChildren().addAll(
new Separator(),
UiHelper.styledHint("Linksklick → platzieren / auswählen"),
UiHelper.styledHint("Rechtsklick → Auswahl aufheben"),
UiHelper.styledHint("Entf → ausgewähltes Licht löschen"));
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(260);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
public void updateLightPanel(String info) {
if (lightDynamicContent == null) return;
lightDynamicContent.getChildren().clear();
if (info == null) {
Label noSel = new Label("Kein Licht ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
lightDynamicContent.getChildren().add(noSel);
return;
}
String[] parts = info.split("\\|", -1);
if (parts.length < 9) return;
float[] vals = new float[9];
try {
for (int i = 0; i < 9; i++) vals[i] = Float.parseFloat(parts[i]);
} catch (NumberFormatException e) { return; }
Label posLabel = new Label(String.format("X: %.1f Y: %.1f Z: %.1f", vals[1], vals[2], vals[3]));
posLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
lightDynamicContent.getChildren().addAll(posLabel, new Separator());
float[] cur = {vals[4], vals[5], vals[6], vals[7], vals[8]};
String[] labels = {"Rot", "Grün", "Blau", "Intensität", "Radius"};
double[] mins = {0, 0, 0, 0.1, 0.5};
double[] maxs = {1, 1, 1, 10.0, 200.0};
double[] steps = {0.01, 0.01, 0.01, 0.1, 1.0};
Runnable publish = () -> ctx.input().pendingLightProp.set(
new SharedInput.LightPropertyChange(cur[0], cur[1], cur[2], cur[3], cur[4]));
for (int i = 0; i < 5; i++) {
final int idx = i;
Label lbl = new Label(labels[i]);
lbl.setStyle("-fx-font-size: 11;");
Slider sl = new Slider(mins[i], maxs[i], cur[i]);
sl.setShowTickLabels(false);
sl.setBlockIncrement(steps[i]);
sl.valueProperty().addListener((o, ov, nv) -> {
cur[idx] = nv.floatValue();
publish.run();
});
lightDynamicContent.getChildren().addAll(lbl, UiHelper.withField(sl, "%.2f"));
}
Button delBtn = new Button("Loeschen");
delBtn.setMaxWidth(Double.MAX_VALUE);
delBtn.setStyle("-fx-text-fill: #c0392b;");
delBtn.setOnAction(e -> ctx.input().deleteLightRequested = true);
lightDynamicContent.getChildren().addAll(new Separator(), delBtn);
}
private VBox buildEmitterPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(
UiHelper.sectionTitle("Partikel-Emitter"),
new Separator());
Button fireBtn = new Button("* Feuer");
Button smokeBtn = new Button("~ Rauch");
Button sparksBtn = new Button("* Funken");
fireBtn.setMaxWidth(Double.MAX_VALUE);
smokeBtn.setMaxWidth(Double.MAX_VALUE);
sparksBtn.setMaxWidth(Double.MAX_VALUE);
fireBtn.setOnAction(e -> applyEmitterPreset(0));
smokeBtn.setOnAction(e -> applyEmitterPreset(1));
sparksBtn.setOnAction(e -> applyEmitterPreset(2));
HBox presets = new HBox(4, fireBtn, smokeBtn, sparksBtn);
HBox.setHgrow(fireBtn, Priority.ALWAYS);
HBox.setHgrow(smokeBtn, Priority.ALWAYS);
HBox.setHgrow(sparksBtn, Priority.ALWAYS);
inner.getChildren().addAll(presets, new Separator(),
UiHelper.sectionTitle("Gewählter Emitter"), new Separator());
emitterDynamicContent = new VBox(6);
Label noSel = new Label("Kein Emitter ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
emitterDynamicContent.getChildren().add(noSel);
inner.getChildren().add(emitterDynamicContent);
inner.getChildren().addAll(
new Separator(),
UiHelper.styledHint("Linksklick → platzieren / auswählen"),
UiHelper.styledHint("Rechtsklick → Auswahl aufheben"),
UiHelper.styledHint("Entf → Emitter löschen"));
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(280);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
private void applyEmitterPreset(int preset) {
ctx.input().emitterPreset = preset;
if (ctx.input().selectedEmitterInfo == null) return;
String[] parts = ctx.input().selectedEmitterInfo.split("\\|", -1);
if (parts.length < 4) return;
try {
float x = Float.parseFloat(parts[1]);
float y = Float.parseFloat(parts[2]);
float z = Float.parseFloat(parts[3]);
PlacedEmitter pe = switch (preset) {
case 1 -> PlacedEmitter.smoke(x, y, z);
case 2 -> PlacedEmitter.sparks(x, y, z);
default -> PlacedEmitter.fire(x, y, z);
};
ctx.input().pendingEmitter.set(pe);
} catch (NumberFormatException ignored) {}
}
public void updateEmitterPanel(String info) {
if (emitterDynamicContent == null) return;
emitterDynamicContent.getChildren().clear();
if (info == null) {
Label noSel = new Label("Kein Emitter ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
emitterDynamicContent.getChildren().add(noSel);
return;
}
String[] p = info.split("\\|", -1);
if (p.length < 29) return;
try {
float x = Float.parseFloat(p[1]);
float y = Float.parseFloat(p[2]);
float z = Float.parseFloat(p[3]);
float activRadius = Float.parseFloat(p[4]);
String texPath = p[5];
int imagesX = Integer.parseInt(p[6]);
int imagesY = Integer.parseInt(p[7]);
float[] vals = new float[19];
for (int i = 0; i < 19; i++) vals[i] = Float.parseFloat(p[8 + i]);
int maxParticles = Integer.parseInt(p[27]);
float emitRate = Float.parseFloat(p[28]);
Label posLabel = new Label(String.format("X:%.1f Y:%.1f Z:%.1f", x, y, z));
posLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
Label texLabel = new Label("Textur: " + labelFromPath(texPath));
texLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #777;");
texLabel.setWrapText(true);
emitterDynamicContent.getChildren().addAll(posLabel, texLabel, new Separator());
float[] actR = {activRadius};
int[] mxPart = {maxParticles};
float[] emit = {emitRate};
int[] imgX = {imagesX};
int[] imgY = {imagesY};
final String[] tPath = {texPath};
final float[] fx = {x}, fy = {y}, fz = {z};
Runnable publish = () -> {
PlacedEmitter pe = new PlacedEmitter(
fx[0], fy[0], fz[0], actR[0],
tPath[0], imgX[0], imgY[0],
vals[0], vals[1], vals[2], vals[3],
vals[4], vals[5], vals[6], vals[7],
vals[8], vals[9],
vals[10], vals[11], vals[12], vals[13],
vals[14], vals[15], vals[16],
vals[17], vals[18],
mxPart[0], emit[0]);
ctx.input().pendingEmitter.set(pe);
};
emitterDynamicContent.getChildren().add(paramLabel("Aktivierungsradius"));
emitterDynamicContent.getChildren().add(buildSlider("Radius", actR, 0, 5, 200, 5, publish));
emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Startfarbe"));
String[] colorLabels = {"Start R","Start G","Start B","Start A",
"Ende R","Ende G","Ende B","Ende A"};
for (int i = 0; i < 8; i++) {
emitterDynamicContent.getChildren().add(buildSlider(colorLabels[i], vals, i, 0, 1, 0.01, publish));
if (i == 3) emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Endfarbe"));
}
emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Partikelgröße"));
emitterDynamicContent.getChildren().add(buildSlider("Start-Größe", vals, 8, 0.01, 5, 0.05, publish));
emitterDynamicContent.getChildren().add(buildSlider("Ende-Größe", vals, 9, 0.01, 5, 0.05, publish));
emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Geschwindigkeit"));
emitterDynamicContent.getChildren().add(buildSlider("Vel X", vals, 10, -10, 10, 0.1, publish));
emitterDynamicContent.getChildren().add(buildSlider("Vel Y", vals, 11, 0, 15, 0.1, publish));
emitterDynamicContent.getChildren().add(buildSlider("Vel Z", vals, 12, -10, 10, 0.1, publish));
emitterDynamicContent.getChildren().add(buildSlider("Variation", vals, 13, 0, 1, 0.05, publish));
emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Gravitation"));
emitterDynamicContent.getChildren().add(buildSlider("Grav Y", vals, 15, -20, 20, 0.5, publish));
emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Lebensdauer (s)"));
emitterDynamicContent.getChildren().add(buildSlider("Min", vals, 17, 0.1, 10, 0.1, publish));
emitterDynamicContent.getChildren().add(buildSlider("Max", vals, 18, 0.1, 10, 0.1, publish));
emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Emission"));
emitterDynamicContent.getChildren().add(buildSlider("Max Partikel", mxPart, 0, 1, 500, 5, publish));
emitterDynamicContent.getChildren().add(buildSlider("Rate/s", emit, 0, 0.5, 100, 0.5, publish));
Button delBtn = new Button("Loeschen");
delBtn.setMaxWidth(Double.MAX_VALUE);
delBtn.setStyle("-fx-text-fill: #c0392b;");
delBtn.setOnAction(e -> ctx.input().deleteEmitterRequested = true);
emitterDynamicContent.getChildren().addAll(new Separator(), delBtn);
} catch (NumberFormatException ignored) {}
}
private HBox buildSlider(String name, float[] arr, int idx,
double min, double max, double step, Runnable publish) {
Label lbl = new Label(name);
lbl.setStyle("-fx-font-size: 10;");
lbl.setMinWidth(90);
Slider sl = new Slider(min, max, arr[idx]);
sl.setShowTickLabels(false);
sl.setBlockIncrement(step);
sl.valueProperty().addListener((o, ov, nv) -> {
arr[idx] = nv.floatValue();
publish.run();
});
HBox slRow = UiHelper.withField(sl, "%.2f");
HBox.setHgrow(slRow, Priority.ALWAYS);
return new HBox(4, lbl, slRow);
}
private HBox buildSlider(String name, int[] arr, int idx,
double min, double max, double step, Runnable publish) {
Label lbl = new Label(name);
lbl.setStyle("-fx-font-size: 10;");
lbl.setMinWidth(90);
Slider sl = new Slider(min, max, arr[idx]);
sl.setShowTickLabels(false);
sl.setBlockIncrement(step);
sl.valueProperty().addListener((o, ov, nv) -> {
arr[idx] = nv.intValue();
publish.run();
});
HBox slRow = UiHelper.withField(sl, "%.0f");
HBox.setHgrow(slRow, Priority.ALWAYS);
return new HBox(4, lbl, slRow);
}
private static Label paramLabel(String text) {
Label lbl = new Label(text);
lbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
return lbl;
}
private String labelFromPath(String path) {
if (path == null || path.isEmpty()) return "(leer)";
java.nio.file.Path p = ctx.assetRoot().resolve(path.replace('/', java.io.File.separatorChar));
java.nio.file.Path parent = p.getParent();
if (parent != null && java.nio.file.Files.exists(parent.resolve("textureset.json"))) {
return parent.getFileName().toString();
}
String name = p.getFileName().toString();
int dot = name.lastIndexOf('.');
return dot > 0 ? name.substring(0, dot) : name;
}
}

View File

@@ -0,0 +1,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)));
}
}

View File

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

View File

@@ -0,0 +1,324 @@
package de.blight.editor.toolset.grass;
import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext;
import de.blight.editor.tool.EditorTool;
import de.blight.editor.toolset.UiHelper;
import de.blight.editor.ui.TextureChooser;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import java.io.File;
import java.nio.file.Path;
import java.util.function.BiConsumer;
import java.util.function.Function;
public class GrassController {
private final ToolContext ctx;
private final ImageView viewport;
private final BiConsumer<VBox, EditorTool> showToolParams;
private final Function<Path, String> normalMapLoader;
public GrassController(ToolContext ctx,
ImageView viewport,
BiConsumer<VBox, EditorTool> showToolParams,
Function<Path, String> normalMapLoader) {
this.ctx = ctx;
this.viewport = viewport;
this.showToolParams = showToolParams;
this.normalMapLoader = normalMapLoader;
}
public void onFrame() {
// no per-frame polling needed
}
public VBox buildPanel() {
VBox panel = new VBox(10);
panel.setPadding(new Insets(10));
panel.setPrefWidth(260);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
Label title = new Label("Gras");
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;");
panel.getChildren().addAll(title, new Separator());
ToggleButton grassSubTexBtn = new ToggleButton(":: Textur");
ToggleButton grassSubVertBtn = new ToggleButton("|| Vertex");
ToggleButton grassSubTerrainBtn = new ToggleButton("~~ Terrain");
grassSubTexBtn.setMaxWidth(Double.MAX_VALUE);
grassSubVertBtn.setMaxWidth(Double.MAX_VALUE);
grassSubTerrainBtn.setMaxWidth(Double.MAX_VALUE);
ToggleGroup subGroup = new ToggleGroup();
grassSubTexBtn.setToggleGroup(subGroup);
grassSubVertBtn.setToggleGroup(subGroup);
grassSubTerrainBtn.setToggleGroup(subGroup);
subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); });
HBox subRow = new HBox(4, grassSubTexBtn, grassSubVertBtn, grassSubTerrainBtn);
HBox.setHgrow(grassSubTexBtn, Priority.ALWAYS);
HBox.setHgrow(grassSubVertBtn, Priority.ALWAYS);
HBox.setHgrow(grassSubTerrainBtn, Priority.ALWAYS);
panel.getChildren().add(subRow);
VBox paramsBox = new VBox(10);
panel.getChildren().add(paramsBox);
VBox colorSection = new VBox(6);
colorSection.getChildren().add(new Separator());
Label colorTitle = new Label("Globale Farben");
colorTitle.setStyle("-fx-font-weight: bold; -fx-font-size: 11; -fx-text-fill: #333;");
colorSection.getChildren().add(colorTitle);
javafx.scene.control.ColorPicker freshPicker = new javafx.scene.control.ColorPicker(
javafx.scene.paint.Color.color(ctx.input().grassFreshR, ctx.input().grassFreshG, ctx.input().grassFreshB));
freshPicker.setMaxWidth(Double.MAX_VALUE);
freshPicker.setOnAction(e -> {
javafx.scene.paint.Color c = freshPicker.getValue();
ctx.input().grassFreshR = (float) c.getRed();
ctx.input().grassFreshG = (float) c.getGreen();
ctx.input().grassFreshB = (float) c.getBlue();
ctx.input().grassColorsChanged = true;
});
javafx.scene.control.ColorPicker dryPicker = new javafx.scene.control.ColorPicker(
javafx.scene.paint.Color.color(ctx.input().grassDryR, ctx.input().grassDryG, ctx.input().grassDryB));
dryPicker.setMaxWidth(Double.MAX_VALUE);
dryPicker.setOnAction(e -> {
javafx.scene.paint.Color c = dryPicker.getValue();
ctx.input().grassDryR = (float) c.getRed();
ctx.input().grassDryG = (float) c.getGreen();
ctx.input().grassDryB = (float) c.getBlue();
ctx.input().grassColorsChanged = true;
});
Button pickFromTerrainBtn = new Button("[ ] Farbe vom Boden picken");
pickFromTerrainBtn.setMaxWidth(Double.MAX_VALUE);
pickFromTerrainBtn.setOnAction(e -> {
pickFromTerrainBtn.setText("[ ] Boden anklicken...");
pickFromTerrainBtn.setDisable(true);
ctx.input().grassTerrainPickCallback = color -> {
freshPicker.setValue(javafx.scene.paint.Color.color(
Math.min(1.0, color[0]), Math.min(1.0, color[1]), Math.min(1.0, color[2])));
pickFromTerrainBtn.setText("[ ] Farbe vom Boden picken");
pickFromTerrainBtn.setDisable(false);
};
javafx.event.EventHandler<javafx.scene.input.MouseEvent> handler =
new javafx.event.EventHandler<>() {
@Override public void handle(javafx.scene.input.MouseEvent ev) {
if (ev.getButton() == javafx.scene.input.MouseButton.PRIMARY) {
ctx.input().grassTerrainPickQueue.offer(
new SharedInput.GrassVertexEdit((float) ev.getX(), (float) ev.getY(), +1));
viewport.removeEventHandler(javafx.scene.input.MouseEvent.MOUSE_PRESSED, this);
ev.consume();
}
}
};
viewport.addEventHandler(javafx.scene.input.MouseEvent.MOUSE_PRESSED, handler);
});
colorSection.getChildren().addAll(new Label("Unvertrocknet:"), freshPicker,
pickFromTerrainBtn,
new Label("Vertrocknet:"), dryPicker);
panel.getChildren().add(colorSection);
Runnable activateTex = () -> {
ctx.input().activeLayer = 3;
ctx.input().activeTool = ctx.input().grassTool;
paramsBox.getChildren().clear();
VBox inner = new VBox(10);
showToolParams.accept(inner, ctx.input().activeTool);
paramsBox.getChildren().add(inner);
colorSection.setVisible(true);
colorSection.setManaged(true);
};
Runnable activateVert = () -> {
ctx.input().activeLayer = SharedInput.LAYER_GRASS_VERTEX;
ctx.input().activeTool = ctx.input().grassVertexTool;
paramsBox.getChildren().clear();
VBox inner = new VBox(10);
showToolParams.accept(inner, ctx.input().activeTool);
paramsBox.getChildren().add(inner);
colorSection.setVisible(true);
colorSection.setManaged(true);
};
Runnable activateTerrain = () -> {
ctx.input().activeLayer = SharedInput.LAYER_TERRAIN_GRASS;
ctx.input().activeTool = ctx.input().terrainGrassTool;
paramsBox.getChildren().clear();
VBox inner = new VBox(10);
showToolParams.accept(inner, ctx.input().activeTool);
paramsBox.getChildren().add(inner);
colorSection.setVisible(false);
colorSection.setManaged(false);
};
grassSubTexBtn.setOnAction(e -> activateTex.run());
grassSubVertBtn.setOnAction(e -> activateVert.run());
grassSubTerrainBtn.setOnAction(e -> activateTerrain.run());
if (ctx.input().activeLayer == SharedInput.LAYER_GRASS_VERTEX) {
grassSubVertBtn.setSelected(true);
activateVert.run();
} else if (ctx.input().activeLayer == SharedInput.LAYER_TERRAIN_GRASS) {
grassSubTerrainBtn.setSelected(true);
activateTerrain.run();
} else {
grassSubTexBtn.setSelected(true);
activateTex.run();
}
return panel;
}
public javafx.scene.Node buildGrassTextureUI() {
Label title = new Label("Textur-Slots");
title.setStyle("-fx-text-fill: #111111; -fx-font-weight: bold;");
float[] initArs = ctx.input().grassAspectRatios.clone();
for (int i = 0; i < 8; i++) {
String p = getGrassSlotPath(i);
if (p != null && !p.isEmpty()) initArs[i] = readAspectRatio(p);
}
ctx.input().grassAspectRatios = initArs;
ToggleGroup slotGroup = new ToggleGroup();
VBox slotRows = new VBox(4);
for (int slotIndex = 0; slotIndex < 8; slotIndex++) {
final int slot = slotIndex;
RadioButton rb = new RadioButton();
rb.setToggleGroup(slotGroup);
if (slot == 0) rb.setSelected(true);
rb.setOnAction(e -> ctx.input().grassActiveSlot = slot);
String currentPath = getGrassSlotPath(slot);
String fname = (currentPath != null && !currentPath.isEmpty())
? labelFromPath(currentPath) : "(Leer)";
Label nameLabel = new Label(slot + " > " + fname);
nameLabel.setStyle("-fx-font-size: 11;");
nameLabel.setWrapText(true);
nameLabel.setMaxWidth(Double.MAX_VALUE);
ImageView thumb = makeSlotThumb(currentPath);
Button chooseBtn = new Button("...");
chooseBtn.setOnAction(e -> {
TextureChooser chooser = new TextureChooser(ctx.assetRoot(), false);
chooser.showAndWait().ifPresent(path -> {
Path colorAbs = ctx.assetRoot().resolve(path.replace('/', File.separatorChar));
String nm = normalMapLoader.apply(colorAbs);
String[] nmCopy = ctx.input().grassNormalMapPaths.clone();
nmCopy[slot] = nm;
ctx.input().grassNormalMapPaths = nmCopy;
float[] arCopy = ctx.input().grassAspectRatios.clone();
arCopy[slot] = readAspectRatio(path);
ctx.input().grassAspectRatios = arCopy;
if (slot == 0) {
ctx.input().grassTexturePath = path;
ctx.input().grassSettingsChanged = true;
} else {
String[] copy = ctx.input().grassTextureSlots.clone();
copy[slot - 1] = path;
ctx.input().grassTextureSlots = copy;
ctx.input().grassSlotsChanged = true;
}
nameLabel.setText(slot + " > " + labelFromPath(path));
ImageView newThumb = makeSlotThumb(path);
thumb.setImage(newThumb.getImage());
});
});
Button clearBtn = new Button("X");
clearBtn.setOnAction(e -> {
String[] nmCopy = ctx.input().grassNormalMapPaths.clone();
nmCopy[slot] = "";
ctx.input().grassNormalMapPaths = nmCopy;
if (slot == 0) {
ctx.input().grassTexturePath = "";
ctx.input().grassSettingsChanged = true;
} else {
String[] copy = ctx.input().grassTextureSlots.clone();
copy[slot - 1] = "";
ctx.input().grassTextureSlots = copy;
ctx.input().grassSlotsChanged = true;
}
nameLabel.setText(slot + " > (Leer)");
thumb.setImage(null);
});
HBox btns = new HBox(4, chooseBtn, clearBtn);
VBox info = new VBox(2, nameLabel, btns);
info.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(info, Priority.ALWAYS);
HBox row = new HBox(6, rb, thumb, info);
row.setAlignment(Pos.CENTER_LEFT);
row.setPadding(new Insets(2, 4, 2, 4));
row.setStyle("-fx-background-color: #f8f8f8; -fx-border-color: #ddd; -fx-border-radius: 3;");
slotRows.getChildren().add(row);
}
ScrollPane scroll = new ScrollPane(slotRows);
scroll.setFitToWidth(true);
scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent;");
VBox.setVgrow(scroll, Priority.ALWAYS);
VBox box = new VBox(5, title, scroll);
box.setPadding(new Insets(4, 0, 0, 0));
VBox.setVgrow(box, Priority.ALWAYS);
return box;
}
private float readAspectRatio(String assetRelPath) {
if (assetRelPath == null || assetRelPath.isEmpty()) return 1f;
try {
File f = ctx.assetRoot().resolve(
assetRelPath.replace('/', File.separatorChar)).toFile();
if (!f.exists()) return 1f;
Image img = new Image(f.toURI().toString());
double w = img.getWidth(), h = img.getHeight();
return (w > 0 && h > 0) ? (float) (w / h) : 1f;
} catch (Exception e) { return 1f; }
}
private String getGrassSlotPath(int slot) {
if (slot == 0) return ctx.input().grassTexturePath != null ? ctx.input().grassTexturePath : "";
String[] slots = ctx.input().grassTextureSlots;
return (slot - 1 < slots.length && slots[slot - 1] != null) ? slots[slot - 1] : "";
}
private ImageView makeSlotThumb(String path) {
ImageView iv = new ImageView();
iv.setFitWidth(48); iv.setFitHeight(48); iv.setPreserveRatio(true);
if (path != null && !path.isEmpty()) {
File f = ctx.assetRoot().resolve(path).toFile();
if (f.exists()) {
try { iv.setImage(new Image(f.toURI().toString(), 48, 48, true, true)); }
catch (Exception ignored) {}
}
}
return iv;
}
private String labelFromPath(String path) {
if (path == null || path.isEmpty()) return "(leer)";
Path p = ctx.assetRoot().resolve(path.replace('/', File.separatorChar));
Path parent = p.getParent();
if (parent != null && java.nio.file.Files.exists(parent.resolve("textureset.json"))) {
return parent.getFileName().toString();
}
String name = p.getFileName().toString();
int dot = name.lastIndexOf('.');
return dot > 0 ? name.substring(0, dot) : name;
}
}

View File

@@ -0,0 +1,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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,214 @@
package de.blight.editor.toolset.voxel;
import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext;
import de.blight.editor.toolset.UiHelper;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import java.util.function.Supplier;
public class VoxelController {
private final ToolContext ctx;
private final Supplier<Node> sculptPanelSupplier;
private final Runnable showSculptPanel;
private VBox voxelCliffDynamicContent;
private Label voxelCliffStatusLabel;
private Button voxelCliffGenerateBtn;
private Button voxelCliffDeleteBtn;
/**
* @param sculptPanelSupplier EditorApp lambda: sets activeLayer/activeTool, calls
* showToolParameters(toolPanel, voxelTool) and returns toolPanel
* @param showSculptPanel fires voxelMergedBtn so the sculpting sub-tab becomes visible
*/
public VoxelController(ToolContext ctx,
Supplier<Node> sculptPanelSupplier,
Runnable showSculptPanel) {
this.ctx = ctx;
this.sculptPanelSupplier = sculptPanelSupplier;
this.showSculptPanel = showSculptPanel;
}
public void onFrame() {
if (ctx.input().cliffZoneSelectionChanged) {
ctx.input().cliffZoneSelectionChanged = false;
updateVoxelCliffPanel(ctx.input().selectedCliffZoneIdx >= 0);
}
String cliffMsg = ctx.input().cliffGenStatusMsg;
if (cliffMsg != null) {
ctx.input().cliffGenStatusMsg = null;
updateVoxelCliffStatus(cliffMsg);
}
if (ctx.input().cliffGenComplete) {
ctx.input().cliffGenComplete = false;
Platform.runLater(showSculptPanel);
}
}
/** Called from EditorApp keyboard handler on Enter while LAYER_VOXEL_CLIFF is active. */
public void onCliffGenerateKeyPressed() {
if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(true);
if (voxelCliffStatusLabel != null) voxelCliffStatusLabel.setText("Generiere…");
}
public VBox buildPanel() {
VBox panel = new VBox(10);
panel.setPadding(new Insets(10));
panel.setPrefWidth(270);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
ToggleButton subSculptBtn = new ToggleButton("◈ Sculpting");
ToggleButton subCliffBtn = new ToggleButton("/\\ Klippe");
subSculptBtn.setMaxWidth(Double.MAX_VALUE);
subCliffBtn.setMaxWidth(Double.MAX_VALUE);
ToggleGroup subGroup = new ToggleGroup();
subSculptBtn.setToggleGroup(subGroup);
subCliffBtn.setToggleGroup(subGroup);
subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); });
HBox subRow = new HBox(4, subSculptBtn, subCliffBtn);
HBox.setHgrow(subSculptBtn, Priority.ALWAYS);
HBox.setHgrow(subCliffBtn, Priority.ALWAYS);
panel.getChildren().add(subRow);
VBox contentBox = new VBox(0);
panel.getChildren().add(contentBox);
Runnable activateSculpt = () -> {
Node sculptContent = sculptPanelSupplier.get();
contentBox.getChildren().setAll(sculptContent);
};
Runnable activateCliff = () -> {
ctx.input().activeLayer = SharedInput.LAYER_VOXEL_CLIFF;
contentBox.getChildren().setAll(buildVoxelCliffPanel());
};
subSculptBtn.setOnAction(e -> activateSculpt.run());
subCliffBtn.setOnAction(e -> activateCliff.run());
if (ctx.input().activeLayer == SharedInput.LAYER_VOXEL_CLIFF) {
subCliffBtn.setSelected(true);
activateCliff.run();
} else {
subSculptBtn.setSelected(true);
activateSculpt.run();
}
return panel;
}
private VBox buildVoxelCliffPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(
UiHelper.sectionTitle("Voxel-Klippen-Zonen"),
UiHelper.styledHint("L-Klick → Punkt setzen / Startpunkt: Polygon schließen"),
UiHelper.styledHint("R-Klick → Letzten Punkt rückgängig"),
UiHelper.styledHint("ESC → Abbrechen / Zone löschen"),
UiHelper.styledHint("Enter → Generieren + Voxel-Tool öffnen"),
UiHelper.styledHint("Entf → Zone löschen"),
new Separator(),
UiHelper.sectionTitle("Höhen-Parameter"));
Slider maxHeightSlider = new Slider(0, 30, ctx.input().cliffMaxHeight);
maxHeightSlider.valueProperty().addListener((o, ov, nv) ->
ctx.input().cliffMaxHeight = nv.floatValue());
inner.getChildren().addAll(new Label("Max. Höhe über Terrain"), UiHelper.withField(maxHeightSlider, "%.1f"));
Slider noiseScaleSlider = new Slider(0.01, 0.5, ctx.input().cliffNoiseScale);
noiseScaleSlider.valueProperty().addListener((o, ov, nv) ->
ctx.input().cliffNoiseScale = nv.floatValue());
inner.getChildren().addAll(new Label("Höhen-Noise-Frequenz"), UiHelper.withField(noiseScaleSlider, "%.3f"));
Slider edgeBlendSlider = new Slider(0, 30, ctx.input().cliffEdgeBlend);
edgeBlendSlider.valueProperty().addListener((o, ov, nv) ->
ctx.input().cliffEdgeBlend = nv.floatValue());
inner.getChildren().addAll(new Label("Rand-Überblendung"), UiHelper.withField(edgeBlendSlider, "%.1f"));
inner.getChildren().addAll(
new Separator(),
UiHelper.sectionTitle("Oberflächen-Rauigkeit"));
Slider roughAmpSlider = new Slider(0, 20, ctx.input().cliffRoughnessAmp);
roughAmpSlider.valueProperty().addListener((o, ov, nv) ->
ctx.input().cliffRoughnessAmp = nv.floatValue());
inner.getChildren().addAll(new Label("Oberflächen-Amplitude"), UiHelper.withField(roughAmpSlider, "%.1f"));
Slider roughFreqSlider = new Slider(0.01, 0.5, ctx.input().cliffRoughnessScale);
roughFreqSlider.valueProperty().addListener((o, ov, nv) ->
ctx.input().cliffRoughnessScale = nv.floatValue());
inner.getChildren().addAll(new Label("Oberflächen-Frequenz"), UiHelper.withField(roughFreqSlider, "%.3f"));
inner.getChildren().addAll(
new Separator(),
UiHelper.sectionTitle("Gewählte Zone"),
new Separator());
voxelCliffDynamicContent = new VBox(6);
inner.getChildren().add(voxelCliffDynamicContent);
voxelCliffStatusLabel = new Label("");
voxelCliffStatusLabel.setStyle("-fx-text-fill: #555; -fx-font-size: 11;");
voxelCliffStatusLabel.setWrapText(true);
inner.getChildren().add(voxelCliffStatusLabel);
updateVoxelCliffPanel(ctx.input().selectedCliffZoneIdx >= 0);
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(270);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
private void updateVoxelCliffPanel(boolean hasSelection) {
if (voxelCliffDynamicContent == null) return;
voxelCliffDynamicContent.getChildren().clear();
if (!hasSelection) {
Label noSel = new Label("Keine Zone ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
voxelCliffDynamicContent.getChildren().add(noSel);
if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(true);
if (voxelCliffDeleteBtn != null) voxelCliffDeleteBtn.setDisable(true);
return;
}
voxelCliffGenerateBtn = new Button("/\\ Voxel generieren");
voxelCliffGenerateBtn.setMaxWidth(Double.MAX_VALUE);
voxelCliffGenerateBtn.setStyle("-fx-background-color: #4a7eba; -fx-text-fill: white;");
voxelCliffGenerateBtn.setOnAction(e -> {
ctx.input().generateCliffVoxelsRequested = true;
voxelCliffGenerateBtn.setDisable(true);
if (voxelCliffStatusLabel != null) voxelCliffStatusLabel.setText("Generiere…");
});
voxelCliffDeleteBtn = new Button("Loeschen");
voxelCliffDeleteBtn.setMaxWidth(Double.MAX_VALUE);
voxelCliffDeleteBtn.setStyle("-fx-text-fill: #c0392b;");
voxelCliffDeleteBtn.setOnAction(e -> ctx.input().deleteVoxelCliffZoneRequested = true);
Label hint = new Label("Hinweis: Generierung legt Voxel über das Terrain. Bestehende Voxel\nim Bereich werden nicht gelöscht (Reset-Brush verwenden).");
hint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
hint.setWrapText(true);
voxelCliffDynamicContent.getChildren().addAll(
voxelCliffGenerateBtn, voxelCliffDeleteBtn, new Separator(), hint);
}
private void updateVoxelCliffStatus(String msg) {
if (voxelCliffStatusLabel != null) voxelCliffStatusLabel.setText(msg);
if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(false);
}
}

View File

@@ -0,0 +1,347 @@
package de.blight.editor.toolset.water;
import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext;
import de.blight.editor.toolset.UiHelper;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.*;
public class WaterController {
private final ToolContext ctx;
private VBox waterDynamicContent;
private Label waterCurrentHeightLabel;
private VBox wasserContentBox;
public WaterController(ToolContext ctx) {
this.ctx = ctx;
}
public void onFrame() {
if (ctx.input().waterSelectionChanged) {
ctx.input().waterSelectionChanged = false;
updateWaterPanel(ctx.input().selectedWaterInfo);
}
if (ctx.input().waterfallSelectionChanged) {
ctx.input().waterfallSelectionChanged = false;
updateWaterfallPanel(ctx.input().selectedWaterfallInfo);
}
if (ctx.input().waterHeightChanged) {
ctx.input().waterHeightChanged = false;
updateWaterHeightDisplay(ctx.input().waterCurrentHeight);
}
}
public VBox buildPanel() {
VBox panel = new VBox(10);
panel.setPadding(new Insets(10));
panel.setPrefWidth(270);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
Label title = new Label("Wasser");
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;");
panel.getChildren().addAll(title, new Separator());
ToggleButton subWasserBtn = new ToggleButton("~ Wasser");
ToggleButton subWasserfallBtn = new ToggleButton("v Wasserfall");
subWasserBtn.setMaxWidth(Double.MAX_VALUE);
subWasserfallBtn.setMaxWidth(Double.MAX_VALUE);
ToggleGroup subGroup = new ToggleGroup();
subWasserBtn.setToggleGroup(subGroup);
subWasserfallBtn.setToggleGroup(subGroup);
subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); });
HBox subRow = new HBox(4, subWasserBtn, subWasserfallBtn);
HBox.setHgrow(subWasserBtn, Priority.ALWAYS);
HBox.setHgrow(subWasserfallBtn, Priority.ALWAYS);
panel.getChildren().add(subRow);
VBox contentBox = new VBox(0);
wasserContentBox = contentBox;
panel.getChildren().add(contentBox);
Runnable activateWasser = () -> {
ctx.input().activeLayer = SharedInput.LAYER_WATER;
contentBox.getChildren().setAll(buildWaterPanel());
};
Runnable activateWasserfall = () -> {
ctx.input().activeLayer = SharedInput.LAYER_WATERFALL;
contentBox.getChildren().setAll(buildWaterfallPanel());
};
subWasserBtn.setOnAction(e -> activateWasser.run());
subWasserfallBtn.setOnAction(e -> activateWasserfall.run());
if (ctx.input().activeLayer == SharedInput.LAYER_WATERFALL) {
subWasserfallBtn.setSelected(true);
activateWasserfall.run();
} else {
subWasserBtn.setSelected(true);
activateWasser.run();
}
return panel;
}
private VBox buildWaterPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
waterCurrentHeightLabel = new Label("Höhe: –");
waterCurrentHeightLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
inner.getChildren().addAll(
UiHelper.sectionTitle("Wasseroberflächen"),
new Separator(),
waterCurrentHeightLabel,
new Separator(),
UiHelper.sectionTitle("Gewählte Fläche"),
new Separator());
waterDynamicContent = new VBox(6);
Label noSel = new Label("Keine Fläche ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
waterDynamicContent.getChildren().add(noSel);
inner.getChildren().add(waterDynamicContent);
inner.getChildren().addAll(
new Separator(),
UiHelper.styledHint("L-Klick (leer) → erste Ecke setzen"),
UiHelper.styledHint("L-Klick (2.) → Quad abschliessen"),
UiHelper.styledHint("Handle ziehen → Ecke / Kante verschieben"),
UiHelper.styledHint("R-Klick → Abbrechen / Auswahl aufheben"),
UiHelper.styledHint("Leertaste → Höhe vom Terrain übernehmen"),
UiHelper.styledHint("ESC → Abbrechen / Auswahl aufheben"),
UiHelper.styledHint("Entf → Quad löschen"));
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(260);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
private VBox buildWaterfallPanel() {
VBox inner = new VBox(10);
inner.setPadding(new Insets(10));
Button undoBtn = new Button("Letzten Punkt entfernen");
undoBtn.setMaxWidth(Double.MAX_VALUE);
undoBtn.setOnAction(e -> ctx.input().undoWaterfallPointRequested = true);
inner.getChildren().addAll(
UiHelper.sectionTitle("Wasserfall (4-Klick Quad)"),
new Separator(),
undoBtn,
new Separator(),
UiHelper.styledHint("L-Klick → Punkt setzen (1→2→3→4)"),
UiHelper.styledHint("Punkte auf Klippen, Voxel & Seenkanten möglich"),
UiHelper.styledHint("R-Klick / Backspace → letzten Punkt rückgängig"),
UiHelper.styledHint("ESC → Platzierung abbrechen"),
UiHelper.styledHint("Wasserfall anklicken → Ecken verschiebbar"));
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(260);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
private VBox buildWaterfallSelectionPanel(String info) {
String[] parts = info.split("\\|");
int numPts = parts.length > 1 ? Integer.parseInt(parts[1]) : 0;
String length = parts.length > 2 ? parts[2] + " m" : "?";
VBox inner = new VBox(10);
inner.setPadding(new Insets(10));
Label infoLabel = new Label("Punkte: " + numPts + " Länge: ~" + length);
infoLabel.setStyle("-fx-font-size: 11;");
Button deleteBtn = new Button("Wasserfall löschen");
deleteBtn.setMaxWidth(Double.MAX_VALUE);
deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white;");
deleteBtn.setOnAction(e -> {
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
"Wasserfall unwiderruflich löschen?",
ButtonType.YES, ButtonType.NO);
confirm.setTitle("Wasserfall löschen");
confirm.setHeaderText(null);
confirm.initOwner(ctx.stageSupplier().get());
confirm.showAndWait().ifPresent(btn -> {
if (btn == ButtonType.YES) ctx.input().deleteWaterfallRequested = true;
});
});
inner.getChildren().addAll(
UiHelper.sectionTitle("Wasserfall selektiert"),
new Separator(),
infoLabel,
new Separator(),
deleteBtn,
new Separator(),
UiHelper.styledHint("L-Klick ins Leere → abwählen"),
UiHelper.styledHint("L-Klick → anderen Wasserfall wählen"));
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(260);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
private void updateWaterHeightDisplay(float h) {
if (waterCurrentHeightLabel != null)
waterCurrentHeightLabel.setText(String.format("Höhe: %.2f m", h));
}
public void updateWaterPanel(String info) {
if (waterDynamicContent == null) return;
waterDynamicContent.getChildren().clear();
if (info == null) {
Label noSel = new Label("Keine Fläche ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
waterDynamicContent.getChildren().add(noSel);
return;
}
String[] p = info.split("\\|", -1);
if (p.length < 3) return;
try {
float waterHeight = Float.parseFloat(p[1]);
int pointCount = Integer.parseInt(p[2]);
float flowDeg = p.length > 3 ? Float.parseFloat(p[3]) : 0f;
float speed = p.length > 4 ? Float.parseFloat(p[4]) : 0.5f;
float waveScale = p.length > 5 ? Float.parseFloat(p[5]) : 0.008f;
float waveAmp = p.length > 6 ? Float.parseFloat(p[6]) : 0.1f;
float transp = p.length > 7 ? Float.parseFloat(p[7]) : 0.15f;
float wr = p.length > 8 ? Float.parseFloat(p[8]) : 0.05f;
float wg = p.length > 9 ? Float.parseFloat(p[9]) : 0.25f;
float wb = p.length > 10 ? Float.parseFloat(p[10]) : 0.55f;
float dr = p.length > 11 ? Float.parseFloat(p[11]) : 0.02f;
float dg = p.length > 12 ? Float.parseFloat(p[12]) : 0.12f;
float db = p.length > 13 ? Float.parseFloat(p[13]) : 0.30f;
float[] params = {speed, waveScale, waveAmp, transp, wr, wg, wb, dr, dg, db};
Label infoLabel = new Label(String.format("Ecken: %d | Höhe: %.2f m", pointCount, waterHeight));
infoLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
Slider heightSlider = new Slider(-50, 500, waterHeight);
heightSlider.setBlockIncrement(0.1);
heightSlider.setShowTickLabels(false);
heightSlider.valueProperty().addListener((o, ov, nv) ->
ctx.input().pendingWaterHeight.set(nv.floatValue()));
Spinner<Integer> flowSpinner = new Spinner<>(0, 359, Math.round(flowDeg));
flowSpinner.setEditable(true);
flowSpinner.setMaxWidth(Double.MAX_VALUE);
flowSpinner.valueProperty().addListener((o, ov, nv) ->
ctx.input().pendingWaterFlowDegrees.set(nv.floatValue()));
Slider speedSlider = new Slider(0.05, 5.0, speed);
speedSlider.setBlockIncrement(0.05);
speedSlider.valueProperty().addListener((o, ov, nv) -> {
params[0] = nv.floatValue();
ctx.input().pendingWaterParams.set(params.clone());
});
Slider waveScaleSlider = new Slider(0.001, 0.05, waveScale);
waveScaleSlider.setBlockIncrement(0.001);
waveScaleSlider.valueProperty().addListener((o, ov, nv) -> {
params[1] = nv.floatValue();
ctx.input().pendingWaterParams.set(params.clone());
});
Slider waveAmpSlider = new Slider(0.0, 2.0, waveAmp);
waveAmpSlider.setBlockIncrement(0.05);
waveAmpSlider.valueProperty().addListener((o, ov, nv) -> {
params[2] = nv.floatValue();
ctx.input().pendingWaterParams.set(params.clone());
});
Slider transpSlider = new Slider(0.0, 1.0, transp);
transpSlider.setBlockIncrement(0.01);
transpSlider.valueProperty().addListener((o, ov, nv) -> {
params[3] = nv.floatValue();
ctx.input().pendingWaterParams.set(params.clone());
});
javafx.scene.control.ColorPicker waterColorPicker =
new javafx.scene.control.ColorPicker(javafx.scene.paint.Color.color(
Math.min(wr, 1.0), Math.min(wg, 1.0), Math.min(wb, 1.0)));
waterColorPicker.setMaxWidth(Double.MAX_VALUE);
waterColorPicker.valueProperty().addListener((o, ov, nv) -> {
params[4] = (float) nv.getRed();
params[5] = (float) nv.getGreen();
params[6] = (float) nv.getBlue();
ctx.input().pendingWaterParams.set(params.clone());
});
javafx.scene.control.ColorPicker deepColorPicker =
new javafx.scene.control.ColorPicker(javafx.scene.paint.Color.color(
Math.min(dr, 1.0), Math.min(dg, 1.0), Math.min(db, 1.0)));
deepColorPicker.setMaxWidth(Double.MAX_VALUE);
deepColorPicker.valueProperty().addListener((o, ov, nv) -> {
params[7] = (float) nv.getRed();
params[8] = (float) nv.getGreen();
params[9] = (float) nv.getBlue();
ctx.input().pendingWaterParams.set(params.clone());
});
Button delBtn = new Button("Quad löschen");
delBtn.setMaxWidth(Double.MAX_VALUE);
delBtn.setStyle("-fx-text-fill: #c0392b;");
delBtn.setOnAction(e -> ctx.input().deleteWaterRequested = true);
waterDynamicContent.getChildren().addAll(
infoLabel,
new Separator(),
lbl("Höhe (m)"), UiHelper.withField(heightSlider, "%.2f"),
new Separator(),
lbl("Fließrichtung"), flowSpinner,
lbl("Geschwindigkeit"), UiHelper.withField(speedSlider, "%.2f"),
lbl("Wellengröße"), UiHelper.withField(waveScaleSlider, "%.4f"),
lbl("Wellenamplitude"), UiHelper.withField(waveAmpSlider, "%.2f"),
lbl("Transparenz"), UiHelper.withField(transpSlider, "%.2f"),
new Separator(),
lbl("Wasserfarbe"), waterColorPicker,
lbl("Tiefwasserfarbe"), deepColorPicker,
new Separator(),
delBtn);
} catch (NumberFormatException ignored) {}
}
public void updateWaterfallPanel(String info) {
if (ctx.input().activeLayer != SharedInput.LAYER_WATERFALL) return;
VBox panel = info != null ? buildWaterfallSelectionPanel(info) : buildWaterfallPanel();
if (wasserContentBox != null) {
wasserContentBox.getChildren().setAll(panel);
} else {
ctx.setRightPanel().accept(buildPanel());
}
}
private static Label lbl(String text) {
Label l = new Label(text);
l.setStyle("-fx-font-size: 11;");
return l;
}
}

View File

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

View File

@@ -120,6 +120,15 @@ public class PlayerInputControl {
/** Autopilot: wenn gesetzt, geht der Charakter automatisch in diese (normalisierte) Richtung. */
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) -> {
if (paused) return;
switch (name) {
@@ -477,7 +486,8 @@ public class PlayerInputControl {
float t = Math.min(1f, animOffsetElapsed / AnimPlayer.BLEND_DURATION);
animOffsetCurrent.set(animOffsetFrom);
animOffsetCurrent.interpolateLocal(animOffsetTarget, t);
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent));
scratchTransl.set(visualBaseTranslation).addLocal(animOffsetCurrent);
visual.setLocalTranslation(scratchTransl);
}
if (inputsBlocked) return;
@@ -513,19 +523,17 @@ public class PlayerInputControl {
turnRemaining -= tpf;
physicsChar.setWalkDirection(Vector3f.ZERO);
if (visual != null && turnDir != null) {
Quaternion targetRot = new Quaternion();
targetRot.lookAt(turnDir, Vector3f.UNIT_Y);
Quaternion current = visual.getLocalRotation().clone();
current.slerp(targetRot, ROTATE_SPEED * tpf);
visual.setLocalRotation(current);
scratchRot.lookAt(turnDir, Vector3f.UNIT_Y);
scratchCurRot.set(visual.getLocalRotation());
scratchCurRot.slerp(scratchRot, ROTATE_SPEED * tpf);
visual.setLocalRotation(scratchCurRot);
}
if (turnRemaining <= 0f) {
turnActive = false;
// Zielrotation exakt einrasten
if (visual != null && turnDir != null) {
Quaternion snap = new Quaternion();
snap.lookAt(turnDir, Vector3f.UNIT_Y);
visual.setLocalRotation(snap);
scratchRot.lookAt(turnDir, Vector3f.UNIT_Y);
visual.setLocalRotation(scratchRot);
}
Runnable cb = turnCallback;
turnCallback = null;
@@ -557,13 +565,12 @@ public class PlayerInputControl {
// Autopilot: Charakter läuft automatisch in eine vorgegebene Richtung (WALK-Animation)
if (autopilotDir != null) {
physicsChar.setWalkDirection(autopilotDir.mult(MOVE_SPEED * WALK_MULT));
physicsChar.setWalkDirection(scratchWalk.set(autopilotDir).multLocal(MOVE_SPEED * WALK_MULT));
if (visual != null) {
Quaternion targetRot = new Quaternion();
targetRot.lookAt(autopilotDir, Vector3f.UNIT_Y);
Quaternion current = visual.getLocalRotation().clone();
current.slerp(targetRot, ROTATE_SPEED * tpf);
visual.setLocalRotation(current);
scratchRot.lookAt(autopilotDir, Vector3f.UNIT_Y);
scratchCurRot.set(visual.getLocalRotation());
scratchCurRot.slerp(scratchRot, ROTATE_SPEED * tpf);
visual.setLocalRotation(scratchCurRot);
}
if (AnimationAction.WALK != currentAnim) {
playAction(AnimationAction.WALK);
@@ -573,16 +580,16 @@ public class PlayerInputControl {
}
// Normale Spielereingabe (WASD)
Vector3f camDir = cam.getDirection().clone().setY(0).normalizeLocal();
Vector3f camLeft = cam.getLeft().clone().setY(0).normalizeLocal();
cam.getDirection(scratchCamDir).setY(0).normalizeLocal();
cam.getLeft(scratchCamLeft).setY(0).normalizeLocal();
Vector3f moveDir = new Vector3f();
if (forward) moveDir.addLocal(camDir);
if (backward) moveDir.subtractLocal(camDir);
if (left) moveDir.addLocal(camLeft);
if (right) moveDir.subtractLocal(camLeft);
scratchMoveDir.set(0f, 0f, 0f);
if (forward) scratchMoveDir.addLocal(scratchCamDir);
if (backward) scratchMoveDir.subtractLocal(scratchCamDir);
if (left) scratchMoveDir.addLocal(scratchCamLeft);
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
Vector3f charPos = physicsChar.getPhysicsLocation();
@@ -590,18 +597,17 @@ public class PlayerInputControl {
boolean inDeepWater = currentWaterDepth > WATER_WADING_DEPTH;
if (moving) {
moveDir.normalizeLocal();
scratchMoveDir.normalizeLocal();
float speed = (inDeepWater || walk) ? MOVE_SPEED * WALK_MULT
: sprint ? MOVE_SPEED * SPRINT_MULT
: MOVE_SPEED;
physicsChar.setWalkDirection(moveDir.mult(speed));
physicsChar.setWalkDirection(scratchWalk.set(scratchMoveDir).multLocal(speed));
if (visual != null) {
Quaternion targetRot = new Quaternion();
targetRot.lookAt(moveDir, Vector3f.UNIT_Y);
Quaternion current = visual.getLocalRotation().clone();
current.slerp(targetRot, ROTATE_SPEED * tpf);
visual.setLocalRotation(current);
scratchRot.lookAt(scratchMoveDir, Vector3f.UNIT_Y);
scratchCurRot.set(visual.getLocalRotation());
scratchCurRot.slerp(scratchRot, ROTATE_SPEED * tpf);
visual.setLocalRotation(scratchCurRot);
}
} else {
physicsChar.setWalkDirection(Vector3f.ZERO);

View File

@@ -48,6 +48,11 @@ public class ThirdPersonCamera {
private float introDuration;
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) {
this.cam = cam;
@@ -111,28 +116,28 @@ public class ThirdPersonCamera {
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
float x = distance * FastMath.cos(pitch) * FastMath.sin(yaw);
float y = distance * FastMath.sin(pitch);
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) {
float fraction = raycastProvider.apply(pivot, camPos);
float fraction = raycastProvider.apply(scratchPivot, scratchCamPos);
if (fraction < 1f) {
Vector3f dir = camPos.subtract(pivot);
float rayLen = dir.length();
dir.normalizeLocal();
scratchDir.set(scratchCamPos).subtractLocal(scratchPivot);
float rayLen = scratchDir.length();
scratchDir.normalizeLocal();
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.lookAt(pivot, Vector3f.UNIT_Y);
cam.setLocation(scratchCamPos);
cam.lookAt(scratchPivot, Vector3f.UNIT_Y);
}
/** Aktueller Yaw-Winkel (für CharacterControl nutzbar). */

View File

@@ -104,10 +104,18 @@ public class WorldScene extends BaseAppState {
private de.blight.game.state.RiverSoundState riverSound;
private de.blight.game.state.AmbientSoundSystem ambientSounds;
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.state.DrownState drownState;
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) {
this.keyBindings = keyBindings;
this.graphicsSettings = graphicsSettings;
@@ -270,6 +278,8 @@ public class WorldScene extends BaseAppState {
BlightGame.status("Lade Welt-Objekte...");
app.getStateManager().attach(new RiverState());
app.getStateManager().attach(new de.blight.game.state.WaterfallState());
emitterSystem = new de.blight.game.state.EmitterSystem();
app.getStateManager().attach(emitterSystem);
WorldObjectsState worldObjects = new WorldObjectsState();
worldObjects.setLodFactor((float) graphicsSettings.viewDistance.lod0Range);
app.getStateManager().attach(worldObjects);
@@ -460,29 +470,32 @@ public class WorldScene extends BaseAppState {
if (riverSound != null) riverSound.setPlayerPosition(pos);
if (ambientSounds != null) ambientSounds.setPlayerPosition(pos);
if (musicSystem != null) musicSystem.setPlayerPosition(pos);
if (emitterSystem != null) emitterSystem.setPlayerPosition(pos);
}
if (musicSystem != null && dayNight != null)
musicSystem.setDaytime(dayNight.getSunElevation() >= 0f);
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 ac = dayNight.getCustomAmbient();
terrainMaterial.setVector3("SunColor", new Vector3f(sc.r, sc.g, sc.b));
terrainMaterial.setVector3("AmbientColor", new Vector3f(ac.r, ac.g, ac.b));
terrainMaterial.setVector3("SunColor", scratchSunColor.set(sc.r, sc.g, sc.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);
if (rs != null) terrainMaterial.setFloat("Wetness", rs.getWetness());
// Charakter-PBR: Emissive = customAmbient (IBL-Fallback ohne fertige LightProbe)
scratchEmissive.set(ac.r, ac.g, ac.b, 1f);
for (Material mat : characterPbrMaterials) {
mat.setColor("Emissive", new ColorRGBA(ac.r, ac.g, ac.b, 1f));
mat.setColor("Emissive", scratchEmissive);
}
if (lightScatterFilter != null) {
Vector3f sunDir = dayNight.getSunDirection();
// Weltposition der Sonne: von der Kamera aus weit entfernt entgegen der Lichtrichtung
Vector3f sunWorld = app.getCamera().getLocation().add(sunDir.negate().mult(10000f));
lightScatterFilter.setLightPosition(sunWorld);
scratchSunPos.set(sunDir).negateLocal().multLocal(10000f).addLocal(app.getCamera().getLocation());
lightScatterFilter.setLightPosition(scratchSunPos);
// Deaktivieren wenn Sonne hinter der Kamera (gleiche Richtung wie Kamera schaut)
lightScatterFilter.setEnabled(sunDir.dot(app.getCamera().getDirection()) < 0.2f);
}

View File

@@ -147,6 +147,12 @@ public class WorldNpcsState extends BaseAppState {
private float revertTimer = 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 ───────────────────────────────────────────────────────────
public WorldNpcsState(KeyBindings keyBindings, CharacterControl physicsChar,
@@ -244,14 +250,14 @@ public class WorldNpcsState extends BaseAppState {
Vector3f playerPos = physicsChar.getPhysicsLocation();
// Bestehende NPCs prüfen: Position aktualisieren oder entladen
List<SpawnedNpc> toRemove = new ArrayList<>();
toRemoveBuf.clear();
for (SpawnedNpc s : spawned) {
Vector3f pos = resolveRoutinePosition(s.npc(), currentHour);
float dist = pos != null ? dist2d(playerPos, pos) : Float.MAX_VALUE;
if (pos == null || dist > UNLOAD_RADIUS) {
s.visual().removeFromParent();
toRemove.add(s);
toRemoveBuf.add(s);
if (rotNpc == s) rotNpc = null;
if (revertNpc == s) revertNpc = null;
} else {
@@ -260,27 +266,28 @@ public class WorldNpcsState extends BaseAppState {
playNpcAction(s, activityActionFor(s.npc(), currentHour));
}
}
spawned.removeAll(toRemove);
spawned.removeAll(toRemoveBuf);
// PBR-Emissive der verbleibenden NPCs mit aktuellem Ambient aktualisieren
if (dayNight != null) {
com.jme3.math.ColorRGBA ac = dayNight.getCustomAmbient();
scratchEmissive.set(ac.r, ac.g, ac.b, 1f);
for (SpawnedNpc s : spawned) {
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
Set<String> spawnedIds = new HashSet<>();
spawnedIdsBuf.clear();
for (SpawnedNpc s : spawned) {
spawnedIds.add(s.npc().getCharacterId());
spawnedIdsBuf.add(s.npc().getCharacterId());
}
// Neue NPCs einladen wenn in Reichweite
for (NPC npc : allNpcs) {
if (spawnedIds.contains(npc.getCharacterId())) continue;
if (spawnedIdsBuf.contains(npc.getCharacterId())) continue;
Vector3f pos = resolveRoutinePosition(npc, currentHour);
if (pos == null) 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);
if (dayNight != null && !s.pbrMaterials.isEmpty()) {
com.jme3.math.ColorRGBA ac = dayNight.getCustomAmbient();
scratchEmissive.set(ac.r, ac.g, ac.b, 1f);
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);
@@ -482,8 +490,8 @@ public class WorldNpcsState extends BaseAppState {
if (rotNpc == null) return;
rotElapsed += tpf;
float p = FastMath.clamp(rotElapsed / ROT_DURATION, 0f, 1f);
Quaternion q = new Quaternion().slerp(rotFrom, rotTo, p);
rotNpc.visual().setLocalRotation(q);
scratchQ.slerp(rotFrom, rotTo, p);
rotNpc.visual().setLocalRotation(scratchQ);
if (p >= 1f) rotNpc = null;
}
@@ -492,8 +500,8 @@ public class WorldNpcsState extends BaseAppState {
revertTimer += tpf;
if (revertTimer < revertDelay) return;
float p = FastMath.clamp((revertTimer - revertDelay) / REVERT_DURATION, 0f, 1f);
Quaternion q = new Quaternion().slerp(revertFrom, revertTo, p);
revertNpc.visual().setLocalRotation(q);
scratchQ.slerp(revertFrom, revertTo, p);
revertNpc.visual().setLocalRotation(scratchQ);
if (p >= 1f) revertNpc = null;
}

View File

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

View File

@@ -1 +1,3 @@
# 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

View File

@@ -1,2 +1,3 @@
# 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
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