Skip to content

Commit 86204a9

Browse files
authored
Merge pull request #622 from scenerygraphics/add-window-size-api
Add API for setting custom window dimensions
2 parents 0633475 + f31ff72 commit 86204a9

File tree

6 files changed

+255
-10
lines changed

6 files changed

+255
-10
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,29 @@ Should you experience any issues, [please try the latest development version](ht
2222

2323
![Overview of sciview's user interface](https://gblobscdn.gitbook.com/assets%2F-LqBCy3SBefXis0YnrcI%2F-MK5WLQvMLIvw2GF6Rn2%2F-MK5WMGzmSavDTwlGro2%2Fmain-cheatsheet.jpg?alt=media&token=70c82549-e939-4752-af12-1756492a5f01)
2424

25+
## API Features
26+
27+
### Custom Window Dimensions
28+
29+
SciView now supports setting custom window dimensions via API, which is essential for VR headsets that require specific resolutions:
30+
31+
```kotlin
32+
// Create SciView with custom dimensions
33+
val sciview = SciView.create(1920, 1080)
34+
35+
// Or resize an existing instance
36+
sciview.setWindowSize(2880, 1700) // Example: Oculus Quest 2 resolution
37+
38+
// Query current dimensions
39+
val (width, height) = sciview.getWindowSize()
40+
```
41+
42+
This feature is particularly useful for:
43+
- VR headset integration requiring exact resolutions
44+
- Multi-monitor setups
45+
- Creating screenshots or recordings at specific resolutions
46+
- Kiosk or presentation modes
47+
2548
## Developers
2649

2750
[Kyle Harrington](https://kyleharrington.com), University of Idaho & [Ulrik Guenther](https://ulrik.is/writing), MPI-CBG

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ jvmTarget=21
66
#useLocalScenery=true
77
kotlinVersion=2.2.10
88
dokkaVersion=1.9.20
9-
scijavaParentPOMVersion=40.0.0
9+
scijavaParentPOMVersion=43.0.0
1010
version=0.4.1-SNAPSHOT
1111

1212
# update site configuration

src/main/kotlin/sc/iview/SciView.kt

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@ import graphics.scenery.controls.TrackerInput
5050
import graphics.scenery.primitives.*
5151
import graphics.scenery.proteins.Protein
5252
import graphics.scenery.proteins.RibbonDiagram
53-
import graphics.scenery.utils.ExtractsNatives
54-
import graphics.scenery.utils.ExtractsNatives.Companion.getPlatform
5553
import graphics.scenery.utils.LogbackUtils
5654
import graphics.scenery.utils.SceneryPanel
5755
import graphics.scenery.utils.Statistics
@@ -84,8 +82,8 @@ import net.imglib2.type.numeric.RealType
8482
import net.imglib2.type.numeric.integer.UnsignedByteType
8583
import net.imglib2.view.Views
8684
import org.joml.Quaternionf
85+
import org.joml.Vector2f
8786
import org.joml.Vector3f
88-
import org.joml.Vector4f
8987
import org.scijava.Context
9088
import org.scijava.`object`.ObjectService
9189
import org.scijava.display.Display
@@ -100,8 +98,6 @@ import org.scijava.service.SciJavaService
10098
import org.scijava.thread.ThreadService
10199
import org.scijava.util.ColorRGB
102100
import org.scijava.util.Colors
103-
import org.scijava.util.VersionUtils
104-
import sc.iview.commands.demo.animation.ParticleDemo
105101
import sc.iview.commands.edit.InspectorInteractiveCommand
106102
import sc.iview.event.NodeActivatedEvent
107103
import sc.iview.event.NodeAddedEvent
@@ -131,12 +127,10 @@ import java.util.function.Predicate
131127
import java.util.stream.Collectors
132128
import kotlin.collections.ArrayList
133129
import kotlin.collections.HashMap
134-
import kotlin.collections.LinkedHashMap
135130
import kotlin.concurrent.thread
136131
import javax.swing.JOptionPane
137132
import kotlin.math.cos
138133
import kotlin.math.sin
139-
import kotlin.system.measureTimeMillis
140134

141135
/**
142136
* Main SciView class.
@@ -1689,22 +1683,28 @@ class SciView : SceneryBase, CalibratedRealInterval<CalibratedAxis> {
16891683
}
16901684

16911685
private var originalFOV = camera?.fov
1686+
private var originalWinSize = getWindowSize()
16921687

16931688
/**
1694-
* Enable VR rendering
1689+
* Enable or disable VR rendering. Automatically stores the original controls and FOV and restores them
1690+
* after VR is toggled off again.
1691+
* @param resizeWindow changes the window resolution to match the stereo rendering of the selected headset.
1692+
* @param resolutionScale Factor that allows changing the VR resolution
16951693
*/
1696-
fun toggleVRRendering() {
1694+
fun toggleVRRendering(resizeWindow: Boolean = true, resolutionScale: Float = 1f) {
16971695
var renderer = renderer ?: return
16981696

16991697
// Save camera's original settings if we switch from 2D to VR
17001698
if (!vrActive) {
17011699
originalFOV = camera?.fov
1700+
originalWinSize = getWindowSize()
17021701
}
17031702

17041703
// If turning off VR, store the controls state before deactivating
17051704
if (vrActive) {
17061705
// We're about to turn off VR
17071706
controls.stashControls()
1707+
setWindowSize(originalWinSize.first, originalWinSize.second)
17081708
}
17091709

17101710
vrActive = !vrActive
@@ -1721,6 +1721,20 @@ class SciView : SceneryBase, CalibratedRealInterval<CalibratedAxis> {
17211721
if (hmd.initializedAndWorking()) {
17221722
hub.add(SceneryElement.HMDInput, hmd)
17231723
ti = hmd
1724+
// Disable the sidebar if it was still open
1725+
if ((mainWindow as SwingMainWindow).sidebarOpen) {
1726+
toggleSidebar()
1727+
}
1728+
if (resizeWindow) {
1729+
val perEyeResolution = hmd.getRenderTargetSize()
1730+
// Recommended resolution is about x1.33 larger than the actual headset resolution
1731+
// due to distortion compensation.
1732+
// Too high resolution gets in the way of volume rendering, so we scale it down a bit again
1733+
setWindowSize(
1734+
(perEyeResolution.x * 2f / 1.33f * resolutionScale).toInt(),
1735+
(perEyeResolution.y / 1.33f * resolutionScale).toInt()
1736+
)
1737+
}
17241738
} else {
17251739
logger.warn("Could not initialise VR headset, just activating stereo rendering.")
17261740
}
@@ -1916,6 +1930,57 @@ class SciView : SceneryBase, CalibratedRealInterval<CalibratedAxis> {
19161930
println(scijavaContext!!.serviceIndex)
19171931
}
19181932

1933+
/**
1934+
* Set the window dimensions of the sciview rendering window.
1935+
* This is essential for VR headsets that require specific resolutions.
1936+
*
1937+
* @param width The desired width of the window in pixels
1938+
* @param height The desired height of the window in pixels
1939+
* @return true if the window was successfully resized, false otherwise
1940+
*/
1941+
fun setWindowSize(width: Int, height: Int): Boolean {
1942+
if (width <= 0 || height <= 0) {
1943+
log.error("Window dimensions must be positive: width=$width, height=$height")
1944+
return false
1945+
}
1946+
1947+
try {
1948+
// Update internal dimensions
1949+
windowWidth = width
1950+
windowHeight = height
1951+
1952+
// Update the main window frame if it exists
1953+
if (mainWindow is SwingMainWindow) {
1954+
val swingWindow = mainWindow as SwingMainWindow
1955+
val scale = getScenerySettings().get("Renderer.SurfaceScale") ?: Vector2f(1f)
1956+
val scaledWidth = (width / scale.x()).toInt()
1957+
val scaleHeight = (height / scale.y()).toInt()
1958+
// We need to scale the swing window with taking the surface scale into account
1959+
swingWindow.frame.setSize(scaledWidth, scaleHeight)
1960+
1961+
// Update the renderer dimensions
1962+
// TODO Is this even needed? Since outdated semaphores will trigger a swapchain recreation anyway
1963+
renderer?.reshape(width, height)
1964+
}
1965+
1966+
log.info("Window resized to ${width}x${height}")
1967+
return true
1968+
} catch (e: Exception) {
1969+
log.error("Failed to resize window: ${e.message}")
1970+
e.printStackTrace()
1971+
return false
1972+
}
1973+
}
1974+
1975+
/**
1976+
* Get the current window dimensions.
1977+
*
1978+
* @return a Pair containing the width and height of the window
1979+
*/
1980+
fun getWindowSize(): Pair<Int, Int> {
1981+
return Pair(windowWidth, windowHeight)
1982+
}
1983+
19191984
/**
19201985
* Return the color table corresponding to the [lutName]
19211986
* @param lutName a String represening an ImageJ style LUT name, like Fire.lut
@@ -2002,6 +2067,26 @@ class SciView : SceneryBase, CalibratedRealInterval<CalibratedAxis> {
20022067
val sciViewService = context.service(SciViewService::class.java)
20032068
return sciViewService.orCreateActiveSciView
20042069
}
2070+
2071+
/**
2072+
* Static launching method with custom window dimensions
2073+
*
2074+
* @param width The desired width of the window in pixels
2075+
* @param height The desired height of the window in pixels
2076+
* @return a newly created SciView with specified dimensions
2077+
*/
2078+
@JvmStatic
2079+
@Throws(Exception::class)
2080+
fun create(width: Int, height: Int): SciView {
2081+
xinitThreads()
2082+
val context = Context(ImageJService::class.java, SciJavaService::class.java, SCIFIOService::class.java)
2083+
val objectService = context.service(ObjectService::class.java)
2084+
objectService.addObject(Utils.SciviewStandalone())
2085+
val sciViewService = context.service(SciViewService::class.java)
2086+
val sciView = sciViewService.orCreateActiveSciView
2087+
sciView.setWindowSize(width, height)
2088+
return sciView
2089+
}
20052090

20062091
/**
20072092
* Static launching method

src/main/kotlin/sc/iview/commands/MenuWeights.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ object MenuWeights {
109109
const val DEMO_BASIC_IMAGEPLANE = 4.0
110110
const val DEMO_BASIC_VOLUME = 6.0
111111
const val DEMO_BASIC_POINTCLOUD = 7.0
112+
const val DEMO_BASIC_CUSTOM_WINDOW = 8.0
112113
// Demo/Animation
113114
const val DEMO_ANIMATION_PARTICLE = 0.0
114115
const val DEMO_ANIMATION_VOLUMETIMESERIES = 1.0
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*-
2+
* #%L
3+
* Scenery-backed 3D visualization package for ImageJ.
4+
* %%
5+
* Copyright (C) 2016 - 2024 sciview developers.
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
* #L%
28+
*/
29+
package sc.iview.commands.demo.basic
30+
31+
import org.joml.Vector3f
32+
import org.scijava.command.Command
33+
import org.scijava.plugin.Menu
34+
import org.scijava.plugin.Parameter
35+
import org.scijava.plugin.Plugin
36+
import org.scijava.util.ColorRGB
37+
import sc.iview.SciView
38+
import sc.iview.commands.MenuWeights
39+
40+
/**
41+
* Demo to test custom window sizing API.
42+
* Shows how to set custom window dimensions for VR or other specific display requirements.
43+
*
44+
* @author Kyle Harrington
45+
*/
46+
@Plugin(type = Command::class,
47+
label = "Custom Window Size Demo",
48+
menuRoot = "SciView",
49+
menu = [Menu(label = "Demo", weight = MenuWeights.DEMO),
50+
Menu(label = "Basic", weight = MenuWeights.DEMO_BASIC),
51+
Menu(label = "Custom Window Size", weight = MenuWeights.DEMO_BASIC_CUSTOM_WINDOW)])
52+
class CustomWindowSizeDemo : Command {
53+
@Parameter
54+
private lateinit var sciview: SciView
55+
56+
@Parameter(label = "Window Width", min = "100", max = "3840")
57+
private var width: Int = 1920
58+
59+
@Parameter(label = "Window Height", min = "100", max = "2160")
60+
private var height: Int = 1080
61+
62+
override fun run() {
63+
// Get current window size
64+
val (currentWidth, currentHeight) = sciview.getWindowSize()
65+
println("Current window size: ${currentWidth}x${currentHeight}")
66+
67+
// Set new window size
68+
println("Setting window size to ${width}x${height}...")
69+
val success = sciview.setWindowSize(width, height)
70+
71+
if (success) {
72+
println("Window successfully resized to ${width}x${height}")
73+
74+
// Add some demo content to visualize the new dimensions
75+
sciview.addSphere(
76+
position = Vector3f(0f, 0f, 0f),
77+
radius = 1f,
78+
color = ColorRGB(128, 255, 128)
79+
) {
80+
name = "Center Sphere"
81+
}
82+
83+
// Add corner markers to show the viewport
84+
val aspectRatio = width.toFloat() / height.toFloat()
85+
val markerSize = 0.2f
86+
87+
// Top-left
88+
sciview.addBox(
89+
position = Vector3f(-aspectRatio * 2, 2f, -5f),
90+
size = Vector3f(markerSize, markerSize, markerSize),
91+
color = ColorRGB(255, 0, 0)
92+
) {
93+
name = "Top-Left Marker"
94+
}
95+
96+
// Top-right
97+
sciview.addBox(
98+
position = Vector3f(aspectRatio * 2, 2f, -5f),
99+
size = Vector3f(markerSize, markerSize, markerSize),
100+
color = ColorRGB(0, 255, 0)
101+
) {
102+
name = "Top-Right Marker"
103+
}
104+
105+
// Bottom-left
106+
sciview.addBox(
107+
position = Vector3f(-aspectRatio * 2, -2f, -5f),
108+
size = Vector3f(markerSize, markerSize, markerSize),
109+
color = ColorRGB(0, 0, 255)
110+
) {
111+
name = "Bottom-Left Marker"
112+
}
113+
114+
// Bottom-right
115+
sciview.addBox(
116+
position = Vector3f(aspectRatio * 2, -2f, -5f),
117+
size = Vector3f(markerSize, markerSize, markerSize),
118+
color = ColorRGB(255, 255, 0)
119+
) {
120+
name = "Bottom-Right Marker"
121+
}
122+
123+
// Center the camera
124+
sciview.centerOnScene()
125+
} else {
126+
println("Failed to resize window")
127+
}
128+
}
129+
}

src/main/kotlin/sc/iview/ui/SwingMainWindow.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import java.util.*
5252
import javax.script.ScriptException
5353
import javax.swing.*
5454
import kotlin.concurrent.thread
55+
import kotlin.math.log
5556
import kotlin.math.roundToInt
5657

5758

@@ -235,6 +236,12 @@ class SwingMainWindow(val sciview: SciView) : MainWindow() {
235236
frame.add(mainSplitPane, BorderLayout.CENTER)
236237
frame.add(toolbar, BorderLayout.EAST)
237238
frame.defaultCloseOperation = JFrame.DO_NOTHING_ON_CLOSE
239+
frame.addComponentListener(object : ComponentAdapter() {
240+
override fun componentResized(e: ComponentEvent) {
241+
sciview.windowWidth = e.component.width
242+
sciview.windowHeight = e.component.height
243+
}
244+
})
238245
frame.addWindowListener(object : WindowAdapter() {
239246
override fun windowClosing(e: WindowEvent) {
240247
logger.debug("Closing SciView window.")

0 commit comments

Comments
 (0)