|
54 | 54 | import com.neuronrobotics.sdk.addons.kinematics.math.TransformNR; |
55 | 55 | import com.neuronrobotics.sdk.common.Log; |
56 | 56 | import eu.mihosoft.vrl.v3d.CSG; |
| 57 | +import eu.mihosoft.vrl.v3d.Vector3d; |
57 | 58 | import eu.mihosoft.vrl.v3d.Cylinder; |
58 | 59 | import eu.mihosoft.vrl.v3d.JavaFXInitializer; |
59 | 60 | import eu.mihosoft.vrl.v3d.MissingManipulatorException; |
|
68 | 69 | //import javafx.embed.swing.SwingFXUtils; |
69 | 70 | import javafx.event.ActionEvent; |
70 | 71 | import javafx.event.EventHandler; |
| 72 | +import javafx.geometry.Point2D; |
71 | 73 | import javafx.geometry.Point3D; |
72 | 74 | import javafx.scene.*; |
73 | 75 | import javafx.scene.control.*; |
|
84 | 86 | import javafx.scene.input.MouseEvent; |
85 | 87 | import javafx.scene.input.ScrollEvent; |
86 | 88 | import javafx.scene.input.PickResult; |
| 89 | +import javafx.scene.layout.Pane; |
87 | 90 | import javafx.scene.layout.AnchorPane; |
88 | 91 | import javafx.scene.layout.HBox; |
89 | 92 | import javafx.scene.paint.*; |
|
99 | 102 | import javafx.scene.transform.Rotate; |
100 | 103 | import javafx.scene.transform.Scale; |
101 | 104 | import javafx.scene.transform.Transform; |
| 105 | +import javafx.scene.transform.NonInvertibleTransformException; |
| 106 | +import javafx.geometry.Bounds; |
102 | 107 |
|
103 | 108 | // Development, for objectDistance methode |
104 | 109 | //import com.sun.javafx.geom.PickRay; |
@@ -206,7 +211,9 @@ public class BowlerStudio3dEngine implements ICameraChangeListener, IMobileBaseU |
206 | 211 |
|
207 | 212 | /** The user group for the user defined objects and the navigation cube */ |
208 | 213 | private final Group userGroup = new Group(); |
209 | | - |
| 214 | + // Skip first userGroup nodes, which are not user objects |
| 215 | + private static int SKIP_USERGROUP_NODES = 0; |
| 216 | + |
210 | 217 | /** The scene. */ |
211 | 218 | private SubScene scene; |
212 | 219 |
|
@@ -266,6 +273,135 @@ public class BowlerStudio3dEngine implements ICameraChangeListener, IMobileBaseU |
266 | 273 | private AmbientLight ambientLight = new AmbientLight(Color.color(1.0, 1.0, 1.0, 0)); |
267 | 274 | private volatile boolean waitingForCompletion; |
268 | 275 |
|
| 276 | + private Pane overlayPane = null; |
| 277 | + public void setOverlayPane(Pane overlayP) { |
| 278 | + this.overlayPane = overlayP; |
| 279 | + } |
| 280 | + |
| 281 | + public Pane getOverlayPane() { |
| 282 | + return overlayPane; |
| 283 | + } |
| 284 | + |
| 285 | + public double cameraDistanceToPixelPerMM() { |
| 286 | + double fovRad = Math.toRadians(camera.getFieldOfView()); |
| 287 | + // 500mm projection plane |
| 288 | + return getSubScene().getHeight() / (1000.0 * Math.tan(fovRad / 2.0)); |
| 289 | + } |
| 290 | + |
| 291 | + // Converts a 3D world point to a 2D point in the scene/screen (default workplane only) |
| 292 | + public Point2D worldToScene(Point3D world3D) { |
| 293 | + Point2D screenPt = controlHandleGroup.getChildren().get(1).localToScreen(world3D); |
| 294 | + return scene.screenToLocal(screenPt); |
| 295 | + } |
| 296 | + |
| 297 | + // Find the 3D world Z-point for a given 3D world X, Y point and the 2D scene/screen point |
| 298 | + public double sceneToWorldFixedXY_WP(Point2D scenePixel, double fixedX, double fixedY) { |
| 299 | + Transform wp = gridPlacementAffine; |
| 300 | + |
| 301 | + VirtualCameraMobileBase cam = getVirtualcam(); |
| 302 | + TransformNR c2w = cam.getCamerFrame().times(new TransformNR(0, 0, cam.getZoomDepth())); |
| 303 | + Vector3d origin = new Vector3d(c2w.getX(), c2w.getY(), c2w.getZ()); |
| 304 | + |
| 305 | + double ppm = cameraDistanceToPixelPerMM(); |
| 306 | + double cx = overlayPane.getWidth() / 2.0; |
| 307 | + double cy = overlayPane.getHeight() / 2.0; |
| 308 | + double camX = -(scenePixel.getX() - cx) / ppm; |
| 309 | + double camY = -(scenePixel.getY() - cy) / ppm; |
| 310 | + |
| 311 | + Vector3d target = new Vector3d(camX, camY, 500.0).transform(TransformFactory.nrToCSG(c2w)); |
| 312 | + Vector3d dir = target.minus(origin); |
| 313 | + dir.normalize(); |
| 314 | + |
| 315 | + try { |
| 316 | + Transform inv = wp.createInverse(); |
| 317 | + Point3D localOrigin = inv.transform(new Point3D(origin.x, origin.y, origin.z)); |
| 318 | + Point3D localDirPt = inv.deltaTransform(new Point3D(dir.x, dir.y, dir.z)); |
| 319 | + Vector3d localDir = new Vector3d(localDirPt.getX(), localDirPt.getY(), localDirPt.getZ()); |
| 320 | + |
| 321 | + double ox = localOrigin.getX(); |
| 322 | + double oy = localOrigin.getY(); |
| 323 | + double dx = localDir.x; |
| 324 | + double dy = localDir.y; |
| 325 | + |
| 326 | + // Minimize distance squared to line |
| 327 | + double denom = dx*dx + dy*dy; |
| 328 | + double t; |
| 329 | + if (denom < 1e-9) |
| 330 | + t = 0; // Parallel, use origin |
| 331 | + else |
| 332 | + t = ((fixedX - ox)*dx + (fixedY - oy)*dy) / denom; |
| 333 | + |
| 334 | + return localOrigin.getZ() + t * localDir.z; |
| 335 | + |
| 336 | + } catch (NonInvertibleTransformException e) { |
| 337 | + return 0; |
| 338 | + } |
| 339 | + } |
| 340 | + |
| 341 | + // Find the 3D world X,Y-point for a given 3D world Z point and the 2D scene/screen point |
| 342 | + public Point3D sceneToWorldFixedZ_WP(Point2D scenePixel, double fixedZ) { |
| 343 | + Transform wp = gridPlacementAffine; |
| 344 | + |
| 345 | + // Get camera ray in world space |
| 346 | + VirtualCameraMobileBase cam = getVirtualcam(); |
| 347 | + TransformNR c2w = cam.getCamerFrame().times(new TransformNR(0, 0, cam.getZoomDepth())); |
| 348 | + Vector3d origin = new Vector3d(c2w.getX(), c2w.getY(), c2w.getZ()); |
| 349 | + |
| 350 | + double ppm = cameraDistanceToPixelPerMM(); |
| 351 | + double cx = overlayPane.getWidth() / 2.0; |
| 352 | + double cy = overlayPane.getHeight() / 2.0; |
| 353 | + double camX = -(scenePixel.getX() - cx) / ppm; |
| 354 | + double camY = -(scenePixel.getY() - cy) / ppm; |
| 355 | + |
| 356 | + Vector3d target = new Vector3d(camX, camY, 500.0).transform(TransformFactory.nrToCSG(c2w)); |
| 357 | + Vector3d dir = target.minus(origin); |
| 358 | + dir.normalize(); |
| 359 | + |
| 360 | + try { |
| 361 | + Transform inv = wp.createInverse(); |
| 362 | + Point3D localOrigin = inv.transform(new Point3D(origin.x, origin.y, origin.z)); |
| 363 | + Point3D localDirPt = inv.deltaTransform(new Point3D(dir.x, dir.y, dir.z)); |
| 364 | + Vector3d localDir = new Vector3d(localDirPt.getX(), localDirPt.getY(), localDirPt.getZ()); |
| 365 | + |
| 366 | + double localZDir = localDir.z; |
| 367 | + if (Math.abs(localZDir) < 1e-9) |
| 368 | + // Ray is parallel to the Z plane, use origin's XY at fixedZ |
| 369 | + return new Point3D(localOrigin.getX(), localOrigin.getY(), fixedZ); |
| 370 | + |
| 371 | + double t = (fixedZ - localOrigin.getZ()) / localZDir; |
| 372 | + |
| 373 | + double localX = localOrigin.getX() + t * localDir.x; |
| 374 | + double localY = localOrigin.getY() + t * localDir.y; |
| 375 | + |
| 376 | + return new Point3D(localX, localY, fixedZ); |
| 377 | + |
| 378 | + } catch (NonInvertibleTransformException e) { |
| 379 | + return new Point3D(0, 0, fixedZ); |
| 380 | + } |
| 381 | + } |
| 382 | + |
| 383 | + // Gives the scale factor for a 1MM object to produce 1 pixel on screen |
| 384 | + public double screenToSceneMMscale(Point3D scenePosition) { |
| 385 | + |
| 386 | + VirtualCameraMobileBase vcam = getVirtualcam(); |
| 387 | + TransformNR c2w = vcam.getCamerFrame().times(new TransformNR(0, 0, vcam.getZoomDepth())); |
| 388 | + |
| 389 | + // Camera to object vector |
| 390 | + double dx = scenePosition.getX() - c2w.getX(); |
| 391 | + double dy = scenePosition.getY() - c2w.getY(); |
| 392 | + double dz = scenePosition.getZ() - c2w.getZ(); |
| 393 | + |
| 394 | + // Camera rotation matrix |
| 395 | + double fwdX = c2w.getRotation().getRotationMatrix()[0][2]; |
| 396 | + double fwdY = c2w.getRotation().getRotationMatrix()[1][2]; |
| 397 | + double fwdZ = c2w.getRotation().getRotationMatrix()[2][2]; |
| 398 | + // Project position vector onto camera forward axis, 0.1mm minimum distance |
| 399 | + double minDistance = Math.max(0.1, dx * fwdX + dy * fwdY + dz * fwdZ); |
| 400 | + |
| 401 | + return (2.0 * minDistance * Math.tan(Math.toRadians(camera.getFieldOfView()) / 2.0)) / getSubScene().getHeight(); |
| 402 | + |
| 403 | + } |
| 404 | + |
269 | 405 | public BowlerStudio3dEngine addListener(ICameraChangeListener listener) { |
270 | 406 | if (!listeners.contains(listener)) |
271 | 407 | listeners.add(listener); |
@@ -1498,6 +1634,11 @@ public void run() { |
1498 | 1634 | customWorkplaneGroup.getChildren().add(workplaneGroup); |
1499 | 1635 | } |
1500 | 1636 |
|
| 1637 | + // Count how many nodes are already present in the userGroup, they are not user objects |
| 1638 | + if (showAxes) |
| 1639 | + SKIP_USERGROUP_NODES = userGroup.getChildren().size(); |
| 1640 | + |
| 1641 | + // Create the world group |
1501 | 1642 | world.getChildren().addAll(lookGroup, cameraGroup, userGroup, axisGroup, customWorkplaneGroup, controlHandleGroup, ambientLight); |
1502 | 1643 |
|
1503 | 1644 | // Use ambient illumination for workplanes and axes, ruler is black so no need to illuminate |
@@ -1791,9 +1932,83 @@ public double objectDistance() { |
1791 | 1932 | } |
1792 | 1933 | */ |
1793 | 1934 |
|
| 1935 | + public double getCamDistanceToClosestObject() { |
| 1936 | + |
| 1937 | + Point3D camPos = camera.localToScene(0, 0, 0); |
| 1938 | + |
| 1939 | + // Adjust for camera rotation |
| 1940 | + camPos = new Point3D(-camPos.getX(), camPos.getY(), -camPos.getZ()); |
| 1941 | + Point3D camDir = camera.localToScene(0, 0, -1).subtract(camera.localToScene(0, 0, 0)).normalize(); |
| 1942 | + camDir = new Point3D(camDir.getX(), -camDir.getY(), camDir.getZ()); |
| 1943 | + |
| 1944 | + int counter = 0; |
| 1945 | + double minDist = Double.MAX_VALUE; |
| 1946 | + Point3D closestPoint = new Point3D(0, 0, 0); |
| 1947 | + |
| 1948 | + List<Node> children = userGroup.getChildren(); |
| 1949 | + for (int i = SKIP_USERGROUP_NODES; i < children.size(); i++) { |
| 1950 | + |
| 1951 | + Node node = children.get(i); |
| 1952 | + |
| 1953 | + // Find closest point to camera on bounding box |
| 1954 | + Bounds b = node.getBoundsInParent(); |
| 1955 | + double closestX = Math.max(b.getMinX(), Math.min(camPos.getX(), b.getMaxX())); |
| 1956 | + double closestY = Math.max(b.getMinY(), Math.min(camPos.getY(), b.getMaxY())); |
| 1957 | + double closestZ = Math.max(b.getMinZ(), Math.min(camPos.getZ(), b.getMaxZ())); |
| 1958 | + |
| 1959 | + Point3D boxPoint = new Point3D(closestX, closestY, closestZ); |
| 1960 | + |
| 1961 | + // Direction from camera to closest point on box |
| 1962 | + Point3D toObject = boxPoint.subtract(camPos); |
| 1963 | + double distToPoint = toObject.magnitude(); |
| 1964 | + |
| 1965 | + // Inside object - skip FOV check |
| 1966 | + if (distToPoint < 0.001) { |
| 1967 | + if (distToPoint < minDist) { |
| 1968 | + minDist = 0; |
| 1969 | + closestPoint = boxPoint; |
| 1970 | + } |
| 1971 | + continue; |
| 1972 | + } |
| 1973 | + |
| 1974 | + // Skip objects behind the camera (negative dot product) |
| 1975 | + double dot = camDir.dotProduct(toObject); |
| 1976 | + if (dot <= 0) |
| 1977 | + continue; |
| 1978 | + |
| 1979 | + // Check FOV with dot product, 60dg FOV, cos(30) |
| 1980 | + double cosAngle = dot / toObject.magnitude(); |
| 1981 | + if (cosAngle < Math.cos(Math.toRadians(30))) |
| 1982 | + continue; |
| 1983 | + |
| 1984 | + if (distToPoint < minDist) { |
| 1985 | + minDist = distToPoint; |
| 1986 | + closestPoint = boxPoint; |
| 1987 | + } |
| 1988 | + } |
| 1989 | + |
| 1990 | + return Math.max(2, minDist); |
| 1991 | + } |
| 1992 | + |
1794 | 1993 | public void zoomIncrement(double deltaY) { |
1795 | 1994 | double zoomFactor = -deltaY * getVirtualcam().getZoomDepth() / 500; |
1796 | | - // |
| 1995 | + |
| 1996 | +/* EXPERIMENTAL FEATURE, SLOW DOWN ZOOM WHEN CLOSE TO OBJECT |
| 1997 | +
|
| 1998 | + double distance = getCamDistanceToClosestObject(); |
| 1999 | +
|
| 2000 | + // Parameters to control zoom in behavior |
| 2001 | + final double ZOOM_IN_START_DISTANCE = 5; |
| 2002 | + final double ZOOM_IN_STEP_REDUCTION = 2; |
| 2003 | + if (ZOOM_IN_START_DISTANCE * zoomFactor > distance) |
| 2004 | + zoomFactor = distance / (ZOOM_IN_START_DISTANCE * ZOOM_IN_STEP_REDUCTION); |
| 2005 | +
|
| 2006 | + // Parameters to control zoom out behavior |
| 2007 | + final double ZOOM_OUT_START_DISTANCE = 3; |
| 2008 | + final double ZOOM_OUT_STEP_REDUCTION = 2; |
| 2009 | + if (-ZOOM_OUT_START_DISTANCE * zoomFactor > distance) |
| 2010 | + zoomFactor = -distance / (ZOOM_OUT_START_DISTANCE * ZOOM_OUT_STEP_REDUCTION); |
| 2011 | +*/ |
1797 | 2012 | // double z = camera.getTranslateY(); |
1798 | 2013 | // double newZ = z + zoomFactor; |
1799 | 2014 | // camera.setTranslateY(newZ); |
|
0 commit comments