diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8cbc073138102fbfdb05cdeacd50eaef1d24426..d1d708419a8576ab7e700ee6c8dcd423b92df455 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/pom.xml b/CityDoctorParent/Extensions/CityDoctorGUI/pom.xml
index c7bfddab8c995ee1f1d3904d32fa0aa8eda5b98d..e9d152c9c2f5e03ce00d618a680092d3e9f620ce 100644
--- a/CityDoctorParent/Extensions/CityDoctorGUI/pom.xml
+++ b/CityDoctorParent/Extensions/CityDoctorGUI/pom.xml
@@ -59,6 +59,24 @@
org.openjfx
javafx-swing
+
+
+ com.huskerdev
+ openglfx-jogl
+ 4.1.19
+
+
+
+ org.jogamp.jogl
+ jogl-all-main
+
+
+ org.jogamp.gluegen
+ gluegen-rt-main
+
+
diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/CityDoctorController.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/CityDoctorController.java
index 57849884f35d68170f26d2d4ea5c580719ac3212..f21c5c17cb1a3a9c38655c61525cce4e13b83417 100644
--- a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/CityDoctorController.java
+++ b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/CityDoctorController.java
@@ -130,7 +130,7 @@ public class CityDoctorController {
mainWindow.getOpenBtn().setDisable(true);
mainWindow.getWriteReportButton().setDisable(true);
- mainWindow.getMeshGroup().getChildren().clear();
+ mainWindow.clearMeshView();
clearTrees();
mainWindow.resetSearchBar();
@@ -1660,7 +1660,7 @@ public class CityDoctorController {
}
public void errorFilterIndexChanged(Number newV) {
- mainWindow.getMeshGroup().getChildren().clear();
+ mainWindow.clearMeshView();
mainWindow.unselectEverything();
mainWindow.resetSearchBar();
renderer.clearCurrentRender();
diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/HighlightController.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/HighlightController.java
index be979f5b14c2b06420ab187798effdb02d1c35cf..20c2b32e1efed2e5241dcbe2c167384372636e75 100644
--- a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/HighlightController.java
+++ b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/HighlightController.java
@@ -1,194 +1,185 @@
package de.hft.stuttgart.citydoctor2.gui;
import java.util.List;
+import java.util.function.Supplier;
import de.hft.stuttgart.citydoctor2.datastructure.Edge;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType;
+import de.hft.stuttgart.citydoctor2.gui.gl.FloatList;
+import de.hft.stuttgart.citydoctor2.gui.gl.GLViewport;
import de.hft.stuttgart.citydoctor2.math.Triangle3d;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
-import javafx.application.Platform;
-import javafx.geometry.Point3D;
-import javafx.scene.Group;
-import javafx.scene.Node;
import javafx.scene.paint.Color;
-import javafx.scene.paint.PhongMaterial;
-import javafx.scene.shape.Cylinder;
-import javafx.scene.shape.Sphere;
-import javafx.scene.transform.Rotate;
-import javafx.scene.transform.Translate;
+/**
+ * Accumulates highlight marker geometry (points and lines) and pushes it to the {@link GLViewport}
+ * overlay. Coordinates are recentred by the current scene center supplied by the {@link Renderer}.
+ */
public class HighlightController {
-
-
- private double scale;
- private Group points;
- private Group edges;
-
- private Group highlights;
-
- public HighlightController(Group world) {
- highlights = new Group();
- points = new Group();
- edges = new Group();
- highlights.getChildren().add(edges);
- highlights.getChildren().add(points);
- Platform.runLater(() -> world.getChildren().add(highlights));
- }
-
- public void clearHighlights() {
- points.getChildren().clear();
- edges.getChildren().clear();
- }
-
- public void changeScaling(double translateZ) {
- scale = Math.abs(translateZ);
- scale = Math.min(150, scale);
- scale = Math.max(10, scale);
- scale = scale * 0.01;
- for (Node n : points.getChildren()) {
- n.setScaleX(scale);
- n.setScaleY(scale);
- n.setScaleZ(scale);
- }
- double edgeSize = scale / 10;
- for (Node n : edges.getChildren()) {
- if (n instanceof Cylinder cy) {
- cy.setRadius(edgeSize);
- }
- }
- }
-
- public void highlight(Polygon p, TriangulatedGeometry currentTriGeom) {
- clearHighlights();
- addHighlight(p, currentTriGeom, Color.RED, Color.BLUE);
- }
-
- private void addHighlight(Polygon p, TriangulatedGeometry currentTriGeom, Color extColor, Color intColor) {
- if (currentTriGeom == null || p == null) {
- return;
- }
- addHighlight(p.getExteriorRing(), currentTriGeom, extColor);
- for (LinearRing intRing : p.getInnerRings()) {
- addHighlight(intRing, currentTriGeom, intColor);
- }
- }
-
- public void highlight(LinearRing ring, TriangulatedGeometry currentTriGeom) {
- clearHighlights();
- if (ring.getType() == LinearRingType.EXTERIOR) {
- addHighlight(ring, currentTriGeom, Color.RED);
- } else {
- addHighlight(ring, currentTriGeom, Color.BLUE);
- }
- }
-
- public void highlight(List rings, TriangulatedGeometry currentTriGeom) {
- clearHighlights();
- for (LinearRing lr : rings) {
- if (lr.getType() == LinearRingType.EXTERIOR) {
- addHighlight(lr, currentTriGeom, Color.RED);
- } else {
- addHighlight(lr, currentTriGeom, Color.BLUE);
- }
- }
- }
-
- private void addHighlight(LinearRing ring, TriangulatedGeometry currentTriGeom, Color pointColor) {
- if (currentTriGeom == null || ring == null) {
- return;
- }
- Vector3d movedBy = currentTriGeom.getMovedBy();
- for (Vertex v : ring.getVertices()) {
- highlightPoint(movedBy, v, pointColor);
- }
- for (int i = 0; i < ring.getVertices().size() - 1; i++) {
- Vertex v1 = ring.getVertices().get(i);
- Vertex v2 = ring.getVertices().get(i + 1);
- highlightEdge(v1, v2, movedBy);
- }
- }
-
- public void highlight(Edge e, TriangulatedGeometry currentTriGeom) {
- clearHighlights();
- addHighlight(e, currentTriGeom);
- }
-
- private void addHighlight(Edge e, TriangulatedGeometry currentTriGeom) {
- if (currentTriGeom == null || e == null) {
- return;
- }
- Vector3d movedBy = currentTriGeom.getMovedBy();
- highlightEdge(e.getFrom(), e.getTo(), movedBy);
- highlightPoint(movedBy, e.getFrom(), Color.RED);
- highlightPoint(movedBy, e.getTo(), Color.RED);
- }
-
- public void highlight(Vertex v, TriangulatedGeometry currentTriGeom) {
- clearHighlights();
- addHighlight(v, currentTriGeom, Color.RED);
- }
-
- public void addHighlight(Vertex v, TriangulatedGeometry currentTriGeom, Color c) {
- if (currentTriGeom == null || v == null) {
- return;
- }
- Vector3d movedBy = currentTriGeom.getMovedBy();
- highlightPoint(movedBy, v, c);
- }
-
- private void highlightPoint(Vector3d movedBy, Vertex v, Color color) {
- Sphere sp = new Sphere(0.5);
- sp.setMaterial(new PhongMaterial(color));
- sp.setTranslateX(v.getX() - movedBy.getX());
- sp.setTranslateY(v.getY() - movedBy.getY());
- sp.setTranslateZ(v.getZ() - movedBy.getZ());
- sp.setScaleX(scale);
- sp.setScaleY(scale);
- sp.setScaleZ(scale);
-
- sp.setUserData(new VertexClickDispatcher(v));
-
- points.getChildren().add(sp);
- }
-
- private void highlightEdge(Vector3d v1, Vector3d v2, Vector3d movedBy) {
- Point3D origin = new Point3D(v1.getX() - movedBy.getX(), v1.getY() - movedBy.getY(),
- v1.getZ() - movedBy.getZ());
- Point3D target = new Point3D(v2.getX() - movedBy.getX(), v2.getY() - movedBy.getY(),
- v2.getZ() - movedBy.getZ());
-
- Point3D yAxis = new Point3D(0, 1, 0);
- Point3D diff = target.subtract(origin);
- double height = diff.magnitude();
-
- Point3D mid = target.midpoint(origin);
- Translate moveToMidpoint = new Translate(mid.getX(), mid.getY(), mid.getZ());
-
- Point3D axisOfRotation = diff.crossProduct(yAxis);
- double angle = Math.acos(diff.normalize().dotProduct(yAxis));
- Rotate rotateAroundCenter = new Rotate(-Math.toDegrees(angle), axisOfRotation);
-
- Cylinder cy = new Cylinder(scale / 10, height);
- cy.setMaterial(new PhongMaterial(Color.ORANGE));
- cy.getTransforms().addAll(moveToMidpoint, rotateAroundCenter);
- edges.getChildren().add(cy);
- }
-
- public void highlightEdges(List errorEdges, TriangulatedGeometry currentTriGeom) {
- clearHighlights();
- for (Edge e : errorEdges) {
- addHighlight(e, currentTriGeom);
- }
- }
-
- public void highlightPolygons(List> components, TriangulatedGeometry currentTriGeom) {
- clearHighlights();
- for (int i = 0; i < components.size(); i++) {
- Color extColor;
- Color intColor = switch (i % 3) {
+
+ private final GLViewport viewport;
+ private Supplier centerSupplier;
+
+ private final FloatList points = new FloatList();
+ private final FloatList pointColors = new FloatList();
+ private final FloatList lines = new FloatList();
+ private final FloatList lineColors = new FloatList();
+
+ public HighlightController(GLViewport viewport) {
+ this.viewport = viewport;
+ }
+
+ public void setCenterSupplier(Supplier centerSupplier) {
+ this.centerSupplier = centerSupplier;
+ }
+
+ private double[] center() {
+ if (centerSupplier == null) {
+ return new double[3];
+ }
+ double[] c = centerSupplier.get();
+ return c == null ? new double[3] : c;
+ }
+
+ private void resetBuffers() {
+ points.clear();
+ pointColors.clear();
+ lines.clear();
+ lineColors.clear();
+ }
+
+ public void clearHighlights() {
+ resetBuffers();
+ push();
+ }
+
+ private void push() {
+ viewport.setHighlights(points.toArray(), pointColors.toArray(), lines.toArray(), lineColors.toArray());
+ }
+
+ public void changeScaling(double translateZ) {
+ // GL points/lines are sized in screen pixels, so highlight markers keep a constant on-screen
+ // size regardless of zoom distance; nothing to recompute here.
+ }
+
+ // ---- point/line accumulation ----
+
+ private void addPoint(Vertex v, Color color) {
+ double[] c = center();
+ points.add((float) (v.getX() - c[0]), (float) (v.getY() - c[1]), (float) (v.getZ() - c[2]));
+ pointColors.add((float) color.getRed(), (float) color.getGreen(), (float) color.getBlue());
+ }
+
+ private void addLine(Vector3d v1, Vector3d v2, Color color) {
+ double[] c = center();
+ lines.add((float) (v1.getX() - c[0]), (float) (v1.getY() - c[1]), (float) (v1.getZ() - c[2]));
+ lines.add((float) (v2.getX() - c[0]), (float) (v2.getY() - c[1]), (float) (v2.getZ() - c[2]));
+ float r = (float) color.getRed();
+ float g = (float) color.getGreen();
+ float b = (float) color.getBlue();
+ lineColors.add(r, g, b);
+ lineColors.add(r, g, b);
+ }
+
+ // ---- public highlight API ----
+
+ public void highlight(Polygon p) {
+ resetBuffers();
+ addHighlight(p, Color.RED, Color.BLUE);
+ push();
+ }
+
+ private void addHighlight(Polygon p, Color extColor, Color intColor) {
+ if (p == null) {
+ return;
+ }
+ addHighlight(p.getExteriorRing(), extColor);
+ for (LinearRing intRing : p.getInnerRings()) {
+ addHighlight(intRing, intColor);
+ }
+ }
+
+ public void highlight(LinearRing ring) {
+ resetBuffers();
+ if (ring.getType() == LinearRingType.EXTERIOR) {
+ addHighlight(ring, Color.RED);
+ } else {
+ addHighlight(ring, Color.BLUE);
+ }
+ push();
+ }
+
+ public void highlight(List rings) {
+ resetBuffers();
+ for (LinearRing lr : rings) {
+ if (lr.getType() == LinearRingType.EXTERIOR) {
+ addHighlight(lr, Color.RED);
+ } else {
+ addHighlight(lr, Color.BLUE);
+ }
+ }
+ push();
+ }
+
+ private void addHighlight(LinearRing ring, Color pointColor) {
+ if (ring == null) {
+ return;
+ }
+ for (Vertex v : ring.getVertices()) {
+ addPoint(v, pointColor);
+ }
+ for (int i = 0; i < ring.getVertices().size() - 1; i++) {
+ Vertex v1 = ring.getVertices().get(i);
+ Vertex v2 = ring.getVertices().get(i + 1);
+ addLine(v1, v2, Color.ORANGE);
+ }
+ }
+
+ public void highlight(Edge e) {
+ resetBuffers();
+ addHighlight(e);
+ push();
+ }
+
+ private void addHighlight(Edge e) {
+ if (e == null) {
+ return;
+ }
+ addLine(e.getFrom(), e.getTo(), Color.ORANGE);
+ addPoint(e.getFrom(), Color.RED);
+ addPoint(e.getTo(), Color.RED);
+ }
+
+ public void highlight(Vertex v) {
+ resetBuffers();
+ addPoint(v, Color.RED);
+ push();
+ }
+
+ public void addHighlight(Vertex v, Color c) {
+ if (v == null) {
+ return;
+ }
+ addPoint(v, c);
+ push();
+ }
+
+ public void highlightEdges(List errorEdges) {
+ resetBuffers();
+ for (Edge e : errorEdges) {
+ addHighlight(e);
+ }
+ push();
+ }
+
+ public void highlightPolygons(List> components) {
+ resetBuffers();
+ for (int i = 0; i < components.size(); i++) {
+ Color extColor;
+ Color intColor = switch (i % 3) {
case 1 -> {
extColor = Color.GREEN;
yield Color.YELLOW;
@@ -202,21 +193,24 @@ public class HighlightController {
yield Color.BLUE;
}
};
- // select some color pairs for exterior and inner rings
+ // select some color pairs for exterior and inner rings
List component = components.get(i);
- for (Polygon p : component) {
- addHighlight(p, currentTriGeom, extColor, intColor);
- }
- }
- }
-
- public void addHighlight(Polygon p, TriangulatedGeometry currentTriGeom) {
- addHighlight(p, currentTriGeom, Color.RED, Color.BLUE);
- }
-
- public void highlight(Triangle3d t, TriangulatedGeometry currentTriGeom) {
- highlightEdge(t.getP1(), t.getP2(), currentTriGeom.getMovedBy());
- highlightEdge(t.getP2(), t.getP3(), currentTriGeom.getMovedBy());
- highlightEdge(t.getP3(), t.getP1(), currentTriGeom.getMovedBy());
- }
+ for (Polygon p : component) {
+ addHighlight(p, extColor, intColor);
+ }
+ }
+ push();
+ }
+
+ public void addHighlight(Polygon p) {
+ addHighlight(p, Color.RED, Color.BLUE);
+ push();
+ }
+
+ public void highlight(Triangle3d t) {
+ addLine(t.getP1(), t.getP2(), Color.ORANGE);
+ addLine(t.getP2(), t.getP3(), Color.ORANGE);
+ addLine(t.getP3(), t.getP1(), Color.ORANGE);
+ push();
+ }
}
diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/ListErrorVisitor.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/ListErrorVisitor.java
index 8412762c26ea8bfe6bba4cdd7b5a79e39279223b..0a999f7b4b908c8517215b291a7dd2c92494245c 100644
--- a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/ListErrorVisitor.java
+++ b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/ListErrorVisitor.java
@@ -55,32 +55,27 @@ import javafx.scene.paint.Color;
public class ListErrorVisitor implements ErrorVisitor {
private HighlightController controller;
- private TriangulatedGeometry geom;
-
+
public ListErrorVisitor(HighlightController controller) {
this.controller = controller;
}
-
- public void setGeometry(TriangulatedGeometry geom) {
- this.geom = geom;
- }
@Override
public void visit(PolygonHoleOutsideError err) {
List highlightedRings = new ArrayList<>();
highlightedRings.add(err.getPolygon().getExteriorRing());
highlightedRings.addAll(err.getHolesOutside());
- controller.highlight(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.highlightPolygons(err.getComponents(), geom);
+ controller.highlightPolygons(err.getComponents());
}
@Override
@@ -88,18 +83,18 @@ public class ListErrorVisitor implements ErrorVisitor {
List highlightedRings = new ArrayList<>();
highlightedRings.add(err.getPolygon().getExteriorRing());
highlightedRings.add(err.getInnerRing());
- controller.highlight(highlightedRings, geom);
+ controller.highlight(highlightedRings);
}
@Override
public void visit(NonManifoldVertexError err) {
- controller.highlightPolygons(err.getComponents(), geom);
- controller.addHighlight(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
@@ -107,12 +102,12 @@ public class ListErrorVisitor implements ErrorVisitor {
List highlightedRings = new ArrayList<>();
highlightedRings.add(err.getPolygon().getExteriorRing());
highlightedRings.add(err.getInnerRing());
- controller.highlight(highlightedRings, geom);
+ controller.highlight(highlightedRings);
}
@Override
public void visit(SolidNotClosedError err) {
- controller.highlightEdges(err.getErrorEdges(), geom);
+ controller.highlightEdges(err.getErrorEdges());
}
@Override
@@ -127,14 +122,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.addHighlight(err.getVertex1(), geom, Color.BLACK);
- controller.addHighlight(err.getVertex2(), geom, Color.BLACK);
+ controller.highlight(err.getRing());
+ controller.addHighlight(err.getVertex1(), Color.BLACK);
+ controller.addHighlight(err.getVertex2(), Color.BLACK);
}
@Override
@@ -144,7 +139,7 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override
public void visit(PolygonInteriorDisconnectedError err) {
- controller.highlight(err.getConnectedRings(), geom);
+ controller.highlight(err.getConnectedRings());
}
@Override
@@ -154,18 +149,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.addHighlight(err.getVertex(), geom, Color.BLACK);
+ controller.highlight(err.getPolygon());
+ controller.addHighlight(err.getVertex(), Color.BLACK);
}
@Override
@@ -173,15 +168,15 @@ public class ListErrorVisitor implements ErrorVisitor {
List rings = new ArrayList<>();
rings.add(err.getIntersectingRings().getValue0());
rings.add(err.getIntersectingRings().getValue1());
- controller.highlight(rings, geom);
+ controller.highlight(rings);
}
@Override
public void visit(SolidSelfIntError err) {
controller.clearHighlights();
for (PolygonIntersection intersection : err.getIntersections()) {
- controller.addHighlight(intersection.getP1(), geom);
- controller.addHighlight(intersection.getP2(), geom);
+ controller.addHighlight(intersection.getP1());
+ controller.addHighlight(intersection.getP2());
}
}
@@ -192,9 +187,9 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override
public void visit(RingDuplicatePointError err) {
- controller.highlight(err.getRing(), geom);
- controller.addHighlight(err.getVertex1(), geom, Color.BLACK);
- controller.addHighlight(err.getVertex2(), geom, Color.BLACK);
+ controller.highlight(err.getRing());
+ controller.addHighlight(err.getVertex1(), Color.BLACK);
+ controller.addHighlight(err.getVertex2(), Color.BLACK);
}
@Override
@@ -202,14 +197,14 @@ public class ListErrorVisitor implements ErrorVisitor {
List list = new ArrayList<>();
list.add(err.getEdge1());
list.add(err.getEdge2());
- controller.highlightEdges(list, geom);
- controller.addHighlight(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.addHighlight(err.getVertex(), geom, Color.BLACK);
+ controller.highlight(err.getEdge());
+ controller.addHighlight(err.getVertex(), Color.BLACK);
}
@Override
@@ -219,22 +214,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
@@ -249,7 +244,7 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override
public void visit(DegeneratedRingError err) {
- controller.highlight(err.getRing(), geom);
+ controller.highlight(err.getRing());
}
@Override
@@ -269,7 +264,7 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override
public void visit(PolygonWithoutSurfaceError err) {
- controller.highlight(err.getPolygon(), geom);
+ controller.highlight(err.getPolygon());
}
@Override
diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/MainWindow.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/MainWindow.java
index 357f70ecb9b86a886f93ec610acb0ab85629d802..298617963b1b1acc02af11d8a244a920f1cc3f06 100644
--- a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/MainWindow.java
+++ b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/MainWindow.java
@@ -31,9 +31,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;
@@ -49,10 +51,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 buildingsView;
@@ -214,19 +212,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
@@ -245,7 +232,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;
@@ -259,6 +245,11 @@ public class MainWindow extends Application {
private ChangeListener 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);
@@ -302,7 +293,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);
@@ -845,36 +836,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);
@@ -885,81 +871,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) {
@@ -978,16 +896,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) {
@@ -1055,8 +968,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() {
@@ -1137,20 +1056,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);
}
diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/Renderer.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/Renderer.java
index 7c2279b3a04e39c85a2a047245e2226dc2f909fa..d584bca9acdc4eb75e113595de8370e0f2f7ba51 100644
--- a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/Renderer.java
+++ b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/Renderer.java
@@ -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 MainWindow mainWindow;
private HighlightController highlightController;
@@ -40,8 +46,6 @@ public class Renderer {
private LoadingInfoDialog loadingDialog;
- private List lodFilters = new ArrayList<>();
- private ViewFilter roofFilter;
private Runnable refresher;
private Runnable errorUpdater;
@@ -50,138 +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) {
@@ -207,18 +147,7 @@ public class Renderer {
private void addPolygons(CityObject co, Set 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);
}
}
@@ -244,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();
@@ -349,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() {
@@ -375,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();
});
@@ -430,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();
});
@@ -455,49 +377,44 @@ public class Renderer {
private void addConcretePolygons(Set 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();
});
@@ -508,35 +425,35 @@ public class Renderer {
}
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 highlightedRings) {
- highlightController.highlight(highlightedRings, currentTriGeom);
+ highlightController.highlight(highlightedRings);
}
public void highlightEdges(List edges) {
- highlightController.highlightEdges(edges, currentTriGeom);
+ highlightController.highlightEdges(edges);
}
public void highlightPolygons(List> components) {
- highlightController.highlightPolygons(components, currentTriGeom);
+ highlightController.highlightPolygons(components);
}
public void addHighlight(Vertex vertex, Color c) {
- highlightController.addHighlight(vertex, currentTriGeom, c);
+ highlightController.addHighlight(vertex, c);
}
public void highlight(CheckError err) {
@@ -560,11 +477,11 @@ public class Renderer {
}
public void addHighlight(Polygon p) {
- highlightController.addHighlight(p, currentTriGeom);
+ highlightController.addHighlight(p);
}
public void addHighlight(Triangle3d t) {
- highlightController.highlight(t, currentTriGeom);
+ highlightController.highlight(t);
}
public void reset() {
diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/TriangulatedGeometry.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/TriangulatedGeometry.java
deleted file mode 100644
index 83a556d32f0189588d14008983c994b0d8ec565f..0000000000000000000000000000000000000000
--- a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/TriangulatedGeometry.java
+++ /dev/null
@@ -1,251 +0,0 @@
-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 meshes;
- private List 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 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 indexMap = new HashMap<>();
- List 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 filters) {
- List 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 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 filters) {
- for (Geometry geom : co.getGeometries()) {
- if (isGeometryFiltered(co, geom, filters)) {
- continue;
- }
- List 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 filters) {
- for (ViewFilter filter : filters) {
- if (filter.allowedToUse(co, geom)) {
- return false;
- }
- }
- return true;
- }
-
- private static void addPointsFromCityObject(List extends CityObject> cos, List 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 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 indexMap, List 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 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 getMeshes() {
- return meshes;
- }
-}
diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/ValidationView.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/ValidationView.java
index 2699d1c712ff1fb855e98c88b71d84c92f2d264f..900294280e4ef5d1cd71ba3c95ee934954425df9 100644
--- a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/ValidationView.java
+++ b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/ValidationView.java
@@ -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();
diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/gl/FloatList.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/gl/FloatList.java
new file mode 100644
index 0000000000000000000000000000000000000000..fa26973ff969f09a3bf9a87e7bcfeecabf687c46
--- /dev/null
+++ b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/gl/FloatList.java
@@ -0,0 +1,41 @@
+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); }
+}
diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/gl/GLViewport.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/gl/GLViewport.java
new file mode 100644
index 0000000000000000000000000000000000000000..47b9e16066b9b24dec38fffb58910f215c0c099b
--- /dev/null
+++ b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/gl/GLViewport.java
@@ -0,0 +1,293 @@
+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 pendingScene = new AtomicReference<>();
+ private final AtomicReference 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