Commit 913a2941 authored by Matthias Betz's avatar Matthias Betz
Browse files

Mount GLViewport in MainWindow and rewire Renderer to SceneData



Co-Authored-By: default avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 419243a7
......@@ -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();
......
......@@ -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<Renderable> 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;
......@@ -302,7 +288,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 +831,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 +866,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 +891,8 @@ public class MainWindow extends Application {
}
public void resetCamera() {
cameraXRot = CAMERA_INITIAL_X_ANGLE;
cameraYRot = CAMERA_INITIAL_Y_ANGLE;
cameraZRotation.setAngle(cameraXRot);
cameraXRotation.setAngle(cameraYRot);
glViewport.resetCamera();
alignNorthArrow();
camera.setTranslateX(0);
camera.setTranslateY(0);
if (controller.getOriginBB() != null) {
zoomOutForBoundingBox(controller.getOriginBB());
}
}
public void addFileNameToTitle(String fileName) {
......@@ -1055,8 +960,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 +1048,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 MainWindow mainWindow;
private HighlightController highlightController;
......@@ -40,8 +46,6 @@ public class Renderer {
private LoadingInfoDialog loadingDialog;
private List<ViewFilter> 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<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);
}
}
......@@ -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<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();
});
......@@ -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<LinearRing> highlightedRings) {
highlightController.highlight(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.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() {
......
......@@ -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();
......
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