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