Platzierung und Bearbeitung von OPbjekten angepasst
This commit is contained in:
@@ -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;");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
blight-map/src/main/map/chunks/sculpt_16_0_07.blsm
Normal file
BIN
blight-map/src/main/map/chunks/sculpt_16_0_07.blsm
Normal file
Binary file not shown.
Reference in New Issue
Block a user