From 4abe2bc5f03be61910faab6c420c77bcf6b02dcd Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Sat, 26 Oct 2024 13:52:29 +0200 Subject: [PATCH 01/28] fix: FIRST_TEMP_ID is temp id --- .../java/org/janelia/saalfeldlab/paintera/id/IdService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/id/IdService.java b/src/main/java/org/janelia/saalfeldlab/paintera/id/IdService.java index 253284638..ee4703909 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/id/IdService.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/id/IdService.java @@ -108,7 +108,7 @@ static long randomTemporaryId() { static boolean isTemporary(final long id) { - return id > FIRST_TEMPORARY_ID && id < LAST_TEMPORARY_ID; + return id >= FIRST_TEMPORARY_ID && id < LAST_TEMPORARY_ID; } class IdServiceNotProvided implements IdService { From f90c35117c2e055d1651b377acb350c4a2c0be2a Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Sat, 26 Oct 2024 13:52:43 +0200 Subject: [PATCH 02/28] refactor: how to generate local temp ids --- .../janelia/saalfeldlab/paintera/id/LocalIdService.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/id/LocalIdService.java b/src/main/java/org/janelia/saalfeldlab/paintera/id/LocalIdService.java index 062b432c1..71335ebd4 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/id/LocalIdService.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/id/LocalIdService.java @@ -1,5 +1,6 @@ package org.janelia.saalfeldlab.paintera.id; +import java.util.Arrays; import java.util.stream.LongStream; public class LocalIdService implements IdService { @@ -19,15 +20,13 @@ public LocalIdService(final long next) { @Override public long nextTemporary() { - final var temp = nextTemp; - nextTemp += 1; - return temp; + return IdService.randomTemps.next(); } @Override public long[] nextTemporary(int n) { - final long[] tempIds = LongStream.range(nextTemp, nextTemp + n).toArray(); - nextTemp += n; + final long[] tempIds = new long[n]; + Arrays.setAll(tempIds, it -> IdService.randomTemps.next()); return tempIds; } From bf8d920230a44c224ead36867a1e737ae0f5cbf1 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Sat, 26 Oct 2024 13:53:18 +0200 Subject: [PATCH 03/28] fix: handle is SAM service is not available more gracefully still can be better --- .../paintera/cache/SamEmbeddingLoaderCache.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt index b07d6ca8c..e8430b4fb 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt @@ -37,6 +37,7 @@ import org.janelia.saalfeldlab.paintera.config.SegmentAnythingConfig import org.janelia.saalfeldlab.paintera.control.tools.paint.SamPredictor import org.janelia.saalfeldlab.paintera.paintera import org.janelia.saalfeldlab.paintera.properties +import java.io.IOException import java.io.PipedInputStream import java.io.PipedOutputStream import java.net.SocketTimeoutException @@ -255,8 +256,12 @@ object SamEmbeddingLoaderCache : AsyncCacheWithLoader Date: Sat, 26 Oct 2024 13:53:47 +0200 Subject: [PATCH 04/28] fix: dont block on the main thread when setting the id. --- .../control/tools/paint/PaintBrushTool.kt | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/PaintBrushTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/PaintBrushTool.kt index 6a5568860..6d0f45006 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/PaintBrushTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/PaintBrushTool.kt @@ -22,9 +22,7 @@ import org.janelia.saalfeldlab.fx.actions.painteraActionSet import org.janelia.saalfeldlab.fx.actions.painteraMidiActionSet import org.janelia.saalfeldlab.fx.actions.verifyPainteraNotDisabled import org.janelia.saalfeldlab.fx.extensions.LazyForeignValue -import org.janelia.saalfeldlab.fx.extensions.createNonNullValueBinding import org.janelia.saalfeldlab.fx.extensions.createNullableValueBinding -import org.janelia.saalfeldlab.fx.extensions.nonnull import org.janelia.saalfeldlab.fx.midi.FaderAction import org.janelia.saalfeldlab.fx.midi.MidiFaderEvent import org.janelia.saalfeldlab.fx.midi.MidiToggleEvent @@ -38,9 +36,11 @@ import org.janelia.saalfeldlab.fx.ui.GlyphScaleView import org.janelia.saalfeldlab.paintera.control.modes.ToolMode import org.janelia.saalfeldlab.paintera.control.paint.PaintActions2D import org.janelia.saalfeldlab.paintera.control.paint.PaintClickOrDragController +import org.janelia.saalfeldlab.paintera.id.IdService import org.janelia.saalfeldlab.paintera.paintera import org.janelia.saalfeldlab.paintera.state.SourceState import java.lang.Double.min +import java.util.concurrent.atomic.AtomicLong private const val CHANGE_BRUSH_DEPTH = "change brush depth" private const val START_BACKGROUND_ERASE = "start background erase" @@ -56,10 +56,13 @@ open class PaintBrushTool(activeSourceStateProperty: SimpleObjectProperty String = { - when (it) { - Label.BACKGROUND -> "BACKGROUND" - Label.TRANSPARENT -> "TRANSPARENT" - Label.INVALID -> "INVALID" - Label.OUTSIDE -> "OUTSIDE" - Label.MAX_ID -> "MAX_ID" - else -> "$it" - } + override val statusProperty = SimpleStringProperty() + + private fun updateStatus() = InvokeOnJavaFXApplicationThread { + val labelNum = currentLabelToPaintAtomic.get() + if (IdService.isTemporary(labelNum)) return@InvokeOnJavaFXApplicationThread + val labelText = when (labelNum) { + Label.BACKGROUND -> "BACKGROUND" + Label.TRANSPARENT -> "TRANSPARENT" + Label.INVALID -> "INVALID" + Label.OUTSIDE -> "OUTSIDE" + Label.MAX_ID -> "MAX_ID" + else -> "$labelNum" } - bind(currentLabelToPaintProperty.createNonNullValueBinding { "Painting Label: ${labelNumToString(it)}" }) + statusProperty.value = "Painting Label: $labelText" } private val selectedIdListener: (obs: Observable) -> Unit = { @@ -150,7 +155,7 @@ open class PaintBrushTool(activeSourceStateProperty: SimpleObjectProperty Date: Sat, 26 Oct 2024 13:54:06 +0200 Subject: [PATCH 05/28] fix: don't expose temporary ID in status text --- .../paintera/control/ShapeInterpolationController.kt | 2 +- .../shapeinterpolation/ShapeInterpolationPaintBrushTool.kt | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) 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 188590343..9521bad57 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt @@ -80,7 +80,7 @@ class ShapeInterpolationController>( Select, Interpolate, Preview, Off, Moving } - private var lastSelectedId: Long = 0 + internal var lastSelectedId: Long = 0 internal var interpolationId: Long = Label.INVALID 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 b4e7fe40b..f0a7ff1ab 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 @@ -56,7 +56,11 @@ internal class ShapeInterpolationPaintBrushTool(activeSourceStateProperty: Simpl } release() } + //TODO Caleb: Don't like it, but otherwise the status text shows a temp label. Do better super.deactivate() + setCurrentLabel(shapeInterpolationMode.controller.lastSelectedId) + setCurrentLabel(shapeInterpolationMode.controller.interpolationId) + } fun finishPaintStroke() { From 5c22eea317d78cf581ff0c4f96142bd0fdfce71e Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Sat, 26 Oct 2024 20:52:23 +0100 Subject: [PATCH 06/28] feat: check if tools/actions are valid when creating buttons, and disable buttons if not --- .../paintera/cache/SamEmbeddingLoaderCache.kt | 30 +++++++++++++------ .../paintera/control/modes/PaintLabelMode.kt | 2 ++ .../paintera/control/tools/Tool.kt | 12 +++++++- .../paintera/control/tools/paint/PaintTool.kt | 2 +- .../paintera/control/tools/paint/SamTool.kt | 2 ++ .../ShapeInterpolationTool.kt | 5 ++++ 6 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt index e8430b4fb..6488da1de 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt @@ -66,6 +66,15 @@ object SamEmbeddingLoaderCache : AsyncCacheWithLoader getDataSourceAndConverter (sac) } // to ensure non-volatile + .map { sac -> getDataSourceAndConverter(sac) } // to ensure non-volatile .toList() return RenderUnitState( globalToViewerTransform?.copy() ?: AffineTransform3D().also { state.getViewerTransform(it) }, diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt index a0c86f63b..5cf7aba0f 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt @@ -28,6 +28,7 @@ import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.paintera.DeviceManager import org.janelia.saalfeldlab.paintera.LabelSourceStateKeys import org.janelia.saalfeldlab.paintera.LabelSourceStateKeys.SHAPE_INTERPOLATION__TOGGLE_MODE +import org.janelia.saalfeldlab.paintera.cache.SamEmbeddingLoaderCache import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationController import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions import org.janelia.saalfeldlab.paintera.control.actions.LabelActionType @@ -152,6 +153,7 @@ object PaintLabelMode : AbstractToolMode() { private val activeSamTool = painteraActionSet(LabelSourceStateKeys.SEGMENT_ANYTHING__TOGGLE_MODE, PaintActionType.Paint) { KEY_PRESSED(samTool.keyTrigger) { + verify { SamEmbeddingLoaderCache.canReachServer } verify { activeSourceStateProperty.get() is ConnectomicsLabelState<*, *> } verify { activeTool !is SamTool } verify { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/Tool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/Tool.kt index ebda557ef..300458d2f 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/Tool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/Tool.kt @@ -23,6 +23,7 @@ import org.janelia.saalfeldlab.paintera.paintera interface Tool { + fun isValid() = true fun activate() {} fun deactivate() {} @@ -62,10 +63,19 @@ interface ToolBarItem { return button.also { btn -> btn.id = name + //FIXME Caleb: this is either not necessary, or magic. Regardless, should fix it + // why conditionally bind isDisabled only if a graphic? btn.graphic?.let { + + val checkIsValid = { action?.isValid(null) ?: (this as? Tool)?.isValid() ?: true } + /* Listen on disabled when visible*/ if ("ignore-disable" !in it.styleClass) { - btn.disableProperty().bind(paintera.baseView.isDisabledProperty) + paintera.baseView.isDisabledProperty.`when`(btn.visibleProperty()).subscribe { disabled -> + btn.disableProperty().set(disabled || !checkIsValid()) + } } + /* set initial state to */ + btn.disableProperty().set(!checkIsValid()) } btn.styleClass += "toolbar-button" btn.tooltip = Tooltip( diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/PaintTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/PaintTool.kt index 0dbe132c4..94b6ae4b8 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/PaintTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/PaintTool.kt @@ -39,7 +39,7 @@ interface ConfigurableTool { abstract class PaintTool( protected val activeSourceStateProperty: SimpleObjectProperty?>, mode: ToolMode? = null -) : ViewerTool(mode), ConfigurableTool, ToolBarItem { +) : ViewerTool(mode), ConfigurableTool { abstract override val keyTrigger: NamedKeyBinding 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 10cfa24b7..8d9199971 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 @@ -246,6 +246,8 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty @@ -361,6 +364,7 @@ internal class ShapeInterpolationTool( } autoSamRight = KEY_PRESSED(SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICE_RIGHT) { graphic = { ScaleView().apply { styleClass += listOf("auto-sam", "slice-right") } } + verify { SamEmbeddingLoaderCache.canReachServer } onAction { val depths = sortedSliceDepths.toMutableList() val (firstDepth, firstSpacing, lastDepth, lastSpacing) = edgeDepthsAndSpacing(depths) @@ -391,6 +395,7 @@ internal class ShapeInterpolationTool( } } autoSamCurrent = KEY_PRESSED(SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICE_HERE) { + verify { SamEmbeddingLoaderCache.canReachServer } onAction { requestSamPrediction(currentDepth, refresh = true) } From 5760b6fa5316ec790bd495813f1b090e0f127dc1 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Tue, 29 Oct 2024 10:02:49 -0400 Subject: [PATCH 07/28] feat: add ellipses after menus that open dialogs --- .../control/actions/paint/SmoothAction.kt | 4 +--- .../paintera/ui/menus/PainteraMenuItems.kt | 22 +++++++++---------- .../paintera/ui/menus/PainteraMenus.kt | 2 +- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt index f1a193b1e..b2cbb41cc 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt @@ -64,8 +64,6 @@ import org.janelia.saalfeldlab.paintera.state.metadata.MultiScaleMetadataState import org.janelia.saalfeldlab.paintera.ui.FontAwesome import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.smallestContainingInterval import org.janelia.saalfeldlab.util.* -import org.slf4j.LoggerFactory -import java.lang.invoke.MethodHandles import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.RejectedExecutionException import java.util.concurrent.ThreadPoolExecutor @@ -120,7 +118,7 @@ class SmoothActionVerifiedState { } } -object SmoothAction : MenuAction("_Smooth") { +object SmoothAction : MenuAction("_Smooth...") { private fun newConvolutionExecutor(): ThreadPoolExecutor { val threads = Runtime.getRuntime().availableProcessors() diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenuItems.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenuItems.kt index 5f23f26ea..251e6c097 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenuItems.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenuItems.kt @@ -30,19 +30,19 @@ enum class PainteraMenuItems( private val allowedAction: MenuActionType? = null ) { NEW_PROJECT("_New Project", allowedAction = MenuActionType.OpenProject), - OPEN_PROJECT("Open _Project", icon = FontAwesomeIcon.FOLDER_OPEN, allowedAction = MenuActionType.OpenProject), - OPEN_SOURCE("_Open Source", PBK.OPEN_SOURCE, FontAwesomeIcon.FOLDER_OPEN, MenuActionType.AddSource), - EXPORT_SOURCE("_Export Source", PBK.EXPORT_SOURCE, FontAwesomeIcon.SAVE, MenuActionType.ExportSource), + OPEN_PROJECT("Open _Project...", icon = FontAwesomeIcon.FOLDER_OPEN, allowedAction = MenuActionType.OpenProject), + OPEN_SOURCE("_Open Source...", PBK.OPEN_SOURCE, FontAwesomeIcon.FOLDER_OPEN, MenuActionType.AddSource), + EXPORT_SOURCE("_Export Source...", PBK.EXPORT_SOURCE, FontAwesomeIcon.SAVE, MenuActionType.ExportSource), SAVE("_Save", PBK.SAVE, FontAwesomeIcon.SAVE, MenuActionType.SaveProject), - SAVE_AS("Save _As", PBK.SAVE_AS, FontAwesomeIcon.FLOPPY_ALT, MenuActionType.SaveProject), + SAVE_AS("Save _As...", PBK.SAVE_AS, FontAwesomeIcon.FLOPPY_ALT, MenuActionType.SaveProject), QUIT("_Quit", PBK.QUIT, FontAwesomeIcon.SIGN_OUT), CYCLE_FORWARD("Cycle _Forward", PBK.CYCLE_CURRENT_SOURCE_FORWARD, allowedAction = MenuActionType.ChangeActiveSource), CYCLE_BACKWARD("Cycle _Backward", PBK.CYCLE_CURRENT_SOURCE_BACKWARD, allowedAction = MenuActionType.ChangeActiveSource), TOGGLE_VISIBILITY("Toggle _Visibility", PBK.TOGGLE_CURRENT_SOURCE_VISIBILITY), - NEW_LABEL_SOURCE("_Label Source (N5)", PBK.CREATE_NEW_LABEL_DATASET, allowedAction = MenuActionType.AddSource), - NEW_CONNECTED_COMPONENT_SOURCE("_Fill Connected Components", PBK.FILL_CONNECTED_COMPONENTS), - NEW_THRESHOLDED_SOURCE("_Thresholded", PBK.THRESHOLDED), + NEW_LABEL_SOURCE("_Label Source...", PBK.CREATE_NEW_LABEL_DATASET, allowedAction = MenuActionType.AddSource), + NEW_CONNECTED_COMPONENT_SOURCE("_Fill Connected Components...", PBK.FILL_CONNECTED_COMPONENTS), + NEW_THRESHOLDED_SOURCE("_Threshold...", PBK.THRESHOLDED), TOGGLE_MENU_BAR_VISIBILITY("Toggle _Visibility", PBK.TOGGLE_MENUBAR_VISIBILITY), TOGGLE_MENU_BAR_MODE("Toggle _Mode", PBK.TOGGLE_MENUBAR_MODE), TOGGLE_STATUS_BAR_VISIBILITY("Toggle _Visibility", PBK.TOGGLE_STATUSBAR_VISIBILITY), @@ -51,12 +51,12 @@ enum class PainteraMenuItems( TOGGLE_TOOL_BAR_MENU_ITEM("Toggle _Visibility", PBK.TOGGLE_TOOL_BAR), RESET_3D_LOCATION_MENU_ITEM("_Reset 3D Location", PBK.RESET_3D_LOCATION), CENTER_3D_LOCATION_MENU_ITEM("_Center 3D Location", PBK.CENTER_3D_LOCATION), - SAVE_3D_PNG_MENU_ITEM("Save 3D As _PNG", PBK.SAVE_3D_PNG), + SAVE_3D_PNG_MENU_ITEM("Save 3D As _PNG...", PBK.SAVE_3D_PNG), FULL_SCREEN_ITEM("Toggle _Fullscreen", PBK.TOGGLE_FULL_SCREEN), - REPL_ITEM("Show _REPL", PBK.SHOW_REPL_TABS), + REPL_ITEM("Show _REPL...", PBK.SHOW_REPL_TABS), RESET_VIEWER_POSITIONS("Reset _Viewer Positions", PBK.RESET_VIEWER_POSITIONS), - SHOW_README("Show _Readme", PBK.OPEN_README, FontAwesomeIcon.QUESTION), - SHOW_KEY_BINDINGS("Show _Key Bindings", PBK.OPEN_KEY_BINDINGS, FontAwesomeIcon.KEYBOARD_ALT); + SHOW_README("Show _Readme...", PBK.OPEN_README, FontAwesomeIcon.QUESTION), + SHOW_KEY_BINDINGS("Show _Key Bindings...", PBK.OPEN_KEY_BINDINGS, FontAwesomeIcon.KEYBOARD_ALT); val menu: MenuItem by LazyForeignValue({ paintera }) { createMenuItem(it, this) } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenus.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenus.kt index c4b0a83ac..8444a9fab 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenus.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenus.kt @@ -42,7 +42,7 @@ private val currentSourceMenu by LazyForeignValue(::paintera) { ) } -private val showVersion by LazyForeignValue(::paintera) { MenuItem("Show _Version").apply { onAction = EventHandler { PainteraAlerts.versionDialog().show() } } } +private val showVersion by LazyForeignValue(::paintera) { MenuItem("Show _Version...").apply { onAction = EventHandler { PainteraAlerts.versionDialog().show() } } } private val recentProjects: ObservableList = FXCollections.observableArrayList() From 34c356a6b4ecae7db4bfd9a1235ad89b52813a15 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Tue, 29 Oct 2024 10:55:35 -0400 Subject: [PATCH 08/28] fix: re-check for sam server availability --- .../saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt index 6488da1de..7ba52b704 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt @@ -67,11 +67,12 @@ object SamEmbeddingLoaderCache : AsyncCacheWithLoader Date: Tue, 29 Oct 2024 11:35:49 -0400 Subject: [PATCH 09/28] refactor: improve non-volatile render support --- .../stream/HighlightingStreamConverter.java | 9 ++++-- ...ighlightingStreamConverterIntegerType.java | 6 ++-- ...ingStreamConverterVolatileIntegerType.java | 12 ++++++++ ...eamConverterVolatileLabelMultisetType.java | 22 ++------------ ...ghlightingStreamConverterVolatileType.java | 29 +++++++++++++++++++ .../saalfeldlab/bdv/fx/viewer/SourceUtils.kt | 3 +- 6 files changed, 55 insertions(+), 26 deletions(-) create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterVolatileIntegerType.java create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterVolatileType.java diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverter.java b/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverter.java index c2ae4f0e9..5f6ab14c5 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverter.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverter.java @@ -27,8 +27,8 @@ import java.util.Map; public abstract class HighlightingStreamConverter - implements Converter, SeedProperty, WithAlpha, ColorFromSegmentId, HideLockedSegments, - UserSpecifiedColors { + implements Converter, + SeedProperty, WithAlpha, ColorFromSegmentId, HideLockedSegments, UserSpecifiedColors { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -132,7 +132,10 @@ public static HighlightingStreamConverter forType( return (HighlightingStreamConverter)new HighlightingStreamConverterLabelMultisetType(stream); } if (t instanceof Volatile && ((Volatile)t).get() instanceof IntegerType) { - return (HighlightingStreamConverter)new HighlightingStreamConverterIntegerType(stream); + return (HighlightingStreamConverter)new HighlightingStreamConverterVolatileIntegerType<>(stream); + } + if (t instanceof IntegerType) { + return (HighlightingStreamConverter)new HighlightingStreamConverterIntegerType<>(stream); } return null; diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterIntegerType.java b/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterIntegerType.java index 53c4fb473..d06bb4e24 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterIntegerType.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterIntegerType.java @@ -8,7 +8,7 @@ import java.lang.invoke.MethodHandles; -public class HighlightingStreamConverterIntegerType, V extends Volatile> extends HighlightingStreamConverter { +public class HighlightingStreamConverterIntegerType> extends HighlightingStreamConverter { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -19,9 +19,9 @@ public HighlightingStreamConverterIntegerType(final AbstractHighlightingARGBStre } @Override - public void convert(final V input, final ARGBType output) { + public void convert(final I input, final ARGBType output) { - output.set(stream.argb(input.get().getIntegerLong())); + output.set(stream.argb(input.getIntegerLong())); } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterVolatileIntegerType.java b/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterVolatileIntegerType.java new file mode 100644 index 000000000..7cf8dd0dd --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterVolatileIntegerType.java @@ -0,0 +1,12 @@ +package org.janelia.saalfeldlab.paintera.stream; + +import net.imglib2.Volatile; +import net.imglib2.type.numeric.IntegerType; + +public class HighlightingStreamConverterVolatileIntegerType, V extends Volatile> extends HighlightingStreamConverterVolatileType { + + public HighlightingStreamConverterVolatileIntegerType(final AbstractHighlightingARGBStream stream) { + + super(stream, new HighlightingStreamConverterIntegerType(stream)); + } +} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterVolatileLabelMultisetType.java b/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterVolatileLabelMultisetType.java index 4b63cd133..c00b29c40 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterVolatileLabelMultisetType.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterVolatileLabelMultisetType.java @@ -1,28 +1,12 @@ package org.janelia.saalfeldlab.paintera.stream; +import net.imglib2.type.label.LabelMultisetType; import net.imglib2.type.label.VolatileLabelMultisetType; -import net.imglib2.type.numeric.ARGBType; -public class HighlightingStreamConverterVolatileLabelMultisetType extends HighlightingStreamConverter { +public class HighlightingStreamConverterVolatileLabelMultisetType extends HighlightingStreamConverterVolatileType { - private final HighlightingStreamConverterLabelMultisetType nonVolatileConverter; public HighlightingStreamConverterVolatileLabelMultisetType(final AbstractHighlightingARGBStream stream) { - super(stream); - nonVolatileConverter = new HighlightingStreamConverterLabelMultisetType(stream); - } - - public HighlightingStreamConverterLabelMultisetType getNonVolatileConverter() { - return nonVolatileConverter; - } - - @Override - public void convert(final VolatileLabelMultisetType input, final ARGBType output) { - - final boolean isValid = input.isValid(); - if (!isValid) { - return; - } - nonVolatileConverter.convert(input.get(), output); + super(stream, new HighlightingStreamConverterLabelMultisetType(stream)); } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterVolatileType.java b/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterVolatileType.java new file mode 100644 index 000000000..3dd363750 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterVolatileType.java @@ -0,0 +1,29 @@ +package org.janelia.saalfeldlab.paintera.stream; + +import net.imglib2.Volatile; +import net.imglib2.type.numeric.ARGBType; + +public abstract class HighlightingStreamConverterVolatileType> extends HighlightingStreamConverter { + + protected final HighlightingStreamConverter nonVolatileConverter; + + public HighlightingStreamConverterVolatileType(AbstractHighlightingARGBStream stream, HighlightingStreamConverter nonVolatileConverter) { + + super(stream); + this.nonVolatileConverter = nonVolatileConverter; + } + + public HighlightingStreamConverter getNonVolatileConverter() { + + return nonVolatileConverter; + } + + @Override public void convert(V input, ARGBType output) { + + final boolean isValid = input.isValid(); + if (!isValid) { + return; + } + getNonVolatileConverter().convert(input.get(), output); + } +} diff --git a/src/main/kotlin/org/janelia/saalfeldlab/bdv/fx/viewer/SourceUtils.kt b/src/main/kotlin/org/janelia/saalfeldlab/bdv/fx/viewer/SourceUtils.kt index 64cae7e35..d9835430e 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/bdv/fx/viewer/SourceUtils.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/bdv/fx/viewer/SourceUtils.kt @@ -8,6 +8,7 @@ import net.imglib2.realtransform.AffineTransform3D import net.imglib2.type.numeric.ARGBType import org.janelia.saalfeldlab.paintera.data.DataSource import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverterVolatileLabelMultisetType +import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverterVolatileType //FIXME Caleb: These are both private because imho this is a bit of a hack. // We want to Paintera's DataSource implemenet Source over the volatile type, @@ -59,7 +60,7 @@ internal fun getDataSourceAndConverter(sourceAndConverter: SourceAndCo val unwrappedDataSource = UnwrappedDataSource(data) val converter = sourceAndConverter.converter.let { - (it as? HighlightingStreamConverterVolatileLabelMultisetType)?.nonVolatileConverter ?: it + (it as? HighlightingStreamConverterVolatileType<*, *>)?.nonVolatileConverter ?: it } as Converter return WrappedSourceAndConverter(sourceAndConverter, unwrappedDataSource, converter) From d5ba019497a22d7d0c3bc854924fd4e2b762092b Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Tue, 29 Oct 2024 11:36:23 -0400 Subject: [PATCH 10/28] fix: valid tool switching, and SAM tool validity --- .../paintera/cache/SamEmbeddingLoaderCache.kt | 2 +- .../saalfeldlab/paintera/control/modes/Modes.kt | 11 +++++++---- .../paintera/control/tools/paint/SamTool.kt | 4 +++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt index 7ba52b704..c9e67ca3b 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt @@ -69,7 +69,7 @@ object SamEmbeddingLoaderCache : AsyncCacheWithLoader null // wrong mode + tool?.isValid() == false -> null // tool is not currently valid + else -> tool?.apply { activate() } // try to activate + } LOG.trace { "Activated $activeTool" } } @@ -154,7 +157,7 @@ interface ToolMode : SourceMode { toggles .firstOrNull { it.userData == newTool } ?.also { toggleForTool -> selectToggle(toggleForTool) } - val toolActionSets = newTool.actionSets.toTypedArray() + val toolActionSets = newTool.actionSets.toTypedArray() InvokeOnJavaFXApplicationThread { toolActionsBar.set(*toolActionSets) } } } @@ -381,7 +384,7 @@ abstract class AbstractToolMode : AbstractSourceMode(), ToolMode { override fun enter() { super.enter() - var statusSubscription : Subscription? = null + var statusSubscription: Subscription? = null activeToolProperty.subscribe { tool -> statusSubscription?.unsubscribe() statusSubscription = tool?.statusProperty?.subscribe { status -> 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 8d9199971..899ef9c4d 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 @@ -246,7 +246,9 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty Date: Tue, 29 Oct 2024 11:36:58 -0400 Subject: [PATCH 11/28] fix: reset to default on empty SAM Service only when focus lost --- .../paintera/config/SegmentAnythingConfig.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/SegmentAnythingConfig.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/SegmentAnythingConfig.kt index b04117c91..960dd244a 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/SegmentAnythingConfig.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/SegmentAnythingConfig.kt @@ -16,6 +16,7 @@ import javafx.scene.layout.VBox import org.janelia.saalfeldlab.fx.extensions.nonnull import org.janelia.saalfeldlab.fx.ui.NumberField import org.janelia.saalfeldlab.fx.ui.ObjectField +import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.paintera.config.SegmentAnythingConfig.Companion.DEFAULT_COMPRESS_ENCODING import org.janelia.saalfeldlab.paintera.config.SegmentAnythingConfig.Companion.DEFAULT_MODEL_LOCATION import org.janelia.saalfeldlab.paintera.config.SegmentAnythingConfig.Companion.DEFAULT_RESPONSE_TIMEOUT @@ -92,14 +93,15 @@ class SegmentAnythingConfigNode(val config: SegmentAnythingConfig) : TitledPane( VBox.setVgrow(it, Priority.NEVER) it.maxWidth = Double.MAX_VALUE it.prefWidth - Double.MAX_VALUE - it.textProperty().addListener { _, _, new -> - if (new.isBlank()) { + it.focusedProperty().subscribe { focused -> + if (!focused && it.text.isBlank()) { it.text = DEFAULT_SERVICE_URL - Platform.runLater { it.positionCaret(0) } - } else { - config.serviceUrl = new + InvokeOnJavaFXApplicationThread { it.positionCaret(0) } } } + it.textProperty().subscribe { _, new -> + config.serviceUrl = new + } add(it, 1, row) } Button().also { From 916b004b66c50cc7530425c0c3d4b608d8923e29 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Wed, 30 Oct 2024 13:32:36 -0400 Subject: [PATCH 12/28] feat: more granular ActionType permissions for resizing/toggling UI elements fix: issue where menu items remain disabled after using Shape Interpolation --- .../control/actions/AllowedActions.java | 2 +- .../control/actions/LabelActionType.java | 29 +++++++- .../control/actions/MenuActionType.java | 9 ++- .../paintera/BorderPaneWithStatusBars.kt | 7 +- .../paintera/PainteraDefaultHandlers.kt | 2 +- .../paintera/control/modes/PaintLabelMode.kt | 25 +++---- .../control/modes/ShapeInterpolationMode.kt | 2 +- .../paintera/control/tools/paint/SamTool.kt | 2 +- .../paintera/ui/menus/PainteraMenuItems.kt | 68 +++++++++++-------- 9 files changed, 97 insertions(+), 49 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java index f97fb38c8..976323395 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java @@ -58,7 +58,7 @@ public final class AllowedActions { @Nonnull public static final AllowedActions VIEW_LABELS = new AllowedActionsBuilder() .add(NAVIGATION) - .add(LabelActionType.Toggle, LabelActionType.Append, LabelActionType.SelectAll) + .add(LabelActionType.readOnly()) .create(); private final Set actions; diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelActionType.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelActionType.java index 9e533c87d..bd51a98d6 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelActionType.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelActionType.java @@ -3,13 +3,29 @@ import java.util.EnumSet; public enum LabelActionType implements ActionType { - Toggle, + Toggle(true), Append, CreateNew, - Lock, + Lock(true), Merge, Split, - SelectAll; + SelectAll(true); + + //TODO Caleb: consider moving this to ActionType. Maybe others too + private final boolean readOnly; + + LabelActionType() { + this(false); + } + + LabelActionType(boolean readOnly) { + this.readOnly = readOnly; + } + + public boolean isReadOnly() { + + return readOnly; + } public static EnumSet of(final LabelActionType first, final LabelActionType... rest) { @@ -21,6 +37,13 @@ public static EnumSet all() { return EnumSet.allOf(LabelActionType.class); } + public static EnumSet readOnly() { + + var readOnly = EnumSet.noneOf(LabelActionType.class); + all().stream().filter(LabelActionType::isReadOnly).forEach(readOnly::add); + return readOnly; + } + public static EnumSet none() { return EnumSet.noneOf(LabelActionType.class); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuActionType.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuActionType.java index 59e36b8ae..11e7fdfe1 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuActionType.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuActionType.java @@ -6,8 +6,15 @@ public enum MenuActionType implements ActionType { AddSource, ChangeActiveSource, - SidePanel, + ToggleSidePanel, + ResizePanel, + ToggleToolBarVisibility, + ToggleMenuBarVisibility, + ToggleMenuBarMode, + ToggleStatusBarVisibility, + ToggleStatusBarMode, ToggleMaximizeViewer, + ResizeViewers, OrthoslicesContextMenu, SaveProject, CommitCanvas, diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.kt index 4bec3923d..8e6cf0494 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.kt @@ -14,6 +14,7 @@ import javafx.scene.control.ScrollPane.ScrollBarPolicy import javafx.scene.control.TitledPane import javafx.scene.layout.* import javafx.scene.paint.Color +import org.janelia.saalfeldlab.fx.actions.verifyPermission import org.janelia.saalfeldlab.fx.extensions.createNonNullValueBinding import org.janelia.saalfeldlab.fx.extensions.createNullableValueBinding import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews @@ -21,6 +22,7 @@ import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews.ViewerAndTransforms import org.janelia.saalfeldlab.fx.ui.ResizeOnLeftSide import org.janelia.saalfeldlab.paintera.config.MenuBarConfig import org.janelia.saalfeldlab.paintera.config.StatusBarConfig +import org.janelia.saalfeldlab.paintera.control.actions.MenuActionType import org.janelia.saalfeldlab.paintera.control.modes.ToolMode import org.janelia.saalfeldlab.paintera.ui.Crosshair import org.janelia.saalfeldlab.paintera.ui.SettingsView @@ -112,7 +114,10 @@ class BorderPaneWithStatusBars(paintera: PainteraMainWindow) { val pane = BorderPane(centerPane, topGroup, scrollPane, bottomGroup, null) @Suppress("unused") - private val resizeSideBar = ResizeOnLeftSide(scrollPane, sideBarWidthProperty).apply { install() } + private val resizeSideBar = ResizeOnLeftSide(scrollPane, sideBarWidthProperty).apply { + verifyPermission(MenuActionType.ResizePanel) + install() + } private val statusBarPrefWidth = Bindings.createDoubleBinding( { pane.width - if (painteraProperties.sideBarConfig.isVisible) painteraProperties.sideBarConfig.width else 0.0 }, diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.kt index a83022886..13b7efd59 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.kt @@ -171,7 +171,7 @@ class PainteraDefaultHandlers(private val paintera: PainteraMainWindow, paneWith val borderPane = paneWithStatus.pane baseView.allowedActionsProperty().addListener { _, _, new -> - val disableSidePanel = new.isAllowed(MenuActionType.SidePanel).not() + val disableSidePanel = new.isAllowed(MenuActionType.ToggleSidePanel).not() paneWithStatus.scrollPane.disableProperty().set(disableSidePanel) } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt index 5cf7aba0f..4228da9f3 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt @@ -26,8 +26,7 @@ import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews import org.janelia.saalfeldlab.fx.ui.ScaleView import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.paintera.DeviceManager -import org.janelia.saalfeldlab.paintera.LabelSourceStateKeys -import org.janelia.saalfeldlab.paintera.LabelSourceStateKeys.SHAPE_INTERPOLATION__TOGGLE_MODE +import org.janelia.saalfeldlab.paintera.LabelSourceStateKeys.* import org.janelia.saalfeldlab.paintera.cache.SamEmbeddingLoaderCache import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationController import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions @@ -101,7 +100,7 @@ object PaintLabelMode : AbstractToolMode() { } private val toggleFill3D = painteraActionSet("toggle fill 3D overlay", PaintActionType.Fill) { - KEY_PRESSED(KeyCode.F, KeyCode.SHIFT) { + KEY_PRESSED(FILL_3D) { onAction { switchTool(fill3DTool) } } KEY_PRESSED { @@ -109,19 +108,21 @@ object PaintLabelMode : AbstractToolMode() { filter = true consume = true verifyEventNotNull() - verify { it!!.code in listOf(KeyCode.F, KeyCode.SHIFT) && activeTool is Fill3DTool } + verify { it!!.code in FILL_3D.keyCodes && activeTool is Fill3DTool } } KEY_RELEASED { verifyEventNotNull() - keysReleased(KeyCode.F, KeyCode.SHIFT) + keysReleased(*FILL_3D.keyCodes.toTypedArray()) verify { activeTool is Fill3DTool } onAction { - when (it!!.code) { - KeyCode.F -> switchTool(NavigationTool) - KeyCode.SHIFT -> switchTool(fill2DTool) - else -> return@onAction + + val nextTool = when { + keyTracker()?.areKeysDown(FILL_3D) == true -> return@onAction + keyTracker()?.areKeysDown(FILL_2D) == true -> fill2DTool + else -> NavigationTool } + switchTool(nextTool) } } } @@ -151,7 +152,7 @@ object PaintLabelMode : AbstractToolMode() { } } - private val activeSamTool = painteraActionSet(LabelSourceStateKeys.SEGMENT_ANYTHING__TOGGLE_MODE, PaintActionType.Paint) { + private val activeSamTool = painteraActionSet(SEGMENT_ANYTHING__TOGGLE_MODE, PaintActionType.Paint) { KEY_PRESSED(samTool.keyTrigger) { verify { SamEmbeddingLoaderCache.canReachServer } verify { activeSourceStateProperty.get() is ConnectomicsLabelState<*, *> } @@ -171,7 +172,7 @@ object PaintLabelMode : AbstractToolMode() { switchTool(defaultTool) } } - KEY_PRESSED(LabelSourceStateKeys.CANCEL) { + KEY_PRESSED(CANCEL) { verify { activeSourceStateProperty.get() is ConnectomicsLabelState<*, *> } verify { activeTool is SamTool } filter = true @@ -205,7 +206,7 @@ object PaintLabelMode : AbstractToolMode() { ) private fun getSelectNextIdActions() = painteraActionSet("Create New Segment", LabelActionType.CreateNew) { - KEY_PRESSED(LabelSourceStateKeys.NEXT_ID) { + KEY_PRESSED(NEXT_ID) { name = "create new segment" verify { activeTool?.let { it !is PaintTool || !it.isPainting } ?: true } onAction { 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 1eda55b03..7b5fe2733 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 @@ -81,7 +81,7 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat override val allowedActions = AllowedActions.AllowedActionsBuilder() .add(PaintActionType.ShapeInterpolation, PaintActionType.Paint, PaintActionType.Erase, PaintActionType.SetBrushSize, PaintActionType.Fill, PaintActionType.SegmentAnything) - .add(MenuActionType.ToggleMaximizeViewer, MenuActionType.DetachViewer) + .add(MenuActionType.ToggleMaximizeViewer, MenuActionType.DetachViewer, MenuActionType.ToggleSidePanel, MenuActionType.ResizePanel) .add(NavigationActionType.Pan, NavigationActionType.Slice, NavigationActionType.Zoom) .create() 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 899ef9c4d..a09ebf0a5 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 @@ -124,6 +124,7 @@ import kotlin.math.* import kotlin.properties.Delegates +//TODO Caleb: refactor to a mode, with proper AllowedActions, and separation of tool logic from sam logic open class SamTool(activeSourceStateProperty: SimpleObjectProperty?>, mode: ToolMode? = null) : PaintTool(activeSourceStateProperty, mode) { override val graphic = { GlyphScaleView(FontAwesomeIconView().also { it.styleClass += "sam-select" }) } @@ -254,7 +255,6 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty = emptyArray() ) { - NEW_PROJECT("_New Project", allowedAction = MenuActionType.OpenProject), - OPEN_PROJECT("Open _Project...", icon = FontAwesomeIcon.FOLDER_OPEN, allowedAction = MenuActionType.OpenProject), - OPEN_SOURCE("_Open Source...", PBK.OPEN_SOURCE, FontAwesomeIcon.FOLDER_OPEN, MenuActionType.AddSource), - EXPORT_SOURCE("_Export Source...", PBK.EXPORT_SOURCE, FontAwesomeIcon.SAVE, MenuActionType.ExportSource), - SAVE("_Save", PBK.SAVE, FontAwesomeIcon.SAVE, MenuActionType.SaveProject), - SAVE_AS("Save _As...", PBK.SAVE_AS, FontAwesomeIcon.FLOPPY_ALT, MenuActionType.SaveProject), + NEW_PROJECT("_New Project", requiredActionTypes = arrayOf(OpenProject)), + OPEN_PROJECT("Open _Project...", icon = FontAwesomeIcon.FOLDER_OPEN, requiredActionTypes = arrayOf(OpenProject, LoadProject)), + OPEN_SOURCE("_Open Source...", PBK.OPEN_SOURCE, FontAwesomeIcon.FOLDER_OPEN, arrayOf(AddSource)), + EXPORT_SOURCE("_Export Source...", PBK.EXPORT_SOURCE, FontAwesomeIcon.SAVE, arrayOf(ExportSource)), + SAVE("_Save", PBK.SAVE, FontAwesomeIcon.SAVE, arrayOf(SaveProject)), + SAVE_AS("Save _As...", PBK.SAVE_AS, FontAwesomeIcon.FLOPPY_ALT, arrayOf(SaveProject)), QUIT("_Quit", PBK.QUIT, FontAwesomeIcon.SIGN_OUT), - - CYCLE_FORWARD("Cycle _Forward", PBK.CYCLE_CURRENT_SOURCE_FORWARD, allowedAction = MenuActionType.ChangeActiveSource), - CYCLE_BACKWARD("Cycle _Backward", PBK.CYCLE_CURRENT_SOURCE_BACKWARD, allowedAction = MenuActionType.ChangeActiveSource), + CYCLE_FORWARD("Cycle _Forward", PBK.CYCLE_CURRENT_SOURCE_FORWARD, requiredActionTypes = arrayOf(ChangeActiveSource)), + CYCLE_BACKWARD("Cycle _Backward", PBK.CYCLE_CURRENT_SOURCE_BACKWARD, requiredActionTypes = arrayOf(ChangeActiveSource)), TOGGLE_VISIBILITY("Toggle _Visibility", PBK.TOGGLE_CURRENT_SOURCE_VISIBILITY), - NEW_LABEL_SOURCE("_Label Source...", PBK.CREATE_NEW_LABEL_DATASET, allowedAction = MenuActionType.AddSource), - NEW_CONNECTED_COMPONENT_SOURCE("_Fill Connected Components...", PBK.FILL_CONNECTED_COMPONENTS), - NEW_THRESHOLDED_SOURCE("_Threshold...", PBK.THRESHOLDED), - TOGGLE_MENU_BAR_VISIBILITY("Toggle _Visibility", PBK.TOGGLE_MENUBAR_VISIBILITY), - TOGGLE_MENU_BAR_MODE("Toggle _Mode", PBK.TOGGLE_MENUBAR_MODE), - TOGGLE_STATUS_BAR_VISIBILITY("Toggle _Visibility", PBK.TOGGLE_STATUSBAR_VISIBILITY), - TOGGLE_STATUS_BAR_MODE("Toggle _Mode", PBK.TOGGLE_STATUSBAR_MODE), - TOGGLE_SIDE_BAR_MENU_ITEM("Toggle _Visibility", PBK.TOGGLE_SIDE_BAR), - TOGGLE_TOOL_BAR_MENU_ITEM("Toggle _Visibility", PBK.TOGGLE_TOOL_BAR), - RESET_3D_LOCATION_MENU_ITEM("_Reset 3D Location", PBK.RESET_3D_LOCATION), - CENTER_3D_LOCATION_MENU_ITEM("_Center 3D Location", PBK.CENTER_3D_LOCATION), - SAVE_3D_PNG_MENU_ITEM("Save 3D As _PNG...", PBK.SAVE_3D_PNG), - FULL_SCREEN_ITEM("Toggle _Fullscreen", PBK.TOGGLE_FULL_SCREEN), + NEW_LABEL_SOURCE("_Label Source...", PBK.CREATE_NEW_LABEL_DATASET, requiredActionTypes = arrayOf(AddSource)), + NEW_CONNECTED_COMPONENT_SOURCE("_Fill Connected Components...", PBK.FILL_CONNECTED_COMPONENTS, requiredActionTypes = arrayOf(CreateVirtualSource)), + NEW_THRESHOLDED_SOURCE("_Threshold...", PBK.THRESHOLDED, requiredActionTypes = arrayOf(CreateVirtualSource)), + TOGGLE_MENU_BAR_VISIBILITY("Toggle _Visibility", PBK.TOGGLE_MENUBAR_VISIBILITY, requiredActionTypes = arrayOf(ToggleMenuBarVisibility)), + TOGGLE_MENU_BAR_MODE("Toggle _Mode", PBK.TOGGLE_MENUBAR_MODE, requiredActionTypes = arrayOf(ToggleMenuBarMode)), + TOGGLE_STATUS_BAR_VISIBILITY("Toggle _Visibility", PBK.TOGGLE_STATUSBAR_VISIBILITY, requiredActionTypes = arrayOf(ToggleStatusBarVisibility)), + TOGGLE_STATUS_BAR_MODE("Toggle _Mode", PBK.TOGGLE_STATUSBAR_MODE, requiredActionTypes = arrayOf(ToggleStatusBarMode)), + TOGGLE_SIDE_BAR_MENU_ITEM("Toggle _Visibility", PBK.TOGGLE_SIDE_BAR, requiredActionTypes = arrayOf(ToggleSidePanel)), + TOGGLE_TOOL_BAR_MENU_ITEM("Toggle _Visibility", PBK.TOGGLE_TOOL_BAR, requiredActionTypes = arrayOf(ToggleToolBarVisibility)), + RESET_3D_LOCATION_MENU_ITEM("_Reset 3D Location", PBK.RESET_3D_LOCATION, requiredActionTypes = arrayOf(OrthoslicesContextMenu)), + CENTER_3D_LOCATION_MENU_ITEM("_Center 3D Location", PBK.CENTER_3D_LOCATION, requiredActionTypes = arrayOf(OrthoslicesContextMenu)), + SAVE_3D_PNG_MENU_ITEM("Save 3D As _PNG...", PBK.SAVE_3D_PNG, requiredActionTypes = arrayOf(OrthoslicesContextMenu)), + FULL_SCREEN_ITEM("Toggle _Fullscreen", PBK.TOGGLE_FULL_SCREEN, requiredActionTypes = arrayOf(ResizeViewers, ResizePanel)), REPL_ITEM("Show _REPL...", PBK.SHOW_REPL_TABS), - RESET_VIEWER_POSITIONS("Reset _Viewer Positions", PBK.RESET_VIEWER_POSITIONS), + RESET_VIEWER_POSITIONS("Reset _Viewer Positions", PBK.RESET_VIEWER_POSITIONS, requiredActionTypes = arrayOf(ResizeViewers, ToggleMaximizeViewer, DetachViewer)), SHOW_README("Show _Readme...", PBK.OPEN_README, FontAwesomeIcon.QUESTION), SHOW_KEY_BINDINGS("Show _Key Bindings...", PBK.OPEN_KEY_BINDINGS, FontAwesomeIcon.KEYBOARD_ALT); @@ -112,9 +115,18 @@ enum class PainteraMenuItems( icon?.let { graphic = FontAwesome[it, 1.5] } onAction = handler namedKeyCombindations[keys]?.let { acceleratorProperty().bind(it.primaryCombinationProperty) } - /* Set up the disabled binding*/ - allowedAction?.let { - disableProperty().bind(paintera.baseView.allowedActionsProperty().allowedActionBinding(allowedAction).not()) + /* Set up the disabled binding by permission type*/ + + val allowedActionsProperty = paintera.baseView.allowedActionsProperty() + val permissionDeniedBinding = { actionType: ActionType -> + Bindings.createBooleanBinding({ !allowedActionsProperty.isAllowed(actionType) }, allowedActionsProperty) + } + var disabledByPermissionsBinding: BooleanBinding? = if (requiredActionTypes.isEmpty()) null else Bindings.createBooleanBinding({false}) + for (actionType in requiredActionTypes) { + disabledByPermissionsBinding = disabledByPermissionsBinding!!.or(permissionDeniedBinding(actionType)) + } + disabledByPermissionsBinding?.let { + disableProperty().bind(it) } } } From 9c45524b47f770c4e2b85a67251ffa5c1217297e Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Thu, 31 Oct 2024 10:28:20 -0400 Subject: [PATCH 13/28] feat: optionally activate replacement label during smoothing, add Smooth permissions --- .../control/actions/PaintActionType.java | 3 +- .../fx/actions/PainteraActionSet.kt | 20 ++++-- .../control/actions/paint/SmoothAction.kt | 68 +++++++++++++++---- 3 files changed, 70 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintActionType.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintActionType.java index baaa3a57f..e5ee8ba75 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintActionType.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintActionType.java @@ -11,7 +11,8 @@ public enum PaintActionType implements ActionType { SetBrushSize, SetBrushDepth, ShapeInterpolation, - SegmentAnything; + SegmentAnything, + Smooth; public static EnumSet of(final PaintActionType first, final PaintActionType... rest) { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/fx/actions/PainteraActionSet.kt b/src/main/kotlin/org/janelia/saalfeldlab/fx/actions/PainteraActionSet.kt index 466e9f220..67a94e731 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/fx/actions/PainteraActionSet.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/fx/actions/PainteraActionSet.kt @@ -9,9 +9,17 @@ import org.janelia.saalfeldlab.paintera.paintera import java.util.function.Consumer -fun ActionSet.verifyPermission(actionType: ActionType? = null) { - actionType?.let { permission -> - verifyAll(Event.ANY, "Permission for $permission") { paintera.baseView.allowedActionsProperty().hasPermission(permission) } +fun ActionSet.verifyPermission(vararg actionType: ActionType) { + if (actionType.isEmpty()) return + actionType.forEach { permission -> + verifyAll(Event.ANY, " No Permission for $permission") { paintera.baseView.allowedActionsProperty().hasPermission(permission) } + } +} + +fun Action<*>.verifyPermission(vararg actionType: ActionType) { + if (actionType.isEmpty()) return + actionType.forEach { permission -> + verify( "No Permission for $permission") { paintera.baseView.allowedActionsProperty().hasPermission(permission) } } } @@ -31,7 +39,7 @@ fun painteraActionSet(namedKey: NamedKeyBinding, actionType: ActionType? = null, @JvmSynthetic fun painteraActionSet(name: String, actionType: ActionType? = null, ignoreDisable: Boolean = false, apply: (ActionSet.() -> Unit)?): ActionSet { return ActionSet(name, { paintera.keyTracker }).apply { - verifyPermission(actionType) + actionType?.let { (verifyPermission(it)) } if (!ignoreDisable) { verifyPainteraNotDisabled() } @@ -56,7 +64,7 @@ fun painteraDragActionSet( apply: (DragActionSet.() -> Unit)? ): DragActionSet { return DragActionSet(name, { paintera.keyTracker }, filter, consumeMouseClicked).apply { - verifyPermission(actionType) + actionType?.let { (verifyPermission(it)) } if (!ignoreDisable) { verifyPainteraNotDisabled() } @@ -74,7 +82,7 @@ fun painteraMidiActionSet( apply: (MidiActionSet.() -> Unit)? ): MidiActionSet { return MidiActionSet(name, device, target, { paintera.keyTracker }) { - verifyPermission(actionType) + actionType?.let { (verifyPermission(it)) } if (!ignoreDisable) { verifyPainteraNotDisabled() } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt index b2cbb41cc..aa54c0b35 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt @@ -24,6 +24,7 @@ import javafx.scene.layout.HBox import javafx.scene.layout.Priority import javafx.scene.layout.VBox import javafx.util.Duration +import javafx.util.Subscription import kotlinx.coroutines.* import net.imglib2.FinalInterval import net.imglib2.FinalRealInterval @@ -41,6 +42,7 @@ import net.imglib2.type.numeric.integer.UnsignedLongType import net.imglib2.type.numeric.real.DoubleType import net.imglib2.util.Intervals import org.janelia.saalfeldlab.fx.actions.Action +import org.janelia.saalfeldlab.fx.actions.verifyPermission import org.janelia.saalfeldlab.fx.extensions.* import org.janelia.saalfeldlab.fx.ui.NumberField import org.janelia.saalfeldlab.fx.ui.ObjectField.SubmitOn @@ -49,6 +51,8 @@ import org.janelia.saalfeldlab.labels.blocks.LabelBlockLookupKey import org.janelia.saalfeldlab.net.imglib2.view.BundleView import org.janelia.saalfeldlab.paintera.Paintera import org.janelia.saalfeldlab.paintera.Style.ADD_GLYPH +import org.janelia.saalfeldlab.paintera.control.actions.PaintActionType +import org.janelia.saalfeldlab.paintera.control.actions.paint.SmoothAction.activateReplacement import org.janelia.saalfeldlab.paintera.control.actions.paint.SmoothActionVerifiedState.Companion.verifyState import org.janelia.saalfeldlab.paintera.control.modes.PaintLabelMode import org.janelia.saalfeldlab.paintera.control.modes.PaintLabelMode.statePaintContext @@ -77,7 +81,6 @@ import kotlin.properties.Delegates import kotlin.reflect.KMutableProperty0 import net.imglib2.type.label.Label as Imglib2Label - open class MenuAction(val label: String) : Action(Event.ANY) { @@ -95,6 +98,8 @@ open class MenuAction(val label: String) : Action(Event.ANY) { } + +//TODO Caleb: Separate Smooth UI from SmoothAction class SmoothActionVerifiedState { internal lateinit var labelSource: ConnectomicsLabelState<*, *> internal lateinit var paintContext: StatePaintContext<*, *> @@ -114,6 +119,10 @@ class SmoothActionVerifiedState { companion object { fun Action.verifyState(state: SmoothActionVerifiedState) { state.run { verifyState() } + verify("Paint Label Mode is Active") { paintera.currentMode is PaintLabelMode } + verify("Paintera is not disabled") { !paintera.baseView.isDisabledProperty.get() } + verify("Mask is in Use") { !state.paintContext.dataSource.isMaskInUseBinding().get() } + } } } @@ -132,9 +141,21 @@ object SmoothAction : MenuAction("_Smooth...") { private var smoothJob: Deferred?>? = null private var convolutionExecutor = newConvolutionExecutor() - private val replacementLabelProperty = SimpleLongProperty(0) + private val replacementLabelProperty = SimpleLongProperty(0).apply { + subscribe { prev, next -> activateReplacementLabel(prev.toLong(), next.toLong()) } + } private val replacementLabel by replacementLabelProperty.nonnullVal() + private val activateReplacementProperty = SimpleBooleanProperty(true).apply { + subscribe { _, activate -> + if (activate) + activateReplacementLabel(0L, replacementLabel) + else + activateReplacementLabel(replacementLabel, 0L) + } + } + private val activateReplacement by activateReplacementProperty.nonnull() + private val kernelSizeProperty = SimpleDoubleProperty() private val kernelSize by kernelSizeProperty.nonnullVal() @@ -164,9 +185,7 @@ object SmoothAction : MenuAction("_Smooth...") { init { verifyState(state) - verify("Paint Label Mode is Active") { paintera.currentMode is PaintLabelMode } - verify("Paintera is not disabled") { !paintera.baseView.isDisabledProperty.get() } - verify("Mask is in Use") { !state.paintContext.dataSource.isMaskInUseBinding().get() } + verifyPermission(PaintActionType.Smooth, PaintActionType.Erase, PaintActionType.Background, PaintActionType.Fill) onAction { finalizeSmoothing = false /* Set lateinit values */ @@ -191,6 +210,17 @@ object SmoothAction : MenuAction("_Smooth...") { private val AffineTransform3D.resolution get() = doubleArrayOf(this[0, 0], this[1, 1], this[2, 2]) + private fun activateReplacementLabel(current : Long, next : Long) { + val selectedIds = state.paintContext.selectedIds + if (current != selectedIds.lastSelection) { + selectedIds.deactivate(current) + + } + if (activateReplacement && next > 0L) { + selectedIds.activateAlso(selectedIds.lastSelection, next) + } + } + private fun SmoothActionVerifiedState.showSmoothDialog() { Dialog().apply { Paintera.registerStylesheets(dialogPane) @@ -241,14 +271,19 @@ object SmoothAction : MenuAction("_Smooth...") { val timeline = Timeline() - dialogPane.content = VBox(10.0).apply { isFillWidth = true val replacementIdLabel = Label("Replacement Label") val kernelSizeLabel = Label("Kernel Size (phyiscal units)") + val activateLabel = CheckBox("").apply { + tooltip = Tooltip("Select Replacement ID") + selectedProperty().bindBidirectional(activateReplacementProperty) + selectedProperty().set(true) + } replacementIdLabel.alignment = Pos.BOTTOM_RIGHT kernelSizeLabel.alignment = Pos.BOTTOM_RIGHT - children += HBox(10.0, replacementIdLabel, nextIdButton, replacementLabelField.textField).also { + activateLabel.alignment = Pos.CENTER_LEFT + children += HBox(10.0, replacementIdLabel, nextIdButton, replacementLabelField.textField, activateLabel).also { it.disableProperty().bind(paintera.baseView.isDisabledProperty) it.cursorProperty().bind(paintera.baseView.node.cursorProperty()) } @@ -381,15 +416,20 @@ object SmoothAction : MenuAction("_Smooth...") { @OptIn(ExperimentalCoroutinesApi::class) private fun SmoothActionVerifiedState.startSmoothTask() { val prevScales = paintera.activeViewer.get()!!.screenScales - - val kernelSizeChange: ChangeListener = ChangeListener { _, _, _ -> - if (scopeJob?.isActive == true) - cancelActiveSmoothing("Kernel Size Changed") - resmooth = true + val smoothTriggerListener = { reason : String -> + { _ : Any? -> + if (scopeJob?.isActive == true) + cancelActiveSmoothing(reason) + resmooth = true + } } + var smoothTriggerSubscription: Subscription = Subscription.EMPTY smoothJob = CoroutineScope(Dispatchers.Default).async { - kernelSizeProperty.addListener(kernelSizeChange) + val kernelSizeChangeSubscription = kernelSizeProperty.subscribe(smoothTriggerListener("Kernel Size Changed")) + val replacementLabelChangeSubscription = replacementLabelProperty.subscribe(smoothTriggerListener("Replacement Label Changed")) + smoothTriggerSubscription.unsubscribe() + smoothTriggerSubscription = kernelSizeChangeSubscription.and(replacementLabelChangeSubscription) paintera.baseView.orthogonalViews().setScreenScales(doubleArrayOf(prevScales[0])) smoothing = true initializeSmoothLabel() @@ -437,7 +477,7 @@ object SmoothAction : MenuAction("_Smooth...") { } paintera.baseView.disabledPropertyBindings -= task - kernelSizeProperty.removeListener(kernelSizeChange) + smoothTriggerSubscription?.unsubscribe() paintera.baseView.orthogonalViews().setScreenScales(prevScales) convolutionExecutor.shutdown() } From 5043f5a267e15c475c76a9c36ab2883474f78e98 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Thu, 31 Oct 2024 14:18:45 -0400 Subject: [PATCH 14/28] fix: change intensity threshold bindings to H to not conflict with key rotation around Y axis --- .../saalfeldlab/paintera/BindingKeys.kt | 4 +- .../paintera/control/modes/RawSourceMode.kt | 86 +++++++++++-------- 2 files changed, 50 insertions(+), 40 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt index c564409d1..b9057e142 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt @@ -159,8 +159,8 @@ enum class LabelSourceStateKeys(lateInitNamedKeyCombo : LateInitNamedKeyCombinat } enum class RawSourceStateKeys(lateInitNamedKeyCombo : LateInitNamedKeyCombination) : NamedKeyBinding by lateInitNamedKeyCombo { - RESET_MIN_MAX_INTENSITY_THRESHOLD ( SHIFT_DOWN + Y, "Reset Min / Max Intensity Threshold"), - AUTO_MIN_MAX_INTENSITY_THRESHOLD ( Y, "Auto Min / Max Intensity Threshold"), + RESET_MIN_MAX_INTENSITY_THRESHOLD ( SHIFT_DOWN + H, "Reset Min / Max Intensity Threshold"), + AUTO_MIN_MAX_INTENSITY_THRESHOLD ( H, "Auto Min / Max Intensity Threshold"), ; diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/RawSourceMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/RawSourceMode.kt index d74dd700b..ca363f2b8 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/RawSourceMode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/RawSourceMode.kt @@ -3,7 +3,6 @@ package org.janelia.saalfeldlab.paintera.control.modes import javafx.beans.value.ChangeListener import javafx.collections.FXCollections import javafx.collections.ObservableList -import javafx.scene.input.KeyCode import javafx.scene.input.KeyEvent.KEY_PRESSED import net.imglib2.RandomAccessibleInterval import net.imglib2.histogram.Histogram1d @@ -53,10 +52,11 @@ object RawSourceMode : AbstractToolMode() { } } KEY_PRESSED(RawSourceStateKeys.AUTO_MIN_MAX_INTENSITY_THRESHOLD) { - lateinit var viewer: ViewerPanelFX graphic = { ScaleView().apply { styleClass += "intensity-auto-min-max" } } - verify("Last focused viewer found") { paintera.baseView.lastFocusHolder.value?.viewer()?.also { viewer = it } != null } onAction { + val viewer = paintera.baseView.run { + lastFocusHolder.value ?: orthogonalViews().topLeft + }.viewer() val rawSource = activeSourceStateProperty.get() as ConnectomicsRawState<*, *> autoIntensityMinMax(rawSource, viewer) } @@ -68,7 +68,7 @@ object RawSourceMode : AbstractToolMode() { val viewerInterval = Intervals.createMinSize(0, 0, 0, viewer.width.toLong(), viewer.height.toLong(), 1L) val scaleLevel = viewer.state.bestMipMapLevel - val dataSource = rawSource.getDataSource().getDataSource(0, scaleLevel) as RandomAccessibleInterval> + val dataSource = rawSource.getDataSource().getSource(0, scaleLevel) as RandomAccessibleInterval> val sourceToGlobalTransform = rawSource.getDataSource().getSourceTransformCopy(0, scaleLevel) @@ -89,21 +89,19 @@ object RawSourceMode : AbstractToolMode() { .interval(viewerInterval) val converter = rawSource.converter() - val curMin = converter.minProperty().get() - val curMax = converter.maxProperty().get() + val curMin = converter.minProperty().get() + val curMax = converter.maxProperty().get() - if ( curMin == curMax) { + if (curMin == curMax) { resetIntensityMinMax(rawSource) return } - if (converterAtDefault(rawSource)) { - estimateWithRange(screenSource, converter) - } - if (extension is IntegerType<*>) - estimateWithHistogram(IntType(), screenSource, rawSource, converter) - else - estimateWithHistogram(DoubleType(), screenSource, rawSource, converter) + when { + converterAtDefault(rawSource) -> estimateWithRange(screenSource, converter) + extension is IntegerType<*> -> estimateWithHistogram(IntType(), screenSource, rawSource, converter) + else -> estimateWithHistogram(DoubleType(), screenSource, rawSource, converter) + } } private fun estimateWithRange(screenSource: IntervalView>, converter: ARGBColorConverter>) { @@ -121,18 +119,37 @@ object RawSourceMode : AbstractToolMode() { } private fun > estimateWithHistogram(type: T, screenSource: IntervalView>, rawSource: ConnectomicsRawState<*, *>, converter: ARGBColorConverter>) { - val binMapper = Real1dBinMapper(converter.min, converter.max, 4, false) + val numSamples = Intervals.numElements(screenSource) + val numBins = numSamples.coerceIn(100, 1000) + val binMapper = Real1dBinMapper(converter.min, converter.max, numBins, false) val histogram = Histogram1d(binMapper) val img = screenSource.convertRAI(type) { src, target -> target.setReal(src.realDouble) }.asIterable() histogram.countData(img) - val numPixels = screenSource.dimensionsAsLongArray().sum() - - - val minBinIdx = histogram.indexOfFirst { i -> i.get() > (numPixels / 5000) } - val maxBinIdx = histogram.indexOfLast { i -> i.get() > (numPixels / 5000) } + val counts = histogram.toLongArray() + var runningSumMin = 0L + var runningSumMax = 0L + var minBinIdx = 0 + var maxBinIdx = counts.size - 1 + val threshold = numSamples / 25 + for (i in counts.indices) { + val count = counts[i] + runningSumMin += count + if (runningSumMin >= threshold) { + minBinIdx = i + break + } + } + for (i in counts.indices.reversed()) { + val count = counts[i] + runningSumMax += count + if (runningSumMax >= threshold) { + maxBinIdx = i + break + } + } - val updateOrResetConverter = { min : Double, max : Double -> + val updateOrResetConverter = { min: Double, max: Double -> if (converter.minProperty().value == min && converter.maxProperty().value == max) resetIntensityMinMax(rawSource) else { @@ -141,28 +158,21 @@ object RawSourceMode : AbstractToolMode() { } } - when { - minBinIdx == -1 && maxBinIdx == -1 -> resetIntensityMinMax(rawSource) - minBinIdx == maxBinIdx -> { - updateOrResetConverter( - histogram.getLowerBound(minBinIdx.toLong(), type).let { type.realDouble }, - histogram.getUpperBound(maxBinIdx.toLong(), type).let { type.realDouble } - ) - } - - else -> { - updateOrResetConverter( - histogram.getCenterValue(minBinIdx.toLong(), type).let { type.realDouble }, - histogram.getCenterValue(maxBinIdx.toLong(), type).let { type.realDouble } - ) - } + if (minBinIdx >= maxBinIdx) { + resetIntensityMinMax(rawSource) + return } + + updateOrResetConverter( + histogram.getLowerBound(minBinIdx.toLong(), type).let { type.realDouble }, + histogram.getUpperBound(maxBinIdx.toLong(), type).let { type.realDouble } + ) } - private fun converterAtDefault(rawSource: ConnectomicsRawState<*, *>) : Boolean { + private fun converterAtDefault(rawSource: ConnectomicsRawState<*, *>): Boolean { val converter = rawSource.converter() (rawSource.backend as? SourceStateBackendN5<*, *>)?.getMetadataState()?.let { - return converter.min == it.minIntensity && converter.max == it.maxIntensity + return converter.min == it.minIntensity && converter.max == it.maxIntensity } val dataSource = rawSource.getDataSource().getDataSource(0, 0) as RandomAccessibleInterval> From 38b8225787e326aba30a2226d26550cb3acb0054 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Thu, 31 Oct 2024 15:11:45 -0400 Subject: [PATCH 15/28] refactor: use actions for 3d context menu --- .../paintera/PainteraDefaultHandlers.kt | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.kt index 13b7efd59..4eb856785 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.kt @@ -1,9 +1,6 @@ package org.janelia.saalfeldlab.paintera -import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import bdv.fx.viewer.multibox.MultiBoxOverlayConfig -import org.janelia.saalfeldlab.bdv.fx.viewer.multibox.MultiBoxOverlayRendererFX -import org.janelia.saalfeldlab.bdv.fx.viewer.scalebar.ScaleBarOverlayRenderer import bdv.viewer.Interpolation import bdv.viewer.Source import javafx.beans.InvalidationListener @@ -19,6 +16,8 @@ import javafx.scene.Node import javafx.scene.control.ContextMenu import javafx.scene.input.* import javafx.scene.input.KeyEvent.KEY_PRESSED +import javafx.scene.input.MouseEvent.MOUSE_CLICKED +import javafx.scene.input.MouseEvent.MOUSE_PRESSED import javafx.scene.layout.BorderPane import javafx.scene.layout.GridPane import javafx.scene.layout.StackPane @@ -28,6 +27,9 @@ import net.imglib2.FinalRealInterval import net.imglib2.Interval import net.imglib2.realtransform.AffineTransform3D import net.imglib2.util.Intervals +import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX +import org.janelia.saalfeldlab.bdv.fx.viewer.multibox.MultiBoxOverlayRendererFX +import org.janelia.saalfeldlab.bdv.fx.viewer.scalebar.ScaleBarOverlayRenderer import org.janelia.saalfeldlab.control.mcu.MCUButtonControl.TOGGLE_OFF import org.janelia.saalfeldlab.control.mcu.MCUButtonControl.TOGGLE_ON import org.janelia.saalfeldlab.fx.actions.Action.Companion.installAction @@ -35,8 +37,10 @@ import org.janelia.saalfeldlab.fx.actions.ActionSet import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.installActionSet import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.removeActionSet import org.janelia.saalfeldlab.fx.actions.KeyAction.Companion.onAction +import org.janelia.saalfeldlab.fx.actions.MouseAction import org.janelia.saalfeldlab.fx.actions.painteraActionSet import org.janelia.saalfeldlab.fx.actions.painteraMidiActionSet +import org.janelia.saalfeldlab.fx.extensions.nullable import org.janelia.saalfeldlab.fx.midi.MidiToggleEvent import org.janelia.saalfeldlab.fx.ortho.DynamicCellPane import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews @@ -243,41 +247,38 @@ class PainteraDefaultHandlers(private val paintera: PainteraMainWindow, paneWith } val contextMenuFactory = MeshesGroupContextMenu(baseView.manager()) + val contextMenuProperty = SimpleObjectProperty() + var contextMenu by contextMenuProperty.nullable() val hideContextMenu = { - if (contextMenuProperty.get() != null) { - contextMenuProperty.get().hide() - contextMenuProperty.set(null) + contextMenu?.let { + it.hide() + contextMenu = null } } - baseView.viewer3D().meshesGroup.addEventHandler( - MouseEvent.MOUSE_CLICKED - ) { - LOG.debug("Handling event {}", it) - if (baseView.isActionAllowed(MenuActionType.OrthoslicesContextMenu) && - MouseButton.SECONDARY == it.button && - it.clickCount == 1 && - !paintera.mouseTracker.isDragging - ) { - LOG.debug("Check passed for event {}", it) - it.consume() - val pickResult = it.pickResult - if (pickResult.intersectedNode != null) { - val pt = pickResult.intersectedPoint - val menu = contextMenuFactory.createMenu(doubleArrayOf(pt.x, pt.y, pt.z)) - menu.show(baseView.viewer3D(), it.screenX, it.screenY) - contextMenuProperty.set(menu) - } else { - hideContextMenu() - } - } else { - hideContextMenu() - } + + val meshContextMenuActions = painteraActionSet("3D_mesh_context_menu", MenuActionType.OrthoslicesContextMenu, ignoreDisable = true) { + MOUSE_CLICKED(MouseButton.SECONDARY) { + verify("single click") { it?.clickCount == 1 } + verify("not dragging") { !paintera.mouseTracker.isDragging } + onAction { + it?.pickResult?.let { result -> + if (result.intersectedNode != null) { + val point = result.intersectedPoint + val menu = contextMenuFactory.createMenu(doubleArrayOf(point.x, point.y, point.z)) + contextMenuProperty.set(menu) + menu.show(baseView.viewer3D(), it.screenX, it.screenY) + } + } ?: hideContextMenu() + } + } } - // hide the context menu when clicked outside the meshes - baseView.viewer3D().addEventHandler( - MouseEvent.MOUSE_CLICKED - ) { hideContextMenu() } + baseView.viewer3D().meshesGroup.installActionSet(meshContextMenuActions) + baseView.viewer3D().installAction ( + MouseAction(MOUSE_PRESSED).apply { + onAction { hideContextMenu() } + } + ) this.baseView.orthogonalViews().topLeft.viewer().addTransformListener(scaleBarOverlays[0]) this.baseView.orthogonalViews().topLeft.viewer().display.addOverlayRenderer(scaleBarOverlays[0]) From d6908b8e577ec7dec7bf0b0e41e984431ddc9271 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Thu, 31 Oct 2024 15:12:09 -0400 Subject: [PATCH 16/28] feat: change default 3D rotation speeds --- .../saalfeldlab/paintera/viewer3d/Scene3DHandler.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/viewer3d/Scene3DHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/viewer3d/Scene3DHandler.java index d87b62641..4c3d55a2a 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/viewer3d/Scene3DHandler.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/viewer3d/Scene3DHandler.java @@ -64,7 +64,7 @@ public Scene3DHandler(final Viewer3DFX viewer) { ActionSet.installActionSet(viewer, additionalCommands); Paintera.whenPaintable(() -> { - /* These depend on the keyTracker in the PainteraMainWindow, so cannot be installed until the MainWindow is done intializing. */ + /* These depend on the keyTracker in the PainteraMainWindow, so cannot be installed until the MainWindow is done initializing. */ final var zoomActionSet = zoom3D(); ActionSet.installActionSet(viewer, zoomActionSet); }); @@ -187,16 +187,16 @@ private void drag(MouseEvent event) { private class Rotate3DView extends DragActionSet { - private double baseSpeed = 1.0; + private double baseSpeed = 0.25; private double factor = 1.0; private double speed = baseSpeed * factor; - private final static double SLOW_FACTOR = 0.1; + private final static double SLOW_FACTOR = 0.5; private final static double NORMAL_FACTOR = 1; - private final static double FAST_FACTOR = 2; + private final static double FAST_FACTOR = 2.0; private final Affine affineDragStart = new Affine(); From fecbd2b7d756d41bbc866d1972d8d7fca4039631 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Fri, 1 Nov 2024 14:41:39 -0400 Subject: [PATCH 17/28] fix: return fill interval from async, instead of querying property. avoids race conditions with `release()` --- .../paintera/control/paint/FloodFill2D.kt | 15 +++++---------- .../paintera/control/tools/paint/Fill2DTool.kt | 5 +++-- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.kt index ac0377f1b..bc205a6da 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.kt @@ -48,13 +48,8 @@ class FloodFill2D>( internal var viewerMask: ViewerMask? = null - private val maskIntervalProperty = ReadOnlyObjectWrapper(null) - val readOnlyMaskInterval: ReadOnlyObjectProperty = maskIntervalProperty.readOnlyProperty - val maskInterval by readOnlyMaskInterval.nullableVal() - fun release() { viewerMask = null - maskIntervalProperty.set(null) } private fun getOrCreateViewerMask(): ViewerMask { @@ -66,21 +61,22 @@ class FloodFill2D>( } } - suspend fun fillViewerAt(viewerSeedX: Double, viewerSeedY: Double, fill: Long, assignment: FragmentSegmentAssignment) { + suspend fun fillViewerAt(viewerSeedX: Double, viewerSeedY: Double, fill: Long, assignment: FragmentSegmentAssignment): Interval { val mask = getOrCreateViewerMask() val maskPos = mask.displayPointToMask(viewerSeedX, viewerSeedY, true) val filter = getBackgroundLabelMaskForAssignment(maskPos, mask, assignment, fill) - fillMaskAt(maskPos, mask, fill, filter) + val interval = fillMaskAt(maskPos, mask, fill, filter) if (!coroutineContext.isActive) { mask.source.resetMasks() mask.requestRepaint() } + return interval } - private suspend fun fillMaskAt(maskPos: Point, mask: ViewerMask, fill: Long, filter: RandomAccessibleInterval) { + private suspend fun fillMaskAt(maskPos: Point, mask: ViewerMask, fill: Long, filter: RandomAccessibleInterval) : Interval { if (fill == Label.INVALID) { val reason = "Received invalid label -- will not fill" LOG.warn { reason } @@ -119,8 +115,7 @@ class FloodFill2D>( launchRepaintRequestUpdater(fillContext, triggerRefresh, mask, sourceAccessTracker::createAccessInterval) fillAt(maskPos, sourceAccessTracker, filter, fill) - if (fillContext.isActive) - maskIntervalProperty.set(sourceAccessTracker.createAccessInterval()) + return sourceAccessTracker.createAccessInterval() } private fun launchRepaintRequestUpdater(fillContext: CoroutineContext, triggerRefresh: AtomicBoolean, mask: ViewerMask, interval: () -> Interval) { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill2DTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill2DTool.kt index de987a293..4a20763c9 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill2DTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill2DTool.kt @@ -126,6 +126,7 @@ open class Fill2DTool(activeSourceStateProperty: SimpleObjectProperty Unit = {}): Job? { val applyIfMaskNotProvided = fill2D.viewerMask == null @@ -136,7 +137,7 @@ open class Fill2DTool(activeSourceStateProperty: SimpleObjectProperty @@ -158,7 +159,7 @@ open class Fill2DTool(activeSourceStateProperty: SimpleObjectProperty Date: Fri, 1 Nov 2024 14:42:05 -0400 Subject: [PATCH 18/28] refactor: select next id verify --- .../saalfeldlab/paintera/control/modes/PaintLabelMode.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt index 4228da9f3..a7c867768 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt @@ -32,6 +32,7 @@ import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationController import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions import org.janelia.saalfeldlab.paintera.control.actions.LabelActionType import org.janelia.saalfeldlab.paintera.control.actions.PaintActionType +import org.janelia.saalfeldlab.paintera.control.actions.paint.SmoothAction.onAction import org.janelia.saalfeldlab.paintera.control.tools.Tool import org.janelia.saalfeldlab.paintera.control.tools.paint.* import org.janelia.saalfeldlab.paintera.control.tools.paint.PaintTool.Companion.createPaintStateContext @@ -207,8 +208,8 @@ object PaintLabelMode : AbstractToolMode() { private fun getSelectNextIdActions() = painteraActionSet("Create New Segment", LabelActionType.CreateNew) { KEY_PRESSED(NEXT_ID) { - name = "create new segment" - verify { activeTool?.let { it !is PaintTool || !it.isPainting } ?: true } + name = "create_new_segment" + verify("Not Painting") { (activeTool as? PaintTool)?.isPainting?.not() ?: true } onAction { statePaintContext?.nextId(activate = true) } From 0131a1fe01833f34adbdf0cb357e498569cc8e2b Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Fri, 1 Nov 2024 14:46:05 -0400 Subject: [PATCH 19/28] fix: correctly handle more general resolution for arbitrary orthoslice painting. perf: work more in mask space, to avoid unnecessary conversions to source space when applying mask to canvas --- .../control/ShapeInterpolationController.kt | 4 +- .../control/modes/ShapeInterpolationMode.kt | 2 +- .../paintera/control/paint/ViewerMask.kt | 272 ++++++++++-------- 3 files changed, 151 insertions(+), 127 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 9521bad57..a78eb149b 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt @@ -622,7 +622,7 @@ class ShapeInterpolationController>( if (oldMask.xScaleChange == 1.0) return@let oldMask val maskInfo = MaskInfo(0, targetMipMapLevel) - val newMask = source.createViewerMask(maskInfo, activeViewer!!, paintDepth = null, setMask = false) + val newMask = source.createViewerMask(maskInfo, activeViewer!!, setMask = false) val oldToNewMask = ViewerMask.maskToMaskTransformation(oldMask, newMask) @@ -666,7 +666,7 @@ class ShapeInterpolationController>( newMask } ?: let { val maskInfo = MaskInfo(0, targetMipMapLevel) - source.createViewerMask(maskInfo, activeViewer!!, paintDepth = null, setMask = false) + source.createViewerMask(maskInfo, activeViewer!!, setMask = false) } currentViewerMask?.setViewerMaskOnSource() 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 7b5fe2733..a38ea34d0 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 @@ -394,7 +394,7 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat val maskInfo = MaskInfo(0, currentBestMipMapLevel) - val mask = source.createViewerMask(maskInfo, viewer, paintDepth = null, setMask = false, initialGlobalToViewerTransform = globalToViewerTransform) + val mask = source.createViewerMask(maskInfo, viewer, setMask = false, initialGlobalToViewerTransform = globalToViewerTransform) val activeSource = activeSourceStateProperty.value!!.sourceAndConverter!!.spimSource val sources = mask.viewer.state.sources 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 6e738271a..2be2ad1c0 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 @@ -14,7 +14,6 @@ import net.imglib2.type.numeric.RealType import net.imglib2.type.numeric.integer.UnsignedLongType import net.imglib2.type.volatiles.VolatileUnsignedLongType import net.imglib2.util.Intervals -import net.imglib2.util.LinAlgHelpers import net.imglib2.view.Views import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import org.janelia.saalfeldlab.fx.extensions.component1 @@ -32,7 +31,9 @@ import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.smallestC import org.janelia.saalfeldlab.util.* import java.util.concurrent.atomic.AtomicBoolean import java.util.function.Predicate -import kotlin.math.ceil +import kotlin.math.absoluteValue +import kotlin.math.roundToLong +import kotlin.math.sqrt class ViewerMask private constructor( val source: MaskedSource, *>, @@ -42,7 +43,7 @@ class ViewerMask private constructor( invalidateVolatile: Invalidate<*>? = null, shutdown: Runnable? = null, private inline val paintedLabelIsValid: (Long) -> Boolean = { MaskedSource.VALID_LABEL_CHECK.test(it) }, - val paintDepthFactor: Double? = 1.0, + val paintDepthFactor: Double = 1.0, private val maskSize: Interval? = null, private val defaultValue: Long = Label.INVALID, globalToViewerTransform: AffineTransform3D? = null @@ -139,12 +140,16 @@ class ViewerMask private constructor( private val depthScale = let { - val sourceToMaskVals = initialMaskToSourceTransform.inverse().rowPackedCopy - DoubleArray(3) { i -> - LinAlgHelpers.length(doubleArrayOf(sourceToMaskVals[i * 4 + 0], sourceToMaskVals[i * 4 + 1], sourceToMaskVals[i * 4 + 2])) - }.max() + var sqSum = 0.0 + val row = 2 + val transform = initialMaskToSourceTransform.inverse() + for (col in 0..2) { + val x = transform.get(row, col) + sqSum += x * x + } + sqrt(sqSum) } - val depthScaleTransform get() = Scale3D(1.0, 1.0, paintDepthFactor?.times(depthScale) ?: 1.0) + val depthScaleTransform get() = Scale3D(1.0, 1.0, paintDepthFactor.times(depthScale)) /** * The ratio of the initial 0 dimensions scale to the current 0 dimension scale. @@ -346,149 +351,164 @@ class ViewerMask private constructor( acceptAsPainted: Predicate ): Set { - val extendedViewerImg = Views.extendValue(viewerImg, Label.INVALID) + val extendedViewerImg = Views.extendBorder(viewerImg) val viewerImgInSourceOverCanvas = viewerImgInSource.raster().interval(canvas) val sourceToMaskTransform = currentMaskToSourceWithDepthTransform.inverse() + val sourceToMaskTransformAsArray = sourceToMaskTransform.rowPackedCopy + val sourceToMaskWithDepthTransform = currentMaskToSourceWithDepthTransform.inverse() + val sourceToMaskWithDepthTransformAsArray = sourceToMaskWithDepthTransform.rowPackedCopy - val sourceToMaskNoTranslation = sourceToMaskTransform.copy().apply { setTranslation(0.0, 0.0, 0.0) } - - val unitXInMask = doubleArrayOf(1.0, 0.0, 0.0).also { sourceToMaskNoTranslation.apply(it, it) } - val unitYInMask = doubleArrayOf(0.0, 1.0, 0.0).also { sourceToMaskNoTranslation.apply(it, it) } - val unitZInMask = doubleArrayOf(0.0, 0.0, 1.0).also { sourceToMaskNoTranslation.apply(it, it) } - - val zeros = doubleArrayOf(0.0, 0.0, 0.0).also { sourceToMaskNoTranslation.apply(it, it) } + val maxDistInMask = (paintDepthFactor * depthScale) * .5 + val minDistInMask = paintDepthFactor * .5 - val maskToSourceScaleX = Affine3DHelpers.extractScale(sourceToMaskTransform.inverse(), 0) - val maskToSourceScaleY = Affine3DHelpers.extractScale(sourceToMaskTransform.inverse(), 1) - val maskToSourceScaleZ = Affine3DHelpers.extractScale(sourceToMaskTransform.inverse(), 2) - val maskScaleInSource = doubleArrayOf( - maskToSourceScaleX, - maskToSourceScaleY, - maskToSourceScaleZ * (paintDepthFactor ?: 1.0) - ).also { - sourceToMaskNoTranslation.inverse().apply(it, it) + val zTransformAtCubeCorner: (DoubleArray, Int?) -> Double = { pos, idx -> + val x = pos[0] + (idx?.let { CUBE_CORNERS[it][0] } ?: 0.0) + val y = pos[1] + (idx?.let { CUBE_CORNERS[it][1] } ?: 0.0) + val z = pos[2] + (idx?.let { CUBE_CORNERS[it][2] } ?: 0.0) + sourceToMaskTransformAsArray.let { transform -> + transform[8] * x + transform[9] * y + transform[10] * z + transform[11] + } } - val maxSourceDistInMask = LinAlgHelpers.distance(maskScaleInSource, zeros) / 2 - val minSourceDistInMask = (paintDepthFactor ?: 1.0) * minOf( - LinAlgHelpers.distance(zeros, unitXInMask), - LinAlgHelpers.distance(zeros, unitYInMask), - LinAlgHelpers.distance(zeros, unitZInMask) - ) / 2 - val sourceToMaskTransformAsArray = sourceToMaskTransform.rowPackedCopy val painted = AtomicBoolean(false) val paintedLabelSet = hashSetOf() + fun trackPaintedLabel(painted: Long) { synchronized(paintedLabelSet) { paintedLabelSet += painted } } + val paintCanvas: (IntegerType<*>, Long) -> Unit = { position, id -> + position.setInteger(id) + painted.set(true) + trackPaintedLabel(id) + } + LoopBuilder.setImages( BundleView(canvas).interval(canvas), viewerImgInSourceOverCanvas - ) - .multiThreaded() - .forEachChunk { chunk -> - val realMinMaskPoint = DoubleArray(3) - val realMaxMaskPoint = DoubleArray(3) - - val minMaskPoint = LongArray(3) - val maxMaskPoint = LongArray(3) - - val canvasPosition = DoubleArray(3) - val canvasPositionInMask = DoubleArray(3) - val canvasPositionInMaskAsRealPoint = RealPoint.wrap(canvasPositionInMask) - - chunk.forEachPixel { canvasBundle, viewerValType -> - canvasBundle.localize(canvasPosition) - val sourceDepthInMask = sourceToMaskTransformAsArray.let { - it[8] * canvasPosition[0] + it[9] * canvasPosition[1] + it[10] * canvasPosition[2] + it[11] + ).multiThreaded().forEachChunk { chunk -> + val realMinMaskPoint = DoubleArray(3) + val realMaxMaskPoint = DoubleArray(3) + + val minMaskPoint = LongArray(3) + val maxMaskPoint = LongArray(3) + + val canvasPosition = DoubleArray(3) + val canvasMinPositionInMask = DoubleArray(3) + val canvasMaxPositionInMask = DoubleArray(3) + val cubeCornerDepths = Array(CUBE_CORNERS.size) { null } + + + chunk.forEachPixel { canvasBundle, viewerValType -> + canvasBundle.localize(canvasPosition) + cubeCornerDepths.fill(null) + + var withinMax = false + for (idx in cubeCornerDepths.indices) { + + cubeCornerDepths[idx] = zTransformAtCubeCorner(canvasPosition, idx).also { + withinMax = it.absoluteValue <= maxDistInMask } - sourceToMaskTransform.apply(canvasPosition, canvasPositionInMask) - if (sourceDepthInMask <= maxSourceDistInMask) { - val viewerVal = viewerValType.get() - if (acceptAsPainted.test(viewerVal)) { - val paintVal = viewerVal - if (sourceDepthInMask < minSourceDistInMask) { - canvasBundle.get().setInteger(paintVal) - } else { - var hasPositive = false - var hasNegative = false - for (corner in CUBE_CORNERS) { - /* transform z only */ - val z = sourceToMaskTransformAsArray.let { - it[8] * (canvasPosition[0] + corner[0]) + it[9] * (canvasPosition[1] + corner[1]) + it[10] * (canvasPosition[2] + corner[2]) + it[11] - } - if (z >= 0) { - hasPositive = true - } else { - hasNegative = true - } - if (hasPositive && hasNegative) { - canvasBundle.get().setInteger(paintVal) - painted.set(true) - trackPaintedLabel(paintVal) - break - } - } + if (withinMax) + break + } + + if (!withinMax) { + return@forEachPixel + } + + var paintVal = viewerValType.get() + + val checkAllCornersForPaint: () -> Boolean = { + var accepted = false + var inMin = false + var pos = false + var neg = false + val cornerPos = canvasPosition.copyOf() + for (idx in CUBE_CORNERS.indices) { + val offset = CUBE_CORNERS[idx] + cornerPos.indices.forEach { i -> cornerPos[i] = canvasPosition[i] + offset[i] } + sourceToMaskTransform.apply(cornerPos, cornerPos) + cubeCornerDepths[idx] = cornerPos[2] + val maskPos = LongArray(3) { cornerPos[it].roundToLong() } + val id = extendedViewerImg.get(*maskPos).get() + accepted = accepted || acceptAsPainted.test(id).also { paintVal = 3 } + val maskZ = maskPos[2] + pos = pos || maskPos[2] >= 0 + neg = neg || maskPos[2] <= 0 + + if (accepted) { + if (pos && neg) { + paintVal = 3 + break + } + inMin = maskZ.absoluteValue < minDistInMask + if (inMin) { + paintVal = 2 + break } - } else { - /* nearest neighbor interval over source */ - realMinMaskPoint[0] = canvasPosition[0] - .5 - realMinMaskPoint[1] = canvasPosition[1] - .5 - realMinMaskPoint[2] = canvasPosition[2] - .5 - realMaxMaskPoint[0] = canvasPosition[0] + .5 - realMaxMaskPoint[1] = canvasPosition[1] + .5 - realMaxMaskPoint[2] = canvasPosition[2] + .5 - - val realIntervalOverSource = FinalRealInterval(realMinMaskPoint, realMaxMaskPoint) - - maskScaleInSource.forEachIndexed { idx, depth -> - realMinMaskPoint[idx] -= depth - realMaxMaskPoint[idx] += depth + } + } + accepted && ((pos && neg) || inMin) + } + + if (acceptAsPainted.test(paintVal)) { + if (zTransformAtCubeCorner(canvasPosition, null).absoluteValue < minDistInMask) + paintCanvas(canvasBundle.get(), 1) + else if (checkAllCornersForPaint()) { + paintCanvas(canvasBundle.get(), paintVal) + } + } else { + /* nearest neighbor interval over source */ + for (idx in 0 until 3) { + realMinMaskPoint[idx] = canvasPosition[idx] + MIN_CORNER_OFFSET[idx] + realMaxMaskPoint[idx] = canvasPosition[idx] + MAX_CORNER_OFFSET[idx] + } + + val realIntervalOverSource = FinalRealInterval(realMinMaskPoint, realMaxMaskPoint, false) + + /* iterate over canvas in viewer */ + val realIntervalOverMask = sourceToMaskWithDepthTransform.estimateBounds(realIntervalOverSource).smallestContainingInterval + if (0 !in realIntervalOverMask.min(2)..realIntervalOverMask.max(2)) { + return@forEachPixel + } + + minMaskPoint[0] = realIntervalOverMask.min(0) + minMaskPoint[1] = realIntervalOverMask.min(1) + minMaskPoint[2] = 0 + + maxMaskPoint[0] = realIntervalOverMask.max(0) + maxMaskPoint[1] = realIntervalOverMask.max(1) + maxMaskPoint[2] = 0 + + val maskInterval = FinalInterval(minMaskPoint, maxMaskPoint) + + val maskCursor = extendedViewerImg.interval(maskInterval).cursor() + while (maskCursor.hasNext()) { + val maskId = maskCursor.next().get() + if (acceptAsPainted.test(maskId)) { + for (idx in 0 until 2) { + canvasMinPositionInMask[idx] = maskCursor.getDoublePosition(idx) + MIN_CORNER_OFFSET[idx] + canvasMaxPositionInMask[idx] = maskCursor.getDoublePosition(idx) + MAX_CORNER_OFFSET[idx] } - /* iterate over canvas in viewer */ - val realIntervalOverMask = sourceToMaskTransform.estimateBounds(realIntervalOverSource) - if (0.0 in realIntervalOverMask.realMin(2)..realIntervalOverMask.realMax(2)) { - - minMaskPoint[0] = realIntervalOverMask.realMin(0).toLong() - minMaskPoint[1] = realIntervalOverMask.realMin(1).toLong() - minMaskPoint[2] = 0 - - maxMaskPoint[0] = ceil(realIntervalOverMask.realMax(0)).toLong() - maxMaskPoint[1] = ceil(realIntervalOverMask.realMax(1)).toLong() - maxMaskPoint[2] = 0 - - val maskInterval = FinalInterval(minMaskPoint, maxMaskPoint) - - val maskCursor = extendedViewerImg.interval(maskInterval).cursor() - while (maskCursor.hasNext()) { - val maskId = maskCursor.next().get() - if (acceptAsPainted.test(maskId)) { - maskCursor.localize(canvasPositionInMask) - sourceToMaskTransform.inverse().apply(canvasPositionInMask, canvasPositionInMask) - /* just renaming for clarity. */ - @Suppress("UnnecessaryVariable") val maskPointInSource = canvasPositionInMaskAsRealPoint - if (!Intervals.contains(realIntervalOverSource, maskPointInSource)) { - continue - } - canvasBundle.get().setInteger(maskId) - painted.set(true) - trackPaintedLabel(maskId) - break - } - } + val maskPixelInterval = FinalRealInterval(canvasMinPositionInMask, canvasMaxPositionInMask, false) + val canvasInterval = sourceToMaskWithDepthTransform.inverse().estimateBounds(maskPixelInterval) + if (!Intervals.isEmpty(Intervals.intersect(realIntervalOverSource, canvasInterval))) { + paintCanvas(canvasBundle.get(), 4) // maskId) + return@forEachPixel } } } } } + } paintera.baseView.orthogonalViews().requestRepaint(sourceToGlobalTransform.estimateBounds(canvas.extendBy(1.0))) return paintedLabelSet } @@ -532,9 +552,12 @@ class ViewerMask private constructor( companion object { private val CELL_DIMS = intArrayOf(64, 64, 1) + private val MIN_CORNER_OFFSET = doubleArrayOf(-.5, -.5, -.5) + private val MAX_CORNER_OFFSET = doubleArrayOf(+.5, +.5, +.5) + private val CUBE_CORNERS = arrayOf( - doubleArrayOf(-.5, -.5, -.5), - doubleArrayOf(+.5, +.5, +.5), + MIN_CORNER_OFFSET, + MAX_CORNER_OFFSET, doubleArrayOf(-.5, -.5, +.5), doubleArrayOf(+.5, +.5, -.5), @@ -546,6 +569,7 @@ class ViewerMask private constructor( doubleArrayOf(+.5, -.5, -.5), ) + @JvmStatic fun ViewerPanelFX.getGlobalViewerInterval(): RealInterval { val zeroGlobal = doubleArrayOf(0.0, 0.0, 0.0).also { displayToGlobalCoordinates(it) } @@ -558,7 +582,7 @@ class ViewerMask private constructor( fun MaskedSource<*, *>.createViewerMask( maskInfo: MaskInfo, viewer: ViewerPanelFX, - paintDepth: Double? = 1.0, + paintDepth: Double = 1.0, maskSize: Interval? = null, defaultValue: Long = Label.INVALID, setMask: Boolean = true, From f1085d40b0c4f3a84d467a88c42fc4d9877f4872 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Tue, 5 Nov 2024 16:29:50 -0500 Subject: [PATCH 20/28] fix: in practice, these edge cases are not hit. Need to think better how to integrate them if we want some other heuristics --- .../paintera/control/paint/ViewerMask.kt | 43 ++----------------- 1 file changed, 3 insertions(+), 40 deletions(-) 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 2be2ad1c0..b1e922eae 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 @@ -425,46 +425,9 @@ class ViewerMask private constructor( var paintVal = viewerValType.get() - val checkAllCornersForPaint: () -> Boolean = { - var accepted = false - var inMin = false - var pos = false - var neg = false - val cornerPos = canvasPosition.copyOf() - for (idx in CUBE_CORNERS.indices) { - val offset = CUBE_CORNERS[idx] - cornerPos.indices.forEach { i -> cornerPos[i] = canvasPosition[i] + offset[i] } - sourceToMaskTransform.apply(cornerPos, cornerPos) - cubeCornerDepths[idx] = cornerPos[2] - val maskPos = LongArray(3) { cornerPos[it].roundToLong() } - val id = extendedViewerImg.get(*maskPos).get() - accepted = accepted || acceptAsPainted.test(id).also { paintVal = 3 } - val maskZ = maskPos[2] - pos = pos || maskPos[2] >= 0 - neg = neg || maskPos[2] <= 0 - - if (accepted) { - if (pos && neg) { - paintVal = 3 - break - } - inMin = maskZ.absoluteValue < minDistInMask - if (inMin) { - paintVal = 2 - break - } - } - } - accepted && ((pos && neg) || inMin) - } - - if (acceptAsPainted.test(paintVal)) { - if (zTransformAtCubeCorner(canvasPosition, null).absoluteValue < minDistInMask) - paintCanvas(canvasBundle.get(), 1) - else if (checkAllCornersForPaint()) { - paintCanvas(canvasBundle.get(), paintVal) - } - } else { + if (acceptAsPainted.test(paintVal) && zTransformAtCubeCorner(canvasPosition, null).absoluteValue < minDistInMask) + paintCanvas(canvasBundle.get(), 1) + else { /* nearest neighbor interval over source */ for (idx in 0 until 3) { realMinMaskPoint[idx] = canvasPosition[idx] + MIN_CORNER_OFFSET[idx] From bb60448cffbfa2cf213df1c6a36667c590bc98f5 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Wed, 6 Nov 2024 16:06:57 -0500 Subject: [PATCH 21/28] fix: update ViewerDisplayListener after repaint. Don't create objects unnecessarily --- .../OrthogonalViewsValueDisplayListener.java | 4 ++- .../navigation/ValueDisplayListener.java | 4 +-- .../bdv/fx/viewer/render/ViewerRenderUnit.kt | 27 +++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/OrthogonalViewsValueDisplayListener.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/OrthogonalViewsValueDisplayListener.java index 1ee85d7a0..fefc312b8 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/OrthogonalViewsValueDisplayListener.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/OrthogonalViewsValueDisplayListener.java @@ -49,7 +49,9 @@ public OrthogonalViewsValueDisplayListener( public void addHandlers(ViewerPanelFX viewer) { - listeners.putIfAbsent(viewer, new ValueDisplayListener<>(viewer, currentSource, interpolation, submitValue)); + if (!listeners.containsKey(viewer)) { + listeners.put(viewer, new ValueDisplayListener<>(viewer, currentSource, interpolation, submitValue)); + } viewer.getDisplay().addEventFilter(MouseEvent.MOUSE_MOVED, listeners.get(viewer)); viewer.addTransformListener(listeners.get(viewer)); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/ValueDisplayListener.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/ValueDisplayListener.java index 0b6d8a1d0..6f58e7a42 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/ValueDisplayListener.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/ValueDisplayListener.java @@ -14,12 +14,10 @@ import net.imglib2.Volatile; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.realtransform.RealViews; -import net.imglib2.type.label.VolatileLabelMultisetType; import net.imglib2.view.composite.Composite; import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX; import org.janelia.saalfeldlab.fx.Tasks; import org.janelia.saalfeldlab.paintera.data.ChannelDataSource; -import org.janelia.saalfeldlab.paintera.data.DataSource; import java.util.Arrays; import java.util.HashMap; @@ -67,7 +65,7 @@ public ValueDisplayListener( ), affine ).realRandomAccess(); - }, currentSource, viewer.getRenderUnit().getScreenScalesProperty(), viewerTransformChanged + }, currentSource, viewer.getRenderUnit().getScreenScalesProperty(), viewerTransformChanged, viewer.getRenderUnit().getRepaintRequestProperty() ); this.accessBinding.addListener((obs, old, newAccess) -> { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/bdv/fx/viewer/render/ViewerRenderUnit.kt b/src/main/kotlin/org/janelia/saalfeldlab/bdv/fx/viewer/render/ViewerRenderUnit.kt index 31d8e82f0..ffb0febb9 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/bdv/fx/viewer/render/ViewerRenderUnit.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/bdv/fx/viewer/render/ViewerRenderUnit.kt @@ -5,6 +5,7 @@ import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerState import bdv.viewer.Interpolation import bdv.viewer.Source import bdv.viewer.render.AccumulateProjectorFactory +import javafx.beans.property.SimpleBooleanProperty import net.imglib2.parallel.TaskExecutor import net.imglib2.realtransform.AffineTransform3D import net.imglib2.type.numeric.ARGBType @@ -32,10 +33,36 @@ open class ViewerRenderUnit( renderingTaskExecutor, useVolatileIfAvailable = true ) { + + val repaintRequestProperty = SimpleBooleanProperty() + override fun paint() { if (viewerStateSupplier.get()?.isVisible == true) super.paint() } + + private fun notifyRepaintObservable() = repaintRequestProperty.set(!repaintRequestProperty.value) + + override fun requestRepaint() { + super.requestRepaint() + notifyRepaintObservable() + } + + override fun requestRepaint(screenScaleIndex: Int) { + super.requestRepaint(screenScaleIndex) + notifyRepaintObservable() + } + + override fun requestRepaint(min: LongArray?, max: LongArray?) { + super.requestRepaint(min, max) + notifyRepaintObservable() + } + + override fun requestRepaint(screenScaleIndex: Int, min: LongArray?, max: LongArray?) { + super.requestRepaint(screenScaleIndex, min, max) + notifyRepaintObservable() + } + companion object { private fun Supplier.getRenderState() : () -> RenderUnitState? = { From 702dd8e6c4ca101e18bda1fe4af7822c549b4c4c Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Thu, 7 Nov 2024 16:59:05 -0500 Subject: [PATCH 22/28] fix: remove debug code --- .../saalfeldlab/paintera/control/paint/ViewerMask.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 b1e922eae..34837ddae 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 @@ -423,10 +423,10 @@ class ViewerMask private constructor( return@forEachPixel } - var paintVal = viewerValType.get() + val paintVal = viewerValType.get() if (acceptAsPainted.test(paintVal) && zTransformAtCubeCorner(canvasPosition, null).absoluteValue < minDistInMask) - paintCanvas(canvasBundle.get(), 1) + paintCanvas(canvasBundle.get(), paintVal) else { /* nearest neighbor interval over source */ for (idx in 0 until 3) { @@ -464,7 +464,7 @@ class ViewerMask private constructor( val maskPixelInterval = FinalRealInterval(canvasMinPositionInMask, canvasMaxPositionInMask, false) val canvasInterval = sourceToMaskWithDepthTransform.inverse().estimateBounds(maskPixelInterval) if (!Intervals.isEmpty(Intervals.intersect(realIntervalOverSource, canvasInterval))) { - paintCanvas(canvasBundle.get(), 4) // maskId) + paintCanvas(canvasBundle.get(), maskId) return@forEachPixel } } From e57cec97c5de8ac72210583435f33af21fd000cf Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Thu, 7 Nov 2024 16:59:57 -0500 Subject: [PATCH 23/28] feat: allow resize views during Shape Interpolation --- .../paintera/control/modes/ShapeInterpolationMode.kt | 3 +-- 1 file changed, 1 insertion(+), 2 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 a38ea34d0..bf83c2815 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 @@ -29,7 +29,6 @@ import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.removeActionSet import org.janelia.saalfeldlab.fx.actions.NamedKeyBinding import org.janelia.saalfeldlab.fx.actions.painteraActionSet import org.janelia.saalfeldlab.fx.actions.painteraMidiActionSet -import org.janelia.saalfeldlab.fx.extensions.onceWhen import org.janelia.saalfeldlab.fx.midi.MidiButtonEvent import org.janelia.saalfeldlab.fx.midi.MidiToggleEvent import org.janelia.saalfeldlab.fx.midi.ToggleAction @@ -81,7 +80,7 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat override val allowedActions = AllowedActions.AllowedActionsBuilder() .add(PaintActionType.ShapeInterpolation, PaintActionType.Paint, PaintActionType.Erase, PaintActionType.SetBrushSize, PaintActionType.Fill, PaintActionType.SegmentAnything) - .add(MenuActionType.ToggleMaximizeViewer, MenuActionType.DetachViewer, MenuActionType.ToggleSidePanel, MenuActionType.ResizePanel) + .add(MenuActionType.ToggleMaximizeViewer, MenuActionType.DetachViewer, MenuActionType.ResizeViewers, MenuActionType.ToggleSidePanel, MenuActionType.ResizePanel) .add(NavigationActionType.Pan, NavigationActionType.Slice, NavigationActionType.Zoom) .create() From 4c27453d48db78071e285fb633467d2fac3a4bfd Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Thu, 7 Nov 2024 17:00:33 -0500 Subject: [PATCH 24/28] refactor: interpolation logic during SI --- .../control/ShapeInterpolationController.kt | 98 +++++++++---------- 1 file changed, 47 insertions(+), 51 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 a78eb149b..0978a331c 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt @@ -61,9 +61,9 @@ import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.smallestC import org.janelia.saalfeldlab.util.* import java.math.BigDecimal import java.math.RoundingMode +import java.util.concurrent.CancellationException import java.util.concurrent.atomic.AtomicReference import java.util.function.Supplier -import java.util.stream.Collectors import kotlin.Pair import kotlin.math.absoluteValue import kotlin.math.sqrt @@ -130,7 +130,8 @@ class ShapeInterpolationController>( return viewerState.getBestMipMapLevel(screenScaleTransform, source) } - private var interpolator: Job? = null + private val interpolationSupervisor = SupervisorJob() + private val interpolationScope = CoroutineScope(newSingleThreadContext("Slice Interpolation Context") + interpolationSupervisor) private var requestRepaintUpdaterJob: Job = Job().apply { complete() } @@ -241,7 +242,7 @@ class ShapeInterpolationController>( // extra cleanup if shape interpolation was aborted if (!completed) { - interruptInterpolation() + interruptInterpolation("Exiting Shape Interpolation") source.resetMasks(true) } @@ -254,7 +255,6 @@ class ShapeInterpolationController>( controllerState = ControllerState.Off slicesAndInterpolants.clear() currentViewerMask = null - interpolator = null globalCompositeFillAndInterpolationImgs = null lastSelectedId = Label.INVALID interpolationId = Label.INVALID @@ -286,6 +286,7 @@ class ShapeInterpolationController>( isBusy = false } + @OptIn(ExperimentalCoroutinesApi::class) @Synchronized fun interpolateBetweenSlices(replaceExistingInterpolants: Boolean) { if (freezeInterpolation) return @@ -301,26 +302,35 @@ class ShapeInterpolationController>( if (replaceExistingInterpolants) { slicesAndInterpolants.removeAllInterpolants() + interruptInterpolation("Replacing Existing Interpolants") } isBusy = true - interpolator = CoroutineScope(Dispatchers.Default).launch { - synchronized(this) { - var updateInterval: RealInterval? = null - for ((firstSlice, secondSlice) in slicesAndInterpolants.zipWithNext().reversed()) { - if (!coroutineContext.isActive) return@launch - if (!(firstSlice.isSlice && secondSlice.isSlice)) continue - - val slice1 = firstSlice.getSlice() - val slice2 = secondSlice.getSlice() - val interpolant = interpolateBetweenTwoSlices(slice1, slice2, interpolationId) - slicesAndInterpolants.add(firstSlice.sliceDepth, interpolant!!) - updateInterval = sequenceOf(slice1.globalBoundingBox, slice2.globalBoundingBox, updateInterval) - .filterNotNull() - .reduceOrNull(Intervals::union) - } - updateSliceAndInterpolantsCompositeMask() - updateInterval?.let { + interpolationScope.async { + var updateInterval: RealInterval? = null + for ((firstSlice, secondSlice) in slicesAndInterpolants.zipWithNext().reversed()) { + ensureActive() + + if (firstSlice.isInterpolant || secondSlice.isInterpolant) + continue + + val slice1 = firstSlice.getSlice() + val slice2 = secondSlice.getSlice() + val interpolant = interpolateBetweenTwoSlices(slice1, slice2, interpolationId) + slicesAndInterpolants.add(firstSlice.sliceDepth, interpolant!!) + updateInterval = sequenceOf(slice1.globalBoundingBox, slice2.globalBoundingBox, updateInterval) + .filterNotNull() + .reduceOrNull(Intervals::union) + } + updateSliceAndInterpolantsCompositeMask() + updateInterval + }.also { job -> + job.invokeOnCompletion { cause -> + cause?.let { + LOG.debug(cause) { "Interpolation job cancelled" } + } ?: InvokeOnJavaFXApplicationThread { controllerState = ControllerState.Preview } + + job.getCompleted()?.let { updateInterval -> requestRepaint(updateInterval) } ?: let { /* a bit of a band-aid. It shouldn't be triggered often, but occasionally when an interpolation is triggered @@ -329,18 +339,7 @@ class ShapeInterpolationController>( * cause, but should stop it from happening as frequently. */ paintera().orthogonalViews().requestRepaint() } - } - }.also { job -> - job.invokeOnCompletion { cause -> - cause?.let { - LOG.debug(cause) { "Interpolation job cancelled" } - } ?: InvokeOnJavaFXApplicationThread { - controllerState = ControllerState.Preview - } - - interpolator = null isBusy = false - } } } @@ -393,7 +392,7 @@ class ShapeInterpolationController>( } if (controllerState == ControllerState.Interpolate) { // wait until the interpolation is done - runBlocking { interpolator!!.join() } + runBlocking { interpolationSupervisor.children.forEach { it.join() } } } assert(controllerState == ControllerState.Preview) @@ -547,10 +546,7 @@ class ShapeInterpolationController>( while (isActive) { /* Don't trigger repaints while interpolating*/ - if (interpolator?.isActive == true) { - awaitPulse() - continue - } + interpolationSupervisor.children.forEach { it.join() } requestRepaintInterval.getAndSet(null)?.let { processRepaintRequest(it) } @@ -576,10 +572,10 @@ class ShapeInterpolationController>( requestRepaintInterval.getAndAccumulate(interval) { l, r -> l?.union(r) ?: r } } - private fun interruptInterpolation() { - interpolator?.let { - it.cancel() - } + private fun interruptInterpolation(reason: String = "Interrupting Interpolation") { + val cause = CancellationException(reason) + LOG.debug(cause) { "Interuppting Interpolation" } + interpolationSupervisor.cancelChildren(cause) } private fun updateSliceAndInterpolantsCompositeMask() { @@ -611,7 +607,7 @@ class ShapeInterpolationController>( fun getMask(targetMipMapLevel: Int = currentBestMipMapLevel, ignoreExisting: Boolean = false): ViewerMask { /* If we have a mask, get it; else create a new one */ - currentViewerMask = (if (ignoreExisting) null else sliceAtCurrentDepth)?.let { oldSlice -> + currentViewerMask = (if (ignoreExisting) null else sliceAtCurrentDepth)?.let { oldSlice -> val oldSliceBoundingBox = oldSlice.maskBoundingBox ?: let { deleteSliceAt() return@let null @@ -838,7 +834,7 @@ class ShapeInterpolationController>( for (i in 0..1) { if (Thread.currentThread().isInterrupted) return null val distanceTransform = ArrayImgFactory(FloatType()).create(slices[i]).also { - val binarySlice = slices[i]!!.convertRAI(BoolType()){ source, target -> + val binarySlice = slices[i]!!.convertRAI(BoolType()) { source, target -> val label = source.get().isInterpolationLabel target.set(label) } @@ -909,7 +905,7 @@ class ShapeInterpolationController>( .interpolate(NLinearInterpolatorFactory()) .affineReal(distanceScale) - val interpolatedShapeRaiInSource = scaledInterpolatedDistanceTransform.convert(targetValue.createVariable()) { input, output : T -> + val interpolatedShapeRaiInSource = scaledInterpolatedDistanceTransform.convert(targetValue.createVariable()) { input, output: T -> val value = if (input.realDouble <= 0) targetValue else invalidValue output.set(value) } @@ -1046,12 +1042,12 @@ class ShapeInterpolationController>( fun getInterpolantAtDepth(depth: Double): InterpolantInfo? { synchronized(this) { for ((index, sliceOrInterpolant) in this.withIndex()) { - if ( - sliceOrInterpolant.isInterpolant - && this.getOrNull(index - 1)?.let { it.sliceDepth < depth } == true - && this.getOrNull(index + 1)?.let { it.sliceDepth > depth } == true - ) - return sliceOrInterpolant.getInterpolant() + return when { + sliceOrInterpolant.isSlice -> continue //If slice, check the next one + getOrNull(index - 1)?.run { sliceDepth >= depth } == true -> null //if the preceding slice is already past our depth, return null + getOrNull(index + 1)?.run { sliceDepth > depth } == true -> sliceOrInterpolant.getInterpolant() // if the next depth is beyond our depth, return this interpolant + else -> continue //no valid interpolants, return null + } } return null } @@ -1091,7 +1087,7 @@ class ShapeInterpolationController>( while (iterator.hasNext()) { val element = iterator.next() if (element.isSlice) - it.add(element.getSlice()) + it.add(element.getSlice()) } it.toList() } From ca439229ea2f36f9a3ac746c5b576db6989a30c4 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Thu, 7 Nov 2024 17:02:22 -0500 Subject: [PATCH 25/28] feat: improved action/tool validation --- .../paintera/cache/SamEmbeddingLoaderCache.kt | 7 +++- .../paintera/config/SegmentAnythingConfig.kt | 38 ++++++++++++------- .../paintera/control/modes/Modes.kt | 2 +- .../paintera/control/tools/Tool.kt | 18 +++++---- .../paintera/control/tools/paint/SamTool.kt | 7 ++-- 5 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt index c9e67ca3b..256694bb9 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt @@ -67,14 +67,15 @@ object SamEmbeddingLoaderCache : AsyncCacheWithLoader() { private val serviceUrlProperty = SimpleStringProperty(System.getenv(SAM_SERVICE_HOST_ENV) ?: DEFAULT_SERVICE_URL).apply { addListener { _, _, new -> if (new.isBlank()) serviceUrl = DEFAULT_SERVICE_URL } @@ -51,6 +53,13 @@ class SegmentAnythingConfig { internal val allDefault get() = serviceUrl == DEFAULT_SERVICE_URL && modelLocation == DEFAULT_MODEL_LOCATION && responseTimeout == DEFAULT_RESPONSE_TIMEOUT + @Transient + private val observableInvalidationBinding = serviceUrlProperty.createObservableBinding(modelLocationProperty, responseTimeoutProperty, compressEncodingProperty) { + UUID.randomUUID() + }.apply { subscribe { _ -> fireValueChangedEvent() } } //trigger the SegmentAnythingConfig listeners + + override fun getValue() = this + companion object { private const val SAM_SERVICE_HOST_ENV = "SAM_SERVICE_HOST" internal const val EMBEDDING_REQUEST_ENDPOINT = "embedded_model" @@ -89,20 +98,21 @@ class SegmentAnythingConfigNode(val config: SegmentAnythingConfig) : TitledPane( it.alignment = Pos.BASELINE_LEFT it.minWidth = Label.USE_PREF_SIZE } - val serviceTextField = TextField(config.serviceUrl).also { - VBox.setVgrow(it, Priority.NEVER) - it.maxWidth = Double.MAX_VALUE - it.prefWidth - Double.MAX_VALUE - it.focusedProperty().subscribe { focused -> - if (!focused && it.text.isBlank()) { - it.text = DEFAULT_SERVICE_URL - InvokeOnJavaFXApplicationThread { it.positionCaret(0) } - } + val serviceTextField = TextField(config.serviceUrl).apply { + VBox.setVgrow(this, Priority.NEVER) + maxWidth = Double.MAX_VALUE + prefWidth - Double.MAX_VALUE + onAction = EventHandler { + if(text.isBlank()) + text = DEFAULT_SERVICE_URL + + config.serviceUrl = text.trim() } - it.textProperty().subscribe { _, new -> - config.serviceUrl = new + focusedProperty().subscribe { focused -> + if (!focused) + onAction.handle(null) } - add(it, 1, row) + add(this, 1, row) } Button().also { it.graphic = FontAwesome[FontAwesomeIcon.UNDO] diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/Modes.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/Modes.kt index 980255a52..b7016f5c1 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/Modes.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/Modes.kt @@ -101,7 +101,7 @@ interface ToolMode : SourceMode { val activeMode = paintera.baseView.activeModeProperty.value activeTool = when { activeMode != this@ToolMode -> null // wrong mode - tool?.isValid() == false -> null // tool is not currently valid + tool?.isValidProperty?.value == false -> null // tool is not currently valid else -> tool?.apply { activate() } // try to activate } LOG.trace { "Activated $activeTool" } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/Tool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/Tool.kt index 300458d2f..aaecfb2a1 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/Tool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/Tool.kt @@ -1,9 +1,7 @@ package org.janelia.saalfeldlab.paintera.control.tools import io.github.oshai.kotlinlogging.KotlinLogging -import javafx.beans.property.SimpleObjectProperty -import javafx.beans.property.SimpleStringProperty -import javafx.beans.property.StringProperty +import javafx.beans.property.* import javafx.event.EventHandler import javafx.scene.Node import javafx.scene.control.* @@ -23,11 +21,11 @@ import org.janelia.saalfeldlab.paintera.paintera interface Tool { - fun isValid() = true fun activate() {} fun deactivate() {} val statusProperty: StringProperty + val isValidProperty: BooleanProperty val actionSets: MutableList } @@ -67,15 +65,20 @@ interface ToolBarItem { // why conditionally bind isDisabled only if a graphic? btn.graphic?.let { - val checkIsValid = { action?.isValid(null) ?: (this as? Tool)?.isValid() ?: true } + val actionIsValid = { action?.isValid(null) ?: true } + /* Listen on disabled when visible*/ if ("ignore-disable" !in it.styleClass) { paintera.baseView.isDisabledProperty.`when`(btn.visibleProperty()).subscribe { disabled -> - btn.disableProperty().set(disabled || !checkIsValid()) + btn.disableProperty().set(disabled || !actionIsValid()) } + } else { + btn.disableProperty().set(!actionIsValid()) } /* set initial state to */ - btn.disableProperty().set(!checkIsValid()) + btn.disableProperty().set(!actionIsValid()) + + (this as? Tool)?.isValidProperty?.`when`(btn.visibleProperty())?.subscribe { isValid -> btn.disableProperty().set(!isValid) } } btn.styleClass += "toolbar-button" btn.tooltip = Tooltip( @@ -93,6 +96,7 @@ abstract class ViewerTool(protected val mode: ToolMode? = null) : Tool, ToolBarI private val installedInto: MutableMap> = mutableMapOf() private var subscriptions : Subscription? = null + override val isValidProperty = SimpleBooleanProperty(true) override fun activate() { activeViewerProperty.bind(mode?.activeViewerProperty ?: paintera.baseView.lastFocusHolder) 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 a09ebf0a5..a47f86534 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 @@ -224,6 +224,9 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty observable.removeListener(setCursorWhenDoneApplying) } + paintera.properties.segmentAnythingConfig.subscribe( Runnable { + isValidProperty.set(SamEmbeddingLoaderCache.canReachServer) + }) } private val isBusyProperty = SimpleBooleanProperty(false) @@ -247,10 +250,6 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty Date: Fri, 8 Nov 2024 09:45:42 -0500 Subject: [PATCH 26/28] fix: allow toggle sam point types in SI --- .../janelia/saalfeldlab/paintera/control/tools/paint/SamTool.kt | 2 -- 1 file changed, 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 a47f86534..1b6ac8fa2 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 @@ -476,7 +476,6 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty Date: Fri, 8 Nov 2024 11:32:30 -0500 Subject: [PATCH 27/28] fix: requires ResizeViewers to resize panel also --- .../janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.kt index 8e6cf0494..ec982bdb6 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.kt @@ -115,7 +115,7 @@ class BorderPaneWithStatusBars(paintera: PainteraMainWindow) { @Suppress("unused") private val resizeSideBar = ResizeOnLeftSide(scrollPane, sideBarWidthProperty).apply { - verifyPermission(MenuActionType.ResizePanel) + verifyPermission(MenuActionType.ResizePanel, MenuActionType.ResizeViewers) install() } From 1ae89453b81f2bbe91ede5cca22587cabd6c6c57 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Fri, 8 Nov 2024 11:52:26 -0500 Subject: [PATCH 28/28] build: readme and version bump --- README.md | 4 +++- pom.xml | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2494171f4..94a49d321 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,9 @@ Paintera is a general visualization tool for 3D volumetric data and proof-readin ## Installation -The current version release version can be installed from the [Github Releases](https://github.com/saalfeldlab/paintera/releases) +The latest release can be installed from the [Github Releases](https://github.com/saalfeldlab/paintera/releases) + +[![GitHub Release](https://img.shields.io/github/v/release/saalfeldlab/paintera)](https://github.com/saalfeldlab/paintera/releases) diff --git a/pom.xml b/pom.xml index 5913e0b16..49873ccfb 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ org.janelia.saalfeldlab paintera - 1.5.2-SNAPSHOT + 1.6.0-SNAPSHOT Paintera New Era Painting and annotation tool @@ -58,7 +58,7 @@ 22.0.1 - 2.1.0 + 2.2.0 4.0.16-alpha 1.4.1 @@ -68,7 +68,7 @@ org.janelia.saalfeldlab.paintera.Paintera Paintera paintera - 1.5.1 + 1.6.0 javafx.base,javafx.controls,javafx.fxml,javafx.media,javafx.swing,javafx.web,javafx.graphics,java.naming,java.management,java.sql UTF-8