Compare commits
3 Commits
8feaf24490
...
936efb7bcc
| Author | SHA1 | Date | |
|---|---|---|---|
| 936efb7bcc | |||
| 96483fada9 | |||
| 3b7f3b7e86 |
@@ -14,6 +14,7 @@ import de.blight.eztree.Billboard;
|
|||||||
import de.blight.eztree.TreeOptions;
|
import de.blight.eztree.TreeOptions;
|
||||||
import de.blight.eztree.TreePresets;
|
import de.blight.eztree.TreePresets;
|
||||||
import de.blight.eztree.TreeType;
|
import de.blight.eztree.TreeType;
|
||||||
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
@@ -799,10 +800,15 @@ public class EditorApp extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Kamera-Koordinaten aktualisieren
|
// Kamera-Koordinaten aktualisieren
|
||||||
camCoordsLabel.setText(String.format(
|
String camText = String.format(
|
||||||
"X:%.1f Y:%.1f Z:%.1f Yaw:%.0f° Pitch:%.0f°",
|
"X:%.1f Y:%.1f Z:%.1f Yaw:%.0f° Pitch:%.0f°",
|
||||||
input.camX, input.camY, input.camZ,
|
input.camX, input.camY, input.camZ,
|
||||||
input.camYaw, input.camPitch));
|
input.camYaw, input.camPitch);
|
||||||
|
float moh = input.mouseOverlayHeight;
|
||||||
|
if (input.debugHeightOverlayEnabled && !Float.isNaN(moh)) {
|
||||||
|
camText += String.format(java.util.Locale.US, " H:%.2fm", moh);
|
||||||
|
}
|
||||||
|
camCoordsLabel.setText(camText);
|
||||||
|
|
||||||
// Konsolen-Antwort anzeigen
|
// Konsolen-Antwort anzeigen
|
||||||
String consoleMsg = input.consoleOutput;
|
String consoleMsg = input.consoleOutput;
|
||||||
@@ -1318,9 +1324,12 @@ public class EditorApp extends Application {
|
|||||||
importTexSetItem.setOnAction(e -> openTextureSetImport(primaryStage));
|
importTexSetItem.setOnAction(e -> openTextureSetImport(primaryStage));
|
||||||
importAudioItem.setOnAction(e -> handleAudioImport(primaryStage));
|
importAudioItem.setOnAction(e -> handleAudioImport(primaryStage));
|
||||||
importAnimItem.setOnAction(e -> handleAnimationImport(primaryStage));
|
importAnimItem.setOnAction(e -> handleAnimationImport(primaryStage));
|
||||||
|
MenuItem screenshotItem = new MenuItem("Screenshot");
|
||||||
|
screenshotItem.setOnAction(e -> takeEditorScreenshot());
|
||||||
fileMenu.getItems().addAll(newItem, saveItem, new SeparatorMenuItem(),
|
fileMenu.getItems().addAll(newItem, saveItem, new SeparatorMenuItem(),
|
||||||
importModelLodItem, importTexItem, importTexZipItem, importTexSetItem,
|
importModelLodItem, importTexItem, importTexZipItem, importTexSetItem,
|
||||||
importAudioItem, importAnimItem);
|
importAudioItem, importAnimItem,
|
||||||
|
new SeparatorMenuItem(), screenshotItem);
|
||||||
|
|
||||||
Menu toolsMenu = new Menu("Werkzeuge");
|
Menu toolsMenu = new Menu("Werkzeuge");
|
||||||
MenuItem vegetationsItem = new MenuItem("Vegetations Generator");
|
MenuItem vegetationsItem = new MenuItem("Vegetations Generator");
|
||||||
@@ -1359,9 +1368,9 @@ public class EditorApp extends Application {
|
|||||||
Menu viewMenu = new Menu("Ansicht");
|
Menu viewMenu = new Menu("Ansicht");
|
||||||
MenuItem resetCam = new MenuItem("Kamera zurücksetzen");
|
MenuItem resetCam = new MenuItem("Kamera zurücksetzen");
|
||||||
resetCam.setOnAction(e -> input.addMouseDelta(0, 0));
|
resetCam.setOnAction(e -> input.addMouseDelta(0, 0));
|
||||||
MenuItem viewTexture = new MenuItem("Textur (Ctrl+G)");
|
MenuItem viewTexture = new MenuItem("Textur (Alt+G)");
|
||||||
MenuItem viewWireframe = new MenuItem("Drahtgitter (Ctrl+G)");
|
MenuItem viewWireframe = new MenuItem("Drahtgitter (Alt+G)");
|
||||||
viewTopologyItem = new CheckMenuItem("Topologie-Overlay (Ctrl+T)");
|
viewTopologyItem = new CheckMenuItem("Topologie-Overlay (Alt+T)");
|
||||||
viewTexture.setOnAction(e -> {
|
viewTexture.setOnAction(e -> {
|
||||||
wireframeActive = false;
|
wireframeActive = false;
|
||||||
input.wireframeRequest = 2;
|
input.wireframeRequest = 2;
|
||||||
@@ -1382,8 +1391,14 @@ public class EditorApp extends Application {
|
|||||||
camOrbitItem.setOnAction(e -> input.camMode = SharedInput.CAM_ORBIT);
|
camOrbitItem.setOnAction(e -> input.camMode = SharedInput.CAM_ORBIT);
|
||||||
camFreeItem.setOnAction(e -> input.camMode = SharedInput.CAM_FREEFLY);
|
camFreeItem.setOnAction(e -> input.camMode = SharedInput.CAM_FREEFLY);
|
||||||
|
|
||||||
|
CheckMenuItem debugHeightItem = new CheckMenuItem("Höhen-Overlay (Alt+H)");
|
||||||
|
debugHeightItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Alt+H"));
|
||||||
|
debugHeightItem.setOnAction(e ->
|
||||||
|
input.debugHeightOverlayEnabled = debugHeightItem.isSelected());
|
||||||
|
|
||||||
viewMenu.getItems().addAll(resetCam, new SeparatorMenuItem(), viewTexture, viewWireframe,
|
viewMenu.getItems().addAll(resetCam, new SeparatorMenuItem(), viewTexture, viewWireframe,
|
||||||
new SeparatorMenuItem(), viewTopologyItem,
|
new SeparatorMenuItem(), viewTopologyItem,
|
||||||
|
new SeparatorMenuItem(), debugHeightItem,
|
||||||
new SeparatorMenuItem(), camOrbitItem, camFreeItem);
|
new SeparatorMenuItem(), camOrbitItem, camFreeItem);
|
||||||
|
|
||||||
Menu zeitMenu = new Menu("Zeit");
|
Menu zeitMenu = new Menu("Zeit");
|
||||||
@@ -3456,12 +3471,47 @@ public class EditorApp extends Application {
|
|||||||
Label name = new Label(param.getName());
|
Label name = new Label(param.getName());
|
||||||
name.setStyle("-fx-text-fill: #111111;");
|
name.setStyle("-fx-text-fill: #111111;");
|
||||||
|
|
||||||
Slider slider = new Slider(param.getMin(), param.getMax(), param.getValue());
|
if (tool instanceof de.blight.editor.tool.VoxelTool vtR
|
||||||
slider.setShowTickMarks(true);
|
&& param == vtR.brushRadius) {
|
||||||
slider.setShowTickLabels(true);
|
// Ganzzahliger Slider: snapToTicks mit majorTickUnit=1 → nur ganze Zahlen
|
||||||
slider.setMajorTickUnit((param.getMax() - param.getMin()) / 2);
|
int initR = Math.max(1, (int) Math.round(param.getValue()));
|
||||||
slider.valueProperty().addListener((obs, oldV, newV) -> param.setValue(newV.doubleValue()));
|
Slider slider = new Slider(1, 30, initR);
|
||||||
panel.getChildren().add(new VBox(3, name, withField(slider, "%.3f")));
|
slider.setShowTickMarks(true);
|
||||||
|
slider.setShowTickLabels(true);
|
||||||
|
slider.setMajorTickUnit(5);
|
||||||
|
slider.setMinorTickCount(4);
|
||||||
|
slider.setSnapToTicks(true);
|
||||||
|
slider.setBlockIncrement(1);
|
||||||
|
slider.valueProperty().addListener((obs, oldV, newV) ->
|
||||||
|
param.setValue(Math.round(newV.doubleValue())));
|
||||||
|
panel.getChildren().add(new VBox(3, name, withField(slider, "%.0f")));
|
||||||
|
|
||||||
|
} else if (tool instanceof de.blight.editor.tool.VoxelTool vtP
|
||||||
|
&& param == vtP.plateauTarget) {
|
||||||
|
// x.5-gestufter Slider: min=0.5, Schritt=1 → 0.5, 1.5, 2.5, …
|
||||||
|
double initSnap = Math.round(param.getValue() - 0.5) + 0.5;
|
||||||
|
initSnap = Math.max(0.5, Math.min(49.5, initSnap));
|
||||||
|
Slider slider = new Slider(0.5, 49.5, initSnap);
|
||||||
|
slider.setShowTickMarks(true);
|
||||||
|
slider.setShowTickLabels(true);
|
||||||
|
slider.setMajorTickUnit(5);
|
||||||
|
slider.setMinorTickCount(4);
|
||||||
|
slider.setSnapToTicks(true);
|
||||||
|
slider.setBlockIncrement(1.0);
|
||||||
|
slider.valueProperty().addListener((obs, oldV, newV) -> {
|
||||||
|
double sv = Math.round(newV.doubleValue() - 0.5) + 0.5;
|
||||||
|
param.setValue(sv);
|
||||||
|
});
|
||||||
|
panel.getChildren().add(new VBox(3, name, withField(slider, "%.1f")));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Slider slider = new Slider(param.getMin(), param.getMax(), param.getValue());
|
||||||
|
slider.setShowTickMarks(true);
|
||||||
|
slider.setShowTickLabels(true);
|
||||||
|
slider.setMajorTickUnit((param.getMax() - param.getMin()) / 2);
|
||||||
|
slider.valueProperty().addListener((obs, oldV, newV) -> param.setValue(newV.doubleValue()));
|
||||||
|
panel.getChildren().add(new VBox(3, name, withField(slider, "%.3f")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Textur-Slot-Konfigurator (nur beim TextureTool)
|
// Textur-Slot-Konfigurator (nur beim TextureTool)
|
||||||
@@ -3620,8 +3670,36 @@ public class EditorApp extends Application {
|
|||||||
selPoller.setCycleCount(javafx.animation.Animation.INDEFINITE);
|
selPoller.setCycleCount(javafx.animation.Animation.INDEFINITE);
|
||||||
selPoller.play();
|
selPoller.play();
|
||||||
|
|
||||||
|
// ── Unterirdische Voxel bereinigen ──────────────────────────────────
|
||||||
|
Label cleanupStatus = new Label("");
|
||||||
|
cleanupStatus.setWrapText(true);
|
||||||
|
cleanupStatus.setStyle("-fx-text-fill: #555; -fx-font-size: 11;");
|
||||||
|
|
||||||
|
Button cleanupBtn = new Button("Unterirdische Voxel bereinigen");
|
||||||
|
cleanupBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
cleanupBtn.setOnAction(e -> {
|
||||||
|
input.cleanupUndergroundStatus = null;
|
||||||
|
input.cleanupUndergroundRequested = true;
|
||||||
|
cleanupBtn.setDisable(true);
|
||||||
|
cleanupStatus.setText("Läuft...");
|
||||||
|
});
|
||||||
|
|
||||||
|
javafx.animation.Timeline cleanupPoller = new javafx.animation.Timeline(
|
||||||
|
new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> {
|
||||||
|
String msg = input.cleanupUndergroundStatus;
|
||||||
|
if (msg != null) {
|
||||||
|
input.cleanupUndergroundStatus = null;
|
||||||
|
cleanupStatus.setText(msg);
|
||||||
|
cleanupBtn.setDisable(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
cleanupPoller.setCycleCount(javafx.animation.Animation.INDEFINITE);
|
||||||
|
cleanupPoller.play();
|
||||||
|
|
||||||
panel.getChildren().addAll(new Separator(), bakeBtn,
|
panel.getChildren().addAll(new Separator(), bakeBtn,
|
||||||
bakeBar, bakeBarLabel, bakeStatus,
|
bakeBar, bakeBarLabel, bakeStatus,
|
||||||
|
new Separator(), cleanupBtn, cleanupStatus,
|
||||||
new Separator(), selInfoLabel, selHintLabel, deleteBtn, revertBtn);
|
new Separator(), selInfoLabel, selHintLabel, deleteBtn, revertBtn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5673,6 +5751,21 @@ public class EditorApp extends Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void takeEditorScreenshot() {
|
||||||
|
WritableImage image = primaryStage.getScene().snapshot(null);
|
||||||
|
String timestamp = java.time.LocalDateTime.now()
|
||||||
|
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"));
|
||||||
|
Path dir = BlightHome.resolve("screenshots");
|
||||||
|
try {
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
File out = dir.resolve("editor_" + timestamp + ".png").toFile();
|
||||||
|
ImageIO.write(SwingFXUtils.fromFXImage(image, null), "PNG", out);
|
||||||
|
setStatus("Screenshot gespeichert: " + out.getPath());
|
||||||
|
} catch (IOException ex) {
|
||||||
|
setStatus("Screenshot fehlgeschlagen: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleTextureImport(javafx.stage.Window owner) {
|
private void handleTextureImport(javafx.stage.Window owner) {
|
||||||
FileChooser fc = new FileChooser();
|
FileChooser fc = new FileChooser();
|
||||||
fc.setTitle("Texturen importieren (nicht-PNG wird automatisch konvertiert)");
|
fc.setTitle("Texturen importieren (nicht-PNG wird automatisch konvertiert)");
|
||||||
@@ -8187,6 +8280,7 @@ public class EditorApp extends Application {
|
|||||||
case E -> input.down = pressed;
|
case E -> input.down = pressed;
|
||||||
case SHIFT -> input.shiftHeld = pressed;
|
case SHIFT -> input.shiftHeld = pressed;
|
||||||
case CONTROL -> input.ctrlHeld = pressed;
|
case CONTROL -> input.ctrlHeld = pressed;
|
||||||
|
case ALT -> input.altHeld = pressed;
|
||||||
case ENTER -> {
|
case ENTER -> {
|
||||||
if (pressed && input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF) {
|
if (pressed && input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF) {
|
||||||
input.generateCliffVoxelsRequested = true;
|
input.generateCliffVoxelsRequested = true;
|
||||||
@@ -8228,13 +8322,13 @@ public class EditorApp extends Application {
|
|||||||
input.waterSampleHeightRequested = true;
|
input.waterSampleHeightRequested = true;
|
||||||
}
|
}
|
||||||
case G -> {
|
case G -> {
|
||||||
if (pressed && input.ctrlHeld) {
|
if (pressed && input.altHeld) {
|
||||||
wireframeActive = !wireframeActive;
|
wireframeActive = !wireframeActive;
|
||||||
input.wireframeRequest = wireframeActive ? 1 : 2;
|
input.wireframeRequest = wireframeActive ? 1 : 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case T -> {
|
case T -> {
|
||||||
if (pressed && input.ctrlHeld && viewTopologyItem != null) {
|
if (pressed && input.altHeld && viewTopologyItem != null) {
|
||||||
boolean sel = !viewTopologyItem.isSelected();
|
boolean sel = !viewTopologyItem.isSelected();
|
||||||
viewTopologyItem.setSelected(sel);
|
viewTopologyItem.setSelected(sel);
|
||||||
input.topologyRequest = sel ? 1 : 2;
|
input.topologyRequest = sel ? 1 : 2;
|
||||||
|
|||||||
@@ -60,9 +60,10 @@ public class SharedInput {
|
|||||||
public final java.util.concurrent.atomic.AtomicInteger scrollAccum =
|
public final java.util.concurrent.atomic.AtomicInteger scrollAccum =
|
||||||
new java.util.concurrent.atomic.AtomicInteger();
|
new java.util.concurrent.atomic.AtomicInteger();
|
||||||
|
|
||||||
// ── Shift / Ctrl ─────────────────────────────────────────────────────────
|
// ── Shift / Ctrl / Alt ───────────────────────────────────────────────────
|
||||||
public volatile boolean shiftHeld;
|
public volatile boolean shiftHeld;
|
||||||
public volatile boolean ctrlHeld;
|
public volatile boolean ctrlHeld;
|
||||||
|
public volatile boolean altHeld;
|
||||||
|
|
||||||
// ── Debug-Toggle: Strg+F8 schaltet Raw-Texture-Modus (kein Lighting) ─────
|
// ── Debug-Toggle: Strg+F8 schaltet Raw-Texture-Modus (kein Lighting) ─────
|
||||||
public volatile boolean debugNoLightToggle;
|
public volatile boolean debugNoLightToggle;
|
||||||
@@ -185,6 +186,8 @@ public class SharedInput {
|
|||||||
// ── Mausposition im Viewport (JavaFX-Pixel, -1 = außerhalb) ─────────────
|
// ── Mausposition im Viewport (JavaFX-Pixel, -1 = außerhalb) ─────────────
|
||||||
public volatile float mouseScreenX = -1f;
|
public volatile float mouseScreenX = -1f;
|
||||||
public volatile float mouseScreenY = -1f;
|
public volatile float mouseScreenY = -1f;
|
||||||
|
/** JME → JavaFX: Höhe am Mauszeiger wenn Height-Overlay aktiv (NaN = unbekannt). */
|
||||||
|
public volatile float mouseOverlayHeight = Float.NaN;
|
||||||
|
|
||||||
// ── Speichern ─────────────────────────────────────────────────────────────
|
// ── Speichern ─────────────────────────────────────────────────────────────
|
||||||
public volatile boolean saveRequested = false;
|
public volatile boolean saveRequested = false;
|
||||||
@@ -783,6 +786,9 @@ public class SharedInput {
|
|||||||
/** 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;
|
||||||
|
|
||||||
|
/** JFX → JME: Höhen-Overlay ein/aus (Terrain / Voxel / Baked im 100m-Radius). */
|
||||||
|
public volatile boolean debugHeightOverlayEnabled = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per BFS selektierte gebackene Chunks (chunkKey-kodiert).
|
* Per BFS selektierte gebackene Chunks (chunkKey-kodiert).
|
||||||
* Thread-sicher; JME schreibt, JFX liest (nur size() für Anzeige).
|
* Thread-sicher; JME schreibt, JFX liest (nur size() für Anzeige).
|
||||||
@@ -795,6 +801,10 @@ public class SharedInput {
|
|||||||
public volatile boolean deleteMarkedBakedRequested = false;
|
public volatile boolean deleteMarkedBakedRequested = false;
|
||||||
/** JFX → JME: selektierte Chunks aus Pre-Bake-Backup wiederherstellen. */
|
/** JFX → JME: selektierte Chunks aus Pre-Bake-Backup wiederherstellen. */
|
||||||
public volatile boolean revertMarkedBakedRequested = false;
|
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;
|
||||||
|
|
||||||
/** 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. */
|
||||||
public volatile int voxelFlatSlot = -1;
|
public volatile int voxelFlatSlot = -1;
|
||||||
|
|||||||
@@ -131,6 +131,23 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
|
|
||||||
private Geometry brushIndicator;
|
private Geometry brushIndicator;
|
||||||
|
|
||||||
|
// ── Höhen-Debug-Overlay ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Node debugPtsNode;
|
||||||
|
private com.jme3.font.BitmapFont debugFont;
|
||||||
|
private com.jme3.font.BitmapText debugHudText;
|
||||||
|
private java.util.List<com.jme3.font.BitmapText> debugLabelsTerrain = new java.util.ArrayList<>();
|
||||||
|
private java.util.List<com.jme3.font.BitmapText> debugLabelsVoxel = new java.util.ArrayList<>();
|
||||||
|
private java.util.List<com.jme3.font.BitmapText> debugLabelsBaked = new java.util.ArrayList<>();
|
||||||
|
private float[] debugLabelXZ; // [i*2]=x, [i*2+1]=z
|
||||||
|
private float[] debugLabelTH; // Terrain-Höhe
|
||||||
|
private float[] debugLabelVH; // Voxel-Höhe (NaN = kein Voxel)
|
||||||
|
private float[] debugLabelBH; // Baked-Höhe (NaN = kein Treffer)
|
||||||
|
private boolean debugOverlayActive = false;
|
||||||
|
private float debugCenterX = Float.NaN;
|
||||||
|
private float debugCenterZ = Float.NaN;
|
||||||
|
private int debugLastStep = -1;
|
||||||
|
|
||||||
// ── Basis-Terrain-Referenzebene (y = -10) ────────────────────────────────
|
// ── Basis-Terrain-Referenzebene (y = -10) ────────────────────────────────
|
||||||
|
|
||||||
/** Flache Referenzebene bei Welt-Y = -10; nur im LAYER_VOXEL sichtbar. */
|
/** Flache Referenzebene bei Welt-Y = -10; nur im LAYER_VOXEL sichtbar. */
|
||||||
@@ -231,9 +248,12 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
protected void cleanup(Application app) {
|
protected void cleanup(Application app) {
|
||||||
executor.shutdownNow();
|
executor.shutdownNow();
|
||||||
voxelRoot.removeFromParent();
|
voxelRoot.removeFromParent();
|
||||||
if (brushIndicator != null) brushIndicator.removeFromParent();
|
if (brushIndicator != null) brushIndicator.removeFromParent();
|
||||||
if (basePlaneNode != null) basePlaneNode.removeFromParent();
|
if (basePlaneNode != null) basePlaneNode.removeFromParent();
|
||||||
if (wireframeActive) applyWireframe(false);
|
if (debugPtsNode != null) debugPtsNode.removeFromParent();
|
||||||
|
if (debugHudText != null) debugHudText.removeFromParent();
|
||||||
|
clearDebugLabels();
|
||||||
|
if (wireframeActive) applyWireframe(false);
|
||||||
nodes.clear();
|
nodes.clear();
|
||||||
chunks.clear();
|
chunks.clear();
|
||||||
}
|
}
|
||||||
@@ -263,6 +283,25 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
// Brush-Indikator immer aktualisieren (zeigen/verstecken je nach Layer)
|
// Brush-Indikator immer aktualisieren (zeigen/verstecken je nach Layer)
|
||||||
updateBrushIndicator();
|
updateBrushIndicator();
|
||||||
|
|
||||||
|
// Höhen-Overlay ein/ausschalten und aktualisieren
|
||||||
|
if (input.debugHeightOverlayEnabled != debugOverlayActive) {
|
||||||
|
debugOverlayActive = input.debugHeightOverlayEnabled;
|
||||||
|
applyDebugOverlay(debugOverlayActive);
|
||||||
|
}
|
||||||
|
if (debugOverlayActive) {
|
||||||
|
float cx = cam.getLocation().x, cz = cam.getLocation().z;
|
||||||
|
int step = debugLabelStep(cam.getLocation().y);
|
||||||
|
float halfStep = step / 2f;
|
||||||
|
if (Float.isNaN(debugCenterX)
|
||||||
|
|| Math.abs(cx - debugCenterX) > halfStep
|
||||||
|
|| Math.abs(cz - debugCenterZ) > halfStep
|
||||||
|
|| step != debugLastStep) {
|
||||||
|
debugCenterX = cx; debugCenterZ = cz; debugLastStep = step;
|
||||||
|
rebuildDebugGeometry(cx, cz);
|
||||||
|
}
|
||||||
|
refreshDebugHud();
|
||||||
|
}
|
||||||
|
|
||||||
// Bake angefordert?
|
// Bake angefordert?
|
||||||
if (input.bakeVoxelsRequested) {
|
if (input.bakeVoxelsRequested) {
|
||||||
input.bakeVoxelsRequested = false;
|
input.bakeVoxelsRequested = false;
|
||||||
@@ -716,9 +755,12 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
// gebakte Meshes nach Distanz priorisiert – immer der korrekte Oberflächenpunkt.
|
// gebakte Meshes nach Distanz priorisiert – immer der korrekte Oberflächenpunkt.
|
||||||
if (!isHorizontal && isColumn && modeIdx == de.blight.editor.tool.VoxelTool.MODE_PLATEAU && lower) {
|
if (!isHorizontal && isColumn && modeIdx == de.blight.editor.tool.VoxelTool.MODE_PLATEAU && lower) {
|
||||||
if (Float.isFinite(wy)) {
|
if (Float.isFinite(wy)) {
|
||||||
input.voxelTool.plateauTarget.setValue(wy);
|
// Auf x.5-Raster einrasten: MC-Oberfläche liegt ~0.5m über dem Zelldach,
|
||||||
|
// daher ist 1.5m, 2.5m, 3.5m usw. der korrekte Zielwert.
|
||||||
|
double snapped = Math.round(wy - 0.5) + 0.5;
|
||||||
|
input.voxelTool.plateauTarget.setValue(snapped);
|
||||||
input.voxelTool.plateauTargetChanged = true;
|
input.voxelTool.plateauTargetChanged = true;
|
||||||
input.heightTool.plateauHeight.setValue(wy);
|
input.heightTool.plateauHeight.setValue(snapped);
|
||||||
input.heightTool.plateauHeightChanged = true;
|
input.heightTool.plateauHeightChanged = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -1112,6 +1154,21 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
return 0f;
|
return 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Höchste bekannte Oberfläche an (worldX, worldZ): max(Terrain, Voxel, Baked). */
|
||||||
|
public float heightAt(float worldX, float worldZ) {
|
||||||
|
float h = terrainH(worldX, worldZ);
|
||||||
|
float vh = columnTopWorldY(worldX, worldZ);
|
||||||
|
if (vh > h + 0.1f) h = vh;
|
||||||
|
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||||
|
if (smes != null) {
|
||||||
|
com.jme3.math.Ray r = new com.jme3.math.Ray(
|
||||||
|
new Vector3f(worldX, 500f, worldZ), new Vector3f(0f, -1f, 0f));
|
||||||
|
Vector3f[] hit = smes.raycastGeometryWithNormal(r);
|
||||||
|
if (hit != null && hit[0].y > h) h = hit[0].y;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean hasTerrainMesh() {
|
private boolean hasTerrainMesh() {
|
||||||
return terrainEditorState != null || terrainQuad != null;
|
return terrainEditorState != null || terrainQuad != null;
|
||||||
}
|
}
|
||||||
@@ -1201,18 +1258,34 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
return results.getClosestCollision().getContactPoint();
|
return results.getClosestCollision().getContactPoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Welt-Y des höchsten Solid-Voxels an (worldX, worldZ), oder Terrain-Höhe wenn keine Voxel. */
|
/**
|
||||||
|
* Interpolierte Welt-Y der Voxel-Oberfläche an (worldX, worldZ).
|
||||||
|
* Verwendet dieselbe Lineare Interpolation wie Marching Cubes (t = -d0 / (d1 - d0)),
|
||||||
|
* sodass der zurückgegebene Wert mit dem echten Mesh übereinstimmt.
|
||||||
|
* Gibt Terrain-Höhe zurück wenn keine Voxel vorhanden.
|
||||||
|
*/
|
||||||
public float columnTopWorldY(float worldX, float worldZ) {
|
public float columnTopWorldY(float worldX, float worldZ) {
|
||||||
int cx = VoxelChunk.worldXToCx(worldX);
|
int cx = VoxelChunk.worldXToCx(worldX);
|
||||||
int cz = VoxelChunk.worldZToCz(worldZ);
|
int cz = VoxelChunk.worldZToCz(worldZ);
|
||||||
int lx = Math.max(0, Math.min(VoxelChunk.SIZE - 1, (int) VoxelChunk.worldXToLocal(worldX, cx)));
|
int lx = Math.max(0, Math.min(VoxelChunk.SIZE - 1, (int) VoxelChunk.worldXToLocal(worldX, cx)));
|
||||||
int lzl = Math.max(0, Math.min(VoxelChunk.SIZE - 1, (int) VoxelChunk.worldZToLocal(worldZ, cz)));
|
int lzl = Math.max(0, Math.min(VoxelChunk.SIZE - 1, (int) VoxelChunk.worldZToLocal(worldZ, cz)));
|
||||||
for (int cy = 10; cy >= -2; cy--) {
|
for (int cy = 10; cy >= -2; cy--) {
|
||||||
VoxelChunk chunk = chunks.get(chunkKey(cx, cy, cz));
|
VoxelChunk chunk = chunks.get(chunkKey(cx, cy, cz));
|
||||||
if (chunk == null || chunk.isEmpty()) continue;
|
if (chunk == null || chunk.isEmpty()) continue;
|
||||||
for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) {
|
for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) {
|
||||||
if (chunk.getDensity(lx, ly, lzl) > 0) {
|
float d0 = chunk.getDensity(lx, ly, lzl);
|
||||||
return VoxelChunk.toWorldY(cy, ly);
|
if (d0 > 0) {
|
||||||
|
// Density der Zelle darüber (ggf. nächster Chunk)
|
||||||
|
float d1;
|
||||||
|
if (ly + 1 < VoxelChunk.SIZE) {
|
||||||
|
d1 = chunk.getDensity(lx, ly + 1, lzl);
|
||||||
|
} else {
|
||||||
|
VoxelChunk above = chunks.get(chunkKey(cx, cy + 1, cz));
|
||||||
|
d1 = (above != null) ? above.getDensity(lx, 0, lzl) : -1f;
|
||||||
|
}
|
||||||
|
float t = (Math.abs(d1 - d0) < 0.001f) ? 0.5f
|
||||||
|
: Math.max(0f, Math.min(1f, -d0 / (d1 - d0)));
|
||||||
|
return VoxelChunk.toWorldY(cy, ly) + t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1260,12 +1333,27 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
// Terrain-Höhe als Fallback wenn noch keine Voxel in dieser Spalte
|
// Terrain-Höhe als Fallback wenn noch keine Voxel in dieser Spalte
|
||||||
float th = currentTopLY < 0 ? terrainH(wx, wz) : Float.NaN;
|
float th = currentTopLY < 0 ? terrainH(wx, wz) : Float.NaN;
|
||||||
float currentTopWY = currentTopLY >= 0
|
|
||||||
? VoxelChunk.toWorldY(cy, currentTopLY)
|
|
||||||
: th;
|
|
||||||
|
|
||||||
float diff = targetH - currentTopWY;
|
// MC-Oberfläche liegt ~0.5m über der obersten soliden Zelle.
|
||||||
if (Math.abs(diff) < 0.5f) continue;
|
// Ohne Offset pendelt das Tool: raise→3m, diff=-0.5→lower→2m, diff=+0.5→raise→...
|
||||||
|
// Für leere Spalten (Terrain-Fallback) reicht terrainH direkt; dort
|
||||||
|
// genügt 0.1m Schwelle, damit z.B. Terrain 2m → Ziel 2.5m zuverlässig greift.
|
||||||
|
float effectiveCurrentH;
|
||||||
|
float threshold;
|
||||||
|
if (currentTopLY >= 0) {
|
||||||
|
effectiveCurrentH = VoxelChunk.toWorldY(cy, currentTopLY) + 0.5f;
|
||||||
|
threshold = 0.5f;
|
||||||
|
} else {
|
||||||
|
effectiveCurrentH = th;
|
||||||
|
threshold = 0.1f;
|
||||||
|
}
|
||||||
|
float diff = targetH - effectiveCurrentH;
|
||||||
|
if (Math.abs(diff) < threshold) continue;
|
||||||
|
|
||||||
|
// Ziel-Zelle: floor(targetH) = oberste SOLIDE Zelle bei Ziel-Höhe.
|
||||||
|
// Raise/Lower nie über diese Zelle hinaus → verhindert Überschuss-Artefakte.
|
||||||
|
int targetLY = Math.max(0, Math.min(VoxelChunk.SIZE - 1,
|
||||||
|
(int) Math.floor(targetH - cy * (float) VoxelChunk.CELLS)));
|
||||||
|
|
||||||
if (diff > 0) {
|
if (diff > 0) {
|
||||||
// Erhöhen
|
// Erhöhen
|
||||||
@@ -1283,13 +1371,13 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int newTop = Math.min(VoxelChunk.SIZE - 1, startLY + step);
|
int newTop = Math.min(startLY + step, targetLY); // nie über Ziel-Zelle
|
||||||
for (int ly = startLY; ly <= newTop; ly++) {
|
for (int ly = startLY; ly <= newTop; ly++) {
|
||||||
chunk.setDensity(lx, ly, lz, (byte) 127);
|
chunk.setDensity(lx, ly, lz, (byte) 127);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (currentTopLY < 0) continue;
|
if (currentTopLY < 0) continue;
|
||||||
int newTop = Math.max(0, currentTopLY - step);
|
int newTop = Math.max(currentTopLY - step, targetLY); // nie unter Ziel-Zelle
|
||||||
for (int ly = newTop + 1; ly <= currentTopLY; ly++) {
|
for (int ly = newTop + 1; ly <= currentTopLY; ly++) {
|
||||||
chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE);
|
chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE);
|
||||||
}
|
}
|
||||||
@@ -1490,6 +1578,235 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
* Bäckt alle übergebenen Chunks: speichert .blvc, glättet die Dichte mit
|
* Bäckt alle übergebenen Chunks: speichert .blvc, glättet die Dichte mit
|
||||||
* Nachbar-Lookup, erzeugt LOD0/1/2-Meshes und exportiert sie als .j3o.
|
* Nachbar-Lookup, erzeugt LOD0/1/2-Meshes und exportiert sie als .j3o.
|
||||||
*/
|
*/
|
||||||
|
// ── Höhen-Debug-Overlay ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void applyDebugOverlay(boolean on) {
|
||||||
|
if (on) {
|
||||||
|
if (debugFont == null) {
|
||||||
|
debugFont = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
||||||
|
}
|
||||||
|
if (debugPtsNode == null) {
|
||||||
|
debugPtsNode = new Node("debugHeightPts");
|
||||||
|
debugPtsNode.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||||
|
}
|
||||||
|
if (debugHudText == null) {
|
||||||
|
debugHudText = new com.jme3.font.BitmapText(debugFont, false);
|
||||||
|
debugHudText.setSize(debugFont.getCharSet().getRenderedSize() * 1.4f);
|
||||||
|
debugHudText.setColor(com.jme3.math.ColorRGBA.Yellow);
|
||||||
|
}
|
||||||
|
app.getRootNode().attachChild(debugPtsNode);
|
||||||
|
app.getGuiNode().attachChild(debugHudText);
|
||||||
|
debugCenterX = Float.NaN; // erzwingt sofortigen Rebuild
|
||||||
|
} else {
|
||||||
|
if (debugPtsNode != null) debugPtsNode.removeFromParent();
|
||||||
|
if (debugHudText != null) debugHudText.removeFromParent();
|
||||||
|
clearDebugLabels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut drei farbige Punkt-Wolken im 100m-Radius um (cx, cz):
|
||||||
|
* Grau = Basis-Terrain
|
||||||
|
* Cyan = Raw-Voxel-Oberfläche
|
||||||
|
* Orange = Gebackene Mesh-Oberfläche (4m-Raster, Downward-Raycast)
|
||||||
|
* Jeder Punkt sitzt an seiner echten Welt-Y-Position, sodass
|
||||||
|
* man von der Seite drei überlagerte Flächen sieht.
|
||||||
|
*/
|
||||||
|
private void rebuildDebugGeometry(float cx, float cz) {
|
||||||
|
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||||
|
clearDebugLabels();
|
||||||
|
|
||||||
|
int ls = debugLastStep;
|
||||||
|
int lr = debugLabelRadius(ls);
|
||||||
|
java.util.List<float[]> lPts = new java.util.ArrayList<>();
|
||||||
|
for (int dz = -lr; dz <= lr; dz += ls) {
|
||||||
|
for (int dx = -lr; dx <= lr; dx += ls) {
|
||||||
|
if (dx * dx + dz * dz > lr * lr) continue;
|
||||||
|
lPts.add(new float[]{cx + dx, cz + dz});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int ln = lPts.size();
|
||||||
|
debugLabelXZ = new float[ln * 2];
|
||||||
|
debugLabelTH = new float[ln];
|
||||||
|
debugLabelVH = new float[ln]; // NaN wenn kein Voxel
|
||||||
|
debugLabelBH = new float[ln];
|
||||||
|
|
||||||
|
com.jme3.math.ColorRGBA colTerrain = com.jme3.math.ColorRGBA.White;
|
||||||
|
com.jme3.math.ColorRGBA colVoxel = new com.jme3.math.ColorRGBA(0.4f, 0.8f, 1f, 1f);
|
||||||
|
com.jme3.math.ColorRGBA colBaked = new com.jme3.math.ColorRGBA(1f, 0.5f, 0.5f, 1f);
|
||||||
|
|
||||||
|
for (int i = 0; i < ln; i++) {
|
||||||
|
float lx = lPts.get(i)[0], lz = lPts.get(i)[1];
|
||||||
|
debugLabelXZ[i * 2] = lx;
|
||||||
|
debugLabelXZ[i * 2 + 1] = lz;
|
||||||
|
float th = terrainH(lx, lz);
|
||||||
|
float vh = columnTopWorldY(lx, lz);
|
||||||
|
debugLabelTH[i] = th;
|
||||||
|
debugLabelVH[i] = (vh > th + 0.1f) ? vh : Float.NaN; // NaN wenn kein Voxel
|
||||||
|
if (smes != null) {
|
||||||
|
com.jme3.math.Ray r = new com.jme3.math.Ray(
|
||||||
|
new Vector3f(lx, 500f, lz), new Vector3f(0f, -1f, 0f));
|
||||||
|
Vector3f[] hit = smes.raycastGeometryWithNormal(r);
|
||||||
|
debugLabelBH[i] = (hit != null) ? hit[0].y : Float.NaN;
|
||||||
|
} else {
|
||||||
|
debugLabelBH[i] = Float.NaN;
|
||||||
|
}
|
||||||
|
debugLabelsTerrain.add(makeLabelBt(colTerrain));
|
||||||
|
debugLabelsVoxel .add(makeLabelBt(colVoxel));
|
||||||
|
debugLabelsBaked .add(makeLabelBt(colBaked));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int debugLabelStep(float camY) {
|
||||||
|
if (camY < 25f) return 1;
|
||||||
|
else if (camY < 70f) return 4;
|
||||||
|
else return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int debugLabelRadius(int step) {
|
||||||
|
if (step <= 1) return 30;
|
||||||
|
else if (step <= 4) return 60;
|
||||||
|
else return 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
private com.jme3.font.BitmapText makeLabelBt(com.jme3.math.ColorRGBA color) {
|
||||||
|
com.jme3.font.BitmapText bt = new com.jme3.font.BitmapText(debugFont, false);
|
||||||
|
bt.setSize(11f);
|
||||||
|
bt.setColor(color);
|
||||||
|
bt.setText("");
|
||||||
|
bt.setCullHint(com.jme3.scene.Spatial.CullHint.Always);
|
||||||
|
app.getGuiNode().attachChild(bt);
|
||||||
|
return bt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearDebugLabels() {
|
||||||
|
for (com.jme3.font.BitmapText bt : debugLabelsTerrain) bt.removeFromParent();
|
||||||
|
for (com.jme3.font.BitmapText bt : debugLabelsVoxel) bt.removeFromParent();
|
||||||
|
for (com.jme3.font.BitmapText bt : debugLabelsBaked) bt.removeFromParent();
|
||||||
|
debugLabelsTerrain.clear();
|
||||||
|
debugLabelsVoxel .clear();
|
||||||
|
debugLabelsBaked .clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erzeugt eine Geometrie aus kurzen vertikalen Tick-Linien (0.5m hoch) an jedem
|
||||||
|
* Probenpunkt. Mode.Lines macht die Ticks deutlich sichtbarer als einzelne Pixel.
|
||||||
|
* posList: [x0,y0,z0, x1,y1,z1, ...] – Fußpunkte der Ticks.
|
||||||
|
*/
|
||||||
|
private Geometry makeTickCloud(String name, java.util.List<Float> posList,
|
||||||
|
com.jme3.math.ColorRGBA color) {
|
||||||
|
int n = posList.size() / 3;
|
||||||
|
// 2 Vertices pro Tick (Basis + Spitze), je 3 Positions-Floats
|
||||||
|
FloatBuffer posBuf = BufferUtils.createFloatBuffer(n * 6);
|
||||||
|
FloatBuffer colBuf = BufferUtils.createFloatBuffer(n * 8);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
float x = posList.get(i * 3);
|
||||||
|
float y = posList.get(i * 3 + 1);
|
||||||
|
float z = posList.get(i * 3 + 2);
|
||||||
|
posBuf.put(x).put(y).put(z); // Basis
|
||||||
|
posBuf.put(x).put(y + 0.5f).put(z); // Spitze
|
||||||
|
colBuf.put(color.r).put(color.g).put(color.b).put(color.a);
|
||||||
|
colBuf.put(color.r).put(color.g).put(color.b).put(color.a);
|
||||||
|
}
|
||||||
|
posBuf.rewind(); colBuf.rewind();
|
||||||
|
|
||||||
|
Mesh mesh = new Mesh();
|
||||||
|
mesh.setMode(Mesh.Mode.Lines);
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Position, 3, posBuf);
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Color, 4, colBuf);
|
||||||
|
mesh.updateBound();
|
||||||
|
|
||||||
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setBoolean("VertexColor", true);
|
||||||
|
mat.getAdditionalRenderState().setDepthTest(false);
|
||||||
|
|
||||||
|
Geometry geo = new Geometry(name, mesh);
|
||||||
|
geo.setMaterial(mat);
|
||||||
|
geo.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||||
|
return geo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert den HUD-Text mit den Höhen direkt unterhalb der Kamera.
|
||||||
|
* Position: oben links im Viewport.
|
||||||
|
*/
|
||||||
|
private void refreshDebugHud() {
|
||||||
|
float wx = cam.getLocation().x;
|
||||||
|
float wz = cam.getLocation().z;
|
||||||
|
float th = terrainH(wx, wz);
|
||||||
|
float vh = columnTopWorldY(wx, wz);
|
||||||
|
|
||||||
|
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||||
|
String bakedLine = "";
|
||||||
|
if (smes != null) {
|
||||||
|
com.jme3.math.Ray ray = new com.jme3.math.Ray(
|
||||||
|
new Vector3f(wx, 500f, wz), new Vector3f(0f, -1f, 0f));
|
||||||
|
Vector3f[] hit = smes.raycastGeometryWithNormal(ray);
|
||||||
|
if (hit != null) {
|
||||||
|
bakedLine = String.format(java.util.Locale.US,
|
||||||
|
"\n Baked (orange): %.2fm", hit[0].y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugHudText.setText(String.format(java.util.Locale.US,
|
||||||
|
"Höhen bei X=%.1f Z=%.1f\n" +
|
||||||
|
" Terrain (grau): %.2fm\n" +
|
||||||
|
" Voxel roh (cyan): %.2fm%s",
|
||||||
|
wx, wz, th, vh, bakedLine));
|
||||||
|
debugHudText.setLocalTranslation(8, cam.getHeight() - 8, 0);
|
||||||
|
|
||||||
|
// ── Projizierte Zahlenlabels ─────────────────────────────────────────
|
||||||
|
if (debugLabelXZ != null) {
|
||||||
|
int ln = debugLabelsTerrain.size();
|
||||||
|
for (int i = 0; i < ln; i++) {
|
||||||
|
float lx = debugLabelXZ[i * 2];
|
||||||
|
float lz = debugLabelXZ[i * 2 + 1];
|
||||||
|
projectLabel(debugLabelsTerrain.get(i), lx, debugLabelTH[i], lz,
|
||||||
|
String.format(java.util.Locale.US, "%.1f", debugLabelTH[i]));
|
||||||
|
float lvh = debugLabelVH[i];
|
||||||
|
if (!Float.isNaN(lvh)) {
|
||||||
|
projectLabel(debugLabelsVoxel.get(i), lx, lvh, lz,
|
||||||
|
String.format(java.util.Locale.US, "%.1f", lvh));
|
||||||
|
} else {
|
||||||
|
debugLabelsVoxel.get(i).setCullHint(com.jme3.scene.Spatial.CullHint.Always);
|
||||||
|
}
|
||||||
|
float lbh = debugLabelBH[i];
|
||||||
|
if (!Float.isNaN(lbh)) {
|
||||||
|
projectLabel(debugLabelsBaked.get(i), lx, lbh, lz,
|
||||||
|
String.format(java.util.Locale.US, "%.1f", lbh));
|
||||||
|
} else {
|
||||||
|
debugLabelsBaked.get(i).setCullHint(com.jme3.scene.Spatial.CullHint.Always);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mauszeiger-Höhe → SharedInput (echter Raycast gegen alle Geometrien) ──
|
||||||
|
float msx = input.mouseScreenX;
|
||||||
|
if (msx >= 0f) {
|
||||||
|
float msy = cam.getHeight() - input.mouseScreenY;
|
||||||
|
Hit mouseHit = raycastHit(msx, msy);
|
||||||
|
input.mouseOverlayHeight = (mouseHit != null && mouseHit.pos != null)
|
||||||
|
? mouseHit.pos.y : Float.NaN;
|
||||||
|
} else {
|
||||||
|
input.mouseOverlayHeight = Float.NaN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void projectLabel(com.jme3.font.BitmapText bt,
|
||||||
|
float wx, float wy, float wz, String text) {
|
||||||
|
Vector3f screen = cam.getScreenCoordinates(new Vector3f(wx, wy + 0.3f, wz));
|
||||||
|
boolean onScreen = screen.z > 0f && screen.z < 1f
|
||||||
|
&& screen.x > 0 && screen.x < cam.getWidth()
|
||||||
|
&& screen.y > 10 && screen.y < cam.getHeight();
|
||||||
|
if (onScreen) {
|
||||||
|
bt.setText(text);
|
||||||
|
bt.setLocalTranslation(screen.x, screen.y, 0);
|
||||||
|
bt.setCullHint(com.jme3.scene.Spatial.CullHint.Never);
|
||||||
|
} else {
|
||||||
|
bt.setCullHint(com.jme3.scene.Spatial.CullHint.Always);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void bakeAll(List<VoxelChunk> toProcess) {
|
private void bakeAll(List<VoxelChunk> toProcess) {
|
||||||
saveAll();
|
saveAll();
|
||||||
List<VoxelChunk> nonEmpty = new java.util.ArrayList<>();
|
List<VoxelChunk> nonEmpty = new java.util.ArrayList<>();
|
||||||
@@ -1595,18 +1912,22 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
// ── Schritt 1: Höhenfeld ──────────────────────────────────────────
|
// ── Schritt 1: Höhenfeld ──────────────────────────────────────────
|
||||||
// Key-Schema: (cx*C + bx + 30000) * 60001 + (cz*C + bz + 30000)
|
// Key-Schema: (cx*C + bx + 30000) * 60001 + (cz*C + bz + 30000)
|
||||||
// Überlapp-Voxel (bx = CELLS) werden ausgelassen.
|
// Überlapp-Voxel (bx = CELLS) werden ausgelassen.
|
||||||
Map<Long, Integer> hfMap = new HashMap<>();
|
Map<Long, Float> hfMap = new HashMap<>();
|
||||||
for (VoxelChunk c : nonEmpty) {
|
for (VoxelChunk c : nonEmpty) {
|
||||||
long k = chunkKey(c.cx, c.cy, c.cz);
|
long k = chunkKey(c.cx, c.cy, c.cz);
|
||||||
float[] buf = curBufs.get(k);
|
float[] buf = curBufs.get(k);
|
||||||
for (int bz = 0; bz < C; bz++) {
|
for (int bz = 0; bz < C; bz++) {
|
||||||
for (int bx = 0; bx < C; bx++) {
|
for (int bx = 0; bx < C; bx++) {
|
||||||
for (int by = blurN - 1; by >= 0; by--) {
|
for (int by = blurN - 1; by >= 0; by--) {
|
||||||
if (buf[c.idx(bx, by, bz)] > 0) {
|
float d0 = buf[c.idx(bx, by, bz)];
|
||||||
int wiy = c.cy * C + by;
|
if (d0 > 0) {
|
||||||
|
float d1 = (by + 1 < blurN) ? buf[c.idx(bx, by + 1, bz)] : -1f;
|
||||||
|
float t = (Math.abs(d1 - d0) < 0.001f) ? 0.5f
|
||||||
|
: Math.max(0f, Math.min(1f, -d0 / (d1 - d0)));
|
||||||
|
float wfy = c.cy * C + by + t;
|
||||||
long hk = (long)(c.cx * C + bx + 30000) * 60001L
|
long hk = (long)(c.cx * C + bx + 30000) * 60001L
|
||||||
+ (c.cz * C + bz + 30000);
|
+ (c.cz * C + bz + 30000);
|
||||||
hfMap.merge(hk, wiy, Math::max);
|
hfMap.merge(hk, wfy, Math::max);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1631,11 +1952,16 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
for (int bz = 0; bz < C; bz++) {
|
for (int bz = 0; bz < C; bz++) {
|
||||||
for (int bx = 0; bx < C; bx++) {
|
for (int bx = 0; bx < C; bx++) {
|
||||||
for (int by = VoxelChunk.SIZE - 1; by >= 0; by--) {
|
for (int by = VoxelChunk.SIZE - 1; by >= 0; by--) {
|
||||||
if (nc.getDensity(bx, by, bz) > 0) {
|
float d0 = nc.getDensity(bx, by, bz);
|
||||||
int wiy = ncy * C + by;
|
if (d0 > 0) {
|
||||||
|
float d1 = (by + 1 < VoxelChunk.SIZE)
|
||||||
|
? nc.getDensity(bx, by + 1, bz) : -1f;
|
||||||
|
float t = (Math.abs(d1 - d0) < 0.001f) ? 0.5f
|
||||||
|
: Math.max(0f, Math.min(1f, -d0 / (d1 - d0)));
|
||||||
|
float wfy = ncy * C + by + t;
|
||||||
long hk = (long)(ncx * C + bx + 30000) * 60001L
|
long hk = (long)(ncx * C + bx + 30000) * 60001L
|
||||||
+ (ncz * C + bz + 30000);
|
+ (ncz * C + bz + 30000);
|
||||||
hfMap.merge(hk, wiy, Math::max);
|
hfMap.merge(hk, wfy, Math::max);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1690,9 +2016,9 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
// kleinsten Höhe > h0 bzw. größten Höhe < h0 wählen. Betraf 17 von 10002 Spalten
|
// kleinsten Höhe > h0 bzw. größten Höhe < h0 wählen. Betraf 17 von 10002 Spalten
|
||||||
// in den Test-Chunks, max. Abweichung 1,0 (diag_realwall*.jsh im scratchpad).
|
// in den Test-Chunks, max. Abweichung 1,0 (diag_realwall*.jsh im scratchpad).
|
||||||
Map<Long, Float> smoothMap = new HashMap<>(hfMap.size());
|
Map<Long, Float> smoothMap = new HashMap<>(hfMap.size());
|
||||||
for (Map.Entry<Long, Integer> entry : hfMap.entrySet()) {
|
for (Map.Entry<Long, Float> entry : hfMap.entrySet()) {
|
||||||
long hk0 = entry.getKey();
|
long hk0 = entry.getKey();
|
||||||
int h0 = entry.getValue();
|
float h0 = entry.getValue();
|
||||||
int colZ = (int)(hk0 % 60001L) - 30000;
|
int colZ = (int)(hk0 % 60001L) - 30000;
|
||||||
int colX = (int)(hk0 / 60001L) - 30000;
|
int colX = (int)(hk0 / 60001L) - 30000;
|
||||||
|
|
||||||
@@ -1706,15 +2032,15 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
// mit der KLEINSTEN Höhe > h0 (bzw. GRÖSSTEN Höhe < h0) wählen, also den
|
// mit der KLEINSTEN Höhe > h0 (bzw. GRÖSSTEN Höhe < h0) wählen, also den
|
||||||
// nächstgelegenen tatsächlichen Stufen-Nachbarn, nicht irgendeinen weiter
|
// nächstgelegenen tatsächlichen Stufen-Nachbarn, nicht irgendeinen weiter
|
||||||
// entfernten. Validiert gegen reale Chunk-Daten (diag_realwall*.jsh).
|
// entfernten. Validiert gegen reale Chunk-Daten (diag_realwall*.jsh).
|
||||||
Integer hUp = null; int dUp = -1;
|
Float hUp = null; int dUp = -1;
|
||||||
Integer hDown = null; int dDownRaw = -1;
|
Float hDown = null; int dDownRaw = -1;
|
||||||
for (int r = 1; r <= RADIUS && (hUp == null || hDown == null); r++) {
|
for (int r = 1; r <= RADIUS && (hUp == null || hDown == null); r++) {
|
||||||
Integer bestUpThisRing = null, bestDownThisRing = null;
|
Float bestUpThisRing = null, bestDownThisRing = null;
|
||||||
for (int dz = -r; dz <= r; dz++) {
|
for (int dz = -r; dz <= r; dz++) {
|
||||||
for (int dx = -r; dx <= r; dx++) {
|
for (int dx = -r; dx <= r; dx++) {
|
||||||
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) continue;
|
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) continue;
|
||||||
long hkN = (long)(colX + dx + 30000) * 60001L + (colZ + dz + 30000);
|
long hkN = (long)(colX + dx + 30000) * 60001L + (colZ + dz + 30000);
|
||||||
Integer hN = hfMap.get(hkN);
|
Float hN = hfMap.get(hkN);
|
||||||
if (hN == null) continue;
|
if (hN == null) continue;
|
||||||
if (hN > h0 && (bestUpThisRing == null || hN < bestUpThisRing)) bestUpThisRing = hN;
|
if (hN > h0 && (bestUpThisRing == null || hN < bestUpThisRing)) bestUpThisRing = hN;
|
||||||
if (hN < h0 && (bestDownThisRing == null || hN > bestDownThisRing)) bestDownThisRing = hN;
|
if (hN < h0 && (bestDownThisRing == null || hN > bestDownThisRing)) bestDownThisRing = hN;
|
||||||
@@ -1739,10 +2065,15 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
// Rand hin auslaufende Rampe über die vollen RADIUS Meter, statt eines
|
// Rand hin auslaufende Rampe über die vollen RADIUS Meter, statt eines
|
||||||
// abrupten Sprungs zwischen "geramped" und "unverändert flach".
|
// abrupten Sprungs zwischen "geramped" und "unverändert flach".
|
||||||
float smoothH;
|
float smoothH;
|
||||||
if (hUp == null) {
|
if (hUp == null || hDown == null) {
|
||||||
smoothH = h0; // Plateau: kein höherer Nachbar -> bewusst flach (Originalregel)
|
// Plateau (kein höherer Nachbar) ODER unterste/äußerste Ebene
|
||||||
|
// (kein niedrigerer Voxel-Nachbar): Höhe unverändert lassen.
|
||||||
|
// Die Formel darf hier NICHT angewendet werden – sie würde die
|
||||||
|
// unterste Terrasse künstlich in Richtung hUp anheben und das
|
||||||
|
// gebackene Terrain systematisch zu hoch machen.
|
||||||
|
smoothH = h0;
|
||||||
} else {
|
} else {
|
||||||
float dDown = (hDown != null) ? (dDownRaw - 1) : (RADIUS - 1);
|
float dDown = dDownRaw - 1;
|
||||||
smoothH = (dDown <= 0f) ? h0 : (h0 * dUp + hUp * dDown) / (dUp + dDown);
|
smoothH = (dDown <= 0f) ? h0 : (h0 * dUp + hUp * dDown) / (dUp + dDown);
|
||||||
}
|
}
|
||||||
smoothMap.put(hk0, smoothH);
|
smoothMap.put(hk0, smoothH);
|
||||||
@@ -1780,89 +2111,80 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
// Fix v6 (aktuell): Steigungs-Fortsetzung komplett gestrichen. Reiner Distanz-Blend
|
// Fix v6 (aktuell): Steigungs-Fortsetzung komplett gestrichen. Reiner Distanz-Blend
|
||||||
// von der (ring-gemittelten) Kanten-Höhe zur tatsächlichen Basisterrain-Höhe. Folgt
|
// von der (ring-gemittelten) Kanten-Höhe zur tatsächlichen Basisterrain-Höhe. Folgt
|
||||||
// nicht mehr exakt dem Rampen-Winkel, ist aber garantiert monoton/glatt.
|
// nicht mehr exakt dem Rampen-Winkel, ist aber garantiert monoton/glatt.
|
||||||
int[][] DIRS8 = {{1,0},{-1,0},{0,1},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1}};
|
// 4:1-Abwärtsrampe bei Stufenabbrüchen (inter-terrace + Basisterrain-Übergang).
|
||||||
|
//
|
||||||
|
// Schritt 2 lässt Spalten mit hUp=null flach (smoothH=h0), auch wenn direkt
|
||||||
|
// daneben eine niedrigere Terasse oder das Basisterrain-Tal liegt. Das erzeugt
|
||||||
|
// vertikale Klippen an allen Plateau-Kanten.
|
||||||
|
//
|
||||||
|
// Fix: Für jede Spalte den nächsten Nachbarn mit Höhe < h0 suchen
|
||||||
|
// (Voxel-Spalte aus hfMap ODER Basisterrain außerhalb). Wenn gefunden bei
|
||||||
|
// Chebyshev-Distanz d mit mittlerer Höhe avgLow:
|
||||||
|
// rampH = avgLow + d / 4.0
|
||||||
|
// Spalte wird auf min(smoothH, rampH) abgesenkt – nie angehoben.
|
||||||
|
// Entspricht 4 Schritte vor / 1 Schritt runter, deckt beide Bedingungen ab:
|
||||||
|
// „Tal darunter" (avgLow << h0) und „<1m über Basisterrain" (kleines dH).
|
||||||
|
// Ringsuche je Spalte unabhängig von anderen Schritt-2b-Ergebnissen →
|
||||||
|
// keine Kettenfortpflanzung, keine Divergenz.
|
||||||
|
{
|
||||||
|
final float RAMP_RATIO = 4.0f;
|
||||||
|
final int MAX_RAMP_R = 20;
|
||||||
|
|
||||||
// Kandidaten sammeln: alle leeren Zellen innerhalb RADIUS einer Rand-Spalte.
|
// Terrain-Cache: vermeidet wiederholte terrainH()-Aufrufe bei Überlappungen
|
||||||
java.util.Set<Long> candidates = new java.util.HashSet<>();
|
Map<Long, Float> terrainCache2b = new HashMap<>();
|
||||||
for (long hk0 : hfMap.keySet()) {
|
|
||||||
int colZ = (int)(hk0 % 60001L) - 30000;
|
|
||||||
int colX = (int)(hk0 / 60001L) - 30000;
|
|
||||||
for (int dz = -RADIUS; dz <= RADIUS; dz++) {
|
|
||||||
for (int dx = -RADIUS; dx <= RADIUS; dx++) {
|
|
||||||
if (Math.max(Math.abs(dx), Math.abs(dz)) > RADIUS) continue;
|
|
||||||
long hkC = (long)(colX + dx + 30000) * 60001L + (colZ + dz + 30000);
|
|
||||||
if (!hfMap.containsKey(hkC)) candidates.add(hkC);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<Long, Float> extensionMap = new HashMap<>();
|
for (Map.Entry<Long, Float> entry : hfMap.entrySet()) {
|
||||||
int extCount = 0, extLogged = 0;
|
long hk0 = entry.getKey();
|
||||||
for (long hkC : candidates) {
|
float h0raw = entry.getValue(); // Original-Voxelhöhe
|
||||||
int colZ = (int)(hkC % 60001L) - 30000;
|
float h0sm = smoothMap.getOrDefault(hk0, h0raw); // Schritt-2-Ausgabe
|
||||||
int colX = (int)(hkC / 60001L) - 30000;
|
int colX2b = (int)(hk0 / 60001L) - 30000;
|
||||||
|
int colZ2b = (int)(hk0 % 60001L) - 30000;
|
||||||
|
|
||||||
// Nächsten Ring mit Struktur-Zellen isotrop suchen (gleiches Vorgehen wie
|
// Ringsuche nach nächstem niedrigeren Nachbarn (Voxel oder Terrain)
|
||||||
// Schritt 2), dann ALLE Struktur-Zellen in diesem Ring mitteln – nicht nur die
|
float sumLowH = 0f;
|
||||||
// erste gefundene. Grund (Log-Befund): benachbarte Zielzellen fanden oft
|
int lowCount = 0;
|
||||||
// unterschiedliche einzelne "nächste" Zellen mit stark abweichender Höhe
|
int lowDist = -1;
|
||||||
// (edgeH schwankte 3,9 bis 5,33 nur 4 Zellen auseinander) → wildes Springen.
|
|
||||||
// Mittelung über den ganzen Ring glättet das.
|
outer2b:
|
||||||
int nearestDist = -1;
|
for (int r = 1; r <= MAX_RAMP_R; r++) {
|
||||||
java.util.List<long[]> ringHits = new java.util.ArrayList<>(); // {colX,colZ}
|
for (int dz2b = -r; dz2b <= r; dz2b++) {
|
||||||
outer:
|
for (int dx2b = -r; dx2b <= r; dx2b++) {
|
||||||
for (int r = 1; r <= RADIUS; r++) {
|
if (Math.max(Math.abs(dx2b), Math.abs(dz2b)) != r) continue;
|
||||||
for (int dz = -r; dz <= r; dz++) {
|
long hkN = (long)(colX2b + dx2b + 30000) * 60001L
|
||||||
for (int dx = -r; dx <= r; dx++) {
|
+ (colZ2b + dz2b + 30000);
|
||||||
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) continue;
|
Float hN = hfMap.get(hkN);
|
||||||
long hkN = (long)(colX + dx + 30000) * 60001L + (colZ + dz + 30000);
|
float neighborH;
|
||||||
if (hfMap.containsKey(hkN)) ringHits.add(new long[]{colX + dx, colZ + dz});
|
if (hN != null) {
|
||||||
|
neighborH = hN; // Voxel-Spalte: Originalhöhe
|
||||||
|
} else {
|
||||||
|
Float cached = terrainCache2b.get(hkN);
|
||||||
|
if (cached == null) {
|
||||||
|
cached = terrainH((float)(colX2b + dx2b),
|
||||||
|
(float)(colZ2b + dz2b));
|
||||||
|
terrainCache2b.put(hkN, cached);
|
||||||
|
}
|
||||||
|
neighborH = cached; // Basisterrain
|
||||||
|
}
|
||||||
|
if (neighborH < h0raw) {
|
||||||
|
sumLowH += neighborH;
|
||||||
|
lowCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lowCount > 0) {
|
||||||
|
lowDist = r;
|
||||||
|
break outer2b;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!ringHits.isEmpty()) { nearestDist = r; break outer; }
|
|
||||||
}
|
|
||||||
if (nearestDist < 0) continue; // außerhalb der Reichweite, bleibt Klippe
|
|
||||||
|
|
||||||
float sumEdgeH = 0f; int hitCnt = 0;
|
if (lowDist < 0) continue; // kein niedrigerer Nachbar im Suchradius
|
||||||
for (long[] hit : ringHits) {
|
|
||||||
int hColX = (int) hit[0], hColZ = (int) hit[1];
|
|
||||||
long hKey = (long)(hColX + 30000) * 60001L + (hColZ + 30000);
|
|
||||||
Float hH = smoothMap.get(hKey);
|
|
||||||
if (hH == null) continue;
|
|
||||||
sumEdgeH += hH; hitCnt++;
|
|
||||||
}
|
|
||||||
if (hitCnt == 0) continue;
|
|
||||||
float edgeH = sumEdgeH / hitCnt;
|
|
||||||
|
|
||||||
// KEINE Steigungs-Extrapolation mehr (v4/v5 hatten das versucht – eine an nur
|
float avgLowH = sumLowH / lowCount;
|
||||||
// wenigen Pixeln gemessene Steigung ist zu verrauscht, verstärkt sich über die
|
float rampH = avgLowH + lowDist / RAMP_RATIO;
|
||||||
// Distanz und erzeugte ein welliges/oszillierendes Band über eigentlich flachen
|
if (rampH < h0sm) {
|
||||||
// Flächen, siehe Screenshot 22-43-57). Reiner, robuster Distanz-Blend von der
|
smoothMap.put(hk0, rampH);
|
||||||
// (gemittelten) Kanten-Höhe zur tatsächlichen Basisterrain-Höhe: nah am Rand
|
}
|
||||||
// dominiert die Kante, weiter draußen die Terrainhöhe. Folgt nicht mehr exakt
|
|
||||||
// dem Winkel der letzten Rampe, ist aber garantiert monoton und ohne Rauschen.
|
|
||||||
float wx = colX - 2048f, wz = colZ - 2048f;
|
|
||||||
float th = terrainH(wx, wz);
|
|
||||||
float finalH = edgeH;
|
|
||||||
if (Float.isFinite(th)) {
|
|
||||||
float t = nearestDist / (float) RADIUS; // 0 am Rand, 1 bei RADIUS
|
|
||||||
finalH = edgeH * (1f - t) + th * t;
|
|
||||||
}
|
}
|
||||||
extensionMap.put(hkC, finalH);
|
|
||||||
extCount++;
|
|
||||||
if (extLogged < 20) {
|
|
||||||
log.info("Perimeter-Rampe: colX={} colZ={} dist={} edgeH={} th={} finalH={}",
|
|
||||||
colX, colZ, nearestDist, edgeH, th, finalH);
|
|
||||||
extLogged++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("Perimeter-Übergang: {} Erweiterungs-Spalten jenseits des Strukturrands erzeugt.", extCount);
|
|
||||||
// Erweiterungs-Spalten in hfMap/smoothMap einspeisen, damit Schritt 3 sie mitschreibt.
|
|
||||||
for (Map.Entry<Long, Float> e : extensionMap.entrySet()) {
|
|
||||||
long hk = e.getKey();
|
|
||||||
if (hfMap.containsKey(hk)) continue;
|
|
||||||
hfMap.put(hk, Math.round(e.getValue()));
|
|
||||||
smoothMap.put(hk, e.getValue());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Schritt 3: Dichte anpassen ────────────────────────────────────
|
// ── Schritt 3: Dichte anpassen ────────────────────────────────────
|
||||||
@@ -1889,7 +2211,7 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
for (int bx = 0; bx < C; bx++) {
|
for (int bx = 0; bx < C; bx++) {
|
||||||
int colX = c.cx * C + bx;
|
int colX = c.cx * C + bx;
|
||||||
long hk0 = (long)(colX + 30000) * 60001L + (colZ + 30000);
|
long hk0 = (long)(colX + 30000) * 60001L + (colZ + 30000);
|
||||||
Integer h0 = hfMap.get(hk0);
|
Float h0 = hfMap.get(hk0);
|
||||||
Float smoothH = smoothMap.get(hk0);
|
Float smoothH = smoothMap.get(hk0);
|
||||||
if (h0 == null || smoothH == null) continue;
|
if (h0 == null || smoothH == null) continue;
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class HeightTool extends EditorTool {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 50.0, 1.0, 500.0);
|
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 1.0, 500.0);
|
||||||
public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 2.0, 0.1, 50.0);
|
public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 2.0, 0.1, 50.0);
|
||||||
public final ToolParameter plateauHeight = new ToolParameter("Plateau-Höhe", 0.0, -500.0, 500.0);
|
public final ToolParameter plateauHeight = new ToolParameter("Plateau-Höhe", 0.0, -500.0, 500.0);
|
||||||
public volatile boolean plateauHeightChanged = false;
|
public volatile boolean plateauHeightChanged = false;
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ public class VoxelTool extends EditorTool {
|
|||||||
public volatile boolean modeChanged = false;
|
public volatile boolean modeChanged = false;
|
||||||
public volatile boolean horizontal = false;
|
public volatile boolean horizontal = false;
|
||||||
|
|
||||||
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 0.5, 30.0);
|
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 1.0, 30.0);
|
||||||
public final ToolParameter brushStrength = new ToolParameter("Stärke", 20.0, 1.0, 80.0);
|
public final ToolParameter brushStrength = new ToolParameter("Stärke", 20.0, 1.0, 80.0);
|
||||||
public final ToolParameter plateauTarget = new ToolParameter("Plateau-Ziel", 0.0, -200.0, 500.0);
|
public final ToolParameter plateauTarget = new ToolParameter("Plateau-Ziel", 1.5, -99.5, 499.5);
|
||||||
public volatile boolean plateauTargetChanged = false;
|
public volatile boolean plateauTargetChanged = false;
|
||||||
|
|
||||||
@Override public String getName() { return "Voxel"; }
|
@Override public String getName() { return "Voxel"; }
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user