Zwischenstand

This commit is contained in:
2026-10-16 13:52:21 +02:00
parent d19538bb63
commit 93a3c266e1
18 changed files with 577 additions and 70 deletions

View File

@@ -1258,6 +1258,18 @@ public class SharedInput {
// ── Attachment-Editor (eigenständige Ansicht für Hand-Anker pro Kategorie) ─
public volatile boolean attachmentEditorActive = false;
// ── Schild-Attachment-Editor ──────────────────────────────────────────────
public volatile boolean shieldEditorActive = false;
public volatile String shieldEditorSubCategory = null;
public volatile float shieldEditorRotX = -90f;
public volatile float shieldEditorRotY = 0f;
public volatile float shieldEditorRotZ = 0f;
public volatile float shieldEditorElbowOffset = 0.25f;
public volatile float shieldEditorGripX = 0f;
public volatile float shieldEditorGripY = 0f;
public volatile float shieldEditorGripZ = 0f;
public volatile boolean shieldEditorOffsetsDirty = false;
/** Klick/Drag im Viewport im Sculpt-Modus. action 0=links, 1=rechts. */
public record SculptEdit(float screenX, float screenY, int action) {}
public final ConcurrentLinkedQueue<SculptEdit> sculptEditQueue = new ConcurrentLinkedQueue<>();

View File

@@ -121,6 +121,7 @@ public class ModelEditorState extends BaseAppState {
private Node charLeftHandNode = null; // LeftHand-Attachment
private Node charPalmNode = null; // RightHandMiddle1-Attachment
private Quaternion charHandBindRot = null;
private Quaternion charLeftHandBindRot = null;
private Vector3f charPalmOffset = null;
private int charPalmDelayFrames = 0;
private float interactableOffsetZ = 0f;
@@ -260,9 +261,21 @@ public class ModelEditorState extends BaseAppState {
guessGripPosition();
}
// Modell laden – MUSS vor enableCharPreview stehen, damit currentPath aktuell ist
String openPath = input.modelEditorOpenPath;
if (openPath != null) {
input.modelEditorOpenPath = null;
mainModelPath = openPath;
input.modelEditorLodPreview = 0;
input.modelImportLod1PreviewNode.set(null);
input.modelImportLod2PreviewNode.set(null);
loadModel(openPath);
}
// Charakter-Preview ein-/ausschalten
boolean wantsPreview = (input.gripEditorCharPreview && input.gripEditorActive)
|| input.attachmentEditorActive;
|| input.attachmentEditorActive
|| input.shieldEditorActive;
if (wantsPreview && charPreviewNode == null) {
enableCharPreview();
} else if (!wantsPreview && charPreviewNode != null) {
@@ -278,7 +291,7 @@ public class ModelEditorState extends BaseAppState {
}
}
// Waffe im Charakter-Preview jedes Frame neu positionieren
// Waffe/Schild im Charakter-Preview jedes Frame neu positionieren
if (charPreviewNode != null && charWeaponSpatial != null && charHandNode != null) {
updateCharPreviewWeapon();
}
@@ -288,17 +301,6 @@ public class ModelEditorState extends BaseAppState {
embedLodsAndSave(input.modelEditorEmbedLodsPath);
}
// Modell laden
String openPath = input.modelEditorOpenPath;
if (openPath != null) {
input.modelEditorOpenPath = null;
mainModelPath = openPath;
input.modelEditorLodPreview = 0;
input.modelImportLod1PreviewNode.set(null);
input.modelImportLod2PreviewNode.set(null);
loadModel(openPath);
}
// Invariante: wenn Charakter-Preview aktiv, standalone-Modell immer ausblenden
if (charPreviewNode != null) {
if (modelWrapper != null) modelWrapper.setCullHint(Spatial.CullHint.Always);
@@ -2083,14 +2085,17 @@ public class ModelEditorState extends BaseAppState {
SkinningControl skin = findSkinCtrl(hero);
if (skin != null) {
charHandBindRot = extractJointBindRot(skin, "mixamorig:RightHand");
charHandBindRot = extractJointBindRot(skin, "mixamorig:RightHand");
charLeftHandBindRot = extractJointBindRot(skin, "mixamorig:LeftHand");
try { charHandNode = skin.getAttachmentsNode("mixamorig:RightHand"); } catch (Exception e) { log.warn("[CharPreview] RightHand: {}", e.getMessage()); }
try { charLeftHandNode = skin.getAttachmentsNode("mixamorig:LeftHand"); } catch (Exception e) { log.warn("[CharPreview] LeftHand: {}", e.getMessage()); }
try { charPalmNode = skin.getAttachmentsNode("mixamorig:RightHandMiddle1"); charPalmDelayFrames = 2; } catch (Exception e) { log.warn("[CharPreview] Palm: {}", e.getMessage()); }
AnimComposer anim = findAnimComposer(hero);
if (anim != null) {
String clipName = previewAnimName(input.gripEditorSubCategory);
String subCat = input.shieldEditorActive
? input.shieldEditorSubCategory : input.gripEditorSubCategory;
String clipName = previewAnimName(subCat);
loadClipToComposer(anim, skin, clipName);
}
}
@@ -2116,13 +2121,14 @@ public class ModelEditorState extends BaseAppState {
private void disableCharPreview() {
if (charPreviewNode != null) {
charPreviewNode.removeFromParent();
charPreviewNode = null;
charWeaponSpatial = null;
charHandNode = null;
charLeftHandNode = null;
charPalmNode = null;
charHandBindRot = null;
charPalmOffset = null;
charPreviewNode = null;
charWeaponSpatial = null;
charHandNode = null;
charLeftHandNode = null;
charPalmNode = null;
charHandBindRot = null;
charLeftHandBindRot = null;
charPalmOffset = null;
}
if (modelWrapper != null) modelWrapper.setCullHint(Spatial.CullHint.Inherit);
if (gripGizmoNode != null) gripGizmoNode.setCullHint(Spatial.CullHint.Always);
@@ -2130,6 +2136,10 @@ public class ModelEditorState extends BaseAppState {
}
private void updateCharPreviewWeapon() {
if (input.shieldEditorActive) {
updateCharPreviewShield();
return;
}
// Kategorie-Offsets aus SharedInput lesen (live-update)
WeaponCategoryAttachment cfg = new WeaponCategoryAttachment();
cfg.setHandOffX(input.gripEditorCatHandOffX);
@@ -2205,11 +2215,39 @@ public class ModelEditorState extends BaseAppState {
charWeaponSpatial.setLocalTranslation(finalPos);
}
private void updateCharPreviewShield() {
if (charLeftHandNode == null) return;
float DEG_TO_RAD = (float) Math.PI / 180f;
Quaternion charWorldRotInv = charPreviewNode.getWorldRotation().inverse();
Vector3f charWorldPos = charPreviewNode.getWorldTranslation();
Vector3f leftBoneInChar = charWorldRotInv.mult(
charLeftHandNode.getWorldTranslation().subtract(charWorldPos));
Quaternion leftBoneRotInChar = charWorldRotInv.mult(charLeftHandNode.getWorldRotation());
Quaternion animDelta = (charLeftHandBindRot != null)
? leftBoneRotInChar.mult(charLeftHandBindRot.inverse())
: leftBoneRotInChar;
Quaternion armRot = new Quaternion().fromAngles(
DEG_TO_RAD * input.shieldEditorRotX,
DEG_TO_RAD * input.shieldEditorRotY,
DEG_TO_RAD * input.shieldEditorRotZ);
Quaternion shieldRot = animDelta.mult(armRot);
Vector3f anchor = leftBoneInChar.add(
leftBoneRotInChar.mult(new Vector3f(0f, -input.shieldEditorElbowOffset, 0f)));
Vector3f gripPos = new Vector3f(
input.shieldEditorGripX, input.shieldEditorGripY, input.shieldEditorGripZ);
charWeaponSpatial.setLocalRotation(shieldRot);
charWeaponSpatial.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos)));
}
private static String previewAnimName(String subCategory) {
if (subCategory == null) return "Idle";
return switch (subCategory) {
case "TWO_HANDED_SWORD" -> "Great Sword Idle";
case "SWORD" -> "Sword And Shield Idle";
case "SWORD", "SHIELD" -> "Sword And Shield Idle";
default -> "Idle";
};
}

View File

@@ -114,12 +114,12 @@ public class AttachmentEditorController {
// ── Panel ─────────────────────────────────────────────────────────────────
private VBox buildPanel(WeaponCategoryAttachment cfg, ItemSubCategory subCategory) {
handOffXSpin = doubleSpin(cfg.getHandOffX(), -0.5, 0.5, 0.005, "m");
handOffYSpin = doubleSpin(cfg.getHandOffY(), -0.5, 0.5, 0.005, "m");
handOffZSpin = doubleSpin(cfg.getHandOffZ(), -0.5, 0.5, 0.005, "m");
hand2OffXSpin = doubleSpin(cfg.getHand2OffX(), -0.5, 0.5, 0.005, "m");
hand2OffYSpin = doubleSpin(cfg.getHand2OffY(), -0.5, 0.5, 0.005, "m");
hand2OffZSpin = doubleSpin(cfg.getHand2OffZ(), -0.5, 0.5, 0.005, "m");
handOffXSpin = doubleSpin(cfg.getHandOffX(), -2.0, 2.0, 0.005, "m");
handOffYSpin = doubleSpin(cfg.getHandOffY(), -2.0, 2.0, 0.005, "m");
handOffZSpin = doubleSpin(cfg.getHandOffZ(), -2.0, 2.0, 0.005, "m");
hand2OffXSpin = doubleSpin(cfg.getHand2OffX(), -2.0, 2.0, 0.005, "m");
hand2OffYSpin = doubleSpin(cfg.getHand2OffY(), -2.0, 2.0, 0.005, "m");
hand2OffZSpin = doubleSpin(cfg.getHand2OffZ(), -2.0, 2.0, 0.005, "m");
handOffXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
handOffYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());

View File

@@ -88,13 +88,15 @@ public class ContentController {
// ──────────────────────────────────────────────────────────────────────────
private Runnable onEnterContentMode;
private GripEditorController gripEditorController;
private AttachmentEditorController attachmentEditorController;
private GripEditorController gripEditorController;
private AttachmentEditorController attachmentEditorController;
private ShieldAttachmentEditorController shieldAttachmentEditorController;
public ContentController(ToolContext ctx) {
this.ctx = ctx;
this.gripEditorController = new GripEditorController(ctx);
this.attachmentEditorController = new AttachmentEditorController(ctx);
this.gripEditorController = new GripEditorController(ctx);
this.attachmentEditorController = new AttachmentEditorController(ctx);
this.shieldAttachmentEditorController = new ShieldAttachmentEditorController(ctx);
}
// ── Öffentliche API ────────────────────────────────────────────────────────
@@ -1024,6 +1026,18 @@ public class ContentController {
});
attachmentEditorController.open(modelPath, subCategory, grip);
});
view.setShieldAttachmentEditorLauncher((modelPath, subCategory, grip) -> {
String itemId = view.getCurrentItemId();
shieldAttachmentEditorController.setOnReturn(() -> {
if (dualWindowMode) {
buildItemViewInContent();
if (activeItemView != null) activeItemView.selectItem(itemId);
} else {
switchToItemEditor(itemId);
}
});
shieldAttachmentEditorController.open(modelPath, subCategory, grip);
});
}
private void buildItemViewInContent() {

View File

@@ -0,0 +1,253 @@
package de.blight.editor.toolset.content;
import de.blight.common.model.ItemGrip;
import de.blight.common.model.ItemSubCategory;
import de.blight.common.model.ShieldAttachmentIO;
import de.blight.common.model.ShieldCategoryAttachment;
import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.*;
/**
* Eigenständige Ansicht zum Bearbeiten der Schild-Arm-Attachment-Parameter pro Kategorie.
* Zeigt den Helden in Sword-And-Shield Idle mit Schild am linken Arm.
*/
public class ShieldAttachmentEditorController {
private final ToolContext ctx;
private Spinner<Double> rotXSpin, rotYSpin, rotZSpin, elbowOffsetSpin;
private boolean suppressEvents = false;
private ItemSubCategory itemSubCategory = null;
private Runnable onReturn = null;
public ShieldAttachmentEditorController(ToolContext ctx) {
this.ctx = ctx;
}
public void setOnReturn(Runnable r) { this.onReturn = r; }
// ── Öffnen ────────────────────────────────────────────────────────────────
public void open(String modelPath, ItemSubCategory subCategory, ItemGrip grip) {
itemSubCategory = subCategory;
SharedInput input = ctx.input();
input.gripEditorActive = false;
input.gripEditorCharPreview = false;
input.attachmentEditorActive = false;
input.shieldEditorActive = true;
input.shieldEditorSubCategory = subCategory != null ? subCategory.name() : null;
ShieldCategoryAttachment cfg = ShieldAttachmentIO.get(subCategory);
input.shieldEditorRotX = cfg.getRotX();
input.shieldEditorRotY = cfg.getRotY();
input.shieldEditorRotZ = cfg.getRotZ();
input.shieldEditorElbowOffset = cfg.getElbowOffset();
input.shieldEditorOffsetsDirty = false;
// Grip des Schilds für Preview-Positionierung
ItemGrip g = grip != null ? grip : new ItemGrip();
input.shieldEditorGripX = g.getPosX();
input.shieldEditorGripY = g.getPosY();
input.shieldEditorGripZ = g.getPosZ();
if (modelPath != null && !modelPath.isBlank()) {
input.modelEditorOpenPath = modelPath;
}
input.activeLayer = SharedInput.LAYER_MODEL_EDITOR;
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false);
ctx.setCenterView().accept(ctx.viewportSupplier().get());
ctx.setCurrentTool().accept("shieldAttachmentEditor");
ctx.setMainToolbar().accept(buildToolbar());
VBox innerPanel = buildPanel(cfg, subCategory);
ScrollPane scroll = new ScrollPane(innerPanel);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color:transparent;-fx-background:transparent;");
scroll.skinProperty().addListener((obs, o, n) -> {
javafx.scene.Node vp = scroll.lookup(".viewport");
if (vp != null) vp.setStyle("-fx-background-color: #252535;");
});
VBox outerPanel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
outerPanel.setPrefWidth(220);
outerPanel.setMaxWidth(220);
outerPanel.setStyle("-fx-background-color: #252535;");
ctx.setRightPanel().accept(outerPanel);
ctx.setLeftPanel().accept(null);
ctx.setBottomPanel().accept(null);
}
// ── Toolbar ───────────────────────────────────────────────────────────────
private ToolBar buildToolbar() {
Button backBtn = new Button("<- Zurück");
backBtn.setOnAction(e -> onCancel());
Label title = new Label("Schild-Anker-Editor");
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
ToolBar tb = new ToolBar();
tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), title);
return tb;
}
// ── Panel ─────────────────────────────────────────────────────────────────
private VBox buildPanel(ShieldCategoryAttachment cfg, ItemSubCategory subCategory) {
rotXSpin = doubleSpin(cfg.getRotX(), -360.0, 360.0, 1.0, "°");
rotYSpin = doubleSpin(cfg.getRotY(), -360.0, 360.0, 1.0, "°");
rotZSpin = doubleSpin(cfg.getRotZ(), -360.0, 360.0, 1.0, "°");
elbowOffsetSpin = doubleSpin(cfg.getElbowOffset(), 0.0, 1.0, 0.005, "m");
rotXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
rotYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
rotZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
elbowOffsetSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
Button saveBtn = new Button("Speichern");
saveBtn.setMaxWidth(Double.MAX_VALUE);
saveBtn.setStyle("-fx-background-color: #2a5a2a; -fx-text-fill: #aaffaa; -fx-cursor: hand; -fx-font-weight: bold;");
saveBtn.setOnAction(e -> saveAndReturn());
Button cancelBtn = new Button("Abbrechen");
cancelBtn.setMaxWidth(Double.MAX_VALUE);
cancelBtn.setStyle("-fx-background-color: #3a3a3a; -fx-text-fill: #cccccc; -fx-cursor: hand;");
cancelBtn.setOnAction(e -> onCancel());
String catLabel = subCategory != null ? subCategory.name() : "–";
VBox panel = new VBox(8);
panel.setPadding(new Insets(12));
panel.setStyle("-fx-background-color: #252535;");
panel.setPrefWidth(220);
panel.getChildren().addAll(
styledTitle("Kategorie: " + catLabel),
styledHint("Steuert wie das Schild am linken Unterarm angebracht ist.\nDer Held zeigt die Pose in der Sword-And-Shield Idle-Animation."),
new Separator(),
styledTitle("Rotation am Arm (Grad)"),
styledHint("Dreht das Schild relativ zur Bind-Pose des LeftHand-Knochens."),
row("X:", rotXSpin),
row("Y:", rotYSpin),
row("Z:", rotZSpin),
new Separator(),
styledTitle("Ellbogen-Offset (m)"),
styledHint("Verschiebung in Richtung Ellbogen (lokale -Y des LeftHand-Knochens)."),
row("Off:", elbowOffsetSpin),
new Separator(),
saveBtn,
cancelBtn
);
return panel;
}
// ── Spinner-Events ────────────────────────────────────────────────────────
private void onSpinnerChanged() {
if (suppressEvents) return;
SharedInput input = ctx.input();
input.shieldEditorRotX = rotXSpin.getValue().floatValue();
input.shieldEditorRotY = rotYSpin.getValue().floatValue();
input.shieldEditorRotZ = rotZSpin.getValue().floatValue();
input.shieldEditorElbowOffset = elbowOffsetSpin.getValue().floatValue();
input.shieldEditorOffsetsDirty = true;
}
// ── Speichern / Abbrechen ─────────────────────────────────────────────────
private void saveAndReturn() {
if (itemSubCategory != null) {
ShieldCategoryAttachment cfg = new ShieldCategoryAttachment();
cfg.setRotX(rotXSpin.getValue().floatValue());
cfg.setRotY(rotYSpin.getValue().floatValue());
cfg.setRotZ(rotZSpin.getValue().floatValue());
cfg.setElbowOffset(elbowOffsetSpin.getValue().floatValue());
ShieldAttachmentIO.set(itemSubCategory, cfg);
java.nio.file.Path dest = ctx.assetRoot().resolve("shield_attachment.json");
ShieldAttachmentIO.save(dest);
syncToMirrors(dest);
}
closeEditor();
}
private void syncToMirrors(java.nio.file.Path savedFile) {
java.nio.file.Path projectRoot = de.blight.editor.ProjectRoot.PATH;
String[] mirrorRoots = {
"blight-assets/bin/main",
"blight-assets/build/resources/main"
};
for (String root : mirrorRoots) {
java.nio.file.Path mirror = projectRoot.resolve(root).resolve("shield_attachment.json");
try {
java.nio.file.Files.copy(savedFile, mirror,
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
// Mirror nicht vorhanden – ignorieren
}
}
}
private void onCancel() {
closeEditor();
}
private void closeEditor() {
SharedInput input = ctx.input();
input.shieldEditorActive = false;
input.shieldEditorOffsetsDirty = false;
Runnable ret = onReturn;
onReturn = null;
itemSubCategory = null;
if (ret != null) ret.run(); else ctx.switchToWorld().run();
}
// ── Helpers ───────────────────────────────────────────────────────────────
private static Spinner<Double> doubleSpin(float initial, double min, double max, double step, String unit) {
SpinnerValueFactory.DoubleSpinnerValueFactory vf =
new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, initial, step);
vf.setConverter(new javafx.util.StringConverter<>() {
@Override public String toString(Double v) { return v == null ? "0" : String.format("%.3f", v) + " " + unit; }
@Override public Double fromString(String s) {
try { return Double.parseDouble(s.replace(unit, "").replace(",", ".").trim()); }
catch (NumberFormatException e) { return vf.getValue(); }
}
});
Spinner<Double> sp = new Spinner<>(vf);
sp.setEditable(true);
sp.setMaxWidth(Double.MAX_VALUE);
return sp;
}
private static Label styledTitle(String text) {
Label l = new Label(text);
l.setStyle("-fx-font-weight: bold; -fx-font-size: 11; -fx-text-fill: #88aacc;");
l.setWrapText(true);
return l;
}
private static Label styledHint(String text) {
Label l = new Label(text);
l.setWrapText(true);
l.setStyle("-fx-text-fill: #888; -fx-font-size: 10;");
return l;
}
private static HBox row(String labelText, Spinner<Double> spinner) {
Label lbl = new Label(labelText);
lbl.setMinWidth(30);
lbl.setStyle("-fx-text-fill: #aaa;");
HBox.setHgrow(spinner, Priority.ALWAYS);
HBox box = new HBox(6, lbl, spinner);
box.setAlignment(Pos.CENTER_LEFT);
return box;
}
}

View File

@@ -63,9 +63,14 @@ public class ItemEditorView extends BorderPane {
void open(String modelPath, ItemSubCategory subCategory, ItemGrip grip);
}
public interface ShieldAttachmentEditorLauncher {
void open(String modelPath, ItemSubCategory subCategory, ItemGrip grip);
}
private Runnable onIdChanged;
private GripEditorLauncher gripEditorLauncher;
private AttachmentEditorLauncher attachmentEditorLauncher;
private GripEditorLauncher gripEditorLauncher;
private AttachmentEditorLauncher attachmentEditorLauncher;
private ShieldAttachmentEditorLauncher shieldAttachmentEditorLauncher;
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
@@ -96,6 +101,7 @@ public class ItemEditorView extends BorderPane {
public void setOnIdChanged(Runnable r) { this.onIdChanged = r; }
public void setGripEditorLauncher(GripEditorLauncher l) { this.gripEditorLauncher = l; }
public void setAttachmentEditorLauncher(AttachmentEditorLauncher l) { this.attachmentEditorLauncher = l; }
public void setShieldAttachmentEditorLauncher(ShieldAttachmentEditorLauncher l) { this.shieldAttachmentEditorLauncher = l; }
public String getCurrentItemId() { return current != null ? current.getItemId() : null; }
public void selectItem(String itemId) {
@@ -264,10 +270,16 @@ public class ItemEditorView extends BorderPane {
shieldGripBtn.setStyle("-fx-background-color: #2a4a5a; -fx-text-fill: #aaddff; -fx-cursor: hand;");
shieldGripBtn.setOnAction(e -> openShieldGripEditor());
Button shieldAttachmentBtn = new Button("Arm-Anker bearbeiten...");
shieldAttachmentBtn.setMaxWidth(Double.MAX_VALUE);
shieldAttachmentBtn.setStyle("-fx-background-color: #1a4a4a; -fx-text-fill: #aaffee; -fx-cursor: hand;");
shieldAttachmentBtn.setOnAction(e -> openShieldAttachmentEditor());
shieldSection = new VBox(6,
new Separator(),
sectionTitle("Schild-Griff"),
shieldGripBtn
shieldGripBtn,
shieldAttachmentBtn
);
shieldSection.setVisible(false);
shieldSection.setManaged(false);
@@ -358,6 +370,13 @@ public class ItemEditorView extends BorderPane {
});
}
private void openShieldAttachmentEditor() {
if (current == null || shieldAttachmentEditorLauncher == null) return;
String modelPath = modelRefField.getText().trim();
shieldAttachmentEditorLauncher.open(modelPath.isBlank() ? null : modelPath,
current.getSubCategory(), current.getGrip());
}
private void updateWeaponSectionVisibility() {
ItemCategory cat = catCombo.getValue();
ItemSubCategory sub = subCatCombo.getValue();