Commit c06d3a89 authored by Luna Riegel's avatar Luna Riegel
Browse files

Merge branch 'dev' into dev_topo_checks

# Conflicts:
#	CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/HighlightController.java
#	CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/ListErrorVisitor.java
#	CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/Renderer.java
parents 15585c6e cc4fe260
......@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- Replaced the JavaFX-based 3D view with an OpenGL renderer (openglfx/JOGL) for smooth display of
large city models. Picking is now GPU-based; the 3D view requires OpenGL 3.3 (Windows/Linux).
## [3.18.3] (2026-04-16)
### Fixes
......
......@@ -59,6 +59,24 @@
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.huskerdev/openglfx-jogl -->
<dependency>
<groupId>com.huskerdev</groupId>
<artifactId>openglfx-jogl</artifactId>
<version>4.1.19</version>
</dependency>
<!-- JOGL runtime + platform natives. openglfx-jogl does not declare JOGL itself;
the GUI now programs directly against com.jogamp.opengl, so depend on it explicitly.
Versions are managed in the parent pom. The -main artifacts bundle the natives. -->
<dependency>
<groupId>org.jogamp.jogl</groupId>
<artifactId>jogl-all-main</artifactId>
</dependency>
<dependency>
<groupId>org.jogamp.gluegen</groupId>
<artifactId>gluegen-rt-main</artifactId>
</dependency>
</dependencies>
<profiles>
<profile>
......
......@@ -108,7 +108,7 @@ public class CityDoctorController {
mainWindow.getOpenBtn().setDisable(true);
mainWindow.getWriteReportButton().setDisable(true);
mainWindow.getMeshGroup().getChildren().clear();
mainWindow.clearMeshView();
clearTrees();
mainWindow.resetSearchBar();
......@@ -1156,7 +1156,7 @@ public class CityDoctorController {
}
public void errorFilterIndexChanged(Number newV) {
mainWindow.getMeshGroup().getChildren().clear();
mainWindow.clearMeshView();
mainWindow.unselectEverything();
mainWindow.resetSearchBar();
renderer.clearCurrentRender();
......
......@@ -44,12 +44,11 @@ import de.hft.stuttgart.citydoctor2.check.error.SchematronError;
import de.hft.stuttgart.citydoctor2.check.error.SolidError;
import de.hft.stuttgart.citydoctor2.check.error.SolidNotClosedError;
import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError;
import de.hft.stuttgart.citydoctor2.check.error.SurfaceUnfragmentedError;
import de.hft.stuttgart.citydoctor2.check.error.DegeneratedRingError;
import de.hft.stuttgart.citydoctor2.check.error.SurfaceUnfragmentedError;
import de.hft.stuttgart.citydoctor2.check.error.TooFewPolygonsError;
import de.hft.stuttgart.citydoctor2.check.error.UnknownCheckError;
import de.hft.stuttgart.citydoctor2.check.error.XMLValidationError;
import de.hft.stuttgart.citydoctor2.datastructure.BoundarySurface;
import de.hft.stuttgart.citydoctor2.datastructure.Edge;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
......@@ -58,33 +57,28 @@ import javafx.scene.paint.Color;
public class ListErrorVisitor implements ErrorVisitor {
private final HighlightController controller;
private TriangulatedGeometry geom;
private HighlightController controller;
public ListErrorVisitor(HighlightController controller) {
this.controller = controller;
}
public void setGeometry(TriangulatedGeometry geom) {
this.geom = geom;
}
@Override
public void visit(PolygonHoleOutsideError err) {
List<LinearRing> highlightedRings = new ArrayList<>();
highlightedRings.add(err.getPolygon().getExteriorRing());
highlightedRings.addAll(err.getHolesOutside());
controller.highlightRings(highlightedRings, geom);
controller.highlight(highlightedRings);
}
@Override
public void visit(NonManifoldEdgeError err) {
controller.highlightEdges(err.getEdges(), geom);
controller.highlightEdges(err.getEdges());
}
@Override
public void visit(MultipleConnectedComponentsError err) {
controller.highlightPolygonClusters(err.getComponents(), geom);
controller.highlightPolygons(err.getComponents());
}
@Override
......@@ -92,18 +86,18 @@ public class ListErrorVisitor implements ErrorVisitor {
List<LinearRing> highlightedRings = new ArrayList<>();
highlightedRings.add(err.getPolygon().getExteriorRing());
highlightedRings.add(err.getInnerRing());
controller.highlightRings(highlightedRings, geom);
controller.highlight(highlightedRings);
}
@Override
public void visit(NonManifoldVertexError err) {
controller.highlightPolygonClusters(err.getComponents(), geom);
controller.addVertexHighlight(err.getVertex(), geom, Color.BLUEVIOLET);
controller.highlightPolygons(err.getComponents());
controller.addHighlight(err.getVertex(), Color.BLUEVIOLET);
}
@Override
public void visit(PolygonWrongOrientationError err) {
controller.highlightEdges(err.getEdges(), geom);
controller.highlightEdges(err.getEdges());
}
@Override
......@@ -111,12 +105,12 @@ public class ListErrorVisitor implements ErrorVisitor {
List<LinearRing> highlightedRings = new ArrayList<>();
highlightedRings.add(err.getPolygon().getExteriorRing());
highlightedRings.add(err.getInnerRing());
controller.highlightRings(highlightedRings, geom);
controller.highlight(highlightedRings);
}
@Override
public void visit(SolidNotClosedError err) {
controller.highlightEdges(err.getErrorEdges(), geom);
controller.highlightEdges(err.getErrorEdges());
}
@Override
......@@ -131,14 +125,14 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override
public void visit(RingNotClosedError err) {
controller.highlight(err.getRing(), geom);
controller.highlight(err.getRing());
}
@Override
public void visit(ConsecutivePointSameError err) {
controller.highlight(err.getRing(), geom);
controller.addVertexHighlight(err.getVertex1(), geom, Color.BLACK);
controller.addVertexHighlight(err.getVertex2(), geom, Color.BLACK);
controller.highlight(err.getRing());
controller.addHighlight(err.getVertex1(), Color.BLACK);
controller.addHighlight(err.getVertex2(), Color.BLACK);
}
@Override
......@@ -148,7 +142,7 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override
public void visit(PolygonInteriorDisconnectedError err) {
controller.highlightRings(err.getConnectedRings(), geom);
controller.highlight(err.getConnectedRings());
}
@Override
......@@ -158,18 +152,18 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override
public void visit(RingTooFewPointsError err) {
controller.highlight(err.getRing(), geom);
controller.highlight(err.getRing());
}
@Override
public void visit(NonPlanarPolygonNormalsDeviation err) {
controller.highlight(err.getPolygon(), geom);
controller.highlight(err.getPolygon());
}
@Override
public void visit(NonPlanarPolygonDistancePlaneError err) {
controller.highlight(err.getPolygon(), geom);
controller.addVertexHighlight(err.getVertex(), geom, Color.BLACK);
controller.highlight(err.getPolygon());
controller.addHighlight(err.getVertex(), Color.BLACK);
}
@Override
......@@ -177,15 +171,15 @@ public class ListErrorVisitor implements ErrorVisitor {
List<LinearRing> rings = new ArrayList<>();
rings.add(err.getIntersectingRings().getValue0());
rings.add(err.getIntersectingRings().getValue1());
controller.highlightRings(rings, geom);
controller.highlight(rings);
}
@Override
public void visit(SolidSelfIntError err) {
controller.clearHighlights();
for (PolygonIntersection intersection : err.getIntersections()) {
controller.addPolygonHighlight(intersection.getP1(), geom);
controller.addPolygonHighlight(intersection.getP2(), geom);
controller.addHighlight(intersection.getP1());
controller.addHighlight(intersection.getP2());
}
}
......@@ -196,9 +190,9 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override
public void visit(RingDuplicatePointError err) {
controller.highlight(err.getRing(), geom);
controller.addVertexHighlight(err.getVertex1(), geom, Color.BLACK);
controller.addVertexHighlight(err.getVertex2(), geom, Color.BLACK);
controller.highlight(err.getRing());
controller.addHighlight(err.getVertex1(), Color.BLACK);
controller.addHighlight(err.getVertex2(), Color.BLACK);
}
@Override
......@@ -206,14 +200,14 @@ public class ListErrorVisitor implements ErrorVisitor {
List<Edge> list = new ArrayList<>();
list.add(err.getEdge1());
list.add(err.getEdge2());
controller.highlightEdges(list, geom);
controller.addVertexHighlight(new Vertex(err.getIntersection()), geom, Color.BLACK);
controller.highlightEdges(list);
controller.addHighlight(new Vertex(err.getIntersection()), Color.BLACK);
}
@Override
public void visit(PointTouchesEdgeError err) {
controller.highlight(err.getEdge(), geom);
controller.addVertexHighlight(err.getVertex(), geom, Color.BLACK);
controller.highlight(err.getEdge());
controller.addHighlight(err.getVertex(), Color.BLACK);
}
@Override
......@@ -223,22 +217,22 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override
public void visit(NotCeilingError err) {
controller.highlight(err.getPolygon(), geom);
controller.highlight(err.getPolygon());
}
@Override
public void visit(NotFloorError err) {
controller.highlight(err.getPolygon(), geom);
controller.highlight(err.getPolygon());
}
@Override
public void visit(NotWallError err) {
controller.highlight(err.getPolygon(), geom);
controller.highlight(err.getPolygon());
}
@Override
public void visit(NotGroundError err) {
controller.highlight(err.getPolygon(), geom);
controller.highlight(err.getPolygon());
}
@Override
......@@ -246,16 +240,16 @@ public class ListErrorVisitor implements ErrorVisitor {
// nothing to display
}
@Override
public void visit(SurfaceUnfragmentedError err) {
controller.clearHighlights();
controller.addObjectHighlight(err.getBoundarySurface(), geom);
controller.addPolygonHighlight(err.getFragment(), geom);
}
@Override
public void visit(SurfaceUnfragmentedError err) {
controller.clearHighlights();
// controller.addObjectHighlight(err.getBoundarySurface(), geom);
// controller.addPolygonHighlight(err.getFragment(), geom);
}
@Override
public void visit(DegeneratedRingError err) {
controller.highlight(err.getRing(), geom);
controller.highlight(err.getRing());
}
@Override
......@@ -275,7 +269,7 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override
public void visit(PolygonWithoutSurfaceError err) {
controller.highlight(err.getPolygon(), geom);
controller.highlight(err.getPolygon());
}
@Override
......@@ -310,7 +304,7 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override
public void visit(FeatureCollisionError err){
controller.highlightGeometryCollisions(err.getCollisionRecord(), geom);
// controller.highlightGeometryCollisions(err.getCollisionRecord(), geom);
//TODO: Implement handling of error here
}
......
......@@ -32,9 +32,11 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import de.hft.stuttgart.citydoctor2.gui.gl.GLViewport;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jspecify.annotations.NonNull;
......@@ -51,10 +53,6 @@ public class MainWindow extends Application {
private static final Logger logger = LogManager.getLogger(MainWindow.class);
private static final double CAMERA_TRANSLATE_Z = -100.0;
private static final double CAMERA_INITIAL_X_ANGLE = 20.0;
private static final double CAMERA_INITIAL_Y_ANGLE = 120.0;
@FXML
private TreeView<Renderable> buildingsView;
......@@ -216,19 +214,8 @@ public class MainWindow extends Application {
private Image north;
private Image northFlip;
private Group meshGroup;
private Group world;
private PerspectiveCamera camera;
private Rotate cameraXRotation = new Rotate();
private Rotate cameraZRotation = new Rotate();
private GLViewport glViewport;
private double dragX;
private double dragY;
private double cameraXRot = CAMERA_INITIAL_X_ANGLE;
private double cameraYRot = CAMERA_INITIAL_Y_ANGLE;
private double translateZ = CAMERA_TRANSLATE_Z;
private double[] clickStart = new double[2];
@FXML
......@@ -247,7 +234,6 @@ public class MainWindow extends Application {
private FeatureType selectedTab = FeatureType.BUILDING;
private MainToolBar mainToolBar;
private SubScene geomScene;
private ImageView northArrow;
private static boolean loadFileAtStartup = false;
......@@ -261,6 +247,11 @@ public class MainWindow extends Application {
private ChangeListener<Number> filterChangeListener;
public static void main(String[] args) {
// The embedded OpenGL canvas (openglfx) breaks JavaFX's partial-repaint accounting, leaving
// sibling controls (e.g. the log panel) with a stale background. Disabling Prism's dirty-region
// optimization forces a full scene repaint each pulse, which fixes the artifacts. Must be set
// before the JavaFX toolkit initializes.
System.setProperty("prism.dirtyopts", "false");
setLocaleFromSettings();
ArgumentParser argParser = new ArgumentParser(args);
inputFile = CityDoctorValidation.getInputFile(argParser, true);
......@@ -304,7 +295,7 @@ public class MainWindow extends Application {
FXMLLoader loader = new FXMLLoader(MainWindow.class.getResource("MainWindow.fxml"));
loader.setController(this);
BorderPane bp = loader.load();
highlightController = new HighlightController(world);
highlightController = new HighlightController(glViewport);
renderer = new Renderer(this, highlightController);
clickHandler = new VertexClickHandler(errorView, renderer, stage, this);
controller = new CityDoctorController(this, highlightController, renderer);
......@@ -850,36 +841,31 @@ public class MainWindow extends Application {
}
private void setup3dView() {
Group root = new Group();
geomScene = new SubScene(root, 500, 300, true, SceneAntialiasing.BALANCED);
geomScene.heightProperty().bind(meshView.heightProperty());
geomScene.widthProperty().bind(meshView.widthProperty());
geomScene.setFill(Color.AZURE);
meshView.getChildren().add(geomScene);
geomScene.addEventFilter(MouseEvent.MOUSE_PRESSED, me -> {
glViewport = new GLViewport();
Region canvas = glViewport.getNode();
canvas.prefWidthProperty().bind(meshView.widthProperty());
canvas.prefHeightProperty().bind(meshView.heightProperty());
meshView.getChildren().add(canvas);
canvas.addEventFilter(MouseEvent.MOUSE_PRESSED, me -> {
clickStart[0] = me.getScreenX();
clickStart[1] = me.getScreenY();
});
geomScene.addEventFilter(MouseEvent.MOUSE_RELEASED, me -> {
canvas.addEventFilter(MouseEvent.MOUSE_RELEASED, me -> {
if (Math.abs(clickStart[0] - me.getScreenX()) > 3 || Math.abs(clickStart[1] - me.getScreenY()) > 3) {
// skip when mouse moved too much
return;
}
Node node = me.getPickResult().getIntersectedNode();
if (node != null) {
Object o = node.getUserData();
if (o instanceof ClickDispatcher cd) {
cd.click(me, clickHandler);
glViewport.requestPick(me.getX(), me.getY(), target -> {
if (target instanceof Polygon p) {
new PolygonClickDispatcher(p).click(me, clickHandler);
}
}
});
});
world = new Group();
root.getChildren().add(world);
meshGroup = new Group();
world.getChildren().add(meshGroup);
// keep the north arrow aligned with the camera after orbit drags
canvas.addEventFilter(MouseEvent.MOUSE_DRAGGED, me -> alignNorthArrow());
north = new Image(getClass().getResourceAsStream("icons/north.png"), 50, 50, true, true);
northFlip = new Image(getClass().getResourceAsStream("icons/north_flip.png"), 50, 50, true, true);
......@@ -890,81 +876,13 @@ public class MainWindow extends Application {
northArrow.setY(10);
meshView.getChildren().add(northArrow);
northArrow.setVisible(false);
AmbientLight al = new AmbientLight(Color.WHITE);
root.getChildren().add(al);
buildCamera();
cameraXRotation.setAxis(Rotate.X_AXIS);
cameraZRotation.setAxis(Rotate.Z_AXIS);
world.getTransforms().add(cameraXRotation);
world.getTransforms().add(cameraZRotation);
root.getChildren().add(camera);
geomScene.setCamera(camera);
setupMeshViewControls();
}
private void setupMeshViewControls() {
meshView.setOnMousePressed(me -> {
if (me.getButton() == MouseButton.PRIMARY) {
dragX = me.getScreenX();
dragY = me.getScreenY();
}
if (me.getButton() == MouseButton.SECONDARY) {
dragX = me.getScreenX();
dragY = me.getScreenY();
}
});
meshView.setOnScroll(se -> {
if (se.getDeltaY() < 0) {
translateZ += translateZ * 0.05;
} else {
translateZ -= translateZ * 0.05;
}
camera.setTranslateZ(translateZ);
highlightController.changeScaling(translateZ);
});
meshView.setOnMouseDragged(me -> {
if (me.getButton() == MouseButton.PRIMARY) {
double deltaX = me.getScreenX() - dragX;
double deltaY = me.getScreenY() - dragY;
dragX = me.getScreenX();
dragY = me.getScreenY();
cameraXRot += (deltaX / 3d) % 360;
cameraYRot += (deltaY / 3d) % 360;
cameraZRotation.setAngle(cameraXRot);
cameraXRotation.setAngle(cameraYRot);
alignNorthArrow();
}
if (me.getButton() == MouseButton.SECONDARY) {
double translationSpeed = Math.abs(camera.getTranslateZ()) / 1000;
double deltaX = me.getScreenX() - dragX;
double deltaY = me.getScreenY() - dragY;
dragX = me.getScreenX();
dragY = me.getScreenY();
camera.setTranslateX(camera.getTranslateX() - deltaX * translationSpeed);
camera.setTranslateY(camera.getTranslateY() - deltaY * translationSpeed);
}
});
}
private void buildCamera() {
camera = new PerspectiveCamera(true);
camera.setNearClip(0.1);
camera.setFarClip(10000d);
camera.setTranslateZ(translateZ);
cameraZRotation.setAngle(cameraXRot);
cameraXRotation.setAngle(cameraYRot);
}
public void alignNorthArrow() {
double absRotZ = cameraZRotation.getAngle() - 360 * Math.floor(cameraZRotation.getAngle() / 360);
double absRotX = cameraXRotation.getAngle() - 360 * Math.floor(cameraXRotation.getAngle() / 360);
double azimuth = glViewport.camera().azimuthDeg();
double elevation = glViewport.camera().elevationDeg();
double absRotZ = azimuth - 360 * Math.floor(azimuth / 360);
double absRotX = elevation - 360 * Math.floor(elevation / 360);
double northRot;
if (80 <= absRotX && absRotX <= 260) {
......@@ -983,16 +901,11 @@ public class MainWindow extends Application {
}
public void resetCamera() {
cameraXRot = CAMERA_INITIAL_X_ANGLE;
cameraYRot = CAMERA_INITIAL_Y_ANGLE;
cameraZRotation.setAngle(cameraXRot);
cameraXRotation.setAngle(cameraYRot);
alignNorthArrow();
camera.setTranslateX(0);
camera.setTranslateY(0);
glViewport.resetCamera();
if (controller.getOriginBB() != null) {
zoomOutForBoundingBox(controller.getOriginBB());
}
alignNorthArrow();
}
public void addFileNameToTitle(String fileName) {
......@@ -1060,8 +973,14 @@ public class MainWindow extends Application {
return mainToolBar.getGridButton();
}
public Group getMeshGroup() {
return meshGroup;
public GLViewport getGlViewport() {
return glViewport;
}
/** Clears whatever geometry is currently displayed in the 3D view. */
public void clearMeshView() {
glViewport.setScene(null);
glViewport.setIndexSet(new int[0]);
}
public ToggleButton getCullingButton() {
......@@ -1142,20 +1061,16 @@ public class MainWindow extends Application {
}
public void takeViewScreenshot() throws IOException {
WritableImage snapshot = geomScene.snapshot(null, null);
WritableImage snapshot = glViewport.getNode().snapshot(null, null);
File outputFile = new File("img.png");
BufferedImage bImage = SwingFXUtils.fromFXImage(snapshot, null);
ImageIO.write(bImage, "png", outputFile);
}
public void zoomOutForBoundingBox(BoundingBox b) {
double longestSide = b.getDiagonalLength() * 0.4;
double d = longestSide / Math.tan(Math.toRadians(30) / 2);
translateZ = -d;
camera.setTranslateZ(translateZ);
camera.setTranslateX(0);
camera.setTranslateY(0);
highlightController.changeScaling(-translateZ);
glViewport.camera().zoomOutForBoundingBox(b.getDiagonalLength());
glViewport.getNode().repaint();
highlightController.changeScaling(glViewport.camera().distance());
controller.setOriginBB(b);
}
......
......@@ -4,7 +4,6 @@ import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.Checkable;
import de.hft.stuttgart.citydoctor2.utils.visitors.CityObjectCollector;
import de.hft.stuttgart.citydoctor2.datastructure.*;
import de.hft.stuttgart.citydoctor2.gui.filter.ViewFilter;
import de.hft.stuttgart.citydoctor2.gui.tree.*;
import de.hft.stuttgart.citydoctor2.gui.tree.node.EdgeNode;
import de.hft.stuttgart.citydoctor2.gui.tree.node.ErrorNode;
......@@ -12,12 +11,15 @@ import de.hft.stuttgart.citydoctor2.gui.tree.node.GenericAttributeNode;
import de.hft.stuttgart.citydoctor2.gui.tree.node.LinearRingNode;
import de.hft.stuttgart.citydoctor2.gui.tree.node.PolygonNode;
import de.hft.stuttgart.citydoctor2.gui.tree.node.VertexNode;
import de.hft.stuttgart.citydoctor2.gui.gl.GLViewport;
import de.hft.stuttgart.citydoctor2.gui.gl.IndexSetBuilder;
import de.hft.stuttgart.citydoctor2.gui.gl.SceneBuilder;
import de.hft.stuttgart.citydoctor2.gui.gl.SceneData;
import de.hft.stuttgart.citydoctor2.gui.gl.TriangleMeta;
import de.hft.stuttgart.citydoctor2.math.Triangle3d;
import javafx.application.Platform;
import javafx.scene.control.TreeItem;
import javafx.scene.paint.Color;
import javafx.scene.shape.CullFace;
import javafx.scene.shape.DrawMode;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
......@@ -28,11 +30,15 @@ public class Renderer {
private static final Logger logger = LogManager.getLogger(Renderer.class);
private TriangulatedGeometry currentTriGeom;
private SceneData currentScene;
private Geometry currentGeometry;
private CullFace currentCulling = CullFace.NONE;
private DrawMode currentDrawMode = DrawMode.FILL;
private boolean culling = false;
private boolean wireframe = false;
// LOD visibility, indexed by Lod.ordinal() (LOD0..LOD4); all enabled by default
private final boolean[] lodEnabled = {true, true, true, true, true};
private boolean roofsHidden = false;
private final MainWindow mainWindow;
private final HighlightController highlightController;
......@@ -40,8 +46,6 @@ public class Renderer {
private final LoadingInfoDialog loadingDialog;
private List<ViewFilter> lodFilters = new ArrayList<>();
private ViewFilter roofFilter;
private Runnable refresher;
private Runnable errorUpdater;
......@@ -50,139 +54,74 @@ public class Renderer {
this.highlightController = highlightController;
loadingDialog = new LoadingInfoDialog(mainWindow.getMainStage());
errVisitor = new ListErrorVisitor(highlightController);
setupLodFilters();
}
private void setupLodFilters() {
lodFilters.add(new ViewFilter() {
@Override
public boolean useGeometry(CityObject co, Geometry geom) {
return geom.getLod() == Lod.LOD1;
}
});
lodFilters.add(new ViewFilter() {
@Override
public boolean useGeometry(CityObject co, Geometry geom) {
return geom.getLod() == Lod.LOD2;
}
});
lodFilters.add(new ViewFilter() {
@Override
public boolean useGeometry(CityObject co, Geometry geom) {
return geom.getLod() == Lod.LOD3;
}
});
lodFilters.add(new ViewFilter() {
@Override
protected boolean useGeometry(CityObject co, Geometry geom) {
return geom.getLod() == Lod.LOD0;
}
});
lodFilters.add(new ViewFilter() {
@Override
public boolean useGeometry(CityObject co, Geometry geom) {
return geom.getLod() == Lod.LOD4;
}
});
roofFilter = new ViewFilter() {
@Override
public boolean allowedToUse(CityObject co, Geometry geom) {
if (!this.isEnabled()) {
return true;
}
return this.useGeometry(co, geom);
}
@Override
public boolean useGeometry(CityObject co, Geometry geom) {
if (co instanceof BoundarySurface bs) {
return bs.getType() != BoundarySurfaceType.ROOF;
}
return true;
}
private boolean triangleVisible(TriangleMeta m) {
int lod = m.lod();
boolean lodOk = lod < 0 || lod >= lodEnabled.length || lodEnabled[lod];
if (!lodOk) {
return false;
}
return !roofsHidden || !m.roof();
}
};
roofFilter.setEnable(false);
private void rebuildIndexSet() {
if (currentScene == null) {
return;
}
mainWindow.getGlViewport().setIndexSet(
IndexSetBuilder.build(currentScene.triangleMetas(), this::triangleVisible));
}
public void enableLod1() {
lodFilters.get(0).enable();
if (refresher != null) {
refresher.run();
}
setLodEnabled(Lod.LOD1.ordinal(), true);
}
public void disableLod1() {
lodFilters.get(0).disable();
if (refresher != null) {
refresher.run();
}
setLodEnabled(Lod.LOD1.ordinal(), false);
}
public void enableLod2() {
lodFilters.get(1).enable();
if (refresher != null) {
refresher.run();
}
setLodEnabled(Lod.LOD2.ordinal(), true);
}
public void disableLod2() {
lodFilters.get(1).disable();
if (refresher != null) {
refresher.run();
}
setLodEnabled(Lod.LOD2.ordinal(), false);
}
public void enableLod3() {
lodFilters.get(2).enable();
if (refresher != null) {
refresher.run();
}
setLodEnabled(Lod.LOD3.ordinal(), true);
}
public void disableLod3() {
lodFilters.get(2).disable();
if (refresher != null) {
refresher.run();
}
setLodEnabled(Lod.LOD3.ordinal(), false);
}
public void enableLod4() {
lodFilters.get(3).enable();
if (refresher != null) {
refresher.run();
}
setLodEnabled(Lod.LOD4.ordinal(), true);
}
public void disableLod4() {
lodFilters.get(3).disable();
if (refresher != null) {
refresher.run();
setLodEnabled(Lod.LOD4.ordinal(), false);
}
private void setLodEnabled(int lodOrdinal, boolean enabled) {
// legacy LOD0 followed LOD3 button; keep LOD0 tied to LOD3 toggle for parity
lodEnabled[lodOrdinal] = enabled;
if (lodOrdinal == Lod.LOD3.ordinal()) {
lodEnabled[Lod.LOD0.ordinal()] = enabled;
}
Platform.runLater(this::rebuildIndexSet);
}
public void hideRoofs() {
roofFilter.setEnable(true);
if (refresher != null) {
refresher.run();
}
roofsHidden = true;
Platform.runLater(this::rebuildIndexSet);
}
public void showRoofs() {
roofFilter.setEnable(false);
if (refresher != null) {
refresher.run();
}
roofsHidden = false;
Platform.runLater(this::rebuildIndexSet);
}
public void setupRefresher(CityObject co) {
......@@ -208,18 +147,7 @@ public class Renderer {
private void addPolygons(CityObject co, Set<ConcretePolygon> polygons) {
for (Geometry geom : co.getGeometries()) {
boolean used = false;
for (ViewFilter filter : lodFilters) {
if (filter.allowedToUse(co, geom)) {
used = true;
break;
}
}
if (used) {
if (roofFilter.allowedToUse(co, geom)) {
addConcretePolygons(polygons, geom);
}
}
addConcretePolygons(polygons, geom);
}
}
......@@ -245,15 +173,14 @@ public class Renderer {
public void render(Geometry geom) {
refresher = () -> {
Platform.runLater(this::clearGeometryTrees);
currentTriGeom = TriangulatedGeometry.of(geom);
SceneData scene = SceneBuilder.fromPolygons(geom.getPolygons(), Color.WHITE);
if (geom.getEdges() == null && currentGeometry != null) {
// if there are no edges available low memory mode is enabled
// clear the old geometry of all meta information
currentGeometry.clearMetaData();
}
errVisitor.setGeometry(currentTriGeom);
Platform.runLater(() -> {
setupRenderState();
applyScene(scene);
if (geom.getEdges() == null) {
// create edges and vertices so they can be listed in the gui
geom.prepareForChecking();
......@@ -350,9 +277,8 @@ public class Renderer {
private void render(Collection<? extends Polygon> polygons) {
Platform.runLater(this::clearGeometryTrees);
currentTriGeom = TriangulatedGeometry.of(polygons);
errVisitor.setGeometry(currentTriGeom);
setupRenderState();
SceneData scene = SceneBuilder.fromPolygons(polygons, Color.WHITE);
Platform.runLater(() -> applyScene(scene));
}
private void clearGeometryTrees() {
......@@ -376,10 +302,9 @@ public class Renderer {
clearAttributes();
});
Thread t = new Thread(() -> {
currentTriGeom = TriangulatedGeometry.of(model, lodFilters);
errVisitor.setGeometry(currentTriGeom);
SceneData scene = SceneBuilder.fromModel(model);
Platform.runLater(() -> {
setupRenderState();
applyScene(scene);
mainWindow.zoomOutForBoundingBox(BoundingBox.of(model));
loadingDialog.hide();
});
......@@ -431,14 +356,10 @@ public class Renderer {
CityObjectCollector collector = new CityObjectCollector();
cos.forEach(cityObject -> cityObject.accept(collector));
collector.getCityObjects().forEach(cityObject -> addPolygons(cityObject, polygons));
if (baseColor != null) {
currentTriGeom = TriangulatedGeometry.of(polygons, baseColor);
} else {
currentTriGeom = TriangulatedGeometry.of(polygons);
}
errVisitor.setGeometry(currentTriGeom);
Color base = baseColor != null ? baseColor : Color.WHITE;
SceneData scene = SceneBuilder.fromPolygons(polygons, base);
Platform.runLater(() -> {
setupRenderState();
applyScene(scene);
mainWindow.zoomOutForBoundingBox(BoundingBox.of(polygons));
loadingDialog.hide();
});
......@@ -456,49 +377,44 @@ public class Renderer {
private void addConcretePolygons(Set<ConcretePolygon> polygons, Geometry geom) {
for (Polygon p : geom.getPolygons()) {
if (p.getOriginal().getPartOfSurface() != null &&
!roofFilter.allowedToUse(p.getOriginal().getPartOfSurface(), p.getParent())) continue;
polygons.add(p.getOriginal());
}
}
private void setupRenderState() {
currentTriGeom.setCullFace(currentCulling);
currentTriGeom.setDrawMode(currentDrawMode);
Platform.runLater(() -> {
mainWindow.getMeshGroup().getChildren().clear();
mainWindow.getMeshGroup().getChildren().addAll(currentTriGeom.getMeshes());
});
/**
* Installs the freshly built scene into the viewport (FX thread), wires the highlight center
* supplier, applies the current wireframe/cull state and rebuilds the visible index set.
*/
private void applyScene(SceneData scene) {
currentScene = scene;
GLViewport viewport = mainWindow.getGlViewport();
viewport.setScene(scene);
highlightController.setCenterSupplier(
() -> currentScene == null ? new double[3] : currentScene.center());
viewport.setWireframe(wireframe);
viewport.setCulling(culling);
rebuildIndexSet();
mainWindow.getGridButton().setDisable(false);
mainWindow.getCullingButton().setDisable(false);
}
public void showWireFrame(boolean show) {
if (currentTriGeom != null) {
if (show) {
currentDrawMode = DrawMode.LINE;
} else {
currentDrawMode = DrawMode.FILL;
}
currentTriGeom.setDrawMode(currentDrawMode);
}
wireframe = show;
mainWindow.getGlViewport().setWireframe(show);
}
public void enableCulling(boolean enable) {
if (currentTriGeom != null) {
if (enable) {
currentCulling = CullFace.BACK;
} else {
currentCulling = CullFace.NONE;
}
currentTriGeom.setCullFace(currentCulling);
}
culling = enable;
mainWindow.getGlViewport().setCulling(enable);
}
public void clearCurrentRender() {
// don't render anything
currentScene = null;
Platform.runLater(() -> {
mainWindow.getMeshGroup().getChildren().clear();
GLViewport viewport = mainWindow.getGlViewport();
viewport.setScene(null);
viewport.setIndexSet(new int[0]);
clearGeometryTrees();
clearAttributes();
});
......@@ -510,72 +426,78 @@ public class Renderer {
}
public void highlightTopoErrorGeometry(Geometry geom, boolean isCandidate) {
if (isCandidate) {
highlightController.clearHighlights();
highlightController.addCandidateHighlight(geom.getPolygons(), currentTriGeom);
} else {
highlightController.highlightPolygons(geom.getPolygons(), currentTriGeom);
}
// TODO: Rework to opengl
// if (isCandidate) {
// highlightController.clearHighlights();
// highlightController.addCandidateHighlight(geom.getPolygons(), currentTriGeom);
// } else {
// highlightController.highlightPolygons(geom.getPolygons(), currentTriGeom);
// }
}
public void highlightTopoErrorPolygon(Polygon polygon, boolean isCandidate) {
if(isCandidate) {
highlightController.clearHighlights();
highlightController.addCandidateHighlight(polygon, currentTriGeom);
} else {
highlightController.highlight(polygon, currentTriGeom);
}
// TODO: Rework to opengl
// if(isCandidate) {
// highlightController.clearHighlights();
// highlightController.addCandidateHighlight(polygon, currentTriGeom);
// } else {
// highlightController.highlight(polygon, currentTriGeom);
// }
}
public void highlightTopoErrorPolygons(Collection<Polygon> subjects, Collection<Polygon> candidates) {
highlightController.highlightPolygons(subjects, currentTriGeom);
highlightController.addCandidateHighlight(candidates, currentTriGeom);
// TODO: Rework to opengl
// highlightController.highlightPolygons(subjects, currentTriGeom);
// highlightController.addCandidateHighlight(candidates, currentTriGeom);
}
public void highlightTopoErrorPolygons(Polygon subject, Collection<Polygon> candidates) {
highlightController.highlight(subject, currentTriGeom);
highlightController.addCandidateHighlight(candidates, currentTriGeom);
// TODO: Rework to opengl
// highlightController.highlight(subject, currentTriGeom);
// highlightController.addCandidateHighlight(candidates, currentTriGeom);
}
public void highlightTopoErrorPolygons(Polygon subject, Polygon candidate) {
highlightController.highlight(subject, currentTriGeom);
highlightController.addCandidateHighlight(candidate, currentTriGeom);
// TODO: Rework to opengl
// highlightController.highlight(subject, currentTriGeom);
// highlightController.addCandidateHighlight(candidate, currentTriGeom);
}
public void addCandidateSkeletonHighlight(GmlId candidateId){
highlightController.addSkeletonHighlight(mainWindow.retrieveCityObject(candidateId), currentTriGeom);
// TODO: Rework to opengl
// highlightController.addSkeletonHighlight(mainWindow.retrieveCityObject(candidateId), currentTriGeom);
}
public void highlight(Polygon p) {
highlightController.highlight(p, currentTriGeom);
highlightController.highlight(p);
}
public void highlight(LinearRing lr) {
highlightController.highlight(lr, currentTriGeom);
highlightController.highlight(lr);
}
public void highlight(Edge e) {
highlightController.highlight(e, currentTriGeom);
highlightController.highlight(e);
}
public void highlight(Vertex v) {
highlightController.highlight(v, currentTriGeom);
highlightController.highlight(v);
}
public void highlight(List<LinearRing> highlightedRings) {
highlightController.highlightRings(highlightedRings, currentTriGeom);
highlightController.highlight(highlightedRings);
}
public void highlightEdges(List<Edge> edges) {
highlightController.highlightEdges(edges, currentTriGeom);
highlightController.highlightEdges(edges);
}
public void highlightPolygons(List<List<Polygon>> components) {
highlightController.highlightPolygonClusters(components, currentTriGeom);
highlightController.highlightPolygons(components);
}
public void addHighlight(Vertex vertex, Color c) {
highlightController.addVertexHighlight(vertex, currentTriGeom, c);
highlightController.addHighlight(vertex, c);
}
public void highlight(CheckError err) {
......@@ -599,11 +521,11 @@ public class Renderer {
}
public void addHighlight(Polygon p) {
highlightController.addPolygonHighlight(p, currentTriGeom);
highlightController.addHighlight(p);
}
public void addHighlight(Triangle3d t) {
highlightController.highlight(t, currentTriGeom);
highlightController.highlight(t);
}
public void reset() {
......
package de.hft.stuttgart.citydoctor2.gui;
import de.hft.stuttgart.citydoctor2.utils.visitors.CityObjectCollector;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.*;
import de.hft.stuttgart.citydoctor2.gui.filter.ViewFilter;
import de.hft.stuttgart.citydoctor2.math.Triangle3d;
import de.hft.stuttgart.citydoctor2.math.UnitVector3d;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
import de.hft.stuttgart.citydoctor2.tesselation.TesselatedPolygon;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.*;
import java.util.*;
import java.util.stream.Stream;
public class TriangulatedGeometry {
private static final PhongMaterial GRID_MAT = new PhongMaterial(Color.BLACK);
// random vector for calculating normal angles, for color determination
private static final UnitVector3d AXIS = new Vector3d(19, 0.8, 1.5).normalize();
private Vector3d movedBy;
private List<MeshView> meshes;
private List<PhongMaterial> materials;
public static TriangulatedGeometry of(Geometry geom) {
return of(geom.getPolygons());
}
public static TriangulatedGeometry of(Collection<? extends Polygon> polygons, Color basePolygonColor) {
TriangulatedGeometry triGeom = new TriangulatedGeometry();
triGeom.materials = new ArrayList<>();
triGeom.meshes = new ArrayList<>();
List<Vector3d> points = new ArrayList<>();
for (Polygon p : polygons) {
points.addAll(p.getExteriorRing().getVertices());
}
triGeom.movedBy = triGeom.findCenter(points);
addPolygonDataToTriGeom(polygons, basePolygonColor, triGeom);
return triGeom;
}
private static void addPolygonDataToTriGeom(Collection<? extends Polygon> polygons, Color basePolygonColor,
TriangulatedGeometry triGeom) {
for (Polygon p : polygons) {
TesselatedPolygon tp = p.tesselate();
TriangleMesh triMesh = new TriangleMesh(VertexFormat.POINT_TEXCOORD);
Map<Vector3d, Integer> indexMap = new HashMap<>();
List<Vector3d> vertices = new ArrayList<>();
int index = 0;
for (Triangle3d t : tp.getTriangles()) {
index = triGeom.filterDuplicates(triMesh, indexMap, vertices, index, t.getP1());
index = triGeom.filterDuplicates(triMesh, indexMap, vertices, index, t.getP2());
index = triGeom.filterDuplicates(triMesh, indexMap, vertices, index, t.getP3());
}
for (Vector3d point : vertices) {
float x = (float) (point.getX() - triGeom.movedBy.getX());
float y = (float) (point.getY() - triGeom.movedBy.getY());
float z = (float) (point.getZ() - triGeom.movedBy.getZ());
triMesh.getPoints().addAll(x, y, z);
}
triMesh.getTexCoords().addAll(0, 0);
MeshView view = new MeshView(triMesh);
view.setUserData(new PolygonClickDispatcher(p));
PhongMaterial mat = triGeom.calculateMaterial(p, basePolygonColor);
triGeom.materials.add(mat);
triGeom.meshes.add(view);
}
}
public static TriangulatedGeometry of(CityDoctorModel model, List<ViewFilter> filters) {
List<Vector3d> points = new ArrayList<>();
addPointsFromCityObject(model.createFeatureStream().toList(), points);
TriangulatedGeometry triGeom = new TriangulatedGeometry();
triGeom.materials = new ArrayList<>();
triGeom.meshes = new ArrayList<>();
triGeom.movedBy = triGeom.findCenter(points);
addPolygonDataFromCityObjects(model.getBuildings(), triGeom, Color.WHITE, filters);
addPolygonDataFromCityObjects(model.getBridges(), triGeom, Color.LIGHTSTEELBLUE, filters);
addPolygonDataFromCityObjects(model.getTunnels(), triGeom, Color.SLATEGRAY, filters);
addPolygonDataFromCityObjects(model.getLand(), triGeom, Color.TAN, filters);
addPolygonDataFromCityObjects(model.getTransportation(), triGeom, Color.DIMGRAY, filters);
addPolygonDataFromCityObjects(model.getVegetation(), triGeom, Color.LIGHTGREEN, filters);
addPolygonDataFromCityObjects(model.getWater(), triGeom, Color.LIGHTSKYBLUE, filters);
addPolygonDataFromCityObjects(model.getCityFurniture(), triGeom, Color.BLUEVIOLET, filters);
addPolygonDataFromCityObjects(model.getOtherCityObjects(), triGeom, Color.BLACK, filters);
return triGeom;
}
private static void addPolygonDataFromCityObjects(Stream<? extends CityObject> cos,
TriangulatedGeometry triGeom, Color color, List<ViewFilter> filters) {
CityObjectCollector collector = new CityObjectCollector();
cos.forEach(cityObject -> cityObject.accept(collector));
collector.getCityObjects().forEach(cityObject -> addPolygonData(cityObject, triGeom, color, filters));
}
private static void addPolygonData(CityObject co, TriangulatedGeometry triGeom, Color color, List<ViewFilter> filters) {
for (Geometry geom : co.getGeometries()) {
if (isGeometryFiltered(co, geom, filters)) {
continue;
}
List<Polygon> polygons = new ArrayList<>();
for (Polygon p : geom.getPolygons()) {
if (p.isLink()) {
continue;
}
polygons.add(p);
}
addPolygonDataToTriGeom(polygons, color, triGeom);
}
}
private static boolean isGeometryFiltered(CityObject co, Geometry geom, List<ViewFilter> filters) {
for (ViewFilter filter : filters) {
if (filter.allowedToUse(co, geom)) {
return false;
}
}
return true;
}
private static void addPointsFromCityObject(List<? extends CityObject> cos, List<Vector3d> points) {
CityObjectCollector collector = new CityObjectCollector();
cos.forEach(cityObject -> cityObject.accept(collector));
collector.getCityObjects().forEach(cityObject -> addPoints(cityObject, points));
}
private static void addPoints(CityObject co, List<Vector3d> points) {
for (Geometry geom : co.getGeometries()) {
for (Polygon p : geom.getPolygons()) {
if (p.isLink()) {
continue;
}
points.addAll(p.getExteriorRing().getVertices());
}
}
}
public static TriangulatedGeometry of(Collection<? extends Polygon> polygons) {
return of(polygons, Color.WHITE);
}
private int filterDuplicates(TriangleMesh triMesh, Map<Vector3d, Integer> indexMap, List<Vector3d> vertices,
int index, Vector3d v) {
Integer vertexIndex = indexMap.get(v);
if (vertexIndex == null) {
indexMap.put(v, index);
vertices.add(v);
vertexIndex = index;
index++;
}
triMesh.getFaces().addAll(vertexIndex, 0);
return index;
}
private PhongMaterial calculateMaterial(Polygon p, Color baseColor) {
Vector3d normal = p.calculateNormalNormalized();
if (p.getRenderColor() != null && p.getRenderColor() != Color.WHITE) {
baseColor = p.getRenderColor();
}
double cos = normal.dot(AXIS);
double acos = Math.acos(cos);
// normalize to range [0.3, 0.9]
acos = acos / Math.PI;
acos = acos * 0.6 + 0.3;
Color derivedColor = baseColor.deriveColor(0, 1.0, acos, 1.0);
return new PhongMaterial(derivedColor);
}
private Vector3d findCenter(List<Vector3d> points) {
double xMin = Double.MAX_VALUE;
double yMin = Double.MAX_VALUE;
double zMin = Double.MAX_VALUE;
double xMax = Double.NEGATIVE_INFINITY;
double yMax = Double.NEGATIVE_INFINITY;
double zMax = Double.NEGATIVE_INFINITY;
for (Vector3d point : points) {
if (point.getX() < xMin) {
xMin = point.getX();
}
if (point.getX() > xMax) {
xMax = point.getX();
}
if (point.getY() < yMin) {
yMin = point.getY();
}
if (point.getY() > yMax) {
yMax = point.getY();
}
if (point.getZ() < zMin) {
zMin = point.getZ();
}
if (point.getZ() > zMax) {
zMax = point.getZ();
}
}
// center
double x = (xMax - xMin) / 2 + xMin;
double y = (yMax - yMin) / 2 + yMin;
double z = (zMax - zMin) / 2 + zMin;
return new Vector3d(x, y, z);
}
public Vector3d getMovedBy() {
return movedBy;
}
public void setCullFace(CullFace currentCulling) {
for (MeshView mesh : meshes) {
mesh.setCullFace(currentCulling);
}
}
public void setDrawMode(DrawMode currentDrawMode) {
if (currentDrawMode == DrawMode.LINE) {
for (MeshView mesh : meshes) {
mesh.setDrawMode(currentDrawMode);
mesh.setMaterial(GRID_MAT);
}
} else if (currentDrawMode == DrawMode.FILL) {
for (int i = 0; i < meshes.size(); i++) {
MeshView mesh = meshes.get(i);
mesh.setDrawMode(currentDrawMode);
mesh.setMaterial(materials.get(i));
}
}
}
public List<MeshView> getMeshes() {
return meshes;
}
}
......@@ -46,7 +46,7 @@ public class ValidationView extends View {
public void onHide() {
Platform.runLater(() -> {
mainWindow.unselectEverything();
mainWindow.getMeshGroup().getChildren().clear();
mainWindow.clearMeshView();
mainWindow.clearHighlights();
mainWindow.getErrorTree().getRoot().getChildren().clear();
mainWindow.getPolygonsView().getRoot().getChildren().clear();
......
package de.hft.stuttgart.citydoctor2.gui.gl;
import java.util.Arrays;
/** Growable primitive float array to avoid boxing when building large vertex buffers. */
public class FloatList {
private float[] data;
private int size;
public FloatList() { this(1024); }
public FloatList(int initialCapacity) { data = new float[Math.max(1, initialCapacity)]; }
public void add(float v) {
ensure(size + 1);
data[size++] = v;
}
public void add(float a, float b, float c) {
ensure(size + 3);
data[size++] = a;
data[size++] = b;
data[size++] = c;
}
private void ensure(int capacity) {
if (capacity > data.length) {
int newCap = data.length;
while (newCap < capacity) {
newCap <<= 1;
}
data = Arrays.copyOf(data, newCap);
}
}
public int size() { return size; }
/** Resets the list to empty without releasing the backing array. */
public void clear() { size = 0; }
public float[] toArray() { return Arrays.copyOf(data, size); }
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import com.huskerdev.grapl.gl.GLProfile;
import com.huskerdev.openglfx.canvas.GLCanvas;
import com.huskerdev.openglfx.jogl.JOGLExecutor;
import com.huskerdev.openglfx.jogl.events.JOGLRenderEvent;
import com.jogamp.opengl.GL;
import com.jogamp.opengl.GL3;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.scene.input.MouseButton;
import javafx.scene.paint.Color;
/** OpenGL viewport embedded in JavaFX via openglfx. Replaces the legacy SubScene. */
public class GLViewport {
private static final Color BACKGROUND = Color.AZURE;
private final GLCanvas canvas;
private final OrbitCamera camera = new OrbitCamera();
private final GlScene scene = new GlScene();
private final PickingFramebuffer picking = new PickingFramebuffer();
private final HighlightOverlay overlay = new HighlightOverlay();
private ShaderProgram sceneProgram;
private ShaderProgram pickProgram;
private ShaderProgram pointProgram;
private boolean initialized;
// state set off-thread, consumed in render callback
private final AtomicReference<SceneData> pendingScene = new AtomicReference<>();
private final AtomicReference<int[]> pendingIndexSet = new AtomicReference<>();
// last index set seen; retained so it can be (re)applied after the scene is uploaded or the GL
// context is recreated (the index buffer only exists once the scene's buffers have been generated)
private volatile int[] lastIndices;
private volatile boolean wireframe = false;
private volatile boolean cull = false;
private volatile float pointScale = 20f;
private volatile float lineScale = 4f;
// highlight marker data, uploaded on the GL thread when dirty
private volatile float[] hlPoints = new float[0];
private volatile float[] hlPointColors = new float[0];
private volatile float[] hlLines = new float[0];
private volatile float[] hlLineColors = new float[0];
private volatile boolean highlightsDirty = false;
// pick request: logical (JavaFX) screen coords + callback (invoked on FX thread with resolved Object)
private volatile double[] pickRequest;
private volatile Consumer<Object> pickCallback;
private SceneData currentScene;
// drag state
private double lastX;
private double lastY;
// optional hook invoked after a camera interaction (orbit/pan/zoom) so a coupled viewport can be
// kept in sync and redrawn (used by the dual-viewport healer)
private Runnable interactionListener;
// on-demand render request. openglfx can drop a repaint() that arrives while a frame is rendering
// (its requestRepaint is an unconditional notify with no pending flag), so a single repaint is not
// reliable. This flag + pump re-deliver repaint() each pulse until a frame consumes the request,
// then the pump stops itself so an idle viewport has no per-pulse cost.
private volatile boolean renderRequested;
private final AnimationTimer repaintPump = new AnimationTimer() {
@Override
public void handle(long now) {
if (renderRequested) {
canvas.repaint();
} else {
stop();
}
}
};
public GLViewport() {
// args: executor, profile, flipY=false, msaa=4, fps=0.0
// fps=0 -> on-demand rendering: a frame is drawn only when repaint() is called (every state
// change / input does so), so an idle viewport allocates and renders nothing.
canvas = new GLCanvas(new JOGLExecutor(), GLProfile.CORE, false, 4, 0.0);
canvas.addOnInitEvent(ev -> {
// GL context (re)created: shaders/buffers from any previous context are gone. Drop our
// handles and re-queue the current scene so it is re-uploaded into the new context.
initialized = false;
scene.markContextLost();
if (currentScene != null) {
pendingScene.set(currentScene);
}
});
canvas.addOnRenderEvent(ev -> render((JOGLRenderEvent) ev));
canvas.addOnReshapeEvent(ev -> { /* aspect handled per-frame from width/height */ });
// on-demand mode: repaint when the canvas is resized so the framebuffer tracks the new size
canvas.widthProperty().addListener((obs, ov, nv) -> requestRender());
canvas.heightProperty().addListener((obs, ov, nv) -> requestRender());
installInputHandlers();
}
public GLCanvas getNode() { return canvas; }
// ---- public API (called from FX / worker threads) ----
public void setScene(SceneData data) { pendingScene.set(data); requestRender(); }
public void setIndexSet(int[] indices) { pendingIndexSet.set(indices); requestRender(); }
public void setWireframe(boolean on) { wireframe = on; requestRender(); }
public void setCulling(boolean on) { cull = on; requestRender(); }
public void setPointScale(float scale) { pointScale = scale; requestRender(); }
/** points: xyz, pointColors rgb-per-point; lines: xyz pairs, lineColors rgb-per-line-vertex. */
public void setHighlights(float[] points, float[] pointColors, float[] lines, float[] lineColors) {
this.hlPoints = points;
this.hlPointColors = pointColors;
this.hlLines = lines;
this.hlLineColors = lineColors;
this.highlightsDirty = true;
requestRender();
}
public OrbitCamera camera() { return camera; }
/** Hook invoked after a camera interaction so a coupled viewport can sync and redraw. */
public void setInteractionListener(Runnable listener) { this.interactionListener = listener; }
/** Public redraw trigger (e.g. for a coupled viewport). */
public void requestRedraw() { requestRender(); }
/** Clears the displayed geometry (draws nothing) until a new scene is set. */
public void clearScene() {
currentScene = null;
lastIndices = new int[0];
setIndexSet(new int[0]);
}
/**
* Requests a redraw. Sets the render flag, wakes the canvas immediately (best effort), and starts
* the pump so the request is re-delivered until a frame actually consumes it. Call on the FX thread.
*/
private void requestRender() {
renderRequested = true;
canvas.repaint();
repaintPump.start(); // idempotent while running
}
public void requestPick(double x, double y, Consumer<Object> callback) {
pickRequest = new double[]{x, y};
pickCallback = callback;
requestRender();
}
// ---- render callback (GL thread) ----
private void render(JOGLRenderEvent ev) {
// consume the render request; if new state arrives during this frame, requestRender re-arms it
renderRequested = false;
GL3 gl = ev.getGl();
int w = ev.width;
int h = ev.height;
int defaultFbo = ev.fbo;
if (!initialized) {
sceneProgram = new ShaderProgram(gl, "scene.vert", "scene.frag");
pickProgram = new ShaderProgram(gl, "pick.vert", "pick.frag");
pointProgram = new ShaderProgram(gl, "point.vert", "point.geom", "point.frag");
overlay.init(gl);
gl.glEnable(GL.GL_DEPTH_TEST);
gl.glEnable(GL.GL_MULTISAMPLE); // anti-aliasing for the multisampled framebuffer
initialized = true;
}
SceneData newScene = pendingScene.getAndSet(null);
boolean uploadedNow = false;
if (newScene != null) {
scene.upload(gl, newScene);
currentScene = newScene;
uploadedNow = true;
}
int[] newIndices = pendingIndexSet.getAndSet(null);
if (newIndices != null) {
lastIndices = newIndices;
}
// Apply the index set only once the scene's buffers exist (the element buffer is created by
// upload). Re-apply after a fresh upload so the indices survive a scene swap or context loss.
if (scene.hasData() && lastIndices != null && (newIndices != null || uploadedNow)) {
scene.setIndexSet(gl, lastIndices);
}
if (highlightsDirty) {
overlay.setPoints(gl, hlPoints, hlPointColors);
overlay.setLines(gl, hlLines, hlLineColors);
highlightsDirty = false;
}
camera.setAspect(h == 0 ? 1f : (float) w / h);
float[] mvp = camera.viewProjection();
gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, defaultFbo);
gl.glViewport(0, 0, w, h);
gl.glClearColor((float) BACKGROUND.getRed(), (float) BACKGROUND.getGreen(),
(float) BACKGROUND.getBlue(), 1f);
gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
if (cull) {
gl.glEnable(GL.GL_CULL_FACE);
} else {
gl.glDisable(GL.GL_CULL_FACE);
}
gl.glPolygonMode(GL.GL_FRONT_AND_BACK, wireframe ? GL3.GL_LINE : GL3.GL_FILL);
sceneProgram.use(gl);
gl.glUniformMatrix4fv(sceneProgram.uniform(gl, "uMVP"), 1, false, mvp, 0);
scene.draw(gl);
// wireframe applies to the geometry only; highlight markers always render filled
gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL3.GL_FILL);
// highlight markers sit at the same depth as geometry edges (and the wireframe lines); use
// LEQUAL so the later-drawn markers win depth ties instead of being rejected by GL_LESS
gl.glDepthFunc(GL.GL_LEQUAL);
// edge markers reuse the flat scene program (pos + colour)
overlay.drawLines(gl, lineScale);
// vertex markers use the sphere-impostor program (geometry shader -> camera-facing quads)
pointProgram.use(gl);
gl.glUniformMatrix4fv(pointProgram.uniform(gl, "uMVP"), 1, false, mvp, 0);
gl.glUniform2f(pointProgram.uniform(gl, "uViewport"), (float) w, (float) h);
gl.glUniform1f(pointProgram.uniform(gl, "uPointSize"), pointScale);
overlay.drawPoints(gl);
gl.glDepthFunc(GL.GL_LESS); // restore for the next frame's scene pass
double[] req = pickRequest;
if (req != null && currentScene != null) {
picking.resize(gl, w, h);
picking.bind(gl);
gl.glClearColor(0, 0, 0, 1);
gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL3.GL_FILL);
pickProgram.use(gl);
gl.glUniformMatrix4fv(pickProgram.uniform(gl, "uMVP"), 1, false, mvp, 0);
scene.draw(gl);
// click coords are logical (JavaFX) px; the picking buffer is physical px. Scale by the
// framebuffer/canvas ratio so HiDPI displays read the pixel actually under the cursor.
double cw = canvas.getWidth();
double ch = canvas.getHeight();
float sx = cw > 0 ? (float) (w / cw) : 1f;
float sy = ch > 0 ? (float) (h / ch) : 1f;
int px = Math.round((float) req[0] * sx);
int py = Math.round((float) req[1] * sy);
int id = picking.readId(gl, px, py);
Object target = currentScene.pickRegistry().resolve(id);
Consumer<Object> cb = pickCallback;
pickRequest = null;
pickCallback = null;
gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, defaultFbo);
if (cb != null) {
Platform.runLater(() -> cb.accept(target));
}
}
}
// ---- input ----
private void installInputHandlers() {
canvas.setOnMousePressed(e -> { lastX = e.getX(); lastY = e.getY(); });
canvas.setOnMouseDragged(e -> {
double dx = e.getX() - lastX;
double dy = e.getY() - lastY;
lastX = e.getX();
lastY = e.getY();
if (e.getButton() == MouseButton.PRIMARY) {
camera.orbit(dx / 3.0, dy / 3.0);
} else if (e.getButton() == MouseButton.SECONDARY) {
double speed = Math.abs(camera.distance()) / 1000.0;
camera.pan(-dx * speed, dy * speed);
}
requestRender();
notifyInteraction();
});
canvas.setOnScroll(e -> {
camera.zoom(e.getDeltaY() < 0 ? 1.05 : 0.95);
requestRender();
notifyInteraction();
});
}
public void resetCamera() { camera.reset(); requestRender(); }
private void notifyInteraction() {
if (interactionListener != null) {
interactionListener.run();
}
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import com.jogamp.opengl.GL;
import com.jogamp.opengl.GL3;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
/** GPU resources for the global scene mesh. Single VBO (pos+color+id), one index buffer redrawn per frame. */
public class GlScene {
private int vao = 0;
private int vboPos;
private int vboColor;
private int vboId;
private int ebo;
private int indexCount;
private boolean uploaded;
public void upload(GL3 gl, SceneData data) {
if (!uploaded) {
int[] ids = new int[3];
gl.glGenVertexArrays(1, ids, 0);
vao = ids[0];
int[] buffers = new int[4];
gl.glGenBuffers(4, buffers, 0);
vboPos = buffers[0];
vboColor = buffers[1];
vboId = buffers[2];
ebo = buffers[3];
}
gl.glBindVertexArray(vao);
uploadFloat(gl, vboPos, data.positions(), 0, 3);
uploadFloat(gl, vboColor, data.colors(), 1, 3);
uploadInt(gl, vboId, data.ids(), 2);
gl.glBindVertexArray(0);
uploaded = true;
}
private void uploadFloat(GL3 gl, int vbo, float[] data, int location, int components) {
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vbo);
FloatBuffer fb = toFloatBuffer(data);
gl.glBufferData(GL.GL_ARRAY_BUFFER, (long) data.length * Float.BYTES, fb, GL.GL_STATIC_DRAW);
gl.glEnableVertexAttribArray(location);
gl.glVertexAttribPointer(location, components, GL.GL_FLOAT, false, 0, 0L);
}
private void uploadInt(GL3 gl, int vbo, int[] data, int location) {
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vbo);
IntBuffer ib = toIntBuffer(data);
gl.glBufferData(GL.GL_ARRAY_BUFFER, (long) data.length * Integer.BYTES, ib, GL.GL_STATIC_DRAW);
gl.glEnableVertexAttribArray(location);
gl.glVertexAttribIPointer(location, 1, GL3.GL_INT, 0, 0L); // integer attribute, not normalized
}
/** Re-uploads which triangles to draw (element indices). Cheap; called on filter/LOD changes. */
public void setIndexSet(GL3 gl, int[] indices) {
if (!uploaded || vao == 0) {
// buffers not generated yet; the caller re-applies once the scene is uploaded
return;
}
gl.glBindVertexArray(vao);
gl.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, ebo);
IntBuffer ib = toIntBuffer(indices);
gl.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER, (long) indices.length * Integer.BYTES, ib,
GL.GL_DYNAMIC_DRAW);
gl.glBindVertexArray(0);
indexCount = indices.length;
}
public void draw(GL3 gl) {
if (vao == 0 || indexCount == 0) {
return;
}
gl.glBindVertexArray(vao);
gl.glDrawElements(GL.GL_TRIANGLES, indexCount, GL.GL_UNSIGNED_INT, 0L);
gl.glBindVertexArray(0);
}
public boolean hasData() { return uploaded; }
/**
* Forgets GL handles after the context was lost/recreated, without issuing GL calls (the old
* handles belong to a dead context). The next {@link #upload} regenerates everything.
*/
public void markContextLost() {
vao = 0;
indexCount = 0;
uploaded = false;
}
public void dispose(GL3 gl) {
if (vao != 0) {
gl.glDeleteVertexArrays(1, new int[]{vao}, 0);
gl.glDeleteBuffers(4, new int[]{vboPos, vboColor, vboId, ebo}, 0);
vao = 0;
uploaded = false;
}
}
private static FloatBuffer toFloatBuffer(float[] data) {
ByteBuffer bb = ByteBuffer.allocateDirect(data.length * Float.BYTES).order(ByteOrder.nativeOrder());
FloatBuffer fb = bb.asFloatBuffer();
fb.put(data).flip();
return fb;
}
private static IntBuffer toIntBuffer(int[] data) {
ByteBuffer bb = ByteBuffer.allocateDirect(data.length * Integer.BYTES).order(ByteOrder.nativeOrder());
IntBuffer ib = bb.asIntBuffer();
ib.put(data).flip();
return ib;
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import com.jogamp.opengl.GL;
import com.jogamp.opengl.GL3;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
/**
* Dynamic overlay for highlight markers: points for vertices, lines for edges. Positions are in the same
* recentred world space as the scene. Colour rides in the color attribute; id attribute is 0.
*/
public class HighlightOverlay {
private int pointVao;
private int pointPos;
private int pointColor;
private int pointId;
private int pointCount;
private int lineVao;
private int linePos;
private int lineColor;
private int lineId;
private int lineVertexCount;
private boolean created;
public void init(GL3 gl) {
int[] v = new int[2];
gl.glGenVertexArrays(2, v, 0);
pointVao = v[0];
lineVao = v[1];
int[] b = new int[6];
gl.glGenBuffers(6, b, 0);
pointPos = b[0]; pointColor = b[1]; pointId = b[2];
linePos = b[3]; lineColor = b[4]; lineId = b[5];
created = true;
}
/** points: xyz triplets; pointColors: rgb triplets (one per point). */
public void setPoints(GL3 gl, float[] points, float[] pointColors) {
pointCount = points.length / 3;
configure(gl, pointVao, pointPos, points, pointColor, pointColors, pointId, pointCount);
}
/** lines: xyz pairs of endpoints (6 floats per segment); lineColors: rgb per line vertex. */
public void setLines(GL3 gl, float[] lines, float[] lineColors) {
lineVertexCount = lines.length / 3;
configure(gl, lineVao, linePos, lines, lineColor, lineColors, lineId, lineVertexCount);
}
private void configure(GL3 gl, int vao, int posVbo, float[] pos, int colVbo, float[] col,
int idVbo, int vertexCount) {
gl.glBindVertexArray(vao);
bindFloat(gl, posVbo, pos, 0, 3);
bindFloat(gl, colVbo, col, 1, 3);
// id attribute all zeros so overlay never matches uSelectedId
int[] zeros = new int[vertexCount];
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, idVbo);
gl.glBufferData(GL.GL_ARRAY_BUFFER, (long) zeros.length * Integer.BYTES, toInt(zeros),
GL.GL_DYNAMIC_DRAW);
gl.glEnableVertexAttribArray(2);
gl.glVertexAttribIPointer(2, 1, GL3.GL_INT, 0, 0L);
gl.glBindVertexArray(0);
}
private void bindFloat(GL3 gl, int vbo, float[] data, int loc, int comp) {
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vbo);
gl.glBufferData(GL.GL_ARRAY_BUFFER, (long) data.length * Float.BYTES, toFloat(data),
GL.GL_DYNAMIC_DRAW);
gl.glEnableVertexAttribArray(loc);
gl.glVertexAttribPointer(loc, comp, GL.GL_FLOAT, false, 0, 0L);
}
/** Draws the edge markers as lines. The caller must bind a program that reads pos+color. */
public void drawLines(GL3 gl, float lineWidth) {
if (!created || lineVertexCount == 0) {
return;
}
gl.glLineWidth(lineWidth);
gl.glBindVertexArray(lineVao);
gl.glDrawArrays(GL.GL_LINES, 0, lineVertexCount);
gl.glBindVertexArray(0);
}
/**
* Draws the vertex markers as GL_POINTS. The caller must bind the sphere-impostor program (a
* geometry shader expands each point into a camera-facing quad shaded as a sphere).
*/
public void drawPoints(GL3 gl) {
if (!created || pointCount == 0) {
return;
}
gl.glBindVertexArray(pointVao);
gl.glDrawArrays(GL.GL_POINTS, 0, pointCount);
gl.glBindVertexArray(0);
}
public void clear() {
pointCount = 0;
lineVertexCount = 0;
}
private static FloatBuffer toFloat(float[] d) {
FloatBuffer fb = ByteBuffer.allocateDirect(Math.max(1, d.length) * Float.BYTES)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
fb.put(d).flip();
return fb;
}
private static IntBuffer toInt(int[] d) {
IntBuffer ib = ByteBuffer.allocateDirect(Math.max(1, d.length) * Integer.BYTES)
.order(ByteOrder.nativeOrder()).asIntBuffer();
ib.put(d).flip();
return ib;
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import java.util.List;
import java.util.function.Predicate;
/** Builds the element index set (triangle vertex indices) for the triangles passing the filter. */
public final class IndexSetBuilder {
private IndexSetBuilder() {}
public static int[] build(List<TriangleMeta> metas, Predicate<TriangleMeta> include) {
IntList out = new IntList();
for (TriangleMeta m : metas) {
if (include.test(m)) {
int base = m.triangleIndex() * 3;
out.add(base);
out.add(base + 1);
out.add(base + 2);
}
}
return out.toArray();
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import java.util.Arrays;
/** Growable primitive int array. */
public class IntList {
private int[] data;
private int size;
public IntList() { this(1024); }
public IntList(int initialCapacity) { data = new int[Math.max(1, initialCapacity)]; }
public void add(int v) {
if (size + 1 > data.length) {
data = Arrays.copyOf(data, data.length << 1);
}
data[size++] = v;
}
public int get(int index) { return data[index]; }
public int size() { return size; }
public int[] toArray() { return Arrays.copyOf(data, size); }
}
package de.hft.stuttgart.citydoctor2.gui.gl;
/**
* Minimal column-major 4x4 float matrix helpers, layout-compatible with OpenGL uniform uploads
* (element index = column*4 + row).
*/
public final class Mat4 {
private Mat4() {}
public static float[] identity() {
float[] m = new float[16];
m[0] = m[5] = m[10] = m[15] = 1f;
return m;
}
public static float[] translation(float x, float y, float z) {
float[] m = identity();
m[12] = x;
m[13] = y;
m[14] = z;
return m;
}
/** Returns a * b (column-major). */
public static float[] multiply(float[] a, float[] b) {
float[] r = new float[16];
for (int col = 0; col < 4; col++) {
for (int row = 0; row < 4; row++) {
float sum = 0f;
for (int k = 0; k < 4; k++) {
sum += a[k * 4 + row] * b[col * 4 + k];
}
r[col * 4 + row] = sum;
}
}
return r;
}
/** Returns m * (x,y,z,w) as a length-4 array. */
public static float[] transform(float[] m, float x, float y, float z, float w) {
return new float[] {
m[0] * x + m[4] * y + m[8] * z + m[12] * w,
m[1] * x + m[5] * y + m[9] * z + m[13] * w,
m[2] * x + m[6] * y + m[10] * z + m[14] * w,
m[3] * x + m[7] * y + m[11] * z + m[15] * w
};
}
public static float[] rotationX(float rad) {
float c = (float) Math.cos(rad);
float s = (float) Math.sin(rad);
float[] m = identity();
m[5] = c; m[9] = -s;
m[6] = s; m[10] = c;
return m;
}
public static float[] rotationZ(float rad) {
float c = (float) Math.cos(rad);
float s = (float) Math.sin(rad);
float[] m = identity();
m[0] = c; m[4] = -s;
m[1] = s; m[5] = c;
return m;
}
/** Right-handed perspective projection (camera looks down -Z). */
public static float[] perspective(float fovyRad, float aspect, float near, float far) {
float f = (float) (1.0 / Math.tan(fovyRad / 2.0));
float[] m = new float[16];
m[0] = f / aspect;
m[5] = f;
m[10] = (far + near) / (near - far);
m[11] = -1f;
m[14] = (2f * far * near) / (near - far);
return m;
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import de.hft.stuttgart.citydoctor2.math.Triangle3d;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
/**
* Builds interleaved-by-attribute vertex arrays from triangles. Vertices are stored per triangle corner
* (3 per triangle) so a triangle's vertices occupy a contiguous, predictable range — which lets the
* draw index set be built by triangle without dedup bookkeeping.
*/
public class MeshAccumulator {
private final Vector3d center;
private final FloatList positions = new FloatList();
private final FloatList colors = new FloatList();
private final IntList ids = new IntList();
private int vertexCount;
public MeshAccumulator(Vector3d center) {
this.center = center;
}
public void addTriangle(Triangle3d t, int id, float r, float g, float b) {
addVertex(t.getP1(), id, r, g, b);
addVertex(t.getP2(), id, r, g, b);
addVertex(t.getP3(), id, r, g, b);
}
private void addVertex(Vector3d p, int id, float r, float g, float b) {
positions.add((float) (p.getX() - center.getX()),
(float) (p.getY() - center.getY()),
(float) (p.getZ() - center.getZ()));
colors.add(r, g, b);
ids.add(id);
vertexCount++;
}
public int vertexCount() { return vertexCount; }
public float[] positions() { return positions.toArray(); }
public float[] colors() { return colors.toArray(); }
public int[] ids() { return ids.toArray(); }
}
package de.hft.stuttgart.citydoctor2.gui.gl;
/**
* Orbit camera reproducing the legacy JavaFX interaction model: world is rotated about Z then X, the
* camera sits at {@code distance} along -Z (negative distance = farther away) and pans in X/Y.
*/
public class OrbitCamera {
private static final double INITIAL_AZIMUTH = 180.0;
private static final double INITIAL_ELEVATION = -60.0;
private static final double INITIAL_DISTANCE = 100.0;
private static final double FOVY_RAD = Math.toRadians(30);
private static final float NEAR = 0.1f;
private static final float FAR = 10000f;
private double azimuthDeg = INITIAL_AZIMUTH;
private double elevationDeg = INITIAL_ELEVATION;
private double distance = INITIAL_DISTANCE;
private double panX = 0;
private double panY = 0;
private float aspect = 1.0f;
public void setAspect(float aspect) {
if (aspect > 0) {
this.aspect = aspect;
}
}
public void orbit(double deltaAzimuthDeg, double deltaElevationDeg) {
azimuthDeg += deltaAzimuthDeg;
elevationDeg += deltaElevationDeg;
}
public void pan(double dx, double dy) {
panX += dx;
panY += dy;
}
/** factor > 1 moves farther, < 1 moves closer. */
public void zoom(double factor) {
distance *= factor;
}
/** Matches legacy MainWindow.zoomOutForBoundingBox distance computation. */
public void zoomOutForBoundingBox(double diagonalLength) {
double longestSide = diagonalLength * 0.4;
distance = longestSide / Math.tan(FOVY_RAD / 2.0);
panX = 0;
panY = 0;
}
public void reset() {
azimuthDeg = INITIAL_AZIMUTH;
elevationDeg = INITIAL_ELEVATION;
distance = INITIAL_DISTANCE;
panX = 0;
panY = 0;
}
/** Copies the orbit/zoom/pan state from another camera (but not its aspect ratio). */
public void copyOrbitFrom(OrbitCamera other) {
this.azimuthDeg = other.azimuthDeg;
this.elevationDeg = other.elevationDeg;
this.distance = other.distance;
this.panX = other.panX;
this.panY = other.panY;
}
public double distance() { return distance; }
/** Current azimuth (rotation about Z) in degrees. */
public double azimuthDeg() { return azimuthDeg; }
/** Current elevation (rotation about X) in degrees. */
public double elevationDeg() { return elevationDeg; }
/** Combined projection * view * model-rotation, column-major, ready for a GL uniform. */
public float[] viewProjection() {
float[] proj = Mat4.perspective((float) FOVY_RAD, aspect, NEAR, FAR);
float[] view = Mat4.translation((float) -panX, (float) -panY, (float) -distance);
float[] rot = Mat4.multiply(
Mat4.rotationX((float) Math.toRadians(elevationDeg)),
Mat4.rotationZ((float) Math.toRadians(azimuthDeg)));
return Mat4.multiply(proj, Mat4.multiply(view, rot));
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import java.util.ArrayList;
import java.util.List;
/** Maps picking ids (1-based) to the model objects they represent. */
public class PickRegistry {
private final List<Object> targets = new ArrayList<>();
/** Registers a target and returns its non-zero picking id. */
public int register(Object target) {
targets.add(target);
return targets.size(); // id = index + 1
}
/** Resolves a picking id to its target, or null for background / unknown id. */
public Object resolve(int id) {
if (id <= 0 || id > targets.size()) {
return null;
}
return targets.get(id - 1);
}
public int size() { return targets.size(); }
}
package de.hft.stuttgart.citydoctor2.gui.gl;
/** Encodes integer ids into RGB bytes for GPU color-picking. Id 0 is reserved for background. */
public final class PickingCodec {
private PickingCodec() {}
public static int red(int id) { return id & 0xFF; }
public static int green(int id) { return (id >> 8) & 0xFF; }
public static int blue(int id) { return (id >> 16) & 0xFF; }
public static int decode(int r, int g, int b) {
return (r & 0xFF) | ((g & 0xFF) << 8) | ((b & 0xFF) << 16);
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment