Initaler Commit

This commit is contained in:
2026-05-07 11:54:11 +02:00
commit b8a0234ad2
158 changed files with 15138 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
plugins {
id 'java'
id 'application'
id 'org.openjfx.javafxplugin' version '0.1.0'
}
group = 'de.blight'
version = '0.1.0'
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
javafx {
version = '21'
modules = ['javafx.controls', 'javafx.swing']
}
application {
mainClass = 'de.blight.editor.EditorLauncher'
applicationDefaultJvmArgs = [
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'--add-opens', 'java.desktop/sun.awt=ALL-UNNAMED',
'-Djava.library.path=${rootDir}/build/natives',
]
}
repositories {
mavenCentral()
}
ext {
jmeVersion = '3.7.0-stable'
}
dependencies {
implementation "org.jmonkeyengine:jme3-core:${jmeVersion}"
implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}"
implementation "org.jmonkeyengine:jme3-lwjgl3:${jmeVersion}"
implementation "org.jmonkeyengine:jme3-terrain:${jmeVersion}"
implementation "org.jmonkeyengine:jme3-effects:${jmeVersion}"
implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}"
}
tasks.register('extractNatives', Copy) {
def nativeConf = configurations.runtimeClasspath.resolvedConfiguration
.resolvedArtifacts
.findAll { it.name.contains('natives') }
.collect { zipTree(it.file) }
from nativeConf
into "${buildDir}/natives"
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
run {
dependsOn extractNatives
workingDir = rootDir
}
jar {
manifest {
attributes 'Main-Class': application.mainClass
}
}

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

249
blight-editor/gradlew vendored Executable file
View File

@@ -0,0 +1,249 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

92
blight-editor/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1 @@
rootProject.name = 'blight-editor'

View File

@@ -0,0 +1,310 @@
package de.blight.editor;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
public class EditorApp extends Application {
// ── Viewport-Auflösung (JME3 rendert intern auf diese Größe) ────────────
static final int VP_WIDTH = 1024;
static final int VP_HEIGHT = 640;
// ── Asset-Verzeichnis ────────────────────────────────────────────────────
private static final Path ASSET_ROOT = Paths.get("editor-assets");
private final SharedInput input = new SharedInput();
private WritableImage jfxImage;
private ImageView viewport;
private Label statusLabel;
// Drag-Tracking für Kamerarotation (mittlere Taste)
private double prevDragX, prevDragY;
// ── Asset-Tree-Items ─────────────────────────────────────────────────────
private TreeItem<String> modelsNode;
private TreeItem<String> texturesNode;
private TreeItem<String> audioNode;
// ── JavaFX Entry-Point ───────────────────────────────────────────────────
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) {
// WritableImage für den JME3-Frame-Export
jfxImage = new WritableImage(VP_WIDTH, VP_HEIGHT);
// JME3 starten (Hintergrund-Daemon-Thread)
JmeEditorApp.launch(input, jfxImage, VP_WIDTH, VP_HEIGHT);
// UI zusammenbauen
BorderPane root = new BorderPane();
root.setTop(buildTop());
root.setLeft(buildAssetPanel());
root.setCenter(buildViewport());
root.setBottom(buildStatusBar());
Scene scene = new Scene(root, 1280, 760);
scene.setOnKeyPressed(e -> handleKeyPress(e.getCode(), true));
scene.setOnKeyReleased(e -> handleKeyPress(e.getCode(), false));
stage.setTitle("Blight World Editor");
stage.setScene(scene);
stage.setMinWidth(900);
stage.setMinHeight(600);
stage.setOnCloseRequest(e -> Platform.exit());
stage.show();
}
// ── Oberer Bereich: MenuBar + ToolBar ────────────────────────────────────
private VBox buildTop() {
// Menüleiste
MenuBar menuBar = new MenuBar();
Menu fileMenu = new Menu("Datei");
MenuItem newItem = new MenuItem("Neue Karte");
MenuItem saveItem = new MenuItem("Speichern");
fileMenu.getItems().addAll(newItem, saveItem);
Menu viewMenu = new Menu("Ansicht");
MenuItem resetCam = new MenuItem("Kamera zurücksetzen");
resetCam.setOnAction(e -> input.addMouseDelta(0, 0)); // noop, Kamera reset via SharedInput wäre aufwändiger
viewMenu.getItems().add(resetCam);
menuBar.getMenus().addAll(fileMenu, viewMenu);
// Werkzeugleiste
ToolBar toolBar = new ToolBar();
Button heightTool = new Button("▲▼ Höhe");
heightTool.setStyle("-fx-font-weight:bold;");
heightTool.setTooltip(new Tooltip(
"Linksklick: Terrain anheben\nRechtsklick: Terrain absenken"));
Separator sep1 = new Separator(Orientation.VERTICAL);
Label brushLabel = new Label("Pinselstärke:");
Slider brushSlider = new Slider(0.1, 1.0, 1.0);
brushSlider.setPrefWidth(100);
brushSlider.setMajorTickUnit(0.5);
brushSlider.setShowTickMarks(true);
Separator sep2 = new Separator(Orientation.VERTICAL);
Label hint = new Label("WASD/QE: Kamera | Mitte-Drag: Drehen | L-Klick: hoch | R-Klick: tief");
hint.setStyle("-fx-text-fill: #555;");
toolBar.getItems().addAll(heightTool, sep1, brushLabel, brushSlider, sep2, hint);
VBox top = new VBox(menuBar, toolBar);
return top;
}
// ── Linke Seite: Asset-Panel ─────────────────────────────────────────────
private VBox buildAssetPanel() {
VBox panel = new VBox(6);
panel.setPadding(new Insets(8));
panel.setPrefWidth(210);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 1 0 0;");
Label title = new Label("Assets");
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
// Baum
TreeItem<String> root = new TreeItem<>("Projekt");
root.setExpanded(true);
modelsNode = new TreeItem<>("Models");
texturesNode = new TreeItem<>("Texturen");
audioNode = new TreeItem<>("Audio");
root.getChildren().addAll(modelsNode, texturesNode, audioNode);
// Bestehende Assets laden
loadAssetsInto(modelsNode, "models", ".j3o", ".obj", ".fbx", ".gltf", ".glb");
loadAssetsInto(texturesNode, "textures", ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds");
loadAssetsInto(audioNode, "audio", ".ogg", ".wav", ".mp3");
TreeView<String> tree = new TreeView<>(root);
tree.setShowRoot(false);
VBox.setVgrow(tree, Priority.ALWAYS);
// Kontextmenü im Baum (Datei öffnen im Dateimanager)
ContextMenu ctx = new ContextMenu();
MenuItem showItem = new MenuItem("Im Dateisystem anzeigen");
ctx.getItems().add(showItem);
tree.setContextMenu(ctx);
// Import-Button
Button importBtn = new Button("⊕ Import…");
importBtn.setMaxWidth(Double.MAX_VALUE);
importBtn.setOnAction(e -> handleImport(tree.getScene().getWindow()));
panel.getChildren().addAll(title, tree, importBtn);
return panel;
}
private void loadAssetsInto(TreeItem<String> parent, String subDir, String... exts) {
Path dir = ASSET_ROOT.resolve(subDir);
if (!Files.exists(dir)) return;
try (var stream = Files.list(dir)) {
stream.filter(p -> {
String name = p.getFileName().toString().toLowerCase();
for (String ext : exts) if (name.endsWith(ext)) return true;
return false;
}).map(p -> new TreeItem<>(p.getFileName().toString()))
.forEach(parent.getChildren()::add);
} catch (IOException ignored) {}
}
private void handleImport(javafx.stage.Window owner) {
FileChooser fc = new FileChooser();
fc.setTitle("Assets importieren");
fc.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Alle unterstützten Dateien",
"*.j3o","*.obj","*.fbx","*.gltf","*.glb",
"*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds",
"*.ogg","*.wav","*.mp3"),
new FileChooser.ExtensionFilter("Modelle", "*.j3o","*.obj","*.fbx","*.gltf","*.glb"),
new FileChooser.ExtensionFilter("Texturen", "*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds"),
new FileChooser.ExtensionFilter("Audio", "*.ogg","*.wav","*.mp3")
);
var files = fc.showOpenMultipleDialog(owner);
if (files == null) return;
for (File file : files) {
String name = file.getName().toLowerCase();
String subDir;
TreeItem<String> parent;
if (name.matches(".*\\.(j3o|obj|fbx|gltf|glb)")) {
subDir = "models"; parent = modelsNode;
} else if (name.matches(".*\\.(ogg|wav|mp3)")) {
subDir = "audio"; parent = audioNode;
} else {
subDir = "textures"; parent = texturesNode;
}
try {
Path dest = ASSET_ROOT.resolve(subDir);
Files.createDirectories(dest);
Path target = dest.resolve(file.getName());
Files.copy(file.toPath(), target, StandardCopyOption.REPLACE_EXISTING);
parent.getChildren().add(new TreeItem<>(file.getName()));
parent.setExpanded(true);
setStatus("Importiert: " + file.getName());
} catch (IOException ex) {
setStatus("Fehler beim Import: " + ex.getMessage());
}
}
}
// ── Zentraler Bereich: JME3-Viewport ────────────────────────────────────
private StackPane buildViewport() {
viewport = new ImageView(jfxImage);
viewport.setPreserveRatio(false);
viewport.setFocusTraversable(true);
StackPane pane = new StackPane(viewport);
pane.setStyle("-fx-background-color: #1a1a2e;");
// ImageView auf Pane-Größe binden → JME3-Pixelskalierung aktualisieren
pane.widthProperty().addListener((o, oldW, newW) -> {
viewport.setFitWidth(newW.doubleValue());
input.viewportScaleX = VP_WIDTH / newW.doubleValue();
});
pane.heightProperty().addListener((o, oldH, newH) -> {
viewport.setFitHeight(newH.doubleValue());
input.viewportScaleY = VP_HEIGHT / newH.doubleValue();
});
// ── Maus-Events ──────────────────────────────────────────────────────
viewport.setOnMousePressed(e -> {
viewport.requestFocus();
if (e.getButton() == MouseButton.MIDDLE) {
prevDragX = e.getX();
prevDragY = e.getY();
}
if (e.getButton() == MouseButton.PRIMARY) {
submitEdit(e.getX(), e.getY(), +1);
}
if (e.getButton() == MouseButton.SECONDARY) {
submitEdit(e.getX(), e.getY(), -1);
}
});
viewport.setOnMouseDragged(e -> {
if (e.isMiddleButtonDown()) {
double dx = e.getX() - prevDragX;
double dy = e.getY() - prevDragY;
input.addMouseDelta((int) dx, (int) dy);
prevDragX = e.getX();
prevDragY = e.getY();
}
if (e.isPrimaryButtonDown()) {
submitEdit(e.getX(), e.getY(), +1);
}
if (e.isSecondaryButtonDown()) {
submitEdit(e.getX(), e.getY(), -1);
}
});
viewport.setOnScroll(e -> {
// Scrollen = Kamera vorwärts/rückwärts
double delta = e.getDeltaY();
input.forward = delta > 0;
input.backward = delta < 0;
// Nach kurzem Delay zurücksetzen (kein physischer Key-Release)
javafx.animation.PauseTransition pause =
new javafx.animation.PauseTransition(javafx.util.Duration.millis(150));
pause.setOnFinished(ev -> { input.forward = false; input.backward = false; });
pause.play();
});
return pane;
}
private void submitEdit(double x, double y, int action) {
input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));
}
// ── Statusleiste ─────────────────────────────────────────────────────────
private HBox buildStatusBar() {
statusLabel = new Label("Bereit | Werkzeug: Höhe | WASD/QE: Bewegen | Mitte-Drag: Drehen");
statusLabel.setPadding(new Insets(3, 8, 3, 8));
statusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #333;");
HBox bar = new HBox(statusLabel);
bar.setStyle("-fx-background-color: #e8e8e8; -fx-border-color: #bbb; -fx-border-width: 1 0 0 0;");
return bar;
}
private void setStatus(String msg) {
Platform.runLater(() -> statusLabel.setText(msg));
}
// ── Tastatur-Handling ────────────────────────────────────────────────────
private void handleKeyPress(KeyCode code, boolean pressed) {
switch (code) {
case W -> input.forward = pressed;
case S -> input.backward = pressed;
case A -> input.left = pressed;
case D -> input.right = pressed;
case Q -> input.up = pressed;
case E -> input.down = pressed;
}
}
}

View File

@@ -0,0 +1,11 @@
package de.blight.editor;
/**
* Separater Launcher-Einstiegspunkt, damit der JavaFX-Klassenpfad korrekt
* aufgelöst wird, bevor Application.launch() aufgerufen wird.
*/
public class EditorLauncher {
public static void main(String[] args) {
EditorApp.main(args);
}
}

View File

@@ -0,0 +1,91 @@
package de.blight.editor;
import com.jme3.post.SceneProcessor;
import com.jme3.profile.AppProfiler;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.Renderer;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.texture.FrameBuffer;
import javafx.application.Platform;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* SceneProcessor: liest nach jedem JME3-Frame den Framebuffer aus und
* schreibt die Pixel (RGBA→ARGB, Y-gespiegelt) in ein JavaFX WritableImage.
*/
public class FrameTransfer implements SceneProcessor {
private final WritableImage image;
private final PixelWriter pw;
private final int width;
private final int height;
private Renderer renderer;
private ByteBuffer cpuBuf;
private byte[] snapshot;
private int[] argbRow; // Zeile für JavaFX-PixelWriter
private final AtomicBoolean jfxBusy = new AtomicBoolean(false);
public FrameTransfer(WritableImage image) {
this.image = image;
this.pw = image.getPixelWriter();
this.width = (int) image.getWidth();
this.height = (int) image.getHeight();
}
@Override
public void initialize(RenderManager rm, ViewPort vp) {
this.renderer = rm.getRenderer();
this.cpuBuf = ByteBuffer.allocateDirect(width * height * 4);
this.snapshot = new byte[width * height * 4];
this.argbRow = new int[width];
}
@Override
public void postFrame(FrameBuffer out) {
if (!jfxBusy.compareAndSet(false, true)) return;
cpuBuf.clear();
renderer.readFrameBuffer(out, cpuBuf);
cpuBuf.rewind();
cpuBuf.get(snapshot);
final byte[] pixels = snapshot.clone();
Platform.runLater(() -> {
try {
// GL: Y=0 unten → JavaFX: Y=0 oben + RGBA → 0xFFRRGGBB (int ARGB)
PixelFormat<IntBuffer> fmt = PixelFormat.getIntArgbInstance();
for (int y = 0; y < height; y++) {
int srcBase = (height - 1 - y) * width * 4;
for (int x = 0; x < width; x++) {
int r = pixels[srcBase + x * 4 ] & 0xFF;
int g = pixels[srcBase + x * 4 + 1] & 0xFF;
int b = pixels[srcBase + x * 4 + 2] & 0xFF;
argbRow[x] = 0xFF000000 | (r << 16) | (g << 8) | b;
}
pw.setPixels(0, y, width, 1, fmt, argbRow, 0, width);
}
} finally {
jfxBusy.set(false);
}
});
}
// ── Pflichtmethoden ──────────────────────────────────────────────────────
@Override public boolean isInitialized() { return renderer != null; }
@Override public void reshape(ViewPort vp, int w, int h) {}
@Override public void preFrame(float tpf) {}
@Override public void postQueue(RenderQueue rq) {}
@Override public void cleanup() {}
@Override public void setProfiler(AppProfiler profiler) {}
}

View File

@@ -0,0 +1,53 @@
package de.blight.editor;
import com.jme3.app.SimpleApplication;
import com.jme3.system.AppSettings;
import com.jme3.system.JmeContext;
import de.blight.editor.state.TerrainEditorState;
import javafx.scene.image.WritableImage;
public class JmeEditorApp extends SimpleApplication {
private final SharedInput input;
private final WritableImage jfxImage;
public JmeEditorApp(SharedInput input, WritableImage jfxImage) {
this.input = input;
this.jfxImage = jfxImage;
}
/** Startet JME3 in einem Daemon-Thread (blockierend bis App endet). */
public static JmeEditorApp launch(SharedInput input, WritableImage jfxImage,
int vpWidth, int vpHeight) {
JmeEditorApp app = new JmeEditorApp(input, jfxImage);
AppSettings settings = new AppSettings(true);
settings.setTitle("Blight Editor JME3");
settings.setResolution(vpWidth, vpHeight);
settings.setRenderer(AppSettings.LWJGL_OPENGL32);
settings.setAudioRenderer(null);
settings.setSamples(4);
app.setSettings(settings);
app.setShowSettings(false);
app.setPauseOnLostFocus(false);
Thread t = new Thread(() -> app.start(JmeContext.Type.OffscreenSurface), "jme3-editor");
t.setDaemon(true);
t.start();
return app;
}
@Override
public void simpleInitApp() {
flyCam.setEnabled(false);
// Frame-Export in das JavaFX-WritableImage
viewPort.addProcessor(new FrameTransfer(jfxImage));
stateManager.attach(new TerrainEditorState(input));
}
@Override
public void simpleUpdate(float tpf) {}
}

View File

@@ -0,0 +1,33 @@
package de.blight.editor;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
/** Thread-safe Brücke: JavaFX-Events → JME3-Update-Schleife. */
public class SharedInput {
// ── Kamerabewegung (WASD + QE) ──────────────────────────────────────────
public volatile boolean forward, backward, left, right, up, down;
// ── Kamerarotation (Maus-Drag mit mittlerer Taste) ───────────────────────
private final AtomicInteger mouseDxAccum = new AtomicInteger();
private final AtomicInteger mouseDyAccum = new AtomicInteger();
public void addMouseDelta(int dx, int dy) {
mouseDxAccum.addAndGet(dx);
mouseDyAccum.addAndGet(dy);
}
/** Gibt akkumulierten Maus-Delta zurück und setzt ihn zurück. */
public int[] consumeMouseDelta() {
return new int[]{ mouseDxAccum.getAndSet(0), mouseDyAccum.getAndSet(0) };
}
// ── Terrain-Edits ────────────────────────────────────────────────────────
public record TerrainEdit(float screenX, float screenY, int action) {}
public final ConcurrentLinkedQueue<TerrainEdit> editQueue = new ConcurrentLinkedQueue<>();
// ── Viewport-Skalierung (JavaFX-Pixel → JME3-Pixel) ─────────────────────
public volatile double viewportScaleX = 1.0;
public volatile double viewportScaleY = 1.0;
}

View File

@@ -0,0 +1,293 @@
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.light.AmbientLight;
import com.jme3.light.DirectionalLight;
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.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.shape.Quad;
import com.jme3.util.BufferUtils;
import de.blight.editor.SharedInput;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
public class TerrainEditorState extends BaseAppState {
// ── Konstanten ──────────────────────────────────────────────────────────
private static final int V = 17; // Vertices pro Achse (16 Zellen)
private static final float BRUSH_RADIUS = 2.0f; // Meter
private static final float BRUSH_DELTA = 0.25f; // Höhenänderung pro Klick
private static final float CAM_SPEED = 12f;
private static final float MOUSE_SENS = 0.003f;
// ── Zustand ─────────────────────────────────────────────────────────────
private SimpleApplication app;
private Camera cam;
private AssetManager assets;
private Node rootNode;
private final SharedInput input;
private final float[] heights = new float[V * V]; // flaches Array, Index = z*V+x
private Mesh terrainMesh;
private Geometry terrainGeo;
// Kamera-Euler-Winkel
private float camYaw = 0f;
private float camPitch = -0.4f;
private final Vector3f camPos = new Vector3f(0, 14, 22);
public TerrainEditorState(SharedInput input) {
this.input = input;
}
// ── Lifecycle ────────────────────────────────────────────────────────────
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.cam = app.getCamera();
this.assets = app.getAssetManager();
this.rootNode = this.app.getRootNode();
}
@Override
protected void onEnable() {
buildScene();
applyCameraTransform();
}
@Override
protected void onDisable() {
rootNode.detachAllChildren();
}
@Override protected void cleanup(Application app) {}
// ── Szene aufbauen ───────────────────────────────────────────────────────
private void buildScene() {
// Licht
DirectionalLight sun = new DirectionalLight();
sun.setDirection(new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal());
sun.setColor(new ColorRGBA(1.2f, 1.1f, 0.9f, 1f));
rootNode.addLight(sun);
AmbientLight ambient = new AmbientLight(new ColorRGBA(0.35f, 0.38f, 0.45f, 1f));
rootNode.addLight(ambient);
// Terrain
terrainGeo = buildTerrainGeometry();
rootNode.attachChild(terrainGeo);
// Wasser bei Y = 0
rootNode.attachChild(buildWater());
// Raster-Linien auf dem Terrain (als dünne Linien-Node wäre komplex, Gitter via Grid-Overlay)
// Einfache Grid-Markierung: ein flaches transparentes Quad mit Wireframe
rootNode.attachChild(buildGridOverlay());
// Himmel (einfacher Hintergrund-Farbverlauf über Viewport-BG-Farbe)
app.getViewPort().setBackgroundColor(new ColorRGBA(0.45f, 0.60f, 0.80f, 1f));
}
private Geometry buildTerrainGeometry() {
terrainMesh = new Mesh();
FloatBuffer posBuf = BufferUtils.createFloatBuffer(V * V * 3);
FloatBuffer normBuf = BufferUtils.createFloatBuffer(V * V * 3);
FloatBuffer texBuf = BufferUtils.createFloatBuffer(V * V * 2);
IntBuffer idxBuf = BufferUtils.createIntBuffer((V - 1) * (V - 1) * 6);
for (int z = 0; z < V; z++) {
for (int x = 0; x < V; x++) {
posBuf.put(x).put(heights[z * V + x]).put(z);
normBuf.put(0).put(1).put(0);
texBuf.put(x / 16f).put(z / 16f);
}
}
for (int z = 0; z < V - 1; z++) {
for (int x = 0; x < V - 1; x++) {
int bl = z * V + x, br = bl + 1;
int tl = bl + V, tr = tl + 1;
idxBuf.put(bl).put(tr).put(br);
idxBuf.put(bl).put(tl).put(tr);
}
}
terrainMesh.setBuffer(VertexBuffer.Type.Position, 3, posBuf);
terrainMesh.setBuffer(VertexBuffer.Type.Normal, 3, normBuf);
terrainMesh.setBuffer(VertexBuffer.Type.TexCoord, 2, texBuf);
terrainMesh.setBuffer(VertexBuffer.Type.Index, 3, idxBuf);
terrainMesh.updateBound();
Geometry geo = new Geometry("terrain", terrainMesh);
geo.setLocalTranslation(-8, 0, -8); // Terrain zentriert bei Ursprung
Material mat = new Material(assets, "Common/MatDefs/Light/Lighting.j3md");
mat.setBoolean("UseMaterialColors", true);
mat.setColor("Diffuse", new ColorRGBA(0.28f, 0.58f, 0.18f, 1f));
mat.setColor("Ambient", new ColorRGBA(0.12f, 0.28f, 0.08f, 1f));
mat.setColor("Specular", ColorRGBA.Black);
mat.setFloat("Shininess", 0f);
geo.setMaterial(mat);
return geo;
}
private Geometry buildWater() {
Geometry water = new Geometry("water", new Quad(16, 16));
water.rotate(-FastMath.HALF_PI, 0, 0);
water.setLocalTranslation(-8, 0.01f, 8); // leicht über Y=0 damit kein Z-Fighting
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0.05f, 0.25f, 0.70f, 0.55f));
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
water.setQueueBucket(RenderQueue.Bucket.Transparent);
water.setMaterial(mat);
return water;
}
private Geometry buildGridOverlay() {
// Einfaches Wireframe-Duplikat des Terrains als Gitter
Geometry grid = new Geometry("grid", terrainMesh);
grid.setLocalTranslation(-8, 0.02f, -8);
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0f, 0f, 0f, 0.25f));
mat.getAdditionalRenderState().setWireframe(true);
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
grid.setQueueBucket(RenderQueue.Bucket.Transparent);
grid.setMaterial(mat);
return grid;
}
// ── Update-Schleife ──────────────────────────────────────────────────────
@Override
public void update(float tpf) {
updateCamera(tpf);
processEdits();
}
private void updateCamera(float tpf) {
// Rotation aus akkumuliertem Maus-Delta
int[] delta = input.consumeMouseDelta();
if (delta[0] != 0 || delta[1] != 0) {
camYaw -= delta[0] * MOUSE_SENS;
camPitch -= delta[1] * MOUSE_SENS;
camPitch = FastMath.clamp(camPitch,
-FastMath.HALF_PI + 0.05f,
FastMath.HALF_PI - 0.05f);
}
applyCameraTransform();
// Bewegung in Kamera-Vorwärtsrichtung projiziert auf XZ-Ebene
float speed = CAM_SPEED * tpf;
Vector3f fwd = cam.getDirection().clone().setY(0);
if (fwd.lengthSquared() > 0.001f) fwd.normalizeLocal();
Vector3f lft = cam.getLeft().clone().setY(0);
if (lft.lengthSquared() > 0.001f) lft.normalizeLocal();
if (input.forward) camPos.addLocal(fwd.mult(speed));
if (input.backward) camPos.subtractLocal(fwd.mult(speed));
if (input.left) camPos.addLocal(lft.mult(speed));
if (input.right) camPos.subtractLocal(lft.mult(speed));
if (input.up) camPos.y += speed;
if (input.down) camPos.y -= speed;
cam.setLocation(camPos);
}
private void applyCameraTransform() {
Quaternion yawQ = new Quaternion().fromAngleAxis(camYaw, Vector3f.UNIT_Y);
Quaternion pitchQ = new Quaternion().fromAngleAxis(camPitch, Vector3f.UNIT_X);
cam.setRotation(yawQ.mult(pitchQ));
cam.setLocation(camPos);
}
private void processEdits() {
SharedInput.TerrainEdit edit;
while ((edit = input.editQueue.poll()) != null) {
// JavaFX-Koordinaten → JME3-Screen-Koordinaten (Y spiegeln)
float jmeX = (float)(edit.screenX() * input.viewportScaleX);
float jmeY = cam.getHeight() - (float)(edit.screenY() * input.viewportScaleY);
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
Vector3f dir = far.subtract(near).normalizeLocal();
CollisionResults hits = new CollisionResults();
terrainGeo.collideWith(new com.jme3.math.Ray(near, dir), hits);
if (hits.size() > 0) {
Vector3f contact = hits.getClosestCollision().getContactPoint();
modifyHeight(contact, edit.action() * BRUSH_DELTA);
}
}
}
// ── Höhen-Werkzeug ───────────────────────────────────────────────────────
private void modifyHeight(Vector3f worldContact, float delta) {
// Terrain-Geometrie ist bei (-8, 0, -8), Vertices bei (0..16, h, 0..16)
float localX = worldContact.x + 8;
float localZ = worldContact.z + 8;
for (int z = 0; z < V; z++) {
for (int x = 0; x < V; x++) {
float dx = x - localX;
float dz = z - localZ;
float dist = FastMath.sqrt(dx * dx + dz * dz);
if (dist < BRUSH_RADIUS) {
float falloff = 1f - dist / BRUSH_RADIUS;
heights[z * V + x] += delta * falloff;
}
}
}
updateTerrainMesh();
}
private void updateTerrainMesh() {
FloatBuffer posBuf = terrainMesh.getFloatBuffer(VertexBuffer.Type.Position);
FloatBuffer normBuf = terrainMesh.getFloatBuffer(VertexBuffer.Type.Normal);
posBuf.rewind();
for (int z = 0; z < V; z++) {
for (int x = 0; x < V; x++) {
posBuf.put(x).put(heights[z * V + x]).put(z);
}
}
// Normalen per finite differences
normBuf.rewind();
for (int z = 0; z < V; z++) {
for (int x = 0; x < V; x++) {
float hL = x > 0 ? heights[z * V + (x - 1)] : heights[z * V + x];
float hR = x < V-1 ? heights[z * V + (x + 1)] : heights[z * V + x];
float hD = z > 0 ? heights[(z - 1) * V + x] : heights[z * V + x];
float hU = z < V-1 ? heights[(z + 1) * V + x] : heights[z * V + x];
float nx = -(hR - hL);
float ny = 2.0f;
float nz = -(hU - hD);
float len = FastMath.sqrt(nx*nx + ny*ny + nz*nz);
normBuf.put(nx / len).put(ny / len).put(nz / len);
}
}
terrainMesh.getBuffer(VertexBuffer.Type.Position).setUpdateNeeded();
terrainMesh.getBuffer(VertexBuffer.Type.Normal).setUpdateNeeded();
terrainMesh.updateBound();
terrainGeo.updateModelBound();
}
}