voxel umgestellt
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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...");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user