Stein Tool ein wenig überarbeitet

This commit is contained in:
2026-10-05 22:14:54 +02:00
parent 7069c5f632
commit c3e028b9f8
47 changed files with 1049 additions and 145 deletions

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {