Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions modules/javafx.graphics/src/main/java/javafx/scene/Camera.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -324,8 +324,24 @@ private void doUpdatePeer() {
}
}

/*
* Both parallel and perspective projections introduce denominators derived from the view dimensions.
* If a view dimension is zero, these divisions can produce NaN or Infinity. For example, the parallel
* projection computes terms proportional to 1/viewWidth and 1/viewHeight, and the perspective projection
* depends on the aspect ratio viewWidth/viewHeight. Once a transform contains NaN or Infinity, downstream
* coordinate conversion APIs that rely on these transforms (such as Node.localToScreen()) can return
* unusable results.
*
* The intent here is not to define semantics for a mathematically degenerate 0x0 view, but to prevent the
* transform pipeline from being poisoned by NaN or Infinity. For this purpose, we clamp the view width
* and height to ulp(1), guaranteeing non-zero view dimensions while keeping the clamp very small.
*/
private static double safeSize(double size) {
return Math.max(Math.ulp(1.0), size);
}

void setViewWidth(double width) {
this.viewWidth = width;
this.viewWidth = safeSize(width);
NodeHelper.markDirty(this, DirtyBits.NODE_CAMERA);
}

Expand All @@ -334,7 +350,7 @@ void setViewWidth(double width) {
}

void setViewHeight(double height) {
this.viewHeight = height;
this.viewHeight = safeSize(height);
NodeHelper.markDirty(this, DirtyBits.NODE_CAMERA);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -76,11 +76,22 @@ public TestScene(final Parent root) {
this("SCENE", root);
}

public TestScene(final Parent root, final double width, final double height) {
this("SCENE", root, width, height);
}

public TestScene(final String name, final Parent root) {
super(root);
this(name, root, -1, -1);
}

public TestScene(final String name, final Parent root, final double width, final double height) {
super(root, width, height);
this.name = name;

// init size for camera to work
SceneHelper.preferredSize(this);
if (width < 0 || height < 0) {
SceneHelper.preferredSize(this);
}
}

public void set_window(final Window window) {
Expand Down
173 changes: 119 additions & 54 deletions modules/javafx.graphics/src/test/java/test/javafx/scene/NodeTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -82,6 +82,8 @@
import javafx.stage.Stage;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
Expand Down Expand Up @@ -1349,14 +1351,20 @@ public void testSynchronizationOfInvisibleNodes_2_withClip() {
// the Circle
}

@Test
public void testLocalToScreen() {
@ParameterizedTest
@CsvSource(textBlock = """
-1, -1
0, 0
1, 1
100, 100
""")
public void testLocalToScreen(double sceneWidth, double sceneHeight) {
Rectangle rect = new Rectangle();

rect.setTranslateX(10);
rect.setTranslateY(20);

TestScene scene = new TestScene(new Group(rect));
TestScene scene = new TestScene(new Group(rect), sceneWidth, sceneHeight);
final TestStage testStage = new TestStage("");
testStage.setX(100);
testStage.setY(200);
Expand All @@ -1371,39 +1379,53 @@ public void testLocalToScreen() {
assertEquals(4.0, b.getHeight(), 0.0001);
}

@Test
public void testLocalToScreen3D() {
@ParameterizedTest
@CsvSource(textBlock = """
-1, -1, 111.420, 223.140, 110.661, 221.080, 1.879, 4.300
0, 0, 100.000, 200.000, 100.000, 200.000, 0.000, 0.000
1, 1, 94.248, 187.699, 93.653, 186.508, 9.973, 20.379
100, 100, 109.926, 221.229, 109.926, 221.229, 3.065, 3.449
""")
public void testLocalToScreen3D(double sceneWidth, double sceneHeight,
double x, double y,
double minX, double minY,
double width, double height) {
Box box = new Box(10, 10, 10);

box.setTranslateX(10);
box.setTranslateY(20);

TestScene scene = new TestScene(new Group(box));
TestScene scene = new TestScene(new Group(box), sceneWidth, sceneHeight);
scene.setCamera(new PerspectiveCamera());
final TestStage testStage = new TestStage("");
testStage.setX(100);
testStage.setY(200);
scene.set_window(testStage);

Point2D p = box.localToScreen(new Point3D(1, 2, -5));
assertEquals(111.42, p.getX(), 0.1);
assertEquals(223.14, p.getY(), 0.1);
Bounds b = box.localToScreen(new BoundingBox(1, 2, -5, 1, 2, 10));
assertEquals(110.66, b.getMinX(), 0.1);
assertEquals(221.08, b.getMinY(), 0.1);
assertEquals(1.88, b.getWidth(), 0.1);
assertEquals(4.3, b.getHeight(), 0.1);
assertEquals(0.0, b.getDepth(), 0.0001);
}
assertEquals(x, p.getX(), 0.0005);
assertEquals(y, p.getY(), 0.0005);

@Test
public void testScreenToLocal() {
Bounds b = box.localToScreen(new BoundingBox(1, 2, -5, 1, 2, 10));
assertEquals(minX, b.getMinX(), 0.0005);
assertEquals(minY, b.getMinY(), 0.0005);
assertEquals(width, b.getWidth(), 0.0005);
assertEquals(height, b.getHeight(), 0.0005);
assertEquals(0, b.getDepth(), 0.00005);
}

@ParameterizedTest
@CsvSource(textBlock = """
-1, -1
0, 0
1, 1
100, 100
""")
public void testScreenToLocal(double sceneWidth, double sceneHeight) {
Rectangle rect = new Rectangle();

rect.setTranslateX(10);
rect.setTranslateY(20);

TestScene scene = new TestScene(new Group(rect));
TestScene scene = new TestScene(new Group(rect), sceneWidth, sceneHeight);
final TestStage testStage = new TestStage("");
testStage.setX(100);
testStage.setY(200);
Expand All @@ -1413,15 +1435,20 @@ public void testScreenToLocal() {
assertEquals(new BoundingBox(1, 2, 3, 4), rect.screenToLocal(new BoundingBox(111, 222, 3, 4)));
}

@Test
public void testLocalToScreenWithTranslatedCamera() {
@ParameterizedTest
@CsvSource(textBlock = """
-1, -1
0, 0
1, 1
100, 100
""")
public void testLocalToScreenWithTranslatedCamera(double sceneWidth, double sceneHeight) {
Rectangle rect = new Rectangle();

rect.setTranslateX(10);
rect.setTranslateY(20);

ParallelCamera cam = new ParallelCamera();
TestScene scene = new TestScene(new Group(rect, cam));
TestScene scene = new TestScene(new Group(rect, cam), sceneWidth, sceneHeight);
scene.setCamera(cam);
final TestStage testStage = new TestStage("");
testStage.setX(100);
Expand All @@ -1440,15 +1467,21 @@ public void testLocalToScreenWithTranslatedCamera() {
assertEquals(4.0, b.getHeight(), 0.0001);
}

@Test
public void testScreenToLocalWithTranslatedCamera() {
@ParameterizedTest
@CsvSource(textBlock = """
-1, -1
0, 0
1, 1
100, 100
""")
public void testScreenToLocalWithTranslatedCamera(double sceneWidth, double sceneHeight) {
Rectangle rect = new Rectangle();

rect.setTranslateX(10);
rect.setTranslateY(20);

ParallelCamera cam = new ParallelCamera();
TestScene scene = new TestScene(new Group(rect, cam));
TestScene scene = new TestScene(new Group(rect, cam), sceneWidth, sceneHeight);
scene.setCamera(cam);
final TestStage testStage = new TestStage("");
testStage.setX(100);
Expand All @@ -1461,12 +1494,18 @@ public void testScreenToLocalWithTranslatedCamera() {
assertEquals(new BoundingBox(31, 22, 3, 4), rect.screenToLocal(new BoundingBox(111, 222, 3, 4)));
}

@Test
public void testLocalToScreenInsideSubScene() {
@ParameterizedTest
@CsvSource(textBlock = """
0, 0
1, 1
100, 100
""")
public void testLocalToScreenInsideSubScene(double subSceneWidth, double subSceneHeight) {
Rectangle rect = new Rectangle();
rect.setTranslateX(4);
rect.setTranslateY(9);
SubScene subScene = new SubScene(new Group(rect), 100, 100);

SubScene subScene = new SubScene(new Group(rect), subSceneWidth, subSceneHeight);
subScene.setTranslateX(6);
subScene.setTranslateY(11);

Expand All @@ -1486,12 +1525,18 @@ public void testLocalToScreenInsideSubScene() {
assertEquals(4.0, b.getHeight(), 0.0001);
}

@Test
public void testScreenToLocalInsideSubScene() {
@ParameterizedTest
@CsvSource(textBlock = """
0, 0
1, 1
100, 100
""")
public void testScreenToLocalInsideSubScene(double subSceneWidth, double subSceneHeight) {
Rectangle rect = new Rectangle();
rect.setTranslateX(4);
rect.setTranslateY(9);
SubScene subScene = new SubScene(new Group(rect), 100, 100);

SubScene subScene = new SubScene(new Group(rect), subSceneWidth, subSceneHeight);
subScene.setTranslateX(6);
subScene.setTranslateY(11);

Expand All @@ -1505,12 +1550,21 @@ public void testScreenToLocalInsideSubScene() {
assertEquals(new BoundingBox(1, 2, 3, 4), rect.screenToLocal(new BoundingBox(111, 222, 3, 4)));
}

@Test
public void test2DLocalToScreenOn3DRotatedSubScene() {
@ParameterizedTest
@CsvSource(textBlock = """
0, 0, 111.445, 226.429, 111.445, 226.429, 4.670, 9.007
1, 1, 111.122, 225.433, 111.122, 225.433, 4.179, 8.003
100, 100, 124.365, 225.996, 124.365, 225.755, 1.851, 3.757
""")
public void test2DLocalToScreenOn3DRotatedSubScene(double subSceneWidth, double subSceneHeight,
double x, double y,
double minX, double minY,
double width, double height) {
Rectangle rect = new Rectangle();
rect.setTranslateX(5);
rect.setTranslateY(10);
SubScene subScene = new SubScene(new Group(rect), 100, 100);

SubScene subScene = new SubScene(new Group(rect), subSceneWidth, subSceneHeight);
subScene.setTranslateX(5);
subScene.setTranslateY(10);
subScene.setRotationAxis(Rotate.Y_AXIS);
Expand All @@ -1524,21 +1578,31 @@ public void test2DLocalToScreenOn3DRotatedSubScene() {
scene.set_window(testStage);

Point2D p = rect.localToScreen(new Point2D(1, 2));
assertEquals(124.36, p.getX(), 0.1);
assertEquals(226.0, p.getY(), 0.1);
Bounds b = rect.localToScreen(new BoundingBox(1, 2, 3, 4));
assertEquals(124.36, b.getMinX(), 0.1);
assertEquals(225.75, b.getMinY(), 0.1);
assertEquals(1.85, b.getWidth(), 0.1);
assertEquals(3.76, b.getHeight(), 0.1);
}
assertEquals(x, p.getX(), 0.0005);
assertEquals(y, p.getY(), 0.0005);

@Test
public void test2DScreenToLocalTo3DRotatedSubScene() {
Bounds b = rect.localToScreen(new BoundingBox(1, 2, 3, 4));
assertEquals(minX, b.getMinX(), 0.0005);
assertEquals(minY, b.getMinY(), 0.0005);
assertEquals(width, b.getWidth(), 0.0005);
assertEquals(height, b.getHeight(), 0.0005);
}

@ParameterizedTest
@CsvSource(textBlock = """
0, 0, 7.745, -3.219, 7.745, -3.828, 0.656, 2.578
1, 1, 8.627, -2.427, 8.627, -3.036, 0.729, 2.677
100, 100, 0.992, 2.003, 0.992, 1.719, 2.998, 4.518
""")
public void test2DScreenToLocalTo3DRotatedSubScene(double subSceneWidth, double subSceneHeight,
double x, double y,
double minX, double minY,
double width, double height) {
Rectangle rect = new Rectangle();
rect.setTranslateX(5);
rect.setTranslateY(10);
SubScene subScene = new SubScene(new Group(rect), 100, 100);

SubScene subScene = new SubScene(new Group(rect), subSceneWidth, subSceneHeight);
subScene.setTranslateX(5);
subScene.setTranslateY(10);
subScene.setRotationAxis(Rotate.Y_AXIS);
Expand All @@ -1552,13 +1616,14 @@ public void test2DScreenToLocalTo3DRotatedSubScene() {
scene.set_window(testStage);

Point2D p = rect.screenToLocal(new Point2D(124.36, 226.0));
assertEquals(1, p.getX(), 0.1);
assertEquals(2, p.getY(), 0.1);
assertEquals(x, p.getX(), 0.0005);
assertEquals(y, p.getY(), 0.0005);

Bounds b = rect.screenToLocal(new BoundingBox(124.36, 225.75, 1.85, 3.76));
assertEquals(1, b.getMinX(), 0.1);
assertEquals(1.72, b.getMinY(), 0.1);
assertEquals(3, b.getWidth(), 0.1);
assertEquals(4.52, b.getHeight(), 0.1);
assertEquals(minX, b.getMinX(), 0.0005);
assertEquals(minY, b.getMinY(), 0.0005);
assertEquals(width, b.getWidth(), 0.0005);
assertEquals(height, b.getHeight(), 0.0005);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we could declare a static constant EPSILON and use it at least in the modified tests?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there are many different epsilons used in this test class, I'd have to call it something specific like EPSILON_0005, and at that point, there's little difference between a numeric literal and a constant field. It's a bit like saying "ONE" instead of "1".

}

@Test
Expand Down