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
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/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/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/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/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 {
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;
}
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/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();
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)
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? = {
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/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/BorderPaneWithStatusBars.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.kt
index 4bec3923d..ec982bdb6 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, MenuActionType.ResizeViewers)
+ 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..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
@@ -171,7 +175,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)
}
@@ -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])
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..256694bb9 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
@@ -65,6 +66,17 @@ 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/config/SegmentAnythingConfig.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/SegmentAnythingConfig.kt
index b04117c91..94dd6f02d 100644
--- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/SegmentAnythingConfig.kt
+++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/SegmentAnythingConfig.kt
@@ -6,6 +6,7 @@ import javafx.application.Platform
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
+import javafx.beans.value.ObservableValueBase
import javafx.event.EventHandler
import javafx.geometry.Pos
import javafx.scene.control.*
@@ -13,6 +14,7 @@ import javafx.scene.layout.ColumnConstraints
import javafx.scene.layout.GridPane
import javafx.scene.layout.Priority
import javafx.scene.layout.VBox
+import org.janelia.saalfeldlab.fx.extensions.createObservableBinding
import org.janelia.saalfeldlab.fx.extensions.nonnull
import org.janelia.saalfeldlab.fx.ui.NumberField
import org.janelia.saalfeldlab.fx.ui.ObjectField
@@ -26,8 +28,9 @@ import org.janelia.saalfeldlab.paintera.serialization.PainteraSerialization
import org.janelia.saalfeldlab.paintera.ui.FontAwesome
import org.scijava.plugin.Plugin
import java.lang.reflect.Type
+import java.util.UUID
-class SegmentAnythingConfig {
+class SegmentAnythingConfig : ObservableValueBase() {
private val serviceUrlProperty = SimpleStringProperty(System.getenv(SAM_SERVICE_HOST_ENV) ?: DEFAULT_SERVICE_URL).apply {
addListener { _, _, new -> if (new.isBlank()) serviceUrl = DEFAULT_SERVICE_URL }
@@ -50,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"
@@ -88,19 +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.textProperty().addListener { _, _, new ->
- if (new.isBlank()) {
- it.text = DEFAULT_SERVICE_URL
- Platform.runLater { it.positionCaret(0) }
- } else {
- config.serviceUrl = new
- }
+ 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()
}
- add(it, 1, row)
+ focusedProperty().subscribe { focused ->
+ if (!focused)
+ onAction.handle(null)
+ }
+ add(this, 1, row)
}
Button().also {
it.graphic = FontAwesome[FontAwesomeIcon.UNDO]
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..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
@@ -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
@@ -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
@@ -622,7 +618,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 +662,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()
@@ -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()
}
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..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
@@ -64,8 +68,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
@@ -79,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) {
@@ -97,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<*, *>
@@ -116,11 +119,15 @@ 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() }
+
}
}
}
-object SmoothAction : MenuAction("_Smooth") {
+object SmoothAction : MenuAction("_Smooth...") {
private fun newConvolutionExecutor(): ThreadPoolExecutor {
val threads = Runtime.getRuntime().availableProcessors()
@@ -134,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()
@@ -166,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 */
@@ -193,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)
@@ -243,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())
}
@@ -383,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()
@@ -439,7 +477,7 @@ object SmoothAction : MenuAction("_Smooth") {
}
paintera.baseView.disabledPropertyBindings -= task
- kernelSizeProperty.removeListener(kernelSizeChange)
+ smoothTriggerSubscription?.unsubscribe()
paintera.baseView.orthogonalViews().setScreenScales(prevScales)
convolutionExecutor.shutdown()
}
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 97f26a554..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
@@ -97,10 +97,13 @@ interface ToolMode : SourceMode {
(activeTool as? ViewerTool)?.removeFromAll()
-
/* If the mode was changed before we can activate, switch to null */
val activeMode = paintera.baseView.activeModeProperty.value
- activeTool = if (activeMode != this@ToolMode) null else tool?.apply { activate() }
+ activeTool = when {
+ activeMode != this@ToolMode -> null // wrong mode
+ tool?.isValidProperty?.value == 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/modes/PaintLabelMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt
index a0c86f63b..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
@@ -26,12 +26,13 @@ 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
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
@@ -100,7 +101,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 {
@@ -108,19 +109,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)
}
}
}
@@ -150,8 +153,9 @@ 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<*, *> }
verify { activeTool !is SamTool }
verify {
@@ -169,7 +173,7 @@ object PaintLabelMode : AbstractToolMode() {
switchTool(defaultTool)
}
}
- KEY_PRESSED(LabelSourceStateKeys.CANCEL) {
+ KEY_PRESSED(CANCEL) {
verify { activeSourceStateProperty.get() is ConnectomicsLabelState<*, *> }
verify { activeTool is SamTool }
filter = true
@@ -203,9 +207,9 @@ object PaintLabelMode : AbstractToolMode() {
)
private fun getSelectNextIdActions() = painteraActionSet("Create New Segment", LabelActionType.CreateNew) {
- KEY_PRESSED(LabelSourceStateKeys.NEXT_ID) {
- name = "create new segment"
- verify { activeTool?.let { it !is PaintTool || !it.isPainting } ?: true }
+ KEY_PRESSED(NEXT_ID) {
+ name = "create_new_segment"
+ verify("Not Painting") { (activeTool as? PaintTool)?.isPainting?.not() ?: true }
onAction {
statePaintContext?.nextId(activate = true)
}
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>
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..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)
+ .add(MenuActionType.ToggleMaximizeViewer, MenuActionType.DetachViewer, MenuActionType.ResizeViewers, MenuActionType.ToggleSidePanel, MenuActionType.ResizePanel)
.add(NavigationActionType.Pan, NavigationActionType.Slice, NavigationActionType.Zoom)
.create()
@@ -394,7 +393,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/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/paint/ViewerMask.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/ViewerMask.kt
index 6e738271a..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
@@ -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,127 @@ 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
- }
- }
- }
- } 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
+ if (withinMax)
+ break
+ }
+
+ if (!withinMax) {
+ return@forEachPixel
+ }
+
+ val paintVal = viewerValType.get()
+
+ if (acceptAsPainted.test(paintVal) && zTransformAtCubeCorner(canvasPosition, null).absoluteValue < minDistInMask)
+ 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(), maskId)
+ return@forEachPixel
}
}
}
}
}
+ }
paintera.baseView.orthogonalViews().requestRepaint(sourceToGlobalTransform.estimateBounds(canvas.extendBy(1.0)))
return paintedLabelSet
}
@@ -532,9 +515,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 +532,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 +545,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,
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..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.*
@@ -27,6 +25,7 @@ interface Tool {
fun deactivate() {}
val statusProperty: StringProperty
+ val isValidProperty: BooleanProperty
val actionSets: MutableList
}
@@ -62,10 +61,24 @@ 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 actionIsValid = { action?.isValid(null) ?: 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 || !actionIsValid())
+ }
+ } else {
+ btn.disableProperty().set(!actionIsValid())
}
+ /* set initial state to */
+ btn.disableProperty().set(!actionIsValid())
+
+ (this as? Tool)?.isValidProperty?.`when`(btn.visibleProperty())?.subscribe { isValid -> btn.disableProperty().set(!isValid) }
}
btn.styleClass += "toolbar-button"
btn.tooltip = Tooltip(
@@ -83,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/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 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