Zwischenstand commited
This commit is contained in:
@@ -190,7 +190,6 @@ public class EditorApp extends Application {
|
||||
private ListView<String> animClipListView;
|
||||
private Label animPreviewStatusLabel;
|
||||
private ComboBox<String> animPreviewModelCombo;
|
||||
private ComboBox<String> addAnimComboField;
|
||||
private double animPrevDragX, animPrevDragY;
|
||||
// Cache: Menge aller j3o-Pfade die ein Skelett besitzen (null = Scan noch nicht abgeschlossen)
|
||||
private java.util.Set<String> skeletalModelPaths = null;
|
||||
@@ -413,6 +412,7 @@ public class EditorApp extends Application {
|
||||
private TreeItem<String> jmeModelsNode;
|
||||
private TreeItem<String> jmeTexturesNode;
|
||||
private TreeItem<String> animationsNode;
|
||||
private TreeItem<String> charactersNode;
|
||||
private TreeItem<String> itemsNode;
|
||||
|
||||
// ── JavaFX Entry-Point ───────────────────────────────────────────────────
|
||||
@@ -620,8 +620,8 @@ public class EditorApp extends Application {
|
||||
}
|
||||
if (input.animImportCompleted) {
|
||||
input.animImportCompleted = false;
|
||||
refreshAddAnimCombo(addAnimComboField);
|
||||
refreshCategoryNode(animationsNode, shouldPreserveExpansion());
|
||||
if (charactersNode != null) refreshCategoryNode(charactersNode, shouldPreserveExpansion());
|
||||
if (animationsNode != null) refreshCategoryNode(animationsNode, shouldPreserveExpansion());
|
||||
}
|
||||
|
||||
String animOp = input.animOpStatus;
|
||||
@@ -630,6 +630,33 @@ public class EditorApp extends Application {
|
||||
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText(animOp);
|
||||
}
|
||||
|
||||
SharedInput.CharModelImportResult charImport = input.charModelImportResult.getAndSet(null);
|
||||
if (charImport != null) {
|
||||
if (charImport.error() != null) {
|
||||
javafx.scene.control.Alert alert = new javafx.scene.control.Alert(
|
||||
javafx.scene.control.Alert.AlertType.ERROR);
|
||||
alert.initOwner(primaryStage);
|
||||
alert.setTitle("Character-Import fehlgeschlagen");
|
||||
alert.setHeaderText("FBX konnte nicht als Character-Modell importiert werden");
|
||||
alert.setContentText(charImport.error());
|
||||
alert.showAndWait();
|
||||
if (charEditorStatusLabel != null)
|
||||
charEditorStatusLabel.setText("Import fehlgeschlagen");
|
||||
} else {
|
||||
String relPath = charImport.relPath();
|
||||
if (charModelCombo != null && !charModelCombo.getItems().contains(relPath)) {
|
||||
charModelCombo.getItems().add(relPath);
|
||||
}
|
||||
if (charModelCombo != null) charModelCombo.setValue(relPath);
|
||||
if (charactersNode != null) refreshCategoryNode(charactersNode, shouldPreserveExpansion());
|
||||
if (animSetModelCombo != null && !animSetModelCombo.getItems().contains(relPath))
|
||||
animSetModelCombo.getItems().add(relPath);
|
||||
if (charEditorStatusLabel != null)
|
||||
charEditorStatusLabel.setText("Modell importiert: " + relPath);
|
||||
setStatus("Character-Modell importiert: " + relPath);
|
||||
}
|
||||
}
|
||||
|
||||
String embedStatus = input.animEmbedStatus;
|
||||
if (embedStatus != null) {
|
||||
input.animEmbedStatus = null;
|
||||
@@ -663,9 +690,9 @@ public class EditorApp extends Application {
|
||||
if (input.refreshAssets) {
|
||||
input.refreshAssets = false;
|
||||
boolean pe = shouldPreserveExpansion();
|
||||
refreshCategoryNode(modelsNode, pe);
|
||||
refreshCategoryNode(animationsNode, pe);
|
||||
refreshAddAnimCombo(addAnimComboField);
|
||||
if (modelsNode != null) refreshCategoryNode(modelsNode, pe);
|
||||
if (animationsNode != null) refreshCategoryNode(animationsNode, pe);
|
||||
if (charactersNode != null) refreshCategoryNode(charactersNode, pe);
|
||||
}
|
||||
|
||||
if (input.refreshTreeFolders) {
|
||||
@@ -1494,26 +1521,29 @@ public class EditorApp extends Application {
|
||||
MenuItem importTexSetItem = new MenuItem("Textur-Set importieren…");
|
||||
MenuItem importAudioItem = new MenuItem("Audio…");
|
||||
MenuItem importAnimItem = new MenuItem("Animationen…");
|
||||
importModelLodItem.setOnAction(e -> openModelImport(primaryStage));
|
||||
importTexItem.setOnAction(e -> handleTextureImport(primaryStage));
|
||||
importTexZipItem.setOnAction(e -> handleZipTextureImport(primaryStage));
|
||||
importTexSetItem.setOnAction(e -> openTextureSetImport(primaryStage));
|
||||
importAudioItem.setOnAction(e -> handleAudioImport(primaryStage));
|
||||
importAnimItem.setOnAction(e -> handleAnimationImport(primaryStage));
|
||||
MenuItem importCharModelItem = new MenuItem("Character-Modell (FBX)…");
|
||||
importModelLodItem.setOnAction(e -> openModelImport(primaryStage));
|
||||
importTexItem.setOnAction(e -> handleTextureImport(primaryStage));
|
||||
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));
|
||||
importMenu.getItems().addAll(importModelLodItem, importTexItem, importTexZipItem,
|
||||
importTexSetItem, importAudioItem, importAnimItem);
|
||||
importTexSetItem, importAudioItem, importAnimItem,
|
||||
new javafx.scene.control.SeparatorMenuItem(), importCharModelItem);
|
||||
|
||||
Menu toolsMenu = new Menu("Werkzeuge");
|
||||
MenuItem vegetationsItem = new MenuItem("Vegetations Generator");
|
||||
MenuItem ezTreeItem = new MenuItem("Baum Generator (EZ Tree)");
|
||||
MenuItem tripoItem = new MenuItem("AI Modell-Generator (Tripo3D)");
|
||||
MenuItem animPrevItem = new MenuItem("Animationseditor");
|
||||
MenuItem animPrevItem = new MenuItem("Character-Editor");
|
||||
MenuItem objEditorItem = new MenuItem("Model Editor");
|
||||
MenuItem thumbItem = new MenuItem("Thumbnail-Verwaltung");
|
||||
vegetationsItem.setOnAction(e -> switchToVegetationGenerator());
|
||||
ezTreeItem.setOnAction(e -> switchToEzTree());
|
||||
tripoItem.setOnAction(e -> switchToTripo());
|
||||
animPrevItem.setOnAction(e -> switchToAnimPreview());
|
||||
animPrevItem.setOnAction(e -> openCharacterEditor());
|
||||
objEditorItem.setOnAction(e -> openModelEditor(null, null));
|
||||
thumbItem.setOnAction(e -> switchToThumbnailManager());
|
||||
toolsMenu.getItems().addAll(vegetationsItem, ezTreeItem, tripoItem,
|
||||
@@ -3410,7 +3440,7 @@ public class EditorApp extends Application {
|
||||
ComboBox<String> animCombo = new ComboBox<>();
|
||||
animCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
animCombo.getItems().add("(keine)");
|
||||
Path animDir = ASSET_ROOT.resolve("animations");
|
||||
Path animDir = ASSET_ROOT.resolve("Characters").resolve("clips");
|
||||
if (java.nio.file.Files.isDirectory(animDir)) {
|
||||
try (java.util.stream.Stream<Path> walk = java.nio.file.Files.walk(animDir)) {
|
||||
walk.filter(p -> p.toString().endsWith(".j3o"))
|
||||
@@ -5069,6 +5099,7 @@ public class EditorApp extends Application {
|
||||
assetTree = tree;
|
||||
tree.setShowRoot(false);
|
||||
VBox.setVgrow(tree, Priority.ALWAYS);
|
||||
tree.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
|
||||
tree.setCellFactory(tv -> buildAssetCell());
|
||||
|
||||
// F2 → Umbenennen
|
||||
@@ -5076,7 +5107,8 @@ public class EditorApp extends Application {
|
||||
if (e.getCode() != KeyCode.F2) return;
|
||||
TreeItem<String> sel = tree.getSelectionModel().getSelectedItem();
|
||||
if (sel == null || sel == modelsNode || sel == texturesNode
|
||||
|| sel == audioNode || sel == animationsNode || sel == itemsNode) return;
|
||||
|| sel == audioNode || sel == animationsNode || sel == itemsNode
|
||||
|| sel == charactersNode) return;
|
||||
renameAsset(sel);
|
||||
e.consume();
|
||||
});
|
||||
@@ -5116,9 +5148,9 @@ public class EditorApp extends Application {
|
||||
boolean isSkeletal = skeletalModelPaths == null /* noch nicht gescannt – zulassen */
|
||||
|| skeletalModelPaths.contains(relPath);
|
||||
|
||||
// Im Animationseditor: j3o aus Models/ oder animations/ direkt laden
|
||||
// Im Animations Viewer: j3o direkt laden
|
||||
if ("animpreview".equals(currentTool)
|
||||
&& (cat == modelsNode || cat == animationsNode)
|
||||
&& (cat == modelsNode || cat == animationsNode || cat == charactersNode)
|
||||
&& relPath.endsWith(".j3o")) {
|
||||
if (!isSkeletal) { setStatus("Kein Skelett: " + relPath); return; }
|
||||
input.animPreviewLoadPath = relPath;
|
||||
@@ -5148,7 +5180,7 @@ public class EditorApp extends Application {
|
||||
if (objPlaceBtn != null) objPlaceBtn.setSelected(true);
|
||||
root.setRight(buildObjectPlacePanel());
|
||||
setStatus("Textur: " + relPath + " | Wird auf platzierte Primitive angewendet");
|
||||
} else if (cat == animationsNode && relPath.endsWith(".j3o") && isSkeletal) {
|
||||
} else if ((cat == animationsNode || cat == charactersNode) && relPath.endsWith(".j3o") && isSkeletal) {
|
||||
switchToAnimPreview();
|
||||
input.animPreviewLoadPath = relPath;
|
||||
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…");
|
||||
@@ -5179,7 +5211,10 @@ public class EditorApp extends Application {
|
||||
miTexZip.setOnAction(ev -> handleZipTextureImport(importBtn.getScene().getWindow()));
|
||||
miAudio.setOnAction(ev -> handleAudioImport(importBtn.getScene().getWindow()));
|
||||
miAnim.setOnAction(ev -> handleAnimationImport(importBtn.getScene().getWindow()));
|
||||
popup.getItems().addAll(miModel, miTex, miTexZip, miAudio, miAnim);
|
||||
MenuItem miCharModel = new MenuItem("Character-Modell (FBX)…");
|
||||
miCharModel.setOnAction(ev -> 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);
|
||||
});
|
||||
|
||||
@@ -5396,7 +5431,7 @@ public class EditorApp extends Application {
|
||||
if (jmePaths.containsKey(item) || jmeFolderNodes.contains(item)) return;
|
||||
// Kategorie-Wurzeln (Models, Textures, …) nicht verschiebbar
|
||||
if (item == modelsNode || item == texturesNode || item == audioNode
|
||||
|| item == animationsNode || item == itemsNode) return;
|
||||
|| item == animationsNode || item == itemsNode || item == charactersNode) return;
|
||||
draggedItem = item;
|
||||
Dragboard db = cell.startDragAndDrop(TransferMode.MOVE);
|
||||
ClipboardContent cc = new ClipboardContent();
|
||||
@@ -5477,6 +5512,53 @@ public class EditorApp extends Application {
|
||||
if (jmeFolderNodes.contains(item) || jmePaths.containsKey(item)) return;
|
||||
ContextMenu ctx = new ContextMenu();
|
||||
|
||||
// ── Multi-Select: Auswahl löschen ────────────────────────────────
|
||||
var selectedItems = assetTree.getSelectionModel().getSelectedItems();
|
||||
java.util.List<TreeItem<String>> deletableSelection = selectedItems.stream()
|
||||
.filter(si -> si != null && !isAssetFolder(si)
|
||||
&& itemPaths.containsKey(si)
|
||||
&& !jmePaths.containsKey(si)
|
||||
&& !jmeFolderNodes.contains(si))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
if (deletableSelection.size() > 1) {
|
||||
MenuItem multiDel = new MenuItem("🗑 Auswahl löschen (" + deletableSelection.size() + " Dateien)");
|
||||
multiDel.setStyle("-fx-text-fill: #c0392b; -fx-font-weight: bold;");
|
||||
multiDel.setOnAction(ev -> {
|
||||
StringBuilder names = new StringBuilder();
|
||||
for (int i = 0; i < Math.min(5, deletableSelection.size()); i++) {
|
||||
names.append(" ").append(deletableSelection.get(i).getValue()).append('\n');
|
||||
}
|
||||
if (deletableSelection.size() > 5)
|
||||
names.append(" … und ").append(deletableSelection.size() - 5).append(" weitere");
|
||||
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
||||
deletableSelection.size() + " Dateien löschen?\n\n" + names,
|
||||
ButtonType.OK, ButtonType.CANCEL);
|
||||
confirm.initOwner(primaryStage);
|
||||
confirm.setHeaderText(null);
|
||||
confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> {
|
||||
int deleted = 0;
|
||||
for (TreeItem<String> si : new java.util.ArrayList<>(deletableSelection)) {
|
||||
Path p = itemPaths.get(si);
|
||||
if (p == null) continue;
|
||||
try {
|
||||
Files.deleteIfExists(p);
|
||||
if (p.toString().endsWith(".j3o")) deleteJ3oSideFiles(p);
|
||||
si.getParent().getChildren().remove(si);
|
||||
itemPaths.remove(si);
|
||||
deleted++;
|
||||
} catch (IOException ex) {
|
||||
setStatus("Fehler beim Löschen: " + p.getFileName());
|
||||
}
|
||||
}
|
||||
setStatus(deleted + " Datei(en) gelöscht");
|
||||
});
|
||||
});
|
||||
ctx.getItems().addAll(multiDel, new SeparatorMenuItem());
|
||||
ctx.show(cell, e.getScreenX(), e.getScreenY());
|
||||
e.consume();
|
||||
return;
|
||||
}
|
||||
|
||||
// Texture-Set-Knoten: eigenes Menü
|
||||
if (textureSetNodes.contains(item)) {
|
||||
Path setDir = itemPaths.get(item);
|
||||
@@ -5527,7 +5609,8 @@ public class EditorApp extends Application {
|
||||
ctx.getItems().add(newSub);
|
||||
|
||||
boolean isCatRoot = (item == modelsNode || item == texturesNode
|
||||
|| item == audioNode || item == animationsNode || item == itemsNode);
|
||||
|| item == audioNode || item == animationsNode || item == itemsNode
|
||||
|| item == charactersNode);
|
||||
Path dir = itemPaths.get(item);
|
||||
|
||||
// Ordner unter Models: Zufällig-Platzieren-Eintrag
|
||||
@@ -5554,7 +5637,15 @@ public class EditorApp extends Application {
|
||||
ctx.getItems().add(importAnim);
|
||||
}
|
||||
|
||||
Path setsDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||
if (item == charactersNode) {
|
||||
MenuItem importChar = new MenuItem("⊕ Character-Modell importieren (FBX)…");
|
||||
importChar.setOnAction(ev -> handleCharacterModelImport(ctx.getOwnerWindow()));
|
||||
MenuItem importAnim = new MenuItem("⊕ Animation hinzufügen…");
|
||||
importAnim.setOnAction(ev -> 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());
|
||||
@@ -5714,11 +5805,11 @@ public class EditorApp extends Application {
|
||||
return p != null && Files.isDirectory(p);
|
||||
}
|
||||
|
||||
/** Findet die übergeordnete Kategorie (modelsNode / texturesNode / audioNode / animationsNode / itemsNode). */
|
||||
/** Findet die übergeordnete Kategorie (modelsNode / texturesNode / audioNode / animationsNode / itemsNode / charactersNode). */
|
||||
private TreeItem<String> getCategoryRoot(TreeItem<String> item) {
|
||||
for (TreeItem<String> cur = item; cur != null; cur = cur.getParent())
|
||||
if (cur == modelsNode || cur == texturesNode || cur == audioNode
|
||||
|| cur == animationsNode || cur == itemsNode) return cur;
|
||||
|| cur == animationsNode || cur == itemsNode || cur == charactersNode) return cur;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6161,6 +6252,7 @@ public class EditorApp extends Application {
|
||||
private void populateAssetTree(TreeItem<String> root) {
|
||||
modelsNode = null; texturesNode = null;
|
||||
audioNode = null; animationsNode = null; itemsNode = null;
|
||||
charactersNode = null;
|
||||
jmeModelsNode = null; jmeTexturesNode = null;
|
||||
jmeFolderNodes.clear();
|
||||
textureSetNodes.clear();
|
||||
@@ -6203,7 +6295,8 @@ public class EditorApp extends Application {
|
||||
animationsNode = node;
|
||||
addAnimClipSubNodes(node);
|
||||
}
|
||||
case "items" -> itemsNode = node;
|
||||
case "characters" -> charactersNode = node;
|
||||
case "items" -> itemsNode = node;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6279,7 +6372,7 @@ public class EditorApp extends Application {
|
||||
|
||||
private void clearItemPathsFor(TreeItem<String> item) {
|
||||
for (TreeItem<String> child : item.getChildren()) clearItemPathsFor(child);
|
||||
if (item != modelsNode && item != texturesNode && item != audioNode && item != animationsNode)
|
||||
if (item != modelsNode && item != texturesNode && item != audioNode && item != animationsNode && item != charactersNode)
|
||||
itemPaths.remove(item);
|
||||
textureSetNodes.remove(item);
|
||||
}
|
||||
@@ -10256,6 +10349,63 @@ public class EditorApp extends Application {
|
||||
|
||||
// ── AnimSet-Editor ────────────────────────────────────────────────────────
|
||||
|
||||
private void openCharacterEditor() {
|
||||
Path setsDir = ASSET_ROOT.resolve("Characters").resolve("sets");
|
||||
java.util.List<String> 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<String> dlg = new javafx.scene.control.Dialog<>();
|
||||
dlg.initOwner(primaryStage);
|
||||
dlg.setTitle("Character-Editor");
|
||||
dlg.setHeaderText("AnimSet auswählen oder neu anlegen");
|
||||
|
||||
ListView<String> 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void openAnimSetEditor(String relPath, Path absPath) {
|
||||
String setName = absPath.getFileName().toString().replaceFirst("\\.animset\\.json$", "");
|
||||
Path setDir = absPath.getParent();
|
||||
@@ -10327,15 +10477,16 @@ public class EditorApp extends Application {
|
||||
animSetModelCombo = new ComboBox<>();
|
||||
animSetModelCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
animSetModelCombo.setPromptText("j3o-Datei wählen…");
|
||||
for (String dir : new String[]{"Models", "animations"}) {
|
||||
Path base = ASSET_ROOT.resolve(dir);
|
||||
if (!java.nio.file.Files.isDirectory(base)) continue;
|
||||
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) {}
|
||||
{
|
||||
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()) {
|
||||
@@ -10352,7 +10503,10 @@ public class EditorApp extends Application {
|
||||
animSet.setPreviewModelPath(path);
|
||||
animSetDirty = true;
|
||||
});
|
||||
inner.getChildren().addAll(animSetModelCombo, loadModelBtn);
|
||||
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()) {
|
||||
@@ -10368,7 +10522,7 @@ public class EditorApp extends Application {
|
||||
animSetClipListView.setPrefHeight(180);
|
||||
editableSubClips = new java.util.LinkedHashMap<>(animSet.getSubClips());
|
||||
|
||||
Path animRootDir = ASSET_ROOT.resolve("animations");
|
||||
Path animRootDir = ASSET_ROOT.resolve("Characters");
|
||||
|
||||
Button addClipBtn = new Button("+ Hinzufügen…");
|
||||
Button removeClipBtn = new Button("- Entfernen");
|
||||
@@ -10411,9 +10565,10 @@ public class EditorApp extends Application {
|
||||
String name = cp.getFileName().toString();
|
||||
boolean isAnim = name.endsWith(".j3o") || name.endsWith(".glb")
|
||||
|| name.endsWith(".gltf") || name.endsWith(".fbx");
|
||||
boolean notSets = !animRootDir.relativize(cp).startsWith("sets");
|
||||
_log.debug("[ClipScan] {} | isAnim={} notSets={}", cp, isAnim, notSets);
|
||||
return isAnim && notSets;
|
||||
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()
|
||||
@@ -10486,7 +10641,12 @@ public class EditorApp extends Application {
|
||||
HBox clipBtns = new HBox(6, addClipBtn, removeClipBtn);
|
||||
HBox.setHgrow(addClipBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(removeClipBtn, Priority.ALWAYS);
|
||||
inner.getChildren().addAll(animSetClipListView, clipBtns);
|
||||
|
||||
Button importAnimBtn = new Button("Animation hinzufügen…");
|
||||
importAnimBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
importAnimBtn.setOnAction(e -> handleAnimationImport(importAnimBtn.getScene().getWindow()));
|
||||
|
||||
inner.getChildren().addAll(animSetClipListView, clipBtns, importAnimBtn);
|
||||
|
||||
// ── Clip-Teile (Sub-Clips) ────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator(), sectionTitle("Clip-Teile"), new Separator());
|
||||
@@ -10818,14 +10978,14 @@ public class EditorApp extends Application {
|
||||
}
|
||||
|
||||
private String findAnimClipPath(String clipName) {
|
||||
Path animDir = ASSET_ROOT.resolve("animations");
|
||||
Path charDir = ASSET_ROOT.resolve("Characters");
|
||||
for (String ext : new String[]{".j3o", ".glb", ".gltf", ".fbx"}) {
|
||||
if (java.nio.file.Files.exists(animDir.resolve("clips").resolve(clipName + ext)))
|
||||
return "animations/clips/" + clipName + ext;
|
||||
if (java.nio.file.Files.exists(animDir.resolve(clipName + ext)))
|
||||
return "animations/" + clipName + ext;
|
||||
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 "animations/clips/" + clipName + ".j3o";
|
||||
return "Characters/clips/" + clipName + ".j3o";
|
||||
}
|
||||
|
||||
private void showAddActionToSetDialog() {
|
||||
@@ -11041,12 +11201,12 @@ public class EditorApp extends Application {
|
||||
dlg.showAndWait().ifPresent(raw -> {
|
||||
String name = raw.trim();
|
||||
if (name.isBlank()) return;
|
||||
Path setDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||
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(animationsNode, shouldPreserveExpansion());
|
||||
refreshCategoryNode(charactersNode, shouldPreserveExpansion());
|
||||
String relPath = ASSET_ROOT.relativize(setFile).toString().replace('\\', '/');
|
||||
openAnimSetEditor(relPath, setFile);
|
||||
} catch (IOException ex) {
|
||||
@@ -11058,7 +11218,7 @@ public class EditorApp extends Application {
|
||||
private ToolBar buildAnimPreviewToolBar() {
|
||||
Button backBtn = new Button("← Welteneditor");
|
||||
backBtn.setOnAction(e -> switchToWorldEditor());
|
||||
Label label = new Label("Animationseditor");
|
||||
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);
|
||||
@@ -11069,39 +11229,8 @@ public class EditorApp extends Application {
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
|
||||
// ── Modell-Auswahl ────────────────────────────────────────────────────
|
||||
inner.getChildren().addAll(sectionTitle("Modell"), new Separator());
|
||||
animPreviewModelCombo = new ComboBox<>();
|
||||
animPreviewModelCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
animPreviewModelCombo.setPromptText("j3o-Datei wählen…");
|
||||
for (String dir : new String[]{"Models", "animations"}) {
|
||||
Path base = ASSET_ROOT.resolve(dir);
|
||||
if (!java.nio.file.Files.isDirectory(base)) continue;
|
||||
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(animPreviewModelCombo.getItems()::add);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
Button loadBtn = new Button("Laden");
|
||||
loadBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
loadBtn.setStyle("-fx-font-weight: bold;");
|
||||
loadBtn.setOnAction(e -> {
|
||||
String path = animPreviewModelCombo.getValue();
|
||||
if (path == null || path.isEmpty()) return;
|
||||
input.animPreviewLoadPath = path;
|
||||
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…");
|
||||
if (animClipListView != null) animClipListView.getItems().clear();
|
||||
});
|
||||
Button reimportBtn = new Button("Modell neu importieren (GLB/GLTF)…");
|
||||
reimportBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
reimportBtn.setOnAction(e -> reimportModelForPreview(reimportBtn.getScene().getWindow()));
|
||||
inner.getChildren().addAll(new Label("Modell:"), animPreviewModelCombo, loadBtn, reimportBtn);
|
||||
|
||||
// ── Animationen ───────────────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator(), sectionTitle("Animationen"), new Separator());
|
||||
inner.getChildren().addAll(sectionTitle("Animationen"), new Separator());
|
||||
animClipListView = new ListView<>();
|
||||
animClipListView.setPrefHeight(200);
|
||||
animClipListView.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.SINGLE);
|
||||
@@ -11116,7 +11245,6 @@ public class EditorApp extends Application {
|
||||
if (clip != null) input.animPreviewPlayClip = clip;
|
||||
});
|
||||
stopBtn.setOnAction(e -> input.animPreviewPlayClip = "");
|
||||
// Doppelklick auf Clip → direkt abspielen
|
||||
animClipListView.setOnMouseClicked(e -> {
|
||||
if (e.getClickCount() == 2) {
|
||||
String clip = animClipListView.getSelectionModel().getSelectedItem();
|
||||
@@ -11128,26 +11256,6 @@ public class EditorApp extends Application {
|
||||
HBox.setHgrow(stopBtn, Priority.ALWAYS);
|
||||
inner.getChildren().addAll(animClipListView, btnRow);
|
||||
|
||||
Button renameClipBtn = new Button("Clip umbenennen / exportieren…");
|
||||
renameClipBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
renameClipBtn.setOnAction(e -> {
|
||||
String clip = animClipListView.getSelectionModel().getSelectedItem();
|
||||
if (clip == null) return;
|
||||
javafx.scene.control.TextInputDialog dlg = new javafx.scene.control.TextInputDialog(clip);
|
||||
dlg.setTitle("Clip umbenennen");
|
||||
dlg.setHeaderText("Clip '" + clip + "' unter neuem Namen speichern");
|
||||
dlg.setContentText("Neuer Name:");
|
||||
dlg.showAndWait().ifPresent(newName -> {
|
||||
if (!newName.isBlank()) input.clipRenameRequest.set(new SharedInput.ClipRenameRequest(clip, newName.trim()));
|
||||
});
|
||||
});
|
||||
|
||||
Button saveSetBtn = new Button("Als Animations-Set speichern…");
|
||||
saveSetBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveSetBtn.setStyle("-fx-font-weight: bold;");
|
||||
saveSetBtn.setOnAction(e -> showSaveAnimSetDialog());
|
||||
inner.getChildren().addAll(renameClipBtn, saveSetBtn);
|
||||
|
||||
// ── Steuerung ─────────────────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator(), sectionTitle("Steuerung"), new Separator());
|
||||
|
||||
@@ -11163,51 +11271,11 @@ public class EditorApp extends Application {
|
||||
speedSlider.valueProperty().addListener((obs, ov, nv) -> input.animPreviewSpeed = nv.floatValue());
|
||||
inner.getChildren().addAll(loopCB, speedLbl, withField(speedSlider, "%.2f"));
|
||||
|
||||
// ── Clip importieren ──────────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator(), sectionTitle("Clip importieren"), new Separator());
|
||||
|
||||
addAnimComboField = new ComboBox<>();
|
||||
ComboBox<String> addAnimCombo = addAnimComboField;
|
||||
|
||||
// Beim Hinzufügen wird der Clip direkt in animations/clips/ gespeichert
|
||||
Label animHint = new Label("Clip wird retargeted und direkt in animations/clips/ gespeichert. Mixamo: \"With Skin\" wählen.");
|
||||
animHint.setWrapText(true);
|
||||
animHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
|
||||
Button importAnimBtn = new Button("Animation importieren (GLB/GLTF)…");
|
||||
importAnimBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
importAnimBtn.setOnAction(e ->
|
||||
handleAnimationImport(importAnimBtn.getScene().getWindow()));
|
||||
addAnimCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
addAnimCombo.setPromptText("Animation aus animations/ wählen…");
|
||||
refreshAddAnimCombo(addAnimCombo);
|
||||
|
||||
Button addAnimBtn = new Button("Animation hinzufügen");
|
||||
addAnimBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
addAnimBtn.setStyle("-fx-font-weight: bold;");
|
||||
addAnimBtn.setOnAction(e -> {
|
||||
String animPath = addAnimCombo.getValue();
|
||||
if (animPath == null || animPath.isEmpty()) {
|
||||
if (animPreviewStatusLabel != null)
|
||||
animPreviewStatusLabel.setText("Bitte eine Animation auswählen");
|
||||
return;
|
||||
}
|
||||
input.animImportQueue.offer(animPath);
|
||||
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Füge Clips hinzu…");
|
||||
});
|
||||
inner.getChildren().addAll(animHint, importAnimBtn, addAnimCombo, addAnimBtn);
|
||||
|
||||
// ── Diagnose ──────────────────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator(), sectionTitle("Diagnose"), new Separator());
|
||||
Button dumpBtn = new Button("Skelett-Info ins Log dumpen");
|
||||
dumpBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
dumpBtn.setOnAction(e -> input.animDumpRequested = true);
|
||||
inner.getChildren().add(dumpBtn);
|
||||
|
||||
// ── Status ────────────────────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator());
|
||||
animPreviewStatusLabel = new Label("Kein Modell geladen");
|
||||
animPreviewStatusLabel = new Label("");
|
||||
animPreviewStatusLabel.setWrapText(true);
|
||||
animPreviewStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
|
||||
animPreviewStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #c00;");
|
||||
inner.getChildren().add(animPreviewStatusLabel);
|
||||
|
||||
ScrollPane scroll = new ScrollPane(inner);
|
||||
@@ -11221,24 +11289,6 @@ public class EditorApp extends Application {
|
||||
return panel;
|
||||
}
|
||||
|
||||
private void refreshAddAnimCombo(ComboBox<String> combo) {
|
||||
if (combo == null) return;
|
||||
String current = combo.getValue();
|
||||
combo.getItems().clear();
|
||||
Path animBase = ASSET_ROOT.resolve("animations");
|
||||
if (java.nio.file.Files.isDirectory(animBase)) {
|
||||
try (var walk = java.nio.file.Files.walk(animBase)) {
|
||||
walk.filter(ap -> {
|
||||
String s = ap.toString();
|
||||
return s.endsWith(".j3o") || s.endsWith(".glb") || s.endsWith(".gltf");
|
||||
})
|
||||
.map(ap -> ASSET_ROOT.relativize(ap).toString().replace('\\', '/'))
|
||||
.sorted()
|
||||
.forEach(combo.getItems()::add);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
if (current != null && combo.getItems().contains(current)) combo.setValue(current);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kopiert die Original-Quelldatei nach {@code <projekt-root>/assets/imported/<assetType>/}.
|
||||
@@ -11259,9 +11309,9 @@ public class EditorApp extends Application {
|
||||
|
||||
private void handleAnimationImport(javafx.stage.Window owner) {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Animation importieren (GLB/GLTF)");
|
||||
fc.setTitle("Animation importieren (FBX/GLB/GLTF)");
|
||||
fc.getExtensionFilters().add(
|
||||
new FileChooser.ExtensionFilter("Animationen (GLTF, GLB)", "*.gltf", "*.glb"));
|
||||
new FileChooser.ExtensionFilter("Animationen (FBX, GLTF, GLB)", "*.fbx", "*.gltf", "*.glb"));
|
||||
var files = fc.showOpenMultipleDialog(owner);
|
||||
if (files == null) return;
|
||||
for (File file : files) {
|
||||
@@ -11273,6 +11323,20 @@ public class EditorApp extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -11370,7 +11434,7 @@ public class EditorApp extends Application {
|
||||
|
||||
java.util.List<String> allClips = animClipListView.getItems();
|
||||
|
||||
Path setDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||
Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets");
|
||||
|
||||
// Aktions-Zuweisung: Liste mit Add/Remove
|
||||
javafx.scene.control.ListView<String> actionList = new javafx.scene.control.ListView<>();
|
||||
@@ -11511,7 +11575,7 @@ public class EditorApp extends Application {
|
||||
topBar.getChildren().set(1, buildCharacterEditorToolBar());
|
||||
dialogEditorView = new de.blight.editor.ui.DialogEditorView();
|
||||
dialogEditorView.setDisable(true);
|
||||
routineEditorView = new de.blight.editor.ui.RoutineEditorView(input, ASSET_ROOT.resolve("character"));
|
||||
routineEditorView = new de.blight.editor.ui.RoutineEditorView(input, ASSET_ROOT.resolve("Characters"));
|
||||
routineEditorView.setDisable(true);
|
||||
monologueEditorView = new de.blight.editor.ui.MonologueEditorView();
|
||||
|
||||
@@ -11690,7 +11754,7 @@ public class EditorApp extends Application {
|
||||
}
|
||||
|
||||
private ScrollPane buildCharacterSettingsPane() {
|
||||
Path charDir = ASSET_ROOT.resolve("character");
|
||||
Path charDir = ASSET_ROOT.resolve("Characters");
|
||||
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
@@ -11698,7 +11762,13 @@ public class EditorApp extends Application {
|
||||
Button newCharBtn = new Button("Neuer Charakter");
|
||||
newCharBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
newCharBtn.setOnAction(e -> clearCharacterForm());
|
||||
inner.getChildren().add(newCharBtn);
|
||||
|
||||
Button importCharModelBtn = new Button("Character-Modell importieren (FBX)…");
|
||||
importCharModelBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
importCharModelBtn.setStyle("-fx-font-weight: bold;");
|
||||
importCharModelBtn.setOnAction(e -> handleCharacterModelImport(importCharModelBtn.getScene().getWindow()));
|
||||
|
||||
inner.getChildren().addAll(newCharBtn, importCharModelBtn);
|
||||
|
||||
charEditorStatusLabel = new Label("Wähle einen Character im Baum oder erstelle einen neuen");
|
||||
charEditorStatusLabel.setWrapText(true);
|
||||
@@ -11891,7 +11961,7 @@ public class EditorApp extends Application {
|
||||
if (charAnimSetCombo == null) return;
|
||||
String cur = charAnimSetCombo.getValue();
|
||||
charAnimSetCombo.getItems().clear();
|
||||
Path setDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||
Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets");
|
||||
if (java.nio.file.Files.isDirectory(setDir)) {
|
||||
try (var walk = java.nio.file.Files.walk(setDir)) {
|
||||
walk.filter(p -> p.toString().endsWith(".animset.json"))
|
||||
@@ -11912,7 +11982,7 @@ public class EditorApp extends Application {
|
||||
charActionLabelsBox.getChildren().add(hint);
|
||||
return;
|
||||
}
|
||||
Path setDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||
Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets");
|
||||
de.blight.game.animation.AnimSet animSet;
|
||||
try {
|
||||
animSet = de.blight.game.animation.AnimSet.load(setDir, setName);
|
||||
@@ -12695,7 +12765,7 @@ public class EditorApp extends Application {
|
||||
}
|
||||
|
||||
private void deleteCharacterById(String id) {
|
||||
Path charFile = ASSET_ROOT.resolve("character").resolve(id + ".character");
|
||||
Path charFile = ASSET_ROOT.resolve("Characters").resolve(id + ".character");
|
||||
try {
|
||||
java.nio.file.Files.deleteIfExists(charFile);
|
||||
} catch (IOException ex) {
|
||||
@@ -12728,7 +12798,7 @@ public class EditorApp extends Application {
|
||||
dlg.setContentText("ID:");
|
||||
dlg.showAndWait().ifPresent(newId -> {
|
||||
if (newId == null || newId.isBlank()) return;
|
||||
Path charDir = ASSET_ROOT.resolve("character");
|
||||
Path charDir = ASSET_ROOT.resolve("Characters");
|
||||
Path src = charDir.resolve(sourceId + ".character");
|
||||
Path dest = charDir.resolve(newId.trim() + ".character");
|
||||
try {
|
||||
@@ -12763,7 +12833,7 @@ public class EditorApp extends Application {
|
||||
contentTreeUpdating = true;
|
||||
try {
|
||||
contentCharNode.getChildren().clear();
|
||||
Path charDir = ASSET_ROOT.resolve("character");
|
||||
Path charDir = ASSET_ROOT.resolve("Characters");
|
||||
if (!java.nio.file.Files.isDirectory(charDir)) return;
|
||||
java.util.List<javafx.scene.control.TreeItem<String>> items = new java.util.ArrayList<>();
|
||||
try (var walk = java.nio.file.Files.list(charDir)) {
|
||||
@@ -12780,7 +12850,7 @@ public class EditorApp extends Application {
|
||||
|
||||
private void loadCharacterById(String id) {
|
||||
if (id == null || id.isBlank()) return;
|
||||
Path charDir = ASSET_ROOT.resolve("character");
|
||||
Path charDir = ASSET_ROOT.resolve("Characters");
|
||||
try {
|
||||
de.blight.common.model.GameCharacter c =
|
||||
de.blight.common.model.CharacterIO.load(charDir.resolve(id + ".character"));
|
||||
|
||||
@@ -619,6 +619,12 @@ public class SharedInput {
|
||||
public volatile String animPreviewPlayClip = null;
|
||||
/** JavaFX → JME3: Animation-j3o-Pfad zum Retargeting + Hinzufügen. null = kein Auftrag. */
|
||||
public final ConcurrentLinkedQueue<String> animImportQueue = new ConcurrentLinkedQueue<>();
|
||||
/** JavaFX → JME3: Absoluter FBX-Pfad für Character-Modell-Import (nach Models/Chars/). */
|
||||
public final ConcurrentLinkedQueue<String> charModelImportQueue = new ConcurrentLinkedQueue<>();
|
||||
/** JME3 → JavaFX: Ergebnis des Character-Modell-Imports. relPath=null wenn Fehler. */
|
||||
public record CharModelImportResult(String relPath, String error) {}
|
||||
public final java.util.concurrent.atomic.AtomicReference<CharModelImportResult>
|
||||
charModelImportResult = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
/** JavaFX → JME3: Clip-Name zum Entfernen aus dem geladenen Modell. null = kein Auftrag. */
|
||||
public volatile String animPreviewRemoveClip = null;
|
||||
/** JavaFX → JME3: Scan aller j3o auf Skelett-Controls anstoßen. */
|
||||
|
||||
@@ -85,6 +85,8 @@ public class AnimPreviewState extends BaseAppState {
|
||||
protected void initialize(Application app) {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.assets = app.getAssetManager();
|
||||
assets.registerLoader(com.github.stephengold.wrench.LwjglAssetLoader.class,
|
||||
"fbx", "dae", "bvh");
|
||||
|
||||
previewFB = buildFrameBuffer(PREVIEW_SIZE, PREVIEW_SIZE);
|
||||
|
||||
@@ -153,6 +155,11 @@ public class AnimPreviewState extends BaseAppState {
|
||||
addAnimation(addAnimPath);
|
||||
}
|
||||
|
||||
String charModelPath = input.charModelImportQueue.poll();
|
||||
if (charModelPath != null) {
|
||||
importFbxAsCharacterModel(charModelPath);
|
||||
}
|
||||
|
||||
String removeClip = input.animPreviewRemoveClip;
|
||||
if (removeClip != null) {
|
||||
input.animPreviewRemoveClip = null;
|
||||
@@ -193,7 +200,8 @@ public class AnimPreviewState extends BaseAppState {
|
||||
double length = currentAction.getLength();
|
||||
double time = ac.getTime();
|
||||
if (length <= 0) {
|
||||
LOG.warn("[AnimPreview] Loop-Check: length=0, Clip hat keine Dauer!");
|
||||
LOG.warn("[AnimPreview] Clip '{}' hat keine Dauer, stoppe.", currentClipName);
|
||||
stopAll();
|
||||
} else if (time >= length - 0.02) {
|
||||
LOG.trace("[AnimPreview] Loop-Check fired: time={} length={}", time, length);
|
||||
if (input.animPreviewLoop) {
|
||||
@@ -267,7 +275,7 @@ public class AnimPreviewState extends BaseAppState {
|
||||
// Alle Clips in-place snappen (verhindert Drift im Preview)
|
||||
AnimComposer previewAC = findControl(model, AnimComposer.class);
|
||||
SkinningControl previewSC = findControl(model, SkinningControl.class);
|
||||
LOG.info("[AnimPreview] Modell-Controls: AnimComposer={}, SkinningControl={}, EmbeddedClips={}",
|
||||
LOG.debug("[AnimPreview] Modell-Controls: AnimComposer={}, SkinningControl={}, EmbeddedClips={}",
|
||||
previewAC != null ? "gefunden" : "NULL",
|
||||
previewSC != null ? "gefunden" : "NULL",
|
||||
previewAC != null ? previewAC.getAnimClips().size() : "n/a");
|
||||
@@ -365,7 +373,10 @@ public class AnimPreviewState extends BaseAppState {
|
||||
private void collectClips(Spatial s, List<String> out) {
|
||||
AnimComposer ac = s.getControl(AnimComposer.class);
|
||||
if (ac != null) {
|
||||
List<String> names = new ArrayList<>(ac.getAnimClipsNames());
|
||||
List<String> names = new ArrayList<>();
|
||||
for (String n : ac.getAnimClipsNames()) {
|
||||
if (!n.startsWith("__")) names.add(n);
|
||||
}
|
||||
Collections.sort(names);
|
||||
out.addAll(names);
|
||||
}
|
||||
@@ -395,7 +406,38 @@ public class AnimPreviewState extends BaseAppState {
|
||||
if (currentAction != null) {
|
||||
currentAction.setSpeed(input.animPreviewSpeed);
|
||||
input.animPreviewCurrentClipDuration = (float) currentAction.getLength();
|
||||
LOG.info("[AnimPreview] Play '{}' length={}", clipName, currentAction.getLength());
|
||||
// DIAG: Clip-Inhalt + aktueller Hips-Zustand
|
||||
AnimClip diagClip = ac.getAnimClip(clipName);
|
||||
if (diagClip != null) {
|
||||
LOG.info("[DIAG] Play '{}' length={} tracks={}", clipName,
|
||||
currentAction.getLength(), diagClip.getTracks().length);
|
||||
for (com.jme3.anim.AnimTrack<?> t : diagClip.getTracks()) {
|
||||
if (!(t instanceof com.jme3.anim.TransformTrack tt)) continue;
|
||||
if (!(tt.getTarget() instanceof com.jme3.anim.Joint j)) continue;
|
||||
if (!j.getName().contains("Hips")) continue;
|
||||
com.jme3.math.Vector3f[] tr = tt.getTranslations();
|
||||
com.jme3.math.Quaternion[] rr = tt.getRotations();
|
||||
String tStr = (tr != null && tr.length > 0) ? tr[0].toString() : "null";
|
||||
String rStr = (rr != null && rr.length > 0) ? rr[0].toString() : "null";
|
||||
LOG.info("[DIAG] Track '{}' target='{}' frames={} t[0]={} r[0]={}",
|
||||
clipName, j.getName(), tt.getTimes().length, tStr, rStr);
|
||||
}
|
||||
}
|
||||
// DIAG: Modell-Hips localTransform + IBM
|
||||
SkinningControl diaSC = findControl(currentModel, SkinningControl.class);
|
||||
if (diaSC != null) {
|
||||
com.jme3.anim.Armature diaArm = diaSC.getArmature();
|
||||
for (int di = 0; di < diaArm.getJointCount(); di++) {
|
||||
com.jme3.anim.Joint dj = diaArm.getJoint(di);
|
||||
if (dj.getParent() == null) {
|
||||
com.jme3.math.Vector3f lt = dj.getLocalTransform().getTranslation();
|
||||
com.jme3.math.Matrix4f ibm = dj.getInverseModelBindMatrix();
|
||||
LOG.info("[DIAG] Model-Root '{}': localT=({},{},{}) IBM_transl=({},{},{})",
|
||||
dj.getName(), lt.x, lt.y, lt.z,
|
||||
ibm != null ? ibm.m30 : 0, ibm != null ? ibm.m31 : 0, ibm != null ? ibm.m32 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
input.animPreviewStatus = "Abspielen fehlgeschlagen: " + e.getMessage();
|
||||
@@ -499,40 +541,39 @@ public class AnimPreviewState extends BaseAppState {
|
||||
com.jme3.anim.Armature srcArm = sourceSC != null ? sourceSC.getArmature() : null;
|
||||
com.jme3.anim.Armature dstArm = targetSC != null ? targetSC.getArmature() : null;
|
||||
|
||||
if (srcArm != null) {
|
||||
LOG.trace("[Retarget] Quell-Knochen ({}):", srcArm.getJointCount());
|
||||
for (var j : srcArm.getJointList()) LOG.trace(" src: {}", j.getName());
|
||||
} else {
|
||||
LOG.warn("[Retarget] Keine SkinningControl in Quelle!");
|
||||
}
|
||||
if (dstArm != null) {
|
||||
LOG.trace("[Retarget] Ziel-Knochen ({}):", dstArm.getJointCount());
|
||||
for (var j : dstArm.getJointList()) LOG.trace(" dst: {}", j.getName());
|
||||
} else {
|
||||
LOG.warn("[Retarget] Keine SkinningControl im Modell!");
|
||||
if (srcArm == null) {
|
||||
LOG.debug("[Retarget] Quelle hat keine SkinningControl (j3o-Clip oder reines Anim-FBX).");
|
||||
} else if (dstArm == null) {
|
||||
LOG.debug("[Retarget] Kein Charakter-Modell geladen – Clip wird ohne Retargeting gespeichert.");
|
||||
}
|
||||
|
||||
boolean retarget = srcArm != null && dstArm != null && srcArm != dstArm;
|
||||
if (retarget) {
|
||||
var mapping = de.blight.game.animation.BoneNameMapping.buildMapping(srcArm, dstArm);
|
||||
LOG.trace("[Retarget] Mapping ({} Treffer): {}", mapping.size(), mapping);
|
||||
}
|
||||
// Retarget wenn Ziel-Armature bekannt UND entweder andere Quelle-Armature
|
||||
// oder Quelle hat gar keine Armature (z.B. j3o-Clip-Wrapper ohne SkinningControl).
|
||||
// Ohne Retarget würden die Track-Targets auf fremde (Standalone-)Joints zeigen
|
||||
// und die Modell-Joints gar nicht bewegen.
|
||||
boolean retarget = dstArm != null && (srcArm == null || srcArm != dstArm);
|
||||
|
||||
java.util.Set<String> srcNames = new java.util.HashSet<>();
|
||||
for (AnimClip c : sourceAC.getAnimClips()) srcNames.add(c.getName());
|
||||
|
||||
java.nio.file.Path clipsDir = ASSET_ROOT.resolve("animations").resolve("clips");
|
||||
java.nio.file.Path clipsDir = ASSET_ROOT.resolve("Characters").resolve("clips");
|
||||
java.nio.file.Files.createDirectories(clipsDir);
|
||||
|
||||
// Blender exportiert GLB-Dateien oft mit internem Namen "Action" statt dem Dateinamen.
|
||||
// Bei einer GLB/GLTF-Datei mit genau einem Clip: Dateinamen als Clip-Namen verwenden.
|
||||
boolean isSingleClipGlb = animAssetPath.matches(".*\\.(glb|gltf)$")
|
||||
// GLB/FBX mit genau einem Clip: Dateinamen als Clip-Namen verwenden.
|
||||
// Mixamo FBX nennt alle Clips "mixamo.com" unabhängig vom Inhalt.
|
||||
boolean isSingleClipSource = animAssetPath.matches("(?i).*\\.(glb|gltf|fbx)$")
|
||||
&& sourceAC.getAnimClips().size() == 1;
|
||||
String fileBaseName = isSingleClipGlb
|
||||
boolean isSingleClipGlb = isSingleClipSource; // Kompatibilität mit unterem Delete-Block
|
||||
String fileBaseName = isSingleClipSource
|
||||
? java.nio.file.Paths.get(animAssetPath).getFileName().toString()
|
||||
.replaceFirst("\\.(glb|gltf)$", "")
|
||||
.replaceFirst("(?i)\\.(glb|gltf|fbx)$", "")
|
||||
: null;
|
||||
|
||||
// j3o-Quelle: Clip bereits gespeichert – nur Preview, kein erneutes Speichern.
|
||||
// snap passiert ausschließlich zur Laufzeit im Spiel via AnimationLibrary.
|
||||
boolean isJ3oSource = animAssetPath.matches("(?i).*\\.j3o$");
|
||||
com.jme3.anim.Armature snapArm = dstArm != null ? dstArm : srcArm;
|
||||
|
||||
int saved = 0;
|
||||
for (AnimClip clip : sourceAC.getAnimClips()) {
|
||||
String name = clip.getName();
|
||||
@@ -543,9 +584,18 @@ public class AnimPreviewState extends BaseAppState {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
AnimClip result = retarget
|
||||
? de.blight.game.animation.RetargetingSystem.retarget(clip, srcArm, dstArm)
|
||||
: clip;
|
||||
AnimClip result;
|
||||
if (!retarget) {
|
||||
result = clip;
|
||||
} else {
|
||||
// Immer erst redirect-by-name wie FbxTestApp: mappt alle passenden Joints,
|
||||
// droppt nur die wenigen fehlenden ($AssimpFbx$-Varianten die im Model nicht existieren).
|
||||
result = redirectAnimClipByName(clip, dstArm);
|
||||
if (result == null) {
|
||||
LOG.warn("[AnimPreview] redirectByName: keine Joints gemappt, versuche Retargeting");
|
||||
result = de.blight.game.animation.RetargetingSystem.retarget(clip, srcArm, dstArm);
|
||||
}
|
||||
}
|
||||
if (result == null) continue;
|
||||
|
||||
String saveName = (fileBaseName != null) ? fileBaseName : name;
|
||||
@@ -556,18 +606,16 @@ public class AnimPreviewState extends BaseAppState {
|
||||
LOG.info("[AnimPreview] Clip '{}' als '{}' gespeichert (Dateiname-Alias)", name, saveName);
|
||||
}
|
||||
|
||||
// XZ-Drift einfrieren bevor gespeichert wird – Clip-Dateien bleiben immer sauber
|
||||
com.jme3.anim.Armature snapArm = dstArm != null ? dstArm : srcArm;
|
||||
toSave = de.blight.game.animation.AnimationLibrary.snapRootBoneXZ(toSave, snapArm);
|
||||
|
||||
// Direkt in die Clip-Bibliothek speichern – das Modell wird nicht modifiziert
|
||||
saveClipToFile(toSave, snapArm, clipsDir.resolve(saveName + ".j3o"));
|
||||
// Für den aktuellen Preview-Session auch auf das Modell anwenden (wenn geladen)
|
||||
if (!isJ3oSource) {
|
||||
// Direkt in die Clip-Bibliothek speichern – snap passiert nur im Spiel zur Laufzeit
|
||||
saveClipToFile(toSave, snapArm, clipsDir.resolve(saveName + ".j3o"));
|
||||
saved++;
|
||||
}
|
||||
// Für den aktuellen Preview auch auf das Modell anwenden (wenn geladen)
|
||||
if (targetAC != null) targetAC.addAnimClip(toSave);
|
||||
saved++;
|
||||
}
|
||||
// Temporäre GLB aus clips/ löschen (nur wenn sie dort drin liegt – nicht externe Dateien)
|
||||
if (saved > 0 && animAssetPath.matches(".*\\.(glb|gltf)$")
|
||||
// Temporäre GLB aus clips/ löschen (nur wenn sie dort drin liegt – nicht externe Dateien, nie FBX)
|
||||
if (saved > 0 && animAssetPath.matches("(?i).*\\.(glb|gltf)$")
|
||||
&& !Path.of(animAssetPath).isAbsolute()) {
|
||||
Path srcGlb = ASSET_ROOT.resolve(animAssetPath.replace('/', java.io.File.separatorChar));
|
||||
try {
|
||||
@@ -578,9 +626,13 @@ public class AnimPreviewState extends BaseAppState {
|
||||
List<String> clips = new ArrayList<>();
|
||||
if (currentModel != null) collectClips(currentModel, clips);
|
||||
input.animPreviewClips.set(Collections.unmodifiableList(clips));
|
||||
input.animPreviewStatus = saved + " Clip(s) in animations/clips/ gespeichert"
|
||||
+ (retarget ? " (retargeted)" : " (direkt)");
|
||||
if (saved > 0) input.animImportCompleted = true;
|
||||
if (isJ3oSource) {
|
||||
input.animPreviewStatus = "Clip geladen (Preview)";
|
||||
} else {
|
||||
input.animPreviewStatus = saved + " Clip(s) in Characters/clips/ gespeichert"
|
||||
+ (retarget ? " (retargeted)" : " (direkt)");
|
||||
if (saved > 0) input.animImportCompleted = true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("[AnimPreview] Fehler beim Importieren von {}", animAssetPath, e);
|
||||
input.animPreviewStatus = "Fehler beim Hinzufügen: " + e.getMessage();
|
||||
@@ -897,10 +949,10 @@ public class AnimPreviewState extends BaseAppState {
|
||||
ac.addAnimClip(renamed);
|
||||
// kein saveModel() – Quell-Modell bleibt unverändert
|
||||
|
||||
// Als eigenständige .j3o nach animations/clips/ exportieren
|
||||
// Als eigenständige .j3o nach Characters/clips/ exportieren
|
||||
try {
|
||||
SkinningControl sc = findControl(currentModel, SkinningControl.class);
|
||||
java.nio.file.Path clipsDir = ASSET_ROOT.resolve("animations").resolve("clips");
|
||||
java.nio.file.Path clipsDir = ASSET_ROOT.resolve("Characters").resolve("clips");
|
||||
saveClipToFile(renamed, sc != null ? sc.getArmature() : null,
|
||||
clipsDir.resolve(req.newName() + ".j3o"));
|
||||
java.util.List<String> clips = new java.util.ArrayList<>();
|
||||
@@ -916,7 +968,7 @@ public class AnimPreviewState extends BaseAppState {
|
||||
|
||||
private void executeAnimSetSave(SharedInput.AnimSetSaveRequest req) {
|
||||
try {
|
||||
java.nio.file.Path setDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||
java.nio.file.Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets");
|
||||
java.nio.file.Files.createDirectories(setDir);
|
||||
de.blight.game.animation.AnimSet animSet = new de.blight.game.animation.AnimSet();
|
||||
animSet.setClips(req.clips());
|
||||
@@ -929,8 +981,8 @@ public class AnimPreviewState extends BaseAppState {
|
||||
}
|
||||
|
||||
private void executeAnimEmbed(SharedInput.AnimEmbedRequest req) {
|
||||
java.nio.file.Path setDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||
java.nio.file.Path clipsDir = ASSET_ROOT.resolve("animations").resolve("clips");
|
||||
java.nio.file.Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets");
|
||||
java.nio.file.Path clipsDir = ASSET_ROOT.resolve("Characters").resolve("clips");
|
||||
|
||||
de.blight.game.animation.AnimSet set;
|
||||
try {
|
||||
@@ -1096,15 +1148,461 @@ public class AnimPreviewState extends BaseAppState {
|
||||
BinaryExporter.getInstance().save(holder, outFile.toFile());
|
||||
}
|
||||
|
||||
/** Returns "animations/clips/<name>.<ext>" for the first matching file, or null if not found. */
|
||||
// ── FBX-Import ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Importiert eine FBX-Datei als Character-Modell nach Models/Chars/.
|
||||
* Prüft vorab ob ein Mixamo-Skelett (mixamorig:-Präfix) vorhanden ist.
|
||||
* Ergebnis wird via charModelImportResult zurück an die UI gemeldet.
|
||||
*/
|
||||
private void importFbxAsCharacterModel(String absolutePath) {
|
||||
java.io.File file = new java.io.File(absolutePath);
|
||||
if (!file.exists()) {
|
||||
input.charModelImportResult.set(new SharedInput.CharModelImportResult(
|
||||
null, "Datei nicht gefunden: " + absolutePath));
|
||||
return;
|
||||
}
|
||||
String basename = file.getName()
|
||||
.replaceFirst("(?i)\\.[^.]+$", "")
|
||||
.replaceAll("[^a-zA-Z0-9_\\-]", "_");
|
||||
|
||||
try {
|
||||
assets.registerLocator(file.getParent(), com.jme3.asset.plugins.FileLocator.class);
|
||||
String fbmBase = file.getName().replaceAll("\\.[^.]+$", "");
|
||||
java.io.File fbmDir = new java.io.File(file.getParentFile(), fbmBase + ".fbm");
|
||||
if (fbmDir.isDirectory()) {
|
||||
assets.registerLocator(fbmDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class);
|
||||
}
|
||||
|
||||
com.github.stephengold.wrench.LwjglAssetKey key =
|
||||
new com.github.stephengold.wrench.LwjglAssetKey(file.getName());
|
||||
key.setVerboseLogging(true);
|
||||
Spatial model = (Spatial) assets.loadAsset(key);
|
||||
|
||||
// Mixamo-Skelett prüfen
|
||||
if (!hasMixamoSkeleton(model)) {
|
||||
input.charModelImportResult.set(new SharedInput.CharModelImportResult(
|
||||
null, "Kein Mixamo-Skelett gefunden (kein 'mixamorig:'-Joint).\n"
|
||||
+ "Bitte Mixamo-FBX mit 'With Skin' exportieren."));
|
||||
return;
|
||||
}
|
||||
|
||||
java.io.File embeddedDir = extractEmbeddedTextures(file, model);
|
||||
if (embeddedDir != null) {
|
||||
assets.registerLocator(embeddedDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class);
|
||||
}
|
||||
|
||||
fixFbxMaterials(model);
|
||||
fixFbxMeshAndSkeleton(model);
|
||||
fixBrokenTexturePaths(model, file.getParentFile());
|
||||
|
||||
// Textur-Keys entfernen → BinaryExporter bettet Image-Daten direkt in die j3o ein.
|
||||
// Filename-only Keys ("character.png") wären nach dem nächsten Editor-Start nicht
|
||||
// mehr auflösbar (kein Temp-Dir mehr registriert) → gelbe Fallback-Textur.
|
||||
embedTexturesInModel(model);
|
||||
|
||||
model.setLocalTranslation(Vector3f.ZERO);
|
||||
model.updateGeometricState();
|
||||
float scale = 0.01f;
|
||||
if (model.getWorldBound() instanceof com.jme3.bounding.BoundingBox bb) {
|
||||
float h = bb.getYExtent() * 2f;
|
||||
if (h > 0.01f) scale = 2.0f / h;
|
||||
}
|
||||
model.setLocalScale(scale);
|
||||
model.updateGeometricState();
|
||||
|
||||
// Animationsclips entfernen
|
||||
AnimComposer ac = findControl(model, AnimComposer.class);
|
||||
if (ac != null) {
|
||||
for (AnimClip c : new java.util.ArrayList<>(ac.getAnimClips())) {
|
||||
ac.removeAnimClip(c);
|
||||
}
|
||||
}
|
||||
|
||||
java.nio.file.Path destJ3o = ASSET_ROOT.resolve("Characters").resolve("Models").resolve(basename + ".j3o");
|
||||
java.nio.file.Files.createDirectories(destJ3o.getParent());
|
||||
com.jme3.export.binary.BinaryExporter.getInstance().save(model, destJ3o.toFile());
|
||||
|
||||
String relStr = ASSET_ROOT.relativize(destJ3o).toString().replace('\\', '/');
|
||||
for (String root : new String[]{"blight-assets/bin/main", "blight-assets/build/resources/main"}) {
|
||||
java.nio.file.Path mirror = java.nio.file.Paths.get(root).resolve(
|
||||
ASSET_ROOT.relativize(destJ3o));
|
||||
if (java.nio.file.Files.exists(mirror)) {
|
||||
try {
|
||||
java.nio.file.Files.copy(destJ3o, mirror,
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
assets.deleteFromCache(new com.jme3.asset.ModelKey(relStr));
|
||||
LOG.info("[CharImport] Character-Modell gespeichert: {} (scale={})", destJ3o, scale);
|
||||
input.charModelImportResult.set(new SharedInput.CharModelImportResult(relStr, null));
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.error("[CharImport] Fehler bei {}", absolutePath, e);
|
||||
input.charModelImportResult.set(new SharedInput.CharModelImportResult(
|
||||
null, "Import-Fehler: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt den AssetKey aller Texturen im Baum, so dass BinaryExporter die Image-Daten direkt
|
||||
* in die j3o-Datei einbettet (kein externer Textur-Verweis → funktioniert nach Editor-Restart).
|
||||
* JME3: Texture mit key != null → AssetLink-Referenz gespeichert; key == null → Image inline.
|
||||
*/
|
||||
private void embedTexturesInModel(Spatial s) {
|
||||
if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) {
|
||||
com.jme3.material.Material mat = geo.getMaterial();
|
||||
for (com.jme3.material.MatParam mp : new java.util.ArrayList<>(mat.getParams())) {
|
||||
if (!(mp.getValue() instanceof com.jme3.texture.Texture tex)) continue;
|
||||
if (tex.getImage() == null) continue;
|
||||
com.jme3.texture.Texture2D embedded = new com.jme3.texture.Texture2D(tex.getImage());
|
||||
embedded.setMinFilter(tex.getMinFilter());
|
||||
embedded.setMagFilter(tex.getMagFilter());
|
||||
embedded.setWrap(com.jme3.texture.Texture.WrapAxis.S,
|
||||
tex.getWrap(com.jme3.texture.Texture.WrapAxis.S));
|
||||
embedded.setWrap(com.jme3.texture.Texture.WrapAxis.T,
|
||||
tex.getWrap(com.jme3.texture.Texture.WrapAxis.T));
|
||||
mat.setTexture(mp.getName(), embedded);
|
||||
}
|
||||
}
|
||||
if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) embedTexturesInModel(child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kopiert Textur-Dateien in ein permanentes Asset-Verzeichnis und setzt die Material-Referenzen
|
||||
* auf asset-relative Pfade um, damit das j3o auch nach dem nächsten Editor-Start funktioniert.
|
||||
*/
|
||||
private void relinkTexturesAsAssets(Spatial s,
|
||||
java.util.List<java.io.File> srcDirs,
|
||||
java.nio.file.Path texDestDir,
|
||||
String texRelBase) {
|
||||
if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) {
|
||||
com.jme3.material.Material mat = geo.getMaterial();
|
||||
for (com.jme3.material.MatParam mp : new java.util.ArrayList<>(mat.getParams())) {
|
||||
if (!(mp.getValue() instanceof com.jme3.texture.Texture tex)) continue;
|
||||
com.jme3.asset.AssetKey<?> key = tex.getKey();
|
||||
if (key == null) continue;
|
||||
String filename = new java.io.File(key.getName()).getName();
|
||||
if (filename.isEmpty()) continue;
|
||||
if (key.getName().contains("/") || key.getName().contains("\\")) continue; // bereits relativ
|
||||
|
||||
// Quelldatei suchen
|
||||
java.io.File src = null;
|
||||
for (java.io.File dir : srcDirs) {
|
||||
java.io.File c = new java.io.File(dir, filename);
|
||||
if (c.exists()) { src = c; break; }
|
||||
}
|
||||
if (src == null) {
|
||||
LOG.warn("[CharImport] Textur-Quelle nicht gefunden: {}", filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
// In Asset-Verzeichnis kopieren
|
||||
java.nio.file.Path dest = texDestDir.resolve(filename);
|
||||
try {
|
||||
java.nio.file.Files.copy(src.toPath(), dest,
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (Exception ex) {
|
||||
LOG.warn("[CharImport] Kopieren fehlgeschlagen ({}): {}", filename, ex.getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Material mit asset-relativem Key neu laden und setzen
|
||||
String relPath = texRelBase + "/" + filename;
|
||||
try {
|
||||
com.jme3.asset.TextureKey newKey = new com.jme3.asset.TextureKey(relPath, true);
|
||||
newKey.setGenerateMips(true);
|
||||
assets.deleteFromCache(newKey);
|
||||
com.jme3.texture.Texture reloaded = assets.loadTexture(newKey);
|
||||
mat.setTexture(mp.getName(), reloaded);
|
||||
LOG.info("[CharImport] Textur → {}", relPath);
|
||||
} catch (Exception ex) {
|
||||
LOG.warn("[CharImport] Textur-Reload fehlgeschlagen ({}): {}", relPath, ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren())
|
||||
relinkTexturesAsAssets(child, srcDirs, texDestDir, texRelBase);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasMixamoSkeleton(Spatial model) {
|
||||
SkinningControl sc = findControl(model, SkinningControl.class);
|
||||
if (sc == null) return false;
|
||||
com.jme3.anim.Armature arm = sc.getArmature();
|
||||
for (int i = 0; i < arm.getJointCount(); i++) {
|
||||
if (arm.getJoint(i).getName().startsWith("mixamorig:")) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── FBX-Hilfsroutinen (portiert aus FbxTestApp) ───────────────────────────
|
||||
|
||||
/**
|
||||
* Bereinigt FBX-Mesh-Knoten und Skelett nach MonkeyWrench-Import.
|
||||
* Assimp konvertiert Animations-Tracks zu Y-up, aber NICHT die Bind-Pose-Translation
|
||||
* des Root-Joints. Der Root-Joint liegt in Z-up vor: t=(x,y,z) wird zu t=(x,z,-y).
|
||||
* Danach werden die inversen Bind-Matrizen aller Joints neu berechnet.
|
||||
*/
|
||||
private void fixFbxMeshAndSkeleton(Spatial model) {
|
||||
fixFbxZeroMeshRotations(model);
|
||||
SkinningControl skinCtrl = findControl(model, SkinningControl.class);
|
||||
if (skinCtrl == null) return;
|
||||
com.jme3.anim.Armature arm = skinCtrl.getArmature();
|
||||
// Assimp konvertiert Mesh-Vertices zu Y-up, aber NICHT die Bind-Pose-Translation
|
||||
// des Root-Joints – der liegt noch in Z-up vor: (x,y,z) → (x,z,-y).
|
||||
boolean fixed = false;
|
||||
for (int i = 0; i < arm.getJointCount(); i++) {
|
||||
com.jme3.anim.Joint j = arm.getJoint(i);
|
||||
if (j.getParent() == null) {
|
||||
com.jme3.math.Vector3f t = j.getLocalTransform().getTranslation();
|
||||
float ox = t.x, oy = t.y, oz = t.z;
|
||||
t.set(ox, oz, -oy);
|
||||
LOG.debug("[fixSkeleton] Root-Joint '{}': ({},{},{}) -> ({},{},{})",
|
||||
j.getName(), ox, oy, oz, t.x, t.y, t.z);
|
||||
fixed = true;
|
||||
}
|
||||
}
|
||||
if (!fixed) return;
|
||||
// Inverse Bind-Matrizen aus den korrigierten lokalen Transforms neu berechnen.
|
||||
java.util.HashMap<com.jme3.anim.Joint, com.jme3.math.Matrix4f> modelMats = new java.util.HashMap<>();
|
||||
for (int i = 0; i < arm.getJointCount(); i++) {
|
||||
buildFbxJointModelMatrix(arm.getJoint(i), modelMats);
|
||||
}
|
||||
for (int i = 0; i < arm.getJointCount(); i++) {
|
||||
com.jme3.anim.Joint j = arm.getJoint(i);
|
||||
com.jme3.math.Matrix4f ibm = j.getInverseModelBindMatrix();
|
||||
if (ibm != null) ibm.set(modelMats.get(j).invert());
|
||||
}
|
||||
arm.saveBindPose();
|
||||
arm.saveInitialPose();
|
||||
}
|
||||
|
||||
private com.jme3.math.Matrix4f buildFbxJointModelMatrix(
|
||||
com.jme3.anim.Joint j,
|
||||
java.util.HashMap<com.jme3.anim.Joint, com.jme3.math.Matrix4f> cache) {
|
||||
com.jme3.math.Matrix4f cached = cache.get(j);
|
||||
if (cached != null) return cached;
|
||||
com.jme3.math.Matrix4f local = j.getLocalTransform().toTransformMatrix();
|
||||
com.jme3.math.Matrix4f result = (j.getParent() != null)
|
||||
? buildFbxJointModelMatrix(j.getParent(), cache).mult(local)
|
||||
: local;
|
||||
cache.put(j, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void fixFbxZeroMeshRotations(Spatial s) {
|
||||
if (!(s instanceof Node n)) return;
|
||||
boolean hasSkeleton = n.getControl(AnimComposer.class) != null
|
||||
|| n.getControl(SkinningControl.class) != null;
|
||||
if (!hasSkeleton) {
|
||||
com.jme3.math.Quaternion rot = n.getLocalRotation();
|
||||
boolean isIdentity = Math.abs(rot.getW() - 1f) < 0.01f && Math.abs(rot.getX()) < 0.01f
|
||||
&& Math.abs(rot.getY()) < 0.01f && Math.abs(rot.getZ()) < 0.01f;
|
||||
if (!isIdentity) n.setLocalRotation(com.jme3.math.Quaternion.IDENTITY);
|
||||
}
|
||||
for (Spatial child : n.getChildren()) fixFbxZeroMeshRotations(child);
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert PBR-Materialien in Lighting.j3md (Editor hat keine LightProbe).
|
||||
* Vorhandene Lighting.j3md-Materialien werden nur um DiffuseColor ergänzt.
|
||||
*/
|
||||
private void fixFbxMaterials(Spatial s) {
|
||||
if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) {
|
||||
com.jme3.material.Material mat = geo.getMaterial();
|
||||
String defName = mat.getMaterialDef().getName();
|
||||
if (defName.contains("Lighting")) {
|
||||
com.jme3.material.MatParam dm = mat.getParam("DiffuseMap");
|
||||
if (dm != null && dm.getValue() instanceof com.jme3.texture.Texture) {
|
||||
mat.setColor("Diffuse", com.jme3.math.ColorRGBA.White.clone());
|
||||
mat.setColor("Ambient", new com.jme3.math.ColorRGBA(0.45f, 0.45f, 0.45f, 1f));
|
||||
}
|
||||
return;
|
||||
}
|
||||
com.jme3.texture.Texture diffuseTex = null;
|
||||
com.jme3.texture.Texture normalTex = null;
|
||||
com.jme3.math.ColorRGBA baseColor = null;
|
||||
for (String nm : new String[]{"BaseColorMap", "DiffuseMap", "Albedo", "ColorMap"}) {
|
||||
com.jme3.material.MatParam p = mat.getParam(nm);
|
||||
if (p != null && p.getValue() instanceof com.jme3.texture.Texture t) { diffuseTex = t; break; }
|
||||
}
|
||||
for (String nm : new String[]{"NormalMap", "NormalCamera"}) {
|
||||
com.jme3.material.MatParam p = mat.getParam(nm);
|
||||
if (p != null && p.getValue() instanceof com.jme3.texture.Texture t) { normalTex = t; break; }
|
||||
}
|
||||
for (String nm : new String[]{"BaseColor", "Diffuse"}) {
|
||||
com.jme3.material.MatParam p = mat.getParam(nm);
|
||||
if (p != null && p.getValue() instanceof com.jme3.math.ColorRGBA c) { baseColor = c.clone(); break; }
|
||||
}
|
||||
com.jme3.material.Material lit = new com.jme3.material.Material(assets, "Common/MatDefs/Light/Lighting.j3md");
|
||||
lit.setBoolean("UseMaterialColors", true);
|
||||
if (diffuseTex != null) {
|
||||
lit.setTexture("DiffuseMap", diffuseTex);
|
||||
lit.setColor("Diffuse", com.jme3.math.ColorRGBA.White.clone());
|
||||
lit.setColor("Ambient", new com.jme3.math.ColorRGBA(0.45f, 0.45f, 0.45f, 1f));
|
||||
} else {
|
||||
com.jme3.math.ColorRGBA col = baseColor != null ? baseColor : com.jme3.math.ColorRGBA.LightGray.clone();
|
||||
lit.setColor("Diffuse", col);
|
||||
lit.setColor("Ambient", col.mult(0.4f).setAlpha(1f));
|
||||
}
|
||||
if (normalTex != null) lit.setTexture("NormalMap", normalTex);
|
||||
lit.setColor("Specular", new com.jme3.math.ColorRGBA(0.1f, 0.1f, 0.1f, 1f));
|
||||
lit.setFloat("Shininess", 24f);
|
||||
geo.setMaterial(lit);
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) fixFbxMaterials(child);
|
||||
}
|
||||
}
|
||||
|
||||
/** Sammelt alle Dateinamen aus Textur-Params eines Spatial-Baums. */
|
||||
private java.util.Set<String> collectTextureFilenames(Spatial s) {
|
||||
java.util.Set<String> names = new java.util.LinkedHashSet<>();
|
||||
if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) {
|
||||
for (com.jme3.material.MatParam p : geo.getMaterial().getParams()) {
|
||||
if (p.getValue() instanceof com.jme3.texture.Texture tex && tex.getKey() != null) {
|
||||
String name = new java.io.File(tex.getKey().getName()).getName();
|
||||
if (!name.isEmpty()) names.add(name);
|
||||
}
|
||||
}
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) names.addAll(collectTextureFilenames(child));
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht eingebettete PNG-Texturen im FBX-Binär (Dateiname → PNG-Magic → IEND)
|
||||
* und schreibt sie in ein temporäres Verzeichnis.
|
||||
*/
|
||||
private java.io.File extractEmbeddedTextures(java.io.File fbxFile, Spatial model) {
|
||||
java.util.Set<String> filenames = collectTextureFilenames(model);
|
||||
if (filenames.isEmpty()) return null;
|
||||
try {
|
||||
byte[] data = java.nio.file.Files.readAllBytes(fbxFile.toPath());
|
||||
java.io.File tempDir = new java.io.File(System.getProperty("java.io.tmpdir"),
|
||||
"fbxtex_" + Math.abs(fbxFile.getAbsolutePath().hashCode()));
|
||||
tempDir.mkdirs();
|
||||
byte[] pngMagic = {(byte)0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A};
|
||||
byte[] iend = {0x00,0x00,0x00,0x00,0x49,0x45,0x4E,0x44,(byte)0xAE,0x42,0x60,(byte)0x82};
|
||||
boolean any = false;
|
||||
for (String filename : filenames) {
|
||||
byte[] nameBytes = filename.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
|
||||
int namePos = binaryIndexOf(data, nameBytes, 0);
|
||||
if (namePos < 0) continue;
|
||||
int pngPos = binaryIndexOf(data, pngMagic, namePos);
|
||||
if (pngPos < 0 || pngPos - namePos > 65536) continue;
|
||||
int endPos = binaryIndexOf(data, iend, pngPos + 8);
|
||||
if (endPos < 0) continue;
|
||||
byte[] png = java.util.Arrays.copyOfRange(data, pngPos, endPos + iend.length);
|
||||
java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), png);
|
||||
LOG.info("[FbxImport] Embedded-Textur extrahiert: {} ({} B)", filename, png.length);
|
||||
any = true;
|
||||
}
|
||||
return any ? tempDir : null;
|
||||
} catch (Exception e) {
|
||||
LOG.warn("[FbxImport] Embedded-Extraktion fehlgeschlagen: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static int binaryIndexOf(byte[] data, byte[] pattern, int from) {
|
||||
outer:
|
||||
for (int i = from, max = data.length - pattern.length; i <= max; i++) {
|
||||
for (int j = 0; j < pattern.length; j++) {
|
||||
if (data[i + j] != pattern[j]) continue outer;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Ersetzt kaputte Textur-Pfade (z.B. Mixamo-Server-Pfade) durch Reload per Dateiname. */
|
||||
private void fixBrokenTexturePaths(Spatial s, java.io.File baseDir) {
|
||||
if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) {
|
||||
com.jme3.material.Material mat = geo.getMaterial();
|
||||
for (com.jme3.material.MatParam mp : new java.util.ArrayList<>(mat.getParams())) {
|
||||
if (!(mp.getValue() instanceof com.jme3.texture.Texture tex)) continue;
|
||||
com.jme3.asset.AssetKey<?> key = tex.getKey();
|
||||
if (key == null) continue;
|
||||
String filename = new java.io.File(key.getName()).getName();
|
||||
if (filename.isEmpty()) continue;
|
||||
try {
|
||||
com.jme3.asset.TextureKey texKey = new com.jme3.asset.TextureKey(filename, true);
|
||||
texKey.setGenerateMips(true);
|
||||
com.jme3.texture.Texture reloaded = assets.loadTexture(texKey);
|
||||
mat.setTexture(mp.getName(), reloaded);
|
||||
continue;
|
||||
} catch (Exception ignored) {}
|
||||
try {
|
||||
final String fn = filename;
|
||||
java.nio.file.Path found = java.nio.file.Files.walk(baseDir.toPath(), 3)
|
||||
.filter(p -> p.getFileName().toString().equalsIgnoreCase(fn))
|
||||
.findFirst().orElse(null);
|
||||
if (found != null) {
|
||||
assets.registerLocator(found.getParent().toAbsolutePath().toString(),
|
||||
com.jme3.asset.plugins.FileLocator.class);
|
||||
com.jme3.asset.TextureKey texKey = new com.jme3.asset.TextureKey(filename, true);
|
||||
texKey.setGenerateMips(true);
|
||||
mat.setTexture(mp.getName(), assets.loadTexture(texKey));
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) fixBrokenTexturePaths(child, baseDir);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns "Characters/clips/<name>.<ext>" for the first matching file, or null if not found. */
|
||||
private static String resolveClipFile(java.nio.file.Path clipsDir, String clipName) {
|
||||
for (String ext : new String[]{".j3o", ".glb", ".gltf", ".fbx"}) {
|
||||
if (Files.exists(clipsDir.resolve(clipName + ext)))
|
||||
return "animations/clips/" + clipName + ext;
|
||||
return "Characters/clips/" + clipName + ext;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Leitet Clip-Tracks per Gelenk-Name auf die Ziel-Armatur um (FbxTestApp-Ansatz für Same-Rig). */
|
||||
private static AnimClip redirectAnimClipByName(AnimClip src, com.jme3.anim.Armature targetArm) {
|
||||
List<com.jme3.anim.AnimTrack<?>> newTracks = new ArrayList<>();
|
||||
for (com.jme3.anim.AnimTrack<?> track : src.getTracks()) {
|
||||
if (!(track instanceof com.jme3.anim.TransformTrack tt)) continue;
|
||||
if (!(tt.getTarget() instanceof com.jme3.anim.Joint srcJoint)) continue;
|
||||
com.jme3.anim.Joint dstJoint = targetArm.getJoint(srcJoint.getName());
|
||||
if (dstJoint == null) continue;
|
||||
|
||||
// Bind-Pose-Delta: "Without Skin"-FBX kann dieselbe Gesamt-Translation
|
||||
// anders auf den Base-Joint und $AssimpFbx$-Helper aufteilen als "With Skin".
|
||||
// Alle Translation-Keyframes um die Differenz (dstInit - srcInit) verschieben,
|
||||
// damit die Modell-Bind-Pose korrekt abgebildet wird.
|
||||
com.jme3.math.Vector3f[] translations = tt.getTranslations();
|
||||
if (translations != null && translations.length > 0) {
|
||||
com.jme3.math.Vector3f srcInitT = srcJoint.getInitialTransform().getTranslation();
|
||||
com.jme3.math.Vector3f dstInitT = dstJoint.getInitialTransform().getTranslation();
|
||||
com.jme3.math.Vector3f delta = dstInitT.subtract(srcInitT);
|
||||
if (delta.lengthSquared() > 1e-6f) {
|
||||
com.jme3.math.Vector3f[] retargeted = new com.jme3.math.Vector3f[translations.length];
|
||||
for (int i = 0; i < translations.length; i++) {
|
||||
retargeted[i] = translations[i].add(delta);
|
||||
}
|
||||
translations = retargeted;
|
||||
}
|
||||
}
|
||||
|
||||
newTracks.add(new com.jme3.anim.TransformTrack(dstJoint, tt.getTimes(),
|
||||
translations, tt.getRotations(), tt.getScales()));
|
||||
}
|
||||
if (newTracks.isEmpty()) return null;
|
||||
AnimClip result = new AnimClip(src.getName());
|
||||
result.setTracks(newTracks.toArray(new com.jme3.anim.AnimTrack[0]));
|
||||
LOG.debug("[AnimPreview] redirectByName '{}': {} Tracks, length={}", src.getName(), newTracks.size(), result.getLength());
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean haveSameBoneNames(com.jme3.anim.Armature a, com.jme3.anim.Armature b) {
|
||||
if (a.getJointCount() != b.getJointCount()) return false;
|
||||
java.util.Set<String> namesA = new java.util.HashSet<>();
|
||||
|
||||
@@ -1362,7 +1362,7 @@ public class DialogEditorView extends BorderPane {
|
||||
odt.normal("Exportiert: " + LocalDate.now());
|
||||
odt.empty();
|
||||
|
||||
Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "character");
|
||||
Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "Characters");
|
||||
for (GameCharacter gc : CharacterIO.loadAll(charDir)) {
|
||||
if (!(gc instanceof NPC npc)) continue;
|
||||
if (npc.getDialogOptions() == null || npc.getDialogOptions().isEmpty()) continue;
|
||||
|
||||
@@ -290,7 +290,7 @@ public class TtsGeneratorDialog extends Dialog<Void> {
|
||||
|
||||
private List<String> collectHeroKeys() {
|
||||
List<String> keys = new ArrayList<>();
|
||||
Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "character");
|
||||
Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "Characters");
|
||||
for (GameCharacter gc : CharacterIO.loadAll(charDir)) {
|
||||
if (!(gc instanceof NPC npc) || npc.getDialogOptions() == null) continue;
|
||||
for (Map.Entry<String, DialogOption> e : npc.getDialogOptions().entrySet()) {
|
||||
|
||||
Reference in New Issue
Block a user