voxel umgestellt

This commit is contained in:
2026-10-04 17:05:18 +02:00
parent f88a710b4d
commit 4ed61998dd
38 changed files with 335 additions and 444 deletions

View File

@@ -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<Long> 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<Long> 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;

View File

@@ -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<Long> 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<Long> current = new HashSet<>(input.markedDeleteBakedKeys);
// Wireframe von nicht mehr selektierten Chunks entfernen
Iterator<Long> 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<Map.Entry<Long, EditableMesh>> 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;

View File

@@ -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;
}
}
}

View File

@@ -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<Long> dirtyChunksThisFrame = new HashSet<>();
/** Session-weite dirty Chunks (für Bake-on-Exit). Nur im JME-Thread modifizieren. */
private final Set<Long> 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<Long> 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<VoxelChunk> 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<Long> 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<long[]> toRemove = new ArrayList<>();
for (Map.Entry<Long, VoxelChunk> 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<String> 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<VoxelChunk> 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<Long> visited = new HashSet<>();
Queue<long[]> 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<Long> 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<Long> keys) {
if (keys.isEmpty()) {
input.bakeStatusMsg = "Keine Chunks markiert.";
return;
}
int restored = 0, noBackup = 0, failed = 0;
List<VoxelChunk> 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<VoxelChunk> 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<Long, VoxelChunk> 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). */

View File

@@ -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
}
);

View File

@@ -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<javafx.scene.control.ButtonType> 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...");
}
});
}
}

View File

@@ -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();
}
}