Skip to content

Commit 14cbd9d

Browse files
committed
Materialization: Implement center of scene option
1 parent 71b425e commit 14cbd9d

4 files changed

Lines changed: 155 additions & 23 deletions

File tree

‎src/main/java/sc/fiji/snt/gui/MaterializeRegionDialog.java‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import sc.fiji.snt.util.BoundingBox;
3333
import sc.fiji.snt.util.PointInImage;
3434
import sc.fiji.snt.util.SNTPoint;
35+
import sc.fiji.snt.viewer.AbstractBigViewer;
3536

3637
import javax.swing.*;
3738
import java.awt.*;
@@ -73,7 +74,7 @@ public class MaterializeRegionDialog extends JDialog {
7374
}
7475

7576
private static final String CENTER_AUTO = "Auto";
76-
private static final String CENTER_SCENE = "Center of scene (not yet implemented; uses Auto)";
77+
private static final String CENTER_SCENE = "Center of scene (current view; falls back to Auto if unavailable)";
7778
private static final String CENTER_OTHER = "Other...";
7879

7980
// Wide enough for this dialog's own content (three X/Y/Z fields plus labels) without forcing the
@@ -86,6 +87,7 @@ public class MaterializeRegionDialog extends JDialog {
8687
// excluded: a thin/2D slab (e.g. depth 1) is a legitimate, intentional choice
8788
private static final long MIN_USEFUL_XY_PX = 100;
8889

90+
private final SNTUI ui;
8991
private final SNT plugin;
9092
private boolean succeeded;
9193
private BoundingBox resolvedBox;
@@ -117,6 +119,7 @@ public class MaterializeRegionDialog extends JDialog {
117119

118120
public MaterializeRegionDialog(final SNTUI ui, final SNT plugin) {
119121
super(ui, "Materialize Region...", true);
122+
this.ui = ui;
120123
this.plugin = plugin;
121124

122125
final List<String> scopeItems = new ArrayList<>(List.of(SCOPE_MAIN));
@@ -455,10 +458,17 @@ private SNTPoint resolveCenter(final StringBuilder errorOut) {
455458
return new PointInImage(centerFields.getValue(0, 0), centerFields.getValue(1, 0),
456459
centerFields.getValue(2, 0));
457460
}
458-
// CENTER_AUTO and the still-stubbed CENTER_SCENE (falls back to Auto until viewport-center math
459-
// is implemented) both resolve to the centroid of the selected paths if any are
460-
// selected, else every loaded path, mirroring materializeDisplayCanvas()'s own selected-else-all
461-
// fallback. Uses the same memoized boxes as the size section, so this never re-traverses paths
461+
if (CENTER_SCENE.equals(choice)) {
462+
final AbstractBigViewer viewer = ui.getActiveBigViewer();
463+
final SNTPoint center = (viewer != null && viewer.isOpen()) ? viewer.getSceneCenter() : null;
464+
if (center != null) return center;
465+
// No viewer open (or its viewport isn't realized yet): fall through to the same
466+
// path-centroid fallback used by CENTER_AUTO
467+
}
468+
// CENTER_AUTO, and CENTER_SCENE's fallback above, resolve to the centroid of the selected
469+
// paths if any are selected, else every loaded path, mirroring materializeDisplayCanvas()'s
470+
// own selected-else-all fallback. Uses the same memoized boxes as the size section, so this
471+
// never re-traverses paths
462472
final BoundingBox box = selectedPathsBox() != null ? selectedPathsBox() : allPathsBox();
463473
if (box == null) {
464474
errorOut.append("No paths exist to center the region on.");

‎src/main/java/sc/fiji/snt/viewer/AbstractBigViewer.java‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,28 @@ public boolean flyTo(final BoundingBox box) {
635635
return true;
636636
}
637637

638+
/**
639+
* The world-space point currently displayed at the center of this viewer, i.e. the inverse
640+
* of {@link #flyTo}'s centroid-to-screen-center mapping: applies the inverse viewer transform
641+
* to the viewport's center screen point, at the focal plane (viewer-space Z = 0).
642+
* <p>
643+
* Correct as-is for an orthogonal slicing view (the displayed slice <em>is</em> the focal
644+
* plane). {@link Bvv} overrides this to prefer the depth of whatever is actually visible
645+
* there (see {@link Bvv#findSceneCenterMaxima()}), falling back to this same focal-plane
646+
* result when that ray finds nothing.
647+
*
648+
* @return the world-space scene center, or {@code null} if the viewport has not yet been
649+
* realized
650+
*/
651+
public SNTPoint getSceneCenter() {
652+
final int cw = getViewerWidth(), ch = getViewerHeight();
653+
if (cw <= 0 || ch <= 0) return null;
654+
final AffineTransform3D t = getViewerTransform();
655+
final double[] world = new double[3];
656+
t.applyInverse(world, new double[]{cw / 2.0, ch / 2.0, 0});
657+
return new PointInImage(world[0], world[1], world[2]);
658+
}
659+
638660
/**
639661
* Creates and returns a new {@link sc.fiji.snt.BookmarkManager} for this viewer.
640662
* Called exactly once (lazily) by {@link #getMarkerManager()}.

‎src/main/java/sc/fiji/snt/viewer/Bvv.java‎

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2461,11 +2461,40 @@ public double[][] findClickRay() {
24612461
* source is loaded
24622462
*/
24632463
public double[] findClickRayMaxima() {
2464+
final double[][] ray = findClickRay();
2465+
return (ray == null) ? null : findRayMaxima(ray);
2466+
}
2467+
2468+
/**
2469+
* Finds the world-space point of maximum intensity along the perspective ray through the
2470+
* center of the viewer's display, i.e. whatever is actually visible at the middle of the
2471+
* screen right now, at whatever depth its signal peaks. Same search as
2472+
* {@link #findClickRayMaxima()}, but along the optical axis rather than through the mouse.
2473+
*
2474+
* @return world-space [x, y, z] of the intensity maximum, or null if the ray misses the
2475+
* volume, the viewer is unavailable, or no source is loaded
2476+
*/
2477+
public double[] findSceneCenterMaxima() {
2478+
final double[][] ray = findCenterRay();
2479+
return (ray == null) ? null : findRayMaxima(ray);
2480+
}
2481+
2482+
/**
2483+
* Shared ray-sampling logic behind {@link #findClickRayMaxima()} and
2484+
* {@link #findSceneCenterMaxima()}: samples the active source(s) along {@code ray} at
2485+
* 0.5-voxel intervals and refines the peak location to sub-voxel accuracy via a 3-point
2486+
* parabola fit, escalating the search radius (tight, then wide, then the full ray) until a
2487+
* significant peak is found.
2488+
*
2489+
* @param ray the near/far world-space endpoints to sample along, e.g. from
2490+
* {@link #findClickRay()} or {@link #findCenterRay()}
2491+
* @return world-space [x, y, z] of the intensity maximum, or null if no source is loaded
2492+
* or no peak is found anywhere along the ray
2493+
*/
2494+
private double[] findRayMaxima(final double[][] ray) {
24642495
if (pathOverlay == null) return null;
24652496
final double near = pathOverlay.overlayRenderer.nearClip;
24662497
final double far = pathOverlay.overlayRenderer.farClip;
2467-
final double[][] ray = findClickRay();
2468-
if (ray == null) return null;
24692498
final VolumeViewerPanel vp = getViewerPanel();
24702499
if (vp == null) return null;
24712500
final int tp = vp.state().getCurrentTimepoint();
@@ -2482,7 +2511,7 @@ public double[] findClickRayMaxima() {
24822511
? java.util.Collections.singleton(current)
24832512
: visible;
24842513
if (SNTUtils.isDebugMode()) {
2485-
SNTUtils.log("BVV findClickRayMaxima: focalT=" + focalT + " near=" + near + " far=" + far
2514+
SNTUtils.log("BVV findRayMaxima: focalT=" + focalT + " near=" + near + " far=" + far
24862515
+ " sampling " + toSample.size() + " source(s)"
24872516
+ (current != null ? " (current-only: " + current.getSpimSource().getName() + ")" : ""));
24882517
}
@@ -2516,13 +2545,40 @@ public double[] findClickRayMaxima() {
25162545
}
25172546
final double[] peak = (result == null) ? null : new double[]{result[0], result[1], result[2]};
25182547
if (SNTUtils.isDebugMode()) {
2519-
SNTUtils.log("BVV findClickRayMaxima: final peak = "
2548+
SNTUtils.log("BVV findRayMaxima: final peak = "
25202549
+ (result == null ? "null" : String.format("(%.3f,%.3f,%.3f) normVal=%.4f",
25212550
result[0], result[1], result[2], result[3])));
25222551
}
25232552
return peak;
25242553
}
25252554

2555+
/**
2556+
* Returns the world-space endpoints of the perspective ray through the center of the
2557+
* viewer's display (its optical axis). Unlike {@link #findClickRay()}, this does not
2558+
* depend on the mouse cursor.
2559+
*
2560+
* @return a 2x3 array { nearWorld, farWorld } in world coordinates, or null if the viewer
2561+
* state is unavailable
2562+
*/
2563+
public double[][] findCenterRay() {
2564+
if (pathOverlay == null) return null;
2565+
final double dCam = pathOverlay.overlayRenderer.dCam;
2566+
final double near = pathOverlay.overlayRenderer.nearClip;
2567+
final double far = pathOverlay.overlayRenderer.farClip;
2568+
return BvvUtils.findCenterRay(getViewerPanel(), dCam, near, far);
2569+
}
2570+
2571+
/**
2572+
* The world-space point currently at the center of this viewer's display: the intensity-peak
2573+
* result of {@link #findSceneCenterMaxima()} when available, otherwise the same focal-plane
2574+
* fallback used by {@link AbstractBigViewer#getSceneCenter()}.
2575+
*/
2576+
@Override
2577+
public SNTPoint getSceneCenter() {
2578+
final double[] peak = findSceneCenterMaxima();
2579+
return (peak == null) ? super.getSceneCenter() : new PointInImage(peak[0], peak[1], peak[2]);
2580+
}
2581+
25262582
/**
25272583
* Searches all of {@code toSample} for the highest-value ray-max peak within {@code searchRadiusVoxels}.
25282584
*

‎src/main/java/sc/fiji/snt/viewer/BvvUtils.java‎

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ static AxisOrder getAxisOrder(final ImagePlus imp) {
169169
}
170170

171171
/**
172-
* Returns the world-space endpoints of the perspective ray through the
173-
* current mouse cursor position in the given viewer panel.
172+
* Returns the world-space endpoints of the perspective ray through an arbitrary screen
173+
* point in the given viewer panel.
174174
*
175175
* <p>BVV uses the formula {@code pf = dCam / (dCam + viewerZ)} to project
176176
* world points onto the screen. Inverting that relationship gives the viewer-space
@@ -187,29 +187,29 @@ static AxisOrder getAxisOrder(final ImagePlus imp) {
187187
* @param dCam camera distance in screen-pixel units (from OverlayRenderer.dCam)
188188
* @param nearClip near clip distance in screen-pixel units (>0, <dCam)
189189
* @param farClip far clip distance in screen-pixel units (>0)
190+
* @param screenX screen-space X of the point the ray passes through
191+
* @param screenY screen-space Y of the point the ray passes through
190192
* @return a 2x3 array { nearWorld, farWorld } in world coordinates, or null
191-
* if the mouse is outside the display or the viewer state is unavailable
193+
* if the viewer state is unavailable
192194
*/
193-
static double[][] findClickRay(final VolumeViewerPanel vp,
195+
private static double[][] rayFromScreenPoint(final VolumeViewerPanel vp,
194196
final double dCam,
195197
final double nearClip,
196-
final double farClip) {
198+
final double farClip,
199+
final double screenX,
200+
final double screenY) {
197201
if (vp == null || dCam <= 0) return null;
198202

199-
// Mouse position in display coordinates.
200-
final Point mouse = vp.getDisplay().getComponent().getMousePosition();
201-
if (mouse == null) return null;
202-
203203
final AffineTransform3D t = new AffineTransform3D();
204204
vp.state().getViewerTransform(t);
205205

206206
// Screen center = perspective vanishing point.
207207
final double cx = vp.getDisplay().getWidth() / 2.0;
208208
final double cy = vp.getDisplay().getHeight() / 2.0;
209209

210-
// Offset of the cursor from screen center.
211-
final double dx = mouse.x - cx;
212-
final double dy = mouse.y - cy;
210+
// Offset of the requested point from screen center.
211+
final double dx = screenX - cx;
212+
final double dy = screenY - cy;
213213

214214
// Viewer-space X,Y scale factor at each clip plane:
215215
// at near (viewerZ = -nearClip): scale = 1 - nearClip/dCam
@@ -229,15 +229,59 @@ static double[][] findClickRay(final VolumeViewerPanel vp,
229229

230230
if (SNTUtils.isDebugMode()) {
231231
SNTUtils.log(String.format(
232-
"BVV click-ray: mouse=(%d,%d) center=(%.1f,%.1f) dCam=%.1f near=%.1f far=%.1f%n"
232+
"BVV ray: screen=(%.1f,%.1f) center=(%.1f,%.1f) dCam=%.1f near=%.1f far=%.1f%n"
233233
+ " nearW=(%.3f,%.3f,%.3f) farW=(%.3f,%.3f,%.3f)",
234-
mouse.x, mouse.y, cx, cy, dCam, nearClip, farClip,
234+
screenX, screenY, cx, cy, dCam, nearClip, farClip,
235235
nearW[0], nearW[1], nearW[2], farW[0], farW[1], farW[2]));
236236
}
237237

238238
return new double[][]{nearW, farW};
239239
}
240240

241+
/**
242+
* Returns the world-space endpoints of the perspective ray through the
243+
* current mouse cursor position in the given viewer panel.
244+
*
245+
* @param vp the BVV viewer panel
246+
* @param dCam camera distance in screen-pixel units (from OverlayRenderer.dCam)
247+
* @param nearClip near clip distance in screen-pixel units (>0, <dCam)
248+
* @param farClip far clip distance in screen-pixel units (>0)
249+
* @return a 2x3 array { nearWorld, farWorld } in world coordinates, or null
250+
* if the mouse is outside the display or the viewer state is unavailable
251+
*/
252+
static double[][] findClickRay(final VolumeViewerPanel vp,
253+
final double dCam,
254+
final double nearClip,
255+
final double farClip) {
256+
if (vp == null) return null;
257+
final Point mouse = vp.getDisplay().getComponent().getMousePosition();
258+
if (mouse == null) return null;
259+
return rayFromScreenPoint(vp, dCam, nearClip, farClip, mouse.x, mouse.y);
260+
}
261+
262+
/**
263+
* Returns the world-space endpoints of the perspective ray through the center of the
264+
* viewer panel's display, i.e. its optical axis. Unlike {@link #findClickRay}, this
265+
* does not depend on the mouse cursor and is defined even when the pointer is outside
266+
* the display (or the viewer has no focus).
267+
*
268+
* @param vp the BVV viewer panel
269+
* @param dCam camera distance in screen-pixel units (from OverlayRenderer.dCam)
270+
* @param nearClip near clip distance in screen-pixel units (>0, <dCam)
271+
* @param farClip far clip distance in screen-pixel units (>0)
272+
* @return a 2x3 array { nearWorld, farWorld } in world coordinates, or null
273+
* if the viewer state is unavailable
274+
*/
275+
static double[][] findCenterRay(final VolumeViewerPanel vp,
276+
final double dCam,
277+
final double nearClip,
278+
final double farClip) {
279+
if (vp == null) return null;
280+
final double cx = vp.getDisplay().getWidth() / 2.0;
281+
final double cy = vp.getDisplay().getHeight() / 2.0;
282+
return rayFromScreenPoint(vp, dCam, nearClip, farClip, cx, cy);
283+
}
284+
241285
/**
242286
* Returns the mip level BVV would choose for the focal plane, replicating
243287
* BVV's own MipmapSizes.bestLevel logic exactly.

0 commit comments

Comments
 (0)