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 1e29256..192c824 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -136,6 +136,8 @@ public class EditorApp extends Application { private final ToolRegistry registry = new ToolRegistry(); private de.blight.editor.toolset.vegetation.VegetationController vegController; private de.blight.editor.toolset.eztree.EzTreeController ezTreeController; + private de.blight.editor.toolset.anim.AnimPreviewController animPreviewController; + private de.blight.editor.toolset.anim.AnimSetController animSetController; // Asset-Panel: Pfad-Map + DnD-Zustand private final Map, Path> itemPaths = new HashMap<>(); @@ -163,40 +165,9 @@ public class EditorApp extends Application { private Button objSaveBtn; private Button objDeleteBtn; - // Animations-Vorschau-Werkzeug-Zustand - private ImageView animPreviewView; - private ListView animClipListView; - private Label animPreviewStatusLabel; - private ComboBox animPreviewModelCombo; - private double animPrevDragX, animPrevDragY; // Cache: Menge aller j3o-Pfade die ein Skelett besitzen (null = Scan noch nicht abgeschlossen) private java.util.Set skeletalModelPaths = null; - /** Relativer Asset-Pfad des aktuell geladenen Modells (für .animset.json-Persistenz). */ - private String animCurrentModelPath = null; - - // AnimSet-Editor - private ListView animSetClipListView; - private ListView animSetActionListView; - private String animSetPendingPlayClip = null; - private ComboBox animSetModelCombo; - private String animSetCurrentName = null; - private Path animSetCurrentDir = null; - // Anim-Offset-Editor (innerhalb AnimSet-Editor) - private javafx.scene.control.ListView animSetOffsetListView; - private javafx.collections.ObservableList animSetOffsetList = - javafx.collections.FXCollections.observableArrayList(); - private java.util.Map - animSetOffsets = new java.util.LinkedHashMap<>(); - // Sub-Clip-Editor (innerhalb AnimSet-Editor) - private ListView animSetPartsListView; - private java.util.Map - editableSubClips = new java.util.LinkedHashMap<>(); - // Sequenz-Editor (innerhalb AnimSet-Editor) - private ListView animSetSequenceListView; - private java.util.Map> - editableSequences = new java.util.LinkedHashMap<>(); - // Character-Editor-Zustand private de.blight.editor.ui.DialogEditorView dialogEditorView; private de.blight.editor.ui.RoutineEditorView routineEditorView; @@ -458,10 +429,12 @@ public class EditorApp extends Application { input, jmeApp, () -> stage, this::setStatus, this::setCenterView, root::setRight, node -> topBar.getChildren().set(1, node), - () -> centerStack, ASSET_ROOT, this::switchToWorldEditor + () -> centerStack, ASSET_ROOT, this::switchToWorldEditor, registry ); - vegController = new de.blight.editor.toolset.vegetation.VegetationController(toolCtx); - ezTreeController = new de.blight.editor.toolset.eztree.EzTreeController(toolCtx); + vegController = new de.blight.editor.toolset.vegetation.VegetationController(toolCtx); + 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); Scene scene = new Scene(root, 1280, 760); java.net.URL cssUrl = getClass().getResource("/editor.css"); @@ -588,46 +561,16 @@ public class EditorApp extends Application { registry.onFrame(); - if (input.animPreviewResized) { - input.animPreviewResized = false; - if (animPreviewView != null) - animPreviewView.setImage(input.animPreviewImage); - } java.util.Set skeletal = input.skeletalPaths.getAndSet(null); if (skeletal != null) { skeletalModelPaths = skeletal; } - java.util.List newClips = input.animPreviewClips.getAndSet(null); - if (newClips != null && animClipListView != null) { - animClipListView.getItems().setAll(newClips); - if (!newClips.isEmpty()) animClipListView.getSelectionModel().selectFirst(); - } - // AnimSet-Editor: nach Clip-Load automatisch abspielen - if (newClips != null && animSetPendingPlayClip != null) { - input.animPreviewPlayClip = animSetPendingPlayClip; - animSetPendingPlayClip = null; - } - String loadedPath = input.animPreviewLoadedPath.getAndSet(null); - if (loadedPath != null) { - animCurrentModelPath = loadedPath; - } - String animStatus = input.animPreviewStatus; - if (animStatus != null) { - input.animPreviewStatus = null; - if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText(animStatus); - } if (input.animImportCompleted) { input.animImportCompleted = false; if (charactersNode != null) refreshCategoryNode(charactersNode, shouldPreserveExpansion()); if (animationsNode != null) refreshCategoryNode(animationsNode, shouldPreserveExpansion()); } - String animOp = input.animOpStatus; - if (animOp != null) { - input.animOpStatus = null; - if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText(animOp); - } - SharedInput.CharModelImportResult charImport = input.charModelImportResult.getAndSet(null); if (charImport != null) { if (charImport.error() != null) { @@ -647,8 +590,7 @@ public class EditorApp extends Application { } if (charModelCombo != null) charModelCombo.setValue(relPath); if (charactersNode != null) refreshCategoryNode(charactersNode, shouldPreserveExpansion()); - if (animSetModelCombo != null && !animSetModelCombo.getItems().contains(relPath)) - animSetModelCombo.getItems().add(relPath); + animSetController.onCharModelImported(relPath); if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Modell importiert: " + relPath); setStatus("Character-Modell importiert: " + relPath); @@ -1245,8 +1187,8 @@ public class EditorApp extends Application { importTexZipItem.setOnAction(e -> handleZipTextureImport(primaryStage)); importTexSetItem.setOnAction(e -> openTextureSetImport(primaryStage)); importAudioItem.setOnAction(e -> handleAudioImport(primaryStage)); - importAnimItem.setOnAction(e -> handleAnimationImport(primaryStage)); - importCharModelItem.setOnAction(e -> handleCharacterModelImport(primaryStage)); + importAnimItem.setOnAction(e -> animSetController.handleAnimationImport(primaryStage)); + importCharModelItem.setOnAction(e -> animSetController.handleCharacterModelImport(primaryStage)); importMenu.getItems().addAll(importModelLodItem, importTexItem, importTexZipItem, importTexSetItem, importAudioItem, importAnimItem, new javafx.scene.control.SeparatorMenuItem(), importCharModelItem); @@ -4460,8 +4402,8 @@ public class EditorApp extends Application { && relPath.endsWith(".j3o")) { if (!isSkeletal) { setStatus("Kein Skelett: " + relPath); return; } input.animPreviewLoadPath = relPath; - if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…"); - if (animClipListView != null) animClipListView.getItems().clear(); + animPreviewController.setStatusText("Lade…"); + animPreviewController.clearClipList(); return; } @@ -4489,7 +4431,7 @@ public class EditorApp extends Application { } else if ((cat == animationsNode || cat == charactersNode) && relPath.endsWith(".j3o") && isSkeletal) { switchToAnimPreview(); input.animPreviewLoadPath = relPath; - if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…"); + animPreviewController.setStatusText("Lade…"); } else if (cat == audioNode && relPath.endsWith(".ogg")) { try { new ProcessBuilder("xdg-open", p.toAbsolutePath().toString()) @@ -4499,7 +4441,7 @@ public class EditorApp extends Application { } setStatus("▶ " + relPath); } else if (relPath.endsWith(".animset.json")) { - openAnimSetEditor(relPath, p); + animSetController.openWithSet(relPath, p); registry.activate(animSetController); } else if (relPath.endsWith(".character")) { String charName = p.getFileName().toString().replace(".character", ""); switchToCharacterEditor(); @@ -4520,9 +4462,9 @@ public class EditorApp extends Application { miTex.setOnAction(ev -> handleTextureImport(importBtn.getScene().getWindow())); miTexZip.setOnAction(ev -> handleZipTextureImport(importBtn.getScene().getWindow())); miAudio.setOnAction(ev -> handleAudioImport(importBtn.getScene().getWindow())); - miAnim.setOnAction(ev -> handleAnimationImport(importBtn.getScene().getWindow())); + miAnim.setOnAction(ev -> animSetController.handleAnimationImport(importBtn.getScene().getWindow())); MenuItem miCharModel = new MenuItem("Character-Modell (FBX)…"); - miCharModel.setOnAction(ev -> handleCharacterModelImport(importBtn.getScene().getWindow())); + miCharModel.setOnAction(ev -> animSetController.handleCharacterModelImport(importBtn.getScene().getWindow())); popup.getItems().addAll(miModel, miTex, miTexZip, miAudio, miAnim, new javafx.scene.control.SeparatorMenuItem(), miCharModel); popup.show(importBtn, javafx.geometry.Side.TOP, 0, 0); @@ -5099,22 +5041,22 @@ public class EditorApp extends Application { if (item == animationsNode) { MenuItem importAnim = new MenuItem("⊕ Animation importieren…"); - importAnim.setOnAction(ev -> handleAnimationImport(ctx.getOwnerWindow())); + importAnim.setOnAction(ev -> animSetController.handleAnimationImport(ctx.getOwnerWindow())); ctx.getItems().add(importAnim); } if (item == charactersNode) { MenuItem importChar = new MenuItem("⊕ Character-Modell importieren (FBX)…"); - importChar.setOnAction(ev -> handleCharacterModelImport(ctx.getOwnerWindow())); + importChar.setOnAction(ev -> animSetController.handleCharacterModelImport(ctx.getOwnerWindow())); MenuItem importAnim = new MenuItem("⊕ Animation hinzufügen…"); - importAnim.setOnAction(ev -> handleAnimationImport(ctx.getOwnerWindow())); + importAnim.setOnAction(ev -> animSetController.handleAnimationImport(ctx.getOwnerWindow())); ctx.getItems().addAll(importChar, importAnim); } Path setsDir = ASSET_ROOT.resolve("Characters").resolve("sets"); if (dir != null && setsDir.equals(dir)) { MenuItem newSet = new MenuItem("➕ Neues Set…"); - newSet.setOnAction(ev -> createNewAnimSet()); + newSet.setOnAction(ev -> animSetController.createNewAnimSet()); ctx.getItems().add(newSet); } @@ -5173,8 +5115,8 @@ public class EditorApp extends Application { openAnim.setOnAction(ev -> { switchToAnimPreview(); input.animPreviewLoadPath = relPath; - if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…"); - if (animClipListView != null) animClipListView.getItems().clear(); + animPreviewController.setStatusText("Lade…"); + animPreviewController.clearClipList(); }); ctx.getItems().addAll(openAnim, new SeparatorMenuItem()); } @@ -5183,7 +5125,7 @@ public class EditorApp extends Application { if (pStr.endsWith(".animset.json")) { String animSetRel = ASSET_ROOT.relativize(p).toString().replace('\\', '/'); MenuItem editSet = new MenuItem("✏ AnimSet bearbeiten"); - editSet.setOnAction(ev -> openAnimSetEditor(animSetRel, p)); + editSet.setOnAction(ev -> { animSetController.openWithSet(animSetRel, p); registry.activate(animSetController); }); ctx.getItems().addAll(editSet, new SeparatorMenuItem()); } @@ -7276,6 +7218,19 @@ public class EditorApp extends Application { } } + 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(); @@ -9933,1505 +9888,26 @@ public class EditorApp extends Application { private void switchToAnimPreview() { ensureNotContentEditor(); currentTool = "animpreview"; - topBar.getChildren().set(1, buildAnimPreviewToolBar()); - - animPreviewView = new ImageView(input.animPreviewImage); - animPreviewView.setPreserveRatio(true); - animPreviewView.setSmooth(true); - - StackPane previewPane = new StackPane(animPreviewView); - previewPane.setStyle("-fx-background-color: #2e2e38;"); - - previewPane.widthProperty().addListener((obs, ov, nv) -> { - animPreviewView.setFitWidth(nv.doubleValue()); - input.animPreviewW = Math.max(64, nv.intValue()); - }); - previewPane.heightProperty().addListener((obs, ov, nv) -> { - animPreviewView.setFitHeight(nv.doubleValue()); - input.animPreviewH = Math.max(64, nv.intValue()); - }); - - previewPane.setOnMousePressed(e -> { - animPrevDragX = e.getSceneX(); - animPrevDragY = e.getSceneY(); - }); - previewPane.setOnMouseDragged(e -> { - double dx = e.getSceneX() - animPrevDragX; - double dy = e.getSceneY() - animPrevDragY; - animPrevDragX = e.getSceneX(); - animPrevDragY = e.getSceneY(); - input.animPreviewRotY += (float) (dx * 0.5); - input.animPreviewRotX = (float) Math.max(-80, Math.min(80, - input.animPreviewRotX - dy * 0.3)); - }); - previewPane.setOnScroll(e -> { - double factor = e.getDeltaY() > 0 ? 0.9 : 1.1; - input.animPreviewZoom = (float) Math.max(0.05, - Math.min(20.0, input.animPreviewZoom * factor)); - }); - - root.setCenter(previewPane); - root.setRight(buildAnimPreviewPanel()); + registry.activate(animPreviewController); } // ── AnimSet-Editor ──────────────────────────────────────────────────────── private void openCharacterEditor() { - Path setsDir = ASSET_ROOT.resolve("Characters").resolve("sets"); - java.util.List available = new java.util.ArrayList<>(); - if (java.nio.file.Files.isDirectory(setsDir)) { - try (var walk = java.nio.file.Files.walk(setsDir, 1)) { - walk.filter(p -> p.toString().endsWith(".animset.json")) - .map(p -> p.getFileName().toString().replaceFirst("\\.animset\\.json$", "")) - .sorted() - .forEach(available::add); - } catch (IOException ignored) {} - } - - // Dialog: vorhandenes AnimSet wählen oder neues anlegen - javafx.scene.control.Dialog dlg = new javafx.scene.control.Dialog<>(); - dlg.initOwner(primaryStage); - dlg.setTitle("Animationset Editor"); - dlg.setHeaderText("AnimSet auswählen oder neu anlegen"); - - ListView listView = new ListView<>(); - listView.getItems().addAll(available); - listView.setPrefHeight(200); - listView.setPrefWidth(320); - - ButtonType openType = new ButtonType("Öffnen", javafx.scene.control.ButtonBar.ButtonData.OK_DONE); - ButtonType newType = new ButtonType("Neu…", javafx.scene.control.ButtonBar.ButtonData.OTHER); - ButtonType cancelType = ButtonType.CANCEL; - dlg.getDialogPane().getButtonTypes().addAll(openType, newType, cancelType); - - javafx.scene.Node openNode = dlg.getDialogPane().lookupButton(openType); - openNode.setDisable(true); - listView.getSelectionModel().selectedItemProperty() - .addListener((obs, ov, nv) -> openNode.setDisable(nv == null)); - listView.setOnMouseClicked(e -> { - if (e.getClickCount() == 2 && listView.getSelectionModel().getSelectedItem() != null) { - dlg.setResult(listView.getSelectionModel().getSelectedItem()); - dlg.close(); - } - }); - - dlg.getDialogPane().setContent(listView); - dlg.setResultConverter(bt -> { - if (bt == openType) return listView.getSelectionModel().getSelectedItem(); - if (bt == newType) return "__NEW__"; - return null; - }); - - dlg.showAndWait().ifPresent(result -> { - if ("__NEW__".equals(result)) { - createNewAnimSet(); - } else if (result != null) { - Path setFile = setsDir.resolve(result + ".animset.json"); - String relPath = ASSET_ROOT.relativize(setFile).toString().replace('\\', '/'); - openAnimSetEditor(relPath, setFile); - } - }); + animSetController.openCharacterEditorDialog(); } private void openAnimSetEditor(String relPath, Path absPath) { - String setName = absPath.getFileName().toString().replaceFirst("\\.animset\\.json$", ""); - Path setDir = absPath.getParent(); - - de.blight.game.animation.AnimSet animSet; - try { - animSet = de.blight.game.animation.AnimSet.load(setDir, setName); - } catch (Exception ex) { - animSet = new de.blight.game.animation.AnimSet(); - setStatus("Fehler beim Laden: " + ex.getMessage()); - } - - animSetCurrentName = setName; - animSetCurrentDir = setDir; - currentTool = "animseteditor"; - topBar.getChildren().set(1, buildAnimSetToolBar(setName)); - - animPreviewView = new ImageView(input.animPreviewImage); - animPreviewView.setPreserveRatio(true); - animPreviewView.setSmooth(true); - StackPane previewPane = new StackPane(animPreviewView); - previewPane.setStyle("-fx-background-color: #2e2e38;"); - previewPane.widthProperty().addListener((obs, ov, nv) -> { - animPreviewView.setFitWidth(nv.doubleValue()); - input.animPreviewW = Math.max(64, nv.intValue()); - }); - previewPane.heightProperty().addListener((obs, ov, nv) -> { - animPreviewView.setFitHeight(nv.doubleValue()); - input.animPreviewH = Math.max(64, nv.intValue()); - }); - previewPane.setOnMousePressed(e -> { animPrevDragX = e.getSceneX(); animPrevDragY = e.getSceneY(); }); - previewPane.setOnMouseDragged(e -> { - double dx = e.getSceneX() - animPrevDragX, dy = e.getSceneY() - animPrevDragY; - animPrevDragX = e.getSceneX(); animPrevDragY = e.getSceneY(); - input.animPreviewRotY += (float)(dx * 0.5); - input.animPreviewRotX = (float) Math.max(-80, Math.min(80, input.animPreviewRotX - dy * 0.3)); - }); - previewPane.setOnScroll(e -> { - double factor = e.getDeltaY() > 0 ? 0.9 : 1.1; - input.animPreviewZoom = (float) Math.max(0.05, Math.min(20.0, input.animPreviewZoom * factor)); - }); - - root.setCenter(previewPane); - root.setRight(buildAnimSetPanel(setName, setDir, animSet)); - setStatus("AnimSet-Editor: " + setName); + animSetController.openWithSet(relPath, absPath); + registry.activate(animSetController); } - private ToolBar buildAnimSetToolBar(String setName) { - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - Label label = new Label("AnimSet: " + setName); - label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - Button newSetBtn = new Button("➕ Neues Set…"); - newSetBtn.setOnAction(e -> createNewAnimSet()); - ToolBar tb = new ToolBar(); - tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label, - new Separator(Orientation.VERTICAL), newSetBtn); - return tb; - } - - private VBox buildAnimSetPanel(String setName, Path setDir, - de.blight.game.animation.AnimSet animSet) { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - - // ── Modell-Auswahl für Vorschau ─────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Charakter-Modell"), new Separator()); - animSetModelCombo = new ComboBox<>(); - animSetModelCombo.setMaxWidth(Double.MAX_VALUE); - animSetModelCombo.setPromptText("j3o-Datei wählen…"); - { - Path base = ASSET_ROOT.resolve("Characters").resolve("Models"); - if (java.nio.file.Files.isDirectory(base)) { - try (var walk = java.nio.file.Files.walk(base)) { - walk.filter(p -> p.toString().endsWith(".j3o")) - .map(p -> ASSET_ROOT.relativize(p).toString().replace('\\', '/')) - .sorted() - .forEach(animSetModelCombo.getItems()::add); - } catch (IOException ignored) {} - } - } - // Gespeicherten Modell-Pfad vorauswählen - if (animSet.getPreviewModelPath() != null && !animSet.getPreviewModelPath().isBlank()) { - animSetModelCombo.setValue(animSet.getPreviewModelPath()); - } - Button loadModelBtn = new Button("Laden"); - loadModelBtn.setMaxWidth(Double.MAX_VALUE); - loadModelBtn.setOnAction(e -> { - String path = animSetModelCombo.getValue(); - if (path == null || path.isBlank()) return; - input.animPreviewLoadPath = path; - if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…"); - // Pfad im AnimSet merken und sofort speichern - animSet.setPreviewModelPath(path); - autoSaveAnimSet(); - }); - Button importCharBtn = new Button("Character-Modell importieren (FBX)…"); - importCharBtn.setMaxWidth(Double.MAX_VALUE); - importCharBtn.setOnAction(e -> handleCharacterModelImport(importCharBtn.getScene().getWindow())); - inner.getChildren().addAll(animSetModelCombo, loadModelBtn, importCharBtn); - - // Modell beim Öffnen automatisch laden, wenn Pfad bekannt - if (animSet.getPreviewModelPath() != null && !animSet.getPreviewModelPath().isBlank()) { - input.animPreviewLoadPath = animSet.getPreviewModelPath(); - if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…"); - } - - // ── Clips im Set ───────────────────────────────────────────────────── - inner.getChildren().addAll(new Separator(), sectionTitle("Clips im Set"), new Separator()); - - animSetClipListView = new ListView<>(); - animSetClipListView.getItems().addAll(animSet.getClips()); - animSetClipListView.setPrefHeight(180); - editableSubClips = new java.util.LinkedHashMap<>(animSet.getSubClips()); - editableSequences = new java.util.LinkedHashMap<>(animSet.getSequences()); - // Clip-Offsets direkt in SharedInput spiegeln damit AnimPreviewState sie kennt - input.animPreviewClipOffsets.clear(); - for (var en : animSet.getAnimOffsets().entrySet()) { - de.blight.game.animation.AnimOffset o = en.getValue(); - input.animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz}); - } - // Locomotion-Clips für snapRootBoneXZ ermitteln - java.util.Set locomotionActions = java.util.Set.of("WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT"); - input.animPreviewLocomotionClips.clear(); - animSet.getActionMap().forEach((action, clip) -> { - if (locomotionActions.contains(action)) input.animPreviewLocomotionClips.add(clip); - }); - - Path animRootDir = ASSET_ROOT.resolve("Characters"); - - Button addClipBtn = new Button("+ Hinzufügen…"); - Button removeClipBtn = new Button("- Entfernen"); - addClipBtn.setMaxWidth(Double.MAX_VALUE); - removeClipBtn.setMaxWidth(Double.MAX_VALUE); - removeClipBtn.setDisable(true); - - animSetClipListView.getSelectionModel().selectedItemProperty() - .addListener((obs, ov, nv) -> { - removeClipBtn.setDisable(nv == null); - // Offset des alten Clips sichern - if (ov != null && animSetOffsetList != null && !animSetOffsetList.isEmpty()) { - animSetOffsets.put(ov, animSetOffsetList.get(0)); - } else if (ov != null) { - animSetOffsets.remove(ov); - } - // Offset des neuen Clips laden - if (animSetOffsetList != null) { - animSetOffsetList.clear(); - if (nv != null) { - de.blight.game.animation.AnimOffset off = animSetOffsets.get(nv); - if (off != null) animSetOffsetList.setAll(off); - // Clip direkt in Vorschau abspielen - input.animPreviewPlayClip = nv; - } - } - // Offset-Bereich aktivieren/deaktivieren - if (animSetOffsetListView != null) animSetOffsetListView.setDisable(nv == null); - }); - - addClipBtn.setOnAction(e -> { - org.slf4j.Logger _log = org.slf4j.LoggerFactory.getLogger("AnimSetClipScan"); - _log.debug("[ClipScan] animRootDir={} exists={}", animRootDir, java.nio.file.Files.isDirectory(animRootDir)); - - // Scan bei jedem Klick frisch – gesamter animations/-Ordner, sets/ ausgenommen - java.util.List allClips = new java.util.ArrayList<>(); - if (java.nio.file.Files.isDirectory(animRootDir)) { - try (var walk = java.nio.file.Files.walk(animRootDir)) { - walk.filter(cp -> { - String name = cp.getFileName().toString(); - boolean isAnim = name.endsWith(".j3o") || name.endsWith(".glb") - || name.endsWith(".gltf") || name.endsWith(".fbx"); - java.nio.file.Path rel = animRootDir.relativize(cp); - boolean onlyClips = rel.startsWith("clips"); - _log.debug("[ClipScan] {} | isAnim={} onlyClips={}", cp, isAnim, onlyClips); - return isAnim && onlyClips; - }) - .map(cp -> cp.getFileName().toString().replaceFirst("\\.[^.]+$", "")) - .distinct() - .sorted() - .forEach(allClips::add); - } catch (IOException ex) { - _log.error("[ClipScan] Walk fehlgeschlagen", ex); - allClips.clear(); - } - } - _log.debug("[ClipScan] allClips={}", allClips); - _log.debug("[ClipScan] bereits im Set={}", animSetClipListView.getItems()); - - // Nur Clips anbieten, die noch nicht im Set enthalten sind - java.util.List notYetAdded = allClips.stream() - .filter(c -> !animSetClipListView.getItems().contains(c)) - .collect(java.util.stream.Collectors.toList()); - _log.debug("[ClipScan] notYetAdded={}", notYetAdded); - - if (notYetAdded.isEmpty()) { - javafx.scene.control.Alert info = new javafx.scene.control.Alert( - javafx.scene.control.Alert.AlertType.INFORMATION); - info.setTitle("Keine Clips verfügbar"); - info.setHeaderText(null); - info.setContentText("Alle verfügbaren Clips sind bereits im Set enthalten."); - info.showAndWait(); - return; - } - - ListView list = new ListView<>(); - list.getItems().addAll(notYetAdded); - list.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); - list.setPrefSize(460, 320); - list.setMinSize(460, 320); - list.getSelectionModel().selectFirst(); - - javafx.scene.control.Dialog> dlg = new javafx.scene.control.Dialog<>(); - dlg.initOwner(primaryStage); - dlg.setTitle("Animation(en) hinzufügen"); - dlg.setHeaderText("Verfügbare Clips (noch nicht im Set) — Mehrfachauswahl möglich:"); - dlg.getDialogPane().setContent(list); - dlg.getDialogPane().setPrefWidth(500); - javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType("Hinzufügen", - javafx.scene.control.ButtonBar.ButtonData.OK_DONE); - dlg.getDialogPane().getButtonTypes().addAll(ok, javafx.scene.control.ButtonType.CANCEL); - dlg.setResultConverter(bt -> bt == ok - ? new java.util.ArrayList<>(list.getSelectionModel().getSelectedItems()) - : null); - dlg.showAndWait().ifPresent(selected -> { - for (String clip : selected) { - if (!animSetClipListView.getItems().contains(clip)) { - animSetClipListView.getItems().add(clip); - autoSaveAnimSet(); - } - } - }); - }); - - removeClipBtn.setOnAction(e -> { - String sel = animSetClipListView.getSelectionModel().getSelectedItem(); - if (sel == null) return; - animSetClipListView.getItems().remove(sel); - if (animSetActionListView != null) - animSetActionListView.getItems().removeIf(it -> it.endsWith(" → " + sel)); - editableSubClips.entrySet().removeIf(en -> sel.equals(en.getValue().source)); - if (animSetPartsListView != null) animSetPartsListView.getItems().clear(); - autoSaveAnimSet(); - }); - - HBox clipBtns = new HBox(6, addClipBtn, removeClipBtn); - HBox.setHgrow(addClipBtn, Priority.ALWAYS); - HBox.setHgrow(removeClipBtn, Priority.ALWAYS); - - inner.getChildren().addAll(animSetClipListView, clipBtns); - - // ── Clip-Teile (Sub-Clips) ──────────────────────────────────────────── - inner.getChildren().addAll(new Separator(), sectionTitle("Clip-Teile"), new Separator()); - - Label partsHint = new Label("Gewählten Clip in benannte Teile aufteilen. Jeder Teil erhält einen Namen und Zeitgrenzen (Sub-Clips)."); - partsHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); - partsHint.setWrapText(true); - - animSetPartsListView = new ListView<>(); - animSetPartsListView.setPrefHeight(120); - animSetPartsListView.setPlaceholder(new Label("Clip auswählen oder noch keine Teile definiert")); - animSetPartsListView.setDisable(true); - - Button addPartBtn = new Button("+ Teil hinzufügen…"); - Button removePartBtn = new Button("- Entfernen"); - addPartBtn.setMaxWidth(Double.MAX_VALUE); - removePartBtn.setMaxWidth(Double.MAX_VALUE); - addPartBtn.setDisable(true); - removePartBtn.setDisable(true); - - animSetPartsListView.getSelectionModel().selectedItemProperty() - .addListener((obs, ov, nv) -> removePartBtn.setDisable(nv == null)); - - animSetPartsListView.setOnMouseClicked(ev -> { - if (ev.getClickCount() == 2) { - String sel = animSetPartsListView.getSelectionModel().getSelectedItem(); - if (sel == null) return; - String selName = sel.substring(0, sel.indexOf(" [")); - de.blight.game.animation.AnimSet.SubClipDef def = editableSubClips.get(selName); - String source = animSetClipListView.getSelectionModel().getSelectedItem(); - if (def != null && source != null) showPartDialog(source, selName, def); - } - }); - - // Teile-Liste aktualisieren wenn Clip-Auswahl ändert - animSetClipListView.getSelectionModel().selectedItemProperty() - .addListener((obs, ov, nv) -> { - animSetPartsListView.getItems().clear(); - boolean hasClip = nv != null; - animSetPartsListView.setDisable(!hasClip); - addPartBtn.setDisable(!hasClip); - if (hasClip) { - for (var en : editableSubClips.entrySet()) { - if (nv.equals(en.getValue().source)) { - animSetPartsListView.getItems().add( - formatPartEntry(en.getKey(), en.getValue())); - } - } - } - }); - - addPartBtn.setOnAction(e -> { - String source = animSetClipListView.getSelectionModel().getSelectedItem(); - if (source != null) showPartDialog(source, null, null); - }); - - removePartBtn.setOnAction(e -> { - String sel = animSetPartsListView.getSelectionModel().getSelectedItem(); - if (sel == null) return; - String name = sel.substring(0, sel.indexOf(" [")); - editableSubClips.remove(name); - animSetPartsListView.getItems().remove(sel); - autoSaveAnimSet(); - }); - - HBox partsBtns = new HBox(6, addPartBtn, removePartBtn); - HBox.setHgrow(addPartBtn, Priority.ALWAYS); - HBox.setHgrow(removePartBtn, Priority.ALWAYS); - inner.getChildren().addAll(partsHint, animSetPartsListView, partsBtns); - - // ── Aktions-Zuordnung ───────────────────────────────────────────────── - inner.getChildren().addAll(new Separator(), sectionTitle("Aktions-Zuordnung"), new Separator()); - - animSetActionListView = new ListView<>(); - animSetActionListView.setPrefHeight(160); - for (var entry : animSet.getActionMap().entrySet()) - animSetActionListView.getItems().add(entry.getKey() + " → " + entry.getValue()); - - Button addActionBtn = new Button("+ Zuordnen…"); - Button removeActionBtn = new Button("- Entfernen"); - addActionBtn.setMaxWidth(Double.MAX_VALUE); - removeActionBtn.setMaxWidth(Double.MAX_VALUE); - removeActionBtn.setDisable(true); - - animSetActionListView.getSelectionModel().selectedItemProperty() - .addListener((obs, ov, nv) -> removeActionBtn.setDisable(nv == null)); - - addActionBtn.setOnAction(e -> showAddActionToSetDialog()); - removeActionBtn.setOnAction(e -> { - String sel = animSetActionListView.getSelectionModel().getSelectedItem(); - if (sel != null) { - animSetActionListView.getItems().remove(sel); - autoSaveAnimSet(); - } - }); - - HBox actionBtns = new HBox(6, addActionBtn, removeActionBtn); - HBox.setHgrow(addActionBtn, Priority.ALWAYS); - HBox.setHgrow(removeActionBtn, Priority.ALWAYS); - inner.getChildren().addAll(animSetActionListView, actionBtns); - - // ── Anim-Offsets ────────────────────────────────────────────────────── - inner.getChildren().addAll(new Separator(), sectionTitle("Anim-Offsets"), new Separator()); - - Label kfHint = new Label("TX/TZ = charakter-lokal (seitlich/vorwärts), TY = Welt-Y (hoch/runter). RX/RY/RZ in Grad, additiv zur Startrotation."); - kfHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); - kfHint.setWrapText(true); - - // Offsets aus AnimSet laden - animSetOffsets = new java.util.LinkedHashMap<>(animSet.getAnimOffsets()); - animSetOffsetList = javafx.collections.FXCollections.observableArrayList(); - animSetOffsetList.addListener((javafx.collections.ListChangeListener) - change -> updateAnimPreviewOffset()); - - // ListView zeigt den einen Offset des gewählten Clips; Doppelklick öffnet Edit-Dialog - animSetOffsetListView = new javafx.scene.control.ListView<>(animSetOffsetList); - animSetOffsetListView.setPrefHeight(60); - animSetOffsetListView.setPlaceholder(new Label("Kein Offset – [+ Offset] zum Setzen")); - animSetOffsetListView.setCellFactory(lv -> new javafx.scene.control.ListCell<>() { - @Override protected void updateItem(de.blight.game.animation.AnimOffset off, boolean empty) { - super.updateItem(off, empty); - if (empty || off == null) { setText(null); return; } - setText(String.format("TX%+.3f TY%+.3f TZ%+.3f | RX%+.1f° RY%+.1f° RZ%+.1f°", - off.tx, off.ty, off.tz, off.rx, off.ry, off.rz)); - } - }); - animSetOffsetListView.setOnMouseClicked(ev -> { - if (ev.getClickCount() == 2) { - de.blight.game.animation.AnimOffset sel = - animSetOffsetListView.getSelectionModel().getSelectedItem(); - if (sel != null) showAnimOffsetDialog(sel); - } - }); - - // initial deaktiviert – wird durch Clip-Selektion gesteuert - animSetOffsetListView.setDisable(true); - - Button addKfBtn = new Button("+ Offset"); - Button removeKfBtn = new Button("- Entfernen"); - addKfBtn.setMaxWidth(Double.MAX_VALUE); - removeKfBtn.setMaxWidth(Double.MAX_VALUE); - addKfBtn.setDisable(true); - removeKfBtn.setDisable(true); - animSetOffsetListView.getSelectionModel().selectedItemProperty() - .addListener((obs, ov, nv) -> removeKfBtn.setDisable(nv == null)); - // addKfBtn folgt Clip-Selektion (nur wenn noch kein Offset gesetzt) - animSetClipListView.getSelectionModel().selectedItemProperty() - .addListener((obs, ov, nv) -> addKfBtn.setDisable(nv == null)); - - addKfBtn.setOnAction(e -> showAnimOffsetDialog(null)); - - removeKfBtn.setOnAction(e -> { - de.blight.game.animation.AnimOffset sel = - animSetOffsetListView.getSelectionModel().getSelectedItem(); - if (sel != null) { - animSetOffsetList.remove(sel); - autoSaveAnimSet(); - } - }); - - HBox kfBtns = new HBox(6, addKfBtn, removeKfBtn); - HBox.setHgrow(addKfBtn, Priority.ALWAYS); - HBox.setHgrow(removeKfBtn, Priority.ALWAYS); - inner.getChildren().addAll(kfHint, animSetOffsetListView, kfBtns); - - // ── Abläufe ─────────────────────────────────────────────────────────── - inner.getChildren().addAll(new Separator(), sectionTitle("Abläufe"), new Separator()); - - Label seqHint = new Label("Aktionen aus der Zuordnung zu Abläufen zusammenstellen und testen."); - seqHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); - seqHint.setWrapText(true); - - animSetSequenceListView = new ListView<>(); - animSetSequenceListView.setPrefHeight(120); - animSetSequenceListView.setPlaceholder(new Label("Noch keine Abläufe definiert")); - for (var en : editableSequences.entrySet()) - animSetSequenceListView.getItems().add(formatSequenceEntry(en.getKey(), en.getValue())); - - Button addSeqBtn = new Button("+ Ablauf…"); - Button removeSeqBtn = new Button("- Entfernen"); - addSeqBtn.setMaxWidth(Double.MAX_VALUE); - removeSeqBtn.setMaxWidth(Double.MAX_VALUE); - removeSeqBtn.setDisable(true); - - animSetSequenceListView.getSelectionModel().selectedItemProperty() - .addListener((obs, ov, nv) -> { - removeSeqBtn.setDisable(nv == null); - addSeqBtn.setText(nv == null ? "+ Ablauf…" : "Bearbeiten…"); - }); - - animSetSequenceListView.setOnMouseClicked(ev -> { - String sel = animSetSequenceListView.getSelectionModel().getSelectedItem(); - if (sel == null) return; - if (ev.getClickCount() == 2) { - playSequence(sel); - } - }); - - addSeqBtn.setOnAction(e -> { - String sel = animSetSequenceListView.getSelectionModel().getSelectedItem(); - if (sel == null) { - showSequenceBuilderDialog(null, null); - } else { - String seqName = extractSequenceName(sel); - showSequenceBuilderDialog(seqName, new java.util.ArrayList<>( - editableSequences.getOrDefault(seqName, java.util.List.of()))); - } - }); - - removeSeqBtn.setOnAction(e -> { - String sel = animSetSequenceListView.getSelectionModel().getSelectedItem(); - if (sel == null) return; - String seqName = extractSequenceName(sel); - editableSequences.remove(seqName); - animSetSequenceListView.getItems().remove(sel); - autoSaveAnimSet(); - }); - - HBox seqBtns = new HBox(6, addSeqBtn, removeSeqBtn); - HBox.setHgrow(addSeqBtn, Priority.ALWAYS); - HBox.setHgrow(removeSeqBtn, Priority.ALWAYS); - inner.getChildren().addAll(seqHint, animSetSequenceListView, seqBtns); - - // ── Vorschau ───────────────────────────────────────────────────────── - inner.getChildren().addAll(new Separator(), sectionTitle("Vorschau"), new Separator()); - - Label previewHint = new Label("Clip auswählen → Abspielen (oder Doppelklick)"); - previewHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); - previewHint.setWrapText(true); - - Button playBtn = new Button("▶ Abspielen"); - Button stopBtn = new Button("■ Stop"); - playBtn.setMaxWidth(Double.MAX_VALUE); - stopBtn.setMaxWidth(Double.MAX_VALUE); - - animSetClipListView.setOnMouseClicked(e -> { - if (e.getClickCount() == 2) previewSelectedClip(); - }); - playBtn.setOnAction(e -> previewSelectedClip()); - stopBtn.setOnAction(e -> input.animPreviewPlayClip = ""); - - HBox previewBtns = new HBox(6, playBtn, stopBtn); - HBox.setHgrow(playBtn, Priority.ALWAYS); - HBox.setHgrow(stopBtn, Priority.ALWAYS); - - CheckBox loopCB = new CheckBox("Loop"); - loopCB.setSelected(true); - loopCB.setOnAction(ev -> input.animPreviewLoop = loopCB.isSelected()); - - Slider speedSlider = new Slider(0.1, 3.0, 1.0); - speedSlider.setShowTickMarks(true); - speedSlider.setShowTickLabels(true); - speedSlider.setMajorTickUnit(1.0); - speedSlider.valueProperty().addListener((obs, ov, nv) -> input.animPreviewSpeed = nv.floatValue()); - - animPreviewStatusLabel = new Label("Kein Modell geladen"); - animPreviewStatusLabel.setWrapText(true); - animPreviewStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); - - inner.getChildren().addAll(previewHint, previewBtns, loopCB, - new Label("Tempo:"), speedSlider, animPreviewStatusLabel); - - ScrollPane scroll = new ScrollPane(inner); - scroll.setFitToWidth(true); - scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); - VBox panel = new VBox(scroll); - VBox.setVgrow(scroll, Priority.ALWAYS); - panel.setPrefWidth(270); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - return panel; - } - - /** Öffnet den Offset-Dialog (null = Hinzufügen, non-null = Bearbeiten). */ - private void showAnimOffsetDialog(de.blight.game.animation.AnimOffset existing) { - boolean isAdd = (existing == null); - - float initTX = isAdd ? 0f : existing.tx; - float initTY = isAdd ? 0f : existing.ty; - float initTZ = isAdd ? 0f : existing.tz; - float initRX = isAdd ? 0f : existing.rx; - float initRY = isAdd ? 0f : existing.ry; - float initRZ = isAdd ? 0f : existing.rz; - - Spinner spTX = new Spinner<>(-10.0, 10.0, initTX, 0.05); - Spinner spTY = new Spinner<>(-10.0, 10.0, initTY, 0.05); - Spinner spTZ = new Spinner<>(-10.0, 10.0, initTZ, 0.05); - Spinner spRX = new Spinner<>(-360.0, 360.0, initRX, 1.0); - Spinner spRY = new Spinner<>(-360.0, 360.0, initRY, 1.0); - Spinner spRZ = new Spinner<>(-360.0, 360.0, initRZ, 1.0); - for (Spinner sp : java.util.List.of(spTX, spTY, spTZ, spRX, spRY, spRZ)) { - sp.setEditable(true); - sp.setMaxWidth(Double.MAX_VALUE); - } - - String[][] rows = {{"TX (m):"}, {"TY (m):"}, {"TZ (m):"}, {"RX (°):"}, {"RY (°):"}, {"RZ (°):"}}; - Spinner[] sps = {spTX, spTY, spTZ, spRX, spRY, spRZ}; - javafx.scene.layout.GridPane kfGrid = new javafx.scene.layout.GridPane(); - kfGrid.setHgap(8); kfGrid.setVgap(5); - for (int i = 0; i < sps.length; i++) { - kfGrid.add(new Label(rows[i][0]), 0, i); - kfGrid.add(sps[i], 1, i); - } - javafx.scene.layout.ColumnConstraints kfCC = new javafx.scene.layout.ColumnConstraints(); - kfCC.setHgrow(Priority.ALWAYS); - kfGrid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), kfCC); - - javafx.scene.control.Dialog kfDlg = - new javafx.scene.control.Dialog<>(); - kfDlg.initOwner(primaryStage); - kfDlg.setTitle(isAdd ? "Offset hinzufügen" : "Offset bearbeiten"); - javafx.scene.control.ButtonType okKf = new javafx.scene.control.ButtonType( - isAdd ? "Hinzufügen" : "Übernehmen", - javafx.scene.control.ButtonBar.ButtonData.OK_DONE); - kfDlg.getDialogPane().getButtonTypes().addAll(okKf, javafx.scene.control.ButtonType.CANCEL); - kfDlg.getDialogPane().setContent(kfGrid); - kfDlg.showAndWait().ifPresent(bt -> { - if (bt != okKf) return; - if (isAdd) { - de.blight.game.animation.AnimOffset off = new de.blight.game.animation.AnimOffset( - spTX.getValue().floatValue(), spTY.getValue().floatValue(), spTZ.getValue().floatValue(), - spRX.getValue().floatValue(), spRY.getValue().floatValue(), spRZ.getValue().floatValue()); - animSetOffsetList.setAll(off); - } else { - existing.tx = spTX.getValue().floatValue(); - existing.ty = spTY.getValue().floatValue(); - existing.tz = spTZ.getValue().floatValue(); - existing.rx = spRX.getValue().floatValue(); - existing.ry = spRY.getValue().floatValue(); - existing.rz = spRZ.getValue().floatValue(); - } - if (animSetOffsetListView != null) animSetOffsetListView.refresh(); - autoSaveAnimSet(); - }); - } - - /** Spielt den Clip der gegebenen Aktion in der Vorschau ab, um animPreviewCurrentClipDuration zu setzen. */ - private String formatSequenceEntry(String name, java.util.List actions) { - return name + " (" + actions.size() + " Aktionen: " + String.join(" → ", actions) + ")"; - } - - private String extractSequenceName(String entry) { - int idx = entry.indexOf(" ("); - return idx >= 0 ? entry.substring(0, idx) : entry; - } - - private void playSequence(String listEntry) { - String seqName = extractSequenceName(listEntry); - java.util.List actions = editableSequences.get(seqName); - if (actions == null || actions.isEmpty()) return; - - java.util.Map actionMap = new java.util.LinkedHashMap<>(); - if (animSetActionListView != null) { - for (String it : animSetActionListView.getItems()) { - String[] parts = it.split(" → ", 2); - if (parts.length == 2) actionMap.put(parts[0], parts[1]); - } - } - java.util.List clips = new java.util.ArrayList<>(); - for (String action : actions) { - String clip = actionMap.get(action); - if (clip != null) clips.add(clip); - else log.warn("[Sequenz] Keine Clip-Zuordnung für Aktion '{}'", action); - } - if (clips.isEmpty()) { - javafx.scene.control.Alert err = new javafx.scene.control.Alert( - javafx.scene.control.Alert.AlertType.WARNING); - err.initOwner(primaryStage); - err.setHeaderText("Keine Clips für Sequenz"); - err.setContentText("Die Aktionen des Ablaufs sind keinen Clips zugeordnet."); - err.showAndWait(); - return; - } - - // Alle Clips vorab laden (wie previewSelectedClip): animImportQueue lädt sie in den AnimComposer. - // Clip[0] wird über animSetPendingPlayClip gestartet sobald er geladen ist. - // Clips[1..n] sind währenddessen schon in der Sequenz-Queue und werden geladen während Clip[0] läuft. - input.animPreviewSequenceQueue.clear(); - input.animPreviewLoop = false; - animSetPendingPlayClip = clips.get(0); - input.animImportQueue.offer(findAnimClipPath(clips.get(0))); - for (int i = 1; i < clips.size(); i++) { - input.animPreviewSequenceQueue.add(clips.get(i)); - input.animImportQueue.offer(findAnimClipPath(clips.get(i))); - } - if (animPreviewStatusLabel != null) - animPreviewStatusLabel.setText("Lade Sequenz \"" + seqName + "\"…"); - } - - /** Dialog: linke Liste = verfügbare Aktionen, rechte Liste = Ablauf-Reihenfolge. */ - private void showSequenceBuilderDialog(String existingName, java.util.List existingActions) { - // Verfügbare Aktionen aus animSetActionListView auslesen - java.util.List availableActions = new java.util.ArrayList<>(); - if (animSetActionListView != null) { - for (String it : animSetActionListView.getItems()) { - String[] parts = it.split(" → ", 2); - if (parts.length == 2) availableActions.add(parts[0]); - } - } - if (availableActions.isEmpty()) { - javafx.scene.control.Alert err = new javafx.scene.control.Alert( - javafx.scene.control.Alert.AlertType.WARNING); - err.initOwner(primaryStage); - err.setHeaderText("Keine Aktionen"); - err.setContentText("Zuerst in der Aktions-Zuordnung Aktionen einem Clip zuweisen."); - err.showAndWait(); - return; - } - - // ── Namensfeld ──────────────────────────────────────────────────────── - javafx.scene.control.TextField nameFld = new javafx.scene.control.TextField( - existingName != null ? existingName : ""); - nameFld.setPromptText("Name des Ablaufs…"); - - // ── Linke Liste (Aktionen) ──────────────────────────────────────────── - ListView leftList = new ListView<>(); - leftList.getItems().addAll(availableActions); - leftList.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); - leftList.setPrefSize(200, 240); - - // ── Rechte Liste (Ablauf) ──────────────────────────────────────────── - ListView rightList = new ListView<>(); - if (existingActions != null) rightList.getItems().addAll(existingActions); - rightList.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); - rightList.setPrefSize(200, 240); - - // ── Buttons Mitte ──────────────────────────────────────────────────── - Button addBtn = new Button("→"); - Button removeBtn = new Button("×"); - addBtn.setMaxWidth(Double.MAX_VALUE); - removeBtn.setMaxWidth(Double.MAX_VALUE); - addBtn.setStyle("-fx-font-size: 14;"); - removeBtn.setStyle("-fx-font-size: 14;"); - - addBtn.setOnAction(ev -> { - java.util.List sel = new java.util.ArrayList<>( - leftList.getSelectionModel().getSelectedItems()); - rightList.getItems().addAll(sel); - }); - - removeBtn.setOnAction(ev -> { - java.util.List sel = new java.util.ArrayList<>( - rightList.getSelectionModel().getSelectedItems()); - rightList.getItems().removeAll(sel); - }); - - VBox midBtns = new VBox(8, addBtn, removeBtn); - midBtns.setAlignment(javafx.geometry.Pos.CENTER); - - // ── Layout ──────────────────────────────────────────────────────────── - javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane(); - grid.setHgap(8); - grid.setVgap(6); - grid.add(new Label("Verfügbare Aktionen:"), 0, 0); - grid.add(new Label("Ablauf:"), 2, 0); - grid.add(leftList, 0, 1); - grid.add(midBtns, 1, 1); - grid.add(rightList, 2, 1); - - javafx.scene.layout.ColumnConstraints ccFill = new javafx.scene.layout.ColumnConstraints(); - ccFill.setHgrow(Priority.ALWAYS); - javafx.scene.layout.ColumnConstraints ccFixed = new javafx.scene.layout.ColumnConstraints(48); - grid.getColumnConstraints().addAll(ccFill, ccFixed, ccFill); - - VBox content = new VBox(8, new Label("Ablauf-Name:"), nameFld, grid); - content.setPadding(new Insets(4)); - - javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType( - "Speichern", javafx.scene.control.ButtonBar.ButtonData.OK_DONE); - javafx.scene.control.Dialog dlg = - new javafx.scene.control.Dialog<>(); - dlg.initOwner(primaryStage); - dlg.setTitle(existingName == null ? "Ablauf erstellen" : "Ablauf bearbeiten"); - dlg.getDialogPane().setContent(content); - dlg.getDialogPane().setPrefWidth(520); - dlg.getDialogPane().getButtonTypes().addAll(ok, javafx.scene.control.ButtonType.CANCEL); - dlg.setResultConverter(bt -> bt); - - dlg.showAndWait().ifPresent(bt -> { - if (bt != ok) return; - String name = nameFld.getText().trim(); - if (name.isEmpty()) return; - java.util.List actions = new java.util.ArrayList<>(rightList.getItems()); - - // Alten Eintrag ggf. umbenennen - if (existingName != null && !existingName.equals(name)) { - editableSequences.remove(existingName); - animSetSequenceListView.getItems().removeIf(it -> extractSequenceName(it).equals(existingName)); - } - editableSequences.put(name, actions); - - // ListView-Eintrag aktualisieren oder neu anlegen - String newEntry = formatSequenceEntry(name, actions); - boolean replaced = false; - if (existingName != null) { - for (int i = 0; i < animSetSequenceListView.getItems().size(); i++) { - if (extractSequenceName(animSetSequenceListView.getItems().get(i)).equals(name)) { - animSetSequenceListView.getItems().set(i, newEntry); - replaced = true; - break; - } - } - } - if (!replaced) animSetSequenceListView.getItems().add(newEntry); - autoSaveAnimSet(); - }); - } - - private void autoPreviewClipForAction(de.blight.game.animation.AnimationAction action) { - if (animSetActionListView == null || action == null) return; - String prefix = action.name() + " → "; - for (String entry : animSetActionListView.getItems()) { - if (entry.startsWith(prefix)) { - String clip = entry.substring(prefix.length()).trim(); - if (!clip.isEmpty()) { - input.animPreviewPlayClip = clip; - } - break; - } - } - } - - private void updateAnimPreviewOffset() { - if (animSetOffsetList == null || animSetOffsetList.isEmpty()) { - input.animPreviewOffsetTx = 0f; - input.animPreviewOffsetTy = 0f; - input.animPreviewOffsetTz = 0f; - input.animPreviewOffsetActive = false; - } else { - de.blight.game.animation.AnimOffset off = animSetOffsetList.get(0); - input.animPreviewOffsetTx = off.tx; - input.animPreviewOffsetTy = off.ty; - input.animPreviewOffsetTz = off.tz; - input.animPreviewOffsetActive = true; - } - } - - private void previewSelectedClip() { - if (animSetClipListView == null) return; - String clip = animSetClipListView.getSelectionModel().getSelectedItem(); - if (clip == null) return; - if (animCurrentModelPath == null || animCurrentModelPath.isBlank()) { - if (animPreviewStatusLabel != null) - animPreviewStatusLabel.setText("Erst ein Charakter-Modell laden."); - return; - } - animSetPendingPlayClip = clip; - input.animImportQueue.offer(findAnimClipPath(clip)); - if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade Clip " + clip + "…"); - } - - private String findAnimClipPath(String clipName) { - Path charDir = ASSET_ROOT.resolve("Characters"); - for (String ext : new String[]{".j3o", ".glb", ".gltf", ".fbx"}) { - if (java.nio.file.Files.exists(charDir.resolve("clips").resolve(clipName + ext))) - return "Characters/clips/" + clipName + ext; - if (java.nio.file.Files.exists(charDir.resolve(clipName + ext))) - return "Characters/" + clipName + ext; - } - return "Characters/clips/" + clipName + ".j3o"; - } - - private void showAddActionToSetDialog() { - if (animSetClipListView == null || animSetClipListView.getItems().isEmpty()) { - setStatus("Keine Clips im Set — erst Clips hinzufügen."); - return; - } - java.util.Set usedActions = new java.util.HashSet<>(); - if (animSetActionListView != null) - for (String it : animSetActionListView.getItems()) - usedActions.add(it.split(" → ")[0]); - - java.util.List available = - java.util.Arrays.stream(de.blight.game.animation.AnimationAction.values()) - .filter(a -> !usedActions.contains(a.name())) - .collect(java.util.stream.Collectors.toList()); - if (available.isEmpty()) { setStatus("Alle Aktionen bereits zugeordnet."); return; } - - ComboBox actionCombo = new ComboBox<>(); - actionCombo.getItems().addAll(available); - javafx.util.Callback, - javafx.scene.control.ListCell> cf = - lv -> new javafx.scene.control.ListCell<>() { - @Override protected void updateItem(de.blight.game.animation.AnimationAction it, - boolean empty) { - super.updateItem(it, empty); - setText(empty || it == null ? null : it.displayName() + " (" + it.name() + ")"); - } - }; - actionCombo.setCellFactory(cf); - actionCombo.setButtonCell(cf.call(null)); - actionCombo.setMaxWidth(Double.MAX_VALUE); - actionCombo.getSelectionModel().selectFirst(); - - ComboBox clipCombo = new ComboBox<>(); - java.util.List allClipNames = new java.util.ArrayList<>(animSetClipListView.getItems()); - for (String subName : editableSubClips.keySet()) { - if (!allClipNames.contains(subName)) allClipNames.add(subName); - } - clipCombo.getItems().addAll(allClipNames); - clipCombo.setMaxWidth(Double.MAX_VALUE); - clipCombo.getSelectionModel().selectFirst(); - - javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane(); - grid.setHgap(8); grid.setVgap(6); - grid.add(new Label("Aktion:"), 0, 0); grid.add(actionCombo, 1, 0); - grid.add(new Label("Clip:"), 0, 1); grid.add(clipCombo, 1, 1); - javafx.scene.layout.ColumnConstraints cc = new javafx.scene.layout.ColumnConstraints(); - cc.setHgrow(Priority.ALWAYS); - grid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), cc); - - javafx.scene.control.Dialog dlg = - new javafx.scene.control.Dialog<>(); - dlg.initOwner(primaryStage); - dlg.setTitle("Aktion zuordnen"); - javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType("Hinzufügen", - javafx.scene.control.ButtonBar.ButtonData.OK_DONE); - dlg.getDialogPane().getButtonTypes().addAll(ok, javafx.scene.control.ButtonType.CANCEL); - dlg.getDialogPane().setContent(grid); - dlg.showAndWait().ifPresent(bt -> { - if (bt != ok) return; - var action = actionCombo.getValue(); - var clip = clipCombo.getValue(); - if (action != null && clip != null) { - String entry = action.name() + " → " + clip; - if (!animSetActionListView.getItems().contains(entry)) { - animSetActionListView.getItems().add(entry); - autoSaveAnimSet(); - } - } - }); - } - - private static String formatPartEntry(String name, - de.blight.game.animation.AnimSet.SubClipDef def) { - return String.format("%s [%.3fs – %.3fs]", name, def.start, def.end); - } - - private void showPartDialog(String source, String existingName, - de.blight.game.animation.AnimSet.SubClipDef existing) { - boolean isEdit = existingName != null && existing != null; - - javafx.scene.control.TextField nameField = new javafx.scene.control.TextField( - isEdit ? existingName : ""); - nameField.setPromptText("Teil-Name (z. B. sit_down_bench)"); - nameField.setDisable(isEdit); // Name beim Bearbeiten nicht änderbar - - Spinner startSpinner = new Spinner<>(0.0, 9999.0, - isEdit ? existing.start : 0.0, 0.033); - startSpinner.setEditable(true); - startSpinner.setMaxWidth(Double.MAX_VALUE); - - Spinner endSpinner = new Spinner<>(0.0, 9999.0, - isEdit ? existing.end : 1.0, 0.033); - endSpinner.setEditable(true); - endSpinner.setMaxWidth(Double.MAX_VALUE); - - javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane(); - grid.setHgap(8); grid.setVgap(6); - grid.add(new Label("Name:"), 0, 0); grid.add(nameField, 1, 0); - grid.add(new Label("Start:"), 0, 1); grid.add(startSpinner, 1, 1); - grid.add(new Label("Ende:"), 0, 2); grid.add(endSpinner, 1, 2); - javafx.scene.layout.ColumnConstraints cc = new javafx.scene.layout.ColumnConstraints(); - cc.setHgrow(Priority.ALWAYS); - grid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), cc); - - javafx.scene.control.Dialog dlg = - new javafx.scene.control.Dialog<>(); - dlg.initOwner(primaryStage); - dlg.setTitle(isEdit ? "Clip-Teil bearbeiten" : "Clip-Teil hinzufügen"); - dlg.setHeaderText((isEdit ? "Bearbeiten: " : "Neuer Teil für: ") + source); - javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType( - isEdit ? "Übernehmen" : "Hinzufügen", - javafx.scene.control.ButtonBar.ButtonData.OK_DONE); - dlg.getDialogPane().getButtonTypes().addAll(ok, javafx.scene.control.ButtonType.CANCEL); - dlg.getDialogPane().setContent(grid); - - javafx.scene.Node okNode = dlg.getDialogPane().lookupButton(ok); - Runnable validate = () -> { - boolean valid = !nameField.getText().isBlank() - && endSpinner.getValue() > startSpinner.getValue(); - okNode.setDisable(!valid); - }; - validate.run(); - nameField.textProperty().addListener((obs, ov, nv) -> validate.run()); - startSpinner.valueProperty().addListener((obs, ov, nv) -> validate.run()); - endSpinner.valueProperty().addListener((obs, ov, nv) -> validate.run()); - - dlg.showAndWait().ifPresent(bt -> { - if (bt != ok) return; - String name = isEdit ? existingName : nameField.getText().trim(); - if (name.isBlank()) return; - de.blight.game.animation.AnimSet.SubClipDef def = - new de.blight.game.animation.AnimSet.SubClipDef(); - def.source = source; - def.start = startSpinner.getValue().floatValue(); - def.end = endSpinner.getValue().floatValue(); - editableSubClips.put(name, def); - if (animSetPartsListView != null) { - animSetPartsListView.getItems().removeIf(s -> s.startsWith(name + " [")); - animSetPartsListView.getItems().add(formatPartEntry(name, def)); - } - autoSaveAnimSet(); - }); - } - - private void autoSaveAnimSet() { - if (animSetCurrentName != null && animSetCurrentDir != null) - saveCurrentAnimSet(animSetCurrentName, animSetCurrentDir); - } private void autoSaveCharacter() { if (charIdField == null || charIdField.getText().trim().isBlank()) return; saveCharacter(ASSET_ROOT.resolve("Characters")); } - private void saveCurrentAnimSet(String setName, Path setDir) { - if (animSetClipListView == null) { - return; - } - de.blight.game.animation.AnimSet animSet = new de.blight.game.animation.AnimSet(); - animSet.setClips(new java.util.ArrayList<>(animSetClipListView.getItems())); - java.util.Map actionMap = new java.util.LinkedHashMap<>(); - if (animSetActionListView != null) { - for (String it : animSetActionListView.getItems()) { - String[] parts = it.split(" → ", 2); - if (parts.length == 2) { - actionMap.put(parts[0], parts[1]); - } - } - } - animSet.setActionMap(actionMap); - // Anim-Offsets: aktuell sichtbaren Offset (des selektierten Clips) sichern, dann schreiben - if (animSetClipListView != null) { - String selClip = animSetClipListView.getSelectionModel().getSelectedItem(); - if (selClip != null) { - if (!animSetOffsetList.isEmpty()) { - animSetOffsets.put(selClip, animSetOffsetList.get(0)); - } else { - animSetOffsets.remove(selClip); - } - } - } - java.util.Map offsetFinal = - new java.util.LinkedHashMap<>(); - for (var entry : animSetOffsets.entrySet()) { - if (entry.getValue() != null) { - offsetFinal.put(entry.getKey(), entry.getValue()); - } - } - animSet.setAnimOffsets(offsetFinal); - animSet.setSubClips(new java.util.LinkedHashMap<>(editableSubClips)); - animSet.setSequences(new java.util.LinkedHashMap<>(editableSequences)); - // Clip-Offset-Map für AnimPreviewState synchronisieren - input.animPreviewClipOffsets.clear(); - for (var en : offsetFinal.entrySet()) { - de.blight.game.animation.AnimOffset o = en.getValue(); - input.animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz}); - } - // Locomotion-Clips für snapRootBoneXZ synchronisieren - java.util.Set locoActions = java.util.Set.of("WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT"); - input.animPreviewLocomotionClips.clear(); - animSet.getActionMap().forEach((action, clip) -> { - if (locoActions.contains(action)) input.animPreviewLocomotionClips.add(clip); - }); - // Vorschau-Modell-Pfad beibehalten - if (animSetModelCombo != null && animSetModelCombo.getValue() != null && !animSetModelCombo.getValue().isBlank()) { - animSet.setPreviewModelPath(animSetModelCombo.getValue()); - } - try { - animSet.save(setDir, setName); - setStatus("AnimSet gespeichert: " + setName + ".animset.json"); - input.refreshAssets = true; - } catch (IOException ex) { - setStatus("Fehler beim Speichern: " + ex.getMessage()); - } - } - - private void createNewAnimSet() { - doCreateNewAnimSet(); - } - - private void doCreateNewAnimSet() { - TextInputDialog dlg = new TextInputDialog(); - dlg.initOwner(primaryStage); - dlg.setTitle("Neues Animations-Set"); - dlg.setHeaderText("Name des neuen Animations-Sets:"); - dlg.setContentText("Name:"); - dlg.showAndWait().ifPresent(raw -> { - String name = raw.trim(); - if (name.isBlank()) return; - Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets"); - Path setFile = setDir.resolve(name + ".animset.json"); - try { - Files.createDirectories(setDir); - new de.blight.game.animation.AnimSet().save(setDir, name); - refreshCategoryNode(charactersNode, shouldPreserveExpansion()); - String relPath = ASSET_ROOT.relativize(setFile).toString().replace('\\', '/'); - openAnimSetEditor(relPath, setFile); - } catch (IOException ex) { - setStatus("Fehler beim Anlegen: " + ex.getMessage()); - } - }); - } - - private ToolBar buildAnimPreviewToolBar() { - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - Label label = new Label("Animations Viewer"); - label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - ToolBar tb = new ToolBar(); - tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); - return tb; - } - - private VBox buildAnimPreviewPanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - - // ── Animationen ─────────────────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Animationen"), new Separator()); - animClipListView = new ListView<>(); - animClipListView.setPrefHeight(200); - animClipListView.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.SINGLE); - - Button playBtn = new Button("▶ Abspielen"); - Button stopBtn = new Button("■ Stop"); - playBtn.setMaxWidth(Double.MAX_VALUE); - stopBtn.setMaxWidth(Double.MAX_VALUE); - playBtn.setStyle("-fx-font-weight: bold;"); - playBtn.setOnAction(e -> { - String clip = animClipListView.getSelectionModel().getSelectedItem(); - if (clip != null) input.animPreviewPlayClip = clip; - }); - stopBtn.setOnAction(e -> input.animPreviewPlayClip = ""); - animClipListView.setOnMouseClicked(e -> { - if (e.getClickCount() == 2) { - String clip = animClipListView.getSelectionModel().getSelectedItem(); - if (clip != null) input.animPreviewPlayClip = clip; - } - }); - HBox btnRow = new HBox(6, playBtn, stopBtn); - HBox.setHgrow(playBtn, Priority.ALWAYS); - HBox.setHgrow(stopBtn, Priority.ALWAYS); - inner.getChildren().addAll(animClipListView, btnRow); - - // ── Steuerung ───────────────────────────────────────────────────────── - inner.getChildren().addAll(new Separator(), sectionTitle("Steuerung"), new Separator()); - - CheckBox loopCB = new CheckBox("Loop"); - loopCB.setSelected(true); - loopCB.setOnAction(e -> input.animPreviewLoop = loopCB.isSelected()); - - Label speedLbl = new Label("Geschwindigkeit"); - Slider speedSlider = new Slider(0.1, 3.0, 1.0); - speedSlider.setShowTickMarks(true); - speedSlider.setShowTickLabels(true); - speedSlider.setMajorTickUnit(1.0); - speedSlider.valueProperty().addListener((obs, ov, nv) -> input.animPreviewSpeed = nv.floatValue()); - inner.getChildren().addAll(loopCB, speedLbl, withField(speedSlider, "%.2f")); - - // ── Status ──────────────────────────────────────────────────────────── - inner.getChildren().addAll(new Separator()); - animPreviewStatusLabel = new Label(""); - animPreviewStatusLabel.setWrapText(true); - animPreviewStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #c00;"); - inner.getChildren().add(animPreviewStatusLabel); - - ScrollPane scroll = new ScrollPane(inner); - scroll.setFitToWidth(true); - scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); - VBox panel = new VBox(scroll); - VBox.setVgrow(scroll, Priority.ALWAYS); - panel.setPrefWidth(270); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - return panel; - } - - - /** - * Kopiert die Original-Quelldatei nach {@code /assets/imported//}. - * Dient als Archiv vor jeder Konvertierung; Fehler werden nur geloggt. - */ - 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 handleAnimationImport(javafx.stage.Window owner) { - FileChooser fc = new FileChooser(); - fc.setTitle("Animation importieren (FBX/GLB/GLTF)"); - fc.getExtensionFilters().add( - new FileChooser.ExtensionFilter("Animationen (FBX, GLTF, GLB)", "*.fbx", "*.gltf", "*.glb")); - var files = fc.showOpenMultipleDialog(owner); - if (files == null) return; - for (File file : files) { - archiveOriginal(file, "animations"); - // Absoluten Pfad übergeben – kein Kopieren nötig. - // addAnimation() lädt direkt vom Ursprungsort und speichert nur J3O nach clips/. - input.animImportQueue.offer(file.getAbsolutePath()); - setStatus("Importiere: " + file.getName() + " …"); - } - } - - private void handleCharacterModelImport(javafx.stage.Window owner) { - FileChooser fc = new FileChooser(); - fc.setTitle("Character-Modell importieren (Mixamo T-Pose FBX)"); - fc.getExtensionFilters().add( - new FileChooser.ExtensionFilter("FBX-Modelle", "*.fbx")); - File file = fc.showOpenDialog(owner); - if (file == null) return; - archiveOriginal(file, "models"); - input.charModelImportQueue.offer(file.getAbsolutePath()); - if (charEditorStatusLabel != null) - charEditorStatusLabel.setText("Importiere: " + file.getName() + " …"); - setStatus("Importiere Character-Modell: " + file.getName() + " …"); - } - - private void reimportModelForPreview(javafx.stage.Window owner) { - FileChooser fc = new FileChooser(); - fc.setTitle("Modell (GLB/GLTF) neu importieren"); - fc.getExtensionFilters().add( - new FileChooser.ExtensionFilter("3D-Modelle (GLTF, GLB)", "*.gltf", "*.glb")); - File file = fc.showOpenDialog(owner); - if (file == null) return; - archiveOriginal(file, "models"); - - String selectedJ3o = animPreviewModelCombo != null ? animPreviewModelCombo.getValue() : null; - Path destDir; - Path destJ3o; - if (selectedJ3o != null && !selectedJ3o.isEmpty()) { - // Ziel: gleicher Pfad wie das aktuell gewählte j3o - destJ3o = ASSET_ROOT.resolve(selectedJ3o.replace('/', java.io.File.separatorChar)); - destDir = destJ3o.getParent(); - } else { - destDir = ASSET_ROOT.resolve("Models"); - String baseName = file.getName().replaceFirst("\\.[^.]+$", ""); - destJ3o = destDir.resolve(baseName + ".j3o"); - } - // ── Korrektur-Dialog ────────────────────────────────────────────────── - float[] correction = showImportCorrectionDialog(owner); - if (correction == null) return; // Abgebrochen - - final float yRot = correction[0]; - final boolean centerOrg = correction[1] != 0f; - final Path destDirFinal = destDir; - final Path destJ3oFinal = destJ3o; - - try { - Files.createDirectories(destDirFinal); - Path destFile = destDirFinal.resolve(file.getName()); - Files.copy(file.toPath(), destFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - String assetPath = ASSET_ROOT.relativize(destFile).toString().replace('\\', '/'); - input.modelConvertQueue.offer( - new SharedInput.ModelConvertRequest(assetPath, destJ3oFinal, destFile, true, - yRot, centerOrg)); - if (animPreviewStatusLabel != null) - animPreviewStatusLabel.setText("Konvertiere " + file.getName() + " → .j3o…"); - setStatus("Konvertiere " + file.getName() + " → .j3o…"); - // Nach Konvertierung Modell automatisch neu laden - String finalJ3oPath = ASSET_ROOT.relativize(destJ3oFinal).toString().replace('\\', '/'); - new Thread(() -> { - for (int i = 0; i < 300; i++) { - try { Thread.sleep(100); } catch (InterruptedException ignored) {} - if (java.nio.file.Files.exists(destJ3oFinal)) break; - } - javafx.application.Platform.runLater(() -> { - if (animPreviewModelCombo != null && - !animPreviewModelCombo.getItems().contains(finalJ3oPath)) { - animPreviewModelCombo.getItems().add(finalJ3oPath); - } - if (animPreviewModelCombo != null) - animPreviewModelCombo.setValue(finalJ3oPath); - input.animPreviewLoadPath = finalJ3oPath; - if (animPreviewStatusLabel != null) - animPreviewStatusLabel.setText("Lade neu importiertes Modell…"); - }); - }, "reimport-wait").start(); - } catch (IOException ex) { - setStatus("Fehler beim Re-Import: " + ex.getMessage()); - } - } - - // ── Animations-Set-Dialog ───────────────────────────────────────────────── - - private void showSaveAnimSetDialog() { - if (animClipListView == null || animClipListView.getItems().isEmpty()) return; - javafx.scene.control.Dialog dlg = new javafx.scene.control.Dialog<>(); - dlg.initOwner(primaryStage); - dlg.setTitle("Animations-Set speichern"); - dlg.setHeaderText("Clips und Aktions-Zuweisung für das Set konfigurieren"); - javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType("Speichern", - javafx.scene.control.ButtonBar.ButtonData.OK_DONE); - dlg.getDialogPane().getButtonTypes().addAll(ok, javafx.scene.control.ButtonType.CANCEL); - - javafx.scene.control.TextField nameField = new javafx.scene.control.TextField(); - nameField.setPromptText("Set-Name…"); - - // Wenn bereits ein Set mit dem aktuellen Modell-Namen existiert, vorausfüllen - if (animCurrentModelPath != null) { - String suggested = java.nio.file.Paths.get(animCurrentModelPath) - .getFileName().toString().replaceFirst("\\.j3o$", ""); - nameField.setText(suggested); - } - - javafx.scene.control.ListView clipSel = new javafx.scene.control.ListView<>(); - clipSel.getItems().addAll(animClipListView.getItems()); - clipSel.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); - clipSel.getSelectionModel().selectAll(); - clipSel.setPrefHeight(180); - Button allBtn = new Button("Alle wählen"); - allBtn.setOnAction(ev -> clipSel.getSelectionModel().selectAll()); - - java.util.List allClips = animClipListView.getItems(); - - Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets"); - - // Aktions-Zuweisung: Liste mit Add/Remove - javafx.scene.control.ListView actionList = new javafx.scene.control.ListView<>(); - actionList.setPrefHeight(130); - - // Lädt bestehende Zuweisungen für den aktuell eingetippten Set-Namen - Runnable reloadActionMap = () -> { - String sn = nameField.getText().trim(); - if (sn.isBlank()) return; - actionList.getItems().clear(); - try { - de.blight.game.animation.AnimSet existing = de.blight.game.animation.AnimSet.load(setDir, sn); - if (existing != null) { - for (var entry : existing.getActionMap().entrySet()) { - if (allClips.contains(entry.getValue())) - actionList.getItems().add(entry.getKey() + " → " + entry.getValue()); - } - } - } catch (Exception ignored) {} - }; - reloadActionMap.run(); - nameField.textProperty().addListener((obs, ov, nv) -> reloadActionMap.run()); - - Button removeActionBtn = new Button("Entfernen"); - removeActionBtn.setDisable(true); - actionList.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> - removeActionBtn.setDisable(nv == null)); - removeActionBtn.setOnAction(ev -> { - String sel = actionList.getSelectionModel().getSelectedItem(); - if (sel != null) actionList.getItems().remove(sel); - }); - - Button addActionBtn = new Button("Hinzufügen"); - addActionBtn.setOnAction(ev -> { - java.util.Set usedActions = new java.util.HashSet<>(); - for (String item : actionList.getItems()) - usedActions.add(item.split(" → ")[0]); - java.util.List available = - java.util.Arrays.stream(de.blight.game.animation.AnimationAction.values()) - .filter(a -> !usedActions.contains(a.name())) - .collect(java.util.stream.Collectors.toList()); - if (available.isEmpty()) return; - - javafx.scene.control.Dialog addDlg = - new javafx.scene.control.Dialog<>(); - addDlg.initOwner(primaryStage); - addDlg.setTitle("Aktion zuweisen"); - javafx.scene.control.ButtonType addOk = new javafx.scene.control.ButtonType("Hinzufügen", - javafx.scene.control.ButtonBar.ButtonData.OK_DONE); - addDlg.getDialogPane().getButtonTypes().addAll(addOk, javafx.scene.control.ButtonType.CANCEL); - - ComboBox actionCombo = new ComboBox<>(); - actionCombo.getItems().addAll(available); - javafx.util.Callback, - javafx.scene.control.ListCell> cellFactory = - lv -> new javafx.scene.control.ListCell<>() { - @Override protected void updateItem(de.blight.game.animation.AnimationAction it, boolean empty) { - super.updateItem(it, empty); - setText(empty || it == null ? null : it.displayName()); - } - }; - actionCombo.setCellFactory(cellFactory); - actionCombo.setButtonCell(cellFactory.call(null)); - actionCombo.setMaxWidth(Double.MAX_VALUE); - actionCombo.getSelectionModel().selectFirst(); - - java.util.List selClips = new java.util.ArrayList<>( - clipSel.getSelectionModel().getSelectedItems()); - if (selClips.isEmpty()) selClips = allClips; - ComboBox clipCombo = new ComboBox<>(); - clipCombo.getItems().addAll(selClips); - clipCombo.setMaxWidth(Double.MAX_VALUE); - clipCombo.getSelectionModel().selectFirst(); - - javafx.scene.layout.GridPane addGrid = new javafx.scene.layout.GridPane(); - addGrid.setHgap(8); - addGrid.setVgap(6); - addGrid.setPadding(new Insets(10)); - addGrid.add(new Label("Aktion:"), 0, 0); - addGrid.add(actionCombo, 1, 0); - addGrid.add(new Label("Animation:"), 0, 1); - addGrid.add(clipCombo, 1, 1); - addGrid.getColumnConstraints().addAll( - new javafx.scene.layout.ColumnConstraints(), - new javafx.scene.layout.ColumnConstraints() {{ setHgrow(Priority.ALWAYS); }}); - addDlg.getDialogPane().setContent(addGrid); - addDlg.getDialogPane().setPrefWidth(320); - - addDlg.showAndWait().ifPresent(bt -> { - if (bt != addOk) return; - de.blight.game.animation.AnimationAction selAction = actionCombo.getValue(); - String selClip = clipCombo.getValue(); - if (selAction == null || selClip == null || selClip.isBlank()) return; - actionList.getItems().add(selAction.name() + " → " + selClip); - }); - }); - - HBox actionBtns = new HBox(4, addActionBtn, removeActionBtn); - - VBox content = new VBox(6, - new Label("Set-Name:"), nameField, - new Label("Clips:"), clipSel, allBtn, - new Separator(), - sectionTitle("Aktions-Zuweisung"), - actionList, actionBtns); - content.setPadding(new Insets(10)); - dlg.getDialogPane().setContent(content); - dlg.getDialogPane().setPrefWidth(400); - - dlg.showAndWait().ifPresent(bt -> { - if (bt != ok) return; - String name = nameField.getText().trim(); - if (name.isBlank()) return; - java.util.List selected = new java.util.ArrayList<>( - clipSel.getSelectionModel().getSelectedItems()); - if (selected.isEmpty()) return; - java.util.Map actionMap = new java.util.LinkedHashMap<>(); - for (String item : actionList.getItems()) { - String[] parts = item.split(" → ", 2); - if (parts.length == 2) actionMap.put(parts[0], parts[1]); - } - input.animSetSaveRequest.set(new SharedInput.AnimSetSaveRequest(selected, name, actionMap)); - }); - } - // ── Character-Editor ────────────────────────────────────────────────────── private void switchToCharacterEditor() { diff --git a/blight-editor/src/main/java/de/blight/editor/ToolContext.java b/blight-editor/src/main/java/de/blight/editor/ToolContext.java index 081f91c..c5ebd81 100644 --- a/blight-editor/src/main/java/de/blight/editor/ToolContext.java +++ b/blight-editor/src/main/java/de/blight/editor/ToolContext.java @@ -25,6 +25,7 @@ import java.util.function.Supplier; * @param viewportSupplier Liefert den JME3-Viewport-StackPane * @param assetRoot Wurzelpfad der blight-assets (für Dateidialoge / IO) * @param switchToWorld Wechselt zurück zum Welteneditor (für "← Welteneditor"-Buttons) + * @param registry ToolRegistry (damit Controller sich selbst aktivieren können) */ public record ToolContext( SharedInput input, @@ -36,5 +37,6 @@ public record ToolContext( Consumer setMainToolbar, Supplier viewportSupplier, Path assetRoot, - Runnable switchToWorld + Runnable switchToWorld, + ToolRegistry registry ) {} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/anim/AnimPreviewController.java b/blight-editor/src/main/java/de/blight/editor/toolset/anim/AnimPreviewController.java new file mode 100644 index 0000000..779a9f5 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/anim/AnimPreviewController.java @@ -0,0 +1,190 @@ +package de.blight.editor.toolset.anim; + +import de.blight.editor.ToolContext; +import de.blight.editor.ToolController; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.scene.control.*; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; + +import static de.blight.editor.toolset.UiHelper.*; + +public class AnimPreviewController implements ToolController { + + private final ToolContext ctx; + + private ImageView animPreviewView; + private ListView animClipListView; + private Label animPreviewStatusLabel; + private ComboBox animPreviewModelCombo; // immer null — dead-code-Stub, null-Guards schützen + private String animCurrentModelPath = null; + private double animPrevDragX, animPrevDragY; + + public AnimPreviewController(ToolContext ctx) { + this.ctx = ctx; + } + + @Override + public String toolId() { return "animpreview"; } + + @Override + public void activate() { + ctx.input().animPreviewSequenceQueue.clear(); + + animPreviewView = new ImageView(ctx.input().animPreviewImage); + animPreviewView.setPreserveRatio(true); + animPreviewView.setSmooth(true); + + StackPane previewPane = new StackPane(animPreviewView); + previewPane.setStyle("-fx-background-color: #2e2e38;"); + + previewPane.widthProperty().addListener((obs, ov, nv) -> { + animPreviewView.setFitWidth(nv.doubleValue()); + ctx.input().animPreviewW = Math.max(64, nv.intValue()); + }); + previewPane.heightProperty().addListener((obs, ov, nv) -> { + animPreviewView.setFitHeight(nv.doubleValue()); + ctx.input().animPreviewH = Math.max(64, nv.intValue()); + }); + previewPane.setOnMousePressed(e -> { + animPrevDragX = e.getSceneX(); + animPrevDragY = e.getSceneY(); + }); + previewPane.setOnMouseDragged(e -> { + double dx = e.getSceneX() - animPrevDragX; + double dy = e.getSceneY() - animPrevDragY; + animPrevDragX = e.getSceneX(); + animPrevDragY = e.getSceneY(); + ctx.input().animPreviewRotY += (float) (dx * 0.5); + ctx.input().animPreviewRotX = (float) Math.max(-80, Math.min(80, + ctx.input().animPreviewRotX - dy * 0.3)); + }); + previewPane.setOnScroll(e -> { + double factor = e.getDeltaY() > 0 ? 0.9 : 1.1; + ctx.input().animPreviewZoom = (float) Math.max(0.05, + Math.min(20.0, ctx.input().animPreviewZoom * factor)); + }); + + ctx.setCenterView().accept(previewPane); + ctx.setMainToolbar().accept(buildAnimPreviewToolBar()); + ctx.setRightPanel().accept(buildAnimPreviewPanel()); + } + + @Override + public void deactivate() {} + + @Override + public void onFrame() { + if (ctx.input().animPreviewResized) { + ctx.input().animPreviewResized = false; + if (animPreviewView != null) { + animPreviewView.setImage(ctx.input().animPreviewImage); + } + } + java.util.List newClips = ctx.input().animPreviewClips.getAndSet(null); + if (newClips != null && animClipListView != null) { + animClipListView.getItems().setAll(newClips); + if (!newClips.isEmpty()) animClipListView.getSelectionModel().selectFirst(); + } + String loadedPath = ctx.input().animPreviewLoadedPath.getAndSet(null); + if (loadedPath != null) { + animCurrentModelPath = loadedPath; + } + String animStatus = ctx.input().animPreviewStatus; + if (animStatus != null) { + ctx.input().animPreviewStatus = null; + if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText(animStatus); + } + String animOp = ctx.input().animOpStatus; + if (animOp != null) { + ctx.input().animOpStatus = null; + if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText(animOp); + } + } + + /** Wird vom EditorApp-Baum-Handler aufgerufen, wenn ein Modell angeklickt wird. */ + public void setStatusText(String text) { + if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText(text); + } + + /** Wird vom EditorApp-Baum-Handler aufgerufen, bevor ein neues Modell geladen wird. */ + public void clearClipList() { + if (animClipListView != null) animClipListView.getItems().clear(); + } + + private ToolBar buildAnimPreviewToolBar() { + Button backBtn = new Button("← Welteneditor"); + backBtn.setOnAction(e -> ctx.switchToWorld().run()); + Label label = new Label("Animations Viewer"); + label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); + ToolBar tb = new ToolBar(); + tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); + return tb; + } + + private VBox buildAnimPreviewPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + + // ── Animationen ──────────────────────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Animationen"), new Separator()); + animClipListView = new ListView<>(); + animClipListView.setPrefHeight(200); + animClipListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + + Button playBtn = new Button("▶ Abspielen"); + Button stopBtn = new Button("■ Stop"); + playBtn.setMaxWidth(Double.MAX_VALUE); + stopBtn.setMaxWidth(Double.MAX_VALUE); + playBtn.setStyle("-fx-font-weight: bold;"); + playBtn.setOnAction(e -> { + String clip = animClipListView.getSelectionModel().getSelectedItem(); + if (clip != null) ctx.input().animPreviewPlayClip = clip; + }); + stopBtn.setOnAction(e -> ctx.input().animPreviewPlayClip = ""); + animClipListView.setOnMouseClicked(e -> { + if (e.getClickCount() == 2) { + String clip = animClipListView.getSelectionModel().getSelectedItem(); + if (clip != null) ctx.input().animPreviewPlayClip = clip; + } + }); + HBox btnRow = new HBox(6, playBtn, stopBtn); + HBox.setHgrow(playBtn, Priority.ALWAYS); + HBox.setHgrow(stopBtn, Priority.ALWAYS); + inner.getChildren().addAll(animClipListView, btnRow); + + // ── Steuerung ────────────────────────────────────────────────────────── + inner.getChildren().addAll(new Separator(), sectionTitle("Steuerung"), new Separator()); + + CheckBox loopCB = new CheckBox("Loop"); + loopCB.setSelected(true); + loopCB.setOnAction(e -> ctx.input().animPreviewLoop = loopCB.isSelected()); + + Label speedLbl = new Label("Geschwindigkeit"); + Slider speedSlider = new Slider(0.1, 3.0, 1.0); + speedSlider.setShowTickMarks(true); + speedSlider.setShowTickLabels(true); + speedSlider.setMajorTickUnit(1.0); + speedSlider.valueProperty().addListener((obs, ov, nv) -> + ctx.input().animPreviewSpeed = nv.floatValue()); + inner.getChildren().addAll(loopCB, speedLbl, withField(speedSlider, "%.2f")); + + // ── Status ───────────────────────────────────────────────────────────── + inner.getChildren().add(new Separator()); + animPreviewStatusLabel = new Label(""); + animPreviewStatusLabel.setWrapText(true); + animPreviewStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #c00;"); + inner.getChildren().add(animPreviewStatusLabel); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/anim/AnimSetController.java b/blight-editor/src/main/java/de/blight/editor/toolset/anim/AnimSetController.java new file mode 100644 index 0000000..35749a7 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/anim/AnimSetController.java @@ -0,0 +1,1209 @@ +package de.blight.editor.toolset.anim; + +import de.blight.editor.ToolContext; +import de.blight.editor.ToolController; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; +import javafx.stage.FileChooser; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static de.blight.editor.toolset.UiHelper.*; + +public class AnimSetController implements ToolController { + + private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AnimSetController.class); + + private final ToolContext ctx; + + // Viewport-State + private ImageView animPreviewView; + private double animPrevDragX, animPrevDragY; + + // Status-Label (wird in buildAnimSetPanel() gesetzt) + private Label animPreviewStatusLabel; + + // Modell-Auswahl + private ComboBox animSetModelCombo; + + // Aktuelle Set-Daten (gesetzt in openWithSet vor activate()) + private String animSetCurrentName = null; + private Path animSetCurrentDir = null; + private de.blight.game.animation.AnimSet currentAnimSet = null; + private String animCurrentModelPath = null; + + // Panel-UI-Felder + private ListView animSetClipListView; + private ListView animSetActionListView; + private String animSetPendingPlayClip = null; + private javafx.scene.control.ListView animSetOffsetListView; + private javafx.collections.ObservableList animSetOffsetList = + javafx.collections.FXCollections.observableArrayList(); + private java.util.Map + animSetOffsets = new java.util.LinkedHashMap<>(); + private ListView animSetPartsListView; + private java.util.Map + editableSubClips = new java.util.LinkedHashMap<>(); + private ListView animSetSequenceListView; + private java.util.Map> + editableSequences = new java.util.LinkedHashMap<>(); + + public AnimSetController(ToolContext ctx) { + this.ctx = ctx; + } + + @Override + public String toolId() { return "animseteditor"; } + + /** Lädt das AnimSet und speichert es für activate(). Muss vor registry.activate() aufgerufen werden. */ + public void openWithSet(String relPath, Path absPath) { + String setName = absPath.getFileName().toString().replaceFirst("\\.animset\\.json$", ""); + Path setDir = absPath.getParent(); + de.blight.game.animation.AnimSet animSet; + try { + animSet = de.blight.game.animation.AnimSet.load(setDir, setName); + } catch (Exception ex) { + animSet = new de.blight.game.animation.AnimSet(); + ctx.setStatus().accept("Fehler beim Laden: " + ex.getMessage()); + } + animSetCurrentName = setName; + animSetCurrentDir = setDir; + currentAnimSet = animSet; + } + + @Override + public void activate() { + animPreviewView = new ImageView(ctx.input().animPreviewImage); + animPreviewView.setPreserveRatio(true); + animPreviewView.setSmooth(true); + + StackPane previewPane = new StackPane(animPreviewView); + previewPane.setStyle("-fx-background-color: #2e2e38;"); + previewPane.widthProperty().addListener((obs, ov, nv) -> { + animPreviewView.setFitWidth(nv.doubleValue()); + ctx.input().animPreviewW = Math.max(64, nv.intValue()); + }); + previewPane.heightProperty().addListener((obs, ov, nv) -> { + animPreviewView.setFitHeight(nv.doubleValue()); + ctx.input().animPreviewH = Math.max(64, nv.intValue()); + }); + previewPane.setOnMousePressed(e -> { animPrevDragX = e.getSceneX(); animPrevDragY = e.getSceneY(); }); + previewPane.setOnMouseDragged(e -> { + double dx = e.getSceneX() - animPrevDragX, dy = e.getSceneY() - animPrevDragY; + animPrevDragX = e.getSceneX(); animPrevDragY = e.getSceneY(); + ctx.input().animPreviewRotY += (float)(dx * 0.5); + ctx.input().animPreviewRotX = (float) Math.max(-80, Math.min(80, + ctx.input().animPreviewRotX - dy * 0.3)); + }); + previewPane.setOnScroll(e -> { + double factor = e.getDeltaY() > 0 ? 0.9 : 1.1; + ctx.input().animPreviewZoom = (float) Math.max(0.05, + Math.min(20.0, ctx.input().animPreviewZoom * factor)); + }); + + ctx.setCenterView().accept(previewPane); + ctx.setMainToolbar().accept(buildAnimSetToolBar(animSetCurrentName)); + ctx.setRightPanel().accept(buildAnimSetPanel(animSetCurrentName, animSetCurrentDir, currentAnimSet)); + ctx.setStatus().accept("AnimSet-Editor: " + animSetCurrentName); + } + + @Override + public void deactivate() {} + + @Override + public void onFrame() { + if (ctx.input().animPreviewResized) { + ctx.input().animPreviewResized = false; + if (animPreviewView != null) { + animPreviewView.setImage(ctx.input().animPreviewImage); + } + } + java.util.List newClips = ctx.input().animPreviewClips.getAndSet(null); + if (newClips != null && animSetPendingPlayClip != null) { + ctx.input().animPreviewPlayClip = animSetPendingPlayClip; + animSetPendingPlayClip = null; + } + String loadedPath = ctx.input().animPreviewLoadedPath.getAndSet(null); + if (loadedPath != null) { + animCurrentModelPath = loadedPath; + } + String animStatus = ctx.input().animPreviewStatus; + if (animStatus != null) { + ctx.input().animPreviewStatus = null; + if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText(animStatus); + } + String animOp = ctx.input().animOpStatus; + if (animOp != null) { + ctx.input().animOpStatus = null; + if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText(animOp); + } + } + + /** Zeigt den Picker-Dialog für AnimSets. Wird von EditorApp aufgerufen. */ + public void openCharacterEditorDialog() { + Path setsDir = ctx.assetRoot().resolve("Characters").resolve("sets"); + java.util.List available = new java.util.ArrayList<>(); + if (java.nio.file.Files.isDirectory(setsDir)) { + try (var walk = java.nio.file.Files.walk(setsDir, 1)) { + walk.filter(p -> p.toString().endsWith(".animset.json")) + .map(p -> p.getFileName().toString().replaceFirst("\\.animset\\.json$", "")) + .sorted() + .forEach(available::add); + } catch (IOException ignored) {} + } + + Dialog dlg = new Dialog<>(); + dlg.initOwner(ctx.stageSupplier().get()); + dlg.setTitle("Animationset Editor"); + dlg.setHeaderText("AnimSet auswählen oder neu anlegen"); + + ListView listView = new ListView<>(); + listView.getItems().addAll(available); + listView.setPrefHeight(200); + listView.setPrefWidth(320); + + ButtonType openType = new ButtonType("Öffnen", ButtonBar.ButtonData.OK_DONE); + ButtonType newType = new ButtonType("Neu…", ButtonBar.ButtonData.OTHER); + ButtonType cancelType = ButtonType.CANCEL; + dlg.getDialogPane().getButtonTypes().addAll(openType, newType, cancelType); + + javafx.scene.Node openNode = dlg.getDialogPane().lookupButton(openType); + openNode.setDisable(true); + listView.getSelectionModel().selectedItemProperty() + .addListener((obs, ov, nv) -> openNode.setDisable(nv == null)); + listView.setOnMouseClicked(e -> { + if (e.getClickCount() == 2 && listView.getSelectionModel().getSelectedItem() != null) { + dlg.setResult(listView.getSelectionModel().getSelectedItem()); + dlg.close(); + } + }); + + dlg.getDialogPane().setContent(listView); + dlg.setResultConverter(bt -> { + if (bt == openType) return listView.getSelectionModel().getSelectedItem(); + if (bt == newType) return "__NEW__"; + return null; + }); + + dlg.showAndWait().ifPresent(result -> { + if ("__NEW__".equals(result)) { + doCreateNewAnimSet(); + } else if (result != null) { + Path setFile = setsDir.resolve(result + ".animset.json"); + String relPath = ctx.assetRoot().relativize(setFile).toString().replace('\\', '/'); + openWithSet(relPath, setFile); + ctx.registry().activate(this); + } + }); + } + + /** Wird vom EditorApp-Kontext-Menü aufgerufen (z. B. Rechtsklick auf sets-Ordner). */ + public void createNewAnimSet() { + doCreateNewAnimSet(); + } + + private void doCreateNewAnimSet() { + TextInputDialog dlg = new TextInputDialog(); + dlg.initOwner(ctx.stageSupplier().get()); + dlg.setTitle("Neues Animations-Set"); + dlg.setHeaderText("Name des neuen Animations-Sets:"); + dlg.setContentText("Name:"); + dlg.showAndWait().ifPresent(raw -> { + String name = raw.trim(); + if (name.isBlank()) return; + Path setDir = ctx.assetRoot().resolve("Characters").resolve("sets"); + Path setFile = setDir.resolve(name + ".animset.json"); + try { + Files.createDirectories(setDir); + new de.blight.game.animation.AnimSet().save(setDir, name); + ctx.input().refreshAssets = true; + String relPath = ctx.assetRoot().relativize(setFile).toString().replace('\\', '/'); + openWithSet(relPath, setFile); + ctx.registry().activate(this); + } catch (IOException ex) { + ctx.setStatus().accept("Fehler beim Anlegen: " + ex.getMessage()); + } + }); + } + + /** Aufgerufen aus EditorApp-AnimationTimer wenn ein Character-Modell importiert wurde. */ + public void onCharModelImported(String relPath) { + if (animSetModelCombo != null && !animSetModelCombo.getItems().contains(relPath)) { + animSetModelCombo.getItems().add(relPath); + } + } + + /** Wird vom EditorApp-Kontext-Menü aufgerufen. */ + public void handleAnimationImport(javafx.stage.Window owner) { + FileChooser fc = new FileChooser(); + fc.setTitle("Animation importieren (FBX/GLB/GLTF)"); + fc.getExtensionFilters().add( + new FileChooser.ExtensionFilter("Animationen (FBX, GLTF, GLB)", "*.fbx", "*.gltf", "*.glb")); + var files = fc.showOpenMultipleDialog(owner); + if (files == null) return; + for (File file : files) { + archiveOriginal(file, "animations"); + ctx.input().animImportQueue.offer(file.getAbsolutePath()); + ctx.setStatus().accept("Importiere: " + file.getName() + " …"); + } + } + + /** Wird vom EditorApp-Kontext-Menü aufgerufen. */ + public void handleCharacterModelImport(javafx.stage.Window owner) { + FileChooser fc = new FileChooser(); + fc.setTitle("Character-Modell importieren (Mixamo T-Pose FBX)"); + fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("FBX-Modelle", "*.fbx")); + File file = fc.showOpenDialog(owner); + if (file == null) return; + archiveOriginal(file, "models"); + ctx.input().charModelImportQueue.offer(file.getAbsolutePath()); + if (animPreviewStatusLabel != null) { + animPreviewStatusLabel.setText("Importiere: " + file.getName() + " …"); + } + ctx.setStatus().accept("Importiere Character-Modell: " + file.getName() + " …"); + } + + // ── Toolbar & Panel ──────────────────────────────────────────────────────── + + private ToolBar buildAnimSetToolBar(String setName) { + Button backBtn = new Button("← Welteneditor"); + backBtn.setOnAction(e -> ctx.switchToWorld().run()); + Label label = new Label("AnimSet: " + setName); + label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); + Button newSetBtn = new Button("➕ Neues Set…"); + newSetBtn.setOnAction(e -> createNewAnimSet()); + ToolBar tb = new ToolBar(); + tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label, + new Separator(Orientation.VERTICAL), newSetBtn); + return tb; + } + + private VBox buildAnimSetPanel(String setName, Path setDir, + de.blight.game.animation.AnimSet animSet) { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + + // ── Modell-Auswahl für Vorschau ──────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Charakter-Modell"), new Separator()); + animSetModelCombo = new ComboBox<>(); + animSetModelCombo.setMaxWidth(Double.MAX_VALUE); + animSetModelCombo.setPromptText("j3o-Datei wählen…"); + { + Path base = ctx.assetRoot().resolve("Characters").resolve("Models"); + if (java.nio.file.Files.isDirectory(base)) { + try (var walk = java.nio.file.Files.walk(base)) { + walk.filter(p -> p.toString().endsWith(".j3o")) + .map(p -> ctx.assetRoot().relativize(p).toString().replace('\\', '/')) + .sorted() + .forEach(animSetModelCombo.getItems()::add); + } catch (IOException ignored) {} + } + } + if (animSet.getPreviewModelPath() != null && !animSet.getPreviewModelPath().isBlank()) { + animSetModelCombo.setValue(animSet.getPreviewModelPath()); + } + Button loadModelBtn = new Button("Laden"); + loadModelBtn.setMaxWidth(Double.MAX_VALUE); + loadModelBtn.setOnAction(e -> { + String path = animSetModelCombo.getValue(); + if (path == null || path.isBlank()) return; + ctx.input().animPreviewLoadPath = path; + if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…"); + animSet.setPreviewModelPath(path); + autoSaveAnimSet(); + }); + Button importCharBtn = new Button("Character-Modell importieren (FBX)…"); + importCharBtn.setMaxWidth(Double.MAX_VALUE); + importCharBtn.setOnAction(e -> handleCharacterModelImport(importCharBtn.getScene().getWindow())); + inner.getChildren().addAll(animSetModelCombo, loadModelBtn, importCharBtn); + + if (animSet.getPreviewModelPath() != null && !animSet.getPreviewModelPath().isBlank()) { + ctx.input().animPreviewLoadPath = animSet.getPreviewModelPath(); + if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…"); + } + + // ── Clips im Set ────────────────────────────────────────────────────── + inner.getChildren().addAll(new Separator(), sectionTitle("Clips im Set"), new Separator()); + + animSetClipListView = new ListView<>(); + animSetClipListView.getItems().addAll(animSet.getClips()); + animSetClipListView.setPrefHeight(180); + editableSubClips = new java.util.LinkedHashMap<>(animSet.getSubClips()); + editableSequences = new java.util.LinkedHashMap<>(animSet.getSequences()); + ctx.input().animPreviewClipOffsets.clear(); + for (var en : animSet.getAnimOffsets().entrySet()) { + de.blight.game.animation.AnimOffset o = en.getValue(); + ctx.input().animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz}); + } + java.util.Set locomotionActions = java.util.Set.of( + "WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT"); + ctx.input().animPreviewLocomotionClips.clear(); + animSet.getActionMap().forEach((action, clip) -> { + if (locomotionActions.contains(action)) ctx.input().animPreviewLocomotionClips.add(clip); + }); + + Path animRootDir = ctx.assetRoot().resolve("Characters"); + + Button addClipBtn = new Button("+ Hinzufügen…"); + Button removeClipBtn = new Button("- Entfernen"); + addClipBtn.setMaxWidth(Double.MAX_VALUE); + removeClipBtn.setMaxWidth(Double.MAX_VALUE); + removeClipBtn.setDisable(true); + + animSetClipListView.getSelectionModel().selectedItemProperty() + .addListener((obs, ov, nv) -> { + removeClipBtn.setDisable(nv == null); + if (ov != null && animSetOffsetList != null && !animSetOffsetList.isEmpty()) { + animSetOffsets.put(ov, animSetOffsetList.get(0)); + } else if (ov != null) { + animSetOffsets.remove(ov); + } + if (animSetOffsetList != null) { + animSetOffsetList.clear(); + if (nv != null) { + de.blight.game.animation.AnimOffset off = animSetOffsets.get(nv); + if (off != null) animSetOffsetList.setAll(off); + ctx.input().animPreviewPlayClip = nv; + } + } + if (animSetOffsetListView != null) animSetOffsetListView.setDisable(nv == null); + }); + + addClipBtn.setOnAction(e -> { + org.slf4j.Logger _log = org.slf4j.LoggerFactory.getLogger("AnimSetClipScan"); + _log.debug("[ClipScan] animRootDir={} exists={}", animRootDir, + java.nio.file.Files.isDirectory(animRootDir)); + java.util.List allClips = new java.util.ArrayList<>(); + if (java.nio.file.Files.isDirectory(animRootDir)) { + try (var walk = java.nio.file.Files.walk(animRootDir)) { + walk.filter(cp -> { + String name = cp.getFileName().toString(); + boolean isAnim = name.endsWith(".j3o") || name.endsWith(".glb") + || name.endsWith(".gltf") || name.endsWith(".fbx"); + java.nio.file.Path rel = animRootDir.relativize(cp); + boolean onlyClips = rel.startsWith("clips"); + _log.debug("[ClipScan] {} | isAnim={} onlyClips={}", cp, isAnim, onlyClips); + return isAnim && onlyClips; + }) + .map(cp -> cp.getFileName().toString().replaceFirst("\\.[^.]+$", "")) + .distinct() + .sorted() + .forEach(allClips::add); + } catch (IOException ex) { + _log.error("[ClipScan] Walk fehlgeschlagen", ex); + allClips.clear(); + } + } + java.util.List notYetAdded = allClips.stream() + .filter(c -> !animSetClipListView.getItems().contains(c)) + .collect(java.util.stream.Collectors.toList()); + + if (notYetAdded.isEmpty()) { + Alert info = new Alert(Alert.AlertType.INFORMATION); + info.setTitle("Keine Clips verfügbar"); + info.setHeaderText(null); + info.setContentText("Alle verfügbaren Clips sind bereits im Set enthalten."); + info.showAndWait(); + return; + } + + ListView list = new ListView<>(); + list.getItems().addAll(notYetAdded); + list.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + list.setPrefSize(460, 320); + list.setMinSize(460, 320); + list.getSelectionModel().selectFirst(); + + Dialog> addDlg = new Dialog<>(); + addDlg.initOwner(ctx.stageSupplier().get()); + addDlg.setTitle("Animation(en) hinzufügen"); + addDlg.setHeaderText("Verfügbare Clips (noch nicht im Set) — Mehrfachauswahl möglich:"); + addDlg.getDialogPane().setContent(list); + addDlg.getDialogPane().setPrefWidth(500); + ButtonType ok = new ButtonType("Hinzufügen", ButtonBar.ButtonData.OK_DONE); + addDlg.getDialogPane().getButtonTypes().addAll(ok, ButtonType.CANCEL); + addDlg.setResultConverter(bt -> bt == ok + ? new java.util.ArrayList<>(list.getSelectionModel().getSelectedItems()) + : null); + addDlg.showAndWait().ifPresent(selected -> { + for (String clip : selected) { + if (!animSetClipListView.getItems().contains(clip)) { + animSetClipListView.getItems().add(clip); + autoSaveAnimSet(); + } + } + }); + }); + + removeClipBtn.setOnAction(e -> { + String sel = animSetClipListView.getSelectionModel().getSelectedItem(); + if (sel == null) return; + animSetClipListView.getItems().remove(sel); + if (animSetActionListView != null) { + animSetActionListView.getItems().removeIf(it -> it.endsWith(" → " + sel)); + } + editableSubClips.entrySet().removeIf(en -> sel.equals(en.getValue().source)); + if (animSetPartsListView != null) animSetPartsListView.getItems().clear(); + autoSaveAnimSet(); + }); + + HBox clipBtns = new HBox(6, addClipBtn, removeClipBtn); + HBox.setHgrow(addClipBtn, Priority.ALWAYS); + HBox.setHgrow(removeClipBtn, Priority.ALWAYS); + inner.getChildren().addAll(animSetClipListView, clipBtns); + + // ── Clip-Teile (Sub-Clips) ───────────────────────────────────────────── + inner.getChildren().addAll(new Separator(), sectionTitle("Clip-Teile"), new Separator()); + + Label partsHint = new Label("Gewählten Clip in benannte Teile aufteilen. Jeder Teil erhält einen Namen und Zeitgrenzen (Sub-Clips)."); + partsHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); + partsHint.setWrapText(true); + + animSetPartsListView = new ListView<>(); + animSetPartsListView.setPrefHeight(120); + animSetPartsListView.setPlaceholder(new Label("Clip auswählen oder noch keine Teile definiert")); + animSetPartsListView.setDisable(true); + + Button addPartBtn = new Button("+ Teil hinzufügen…"); + Button removePartBtn = new Button("- Entfernen"); + addPartBtn.setMaxWidth(Double.MAX_VALUE); + removePartBtn.setMaxWidth(Double.MAX_VALUE); + addPartBtn.setDisable(true); + removePartBtn.setDisable(true); + + animSetPartsListView.getSelectionModel().selectedItemProperty() + .addListener((obs, ov, nv) -> removePartBtn.setDisable(nv == null)); + + animSetPartsListView.setOnMouseClicked(ev -> { + if (ev.getClickCount() == 2) { + String sel = animSetPartsListView.getSelectionModel().getSelectedItem(); + if (sel == null) return; + String selName = sel.substring(0, sel.indexOf(" [")); + de.blight.game.animation.AnimSet.SubClipDef def = editableSubClips.get(selName); + String source = animSetClipListView.getSelectionModel().getSelectedItem(); + if (def != null && source != null) showPartDialog(source, selName, def); + } + }); + + animSetClipListView.getSelectionModel().selectedItemProperty() + .addListener((obs, ov, nv) -> { + animSetPartsListView.getItems().clear(); + boolean hasClip = nv != null; + animSetPartsListView.setDisable(!hasClip); + addPartBtn.setDisable(!hasClip); + if (hasClip) { + for (var en : editableSubClips.entrySet()) { + if (nv.equals(en.getValue().source)) { + animSetPartsListView.getItems().add( + formatPartEntry(en.getKey(), en.getValue())); + } + } + } + }); + + addPartBtn.setOnAction(e -> { + String source = animSetClipListView.getSelectionModel().getSelectedItem(); + if (source != null) showPartDialog(source, null, null); + }); + + removePartBtn.setOnAction(e -> { + String sel = animSetPartsListView.getSelectionModel().getSelectedItem(); + if (sel == null) return; + String name = sel.substring(0, sel.indexOf(" [")); + editableSubClips.remove(name); + animSetPartsListView.getItems().remove(sel); + autoSaveAnimSet(); + }); + + HBox partsBtns = new HBox(6, addPartBtn, removePartBtn); + HBox.setHgrow(addPartBtn, Priority.ALWAYS); + HBox.setHgrow(removePartBtn, Priority.ALWAYS); + inner.getChildren().addAll(partsHint, animSetPartsListView, partsBtns); + + // ── Aktions-Zuordnung ────────────────────────────────────────────────── + inner.getChildren().addAll(new Separator(), sectionTitle("Aktions-Zuordnung"), new Separator()); + + animSetActionListView = new ListView<>(); + animSetActionListView.setPrefHeight(160); + for (var entry : animSet.getActionMap().entrySet()) { + animSetActionListView.getItems().add(entry.getKey() + " → " + entry.getValue()); + } + + Button addActionBtn = new Button("+ Zuordnen…"); + Button removeActionBtn = new Button("- Entfernen"); + addActionBtn.setMaxWidth(Double.MAX_VALUE); + removeActionBtn.setMaxWidth(Double.MAX_VALUE); + removeActionBtn.setDisable(true); + + animSetActionListView.getSelectionModel().selectedItemProperty() + .addListener((obs, ov, nv) -> removeActionBtn.setDisable(nv == null)); + + addActionBtn.setOnAction(e -> showAddActionToSetDialog()); + removeActionBtn.setOnAction(e -> { + String sel = animSetActionListView.getSelectionModel().getSelectedItem(); + if (sel != null) { + animSetActionListView.getItems().remove(sel); + autoSaveAnimSet(); + } + }); + + HBox actionBtns = new HBox(6, addActionBtn, removeActionBtn); + HBox.setHgrow(addActionBtn, Priority.ALWAYS); + HBox.setHgrow(removeActionBtn, Priority.ALWAYS); + inner.getChildren().addAll(animSetActionListView, actionBtns); + + // ── Anim-Offsets ─────────────────────────────────────────────────────── + inner.getChildren().addAll(new Separator(), sectionTitle("Anim-Offsets"), new Separator()); + + Label kfHint = new Label("TX/TZ = charakter-lokal (seitlich/vorwärts), TY = Welt-Y (hoch/runter). RX/RY/RZ in Grad, additiv zur Startrotation."); + kfHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); + kfHint.setWrapText(true); + + animSetOffsets = new java.util.LinkedHashMap<>(animSet.getAnimOffsets()); + animSetOffsetList = javafx.collections.FXCollections.observableArrayList(); + animSetOffsetList.addListener((javafx.collections.ListChangeListener) + change -> updateAnimPreviewOffset()); + + animSetOffsetListView = new javafx.scene.control.ListView<>(animSetOffsetList); + animSetOffsetListView.setPrefHeight(60); + animSetOffsetListView.setPlaceholder(new Label("Kein Offset – [+ Offset] zum Setzen")); + animSetOffsetListView.setCellFactory(lv -> new javafx.scene.control.ListCell<>() { + @Override protected void updateItem(de.blight.game.animation.AnimOffset off, boolean empty) { + super.updateItem(off, empty); + if (empty || off == null) { setText(null); return; } + setText(String.format("TX%+.3f TY%+.3f TZ%+.3f | RX%+.1f° RY%+.1f° RZ%+.1f°", + off.tx, off.ty, off.tz, off.rx, off.ry, off.rz)); + } + }); + animSetOffsetListView.setOnMouseClicked(ev -> { + if (ev.getClickCount() == 2) { + de.blight.game.animation.AnimOffset sel = + animSetOffsetListView.getSelectionModel().getSelectedItem(); + if (sel != null) showAnimOffsetDialog(sel); + } + }); + animSetOffsetListView.setDisable(true); + + Button addKfBtn = new Button("+ Offset"); + Button removeKfBtn = new Button("- Entfernen"); + addKfBtn.setMaxWidth(Double.MAX_VALUE); + removeKfBtn.setMaxWidth(Double.MAX_VALUE); + addKfBtn.setDisable(true); + removeKfBtn.setDisable(true); + animSetOffsetListView.getSelectionModel().selectedItemProperty() + .addListener((obs, ov, nv) -> removeKfBtn.setDisable(nv == null)); + animSetClipListView.getSelectionModel().selectedItemProperty() + .addListener((obs, ov, nv) -> addKfBtn.setDisable(nv == null)); + + addKfBtn.setOnAction(e -> showAnimOffsetDialog(null)); + removeKfBtn.setOnAction(e -> { + de.blight.game.animation.AnimOffset sel = + animSetOffsetListView.getSelectionModel().getSelectedItem(); + if (sel != null) { + animSetOffsetList.remove(sel); + autoSaveAnimSet(); + } + }); + + HBox kfBtns = new HBox(6, addKfBtn, removeKfBtn); + HBox.setHgrow(addKfBtn, Priority.ALWAYS); + HBox.setHgrow(removeKfBtn, Priority.ALWAYS); + inner.getChildren().addAll(kfHint, animSetOffsetListView, kfBtns); + + // ── Abläufe ──────────────────────────────────────────────────────────── + inner.getChildren().addAll(new Separator(), sectionTitle("Abläufe"), new Separator()); + + Label seqHint = new Label("Aktionen aus der Zuordnung zu Abläufen zusammenstellen und testen."); + seqHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); + seqHint.setWrapText(true); + + animSetSequenceListView = new ListView<>(); + animSetSequenceListView.setPrefHeight(120); + animSetSequenceListView.setPlaceholder(new Label("Noch keine Abläufe definiert")); + for (var en : editableSequences.entrySet()) { + animSetSequenceListView.getItems().add(formatSequenceEntry(en.getKey(), en.getValue())); + } + + Button addSeqBtn = new Button("+ Ablauf…"); + Button removeSeqBtn = new Button("- Entfernen"); + addSeqBtn.setMaxWidth(Double.MAX_VALUE); + removeSeqBtn.setMaxWidth(Double.MAX_VALUE); + removeSeqBtn.setDisable(true); + + animSetSequenceListView.getSelectionModel().selectedItemProperty() + .addListener((obs, ov, nv) -> { + removeSeqBtn.setDisable(nv == null); + addSeqBtn.setText(nv == null ? "+ Ablauf…" : "Bearbeiten…"); + }); + + animSetSequenceListView.setOnMouseClicked(ev -> { + String sel = animSetSequenceListView.getSelectionModel().getSelectedItem(); + if (sel == null) return; + if (ev.getClickCount() == 2) playSequence(sel); + }); + + addSeqBtn.setOnAction(e -> { + String sel = animSetSequenceListView.getSelectionModel().getSelectedItem(); + if (sel == null) { + showSequenceBuilderDialog(null, null); + } else { + String seqName = extractSequenceName(sel); + showSequenceBuilderDialog(seqName, new java.util.ArrayList<>( + editableSequences.getOrDefault(seqName, java.util.List.of()))); + } + }); + + removeSeqBtn.setOnAction(e -> { + String sel = animSetSequenceListView.getSelectionModel().getSelectedItem(); + if (sel == null) return; + String seqName = extractSequenceName(sel); + editableSequences.remove(seqName); + animSetSequenceListView.getItems().remove(sel); + autoSaveAnimSet(); + }); + + HBox seqBtns = new HBox(6, addSeqBtn, removeSeqBtn); + HBox.setHgrow(addSeqBtn, Priority.ALWAYS); + HBox.setHgrow(removeSeqBtn, Priority.ALWAYS); + inner.getChildren().addAll(seqHint, animSetSequenceListView, seqBtns); + + // ── Vorschau ─────────────────────────────────────────────────────────── + inner.getChildren().addAll(new Separator(), sectionTitle("Vorschau"), new Separator()); + + Label previewHint = new Label("Clip auswählen → Abspielen (oder Doppelklick)"); + previewHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); + previewHint.setWrapText(true); + + Button playBtn = new Button("▶ Abspielen"); + Button stopBtn = new Button("■ Stop"); + playBtn.setMaxWidth(Double.MAX_VALUE); + stopBtn.setMaxWidth(Double.MAX_VALUE); + + animSetClipListView.setOnMouseClicked(e -> { + if (e.getClickCount() == 2) previewSelectedClip(); + }); + playBtn.setOnAction(e -> previewSelectedClip()); + stopBtn.setOnAction(e -> ctx.input().animPreviewPlayClip = ""); + + HBox previewBtns = new HBox(6, playBtn, stopBtn); + HBox.setHgrow(playBtn, Priority.ALWAYS); + HBox.setHgrow(stopBtn, Priority.ALWAYS); + + CheckBox loopCB = new CheckBox("Loop"); + loopCB.setSelected(true); + loopCB.setOnAction(ev -> ctx.input().animPreviewLoop = loopCB.isSelected()); + + Slider speedSlider = new Slider(0.1, 3.0, 1.0); + speedSlider.setShowTickMarks(true); + speedSlider.setShowTickLabels(true); + speedSlider.setMajorTickUnit(1.0); + speedSlider.valueProperty().addListener((obs, ov, nv) -> + ctx.input().animPreviewSpeed = nv.floatValue()); + + animPreviewStatusLabel = new Label("Kein Modell geladen"); + animPreviewStatusLabel.setWrapText(true); + animPreviewStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); + + inner.getChildren().addAll(previewHint, previewBtns, loopCB, + new Label("Tempo:"), speedSlider, animPreviewStatusLabel); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + // ── Offset-Dialog ────────────────────────────────────────────────────────── + + private void showAnimOffsetDialog(de.blight.game.animation.AnimOffset existing) { + boolean isAdd = (existing == null); + float initTX = isAdd ? 0f : existing.tx; + float initTY = isAdd ? 0f : existing.ty; + float initTZ = isAdd ? 0f : existing.tz; + float initRX = isAdd ? 0f : existing.rx; + float initRY = isAdd ? 0f : existing.ry; + float initRZ = isAdd ? 0f : existing.rz; + + Spinner spTX = new Spinner<>(-10.0, 10.0, initTX, 0.05); + Spinner spTY = new Spinner<>(-10.0, 10.0, initTY, 0.05); + Spinner spTZ = new Spinner<>(-10.0, 10.0, initTZ, 0.05); + Spinner spRX = new Spinner<>(-360.0, 360.0, initRX, 1.0); + Spinner spRY = new Spinner<>(-360.0, 360.0, initRY, 1.0); + Spinner spRZ = new Spinner<>(-360.0, 360.0, initRZ, 1.0); + for (Spinner sp : java.util.List.of(spTX, spTY, spTZ, spRX, spRY, spRZ)) { + sp.setEditable(true); + sp.setMaxWidth(Double.MAX_VALUE); + } + + String[][] rows = {{"TX (m):"}, {"TY (m):"}, {"TZ (m):"}, {"RX (°):"}, {"RY (°):"}, {"RZ (°):"}}; + Spinner[] sps = {spTX, spTY, spTZ, spRX, spRY, spRZ}; + javafx.scene.layout.GridPane kfGrid = new javafx.scene.layout.GridPane(); + kfGrid.setHgap(8); kfGrid.setVgap(5); + for (int i = 0; i < sps.length; i++) { + kfGrid.add(new Label(rows[i][0]), 0, i); + kfGrid.add(sps[i], 1, i); + } + javafx.scene.layout.ColumnConstraints kfCC = new javafx.scene.layout.ColumnConstraints(); + kfCC.setHgrow(Priority.ALWAYS); + kfGrid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), kfCC); + + Dialog kfDlg = new Dialog<>(); + kfDlg.initOwner(ctx.stageSupplier().get()); + kfDlg.setTitle(isAdd ? "Offset hinzufügen" : "Offset bearbeiten"); + ButtonType okKf = new ButtonType( + isAdd ? "Hinzufügen" : "Übernehmen", ButtonBar.ButtonData.OK_DONE); + kfDlg.getDialogPane().getButtonTypes().addAll(okKf, ButtonType.CANCEL); + kfDlg.getDialogPane().setContent(kfGrid); + kfDlg.showAndWait().ifPresent(bt -> { + if (bt != okKf) return; + if (isAdd) { + de.blight.game.animation.AnimOffset off = new de.blight.game.animation.AnimOffset( + spTX.getValue().floatValue(), spTY.getValue().floatValue(), + spTZ.getValue().floatValue(), spRX.getValue().floatValue(), + spRY.getValue().floatValue(), spRZ.getValue().floatValue()); + animSetOffsetList.setAll(off); + } else { + existing.tx = spTX.getValue().floatValue(); + existing.ty = spTY.getValue().floatValue(); + existing.tz = spTZ.getValue().floatValue(); + existing.rx = spRX.getValue().floatValue(); + existing.ry = spRY.getValue().floatValue(); + existing.rz = spRZ.getValue().floatValue(); + } + if (animSetOffsetListView != null) animSetOffsetListView.refresh(); + autoSaveAnimSet(); + }); + } + + // ── Sequenz-Helpers ──────────────────────────────────────────────────────── + + private String formatSequenceEntry(String name, java.util.List actions) { + return name + " (" + actions.size() + " Aktionen: " + String.join(" → ", actions) + ")"; + } + + private String extractSequenceName(String entry) { + int idx = entry.indexOf(" ("); + return idx >= 0 ? entry.substring(0, idx) : entry; + } + + private void playSequence(String listEntry) { + String seqName = extractSequenceName(listEntry); + java.util.List actions = editableSequences.get(seqName); + if (actions == null || actions.isEmpty()) return; + + java.util.Map actionMap = new java.util.LinkedHashMap<>(); + if (animSetActionListView != null) { + for (String it : animSetActionListView.getItems()) { + String[] parts = it.split(" → ", 2); + if (parts.length == 2) actionMap.put(parts[0], parts[1]); + } + } + java.util.List clips = new java.util.ArrayList<>(); + for (String action : actions) { + String clip = actionMap.get(action); + if (clip != null) clips.add(clip); + else log.warn("[Sequenz] Keine Clip-Zuordnung für Aktion '{}'", action); + } + if (clips.isEmpty()) { + Alert err = new Alert(Alert.AlertType.WARNING); + err.initOwner(ctx.stageSupplier().get()); + err.setHeaderText("Keine Clips für Sequenz"); + err.setContentText("Die Aktionen des Ablaufs sind keinen Clips zugeordnet."); + err.showAndWait(); + return; + } + + ctx.input().animPreviewSequenceQueue.clear(); + ctx.input().animPreviewLoop = false; + animSetPendingPlayClip = clips.get(0); + ctx.input().animImportQueue.offer(findAnimClipPath(clips.get(0))); + for (int i = 1; i < clips.size(); i++) { + ctx.input().animPreviewSequenceQueue.add(clips.get(i)); + ctx.input().animImportQueue.offer(findAnimClipPath(clips.get(i))); + } + if (animPreviewStatusLabel != null) { + animPreviewStatusLabel.setText("Lade Sequenz \"" + seqName + "\"…"); + } + } + + private void showSequenceBuilderDialog(String existingName, java.util.List existingActions) { + java.util.List availableActions = new java.util.ArrayList<>(); + if (animSetActionListView != null) { + for (String it : animSetActionListView.getItems()) { + String[] parts = it.split(" → ", 2); + if (parts.length == 2) availableActions.add(parts[0]); + } + } + if (availableActions.isEmpty()) { + Alert err = new Alert(Alert.AlertType.WARNING); + err.initOwner(ctx.stageSupplier().get()); + err.setHeaderText("Keine Aktionen"); + err.setContentText("Zuerst in der Aktions-Zuordnung Aktionen einem Clip zuweisen."); + err.showAndWait(); + return; + } + + javafx.scene.control.TextField nameFld = new javafx.scene.control.TextField( + existingName != null ? existingName : ""); + nameFld.setPromptText("Name des Ablaufs…"); + + ListView leftList = new ListView<>(); + leftList.getItems().addAll(availableActions); + leftList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + leftList.setPrefSize(200, 240); + + ListView rightList = new ListView<>(); + if (existingActions != null) rightList.getItems().addAll(existingActions); + rightList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + rightList.setPrefSize(200, 240); + + Button addBtn = new Button("→"); + Button removeBtn = new Button("×"); + addBtn.setMaxWidth(Double.MAX_VALUE); + removeBtn.setMaxWidth(Double.MAX_VALUE); + addBtn.setStyle("-fx-font-size: 14;"); + removeBtn.setStyle("-fx-font-size: 14;"); + addBtn.setOnAction(ev -> rightList.getItems().addAll( + new java.util.ArrayList<>(leftList.getSelectionModel().getSelectedItems()))); + removeBtn.setOnAction(ev -> rightList.getItems().removeAll( + new java.util.ArrayList<>(rightList.getSelectionModel().getSelectedItems()))); + + VBox midBtns = new VBox(8, addBtn, removeBtn); + midBtns.setAlignment(Pos.CENTER); + + javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane(); + grid.setHgap(8); grid.setVgap(6); + grid.add(new Label("Verfügbare Aktionen:"), 0, 0); + grid.add(new Label("Ablauf:"), 2, 0); + grid.add(leftList, 0, 1); + grid.add(midBtns, 1, 1); + grid.add(rightList, 2, 1); + javafx.scene.layout.ColumnConstraints ccFill = new javafx.scene.layout.ColumnConstraints(); + ccFill.setHgrow(Priority.ALWAYS); + javafx.scene.layout.ColumnConstraints ccFixed = new javafx.scene.layout.ColumnConstraints(48); + grid.getColumnConstraints().addAll(ccFill, ccFixed, ccFill); + + VBox content = new VBox(8, new Label("Ablauf-Name:"), nameFld, grid); + content.setPadding(new Insets(4)); + + ButtonType ok = new ButtonType("Speichern", ButtonBar.ButtonData.OK_DONE); + Dialog dlg = new Dialog<>(); + dlg.initOwner(ctx.stageSupplier().get()); + dlg.setTitle(existingName == null ? "Ablauf erstellen" : "Ablauf bearbeiten"); + dlg.getDialogPane().setContent(content); + dlg.getDialogPane().setPrefWidth(520); + dlg.getDialogPane().getButtonTypes().addAll(ok, ButtonType.CANCEL); + dlg.setResultConverter(bt -> bt); + + dlg.showAndWait().ifPresent(bt -> { + if (bt != ok) return; + String name = nameFld.getText().trim(); + if (name.isEmpty()) return; + java.util.List actions = new java.util.ArrayList<>(rightList.getItems()); + + if (existingName != null && !existingName.equals(name)) { + editableSequences.remove(existingName); + animSetSequenceListView.getItems().removeIf( + it -> extractSequenceName(it).equals(existingName)); + } + editableSequences.put(name, actions); + + String newEntry = formatSequenceEntry(name, actions); + boolean replaced = false; + if (existingName != null) { + for (int i = 0; i < animSetSequenceListView.getItems().size(); i++) { + if (extractSequenceName(animSetSequenceListView.getItems().get(i)).equals(name)) { + animSetSequenceListView.getItems().set(i, newEntry); + replaced = true; + break; + } + } + } + if (!replaced) animSetSequenceListView.getItems().add(newEntry); + autoSaveAnimSet(); + }); + } + + // ── Aktions-Dialog ───────────────────────────────────────────────────────── + + private void showAddActionToSetDialog() { + if (animSetClipListView == null || animSetClipListView.getItems().isEmpty()) { + ctx.setStatus().accept("Keine Clips im Set — erst Clips hinzufügen."); + return; + } + java.util.Set usedActions = new java.util.HashSet<>(); + if (animSetActionListView != null) { + for (String it : animSetActionListView.getItems()) { + usedActions.add(it.split(" → ")[0]); + } + } + java.util.List available = + java.util.Arrays.stream(de.blight.game.animation.AnimationAction.values()) + .filter(a -> !usedActions.contains(a.name())) + .collect(java.util.stream.Collectors.toList()); + if (available.isEmpty()) { + ctx.setStatus().accept("Alle Aktionen bereits zugeordnet."); + return; + } + + ComboBox actionCombo = new ComboBox<>(); + actionCombo.getItems().addAll(available); + javafx.util.Callback, + javafx.scene.control.ListCell> cf = + lv -> new javafx.scene.control.ListCell<>() { + @Override protected void updateItem(de.blight.game.animation.AnimationAction it, + boolean empty) { + super.updateItem(it, empty); + setText(empty || it == null ? null : it.displayName() + " (" + it.name() + ")"); + } + }; + actionCombo.setCellFactory(cf); + actionCombo.setButtonCell(cf.call(null)); + actionCombo.setMaxWidth(Double.MAX_VALUE); + actionCombo.getSelectionModel().selectFirst(); + + ComboBox clipCombo = new ComboBox<>(); + java.util.List allClipNames = new java.util.ArrayList<>(animSetClipListView.getItems()); + for (String subName : editableSubClips.keySet()) { + if (!allClipNames.contains(subName)) allClipNames.add(subName); + } + clipCombo.getItems().addAll(allClipNames); + clipCombo.setMaxWidth(Double.MAX_VALUE); + clipCombo.getSelectionModel().selectFirst(); + + javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane(); + grid.setHgap(8); grid.setVgap(6); + grid.add(new Label("Aktion:"), 0, 0); grid.add(actionCombo, 1, 0); + grid.add(new Label("Clip:"), 0, 1); grid.add(clipCombo, 1, 1); + javafx.scene.layout.ColumnConstraints cc = new javafx.scene.layout.ColumnConstraints(); + cc.setHgrow(Priority.ALWAYS); + grid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), cc); + + Dialog dlg = new Dialog<>(); + dlg.initOwner(ctx.stageSupplier().get()); + dlg.setTitle("Aktion zuordnen"); + ButtonType ok = new ButtonType("Hinzufügen", ButtonBar.ButtonData.OK_DONE); + dlg.getDialogPane().getButtonTypes().addAll(ok, ButtonType.CANCEL); + dlg.getDialogPane().setContent(grid); + dlg.showAndWait().ifPresent(bt -> { + if (bt != ok) return; + var action = actionCombo.getValue(); + var clip = clipCombo.getValue(); + if (action != null && clip != null) { + String entry = action.name() + " → " + clip; + if (!animSetActionListView.getItems().contains(entry)) { + animSetActionListView.getItems().add(entry); + autoSaveAnimSet(); + } + } + }); + } + + // ── Sub-Clip-Dialog ──────────────────────────────────────────────────────── + + private static String formatPartEntry(String name, + de.blight.game.animation.AnimSet.SubClipDef def) { + return String.format("%s [%.3fs – %.3fs]", name, def.start, def.end); + } + + private void showPartDialog(String source, String existingName, + de.blight.game.animation.AnimSet.SubClipDef existing) { + boolean isEdit = existingName != null && existing != null; + + javafx.scene.control.TextField nameField = new javafx.scene.control.TextField( + isEdit ? existingName : ""); + nameField.setPromptText("Teil-Name (z. B. sit_down_bench)"); + nameField.setDisable(isEdit); + + Spinner startSpinner = new Spinner<>(0.0, 9999.0, + isEdit ? existing.start : 0.0, 0.033); + startSpinner.setEditable(true); + startSpinner.setMaxWidth(Double.MAX_VALUE); + + Spinner endSpinner = new Spinner<>(0.0, 9999.0, + isEdit ? existing.end : 1.0, 0.033); + endSpinner.setEditable(true); + endSpinner.setMaxWidth(Double.MAX_VALUE); + + javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane(); + grid.setHgap(8); grid.setVgap(6); + grid.add(new Label("Name:"), 0, 0); grid.add(nameField, 1, 0); + grid.add(new Label("Start:"), 0, 1); grid.add(startSpinner, 1, 1); + grid.add(new Label("Ende:"), 0, 2); grid.add(endSpinner, 1, 2); + javafx.scene.layout.ColumnConstraints cc = new javafx.scene.layout.ColumnConstraints(); + cc.setHgrow(Priority.ALWAYS); + grid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), cc); + + Dialog dlg = new Dialog<>(); + dlg.initOwner(ctx.stageSupplier().get()); + dlg.setTitle(isEdit ? "Clip-Teil bearbeiten" : "Clip-Teil hinzufügen"); + dlg.setHeaderText((isEdit ? "Bearbeiten: " : "Neuer Teil für: ") + source); + ButtonType ok = new ButtonType( + isEdit ? "Übernehmen" : "Hinzufügen", ButtonBar.ButtonData.OK_DONE); + dlg.getDialogPane().getButtonTypes().addAll(ok, ButtonType.CANCEL); + dlg.getDialogPane().setContent(grid); + + javafx.scene.Node okNode = dlg.getDialogPane().lookupButton(ok); + Runnable validate = () -> { + boolean valid = !nameField.getText().isBlank() + && endSpinner.getValue() > startSpinner.getValue(); + okNode.setDisable(!valid); + }; + validate.run(); + nameField.textProperty().addListener((obs, ov, nv) -> validate.run()); + startSpinner.valueProperty().addListener((obs, ov, nv) -> validate.run()); + endSpinner.valueProperty().addListener((obs, ov, nv) -> validate.run()); + + dlg.showAndWait().ifPresent(bt -> { + if (bt != ok) return; + String name = isEdit ? existingName : nameField.getText().trim(); + if (name.isBlank()) return; + de.blight.game.animation.AnimSet.SubClipDef def = + new de.blight.game.animation.AnimSet.SubClipDef(); + def.source = source; + def.start = startSpinner.getValue().floatValue(); + def.end = endSpinner.getValue().floatValue(); + editableSubClips.put(name, def); + if (animSetPartsListView != null) { + animSetPartsListView.getItems().removeIf(s -> s.startsWith(name + " [")); + animSetPartsListView.getItems().add(formatPartEntry(name, def)); + } + autoSaveAnimSet(); + }); + } + + // ── Clip-Vorschau ────────────────────────────────────────────────────────── + + private void previewSelectedClip() { + if (animSetClipListView == null) return; + String clip = animSetClipListView.getSelectionModel().getSelectedItem(); + if (clip == null) return; + if (animCurrentModelPath == null || animCurrentModelPath.isBlank()) { + if (animPreviewStatusLabel != null) { + animPreviewStatusLabel.setText("Erst ein Charakter-Modell laden."); + } + return; + } + animSetPendingPlayClip = clip; + ctx.input().animImportQueue.offer(findAnimClipPath(clip)); + if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade Clip " + clip + "…"); + } + + private String findAnimClipPath(String clipName) { + Path charDir = ctx.assetRoot().resolve("Characters"); + for (String ext : new String[]{".j3o", ".glb", ".gltf", ".fbx"}) { + if (java.nio.file.Files.exists(charDir.resolve("clips").resolve(clipName + ext))) { + return "Characters/clips/" + clipName + ext; + } + if (java.nio.file.Files.exists(charDir.resolve(clipName + ext))) { + return "Characters/" + clipName + ext; + } + } + return "Characters/clips/" + clipName + ".j3o"; + } + + private void updateAnimPreviewOffset() { + if (animSetOffsetList == null || animSetOffsetList.isEmpty()) { + ctx.input().animPreviewOffsetTx = 0f; + ctx.input().animPreviewOffsetTy = 0f; + ctx.input().animPreviewOffsetTz = 0f; + ctx.input().animPreviewOffsetActive = false; + } else { + de.blight.game.animation.AnimOffset off = animSetOffsetList.get(0); + ctx.input().animPreviewOffsetTx = off.tx; + ctx.input().animPreviewOffsetTy = off.ty; + ctx.input().animPreviewOffsetTz = off.tz; + ctx.input().animPreviewOffsetActive = true; + } + } + + // ── AutoSave & Save ──────────────────────────────────────────────────────── + + private void autoSaveAnimSet() { + if (animSetCurrentName != null && animSetCurrentDir != null) { + saveCurrentAnimSet(animSetCurrentName, animSetCurrentDir); + } + } + + private void saveCurrentAnimSet(String setName, Path setDir) { + if (animSetClipListView == null) return; + de.blight.game.animation.AnimSet animSet = new de.blight.game.animation.AnimSet(); + animSet.setClips(new java.util.ArrayList<>(animSetClipListView.getItems())); + java.util.Map actionMap = new java.util.LinkedHashMap<>(); + if (animSetActionListView != null) { + for (String it : animSetActionListView.getItems()) { + String[] parts = it.split(" → ", 2); + if (parts.length == 2) actionMap.put(parts[0], parts[1]); + } + } + animSet.setActionMap(actionMap); + if (animSetClipListView != null) { + String selClip = animSetClipListView.getSelectionModel().getSelectedItem(); + if (selClip != null) { + if (!animSetOffsetList.isEmpty()) { + animSetOffsets.put(selClip, animSetOffsetList.get(0)); + } else { + animSetOffsets.remove(selClip); + } + } + } + java.util.Map offsetFinal = + new java.util.LinkedHashMap<>(); + for (var entry : animSetOffsets.entrySet()) { + if (entry.getValue() != null) offsetFinal.put(entry.getKey(), entry.getValue()); + } + animSet.setAnimOffsets(offsetFinal); + animSet.setSubClips(new java.util.LinkedHashMap<>(editableSubClips)); + animSet.setSequences(new java.util.LinkedHashMap<>(editableSequences)); + ctx.input().animPreviewClipOffsets.clear(); + for (var en : offsetFinal.entrySet()) { + de.blight.game.animation.AnimOffset o = en.getValue(); + ctx.input().animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz}); + } + java.util.Set locoActions = java.util.Set.of( + "WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT"); + ctx.input().animPreviewLocomotionClips.clear(); + animSet.getActionMap().forEach((action, clip) -> { + if (locoActions.contains(action)) ctx.input().animPreviewLocomotionClips.add(clip); + }); + if (animSetModelCombo != null && animSetModelCombo.getValue() != null + && !animSetModelCombo.getValue().isBlank()) { + animSet.setPreviewModelPath(animSetModelCombo.getValue()); + } + try { + animSet.save(setDir, setName); + ctx.setStatus().accept("AnimSet gespeichert: " + setName + ".animset.json"); + ctx.input().refreshAssets = true; + } catch (IOException ex) { + ctx.setStatus().accept("Fehler beim Speichern: " + ex.getMessage()); + } + } + + // ── Import-Hilfsmethoden ─────────────────────────────────────────────────── + + 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()); + } + } +}