Objekte können jetzt Sounds haben (z.B. Lagerfeuer). Sound System optimiert
This commit is contained in:
@@ -165,7 +165,8 @@ public class EditorApp extends Application {
|
||||
this::setStatus, this::setCenterView,
|
||||
root::setRight, node -> topBar.getChildren().set(1, node),
|
||||
() -> worldViewport, ASSET_ROOT, this::switchToWorldEditor, registry,
|
||||
root::setLeft, root::setBottom, () -> centerStack, tool -> currentTool = tool
|
||||
root::setLeft, root::setBottom, () -> centerStack, tool -> currentTool = tool,
|
||||
this::scheduleAutoSave
|
||||
);
|
||||
vegController = new de.blight.editor.toolset.vegetation.VegetationController(toolCtx);
|
||||
ezTreeController = new de.blight.editor.toolset.eztree.EzTreeController(toolCtx);
|
||||
@@ -447,6 +448,14 @@ public class EditorApp extends Application {
|
||||
atmosphereController.updateEmitterPanel(input.selectedEmitterInfo);
|
||||
}
|
||||
|
||||
if (input.auswahlSoundSelected) {
|
||||
input.auswahlSoundSelected = false;
|
||||
topBarController.getAtmosphaereBtn().setSelected(true);
|
||||
input.activeLayer = SharedInput.LAYER_PLACED_SOUNDS;
|
||||
root.setRight(atmosphereController.buildPanel());
|
||||
atmosphereController.updateSoundPanel(input.selectedSoundInfo);
|
||||
}
|
||||
|
||||
if (input.auswahlWaterSelected) {
|
||||
input.auswahlWaterSelected = false;
|
||||
topBarController.getWasserMergedBtn().setSelected(true);
|
||||
@@ -548,6 +557,9 @@ public class EditorApp extends Application {
|
||||
setStatus("Auto-Speicherung…");
|
||||
}
|
||||
});
|
||||
|
||||
input.onObjectDeleted = () ->
|
||||
Platform.runLater(() -> topBarController.getAuswahlBtn().fire());
|
||||
}
|
||||
|
||||
// ── Modus-Wechsel ────────────────────────────────────────────────────────
|
||||
@@ -930,6 +942,14 @@ public class EditorApp extends Application {
|
||||
if (input.activeLayer == SharedInput.LAYER_OBJECTS
|
||||
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)
|
||||
input.deleteSelectedRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_LIGHTS && input.selectedLightInfo != null)
|
||||
input.deleteLightRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_EMITTERS && input.selectedEmitterInfo != null)
|
||||
input.deleteEmitterRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_PLACED_SOUNDS && input.selectedSoundInfo != null)
|
||||
input.deleteSoundRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_ITEMS && input.selectedItemInfo != null)
|
||||
input.deleteSelectedItemRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) {
|
||||
if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }
|
||||
else { input.deleteSoundAreaRequested = true; scheduleAutoSave(); }
|
||||
@@ -965,8 +985,10 @@ public class EditorApp extends Application {
|
||||
confirm.setHeaderText(null);
|
||||
confirm.initOwner(primaryStage);
|
||||
confirm.showAndWait().ifPresent(btn -> {
|
||||
if (btn == javafx.scene.control.ButtonType.YES)
|
||||
if (btn == javafx.scene.control.ButtonType.YES) {
|
||||
input.deleteWaterfallRequested = true;
|
||||
topBarController.getAuswahlBtn().fire();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import de.blight.editor.state.LocationZoneState;
|
||||
import de.blight.editor.state.RiverEditorState;
|
||||
import de.blight.editor.state.WaterfallEditorState;
|
||||
import de.blight.editor.state.PlayToolState;
|
||||
import de.blight.editor.state.PlacedSoundState;
|
||||
import de.blight.editor.state.SoundAreaState;
|
||||
import de.blight.editor.state.WaterBodyState;
|
||||
import de.blight.editor.state.EzTreeState;
|
||||
@@ -204,6 +205,7 @@ public class JmeEditorApp extends SimpleApplication {
|
||||
stateManager.attach(new WillowGeneratorState(input));
|
||||
stateManager.attach(new LightState(input));
|
||||
stateManager.attach(new EmitterState(input));
|
||||
stateManager.attach(new PlacedSoundState(input));
|
||||
stateManager.attach(new WaterBodyState(input));
|
||||
stateManager.attach(new SoundAreaState(input));
|
||||
stateManager.attach(new AreaState(input));
|
||||
|
||||
@@ -445,6 +445,10 @@ public class SharedInput {
|
||||
/** activeLayer==8 → Partikel-Emitter platzieren und bearbeiten */
|
||||
public static final int LAYER_EMITTERS = 8;
|
||||
|
||||
// ── Platzierte Sound-Quellen ──────────────────────────────────────────────
|
||||
/** activeLayer==35 → einzelne Sound-Quellen platzieren und bearbeiten */
|
||||
public static final int LAYER_PLACED_SOUNDS = 35;
|
||||
|
||||
/** Klick im Viewport im Emitter-Modus: platzieren oder selektieren. */
|
||||
public record EmitterClick(float screenX, float screenY, boolean rightButton) {}
|
||||
public final ConcurrentLinkedQueue<EmitterClick> emitterClickQueue = new ConcurrentLinkedQueue<>();
|
||||
@@ -469,6 +473,35 @@ public class SharedInput {
|
||||
/** JavaFX → JME: Preset für nächsten neu platzierten Emitter (0=Feuer,1=Rauch,2=Funken). */
|
||||
public volatile int emitterPreset = 0;
|
||||
|
||||
// ── Sound-Quellen-Werkzeug ────────────────────────────────────────────────
|
||||
/** Klick im Viewport im Sound-Modus: platzieren oder selektieren. */
|
||||
public record SoundClick(float screenX, float screenY, boolean rightButton) {}
|
||||
public final ConcurrentLinkedQueue<SoundClick> soundClickQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
/** JME → JavaFX: Info der selektierten Sound-Quelle. Format: "idx|x|y|z|soundPath|volume|radius" oder null. */
|
||||
public volatile String selectedSoundInfo = null;
|
||||
public volatile boolean soundSelectionChanged = false;
|
||||
|
||||
/** JavaFX → JME: aktualisierte Parameter der selektierten Sound-Quelle. */
|
||||
public record SoundPropertyChange(String soundPath, float volume, float radius) {}
|
||||
public final AtomicReference<SoundPropertyChange> pendingSoundProp = new AtomicReference<>();
|
||||
|
||||
/** JavaFX → JME: Selektierte Sound-Quelle löschen. */
|
||||
public volatile boolean deleteSoundRequested = false;
|
||||
|
||||
/** JME3-intern: Auswahl-Klick für PlacedSoundState. */
|
||||
public final ConcurrentLinkedQueue<ObjectClick> auswahlSoundClickQueue = new ConcurrentLinkedQueue<>();
|
||||
/** JME3 → JavaFX: Sound-Quelle im Auswahl-Modus selektiert → Atmosphäre/Sound-Werkzeug aktivieren. */
|
||||
public volatile boolean auswahlSoundSelected = false;
|
||||
|
||||
// ── Kaskaden-Löschung (SceneObject → Lichter/Emitter/Sounds) ─────────────
|
||||
/** SceneObjectState → LightState: Weltpositionen von zu löschenden Lichtern. */
|
||||
public final ConcurrentLinkedQueue<float[]> removeLightPositions = new ConcurrentLinkedQueue<>();
|
||||
/** SceneObjectState → EmitterState: Weltpositionen von zu löschenden Emittern. */
|
||||
public final ConcurrentLinkedQueue<float[]> removeEmitterPositions = new ConcurrentLinkedQueue<>();
|
||||
/** SceneObjectState → PlacedSoundState: Weltpositionen von zu löschenden Sounds. */
|
||||
public final ConcurrentLinkedQueue<float[]> removeSoundPositions = new ConcurrentLinkedQueue<>();
|
||||
|
||||
// ── Wasser-Werkzeug ───────────────────────────────────────────────────────
|
||||
/** activeLayer==9 → Wasseroberflächen platzieren und bearbeiten */
|
||||
public static final int LAYER_WATER = 9;
|
||||
@@ -794,6 +827,7 @@ public class SharedInput {
|
||||
/** JFX → JME: aktualisierte Anhang-Listen; JME rendert Gizmos neu. */
|
||||
public volatile java.util.List<de.blight.common.ModelMeta.AttachedLight> modelEditorAttachedLights = java.util.List.of();
|
||||
public volatile java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorAttachedEmitters = java.util.List.of();
|
||||
public volatile java.util.List<de.blight.common.ModelMeta.AttachedSound> modelEditorAttachedSounds = java.util.List.of();
|
||||
public volatile boolean modelEditorAttachmentsChanged = false;
|
||||
|
||||
/** JFX → JME: Pfad des zu ladenden Modells (relativ zu blight-assets/src/main/resources/). */
|
||||
@@ -1223,6 +1257,7 @@ public class SharedInput {
|
||||
public volatile boolean auswahlWaterSelected = false;
|
||||
/** JME3 → JavaFX: Wasserfall im Auswahl-Modus selektiert → Wasserfall-Werkzeug aktivieren. */
|
||||
public volatile boolean auswahlWaterfallSelected = false;
|
||||
// auswahlSoundSelected + auswahlSoundClickQueue → defined in Sound-Quellen-Werkzeug section above
|
||||
|
||||
// ── Voxel-Textur-Malen ────────────────────────────────────────────────────
|
||||
/** Parallel-Queue zu textureEditQueue – wird von submitEdit(layer=4) mitbefüllt,
|
||||
@@ -1241,4 +1276,11 @@ public class SharedInput {
|
||||
public final ConcurrentLinkedQueue<PathRename> sceneObjectPathRenames = new ConcurrentLinkedQueue<>();
|
||||
/** EmitterState pollt diese Queue und ersetzt betroffene PlacedEmitter-Records. */
|
||||
public final ConcurrentLinkedQueue<PathRename> emitterPathRenames = new ConcurrentLinkedQueue<>();
|
||||
|
||||
/**
|
||||
* JME3 → JavaFX: Callback nach Löschung eines ganzen Objekts (Zone, Knoten, Licht, …).
|
||||
* States rufen dies nach der Löschung auf; EditorApp setzt den Callback auf den FX-Thread.
|
||||
* Nie bei Vertex-Löschung aufrufen.
|
||||
*/
|
||||
public Runnable onObjectDeleted = () -> {};
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.util.function.Supplier;
|
||||
* @param setBottomPanel Setzt den Bottom-Node des Haupt-BorderPane
|
||||
* @param centerStackSupplier Liefert den persistenten 3D-Viewport-Wrapper
|
||||
* @param setCurrentTool Setzt das aktive Tool-Label in EditorApp (für Keyboard-Routing)
|
||||
* @param scheduleAutoSave Triggert den 8-Sekunden-Autosave-Timer neu (FX-Thread)
|
||||
*/
|
||||
public record ToolContext(
|
||||
SharedInput input,
|
||||
@@ -43,5 +44,6 @@ public record ToolContext(
|
||||
Consumer<Node> setLeftPanel,
|
||||
Consumer<Node> setBottomPanel,
|
||||
Supplier<StackPane> centerStackSupplier,
|
||||
Consumer<String> setCurrentTool
|
||||
Consumer<String> setCurrentTool,
|
||||
Runnable scheduleAutoSave
|
||||
) {}
|
||||
|
||||
@@ -128,7 +128,7 @@ public class AreaState extends BaseAppState {
|
||||
|
||||
if (input.deleteAreaRequested) {
|
||||
input.deleteAreaRequested = false;
|
||||
if (selectedIdx >= 0) removeArea(selectedIdx);
|
||||
if (selectedIdx >= 0) { removeArea(selectedIdx); input.onObjectDeleted.run(); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,8 @@ public class EmitterState extends BaseAppState {
|
||||
private final List<Node> markers = new ArrayList<>();
|
||||
private final List<ParticleEmitter> particles = new ArrayList<>();
|
||||
|
||||
private int selectedIdx = -1;
|
||||
private int selectedIdx = -1;
|
||||
private int lastKnownLayer = -1;
|
||||
private List<PlacedEmitter> pendingEmitters = null;
|
||||
|
||||
public EmitterState(SharedInput input) {
|
||||
@@ -80,8 +81,23 @@ public class EmitterState extends BaseAppState {
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static final float POS_EPSILON = 0.05f;
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
float[] rmPos;
|
||||
while ((rmPos = input.removeEmitterPositions.poll()) != null) {
|
||||
removeByPosition(rmPos[0], rmPos[1], rmPos[2]);
|
||||
}
|
||||
|
||||
// Beim Verlassen des Emitter-Layers Selektion aufheben
|
||||
if (lastKnownLayer == SharedInput.LAYER_EMITTERS
|
||||
&& input.activeLayer != SharedInput.LAYER_EMITTERS
|
||||
&& selectedIdx >= 0) {
|
||||
deselect();
|
||||
}
|
||||
lastKnownLayer = input.activeLayer;
|
||||
|
||||
SharedInput.PathRename rename;
|
||||
while ((rename = input.emitterPathRenames.poll()) != null) {
|
||||
for (int i = 0; i < emitters.size(); i++) {
|
||||
@@ -121,7 +137,11 @@ public class EmitterState extends BaseAppState {
|
||||
|
||||
if (input.deleteEmitterRequested) {
|
||||
input.deleteEmitterRequested = false;
|
||||
if (selectedIdx >= 0) removeEmitter(selectedIdx);
|
||||
if (selectedIdx >= 0) {
|
||||
removeEmitter(selectedIdx);
|
||||
try { de.blight.common.EmitterIO.save(new ArrayList<>(emitters)); } catch (java.io.IOException ignored) {}
|
||||
input.onObjectDeleted.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +208,8 @@ public class EmitterState extends BaseAppState {
|
||||
deselect();
|
||||
selectedIdx = idx;
|
||||
setMarkerColor(idx, SEL_COLOR);
|
||||
setMarkerVisible(idx, false);
|
||||
setParticleVisible(idx, true);
|
||||
publishSelection(idx);
|
||||
}
|
||||
|
||||
@@ -195,12 +217,25 @@ public class EmitterState extends BaseAppState {
|
||||
if (selectedIdx >= 0 && selectedIdx < emitters.size()) {
|
||||
PlacedEmitter e = emitters.get(selectedIdx);
|
||||
setMarkerColor(selectedIdx, new ColorRGBA(e.startR(), e.startG(), e.startB(), 1f));
|
||||
setMarkerVisible(selectedIdx, true);
|
||||
setParticleVisible(selectedIdx, false);
|
||||
}
|
||||
selectedIdx = -1;
|
||||
input.selectedEmitterInfo = null;
|
||||
input.emitterSelectionChanged = true;
|
||||
}
|
||||
|
||||
private void setMarkerVisible(int idx, boolean visible) {
|
||||
markers.get(idx).setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
private void setParticleVisible(int idx, boolean visible) {
|
||||
ParticleEmitter pe = particles.get(idx);
|
||||
if (pe != null) {
|
||||
pe.setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
|
||||
private void publishSelection(int idx) {
|
||||
PlacedEmitter e = emitters.get(idx);
|
||||
input.selectedEmitterInfo = buildInfoString(idx, e);
|
||||
@@ -234,7 +269,10 @@ public class EmitterState extends BaseAppState {
|
||||
markers.add(marker);
|
||||
|
||||
ParticleEmitter particle = buildParticleEmitter(pe);
|
||||
if (particle != null) rootNode.attachChild(particle);
|
||||
if (particle != null) {
|
||||
particle.setCullHint(Spatial.CullHint.Always); // standardmäßig unsichtbar
|
||||
rootNode.attachChild(particle);
|
||||
}
|
||||
particles.add(particle);
|
||||
|
||||
emitters.add(pe);
|
||||
@@ -252,6 +290,25 @@ public class EmitterState extends BaseAppState {
|
||||
input.emitterSelectionChanged = true;
|
||||
}
|
||||
|
||||
private void removeByPosition(float x, float y, float z) {
|
||||
for (int i = emitters.size() - 1; i >= 0; i--) {
|
||||
PlacedEmitter e = emitters.get(i);
|
||||
if (Math.abs(e.x() - x) < POS_EPSILON
|
||||
&& Math.abs(e.y() - y) < POS_EPSILON
|
||||
&& Math.abs(e.z() - z) < POS_EPSILON) {
|
||||
if (selectedIdx == i) {
|
||||
selectedIdx = -1;
|
||||
input.selectedEmitterInfo = null;
|
||||
input.emitterSelectionChanged = true;
|
||||
} else if (selectedIdx > i) {
|
||||
selectedIdx--;
|
||||
}
|
||||
removeEmitter(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearAll() {
|
||||
for (Node m : markers) rootNode.detachChild(m);
|
||||
for (ParticleEmitter pe : particles) if (pe != null) rootNode.detachChild(pe);
|
||||
@@ -270,15 +327,18 @@ public class EmitterState extends BaseAppState {
|
||||
|
||||
Node newMarker = buildMarker(updated);
|
||||
setMarkerColor(newMarker, SEL_COLOR);
|
||||
newMarker.setCullHint(Spatial.CullHint.Always); // selektiert → Marker versteckt
|
||||
rootNode.attachChild(newMarker);
|
||||
markers.set(idx, newMarker);
|
||||
|
||||
ParticleEmitter newPe = buildParticleEmitter(updated);
|
||||
if (newPe != null) rootNode.attachChild(newPe);
|
||||
if (newPe != null) rootNode.attachChild(newPe); // selektiert → Partikel sichtbar
|
||||
particles.set(idx, newPe);
|
||||
|
||||
emitters.set(idx, updated);
|
||||
publishSelection(idx);
|
||||
// selectedEmitterInfo aktualisieren, aber kein Panel-Rebuild auslösen –
|
||||
// sonst werden initStartSize/initVelY neu erfasst und Groesse-Buttons compoundieren.
|
||||
input.selectedEmitterInfo = buildInfoString(idx, updated);
|
||||
}
|
||||
|
||||
// ── Marker visuals ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -737,7 +737,7 @@ public class EzTreeState extends BaseAppState {
|
||||
true, true,
|
||||
1f, 1f,
|
||||
"", "", 30f, 80f, 120f,
|
||||
java.util.List.of(), java.util.List.of(),
|
||||
java.util.List.of(), java.util.List.of(), java.util.List.of(),
|
||||
de.blight.common.model.InteractableType.NONE,
|
||||
0f, 0.5f, 0f, 0f, "bark", autoCategory);
|
||||
de.blight.common.ModelMetaIO.save(meta, outPath);
|
||||
|
||||
@@ -152,6 +152,7 @@ public class ItemPlacementState extends BaseAppState {
|
||||
if (selectedItemIdx >= 0) {
|
||||
removeItem(selectedItemIdx);
|
||||
deselectItem();
|
||||
input.onObjectDeleted.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +84,15 @@ public class LightState extends BaseAppState {
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static final float POS_EPSILON = 0.05f;
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
float[] rmPos;
|
||||
while ((rmPos = input.removeLightPositions.poll()) != null) {
|
||||
removeByPosition(rmPos[0], rmPos[1], rmPos[2]);
|
||||
}
|
||||
|
||||
SharedInput.ObjectClick auswahlClick;
|
||||
while ((auswahlClick = input.auswahlLightClickQueue.poll()) != null) {
|
||||
handleAuswahlClick(auswahlClick);
|
||||
@@ -105,7 +112,11 @@ public class LightState extends BaseAppState {
|
||||
|
||||
if (input.deleteLightRequested) {
|
||||
input.deleteLightRequested = false;
|
||||
if (selectedIdx >= 0) removeLight(selectedIdx);
|
||||
if (selectedIdx >= 0) {
|
||||
removeLight(selectedIdx);
|
||||
try { de.blight.common.LightIO.save(new ArrayList<>(lights)); } catch (java.io.IOException ignored) {}
|
||||
input.onObjectDeleted.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +232,25 @@ public class LightState extends BaseAppState {
|
||||
input.lightSelectionChanged = true;
|
||||
}
|
||||
|
||||
private void removeByPosition(float x, float y, float z) {
|
||||
for (int i = lights.size() - 1; i >= 0; i--) {
|
||||
PlacedLight l = lights.get(i);
|
||||
if (Math.abs(l.x() - x) < POS_EPSILON
|
||||
&& Math.abs(l.y() - y) < POS_EPSILON
|
||||
&& Math.abs(l.z() - z) < POS_EPSILON) {
|
||||
if (selectedIdx == i) {
|
||||
selectedIdx = -1;
|
||||
input.selectedLightInfo = null;
|
||||
input.lightSelectionChanged = true;
|
||||
} else if (selectedIdx > i) {
|
||||
selectedIdx--;
|
||||
}
|
||||
removeLight(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearAll() {
|
||||
for (PointLight pl : pointLights) rootNode.removeLight(pl);
|
||||
for (Node m : markers) rootNode.detachChild(m);
|
||||
|
||||
@@ -127,7 +127,7 @@ public class LocationZoneState extends BaseAppState {
|
||||
|
||||
if (input.deleteLocationZoneRequested) {
|
||||
input.deleteLocationZoneRequested = false;
|
||||
if (selectedIdx >= 0) removeZone(selectedIdx);
|
||||
if (selectedIdx >= 0) { removeZone(selectedIdx); input.onObjectDeleted.run(); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ import com.jme3.scene.shape.Cylinder;
|
||||
import com.jme3.scene.shape.Dome;
|
||||
import com.jme3.scene.shape.Sphere;
|
||||
import com.jme3.anim.SkinningControl;
|
||||
import com.jme3.effect.ParticleEmitter;
|
||||
import com.jme3.effect.ParticleMesh;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.util.ImpostorUtil;
|
||||
@@ -333,7 +336,8 @@ public class ModelEditorState extends BaseAppState {
|
||||
input.modelEditorAttachmentsChanged = false;
|
||||
rebuildAttachmentGizmos(
|
||||
input.modelEditorAttachedLights,
|
||||
input.modelEditorAttachedEmitters);
|
||||
input.modelEditorAttachedEmitters,
|
||||
input.modelEditorAttachedSounds);
|
||||
}
|
||||
|
||||
// Thumbnail auf Anforderung generieren (Model-Editor)
|
||||
@@ -953,7 +957,8 @@ public class ModelEditorState extends BaseAppState {
|
||||
|
||||
private void rebuildAttachmentGizmos(
|
||||
java.util.List<de.blight.common.ModelMeta.AttachedLight> lights,
|
||||
java.util.List<de.blight.common.ModelMeta.AttachedEmitter> emitters) {
|
||||
java.util.List<de.blight.common.ModelMeta.AttachedEmitter> emitters,
|
||||
java.util.List<de.blight.common.ModelMeta.AttachedSound> sounds) {
|
||||
|
||||
if (previewRoot == null) return;
|
||||
|
||||
@@ -965,7 +970,7 @@ public class ModelEditorState extends BaseAppState {
|
||||
attachmentGizmos = null;
|
||||
}
|
||||
|
||||
if (lights.isEmpty() && emitters.isEmpty()) return;
|
||||
if (lights.isEmpty() && emitters.isEmpty() && sounds.isEmpty()) return;
|
||||
|
||||
attachmentGizmos = new Node("attachment_gizmos");
|
||||
|
||||
@@ -991,21 +996,22 @@ public class ModelEditorState extends BaseAppState {
|
||||
previewRoot.addLight(pl);
|
||||
}
|
||||
|
||||
// Emitter-Gizmos: kleine Kugel in Preset-Farbe
|
||||
// Emitter: echte ParticleEmitter, keine Kugel-Gizmos
|
||||
for (de.blight.common.ModelMeta.AttachedEmitter ae : emitters) {
|
||||
ColorRGBA color = switch (ae.preset()) {
|
||||
case 1 -> new ColorRGBA(0.5f, 0.5f, 0.5f, 1f); // Rauch
|
||||
case 2 -> new ColorRGBA(1.0f, 0.9f, 0.1f, 1f); // Funken
|
||||
default -> new ColorRGBA(1.0f, 0.4f, 0.0f, 1f); // Feuer
|
||||
};
|
||||
Geometry sphere = new Geometry("emitter_gizmo",
|
||||
new Sphere(8, 8, 0.10f));
|
||||
Material mat = new Material(app.getAssetManager(),
|
||||
ParticleEmitter pe = buildAttachedParticleEffect(ae);
|
||||
if (pe != null) attachmentGizmos.attachChild(pe);
|
||||
}
|
||||
|
||||
// Sound-Gizmos: kleine türkise Kugel
|
||||
for (de.blight.common.ModelMeta.AttachedSound as : sounds) {
|
||||
Geometry soundSphere = new Geometry("sound_gizmo",
|
||||
new com.jme3.scene.shape.Sphere(8, 8, 0.10f));
|
||||
Material sMat = new Material(app.getAssetManager(),
|
||||
"Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
sphere.setMaterial(mat);
|
||||
sphere.setLocalTranslation(ae.offsetX(), ae.offsetY(), ae.offsetZ());
|
||||
attachmentGizmos.attachChild(sphere);
|
||||
sMat.setColor("Color", new ColorRGBA(0.0f, 0.85f, 0.85f, 1f));
|
||||
soundSphere.setMaterial(sMat);
|
||||
soundSphere.setLocalTranslation(as.offsetX(), as.offsetY(), as.offsetZ());
|
||||
attachmentGizmos.attachChild(soundSphere);
|
||||
}
|
||||
|
||||
previewRoot.attachChild(attachmentGizmos);
|
||||
@@ -1019,6 +1025,34 @@ public class ModelEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
private ParticleEmitter buildAttachedParticleEffect(de.blight.common.ModelMeta.AttachedEmitter ae) {
|
||||
try {
|
||||
ParticleEmitter effect = new ParticleEmitter(
|
||||
"attached_particle", ParticleMesh.Type.Triangle, ae.maxParticles());
|
||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
|
||||
mat.setTexture("Texture", app.getAssetManager().loadTexture(ae.texturePath()));
|
||||
effect.setMaterial(mat);
|
||||
effect.setImagesX(ae.imagesX());
|
||||
effect.setImagesY(ae.imagesY());
|
||||
effect.setStartColor(new ColorRGBA(ae.startR(), ae.startG(), ae.startB(), ae.startA()));
|
||||
effect.setEndColor( new ColorRGBA(ae.endR(), ae.endG(), ae.endB(), ae.endA()));
|
||||
effect.setStartSize(ae.startSize());
|
||||
effect.setEndSize(ae.endSize());
|
||||
effect.getParticleInfluencer()
|
||||
.setInitialVelocity(new Vector3f(ae.velX(), ae.velY(), ae.velZ()));
|
||||
effect.getParticleInfluencer().setVelocityVariation(ae.velocityVariation());
|
||||
effect.setGravity(ae.gravX(), ae.gravY(), ae.gravZ());
|
||||
effect.setLowLife(ae.lowLife());
|
||||
effect.setHighLife(ae.highLife());
|
||||
effect.setParticlesPerSec(ae.emitRate());
|
||||
effect.setLocalTranslation(ae.offsetX(), ae.offsetY(), ae.offsetZ());
|
||||
return effect;
|
||||
} catch (Exception e) {
|
||||
log.warn("[ModelEditorState] Emitter-Textur nicht ladbar: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Markiert alle Vertex-Buffer als upload-needed – zwingt JME3 zu korrektem erstem Frame. */
|
||||
|
||||
@@ -424,7 +424,7 @@ public class ModelImportState extends BaseAppState {
|
||||
def.randomScaleMin(), def.randomScaleMax(),
|
||||
def.lod1Path(), def.lod2Path(),
|
||||
def.lod1Distance(), def.lod2Distance(), def.cullDistance(),
|
||||
def.attachedLights(), def.attachedEmitters(),
|
||||
def.attachedLights(), def.attachedEmitters(), def.attachedSounds(),
|
||||
def.interactableType(),
|
||||
def.interactableOffsetX(), def.interactableOffsetY(),
|
||||
def.interactableOffsetZ(), def.interactableRotY(),
|
||||
|
||||
@@ -138,7 +138,7 @@ public class PathNetworkEditorState extends BaseAppState {
|
||||
|
||||
if (input.deletePathNodeRequested) {
|
||||
input.deletePathNodeRequested = false;
|
||||
deleteSelected();
|
||||
if (selectedNode != null) { deleteSelected(); input.onObjectDeleted.run(); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
package de.blight.editor.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.collision.CollisionResults;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.shape.Sphere;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import de.blight.common.PlacedSound;
|
||||
import de.blight.editor.SharedInput;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PlacedSoundState extends BaseAppState {
|
||||
|
||||
private static final float MARKER_RADIUS = 0.30f;
|
||||
private static final float GLOW_RADIUS = MARKER_RADIUS * 2.4f;
|
||||
private static final float GLOW_ALPHA = 0.18f;
|
||||
|
||||
private static final ColorRGBA COL_NORMAL = new ColorRGBA(0.0f, 0.85f, 0.85f, 1f);
|
||||
private static final ColorRGBA COL_SELECTED = new ColorRGBA(1.0f, 1.0f, 0.0f, 1f);
|
||||
private static final ColorRGBA COL_GLOW = new ColorRGBA(0.0f, 0.85f, 0.85f, GLOW_ALPHA);
|
||||
|
||||
private static final float POS_EPSILON = 0.05f;
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
private AssetManager assets;
|
||||
private Node rootNode;
|
||||
private TerrainQuad terrain;
|
||||
|
||||
// parallel lists: sounds[i] <-> markers[i]
|
||||
private final List<PlacedSound> sounds = new ArrayList<>();
|
||||
private final List<Node> markers = new ArrayList<>();
|
||||
|
||||
private int selectedIdx = -1;
|
||||
private List<PlacedSound> pendingSounds = null;
|
||||
|
||||
public PlacedSoundState(SharedInput input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
cam = app.getCamera();
|
||||
assets = app.getAssetManager();
|
||||
rootNode = app.getRootNode();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {
|
||||
clearAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
if (pendingSounds != null) {
|
||||
loadPlacedSounds(pendingSounds);
|
||||
pendingSounds = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
float[] rmPos;
|
||||
while ((rmPos = input.removeSoundPositions.poll()) != null) {
|
||||
removeByPosition(rmPos[0], rmPos[1], rmPos[2]);
|
||||
}
|
||||
|
||||
SharedInput.ObjectClick auswahlClick;
|
||||
while ((auswahlClick = input.auswahlSoundClickQueue.poll()) != null) {
|
||||
handleAuswahlClick(auswahlClick);
|
||||
}
|
||||
|
||||
if (input.activeLayer != SharedInput.LAYER_PLACED_SOUNDS) return;
|
||||
|
||||
SharedInput.SoundClick click;
|
||||
while ((click = input.soundClickQueue.poll()) != null) {
|
||||
handleClick(click);
|
||||
}
|
||||
|
||||
SharedInput.SoundPropertyChange prop = input.pendingSoundProp.getAndSet(null);
|
||||
if (prop != null && selectedIdx >= 0) {
|
||||
applyProperty(selectedIdx, prop);
|
||||
}
|
||||
|
||||
if (input.deleteSoundRequested) {
|
||||
input.deleteSoundRequested = false;
|
||||
if (selectedIdx >= 0) {
|
||||
removeSound(selectedIdx);
|
||||
try { de.blight.common.SoundIO.save(new ArrayList<>(sounds)); } catch (java.io.IOException ignored) {}
|
||||
input.onObjectDeleted.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Click handling ────────────────────────────────────────────────────────
|
||||
|
||||
private void handleClick(SharedInput.SoundClick click) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
|
||||
int hit = pickMarker(ray);
|
||||
if (hit >= 0) {
|
||||
if (click.rightButton()) {
|
||||
deselect();
|
||||
} else {
|
||||
selectSound(hit);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (click.rightButton()) {
|
||||
deselect();
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3f pt = raycastTerrain(ray);
|
||||
if (pt == null) return;
|
||||
|
||||
PlacedSound ps = new PlacedSound(pt.x, pt.y + 0.5f, pt.z, "", 1.0f, 20.0f);
|
||||
addSound(ps);
|
||||
selectSound(sounds.size() - 1);
|
||||
}
|
||||
|
||||
private void handleAuswahlClick(SharedInput.ObjectClick click) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
int hit = pickMarker(ray);
|
||||
if (hit >= 0) {
|
||||
selectSound(hit);
|
||||
input.auswahlSoundSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
private int pickMarker(Ray ray) {
|
||||
for (int i = 0; i < markers.size(); i++) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
markers.get(i).collideWith(ray, res);
|
||||
if (res.size() > 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ── Selection ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void selectSound(int idx) {
|
||||
deselect();
|
||||
selectedIdx = idx;
|
||||
setMarkerColor(idx, COL_SELECTED);
|
||||
publishSelection(idx);
|
||||
}
|
||||
|
||||
private void deselect() {
|
||||
if (selectedIdx >= 0 && selectedIdx < sounds.size()) {
|
||||
setMarkerColor(selectedIdx, COL_NORMAL);
|
||||
}
|
||||
selectedIdx = -1;
|
||||
input.selectedSoundInfo = null;
|
||||
input.soundSelectionChanged = true;
|
||||
}
|
||||
|
||||
private void publishSelection(int idx) {
|
||||
PlacedSound s = sounds.get(idx);
|
||||
input.selectedSoundInfo = String.format(
|
||||
java.util.Locale.ROOT,
|
||||
"%d|%.3f|%.3f|%.3f|%s|%.3f|%.3f",
|
||||
idx, s.x(), s.y(), s.z(), s.soundPath(), s.volume(), s.radius());
|
||||
input.soundSelectionChanged = true;
|
||||
}
|
||||
|
||||
// ── Add / Remove ──────────────────────────────────────────────────────────
|
||||
|
||||
private void addSound(PlacedSound ps) {
|
||||
Node marker = buildMarker(ps);
|
||||
rootNode.attachChild(marker);
|
||||
sounds.add(ps);
|
||||
markers.add(marker);
|
||||
}
|
||||
|
||||
private void removeSound(int idx) {
|
||||
rootNode.detachChild(markers.get(idx));
|
||||
sounds.remove(idx);
|
||||
markers.remove(idx);
|
||||
selectedIdx = -1;
|
||||
input.selectedSoundInfo = null;
|
||||
input.soundSelectionChanged = true;
|
||||
}
|
||||
|
||||
private void removeByPosition(float x, float y, float z) {
|
||||
for (int i = sounds.size() - 1; i >= 0; i--) {
|
||||
PlacedSound s = sounds.get(i);
|
||||
if (Math.abs(s.x() - x) < POS_EPSILON
|
||||
&& Math.abs(s.y() - y) < POS_EPSILON
|
||||
&& Math.abs(s.z() - z) < POS_EPSILON) {
|
||||
if (selectedIdx == i) {
|
||||
selectedIdx = -1;
|
||||
input.selectedSoundInfo = null;
|
||||
input.soundSelectionChanged = true;
|
||||
} else if (selectedIdx > i) {
|
||||
selectedIdx--;
|
||||
}
|
||||
rootNode.detachChild(markers.get(i));
|
||||
sounds.remove(i);
|
||||
markers.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearAll() {
|
||||
for (Node m : markers) rootNode.detachChild(m);
|
||||
sounds.clear();
|
||||
markers.clear();
|
||||
selectedIdx = -1;
|
||||
}
|
||||
|
||||
// ── Property application ──────────────────────────────────────────────────
|
||||
|
||||
private void applyProperty(int idx, SharedInput.SoundPropertyChange prop) {
|
||||
PlacedSound old = sounds.get(idx);
|
||||
PlacedSound updated = new PlacedSound(
|
||||
old.x(), old.y(), old.z(),
|
||||
prop.soundPath(), prop.volume(), prop.radius());
|
||||
sounds.set(idx, updated);
|
||||
publishSelection(idx);
|
||||
}
|
||||
|
||||
// ── Marker visuals ────────────────────────────────────────────────────────
|
||||
|
||||
private Node buildMarker(PlacedSound ps) {
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", COL_NORMAL);
|
||||
Geometry sphere = new Geometry("sound_sphere", new Sphere(10, 10, MARKER_RADIUS));
|
||||
sphere.setMaterial(mat);
|
||||
|
||||
Material glowMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
glowMat.setColor("Color", COL_GLOW);
|
||||
glowMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
glowMat.getAdditionalRenderState().setDepthWrite(false);
|
||||
glowMat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Front);
|
||||
Geometry glow = new Geometry("sound_glow", new Sphere(8, 8, GLOW_RADIUS));
|
||||
glow.setMaterial(glowMat);
|
||||
glow.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
|
||||
Node node = new Node("sound_node");
|
||||
node.attachChild(glow);
|
||||
node.attachChild(sphere);
|
||||
node.setLocalTranslation(ps.x(), ps.y(), ps.z());
|
||||
return node;
|
||||
}
|
||||
|
||||
private void setMarkerColor(int idx, ColorRGBA color) {
|
||||
Node marker = markers.get(idx);
|
||||
for (Spatial child : marker.getChildren()) {
|
||||
if (child instanceof Geometry geo && "sound_sphere".equals(geo.getName())) {
|
||||
geo.getMaterial().setColor("Color", color);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Save / Load ───────────────────────────────────────────────────────────
|
||||
|
||||
public List<PlacedSound> getPlacedSounds() {
|
||||
return new ArrayList<>(sounds);
|
||||
}
|
||||
|
||||
public void loadPlacedSounds(List<PlacedSound> loaded) {
|
||||
if (rootNode == null) {
|
||||
pendingSounds = new ArrayList<>(loaded);
|
||||
return;
|
||||
}
|
||||
clearAll();
|
||||
for (PlacedSound ps : loaded) {
|
||||
addSound(ps);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3f raycastTerrain(Ray ray) {
|
||||
if (terrain == null) return null;
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() > 0) return hits.getClosestCollision().getContactPoint();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -85,9 +85,13 @@ public class SceneObjectState extends BaseAppState {
|
||||
private Node rootNode;
|
||||
private TerrainQuad terrain;
|
||||
|
||||
private final List<SceneObject> objects = new ArrayList<>();
|
||||
private final List<Node> objNodes = new ArrayList<>();
|
||||
private final List<String> animClips = new ArrayList<>(); // parallel zu objects
|
||||
private final List<SceneObject> objects = new ArrayList<>();
|
||||
private final List<Node> objNodes = new ArrayList<>();
|
||||
private final List<String> animClips = new ArrayList<>(); // parallel zu objects
|
||||
// Kaskaden-Löschung: Welt-Positionen der mitplatzierten Lichter/Emitter/Sounds pro Objekt
|
||||
private final List<java.util.List<float[]>> placedLightPositions = new ArrayList<>();
|
||||
private final List<java.util.List<float[]>> placedEmitterPositions = new ArrayList<>();
|
||||
private final List<java.util.List<float[]>> placedSoundPositions = new ArrayList<>();
|
||||
|
||||
private Node objectRoot;
|
||||
private Node gizmoNode; // Wurzel aller Gizmo-Geometrien (direktes Kind von rootNode)
|
||||
@@ -205,6 +209,9 @@ public class SceneObjectState extends BaseAppState {
|
||||
objects.clear();
|
||||
objNodes.clear();
|
||||
animClips.clear();
|
||||
placedLightPositions.clear();
|
||||
placedEmitterPositions.clear();
|
||||
placedSoundPositions.clear();
|
||||
sceneLitMaterials.clear();
|
||||
windMaterials.clear();
|
||||
pbrMaterials.clear();
|
||||
@@ -231,6 +238,9 @@ public class SceneObjectState extends BaseAppState {
|
||||
so.modelCategory = pm.modelCategory() != null ? pm.modelCategory() : de.blight.common.ModelCategory.NONE;
|
||||
objects.add(so);
|
||||
animClips.add(pm.animClip() != null ? pm.animClip() : "");
|
||||
placedLightPositions.add(new ArrayList<>());
|
||||
placedEmitterPositions.add(new ArrayList<>());
|
||||
placedSoundPositions.add(new ArrayList<>());
|
||||
|
||||
// meshFile hat Vorrang: enthält exportierte Geometrie inkl. Material
|
||||
String loadPath = !pm.meshFile().isEmpty() ? pm.meshFile() : pm.modelPath();
|
||||
@@ -465,7 +475,7 @@ public class SceneObjectState extends BaseAppState {
|
||||
// Löschen
|
||||
if (input.deleteSelectedRequested) {
|
||||
input.deleteSelectedRequested = false;
|
||||
deleteSelected();
|
||||
if (!selectedIndices.isEmpty()) { deleteSelected(); input.onObjectDeleted.run(); }
|
||||
}
|
||||
|
||||
// Zusammenfassen
|
||||
@@ -731,6 +741,7 @@ public class SceneObjectState extends BaseAppState {
|
||||
input.auswahlClickQueue.offer(click);
|
||||
input.auswahlLightClickQueue.offer(click);
|
||||
input.auswahlEmitterClickQueue.offer(click);
|
||||
input.auswahlSoundClickQueue.offer(click);
|
||||
input.auswahlWaterClickQueue.offer(click);
|
||||
input.auswahlWaterfallClickQueue.offer(click);
|
||||
return;
|
||||
@@ -879,6 +890,9 @@ public class SceneObjectState extends BaseAppState {
|
||||
so.receiveShadow = defaultReceive;
|
||||
objects.add(so);
|
||||
animClips.add("");
|
||||
placedLightPositions.add(new ArrayList<>());
|
||||
placedEmitterPositions.add(new ArrayList<>());
|
||||
placedSoundPositions.add(new ArrayList<>());
|
||||
|
||||
Node node = loadModelNode(modelPath, wx, wy + placementOffY, wz);
|
||||
node.setLocalScale(defaultScale);
|
||||
@@ -891,10 +905,11 @@ public class SceneObjectState extends BaseAppState {
|
||||
input.objectJustPlaced = true;
|
||||
setStatus("Platziert: " + modelPath);
|
||||
|
||||
// Anhänge aus ModelMeta: Lichter + Emitter mitplatzieren
|
||||
// Anhänge aus ModelMeta: Lichter + Emitter + Sounds mitplatzieren
|
||||
if (meta != null) {
|
||||
placeAttachedLights(meta, wx, wy + placementOffY, wz);
|
||||
placeAttachedEmitters(meta, wx, wy + placementOffY, wz);
|
||||
placeAttachedSounds(meta, wx, wy + placementOffY, wz);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,10 +919,12 @@ public class SceneObjectState extends BaseAppState {
|
||||
try {
|
||||
java.util.List<de.blight.common.PlacedLight> list =
|
||||
new java.util.ArrayList<>(de.blight.common.LightIO.load());
|
||||
java.util.List<float[]> track = placedLightPositions.get(objects.size() - 1);
|
||||
for (de.blight.common.ModelMeta.AttachedLight al : meta.attachedLights()) {
|
||||
list.add(new de.blight.common.PlacedLight(
|
||||
wx + al.offsetX(), wy + al.offsetY(), wz + al.offsetZ(),
|
||||
float lx = wx + al.offsetX(), ly = wy + al.offsetY(), lz = wz + al.offsetZ();
|
||||
list.add(new de.blight.common.PlacedLight(lx, ly, lz,
|
||||
al.r(), al.g(), al.b(), al.intensity(), al.radius()));
|
||||
track.add(new float[]{lx, ly, lz});
|
||||
}
|
||||
de.blight.common.LightIO.save(list);
|
||||
input.reloadLightsEmitters = true;
|
||||
@@ -922,16 +939,22 @@ public class SceneObjectState extends BaseAppState {
|
||||
try {
|
||||
java.util.List<de.blight.common.PlacedEmitter> list =
|
||||
new java.util.ArrayList<>(de.blight.common.EmitterIO.load());
|
||||
java.util.List<float[]> track = placedEmitterPositions.get(objects.size() - 1);
|
||||
for (de.blight.common.ModelMeta.AttachedEmitter ae : meta.attachedEmitters()) {
|
||||
float ax = wx + ae.offsetX();
|
||||
float ay = wy + ae.offsetY();
|
||||
float az = wz + ae.offsetZ();
|
||||
de.blight.common.PlacedEmitter pe = switch (ae.preset()) {
|
||||
case 1 -> de.blight.common.PlacedEmitter.smoke(ax, ay, az);
|
||||
case 2 -> de.blight.common.PlacedEmitter.sparks(ax, ay, az);
|
||||
default -> de.blight.common.PlacedEmitter.fire(ax, ay, az);
|
||||
};
|
||||
list.add(pe);
|
||||
list.add(new de.blight.common.PlacedEmitter(
|
||||
ax, ay, az, 20f,
|
||||
ae.texturePath(), ae.imagesX(), ae.imagesY(),
|
||||
ae.startR(), ae.startG(), ae.startB(), ae.startA(),
|
||||
ae.endR(), ae.endG(), ae.endB(), ae.endA(),
|
||||
ae.startSize(), ae.endSize(),
|
||||
ae.velX(), ae.velY(), ae.velZ(), ae.velocityVariation(),
|
||||
ae.gravX(), ae.gravY(), ae.gravZ(),
|
||||
ae.lowLife(), ae.highLife(),
|
||||
ae.maxParticles(), ae.emitRate()));
|
||||
track.add(new float[]{ax, ay, az});
|
||||
}
|
||||
de.blight.common.EmitterIO.save(list);
|
||||
input.reloadLightsEmitters = true;
|
||||
@@ -940,6 +963,28 @@ public class SceneObjectState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
private void placeAttachedSounds(de.blight.common.ModelMeta meta,
|
||||
float wx, float wy, float wz) {
|
||||
if (meta.attachedSounds().isEmpty()) return;
|
||||
try {
|
||||
java.util.List<de.blight.common.PlacedSound> list =
|
||||
new java.util.ArrayList<>(de.blight.common.SoundIO.load());
|
||||
java.util.List<float[]> track = placedSoundPositions.get(objects.size() - 1);
|
||||
for (de.blight.common.ModelMeta.AttachedSound as : meta.attachedSounds()) {
|
||||
float sx = wx + as.offsetX();
|
||||
float sy = wy + as.offsetY();
|
||||
float sz = wz + as.offsetZ();
|
||||
list.add(new de.blight.common.PlacedSound(sx, sy, sz,
|
||||
as.soundPath(), as.volume(), as.radius()));
|
||||
track.add(new float[]{sx, sy, sz});
|
||||
}
|
||||
de.blight.common.SoundIO.save(list);
|
||||
input.reloadLightsEmitters = true;
|
||||
} catch (java.io.IOException e) {
|
||||
log.error("[SceneObject] Anhang-Sound speichern fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void convertUnshadedToPbr(Spatial spatial) {
|
||||
if (spatial instanceof Geometry geo) {
|
||||
Material mat = geo.getMaterial();
|
||||
@@ -1898,16 +1943,48 @@ public class SceneObjectState extends BaseAppState {
|
||||
List<Integer> sortedDesc = new ArrayList<>(selectedIndices);
|
||||
sortedDesc.sort(Comparator.reverseOrder());
|
||||
|
||||
// Positionen aller Kaskaden-Anhänge sammeln (aus Meta – funktioniert auch nach Neustart)
|
||||
List<float[]> cascadeLights = new ArrayList<>();
|
||||
List<float[]> cascadeEmitters = new ArrayList<>();
|
||||
List<float[]> cascadeSounds = new ArrayList<>();
|
||||
|
||||
for (int idx : sortedDesc) {
|
||||
SceneObject so = objects.get(idx);
|
||||
de.blight.common.ModelMeta meta = loadMetaCached(so.modelPath);
|
||||
if (meta != null) {
|
||||
float wx = so.getWorldX(), wy = so.getGroundY(), wz = so.getWorldZ();
|
||||
for (var al : meta.attachedLights()) {
|
||||
float[] p = {wx + al.offsetX(), wy + al.offsetY(), wz + al.offsetZ()};
|
||||
input.removeLightPositions.offer(p);
|
||||
cascadeLights.add(p);
|
||||
}
|
||||
for (var ae : meta.attachedEmitters()) {
|
||||
float[] p = {wx + ae.offsetX(), wy + ae.offsetY(), wz + ae.offsetZ()};
|
||||
input.removeEmitterPositions.offer(p);
|
||||
cascadeEmitters.add(p);
|
||||
}
|
||||
for (var as : meta.attachedSounds()) {
|
||||
float[] p = {wx + as.offsetX(), wy + as.offsetY(), wz + as.offsetZ()};
|
||||
input.removeSoundPositions.offer(p);
|
||||
cascadeSounds.add(p);
|
||||
}
|
||||
}
|
||||
deleteInteractableFile(so);
|
||||
objectRoot.detachChild(objNodes.get(idx));
|
||||
cachedGizmoRadii.remove(objNodes.get(idx));
|
||||
objects.remove(idx);
|
||||
objNodes.remove(idx);
|
||||
animClips.remove(idx);
|
||||
placedLightPositions.remove(idx);
|
||||
placedEmitterPositions.remove(idx);
|
||||
placedSoundPositions.remove(idx);
|
||||
}
|
||||
|
||||
// Dateien sofort bereinigen, damit reloadLightsEmitters keine alten Daten zurückbringt
|
||||
if (!cascadeLights.isEmpty()) cascadeRemoveFromFile(cascadeLights, "lights");
|
||||
if (!cascadeEmitters.isEmpty()) cascadeRemoveFromFile(cascadeEmitters, "emitters");
|
||||
if (!cascadeSounds.isEmpty()) cascadeRemoveFromFile(cascadeSounds, "sounds");
|
||||
|
||||
selectedIndices.clear();
|
||||
clearSubSelection();
|
||||
activeGizmo = -1;
|
||||
@@ -1920,6 +1997,42 @@ public class SceneObjectState extends BaseAppState {
|
||||
setStatus("Objekt(e) gelöscht");
|
||||
}
|
||||
|
||||
private void cascadeRemoveFromFile(List<float[]> positions, String type) {
|
||||
try {
|
||||
switch (type) {
|
||||
case "lights" -> {
|
||||
List<de.blight.common.PlacedLight> list = new java.util.ArrayList<>(de.blight.common.LightIO.load());
|
||||
for (float[] p : positions) {
|
||||
list.removeIf(l -> Math.abs(l.x()-p[0]) < 0.05f
|
||||
&& Math.abs(l.y()-p[1]) < 0.05f
|
||||
&& Math.abs(l.z()-p[2]) < 0.05f);
|
||||
}
|
||||
de.blight.common.LightIO.save(list);
|
||||
}
|
||||
case "emitters" -> {
|
||||
List<de.blight.common.PlacedEmitter> list = new java.util.ArrayList<>(de.blight.common.EmitterIO.load());
|
||||
for (float[] p : positions) {
|
||||
list.removeIf(e -> Math.abs(e.x()-p[0]) < 0.05f
|
||||
&& Math.abs(e.y()-p[1]) < 0.05f
|
||||
&& Math.abs(e.z()-p[2]) < 0.05f);
|
||||
}
|
||||
de.blight.common.EmitterIO.save(list);
|
||||
}
|
||||
case "sounds" -> {
|
||||
List<de.blight.common.PlacedSound> list = new java.util.ArrayList<>(de.blight.common.SoundIO.load());
|
||||
for (float[] p : positions) {
|
||||
list.removeIf(s -> Math.abs(s.x()-p[0]) < 0.05f
|
||||
&& Math.abs(s.y()-p[1]) < 0.05f
|
||||
&& Math.abs(s.z()-p[2]) < 0.05f);
|
||||
}
|
||||
de.blight.common.SoundIO.save(list);
|
||||
}
|
||||
}
|
||||
} catch (java.io.IOException e) {
|
||||
log.warn("[SceneObject] Kaskade {}-Datei bereinigen fehlgeschlagen: {}", type, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteInteractableFile(SceneObject so) {
|
||||
if (so.interactableId == null || so.interactableId.isEmpty()) return;
|
||||
try {
|
||||
|
||||
@@ -131,7 +131,7 @@ public class SoundAreaState extends BaseAppState {
|
||||
|
||||
if (input.deleteSoundAreaRequested) {
|
||||
input.deleteSoundAreaRequested = false;
|
||||
if (selectedIdx >= 0) removeArea(selectedIdx);
|
||||
if (selectedIdx >= 0) { removeArea(selectedIdx); input.onObjectDeleted.run(); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import de.blight.common.MapIO;
|
||||
import de.blight.common.PlacedModelIO;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.state.PathNetworkEditorState;
|
||||
import de.blight.editor.state.PlacedSoundState;
|
||||
import de.blight.editor.state.RoutineMapState;
|
||||
import de.blight.editor.tool.HeightTool;
|
||||
import org.slf4j.Logger;
|
||||
@@ -105,6 +106,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
private ItemPlacementState itemPlacementState;
|
||||
private LightState lightState;
|
||||
private EmitterState emitterState;
|
||||
private PlacedSoundState placedSoundState;
|
||||
private WaterBodyState waterBodyState;
|
||||
private SoundAreaState soundAreaState;
|
||||
private AreaState areaState;
|
||||
@@ -326,6 +328,18 @@ public class TerrainEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
input.loadingStatus = "Lade Sound-Quellen...";
|
||||
placedSoundState = app.getStateManager().getState(PlacedSoundState.class);
|
||||
if (placedSoundState != null) {
|
||||
placedSoundState.setTerrain(terrain);
|
||||
try {
|
||||
var sounds = de.blight.common.SoundIO.load();
|
||||
if (!sounds.isEmpty()) placedSoundState.loadPlacedSounds(sounds);
|
||||
} catch (IOException e) {
|
||||
log.error("Sound-Quellen nicht ladbar", e);
|
||||
}
|
||||
}
|
||||
|
||||
input.loadingStatus = "Lade Wasserflächen...";
|
||||
waterBodyState = app.getStateManager().getState(WaterBodyState.class);
|
||||
if (waterBodyState != null) {
|
||||
@@ -1303,14 +1317,16 @@ public class TerrainEditorState extends BaseAppState {
|
||||
|
||||
if (input.reloadLightsEmitters) {
|
||||
input.reloadLightsEmitters = false;
|
||||
try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {}
|
||||
try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {}
|
||||
try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {}
|
||||
try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {}
|
||||
try { if (placedSoundState != null) placedSoundState.loadPlacedSounds(de.blight.common.SoundIO.load()); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
if (input.reloadPlacedOther) {
|
||||
input.reloadPlacedOther = false;
|
||||
try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {}
|
||||
try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {}
|
||||
try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {}
|
||||
try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {}
|
||||
try { if (placedSoundState != null) placedSoundState.loadPlacedSounds(de.blight.common.SoundIO.load()); } catch (Exception ignored) {}
|
||||
try { if (waterBodyState != null) waterBodyState.loadPlacedBodies(de.blight.common.WaterBodyIO.load()); } catch (Exception ignored) {}
|
||||
try { if (soundAreaState != null) soundAreaState.loadAreas(de.blight.common.SoundAreaIO.load()); } catch (Exception ignored) {}
|
||||
try { if (areaState != null) areaState.loadAreas(de.blight.common.AreaIO.load()); } catch (Exception ignored) {}
|
||||
@@ -1558,6 +1574,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
final List<PlacedModel> models = sceneObjState != null ? sceneObjState.getPlacedModels() : null;
|
||||
final List<PlacedLight> lights = lightState != null ? lightState.getPlacedLights() : null;
|
||||
final List<PlacedEmitter> emitters = emitterState != null ? emitterState.getPlacedEmitters() : null;
|
||||
final List<de.blight.common.PlacedSound> placedSounds = placedSoundState != null ? placedSoundState.getPlacedSounds() : null;
|
||||
final List<PlacedWater> waters = waterBodyState != null ? waterBodyState.getPlacedBodies() : null;
|
||||
final List<List<RiverPoint>> rivers = riverEditorState != null ? riverEditorState.getPlacedRivers() : null;
|
||||
final List<PlacedSoundArea> soundAreas = soundAreaState != null ? soundAreaState.getPlacedAreas() : null;
|
||||
@@ -1573,6 +1590,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
try { if (models != null) PlacedModelIO.save(models); } catch (IOException e) { log.error("Modelle speichern", e); }
|
||||
try { if (lights != null) LightIO.save(lights); } catch (IOException e) { log.error("Lichter speichern", e); }
|
||||
try { if (emitters != null) EmitterIO.save(emitters); } catch (IOException e) { log.error("Emitter speichern", e); }
|
||||
try { if (placedSounds != null) de.blight.common.SoundIO.save(placedSounds); } catch (IOException e) { log.error("Sound-Quellen speichern", e); }
|
||||
try { if (waters != null) WaterBodyIO.save(waters); } catch (IOException e) { log.error("Wasser speichern", e); }
|
||||
try { if (rivers != null) de.blight.common.RiverIO.save(rivers); } catch (IOException e) { log.error("Flüsse speichern", e); }
|
||||
try { if (soundAreas != null) SoundAreaIO.save(soundAreas); } catch (IOException e) { log.error("Soundbereiche speichern", e); }
|
||||
|
||||
@@ -489,7 +489,7 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
de.blight.common.ModelMeta autoMeta = new de.blight.common.ModelMeta(
|
||||
fileName, "trees", "", 1f, 1f, 1f, true, 0f, 0f,
|
||||
true, true, true, 1f, 1f, "", "", 30f, 80f, 120f,
|
||||
java.util.List.of(), java.util.List.of(),
|
||||
java.util.List.of(), java.util.List.of(), java.util.List.of(),
|
||||
de.blight.common.model.InteractableType.NONE,
|
||||
0f, 0.5f, 0f, 0f, "bark", autoCategory);
|
||||
de.blight.common.ModelMetaIO.save(autoMeta, out.toPath());
|
||||
|
||||
@@ -109,9 +109,7 @@ public class VoxelCliffEditorState extends BaseAppState {
|
||||
|
||||
if (input.deleteVoxelCliffZoneRequested) {
|
||||
input.deleteVoxelCliffZoneRequested = false;
|
||||
if (selectedIdx >= 0) {
|
||||
removeZone(selectedIdx);
|
||||
}
|
||||
if (selectedIdx >= 0) { removeZone(selectedIdx); input.onObjectDeleted.run(); }
|
||||
}
|
||||
|
||||
if (input.generateCliffVoxelsRequested) {
|
||||
|
||||
@@ -191,8 +191,10 @@ public class WaterBodyState extends BaseAppState {
|
||||
input.deleteWaterRequested = false;
|
||||
if (selectedHandle >= 0 && selectedIdx >= 0 && selectedHandle < liveXs.length) {
|
||||
deleteCornerPoint(selectedIdx, selectedHandle);
|
||||
input.onObjectDeleted.run();
|
||||
} else if (selectedIdx >= 0) {
|
||||
removeBody(selectedIdx);
|
||||
input.onObjectDeleted.run();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import de.blight.common.PlacedEmitter;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.ToolContext;
|
||||
import de.blight.editor.toolset.UiHelper;
|
||||
import de.blight.editor.ui.SoundChooser;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
@@ -14,6 +16,7 @@ public class AtmosphereController {
|
||||
|
||||
private VBox lightDynamicContent;
|
||||
private VBox emitterDynamicContent;
|
||||
private VBox soundDynamicContent;
|
||||
|
||||
public AtmosphereController(ToolContext ctx) {
|
||||
this.ctx = ctx;
|
||||
@@ -28,6 +31,10 @@ public class AtmosphereController {
|
||||
ctx.input().emitterSelectionChanged = false;
|
||||
updateEmitterPanel(ctx.input().selectedEmitterInfo);
|
||||
}
|
||||
if (ctx.input().soundSelectionChanged) {
|
||||
ctx.input().soundSelectionChanged = false;
|
||||
updateSoundPanel(ctx.input().selectedSoundInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public VBox buildPanel() {
|
||||
@@ -41,15 +48,19 @@ public class AtmosphereController {
|
||||
|
||||
ToggleButton subLichtBtn = new ToggleButton("* Licht");
|
||||
ToggleButton subEmitterBtn = new ToggleButton("~ Emitter");
|
||||
ToggleButton subSoundBtn = new ToggleButton("♪ Sound");
|
||||
subLichtBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
subEmitterBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
subSoundBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
ToggleGroup subGroup = new ToggleGroup();
|
||||
subLichtBtn.setToggleGroup(subGroup);
|
||||
subEmitterBtn.setToggleGroup(subGroup);
|
||||
subSoundBtn.setToggleGroup(subGroup);
|
||||
subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); });
|
||||
HBox subRow = new HBox(4, subLichtBtn, subEmitterBtn);
|
||||
HBox subRow = new HBox(4, subLichtBtn, subEmitterBtn, subSoundBtn);
|
||||
HBox.setHgrow(subLichtBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(subEmitterBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(subSoundBtn, Priority.ALWAYS);
|
||||
panel.getChildren().add(subRow);
|
||||
|
||||
VBox contentBox = new VBox(0);
|
||||
@@ -63,13 +74,21 @@ public class AtmosphereController {
|
||||
ctx.input().activeLayer = SharedInput.LAYER_EMITTERS;
|
||||
contentBox.getChildren().setAll(buildEmitterPanel());
|
||||
};
|
||||
Runnable activateSound = () -> {
|
||||
ctx.input().activeLayer = SharedInput.LAYER_PLACED_SOUNDS;
|
||||
contentBox.getChildren().setAll(buildSoundPanel());
|
||||
};
|
||||
|
||||
subLichtBtn.setOnAction(e -> activateLicht.run());
|
||||
subEmitterBtn.setOnAction(e -> activateEmitter.run());
|
||||
subSoundBtn.setOnAction(e -> activateSound.run());
|
||||
|
||||
if (ctx.input().activeLayer == SharedInput.LAYER_EMITTERS) {
|
||||
subEmitterBtn.setSelected(true);
|
||||
activateEmitter.run();
|
||||
} else if (ctx.input().activeLayer == SharedInput.LAYER_PLACED_SOUNDS) {
|
||||
subSoundBtn.setSelected(true);
|
||||
activateSound.run();
|
||||
} else {
|
||||
subLichtBtn.setSelected(true);
|
||||
activateLicht.run();
|
||||
@@ -282,6 +301,7 @@ public class AtmosphereController {
|
||||
vals[17], vals[18],
|
||||
mxPart[0], emit[0]);
|
||||
ctx.input().pendingEmitter.set(pe);
|
||||
ctx.scheduleAutoSave().run();
|
||||
};
|
||||
|
||||
emitterDynamicContent.getChildren().add(paramLabel("Aktivierungsradius"));
|
||||
@@ -296,14 +316,50 @@ public class AtmosphereController {
|
||||
}
|
||||
|
||||
emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Partikelgröße"));
|
||||
final float initStartSize = vals[8], initEndSize = vals[9];
|
||||
final float initVelX = vals[10], initVelY = vals[11], initVelZ = vals[12];
|
||||
Slider groesseSl = new Slider(0.05, 10.0, 1.0);
|
||||
groesseSl.setBlockIncrement(0.05);
|
||||
Label groesseValLbl = new Label("1.00");
|
||||
groesseValLbl.setStyle("-fx-font-size: 10;");
|
||||
groesseValLbl.setMinWidth(38);
|
||||
groesseSl.valueProperty().addListener((o, ov, nv) -> {
|
||||
float f = nv.floatValue();
|
||||
float sqrtF = (float) Math.sqrt(f);
|
||||
groesseValLbl.setText(String.format("%.2f", f));
|
||||
vals[8] = Math.max(0.01f, initStartSize * sqrtF);
|
||||
vals[9] = Math.max(0.01f, initEndSize * sqrtF);
|
||||
vals[10] = initVelX * f;
|
||||
vals[11] = initVelY * f;
|
||||
vals[12] = initVelZ * f;
|
||||
publish.run();
|
||||
});
|
||||
Label groesseNameLbl = new Label("Groesse");
|
||||
groesseNameLbl.setStyle("-fx-font-size: 10;");
|
||||
groesseNameLbl.setMinWidth(120);
|
||||
HBox groesseRow = new HBox(4, groesseNameLbl, groesseSl, groesseValLbl);
|
||||
HBox.setHgrow(groesseSl, Priority.ALWAYS);
|
||||
emitterDynamicContent.getChildren().add(groesseRow);
|
||||
double[] sizeFactors = {0.1, 0.3, 1.0, 3.0, 8.0};
|
||||
String[] sizeLabels = {"XS","S","M","L","XL"};
|
||||
HBox sizeRow = new HBox(4);
|
||||
for (int pi = 0; pi < sizeLabels.length; pi++) {
|
||||
Button sb = new Button(sizeLabels[pi]);
|
||||
sb.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(sb, Priority.ALWAYS);
|
||||
final double factor = sizeFactors[pi];
|
||||
sb.setOnAction(ev -> groesseSl.setValue(factor));
|
||||
sizeRow.getChildren().add(sb);
|
||||
}
|
||||
emitterDynamicContent.getChildren().add(sizeRow);
|
||||
emitterDynamicContent.getChildren().add(buildSlider("Start-Größe", vals, 8, 0.01, 5, 0.05, publish));
|
||||
emitterDynamicContent.getChildren().add(buildSlider("Ende-Größe", vals, 9, 0.01, 5, 0.05, publish));
|
||||
|
||||
emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Geschwindigkeit"));
|
||||
emitterDynamicContent.getChildren().add(buildSlider("Vel X", vals, 10, -10, 10, 0.1, publish));
|
||||
emitterDynamicContent.getChildren().add(buildSlider("Vel Y", vals, 11, 0, 15, 0.1, publish));
|
||||
emitterDynamicContent.getChildren().add(buildSlider("Vel Z", vals, 12, -10, 10, 0.1, publish));
|
||||
emitterDynamicContent.getChildren().add(buildSlider("Variation", vals, 13, 0, 1, 0.05, publish));
|
||||
emitterDynamicContent.getChildren().add(buildSlider("Tempo X", vals, 10, -5, 5, 0.05, publish));
|
||||
emitterDynamicContent.getChildren().add(buildSlider("Tempo Y", vals, 11, -5, 5, 0.05, publish));
|
||||
emitterDynamicContent.getChildren().add(buildSlider("Tempo Z", vals, 12, -5, 5, 0.05, publish));
|
||||
emitterDynamicContent.getChildren().add(buildSlider("Streuung (0=eng)", vals, 13, 0, 1, 0.05, publish));
|
||||
|
||||
emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Gravitation"));
|
||||
emitterDynamicContent.getChildren().add(buildSlider("Grav Y", vals, 15, -20, 20, 0.5, publish));
|
||||
@@ -376,4 +432,128 @@ public class AtmosphereController {
|
||||
int dot = name.lastIndexOf('.');
|
||||
return dot > 0 ? name.substring(0, dot) : name;
|
||||
}
|
||||
|
||||
// ── Sound-Panel ───────────────────────────────────────────────────────────
|
||||
|
||||
private VBox buildSoundPanel() {
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
inner.getChildren().addAll(
|
||||
UiHelper.sectionTitle("Sound-Quellen"),
|
||||
new Separator(),
|
||||
UiHelper.sectionTitle("Gewählte Quelle"),
|
||||
new Separator());
|
||||
|
||||
soundDynamicContent = new VBox(6);
|
||||
Label noSel = new Label("Keine Quelle ausgewählt");
|
||||
noSel.setStyle("-fx-text-fill: #888;");
|
||||
soundDynamicContent.getChildren().add(noSel);
|
||||
inner.getChildren().add(soundDynamicContent);
|
||||
|
||||
inner.getChildren().addAll(
|
||||
new Separator(),
|
||||
UiHelper.styledHint("Linksklick → platzieren / auswählen"),
|
||||
UiHelper.styledHint("Rechtsklick → Auswahl aufheben"),
|
||||
UiHelper.styledHint("Entf → Quelle löschen"));
|
||||
|
||||
ScrollPane scroll = new ScrollPane(inner);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
|
||||
|
||||
VBox panel = new VBox(scroll);
|
||||
VBox.setVgrow(scroll, Priority.ALWAYS);
|
||||
panel.setPrefWidth(270);
|
||||
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
|
||||
return panel;
|
||||
}
|
||||
|
||||
public void updateSoundPanel(String info) {
|
||||
if (soundDynamicContent == null) return;
|
||||
soundDynamicContent.getChildren().clear();
|
||||
|
||||
if (info == null) {
|
||||
Label noSel = new Label("Keine Quelle ausgewählt");
|
||||
noSel.setStyle("-fx-text-fill: #888;");
|
||||
soundDynamicContent.getChildren().add(noSel);
|
||||
return;
|
||||
}
|
||||
|
||||
String[] parts = info.split("\\|", -1);
|
||||
// format: idx|x|y|z|soundPath|volume|radius
|
||||
if (parts.length < 7) return;
|
||||
try {
|
||||
float x = Float.parseFloat(parts[1]);
|
||||
float y = Float.parseFloat(parts[2]);
|
||||
float z = Float.parseFloat(parts[3]);
|
||||
String path0 = parts[4];
|
||||
float vol0 = Float.parseFloat(parts[5]);
|
||||
float rad0 = Float.parseFloat(parts[6]);
|
||||
|
||||
Label posLabel = new Label(String.format("X: %.1f Y: %.1f Z: %.1f", x, y, z));
|
||||
posLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
|
||||
soundDynamicContent.getChildren().addAll(posLabel, new Separator());
|
||||
|
||||
final String[] curPath = {path0};
|
||||
final float[] curVol = {vol0};
|
||||
final float[] curRad = {rad0};
|
||||
|
||||
Runnable publish = () -> {
|
||||
ctx.input().pendingSoundProp.set(
|
||||
new SharedInput.SoundPropertyChange(curPath[0], curVol[0], curRad[0]));
|
||||
ctx.scheduleAutoSave().run();
|
||||
};
|
||||
|
||||
// Sound-Datei wählen
|
||||
Label soundLbl = new Label("Sound:");
|
||||
soundLbl.setStyle("-fx-font-size: 11;");
|
||||
Button soundBtn = new Button(shortFileName(curPath[0]));
|
||||
soundBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
soundBtn.setOnAction(e -> Platform.runLater(() -> {
|
||||
SoundChooser chooser = new SoundChooser(ctx.assetRoot(), SoundChooser.Mode.ALL);
|
||||
chooser.showAndWait().ifPresent(p -> {
|
||||
curPath[0] = p;
|
||||
soundBtn.setText(shortFileName(p));
|
||||
publish.run();
|
||||
});
|
||||
}));
|
||||
soundDynamicContent.getChildren().addAll(soundLbl, soundBtn, new Separator());
|
||||
|
||||
// Lautstärke
|
||||
Label volLbl = new Label("Lautstärke");
|
||||
volLbl.setStyle("-fx-font-size: 11;");
|
||||
Slider volSl = new Slider(0.0, 1.0, curVol[0]);
|
||||
volSl.setBlockIncrement(0.05);
|
||||
volSl.valueProperty().addListener((o, ov, nv) -> {
|
||||
curVol[0] = nv.floatValue();
|
||||
publish.run();
|
||||
});
|
||||
soundDynamicContent.getChildren().addAll(volLbl, UiHelper.withField(volSl, "%.2f"));
|
||||
|
||||
// Radius
|
||||
Label radLbl = new Label("Radius");
|
||||
radLbl.setStyle("-fx-font-size: 11;");
|
||||
Slider radSl = new Slider(0.5, 200.0, curRad[0]);
|
||||
radSl.setBlockIncrement(1.0);
|
||||
radSl.valueProperty().addListener((o, ov, nv) -> {
|
||||
curRad[0] = nv.floatValue();
|
||||
publish.run();
|
||||
});
|
||||
soundDynamicContent.getChildren().addAll(radLbl, UiHelper.withField(radSl, "%.1f"));
|
||||
|
||||
Button delBtn = new Button("Loeschen");
|
||||
delBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
delBtn.setStyle("-fx-text-fill: #c0392b;");
|
||||
delBtn.setOnAction(e -> ctx.input().deleteSoundRequested = true);
|
||||
soundDynamicContent.getChildren().addAll(new Separator(), delBtn);
|
||||
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
private String shortFileName(String path) {
|
||||
if (path == null || path.isEmpty()) return "(kein Sound)";
|
||||
String name = java.nio.file.Paths.get(path).getFileName().toString();
|
||||
int dot = name.lastIndexOf('.');
|
||||
return dot > 0 ? name.substring(0, dot) : name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ public class ModelEditorController implements ToolController {
|
||||
private final java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorEmitters = new java.util.ArrayList<>();
|
||||
private VBox modelEditorLightBox = null;
|
||||
private VBox modelEditorEmitterBox = null;
|
||||
private VBox modelEditorSoundBox = null;
|
||||
private final java.util.List<de.blight.common.ModelMeta.AttachedSound> modelEditorSounds = new java.util.ArrayList<>();
|
||||
private ComboBox<de.blight.common.ModelCategory> modelEditorCategoryCB = null;
|
||||
private ComboBox<de.blight.common.model.InteractableType> modelEditorInteractableCB = null;
|
||||
private Spinner<Double> modelEditorInteractableXSpin = null;
|
||||
@@ -555,8 +557,8 @@ public class ModelEditorController implements ToolController {
|
||||
HBox lod2Row = new HBox(4, modelEditorLod2GenBtn, lod2ClearBtn);
|
||||
HBox.setHgrow(modelEditorLod2GenBtn, Priority.ALWAYS);
|
||||
|
||||
// Anhänge: Lichter & Partikel
|
||||
Label attachTitle = new Label("Anhänge: Lichter & Partikel");
|
||||
// Anhänge: Lichter, Partikel & Sounds
|
||||
Label attachTitle = new Label("Anhänge: Lichter, Partikel & Sound");
|
||||
attachTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
||||
|
||||
Label lightSectionLbl = new Label("Lichtquellen:");
|
||||
@@ -591,12 +593,29 @@ public class ModelEditorController implements ToolController {
|
||||
addEmitterBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
addEmitterBtn.setStyle("-fx-background-color:#4a2e2e;");
|
||||
addEmitterBtn.setOnAction(ev -> {
|
||||
modelEditorEmitters.add(new de.blight.common.ModelMeta.AttachedEmitter(
|
||||
0f, 0.5f, 0f, 0));
|
||||
modelEditorEmitters.add(de.blight.common.ModelMeta.AttachedEmitter.fire(0f, 0.5f, 0f));
|
||||
rebuildEmitterBox();
|
||||
pushAttachmentsToJme();
|
||||
});
|
||||
|
||||
Label soundSectionLbl = new Label("Sound-Quellen:");
|
||||
soundSectionLbl.setStyle("-fx-text-fill:#aaa;");
|
||||
|
||||
modelEditorSounds.clear();
|
||||
modelEditorSounds.addAll(meta.attachedSounds());
|
||||
|
||||
modelEditorSoundBox = new VBox(4);
|
||||
rebuildSoundBox();
|
||||
|
||||
Button addSoundBtn = new Button("+ Sound-Quelle");
|
||||
addSoundBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
addSoundBtn.setStyle("-fx-background-color:#2e3a4a;");
|
||||
addSoundBtn.setOnAction(ev -> {
|
||||
modelEditorSounds.add(new de.blight.common.ModelMeta.AttachedSound(0f, 0.5f, 0f, "", 1.0f, 20.0f));
|
||||
rebuildSoundBox();
|
||||
pushAttachmentsToJme();
|
||||
});
|
||||
|
||||
pushAttachmentsToJme();
|
||||
|
||||
// Modell-Kategorie
|
||||
@@ -811,6 +830,7 @@ public class ModelEditorController implements ToolController {
|
||||
modelEditorLod1Path, modelEditorLod2Path,
|
||||
new java.util.ArrayList<>(modelEditorLights),
|
||||
new java.util.ArrayList<>(modelEditorEmitters),
|
||||
new java.util.ArrayList<>(modelEditorSounds),
|
||||
modelEditorInteractableCB != null
|
||||
? modelEditorInteractableCB.getValue()
|
||||
: de.blight.common.model.InteractableType.NONE,
|
||||
@@ -861,6 +881,7 @@ public class ModelEditorController implements ToolController {
|
||||
attachTitle,
|
||||
lightSectionLbl, modelEditorLightBox, addLightBtn,
|
||||
emitterSectionLbl, modelEditorEmitterBox, addEmitterBtn,
|
||||
soundSectionLbl, modelEditorSoundBox, addSoundBtn,
|
||||
new Separator(),
|
||||
catTitle, modelEditorCategoryCB,
|
||||
new Separator(),
|
||||
@@ -1342,6 +1363,7 @@ public class ModelEditorController implements ToolController {
|
||||
String lod1Path, String lod2Path,
|
||||
java.util.List<de.blight.common.ModelMeta.AttachedLight> lights,
|
||||
java.util.List<de.blight.common.ModelMeta.AttachedEmitter> emitters,
|
||||
java.util.List<de.blight.common.ModelMeta.AttachedSound> sounds,
|
||||
de.blight.common.model.InteractableType interactableType,
|
||||
float interactableOffsetX, float interactableOffsetY,
|
||||
float interactableOffsetZ, float interactableRotY,
|
||||
@@ -1351,7 +1373,7 @@ public class ModelEditorController implements ToolController {
|
||||
name, category, tags, 1f, 1f, 1f, uniform,
|
||||
pivotY, placeY, solid, cast, receive, rndMin, rndMax,
|
||||
lod1Path, lod2Path, 30f, 80f, 120f,
|
||||
lights, emitters,
|
||||
lights, emitters, sounds,
|
||||
interactableType != null ? interactableType : de.blight.common.model.InteractableType.NONE,
|
||||
interactableOffsetX, interactableOffsetY, interactableOffsetZ, interactableRotY,
|
||||
footstepSurface != null ? footstepSurface : "",
|
||||
@@ -1460,6 +1482,7 @@ public class ModelEditorController implements ToolController {
|
||||
private void pushAttachmentsToJme() {
|
||||
ctx.input().modelEditorAttachedLights = new java.util.ArrayList<>(modelEditorLights);
|
||||
ctx.input().modelEditorAttachedEmitters = new java.util.ArrayList<>(modelEditorEmitters);
|
||||
ctx.input().modelEditorAttachedSounds = new java.util.ArrayList<>(modelEditorSounds);
|
||||
ctx.input().modelEditorAttachmentsChanged = true;
|
||||
}
|
||||
|
||||
@@ -1553,34 +1576,134 @@ public class ModelEditorController implements ToolController {
|
||||
tp.setExpanded(false);
|
||||
tp.setStyle("-fx-text-fill:#ddd; -fx-background-color:#333;");
|
||||
|
||||
GridPane g = new GridPane();
|
||||
g.setHgap(6); g.setVgap(3);
|
||||
g.setPadding(new Insets(4));
|
||||
|
||||
// Offset-Spinner
|
||||
Spinner<Double> ox = buildOffsetSpinner(ae.offsetX());
|
||||
Spinner<Double> oy = buildOffsetSpinner(ae.offsetY());
|
||||
Spinner<Double> oz = buildOffsetSpinner(ae.offsetZ());
|
||||
addLabeledRow(g, 0, "Offset X:", ox);
|
||||
addLabeledRow(g, 1, "Offset Y:", oy);
|
||||
addLabeledRow(g, 2, "Offset Z:", oz);
|
||||
GridPane offsetGrid = new GridPane();
|
||||
offsetGrid.setHgap(6); offsetGrid.setVgap(3);
|
||||
offsetGrid.setPadding(new Insets(4, 4, 0, 4));
|
||||
addLabeledRow(offsetGrid, 0, "Offset X:", ox);
|
||||
addLabeledRow(offsetGrid, 1, "Offset Y:", oy);
|
||||
addLabeledRow(offsetGrid, 2, "Offset Z:", oz);
|
||||
|
||||
ComboBox<String> presetCB = new ComboBox<>();
|
||||
presetCB.getItems().addAll("Feuer", "Rauch", "Funken");
|
||||
presetCB.getSelectionModel().select(Math.max(0, Math.min(2, ae.preset())));
|
||||
presetCB.setMaxWidth(Double.MAX_VALUE);
|
||||
g.add(new Label("Typ:") {{ setStyle("-fx-text-fill:#aaa;"); }}, 0, 3);
|
||||
g.add(presetCB, 1, 3);
|
||||
// Mutable state for all parameters
|
||||
float[] vals = {
|
||||
ae.startR(), ae.startG(), ae.startB(), ae.startA(), // 0-3
|
||||
ae.endR(), ae.endG(), ae.endB(), ae.endA(), // 4-7
|
||||
ae.startSize(), ae.endSize(), // 8-9
|
||||
ae.velX(), ae.velY(), ae.velZ(), ae.velocityVariation(), // 10-13
|
||||
ae.gravX(), ae.gravY(), ae.gravZ(), // 14-16
|
||||
ae.lowLife(), ae.highLife(), ae.emitRate() // 17-19
|
||||
};
|
||||
int[] mxPart = {ae.maxParticles()};
|
||||
|
||||
Runnable sync = () -> {
|
||||
modelEditorEmitters.set(idx, new de.blight.common.ModelMeta.AttachedEmitter(
|
||||
ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue(),
|
||||
presetCB.getSelectionModel().getSelectedIndex()));
|
||||
ae.texturePath(), ae.imagesX(), ae.imagesY(),
|
||||
vals[0], vals[1], vals[2], vals[3],
|
||||
vals[4], vals[5], vals[6], vals[7],
|
||||
vals[8], vals[9],
|
||||
vals[10], vals[11], vals[12], vals[13],
|
||||
vals[14], vals[15], vals[16],
|
||||
vals[17], vals[18],
|
||||
mxPart[0], vals[19]));
|
||||
pushAttachmentsToJme();
|
||||
};
|
||||
ox.valueProperty().addListener((o,ov,nv) -> sync.run());
|
||||
oy.valueProperty().addListener((o,ov,nv) -> sync.run());
|
||||
oz.valueProperty().addListener((o,ov,nv) -> sync.run());
|
||||
presetCB.valueProperty().addListener((o,ov,nv) -> sync.run());
|
||||
ox.valueProperty().addListener((o, ov, nv) -> sync.run());
|
||||
oy.valueProperty().addListener((o, ov, nv) -> sync.run());
|
||||
oz.valueProperty().addListener((o, ov, nv) -> sync.run());
|
||||
|
||||
// Preset-Buttons
|
||||
Button btnFire = new Button("Feuer");
|
||||
Button btnSmoke = new Button("Rauch");
|
||||
Button btnSparks = new Button("Funken");
|
||||
for (Button b : new Button[]{btnFire, btnSmoke, btnSparks}) {
|
||||
b.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(b, Priority.ALWAYS);
|
||||
}
|
||||
btnFire.setOnAction(ev -> applyEmitterPreset(idx, de.blight.common.ModelMeta.AttachedEmitter.fire(
|
||||
ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue())));
|
||||
btnSmoke.setOnAction(ev -> applyEmitterPreset(idx, de.blight.common.ModelMeta.AttachedEmitter.smoke(
|
||||
ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue())));
|
||||
btnSparks.setOnAction(ev -> applyEmitterPreset(idx, de.blight.common.ModelMeta.AttachedEmitter.sparks(
|
||||
ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue())));
|
||||
|
||||
VBox content = new VBox(4);
|
||||
content.setPadding(new Insets(2));
|
||||
content.getChildren().add(offsetGrid);
|
||||
content.getChildren().add(new HBox(4, btnFire, btnSmoke, btnSparks));
|
||||
|
||||
content.getChildren().addAll(emSep(), emLabel("Startfarbe"));
|
||||
content.getChildren().add(emSlider("Start R", vals, 0, 0, 1, 0.01, sync));
|
||||
content.getChildren().add(emSlider("Start G", vals, 1, 0, 1, 0.01, sync));
|
||||
content.getChildren().add(emSlider("Start B", vals, 2, 0, 1, 0.01, sync));
|
||||
content.getChildren().add(emSlider("Start A", vals, 3, 0, 1, 0.01, sync));
|
||||
content.getChildren().addAll(emSep(), emLabel("Endfarbe"));
|
||||
content.getChildren().add(emSlider("Ende R", vals, 4, 0, 1, 0.01, sync));
|
||||
content.getChildren().add(emSlider("Ende G", vals, 5, 0, 1, 0.01, sync));
|
||||
content.getChildren().add(emSlider("Ende B", vals, 6, 0, 1, 0.01, sync));
|
||||
content.getChildren().add(emSlider("Ende A", vals, 7, 0, 1, 0.01, sync));
|
||||
|
||||
content.getChildren().addAll(emSep(), emLabel("Partikelgroesse"));
|
||||
final float initStartSizeE = vals[8], initEndSizeE = vals[9];
|
||||
final float initVelXE = vals[10], initVelYE = vals[11], initVelZE = vals[12];
|
||||
Slider groesseSlE = new Slider(0.05, 10.0, 1.0);
|
||||
groesseSlE.setBlockIncrement(0.05);
|
||||
Label groesseValE = new Label("1.00");
|
||||
groesseValE.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;");
|
||||
groesseValE.setMinWidth(38);
|
||||
groesseSlE.valueProperty().addListener((o, ov, nv) -> {
|
||||
float f = nv.floatValue();
|
||||
float sqrtF = (float) Math.sqrt(f);
|
||||
groesseValE.setText(String.format("%.2f", f));
|
||||
vals[8] = Math.max(0.01f, initStartSizeE * sqrtF);
|
||||
vals[9] = Math.max(0.01f, initEndSizeE * sqrtF);
|
||||
vals[10] = initVelXE * f;
|
||||
vals[11] = initVelYE * f;
|
||||
vals[12] = initVelZE * f;
|
||||
sync.run();
|
||||
});
|
||||
Label groesseNameE = new Label("Groesse");
|
||||
groesseNameE.setStyle("-fx-font-size:10; -fx-text-fill:#ccc;");
|
||||
groesseNameE.setMinWidth(115);
|
||||
HBox groesseRowE = new HBox(4, groesseNameE, groesseSlE, groesseValE);
|
||||
HBox.setHgrow(groesseSlE, Priority.ALWAYS);
|
||||
groesseRowE.setPadding(new Insets(0, 4, 0, 4));
|
||||
content.getChildren().add(groesseRowE);
|
||||
double[] sizeFactors = {0.1, 0.3, 1.0, 3.0, 8.0};
|
||||
String[] sizeLabels = {"XS","S","M","L","XL"};
|
||||
HBox sizeRow = new HBox(4);
|
||||
sizeRow.setPadding(new Insets(0, 4, 0, 4));
|
||||
for (int pi = 0; pi < sizeLabels.length; pi++) {
|
||||
Button sb = new Button(sizeLabels[pi]);
|
||||
sb.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(sb, Priority.ALWAYS);
|
||||
final double factor = sizeFactors[pi];
|
||||
sb.setOnAction(ev -> groesseSlE.setValue(factor));
|
||||
sizeRow.getChildren().add(sb);
|
||||
}
|
||||
content.getChildren().add(sizeRow);
|
||||
content.getChildren().add(emSlider("Start-Groesse", vals, 8, 0.01, 5, 0.05, sync));
|
||||
content.getChildren().add(emSlider("Ende-Groesse", vals, 9, 0.01, 5, 0.05, sync));
|
||||
|
||||
content.getChildren().addAll(emSep(), emLabel("Geschwindigkeit"));
|
||||
content.getChildren().add(emSlider("Tempo X", vals, 10, -5, 5, 0.05, sync));
|
||||
content.getChildren().add(emSlider("Tempo Y", vals, 11, -5, 5, 0.05, sync));
|
||||
content.getChildren().add(emSlider("Tempo Z", vals, 12, -5, 5, 0.05, sync));
|
||||
content.getChildren().add(emSlider("Streuung (0=eng)", vals, 13, 0, 1, 0.05, sync));
|
||||
|
||||
content.getChildren().addAll(emSep(), emLabel("Gravitation"));
|
||||
content.getChildren().add(emSlider("Grav Y", vals, 15, -20, 20, 0.5, sync));
|
||||
|
||||
content.getChildren().addAll(emSep(), emLabel("Lebensdauer (s)"));
|
||||
content.getChildren().add(emSlider("Min", vals, 17, 0.1, 10, 0.1, sync));
|
||||
content.getChildren().add(emSlider("Max", vals, 18, 0.1, 10, 0.1, sync));
|
||||
|
||||
content.getChildren().addAll(emSep(), emLabel("Emission"));
|
||||
content.getChildren().add(emSlider("Max Partikel", mxPart, 0, 1, 500, 5, sync));
|
||||
content.getChildren().add(emSlider("Rate/s", vals, 19, 0.5, 100, 0.5, sync));
|
||||
|
||||
Button del = new Button("✕ Entfernen");
|
||||
del.setStyle("-fx-background-color:#7a2222; -fx-text-fill:#fff;");
|
||||
@@ -1590,13 +1713,68 @@ public class ModelEditorController implements ToolController {
|
||||
rebuildEmitterBox();
|
||||
pushAttachmentsToJme();
|
||||
});
|
||||
content.getChildren().addAll(emSep(), del);
|
||||
|
||||
VBox content = new VBox(4, g, del);
|
||||
content.setPadding(new Insets(2));
|
||||
tp.setContent(content);
|
||||
return tp;
|
||||
}
|
||||
|
||||
private void applyEmitterPreset(int idx, de.blight.common.ModelMeta.AttachedEmitter preset) {
|
||||
modelEditorEmitters.set(idx, preset);
|
||||
rebuildEmitterBox();
|
||||
pushAttachmentsToJme();
|
||||
}
|
||||
|
||||
private static Separator emSep() { return new Separator(); }
|
||||
|
||||
private static Label emLabel(String text) {
|
||||
Label l = new Label(text);
|
||||
l.setStyle("-fx-font-size:11; -fx-font-weight:bold; -fx-text-fill:#ccc;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static HBox emSlider(String name, float[] arr, int idx,
|
||||
double min, double max, double step, Runnable sync) {
|
||||
Label lbl = new Label(name);
|
||||
lbl.setStyle("-fx-font-size:10; -fx-text-fill:#ccc;");
|
||||
lbl.setMinWidth(115);
|
||||
Slider sl = new Slider(min, max, Math.max(min, Math.min(max, arr[idx])));
|
||||
sl.setBlockIncrement(step);
|
||||
Label val = new Label(String.format("%.2f", arr[idx]));
|
||||
val.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;");
|
||||
val.setMinWidth(38);
|
||||
sl.valueProperty().addListener((o, ov, nv) -> {
|
||||
arr[idx] = nv.floatValue();
|
||||
val.setText(String.format("%.2f", arr[idx]));
|
||||
sync.run();
|
||||
});
|
||||
HBox row = new HBox(4, lbl, sl, val);
|
||||
HBox.setHgrow(sl, Priority.ALWAYS);
|
||||
row.setPadding(new Insets(0, 4, 0, 4));
|
||||
return row;
|
||||
}
|
||||
|
||||
private static HBox emSlider(String name, int[] arr, int idx,
|
||||
double min, double max, double step, Runnable sync) {
|
||||
Label lbl = new Label(name);
|
||||
lbl.setStyle("-fx-font-size:10; -fx-text-fill:#ccc;");
|
||||
lbl.setMinWidth(115);
|
||||
Slider sl = new Slider(min, max, Math.max(min, Math.min(max, arr[idx])));
|
||||
sl.setBlockIncrement(step);
|
||||
Label val = new Label(String.format("%.0f", (double) arr[idx]));
|
||||
val.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;");
|
||||
val.setMinWidth(38);
|
||||
sl.valueProperty().addListener((o, ov, nv) -> {
|
||||
arr[idx] = nv.intValue();
|
||||
val.setText(String.format("%.0f", (double) arr[idx]));
|
||||
sync.run();
|
||||
});
|
||||
HBox row = new HBox(4, lbl, sl, val);
|
||||
HBox.setHgrow(sl, Priority.ALWAYS);
|
||||
row.setPadding(new Insets(0, 4, 0, 4));
|
||||
return row;
|
||||
}
|
||||
|
||||
private void importLodFile(int slot) {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("LOD " + slot + " – j3o wählen");
|
||||
@@ -1674,4 +1852,86 @@ public class ModelEditorController implements ToolController {
|
||||
new SharedInput.ModelLodGenRequest(level, method, null, reductionPct));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Sound-Anhänge ─────────────────────────────────────────────────────────
|
||||
|
||||
private void rebuildSoundBox() {
|
||||
if (modelEditorSoundBox == null) return;
|
||||
modelEditorSoundBox.getChildren().clear();
|
||||
for (int i = 0; i < modelEditorSounds.size(); i++) {
|
||||
final int idx = i;
|
||||
modelEditorSoundBox.getChildren().add(buildSoundEntry(idx, modelEditorSounds.get(i)));
|
||||
}
|
||||
}
|
||||
|
||||
private javafx.scene.Node buildSoundEntry(int idx,
|
||||
de.blight.common.ModelMeta.AttachedSound as) {
|
||||
TitledPane tp = new TitledPane();
|
||||
tp.setText("Sound " + (idx + 1));
|
||||
tp.setExpanded(false);
|
||||
tp.setStyle("-fx-text-fill:#ddd; -fx-background-color:#333;");
|
||||
|
||||
GridPane g = new GridPane();
|
||||
g.setHgap(6); g.setVgap(3);
|
||||
g.setPadding(new Insets(4));
|
||||
|
||||
Spinner<Double> ox = buildOffsetSpinner(as.offsetX());
|
||||
Spinner<Double> oy = buildOffsetSpinner(as.offsetY());
|
||||
Spinner<Double> oz = buildOffsetSpinner(as.offsetZ());
|
||||
addLabeledRow(g, 0, "Offset X:", ox);
|
||||
addLabeledRow(g, 1, "Offset Y:", oy);
|
||||
addLabeledRow(g, 2, "Offset Z:", oz);
|
||||
|
||||
final String[] curPath = {as.soundPath()};
|
||||
Button soundBtn = new Button(curPath[0].isEmpty() ? "(kein Sound)" : java.nio.file.Paths.get(curPath[0]).getFileName().toString());
|
||||
soundBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
g.add(new Label("Sound:") {{ setStyle("-fx-text-fill:#aaa;"); }}, 0, 3);
|
||||
g.add(soundBtn, 1, 3);
|
||||
|
||||
Spinner<Double> volSpin = new Spinner<>(0.0, 1.0, as.volume(), 0.05);
|
||||
volSpin.setEditable(true);
|
||||
addLabeledRow(g, 4, "Lautstärke:", volSpin);
|
||||
|
||||
Spinner<Double> radSpin = new Spinner<>(0.5, 200.0, as.radius(), 1.0);
|
||||
radSpin.setEditable(true);
|
||||
addLabeledRow(g, 5, "Radius:", radSpin);
|
||||
|
||||
Runnable sync = () -> {
|
||||
modelEditorSounds.set(idx, new de.blight.common.ModelMeta.AttachedSound(
|
||||
ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue(),
|
||||
curPath[0],
|
||||
volSpin.getValue().floatValue(), radSpin.getValue().floatValue()));
|
||||
pushAttachmentsToJme();
|
||||
};
|
||||
|
||||
soundBtn.setOnAction(ev -> {
|
||||
de.blight.editor.ui.SoundChooser chooser = new de.blight.editor.ui.SoundChooser(
|
||||
ctx.assetRoot(), de.blight.editor.ui.SoundChooser.Mode.ALL);
|
||||
chooser.showAndWait().ifPresent(p -> {
|
||||
curPath[0] = p;
|
||||
soundBtn.setText(java.nio.file.Paths.get(p).getFileName().toString());
|
||||
sync.run();
|
||||
});
|
||||
});
|
||||
|
||||
ox.valueProperty().addListener((o, ov, nv) -> sync.run());
|
||||
oy.valueProperty().addListener((o, ov, nv) -> sync.run());
|
||||
oz.valueProperty().addListener((o, ov, nv) -> sync.run());
|
||||
volSpin.valueProperty().addListener((o, ov, nv) -> sync.run());
|
||||
radSpin.valueProperty().addListener((o, ov, nv) -> sync.run());
|
||||
|
||||
Button del = new Button("✕ Entfernen");
|
||||
del.setStyle("-fx-background-color:#7a2222; -fx-text-fill:#fff;");
|
||||
del.setMaxWidth(Double.MAX_VALUE);
|
||||
del.setOnAction(ev -> {
|
||||
modelEditorSounds.remove(idx);
|
||||
rebuildSoundBox();
|
||||
pushAttachmentsToJme();
|
||||
});
|
||||
|
||||
VBox content = new VBox(4, g, del);
|
||||
content.setPadding(new Insets(2));
|
||||
tp.setContent(content);
|
||||
return tp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,6 +446,8 @@ public class ViewportController {
|
||||
input.lightClickQueue.offer(new SharedInput.LightClick((float) x, (float) y, action < 0));
|
||||
case SharedInput.LAYER_EMITTERS ->
|
||||
input.emitterClickQueue.offer(new SharedInput.EmitterClick((float) x, (float) y, action < 0));
|
||||
case SharedInput.LAYER_PLACED_SOUNDS ->
|
||||
input.soundClickQueue.offer(new SharedInput.SoundClick((float) x, (float) y, action < 0));
|
||||
case SharedInput.LAYER_WATER ->
|
||||
input.waterClickQueue.offer(new SharedInput.WaterClick((float) x, (float) y, action < 0));
|
||||
case SharedInput.LAYER_SOUND_AREAS ->
|
||||
|
||||
Reference in New Issue
Block a user