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
package de.hft.stuttgart.citydoctor2.gui.gl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParser;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import java.util.ArrayList;
import java.util.List;
import javafx.scene.paint.Color;
import org.junit.BeforeClass;
import org.junit.Test;
public class SceneBuilderTest {
private static List<Polygon> polygons;
@BeforeClass
public static void loadFixture() throws Exception {
ParserConfiguration config = new ParserConfiguration(8, false);
CityDoctorModel model = CityGmlParser.parseCityGmlFile(
"src/test/resources/SimpleSolid_SrefBS-GE-gml-LR-0001-T0001.gml", config);
polygons = new ArrayList<>();
model.getBuildings().forEach(b -> {
for (Geometry g : b.getGeometries()) {
for (Polygon p : g.getPolygons()) {
if (!p.isLink()) {
polygons.add(p);
}
}
}
for (de.hft.stuttgart.citydoctor2.datastructure.BoundarySurface bs : b.getBoundarySurfaces()) {
for (Geometry g : bs.getGeometries()) {
for (Polygon p : g.getPolygons()) {
if (!p.isLink()) {
polygons.add(p);
}
}
}
}
});
assertTrue("fixture should contain polygons", !polygons.isEmpty());
}
@Test
public void buildsConsistentBufferLengths() {
SceneData data = SceneBuilder.fromPolygons(polygons, Color.WHITE);
int vertices = data.positions().length / 3;
assertEquals(vertices * 3, data.colors().length);
assertEquals(vertices, data.ids().length);
assertEquals(vertices, data.triangleCount() * 3);
}
@Test
public void everyPolygonGetsAPickingIdResolvingToAPolygon() {
SceneData data = SceneBuilder.fromPolygons(polygons, Color.WHITE);
for (int id : data.ids()) {
assertTrue(data.pickRegistry().resolve(id) instanceof Polygon);
}
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import java.util.List;
import org.junit.Test;
public class SceneDataTest {
@Test
public void exposesItsBuffersAndRegistry() {
float[] pos = {0,0,0, 1,0,0, 0,1,0};
float[] col = {1,1,1, 1,1,1, 1,1,1};
int[] ids = {1,1,1};
PickRegistry reg = new PickRegistry();
List<TriangleMeta> metas = List.of(new TriangleMeta(0, null, 2, false));
SceneData data = new SceneData(pos, col, ids, metas, reg, new double[]{5, 6, 7});
assertSame(pos, data.positions());
assertEquals(1, data.triangleMetas().size());
assertEquals(6.0, data.center()[1], 0.0);
assertSame(reg, data.pickRegistry());
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
import javafx.scene.paint.Color;
import org.junit.Test;
public class ShadeColorTest {
@Test
public void brightnessStaysWithinExpectedBand() {
// Shade brightness is acos(normal.AXIS)/PI * 0.6 + 0.3 -> always within [0.3, 0.9].
Vector3d[] normals = {
new Vector3d(1, 0, 0), new Vector3d(0, 1, 0), new Vector3d(0, 0, 1),
new Vector3d(-1, 0, 0), new Vector3d(0.3, -0.7, 0.5)
};
for (Vector3d n : normals) {
Color c = ShadeColor.shade(n.normalize(), Color.WHITE);
assertTrue("brightness >= 0.3", c.getBrightness() >= 0.3 - 1e-6);
assertTrue("brightness <= 0.9", c.getBrightness() <= 0.9 + 1e-6);
}
}
@Test
public void matchesLegacyFormulaForKnownNormal() {
Vector3d axis = new Vector3d(19, 0.8, 1.5).normalize();
Vector3d normal = new Vector3d(0, 0, 1).normalize();
double cos = Math.max(-1.0, Math.min(1.0, normal.dot(axis)));
double shade = Math.acos(cos) / Math.PI * 0.6 + 0.3;
Color expected = Color.WHITE.deriveColor(0, 1.0, shade, 1.0);
Color actual = ShadeColor.shade(normal, Color.WHITE);
assertEquals(expected.getRed(), actual.getRed(), 1e-9);
assertEquals(expected.getGreen(), actual.getGreen(), 1e-9);
assertEquals(expected.getBlue(), actual.getBlue(), 1e-9);
}
}
...@@ -31,15 +31,16 @@ import de.hft.stuttgart.citydoctor2.datastructure.Opening; ...@@ -31,15 +31,16 @@ import de.hft.stuttgart.citydoctor2.datastructure.Opening;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon; import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.gui.HighlightController; import de.hft.stuttgart.citydoctor2.gui.HighlightController;
import de.hft.stuttgart.citydoctor2.gui.ListErrorVisitor; import de.hft.stuttgart.citydoctor2.gui.ListErrorVisitor;
import de.hft.stuttgart.citydoctor2.gui.TriangulatedGeometry; 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.healer.Healer; import de.hft.stuttgart.citydoctor2.healer.Healer;
import de.hft.stuttgart.citydoctor2.optimization.MeshGenerator; import de.hft.stuttgart.citydoctor2.optimization.MeshGenerator;
import de.hft.stuttgart.citydoctor2.utils.Copy; import de.hft.stuttgart.citydoctor2.utils.Copy;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.scene.control.TextInputDialog; import javafx.scene.control.TextInputDialog;
import javafx.scene.control.TreeItem; import javafx.scene.control.TreeItem;
import javafx.scene.shape.CullFace; import javafx.scene.paint.Color;
import javafx.scene.shape.DrawMode;
public class HealerController { public class HealerController {
...@@ -53,11 +54,11 @@ public class HealerController { ...@@ -53,11 +54,11 @@ public class HealerController {
private Geometry nextGeometry; private Geometry nextGeometry;
private Checker checker; private Checker checker;
private CullFace culling = CullFace.BACK; private boolean culling = false;
private DrawMode drawMode = DrawMode.FILL; private boolean wireframe = false;
private TriangulatedGeometry nextTriangulatedGeometry; private SceneData currentScene;
private TriangulatedGeometry currentTriangulatedGeometry; private SceneData nextScene;
private ListErrorVisitor currentErrorVisitor; private ListErrorVisitor currentErrorVisitor;
private ListErrorVisitor nextErrorVisitor; private ListErrorVisitor nextErrorVisitor;
...@@ -69,14 +70,54 @@ public class HealerController { ...@@ -69,14 +70,54 @@ public class HealerController {
public HealerController(HealerView healerView) { public HealerController(HealerView healerView) {
this.healerView = healerView; this.healerView = healerView;
this.currentHighlights = new HighlightController(healerView.getCurrentWorld()); this.currentHighlights = new HighlightController(healerView.getCurrentViewport());
this.nextHighlights = new HighlightController(healerView.getNextWorld()); this.nextHighlights = new HighlightController(healerView.getNextViewport());
currentHighlights.setCenterSupplier(() -> currentScene == null ? new double[3] : currentScene.center());
nextHighlights.setCenterSupplier(() -> nextScene == null ? new double[3] : nextScene.center());
currentErrorVisitor = new ListErrorVisitor(currentHighlights); currentErrorVisitor = new ListErrorVisitor(currentHighlights);
nextErrorVisitor = new ListErrorVisitor(nextHighlights); nextErrorVisitor = new ListErrorVisitor(nextHighlights);
healerView.translateZProperty().addListener((obs, oldV, newV) -> { }
currentHighlights.changeScaling(-newV.doubleValue());
nextHighlights.changeScaling(-newV.doubleValue()); /** Installs the current scene into the current viewport and applies the current view options. */
}); private void showCurrentScene() {
currentHighlights.clearHighlights();
healerView.getCurrentViewport().setScene(currentScene);
// the viewport draws nothing until it is told which triangles are visible; the healer has no
// LOD/roof filtering, so show every triangle of the scene
healerView.getCurrentViewport().setIndexSet(allTriangles(currentScene));
healerView.getCurrentViewport().setWireframe(wireframe);
healerView.getCurrentViewport().setCulling(culling);
}
/** Installs the next scene into the next viewport and applies the current view options. */
private void showNextScene() {
nextHighlights.clearHighlights();
healerView.getNextViewport().setScene(nextScene);
// the viewport draws nothing until it is told which triangles are visible; the healer has no
// LOD/roof filtering, so show every triangle of the scene
healerView.getNextViewport().setIndexSet(allTriangles(nextScene));
healerView.getNextViewport().setWireframe(wireframe);
healerView.getNextViewport().setCulling(culling);
}
/** Builds an index set covering every triangle of the scene (no filtering). */
private static int[] allTriangles(SceneData scene) {
if (scene == null) {
return new int[0];
}
return IndexSetBuilder.build(scene.triangleMetas(), m -> true);
}
private void clearCurrentScene() {
currentScene = null;
currentHighlights.clearHighlights();
healerView.getCurrentViewport().clearScene();
}
private void clearNextScene() {
nextScene = null;
nextHighlights.clearHighlights();
healerView.getNextViewport().clearScene();
} }
public void showSimplyfication() { public void showSimplyfication() {
...@@ -91,8 +132,8 @@ public class HealerController { ...@@ -91,8 +132,8 @@ public class HealerController {
public void setCurrentModel(CityDoctorModel model) { public void setCurrentModel(CityDoctorModel model) {
if (this.model != model) { if (this.model != model) {
healerView.getCurrentMeshGroup().getChildren().clear(); clearCurrentScene();
healerView.getNextMeshGroup().getChildren().clear(); clearNextScene();
healerView.getCurrentErrorList().getItems().clear(); healerView.getCurrentErrorList().getItems().clear();
} }
this.model = model; this.model = model;
...@@ -274,18 +315,14 @@ public class HealerController { ...@@ -274,18 +315,14 @@ public class HealerController {
SelectableGeometry selectedGeometry = (SelectableGeometry) selectedItem; SelectableGeometry selectedGeometry = (SelectableGeometry) selectedItem;
currentGeometry = selectedGeometry.getGeometry(); currentGeometry = selectedGeometry.getGeometry();
currentFeature = selectedGeometry.getFeature(); currentFeature = selectedGeometry.getFeature();
currentTriangulatedGeometry = TriangulatedGeometry.of(currentGeometry); currentScene = SceneBuilder.fromPolygons(currentGeometry.getPolygons(), Color.WHITE);
currentErrorVisitor.setGeometry(currentTriangulatedGeometry);
currentTriangulatedGeometry.setCullFace(culling);
currentTriangulatedGeometry.setDrawMode(drawMode);
healerView.setSelectedFeatureText(currentFeature.getGmlId().getGmlString()); healerView.setSelectedFeatureText(currentFeature.getGmlId().getGmlString());
healerView.setFeatureTypeText(currentFeature.getFeatureType().toString()); healerView.setFeatureTypeText(currentFeature.getFeatureType().toString());
healerView.setGeometryText(currentGeometry.getLod().toString() + ", " + currentGeometry.getType().toString()); healerView.setGeometryText(currentGeometry.getLod().toString() + ", " + currentGeometry.getType().toString());
updateCurrentErrors(); updateCurrentErrors();
healerView.zoomOutForBoundingBox(currentGeometry.calculateBoundingBox()); healerView.zoomOutForBoundingBox(currentGeometry.calculateBoundingBox());
healerView.getCurrentMeshGroup().getChildren().clear(); showCurrentScene();
healerView.getNextMeshGroup().getChildren().clear(); clearNextScene();
healerView.getCurrentMeshGroup().getChildren().addAll(currentTriangulatedGeometry.getMeshes());
healerView.getNextStepBtn().setDisable(false); healerView.getNextStepBtn().setDisable(false);
healerView.getHealCompleteBtn().setDisable(false); healerView.getHealCompleteBtn().setDisable(false);
healerView.getAcceptBtn().setDisable(true); healerView.getAcceptBtn().setDisable(true);
...@@ -322,12 +359,8 @@ public class HealerController { ...@@ -322,12 +359,8 @@ public class HealerController {
} }
private void updateNextGeometryView() { private void updateNextGeometryView() {
nextTriangulatedGeometry = TriangulatedGeometry.of(nextGeometry); nextScene = SceneBuilder.fromPolygons(nextGeometry.getPolygons(), Color.WHITE);
nextErrorVisitor.setGeometry(nextTriangulatedGeometry); showNextScene();
nextTriangulatedGeometry.setCullFace(culling);
nextTriangulatedGeometry.setDrawMode(drawMode);
healerView.getNextMeshGroup().getChildren().clear();
healerView.getNextMeshGroup().getChildren().addAll(nextTriangulatedGeometry.getMeshes());
healerView.getAcceptBtn().setDisable(false); healerView.getAcceptBtn().setDisable(false);
healerView.getCancelBtn().setDisable(false); healerView.getCancelBtn().setDisable(false);
healerView.getSolidInjectorBtn().setDisable(true); healerView.getSolidInjectorBtn().setDisable(true);
...@@ -346,12 +379,8 @@ public class HealerController { ...@@ -346,12 +379,8 @@ public class HealerController {
Healer healer = new Healer(checker); Healer healer = new Healer(checker);
healer.heal(nextGeometry, nextFeature); healer.heal(nextGeometry, nextFeature);
updateNextErrors(); updateNextErrors();
nextTriangulatedGeometry = TriangulatedGeometry.of(nextGeometry); nextScene = SceneBuilder.fromPolygons(nextGeometry.getPolygons(), Color.WHITE);
nextErrorVisitor.setGeometry(nextTriangulatedGeometry); showNextScene();
nextTriangulatedGeometry.setCullFace(culling);
nextTriangulatedGeometry.setDrawMode(drawMode);
healerView.getNextMeshGroup().getChildren().clear();
healerView.getNextMeshGroup().getChildren().addAll(nextTriangulatedGeometry.getMeshes());
healerView.getAcceptBtn().setDisable(false); healerView.getAcceptBtn().setDisable(false);
healerView.getNextStepBtn().setDisable(true); healerView.getNextStepBtn().setDisable(true);
healerView.getHealCompleteBtn().setDisable(true); healerView.getHealCompleteBtn().setDisable(true);
...@@ -414,14 +443,9 @@ public class HealerController { ...@@ -414,14 +443,9 @@ public class HealerController {
currentGeometry = nextGeometry; currentGeometry = nextGeometry;
updateCurrentErrors(); updateCurrentErrors();
healerView.getNextErrorList().getItems().clear(); healerView.getNextErrorList().getItems().clear();
healerView.getCurrentMeshGroup().getChildren().clear(); currentScene = nextScene;
healerView.getCurrentMeshGroup().getChildren().addAll(nextTriangulatedGeometry.getMeshes()); showCurrentScene();
currentTriangulatedGeometry = nextTriangulatedGeometry; clearNextScene();
currentErrorVisitor.setGeometry(currentTriangulatedGeometry);
nextTriangulatedGeometry = null;
// nextHighlights = new HighlightController(healerView.getNextWorld());
// nextErrorVisitor = new ListErrorVisitor(nextHighlights);
healerView.getNextMeshGroup().getChildren().clear();
healerView.getHealCompleteBtn().setDisable(false); healerView.getHealCompleteBtn().setDisable(false);
healerView.getNextStepBtn().setDisable(false); healerView.getNextStepBtn().setDisable(false);
healerView.getAcceptBtn().setDisable(true); healerView.getAcceptBtn().setDisable(true);
...@@ -429,27 +453,15 @@ public class HealerController { ...@@ -429,27 +453,15 @@ public class HealerController {
} }
public void setWireframe(boolean b) { public void setWireframe(boolean b) {
if (b) { this.wireframe = b;
drawMode = DrawMode.LINE; healerView.getCurrentViewport().setWireframe(b);
} else { healerView.getNextViewport().setWireframe(b);
drawMode = DrawMode.FILL;
}
if (currentTriangulatedGeometry != null) {
currentTriangulatedGeometry.setDrawMode(drawMode);
}
if (nextTriangulatedGeometry != null) {
nextTriangulatedGeometry.setDrawMode(drawMode);
}
} }
public void setCulling(CullFace culling) { public void setCulling(boolean culling) {
this.culling = culling; this.culling = culling;
if (currentTriangulatedGeometry != null) { healerView.getCurrentViewport().setCulling(culling);
currentTriangulatedGeometry.setCullFace(culling); healerView.getNextViewport().setCulling(culling);
}
if (nextTriangulatedGeometry != null) {
nextTriangulatedGeometry.setCullFace(culling);
}
} }
public void cancelClicked() { public void cancelClicked() {
...@@ -457,14 +469,13 @@ public class HealerController { ...@@ -457,14 +469,13 @@ public class HealerController {
return; return;
} }
healerView.getNextErrorList().getItems().clear(); healerView.getNextErrorList().getItems().clear();
healerView.getNextMeshGroup().getChildren().clear(); clearNextScene();
healerView.getAcceptBtn().setDisable(true); healerView.getAcceptBtn().setDisable(true);
healerView.getCancelBtn().setDisable(true); healerView.getCancelBtn().setDisable(true);
healerView.getNextStepBtn().setDisable(false); healerView.getNextStepBtn().setDisable(false);
healerView.getHealCompleteBtn().setDisable(false); healerView.getHealCompleteBtn().setDisable(false);
nextFeature = null; nextFeature = null;
nextGeometry = null; nextGeometry = null;
nextTriangulatedGeometry = null;
} }
public void currentHighlight(CheckError err) { public void currentHighlight(CheckError err) {
...@@ -527,8 +538,8 @@ public class HealerController { ...@@ -527,8 +538,8 @@ public class HealerController {
healerView.getHealCompleteBtn().setDisable(true); healerView.getHealCompleteBtn().setDisable(true);
healerView.getCurrentErrorList().getItems().clear(); healerView.getCurrentErrorList().getItems().clear();
healerView.getNextErrorList().getItems().clear(); healerView.getNextErrorList().getItems().clear();
healerView.getCurrentMeshGroup().getChildren().clear(); clearCurrentScene();
healerView.getNextMeshGroup().getChildren().clear(); clearNextScene();
healerView.setSelectedFeatureText(""); healerView.setSelectedFeatureText("");
healerView.setFeatureTypeText(""); healerView.setFeatureTypeText("");
healerView.setGeometryText(""); healerView.setGeometryText("");
......
...@@ -13,7 +13,6 @@ import javafx.scene.image.Image; ...@@ -13,7 +13,6 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.shape.CullFace;
public class HealerToolbar { public class HealerToolbar {
...@@ -63,13 +62,7 @@ public class HealerToolbar { ...@@ -63,13 +62,7 @@ public class HealerToolbar {
wireframeImage.setImage(new Image(MainToolBar.class.getResourceAsStream("icons/wireframe32x32.png"))); wireframeImage.setImage(new Image(MainToolBar.class.getResourceAsStream("icons/wireframe32x32.png")));
simplyImageView.setImage(new Image(HealerToolbar.class.getResourceAsStream("autopro32x32.png"))); simplyImageView.setImage(new Image(HealerToolbar.class.getResourceAsStream("autopro32x32.png")));
wireframeBtn.selectedProperty().addListener((obs, oldV, newV) -> controller.setWireframe(newV)); wireframeBtn.selectedProperty().addListener((obs, oldV, newV) -> controller.setWireframe(newV));
cullingBtn.selectedProperty().addListener((obs, oldV, newV) -> { cullingBtn.selectedProperty().addListener((obs, oldV, newV) -> controller.setCulling(Boolean.TRUE.equals(newV)));
if (Boolean.TRUE.equals(newV)) {
controller.setCulling(CullFace.BACK);
} else {
controller.setCulling(CullFace.NONE);
}
});
saveBtn.setOnAction(ae -> cdController.askAndSave()); saveBtn.setOnAction(ae -> cdController.askAndSave());
saveBtn.setDisable(true); saveBtn.setDisable(true);
saveImage.setImage(new Image(MainWindow.class.getResourceAsStream("icons/save.png"))); saveImage.setImage(new Image(MainWindow.class.getResourceAsStream("icons/save.png")));
......
...@@ -13,16 +13,10 @@ import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel; ...@@ -13,16 +13,10 @@ import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.gui.FilterPane; import de.hft.stuttgart.citydoctor2.gui.FilterPane;
import de.hft.stuttgart.citydoctor2.gui.MainWindow; import de.hft.stuttgart.citydoctor2.gui.MainWindow;
import de.hft.stuttgart.citydoctor2.gui.View; import de.hft.stuttgart.citydoctor2.gui.View;
import javafx.beans.property.DoubleProperty; import de.hft.stuttgart.citydoctor2.gui.gl.GLViewport;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.AmbientLight;
import javafx.scene.Group;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.PerspectiveCamera;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ListView; import javafx.scene.control.ListView;
...@@ -30,11 +24,9 @@ import javafx.scene.control.Tooltip; ...@@ -30,11 +24,9 @@ import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.TextFieldListCell; import javafx.scene.control.cell.TextFieldListCell;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.scene.paint.Color; import javafx.scene.layout.Region;
import javafx.scene.transform.Rotate;
import javafx.stage.Window; import javafx.stage.Window;
import javafx.util.StringConverter; import javafx.util.StringConverter;
...@@ -42,10 +34,6 @@ public class HealerView extends View { ...@@ -42,10 +34,6 @@ public class HealerView extends View {
private static final Logger logger = LogManager.getLogger(HealerView.class); private static final Logger logger = LogManager.getLogger(HealerView.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;
private Image image; private Image image;
private Node healerWindow; private Node healerWindow;
...@@ -55,13 +43,6 @@ public class HealerView extends View { ...@@ -55,13 +43,6 @@ public class HealerView extends View {
private ChooseFeatureDialog chooseFeatureDialog; private ChooseFeatureDialog chooseFeatureDialog;
private HealerToolbar toolBar; private HealerToolbar toolBar;
private DoubleProperty translateZ;
private DoubleProperty cameraXRot;
private DoubleProperty cameraYRot;
private double dragX;
private double dragY;
@FXML @FXML
private Button fixBtn; private Button fixBtn;
...@@ -121,11 +102,9 @@ public class HealerView extends View { ...@@ -121,11 +102,9 @@ public class HealerView extends View {
@FXML @FXML
private Button meshBtn; private Button meshBtn;
private Group currentWorld; private GLViewport currentViewport;
private Group nextWorld; private GLViewport nextViewport;
private Group currentMeshGroup;
private Group nextMeshGroup;
public HealerView() { public HealerView() {
image = new Image(getClass().getResourceAsStream("healing.png")); image = new Image(getClass().getResourceAsStream("healing.png"));
...@@ -198,104 +177,40 @@ public class HealerView extends View { ...@@ -198,104 +177,40 @@ public class HealerView extends View {
} }
private void setup3dViews() { private void setup3dViews() {
translateZ = new SimpleDoubleProperty(CAMERA_TRANSLATE_Z); currentViewport = new GLViewport();
cameraXRot = new SimpleDoubleProperty(CAMERA_INITIAL_X_ANGLE); nextViewport = new GLViewport();
cameraYRot = new SimpleDoubleProperty(CAMERA_INITIAL_Y_ANGLE); attachViewport(currentCanvas, currentViewport);
attachViewport(nextCanvas, nextViewport);
buildCurrent3dView(); coupleViewports();
buildNext3dView(); }
}
private void attachViewport(Pane canvas, GLViewport viewport) {
private void buildCurrent3dView() { Region node = viewport.getNode();
Group currentRoot = new Group(); node.prefWidthProperty().bind(canvas.widthProperty());
SubScene currentScene = new SubScene(currentRoot, 500, 300, true, SceneAntialiasing.BALANCED); node.prefHeightProperty().bind(canvas.heightProperty());
currentScene.heightProperty().bind(currentCanvas.heightProperty()); canvas.getChildren().add(node);
currentScene.widthProperty().bind(currentCanvas.widthProperty()); }
currentScene.setFill(Color.AZURE);
currentCanvas.getChildren().add(currentScene); /**
currentWorld = new Group(); * Keeps the two viewports' cameras in sync: orbiting/panning/zooming one mirrors the camera into
currentRoot.getChildren().add(currentWorld); * the other and redraws it, reproducing the legacy shared-camera behaviour of the dual SubScenes.
currentMeshGroup = new Group(); */
currentWorld.getChildren().add(currentMeshGroup); private void coupleViewports() {
currentViewport.setInteractionListener(() -> {
AmbientLight al = new AmbientLight(Color.WHITE); nextViewport.camera().copyOrbitFrom(currentViewport.camera());
currentRoot.getChildren().add(al); nextViewport.requestRedraw();
});
PerspectiveCamera currentCamera = new PerspectiveCamera(true); nextViewport.setInteractionListener(() -> {
currentCamera.setNearClip(0.1); currentViewport.camera().copyOrbitFrom(nextViewport.camera());
currentCamera.setFarClip(10000d); currentViewport.requestRedraw();
currentCamera.translateZProperty().bind(translateZ); });
Rotate cameraZRotation = new Rotate();
Rotate cameraXRotation = new Rotate();
cameraXRotation.setAxis(Rotate.X_AXIS);
cameraZRotation.setAxis(Rotate.Z_AXIS);
cameraZRotation.angleProperty().bind(cameraXRot);
cameraXRotation.angleProperty().bind(cameraYRot);
currentWorld.getTransforms().add(cameraXRotation);
currentWorld.getTransforms().add(cameraZRotation);
currentRoot.getChildren().add(currentCamera);
currentScene.setCamera(currentCamera);
setupMeshViewControls(currentCanvas);
}
private void buildNext3dView() {
Group nextRoot = new Group();
SubScene nextScene = new SubScene(nextRoot, 500, 300, true, SceneAntialiasing.BALANCED);
nextScene.heightProperty().bind(nextCanvas.heightProperty());
nextScene.widthProperty().bind(nextCanvas.widthProperty());
nextScene.setFill(Color.AZURE);
nextCanvas.getChildren().add(nextScene);
nextWorld = new Group();
nextRoot.getChildren().add(nextWorld);
nextMeshGroup = new Group();
nextWorld.getChildren().add(nextMeshGroup);
AmbientLight al = new AmbientLight(Color.WHITE);
nextRoot.getChildren().add(al);
PerspectiveCamera nextCamera = new PerspectiveCamera(true);
nextCamera.setNearClip(0.1);
nextCamera.setFarClip(10000d);
nextCamera.translateZProperty().bind(translateZ);
Rotate cameraZRotation = new Rotate();
Rotate cameraXRotation = new Rotate();
cameraXRotation.setAxis(Rotate.X_AXIS);
cameraZRotation.setAxis(Rotate.Z_AXIS);
cameraZRotation.angleProperty().bind(cameraXRot);
cameraXRotation.angleProperty().bind(cameraYRot);
nextWorld.getTransforms().add(cameraXRotation);
nextWorld.getTransforms().add(cameraZRotation);
nextRoot.getChildren().add(nextCamera);
nextScene.setCamera(nextCamera);
setupMeshViewControls(nextCanvas);
} }
public void zoomOutForBoundingBox(BoundingBox b) { public void zoomOutForBoundingBox(BoundingBox b) {
double longestSide = b.getDiagonalLength() * 0.4; currentViewport.camera().zoomOutForBoundingBox(b.getDiagonalLength());
double d = longestSide / Math.tan(Math.toRadians(30) / 2); nextViewport.camera().copyOrbitFrom(currentViewport.camera());
translateZ.set(-d); currentViewport.requestRedraw();
} nextViewport.requestRedraw();
private void setupMeshViewControls(Pane canvas) {
canvas.setOnMousePressed(me -> {
if (me.getButton() == MouseButton.PRIMARY) {
dragX = me.getScreenX();
dragY = me.getScreenY();
}
});
canvas.setOnScroll(se -> translateZ.set(translateZ.get() + se.getDeltaY() / 10d));
canvas.setOnMouseDragged(me -> {
if (me.getButton() == MouseButton.PRIMARY) {
double deltaX = me.getScreenX() - dragX;
double deltaY = me.getScreenY() - dragY;
dragX = me.getScreenX();
dragY = me.getScreenY();
cameraXRot.set(cameraXRot.get() + ((deltaX / 3d) % 360));
cameraYRot.set(cameraYRot.get() + ((deltaY / 3d) % 360));
}
});
} }
@Override @Override
...@@ -330,12 +245,12 @@ public class HealerView extends View { ...@@ -330,12 +245,12 @@ public class HealerView extends View {
return chooseFeatureDialog; return chooseFeatureDialog;
} }
public Group getCurrentWorld() { public GLViewport getCurrentViewport() {
return currentWorld; return currentViewport;
} }
public Group getNextWorld() { public GLViewport getNextViewport() {
return nextWorld; return nextViewport;
} }
public void setSelectedFeatureText(String text) { public void setSelectedFeatureText(String text) {
...@@ -378,18 +293,6 @@ public class HealerView extends View { ...@@ -378,18 +293,6 @@ public class HealerView extends View {
return cancelButton; return cancelButton;
} }
public DoubleProperty translateZProperty() {
return translateZ;
}
public Group getNextMeshGroup() {
return nextMeshGroup;
}
public Group getCurrentMeshGroup() {
return currentMeshGroup;
}
public Button getSolidInjectorBtn() { public Button getSolidInjectorBtn() {
return solidInjectorBtn; return solidInjectorBtn;
} }
......
...@@ -28,6 +28,11 @@ ...@@ -28,6 +28,11 @@
<id>sonartype</id> <id>sonartype</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url> <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
</repository> </repository>
<!-- JogAmp publishes JOGL/GlueGen 2.4.0+ here; Maven Central only has up to 2.3.2. -->
<repository>
<id>jogamp</id>
<url>https://jogamp.org/deployment/maven/</url>
</repository>
</repositories> </repositories>
<build> <build>
<pluginManagement> <pluginManagement>
...@@ -168,7 +173,7 @@ ...@@ -168,7 +173,7 @@
<dependency> <dependency>
<groupId>org.jogamp.gluegen</groupId> <groupId>org.jogamp.gluegen</groupId>
<artifactId>gluegen-rt-main</artifactId> <artifactId>gluegen-rt-main</artifactId>
<version>2.3.2</version> <version>2.5.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>gov.nist.math</groupId> <groupId>gov.nist.math</groupId>
...@@ -178,7 +183,7 @@ ...@@ -178,7 +183,7 @@
<dependency> <dependency>
<groupId>org.jogamp.jogl</groupId> <groupId>org.jogamp.jogl</groupId>
<artifactId>jogl-all-main</artifactId> <artifactId>jogl-all-main</artifactId>
<version>2.3.2</version> <version>2.5.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.logging.log4j</groupId> <groupId>org.apache.logging.log4j</groupId>
......
This diff is collapsed.
# OpenGL 3D View — Design Spec
**Date:** 2026-06-18
**Branch:** 107-opengl-view
**Component:** `CityDoctorParent/Extensions/CityDoctorGUI`
## 1. Problem & Goal
The GUI renders 3D CityGML geometry with JavaFX, tesselating **each polygon into its own
`TriangleMesh` + `MeshView` + `PhongMaterial`** (see `TriangulatedGeometry`). At city scale this
produces thousands of scene-graph nodes, each with its own material — the JavaFX node overhead, not
just the lack of shader access, makes orbit/zoom stutter or freeze.
**Goal:** Replace the JavaFX `SubScene` 3D view with an OpenGL-rendered viewport that orbits, zooms,
and pans smoothly on whole-city models (millions of triangles), while preserving every existing view
feature (picking, highlights, filters, wireframe, culling, north arrow, screenshot).
## 2. Decisions (settled during brainstorming)
| Topic | Decision |
|-------|----------|
| Scale target | Whole cities, millions of triangles; smooth (30–60 fps) interaction. |
| Integration | Embed an OpenGL canvas via the already-present `openglfx-jogl` (huskerdev) dependency, in the `meshView` `Pane` where the `SubScene` lives today. |
| GL version | OpenGL **3.3 core profile**, GLSL shaders, VAO/VBO. |
| Platforms | **Windows + Linux** are hard targets. macOS bundle still builds but the GL view may degrade — accepted. |
| JavaFX 3D path | **Fully removed** (no fallback). |
| Buffer strategy | **Single global static VBO**; one `glDrawElements` per frame; filter/LOD changes rebuild the draw **index set** (EBO), not the VBO. |
| Picking | **GPU color-picking**: off-screen FBO encodes `polygonId` per fragment; `glReadPixels` on click. |
| Shading | **Keep existing fake shading** (`normal·AXIS` → brightness), **baked into per-vertex color on the CPU** at build time. No lighting in the shader. Normal attribute dropped from the VBO. |
| Selection | Shader `selectedId` uniform tints matching fragments — no buffer mutation. |
| Vertex/edge highlights | Separate small **dynamic** GL overlay buffer (replaces JavaFX `Sphere`/`Cylinder`). |
## 3. Architecture & Components
New/changed classes in `de.hft.stuttgart.citydoctor2.gui` (or a new `gui.gl` sub-package):
- **`GLViewport`** *(new)* — wraps the openglfx canvas, owns the GL context, the render callback, the
`Camera`, and GL resources (shaders, VAO/VBO/EBO, picking FBO, overlay buffer). Replaces the JavaFX
`SubScene`/`world`/`meshGroup`/`PerspectiveCamera`/`AmbientLight` built in `MainWindow.setup3dView`.
- **`SceneBuilder`** *(replaces `TriangulatedGeometry`)* — flattens the model into interleaved vertex
data and the `id → Polygon/Vertex/Edge` maps. Runs on a worker thread (as `Renderer.render` already
does). Reuses existing `Polygon.tesselate()` → `TesselatedPolygon`/`Triangle3d`, the
`findCenter`/`movedBy` recentering, and the `calculateMaterial` shade math.
- **`SceneBuffers`** *(new)* — owns the global VBO + static vertex data and rebuilds/uploads the draw
index set on filter/LOD changes. GL-thread owned; uploads queued from the worker thread.
- **`Camera`** *(new)* — view/projection matrices replicating today's interaction: orbit (Z + X
rotation, today on the `world` group), right-drag pan, scroll zoom, `zoomOutForBoundingBox`,
`resetCamera`. Reuses the existing angle/translate state in `MainWindow`.
- **`PickingPass`** *(new)* — off-screen FBO sized to the canvas; renders the `polygonId` pass and
reads back the cursor pixel on click.
- **`HighlightOverlay`** *(replaces the `Sphere`/`Cylinder` rendering in `HighlightController`)* —
small dynamic buffer for vertex/edge markers, rescaled on zoom like `changeScaling` does now.
- **`Renderer`** *(kept)* — **public API unchanged** (`render(model)`, `render(Geometry)`,
`render(CityObject)`, `highlight(...)`, `showWireFrame`, `enableCulling`, `enableLodN`/`disableLodN`,
`hideRoofs`/`showRoofs`, `clearCurrentRender`, etc.) so `CityDoctorController`, `MainWindow`, and the
error visitors (`ListErrorVisitor`, `GlobalErrorVisitor`) are largely untouched. Internally it drives
`SceneBuilder`/`SceneBuffers`/`GLViewport` instead of the JavaFX `meshGroup`.
## 4. Geometry Pipeline (single global VBO)
At load, off the FX thread:
1. Iterate city objects → polygons → `tesselate()` → triangles, recentered by `movedBy`
(`SceneBuilder.findCenter`, same as today).
2. Build **one interleaved static VBO** with per-vertex attributes:
- `position` — `vec3 float`
- `color` — `vec3 float`, the **baked shaded color** (see §6)
- `polygonId` — `uint` (or packed into a `vec4` for the picking pass)
(No normal attribute — shading is baked, picking/selection use the id.)
3. Build CPU-side maps **`polygonId → Polygon`** (and ids for vertices/edges) — these replace the
per-`MeshView` `userData` dispatchers (`PolygonClickDispatcher`, `VertexClickDispatcher`).
4. Build an **index buffer (EBO)** listing the triangles to draw. The existing `Renderer` filter set
(`lodFilters`, `roofFilter`) determines membership; toggling a filter rebuilds the index array on
the CPU and re-uploads it. The VBO is never touched. One `glDrawElements` per frame.
`polygonId` is a 32-bit value (RGBA8 in the picking FBO → ~4.2B ids), more than enough.
## 5. Render Loop & Threading
- openglfx calls a render callback on its GL render thread with a live context. The callback only
**draws** from prepared buffers (main pass; picking pass on demand; overlay pass).
- GL resource **creation/upload** also happens on the GL thread: `SceneBuilder` runs on a worker
thread and **queues** the prepared arrays; the render callback drains the queue and uploads.
- FX-thread code (filter toggles, tree selection, camera input) sets dirty flags / state the render
callback consumes. No GL calls off the GL thread.
## 6. Shading (baked, fake)
Keep the current surface-differentiation shading. At build time, for each polygon compute exactly as
`TriangulatedGeometry.calculateMaterial` does today:
```
normal = p.calculateNormalNormalized()
base = p.getRenderColor() (if set and not WHITE) else feature base color
acos = Math.acos(normal·AXIS) / Math.PI // AXIS = normalize(19, 0.8, 1.5)
shade = acos * 0.6 + 0.3 // range [0.3, 0.9]
vertexColor = base.deriveColor(0, 1.0, shade, 1.0)
```
Store `vertexColor` as the per-vertex `color` attribute. The fragment shader outputs the interpolated
vertex color directly — no lighting computation in the shader.
## 7. Picking (GPU color-picking)
- A picking shader writes `polygonId` (as RGBA8) into an off-screen FBO sized to the canvas.
- On click, reuse `MainWindow`'s existing press/release "did the mouse move >3px?" gate. Render (or
reuse) the id pass, `glReadPixels` the cursor pixel → `polygonId` → `Polygon`/`Vertex`/`Edge` via the
CPU map → same downstream handling that `PolygonClickDispatcher`/`VertexClickDispatcher` perform now.
- Readback only on click (never per frame) to avoid GPU stalls.
## 8. Selection & Highlights
- **Selection tint:** the main shader takes a `selectedId` uniform (or a small id set); fragments whose
id matches are tinted. Highlighting a clicked polygon needs no buffer mutation.
- **Vertex/edge markers** (today's `HighlightController` spheres/cylinders): drawn from a separate
**dynamic** buffer after the main pass — points/instanced quads for vertices, line segments (or thin
instanced cylinders) for edges — rescaled on zoom as `changeScaling` does today. `HighlightController`
keeps its public API (`highlight(Polygon/LinearRing/Edge/Vertex/...)`, `clearHighlights`,
`changeScaling`, `highlightEdges`, `highlightPolygons`) so callers are unaffected.
## 9. Feature Parity Checklist
All preserved:
- LOD1–LOD4 filters, hide-roofs toggle (index-set rebuild).
- Wireframe (`DrawMode.LINE` → `glPolygonMode(GL_LINE)` with the black grid color).
- Back-face culling toggle (`glEnable/Disable(GL_CULL_FACE)`).
- Per-feature base colors and per-polygon error coloring (`Polygon.getRenderColor()`).
- Vertex/edge/polygon highlighting.
- North-arrow overlay — stays a JavaFX `ImageView` layered on top of the GL canvas in the `meshView`
`Pane`; `alignNorthArrow` logic unchanged.
- Screenshot (`MainWindow.takeViewScreenshot`) — `glReadPixels` of the framebuffer → `BufferedImage` →
PNG, replacing `SubScene.snapshot`.
- Camera orbit/pan/zoom, `resetCamera`, `zoomOutForBoundingBox`.
## 10. Removals
- `TriangulatedGeometry` (replaced by `SceneBuilder`/`SceneBuffers`).
- The JavaFX 3D scene in `MainWindow.setup3dView`: `SubScene geomScene`, `world`/`meshGroup` groups,
`PerspectiveCamera`, `AmbientLight`, `cameraXRotation`/`cameraZRotation` transforms, and
`getMeshGroup()`.
- `Sphere`/`Cylinder` rendering inside `HighlightController` (logic moves to `HighlightOverlay`).
- Mesh-level JavaFX picking (`pickResult.getIntersectedNode()` → replaced by color-picking).
## 11. Risks & Mitigations
- **Native lib packaging:** openglfx + JOGL natives must ship in the win/lin assembly descriptors
(`src/assembly/{win,lin}/assembly.xml`). Verify the assembled bundles launch and render.
- **GL context lifecycle vs JavaFX resize:** handle canvas resize (recreate picking FBO, update
viewport/projection).
- **Picking readback stalls:** mitigated by reading only on click.
- **macOS degradation:** accepted; not a hard target.
## 12. Testing
- **Unit (headless):** `SceneBuilder` flattening — vertex/triangle counts, `id → Polygon` mapping
correctness, recentering by `movedBy`, baked-color math matches `calculateMaterial`.
- **Manual/visual:** rendering correctness, picking accuracy (click → correct polygon/vertex/edge),
filter/LOD/roof toggles, wireframe, culling, highlights, north arrow, screenshot, camera controls —
on a large city model, confirming smooth interaction.
## 13. Out of Scope
- LOD-of-detail mesh simplification / frustum culling / occlusion culling (single global VBO + one draw
call is expected to suffice; revisit only if profiling shows a need).
- macOS-specific GL/threading work.
- Any change to validation, parsing, or non-GUI modules.
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