Weiter gebastelt
This commit is contained in:
@@ -3523,7 +3523,12 @@ public class EditorApp extends Application {
|
||||
if (pStr.endsWith(".item")) {
|
||||
MenuItem placeItem = new MenuItem("📍 Platzieren");
|
||||
placeItem.setOnAction(ev -> activateItemPlacement(p));
|
||||
ctx.getItems().addAll(placeItem, new SeparatorMenuItem());
|
||||
MenuItem editItem = new MenuItem("✏ Im Item-Editor öffnen");
|
||||
editItem.setOnAction(ev -> {
|
||||
String itemId = p.getFileName().toString().replace(".item", "");
|
||||
switchToItemEditor(itemId);
|
||||
});
|
||||
ctx.getItems().addAll(placeItem, editItem, new SeparatorMenuItem());
|
||||
}
|
||||
|
||||
MenuItem reveal = new MenuItem("Im Dateisystem anzeigen");
|
||||
@@ -4535,6 +4540,14 @@ public class EditorApp extends Application {
|
||||
|
||||
// Asset-Tree aktualisieren
|
||||
input.refreshAssets = true;
|
||||
|
||||
// Thumbnail generieren (JME3-Thread liest das Flag und rendert)
|
||||
java.nio.file.Path finalJ3o = category.isEmpty() ? absolutePath
|
||||
: ASSET_ROOT.resolve("Models")
|
||||
.resolve(java.nio.file.Path.of(category.replace('/', java.io.File.separatorChar)))
|
||||
.resolve(name.isEmpty() ? absolutePath.getFileName().toString()
|
||||
: name.replaceAll("[\\\\/:*?\"<>|]", "_") + ".j3o");
|
||||
input.modelEditorThumbnailRequest = finalJ3o;
|
||||
}
|
||||
|
||||
private void importLodFile(int slot) {
|
||||
@@ -6610,6 +6623,10 @@ public class EditorApp extends Application {
|
||||
}
|
||||
|
||||
private void switchToItemEditor() {
|
||||
switchToItemEditor(null);
|
||||
}
|
||||
|
||||
private void switchToItemEditor(String selectItemId) {
|
||||
onF5 = null;
|
||||
currentTool = "itemEditor";
|
||||
ToolBar tb = new ToolBar();
|
||||
@@ -6619,8 +6636,11 @@ public class EditorApp extends Application {
|
||||
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||
tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label);
|
||||
topBar.getChildren().set(1, tb);
|
||||
root.setCenter(new de.blight.editor.ui.ItemEditorView(ASSET_ROOT.resolve("items")));
|
||||
de.blight.editor.ui.ItemEditorView view =
|
||||
new de.blight.editor.ui.ItemEditorView(ASSET_ROOT.resolve("items"));
|
||||
root.setCenter(view);
|
||||
root.setRight(null);
|
||||
if (selectItemId != null) view.selectItem(selectItemId);
|
||||
}
|
||||
|
||||
private void switchToQuestEditor() {
|
||||
|
||||
@@ -587,6 +587,12 @@ public class SharedInput {
|
||||
/** JFX → JME: Model-Editor schließen. */
|
||||
public volatile boolean modelEditorCloseRequest = false;
|
||||
|
||||
/**
|
||||
* JFX → JME: Thumbnail für das Modell unter diesem absoluten Pfad generieren.
|
||||
* Wird nach dem Speichern im ModelEditor gesetzt; JME3 rendert + speichert Sidecar + UserData.
|
||||
*/
|
||||
public volatile java.nio.file.Path modelEditorThumbnailRequest = null;
|
||||
|
||||
/** JME → JFX: true wenn das geladene Modell eingebettete LOD-Kinder hat (kein separater Pfad nötig). */
|
||||
public volatile boolean modelEditorHasEmbeddedLods = false;
|
||||
|
||||
|
||||
@@ -13,13 +13,18 @@ import com.jme3.scene.shape.Box;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import de.blight.common.PlacedItem;
|
||||
import de.blight.common.PlacedItemIO;
|
||||
import de.blight.common.model.Item;
|
||||
import de.blight.common.model.ItemIO;
|
||||
import de.blight.editor.ProjectRoot;
|
||||
import de.blight.editor.SharedInput;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Verwaltet die Platzierung von Items (Pickups) auf der Karte im Editor.
|
||||
@@ -36,10 +41,11 @@ public class ItemPlacementState extends BaseAppState {
|
||||
private Node rootNode;
|
||||
private TerrainQuad terrain;
|
||||
|
||||
private final List<PlacedItem> items = new ArrayList<>();
|
||||
private final List<Node> nodes = new ArrayList<>();
|
||||
private Node itemRoot;
|
||||
private Node previewNode;
|
||||
private final List<PlacedItem> items = new ArrayList<>();
|
||||
private final List<Node> nodes = new ArrayList<>();
|
||||
private final Map<String, Item> itemDefs = new HashMap<>();
|
||||
private Node itemRoot;
|
||||
private Node previewNode;
|
||||
|
||||
public ItemPlacementState(SharedInput input) {
|
||||
this.input = input;
|
||||
@@ -58,6 +64,11 @@ public class ItemPlacementState extends BaseAppState {
|
||||
this.assets = app.getAssetManager();
|
||||
this.rootNode = this.app.getRootNode();
|
||||
|
||||
java.nio.file.Path itemDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources").resolve("items");
|
||||
for (Item it : ItemIO.loadAll(itemDir)) {
|
||||
if (it.getItemId() != null) itemDefs.put(it.getItemId(), it);
|
||||
}
|
||||
|
||||
itemRoot = new Node("itemRoot");
|
||||
previewNode = new Node("itemPreview");
|
||||
previewNode.setCullHint(Spatial.CullHint.Always);
|
||||
@@ -171,12 +182,28 @@ public class ItemPlacementState extends BaseAppState {
|
||||
|
||||
private Node buildItemNode(String itemId) {
|
||||
Node n = new Node("item_" + itemId);
|
||||
n.setUserData("itemId", itemId);
|
||||
|
||||
Item def = itemDefs.get(itemId);
|
||||
if (def != null && def.getModelRef() != null
|
||||
&& def.getModelRef().getPath() != null
|
||||
&& !def.getModelRef().getPath().isBlank()) {
|
||||
try {
|
||||
Spatial model = assets.loadModel(def.getModelRef().getPath());
|
||||
model.setName("itemModel_" + itemId);
|
||||
n.attachChild(model);
|
||||
return n;
|
||||
} catch (Exception e) {
|
||||
log.warn("[ItemPlacement] Modell für '{}' nicht ladbar: {}", itemId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: goldener Würfel
|
||||
Geometry g = new Geometry("itemGeo_" + itemId, new Box(0.15f, 0.15f, 0.15f));
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(1f, 0.82f, 0.1f, 1f));
|
||||
g.setMaterial(mat);
|
||||
n.attachChild(g);
|
||||
n.setUserData("itemId", itemId);
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.bounding.BoundingBox;
|
||||
import com.jme3.export.Savable;
|
||||
import com.jme3.export.binary.BinaryExporter;
|
||||
import com.jme3.export.binary.BinaryImporter;
|
||||
import com.jme3.light.AmbientLight;
|
||||
import com.jme3.light.DirectionalLight;
|
||||
import com.jme3.material.Material;
|
||||
@@ -15,6 +18,7 @@ import com.jme3.util.BufferUtils;
|
||||
import de.blight.editor.SharedInput;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Modell-Editor-Modus: zeigt ein einzelnes Modell in isolierter Vorschau
|
||||
@@ -148,6 +152,13 @@ public class ModelEditorState extends BaseAppState {
|
||||
applyPivot(input.modelEditorPivotY);
|
||||
}
|
||||
|
||||
// Thumbnail auf Anforderung generieren
|
||||
Path thumbReq = input.modelEditorThumbnailRequest;
|
||||
if (thumbReq != null && modelWrapper != null) {
|
||||
input.modelEditorThumbnailRequest = null;
|
||||
generateThumbnail(thumbReq);
|
||||
}
|
||||
|
||||
// Orbit-Kamera: Maus-Delta (Middle-Button zieht Kamera, Right-Button dreht Orbit)
|
||||
int[] delta = input.consumeMouseDelta();
|
||||
if (delta[0] != 0 || delta[1] != 0) {
|
||||
@@ -401,5 +412,35 @@ public class ModelEditorState extends BaseAppState {
|
||||
if (s instanceof Node n) n.getChildren().forEach(ModelEditorState::stripControls);
|
||||
}
|
||||
|
||||
// ── Thumbnail ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void generateThumbnail(Path j3oPath) {
|
||||
if (modelWrapper == null) return;
|
||||
try {
|
||||
// Klon des aktuellen Modells (inkl. angewandter Skalierung)
|
||||
Spatial modelClone = modelWrapper.clone();
|
||||
byte[] thumb = ThumbnailRenderer.render(modelClone, app.getRenderManager(), app.getRenderer());
|
||||
if (thumb == null) return;
|
||||
ThumbnailRenderer.saveSidecar(thumb, j3oPath);
|
||||
embedThumbnail(thumb, j3oPath);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[ModelEditor] Thumbnail-Fehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void embedThumbnail(byte[] pngBytes, Path j3oPath) {
|
||||
try {
|
||||
BinaryImporter importer = BinaryImporter.getInstance();
|
||||
importer.setAssetManager(app.getAssetManager());
|
||||
Savable savable = importer.load(j3oPath.toFile());
|
||||
if (savable instanceof Spatial root) {
|
||||
ThumbnailRenderer.embed(root, pngBytes);
|
||||
BinaryExporter.getInstance().save(root, j3oPath.toFile());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("[ModelEditor] j3o-Embed fehlgeschlagen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public String getCurrentPath() { return currentPath; }
|
||||
}
|
||||
|
||||
@@ -1243,8 +1243,24 @@ public class SceneObjectState extends BaseAppState {
|
||||
model.setLocalRotation(rot);
|
||||
}
|
||||
|
||||
Files.createDirectories(req.destJ3o().getParent());
|
||||
BinaryExporter.getInstance().save(model, req.destJ3o().toFile());
|
||||
// Thumbnail generieren und in j3o einbetten
|
||||
try {
|
||||
byte[] thumb = ThumbnailRenderer.render(model, app.getRenderManager(), app.getRenderer());
|
||||
if (thumb != null) {
|
||||
ThumbnailRenderer.embed(model, thumb);
|
||||
Files.createDirectories(req.destJ3o().getParent());
|
||||
BinaryExporter.getInstance().save(model, req.destJ3o().toFile());
|
||||
ThumbnailRenderer.saveSidecar(thumb, req.destJ3o());
|
||||
} else {
|
||||
Files.createDirectories(req.destJ3o().getParent());
|
||||
BinaryExporter.getInstance().save(model, req.destJ3o().toFile());
|
||||
}
|
||||
} catch (Exception thumbEx) {
|
||||
System.err.println("[SceneObject] Thumbnail-Fehler: " + thumbEx.getMessage());
|
||||
Files.createDirectories(req.destJ3o().getParent());
|
||||
BinaryExporter.getInstance().save(model, req.destJ3o().toFile());
|
||||
}
|
||||
|
||||
if (req.srcToDelete() != null) Files.deleteIfExists(req.srcToDelete());
|
||||
setStatus("Konvertiert: " + req.destJ3o().getFileName());
|
||||
input.refreshAssets = true;
|
||||
|
||||
@@ -408,15 +408,19 @@ public class TerrainEditorState extends BaseAppState {
|
||||
for (byte b : splatR) { if (b != 0) { rAllZero = false; break; } }
|
||||
if (rAllZero) Arrays.fill(splatR, (byte) 255);
|
||||
// Gespeicherte Texturpfade in SharedInput übernehmen
|
||||
System.arraycopy(loadedMapData.terrainTextures, 0,
|
||||
input.terrainTexturePaths, 0, MapData.TEXTURE_SLOTS);
|
||||
System.arraycopy(loadedMapData.terrainTextures, 0,
|
||||
input.terrainTexturePaths, 0, MapData.TEXTURE_SLOTS);
|
||||
System.arraycopy(loadedMapData.terrainNormalMaps, 0,
|
||||
input.terrainNormalMapPaths, 0, MapData.TEXTURE_SLOTS);
|
||||
// Zweite Splatmap (Slots 5-8)
|
||||
upperSplatR = loadedMapData.upperSplatR.clone();
|
||||
upperSplatG = loadedMapData.upperSplatG.clone();
|
||||
upperSplatB = loadedMapData.upperSplatB.clone();
|
||||
upperSplatA = loadedMapData.upperSplatA.clone();
|
||||
System.arraycopy(loadedMapData.upperTextures, 0,
|
||||
input.upperTexturePaths, 0, MapData.TEXTURE_SLOTS);
|
||||
System.arraycopy(loadedMapData.upperTextures, 0,
|
||||
input.upperTexturePaths, 0, MapData.TEXTURE_SLOTS);
|
||||
System.arraycopy(loadedMapData.upperNormalMaps, 0,
|
||||
input.upperNormalMapPaths, 0, MapData.TEXTURE_SLOTS);
|
||||
// Alte Gebirge-Splatmap-Migration: R=255 überall war der Gebirge-Standard.
|
||||
// Im neuen 1-Terrain-System bedeutet das: Slot-5-Textur deckt alles ab → auf 0 setzen.
|
||||
boolean upperRAllMax = true;
|
||||
@@ -837,8 +841,10 @@ public class TerrainEditorState extends BaseAppState {
|
||||
final byte[] upG = upperSplatG != null ? upperSplatG.clone() : null;
|
||||
final byte[] upB = upperSplatB != null ? upperSplatB.clone() : null;
|
||||
final byte[] upA = upperSplatA != null ? upperSplatA.clone() : null;
|
||||
final String[] texPaths = input.terrainTexturePaths.clone();
|
||||
final String[] upperPaths = input.upperTexturePaths.clone();
|
||||
final String[] texPaths = input.terrainTexturePaths.clone();
|
||||
final String[] normalPaths = input.terrainNormalMapPaths.clone();
|
||||
final String[] upperPaths = input.upperTexturePaths.clone();
|
||||
final String[] upperNormPaths = input.upperNormalMapPaths.clone();
|
||||
|
||||
final GrassTuftIO.GrassData grassData = placedObjectState != null
|
||||
? new GrassTuftIO.GrassData(placedObjectState.getSlotPaths(), placedObjectState.getAllTufts())
|
||||
@@ -870,14 +876,16 @@ public class TerrainEditorState extends BaseAppState {
|
||||
System.arraycopy(snapG, 0, data.splatG, 0, data.splatG.length);
|
||||
System.arraycopy(snapB, 0, data.splatB, 0, data.splatB.length);
|
||||
System.arraycopy(snapA, 0, data.splatA, 0, data.splatA.length);
|
||||
System.arraycopy(texPaths, 0, data.terrainTextures, 0, MapData.TEXTURE_SLOTS);
|
||||
System.arraycopy(texPaths, 0, data.terrainTextures, 0, MapData.TEXTURE_SLOTS);
|
||||
System.arraycopy(normalPaths, 0, data.terrainNormalMaps, 0, MapData.TEXTURE_SLOTS);
|
||||
}
|
||||
if (upR != null) {
|
||||
System.arraycopy(upR, 0, data.upperSplatR, 0, data.upperSplatR.length);
|
||||
System.arraycopy(upG, 0, data.upperSplatG, 0, data.upperSplatG.length);
|
||||
System.arraycopy(upB, 0, data.upperSplatB, 0, data.upperSplatB.length);
|
||||
System.arraycopy(upA, 0, data.upperSplatA, 0, data.upperSplatA.length);
|
||||
System.arraycopy(upperPaths, 0, data.upperTextures, 0, MapData.TEXTURE_SLOTS);
|
||||
System.arraycopy(upperPaths, 0, data.upperTextures, 0, MapData.TEXTURE_SLOTS);
|
||||
System.arraycopy(upperNormPaths, 0, data.upperNormalMaps, 0, MapData.TEXTURE_SLOTS);
|
||||
}
|
||||
|
||||
if (grassData != null) {
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package de.blight.editor.state;
|
||||
|
||||
import com.jme3.bounding.BoundingBox;
|
||||
import com.jme3.bounding.BoundingVolume;
|
||||
import com.jme3.light.AmbientLight;
|
||||
import com.jme3.light.DirectionalLight;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.FastMath;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.RenderManager;
|
||||
import com.jme3.renderer.Renderer;
|
||||
import com.jme3.renderer.ViewPort;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.texture.FrameBuffer;
|
||||
import com.jme3.texture.Image;
|
||||
import com.jme3.texture.Texture2D;
|
||||
import com.jme3.util.BufferUtils;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Offscreen-Thumbnail-Rendering für j3o-Modelle (128×128 PNG, transparenter Hintergrund).
|
||||
*
|
||||
* Kamera: 45° von oben, 45° Yaw von der X-Achse (entspricht Orbit yaw=45°, pitch=45°).
|
||||
* Zoom: Die Bounding-Box wird auf die Kamera-Bildebene projiziert; der Abstand wird so
|
||||
* gewählt, dass das Modell die volle Breite oder Höhe des Thumbnails ausfüllt.
|
||||
* Licht: ein Hauptlicht genau aus Blickrichtung + leichtes Umgebungslicht.
|
||||
*
|
||||
* Alle Methoden müssen vom JME3-Render-Thread aufgerufen werden.
|
||||
*/
|
||||
public final class ThumbnailRenderer {
|
||||
|
||||
public static final int SIZE = 128;
|
||||
|
||||
// Kamerawinkel: yaw = 45° (zwischen X- und Z-Achse), pitch = 45° (von oben)
|
||||
private static final float YAW_RAD = FastMath.DEG_TO_RAD * 45f;
|
||||
private static final float PITCH_RAD = FastMath.DEG_TO_RAD * 45f;
|
||||
|
||||
// Einheits-Versatzvektor Kamera→Zentrum (Orbit-Formel mit yaw=45°, pitch=45°):
|
||||
// x = cos(pitch)*sin(yaw) = 0.5
|
||||
// y = sin(pitch) = √2/2
|
||||
// z = cos(pitch)*cos(yaw) = 0.5 → Länge = 1.0 (bereits normiert)
|
||||
private static final Vector3f CAM_OFFSET = new Vector3f(
|
||||
FastMath.cos(PITCH_RAD) * FastMath.sin(YAW_RAD),
|
||||
FastMath.sin(PITCH_RAD),
|
||||
FastMath.cos(PITCH_RAD) * FastMath.cos(YAW_RAD));
|
||||
|
||||
// Kamera-Basisvektoren (JME3-lookAt von CAM_OFFSET in Richtung Ursprung, up=Y):
|
||||
// forward = -CAM_OFFSET = (-0.5, -0.7071, -0.5)
|
||||
// left = UNIT_Y × forward / |...| = (-0.7071, 0, 0.7071)
|
||||
// screen_right = -left = ( 0.7071, 0, -0.7071)
|
||||
// screen_up = forward × left = (-0.5, 0.7071, -0.5)
|
||||
private static final Vector3f SCREEN_RIGHT = new Vector3f( 0.7071f, 0f, -0.7071f);
|
||||
private static final Vector3f SCREEN_UP = new Vector3f(-0.5f, 0.7071f, -0.5f);
|
||||
|
||||
// FOV des Thumbnails (45°, quadratisch → aspect = 1)
|
||||
private static final float FOV_DEG = 45f;
|
||||
private static final float HALF_FOV = FastMath.DEG_TO_RAD * (FOV_DEG * 0.5f);
|
||||
|
||||
// Rand: 2 % Abstand zum Frame-Rand (verhindert Clipping)
|
||||
private static final float MARGIN = 0.98f;
|
||||
|
||||
private static final String USERDATA_KEY = "thumbnail";
|
||||
|
||||
private ThumbnailRenderer() {}
|
||||
|
||||
// ── Öffentliche API ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Rendert {@code model} zu einem 128×128 ARGB-PNG mit transparentem Hintergrund.
|
||||
* {@code model} darf beim Aufruf nicht in einem anderen Scene-Graph hängen.
|
||||
*/
|
||||
public static byte[] render(Spatial model, RenderManager rm, Renderer renderer) {
|
||||
model.updateGeometricState();
|
||||
BoundingVolume bv = model.getWorldBound();
|
||||
|
||||
Vector3f center = (bv instanceof BoundingBox bb)
|
||||
? bb.getCenter().clone() : Vector3f.ZERO.clone();
|
||||
float ex = (bv instanceof BoundingBox bb) ? bb.getXExtent() : 1f;
|
||||
float ey = (bv instanceof BoundingBox bb) ? bb.getYExtent() : 1f;
|
||||
float ez = (bv instanceof BoundingBox bb) ? bb.getZExtent() : 1f;
|
||||
|
||||
// Maximale Ausdehnung der Bounding-Box auf die Kamera-Bildebene projizieren
|
||||
float maxR = ex * Math.abs(SCREEN_RIGHT.x) + ey * Math.abs(SCREEN_RIGHT.y) + ez * Math.abs(SCREEN_RIGHT.z);
|
||||
float maxU = ex * Math.abs(SCREEN_UP.x) + ey * Math.abs(SCREEN_UP.y) + ez * Math.abs(SCREEN_UP.z);
|
||||
float screenRadius = Math.max(maxR, maxU);
|
||||
if (screenRadius < 1e-4f) screenRadius = 1f;
|
||||
|
||||
// Kameradistanz: Modell füllt Thumbnail (mit MARGIN Spielraum)
|
||||
float dist = screenRadius / (MARGIN * FastMath.tan(HALF_FOV));
|
||||
float near = Math.max(dist * 0.01f, 0.001f);
|
||||
float far = dist * 10f;
|
||||
|
||||
Camera cam = new Camera(SIZE, SIZE);
|
||||
cam.setFrustumPerspective(FOV_DEG, 1f, near, far);
|
||||
cam.setLocation(center.add(CAM_OFFSET.mult(dist)));
|
||||
cam.lookAt(center, Vector3f.UNIT_Y);
|
||||
|
||||
// Licht genau aus Blickrichtung (= Richtung von Kamera zum Modell)
|
||||
Vector3f lightDir = CAM_OFFSET.negate(); // von Kamera → Zentrum
|
||||
|
||||
Node scene = new Node("_thumb_scene");
|
||||
scene.addLight(new DirectionalLight(lightDir, new ColorRGBA(1.6f, 1.6f, 1.6f, 1f)));
|
||||
scene.addLight(new AmbientLight(new ColorRGBA(0.25f, 0.25f, 0.25f, 1f)));
|
||||
scene.attachChild(model);
|
||||
scene.updateGeometricState();
|
||||
|
||||
return renderToFramebuffer(scene, cam, rm, renderer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die UserData {@code "thumbnail"} als Base64-PNG auf dem Modell.
|
||||
* Vor dem BinaryExporter.save() aufrufen.
|
||||
*/
|
||||
public static void embed(Spatial model, byte[] pngBytes) {
|
||||
if (pngBytes == null) return;
|
||||
model.setUserData(USERDATA_KEY, Base64.getEncoder().encodeToString(pngBytes));
|
||||
}
|
||||
|
||||
public static String getUserDataKey() { return USERDATA_KEY; }
|
||||
|
||||
/** Speichert PNG-Bytes als {@code <j3oPath>.thumb.png}. */
|
||||
public static void saveSidecar(byte[] pngBytes, Path j3oPath) {
|
||||
if (pngBytes == null) return;
|
||||
try {
|
||||
Files.write(sidecarPath(j3oPath), pngBytes);
|
||||
} catch (IOException e) {
|
||||
System.err.println("[ThumbnailRenderer] Sidecar-Fehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Pfad der Sidecar-Datei: {@code <j3oPath>.thumb.png}. */
|
||||
public static Path sidecarPath(Path j3oPath) {
|
||||
return j3oPath.resolveSibling(j3oPath.getFileName() + ".thumb.png");
|
||||
}
|
||||
|
||||
// ── Intern ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static byte[] renderToFramebuffer(Node scene, Camera cam,
|
||||
RenderManager rm, Renderer renderer) {
|
||||
Texture2D colorTex = new Texture2D(SIZE, SIZE, Image.Format.RGBA8);
|
||||
FrameBuffer fb = new FrameBuffer(SIZE, SIZE, 1);
|
||||
fb.setDepthBuffer(Image.Format.Depth);
|
||||
fb.setColorTexture(colorTex);
|
||||
|
||||
ViewPort vp = rm.createPreView("_blight_thumb", cam);
|
||||
vp.setClearFlags(true, true, true);
|
||||
vp.setBackgroundColor(new ColorRGBA(0f, 0f, 0f, 0f)); // transparent
|
||||
vp.setOutputFrameBuffer(fb);
|
||||
vp.attachScene(scene);
|
||||
|
||||
try {
|
||||
rm.renderViewPort(vp, 0.016f);
|
||||
ByteBuffer buf = BufferUtils.createByteBuffer(SIZE * SIZE * 4);
|
||||
renderer.readFrameBuffer(fb, buf);
|
||||
return pngFromRgba(buf);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[ThumbnailRenderer] Render-Fehler: " + e.getMessage());
|
||||
return null;
|
||||
} finally {
|
||||
rm.removePreView(vp);
|
||||
scene.detachAllChildren(); // model-Node löst sich aus _thumb_scene
|
||||
fb.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/** Konvertiert RGBA-Rohdaten (OpenGL, Y=0 unten) in ein ARGB-PNG mit Alphakanal. */
|
||||
private static byte[] pngFromRgba(ByteBuffer buf) {
|
||||
buf.rewind();
|
||||
byte[] raw = new byte[SIZE * SIZE * 4];
|
||||
buf.get(raw);
|
||||
BufferedImage img = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < SIZE; y++) {
|
||||
for (int x = 0; x < SIZE; x++) {
|
||||
int srcY = SIZE - 1 - y; // Y-Flip: OpenGL ist bottom-up
|
||||
int i = (srcY * SIZE + x) * 4;
|
||||
int r = raw[i] & 0xFF;
|
||||
int g = raw[i + 1] & 0xFF;
|
||||
int b = raw[i + 2] & 0xFF;
|
||||
int a = raw[i + 3] & 0xFF;
|
||||
img.setRGB(x, y, (a << 24) | (r << 16) | (g << 8) | b);
|
||||
}
|
||||
}
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
ImageIO.write(img, "PNG", baos);
|
||||
return baos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
@@ -21,31 +20,121 @@ import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Item-Verwaltung: zwei identische ItemPanel-Instanzen nebeneinander.
|
||||
* Liste sortiert nach Kategorie, dann nach Name.
|
||||
* Item-Manager: Liste links (volle Höhe), Bearbeitungsformular rechts.
|
||||
*/
|
||||
public class ItemEditorView extends BorderPane {
|
||||
|
||||
private final ObservableList<Item> sharedItems = FXCollections.observableArrayList();
|
||||
private final ObservableList<Item> items = FXCollections.observableArrayList();
|
||||
private final Path itemDir;
|
||||
private final Path assetRoot;
|
||||
|
||||
private final SortedList<Item> sortedItems;
|
||||
private final ListView<Item> listView;
|
||||
private Item current = null;
|
||||
|
||||
// Form-Felder
|
||||
private TextField idField;
|
||||
private ComboBox<ItemCategory> catCombo;
|
||||
private TextField nameField;
|
||||
private TextField descField;
|
||||
private Spinner<Integer> goldSpinner;
|
||||
private TextField modelRefField;
|
||||
private VBox formContainer;
|
||||
private Button deleteBtn;
|
||||
|
||||
public ItemEditorView(Path itemDir) {
|
||||
this.itemDir = itemDir;
|
||||
this.itemDir = itemDir;
|
||||
this.assetRoot = itemDir.getParent(); // items/ ist direkt unter assetRoot
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
reload();
|
||||
|
||||
ItemPanel left = new ItemPanel("Liste 1", sharedItems, itemDir, this::reload);
|
||||
ItemPanel right = new ItemPanel("Liste 2", sharedItems, itemDir, this::reload);
|
||||
// ── Linke Seite: Liste ────────────────────────────────────────────────
|
||||
sortedItems = new SortedList<>(items, ItemIO.SORT_ORDER);
|
||||
listView = new ListView<>(sortedItems);
|
||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
listView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Item item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) { setText(null); setStyle(""); return; }
|
||||
String catName = item.getCategory() != null ? item.getCategory().name() : "—";
|
||||
String id = item.getItemId() != null ? item.getItemId() : "—";
|
||||
setText(id);
|
||||
String color = categoryColor(item.getCategory());
|
||||
setStyle("-fx-text-fill: #dddddd;"
|
||||
+ " -fx-border-color: transparent transparent transparent " + color + ";"
|
||||
+ " -fx-border-width: 0 0 0 3;"
|
||||
+ " -fx-padding: 3 6 3 8;");
|
||||
setTooltip(new Tooltip("[" + catName + "] " + id));
|
||||
}
|
||||
});
|
||||
listView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, old, nw) -> onItemSelected(old, nw));
|
||||
|
||||
HBox panels = new HBox(1, left, right);
|
||||
HBox.setHgrow(left, Priority.ALWAYS);
|
||||
HBox.setHgrow(right, Priority.ALWAYS);
|
||||
setCenter(panels);
|
||||
Button refreshBtn = new Button("↺");
|
||||
refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
||||
refreshBtn.setOnAction(e -> reload());
|
||||
Label titleLbl = new Label("Items");
|
||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #aaccee;");
|
||||
HBox header = new HBox(8, titleLbl, refreshBtn);
|
||||
header.setPadding(new Insets(8, 10, 8, 10));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
header.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444; -fx-border-width: 0 0 1 0;");
|
||||
|
||||
HBox legend = buildLegend();
|
||||
legend.setPadding(new Insets(4, 8, 4, 8));
|
||||
legend.setStyle("-fx-background-color: #1a1a2a;");
|
||||
|
||||
Button newBtn = new Button("Neues Item");
|
||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||
newBtn.setOnAction(e -> createItem());
|
||||
|
||||
deleteBtn = new Button("Löschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||
deleteBtn.setDisable(true);
|
||||
deleteBtn.setOnAction(e -> deleteSelected());
|
||||
|
||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||
|
||||
VBox leftPane = new VBox(header, listView, legend, listButtons);
|
||||
leftPane.setPrefWidth(440);
|
||||
leftPane.setMinWidth(300);
|
||||
leftPane.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #3a3a4a; -fx-border-width: 0 1 0 0;");
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
|
||||
// ── Rechte Seite: Formular ────────────────────────────────────────────
|
||||
formContainer = buildForm();
|
||||
formContainer.setDisable(true);
|
||||
|
||||
ScrollPane formScroll = new ScrollPane(formContainer);
|
||||
formScroll.setFitToWidth(true);
|
||||
formScroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||
|
||||
setLeft(leftPane);
|
||||
setCenter(formScroll);
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
// ── Öffentliche API ───────────────────────────────────────────────────────
|
||||
|
||||
public void selectItem(String itemId) {
|
||||
if (itemId == null) return;
|
||||
items.stream()
|
||||
.filter(i -> itemId.equals(i.getItemId()))
|
||||
.findFirst()
|
||||
.ifPresent(listView.getSelectionModel()::select);
|
||||
}
|
||||
|
||||
// ── Reload ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void reload() {
|
||||
String selectedId = current != null ? current.getItemId() : null;
|
||||
List<Item> loaded = ItemIO.loadAll(itemDir);
|
||||
sharedItems.setAll(loaded);
|
||||
items.setAll(loaded);
|
||||
if (selectedId != null) selectItem(selectedId);
|
||||
}
|
||||
|
||||
// ── Category colors ───────────────────────────────────────────────────────
|
||||
@@ -62,306 +151,233 @@ public class ItemEditorView extends BorderPane {
|
||||
};
|
||||
}
|
||||
|
||||
// ── Single panel ──────────────────────────────────────────────────────────
|
||||
// ── Legend ────────────────────────────────────────────────────────────────
|
||||
|
||||
static class ItemPanel extends VBox {
|
||||
private HBox buildLegend() {
|
||||
HBox box = new HBox(8);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
for (ItemCategory cat : ItemCategory.values()) {
|
||||
Label dot = new Label("■");
|
||||
dot.setStyle("-fx-text-fill: " + categoryColor(cat) + "; -fx-font-size: 10;");
|
||||
Label lbl = new Label(cat.name());
|
||||
lbl.setStyle("-fx-text-fill: #888; -fx-font-size: 10;");
|
||||
box.getChildren().addAll(dot, lbl);
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
private final ObservableList<Item> items;
|
||||
private final Path itemDir;
|
||||
private final Runnable onSaved;
|
||||
// ── Form construction ─────────────────────────────────────────────────────
|
||||
|
||||
private final SortedList<Item> sortedItems;
|
||||
private final ListView<Item> listView;
|
||||
private Item current = null;
|
||||
private VBox buildForm() {
|
||||
VBox form = new VBox(6);
|
||||
form.setPadding(new Insets(14));
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
// Form fields
|
||||
private TextField idField;
|
||||
private ComboBox<ItemCategory> catCombo;
|
||||
private TextField nameField;
|
||||
private TextField descField;
|
||||
private Spinner<Integer> goldSpinner;
|
||||
private TextField modelRefField;
|
||||
idField = new TextField();
|
||||
idField.setPromptText("eindeutige ID");
|
||||
idField.focusedProperty().addListener((obs, wasFocused, isFocused) -> {
|
||||
if (wasFocused && !isFocused) onIdCommitted();
|
||||
});
|
||||
idField.setOnAction(e -> onIdCommitted());
|
||||
|
||||
// Form container
|
||||
private VBox formContainer;
|
||||
private Button deleteBtn;
|
||||
catCombo = new ComboBox<>();
|
||||
catCombo.getItems().addAll(ItemCategory.values());
|
||||
catCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
catCombo.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(ItemCategory cat, boolean empty) {
|
||||
super.updateItem(cat, empty);
|
||||
if (empty || cat == null) { setText(null); return; }
|
||||
setText(cat.name());
|
||||
setStyle("-fx-text-fill: " + categoryColor(cat) + ";");
|
||||
}
|
||||
});
|
||||
catCombo.setButtonCell(new ListCell<>() {
|
||||
@Override protected void updateItem(ItemCategory cat, boolean empty) {
|
||||
super.updateItem(cat, empty);
|
||||
if (empty || cat == null) { setText(null); return; }
|
||||
setText(cat.name());
|
||||
setStyle("-fx-text-fill: " + categoryColor(cat) + ";");
|
||||
}
|
||||
});
|
||||
|
||||
ItemPanel(String title, ObservableList<Item> items, Path itemDir, Runnable onSaved) {
|
||||
this.items = items;
|
||||
this.itemDir = itemDir;
|
||||
this.onSaved = onSaved;
|
||||
nameField = new TextField();
|
||||
nameField.setPromptText("TextReference-Schlüssel");
|
||||
descField = new TextField();
|
||||
descField.setPromptText("TextReference-Schlüssel");
|
||||
goldSpinner = new Spinner<>(0, 999999, 0);
|
||||
goldSpinner.setEditable(true);
|
||||
goldSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
setStyle("-fx-background-color: #252535; -fx-border-color: #3a3a4a; -fx-border-width: 0 1 0 0;");
|
||||
setSpacing(0);
|
||||
modelRefField = new TextField();
|
||||
modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)");
|
||||
modelRefField.setEditable(false);
|
||||
modelRefField.setStyle("-fx-background-color: #1e1e2e; -fx-text-fill: #ccc;");
|
||||
|
||||
// ── Header ────────────────────────────────────────────────────────
|
||||
Label titleLbl = new Label(title);
|
||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #aaccee;");
|
||||
Button refreshBtn = new Button("↺");
|
||||
refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
||||
refreshBtn.setOnAction(e -> onSaved.run());
|
||||
HBox header = new HBox(8, titleLbl, refreshBtn);
|
||||
header.setPadding(new Insets(8, 10, 8, 10));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
header.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444;"
|
||||
+ " -fx-border-width: 0 0 1 0;");
|
||||
Button pickModelBtn = new Button("…");
|
||||
pickModelBtn.setTooltip(new Tooltip("Modell auswählen"));
|
||||
pickModelBtn.setOnAction(e -> pickModel());
|
||||
|
||||
// ── Item list (sorted) ─────────────────────────────────────────────
|
||||
sortedItems = new SortedList<>(items, ItemIO.SORT_ORDER);
|
||||
listView = new ListView<>(sortedItems);
|
||||
listView.setPrefHeight(200);
|
||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
listView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Item item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) { setText(null); setStyle(""); return; }
|
||||
HBox modelRow = new HBox(4, modelRefField, pickModelBtn);
|
||||
HBox.setHgrow(modelRefField, Priority.ALWAYS);
|
||||
modelRow.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
String catName = item.getCategory() != null ? item.getCategory().name() : "—";
|
||||
String name = item.getName() != null ? item.getName().id()
|
||||
: (item.getItemId() != null ? item.getItemId() : "—");
|
||||
setText(name);
|
||||
String color = categoryColor(item.getCategory());
|
||||
setStyle("-fx-text-fill: #dddddd;"
|
||||
+ " -fx-border-color: transparent transparent transparent " + color + ";"
|
||||
+ " -fx-border-width: 0 0 0 3;"
|
||||
+ " -fx-padding: 3 6 3 8;");
|
||||
setTooltip(new Tooltip("[" + catName + "] " + item.getItemId()));
|
||||
}
|
||||
});
|
||||
listView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, old, nw) -> onItemSelected(old, nw));
|
||||
Button clearModelBtn = new Button("✕");
|
||||
clearModelBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
||||
clearModelBtn.setTooltip(new Tooltip("Modell-Referenz entfernen"));
|
||||
clearModelBtn.setOnAction(e -> modelRefField.clear());
|
||||
|
||||
// Category legend
|
||||
HBox legend = buildLegend();
|
||||
legend.setPadding(new Insets(4, 8, 4, 8));
|
||||
legend.setStyle("-fx-background-color: #1a1a2a;");
|
||||
HBox modelFullRow = new HBox(4, modelRefField, pickModelBtn, clearModelBtn);
|
||||
HBox.setHgrow(modelRefField, Priority.ALWAYS);
|
||||
modelFullRow.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
Button newBtn = new Button("Neues Item");
|
||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||
newBtn.setOnAction(e -> createItem());
|
||||
Button saveBtn = new Button("Item speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentItem());
|
||||
|
||||
deleteBtn = new Button("Löschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||
deleteBtn.setDisable(true);
|
||||
deleteBtn.setOnAction(e -> deleteSelected());
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Item"),
|
||||
new Separator(),
|
||||
row("Item-ID:", idField),
|
||||
row("Kategorie:", catCombo),
|
||||
new Separator(),
|
||||
sectionTitle("Texte"),
|
||||
row("Name:", nameField),
|
||||
row("Beschreibung:", descField),
|
||||
new Separator(),
|
||||
sectionTitle("Werte"),
|
||||
row("Wert (Gold):", goldSpinner),
|
||||
row("Modell:", modelFullRow),
|
||||
new Separator(),
|
||||
saveBtn
|
||||
);
|
||||
return form;
|
||||
}
|
||||
|
||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||
// ── Modell-Auswahl ────────────────────────────────────────────────────────
|
||||
|
||||
VBox listSection = new VBox(listView, legend, listButtons);
|
||||
listSection.setStyle("-fx-background-color: #1a1a2a;");
|
||||
private void pickModel() {
|
||||
ModelChooser chooser = new ModelChooser(assetRoot);
|
||||
chooser.showAndWait().ifPresent(path -> modelRefField.setText(path));
|
||||
}
|
||||
|
||||
// ── Form ──────────────────────────────────────────────────────────
|
||||
formContainer = buildForm();
|
||||
private void onIdCommitted() {
|
||||
String id = idField.getText().trim();
|
||||
if (id.isBlank()) return;
|
||||
if (nameField.getText().isBlank()) nameField.setText(id + ".name");
|
||||
if (descField.getText().isBlank()) descField.setText(id + ".description");
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onItemSelected(Item old, Item nw) {
|
||||
if (old != null) saveFormToItem(old);
|
||||
current = nw;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) {
|
||||
formContainer.setDisable(false);
|
||||
loadFormFromItem(nw);
|
||||
} else {
|
||||
formContainer.setDisable(true);
|
||||
|
||||
ScrollPane formScroll = new ScrollPane(formContainer);
|
||||
formScroll.setFitToWidth(true);
|
||||
formScroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||
VBox.setVgrow(formScroll, Priority.ALWAYS);
|
||||
|
||||
getChildren().addAll(header, listSection, new Separator(), formScroll);
|
||||
}
|
||||
|
||||
// ── Legend ────────────────────────────────────────────────────────────
|
||||
|
||||
private HBox buildLegend() {
|
||||
HBox box = new HBox(10);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
for (ItemCategory cat : ItemCategory.values()) {
|
||||
Label dot = new Label("■");
|
||||
dot.setStyle("-fx-text-fill: " + categoryColor(cat) + "; -fx-font-size: 10;");
|
||||
Label lbl = new Label(cat.name());
|
||||
lbl.setStyle("-fx-text-fill: #888; -fx-font-size: 10;");
|
||||
box.getChildren().addAll(dot, lbl);
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
// ── Form construction ─────────────────────────────────────────────────
|
||||
|
||||
private VBox buildForm() {
|
||||
VBox form = new VBox(6);
|
||||
form.setPadding(new Insets(10));
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
idField = new TextField();
|
||||
idField.setPromptText("eindeutige ID");
|
||||
|
||||
catCombo = new ComboBox<>();
|
||||
catCombo.getItems().addAll(ItemCategory.values());
|
||||
catCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
catCombo.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(ItemCategory cat, boolean empty) {
|
||||
super.updateItem(cat, empty);
|
||||
if (empty || cat == null) { setText(null); return; }
|
||||
setText(cat.name());
|
||||
setStyle("-fx-text-fill: " + categoryColor(cat) + ";");
|
||||
}
|
||||
});
|
||||
catCombo.setButtonCell(new ListCell<>() {
|
||||
@Override protected void updateItem(ItemCategory cat, boolean empty) {
|
||||
super.updateItem(cat, empty);
|
||||
if (empty || cat == null) { setText(null); return; }
|
||||
setText(cat.name());
|
||||
setStyle("-fx-text-fill: " + categoryColor(cat) + ";");
|
||||
}
|
||||
});
|
||||
|
||||
nameField = new TextField();
|
||||
nameField.setPromptText("TextReference-Schlüssel");
|
||||
descField = new TextField();
|
||||
descField.setPromptText("TextReference-Schlüssel");
|
||||
goldSpinner = new Spinner<>(0, 999999, 0);
|
||||
goldSpinner.setEditable(true);
|
||||
goldSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||
modelRefField = new TextField();
|
||||
modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)");
|
||||
|
||||
Button saveBtn = new Button("Item speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentItem());
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Item"),
|
||||
new Separator(),
|
||||
row("Item-ID:", idField),
|
||||
row("Kategorie:", catCombo),
|
||||
new Separator(),
|
||||
sectionTitle("Texte"),
|
||||
row("Name:", nameField),
|
||||
row("Beschreibung:", descField),
|
||||
new Separator(),
|
||||
sectionTitle("Werte"),
|
||||
row("Wert (Gold):", goldSpinner),
|
||||
row("Modell:", modelRefField),
|
||||
new Separator(),
|
||||
saveBtn
|
||||
);
|
||||
return form;
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────
|
||||
|
||||
private void onItemSelected(Item old, Item nw) {
|
||||
if (old != null) saveFormToItem(old);
|
||||
current = nw;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) {
|
||||
formContainer.setDisable(false);
|
||||
loadFormFromItem(nw);
|
||||
} else {
|
||||
formContainer.setDisable(true);
|
||||
clearForm();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFormFromItem(Item item) {
|
||||
idField.setText(safe(item.getItemId()));
|
||||
catCombo.setValue(item.getCategory());
|
||||
nameField.setText(item.getName() != null ? item.getName().id() : "");
|
||||
descField.setText(item.getDescription() != null ? item.getDescription().id() : "");
|
||||
goldSpinner.getValueFactory().setValue(item.getWorthGold());
|
||||
ObjectReference ref = item.getModelRef();
|
||||
modelRefField.setText(ref != null && ref.getPath() != null ? ref.getPath() : "");
|
||||
}
|
||||
|
||||
private void saveFormToItem(Item item) {
|
||||
item.setItemId(idField.getText().trim());
|
||||
item.setCategory(catCombo.getValue());
|
||||
item.setName(ref(nameField));
|
||||
item.setDescription(ref(descField));
|
||||
item.setWorthGold(goldSpinner.getValue());
|
||||
String mr = modelRefField.getText().trim();
|
||||
item.setModelRef(mr.isBlank() ? null : new ObjectReference(mr));
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
idField.clear();
|
||||
catCombo.setValue(null);
|
||||
nameField.clear();
|
||||
descField.clear();
|
||||
goldSpinner.getValueFactory().setValue(0);
|
||||
modelRefField.clear();
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────
|
||||
|
||||
private void createItem() {
|
||||
Item item = new Item();
|
||||
item.setItemId("neues_item_" + System.currentTimeMillis());
|
||||
item.setCategory(ItemCategory.MISC);
|
||||
items.add(item);
|
||||
// Select the new item in the sorted view
|
||||
listView.getSelectionModel().select(item);
|
||||
}
|
||||
|
||||
private void deleteSelected() {
|
||||
Item sel = listView.getSelectionModel().getSelectedItem();
|
||||
if (sel == null) return;
|
||||
String iId = sel.getItemId();
|
||||
items.remove(sel);
|
||||
if (iId != null && !iId.isBlank()) {
|
||||
try { ItemIO.delete(iId, itemDir); }
|
||||
catch (IOException e) { /* ignore */ }
|
||||
}
|
||||
current = null;
|
||||
clearForm();
|
||||
formContainer.setDisable(true);
|
||||
deleteBtn.setDisable(true);
|
||||
onSaved.run();
|
||||
}
|
||||
|
||||
private void saveCurrentItem() {
|
||||
if (current == null) return;
|
||||
saveFormToItem(current);
|
||||
if (current.getItemId() == null || current.getItemId().isBlank()) {
|
||||
new Alert(Alert.AlertType.ERROR, "Item-ID darf nicht leer sein.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ItemIO.save(current, itemDir);
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
onSaved.run();
|
||||
// Re-select after reload (list re-sorts)
|
||||
String savedId = current.getItemId();
|
||||
items.stream()
|
||||
.filter(i -> savedId.equals(i.getItemId()))
|
||||
.findFirst()
|
||||
.ifPresent(listView.getSelectionModel()::select);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
private static Label sectionTitle(String text) {
|
||||
Label l = new Label(text);
|
||||
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(120);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
|
||||
private static String safe(String s) { return s != null ? s : ""; }
|
||||
|
||||
private static TextReference ref(TextField f) {
|
||||
String s = f.getText().trim();
|
||||
return s.isBlank() ? null : new TextReference(s);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFormFromItem(Item item) {
|
||||
idField.setText(safe(item.getItemId()));
|
||||
catCombo.setValue(item.getCategory());
|
||||
nameField.setText(item.getName() != null ? item.getName().id() : "");
|
||||
descField.setText(item.getDescription() != null ? item.getDescription().id() : "");
|
||||
goldSpinner.getValueFactory().setValue(item.getWorthGold());
|
||||
ObjectReference ref = item.getModelRef();
|
||||
modelRefField.setText(ref != null && ref.getPath() != null ? ref.getPath() : "");
|
||||
}
|
||||
|
||||
private void saveFormToItem(Item item) {
|
||||
item.setItemId(idField.getText().trim());
|
||||
item.setCategory(catCombo.getValue());
|
||||
item.setName(ref(nameField));
|
||||
item.setDescription(ref(descField));
|
||||
item.setWorthGold(goldSpinner.getValue());
|
||||
String mr = modelRefField.getText().trim();
|
||||
item.setModelRef(mr.isBlank() ? null : new ObjectReference(mr));
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
idField.clear();
|
||||
catCombo.setValue(null);
|
||||
nameField.clear();
|
||||
descField.clear();
|
||||
goldSpinner.getValueFactory().setValue(0);
|
||||
modelRefField.clear();
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────────
|
||||
|
||||
private void createItem() {
|
||||
Item item = new Item();
|
||||
item.setItemId("neues_item_" + System.currentTimeMillis());
|
||||
item.setCategory(ItemCategory.MISC);
|
||||
items.add(item);
|
||||
listView.getSelectionModel().select(item);
|
||||
}
|
||||
|
||||
private void deleteSelected() {
|
||||
Item sel = listView.getSelectionModel().getSelectedItem();
|
||||
if (sel == null) return;
|
||||
String iId = sel.getItemId();
|
||||
items.remove(sel);
|
||||
if (iId != null && !iId.isBlank()) {
|
||||
try { ItemIO.delete(iId, itemDir); }
|
||||
catch (IOException e) { /* ignore */ }
|
||||
}
|
||||
current = null;
|
||||
clearForm();
|
||||
formContainer.setDisable(true);
|
||||
deleteBtn.setDisable(true);
|
||||
reload();
|
||||
}
|
||||
|
||||
private void saveCurrentItem() {
|
||||
if (current == null) return;
|
||||
saveFormToItem(current);
|
||||
if (current.getItemId() == null || current.getItemId().isBlank()) {
|
||||
new Alert(Alert.AlertType.ERROR, "Item-ID darf nicht leer sein.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ItemIO.save(current, itemDir);
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
String savedId = current.getItemId();
|
||||
reload();
|
||||
selectItem(savedId);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static Label sectionTitle(String text) {
|
||||
Label l = new Label(text);
|
||||
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(100);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
|
||||
private static String safe(String s) { return s != null ? s : ""; }
|
||||
|
||||
private static TextReference ref(TextField f) {
|
||||
String s = f.getText().trim();
|
||||
return s.isBlank() ? null : new TextReference(s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Modality;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Dialog zum Auswählen eines .j3o-Modells.
|
||||
* Zeigt Karten mit 128×128-Thumbnail wenn eine {@code .j3o.thumb.png} Sidecar-Datei vorhanden ist.
|
||||
* Gibt den relativen Asset-Pfad (z.B. "Models/Items/sword.j3o") zurück oder {@code null} bei Abbruch.
|
||||
*/
|
||||
public class ModelChooser extends Dialog<String> {
|
||||
|
||||
private static final int THUMB_SIZE = 96;
|
||||
private static final int CARD_W = THUMB_SIZE + 16;
|
||||
private static final int CARD_H = THUMB_SIZE + 38;
|
||||
|
||||
private final ToggleGroup toggleGroup = new ToggleGroup();
|
||||
private final List<ToggleButton> allCards = new ArrayList<>();
|
||||
private final VBox contentBox = new VBox(16);
|
||||
private final TextField filterField = new TextField();
|
||||
private final Path assetRoot;
|
||||
|
||||
private String selectedPath;
|
||||
|
||||
public ModelChooser(Path assetRoot) {
|
||||
this.assetRoot = assetRoot;
|
||||
setTitle("Modell auswählen");
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
setResizable(true);
|
||||
|
||||
contentBox.setPadding(new Insets(4));
|
||||
|
||||
Path modelsDir = assetRoot.resolve("Models");
|
||||
if (Files.isDirectory(modelsDir)) {
|
||||
// Modelle nach Unterordner gruppieren
|
||||
buildCards(modelsDir);
|
||||
}
|
||||
|
||||
filterField.setPromptText("Filtern…");
|
||||
filterField.textProperty().addListener(
|
||||
(obs, o, n) -> applyFilter(n == null ? "" : n.toLowerCase()));
|
||||
|
||||
ScrollPane scroll = new ScrollPane(contentBox);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setPrefSize(720, 500);
|
||||
scroll.setStyle("-fx-background-color: transparent;");
|
||||
|
||||
VBox root = new VBox(8, filterField, scroll);
|
||||
root.setPadding(new Insets(10));
|
||||
|
||||
getDialogPane().setContent(root);
|
||||
getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK);
|
||||
okBtn.setText("Auswählen");
|
||||
okBtn.setDisable(true);
|
||||
|
||||
toggleGroup.selectedToggleProperty().addListener((obs, o, n) -> {
|
||||
okBtn.setDisable(n == null);
|
||||
selectedPath = (n instanceof ToggleButton tb) ? (String) tb.getUserData() : null;
|
||||
});
|
||||
|
||||
setResultConverter(btn -> btn == ButtonType.OK ? selectedPath : null);
|
||||
}
|
||||
|
||||
// ── Karten bauen ─────────────────────────────────────────────────────────
|
||||
|
||||
private void buildCards(Path modelsDir) {
|
||||
// Alle .j3o-Dateien sammeln, nach Verzeichnis gruppiert
|
||||
List<Path> j3oFiles = new ArrayList<>();
|
||||
try (Stream<Path> walk = Files.walk(modelsDir)) {
|
||||
walk.filter(Files::isRegularFile)
|
||||
.filter(p -> p.getFileName().toString().toLowerCase().endsWith(".j3o"))
|
||||
.sorted()
|
||||
.forEach(j3oFiles::add);
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
if (j3oFiles.isEmpty()) return;
|
||||
|
||||
// Nach Unterordner gruppieren
|
||||
String currentGroup = null;
|
||||
FlowPane currentPane = null;
|
||||
|
||||
for (Path j3o : j3oFiles) {
|
||||
String relToModels = modelsDir.relativize(j3o.getParent()).toString().replace('\\', '/');
|
||||
if (relToModels.isEmpty()) relToModels = "(Wurzel)";
|
||||
|
||||
if (!relToModels.equals(currentGroup)) {
|
||||
currentGroup = relToModels;
|
||||
currentPane = newFlowPane();
|
||||
contentBox.getChildren().add(section(currentGroup, currentPane));
|
||||
}
|
||||
|
||||
String assetPath = assetRoot.relativize(j3o).toString().replace('\\', '/');
|
||||
Image thumb = loadThumb(j3o);
|
||||
ToggleButton card = buildCard(j3o.getFileName().toString(), assetPath, thumb);
|
||||
currentPane.getChildren().add(card);
|
||||
allCards.add(card);
|
||||
}
|
||||
}
|
||||
|
||||
private Image loadThumb(Path j3o) {
|
||||
// Sidecar: <model.j3o>.thumb.png
|
||||
Path sidecar = j3o.resolveSibling(j3o.getFileName() + ".thumb.png");
|
||||
if (Files.isRegularFile(sidecar)) {
|
||||
try (InputStream is = Files.newInputStream(sidecar)) {
|
||||
return new Image(is, THUMB_SIZE, THUMB_SIZE, true, true);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Karte ────────────────────────────────────────────────────────────────
|
||||
|
||||
private ToggleButton buildCard(String displayName, String assetPath, Image thumb) {
|
||||
ImageView iv;
|
||||
if (thumb != null) {
|
||||
iv = new ImageView(thumb);
|
||||
} else {
|
||||
// Platzhalter: Gitterbox-Symbol
|
||||
iv = new ImageView();
|
||||
iv.setStyle("-fx-background-color: #2a2a3a;");
|
||||
}
|
||||
iv.setFitWidth(THUMB_SIZE);
|
||||
iv.setFitHeight(THUMB_SIZE);
|
||||
iv.setPreserveRatio(true);
|
||||
|
||||
String short_ = truncate(displayName.replace(".j3o", ""), 14);
|
||||
Label lbl = new Label(short_);
|
||||
lbl.setMaxWidth(CARD_W - 4);
|
||||
lbl.setAlignment(Pos.CENTER);
|
||||
lbl.setStyle("-fx-font-size: 10;");
|
||||
|
||||
VBox graphic = new VBox(4, iv, lbl);
|
||||
graphic.setAlignment(Pos.TOP_CENTER);
|
||||
|
||||
ToggleButton btn = new ToggleButton();
|
||||
btn.setGraphic(graphic);
|
||||
btn.setUserData(assetPath);
|
||||
btn.setToggleGroup(toggleGroup);
|
||||
btn.setPrefSize(CARD_W, CARD_H);
|
||||
btn.setTooltip(new Tooltip(assetPath));
|
||||
|
||||
btn.setOnMouseClicked(e -> {
|
||||
if (e.getClickCount() == 2) {
|
||||
selectedPath = assetPath;
|
||||
Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK);
|
||||
if (okBtn != null) okBtn.fire();
|
||||
}
|
||||
});
|
||||
return btn;
|
||||
}
|
||||
|
||||
// ── Filter ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void applyFilter(String lower) {
|
||||
for (ToggleButton card : allCards) {
|
||||
String path = (String) card.getUserData();
|
||||
boolean visible = lower.isBlank() || path.toLowerCase().contains(lower);
|
||||
card.setVisible(visible);
|
||||
card.setManaged(visible);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Layout-Helfer ─────────────────────────────────────────────────────────
|
||||
|
||||
private static FlowPane newFlowPane() {
|
||||
FlowPane fp = new FlowPane(8, 8);
|
||||
fp.setPadding(new Insets(4, 4, 4, 4));
|
||||
return fp;
|
||||
}
|
||||
|
||||
private static VBox section(String title, FlowPane pane) {
|
||||
Label lbl = new Label(title);
|
||||
lbl.setStyle("-fx-font-weight: bold; -fx-font-size: 11;");
|
||||
return new VBox(4, lbl, new Separator(), pane);
|
||||
}
|
||||
|
||||
private static String truncate(String s, int max) {
|
||||
if (s.length() <= max) return s;
|
||||
return s.substring(0, max - 1) + "…";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user