Commit 6d0d664e authored by Matthias Betz's avatar Matthias Betz
Browse files

Merge remote-tracking branch 'remotes/origin/dev' into dev_baris_aabb_auto_mergeable

parents a7ed4b64 cc4fe260
Pipeline #12443 passed with stage
in 2 minutes and 42 seconds
...@@ -24,16 +24,7 @@ import de.hft.stuttgart.citydoctor2.check.CheckError; ...@@ -24,16 +24,7 @@ import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration; import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration;
import de.hft.stuttgart.citydoctor2.checkresult.utility.CheckReportWriteException; import de.hft.stuttgart.citydoctor2.checkresult.utility.CheckReportWriteException;
import de.hft.stuttgart.citydoctor2.checks.Checks; import de.hft.stuttgart.citydoctor2.checks.Checks;
import de.hft.stuttgart.citydoctor2.datastructure.BridgeObject;
import de.hft.stuttgart.citydoctor2.datastructure.Building;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel; import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.CityFurniture;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.GenericCityObject;
import de.hft.stuttgart.citydoctor2.datastructure.TransportationObject;
import de.hft.stuttgart.citydoctor2.datastructure.Tunnel;
import de.hft.stuttgart.citydoctor2.datastructure.Vegetation;
import de.hft.stuttgart.citydoctor2.datastructure.WaterObject;
/** /**
* *
......
...@@ -285,7 +285,6 @@ public class PdfStreamReporter implements StreamReporter { ...@@ -285,7 +285,6 @@ public class PdfStreamReporter implements StreamReporter {
tSection.setHeadlineColor(OK_COLOR); tSection.setHeadlineColor(OK_COLOR);
} }
writeErrorForCityObject(co, tSection); writeErrorForCityObject(co, tSection);
TransportationObject to = (TransportationObject) co;
} }
private void reportVegetation(CityObject co, boolean hasError) { private void reportVegetation(CityObject co, boolean hasError) {
...@@ -389,25 +388,6 @@ public class PdfStreamReporter implements StreamReporter { ...@@ -389,25 +388,6 @@ public class PdfStreamReporter implements StreamReporter {
} }
} }
private void writeCheckResultForTransportationObject(TransportationObject to, Section root) {
Map<CheckId, CheckResult> results = to.getAllCheckResults();
writeCheckResults(results.values(), root);
for (Geometry geom : to.getGeometries()) {
writeCheckResultForGeometry(geom, root);
}
}
private void writeCheckResultForBuildingPart(BuildingPart bp, Section root) {
Map<CheckId, CheckResult> results = bp.getAllCheckResults();
writeCheckResults(results.values(), root);
for (Geometry geom : bp.getGeometries()) {
writeCheckResultForGeometry(geom, root);
}
for (BoundarySurface bs : bp.getBoundarySurfaces()) {
writeCheckResultForBoundarySurface(bs, root);
}
}
private void writeCheckResultForAbstractBuilding(AbstractBuilding ab, Section root) { private void writeCheckResultForAbstractBuilding(AbstractBuilding ab, Section root) {
Map<CheckId, CheckResult> results = ab.getAllCheckResults(); Map<CheckId, CheckResult> results = ab.getAllCheckResults();
writeCheckResults(results.values(), root); writeCheckResults(results.values(), root);
...@@ -714,24 +694,6 @@ public class PdfStreamReporter implements StreamReporter { ...@@ -714,24 +694,6 @@ public class PdfStreamReporter implements StreamReporter {
report.save(outFile); report.save(outFile);
} }
private void countFinishedReportBuildings() {
for (Section s : buildings.getSubSections()) {
if (!s.hasErrors()) {
numOkBuildings++;
// building has no errors, no table
continue;
}
numErrorBuildings++;
Table t = new Table(2);
t.setTableColumnWidth(75, 25);
t.setTitle("Error", "Count");
for (Entry<String, AtomicInteger> e : s.getStats().getErrorCounts().entrySet()) {
t.addRow(e.getKey(), e.getValue().toString());
}
s.addTable(1, t);
}
}
@Override @Override
public void reportGlobalError(CheckError err) { public void reportGlobalError(CheckError err) {
AtomicInteger errorCount = errorStatistics.computeIfAbsent(err.getErrorId(), k -> new AtomicInteger(0)); AtomicInteger errorCount = errorStatistics.computeIfAbsent(err.getErrorId(), k -> new AtomicInteger(0));
......
...@@ -115,7 +115,7 @@ public class RingSelfIntCheckBvhVariantCityGmlTest { ...@@ -115,7 +115,7 @@ public class RingSelfIntCheckBvhVariantCityGmlTest {
private RingSelfIntCheck createCheck(RingSelfIntCheck.Variant variant) { private RingSelfIntCheck createCheck(RingSelfIntCheck.Variant variant) {
RingSelfIntCheck check = new RingSelfIntCheck(variant); RingSelfIntCheck check = new RingSelfIntCheck(variant);
check.init(Collections.singletonMap("minVertexDistance", String.valueOf(EPSILON)), null); check.init(Collections.singletonMap(check.getCheckId(), Collections.singletonMap("minVertexDistance", String.valueOf(EPSILON))), null);
return check; return check;
} }
......
...@@ -63,7 +63,7 @@ public class RingSelfIntCheckBvhVariantSyntheticTest { ...@@ -63,7 +63,7 @@ public class RingSelfIntCheckBvhVariantSyntheticTest {
private static CheckSummary runCheck(LinearRing ring, RingSelfIntCheck.Variant variant) { private static CheckSummary runCheck(LinearRing ring, RingSelfIntCheck.Variant variant) {
RingSelfIntCheck check = new RingSelfIntCheck(variant); RingSelfIntCheck check = new RingSelfIntCheck(variant);
check.init(Collections.singletonMap("minVertexDistance", String.valueOf(EPSILON)), null); check.init(Collections.singletonMap(check.getCheckId(), Collections.singletonMap("minVertexDistance", String.valueOf(EPSILON))), null);
check.check(ring); check.check(ring);
CheckResult result = ring.getCheckResult(check); CheckResult result = ring.getCheckResult(check);
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>de.hft.stuttgart</groupId> <groupId>de.hft.stuttgart</groupId>
<artifactId>CityDoctorParent</artifactId> <artifactId>CityDoctorParent</artifactId>
<version>3.18.1</version> <version>3.18.2</version>
<relativePath>../../pom.xml</relativePath> <relativePath>../../pom.xml</relativePath>
</parent> </parent>
<artifactId>CityDoctorAutoPro</artifactId> <artifactId>CityDoctorAutoPro</artifactId>
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>de.hft.stuttgart</groupId> <groupId>de.hft.stuttgart</groupId>
<artifactId>CityDoctorParent</artifactId> <artifactId>CityDoctorParent</artifactId>
<version>3.18.1</version> <version>3.18.3</version>
<relativePath>../../pom.xml</relativePath> <relativePath>../../pom.xml</relativePath>
</parent> </parent>
<artifactId>CityDoctorGUI</artifactId> <artifactId>CityDoctorGUI</artifactId>
...@@ -59,6 +59,24 @@ ...@@ -59,6 +59,24 @@
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId> <artifactId>javafx-swing</artifactId>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/com.huskerdev/openglfx-jogl -->
<dependency>
<groupId>com.huskerdev</groupId>
<artifactId>openglfx-jogl</artifactId>
<version>4.1.19</version>
</dependency>
<!-- JOGL runtime + platform natives. openglfx-jogl does not declare JOGL itself;
the GUI now programs directly against com.jogamp.opengl, so depend on it explicitly.
Versions are managed in the parent pom. The -main artifacts bundle the natives. -->
<dependency>
<groupId>org.jogamp.jogl</groupId>
<artifactId>jogl-all-main</artifactId>
</dependency>
<dependency>
<groupId>org.jogamp.gluegen</groupId>
<artifactId>gluegen-rt-main</artifactId>
</dependency>
</dependencies> </dependencies>
<profiles> <profiles>
<profile> <profile>
......
package de.hft.stuttgart.citydoctor2.gui; package de.hft.stuttgart.citydoctor2.gui;
import java.awt.*;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
...@@ -10,6 +13,12 @@ import java.util.List; ...@@ -10,6 +13,12 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
...@@ -112,6 +121,18 @@ public class CheckDialog { ...@@ -112,6 +121,18 @@ public class CheckDialog {
@FXML @FXML
private Button selectBtn; private Button selectBtn;
@FXML
private Button selectAllGCBtn;
@FXML
private Button selectNoneGCBtn;
@FXML
private Button selectAllSCBtn;
@FXML
private Button selectNoneSCBtn;
@FXML @FXML
private TextField schematronField; private TextField schematronField;
...@@ -133,6 +154,12 @@ public class CheckDialog { ...@@ -133,6 +154,12 @@ public class CheckDialog {
@FXML @FXML
private Label schematronFileLabel; private Label schematronFileLabel;
@FXML
private Label checkDescriptionsLabel;
@FXML
private Hyperlink homepageHyperlink;
@FXML @FXML
private CheckBox streamCheckOption; private CheckBox streamCheckOption;
@FXML @FXML
...@@ -153,6 +180,7 @@ public class CheckDialog { ...@@ -153,6 +180,7 @@ public class CheckDialog {
private CityDoctorController controller; private CityDoctorController controller;
private FilterPane filterPane; private FilterPane filterPane;
private MainWindow window; private MainWindow window;
private IndeterminateSpinnerDialog waitingDialog;
public CheckDialog(MainWindow window, Window parent, CityDoctorController controller) throws IOException { public CheckDialog(MainWindow window, Window parent, CityDoctorController controller) throws IOException {
this.window = window; this.window = window;
...@@ -190,9 +218,15 @@ public class CheckDialog { ...@@ -190,9 +218,15 @@ public class CheckDialog {
geometricChecksLabel.setText(Localization.getText("CheckDialog.geometricChecksLabel")); geometricChecksLabel.setText(Localization.getText("CheckDialog.geometricChecksLabel"));
semanticChecksLabel.setText(Localization.getText("CheckDialog.semanticChecksLabel")); semanticChecksLabel.setText(Localization.getText("CheckDialog.semanticChecksLabel"));
schematronFileLabel.setText(Localization.getText("CheckDialog.schematronFileLabel")); schematronFileLabel.setText(Localization.getText("CheckDialog.schematronFileLabel"));
checkDescriptionsLabel.setText(Localization.getText("CheckDialog.checkDescriptionsLabel"));
homepageHyperlink.setText(Localization.getText("CheckDialog.clickHereToOpen"));
selectBtn.setText(Localization.getText("CheckDialog.selectBtn")); selectBtn.setText(Localization.getText("CheckDialog.selectBtn"));
checkBtn.setText(Localization.getText("CheckDialog.checkBtn")); checkBtn.setText(Localization.getText("CheckDialog.checkBtn"));
cancelBtn.setText(Localization.getText("CheckDialog.cancelBtn")); cancelBtn.setText(Localization.getText("CheckDialog.cancelBtn"));
selectAllGCBtn.setText(Localization.getText("CheckDialog.selectAllChecksBtn"));
selectNoneGCBtn.setText(Localization.getText("CheckDialog.unselectAllChecksBtn"));
selectAllSCBtn.setText(Localization.getText("CheckDialog.selectAllChecksBtn"));
selectNoneSCBtn.setText(Localization.getText("CheckDialog.unselectAllChecksBtn"));
List<TableColumn<GlobalParameter, ?>> columns = globalParametersTable.getColumns(); List<TableColumn<GlobalParameter, ?>> columns = globalParametersTable.getColumns();
TableColumn<GlobalParameter, String> nameCol = new TableColumn<>(NAME); TableColumn<GlobalParameter, String> nameCol = new TableColumn<>(NAME);
...@@ -258,7 +292,51 @@ public class CheckDialog { ...@@ -258,7 +292,51 @@ public class CheckDialog {
// failed to read config, ignore as default values are still available // failed to read config, ignore as default values are still available
} }
} }
}
selectAllGCBtn.setOnAction(ae ->
geometricTable.getRoot().getChildren().forEach(item -> item.getValue().setEnabled(true)));
selectNoneGCBtn.setOnAction(ae ->
geometricTable.getRoot().getChildren().forEach(item -> item.getValue().setEnabled(false)));
selectAllSCBtn.setOnAction(ae ->
semanticTable.getRoot().getChildren().forEach(item -> item.getValue().setEnabled(true)));
selectNoneSCBtn.setOnAction(ae ->
semanticTable.getRoot().getChildren().forEach(item -> item.getValue().setEnabled(false)));
try {
homepageHyperlink.setTooltip(new Tooltip(Localization.getText("CheckDialog.homepageHyperlink")));
final URI hpUrl = new URI(Localization.getText("CheckDialog.homepageHyperlink"));
homepageHyperlink.setOnAction(event -> {
try {
Desktop.getDesktop().browse(hpUrl);
} catch (IOException e) {
homepageHyperlink.setText("[Hyperlink blocked]");
Alert alert = new Alert(Alert.AlertType.ERROR,
"Opening of hyperlink was blocked by the system, URL has been copied to Clipboard",
ButtonType.OK);
Clipboard cb = Clipboard.getSystemClipboard();
ClipboardContent content = new ClipboardContent();
content.putString(hpUrl.toString());
cb.setContent(content);
alert.showAndWait();
}
});
} catch (URISyntaxException e) {
//Should never actually occur
homepageHyperlink.setText("[Hyperlink missing/invalid]");
}
try{
waitingDialog = new IndeterminateSpinnerDialog(window.getMainStage(),"Schematron still running, please wait");
} catch (IOException e) {
logger.error("Failed to load IndeterminateSpinnerDialog");
}
}
private void setupValueColumns(List<TableColumn<GlobalParameter, ?>> columns) { private void setupValueColumns(List<TableColumn<GlobalParameter, ?>> columns) {
TableColumn<GlobalParameter, String> valueCol = new TableColumn<>(VALUE); TableColumn<GlobalParameter, String> valueCol = new TableColumn<>(VALUE);
...@@ -462,7 +540,12 @@ public class CheckDialog { ...@@ -462,7 +540,12 @@ public class CheckDialog {
ProgressListener l = new ProgressListener() { ProgressListener l = new ProgressListener() {
@Override @Override
public void updateProgress(float state) { public void updateProgress(float state) {
Platform.runLater(() -> progress.setProgress(state)); Platform.runLater(() -> {
progress.setProgress(state);
if (state < 0 ){
waitingDialog.show();
}
});
} }
}; };
controller.startChecks(config, l); controller.startChecks(config, l);
...@@ -484,6 +567,7 @@ public class CheckDialog { ...@@ -484,6 +567,7 @@ public class CheckDialog {
Platform.runLater(() -> { Platform.runLater(() -> {
checkBtn.setDisable(false); checkBtn.setDisable(false);
cancelBtn.setDisable(false); cancelBtn.setDisable(false);
waitingDialog.hide();
stage.setOnCloseRequest(null); stage.setOnCloseRequest(null);
stage.close(); stage.close();
}); });
......
...@@ -130,7 +130,7 @@ public class CityDoctorController { ...@@ -130,7 +130,7 @@ public class CityDoctorController {
mainWindow.getOpenBtn().setDisable(true); mainWindow.getOpenBtn().setDisable(true);
mainWindow.getWriteReportButton().setDisable(true); mainWindow.getWriteReportButton().setDisable(true);
mainWindow.getMeshGroup().getChildren().clear(); mainWindow.clearMeshView();
clearTrees(); clearTrees();
mainWindow.resetSearchBar(); mainWindow.resetSearchBar();
...@@ -1379,7 +1379,9 @@ public class CityDoctorController { ...@@ -1379,7 +1379,9 @@ public class CityDoctorController {
if (model == null) { if (model == null) {
return; return;
} }
buildTrees(); mainWindow.rebuildErrorFilterTrees();
// buildTrees();
} }
public Series<String, Number> createErrorSeries() { public Series<String, Number> createErrorSeries() {
...@@ -1414,7 +1416,8 @@ public class CityDoctorController { ...@@ -1414,7 +1416,8 @@ public class CityDoctorController {
List<GmlId> idList = model.getBuildingIds(); List<GmlId> idList = model.getBuildingIds();
foundBuildings.removeIf(gmlId -> (!idList.contains(gmlId))); foundBuildings.removeIf(gmlId -> (!idList.contains(gmlId)));
Supplier<Stream<Building>> supply = () -> model.getCache().getStream(foundBuildings).map(co -> (Building) co); //Supplier<Stream<Building>> supply = () -> model.getCache().getStream(foundBuildings).map(co -> (Building) co);
Supplier<Stream<Building>> supply = () -> model.getCache().getAll(foundBuildings).stream().map(co -> (Building) co);
buildBuildingTreeFromList(supply, root); buildBuildingTreeFromList(supply, root);
updateTree(root); updateTree(root);
addMoreButtonForBuildings(buildingsView, root, buildingChunkNr, supply, foundBuildings.size()); addMoreButtonForBuildings(buildingsView, root, buildingChunkNr, supply, foundBuildings.size());
...@@ -1657,8 +1660,10 @@ public class CityDoctorController { ...@@ -1657,8 +1660,10 @@ public class CityDoctorController {
} }
public void errorFilterIndexChanged(Number newV) { public void errorFilterIndexChanged(Number newV) {
mainWindow.getMeshGroup().getChildren().clear(); mainWindow.clearMeshView();
mainWindow.unselectEverything(); mainWindow.unselectEverything();
mainWindow.resetSearchBar();
renderer.clearCurrentRender();
if (newV.intValue() == 0) { // Error filter was set to "All" if (newV.intValue() == 0) { // Error filter was set to "All"
buildTrees(); buildTrees();
updateFeatureTrees(); updateFeatureTrees();
......
...@@ -39,7 +39,7 @@ import de.hft.stuttgart.citydoctor2.check.error.SchematronError; ...@@ -39,7 +39,7 @@ import de.hft.stuttgart.citydoctor2.check.error.SchematronError;
import de.hft.stuttgart.citydoctor2.check.error.SolidError; import de.hft.stuttgart.citydoctor2.check.error.SolidError;
import de.hft.stuttgart.citydoctor2.check.error.SolidNotClosedError; import de.hft.stuttgart.citydoctor2.check.error.SolidNotClosedError;
import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError; import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError;
import de.hft.stuttgart.citydoctor2.check.error.SurfaceUnfragmentedError; import de.hft.stuttgart.citydoctor2.check.error.SurfaceFragmentedError;
import de.hft.stuttgart.citydoctor2.check.error.TooFewPolygonsError; import de.hft.stuttgart.citydoctor2.check.error.TooFewPolygonsError;
import de.hft.stuttgart.citydoctor2.check.error.UnknownCheckError; import de.hft.stuttgart.citydoctor2.check.error.UnknownCheckError;
import de.hft.stuttgart.citydoctor2.check.error.XMLValidationError; import de.hft.stuttgart.citydoctor2.check.error.XMLValidationError;
...@@ -186,7 +186,7 @@ public class GlobalErrorVisitor implements ErrorVisitor { ...@@ -186,7 +186,7 @@ public class GlobalErrorVisitor implements ErrorVisitor {
} }
@Override @Override
public void visit(SurfaceUnfragmentedError err) { public void visit(SurfaceFragmentedError err) {
} }
@Override @Override
......
package de.hft.stuttgart.citydoctor2.gui; package de.hft.stuttgart.citydoctor2.gui;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import de.hft.stuttgart.citydoctor2.datastructure.Edge; import de.hft.stuttgart.citydoctor2.datastructure.Edge;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon; import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex; import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType; 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.Triangle3d;
import de.hft.stuttgart.citydoctor2.math.Vector3d; 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.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 { public class HighlightController {
private final GLViewport viewport;
private double scale; private Supplier<double[]> centerSupplier;
private Group points;
private Group edges; private final FloatList points = new FloatList();
private final FloatList pointColors = new FloatList();
private Group highlights; private final FloatList lines = new FloatList();
private final FloatList lineColors = new FloatList();
public HighlightController(Group world) {
highlights = new Group(); public HighlightController(GLViewport viewport) {
points = new Group(); this.viewport = viewport;
edges = new Group(); }
highlights.getChildren().add(edges);
highlights.getChildren().add(points); public void setCenterSupplier(Supplier<double[]> centerSupplier) {
Platform.runLater(() -> world.getChildren().add(highlights)); this.centerSupplier = centerSupplier;
} }
public void clearHighlights() { private double[] center() {
points.getChildren().clear(); if (centerSupplier == null) {
edges.getChildren().clear(); return new double[3];
} }
double[] c = centerSupplier.get();
public void changeScaling(double translateZ) { return c == null ? new double[3] : c;
scale = Math.abs(translateZ); }
scale = Math.min(150, scale);
scale = Math.max(10, scale); private void resetBuffers() {
scale = scale * 0.01; points.clear();
for (Node n : points.getChildren()) { pointColors.clear();
n.setScaleX(scale); lines.clear();
n.setScaleY(scale); lineColors.clear();
n.setScaleZ(scale); }
}
double edgeSize = scale / 10; public void clearHighlights() {
for (Node n : edges.getChildren()) { resetBuffers();
if (n instanceof Cylinder cy) { push();
cy.setRadius(edgeSize); }
}
} private void push() {
} viewport.setHighlights(points.toArray(), pointColors.toArray(), lines.toArray(), lineColors.toArray());
}
public void highlight(Polygon p, TriangulatedGeometry currentTriGeom) {
clearHighlights(); public void changeScaling(double translateZ) {
addHighlight(p, currentTriGeom, Color.RED, Color.BLUE); // 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.
}
private void addHighlight(Polygon p, TriangulatedGeometry currentTriGeom, Color extColor, Color intColor) {
if (currentTriGeom == null || p == null) { // ---- point/line accumulation ----
return;
} private void addPoint(Vertex v, Color color) {
addHighlight(p.getExteriorRing(), currentTriGeom, extColor); double[] c = center();
for (LinearRing intRing : p.getInnerRings()) { points.add((float) (v.getX() - c[0]), (float) (v.getY() - c[1]), (float) (v.getZ() - c[2]));
addHighlight(intRing, currentTriGeom, intColor); pointColors.add((float) color.getRed(), (float) color.getGreen(), (float) color.getBlue());
} }
}
private void addLine(Vector3d v1, Vector3d v2, Color color) {
public void highlight(LinearRing ring, TriangulatedGeometry currentTriGeom) { double[] c = center();
clearHighlights(); lines.add((float) (v1.getX() - c[0]), (float) (v1.getY() - c[1]), (float) (v1.getZ() - c[2]));
if (ring.getType() == LinearRingType.EXTERIOR) { lines.add((float) (v2.getX() - c[0]), (float) (v2.getY() - c[1]), (float) (v2.getZ() - c[2]));
addHighlight(ring, currentTriGeom, Color.RED); float r = (float) color.getRed();
} else { float g = (float) color.getGreen();
addHighlight(ring, currentTriGeom, Color.BLUE); float b = (float) color.getBlue();
} lineColors.add(r, g, b);
} lineColors.add(r, g, b);
}
public void highlight(List<LinearRing> rings, TriangulatedGeometry currentTriGeom) {
clearHighlights(); // ---- public highlight API ----
for (LinearRing lr : rings) {
if (lr.getType() == LinearRingType.EXTERIOR) { public void highlight(Polygon p) {
addHighlight(lr, currentTriGeom, Color.RED); resetBuffers();
} else { addHighlight(p, Color.RED, Color.BLUE);
addHighlight(lr, currentTriGeom, Color.BLUE); push();
} }
}
} private void addHighlight(Polygon p, Color extColor, Color intColor) {
if (p == null) {
private void addHighlight(LinearRing ring, TriangulatedGeometry currentTriGeom, Color pointColor) { return;
if (currentTriGeom == null || ring == null) { }
return; addHighlight(p.getExteriorRing(), extColor);
} for (LinearRing intRing : p.getInnerRings()) {
Vector3d movedBy = currentTriGeom.getMovedBy(); addHighlight(intRing, intColor);
for (Vertex v : ring.getVertices()) { }
highlightPoint(movedBy, v, pointColor); }
}
for (int i = 0; i < ring.getVertices().size() - 1; i++) { public void highlight(LinearRing ring) {
Vertex v1 = ring.getVertices().get(i); resetBuffers();
Vertex v2 = ring.getVertices().get(i + 1); if (ring.getType() == LinearRingType.EXTERIOR) {
highlightEdge(v1, v2, movedBy); addHighlight(ring, Color.RED);
} } else {
} addHighlight(ring, Color.BLUE);
}
public void highlight(Edge e, TriangulatedGeometry currentTriGeom) { push();
clearHighlights(); }
addHighlight(e, currentTriGeom);
} public void highlight(List<LinearRing> rings) {
resetBuffers();
private void addHighlight(Edge e, TriangulatedGeometry currentTriGeom) { for (LinearRing lr : rings) {
if (currentTriGeom == null || e == null) { if (lr.getType() == LinearRingType.EXTERIOR) {
return; addHighlight(lr, Color.RED);
} } else {
Vector3d movedBy = currentTriGeom.getMovedBy(); addHighlight(lr, Color.BLUE);
highlightEdge(e.getFrom(), e.getTo(), movedBy); }
highlightPoint(movedBy, e.getFrom(), Color.RED); }
highlightPoint(movedBy, e.getTo(), Color.RED); push();
} }
public void highlight(Vertex v, TriangulatedGeometry currentTriGeom) { private void addHighlight(LinearRing ring, Color pointColor) {
clearHighlights(); if (ring == null) {
addHighlight(v, currentTriGeom, Color.RED); return;
} }
for (Vertex v : ring.getVertices()) {
public void addHighlight(Vertex v, TriangulatedGeometry currentTriGeom, Color c) { addPoint(v, pointColor);
if (currentTriGeom == null || v == null) { }
return; for (int i = 0; i < ring.getVertices().size() - 1; i++) {
} Vertex v1 = ring.getVertices().get(i);
Vector3d movedBy = currentTriGeom.getMovedBy(); Vertex v2 = ring.getVertices().get(i + 1);
highlightPoint(movedBy, v, c); addLine(v1, v2, Color.ORANGE);
} }
}
private void highlightPoint(Vector3d movedBy, Vertex v, Color color) {
Sphere sp = new Sphere(0.5); public void highlight(Edge e) {
sp.setMaterial(new PhongMaterial(color)); resetBuffers();
sp.setTranslateX(v.getX() - movedBy.getX()); addHighlight(e);
sp.setTranslateY(v.getY() - movedBy.getY()); push();
sp.setTranslateZ(v.getZ() - movedBy.getZ()); }
sp.setScaleX(scale);
sp.setScaleY(scale); private void addHighlight(Edge e) {
sp.setScaleZ(scale); if (e == null) {
return;
sp.setUserData(new VertexClickDispatcher(v)); }
addLine(e.getFrom(), e.getTo(), Color.ORANGE);
points.getChildren().add(sp); addPoint(e.getFrom(), Color.RED);
} addPoint(e.getTo(), Color.RED);
}
private void highlightEdge(Vector3d v1, Vector3d v2, Vector3d movedBy) {
Point3D origin = new Point3D(v1.getX() - movedBy.getX(), v1.getY() - movedBy.getY(), public void highlight(Vertex v) {
v1.getZ() - movedBy.getZ()); resetBuffers();
Point3D target = new Point3D(v2.getX() - movedBy.getX(), v2.getY() - movedBy.getY(), addPoint(v, Color.RED);
v2.getZ() - movedBy.getZ()); push();
}
Point3D yAxis = new Point3D(0, 1, 0);
Point3D diff = target.subtract(origin); public void addHighlight(Vertex v, Color c) {
double height = diff.magnitude(); if (v == null) {
return;
Point3D mid = target.midpoint(origin); }
Translate moveToMidpoint = new Translate(mid.getX(), mid.getY(), mid.getZ()); addPoint(v, c);
push();
Point3D axisOfRotation = diff.crossProduct(yAxis); }
double angle = Math.acos(diff.normalize().dotProduct(yAxis));
Rotate rotateAroundCenter = new Rotate(-Math.toDegrees(angle), axisOfRotation); public void highlightEdges(List<Edge> errorEdges) {
resetBuffers();
Cylinder cy = new Cylinder(scale / 10, height); for (Edge e : errorEdges) {
cy.setMaterial(new PhongMaterial(Color.ORANGE)); addHighlight(e);
cy.getTransforms().addAll(moveToMidpoint, rotateAroundCenter); }
edges.getChildren().add(cy); push();
} }
public void highlightEdges(List<Edge> errorEdges, TriangulatedGeometry currentTriGeom) { public void highlightPolygons(List<List<Polygon>> components) {
clearHighlights(); resetBuffers();
for (Edge e : errorEdges) { for (int i = 0; i < components.size(); i++) {
addHighlight(e, currentTriGeom); Color extColor;
} Color intColor = switch (i % 3) {
}
public void highlightPolygons(List<List<Polygon>> components, TriangulatedGeometry currentTriGeom) {
clearHighlights();
for (int i = 0; i < components.size(); i++) {
Color extColor;
Color intColor = switch (i % 3) {
case 1 -> { case 1 -> {
extColor = Color.GREEN; extColor = Color.GREEN;
yield Color.YELLOW; yield Color.YELLOW;
...@@ -202,21 +193,24 @@ public class HighlightController { ...@@ -202,21 +193,24 @@ public class HighlightController {
yield Color.BLUE; yield Color.BLUE;
} }
}; };
// select some color pairs for exterior and inner rings // select some color pairs for exterior and inner rings
List<Polygon> component = components.get(i); List<Polygon> component = components.get(i);
for (Polygon p : component) { for (Polygon p : component) {
addHighlight(p, currentTriGeom, extColor, intColor); addHighlight(p, extColor, intColor);
} }
} }
} push();
}
public void addHighlight(Polygon p, TriangulatedGeometry currentTriGeom) {
addHighlight(p, currentTriGeom, Color.RED, Color.BLUE); public void addHighlight(Polygon p) {
} addHighlight(p, Color.RED, Color.BLUE);
push();
public void highlight(Triangle3d t, TriangulatedGeometry currentTriGeom) { }
highlightEdge(t.getP1(), t.getP2(), currentTriGeom.getMovedBy());
highlightEdge(t.getP2(), t.getP3(), currentTriGeom.getMovedBy()); public void highlight(Triangle3d t) {
highlightEdge(t.getP3(), t.getP1(), currentTriGeom.getMovedBy()); addLine(t.getP1(), t.getP2(), Color.ORANGE);
} addLine(t.getP2(), t.getP3(), Color.ORANGE);
addLine(t.getP3(), t.getP1(), Color.ORANGE);
push();
}
} }
package de.hft.stuttgart.citydoctor2.gui;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;
import java.io.IOException;
public class IndeterminateSpinnerDialog {
private Stage stage;
@FXML
private Label messageLbl;
public IndeterminateSpinnerDialog(Window parent, String message) throws IOException {
FXMLLoader loader = new FXMLLoader(LoadingInfoDialog.class.getResource("IndeterminateSpinnerDialog.fxml"));
loader.setController(this);
VBox box = loader.load();
stage = new Stage(StageStyle.UNDECORATED);
Scene scene = new Scene(box);
scene.setFill(null);
stage.setScene(scene);
stage.initOwner(parent);
stage.initModality(Modality.APPLICATION_MODAL);
messageLbl.setText(message);
}
public void changeMessage(String message){
messageLbl.setText(message);
}
public void show() {
stage.show();
}
public void hide() {
stage.hide();
}
}
...@@ -41,7 +41,7 @@ import de.hft.stuttgart.citydoctor2.check.error.SchematronError; ...@@ -41,7 +41,7 @@ import de.hft.stuttgart.citydoctor2.check.error.SchematronError;
import de.hft.stuttgart.citydoctor2.check.error.SolidError; import de.hft.stuttgart.citydoctor2.check.error.SolidError;
import de.hft.stuttgart.citydoctor2.check.error.SolidNotClosedError; import de.hft.stuttgart.citydoctor2.check.error.SolidNotClosedError;
import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError; import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError;
import de.hft.stuttgart.citydoctor2.check.error.SurfaceUnfragmentedError; import de.hft.stuttgart.citydoctor2.check.error.SurfaceFragmentedError;
import de.hft.stuttgart.citydoctor2.check.error.DegeneratedRingError; import de.hft.stuttgart.citydoctor2.check.error.DegeneratedRingError;
import de.hft.stuttgart.citydoctor2.check.error.TooFewPolygonsError; import de.hft.stuttgart.citydoctor2.check.error.TooFewPolygonsError;
import de.hft.stuttgart.citydoctor2.check.error.UnknownCheckError; import de.hft.stuttgart.citydoctor2.check.error.UnknownCheckError;
...@@ -55,32 +55,27 @@ import javafx.scene.paint.Color; ...@@ -55,32 +55,27 @@ import javafx.scene.paint.Color;
public class ListErrorVisitor implements ErrorVisitor { public class ListErrorVisitor implements ErrorVisitor {
private HighlightController controller; private HighlightController controller;
private TriangulatedGeometry geom;
public ListErrorVisitor(HighlightController controller) { public ListErrorVisitor(HighlightController controller) {
this.controller = controller; this.controller = controller;
} }
public void setGeometry(TriangulatedGeometry geom) {
this.geom = geom;
}
@Override @Override
public void visit(PolygonHoleOutsideError err) { public void visit(PolygonHoleOutsideError err) {
List<LinearRing> highlightedRings = new ArrayList<>(); List<LinearRing> highlightedRings = new ArrayList<>();
highlightedRings.add(err.getPolygon().getExteriorRing()); highlightedRings.add(err.getPolygon().getExteriorRing());
highlightedRings.addAll(err.getHolesOutside()); highlightedRings.addAll(err.getHolesOutside());
controller.highlight(highlightedRings, geom); controller.highlight(highlightedRings);
} }
@Override @Override
public void visit(NonManifoldEdgeError err) { public void visit(NonManifoldEdgeError err) {
controller.highlightEdges(err.getEdges(), geom); controller.highlightEdges(err.getEdges());
} }
@Override @Override
public void visit(MultipleConnectedComponentsError err) { public void visit(MultipleConnectedComponentsError err) {
controller.highlightPolygons(err.getComponents(), geom); controller.highlightPolygons(err.getComponents());
} }
@Override @Override
...@@ -88,18 +83,18 @@ public class ListErrorVisitor implements ErrorVisitor { ...@@ -88,18 +83,18 @@ public class ListErrorVisitor implements ErrorVisitor {
List<LinearRing> highlightedRings = new ArrayList<>(); List<LinearRing> highlightedRings = new ArrayList<>();
highlightedRings.add(err.getPolygon().getExteriorRing()); highlightedRings.add(err.getPolygon().getExteriorRing());
highlightedRings.add(err.getInnerRing()); highlightedRings.add(err.getInnerRing());
controller.highlight(highlightedRings, geom); controller.highlight(highlightedRings);
} }
@Override @Override
public void visit(NonManifoldVertexError err) { public void visit(NonManifoldVertexError err) {
controller.highlightPolygons(err.getComponents(), geom); controller.highlightPolygons(err.getComponents());
controller.addHighlight(err.getVertex(), geom, Color.BLUEVIOLET); controller.addHighlight(err.getVertex(), Color.BLUEVIOLET);
} }
@Override @Override
public void visit(PolygonWrongOrientationError err) { public void visit(PolygonWrongOrientationError err) {
controller.highlightEdges(err.getEdges(), geom); controller.highlightEdges(err.getEdges());
} }
@Override @Override
...@@ -107,12 +102,12 @@ public class ListErrorVisitor implements ErrorVisitor { ...@@ -107,12 +102,12 @@ public class ListErrorVisitor implements ErrorVisitor {
List<LinearRing> highlightedRings = new ArrayList<>(); List<LinearRing> highlightedRings = new ArrayList<>();
highlightedRings.add(err.getPolygon().getExteriorRing()); highlightedRings.add(err.getPolygon().getExteriorRing());
highlightedRings.add(err.getInnerRing()); highlightedRings.add(err.getInnerRing());
controller.highlight(highlightedRings, geom); controller.highlight(highlightedRings);
} }
@Override @Override
public void visit(SolidNotClosedError err) { public void visit(SolidNotClosedError err) {
controller.highlightEdges(err.getErrorEdges(), geom); controller.highlightEdges(err.getErrorEdges());
} }
@Override @Override
...@@ -127,14 +122,14 @@ public class ListErrorVisitor implements ErrorVisitor { ...@@ -127,14 +122,14 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override @Override
public void visit(RingNotClosedError err) { public void visit(RingNotClosedError err) {
controller.highlight(err.getRing(), geom); controller.highlight(err.getRing());
} }
@Override @Override
public void visit(ConsecutivePointSameError err) { public void visit(ConsecutivePointSameError err) {
controller.highlight(err.getRing(), geom); controller.highlight(err.getRing());
controller.addHighlight(err.getVertex1(), geom, Color.BLACK); controller.addHighlight(err.getVertex1(), Color.BLACK);
controller.addHighlight(err.getVertex2(), geom, Color.BLACK); controller.addHighlight(err.getVertex2(), Color.BLACK);
} }
@Override @Override
...@@ -144,7 +139,7 @@ public class ListErrorVisitor implements ErrorVisitor { ...@@ -144,7 +139,7 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override @Override
public void visit(PolygonInteriorDisconnectedError err) { public void visit(PolygonInteriorDisconnectedError err) {
controller.highlight(err.getConnectedRings(), geom); controller.highlight(err.getConnectedRings());
} }
@Override @Override
...@@ -154,18 +149,18 @@ public class ListErrorVisitor implements ErrorVisitor { ...@@ -154,18 +149,18 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override @Override
public void visit(RingTooFewPointsError err) { public void visit(RingTooFewPointsError err) {
controller.highlight(err.getRing(), geom); controller.highlight(err.getRing());
} }
@Override @Override
public void visit(NonPlanarPolygonNormalsDeviation err) { public void visit(NonPlanarPolygonNormalsDeviation err) {
controller.highlight(err.getPolygon(), geom); controller.highlight(err.getPolygon());
} }
@Override @Override
public void visit(NonPlanarPolygonDistancePlaneError err) { public void visit(NonPlanarPolygonDistancePlaneError err) {
controller.highlight(err.getPolygon(), geom); controller.highlight(err.getPolygon());
controller.addHighlight(err.getVertex(), geom, Color.BLACK); controller.addHighlight(err.getVertex(), Color.BLACK);
} }
@Override @Override
...@@ -173,15 +168,15 @@ public class ListErrorVisitor implements ErrorVisitor { ...@@ -173,15 +168,15 @@ public class ListErrorVisitor implements ErrorVisitor {
List<LinearRing> rings = new ArrayList<>(); List<LinearRing> rings = new ArrayList<>();
rings.add(err.getIntersectingRings().getValue0()); rings.add(err.getIntersectingRings().getValue0());
rings.add(err.getIntersectingRings().getValue1()); rings.add(err.getIntersectingRings().getValue1());
controller.highlight(rings, geom); controller.highlight(rings);
} }
@Override @Override
public void visit(SolidSelfIntError err) { public void visit(SolidSelfIntError err) {
controller.clearHighlights(); controller.clearHighlights();
for (PolygonIntersection intersection : err.getIntersections()) { for (PolygonIntersection intersection : err.getIntersections()) {
controller.addHighlight(intersection.getP1(), geom); controller.addHighlight(intersection.getP1());
controller.addHighlight(intersection.getP2(), geom); controller.addHighlight(intersection.getP2());
} }
} }
...@@ -192,9 +187,9 @@ public class ListErrorVisitor implements ErrorVisitor { ...@@ -192,9 +187,9 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override @Override
public void visit(RingDuplicatePointError err) { public void visit(RingDuplicatePointError err) {
controller.highlight(err.getRing(), geom); controller.highlight(err.getRing());
controller.addHighlight(err.getVertex1(), geom, Color.BLACK); controller.addHighlight(err.getVertex1(), Color.BLACK);
controller.addHighlight(err.getVertex2(), geom, Color.BLACK); controller.addHighlight(err.getVertex2(), Color.BLACK);
} }
@Override @Override
...@@ -202,14 +197,14 @@ public class ListErrorVisitor implements ErrorVisitor { ...@@ -202,14 +197,14 @@ public class ListErrorVisitor implements ErrorVisitor {
List<Edge> list = new ArrayList<>(); List<Edge> list = new ArrayList<>();
list.add(err.getEdge1()); list.add(err.getEdge1());
list.add(err.getEdge2()); list.add(err.getEdge2());
controller.highlightEdges(list, geom); controller.highlightEdges(list);
controller.addHighlight(new Vertex(err.getIntersection()), geom, Color.BLACK); controller.addHighlight(new Vertex(err.getIntersection()), Color.BLACK);
} }
@Override @Override
public void visit(PointTouchesEdgeError err) { public void visit(PointTouchesEdgeError err) {
controller.highlight(err.getEdge(), geom); controller.highlight(err.getEdge());
controller.addHighlight(err.getVertex(), geom, Color.BLACK); controller.addHighlight(err.getVertex(), Color.BLACK);
} }
@Override @Override
...@@ -219,22 +214,22 @@ public class ListErrorVisitor implements ErrorVisitor { ...@@ -219,22 +214,22 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override @Override
public void visit(NotCeilingError err) { public void visit(NotCeilingError err) {
controller.highlight(err.getPolygon(), geom); controller.highlight(err.getPolygon());
} }
@Override @Override
public void visit(NotFloorError err) { public void visit(NotFloorError err) {
controller.highlight(err.getPolygon(), geom); controller.highlight(err.getPolygon());
} }
@Override @Override
public void visit(NotWallError err) { public void visit(NotWallError err) {
controller.highlight(err.getPolygon(), geom); controller.highlight(err.getPolygon());
} }
@Override @Override
public void visit(NotGroundError err) { public void visit(NotGroundError err) {
controller.highlight(err.getPolygon(), geom); controller.highlight(err.getPolygon());
} }
@Override @Override
...@@ -243,13 +238,13 @@ public class ListErrorVisitor implements ErrorVisitor { ...@@ -243,13 +238,13 @@ public class ListErrorVisitor implements ErrorVisitor {
} }
@Override @Override
public void visit(SurfaceUnfragmentedError err) { public void visit(SurfaceFragmentedError err) {
// nothing to display // nothing to display
} }
@Override @Override
public void visit(DegeneratedRingError err) { public void visit(DegeneratedRingError err) {
controller.highlight(err.getRing(), geom); controller.highlight(err.getRing());
} }
@Override @Override
...@@ -269,7 +264,7 @@ public class ListErrorVisitor implements ErrorVisitor { ...@@ -269,7 +264,7 @@ public class ListErrorVisitor implements ErrorVisitor {
@Override @Override
public void visit(PolygonWithoutSurfaceError err) { public void visit(PolygonWithoutSurfaceError err) {
controller.highlight(err.getPolygon(), geom); controller.highlight(err.getPolygon());
} }
@Override @Override
......
...@@ -31,9 +31,11 @@ import javafx.scene.layout.BorderPane; ...@@ -31,9 +31,11 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage; 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.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
...@@ -49,10 +51,6 @@ public class MainWindow extends Application { ...@@ -49,10 +51,6 @@ public class MainWindow extends Application {
private static final Logger logger = LogManager.getLogger(MainWindow.class); 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 @FXML
private TreeView<Renderable> buildingsView; private TreeView<Renderable> buildingsView;
...@@ -214,19 +212,8 @@ public class MainWindow extends Application { ...@@ -214,19 +212,8 @@ public class MainWindow extends Application {
private Image north; private Image north;
private Image northFlip; private Image northFlip;
private Group meshGroup; private GLViewport glViewport;
private Group world;
private PerspectiveCamera camera;
private Rotate cameraXRotation = new Rotate();
private Rotate cameraZRotation = new Rotate();
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]; private double[] clickStart = new double[2];
@FXML @FXML
...@@ -245,7 +232,6 @@ public class MainWindow extends Application { ...@@ -245,7 +232,6 @@ public class MainWindow extends Application {
private FeatureType selectedTab = FeatureType.BUILDING; private FeatureType selectedTab = FeatureType.BUILDING;
private MainToolBar mainToolBar; private MainToolBar mainToolBar;
private SubScene geomScene;
private ImageView northArrow; private ImageView northArrow;
private static boolean loadFileAtStartup = false; private static boolean loadFileAtStartup = false;
...@@ -259,6 +245,11 @@ public class MainWindow extends Application { ...@@ -259,6 +245,11 @@ public class MainWindow extends Application {
private ChangeListener<Number> filterChangeListener; private ChangeListener<Number> filterChangeListener;
public static void main(String[] args) { 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(); setLocaleFromSettings();
ArgumentParser argParser = new ArgumentParser(args); ArgumentParser argParser = new ArgumentParser(args);
inputFile = CityDoctorValidation.getInputFile(argParser, true); inputFile = CityDoctorValidation.getInputFile(argParser, true);
...@@ -278,6 +269,8 @@ public class MainWindow extends Application { ...@@ -278,6 +269,8 @@ public class MainWindow extends Application {
System.exit(4); System.exit(4);
} }
} }
CityDoctorValidation.lookForDbConfigFileParameter(argParser);
CityDoctorValidation.lookForDbLocationParameter(argParser);
Application.launch(args); Application.launch(args);
} }
...@@ -300,7 +293,7 @@ public class MainWindow extends Application { ...@@ -300,7 +293,7 @@ public class MainWindow extends Application {
FXMLLoader loader = new FXMLLoader(MainWindow.class.getResource("MainWindow.fxml")); FXMLLoader loader = new FXMLLoader(MainWindow.class.getResource("MainWindow.fxml"));
loader.setController(this); loader.setController(this);
BorderPane bp = loader.load(); BorderPane bp = loader.load();
highlightController = new HighlightController(world); highlightController = new HighlightController(glViewport);
renderer = new Renderer(this, highlightController); renderer = new Renderer(this, highlightController);
clickHandler = new VertexClickHandler(errorView, renderer, stage, this); clickHandler = new VertexClickHandler(errorView, renderer, stage, this);
controller = new CityDoctorController(this, highlightController, renderer); controller = new CityDoctorController(this, highlightController, renderer);
...@@ -592,6 +585,10 @@ public class MainWindow extends Application { ...@@ -592,6 +585,10 @@ public class MainWindow extends Application {
showCityObjectsCombo.getSelectionModel().selectedIndexProperty().addListener(filterChangeListener); showCityObjectsCombo.getSelectionModel().selectedIndexProperty().addListener(filterChangeListener);
} }
public void rebuildErrorFilterTrees(){
controller.errorFilterIndexChanged(showCityObjectsCombo.getSelectionModel().getSelectedIndex());
}
private ChangeListener<Number> setupFilterSelectionListener() { private ChangeListener<Number> setupFilterSelectionListener() {
return (obs, oldV, newV) -> controller.errorFilterIndexChanged(newV); return (obs, oldV, newV) -> controller.errorFilterIndexChanged(newV);
} }
...@@ -814,8 +811,11 @@ public class MainWindow extends Application { ...@@ -814,8 +811,11 @@ public class MainWindow extends Application {
MenuItem deleteOption = new MenuItem(Localization.getText("MainWindow.delete")); MenuItem deleteOption = new MenuItem(Localization.getText("MainWindow.delete"));
deleteOption.setOnAction(ae -> controller.delete(featureView.getSelectionModel().getSelectedItem())); deleteOption.setOnAction(ae -> controller.delete(featureView.getSelectionModel().getSelectedItem()));
menu.getItems().add(deleteOption); menu.getItems().add(deleteOption);
featureView.setContextMenu(menu);
*/ */
featureView.setContextMenu(menu);
} }
public void fetchCopyAction(Renderable node) { public void fetchCopyAction(Renderable node) {
...@@ -836,36 +836,31 @@ public class MainWindow extends Application { ...@@ -836,36 +836,31 @@ public class MainWindow extends Application {
} }
private void setup3dView() { private void setup3dView() {
Group root = new Group(); glViewport = new GLViewport();
geomScene = new SubScene(root, 500, 300, true, SceneAntialiasing.BALANCED); Region canvas = glViewport.getNode();
geomScene.heightProperty().bind(meshView.heightProperty()); canvas.prefWidthProperty().bind(meshView.widthProperty());
geomScene.widthProperty().bind(meshView.widthProperty()); canvas.prefHeightProperty().bind(meshView.heightProperty());
geomScene.setFill(Color.AZURE); meshView.getChildren().add(canvas);
meshView.getChildren().add(geomScene);
canvas.addEventFilter(MouseEvent.MOUSE_PRESSED, me -> {
geomScene.addEventFilter(MouseEvent.MOUSE_PRESSED, me -> {
clickStart[0] = me.getScreenX(); clickStart[0] = me.getScreenX();
clickStart[1] = me.getScreenY(); 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) { if (Math.abs(clickStart[0] - me.getScreenX()) > 3 || Math.abs(clickStart[1] - me.getScreenY()) > 3) {
// skip when mouse moved too much // skip when mouse moved too much
return; return;
} }
Node node = me.getPickResult().getIntersectedNode(); glViewport.requestPick(me.getX(), me.getY(), target -> {
if (node != null) { if (target instanceof Polygon p) {
Object o = node.getUserData(); new PolygonClickDispatcher(p).click(me, clickHandler);
if (o instanceof ClickDispatcher cd) {
cd.click(me, clickHandler);
} }
} });
}); });
world = new Group(); // keep the north arrow aligned with the camera after orbit drags
root.getChildren().add(world); canvas.addEventFilter(MouseEvent.MOUSE_DRAGGED, me -> alignNorthArrow());
meshGroup = new Group();
world.getChildren().add(meshGroup);
north = new Image(getClass().getResourceAsStream("icons/north.png"), 50, 50, true, true); 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); northFlip = new Image(getClass().getResourceAsStream("icons/north_flip.png"), 50, 50, true, true);
...@@ -876,81 +871,13 @@ public class MainWindow extends Application { ...@@ -876,81 +871,13 @@ public class MainWindow extends Application {
northArrow.setY(10); northArrow.setY(10);
meshView.getChildren().add(northArrow); meshView.getChildren().add(northArrow);
northArrow.setVisible(false); 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() { public void alignNorthArrow() {
double absRotZ = cameraZRotation.getAngle() - 360 * Math.floor(cameraZRotation.getAngle() / 360); double azimuth = glViewport.camera().azimuthDeg();
double absRotX = cameraXRotation.getAngle() - 360 * Math.floor(cameraXRotation.getAngle() / 360); double elevation = glViewport.camera().elevationDeg();
double absRotZ = azimuth - 360 * Math.floor(azimuth / 360);
double absRotX = elevation - 360 * Math.floor(elevation / 360);
double northRot; double northRot;
if (80 <= absRotX && absRotX <= 260) { if (80 <= absRotX && absRotX <= 260) {
...@@ -969,16 +896,11 @@ public class MainWindow extends Application { ...@@ -969,16 +896,11 @@ public class MainWindow extends Application {
} }
public void resetCamera() { public void resetCamera() {
cameraXRot = CAMERA_INITIAL_X_ANGLE; glViewport.resetCamera();
cameraYRot = CAMERA_INITIAL_Y_ANGLE;
cameraZRotation.setAngle(cameraXRot);
cameraXRotation.setAngle(cameraYRot);
alignNorthArrow();
camera.setTranslateX(0);
camera.setTranslateY(0);
if (controller.getOriginBB() != null) { if (controller.getOriginBB() != null) {
zoomOutForBoundingBox(controller.getOriginBB()); zoomOutForBoundingBox(controller.getOriginBB());
} }
alignNorthArrow();
} }
public void addFileNameToTitle(String fileName) { public void addFileNameToTitle(String fileName) {
...@@ -1046,8 +968,14 @@ public class MainWindow extends Application { ...@@ -1046,8 +968,14 @@ public class MainWindow extends Application {
return mainToolBar.getGridButton(); return mainToolBar.getGridButton();
} }
public Group getMeshGroup() { public GLViewport getGlViewport() {
return meshGroup; 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() { public ToggleButton getCullingButton() {
...@@ -1128,20 +1056,16 @@ public class MainWindow extends Application { ...@@ -1128,20 +1056,16 @@ public class MainWindow extends Application {
} }
public void takeViewScreenshot() throws IOException { public void takeViewScreenshot() throws IOException {
WritableImage snapshot = geomScene.snapshot(null, null); WritableImage snapshot = glViewport.getNode().snapshot(null, null);
File outputFile = new File("img.png"); File outputFile = new File("img.png");
BufferedImage bImage = SwingFXUtils.fromFXImage(snapshot, null); BufferedImage bImage = SwingFXUtils.fromFXImage(snapshot, null);
ImageIO.write(bImage, "png", outputFile); ImageIO.write(bImage, "png", outputFile);
} }
public void zoomOutForBoundingBox(BoundingBox b) { public void zoomOutForBoundingBox(BoundingBox b) {
double longestSide = b.getDiagonalLength() * 0.4; glViewport.camera().zoomOutForBoundingBox(b.getDiagonalLength());
double d = longestSide / Math.tan(Math.toRadians(30) / 2); glViewport.getNode().repaint();
translateZ = -d; highlightController.changeScaling(glViewport.camera().distance());
camera.setTranslateZ(translateZ);
camera.setTranslateX(0);
camera.setTranslateY(0);
highlightController.changeScaling(-translateZ);
controller.setOriginBB(b); controller.setOriginBB(b);
} }
......
...@@ -4,7 +4,6 @@ import de.hft.stuttgart.citydoctor2.check.CheckError; ...@@ -4,7 +4,6 @@ import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.Checkable; import de.hft.stuttgart.citydoctor2.check.Checkable;
import de.hft.stuttgart.citydoctor2.utils.visitors.CityObjectCollector; import de.hft.stuttgart.citydoctor2.utils.visitors.CityObjectCollector;
import de.hft.stuttgart.citydoctor2.datastructure.*; 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.*;
import de.hft.stuttgart.citydoctor2.gui.tree.node.EdgeNode; import de.hft.stuttgart.citydoctor2.gui.tree.node.EdgeNode;
import de.hft.stuttgart.citydoctor2.gui.tree.node.ErrorNode; import de.hft.stuttgart.citydoctor2.gui.tree.node.ErrorNode;
...@@ -12,12 +11,15 @@ import de.hft.stuttgart.citydoctor2.gui.tree.node.GenericAttributeNode; ...@@ -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.LinearRingNode;
import de.hft.stuttgart.citydoctor2.gui.tree.node.PolygonNode; import de.hft.stuttgart.citydoctor2.gui.tree.node.PolygonNode;
import de.hft.stuttgart.citydoctor2.gui.tree.node.VertexNode; 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 de.hft.stuttgart.citydoctor2.math.Triangle3d;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.scene.control.TreeItem; import javafx.scene.control.TreeItem;
import javafx.scene.paint.Color; 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.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
...@@ -28,11 +30,15 @@ public class Renderer { ...@@ -28,11 +30,15 @@ public class Renderer {
private static final Logger logger = LogManager.getLogger(Renderer.class); private static final Logger logger = LogManager.getLogger(Renderer.class);
private TriangulatedGeometry currentTriGeom; private SceneData currentScene;
private Geometry currentGeometry; private Geometry currentGeometry;
private CullFace currentCulling = CullFace.NONE; private boolean culling = false;
private DrawMode currentDrawMode = DrawMode.FILL; 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 MainWindow mainWindow;
private HighlightController highlightController; private HighlightController highlightController;
...@@ -40,8 +46,6 @@ public class Renderer { ...@@ -40,8 +46,6 @@ public class Renderer {
private LoadingInfoDialog loadingDialog; private LoadingInfoDialog loadingDialog;
private List<ViewFilter> lodFilters = new ArrayList<>();
private ViewFilter roofFilter;
private Runnable refresher; private Runnable refresher;
private Runnable errorUpdater; private Runnable errorUpdater;
...@@ -50,138 +54,74 @@ public class Renderer { ...@@ -50,138 +54,74 @@ public class Renderer {
this.highlightController = highlightController; this.highlightController = highlightController;
loadingDialog = new LoadingInfoDialog(mainWindow.getMainStage()); loadingDialog = new LoadingInfoDialog(mainWindow.getMainStage());
errVisitor = new ListErrorVisitor(highlightController); errVisitor = new ListErrorVisitor(highlightController);
setupLodFilters();
} }
private void setupLodFilters() { private boolean triangleVisible(TriangleMeta m) {
lodFilters.add(new ViewFilter() { int lod = m.lod();
boolean lodOk = lod < 0 || lod >= lodEnabled.length || lodEnabled[lod];
@Override if (!lodOk) {
public boolean useGeometry(CityObject co, Geometry geom) { return false;
return geom.getLod() == Lod.LOD1; }
} return !roofsHidden || !m.roof();
}
});
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 void rebuildIndexSet() {
roofFilter.setEnable(false); if (currentScene == null) {
return;
}
mainWindow.getGlViewport().setIndexSet(
IndexSetBuilder.build(currentScene.triangleMetas(), this::triangleVisible));
} }
public void enableLod1() { public void enableLod1() {
lodFilters.get(0).enable(); setLodEnabled(Lod.LOD1.ordinal(), true);
if (refresher != null) {
refresher.run();
}
} }
public void disableLod1() { public void disableLod1() {
lodFilters.get(0).disable(); setLodEnabled(Lod.LOD1.ordinal(), false);
if (refresher != null) {
refresher.run();
}
} }
public void enableLod2() { public void enableLod2() {
lodFilters.get(1).enable(); setLodEnabled(Lod.LOD2.ordinal(), true);
if (refresher != null) {
refresher.run();
}
} }
public void disableLod2() { public void disableLod2() {
lodFilters.get(1).disable(); setLodEnabled(Lod.LOD2.ordinal(), false);
if (refresher != null) {
refresher.run();
}
} }
public void enableLod3() { public void enableLod3() {
lodFilters.get(2).enable(); setLodEnabled(Lod.LOD3.ordinal(), true);
if (refresher != null) {
refresher.run();
}
} }
public void disableLod3() { public void disableLod3() {
lodFilters.get(2).disable(); setLodEnabled(Lod.LOD3.ordinal(), false);
if (refresher != null) {
refresher.run();
}
} }
public void enableLod4() { public void enableLod4() {
lodFilters.get(3).enable(); setLodEnabled(Lod.LOD4.ordinal(), true);
if (refresher != null) {
refresher.run();
}
} }
public void disableLod4() { public void disableLod4() {
lodFilters.get(3).disable(); setLodEnabled(Lod.LOD4.ordinal(), false);
if (refresher != null) { }
refresher.run();
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() { public void hideRoofs() {
roofFilter.setEnable(true); roofsHidden = true;
if (refresher != null) { Platform.runLater(this::rebuildIndexSet);
refresher.run();
}
} }
public void showRoofs() { public void showRoofs() {
roofFilter.setEnable(false); roofsHidden = false;
if (refresher != null) { Platform.runLater(this::rebuildIndexSet);
refresher.run();
}
} }
public void setupRefresher(CityObject co) { public void setupRefresher(CityObject co) {
...@@ -207,18 +147,7 @@ public class Renderer { ...@@ -207,18 +147,7 @@ public class Renderer {
private void addPolygons(CityObject co, Set<ConcretePolygon> polygons) { private void addPolygons(CityObject co, Set<ConcretePolygon> polygons) {
for (Geometry geom : co.getGeometries()) { for (Geometry geom : co.getGeometries()) {
boolean used = false; addConcretePolygons(polygons, geom);
for (ViewFilter filter : lodFilters) {
if (filter.allowedToUse(co, geom)) {
used = true;
break;
}
}
if (used) {
if (roofFilter.allowedToUse(co, geom)) {
addConcretePolygons(polygons, geom);
}
}
} }
} }
...@@ -244,15 +173,14 @@ public class Renderer { ...@@ -244,15 +173,14 @@ public class Renderer {
public void render(Geometry geom) { public void render(Geometry geom) {
refresher = () -> { refresher = () -> {
Platform.runLater(this::clearGeometryTrees); Platform.runLater(this::clearGeometryTrees);
currentTriGeom = TriangulatedGeometry.of(geom); SceneData scene = SceneBuilder.fromPolygons(geom.getPolygons(), Color.WHITE);
if (geom.getEdges() == null && currentGeometry != null) { if (geom.getEdges() == null && currentGeometry != null) {
// if there are no edges available low memory mode is enabled // if there are no edges available low memory mode is enabled
// clear the old geometry of all meta information // clear the old geometry of all meta information
currentGeometry.clearMetaData(); currentGeometry.clearMetaData();
} }
errVisitor.setGeometry(currentTriGeom);
Platform.runLater(() -> { Platform.runLater(() -> {
setupRenderState(); applyScene(scene);
if (geom.getEdges() == null) { if (geom.getEdges() == null) {
// create edges and vertices so they can be listed in the gui // create edges and vertices so they can be listed in the gui
geom.prepareForChecking(); geom.prepareForChecking();
...@@ -349,9 +277,8 @@ public class Renderer { ...@@ -349,9 +277,8 @@ public class Renderer {
private void render(Collection<? extends Polygon> polygons) { private void render(Collection<? extends Polygon> polygons) {
Platform.runLater(this::clearGeometryTrees); Platform.runLater(this::clearGeometryTrees);
currentTriGeom = TriangulatedGeometry.of(polygons); SceneData scene = SceneBuilder.fromPolygons(polygons, Color.WHITE);
errVisitor.setGeometry(currentTriGeom); Platform.runLater(() -> applyScene(scene));
setupRenderState();
} }
private void clearGeometryTrees() { private void clearGeometryTrees() {
...@@ -375,10 +302,9 @@ public class Renderer { ...@@ -375,10 +302,9 @@ public class Renderer {
clearAttributes(); clearAttributes();
}); });
Thread t = new Thread(() -> { Thread t = new Thread(() -> {
currentTriGeom = TriangulatedGeometry.of(model, lodFilters); SceneData scene = SceneBuilder.fromModel(model);
errVisitor.setGeometry(currentTriGeom);
Platform.runLater(() -> { Platform.runLater(() -> {
setupRenderState(); applyScene(scene);
mainWindow.zoomOutForBoundingBox(BoundingBox.of(model)); mainWindow.zoomOutForBoundingBox(BoundingBox.of(model));
loadingDialog.hide(); loadingDialog.hide();
}); });
...@@ -430,14 +356,10 @@ public class Renderer { ...@@ -430,14 +356,10 @@ public class Renderer {
CityObjectCollector collector = new CityObjectCollector(); CityObjectCollector collector = new CityObjectCollector();
cos.forEach(cityObject -> cityObject.accept(collector)); cos.forEach(cityObject -> cityObject.accept(collector));
collector.getCityObjects().forEach(cityObject -> addPolygons(cityObject, polygons)); collector.getCityObjects().forEach(cityObject -> addPolygons(cityObject, polygons));
if (baseColor != null) { Color base = baseColor != null ? baseColor : Color.WHITE;
currentTriGeom = TriangulatedGeometry.of(polygons, baseColor); SceneData scene = SceneBuilder.fromPolygons(polygons, base);
} else {
currentTriGeom = TriangulatedGeometry.of(polygons);
}
errVisitor.setGeometry(currentTriGeom);
Platform.runLater(() -> { Platform.runLater(() -> {
setupRenderState(); applyScene(scene);
mainWindow.zoomOutForBoundingBox(BoundingBox.of(polygons)); mainWindow.zoomOutForBoundingBox(BoundingBox.of(polygons));
loadingDialog.hide(); loadingDialog.hide();
}); });
...@@ -455,49 +377,44 @@ public class Renderer { ...@@ -455,49 +377,44 @@ public class Renderer {
private void addConcretePolygons(Set<ConcretePolygon> polygons, Geometry geom) { private void addConcretePolygons(Set<ConcretePolygon> polygons, Geometry geom) {
for (Polygon p : geom.getPolygons()) { for (Polygon p : geom.getPolygons()) {
if (p.getOriginal().getPartOfSurface() != null &&
!roofFilter.allowedToUse(p.getOriginal().getPartOfSurface(), p.getParent())) continue;
polygons.add(p.getOriginal()); polygons.add(p.getOriginal());
} }
} }
private void setupRenderState() { /**
currentTriGeom.setCullFace(currentCulling); * Installs the freshly built scene into the viewport (FX thread), wires the highlight center
currentTriGeom.setDrawMode(currentDrawMode); * supplier, applies the current wireframe/cull state and rebuilds the visible index set.
Platform.runLater(() -> { */
mainWindow.getMeshGroup().getChildren().clear(); private void applyScene(SceneData scene) {
mainWindow.getMeshGroup().getChildren().addAll(currentTriGeom.getMeshes()); 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.getGridButton().setDisable(false);
mainWindow.getCullingButton().setDisable(false); mainWindow.getCullingButton().setDisable(false);
} }
public void showWireFrame(boolean show) { public void showWireFrame(boolean show) {
if (currentTriGeom != null) { wireframe = show;
if (show) { mainWindow.getGlViewport().setWireframe(show);
currentDrawMode = DrawMode.LINE;
} else {
currentDrawMode = DrawMode.FILL;
}
currentTriGeom.setDrawMode(currentDrawMode);
}
} }
public void enableCulling(boolean enable) { public void enableCulling(boolean enable) {
if (currentTriGeom != null) { culling = enable;
if (enable) { mainWindow.getGlViewport().setCulling(enable);
currentCulling = CullFace.BACK;
} else {
currentCulling = CullFace.NONE;
}
currentTriGeom.setCullFace(currentCulling);
}
} }
public void clearCurrentRender() { public void clearCurrentRender() {
// don't render anything // don't render anything
currentScene = null;
Platform.runLater(() -> { Platform.runLater(() -> {
mainWindow.getMeshGroup().getChildren().clear(); GLViewport viewport = mainWindow.getGlViewport();
viewport.setScene(null);
viewport.setIndexSet(new int[0]);
clearGeometryTrees(); clearGeometryTrees();
clearAttributes(); clearAttributes();
}); });
...@@ -508,35 +425,35 @@ public class Renderer { ...@@ -508,35 +425,35 @@ public class Renderer {
} }
public void highlight(Polygon p) { public void highlight(Polygon p) {
highlightController.highlight(p, currentTriGeom); highlightController.highlight(p);
} }
public void highlight(LinearRing lr) { public void highlight(LinearRing lr) {
highlightController.highlight(lr, currentTriGeom); highlightController.highlight(lr);
} }
public void highlight(Edge e) { public void highlight(Edge e) {
highlightController.highlight(e, currentTriGeom); highlightController.highlight(e);
} }
public void highlight(Vertex v) { public void highlight(Vertex v) {
highlightController.highlight(v, currentTriGeom); highlightController.highlight(v);
} }
public void highlight(List<LinearRing> highlightedRings) { public void highlight(List<LinearRing> highlightedRings) {
highlightController.highlight(highlightedRings, currentTriGeom); highlightController.highlight(highlightedRings);
} }
public void highlightEdges(List<Edge> edges) { public void highlightEdges(List<Edge> edges) {
highlightController.highlightEdges(edges, currentTriGeom); highlightController.highlightEdges(edges);
} }
public void highlightPolygons(List<List<Polygon>> components) { public void highlightPolygons(List<List<Polygon>> components) {
highlightController.highlightPolygons(components, currentTriGeom); highlightController.highlightPolygons(components);
} }
public void addHighlight(Vertex vertex, Color c) { public void addHighlight(Vertex vertex, Color c) {
highlightController.addHighlight(vertex, currentTriGeom, c); highlightController.addHighlight(vertex, c);
} }
public void highlight(CheckError err) { public void highlight(CheckError err) {
...@@ -560,11 +477,11 @@ public class Renderer { ...@@ -560,11 +477,11 @@ public class Renderer {
} }
public void addHighlight(Polygon p) { public void addHighlight(Polygon p) {
highlightController.addHighlight(p, currentTriGeom); highlightController.addHighlight(p);
} }
public void addHighlight(Triangle3d t) { public void addHighlight(Triangle3d t) {
highlightController.highlight(t, currentTriGeom); highlightController.highlight(t);
} }
public void reset() { public void reset() {
......
package de.hft.stuttgart.citydoctor2.gui;
import de.hft.stuttgart.citydoctor2.utils.visitors.CityObjectCollector;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.*;
import de.hft.stuttgart.citydoctor2.gui.filter.ViewFilter;
import de.hft.stuttgart.citydoctor2.math.Triangle3d;
import de.hft.stuttgart.citydoctor2.math.UnitVector3d;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
import de.hft.stuttgart.citydoctor2.tesselation.TesselatedPolygon;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.*;
import java.util.*;
import java.util.stream.Stream;
public class TriangulatedGeometry {
private static final PhongMaterial GRID_MAT = new PhongMaterial(Color.BLACK);
// random vector for calculating normal angles, for color determination
private static final UnitVector3d AXIS = new Vector3d(19, 0.8, 1.5).normalize();
private Vector3d movedBy;
private List<MeshView> meshes;
private List<PhongMaterial> materials;
public static TriangulatedGeometry of(Geometry geom) {
return of(geom.getPolygons());
}
public static TriangulatedGeometry of(Collection<? extends Polygon> polygons, Color basePolygonColor) {
TriangulatedGeometry triGeom = new TriangulatedGeometry();
triGeom.materials = new ArrayList<>();
triGeom.meshes = new ArrayList<>();
List<Vector3d> points = new ArrayList<>();
for (Polygon p : polygons) {
points.addAll(p.getExteriorRing().getVertices());
}
triGeom.movedBy = triGeom.findCenter(points);
addPolygonDataToTriGeom(polygons, basePolygonColor, triGeom);
return triGeom;
}
private static void addPolygonDataToTriGeom(Collection<? extends Polygon> polygons, Color basePolygonColor,
TriangulatedGeometry triGeom) {
for (Polygon p : polygons) {
TesselatedPolygon tp = p.tesselate();
TriangleMesh triMesh = new TriangleMesh(VertexFormat.POINT_TEXCOORD);
Map<Vector3d, Integer> indexMap = new HashMap<>();
List<Vector3d> vertices = new ArrayList<>();
int index = 0;
for (Triangle3d t : tp.getTriangles()) {
index = triGeom.filterDuplicates(triMesh, indexMap, vertices, index, t.getP1());
index = triGeom.filterDuplicates(triMesh, indexMap, vertices, index, t.getP2());
index = triGeom.filterDuplicates(triMesh, indexMap, vertices, index, t.getP3());
}
for (Vector3d point : vertices) {
float x = (float) (point.getX() - triGeom.movedBy.getX());
float y = (float) (point.getY() - triGeom.movedBy.getY());
float z = (float) (point.getZ() - triGeom.movedBy.getZ());
triMesh.getPoints().addAll(x, y, z);
}
triMesh.getTexCoords().addAll(0, 0);
MeshView view = new MeshView(triMesh);
view.setUserData(new PolygonClickDispatcher(p));
PhongMaterial mat = triGeom.calculateMaterial(p, basePolygonColor);
triGeom.materials.add(mat);
triGeom.meshes.add(view);
}
}
public static TriangulatedGeometry of(CityDoctorModel model, List<ViewFilter> filters) {
List<Vector3d> points = new ArrayList<>();
addPointsFromCityObject(model.createFeatureStream().toList(), points);
TriangulatedGeometry triGeom = new TriangulatedGeometry();
triGeom.materials = new ArrayList<>();
triGeom.meshes = new ArrayList<>();
triGeom.movedBy = triGeom.findCenter(points);
addPolygonDataFromCityObjects(model.getBuildings(), triGeom, Color.WHITE, filters);
addPolygonDataFromCityObjects(model.getBridges(), triGeom, Color.LIGHTSTEELBLUE, filters);
addPolygonDataFromCityObjects(model.getTunnels(), triGeom, Color.SLATEGRAY, filters);
addPolygonDataFromCityObjects(model.getLand(), triGeom, Color.TAN, filters);
addPolygonDataFromCityObjects(model.getTransportation(), triGeom, Color.DIMGRAY, filters);
addPolygonDataFromCityObjects(model.getVegetation(), triGeom, Color.LIGHTGREEN, filters);
addPolygonDataFromCityObjects(model.getWater(), triGeom, Color.LIGHTSKYBLUE, filters);
addPolygonDataFromCityObjects(model.getCityFurniture(), triGeom, Color.BLUEVIOLET, filters);
addPolygonDataFromCityObjects(model.getOtherCityObjects(), triGeom, Color.BLACK, filters);
return triGeom;
}
private static void addPolygonDataFromCityObjects(Stream<? extends CityObject> cos,
TriangulatedGeometry triGeom, Color color, List<ViewFilter> filters) {
CityObjectCollector collector = new CityObjectCollector();
cos.forEach(cityObject -> cityObject.accept(collector));
collector.getCityObjects().forEach(cityObject -> addPolygonData(cityObject, triGeom, color, filters));
}
private static void addPolygonData(CityObject co, TriangulatedGeometry triGeom, Color color, List<ViewFilter> filters) {
for (Geometry geom : co.getGeometries()) {
if (isGeometryFiltered(co, geom, filters)) {
continue;
}
List<Polygon> polygons = new ArrayList<>();
for (Polygon p : geom.getPolygons()) {
if (p.isLink()) {
continue;
}
polygons.add(p);
}
addPolygonDataToTriGeom(polygons, color, triGeom);
}
}
private static boolean isGeometryFiltered(CityObject co, Geometry geom, List<ViewFilter> filters) {
for (ViewFilter filter : filters) {
if (filter.allowedToUse(co, geom)) {
return false;
}
}
return true;
}
private static void addPointsFromCityObject(List<? extends CityObject> cos, List<Vector3d> points) {
CityObjectCollector collector = new CityObjectCollector();
cos.forEach(cityObject -> cityObject.accept(collector));
collector.getCityObjects().forEach(cityObject -> addPoints(cityObject, points));
}
private static void addPoints(CityObject co, List<Vector3d> points) {
for (Geometry geom : co.getGeometries()) {
for (Polygon p : geom.getPolygons()) {
if (p.isLink()) {
continue;
}
points.addAll(p.getExteriorRing().getVertices());
}
}
}
public static TriangulatedGeometry of(Collection<? extends Polygon> polygons) {
return of(polygons, Color.WHITE);
}
private int filterDuplicates(TriangleMesh triMesh, Map<Vector3d, Integer> indexMap, List<Vector3d> vertices,
int index, Vector3d v) {
Integer vertexIndex = indexMap.get(v);
if (vertexIndex == null) {
indexMap.put(v, index);
vertices.add(v);
vertexIndex = index;
index++;
}
triMesh.getFaces().addAll(vertexIndex, 0);
return index;
}
private PhongMaterial calculateMaterial(Polygon p, Color baseColor) {
Vector3d normal = p.calculateNormalNormalized();
if (p.getRenderColor() != null && p.getRenderColor() != Color.WHITE) {
baseColor = p.getRenderColor();
}
double cos = normal.dot(AXIS);
double acos = Math.acos(cos);
// normalize to range [0.3, 0.9]
acos = acos / Math.PI;
acos = acos * 0.6 + 0.3;
Color derivedColor = baseColor.deriveColor(0, 1.0, acos, 1.0);
return new PhongMaterial(derivedColor);
}
private Vector3d findCenter(List<Vector3d> points) {
double xMin = Double.MAX_VALUE;
double yMin = Double.MAX_VALUE;
double zMin = Double.MAX_VALUE;
double xMax = Double.NEGATIVE_INFINITY;
double yMax = Double.NEGATIVE_INFINITY;
double zMax = Double.NEGATIVE_INFINITY;
for (Vector3d point : points) {
if (point.getX() < xMin) {
xMin = point.getX();
}
if (point.getX() > xMax) {
xMax = point.getX();
}
if (point.getY() < yMin) {
yMin = point.getY();
}
if (point.getY() > yMax) {
yMax = point.getY();
}
if (point.getZ() < zMin) {
zMin = point.getZ();
}
if (point.getZ() > zMax) {
zMax = point.getZ();
}
}
// center
double x = (xMax - xMin) / 2 + xMin;
double y = (yMax - yMin) / 2 + yMin;
double z = (zMax - zMin) / 2 + zMin;
return new Vector3d(x, y, z);
}
public Vector3d getMovedBy() {
return movedBy;
}
public void setCullFace(CullFace currentCulling) {
for (MeshView mesh : meshes) {
mesh.setCullFace(currentCulling);
}
}
public void setDrawMode(DrawMode currentDrawMode) {
if (currentDrawMode == DrawMode.LINE) {
for (MeshView mesh : meshes) {
mesh.setDrawMode(currentDrawMode);
mesh.setMaterial(GRID_MAT);
}
} else if (currentDrawMode == DrawMode.FILL) {
for (int i = 0; i < meshes.size(); i++) {
MeshView mesh = meshes.get(i);
mesh.setDrawMode(currentDrawMode);
mesh.setMaterial(materials.get(i));
}
}
}
public List<MeshView> getMeshes() {
return meshes;
}
}
...@@ -46,7 +46,7 @@ public class ValidationView extends View { ...@@ -46,7 +46,7 @@ public class ValidationView extends View {
public void onHide() { public void onHide() {
Platform.runLater(() -> { Platform.runLater(() -> {
mainWindow.unselectEverything(); mainWindow.unselectEverything();
mainWindow.getMeshGroup().getChildren().clear(); mainWindow.clearMeshView();
mainWindow.clearHighlights(); mainWindow.clearHighlights();
mainWindow.getErrorTree().getRoot().getChildren().clear(); mainWindow.getErrorTree().getRoot().getChildren().clear();
mainWindow.getPolygonsView().getRoot().getChildren().clear(); mainWindow.getPolygonsView().getRoot().getChildren().clear();
......
package de.hft.stuttgart.citydoctor2.gui.gl;
import java.util.Arrays;
/** Growable primitive float array to avoid boxing when building large vertex buffers. */
public class FloatList {
private float[] data;
private int size;
public FloatList() { this(1024); }
public FloatList(int initialCapacity) { data = new float[Math.max(1, initialCapacity)]; }
public void add(float v) {
ensure(size + 1);
data[size++] = v;
}
public void add(float a, float b, float c) {
ensure(size + 3);
data[size++] = a;
data[size++] = b;
data[size++] = c;
}
private void ensure(int capacity) {
if (capacity > data.length) {
int newCap = data.length;
while (newCap < capacity) {
newCap <<= 1;
}
data = Arrays.copyOf(data, newCap);
}
}
public int size() { return size; }
/** Resets the list to empty without releasing the backing array. */
public void clear() { size = 0; }
public float[] toArray() { return Arrays.copyOf(data, size); }
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import com.huskerdev.grapl.gl.GLProfile;
import com.huskerdev.openglfx.canvas.GLCanvas;
import com.huskerdev.openglfx.jogl.JOGLExecutor;
import com.huskerdev.openglfx.jogl.events.JOGLRenderEvent;
import com.jogamp.opengl.GL;
import com.jogamp.opengl.GL3;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.scene.input.MouseButton;
import javafx.scene.paint.Color;
/** OpenGL viewport embedded in JavaFX via openglfx. Replaces the legacy SubScene. */
public class GLViewport {
private static final Color BACKGROUND = Color.AZURE;
private final GLCanvas canvas;
private final OrbitCamera camera = new OrbitCamera();
private final GlScene scene = new GlScene();
private final PickingFramebuffer picking = new PickingFramebuffer();
private final HighlightOverlay overlay = new HighlightOverlay();
private ShaderProgram sceneProgram;
private ShaderProgram pickProgram;
private ShaderProgram pointProgram;
private boolean initialized;
// state set off-thread, consumed in render callback
private final AtomicReference<SceneData> pendingScene = new AtomicReference<>();
private final AtomicReference<int[]> pendingIndexSet = new AtomicReference<>();
// last index set seen; retained so it can be (re)applied after the scene is uploaded or the GL
// context is recreated (the index buffer only exists once the scene's buffers have been generated)
private volatile int[] lastIndices;
private volatile boolean wireframe = false;
private volatile boolean cull = false;
private volatile float pointScale = 20f;
private volatile float lineScale = 4f;
// highlight marker data, uploaded on the GL thread when dirty
private volatile float[] hlPoints = new float[0];
private volatile float[] hlPointColors = new float[0];
private volatile float[] hlLines = new float[0];
private volatile float[] hlLineColors = new float[0];
private volatile boolean highlightsDirty = false;
// pick request: logical (JavaFX) screen coords + callback (invoked on FX thread with resolved Object)
private volatile double[] pickRequest;
private volatile Consumer<Object> pickCallback;
private SceneData currentScene;
// drag state
private double lastX;
private double lastY;
// optional hook invoked after a camera interaction (orbit/pan/zoom) so a coupled viewport can be
// kept in sync and redrawn (used by the dual-viewport healer)
private Runnable interactionListener;
// on-demand render request. openglfx can drop a repaint() that arrives while a frame is rendering
// (its requestRepaint is an unconditional notify with no pending flag), so a single repaint is not
// reliable. This flag + pump re-deliver repaint() each pulse until a frame consumes the request,
// then the pump stops itself so an idle viewport has no per-pulse cost.
private volatile boolean renderRequested;
private final AnimationTimer repaintPump = new AnimationTimer() {
@Override
public void handle(long now) {
if (renderRequested) {
canvas.repaint();
} else {
stop();
}
}
};
public GLViewport() {
// args: executor, profile, flipY=false, msaa=4, fps=0.0
// fps=0 -> on-demand rendering: a frame is drawn only when repaint() is called (every state
// change / input does so), so an idle viewport allocates and renders nothing.
canvas = new GLCanvas(new JOGLExecutor(), GLProfile.CORE, false, 4, 0.0);
canvas.addOnInitEvent(ev -> {
// GL context (re)created: shaders/buffers from any previous context are gone. Drop our
// handles and re-queue the current scene so it is re-uploaded into the new context.
initialized = false;
scene.markContextLost();
if (currentScene != null) {
pendingScene.set(currentScene);
}
});
canvas.addOnRenderEvent(ev -> render((JOGLRenderEvent) ev));
canvas.addOnReshapeEvent(ev -> { /* aspect handled per-frame from width/height */ });
// on-demand mode: repaint when the canvas is resized so the framebuffer tracks the new size
canvas.widthProperty().addListener((obs, ov, nv) -> requestRender());
canvas.heightProperty().addListener((obs, ov, nv) -> requestRender());
installInputHandlers();
}
public GLCanvas getNode() { return canvas; }
// ---- public API (called from FX / worker threads) ----
public void setScene(SceneData data) { pendingScene.set(data); requestRender(); }
public void setIndexSet(int[] indices) { pendingIndexSet.set(indices); requestRender(); }
public void setWireframe(boolean on) { wireframe = on; requestRender(); }
public void setCulling(boolean on) { cull = on; requestRender(); }
public void setPointScale(float scale) { pointScale = scale; requestRender(); }
/** points: xyz, pointColors rgb-per-point; lines: xyz pairs, lineColors rgb-per-line-vertex. */
public void setHighlights(float[] points, float[] pointColors, float[] lines, float[] lineColors) {
this.hlPoints = points;
this.hlPointColors = pointColors;
this.hlLines = lines;
this.hlLineColors = lineColors;
this.highlightsDirty = true;
requestRender();
}
public OrbitCamera camera() { return camera; }
/** Hook invoked after a camera interaction so a coupled viewport can sync and redraw. */
public void setInteractionListener(Runnable listener) { this.interactionListener = listener; }
/** Public redraw trigger (e.g. for a coupled viewport). */
public void requestRedraw() { requestRender(); }
/** Clears the displayed geometry (draws nothing) until a new scene is set. */
public void clearScene() {
currentScene = null;
lastIndices = new int[0];
setIndexSet(new int[0]);
}
/**
* Requests a redraw. Sets the render flag, wakes the canvas immediately (best effort), and starts
* the pump so the request is re-delivered until a frame actually consumes it. Call on the FX thread.
*/
private void requestRender() {
renderRequested = true;
canvas.repaint();
repaintPump.start(); // idempotent while running
}
public void requestPick(double x, double y, Consumer<Object> callback) {
pickRequest = new double[]{x, y};
pickCallback = callback;
requestRender();
}
// ---- render callback (GL thread) ----
private void render(JOGLRenderEvent ev) {
// consume the render request; if new state arrives during this frame, requestRender re-arms it
renderRequested = false;
GL3 gl = ev.getGl();
int w = ev.width;
int h = ev.height;
int defaultFbo = ev.fbo;
if (!initialized) {
sceneProgram = new ShaderProgram(gl, "scene.vert", "scene.frag");
pickProgram = new ShaderProgram(gl, "pick.vert", "pick.frag");
pointProgram = new ShaderProgram(gl, "point.vert", "point.geom", "point.frag");
overlay.init(gl);
gl.glEnable(GL.GL_DEPTH_TEST);
gl.glEnable(GL.GL_MULTISAMPLE); // anti-aliasing for the multisampled framebuffer
initialized = true;
}
SceneData newScene = pendingScene.getAndSet(null);
boolean uploadedNow = false;
if (newScene != null) {
scene.upload(gl, newScene);
currentScene = newScene;
uploadedNow = true;
}
int[] newIndices = pendingIndexSet.getAndSet(null);
if (newIndices != null) {
lastIndices = newIndices;
}
// Apply the index set only once the scene's buffers exist (the element buffer is created by
// upload). Re-apply after a fresh upload so the indices survive a scene swap or context loss.
if (scene.hasData() && lastIndices != null && (newIndices != null || uploadedNow)) {
scene.setIndexSet(gl, lastIndices);
}
if (highlightsDirty) {
overlay.setPoints(gl, hlPoints, hlPointColors);
overlay.setLines(gl, hlLines, hlLineColors);
highlightsDirty = false;
}
camera.setAspect(h == 0 ? 1f : (float) w / h);
float[] mvp = camera.viewProjection();
gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, defaultFbo);
gl.glViewport(0, 0, w, h);
gl.glClearColor((float) BACKGROUND.getRed(), (float) BACKGROUND.getGreen(),
(float) BACKGROUND.getBlue(), 1f);
gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
if (cull) {
gl.glEnable(GL.GL_CULL_FACE);
} else {
gl.glDisable(GL.GL_CULL_FACE);
}
gl.glPolygonMode(GL.GL_FRONT_AND_BACK, wireframe ? GL3.GL_LINE : GL3.GL_FILL);
sceneProgram.use(gl);
gl.glUniformMatrix4fv(sceneProgram.uniform(gl, "uMVP"), 1, false, mvp, 0);
scene.draw(gl);
// wireframe applies to the geometry only; highlight markers always render filled
gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL3.GL_FILL);
// highlight markers sit at the same depth as geometry edges (and the wireframe lines); use
// LEQUAL so the later-drawn markers win depth ties instead of being rejected by GL_LESS
gl.glDepthFunc(GL.GL_LEQUAL);
// edge markers reuse the flat scene program (pos + colour)
overlay.drawLines(gl, lineScale);
// vertex markers use the sphere-impostor program (geometry shader -> camera-facing quads)
pointProgram.use(gl);
gl.glUniformMatrix4fv(pointProgram.uniform(gl, "uMVP"), 1, false, mvp, 0);
gl.glUniform2f(pointProgram.uniform(gl, "uViewport"), (float) w, (float) h);
gl.glUniform1f(pointProgram.uniform(gl, "uPointSize"), pointScale);
overlay.drawPoints(gl);
gl.glDepthFunc(GL.GL_LESS); // restore for the next frame's scene pass
double[] req = pickRequest;
if (req != null && currentScene != null) {
picking.resize(gl, w, h);
picking.bind(gl);
gl.glClearColor(0, 0, 0, 1);
gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL3.GL_FILL);
pickProgram.use(gl);
gl.glUniformMatrix4fv(pickProgram.uniform(gl, "uMVP"), 1, false, mvp, 0);
scene.draw(gl);
// click coords are logical (JavaFX) px; the picking buffer is physical px. Scale by the
// framebuffer/canvas ratio so HiDPI displays read the pixel actually under the cursor.
double cw = canvas.getWidth();
double ch = canvas.getHeight();
float sx = cw > 0 ? (float) (w / cw) : 1f;
float sy = ch > 0 ? (float) (h / ch) : 1f;
int px = Math.round((float) req[0] * sx);
int py = Math.round((float) req[1] * sy);
int id = picking.readId(gl, px, py);
Object target = currentScene.pickRegistry().resolve(id);
Consumer<Object> cb = pickCallback;
pickRequest = null;
pickCallback = null;
gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, defaultFbo);
if (cb != null) {
Platform.runLater(() -> cb.accept(target));
}
}
}
// ---- input ----
private void installInputHandlers() {
canvas.setOnMousePressed(e -> { lastX = e.getX(); lastY = e.getY(); });
canvas.setOnMouseDragged(e -> {
double dx = e.getX() - lastX;
double dy = e.getY() - lastY;
lastX = e.getX();
lastY = e.getY();
if (e.getButton() == MouseButton.PRIMARY) {
camera.orbit(dx / 3.0, dy / 3.0);
} else if (e.getButton() == MouseButton.SECONDARY) {
double speed = Math.abs(camera.distance()) / 1000.0;
camera.pan(-dx * speed, dy * speed);
}
requestRender();
notifyInteraction();
});
canvas.setOnScroll(e -> {
camera.zoom(e.getDeltaY() < 0 ? 1.05 : 0.95);
requestRender();
notifyInteraction();
});
}
public void resetCamera() { camera.reset(); requestRender(); }
private void notifyInteraction() {
if (interactionListener != null) {
interactionListener.run();
}
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import com.jogamp.opengl.GL;
import com.jogamp.opengl.GL3;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
/** GPU resources for the global scene mesh. Single VBO (pos+color+id), one index buffer redrawn per frame. */
public class GlScene {
private int vao = 0;
private int vboPos;
private int vboColor;
private int vboId;
private int ebo;
private int indexCount;
private boolean uploaded;
public void upload(GL3 gl, SceneData data) {
if (!uploaded) {
int[] ids = new int[3];
gl.glGenVertexArrays(1, ids, 0);
vao = ids[0];
int[] buffers = new int[4];
gl.glGenBuffers(4, buffers, 0);
vboPos = buffers[0];
vboColor = buffers[1];
vboId = buffers[2];
ebo = buffers[3];
}
gl.glBindVertexArray(vao);
uploadFloat(gl, vboPos, data.positions(), 0, 3);
uploadFloat(gl, vboColor, data.colors(), 1, 3);
uploadInt(gl, vboId, data.ids(), 2);
gl.glBindVertexArray(0);
uploaded = true;
}
private void uploadFloat(GL3 gl, int vbo, float[] data, int location, int components) {
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vbo);
FloatBuffer fb = toFloatBuffer(data);
gl.glBufferData(GL.GL_ARRAY_BUFFER, (long) data.length * Float.BYTES, fb, GL.GL_STATIC_DRAW);
gl.glEnableVertexAttribArray(location);
gl.glVertexAttribPointer(location, components, GL.GL_FLOAT, false, 0, 0L);
}
private void uploadInt(GL3 gl, int vbo, int[] data, int location) {
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vbo);
IntBuffer ib = toIntBuffer(data);
gl.glBufferData(GL.GL_ARRAY_BUFFER, (long) data.length * Integer.BYTES, ib, GL.GL_STATIC_DRAW);
gl.glEnableVertexAttribArray(location);
gl.glVertexAttribIPointer(location, 1, GL3.GL_INT, 0, 0L); // integer attribute, not normalized
}
/** Re-uploads which triangles to draw (element indices). Cheap; called on filter/LOD changes. */
public void setIndexSet(GL3 gl, int[] indices) {
if (!uploaded || vao == 0) {
// buffers not generated yet; the caller re-applies once the scene is uploaded
return;
}
gl.glBindVertexArray(vao);
gl.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, ebo);
IntBuffer ib = toIntBuffer(indices);
gl.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER, (long) indices.length * Integer.BYTES, ib,
GL.GL_DYNAMIC_DRAW);
gl.glBindVertexArray(0);
indexCount = indices.length;
}
public void draw(GL3 gl) {
if (vao == 0 || indexCount == 0) {
return;
}
gl.glBindVertexArray(vao);
gl.glDrawElements(GL.GL_TRIANGLES, indexCount, GL.GL_UNSIGNED_INT, 0L);
gl.glBindVertexArray(0);
}
public boolean hasData() { return uploaded; }
/**
* Forgets GL handles after the context was lost/recreated, without issuing GL calls (the old
* handles belong to a dead context). The next {@link #upload} regenerates everything.
*/
public void markContextLost() {
vao = 0;
indexCount = 0;
uploaded = false;
}
public void dispose(GL3 gl) {
if (vao != 0) {
gl.glDeleteVertexArrays(1, new int[]{vao}, 0);
gl.glDeleteBuffers(4, new int[]{vboPos, vboColor, vboId, ebo}, 0);
vao = 0;
uploaded = false;
}
}
private static FloatBuffer toFloatBuffer(float[] data) {
ByteBuffer bb = ByteBuffer.allocateDirect(data.length * Float.BYTES).order(ByteOrder.nativeOrder());
FloatBuffer fb = bb.asFloatBuffer();
fb.put(data).flip();
return fb;
}
private static IntBuffer toIntBuffer(int[] data) {
ByteBuffer bb = ByteBuffer.allocateDirect(data.length * Integer.BYTES).order(ByteOrder.nativeOrder());
IntBuffer ib = bb.asIntBuffer();
ib.put(data).flip();
return ib;
}
}
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