Platzierung und Bearbeitung von OPbjekten angepasst

This commit is contained in:
2026-08-10 14:41:44 +02:00
parent 294a080a3c
commit 753af2dc13
5 changed files with 306 additions and 67 deletions

View File

@@ -2398,6 +2398,15 @@ public class EditorApp extends Application {
}
inner.getChildren().add(grid);
// ── Terrain-Ausrichten ────────────────────────────────────────────────
inner.getChildren().addAll(sectionTitle("Terrain-Ausrichten"), new Separator());
CheckBox alignCB = new CheckBox("An Terrain angleichen");
alignCB.setSelected(input.terrainAlignEnabled);
alignCB.setStyle("-fx-text-fill: #111111;");
alignCB.setOnAction(e -> input.terrainAlignEnabled = alignCB.isSelected());
inner.getChildren().add(alignCB);
inner.getChildren().add(styledHint("Passt rotX/rotZ an die Geländesteigung an"));
// ── Vertex-Snap (beim Platzieren von custom Meshes) ───────────────────
inner.getChildren().addAll(sectionTitle("Vertex-Snap"), new Separator());
CheckBox placeSnapCB = new CheckBox("Snap aktiv");
@@ -2467,7 +2476,7 @@ public class EditorApp extends Application {
record ToolEntry(String label, String tooltip, int mode) {}
var tools = List.of(
new ToolEntry("Bewegen", "Objekte entlang X/Y/Z verschieben", SharedInput.EDIT_TOOL_MOVE),
new ToolEntry("Rotieren", "Objekte rotieren (noch nicht implementiert)", SharedInput.EDIT_TOOL_ROTATE),
new ToolEntry("Rotieren", "Objekte um X/Y/Z rotieren (Ring-Gizmo)", SharedInput.EDIT_TOOL_ROTATE),
new ToolEntry("Skalieren", "Objekte skalieren (noch nicht implementiert)", SharedInput.EDIT_TOOL_SCALE)
);
@@ -2481,8 +2490,7 @@ public class EditorApp extends Application {
if (t.mode() == input.objectEditTool) btn.setSelected(true);
int mode = t.mode();
btn.setOnAction(e -> input.objectEditTool = mode);
// Noch nicht implementierte Tools ausgegraut
if (t.mode() != SharedInput.EDIT_TOOL_MOVE) btn.setStyle("-fx-opacity: 0.55;");
if (t.mode() == SharedInput.EDIT_TOOL_SCALE) btn.setStyle("-fx-opacity: 0.55;");
bar.getChildren().add(btn);
}
bar.setStyle("-fx-font-size: 11;");

View File

@@ -382,6 +382,10 @@ public class SharedInput {
/** Status-/Fehlermeldungen von JME an JavaFX-Statusleiste. */
public volatile String consoleOutput = null;
// ── Terrain-Ausrichten ────────────────────────────────────────────────────
/** Wenn true: Objekte beim Platzieren an die Geländeneigung anpassen (rotX/rotZ). */
public volatile boolean terrainAlignEnabled = false;
// ── Vertex-Snap ───────────────────────────────────────────────────────────
/** Wenn true: Punkte, die beim Ziehen in Snap-Radius geraten, verschmelzen. */
public volatile boolean vertexSnapEnabled = false;

View File

@@ -94,12 +94,20 @@ public class SceneObjectState extends BaseAppState {
private Node arrowX, arrowY, arrowZ; // Verschiebe-Pfeile
private Node ringX, ringY, ringZ; // Rotationsringe
/** Gecachter Gizmo-Radius pro Objekt-Node (berechnet vor Rotation → rotationsunabhängig). */
private final java.util.Map<Node, Float> cachedGizmoRadii = new java.util.HashMap<>();
private final List<Integer> selectedIndices = new ArrayList<>();
private int activeGizmo = -1; // 0=X,1=Y,2=Z (Verschieben); -1=keins
private int activeRing = -1; // 0=X,1=Y,2=Z (Rotieren); -1=keins
private Node previewNode;
private String previewModelPath; // gecachter Pfad, um Reload zu vermeiden
private final List<Material> previewMaterials = new ArrayList<>();
private boolean previewValid = true;
private static final ColorRGBA PREVIEW_COL_VALID = new ColorRGBA(0.3f, 0.8f, 1.0f, 1f);
private static final ColorRGBA PREVIEW_COL_INVALID = new ColorRGBA(1.0f, 0.2f, 0.1f, 1f);
// ── Baum-Ordner-Modus ────────────────────────────────────────────────────
private java.util.List<String> folderTreeAssetPaths = new java.util.ArrayList<>();
@@ -122,6 +130,12 @@ public class SceneObjectState extends BaseAppState {
private final List<Material> pbrMaterials = new ArrayList<>();
// ── Bett-Liegefläche ─────────────────────────────────────────────────────
// ── Terrain-Level-Indikator ───────────────────────────────────────────────
/** Flacher Ring bei Terrain-Höhe unter dem selektierten Objekt. */
private Node terrainRingNode = null;
private Node terrainStickNode = null;
private Material terrainIndicatorMat = null;
/** Visualisierungspfeil für die Liegefläche (1,8 m); wird nur gezeigt wenn Bett-Objekt gewählt. */
private Node bedArrowNode = null;
/** UUID des Bettes, für das der Pfeil gerade angezeigt wird. */
@@ -282,6 +296,8 @@ public class SceneObjectState extends BaseAppState {
subOverlay.setCullHint(Spatial.CullHint.Always);
rootNode.attachChild(subOverlay);
buildTerrainLevelIndicator();
bedArrowNode = new Node("bedArrow");
bedArrowNode.setCullHint(Spatial.CullHint.Always);
rootNode.attachChild(bedArrowNode);
@@ -297,6 +313,8 @@ public class SceneObjectState extends BaseAppState {
gizmoNode.removeFromParent();
previewNode.removeFromParent();
subOverlay.removeFromParent();
if (terrainRingNode != null) terrainRingNode.removeFromParent();
if (terrainStickNode != null) terrainStickNode.removeFromParent();
bedArrowNode.removeFromParent();
benchArrowNode.removeFromParent();
}
@@ -320,7 +338,7 @@ public class SceneObjectState extends BaseAppState {
// Gizmo-Sichtbarkeit immer aktualisieren (auch wenn Layer wechselt)
updateGizmoVisibility();
updateGizmoOrientation();
updateTerrainLevelIndicator();
// Baum- und Farn-Shader mit Sonnen- und Windwerten versorgen; PBR-Emissive setzen
if (!sceneLitMaterials.isEmpty() || !windMaterials.isEmpty() || !pbrMaterials.isEmpty()) {
@@ -471,7 +489,7 @@ public class SceneObjectState extends BaseAppState {
String modelPath = input.pendingModelPath;
if (input.activeLayer != SharedInput.LAYER_OBJECTS || modelPath == null
|| input.mouseScreenX < 0 || terrain == null) {
|| input.mouseScreenX < 0) {
previewNode.setCullHint(Spatial.CullHint.Always);
return;
}
@@ -479,6 +497,8 @@ public class SceneObjectState extends BaseAppState {
if (!modelPath.equals(previewModelPath)) {
previewNode.detachAllChildren();
previewModelPath = modelPath;
previewMaterials.clear();
previewValid = true;
try {
Spatial model = modelPath.startsWith("@")
? createPrimitiveSpatial(modelPath.substring(1))
@@ -496,18 +516,50 @@ public class SceneObjectState extends BaseAppState {
float jmeY = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY;
Ray ray = screenToRay(jmeX, jmeY);
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() == 0) {
// Raycast: Basis-Terrain + gebackenes Voxel-Terrain
Vector3f pt = null;
float bestDist = Float.MAX_VALUE;
if (terrain != null) {
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() > 0) {
CollisionResult cr = hits.getClosestCollision();
pt = cr.getContactPoint();
bestDist = cr.getDistance();
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f[] hit = smes.raycastGeometryWithNormal(ray);
if (hit != null) {
float d = ray.getOrigin().distance(hit[0]);
if (d < bestDist) {
pt = hit[0];
}
}
}
if (pt == null) {
previewNode.setCullHint(Spatial.CullHint.Always);
return;
}
Vector3f pt = hits.getClosestCollision().getContactPoint();
if (input.vertexSnapEnabled && modelPath.startsWith("@")) {
Vector3f snapped = findNearestVertexWorld(pt, input.vertexSnapRadius);
if (snapped != null) pt = snapped;
}
// Gültigkeitsprüfung: ungebackenes Voxel → rot
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
boolean valid = (ves == null)
|| (ves.terrainTypeAt(pt.x, pt.z) != VoxelEditorState.TerrainType.VOXEL_UNBAKED);
if (valid != previewValid) {
previewValid = valid;
setPreviewColor(valid ? PREVIEW_COL_VALID : PREVIEW_COL_INVALID);
}
previewNode.setLocalTranslation(pt.x, pt.y, pt.z);
float totalRotY = (input.treeFolderPath != null ? currentRandomRotY : 0f) + previewRotY;
com.jme3.math.Quaternion rot = new com.jme3.math.Quaternion();
@@ -558,11 +610,12 @@ public class SceneObjectState extends BaseAppState {
private void applyPreviewMaterial(Spatial s) {
if (s instanceof Geometry geo) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0.3f, 0.8f, 1.0f, 1f));
mat.setColor("Color", PREVIEW_COL_VALID);
mat.getAdditionalRenderState().setWireframe(true);
mat.getAdditionalRenderState().setDepthTest(false);
geo.setMaterial(mat);
geo.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Transparent);
previewMaterials.add(mat);
} else if (s instanceof Node n) {
for (Spatial child : new java.util.ArrayList<>(n.getChildren())) {
applyPreviewMaterial(child);
@@ -570,6 +623,12 @@ public class SceneObjectState extends BaseAppState {
}
}
private void setPreviewColor(ColorRGBA color) {
for (Material mat : previewMaterials) {
mat.setColor("Color", color);
}
}
// ── Klick-Handling ────────────────────────────────────────────────────────
private void handleClick(SharedInput.ObjectClick click) {
@@ -629,21 +688,56 @@ public class SceneObjectState extends BaseAppState {
if (input.activeLayer != SharedInput.LAYER_OBJECTS) { deselectAll(); return; }
String modelPath = input.pendingModelPath;
if (terrain == null) return;
CollisionResults terrHits = new CollisionResults();
terrain.collideWith(ray, terrHits);
if (terrHits.size() == 0) return;
// Raycast: Basis-Terrain + gebackenes Voxel-Terrain nächsten Treffer ermitteln
Vector3f pt = null;
Vector3f ptNormal = new Vector3f(Vector3f.UNIT_Y);
float bestDist = Float.MAX_VALUE;
if (terrain != null) {
CollisionResults terrHits = new CollisionResults();
terrain.collideWith(ray, terrHits);
if (terrHits.size() > 0) {
CollisionResult cr = terrHits.getClosestCollision();
pt = cr.getContactPoint();
Vector3f cn = cr.getContactNormal();
if (cn != null && cn.lengthSquared() > 0.01f) {
ptNormal = cn.normalize();
}
bestDist = cr.getDistance();
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f[] hit = smes.raycastGeometryWithNormal(ray);
if (hit != null) {
float d = ray.getOrigin().distance(hit[0]);
if (d < bestDist) {
pt = hit[0];
ptNormal = hit[1];
bestDist = d;
}
}
}
if (pt == null) { return; }
if (modelPath == null) { deselectAll(); return; }
Vector3f pt = terrHits.getClosestCollision().getContactPoint();
// Platzierung auf ungebackenem Voxel-Terrain blockieren
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null && ves.terrainTypeAt(pt.x, pt.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) {
setStatus("Platzierung nicht möglich: Voxel-Terrain zuerst backen!");
return;
}
if (input.vertexSnapEnabled && modelPath.startsWith("@")) {
Vector3f snapped = findNearestVertexWorld(pt, input.vertexSnapRadius);
if (snapped != null) pt = snapped;
}
previewNode.setCullHint(Spatial.CullHint.Always);
float rotY = (input.treeFolderPath != null ? currentRandomRotY : 0f) + previewRotY;
placeObject(modelPath, pt.x, pt.z, pt.y, rotY);
placeObject(modelPath, pt.x, pt.z, pt.y, rotY, ptNormal);
if (input.treeFolderPath != null) {
applyRandomTree();
previewRotY = 0f;
@@ -652,7 +746,7 @@ public class SceneObjectState extends BaseAppState {
// ── Objekt platzieren ────────────────────────────────────────────────────
private void placeObject(String modelPath, float wx, float wz, float wy, float rotY) {
private void placeObject(String modelPath, float wx, float wz, float wy, float rotY, Vector3f terrNormal) {
// Meta-Defaults anwenden wenn vorhanden
de.blight.common.ModelMeta meta = null;
if (!modelPath.startsWith("@")) {
@@ -715,7 +809,20 @@ public class SceneObjectState extends BaseAppState {
}
}
}
so.setRotation(0f, rotY, 0f);
// Terrain-Ausrichten: volle Neigungsanpassung via Quaternion (Y → Terrain-Normale)
Quaternion placementRot = new Quaternion().fromAngleAxis(rotY, Vector3f.UNIT_Y);
if (input.terrainAlignEnabled && terrNormal != null && terrNormal.y < 0.9999f) {
Vector3f axis = Vector3f.UNIT_Y.cross(terrNormal);
float axisLen = axis.length();
if (axisLen > 1e-5f) {
axis.divideLocal(axisLen);
float tiltAngle = FastMath.acos(FastMath.clamp(terrNormal.y, -1f, 1f));
Quaternion qTilt = new Quaternion().fromAngleAxis(tiltAngle, axis);
placementRot = qTilt.mult(placementRot);
}
}
float[] eulers = placementRot.toAngles(null);
so.setRotation(eulers[0], eulers[1], eulers[2]);
so.setScale(defaultScale);
so.castShadow = defaultCast;
so.receiveShadow = defaultReceive;
@@ -724,11 +831,7 @@ public class SceneObjectState extends BaseAppState {
Node node = loadModelNode(modelPath, wx, wy + placementOffY, wz);
node.setLocalScale(defaultScale);
if (rotY != 0f) {
Quaternion q = new Quaternion();
q.fromAngleAxis(rotY, Vector3f.UNIT_Y);
node.setLocalRotation(q);
}
node.setLocalRotation(placementRot);
objNodes.add(node);
objectRoot.attachChild(node);
collectLitMaterials(node);
@@ -866,6 +969,16 @@ public class SceneObjectState extends BaseAppState {
node.attachChild(box);
}
node.setLocalTranslation(wx, wy, wz);
// Radius vor Rotation cachen getWorldBound() ist nach Rotation größer (AABB-Effekt).
node.updateGeometricState();
BoundingVolume initBv = node.getWorldBound();
float initR;
if (initBv instanceof BoundingSphere bsI) initR = bsI.getRadius();
else if (initBv instanceof BoundingBox bbI) initR = bbI.getExtent(null).length();
else initR = 0.5f;
cachedGizmoRadii.put(node, Math.max(0.5f, initR));
return node;
}
@@ -1001,49 +1114,119 @@ public class SceneObjectState extends BaseAppState {
}
}
/**
* Richtet die drei Translationspfeile so aus, dass sie immer entlang der
* Kamera-Achsen zeigen (screen-right, screen-up, screen-depth).
*/
private void updateGizmoOrientation() {
Vector3f camRight = cam.getLeft().negate();
Vector3f camUp = cam.getUp();
Vector3f camForward = cam.getDirection();
// arrowX wurde mit dir=(1,0,0) gebaut → drehen auf camRight
arrowX.setLocalRotation(rotFromTo(new Vector3f(1, 0, 0), camRight));
// arrowY wurde mit dir=(0,1,0) gebaut → drehen auf camUp
arrowY.setLocalRotation(rotFromTo(new Vector3f(0, 1, 0), camUp));
// arrowZ wurde mit dir=(0,0,-1) gebaut → drehen auf camForward (Tiefe)
arrowZ.setLocalRotation(rotFromTo(new Vector3f(0, 0, -1), camForward));
}
/** Gibt die aktuelle Weltrichtung des Gizmo-Pfeils mit Index {@code idx} zurück. */
/** Gibt die Weltrichtung des Gizmo-Pfeils mit Index {@code idx} zurück (Welt-Achsen: X/Y/-Z). */
private Vector3f getArrowWorldDir(int idx) {
Node node = switch (idx) { case 0 -> arrowX; case 1 -> arrowY; default -> arrowZ; };
Vector3f localDir = switch (idx) {
return switch (idx) {
case 0 -> new Vector3f(1, 0, 0);
case 1 -> new Vector3f(0, 1, 0);
default -> new Vector3f(0, 0, -1);
};
return node.getWorldRotation().mult(localDir);
}
/** Kürzeste Quaternion-Rotation von Einheitsvektor {@code from} nach {@code to}. */
private static Quaternion rotFromTo(Vector3f from, Vector3f to) {
Vector3f cross = from.cross(to);
float dot = from.dot(to);
if (cross.lengthSquared() < 1e-6f) {
if (dot > 0f) return new Quaternion(); // gleiche Richtung
// Entgegengesetzte Richtung: 180° um eine senkrechte Achse
Vector3f perp = (Math.abs(from.x) < 0.9f)
? Vector3f.UNIT_X.cross(from).normalizeLocal()
: Vector3f.UNIT_Y.cross(from).normalizeLocal();
return new Quaternion().fromAngleAxis(FastMath.PI, perp);
// ── Terrain-Level-Indikator ───────────────────────────────────────────────
private static final float TL_RING_RADIUS = 1.4f;
private static final float TL_TUBE_RADIUS = 0.07f;
private static final float TL_STICK_RADIUS = 0.04f;
private void buildTerrainLevelIndicator() {
terrainIndicatorMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
terrainIndicatorMat.setColor("Color", new ColorRGBA(0.2f, 1f, 0.2f, 1f));
terrainIndicatorMat.getAdditionalRenderState().setDepthTest(false);
// Flacher Ring auf der XZ-Ebene (Torus liegt default in XY → 90° um X kippen)
Geometry ringGeo = new Geometry("terrainRing",
new Torus(32, 8, TL_TUBE_RADIUS, TL_RING_RADIUS));
ringGeo.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X));
ringGeo.setMaterial(terrainIndicatorMat);
ringGeo.setQueueBucket(RenderQueue.Bucket.Transparent);
terrainRingNode = new Node("terrainRingNode");
terrainRingNode.attachChild(ringGeo);
terrainRingNode.setCullHint(Spatial.CullHint.Always);
rootNode.attachChild(terrainRingNode);
// Dünner Stab von Terrain-Level bis Objekt (Zylinder entlang Y, Höhe = 1 → wird skaliert)
Cylinder stickMesh = new Cylinder(2, 8, TL_STICK_RADIUS, 1f, true);
Geometry stickGeo = new Geometry("terrainStick", stickMesh);
// Zylinder liegt default entlang Z → um X drehen damit er entlang Y steht
stickGeo.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X));
stickGeo.setMaterial(terrainIndicatorMat);
stickGeo.setQueueBucket(RenderQueue.Bucket.Transparent);
terrainStickNode = new Node("terrainStickNode");
terrainStickNode.attachChild(stickGeo);
terrainStickNode.setCullHint(Spatial.CullHint.Always);
rootNode.attachChild(terrainStickNode);
}
private void updateTerrainLevelIndicator() {
if (terrainRingNode == null) return;
if (selectedIndices.isEmpty() || input.activeLayer != SharedInput.LAYER_OBJECTS_EDIT) {
terrainRingNode.setCullHint(Spatial.CullHint.Always);
terrainStickNode.setCullHint(Spatial.CullHint.Always);
return;
}
int idx = primaryIdx();
if (idx < 0) {
terrainRingNode.setCullHint(Spatial.CullHint.Always);
terrainStickNode.setCullHint(Spatial.CullHint.Always);
return;
}
SceneObject so = objects.get(idx);
float wx = so.getWorldX(), wz = so.getWorldZ(), wy = so.getGroundY();
// Raycast senkrecht nach unten von deutlich oberhalb des Objekts
Ray downRay = new Ray(new Vector3f(wx, wy + 500f, wz), new Vector3f(0f, -1f, 0f));
float terrainY = Float.NEGATIVE_INFINITY;
if (terrain != null) {
CollisionResults hits = new CollisionResults();
terrain.collideWith(downRay, hits);
if (hits.size() > 0) {
terrainY = Math.max(terrainY, hits.getClosestCollision().getContactPoint().y);
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f[] hit = smes.raycastGeometryWithNormal(downRay);
if (hit != null) {
terrainY = Math.max(terrainY, hit[0].y);
}
}
if (terrainY == Float.NEGATIVE_INFINITY) {
terrainRingNode.setCullHint(Spatial.CullHint.Always);
terrainStickNode.setCullHint(Spatial.CullHint.Always);
return;
}
// Farbe: grün = Objekt über Terrain, rot = Objekt versinkt
boolean above = wy >= terrainY - 0.02f;
ColorRGBA col = above
? new ColorRGBA(0.2f, 1f, 0.2f, 1f)
: new ColorRGBA(1f, 0.15f, 0.1f, 1f);
terrainIndicatorMat.setColor("Color", col);
// Ring leicht über Terrain-Oberfläche platzieren (Anti-Z-Fighting)
terrainRingNode.setLocalTranslation(wx, terrainY + 0.05f, wz);
terrainRingNode.setCullHint(Spatial.CullHint.Inherit);
// Stab zwischen Terrain-Level und Objekt-Y
float height = wy - terrainY;
if (Math.abs(height) > 0.05f) {
float midY = terrainY + height * 0.5f;
terrainStickNode.setLocalTranslation(wx, midY, wz);
terrainStickNode.setLocalScale(1f, Math.abs(height), 1f);
terrainStickNode.setCullHint(Spatial.CullHint.Inherit);
} else {
terrainStickNode.setCullHint(Spatial.CullHint.Always);
}
// Halber-Winkel-Trick für Einheitsvektoren: q = (cross, 1+dot), normalisieren
Quaternion q = new Quaternion(cross.x, cross.y, cross.z, 1f + dot);
return q.normalizeLocal();
}
/** Zeigt/versteckt Pfeile oder Ringe abhängig von Selektion und aktivem Werkzeug. */
@@ -1152,7 +1335,38 @@ public class SceneObjectState extends BaseAppState {
private void handleRotateDrag(SharedInput.ObjectDrag drag) {
if (activeRing < 0) return;
float angle = drag.dx() * DEG_PER_PX * FastMath.DEG_TO_RAD;
// Rotationswinkel per Bildschirm-Projektion des Ring-Tangentenvektors bestimmen.
// X- und Z-Ring folgen der Y-Rotation des Objekts (lokale Achsen), Y-Ring bleibt in Welt-Y.
int pidx = primaryIdx();
float objRotY = (pidx >= 0) ? objects.get(pidx).getRotY() : 0f;
Quaternion qObjY = new Quaternion().fromAngleAxis(objRotY, Vector3f.UNIT_Y);
Vector3f rotAxis = switch (activeRing) {
case 0 -> qObjY.mult(new Vector3f(1, 0, 0)); // lokales X des Objekts
case 1 -> new Vector3f(0, 1, 0); // immer Welt-Y
default -> qObjY.mult(new Vector3f(0, 0, 1)); // lokales Z des Objekts
};
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); // JME Y↑, Drag Y↓ → umkehren
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;
}
}
int mode = input.objectSelectionMode;
if (mode != SharedInput.SEL_MODE_OBJECT && subSelGeom != null) {
@@ -1238,7 +1452,7 @@ public class SceneObjectState extends BaseAppState {
Geometry ring = new Geometry(name + "_ring",
new Torus(48, 10, RING_TUBE, RING_RADIUS));
ring.setMaterial(mat);
ring.setQueueBucket(RenderQueue.Bucket.Transparent);
ring.setQueueBucket(RenderQueue.Bucket.Translucent);
Node node = new Node(name);
node.setLocalRotation(nodeRot);
@@ -1261,14 +1475,14 @@ public class SceneObjectState extends BaseAppState {
shaft.setLocalRotation(geomRot);
shaft.setLocalTranslation(dir.mult(SHAFT_LEN * 0.5f));
shaft.setMaterial(mat);
shaft.setQueueBucket(RenderQueue.Bucket.Transparent);
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.Transparent);
head.setQueueBucket(RenderQueue.Bucket.Translucent);
Node node = new Node(name);
node.attachChild(shaft);
@@ -1320,20 +1534,27 @@ public class SceneObjectState extends BaseAppState {
}
// Objekt-Modus: Gizmo am Schwerpunkt aller selektierten Objekte
// Radius aus Cache (unrotiert) vermeidet wachsende Ringe beim Kippen des Objekts.
float cx = 0, cy = 0, cz = 0, maxR = 0.5f;
for (int i : selectedIndices) {
Vector3f wt = objNodes.get(i).getWorldTranslation();
cx += wt.x; cy += wt.y; cz += wt.z;
BoundingVolume bv = objNodes.get(i).getWorldBound();
float r;
if (bv instanceof BoundingSphere bs) r = bs.getRadius();
else if (bv instanceof BoundingBox bb) r = bb.getExtent(null).length();
else r = 0.5f;
float r = cachedGizmoRadii.getOrDefault(objNodes.get(i), 0.5f);
if (r > maxR) maxR = r;
}
int n = selectedIndices.size();
gizmoNode.setLocalTranslation(cx / n, cy / n, cz / n);
gizmoNode.setLocalScale(Math.max(0.01f, maxR / RING_RADIUS));
// Ringe an die Y-Rotation des primären Objekts ausrichten:
// rotX/rotZ entsprechen Rotationen um die lokalen Achsen des Objekts (nach Y-Rotation),
// daher sollen die Ringe diese Kipprichtung zeigen, nicht die Weltachsen.
int pidx = primaryIdx();
float objRotY = (pidx >= 0) ? objects.get(pidx).getRotY() : 0f;
Quaternion qObjY = new Quaternion().fromAngleAxis(objRotY, Vector3f.UNIT_Y);
ringX.setLocalRotation(qObjY.mult(new Quaternion().fromAngleAxis( FastMath.HALF_PI, Vector3f.UNIT_Y)));
ringY.setLocalRotation(qObjY.mult(new Quaternion().fromAngleAxis( FastMath.HALF_PI, Vector3f.UNIT_X)));
ringZ.setLocalRotation(qObjY.mult(new Quaternion()));
}
// ── Gizmo-Picking ─────────────────────────────────────────────────────────
@@ -1627,6 +1848,7 @@ public class SceneObjectState extends BaseAppState {
SceneObject so = objects.get(idx);
deleteInteractableFile(so);
objectRoot.detachChild(objNodes.get(idx));
cachedGizmoRadii.remove(objNodes.get(idx));
objects.remove(idx);
objNodes.remove(idx);
animClips.remove(idx);
@@ -1732,6 +1954,7 @@ public class SceneObjectState extends BaseAppState {
// Originals entfernen
for (int idx : sortedDesc) {
objectRoot.detachChild(objNodes.get(idx));
cachedGizmoRadii.remove(objNodes.get(idx));
objects.remove(idx);
objNodes.remove(idx);
animClips.remove(idx);

View File

@@ -5,3 +5,7 @@ Models/trees/pine/medium/pine_medium_20260706_190953.j3o -17.84356 1.24567 -1316
Models/plants/misc/kaktusfeige.j3o -6.18754 1.22995 -1320.34875 0.00000 2.50000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000
Models/imported/alter_steg.j3o -8.42317 -0.11015 -1359.15662 3.14159 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000
Models/trees/grapevine/grapevine_20260725_195635.j3o -85.78869 1.32641 -1297.17883 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/trees/pine/medium/pine_medium_20260706_190939.j3o 56.66702 5.49606 -1107.68445 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/trees/pine/medium/pine_medium_20260706_190953.j3o 71.94359 5.49606 -1112.82898 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/trees/pine/medium/pine_medium_20260706_190947.j3o 78.22437 5.23387 -1107.42407 -1.45721 1.00000 0.06351 -0.13191 false true true 30.00000 80.00000 120.00000
Models/trees/pine/medium/pine_medium_20260706_190947.j3o 104.65085 0.89589 -1111.47168 -1.67187 1.00000 0.04752 0.04310 false true true 30.00000 80.00000 120.00000

Binary file not shown.