From 8cf9fddca80cf13398810503515abc6d7b9fbcf1 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Thu, 23 May 2024 16:56:38 -0400 Subject: [PATCH 1/7] fix: incomplete rendering at target resolution perf: don't trigger old renderers, don't render when source not visible old renderer where cancelling newer ones, and then the new ones would never finish. Turn off the old renderers. --- .../saalfeldlab/bdv/fx/viewer/ViewerPanelFX.java | 10 ++++++++-- .../viewer/project/VolatileHierarchyProjector.java | 13 ++++++++++--- .../bdv/fx/viewer/render/RenderUnit.java | 2 ++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/ViewerPanelFX.java b/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/ViewerPanelFX.java index cf1b10a2a..8148fc9d7 100644 --- a/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/ViewerPanelFX.java +++ b/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/ViewerPanelFX.java @@ -188,8 +188,14 @@ public ViewerPanelFX( startRenderAnimator(); setWidth(options.getWidth()); setHeight(options.getHeight()); - this.widthProperty().addListener((obs, oldv, newv) -> this.renderUnit.setDimensions((long) getWidth(), (long) getHeight())); - this.heightProperty().addListener((obs, oldv, newv) -> this.renderUnit.setDimensions((long) getWidth(), (long) getHeight())); + widthProperty().subscribe(width -> renderUnit.setDimensions(width.longValue(), (long)getHeight())); + heightProperty().subscribe(height -> renderUnit.setDimensions((long)getWidth(), height.longValue())); + + visibleProperty().subscribe(visible -> { + if (!visible) + renderUnit.stopRendering(); + }); + transformListeners.add(tf -> Paintera.whenPaintable(getDisplay()::drawOverlays)); diff --git a/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/project/VolatileHierarchyProjector.java b/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/project/VolatileHierarchyProjector.java index fe70ca1a3..fd5e5e66b 100644 --- a/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/project/VolatileHierarchyProjector.java +++ b/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/project/VolatileHierarchyProjector.java @@ -29,6 +29,8 @@ package org.janelia.saalfeldlab.bdv.fx.viewer.project; import bdv.viewer.render.VolatileProjector; +import io.github.oshai.kotlinlogging.KLogger; +import io.github.oshai.kotlinlogging.KotlinLogging; import net.imglib2.FinalInterval; import net.imglib2.IterableInterval; import net.imglib2.RandomAccessible; @@ -51,6 +53,7 @@ import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -64,6 +67,8 @@ */ public class VolatileHierarchyProjector, B extends SetZero> implements VolatileProjector { + private static KLogger LOG = KotlinLogging.INSTANCE.logger(() -> null); + /** * A converter from the source pixel type to the target pixel type. */ @@ -211,10 +216,12 @@ public void clearMask() { try { LoopBuilder.setImages(mask).multiThreaded(taskExecutor).forEachPixel(val -> val.set(Byte.MAX_VALUE)); + } catch (RejectedExecutionException e) { + LOG.trace(e, () -> "Clear Mask Rejected"); } catch (RuntimeException e) { - if (!e.getMessage().contains("Interrupted")) { - throw e; - } + if (e.getMessage() != null && e.getMessage().contains("Interrupted")) + LOG.trace(e, () -> "Clear Mask Interrupted"); + else throw e; } numInvalidLevels = sources.size(); } diff --git a/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/render/RenderUnit.java b/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/render/RenderUnit.java index 24fc62457..907fe5ca5 100644 --- a/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/render/RenderUnit.java +++ b/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/render/RenderUnit.java @@ -158,6 +158,8 @@ private Interval clampRepaintInterval(final Interval interval) { protected synchronized void update() { LOG.debug("Updating render unit"); + if (renderer != null) + renderer.animation.stop(); renderTarget = new TransformAwareBufferedImageOverlayRendererFX(); renderTarget.setCanvasSize((int)dimensions[0], (int)dimensions[1]); From 1ea1cc42d6d732ba64b34ee46e5cbf8dda758819 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Fri, 24 May 2024 14:33:12 -0400 Subject: [PATCH 2/7] fix: remove old projector after cancel, replace with new on next frame --- ...mpleInterruptibleProjectorPreMultiply.java | 53 ------------------- .../MultiResolutionRendererGeneric.java | 6 ++- 2 files changed, 4 insertions(+), 55 deletions(-) delete mode 100644 src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/project/SimpleInterruptibleProjectorPreMultiply.java diff --git a/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/project/SimpleInterruptibleProjectorPreMultiply.java b/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/project/SimpleInterruptibleProjectorPreMultiply.java deleted file mode 100644 index 7c24e9cc1..000000000 --- a/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/project/SimpleInterruptibleProjectorPreMultiply.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.janelia.saalfeldlab.bdv.fx.viewer.project; - -import bdv.viewer.render.SimpleVolatileProjector; -import com.sun.javafx.image.PixelUtils; -import net.imglib2.RandomAccessible; -import net.imglib2.RandomAccessibleInterval; -import net.imglib2.converter.Converter; -import net.imglib2.type.numeric.ARGBType; - -/** - * An {@link SimpleVolatileProjector}, that renders a target 2D {@link RandomAccessibleInterval} by copying values from a - * source {@link RandomAccessible}. The source can have more dimensions than the target. Target coordinate - * (x,y) is copied from source coordinate - * (x,y,0,...,0). - *

- * A specified number of threads is used for rendering. - * - * @param pixel type of the source {@link RandomAccessible}. - * @author Tobias Pietzsch - * @author Stephan Saalfeld - * @author Philipp Hanslovsky - */ -@SuppressWarnings("restriction") -public class SimpleInterruptibleProjectorPreMultiply extends SimpleVolatileProjector { - - /** - * Time needed for rendering the last frame, in nano-seconds. - */ - protected long lastFrameRenderNanoTime; - - /** - * Create new projector with the given source and a converter from source to target pixel type. - * - * @param source source pixels. - * @param converter converts from the source pixel type to the target pixel type. - * @param target the target interval that this projector maps to - */ - public SimpleInterruptibleProjectorPreMultiply( - final RandomAccessible source, - final Converter converter, - final RandomAccessibleInterval target) { - - super(source, wrapWithPremultiplyConverter(converter), target); - } - - private static Converter wrapWithPremultiplyConverter(Converter converter) { - - return (input, output) -> { - converter.convert(input, output); - output.set(PixelUtils.NonPretoPre(output.get())); - }; - } -} diff --git a/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/render/MultiResolutionRendererGeneric.java b/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/render/MultiResolutionRendererGeneric.java index afa88d08f..1ec6014ea 100644 --- a/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/render/MultiResolutionRendererGeneric.java +++ b/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/render/MultiResolutionRendererGeneric.java @@ -457,7 +457,7 @@ public int paint( clearQueue = newFrameRequest; if (clearQueue) cacheControl.prepareNextFrame(); - createProjector = newFrameRequest || resized || requestedScreenScaleIndex != currentScreenScaleIndex || !sameAsLastRenderedInterval; + createProjector = projector == null || newFrameRequest || resized || requestedScreenScaleIndex != currentScreenScaleIndex || !sameAsLastRenderedInterval; newFrameRequest = false; final List> sacs = List.copyOf(sources); @@ -624,8 +624,10 @@ public void requestRepaint(final Interval interval, final int screenScaleIndex) if (Intervals.isEmpty(interval)) return; - if (renderingMayBeCancelled && projector != null) + if (renderingMayBeCancelled && projector != null) { projector.cancel(); + projector = null; + } int newRequestedScaleIdx; if (screenScaleIndex > maxScreenScaleIndex) { From 1985211e1f51eab2453b39517b803034843242c9 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Fri, 24 May 2024 14:34:06 -0400 Subject: [PATCH 3/7] fix: specify current or initial for displayToMask --- .../paintera/control/paint/FloodFill2D.java | 4 +- .../paint/PaintClickOrDragController.kt | 2 +- .../paintera/control/paint/ViewerMask.kt | 37 ++++++++++++------- .../paintera/control/tools/paint/SamTool.kt | 2 +- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java index cf4e5b9e9..146af2d1a 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java @@ -137,7 +137,7 @@ public Interval getMaskInterval() { } else { mask = viewerMask; } - final var maskPos = mask.displayPointToInitialMaskPoint(viewerSeedX, viewerSeedY); + final var maskPos = mask.displayPointToMask(viewerSeedX, viewerSeedY, true); final var filter = getBackgorundLabelMaskForAssignment(maskPos, mask, assignment, fill); if (filter == null) return null; @@ -179,7 +179,7 @@ public UtilityTask fillViewerAt(final double viewerSeedX, final double viewer } else { mask = viewerMask; } - final var maskPos = mask.displayPointToInitialMaskPoint(viewerSeedX, viewerSeedY); + final var maskPos = mask.displayPointToMask(viewerSeedX, viewerSeedY, true); final UtilityTask floodFillTask = fillMaskAt(maskPos, mask, fill, filter); if (this.viewerMask == null) { floodFillTask.onCancelled(true, (state, task) -> { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/PaintClickOrDragController.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/PaintClickOrDragController.kt index 6dd9e831e..a51fb1bd8 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/PaintClickOrDragController.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/PaintClickOrDragController.kt @@ -326,7 +326,7 @@ class PaintClickOrDragController( viewerMask?.run { Tasks.createTask { - val viewerPointToMaskPoint = this.displayPointToInitialMaskPoint(viewerX.toInt(), viewerY.toInt()) + val viewerPointToMaskPoint = this.displayPointToMask(viewerX.toInt(), viewerY.toInt(), pointInCurrentDisplay = true) val paintIntervalInMask = Paint2D.paintIntoViewer( viewerImg.writableSource!!.extendValue(Label.INVALID), paintId(), diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/ViewerMask.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/ViewerMask.kt index 0b9f195fb..fa3afc3f7 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/ViewerMask.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/ViewerMask.kt @@ -1,6 +1,5 @@ package org.janelia.saalfeldlab.paintera.control.paint -import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import bdv.util.Affine3DHelpers import javafx.beans.property.SimpleBooleanProperty import net.imglib2.* @@ -17,11 +16,12 @@ import net.imglib2.type.numeric.integer.UnsignedLongType import net.imglib2.type.volatiles.VolatileUnsignedLongType import net.imglib2.util.Intervals import net.imglib2.util.LinAlgHelpers -import org.janelia.saalfeldlab.net.imglib2.view.BundleView import net.imglib2.view.Views +import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import org.janelia.saalfeldlab.fx.extensions.component1 import org.janelia.saalfeldlab.fx.extensions.component2 import org.janelia.saalfeldlab.fx.extensions.nonnull +import org.janelia.saalfeldlab.net.imglib2.view.BundleView import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource import org.janelia.saalfeldlab.paintera.data.mask.SourceMask @@ -215,18 +215,23 @@ class ViewerMask private constructor( return WrappedRandomAccessibleInterval(cachedCellImg) to WrappedRandomAccessibleInterval(volatileRaiWithInvalidate.rai) } - fun displayPointToInitialMaskPoint(displayX: Int, displayY: Int) = displayPointToInitialMaskPoint(Point(displayX, displayY, 0)) - fun displayPointToInitialMaskPoint(displayX: Double, displayY: Double) = displayPointToInitialMaskPoint(RealPoint(displayX, displayY, 0.0)) - fun displayPointToInitialMaskPoint(displayPoint: Point) = displayPointToInitialMaskPoint(displayPoint.positionAsRealPoint()) - fun displayPointToInitialMaskPoint(displayPoint: RealPoint): Point { - val globalPoint = displayPoint.also { currentGlobalToViewerTransform.applyInverse(it, it) } + @JvmOverloads + fun displayPointToMask(displayX: Int, displayY: Int, pointInCurrentDisplay: Boolean = false) = displayPointToMask(Point(displayX, displayY, 0), pointInCurrentDisplay) + @JvmOverloads + fun displayPointToMask(displayX: Double, displayY: Double, pointInCurrentDisplay: Boolean = false) = displayPointToMask(RealPoint(displayX, displayY, 0.0), pointInCurrentDisplay) + @JvmOverloads + fun displayPointToMask(displayPoint: Point, pointInCurrentDisplay: Boolean = false) = displayPointToMask(displayPoint.positionAsRealPoint(), pointInCurrentDisplay) + @JvmOverloads + fun displayPointToMask(displayPoint: RealPoint, pointInCurrentDisplay: Boolean = false): Point { + val globalToMask = if (pointInCurrentDisplay) currentGlobalToViewerTransform else initialGlobalToViewerTransform + val globalPoint = displayPoint.also { globalToMask.applyInverse(it, it) } val xyScaleOnly = depthScaleTransform.copy().also { it.set(it.getScale(0), it.getScale(1), 1.0) } - val pointInInitialMask = RealPoint(globalPoint).also { initialGlobalToMaskTransform.copy().concatenate(xyScaleOnly.inverse()).apply(globalPoint, it) } - return pointInInitialMask.toPoint() + val pointInMask = RealPoint(globalPoint).also { initialGlobalToMaskTransform.copy().concatenate(xyScaleOnly.inverse()).apply(globalPoint, it) } + return pointInMask.toPoint() } @@ -502,20 +507,24 @@ class ViewerMask private constructor( /** * Returns the screen interval in the ViewerMask space, based on the given width and height. + * if [currentScreenInterval] then will be based on the location of (0,0) in the + * [currentGlobalToMaskTransform], otherwise it will use [initialGlobalToMaskTransform]. + * + * Results may differ, depending on e.g. zoom since Mask creation ( in SI for example). * * @param width The width of the interval. Defaults to the width of the viewer. * @param height The height of the interval. Defaults to the height of the viewer. + * @param currentScreenInterval whether the screen interval should map to the current screen, + * or the in initial screen when the mask was created. * - * @return The screen interval. + * @return The screen interval in ViewerMask space. */ @JvmOverloads - fun getScreenInterval(width: Long = viewer.width.toLong(), height: Long = viewer.height.toLong()): Interval { - val (x: Long, y: Long) = displayPointToInitialMaskPoint(0, 0) + fun getScreenInterval(width: Long = viewer.width.toLong(), height: Long = viewer.height.toLong(), currentScreenInterval : Boolean = false): Interval { + val (x: Long, y: Long) = displayPointToMask(0, 0, currentScreenInterval) return Intervals.createMinSize(x, y, 0, width, height, 1) } - fun maskOverScreenInterval(): RandomAccessibleInterval = viewerImg.interval(getScreenInterval()) - fun getInitialGlobalViewerInterval(width: Double, height: Double): RealInterval { val zeroGlobal = doubleArrayOf(0.0, 0.0, 0.0).also { initialGlobalToViewerTransform.applyInverse(it, it) } val sizeGlobal = doubleArrayOf(width, height, 1.0).also { initialGlobalToViewerTransform.applyInverse(it, it) } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/SamTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/SamTool.kt index 6be195872..0d9c51c6f 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/SamTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/SamTool.kt @@ -974,7 +974,7 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty Date: Fri, 24 May 2024 14:34:50 -0400 Subject: [PATCH 4/7] refactor: current depth/slice updates and downstream listeners --- .../control/ShapeInterpolationController.kt | 68 ++++++++++++------- .../ShapeInterpolationTool.kt | 9 ++- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt index 95dc425a8..12a47d045 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt @@ -3,13 +3,14 @@ package org.janelia.saalfeldlab.paintera.control import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import bdv.viewer.TransformListener import io.github.oshai.kotlinlogging.KotlinLogging +import javafx.application.Platform +import javafx.beans.InvalidationListener +import javafx.beans.Observable import javafx.beans.property.ObjectProperty import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleDoubleProperty import javafx.beans.property.SimpleObjectProperty import javafx.beans.value.ChangeListener -import javafx.collections.FXCollections -import javafx.collections.ObservableList import javafx.concurrent.Task import javafx.concurrent.Worker import javafx.scene.paint.Color @@ -40,6 +41,7 @@ import net.imglib2.util.* import net.imglib2.view.ExtendedRealRandomAccessibleRealInterval import net.imglib2.view.IntervalView import net.imglib2.view.Views +import org.checkerframework.common.reflection.qual.Invoke import org.janelia.saalfeldlab.fx.Tasks import org.janelia.saalfeldlab.fx.extensions.* import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread @@ -62,6 +64,7 @@ import org.janelia.saalfeldlab.util.* import org.janelia.saalfeldlab.net.imglib2.view.BundleView import java.math.BigDecimal import java.math.RoundingMode +import java.util.Collections import java.util.concurrent.ExecutionException import java.util.concurrent.atomic.AtomicBoolean import java.util.function.Supplier @@ -88,9 +91,6 @@ class ShapeInterpolationController>( private val slicesAndInterpolants = SlicesAndInterpolants() - val sliceDepthProperty = SimpleDoubleProperty(0.0) - private var sliceDepth: Double by sliceDepthProperty.nonnull() - val isBusyProperty = SimpleBooleanProperty(false, "Shape Interpolation Controller is Busy") private var isBusy: Boolean by isBusyProperty.nonnull() @@ -99,8 +99,11 @@ class ShapeInterpolationController>( val isControllerActive: Boolean get() = controllerState != ControllerState.Off - private val sliceAtCurrentDepthBinding = sliceDepthProperty.createNonNullValueBinding(slicesAndInterpolants) { slicesAndInterpolants.getSliceAtDepth(it.toDouble()) } - private val sliceAtCurrentDepth by sliceAtCurrentDepthBinding.nullableVal() + internal val currentDepthProperty = SimpleDoubleProperty() + internal val currentDepth: Double by currentDepthProperty.nonnullVal() + + internal val sliceAtCurrentDepthProperty = slicesAndInterpolants.createObservableBinding(currentDepthProperty) { it.getSliceAtDepth(currentDepth) } + private val sliceAtCurrentDepth by sliceAtCurrentDepthProperty.nullableVal() val currentSliceMaskInterval get() = sliceAtCurrentDepth?.maskBoundingBox @@ -133,9 +136,6 @@ class ShapeInterpolationController>( return viewerState.getBestMipMapLevel(screenScaleTransform, source) } - - internal val currentDepth: Double by LazyForeignValue(this::globalToViewerTransform) { depthAt(it) } - private var selector: Task? = null private var interpolator: Task? = null private val onTaskFinished = { @@ -152,10 +152,7 @@ class ShapeInterpolationController>( private var globalCompositeFillAndInterpolationImgs: Pair, RealRandomAccessible>? = null private val viewerTransformDepthUpdater = TransformListener { - updateDepth() - sliceAtCurrentDepth?.mask.let { - currentViewerMask = it - } + currentDepthProperty.set(depthAt(it)) } internal fun depthAt(globalTransform: AffineTransform3D): Double { @@ -240,19 +237,17 @@ class ShapeInterpolationController>( selectNewInterpolationId() initialGlobalToViewerTransform = globalToViewerTransform activeViewer!!.addTransformListener(viewerTransformDepthUpdater) - updateDepth() controllerState = ControllerState.Select - sliceAtCurrentDepthBinding.addListener { _, old, new -> + sliceAtCurrentDepthProperty.addListener { _, old, new -> old?.mask?.setMaskOnUpdate = false - new?.mask?.setMaskOnUpdate = false + new?.mask?.also { + currentViewerMask = it + it.setMaskOnUpdate = false + } } } - private fun updateDepth() { - sliceDepth = currentDepth - } - fun exitShapeInterpolation(completed: Boolean) { if (!isControllerActive) { LOG.debug { "Not in shape interpolation" } @@ -274,7 +269,6 @@ class ShapeInterpolationController>( selectedIds.activateAlso(lastSelectedId) controllerState = ControllerState.Off slicesAndInterpolants.clear() - sliceDepth = 0.0 currentViewerMask = null interpolator = null globalCompositeFillAndInterpolationImgs = null @@ -381,7 +375,6 @@ class ShapeInterpolationController>( paintera().manager().apply { setTransform(globalTransform, Duration(300.0)) { transform = globalTransform - updateDepth() controllerState = ControllerState.Select } } @@ -989,7 +982,7 @@ class ShapeInterpolationController>( } } - private class SlicesAndInterpolants : ObservableList by FXCollections.synchronizedObservableList(FXCollections.observableArrayList()) { + private class SlicesAndInterpolants : MutableList by Collections.synchronizedList(mutableListOf()), Observable { fun removeSlice(slice: SliceInfo): Boolean { synchronized(this) { for (idx in indices) { @@ -998,6 +991,9 @@ class ShapeInterpolationController>( LOG.trace { "Removing Slice: $idx" } removeAt(idx).getSlice() removeIfInterpolant(idx - 1) + + notifyListeners() + return true } } @@ -1017,7 +1013,11 @@ class ShapeInterpolationController>( synchronized(this) { return if (idx >= 0 && idx <= size - 1 && get(idx).isInterpolant) { LOG.trace { "Removing Interpolant: $idx" } - removeAt(idx).getInterpolant() + val interp = removeAt(idx).getInterpolant() + + notifyListeners() + + interp } else null } @@ -1043,6 +1043,10 @@ class ShapeInterpolationController>( } LOG.trace { "Adding Slice: ${this.size}" } add(sliceOrInterpolant) + + InvokeOnJavaFXApplicationThread { + listeners.forEach { it.invalidated(this) } + } } } @@ -1145,6 +1149,20 @@ class ShapeInterpolationController>( return false } } + + private val listeners = mutableListOf() + + override fun addListener(p0: InvalidationListener) { + listeners += p0 + } + + override fun removeListener(p0: InvalidationListener) { + listeners -= p0 + } + + private fun notifyListeners() = Platform.runLater { + listeners.forEach { it.invalidated(this) } + } } var initialGlobalToViewerTransform: AffineTransform3D? = null diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationTool.kt index d361b6f92..7117387de 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationTool.kt @@ -12,7 +12,6 @@ import javafx.util.Duration import kotlinx.coroutines.* import net.imglib2.realtransform.AffineTransform3D import net.imglib2.util.Intervals -import org.checkerframework.checker.units.qual.Current import org.janelia.saalfeldlab.fx.UtilityTask import org.janelia.saalfeldlab.fx.actions.* import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.installActionSet @@ -92,7 +91,7 @@ internal class ShapeInterpolationTool( override val statusProperty = SimpleStringProperty().apply { - val statusBinding = controller.controllerStateProperty.createNullableValueBinding(controller.sliceDepthProperty) { + val statusBinding = controller.controllerStateProperty.createNullableValueBinding(controller.currentDepthProperty, controller.sliceAtCurrentDepthProperty) { controller.getStatusText() } bind(statusBinding) @@ -103,7 +102,7 @@ internal class ShapeInterpolationTool( controllerState == ShapeInterpolationController.ControllerState.Interpolate -> "Interpolating..." numSlices == 0 -> "Select or Paint ..." else -> { - val sliceIdx = sortedSliceDepths.indexOf(sliceDepthProperty.get()) + val sliceIdx = sortedSliceDepths.indexOf(currentDepth) "Slice: ${if (sliceIdx == -1) "N/A" else "${sliceIdx + 1}"} / ${numSlices}" } } @@ -382,7 +381,7 @@ internal class ShapeInterpolationTool( val mask = getMask() fill2D.fill2D.provideMask(mask) - val pointInMask = mask.displayPointToInitialMaskPoint(event!!.x, event.y) + val pointInMask = mask.displayPointToMask(event!!.x, event.y, pointInCurrentDisplay = true) val pointInSource = pointInMask.positionAsRealPoint().also { mask.initialMaskToSourceTransform.apply(it, it) } val info = mask.info val sourceLabel = source.getInterpolatedDataSource(info.time, info.level, null).getAt(pointInSource).integerLong @@ -491,7 +490,7 @@ internal class ShapeInterpolationTool( } fill2D.fill2D.provideMask(mask) - val pointInMask = mask.displayPointToInitialMaskPoint(event.x, event.y) + val pointInMask = mask.displayPointToMask(event.x, event.y, pointInCurrentDisplay = true) val pointInSource = pointInMask.positionAsRealPoint().also { mask.initialMaskToSourceTransform.apply(it, it) } val info = mask.info val sourceLabel = source.getInterpolatedDataSource(info.time, info.level, null).getAt(pointInSource).integerLong From c10e926044b614e4d001fb093b9ddec099261a66 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Fri, 24 May 2024 14:35:51 -0400 Subject: [PATCH 5/7] fix: ignore disable on tool switch-back triggers --- .../control/modes/ShapeInterpolationMode.kt | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ShapeInterpolationMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ShapeInterpolationMode.kt index 07655423a..e14b28114 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ShapeInterpolationMode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ShapeInterpolationMode.kt @@ -153,7 +153,7 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat private fun modeActions(): List { return mutableListOf( - painteraActionSet(CANCEL) { + painteraActionSet(CANCEL, ignoreDisable = true) { with(controller) { verifyAll(KEY_PRESSED, "Shape Interpolation Controller is Active ") { isControllerActive } verifyAll(Event.ANY, "Shape Interpolation Tool is Active") { activeTool is ShapeInterpolationTool } @@ -170,36 +170,36 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat } } }, - painteraActionSet("paint during shape interpolation", PaintActionType.Paint) { - KEY_PRESSED(paintBrushTool.keyTrigger) { - name = "switch to paint tool" - verify { activeSourceStateProperty.get()?.dataSource is MaskedSource<*, *> } - onAction { switchTool(paintBrushTool) } - } - + painteraActionSet("paint return tool triggers", PaintActionType.Paint, ignoreDisable = true) { KEY_RELEASED(paintBrushTool.keyTrigger) { name = "switch back to shape interpolation tool from paint brush" filter = true - verify { activeTool is PaintBrushTool } + verify("PaintBrushTool is active") { activeTool is PaintBrushTool } onAction { switchTool(shapeInterpolationTool) } } - - KEY_PRESSED(fill2DTool.keyTrigger) { - name = "switch to fill2d tool" - verify { activeSourceStateProperty.get()?.dataSource is MaskedSource<*, *> } - onAction { switchTool(fill2DTool) } - } KEY_RELEASED(fill2DTool.keyTrigger) { name = "switch to shape interpolation tool from fill2d" filter = true - verify { activeTool is Fill2DTool } + verify("Fill2DTool is active") { activeTool is Fill2DTool } onAction { switchTool(shapeInterpolationTool) } } + }, + painteraActionSet("paint during shape interpolation", PaintActionType.Paint) { + KEY_PRESSED(paintBrushTool.keyTrigger) { + name = "switch to paint tool" + verify("Active source is MaskedSource") { activeSourceStateProperty.get()?.dataSource is MaskedSource<*, *> } + onAction { switchTool(paintBrushTool) } + } + KEY_PRESSED(fill2DTool.keyTrigger) { + name = "switch to fill2d tool" + verify("Active source is MaskedSource") { activeSourceStateProperty.get()?.dataSource is MaskedSource<*, *> } + onAction { switchTool(fill2DTool) } + } KEY_PRESSED(samTool.keyTrigger) { name = "toggle SAM tool" - verify { activeSourceStateProperty.get()?.dataSource is MaskedSource<*, *> } + verify("Active source is MaskedSource") { activeSourceStateProperty.get()?.dataSource is MaskedSource<*, *> } onAction { val nextTool = if (activeTool != samTool) samTool else shapeInterpolationTool switchTool(nextTool) From af539d09c5540f027205a15e37f90484654593d5 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Fri, 24 May 2024 14:36:47 -0400 Subject: [PATCH 6/7] fix: paint in auto-SAM slices sometimes painting would fail in auto-sam slices. Add a new mask layer to paint into when switching to Paint Brush --- .../ShapeInterpolationPaintBrushTool.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationPaintBrushTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationPaintBrushTool.kt index a4449c51d..c57ae2338 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationPaintBrushTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationPaintBrushTool.kt @@ -5,12 +5,15 @@ import javafx.event.Event import javafx.scene.input.MouseButton import javafx.scene.input.MouseEvent.MOUSE_PRESSED import javafx.scene.input.MouseEvent.MOUSE_RELEASED +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import org.janelia.saalfeldlab.fx.actions.ActionSet import org.janelia.saalfeldlab.fx.actions.painteraActionSet import org.janelia.saalfeldlab.fx.extensions.LazyForeignValue import org.janelia.saalfeldlab.fx.midi.MidiActionSet import org.janelia.saalfeldlab.labels.Label -import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationController import org.janelia.saalfeldlab.paintera.control.actions.PaintActionType import org.janelia.saalfeldlab.paintera.control.modes.NavigationTool import org.janelia.saalfeldlab.paintera.control.modes.ShapeInterpolationMode @@ -59,13 +62,14 @@ internal class ShapeInterpolationPaintBrushTool(activeSourceStateProperty: Simpl fun finishPaintStroke() { paintClickOrDrag?.let { it.maskInterval?.let { interval -> - shapeInterpolationMode.addSelection(interval)?.also { slice -> slice.locked = true } + CoroutineScope(Dispatchers.Default + Job()).launch { + shapeInterpolationMode.addSelection(interval)?.also { slice -> slice.locked = true } + } } } } - /** * Additional paint brush actions for Shape Interpolation. * @@ -85,7 +89,10 @@ internal class ShapeInterpolationPaintBrushTool(activeSourceStateProperty: Simpl (activeSourceStateProperty.get()?.dataSource as? MaskedSource<*, *>)?.let { source -> paintClickOrDrag!!.let { paintController -> source.resetMasks(false) - paintController.provideMask(controller.getMask()) + val mask = controller.getMask() + mask.pushNewImageLayer() + paintController.provideMask(mask) + } } } From ccdb497c2a3930cebeda87941140fba19d37f050 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Fri, 24 May 2024 14:54:46 -0400 Subject: [PATCH 7/7] fix: SAM MIDI device accept prediction reset prompt was happening too quickkly, no put it off by doing it on. --- .../saalfeldlab/paintera/control/tools/paint/SamTool.kt | 4 +++- .../tools/shapeinterpolation/ShapeInterpolationTool.kt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/SamTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/SamTool.kt index 0d9c51c6f..062d65120 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/SamTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/SamTool.kt @@ -597,7 +597,9 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty