diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 192c824..ee60772 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -138,6 +138,8 @@ public class EditorApp extends Application { private de.blight.editor.toolset.eztree.EzTreeController ezTreeController; private de.blight.editor.toolset.anim.AnimPreviewController animPreviewController; private de.blight.editor.toolset.anim.AnimSetController animSetController; + private de.blight.editor.toolset.object.ObjectController objectController; + private de.blight.editor.toolset.model.ModelEditorController modelEditorController; // Asset-Panel: Pfad-Map + DnD-Zustand private final Map, Path> itemPaths = new HashMap<>(); @@ -146,24 +148,6 @@ public class EditorApp extends Application { private final java.util.Set> textureSetNodes = new java.util.HashSet<>(); private TreeItem draggedItem = null; - // Objekt-Werkzeug-Zustand - private Label objModelLabel; // zeigt den ausgewählten Modell-Pfad - private Label objPosLabel; // zeigt Position/Rotation - private CheckBox objSolidCB; // Solid-Flag des selektierten Objekts - private CheckBox objCastShadowCB; // Schatten werfen - private CheckBox objReceiveShadowCB; // Schatten empfangen - private ComboBox objCatCombo; // Objekt-Kategorie - private CheckBox multiPlaceCB; // Mehrfach-Platzierungs-Modus - 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; // Cache: Menge aller j3o-Pfade die ein Skelett besitzen (null = Scan noch nicht abgeschlossen) private java.util.Set skeletalModelPaths = null; @@ -247,9 +231,6 @@ public class EditorApp extends Application { private boolean pendingNewGame = false; private boolean pendingAutostart = true; - // Baum-Ordner-Modus - private Label randomTreeStatusLabel; - private ComboBox treeFolderCB; // Tripo3D-Generator-Zustand private ImageView tripoPreviewView; @@ -261,50 +242,6 @@ public class EditorApp extends Application { private String tripoLastModelUrl; private String tripoLastTaskId; - // Modell-Editor-Zustand - private Label modelEditorDimLabel; - private Label modelEditorPolyLabel; - private Spinner modelEditorSpinX, modelEditorSpinY, modelEditorSpinZ; - private CheckBox modelEditorUniformCB; - private boolean modelEditorSuppressListeners = false; - private String modelEditorCurrentPath = null; - private String modelEditorLod1Path = ""; - private String modelEditorLod2Path = ""; - private Label modelEditorLod1Label; - private Label modelEditorLod2Label; - private ToggleButton modelEditorLod0Btn; - private ToggleButton modelEditorLod1Btn; - private ToggleButton modelEditorLod2Btn; - private Label modelEditorLod1StatusLabel; - private Label modelEditorLod2StatusLabel; - private Button modelEditorLod1GenBtn; - private Button modelEditorLod2GenBtn; - private ToggleButton importLod0Btn; - private ToggleButton importLod1Btn; - private ToggleButton importLod2Btn; - private Button modelImportLod1ClearBtn; - private Button modelImportLod2ClearBtn; - private Button modelImportLod2GenBtn; - // Anhänge (veränderliche Listen, werden bei Änderung an JME übertragen) - private final java.util.List modelEditorLights = new java.util.ArrayList<>(); - private final java.util.List modelEditorEmitters = new java.util.ArrayList<>(); - private javafx.scene.layout.VBox modelEditorLightBox = null; - private javafx.scene.layout.VBox modelEditorEmitterBox = null; - private ComboBox modelEditorCategoryCB = null; - private ComboBox modelEditorInteractableCB = null; - private Spinner modelEditorInteractableXSpin = null; - private Spinner modelEditorInteractableYSpin = null; - private Spinner modelEditorInteractableZSpin = null; - private ComboBox modelEditorFootstepSurfaceCB = null; - private boolean updatingInteractableSpinnersFromJme = false; - - // Modell-Import-Zustand - private Label modelImportLod1StatusLabel; - private Label modelImportLod2StatusLabel; - private Label modelImportExportStatusLabel; - private String modelImportCurrentPath = null; - /** Warteschlange für Mehrfach-Import – nächste Datei wird nach Export der aktuellen geladen. */ - private final java.util.Deque modelImportQueue = new java.util.ArrayDeque<>(); // Asset-Overlay-Zustand private boolean assetOverlayOpen = false; @@ -320,7 +257,6 @@ public class EditorApp extends Application { private ToggleButton textureBtn; private ToggleButton stoneBtn; private ToggleButton objPlaceBtn; // Unified Objekte-Button (Place + Edit) - private ToggleButton objEditBtn; // nur im Object-Editor-Toolbar (nicht Welt-Toolbar) private ToggleButton lightBtn; // Sub-Toggle in Atmosphäre-Panel private ToggleButton emitterBtn; // Sub-Toggle in Atmosphäre-Panel private ToggleButton waterBtn; // Sub-Toggle in Wasser-Panel @@ -337,11 +273,6 @@ public class EditorApp extends Application { private RadioMenuItem camOrbitItem; private RadioMenuItem camFreeItem; - // "Objekt"-Button in der Selektionsleiste (zum Zurückschalten bei importierten Objekten) - private ToggleButton selModeObjectBtn; - - // Mesh-Primitiv-Auswahl (Platzieren-Modus) - private ToggleGroup meshToggleGroup; // Drag-Tracking für Kamerarotation (mittlere Taste) private double prevDragX, prevDragY; @@ -435,6 +366,9 @@ public class EditorApp extends Application { ezTreeController = new de.blight.editor.toolset.eztree.EzTreeController(toolCtx); animPreviewController = new de.blight.editor.toolset.anim.AnimPreviewController(toolCtx); animSetController = new de.blight.editor.toolset.anim.AnimSetController(toolCtx); + objectController = new de.blight.editor.toolset.object.ObjectController(toolCtx); + modelEditorController = new de.blight.editor.toolset.model.ModelEditorController(toolCtx, this::openAssetOverlay); + modelEditorController.setObjectController(objectController); Scene scene = new Scene(root, 1280, 760); java.net.URL cssUrl = getClass().getResource("/editor.css"); @@ -609,24 +543,6 @@ public class EditorApp extends Application { if (charEditorStatusLabel != null) charEditorStatusLabel.setText(stripStatus); } - String rts = input.randomTreeStatus; - if (randomTreeStatusLabel != null && rts != null) { - randomTreeStatusLabel.setText(rts); - } - - // Modell-Editor: Ruhepunkt-Spinner nach Raycast-Klick (JME→JFX) - if (input.modelInteractablePosSetFromJme && modelEditorInteractableXSpin != null) { - input.modelInteractablePosSetFromJme = false; - updatingInteractableSpinnersFromJme = true; - modelEditorInteractableXSpin.getValueFactory().setValue( - Math.round(input.modelInteractableOffsetX * 100.0) / 100.0); - modelEditorInteractableYSpin.getValueFactory().setValue( - Math.round(input.modelInteractableOffsetY * 100.0) / 100.0); - modelEditorInteractableZSpin.getValueFactory().setValue( - Math.round(input.modelInteractableOffsetZ * 100.0) / 100.0); - updatingInteractableSpinnersFromJme = false; - } - if (input.refreshAssets) { input.refreshAssets = false; boolean pe = shouldPreserveExpansion(); @@ -635,31 +551,6 @@ public class EditorApp extends Application { if (charactersNode != null) refreshCategoryNode(charactersNode, pe); } - if (input.refreshTreeFolders) { - input.refreshTreeFolders = false; - populateTreeFolderCombo(); - } - - if (input.objectJustPlaced) { - input.objectJustPlaced = false; - if (multiPlaceCB == null || !multiPlaceCB.isSelected()) { - input.activeLayer = SharedInput.LAYER_OBJECTS_EDIT; - if (objPlaceBtn != null) objPlaceBtn.setSelected(true); - root.setRight(buildObjectEditPanel()); - } - } - - if (input.objectClickedWhileInPlaceMode) { - input.objectClickedWhileInPlaceMode = false; - if (objPlaceBtn != null) objPlaceBtn.setSelected(true); - root.setRight(buildObjectEditPanel()); - } - - if (input.objectSelectionChanged) { - input.objectSelectionChanged = false; - updateObjectPanel(input.selectedObjectInfo); - } - if (input.lightSelectionChanged) { input.lightSelectionChanged = false; updateLightPanel(input.selectedLightInfo); @@ -809,98 +700,6 @@ public class EditorApp extends Application { if (permSpawnCoordsLabel != null) permSpawnCoordsLabel.setText(permSpawnCoordsText()); } - // Modell-Editor: gebakte Scale aus j3o erkannt → Spinner aktualisieren - if (input.modelEditorBakedScaleDetected) { - input.modelEditorBakedScaleDetected = false; - double bsX = input.modelEditorScaleX; - double bsY = input.modelEditorScaleY; - double bsZ = input.modelEditorScaleZ; - if (modelEditorSpinX != null) modelEditorSpinX.getValueFactory().setValue(bsX); - if (modelEditorSpinY != null) modelEditorSpinY.getValueFactory().setValue(bsY); - if (modelEditorSpinZ != null) modelEditorSpinZ.getValueFactory().setValue(bsZ); - } - - // Modell-Editor: Bounds-Aktualisierung - if (input.modelEditorBoundsReady) { - input.modelEditorBoundsReady = false; - updateModelEditorDimensions( - input.modelEditorBoundsW, - input.modelEditorBoundsH, - input.modelEditorBoundsD, - input.modelEditorPolyCount); - } - - // Modell-Editor: eingebettete LODs erkannt → Labels aktualisieren - if (input.modelEditorHasEmbeddedLod1 && modelEditorLod1Label != null - && "(kein Pfad)".equals(modelEditorLod1Label.getText())) { - modelEditorLod1Label.setText("(eingebettet im j3o)"); - modelEditorLod1Label.setStyle("-fx-text-fill:#6aaa6a; -fx-font-size:11;"); - } - if (input.modelEditorHasEmbeddedLod2 && modelEditorLod2Label != null - && "(kein Pfad)".equals(modelEditorLod2Label.getText())) { - modelEditorLod2Label.setText("(eingebettet im j3o)"); - modelEditorLod2Label.setStyle("-fx-text-fill:#6aaa6a; -fx-font-size:11;"); - } - // Model-Editor LOD-Buttons freischalten/sperren (per Level) - if (modelEditorLod1Btn != null) - modelEditorLod1Btn.setDisable(!input.modelEditorHasEmbeddedLod1 && modelEditorLod1Path.isBlank() && !input.modelImportHasLod1); - if (modelEditorLod2Btn != null) - modelEditorLod2Btn.setDisable(!input.modelEditorHasEmbeddedLod2 && modelEditorLod2Path.isBlank() && !input.modelImportHasLod2); - // Generieren-Buttons: nur aktiv wenn genau dieses LOD-Level noch nicht gesetzt - if (modelEditorLod1GenBtn != null) - modelEditorLod1GenBtn.setDisable(input.modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || input.modelImportHasLod1); - if (modelEditorLod2GenBtn != null) { - boolean lod1Set = input.modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || input.modelImportHasLod1; - boolean lod2Set = input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2; - modelEditorLod2GenBtn.setDisable(!lod1Set || lod2Set); - } - // Importer LOD-Buttons freischalten sobald generiert - if (importLod1Btn != null) importLod1Btn.setDisable(!input.modelImportHasLod1); - if (importLod2Btn != null) importLod2Btn.setDisable(!input.modelImportHasLod2); - // Ausgewählten LOD-Button mit aktuellem Preview-Level synchronisieren - int lodPreview = input.modelEditorLodPreview; - if (modelEditorLod0Btn != null) { modelEditorLod0Btn.setSelected(lodPreview == 0); } - if (modelEditorLod1Btn != null) { modelEditorLod1Btn.setSelected(lodPreview == 1); } - if (modelEditorLod2Btn != null) { modelEditorLod2Btn.setSelected(lodPreview == 2); } - if (importLod0Btn != null) { importLod0Btn.setSelected(lodPreview == 0); } - if (importLod1Btn != null) { importLod1Btn.setSelected(lodPreview == 1); } - if (importLod2Btn != null) { importLod2Btn.setSelected(lodPreview == 2); } - if (modelImportLod1ClearBtn != null) modelImportLod1ClearBtn.setDisable(!input.modelImportHasLod1); - if (modelImportLod2ClearBtn != null) modelImportLod2ClearBtn.setDisable(!input.modelImportHasLod2); - if (modelImportLod2GenBtn != null) modelImportLod2GenBtn.setDisable(!input.modelImportHasLod1 || input.modelImportHasLod2); - - // Modell-Import/Editor: LOD-Status - String lodStatus = input.modelLodGenStatus; - if (lodStatus != null) { - input.modelLodGenStatus = null; - boolean isLod1 = lodStatus.startsWith("LOD1"); - boolean isLod2 = lodStatus.startsWith("LOD2") || lodStatus.contains("Impostor"); - if (isLod1) { - if (modelImportLod1StatusLabel != null) modelImportLod1StatusLabel.setText(lodStatus); - if (modelEditorLod1StatusLabel != null) modelEditorLod1StatusLabel.setText(lodStatus); - } else if (isLod2) { - if (modelImportLod2StatusLabel != null) modelImportLod2StatusLabel.setText(lodStatus); - if (modelEditorLod2StatusLabel != null) modelEditorLod2StatusLabel.setText(lodStatus); - } - setStatus(lodStatus); - } - // Modell-Import: Export-Ergebnis - String expStatus = input.modelImportExportStatus; - if (expStatus != null) { - input.modelImportExportStatus = null; - if (modelImportExportStatusLabel != null) modelImportExportStatusLabel.setText(expStatus); - int remaining = modelImportQueue.size(); - setStatus("Import: " + expStatus + (remaining > 0 ? " (" + remaining + " weitere)" : "")); - if (!expStatus.startsWith("FEHLER")) { - input.refreshAssets = true; - } - // Nächste Datei aus Warteschlange laden - if (!modelImportQueue.isEmpty()) { - File next = modelImportQueue.poll(); - startImportFile(next); - } - } - // Kamera-Koordinaten aktualisieren String camText = String.format( "X:%.1f Y:%.1f Z:%.1f Yaw:%.0f° Pitch:%.0f°", @@ -1043,88 +842,6 @@ public class EditorApp extends Application { return tb; } - private void switchToObjectEditor(boolean placeMode) { - currentTool = "objecteditor"; - topBar.getChildren().set(1, buildObjectEditorToolBar(placeMode)); - setCenterView(worldViewport); - if (placeMode) { - input.activeLayer = SharedInput.LAYER_OBJECTS; - root.setRight(buildObjectPlacePanel()); - } else { - input.activeLayer = SharedInput.LAYER_OBJECTS_EDIT; - input.pendingModelPath = null; - root.setRight(buildObjectEditPanel()); - } - } - - private ToolBar buildObjectEditorToolBar(boolean placeMode) { - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - - 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 -> { - input.activeLayer = SharedInput.LAYER_OBJECTS; - root.setRight(buildObjectPlacePanel()); - }); - objEditBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_OBJECTS_EDIT; - input.pendingModelPath = null; - root.setRight(buildObjectEditPanel()); - }); - - 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; - } - - private ToolBar buildModelEditorToolBar() { - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - Label label = new Label("Model Editor"); - label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - - modelEditorLod0Btn = new ToggleButton("LOD 0"); - ToggleButton lod0Btn = modelEditorLod0Btn; - modelEditorLod1Btn = new ToggleButton("LOD 1"); - modelEditorLod2Btn = new ToggleButton("LOD 2"); - javafx.scene.control.ToggleGroup lodGroup = new javafx.scene.control.ToggleGroup(); - lod0Btn.setToggleGroup(lodGroup); - modelEditorLod1Btn.setToggleGroup(lodGroup); - modelEditorLod2Btn.setToggleGroup(lodGroup); - lod0Btn.setSelected(true); - String lodStyle = "-fx-font-size:11; -fx-padding:2 8 2 8;"; - lod0Btn.setStyle(lodStyle); - modelEditorLod1Btn.setStyle(lodStyle); - modelEditorLod2Btn.setStyle(lodStyle); - lod0Btn.setOnAction(e -> { input.modelEditorLodPreview = 0; input.modelEditorLodChanged = true; }); - modelEditorLod1Btn.setOnAction(e -> { input.modelEditorLodPreview = 1; input.modelEditorLodChanged = true; }); - modelEditorLod2Btn.setOnAction(e -> { input.modelEditorLodPreview = 2; input.modelEditorLodChanged = true; }); - // Initial deaktiviert – werden freigegeben sobald LOD-Pfade/eingebettete LODs bekannt sind - boolean hasLod1 = input.modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || input.modelImportHasLod1; - boolean hasLod2 = input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2; - modelEditorLod1Btn.setDisable(!hasLod1); - modelEditorLod2Btn.setDisable(!hasLod2); - - ToolBar tb = new ToolBar(); - tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label, - new Separator(Orientation.VERTICAL), lod0Btn, - modelEditorLod1Btn, modelEditorLod2Btn); - return tb; - } private void switchToEzTree() { ensureNotContentEditor(); @@ -1182,7 +899,7 @@ public class EditorApp extends Application { MenuItem importAudioItem = new MenuItem("Audio…"); MenuItem importAnimItem = new MenuItem("Animationen…"); MenuItem importCharModelItem = new MenuItem("Character-Modell (FBX)…"); - importModelLodItem.setOnAction(e -> openModelImport(primaryStage)); + importModelLodItem.setOnAction(e -> modelEditorController.openModelImport(primaryStage)); importTexItem.setOnAction(e -> handleTextureImport(primaryStage)); importTexZipItem.setOnAction(e -> handleZipTextureImport(primaryStage)); importTexSetItem.setOnAction(e -> openTextureSetImport(primaryStage)); @@ -1204,7 +921,7 @@ public class EditorApp extends Application { ezTreeItem.setOnAction(e -> switchToEzTree()); tripoItem.setOnAction(e -> switchToTripo()); animPrevItem.setOnAction(e -> openCharacterEditor()); - objEditorItem.setOnAction(e -> openModelEditor(null, null)); + objEditorItem.setOnAction(e -> modelEditorController.openModelEditor(null, null)); thumbItem.setOnAction(e -> switchToThumbnailManager()); toolsMenu.getItems().addAll(vegetationsItem, ezTreeItem, tripoItem, animPrevItem, objEditorItem, new SeparatorMenuItem(), thumbItem); @@ -1350,11 +1067,9 @@ public class EditorApp extends Application { showToolParameters(toolPanel, input.activeTool); }); objPlaceBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_OBJECTS; - input.pendingModelPath = null; if (modelsNode != null) modelsNode.setExpanded(true); openAssetOverlay(); - root.setRight(buildObjectPlacePanel()); + objectController.activatePlaceMode(); }); atmosphaereBtn.setOnAction(e -> root.setRight(buildAtmospherePanel())); wasserMergedBtn.setOnAction(e -> root.setRight(buildWasserPanel())); @@ -1523,336 +1238,6 @@ public class EditorApp extends Application { // ── Objekt-Werkzeug – Platzieren-Panel ────────────────────────────────── - private VBox buildObjectPlacePanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - - // ── Baum aus Ordner ─────────────────────────────────────────────────── - 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)")) { - input.treeFolderPath = null; - if (randomTreeStatusLabel != null) randomTreeStatusLabel.setText(""); - } else { - input.treeFolderPath = sel; - 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")); - - // ── Modell aus Asset-Baum ───────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Modell"), new Separator()); - objModelLabel = new Label(input.pendingModelPath != null - && !input.pendingModelPath.startsWith("@") - ? 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")); - - // ── Textur (optional, nur für Primitive) ───────────────────────────── - inner.getChildren().addAll(sectionTitle("Textur"), new Separator()); - Label texLabel = new Label(input.pendingTexturePath != null && !input.pendingTexturePath.isEmpty() - ? labelFromPath(input.pendingTexturePath) - : "(keine)"); - texLabel.setWrapText(true); - texLabel.setStyle("-fx-text-fill: #333; -fx-font-size: 11;"); - Button clearTexBtn = new javafx.scene.control.Button("✕"); - clearTexBtn.setTooltip(new Tooltip("Textur entfernen")); - clearTexBtn.setOnAction(ev -> { - input.pendingTexturePath = ""; - root.setRight(buildObjectPlacePanel()); - }); - HBox texRow = new HBox(6, texLabel, clearTexBtn); - texRow.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - HBox.setHgrow(texLabel, Priority.ALWAYS); - inner.getChildren().add(texRow); - inner.getChildren().add(styledHint("Doppelklick auf Textur links → auswählen")); - - // ── Platziermodus ───────────────────────────────────────────────────── - 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); - - // ── Meshes (Primitive) ──────────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Meshes"), new Separator()); - - meshToggleGroup = new ToggleGroup(); - String[][] shapes = { - {"Box", "@box"}, - {"Kugel", "@sphere"}, - {"Zylinder", "@cylinder"}, - {"Ebene", "@plane"}, - }; - - // 2-spaltige Gitteranordnung - javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane(); - grid.setHgap(6); - grid.setVgap(6); - grid.setMaxWidth(Double.MAX_VALUE); - - for (int i = 0; i < shapes.length; i++) { - String label = shapes[i][0]; - String marker = shapes[i][1]; - ToggleButton btn = new ToggleButton(label); - btn.setToggleGroup(meshToggleGroup); - btn.setMaxWidth(Double.MAX_VALUE); - btn.setStyle("-fx-font-size: 12;"); - btn.setOnAction(e -> { - if (btn.isSelected()) { - input.pendingModelPath = marker; - if (objModelLabel != null) - objModelLabel.setText("(Doppelklick im Asset-Baum)"); - setStatus("Mesh: " + label + " | Linksklick ins Terrain zum Platzieren"); - } else { - input.pendingModelPath = null; - } - }); - // Vorauswahl wiederherstellen - if (marker.equals(input.pendingModelPath)) btn.setSelected(true); - grid.add(btn, i % 2, i / 2); - javafx.scene.layout.GridPane.setHgrow(btn, Priority.ALWAYS); - } - inner.getChildren().add(grid); - - // ── Terrain-Ausrichten ──────────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Terrain-Ausrichten"), new Separator()); - CheckBox alignCB = new CheckBox("An Terrain angleichen"); - alignCB.setSelected(input.terrainAlignEnabled); - alignCB.setStyle("-fx-text-fill: #111111;"); - alignCB.setOnAction(e -> input.terrainAlignEnabled = alignCB.isSelected()); - inner.getChildren().add(alignCB); - inner.getChildren().add(styledHint("Passt rotX/rotZ an die Geländesteigung an")); - - // ── Vertex-Snap (beim Platzieren von custom Meshes) ─────────────────── - inner.getChildren().addAll(sectionTitle("Vertex-Snap"), new Separator()); - CheckBox placeSnapCB = new CheckBox("Snap aktiv"); - placeSnapCB.setSelected(input.vertexSnapEnabled); - placeSnapCB.setStyle("-fx-text-fill: #111111;"); - placeSnapCB.setOnAction(e -> input.vertexSnapEnabled = placeSnapCB.isSelected()); - - Slider placeSnapSlider = new Slider(0.05, 2.0, input.vertexSnapRadius); - placeSnapSlider.setShowTickLabels(false); - placeSnapSlider.setBlockIncrement(0.05); - placeSnapSlider.valueProperty().addListener((o, ov, nv) -> 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; - } - - // ── Objekt-Werkzeug – Bearbeiten-Panel ────────────────────────────────── - - /** Selektionsmodus-Leiste: Objekt / Polygon / Kante / Punkt */ - 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] == input.objectSelectionMode) btn.setSelected(true); - int mode = modes[i]; - btn.setOnAction(e -> 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; - } - - /** Schaltet den Selektionsmodus visuell auf "Objekt" zurück. */ - private void resetSelectionModeToObject() { - if (selModeObjectBtn != null) selModeObjectBtn.setSelected(true); - } - - /** Bearbeitungswerkzeug-Leiste: Bewegen / Rotieren / Skalieren */ - 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() == input.objectEditTool) btn.setSelected(true); - int mode = t.mode(); - btn.setOnAction(e -> input.objectEditTool = mode); - if (t.mode() == SharedInput.EDIT_TOOL_SCALE) btn.setStyle("-fx-opacity: 0.55;"); - bar.getChildren().add(btn); - } - bar.setStyle("-fx-font-size: 11;"); - - VBox box = new VBox(3, new Label("Werkzeug"), bar); - return box; - } - - private void populateTreeFolderCombo() { - if (treeFolderCB == null) return; - String prev = treeFolderCB.getValue(); - - java.nio.file.Path treeRoot = de.blight.editor.ProjectRoot.resolve( - "blight-assets", "src", "main", "resources", "trees"); - java.util.List items = new java.util.ArrayList<>(); - items.add("(keiner)"); - if (java.nio.file.Files.isDirectory(treeRoot)) { - try (var walk = java.nio.file.Files.walk(treeRoot)) { - walk.filter(java.nio.file.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) {} - } - - // Aktuell gesetzten Pfad in die Liste aufnehmen, auch wenn er außerhalb von trees/ liegt - String cur = 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)"); - } - } - - private VBox buildObjectEditPanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - CheckBox snapCB = new CheckBox("Snap – Punkte verschmelzen"); - snapCB.setSelected(input.vertexSnapEnabled); - snapCB.setStyle("-fx-font-size: 11;"); - snapCB.setOnAction(e -> input.vertexSnapEnabled = snapCB.isSelected()); - - Slider snapRadiusSlider = new Slider(0.05, 2.0, input.vertexSnapRadius); - snapRadiusSlider.setShowTickLabels(false); - snapRadiusSlider.setBlockIncrement(0.05); - snapRadiusSlider.valueProperty().addListener((o, ov, nv) -> 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 -> 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(primaryStage); - dlg.setTitle("Vorlage speichern"); - dlg.setHeaderText("Name der Vorlage (ohne .j3o):"); - dlg.setContentText("Name:"); - dlg.showAndWait().ifPresent(name -> { - if (!name.trim().isEmpty()) 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 -> 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; - } - - // ── Licht-Panel ─────────────────────────────────────────────────────────── - private VBox buildLightPanel() { VBox inner = new VBox(8); inner.setPadding(new Insets(10)); @@ -2455,294 +1840,6 @@ public class EditorApp extends Application { return lbl; } - private void updateObjectPanel(String info) { - if (objDynamicContent == null) return; - - // Null field references first to prevent stale focus-lost events from firing - 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) { - // ── Einzelselektion ────────────────────────────────────────────────── - String modelPath = parts[1]; - // Bei importierten Objekten visuell auf "Objekt" zurückschalten - // (JME-Seite setzt objectSelectionMode bereits zurück). - 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; - de.blight.common.ModelCategory modelCat = parts.length >= 19 - ? de.blight.common.ModelCategory.fromString(parts[18]) - : de.blight.common.ModelCategory.NONE; - - // Modell-Pfad - 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(de.blight.common.ModelCategory.values()); - objCatCombo.setValue(modelCat); - objCatCombo.setConverter(new javafx.util.StringConverter<>() { - @Override public String toString(de.blight.common.ModelCategory c) { - return c == null ? "" : c.getLabel(); - } - @Override public de.blight.common.ModelCategory fromString(String s) { - return de.blight.common.ModelCategory.fromString(s); - } - }); - objCatCombo.setMaxWidth(Double.MAX_VALUE); - objCatCombo.setOnAction(ev -> enqueuePropertyChange(null, null, null)); - HBox catRow = new HBox(6, new javafx.scene.control.Label("Kategorie:"), objCatCombo); - catRow.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - - // Textur - 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(ASSET_ROOT, false); - if (primaryStage != null) chooser.initOwner(primaryStage); - 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); - - // Normal Map - 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(ASSET_ROOT, true); - if (primaryStage != null) chooser.initOwner(primaryStage); - 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); - - // Material - 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(ASSET_ROOT.resolve("MatDefs")); - if (primaryStage != null) chooser.initOwner(primaryStage); - 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); - - // Position - objXField = makeFloatField(x); - objYField = makeFloatField(y); - objZField = makeFloatField(z); - - // Rotation - objRotXField = makeFloatField(rotXDeg); - objRotYField = makeFloatField(rotYDeg); - objRotZField = makeFloatField(rotZDeg); - - // Skalierung - objScaleField = makeFloatField(scale); - - // "Übernehmen"-Button: liest Position, Rotation und Scale aus den Feldern - // und schreibt sie atomar in die JME-Szene. Kein Auto-Apply bei Focus-Lost, - // damit versehentliche Tab-Tastendrücke das Objekt nicht verschieben. - Button uebernehmenBtn = new Button("✔ Übernehmen"); - uebernehmenBtn.setMaxWidth(Double.MAX_VALUE); - uebernehmenBtn.setDefaultButton(true); - uebernehmenBtn.setOnAction(ev -> enqueuePropertyChange(null, null, null)); - - // Solid / Schatten - 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)); - - // Animation - ComboBox animCombo = new ComboBox<>(); - animCombo.setMaxWidth(Double.MAX_VALUE); - animCombo.getItems().add("(keine)"); - Path animDir = ASSET_ROOT.resolve("Characters").resolve("clips"); - if (java.nio.file.Files.isDirectory(animDir)) { - try (java.util.stream.Stream walk = java.nio.file.Files.walk(animDir)) { - walk.filter(p -> p.toString().endsWith(".j3o")) - .map(p -> animDir.relativize(p).toString().replace('\\', '/')) - .sorted() - .forEach(animCombo.getItems()::add); - } catch (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(); - input.pendingAnimClip = "(keine)".equals(val) ? "" : val; - }); - - // Scale-Feld: breiteren TextField, da nur ein Wert - HBox scaleRow = new HBox(6, new Label("Skalierung:"), objScaleField); - scaleRow.setAlignment(javafx.geometry.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 { - // ── Mehrselektion ──────────────────────────────────────────────────── - 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 -> 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); - } - - // ── Objekt-Bearbeiten-Hilfsmethoden ───────────────────────────────────── - - 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; - } - - /** Normalisiert einen vom Nutzer eingetippten Dezimalwert (Komma oder Punkt). */ - 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 void hookApply(TextField tf) { - tf.setOnAction(e -> enqueuePropertyChange(null, null, null)); - tf.focusedProperty().addListener((o, ov, nv) -> { if (!nv) enqueuePropertyChange(null, null, null); }); - } - - 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(); - de.blight.common.ModelCategory cat = objCatCombo != null ? objCatCombo.getValue() : null; - input.objectPropertyQueue.offer( - new SharedInput.ObjectPropertyChange(x, y, z, rotX, rotY, rotZ, scale, - solid, castShadow, receiveShadow, - texOverride, normalOverride, matOverride, cat)); - } catch (NumberFormatException ignored) {} - } - - // ── Rechte Seite: Tool-Parameter-Panel (Welteneditor) ──────────────────── - private VBox buildToolPanel() { VBox panel = new VBox(10); panel.setPadding(new Insets(10)); @@ -4374,12 +3471,10 @@ public class EditorApp extends Application { Path folderPath = itemPaths.get(sel); if (folderPath != null && java.nio.file.Files.isDirectory(folderPath)) { String relFolder = ASSET_ROOT.relativize(folderPath).toString().replace('\\', '/'); - input.treeFolderPath = relFolder; - input.pendingModelPath = null; - input.activeLayer = SharedInput.LAYER_OBJECTS; - if (objPlaceBtn != null) objPlaceBtn.setSelected(true); - root.setRight(buildObjectPlacePanel()); - if (multiPlaceCB != null) multiPlaceCB.setSelected(true); + input.treeFolderPath = relFolder; + input.pendingModelPath = null; + objectController.activatePlaceMode(); + objectController.enableMultiPlace(); setStatus("Ordner: " + relFolder + " | Linksklick → zufälliges Modell | Rechtsklick → neu würfeln"); } } @@ -4414,19 +3509,15 @@ public class EditorApp extends Application { if (cat == modelsNode) { if ("modeleditor".equals(currentTool) && relPath.endsWith(".j3o")) { - openModelEditor(relPath, p); + modelEditorController.openModelEditor(relPath, p); return; } input.pendingModelPath = relPath; - input.activeLayer = SharedInput.LAYER_OBJECTS; - if (objPlaceBtn != null) objPlaceBtn.setSelected(true); - root.setRight(buildObjectPlacePanel()); + objectController.activatePlaceMode(); setStatus("Modell: " + relPath + " | Linksklick ins Terrain zum Platzieren"); } else if (cat == texturesNode) { input.pendingTexturePath = relPath; - input.activeLayer = SharedInput.LAYER_OBJECTS; - if (objPlaceBtn != null) objPlaceBtn.setSelected(true); - root.setRight(buildObjectPlacePanel()); + objectController.activatePlaceMode(); setStatus("Textur: " + relPath + " | Wird auf platzierte Primitive angewendet"); } else if ((cat == animationsNode || cat == charactersNode) && relPath.endsWith(".j3o") && isSkeletal) { switchToAnimPreview(); @@ -4458,7 +3549,7 @@ public class EditorApp extends Application { MenuItem miTexZip = new MenuItem("Texturen-ZIP…"); MenuItem miAudio = new MenuItem("Audio…"); MenuItem miAnim = new MenuItem("Animationen…"); - miModel.setOnAction(ev -> openModelImport(importBtn.getScene().getWindow() instanceof javafx.stage.Stage s ? s : primaryStage)); + miModel.setOnAction(ev -> modelEditorController.openModelImport(importBtn.getScene().getWindow())); miTex.setOnAction(ev -> handleTextureImport(importBtn.getScene().getWindow())); miTexZip.setOnAction(ev -> handleZipTextureImport(importBtn.getScene().getWindow())); miAudio.setOnAction(ev -> handleAudioImport(importBtn.getScene().getWindow())); @@ -4488,7 +3579,7 @@ public class EditorApp extends Application { input.pendingGotoZ = z; input.pendingGotoPitch = (float) (-Math.PI / 3); switch (toolHint) { - case "model" -> { input.activeLayer = SharedInput.LAYER_OBJECTS_EDIT; root.setRight(buildObjectEditPanel()); } + case "model" -> { objectController.activateEditMode(); } case "item" -> { input.activeLayer = SharedInput.LAYER_ITEMS; root.setRight(buildItemPlacePanel(null)); } case "light" -> { input.activeLayer = SharedInput.LAYER_LIGHTS; root.setRight(buildLightPanel()); } case "emitter" -> { input.activeLayer = SharedInput.LAYER_EMITTERS; root.setRight(buildEmitterPanel()); } @@ -5029,11 +4120,8 @@ public class EditorApp extends Application { placeRandom.setOnAction(ev -> { input.treeFolderPath = relFolder; input.pendingModelPath = null; - input.activeLayer = SharedInput.LAYER_OBJECTS; - if (objPlaceBtn != null) objPlaceBtn.setSelected(true); - openAssetOverlay(); - root.setRight(buildObjectPlacePanel()); - if (multiPlaceCB != null) multiPlaceCB.setSelected(true); + objectController.activatePlaceMode(); + objectController.enableMultiPlace(); setStatus("Ordner: " + relFolder + " | Linksklick → zufälliges Modell | Rechtsklick → neu würfeln"); }); ctx.getItems().addAll(new SeparatorMenuItem(), placeRandom); @@ -5098,14 +4186,11 @@ public class EditorApp extends Application { MenuItem placeModel = new MenuItem("📦 Platzieren"); placeModel.setOnAction(ev -> { input.pendingModelPath = relPath; - input.activeLayer = SharedInput.LAYER_OBJECTS; - if (objPlaceBtn != null) objPlaceBtn.setSelected(true); - openAssetOverlay(); - root.setRight(buildObjectPlacePanel()); + objectController.activatePlaceMode(); setStatus("Modell: " + relPath + " | Linksklick ins Terrain zum Platzieren"); }); MenuItem openEditor = new MenuItem("🔧 Im Model Editor öffnen"); - openEditor.setOnAction(ev -> openModelEditor(relPath, p)); + openEditor.setOnAction(ev -> modelEditorController.openModelEditor(relPath, p)); ctx.getItems().addAll(placeModel, openEditor, new SeparatorMenuItem()); boolean skeletal = skeletalModelPaths == null @@ -6029,6 +5114,19 @@ public class EditorApp extends Application { t.start(); } + private void archiveOriginal(File source, String assetType) { + java.nio.file.Path dest = de.blight.editor.ProjectRoot.PATH.resolve("assets").resolve("imported").resolve(assetType); + try { + java.nio.file.Files.createDirectories(dest); + java.nio.file.Files.copy(source.toPath(), dest.resolve(source.getName()), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + log.info("[Import] Archiviert: {}/{}", assetType, source.getName()); + } catch (IOException ex) { + log.warn("[Import] Archivierung fehlgeschlagen ({}/{}): {}", + assetType, source.getName(), ex.getMessage()); + } + } + private void handleTextureImport(javafx.stage.Window owner) { FileChooser fc = new FileChooser(); fc.setTitle("Texturen importieren (nicht-PNG wird automatisch konvertiert)"); @@ -6567,1474 +5665,6 @@ public class EditorApp extends Application { // ── Modell-Editor ──────────────────────────────────────────────────────── - private void openModelEditor(String relPath, java.nio.file.Path absolutePath) { - ensureNotContentEditor(); - modelEditorCurrentPath = relPath; - currentTool = "modeleditor"; - input.activeLayer = SharedInput.LAYER_MODEL_EDITOR; - setCenterView(worldViewport); - openAssetOverlay(); - - if (relPath == null) { - // Pfade + Flags sofort zurücksetzen bevor Toolbar gebaut wird - modelEditorLod1Path = ""; - modelEditorLod2Path = ""; - input.modelEditorHasEmbeddedLods = false; - topBar.getChildren().set(1, buildModelEditorToolBar()); - // Kein Modell vorausgewählt – Platzhalter zeigen - VBox placeholder = new VBox(12); - placeholder.setPadding(new javafx.geometry.Insets(12)); - placeholder.setPrefWidth(280); - placeholder.setStyle("-fx-background-color: #2a2a3e;"); - Label title = new Label("Modell-Editor"); - title.setStyle("-fx-font-weight:bold; -fx-font-size:14; -fx-text-fill:#ddd;"); - Label hint = new Label("Modell im Asset-Baum\nper Rechtsklick → Im Objekt-Editor öffnen\nauswählen."); - hint.setStyle("-fx-text-fill:#888; -fx-font-size:12;"); - hint.setWrapText(true); - placeholder.getChildren().addAll(title, hint); - root.setRight(placeholder); - setStatus("Modell-Editor: Modell aus Asset-Baum wählen"); - return; - } - - // Meta laden - de.blight.common.ModelMeta meta = absolutePath != null && absolutePath.toFile().exists() - ? de.blight.common.ModelMetaIO.load(absolutePath) - : de.blight.common.ModelMeta.defaults(relPath.replaceAll(".*/", "")); - - // LOD-Pfade + Embedded-LOD-Flag VOR Toolbar-Build setzen - modelEditorLod1Path = meta.lod1Path(); - modelEditorLod2Path = meta.lod2Path(); - input.modelEditorLod1Path = modelEditorLod1Path; - input.modelEditorLod2Path = modelEditorLod2Path; - input.modelEditorHasEmbeddedLods = false; // wird vom JME-Thread ggf. auf true gesetzt - input.modelEditorLodPreview = 0; - topBar.getChildren().set(1, buildModelEditorToolBar()); - - // JME-State anweisen - input.modelEditorScaleX = meta.scaleX(); - input.modelEditorScaleY = meta.scaleY(); - input.modelEditorScaleZ = meta.scaleZ(); - input.modelEditorPivotY = meta.pivotOffsetY(); - input.modelEditorOpenPath = relPath; - - VBox modelEditorInner = buildModelEditorPanel(relPath, absolutePath, meta); - ScrollPane modelEditorScroll = new ScrollPane(modelEditorInner); - modelEditorScroll.setFitToWidth(true); - modelEditorScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - modelEditorScroll.setStyle("-fx-background-color:transparent;-fx-background:transparent;"); - VBox modelEditorOuter = new VBox(modelEditorScroll); - VBox.setVgrow(modelEditorScroll, Priority.ALWAYS); - modelEditorOuter.setPrefWidth(280); - modelEditorOuter.setMaxWidth(280); - root.setRight(modelEditorOuter); - setStatus("Modell-Editor: " + relPath); - } - - private VBox buildModelEditorPanel(String relPath, - java.nio.file.Path absolutePath, - de.blight.common.ModelMeta meta) { - VBox panel = new VBox(6); - panel.setPadding(new javafx.geometry.Insets(8)); - panel.setPrefWidth(280); - panel.setStyle("-fx-background-color: #2a2a3e;"); - - // ── Titel ──────────────────────────────────────────────────────────── - Label title = new Label("Modell-Editor"); - title.setStyle("-fx-font-weight:bold; -fx-font-size:14; -fx-text-fill:#ddd;"); - - // ── Name ────────────────────────────────────────────────────────────── - Label nameLabel = new Label("Name:"); - nameLabel.setStyle("-fx-text-fill:#aaa;"); - TextField nameTF = new TextField(meta.name()); - nameTF.setStyle("-fx-background-color:#3a3a4e; -fx-text-fill:#eee;"); - - // ── Kategorie / Unterordner ──────────────────────────────────────────── - Label catLabel = new Label("Kategorie / Unterordner:"); - catLabel.setStyle("-fx-text-fill:#aaa;"); - ComboBox catCB = new ComboBox<>(); - catCB.setEditable(true); - catCB.getItems().addAll("", "vegetation/trees", "vegetation/bushes", - "vegetation/rocks", "buildings", "props", "characters", - "terrain", "effects", "misc"); - catCB.setValue(meta.category()); - catCB.setMaxWidth(Double.MAX_VALUE); - catCB.setStyle("-fx-background-color:#3a3a4e;"); - - // ── Tags ────────────────────────────────────────────────────────────── - Label tagsLabel = new Label("Tags (kommagetrennt):"); - tagsLabel.setStyle("-fx-text-fill:#aaa;"); - TextField tagsTF = new TextField(meta.tags()); - tagsTF.setStyle("-fx-background-color:#3a3a4e; -fx-text-fill:#eee;"); - tagsTF.setPromptText("z.B. tree, oak, large"); - - // ── Skalierung ─────────────────────────────────────────────────────── - Label scaleTitle = new Label("Skalierung:"); - scaleTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); - - modelEditorUniformCB = new CheckBox("Gleichmäßig skalieren (X=Y=Z)"); - modelEditorUniformCB.setSelected(meta.uniformScale()); - modelEditorUniformCB.setStyle("-fx-text-fill:#ccc;"); - - modelEditorSpinX = buildScaleSpinner(meta.scaleX()); - modelEditorSpinY = buildScaleSpinner(meta.scaleY()); - modelEditorSpinZ = buildScaleSpinner(meta.scaleZ()); - - GridPane scaleGrid = new GridPane(); - scaleGrid.setHgap(6); scaleGrid.setVgap(4); - addLabeledRow(scaleGrid, 0, "X:", modelEditorSpinX); - addLabeledRow(scaleGrid, 1, "Y:", modelEditorSpinY); - addLabeledRow(scaleGrid, 2, "Z:", modelEditorSpinZ); - - // Uniform-Listener - modelEditorSpinX.valueProperty().addListener((o, ov, nv) -> { - if (modelEditorSuppressListeners) return; - if (modelEditorUniformCB.isSelected()) { - modelEditorSuppressListeners = true; - modelEditorSpinY.getValueFactory().setValue(nv); - modelEditorSpinZ.getValueFactory().setValue(nv); - modelEditorSuppressListeners = false; - } - pushScaleToJme(); - }); - modelEditorSpinY.valueProperty().addListener((o, ov, nv) -> { - if (modelEditorSuppressListeners) return; - if (modelEditorUniformCB.isSelected()) { - modelEditorSuppressListeners = true; - modelEditorSpinX.getValueFactory().setValue(nv); - modelEditorSpinZ.getValueFactory().setValue(nv); - modelEditorSuppressListeners = false; - } - pushScaleToJme(); - }); - modelEditorSpinZ.valueProperty().addListener((o, ov, nv) -> { - if (modelEditorSuppressListeners) return; - if (modelEditorUniformCB.isSelected()) { - modelEditorSuppressListeners = true; - modelEditorSpinX.getValueFactory().setValue(nv); - modelEditorSpinY.getValueFactory().setValue(nv); - modelEditorSuppressListeners = false; - } - pushScaleToJme(); - }); - modelEditorUniformCB.setOnAction(e -> { - if (modelEditorUniformCB.isSelected()) { - // Y und Z auf X-Wert angleichen - double xVal = modelEditorSpinX.getValue(); - modelEditorSuppressListeners = true; - modelEditorSpinY.getValueFactory().setValue(xVal); - modelEditorSpinZ.getValueFactory().setValue(xVal); - modelEditorSuppressListeners = false; - pushScaleToJme(); - } - }); - - // ── Maße (read-only, von JME aktualisiert) ─────────────────────────── - modelEditorDimLabel = new Label("B – × H – × T – m"); - modelEditorDimLabel.setStyle("-fx-text-fill:#8cf; -fx-font-family:monospace;"); - modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE); - modelEditorDimLabel.setWrapText(true); - - modelEditorPolyLabel = new Label("– Polygone"); - modelEditorPolyLabel.setStyle("-fx-text-fill:#aaa; -fx-font-family:monospace; -fx-font-size:11;"); - modelEditorPolyLabel.setMaxWidth(Double.MAX_VALUE); - - // ── Pivot & Platzierungs-Versatz ───────────────────────────────────── - Label offsetTitle = new Label("Versatz:"); - offsetTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); - - Spinner pivotSpin = buildOffsetSpinner(meta.pivotOffsetY()); - pivotSpin.valueProperty().addListener((o, ov, nv) -> { - input.modelEditorPivotY = nv.floatValue(); - input.modelEditorPivotChanged = true; - }); - - Spinner placeSpin = buildOffsetSpinner(meta.placementOffsetY()); - - GridPane offsetGrid = new GridPane(); - offsetGrid.setHgap(6); offsetGrid.setVgap(4); - addLabeledRow(offsetGrid, 0, "Pivot Y:", pivotSpin); - addLabeledRow(offsetGrid, 1, "Platzierung Y:", placeSpin); - - // ── Standardwerte beim Platzieren ──────────────────────────────────── - Label defaultsTitle = new Label("Standard beim Platzieren:"); - defaultsTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); - - CheckBox solidCB = new CheckBox("Kollision (solid)"); - CheckBox castCB = new CheckBox("Schatten werfen"); - CheckBox receiveCB = new CheckBox("Schatten empfangen"); - solidCB.setSelected(meta.solid()); - castCB.setSelected(meta.castShadow()); - receiveCB.setSelected(meta.receiveShadow()); - for (CheckBox cb : new CheckBox[]{solidCB, castCB, receiveCB}) - cb.setStyle("-fx-text-fill:#ccc;"); - if (relPath.startsWith("Models/trees/")) { - solidCB.setSelected(false); - solidCB.setDisable(true); - } - - // ── Zufällige Skalierung beim Multi-Place ───────────────────────────── - Label rndTitle = new Label("Zufallsskalierung (Multi-Place):"); - rndTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); - - Spinner rndMinSpin = buildScaleSpinner(meta.randomScaleMin()); - Spinner rndMaxSpin = buildScaleSpinner(meta.randomScaleMax()); - - GridPane rndGrid = new GridPane(); - rndGrid.setHgap(6); rndGrid.setVgap(4); - addLabeledRow(rndGrid, 0, "Min:", rndMinSpin); - addLabeledRow(rndGrid, 1, "Max:", rndMaxSpin); - - // ── LOD ─────────────────────────────────────────────────────────────── - Label lodTitle = new Label("Level of Detail"); - lodTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); - - // ── LOD 1 ───────────────────────────────────────────────────────────── - modelEditorLod1GenBtn = new Button("LOD 1 generieren"); - modelEditorLod1GenBtn.setMaxWidth(Double.MAX_VALUE); - boolean lod1Exists = input.modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || input.modelImportHasLod1; - modelEditorLod1GenBtn.setDisable(lod1Exists); - modelEditorLod1GenBtn.setOnAction(e -> showLodGenDialog(1, 50)); - - modelEditorLod1StatusLabel = new Label("–"); - modelEditorLod1StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); - modelEditorLod1StatusLabel.setWrapText(true); - - modelEditorLod1Label = new Label(modelEditorLod1Path.isBlank() ? "(kein Pfad)" : modelEditorLod1Path); - modelEditorLod1Label.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); - modelEditorLod1Label.setWrapText(true); - - Button lod1ClearBtn = new Button("✕"); - lod1ClearBtn.setStyle("-fx-text-fill:#c0392b;"); - lod1ClearBtn.setOnAction(e -> { - if (input.modelEditorHasEmbeddedLod1) { - // ModelEditorState.removeEmbeddedLod(1) kaskadiert automatisch LOD2 - input.modelEditorRemoveEmbeddedLod = 1; - } - if (input.modelImportHasLod1) { - // ModelImportState.clearLod==1 kaskadiert automatisch LOD2 - input.modelImportClearLodLevel = 1; - if (modelEditorLod1StatusLabel != null) modelEditorLod1StatusLabel.setText("–"); - if (modelEditorLod2StatusLabel != null) modelEditorLod2StatusLabel.setText("–"); - } - modelEditorLod1Path = ""; - input.modelEditorLod1Path = ""; - modelEditorLod1Label.setText("(kein Pfad)"); - // Kaskade LOD2 (Pfad + Preview) - modelEditorLod2Path = ""; - input.modelEditorLod2Path = ""; - if (modelEditorLod2Label != null) modelEditorLod2Label.setText("(kein Pfad)"); - if (input.modelEditorLodPreview == 1 || input.modelEditorLodPreview == 2) { - input.modelEditorLodPreview = 0; - input.modelEditorLodChanged = true; - } - }); - - HBox lod1Row = new HBox(4, modelEditorLod1GenBtn, lod1ClearBtn); - HBox.setHgrow(modelEditorLod1GenBtn, javafx.scene.layout.Priority.ALWAYS); - - // ── LOD 2 ───────────────────────────────────────────────────────────── - modelEditorLod2GenBtn = new Button("LOD 2 generieren"); - modelEditorLod2GenBtn.setMaxWidth(Double.MAX_VALUE); - boolean lod2Exists = input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2; - modelEditorLod2GenBtn.setDisable(!lod1Exists || lod2Exists); - modelEditorLod2GenBtn.setOnAction(e -> showLodGenDialog(2, 85)); - - modelEditorLod2StatusLabel = new Label("–"); - modelEditorLod2StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); - modelEditorLod2StatusLabel.setWrapText(true); - - modelEditorLod2Label = new Label(modelEditorLod2Path.isBlank() ? "(kein Pfad)" : modelEditorLod2Path); - modelEditorLod2Label.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); - modelEditorLod2Label.setWrapText(true); - - Button lod2ClearBtn = new Button("✕"); - lod2ClearBtn.setStyle("-fx-text-fill:#c0392b;"); - lod2ClearBtn.setOnAction(e -> { - if (input.modelEditorHasEmbeddedLod2) { - input.modelEditorRemoveEmbeddedLod = 2; - } - if (input.modelImportHasLod2) { - input.modelImportClearLodLevel = 2; - if (modelEditorLod2StatusLabel != null) modelEditorLod2StatusLabel.setText("–"); - } - modelEditorLod2Path = ""; - input.modelEditorLod2Path = ""; - modelEditorLod2Label.setText("(kein Pfad)"); - if (input.modelEditorLodPreview == 2) { - input.modelEditorLodPreview = 0; - input.modelEditorLodChanged = true; - } - }); - - HBox lod2Row = new HBox(4, modelEditorLod2GenBtn, lod2ClearBtn); - HBox.setHgrow(modelEditorLod2GenBtn, javafx.scene.layout.Priority.ALWAYS); - - // ── Anhänge: Lichter & Partikel ─────────────────────────────────────── - Label attachTitle = new Label("Anhänge: Lichter & Partikel"); - attachTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); - - // Lichter - Label lightSectionLbl = new Label("Lichtquellen:"); - lightSectionLbl.setStyle("-fx-text-fill:#aaa;"); - - modelEditorLights.clear(); - modelEditorLights.addAll(meta.attachedLights()); - - modelEditorLightBox = new javafx.scene.layout.VBox(4); - rebuildLightBox(); - - Button addLightBtn = new Button("+ Lichtquelle"); - addLightBtn.setMaxWidth(Double.MAX_VALUE); - addLightBtn.setStyle("-fx-background-color:#4a4a2e;"); - addLightBtn.setOnAction(ev -> { - modelEditorLights.add(new de.blight.common.ModelMeta.AttachedLight( - 0f, 1f, 0f, 1f, 0.85f, 0.5f, 2f, 8f)); - rebuildLightBox(); - pushAttachmentsToJme(); - }); - - // Emitter - Label emitterSectionLbl = new Label("Partikel-Emitter:"); - emitterSectionLbl.setStyle("-fx-text-fill:#aaa;"); - - modelEditorEmitters.clear(); - modelEditorEmitters.addAll(meta.attachedEmitters()); - - modelEditorEmitterBox = new javafx.scene.layout.VBox(4); - rebuildEmitterBox(); - - Button addEmitterBtn = new Button("+ Partikel-Emitter"); - addEmitterBtn.setMaxWidth(Double.MAX_VALUE); - addEmitterBtn.setStyle("-fx-background-color:#4a2e2e;"); - addEmitterBtn.setOnAction(ev -> { - modelEditorEmitters.add(new de.blight.common.ModelMeta.AttachedEmitter( - 0f, 0.5f, 0f, 0)); - rebuildEmitterBox(); - pushAttachmentsToJme(); - }); - - // Initiale Gizmos pushen - pushAttachmentsToJme(); - - // ── Modell-Kategorie ────────────────────────────────────────────────── - Label catTitle = new Label("Kategorie:"); - catTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); - modelEditorCategoryCB = new ComboBox<>(); - modelEditorCategoryCB.getItems().addAll(de.blight.common.ModelCategory.values()); - modelEditorCategoryCB.setValue(meta.modelCategory() != null ? meta.modelCategory() : de.blight.common.ModelCategory.NONE); - modelEditorCategoryCB.setMaxWidth(Double.MAX_VALUE); - modelEditorCategoryCB.setConverter(new javafx.util.StringConverter<>() { - @Override public String toString(de.blight.common.ModelCategory c) { return c == null ? "" : c.getLabel(); } - @Override public de.blight.common.ModelCategory fromString(String s) { return de.blight.common.ModelCategory.fromString(s); } - }); - - // ── Interaktivität ──────────────────────────────────────────────────── - Label interactTitle = new Label("Interaktivität:"); - interactTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); - - modelEditorInteractableCB = new ComboBox<>(); - modelEditorInteractableCB.getItems().addAll(de.blight.common.model.InteractableType.values()); - modelEditorInteractableCB.setValue(meta.interactableType()); - modelEditorInteractableCB.setMaxWidth(Double.MAX_VALUE); - modelEditorInteractableCB.setConverter(new javafx.util.StringConverter<>() { - @Override public String toString(de.blight.common.model.InteractableType t) { - return t == null ? "" : t.getLabel(); - } - @Override public de.blight.common.model.InteractableType fromString(String s) { - return de.blight.common.model.InteractableType.fromString(s); - } - }); - - // Ruhepunkt-Controls (nur sichtbar wenn BED oder BENCH gewählt) - // SharedInput mit bestehenden Meta-Werten initialisieren - input.modelInteractableOffsetX = meta.interactableOffsetX(); - input.modelInteractableOffsetY = meta.interactableOffsetY(); - input.modelInteractableOffsetZ = meta.interactableOffsetZ(); - input.modelInteractableRotY = meta.interactableRotY(); - input.modelInteractableActive = meta.interactableType() == de.blight.common.model.InteractableType.BED - || meta.interactableType() == de.blight.common.model.InteractableType.BENCH; - input.modelInteractableOffsetChanged = true; - - Label restPosHint = new Label("Klicke auf das Modell um den Ruhepunkt zu setzen:"); - restPosHint.setStyle("-fx-text-fill:#aaa; -fx-font-size:10;"); - restPosHint.setWrapText(true); - - Button setRestBtn = new Button("⊕ Im Modell klicken"); - setRestBtn.setMaxWidth(Double.MAX_VALUE); - setRestBtn.setStyle("-fx-background-color:#1a5276; -fx-text-fill:#fff;"); - setRestBtn.setOnAction(ev -> { - input.activeLayer = SharedInput.LAYER_MODEL_INTERACTABLE; - setStatus("Klicke auf das Modell um den Ruhepunkt zu setzen"); - }); - - // Position-Spinner (X / Y / Z) - Label posLabel = new Label("Position (Modell-Koordinaten):"); - posLabel.setStyle("-fx-text-fill:#aaa; -fx-font-size:10;"); - - modelEditorInteractableXSpin = new Spinner<>( - new javafx.scene.control.SpinnerValueFactory.DoubleSpinnerValueFactory(-5.0, 5.0, - Math.round(meta.interactableOffsetX() * 100.0) / 100.0, 0.05)); - modelEditorInteractableXSpin.setEditable(true); - modelEditorInteractableXSpin.setMaxWidth(Double.MAX_VALUE); - modelEditorInteractableXSpin.valueProperty().addListener((obs, ov, nv) -> { - if (updatingInteractableSpinnersFromJme) return; - input.modelInteractableOffsetX = nv.floatValue(); - input.modelInteractableOffsetChanged = true; - }); - - modelEditorInteractableYSpin = new Spinner<>( - new javafx.scene.control.SpinnerValueFactory.DoubleSpinnerValueFactory(-1.0, 4.0, - Math.round(meta.interactableOffsetY() * 100.0) / 100.0, 0.05)); - modelEditorInteractableYSpin.setEditable(true); - modelEditorInteractableYSpin.setMaxWidth(Double.MAX_VALUE); - modelEditorInteractableYSpin.valueProperty().addListener((obs, ov, nv) -> { - if (updatingInteractableSpinnersFromJme) return; - input.modelInteractableOffsetY = nv.floatValue(); - input.modelInteractableOffsetChanged = true; - }); - - modelEditorInteractableZSpin = new Spinner<>( - new javafx.scene.control.SpinnerValueFactory.DoubleSpinnerValueFactory(-5.0, 5.0, - Math.round(meta.interactableOffsetZ() * 100.0) / 100.0, 0.05)); - modelEditorInteractableZSpin.setEditable(true); - modelEditorInteractableZSpin.setMaxWidth(Double.MAX_VALUE); - modelEditorInteractableZSpin.valueProperty().addListener((obs, ov, nv) -> { - if (updatingInteractableSpinnersFromJme) return; - input.modelInteractableOffsetZ = nv.floatValue(); - input.modelInteractableOffsetChanged = true; - }); - - javafx.scene.layout.GridPane posGrid = new javafx.scene.layout.GridPane(); - posGrid.setHgap(4); posGrid.setVgap(2); - javafx.scene.layout.ColumnConstraints posCC0 = new javafx.scene.layout.ColumnConstraints(); - posCC0.setMinWidth(20); posCC0.setHgrow(javafx.scene.layout.Priority.NEVER); - javafx.scene.layout.ColumnConstraints posCC1 = new javafx.scene.layout.ColumnConstraints(); - posCC1.setHgrow(javafx.scene.layout.Priority.ALWAYS); posCC1.setFillWidth(true); - posGrid.getColumnConstraints().addAll(posCC0, posCC1); - posGrid.add(new Label("X:"), 0, 0); posGrid.add(modelEditorInteractableXSpin, 1, 0); - posGrid.add(new Label("Y:"), 0, 1); posGrid.add(modelEditorInteractableYSpin, 1, 1); - posGrid.add(new Label("Z:"), 0, 2); posGrid.add(modelEditorInteractableZSpin, 1, 2); - posGrid.getChildren().stream() - .filter(n -> n instanceof Label) - .forEach(n -> ((Label) n).setStyle("-fx-text-fill:#aaa;")); - - Label rotLabel = new Label("Blickrichtung (°):"); - rotLabel.setStyle("-fx-text-fill:#aaa;"); - - double initDeg = Math.toDegrees(meta.interactableRotY()); - Slider rotSlider = new Slider(0, 360, initDeg); - rotSlider.setShowTickMarks(true); - rotSlider.setMajorTickUnit(90); - rotSlider.setPrefWidth(150); - - TextField rotField = new TextField(String.format("%.1f", initDeg)); - rotField.setPrefWidth(60); - rotField.setStyle("-fx-background-color:#333; -fx-text-fill:#eee;"); - - boolean[] syncingRot = {false}; - - rotSlider.valueProperty().addListener((obs, ov, nv) -> { - if (syncingRot[0]) return; - syncingRot[0] = true; - rotField.setText(String.format("%.1f", nv.doubleValue())); - syncingRot[0] = false; - input.modelInteractableRotY = (float) Math.toRadians(nv.doubleValue()); - input.modelInteractableOffsetChanged = true; - }); - - Runnable applyRotField = () -> { - try { - double deg = Double.parseDouble(rotField.getText().replace(',', '.')); - deg = ((deg % 360) + 360) % 360; - if (syncingRot[0]) return; - syncingRot[0] = true; - rotSlider.setValue(deg); - syncingRot[0] = false; - input.modelInteractableRotY = (float) Math.toRadians(deg); - input.modelInteractableOffsetChanged = true; - } catch (NumberFormatException ignored) {} - }; - rotField.setOnAction(e -> applyRotField.run()); - rotField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) applyRotField.run(); }); - - HBox rotRow = new HBox(6, rotSlider, rotField); - rotRow.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - - javafx.scene.layout.VBox restPointBox = new javafx.scene.layout.VBox(4, - restPosHint, setRestBtn, posLabel, posGrid, - rotLabel, rotRow); - restPointBox.setStyle("-fx-padding: 4 0 0 0;"); - - boolean isBedOrBench = meta.interactableType() == de.blight.common.model.InteractableType.BED - || meta.interactableType() == de.blight.common.model.InteractableType.BENCH; - restPointBox.setVisible(isBedOrBench); - restPointBox.setManaged(isBedOrBench); - - modelEditorInteractableCB.valueProperty().addListener((obs, ov, nv) -> { - boolean show = nv == de.blight.common.model.InteractableType.BED - || nv == de.blight.common.model.InteractableType.BENCH; - restPointBox.setVisible(show); - restPointBox.setManaged(show); - // Pfeil ein-/ausblenden — immer rebuilden, damit Sichtbarkeit aktualisiert wird - input.modelInteractableActive = show; - input.modelInteractableOffsetChanged = true; - }); - - // ── Fußgeräusch-Untergrund ──────────────────────────────────────────── - Label footstepTitle = new Label("Fußgeräusch-Untergrund:"); - footstepTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); - - modelEditorFootstepSurfaceCB = new ComboBox<>(); - modelEditorFootstepSurfaceCB.getItems().add(""); - for (de.blight.game.audio.SurfaceType st : de.blight.game.audio.SurfaceType.values()) { - modelEditorFootstepSurfaceCB.getItems().add(st.name()); - } - String currentSurface = meta.footstepSurface() != null ? meta.footstepSurface() : ""; - modelEditorFootstepSurfaceCB.setValue( - modelEditorFootstepSurfaceCB.getItems().contains(currentSurface) ? currentSurface : ""); - modelEditorFootstepSurfaceCB.setMaxWidth(Double.MAX_VALUE); - modelEditorFootstepSurfaceCB.setConverter(new javafx.util.StringConverter<>() { - @Override public String toString(String s) { - return (s == null || s.isEmpty()) ? "(aus Textur)" : s; - } - @Override public String fromString(String s) { return s; } - }); - - // ── Buttons ─────────────────────────────────────────────────────────── - Button saveBtn = new Button("💾 Speichern"); - saveBtn.setMaxWidth(Double.MAX_VALUE); - saveBtn.setStyle("-fx-background-color:#2e7d32; -fx-text-fill:#fff; -fx-font-weight:bold;"); - - Button placeBtn = new Button("⊕ Platzieren"); - placeBtn.setMaxWidth(Double.MAX_VALUE); - placeBtn.setStyle("-fx-background-color:#1565c0; -fx-text-fill:#fff;"); - - Button closeBtn = new Button("✕ Schließen"); - closeBtn.setMaxWidth(Double.MAX_VALUE); - closeBtn.setStyle("-fx-background-color:#555; -fx-text-fill:#ccc;"); - - saveBtn.setOnAction(e -> saveModelMeta( - relPath, absolutePath, - nameTF.getText().trim(), - catCB.getValue() != null ? catCB.getValue().trim() : "", - tagsTF.getText().trim(), - (float)(double) modelEditorSpinX.getValue(), - (float)(double) modelEditorSpinY.getValue(), - (float)(double) modelEditorSpinZ.getValue(), - modelEditorUniformCB.isSelected(), - (float)(double) pivotSpin.getValue(), - (float)(double) placeSpin.getValue(), - solidCB.isSelected(), - castCB.isSelected(), - receiveCB.isSelected(), - (float)(double) rndMinSpin.getValue(), - (float)(double) rndMaxSpin.getValue(), - modelEditorLod1Path, modelEditorLod2Path, - new java.util.ArrayList<>(modelEditorLights), - new java.util.ArrayList<>(modelEditorEmitters), - modelEditorInteractableCB != null - ? modelEditorInteractableCB.getValue() - : de.blight.common.model.InteractableType.NONE, - input.modelInteractableOffsetX, - input.modelInteractableOffsetY, - input.modelInteractableOffsetZ, - input.modelInteractableRotY, - modelEditorFootstepSurfaceCB != null - ? modelEditorFootstepSurfaceCB.getValue() - : "", - modelEditorCategoryCB != null - ? modelEditorCategoryCB.getValue() - : de.blight.common.ModelCategory.NONE)); - - placeBtn.setOnAction(e -> { - input.modelEditorCloseRequest = true; - input.pendingModelPath = modelEditorCurrentPath; - input.activeLayer = SharedInput.LAYER_OBJECTS; - if (objPlaceBtn != null) objPlaceBtn.setSelected(true); - root.setRight(buildObjectPlacePanel()); - }); - - closeBtn.setOnAction(e -> { - input.modelEditorCloseRequest = true; - input.activeLayer = 0; - if (baseBtn != null) baseBtn.setSelected(true); - root.setRight(toolPanel); - }); - - panel.getChildren().addAll( - title, - new Separator(), - nameLabel, nameTF, - - tagsLabel, tagsTF, - new Separator(), - scaleTitle, modelEditorUniformCB, scaleGrid, - modelEditorDimLabel, modelEditorPolyLabel, - new Separator(), - offsetTitle, offsetGrid, - new Separator(), - defaultsTitle, solidCB, castCB, receiveCB, - new Separator(), - rndTitle, rndGrid, - new Separator(), - lodTitle, - lod1Row, modelEditorLod1StatusLabel, modelEditorLod1Label, - lod2Row, modelEditorLod2StatusLabel, modelEditorLod2Label, - new Separator(), - attachTitle, - lightSectionLbl, modelEditorLightBox, addLightBtn, - emitterSectionLbl, modelEditorEmitterBox, addEmitterBtn, - new Separator(), - catTitle, modelEditorCategoryCB, - new Separator(), - interactTitle, modelEditorInteractableCB, restPointBox, - new Separator(), - footstepTitle, modelEditorFootstepSurfaceCB, - new Separator(), - saveBtn, placeBtn, closeBtn - ); - return panel; - } - - // ── Modell-Import ───────────────────────────────────────────────────────── - - private void openModelImport(Stage stage) { - FileChooser fc = new FileChooser(); - fc.setTitle("Modell importieren"); - fc.getExtensionFilters().add( - new FileChooser.ExtensionFilter("3D-Modelle", "*.j3o", "*.gltf", "*.glb", "*.obj")); - List files = fc.showOpenMultipleDialog(stage); - if (files == null || files.isEmpty()) return; - - modelImportQueue.clear(); - if (files.size() == 1) { - startImportFile(files.get(0)); - } else { - // Mehrfach-Import: erste Datei sofort, Rest in Warteschlange - modelImportQueue.addAll(files.subList(1, files.size())); - setStatus("Mehrfach-Import: " + files.size() + " Dateien – starte mit " + files.get(0).getName()); - startImportFile(files.get(0)); - } - } - - private void archiveOriginal(File source, String assetType) { - Path dest = ProjectRoot.PATH.resolve("assets").resolve("imported").resolve(assetType); - try { - Files.createDirectories(dest); - Files.copy(source.toPath(), dest.resolve(source.getName()), - java.nio.file.StandardCopyOption.REPLACE_EXISTING); - log.info("[Import] Archiviert: {}/{}", assetType, source.getName()); - } catch (IOException ex) { - log.warn("[Import] Archivierung fehlgeschlagen ({}/{}): {}", - assetType, source.getName(), ex.getMessage()); - } - } - - private void startImportFile(File file) { - archiveOriginal(file, "models"); - String name = file.getName(); - String baseName = name.replaceFirst("\\.[^.]+$", ""); - Path destDir = ASSET_ROOT.resolve("Models").resolve("imported"); - try { Files.createDirectories(destDir); } - catch (IOException ex) { setStatus("Fehler: " + ex.getMessage()); return; } - - boolean isJ3o = name.toLowerCase().endsWith(".j3o"); - Path destFile = destDir.resolve(name); - Path destJ3o = destDir.resolve(baseName + ".j3o"); - - try { - Files.copy(file.toPath(), destFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - } catch (IOException ex) { - setStatus("Fehler beim Kopieren: " + ex.getMessage()); - return; - } - - if (isJ3o) { - String relPath = ASSET_ROOT.relativize(destJ3o).toString().replace('\\', '/'); - activateModelImport(relPath, baseName); - } else { - String assetPath = ASSET_ROOT.relativize(destFile).toString().replace('\\', '/'); - boolean keepCtrls = name.matches(".*\\.(gltf|glb)"); - input.modelConvertQueue.offer( - new SharedInput.ModelConvertRequest(assetPath, destJ3o, destFile, keepCtrls, - 0f, false, true)); - setStatus("Konvertiere " + name + " → .j3o …"); - String finalRel = ASSET_ROOT.relativize(destJ3o).toString().replace('\\', '/'); - new Thread(() -> { - for (int i = 0; i < 300; i++) { - try { Thread.sleep(100); } catch (InterruptedException ignored) {} - if (Files.exists(destJ3o)) break; - } - Platform.runLater(() -> activateModelImport(finalRel, baseName)); - }, "import-wait").start(); - } - } - - private void activateModelImport(String relPath, String suggestedName) { - modelImportCurrentPath = relPath; - modelImportLod1StatusLabel = null; - modelImportLod2StatusLabel = null; - modelImportLod1ClearBtn = null; - modelImportLod2ClearBtn = null; - modelImportLod2GenBtn = null; - modelImportExportStatusLabel = null; - input.modelImportHasLod1 = false; - input.modelImportHasLod2 = false; - input.modelImportExportName = null; - input.modelImportExportStatus = null; - input.modelImportDummyVisible = false; - input.modelImportDummyX = 0f; - input.modelImportDummyY = 0f; - input.modelImportDummyZ = 0f; - input.modelImportLod1PreviewNode.set(null); - input.modelImportLod2PreviewNode.set(null); - if (importLod1Btn != null) { importLod1Btn.setDisable(true); importLod1Btn.setSelected(false); } - if (importLod2Btn != null) { importLod2Btn.setDisable(true); importLod2Btn.setSelected(false); } - - currentTool = "modelimport"; - topBar.getChildren().set(1, buildModelImportToolBar()); - input.activeLayer = SharedInput.LAYER_MODEL_EDITOR; - setCenterView(worldViewport); - openAssetOverlay(); - - // Reset scale - input.modelEditorScaleX = 1f; - input.modelEditorScaleY = 1f; - input.modelEditorScaleZ = 1f; - input.modelEditorPivotY = 0f; - input.modelEditorOpenPath = relPath; - input.modelImportIntermediatePath = relPath; - - root.setRight(buildModelImportPanel(relPath, suggestedName)); - setStatus("Modell-Import: " + relPath); - } - - private ToolBar buildModelImportToolBar() { - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> { - input.modelEditorCloseRequest = true; - input.activeLayer = 0; - if (baseBtn != null) baseBtn.setSelected(true); - root.setRight(toolPanel); - switchToWorldEditor(); - }); - Label label = new Label("Modell importieren"); - label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - - importLod0Btn = new ToggleButton("LOD 0"); - ToggleButton lod0Btn = importLod0Btn; - importLod1Btn = new ToggleButton("LOD 1"); - importLod2Btn = new ToggleButton("LOD 2"); - javafx.scene.control.ToggleGroup lodGroup = new javafx.scene.control.ToggleGroup(); - lod0Btn.setToggleGroup(lodGroup); importLod1Btn.setToggleGroup(lodGroup); importLod2Btn.setToggleGroup(lodGroup); - lod0Btn.setSelected(true); - String lodStyle = "-fx-font-size:11; -fx-padding:2 8 2 8;"; - lod0Btn.setStyle(lodStyle); importLod1Btn.setStyle(lodStyle); importLod2Btn.setStyle(lodStyle); - lod0Btn.setOnAction(e -> { input.modelEditorLodPreview = 0; input.modelEditorLodChanged = true; }); - importLod1Btn.setOnAction(e -> { input.modelEditorLodPreview = 1; input.modelEditorLodChanged = true; }); - importLod2Btn.setOnAction(e -> { input.modelEditorLodPreview = 2; input.modelEditorLodChanged = true; }); - // Initial deaktiviert – werden freigegeben sobald LOD generiert wurde - importLod1Btn.setDisable(true); - importLod2Btn.setDisable(true); - - ToggleButton cylBtn = new ToggleButton("⇕ 2m"); - cylBtn.setStyle("-fx-font-size:11; -fx-padding:2 8 2 8;"); - cylBtn.setTooltip(new Tooltip("Vergleichsfigur (2m Zylinder) ein-/ausblenden")); - cylBtn.setSelected(input.modelImportDummyVisible); - cylBtn.setOnAction(e -> input.modelImportDummyVisible = cylBtn.isSelected()); - - String spnStyle = "-fx-pref-width:64; -fx-font-size:11;"; - javafx.scene.control.Spinner spnX = new javafx.scene.control.Spinner<>(-50.0, 50.0, 0.0, 0.25); - javafx.scene.control.Spinner spnY = new javafx.scene.control.Spinner<>(-20.0, 20.0, 0.0, 0.25); - javafx.scene.control.Spinner spnZ = new javafx.scene.control.Spinner<>(-50.0, 50.0, 0.0, 0.25); - spnX.setStyle(spnStyle); spnY.setStyle(spnStyle); spnZ.setStyle(spnStyle); - spnX.setEditable(true); spnY.setEditable(true); spnZ.setEditable(true); - spnX.setTooltip(new Tooltip("Vergleichsfigur X-Offset")); - spnY.setTooltip(new Tooltip("Vergleichsfigur Y-Offset")); - spnZ.setTooltip(new Tooltip("Vergleichsfigur Z-Offset")); - spnX.valueProperty().addListener((ob, o, v) -> input.modelImportDummyX = v.floatValue()); - spnY.valueProperty().addListener((ob, o, v) -> input.modelImportDummyY = v.floatValue()); - spnZ.valueProperty().addListener((ob, o, v) -> input.modelImportDummyZ = v.floatValue()); - - Label lblX = new Label("X"); lblX.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;"); - Label lblY = new Label("Y"); lblY.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;"); - Label lblZ = new Label("Z"); lblZ.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;"); - - ToolBar tb = new ToolBar(); - tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label, - new Separator(Orientation.VERTICAL), lod0Btn, importLod1Btn, importLod2Btn, - new Separator(Orientation.VERTICAL), cylBtn, - lblX, spnX, lblY, spnY, lblZ, spnZ); - return tb; - } - - private VBox buildModelImportPanel(String relPath, String suggestedName) { - VBox panel = new VBox(8); - panel.setPadding(new Insets(10)); - panel.setPrefWidth(300); - panel.setStyle("-fx-background-color: #2a2a3e;"); - - // ── Titel ───────────────────────────────────────────────────────────── - Label title = new Label("Modell importieren"); - title.setStyle("-fx-font-weight:bold; -fx-font-size:14; -fx-text-fill:#ddd;"); - - // ── Skalierung ──────────────────────────────────────────────────────── - Label scaleTitle = new Label("Skalierung:"); - scaleTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); - - modelEditorUniformCB = new CheckBox("Gleichmäßig skalieren (X=Y=Z)"); - modelEditorUniformCB.setSelected(true); - modelEditorUniformCB.setStyle("-fx-text-fill:#ccc;"); - - modelEditorSpinX = buildScaleSpinner(1.0); - modelEditorSpinY = buildScaleSpinner(1.0); - modelEditorSpinZ = buildScaleSpinner(1.0); - - GridPane scaleGrid = new GridPane(); - scaleGrid.setHgap(6); scaleGrid.setVgap(4); - addLabeledRow(scaleGrid, 0, "X:", modelEditorSpinX); - addLabeledRow(scaleGrid, 1, "Y:", modelEditorSpinY); - addLabeledRow(scaleGrid, 2, "Z:", modelEditorSpinZ); - - modelEditorSpinX.valueProperty().addListener((o, ov, nv) -> { - if (modelEditorSuppressListeners) return; - if (modelEditorUniformCB.isSelected()) { - modelEditorSuppressListeners = true; - modelEditorSpinY.getValueFactory().setValue(nv); - modelEditorSpinZ.getValueFactory().setValue(nv); - modelEditorSuppressListeners = false; - } - pushScaleToJme(); - }); - modelEditorSpinY.valueProperty().addListener((o, ov, nv) -> { - if (modelEditorSuppressListeners) return; - if (modelEditorUniformCB.isSelected()) { - modelEditorSuppressListeners = true; - modelEditorSpinX.getValueFactory().setValue(nv); - modelEditorSpinZ.getValueFactory().setValue(nv); - modelEditorSuppressListeners = false; - } - pushScaleToJme(); - }); - modelEditorSpinZ.valueProperty().addListener((o, ov, nv) -> { - if (modelEditorSuppressListeners) return; - if (modelEditorUniformCB.isSelected()) { - modelEditorSuppressListeners = true; - modelEditorSpinX.getValueFactory().setValue(nv); - modelEditorSpinY.getValueFactory().setValue(nv); - modelEditorSuppressListeners = false; - } - pushScaleToJme(); - }); - modelEditorUniformCB.setOnAction(e -> { - if (modelEditorUniformCB.isSelected()) { - double xVal = modelEditorSpinX.getValue(); - modelEditorSuppressListeners = true; - modelEditorSpinY.getValueFactory().setValue(xVal); - modelEditorSpinZ.getValueFactory().setValue(xVal); - modelEditorSuppressListeners = false; - pushScaleToJme(); - } - }); - - modelEditorDimLabel = new Label("B – × H – × T – m"); - modelEditorDimLabel.setStyle("-fx-text-fill:#8cf; -fx-font-family:monospace;"); - modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE); - modelEditorDimLabel.setWrapText(true); - - modelEditorPolyLabel = new Label("– Polygone"); - modelEditorPolyLabel.setStyle("-fx-text-fill:#aaa; -fx-font-family:monospace; -fx-font-size:11;"); - modelEditorPolyLabel.setMaxWidth(Double.MAX_VALUE); - - // ── LOD-Algorithmus ─────────────────────────────────────────────────── - Label algoTitle = new Label("Reduktions-Algorithmus"); - algoTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); - RadioButton algoBlightRB = new RadioButton("Blight (Edge Collapse, Dihedral-aware)"); - RadioButton algoJmeRB = new RadioButton("JME (Progressive Mesh)"); - algoBlightRB.setStyle("-fx-text-fill:#ccc;"); - algoJmeRB.setStyle("-fx-text-fill:#ccc;"); - javafx.scene.control.ToggleGroup algoGroup = new javafx.scene.control.ToggleGroup(); - algoBlightRB.setToggleGroup(algoGroup); - algoJmeRB.setToggleGroup(algoGroup); - algoBlightRB.setSelected(true); - algoBlightRB.setOnAction(e -> input.modelLodAlgorithm = "blight"); - algoJmeRB.setOnAction(e -> input.modelLodAlgorithm = "jme"); - - // ── LOD 1 ───────────────────────────────────────────────────────────── - Label lod1Title = new Label("LOD 1"); - lod1Title.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); - - RadioButton lod1AutoRB = new RadioButton("Auto (Mesh-Reduktion)"); - RadioButton lod1FileRB = new RadioButton("Aus Datei…"); - lod1AutoRB.setStyle("-fx-text-fill:#ccc;"); - lod1FileRB.setStyle("-fx-text-fill:#ccc;"); - javafx.scene.control.ToggleGroup lod1Group = new javafx.scene.control.ToggleGroup(); - lod1AutoRB.setToggleGroup(lod1Group); - lod1FileRB.setToggleGroup(lod1Group); - lod1AutoRB.setSelected(true); - - Button lod1GenBtn = new Button("LOD 1 generieren"); - lod1GenBtn.setMaxWidth(Double.MAX_VALUE); - lod1GenBtn.setOnAction(e -> { - if (lod1AutoRB.isSelected()) { - input.modelLodGenRequest.set(new SharedInput.ModelLodGenRequest(1, "auto_mesh", null)); - } else { - FileChooser lfc = new FileChooser(); - lfc.setTitle("LOD 1 – j3o wählen"); - lfc.getExtensionFilters().add(new FileChooser.ExtensionFilter("JME3 Modell (*.j3o)", "*.j3o")); - File lFile = lfc.showOpenDialog(primaryStage); - if (lFile == null) return; - Path lodDir = ASSET_ROOT.resolve("Models").resolve("lods"); - try { - Files.createDirectories(lodDir); - Path dest = lodDir.resolve(lFile.getName()); - Files.copy(lFile.toPath(), dest, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - String assetPath = ASSET_ROOT.relativize(dest).toString().replace('\\', '/'); - input.modelLodGenRequest.set(new SharedInput.ModelLodGenRequest(1, "file", assetPath)); - } catch (IOException ex) { - setStatus("LOD 1 Import fehlgeschlagen: " + ex.getMessage()); - } - } - }); - - modelImportLod1StatusLabel = new Label("(nicht gesetzt)"); - modelImportLod1StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); - modelImportLod1StatusLabel.setWrapText(true); - modelImportLod1ClearBtn = new Button("✕ LOD 1 entfernen"); - modelImportLod1ClearBtn.setStyle("-fx-text-fill:#c0392b;"); - modelImportLod1ClearBtn.setMaxWidth(Double.MAX_VALUE); - modelImportLod1ClearBtn.setDisable(true); - modelImportLod1ClearBtn.setOnAction(e -> { - // ModelImportState.clearLod==1 kaskadiert automatisch LOD2 - input.modelImportClearLodLevel = 1; - modelImportLod1StatusLabel.setText("(nicht gesetzt)"); - modelImportLod2StatusLabel.setText("(nicht gesetzt)"); - }); - - // ── LOD 2 ───────────────────────────────────────────────────────────── - Label lod2Title = new Label("LOD 2"); - lod2Title.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); - - RadioButton lod2MeshRB = new RadioButton("Auto (Starke Mesh-Reduktion, ~15%)"); - RadioButton lod2FileRB = new RadioButton("Aus Datei…"); - for (RadioButton rb : new RadioButton[]{lod2MeshRB, lod2FileRB}) - rb.setStyle("-fx-text-fill:#ccc;"); - javafx.scene.control.ToggleGroup lod2Group = new javafx.scene.control.ToggleGroup(); - lod2MeshRB.setToggleGroup(lod2Group); - lod2FileRB.setToggleGroup(lod2Group); - lod2MeshRB.setSelected(true); - - modelImportLod2GenBtn = new Button("LOD 2 generieren"); - Button lod2GenBtn = modelImportLod2GenBtn; - lod2GenBtn.setMaxWidth(Double.MAX_VALUE); - lod2GenBtn.setOnAction(e -> { - if (lod2MeshRB.isSelected()) { - input.modelLodGenRequest.set(new SharedInput.ModelLodGenRequest(2, "auto_mesh_heavy", null)); - } else { - FileChooser lfc = new FileChooser(); - lfc.setTitle("LOD 2 – j3o wählen"); - lfc.getExtensionFilters().add(new FileChooser.ExtensionFilter("JME3 Modell (*.j3o)", "*.j3o")); - File lFile = lfc.showOpenDialog(primaryStage); - if (lFile == null) return; - Path lodDir = ASSET_ROOT.resolve("Models").resolve("lods"); - try { - Files.createDirectories(lodDir); - Path dest = lodDir.resolve(lFile.getName()); - Files.copy(lFile.toPath(), dest, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - String assetPath = ASSET_ROOT.relativize(dest).toString().replace('\\', '/'); - input.modelLodGenRequest.set(new SharedInput.ModelLodGenRequest(2, "file", assetPath)); - } catch (IOException ex) { - setStatus("LOD 2 Import fehlgeschlagen: " + ex.getMessage()); - } - } - }); - - modelImportLod2StatusLabel = new Label("(nicht gesetzt)"); - modelImportLod2StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); - modelImportLod2StatusLabel.setWrapText(true); - modelImportLod2ClearBtn = new Button("✕ LOD 2 entfernen"); - modelImportLod2ClearBtn.setStyle("-fx-text-fill:#c0392b;"); - modelImportLod2ClearBtn.setMaxWidth(Double.MAX_VALUE); - modelImportLod2ClearBtn.setDisable(true); - modelImportLod2ClearBtn.setOnAction(e -> { - input.modelImportClearLodLevel = 2; - modelImportLod2StatusLabel.setText("(nicht gesetzt)"); - }); - - // ── Dateiname ────────────────────────────────────────────────────────── - Label fileNameLabel = new Label("Dateiname:"); - fileNameLabel.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); - TextField nameTF = new TextField(suggestedName); - nameTF.setStyle("-fx-background-color:#3a3a4e; -fx-text-fill:#eee;"); - nameTF.setPromptText("asset-name"); - - // ── Export ──────────────────────────────────────────────────────────── - modelImportExportStatusLabel = new Label(""); - modelImportExportStatusLabel.setStyle("-fx-text-fill:#8cf; -fx-font-size:11;"); - modelImportExportStatusLabel.setWrapText(true); - - Button exportBtn = new Button("📦 Als Asset importieren"); - exportBtn.setMaxWidth(Double.MAX_VALUE); - exportBtn.setStyle("-fx-background-color:#2e7d32; -fx-text-fill:#fff; -fx-font-weight:bold;"); - exportBtn.setOnAction(e -> { - String n = nameTF.getText().trim(); - if (!n.isEmpty()) input.modelImportExportName = n; - }); - - Button cancelBtn = new Button("✕ Abbrechen"); - cancelBtn.setMaxWidth(Double.MAX_VALUE); - cancelBtn.setStyle("-fx-background-color:#555; -fx-text-fill:#ccc;"); - cancelBtn.setOnAction(e -> { - input.modelEditorCloseRequest = true; - input.activeLayer = 0; - if (baseBtn != null) baseBtn.setSelected(true); - root.setRight(toolPanel); - switchToWorldEditor(); - }); - - panel.getChildren().addAll( - title, - new Separator(), - fileNameLabel, nameTF, - new Separator(), - scaleTitle, modelEditorUniformCB, scaleGrid, modelEditorDimLabel, modelEditorPolyLabel, - new Separator(), - algoTitle, algoBlightRB, algoJmeRB, - new Separator(), - lod1Title, lod1AutoRB, lod1FileRB, lod1GenBtn, modelImportLod1StatusLabel, modelImportLod1ClearBtn, - new Separator(), - lod2Title, lod2MeshRB, lod2FileRB, lod2GenBtn, modelImportLod2StatusLabel, modelImportLod2ClearBtn, - new Separator(), - modelImportExportStatusLabel, exportBtn, cancelBtn - ); - return panel; - } - - private Spinner buildScaleSpinner(double initial) { - SpinnerValueFactory.DoubleSpinnerValueFactory f = - new SpinnerValueFactory.DoubleSpinnerValueFactory(0.001, 1000.0, initial, 0.1); - f.setConverter(new javafx.util.converter.DoubleStringConverter() { - @Override public Double fromString(String s) { - try { return Double.parseDouble(s.replace(',', '.')); } - catch (Exception ex) { return initial; } - } - }); - Spinner s = new Spinner<>(f); - s.setEditable(true); - s.setMaxWidth(Double.MAX_VALUE); - s.setStyle("-fx-background-color:#3a3a4e;"); - return s; - } - - private Spinner buildOffsetSpinner(double initial) { - SpinnerValueFactory.DoubleSpinnerValueFactory f = - new SpinnerValueFactory.DoubleSpinnerValueFactory(-100.0, 100.0, initial, 0.1); - Spinner s = new Spinner<>(f); - s.setEditable(true); - s.setMaxWidth(Double.MAX_VALUE); - s.setStyle("-fx-background-color:#3a3a4e;"); - return s; - } - - private Spinner buildDistSpinner(double initial) { - SpinnerValueFactory.DoubleSpinnerValueFactory f = - new SpinnerValueFactory.DoubleSpinnerValueFactory(1.0, 2000.0, initial, 5.0); - f.setConverter(new javafx.util.converter.DoubleStringConverter() { - @Override public Double fromString(String s) { - try { return Double.parseDouble(s.replace(',', '.')); } - catch (Exception ex) { return initial; } - } - }); - Spinner s = new Spinner<>(f); - s.setEditable(true); - s.setMaxWidth(Double.MAX_VALUE); - s.setStyle("-fx-background-color:#3a3a4e;"); - return s; - } - - private void addLabeledRow(GridPane grid, int row, String labelText, javafx.scene.Node ctrl) { - if (grid.getColumnConstraints().isEmpty()) { - javafx.scene.layout.ColumnConstraints cc0 = new javafx.scene.layout.ColumnConstraints(); - cc0.setMinWidth(80); - cc0.setHgrow(javafx.scene.layout.Priority.NEVER); - javafx.scene.layout.ColumnConstraints cc1 = new javafx.scene.layout.ColumnConstraints(); - cc1.setHgrow(javafx.scene.layout.Priority.ALWAYS); - cc1.setFillWidth(true); - grid.getColumnConstraints().addAll(cc0, cc1); - } - Label l = new Label(labelText); - l.setStyle("-fx-text-fill:#aaa;"); - grid.add(l, 0, row); - grid.add(ctrl, 1, row); - } - - private void pushScaleToJme() { - if (modelEditorSpinX == null) return; - input.modelEditorScaleX = (float)(double) modelEditorSpinX.getValue(); - input.modelEditorScaleY = (float)(double) modelEditorSpinY.getValue(); - input.modelEditorScaleZ = (float)(double) modelEditorSpinZ.getValue(); - input.modelEditorScaleChanged = true; - } - - private void updateModelEditorDimensions(float w, float h, float d, int polyCount) { - if (modelEditorDimLabel != null) - modelEditorDimLabel.setText(String.format("B %.2f × H %.2f × T %.2f m", w, h, d)); - if (modelEditorPolyLabel != null) - modelEditorPolyLabel.setText(String.format("%,d Polygone", polyCount).replace(',', '.')); - } - - private void saveModelMeta(String relPath, - java.nio.file.Path absolutePath, - String name, String category, String tags, - float sx, float sy, float sz, boolean uniform, - float pivotY, float placeY, - boolean solid, boolean cast, boolean receive, - float rndMin, float rndMax, - String lod1Path, String lod2Path, - java.util.List lights, - java.util.List emitters, - de.blight.common.model.InteractableType interactableType, - float interactableOffsetX, float interactableOffsetY, - float interactableOffsetZ, float interactableRotY, - String footstepSurface, - de.blight.common.ModelCategory modelCategory) { - // Scale wird in j3o eingebrannt → Meta bekommt immer 1.0 (kein doppelter Scale beim Laden) - de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta( - name, category, tags, 1f, 1f, 1f, uniform, - pivotY, placeY, solid, cast, receive, rndMin, rndMax, - lod1Path, lod2Path, 30f, 80f, 120f, - lights, emitters, - interactableType != null ? interactableType : de.blight.common.model.InteractableType.NONE, - interactableOffsetX, interactableOffsetY, interactableOffsetZ, interactableRotY, - footstepSurface != null ? footstepSurface : "", - modelCategory != null ? modelCategory : de.blight.common.ModelCategory.NONE); - - if (absolutePath == null || !absolutePath.toFile().exists()) { - setStatus("Fehler: Modell-Datei nicht gefunden – Meta nicht gespeichert"); - return; - } - - // ── Meta-Datei speichern ────────────────────────────────────────────── - try { - de.blight.common.ModelMetaIO.save(meta, absolutePath); - } catch (java.io.IOException ex) { - setStatus("Fehler beim Speichern der Meta-Datei: " + ex.getMessage()); - return; - } - - // ── Unterordner: Datei verschieben wenn Kategorie gesetzt ────────────── - if (!category.isEmpty()) { - java.nio.file.Path newDir = ASSET_ROOT.resolve("Models") - .resolve(java.nio.file.Path.of(category.replace('/', java.io.File.separatorChar))); - java.nio.file.Path newJ3o = newDir.resolve( - (name.isEmpty() ? absolutePath.getFileName().toString() - : name.replaceAll("[\\\\/:*?\"<>|]", "_") + ".j3o")); - - if (!newJ3o.equals(absolutePath)) { - try { - java.nio.file.Files.createDirectories(newDir); - java.nio.file.Path metaSrc = de.blight.common.ModelMetaIO.metaPath(absolutePath); - java.nio.file.Files.move(absolutePath, newJ3o, - java.nio.file.StandardCopyOption.REPLACE_EXISTING); - if (java.nio.file.Files.exists(metaSrc)) { - java.nio.file.Files.move(metaSrc, - de.blight.common.ModelMetaIO.metaPath(newJ3o), - java.nio.file.StandardCopyOption.REPLACE_EXISTING); - } - // Pfad für Platzierungsmodus aktualisieren - modelEditorCurrentPath = ASSET_ROOT.relativize(newJ3o) - .toString().replace('\\', '/'); - setStatus("Gespeichert und verschoben → " + modelEditorCurrentPath); - } catch (java.io.IOException ex) { - setStatus("Meta gespeichert, Verschieben fehlgeschlagen: " + ex.getMessage()); - return; - } - } else { - setStatus("Meta gespeichert: " + relPath); - } - } else { - setStatus("Meta gespeichert: " + relPath); - } - - // Asset-Tree aktualisieren - input.refreshAssets = true; - - // Bereits platzierte Instanzen dieses Modells synchronisieren (solid + modelCategory) - try { - java.util.List placed = de.blight.common.PlacedModelIO.load(); - boolean anyChanged = false; - java.util.List updatedPlaced = new java.util.ArrayList<>(placed.size()); - for (de.blight.common.PlacedModel pm : placed) { - if (relPath.equals(pm.modelPath()) || relPath.equals(pm.meshFile())) { - updatedPlaced.add(new de.blight.common.PlacedModel( - pm.modelPath(), pm.x(), pm.y(), pm.z(), - pm.rotY(), pm.rotX(), pm.rotZ(), pm.scale(), - solid, - pm.texturePath(), pm.normalMapPath(), pm.materialPath(), - pm.meshFile(), pm.animClip(), pm.castShadow(), pm.receiveShadow(), - pm.lod1Path(), pm.lod2Path(), pm.lod1Distance(), pm.lod2Distance(), - pm.cullDistance(), pm.interactableType(), pm.interactableId(), - modelCategory != null ? modelCategory : de.blight.common.ModelCategory.NONE)); - anyChanged = true; - } else { - updatedPlaced.add(pm); - } - } - if (anyChanged) { - de.blight.common.PlacedModelIO.save(updatedPlaced); - setStatus(statusLabel.getText() + " (Instanzen aktualisiert)"); - } - } catch (Exception ex) { - log.warn("[ModelMeta] Instanz-Update fehlgeschlagen: {}", ex.getMessage()); - } - - java.nio.file.Path finalJ3o = category.isEmpty() ? absolutePath - : ASSET_ROOT.resolve("Models") - .resolve(java.nio.file.Path.of(category.replace('/', java.io.File.separatorChar))) - .resolve(name.isEmpty() ? absolutePath.getFileName().toString() - : name.replaceAll("[\\\\/:*?\"<>|]", "_") + ".j3o"); - - // Scale in j3o einbrennen (JME3-Thread) – muss vor Thumbnail passieren - if (sx != 1f || sy != 1f || sz != 1f) { - input.modelEditorScaleX = sx; - input.modelEditorScaleY = sy; - input.modelEditorScaleZ = sz; - input.modelEditorBakeScalePath = finalJ3o; - input.modelEditorBakeScaleRequest = true; - } - - // In-Memory-LODs in j3o einbetten (nach Scale-Bake, vor Thumbnail) - if (input.modelImportHasLod1 || input.modelImportHasLod2) { - input.modelEditorEmbedLodsPath = finalJ3o; - input.modelEditorEmbedLodsRequest = true; - } - - // Thumbnail generieren (JME3-Thread liest das Flag und rendert) - input.modelEditorThumbnailRequest = finalJ3o; - } - - // ── Anhang-Hilfsmethoden (Modell-Editor) ────────────────────────────────── - - private void pushAttachmentsToJme() { - input.modelEditorAttachedLights = new java.util.ArrayList<>(modelEditorLights); - input.modelEditorAttachedEmitters = new java.util.ArrayList<>(modelEditorEmitters); - input.modelEditorAttachmentsChanged = true; - } - - private void rebuildLightBox() { - if (modelEditorLightBox == null) return; - modelEditorLightBox.getChildren().clear(); - for (int i = 0; i < modelEditorLights.size(); i++) { - final int idx = i; - de.blight.common.ModelMeta.AttachedLight al = modelEditorLights.get(i); - modelEditorLightBox.getChildren().add(buildLightEntry(idx, al)); - } - } - - private javafx.scene.Node buildLightEntry(int idx, - de.blight.common.ModelMeta.AttachedLight al) { - javafx.scene.control.TitledPane tp = new javafx.scene.control.TitledPane(); - tp.setText("Licht " + (idx + 1)); - tp.setExpanded(false); - tp.setStyle("-fx-text-fill:#ddd; -fx-background-color:#333;"); - - GridPane g = new GridPane(); - g.setHgap(6); g.setVgap(3); - g.setPadding(new javafx.geometry.Insets(4)); - - Spinner ox = buildOffsetSpinner(al.offsetX()); - Spinner oy = buildOffsetSpinner(al.offsetY()); - Spinner oz = buildOffsetSpinner(al.offsetZ()); - addLabeledRow(g, 0, "Offset X:", ox); - addLabeledRow(g, 1, "Offset Y:", oy); - addLabeledRow(g, 2, "Offset Z:", oz); - - javafx.scene.control.ColorPicker cp = new javafx.scene.control.ColorPicker( - javafx.scene.paint.Color.color( - Math.min(1, al.r()), Math.min(1, al.g()), Math.min(1, al.b()))); - cp.setMaxWidth(Double.MAX_VALUE); - g.add(new Label("Farbe:") {{ setStyle("-fx-text-fill:#aaa;"); }}, 0, 3); - g.add(cp, 1, 3); - - Spinner intSpin = new Spinner<>(0.0, 20.0, al.intensity(), 0.5); - intSpin.setEditable(true); - addLabeledRow(g, 4, "Intensität:", intSpin); - - Spinner radSpin = new Spinner<>(0.5, 50.0, al.radius(), 1.0); - radSpin.setEditable(true); - addLabeledRow(g, 5, "Radius:", radSpin); - - Runnable sync = () -> { - javafx.scene.paint.Color c = cp.getValue(); - modelEditorLights.set(idx, new de.blight.common.ModelMeta.AttachedLight( - ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue(), - (float) c.getRed(), (float) c.getGreen(), (float) c.getBlue(), - intSpin.getValue().floatValue(), radSpin.getValue().floatValue())); - pushAttachmentsToJme(); - }; - ox.valueProperty().addListener((o,ov,nv) -> sync.run()); - oy.valueProperty().addListener((o,ov,nv) -> sync.run()); - oz.valueProperty().addListener((o,ov,nv) -> sync.run()); - cp.valueProperty().addListener((o,ov,nv) -> sync.run()); - intSpin.valueProperty().addListener((o,ov,nv) -> sync.run()); - radSpin.valueProperty().addListener((o,ov,nv) -> sync.run()); - - Button del = new Button("✕ Entfernen"); - del.setStyle("-fx-background-color:#7a2222; -fx-text-fill:#fff;"); - del.setMaxWidth(Double.MAX_VALUE); - del.setOnAction(ev -> { - modelEditorLights.remove(idx); - rebuildLightBox(); - pushAttachmentsToJme(); - }); - - VBox content = new VBox(4, g, del); - content.setPadding(new javafx.geometry.Insets(2)); - tp.setContent(content); - return tp; - } - - private void rebuildEmitterBox() { - if (modelEditorEmitterBox == null) return; - modelEditorEmitterBox.getChildren().clear(); - for (int i = 0; i < modelEditorEmitters.size(); i++) { - final int idx = i; - de.blight.common.ModelMeta.AttachedEmitter ae = modelEditorEmitters.get(i); - modelEditorEmitterBox.getChildren().add(buildEmitterEntry(idx, ae)); - } - } - - private javafx.scene.Node buildEmitterEntry(int idx, - de.blight.common.ModelMeta.AttachedEmitter ae) { - javafx.scene.control.TitledPane tp = new javafx.scene.control.TitledPane(); - tp.setText("Emitter " + (idx + 1)); - tp.setExpanded(false); - tp.setStyle("-fx-text-fill:#ddd; -fx-background-color:#333;"); - - GridPane g = new GridPane(); - g.setHgap(6); g.setVgap(3); - g.setPadding(new javafx.geometry.Insets(4)); - - Spinner ox = buildOffsetSpinner(ae.offsetX()); - Spinner oy = buildOffsetSpinner(ae.offsetY()); - Spinner oz = buildOffsetSpinner(ae.offsetZ()); - addLabeledRow(g, 0, "Offset X:", ox); - addLabeledRow(g, 1, "Offset Y:", oy); - addLabeledRow(g, 2, "Offset Z:", oz); - - ComboBox presetCB = new ComboBox<>(); - presetCB.getItems().addAll("Feuer", "Rauch", "Funken"); - presetCB.getSelectionModel().select(Math.max(0, Math.min(2, ae.preset()))); - presetCB.setMaxWidth(Double.MAX_VALUE); - g.add(new Label("Typ:") {{ setStyle("-fx-text-fill:#aaa;"); }}, 0, 3); - g.add(presetCB, 1, 3); - - Runnable sync = () -> { - modelEditorEmitters.set(idx, new de.blight.common.ModelMeta.AttachedEmitter( - ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue(), - presetCB.getSelectionModel().getSelectedIndex())); - pushAttachmentsToJme(); - }; - ox.valueProperty().addListener((o,ov,nv) -> sync.run()); - oy.valueProperty().addListener((o,ov,nv) -> sync.run()); - oz.valueProperty().addListener((o,ov,nv) -> sync.run()); - presetCB.valueProperty().addListener((o,ov,nv) -> sync.run()); - - Button del = new Button("✕ Entfernen"); - del.setStyle("-fx-background-color:#7a2222; -fx-text-fill:#fff;"); - del.setMaxWidth(Double.MAX_VALUE); - del.setOnAction(ev -> { - modelEditorEmitters.remove(idx); - rebuildEmitterBox(); - pushAttachmentsToJme(); - }); - - VBox content = new VBox(4, g, del); - content.setPadding(new javafx.geometry.Insets(2)); - tp.setContent(content); - return tp; - } - - private void importLodFile(int slot) { - FileChooser fc = new FileChooser(); - fc.setTitle("LOD " + slot + " – j3o wählen"); - fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("JME3 Modell (*.j3o)", "*.j3o")); - File chosen = fc.showOpenDialog(primaryStage); - if (chosen == null) return; - java.nio.file.Path lodDir = ASSET_ROOT.resolve("Models").resolve("lods"); - try { - java.nio.file.Files.createDirectories(lodDir); - java.nio.file.Path dest = lodDir.resolve(chosen.getName()); - java.nio.file.Files.copy(chosen.toPath(), dest, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - String relPath = ASSET_ROOT.relativize(dest).toString().replace('\\', '/'); - if (slot == 1) { - modelEditorLod1Path = relPath; - input.modelEditorLod1Path = relPath; - if (modelEditorLod1Label != null) modelEditorLod1Label.setText(relPath); - } else { - modelEditorLod2Path = relPath; - input.modelEditorLod2Path = relPath; - if (modelEditorLod2Label != null) modelEditorLod2Label.setText(relPath); - } - setStatus("LOD " + slot + " importiert: " + relPath); - boolean pe = shouldPreserveExpansion(); - refreshCategoryNode(modelsNode, pe); - } catch (java.io.IOException ex) { - setStatus("Import fehlgeschlagen: " + ex.getMessage()); - } - } - - private void showLodGenDialog(int level, int defaultPct) { - javafx.scene.control.Dialog> dlg = new javafx.scene.control.Dialog<>(); - dlg.initOwner(primaryStage); - dlg.setTitle("LOD " + level + " generieren"); - dlg.setHeaderText("Methode und Reduktion für LOD " + level + " wählen"); - - RadioButton jmeRB = new RadioButton("JME (Progressive Mesh)"); - RadioButton blightRB = new RadioButton("Blight (Edge Collapse)"); - RadioButton impostorRB = new RadioButton("Impostor Image (Billboard)"); - jmeRB.setSelected(true); - javafx.scene.control.ToggleGroup tg = new javafx.scene.control.ToggleGroup(); - jmeRB.setToggleGroup(tg); - blightRB.setToggleGroup(tg); - impostorRB.setToggleGroup(tg); - - Label pctLabel = new Label("Reduktion (%):"); - Spinner pctSpinner = new Spinner<>( - new javafx.scene.control.SpinnerValueFactory.IntegerSpinnerValueFactory(5, 95, defaultPct, 5)); - pctSpinner.setEditable(true); - pctSpinner.setPrefWidth(90); - - impostorRB.selectedProperty().addListener((obs, ov, nv) -> { - pctLabel.setDisable(nv); - pctSpinner.setDisable(nv); - }); - - javafx.scene.layout.VBox content = new javafx.scene.layout.VBox(8, - jmeRB, blightRB, impostorRB, - new Separator(), - new HBox(8, pctLabel, pctSpinner)); - content.setPadding(new javafx.geometry.Insets(12)); - dlg.getDialogPane().setContent(content); - dlg.getDialogPane().getButtonTypes().addAll( - javafx.scene.control.ButtonType.OK, javafx.scene.control.ButtonType.CANCEL); - - dlg.setResultConverter(bt -> { - if (bt != javafx.scene.control.ButtonType.OK) return null; - String method = impostorRB.isSelected() ? "impostor" - : blightRB.isSelected() ? "blight_auto" - : "jme_auto"; - return new javafx.util.Pair<>(method, pctSpinner.getValue()); - }); - - dlg.showAndWait().ifPresent(result -> { - String method = result.getKey(); - float reductionPct = result.getValue() / 100f; - input.modelLodGenRequest.set( - new SharedInput.ModelLodGenRequest(level, method, null, reductionPct)); - }); - } // ── Zentraler Bereich: JME3-Viewport ──────────────────────────────────── @@ -8945,8 +6575,8 @@ public class EditorApp extends Application { } } case F7 -> { - if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (objPlaceBtn != null) objPlaceBtn.fire(); }); - else if (pressed && "objecteditor".equals(currentTool)) Platform.runLater(() -> { if (objPlaceBtn != null) objPlaceBtn.fire(); }); + if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (objPlaceBtn != null) objPlaceBtn.fire(); }); + else if (pressed && "objecteditor".equals(registry.activeToolId())) Platform.runLater(objectController::activatePlaceMode); } case F8 -> { if (pressed) { diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/model/ModelEditorController.java b/blight-editor/src/main/java/de/blight/editor/toolset/model/ModelEditorController.java new file mode 100644 index 0000000..8128e2f --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/model/ModelEditorController.java @@ -0,0 +1,1673 @@ +package de.blight.editor.toolset.model; + +import de.blight.editor.SharedInput; +import de.blight.editor.ToolContext; +import de.blight.editor.ToolController; +import de.blight.editor.toolset.object.ObjectController; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.stage.FileChooser; +import javafx.stage.Window; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Deque; +import java.util.List; + +public class ModelEditorController implements ToolController { + + private static final Logger log = LoggerFactory.getLogger(ModelEditorController.class); + + private final ToolContext ctx; + private final Runnable openAssetOverlay; + private ObjectController objectController; + + // ── Felder ────────────────────────────────────────────────────────────── + + private Label modelEditorDimLabel; + private Label modelEditorPolyLabel; + private Spinner modelEditorSpinX, modelEditorSpinY, modelEditorSpinZ; + private CheckBox modelEditorUniformCB; + private boolean modelEditorSuppressListeners = false; + private String modelEditorCurrentPath = null; + private String modelEditorLod1Path = ""; + private String modelEditorLod2Path = ""; + private Label modelEditorLod1Label; + private Label modelEditorLod2Label; + private ToggleButton modelEditorLod0Btn; + private ToggleButton modelEditorLod1Btn; + private ToggleButton modelEditorLod2Btn; + private Label modelEditorLod1StatusLabel; + private Label modelEditorLod2StatusLabel; + private Button modelEditorLod1GenBtn; + private Button modelEditorLod2GenBtn; + private ToggleButton importLod0Btn; + private ToggleButton importLod1Btn; + private ToggleButton importLod2Btn; + private Button modelImportLod1ClearBtn; + private Button modelImportLod2ClearBtn; + private Button modelImportLod2GenBtn; + private final java.util.List modelEditorLights = new java.util.ArrayList<>(); + private final java.util.List modelEditorEmitters = new java.util.ArrayList<>(); + private VBox modelEditorLightBox = null; + private VBox modelEditorEmitterBox = null; + private ComboBox modelEditorCategoryCB = null; + private ComboBox modelEditorInteractableCB = null; + private Spinner modelEditorInteractableXSpin = null; + private Spinner modelEditorInteractableYSpin = null; + private Spinner modelEditorInteractableZSpin = null; + private ComboBox modelEditorFootstepSurfaceCB = null; + private boolean updatingInteractableSpinnersFromJme = false; + private Label modelImportLod1StatusLabel; + private Label modelImportLod2StatusLabel; + private Label modelImportExportStatusLabel; + private String modelImportCurrentPath = null; + private final Deque modelImportQueue = new java.util.ArrayDeque<>(); + + // Pending open params (set before registry.activate) + private String pendingRelPath; + private Path pendingAbsPath; + + public ModelEditorController(ToolContext ctx, Runnable openAssetOverlay) { + this.ctx = ctx; + this.openAssetOverlay = openAssetOverlay; + } + + public void setObjectController(ObjectController oc) { + this.objectController = oc; + } + + // ── ToolController ────────────────────────────────────────────────────── + + @Override + public String toolId() { return "modeleditor"; } + + @Override + public void activate() { + String relPath = pendingRelPath; + Path absPath = pendingAbsPath; + + modelEditorCurrentPath = relPath; + ctx.input().activeLayer = SharedInput.LAYER_MODEL_EDITOR; + ctx.setCenterView().accept(ctx.viewportSupplier().get()); + openAssetOverlay.run(); + + if (relPath == null) { + modelEditorLod1Path = ""; + modelEditorLod2Path = ""; + ctx.input().modelEditorHasEmbeddedLods = false; + ctx.setMainToolbar().accept(buildModelEditorToolBar()); + VBox placeholder = new VBox(12); + placeholder.setPadding(new Insets(12)); + placeholder.setPrefWidth(280); + placeholder.setStyle("-fx-background-color: #2a2a3e;"); + Label title = new Label("Modell-Editor"); + title.setStyle("-fx-font-weight:bold; -fx-font-size:14; -fx-text-fill:#ddd;"); + Label hint = new Label("Modell im Asset-Baum\nper Rechtsklick → Im Objekt-Editor öffnen\nauswählen."); + hint.setStyle("-fx-text-fill:#888; -fx-font-size:12;"); + hint.setWrapText(true); + placeholder.getChildren().addAll(title, hint); + ctx.setRightPanel().accept(placeholder); + ctx.setStatus().accept("Modell-Editor: Modell aus Asset-Baum wählen"); + return; + } + + de.blight.common.ModelMeta meta = absPath != null && absPath.toFile().exists() + ? de.blight.common.ModelMetaIO.load(absPath) + : de.blight.common.ModelMeta.defaults(relPath.replaceAll(".*/", "")); + + modelEditorLod1Path = meta.lod1Path(); + modelEditorLod2Path = meta.lod2Path(); + ctx.input().modelEditorLod1Path = modelEditorLod1Path; + ctx.input().modelEditorLod2Path = modelEditorLod2Path; + ctx.input().modelEditorHasEmbeddedLods = false; + ctx.input().modelEditorLodPreview = 0; + ctx.setMainToolbar().accept(buildModelEditorToolBar()); + + ctx.input().modelEditorScaleX = meta.scaleX(); + ctx.input().modelEditorScaleY = meta.scaleY(); + ctx.input().modelEditorScaleZ = meta.scaleZ(); + ctx.input().modelEditorPivotY = meta.pivotOffsetY(); + ctx.input().modelEditorOpenPath = relPath; + + VBox inner = buildModelEditorPanel(relPath, absPath, meta); + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color:transparent;-fx-background:transparent;"); + VBox outer = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + outer.setPrefWidth(280); + outer.setMaxWidth(280); + ctx.setRightPanel().accept(outer); + ctx.setStatus().accept("Modell-Editor: " + relPath); + } + + @Override + public void deactivate() {} + + @Override + public void onFrame() { + // Ruhepunkt-Spinner nach Raycast-Klick (JME→JFX) + if (ctx.input().modelInteractablePosSetFromJme && modelEditorInteractableXSpin != null) { + ctx.input().modelInteractablePosSetFromJme = false; + updatingInteractableSpinnersFromJme = true; + modelEditorInteractableXSpin.getValueFactory().setValue( + Math.round(ctx.input().modelInteractableOffsetX * 100.0) / 100.0); + modelEditorInteractableYSpin.getValueFactory().setValue( + Math.round(ctx.input().modelInteractableOffsetY * 100.0) / 100.0); + modelEditorInteractableZSpin.getValueFactory().setValue( + Math.round(ctx.input().modelInteractableOffsetZ * 100.0) / 100.0); + updatingInteractableSpinnersFromJme = false; + } + + // Baked-Scale aus j3o erkannt → Spinner aktualisieren + if (ctx.input().modelEditorBakedScaleDetected) { + ctx.input().modelEditorBakedScaleDetected = false; + double bsX = ctx.input().modelEditorScaleX; + double bsY = ctx.input().modelEditorScaleY; + double bsZ = ctx.input().modelEditorScaleZ; + if (modelEditorSpinX != null) modelEditorSpinX.getValueFactory().setValue(bsX); + if (modelEditorSpinY != null) modelEditorSpinY.getValueFactory().setValue(bsY); + if (modelEditorSpinZ != null) modelEditorSpinZ.getValueFactory().setValue(bsZ); + } + + // Bounds-Aktualisierung + if (ctx.input().modelEditorBoundsReady) { + ctx.input().modelEditorBoundsReady = false; + updateModelEditorDimensions( + ctx.input().modelEditorBoundsW, + ctx.input().modelEditorBoundsH, + ctx.input().modelEditorBoundsD, + ctx.input().modelEditorPolyCount); + } + + // Eingebettete LODs erkannt → Labels aktualisieren + if (ctx.input().modelEditorHasEmbeddedLod1 && modelEditorLod1Label != null + && "(kein Pfad)".equals(modelEditorLod1Label.getText())) { + modelEditorLod1Label.setText("(eingebettet im j3o)"); + modelEditorLod1Label.setStyle("-fx-text-fill:#6aaa6a; -fx-font-size:11;"); + } + if (ctx.input().modelEditorHasEmbeddedLod2 && modelEditorLod2Label != null + && "(kein Pfad)".equals(modelEditorLod2Label.getText())) { + modelEditorLod2Label.setText("(eingebettet im j3o)"); + modelEditorLod2Label.setStyle("-fx-text-fill:#6aaa6a; -fx-font-size:11;"); + } + + // LOD-Buttons freischalten/sperren + if (modelEditorLod1Btn != null) + modelEditorLod1Btn.setDisable(!ctx.input().modelEditorHasEmbeddedLod1 && modelEditorLod1Path.isBlank() && !ctx.input().modelImportHasLod1); + if (modelEditorLod2Btn != null) + modelEditorLod2Btn.setDisable(!ctx.input().modelEditorHasEmbeddedLod2 && modelEditorLod2Path.isBlank() && !ctx.input().modelImportHasLod2); + if (modelEditorLod1GenBtn != null) + modelEditorLod1GenBtn.setDisable(ctx.input().modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || ctx.input().modelImportHasLod1); + if (modelEditorLod2GenBtn != null) { + boolean lod1Set = ctx.input().modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || ctx.input().modelImportHasLod1; + boolean lod2Set = ctx.input().modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || ctx.input().modelImportHasLod2; + modelEditorLod2GenBtn.setDisable(!lod1Set || lod2Set); + } + if (importLod1Btn != null) importLod1Btn.setDisable(!ctx.input().modelImportHasLod1); + if (importLod2Btn != null) importLod2Btn.setDisable(!ctx.input().modelImportHasLod2); + + // LOD-Buttons mit Preview-Level synchronisieren + int lodPreview = ctx.input().modelEditorLodPreview; + if (modelEditorLod0Btn != null) modelEditorLod0Btn.setSelected(lodPreview == 0); + if (modelEditorLod1Btn != null) modelEditorLod1Btn.setSelected(lodPreview == 1); + if (modelEditorLod2Btn != null) modelEditorLod2Btn.setSelected(lodPreview == 2); + if (importLod0Btn != null) importLod0Btn.setSelected(lodPreview == 0); + if (importLod1Btn != null) importLod1Btn.setSelected(lodPreview == 1); + if (importLod2Btn != null) importLod2Btn.setSelected(lodPreview == 2); + if (modelImportLod1ClearBtn != null) modelImportLod1ClearBtn.setDisable(!ctx.input().modelImportHasLod1); + if (modelImportLod2ClearBtn != null) modelImportLod2ClearBtn.setDisable(!ctx.input().modelImportHasLod2); + if (modelImportLod2GenBtn != null) modelImportLod2GenBtn.setDisable(!ctx.input().modelImportHasLod1 || ctx.input().modelImportHasLod2); + + // LOD-Status-Nachrichten + String lodStatus = ctx.input().modelLodGenStatus; + if (lodStatus != null) { + ctx.input().modelLodGenStatus = null; + boolean isLod1 = lodStatus.startsWith("LOD1"); + boolean isLod2 = lodStatus.startsWith("LOD2") || lodStatus.contains("Impostor"); + if (isLod1) { + if (modelImportLod1StatusLabel != null) modelImportLod1StatusLabel.setText(lodStatus); + if (modelEditorLod1StatusLabel != null) modelEditorLod1StatusLabel.setText(lodStatus); + } else if (isLod2) { + if (modelImportLod2StatusLabel != null) modelImportLod2StatusLabel.setText(lodStatus); + if (modelEditorLod2StatusLabel != null) modelEditorLod2StatusLabel.setText(lodStatus); + } + ctx.setStatus().accept(lodStatus); + } + + // Import-Export-Ergebnis + String expStatus = ctx.input().modelImportExportStatus; + if (expStatus != null) { + ctx.input().modelImportExportStatus = null; + if (modelImportExportStatusLabel != null) modelImportExportStatusLabel.setText(expStatus); + int remaining = modelImportQueue.size(); + ctx.setStatus().accept("Import: " + expStatus + (remaining > 0 ? " (" + remaining + " weitere)" : "")); + if (!expStatus.startsWith("FEHLER")) { + ctx.input().refreshAssets = true; + } + if (!modelImportQueue.isEmpty()) { + File next = modelImportQueue.poll(); + startImportFile(next); + } + } + } + + // ── Öffentliche API ───────────────────────────────────────────────────── + + /** Öffnet den Modell-Editor für ein bestimmtes Modell (oder leer wenn relPath==null). */ + public void openModelEditor(String relPath, Path absolutePath) { + this.pendingRelPath = relPath; + this.pendingAbsPath = absolutePath; + ctx.registry().activate(this); + } + + /** Öffnet den Datei-Dialog für den Modell-Import. */ + public void openModelImport(Window owner) { + FileChooser fc = new FileChooser(); + fc.setTitle("Modell importieren"); + fc.getExtensionFilters().add( + new FileChooser.ExtensionFilter("3D-Modelle", "*.j3o", "*.gltf", "*.glb", "*.obj")); + List files = fc.showOpenMultipleDialog(owner); + if (files == null || files.isEmpty()) return; + + modelImportQueue.clear(); + if (files.size() == 1) { + startImportFile(files.get(0)); + } else { + modelImportQueue.addAll(files.subList(1, files.size())); + ctx.setStatus().accept("Mehrfach-Import: " + files.size() + " Dateien – starte mit " + files.get(0).getName()); + startImportFile(files.get(0)); + } + } + + // ── Toolbar ────────────────────────────────────────────────────────────── + + private ToolBar buildModelEditorToolBar() { + Button backBtn = new Button("← Welteneditor"); + backBtn.setOnAction(e -> ctx.switchToWorld().run()); + Label label = new Label("Model Editor"); + label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); + + modelEditorLod0Btn = new ToggleButton("LOD 0"); + ToggleButton lod0Btn = modelEditorLod0Btn; + modelEditorLod1Btn = new ToggleButton("LOD 1"); + modelEditorLod2Btn = new ToggleButton("LOD 2"); + ToggleGroup lodGroup = new ToggleGroup(); + lod0Btn.setToggleGroup(lodGroup); + modelEditorLod1Btn.setToggleGroup(lodGroup); + modelEditorLod2Btn.setToggleGroup(lodGroup); + lod0Btn.setSelected(true); + String lodStyle = "-fx-font-size:11; -fx-padding:2 8 2 8;"; + lod0Btn.setStyle(lodStyle); + modelEditorLod1Btn.setStyle(lodStyle); + modelEditorLod2Btn.setStyle(lodStyle); + lod0Btn.setOnAction(e -> { ctx.input().modelEditorLodPreview = 0; ctx.input().modelEditorLodChanged = true; }); + modelEditorLod1Btn.setOnAction(e -> { ctx.input().modelEditorLodPreview = 1; ctx.input().modelEditorLodChanged = true; }); + modelEditorLod2Btn.setOnAction(e -> { ctx.input().modelEditorLodPreview = 2; ctx.input().modelEditorLodChanged = true; }); + boolean hasLod1 = ctx.input().modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || ctx.input().modelImportHasLod1; + boolean hasLod2 = ctx.input().modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || ctx.input().modelImportHasLod2; + modelEditorLod1Btn.setDisable(!hasLod1); + modelEditorLod2Btn.setDisable(!hasLod2); + + ToolBar tb = new ToolBar(); + tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label, + new Separator(Orientation.VERTICAL), lod0Btn, + modelEditorLod1Btn, modelEditorLod2Btn); + return tb; + } + + // ── Modell-Editor-Panel ─────────────────────────────────────────────────── + + private VBox buildModelEditorPanel(String relPath, + Path absolutePath, + de.blight.common.ModelMeta meta) { + VBox panel = new VBox(6); + panel.setPadding(new Insets(8)); + panel.setPrefWidth(280); + panel.setStyle("-fx-background-color: #2a2a3e;"); + + Label title = new Label("Modell-Editor"); + title.setStyle("-fx-font-weight:bold; -fx-font-size:14; -fx-text-fill:#ddd;"); + + Label nameLabel = new Label("Name:"); + nameLabel.setStyle("-fx-text-fill:#aaa;"); + TextField nameTF = new TextField(meta.name()); + nameTF.setStyle("-fx-background-color:#3a3a4e; -fx-text-fill:#eee;"); + + Label catLabel = new Label("Kategorie / Unterordner:"); + catLabel.setStyle("-fx-text-fill:#aaa;"); + ComboBox catCB = new ComboBox<>(); + catCB.setEditable(true); + catCB.getItems().addAll("", "vegetation/trees", "vegetation/bushes", + "vegetation/rocks", "buildings", "props", "characters", + "terrain", "effects", "misc"); + catCB.setValue(meta.category()); + catCB.setMaxWidth(Double.MAX_VALUE); + catCB.setStyle("-fx-background-color:#3a3a4e;"); + + Label tagsLabel = new Label("Tags (kommagetrennt):"); + tagsLabel.setStyle("-fx-text-fill:#aaa;"); + TextField tagsTF = new TextField(meta.tags()); + tagsTF.setStyle("-fx-background-color:#3a3a4e; -fx-text-fill:#eee;"); + tagsTF.setPromptText("z.B. tree, oak, large"); + + Label scaleTitle = new Label("Skalierung:"); + scaleTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + modelEditorUniformCB = new CheckBox("Gleichmäßig skalieren (X=Y=Z)"); + modelEditorUniformCB.setSelected(meta.uniformScale()); + modelEditorUniformCB.setStyle("-fx-text-fill:#ccc;"); + + modelEditorSpinX = buildScaleSpinner(meta.scaleX()); + modelEditorSpinY = buildScaleSpinner(meta.scaleY()); + modelEditorSpinZ = buildScaleSpinner(meta.scaleZ()); + + GridPane scaleGrid = new GridPane(); + scaleGrid.setHgap(6); scaleGrid.setVgap(4); + addLabeledRow(scaleGrid, 0, "X:", modelEditorSpinX); + addLabeledRow(scaleGrid, 1, "Y:", modelEditorSpinY); + addLabeledRow(scaleGrid, 2, "Z:", modelEditorSpinZ); + + modelEditorSpinX.valueProperty().addListener((o, ov, nv) -> { + if (modelEditorSuppressListeners) return; + if (modelEditorUniformCB.isSelected()) { + modelEditorSuppressListeners = true; + modelEditorSpinY.getValueFactory().setValue(nv); + modelEditorSpinZ.getValueFactory().setValue(nv); + modelEditorSuppressListeners = false; + } + pushScaleToJme(); + }); + modelEditorSpinY.valueProperty().addListener((o, ov, nv) -> { + if (modelEditorSuppressListeners) return; + if (modelEditorUniformCB.isSelected()) { + modelEditorSuppressListeners = true; + modelEditorSpinX.getValueFactory().setValue(nv); + modelEditorSpinZ.getValueFactory().setValue(nv); + modelEditorSuppressListeners = false; + } + pushScaleToJme(); + }); + modelEditorSpinZ.valueProperty().addListener((o, ov, nv) -> { + if (modelEditorSuppressListeners) return; + if (modelEditorUniformCB.isSelected()) { + modelEditorSuppressListeners = true; + modelEditorSpinX.getValueFactory().setValue(nv); + modelEditorSpinY.getValueFactory().setValue(nv); + modelEditorSuppressListeners = false; + } + pushScaleToJme(); + }); + modelEditorUniformCB.setOnAction(e -> { + if (modelEditorUniformCB.isSelected()) { + double xVal = modelEditorSpinX.getValue(); + modelEditorSuppressListeners = true; + modelEditorSpinY.getValueFactory().setValue(xVal); + modelEditorSpinZ.getValueFactory().setValue(xVal); + modelEditorSuppressListeners = false; + pushScaleToJme(); + } + }); + + modelEditorDimLabel = new Label("B – × H – × T – m"); + modelEditorDimLabel.setStyle("-fx-text-fill:#8cf; -fx-font-family:monospace;"); + modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE); + modelEditorDimLabel.setWrapText(true); + + modelEditorPolyLabel = new Label("– Polygone"); + modelEditorPolyLabel.setStyle("-fx-text-fill:#aaa; -fx-font-family:monospace; -fx-font-size:11;"); + modelEditorPolyLabel.setMaxWidth(Double.MAX_VALUE); + + Label offsetTitle = new Label("Versatz:"); + offsetTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + Spinner pivotSpin = buildOffsetSpinner(meta.pivotOffsetY()); + pivotSpin.valueProperty().addListener((o, ov, nv) -> { + ctx.input().modelEditorPivotY = nv.floatValue(); + ctx.input().modelEditorPivotChanged = true; + }); + + Spinner placeSpin = buildOffsetSpinner(meta.placementOffsetY()); + + GridPane offsetGrid = new GridPane(); + offsetGrid.setHgap(6); offsetGrid.setVgap(4); + addLabeledRow(offsetGrid, 0, "Pivot Y:", pivotSpin); + addLabeledRow(offsetGrid, 1, "Platzierung Y:", placeSpin); + + Label defaultsTitle = new Label("Standard beim Platzieren:"); + defaultsTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + CheckBox solidCB = new CheckBox("Kollision (solid)"); + CheckBox castCB = new CheckBox("Schatten werfen"); + CheckBox receiveCB = new CheckBox("Schatten empfangen"); + solidCB.setSelected(meta.solid()); + castCB.setSelected(meta.castShadow()); + receiveCB.setSelected(meta.receiveShadow()); + for (CheckBox cb : new CheckBox[]{solidCB, castCB, receiveCB}) + cb.setStyle("-fx-text-fill:#ccc;"); + if (relPath.startsWith("Models/trees/")) { + solidCB.setSelected(false); + solidCB.setDisable(true); + } + + Label rndTitle = new Label("Zufallsskalierung (Multi-Place):"); + rndTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + Spinner rndMinSpin = buildScaleSpinner(meta.randomScaleMin()); + Spinner rndMaxSpin = buildScaleSpinner(meta.randomScaleMax()); + + GridPane rndGrid = new GridPane(); + rndGrid.setHgap(6); rndGrid.setVgap(4); + addLabeledRow(rndGrid, 0, "Min:", rndMinSpin); + addLabeledRow(rndGrid, 1, "Max:", rndMaxSpin); + + Label lodTitle = new Label("Level of Detail"); + lodTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + // LOD 1 + modelEditorLod1GenBtn = new Button("LOD 1 generieren"); + modelEditorLod1GenBtn.setMaxWidth(Double.MAX_VALUE); + boolean lod1Exists = ctx.input().modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || ctx.input().modelImportHasLod1; + modelEditorLod1GenBtn.setDisable(lod1Exists); + modelEditorLod1GenBtn.setOnAction(e -> showLodGenDialog(1, 50)); + + modelEditorLod1StatusLabel = new Label("–"); + modelEditorLod1StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); + modelEditorLod1StatusLabel.setWrapText(true); + + modelEditorLod1Label = new Label(modelEditorLod1Path.isBlank() ? "(kein Pfad)" : modelEditorLod1Path); + modelEditorLod1Label.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); + modelEditorLod1Label.setWrapText(true); + + Button lod1ClearBtn = new Button("✕"); + lod1ClearBtn.setStyle("-fx-text-fill:#c0392b;"); + lod1ClearBtn.setOnAction(e -> { + if (ctx.input().modelEditorHasEmbeddedLod1) { + ctx.input().modelEditorRemoveEmbeddedLod = 1; + } + if (ctx.input().modelImportHasLod1) { + ctx.input().modelImportClearLodLevel = 1; + if (modelEditorLod1StatusLabel != null) modelEditorLod1StatusLabel.setText("–"); + if (modelEditorLod2StatusLabel != null) modelEditorLod2StatusLabel.setText("–"); + } + modelEditorLod1Path = ""; + ctx.input().modelEditorLod1Path = ""; + modelEditorLod1Label.setText("(kein Pfad)"); + modelEditorLod2Path = ""; + ctx.input().modelEditorLod2Path = ""; + if (modelEditorLod2Label != null) modelEditorLod2Label.setText("(kein Pfad)"); + if (ctx.input().modelEditorLodPreview == 1 || ctx.input().modelEditorLodPreview == 2) { + ctx.input().modelEditorLodPreview = 0; + ctx.input().modelEditorLodChanged = true; + } + }); + + HBox lod1Row = new HBox(4, modelEditorLod1GenBtn, lod1ClearBtn); + HBox.setHgrow(modelEditorLod1GenBtn, Priority.ALWAYS); + + // LOD 2 + modelEditorLod2GenBtn = new Button("LOD 2 generieren"); + modelEditorLod2GenBtn.setMaxWidth(Double.MAX_VALUE); + boolean lod2Exists = ctx.input().modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || ctx.input().modelImportHasLod2; + modelEditorLod2GenBtn.setDisable(!lod1Exists || lod2Exists); + modelEditorLod2GenBtn.setOnAction(e -> showLodGenDialog(2, 85)); + + modelEditorLod2StatusLabel = new Label("–"); + modelEditorLod2StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); + modelEditorLod2StatusLabel.setWrapText(true); + + modelEditorLod2Label = new Label(modelEditorLod2Path.isBlank() ? "(kein Pfad)" : modelEditorLod2Path); + modelEditorLod2Label.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); + modelEditorLod2Label.setWrapText(true); + + Button lod2ClearBtn = new Button("✕"); + lod2ClearBtn.setStyle("-fx-text-fill:#c0392b;"); + lod2ClearBtn.setOnAction(e -> { + if (ctx.input().modelEditorHasEmbeddedLod2) { + ctx.input().modelEditorRemoveEmbeddedLod = 2; + } + if (ctx.input().modelImportHasLod2) { + ctx.input().modelImportClearLodLevel = 2; + if (modelEditorLod2StatusLabel != null) modelEditorLod2StatusLabel.setText("–"); + } + modelEditorLod2Path = ""; + ctx.input().modelEditorLod2Path = ""; + modelEditorLod2Label.setText("(kein Pfad)"); + if (ctx.input().modelEditorLodPreview == 2) { + ctx.input().modelEditorLodPreview = 0; + ctx.input().modelEditorLodChanged = true; + } + }); + + HBox lod2Row = new HBox(4, modelEditorLod2GenBtn, lod2ClearBtn); + HBox.setHgrow(modelEditorLod2GenBtn, Priority.ALWAYS); + + // Anhänge: Lichter & Partikel + Label attachTitle = new Label("Anhänge: Lichter & Partikel"); + attachTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + Label lightSectionLbl = new Label("Lichtquellen:"); + lightSectionLbl.setStyle("-fx-text-fill:#aaa;"); + + modelEditorLights.clear(); + modelEditorLights.addAll(meta.attachedLights()); + + modelEditorLightBox = new VBox(4); + rebuildLightBox(); + + Button addLightBtn = new Button("+ Lichtquelle"); + addLightBtn.setMaxWidth(Double.MAX_VALUE); + addLightBtn.setStyle("-fx-background-color:#4a4a2e;"); + addLightBtn.setOnAction(ev -> { + modelEditorLights.add(new de.blight.common.ModelMeta.AttachedLight( + 0f, 1f, 0f, 1f, 0.85f, 0.5f, 2f, 8f)); + rebuildLightBox(); + pushAttachmentsToJme(); + }); + + Label emitterSectionLbl = new Label("Partikel-Emitter:"); + emitterSectionLbl.setStyle("-fx-text-fill:#aaa;"); + + modelEditorEmitters.clear(); + modelEditorEmitters.addAll(meta.attachedEmitters()); + + modelEditorEmitterBox = new VBox(4); + rebuildEmitterBox(); + + Button addEmitterBtn = new Button("+ Partikel-Emitter"); + addEmitterBtn.setMaxWidth(Double.MAX_VALUE); + addEmitterBtn.setStyle("-fx-background-color:#4a2e2e;"); + addEmitterBtn.setOnAction(ev -> { + modelEditorEmitters.add(new de.blight.common.ModelMeta.AttachedEmitter( + 0f, 0.5f, 0f, 0)); + rebuildEmitterBox(); + pushAttachmentsToJme(); + }); + + pushAttachmentsToJme(); + + // Modell-Kategorie + Label catTitle = new Label("Kategorie:"); + catTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + modelEditorCategoryCB = new ComboBox<>(); + modelEditorCategoryCB.getItems().addAll(de.blight.common.ModelCategory.values()); + modelEditorCategoryCB.setValue(meta.modelCategory() != null ? meta.modelCategory() : de.blight.common.ModelCategory.NONE); + modelEditorCategoryCB.setMaxWidth(Double.MAX_VALUE); + modelEditorCategoryCB.setConverter(new javafx.util.StringConverter<>() { + @Override public String toString(de.blight.common.ModelCategory c) { return c == null ? "" : c.getLabel(); } + @Override public de.blight.common.ModelCategory fromString(String s) { return de.blight.common.ModelCategory.fromString(s); } + }); + + // Interaktivität + Label interactTitle = new Label("Interaktivität:"); + interactTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + modelEditorInteractableCB = new ComboBox<>(); + modelEditorInteractableCB.getItems().addAll(de.blight.common.model.InteractableType.values()); + modelEditorInteractableCB.setValue(meta.interactableType()); + modelEditorInteractableCB.setMaxWidth(Double.MAX_VALUE); + modelEditorInteractableCB.setConverter(new javafx.util.StringConverter<>() { + @Override public String toString(de.blight.common.model.InteractableType t) { + return t == null ? "" : t.getLabel(); + } + @Override public de.blight.common.model.InteractableType fromString(String s) { + return de.blight.common.model.InteractableType.fromString(s); + } + }); + + ctx.input().modelInteractableOffsetX = meta.interactableOffsetX(); + ctx.input().modelInteractableOffsetY = meta.interactableOffsetY(); + ctx.input().modelInteractableOffsetZ = meta.interactableOffsetZ(); + ctx.input().modelInteractableRotY = meta.interactableRotY(); + ctx.input().modelInteractableActive = meta.interactableType() == de.blight.common.model.InteractableType.BED + || meta.interactableType() == de.blight.common.model.InteractableType.BENCH; + ctx.input().modelInteractableOffsetChanged = true; + + Label restPosHint = new Label("Klicke auf das Modell um den Ruhepunkt zu setzen:"); + restPosHint.setStyle("-fx-text-fill:#aaa; -fx-font-size:10;"); + restPosHint.setWrapText(true); + + Button setRestBtn = new Button("⊕ Im Modell klicken"); + setRestBtn.setMaxWidth(Double.MAX_VALUE); + setRestBtn.setStyle("-fx-background-color:#1a5276; -fx-text-fill:#fff;"); + setRestBtn.setOnAction(ev -> { + ctx.input().activeLayer = SharedInput.LAYER_MODEL_INTERACTABLE; + ctx.setStatus().accept("Klicke auf das Modell um den Ruhepunkt zu setzen"); + }); + + Label posLabel = new Label("Position (Modell-Koordinaten):"); + posLabel.setStyle("-fx-text-fill:#aaa; -fx-font-size:10;"); + + modelEditorInteractableXSpin = new Spinner<>( + new SpinnerValueFactory.DoubleSpinnerValueFactory(-5.0, 5.0, + Math.round(meta.interactableOffsetX() * 100.0) / 100.0, 0.05)); + modelEditorInteractableXSpin.setEditable(true); + modelEditorInteractableXSpin.setMaxWidth(Double.MAX_VALUE); + modelEditorInteractableXSpin.valueProperty().addListener((obs, ov, nv) -> { + if (updatingInteractableSpinnersFromJme) return; + ctx.input().modelInteractableOffsetX = nv.floatValue(); + ctx.input().modelInteractableOffsetChanged = true; + }); + + modelEditorInteractableYSpin = new Spinner<>( + new SpinnerValueFactory.DoubleSpinnerValueFactory(-1.0, 4.0, + Math.round(meta.interactableOffsetY() * 100.0) / 100.0, 0.05)); + modelEditorInteractableYSpin.setEditable(true); + modelEditorInteractableYSpin.setMaxWidth(Double.MAX_VALUE); + modelEditorInteractableYSpin.valueProperty().addListener((obs, ov, nv) -> { + if (updatingInteractableSpinnersFromJme) return; + ctx.input().modelInteractableOffsetY = nv.floatValue(); + ctx.input().modelInteractableOffsetChanged = true; + }); + + modelEditorInteractableZSpin = new Spinner<>( + new SpinnerValueFactory.DoubleSpinnerValueFactory(-5.0, 5.0, + Math.round(meta.interactableOffsetZ() * 100.0) / 100.0, 0.05)); + modelEditorInteractableZSpin.setEditable(true); + modelEditorInteractableZSpin.setMaxWidth(Double.MAX_VALUE); + modelEditorInteractableZSpin.valueProperty().addListener((obs, ov, nv) -> { + if (updatingInteractableSpinnersFromJme) return; + ctx.input().modelInteractableOffsetZ = nv.floatValue(); + ctx.input().modelInteractableOffsetChanged = true; + }); + + GridPane posGrid = new GridPane(); + posGrid.setHgap(4); posGrid.setVgap(2); + javafx.scene.layout.ColumnConstraints posCC0 = new javafx.scene.layout.ColumnConstraints(); + posCC0.setMinWidth(20); posCC0.setHgrow(Priority.NEVER); + javafx.scene.layout.ColumnConstraints posCC1 = new javafx.scene.layout.ColumnConstraints(); + posCC1.setHgrow(Priority.ALWAYS); posCC1.setFillWidth(true); + posGrid.getColumnConstraints().addAll(posCC0, posCC1); + posGrid.add(new Label("X:"), 0, 0); posGrid.add(modelEditorInteractableXSpin, 1, 0); + posGrid.add(new Label("Y:"), 0, 1); posGrid.add(modelEditorInteractableYSpin, 1, 1); + posGrid.add(new Label("Z:"), 0, 2); posGrid.add(modelEditorInteractableZSpin, 1, 2); + posGrid.getChildren().stream() + .filter(n -> n instanceof Label) + .forEach(n -> ((Label) n).setStyle("-fx-text-fill:#aaa;")); + + Label rotLabel = new Label("Blickrichtung (°):"); + rotLabel.setStyle("-fx-text-fill:#aaa;"); + + double initDeg = Math.toDegrees(meta.interactableRotY()); + Slider rotSlider = new Slider(0, 360, initDeg); + rotSlider.setShowTickMarks(true); + rotSlider.setMajorTickUnit(90); + rotSlider.setPrefWidth(150); + + TextField rotField = new TextField(String.format("%.1f", initDeg)); + rotField.setPrefWidth(60); + rotField.setStyle("-fx-background-color:#333; -fx-text-fill:#eee;"); + + boolean[] syncingRot = {false}; + + rotSlider.valueProperty().addListener((obs, ov, nv) -> { + if (syncingRot[0]) return; + syncingRot[0] = true; + rotField.setText(String.format("%.1f", nv.doubleValue())); + syncingRot[0] = false; + ctx.input().modelInteractableRotY = (float) Math.toRadians(nv.doubleValue()); + ctx.input().modelInteractableOffsetChanged = true; + }); + + Runnable applyRotField = () -> { + try { + double deg = Double.parseDouble(rotField.getText().replace(',', '.')); + deg = ((deg % 360) + 360) % 360; + if (syncingRot[0]) return; + syncingRot[0] = true; + rotSlider.setValue(deg); + syncingRot[0] = false; + ctx.input().modelInteractableRotY = (float) Math.toRadians(deg); + ctx.input().modelInteractableOffsetChanged = true; + } catch (NumberFormatException ignored) {} + }; + rotField.setOnAction(e -> applyRotField.run()); + rotField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) applyRotField.run(); }); + + HBox rotRow = new HBox(6, rotSlider, rotField); + rotRow.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + + VBox restPointBox = new VBox(4, + restPosHint, setRestBtn, posLabel, posGrid, + rotLabel, rotRow); + restPointBox.setStyle("-fx-padding: 4 0 0 0;"); + + boolean isBedOrBench = meta.interactableType() == de.blight.common.model.InteractableType.BED + || meta.interactableType() == de.blight.common.model.InteractableType.BENCH; + restPointBox.setVisible(isBedOrBench); + restPointBox.setManaged(isBedOrBench); + + modelEditorInteractableCB.valueProperty().addListener((obs, ov, nv) -> { + boolean show = nv == de.blight.common.model.InteractableType.BED + || nv == de.blight.common.model.InteractableType.BENCH; + restPointBox.setVisible(show); + restPointBox.setManaged(show); + ctx.input().modelInteractableActive = show; + ctx.input().modelInteractableOffsetChanged = true; + }); + + // Fußgeräusch-Untergrund + Label footstepTitle = new Label("Fußgeräusch-Untergrund:"); + footstepTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + modelEditorFootstepSurfaceCB = new ComboBox<>(); + modelEditorFootstepSurfaceCB.getItems().add(""); + for (de.blight.game.audio.SurfaceType st : de.blight.game.audio.SurfaceType.values()) { + modelEditorFootstepSurfaceCB.getItems().add(st.name()); + } + String currentSurface = meta.footstepSurface() != null ? meta.footstepSurface() : ""; + modelEditorFootstepSurfaceCB.setValue( + modelEditorFootstepSurfaceCB.getItems().contains(currentSurface) ? currentSurface : ""); + modelEditorFootstepSurfaceCB.setMaxWidth(Double.MAX_VALUE); + modelEditorFootstepSurfaceCB.setConverter(new javafx.util.StringConverter<>() { + @Override public String toString(String s) { + return (s == null || s.isEmpty()) ? "(aus Textur)" : s; + } + @Override public String fromString(String s) { return s; } + }); + + // Buttons + Button saveBtn = new Button("💾 Speichern"); + saveBtn.setMaxWidth(Double.MAX_VALUE); + saveBtn.setStyle("-fx-background-color:#2e7d32; -fx-text-fill:#fff; -fx-font-weight:bold;"); + + Button placeBtn = new Button("⊕ Platzieren"); + placeBtn.setMaxWidth(Double.MAX_VALUE); + placeBtn.setStyle("-fx-background-color:#1565c0; -fx-text-fill:#fff;"); + + Button closeBtn = new Button("✕ Schließen"); + closeBtn.setMaxWidth(Double.MAX_VALUE); + closeBtn.setStyle("-fx-background-color:#555; -fx-text-fill:#ccc;"); + + saveBtn.setOnAction(e -> saveModelMeta( + relPath, absolutePath, + nameTF.getText().trim(), + catCB.getValue() != null ? catCB.getValue().trim() : "", + tagsTF.getText().trim(), + (float)(double) modelEditorSpinX.getValue(), + (float)(double) modelEditorSpinY.getValue(), + (float)(double) modelEditorSpinZ.getValue(), + modelEditorUniformCB.isSelected(), + (float)(double) pivotSpin.getValue(), + (float)(double) placeSpin.getValue(), + solidCB.isSelected(), + castCB.isSelected(), + receiveCB.isSelected(), + (float)(double) rndMinSpin.getValue(), + (float)(double) rndMaxSpin.getValue(), + modelEditorLod1Path, modelEditorLod2Path, + new java.util.ArrayList<>(modelEditorLights), + new java.util.ArrayList<>(modelEditorEmitters), + modelEditorInteractableCB != null + ? modelEditorInteractableCB.getValue() + : de.blight.common.model.InteractableType.NONE, + ctx.input().modelInteractableOffsetX, + ctx.input().modelInteractableOffsetY, + ctx.input().modelInteractableOffsetZ, + ctx.input().modelInteractableRotY, + modelEditorFootstepSurfaceCB != null + ? modelEditorFootstepSurfaceCB.getValue() + : "", + modelEditorCategoryCB != null + ? modelEditorCategoryCB.getValue() + : de.blight.common.ModelCategory.NONE)); + + placeBtn.setOnAction(e -> { + ctx.input().modelEditorCloseRequest = true; + ctx.input().pendingModelPath = modelEditorCurrentPath; + ctx.input().activeLayer = SharedInput.LAYER_OBJECTS; + if (objectController != null) objectController.showPlacePanel(); + }); + + closeBtn.setOnAction(e -> { + ctx.input().modelEditorCloseRequest = true; + ctx.input().activeLayer = 0; + ctx.switchToWorld().run(); + }); + + panel.getChildren().addAll( + title, + new Separator(), + nameLabel, nameTF, + catLabel, catCB, + tagsLabel, tagsTF, + new Separator(), + scaleTitle, modelEditorUniformCB, scaleGrid, + modelEditorDimLabel, modelEditorPolyLabel, + new Separator(), + offsetTitle, offsetGrid, + new Separator(), + defaultsTitle, solidCB, castCB, receiveCB, + new Separator(), + rndTitle, rndGrid, + new Separator(), + lodTitle, + lod1Row, modelEditorLod1StatusLabel, modelEditorLod1Label, + lod2Row, modelEditorLod2StatusLabel, modelEditorLod2Label, + new Separator(), + attachTitle, + lightSectionLbl, modelEditorLightBox, addLightBtn, + emitterSectionLbl, modelEditorEmitterBox, addEmitterBtn, + new Separator(), + catTitle, modelEditorCategoryCB, + new Separator(), + interactTitle, modelEditorInteractableCB, restPointBox, + new Separator(), + footstepTitle, modelEditorFootstepSurfaceCB, + new Separator(), + saveBtn, placeBtn, closeBtn + ); + return panel; + } + + // ── Modell-Import ───────────────────────────────────────────────────────── + + private void archiveOriginal(File source, String assetType) { + Path dest = de.blight.editor.ProjectRoot.PATH.resolve("assets").resolve("imported").resolve(assetType); + try { + Files.createDirectories(dest); + Files.copy(source.toPath(), dest.resolve(source.getName()), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + log.info("[Import] Archiviert: {}/{}", assetType, source.getName()); + } catch (IOException ex) { + log.warn("[Import] Archivierung fehlgeschlagen ({}/{}): {}", + assetType, source.getName(), ex.getMessage()); + } + } + + private void startImportFile(File file) { + archiveOriginal(file, "models"); + String name = file.getName(); + String baseName = name.replaceFirst("\\.[^.]+$", ""); + Path destDir = ctx.assetRoot().resolve("Models").resolve("imported"); + try { Files.createDirectories(destDir); } + catch (IOException ex) { ctx.setStatus().accept("Fehler: " + ex.getMessage()); return; } + + boolean isJ3o = name.toLowerCase().endsWith(".j3o"); + Path destFile = destDir.resolve(name); + Path destJ3o = destDir.resolve(baseName + ".j3o"); + + try { + Files.copy(file.toPath(), destFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } catch (IOException ex) { + ctx.setStatus().accept("Fehler beim Kopieren: " + ex.getMessage()); + return; + } + + if (isJ3o) { + String relPath = ctx.assetRoot().relativize(destJ3o).toString().replace('\\', '/'); + activateModelImport(relPath, baseName); + } else { + String assetPath = ctx.assetRoot().relativize(destFile).toString().replace('\\', '/'); + boolean keepCtrls = name.matches(".*\\.(gltf|glb)"); + ctx.input().modelConvertQueue.offer( + new SharedInput.ModelConvertRequest(assetPath, destJ3o, destFile, keepCtrls, + 0f, false, true)); + ctx.setStatus().accept("Konvertiere " + name + " → .j3o …"); + String finalRel = ctx.assetRoot().relativize(destJ3o).toString().replace('\\', '/'); + new Thread(() -> { + for (int i = 0; i < 300; i++) { + try { Thread.sleep(100); } catch (InterruptedException ignored) {} + if (Files.exists(destJ3o)) break; + } + Platform.runLater(() -> activateModelImport(finalRel, baseName)); + }, "import-wait").start(); + } + } + + private void activateModelImport(String relPath, String suggestedName) { + modelImportCurrentPath = relPath; + modelImportLod1StatusLabel = null; + modelImportLod2StatusLabel = null; + modelImportLod1ClearBtn = null; + modelImportLod2ClearBtn = null; + modelImportLod2GenBtn = null; + modelImportExportStatusLabel = null; + ctx.input().modelImportHasLod1 = false; + ctx.input().modelImportHasLod2 = false; + ctx.input().modelImportExportName = null; + ctx.input().modelImportExportStatus = null; + ctx.input().modelImportDummyVisible = false; + ctx.input().modelImportDummyX = 0f; + ctx.input().modelImportDummyY = 0f; + ctx.input().modelImportDummyZ = 0f; + ctx.input().modelImportLod1PreviewNode.set(null); + ctx.input().modelImportLod2PreviewNode.set(null); + if (importLod1Btn != null) { importLod1Btn.setDisable(true); importLod1Btn.setSelected(false); } + if (importLod2Btn != null) { importLod2Btn.setDisable(true); importLod2Btn.setSelected(false); } + + ctx.setMainToolbar().accept(buildModelImportToolBar()); + ctx.input().activeLayer = SharedInput.LAYER_MODEL_EDITOR; + ctx.setCenterView().accept(ctx.viewportSupplier().get()); + openAssetOverlay.run(); + + ctx.input().modelEditorScaleX = 1f; + ctx.input().modelEditorScaleY = 1f; + ctx.input().modelEditorScaleZ = 1f; + ctx.input().modelEditorPivotY = 0f; + ctx.input().modelEditorOpenPath = relPath; + ctx.input().modelImportIntermediatePath = relPath; + + ctx.setRightPanel().accept(buildModelImportPanel(relPath, suggestedName)); + ctx.setStatus().accept("Modell-Import: " + relPath); + } + + private ToolBar buildModelImportToolBar() { + Button backBtn = new Button("← Welteneditor"); + backBtn.setOnAction(e -> { + ctx.input().modelEditorCloseRequest = true; + ctx.input().activeLayer = 0; + ctx.switchToWorld().run(); + }); + Label label = new Label("Modell importieren"); + label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); + + importLod0Btn = new ToggleButton("LOD 0"); + ToggleButton lod0Btn = importLod0Btn; + importLod1Btn = new ToggleButton("LOD 1"); + importLod2Btn = new ToggleButton("LOD 2"); + ToggleGroup lodGroup = new ToggleGroup(); + lod0Btn.setToggleGroup(lodGroup); importLod1Btn.setToggleGroup(lodGroup); importLod2Btn.setToggleGroup(lodGroup); + lod0Btn.setSelected(true); + String lodStyle = "-fx-font-size:11; -fx-padding:2 8 2 8;"; + lod0Btn.setStyle(lodStyle); importLod1Btn.setStyle(lodStyle); importLod2Btn.setStyle(lodStyle); + lod0Btn.setOnAction(e -> { ctx.input().modelEditorLodPreview = 0; ctx.input().modelEditorLodChanged = true; }); + importLod1Btn.setOnAction(e -> { ctx.input().modelEditorLodPreview = 1; ctx.input().modelEditorLodChanged = true; }); + importLod2Btn.setOnAction(e -> { ctx.input().modelEditorLodPreview = 2; ctx.input().modelEditorLodChanged = true; }); + importLod1Btn.setDisable(true); + importLod2Btn.setDisable(true); + + ToggleButton cylBtn = new ToggleButton("⇕ 2m"); + cylBtn.setStyle("-fx-font-size:11; -fx-padding:2 8 2 8;"); + cylBtn.setTooltip(new Tooltip("Vergleichsfigur (2m Zylinder) ein-/ausblenden")); + cylBtn.setSelected(ctx.input().modelImportDummyVisible); + cylBtn.setOnAction(e -> ctx.input().modelImportDummyVisible = cylBtn.isSelected()); + + String spnStyle = "-fx-pref-width:64; -fx-font-size:11;"; + Spinner spnX = new Spinner<>(-50.0, 50.0, 0.0, 0.25); + Spinner spnY = new Spinner<>(-20.0, 20.0, 0.0, 0.25); + Spinner spnZ = new Spinner<>(-50.0, 50.0, 0.0, 0.25); + spnX.setStyle(spnStyle); spnY.setStyle(spnStyle); spnZ.setStyle(spnStyle); + spnX.setEditable(true); spnY.setEditable(true); spnZ.setEditable(true); + spnX.setTooltip(new Tooltip("Vergleichsfigur X-Offset")); + spnY.setTooltip(new Tooltip("Vergleichsfigur Y-Offset")); + spnZ.setTooltip(new Tooltip("Vergleichsfigur Z-Offset")); + spnX.valueProperty().addListener((ob, o, v) -> ctx.input().modelImportDummyX = v.floatValue()); + spnY.valueProperty().addListener((ob, o, v) -> ctx.input().modelImportDummyY = v.floatValue()); + spnZ.valueProperty().addListener((ob, o, v) -> ctx.input().modelImportDummyZ = v.floatValue()); + + Label lblX = new Label("X"); lblX.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;"); + Label lblY = new Label("Y"); lblY.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;"); + Label lblZ = new Label("Z"); lblZ.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;"); + + ToolBar tb = new ToolBar(); + tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label, + new Separator(Orientation.VERTICAL), lod0Btn, importLod1Btn, importLod2Btn, + new Separator(Orientation.VERTICAL), cylBtn, + lblX, spnX, lblY, spnY, lblZ, spnZ); + return tb; + } + + private VBox buildModelImportPanel(String relPath, String suggestedName) { + VBox panel = new VBox(8); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(300); + panel.setStyle("-fx-background-color: #2a2a3e;"); + + Label title = new Label("Modell importieren"); + title.setStyle("-fx-font-weight:bold; -fx-font-size:14; -fx-text-fill:#ddd;"); + + Label scaleTitle = new Label("Skalierung:"); + scaleTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + modelEditorUniformCB = new CheckBox("Gleichmäßig skalieren (X=Y=Z)"); + modelEditorUniformCB.setSelected(true); + modelEditorUniformCB.setStyle("-fx-text-fill:#ccc;"); + + modelEditorSpinX = buildScaleSpinner(1.0); + modelEditorSpinY = buildScaleSpinner(1.0); + modelEditorSpinZ = buildScaleSpinner(1.0); + + GridPane scaleGrid = new GridPane(); + scaleGrid.setHgap(6); scaleGrid.setVgap(4); + addLabeledRow(scaleGrid, 0, "X:", modelEditorSpinX); + addLabeledRow(scaleGrid, 1, "Y:", modelEditorSpinY); + addLabeledRow(scaleGrid, 2, "Z:", modelEditorSpinZ); + + modelEditorSpinX.valueProperty().addListener((o, ov, nv) -> { + if (modelEditorSuppressListeners) return; + if (modelEditorUniformCB.isSelected()) { + modelEditorSuppressListeners = true; + modelEditorSpinY.getValueFactory().setValue(nv); + modelEditorSpinZ.getValueFactory().setValue(nv); + modelEditorSuppressListeners = false; + } + pushScaleToJme(); + }); + modelEditorSpinY.valueProperty().addListener((o, ov, nv) -> { + if (modelEditorSuppressListeners) return; + if (modelEditorUniformCB.isSelected()) { + modelEditorSuppressListeners = true; + modelEditorSpinX.getValueFactory().setValue(nv); + modelEditorSpinZ.getValueFactory().setValue(nv); + modelEditorSuppressListeners = false; + } + pushScaleToJme(); + }); + modelEditorSpinZ.valueProperty().addListener((o, ov, nv) -> { + if (modelEditorSuppressListeners) return; + if (modelEditorUniformCB.isSelected()) { + modelEditorSuppressListeners = true; + modelEditorSpinX.getValueFactory().setValue(nv); + modelEditorSpinY.getValueFactory().setValue(nv); + modelEditorSuppressListeners = false; + } + pushScaleToJme(); + }); + modelEditorUniformCB.setOnAction(e -> { + if (modelEditorUniformCB.isSelected()) { + double xVal = modelEditorSpinX.getValue(); + modelEditorSuppressListeners = true; + modelEditorSpinY.getValueFactory().setValue(xVal); + modelEditorSpinZ.getValueFactory().setValue(xVal); + modelEditorSuppressListeners = false; + pushScaleToJme(); + } + }); + + modelEditorDimLabel = new Label("B – × H – × T – m"); + modelEditorDimLabel.setStyle("-fx-text-fill:#8cf; -fx-font-family:monospace;"); + modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE); + modelEditorDimLabel.setWrapText(true); + + modelEditorPolyLabel = new Label("– Polygone"); + modelEditorPolyLabel.setStyle("-fx-text-fill:#aaa; -fx-font-family:monospace; -fx-font-size:11;"); + modelEditorPolyLabel.setMaxWidth(Double.MAX_VALUE); + + Label algoTitle = new Label("Reduktions-Algorithmus"); + algoTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + RadioButton algoBlightRB = new RadioButton("Blight (Edge Collapse, Dihedral-aware)"); + RadioButton algoJmeRB = new RadioButton("JME (Progressive Mesh)"); + algoBlightRB.setStyle("-fx-text-fill:#ccc;"); + algoJmeRB.setStyle("-fx-text-fill:#ccc;"); + ToggleGroup algoGroup = new ToggleGroup(); + algoBlightRB.setToggleGroup(algoGroup); + algoJmeRB.setToggleGroup(algoGroup); + algoBlightRB.setSelected(true); + algoBlightRB.setOnAction(e -> ctx.input().modelLodAlgorithm = "blight"); + algoJmeRB.setOnAction(e -> ctx.input().modelLodAlgorithm = "jme"); + + // LOD 1 + Label lod1Title = new Label("LOD 1"); + lod1Title.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + RadioButton lod1AutoRB = new RadioButton("Auto (Mesh-Reduktion)"); + RadioButton lod1FileRB = new RadioButton("Aus Datei…"); + lod1AutoRB.setStyle("-fx-text-fill:#ccc;"); + lod1FileRB.setStyle("-fx-text-fill:#ccc;"); + ToggleGroup lod1Group = new ToggleGroup(); + lod1AutoRB.setToggleGroup(lod1Group); + lod1FileRB.setToggleGroup(lod1Group); + lod1AutoRB.setSelected(true); + + Button lod1GenBtn = new Button("LOD 1 generieren"); + lod1GenBtn.setMaxWidth(Double.MAX_VALUE); + lod1GenBtn.setOnAction(e -> { + if (lod1AutoRB.isSelected()) { + ctx.input().modelLodGenRequest.set(new SharedInput.ModelLodGenRequest(1, "auto_mesh", null)); + } else { + FileChooser lfc = new FileChooser(); + lfc.setTitle("LOD 1 – j3o wählen"); + lfc.getExtensionFilters().add(new FileChooser.ExtensionFilter("JME3 Modell (*.j3o)", "*.j3o")); + File lFile = lfc.showOpenDialog(ctx.stageSupplier().get()); + if (lFile == null) return; + Path lodDir = ctx.assetRoot().resolve("Models").resolve("lods"); + try { + Files.createDirectories(lodDir); + Path dest = lodDir.resolve(lFile.getName()); + Files.copy(lFile.toPath(), dest, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + String assetPath = ctx.assetRoot().relativize(dest).toString().replace('\\', '/'); + ctx.input().modelLodGenRequest.set(new SharedInput.ModelLodGenRequest(1, "file", assetPath)); + } catch (IOException ex) { + ctx.setStatus().accept("LOD 1 Import fehlgeschlagen: " + ex.getMessage()); + } + } + }); + + modelImportLod1StatusLabel = new Label("(nicht gesetzt)"); + modelImportLod1StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); + modelImportLod1StatusLabel.setWrapText(true); + modelImportLod1ClearBtn = new Button("✕ LOD 1 entfernen"); + modelImportLod1ClearBtn.setStyle("-fx-text-fill:#c0392b;"); + modelImportLod1ClearBtn.setMaxWidth(Double.MAX_VALUE); + modelImportLod1ClearBtn.setDisable(true); + modelImportLod1ClearBtn.setOnAction(e -> { + ctx.input().modelImportClearLodLevel = 1; + modelImportLod1StatusLabel.setText("(nicht gesetzt)"); + if (modelImportLod2StatusLabel != null) modelImportLod2StatusLabel.setText("(nicht gesetzt)"); + }); + + // LOD 2 + Label lod2Title = new Label("LOD 2"); + lod2Title.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + + RadioButton lod2MeshRB = new RadioButton("Auto (Starke Mesh-Reduktion, ~15%)"); + RadioButton lod2FileRB = new RadioButton("Aus Datei…"); + for (RadioButton rb : new RadioButton[]{lod2MeshRB, lod2FileRB}) + rb.setStyle("-fx-text-fill:#ccc;"); + ToggleGroup lod2Group = new ToggleGroup(); + lod2MeshRB.setToggleGroup(lod2Group); + lod2FileRB.setToggleGroup(lod2Group); + lod2MeshRB.setSelected(true); + + modelImportLod2GenBtn = new Button("LOD 2 generieren"); + Button lod2GenBtn = modelImportLod2GenBtn; + lod2GenBtn.setMaxWidth(Double.MAX_VALUE); + lod2GenBtn.setOnAction(e -> { + if (lod2MeshRB.isSelected()) { + ctx.input().modelLodGenRequest.set(new SharedInput.ModelLodGenRequest(2, "auto_mesh_heavy", null)); + } else { + FileChooser lfc = new FileChooser(); + lfc.setTitle("LOD 2 – j3o wählen"); + lfc.getExtensionFilters().add(new FileChooser.ExtensionFilter("JME3 Modell (*.j3o)", "*.j3o")); + File lFile = lfc.showOpenDialog(ctx.stageSupplier().get()); + if (lFile == null) return; + Path lodDir = ctx.assetRoot().resolve("Models").resolve("lods"); + try { + Files.createDirectories(lodDir); + Path dest = lodDir.resolve(lFile.getName()); + Files.copy(lFile.toPath(), dest, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + String assetPath = ctx.assetRoot().relativize(dest).toString().replace('\\', '/'); + ctx.input().modelLodGenRequest.set(new SharedInput.ModelLodGenRequest(2, "file", assetPath)); + } catch (IOException ex) { + ctx.setStatus().accept("LOD 2 Import fehlgeschlagen: " + ex.getMessage()); + } + } + }); + + modelImportLod2StatusLabel = new Label("(nicht gesetzt)"); + modelImportLod2StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); + modelImportLod2StatusLabel.setWrapText(true); + modelImportLod2ClearBtn = new Button("✕ LOD 2 entfernen"); + modelImportLod2ClearBtn.setStyle("-fx-text-fill:#c0392b;"); + modelImportLod2ClearBtn.setMaxWidth(Double.MAX_VALUE); + modelImportLod2ClearBtn.setDisable(true); + modelImportLod2ClearBtn.setOnAction(e -> { + ctx.input().modelImportClearLodLevel = 2; + if (modelImportLod2StatusLabel != null) modelImportLod2StatusLabel.setText("(nicht gesetzt)"); + }); + + Label fileNameLabel = new Label("Dateiname:"); + fileNameLabel.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + TextField nameTF = new TextField(suggestedName); + nameTF.setStyle("-fx-background-color:#3a3a4e; -fx-text-fill:#eee;"); + nameTF.setPromptText("asset-name"); + + modelImportExportStatusLabel = new Label(""); + modelImportExportStatusLabel.setStyle("-fx-text-fill:#8cf; -fx-font-size:11;"); + modelImportExportStatusLabel.setWrapText(true); + + Button exportBtn = new Button("📦 Als Asset importieren"); + exportBtn.setMaxWidth(Double.MAX_VALUE); + exportBtn.setStyle("-fx-background-color:#2e7d32; -fx-text-fill:#fff; -fx-font-weight:bold;"); + exportBtn.setOnAction(e -> { + String n = nameTF.getText().trim(); + if (!n.isEmpty()) ctx.input().modelImportExportName = n; + }); + + Button cancelBtn = new Button("✕ Abbrechen"); + cancelBtn.setMaxWidth(Double.MAX_VALUE); + cancelBtn.setStyle("-fx-background-color:#555; -fx-text-fill:#ccc;"); + cancelBtn.setOnAction(e -> { + ctx.input().modelEditorCloseRequest = true; + ctx.input().activeLayer = 0; + ctx.switchToWorld().run(); + }); + + panel.getChildren().addAll( + title, + new Separator(), + fileNameLabel, nameTF, + new Separator(), + scaleTitle, modelEditorUniformCB, scaleGrid, modelEditorDimLabel, modelEditorPolyLabel, + new Separator(), + algoTitle, algoBlightRB, algoJmeRB, + new Separator(), + lod1Title, lod1AutoRB, lod1FileRB, lod1GenBtn, modelImportLod1StatusLabel, modelImportLod1ClearBtn, + new Separator(), + lod2Title, lod2MeshRB, lod2FileRB, lod2GenBtn, modelImportLod2StatusLabel, modelImportLod2ClearBtn, + new Separator(), + modelImportExportStatusLabel, exportBtn, cancelBtn + ); + return panel; + } + + // ── Spinner-Builder ─────────────────────────────────────────────────────── + + private Spinner buildScaleSpinner(double initial) { + SpinnerValueFactory.DoubleSpinnerValueFactory f = + new SpinnerValueFactory.DoubleSpinnerValueFactory(0.001, 1000.0, initial, 0.1); + f.setConverter(new javafx.util.converter.DoubleStringConverter() { + @Override public Double fromString(String s) { + try { return Double.parseDouble(s.replace(',', '.')); } + catch (Exception ex) { return initial; } + } + }); + Spinner s = new Spinner<>(f); + s.setEditable(true); + s.setMaxWidth(Double.MAX_VALUE); + s.setStyle("-fx-background-color:#3a3a4e;"); + return s; + } + + private Spinner buildOffsetSpinner(double initial) { + SpinnerValueFactory.DoubleSpinnerValueFactory f = + new SpinnerValueFactory.DoubleSpinnerValueFactory(-100.0, 100.0, initial, 0.1); + Spinner s = new Spinner<>(f); + s.setEditable(true); + s.setMaxWidth(Double.MAX_VALUE); + s.setStyle("-fx-background-color:#3a3a4e;"); + return s; + } + + private Spinner buildDistSpinner(double initial) { + SpinnerValueFactory.DoubleSpinnerValueFactory f = + new SpinnerValueFactory.DoubleSpinnerValueFactory(1.0, 2000.0, initial, 5.0); + f.setConverter(new javafx.util.converter.DoubleStringConverter() { + @Override public Double fromString(String s) { + try { return Double.parseDouble(s.replace(',', '.')); } + catch (Exception ex) { return initial; } + } + }); + Spinner s = new Spinner<>(f); + s.setEditable(true); + s.setMaxWidth(Double.MAX_VALUE); + s.setStyle("-fx-background-color:#3a3a4e;"); + return s; + } + + private void addLabeledRow(GridPane grid, int row, String labelText, javafx.scene.Node ctrl) { + if (grid.getColumnConstraints().isEmpty()) { + javafx.scene.layout.ColumnConstraints cc0 = new javafx.scene.layout.ColumnConstraints(); + cc0.setMinWidth(80); + cc0.setHgrow(Priority.NEVER); + javafx.scene.layout.ColumnConstraints cc1 = new javafx.scene.layout.ColumnConstraints(); + cc1.setHgrow(Priority.ALWAYS); + cc1.setFillWidth(true); + grid.getColumnConstraints().addAll(cc0, cc1); + } + Label l = new Label(labelText); + l.setStyle("-fx-text-fill:#aaa;"); + grid.add(l, 0, row); + grid.add(ctrl, 1, row); + } + + // ── Hilfs-Methoden ──────────────────────────────────────────────────────── + + private void pushScaleToJme() { + if (modelEditorSpinX == null) return; + ctx.input().modelEditorScaleX = (float)(double) modelEditorSpinX.getValue(); + ctx.input().modelEditorScaleY = (float)(double) modelEditorSpinY.getValue(); + ctx.input().modelEditorScaleZ = (float)(double) modelEditorSpinZ.getValue(); + ctx.input().modelEditorScaleChanged = true; + } + + private void updateModelEditorDimensions(float w, float h, float d, int polyCount) { + if (modelEditorDimLabel != null) + modelEditorDimLabel.setText(String.format("B %.2f × H %.2f × T %.2f m", w, h, d)); + if (modelEditorPolyLabel != null) + modelEditorPolyLabel.setText(String.format("%,d Polygone", polyCount).replace(',', '.')); + } + + private void saveModelMeta(String relPath, + Path absolutePath, + String name, String category, String tags, + float sx, float sy, float sz, boolean uniform, + float pivotY, float placeY, + boolean solid, boolean cast, boolean receive, + float rndMin, float rndMax, + String lod1Path, String lod2Path, + java.util.List lights, + java.util.List emitters, + de.blight.common.model.InteractableType interactableType, + float interactableOffsetX, float interactableOffsetY, + float interactableOffsetZ, float interactableRotY, + String footstepSurface, + de.blight.common.ModelCategory modelCategory) { + de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta( + name, category, tags, 1f, 1f, 1f, uniform, + pivotY, placeY, solid, cast, receive, rndMin, rndMax, + lod1Path, lod2Path, 30f, 80f, 120f, + lights, emitters, + interactableType != null ? interactableType : de.blight.common.model.InteractableType.NONE, + interactableOffsetX, interactableOffsetY, interactableOffsetZ, interactableRotY, + footstepSurface != null ? footstepSurface : "", + modelCategory != null ? modelCategory : de.blight.common.ModelCategory.NONE); + + if (absolutePath == null || !absolutePath.toFile().exists()) { + ctx.setStatus().accept("Fehler: Modell-Datei nicht gefunden – Meta nicht gespeichert"); + return; + } + + try { + de.blight.common.ModelMetaIO.save(meta, absolutePath); + } catch (IOException ex) { + ctx.setStatus().accept("Fehler beim Speichern der Meta-Datei: " + ex.getMessage()); + return; + } + + String statusMsg; + if (!category.isEmpty()) { + Path newDir = ctx.assetRoot().resolve("Models") + .resolve(Path.of(category.replace('/', java.io.File.separatorChar))); + Path newJ3o = newDir.resolve( + (name.isEmpty() ? absolutePath.getFileName().toString() + : name.replaceAll("[\\\\/:*?\"<>|]", "_") + ".j3o")); + + if (!newJ3o.equals(absolutePath)) { + try { + Files.createDirectories(newDir); + Path metaSrc = de.blight.common.ModelMetaIO.metaPath(absolutePath); + Files.move(absolutePath, newJ3o, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + if (Files.exists(metaSrc)) { + Files.move(metaSrc, + de.blight.common.ModelMetaIO.metaPath(newJ3o), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + modelEditorCurrentPath = ctx.assetRoot().relativize(newJ3o) + .toString().replace('\\', '/'); + statusMsg = "Gespeichert und verschoben → " + modelEditorCurrentPath; + ctx.setStatus().accept(statusMsg); + } catch (IOException ex) { + ctx.setStatus().accept("Meta gespeichert, Verschieben fehlgeschlagen: " + ex.getMessage()); + return; + } + } else { + statusMsg = "Meta gespeichert: " + relPath; + ctx.setStatus().accept(statusMsg); + } + } else { + statusMsg = "Meta gespeichert: " + relPath; + ctx.setStatus().accept(statusMsg); + } + + ctx.input().refreshAssets = true; + + try { + java.util.List placed = de.blight.common.PlacedModelIO.load(); + boolean anyChanged = false; + java.util.List updatedPlaced = new java.util.ArrayList<>(placed.size()); + for (de.blight.common.PlacedModel pm : placed) { + if (relPath.equals(pm.modelPath()) || relPath.equals(pm.meshFile())) { + updatedPlaced.add(new de.blight.common.PlacedModel( + pm.modelPath(), pm.x(), pm.y(), pm.z(), + pm.rotY(), pm.rotX(), pm.rotZ(), pm.scale(), + solid, + pm.texturePath(), pm.normalMapPath(), pm.materialPath(), + pm.meshFile(), pm.animClip(), pm.castShadow(), pm.receiveShadow(), + pm.lod1Path(), pm.lod2Path(), pm.lod1Distance(), pm.lod2Distance(), + pm.cullDistance(), pm.interactableType(), pm.interactableId(), + modelCategory != null ? modelCategory : de.blight.common.ModelCategory.NONE)); + anyChanged = true; + } else { + updatedPlaced.add(pm); + } + } + if (anyChanged) { + de.blight.common.PlacedModelIO.save(updatedPlaced); + ctx.setStatus().accept(statusMsg + " (Instanzen aktualisiert)"); + } + } catch (Exception ex) { + log.warn("[ModelMeta] Instanz-Update fehlgeschlagen: {}", ex.getMessage()); + } + + Path finalJ3o = category.isEmpty() ? absolutePath + : ctx.assetRoot().resolve("Models") + .resolve(Path.of(category.replace('/', java.io.File.separatorChar))) + .resolve(name.isEmpty() ? absolutePath.getFileName().toString() + : name.replaceAll("[\\\\/:*?\"<>|]", "_") + ".j3o"); + + if (sx != 1f || sy != 1f || sz != 1f) { + ctx.input().modelEditorScaleX = sx; + ctx.input().modelEditorScaleY = sy; + ctx.input().modelEditorScaleZ = sz; + ctx.input().modelEditorBakeScalePath = finalJ3o; + ctx.input().modelEditorBakeScaleRequest = true; + } + + if (ctx.input().modelImportHasLod1 || ctx.input().modelImportHasLod2) { + ctx.input().modelEditorEmbedLodsPath = finalJ3o; + ctx.input().modelEditorEmbedLodsRequest = true; + } + + ctx.input().modelEditorThumbnailRequest = finalJ3o; + } + + private void pushAttachmentsToJme() { + ctx.input().modelEditorAttachedLights = new java.util.ArrayList<>(modelEditorLights); + ctx.input().modelEditorAttachedEmitters = new java.util.ArrayList<>(modelEditorEmitters); + ctx.input().modelEditorAttachmentsChanged = true; + } + + private void rebuildLightBox() { + if (modelEditorLightBox == null) return; + modelEditorLightBox.getChildren().clear(); + for (int i = 0; i < modelEditorLights.size(); i++) { + final int idx = i; + de.blight.common.ModelMeta.AttachedLight al = modelEditorLights.get(i); + modelEditorLightBox.getChildren().add(buildLightEntry(idx, al)); + } + } + + private javafx.scene.Node buildLightEntry(int idx, + de.blight.common.ModelMeta.AttachedLight al) { + TitledPane tp = new TitledPane(); + tp.setText("Licht " + (idx + 1)); + tp.setExpanded(false); + tp.setStyle("-fx-text-fill:#ddd; -fx-background-color:#333;"); + + GridPane g = new GridPane(); + g.setHgap(6); g.setVgap(3); + g.setPadding(new Insets(4)); + + Spinner ox = buildOffsetSpinner(al.offsetX()); + Spinner oy = buildOffsetSpinner(al.offsetY()); + Spinner oz = buildOffsetSpinner(al.offsetZ()); + addLabeledRow(g, 0, "Offset X:", ox); + addLabeledRow(g, 1, "Offset Y:", oy); + addLabeledRow(g, 2, "Offset Z:", oz); + + javafx.scene.control.ColorPicker cp = new javafx.scene.control.ColorPicker( + javafx.scene.paint.Color.color( + Math.min(1, al.r()), Math.min(1, al.g()), Math.min(1, al.b()))); + cp.setMaxWidth(Double.MAX_VALUE); + g.add(new Label("Farbe:") {{ setStyle("-fx-text-fill:#aaa;"); }}, 0, 3); + g.add(cp, 1, 3); + + Spinner intSpin = new Spinner<>(0.0, 20.0, al.intensity(), 0.5); + intSpin.setEditable(true); + addLabeledRow(g, 4, "Intensität:", intSpin); + + Spinner radSpin = new Spinner<>(0.5, 50.0, al.radius(), 1.0); + radSpin.setEditable(true); + addLabeledRow(g, 5, "Radius:", radSpin); + + Runnable sync = () -> { + javafx.scene.paint.Color c = cp.getValue(); + modelEditorLights.set(idx, new de.blight.common.ModelMeta.AttachedLight( + ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue(), + (float) c.getRed(), (float) c.getGreen(), (float) c.getBlue(), + intSpin.getValue().floatValue(), radSpin.getValue().floatValue())); + pushAttachmentsToJme(); + }; + ox.valueProperty().addListener((o,ov,nv) -> sync.run()); + oy.valueProperty().addListener((o,ov,nv) -> sync.run()); + oz.valueProperty().addListener((o,ov,nv) -> sync.run()); + cp.valueProperty().addListener((o,ov,nv) -> sync.run()); + intSpin.valueProperty().addListener((o,ov,nv) -> sync.run()); + radSpin.valueProperty().addListener((o,ov,nv) -> sync.run()); + + Button del = new Button("✕ Entfernen"); + del.setStyle("-fx-background-color:#7a2222; -fx-text-fill:#fff;"); + del.setMaxWidth(Double.MAX_VALUE); + del.setOnAction(ev -> { + modelEditorLights.remove(idx); + rebuildLightBox(); + pushAttachmentsToJme(); + }); + + VBox content = new VBox(4, g, del); + content.setPadding(new Insets(2)); + tp.setContent(content); + return tp; + } + + private void rebuildEmitterBox() { + if (modelEditorEmitterBox == null) return; + modelEditorEmitterBox.getChildren().clear(); + for (int i = 0; i < modelEditorEmitters.size(); i++) { + final int idx = i; + de.blight.common.ModelMeta.AttachedEmitter ae = modelEditorEmitters.get(i); + modelEditorEmitterBox.getChildren().add(buildEmitterEntry(idx, ae)); + } + } + + private javafx.scene.Node buildEmitterEntry(int idx, + de.blight.common.ModelMeta.AttachedEmitter ae) { + TitledPane tp = new TitledPane(); + tp.setText("Emitter " + (idx + 1)); + tp.setExpanded(false); + tp.setStyle("-fx-text-fill:#ddd; -fx-background-color:#333;"); + + GridPane g = new GridPane(); + g.setHgap(6); g.setVgap(3); + g.setPadding(new Insets(4)); + + Spinner ox = buildOffsetSpinner(ae.offsetX()); + Spinner oy = buildOffsetSpinner(ae.offsetY()); + Spinner oz = buildOffsetSpinner(ae.offsetZ()); + addLabeledRow(g, 0, "Offset X:", ox); + addLabeledRow(g, 1, "Offset Y:", oy); + addLabeledRow(g, 2, "Offset Z:", oz); + + ComboBox presetCB = new ComboBox<>(); + presetCB.getItems().addAll("Feuer", "Rauch", "Funken"); + presetCB.getSelectionModel().select(Math.max(0, Math.min(2, ae.preset()))); + presetCB.setMaxWidth(Double.MAX_VALUE); + g.add(new Label("Typ:") {{ setStyle("-fx-text-fill:#aaa;"); }}, 0, 3); + g.add(presetCB, 1, 3); + + Runnable sync = () -> { + modelEditorEmitters.set(idx, new de.blight.common.ModelMeta.AttachedEmitter( + ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue(), + presetCB.getSelectionModel().getSelectedIndex())); + pushAttachmentsToJme(); + }; + ox.valueProperty().addListener((o,ov,nv) -> sync.run()); + oy.valueProperty().addListener((o,ov,nv) -> sync.run()); + oz.valueProperty().addListener((o,ov,nv) -> sync.run()); + presetCB.valueProperty().addListener((o,ov,nv) -> sync.run()); + + Button del = new Button("✕ Entfernen"); + del.setStyle("-fx-background-color:#7a2222; -fx-text-fill:#fff;"); + del.setMaxWidth(Double.MAX_VALUE); + del.setOnAction(ev -> { + modelEditorEmitters.remove(idx); + rebuildEmitterBox(); + pushAttachmentsToJme(); + }); + + VBox content = new VBox(4, g, del); + content.setPadding(new Insets(2)); + tp.setContent(content); + return tp; + } + + private void importLodFile(int slot) { + FileChooser fc = new FileChooser(); + fc.setTitle("LOD " + slot + " – j3o wählen"); + fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("JME3 Modell (*.j3o)", "*.j3o")); + File chosen = fc.showOpenDialog(ctx.stageSupplier().get()); + if (chosen == null) return; + Path lodDir = ctx.assetRoot().resolve("Models").resolve("lods"); + try { + Files.createDirectories(lodDir); + Path dest = lodDir.resolve(chosen.getName()); + Files.copy(chosen.toPath(), dest, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + String relPath = ctx.assetRoot().relativize(dest).toString().replace('\\', '/'); + if (slot == 1) { + modelEditorLod1Path = relPath; + ctx.input().modelEditorLod1Path = relPath; + if (modelEditorLod1Label != null) modelEditorLod1Label.setText(relPath); + } else { + modelEditorLod2Path = relPath; + ctx.input().modelEditorLod2Path = relPath; + if (modelEditorLod2Label != null) modelEditorLod2Label.setText(relPath); + } + ctx.setStatus().accept("LOD " + slot + " importiert: " + relPath); + ctx.input().refreshAssets = true; + } catch (IOException ex) { + ctx.setStatus().accept("Import fehlgeschlagen: " + ex.getMessage()); + } + } + + private void showLodGenDialog(int level, int defaultPct) { + javafx.scene.control.Dialog> dlg = new javafx.scene.control.Dialog<>(); + dlg.initOwner(ctx.stageSupplier().get()); + dlg.setTitle("LOD " + level + " generieren"); + dlg.setHeaderText("Methode und Reduktion für LOD " + level + " wählen"); + + RadioButton jmeRB = new RadioButton("JME (Progressive Mesh)"); + RadioButton blightRB = new RadioButton("Blight (Edge Collapse)"); + RadioButton impostorRB = new RadioButton("Impostor Image (Billboard)"); + jmeRB.setSelected(true); + ToggleGroup tg = new ToggleGroup(); + jmeRB.setToggleGroup(tg); + blightRB.setToggleGroup(tg); + impostorRB.setToggleGroup(tg); + + Label pctLabel = new Label("Reduktion (%):"); + Spinner pctSpinner = new Spinner<>( + new SpinnerValueFactory.IntegerSpinnerValueFactory(5, 95, defaultPct, 5)); + pctSpinner.setEditable(true); + pctSpinner.setPrefWidth(90); + + impostorRB.selectedProperty().addListener((obs, ov, nv) -> { + pctLabel.setDisable(nv); + pctSpinner.setDisable(nv); + }); + + VBox content = new VBox(8, + jmeRB, blightRB, impostorRB, + new Separator(), + new HBox(8, pctLabel, pctSpinner)); + content.setPadding(new Insets(12)); + dlg.getDialogPane().setContent(content); + dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + dlg.setResultConverter(bt -> { + if (bt != ButtonType.OK) return null; + String method = impostorRB.isSelected() ? "impostor" + : blightRB.isSelected() ? "blight_auto" + : "jme_auto"; + return new javafx.util.Pair<>(method, pctSpinner.getValue()); + }); + + dlg.showAndWait().ifPresent(result -> { + String method = result.getKey(); + float reductionPct = result.getValue() / 100f; + ctx.input().modelLodGenRequest.set( + new SharedInput.ModelLodGenRequest(level, method, null, reductionPct)); + }); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/object/ObjectController.java b/blight-editor/src/main/java/de/blight/editor/toolset/object/ObjectController.java new file mode 100644 index 0000000..46b0d4b --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/object/ObjectController.java @@ -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 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 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 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 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 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; + } +}