Skip to content

Commit 45f6bf7

Browse files
Merge pull request #476 from rondlh/development
Add support function for direct mouse control, experimental zoom control
2 parents 0cea7f9 + 261b2ec commit 45f6bf7

1 file changed

Lines changed: 217 additions & 2 deletions

File tree

src/main/java/com/neuronrobotics/bowlerstudio/threed/BowlerStudio3dEngine.java

Lines changed: 217 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import com.neuronrobotics.sdk.addons.kinematics.math.TransformNR;
5555
import com.neuronrobotics.sdk.common.Log;
5656
import eu.mihosoft.vrl.v3d.CSG;
57+
import eu.mihosoft.vrl.v3d.Vector3d;
5758
import eu.mihosoft.vrl.v3d.Cylinder;
5859
import eu.mihosoft.vrl.v3d.JavaFXInitializer;
5960
import eu.mihosoft.vrl.v3d.MissingManipulatorException;
@@ -68,6 +69,7 @@
6869
//import javafx.embed.swing.SwingFXUtils;
6970
import javafx.event.ActionEvent;
7071
import javafx.event.EventHandler;
72+
import javafx.geometry.Point2D;
7173
import javafx.geometry.Point3D;
7274
import javafx.scene.*;
7375
import javafx.scene.control.*;
@@ -84,6 +86,7 @@
8486
import javafx.scene.input.MouseEvent;
8587
import javafx.scene.input.ScrollEvent;
8688
import javafx.scene.input.PickResult;
89+
import javafx.scene.layout.Pane;
8790
import javafx.scene.layout.AnchorPane;
8891
import javafx.scene.layout.HBox;
8992
import javafx.scene.paint.*;
@@ -99,6 +102,8 @@
99102
import javafx.scene.transform.Rotate;
100103
import javafx.scene.transform.Scale;
101104
import javafx.scene.transform.Transform;
105+
import javafx.scene.transform.NonInvertibleTransformException;
106+
import javafx.geometry.Bounds;
102107

103108
// Development, for objectDistance methode
104109
//import com.sun.javafx.geom.PickRay;
@@ -206,7 +211,9 @@ public class BowlerStudio3dEngine implements ICameraChangeListener, IMobileBaseU
206211

207212
/** The user group for the user defined objects and the navigation cube */
208213
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+
210217
/** The scene. */
211218
private SubScene scene;
212219

@@ -266,6 +273,135 @@ public class BowlerStudio3dEngine implements ICameraChangeListener, IMobileBaseU
266273
private AmbientLight ambientLight = new AmbientLight(Color.color(1.0, 1.0, 1.0, 0));
267274
private volatile boolean waitingForCompletion;
268275

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+
269405
public BowlerStudio3dEngine addListener(ICameraChangeListener listener) {
270406
if (!listeners.contains(listener))
271407
listeners.add(listener);
@@ -1498,6 +1634,11 @@ public void run() {
14981634
customWorkplaneGroup.getChildren().add(workplaneGroup);
14991635
}
15001636

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
15011642
world.getChildren().addAll(lookGroup, cameraGroup, userGroup, axisGroup, customWorkplaneGroup, controlHandleGroup, ambientLight);
15021643

15031644
// Use ambient illumination for workplanes and axes, ruler is black so no need to illuminate
@@ -1791,9 +1932,83 @@ public double objectDistance() {
17911932
}
17921933
*/
17931934

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+
17941993
public void zoomIncrement(double deltaY) {
17951994
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+
*/
17972012
// double z = camera.getTranslateY();
17982013
// double newZ = z + zoomFactor;
17992014
// camera.setTranslateY(newZ);

0 commit comments

Comments
 (0)