Animationen jetzt heil und Keyframes eingebaut

This commit is contained in:
2026-06-23 17:44:22 +02:00
parent 914bf6e673
commit 79f9cf12a3
9 changed files with 384 additions and 390 deletions

View File

@@ -152,15 +152,17 @@ public class EditorApp extends Application {
// AnimSet-Editor
private ListView<String> animSetClipListView;
private ListView<String> animSetActionListView;
private ListView<String> animSetSinkListView;
private ListView<String> animSetAnchorBoneListView;
private String animSetPendingPlayClip = null;
private ComboBox<String> animSetModelCombo;
private boolean animSetDirty = false;
private String animSetCurrentName = null;
private Path animSetCurrentDir = null;
private java.util.List<String> animJointNames = new java.util.ArrayList<>();
private Label animSetBonesLabel;
// Motion-Keyframe-Editor (innerhalb AnimSet-Editor)
private javafx.scene.control.ListView<de.blight.game.animation.AnimKeyframe> animSetKfListView;
private javafx.collections.ObservableList<de.blight.game.animation.AnimKeyframe> animSetKfList =
javafx.collections.FXCollections.observableArrayList();
private java.util.Map<String, java.util.List<de.blight.game.animation.AnimKeyframe>>
animSetMotionKeyframes = new java.util.LinkedHashMap<>();
// Character-Editor-Zustand
private de.blight.editor.ui.DialogEditorView dialogEditorView;
@@ -453,19 +455,6 @@ public class EditorApp extends Application {
animClipListView.getItems().setAll(newClips);
if (!newClips.isEmpty()) animClipListView.getSelectionModel().selectFirst();
}
java.util.List<String> newJoints = input.animPreviewJointNames.getAndSet(null);
if (newJoints != null) {
animJointNames = new java.util.ArrayList<>(newJoints);
if (animSetBonesLabel != null) {
if (animJointNames.isEmpty()) {
animSetBonesLabel.setText("Kein Armature gefunden");
animSetBonesLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #c66;");
} else {
animSetBonesLabel.setText(animJointNames.size() + " Joints geladen");
animSetBonesLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #6a6;");
}
}
}
// AnimSet-Editor: nach Clip-Load automatisch abspielen
if (newClips != null && animSetPendingPlayClip != null) {
input.animPreviewPlayClip = animSetPendingPlayClip;
@@ -8276,7 +8265,25 @@ public class EditorApp extends Application {
removeClipBtn.setDisable(true);
animSetClipListView.getSelectionModel().selectedItemProperty()
.addListener((obs, ov, nv) -> removeClipBtn.setDisable(nv == null));
.addListener((obs, ov, nv) -> {
removeClipBtn.setDisable(nv == null);
// Keyframes des alten Clips sichern
if (ov != null && animSetKfList != null) {
animSetMotionKeyframes.put(ov, new java.util.ArrayList<>(animSetKfList));
}
// Keyframes des neuen Clips laden
if (animSetKfList != null) {
animSetKfList.clear();
if (nv != null) {
var kfs = animSetMotionKeyframes.getOrDefault(nv, new java.util.ArrayList<>());
animSetKfList.setAll(kfs);
// Clip direkt in Vorschau abspielen
input.animPreviewPlayClip = nv;
}
}
// KF-Bereich aktivieren/deaktivieren
if (animSetKfListView != null) animSetKfListView.setDisable(nv == null);
});
addClipBtn.setOnAction(e -> {
org.slf4j.Logger _log = org.slf4j.LoggerFactory.getLogger("AnimSetClipScan");
@@ -8395,196 +8402,72 @@ public class EditorApp extends Application {
HBox.setHgrow(removeActionBtn, Priority.ALWAYS);
inner.getChildren().addAll(animSetActionListView, actionBtns);
// ── Bone-Anchoring ────────────────────────────────────────────────────
inner.getChildren().addAll(new Separator(), sectionTitle("Bone-Anchoring"), new Separator());
// ── Motion Keyframes ──────────────────────────────────────────────────
inner.getChildren().addAll(new Separator(), sectionTitle("Motion Keyframes"), new Separator());
Label anchorHint = new Label("Pro Aktion: Knochen angeben, der auf seiner Welt-Y fixiert bleibt (z. B. SIT_DOWN → foot.l). Hat Vorrang vor manuellem Sink.");
anchorHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
anchorHint.setWrapText(true);
animSetBonesLabel = new Label(animJointNames.isEmpty() ? "Kein Modell geladen" : animJointNames.size() + " Joints geladen");
animSetBonesLabel.setStyle("-fx-font-size: 10; -fx-text-fill: " + (animJointNames.isEmpty() ? "#888;" : "#6a6;"));
Label kfHint = new Label("TX/TZ = charakter-lokal (seitlich/vorwärts), TY = Welt-Y (hoch/runter). RX/RY/RZ in Grad, additiv zur Startrotation. Zeit ≤ Clip-Dauer (Clip vorab in Vorschau abspielen).");
kfHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
kfHint.setWrapText(true);
animSetAnchorBoneListView = new ListView<>();
animSetAnchorBoneListView.setPrefHeight(110);
if (animSet.getAnchorBoneMap() != null) {
for (var e2 : animSet.getAnchorBoneMap().entrySet()) {
animSetAnchorBoneListView.getItems().add(e2.getKey() + "" + e2.getValue());
}
// Keyframes aus AnimSet laden
animSetMotionKeyframes = new java.util.LinkedHashMap<>();
for (var entry : animSet.getMotionKeyframes().entrySet()) {
animSetMotionKeyframes.put(entry.getKey(), new java.util.ArrayList<>(entry.getValue()));
}
animSetKfList = javafx.collections.FXCollections.observableArrayList();
animSetKfList.addListener((javafx.collections.ListChangeListener<de.blight.game.animation.AnimKeyframe>)
change -> updateAnimPreviewKfOffset());
Button addAnchorBtn = new Button("+ Hinzufügen…");
Button removeAnchorBtn = new Button("- Entfernen");
addAnchorBtn.setMaxWidth(Double.MAX_VALUE);
removeAnchorBtn.setMaxWidth(Double.MAX_VALUE);
removeAnchorBtn.setDisable(true);
animSetAnchorBoneListView.getSelectionModel().selectedItemProperty()
.addListener((obs, ov, nv) -> removeAnchorBtn.setDisable(nv == null));
addAnchorBtn.setOnAction(e -> {
// Pending joint names aus JME3-Thread abholen (falls Timer sie noch nicht konsumiert hat)
java.util.List<String> fresh = input.animPreviewJointNames.getAndSet(null);
if (fresh != null) {
animJointNames = new java.util.ArrayList<>(fresh);
if (animSetBonesLabel != null) {
animSetBonesLabel.setText(animJointNames.isEmpty() ? "Kein Armature gefunden" : animJointNames.size() + " Joints geladen");
animSetBonesLabel.setStyle("-fx-font-size: 10; -fx-text-fill: " + (animJointNames.isEmpty() ? "#c66;" : "#6a6;"));
}
// ListView mit Zusammenfassung, Doppelklick öffnet Edit-Dialog
animSetKfListView = new javafx.scene.control.ListView<>(animSetKfList);
animSetKfListView.setPrefHeight(150);
animSetKfListView.setPlaceholder(new Label("Keine Keyframes [+ Keyframe] zum Hinzufügen"));
animSetKfListView.setCellFactory(lv -> new javafx.scene.control.ListCell<>() {
@Override protected void updateItem(de.blight.game.animation.AnimKeyframe kf, boolean empty) {
super.updateItem(kf, empty);
if (empty || kf == null) { setText(null); return; }
setText(String.format("%.3fs | TX%+.3f TY%+.3f TZ%+.3f | RX%+.1f° RY%+.1f° RZ%+.1f°",
kf.time, kf.tx, kf.ty, kf.tz, kf.rx, kf.ry, kf.rz));
}
ComboBox<de.blight.game.animation.AnimationAction> anchorActionCombo = new ComboBox<>();
anchorActionCombo.getItems().addAll(de.blight.game.animation.AnimationAction.values());
javafx.util.Callback<javafx.scene.control.ListView<de.blight.game.animation.AnimationAction>,
javafx.scene.control.ListCell<de.blight.game.animation.AnimationAction>> acf =
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() + ")");
}
};
anchorActionCombo.setCellFactory(acf);
anchorActionCombo.setButtonCell(acf.call(null));
anchorActionCombo.setMaxWidth(Double.MAX_VALUE);
anchorActionCombo.getSelectionModel().selectFirst();
// Joint-Auswahl: ComboBox mit geladenen Namen, editierbar als Fallback
ComboBox<String> boneCombo = new ComboBox<>();
boneCombo.setEditable(true);
boneCombo.setMaxWidth(Double.MAX_VALUE);
if (animJointNames.isEmpty()) {
boneCombo.setPromptText("Joint-Name (erst Modell laden)");
} else {
boneCombo.getItems().addAll(animJointNames);
boneCombo.setPromptText("Joint auswählen…");
});
animSetKfListView.setOnMouseClicked(ev -> {
if (ev.getClickCount() == 2) {
de.blight.game.animation.AnimKeyframe sel =
animSetKfListView.getSelectionModel().getSelectedItem();
if (sel != null) showAnimKfDialog(sel);
}
javafx.scene.layout.GridPane anchorGrid = new javafx.scene.layout.GridPane();
anchorGrid.setHgap(8); anchorGrid.setVgap(6);
anchorGrid.add(new Label("Aktion:"), 0, 0); anchorGrid.add(anchorActionCombo, 1, 0);
anchorGrid.add(new Label("Joint-Name:"), 0, 1); anchorGrid.add(boneCombo, 1, 1);
javafx.scene.layout.ColumnConstraints anchorCc = new javafx.scene.layout.ColumnConstraints();
anchorCc.setHgrow(Priority.ALWAYS);
anchorGrid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), anchorCc);
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> anchorDlg = new javafx.scene.control.Dialog<>();
anchorDlg.setTitle("Bone-Anchoring konfigurieren");
javafx.scene.control.ButtonType okAnchor = new javafx.scene.control.ButtonType("Setzen",
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
anchorDlg.getDialogPane().getButtonTypes().addAll(okAnchor, javafx.scene.control.ButtonType.CANCEL);
anchorDlg.getDialogPane().setContent(anchorGrid);
anchorDlg.showAndWait().ifPresent(bt -> {
if (bt != okAnchor) {
return;
}
var selAction = anchorActionCombo.getValue();
String bone = boneCombo.getEditor().getText();
if (selAction == null || bone == null || bone.isBlank()) {
return;
}
String newEntry = selAction.name() + "" + bone.trim();
animSetAnchorBoneListView.getItems().removeIf(it -> it.startsWith(selAction.name() + ""));
animSetAnchorBoneListView.getItems().add(newEntry);
animSetDirty = true;
});
});
removeAnchorBtn.setOnAction(e -> {
String sel = animSetAnchorBoneListView.getSelectionModel().getSelectedItem();
// initial deaktiviert wird durch Clip-Selektion gesteuert
animSetKfListView.setDisable(true);
Button addKfBtn = new Button("+ Keyframe");
Button removeKfBtn = new Button("- Entfernen");
addKfBtn.setMaxWidth(Double.MAX_VALUE);
removeKfBtn.setMaxWidth(Double.MAX_VALUE);
addKfBtn.setDisable(true);
removeKfBtn.setDisable(true);
animSetKfListView.getSelectionModel().selectedItemProperty()
.addListener((obs, ov, nv) -> removeKfBtn.setDisable(nv == null));
// addKfBtn folgt Clip-Selektion
animSetClipListView.getSelectionModel().selectedItemProperty()
.addListener((obs, ov, nv) -> addKfBtn.setDisable(nv == null));
addKfBtn.setOnAction(e -> showAnimKfDialog(null));
removeKfBtn.setOnAction(e -> {
de.blight.game.animation.AnimKeyframe sel =
animSetKfListView.getSelectionModel().getSelectedItem();
if (sel != null) {
animSetAnchorBoneListView.getItems().remove(sel);
animSetKfList.remove(sel);
animSetDirty = true;
}
});
HBox anchorBtns = new HBox(6, addAnchorBtn, removeAnchorBtn);
HBox.setHgrow(addAnchorBtn, Priority.ALWAYS);
HBox.setHgrow(removeAnchorBtn, Priority.ALWAYS);
inner.getChildren().addAll(anchorHint, animSetBonesLabel, animSetAnchorBoneListView, anchorBtns);
// ── Sink-Konfiguration (Fallback) ─────────────────────────────────────
inner.getChildren().addAll(new Separator(), sectionTitle("Manueller Sink-Fallback"), new Separator());
Label sinkHint = new Label("Root-Motion-Ersatz: Körper senkt/hebt sich während der Animation.\nNegativ = nach unten (Setzen), Positiv = nach oben.");
sinkHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
sinkHint.setWrapText(true);
animSetSinkListView = new ListView<>();
animSetSinkListView.setPrefHeight(120);
if (animSet.getSinkMap() != null) {
for (var e2 : animSet.getSinkMap().entrySet()) {
animSetSinkListView.getItems().add(e2.getKey() + "" + e2.getValue());
}
}
Button addSinkBtn = new Button("+ Setzen…");
Button removeSinkBtn = new Button("- Entfernen");
addSinkBtn.setMaxWidth(Double.MAX_VALUE);
removeSinkBtn.setMaxWidth(Double.MAX_VALUE);
removeSinkBtn.setDisable(true);
animSetSinkListView.getSelectionModel().selectedItemProperty()
.addListener((obs, ov, nv) -> removeSinkBtn.setDisable(nv == null));
addSinkBtn.setOnAction(e -> {
ComboBox<de.blight.game.animation.AnimationAction> actionSinkCombo = new ComboBox<>();
actionSinkCombo.getItems().addAll(de.blight.game.animation.AnimationAction.values());
javafx.util.Callback<javafx.scene.control.ListView<de.blight.game.animation.AnimationAction>,
javafx.scene.control.ListCell<de.blight.game.animation.AnimationAction>> cf2 =
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() + ")");
}
};
actionSinkCombo.setCellFactory(cf2);
actionSinkCombo.setButtonCell(cf2.call(null));
actionSinkCombo.setMaxWidth(Double.MAX_VALUE);
actionSinkCombo.getSelectionModel().selectFirst();
Spinner<Double> sinkSpinner = new Spinner<>(-3.0, 3.0, 0.0, 0.05);
sinkSpinner.setEditable(true);
sinkSpinner.setMaxWidth(Double.MAX_VALUE);
javafx.scene.layout.GridPane sinkGrid = new javafx.scene.layout.GridPane();
sinkGrid.setHgap(8); sinkGrid.setVgap(6);
sinkGrid.add(new Label("Aktion:"), 0, 0); sinkGrid.add(actionSinkCombo, 1, 0);
sinkGrid.add(new Label("Versatz (m):"), 0, 1); sinkGrid.add(sinkSpinner, 1, 1);
javafx.scene.layout.ColumnConstraints sinkCc = new javafx.scene.layout.ColumnConstraints();
sinkCc.setHgrow(Priority.ALWAYS);
sinkGrid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), sinkCc);
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> sinkDlg = new javafx.scene.control.Dialog<>();
sinkDlg.setTitle("Sink-Wert setzen");
javafx.scene.control.ButtonType okSink = new javafx.scene.control.ButtonType("Setzen",
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
sinkDlg.getDialogPane().getButtonTypes().addAll(okSink, javafx.scene.control.ButtonType.CANCEL);
sinkDlg.getDialogPane().setContent(sinkGrid);
sinkDlg.showAndWait().ifPresent(bt -> {
if (bt != okSink) {
return;
}
var selAction = actionSinkCombo.getValue();
if (selAction == null) {
return;
}
double val = sinkSpinner.getValue();
String newEntry = selAction.name() + "" + val;
// Bestehenden Eintrag für diese Aktion ersetzen
animSetSinkListView.getItems().removeIf(it -> it.startsWith(selAction.name() + ""));
animSetSinkListView.getItems().add(newEntry);
animSetDirty = true;
});
});
removeSinkBtn.setOnAction(e -> {
String sel = animSetSinkListView.getSelectionModel().getSelectedItem();
if (sel != null) {
animSetSinkListView.getItems().remove(sel);
animSetDirty = true;
}
});
HBox sinkBtns = new HBox(6, addSinkBtn, removeSinkBtn);
HBox.setHgrow(addSinkBtn, Priority.ALWAYS);
HBox.setHgrow(removeSinkBtn, Priority.ALWAYS);
inner.getChildren().addAll(sinkHint, animSetSinkListView, sinkBtns);
HBox kfBtns = new HBox(6, addKfBtn, removeKfBtn);
HBox.setHgrow(addKfBtn, Priority.ALWAYS);
HBox.setHgrow(removeKfBtn, Priority.ALWAYS);
inner.getChildren().addAll(kfHint, animSetKfListView, kfBtns);
// ── Vorschau ─────────────────────────────────────────────────────────
inner.getChildren().addAll(new Separator(), sectionTitle("Vorschau"), new Separator());
@@ -8644,6 +8527,117 @@ public class EditorApp extends Application {
return panel;
}
/** Öffnet den Keyframe-Dialog (null = Hinzufügen, non-null = Bearbeiten). */
private void showAnimKfDialog(de.blight.game.animation.AnimKeyframe existing) {
boolean isAdd = (existing == null);
// Zeitlimit: zuletzt gemessene Clip-Dauer aus Preview, sonst 999
float maxTime = input.animPreviewCurrentClipDuration > 0.01f
? input.animPreviewCurrentClipDuration : 999.0f;
float initTime = isAdd
? (animSetKfList.isEmpty() ? 0f : Math.min(animSetKfList.get(animSetKfList.size() - 1).time + 0.5f, maxTime))
: existing.time;
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<Double> spTime = new Spinner<>(0.0, maxTime, initTime, 0.05);
Spinner<Double> spTX = new Spinner<>(-10.0, 10.0, initTX, 0.05);
Spinner<Double> spTY = new Spinner<>(-10.0, 10.0, initTY, 0.05);
Spinner<Double> spTZ = new Spinner<>(-10.0, 10.0, initTZ, 0.05);
Spinner<Double> spRX = new Spinner<>(-360.0, 360.0, initRX, 1.0);
Spinner<Double> spRY = new Spinner<>(-360.0, 360.0, initRY, 1.0);
Spinner<Double> spRZ = new Spinner<>(-360.0, 360.0, initRZ, 1.0);
for (Spinner<Double> sp : new Spinner[]{spTime, spTX, spTY, spTZ, spRX, spRY, spRZ}) {
sp.setEditable(true);
sp.setMaxWidth(Double.MAX_VALUE);
}
String timeLabel = maxTime < 999f
? String.format("Zeit (0 %.2fs):", maxTime)
: "Zeit (s):";
String[][] rows = {
{timeLabel}, {"TX (m):"}, {"TY (m):"}, {"TZ (m):"}, {"RX (°):"}, {"RY (°):"}, {"RZ (°):"}
};
Spinner<?>[] sps = {spTime, 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<javafx.scene.control.ButtonType> kfDlg =
new javafx.scene.control.Dialog<>();
kfDlg.setTitle(isAdd ? "Keyframe hinzufügen" : "Keyframe 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;
float t = (float) Math.min(spTime.getValue(), maxTime);
if (isAdd) {
de.blight.game.animation.AnimKeyframe kf = new de.blight.game.animation.AnimKeyframe(
t,
spTX.getValue().floatValue(), spTY.getValue().floatValue(), spTZ.getValue().floatValue(),
spRX.getValue().floatValue(), spRY.getValue().floatValue(), spRZ.getValue().floatValue());
animSetKfList.add(kf);
} else {
existing.time = t;
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();
}
animSetKfList.sort(java.util.Comparator.comparingDouble(k -> k.time));
if (animSetKfListView != null) animSetKfListView.refresh();
animSetDirty = true;
});
}
/** Spielt den Clip der gegebenen Aktion in der Vorschau ab, um animPreviewCurrentClipDuration zu setzen. */
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 updateAnimPreviewKfOffset() {
if (animSetKfList == null || animSetKfList.isEmpty()) {
input.animPreviewKfTx = 0f;
input.animPreviewKfTy = 0f;
input.animPreviewKfTz = 0f;
input.animPreviewKfActive = false;
} else {
de.blight.game.animation.AnimKeyframe first = animSetKfList.get(0);
for (de.blight.game.animation.AnimKeyframe k : animSetKfList) {
if (k.time < first.time) { first = k; }
}
input.animPreviewKfTx = first.tx;
input.animPreviewKfTy = first.ty;
input.animPreviewKfTz = first.tz;
input.animPreviewKfActive = true;
}
}
private void previewSelectedClip() {
if (animSetClipListView == null) return;
String clip = animSetClipListView.getSelectionModel().getSelectedItem();
@@ -8749,28 +8743,21 @@ public class EditorApp extends Application {
}
}
animSet.setActionMap(actionMap);
java.util.Map<String, Float> sinkMap = new java.util.LinkedHashMap<>();
if (animSetSinkListView != null) {
for (String it : animSetSinkListView.getItems()) {
String[] parts = it.split("", 2);
if (parts.length == 2) {
try {
sinkMap.put(parts[0], Float.parseFloat(parts[1]));
} catch (NumberFormatException ignored) {}
}
// Motion Keyframes: aktuell sichtbare Liste (des selektierten Clips) sichern, dann schreiben
if (animSetClipListView != null) {
String selClip = animSetClipListView.getSelectionModel().getSelectedItem();
if (selClip != null) {
animSetMotionKeyframes.put(selClip, new java.util.ArrayList<>(animSetKfList));
}
}
animSet.setSinkMap(sinkMap);
java.util.Map<String, String> anchorBoneMap = new java.util.LinkedHashMap<>();
if (animSetAnchorBoneListView != null) {
for (String it : animSetAnchorBoneListView.getItems()) {
String[] parts = it.split("", 2);
if (parts.length == 2) {
anchorBoneMap.put(parts[0], parts[1]);
}
java.util.Map<String, java.util.List<de.blight.game.animation.AnimKeyframe>> kfFinal =
new java.util.LinkedHashMap<>();
for (var kfEntry : animSetMotionKeyframes.entrySet()) {
if (kfEntry.getValue() != null && !kfEntry.getValue().isEmpty()) {
kfFinal.put(kfEntry.getKey(), kfEntry.getValue());
}
}
animSet.setAnchorBoneMap(anchorBoneMap);
animSet.setMotionKeyframes(kfFinal);
// Vorschau-Modell-Pfad beibehalten
if (animSetModelCombo != null && animSetModelCombo.getValue() != null && !animSetModelCombo.getValue().isBlank()) {
animSet.setPreviewModelPath(animSetModelCombo.getValue());

View File

@@ -567,9 +567,16 @@ public class SharedInput {
/** JME3 → JavaFX: Clip-Namen nach Modell-Laden. getAndSet(null) konsumiert. */
public final java.util.concurrent.atomic.AtomicReference<java.util.List<String>>
animPreviewClips = new java.util.concurrent.atomic.AtomicReference<>();
/** JME3 → JavaFX: Joint-Namen des geladenen Armatures (für Bone-Anchoring-Auswahl). */
/** JME3 → JavaFX: Joint-Namen des geladenen Armatures. */
public final java.util.concurrent.atomic.AtomicReference<java.util.List<String>>
animPreviewJointNames = new java.util.concurrent.atomic.AtomicReference<>();
/** JME3 → JavaFX: Länge des zuletzt gestarteten Clips in Sekunden (0 = unbekannt). */
public volatile float animPreviewCurrentClipDuration = 0f;
/** JavaFX → JME3: Motion-Keyframe-Vorschau-Versatz (erster KF, tx/ty/tz in Metern). */
public volatile float animPreviewKfTx = 0f;
public volatile float animPreviewKfTy = 0f;
public volatile float animPreviewKfTz = 0f;
public volatile boolean animPreviewKfActive = false;
/**
* JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells.

View File

@@ -232,6 +232,16 @@ public class AnimPreviewState extends BaseAppState {
axesNode.setLocalTranslation(Vector3f.ZERO);
}
// Motion-Keyframe-Vorschau: Versatz auf Modell anwenden (TX/TY/TZ in Welt-Koordinaten)
if (currentModel != null) {
if (input.animPreviewKfActive) {
currentModel.setLocalTranslation(
input.animPreviewKfTx, input.animPreviewKfTy, input.animPreviewKfTz);
} else {
currentModel.setLocalTranslation(Vector3f.ZERO);
}
}
previewScene.updateLogicalState(tpf);
previewScene.updateGeometricState();
}
@@ -297,13 +307,15 @@ public class AnimPreviewState extends BaseAppState {
previewSC != null ? "ok" : "NULL");
}
// Kamera: immer auf Hüfthöhe (0, 1, 0) zielen; Distanz aus BoundingBox
// Kamera: immer auf Hüfthöhe (0, 1, 0) zielen; Distanz aus BoundingBox.
// WICHTIG: BoundingBox wird VOR dem ersten SC-Update berechnet (T-Pose noch nicht sichtbar).
// Deshalb Minimum von 3f + 2m Puffer, damit die Kamera nicht im Körper startet.
model.updateGeometricState();
if (model.getWorldBound() instanceof BoundingBox bb) {
float ext = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
previewCamDist = ext * 2.8f;
previewCamDist = Math.max(ext * 2.8f, 3f) + 2f;
} else {
previewCamDist = 3f;
previewCamDist = 5f;
}
previewTarget.set(0, 1, 0);
input.animPreviewZoom = 1.0f;
@@ -381,6 +393,7 @@ public class AnimPreviewState extends BaseAppState {
currentClipName = clipName;
if (currentAction != null) {
currentAction.setSpeed(input.animPreviewSpeed);
input.animPreviewCurrentClipDuration = (float) currentAction.getLength();
LOG.info("[AnimPreview] Play '{}' length={}", clipName, currentAction.getLength());
}
} catch (Exception e) {