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; public volatile int bakeTotal = 0;
/** JME → JFX: Status-Meldung nach Abschluss des Backens oder Löschens. */ /** JME → JFX: Status-Meldung nach Abschluss des Backens oder Löschens. */
public volatile String bakeStatusMsg = null; 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). */ /** JFX → JME: Höhen-Overlay ein/aus (Terrain / Voxel / Baked im 100m-Radius). */
public volatile boolean debugHeightOverlayEnabled = false; public volatile boolean debugHeightOverlayEnabled = false;
@@ -937,21 +947,19 @@ public class SharedInput {
/** JFX → JME: 1m-Gitternetz auf Terrain/Voxel ein/aus. */ /** JFX → JME: 1m-Gitternetz auf Terrain/Voxel ein/aus. */
public volatile boolean showGrid = true; public volatile boolean showGrid = true;
/** /** JME → JME: Keys von Chunks die nach einem Bake neu geladen werden müssen. */
* Per BFS selektierte gebackene Chunks (chunkKey-kodiert). public final java.util.concurrent.ConcurrentLinkedQueue<Long> sculptInvalidateKeys
* Thread-sicher; JME schreibt, JFX liest (nur size() für Anzeige). = new java.util.concurrent.ConcurrentLinkedQueue<>();
*/
public final java.util.Set<Long> markedDeleteBakedKeys = /** JFX → JME: Bereinigung starten (Flags unten bestimmen was). */
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; public volatile boolean cleanupUndergroundRequested = false;
/** JME → JFX: Ergebnis der Unterirdisch-Bereinigung (null = läuft noch / nicht gestartet). */ /** 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; public volatile String cleanupUndergroundStatus = null;
/** Terrain-Slot (0-7) für flache Voxel-Flächen, -1 = kein Slot. */ /** Terrain-Slot (0-7) für flache Voxel-Flächen, -1 = kein Slot. */

View File

@@ -71,8 +71,6 @@ public class SculptedMeshEditorState extends BaseAppState {
private Material bakedMat = null; private Material bakedMat = null;
private boolean gridVisible = true; private boolean gridVisible = true;
private Material highlightMat = null; private Material highlightMat = null;
private Material selectionWireframeMat = null;
private final Set<Long> appliedSelectionKeys = new HashSet<>();
// ── innere Klasse ───────────────────────────────────────────────────────── // ── innere Klasse ─────────────────────────────────────────────────────────
@@ -125,10 +123,6 @@ public class SculptedMeshEditorState extends BaseAppState {
highlightMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); highlightMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
highlightMat.setColor("Color", new ColorRGBA(1f, 0.65f, 0f, 1f)); 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(); brushIndicator = buildBrushIndicator();
app.getRootNode().attachChild(brushIndicator); app.getRootNode().attachChild(brushIndicator);
@@ -138,45 +132,11 @@ public class SculptedMeshEditorState extends BaseAppState {
@Override @Override
protected void cleanup(Application application) { protected void cleanup(Application application) {
saveAllDirty(); 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(); sculptRoot.removeFromParent();
if (brushIndicator != null) brushIndicator.removeFromParent(); if (brushIndicator != null) brushIndicator.removeFromParent();
meshes.clear(); 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 onEnable() {}
@Override protected void onDisable() {} @Override protected void onDisable() {}
@@ -195,12 +155,6 @@ public class SculptedMeshEditorState extends BaseAppState {
if (input.sculptRescanNeeded) { if (input.sculptRescanNeeded) {
input.sculptRescanNeeded = false; input.sculptRescanNeeded = false;
rescanBakedChunks(); rescanBakedChunks();
input.markedBakedSelectionDirty = true;
}
if (input.markedBakedSelectionDirty) {
input.markedBakedSelectionDirty = false;
updateSelectionWireframe();
} }
updateBrushIndicator(); updateBrushIndicator();
@@ -262,6 +216,12 @@ public class SculptedMeshEditorState extends BaseAppState {
// ── Scan / Laden ────────────────────────────────────────────────────────── // ── Scan / Laden ──────────────────────────────────────────────────────────
private void rescanBakedChunks() { 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 // Geladene Meshes entfernen, deren Baked-Dateien nicht mehr existieren
Iterator<Map.Entry<Long, EditableMesh>> it = meshes.entrySet().iterator(); Iterator<Map.Entry<Long, EditableMesh>> it = meshes.entrySet().iterator();
while (it.hasNext()) { while (it.hasNext()) {
@@ -862,6 +822,13 @@ public class SculptedMeshEditorState extends BaseAppState {
return ((long)(cx & 0xFFFF)) | (((long)(cy & 0xFFFF)) << 16) | (((long)(cz & 0xFFFF)) << 32); 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. */ /** 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) { public com.jme3.math.Vector3f raycastGeometry(com.jme3.math.Ray ray) {
if (sculptRoot == null) return null; if (sculptRoot == null) return null;

View File

@@ -1389,7 +1389,6 @@ public class TerrainEditorState extends BaseAppState {
private void processTextureEdits() { private void processTextureEdits() {
SharedInput.TextureEdit edit; SharedInput.TextureEdit edit;
int processed = 0; int processed = 0;
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
while ((edit = input.textureEditQueue.poll()) != null && processed < MAX_EDITS_PER_FRAME) { while ((edit = input.textureEditQueue.poll()) != null && processed < MAX_EDITS_PER_FRAME) {
processed++; processed++;
float jmeX = (float)(edit.screenX() * input.viewportScaleX); float jmeX = (float)(edit.screenX() * input.viewportScaleX);
@@ -1402,7 +1401,6 @@ public class TerrainEditorState extends BaseAppState {
SurfaceHit hit = raycastSurfaceWithType(ray); SurfaceHit hit = raycastSurfaceWithType(ray);
if (hit == null) continue; if (hit == null) continue;
Vector3f contact = hit.point; 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(); int selIdx = input.textureTool.textureIndex.getSelectedIndex();
float str = (float) input.textureTool.brushStrength.getValue(); float str = (float) input.textureTool.brushStrength.getValue();
@@ -1763,37 +1761,21 @@ public class TerrainEditorState extends BaseAppState {
brushRadius = (layer == 0) brushRadius = (layer == 0)
? (float) input.heightTool.brushRadius.getValue() ? (float) input.heightTool.brushRadius.getValue()
: (float) input.textureTool.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) { } else if (layer == 3) {
contactPoint = raycastSurface(ray); contactPoint = raycastSurface(ray);
if (contactPoint != null) { if (contactPoint != null) {
brushRadius = (float) input.grassTool.brushRadius.getValue(); 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) { } else if (layer == SharedInput.LAYER_GRASS_VERTEX) {
contactPoint = raycastSurface(ray); contactPoint = raycastSurface(ray);
if (contactPoint != null) { if (contactPoint != null) {
brushRadius = (float) input.grassVertexTool.brushRadius.getValue(); 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) { } else if (layer == SharedInput.LAYER_TERRAIN_GRASS) {
contactPoint = raycastSurface(ray); contactPoint = raycastSurface(ray);
if (contactPoint != null) { if (contactPoint != null) {
brushRadius = (float) input.terrainGrassTool.brushRadius.getValue(); 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.Texture2D;
import com.jme3.texture.image.ColorSpace; import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import de.blight.common.SculptedMeshIO;
import de.blight.common.VoxelChunk; import de.blight.common.VoxelChunk;
import de.blight.common.VoxelChunkIO; import de.blight.common.VoxelChunkIO;
import de.blight.editor.SharedInput; import de.blight.editor.SharedInput;
@@ -104,6 +105,8 @@ public class VoxelEditorState extends BaseAppState {
* nicht mehrfach pro Edit-Event. * nicht mehrfach pro Edit-Event.
*/ */
private final Set<Long> dirtyChunksThisFrame = new HashSet<>(); 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 ─────────────────────────────────────────────────────────────── // ── Timers ───────────────────────────────────────────────────────────────
@@ -212,6 +215,7 @@ public class VoxelEditorState extends BaseAppState {
this.cam = app.getCamera(); this.cam = app.getCamera();
voxelRoot = new Node("voxelEditorRoot"); voxelRoot = new Node("voxelEditorRoot");
voxelRoot.setCullHint(Spatial.CullHint.Always); // nur im Voxel-Tab sichtbar
app.getRootNode().attachChild(voxelRoot); app.getRootNode().attachChild(voxelRoot);
// TerrainEditorState wurde vor uns attached und ist zu diesem Zeitpunkt initialisiert // TerrainEditorState wurde vor uns attached und ist zu diesem Zeitpunkt initialisiert
@@ -323,35 +327,112 @@ public class VoxelEditorState extends BaseAppState {
}); });
} }
// Selektierte Baked-Chunks löschen // Bake-on-Exit: nur dirty Chunks backen
if (input.deleteMarkedBakedRequested) { if (input.bakeOnExitRequested) {
input.deleteMarkedBakedRequested = false; input.bakeOnExitRequested = false;
Set<Long> toDelete = new HashSet<>(input.markedDeleteBakedKeys); input.bakeCancelRequested = false;
input.markedDeleteBakedKeys.clear(); List<VoxelChunk> toBake = new ArrayList<>();
input.markedBakedSelectionDirty = true; for (long key : sessionDirtyChunks) {
VoxelChunk c = chunks.get(key);
if (c != null) toBake.add(c);
}
executor.submit(() -> { executor.submit(() -> {
try { 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) { } 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.bakeStatusMsg = "FEHLER: " + e.getMessage();
input.bakeOnExitSuccess = false;
} finally {
input.bakeOnExitDone = true;
} }
}); });
} }
// Selektierte Chunks aus Pre-Bake-Backup wiederherstellen // Bereinigung
if (input.revertMarkedBakedRequested) { if (input.cleanupUndergroundRequested) {
input.revertMarkedBakedRequested = false; input.cleanupUndergroundRequested = false;
Set<Long> toRevert = new HashSet<>(input.markedDeleteBakedKeys); final boolean doUnderground = input.cleanupUndergroundVoxels;
input.markedDeleteBakedKeys.clear(); final boolean doOrphaned = input.cleanupOrphanedBaked;
input.markedBakedSelectionDirty = true;
executor.submit(() -> { executor.submit(() -> {
int removedVoxel = 0;
int removedBaked = 0;
try { try {
revertMarkedBaked(toRevert); if (doUnderground) {
} catch (Throwable e) { // Unterirdische Chunks (cy < 0): .blvc löschen, aus Memory entfernen
log.error("Markierter-Revert fehlgeschlagen: {}", e.getMessage(), e); List<long[]> toRemove = new ArrayList<>();
input.bakeStatusMsg = "Revert FEHLER: " + e.getMessage(); 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();
});
}); });
} }
@@ -501,6 +582,8 @@ public class VoxelEditorState extends BaseAppState {
addNodeForChunk(key, chunk); addNodeForChunk(key, chunk);
} }
dirtyChunksThisFrame.add(key); dirtyChunksThisFrame.add(key);
sessionDirtyChunks.add(key);
input.sessionDirtyVoxelCount = sessionDirtyChunks.size();
} }
} }
@@ -561,6 +644,8 @@ public class VoxelEditorState extends BaseAppState {
addNodeForChunk(key, chunk); addNodeForChunk(key, chunk);
} }
dirtyChunksThisFrame.add(key); 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(); int modeIdx = input.voxelTool.mode.getSelectedIndex();
boolean isHorizontal = input.voxelTool.horizontal; 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 isCave = (modeIdx == de.blight.editor.tool.VoxelTool.MODE_REMOVE);
boolean isColumn = !isCave; boolean isColumn = !isCave;
boolean lower = action < 0; boolean lower = action < 0;
@@ -865,6 +938,8 @@ public class VoxelEditorState extends BaseAppState {
addNodeForChunk(key, chunk); addNodeForChunk(key, chunk);
} }
dirtyChunksThisFrame.add(key); 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); long key = chunkKey(chunk.cx, chunk.cy, chunk.cz);
if (!chunk.isEmpty() && !nodes.containsKey(key)) addNodeForChunk(key, chunk); if (!chunk.isEmpty() && !nodes.containsKey(key)) addNodeForChunk(key, chunk);
dirtyChunksThisFrame.add(key); 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) ───────────────────────────────── // ── Terrain-Höhe (schneller O(1)-Zugriff) ─────────────────────────────────
private float terrainH(float worldX, float worldZ) { private float terrainH(float worldX, float worldZ) {
@@ -2746,6 +2734,9 @@ public class VoxelEditorState extends BaseAppState {
float bakeSmoothStr = input.bakeSmoothStrength; float bakeSmoothStr = input.bakeSmoothStrength;
float bakeCliffStr = input.bakeCliffNoiseStrength; float bakeCliffStr = input.bakeCliffNoiseStrength;
for (VoxelChunk chunk : nonEmpty) { for (VoxelChunk chunk : nonEmpty) {
if (input.bakeCancelRequested) {
break;
}
if (bakeChunk(chunk, blurredMap, bakeSmoothStr, bakeCliffStr)) { if (bakeChunk(chunk, blurredMap, bakeSmoothStr, bakeCliffStr)) {
successfullyBaked.add(chunk); successfullyBaked.add(chunk);
} else { } else {
@@ -2756,111 +2747,21 @@ public class VoxelEditorState extends BaseAppState {
input.bakeDone = baked; input.bakeDone = baked;
} }
// Voxel-Daten löschen: nur für erfolgreich gebackene Chunks // Voxeldaten (.blvc) bleiben erhalten: nach dem Baken sind Raw-Voxels weiterhin
for (VoxelChunk chunk : successfullyBaked) { // im Voxel-Tab sichtbar und editierbar. .blvc und .j3o koexistieren.
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);
}
});
int failed = nonEmpty.size() - successfullyBaked.size(); int failed = nonEmpty.size() - successfullyBaked.size();
String msg = "Fertig: " + successfullyBaked.size() + " Chunk" String msg = "Fertig: " + successfullyBaked.size() + " Chunk"
+ (successfullyBaked.size() != 1 ? "s" : "") + " gebacken" + (successfullyBaked.size() != 1 ? "s" : "") + " gebacken"
+ (failed > 0 ? ", " + failed + " fehlgeschlagen (Voxel erhalten)" : "") + "."; + (failed > 0 ? ", " + failed + " fehlgeschlagen (Voxel erhalten)" : "") + ".";
// bakeTotal/bakeDone werden vom UI-Thread nach Empfang der Statusmeldung zurückgesetzt // 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.bakeStatusMsg = msg;
input.sculptRescanNeeded = true; input.sculptRescanNeeded = true;
log.info("Voxel-Bake abgeschlossen – {}.", msg); 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. */ /** Bäckt einen einzelnen Chunk. Gibt true zurück wenn erfolgreich, false bei Fehler. */
private boolean bakeChunk(VoxelChunk original, Map<Long, VoxelChunk> blurredMap, private boolean bakeChunk(VoxelChunk original, Map<Long, VoxelChunk> blurredMap,
float smoothStrength, float cliffNoiseStrength) { float smoothStrength, float cliffNoiseStrength) {
@@ -3428,11 +3329,6 @@ public class VoxelEditorState extends BaseAppState {
brushIndicator.setCullHint(Spatial.CullHint.Always); brushIndicator.setCullHint(Spatial.CullHint.Always);
return; 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 mx = input.mouseScreenX;
float my = input.mouseScreenY; float my = input.mouseScreenY;
if (mx < 0) { if (mx < 0) {
@@ -3599,11 +3495,22 @@ public class VoxelEditorState extends BaseAppState {
// ── Layer-Wechsel-Reaktion ───────────────────────────────────────────────── // ── Layer-Wechsel-Reaktion ─────────────────────────────────────────────────
private void onVoxelLayerChanged(boolean entered) { private void onVoxelLayerChanged(boolean entered) {
if (basePlane != null) if (basePlane != null) {
basePlane.setCullHint(entered ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); basePlane.setCullHint(entered ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
if (chunkGrid != null) }
if (chunkGrid != null) {
chunkGrid.setCullHint(entered ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); chunkGrid.setCullHint(entered ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
}
// Wireframe-Zustand beim Layer-Wechsel NICHT ändern — wird global via Ctrl+G gesteuert. // 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). */ /** Zeigt/verbirgt die Voxel-Chunk-Nodes (wird vom SculptedMeshEditorState gesteuert). */

View File

@@ -27,11 +27,10 @@ public class VoxelTool extends EditorTool {
public static final int MODE_SMOOTH = 3; public static final int MODE_SMOOTH = 3;
public static final int MODE_REMOVE = 4; public static final int MODE_REMOVE = 4;
public static final int MODE_RESET = 5; public static final int MODE_RESET = 5;
public static final int MODE_DELETE_BAKED = 6;
public final ChoiceToolParameter mode = new ChoiceToolParameter( public final ChoiceToolParameter mode = new ChoiceToolParameter(
"Modus", "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, MODE_SINUS,
new String[]{ new String[]{
"img/editor/terraintool_sinus.png", "img/editor/terraintool_sinus.png",
@@ -39,7 +38,6 @@ public class VoxelTool extends EditorTool {
"img/editor/terraintool_plateau.png", "img/editor/terraintool_plateau.png",
"img/editor/terraintool_smooth.png", "img/editor/terraintool_smooth.png",
null, null,
null,
null null
} }
); );

View File

@@ -336,22 +336,8 @@ public class TerrainPanelController {
panel.getChildren().addAll(new Separator(), buildVoxelTextureSlotsUI()); 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) { 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 ──────────────────────────────────────────────── // ── Bake-Parameter ────────────────────────────────────────────────
Label smoothLbl = new Label(String.format("Glättung: %.1f", input.bakeSmoothStrength)); Label smoothLbl = new Label(String.format("Glättung: %.1f", input.bakeSmoothStrength));
smoothLbl.setStyle("-fx-font-size: 11;"); smoothLbl.setStyle("-fx-font-size: 11;");
@@ -373,107 +359,14 @@ public class TerrainPanelController {
cliffLbl.setText(String.format("Klippen-Noise: %.1f", n.floatValue())); cliffLbl.setText(String.format("Klippen-Noise: %.1f", n.floatValue()));
}); });
Button bakeBtn = new Button("Voxels backen (J3O)"); // ── Bereinigung ──────────────────────────────────────────────────────
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 ──────────────────────────────────
Label cleanupStatus = new Label(""); Label cleanupStatus = new Label("");
cleanupStatus.setWrapText(true); cleanupStatus.setWrapText(true);
cleanupStatus.setStyle("-fx-text-fill: #555; -fx-font-size: 11;"); 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.setMaxWidth(Double.MAX_VALUE);
cleanupBtn.setOnAction(e -> { cleanupBtn.setOnAction(e -> showCleanupDialog(cleanupBtn, cleanupStatus));
input.cleanupUndergroundStatus = null;
input.cleanupUndergroundRequested = true;
cleanupBtn.setDisable(true);
cleanupStatus.setText("Läuft...");
});
javafx.animation.Timeline cleanupPoller = new javafx.animation.Timeline( javafx.animation.Timeline cleanupPoller = new javafx.animation.Timeline(
new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> { new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> {
@@ -490,9 +383,7 @@ public class TerrainPanelController {
panel.getChildren().addAll(new Separator(), panel.getChildren().addAll(new Separator(),
smoothLbl, smoothSlider, cliffLbl, cliffSlider, smoothLbl, smoothSlider, cliffLbl, cliffSlider,
bakeBtn, bakeBar, bakeBarLabel, bakeStatus, new Separator(), cleanupBtn, cleanupStatus);
new Separator(), cleanupBtn, cleanupStatus,
new Separator(), selInfoLabel, selHintLabel, deleteBtn, revertBtn);
} }
} }
@@ -1360,4 +1251,64 @@ public class TerrainPanelController {
return String.format("%.1f MB", bytes / (1024.0 * 1024)); 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.SharedInput;
import de.blight.editor.ToolContext; import de.blight.editor.ToolContext;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.geometry.Insets;
import javafx.geometry.Orientation; import javafx.geometry.Orientation;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TopBarController { public class TopBarController {
@@ -32,6 +38,8 @@ public class TopBarController {
) {} ) {}
private final SharedInput input; private final SharedInput input;
private ToggleGroup layerGroup;
private Runnable pendingTabSwitchAction = null;
// ToggleButtons – pre-created so getters work before buildTop() is called // ToggleButtons – pre-created so getters work before buildTop() is called
private final ToggleButton auswahlBtn = new ToggleButton("[k] Auswahl"); private final ToggleButton auswahlBtn = new ToggleButton("[k] Auswahl");
@@ -77,7 +85,7 @@ public class TopBarController {
playToolBtn.setStyle(BOLD); playToolBtn.setStyle(BOLD);
// Mutually exclusive toggle group // Mutually exclusive toggle group
ToggleGroup layerGroup = new ToggleGroup(); layerGroup = new ToggleGroup();
auswahlBtn.setToggleGroup(layerGroup); auswahlBtn.setToggleGroup(layerGroup);
baseBtn.setToggleGroup(layerGroup); baseBtn.setToggleGroup(layerGroup);
voxelMergedBtn.setToggleGroup(layerGroup); voxelMergedBtn.setToggleGroup(layerGroup);
@@ -262,52 +270,57 @@ public class TopBarController {
auswahlBtn.setSelected(true); auswahlBtn.setSelected(true);
input.activeLayer = SharedInput.LAYER_AUSWAHL; input.activeLayer = SharedInput.LAYER_AUSWAHL;
auswahlBtn.setOnAction(e -> { Stage ownerStage = (Stage) ctx.stageSupplier().get();
auswahlBtn.setOnAction(e -> guardVoxelExit(() -> {
input.activeLayer = SharedInput.LAYER_AUSWAHL; input.activeLayer = SharedInput.LAYER_AUSWAHL;
input.deselectAllOnAuswahl = true; input.deselectAllOnAuswahl = true;
ctx.setRightPanel().accept(deps.worldPanelController().buildAuswahlPanel()); ctx.setRightPanel().accept(deps.worldPanelController().buildAuswahlPanel());
}); }, ownerStage));
baseBtn.setOnAction(e -> { baseBtn.setOnAction(e -> guardVoxelExit(() -> {
input.heightTool.brushRadius.setValue(input.voxelTool.brushRadius.getValue()); input.heightTool.brushRadius.setValue(input.voxelTool.brushRadius.getValue());
input.heightTool.brushStrength.setValue(input.voxelTool.brushStrength.getValue()); input.heightTool.brushStrength.setValue(input.voxelTool.brushStrength.getValue());
input.heightTool.plateauHeight.setValue(input.voxelTool.plateauTarget.getValue()); input.heightTool.plateauHeight.setValue(input.voxelTool.plateauTarget.getValue());
input.activeLayer = 0; input.activeTool = input.heightTool; input.activeLayer = 0; input.activeTool = input.heightTool;
ctx.setRightPanel().accept(deps.toolPanel()); ctx.setRightPanel().accept(deps.toolPanel());
deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool); deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool);
}); }, ownerStage));
voxelMergedBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.voxelController().buildPanel())); voxelMergedBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.voxelController().buildPanel()));
textureBtn.setOnAction(e -> { textureBtn.setOnAction(e -> guardVoxelExit(() -> {
input.activeLayer = 4; input.activeTool = input.textureTool; input.activeLayer = 4; input.activeTool = input.textureTool;
ctx.setRightPanel().accept(deps.toolPanel()); ctx.setRightPanel().accept(deps.toolPanel());
deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool); deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool);
}); }, ownerStage));
grassBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.grassController().buildPanel())); grassBtn.setOnAction(e -> guardVoxelExit(
stoneBtn.setOnAction(e -> { () -> ctx.setRightPanel().accept(deps.grassController().buildPanel()), ownerStage));
stoneBtn.setOnAction(e -> guardVoxelExit(() -> {
input.activeLayer = SharedInput.LAYER_STONE; input.activeLayer = SharedInput.LAYER_STONE;
input.activeTool = input.stoneTool; input.activeTool = input.stoneTool;
ctx.setRightPanel().accept(deps.toolPanel()); ctx.setRightPanel().accept(deps.toolPanel());
deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool); deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool);
}); }, ownerStage));
objPlaceBtn.setOnAction(e -> { objPlaceBtn.setOnAction(e -> guardVoxelExit(() -> {
deps.assetController().expandModelsNode(); deps.assetController().expandModelsNode();
deps.assetController().openOverlay(); deps.assetController().openOverlay();
deps.objectController().activatePlaceMode(); deps.objectController().activatePlaceMode();
}); }, ownerStage));
atmosphaereBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.atmosphereController().buildPanel())); atmosphaereBtn.setOnAction(e -> guardVoxelExit(
wasserMergedBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.waterController().buildPanel())); () -> ctx.setRightPanel().accept(deps.atmosphereController().buildPanel()), ownerStage));
wegeBtn.setOnAction(e -> { wasserMergedBtn.setOnAction(e -> guardVoxelExit(
() -> ctx.setRightPanel().accept(deps.waterController().buildPanel()), ownerStage));
wegeBtn.setOnAction(e -> guardVoxelExit(() -> {
input.activeLayer = SharedInput.LAYER_PATH_NETWORK; input.activeLayer = SharedInput.LAYER_PATH_NETWORK;
ctx.setRightPanel().accept(deps.worldPanelController().buildWegePanel()); ctx.setRightPanel().accept(deps.worldPanelController().buildWegePanel());
}); }, ownerStage));
zonenBtn.setOnAction(e -> { zonenBtn.setOnAction(e -> guardVoxelExit(() -> {
input.activeLayer = SharedInput.LAYER_ZONEN; input.activeLayer = SharedInput.LAYER_ZONEN;
ctx.setRightPanel().accept(deps.worldPanelController().buildZonenPanel()); ctx.setRightPanel().accept(deps.worldPanelController().buildZonenPanel());
}); }, ownerStage));
playToolBtn.setOnAction(e -> { playToolBtn.setOnAction(e -> guardVoxelExit(() -> {
input.activeLayer = SharedInput.LAYER_PLAY_TOOL; input.activeLayer = SharedInput.LAYER_PLAY_TOOL;
input.playToolMode = SharedInput.PlayToolMode.EDIT; input.playToolMode = SharedInput.PlayToolMode.EDIT;
ctx.setRightPanel().accept(deps.gameController().buildPlayToolPanel()); ctx.setRightPanel().accept(deps.gameController().buildPlayToolPanel());
}); }, ownerStage));
worldHintLabel = new Label(camHintText()); worldHintLabel = new Label(camHintText());
worldHintLabel.setStyle("-fx-text-fill: #555;"); worldHintLabel.setStyle("-fx-text-fill: #555;");
@@ -332,4 +345,69 @@ public class TopBarController {
worldToolBar = toolBar; worldToolBar = toolBar;
return new VBox(menuBar, 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();
}
} }