diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 000000000..9f3a07680 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,13 @@ +# .github/release.yml + +changelog: + categories: + - title: Features + labels: + - '*' + exclude: + labels: + - dependencies + - title: Dependencies + labels: + - dependencies diff --git a/.github/workflows/maven-build-all-installer.yml b/.github/workflows/build-installers.yml similarity index 75% rename from .github/workflows/maven-build-all-installer.yml rename to .github/workflows/build-installers.yml index 9732a97ae..10264c6e9 100644 --- a/.github/workflows/maven-build-all-installer.yml +++ b/.github/workflows/build-installers.yml @@ -1,15 +1,12 @@ # This workflow will build a Java project with Maven # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven -name: Build All Installers +name: Build Installers on: - push: - branches: [ master ] + workflow_call: pull_request: - branches: [ master ] - workflow_dispatch: - branches: [ master ] + branches: [master] env: DEV_IDENTITY: BXPZTQZ35S # Your Apple Dev identity, something like BXPZTQZ35S @@ -21,9 +18,14 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest, macos-14] runs-on: ${{ matrix.os }} - env: - RELEASE_INSTALLERS: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set Version + shell: bash + run: | + short_version=`git rev-parse --short HEAD` + echo "VERSION=$short_version" >> $GITHUB_ENV - name: Download Wix uses: i3h/download-release-asset@v1 if: matrix.os == 'windows-latest' @@ -42,7 +44,7 @@ jobs: run: echo "$HOME/target/wix" >> $GITHUB_PATH if: matrix.os == 'windows-latest' - uses: actions/checkout@v2 - - name: Set up JDK 17 + - name: Set up JDK uses: actions/setup-java@v2 with: java-version: 18.0.2 @@ -58,14 +60,11 @@ jobs: MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} if: ${{ env.MACOS_CERTIFICATE == null && (matrix.os == 'macos-latest' || matrix.os == 'macos-14') }} run: mvn -B clean install -DskipTests -Pbuild-installer "-Dmatrix.os=${{ matrix.os }}" --file pom.xml - - name: Update Automatic Release - uses: marvinpinto/action-automatic-releases@latest - if: ${{ env.RELEASE_INSTALLERS }} + + - name: Upload Installers + uses: actions/upload-artifact@v4 with: - repo_token: "${{ secrets.GITHUB_TOKEN}}" - automatic_release_tag: ${{ matrix.os }} - prerelease: true - title: ${{ matrix.os }} Development Build - files: | + name: Paintera-${{ matrix.os }}-${{ env.VERSION }} + path: | ${{ env.DMG_PATH }} - ./target/installer-${{ matrix.os }}/* + ./target/installer-${{ matrix.os }}/* \ No newline at end of file diff --git a/.github/workflows/publish-installers.yml b/.github/workflows/publish-installers.yml new file mode 100644 index 000000000..6093fae3e --- /dev/null +++ b/.github/workflows/publish-installers.yml @@ -0,0 +1,50 @@ +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Publish Installers + +on: + push: + tags: "paintera-*.*.*" + workflow_dispatch: + + + +env: + DEV_IDENTITY: BXPZTQZ35S # Your Apple Dev identity, something like BXPZTQZ35S + PRIMARY_BUNDLE_ID: org.janelia.saalfeldlab.Paintera # Unique to your app, often the launcher class + +jobs: + build_installers: + name: Build Installers + uses: ./.github/workflows/build-installers.yml + + create_release: + needs: build_installers + runs-on: ubuntu-latest + steps: + - name: Download Artifacts + uses: actions/download-artifact@v4 + - name: Set Version + run: | + tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") + echo "VERSION=$tag_name" >> $GITHUB_ENV + - name: Display structure of downloaded files + run: ls -R + - name: Rename Artifacts + run: | + mv Paintera-windows-latest-*/*.msi Paintera-${{ env.VERSION }}-Windows.msi + mv Paintera-ubuntu-latest-*/*.deb Paintera-${{ env.VERSION }}_x86_64.deb + mv Paintera-macos-latest-*/*.dmg Paintera-${{ env.VERSION }}-MacOS.dmg + mv Paintera-macos-14-*/*.dmg Paintera-${{ env.VERSION }}-MacOS-AppleSilicon.dmg + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + name: Paintera ${{ env.VERSION }} + tag_name: ${{ github.ref }} + prerelease: false + files: | + Paintera-${{ env.VERSION }}-Windows.msi + Paintera-${{ env.VERSION }}_x86_64.deb + Paintera-${{ env.VERSION }}-MacOS.dmg + Paintera-${{ env.VERSION }}-MacOS-AppleSilicon.dmg \ No newline at end of file diff --git a/pom.xml b/pom.xml index 99a63c6e0..c349f5254 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ org.janelia.saalfeldlab paintera - 1.1.1-SNAPSHOT + 1.2.0-SNAPSHOT Paintera New Era Painting and annotation tool @@ -53,7 +53,7 @@ true ${javadoc.skip} - 1.2.0 + 1.3.0 3.0.7 1.4.0 diff --git a/src/main/kotlin/org/janelia/saalfeldlab/fx/ui/ActionBar.kt b/src/main/kotlin/org/janelia/saalfeldlab/fx/ui/ActionBar.kt index d52666c0e..a254883b1 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/fx/ui/ActionBar.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/fx/ui/ActionBar.kt @@ -25,7 +25,7 @@ class ActionBar : HBox() { var newGroup : ToggleGroup? = null buttons.forEach { node -> (node as? Toggle)?.apply { - toggleGroup = toggleGroup ?: ToggleGroup().also { newGroup = it } + toggleGroup = toggleGroup ?: newGroup ?: ToggleGroup().also { newGroup = it } } } toggleGroup = newGroup @@ -77,7 +77,7 @@ class ActionBar : HBox() { fun List.toolBarNodes() = map { item -> item.toolBarButton.apply { - this.onAction ?: let { + onAction ?: let { userData = item } isFocusTraversable = false diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt index 6f281366d..767f79611 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt @@ -103,36 +103,37 @@ private class LateInitNamedKeyCombination(keyCombination: KeyCombination, initNa } enum class LabelSourceStateKeys(lateInitNamedKeyCombo : LateInitNamedKeyCombination) : NamedKeyBinding by lateInitNamedKeyCombo { - SELECT_ALL ( CONTROL_DOWN + A), - SELECT_ALL_IN_CURRENT_VIEW ( CONTROL_DOWN + SHIFT_DOWN + A), - LOCK_SEGMENT ( L), - NEXT_ID ( N), - COMMIT_DIALOG ( C + CONTROL_DOWN), - MERGE_ALL_SELECTED ( ENTER + CONTROL_DOWN), - ARGB_STREAM__INCREMENT_SEED ( C), - ARGB_STREAM__DECREMENT_SEED ( C + SHIFT_DOWN), - REFRESH_MESHES ( R), - CANCEL ( ESCAPE, "cancel tool / exit mode"), - TOGGLE_NON_SELECTED_LABELS_VISIBILITY ( V + SHIFT_DOWN, "toggle non-selected labels visibility"), - SEGMENT_ANYTHING__TOGGLE_MODE ( A), - PAINT_BRUSH ( SPACE), - FILL_2D ( F), - FILL_3D ( SHIFT_DOWN + F), - CLEAR_CANVAS ( CONTROL_DOWN + SHIFT_DOWN + C), - INTERSECT_UNDERLYING_LABEL ( SHIFT_DOWN + R), - SHAPE_INTERPOLATION__TOGGLE_MODE ( S), - SHAPE_INTERPOLATION__TOGGLE_PREVIEW ( CONTROL_DOWN + P), - SHAPE_INTERPOLATION__ACCEPT_INTERPOLATION ( ENTER), - SHAPE_INTERPOLATION__SELECT_FIRST_SLICE ( SHIFT_DOWN + LEFT), - SHAPE_INTERPOLATION__SELECT_LAST_SLICE ( SHIFT_DOWN + RIGHT), - SHAPE_INTERPOLATION__SELECT_PREVIOUS_SLICE ( LEFT), - SHAPE_INTERPOLATION__SELECT_NEXT_SLICE ( RIGHT ), - SHAPE_INTERPOLATION__REMOVE_SLICE_1 ( DELETE, "delete current slice "), - SHAPE_INTERPOLATION__REMOVE_SLICE_2 ( BACK_SPACE, "delete current slice "), - SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICE_LEFT ( OPEN_BRACKET, "shape interpolation: auto SAM: new slice left" ), - SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICES_BISECT ( QUOTE, "shape interpolation: auto SAM: new bisect slices" ), - SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICE_RIGHT ( CLOSE_BRACKET, "shape interpolation: auto SAM: new slice right" ), - SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICE_HERE ( SHIFT_DOWN + A, "shape interpolation: auto SAM: new slice at current location" ), + SELECT_ALL ( CONTROL_DOWN + A), + SELECT_ALL_IN_CURRENT_VIEW ( CONTROL_DOWN + SHIFT_DOWN + A), + LOCK_SEGMENT ( L), + NEXT_ID ( N), + COMMIT_DIALOG ( C + CONTROL_DOWN), + MERGE_ALL_SELECTED ( ENTER + CONTROL_DOWN), + ARGB_STREAM__INCREMENT_SEED ( C), + ARGB_STREAM__DECREMENT_SEED ( C + SHIFT_DOWN), + REFRESH_MESHES ( R), + CANCEL ( ESCAPE, "cancel tool / exit mode"), + TOGGLE_NON_SELECTED_LABELS_VISIBILITY ( V + SHIFT_DOWN, "toggle non-selected labels visibility"), + SEGMENT_ANYTHING__TOGGLE_MODE ( A), + PAINT_BRUSH ( SPACE), + FILL_2D ( F), + FILL_3D ( SHIFT_DOWN + F), + CLEAR_CANVAS ( CONTROL_DOWN + SHIFT_DOWN + C), + INTERSECT_UNDERLYING_LABEL ( SHIFT_DOWN + R), + SHAPE_INTERPOLATION__TOGGLE_MODE ( S), + SHAPE_INTERPOLATION__TOGGLE_PREVIEW ( CONTROL_DOWN + P), + SHAPE_INTERPOLATION__ACCEPT_INTERPOLATION ( ENTER), + SHAPE_INTERPOLATION__SELECT_FIRST_SLICE ( SHIFT_DOWN + LEFT), + SHAPE_INTERPOLATION__SELECT_LAST_SLICE ( SHIFT_DOWN + RIGHT), + SHAPE_INTERPOLATION__SELECT_PREVIOUS_SLICE ( LEFT), + SHAPE_INTERPOLATION__SELECT_NEXT_SLICE ( RIGHT ), + SHAPE_INTERPOLATION__REMOVE_SLICE_1 ( DELETE, "delete current slice "), + SHAPE_INTERPOLATION__REMOVE_SLICE_2 ( BACK_SPACE, "delete current slice "), + SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICE_LEFT ( OPEN_BRACKET, "shape interpolation: auto SAM: new slice left" ), + SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICES_BISECT ( QUOTE, "shape interpolation: auto SAM: new slice between closest slices" ), + SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICES_BISECT_ALL ( SHIFT_DOWN + QUOTE, "shape interpolation: auto SAM: new slice between all slices" ), + SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICE_RIGHT ( CLOSE_BRACKET, "shape interpolation: auto SAM: new slice right" ), + SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICE_HERE ( SHIFT_DOWN + A, "shape interpolation: auto SAM: new slice at current location" ), ; diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfig.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfig.kt index a37de627b..103f0501b 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfig.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfig.kt @@ -28,8 +28,7 @@ class LoggingConfig { .apply { addTriggeredListener { _, _, level -> LogUtils.rootLoggerLevel = level } } var rootLoggerLevel: Level by rootLoggerLevelProperty.nonnull() - val isLoggingEnabledProperty = SimpleBooleanProperty(DEFAULT_IS_LOGGING_ENABLED) - .apply { + val isLoggingEnabledProperty = SimpleBooleanProperty(DEFAULT_IS_LOGGING_ENABLED).apply { addTriggeredListener { _, _, new -> LogUtils.setLoggingEnabled(new) loggerLevels.forEach { (logger, level) -> LogUtils.setLogLevelFor(logger, level.get()) } @@ -37,8 +36,7 @@ class LoggingConfig { } var isLoggingEnabled: Boolean by isLoggingEnabledProperty.nonnull() - val isLoggingToConsoleEnabledProperty = SimpleBooleanProperty(DEFAULT_IS_LOGGING_TO_CONSOLE_ENABLED) - .apply { + val isLoggingToConsoleEnabledProperty = SimpleBooleanProperty(DEFAULT_IS_LOGGING_TO_CONSOLE_ENABLED).apply { addTriggeredListener { _, _, new -> LogUtils.setLoggingToConsoleEnabled(new) loggerLevels.forEach { (logger, level) -> LogUtils.setLogLevelFor(logger, level.get()) } @@ -46,8 +44,7 @@ class LoggingConfig { } var isLoggingToConsoleEnabled: Boolean by isLoggingEnabledProperty.nonnull() - val isLoggingToFileEnabledProperty = SimpleBooleanProperty(DEFAULT_IS_LOGGING_TO_FILE_ENABLED) - .apply { + val isLoggingToFileEnabledProperty = SimpleBooleanProperty(DEFAULT_IS_LOGGING_TO_FILE_ENABLED).apply { addTriggeredListener { _, _, new -> LogUtils.setLoggingToFileEnabled(new) loggerLevels.forEach { (logger, level) -> LogUtils.setLogLevelFor(logger, level.get()) } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfigNode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfigNode.kt index e1b2a781f..d6f64bda1 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfigNode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfigNode.kt @@ -2,6 +2,7 @@ package org.janelia.saalfeldlab.paintera.config import ch.qos.logback.classic.Level import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.LoggerContext import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon import javafx.beans.InvalidationListener import javafx.beans.property.ObjectProperty @@ -20,10 +21,12 @@ import org.janelia.saalfeldlab.fx.Buttons import org.janelia.saalfeldlab.fx.Labels import org.janelia.saalfeldlab.fx.TitledPanes import org.janelia.saalfeldlab.fx.extensions.TitledPaneExtensions +import org.janelia.saalfeldlab.fx.ui.MatchSelection import org.janelia.saalfeldlab.fx.ui.NamedNode import org.janelia.saalfeldlab.paintera.ui.FontAwesome import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts import org.janelia.saalfeldlab.paintera.util.logging.LogUtils +import org.slf4j.LoggerFactory class LoggingConfigNode(private val config: LoggingConfig) { @@ -122,12 +125,25 @@ class LoggingConfigNode(private val config: LoggingConfig) { } } } + + val loggerList = FXCollections.observableArrayList() + val updateLoggerList = { + (LoggerFactory.getILoggerFactory() as? LoggerContext)?.let { + loggerList.setAll(it.loggerList.map { logger -> logger.name }.toList()) + } + } val newLoggerField = TextField("") - val newLoggerChoiceBox = logLevelChoiceBox(null) + val matcherField = MatchSelection.fuzzyTop(loggerList, { name -> newLoggerField.text = name }, 5) + matcherField.emptyBehavior = MatchSelection.EmptyBehavior.MATCH_NONE + matcherField.promptText = "Search for Loggers..." + matcherField.focusedProperty().addListener { _, _, focused -> if (focused) updateLoggerList() } + + val newLogLevelChoiceBox = logLevelChoiceBox(null) val newLoggerButton = Buttons - .withTooltip(null) { config.setLogLevelFor(newLoggerField.text, newLoggerChoiceBox.value) } + .withTooltip(null) { config.setLogLevelFor(newLoggerField.text, newLogLevelChoiceBox.value) } .also { it.graphic = FontAwesome[FontAwesomeIcon.PLUS, 2.0] } val listener = InvalidationListener { + updateLoggerList() val name = newLoggerField.text val isRootLoggerName = LogUtils.rootLogger.name == name val isExistingLogger = name in keys @@ -147,11 +163,13 @@ class LoggingConfigNode(private val config: LoggingConfig) { newLoggerField.textProperty().addListener(listener) listener.invalidated(newLoggerField.textProperty()) add(newLoggerField, 0, row) - add(newLoggerChoiceBox, 1, row) + add(newLogLevelChoiceBox, 1, row) add(newLoggerButton, 2, row) + add(matcherField, 0, row + 1, GridPane.REMAINING, GridPane.REMAINING) + GridPane.setHgrow(matcherField, Priority.ALWAYS) + GridPane.setVgrow(matcherField, Priority.ALWAYS) + matcherField.maxWidthProperty().bind(widthProperty()) } - - } } 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 a5063f513..b86a11399 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt @@ -56,6 +56,7 @@ import org.janelia.saalfeldlab.paintera.id.IdService import org.janelia.saalfeldlab.paintera.stream.AbstractHighlightingARGBStream import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverter import org.janelia.saalfeldlab.paintera.util.IntervalHelpers +import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.extendBy import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.smallestContainingInterval import org.janelia.saalfeldlab.util.* import org.slf4j.LoggerFactory @@ -187,11 +188,9 @@ class ShapeInterpolationController>( ?.also { repaintInterval -> if (preview) { isBusy = true - requestRepaintInterval = repaintInterval union requestRepaintInterval interpolateBetweenSlices(false) - } else { - requestRepaintAfterTasks(repaintInterval) } + requestRepaintAfterTasks(repaintInterval) } } @@ -364,6 +363,7 @@ class ShapeInterpolationController>( Last } + //TODO Caleb: Controller should not move, let the tool/mode do that fun editSelection(choice: EditSelectionChoice) { val slices = slicesAndInterpolants.slices when (choice) { @@ -751,7 +751,9 @@ class ShapeInterpolationController>( val interpolatedMaskView = interpolant.dataInterpolant .affine(viewerMask.currentGlobalToMaskTransform) .interval(interpolantIntervalSliceInMaskSpace) - val fillMaskOverInterval = viewerMask.viewerImg.interval(interpolantIntervalSliceInMaskSpace) + val fillMaskOverInterval = viewerMask.viewerImg.apply { + extendValue(Label.INVALID) + }.interval(interpolantIntervalSliceInMaskSpace) LoopBuilder.setImages(interpolatedMaskView, fillMaskOverInterval) .multiThreaded() @@ -1164,7 +1166,7 @@ class ShapeInterpolationController>( computeBoundingBoxInInitialMask() } val globalBoundingBox: RealInterval? - get() = maskBoundingBox?.let { mask.initialGlobalToMaskTransform.inverse().estimateBounds(it) } + get() = maskBoundingBox?.let { mask.initialMaskToGlobalWithDepthTransform.estimateBounds(it.extendBy(0.0, 0.0, .5)) } private val selectionIntervals: MutableList = mutableListOf() 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 505ccc779..5d94020cf 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 @@ -21,14 +21,20 @@ 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.painteraActionSet -import org.janelia.saalfeldlab.fx.extensions.* +import org.janelia.saalfeldlab.fx.extensions.addTriggeredListener +import org.janelia.saalfeldlab.fx.extensions.createNullableValueBinding +import org.janelia.saalfeldlab.fx.extensions.nullable +import org.janelia.saalfeldlab.fx.extensions.nullableVal import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews.ViewerAndTransforms import org.janelia.saalfeldlab.fx.ui.ActionBar import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.paintera.PainteraBaseKeys import org.janelia.saalfeldlab.paintera.config.input.KeyAndMouseBindings import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions -import org.janelia.saalfeldlab.paintera.control.tools.* +import org.janelia.saalfeldlab.paintera.control.tools.REQUIRES_ACTIVE_VIEWER +import org.janelia.saalfeldlab.paintera.control.tools.Tool +import org.janelia.saalfeldlab.paintera.control.tools.ToolBarItem +import org.janelia.saalfeldlab.paintera.control.tools.ViewerTool import org.janelia.saalfeldlab.paintera.control.tools.paint.PaintTool import org.janelia.saalfeldlab.paintera.paintera import org.janelia.saalfeldlab.paintera.state.SourceState @@ -87,7 +93,6 @@ interface ToolMode : SourceMode { activeTool?.deactivate() showToolBars() - modeToolsBar.toggleGroup?.toggles?.forEach { it.isSelected = false } tool?.activate() activeTool = tool @@ -136,7 +141,6 @@ interface ToolMode : SourceMode { /* when the active tool changes, update the toggle to reflect the active tool */ activeToolProperty.addTriggeredListener { _, old, new -> - old?.let { toolActionsBar.reset() } new?.let { newTool -> toggles .firstOrNull { it.userData == newTool } @@ -234,7 +238,7 @@ interface ToolMode : SourceMode { /* temporarily revoke permissions, so no actions are performed until we receive event [T] */ paintera.baseView.allowedActionsProperty().suspendPermisssions() - val queue = LinkedBlockingQueue() + val queue = LinkedBlockingQueue() InvokeOnJavaFXApplicationThread { lateinit var escapeFilter: EventHandler @@ -244,7 +248,7 @@ interface ToolMode : SourceMode { removeEventFilter(event, waitForEventFilter) removeEventFilter(KEY_PRESSED, escapeFilter) paintera.baseView.allowedActionsProperty().restorePermisssions() - queue.offer(it) + queue.offer(it ?: Any()) } waitForEventFilter = EventHandler { @@ -261,7 +265,7 @@ interface ToolMode : SourceMode { addEventFilter(KEY_PRESSED, escapeFilter) } - return queue.take() + return (queue.take() as? T) } fun disableUnfocusedViewers() { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/NavigationControlMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/NavigationControlMode.kt index 521168f15..bf4cad640 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/NavigationControlMode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/NavigationControlMode.kt @@ -259,11 +259,11 @@ object NavigationTool : ViewerTool() { painteraMidiActionSet("midi translate along normal", device, target, NavigationActionType.Slice) { MidiPotentiometerEvent.POTENTIOMETER_RELATIVE(2) { name = "midi_normal" - setDisplayType(DisplayType.TRIM) + displayType = DisplayType.TRIM verifyEventNotNull() onAction { InvokeOnJavaFXApplicationThread { - translator.translate(0.0, 0.0, it!!.value.sign * FAST) + translator.translate(0.0, 0.0, it!!.value.toInt().sign * FAST) } } } @@ -294,7 +294,7 @@ object NavigationTool : ViewerTool() { painteraMidiActionSet("midi translate xy", device, target, NavigationActionType.Pan) { MidiPotentiometerEvent.POTENTIOMETER_RELATIVE(0) { name = "midi translate x" - setDisplayType(DisplayType.TRIM) + displayType = DisplayType.TRIM verifyEventNotNull() onAction { InvokeOnJavaFXApplicationThread { @@ -304,7 +304,7 @@ object NavigationTool : ViewerTool() { } MidiPotentiometerEvent.POTENTIOMETER_RELATIVE(1) { name = "midi translate y" - setDisplayType(DisplayType.TRIM) + displayType = DisplayType.TRIM verifyEventNotNull() onAction { InvokeOnJavaFXApplicationThread { @@ -371,7 +371,7 @@ object NavigationTool : ViewerTool() { painteraMidiActionSet("zoom", device, target, NavigationActionType.Zoom) { MidiPotentiometerEvent.POTENTIOMETER_RELATIVE(3) { name = "midi_zoom" - setDisplayType(DisplayType.TRIM) + displayType = DisplayType.TRIM verifyEventNotNull() onAction { InvokeOnJavaFXApplicationThread { @@ -412,7 +412,7 @@ object NavigationTool : ViewerTool() { KEY_PRESSED(keyBindings, NavigationKeys.SET_ROTATION_AXIS_Z) { onAction { keyRotationAxis.set(Axis.Z) } } } - val mouseRotation = painteraDragActionSet("mousde-drag-rotate", NavigationActionType.Rotate) { + val mouseRotation = painteraDragActionSet("mouse-drag-rotate", NavigationActionType.Rotate) { verify { it.isPrimaryButtonDown } dragDetectedAction.verify { NavigationTool.allowRotationsProperty() } onDragDetected { @@ -455,7 +455,7 @@ object NavigationTool : ViewerTool() { DeviceManager.xTouchMini?.let { device -> targetPositionObservable?.let { targetPos -> painteraMidiActionSet(NavigationKeys.REMOVE_ROTATION, device, target, NavigationActionType.Rotate) { - MidiButtonEvent.BUTTON_PRESED(18) { + MidiButtonEvent.BUTTON_PRESSED(18) { name = "midi_remove_rotation" verifyEventNotNull() onAction { InvokeOnJavaFXApplicationThread { resetRotationController.removeRotationCenteredAt(targetPos.x, targetPos.y) } } @@ -480,12 +480,12 @@ object NavigationTool : ViewerTool() { painteraMidiActionSet("rotate", device, target, NavigationActionType.Rotate) { MidiPotentiometerEvent.POTENTIOMETER_RELATIVE ( handle) { name = "midi_rotate_${axis.name.lowercase()}" - setDisplayType(DisplayType.TRIM) + displayType = DisplayType.TRIM verifyEventNotNull() verify { allowRotationsProperty() } onAction { InvokeOnJavaFXApplicationThread { - val direction = it!!.value.sign + val direction = it!!.value.toInt().sign rotationController.setSpeed(direction * speed) rotationController.rotateAroundAxis(targetPosition.x, targetPosition.y, axis) } @@ -498,15 +498,20 @@ object NavigationTool : ViewerTool() { } } - fun midiNavigationActions() = mutableListOf().also { - midiPanActions()?.let { midiActions -> it.add(midiActions) } - midiSliceActions()?.let { midiActions -> it.add(midiActions) } - midiZoomActions()?.let { midiActions -> it.add(midiActions) } - midiRotationActions()?.let { midiActions -> it.addAll(midiActions) } - midiResetRotationAction()?.let { midiActions -> it.add(midiActions) } + fun midiNavigationActions( + pan: Boolean = true, + slice: Boolean = true, + zoom: Boolean = true, + rotation: Boolean = true, + resetRotation: Boolean = true + ) = mutableListOf().also { + if (pan) midiPanActions()?.let { midiActions -> it.add(midiActions) } + if (slice) midiSliceActions()?.let { midiActions -> it.add(midiActions) } + if (zoom) midiZoomActions()?.let { midiActions -> it.add(midiActions) } + if (rotation) midiRotationActions()?.let { midiActions -> it.addAll(midiActions) } + if (resetRotation) midiResetRotationAction()?.let { midiActions -> it.add(midiActions) } } - private fun goToPositionAction(translateXYController: TranslationController) = painteraActionSet("center on position", NavigationActionType.Pan) { KEY_PRESSED(KeyCode.CONTROL, KeyCode.G) { 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 6d82de49b..0ac9cfe5f 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 @@ -180,6 +180,15 @@ object PaintLabelMode : AbstractToolMode() { } } + override fun switchTool(tool: Tool?) { + super.switchTool(tool) + /*SAM Tool restrict the active ViewerPanel, so we don't want it changing on mouseover of the other views, for example */ + if (activeTool is SamTool) + activeViewerProperty.unbind() + else if (!activeViewerProperty.isBound) + activeViewerProperty.bind(paintera.baseView.currentFocusHolder) + } + private fun getToolTriggers() = listOf( paintBrushTool.createTriggers(this, PaintActionType.Paint), 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 83abaede9..075023f3b 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 @@ -61,10 +61,10 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat internal val samSliceCache = SamSliceCache() - private val paintBrushTool = ShapeInterpolationPaintBrushTool(activeSourceStateProperty, this) - private val fill2DTool = ShapeInterpolationFillTool(controller, activeSourceStateProperty, this) - private val samTool = ShapeInterpolationSAMTool(controller, activeSourceStateProperty, this@ShapeInterpolationMode) - private val shapeInterpolationTool = ShapeInterpolationTool(controller, previousMode, this@ShapeInterpolationMode, this@ShapeInterpolationMode.fill2DTool) + private val paintBrushTool by lazy { ShapeInterpolationPaintBrushTool(activeSourceStateProperty, this) } + private val fill2DTool by lazy { ShapeInterpolationFillTool(controller, activeSourceStateProperty, this) } + private val samTool by lazy { ShapeInterpolationSAMTool(controller, activeSourceStateProperty, this@ShapeInterpolationMode) } + private val shapeInterpolationTool by lazy { ShapeInterpolationTool(controller, previousMode, this@ShapeInterpolationMode, this@ShapeInterpolationMode.fill2DTool) } override val defaultTool: Tool? by lazy { shapeInterpolationTool } override val modeActions by lazy { modeActions() } @@ -275,27 +275,31 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat } } with(controller) { - MidiButtonEvent.BUTTON_PRESED(9) { + MidiButtonEvent.BUTTON_PRESSED(9) { name = "midi go to first slice" verify { controllerState != Moving } + verify { activeTool !is SamTool } onAction { editSelection(EditSelectionChoice.First) } } - MidiButtonEvent.BUTTON_PRESED(10) { + MidiButtonEvent.BUTTON_PRESSED(10) { name = "midi go to previous slice" verify { controllerState != Moving } + verify { activeTool !is SamTool } onAction { editSelection(EditSelectionChoice.Previous) } } - MidiButtonEvent.BUTTON_PRESED(11) { + MidiButtonEvent.BUTTON_PRESSED(11) { name = "midi go to next slice" verify { controllerState != Moving } + verify { activeTool !is SamTool } onAction { editSelection(EditSelectionChoice.Next) } } - MidiButtonEvent.BUTTON_PRESED(12) { + MidiButtonEvent.BUTTON_PRESSED(12) { name = "midi go to last slice" verify { controllerState != Moving } + verify { activeTool !is SamTool } onAction { editSelection(EditSelectionChoice.Last) } } } 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 598d52077..726467193 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 @@ -152,6 +152,7 @@ class ViewerMask private constructor( */ val xScaleChange get() = (Affine3DHelpers.extractScale(initialGlobalToMaskTransform, 0) / Affine3DHelpers.extractScale(currentGlobalToMaskTransform, 0)) + val initialMaskToGlobalWithDepthTransform: AffineTransform3D = initialGlobalToMaskTransform.inverse().copy().concatenate(depthScaleTransform) val initialMaskToSourceWithDepthTransform: AffineTransform3D = initialMaskToSourceTransform.copy().concatenate(depthScaleTransform) val currentMaskToSourceWithDepthTransform: AffineTransform3D = currentMaskToSourceTransform.copy().concatenate(depthScaleTransform) private val maskSourceInterval 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 cc18b95de..2e9424b5f 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 @@ -281,15 +281,15 @@ open class PaintBrushTool(activeSourceStateProperty: SimpleObjectProperty(null) + private var primaryClickLabel: SamPredictor.SparseLabel? by primaryClickLabelProperty.nullable() override val actionSets by LazyForeignValue({ activeViewerAndTransforms }) { mutableListOf( - *super.actionSets.toTypedArray(), *getSamActions().filterNotNull().toTypedArray(), ) } @@ -190,6 +216,8 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty(null) + private var estimatedThreshold by estimatedThresholdProperty.nullable() private var threshold = 0.0 set(value) { field = value.coerceIn(thresholdBounds.min, thresholdBounds.max) @@ -231,13 +259,12 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty - if (maskProvided || !viewer.isMouseInside) return@let Platform.runLater { statusProperty.set("Predicting...") } val x = viewer.mouseXProperty.get().toLong() @@ -258,7 +285,6 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty - val floatVal = float.get() - sum += floatVal - sumSquared += floatVal.pow(2) - if (max < floatVal) max = floatVal + private fun getSamActions(): Array { + lateinit var primaryClickToggleIncludeAction: Action<*> + lateinit var primaryClickToggleExcludeAction: Action<*> + lateinit var resetPromptAction: Action<*> + lateinit var applyPredictionAction: Action<*> + return arrayOf( + painteraActionSet("sam selections", PaintActionType.Paint, ignoreDisable = true) { + /* Handle Painting */ + MOUSE_CLICKED(MouseButton.PRIMARY, withKeysDown = arrayOf(KeyCode.CONTROL)) { + name = "apply last segmentation result to canvas" + verifyEventNotNull() + verifyPainteraNotDisabled() + verify(" label is not valid ") { isLabelValid } + onAction { applyPrediction() } + } + KEY_PRESSED(KeyCode.D) { + name = "view prediction" + verifyEventNotNull() + verifyPainteraNotDisabled() + verify { Paintera.debugMode } + verify("no current prediction ") { currentPrediction != null } + var toggle = true + onAction { + val highResPrediction = currentPrediction!!.image + val lowResPrediction = currentPrediction!!.lowResImage + + val name: String + val maskRai = if (toggle) { + toggle = false + name = "high res" + highResPrediction + } else { + toggle = true + name = "low res" + lowResPrediction } - val area = Intervals.numElements(it) - val mean = sum / area - val stddev = sqrt(sumSquared / area - mean.pow(2)) - doubleArrayOf(max.toDouble(), mean, stddev) - } - val min = (mean - std).toFloat() - val zeroMinValue = maskRai.convert(FloatType()) { input, output -> output.set(input.get() - min) } - val predictionSource = paintera.baseView.addConnectomicsRawSource( - zeroMinValue.let { - val prediction3D = Views.addDimension(it) - val interval3D = Intervals.createMinMax(*it.minAsLongArray(), 0, *it.maxAsLongArray(), 0) - prediction3D.interval(interval3D) - }, - doubleArrayOf(1.0, 1.0, 1.0), - doubleArrayOf(0.0, 0.0, 0.0), - 0.0, max - min, - "$name prediction" - ) - val transform = object : AffineTransform3D() { - override fun set(value: Double, row: Int, column: Int) { - super.set(value, row, column) - predictionSource.backend.updateTransform(this) - setViewer!!.requestRepaint() + val (max, mean, std) = maskRai.let { + var sum = 0.0 + var sumSquared = 0.0 + var max = Float.MIN_VALUE + it.forEach { float -> + val floatVal = float.get() + sum += floatVal + sumSquared += floatVal.pow(2) + if (max < floatVal) max = floatVal + } + val area = Intervals.numElements(it) + val mean = sum / area + val stddev = sqrt(sumSquared / area - mean.pow(2)) + doubleArrayOf(max.toDouble(), mean, stddev) } - } - - viewerMask!!.getInitialGlobalViewerInterval(setViewer!!.width, setViewer!!.height).also { - - val width = it.realMax(0) - it.realMin(0) - val height = it.realMax(1) - it.realMin(1) - val depth = it.realMax(2) - it.realMin(2) - transform.set( - *AffineTransform3D() - .concatenate(Translation3D(it.realMin(0), it.realMin(1), it.realMin(2))) - .concatenate(Scale3D(width / maskRai.shape()[0], height / maskRai.shape()[1], depth)) - .concatenate(Translation3D(.5, .5, 0.0)) //half-pixel offset - .inverse() - .rowPackedCopy + val min = (mean - std).toFloat() + val zeroMinValue = maskRai.convert(FloatType()) { input, output -> output.set(input.get() - min) } + val predictionSource = paintera.baseView.addConnectomicsRawSource( + zeroMinValue.let { + val prediction3D = Views.addDimension(it) + val interval3D = Intervals.createMinMax(*it.minAsLongArray(), 0, *it.maxAsLongArray(), 0) + prediction3D.interval(interval3D) + }, + doubleArrayOf(1.0, 1.0, 1.0), + doubleArrayOf(0.0, 0.0, 0.0), + 0.0, max - min, + "$name prediction" ) - } - predictionSource.backend.updateTransform(transform) - predictionSource.composite = ARGBCompositeAlphaAdd() - setViewer!!.requestRepaint() - } - } - val controlPointLabelToggleGroup = ToggleGroup().also { - it.selectedToggleProperty().addListener { _, _, selected -> - controlMode = selected != null - } - } - KEY_PRESSED(KeyCode.CONTROL) { - val iconClsBinding = controlModeProperty.createNonNullValueBinding { if (it) "toggle-on" else "toggle-off" } - val iconCls by iconClsBinding.nonnullVal() - graphic = { - val icon = FontAwesomeIconView().also { - it.styleClass.addAll(iconCls) - it.id = iconCls - "asdfasd f".stripLeading() - iconClsBinding.addListener { _, old, new -> - it.styleClass.removeAll(old) - it.styleClass.add(new) + val transform = object : AffineTransform3D() { + override fun set(value: Double, row: Int, column: Int) { + super.set(value, row, column) + predictionSource.backend.updateTransform(this) + setViewer!!.requestRepaint() + } } - } - GlyphScaleView(icon).apply { - styleClass += "ignore-disable" - } - } - onAction { event -> - if (event != null) { - /* If triggered by key down, always on when down*/ - controlMode = true - controlPointLabelToggleGroup.selectedToggle?.isSelected = false - controlPointLabelToggleGroup.selectToggle(null) - controlPointLabel = SamPredictor.SparseLabel.IN - } else { - /* If triggered programmatically, negate the current state */ - controlMode = !controlMode - if (!controlMode) { - controlPointLabel = SamPredictor.SparseLabel.IN - resetPromptAndPrediction() - controlPointLabelToggleGroup.selectedToggle?.isSelected = false - controlPointLabelToggleGroup.selectToggle(null) + + viewerMask!!.getInitialGlobalViewerInterval(setViewer!!.width, setViewer!!.height).also { + + val width = it.realMax(0) - it.realMin(0) + val height = it.realMax(1) - it.realMin(1) + val depth = it.realMax(2) - it.realMin(2) + transform.set( + *AffineTransform3D() + .concatenate(Translation3D(it.realMin(0), it.realMin(1), it.realMin(2))) + .concatenate(Scale3D(width / maskRai.shape()[0], height / maskRai.shape()[1], depth)) + .concatenate(Translation3D(.5, .5, 0.0)) //half-pixel offset + .inverse() + .rowPackedCopy + ) } + predictionSource.backend.updateTransform(transform) + predictionSource.composite = ARGBCompositeAlphaAdd() + setViewer!!.requestRepaint() } } - } - KEY_RELEASED(KeyCode.CONTROL) { - onAction { controlMode = false } - } - SCROLL { - verify("scroll size at least 1 pixel") { max(it!!.deltaX.absoluteValue, it.deltaY.absoluteValue) > 1.0 } - verify { controlMode } - verifyEventNotNull() - verifyPainteraNotDisabled() - onAction { scroll -> - /* ScrollEvent deltas are internally multiplied to correspond to some estimate of pixels-per-unit-scroll. - * For example, on the platform I'm using now, it's `40` for both x and y. But our threshold is NOT - * in pixel units, so we divide by the multiplier, and specify our own. */ - val delta = with(scroll!!) { - when { - deltaY.absoluteValue > deltaX.absoluteValue -> deltaY / multiplierY - else -> deltaX / multiplierX + SCROLL { + verify("scroll size at least 1 pixel") { max(it!!.deltaX.absoluteValue, it.deltaY.absoluteValue) > 1.0 } + verifyEventNotNull() + verifyPainteraNotDisabled() + onAction { scroll -> + /* ScrollEvent deltas are internally multiplied to correspond to some estimate of pixels-per-unit-scroll. + * For example, on the platform I'm using now, it's `40` for both x and y. But our threshold is NOT + * in pixel units, so we divide by the multiplier, and specify our own. */ + val delta = with(scroll!!) { + when { + deltaY.absoluteValue > deltaX.absoluteValue -> deltaY / multiplierY + else -> deltaX / multiplierX + } + } + val increment = (thresholdBounds.max - thresholdBounds.min) / 100.0 + threshold += delta * increment + (currentPredictionRequest?.first as? SparsePrediction)?.points?.let { + requestPrediction(it, false) } - } - val increment = (thresholdBounds.max - thresholdBounds.min) / 100.0 - threshold += delta * increment - (currentPredictionRequest?.first as? SparsePrediction)?.points?.let { - requestPrediction(it, false) } } - } - MOUSE_MOVED { - name = "prediction overlay" - verifyEventNotNull() - verifyPainteraNotDisabled() - verify("Must be a temporary prompt, or not in controlMode ") { temporaryPrompt || !controlMode } - verify("Label is not valid") { isLabelValid } - onAction { - clearPromptDrawings() - temporaryPrompt = true - requestPrediction(listOf(SamPoint(it!!.x * screenScale, it.y * screenScale, SamPredictor.SparseLabel.IN))) + MOUSE_MOVED { + name = "prediction overlay" + verifyEventNotNull() + verifyPainteraNotDisabled() + verify("Must be a temporary prompt") { temporaryPrompt } + verify("Label is not valid") { isLabelValid } + onAction { + clearPromptDrawings() + temporaryPrompt = true + requestPrediction(listOf(SamPoint(it!!.x * screenScale, it.y * screenScale, SamPredictor.SparseLabel.IN))) + } } - } - /* Handle Include Points */ - MOUSE_CLICKED(MouseButton.PRIMARY) { - name = "add include point" - graphic = { - val circle = Circle().apply { - styleClass += "sam-point-in" + + val primaryClickLabelToggleGroup = ToggleGroup() + primaryClickLabelProperty.addListener { _, _, new -> + val id = when (new) { + SamPredictor.SparseLabel.IN -> "add include point" + SamPredictor.SparseLabel.OUT -> "add exclude point" + else -> null } - CircleScaleView(circle).apply { - styleClass += listOf("sam-point", "ignore-disable") - properties["TOGGLE_GROUP"] = controlPointLabelToggleGroup + primaryClickLabelToggleGroup.toggles.forEach { toggle -> + toggle.isSelected = (toggle as? ToggleButton)?.let { it.id != null && it.id == id } ?: false } } - verifyPainteraNotDisabled() - verify { controlMode || it == null } - onAction { - CoroutineScope(Dispatchers.IO).launch { - /* If no event, triggered via button; enter control mode, set Label to IN */ - it ?: let { - if ((controlPointLabelToggleGroup.selectedToggle as? ToggleButton)?.id == name) { - controlPointLabel = SamPredictor.SparseLabel.IN - controlMode = true - } else { - resetPromptAndPrediction() - return@launch - } + /* Handle Include Points */ + MOUSE_CLICKED(MouseButton.PRIMARY) { + name = "add include point" + graphic = { + val circle = Circle().apply { + styleClass += "sam-point-in" } + CircleScaleView(circle).apply { + styleClass += listOf("sam-point", "ignore-disable") + properties["TOGGLE_GROUP"] = primaryClickLabelToggleGroup + } + } + verifyPainteraNotDisabled() + onAction { + CoroutineScope(Dispatchers.IO).launch { + /* If no event, triggered via button; set Label to IN */ + it ?: let { + /* If already IN, toggle off and return, otherwise toggle on */ + if (primaryClickLabel == SamPredictor.SparseLabel.IN) { + primaryClickLabel = null + return@launch + } else { + primaryClickLabel = SamPredictor.SparseLabel.IN + } + } - /*If not in control mode, Label for this Action is always IN*/ - if (!controlMode) - controlPointLabel = SamPredictor.SparseLabel.IN + /* If no event, triggered via button, wait for click before continuing */ + (it ?: viewerMask!!.viewer.waitForEvent(MOUSE_CLICKED))?.let { event -> + val label = primaryClickLabel ?: SamPredictor.SparseLabel.IN + val points = currentPredictionRequest?.first.addPoints(SamPoint(event.x * screenScale, event.y * screenScale, label)) + temporaryPrompt = false + requestPrediction(points) + } + } + } + }.also { primaryClickToggleIncludeAction = it } - /* If no event, triggered via button, wait for click before continuing */ - (it ?: viewerMask!!.viewer.waitForEvent(MOUSE_CLICKED))?.let { event -> - val points = currentPredictionRequest?.first.addPoints(SamPoint(event.x * screenScale, event.y * screenScale, controlPointLabel)) - temporaryPrompt = false - requestPrediction(points) + MOUSE_CLICKED(MouseButton.SECONDARY) { + name = "add exclude point" + graphic = { + val circle = Circle().apply { + styleClass += "sam-point-out" + } + CircleScaleView(circle).apply { + styleClass += listOf("sam-point", "ignore-disable") + properties["TOGGLE_GROUP"] = primaryClickLabelToggleGroup } } - } - } + verifyPainteraNotDisabled() + onAction { + CoroutineScope(Dispatchers.IO).launch { + it ?: let { + /* If already IN, toggle off and return, otherwise toggle on */ + if (primaryClickLabel == SamPredictor.SparseLabel.OUT) { + primaryClickLabel = null + return@launch + } else { + primaryClickLabel = SamPredictor.SparseLabel.OUT + } + } - MOUSE_CLICKED(MouseButton.SECONDARY) { - name = "add exclude point" - graphic = { - val circle = Circle().apply { - styleClass += "sam-point-out" + /* If no event, triggered via button, wait for click before continuing */ + (it ?: viewerMask!!.viewer.waitForEvent(MOUSE_CLICKED))?.let { event -> + val points = currentPredictionRequest?.first.addPoints(SamPoint(event.x * screenScale, event.y * screenScale, SamPredictor.SparseLabel.OUT)) + temporaryPrompt = false + requestPrediction(points) + } + } + } + }.also { primaryClickToggleExcludeAction = it } + + KEY_PRESSED(KeyCode.BACK_SPACE) { + name = "Reset Prompt" + graphic = { GlyphScaleView(FontAwesomeIconView(FontAwesomeIcon.REFRESH).apply { styleClass += "reset" }) } + onAction { + resetPromptAndPrediction() + primaryClickLabel = null + primaryClickLabelToggleGroup.selectedToggle?.isSelected = false + temporaryPrompt = true } - CircleScaleView(circle).apply { - styleClass += listOf("sam-point", "ignore-disable") - properties["TOGGLE_GROUP"] = controlPointLabelToggleGroup + }.also { resetPromptAction = it } + + KEY_PRESSED(KeyCode.ENTER) { + name = "apply last segmentation result to canvas" + graphic = { GlyphScaleView(FontAwesomeIconView().apply { styleClass += "accept" }) } + verifyPainteraNotDisabled() + verify(" label is not valid ") { isLabelValid } + onAction { + applyPrediction() + clearPromptDrawings() + currentPredictionRequest = null + primaryClickLabel = null + primaryClickLabelToggleGroup.selectedToggle?.isSelected = false + temporaryPrompt = true } + }.also { applyPredictionAction = it } + + KEY_PRESSED(LabelSourceStateKeys.CANCEL) { + name = "exit SAM tool" + graphic = { GlyphScaleView(FontAwesomeIconView().apply { styleClass += "reject" }).apply { styleClass += "ignore-disable" } } + onAction { mode?.switchTool(mode.defaultTool) } } - verifyPainteraNotDisabled() - verify { controlMode || it == null } - onAction { - CoroutineScope(Dispatchers.IO).launch { - it ?: let { - if ((controlPointLabelToggleGroup.selectedToggle as? ToggleButton)?.id == name) { - controlPointLabel = SamPredictor.SparseLabel.OUT - controlMode = true - } else { - resetPromptAndPrediction() - return@launch + }, + + DeviceManager.xTouchMini?.let { device -> + activeViewerProperty.get()?.viewer()?.let { viewer -> + painteraMidiActionSet("sam", device, viewer) { + MidiToggleEvent.BUTTON_TOGGLE(8) { + name = "PrimaryClickIn" + afterRegisterEvent = { + toggleDisplayProperty.bind(primaryClickLabelProperty.isEqualTo(SamPredictor.SparseLabel.IN)) + } + onAction { primaryClickToggleIncludeAction(null) } + } + MidiToggleEvent.BUTTON_TOGGLE(9) { + name = "PrimaryClickOut" + afterRegisterEvent = { + toggleDisplayProperty.bind(primaryClickLabelProperty.isEqualTo(SamPredictor.SparseLabel.OUT)) + } + onAction { primaryClickToggleExcludeAction(null) } + } + + MidiButtonEvent.BUTTON_PRESSED(10) { + name = "ResetPrompt" + verifyEventNotNull() + onAction { resetPromptAction(null) } + } + MidiButtonEvent.BUTTON_PRESSED(11) { + name = "ApplyPrediction" + verifyEventNotNull() + onAction { + applyPredictionAction(null) + resetPromptAction(null) } } - /* If no event, triggered via button, wait for click before continuing */ - (it ?: viewerMask!!.viewer.waitForEvent(MOUSE_CLICKED))?.let { event -> - val points = currentPredictionRequest?.first.addPoints(SamPoint(event.x * screenScale, event.y * screenScale, SamPredictor.SparseLabel.OUT)) - temporaryPrompt = false - requestPrediction(points) + MidiFaderEvent.FADER(0) { + estimatedThresholdProperty.addListener { _, _, est -> + min = (thresholdBounds.min).toInt() + max = (thresholdBounds.max).toInt() + } + onAction { + threshold = it!!.value.toDouble() + (currentPredictionRequest?.first as? SparsePrediction)?.points?.let { + requestPrediction(it, false) + } + } + } + + MidiPotentiometerEvent.POTENTIOMETER_ABSOLUTE(7) { + displayType = VPotControl.DisplayType.FAN + asPercent = true + estimatedThresholdProperty.addListener { _, _, estimated -> + /*do nothing if estimated threshold is null */ + estimated ?: return@addListener + val thresholdPercent = ((threshold - thresholdBounds.min) / (thresholdBounds.max - thresholdBounds.min)).absoluteValue.coerceIn(0.0, 1.0) + control.setValueSilently((127 * thresholdPercent).toInt()) + control.display() + } + onAction { + threshold = thresholdBounds.min + (thresholdBounds.max - thresholdBounds.min) * it!!.value.toDouble() + (currentPredictionRequest?.first as? SparsePrediction)?.points?.let { + requestPrediction(it, false) + } + } } } } - } + }, - KEY_PRESSED(KeyCode.DELETE, KeyCode.BACK_SPACE) { - name = "Reset Prompt" - graphic = { GlyphScaleView(FontAwesomeIconView(FontAwesomeIcon.REFRESH).apply { styleClass += "reset" }) } - onAction { - resetPromptAndPrediction() - } - } + painteraDragActionSet("box prediction request", PaintActionType.Paint, ignoreDisable = true, consumeMouseClicked = true) { + onDrag { mouse -> - KEY_PRESSED(KeyCode.ENTER) { - name = "apply last segmentation result to canvas" - graphic = { GlyphScaleView(FontAwesomeIconView().apply { styleClass += "accept" }) } - verifyPainteraNotDisabled() - verify(" label is not valid ") { isLabelValid } - onAction { - applyPrediction() - currentPredictionRequest = null - } - } + val xInBounds = mouse.x.coerceIn(0.0, activeViewer!!.width) + val yInBounds = mouse.y.coerceIn(0.0, activeViewer!!.height) - KEY_PRESSED(LabelSourceStateKeys.CANCEL) { - name = "exit SAM tool" - graphic = { GlyphScaleView(FontAwesomeIconView().apply { styleClass += "reject" }).apply {styleClass += "ignore-disable"} } - onAction { mode?.switchTool(mode.defaultTool) } - } - }, + val (minX, maxX) = (if (startX < mouse.x) startX to xInBounds else xInBounds to startX) + val (minY, maxY) = (if (startY < mouse.y) startY to yInBounds else yInBounds to startY) - DeviceManager.xTouchMini?.let { device -> - activeViewerProperty.get()?.viewer()?.let { viewer -> - painteraMidiActionSet("midi sam tool actions", device, viewer, PaintActionType.Paint) { - MidiButtonEvent.BUTTON_PRESED(8) { - onAction { controlMode = true } - } - MidiButtonEvent.BUTTON_RELEASED(8) { - onAction { controlMode = false } - } + val topLeft = SamPoint(minX * screenScale, minY * screenScale, SamPredictor.SparseLabel.TOP_LEFT_BOX) + val bottomRight = SamPoint(maxX * screenScale, maxY * screenScale, SamPredictor.SparseLabel.BOTTOM_RIGHT_BOX) + val points = setBoxPrompt(topLeft, bottomRight) + temporaryPrompt = false + requestPrediction(points) } } - }, - - painteraDragActionSet("box prediction request", PaintActionType.Paint, ignoreDisable = true, consumeMouseClicked = true) { - onDrag { mouse -> - val (minX, maxX) = (if (startX < mouse.x) startX to mouse.x else mouse.x to startX) - val (minY, maxY) = (if (startY < mouse.y) startY to mouse.y else mouse.y to startY) - - val topLeft = SamPoint(minX * screenScale, minY * screenScale, SamPredictor.SparseLabel.TOP_LEFT_BOX) - val bottomRight = SamPoint(maxX * screenScale, maxY * screenScale, SamPredictor.SparseLabel.BOTTOM_RIGHT_BOX) - val points = setBoxPrompt(topLeft, bottomRight) - temporaryPrompt = false - requestPrediction(points) - } - } - ) + ) + } private fun resetPromptAndPrediction() { clearPromptDrawings() @@ -831,7 +888,7 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty val predictionType = predictionMaskRA.get() val predictionValue = predictionType.get() @@ -1023,6 +1080,7 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty, samPrediction: SamPredictor.SamPrediction, lowRes: Boolean = false): FinalInterval? { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationSAMTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationSAMTool.kt index 0eb93f591..47806584f 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationSAMTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationSAMTool.kt @@ -5,6 +5,7 @@ import javafx.scene.control.ButtonBase import org.janelia.saalfeldlab.fx.actions.ActionSet import org.janelia.saalfeldlab.fx.actions.painteraActionSet import org.janelia.saalfeldlab.fx.extensions.LazyForeignValue +import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.paintera.cache.SamEmbeddingLoaderCache import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationController import org.janelia.saalfeldlab.paintera.control.actions.PaintActionType @@ -55,9 +56,12 @@ internal class ShapeInterpolationSAMTool(private val controller: ShapeInterpolat it.prediction = predictionRequest it.locked = true } - shapeInterpolationMode.run { - switchTool(defaultTool) - modeToolsBar.toggleGroup?.selectToggle(null) + InvokeOnJavaFXApplicationThread { + + shapeInterpolationMode.run { + switchTool(defaultTool) + modeToolsBar.toggleGroup?.selectToggle(null) + } } } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationTool.kt index 6e194ae18..d361b6f92 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationTool.kt @@ -12,20 +12,22 @@ import javafx.util.Duration import kotlinx.coroutines.* import net.imglib2.realtransform.AffineTransform3D import net.imglib2.util.Intervals +import org.checkerframework.checker.units.qual.Current import org.janelia.saalfeldlab.fx.UtilityTask -import org.janelia.saalfeldlab.fx.actions.ActionSet +import org.janelia.saalfeldlab.fx.actions.* import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.installActionSet import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.removeActionSet -import org.janelia.saalfeldlab.fx.actions.painteraActionSet -import org.janelia.saalfeldlab.fx.actions.painteraDragActionSet import org.janelia.saalfeldlab.fx.extensions.addWithListener import org.janelia.saalfeldlab.fx.extensions.createNonNullValueBinding import org.janelia.saalfeldlab.fx.extensions.createNullableValueBinding import org.janelia.saalfeldlab.fx.extensions.nonnullVal +import org.janelia.saalfeldlab.fx.midi.MidiButtonEvent +import org.janelia.saalfeldlab.fx.midi.MidiToggleEvent import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews import org.janelia.saalfeldlab.fx.ui.ScaleView import org.janelia.saalfeldlab.fx.ui.GlyphScaleView import org.janelia.saalfeldlab.labels.Label +import org.janelia.saalfeldlab.paintera.DeviceManager import org.janelia.saalfeldlab.paintera.LabelSourceStateKeys.* import org.janelia.saalfeldlab.paintera.cache.SamEmbeddingLoaderCache import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationController @@ -47,10 +49,13 @@ internal class ShapeInterpolationTool( ) : ViewerTool(shapeInterpolationMode) { - override val actionSets: MutableList = mutableListOf( - shapeInterpolationActions(), - cancelShapeInterpolationTask() - ) + override val actionSets: MutableList by lazy { + mutableListOf( + *shapeInterpolationActions().filterNotNull().toTypedArray(), + cancelShapeInterpolationTask(), + *NavigationTool.actionSets.filter { "rotat" !in it.name }.toTypedArray() //Kinda ugly to filter like this, but this is a weird case. Still, should do better + ) + } override val graphic = { GlyphScaleView(FontAwesomeIconView().also { it.styleClass += listOf("navigation-tool") }) } override val name: String = "Shape Interpolation" @@ -73,14 +78,12 @@ internal class ShapeInterpolationTool( NavigationTool.activate() NavigationTool.activeViewerProperty.unbind() NavigationTool.activeViewerProperty.bind(activeViewerProperty) - NavigationTool.installInto(activeViewer!!) } override fun deactivate() { /* We intentionally unbound the activeViewer for this, to support the button toggle. * We now need to explicitly remove the NavigationTool from the activeViewer we care about. * Still deactive it first, to handle the rest of the cleanup */ - NavigationTool.removeFrom(activeViewer!!) NavigationTool.deactivate() disabledViewerActions.forEach { (vat, actionSets) -> actionSets.forEach { vat.viewer().removeActionSet(it) } } disabledViewerActions.clear() @@ -214,187 +217,239 @@ internal class ShapeInterpolationTool( return EdgeDepthsAndSpacing(firstDepth, firstSpacing, lastDepth, lastSpacing) } - private fun shapeInterpolationActions(): ActionSet { - return painteraActionSet("shape interpolation", PaintActionType.ShapeInterpolation) { - with(controller) { - verifyAll(KEY_PRESSED) { isControllerActive } - KEY_PRESSED(SHAPE_INTERPOLATION__ACCEPT_INTERPOLATION){ - graphic = { GlyphScaleView(FontAwesomeIconView().apply { styleClass += listOf("accept", "accept-shape-interpolation") }) } - onAction { - if (applyMask()) { + private fun shapeInterpolationActions(): Array { + lateinit var autoSamCurrent: Action<*> + lateinit var autoSamLeft: Action<*> + lateinit var autoSamBisectAll: Action<*> + lateinit var autoSamBisectCurrent: Action<*> + lateinit var autoSamRight: Action<*> + with(controller) { + return arrayOf( + painteraActionSet("shape interpolation", PaintActionType.ShapeInterpolation) { + verifyAll(KEY_PRESSED) { isControllerActive } + KEY_PRESSED(SHAPE_INTERPOLATION__ACCEPT_INTERPOLATION) { + graphic = { GlyphScaleView(FontAwesomeIconView().apply { styleClass += listOf("accept", "accept-shape-interpolation") }) } + onAction { + if (applyMask()) { + paintera.baseView.changeMode(previousMode) + } + } + handleException { + it.printStackTrace() paintera.baseView.changeMode(previousMode) } } - handleException { - it.printStackTrace() - paintera.baseView.changeMode(previousMode) - } - } - KEY_PRESSED(SHAPE_INTERPOLATION__TOGGLE_PREVIEW) { - val iconClsBinding = controller.previewProperty.createNonNullValueBinding { if (it) "toggle-on" else "toggle-off" } - val iconCls by iconClsBinding.nonnullVal() - graphic = { - val icon = FontAwesomeIconView().also { - it.styleClass.addAll(iconCls) - it.id = iconCls - iconClsBinding.addListener { _, old, new -> - it.styleClass.removeAll(old) - it.styleClass.add(new) + KEY_PRESSED(SHAPE_INTERPOLATION__TOGGLE_PREVIEW) { + val iconClsBinding = controller.previewProperty.createNonNullValueBinding { if (it) "toggle-on" else "toggle-off" } + val iconCls by iconClsBinding.nonnullVal() + graphic = { + val icon = FontAwesomeIconView().also { + it.styleClass.addAll(iconCls) + it.id = iconCls + iconClsBinding.addListener { _, old, new -> + it.styleClass.removeAll(old) + it.styleClass.add(new) + } } + GlyphScaleView(icon) + } + onAction { controller.togglePreviewMode() } + handleException { + paintera.baseView.changeMode(previousMode) } - GlyphScaleView(icon) - } - onAction { controller.togglePreviewMode() } - handleException { - paintera.baseView.changeMode(previousMode) } - } - KEY_PRESSED(SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICE_LEFT) { - graphic = { ScaleView().apply { styleClass += listOf("auto-sam", "slice-left") } } - onAction { - val depths = sortedSliceDepths.toMutableList() - val (firstDepth, firstSpacing, lastDepth, lastSpacing) = edgeDepthsAndSpacing(depths) - - /* trigger the prediction, and move there when done */ - val newFirstDepth = (firstDepth ?: firstSpacing) - firstSpacing - - /* request the next depth immediately, request the rest when this one finishes */ - requestSamPrediction(newFirstDepth, moveToSlice = true) { - requestEmbedding(newFirstDepth - 2 * firstSpacing) - /* Request next in the opposite direction */ - requestEmbedding((lastDepth ?: 0.0) + lastSpacing) - firstDepth?.let { - /* request the center between the newest prediction and previous */ - requestEmbedding(it - firstSpacing * .5) + autoSamLeft = KEY_PRESSED(SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICE_LEFT) { + graphic = { ScaleView().apply { styleClass += listOf("auto-sam", "slice-left") } } + onAction { + val depths = sortedSliceDepths.toMutableList() + val (firstDepth, firstSpacing, lastDepth, lastSpacing) = edgeDepthsAndSpacing(depths) + + /* trigger the prediction, and move there when done */ + val newFirstDepth = (firstDepth ?: firstSpacing) - firstSpacing + + /* request the next depth immediately, request the rest when this one finishes */ + requestSamPrediction(newFirstDepth, moveToSlice = true) { + requestEmbedding(newFirstDepth - 2 * firstSpacing) + /* Request next in the opposite direction */ + requestEmbedding((lastDepth ?: 0.0) + lastSpacing) + firstDepth?.let { + /* request the center between the newest prediction and previous */ + requestEmbedding(it - firstSpacing * .5) + } } + requestEmbedding(newFirstDepth - firstSpacing) } - requestEmbedding(newFirstDepth - firstSpacing) } - } - KEY_PRESSED(SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICES_BISECT) { - graphic = { ScaleView().apply { styleClass += listOf("auto-sam", "slice-bisect") } } - onAction { - - val depths = sortedSliceDepths.toMutableList() - val remainingRequest = SimpleIntegerProperty().apply { - addListener { _, _, remaining -> - if (remaining == 0) { - depths.forEach { requestSamPrediction(it, refresh = true) } - - depths.sort() - /* eagerly request the next embeddings */ - depths.zipWithNext { before, after -> - val eagerDepth = (before + after) / 2.0 - requestEmbedding(eagerDepth) + autoSamBisectAll = KEY_PRESSED(SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICES_BISECT_ALL) { + graphic = { ScaleView().apply { styleClass += listOf("auto-sam", "slice-bisect") } } + onAction { + val depths = sortedSliceDepths.toMutableList() + val remainingRequest = SimpleIntegerProperty().apply { + addListener { _, _, remaining -> + if (remaining == 0) { + depths.forEach { requestSamPrediction(it, refresh = true) } + + depths.sort() + /* eagerly request the next embeddings */ + depths.zipWithNext { before, after -> + val eagerDepth = (before + after) / 2.0 + requestEmbedding(eagerDepth) + } } } } - } - /* Do the prediction */ - sortedSliceDepths.zipWithNext { before, after -> (before + after) / 2.0 }.forEach { - depths += it - remainingRequest.value++ - requestSamPrediction(it) { - remainingRequest.value-- + /* Do the prediction */ + sortedSliceDepths.zipWithNext { before, after -> (before + after) / 2.0 }.forEach { + depths += it + remainingRequest.value++ + requestSamPrediction(it) { + remainingRequest.value-- + } } } } - } - KEY_PRESSED(SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICE_RIGHT) { - graphic = { ScaleView().apply { styleClass += listOf("auto-sam", "slice-right") } } - onAction { - val depths = sortedSliceDepths.toMutableList() - val (firstDepth, firstSpacing, lastDepth, lastSpacing) = edgeDepthsAndSpacing(depths) - - val newLastDepth = (lastDepth ?: -lastSpacing) + lastSpacing - requestSamPrediction(newLastDepth, moveToSlice = true) { - requestEmbedding(newLastDepth + 2 * lastSpacing) - /* Request next in the opposite direction */ - requestEmbedding((firstDepth ?: 0.0) - firstSpacing) - lastDepth?.let { - /* request the center between the newest prediction and previous */ - requestEmbedding(it + lastSpacing * .5) - } + + autoSamBisectAll = KEY_PRESSED(SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICES_BISECT) { + onAction { + val depths = sortedSliceDepths.toMutableList() + val (left, right) = depths.zipWithNext().firstOrNull { (left, right) -> + currentDepth in left..right + } ?: return@onAction + requestSamPrediction((right + left) * .5, refresh = true) } - /* Request next two in the same direction */ - requestEmbedding(newLastDepth + lastSpacing) } - } - KEY_PRESSED(KeyCode.ALT, KeyCode.A) { - name = "refresh unlocked slice predictions" - onAction { - shapeInterpolationMode.samSliceCache - .filter { (_, info) -> !info.locked } - .toList() - .forEach { (depth, _) -> - requestSamPrediction(depth.toDouble(), refresh = true) + autoSamRight = KEY_PRESSED(SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICE_RIGHT) { + graphic = { ScaleView().apply { styleClass += listOf("auto-sam", "slice-right") } } + onAction { + val depths = sortedSliceDepths.toMutableList() + val (firstDepth, firstSpacing, lastDepth, lastSpacing) = edgeDepthsAndSpacing(depths) + + val newLastDepth = (lastDepth ?: -lastSpacing) + lastSpacing + requestSamPrediction(newLastDepth, moveToSlice = true) { + requestEmbedding(newLastDepth + 2 * lastSpacing) + /* Request next in the opposite direction */ + requestEmbedding((firstDepth ?: 0.0) - firstSpacing) + lastDepth?.let { + /* request the center between the newest prediction and previous */ + requestEmbedding(it + lastSpacing * .5) + } } + /* Request next two in the same direction */ + requestEmbedding(newLastDepth + lastSpacing) + } } - } - KEY_PRESSED(SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICE_HERE) { - onAction { - requestSamPrediction(currentDepth, refresh = true) + KEY_PRESSED(KeyCode.ALT, KeyCode.A) { + name = "refresh unlocked slice predictions" + onAction { + shapeInterpolationMode.samSliceCache + .filter { (_, info) -> !info.locked } + .toList() + .forEach { (depth, _) -> + requestSamPrediction(depth.toDouble(), refresh = true) + } + } } - } - - listOf(SHAPE_INTERPOLATION__REMOVE_SLICE_1, SHAPE_INTERPOLATION__REMOVE_SLICE_2).forEach { key -> - KEY_PRESSED(key) { - filter = true - consume = false + autoSamCurrent = KEY_PRESSED(SHAPE_INTERPOLATION__AUTO_SAM__NEW_SLICE_HERE) { onAction { - deleteSliceAt(currentDepth)?.also { shapeInterpolationMode.samSliceCache -= currentDepth } + requestSamPrediction(currentDepth, refresh = true) } } - } - MOUSE_CLICKED { - name = "select object in current slice" - - verifyNoKeysDown() - verifyEventNotNull() - verify { !paintera.mouseTracker.isDragging } - verify { shapeInterpolationMode.activeTool !is Fill2DTool } - verify { it!!.button == MouseButton.PRIMARY } // respond to primary click - verify { controllerState != ShapeInterpolationController.ControllerState.Interpolate } // need to be in the select state - verify("Can't select BACKGROUND or higher MAX_ID ") { event -> - - source.resetMasks(false) - val mask = getMask() - - fill2D.fill2D.provideMask(mask) - val pointInMask = mask.displayPointToInitialMaskPoint(event!!.x, event.y) - val pointInSource = pointInMask.positionAsRealPoint().also { mask.initialMaskToSourceTransform.apply(it, it) } - val info = mask.info - val sourceLabel = source.getInterpolatedDataSource(info.time, info.level, null).getAt(pointInSource).integerLong - return@verify sourceLabel != Label.BACKGROUND && sourceLabel.toULong() <= Label.MAX_ID.toULong() + listOf(SHAPE_INTERPOLATION__REMOVE_SLICE_1, SHAPE_INTERPOLATION__REMOVE_SLICE_2).forEach { key -> + KEY_PRESSED(key) { + filter = true + consume = false + onAction { + deleteSliceAt(currentDepth)?.also { shapeInterpolationMode.samSliceCache -= currentDepth } + } + } } - onAction { event -> - /* get value at position */ - deleteSliceOrInterpolant()?.let { prevSliceGlobalInterval -> - source.resetMasks(true) - paintera.baseView.orthogonalViews().requestRepaint(Intervals.smallestContainingInterval(prevSliceGlobalInterval)) + MOUSE_CLICKED { + name = "select object in current slice" + + verifyNoKeysDown() + verifyEventNotNull() + verify { !paintera.mouseTracker.isDragging } + verify { shapeInterpolationMode.activeTool !is Fill2DTool } + verify { it!!.button == MouseButton.PRIMARY } // respond to primary click + verify { controllerState != ShapeInterpolationController.ControllerState.Interpolate } // need to be in the select state + verify("Can't select BACKGROUND or higher MAX_ID ") { event -> + + source.resetMasks(false) + val mask = getMask() + + fill2D.fill2D.provideMask(mask) + val pointInMask = mask.displayPointToInitialMaskPoint(event!!.x, event.y) + val pointInSource = pointInMask.positionAsRealPoint().also { mask.initialMaskToSourceTransform.apply(it, it) } + val info = mask.info + val sourceLabel = source.getInterpolatedDataSource(info.time, info.level, null).getAt(pointInSource).integerLong + return@verify sourceLabel != Label.BACKGROUND && sourceLabel.toULong() <= Label.MAX_ID.toULong() + + } + onAction { event -> + /* get value at position */ + deleteSliceOrInterpolant()?.let { prevSliceGlobalInterval -> + source.resetMasks(true) + paintera.baseView.orthogonalViews().requestRepaint(Intervals.smallestContainingInterval(prevSliceGlobalInterval)) + } + currentTask = fillObjectInSlice(event!!) } - currentTask = fillObjectInSlice(event!!) } - } - MOUSE_CLICKED { - name = "toggle object in current slice" - verify { !paintera.mouseTracker.isDragging } - verify { controllerState != ShapeInterpolationController.ControllerState.Interpolate } - verifyEventNotNull() - verify { - val triggerByRightClick = (it?.button == MouseButton.SECONDARY) && keyTracker()!!.noKeysActive() - val triggerByCtrlLeftClick = (it?.button == MouseButton.PRIMARY) && keyTracker()!!.areOnlyTheseKeysDown(KeyCode.CONTROL) - triggerByRightClick || triggerByCtrlLeftClick + MOUSE_CLICKED { + name = "toggle object in current slice" + verify { !paintera.mouseTracker.isDragging } + verify { controllerState != ShapeInterpolationController.ControllerState.Interpolate } + verifyEventNotNull() + verify { + val triggerByRightClick = (it?.button == MouseButton.SECONDARY) && keyTracker()!!.noKeysActive() + val triggerByCtrlLeftClick = (it?.button == MouseButton.PRIMARY) && keyTracker()!!.areOnlyTheseKeysDown(KeyCode.CONTROL) + triggerByRightClick || triggerByCtrlLeftClick + } + onAction { event -> + currentTask = fillObjectInSlice(event!!) + } } - onAction { event -> - currentTask = fillObjectInSlice(event!!) + }, + DeviceManager.xTouchMini?.let { device -> + painteraMidiActionSet("AutoSam", device, activeViewer!!) { + MidiToggleEvent.BUTTON_TOGGLE(4) { + name = "preview" + afterRegisterEvent = { + toggleDisplayProperty.bind(controller.previewProperty) + } + onAction { controller.togglePreviewMode() } + } + MidiButtonEvent.BUTTON_PRESSED(5) { + name = "left" + verifyEventNotNull() + onAction { autoSamLeft(null) } + } + MidiButtonEvent.BUTTON_PRESSED(6) { + name = "bisect.all" + verifyEventNotNull() + onAction { autoSamBisectAll(null) } + } + MidiButtonEvent.BUTTON_PRESSED(7) { + name = "right" + verifyEventNotNull() + onAction { autoSamRight(null) } + } + MidiButtonEvent.BUTTON_PRESSED(14) { + name = "bisect.current" + verifyEventNotNull() + onAction { autoSamBisectCurrent(null) } + } + MidiButtonEvent.BUTTON_PRESSED(15) { + name = "current" + verifyEventNotNull() + onAction { autoSamCurrent(null) } + } } } - } + ) } } diff --git a/src/test/java/org/janelia/saalfeldlab/fx/ui/MatchSelectionExample.java b/src/test/java/org/janelia/saalfeldlab/fx/ui/MatchSelectionExample.java index 3c361472c..c599e9c5d 100644 --- a/src/test/java/org/janelia/saalfeldlab/fx/ui/MatchSelectionExample.java +++ b/src/test/java/org/janelia/saalfeldlab/fx/ui/MatchSelectionExample.java @@ -2,14 +2,20 @@ import com.sun.javafx.application.PlatformImpl; import javafx.application.Platform; +import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.ContextMenu; import javafx.scene.control.CustomMenuItem; +import javafx.scene.control.Label; import javafx.scene.control.Menu; import javafx.scene.control.MenuButton; import javafx.scene.control.MenuItem; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; import javafx.stage.DirectoryChooser; import javafx.stage.Stage; @@ -51,7 +57,7 @@ private static Menu menu( return menu; } - public static void main(String[] args) throws IOException { + private static void menuExample() throws IOException { final List filenames = Files.list(Paths.get(System.getProperty("user.home"))).map(Path::toAbsolutePath).map(Path::toString) .collect(Collectors.toList()); @@ -86,4 +92,63 @@ public static void main(String[] args) throws IOException { }); } + public static void gridPaneExample() throws IOException { + + final List filenames = Files.list(Paths.get(System.getProperty("user.home"))).map(Path::toAbsolutePath).map(Path::toString) + .collect(Collectors.toList()); + Collections.sort(filenames); + PlatformImpl.startup(() -> { + }); + Platform.setImplicitExit(true); + Platform.runLater(() -> { + + GridPane grid = new GridPane(); + grid.add(new Label("asdfasdfasdf"), 0, 0); + grid.add(new Label("asdfasdfasdf"), 1, 0); + grid.add(new Label("asdfasdfasdf"), 2, 0); + grid.add(new Label("asdfasdfasdf"), 3, 0); + grid.add(new Label("asdfasdfasdf"), 4, 0); + + final VBox vbox = new VBox(new Label("test")); + VBox.setVgrow(vbox, Priority.ALWAYS); + vbox.setAlignment(Pos.CENTER); + grid.add(vbox, 0, 1); + GridPane.setColumnSpan(vbox, GridPane.REMAINING); + GridPane.setHgrow(vbox, Priority.ALWAYS); + GridPane.setVgrow(vbox, Priority.ALWAYS); + + final MatchSelection node = MatchSelection.fuzzyTop(filenames, it -> {}, 20); + node.maxWidthProperty().bind(grid.widthProperty()); + grid.add(node, 0, 2); + GridPane.setColumnSpan(node, GridPane.REMAINING); + GridPane.setRowSpan(node, GridPane.REMAINING); + GridPane.setHgrow(node, Priority.ALWAYS); + GridPane.setVgrow(node, Priority.ALWAYS); + + for (int i = 0; i < grid.getColumnCount(); i++) { + final ColumnConstraints constraint = new ColumnConstraints(); + constraint.setFillWidth(true); + constraint.setHgrow(Priority.ALWAYS); + grid.getColumnConstraints().add(constraint); + } + + final Stage stage = new Stage(); + final Scene scene = new Scene(grid); + stage.setScene(scene); + stage.addEventHandler(KeyEvent.KEY_PRESSED, e -> { + if (KeyCode.ESCAPE.equals(e.getCode())) { + if (!e.isConsumed()) + stage.close(); + e.consume(); + } + }); + stage.show(); + }); + } + + public static void main(String[] args) throws IOException { + + gridPaneExample(); + } + }