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>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,768 @@
|
|||||||
|
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.Orientation;
|
||||||
|
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 objPlaceBtn;
|
||||||
|
private ToggleButton objEditBtn;
|
||||||
|
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());
|
||||||
|
ctx.setMainToolbar().accept(buildObjectEditorToolBar(pendingPlaceMode));
|
||||||
|
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() {
|
||||||
|
pendingPlaceMode = true;
|
||||||
|
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 (ohne Toolbar-Wechsel – für Aufrufe aus dem Welt-Editor). */
|
||||||
|
public void showPlacePanel() {
|
||||||
|
ctx.input().activeLayer = SharedInput.LAYER_OBJECTS;
|
||||||
|
ctx.setRightPanel().accept(buildObjectPlacePanel());
|
||||||
|
if (objPlaceBtn != null) objPlaceBtn.setSelected(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zeigt das Bearbeiten-Panel (ohne Toolbar-Wechsel). */
|
||||||
|
public void showEditPanel() {
|
||||||
|
ctx.input().activeLayer = SharedInput.LAYER_OBJECTS_EDIT;
|
||||||
|
ctx.input().pendingModelPath = null;
|
||||||
|
ctx.setRightPanel().accept(buildObjectEditPanel());
|
||||||
|
if (objEditBtn != null) objEditBtn.setSelected(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toolbar ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private ToolBar buildObjectEditorToolBar(boolean placeMode) {
|
||||||
|
Button backBtn = new Button("← Welteneditor");
|
||||||
|
backBtn.setOnAction(e -> ctx.switchToWorld().run());
|
||||||
|
|
||||||
|
objPlaceBtn = new ToggleButton("+ Platzieren");
|
||||||
|
objEditBtn = new ToggleButton("# Bearbeiten");
|
||||||
|
objPlaceBtn.setStyle("-fx-font-weight: bold;");
|
||||||
|
objEditBtn.setStyle("-fx-font-weight: bold;");
|
||||||
|
|
||||||
|
ToggleGroup objGroup = new ToggleGroup();
|
||||||
|
objPlaceBtn.setToggleGroup(objGroup);
|
||||||
|
objEditBtn.setToggleGroup(objGroup);
|
||||||
|
if (placeMode) objPlaceBtn.setSelected(true);
|
||||||
|
else objEditBtn.setSelected(true);
|
||||||
|
|
||||||
|
objPlaceBtn.setOnAction(e -> showPlacePanel());
|
||||||
|
objEditBtn.setOnAction(e -> showEditPanel());
|
||||||
|
|
||||||
|
Label label = new Label("Objekt Editor");
|
||||||
|
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||||
|
ToolBar tb = new ToolBar();
|
||||||
|
tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL),
|
||||||
|
objPlaceBtn, objEditBtn,
|
||||||
|
new Separator(Orientation.VERTICAL), label);
|
||||||
|
return tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Platzieren-Panel ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private VBox buildObjectPlacePanel() {
|
||||||
|
VBox inner = new VBox(8);
|
||||||
|
inner.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user