Stein Tool ein wenig überarbeitet
This commit is contained in:
@@ -92,6 +92,7 @@ public class EditorApp extends Application {
|
||||
private de.blight.editor.toolset.water.WaterController waterController;
|
||||
private de.blight.editor.toolset.voxel.VoxelController voxelController;
|
||||
private de.blight.editor.toolset.grass.GrassController grassController;
|
||||
private de.blight.editor.toolset.stone.StoneController stoneController;
|
||||
private de.blight.editor.toolset.content.ContentController contentController;
|
||||
private de.blight.editor.toolset.tripo.TripoController tripoController;
|
||||
private de.blight.editor.toolset.game.GameController gameController;
|
||||
@@ -193,6 +194,7 @@ public class EditorApp extends Application {
|
||||
viewportController.getViewport(), terrainController::showToolParameters,
|
||||
colorAbsPath -> { de.blight.editor.TextureMapData d = terrainController.loadTexMeta(colorAbsPath); return d.normalMap != null ? d.normalMap : ""; });
|
||||
terrainController.setGrassController(grassController);
|
||||
stoneController = new de.blight.editor.toolset.stone.StoneController(toolCtx);
|
||||
contentController = new de.blight.editor.toolset.content.ContentController(toolCtx);
|
||||
contentController.setOnEnterContentMode(() -> {
|
||||
if (assetController.isOverlayOpen()) assetController.closeOverlay();
|
||||
@@ -227,7 +229,7 @@ public class EditorApp extends Application {
|
||||
modelEditorController, assetController, animSetController,
|
||||
contentController, worldPanelController, voxelController,
|
||||
grassController, atmosphereController, waterController,
|
||||
gameController, terrainController, objectController,
|
||||
gameController, terrainController, objectController, stoneController,
|
||||
this::switchToVegetationGenerator, this::switchToEzTree,
|
||||
this::switchToTripo, this::openCharacterEditor,
|
||||
this::switchToThumbnailManager, this::takeEditorScreenshot, this::openSettings
|
||||
@@ -375,6 +377,12 @@ public class EditorApp extends Application {
|
||||
atmosphereController.onFrame();
|
||||
waterController.onFrame();
|
||||
voxelController.onFrame();
|
||||
stoneController.onFrame();
|
||||
if (input.stoneAutoActivateEditMode) {
|
||||
input.stoneAutoActivateEditMode = false;
|
||||
stoneController.activateEditMode();
|
||||
topBarController.selectStoneBtnSilently();
|
||||
}
|
||||
|
||||
java.util.Set<String> skeletal = input.skeletalPaths.getAndSet(null);
|
||||
if (skeletal != null) {
|
||||
|
||||
@@ -1023,6 +1023,8 @@ public class SharedInput {
|
||||
// ── Steine ────────────────────────────────────────────────────────────────
|
||||
/** activeLayer==23 → Steine setzen/entfernen */
|
||||
public static final int LAYER_STONE = 23;
|
||||
/** activeLayer==37 → Stein selektieren / Gizmo bedienen */
|
||||
public static final int LAYER_STONE_EDIT = 37;
|
||||
|
||||
public record StoneEdit(float screenX, float screenY, int action) {}
|
||||
public final ConcurrentLinkedQueue<StoneEdit> stoneEditQueue = new ConcurrentLinkedQueue<>();
|
||||
@@ -1033,6 +1035,24 @@ public class SharedInput {
|
||||
/** JFX → JME: Reload der Materialien auslösen. */
|
||||
public volatile boolean stoneTexturesChanged = false;
|
||||
|
||||
/** JME → JFX: Info über selektierten Stein ("x|z|rotX|rotY|rotZ|radius" oder null). */
|
||||
public volatile String stoneSelectedInfo = null;
|
||||
public volatile boolean stoneSelectionChanged = false;
|
||||
|
||||
/** JFX → JME: aktives Gizmo-Werkzeug im Bearbeiten-Modus (0=Bewegen, 1=Rotieren). */
|
||||
public volatile int stoneEditTool = EDIT_TOOL_MOVE;
|
||||
|
||||
/** Drag-Ereignisse aus ViewportController für Stein-Gizmo. */
|
||||
public final ConcurrentLinkedQueue<ObjectDrag> stoneDragQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
/** JFX → JME: Selektierten Stein löschen. */
|
||||
public volatile boolean stoneDeleteRequested = false;
|
||||
|
||||
/** Klicks aus LAYER_AUSWAHL – StoneEditorState testet parallel auf Stein-Treffer. */
|
||||
public final ConcurrentLinkedQueue<StoneEdit> stoneAuswahlQueue = new ConcurrentLinkedQueue<>();
|
||||
/** JME → JFX: Stein wurde per Auswahl-Tool selektiert → automatisch Edit-Panel öffnen. */
|
||||
public volatile boolean stoneAutoActivateEditMode = false;
|
||||
|
||||
/** JME → JME: Terrain-Edits, die Stein-Höhen neu berechnen müssen. float[3] = {wx, wz, radius}. */
|
||||
public final ConcurrentLinkedQueue<float[]> terrainEditedAreas = new ConcurrentLinkedQueue<>();
|
||||
|
||||
|
||||
@@ -220,6 +220,7 @@ public class PlacedObjectState extends BaseAppState {
|
||||
mat.setFloat("WindStrength", 0.12f);
|
||||
mat.setVector3("SunDir", new Vector3f(0.55f, 0.80f, 0.35f));
|
||||
mat.setColor("SunColor", ColorRGBA.White);
|
||||
mat.setBoolean("GammaCorrect", true);
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
return mat;
|
||||
} catch (Exception e) {
|
||||
@@ -444,8 +445,7 @@ public class PlacedObjectState extends BaseAppState {
|
||||
int vi = 0;
|
||||
for (float[] blade : blades) {
|
||||
float x = blade[0], y = blade[1], z = blade[2], h = blade[3];
|
||||
// Klingenbreite: Hälfte der Gesamtbreite (2w × h = Quad-Seitenverhältnis ar)
|
||||
float w = Math.max(0.05f, h * ar * 0.5f);
|
||||
float w = Math.max(0.001f, h * ar * 0.5f);
|
||||
// Quad 1: liegt in XY-Ebene (konstantes z) – Normal zeigt +Z, Tangent zeigt +X
|
||||
pos.put(x-w).put(y ).put(z); uv.put(0).put(0);
|
||||
pos.put(x+w).put(y ).put(z); uv.put(1).put(0);
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.bounding.BoundingVolume;
|
||||
import com.jme3.collision.CollisionResults;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
@@ -12,6 +13,8 @@ import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.VertexBuffer.Type;
|
||||
import com.jme3.scene.shape.Cylinder;
|
||||
import com.jme3.scene.shape.Torus;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.PlacedStone;
|
||||
@@ -68,11 +71,38 @@ public class StoneEditorState extends BaseAppState {
|
||||
private final Material[] slotMat = new Material[PlacedStoneIO.SLOT_COUNT];
|
||||
private Material defaultMat;
|
||||
private Geometry brushIndicator;
|
||||
private Geometry selHighlight;
|
||||
private Geometry previewGeom;
|
||||
private PlacedStone pendingStone;
|
||||
|
||||
private final Random random = new Random();
|
||||
private boolean modified = false;
|
||||
private double lastRoughness = -1;
|
||||
|
||||
// ── Selektion & Gizmo ────────────────────────────────────────────────────
|
||||
private static final ColorRGBA COL_X = new ColorRGBA(0.92f, 0.12f, 0.12f, 1f);
|
||||
private static final ColorRGBA COL_Y = new ColorRGBA(0.12f, 0.88f, 0.12f, 1f);
|
||||
private static final ColorRGBA COL_Z = new ColorRGBA(0.12f, 0.35f, 1.00f, 1f);
|
||||
private static final float SHAFT_LEN = 3.2f;
|
||||
private static final float HEAD_LEN = 0.9f;
|
||||
private static final float SHAFT_RADIUS = 0.06f;
|
||||
private static final float HEAD_RADIUS = 0.22f;
|
||||
private static final float RING_RADIUS = 4.5f;
|
||||
private static final float RING_TUBE = 0.25f;
|
||||
private static final float PX_PER_WE = 0.15f;
|
||||
private static final float DEG_PER_PX = 0.8f;
|
||||
|
||||
private Node gizmoNode;
|
||||
private Node arrowX, arrowY, arrowZ;
|
||||
private Node ringX, ringY, ringZ;
|
||||
private int activeGizmo = -1;
|
||||
private int activeRing = -1;
|
||||
private int lastStoneEditTool = -1;
|
||||
|
||||
private int selChunk = -1;
|
||||
private int selIdx = -1;
|
||||
/** Rückwärts-Lookup: Geometrie-Name → {chunkIdx, stoneIdx} */
|
||||
private final Map<String, int[]> geomNameToRef = new HashMap<>();
|
||||
|
||||
public StoneEditorState(SharedInput input) {
|
||||
this.input = input;
|
||||
@@ -99,6 +129,21 @@ public class StoneEditorState extends BaseAppState {
|
||||
|
||||
previewGeom = buildPreviewGeom();
|
||||
rootNode.attachChild(previewGeom);
|
||||
|
||||
gizmoNode = new Node("stoneGizmo");
|
||||
buildGizmo();
|
||||
gizmoNode.setCullHint(Spatial.CullHint.Always);
|
||||
rootNode.attachChild(gizmoNode);
|
||||
|
||||
Material hlMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
hlMat.setColor("Color", new ColorRGBA(0.3f, 0.8f, 1.0f, 1f));
|
||||
hlMat.getAdditionalRenderState().setWireframe(true);
|
||||
hlMat.getAdditionalRenderState().setDepthTest(false);
|
||||
selHighlight = new Geometry("stoneSelHighlight", new Mesh());
|
||||
selHighlight.setMaterial(hlMat);
|
||||
selHighlight.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
selHighlight.setCullHint(Spatial.CullHint.Always);
|
||||
rootNode.attachChild(selHighlight);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -107,6 +152,8 @@ public class StoneEditorState extends BaseAppState {
|
||||
rootNode.detachChild(stoneRoot);
|
||||
rootNode.detachChild(brushIndicator);
|
||||
rootNode.detachChild(previewGeom);
|
||||
rootNode.detachChild(gizmoNode);
|
||||
rootNode.detachChild(selHighlight);
|
||||
}
|
||||
|
||||
@Override protected void onEnable() { stoneRoot.setCullHint(Spatial.CullHint.Inherit); }
|
||||
@@ -122,16 +169,81 @@ public class StoneEditorState extends BaseAppState {
|
||||
Arrays.fill(dirtyChunks, true);
|
||||
}
|
||||
|
||||
double curRoughness = input.stoneTool.roughness.getValue();
|
||||
if (curRoughness != lastRoughness) {
|
||||
lastRoughness = curRoughness;
|
||||
Arrays.fill(dirtyChunks, true);
|
||||
pendingStone = null;
|
||||
}
|
||||
|
||||
if (input.stoneEditTool != lastStoneEditTool) {
|
||||
lastStoneEditTool = input.stoneEditTool;
|
||||
activeGizmo = -1;
|
||||
activeRing = -1;
|
||||
}
|
||||
|
||||
updateBrushIndicator();
|
||||
updatePreviewStone();
|
||||
processEdits();
|
||||
processAuswahlClicks();
|
||||
processTerrainUpdates();
|
||||
rebuildDirtyChunks();
|
||||
updateChunkLOD();
|
||||
|
||||
if (input.activeLayer == SharedInput.LAYER_STONE_EDIT) {
|
||||
brushIndicator.setCullHint(Spatial.CullHint.Always);
|
||||
SharedInput.ObjectDrag drag;
|
||||
while ((drag = input.stoneDragQueue.poll()) != null) {
|
||||
if (activeGizmo >= 0) handleMoveDrag(drag);
|
||||
else if (activeRing >= 0) handleRotateDrag(drag);
|
||||
}
|
||||
if (input.stoneDeleteRequested) {
|
||||
input.stoneDeleteRequested = false;
|
||||
deleteSelectedStone();
|
||||
}
|
||||
if (selChunk >= 0) {
|
||||
selHighlight.setCullHint(Spatial.CullHint.Inherit);
|
||||
updateGizmoPosition();
|
||||
updateGizmoVisibility();
|
||||
} else {
|
||||
gizmoNode.setCullHint(Spatial.CullHint.Always);
|
||||
selHighlight.setCullHint(Spatial.CullHint.Always);
|
||||
}
|
||||
} else {
|
||||
while (input.stoneDragQueue.poll() != null) { /* drain */ }
|
||||
activeGizmo = -1;
|
||||
activeRing = -1;
|
||||
gizmoNode.setCullHint(Spatial.CullHint.Always);
|
||||
selHighlight.setCullHint(Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Edit-Verarbeitung ─────────────────────────────────────────────────────
|
||||
|
||||
private void processAuswahlClicks() {
|
||||
SharedInput.StoneEdit click;
|
||||
while ((click = input.stoneAuswahlQueue.poll()) != null) {
|
||||
if (input.activeLayer != SharedInput.LAYER_AUSWAHL) continue;
|
||||
float jx = click.screenX() * (float) input.viewportScaleX;
|
||||
float jy = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Vector3f origin = cam.getWorldCoordinates(new Vector2f(jx, jy), 0f);
|
||||
Vector3f target = cam.getWorldCoordinates(new Vector2f(jx, jy), 1f);
|
||||
Ray ray = new Ray(origin, target.subtractLocal(origin).normalizeLocal());
|
||||
CollisionResults res = new CollisionResults();
|
||||
stoneRoot.collideWith(ray, res);
|
||||
if (res.size() > 0) {
|
||||
String rawName = res.getClosestCollision().getGeometry().getName();
|
||||
String name = rawName.endsWith("_lod1")
|
||||
? rawName.substring(0, rawName.length() - 5) : rawName;
|
||||
int[] ref = geomNameToRef.get(name);
|
||||
if (ref != null) {
|
||||
selectStone(ref[0], ref[1]);
|
||||
input.stoneAutoActivateEditMode = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processTerrainUpdates() {
|
||||
float[] area;
|
||||
while ((area = input.terrainEditedAreas.poll()) != null) {
|
||||
@@ -150,15 +262,22 @@ public class StoneEditorState extends BaseAppState {
|
||||
while ((edit = input.stoneEditQueue.poll()) != null) {
|
||||
float jx = edit.screenX() * (float) input.viewportScaleX;
|
||||
float jy = cam.getHeight() - edit.screenY() * (float) input.viewportScaleY;
|
||||
|
||||
if (input.activeLayer == SharedInput.LAYER_STONE_EDIT) {
|
||||
if (edit.action() > 0) processEditModeClick(jx, jy);
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3f hit = raycastTerrain(jx, jy);
|
||||
if (hit == null) continue;
|
||||
|
||||
if (isSingleStoneMode()) {
|
||||
if (edit.action() < 0) { // Rechtsklick: platzierten Stein + neuen Preview
|
||||
if (edit.action() > 0) { // Linksklick: Stein platzieren + neuen Preview erzeugen
|
||||
placePendingStoneAt(hit.x, hit.z);
|
||||
generatePendingStone();
|
||||
} else { // Rechtsklick: nur neuen Zufallsstein zum Platzieren bereitstellen
|
||||
generatePendingStone();
|
||||
}
|
||||
// Linksklick ignoriert in Single-Stone-Modus
|
||||
} else {
|
||||
if (edit.action() > 0) addStonesAt(hit.x, hit.z);
|
||||
else removeStonesAt(hit.x, hit.z);
|
||||
@@ -187,17 +306,20 @@ public class StoneEditorState extends BaseAppState {
|
||||
int slot = random.nextInt(activeSlots);
|
||||
int seed = random.nextInt();
|
||||
|
||||
pendingStone = new PlacedStone(0f, 0f, radius, rotY, slot, sinkFrac, seed);
|
||||
pendingStone = new PlacedStone(0f, 0f, radius, 0f, rotY, 0f, slot, sinkFrac, seed);
|
||||
|
||||
Mesh mesh = buildStoneMesh(radius, seed, 2);
|
||||
float roughness = (float) input.stoneTool.roughness.getValue();
|
||||
Mesh mesh = buildStoneMesh(radius, seed, subdivForRadius(radius), roughness);
|
||||
previewGeom.setMesh(mesh);
|
||||
previewGeom.setLocalRotation(new Quaternion().fromAngles(0f, rotY * FastMath.DEG_TO_RAD, 0f));
|
||||
previewGeom.setMaterial(materialForSlot(slot));
|
||||
}
|
||||
|
||||
private void placePendingStoneAt(float wx, float wz) {
|
||||
if (pendingStone == null) { return; }
|
||||
if (!Float.isFinite(getHeightAt(wx, wz))) { return; }
|
||||
PlacedStone stone = new PlacedStone(wx, wz, pendingStone.radius(), pendingStone.rotY(),
|
||||
PlacedStone stone = new PlacedStone(wx, wz, pendingStone.radius(),
|
||||
pendingStone.rotX(), pendingStone.rotY(), pendingStone.rotZ(),
|
||||
pendingStone.textureSlot(), pendingStone.sinkFraction(), pendingStone.noiseSeed());
|
||||
int ci = chunkIndex(wx, wz);
|
||||
if (ci >= 0) {
|
||||
@@ -254,7 +376,7 @@ public class StoneEditorState extends BaseAppState {
|
||||
int slot = random.nextInt(activeSlots);
|
||||
int seed = random.nextInt();
|
||||
|
||||
PlacedStone stone = new PlacedStone(sx, sz, radius, rotY, slot, sinkFrac, seed);
|
||||
PlacedStone stone = new PlacedStone(sx, sz, radius, 0f, rotY, 0f, slot, sinkFrac, seed);
|
||||
int ci = chunkIndex(sx, sz);
|
||||
if (ci >= 0) {
|
||||
chunkStones[ci].add(stone);
|
||||
@@ -292,7 +414,9 @@ public class StoneEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
private void rebuildChunk(int ci) {
|
||||
// Alte Nodes entfernen
|
||||
// Rückwärts-Lookup für diesen Chunk bereinigen
|
||||
geomNameToRef.entrySet().removeIf(e -> e.getValue()[0] == ci);
|
||||
|
||||
if (lod0Nodes[ci] != null) { lod0Nodes[ci].detachAllChildren(); stoneRoot.detachChild(lod0Nodes[ci]); }
|
||||
if (lod1Nodes[ci] != null) { lod1Nodes[ci].detachAllChildren(); stoneRoot.detachChild(lod1Nodes[ci]); }
|
||||
|
||||
@@ -302,32 +426,48 @@ public class StoneEditorState extends BaseAppState {
|
||||
Node n0 = new Node("stone_lod0_" + ci);
|
||||
Node n1 = new Node("stone_lod1_" + ci);
|
||||
|
||||
for (PlacedStone s : stones) {
|
||||
float roughness = (float) input.stoneTool.roughness.getValue();
|
||||
for (int si = 0; si < stones.size(); si++) {
|
||||
PlacedStone s = stones.get(si);
|
||||
float y = stoneWorldY(s);
|
||||
String name = "stone_ci" + ci + "_si" + si;
|
||||
|
||||
Geometry g0 = buildStoneGeom(s, y, 2); // LOD0: 2 Subdiv
|
||||
int sd0 = subdivForRadius(s.radius());
|
||||
Geometry g0 = buildStoneGeom(s, y, sd0, roughness, name);
|
||||
n0.attachChild(g0);
|
||||
geomNameToRef.put(name, new int[]{ci, si});
|
||||
|
||||
if (s.radius() * 2f > 1f) { // Durchmesser > 1m → LOD1
|
||||
Geometry g1 = buildStoneGeom(s, y, 1); // LOD1: 1 Subdiv
|
||||
if (s.radius() * 2f > 1f) {
|
||||
Geometry g1 = buildStoneGeom(s, y, Math.max(1, sd0 - 2), roughness, name + "_lod1");
|
||||
n1.attachChild(g1);
|
||||
}
|
||||
}
|
||||
|
||||
stoneRoot.attachChild(n0);
|
||||
stoneRoot.attachChild(n1);
|
||||
lod0Nodes[ci] = n0;
|
||||
lod1Nodes[ci] = n1;
|
||||
// Nur wenn LOD1-Geometrien vorhanden – sonst immer LOD0 benutzen
|
||||
if (n1.getQuantity() > 0) {
|
||||
stoneRoot.attachChild(n1);
|
||||
lod1Nodes[ci] = n1;
|
||||
} else {
|
||||
lod1Nodes[ci] = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Geometry buildStoneGeom(PlacedStone s, float worldY, int subdivisions) {
|
||||
Mesh mesh = buildStoneMesh(s.radius(), s.noiseSeed(), subdivisions);
|
||||
// Einsinken: Zentrum liegt bei terrain - sinkFraction * radius * 2 + radius (= terrain + radius*(1 - 2*sinkFraction))
|
||||
private Geometry buildStoneGeom(PlacedStone s, float worldY, int subdivisions, float roughness) {
|
||||
return buildStoneGeom(s, worldY, subdivisions, roughness, "stone");
|
||||
}
|
||||
|
||||
private Geometry buildStoneGeom(PlacedStone s, float worldY, int subdivisions, float roughness, String name) {
|
||||
Mesh mesh = buildStoneMesh(s.radius(), s.noiseSeed(), subdivisions, roughness);
|
||||
float yCenter = worldY + s.radius() * (1f - 2f * s.sinkFraction());
|
||||
Geometry g = new Geometry("stone", mesh);
|
||||
Geometry g = new Geometry(name, mesh);
|
||||
g.setMaterial(materialForSlot(s.textureSlot()));
|
||||
g.setLocalTranslation(s.x(), yCenter, s.z());
|
||||
g.rotate(0f, s.rotY() * FastMath.DEG_TO_RAD, 0f);
|
||||
Quaternion rot = new Quaternion();
|
||||
rot.fromAngles(s.rotX(), s.rotY() * FastMath.DEG_TO_RAD, s.rotZ());
|
||||
g.setLocalRotation(rot);
|
||||
return g;
|
||||
}
|
||||
|
||||
@@ -344,9 +484,10 @@ public class StoneEditorState extends BaseAppState {
|
||||
if (dist > CULL_DIST) {
|
||||
lod0Nodes[ci].setCullHint(Spatial.CullHint.Always);
|
||||
if (lod1Nodes[ci] != null) lod1Nodes[ci].setCullHint(Spatial.CullHint.Always);
|
||||
} else if (dist > LOD1_DIST) {
|
||||
} else if (dist > LOD1_DIST && lod1Nodes[ci] != null) {
|
||||
// Nur auf LOD1 wechseln wenn LOD1 tatsächlich Geometrien hat
|
||||
lod0Nodes[ci].setCullHint(Spatial.CullHint.Always);
|
||||
if (lod1Nodes[ci] != null) lod1Nodes[ci].setCullHint(Spatial.CullHint.Inherit);
|
||||
lod1Nodes[ci].setCullHint(Spatial.CullHint.Inherit);
|
||||
} else {
|
||||
lod0Nodes[ci].setCullHint(Spatial.CullHint.Inherit);
|
||||
if (lod1Nodes[ci] != null) lod1Nodes[ci].setCullHint(Spatial.CullHint.Always);
|
||||
@@ -354,6 +495,309 @@ public class StoneEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gizmo-System ─────────────────────────────────────────────────────────
|
||||
|
||||
// JME3-3.9: Cylinder-Achse ist +Z. geomRot dreht Z auf die Zielachse.
|
||||
private void buildGizmo() {
|
||||
arrowX = buildArrowNode(COL_X, "stoneArrowX",
|
||||
new Vector3f(1, 0, 0),
|
||||
new Quaternion().fromAngleAxis( FastMath.HALF_PI, Vector3f.UNIT_Y)); // Z→+X
|
||||
arrowY = buildArrowNode(COL_Y, "stoneArrowY",
|
||||
new Vector3f(0, 1, 0),
|
||||
new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X)); // Z→+Y
|
||||
arrowZ = buildArrowNode(COL_Z, "stoneArrowZ",
|
||||
new Vector3f(0, 0, -1),
|
||||
new Quaternion()); // Z→-Z
|
||||
gizmoNode.attachChild(arrowX);
|
||||
gizmoNode.attachChild(arrowY);
|
||||
gizmoNode.attachChild(arrowZ);
|
||||
|
||||
// Torus liegt default in XY-Ebene (Symmetrieachse=Z). Node-Rotation dreht in Zielebene.
|
||||
ringX = buildRingNode(COL_X, "stoneRingX",
|
||||
new Quaternion().fromAngleAxis( FastMath.HALF_PI, Vector3f.UNIT_Y)); // XY→YZ (dreht um X)
|
||||
ringY = buildRingNode(COL_Y, "stoneRingY",
|
||||
new Quaternion().fromAngleAxis( FastMath.HALF_PI, Vector3f.UNIT_X)); // XY→XZ (dreht um Y)
|
||||
ringZ = buildRingNode(COL_Z, "stoneRingZ",
|
||||
new Quaternion()); // XY-Ebene (dreht um Z)
|
||||
gizmoNode.attachChild(ringX);
|
||||
gizmoNode.attachChild(ringY);
|
||||
gizmoNode.attachChild(ringZ);
|
||||
}
|
||||
|
||||
private Node buildArrowNode(ColorRGBA color, String name, Vector3f dir, Quaternion geomRot) {
|
||||
Material mat = gizmoMat(color);
|
||||
|
||||
Cylinder shaftMesh = new Cylinder(2, 10, SHAFT_RADIUS, SHAFT_LEN, false);
|
||||
Geometry shaft = new Geometry(name + "_shaft", shaftMesh);
|
||||
shaft.setLocalRotation(geomRot);
|
||||
shaft.setLocalTranslation(dir.mult(SHAFT_LEN * 0.5f));
|
||||
shaft.setMaterial(mat);
|
||||
shaft.setQueueBucket(RenderQueue.Bucket.Translucent);
|
||||
|
||||
Cylinder headMesh = new Cylinder(2, 10, HEAD_RADIUS, HEAD_LEN, true);
|
||||
Geometry head = new Geometry(name + "_head", headMesh);
|
||||
head.setLocalRotation(geomRot);
|
||||
head.setLocalTranslation(dir.mult(SHAFT_LEN + HEAD_LEN * 0.5f));
|
||||
head.setMaterial(mat);
|
||||
head.setQueueBucket(RenderQueue.Bucket.Translucent);
|
||||
|
||||
Node node = new Node(name);
|
||||
node.attachChild(shaft);
|
||||
node.attachChild(head);
|
||||
return node;
|
||||
}
|
||||
|
||||
private Node buildRingNode(ColorRGBA color, String name, Quaternion nodeRot) {
|
||||
Material mat = gizmoMat(color);
|
||||
Geometry ring = new Geometry(name + "_ring", new Torus(48, 10, RING_TUBE, RING_RADIUS));
|
||||
ring.setMaterial(mat);
|
||||
ring.setQueueBucket(RenderQueue.Bucket.Translucent);
|
||||
Node node = new Node(name);
|
||||
node.setLocalRotation(nodeRot);
|
||||
node.attachChild(ring);
|
||||
return node;
|
||||
}
|
||||
|
||||
private Material gizmoMat(ColorRGBA color) {
|
||||
Material m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
m.setColor("Color", color);
|
||||
m.getAdditionalRenderState().setDepthTest(false);
|
||||
return m;
|
||||
}
|
||||
|
||||
private int pickGizmo(Ray ray) {
|
||||
CollisionResults hits = new CollisionResults();
|
||||
arrowX.collideWith(ray, hits); if (hits.size() > 0) return 0;
|
||||
hits.clear(); arrowY.collideWith(ray, hits); if (hits.size() > 0) return 1;
|
||||
hits.clear(); arrowZ.collideWith(ray, hits); if (hits.size() > 0) return 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int pickRing(Ray ray) {
|
||||
CollisionResults hits = new CollisionResults();
|
||||
ringX.collideWith(ray, hits); if (hits.size() > 0) return 0;
|
||||
hits.clear(); ringY.collideWith(ray, hits); if (hits.size() > 0) return 1;
|
||||
hits.clear(); ringZ.collideWith(ray, hits); if (hits.size() > 0) return 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void updateGizmoPosition() {
|
||||
if (selChunk < 0 || selIdx < 0) return;
|
||||
PlacedStone s = chunkStones[selChunk].get(selIdx);
|
||||
float worldY = stoneWorldY(s);
|
||||
float yCenter = worldY + s.radius() * (1f - 2f * s.sinkFraction());
|
||||
Vector3f pos = new Vector3f(s.x(), yCenter, s.z());
|
||||
gizmoNode.setLocalTranslation(pos);
|
||||
// Skalierung: Stein-Radius / RING_RADIUS — exakt wie SceneObjectState (maxR / RING_RADIUS)
|
||||
gizmoNode.setLocalScale(Math.max(0.01f, s.radius() / RING_RADIUS));
|
||||
}
|
||||
|
||||
private void updateGizmoVisibility() {
|
||||
boolean move = (selChunk >= 0) && (input.stoneEditTool == SharedInput.EDIT_TOOL_MOVE);
|
||||
boolean rot = (selChunk >= 0) && (input.stoneEditTool == SharedInput.EDIT_TOOL_ROTATE);
|
||||
arrowX.setCullHint(move ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
arrowY.setCullHint(move ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
arrowZ.setCullHint(move ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
ringX.setCullHint(rot ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
ringY.setCullHint(rot ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
ringZ.setCullHint(rot ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
gizmoNode.setCullHint((move || rot) ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
private static Vector3f arrowWorldDir(int idx) {
|
||||
return switch (idx) {
|
||||
case 0 -> new Vector3f(1, 0, 0);
|
||||
case 1 -> new Vector3f(0, 1, 0);
|
||||
default -> new Vector3f(0, 0, -1);
|
||||
};
|
||||
}
|
||||
|
||||
private void selectStone(int ci, int si) {
|
||||
selChunk = ci;
|
||||
selIdx = si;
|
||||
PlacedStone s = chunkStones[ci].get(si);
|
||||
float worldY = stoneWorldY(s);
|
||||
float yCenter = worldY + s.radius() * (1f - 2f * s.sinkFraction());
|
||||
selHighlight.setMesh(buildStoneMesh(s.radius(), s.noiseSeed(),
|
||||
subdivForRadius(s.radius()), (float) input.stoneTool.roughness.getValue()));
|
||||
selHighlight.setLocalTranslation(s.x(), yCenter, s.z());
|
||||
selHighlight.setLocalRotation(new Quaternion().fromAngles(
|
||||
s.rotX(), s.rotY() * FastMath.DEG_TO_RAD, s.rotZ()));
|
||||
selHighlight.setLocalScale(1.02f);
|
||||
selHighlight.setCullHint(Spatial.CullHint.Inherit);
|
||||
updateGizmoPosition();
|
||||
updateGizmoVisibility();
|
||||
broadcastSelection();
|
||||
}
|
||||
|
||||
private void deselectStone() {
|
||||
selChunk = -1;
|
||||
selIdx = -1;
|
||||
activeGizmo = -1;
|
||||
activeRing = -1;
|
||||
selHighlight.setCullHint(Spatial.CullHint.Always);
|
||||
gizmoNode.setCullHint(Spatial.CullHint.Always);
|
||||
broadcastSelection();
|
||||
}
|
||||
|
||||
private void broadcastSelection() {
|
||||
if (selChunk < 0 || selIdx < 0) {
|
||||
input.stoneSelectedInfo = null;
|
||||
} else {
|
||||
PlacedStone s = chunkStones[selChunk].get(selIdx);
|
||||
input.stoneSelectedInfo = s.x() + "|" + s.z() + "|" +
|
||||
s.rotX() + "|" + s.rotY() + "|" + s.rotZ() + "|" + s.radius();
|
||||
}
|
||||
input.stoneSelectionChanged = true;
|
||||
}
|
||||
|
||||
private void processEditModeClick(float jx, float jy) {
|
||||
Vector3f origin = cam.getWorldCoordinates(new Vector2f(jx, jy), 0f);
|
||||
Vector3f target = cam.getWorldCoordinates(new Vector2f(jx, jy), 1f);
|
||||
Ray ray = new Ray(origin, target.subtractLocal(origin).normalizeLocal());
|
||||
|
||||
// 1. Gizmo-Pick (nur wenn etwas selektiert ist und Gizmo sichtbar)
|
||||
if (selChunk >= 0) {
|
||||
if (input.stoneEditTool == SharedInput.EDIT_TOOL_MOVE) {
|
||||
int g = pickGizmo(ray);
|
||||
if (g >= 0) { activeGizmo = g; return; }
|
||||
} else {
|
||||
int r = pickRing(ray);
|
||||
if (r >= 0) { activeRing = r; return; }
|
||||
}
|
||||
}
|
||||
activeGizmo = -1;
|
||||
activeRing = -1;
|
||||
|
||||
// 2. Stein-Pick
|
||||
CollisionResults res = new CollisionResults();
|
||||
stoneRoot.collideWith(ray, res);
|
||||
if (res.size() > 0) {
|
||||
String rawName = res.getClosestCollision().getGeometry().getName();
|
||||
// LOD1-Geometrien heißen "<name>_lod1" – Suffix abschneiden für Lookup
|
||||
String name = rawName.endsWith("_lod1")
|
||||
? rawName.substring(0, rawName.length() - 5) : rawName;
|
||||
int[] ref = geomNameToRef.get(name);
|
||||
if (ref != null) { selectStone(ref[0], ref[1]); return; }
|
||||
}
|
||||
|
||||
// 3. Kein Treffer → Selektion aufheben
|
||||
deselectStone();
|
||||
}
|
||||
|
||||
private void handleMoveDrag(SharedInput.ObjectDrag drag) {
|
||||
if (selChunk < 0 || selIdx < 0 || activeGizmo < 0) return;
|
||||
|
||||
PlacedStone s = chunkStones[selChunk].get(selIdx);
|
||||
|
||||
// Pfeilrichtung auf Bildschirm projizieren (exakt wie SceneObjectState)
|
||||
Vector3f arrowDir = arrowWorldDir(activeGizmo);
|
||||
Vector3f origin = gizmoNode.getWorldTranslation();
|
||||
Vector3f scrO = cam.getScreenCoordinates(origin);
|
||||
Vector3f scrT_ = cam.getScreenCoordinates(origin.add(arrowDir));
|
||||
float sdx = scrT_.x - scrO.x;
|
||||
float sdy = -(scrT_.y - scrO.y); // JME Y↑, Drag Y↓ → invertieren
|
||||
float screenLen2 = sdx * sdx + sdy * sdy;
|
||||
|
||||
float magnitude;
|
||||
if (screenLen2 < 0.01f) {
|
||||
magnitude = -drag.dy() * PX_PER_WE;
|
||||
} else {
|
||||
magnitude = (drag.dx() * sdx + drag.dy() * sdy) / screenLen2;
|
||||
}
|
||||
|
||||
Vector3f worldDelta = arrowDir.mult(magnitude);
|
||||
float newX = s.x() + worldDelta.x;
|
||||
float newZ = s.z() + worldDelta.z;
|
||||
if (!Float.isFinite(getHeightAt(newX, newZ))) return;
|
||||
|
||||
// Y-Achse: sinkFraction anpassen (nach oben = weniger einsinken)
|
||||
float newSinkFraction = s.sinkFraction() - worldDelta.y / (2f * s.radius());
|
||||
|
||||
PlacedStone updated = new PlacedStone(newX, newZ, s.radius(),
|
||||
s.rotX(), s.rotY(), s.rotZ(), s.textureSlot(), newSinkFraction, s.noiseSeed());
|
||||
|
||||
int newCi = chunkIndex(newX, newZ);
|
||||
if (newCi < 0) return;
|
||||
|
||||
if (newCi != selChunk) {
|
||||
chunkStones[selChunk].remove(selIdx);
|
||||
dirtyChunks[selChunk] = true;
|
||||
chunkStones[newCi].add(updated);
|
||||
dirtyChunks[newCi] = true;
|
||||
selChunk = newCi;
|
||||
selIdx = chunkStones[newCi].size() - 1;
|
||||
} else {
|
||||
chunkStones[selChunk].set(selIdx, updated);
|
||||
dirtyChunks[selChunk] = true;
|
||||
}
|
||||
modified = true;
|
||||
|
||||
float nY = stoneWorldY(updated);
|
||||
float nCenter = nY + updated.radius() * (1f - 2f * updated.sinkFraction());
|
||||
selHighlight.setLocalTranslation(updated.x(), nCenter, updated.z());
|
||||
broadcastSelection();
|
||||
}
|
||||
|
||||
private void handleRotateDrag(SharedInput.ObjectDrag drag) {
|
||||
if (selChunk < 0 || selIdx < 0 || activeRing < 0) return;
|
||||
PlacedStone s = chunkStones[selChunk].get(selIdx);
|
||||
|
||||
// Tangent-basierte Winkel-Projektion (exakt wie SceneObjectState)
|
||||
Vector3f rotAxis = switch (activeRing) {
|
||||
case 0 -> new Vector3f(1, 0, 0); // Welt-X
|
||||
case 1 -> new Vector3f(0, 1, 0); // Welt-Y
|
||||
default -> new Vector3f(0, 0, 1); // Welt-Z
|
||||
};
|
||||
Vector3f center = gizmoNode.getWorldTranslation();
|
||||
Vector3f toCam = cam.getLocation().subtract(center).normalizeLocal();
|
||||
Vector3f tangent = rotAxis.cross(toCam);
|
||||
float tLen = tangent.length();
|
||||
float angle;
|
||||
if (tLen < 1e-4f) {
|
||||
angle = drag.dx() * DEG_PER_PX * FastMath.DEG_TO_RAD;
|
||||
} else {
|
||||
tangent.divideLocal(tLen);
|
||||
Vector3f scrC = cam.getScreenCoordinates(center);
|
||||
Vector3f scrT = cam.getScreenCoordinates(center.add(tangent));
|
||||
float sdx = scrT.x - scrC.x;
|
||||
float sdy = -(scrT.y - scrC.y);
|
||||
float sl2 = sdx * sdx + sdy * sdy;
|
||||
if (sl2 < 0.5f) {
|
||||
angle = drag.dx() * DEG_PER_PX * FastMath.DEG_TO_RAD;
|
||||
} else {
|
||||
float proj = (drag.dx() * sdx + drag.dy() * sdy) / sl2;
|
||||
angle = proj / RING_RADIUS;
|
||||
}
|
||||
}
|
||||
|
||||
float newRotX = s.rotX();
|
||||
float newRotY = s.rotY();
|
||||
float newRotZ = s.rotZ();
|
||||
switch (activeRing) {
|
||||
case 0 -> newRotX += angle;
|
||||
case 1 -> newRotY += angle * FastMath.RAD_TO_DEG; // rotY in Grad gespeichert
|
||||
case 2 -> newRotZ += angle;
|
||||
}
|
||||
|
||||
PlacedStone updated = new PlacedStone(s.x(), s.z(), s.radius(),
|
||||
newRotX, newRotY, newRotZ, s.textureSlot(), s.sinkFraction(), s.noiseSeed());
|
||||
chunkStones[selChunk].set(selIdx, updated);
|
||||
dirtyChunks[selChunk] = true;
|
||||
modified = true;
|
||||
selHighlight.setLocalRotation(new Quaternion().fromAngles(
|
||||
newRotX, newRotY * FastMath.DEG_TO_RAD, newRotZ));
|
||||
broadcastSelection();
|
||||
}
|
||||
|
||||
private void deleteSelectedStone() {
|
||||
if (selChunk < 0 || selIdx < 0) return;
|
||||
chunkStones[selChunk].remove(selIdx);
|
||||
dirtyChunks[selChunk] = true;
|
||||
modified = true;
|
||||
deselectStone();
|
||||
}
|
||||
|
||||
// ── Mesh-Generierung: Icosphere ───────────────────────────────────────────
|
||||
|
||||
private static final float PHI = (1f + (float) Math.sqrt(5)) / 2f;
|
||||
@@ -388,7 +832,7 @@ public class StoneEditorState extends BaseAppState {
|
||||
* Erzeugt einen Stein-Mesh mit der gegebenen Anzahl an Icosphere-Unterteilungen.
|
||||
* Noise-Verformung wird durch noiseSeed deterministisch gesteuert.
|
||||
*/
|
||||
private static Mesh buildStoneMesh(float radius, int noiseSeed, int subdivisions) {
|
||||
private static Mesh buildStoneMesh(float radius, int noiseSeed, int subdivisions, float roughness) {
|
||||
// 1. Icosphere aufbauen und unterteilen
|
||||
List<float[]> verts = new ArrayList<>(Arrays.asList(ICO_V));
|
||||
List<int[]> faces = new ArrayList<>(Arrays.asList(ICO_F));
|
||||
@@ -410,57 +854,111 @@ public class StoneEditorState extends BaseAppState {
|
||||
verts = nv; faces = nf;
|
||||
}
|
||||
|
||||
// 2. Noise-Verformung (entlang Vertex-Normal = Einheitskugel-Richtung)
|
||||
Random rng = new Random(noiseSeed);
|
||||
float ox = rng.nextFloat() * 50f, oy = rng.nextFloat() * 50f, oz = rng.nextFloat() * 50f;
|
||||
float freq = 2.5f + (Math.abs(noiseSeed) % 3); // 2.5–4.5
|
||||
float amp = 0.22f; // ±22 % Verformung
|
||||
// 2. Variantenreiche Verformung: individuelle Achsenskalierung + 3 Oktaven
|
||||
Random rng = new Random(noiseSeed);
|
||||
float ox = rng.nextFloat() * 50f;
|
||||
float oy = rng.nextFloat() * 50f;
|
||||
float oz = rng.nextFloat() * 50f;
|
||||
float flattenY = 0.50f + rng.nextFloat() * 0.45f;
|
||||
float scaleX = 0.80f + rng.nextFloat() * 0.70f;
|
||||
float scaleZ = 0.80f + rng.nextFloat() * 0.70f;
|
||||
float freq = 1.5f + (Math.abs(noiseSeed) % 4);
|
||||
float amp = roughness * 0.60f;
|
||||
|
||||
int nv = verts.size();
|
||||
float[] pos = new float[nv * 3];
|
||||
float[] nor = new float[nv * 3];
|
||||
float[] uv = new float[nv * 2];
|
||||
|
||||
for (int i = 0; i < nv; i++) {
|
||||
float[] unit = verts.get(i);
|
||||
float n = smoothNoise3D(unit[0] * freq + ox, unit[1] * freq + oy, unit[2] * freq + oz);
|
||||
float r = radius * (1f + n * amp);
|
||||
pos[i*3] = unit[0] * r;
|
||||
pos[i*3+1] = unit[1] * r;
|
||||
pos[i*3+2] = unit[2] * r;
|
||||
// UV: sphärisch
|
||||
uv[i*2] = (float)(Math.atan2(unit[2], unit[0]) / (2 * Math.PI) + 0.5);
|
||||
uv[i*2+1] = (float)(Math.asin(Math.max(-1, Math.min(1, unit[1]))) / Math.PI + 0.5);
|
||||
int numVerts = verts.size();
|
||||
float[] def = new float[numVerts * 3];
|
||||
for (int i = 0; i < numVerts; i++) {
|
||||
float[] u = verts.get(i);
|
||||
float n1 = smoothNoise3D(u[0]*freq+ox, u[1]*freq+oy, u[2]*freq+oz);
|
||||
float n2 = smoothNoise3D(u[0]*freq*2.5f+ox+11f, u[1]*freq*2.5f+oy+11f, u[2]*freq*2.5f+oz+11f);
|
||||
float n3 = smoothNoise3D(u[0]*freq*5.5f+ox+23f, u[1]*freq*5.5f+oy+23f, u[2]*freq*5.5f+oz+23f);
|
||||
float r = radius * (1f + (n1 * 0.55f + n2 * 0.30f + n3 * 0.15f) * amp);
|
||||
def[i*3] = u[0] * r * scaleX;
|
||||
def[i*3+1] = u[1] * r * flattenY;
|
||||
def[i*3+2] = u[2] * r * scaleZ;
|
||||
}
|
||||
|
||||
// 3. Vertex-Normalen aus Dreiecken akkumulieren
|
||||
for (int[] f : faces) {
|
||||
int ai = f[0]*3, bi = f[1]*3, ci = f[2]*3;
|
||||
float ax = pos[ai], ay = pos[ai+1], az = pos[ai+2];
|
||||
float bx = pos[bi], by = pos[bi+1], bz = pos[bi+2];
|
||||
float cx2= pos[ci], cy = pos[ci+1], cz = pos[ci+2];
|
||||
float nx = (by-ay)*(cz-az) - (bz-az)*(cy-ay);
|
||||
float ny = (bz-az)*(cx2-ax) - (bx-ax)*(cz-az);
|
||||
float nz = (bx-ax)*(cy-ay) - (by-ay)*(cx2-ax);
|
||||
for (int vi : f) { nor[vi*3] += nx; nor[vi*3+1] += ny; nor[vi*3+2] += nz; }
|
||||
}
|
||||
for (int i = 0; i < nv; i++) {
|
||||
float len = (float) Math.sqrt(nor[i*3]*nor[i*3] + nor[i*3+1]*nor[i*3+1] + nor[i*3+2]*nor[i*3+2]);
|
||||
if (len > 0) { nor[i*3] /= len; nor[i*3+1] /= len; nor[i*3+2] /= len; }
|
||||
// 3. Taubin-Glättung (lambda-mu, kein Mesh-Schrumpfen)
|
||||
{
|
||||
List<List<Integer>> adj = new ArrayList<>(numVerts);
|
||||
for (int i = 0; i < numVerts; i++) adj.add(new ArrayList<>(6));
|
||||
for (int[] f : faces) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int a = f[i], b = f[(i + 1) % 3];
|
||||
List<Integer> la = adj.get(a), lb = adj.get(b);
|
||||
if (!la.contains(b)) la.add(b);
|
||||
if (!lb.contains(a)) lb.add(a);
|
||||
}
|
||||
}
|
||||
float lambda = 0.5f, mu = -0.53f;
|
||||
for (int iter = 0; iter < 6; iter++) {
|
||||
float step = (iter % 2 == 0) ? lambda : mu;
|
||||
float[] next = new float[numVerts * 3];
|
||||
for (int i = 0; i < numVerts; i++) {
|
||||
List<Integer> nb = adj.get(i);
|
||||
float sx = 0, sy = 0, sz = 0;
|
||||
for (int n : nb) { sx += def[n*3]; sy += def[n*3+1]; sz += def[n*3+2]; }
|
||||
float inv = nb.isEmpty() ? 1f : 1f / nb.size();
|
||||
next[i*3] = def[i*3] + step * (sx*inv - def[i*3]);
|
||||
next[i*3+1] = def[i*3+1] + step * (sy*inv - def[i*3+1]);
|
||||
next[i*3+2] = def[i*3+2] + step * (sz*inv - def[i*3+2]);
|
||||
}
|
||||
def = next;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. JME3-Mesh zusammenbauen
|
||||
FloatBuffer pb = BufferUtils.createFloatBuffer(pos);
|
||||
FloatBuffer nb = BufferUtils.createFloatBuffer(nor);
|
||||
FloatBuffer ub = BufferUtils.createFloatBuffer(uv);
|
||||
java.nio.IntBuffer ib = BufferUtils.createIntBuffer(faces.size() * 3);
|
||||
for (int[] f : faces) ib.put(f[0]).put(f[1]).put(f[2]);
|
||||
// 4. Flat-Shading: jedes Dreieck bekommt eigene Vertices mit Flächennormale + Box-Map UV
|
||||
int fc = faces.size();
|
||||
float[] pos = new float[fc * 9];
|
||||
float[] nor = new float[fc * 9];
|
||||
float[] uv = new float[fc * 6];
|
||||
java.nio.IntBuffer ib = BufferUtils.createIntBuffer(fc * 3);
|
||||
for (int i = 0; i < fc * 3; i++) ib.put(i);
|
||||
ib.flip();
|
||||
|
||||
float uvScale = 0.5f / radius;
|
||||
for (int fi = 0; fi < fc; fi++) {
|
||||
int[] f = faces.get(fi);
|
||||
float ax = def[f[0]*3], ay = def[f[0]*3+1], az = def[f[0]*3+2];
|
||||
float bx = def[f[1]*3], by = def[f[1]*3+1], bz = def[f[1]*3+2];
|
||||
float cx = def[f[2]*3], cy = def[f[2]*3+1], cz = def[f[2]*3+2];
|
||||
float ex = bx-ax, ey = by-ay, ez = bz-az;
|
||||
float gx = cx-ax, gy = cy-ay, gz = cz-az;
|
||||
float nx = ey*gz-ez*gy, ny = ez*gx-ex*gz, nz = ex*gy-ey*gx;
|
||||
float nlen = (float) Math.sqrt(nx*nx+ny*ny+nz*nz);
|
||||
if (nlen > 0) { nx/=nlen; ny/=nlen; nz/=nlen; }
|
||||
|
||||
float anx = Math.abs(nx), any = Math.abs(ny), anz = Math.abs(nz);
|
||||
float uA, vA, uB, vB, uC, vC;
|
||||
if (anx >= any && anx >= anz) {
|
||||
uA=az*uvScale+0.5f; vA=ay*uvScale+0.5f;
|
||||
uB=bz*uvScale+0.5f; vB=by*uvScale+0.5f;
|
||||
uC=cz*uvScale+0.5f; vC=cy*uvScale+0.5f;
|
||||
} else if (any >= anx && any >= anz) {
|
||||
uA=ax*uvScale+0.5f; vA=az*uvScale+0.5f;
|
||||
uB=bx*uvScale+0.5f; vB=bz*uvScale+0.5f;
|
||||
uC=cx*uvScale+0.5f; vC=cz*uvScale+0.5f;
|
||||
} else {
|
||||
uA=ax*uvScale+0.5f; vA=ay*uvScale+0.5f;
|
||||
uB=bx*uvScale+0.5f; vB=by*uvScale+0.5f;
|
||||
uC=cx*uvScale+0.5f; vC=cy*uvScale+0.5f;
|
||||
}
|
||||
int p3 = fi*9, u2 = fi*6;
|
||||
pos[p3]=ax; pos[p3+1]=ay; pos[p3+2]=az;
|
||||
pos[p3+3]=bx; pos[p3+4]=by; pos[p3+5]=bz;
|
||||
pos[p3+6]=cx; pos[p3+7]=cy; pos[p3+8]=cz;
|
||||
nor[p3]=nx; nor[p3+1]=ny; nor[p3+2]=nz;
|
||||
nor[p3+3]=nx; nor[p3+4]=ny; nor[p3+5]=nz;
|
||||
nor[p3+6]=nx; nor[p3+7]=ny; nor[p3+8]=nz;
|
||||
uv[u2]=uA; uv[u2+1]=vA;
|
||||
uv[u2+2]=uB; uv[u2+3]=vB;
|
||||
uv[u2+4]=uC; uv[u2+5]=vC;
|
||||
}
|
||||
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setBuffer(Type.Position, 3, pb);
|
||||
mesh.setBuffer(Type.Normal, 3, nb);
|
||||
mesh.setBuffer(Type.TexCoord, 2, ub);
|
||||
mesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(pos));
|
||||
mesh.setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(nor));
|
||||
mesh.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(uv));
|
||||
mesh.setBuffer(Type.Index, 3, ib);
|
||||
mesh.updateBound();
|
||||
return mesh;
|
||||
@@ -498,6 +996,13 @@ public class StoneEditorState extends BaseAppState {
|
||||
|
||||
private static float lerp(float a, float b, float t) { return a + (b-a)*t; }
|
||||
|
||||
/** Unterteilungsstufe proportional zum Steinradius: kleiner Kiesel → weniger Faces, großer Boulder → mehr. */
|
||||
private static int subdivForRadius(float radius) {
|
||||
// r≈0.10m→2 (320), r≈0.25m→3 (1280), r≈0.50m→4 (5120), r≥1m→5 (20480)
|
||||
double logR = Math.log(Math.max(0.01, radius) / 0.1) / Math.log(2.0);
|
||||
return Math.max(1, Math.min(5, (int) Math.round(logR) + 2));
|
||||
}
|
||||
|
||||
// ── Materialien ───────────────────────────────────────────────────────────
|
||||
|
||||
private void reloadMaterials() {
|
||||
@@ -534,8 +1039,6 @@ public class StoneEditorState extends BaseAppState {
|
||||
return defaultMat;
|
||||
}
|
||||
|
||||
// ── Brush-Indicator ───────────────────────────────────────────────────────
|
||||
|
||||
private void updateBrushIndicator() {
|
||||
if (input.activeLayer != SharedInput.LAYER_STONE || input.mouseScreenX < 0
|
||||
|| isSingleStoneMode()) {
|
||||
@@ -573,7 +1076,6 @@ public class StoneEditorState extends BaseAppState {
|
||||
mesh.setBuffer(Type.Position, 3, pos);
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
|
||||
mesh.updateBound();
|
||||
|
||||
Geometry geo = new Geometry("stoneBrushIndicator", mesh);
|
||||
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(0.9f, 0.5f, 0.1f, 0.4f));
|
||||
@@ -587,14 +1089,9 @@ public class StoneEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
private Geometry buildPreviewGeom() {
|
||||
Mesh mesh = buildStoneMesh(0.5f, 0, 2);
|
||||
Mesh mesh = buildStoneMesh(0.5f, 0, subdivForRadius(0.5f), 0.40f);
|
||||
Geometry g = new Geometry("stonePreview", mesh);
|
||||
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(0.75f, 0.82f, 0.95f, 0.5f));
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
g.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
g.setMaterial(mat);
|
||||
g.setMaterial(defaultMat);
|
||||
g.setCullHint(Spatial.CullHint.Always);
|
||||
return g;
|
||||
}
|
||||
@@ -705,6 +1202,8 @@ public class StoneEditorState extends BaseAppState {
|
||||
input.stoneTool.texturePaths[i] = paths[i] != null ? paths[i] : "";
|
||||
reloadMaterials();
|
||||
}
|
||||
input.stoneTool.roughness.setValue(data.roughness());
|
||||
lastRoughness = data.roughness();
|
||||
for (PlacedStone s : data.stones()) {
|
||||
int ci = chunkIndex(s.x(), s.z());
|
||||
if (ci >= 0) { chunkStones[ci].add(s); dirtyChunks[ci] = true; }
|
||||
@@ -720,7 +1219,8 @@ public class StoneEditorState extends BaseAppState {
|
||||
try {
|
||||
List<PlacedStone> all = new ArrayList<>();
|
||||
for (List<PlacedStone> list : chunkStones) all.addAll(list);
|
||||
PlacedStoneIO.save(new PlacedStoneIO.StoneData(input.stoneTool.texturePaths, all));
|
||||
float roughness = (float) input.stoneTool.roughness.getValue();
|
||||
PlacedStoneIO.save(new PlacedStoneIO.StoneData(input.stoneTool.texturePaths, roughness, all));
|
||||
modified = false;
|
||||
log.info("[StoneEditorState] {} Steine gespeichert.", all.size());
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -1738,6 +1738,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
|| layer == SharedInput.LAYER_LOCATION_ZONES
|
||||
|| layer == SharedInput.LAYER_PLAY_TOOL
|
||||
|| layer == SharedInput.LAYER_VOXEL || layer == SharedInput.LAYER_STONE
|
||||
|| layer == SharedInput.LAYER_STONE_EDIT
|
||||
|| layer == SharedInput.LAYER_BED_LIEGE
|
||||
|| layer == SharedInput.LAYER_BENCH_SITZ
|
||||
|| layer == SharedInput.LAYER_AUSWAHL
|
||||
|
||||
@@ -8,6 +8,7 @@ public class StoneTool extends EditorTool {
|
||||
public final ToolParameter minSize = new ToolParameter("Min-Größe (m)", 0.3, 0.1, 5.0);
|
||||
public final ToolParameter maxSize = new ToolParameter("Max-Größe (m)", 1.5, 0.2, 10.0);
|
||||
public final ToolParameter density = new ToolParameter("Dichte (Klick)", 4.0, 1.0, 30.0);
|
||||
public final ToolParameter roughness = new ToolParameter("Rauigkeit", 0.40, 0.0, 1.0);
|
||||
|
||||
/** Texturpfade für bis zu 3 Slots; "" = kein Texture (Fallback: Grau). */
|
||||
public volatile String[] texturePaths = new String[]{"", "", ""};
|
||||
@@ -20,6 +21,6 @@ public class StoneTool extends EditorTool {
|
||||
@Override public ToolParameter getBrushRadius() { return brushRadius; }
|
||||
|
||||
@Override public List<ToolParameter> getParameters() {
|
||||
return List.of(brushRadius, minSize, maxSize, density);
|
||||
return List.of(brushRadius, minSize, maxSize, density, roughness);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package de.blight.editor.toolset.stone;
|
||||
|
||||
import de.blight.common.PlacedStoneIO;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.ToolContext;
|
||||
import de.blight.editor.toolset.UiHelper;
|
||||
import de.blight.editor.ui.TextureChooser;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class StoneController {
|
||||
|
||||
private final ToolContext ctx;
|
||||
private final SharedInput input;
|
||||
|
||||
// "Bearbeiten"-Tab Widgets – aktualisiert von onFrame()
|
||||
private Label selInfoLabel;
|
||||
private Label selPosLabel;
|
||||
private Label selRotLabel;
|
||||
private Button deleteBtn;
|
||||
|
||||
public StoneController(ToolContext ctx) {
|
||||
this.ctx = ctx;
|
||||
this.input = ctx.input();
|
||||
}
|
||||
|
||||
public void onFrame() {
|
||||
if (!input.stoneSelectionChanged) return;
|
||||
input.stoneSelectionChanged = false;
|
||||
Platform.runLater(this::refreshSelectionUI);
|
||||
}
|
||||
|
||||
/** Malen-Panel; setzt activeLayer=LAYER_STONE. */
|
||||
public VBox buildPanel() {
|
||||
input.activeLayer = SharedInput.LAYER_STONE;
|
||||
input.activeTool = input.stoneTool;
|
||||
|
||||
VBox panel = new VBox(8);
|
||||
panel.setPadding(new Insets(10));
|
||||
panel.setPrefWidth(270);
|
||||
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
|
||||
|
||||
Label title = new Label("Steine");
|
||||
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;");
|
||||
panel.getChildren().addAll(title, new Separator());
|
||||
panel.getChildren().add(buildMalenPanel());
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
/** Bearbeiten-Panel via F1; setzt activeLayer=LAYER_STONE_EDIT. */
|
||||
public void activateEditMode() {
|
||||
input.activeLayer = SharedInput.LAYER_STONE_EDIT;
|
||||
|
||||
VBox panel = new VBox(8);
|
||||
panel.setPadding(new Insets(10));
|
||||
panel.setPrefWidth(270);
|
||||
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
|
||||
|
||||
Label title = new Label("Steine bearbeiten");
|
||||
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;");
|
||||
panel.getChildren().addAll(title, new Separator());
|
||||
panel.getChildren().add(buildBearbeitenPanel());
|
||||
refreshSelectionUI();
|
||||
|
||||
ctx.setRightPanel().accept(panel);
|
||||
}
|
||||
|
||||
private VBox buildMalenPanel() {
|
||||
VBox box = new VBox(6);
|
||||
|
||||
// Parameter-Slider
|
||||
for (var p : input.stoneTool.getParameters()) {
|
||||
box.getChildren().add(UiHelper.paramSlider(p.getName(), p.getMin(), p.getMax(), p.getValue(),
|
||||
v -> p.setValue(v)));
|
||||
}
|
||||
box.getChildren().add(new Separator());
|
||||
|
||||
// Textur-Slots 0-2
|
||||
Label texTitle = new Label("Texturen");
|
||||
texTitle.setStyle("-fx-font-weight: bold; -fx-text-fill: #111;");
|
||||
box.getChildren().add(texTitle);
|
||||
|
||||
for (int i = 0; i < PlacedStoneIO.SLOT_COUNT; i++) {
|
||||
box.getChildren().add(buildTextureSlotRow(i));
|
||||
}
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
private HBox buildTextureSlotRow(int slot) {
|
||||
ImageView thumb = new ImageView();
|
||||
thumb.setFitWidth(36);
|
||||
thumb.setFitHeight(36);
|
||||
thumb.setPreserveRatio(true);
|
||||
refreshThumb(thumb, slot);
|
||||
|
||||
Label pathLabel = new Label(shortName(input.stoneTool.texturePaths[slot]));
|
||||
pathLabel.setStyle("-fx-text-fill: #555; -fx-font-size: 10;");
|
||||
pathLabel.setMaxWidth(120);
|
||||
pathLabel.setEllipsisString("...");
|
||||
|
||||
Button chooseBtn = new Button("...");
|
||||
chooseBtn.setTooltip(new Tooltip("Textur wählen (Slot " + (slot + 1) + ")"));
|
||||
chooseBtn.setOnAction(e -> {
|
||||
TextureChooser chooser = new TextureChooser(ctx.assetRoot(), true);
|
||||
chooser.initOwner(ctx.stageSupplier().get());
|
||||
chooser.showAndWait().ifPresent(path -> {
|
||||
input.stoneTool.texturePaths[slot] = path;
|
||||
input.stoneTexturesChanged = true;
|
||||
pathLabel.setText(shortName(path));
|
||||
refreshThumb(thumb, slot);
|
||||
});
|
||||
});
|
||||
|
||||
Button clearBtn = new Button("X");
|
||||
clearBtn.setTooltip(new Tooltip("Slot leeren"));
|
||||
clearBtn.setOnAction(e -> {
|
||||
input.stoneTool.texturePaths[slot] = "";
|
||||
input.stoneTexturesChanged = true;
|
||||
pathLabel.setText("");
|
||||
thumb.setImage(null);
|
||||
});
|
||||
|
||||
Label slotLabel = new Label("Slot " + (slot + 1));
|
||||
slotLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #333;");
|
||||
VBox info = new VBox(2, slotLabel, pathLabel);
|
||||
HBox row = new HBox(6, thumb, info, new Region(), chooseBtn, clearBtn);
|
||||
HBox.setHgrow(info, Priority.ALWAYS);
|
||||
row.setAlignment(javafx.geometry.Pos.CENTER_LEFT);
|
||||
return row;
|
||||
}
|
||||
|
||||
private VBox buildBearbeitenPanel() {
|
||||
VBox box = new VBox(8);
|
||||
|
||||
// Werkzeug-Toggle: Bewegen / Rotieren
|
||||
ToggleButton btnBewegen = new ToggleButton("Bewegen");
|
||||
ToggleButton btnRotieren = new ToggleButton("Rotieren");
|
||||
btnBewegen.setMaxWidth(Double.MAX_VALUE);
|
||||
btnRotieren.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(btnBewegen, Priority.ALWAYS);
|
||||
HBox.setHgrow(btnRotieren, Priority.ALWAYS);
|
||||
ToggleGroup tg = new ToggleGroup();
|
||||
btnBewegen.setToggleGroup(tg);
|
||||
btnRotieren.setToggleGroup(tg);
|
||||
if (input.stoneEditTool == SharedInput.EDIT_TOOL_MOVE) btnBewegen.setSelected(true);
|
||||
else btnRotieren.setSelected(true);
|
||||
btnBewegen.setOnAction(e -> {
|
||||
if (!btnBewegen.isSelected()) { btnBewegen.setSelected(true); return; }
|
||||
input.stoneEditTool = SharedInput.EDIT_TOOL_MOVE;
|
||||
});
|
||||
btnRotieren.setOnAction(e -> {
|
||||
if (!btnRotieren.isSelected()) { btnRotieren.setSelected(true); return; }
|
||||
input.stoneEditTool = SharedInput.EDIT_TOOL_ROTATE;
|
||||
});
|
||||
HBox toolBar = new HBox(4, btnBewegen, btnRotieren);
|
||||
toolBar.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
box.getChildren().add(toolBar);
|
||||
box.getChildren().add(new Separator());
|
||||
|
||||
// Selektion-Info
|
||||
selInfoLabel = new Label("Kein Stein selektiert");
|
||||
selInfoLabel.setStyle("-fx-text-fill: #555; -fx-font-size: 11;");
|
||||
selPosLabel = new Label("");
|
||||
selPosLabel.setStyle("-fx-text-fill: #333; -fx-font-size: 11;");
|
||||
selRotLabel = new Label("");
|
||||
selRotLabel.setStyle("-fx-text-fill: #333; -fx-font-size: 11;");
|
||||
box.getChildren().addAll(selInfoLabel, selPosLabel, selRotLabel);
|
||||
|
||||
box.getChildren().add(new Separator());
|
||||
|
||||
// Löschen
|
||||
deleteBtn = new Button("Stein loeschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setDisable(true);
|
||||
deleteBtn.setOnAction(e -> input.stoneDeleteRequested = true);
|
||||
box.getChildren().add(deleteBtn);
|
||||
|
||||
Label hint = new Label("Klick = Stein auswaehlen\nZiehen an Pfeil/Ring = verschieben/drehen");
|
||||
hint.setStyle("-fx-text-fill: #888; -fx-font-size: 10;");
|
||||
box.getChildren().add(hint);
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
private void refreshSelectionUI() {
|
||||
String info = input.stoneSelectedInfo;
|
||||
if (selInfoLabel == null) return;
|
||||
if (info == null) {
|
||||
selInfoLabel.setText("Kein Stein selektiert");
|
||||
selPosLabel.setText("");
|
||||
selRotLabel.setText("");
|
||||
if (deleteBtn != null) deleteBtn.setDisable(true);
|
||||
} else {
|
||||
String[] parts = info.split("\\|");
|
||||
if (parts.length >= 6) {
|
||||
float x = Float.parseFloat(parts[0]);
|
||||
float z = Float.parseFloat(parts[1]);
|
||||
float rotX = Float.parseFloat(parts[2]);
|
||||
float rotY = Float.parseFloat(parts[3]);
|
||||
float rotZ = Float.parseFloat(parts[4]);
|
||||
float r = Float.parseFloat(parts[5]);
|
||||
selInfoLabel.setText(String.format("Radius: %.2f m", r));
|
||||
selPosLabel.setText(String.format("X: %.1f Z: %.1f", x, z));
|
||||
selRotLabel.setText(String.format("rotX: %.1f rotY: %.1f rotZ: %.1f",
|
||||
(float)Math.toDegrees(rotX), rotY, (float)Math.toDegrees(rotZ)));
|
||||
}
|
||||
if (deleteBtn != null) deleteBtn.setDisable(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshThumb(ImageView iv, int slot) {
|
||||
String path = input.stoneTool.texturePaths[slot];
|
||||
if (path == null || path.isEmpty()) { iv.setImage(null); return; }
|
||||
try {
|
||||
File f = ctx.assetRoot().resolve(path).toFile();
|
||||
if (f.exists()) iv.setImage(new Image(f.toURI().toString(), 36, 36, true, true));
|
||||
} catch (Exception ex) { iv.setImage(null); }
|
||||
}
|
||||
|
||||
private static String shortName(String path) {
|
||||
if (path == null || path.isEmpty()) return "";
|
||||
int sl = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
|
||||
return sl >= 0 ? path.substring(sl + 1) : path;
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ public class TopBarController {
|
||||
de.blight.editor.toolset.game.GameController gameController,
|
||||
de.blight.editor.toolset.terrain.TerrainPanelController terrainController,
|
||||
de.blight.editor.toolset.object.ObjectController objectController,
|
||||
de.blight.editor.toolset.stone.StoneController stoneController,
|
||||
Runnable switchToVegetationGenerator,
|
||||
Runnable switchToEzTree,
|
||||
Runnable switchToTripo,
|
||||
@@ -39,7 +40,8 @@ public class TopBarController {
|
||||
|
||||
private final SharedInput input;
|
||||
private ToggleGroup layerGroup;
|
||||
private Runnable pendingTabSwitchAction = null;
|
||||
private Runnable pendingTabSwitchAction = null;
|
||||
private boolean suppressStonePanelBuild = false;
|
||||
|
||||
// ToggleButtons – pre-created so getters work before buildTop() is called
|
||||
private final ToggleButton auswahlBtn = new ToggleButton("[k] Auswahl");
|
||||
@@ -117,6 +119,7 @@ public class TopBarController {
|
||||
public ToggleButton getTextureBtn() { return textureBtn; }
|
||||
public ToggleButton getGrassBtn() { return grassBtn; }
|
||||
public ToggleButton getStoneBtn() { return stoneBtn; }
|
||||
public void selectStoneBtnSilently() { suppressStonePanelBuild = true; stoneBtn.setSelected(true); }
|
||||
public ToggleButton getObjPlaceBtn() { return objPlaceBtn; }
|
||||
public ToggleButton getAtmosphaereBtn() { return atmosphaereBtn; }
|
||||
public ToggleButton getWasserMergedBtn(){ return wasserMergedBtn; }
|
||||
@@ -293,12 +296,10 @@ public class TopBarController {
|
||||
}, 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);
|
||||
}, ownerStage));
|
||||
stoneBtn.setOnAction(e -> {
|
||||
if (suppressStonePanelBuild) { suppressStonePanelBuild = false; return; }
|
||||
guardVoxelExit(() -> ctx.setRightPanel().accept(deps.stoneController().buildPanel()), ownerStage);
|
||||
});
|
||||
objPlaceBtn.setOnAction(e -> guardVoxelExit(() -> {
|
||||
deps.assetController().expandModelsNode();
|
||||
deps.assetController().openOverlay();
|
||||
|
||||
@@ -20,6 +20,8 @@ public class ViewportController {
|
||||
private double prevDragX, prevDragY;
|
||||
private double objDragPrevX, objDragPrevY;
|
||||
private boolean objDragging = false;
|
||||
private double stoneDragPrevX, stoneDragPrevY;
|
||||
private boolean stoneDragging = false;
|
||||
private double editPressX, editPressY;
|
||||
private int editPressAction;
|
||||
private javafx.animation.Timeline editTimer;
|
||||
@@ -107,6 +109,9 @@ public class ViewportController {
|
||||
if (e.getButton() == MouseButton.PRIMARY && !bothDown) {
|
||||
input.objectClickQueue.offer(
|
||||
new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), false, e.isShiftDown()));
|
||||
// Parallel auf Steine testen (StoneEditorState liest diesen Queue)
|
||||
input.stoneAuswahlQueue.offer(
|
||||
new SharedInput.StoneEdit((float)e.getX(), (float)e.getY(), +1));
|
||||
objDragPrevX = e.getX(); objDragPrevY = e.getY();
|
||||
objDragging = true;
|
||||
} else if (e.getButton() == MouseButton.SECONDARY && !bothDown) {
|
||||
@@ -173,12 +178,28 @@ public class ViewportController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.activeLayer == SharedInput.LAYER_STONE_EDIT) {
|
||||
// Bearbeiten-Modus: Linksklick = Gizmo-Pick oder Stein selektieren
|
||||
if (!bothDown && e.getButton() == MouseButton.PRIMARY && !e.isAltDown()) {
|
||||
input.stoneEditQueue.offer(
|
||||
new SharedInput.StoneEdit((float) e.getX(), (float) e.getY(), +1));
|
||||
stoneDragPrevX = e.getX(); stoneDragPrevY = e.getY();
|
||||
stoneDragging = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.activeLayer == SharedInput.LAYER_STONE
|
||||
&& input.stoneTool.brushRadius.getValue() == 1.0) {
|
||||
// Single-Stone-Modus: nur Rechtsklick, einmalig ohne Timer
|
||||
if (e.getButton() == MouseButton.SECONDARY && !bothDown) {
|
||||
input.stoneEditQueue.offer(
|
||||
new SharedInput.StoneEdit((float) e.getX(), (float) e.getY(), -1));
|
||||
// Single-Stone-Modus: Linksklick = platzieren, Rechtsklick = neuen Stein würfeln
|
||||
if (!bothDown) {
|
||||
if (e.getButton() == MouseButton.PRIMARY && !e.isAltDown()) {
|
||||
input.stoneEditQueue.offer(
|
||||
new SharedInput.StoneEdit((float) e.getX(), (float) e.getY(), +1));
|
||||
} else if (e.getButton() == MouseButton.SECONDARY) {
|
||||
input.stoneEditQueue.offer(
|
||||
new SharedInput.StoneEdit((float) e.getX(), (float) e.getY(), -1));
|
||||
}
|
||||
}
|
||||
} else if (bothDown) {
|
||||
stopEditTimer();
|
||||
@@ -213,6 +234,17 @@ public class ViewportController {
|
||||
});
|
||||
|
||||
viewport.setOnMouseDragged(e -> {
|
||||
if (input.activeLayer == SharedInput.LAYER_STONE_EDIT) {
|
||||
if (stoneDragging && e.isPrimaryButtonDown()
|
||||
&& !e.isSecondaryButtonDown() && !e.isMiddleButtonDown() && !e.isAltDown()) {
|
||||
float dx = (float)(e.getX() - stoneDragPrevX);
|
||||
float dy = (float)(e.getY() - stoneDragPrevY);
|
||||
input.stoneDragQueue.offer(new SharedInput.ObjectDrag(dx, dy));
|
||||
stoneDragPrevX = e.getX(); stoneDragPrevY = e.getY();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isObjectMode()) {
|
||||
if (objDragging && e.isPrimaryButtonDown()
|
||||
&& !e.isSecondaryButtonDown() && !e.isMiddleButtonDown() && !e.isAltDown()) {
|
||||
@@ -302,6 +334,7 @@ public class ViewportController {
|
||||
|
||||
viewport.setOnMouseReleased(e -> {
|
||||
objDragging = false;
|
||||
stoneDragging = false;
|
||||
if (!isObjectMode()) { stopEditTimer(); }
|
||||
if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) {
|
||||
if (pathNetDragging) {
|
||||
|
||||
Reference in New Issue
Block a user