diff --git a/README.md b/README.md index 2d7e2676..be82fa66 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,29 @@ Should you experience any issues, [please try the latest development version](ht ![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) +## API Features + +### Custom Window Dimensions + +SciView now supports setting custom window dimensions via API, which is essential for VR headsets that require specific resolutions: + +```kotlin +// Create SciView with custom dimensions +val sciview = SciView.create(1920, 1080) + +// Or resize an existing instance +sciview.setWindowSize(2880, 1700) // Example: Oculus Quest 2 resolution + +// Query current dimensions +val (width, height) = sciview.getWindowSize() +``` + +This feature is particularly useful for: +- VR headset integration requiring exact resolutions +- Multi-monitor setups +- Creating screenshots or recordings at specific resolutions +- Kiosk or presentation modes + ## Developers [Kyle Harrington](https://kyleharrington.com), University of Idaho & [Ulrik Guenther](https://ulrik.is/writing), MPI-CBG diff --git a/gradle.properties b/gradle.properties index e890f9e7..38c302c8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ jvmTarget=21 #useLocalScenery=true kotlinVersion=2.2.10 dokkaVersion=1.9.20 -scijavaParentPOMVersion=40.0.0 +scijavaParentPOMVersion=43.0.0 version=0.4.1-SNAPSHOT # update site configuration diff --git a/src/main/kotlin/sc/iview/SciView.kt b/src/main/kotlin/sc/iview/SciView.kt index 3ee67b5c..c4eec578 100644 --- a/src/main/kotlin/sc/iview/SciView.kt +++ b/src/main/kotlin/sc/iview/SciView.kt @@ -50,8 +50,6 @@ import graphics.scenery.controls.TrackerInput import graphics.scenery.primitives.* import graphics.scenery.proteins.Protein import graphics.scenery.proteins.RibbonDiagram -import graphics.scenery.utils.ExtractsNatives -import graphics.scenery.utils.ExtractsNatives.Companion.getPlatform import graphics.scenery.utils.LogbackUtils import graphics.scenery.utils.SceneryPanel import graphics.scenery.utils.Statistics @@ -84,8 +82,8 @@ import net.imglib2.type.numeric.RealType import net.imglib2.type.numeric.integer.UnsignedByteType import net.imglib2.view.Views import org.joml.Quaternionf +import org.joml.Vector2f import org.joml.Vector3f -import org.joml.Vector4f import org.scijava.Context import org.scijava.`object`.ObjectService import org.scijava.display.Display @@ -100,8 +98,6 @@ import org.scijava.service.SciJavaService import org.scijava.thread.ThreadService import org.scijava.util.ColorRGB import org.scijava.util.Colors -import org.scijava.util.VersionUtils -import sc.iview.commands.demo.animation.ParticleDemo import sc.iview.commands.edit.InspectorInteractiveCommand import sc.iview.event.NodeActivatedEvent import sc.iview.event.NodeAddedEvent @@ -131,12 +127,10 @@ import java.util.function.Predicate import java.util.stream.Collectors import kotlin.collections.ArrayList import kotlin.collections.HashMap -import kotlin.collections.LinkedHashMap import kotlin.concurrent.thread import javax.swing.JOptionPane import kotlin.math.cos import kotlin.math.sin -import kotlin.system.measureTimeMillis /** * Main SciView class. @@ -1689,22 +1683,28 @@ class SciView : SceneryBase, CalibratedRealInterval { } private var originalFOV = camera?.fov + private var originalWinSize = getWindowSize() /** - * Enable VR rendering + * Enable or disable VR rendering. Automatically stores the original controls and FOV and restores them + * after VR is toggled off again. + * @param resizeWindow changes the window resolution to match the stereo rendering of the selected headset. + * @param resolutionScale Factor that allows changing the VR resolution */ - fun toggleVRRendering() { + fun toggleVRRendering(resizeWindow: Boolean = true, resolutionScale: Float = 1f) { var renderer = renderer ?: return // Save camera's original settings if we switch from 2D to VR if (!vrActive) { originalFOV = camera?.fov + originalWinSize = getWindowSize() } // If turning off VR, store the controls state before deactivating if (vrActive) { // We're about to turn off VR controls.stashControls() + setWindowSize(originalWinSize.first, originalWinSize.second) } vrActive = !vrActive @@ -1721,6 +1721,20 @@ class SciView : SceneryBase, CalibratedRealInterval { if (hmd.initializedAndWorking()) { hub.add(SceneryElement.HMDInput, hmd) ti = hmd + // Disable the sidebar if it was still open + if ((mainWindow as SwingMainWindow).sidebarOpen) { + toggleSidebar() + } + if (resizeWindow) { + val perEyeResolution = hmd.getRenderTargetSize() + // Recommended resolution is about x1.33 larger than the actual headset resolution + // due to distortion compensation. + // Too high resolution gets in the way of volume rendering, so we scale it down a bit again + setWindowSize( + (perEyeResolution.x * 2f / 1.33f * resolutionScale).toInt(), + (perEyeResolution.y / 1.33f * resolutionScale).toInt() + ) + } } else { logger.warn("Could not initialise VR headset, just activating stereo rendering.") } @@ -1916,6 +1930,57 @@ class SciView : SceneryBase, CalibratedRealInterval { println(scijavaContext!!.serviceIndex) } + /** + * Set the window dimensions of the sciview rendering window. + * This is essential for VR headsets that require specific resolutions. + * + * @param width The desired width of the window in pixels + * @param height The desired height of the window in pixels + * @return true if the window was successfully resized, false otherwise + */ + fun setWindowSize(width: Int, height: Int): Boolean { + if (width <= 0 || height <= 0) { + log.error("Window dimensions must be positive: width=$width, height=$height") + return false + } + + try { + // Update internal dimensions + windowWidth = width + windowHeight = height + + // Update the main window frame if it exists + if (mainWindow is SwingMainWindow) { + val swingWindow = mainWindow as SwingMainWindow + val scale = getScenerySettings().get("Renderer.SurfaceScale") ?: Vector2f(1f) + val scaledWidth = (width / scale.x()).toInt() + val scaleHeight = (height / scale.y()).toInt() + // We need to scale the swing window with taking the surface scale into account + swingWindow.frame.setSize(scaledWidth, scaleHeight) + + // Update the renderer dimensions + // TODO Is this even needed? Since outdated semaphores will trigger a swapchain recreation anyway + renderer?.reshape(width, height) + } + + log.info("Window resized to ${width}x${height}") + return true + } catch (e: Exception) { + log.error("Failed to resize window: ${e.message}") + e.printStackTrace() + return false + } + } + + /** + * Get the current window dimensions. + * + * @return a Pair containing the width and height of the window + */ + fun getWindowSize(): Pair { + return Pair(windowWidth, windowHeight) + } + /** * Return the color table corresponding to the [lutName] * @param lutName a String represening an ImageJ style LUT name, like Fire.lut @@ -2002,6 +2067,26 @@ class SciView : SceneryBase, CalibratedRealInterval { val sciViewService = context.service(SciViewService::class.java) return sciViewService.orCreateActiveSciView } + + /** + * Static launching method with custom window dimensions + * + * @param width The desired width of the window in pixels + * @param height The desired height of the window in pixels + * @return a newly created SciView with specified dimensions + */ + @JvmStatic + @Throws(Exception::class) + fun create(width: Int, height: Int): SciView { + xinitThreads() + val context = Context(ImageJService::class.java, SciJavaService::class.java, SCIFIOService::class.java) + val objectService = context.service(ObjectService::class.java) + objectService.addObject(Utils.SciviewStandalone()) + val sciViewService = context.service(SciViewService::class.java) + val sciView = sciViewService.orCreateActiveSciView + sciView.setWindowSize(width, height) + return sciView + } /** * Static launching method diff --git a/src/main/kotlin/sc/iview/commands/MenuWeights.kt b/src/main/kotlin/sc/iview/commands/MenuWeights.kt index 610deead..29756444 100644 --- a/src/main/kotlin/sc/iview/commands/MenuWeights.kt +++ b/src/main/kotlin/sc/iview/commands/MenuWeights.kt @@ -109,6 +109,7 @@ object MenuWeights { const val DEMO_BASIC_IMAGEPLANE = 4.0 const val DEMO_BASIC_VOLUME = 6.0 const val DEMO_BASIC_POINTCLOUD = 7.0 + const val DEMO_BASIC_CUSTOM_WINDOW = 8.0 // Demo/Animation const val DEMO_ANIMATION_PARTICLE = 0.0 const val DEMO_ANIMATION_VOLUMETIMESERIES = 1.0 diff --git a/src/main/kotlin/sc/iview/commands/demo/basic/CustomWindowSizeDemo.kt b/src/main/kotlin/sc/iview/commands/demo/basic/CustomWindowSizeDemo.kt new file mode 100644 index 00000000..15bcdb19 --- /dev/null +++ b/src/main/kotlin/sc/iview/commands/demo/basic/CustomWindowSizeDemo.kt @@ -0,0 +1,129 @@ +/*- + * #%L + * Scenery-backed 3D visualization package for ImageJ. + * %% + * Copyright (C) 2016 - 2024 sciview developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package sc.iview.commands.demo.basic + +import org.joml.Vector3f +import org.scijava.command.Command +import org.scijava.plugin.Menu +import org.scijava.plugin.Parameter +import org.scijava.plugin.Plugin +import org.scijava.util.ColorRGB +import sc.iview.SciView +import sc.iview.commands.MenuWeights + +/** + * Demo to test custom window sizing API. + * Shows how to set custom window dimensions for VR or other specific display requirements. + * + * @author Kyle Harrington + */ +@Plugin(type = Command::class, + label = "Custom Window Size Demo", + menuRoot = "SciView", + menu = [Menu(label = "Demo", weight = MenuWeights.DEMO), + Menu(label = "Basic", weight = MenuWeights.DEMO_BASIC), + Menu(label = "Custom Window Size", weight = MenuWeights.DEMO_BASIC_CUSTOM_WINDOW)]) +class CustomWindowSizeDemo : Command { + @Parameter + private lateinit var sciview: SciView + + @Parameter(label = "Window Width", min = "100", max = "3840") + private var width: Int = 1920 + + @Parameter(label = "Window Height", min = "100", max = "2160") + private var height: Int = 1080 + + override fun run() { + // Get current window size + val (currentWidth, currentHeight) = sciview.getWindowSize() + println("Current window size: ${currentWidth}x${currentHeight}") + + // Set new window size + println("Setting window size to ${width}x${height}...") + val success = sciview.setWindowSize(width, height) + + if (success) { + println("Window successfully resized to ${width}x${height}") + + // Add some demo content to visualize the new dimensions + sciview.addSphere( + position = Vector3f(0f, 0f, 0f), + radius = 1f, + color = ColorRGB(128, 255, 128) + ) { + name = "Center Sphere" + } + + // Add corner markers to show the viewport + val aspectRatio = width.toFloat() / height.toFloat() + val markerSize = 0.2f + + // Top-left + sciview.addBox( + position = Vector3f(-aspectRatio * 2, 2f, -5f), + size = Vector3f(markerSize, markerSize, markerSize), + color = ColorRGB(255, 0, 0) + ) { + name = "Top-Left Marker" + } + + // Top-right + sciview.addBox( + position = Vector3f(aspectRatio * 2, 2f, -5f), + size = Vector3f(markerSize, markerSize, markerSize), + color = ColorRGB(0, 255, 0) + ) { + name = "Top-Right Marker" + } + + // Bottom-left + sciview.addBox( + position = Vector3f(-aspectRatio * 2, -2f, -5f), + size = Vector3f(markerSize, markerSize, markerSize), + color = ColorRGB(0, 0, 255) + ) { + name = "Bottom-Left Marker" + } + + // Bottom-right + sciview.addBox( + position = Vector3f(aspectRatio * 2, -2f, -5f), + size = Vector3f(markerSize, markerSize, markerSize), + color = ColorRGB(255, 255, 0) + ) { + name = "Bottom-Right Marker" + } + + // Center the camera + sciview.centerOnScene() + } else { + println("Failed to resize window") + } + } +} diff --git a/src/main/kotlin/sc/iview/ui/SwingMainWindow.kt b/src/main/kotlin/sc/iview/ui/SwingMainWindow.kt index cc7095a2..6904969e 100644 --- a/src/main/kotlin/sc/iview/ui/SwingMainWindow.kt +++ b/src/main/kotlin/sc/iview/ui/SwingMainWindow.kt @@ -52,6 +52,7 @@ import java.util.* import javax.script.ScriptException import javax.swing.* import kotlin.concurrent.thread +import kotlin.math.log import kotlin.math.roundToInt @@ -235,6 +236,12 @@ class SwingMainWindow(val sciview: SciView) : MainWindow() { frame.add(mainSplitPane, BorderLayout.CENTER) frame.add(toolbar, BorderLayout.EAST) frame.defaultCloseOperation = JFrame.DO_NOTHING_ON_CLOSE + frame.addComponentListener(object : ComponentAdapter() { + override fun componentResized(e: ComponentEvent) { + sciview.windowWidth = e.component.width + sciview.windowHeight = e.component.height + } + }) frame.addWindowListener(object : WindowAdapter() { override fun windowClosing(e: WindowEvent) { logger.debug("Closing SciView window.")