diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index 51c4e6c..b98ee88 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -930,6 +930,16 @@ public class SharedInput { public volatile int bakeTotal = 0; /** JME → JFX: Status-Meldung nach Abschluss des Backens oder Löschens. */ public volatile String bakeStatusMsg = null; + /** JME → JFX: Anzahl dirty Chunks in der Session (0 = nichts zu backen). */ + public volatile int sessionDirtyVoxelCount = 0; + /** JFX → JME: Nur dirty Chunks backen (Bake-on-Exit). */ + public volatile boolean bakeOnExitRequested = false; + /** JFX → JME: Laufenden Bake-on-Exit abbrechen. */ + public volatile boolean bakeCancelRequested = false; + /** JME → JFX: Bake-on-Exit abgeschlossen (true) oder abgebrochen (false). */ + public volatile boolean bakeOnExitSuccess = false; + /** JME → JFX: Bake-on-Exit ist fertig (Erfolg oder Abbruch). */ + public volatile boolean bakeOnExitDone = false; /** JFX → JME: Höhen-Overlay ein/aus (Terrain / Voxel / Baked im 100m-Radius). */ public volatile boolean debugHeightOverlayEnabled = false; @@ -937,22 +947,20 @@ public class SharedInput { /** JFX → JME: 1m-Gitternetz auf Terrain/Voxel ein/aus. */ public volatile boolean showGrid = true; - /** - * Per BFS selektierte gebackene Chunks (chunkKey-kodiert). - * Thread-sicher; JME schreibt, JFX liest (nur size() für Anzeige). - */ - public final java.util.Set markedDeleteBakedKeys = - java.util.concurrent.ConcurrentHashMap.newKeySet(); - /** JME/JFX → JME: Wireframe-Visualisierung der Selektion neu berechnen. */ - public volatile boolean markedBakedSelectionDirty = false; - /** JFX → JME: selektierte Chunks löschen. */ - public volatile boolean deleteMarkedBakedRequested = false; - /** JFX → JME: selektierte Chunks aus Pre-Bake-Backup wiederherstellen. */ - public volatile boolean revertMarkedBakedRequested = false; - /** JFX → JME: unterirdische Voxel-Spalten (Oberfläche < Basis-Terrain) löschen. */ - public volatile boolean cleanupUndergroundRequested = false; - /** JME → JFX: Ergebnis der Unterirdisch-Bereinigung (null = läuft noch / nicht gestartet). */ - public volatile String cleanupUndergroundStatus = null; + /** JME → JME: Keys von Chunks die nach einem Bake neu geladen werden müssen. */ + public final java.util.concurrent.ConcurrentLinkedQueue sculptInvalidateKeys + = new java.util.concurrent.ConcurrentLinkedQueue<>(); + + /** JFX → JME: Bereinigung starten (Flags unten bestimmen was). */ + public volatile boolean cleanupUndergroundRequested = false; + /** JFX → JME: unterirdische Chunks (cy < 0) entfernen. */ + public volatile boolean cleanupUndergroundVoxels = false; + /** JFX → JME: verwaiste .j3o ohne zugehörige .blvc löschen. */ + public volatile boolean cleanupOrphanedBaked = false; + /** JFX → JME: alle geladenen Chunks als dirty markieren (→ Bake-on-Exit). */ + public volatile boolean markAllChunksDirtyRequested = false; + /** JME → JFX: Ergebnis der Bereinigung (null = läuft noch / nicht gestartet). */ + public volatile String cleanupUndergroundStatus = null; /** Terrain-Slot (0-7) für flache Voxel-Flächen, -1 = kein Slot. */ public volatile int voxelFlatSlot = -1; diff --git a/blight-editor/src/main/java/de/blight/editor/state/SculptedMeshEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/SculptedMeshEditorState.java index 6e25049..eeb7b3a 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/SculptedMeshEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/SculptedMeshEditorState.java @@ -71,8 +71,6 @@ public class SculptedMeshEditorState extends BaseAppState { private Material bakedMat = null; private boolean gridVisible = true; private Material highlightMat = null; - private Material selectionWireframeMat = null; - private final Set appliedSelectionKeys = new HashSet<>(); // ── innere Klasse ───────────────────────────────────────────────────────── @@ -125,10 +123,6 @@ public class SculptedMeshEditorState extends BaseAppState { highlightMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); highlightMat.setColor("Color", new ColorRGBA(1f, 0.65f, 0f, 1f)); - selectionWireframeMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); - selectionWireframeMat.setColor("Color", new ColorRGBA(0.1f, 1f, 0.45f, 1f)); - selectionWireframeMat.getAdditionalRenderState().setWireframe(true); - brushIndicator = buildBrushIndicator(); app.getRootNode().attachChild(brushIndicator); @@ -138,45 +132,11 @@ public class SculptedMeshEditorState extends BaseAppState { @Override protected void cleanup(Application application) { saveAllDirty(); - // Wireframe-Material zurücksetzen - for (long key : appliedSelectionKeys) { - EditableMesh em = meshes.get(key); - if (em != null && em.geo != null && normalMat != null) { - em.geo.setMaterial(normalMat); - } - } - appliedSelectionKeys.clear(); sculptRoot.removeFromParent(); if (brushIndicator != null) brushIndicator.removeFromParent(); meshes.clear(); } - private void updateSelectionWireframe() { - Set current = new HashSet<>(input.markedDeleteBakedKeys); - // Wireframe von nicht mehr selektierten Chunks entfernen - Iterator it = appliedSelectionKeys.iterator(); - while (it.hasNext()) { - long key = it.next(); - if (!current.contains(key)) { - EditableMesh em = meshes.get(key); - if (em != null && em.geo != null) { - em.geo.setMaterial(key == selectedKey ? highlightMat : normalMat); - } - it.remove(); - } - } - // Wireframe auf neu selektierte Chunks anwenden - for (long key : current) { - if (!appliedSelectionKeys.contains(key)) { - EditableMesh em = meshes.get(key); - if (em != null && em.geo != null) { - em.geo.setMaterial(selectionWireframeMat); - appliedSelectionKeys.add(key); - } - } - } - } - @Override protected void onEnable() {} @Override protected void onDisable() {} @@ -195,12 +155,6 @@ public class SculptedMeshEditorState extends BaseAppState { if (input.sculptRescanNeeded) { input.sculptRescanNeeded = false; rescanBakedChunks(); - input.markedBakedSelectionDirty = true; - } - - if (input.markedBakedSelectionDirty) { - input.markedBakedSelectionDirty = false; - updateSelectionWireframe(); } updateBrushIndicator(); @@ -262,6 +216,12 @@ public class SculptedMeshEditorState extends BaseAppState { // ── Scan / Laden ────────────────────────────────────────────────────────── private void rescanBakedChunks() { + // Neu gebackene Chunks aus dem Cache werfen damit sie frisch geladen werden + Long invalidKey; + while ((invalidKey = input.sculptInvalidateKeys.poll()) != null) { + EditableMesh old = meshes.remove(invalidKey); + if (old != null) old.node.removeFromParent(); + } // Geladene Meshes entfernen, deren Baked-Dateien nicht mehr existieren Iterator> it = meshes.entrySet().iterator(); while (it.hasNext()) { @@ -862,6 +822,13 @@ public class SculptedMeshEditorState extends BaseAppState { return ((long)(cx & 0xFFFF)) | (((long)(cy & 0xFFFF)) << 16) | (((long)(cz & 0xFFFF)) << 32); } + /** Zeigt oder verbirgt alle gebackenen Sculpt-Meshes (beim Betreten/Verlassen des Voxel-Tabs). */ + public void setBakedVisible(boolean visible) { + if (sculptRoot != null) { + sculptRoot.setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + } + } + /** Raycast gegen alle gebackenen Voxel-Meshes; gibt den nächsten Treffpunkt oder null zurück. */ public com.jme3.math.Vector3f raycastGeometry(com.jme3.math.Ray ray) { if (sculptRoot == null) return null; diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index c9401a3..ee9db4a 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -1389,7 +1389,6 @@ public class TerrainEditorState extends BaseAppState { private void processTextureEdits() { SharedInput.TextureEdit edit; int processed = 0; - VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); while ((edit = input.textureEditQueue.poll()) != null && processed < MAX_EDITS_PER_FRAME) { processed++; float jmeX = (float)(edit.screenX() * input.viewportScaleX); @@ -1402,7 +1401,6 @@ public class TerrainEditorState extends BaseAppState { SurfaceHit hit = raycastSurfaceWithType(ray); if (hit == null) continue; Vector3f contact = hit.point; - if (!hit.voxelMesh && ves != null && ves.terrainTypeAt(contact.x, contact.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) continue; int selIdx = input.textureTool.textureIndex.getSelectedIndex(); float str = (float) input.textureTool.brushStrength.getValue(); @@ -1763,37 +1761,21 @@ public class TerrainEditorState extends BaseAppState { brushRadius = (layer == 0) ? (float) input.heightTool.brushRadius.getValue() : (float) input.textureTool.brushRadius.getValue(); - VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); - if (ves != null && ves.terrainTypeAt(contactPoint.x, contactPoint.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) { - contactPoint = null; - } } } else if (layer == 3) { contactPoint = raycastSurface(ray); if (contactPoint != null) { brushRadius = (float) input.grassTool.brushRadius.getValue(); - VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); - if (ves != null && ves.terrainTypeAt(contactPoint.x, contactPoint.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) { - contactPoint = null; - } } } else if (layer == SharedInput.LAYER_GRASS_VERTEX) { contactPoint = raycastSurface(ray); if (contactPoint != null) { brushRadius = (float) input.grassVertexTool.brushRadius.getValue(); - VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); - if (ves != null && ves.terrainTypeAt(contactPoint.x, contactPoint.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) { - contactPoint = null; - } } } else if (layer == SharedInput.LAYER_TERRAIN_GRASS) { contactPoint = raycastSurface(ray); if (contactPoint != null) { brushRadius = (float) input.terrainGrassTool.brushRadius.getValue(); - VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); - if (ves != null && ves.terrainTypeAt(contactPoint.x, contactPoint.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) { - contactPoint = null; - } } } diff --git a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java index a3cddd3..641e967 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java @@ -28,6 +28,7 @@ import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; import com.jme3.texture.image.ColorSpace; import com.jme3.util.BufferUtils; +import de.blight.common.SculptedMeshIO; import de.blight.common.VoxelChunk; import de.blight.common.VoxelChunkIO; import de.blight.editor.SharedInput; @@ -104,6 +105,8 @@ public class VoxelEditorState extends BaseAppState { * nicht mehrfach pro Edit-Event. */ private final Set dirtyChunksThisFrame = new HashSet<>(); + /** Session-weite dirty Chunks (für Bake-on-Exit). Nur im JME-Thread modifizieren. */ + private final Set sessionDirtyChunks = new HashSet<>(); // ── Timers ─────────────────────────────────────────────────────────────── @@ -212,6 +215,7 @@ public class VoxelEditorState extends BaseAppState { this.cam = app.getCamera(); voxelRoot = new Node("voxelEditorRoot"); + voxelRoot.setCullHint(Spatial.CullHint.Always); // nur im Voxel-Tab sichtbar app.getRootNode().attachChild(voxelRoot); // TerrainEditorState wurde vor uns attached und ist zu diesem Zeitpunkt initialisiert @@ -323,38 +327,115 @@ public class VoxelEditorState extends BaseAppState { }); } - // Selektierte Baked-Chunks löschen - if (input.deleteMarkedBakedRequested) { - input.deleteMarkedBakedRequested = false; - Set toDelete = new HashSet<>(input.markedDeleteBakedKeys); - input.markedDeleteBakedKeys.clear(); - input.markedBakedSelectionDirty = true; + // Bake-on-Exit: nur dirty Chunks backen + if (input.bakeOnExitRequested) { + input.bakeOnExitRequested = false; + input.bakeCancelRequested = false; + List toBake = new ArrayList<>(); + for (long key : sessionDirtyChunks) { + VoxelChunk c = chunks.get(key); + if (c != null) toBake.add(c); + } executor.submit(() -> { try { - deleteMarkedBaked(toDelete); + bakeAll(toBake); + if (!input.bakeCancelRequested) { + app.enqueue(() -> { + sessionDirtyChunks.clear(); + input.sessionDirtyVoxelCount = 0; + }); + input.bakeOnExitSuccess = true; + } else { + input.bakeOnExitSuccess = false; + } } catch (Throwable e) { - log.error("Baked-Löschen fehlgeschlagen: {}", e.getMessage(), e); + log.error("Bake-on-Exit fehlgeschlagen: {}", e.getMessage(), e); input.bakeStatusMsg = "FEHLER: " + e.getMessage(); + input.bakeOnExitSuccess = false; + } finally { + input.bakeOnExitDone = true; } }); } - // Selektierte Chunks aus Pre-Bake-Backup wiederherstellen - if (input.revertMarkedBakedRequested) { - input.revertMarkedBakedRequested = false; - Set toRevert = new HashSet<>(input.markedDeleteBakedKeys); - input.markedDeleteBakedKeys.clear(); - input.markedBakedSelectionDirty = true; + // Bereinigung + if (input.cleanupUndergroundRequested) { + input.cleanupUndergroundRequested = false; + final boolean doUnderground = input.cleanupUndergroundVoxels; + final boolean doOrphaned = input.cleanupOrphanedBaked; executor.submit(() -> { + int removedVoxel = 0; + int removedBaked = 0; try { - revertMarkedBaked(toRevert); - } catch (Throwable e) { - log.error("Markierter-Revert fehlgeschlagen: {}", e.getMessage(), e); - input.bakeStatusMsg = "Revert FEHLER: " + e.getMessage(); + if (doUnderground) { + // Unterirdische Chunks (cy < 0): .blvc löschen, aus Memory entfernen + List toRemove = new ArrayList<>(); + for (Map.Entry e : new ArrayList<>(chunks.entrySet())) { + VoxelChunk c = e.getValue(); + if (c.cy < 0) toRemove.add(new long[]{e.getKey(), c.cx, c.cy, c.cz}); + } + for (long[] t : toRemove) { + try { VoxelChunkIO.delete((int)t[1], (int)t[2], (int)t[3]); } catch (Exception ignored) {} + final long key = t[0]; + app.enqueue(() -> { + chunks.remove(key); + VoxelChunkNode node = nodes.remove(key); + if (node != null) node.removeFromParent(); + }); + removedVoxel++; + } + // Auch .blvc-Dateien mit cy < 0 die nicht im Memory sind + for (int[] coord : VoxelChunkIO.loadAll().stream() + .filter(c -> c.cy < 0) + .map(c -> new int[]{c.cx, c.cy, c.cz}) + .toList()) { + long key = chunkKey(coord[0], coord[1], coord[2]); + if (!chunks.containsKey(key)) { + try { VoxelChunkIO.delete(coord[0], coord[1], coord[2]); removedVoxel++; } catch (Exception ignored) {} + } + } + } + if (doOrphaned) { + // Verwaiste gebackene .j3o ohne zugehörige .blvc löschen + for (int[] cxyz : SculptedMeshIO.findAllBakedChunks()) { + int cx = cxyz[0], cy = cxyz[1], cz = cxyz[2]; + if (!VoxelChunkIO.exists(cx, cy, cz) && !VoxelChunkIO.preBakeExists(cx, cy, cz)) { + for (int lod = 0; lod <= 3; lod++) { + try { Files.deleteIfExists(VoxelChunkIO.getBakedPath(cx, cy, cz, lod)); } catch (Exception ignored) {} + } + removedBaked++; + } + } + } + List parts = new ArrayList<>(); + if (doUnderground) parts.add(removedVoxel + " unterirdische Chunks entfernt"); + if (doOrphaned) parts.add(removedBaked + " verwaiste Bakes gelöscht"); + input.cleanupUndergroundStatus = String.join(", ", parts); + } catch (Exception e) { + log.warn("Bereinigung fehlgeschlagen: {}", e.getMessage()); + input.cleanupUndergroundStatus = "Fehler: " + e.getMessage(); } }); } + // Alle Chunks (Memory + Disk) als dirty markieren → Bake-on-Exit backt alles neu + if (input.markAllChunksDirtyRequested) { + input.markAllChunksDirtyRequested = false; + for (long key : chunks.keySet()) sessionDirtyChunks.add(key); + // Chunks die noch nicht geladen sind von Disk nachladen, damit der Bake sie kennt + executor.submit(() -> { + List allDisk = VoxelChunkIO.loadAll(); + app.enqueue(() -> { + for (VoxelChunk c : allDisk) { + long key = chunkKey(c.cx, c.cy, c.cz); + if (!chunks.containsKey(key)) chunks.put(key, c); + sessionDirtyChunks.add(key); + } + input.sessionDirtyVoxelCount = sessionDirtyChunks.size(); + }); + }); + } + // Voxel-Texturen (Slot/Normal-Map) aktualisiert? if (input.voxelTexturesChanged) { input.voxelTexturesChanged = false; @@ -501,6 +582,8 @@ public class VoxelEditorState extends BaseAppState { addNodeForChunk(key, chunk); } dirtyChunksThisFrame.add(key); + sessionDirtyChunks.add(key); + input.sessionDirtyVoxelCount = sessionDirtyChunks.size(); } } @@ -561,6 +644,8 @@ public class VoxelEditorState extends BaseAppState { addNodeForChunk(key, chunk); } dirtyChunksThisFrame.add(key); + sessionDirtyChunks.add(key); + input.sessionDirtyVoxelCount = sessionDirtyChunks.size(); } } @@ -736,18 +821,6 @@ public class VoxelEditorState extends BaseAppState { int modeIdx = input.voxelTool.mode.getSelectedIndex(); boolean isHorizontal = input.voxelTool.horizontal; - // Baked-Selektierungs-Modus: keine Voxel-Bearbeitung, nur BFS-Selektion. - // Nach der Aktion automatisch auf Sinus (Modus 0) zurückschalten. - if (modeIdx == de.blight.editor.tool.VoxelTool.MODE_DELETE_BAKED) { - if (action >= 0) { - markConnectedBakedStructure(hit.pos.x, hit.pos.y, hit.pos.z); - } else { - input.markedDeleteBakedKeys.clear(); - input.markedBakedSelectionDirty = true; - } - return; - } - boolean isCave = (modeIdx == de.blight.editor.tool.VoxelTool.MODE_REMOVE); boolean isColumn = !isCave; boolean lower = action < 0; @@ -865,6 +938,8 @@ public class VoxelEditorState extends BaseAppState { addNodeForChunk(key, chunk); } dirtyChunksThisFrame.add(key); + sessionDirtyChunks.add(key); + input.sessionDirtyVoxelCount = sessionDirtyChunks.size(); } } } @@ -1058,6 +1133,8 @@ public class VoxelEditorState extends BaseAppState { long key = chunkKey(chunk.cx, chunk.cy, chunk.cz); if (!chunk.isEmpty() && !nodes.containsKey(key)) addNodeForChunk(key, chunk); dirtyChunksThisFrame.add(key); + sessionDirtyChunks.add(key); + input.sessionDirtyVoxelCount = sessionDirtyChunks.size(); } } @@ -1086,95 +1163,6 @@ public class VoxelEditorState extends BaseAppState { } } - // ── Baked-Chunk Markierung ──────────────────────────────────────────────── - - /** - * Flood-fill BFS: Markiert alle zusammenhängend gebackenen Chunks, - * ausgehend vom angeklickten Chunk. Bisherige Selektion wird vorher geleert. - * LMB = markieren; RMB = Selektion leeren (wird in applyEdit gehandhabt). - */ - private void markConnectedBakedStructure(float hitX, float hitY, float hitZ) { - input.markedDeleteBakedKeys.clear(); - - int startCx = VoxelChunk.worldXToCx(hitX); - int startCz = VoxelChunk.worldZToCz(hitZ); - int startCy = VoxelChunk.worldYToCy(hitY); - - // Nächsten gebackenen Chunk in der Nähe des Treffers finden - boolean found = false; - outer: - for (int delta = 0; delta <= 4; delta++) { - for (int dcy : (delta == 0 ? new int[]{0} : new int[]{delta, -delta})) { - if (VoxelChunkIO.bakedExists(startCx, startCy + dcy, startCz)) { - startCy = startCy + dcy; - found = true; - break outer; - } - } - } - if (!found) return; - - // 6-Richtungs-BFS durch angrenzende gebackene Chunks - Set visited = new HashSet<>(); - Queue queue = new ArrayDeque<>(); - long startKey = chunkKey(startCx, startCy, startCz); - visited.add(startKey); - queue.add(new long[]{startCx, startCy, startCz}); - - int[][] dirs = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}}; - while (!queue.isEmpty()) { - long[] cur = queue.poll(); - int cx = (int)cur[0], cy = (int)cur[1], cz = (int)cur[2]; - for (int[] d : dirs) { - int ncx = cx + d[0], ncy = cy + d[1], ncz = cz + d[2]; - long nk = chunkKey(ncx, ncy, ncz); - if (!visited.contains(nk) && VoxelChunkIO.bakedExists(ncx, ncy, ncz)) { - visited.add(nk); - queue.add(new long[]{ncx, ncy, ncz}); - } - } - } - - // Alle gefundenen Chunks selektieren + Wireframe signalisieren - for (long key : visited) { - input.markedDeleteBakedKeys.add(key); - } - input.markedBakedSelectionDirty = true; - } - - /** - * Löscht die selektierten gebackenen Chunks (alle LODs + Sculpt-Overlays). - * Läuft im Hintergrund-Thread. - */ - private void deleteMarkedBaked(Set keys) { - if (keys.isEmpty()) { - input.bakeStatusMsg = "Keine Chunks markiert."; - return; - } - int deleted = 0; - for (long key : keys) { - int cx = (int)(key & 0xFFFF); if (cx >= 0x8000) cx -= 0x10000; - int cy = (int)((key >> 16) & 0xFFFF); if (cy >= 0x8000) cy -= 0x10000; - int cz = (int)((key >> 32) & 0xFFFF); if (cz >= 0x8000) cz -= 0x10000; - for (int lod = 0; lod < 3; lod++) { - try { - Files.deleteIfExists(VoxelChunkIO.getBakedPath(cx, cy, cz, lod)); - } catch (IOException e) { - log.warn("Baked LOD{} löschen fehlgeschlagen ({},{},{}): {}", - lod, cx, cy, cz, e.getMessage()); - } - } - if (de.blight.common.SculptedMeshIO.exists(cx, cy, cz)) { - try { de.blight.common.SculptedMeshIO.delete(cx, cy, cz); } - catch (Exception e) { log.warn("Sculpt-Overlay löschen fehlgeschlagen: {}", e.getMessage()); } - } - deleted++; - } - input.sculptRescanNeeded = true; - input.bakeStatusMsg = "Gelöscht: " + deleted + " Chunk" + (deleted != 1 ? "s" : "") + "."; - log.info("Markierte Baked-Chunks gelöscht: {}.", deleted); - } - // ── Terrain-Höhe (schneller O(1)-Zugriff) ───────────────────────────────── private float terrainH(float worldX, float worldZ) { @@ -2746,6 +2734,9 @@ public class VoxelEditorState extends BaseAppState { float bakeSmoothStr = input.bakeSmoothStrength; float bakeCliffStr = input.bakeCliffNoiseStrength; for (VoxelChunk chunk : nonEmpty) { + if (input.bakeCancelRequested) { + break; + } if (bakeChunk(chunk, blurredMap, bakeSmoothStr, bakeCliffStr)) { successfullyBaked.add(chunk); } else { @@ -2756,111 +2747,21 @@ public class VoxelEditorState extends BaseAppState { input.bakeDone = baked; } - // Voxel-Daten löschen: nur für erfolgreich gebackene Chunks - for (VoxelChunk chunk : successfullyBaked) { - chunk.clear(); - try { VoxelChunkIO.delete(chunk.cx, chunk.cy, chunk.cz); } - catch (Exception e) { log.warn("Voxel-Datei löschen fehlgeschlagen ({},{},{}): {}", - chunk.cx, chunk.cy, chunk.cz, e.getMessage()); } - } - app.enqueue(() -> { - for (VoxelChunk chunk : successfullyBaked) { - long key = chunkKey(chunk.cx, chunk.cy, chunk.cz); - VoxelChunkNode node = nodes.remove(key); - if (node != null) node.removeFromParent(); - chunks.remove(key); - } - }); + // Voxeldaten (.blvc) bleiben erhalten: nach dem Baken sind Raw-Voxels weiterhin + // im Voxel-Tab sichtbar und editierbar. .blvc und .j3o koexistieren. int failed = nonEmpty.size() - successfullyBaked.size(); String msg = "Fertig: " + successfullyBaked.size() + " Chunk" + (successfullyBaked.size() != 1 ? "s" : "") + " gebacken" + (failed > 0 ? ", " + failed + " fehlgeschlagen (Voxel erhalten)" : "") + "."; // bakeTotal/bakeDone werden vom UI-Thread nach Empfang der Statusmeldung zurückgesetzt + for (VoxelChunk c : successfullyBaked) + input.sculptInvalidateKeys.add(chunkKey(c.cx, c.cy, c.cz)); input.bakeStatusMsg = msg; input.sculptRescanNeeded = true; log.info("Voxel-Bake abgeschlossen – {}.", msg); } - /** - * Stellt die selektierten Chunks auf den Vor-Bake-Zustand zurück: - * - Wenn ein Pre-Bake-Backup (.blvc.prebake) existiert, wird es eingespielt. - * - In jedem Fall werden die Baked-J3O-Dateien gelöscht, so dass - * die aktuellen .blvc-Voxeldaten wieder als ungebackene Meshes sichtbar sind. - */ - private void revertMarkedBaked(Set keys) { - if (keys.isEmpty()) { - input.bakeStatusMsg = "Keine Chunks markiert."; - return; - } - int restored = 0, noBackup = 0, failed = 0; - List reloadedChunks = new java.util.ArrayList<>(); - - for (long key : keys) { - int cx = (int)(key & 0xFFFF); if (cx >= 0x8000) cx -= 0x10000; - int cy = (int)((key >> 16) & 0xFFFF); if (cy >= 0x8000) cy -= 0x10000; - int cz = (int)((key >> 32) & 0xFFFF); if (cz >= 0x8000) cz -= 0x10000; - try { - boolean hadBackup = de.blight.common.VoxelChunkIO.preBakeExists(cx, cy, cz); - boolean hasBlvc = de.blight.common.VoxelChunkIO.exists(cx, cy, cz); - - if (!hadBackup && !hasBlvc) { - // Kein Backup vorhanden – J3O behalten, sonst entsteht leerer Bereich - noBackup++; - continue; - } - - if (hadBackup) { - // Backup vorhanden → .blvc aus Backup wiederherstellen - de.blight.common.VoxelChunkIO.restorePreBake(cx, cy, cz); - de.blight.common.VoxelChunkIO.deletePreBake(cx, cy, cz); - } - // Baked J3O-Dateien löschen, damit das ungebackene Mesh sichtbar wird - for (int lod = 0; lod < 3; lod++) { - Files.deleteIfExists(de.blight.common.VoxelChunkIO.getBakedPath(cx, cy, cz, lod)); - } - // Chunk-Daten (aus Backup oder aktuellem .blvc) in die Voxel-Engine laden - if (de.blight.common.VoxelChunkIO.exists(cx, cy, cz)) { - VoxelChunk chunk = de.blight.common.VoxelChunkIO.load(cx, cy, cz); - reloadedChunks.add(chunk); - } - restored++; - } catch (Exception e) { - log.error("Markierter-Revert fehlgeschlagen ({},{},{}): {}", cx, cy, cz, e.getMessage()); - failed++; - } - } - - List toAdd = new java.util.ArrayList<>(reloadedChunks); - app.enqueue(() -> { - for (VoxelChunk chunk : toAdd) { - long k = chunkKey(chunk.cx, chunk.cy, chunk.cz); - VoxelChunkNode oldNode = nodes.remove(k); - if (oldNode != null) oldNode.removeFromParent(); - chunks.put(k, chunk); - addNodeForChunk(k, chunk); - } - }); - - input.sculptRescanNeeded = true; - StringBuilder sb = new StringBuilder(); - if (restored > 0) { - sb.append("Bake rückgängig: ").append(restored).append(" Chunk") - .append(restored != 1 ? "s" : "").append("."); - } - if (noBackup > 0) { - if (sb.length() > 0) sb.append(" "); - sb.append(noBackup).append(" ohne Backup – unverändert."); - } - if (failed > 0) { - if (sb.length() > 0) sb.append(" "); - sb.append(failed).append(" fehlgeschlagen."); - } - if (sb.length() == 0) sb.append("Nichts wiederhergestellt."); - input.bakeStatusMsg = sb.toString(); - log.info("Markierter-Revert: {} erledigt, {} fehlgeschlagen.", restored, failed); - } - /** Bäckt einen einzelnen Chunk. Gibt true zurück wenn erfolgreich, false bei Fehler. */ private boolean bakeChunk(VoxelChunk original, Map blurredMap, float smoothStrength, float cliffNoiseStrength) { @@ -3428,11 +3329,6 @@ public class VoxelEditorState extends BaseAppState { brushIndicator.setCullHint(Spatial.CullHint.Always); return; } - // Im Selektierungs-Modus nur Mauszeiger, kein Brush-Indikator - if (input.voxelTool.mode.getSelectedIndex() == de.blight.editor.tool.VoxelTool.MODE_DELETE_BAKED) { - brushIndicator.setCullHint(Spatial.CullHint.Always); - return; - } float mx = input.mouseScreenX; float my = input.mouseScreenY; if (mx < 0) { @@ -3599,11 +3495,22 @@ public class VoxelEditorState extends BaseAppState { // ── Layer-Wechsel-Reaktion ───────────────────────────────────────────────── private void onVoxelLayerChanged(boolean entered) { - if (basePlane != null) + if (basePlane != null) { basePlane.setCullHint(entered ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); - if (chunkGrid != null) + } + if (chunkGrid != null) { chunkGrid.setCullHint(entered ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + } // Wireframe-Zustand beim Layer-Wechsel NICHT ändern — wird global via Ctrl+G gesteuert. + // Raw-Voxels: sichtbar im Voxel-Tab, versteckt außerhalb + if (voxelRoot != null) + voxelRoot.setCullHint(entered ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class); + if (entered) { + if (smes != null) smes.setBakedVisible(false); + return; + } + if (smes != null) smes.setBakedVisible(true); } /** Zeigt/verbirgt die Voxel-Chunk-Nodes (wird vom SculptedMeshEditorState gesteuert). */ diff --git a/blight-editor/src/main/java/de/blight/editor/tool/VoxelTool.java b/blight-editor/src/main/java/de/blight/editor/tool/VoxelTool.java index 672637e..d5e3692 100644 --- a/blight-editor/src/main/java/de/blight/editor/tool/VoxelTool.java +++ b/blight-editor/src/main/java/de/blight/editor/tool/VoxelTool.java @@ -21,17 +21,16 @@ import java.util.List; */ public class VoxelTool extends EditorTool { - public static final int MODE_SINUS = 0; - public static final int MODE_SPIKE = 1; - public static final int MODE_PLATEAU = 2; - public static final int MODE_SMOOTH = 3; - public static final int MODE_REMOVE = 4; - public static final int MODE_RESET = 5; - public static final int MODE_DELETE_BAKED = 6; + public static final int MODE_SINUS = 0; + public static final int MODE_SPIKE = 1; + public static final int MODE_PLATEAU = 2; + public static final int MODE_SMOOTH = 3; + public static final int MODE_REMOVE = 4; + public static final int MODE_RESET = 5; public final ChoiceToolParameter mode = new ChoiceToolParameter( "Modus", - new String[]{"Sinus", "Spike", "Plateau", "Smooth", "Aushöhlen", "Zurücksetzen", "Baked selektieren"}, + new String[]{"Sinus", "Spike", "Plateau", "Smooth", "Aushöhlen", "Zurücksetzen"}, MODE_SINUS, new String[]{ "img/editor/terraintool_sinus.png", @@ -39,7 +38,6 @@ public class VoxelTool extends EditorTool { "img/editor/terraintool_plateau.png", "img/editor/terraintool_smooth.png", null, - null, null } ); diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/terrain/TerrainPanelController.java b/blight-editor/src/main/java/de/blight/editor/toolset/terrain/TerrainPanelController.java index 2e6608d..d072e7e 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/terrain/TerrainPanelController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/terrain/TerrainPanelController.java @@ -336,22 +336,8 @@ public class TerrainPanelController { panel.getChildren().addAll(new Separator(), buildVoxelTextureSlotsUI()); } - // Backen-Button (nur beim VoxelTool) + // Bake-Parameter + Bereinigung (nur beim VoxelTool) if (tool instanceof de.blight.editor.tool.VoxelTool) { - Label bakeStatus = new Label(""); - bakeStatus.setWrapText(true); - bakeStatus.setStyle("-fx-text-fill: #555; -fx-font-size: 11;"); - - javafx.scene.control.ProgressBar bakeBar = new javafx.scene.control.ProgressBar(0); - bakeBar.setMaxWidth(Double.MAX_VALUE); - bakeBar.setVisible(false); - bakeBar.setManaged(false); - - Label bakeBarLabel = new Label(""); - bakeBarLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #444;"); - bakeBarLabel.setVisible(false); - bakeBarLabel.setManaged(false); - // ── Bake-Parameter ──────────────────────────────────────────────── Label smoothLbl = new Label(String.format("Glättung: %.1f", input.bakeSmoothStrength)); smoothLbl.setStyle("-fx-font-size: 11;"); @@ -373,107 +359,14 @@ public class TerrainPanelController { cliffLbl.setText(String.format("Klippen-Noise: %.1f", n.floatValue())); }); - Button bakeBtn = new Button("Voxels backen (J3O)"); - bakeBtn.setMaxWidth(Double.MAX_VALUE); - bakeBtn.setStyle("-fx-background-color: #4a7eba; -fx-text-fill: white;"); - bakeBtn.setOnAction(e -> { - input.bakeVoxelsRequested = true; - bakeStatus.setText(""); - bakeBtn.setDisable(true); - bakeBar.setProgress(0); - bakeBar.setVisible(true); - bakeBar.setManaged(true); - bakeBarLabel.setVisible(true); - bakeBarLabel.setManaged(true); - bakeBarLabel.setText("Starte..."); - }); - - javafx.animation.Timeline bakePoller = new javafx.animation.Timeline( - new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> { - int total = input.bakeTotal; - int done = input.bakeDone; - if (total > 0) { - bakeBar.setProgress((double) done / total); - bakeBarLabel.setText(done + " / " + total + " Chunks"); - } - String msg = input.bakeStatusMsg; - if (msg != null) { - input.bakeStatusMsg = null; - input.bakeTotal = 0; - input.bakeDone = 0; - bakeStatus.setText(msg); - bakeBar.setVisible(false); - bakeBar.setManaged(false); - bakeBarLabel.setVisible(false); - bakeBarLabel.setManaged(false); - bakeBtn.setDisable(false); - } - }) - ); - bakePoller.setCycleCount(javafx.animation.Animation.INDEFINITE); - bakePoller.play(); - - // ── Selektion: Löschen / Bake rückgängig (Modus "Baked selektieren") ── - Label selInfoLabel = new Label("Keine Auswahl"); - selInfoLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); - Label selHintLabel = new Label("LMK: Struktur wählen | RMK: Auswahl leeren"); - selHintLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); - selHintLabel.setWrapText(true); - - Button deleteBtn = new Button("Löschen"); - deleteBtn.setMaxWidth(Double.MAX_VALUE); - deleteBtn.setStyle("-fx-background-color: #ba4a4a; -fx-text-fill: white;"); - deleteBtn.setDisable(true); - deleteBtn.setOnAction(e -> { - int n = input.markedDeleteBakedKeys.size(); - javafx.scene.control.Alert confirm = new javafx.scene.control.Alert( - javafx.scene.control.Alert.AlertType.CONFIRMATION, - n + " Chunk" + (n != 1 ? "s" : "") + " unwiderruflich löschen?", - javafx.scene.control.ButtonType.YES, - javafx.scene.control.ButtonType.NO); - confirm.setTitle("Gebackene Chunks löschen"); - confirm.setHeaderText(null); - confirm.initOwner((javafx.stage.Stage) ctx.stageSupplier().get()); - confirm.showAndWait().ifPresent(btn -> { - if (btn == javafx.scene.control.ButtonType.YES) { - input.deleteMarkedBakedRequested = true; - } - }); - }); - - Button revertBtn = new Button("Bake rückgängig"); - revertBtn.setMaxWidth(Double.MAX_VALUE); - revertBtn.setStyle("-fx-background-color: #7a5a2a; -fx-text-fill: white;"); - revertBtn.setDisable(true); - revertBtn.setOnAction(e -> input.revertMarkedBakedRequested = true); - - javafx.animation.Timeline selPoller = new javafx.animation.Timeline( - new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> { - int n = input.markedDeleteBakedKeys.size(); - boolean has = n > 0; - selInfoLabel.setText(has - ? n + " Chunk" + (n != 1 ? "s" : "") + " ausgewählt" - : "Keine Auswahl"); - deleteBtn.setDisable(!has); - revertBtn.setDisable(!has); - }) - ); - selPoller.setCycleCount(javafx.animation.Animation.INDEFINITE); - selPoller.play(); - - // ── Unterirdische Voxel bereinigen ────────────────────────────────── + // ── Bereinigung ────────────────────────────────────────────────────── Label cleanupStatus = new Label(""); cleanupStatus.setWrapText(true); cleanupStatus.setStyle("-fx-text-fill: #555; -fx-font-size: 11;"); - Button cleanupBtn = new Button("Unterirdische Voxel bereinigen"); + Button cleanupBtn = new Button("Bereinigen..."); cleanupBtn.setMaxWidth(Double.MAX_VALUE); - cleanupBtn.setOnAction(e -> { - input.cleanupUndergroundStatus = null; - input.cleanupUndergroundRequested = true; - cleanupBtn.setDisable(true); - cleanupStatus.setText("Läuft..."); - }); + cleanupBtn.setOnAction(e -> showCleanupDialog(cleanupBtn, cleanupStatus)); javafx.animation.Timeline cleanupPoller = new javafx.animation.Timeline( new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> { @@ -490,9 +383,7 @@ public class TerrainPanelController { panel.getChildren().addAll(new Separator(), smoothLbl, smoothSlider, cliffLbl, cliffSlider, - bakeBtn, bakeBar, bakeBarLabel, bakeStatus, - new Separator(), cleanupBtn, cleanupStatus, - new Separator(), selInfoLabel, selHintLabel, deleteBtn, revertBtn); + new Separator(), cleanupBtn, cleanupStatus); } } @@ -1360,4 +1251,64 @@ public class TerrainPanelController { return String.format("%.1f MB", bytes / (1024.0 * 1024)); } + private void showCleanupDialog(Button triggerBtn, Label statusLbl) { + javafx.scene.control.CheckBox cbUnderground = new javafx.scene.control.CheckBox( + "Unterirdische Voxel-Chunks entfernen (cy < 0)"); + javafx.scene.control.CheckBox cbOrphaned = new javafx.scene.control.CheckBox( + "Verwaiste gebackene Meshes (.j3o ohne .blvc) löschen"); + javafx.scene.control.CheckBox cbRebakeAll = new javafx.scene.control.CheckBox( + "Alle Chunks als dirty markieren (Alles neu backen)"); + cbUnderground.setSelected(true); + cbOrphaned.setSelected(true); + + Label rebakeWarning = new Label( + "Achtung: Markiert alle geladenen Chunks dirty. Beim naechsten Tab-Wechsel\n" + + "wird alles neu gebacken – kann je nach Groesse der Karte sehr lange dauern."); + rebakeWarning.setStyle("-fx-text-fill: #b05000; -fx-font-size: 10;"); + rebakeWarning.setWrapText(true); + rebakeWarning.visibleProperty().bind(cbRebakeAll.selectedProperty()); + rebakeWarning.managedProperty().bind(cbRebakeAll.selectedProperty()); + + javafx.scene.layout.VBox content = new javafx.scene.layout.VBox(8, + cbUnderground, cbOrphaned, + new Separator(), + cbRebakeAll, rebakeWarning); + content.setPadding(new javafx.geometry.Insets(12)); + + javafx.scene.control.ButtonType okType = new javafx.scene.control.ButtonType("Ausführen", + javafx.scene.control.ButtonBar.ButtonData.OK_DONE); + javafx.scene.control.ButtonType cancelType = new javafx.scene.control.ButtonType("Abbrechen", + javafx.scene.control.ButtonBar.ButtonData.CANCEL_CLOSE); + javafx.scene.control.Dialog dialog = + new javafx.scene.control.Dialog<>(); + dialog.setTitle("Bereinigen"); + dialog.setHeaderText(null); + dialog.getDialogPane().setContent(content); + dialog.getDialogPane().getButtonTypes().addAll(okType, cancelType); + + // Owner setzen damit kein Java-Default-Icon im Taskbar + javafx.stage.Window owner = triggerBtn.getScene() != null ? triggerBtn.getScene().getWindow() : null; + if (owner instanceof javafx.stage.Stage s) dialog.initOwner(s); + + dialog.showAndWait().ifPresent(result -> { + if (result != okType) return; + boolean doUnderground = cbUnderground.isSelected(); + boolean doOrphaned = cbOrphaned.isSelected(); + boolean doRebakeAll = cbRebakeAll.isSelected(); + + if (doRebakeAll) { + input.markAllChunksDirtyRequested = true; + statusLbl.setText("Alle Chunks als dirty markiert."); + } + if (doUnderground || doOrphaned) { + input.cleanupUndergroundVoxels = doUnderground; + input.cleanupOrphanedBaked = doOrphaned; + input.cleanupUndergroundStatus = null; + input.cleanupUndergroundRequested = true; + triggerBtn.setDisable(true); + statusLbl.setText("Läuft..."); + } + }); + } + } diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/topbar/TopBarController.java b/blight-editor/src/main/java/de/blight/editor/toolset/topbar/TopBarController.java index df5747c..6599937 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/topbar/TopBarController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/topbar/TopBarController.java @@ -2,9 +2,15 @@ package de.blight.editor.toolset.topbar; import de.blight.editor.SharedInput; import de.blight.editor.ToolContext; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.scene.control.*; import javafx.scene.layout.*; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.util.Duration; public class TopBarController { @@ -32,6 +38,8 @@ public class TopBarController { ) {} private final SharedInput input; + private ToggleGroup layerGroup; + private Runnable pendingTabSwitchAction = null; // ToggleButtons – pre-created so getters work before buildTop() is called private final ToggleButton auswahlBtn = new ToggleButton("[k] Auswahl"); @@ -77,7 +85,7 @@ public class TopBarController { playToolBtn.setStyle(BOLD); // Mutually exclusive toggle group - ToggleGroup layerGroup = new ToggleGroup(); + layerGroup = new ToggleGroup(); auswahlBtn.setToggleGroup(layerGroup); baseBtn.setToggleGroup(layerGroup); voxelMergedBtn.setToggleGroup(layerGroup); @@ -262,52 +270,57 @@ public class TopBarController { auswahlBtn.setSelected(true); input.activeLayer = SharedInput.LAYER_AUSWAHL; - auswahlBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_AUSWAHL; + Stage ownerStage = (Stage) ctx.stageSupplier().get(); + + auswahlBtn.setOnAction(e -> guardVoxelExit(() -> { + input.activeLayer = SharedInput.LAYER_AUSWAHL; input.deselectAllOnAuswahl = true; ctx.setRightPanel().accept(deps.worldPanelController().buildAuswahlPanel()); - }); - baseBtn.setOnAction(e -> { + }, ownerStage)); + baseBtn.setOnAction(e -> guardVoxelExit(() -> { input.heightTool.brushRadius.setValue(input.voxelTool.brushRadius.getValue()); input.heightTool.brushStrength.setValue(input.voxelTool.brushStrength.getValue()); input.heightTool.plateauHeight.setValue(input.voxelTool.plateauTarget.getValue()); input.activeLayer = 0; input.activeTool = input.heightTool; ctx.setRightPanel().accept(deps.toolPanel()); deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool); - }); + }, ownerStage)); voxelMergedBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.voxelController().buildPanel())); - textureBtn.setOnAction(e -> { + textureBtn.setOnAction(e -> guardVoxelExit(() -> { input.activeLayer = 4; input.activeTool = input.textureTool; ctx.setRightPanel().accept(deps.toolPanel()); deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool); - }); - grassBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.grassController().buildPanel())); - stoneBtn.setOnAction(e -> { + }, ownerStage)); + grassBtn.setOnAction(e -> guardVoxelExit( + () -> ctx.setRightPanel().accept(deps.grassController().buildPanel()), ownerStage)); + stoneBtn.setOnAction(e -> guardVoxelExit(() -> { input.activeLayer = SharedInput.LAYER_STONE; input.activeTool = input.stoneTool; ctx.setRightPanel().accept(deps.toolPanel()); deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool); - }); - objPlaceBtn.setOnAction(e -> { + }, ownerStage)); + objPlaceBtn.setOnAction(e -> guardVoxelExit(() -> { deps.assetController().expandModelsNode(); deps.assetController().openOverlay(); deps.objectController().activatePlaceMode(); - }); - atmosphaereBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.atmosphereController().buildPanel())); - wasserMergedBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.waterController().buildPanel())); - wegeBtn.setOnAction(e -> { + }, ownerStage)); + atmosphaereBtn.setOnAction(e -> guardVoxelExit( + () -> ctx.setRightPanel().accept(deps.atmosphereController().buildPanel()), ownerStage)); + wasserMergedBtn.setOnAction(e -> guardVoxelExit( + () -> ctx.setRightPanel().accept(deps.waterController().buildPanel()), ownerStage)); + wegeBtn.setOnAction(e -> guardVoxelExit(() -> { input.activeLayer = SharedInput.LAYER_PATH_NETWORK; ctx.setRightPanel().accept(deps.worldPanelController().buildWegePanel()); - }); - zonenBtn.setOnAction(e -> { + }, ownerStage)); + zonenBtn.setOnAction(e -> guardVoxelExit(() -> { input.activeLayer = SharedInput.LAYER_ZONEN; ctx.setRightPanel().accept(deps.worldPanelController().buildZonenPanel()); - }); - playToolBtn.setOnAction(e -> { + }, ownerStage)); + playToolBtn.setOnAction(e -> guardVoxelExit(() -> { input.activeLayer = SharedInput.LAYER_PLAY_TOOL; input.playToolMode = SharedInput.PlayToolMode.EDIT; ctx.setRightPanel().accept(deps.gameController().buildPlayToolPanel()); - }); + }, ownerStage)); worldHintLabel = new Label(camHintText()); worldHintLabel.setStyle("-fx-text-fill: #555;"); @@ -332,4 +345,69 @@ public class TopBarController { worldToolBar = toolBar; return new VBox(menuBar, toolBar); } + + // ── Voxel-Exit-Guard ───────────────────────────────────────────────────── + + private void guardVoxelExit(Runnable action, Stage ownerStage) { + if (input.activeLayer != SharedInput.LAYER_VOXEL || input.sessionDirtyVoxelCount == 0) { + action.run(); + return; + } + voxelMergedBtn.setSelected(true); + pendingTabSwitchAction = action; + showVoxelBakeDialog(ownerStage); + } + + private void showVoxelBakeDialog(Stage ownerStage) { + Stage dialog = new Stage(); + dialog.initOwner(ownerStage); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setTitle("Voxel backen..."); + dialog.setResizable(false); + + Label statusLabel = new Label("Backe geänderte Chunks..."); + ProgressBar bar = new ProgressBar(0); + bar.setMaxWidth(Double.MAX_VALUE); + + Button cancelBtn = new Button("Abbrechen"); + cancelBtn.setMaxWidth(Double.MAX_VALUE); + cancelBtn.setOnAction(e -> input.bakeCancelRequested = true); + + VBox vbox = new VBox(10, statusLabel, bar, cancelBtn); + vbox.setPadding(new Insets(16)); + vbox.setMinWidth(320); + + dialog.setScene(new javafx.scene.Scene(vbox)); + + input.bakeOnExitRequested = true; + input.bakeOnExitDone = false; + input.bakeOnExitSuccess = false; + + Timeline poller = new Timeline(new KeyFrame(Duration.millis(100), ev -> { + int total = input.bakeTotal; + int done = input.bakeDone; + if (total > 0) { + bar.setProgress((double) done / total); + statusLabel.setText("Backe... " + done + " / " + total); + } + if (input.bakeOnExitDone) { + input.bakeOnExitDone = false; + dialog.close(); + if (input.bakeOnExitSuccess) { + if (pendingTabSwitchAction != null) { + pendingTabSwitchAction.run(); + pendingTabSwitchAction = null; + } + } else { + pendingTabSwitchAction = null; + voxelMergedBtn.setSelected(true); + } + } + })); + poller.setCycleCount(javafx.animation.Animation.INDEFINITE); + + dialog.setOnShown(e -> poller.play()); + dialog.setOnHidden(e -> poller.stop()); + dialog.show(); + } } diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index 0f7e2df..33ba29d 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_15.blvc b/blight-map/src/main/map/chunks/voxel_16_0_15.blvc.prebake similarity index 100% rename from blight-map/src/main/map/chunks/voxel_16_0_15.blvc rename to blight-map/src/main/map/chunks/voxel_16_0_15.blvc.prebake diff --git a/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod0.j3o new file mode 100644 index 0000000..a6c9e91 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod1.j3o new file mode 100644 index 0000000..88edf89 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_15_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_16.blvc b/blight-map/src/main/map/chunks/voxel_16_0_16.blvc.prebake similarity index 100% rename from blight-map/src/main/map/chunks/voxel_16_0_16.blvc rename to blight-map/src/main/map/chunks/voxel_16_0_16.blvc.prebake diff --git a/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod0.j3o new file mode 100644 index 0000000..349aa51 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod1.j3o new file mode 100644 index 0000000..bb7f429 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod2.j3o new file mode 100644 index 0000000..d2beff9 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_15.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_15.blvc.prebake similarity index 100% rename from blight-map/src/main/map/chunks/voxel_16_m1_15.blvc rename to blight-map/src/main/map/chunks/voxel_16_m1_15.blvc.prebake diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_15_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_15_baked_lod0.j3o new file mode 100644 index 0000000..e13b822 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_15_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_15_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_15_baked_lod1.j3o new file mode 100644 index 0000000..cd6964a Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_15_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_16.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_16.blvc.prebake similarity index 100% rename from blight-map/src/main/map/chunks/voxel_16_m1_16.blvc rename to blight-map/src/main/map/chunks/voxel_16_m1_16.blvc.prebake diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod0.j3o new file mode 100644 index 0000000..8a3a624 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod1.j3o new file mode 100644 index 0000000..c590b47 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod2.j3o new file mode 100644 index 0000000..4be4453 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_16_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_07.blvc b/blight-map/src/main/map/chunks/voxel_17_0_15.blvc similarity index 92% rename from blight-map/src/main/map/chunks/voxel_17_m1_07.blvc rename to blight-map/src/main/map/chunks/voxel_17_0_15.blvc index b9df499..8567777 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_07.blvc and b/blight-map/src/main/map/chunks/voxel_17_0_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_08.blvc b/blight-map/src/main/map/chunks/voxel_17_0_15.blvc.prebake similarity index 92% rename from blight-map/src/main/map/chunks/voxel_17_m1_08.blvc rename to blight-map/src/main/map/chunks/voxel_17_0_15.blvc.prebake index b044a23..8567777 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_08.blvc and b/blight-map/src/main/map/chunks/voxel_17_0_15.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod0.j3o new file mode 100644 index 0000000..d01ce3a Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod1.j3o new file mode 100644 index 0000000..d3aa93d Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_15_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_16.blvc b/blight-map/src/main/map/chunks/voxel_17_0_16.blvc index f7b4550..5121730 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_16.blvc and b/blight-map/src/main/map/chunks/voxel_17_0_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_16.blvc.prebake b/blight-map/src/main/map/chunks/voxel_17_0_16.blvc.prebake new file mode 100644 index 0000000..5121730 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_16.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod0.j3o new file mode 100644 index 0000000..62fa98f Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod1.j3o new file mode 100644 index 0000000..dc2e38a Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod2.j3o new file mode 100644 index 0000000..c422526 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_16_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_16.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_15.blvc.prebake similarity index 67% rename from blight-map/src/main/map/chunks/voxel_17_m1_16.blvc rename to blight-map/src/main/map/chunks/voxel_17_m1_15.blvc.prebake index c436a1d..4badf4b 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_16.blvc and b/blight-map/src/main/map/chunks/voxel_17_m1_15.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_15_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_15_baked_lod0.j3o new file mode 100644 index 0000000..7021022 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_15_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_19_m1_10.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_16.blvc.prebake similarity index 61% rename from blight-map/src/main/map/chunks/voxel_19_m1_10.blvc rename to blight-map/src/main/map/chunks/voxel_17_m1_16.blvc.prebake index 8e79674..100f016 100644 Binary files a/blight-map/src/main/map/chunks/voxel_19_m1_10.blvc and b/blight-map/src/main/map/chunks/voxel_17_m1_16.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod0.j3o new file mode 100644 index 0000000..eabe765 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod1.j3o new file mode 100644 index 0000000..54d009c Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod2.j3o new file mode 100644 index 0000000..d8bf366 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_16_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_19_m1_09.blvc b/blight-map/src/main/map/chunks/voxel_19_m1_09.blvc deleted file mode 100644 index db74274..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_19_m1_09.blvc and /dev/null differ