From bad728d8959e2159e78ea80b079c67395d25ec0e Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 17 May 2019 14:57:55 -0400
Subject: [PATCH 01/84] add viewer-specific handler for entering shape
 interpolation mode

---
 .../control/ShapeInterpolationMode.java       | 62 +++++++++++++++++++
 .../paintera/state/LabelSourceState.java      |  9 ++-
 2 files changed, 70 insertions(+), 1 deletion(-)
 create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
new file mode 100644
index 000000000..b669ff417
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -0,0 +1,62 @@
+package org.janelia.saalfeldlab.paintera.control;
+
+import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
+import org.janelia.saalfeldlab.fx.event.EventFX;
+import org.janelia.saalfeldlab.fx.event.KeyTracker;
+import org.janelia.saalfeldlab.paintera.PainteraBaseView;
+import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
+
+import bdv.fx.viewer.ViewerPanelFX;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+
+public class ShapeInterpolationMode
+{
+	private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>();
+
+	private final SelectedIds selectedIds;
+
+	public ShapeInterpolationMode(final SelectedIds selectedIds)
+	{
+		this.selectedIds = selectedIds;
+	}
+
+	public ObjectProperty<ViewerPanelFX> activeViewerProperty()
+	{
+		return this.activeViewer;
+	}
+
+	public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final KeyTracker keyTracker)
+	{
+		final DelegateEventHandlers.AnyHandler filter = DelegateEventHandlers.handleAny();
+		filter.addEventHandler(
+				KeyEvent.KEY_PRESSED,
+				EventFX.KEY_PRESSED(
+						"enter shape interpolation mode",
+						e -> enterMode((ViewerPanelFX) e.getTarget()),
+						e -> e.getTarget() instanceof ViewerPanelFX &&
+							selectedIds.isLastSelectionValid() &&
+							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
+					)
+			);
+		return filter;
+	}
+
+	public void enterMode(final ViewerPanelFX viewer)
+	{
+		assert this.activeViewer.get() == null;
+		activeViewer.set(viewer);
+		// ...
+	}
+
+	public void exitMode()
+	{
+		assert this.activeViewer.get() != null;
+		// ...
+		this.activeViewer.set(null);
+	}
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
index b25b315b2..64a5ad090 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
@@ -52,6 +52,7 @@
 import org.janelia.saalfeldlab.paintera.cache.global.GlobalCache;
 import org.janelia.saalfeldlab.paintera.composition.ARGBCompositeAlphaYCbCr;
 import org.janelia.saalfeldlab.paintera.composition.Composite;
+import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode;
 import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentOnlyLocal;
 import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentState;
 import org.janelia.saalfeldlab.paintera.control.lock.LockedSegmentsOnlyLocal;
@@ -124,6 +125,8 @@ public class LabelSourceState<D extends IntegerType<D>, T>
 
 	private final LabelSourceStateMergeDetachHandler mergeDetachHandler;
 
+	private final ShapeInterpolationMode shapeInterpolationMode;
+
 	private final ObjectProperty<FloodFillState> floodFillState = new SimpleObjectProperty<>();
 
 	private final HBox displayStatus;
@@ -152,6 +155,7 @@ public LabelSourceState(
 		this.paintHandler = new LabelSourceStatePaintHandler(selectedIds);
 		this.idSelectorHandler = new LabelSourceStateIdSelectorHandler(dataSource, selectedIds, assignment, lockedSegments);
 		this.mergeDetachHandler = new LabelSourceStateMergeDetachHandler(dataSource, selectedIds, assignment, idService);
+		this.shapeInterpolationMode = new ShapeInterpolationMode(selectedIds);
 		this.displayStatus = createDisplayStatus();
 		assignment.addListener(obs -> stain());
 		selectedIds.addListener(obs -> stain());
@@ -478,7 +482,10 @@ public EventHandler<Event> stateSpecificViewerEventHandler(PainteraBaseView pain
 	@Override
 	public EventHandler<Event> stateSpecificViewerEventFilter(PainteraBaseView paintera, KeyTracker keyTracker) {
 		LOG.info("Returning {}-specific filter", getClass().getSimpleName());
-		return paintHandler.viewerFilter(paintera, keyTracker);
+		final DelegateEventHandlers.ListDelegateEventHandler<Event> filter = DelegateEventHandlers.listHandler();
+		filter.addHandler(paintHandler.viewerFilter(paintera, keyTracker));
+		filter.addHandler(shapeInterpolationMode.modeHandler(paintera, keyTracker));
+		return filter;
 	}
 
 	@Override

From a5b7aa7c1ba5f8d267d430e1c13d13afec6f1855 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 17 May 2019 14:58:25 -0400
Subject: [PATCH 02/84] block user input in other viewers upon entering shape
 interpolation mode

---
 .../control/ShapeInterpolationMode.java         | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index b669ff417..3abf7feb8 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -11,6 +11,8 @@
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.event.Event;
 import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.Parent;
 import javafx.scene.input.KeyCode;
 import javafx.scene.input.KeyEvent;
 
@@ -50,13 +52,28 @@ public void enterMode(final ViewerPanelFX viewer)
 	{
 		assert this.activeViewer.get() == null;
 		activeViewer.set(viewer);
+		setDisableOtherViewers(true);
 		// ...
 	}
 
 	public void exitMode()
 	{
 		assert this.activeViewer.get() != null;
+		setDisableOtherViewers(false);
 		// ...
 		this.activeViewer.set(null);
 	}
+
+	private void setDisableOtherViewers(final boolean disable)
+	{
+		final Parent parent = this.activeViewer.get().getParent();
+		for (final Node child : parent.getChildrenUnmodifiable())
+		{
+			if (child instanceof ViewerPanelFX && child != this.activeViewer.get())
+			{
+				final ViewerPanelFX viewer = (ViewerPanelFX) child;
+				viewer.setDisable(disable);
+			}
+		}
+	}
 }

From 81fda9f85fd1ac328e75a8cb7d7c6fd76ad1be49 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 17 May 2019 15:26:36 -0400
Subject: [PATCH 03/84] add grayed out effect to other viewers in shape
 interpolation mode

---
 .../paintera/control/ShapeInterpolationMode.java    | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 3abf7feb8..bb37178dc 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -5,7 +5,6 @@
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
 import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
-
 import bdv.fx.viewer.ViewerPanelFX;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
@@ -13,6 +12,7 @@
 import javafx.event.EventHandler;
 import javafx.scene.Node;
 import javafx.scene.Parent;
+import javafx.scene.effect.ColorAdjust;
 import javafx.scene.input.KeyCode;
 import javafx.scene.input.KeyEvent;
 
@@ -73,6 +73,17 @@ private void setDisableOtherViewers(final boolean disable)
 			{
 				final ViewerPanelFX viewer = (ViewerPanelFX) child;
 				viewer.setDisable(disable);
+				if (disable)
+				{
+					final ColorAdjust grayedOutEffect = new ColorAdjust();
+					grayedOutEffect.setContrast(-0.2);
+					grayedOutEffect.setBrightness(-0.5);
+					viewer.setEffect(grayedOutEffect);
+				}
+				else
+				{
+					viewer.setEffect(null);
+				}
 			}
 		}
 	}

From 7f5e75e480d81aa5e44fe13bf4a43432798f2bd5 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 17 May 2019 15:32:54 -0400
Subject: [PATCH 04/84] add handler to exit shape interpolation mode

---
 .../paintera/control/ShapeInterpolationMode.java       | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index bb37178dc..c5ba974b8 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -41,10 +41,20 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 						"enter shape interpolation mode",
 						e -> enterMode((ViewerPanelFX) e.getTarget()),
 						e -> e.getTarget() instanceof ViewerPanelFX &&
+							this.activeViewer.get() == null &&
 							selectedIds.isLastSelectionValid() &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
 					)
 			);
+		filter.addEventHandler(
+				KeyEvent.KEY_PRESSED,
+				EventFX.KEY_PRESSED(
+						"exit shape interpolation mode",
+						e -> exitMode(),
+						e -> this.activeViewer.get() != null &&
+							keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE)
+					)
+			);
 		return filter;
 	}
 

From 81f354cd8967b24acf3ed18ad6e08e46ea19fd6b Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 17 May 2019 15:33:37 -0400
Subject: [PATCH 05/84] add basic logging for shape interpolation mode

---
 .../paintera/control/ShapeInterpolationMode.java         | 9 +++++++++
 .../saalfeldlab/paintera/state/LabelSourceState.java     | 2 ++
 2 files changed, 11 insertions(+)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index c5ba974b8..0b61e7643 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -1,10 +1,15 @@
 package org.janelia.saalfeldlab.paintera.control;
 
+import java.lang.invoke.MethodHandles;
+
 import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
 import org.janelia.saalfeldlab.fx.event.EventFX;
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
 import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import bdv.fx.viewer.ViewerPanelFX;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
@@ -18,6 +23,8 @@
 
 public class ShapeInterpolationMode
 {
+	private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
 	private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>();
 
 	private final SelectedIds selectedIds;
@@ -60,6 +67,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 
 	public void enterMode(final ViewerPanelFX viewer)
 	{
+		LOG.info("Entering shape interpolation mode");
 		assert this.activeViewer.get() == null;
 		activeViewer.set(viewer);
 		setDisableOtherViewers(true);
@@ -68,6 +76,7 @@ public void enterMode(final ViewerPanelFX viewer)
 
 	public void exitMode()
 	{
+		LOG.info("Exiting shape interpolation mode");
 		assert this.activeViewer.get() != null;
 		setDisableOtherViewers(false);
 		// ...
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
index 64a5ad090..dd283cbd8 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
@@ -472,6 +472,7 @@ public EventHandler<Event> stateSpecificGlobalEventHandler(PainteraBaseView pain
 	@Override
 	public EventHandler<Event> stateSpecificViewerEventHandler(PainteraBaseView paintera, KeyTracker keyTracker) {
 		LOG.info("Returning {}-specific handler", getClass().getSimpleName());
+		LOG.debug("Returning {}-specific handler", getClass().getSimpleName());
 		final DelegateEventHandlers.ListDelegateEventHandler<Event> handler = DelegateEventHandlers.listHandler();
 		handler.addHandler(paintHandler.viewerHandler(paintera, keyTracker));
 		handler.addHandler(idSelectorHandler.viewerHandler(paintera, keyTracker));
@@ -482,6 +483,7 @@ public EventHandler<Event> stateSpecificViewerEventHandler(PainteraBaseView pain
 	@Override
 	public EventHandler<Event> stateSpecificViewerEventFilter(PainteraBaseView paintera, KeyTracker keyTracker) {
 		LOG.info("Returning {}-specific filter", getClass().getSimpleName());
+		LOG.debug("Returning {}-specific filter", getClass().getSimpleName());
 		final DelegateEventHandlers.ListDelegateEventHandler<Event> filter = DelegateEventHandlers.listHandler();
 		filter.addHandler(paintHandler.viewerFilter(paintera, keyTracker));
 		filter.addHandler(shapeInterpolationMode.modeHandler(paintera, keyTracker));

From 6b00ba1b406d66a287cca2284482b1c1ef1894ef Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 17 May 2019 16:57:24 -0400
Subject: [PATCH 06/84] add basic structure for specifying allowed UI actions

---
 .../paintera/PainteraBaseView.java            | 15 ++++++
 .../control/actions/AllowedActions.java       | 47 +++++++++++++++++++
 .../control/actions/NavigationAction.java     | 26 ++++++++++
 .../paintera/control/actions/PaintAction.java | 28 +++++++++++
 .../control/actions/SelectIdAction.java       | 26 ++++++++++
 5 files changed, 142 insertions(+)
 create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java
 create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java
 create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java
 create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java
index 73df8cba9..9edf5c31d 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java
@@ -4,6 +4,7 @@
 import bdv.viewer.SourceAndConverter;
 import bdv.viewer.ViewerOptions;
 import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
 import javafx.collections.ListChangeListener;
 import javafx.collections.ObservableList;
 import javafx.event.Event;
@@ -37,6 +38,7 @@
 import org.janelia.saalfeldlab.paintera.config.OrthoSliceConfig;
 import org.janelia.saalfeldlab.paintera.config.OrthoSliceConfigBase;
 import org.janelia.saalfeldlab.paintera.config.Viewer3DConfig;
+import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions;
 import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder;
 import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrderNotSupported;
 import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
@@ -72,6 +74,7 @@
  * <li>{@link SourceInfo source state management}</li>
  * <li>{@link GlobalCache global cache management}</li>
  * <li>{@link ExecutorService thread management} for number crunching</li>
+ * <li>{@link AllowedActions UI mode}</li>
  * </ul><p>
  */
 public class PainteraBaseView
@@ -101,6 +104,8 @@ public class PainteraBaseView
 
 	private final OrthogonalViews<Viewer3DFX> views;
 
+	private final ObjectProperty<AllowedActions> allowedActionsProperty;
+
 	private final ObservableList<SourceAndConverter<?>> visibleSourcesAndConverters = sourceInfo
 			.trackVisibleSourcesAndConverters();
 
@@ -157,6 +162,7 @@ public PainteraBaseView(
 				s -> Optional.ofNullable(sourceInfo.getState(s)).map(SourceState::interpolationProperty).map(ObjectProperty::get).orElse(Interpolation.NLINEAR),
 				s -> Optional.ofNullable(sourceInfo.getState(s)).map(SourceState::getAxisOrder).orElse(null)
 		);
+		this.allowedActionsProperty = new SimpleObjectProperty<>(AllowedActions.all());
 		this.vsacUpdate = change -> views.setAllSources(visibleSourcesAndConverters);
 		visibleSourcesAndConverters.addListener(vsacUpdate);
 		LOG.debug("Meshes group={}", viewer3D.meshesGroup());
@@ -207,6 +213,15 @@ public GlobalTransformManager manager()
 		return this.manager;
 	}
 
+	/**
+	 *
+	 * @return {@link AllowedActions} that describe the user interface in the current application mode
+	 */
+	public ObjectProperty<AllowedActions> allowedActionsProperty()
+	{
+		return this.allowedActionsProperty;
+	}
+
 	/**
 	 *
 	 * Add a source and state to the 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
new file mode 100644
index 000000000..b16d66f45
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java
@@ -0,0 +1,47 @@
+package org.janelia.saalfeldlab.paintera.control.actions;
+
+import java.util.EnumSet;
+
+/**
+ * Describes what actions in the UI are allowed in the current application mode.
+ */
+public final class AllowedActions
+{
+	private final EnumSet<NavigationAction> navigationAllowedActions;
+	private final EnumSet<SelectIdAction> selectIdAllowedActions;
+	private final EnumSet<PaintAction> paintAllowedActions;
+
+	public AllowedActions(
+			final EnumSet<NavigationAction> navigationAllowedActions,
+			final EnumSet<SelectIdAction> selectIdAllowedActions,
+			final EnumSet<PaintAction> paintAllowedActions)
+	{
+		this.navigationAllowedActions = navigationAllowedActions;
+		this.selectIdAllowedActions = selectIdAllowedActions;
+		this.paintAllowedActions = paintAllowedActions;
+	}
+
+	public boolean isAllowed(final NavigationAction navigationAction)
+	{
+		return this.navigationAllowedActions.contains(navigationAction);
+	}
+
+	public boolean isAllowed(final SelectIdAction selectIdAction)
+	{
+		return this.selectIdAllowedActions.contains(selectIdAction);
+	}
+
+	public boolean isAllowed(final PaintAction paintAction)
+	{
+		return this.paintAllowedActions.contains(paintAction);
+	}
+
+	public static AllowedActions all()
+	{
+		return new AllowedActions(
+			NavigationAction.all(),
+			SelectIdAction.all(),
+			PaintAction.all()
+		);
+	}
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java
new file mode 100644
index 000000000..57788a4e8
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java
@@ -0,0 +1,26 @@
+package org.janelia.saalfeldlab.paintera.control.actions;
+
+import java.util.EnumSet;
+
+public enum NavigationAction
+{
+	Drag,
+	Scroll,
+	Zoom,
+	Rotate;
+
+	public static EnumSet<NavigationAction> of(final NavigationAction first, final NavigationAction... rest)
+	{
+		return EnumSet.of(first, rest);
+	}
+
+	public static EnumSet<NavigationAction> all()
+	{
+		return EnumSet.allOf(NavigationAction.class);
+	}
+
+	public static EnumSet<NavigationAction> none()
+	{
+		return EnumSet.noneOf(NavigationAction.class);
+	}
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java
new file mode 100644
index 000000000..dc0436c7a
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java
@@ -0,0 +1,28 @@
+package org.janelia.saalfeldlab.paintera.control.actions;
+
+import java.util.EnumSet;
+
+public enum PaintAction
+{
+	Paint,
+	Erase,
+	Background,
+	Fill,
+	Restrict,
+	SetBrush;
+
+	public static EnumSet<PaintAction> of(final PaintAction first, final PaintAction... rest)
+	{
+		return EnumSet.of(first, rest);
+	}
+
+	public static EnumSet<PaintAction> all()
+	{
+		return EnumSet.allOf(PaintAction.class);
+	}
+
+	public static EnumSet<PaintAction> none()
+	{
+		return EnumSet.noneOf(PaintAction.class);
+	}
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java
new file mode 100644
index 000000000..06d0e826e
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java
@@ -0,0 +1,26 @@
+package org.janelia.saalfeldlab.paintera.control.actions;
+
+import java.util.EnumSet;
+
+public enum SelectIdAction
+{
+	Toggle,
+	Append,
+	Create,
+	Lock;
+
+	public static EnumSet<SelectIdAction> of(final SelectIdAction first, final SelectIdAction... rest)
+	{
+		return EnumSet.of(first, rest);
+	}
+
+	public static EnumSet<SelectIdAction> all()
+	{
+		return EnumSet.allOf(SelectIdAction.class);
+	}
+
+	public static EnumSet<SelectIdAction> none()
+	{
+		return EnumSet.noneOf(SelectIdAction.class);
+	}
+}

From 68eedbb6362a24786b0a1055d3df0ed1309d3fb6 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 17 May 2019 16:58:22 -0400
Subject: [PATCH 07/84] set appropriate allowed UI actions while in shape
 interpolation mode

---
 .../control/ShapeInterpolationMode.java       | 34 ++++++++++++++++---
 1 file changed, 29 insertions(+), 5 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 0b61e7643..2c82550f7 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -1,11 +1,14 @@
 package org.janelia.saalfeldlab.paintera.control;
 
 import java.lang.invoke.MethodHandles;
-
 import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
 import org.janelia.saalfeldlab.fx.event.EventFX;
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
+import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions;
+import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction;
+import org.janelia.saalfeldlab.paintera.control.actions.PaintAction;
+import org.janelia.saalfeldlab.paintera.control.actions.SelectIdAction;
 import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -25,10 +28,22 @@ public class ShapeInterpolationMode
 {
 	private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
+	private static final AllowedActions allowedActionsInShapeInterpolationMode;
+	static
+	{
+		allowedActionsInShapeInterpolationMode = new AllowedActions(
+			NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll),
+			SelectIdAction.none(),
+			PaintAction.of(PaintAction.Paint)
+		);
+	}
+
 	private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>();
 
 	private final SelectedIds selectedIds;
 
+	private AllowedActions lastAllowedActions;
+
 	public ShapeInterpolationMode(final SelectedIds selectedIds)
 	{
 		this.selectedIds = selectedIds;
@@ -46,7 +61,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				KeyEvent.KEY_PRESSED,
 				EventFX.KEY_PRESSED(
 						"enter shape interpolation mode",
-						e -> enterMode((ViewerPanelFX) e.getTarget()),
+						e -> enterMode(paintera, (ViewerPanelFX) e.getTarget()),
 						e -> e.getTarget() instanceof ViewerPanelFX &&
 							this.activeViewer.get() == null &&
 							selectedIds.isLastSelectionValid() &&
@@ -57,7 +72,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				KeyEvent.KEY_PRESSED,
 				EventFX.KEY_PRESSED(
 						"exit shape interpolation mode",
-						e -> exitMode(),
+						e -> exitMode(paintera),
 						e -> this.activeViewer.get() != null &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE)
 					)
@@ -65,21 +80,30 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 		return filter;
 	}
 
-	public void enterMode(final ViewerPanelFX viewer)
+	public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewer)
 	{
 		LOG.info("Entering shape interpolation mode");
 		assert this.activeViewer.get() == null;
 		activeViewer.set(viewer);
 		setDisableOtherViewers(true);
+
+		this.lastAllowedActions = paintera.allowedActionsProperty().get();
+		paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationMode);
+
 		// ...
 	}
 
-	public void exitMode()
+	public void exitMode(final PainteraBaseView paintera)
 	{
 		LOG.info("Exiting shape interpolation mode");
 		assert this.activeViewer.get() != null;
 		setDisableOtherViewers(false);
+
+		paintera.allowedActionsProperty().set(this.lastAllowedActions);
+		this.lastAllowedActions = null;
+
 		// ...
+
 		this.activeViewer.set(null);
 	}
 

From cf90564f23950c74b1927c2bd365ac4c693177a3 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 17 May 2019 18:11:51 -0400
Subject: [PATCH 08/84] check if label selection actions are allowed

---
 .../control/actions/SelectIdAction.java         |  2 +-
 .../LabelSourceStateIdSelectorHandler.java      | 17 +++++++++++++----
 .../state/LabelSourceStatePaintHandler.java     |  7 -------
 3 files changed, 14 insertions(+), 12 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java
index 06d0e826e..a92f0dd5f 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java
@@ -6,7 +6,7 @@ public enum SelectIdAction
 {
 	Toggle,
 	Append,
-	Create,
+	CreateNewLabel,
 	Lock;
 
 	public static EnumSet<SelectIdAction> of(final SelectIdAction first, final SelectIdAction... rest)
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java
index c280a4bfd..cf8504e3b 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java
@@ -13,8 +13,10 @@
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
 import org.janelia.saalfeldlab.paintera.control.IdSelector;
+import org.janelia.saalfeldlab.paintera.control.actions.SelectIdAction;
 import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment;
 import org.janelia.saalfeldlab.paintera.control.lock.LockedSegments;
+import org.janelia.saalfeldlab.paintera.control.paint.SelectNextId;
 import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
 import org.janelia.saalfeldlab.paintera.data.DataSource;
 import org.slf4j.Logger;
@@ -66,20 +68,27 @@ public EventHandler<Event> viewerHandler(final PainteraBaseView paintera, final
 		};
 	}
 
-	private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker keyTracker, ViewerPanelFX vp) {
+	private EventHandler<Event> makeHandler(final PainteraBaseView paintera, final KeyTracker keyTracker, final ViewerPanelFX vp) {
 		final IdSelector selector = new IdSelector(source, selectedIds, vp);
 		final DelegateEventHandlers.AnyHandler handler = DelegateEventHandlers.handleAny();
 		// TODO event handlers should probably not be on ANY/RELEASED but on PRESSED
 		handler.addEventHandler(MouseEvent.ANY, selector.selectFragmentWithMaximumCount(
 				"toggle single id",
-				event -> event.isPrimaryButtonDown() && keyTracker.noKeysActive()).handler());
+				event -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.Toggle) && event.isPrimaryButtonDown() && keyTracker.noKeysActive()).handler());
 		handler.addEventHandler(MouseEvent.ANY, selector.appendFragmentWithMaximumCount(
 				"append id",
-				event -> event.isSecondaryButtonDown() && keyTracker.noKeysActive()).handler());
+				event -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.Append) && event.isSecondaryButtonDown() && keyTracker.noKeysActive()).handler());
 		handler.addOnKeyPressed(EventFX.KEY_PRESSED(
 				"lock segment",
 				e -> selector.toggleLock(selectedIds, assignment, lockedSegments),
-				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.L)));
+				e -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.Lock) && keyTracker.areOnlyTheseKeysDown(KeyCode.L)));
+
+		final SourceInfo sourceInfo = paintera.sourceInfo();
+		final SelectNextId nextId = new SelectNextId(sourceInfo);
+		handler.addOnKeyPressed(EventFX.KEY_PRESSED(
+				"next id",
+				event -> nextId.getNextId(),
+				event -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.CreateNewLabel) && keyTracker.areOnlyTheseKeysDown(KeyCode.N)));
 		return handler;
 	}
 }
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java
index d5219484a..31094ba57 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java
@@ -24,7 +24,6 @@
 import org.janelia.saalfeldlab.paintera.control.paint.PaintActions2D;
 import org.janelia.saalfeldlab.paintera.control.paint.PaintClickOrDrag;
 import org.janelia.saalfeldlab.paintera.control.paint.RestrictPainting;
-import org.janelia.saalfeldlab.paintera.control.paint.SelectNextId;
 import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -217,12 +216,6 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke
 						KeyCode.SHIFT,
 						KeyCode.R)));
 
-		final SelectNextId nextId = new SelectNextId(sourceInfo);
-		handler.addOnKeyPressed(EventFX.KEY_PRESSED(
-				"next id",
-				event -> nextId.getNextId(),
-				event -> keyTracker.areOnlyTheseKeysDown(KeyCode.N)));
-
 		return handler;
 
 	}

From 23e6affb970281750144e2423960b1070a92ec7e Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 17 May 2019 18:28:43 -0400
Subject: [PATCH 09/84] check if navigation actions are allowed

---
 .../paintera/PainteraDefaultHandlers.java     |  3 +-
 .../paintera/control/Navigation.java          | 69 +++++++++++--------
 2 files changed, 41 insertions(+), 31 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
index 573461c47..46037f7b4 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
@@ -178,7 +178,8 @@ public PainteraDefaultHandlers(
 				baseView.manager(),
 				v -> viewerToTransforms.get(v).displayTransform(),
 				v -> viewerToTransforms.get(v).globalToViewerTransform(),
-				keyTracker
+				keyTracker,
+				baseView.allowedActionsProperty()
 		);
 
 		this.onEnterOnExit = createOnEnterOnExit(paneWithStatus.currentFocusHolder());
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java
index 82c206bb1..43bc865a5 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java
@@ -33,6 +33,8 @@
 import org.janelia.saalfeldlab.fx.event.InstallAndRemove;
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.fx.event.MouseDragFX;
+import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions;
+import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction;
 import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners;
 import org.janelia.saalfeldlab.paintera.control.navigation.ButtonRotationSpeedConfig;
 import org.janelia.saalfeldlab.paintera.control.navigation.KeyRotate;
@@ -62,6 +64,8 @@ public class Navigation implements ToOnEnterOnExit
 
 	private final KeyTracker keyTracker;
 
+	private final ObjectProperty<AllowedActions> allowedActionsProperty;
+
 	private final HashMap<ViewerPanelFX, Collection<InstallAndRemove<Node>>> mouseAndKeyHandlers = new HashMap<>();
 
 	private final Function<ViewerPanelFX, AffineTransformWithListeners> displayTransform;
@@ -72,13 +76,15 @@ public Navigation(
 			final GlobalTransformManager manager,
 			final Function<ViewerPanelFX, AffineTransformWithListeners> displayTransform,
 			final Function<ViewerPanelFX, AffineTransformWithListeners> globalToViewerTransform,
-			final KeyTracker keyTracker)
+			final KeyTracker keyTracker,
+			final ObjectProperty<AllowedActions> allowedActionsProperty)
 	{
 		super();
 		this.manager = manager;
 		this.displayTransform = displayTransform;
 		this.globalToViewerTransform = globalToViewerTransform;
 		this.keyTracker = keyTracker;
+		this.allowedActionsProperty = allowedActionsProperty;
 	}
 
 	@Override
@@ -160,55 +166,55 @@ public Consumer<ViewerPanelFX> getOnEnter()
 				iars.add(EventFX.SCROLL(
 						"translate along normal",
 						e -> scrollDefault.scroll(-ControlUtils.getBiggestScroll(e)),
-						event -> keyTracker.noKeysActive()
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.noKeysActive()
 				                       ));
 				iars.add(EventFX.SCROLL(
 						"translate along normal fast",
 						e -> scrollFast.scroll(-ControlUtils.getBiggestScroll(e)),
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)
 				                       ));
 				iars.add(EventFX.SCROLL(
 						"translate along normal slow",
 						e -> scrollSlow.scroll(-ControlUtils.getBiggestScroll(e)),
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL)
 				                       ));
 
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal bck",
 						e -> scrollDefault.scroll(+1),
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA)
 				                            ));
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal fwd",
 						e -> scrollDefault.scroll(-1),
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD)
 				                            ));
 
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal fast bck",
 						e -> scrollFast.scroll(+1),
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.SHIFT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.SHIFT)
 				                            ));
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal fast fwd",
 						e -> scrollFast.scroll(-1),
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.SHIFT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.SHIFT)
 				                            ));
 
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal slow bck",
 						e -> scrollSlow.scroll(+1),
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.CONTROL)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.CONTROL)
 				                            ));
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal slow fwd",
 						e -> scrollSlow.scroll(-1),
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.CONTROL)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.CONTROL)
 				                            ));
 
 				iars.add(MouseDragFX.createDrag(
 						"translate xy",
-						e -> e.isSecondaryButtonDown() && keyTracker.noKeysActive(),
+						e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Drag) && e.isSecondaryButtonDown() && keyTracker.noKeysActive(),
 						true,
 						manager,
 						e -> translateXY.init(),
@@ -220,8 +226,9 @@ public Consumer<ViewerPanelFX> getOnEnter()
 				iars.add(EventFX.SCROLL(
 						"zoom",
 						event -> zoom.zoomCenteredAt(-ControlUtils.getBiggestScroll(event), event.getX(), event.getY()),
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.META) ||
-								keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Zoom) &&
+								(keyTracker.areOnlyTheseKeysDown(KeyCode.META) ||
+								keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT))
 				                       ));
 
 				iars.add(EventFX.KEY_PRESSED(
@@ -231,9 +238,10 @@ public Consumer<ViewerPanelFX> getOnEnter()
 								mouseXIfInsideElseCenterX.get(),
 								mouseYIfInsideElseCenterY.get()
 						                            ),
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.MINUS)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Zoom) &&
+								(keyTracker.areOnlyTheseKeysDown(KeyCode.MINUS)
 								|| keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.MINUS)
-								|| keyTracker.areOnlyTheseKeysDown(KeyCode.DOWN)
+								|| keyTracker.areOnlyTheseKeysDown(KeyCode.DOWN))
 				                            ));
 
 				iars.add(EventFX.KEY_PRESSED(
@@ -243,9 +251,10 @@ public Consumer<ViewerPanelFX> getOnEnter()
 								mouseXIfInsideElseCenterX.get(),
 								mouseYIfInsideElseCenterY.get()
 						                            ),
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.EQUALS)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Zoom) &&
+								(keyTracker.areOnlyTheseKeysDown(KeyCode.EQUALS)
 								|| keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.EQUALS)
-								|| keyTracker.areOnlyTheseKeysDown(KeyCode.UP)
+								|| keyTracker.areOnlyTheseKeysDown(KeyCode.UP))
 				                            ));
 
 				iars.add(rotationHandler(
@@ -257,7 +266,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalToViewerTransform,
 						manager::setTransform,
 						manager,
-						event -> keyTracker.noKeysActive() && event.getButton().equals(MouseButton.PRIMARY)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.noKeysActive() && event.getButton().equals(MouseButton.PRIMARY)
 				                        ));
 
 				iars.add(rotationHandler(
@@ -269,7 +278,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalToViewerTransform,
 						manager::setTransform,
 						manager,
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT) && event.getButton().equals
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT) && event.getButton().equals
 								(MouseButton.PRIMARY)
 				                        ));
 
@@ -282,7 +291,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalToViewerTransform,
 						manager::setTransform,
 						manager,
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL) && event.getButton().equals(
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL) && event.getButton().equals(
 								MouseButton.PRIMARY)
 				                        ));
 
@@ -290,17 +299,17 @@ public Consumer<ViewerPanelFX> getOnEnter()
 				iars.add(EventFX.KEY_PRESSED(
 						"set key rotation axis x",
 						e -> keyRotationAxis.set(KeyRotate.Axis.X),
-						e -> keyTracker.areOnlyTheseKeysDown(KeyCode.X)
+						e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.X)
 				                            ));
 				iars.add(EventFX.KEY_PRESSED(
 						"set key rotation axis y",
 						e -> keyRotationAxis.set(KeyRotate.Axis.Y),
-						e -> keyTracker.areOnlyTheseKeysDown(KeyCode.Y)
+						e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.Y)
 				                            ));
 				iars.add(EventFX.KEY_PRESSED(
 						"set key rotation axis z",
 						e -> keyRotationAxis.set(KeyRotate.Axis.Z),
-						e -> keyTracker.areOnlyTheseKeysDown(KeyCode.Z)
+						e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.Z)
 				                            ));
 
 				iars.add(keyRotationHandler(
@@ -315,7 +324,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalTransform,
 						manager::setTransform,
 						manager,
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.LEFT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.LEFT)
 				                           ));
 
 				iars.add(keyRotationHandler(
@@ -330,7 +339,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalTransform,
 						manager::setTransform,
 						manager,
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.LEFT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.LEFT)
 				                           ));
 
 				iars.add(keyRotationHandler(
@@ -345,7 +354,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalTransform,
 						manager::setTransform,
 						manager,
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.LEFT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.LEFT)
 				                           ));
 
 				iars.add(keyRotationHandler(
@@ -360,7 +369,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalTransform,
 						manager::setTransform,
 						manager,
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.RIGHT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.RIGHT)
 				                           ));
 
 				iars.add(keyRotationHandler(
@@ -375,7 +384,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalTransform,
 						manager::setTransform,
 						manager,
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.RIGHT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.RIGHT)
 				                           ));
 
 				iars.add(keyRotationHandler(
@@ -390,7 +399,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalTransform,
 						manager::setTransform,
 						manager,
-						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.RIGHT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.RIGHT)
 				                           ));
 
 				final RemoveRotation removeRotation = new RemoveRotation(
@@ -405,7 +414,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 								mouseXIfInsideElseCenterX.get(),
 								mouseYIfInsideElseCenterY.get()
 						                                            ),
-						e -> keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.Z)
+						e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.Z)
 				                            ));
 
 				this.mouseAndKeyHandlers.put(t, iars);

From 1aa05a82fcc8d56319dd9612d18e52ca1d8329a9 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 17 May 2019 18:43:22 -0400
Subject: [PATCH 10/84] check if merge/split actions are allowed

---
 .../control/ShapeInterpolationMode.java       |  4 +--
 .../control/actions/AllowedActions.java       | 12 ++++----
 .../paintera/control/actions/LabelAction.java | 28 +++++++++++++++++++
 .../control/actions/SelectIdAction.java       | 26 -----------------
 .../LabelSourceStateIdSelectorHandler.java    | 10 +++----
 .../LabelSourceStateMergeDetachHandler.java   |  7 +++--
 6 files changed, 45 insertions(+), 42 deletions(-)
 create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java
 delete mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 2c82550f7..2aa5a53b0 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -6,9 +6,9 @@
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
 import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions;
+import org.janelia.saalfeldlab.paintera.control.actions.LabelAction;
 import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction;
 import org.janelia.saalfeldlab.paintera.control.actions.PaintAction;
-import org.janelia.saalfeldlab.paintera.control.actions.SelectIdAction;
 import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -33,7 +33,7 @@ public class ShapeInterpolationMode
 	{
 		allowedActionsInShapeInterpolationMode = new AllowedActions(
 			NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll),
-			SelectIdAction.none(),
+			LabelAction.none(),
 			PaintAction.of(PaintAction.Paint)
 		);
 	}
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 b16d66f45..9a1d899aa 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
@@ -8,16 +8,16 @@
 public final class AllowedActions
 {
 	private final EnumSet<NavigationAction> navigationAllowedActions;
-	private final EnumSet<SelectIdAction> selectIdAllowedActions;
+	private final EnumSet<LabelAction> labelAllowedActions;
 	private final EnumSet<PaintAction> paintAllowedActions;
 
 	public AllowedActions(
 			final EnumSet<NavigationAction> navigationAllowedActions,
-			final EnumSet<SelectIdAction> selectIdAllowedActions,
+			final EnumSet<LabelAction> labelAllowedActions,
 			final EnumSet<PaintAction> paintAllowedActions)
 	{
 		this.navigationAllowedActions = navigationAllowedActions;
-		this.selectIdAllowedActions = selectIdAllowedActions;
+		this.labelAllowedActions = labelAllowedActions;
 		this.paintAllowedActions = paintAllowedActions;
 	}
 
@@ -26,9 +26,9 @@ public boolean isAllowed(final NavigationAction navigationAction)
 		return this.navigationAllowedActions.contains(navigationAction);
 	}
 
-	public boolean isAllowed(final SelectIdAction selectIdAction)
+	public boolean isAllowed(final LabelAction labelAction)
 	{
-		return this.selectIdAllowedActions.contains(selectIdAction);
+		return this.labelAllowedActions.contains(labelAction);
 	}
 
 	public boolean isAllowed(final PaintAction paintAction)
@@ -40,7 +40,7 @@ public static AllowedActions all()
 	{
 		return new AllowedActions(
 			NavigationAction.all(),
-			SelectIdAction.all(),
+			LabelAction.all(),
 			PaintAction.all()
 		);
 	}
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java
new file mode 100644
index 000000000..190d5ba4b
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java
@@ -0,0 +1,28 @@
+package org.janelia.saalfeldlab.paintera.control.actions;
+
+import java.util.EnumSet;
+
+public enum LabelAction
+{
+	Toggle,
+	Append,
+	CreateNew,
+	Lock,
+	Merge,
+	Split;
+
+	public static EnumSet<LabelAction> of(final LabelAction first, final LabelAction... rest)
+	{
+		return EnumSet.of(first, rest);
+	}
+
+	public static EnumSet<LabelAction> all()
+	{
+		return EnumSet.allOf(LabelAction.class);
+	}
+
+	public static EnumSet<LabelAction> none()
+	{
+		return EnumSet.noneOf(LabelAction.class);
+	}
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java
deleted file mode 100644
index a92f0dd5f..000000000
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.janelia.saalfeldlab.paintera.control.actions;
-
-import java.util.EnumSet;
-
-public enum SelectIdAction
-{
-	Toggle,
-	Append,
-	CreateNewLabel,
-	Lock;
-
-	public static EnumSet<SelectIdAction> of(final SelectIdAction first, final SelectIdAction... rest)
-	{
-		return EnumSet.of(first, rest);
-	}
-
-	public static EnumSet<SelectIdAction> all()
-	{
-		return EnumSet.allOf(SelectIdAction.class);
-	}
-
-	public static EnumSet<SelectIdAction> none()
-	{
-		return EnumSet.noneOf(SelectIdAction.class);
-	}
-}
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java
index cf8504e3b..ca70893be 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java
@@ -13,7 +13,7 @@
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
 import org.janelia.saalfeldlab.paintera.control.IdSelector;
-import org.janelia.saalfeldlab.paintera.control.actions.SelectIdAction;
+import org.janelia.saalfeldlab.paintera.control.actions.LabelAction;
 import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment;
 import org.janelia.saalfeldlab.paintera.control.lock.LockedSegments;
 import org.janelia.saalfeldlab.paintera.control.paint.SelectNextId;
@@ -74,21 +74,21 @@ private EventHandler<Event> makeHandler(final PainteraBaseView paintera, final K
 		// TODO event handlers should probably not be on ANY/RELEASED but on PRESSED
 		handler.addEventHandler(MouseEvent.ANY, selector.selectFragmentWithMaximumCount(
 				"toggle single id",
-				event -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.Toggle) && event.isPrimaryButtonDown() && keyTracker.noKeysActive()).handler());
+				event -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Toggle) && event.isPrimaryButtonDown() && keyTracker.noKeysActive()).handler());
 		handler.addEventHandler(MouseEvent.ANY, selector.appendFragmentWithMaximumCount(
 				"append id",
-				event -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.Append) && event.isSecondaryButtonDown() && keyTracker.noKeysActive()).handler());
+				event -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Append) && event.isSecondaryButtonDown() && keyTracker.noKeysActive()).handler());
 		handler.addOnKeyPressed(EventFX.KEY_PRESSED(
 				"lock segment",
 				e -> selector.toggleLock(selectedIds, assignment, lockedSegments),
-				e -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.Lock) && keyTracker.areOnlyTheseKeysDown(KeyCode.L)));
+				e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Lock) && keyTracker.areOnlyTheseKeysDown(KeyCode.L)));
 
 		final SourceInfo sourceInfo = paintera.sourceInfo();
 		final SelectNextId nextId = new SelectNextId(sourceInfo);
 		handler.addOnKeyPressed(EventFX.KEY_PRESSED(
 				"next id",
 				event -> nextId.getNextId(),
-				event -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.CreateNewLabel) && keyTracker.areOnlyTheseKeysDown(KeyCode.N)));
+				event -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.CreateNew) && keyTracker.areOnlyTheseKeysDown(KeyCode.N)));
 		return handler;
 	}
 }
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java
index 4f7608147..4c968b18e 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java
@@ -28,6 +28,7 @@
 import org.janelia.saalfeldlab.fx.event.EventFX;
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
+import org.janelia.saalfeldlab.paintera.control.actions.LabelAction;
 import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment;
 import org.janelia.saalfeldlab.paintera.control.assignment.action.AssignmentAction;
 import org.janelia.saalfeldlab.paintera.control.assignment.action.Detach;
@@ -92,15 +93,15 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke
 		handler.addOnMousePressed(EventFX.MOUSE_PRESSED(
 				"merge fragments",
 				new MergeFragments(vp),
-				e -> e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)));
+				e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Merge) && e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)));
 		handler.addOnMousePressed(EventFX.MOUSE_PRESSED(
 				"detach fragment",
 				new DetachFragment(vp),
-				e -> e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)));
+				e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Split) && e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)));
 		handler.addOnMousePressed(EventFX.MOUSE_PRESSED(
 				"detach fragment",
 				new ConfirmSelection(vp),
-				e -> e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.CONTROL)));
+				e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Split) && e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.CONTROL)));
 		return handler;
 	}
 

From 0519dcf7db7e08c1e25710da5c4f1b940f77de34 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 17 May 2019 19:02:01 -0400
Subject: [PATCH 11/84] check if paint actions are allowed

---
 .../control/ShapeInterpolationMode.java       |  2 +-
 .../state/LabelSourceStatePaintHandler.java   | 40 ++++++++++---------
 2 files changed, 22 insertions(+), 20 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 2aa5a53b0..dc3673b99 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -34,7 +34,7 @@ public class ShapeInterpolationMode
 		allowedActionsInShapeInterpolationMode = new AllowedActions(
 			NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll),
 			LabelAction.none(),
-			PaintAction.of(PaintAction.Paint)
+			PaintAction.of(PaintAction.Paint, PaintAction.SetBrush)
 		);
 	}
 
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java
index 31094ba57..483a60dfa 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java
@@ -17,6 +17,7 @@
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
 import org.janelia.saalfeldlab.paintera.control.ControlUtils;
+import org.janelia.saalfeldlab.paintera.control.actions.PaintAction;
 import org.janelia.saalfeldlab.paintera.control.paint.Fill2DOverlay;
 import org.janelia.saalfeldlab.paintera.control.paint.FillOverlay;
 import org.janelia.saalfeldlab.paintera.control.paint.FloodFill;
@@ -124,47 +125,48 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke
 		handler.addEventHandler(KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED(
 				"show brush overlay",
 				event -> {LOG.trace("Showing brush overlay!"); paint2D.showBrushOverlay();},
-				event -> keyTracker.areKeysDown(KeyCode.SPACE)));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Paint) && keyTracker.areKeysDown(KeyCode.SPACE)));
 
 		handler.addEventHandler(KeyEvent.KEY_RELEASED, EventFX.KEY_RELEASED(
 				"hide brush overlay",
 				event -> {LOG.trace("Hiding brush overlay!"); paint2D.hideBrushOverlay();},
-				event -> event.getCode().equals(KeyCode.SPACE) && !keyTracker.areKeysDown(KeyCode.SPACE)));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Paint) && event.getCode().equals(KeyCode.SPACE) && !keyTracker.areKeysDown(KeyCode.SPACE)));
 
 		handler.addOnScroll(EventFX.SCROLL(
 				"change brush size",
 				event -> paint2D.changeBrushRadius(event.getDeltaY()),
-				event -> keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE)));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.SetBrush) && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE)));
 
 		handler.addOnScroll(EventFX.SCROLL(
 				"change brush depth",
 				event -> paint2D.changeBrushDepth(-ControlUtils.getBiggestScroll(event)),
-				event -> keyTracker.areOnlyTheseKeysDown(
-						KeyCode.SPACE,
-						KeyCode.SHIFT
-				) || keyTracker.areOnlyTheseKeysDown(KeyCode.F) ||
-						keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT,KeyCode.F)));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.SetBrush) &&
+					(keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE, KeyCode.SHIFT) ||
+					keyTracker.areOnlyTheseKeysDown(KeyCode.F) ||
+					keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT,KeyCode.F))));
 
 		handler.addOnKeyPressed(EventFX.KEY_PRESSED("show fill 2D overlay", event -> {
 			fill2DOverlay.setVisible(true);
 			fillOverlay.setVisible(false);
-		}, event -> keyTracker.areOnlyTheseKeysDown(KeyCode.F)));
+		}, event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && keyTracker.areOnlyTheseKeysDown(KeyCode.F)));
 
 		handler.addOnKeyReleased(EventFX.KEY_RELEASED(
 				"show fill 2D overlay",
 				event -> fill2DOverlay.setVisible(false),
-				event -> event.getCode().equals(KeyCode.F) && keyTracker.noKeysActive()));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && event.getCode().equals(KeyCode.F) && keyTracker.noKeysActive()));
 
 		handler.addOnKeyPressed(EventFX.KEY_PRESSED("show fill overlay", event -> {
 			fillOverlay.setVisible(true);
 			fill2DOverlay.setVisible(false);
-		}, event -> keyTracker.areOnlyTheseKeysDown(KeyCode.F, KeyCode.SHIFT)));
+		}, event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && keyTracker.areOnlyTheseKeysDown(KeyCode.F, KeyCode.SHIFT)));
 
 		handler.addOnKeyReleased(EventFX.KEY_RELEASED(
 				"show fill overlay",
 				event -> fillOverlay.setVisible(false),
-				event -> event.getCode().equals(KeyCode.F) && keyTracker.areOnlyTheseKeysDown(KeyCode
-						.SHIFT) || event.isShiftDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.F)));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) &&
+					((event.getCode().equals(KeyCode.F) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)) ||
+					(event.getCode().equals(KeyCode.SHIFT) && keyTracker.areOnlyTheseKeysDown(KeyCode.F))
+			)));
 
 		// paint
 		final PaintClickOrDrag paintDrag = new PaintClickOrDrag(
@@ -173,7 +175,7 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke
 				paintSelection,
 				brushRadius::get,
 				brushDepth::get,
-				event -> event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Paint) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE));
 		handler.addEventHandler(MouseEvent.ANY, paintDrag.singleEventHandler());
 
 		// erase
@@ -183,7 +185,7 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke
 				() -> Label.TRANSPARENT,
 				brushRadius::get,
 				brushDepth::get,
-				event -> event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Erase) && event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE));
 		handler.addEventHandler(MouseEvent.ANY, eraseDrag.singleEventHandler());
 
 		// background
@@ -193,26 +195,26 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke
 				() -> Label.BACKGROUND,
 				brushRadius::get,
 				brushDepth::get,
-				event -> event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE, KeyCode.SHIFT));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Background) && event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE, KeyCode.SHIFT));
 		handler.addEventHandler(MouseEvent.ANY, backgroundDrag.singleEventHandler());
 
 		// advanced paint stuff
 		handler.addOnMousePressed((EventFX.MOUSE_PRESSED(
 				"fill",
 				event -> fill.fillAt(event.getX(), event.getY(), paintSelection::get),
-				event -> event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(
 						KeyCode.SHIFT,
 						KeyCode.F))));
 
 		handler.addOnMousePressed(EventFX.MOUSE_PRESSED(
 				"fill 2D",
 				event -> fill2D.fillAt(event.getX(), event.getY(), paintSelection::get),
-				event -> event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.F)));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.F)));
 
 		handler.addOnMousePressed(EventFX.MOUSE_PRESSED(
 				"restrict",
 				event -> restrictor.restrictTo(event.getX(), event.getY()),
-				event -> event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Restrict) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(
 						KeyCode.SHIFT,
 						KeyCode.R)));
 

From fd2e9471df25cf74ae78cfc07935fc4f717226c4 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 21 May 2019 18:13:07 -0400
Subject: [PATCH 12/84] use 2d flood-filling in shape interpolation mode
 instead of painting

---
 .../control/ShapeInterpolationMode.java       | 125 +++++++++--
 .../paintera/control/paint/FloodFill2D.java   | 200 ++++++++++--------
 .../paintera/state/LabelSourceState.java      |  21 +-
 3 files changed, 236 insertions(+), 110 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index dc3673b99..771cbd056 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -1,6 +1,8 @@
 package org.janelia.saalfeldlab.paintera.control;
 
 import java.lang.invoke.MethodHandles;
+import java.util.function.Predicate;
+
 import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
 import org.janelia.saalfeldlab.fx.event.EventFX;
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
@@ -9,11 +11,19 @@
 import org.janelia.saalfeldlab.paintera.control.actions.LabelAction;
 import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction;
 import org.janelia.saalfeldlab.paintera.control.actions.PaintAction;
+import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D;
 import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
+import org.janelia.saalfeldlab.paintera.data.mask.Mask;
+import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo;
+import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
+import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse;
+import org.janelia.saalfeldlab.paintera.id.IdService;
+import org.janelia.saalfeldlab.paintera.state.SourceInfo;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import bdv.fx.viewer.ViewerPanelFX;
+import bdv.fx.viewer.ViewerState;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.event.Event;
@@ -23,8 +33,12 @@
 import javafx.scene.effect.ColorAdjust;
 import javafx.scene.input.KeyCode;
 import javafx.scene.input.KeyEvent;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.label.Label;
+import net.imglib2.type.numeric.IntegerType;
+import net.imglib2.type.numeric.integer.UnsignedLongType;
 
-public class ShapeInterpolationMode
+public class ShapeInterpolationMode<D extends IntegerType<D>>
 {
 	private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
@@ -34,24 +48,44 @@ public class ShapeInterpolationMode
 		allowedActionsInShapeInterpolationMode = new AllowedActions(
 			NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll),
 			LabelAction.none(),
-			PaintAction.of(PaintAction.Paint, PaintAction.SetBrush)
+			PaintAction.none()
 		);
 	}
 
+	private static final class ForegroundCheck implements Predicate<UnsignedLongType>
+	{
+
+		@Override
+		public boolean test(final UnsignedLongType t)
+		{
+			return t.getIntegerLong() == 1;
+		}
+
+	}
+
+	private static final ForegroundCheck FOREGROUND_CHECK = new ForegroundCheck();
+
 	private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>();
 
+	private final MaskedSource<D, ?> source;
+
 	private final SelectedIds selectedIds;
+	private final IdService idService;
 
 	private AllowedActions lastAllowedActions;
 
-	public ShapeInterpolationMode(final SelectedIds selectedIds)
+	private Mask<UnsignedLongType> mask;
+
+	public ShapeInterpolationMode(final MaskedSource<D, ?> source, final SelectedIds selectedIds, final IdService idService)
 	{
+		this.source = source;
 		this.selectedIds = selectedIds;
+		this.idService = idService;
 	}
 
 	public ObjectProperty<ViewerPanelFX> activeViewerProperty()
 	{
-		return this.activeViewer;
+		return activeViewer;
 	}
 
 	public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final KeyTracker keyTracker)
@@ -63,8 +97,8 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 						"enter shape interpolation mode",
 						e -> enterMode(paintera, (ViewerPanelFX) e.getTarget()),
 						e -> e.getTarget() instanceof ViewerPanelFX &&
-							this.activeViewer.get() == null &&
-							selectedIds.isLastSelectionValid() &&
+							!isModeOn() &&
+							!source.isApplyingMaskProperty().get() &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
 					)
 			);
@@ -73,46 +107,95 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				EventFX.KEY_PRESSED(
 						"exit shape interpolation mode",
 						e -> exitMode(paintera),
-						e -> this.activeViewer.get() != null &&
-							keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE)
+						e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE)
 					)
 			);
+		filter.addOnMousePressed(EventFX.MOUSE_PRESSED(
+				"select object in current section",
+				e -> selectObjectSection(paintera.sourceInfo(), e.getX(), e.getY()),
+				e -> isModeOn() && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
+			);
 		return filter;
 	}
 
 	public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewer)
 	{
+		if (isModeOn())
+		{
+			LOG.info("Already in shape interpolation mode");
+			return;
+		}
 		LOG.info("Entering shape interpolation mode");
-		assert this.activeViewer.get() == null;
 		activeViewer.set(viewer);
 		setDisableOtherViewers(true);
 
-		this.lastAllowedActions = paintera.allowedActionsProperty().get();
+		lastAllowedActions = paintera.allowedActionsProperty().get();
 		paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationMode);
 
-		// ...
+		try
+		{
+			createMask();
+		}
+		catch (final MaskInUse e)
+		{
+			e.printStackTrace();
+		}
 	}
 
 	public void exitMode(final PainteraBaseView paintera)
 	{
+		if (!isModeOn())
+		{
+			LOG.info("Not in shape interpolation mode");
+			return;
+		}
 		LOG.info("Exiting shape interpolation mode");
-		assert this.activeViewer.get() != null;
 		setDisableOtherViewers(false);
 
-		paintera.allowedActionsProperty().set(this.lastAllowedActions);
-		this.lastAllowedActions = null;
+		paintera.allowedActionsProperty().set(lastAllowedActions);
+		lastAllowedActions = null;
+
+		forgetMask();
+		activeViewer.get().requestRepaint();
+
+		activeViewer.set(null);
+	}
+
+	public boolean isModeOn()
+	{
+		return activeViewer.get() != null;
+	}
 
-		// ...
+	private void createMask() throws MaskInUse
+	{
+		final ViewerState viewerState = activeViewer.get().getState();
+		final int time = viewerState.timepointProperty().get();
+		final int level = 0;
+		final long labelId = idService.next();
+
+		final AffineTransform3D labelTransform = new AffineTransform3D();
+		source.getSourceTransform(time, level, labelTransform);
+		final AffineTransform3D viewerTransform = new AffineTransform3D();
+		viewerState.getViewerTransform(viewerTransform);
+		final AffineTransform3D labelToViewerTransform = viewerTransform.copy();
+		labelToViewerTransform.concatenate(labelTransform);
+
+		final MaskInfo<UnsignedLongType> maskInfo = new MaskInfo<>(time, level, new UnsignedLongType(labelId));
+		mask = source.generateMask(maskInfo, FOREGROUND_CHECK);
+	}
 
-		this.activeViewer.set(null);
+	private void forgetMask()
+	{
+		mask = null;
+		source.resetMasks();
 	}
 
 	private void setDisableOtherViewers(final boolean disable)
 	{
-		final Parent parent = this.activeViewer.get().getParent();
+		final Parent parent = activeViewer.get().getParent();
 		for (final Node child : parent.getChildrenUnmodifiable())
 		{
-			if (child instanceof ViewerPanelFX && child != this.activeViewer.get())
+			if (child instanceof ViewerPanelFX && child != activeViewer.get())
 			{
 				final ViewerPanelFX viewer = (ViewerPanelFX) child;
 				viewer.setDisable(disable);
@@ -130,4 +213,10 @@ private void setDisableOtherViewers(final boolean disable)
 			}
 		}
 	}
+
+	private void selectObjectSection(final SourceInfo sourceInfo, final double x, final double y)
+	{
+		FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, 1.0);
+		activeViewer.get().requestRepaint();
+	}
 }
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
index b960512b0..bf6da8d95 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
@@ -14,6 +14,7 @@
 import javafx.scene.Cursor;
 import javafx.scene.Scene;
 import net.imglib2.FinalInterval;
+import net.imglib2.Interval;
 import net.imglib2.Point;
 import net.imglib2.RandomAccess;
 import net.imglib2.RandomAccessible;
@@ -26,20 +27,18 @@
 import net.imglib2.converter.Converter;
 import net.imglib2.converter.Converters;
 import net.imglib2.realtransform.AffineTransform3D;
-import net.imglib2.type.label.LabelMultisetType;
 import net.imglib2.type.logic.BoolType;
 import net.imglib2.type.numeric.IntegerType;
-import net.imglib2.type.numeric.RealType;
 import net.imglib2.type.numeric.integer.UnsignedLongType;
 import net.imglib2.util.AccessBoxRandomAccessibleOnGet;
 import net.imglib2.view.MixedTransformView;
 import net.imglib2.view.Views;
+
 import org.janelia.saalfeldlab.paintera.data.mask.Mask;
 import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse;
 import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo;
 import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
 import org.janelia.saalfeldlab.paintera.state.HasMaskForLabel;
-import org.janelia.saalfeldlab.paintera.state.LabelSourceState;
 import org.janelia.saalfeldlab.paintera.state.SourceInfo;
 import org.janelia.saalfeldlab.paintera.state.SourceState;
 import org.slf4j.Logger;
@@ -172,89 +171,9 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi
 		try
 		{
 			final Mask<UnsignedLongType> mask = source.generateMask(maskInfo, FOREGROUND_CHECK);
-			final long                                       seedLabel = access.get().getIntegerLong();
-			LOG.debug("Got seed label {}", seedLabel);
-			final RandomAccessibleInterval<BoolType> relevantBackground = Converters.convert(
-					background,
-					(src, tgt) -> tgt.set(src.getIntegerLong() == seedLabel),
-					new BoolType()
-			                                                                                );
-			final RandomAccessible<BoolType> extended = Views.extendValue(relevantBackground, new BoolType(false));
-
-			final int fillNormalAxisInLabelCoordinateSystem = PaintUtils.labelAxisCorrespondingToViewerAxis(labelTransform, viewerTransform, 2);
-			final AccessBoxRandomAccessibleOnGet<UnsignedLongType> accessTracker = new
-					AccessBoxRandomAccessibleOnGet<>(
-					Views.extendValue(mask.mask, new UnsignedLongType(1l)));
-			accessTracker.initAccessBox();
-
-			if (fillNormalAxisInLabelCoordinateSystem < 0)
-
-			{
-
-				FloodFillTransformedPlane.fill(
-						labelToViewerTransform,
-						(0.5 + this.fillDepth.get() - 1.0) * PaintUtils.maximumVoxelDiagonalLengthPerDimension(
-								labelTransform,
-								viewerTransform
-						                                                                                      )[2],
-						extended.randomAccess(),
-						accessTracker.randomAccess(),
-						new RealPoint(x, y, 0),
-						1l
-				                              );
-
-				requestRepaint.run();
-				source.applyMask(
-						mask,
-						new FinalInterval(accessTracker.getMin(), accessTracker.getMax()),
-						FOREGROUND_CHECK
-				                );
-			}
-
-			else
-			{
-				LOG.debug(
-						"Flood filling axis aligned. Corressponding viewer axis={}",
-						fillNormalAxisInLabelCoordinateSystem
-				         );
-				final long slicePos  = access.getLongPosition(fillNormalAxisInLabelCoordinateSystem);
-				final long numSlices = Math.max((long) Math.ceil(this.fillDepth.get()) - 1, 0);
-				final long[] seed2D = {
-						access.getLongPosition(fillNormalAxisInLabelCoordinateSystem == 0 ? 1 : 0),
-						access.getLongPosition(fillNormalAxisInLabelCoordinateSystem != 2 ? 2 : 1)
-				};
-				accessTracker.initAccessBox();
-				final long[] min = {Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE};
-				final long[] max = {Long.MIN_VALUE, Long.MIN_VALUE, Long.MIN_VALUE};
-				for (long i = slicePos - numSlices; i <= slicePos + numSlices; ++i)
-				{
-					final MixedTransformView<BoolType> relevantBackgroundSlice = Views.hyperSlice(
-							extended,
-							fillNormalAxisInLabelCoordinateSystem,
-							i
-					                                                                             );
-					final MixedTransformView<UnsignedLongType> relevantAccessTracker = Views.hyperSlice(
-							accessTracker,
-							fillNormalAxisInLabelCoordinateSystem,
-							i
-					                                                                                   );
-
-					FloodFill.fill(
-							relevantBackgroundSlice,
-							relevantAccessTracker,
-							new Point(seed2D),
-							new UnsignedLongType(1l),
-							new DiamondShape(1)
-					              );
-					Arrays.setAll(min, d -> Math.min(accessTracker.getMin()[d], min[d]));
-					Arrays.setAll(max, d -> Math.max(accessTracker.getMax()[d], max[d]));
-				}
-
-				requestRepaint.run();
-				source.applyMask(mask, new FinalInterval(min, max), FOREGROUND_CHECK);
-
-			}
-
+			final Interval affectedInterval = fillMaskAt(x, y, this.viewer, mask, source, this.fillDepth.get());
+			requestRepaint.run();
+			source.applyMask(mask, affectedInterval, FOREGROUND_CHECK);
 		} catch (final MaskInUse e)
 		{
 			LOG.debug(e.getMessage());
@@ -263,7 +182,116 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi
 		{
 			scene.setCursor(previousCursor);
 		}
+	}
+
+	/**
+	 * Flood-fills the given mask starting at the specified 2D location in the viewer.
+	 * Returns the affected interval in source coordinates.
+	 *
+	 * @param x
+	 * @param y
+	 * @param viewer
+	 * @param mask
+	 * @param source
+	 * @param fillDepth
+	 * @return affected interval
+	 */
+	public static <T extends IntegerType<T>> Interval fillMaskAt(
+			final double x,
+			final double y,
+			final ViewerPanelFX viewer,
+			final Mask<UnsignedLongType> mask,
+			final MaskedSource<T, ?> source,
+			final double fillDepth)
+	{
+		final int time = mask.info.t;
+		final int level = mask.info.level;
+
+		final AffineTransform3D labelTransform = new AffineTransform3D();
+		source.getSourceTransform(time, level, labelTransform);
+		final AffineTransform3D viewerTransform = new AffineTransform3D();
+		viewer.getState().getViewerTransform(viewerTransform);
+		final AffineTransform3D labelToViewerTransform = viewerTransform.copy().concatenate(labelTransform);
+
+		final RealPoint rp = setCoordinates(x, y, viewer, labelTransform);
+		final RandomAccessibleInterval<T> background = source.getDataSource(time, level);
+		final RandomAccess<T> access = background.randomAccess();
+		for (int d = 0; d < access.numDimensions(); ++d)
+			access.setPosition(Math.round(rp.getDoublePosition(d)), d);
+		final long seedLabel = access.get().getIntegerLong();
+		LOG.debug("Got seed label {}", seedLabel);
+		final RandomAccessibleInterval<BoolType> relevantBackground = Converters.convert(
+				background,
+				(src, tgt) -> tgt.set(src.getIntegerLong() == seedLabel),
+				new BoolType()
+		                                                                                );
+		final RandomAccessible<BoolType> extended = Views.extendValue(relevantBackground, new BoolType(false));
+
+		final int fillNormalAxisInLabelCoordinateSystem = PaintUtils.labelAxisCorrespondingToViewerAxis(labelTransform, viewerTransform, 2);
+		final AccessBoxRandomAccessibleOnGet<UnsignedLongType> accessTracker = new
+				AccessBoxRandomAccessibleOnGet<>(
+				Views.extendValue(mask.mask, new UnsignedLongType(1l)));
+		accessTracker.initAccessBox();
+
+		final Interval affectedInterval;
+
+		if (fillNormalAxisInLabelCoordinateSystem < 0)
+		{
+			FloodFillTransformedPlane.fill(
+					labelToViewerTransform,
+					(0.5 + fillDepth - 1.0) * PaintUtils.maximumVoxelDiagonalLengthPerDimension(
+							labelTransform,
+							viewerTransform
+					                                                                                      )[2],
+					extended.randomAccess(),
+					accessTracker.randomAccess(),
+					new RealPoint(x, y, 0),
+					1l
+			                              );
+			affectedInterval = new FinalInterval(accessTracker.getMin(), accessTracker.getMax());
+		}
+		else
+		{
+			LOG.debug(
+					"Flood filling axis aligned. Corressponding viewer axis={}",
+					fillNormalAxisInLabelCoordinateSystem
+			         );
+			final long slicePos  = access.getLongPosition(fillNormalAxisInLabelCoordinateSystem);
+			final long numSlices = Math.max((long) Math.ceil(fillDepth) - 1, 0);
+			final long[] seed2D = {
+					access.getLongPosition(fillNormalAxisInLabelCoordinateSystem == 0 ? 1 : 0),
+					access.getLongPosition(fillNormalAxisInLabelCoordinateSystem != 2 ? 2 : 1)
+			};
+			accessTracker.initAccessBox();
+			final long[] min = {Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE};
+			final long[] max = {Long.MIN_VALUE, Long.MIN_VALUE, Long.MIN_VALUE};
+			for (long i = slicePos - numSlices; i <= slicePos + numSlices; ++i)
+			{
+				final MixedTransformView<BoolType> relevantBackgroundSlice = Views.hyperSlice(
+						extended,
+						fillNormalAxisInLabelCoordinateSystem,
+						i
+				                                                                             );
+				final MixedTransformView<UnsignedLongType> relevantAccessTracker = Views.hyperSlice(
+						accessTracker,
+						fillNormalAxisInLabelCoordinateSystem,
+						i
+				                                                                                   );
+
+				FloodFill.fill(
+						relevantBackgroundSlice,
+						relevantAccessTracker,
+						new Point(seed2D),
+						new UnsignedLongType(1l),
+						new DiamondShape(1)
+				              );
+				Arrays.setAll(min, d -> Math.min(accessTracker.getMin()[d], min[d]));
+				Arrays.setAll(max, d -> Math.max(accessTracker.getMax()[d], max[d]));
+			}
+			affectedInterval = new FinalInterval(min, max);
+		}
 
+		return affectedInterval;
 	}
 
 	private static RealPoint setCoordinates(
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
index dd283cbd8..00649d705 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
@@ -125,7 +125,7 @@ public class LabelSourceState<D extends IntegerType<D>, T>
 
 	private final LabelSourceStateMergeDetachHandler mergeDetachHandler;
 
-	private final ShapeInterpolationMode shapeInterpolationMode;
+	private final ShapeInterpolationMode<D> shapeInterpolationMode;
 
 	private final ObjectProperty<FloodFillState> floodFillState = new SimpleObjectProperty<>();
 
@@ -155,7 +155,10 @@ public LabelSourceState(
 		this.paintHandler = new LabelSourceStatePaintHandler(selectedIds);
 		this.idSelectorHandler = new LabelSourceStateIdSelectorHandler(dataSource, selectedIds, assignment, lockedSegments);
 		this.mergeDetachHandler = new LabelSourceStateMergeDetachHandler(dataSource, selectedIds, assignment, idService);
-		this.shapeInterpolationMode = new ShapeInterpolationMode(selectedIds);
+		if (dataSource instanceof MaskedSource<?, ?>)
+			this.shapeInterpolationMode = new ShapeInterpolationMode<>((MaskedSource<D, ?>) dataSource, selectedIds, idService);
+		else
+			this.shapeInterpolationMode = null;
 		this.displayStatus = createDisplayStatus();
 		assignment.addListener(obs -> stain());
 		selectedIds.addListener(obs -> stain());
@@ -166,6 +169,7 @@ public LabelBlockLookup labelBlockLookup() {
 		return this.labelBlockLookup;
 	}
 
+	@Override
 	public LongFunction<Converter<D, BoolType>> maskForLabel()
 	{
 		return this.maskForLabel;
@@ -183,16 +187,19 @@ public ManagedMeshSettings managedMeshSettings()
 		return this.meshManager.managedMeshSettings();
 	}
 
+	@Override
 	public FragmentSegmentAssignmentState assignment()
 	{
 		return this.assignment;
 	}
 
+	@Override
 	public IdService idService()
 	{
 		return this.idService;
 	}
 
+	@Override
 	public SelectedIds selectedIds()
 	{
 		return this.selectedIds;
@@ -210,6 +217,7 @@ public void invalidateAllMeshCaches()
 		this.meshManager.invalidateMeshCaches();
 	}
 
+	@Override
 	public LockedSegmentsState lockedSegments()
 	{
 		return this.lockedSegments;
@@ -453,7 +461,7 @@ private static <D extends RealType<D>> LongFunction<Converter<D, BoolType>> equa
 	}
 
 	@Override
-	public EventHandler<Event> stateSpecificGlobalEventHandler(PainteraBaseView paintera, KeyTracker keyTracker) {
+	public EventHandler<Event> stateSpecificGlobalEventHandler(final PainteraBaseView paintera, final KeyTracker keyTracker) {
 		LOG.debug("Returning {}-specific global handler", getClass().getSimpleName());
 		final DelegateEventHandlers.AnyHandler handler = DelegateEventHandlers.handleAny();
 		handler.addEventHandler(
@@ -470,7 +478,7 @@ public EventHandler<Event> stateSpecificGlobalEventHandler(PainteraBaseView pain
 //	}
 
 	@Override
-	public EventHandler<Event> stateSpecificViewerEventHandler(PainteraBaseView paintera, KeyTracker keyTracker) {
+	public EventHandler<Event> stateSpecificViewerEventHandler(final PainteraBaseView paintera, final KeyTracker keyTracker) {
 		LOG.info("Returning {}-specific handler", getClass().getSimpleName());
 		LOG.debug("Returning {}-specific handler", getClass().getSimpleName());
 		final DelegateEventHandlers.ListDelegateEventHandler<Event> handler = DelegateEventHandlers.listHandler();
@@ -481,12 +489,13 @@ public EventHandler<Event> stateSpecificViewerEventHandler(PainteraBaseView pain
 	}
 
 	@Override
-	public EventHandler<Event> stateSpecificViewerEventFilter(PainteraBaseView paintera, KeyTracker keyTracker) {
+	public EventHandler<Event> stateSpecificViewerEventFilter(final PainteraBaseView paintera, final KeyTracker keyTracker) {
 		LOG.info("Returning {}-specific filter", getClass().getSimpleName());
 		LOG.debug("Returning {}-specific filter", getClass().getSimpleName());
 		final DelegateEventHandlers.ListDelegateEventHandler<Event> filter = DelegateEventHandlers.listHandler();
 		filter.addHandler(paintHandler.viewerFilter(paintera, keyTracker));
-		filter.addHandler(shapeInterpolationMode.modeHandler(paintera, keyTracker));
+		if (shapeInterpolationMode != null)
+			filter.addHandler(shapeInterpolationMode.modeHandler(paintera, keyTracker));
 		return filter;
 	}
 

From cf73327d4625d5c44501bd2ce9f570d0ca1a2ecc Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 22 May 2019 10:50:30 -0400
Subject: [PATCH 13/84] activate new label id in interpolation and restore old
 selection on exit

---
 .../paintera/control/ShapeInterpolationMode.java      | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 771cbd056..6154bf4d4 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -73,6 +73,8 @@ public boolean test(final UnsignedLongType t)
 	private final IdService idService;
 
 	private AllowedActions lastAllowedActions;
+	private long lastSelectedId;
+	private long[] lastActiveIds;
 
 	private Mask<UnsignedLongType> mask;
 
@@ -135,6 +137,10 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 		try
 		{
 			createMask();
+			lastSelectedId = selectedIds.getLastSelection();
+			lastActiveIds = selectedIds.getActiveIds();
+			final long newLabelId = mask.info.value.get();
+			selectedIds.activate(newLabelId);
 		}
 		catch (final MaskInUse e)
 		{
@@ -155,6 +161,11 @@ public void exitMode(final PainteraBaseView paintera)
 		paintera.allowedActionsProperty().set(lastAllowedActions);
 		lastAllowedActions = null;
 
+		final long newLabelId = mask.info.value.get();
+		selectedIds.activate(lastActiveIds);
+		selectedIds.activateAlso(lastSelectedId);
+		lastSelectedId = Label.INVALID;
+		lastActiveIds = null;
 		forgetMask();
 		activeViewer.get().requestRepaint();
 

From 339fa615b60c36b2179ad677f2252e1aeb98d49f Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 22 May 2019 10:52:05 -0400
Subject: [PATCH 14/84] slightly increase floodfill depth in shape
 interpolation mode

with 1.0 we can still see bleedthrough from adjacent sections in a rotated view
---
 .../paintera/control/ShapeInterpolationMode.java            | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 6154bf4d4..cdcf84b5d 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -54,17 +54,17 @@ public class ShapeInterpolationMode<D extends IntegerType<D>>
 
 	private static final class ForegroundCheck implements Predicate<UnsignedLongType>
 	{
-
 		@Override
 		public boolean test(final UnsignedLongType t)
 		{
 			return t.getIntegerLong() == 1;
 		}
-
 	}
 
 	private static final ForegroundCheck FOREGROUND_CHECK = new ForegroundCheck();
 
+	private static final double FILL_DEPTH = 2.0;
+
 	private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>();
 
 	private final MaskedSource<D, ?> source;
@@ -227,7 +227,7 @@ private void setDisableOtherViewers(final boolean disable)
 
 	private void selectObjectSection(final SourceInfo sourceInfo, final double x, final double y)
 	{
-		FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, 1.0);
+		FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, FILL_DEPTH);
 		activeViewer.get().requestRepaint();
 	}
 }

From f4f7a3b1a88e407db54ffef4e119fc0ce0e9bdd0 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 22 May 2019 10:53:22 -0400
Subject: [PATCH 15/84] use predefined color to highlight selection in shape
 interpolation mode

---
 .../paintera/control/ShapeInterpolationMode.java  | 15 +++++++++++++--
 .../paintera/state/LabelSourceState.java          |  2 +-
 2 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index cdcf84b5d..f2f6e5092 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -19,6 +19,7 @@
 import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse;
 import org.janelia.saalfeldlab.paintera.id.IdService;
 import org.janelia.saalfeldlab.paintera.state.SourceInfo;
+import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -33,6 +34,7 @@
 import javafx.scene.effect.ColorAdjust;
 import javafx.scene.input.KeyCode;
 import javafx.scene.input.KeyEvent;
+import javafx.scene.paint.Color;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.type.label.Label;
 import net.imglib2.type.numeric.IntegerType;
@@ -65,12 +67,14 @@ public boolean test(final UnsignedLongType t)
 
 	private static final double FILL_DEPTH = 2.0;
 
+	private static final Color MASK_COLOR = Color.web("00CCFF");
+
 	private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>();
 
 	private final MaskedSource<D, ?> source;
-
 	private final SelectedIds selectedIds;
 	private final IdService idService;
+	private final HighlightingStreamConverter<?> converter;
 
 	private AllowedActions lastAllowedActions;
 	private long lastSelectedId;
@@ -78,11 +82,16 @@ public boolean test(final UnsignedLongType t)
 
 	private Mask<UnsignedLongType> mask;
 
-	public ShapeInterpolationMode(final MaskedSource<D, ?> source, final SelectedIds selectedIds, final IdService idService)
+	public ShapeInterpolationMode(
+			final MaskedSource<D, ?> source,
+			final SelectedIds selectedIds,
+			final IdService idService,
+			final HighlightingStreamConverter<?> converter)
 	{
 		this.source = source;
 		this.selectedIds = selectedIds;
 		this.idService = idService;
+		this.converter = converter;
 	}
 
 	public ObjectProperty<ViewerPanelFX> activeViewerProperty()
@@ -140,6 +149,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 			lastSelectedId = selectedIds.getLastSelection();
 			lastActiveIds = selectedIds.getActiveIds();
 			final long newLabelId = mask.info.value.get();
+			converter.setColor(newLabelId, MASK_COLOR);
 			selectedIds.activate(newLabelId);
 		}
 		catch (final MaskInUse e)
@@ -162,6 +172,7 @@ public void exitMode(final PainteraBaseView paintera)
 		lastAllowedActions = null;
 
 		final long newLabelId = mask.info.value.get();
+		converter.removeColor(newLabelId);
 		selectedIds.activate(lastActiveIds);
 		selectedIds.activateAlso(lastSelectedId);
 		lastSelectedId = Label.INVALID;
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
index 00649d705..97ecdd837 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
@@ -156,7 +156,7 @@ public LabelSourceState(
 		this.idSelectorHandler = new LabelSourceStateIdSelectorHandler(dataSource, selectedIds, assignment, lockedSegments);
 		this.mergeDetachHandler = new LabelSourceStateMergeDetachHandler(dataSource, selectedIds, assignment, idService);
 		if (dataSource instanceof MaskedSource<?, ?>)
-			this.shapeInterpolationMode = new ShapeInterpolationMode<>((MaskedSource<D, ?>) dataSource, selectedIds, idService);
+			this.shapeInterpolationMode = new ShapeInterpolationMode<>((MaskedSource<D, ?>) dataSource, selectedIds, idService, converter);
 		else
 			this.shapeInterpolationMode = null;
 		this.displayStatus = createDisplayStatus();

From 08c013e9ac12c803a882f4bc5b0a0d0af4d51c09 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 22 May 2019 10:54:07 -0400
Subject: [PATCH 16/84] remove duplicate method for generating and caching
 colors from stream

---
 ...nAngleSaturatedHighlightingARGBStream.java | 49 -------------------
 1 file changed, 49 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/stream/ModalGoldenAngleSaturatedHighlightingARGBStream.java b/src/main/java/org/janelia/saalfeldlab/paintera/stream/ModalGoldenAngleSaturatedHighlightingARGBStream.java
index 8c66c1cb0..a978c212a 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/stream/ModalGoldenAngleSaturatedHighlightingARGBStream.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/stream/ModalGoldenAngleSaturatedHighlightingARGBStream.java
@@ -13,7 +13,6 @@
  */
 package org.janelia.saalfeldlab.paintera.stream;
 
-import net.imglib2.type.label.Label;
 import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentOnlyLocal;
 import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentState;
 import org.janelia.saalfeldlab.paintera.control.lock.LockedSegments;
@@ -55,52 +54,4 @@ public ModalGoldenAngleSaturatedHighlightingARGBStream(
 		super(highlights, assignment, lockedSegments);
 		seed = 1;
 	}
-
-	@Override
-	protected int argbImpl(final long fragmentId, final boolean colorFromSegmentId)
-	{
-
-		final long assigned = colorFromSegmentId ? assignment.getSegment(fragmentId) : fragmentId;
-
-		if (!argbCache.contains(assigned))
-		{
-			double x = getDouble(seed + assigned);
-			x *= 6.0;
-			final int    k = (int) x;
-			final int    l = k + 1;
-			final double u = x - k;
-			final double v = 1.0 - u;
-
-			final int r = interpolate(rs, k, l, u, v);
-			final int g = interpolate(gs, k, l, u, v);
-			final int b = interpolate(bs, k, l, u, v);
-
-			final int argb = argb(r, g, b, alpha);
-
-			synchronized (argbCache)
-			{
-				argbCache.put(assigned, argb);
-			}
-		}
-
-		int argb = argbCache.get(assigned);
-
-		if (Label.INVALID == fragmentId)
-		{
-			argb = argb & 0x00ffffff | invalidSegmentAlpha;
-		}
-		else if (isLockedSegment(fragmentId) && hideLockedSegments)
-		{
-			argb = argb & 0x00ffffff;
-		}
-		else
-		{
-			final boolean isActiveSegment = isActiveSegment(fragmentId);
-			argb = argb & 0x00ffffff | (isActiveSegment ? isActiveFragment(fragmentId)
-			                                              ? activeFragmentAlpha
-			                                              : activeSegmentAlpha : alpha);
-		}
-
-		return argb;
-	}
 }

From 6f8d1c50878b3dfedede277716d9a9481dc5d7cf Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 22 May 2019 13:53:27 -0400
Subject: [PATCH 17/84] block scrolling when selecting objects in section until
 selection is fixed

---
 .../control/ShapeInterpolationMode.java       | 33 +++++++++++++++++--
 1 file changed, 30 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index f2f6e5092..f7c195794 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -18,7 +18,6 @@
 import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
 import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse;
 import org.janelia.saalfeldlab.paintera.id.IdService;
-import org.janelia.saalfeldlab.paintera.state.SourceInfo;
 import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -45,6 +44,7 @@ public class ShapeInterpolationMode<D extends IntegerType<D>>
 	private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
 	private static final AllowedActions allowedActionsInShapeInterpolationMode;
+	private static final AllowedActions allowedActionsInShapeInterpolationModeWhenSelected;
 	static
 	{
 		allowedActionsInShapeInterpolationMode = new AllowedActions(
@@ -52,6 +52,11 @@ public class ShapeInterpolationMode<D extends IntegerType<D>>
 			LabelAction.none(),
 			PaintAction.none()
 		);
+		allowedActionsInShapeInterpolationModeWhenSelected = new AllowedActions(
+				NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom),
+				LabelAction.none(),
+				PaintAction.none()
+			);
 	}
 
 	private static final class ForegroundCheck implements Predicate<UnsignedLongType>
@@ -81,6 +86,7 @@ public boolean test(final UnsignedLongType t)
 	private long[] lastActiveIds;
 
 	private Mask<UnsignedLongType> mask;
+	private boolean hasActiveSelection;
 
 	public ShapeInterpolationMode(
 			final MaskedSource<D, ?> source,
@@ -113,6 +119,16 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
 					)
 			);
+		filter.addEventHandler(
+				KeyEvent.KEY_PRESSED,
+				EventFX.KEY_PRESSED(
+						"fix selection",
+						e -> fixSelection(paintera),
+						e -> isModeOn() &&
+							hasActiveSelection &&
+							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
+					)
+			);
 		filter.addEventHandler(
 				KeyEvent.KEY_PRESSED,
 				EventFX.KEY_PRESSED(
@@ -123,7 +139,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 			);
 		filter.addOnMousePressed(EventFX.MOUSE_PRESSED(
 				"select object in current section",
-				e -> selectObjectSection(paintera.sourceInfo(), e.getX(), e.getY()),
+				e -> selectObject(paintera, e.getX(), e.getY()),
 				e -> isModeOn() && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
 			);
 		return filter;
@@ -151,6 +167,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 			final long newLabelId = mask.info.value.get();
 			converter.setColor(newLabelId, MASK_COLOR);
 			selectedIds.activate(newLabelId);
+			hasActiveSelection = false;
 		}
 		catch (final MaskInUse e)
 		{
@@ -177,6 +194,7 @@ public void exitMode(final PainteraBaseView paintera)
 		selectedIds.activateAlso(lastSelectedId);
 		lastSelectedId = Label.INVALID;
 		lastActiveIds = null;
+		hasActiveSelection = false;
 		forgetMask();
 		activeViewer.get().requestRepaint();
 
@@ -194,6 +212,7 @@ private void createMask() throws MaskInUse
 		final int time = viewerState.timepointProperty().get();
 		final int level = 0;
 		final long labelId = idService.next();
+		LOG.info("Created new label ID for shape interpolation: {}", labelId);
 
 		final AffineTransform3D labelTransform = new AffineTransform3D();
 		source.getSourceTransform(time, level, labelTransform);
@@ -236,9 +255,17 @@ private void setDisableOtherViewers(final boolean disable)
 		}
 	}
 
-	private void selectObjectSection(final SourceInfo sourceInfo, final double x, final double y)
+	private void fixSelection(final PainteraBaseView paintera)
+	{
+		hasActiveSelection = false;
+		paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationMode);
+	}
+
+	private void selectObject(final PainteraBaseView paintera, final double x, final double y)
 	{
 		FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, FILL_DEPTH);
 		activeViewer.get().requestRepaint();
+		hasActiveSelection = true;
+		paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationModeWhenSelected);
 	}
 }

From 5be37721b9c2d7c47ba458055b9deebe245e4bb7 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 22 May 2019 13:54:21 -0400
Subject: [PATCH 18/84] allow 2d flood-filling with provided label id

---
 .../java/bdv/fx/viewer/ViewerPanelFX.java     | 38 ++++++-----
 .../control/ShapeInterpolationMode.java       | 35 ++++++----
 .../paintera/control/paint/FloodFill2D.java   | 66 ++++---------------
 3 files changed, 57 insertions(+), 82 deletions(-)

diff --git a/src/main/java/bdv/fx/viewer/ViewerPanelFX.java b/src/main/java/bdv/fx/viewer/ViewerPanelFX.java
index d87f39aae..a461e0417 100644
--- a/src/main/java/bdv/fx/viewer/ViewerPanelFX.java
+++ b/src/main/java/bdv/fx/viewer/ViewerPanelFX.java
@@ -36,43 +36,26 @@
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import bdv.viewer.ViewerOptions;
-import gnu.trove.list.array.TIntArrayList;
-import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.ReadOnlyBooleanProperty;
 import javafx.beans.property.ReadOnlyDoubleProperty;
-import javafx.beans.property.ReadOnlyObjectProperty;
-import javafx.beans.property.SimpleObjectProperty;
 import javafx.collections.FXCollections;
 import javafx.collections.ListChangeListener;
 import javafx.collections.ObservableList;
 import javafx.scene.Node;
-import javafx.scene.canvas.Canvas;
 import javafx.scene.layout.StackPane;
 import net.imglib2.Interval;
-import net.imglib2.Point;
 import net.imglib2.Positionable;
 import net.imglib2.RealInterval;
 import net.imglib2.RealLocalizable;
 import net.imglib2.RealPoint;
 import net.imglib2.RealPositionable;
-import net.imglib2.algorithm.fill.FloodFill;
-import net.imglib2.algorithm.neighborhood.DiamondShape;
-import net.imglib2.img.array.ArrayImg;
-import net.imglib2.img.array.ArrayImgs;
-import net.imglib2.img.basictypeaccess.array.IntArray;
-import net.imglib2.img.cell.CellGrid;
 import net.imglib2.realtransform.AffineTransform3D;
-import net.imglib2.type.numeric.integer.IntType;
 import net.imglib2.ui.TransformListener;
-import net.imglib2.util.Intervals;
-import net.imglib2.view.Views;
-
 import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -287,6 +270,25 @@ public void displayToGlobalCoordinates(final double x, final double y, final Rea
 		viewerTransform.applyInverse(gPos, lPos);
 	}
 
+	/**
+	 * Set {@code pos} to the display coordinates (x,y,0)<sup>T</sup> transformed into the source coordinate system.
+	 *
+	 * @param pos
+	 * 		is set to the source coordinates at display (x,y,0)<sup>T</sup>.
+	 */
+	public <P extends RealLocalizable & RealPositionable> void displayToSourceCoordinates(
+			final double x,
+			final double y,
+			final AffineTransform3D sourceTransform,
+			final P pos)
+	{
+		pos.setPosition(x, 0);
+		pos.setPosition(y, 1);
+		pos.setPosition(0, 2);
+		displayToGlobalCoordinates(pos);
+		sourceTransform.applyInverse(pos, pos);
+	}
+
 	/**
 	 * Set {@code gPos} to the current mouse coordinates transformed into the global coordinate system.
 	 *
@@ -499,7 +501,7 @@ public ReadOnlyDoubleProperty mouseYProperty()
 	 *                     1. {@code 0 < sceenScales[i] <= 1} for all {@code i}
 	 *                     2. {@code screenScales[i] < screenScales[i - 1]} for all {@code i > 0}
 	 */
-	public void setScreenScales(double[] screenScales)
+	public void setScreenScales(final double[] screenScales)
 	{
 		LOG.debug("Setting screen scales to {}", screenScales);
 		this.renderUnit.setScreenScales(screenScales.clone());
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index f7c195794..98a061fe8 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -34,6 +34,8 @@
 import javafx.scene.input.KeyCode;
 import javafx.scene.input.KeyEvent;
 import javafx.scene.paint.Color;
+import net.imglib2.RandomAccess;
+import net.imglib2.RealPoint;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.type.label.Label;
 import net.imglib2.type.numeric.IntegerType;
@@ -59,21 +61,12 @@ public class ShapeInterpolationMode<D extends IntegerType<D>>
 			);
 	}
 
-	private static final class ForegroundCheck implements Predicate<UnsignedLongType>
-	{
-		@Override
-		public boolean test(final UnsignedLongType t)
-		{
-			return t.getIntegerLong() == 1;
-		}
-	}
-
-	private static final ForegroundCheck FOREGROUND_CHECK = new ForegroundCheck();
-
 	private static final double FILL_DEPTH = 2.0;
 
 	private static final Color MASK_COLOR = Color.web("00CCFF");
 
+	private static final Predicate<UnsignedLongType> FOREGROUND_CHECK = t -> t.get() > 0;
+
 	private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>();
 
 	private final MaskedSource<D, ?> source;
@@ -86,6 +79,7 @@ public boolean test(final UnsignedLongType t)
 	private long[] lastActiveIds;
 
 	private Mask<UnsignedLongType> mask;
+	private int currentFillValue;
 	private boolean hasActiveSelection;
 
 	public ShapeInterpolationMode(
@@ -168,6 +162,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 			converter.setColor(newLabelId, MASK_COLOR);
 			selectedIds.activate(newLabelId);
 			hasActiveSelection = false;
+			currentFillValue = 0;
 		}
 		catch (final MaskInUse e)
 		{
@@ -195,6 +190,7 @@ public void exitMode(final PainteraBaseView paintera)
 		lastSelectedId = Label.INVALID;
 		lastActiveIds = null;
 		hasActiveSelection = false;
+		currentFillValue = 0;
 		forgetMask();
 		activeViewer.get().requestRepaint();
 
@@ -263,9 +259,24 @@ private void fixSelection(final PainteraBaseView paintera)
 
 	private void selectObject(final PainteraBaseView paintera, final double x, final double y)
 	{
-		FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, FILL_DEPTH);
+		final long fill = isSelected(x, y) ? Label.BACKGROUND : ++currentFillValue;
+		FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, fill, FILL_DEPTH);
 		activeViewer.get().requestRepaint();
 		hasActiveSelection = true;
 		paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationModeWhenSelected);
 	}
+
+	private boolean isSelected(final double x, final double y)
+	{
+		final AffineTransform3D labelTransform = new AffineTransform3D();
+		source.getSourceTransform(mask.info.t, mask.info.level, labelTransform);
+		final RealPoint pos = new RealPoint(labelTransform.numDimensions());
+		activeViewer.get().displayToSourceCoordinates(x, y, labelTransform, pos);
+
+		final RandomAccess<UnsignedLongType> maskAccess = mask.mask.randomAccess();
+		for (int d = 0; d < pos.numDimensions(); ++d)
+			maskAccess.setPosition(Math.round(pos.getDoublePosition(d)), d);
+
+		return FOREGROUND_CHECK.test(maskAccess.get());
+	}
 }
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
index bf6da8d95..c2fdde69f 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
@@ -19,9 +19,7 @@
 import net.imglib2.RandomAccess;
 import net.imglib2.RandomAccessible;
 import net.imglib2.RandomAccessibleInterval;
-import net.imglib2.RealLocalizable;
 import net.imglib2.RealPoint;
-import net.imglib2.RealPositionable;
 import net.imglib2.algorithm.fill.FloodFill;
 import net.imglib2.algorithm.neighborhood.DiamondShape;
 import net.imglib2.converter.Converter;
@@ -59,15 +57,15 @@ public class FloodFill2D
 
 	private final SimpleDoubleProperty fillDepth = new SimpleDoubleProperty(1.0);
 
+	private static final long FILL_VALUE = 1l;
+
 	private static final class ForegroundCheck implements Predicate<UnsignedLongType>
 	{
-
 		@Override
 		public boolean test(final UnsignedLongType t)
 		{
-			return t.getIntegerLong() == 1;
+			return t.getIntegerLong() == FILL_VALUE;
 		}
-
 	}
 
 	private static final ForegroundCheck FOREGROUND_CHECK = new ForegroundCheck();
@@ -148,21 +146,8 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi
 			return;
 		}
 
-		final int               level          = 0;
-		final AffineTransform3D labelTransform = new AffineTransform3D();
-		final int               time           = viewerState.timepointProperty().get();
-		source.getSourceTransform(time, level, labelTransform);
-		final AffineTransform3D viewerTransform        = this.viewerTransform.copy();
-		final AffineTransform3D labelToViewerTransform = this.viewerTransform.copy().concatenate(labelTransform);
-
-		final RealPoint                   rp         = setCoordinates(x, y, viewer, labelTransform);
-		final RandomAccessibleInterval<T> background = source.getDataSource(time, level);
-		final RandomAccess<T>             access     = background.randomAccess();
-		for (int d = 0; d < access.numDimensions(); ++d)
-		{
-			access.setPosition(Math.round(rp.getDoublePosition(d)), d);
-		}
-
+		final int level = 0;
+		final int time = viewerState.timepointProperty().get();
 		final MaskInfo<UnsignedLongType> maskInfo = new MaskInfo<>(time, level, new UnsignedLongType(fill));
 
 		final Scene  scene          = viewer.getScene();
@@ -171,7 +156,7 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi
 		try
 		{
 			final Mask<UnsignedLongType> mask = source.generateMask(maskInfo, FOREGROUND_CHECK);
-			final Interval affectedInterval = fillMaskAt(x, y, this.viewer, mask, source, this.fillDepth.get());
+			final Interval affectedInterval = fillMaskAt(x, y, this.viewer, mask, source, FILL_VALUE, this.fillDepth.get());
 			requestRepaint.run();
 			source.applyMask(mask, affectedInterval, FOREGROUND_CHECK);
 		} catch (final MaskInUse e)
@@ -193,6 +178,7 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi
 	 * @param viewer
 	 * @param mask
 	 * @param source
+	 * @param fillValue
 	 * @param fillDepth
 	 * @return affected interval
 	 */
@@ -202,6 +188,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 			final ViewerPanelFX viewer,
 			final Mask<UnsignedLongType> mask,
 			final MaskedSource<T, ?> source,
+			final long fillValue,
 			final double fillDepth)
 	{
 		final int time = mask.info.t;
@@ -213,11 +200,12 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 		viewer.getState().getViewerTransform(viewerTransform);
 		final AffineTransform3D labelToViewerTransform = viewerTransform.copy().concatenate(labelTransform);
 
-		final RealPoint rp = setCoordinates(x, y, viewer, labelTransform);
 		final RandomAccessibleInterval<T> background = source.getDataSource(time, level);
 		final RandomAccess<T> access = background.randomAccess();
+		final RealPoint pos = new RealPoint(access.numDimensions());
+		viewer.displayToSourceCoordinates(x, y, labelTransform, pos);
 		for (int d = 0; d < access.numDimensions(); ++d)
-			access.setPosition(Math.round(rp.getDoublePosition(d)), d);
+			access.setPosition(Math.round(pos.getDoublePosition(d)), d);
 		final long seedLabel = access.get().getIntegerLong();
 		LOG.debug("Got seed label {}", seedLabel);
 		final RandomAccessibleInterval<BoolType> relevantBackground = Converters.convert(
@@ -230,7 +218,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 		final int fillNormalAxisInLabelCoordinateSystem = PaintUtils.labelAxisCorrespondingToViewerAxis(labelTransform, viewerTransform, 2);
 		final AccessBoxRandomAccessibleOnGet<UnsignedLongType> accessTracker = new
 				AccessBoxRandomAccessibleOnGet<>(
-				Views.extendValue(mask.mask, new UnsignedLongType(1l)));
+				Views.extendValue(mask.mask, new UnsignedLongType(fillValue)));
 		accessTracker.initAccessBox();
 
 		final Interval affectedInterval;
@@ -246,7 +234,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 					extended.randomAccess(),
 					accessTracker.randomAccess(),
 					new RealPoint(x, y, 0),
-					1l
+					fillValue
 			                              );
 			affectedInterval = new FinalInterval(accessTracker.getMin(), accessTracker.getMax());
 		}
@@ -282,7 +270,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 						relevantBackgroundSlice,
 						relevantAccessTracker,
 						new Point(seed2D),
-						new UnsignedLongType(1l),
+						new UnsignedLongType(fillValue),
 						new DiamondShape(1)
 				              );
 				Arrays.setAll(min, d -> Math.min(accessTracker.getMin()[d], min[d]));
@@ -294,32 +282,6 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 		return affectedInterval;
 	}
 
-	private static RealPoint setCoordinates(
-			final double x,
-			final double y,
-			final ViewerPanelFX viewer,
-			final AffineTransform3D labelTransform)
-	{
-		return setCoordinates(x, y, new RealPoint(labelTransform.numDimensions()), viewer, labelTransform);
-	}
-
-	private static <P extends RealLocalizable & RealPositionable> P setCoordinates(
-			final double x,
-			final double y,
-			final P location,
-			final ViewerPanelFX viewer,
-			final AffineTransform3D labelTransform)
-	{
-		location.setPosition(x, 0);
-		location.setPosition(y, 1);
-		location.setPosition(0, 2);
-
-		viewer.displayToGlobalCoordinates(location);
-		labelTransform.applyInverse(location, location);
-
-		return location;
-	}
-
 	public DoubleProperty fillDepthProperty()
 	{
 		return this.fillDepth;

From e18af82b1e0d77dfdd23c6ab2a56f1a46a86d5fd Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 22 May 2019 14:40:34 -0400
Subject: [PATCH 19/84] allow specifying custom predicate for 2d flood-filling

---
 .../paintera/control/paint/FloodFill2D.java   | 58 +++++++++++++++----
 1 file changed, 46 insertions(+), 12 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
index c2fdde69f..b40673065 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
@@ -95,6 +95,7 @@ public void fillAt(final double x, final double y, final Supplier<Long> fillSupp
 		fillAt(x, y, fill);
 	}
 
+	@SuppressWarnings("unchecked")
 	public <T extends IntegerType<T>> void fillAt(final double x, final double y, final long fill)
 	{
 		final Source<?>   currentSource = sourceInfo.currentSourceProperty().get();
@@ -105,7 +106,7 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi
 			return;
 		}
 
-		@SuppressWarnings("unchecked") final SourceState<T, ?> currentSourceState = (SourceState<T, ?>) sourceInfo
+		final SourceState<T, ?> currentSourceState = (SourceState<T, ?>) sourceInfo
 				.getState(
 				currentSource);
 
@@ -136,7 +137,7 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi
 			return;
 		}
 
-		@SuppressWarnings("unchecked") final MaskedSource<T, ?> source = (MaskedSource<T, ?>) currentSource;
+		final MaskedSource<T, ?> source = (MaskedSource<T, ?>) currentSource;
 
 		final T t = source.getDataType();
 
@@ -196,9 +197,6 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 
 		final AffineTransform3D labelTransform = new AffineTransform3D();
 		source.getSourceTransform(time, level, labelTransform);
-		final AffineTransform3D viewerTransform = new AffineTransform3D();
-		viewer.getState().getViewerTransform(viewerTransform);
-		final AffineTransform3D labelToViewerTransform = viewerTransform.copy().concatenate(labelTransform);
 
 		final RandomAccessibleInterval<T> background = source.getDataSource(time, level);
 		final RandomAccess<T> access = background.randomAccess();
@@ -212,8 +210,44 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 				background,
 				(src, tgt) -> tgt.set(src.getIntegerLong() == seedLabel),
 				new BoolType()
-		                                                                                );
-		final RandomAccessible<BoolType> extended = Views.extendValue(relevantBackground, new BoolType(false));
+			);
+
+		return fillMaskAt(x, y, viewer, mask, relevantBackground, labelTransform, fillValue, fillDepth);
+	}
+
+	/**
+	 * Flood-fills the given mask starting at the specified 2D location in the viewer
+	 * based on the given boolean filter.
+	 * Returns the affected interval in source coordinates.
+	 *
+	 * @param x
+	 * @param y
+	 * @param viewer
+	 * @param mask
+	 * @param filter
+	 * @param labelTransform
+	 * @param fillValue
+	 * @param fillDepth
+	 * @return affected interval
+	 */
+	public static <T extends IntegerType<T>> Interval fillMaskAt(
+			final double x,
+			final double y,
+			final ViewerPanelFX viewer,
+			final Mask<UnsignedLongType> mask,
+			final RandomAccessibleInterval<BoolType> filter,
+			final AffineTransform3D labelTransform,
+			final long fillValue,
+			final double fillDepth)
+	{
+		final AffineTransform3D viewerTransform = new AffineTransform3D();
+		viewer.getState().getViewerTransform(viewerTransform);
+		final AffineTransform3D labelToViewerTransform = viewerTransform.copy().concatenate(labelTransform);
+
+		final RealPoint pos = new RealPoint(labelTransform.numDimensions());
+		viewer.displayToSourceCoordinates(x, y, labelTransform, pos);
+
+		final RandomAccessible<BoolType> extendedFilter = Views.extendValue(filter, new BoolType(false));
 
 		final int fillNormalAxisInLabelCoordinateSystem = PaintUtils.labelAxisCorrespondingToViewerAxis(labelTransform, viewerTransform, 2);
 		final AccessBoxRandomAccessibleOnGet<UnsignedLongType> accessTracker = new
@@ -231,7 +265,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 							labelTransform,
 							viewerTransform
 					                                                                                      )[2],
-					extended.randomAccess(),
+					extendedFilter.randomAccess(),
 					accessTracker.randomAccess(),
 					new RealPoint(x, y, 0),
 					fillValue
@@ -244,11 +278,11 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 					"Flood filling axis aligned. Corressponding viewer axis={}",
 					fillNormalAxisInLabelCoordinateSystem
 			         );
-			final long slicePos  = access.getLongPosition(fillNormalAxisInLabelCoordinateSystem);
+			final long slicePos  = Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem));
 			final long numSlices = Math.max((long) Math.ceil(fillDepth) - 1, 0);
 			final long[] seed2D = {
-					access.getLongPosition(fillNormalAxisInLabelCoordinateSystem == 0 ? 1 : 0),
-					access.getLongPosition(fillNormalAxisInLabelCoordinateSystem != 2 ? 2 : 1)
+					Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem == 0 ? 1 : 0)),
+					Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem != 2 ? 2 : 1))
 			};
 			accessTracker.initAccessBox();
 			final long[] min = {Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE};
@@ -256,7 +290,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 			for (long i = slicePos - numSlices; i <= slicePos + numSlices; ++i)
 			{
 				final MixedTransformView<BoolType> relevantBackgroundSlice = Views.hyperSlice(
-						extended,
+						extendedFilter,
 						fillNormalAxisInLabelCoordinateSystem,
 						i
 				                                                                             );

From d8eee22b6c1d715cee2ab632fc632ad4e84222a8 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 22 May 2019 14:42:02 -0400
Subject: [PATCH 20/84] fix de-selecting objects in shape interpolation mode

Create a custom predicate for flood-filling with the background label to only accept the current fill value. Without it the flood-filling would also deselect adjacent objects if they were active.
---
 .../control/ShapeInterpolationMode.java       | 66 +++++++++++++------
 1 file changed, 45 insertions(+), 21 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 98a061fe8..65d49f06c 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -23,7 +23,6 @@
 import org.slf4j.LoggerFactory;
 
 import bdv.fx.viewer.ViewerPanelFX;
-import bdv.fx.viewer.ViewerState;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.event.Event;
@@ -35,9 +34,12 @@
 import javafx.scene.input.KeyEvent;
 import javafx.scene.paint.Color;
 import net.imglib2.RandomAccess;
+import net.imglib2.RandomAccessibleInterval;
 import net.imglib2.RealPoint;
+import net.imglib2.converter.Converters;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.type.label.Label;
+import net.imglib2.type.logic.BoolType;
 import net.imglib2.type.numeric.IntegerType;
 import net.imglib2.type.numeric.integer.UnsignedLongType;
 
@@ -136,6 +138,11 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				e -> selectObject(paintera, e.getX(), e.getY()),
 				e -> isModeOn() && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
 			);
+		filter.addOnMousePressed(EventFX.MOUSE_PRESSED(
+				"toggle object in current section",
+				e -> selectObject(paintera, e.getX(), e.getY()),
+				e -> isModeOn() && e.isSecondaryButtonDown() && keyTracker.noKeysActive())
+			);
 		return filter;
 	}
 
@@ -204,21 +211,12 @@ public boolean isModeOn()
 
 	private void createMask() throws MaskInUse
 	{
-		final ViewerState viewerState = activeViewer.get().getState();
-		final int time = viewerState.timepointProperty().get();
+		final int time = activeViewer.get().getState().timepointProperty().get();
 		final int level = 0;
-		final long labelId = idService.next();
-		LOG.info("Created new label ID for shape interpolation: {}", labelId);
-
-		final AffineTransform3D labelTransform = new AffineTransform3D();
-		source.getSourceTransform(time, level, labelTransform);
-		final AffineTransform3D viewerTransform = new AffineTransform3D();
-		viewerState.getViewerTransform(viewerTransform);
-		final AffineTransform3D labelToViewerTransform = viewerTransform.copy();
-		labelToViewerTransform.concatenate(labelTransform);
-
-		final MaskInfo<UnsignedLongType> maskInfo = new MaskInfo<>(time, level, new UnsignedLongType(labelId));
+		final long newLabelId = idService.next();
+		final MaskInfo<UnsignedLongType> maskInfo = new MaskInfo<>(time, level, new UnsignedLongType(newLabelId));
 		mask = source.generateMask(maskInfo, FOREGROUND_CHECK);
+		LOG.info("Generated mask for shape interpolation using new label ID {}", newLabelId);
 	}
 
 	private void forgetMask()
@@ -259,8 +257,23 @@ private void fixSelection(final PainteraBaseView paintera)
 
 	private void selectObject(final PainteraBaseView paintera, final double x, final double y)
 	{
-		final long fill = isSelected(x, y) ? Label.BACKGROUND : ++currentFillValue;
-		FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, fill, FILL_DEPTH);
+		if (!isSelected(x, y))
+		{
+			// Flood-fill using new fill value.
+			FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, ++currentFillValue, FILL_DEPTH);
+		}
+		else
+		{
+			// Flood-fill using background value.
+			// The predicate is set to accept only the fill value at the clicked location to avoid deselecting adjacent objects.
+			final long maskValue = getMaskValue(x, y).get();
+			final RandomAccessibleInterval<BoolType> predicate = Converters.convert(
+					mask.mask,
+					(in, out) -> out.set(in.getIntegerLong() == maskValue),
+					new BoolType()
+				);
+			FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, predicate, getMaskTransform(), Label.BACKGROUND, FILL_DEPTH);
+		}
 		activeViewer.get().requestRepaint();
 		hasActiveSelection = true;
 		paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationModeWhenSelected);
@@ -268,15 +281,26 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 
 	private boolean isSelected(final double x, final double y)
 	{
-		final AffineTransform3D labelTransform = new AffineTransform3D();
-		source.getSourceTransform(mask.info.t, mask.info.level, labelTransform);
-		final RealPoint pos = new RealPoint(labelTransform.numDimensions());
-		activeViewer.get().displayToSourceCoordinates(x, y, labelTransform, pos);
+		return FOREGROUND_CHECK.test(getMaskValue(x, y));
+	}
+
+	private UnsignedLongType getMaskValue(final double x, final double y)
+	{
+		final AffineTransform3D maskTransform = getMaskTransform();
+		final RealPoint pos = new RealPoint(maskTransform.numDimensions());
+		activeViewer.get().displayToSourceCoordinates(x, y, maskTransform, pos);
 
 		final RandomAccess<UnsignedLongType> maskAccess = mask.mask.randomAccess();
 		for (int d = 0; d < pos.numDimensions(); ++d)
 			maskAccess.setPosition(Math.round(pos.getDoublePosition(d)), d);
 
-		return FOREGROUND_CHECK.test(maskAccess.get());
+		return maskAccess.get();
+	}
+
+	private AffineTransform3D getMaskTransform()
+	{
+		final AffineTransform3D labelTransform = new AffineTransform3D();
+		source.getSourceTransform(mask.info.t, mask.info.level, labelTransform);
+		return labelTransform;
 	}
 }

From 9ff5b4d7ce1ed74e1e861e67c1b2ddb5a6c140e4 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 22 May 2019 16:02:27 -0400
Subject: [PATCH 21/84] fix click vs. drag mouse event detection in shape
 interpolation mode

---
 .../paintera/control/ShapeInterpolationMode.java       | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 65d49f06c..67397e9a5 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -6,6 +6,7 @@
 import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
 import org.janelia.saalfeldlab.fx.event.EventFX;
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
+import org.janelia.saalfeldlab.fx.event.MouseClickFX;
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
 import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions;
 import org.janelia.saalfeldlab.paintera.control.actions.LabelAction;
@@ -32,6 +33,7 @@
 import javafx.scene.effect.ColorAdjust;
 import javafx.scene.input.KeyCode;
 import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
 import javafx.scene.paint.Color;
 import net.imglib2.RandomAccess;
 import net.imglib2.RandomAccessibleInterval;
@@ -133,16 +135,16 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 						e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE)
 					)
 			);
-		filter.addOnMousePressed(EventFX.MOUSE_PRESSED(
+		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"select object in current section",
 				e -> selectObject(paintera, e.getX(), e.getY()),
 				e -> isModeOn() && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
-			);
-		filter.addOnMousePressed(EventFX.MOUSE_PRESSED(
+			.handler());
+		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"toggle object in current section",
 				e -> selectObject(paintera, e.getX(), e.getY()),
 				e -> isModeOn() && e.isSecondaryButtonDown() && keyTracker.noKeysActive())
-			);
+			.handler());
 		return filter;
 	}
 

From f9db064d9995a5b4635ff118952d0f93e0c08c51 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 22 May 2019 17:14:01 -0400
Subject: [PATCH 22/84] fix select/toggle/deselect objects in shape
 interpolation mode

---
 .../control/ShapeInterpolationMode.java       | 139 +++++++++++++-----
 1 file changed, 101 insertions(+), 38 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 67397e9a5..766f2c0ae 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -24,6 +24,9 @@
 import org.slf4j.LoggerFactory;
 
 import bdv.fx.viewer.ViewerPanelFX;
+import gnu.trove.iterator.TLongObjectIterator;
+import gnu.trove.map.TLongObjectMap;
+import gnu.trove.map.hash.TLongObjectHashMap;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.event.Event;
@@ -44,21 +47,22 @@
 import net.imglib2.type.logic.BoolType;
 import net.imglib2.type.numeric.IntegerType;
 import net.imglib2.type.numeric.integer.UnsignedLongType;
+import net.imglib2.util.Util;
 
 public class ShapeInterpolationMode<D extends IntegerType<D>>
 {
 	private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
-	private static final AllowedActions allowedActionsInShapeInterpolationMode;
-	private static final AllowedActions allowedActionsInShapeInterpolationModeWhenSelected;
+	private static final AllowedActions allowedActions;
+	private static final AllowedActions allowedActionsWhenSelected;
 	static
 	{
-		allowedActionsInShapeInterpolationMode = new AllowedActions(
+		allowedActions = new AllowedActions(
 			NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll),
 			LabelAction.none(),
 			PaintAction.none()
 		);
-		allowedActionsInShapeInterpolationModeWhenSelected = new AllowedActions(
+		allowedActionsWhenSelected = new AllowedActions(
 				NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom),
 				LabelAction.none(),
 				PaintAction.none()
@@ -84,7 +88,8 @@ public class ShapeInterpolationMode<D extends IntegerType<D>>
 
 	private Mask<UnsignedLongType> mask;
 	private int currentFillValue;
-	private boolean hasActiveSelection;
+
+	private final TLongObjectMap<RealPoint> selectedObjects = new TLongObjectHashMap<>();
 
 	public ShapeInterpolationMode(
 			final MaskedSource<D, ?> source,
@@ -123,7 +128,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 						"fix selection",
 						e -> fixSelection(paintera),
 						e -> isModeOn() &&
-							hasActiveSelection &&
+							!selectedObjects.isEmpty() &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
 					)
 			);
@@ -137,12 +142,12 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 			);
 		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"select object in current section",
-				e -> selectObject(paintera, e.getX(), e.getY()),
+				e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), true);},
 				e -> isModeOn() && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
 			.handler());
 		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"toggle object in current section",
-				e -> selectObject(paintera, e.getX(), e.getY()),
+				e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), false);},
 				e -> isModeOn() && e.isSecondaryButtonDown() && keyTracker.noKeysActive())
 			.handler());
 		return filter;
@@ -160,7 +165,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 		setDisableOtherViewers(true);
 
 		lastAllowedActions = paintera.allowedActionsProperty().get();
-		paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationMode);
+		paintera.allowedActionsProperty().set(allowedActions);
 
 		try
 		{
@@ -170,8 +175,8 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 			final long newLabelId = mask.info.value.get();
 			converter.setColor(newLabelId, MASK_COLOR);
 			selectedIds.activate(newLabelId);
-			hasActiveSelection = false;
 			currentFillValue = 0;
+			selectedObjects.clear();
 		}
 		catch (final MaskInUse e)
 		{
@@ -198,8 +203,8 @@ public void exitMode(final PainteraBaseView paintera)
 		selectedIds.activateAlso(lastSelectedId);
 		lastSelectedId = Label.INVALID;
 		lastActiveIds = null;
-		hasActiveSelection = false;
 		currentFillValue = 0;
+		selectedObjects.clear();
 		forgetMask();
 		activeViewer.get().requestRepaint();
 
@@ -253,32 +258,74 @@ private void setDisableOtherViewers(final boolean disable)
 
 	private void fixSelection(final PainteraBaseView paintera)
 	{
-		hasActiveSelection = false;
-		paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationMode);
+		selectedObjects.clear();
+		paintera.allowedActionsProperty().set(allowedActions);
 	}
 
-	private void selectObject(final PainteraBaseView paintera, final double x, final double y)
+	private void selectObject(final PainteraBaseView paintera, final double x, final double y, final boolean deactivateOthers)
 	{
-		if (!isSelected(x, y))
+		final boolean wasSelected = isSelected(x, y);
+		final int numSelectedObjects = selectedObjects.size();
+
+		LOG.debug("Object was clicked: deactivateOthers={}, wasSelected={}, numSelectedObjects", deactivateOthers, wasSelected, numSelectedObjects);
+
+		if (deactivateOthers)
+		{
+			for (final TLongObjectIterator<RealPoint> it = selectedObjects.iterator(); it.hasNext();)
+			{
+				it.advance();
+				final double[] deselectDisplayPos = getDisplayCoordinates(it.value());
+				runFloodFillToDeselect(deselectDisplayPos[0], deselectDisplayPos[1]);
+			}
+			selectedObjects.clear();
+		}
+
+		if (!wasSelected || (deactivateOthers && numSelectedObjects > 1))
 		{
-			// Flood-fill using new fill value.
-			FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, ++currentFillValue, FILL_DEPTH);
+			final long newFillValue = runFloodFillToSelect(x, y);
+			selectedObjects.put(newFillValue, getSourceCoordinates(x, y));
 		}
 		else
 		{
-			// Flood-fill using background value.
-			// The predicate is set to accept only the fill value at the clicked location to avoid deselecting adjacent objects.
-			final long maskValue = getMaskValue(x, y).get();
-			final RandomAccessibleInterval<BoolType> predicate = Converters.convert(
-					mask.mask,
-					(in, out) -> out.set(in.getIntegerLong() == maskValue),
-					new BoolType()
-				);
-			FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, predicate, getMaskTransform(), Label.BACKGROUND, FILL_DEPTH);
+			final long oldFillValue = runFloodFillToDeselect(x, y);
+			selectedObjects.remove(oldFillValue);
 		}
+
 		activeViewer.get().requestRepaint();
-		hasActiveSelection = true;
-		paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationModeWhenSelected);
+		paintera.allowedActionsProperty().set(selectedObjects.isEmpty() ? allowedActions : allowedActionsWhenSelected);
+	}
+
+	/**
+	 * Flood-fills the mask using a new fill value to mark the object as selected.
+	 *
+	 * @param x
+	 * @param y
+	 * @return the fill value of the selected object
+	 */
+	private long runFloodFillToSelect(final double x, final double y)
+	{
+		FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, ++currentFillValue, FILL_DEPTH);
+		return currentFillValue;
+	}
+
+	/**
+	 * Flood-fills the mask using a background value to remove the object from the selection.
+	 *
+	 * @param x
+	 * @param y
+	 * @return the fill value of the deselected object
+	 */
+	private long runFloodFillToDeselect(final double x, final double y)
+	{
+		// set the predicate to accept only the fill value at the clicked location to avoid deselecting adjacent objects.
+		final long maskValue = getMaskValue(x, y).get();
+		final RandomAccessibleInterval<BoolType> predicate = Converters.convert(
+				mask.mask,
+				(in, out) -> out.set(in.getIntegerLong() == maskValue),
+				new BoolType()
+			);
+		FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, predicate, getMaskTransform(), Label.BACKGROUND, FILL_DEPTH);
+		return maskValue;
 	}
 
 	private boolean isSelected(final double x, final double y)
@@ -288,21 +335,37 @@ private boolean isSelected(final double x, final double y)
 
 	private UnsignedLongType getMaskValue(final double x, final double y)
 	{
-		final AffineTransform3D maskTransform = getMaskTransform();
-		final RealPoint pos = new RealPoint(maskTransform.numDimensions());
-		activeViewer.get().displayToSourceCoordinates(x, y, maskTransform, pos);
-
+		final RealPoint sourcePos = getSourceCoordinates(x, y);
 		final RandomAccess<UnsignedLongType> maskAccess = mask.mask.randomAccess();
-		for (int d = 0; d < pos.numDimensions(); ++d)
-			maskAccess.setPosition(Math.round(pos.getDoublePosition(d)), d);
-
+		for (int d = 0; d < sourcePos.numDimensions(); ++d)
+			maskAccess.setPosition(Math.round(sourcePos.getDoublePosition(d)), d);
 		return maskAccess.get();
 	}
 
 	private AffineTransform3D getMaskTransform()
 	{
-		final AffineTransform3D labelTransform = new AffineTransform3D();
-		source.getSourceTransform(mask.info.t, mask.info.level, labelTransform);
-		return labelTransform;
+		final AffineTransform3D maskTransform = new AffineTransform3D();
+		source.getSourceTransform(mask.info.t, mask.info.level, maskTransform);
+		return maskTransform;
+	}
+
+	private RealPoint getSourceCoordinates(final double x, final double y)
+	{
+		final AffineTransform3D maskTransform = getMaskTransform();
+		final RealPoint sourcePos = new RealPoint(maskTransform.numDimensions());
+		activeViewer.get().displayToSourceCoordinates(x, y, maskTransform, sourcePos);
+		return sourcePos;
+	}
+
+	private double[] getDisplayCoordinates(final RealPoint sourcePos)
+	{
+		final AffineTransform3D maskTransform = getMaskTransform();
+		final RealPoint displayPos = new RealPoint(maskTransform.numDimensions());
+		final AffineTransform3D viewerTransform = new AffineTransform3D();
+		activeViewer.get().getState().getViewerTransform(viewerTransform);
+		final AffineTransform3D maskToViewerTransform = viewerTransform.copy().concatenate(maskTransform);
+		maskToViewerTransform.apply(sourcePos, displayPos);
+		assert Util.isApproxEqual(displayPos.getDoublePosition(2), 0.0, 1e-10);
+		return new double[] {displayPos.getDoublePosition(0), displayPos.getDoublePosition(1)};
 	}
 }

From 118d3f2407c8046070a6f82713fb0fba8c1fe50a Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Thu, 23 May 2019 11:11:46 -0400
Subject: [PATCH 23/84] allow ctrl+left click to toggle objects in shape
 interpolation mode

---
 .../saalfeldlab/paintera/control/ShapeInterpolationMode.java  | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 766f2c0ae..47f68c104 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -148,7 +148,9 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"toggle object in current section",
 				e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), false);},
-				e -> isModeOn() && e.isSecondaryButtonDown() && keyTracker.noKeysActive())
+				e -> isModeOn() &&
+					((e.isSecondaryButtonDown() && keyTracker.noKeysActive()) ||
+					(e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL))))
 			.handler());
 		return filter;
 	}

From d652a3773b7308e45e0e1e17599661402aec3b6c Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 24 May 2019 09:52:19 -0400
Subject: [PATCH 24/84] fix section by copying object selection from the mask
 in the current view

---
 .../control/ShapeInterpolationMode.java       | 116 ++++++++++++++++--
 1 file changed, 105 insertions(+), 11 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 47f68c104..300744fc9 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -1,6 +1,7 @@
 package org.janelia.saalfeldlab.paintera.control;
 
 import java.lang.invoke.MethodHandles;
+import java.util.Arrays;
 import java.util.function.Predicate;
 
 import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
@@ -14,6 +15,7 @@
 import org.janelia.saalfeldlab.paintera.control.actions.PaintAction;
 import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D;
 import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
+import org.janelia.saalfeldlab.paintera.data.DataSource;
 import org.janelia.saalfeldlab.paintera.data.mask.Mask;
 import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo;
 import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
@@ -24,6 +26,7 @@
 import org.slf4j.LoggerFactory;
 
 import bdv.fx.viewer.ViewerPanelFX;
+import bdv.util.Affine3DHelpers;
 import gnu.trove.iterator.TLongObjectIterator;
 import gnu.trove.map.TLongObjectMap;
 import gnu.trove.map.hash.TLongObjectHashMap;
@@ -38,16 +41,26 @@
 import javafx.scene.input.KeyEvent;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.paint.Color;
+import net.imglib2.Cursor;
+import net.imglib2.Interval;
 import net.imglib2.RandomAccess;
 import net.imglib2.RandomAccessibleInterval;
 import net.imglib2.RealPoint;
+import net.imglib2.RealRandomAccessible;
 import net.imglib2.converter.Converters;
+import net.imglib2.img.array.ArrayImgs;
+import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory;
 import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.realtransform.RealViews;
+import net.imglib2.realtransform.Scale3D;
 import net.imglib2.type.label.Label;
 import net.imglib2.type.logic.BoolType;
+import net.imglib2.type.logic.NativeBoolType;
 import net.imglib2.type.numeric.IntegerType;
 import net.imglib2.type.numeric.integer.UnsignedLongType;
+import net.imglib2.util.Intervals;
 import net.imglib2.util.Util;
+import net.imglib2.view.Views;
 
 public class ShapeInterpolationMode<D extends IntegerType<D>>
 {
@@ -71,6 +84,10 @@ public class ShapeInterpolationMode<D extends IntegerType<D>>
 
 	private static final double FILL_DEPTH = 2.0;
 
+	private static final int MASK_SCALE_LEVEL = 0; // NOTE: getMaskScaledDisplayTransform() would need to change if it is needed to support masks created at arbitrary scale level
+
+	private static final int SHAPE_INTERPOLATION_SCALE_LEVEL = 0;
+
 	private static final Color MASK_COLOR = Color.web("00CCFF");
 
 	private static final Predicate<UnsignedLongType> FOREGROUND_CHECK = t -> t.get() > 0;
@@ -91,6 +108,9 @@ public class ShapeInterpolationMode<D extends IntegerType<D>>
 
 	private final TLongObjectMap<RealPoint> selectedObjects = new TLongObjectHashMap<>();
 
+	private RandomAccessibleInterval<NativeBoolType> section1;
+	private RandomAccessibleInterval<NativeBoolType> section2;
+
 	public ShapeInterpolationMode(
 			final MaskedSource<D, ?> source,
 			final SelectedIds selectedIds,
@@ -177,8 +197,6 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 			final long newLabelId = mask.info.value.get();
 			converter.setColor(newLabelId, MASK_COLOR);
 			selectedIds.activate(newLabelId);
-			currentFillValue = 0;
-			selectedObjects.clear();
 		}
 		catch (final MaskInUse e)
 		{
@@ -207,6 +225,8 @@ public void exitMode(final PainteraBaseView paintera)
 		lastActiveIds = null;
 		currentFillValue = 0;
 		selectedObjects.clear();
+		section1 = null;
+		section2 = null;
 		forgetMask();
 		activeViewer.get().requestRepaint();
 
@@ -221,7 +241,7 @@ public boolean isModeOn()
 	private void createMask() throws MaskInUse
 	{
 		final int time = activeViewer.get().getState().timepointProperty().get();
-		final int level = 0;
+		final int level = MASK_SCALE_LEVEL;
 		final long newLabelId = idService.next();
 		final MaskInfo<UnsignedLongType> maskInfo = new MaskInfo<>(time, level, new UnsignedLongType(newLabelId));
 		mask = source.generateMask(maskInfo, FOREGROUND_CHECK);
@@ -260,8 +280,52 @@ private void setDisableOtherViewers(final boolean disable)
 
 	private void fixSelection(final PainteraBaseView paintera)
 	{
-		selectedObjects.clear();
-		paintera.allowedActionsProperty().set(allowedActions);
+		if (section1 == null)
+		{
+			section1 = writeCurrentSelectionToImage();
+			selectedObjects.clear();
+			paintera.allowedActionsProperty().set(allowedActions);
+		}
+		else
+		{
+			section2 = writeCurrentSelectionToImage();
+			interpolateBetweenSections(paintera);
+		}
+	}
+
+	private RandomAccessibleInterval<NativeBoolType> writeCurrentSelectionToImage()
+	{
+		final AffineTransform3D maskScaledDisplayTransform = getMaskScaledDisplayTransform(SHAPE_INTERPOLATION_SCALE_LEVEL);
+
+		final RandomAccessibleInterval<BoolType> selectionMask = Converters.convert(
+				mask.mask,
+				(in, out) -> out.set(in.getIntegerLong() != 0 && in.getIntegerLong() <= currentFillValue),
+				new BoolType()
+			);
+		final RealRandomAccessible<BoolType> realSelectionMask = Views.interpolate(
+				Views.extendValue(selectionMask, new BoolType()),
+				new NearestNeighborInterpolatorFactory<>()
+			);
+		final RealRandomAccessible<BoolType> transformedSelectionMask = RealViews.affine(realSelectionMask, maskScaledDisplayTransform );
+		final Interval displayBoundingBox = Intervals.smallestContainingInterval(maskScaledDisplayTransform.estimateBounds(mask.mask));
+		final RandomAccessibleInterval<BoolType> transformedSelectionMaskInterval = Views.interval(Views.raster(transformedSelectionMask), displayBoundingBox);
+		final RandomAccessibleInterval<BoolType> src = Views.hyperSlice(transformedSelectionMaskInterval, 2, 0l);
+
+		final RandomAccessibleInterval<NativeBoolType> dst = ArrayImgs.booleans(Intervals.dimensionsAsLongArray(src));
+		LOG.debug("Copying the current selection into an image of size {}", Intervals.dimensionsAsLongArray(dst));
+		System.out.println("Copying the current selection into an image of size " + Arrays.toString(Intervals.dimensionsAsLongArray(dst)));
+		final Cursor<BoolType> srcCursor = Views.flatIterable(src).cursor();
+		final Cursor<NativeBoolType> dstCursor = Views.flatIterable(dst).cursor();
+		while (dstCursor.hasNext() || srcCursor.hasNext())
+			dstCursor.next().set(srcCursor.next().get());
+
+		return dst;
+	}
+
+	private void interpolateBetweenSections(final PainteraBaseView paintera)
+	{
+		assert section1 != null && section2 != null;
+		throw new RuntimeException("TODO");
 	}
 
 	private void selectObject(final PainteraBaseView paintera, final double x, final double y, final boolean deactivateOthers)
@@ -351,6 +415,39 @@ private AffineTransform3D getMaskTransform()
 		return maskTransform;
 	}
 
+	private AffineTransform3D getMaskDisplayTransform()
+	{
+		final AffineTransform3D viewerTransform = new AffineTransform3D();
+		activeViewer.get().getState().getViewerTransform(viewerTransform);
+		return viewerTransform.concatenate(getMaskTransform());
+	}
+
+	/**
+	 * Returns the transformation to map the mask at full resolution to the viewer plane.
+	 * Ignores the current viewer scaling and instead can downscale the resulting image with respect to the requested mipmap level.
+	 *
+	 * NOTE: only works when the mask is created at scale level 0 (full resolution)!
+	 * The way the viewer scaling is ignored would need to change if it is needed to support masks created at arbitrary scale level.
+	 *
+	 * @param level
+	 * @return
+	 */
+	private AffineTransform3D getMaskScaledDisplayTransform(final int level)
+	{
+		final AffineTransform3D viewerTransform = new AffineTransform3D();
+		activeViewer.get().getState().getViewerTransform(viewerTransform);
+		// undo the scaling in the viewer
+		final double[] viewerScale = new double[viewerTransform.numDimensions()];
+		Arrays.setAll(viewerScale, d -> Affine3DHelpers.extractScale(viewerTransform, d));
+		final Scale3D viewerScaleTransform = new Scale3D(viewerScale);
+		viewerTransform.preConcatenate(viewerScaleTransform.inverse());
+		// build the scaling transform to account for the given scale level
+		final Scale3D maskScaleTransform = new Scale3D(DataSource.getRelativeScales(source, mask.info.t, mask.info.level, level));
+		viewerTransform.preConcatenate(maskScaleTransform.inverse());
+		// build the resulting transform
+		return viewerTransform.concatenate(getMaskTransform());
+	}
+
 	private RealPoint getSourceCoordinates(final double x, final double y)
 	{
 		final AffineTransform3D maskTransform = getMaskTransform();
@@ -361,12 +458,9 @@ private RealPoint getSourceCoordinates(final double x, final double y)
 
 	private double[] getDisplayCoordinates(final RealPoint sourcePos)
 	{
-		final AffineTransform3D maskTransform = getMaskTransform();
-		final RealPoint displayPos = new RealPoint(maskTransform.numDimensions());
-		final AffineTransform3D viewerTransform = new AffineTransform3D();
-		activeViewer.get().getState().getViewerTransform(viewerTransform);
-		final AffineTransform3D maskToViewerTransform = viewerTransform.copy().concatenate(maskTransform);
-		maskToViewerTransform.apply(sourcePos, displayPos);
+		final AffineTransform3D maskDisplayTransform = getMaskDisplayTransform();
+		final RealPoint displayPos = new RealPoint(maskDisplayTransform.numDimensions());
+		maskDisplayTransform.apply(sourcePos, displayPos);
 		assert Util.isApproxEqual(displayPos.getDoublePosition(2), 0.0, 1e-10);
 		return new double[] {displayPos.getDoublePosition(0), displayPos.getDoublePosition(1)};
 	}

From 652c8d30547595ccff49f282bea47ab12da4527e Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 28 May 2019 14:49:21 -0400
Subject: [PATCH 25/84] keep track of the bounding box of a selection in shape
 interpolation mode

---
 .../control/ShapeInterpolationMode.java       | 89 ++++++++++++++-----
 1 file changed, 67 insertions(+), 22 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 300744fc9..d38ca2936 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -59,13 +59,39 @@
 import net.imglib2.type.numeric.IntegerType;
 import net.imglib2.type.numeric.integer.UnsignedLongType;
 import net.imglib2.util.Intervals;
+import net.imglib2.util.Pair;
 import net.imglib2.util.Util;
+import net.imglib2.util.ValuePair;
 import net.imglib2.view.Views;
 
 public class ShapeInterpolationMode<D extends IntegerType<D>>
 {
 	private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
+	private static final class SelectedObjectInfo
+	{
+		final RealPoint sourceClickPosition;
+		final Interval sourceBoundingBox;
+
+		SelectedObjectInfo(final RealPoint sourceClickPosition, final Interval sourceBoundingBox)
+		{
+			this.sourceClickPosition = sourceClickPosition;
+			this.sourceBoundingBox = sourceBoundingBox;
+		}
+	}
+
+	private static final class SectionInfo
+	{
+		final AffineTransform3D sourceToDisplayTransform;
+		final Interval sourceBoundingBox;
+
+		SectionInfo(final AffineTransform3D sourceToDisplayTransform, final Interval sourceBoundingBox)
+		{
+			this.sourceToDisplayTransform = sourceToDisplayTransform;
+			this.sourceBoundingBox = sourceBoundingBox;
+		}
+	}
+
 	private static final AllowedActions allowedActions;
 	private static final AllowedActions allowedActionsWhenSelected;
 	static
@@ -104,12 +130,11 @@ public class ShapeInterpolationMode<D extends IntegerType<D>>
 	private long[] lastActiveIds;
 
 	private Mask<UnsignedLongType> mask;
-	private int currentFillValue;
+	private long currentFillValue;
 
-	private final TLongObjectMap<RealPoint> selectedObjects = new TLongObjectHashMap<>();
+	private final TLongObjectMap<SelectedObjectInfo> selectedObjects = new TLongObjectHashMap<>();
 
-	private RandomAccessibleInterval<NativeBoolType> section1;
-	private RandomAccessibleInterval<NativeBoolType> section2;
+	private SectionInfo section1, section2;
 
 	public ShapeInterpolationMode(
 			final MaskedSource<D, ?> source,
@@ -225,8 +250,7 @@ public void exitMode(final PainteraBaseView paintera)
 		lastActiveIds = null;
 		currentFillValue = 0;
 		selectedObjects.clear();
-		section1 = null;
-		section2 = null;
+		section1 = section2 = null;
 		forgetMask();
 		activeViewer.get().requestRepaint();
 
@@ -282,32 +306,53 @@ private void fixSelection(final PainteraBaseView paintera)
 	{
 		if (section1 == null)
 		{
-			section1 = writeCurrentSelectionToImage();
+			LOG.debug("Fix selection in the first section");
+			section1 = createSectionInfo();
 			selectedObjects.clear();
 			paintera.allowedActionsProperty().set(allowedActions);
 		}
 		else
 		{
-			section2 = writeCurrentSelectionToImage();
+			LOG.debug("Fix selection in the second section");
+			section2 = createSectionInfo();
 			interpolateBetweenSections(paintera);
 		}
 	}
 
-	private RandomAccessibleInterval<NativeBoolType> writeCurrentSelectionToImage()
+	private SectionInfo createSectionInfo()
 	{
-		final AffineTransform3D maskScaledDisplayTransform = getMaskScaledDisplayTransform(SHAPE_INTERPOLATION_SCALE_LEVEL);
+		Interval selectionSourceBoundingBox = null;
+		for (final TLongObjectIterator<SelectedObjectInfo> it = selectedObjects.iterator(); it.hasNext();)
+		{
+			it.advance();
+			if (selectionSourceBoundingBox == null)
+				selectionSourceBoundingBox = it.value().sourceBoundingBox;
+			else
+				selectionSourceBoundingBox = Intervals.union(selectionSourceBoundingBox, it.value().sourceBoundingBox);
+		}
+		return new SectionInfo(
+				getMaskScaledDisplayTransform(SHAPE_INTERPOLATION_SCALE_LEVEL),
+				selectionSourceBoundingBox
+			);
+	}
+
+	private RandomAccessibleInterval<NativeBoolType> writeSectionToImage(final SectionInfo sectionInfo)
+	{
+		final AffineTransform3D maskScaledDisplayTransform = sectionInfo.sourceToDisplayTransform;
+		final Interval sourceSelectionBoundingBox = sectionInfo.sourceBoundingBox;
 
+		final RandomAccessibleInterval<UnsignedLongType> maskInterval = Views.interval(mask.mask, sourceSelectionBoundingBox);
 		final RandomAccessibleInterval<BoolType> selectionMask = Converters.convert(
-				mask.mask,
+				maskInterval,
 				(in, out) -> out.set(in.getIntegerLong() != 0 && in.getIntegerLong() <= currentFillValue),
 				new BoolType()
 			);
 		final RealRandomAccessible<BoolType> realSelectionMask = Views.interpolate(
-				Views.extendValue(selectionMask, new BoolType()),
+				Views.extendZero(selectionMask),
 				new NearestNeighborInterpolatorFactory<>()
 			);
-		final RealRandomAccessible<BoolType> transformedSelectionMask = RealViews.affine(realSelectionMask, maskScaledDisplayTransform );
-		final Interval displayBoundingBox = Intervals.smallestContainingInterval(maskScaledDisplayTransform.estimateBounds(mask.mask));
+		final RealRandomAccessible<BoolType> transformedSelectionMask = RealViews.affine(realSelectionMask, maskScaledDisplayTransform);
+		final Interval displayBoundingBox = Intervals.smallestContainingInterval(maskScaledDisplayTransform.estimateBounds(sourceSelectionBoundingBox));
 		final RandomAccessibleInterval<BoolType> transformedSelectionMaskInterval = Views.interval(Views.raster(transformedSelectionMask), displayBoundingBox);
 		final RandomAccessibleInterval<BoolType> src = Views.hyperSlice(transformedSelectionMaskInterval, 2, 0l);
 
@@ -337,10 +382,10 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 
 		if (deactivateOthers)
 		{
-			for (final TLongObjectIterator<RealPoint> it = selectedObjects.iterator(); it.hasNext();)
+			for (final TLongObjectIterator<SelectedObjectInfo> it = selectedObjects.iterator(); it.hasNext();)
 			{
 				it.advance();
-				final double[] deselectDisplayPos = getDisplayCoordinates(it.value());
+				final double[] deselectDisplayPos = getDisplayCoordinates(it.value().sourceClickPosition);
 				runFloodFillToDeselect(deselectDisplayPos[0], deselectDisplayPos[1]);
 			}
 			selectedObjects.clear();
@@ -348,8 +393,8 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 
 		if (!wasSelected || (deactivateOthers && numSelectedObjects > 1))
 		{
-			final long newFillValue = runFloodFillToSelect(x, y);
-			selectedObjects.put(newFillValue, getSourceCoordinates(x, y));
+			final Pair<Long, Interval> fillValueAndInterval = runFloodFillToSelect(x, y);
+			selectedObjects.put(fillValueAndInterval.getA(), new SelectedObjectInfo(getSourceCoordinates(x, y), fillValueAndInterval.getB()));
 		}
 		else
 		{
@@ -366,12 +411,12 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 	 *
 	 * @param x
 	 * @param y
-	 * @return the fill value of the selected object
+	 * @return the fill value of the selected object and the affected interval in source coordinates
 	 */
-	private long runFloodFillToSelect(final double x, final double y)
+	private Pair<Long, Interval> runFloodFillToSelect(final double x, final double y)
 	{
-		FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, ++currentFillValue, FILL_DEPTH);
-		return currentFillValue;
+		final Interval affectedInterval = FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, ++currentFillValue, FILL_DEPTH);
+		return new ValuePair<>(currentFillValue, affectedInterval);
 	}
 
 	/**

From 63ba0df7166a4e3310167f764859c86af2a5cfa3 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 28 May 2019 14:54:33 -0400
Subject: [PATCH 26/84] transform mask section as is without any unwanted
 scaling

For example, when the transform of a masked source is 4,4,40 at scale level 0, the same scaling was applied to the mask. Now the mask image is transformed directly without any extra scaling. (the mask is scaled only if a different mipmap level is requested)
---
 .../control/ShapeInterpolationMode.java       | 57 ++++++++++++-------
 1 file changed, 37 insertions(+), 20 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index d38ca2936..9c49478a0 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -110,9 +110,9 @@ private static final class SectionInfo
 
 	private static final double FILL_DEPTH = 2.0;
 
-	private static final int MASK_SCALE_LEVEL = 0; // NOTE: getMaskScaledDisplayTransform() would need to change if it is needed to support masks created at arbitrary scale level
+	private static final int MASK_SCALE_LEVEL = 0;
 
-	private static final int SHAPE_INTERPOLATION_SCALE_LEVEL = 0;
+	private static final int SHAPE_INTERPOLATION_SCALE_LEVEL = MASK_SCALE_LEVEL;
 
 	private static final Color MASK_COLOR = Color.web("00CCFF");
 
@@ -331,7 +331,7 @@ private SectionInfo createSectionInfo()
 				selectionSourceBoundingBox = Intervals.union(selectionSourceBoundingBox, it.value().sourceBoundingBox);
 		}
 		return new SectionInfo(
-				getMaskScaledDisplayTransform(SHAPE_INTERPOLATION_SCALE_LEVEL),
+				getMaskDisplayTransformIgnoreScaling(SHAPE_INTERPOLATION_SCALE_LEVEL),
 				selectionSourceBoundingBox
 			);
 	}
@@ -460,37 +460,54 @@ private AffineTransform3D getMaskTransform()
 		return maskTransform;
 	}
 
-	private AffineTransform3D getMaskDisplayTransform()
+	private AffineTransform3D getDisplayTransform()
 	{
 		final AffineTransform3D viewerTransform = new AffineTransform3D();
 		activeViewer.get().getState().getViewerTransform(viewerTransform);
-		return viewerTransform.concatenate(getMaskTransform());
+		return viewerTransform;
+	}
+
+	private AffineTransform3D getMaskDisplayTransform()
+	{
+		return getDisplayTransform().concatenate(getMaskTransform());
 	}
 
 	/**
-	 * Returns the transformation to map the mask at full resolution to the viewer plane.
-	 * Ignores the current viewer scaling and instead can downscale the resulting image with respect to the requested mipmap level.
-	 *
-	 * NOTE: only works when the mask is created at scale level 0 (full resolution)!
-	 * The way the viewer scaling is ignored would need to change if it is needed to support masks created at arbitrary scale level.
+	 * Returns the transformation to bring the mask to the current viewer plane at the requested mipmap level.
+	 * Ignores the scaling in the viewer and in the mask and instead uses the requested mipmap level for scaling.
 	 *
 	 * @param level
 	 * @return
 	 */
-	private AffineTransform3D getMaskScaledDisplayTransform(final int level)
+	private AffineTransform3D getMaskDisplayTransformIgnoreScaling(final int level)
 	{
-		final AffineTransform3D viewerTransform = new AffineTransform3D();
-		activeViewer.get().getState().getViewerTransform(viewerTransform);
-		// undo the scaling in the viewer
+		final AffineTransform3D maskMipmapDisplayTransform = getMaskDisplayTransformIgnoreScaling();
+		if (level != mask.info.level)
+		{
+			// scale with respect to the given mipmap level
+			final Scale3D relativeScaleTransform = new Scale3D(DataSource.getRelativeScales(source, mask.info.t, level, mask.info.level));
+			maskMipmapDisplayTransform.preConcatenate(relativeScaleTransform);
+		}
+		return maskMipmapDisplayTransform;
+	}
+
+	/**
+	 * Returns the transformation to bring the mask to the current viewer plane.
+	 * Ignores the scaling in the viewer and in the mask.
+	 *
+	 * @return
+	 */
+	private AffineTransform3D getMaskDisplayTransformIgnoreScaling()
+	{
+		final AffineTransform3D viewerTransform = getDisplayTransform();
+		// undo scaling in the viewer
 		final double[] viewerScale = new double[viewerTransform.numDimensions()];
 		Arrays.setAll(viewerScale, d -> Affine3DHelpers.extractScale(viewerTransform, d));
-		final Scale3D viewerScaleTransform = new Scale3D(viewerScale);
-		viewerTransform.preConcatenate(viewerScaleTransform.inverse());
-		// build the scaling transform to account for the given scale level
-		final Scale3D maskScaleTransform = new Scale3D(DataSource.getRelativeScales(source, mask.info.t, mask.info.level, level));
-		viewerTransform.preConcatenate(maskScaleTransform.inverse());
+		final Scale3D scalingTransform = new Scale3D(viewerScale);
+		// neutralize mask scaling if there is any
+		scalingTransform.concatenate(new Scale3D(DataSource.getScale(source, mask.info.t, mask.info.level)));
 		// build the resulting transform
-		return viewerTransform.concatenate(getMaskTransform());
+		return viewerTransform.preConcatenate(scalingTransform.inverse()).concatenate(getMaskTransform());
 	}
 
 	private RealPoint getSourceCoordinates(final double x, final double y)

From 1e5b7e8c864d4ac1e4fa3885104da34affe4f73f Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 29 May 2019 13:38:18 -0400
Subject: [PATCH 27/84] first working version of shape interpolation

---
 pom.xml                                       |   2 +-
 .../control/ShapeInterpolationMode.java       | 223 ++++++++++++++----
 2 files changed, 181 insertions(+), 44 deletions(-)

diff --git a/pom.xml b/pom.xml
index ccdfe4fe5..917e8ba4c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -119,7 +119,7 @@
 		<dependency>
 			<groupId>org.janelia.saalfeldlab</groupId>
 			<artifactId>label-utilities</artifactId>
-			<version>0.2.1</version>
+			<version>0.4.1</version>
 		</dependency>
 		<dependency>
 			<groupId>org.janelia.saalfeldlab</groupId>
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 9c49478a0..89a7c5c90 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -1,13 +1,20 @@
 package org.janelia.saalfeldlab.paintera.control;
 
+import java.io.IOException;
 import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.function.Predicate;
 
 import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
 import org.janelia.saalfeldlab.fx.event.EventFX;
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.fx.event.MouseClickFX;
+import org.janelia.saalfeldlab.labels.InterpolateBetweenSections;
+import org.janelia.saalfeldlab.n5.GzipCompression;
+import org.janelia.saalfeldlab.n5.N5FSWriter;
+import org.janelia.saalfeldlab.n5.imglib2.N5Utils;
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
 import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions;
 import org.janelia.saalfeldlab.paintera.control.actions.LabelAction;
@@ -41,23 +48,27 @@
 import javafx.scene.input.KeyEvent;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.paint.Color;
-import net.imglib2.Cursor;
+import net.imglib2.FinalInterval;
 import net.imglib2.Interval;
 import net.imglib2.RandomAccess;
 import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.RealInterval;
 import net.imglib2.RealPoint;
 import net.imglib2.RealRandomAccessible;
+import net.imglib2.algorithm.morphology.distance.DistanceTransform.DISTANCE_TYPE;
 import net.imglib2.converter.Converters;
-import net.imglib2.img.array.ArrayImgs;
+import net.imglib2.img.array.ArrayImgFactory;
 import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.realtransform.RealViews;
 import net.imglib2.realtransform.Scale3D;
+import net.imglib2.realtransform.Translation3D;
 import net.imglib2.type.label.Label;
 import net.imglib2.type.logic.BoolType;
-import net.imglib2.type.logic.NativeBoolType;
 import net.imglib2.type.numeric.IntegerType;
 import net.imglib2.type.numeric.integer.UnsignedLongType;
+import net.imglib2.type.numeric.integer.UnsignedShortType;
+import net.imglib2.type.numeric.real.DoubleType;
 import net.imglib2.util.Intervals;
 import net.imglib2.util.Pair;
 import net.imglib2.util.Util;
@@ -68,6 +79,13 @@ public class ShapeInterpolationMode<D extends IntegerType<D>>
 {
 	private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
+	private static enum ModeState
+	{
+		Selecting,
+		Interpolating,
+		Review
+	}
+
 	private static final class SelectedObjectInfo
 	{
 		final RealPoint sourceClickPosition;
@@ -118,6 +136,8 @@ private static final class SectionInfo
 
 	private static final Predicate<UnsignedLongType> FOREGROUND_CHECK = t -> t.get() > 0;
 
+	private static final long INTERPOLATION_FILL_VALUE = 1;
+
 	private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>();
 
 	private final MaskedSource<D, ?> source;
@@ -134,7 +154,9 @@ private static final class SectionInfo
 
 	private final TLongObjectMap<SelectedObjectInfo> selectedObjects = new TLongObjectHashMap<>();
 
-	private SectionInfo section1, section2;
+	private SectionInfo sectionInfo1, sectionInfo2;
+
+	private ModeState modeState;
 
 	public ShapeInterpolationMode(
 			final MaskedSource<D, ?> source,
@@ -160,7 +182,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				KeyEvent.KEY_PRESSED,
 				EventFX.KEY_PRESSED(
 						"enter shape interpolation mode",
-						e -> enterMode(paintera, (ViewerPanelFX) e.getTarget()),
+						e -> {e.consume(); enterMode(paintera, (ViewerPanelFX) e.getTarget());},
 						e -> e.getTarget() instanceof ViewerPanelFX &&
 							!isModeOn() &&
 							!source.isApplyingMaskProperty().get() &&
@@ -171,29 +193,38 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				KeyEvent.KEY_PRESSED,
 				EventFX.KEY_PRESSED(
 						"fix selection",
-						e -> fixSelection(paintera),
-						e -> isModeOn() &&
+						e -> {e.consume(); fixSelection(paintera);},
+						e -> modeState == ModeState.Selecting &&
 							!selectedObjects.isEmpty() &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
 					)
 			);
+		filter.addEventHandler(
+				KeyEvent.KEY_PRESSED,
+				EventFX.KEY_PRESSED(
+						"apply mask",
+						e -> {e.consume(); applyMask(paintera);},
+						e -> modeState == ModeState.Review &&
+							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
+					)
+			);
 		filter.addEventHandler(
 				KeyEvent.KEY_PRESSED,
 				EventFX.KEY_PRESSED(
 						"exit shape interpolation mode",
-						e -> exitMode(paintera),
+						e -> {e.consume(); exitMode(paintera);},
 						e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE)
 					)
 			);
 		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"select object in current section",
 				e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), true);},
-				e -> isModeOn() && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
+				e -> modeState == ModeState.Selecting && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
 			.handler());
 		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"toggle object in current section",
 				e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), false);},
-				e -> isModeOn() &&
+				e -> modeState == ModeState.Selecting &&
 					((e.isSecondaryButtonDown() && keyTracker.noKeysActive()) ||
 					(e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL))))
 			.handler());
@@ -227,6 +258,8 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 		{
 			e.printStackTrace();
 		}
+
+		modeState = ModeState.Selecting;
 	}
 
 	public void exitMode(final PainteraBaseView paintera)
@@ -250,7 +283,8 @@ public void exitMode(final PainteraBaseView paintera)
 		lastActiveIds = null;
 		currentFillValue = 0;
 		selectedObjects.clear();
-		section1 = section2 = null;
+		sectionInfo1 = sectionInfo2 = null;
+		modeState = null;
 		forgetMask();
 		activeViewer.get().requestRepaint();
 
@@ -259,7 +293,7 @@ public void exitMode(final PainteraBaseView paintera)
 
 	public boolean isModeOn()
 	{
-		return activeViewer.get() != null;
+		return modeState != null;
 	}
 
 	private void createMask() throws MaskInUse
@@ -304,21 +338,35 @@ private void setDisableOtherViewers(final boolean disable)
 
 	private void fixSelection(final PainteraBaseView paintera)
 	{
-		if (section1 == null)
+		if (sectionInfo1 == null)
 		{
 			LOG.debug("Fix selection in the first section");
-			section1 = createSectionInfo();
+			sectionInfo1 = createSectionInfo();
 			selectedObjects.clear();
 			paintera.allowedActionsProperty().set(allowedActions);
 		}
 		else
 		{
 			LOG.debug("Fix selection in the second section");
-			section2 = createSectionInfo();
-			interpolateBetweenSections(paintera);
+			sectionInfo2 = createSectionInfo();
+			modeState = ModeState.Interpolating;
+			interpolateBetweenSections();
+			modeState = ModeState.Review;
+			paintera.allowedActionsProperty().set(allowedActions);
 		}
 	}
 
+	private void applyMask(final PainteraBaseView paintera)
+	{
+		final Interval sectionUnionSourceInterval = Intervals.union(
+				sectionInfo1.sourceBoundingBox,
+				sectionInfo2.sourceBoundingBox
+			);
+		System.out.println("applying mask using affected interval of size " + Arrays.toString(Intervals.dimensionsAsLongArray(sectionUnionSourceInterval)));
+		source.applyMask(mask, sectionUnionSourceInterval, FOREGROUND_CHECK);
+		exitMode(paintera);
+	}
+
 	private SectionInfo createSectionInfo()
 	{
 		Interval selectionSourceBoundingBox = null;
@@ -336,41 +384,130 @@ private SectionInfo createSectionInfo()
 			);
 	}
 
-	private RandomAccessibleInterval<NativeBoolType> writeSectionToImage(final SectionInfo sectionInfo)
+	private void testOutput(final RandomAccessibleInterval<UnsignedLongType> mask, final String dataset)
 	{
-		final AffineTransform3D maskScaledDisplayTransform = sectionInfo.sourceToDisplayTransform;
-		final Interval sourceSelectionBoundingBox = sectionInfo.sourceBoundingBox;
+		try
+		{
+			N5Utils.save(
+					Converters.convert(mask, (in, out) -> out.setInteger(in.get()), new UnsignedShortType()),
+					new N5FSWriter("/groups/saalfeld/home/pisarevi/data/paintera-test/test.n5"),
+					dataset,
+					mask.numDimensions() == 2 ? new int[] {4096, 4096} : new int[] {256, 256, 256},
+					new GzipCompression()
+				);
+		}
+		catch (final IOException e)
+		{
+			e.printStackTrace();
+		}
+	}
 
-		final RandomAccessibleInterval<UnsignedLongType> maskInterval = Views.interval(mask.mask, sourceSelectionBoundingBox);
-		final RandomAccessibleInterval<BoolType> selectionMask = Converters.convert(
-				maskInterval,
-				(in, out) -> out.set(in.getIntegerLong() != 0 && in.getIntegerLong() <= currentFillValue),
-				new BoolType()
+	@SuppressWarnings("unchecked")
+	private void interpolateBetweenSections()
+	{
+		final SectionInfo[] sectionInfoPair = {sectionInfo1, sectionInfo2};
+
+		// find the interval that encompasses both of the sections
+		final Interval sectionsUnionInterval = Intervals.union(
+				getSectionInterval(sectionInfoPair[0]),
+				getSectionInterval(sectionInfoPair[1])
 			);
-		final RealRandomAccessible<BoolType> realSelectionMask = Views.interpolate(
-				Views.extendZero(selectionMask),
-				new NearestNeighborInterpolatorFactory<>()
+
+		// extract the sections using the union interval
+		final RandomAccessibleInterval<UnsignedLongType>[] sectionPair = new RandomAccessibleInterval[2];
+		for (int i = 0; i < 2; ++i)
+			sectionPair[i] = getMaskSection(sectionInfoPair[i].sourceToDisplayTransform, sectionsUnionInterval);
+
+		// replace fill values in the sections with a single value because we do not want to interpolate between multiple labels
+		for (final RandomAccessibleInterval<UnsignedLongType> section : sectionPair)
+			for (final UnsignedLongType val : Views.iterable(section))
+				if (FOREGROUND_CHECK.test(val))
+					val.set(INTERPOLATION_FILL_VALUE);
+
+
+//		testOutput(sectionPair[0], "section1");
+//		testOutput(sectionPair[1], "section2");
+
+
+		// find the distance between the sections and generate the filler sections between them
+		final double distanceBetweenSections = computeDistanceBetweenSections();
+		final int numFillers = (int) Math.round(Math.abs(distanceBetweenSections)) - 1;
+		final double fillerStep = distanceBetweenSections / (numFillers + 1);
+		final RandomAccessibleInterval<UnsignedLongType>[] fillers = new RandomAccessibleInterval[numFillers];
+		System.out.println("Distance between the sections: " + distanceBetweenSections);
+		System.out.println("Creating " + numFillers + " fillers of size " + Arrays.toString(Intervals.dimensionsAsLongArray(sectionsUnionInterval)) + ":");
+		for (int i = 0; i < numFillers; ++i)
+		{
+			final double fillerShift = fillerStep * (i + 1);
+			final AffineTransform3D fillerTransform = sectionInfo1.sourceToDisplayTransform.copy();
+			fillerTransform.preConcatenate(new Translation3D(0, 0, fillerShift));
+			fillers[i] = getMaskSection(fillerTransform, sectionsUnionInterval);
+			System.out.println("  " + fillerShift);
+		}
+
+
+		final List<RandomAccessibleInterval<UnsignedLongType>> sectionsList = new ArrayList<>();
+		sectionsList.add(sectionPair[0]);
+		sectionsList.addAll(Arrays.asList(fillers));
+		sectionsList.add(sectionPair[1]);
+
+		System.out.println("writing debug output before interpolating...");
+		testOutput(Views.stack(sectionsList), "sections-before");
+		System.out.println("done");
+
+		System.out.println("Running interpolation...");
+		InterpolateBetweenSections.interpolateBetweenSections(
+				sectionPair[0],
+				sectionPair[1],
+				new ArrayImgFactory<>(new DoubleType()),
+				fillers,
+				DISTANCE_TYPE.EUCLIDIAN,
+				new double[] {1},
+				Label.BACKGROUND
 			);
-		final RealRandomAccessible<BoolType> transformedSelectionMask = RealViews.affine(realSelectionMask, maskScaledDisplayTransform);
-		final Interval displayBoundingBox = Intervals.smallestContainingInterval(maskScaledDisplayTransform.estimateBounds(sourceSelectionBoundingBox));
-		final RandomAccessibleInterval<BoolType> transformedSelectionMaskInterval = Views.interval(Views.raster(transformedSelectionMask), displayBoundingBox);
-		final RandomAccessibleInterval<BoolType> src = Views.hyperSlice(transformedSelectionMaskInterval, 2, 0l);
+		System.out.println("Done");
 
-		final RandomAccessibleInterval<NativeBoolType> dst = ArrayImgs.booleans(Intervals.dimensionsAsLongArray(src));
-		LOG.debug("Copying the current selection into an image of size {}", Intervals.dimensionsAsLongArray(dst));
-		System.out.println("Copying the current selection into an image of size " + Arrays.toString(Intervals.dimensionsAsLongArray(dst)));
-		final Cursor<BoolType> srcCursor = Views.flatIterable(src).cursor();
-		final Cursor<NativeBoolType> dstCursor = Views.flatIterable(dst).cursor();
-		while (dstCursor.hasNext() || srcCursor.hasNext())
-			dstCursor.next().set(srcCursor.next().get());
+		System.out.println("writing debug output after interpolating...");
+		testOutput(Views.stack(sectionsList), "sections-after");
+		System.out.println("done");
+	}
+
+	private Interval getSectionInterval(final SectionInfo sectionInfo)
+	{
+		final RealInterval sectionBounds = sectionInfo.sourceToDisplayTransform.estimateBounds(sectionInfo.sourceBoundingBox);
+		final Interval sectionInterval3D = Intervals.smallestContainingInterval(sectionBounds);
+		return new FinalInterval(
+				new long[] {sectionInterval3D.min(0), sectionInterval3D.min(1)},
+				new long[] {sectionInterval3D.max(0), sectionInterval3D.max(1)}
+			);
+	}
+
+	private RandomAccessibleInterval<UnsignedLongType> getMaskSection(final AffineTransform3D transform, final Interval sectionInterval)
+	{
+		final Interval sectionInterval3D = sectionInterval.numDimensions() == 3 ? sectionInterval : new FinalInterval(
+				new long[] {sectionInterval.min(0), sectionInterval.min(1), 0l},
+				new long[] {sectionInterval.max(0), sectionInterval.max(1), 0l}
+			);
+		final RealRandomAccessible<UnsignedLongType> transformedMask = getTransformedMask(transform);
+		final RandomAccessibleInterval<UnsignedLongType> transformedMaskInterval = Views.interval(Views.raster(transformedMask), sectionInterval3D);
+		return Views.hyperSlice(transformedMaskInterval, 2, 0l);
+	}
 
-		return dst;
+	private RealRandomAccessible<UnsignedLongType> getTransformedMask(final AffineTransform3D transform)
+	{
+		final RealRandomAccessible<UnsignedLongType> interpolatedMask = Views.interpolate(
+				Views.extendValue(mask.mask, new UnsignedLongType(Label.BACKGROUND)),
+				new NearestNeighborInterpolatorFactory<>()
+			);
+		return RealViews.affine(interpolatedMask, transform);
 	}
 
-	private void interpolateBetweenSections(final PainteraBaseView paintera)
+	private double computeDistanceBetweenSections()
 	{
-		assert section1 != null && section2 != null;
-		throw new RuntimeException("TODO");
+		final double[] pos1 = new double[3], pos2 = new double[3];
+		sectionInfo1.sourceToDisplayTransform.apply(pos1, pos1);
+		sectionInfo2.sourceToDisplayTransform.apply(pos2, pos2);
+		return pos2[2] - pos1[2]; // We care only about the shift between the sections (Z distance in the viewer)
 	}
 
 	private void selectObject(final PainteraBaseView paintera, final double x, final double y, final boolean deactivateOthers)
@@ -420,7 +557,7 @@ private Pair<Long, Interval> runFloodFillToSelect(final double x, final double y
 	}
 
 	/**
-	 * Flood-fills the mask using a background value to remove the object from the selection.
+	 * Flood-fills the mask using the background value to remove the object from the selection.
 	 *
 	 * @param x
 	 * @param y

From 61e5a6e821317c1361cbc18ae91adbfb3a49cb22 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 31 May 2019 11:30:25 -0400
Subject: [PATCH 28/84] allow setting mask from outside in MaskedSource

the setMask() method takes a RealRandomAccessible to make it easy to use virtual masks
---
 .../paintera/data/mask/MaskedSource.java      | 95 +++++++++++++++----
 1 file changed, 79 insertions(+), 16 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java
index 159027150..1c2568f70 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java
@@ -279,12 +279,12 @@ public Mask<UnsignedLongType> getCurrentMask()
 	}
 
 	public Mask<UnsignedLongType> generateMask(
-			final MaskInfo<UnsignedLongType> mask,
+			final MaskInfo<UnsignedLongType> maskInfo,
 			final Predicate<UnsignedLongType> isPaintedForeground)
 	throws MaskInUse
 	{
 
-		LOG.debug("Asking for mask: {}", mask);
+		LOG.debug("Asking for mask: {}", maskInfo);
 		synchronized (this)
 		{
 			final boolean canGenerateMask = !isCreatingMask && currentMask == null && !isApplyingMask.get() && !isPersisting;
@@ -300,24 +300,55 @@ public Mask<UnsignedLongType> generateMask(
 			}
 			this.isCreatingMask = true;
 		}
-		LOG.debug("Generating mask: {}", mask);
+		LOG.debug("Generating mask: {}", maskInfo);
 
-		Pair<RandomAccessibleInterval<UnsignedLongType>, RandomAccessibleInterval<VolatileUnsignedLongType>>
-				storeWithVolatile = createMaskStoreWithVolatile(mask.level);
+		final Pair<RandomAccessibleInterval<UnsignedLongType>, RandomAccessibleInterval<VolatileUnsignedLongType>>
+				storeWithVolatile = createMaskStoreWithVolatile(maskInfo.level);
 		final RandomAccessibleInterval<UnsignedLongType> store = storeWithVolatile.getKey();
 		final RandomAccessibleInterval<VolatileUnsignedLongType> vstore  = storeWithVolatile.getValue();
-		setMasks(store, vstore, mask.level, mask.value, isPaintedForeground);
+		setMasks(store, vstore, maskInfo.level, maskInfo.value, isPaintedForeground);
 		final AccessedBlocksRandomAccessible<UnsignedLongType> trackingStore = new AccessedBlocksRandomAccessible<>(
 				store,
 				((AbstractCellImg<?,?,?,?>)store).getCellGrid()
 		);
-		final Mask<UnsignedLongType> m = new Mask<>(mask, trackingStore);
+		final Mask<UnsignedLongType> mask = new Mask<>(maskInfo, trackingStore);
 		synchronized(this)
 		{
 			this.isCreatingMask = false;
-			this.currentMask = m;
+			this.currentMask = mask;
+		}
+		return mask;
+	}
+
+	public void setMask(
+			final MaskInfo<UnsignedLongType> maskInfo,
+			final RealRandomAccessible<UnsignedLongType> mask,
+			final RealRandomAccessible<VolatileUnsignedLongType> vmask,
+			final Predicate<UnsignedLongType> isPaintedForeground)
+	throws MaskInUse
+	{
+		synchronized (this)
+		{
+			final boolean canSetMask = !isCreatingMask && currentMask == null && !isApplyingMask.get() && !isPersisting;
+			LOG.debug("Can set mask? {}", canSetMask);
+			if (!canSetMask)
+			{
+				LOG.error(
+						"Currently processing, cannot set new mask: persisting? {} mask in use? {}",
+						isPersisting,
+						currentMask
+				         );
+				throw new MaskInUse("Busy, cannot set new mask.");
+			}
+		}
+
+		setMasks(mask, vmask, maskInfo.level, maskInfo.value, isPaintedForeground);
+
+		synchronized(this)
+		{
+			final RandomAccessibleInterval<UnsignedLongType> rasteredMask = Views.interval(Views.raster(mask), source.getSource(0, maskInfo.level));
+			this.currentMask = new Mask<>(maskInfo, rasteredMask);
 		}
-		return m;
 	}
 
 	public void applyMask(
@@ -495,7 +526,7 @@ public void persistCanvas() throws CannotPersist
 				this.affectedBlocks.clear();
 				final MaskedSource<D, T> thiz = this;
 				final BooleanProperty proxy = new SimpleBooleanProperty(this.isPersisting);
-				ObservableList<String> states = FXCollections.observableArrayList();
+				final ObservableList<String> states = FXCollections.observableArrayList();
 				final Runnable dialogHandler = () -> {
 					LOG.warn("Creating commit status dialog.");
 					final Alert isCommittingDialog = PainteraAlerts.alert(Alert.AlertType.INFORMATION);
@@ -522,7 +553,7 @@ public void persistCanvas() throws CannotPersist
 						}
 						try {
 							states.add("Persisting painted labels...");
-							List<TLongObjectMap<PersistCanvas.BlockDiff>> blockDiffs = this.persistCanvas.persistCanvas(canvas, affectedBlocks);
+							final List<TLongObjectMap<PersistCanvas.BlockDiff>> blockDiffs = this.persistCanvas.persistCanvas(canvas, affectedBlocks);
 							states.set(states.size() - 1, "Persisting painted labels...   Done");
 							if (this.persistCanvas.supportsLabelBlockLookupUpdate()) {
 								states.add("Updating label-to-block lookup...");
@@ -539,7 +570,7 @@ public void persistCanvas() throws CannotPersist
 							caughtException = e;
 							throw new RuntimeException("Error while trying to persist.", e);
 						}
-						catch (RuntimeException e)
+						catch (final RuntimeException e)
 						{
 							caughtException = e;
 							throw e;
@@ -1350,7 +1381,7 @@ void affectBlocks(
 
 	}
 
-	private DiskCachedCellImgOptions getMaskDiskCachedCellImgOptions(int level)
+	private DiskCachedCellImgOptions getMaskDiskCachedCellImgOptions(final int level)
 	{
 		return DiskCachedCellImgOptions
 				.options()
@@ -1385,7 +1416,23 @@ private void setMasks(
 	{
 		setAtMaskLevel(store, vstore, maskLevel, value, isPaintedForeground);
 		LOG.debug("Created mask at scale level {}", maskLevel);
+		setMaskScaleLevels(maskLevel);
+	}
 
+	private void setMasks(
+			final RealRandomAccessible<UnsignedLongType> mask,
+			final RealRandomAccessible<VolatileUnsignedLongType> vmask,
+			final int maskLevel,
+			final UnsignedLongType value,
+			final Predicate<UnsignedLongType> isPaintedForeground)
+	{
+		setAtMaskLevel(mask, vmask, maskLevel, value, isPaintedForeground);
+		LOG.debug("Created mask at scale level {}", maskLevel);
+		setMaskScaleLevels(maskLevel);
+	}
+
+	private void setMaskScaleLevels(final int maskLevel)
+	{
 		final RealRandomAccessible<UnsignedLongType> dMask = this.dMasks[maskLevel];
 		final RealRandomAccessible<VolatileUnsignedLongType> tMask = this.tMasks[maskLevel];
 
@@ -1419,13 +1466,29 @@ private void setAtMaskLevel(
 			final UnsignedLongType value,
 			final Predicate<UnsignedLongType> isPaintedForeground)
 	{
-		this.dMasks[maskLevel] = Converters.convert(
+		setAtMaskLevel(
 				interpolateNearestNeighbor(Views.extendZero(store)),
+				interpolateNearestNeighbor(Views.extendZero(vstore)),
+				maskLevel,
+				value,
+				isPaintedForeground
+			);
+	}
+
+	private void setAtMaskLevel(
+			final RealRandomAccessible<UnsignedLongType> mask,
+			final RealRandomAccessible<VolatileUnsignedLongType> vmask,
+			final int maskLevel,
+			final UnsignedLongType value,
+			final Predicate<UnsignedLongType> isPaintedForeground)
+	{
+		this.dMasks[maskLevel] = Converters.convert(
+				mask,
 				(input, output) -> output.set(isPaintedForeground.test(input) ? value : INVALID),
 				new UnsignedLongType());
 
 		this.tMasks[maskLevel] = Converters.convert(
-				interpolateNearestNeighbor(Views.extendZero(vstore)),
+				vmask,
 				(input, output) -> {
 					final boolean isValid = input.isValid();
 					output.setValid(isValid);
@@ -1437,7 +1500,7 @@ private void setAtMaskLevel(
 				new VolatileUnsignedLongType());
 	}
 
-	private static <T> RealRandomAccessible<T> interpolateNearestNeighbor(RandomAccessible<T> ra)
+	private static <T> RealRandomAccessible<T> interpolateNearestNeighbor(final RandomAccessible<T> ra)
 	{
 		return Views.interpolate(ra, new NearestNeighborInterpolatorFactory<>());
 	}

From 8c14b241faf0c1683975315a7316e2d4316fcfa6 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 31 May 2019 11:31:33 -0400
Subject: [PATCH 29/84] use virtual mask for interpolation between two distance
 transforms

---
 .../control/ShapeInterpolationMode.java       | 341 +++++++++++-------
 1 file changed, 202 insertions(+), 139 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 89a7c5c90..7c0ff6a95 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -1,20 +1,14 @@
 package org.janelia.saalfeldlab.paintera.control;
 
-import java.io.IOException;
 import java.lang.invoke.MethodHandles;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
 import java.util.function.Predicate;
 
 import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
 import org.janelia.saalfeldlab.fx.event.EventFX;
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.fx.event.MouseClickFX;
-import org.janelia.saalfeldlab.labels.InterpolateBetweenSections;
-import org.janelia.saalfeldlab.n5.GzipCompression;
-import org.janelia.saalfeldlab.n5.N5FSWriter;
-import org.janelia.saalfeldlab.n5.imglib2.N5Utils;
+import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread;
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
 import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions;
 import org.janelia.saalfeldlab.paintera.control.actions.LabelAction;
@@ -23,6 +17,7 @@
 import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D;
 import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
 import org.janelia.saalfeldlab.paintera.data.DataSource;
+import org.janelia.saalfeldlab.paintera.data.PredicateDataSource.PredicateConverter;
 import org.janelia.saalfeldlab.paintera.data.mask.Mask;
 import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo;
 import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
@@ -48,6 +43,7 @@
 import javafx.scene.input.KeyEvent;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.paint.Color;
+import net.imglib2.Cursor;
 import net.imglib2.FinalInterval;
 import net.imglib2.Interval;
 import net.imglib2.RandomAccess;
@@ -55,20 +51,27 @@
 import net.imglib2.RealInterval;
 import net.imglib2.RealPoint;
 import net.imglib2.RealRandomAccessible;
+import net.imglib2.algorithm.morphology.distance.DistanceTransform;
 import net.imglib2.algorithm.morphology.distance.DistanceTransform.DISTANCE_TYPE;
 import net.imglib2.converter.Converters;
+import net.imglib2.converter.logical.Logical;
 import net.imglib2.img.array.ArrayImgFactory;
+import net.imglib2.interpolation.randomaccess.NLinearInterpolatorFactory;
 import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory;
+import net.imglib2.loops.LoopBuilder;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.realtransform.RealViews;
 import net.imglib2.realtransform.Scale3D;
 import net.imglib2.realtransform.Translation3D;
+import net.imglib2.type.BooleanType;
+import net.imglib2.type.NativeType;
 import net.imglib2.type.label.Label;
 import net.imglib2.type.logic.BoolType;
 import net.imglib2.type.numeric.IntegerType;
+import net.imglib2.type.numeric.RealType;
 import net.imglib2.type.numeric.integer.UnsignedLongType;
-import net.imglib2.type.numeric.integer.UnsignedShortType;
-import net.imglib2.type.numeric.real.DoubleType;
+import net.imglib2.type.numeric.real.FloatType;
+import net.imglib2.type.volatiles.VolatileUnsignedLongType;
 import net.imglib2.util.Intervals;
 import net.imglib2.util.Pair;
 import net.imglib2.util.Util;
@@ -136,8 +139,6 @@ private static final class SectionInfo
 
 	private static final Predicate<UnsignedLongType> FOREGROUND_CHECK = t -> t.get() > 0;
 
-	private static final long INTERPOLATION_FILL_VALUE = 1;
-
 	private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>();
 
 	private final MaskedSource<D, ?> source;
@@ -156,7 +157,9 @@ private static final class SectionInfo
 
 	private SectionInfo sectionInfo1, sectionInfo2;
 
-	private ModeState modeState;
+	private ObjectProperty<ModeState> modeState = new SimpleObjectProperty<>();
+
+	private Thread workerThread;
 
 	public ShapeInterpolationMode(
 			final MaskedSource<D, ?> source,
@@ -194,7 +197,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				EventFX.KEY_PRESSED(
 						"fix selection",
 						e -> {e.consume(); fixSelection(paintera);},
-						e -> modeState == ModeState.Selecting &&
+						e -> modeState.get() == ModeState.Selecting &&
 							!selectedObjects.isEmpty() &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
 					)
@@ -204,7 +207,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				EventFX.KEY_PRESSED(
 						"apply mask",
 						e -> {e.consume(); applyMask(paintera);},
-						e -> modeState == ModeState.Review &&
+						e -> modeState.get() == ModeState.Review &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
 					)
 			);
@@ -212,19 +215,19 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				KeyEvent.KEY_PRESSED,
 				EventFX.KEY_PRESSED(
 						"exit shape interpolation mode",
-						e -> {e.consume(); exitMode(paintera);},
+						e -> {e.consume(); exitMode(paintera, false);},
 						e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE)
 					)
 			);
 		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"select object in current section",
 				e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), true);},
-				e -> modeState == ModeState.Selecting && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
+				e -> modeState.get() == ModeState.Selecting && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
 			.handler());
 		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"toggle object in current section",
 				e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), false);},
-				e -> modeState == ModeState.Selecting &&
+				e -> modeState.get() == ModeState.Selecting &&
 					((e.isSecondaryButtonDown() && keyTracker.noKeysActive()) ||
 					(e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL))))
 			.handler());
@@ -259,10 +262,10 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 			e.printStackTrace();
 		}
 
-		modeState = ModeState.Selecting;
+		modeState.set(ModeState.Selecting);
 	}
 
-	public void exitMode(final PainteraBaseView paintera)
+	public void exitMode(final PainteraBaseView paintera, final boolean completed)
 	{
 		if (!isModeOn())
 		{
@@ -272,28 +275,47 @@ public void exitMode(final PainteraBaseView paintera)
 		LOG.info("Exiting shape interpolation mode");
 		setDisableOtherViewers(false);
 
-		paintera.allowedActionsProperty().set(lastAllowedActions);
-		lastAllowedActions = null;
+		if (!completed) // extra cleanup if the mode is aborted
+		{
+			if (workerThread != null)
+			{
+				workerThread.interrupt();
+				try {
+					workerThread.join();
+				} catch (final InterruptedException e) {
+					e.printStackTrace();
+				}
+			}
+
+			selectedIds.activate(lastActiveIds);
+			selectedIds.activateAlso(lastSelectedId);
+
+			source.resetMasks();
+		}
 
 		final long newLabelId = mask.info.value.get();
 		converter.removeColor(newLabelId);
-		selectedIds.activate(lastActiveIds);
-		selectedIds.activateAlso(lastSelectedId);
-		lastSelectedId = Label.INVALID;
-		lastActiveIds = null;
+
+		paintera.allowedActionsProperty().set(lastAllowedActions);
+		lastAllowedActions = null;
+
 		currentFillValue = 0;
 		selectedObjects.clear();
 		sectionInfo1 = sectionInfo2 = null;
-		modeState = null;
-		forgetMask();
-		activeViewer.get().requestRepaint();
+		modeState.set(null);
 
+		workerThread = null;
+		lastSelectedId = Label.INVALID;
+		lastActiveIds = null;
+		mask = null;
+
+		activeViewer.get().requestRepaint();
 		activeViewer.set(null);
 	}
 
 	public boolean isModeOn()
 	{
-		return modeState != null;
+		return modeState.get() != null;
 	}
 
 	private void createMask() throws MaskInUse
@@ -306,12 +328,6 @@ private void createMask() throws MaskInUse
 		LOG.info("Generated mask for shape interpolation using new label ID {}", newLabelId);
 	}
 
-	private void forgetMask()
-	{
-		mask = null;
-		source.resetMasks();
-	}
-
 	private void setDisableOtherViewers(final boolean disable)
 	{
 		final Parent parent = activeViewer.get().getParent();
@@ -349,22 +365,19 @@ private void fixSelection(final PainteraBaseView paintera)
 		{
 			LOG.debug("Fix selection in the second section");
 			sectionInfo2 = createSectionInfo();
-			modeState = ModeState.Interpolating;
-			interpolateBetweenSections();
-			modeState = ModeState.Review;
-			paintera.allowedActionsProperty().set(allowedActions);
+			interpolateBetweenSections(paintera);
 		}
 	}
 
 	private void applyMask(final PainteraBaseView paintera)
 	{
-		final Interval sectionUnionSourceInterval = Intervals.union(
+		final Interval sectionsUnionSourceInterval = Intervals.union(
 				sectionInfo1.sourceBoundingBox,
 				sectionInfo2.sourceBoundingBox
 			);
-		System.out.println("applying mask using affected interval of size " + Arrays.toString(Intervals.dimensionsAsLongArray(sectionUnionSourceInterval)));
-		source.applyMask(mask, sectionUnionSourceInterval, FOREGROUND_CHECK);
-		exitMode(paintera);
+		LOG.info("Applying interpolated mask using bounding box of size {}", Intervals.dimensionsAsLongArray(sectionsUnionSourceInterval));
+		source.applyMask(source.getCurrentMask(), sectionsUnionSourceInterval, FOREGROUND_CHECK);
+		exitMode(paintera, true);
 	}
 
 	private SectionInfo createSectionInfo()
@@ -384,112 +397,156 @@ private SectionInfo createSectionInfo()
 			);
 	}
 
-	private void testOutput(final RandomAccessibleInterval<UnsignedLongType> mask, final String dataset)
+	@SuppressWarnings("unchecked")
+	private void interpolateBetweenSections(final PainteraBaseView paintera)
 	{
-		try
+		modeState.set(ModeState.Interpolating);
+
+		workerThread = new Thread(() ->
 		{
-			N5Utils.save(
-					Converters.convert(mask, (in, out) -> out.setInteger(in.get()), new UnsignedShortType()),
-					new N5FSWriter("/groups/saalfeld/home/pisarevi/data/paintera-test/test.n5"),
-					dataset,
-					mask.numDimensions() == 2 ? new int[] {4096, 4096} : new int[] {256, 256, 256},
-					new GzipCompression()
+			final SectionInfo[] sectionInfoPair = {sectionInfo1, sectionInfo2};
+
+			final Interval affectedUnionSourceInterval = Intervals.union(
+					sectionInfoPair[0].sourceBoundingBox,
+					sectionInfoPair[1].sourceBoundingBox
 				);
-		}
-		catch (final IOException e)
-		{
-			e.printStackTrace();
-		}
-	}
 
-	@SuppressWarnings("unchecked")
-	private void interpolateBetweenSections()
-	{
-		final SectionInfo[] sectionInfoPair = {sectionInfo1, sectionInfo2};
+			final Interval[] displaySectionIntervalPair = new Interval[2];
+			final RandomAccessibleInterval<UnsignedLongType>[] sectionPair = new RandomAccessibleInterval[2];
+			for (int i = 0; i < 2; ++i)
+			{
+				final SectionInfo newSectionInfo = new SectionInfo(sectionInfoPair[i].sourceToDisplayTransform, affectedUnionSourceInterval);
+				final RandomAccessibleInterval<UnsignedLongType> section = getTransformedMaskSection(newSectionInfo);
+				displaySectionIntervalPair[i] = new FinalInterval(section);
+				sectionPair[i] = new ArrayImgFactory<>(new UnsignedLongType()).create(section);
+				final Cursor<UnsignedLongType> srcCursor = Views.flatIterable(section).cursor();
+				final Cursor<UnsignedLongType> dstCursor = Views.flatIterable(sectionPair[i]).cursor();
+				while (dstCursor.hasNext() || srcCursor.hasNext())
+					dstCursor.next().set(srcCursor.next());
+			}
 
-		// find the interval that encompasses both of the sections
-		final Interval sectionsUnionInterval = Intervals.union(
-				getSectionInterval(sectionInfoPair[0]),
-				getSectionInterval(sectionInfoPair[1])
-			);
+			// compute distance transform on both sections
+			final RandomAccessibleInterval<FloatType>[] distanceTransformPair = new RandomAccessibleInterval[2];
+			for (int i = 0; i < 2; ++i)
+			{
+				distanceTransformPair[i] = new ArrayImgFactory<>(new FloatType()).create(sectionPair[i]);
+				final RandomAccessibleInterval<BoolType> binarySection = Converters.convert(sectionPair[i], new PredicateConverter<>(FOREGROUND_CHECK), new BoolType());
+				computeSignedDistanceTransform(binarySection, distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN);
+			}
 
-		// extract the sections using the union interval
-		final RandomAccessibleInterval<UnsignedLongType>[] sectionPair = new RandomAccessibleInterval[2];
-		for (int i = 0; i < 2; ++i)
-			sectionPair[i] = getMaskSection(sectionInfoPair[i].sourceToDisplayTransform, sectionsUnionInterval);
+			final double distanceBetweenSections = computeDistanceBetweenSections(sectionInfo1, sectionInfo2);
+			final AffineTransform3D transformToSource = new AffineTransform3D();
+			transformToSource
+				.preConcatenate(new Translation3D(displaySectionIntervalPair[0].min(0), displaySectionIntervalPair[0].min(1), 0))
+				.preConcatenate(sectionInfo1.sourceToDisplayTransform.inverse());
+
+			final RealRandomAccessible<UnsignedLongType> interpolatedShapeMask = getInterpolatedDistanceTransformMask(
+					distanceTransformPair[0],
+					distanceTransformPair[1],
+					distanceBetweenSections,
+					new UnsignedLongType(1),
+					transformToSource
+				);
 
-		// replace fill values in the sections with a single value because we do not want to interpolate between multiple labels
-		for (final RandomAccessibleInterval<UnsignedLongType> section : sectionPair)
-			for (final UnsignedLongType val : Views.iterable(section))
-				if (FOREGROUND_CHECK.test(val))
-					val.set(INTERPOLATION_FILL_VALUE);
+			final RealRandomAccessible<VolatileUnsignedLongType> volatileInterpolatedShapeMask = getInterpolatedDistanceTransformMask(
+					distanceTransformPair[0],
+					distanceTransformPair[1],
+					distanceBetweenSections,
+					new VolatileUnsignedLongType(1),
+					transformToSource
+				);
 
+			try
+			{
+				synchronized (source)
+				{
+					source.resetMasks();
+					source.setMask(
+							mask.info,
+							interpolatedShapeMask,
+							volatileInterpolatedShapeMask,
+							FOREGROUND_CHECK
+						);
+				}
+				activeViewer.get().requestRepaint();
+			}
+			catch (final MaskInUse e)
+			{
+				LOG.error("Label source already has an active mask");
+			}
 
-//		testOutput(sectionPair[0], "section1");
-//		testOutput(sectionPair[1], "section2");
+			InvokeOnJavaFXApplicationThread.invoke(() -> {
+				modeState.set(ModeState.Review);
+				paintera.allowedActionsProperty().set(allowedActions);
+			});
+		});
+		workerThread.start();
+	}
+
+	private static <R extends RealType<R> & NativeType<R>, B extends BooleanType<B>> void computeSignedDistanceTransform(
+			final RandomAccessibleInterval<B> mask,
+			final RandomAccessibleInterval<R> target,
+			final DISTANCE_TYPE distanceType,
+			final double... weights)
+	{
+		final RandomAccessibleInterval<R> distanceOutside = target;
+		final RandomAccessibleInterval<R> distanceInside = new ArrayImgFactory<>(Util.getTypeFromInterval(target)).create(target);
+		DistanceTransform.binaryTransform(mask, distanceOutside, distanceType, weights);
+		DistanceTransform.binaryTransform(Logical.complement(mask), distanceInside, distanceType, weights);
+		LoopBuilder.setImages(distanceOutside, distanceInside, target).forEachPixel((outside, inside, result) -> {
+			switch (distanceType)
+			{
+			case EUCLIDIAN:
+				result.setReal(Math.sqrt(outside.getRealDouble()) - Math.sqrt(inside.getRealDouble()));
+				break;
+			case L1:
+				result.setReal(outside.getRealDouble() - inside.getRealDouble());
+				break;
+			}
+		});
+	}
 
+	private static <R extends RealType<R>, T extends NativeType<T> & RealType<T>> RealRandomAccessible<T> getInterpolatedDistanceTransformMask(
+			final RandomAccessibleInterval<R> dt1,
+			final RandomAccessibleInterval<R> dt2,
+			final double distance,
+			final T targetValue,
+			final AffineTransform3D transformToSource)
+	{
+		final RandomAccessibleInterval<R> distanceTransformStack = Views.stack(dt1, dt2);
 
-		// find the distance between the sections and generate the filler sections between them
-		final double distanceBetweenSections = computeDistanceBetweenSections();
-		final int numFillers = (int) Math.round(Math.abs(distanceBetweenSections)) - 1;
-		final double fillerStep = distanceBetweenSections / (numFillers + 1);
-		final RandomAccessibleInterval<UnsignedLongType>[] fillers = new RandomAccessibleInterval[numFillers];
-		System.out.println("Distance between the sections: " + distanceBetweenSections);
-		System.out.println("Creating " + numFillers + " fillers of size " + Arrays.toString(Intervals.dimensionsAsLongArray(sectionsUnionInterval)) + ":");
-		for (int i = 0; i < numFillers; ++i)
-		{
-			final double fillerShift = fillerStep * (i + 1);
-			final AffineTransform3D fillerTransform = sectionInfo1.sourceToDisplayTransform.copy();
-			fillerTransform.preConcatenate(new Translation3D(0, 0, fillerShift));
-			fillers[i] = getMaskSection(fillerTransform, sectionsUnionInterval);
-			System.out.println("  " + fillerShift);
-		}
+		final R extendValue = Util.getTypeFromInterval(distanceTransformStack).createVariable();
+		extendValue.setReal(extendValue.getMaxValue());
+		final RealRandomAccessible<R> interpolatedDistanceTransform = Views.interpolate(
+				Views.extendValue(distanceTransformStack, extendValue),
+				new NLinearInterpolatorFactory<>()
+			);
 
+		final double distanceSign = Math.signum(distance);
+		final double padding = 0.5; // slightly stretches the mask past the end sections to ensure that it's visible in the current plane
+		final RealRandomAccessible<R> scaledInterpolatedDistanceTransform = RealViews.affineReal(
+				interpolatedDistanceTransform,
+				new AffineTransform3D()
+					.preConcatenate(new Scale3D(1, 1, -(distance + padding * distanceSign)))
+					.preConcatenate(new Translation3D(0, 0, padding * 0.5 * distanceSign))
+			);
 
-		final List<RandomAccessibleInterval<UnsignedLongType>> sectionsList = new ArrayList<>();
-		sectionsList.add(sectionPair[0]);
-		sectionsList.addAll(Arrays.asList(fillers));
-		sectionsList.add(sectionPair[1]);
-
-		System.out.println("writing debug output before interpolating...");
-		testOutput(Views.stack(sectionsList), "sections-before");
-		System.out.println("done");
-
-		System.out.println("Running interpolation...");
-		InterpolateBetweenSections.interpolateBetweenSections(
-				sectionPair[0],
-				sectionPair[1],
-				new ArrayImgFactory<>(new DoubleType()),
-				fillers,
-				DISTANCE_TYPE.EUCLIDIAN,
-				new double[] {1},
-				Label.BACKGROUND
+		final T emptyValue = targetValue.createVariable();
+		final RealRandomAccessible<T> interpolatedShape = Converters.convert(
+				scaledInterpolatedDistanceTransform,
+				(in, out) -> out.set(in.getRealDouble() <= 0 ? targetValue : emptyValue),
+				emptyValue.createVariable()
 			);
-		System.out.println("Done");
 
-		System.out.println("writing debug output after interpolating...");
-		testOutput(Views.stack(sectionsList), "sections-after");
-		System.out.println("done");
+		return RealViews.affineReal(interpolatedShape, transformToSource);
 	}
 
-	private Interval getSectionInterval(final SectionInfo sectionInfo)
+	private RandomAccessibleInterval<UnsignedLongType> getTransformedMaskSection(final SectionInfo sectionInfo)
 	{
 		final RealInterval sectionBounds = sectionInfo.sourceToDisplayTransform.estimateBounds(sectionInfo.sourceBoundingBox);
-		final Interval sectionInterval3D = Intervals.smallestContainingInterval(sectionBounds);
-		return new FinalInterval(
-				new long[] {sectionInterval3D.min(0), sectionInterval3D.min(1)},
-				new long[] {sectionInterval3D.max(0), sectionInterval3D.max(1)}
-			);
-	}
-
-	private RandomAccessibleInterval<UnsignedLongType> getMaskSection(final AffineTransform3D transform, final Interval sectionInterval)
-	{
-		final Interval sectionInterval3D = sectionInterval.numDimensions() == 3 ? sectionInterval : new FinalInterval(
-				new long[] {sectionInterval.min(0), sectionInterval.min(1), 0l},
-				new long[] {sectionInterval.max(0), sectionInterval.max(1), 0l}
-			);
-		final RealRandomAccessible<UnsignedLongType> transformedMask = getTransformedMask(transform);
-		final RandomAccessibleInterval<UnsignedLongType> transformedMaskInterval = Views.interval(Views.raster(transformedMask), sectionInterval3D);
+		final Interval sectionInterval = Intervals.smallestContainingInterval(sectionBounds);
+		final RealRandomAccessible<UnsignedLongType> transformedMask = getTransformedMask(sectionInfo.sourceToDisplayTransform);
+		final RandomAccessibleInterval<UnsignedLongType> transformedMaskInterval = Views.interval(Views.raster(transformedMask), sectionInterval);
 		return Views.hyperSlice(transformedMaskInterval, 2, 0l);
 	}
 
@@ -502,11 +559,11 @@ private RealRandomAccessible<UnsignedLongType> getTransformedMask(final AffineTr
 		return RealViews.affine(interpolatedMask, transform);
 	}
 
-	private double computeDistanceBetweenSections()
+	private static double computeDistanceBetweenSections(final SectionInfo s1, final SectionInfo s2)
 	{
 		final double[] pos1 = new double[3], pos2 = new double[3];
-		sectionInfo1.sourceToDisplayTransform.apply(pos1, pos1);
-		sectionInfo2.sourceToDisplayTransform.apply(pos2, pos2);
+		s1.sourceToDisplayTransform.apply(pos1, pos1);
+		s2.sourceToDisplayTransform.apply(pos2, pos2);
 		return pos2[2] - pos1[2]; // We care only about the shift between the sections (Z distance in the viewer)
 	}
 
@@ -593,7 +650,9 @@ private UnsignedLongType getMaskValue(final double x, final double y)
 	private AffineTransform3D getMaskTransform()
 	{
 		final AffineTransform3D maskTransform = new AffineTransform3D();
-		source.getSourceTransform(mask.info.t, mask.info.level, maskTransform);
+		final int time = activeViewer.get().getState().timepointProperty().get();
+		final int level = MASK_SCALE_LEVEL;
+		source.getSourceTransform(time, level, maskTransform);
 		return maskTransform;
 	}
 
@@ -616,14 +675,16 @@ private AffineTransform3D getMaskDisplayTransform()
 	 * @param level
 	 * @return
 	 */
-	private AffineTransform3D getMaskDisplayTransformIgnoreScaling(final int level)
+	private AffineTransform3D getMaskDisplayTransformIgnoreScaling(final int targetLevel)
 	{
 		final AffineTransform3D maskMipmapDisplayTransform = getMaskDisplayTransformIgnoreScaling();
-		if (level != mask.info.level)
+		final int maskLevel = MASK_SCALE_LEVEL;
+		if (targetLevel != maskLevel)
 		{
 			// scale with respect to the given mipmap level
-			final Scale3D relativeScaleTransform = new Scale3D(DataSource.getRelativeScales(source, mask.info.t, level, mask.info.level));
-			maskMipmapDisplayTransform.preConcatenate(relativeScaleTransform);
+			final int time = activeViewer.get().getState().timepointProperty().get();
+			final Scale3D relativeScaleTransform = new Scale3D(DataSource.getRelativeScales(source, time, maskLevel, targetLevel));
+			maskMipmapDisplayTransform.preConcatenate(relativeScaleTransform.inverse());
 		}
 		return maskMipmapDisplayTransform;
 	}
@@ -642,7 +703,9 @@ private AffineTransform3D getMaskDisplayTransformIgnoreScaling()
 		Arrays.setAll(viewerScale, d -> Affine3DHelpers.extractScale(viewerTransform, d));
 		final Scale3D scalingTransform = new Scale3D(viewerScale);
 		// neutralize mask scaling if there is any
-		scalingTransform.concatenate(new Scale3D(DataSource.getScale(source, mask.info.t, mask.info.level)));
+		final int time = activeViewer.get().getState().timepointProperty().get();
+		final int level = MASK_SCALE_LEVEL;
+		scalingTransform.concatenate(new Scale3D(DataSource.getScale(source, time, level)));
 		// build the resulting transform
 		return viewerTransform.preConcatenate(scalingTransform.inverse()).concatenate(getMaskTransform());
 	}

From bb4031716001b512af2183e7403c34788ae2b49c Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 31 May 2019 11:31:58 -0400
Subject: [PATCH 30/84] display progress indicator when running shape
 interpolation

---
 .../paintera/control/ShapeInterpolationMode.java         | 7 ++++++-
 .../saalfeldlab/paintera/state/LabelSourceState.java     | 9 +++++++++
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 7c0ff6a95..b48e51840 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -82,7 +82,7 @@ public class ShapeInterpolationMode<D extends IntegerType<D>>
 {
 	private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
-	private static enum ModeState
+	public static enum ModeState
 	{
 		Selecting,
 		Interpolating,
@@ -178,6 +178,11 @@ public ObjectProperty<ViewerPanelFX> activeViewerProperty()
 		return activeViewer;
 	}
 
+	public ObjectProperty<ModeState> modeStateProperty()
+	{
+		return modeState;
+	}
+
 	public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final KeyTracker keyTracker)
 	{
 		final DelegateEventHandlers.AnyHandler filter = DelegateEventHandlers.handleAny();
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
index 97ecdd837..719e11f39 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
@@ -53,6 +53,7 @@
 import org.janelia.saalfeldlab.paintera.composition.ARGBCompositeAlphaYCbCr;
 import org.janelia.saalfeldlab.paintera.composition.Composite;
 import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode;
+import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode.ModeState;
 import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentOnlyLocal;
 import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentState;
 import org.janelia.saalfeldlab.paintera.control.lock.LockedSegmentsOnlyLocal;
@@ -606,6 +607,14 @@ private HBox createDisplayStatus()
 			});
 		});
 
+		this.shapeInterpolationMode.modeStateProperty().addListener((obs, oldv, newv) -> {
+			InvokeOnJavaFXApplicationThread.invoke(() -> {
+				paintingProgressIndicator.setVisible(newv == ModeState.Interpolating);
+				if (newv == ModeState.Interpolating)
+					paintingProgressIndicatorTooltip.setText("Interpolating between sections...");
+			});
+		});
+
 		final HBox displayStatus = new HBox(5,
 				lastSelectedLabelColorRect,
 				paintingProgressIndicator

From ee86f22833250242d1ffbe227503436450ab23a1 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 31 May 2019 12:35:28 -0400
Subject: [PATCH 31/84] check if shape interpolation thread was interrupted for
 early exit

---
 .../saalfeldlab/paintera/control/ShapeInterpolationMode.java | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index b48e51840..d3810469e 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -434,6 +434,8 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 			final RandomAccessibleInterval<FloatType>[] distanceTransformPair = new RandomAccessibleInterval[2];
 			for (int i = 0; i < 2; ++i)
 			{
+				if (Thread.currentThread().isInterrupted())
+					return;
 				distanceTransformPair[i] = new ArrayImgFactory<>(new FloatType()).create(sectionPair[i]);
 				final RandomAccessibleInterval<BoolType> binarySection = Converters.convert(sectionPair[i], new PredicateConverter<>(FOREGROUND_CHECK), new BoolType());
 				computeSignedDistanceTransform(binarySection, distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN);
@@ -461,6 +463,9 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 					transformToSource
 				);
 
+			if (Thread.currentThread().isInterrupted())
+				return;
+
 			try
 			{
 				synchronized (source)

From 4357e44c71b853c13d58f9d113f25ed69f1723dd Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 31 May 2019 12:37:45 -0400
Subject: [PATCH 32/84] disable menu actions in shape interpolation mode except
 viewer maximize/minimize

---
 .../paintera/BorderPaneWithStatusBars.java    |  5 +++
 .../paintera/PainteraDefaultHandlers.java     | 40 +++++++++++--------
 .../control/ShapeInterpolationMode.java       |  7 +++-
 .../control/actions/AllowedActions.java       | 13 +++++-
 .../paintera/control/actions/MenuAction.java  | 29 ++++++++++++++
 5 files changed, 73 insertions(+), 21 deletions(-)
 create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java
index cab1b2a7a..b1c0c9f53 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java
@@ -279,6 +279,11 @@ public BorderPaneWithStatusBars(
 		resizeSideBar = new ResizeOnLeftSide(sideBar, sideBar.prefWidthProperty(), dist -> Math.abs(dist) < 5);
 	}
 
+	public boolean isSideBarActive()
+	{
+		return pane.getRight() != null;
+	}
+
 	public void toggleSideBar()
 	{
 		if (pane.getRight() == null)
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
index 46037f7b4..28e5a8c88 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
@@ -46,6 +46,7 @@
 import org.janelia.saalfeldlab.paintera.control.OrthogonalViewsValueDisplayListener;
 import org.janelia.saalfeldlab.paintera.control.RunWhenFirstElementIsAdded;
 import org.janelia.saalfeldlab.paintera.control.ShowOnlySelectedInStreamToggle;
+import org.janelia.saalfeldlab.paintera.control.actions.MenuAction;
 import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners;
 import org.janelia.saalfeldlab.paintera.control.navigation.DisplayTransformUpdateOnResize;
 import org.janelia.saalfeldlab.paintera.state.SourceInfo;
@@ -248,12 +249,17 @@ public PainteraDefaultHandlers(
 		final EventFX<KeyEvent> toggleSideBar = EventFX.KEY_RELEASED(
 				"toggle sidebar",
 				e -> paneWithStatus.toggleSideBar(),
-				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.P)
-		                                                            );
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.SidePanel) && keyTracker.areOnlyTheseKeysDown(KeyCode.P)
+			);
 		borderPane.sceneProperty().addListener((obs, oldv, newv) -> newv.addEventHandler(
 				KeyEvent.KEY_PRESSED,
 				toggleSideBar));
 
+		baseView.allowedActionsProperty().addListener((obs, oldv, newv) -> {
+			if (!newv.isAllowed(MenuAction.SidePanel) && paneWithStatus.isSideBarActive())
+				paneWithStatus.toggleSideBar();
+		});
+
 		EventFX.KEY_PRESSED(
 				"toggle interpolation",
 				e -> toggleInterpolation(),
@@ -261,11 +267,11 @@ public PainteraDefaultHandlers(
 		EventFX.KEY_PRESSED(
 				"cycle current source",
 				e -> sourceInfo.incrementCurrentSourceIndex(),
-				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.TAB)).installInto(borderPane);
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ChangeActiveSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.TAB)).installInto(borderPane);
 		EventFX.KEY_PRESSED(
 				"backwards cycle current source",
 				e -> sourceInfo.decrementCurrentSourceIndex(),
-				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.TAB)).installInto(borderPane);
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ChangeActiveSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.TAB)).installInto(borderPane);
 
 		this.resizer = new GridResizer(gridConstraintsManager, 5, baseView.pane(), keyTracker);
 		this.resizer.installInto(baseView.pane());
@@ -296,19 +302,19 @@ public PainteraDefaultHandlers(
 		EventFX.KEY_PRESSED(
 				"maximize",
 				e -> toggleMaximizeTopLeft.toggleFullScreen(),
-				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topLeft().viewer());
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topLeft().viewer());
 		EventFX.KEY_PRESSED(
 				"maximize",
 				e -> toggleMaximizeTopRight.toggleFullScreen(),
-				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topRight().viewer());
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topRight().viewer());
 		EventFX.KEY_PRESSED(
 				"maximize",
 				e -> toggleMaximizeBottomLeft.toggleFullScreen(),
-				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.bottomLeft().viewer());
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.bottomLeft().viewer());
 		EventFX.KEY_PRESSED(
 				"maximize",
 				e -> toggleMaximizeBottomRight.toggleFullScreen(),
-				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(baseView.viewer3D());
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(baseView.viewer3D());
 
 		final CurrentSourceVisibilityToggle csv = new CurrentSourceVisibilityToggle(sourceInfo.currentState());
 		EventFX.KEY_PRESSED(
@@ -326,10 +332,8 @@ public PainteraDefaultHandlers(
 
 		EventFX.KEY_PRESSED(
 				"toggle maximize bottom row",
-				e -> {
-					gridConstraintsManager.maximize(MaximizedRow.BOTTOM, 0);
-				},
-				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(paneWithStatus.getPane());
+				e -> gridConstraintsManager.maximize(MaximizedRow.BOTTOM, 0),
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerAndOrthoslicesView) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(paneWithStatus.getPane());
 
 		bottomLeftNeedsZNormal = Bindings.createBooleanBinding(
 				() -> MaximizedColumn.NONE.equals(gridConstraintsManager.getMaximizedColumn()) && MaximizedRow.BOTTOM
@@ -363,8 +367,10 @@ public PainteraDefaultHandlers(
 				MouseEvent.MOUSE_CLICKED,
 				e -> {
 					LOG.debug("Handling event {}", e);
-					if (MouseButton.SECONDARY.equals(e.getButton()) && e.getClickCount() == 1 && !mouseTracker
-							.isDragging())
+					if (baseView.allowedActionsProperty().get().isAllowed(MenuAction.OrthoslicesContextMenu) &&
+							MouseButton.SECONDARY.equals(e.getButton()) &&
+							e.getClickCount() == 1 &&
+							!mouseTracker.isDragging())
 					{
 						LOG.debug("Check passed for event {}", e);
 						e.consume();
@@ -380,7 +386,7 @@ public PainteraDefaultHandlers(
 						projectDirectory,
 						Exceptions.handler("Paintera", "Unable to create new Dataset"),
 						baseView.sourceInfo().currentSourceProperty().get()),
-				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.N)).installInto(paneWithStatus.getPane());
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.CreateNewLabelSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.N)).installInto(paneWithStatus.getPane());
 	}
 
 	private final Map<ViewerPanelFX, ViewerAndTransforms> viewerToTransforms = new HashMap<>();
@@ -510,10 +516,10 @@ public static EventHandler<KeyEvent> addOpenDatasetContextMenuHandler(
 
 		assert triggers.length > 0;
 
-		EventHandler<KeyEvent> handler = OpenDialogMenu.keyPressedHandler(
+		final EventHandler<KeyEvent> handler = OpenDialogMenu.keyPressedHandler(
 				target,
 				exception -> Exceptions.exceptionAlert(Paintera.NAME, "Unable to show open dataset menu", exception),
-				e -> keyTracker.areOnlyTheseKeysDown(triggers),
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.AddSource) && keyTracker.areOnlyTheseKeysDown(triggers),
 				"Open dataset",
 				baseView,
 				projectDirectory,
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index d3810469e..0087dcb4f 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -12,6 +12,7 @@
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
 import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions;
 import org.janelia.saalfeldlab.paintera.control.actions.LabelAction;
+import org.janelia.saalfeldlab.paintera.control.actions.MenuAction;
 import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction;
 import org.janelia.saalfeldlab.paintera.control.actions.PaintAction;
 import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D;
@@ -120,12 +121,14 @@ private static final class SectionInfo
 		allowedActions = new AllowedActions(
 			NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll),
 			LabelAction.none(),
-			PaintAction.none()
+			PaintAction.none(),
+			MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized)
 		);
 		allowedActionsWhenSelected = new AllowedActions(
 				NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom),
 				LabelAction.none(),
-				PaintAction.none()
+				PaintAction.none(),
+				MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized)
 			);
 	}
 
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 9a1d899aa..aca57c552 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
@@ -10,15 +10,18 @@ public final class AllowedActions
 	private final EnumSet<NavigationAction> navigationAllowedActions;
 	private final EnumSet<LabelAction> labelAllowedActions;
 	private final EnumSet<PaintAction> paintAllowedActions;
+	private final EnumSet<MenuAction> menuAllowedActions;
 
 	public AllowedActions(
 			final EnumSet<NavigationAction> navigationAllowedActions,
 			final EnumSet<LabelAction> labelAllowedActions,
-			final EnumSet<PaintAction> paintAllowedActions)
+			final EnumSet<PaintAction> paintAllowedActions,
+			final EnumSet<MenuAction> menuAllowedActions)
 	{
 		this.navigationAllowedActions = navigationAllowedActions;
 		this.labelAllowedActions = labelAllowedActions;
 		this.paintAllowedActions = paintAllowedActions;
+		this.menuAllowedActions = menuAllowedActions;
 	}
 
 	public boolean isAllowed(final NavigationAction navigationAction)
@@ -36,12 +39,18 @@ public boolean isAllowed(final PaintAction paintAction)
 		return this.paintAllowedActions.contains(paintAction);
 	}
 
+	public boolean isAllowed(final MenuAction menuAction)
+	{
+		return this.menuAllowedActions.contains(menuAction);
+	}
+
 	public static AllowedActions all()
 	{
 		return new AllowedActions(
 			NavigationAction.all(),
 			LabelAction.all(),
-			PaintAction.all()
+			PaintAction.all(),
+			MenuAction.all()
 		);
 	}
 }
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java
new file mode 100644
index 000000000..d1c1f6d5c
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java
@@ -0,0 +1,29 @@
+package org.janelia.saalfeldlab.paintera.control.actions;
+
+import java.util.EnumSet;
+
+public enum MenuAction
+{
+	AddSource,
+	CreateNewLabelSource,
+	ChangeActiveSource,
+	SidePanel,
+	ToggleViewerMaximizedMinimized,
+	ToggleViewerAndOrthoslicesView,
+	OrthoslicesContextMenu;
+
+	public static EnumSet<MenuAction> of(final MenuAction first, final MenuAction... rest)
+	{
+		return EnumSet.of(first, rest);
+	}
+
+	public static EnumSet<MenuAction> all()
+	{
+		return EnumSet.allOf(MenuAction.class);
+	}
+
+	public static EnumSet<MenuAction> none()
+	{
+		return EnumSet.noneOf(MenuAction.class);
+	}
+}

From 83c3b5e889d0044fdd4948771f001a26b907f931 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 31 May 2019 13:15:32 -0400
Subject: [PATCH 33/84] properly ignore clicks outside the mask in shape
 interpolation mode

---
 .../paintera/control/ShapeInterpolationMode.java  | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 0087dcb4f..dc435172a 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -566,7 +566,7 @@ private RandomAccessibleInterval<UnsignedLongType> getTransformedMaskSection(fin
 	private RealRandomAccessible<UnsignedLongType> getTransformedMask(final AffineTransform3D transform)
 	{
 		final RealRandomAccessible<UnsignedLongType> interpolatedMask = Views.interpolate(
-				Views.extendValue(mask.mask, new UnsignedLongType(Label.BACKGROUND)),
+				Views.extendValue(mask.mask, new UnsignedLongType(Label.OUTSIDE)),
 				new NearestNeighborInterpolatorFactory<>()
 			);
 		return RealViews.affine(interpolatedMask, transform);
@@ -582,7 +582,11 @@ private static double computeDistanceBetweenSections(final SectionInfo s1, final
 
 	private void selectObject(final PainteraBaseView paintera, final double x, final double y, final boolean deactivateOthers)
 	{
-		final boolean wasSelected = isSelected(x, y);
+		final UnsignedLongType maskValue = getMaskValue(x, y);
+		if (maskValue.get() == Label.OUTSIDE)
+			return;
+
+		final boolean wasSelected = FOREGROUND_CHECK.test(maskValue);
 		final int numSelectedObjects = selectedObjects.size();
 
 		LOG.debug("Object was clicked: deactivateOthers={}, wasSelected={}, numSelectedObjects", deactivateOthers, wasSelected, numSelectedObjects);
@@ -646,15 +650,10 @@ private long runFloodFillToDeselect(final double x, final double y)
 		return maskValue;
 	}
 
-	private boolean isSelected(final double x, final double y)
-	{
-		return FOREGROUND_CHECK.test(getMaskValue(x, y));
-	}
-
 	private UnsignedLongType getMaskValue(final double x, final double y)
 	{
 		final RealPoint sourcePos = getSourceCoordinates(x, y);
-		final RandomAccess<UnsignedLongType> maskAccess = mask.mask.randomAccess();
+		final RandomAccess<UnsignedLongType> maskAccess = Views.extendValue(mask.mask, new UnsignedLongType(Label.OUTSIDE)).randomAccess();
 		for (int d = 0; d < sourcePos.numDimensions(); ++d)
 			maskAccess.setPosition(Math.round(sourcePos.getDoublePosition(d)), d);
 		return maskAccess.get();

From 3a1782079ef8b1c52bd2ca3d3c5dd2db95467b63 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 31 May 2019 13:39:21 -0400
Subject: [PATCH 34/84] use different color for fixed selection in shape
 interpolation mode

---
 .../saalfeldlab/paintera/control/ShapeInterpolationMode.java  | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index dc435172a..5936ff15f 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -139,6 +139,7 @@ private static final class SectionInfo
 	private static final int SHAPE_INTERPOLATION_SCALE_LEVEL = MASK_SCALE_LEVEL;
 
 	private static final Color MASK_COLOR = Color.web("00CCFF");
+	private static final Color MASK_COLOR_FIXED_SELECTION = Color.web("44AACC");
 
 	private static final Predicate<UnsignedLongType> FOREGROUND_CHECK = t -> t.get() > 0;
 
@@ -367,6 +368,8 @@ private void fixSelection(final PainteraBaseView paintera)
 			LOG.debug("Fix selection in the first section");
 			sectionInfo1 = createSectionInfo();
 			selectedObjects.clear();
+			converter.setColor(mask.info.value.get(), MASK_COLOR_FIXED_SELECTION);
+			activeViewerProperty().get().requestRepaint();
 			paintera.allowedActionsProperty().set(allowedActions);
 		}
 		else
@@ -588,6 +591,7 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 
 		final boolean wasSelected = FOREGROUND_CHECK.test(maskValue);
 		final int numSelectedObjects = selectedObjects.size();
+		converter.setColor(mask.info.value.get(), MASK_COLOR);
 
 		LOG.debug("Object was clicked: deactivateOthers={}, wasSelected={}, numSelectedObjects", deactivateOthers, wasSelected, numSelectedObjects);
 

From bd6e2e4550d430ed5986dea212c3889b9c647d56 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <igorpisarev@users.noreply.github.com>
Date: Fri, 31 May 2019 15:35:39 -0400
Subject: [PATCH 35/84] add shape interpolation mode shortcuts to readme

---
 README.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/README.md b/README.md
index a165cce34..e4573aa67 100644
--- a/README.md
+++ b/README.md
@@ -162,6 +162,8 @@ Usage: Paintera [-h] [--default-to-temp-directory] [--print-error-codes]
 | `F` + left click | 2D Flood-fill in current viewer plane with id that was last toggled active (if any) |
 | `Shift` + `F` + left click | Flood-fill with id that was last toggled active (if any) |
 | `N` | Select new, previously unused id |
+| `S` | Enter/advance shape interpolation mode |
+| `ESC` | Exit shape interpolation mode |
 | `Ctrl` + `C` | Show dialog to commit canvas and/or assignments |
 | `C` | Increment ARGB stream seed by one |
 | `Shift` + `C` | Decrement ARGB stream seed by one |

From a970c085800d55289e7fba70bd3cea62df6c6e7b Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 31 May 2019 16:18:52 -0400
Subject: [PATCH 36/84] use two separate masks for selecting objects in shape
 interpolation mode

---
 .../control/ShapeInterpolationMode.java       | 62 +++++++++++--------
 1 file changed, 36 insertions(+), 26 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 5936ff15f..7e2168e7e 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -104,11 +104,13 @@ private static final class SelectedObjectInfo
 
 	private static final class SectionInfo
 	{
+		final Mask<UnsignedLongType> mask;
 		final AffineTransform3D sourceToDisplayTransform;
 		final Interval sourceBoundingBox;
 
-		SectionInfo(final AffineTransform3D sourceToDisplayTransform, final Interval sourceBoundingBox)
+		SectionInfo(final Mask<UnsignedLongType> mask, final AffineTransform3D sourceToDisplayTransform, final Interval sourceBoundingBox)
 		{
+			this.mask = mask;
 			this.sourceToDisplayTransform = sourceToDisplayTransform;
 			this.sourceBoundingBox = sourceBoundingBox;
 		}
@@ -139,7 +141,6 @@ private static final class SectionInfo
 	private static final int SHAPE_INTERPOLATION_SCALE_LEVEL = MASK_SCALE_LEVEL;
 
 	private static final Color MASK_COLOR = Color.web("00CCFF");
-	private static final Color MASK_COLOR_FIXED_SELECTION = Color.web("44AACC");
 
 	private static final Predicate<UnsignedLongType> FOREGROUND_CHECK = t -> t.get() > 0;
 
@@ -154,14 +155,14 @@ private static final class SectionInfo
 	private long lastSelectedId;
 	private long[] lastActiveIds;
 
-	private Mask<UnsignedLongType> mask;
-	private long currentFillValue;
-
 	private final TLongObjectMap<SelectedObjectInfo> selectedObjects = new TLongObjectHashMap<>();
+	private long currentFillValue;
 
 	private SectionInfo sectionInfo1, sectionInfo2;
+	private Mask<UnsignedLongType> mask;
 
 	private ObjectProperty<ModeState> modeState = new SimpleObjectProperty<>();
+	private long newLabelId;
 
 	private Thread workerThread;
 
@@ -257,19 +258,11 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 		lastAllowedActions = paintera.allowedActionsProperty().get();
 		paintera.allowedActionsProperty().set(allowedActions);
 
-		try
-		{
-			createMask();
-			lastSelectedId = selectedIds.getLastSelection();
-			lastActiveIds = selectedIds.getActiveIds();
-			final long newLabelId = mask.info.value.get();
-			converter.setColor(newLabelId, MASK_COLOR);
-			selectedIds.activate(newLabelId);
-		}
-		catch (final MaskInUse e)
-		{
-			e.printStackTrace();
-		}
+		lastSelectedId = selectedIds.getLastSelection();
+		lastActiveIds = selectedIds.getActiveIds();
+		newLabelId = idService.next();
+		converter.setColor(newLabelId, MASK_COLOR);
+		selectedIds.activate(newLabelId);
 
 		modeState.set(ModeState.Selecting);
 	}
@@ -302,8 +295,8 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed)
 			source.resetMasks();
 		}
 
-		final long newLabelId = mask.info.value.get();
 		converter.removeColor(newLabelId);
+		newLabelId = Label.INVALID;
 
 		paintera.allowedActionsProperty().set(lastAllowedActions);
 		lastAllowedActions = null;
@@ -311,12 +304,12 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed)
 		currentFillValue = 0;
 		selectedObjects.clear();
 		sectionInfo1 = sectionInfo2 = null;
+		mask = null;
 		modeState.set(null);
 
 		workerThread = null;
 		lastSelectedId = Label.INVALID;
 		lastActiveIds = null;
-		mask = null;
 
 		activeViewer.get().requestRepaint();
 		activeViewer.set(null);
@@ -331,10 +324,8 @@ private void createMask() throws MaskInUse
 	{
 		final int time = activeViewer.get().getState().timepointProperty().get();
 		final int level = MASK_SCALE_LEVEL;
-		final long newLabelId = idService.next();
 		final MaskInfo<UnsignedLongType> maskInfo = new MaskInfo<>(time, level, new UnsignedLongType(newLabelId));
 		mask = source.generateMask(maskInfo, FOREGROUND_CHECK);
-		LOG.info("Generated mask for shape interpolation using new label ID {}", newLabelId);
 	}
 
 	private void setDisableOtherViewers(final boolean disable)
@@ -368,7 +359,8 @@ private void fixSelection(final PainteraBaseView paintera)
 			LOG.debug("Fix selection in the first section");
 			sectionInfo1 = createSectionInfo();
 			selectedObjects.clear();
-			converter.setColor(mask.info.value.get(), MASK_COLOR_FIXED_SELECTION);
+			source.resetMasks();
+			mask = null;
 			activeViewerProperty().get().requestRepaint();
 			paintera.allowedActionsProperty().set(allowedActions);
 		}
@@ -403,6 +395,7 @@ private SectionInfo createSectionInfo()
 				selectionSourceBoundingBox = Intervals.union(selectionSourceBoundingBox, it.value().sourceBoundingBox);
 		}
 		return new SectionInfo(
+				mask,
 				getMaskDisplayTransformIgnoreScaling(SHAPE_INTERPOLATION_SCALE_LEVEL),
 				selectionSourceBoundingBox
 			);
@@ -426,7 +419,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 			final RandomAccessibleInterval<UnsignedLongType>[] sectionPair = new RandomAccessibleInterval[2];
 			for (int i = 0; i < 2; ++i)
 			{
-				final SectionInfo newSectionInfo = new SectionInfo(sectionInfoPair[i].sourceToDisplayTransform, affectedUnionSourceInterval);
+				final SectionInfo newSectionInfo = new SectionInfo(sectionInfoPair[i].mask, sectionInfoPair[i].sourceToDisplayTransform, affectedUnionSourceInterval);
 				final RandomAccessibleInterval<UnsignedLongType> section = getTransformedMaskSection(newSectionInfo);
 				displaySectionIntervalPair[i] = new FinalInterval(section);
 				sectionPair[i] = new ArrayImgFactory<>(new UnsignedLongType()).create(section);
@@ -561,12 +554,12 @@ private RandomAccessibleInterval<UnsignedLongType> getTransformedMaskSection(fin
 	{
 		final RealInterval sectionBounds = sectionInfo.sourceToDisplayTransform.estimateBounds(sectionInfo.sourceBoundingBox);
 		final Interval sectionInterval = Intervals.smallestContainingInterval(sectionBounds);
-		final RealRandomAccessible<UnsignedLongType> transformedMask = getTransformedMask(sectionInfo.sourceToDisplayTransform);
+		final RealRandomAccessible<UnsignedLongType> transformedMask = getTransformedMask(sectionInfo.mask, sectionInfo.sourceToDisplayTransform);
 		final RandomAccessibleInterval<UnsignedLongType> transformedMaskInterval = Views.interval(Views.raster(transformedMask), sectionInterval);
 		return Views.hyperSlice(transformedMaskInterval, 2, 0l);
 	}
 
-	private RealRandomAccessible<UnsignedLongType> getTransformedMask(final AffineTransform3D transform)
+	private static RealRandomAccessible<UnsignedLongType> getTransformedMask(final Mask<UnsignedLongType> mask, final AffineTransform3D transform)
 	{
 		final RealRandomAccessible<UnsignedLongType> interpolatedMask = Views.interpolate(
 				Views.extendValue(mask.mask, new UnsignedLongType(Label.OUTSIDE)),
@@ -585,6 +578,16 @@ private static double computeDistanceBetweenSections(final SectionInfo s1, final
 
 	private void selectObject(final PainteraBaseView paintera, final double x, final double y, final boolean deactivateOthers)
 	{
+		// create the mask if needed
+		if (mask == null)
+		{
+			try {
+				createMask();
+			} catch (final MaskInUse e) {
+				e.printStackTrace();
+			}
+		}
+
 		final UnsignedLongType maskValue = getMaskValue(x, y);
 		if (maskValue.get() == Label.OUTSIDE)
 			return;
@@ -617,6 +620,13 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 			selectedObjects.remove(oldFillValue);
 		}
 
+		// free the mask if there are no selected objects
+		if (selectedObjects.isEmpty())
+		{
+			source.resetMasks();
+			mask = null;
+		}
+
 		activeViewer.get().requestRepaint();
 		paintera.allowedActionsProperty().set(selectedObjects.isEmpty() ? allowedActions : allowedActionsWhenSelected);
 	}

From 42954b0b8b9f8b2dd9d34595170c500df0208390 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Mon, 3 Jun 2019 10:32:48 -0400
Subject: [PATCH 37/84] disable commiting canvas and saving project in shape
 interpolation mode

---
 src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java  | 4 ++--
 .../saalfeldlab/paintera/control/actions/MenuAction.java      | 4 +++-
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java b/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java
index 509d59104..5538bb6f5 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java
@@ -288,7 +288,7 @@ public void startImpl(final Stage stage) throws Exception
 						LOG.error("Project undefined");
 					}
 				},
-				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.S)
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.SaveProject) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.S)
 		                   ).installInto(paneWithStatus.getPane());
 
 		EventFX.KEY_PRESSED("commit", e -> {
@@ -305,7 +305,7 @@ public void startImpl(final Stage stage) throws Exception
 						LOG.error("Unable to persist fragment-segment-assignment: {}", e1.getMessage());
 					}
 				},
-				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.C)
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.CommitCanvas) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.C)
 		                   ).installInto(paneWithStatus.getPane());
 
 		keyTracker.installInto(scene);
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java
index d1c1f6d5c..9dc81b202 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java
@@ -10,7 +10,9 @@ public enum MenuAction
 	SidePanel,
 	ToggleViewerMaximizedMinimized,
 	ToggleViewerAndOrthoslicesView,
-	OrthoslicesContextMenu;
+	OrthoslicesContextMenu,
+	SaveProject,
+	CommitCanvas;
 
 	public static EnumSet<MenuAction> of(final MenuAction first, final MenuAction... rest)
 	{

From 252279b4d6260f188a555eb5f6641b329866c241 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Mon, 3 Jun 2019 10:34:41 -0400
Subject: [PATCH 38/84] show message if attempted to save project in shape
 interpolation mode

better than silently ignoring the action
---
 .../saalfeldlab/paintera/Paintera.java        | 54 +++++++++++--------
 1 file changed, 33 insertions(+), 21 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java b/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java
index 5538bb6f5..c65f1dbbf 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java
@@ -1,50 +1,52 @@
 package org.janelia.saalfeldlab.paintera;
 
-import bdv.viewer.ViewerOptions;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonObject;
-import javafx.application.Application;
-import javafx.application.Platform;
-import javafx.scene.Scene;
-import javafx.scene.input.KeyCode;
-import javafx.scene.input.MouseEvent;
-import javafx.stage.Stage;
-import javafx.stage.WindowEvent;
+import java.io.File;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
 import org.janelia.saalfeldlab.fx.event.EventFX;
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.fx.event.MouseTracker;
 import org.janelia.saalfeldlab.fx.ortho.GridConstraintsManager;
 import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews;
-import org.janelia.saalfeldlab.n5.N5Writer;
 import org.janelia.saalfeldlab.paintera.SaveProject.ProjectUndefined;
 import org.janelia.saalfeldlab.paintera.config.CoordinateConfigNode;
 import org.janelia.saalfeldlab.paintera.config.NavigationConfigNode;
 import org.janelia.saalfeldlab.paintera.config.OrthoSliceConfig;
 import org.janelia.saalfeldlab.paintera.config.ScreenScalesConfig;
 import org.janelia.saalfeldlab.paintera.control.CommitChanges;
-import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentState;
+import org.janelia.saalfeldlab.paintera.control.actions.MenuAction;
 import org.janelia.saalfeldlab.paintera.control.assignment.UnableToPersist;
 import org.janelia.saalfeldlab.paintera.data.mask.exception.CannotPersist;
-import org.janelia.saalfeldlab.paintera.id.IdService;
 import org.janelia.saalfeldlab.paintera.serialization.GsonHelpers;
 import org.janelia.saalfeldlab.paintera.serialization.Properties;
 import org.janelia.saalfeldlab.paintera.state.HasSelectedIds;
 import org.janelia.saalfeldlab.paintera.state.SourceState;
+import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts;
 import org.janelia.saalfeldlab.paintera.viewer3d.Viewer3DFX;
 import org.janelia.saalfeldlab.util.n5.N5Helpers;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+
+import bdv.viewer.ViewerOptions;
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.scene.Scene;
+import javafx.scene.control.Alert;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.MouseEvent;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
 import picocli.CommandLine;
 import pl.touk.throwing.ThrowingFunction;
 import pl.touk.throwing.ThrowingSupplier;
 
-import java.io.File;
-import java.io.IOException;
-import java.lang.invoke.MethodHandles;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-
 public class Paintera extends Application
 {
 
@@ -270,6 +272,16 @@ public void startImpl(final Stage stage) throws Exception
 				"save project",
 				e -> {
 					e.consume();
+
+					if (!baseView.allowedActionsProperty().get().isAllowed(MenuAction.SaveProject))
+					{
+						final Alert cannotSaveProjectDialog = PainteraAlerts.alert(Alert.AlertType.WARNING);
+						cannotSaveProjectDialog.setHeaderText("Cannot currently save the project.");
+						cannotSaveProjectDialog.setContentText("Please return to the normal application mode.");
+						cannotSaveProjectDialog.show();
+						return;
+					}
+
 					try
 					{
 						SaveProject.persistProperties(
@@ -288,7 +300,7 @@ public void startImpl(final Stage stage) throws Exception
 						LOG.error("Project undefined");
 					}
 				},
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.SaveProject) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.S)
+				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.S)
 		                   ).installInto(paneWithStatus.getPane());
 
 		EventFX.KEY_PRESSED("commit", e -> {

From 98f15e992318e85d8da1c4ea6978ad6d426e799f Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Mon, 3 Jun 2019 10:46:34 -0400
Subject: [PATCH 39/84] return to normal mode if requested to exit in shape
 interpolation mode

---
 .../paintera/SaveOnExitDialog.java            | 14 ++++---
 .../control/ShapeInterpolationMode.java       | 38 ++++++++++---------
 .../control/actions/AllowedActions.java       | 18 ++++++++-
 3 files changed, 45 insertions(+), 25 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java b/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java
index 901e411fb..62e0d831b 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java
@@ -4,11 +4,6 @@
 import java.lang.invoke.MethodHandles;
 import java.util.Optional;
 
-import javafx.event.EventHandler;
-import javafx.scene.control.ButtonBar.ButtonData;
-import javafx.scene.control.ButtonType;
-import javafx.scene.control.Dialog;
-import javafx.stage.WindowEvent;
 import org.janelia.saalfeldlab.paintera.SaveProject.ProjectUndefined;
 import org.janelia.saalfeldlab.paintera.control.CommitChanges;
 import org.janelia.saalfeldlab.paintera.control.CommitChanges.Commitable;
@@ -17,6 +12,12 @@
 import org.janelia.saalfeldlab.paintera.serialization.Properties;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+
+import javafx.event.EventHandler;
+import javafx.scene.control.ButtonBar.ButtonData;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.Dialog;
+import javafx.stage.WindowEvent;
 import pl.touk.throwing.ThrowingConsumer;
 
 public class SaveOnExitDialog implements EventHandler<WindowEvent>
@@ -62,6 +63,9 @@ public void handle(final WindowEvent event)
 				return;
 			}
 
+			// run cleanup if the application is not currently in the normal mode
+			baseView.allowedActionsProperty().get().cleanup(baseView);
+
 			if (saveButton.equals(response))
 			{
 				LOG.debug("Saving project before exit");
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 7e2168e7e..7e1a4983f 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -2,6 +2,7 @@
 
 import java.lang.invoke.MethodHandles;
 import java.util.Arrays;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
@@ -116,24 +117,6 @@ private static final class SectionInfo
 		}
 	}
 
-	private static final AllowedActions allowedActions;
-	private static final AllowedActions allowedActionsWhenSelected;
-	static
-	{
-		allowedActions = new AllowedActions(
-			NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll),
-			LabelAction.none(),
-			PaintAction.none(),
-			MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized)
-		);
-		allowedActionsWhenSelected = new AllowedActions(
-				NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom),
-				LabelAction.none(),
-				PaintAction.none(),
-				MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized)
-			);
-	}
-
 	private static final double FILL_DEPTH = 2.0;
 
 	private static final int MASK_SCALE_LEVEL = 0;
@@ -151,6 +134,9 @@ private static final class SectionInfo
 	private final IdService idService;
 	private final HighlightingStreamConverter<?> converter;
 
+	private final AllowedActions allowedActions;
+	private final AllowedActions allowedActionsWhenSelected;
+
 	private AllowedActions lastAllowedActions;
 	private long lastSelectedId;
 	private long[] lastActiveIds;
@@ -176,6 +162,22 @@ public ShapeInterpolationMode(
 		this.selectedIds = selectedIds;
 		this.idService = idService;
 		this.converter = converter;
+
+		final Consumer<PainteraBaseView> cleanup = baseView -> exitMode(baseView, false);
+		this.allowedActions = new AllowedActions(
+			NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll),
+			LabelAction.none(),
+			PaintAction.none(),
+			MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized),
+			cleanup
+		);
+		this.allowedActionsWhenSelected = new AllowedActions(
+				NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom),
+				LabelAction.none(),
+				PaintAction.none(),
+				MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized),
+				cleanup
+			);
 	}
 
 	public ObjectProperty<ViewerPanelFX> activeViewerProperty()
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 aca57c552..906040bb5 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
@@ -1,6 +1,9 @@
 package org.janelia.saalfeldlab.paintera.control.actions;
 
 import java.util.EnumSet;
+import java.util.function.Consumer;
+
+import org.janelia.saalfeldlab.paintera.PainteraBaseView;
 
 /**
  * Describes what actions in the UI are allowed in the current application mode.
@@ -12,16 +15,20 @@ public final class AllowedActions
 	private final EnumSet<PaintAction> paintAllowedActions;
 	private final EnumSet<MenuAction> menuAllowedActions;
 
+	private final Consumer<PainteraBaseView> cleanup;
+
 	public AllowedActions(
 			final EnumSet<NavigationAction> navigationAllowedActions,
 			final EnumSet<LabelAction> labelAllowedActions,
 			final EnumSet<PaintAction> paintAllowedActions,
-			final EnumSet<MenuAction> menuAllowedActions)
+			final EnumSet<MenuAction> menuAllowedActions,
+			final Consumer<PainteraBaseView> cleanup)
 	{
 		this.navigationAllowedActions = navigationAllowedActions;
 		this.labelAllowedActions = labelAllowedActions;
 		this.paintAllowedActions = paintAllowedActions;
 		this.menuAllowedActions = menuAllowedActions;
+		this.cleanup = cleanup;
 	}
 
 	public boolean isAllowed(final NavigationAction navigationAction)
@@ -44,13 +51,20 @@ public boolean isAllowed(final MenuAction menuAction)
 		return this.menuAllowedActions.contains(menuAction);
 	}
 
+	public void cleanup(final PainteraBaseView baseView)
+	{
+		if (this.cleanup != null)
+			this.cleanup.accept(baseView);
+	}
+
 	public static AllowedActions all()
 	{
 		return new AllowedActions(
 			NavigationAction.all(),
 			LabelAction.all(),
 			PaintAction.all(),
-			MenuAction.all()
+			MenuAction.all(),
+			null
 		);
 	}
 }

From 322db3473bbcc07ff9c13f0a4c3119c9d070721a Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Mon, 3 Jun 2019 12:29:17 -0400
Subject: [PATCH 40/84] use slightly larger depth for flood filling in rotated
 view

extra 0.5 help to prevent displaying flood-filled sections as stripes instead of a solid object
---
 .../paintera/control/ShapeInterpolationMode.java            | 2 +-
 .../saalfeldlab/paintera/control/paint/FloodFill2D.java     | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 7e1a4983f..1d7c534ba 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -117,7 +117,7 @@ private static final class SectionInfo
 		}
 	}
 
-	private static final double FILL_DEPTH = 2.0;
+	private static final double FILL_DEPTH = 1.0;
 
 	private static final int MASK_SCALE_LEVEL = 0;
 
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
index b40673065..4f57592ab 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
@@ -261,15 +261,15 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 		{
 			FloodFillTransformedPlane.fill(
 					labelToViewerTransform,
-					(0.5 + fillDepth - 1.0) * PaintUtils.maximumVoxelDiagonalLengthPerDimension(
+					fillDepth * PaintUtils.maximumVoxelDiagonalLengthPerDimension(
 							labelTransform,
 							viewerTransform
-					                                                                                      )[2],
+						)[2],
 					extendedFilter.randomAccess(),
 					accessTracker.randomAccess(),
 					new RealPoint(x, y, 0),
 					fillValue
-			                              );
+				);
 			affectedInterval = new FinalInterval(accessTracker.getMin(), accessTracker.getMax());
 		}
 		else

From e513f8b446c7044f5fcc63e27c467d4910875679 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Mon, 3 Jun 2019 15:23:15 -0400
Subject: [PATCH 41/84] make maximized+3d view viewer-specific and allow it in
 interpolation mode

---
 .../saalfeldlab/fx/ortho/OrthogonalViews.java |  18 +-
 .../fx/ortho/ResizableGridPane2x2.java        |  12 +-
 .../paintera/PainteraDefaultHandlers.java     | 158 ++++++++----------
 .../control/ShapeInterpolationMode.java       |  14 +-
 .../paintera/control/actions/MenuAction.java  |   3 +-
 .../paintera/ui/ToggleMaximize.java           |  37 +++-
 6 files changed, 124 insertions(+), 118 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/fx/ortho/OrthogonalViews.java b/src/main/java/org/janelia/saalfeldlab/fx/ortho/OrthogonalViews.java
index b087f5665..b13714a5f 100644
--- a/src/main/java/org/janelia/saalfeldlab/fx/ortho/OrthogonalViews.java
+++ b/src/main/java/org/janelia/saalfeldlab/fx/ortho/OrthogonalViews.java
@@ -5,9 +5,15 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 
+import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners;
+import org.janelia.saalfeldlab.paintera.control.navigation.TransformConcatenator;
+import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder;
+import org.janelia.saalfeldlab.paintera.state.GlobalTransformManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import bdv.cache.CacheControl;
 import bdv.fx.viewer.ViewerPanelFX;
-import bdv.util.volatiles.SharedQueue;
 import bdv.viewer.Interpolation;
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
@@ -16,14 +22,8 @@
 import javafx.event.EventHandler;
 import javafx.event.EventType;
 import javafx.scene.Node;
-import javafx.scene.layout.Pane;
+import javafx.scene.layout.GridPane;
 import net.imglib2.realtransform.AffineTransform3D;
-import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners;
-import org.janelia.saalfeldlab.paintera.control.navigation.TransformConcatenator;
-import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder;
-import org.janelia.saalfeldlab.paintera.state.GlobalTransformManager;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Wrap a {@link ResizableGridPane2x2} with {@link ViewerPanelFX viewer panels} at top left, top right, and bottom left. Bottom right
@@ -157,7 +157,7 @@ public ResizableGridPane2x2<ViewerPanelFX, ViewerPanelFX, ViewerPanelFX, BR> gri
 	 *
 	 * @return {@link ResizableGridPane2x2#pane()} for the underlying {@link ResizableGridPane2x2}
 	 */
-	public Pane pane()
+	public GridPane pane()
 	{
 		return grid().pane();
 	}
diff --git a/src/main/java/org/janelia/saalfeldlab/fx/ortho/ResizableGridPane2x2.java b/src/main/java/org/janelia/saalfeldlab/fx/ortho/ResizableGridPane2x2.java
index a815dfc9a..6bd74c828 100644
--- a/src/main/java/org/janelia/saalfeldlab/fx/ortho/ResizableGridPane2x2.java
+++ b/src/main/java/org/janelia/saalfeldlab/fx/ortho/ResizableGridPane2x2.java
@@ -4,7 +4,6 @@
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.scene.Node;
 import javafx.scene.layout.GridPane;
-import javafx.scene.layout.Pane;
 
 /**
  * A wrapper around {@link GridPane} that holds for children organized in a 2x2 grid. The underlying
@@ -62,7 +61,7 @@ public ResizableGridPane2x2(
 	 *
 	 * @return underlying {@link GridPane}
 	 */
-	public Pane pane()
+	public GridPane pane()
 	{
 		return this.grid;
 	}
@@ -149,11 +148,18 @@ public void manage(final GridConstraintsManager manager)
 		manager.manageGrid(this.grid);
 	}
 
+	public Node getNodeAt(final int col, final int row)
+	{
+		for (final Node child : grid.getChildren())
+			if (GridPane.getColumnIndex(child) == col && GridPane.getRowIndex(child) == row)
+				return child;
+		return null;
+	}
+
 	private static void replace(final GridPane grid, final Node oldValue, final Node newValue, final int col, final int
 			row)
 	{
 		grid.getChildren().remove(oldValue);
 		grid.add(newValue, col, row);
 	}
-
 }
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
index 28e5a8c88..a4e0708dd 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
@@ -1,31 +1,16 @@
 package org.janelia.saalfeldlab.paintera;
 
-import bdv.fx.viewer.ViewerPanelFX;
-import bdv.fx.viewer.multibox.MultiBoxOverlayRendererFX;
-import bdv.viewer.Interpolation;
-import bdv.viewer.Source;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.binding.IntegerBinding;
-import javafx.beans.binding.ObjectBinding;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.ReadOnlyBooleanProperty;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableBooleanValue;
-import javafx.beans.value.ObservableObjectValue;
-import javafx.event.Event;
-import javafx.event.EventHandler;
-import javafx.scene.Node;
-import javafx.scene.control.ContextMenu;
-import javafx.scene.input.KeyCode;
-import javafx.scene.input.KeyEvent;
-import javafx.scene.input.MouseButton;
-import javafx.scene.input.MouseEvent;
-import javafx.scene.layout.Pane;
-import net.imglib2.FinalRealInterval;
-import net.imglib2.Interval;
-import net.imglib2.realtransform.AffineTransform3D;
-import net.imglib2.util.Intervals;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.DoubleSupplier;
+
 import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
 import org.janelia.saalfeldlab.fx.event.EventFX;
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
@@ -37,7 +22,6 @@
 import org.janelia.saalfeldlab.fx.ortho.OnEnterOnExit;
 import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews;
 import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews.ViewerAndTransforms;
-import org.janelia.saalfeldlab.fx.ortho.ViewerAxis;
 import org.janelia.saalfeldlab.fx.ui.Exceptions;
 import org.janelia.saalfeldlab.paintera.control.CurrentSourceVisibilityToggle;
 import org.janelia.saalfeldlab.paintera.control.FitToInterval;
@@ -59,16 +43,31 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.lang.invoke.MethodHandles;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.function.Consumer;
-import java.util.function.DoubleSupplier;
+import bdv.fx.viewer.ViewerPanelFX;
+import bdv.fx.viewer.multibox.MultiBoxOverlayRendererFX;
+import bdv.viewer.Interpolation;
+import bdv.viewer.Source;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.binding.IntegerBinding;
+import javafx.beans.binding.ObjectBinding;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableObjectValue;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Pane;
+import net.imglib2.FinalRealInterval;
+import net.imglib2.Interval;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.util.Intervals;
 
 public class PainteraDefaultHandlers
 {
@@ -97,28 +96,16 @@ public class PainteraDefaultHandlers
 
 	private final Navigation navigation;
 
-//	private final Merges merges;
-//
-//	private final Paint paint;
-//
-//	private final Selection selection;
-
 	private final Consumer<OnEnterOnExit> onEnterOnExit;
 
 	private final ToggleMaximize toggleMaximizeTopLeft;
-
 	private final ToggleMaximize toggleMaximizeTopRight;
-
 	private final ToggleMaximize toggleMaximizeBottomLeft;
 
-	private final ToggleMaximize toggleMaximizeBottomRight;
-
 	private final MultiBoxOverlayRendererFX[] multiBoxes;
 
 	private final GridResizer resizer;
 
-	private final ObservableBooleanValue bottomLeftNeedsZNormal;
-
 	private final EventHandler<KeyEvent> openDatasetContextMenuHandler;
 
 	private final ObjectBinding<EventHandler<Event>> sourceSpecificGlobalEventHandler;
@@ -211,10 +198,9 @@ public PainteraDefaultHandlers(
 				KeyCode.CONTROL,
 				KeyCode.O);
 
-		this.toggleMaximizeTopLeft = toggleMaximizeNode(gridConstraintsManager, 0, 0);
-		this.toggleMaximizeTopRight = toggleMaximizeNode(gridConstraintsManager, 1, 0);
-		this.toggleMaximizeBottomLeft = toggleMaximizeNode(gridConstraintsManager, 0, 1);
-		this.toggleMaximizeBottomRight = toggleMaximizeNode(gridConstraintsManager, 1, 1);
+		this.toggleMaximizeTopLeft = toggleMaximizeNode(orthogonalViews, gridConstraintsManager, 0, 0);
+		this.toggleMaximizeTopRight = toggleMaximizeNode(orthogonalViews, gridConstraintsManager, 1, 0);
+		this.toggleMaximizeBottomLeft = toggleMaximizeNode(orthogonalViews, gridConstraintsManager, 0, 1);
 
 		viewerToTransforms.put(orthogonalViews.topLeft().viewer(), orthogonalViews.topLeft());
 		viewerToTransforms.put(orthogonalViews.topRight().viewer(), orthogonalViews.topRight());
@@ -299,22 +285,35 @@ public PainteraDefaultHandlers(
 				.setInitialTransformToInterval(
 				sourceIntervalInWorldSpace(c.getAddedSubList().get(0)))));
 
+
+		EventFX.KEY_PRESSED(
+				"toggle maximize viewer",
+				e -> toggleMaximizeTopLeft.toggleMaximizeViewer(),
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topLeft().viewer());
+		EventFX.KEY_PRESSED(
+				"toggle maximize viewer",
+				e -> toggleMaximizeTopRight.toggleMaximizeViewer(),
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topRight().viewer());
 		EventFX.KEY_PRESSED(
-				"maximize",
-				e -> toggleMaximizeTopLeft.toggleFullScreen(),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topLeft().viewer());
+				"toggle maximize viewer",
+				e -> toggleMaximizeBottomLeft.toggleMaximizeViewer(),
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.bottomLeft().viewer());
+
 		EventFX.KEY_PRESSED(
-				"maximize",
-				e -> toggleMaximizeTopRight.toggleFullScreen(),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topRight().viewer());
+				"toggle maximize viewer and orthoslice",
+				e -> toggleMaximizeTopLeft.toggleMaximizeViewerAndOrthoslice(),
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.topLeft().viewer());
+
 		EventFX.KEY_PRESSED(
-				"maximize",
-				e -> toggleMaximizeBottomLeft.toggleFullScreen(),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.bottomLeft().viewer());
+				"toggle maximize viewer and orthoslice",
+				e -> toggleMaximizeTopRight.toggleMaximizeViewerAndOrthoslice(),
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.topRight().viewer());
+
 		EventFX.KEY_PRESSED(
-				"maximize",
-				e -> toggleMaximizeBottomRight.toggleFullScreen(),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(baseView.viewer3D());
+				"toggle maximize viewer and orthoslice",
+				e -> toggleMaximizeBottomLeft.toggleMaximizeViewerAndOrthoslice(),
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.bottomLeft().viewer());
+
 
 		final CurrentSourceVisibilityToggle csv = new CurrentSourceVisibilityToggle(sourceInfo.currentState());
 		EventFX.KEY_PRESSED(
@@ -330,33 +329,6 @@ public PainteraDefaultHandlers(
 				e -> sosist.toggleNonSelectionVisibility(),
 				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.V)).installInto(borderPane);
 
-		EventFX.KEY_PRESSED(
-				"toggle maximize bottom row",
-				e -> gridConstraintsManager.maximize(MaximizedRow.BOTTOM, 0),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerAndOrthoslicesView) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(paneWithStatus.getPane());
-
-		bottomLeftNeedsZNormal = Bindings.createBooleanBinding(
-				() -> MaximizedColumn.NONE.equals(gridConstraintsManager.getMaximizedColumn()) && MaximizedRow.BOTTOM
-						.equals(
-						gridConstraintsManager.getMaximizedRow()),
-				gridConstraintsManager.observeMaximizedColumn(),
-				gridConstraintsManager.observeMaximizedRow());
-
-		final AffineTransformWithListeners bottomLeftGlobalToViewer = orthogonalViews.bottomLeft()
-				.globalToViewerTransform();
-		bottomLeftNeedsZNormal.addListener((obs, oldv, newv) -> bottomLeftGlobalToViewer.setTransform(ViewerAxis
-				.globalToViewer(
-				newv ? ViewerAxis.Z : ViewerAxis.Y)));
-
-		if (gridConstraintsManager.isFullScreen() && GridConstraintsManager.MaximizedRow.BOTTOM.equals(
-				gridConstraintsManager.getMaximizedRow()) && GridConstraintsManager.MaximizedColumn.NONE.equals(
-				gridConstraintsManager.getMaximizedColumn()))
-		{
-			orthogonalViews
-					.bottomLeft()
-					.globalToViewerTransform()
-					.setTransform(ViewerAxis.globalToViewer(ViewerAxis.Z));
-		}
 
 		// TODO does MouseEvent.getPickResult make the coordinate tracker
 		// TODO obsolete?
@@ -531,11 +503,13 @@ public static EventHandler<KeyEvent> addOpenDatasetContextMenuHandler(
 	}
 
 	public static ToggleMaximize toggleMaximizeNode(
+			final OrthogonalViews<? extends Node> orthogonalViews,
 			final GridConstraintsManager manager,
 			final int column,
 			final int row)
 	{
 		return new ToggleMaximize(
+				orthogonalViews,
 				manager,
 				MaximizedColumn.fromIndex(column),
 				MaximizedRow.fromIndex(row));
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 1d7c534ba..048a690fd 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -165,17 +165,17 @@ public ShapeInterpolationMode(
 
 		final Consumer<PainteraBaseView> cleanup = baseView -> exitMode(baseView, false);
 		this.allowedActions = new AllowedActions(
-			NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll),
-			LabelAction.none(),
-			PaintAction.none(),
-			MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized),
-			cleanup
-		);
+				NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll),
+				LabelAction.none(),
+				PaintAction.none(),
+				MenuAction.of(MenuAction.ToggleMaximizeViewer),
+				cleanup
+			);
 		this.allowedActionsWhenSelected = new AllowedActions(
 				NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom),
 				LabelAction.none(),
 				PaintAction.none(),
-				MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized),
+				MenuAction.of(MenuAction.ToggleMaximizeViewer),
 				cleanup
 			);
 	}
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java
index 9dc81b202..63b0108c7 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java
@@ -8,8 +8,7 @@ public enum MenuAction
 	CreateNewLabelSource,
 	ChangeActiveSource,
 	SidePanel,
-	ToggleViewerMaximizedMinimized,
-	ToggleViewerAndOrthoslicesView,
+	ToggleMaximizeViewer,
 	OrthoslicesContextMenu,
 	SaveProject,
 	CommitCanvas;
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ui/ToggleMaximize.java b/src/main/java/org/janelia/saalfeldlab/paintera/ui/ToggleMaximize.java
index a9f45421c..7cc59f13e 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/ToggleMaximize.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/ui/ToggleMaximize.java
@@ -3,30 +3,57 @@
 import org.janelia.saalfeldlab.fx.ortho.GridConstraintsManager;
 import org.janelia.saalfeldlab.fx.ortho.GridConstraintsManager.MaximizedColumn;
 import org.janelia.saalfeldlab.fx.ortho.GridConstraintsManager.MaximizedRow;
+import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews;
+
+import javafx.scene.Node;
 
 public class ToggleMaximize
 {
-
+	private final OrthogonalViews<? extends Node> orthogonalViews;
 	private final GridConstraintsManager manager;
 
 	private final MaximizedColumn col;
-
 	private final MaximizedRow row;
 
 	public ToggleMaximize(
+			final OrthogonalViews<? extends Node> orthogonalViews,
 			final GridConstraintsManager manager,
 			final MaximizedColumn col,
 			final MaximizedRow row)
 	{
-		super();
+		this.orthogonalViews = orthogonalViews;
 		this.manager = manager;
 		this.col = col;
 		this.row = row;
 	}
 
-	public void toggleFullScreen()
+	public void toggleMaximizeViewer()
 	{
-		this.manager.maximize(row, col, 200);
+		if (manager.getMaximizedColumn() == MaximizedColumn.NONE && manager.getMaximizedRow() == MaximizedRow.BOTTOM)
+			toggleMaximizeViewerAndOrthoslice();
+		else
+			manager.maximize(row, col, 0);
 	}
 
+	public void toggleMaximizeViewerAndOrthoslice()
+	{
+		if (manager.getMaximizedColumn() != MaximizedColumn.NONE && manager.getMaximizedRow() != MaximizedRow.NONE)
+		{
+			toggleMaximizeViewer();
+			return;
+		}
+
+		if (col != MaximizedColumn.LEFT || row != MaximizedRow.BOTTOM)
+		{
+			final Node swappedNode = orthogonalViews.grid().getNodeAt(col.asIndex(), row.asIndex());
+			final Node bottomLeftNode = orthogonalViews.grid().getNodeAt(MaximizedColumn.LEFT.asIndex(), MaximizedRow.BOTTOM.asIndex());
+
+			orthogonalViews.pane().getChildren().remove(swappedNode);
+			orthogonalViews.pane().getChildren().remove(bottomLeftNode);
+
+			orthogonalViews.pane().add(swappedNode, MaximizedColumn.LEFT.asIndex(), MaximizedRow.BOTTOM.asIndex());
+			orthogonalViews.pane().add(bottomLeftNode, col.asIndex(), row.asIndex());
+		}
+		manager.maximize(MaximizedRow.BOTTOM, 0);
+	}
 }

From f899c8661fad4fa7e9c56fe9c988f136c1ea591d Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Mon, 3 Jun 2019 15:30:59 -0400
Subject: [PATCH 42/84] todo: optimize keeping track of union bounding box for
 distance transform

Currently it's being tracked in the source coordinate space, which may make it unnecessarily large in the viewer space because of the rotating transform from source to display coordinates
---
 .../saalfeldlab/paintera/control/ShapeInterpolationMode.java    | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 048a690fd..29243cc24 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -103,6 +103,8 @@ private static final class SelectedObjectInfo
 		}
 	}
 
+	// TODO: instead of keeping track of the bounding box in the source coordinate space, track affected interval in the current viewer plane.
+	// This would make the union bounding box smaller. Currently it may be larger than necessary because of the rotating transform from source to display coordinates.
 	private static final class SectionInfo
 	{
 		final Mask<UnsignedLongType> mask;

From 25f5952bf27136077c10afa26ba18443aba1933f Mon Sep 17 00:00:00 2001
From: Igor Pisarev <igorpisarev@users.noreply.github.com>
Date: Mon, 3 Jun 2019 15:55:08 -0400
Subject: [PATCH 43/84] add usage of shape interpolation mode to readme

---
 README.md | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/README.md b/README.md
index e4573aa67..2a93e6247 100644
--- a/README.md
+++ b/README.md
@@ -176,6 +176,14 @@ Usage: Paintera [-h] [--default-to-temp-directory] [--print-error-codes]
 | `Ctrl` + `Shift` + `N` | Create new label dataset |
 | `Ctrl` + `T` | Threshold raw source (only available if current source is raw source) |
 
+### Shape interpolation mode
+
+The mode is activated by pressing the `S` key when the current source is a label source. Then, you can select the objects in the first section by left/right clicking and hit `S` to fix the selection and switch to the second section (scrolling through sections works only when there are no selected objects).
+
+When you're done with selecting the objects in the second section, hit `S` again to interpolate between the sections. The interpolated preview will be displayed, where you can hit `S` again to initiate committing the results into the canvas using a new label ID and return back to normal mode.
+
+While in the shape interpolation mode, at any point in time you can hit `ESC` to discard the current state and exit the mode.
+
 ## Data
 
 In [#61](https://github.com/saalfeldlab/paintera/issues/61) we introduced a specification for the data format that Paintera can load through the opener dialog (`Ctrl O`).

From 2c2b5564f712b88ade6ddafb27cac0649d703a23 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 4 Jun 2019 10:44:15 -0400
Subject: [PATCH 44/84] get all viewer panels directly from base view instead
 of parent node

---
 .../control/ShapeInterpolationMode.java       | 23 +++++++++++--------
 1 file changed, 14 insertions(+), 9 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 29243cc24..b0e2e448e 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -38,8 +38,6 @@
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.event.Event;
 import javafx.event.EventHandler;
-import javafx.scene.Node;
-import javafx.scene.Parent;
 import javafx.scene.effect.ColorAdjust;
 import javafx.scene.input.KeyCode;
 import javafx.scene.input.KeyEvent;
@@ -257,7 +255,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 		}
 		LOG.info("Entering shape interpolation mode");
 		activeViewer.set(viewer);
-		setDisableOtherViewers(true);
+		setDisableOtherViewers(paintera, true);
 
 		lastAllowedActions = paintera.allowedActionsProperty().get();
 		paintera.allowedActionsProperty().set(allowedActions);
@@ -279,7 +277,7 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed)
 			return;
 		}
 		LOG.info("Exiting shape interpolation mode");
-		setDisableOtherViewers(false);
+		setDisableOtherViewers(paintera, false);
 
 		if (!completed) // extra cleanup if the mode is aborted
 		{
@@ -332,14 +330,12 @@ private void createMask() throws MaskInUse
 		mask = source.generateMask(maskInfo, FOREGROUND_CHECK);
 	}
 
-	private void setDisableOtherViewers(final boolean disable)
+	private void setDisableOtherViewers(final PainteraBaseView paintera, final boolean disable)
 	{
-		final Parent parent = activeViewer.get().getParent();
-		for (final Node child : parent.getChildrenUnmodifiable())
+		for (final ViewerPanelFX viewer : getViewerPanels(paintera))
 		{
-			if (child instanceof ViewerPanelFX && child != activeViewer.get())
+			if (viewer != activeViewer.get())
 			{
-				final ViewerPanelFX viewer = (ViewerPanelFX) child;
 				viewer.setDisable(disable);
 				if (disable)
 				{
@@ -756,4 +752,13 @@ private double[] getDisplayCoordinates(final RealPoint sourcePos)
 		assert Util.isApproxEqual(displayPos.getDoublePosition(2), 0.0, 1e-10);
 		return new double[] {displayPos.getDoublePosition(0), displayPos.getDoublePosition(1)};
 	}
+
+	private static ViewerPanelFX[] getViewerPanels(final PainteraBaseView paintera)
+	{
+		return new ViewerPanelFX[] {
+				paintera.orthogonalViews().topLeft().viewer(),
+				paintera.orthogonalViews().topRight().viewer(),
+				paintera.orthogonalViews().bottomLeft().viewer()
+			};
+	}
 }

From 3b6c84bb20241412b66845b20a880613f62e5fc8 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 4 Jun 2019 10:44:40 -0400
Subject: [PATCH 45/84] repaint in all viewer panels to properly see 3d
 interpolated shape

---
 .../saalfeldlab/paintera/control/ShapeInterpolationMode.java  | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index b0e2e448e..ef00c96cf 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -477,7 +477,9 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 							FOREGROUND_CHECK
 						);
 				}
-				activeViewer.get().requestRepaint();
+
+				for (final ViewerPanelFX viewer : getViewerPanels(paintera))
+					viewer.requestRepaint();
 			}
 			catch (final MaskInUse e)
 			{

From 41812373072abb860c5e1dd7abfca0da994d68c3 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 4 Jun 2019 12:19:11 -0400
Subject: [PATCH 46/84] revert increased 2d floodfill thickness in arbitrary
 orientation

remove extra 0.5 px so that it only affets the clicked section, which may paint only part of the displayed view if it's rotated and encompasses several sections at once
---
 .../janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
index 4f57592ab..63d0e0f9a 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
@@ -261,7 +261,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 		{
 			FloodFillTransformedPlane.fill(
 					labelToViewerTransform,
-					fillDepth * PaintUtils.maximumVoxelDiagonalLengthPerDimension(
+					(fillDepth - 0.5) * PaintUtils.maximumVoxelDiagonalLengthPerDimension(
 							labelTransform,
 							viewerTransform
 						)[2],

From 323bfd1b0cf6fad6839c417d290a29e2cb0bd587 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 4 Jun 2019 12:23:31 -0400
Subject: [PATCH 47/84] do not stretch virtual mask after interpolation because
 of side effects

The extra 0.5 px on each side was to make sure that the interpolated shape is fully visible in the end sections and doesn't disappear or look like discontinuous stripes. However, this has undesired side effects when painting in an orthogonal view (or very close to it) because it affects adjacent sections outside the defined span.
---
 .../paintera/control/ShapeInterpolationMode.java            | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index ef00c96cf..ee67a6ecb 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -533,13 +533,9 @@ private static <R extends RealType<R>, T extends NativeType<T> & RealType<T>> Re
 				new NLinearInterpolatorFactory<>()
 			);
 
-		final double distanceSign = Math.signum(distance);
-		final double padding = 0.5; // slightly stretches the mask past the end sections to ensure that it's visible in the current plane
 		final RealRandomAccessible<R> scaledInterpolatedDistanceTransform = RealViews.affineReal(
 				interpolatedDistanceTransform,
-				new AffineTransform3D()
-					.preConcatenate(new Scale3D(1, 1, -(distance + padding * distanceSign)))
-					.preConcatenate(new Translation3D(0, 0, padding * 0.5 * distanceSign))
+				new Scale3D(1, 1, -distance)
 			);
 
 		final T emptyValue = targetValue.createVariable();

From 221659d9bb6f368548fa05df8f715467c60f79da Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 4 Jun 2019 15:39:46 -0400
Subject: [PATCH 48/84] more consistent naming of shape interpolation mode
 states

---
 .../paintera/control/ShapeInterpolationMode.java   | 14 +++++++-------
 .../paintera/state/LabelSourceState.java           |  6 +++---
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index ee67a6ecb..b6c6959f9 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -84,8 +84,8 @@ public class ShapeInterpolationMode<D extends IntegerType<D>>
 
 	public static enum ModeState
 	{
-		Selecting,
-		Interpolating,
+		Select,
+		Interpolate,
 		Review
 	}
 
@@ -209,7 +209,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				EventFX.KEY_PRESSED(
 						"fix selection",
 						e -> {e.consume(); fixSelection(paintera);},
-						e -> modeState.get() == ModeState.Selecting &&
+						e -> modeState.get() == ModeState.Select &&
 							!selectedObjects.isEmpty() &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
 					)
@@ -234,12 +234,12 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"select object in current section",
 				e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), true);},
-				e -> modeState.get() == ModeState.Selecting && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
+				e -> modeState.get() == ModeState.Select && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
 			.handler());
 		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"toggle object in current section",
 				e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), false);},
-				e -> modeState.get() == ModeState.Selecting &&
+				e -> modeState.get() == ModeState.Select &&
 					((e.isSecondaryButtonDown() && keyTracker.noKeysActive()) ||
 					(e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL))))
 			.handler());
@@ -266,7 +266,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 		converter.setColor(newLabelId, MASK_COLOR);
 		selectedIds.activate(newLabelId);
 
-		modeState.set(ModeState.Selecting);
+		modeState.set(ModeState.Select);
 	}
 
 	public void exitMode(final PainteraBaseView paintera, final boolean completed)
@@ -404,7 +404,7 @@ private SectionInfo createSectionInfo()
 	@SuppressWarnings("unchecked")
 	private void interpolateBetweenSections(final PainteraBaseView paintera)
 	{
-		modeState.set(ModeState.Interpolating);
+		modeState.set(ModeState.Interpolate);
 
 		workerThread = new Thread(() ->
 		{
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
index 719e11f39..99ea369e7 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
@@ -609,9 +609,9 @@ private HBox createDisplayStatus()
 
 		this.shapeInterpolationMode.modeStateProperty().addListener((obs, oldv, newv) -> {
 			InvokeOnJavaFXApplicationThread.invoke(() -> {
-				paintingProgressIndicator.setVisible(newv == ModeState.Interpolating);
-				if (newv == ModeState.Interpolating)
-					paintingProgressIndicatorTooltip.setText("Interpolating between sections...");
+				final boolean showProgressIndicator = newv == ModeState.Interpolate;
+				paintingProgressIndicator.setVisible(showProgressIndicator);
+				paintingProgressIndicatorTooltip.setText(showProgressIndicator ? "Interpolating between sections..." : "");
 			});
 		});
 

From 59dde9370652ab52015f76dd5b18814cfb04250e Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 4 Jun 2019 15:41:26 -0400
Subject: [PATCH 49/84] allow to set mask created by generateMask() earlier in
 the label source

---
 .../paintera/data/mask/MaskedSource.java      | 35 +++++++++++++++++++
 1 file changed, 35 insertions(+)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java
index 1c2568f70..b07a4406a 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java
@@ -320,6 +320,41 @@ public Mask<UnsignedLongType> generateMask(
 		return mask;
 	}
 
+	public void setMask(
+			final Mask<UnsignedLongType> mask,
+			final Predicate<UnsignedLongType> isPaintedForeground)
+	throws MaskInUse
+	{
+		synchronized (this)
+		{
+			final boolean canSetMask = !isCreatingMask && currentMask == null && !isApplyingMask.get() && !isPersisting;
+			LOG.debug("Can set mask? {}", canSetMask);
+			if (!canSetMask)
+			{
+				LOG.error(
+						"Currently processing, cannot set new mask: persisting? {} mask in use? {}",
+						isPersisting,
+						currentMask
+				         );
+				throw new MaskInUse("Busy, cannot set new mask.");
+			}
+		}
+
+		final RandomAccessibleInterval<UnsignedLongType> store;
+		if (mask.mask instanceof AccessedBlocksRandomAccessible<?>)
+			store = ((AccessedBlocksRandomAccessible<UnsignedLongType>) mask.mask).getSource();
+		else
+			store = mask.mask;
+		final RandomAccessibleInterval<VolatileUnsignedLongType> vstore = VolatileViews.wrapAsVolatile(store);
+
+		setMasks(store, vstore, mask.info.level, mask.info.value, isPaintedForeground);
+
+		synchronized(this)
+		{
+			this.currentMask = mask;
+		}
+	}
+
 	public void setMask(
 			final MaskInfo<UnsignedLongType> maskInfo,
 			final RealRandomAccessible<UnsignedLongType> mask,

From 11f1fd2215e9fe0391e1b066e1aa4ddff774b773 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 4 Jun 2019 15:45:42 -0400
Subject: [PATCH 50/84] allow to edit selection when previewing shape
 interpolation results

---
 .../control/ShapeInterpolationMode.java       | 145 ++++++++++++++----
 1 file changed, 115 insertions(+), 30 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index b6c6959f9..c1b44ae6c 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -106,14 +106,23 @@ private static final class SelectedObjectInfo
 	private static final class SectionInfo
 	{
 		final Mask<UnsignedLongType> mask;
+		final AffineTransform3D globalTransform;
 		final AffineTransform3D sourceToDisplayTransform;
 		final Interval sourceBoundingBox;
-
-		SectionInfo(final Mask<UnsignedLongType> mask, final AffineTransform3D sourceToDisplayTransform, final Interval sourceBoundingBox)
+		final TLongObjectMap<SelectedObjectInfo> selectedObjects;
+
+		SectionInfo(
+				final Mask<UnsignedLongType> mask,
+				final AffineTransform3D globalTransform,
+				final AffineTransform3D sourceToDisplayTransform,
+				final Interval sourceBoundingBox,
+				final TLongObjectMap<SelectedObjectInfo> selectedObjects)
 		{
 			this.mask = mask;
+			this.globalTransform = globalTransform;
 			this.sourceToDisplayTransform = sourceToDisplayTransform;
 			this.sourceBoundingBox = sourceBoundingBox;
+			this.selectedObjects = selectedObjects;
 		}
 	}
 
@@ -141,16 +150,19 @@ private static final class SectionInfo
 	private long lastSelectedId;
 	private long[] lastActiveIds;
 
-	private final TLongObjectMap<SelectedObjectInfo> selectedObjects = new TLongObjectHashMap<>();
+	private Mask<UnsignedLongType> mask;
+	private long newLabelId;
 	private long currentFillValue;
 
-	private SectionInfo sectionInfo1, sectionInfo2;
-	private Mask<UnsignedLongType> mask;
+	private final TLongObjectMap<SelectedObjectInfo> selectedObjects = new TLongObjectHashMap<>();
 
-	private ObjectProperty<ModeState> modeState = new SimpleObjectProperty<>();
-	private long newLabelId;
+	private final ObjectProperty<SectionInfo> sectionInfo1 = new SimpleObjectProperty<>();
+	private final ObjectProperty<SectionInfo> sectionInfo2 = new SimpleObjectProperty<>();
+
+	private final ObjectProperty<ModeState> modeState = new SimpleObjectProperty<>();
 
 	private Thread workerThread;
+	private ObjectProperty<SectionInfo> editedSectionInfo = null;
 
 	public ShapeInterpolationMode(
 			final MaskedSource<D, ?> source,
@@ -231,6 +243,25 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 						e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE)
 					)
 			);
+		filter.addEventHandler(
+				KeyEvent.KEY_PRESSED,
+				EventFX.KEY_PRESSED(
+						"edit selection 1",
+						e -> {e.consume(); editSelection(paintera, sectionInfo1);},
+						e -> modeState.get() == ModeState.Review &&
+							keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT1)
+					)
+			);
+		filter.addEventHandler(
+				KeyEvent.KEY_PRESSED,
+				EventFX.KEY_PRESSED(
+						"edit selection 2",
+						e -> {e.consume(); editSelection(paintera, sectionInfo2);},
+						e -> modeState.get() == ModeState.Review &&
+							keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT2)
+					)
+			);
+
 		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"select object in current section",
 				e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), true);},
@@ -243,6 +274,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 					((e.isSecondaryButtonDown() && keyTracker.noKeysActive()) ||
 					(e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL))))
 			.handler());
+
 		return filter;
 	}
 
@@ -294,7 +326,7 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed)
 			selectedIds.activate(lastActiveIds);
 			selectedIds.activateAlso(lastSelectedId);
 
-			source.resetMasks();
+			resetMask();
 		}
 
 		converter.removeColor(newLabelId);
@@ -305,9 +337,11 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed)
 
 		currentFillValue = 0;
 		selectedObjects.clear();
-		sectionInfo1 = sectionInfo2 = null;
-		mask = null;
+		sectionInfo1.set(null);
+		sectionInfo2.set(null);
 		modeState.set(null);
+		editedSectionInfo = null;
+		mask = null;
 
 		workerThread = null;
 		lastSelectedId = Label.INVALID;
@@ -330,6 +364,12 @@ private void createMask() throws MaskInUse
 		mask = source.generateMask(maskInfo, FOREGROUND_CHECK);
 	}
 
+	private void resetMask()
+	{
+		source.resetMasks();
+		mask = null;
+	}
+
 	private void setDisableOtherViewers(final PainteraBaseView paintera, final boolean disable)
 	{
 		for (final ViewerPanelFX viewer : getViewerPanels(paintera))
@@ -354,36 +394,67 @@ private void setDisableOtherViewers(final PainteraBaseView paintera, final boole
 
 	private void fixSelection(final PainteraBaseView paintera)
 	{
-		if (sectionInfo1 == null)
+		final ObjectProperty<SectionInfo> sectionInfoPropertyToSet;
+		if (editedSectionInfo != null)
+			sectionInfoPropertyToSet = editedSectionInfo;
+		else if (sectionInfo1.get() == null)
+			sectionInfoPropertyToSet = sectionInfo1;
+		else
+			sectionInfoPropertyToSet = sectionInfo2;
+
+		LOG.debug("Fix selection");
+		sectionInfoPropertyToSet.set(createSectionInfo(paintera));
+		selectedObjects.clear();
+		editedSectionInfo = null;
+
+		if (sectionInfo2.get() == null)
 		{
-			LOG.debug("Fix selection in the first section");
-			sectionInfo1 = createSectionInfo();
-			selectedObjects.clear();
-			source.resetMasks();
-			mask = null;
+			// let the user now select the second section
+			resetMask();
 			activeViewerProperty().get().requestRepaint();
 			paintera.allowedActionsProperty().set(allowedActions);
 		}
 		else
 		{
-			LOG.debug("Fix selection in the second section");
-			sectionInfo2 = createSectionInfo();
+			// both sections are ready, run interpolation
 			interpolateBetweenSections(paintera);
 		}
 	}
 
+	private void editSelection(final PainteraBaseView paintera, final ObjectProperty<SectionInfo> sectionInfoProperty)
+	{
+		final SectionInfo sectionInfo = sectionInfoProperty.get();
+
+		resetMask();
+		try {
+			source.setMask(sectionInfo.mask, FOREGROUND_CHECK);
+		} catch (final MaskInUse e) {
+			e.printStackTrace();
+		}
+		mask = sectionInfo.mask;
+
+		paintera.manager().setTransform(sectionInfo.globalTransform);
+		paintera.allowedActionsProperty().set(allowedActionsWhenSelected);
+
+		selectedObjects.clear();
+		selectedObjects.putAll(sectionInfo.selectedObjects);
+
+		editedSectionInfo = sectionInfoProperty;
+		modeState.set(ModeState.Select);
+	}
+
 	private void applyMask(final PainteraBaseView paintera)
 	{
 		final Interval sectionsUnionSourceInterval = Intervals.union(
-				sectionInfo1.sourceBoundingBox,
-				sectionInfo2.sourceBoundingBox
+				sectionInfo1.get().sourceBoundingBox,
+				sectionInfo2.get().sourceBoundingBox
 			);
 		LOG.info("Applying interpolated mask using bounding box of size {}", Intervals.dimensionsAsLongArray(sectionsUnionSourceInterval));
 		source.applyMask(source.getCurrentMask(), sectionsUnionSourceInterval, FOREGROUND_CHECK);
 		exitMode(paintera, true);
 	}
 
-	private SectionInfo createSectionInfo()
+	private SectionInfo createSectionInfo(final PainteraBaseView paintera)
 	{
 		Interval selectionSourceBoundingBox = null;
 		for (final TLongObjectIterator<SelectedObjectInfo> it = selectedObjects.iterator(); it.hasNext();)
@@ -394,10 +465,16 @@ private SectionInfo createSectionInfo()
 			else
 				selectionSourceBoundingBox = Intervals.union(selectionSourceBoundingBox, it.value().sourceBoundingBox);
 		}
+
+		final AffineTransform3D globalTransform = new AffineTransform3D();
+		paintera.manager().getTransform(globalTransform);
+
 		return new SectionInfo(
 				mask,
+				globalTransform,
 				getMaskDisplayTransformIgnoreScaling(SHAPE_INTERPOLATION_SCALE_LEVEL),
-				selectionSourceBoundingBox
+				selectionSourceBoundingBox,
+				new TLongObjectHashMap<>(selectedObjects)
 			);
 	}
 
@@ -408,7 +485,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 
 		workerThread = new Thread(() ->
 		{
-			final SectionInfo[] sectionInfoPair = {sectionInfo1, sectionInfo2};
+			final SectionInfo[] sectionInfoPair = {sectionInfo1.get(), sectionInfo2.get()};
 
 			final Interval affectedUnionSourceInterval = Intervals.union(
 					sectionInfoPair[0].sourceBoundingBox,
@@ -419,7 +496,13 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 			final RandomAccessibleInterval<UnsignedLongType>[] sectionPair = new RandomAccessibleInterval[2];
 			for (int i = 0; i < 2; ++i)
 			{
-				final SectionInfo newSectionInfo = new SectionInfo(sectionInfoPair[i].mask, sectionInfoPair[i].sourceToDisplayTransform, affectedUnionSourceInterval);
+				final SectionInfo newSectionInfo = new SectionInfo(
+						sectionInfoPair[i].mask,
+						sectionInfoPair[i].globalTransform,
+						sectionInfoPair[i].sourceToDisplayTransform,
+						affectedUnionSourceInterval,
+						sectionInfoPair[i].selectedObjects
+					);
 				final RandomAccessibleInterval<UnsignedLongType> section = getTransformedMaskSection(newSectionInfo);
 				displaySectionIntervalPair[i] = new FinalInterval(section);
 				sectionPair[i] = new ArrayImgFactory<>(new UnsignedLongType()).create(section);
@@ -440,11 +523,11 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 				computeSignedDistanceTransform(binarySection, distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN);
 			}
 
-			final double distanceBetweenSections = computeDistanceBetweenSections(sectionInfo1, sectionInfo2);
+			final double distanceBetweenSections = computeDistanceBetweenSections(sectionInfoPair[0], sectionInfoPair[1]);
 			final AffineTransform3D transformToSource = new AffineTransform3D();
 			transformToSource
 				.preConcatenate(new Translation3D(displaySectionIntervalPair[0].min(0), displaySectionIntervalPair[0].min(1), 0))
-				.preConcatenate(sectionInfo1.sourceToDisplayTransform.inverse());
+				.preConcatenate(sectionInfoPair[0].sourceToDisplayTransform.inverse());
 
 			final RealRandomAccessible<UnsignedLongType> interpolatedShapeMask = getInterpolatedDistanceTransformMask(
 					distanceTransformPair[0],
@@ -469,9 +552,10 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 			{
 				synchronized (source)
 				{
-					source.resetMasks();
+					final MaskInfo<UnsignedLongType> maskInfo = mask.info;
+					resetMask();
 					source.setMask(
-							mask.info,
+							maskInfo,
 							interpolatedShapeMask,
 							volatileInterpolatedShapeMask,
 							FOREGROUND_CHECK
@@ -579,6 +663,7 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 		// create the mask if needed
 		if (mask == null)
 		{
+			LOG.debug("No selected objects yet, create mask");
 			try {
 				createMask();
 			} catch (final MaskInUse e) {
@@ -621,8 +706,8 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 		// free the mask if there are no selected objects
 		if (selectedObjects.isEmpty())
 		{
-			source.resetMasks();
-			mask = null;
+			LOG.debug("No selected objects, reset mask");
+			resetMask();
 		}
 
 		activeViewer.get().requestRepaint();

From 8c45b8f01dcc3788271928d40c6faefd9e110b62 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <igorpisarev@users.noreply.github.com>
Date: Tue, 4 Jun 2019 16:19:35 -0400
Subject: [PATCH 51/84] describe section editing functionality in readme

---
 README.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 2a93e6247..ddd680b36 100644
--- a/README.md
+++ b/README.md
@@ -163,6 +163,7 @@ Usage: Paintera [-h] [--default-to-temp-directory] [--print-error-codes]
 | `Shift` + `F` + left click | Flood-fill with id that was last toggled active (if any) |
 | `N` | Select new, previously unused id |
 | `S` | Enter/advance shape interpolation mode |
+| `1` / `2` | Edit first/second section when previewing interpolated shape |
 | `ESC` | Exit shape interpolation mode |
 | `Ctrl` + `C` | Show dialog to commit canvas and/or assignments |
 | `C` | Increment ARGB stream seed by one |
@@ -180,7 +181,7 @@ Usage: Paintera [-h] [--default-to-temp-directory] [--print-error-codes]
 
 The mode is activated by pressing the `S` key when the current source is a label source. Then, you can select the objects in the first section by left/right clicking and hit `S` to fix the selection and switch to the second section (scrolling through sections works only when there are no selected objects).
 
-When you're done with selecting the objects in the second section, hit `S` again to interpolate between the sections. The interpolated preview will be displayed, where you can hit `S` again to initiate committing the results into the canvas using a new label ID and return back to normal mode.
+When you're done with selecting the objects in the second section, hit `S` again to interpolate between the sections. The interpolated preview will be displayed, where you can scroll through the sections and make sure that the interpolated shape is correct. If something is not right, you can edit the selection in the first or second section by pressing `1` or `2`, and then press `S` to update the interpolated shape. When the desired result is reached, hit `S` again to initiate committing the results into the canvas using a new label ID and return back to normal mode.
 
 While in the shape interpolation mode, at any point in time you can hit `ESC` to discard the current state and exit the mode.
 

From 19d3448a40ec9a95d6a3286ef3fe8378a63e7803 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 4 Jun 2019 17:00:36 -0400
Subject: [PATCH 52/84] generate mesh for interpolated shape once committed
 into canvas

---
 .../control/ShapeInterpolationMode.java       | 20 +++++++++++++++++++
 .../paintera/state/LabelSourceState.java      |  4 ++--
 2 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index c1b44ae6c..f7c27016d 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -25,6 +25,7 @@
 import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
 import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse;
 import org.janelia.saalfeldlab.paintera.id.IdService;
+import org.janelia.saalfeldlab.paintera.state.LabelSourceState;
 import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -36,6 +37,7 @@
 import gnu.trove.map.hash.TLongObjectHashMap;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ChangeListener;
 import javafx.event.Event;
 import javafx.event.EventHandler;
 import javafx.scene.effect.ColorAdjust;
@@ -139,6 +141,7 @@ private static final class SectionInfo
 	private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>();
 
 	private final MaskedSource<D, ?> source;
+	private final LabelSourceState<D, ?> sourceState;
 	private final SelectedIds selectedIds;
 	private final IdService idService;
 	private final HighlightingStreamConverter<?> converter;
@@ -146,6 +149,8 @@ private static final class SectionInfo
 	private final AllowedActions allowedActions;
 	private final AllowedActions allowedActionsWhenSelected;
 
+	private final ChangeListener<Boolean> doneApplyingMaskListener;
+
 	private AllowedActions lastAllowedActions;
 	private long lastSelectedId;
 	private long[] lastActiveIds;
@@ -166,11 +171,13 @@ private static final class SectionInfo
 
 	public ShapeInterpolationMode(
 			final MaskedSource<D, ?> source,
+			final LabelSourceState<D, ?> sourceState,
 			final SelectedIds selectedIds,
 			final IdService idService,
 			final HighlightingStreamConverter<?> converter)
 	{
 		this.source = source;
+		this.sourceState = sourceState;
 		this.selectedIds = selectedIds;
 		this.idService = idService;
 		this.converter = converter;
@@ -190,6 +197,11 @@ public ShapeInterpolationMode(
 				MenuAction.of(MenuAction.ToggleMaximizeViewer),
 				cleanup
 			);
+
+		this.doneApplyingMaskListener = (obs, oldv, newv) -> {
+			if (!newv)
+				doneApplyingMask();
+		};
 	}
 
 	public ObjectProperty<ViewerPanelFX> activeViewerProperty()
@@ -450,10 +462,18 @@ private void applyMask(final PainteraBaseView paintera)
 				sectionInfo2.get().sourceBoundingBox
 			);
 		LOG.info("Applying interpolated mask using bounding box of size {}", Intervals.dimensionsAsLongArray(sectionsUnionSourceInterval));
+		source.isApplyingMaskProperty().addListener(doneApplyingMaskListener);
 		source.applyMask(source.getCurrentMask(), sectionsUnionSourceInterval, FOREGROUND_CHECK);
 		exitMode(paintera, true);
 	}
 
+	private void doneApplyingMask()
+	{
+		// generate mesh for the interpolated shape
+		source.isApplyingMaskProperty().removeListener(doneApplyingMaskListener);
+		sourceState.refreshMeshes();
+	}
+
 	private SectionInfo createSectionInfo(final PainteraBaseView paintera)
 	{
 		Interval selectionSourceBoundingBox = null;
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
index 99ea369e7..46f25103a 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
@@ -157,7 +157,7 @@ public LabelSourceState(
 		this.idSelectorHandler = new LabelSourceStateIdSelectorHandler(dataSource, selectedIds, assignment, lockedSegments);
 		this.mergeDetachHandler = new LabelSourceStateMergeDetachHandler(dataSource, selectedIds, assignment, idService);
 		if (dataSource instanceof MaskedSource<?, ?>)
-			this.shapeInterpolationMode = new ShapeInterpolationMode<>((MaskedSource<D, ?>) dataSource, selectedIds, idService, converter);
+			this.shapeInterpolationMode = new ShapeInterpolationMode<>((MaskedSource<D, ?>) dataSource, this, selectedIds, idService, converter);
 		else
 			this.shapeInterpolationMode = null;
 		this.displayStatus = createDisplayStatus();
@@ -468,7 +468,7 @@ public EventHandler<Event> stateSpecificGlobalEventHandler(final PainteraBaseVie
 		handler.addEventHandler(
 				KeyEvent.KEY_PRESSED,
 				EventFX.KEY_PRESSED("refresh meshes", e -> {LOG.debug("Key event triggered refresh meshes"); refreshMeshes();}, e -> keyTracker.areOnlyTheseKeysDown(KeyCode.R)));
-			return handler;
+		return handler;
 	}
 
 //	@Override

From b11b35d1cf0b88dc22c23f34b40a6495ad44e23a Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 5 Jun 2019 12:46:56 -0400
Subject: [PATCH 53/84] do not automatically hide side bar upon entering shape
 interpolation mode

---
 .../saalfeldlab/paintera/BorderPaneWithStatusBars.java     | 5 ++---
 .../saalfeldlab/paintera/PainteraDefaultHandlers.java      | 7 ++-----
 2 files changed, 4 insertions(+), 8 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java
index b1c0c9f53..987563be9 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java
@@ -279,9 +279,9 @@ public BorderPaneWithStatusBars(
 		resizeSideBar = new ResizeOnLeftSide(sideBar, sideBar.prefWidthProperty(), dist -> Math.abs(dist) < 5);
 	}
 
-	public boolean isSideBarActive()
+	public ScrollPane getSideBar()
 	{
-		return pane.getRight() != null;
+		return sideBar;
 	}
 
 	public void toggleSideBar()
@@ -291,7 +291,6 @@ public void toggleSideBar()
 			pane.setRight(sideBar);
 			resizeSideBar.install();
 		}
-
 		else
 		{
 			resizeSideBar.remove();
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
index a4e0708dd..50b12d360 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
@@ -235,16 +235,13 @@ public PainteraDefaultHandlers(
 		final EventFX<KeyEvent> toggleSideBar = EventFX.KEY_RELEASED(
 				"toggle sidebar",
 				e -> paneWithStatus.toggleSideBar(),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.SidePanel) && keyTracker.areOnlyTheseKeysDown(KeyCode.P)
+				e -> keyTracker.areOnlyTheseKeysDown(KeyCode.P)
 			);
 		borderPane.sceneProperty().addListener((obs, oldv, newv) -> newv.addEventHandler(
 				KeyEvent.KEY_PRESSED,
 				toggleSideBar));
 
-		baseView.allowedActionsProperty().addListener((obs, oldv, newv) -> {
-			if (!newv.isAllowed(MenuAction.SidePanel) && paneWithStatus.isSideBarActive())
-				paneWithStatus.toggleSideBar();
-		});
+		baseView.allowedActionsProperty().addListener((obs, oldv, newv) -> paneWithStatus.getSideBar().setDisable(!newv.isAllowed(MenuAction.SidePanel)));
 
 		EventFX.KEY_PRESSED(
 				"toggle interpolation",

From 83fd6ec9ccab609949bcb4461f50354749935ef0 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 5 Jun 2019 16:24:39 -0400
Subject: [PATCH 54/84] allow source state to set its status text for
 displaying

---
 .../paintera/BorderPaneWithStatusBars.java    | 87 ++++++++++++-------
 .../paintera/state/MinimalSourceState.java    |  8 ++
 .../paintera/state/SourceInfo.java            | 10 ---
 .../paintera/state/SourceState.java           |  2 +
 4 files changed, 67 insertions(+), 40 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java
index 987563be9..4e91ee554 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java
@@ -4,34 +4,14 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Optional;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
-import java.util.function.*;
+import java.util.function.BiConsumer;
+import java.util.function.LongSupplier;
+import java.util.function.LongUnaryOperator;
+import java.util.function.Supplier;
 
-import bdv.fx.viewer.ViewerPanelFX;
-import bdv.viewer.Source;
-import javafx.animation.KeyFrame;
-import javafx.animation.Timeline;
-import javafx.beans.binding.Bindings;
-import javafx.beans.property.*;
-import javafx.beans.value.ObservableObjectValue;
-import javafx.event.ActionEvent;
-import javafx.event.EventHandler;
-import javafx.scene.Group;
-import javafx.scene.control.*;
-import javafx.scene.control.ScrollPane.ScrollBarPolicy;
-import javafx.scene.input.MouseEvent;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.layout.HBox;
-import javafx.scene.layout.Priority;
-import javafx.scene.layout.Region;
-import javafx.scene.layout.VBox;
-import javafx.scene.paint.Color;
-import javafx.scene.text.Font;
-import javafx.util.Duration;
-import net.imglib2.RealPoint;
 import org.janelia.saalfeldlab.fx.TitledPanes;
 import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews;
 import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews.ViewerAndTransforms;
@@ -57,6 +37,40 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import bdv.fx.viewer.ViewerPanelFX;
+import bdv.viewer.Source;
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.LongProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ObservableObjectValue;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.Group;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.ScrollPane.ScrollBarPolicy;
+import javafx.scene.control.TitledPane;
+import javafx.scene.control.Tooltip;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.scene.text.Font;
+import javafx.util.Duration;
+import net.imglib2.RealPoint;
+
 public class BorderPaneWithStatusBars
 {
 
@@ -160,16 +174,29 @@ public BorderPaneWithStatusBars(
 				center.orthogonalViews(),
 				center.viewer3D().meshesGroup(),
 				center.sourceInfo()
-		                                  );
-
-		center.sourceInfo().currentNameProperty().addListener((obs, oldv, newv) -> {
-			currentSourceStatus.textProperty().unbind();
-			Optional.ofNullable(newv).ifPresent(currentSourceStatus.textProperty()::bind);
-		});
+			);
 
 		final SingleChildStackPane sourceDisplayStatus = new SingleChildStackPane();
 		center.sourceInfo().currentState().addListener((obs, oldv, newv) -> sourceDisplayStatus.setChild(newv.getDisplayStatus()));
 
+		// show source name by default, or override it with source status text if any
+		center.sourceInfo().currentState().addListener((obs, oldv, newv) -> {
+			sourceDisplayStatus.setChild(newv.getDisplayStatus());
+			currentSourceStatus.textProperty().unbind();
+			currentSourceStatus.textProperty().bind(Bindings.createStringBinding(
+					() -> {
+						if (newv.statusTextProperty() != null && newv.statusTextProperty().get() != null)
+							return newv.statusTextProperty().get();
+						else if (newv.nameProperty().get() != null)
+							return newv.nameProperty().get();
+						else
+							return null;
+					},
+					newv.nameProperty(),
+					newv.statusTextProperty()
+				));
+		});
+
 		// for positioning the 'show status bar' checkbox on the right
 		final Region valueStatusSpacing = new Region();
 		HBox.setHgrow(valueStatusSpacing, Priority.ALWAYS);
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/MinimalSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/MinimalSourceState.java
index 966c1e8c7..dbc112fba 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/MinimalSourceState.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/MinimalSourceState.java
@@ -34,6 +34,8 @@ public class MinimalSourceState<D, T, S extends DataSource<D, T>, C extends Conv
 
 	private final StringProperty name;
 
+	private final StringProperty statusText = new SimpleStringProperty(null);
+
 	private final BooleanProperty isVisible = new SimpleBooleanProperty(true);
 
 	private final BooleanProperty isDirty = new SimpleBooleanProperty(false);
@@ -100,6 +102,12 @@ public StringProperty nameProperty()
 		return this.name;
 	}
 
+	@Override
+	public StringProperty statusTextProperty()
+	{
+		return this.statusText;
+	}
+
 	@Override
 	public BooleanProperty isVisibleProperty()
 	{
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceInfo.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceInfo.java
index a20676a41..bac6b123d 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceInfo.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceInfo.java
@@ -12,7 +12,6 @@
 import javafx.beans.property.SimpleBooleanProperty;
 import javafx.beans.property.SimpleIntegerProperty;
 import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.property.StringProperty;
 import javafx.beans.value.ObservableBooleanValue;
 import javafx.beans.value.ObservableIntegerValue;
 import javafx.beans.value.ObservableObjectValue;
@@ -122,10 +121,6 @@ public class SourceInfo
 			() -> Optional.ofNullable(currentSource.get()).map(this::getState).orElse(null),
 			currentSource);
 
-	private final ObservableObjectValue<StringProperty> currentName = Bindings.createObjectBinding(
-			() -> Optional.ofNullable(currentState.get()).map(SourceState::nameProperty).orElse(null),
-			currentState);
-
 	private final ObservableMap<Source<?>, Composite<ARGBType, ARGBType>> composites = FXCollections
 			.observableHashMap();
 
@@ -437,9 +432,4 @@ public int indexOf(final Source<?> source)
 		return this.currentState;
 	}
 
-	public ObservableObjectValue<StringProperty> currentNameProperty()
-	{
-		return this.currentName;
-	}
-
 }
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceState.java
index cf27258bb..a4060bc05 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceState.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceState.java
@@ -34,6 +34,8 @@ public interface SourceState<D, T> extends HasModifiableAxisOrder
 
 	StringProperty nameProperty();
 
+	StringProperty statusTextProperty();
+
 	BooleanProperty isVisibleProperty();
 
 	ObservableBooleanValue isDirtyProperty();

From c0305a660449eacb63805d31e127b927ec4a5af0 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 5 Jun 2019 16:25:57 -0400
Subject: [PATCH 55/84] display shape interpolation mode status on each step

---
 .../control/ShapeInterpolationMode.java       |  90 ++++++++-----
 .../paintera/state/LabelSourceState.java      | 122 +++++++++++-------
 2 files changed, 131 insertions(+), 81 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index f7c27016d..ccc13e8ef 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -88,7 +88,27 @@ public static enum ModeState
 	{
 		Select,
 		Interpolate,
-		Review
+		Preview,
+		Edit
+	}
+
+	public static enum ActiveSection
+	{
+		First("1"),
+		Second("2");
+
+		private final String s;
+
+		private ActiveSection(final String s)
+		{
+			this.s = s;
+		}
+
+		@Override
+		public String toString()
+		{
+			return s;
+		}
 	}
 
 	private static final class SelectedObjectInfo
@@ -165,9 +185,9 @@ private static final class SectionInfo
 	private final ObjectProperty<SectionInfo> sectionInfo2 = new SimpleObjectProperty<>();
 
 	private final ObjectProperty<ModeState> modeState = new SimpleObjectProperty<>();
+	private final ObjectProperty<ActiveSection> activeSection = new SimpleObjectProperty<>();
 
 	private Thread workerThread;
-	private ObjectProperty<SectionInfo> editedSectionInfo = null;
 
 	public ShapeInterpolationMode(
 			final MaskedSource<D, ?> source,
@@ -214,6 +234,11 @@ public ObjectProperty<ModeState> modeStateProperty()
 		return modeState;
 	}
 
+	public ObjectProperty<ActiveSection> activeSectionProperty()
+	{
+		return activeSection;
+	}
+
 	public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final KeyTracker keyTracker)
 	{
 		final DelegateEventHandlers.AnyHandler filter = DelegateEventHandlers.handleAny();
@@ -228,12 +253,20 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
 					)
 			);
+		filter.addEventHandler(
+				KeyEvent.KEY_PRESSED,
+				EventFX.KEY_PRESSED(
+						"exit shape interpolation mode",
+						e -> {e.consume(); exitMode(paintera, false);},
+						e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE)
+					)
+			);
 		filter.addEventHandler(
 				KeyEvent.KEY_PRESSED,
 				EventFX.KEY_PRESSED(
 						"fix selection",
 						e -> {e.consume(); fixSelection(paintera);},
-						e -> modeState.get() == ModeState.Select &&
+						e -> (modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) &&
 							!selectedObjects.isEmpty() &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
 					)
@@ -243,24 +276,16 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				EventFX.KEY_PRESSED(
 						"apply mask",
 						e -> {e.consume(); applyMask(paintera);},
-						e -> modeState.get() == ModeState.Review &&
+						e -> modeState.get() == ModeState.Preview &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
 					)
 			);
-		filter.addEventHandler(
-				KeyEvent.KEY_PRESSED,
-				EventFX.KEY_PRESSED(
-						"exit shape interpolation mode",
-						e -> {e.consume(); exitMode(paintera, false);},
-						e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE)
-					)
-			);
 		filter.addEventHandler(
 				KeyEvent.KEY_PRESSED,
 				EventFX.KEY_PRESSED(
 						"edit selection 1",
-						e -> {e.consume(); editSelection(paintera, sectionInfo1);},
-						e -> modeState.get() == ModeState.Review &&
+						e -> {e.consume(); editSelection(paintera, ActiveSection.First);},
+						e -> modeState.get() == ModeState.Preview &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT1)
 					)
 			);
@@ -268,8 +293,8 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				KeyEvent.KEY_PRESSED,
 				EventFX.KEY_PRESSED(
 						"edit selection 2",
-						e -> {e.consume(); editSelection(paintera, sectionInfo2);},
-						e -> modeState.get() == ModeState.Review &&
+						e -> {e.consume(); editSelection(paintera, ActiveSection.Second);},
+						e -> modeState.get() == ModeState.Preview &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT2)
 					)
 			);
@@ -277,12 +302,12 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"select object in current section",
 				e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), true);},
-				e -> modeState.get() == ModeState.Select && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
+				e -> (modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
 			.handler());
 		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"toggle object in current section",
 				e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), false);},
-				e -> modeState.get() == ModeState.Select &&
+				e -> (modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) &&
 					((e.isSecondaryButtonDown() && keyTracker.noKeysActive()) ||
 					(e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL))))
 			.handler());
@@ -310,6 +335,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 		converter.setColor(newLabelId, MASK_COLOR);
 		selectedIds.activate(newLabelId);
 
+		activeSection.set(ActiveSection.First);
 		modeState.set(ModeState.Select);
 	}
 
@@ -352,7 +378,7 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed)
 		sectionInfo1.set(null);
 		sectionInfo2.set(null);
 		modeState.set(null);
-		editedSectionInfo = null;
+		activeSection.set(null);
 		mask = null;
 
 		workerThread = null;
@@ -406,22 +432,15 @@ private void setDisableOtherViewers(final PainteraBaseView paintera, final boole
 
 	private void fixSelection(final PainteraBaseView paintera)
 	{
-		final ObjectProperty<SectionInfo> sectionInfoPropertyToSet;
-		if (editedSectionInfo != null)
-			sectionInfoPropertyToSet = editedSectionInfo;
-		else if (sectionInfo1.get() == null)
-			sectionInfoPropertyToSet = sectionInfo1;
-		else
-			sectionInfoPropertyToSet = sectionInfo2;
-
+		final ObjectProperty<SectionInfo> sectionInfoPropertyToSet = activeSection.get() == ActiveSection.First ? sectionInfo1 : sectionInfo2;
 		LOG.debug("Fix selection");
 		sectionInfoPropertyToSet.set(createSectionInfo(paintera));
 		selectedObjects.clear();
-		editedSectionInfo = null;
 
-		if (sectionInfo2.get() == null)
+		if (modeState.get() != ModeState.Edit && sectionInfo2.get() == null)
 		{
 			// let the user now select the second section
+			activeSection.set(ActiveSection.Second);
 			resetMask();
 			activeViewerProperty().get().requestRepaint();
 			paintera.allowedActionsProperty().set(allowedActions);
@@ -429,13 +448,15 @@ else if (sectionInfo1.get() == null)
 		else
 		{
 			// both sections are ready, run interpolation
+			activeSection.set(null);
 			interpolateBetweenSections(paintera);
 		}
 	}
 
-	private void editSelection(final PainteraBaseView paintera, final ObjectProperty<SectionInfo> sectionInfoProperty)
+	private void editSelection(final PainteraBaseView paintera, final ActiveSection section)
 	{
-		final SectionInfo sectionInfo = sectionInfoProperty.get();
+		final ObjectProperty<SectionInfo> sectionInfoPropertyToEdit = section == ActiveSection.First ? sectionInfo1 : sectionInfo2;
+		final SectionInfo sectionInfo = sectionInfoPropertyToEdit.get();
 
 		resetMask();
 		try {
@@ -451,8 +472,9 @@ private void editSelection(final PainteraBaseView paintera, final ObjectProperty
 		selectedObjects.clear();
 		selectedObjects.putAll(sectionInfo.selectedObjects);
 
-		editedSectionInfo = sectionInfoProperty;
-		modeState.set(ModeState.Select);
+		sectionInfoPropertyToEdit.set(null);
+		activeSection.set(section);
+		modeState.set(ModeState.Edit);
 	}
 
 	private void applyMask(final PainteraBaseView paintera)
@@ -591,7 +613,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 			}
 
 			InvokeOnJavaFXApplicationThread.invoke(() -> {
-				modeState.set(ModeState.Review);
+				modeState.set(ModeState.Preview);
 				paintera.allowedActionsProperty().set(allowedActions);
 			});
 		});
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
index 46f25103a..7f0bf5123 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
@@ -1,5 +1,51 @@
 package org.janelia.saalfeldlab.paintera.state;
 
+import java.lang.invoke.MethodHandles;
+import java.util.Arrays;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.LongFunction;
+import java.util.function.ToLongFunction;
+
+import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
+import org.janelia.saalfeldlab.fx.event.EventFX;
+import org.janelia.saalfeldlab.fx.event.KeyTracker;
+import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread;
+import org.janelia.saalfeldlab.labels.blocks.LabelBlockLookup;
+import org.janelia.saalfeldlab.paintera.PainteraBaseView;
+import org.janelia.saalfeldlab.paintera.cache.InvalidateAll;
+import org.janelia.saalfeldlab.paintera.cache.global.GlobalCache;
+import org.janelia.saalfeldlab.paintera.composition.ARGBCompositeAlphaYCbCr;
+import org.janelia.saalfeldlab.paintera.composition.Composite;
+import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode;
+import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode.ActiveSection;
+import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode.ModeState;
+import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentOnlyLocal;
+import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentState;
+import org.janelia.saalfeldlab.paintera.control.lock.LockedSegmentsOnlyLocal;
+import org.janelia.saalfeldlab.paintera.control.lock.LockedSegmentsState;
+import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
+import org.janelia.saalfeldlab.paintera.data.DataSource;
+import org.janelia.saalfeldlab.paintera.data.RandomAccessibleIntervalDataSource;
+import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder;
+import org.janelia.saalfeldlab.paintera.data.mask.Mask;
+import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
+import org.janelia.saalfeldlab.paintera.id.IdService;
+import org.janelia.saalfeldlab.paintera.id.LocalIdService;
+import org.janelia.saalfeldlab.paintera.meshes.InterruptibleFunction;
+import org.janelia.saalfeldlab.paintera.meshes.ManagedMeshSettings;
+import org.janelia.saalfeldlab.paintera.meshes.MeshManager;
+import org.janelia.saalfeldlab.paintera.meshes.MeshManagerWithAssignmentForSegments;
+import org.janelia.saalfeldlab.paintera.stream.AbstractHighlightingARGBStream;
+import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverter;
+import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverterIntegerType;
+import org.janelia.saalfeldlab.paintera.stream.ModalGoldenAngleSaturatedHighlightingARGBStream;
+import org.janelia.saalfeldlab.util.Colors;
+import org.janelia.saalfeldlab.util.grids.LabelBlockLookupNoBlocks;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import bdv.util.volatiles.VolatileTypeMatcher;
 import gnu.trove.set.hash.TLongHashSet;
 import javafx.beans.InvalidationListener;
@@ -42,52 +88,8 @@
 import net.imglib2.util.Intervals;
 import net.imglib2.util.Util;
 import net.imglib2.view.Views;
-import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
-import org.janelia.saalfeldlab.fx.event.EventFX;
-import org.janelia.saalfeldlab.fx.event.KeyTracker;
-import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread;
-import org.janelia.saalfeldlab.labels.blocks.LabelBlockLookup;
-import org.janelia.saalfeldlab.paintera.PainteraBaseView;
-import org.janelia.saalfeldlab.paintera.cache.InvalidateAll;
-import org.janelia.saalfeldlab.paintera.cache.global.GlobalCache;
-import org.janelia.saalfeldlab.paintera.composition.ARGBCompositeAlphaYCbCr;
-import org.janelia.saalfeldlab.paintera.composition.Composite;
-import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode;
-import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode.ModeState;
-import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentOnlyLocal;
-import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentState;
-import org.janelia.saalfeldlab.paintera.control.lock.LockedSegmentsOnlyLocal;
-import org.janelia.saalfeldlab.paintera.control.lock.LockedSegmentsState;
-import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
-import org.janelia.saalfeldlab.paintera.data.DataSource;
-import org.janelia.saalfeldlab.paintera.data.RandomAccessibleIntervalDataSource;
-import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder;
-import org.janelia.saalfeldlab.paintera.data.mask.Mask;
-import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
-import org.janelia.saalfeldlab.paintera.id.IdService;
-import org.janelia.saalfeldlab.paintera.id.LocalIdService;
-import org.janelia.saalfeldlab.paintera.meshes.InterruptibleFunction;
-import org.janelia.saalfeldlab.paintera.meshes.ManagedMeshSettings;
-import org.janelia.saalfeldlab.paintera.meshes.MeshManager;
-import org.janelia.saalfeldlab.paintera.meshes.MeshManagerWithAssignmentForSegments;
-import org.janelia.saalfeldlab.paintera.stream.AbstractHighlightingARGBStream;
-import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverter;
-import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverterIntegerType;
-import org.janelia.saalfeldlab.paintera.stream.ModalGoldenAngleSaturatedHighlightingARGBStream;
-import org.janelia.saalfeldlab.util.Colors;
-import org.janelia.saalfeldlab.util.grids.LabelBlockLookupNoBlocks;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import pl.touk.throwing.ThrowingFunction;
 
-import java.lang.invoke.MethodHandles;
-import java.util.Arrays;
-import java.util.concurrent.ExecutorService;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.LongFunction;
-import java.util.function.ToLongFunction;
-
 public class LabelSourceState<D extends IntegerType<D>, T>
 		extends
 		MinimalSourceState<D, T, DataSource<D, T>, HighlightingStreamConverter<T>>
@@ -607,13 +609,39 @@ private HBox createDisplayStatus()
 			});
 		});
 
-		this.shapeInterpolationMode.modeStateProperty().addListener((obs, oldv, newv) -> {
+		final InvalidationListener shapeInterpolationModeStatusUpdater = obs -> {
 			InvokeOnJavaFXApplicationThread.invoke(() -> {
-				final boolean showProgressIndicator = newv == ModeState.Interpolate;
+				final ModeState modeState = this.shapeInterpolationMode.modeStateProperty().get();
+				final ActiveSection activeSection = this.shapeInterpolationMode.activeSectionProperty().get();
+				if (modeState != null) {
+					switch (modeState) {
+					case Select:
+						statusTextProperty().set("Select #" + activeSection);
+						break;
+					case Edit:
+						statusTextProperty().set("Edit #" + activeSection);
+						break;
+					case Interpolate:
+						statusTextProperty().set("Interpolating");
+						break;
+					case Preview:
+						statusTextProperty().set("Preview");
+						break;
+					default:
+						statusTextProperty().set(null);
+						break;
+					}
+				} else {
+					statusTextProperty().set(null);
+				}
+				final boolean showProgressIndicator = modeState == ModeState.Interpolate;
 				paintingProgressIndicator.setVisible(showProgressIndicator);
 				paintingProgressIndicatorTooltip.setText(showProgressIndicator ? "Interpolating between sections..." : "");
 			});
-		});
+		};
+
+		this.shapeInterpolationMode.modeStateProperty().addListener(shapeInterpolationModeStatusUpdater);
+		this.shapeInterpolationMode.activeSectionProperty().addListener(shapeInterpolationModeStatusUpdater);
 
 		final HBox displayStatus = new HBox(5,
 				lastSelectedLabelColorRect,

From cf532eb71af9b1583b3f6ebc34d43f1987287d18 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 5 Jun 2019 17:34:47 -0400
Subject: [PATCH 56/84] improve thread synchronization in MaskedSource

previously, applyMask and persistCanvas used to synchronize on this until the process is completed. This was causing the application to freeze when, for example, a new mask is requested while the last one is still being committed into the canvas. Now the application will not block anymore and instead show a dialog suggesting to wait until the process is completed.
---
 .../control/ShapeInterpolationMode.java       |   6 +-
 .../paintera/control/paint/FloodFill.java     |  29 +-
 .../paintera/data/mask/MaskedSource.java      | 338 +++++++++---------
 3 files changed, 193 insertions(+), 180 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index ccc13e8ef..38f26a0ad 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -404,7 +404,11 @@ private void createMask() throws MaskInUse
 
 	private void resetMask()
 	{
-		source.resetMasks();
+		try {
+			source.resetMasks();
+		} catch (final MaskInUse e) {
+			e.printStackTrace();
+		}
 		mask = null;
 	}
 
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java
index 6eb77a696..41bbf7da2 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java
@@ -10,6 +10,18 @@
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 
+import org.janelia.saalfeldlab.paintera.data.mask.Mask;
+import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo;
+import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
+import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse;
+import org.janelia.saalfeldlab.paintera.state.HasFloodFillState;
+import org.janelia.saalfeldlab.paintera.state.HasFloodFillState.FloodFillState;
+import org.janelia.saalfeldlab.paintera.state.HasMaskForLabel;
+import org.janelia.saalfeldlab.paintera.state.SourceInfo;
+import org.janelia.saalfeldlab.paintera.state.SourceState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import bdv.fx.viewer.ViewerPanelFX;
 import bdv.fx.viewer.ViewerState;
 import bdv.viewer.Source;
@@ -34,17 +46,6 @@
 import net.imglib2.util.Intervals;
 import net.imglib2.util.Util;
 import net.imglib2.view.Views;
-import org.janelia.saalfeldlab.paintera.data.mask.Mask;
-import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse;
-import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo;
-import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
-import org.janelia.saalfeldlab.paintera.state.HasFloodFillState;
-import org.janelia.saalfeldlab.paintera.state.HasMaskForLabel;
-import org.janelia.saalfeldlab.paintera.state.SourceInfo;
-import org.janelia.saalfeldlab.paintera.state.SourceState;
-import org.janelia.saalfeldlab.paintera.state.HasFloodFillState.FloodFillState;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class FloodFill
 {
@@ -270,7 +271,11 @@ private <T extends IntegerType<T>> void fill(
 			if (Thread.interrupted())
 			{
 				floodFillThread.interrupt();
-				source.resetMasks();
+				try {
+					source.resetMasks();
+				} catch (final MaskInUse e) {
+					e.printStackTrace();
+				}
 			}
 			else
 			{
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java
index b07a4406a..41cd978b7 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java
@@ -1,5 +1,37 @@
 package org.janelia.saalfeldlab.paintera.data.mask;
 
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.DoubleStream;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+
+import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread;
+import org.janelia.saalfeldlab.paintera.data.DataSource;
+import org.janelia.saalfeldlab.paintera.data.mask.PickOne.PickAndConvert;
+import org.janelia.saalfeldlab.paintera.data.mask.exception.CannotClearCanvas;
+import org.janelia.saalfeldlab.paintera.data.mask.exception.CannotPersist;
+import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse;
+import org.janelia.saalfeldlab.paintera.data.mask.persist.PersistCanvas;
+import org.janelia.saalfeldlab.paintera.data.mask.persist.UnableToPersistCanvas;
+import org.janelia.saalfeldlab.paintera.data.mask.persist.UnableToUpdateLabelBlockLookup;
+import org.janelia.saalfeldlab.paintera.data.n5.BlockSpec;
+import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import bdv.util.volatiles.VolatileViews;
 import bdv.viewer.Interpolation;
 import gnu.trove.iterator.TLongIterator;
@@ -69,37 +101,6 @@
 import net.imglib2.view.IntervalView;
 import net.imglib2.view.RealRandomAccessibleTriple;
 import net.imglib2.view.Views;
-import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread;
-import org.janelia.saalfeldlab.paintera.data.DataSource;
-import org.janelia.saalfeldlab.paintera.data.mask.PickOne.PickAndConvert;
-import org.janelia.saalfeldlab.paintera.data.mask.exception.CannotClearCanvas;
-import org.janelia.saalfeldlab.paintera.data.mask.exception.CannotPersist;
-import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse;
-import org.janelia.saalfeldlab.paintera.data.mask.persist.PersistCanvas;
-import org.janelia.saalfeldlab.paintera.data.mask.persist.UnableToPersistCanvas;
-import org.janelia.saalfeldlab.paintera.data.mask.persist.UnableToUpdateLabelBlockLookup;
-import org.janelia.saalfeldlab.paintera.data.n5.BlockSpec;
-import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.lang.invoke.MethodHandles;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Optional;
-import java.util.concurrent.ExecutorService;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
-import java.util.stream.DoubleStream;
-import java.util.stream.IntStream;
-import java.util.stream.LongStream;
-import java.util.stream.Stream;
 
 /**
  *
@@ -283,7 +284,6 @@ public Mask<UnsignedLongType> generateMask(
 			final Predicate<UnsignedLongType> isPaintedForeground)
 	throws MaskInUse
 	{
-
 		LOG.debug("Asking for mask: {}", maskInfo);
 		synchronized (this)
 		{
@@ -312,10 +312,10 @@ public Mask<UnsignedLongType> generateMask(
 				((AbstractCellImg<?,?,?,?>)store).getCellGrid()
 		);
 		final Mask<UnsignedLongType> mask = new Mask<>(maskInfo, trackingStore);
-		synchronized(this)
+		synchronized (this)
 		{
-			this.isCreatingMask = false;
 			this.currentMask = mask;
+			this.isCreatingMask = false;
 		}
 		return mask;
 	}
@@ -338,6 +338,7 @@ public void setMask(
 				         );
 				throw new MaskInUse("Busy, cannot set new mask.");
 			}
+			this.isCreatingMask = true;
 		}
 
 		final RandomAccessibleInterval<UnsignedLongType> store;
@@ -349,9 +350,10 @@ public void setMask(
 
 		setMasks(store, vstore, mask.info.level, mask.info.value, isPaintedForeground);
 
-		synchronized(this)
+		synchronized (this)
 		{
 			this.currentMask = mask;
+			this.isCreatingMask = false;
 		}
 	}
 
@@ -375,14 +377,16 @@ public void setMask(
 				         );
 				throw new MaskInUse("Busy, cannot set new mask.");
 			}
+			this.isCreatingMask = true;
 		}
 
 		setMasks(mask, vmask, maskInfo.level, maskInfo.value, isPaintedForeground);
 
-		synchronized(this)
+		synchronized (this)
 		{
 			final RandomAccessibleInterval<UnsignedLongType> rasteredMask = Views.interval(Views.raster(mask), source.getSource(0, maskInfo.level));
 			this.currentMask = new Mask<>(maskInfo, rasteredMask);
+			this.isCreatingMask = false;
 		}
 	}
 
@@ -404,60 +408,63 @@ public void applyMask(
 					return;
 				}
 				this.isApplyingMask.set(true);
+			}
 
-				LOG.debug("Applying mask: {}", mask, paintedInterval);
-				final MaskInfo<UnsignedLongType> maskInfo = mask.info;
-				final CachedCellImg<UnsignedLongType, ?> canvas = dataCanvases[maskInfo.level];
-				final CellGrid                           grid   = canvas.getCellGrid();
+			LOG.debug("Applying mask: {}", mask, paintedInterval);
+			final MaskInfo<UnsignedLongType> maskInfo = mask.info;
+			final CachedCellImg<UnsignedLongType, ?> canvas = dataCanvases[maskInfo.level];
+			final CellGrid                           grid   = canvas.getCellGrid();
 
-				final int[] blockSize = new int[grid.numDimensions()];
-				grid.cellDimensions(blockSize);
+			final int[] blockSize = new int[grid.numDimensions()];
+			grid.cellDimensions(blockSize);
 
-				final TLongSet affectedBlocks = affectedBlocks(mask.mask, canvas.getCellGrid(), paintedInterval);
+			final TLongSet affectedBlocks = affectedBlocks(mask.mask, canvas.getCellGrid(), paintedInterval);
 
-				paintAffectedPixels(
-						affectedBlocks,
-						Converters.convert(
-								Views.extendZero(mask.mask),
-								(s, t) -> t.set(acceptAsPainted.test(s)),
-								new BitType()),
-						canvas,
-						maskInfo.value,
-						canvas.getCellGrid(),
-						paintedInterval);
+			paintAffectedPixels(
+					affectedBlocks,
+					Converters.convert(
+							Views.extendZero(mask.mask),
+							(s, t) -> t.set(acceptAsPainted.test(s)),
+							new BitType()),
+					canvas,
+					maskInfo.value,
+					canvas.getCellGrid(),
+					paintedInterval);
 
-				forgetMasks();
+			synchronized (this)
+			{
+				this.currentMask = null;
+			}
 
-				final TLongSet paintedBlocksAtHighestResolution = this.scaleBlocksToLevel(
+			final TLongSet paintedBlocksAtHighestResolution = this.scaleBlocksToLevel(
+					affectedBlocks,
+					maskInfo.level,
+					0);
+
+			this.affectedBlocksByLabel[maskInfo.level].computeIfAbsent(
+					maskInfo.value.getIntegerLong(),
+					key -> new TLongHashSet()
+			                                                          ).addAll(affectedBlocks);
+			LOG.debug("Added affected block: {}", affectedBlocksByLabel[maskInfo.level]);
+			this.affectedBlocks.addAll(paintedBlocksAtHighestResolution);
+
+			propagationExecutor.submit(() -> {
+				propagateMask(
+						mask.mask,
 						affectedBlocks,
 						maskInfo.level,
-						0);
-
-				this.affectedBlocksByLabel[maskInfo.level].computeIfAbsent(
-						maskInfo.value.getIntegerLong(),
-						key -> new TLongHashSet()
-				                                                          ).addAll(affectedBlocks);
-				LOG.debug("Added affected block: {}", affectedBlocksByLabel[maskInfo.level]);
-				this.affectedBlocks.addAll(paintedBlocksAtHighestResolution);
-
-				propagationExecutor.submit(() -> {
-					propagateMask(
-							mask.mask,
-							affectedBlocks,
-							maskInfo.level,
-							maskInfo.value,
-							paintedInterval,
-							acceptAsPainted
-					             );
-					setMasksConstant();
-					synchronized(MaskedSource.this)
-					{
-						LOG.debug("Done applying mask!");
-						MaskedSource.this.isApplyingMask.set(false);
-					}
-				});
+						maskInfo.value,
+						paintedInterval,
+						acceptAsPainted
+					);
+				setMasksConstant();
+				synchronized (this)
+				{
+					LOG.debug("Done applying mask!");
+					this.isApplyingMask.set(false);
+				}
+			});
 
-			}
 		}).start();
 
 	}
@@ -518,21 +525,20 @@ private void scalePositionToLevel(final long[] position, final int intervalLevel
 		toTargetScale.apply(positionDouble, positionDouble);
 
 		Arrays.setAll(targetPosition, d -> (long) Math.ceil(positionDouble[d]));
-
 	}
 
-	public synchronized void resetMasks()
-	{
-		forgetMasks();
-		setMasksConstant();
-	}
-
-	public void forgetMasks()
+	public void resetMasks() throws MaskInUse
 	{
 		synchronized (this)
 		{
+			final boolean canResetMask = !isCreatingMask && !isApplyingMask.get();
+			LOG.debug("Can reset mask? {}", canResetMask);
+			if (!canResetMask)
+				throw new MaskInUse("Busy, cannot reset mask.");
+
 			this.currentMask = null;
 		}
+		setMasksConstant();
 	}
 
 	public void forgetCanvases() throws CannotClearCanvas
@@ -540,102 +546,100 @@ public void forgetCanvases() throws CannotClearCanvas
 		synchronized (this)
 		{
 			if (this.isPersisting)
-			{
 				throw new CannotClearCanvas("Currently persisting canvas -- try again later.");
-			}
 			this.currentMask = null;
-			clearCanvases();
 		}
+		clearCanvases();
 	}
 
 	public void persistCanvas() throws CannotPersist
 	{
 		synchronized (this)
 		{
-			if (!this.isCreatingMask && this.currentMask == null && !this.isApplyingMask.get() && !this.isPersisting)
-			{
-				this.isPersisting = true;
-				LOG.debug("Merging canvas into background for blocks {}", this.affectedBlocks);
-				final CachedCellImg<UnsignedLongType, ?> canvas         = this.dataCanvases[0];
-				final long[]                             affectedBlocks = this.affectedBlocks.toArray();
-				this.affectedBlocks.clear();
-				final MaskedSource<D, T> thiz = this;
-				final BooleanProperty proxy = new SimpleBooleanProperty(this.isPersisting);
-				final ObservableList<String> states = FXCollections.observableArrayList();
-				final Runnable dialogHandler = () -> {
-					LOG.warn("Creating commit status dialog.");
-					final Alert isCommittingDialog = PainteraAlerts.alert(Alert.AlertType.INFORMATION);
-					isCommittingDialog.setHeaderText("Committing canvas.");
-					isCommittingDialog.getDialogPane().lookupButton(ButtonType.OK).setDisable(true);
-					isCommittingDialog.initModality(Modality.NONE);
-					states.addListener((ListChangeListener<? super String>) change -> InvokeOnJavaFXApplicationThread.invoke(() -> isCommittingDialog.getDialogPane().setContent(new VBox(asLabels(states)))));
-					synchronized(MaskedSource.this) {
-						isCommittingDialog.getDialogPane().lookupButton(ButtonType.OK).disableProperty().bind(proxy);
-					}
-					LOG.info("Will show dialog? {}", proxy.get());
-					if(proxy.get()) isCommittingDialog.show();
-				};
-				new Thread(() -> {
-					Exception caughtException = null;
-					try
-					{
-						try {
-							InvokeOnJavaFXApplicationThread.invokeAndWait(dialogHandler);
-						}
-						catch (final InterruptedException e)
-						{
-							throw new RuntimeException(e);
-						}
-						try {
-							states.add("Persisting painted labels...");
-							final List<TLongObjectMap<PersistCanvas.BlockDiff>> blockDiffs = this.persistCanvas.persistCanvas(canvas, affectedBlocks);
-							states.set(states.size() - 1, "Persisting painted labels...   Done");
-							if (this.persistCanvas.supportsLabelBlockLookupUpdate()) {
-								states.add("Updating label-to-block lookup...");
-								this.persistCanvas.updateLabelBlockLookup(blockDiffs);
-								states.set(states.size() - 1, "Updating label-to-block lookup...   Done");
-							}
-							states.add("Clearing canvases...");
-							clearCanvases();
-							states.set(states.size() - 1, "Clearing canvases...   Done");
-							this.source.invalidateAll();
-						}
-						catch (UnableToPersistCanvas | UnableToUpdateLabelBlockLookup e)
-						{
-							caughtException = e;
-							throw new RuntimeException("Error while trying to persist.", e);
-						}
-						catch (final RuntimeException e)
-						{
-							caughtException = e;
-							throw e;
-						}
-					} finally
-					{
-						synchronized (thiz)
-						{
-							thiz.isPersisting = false;
-							proxy.set(false);
-							if (caughtException == null)
-								states.add("Successfully finished committing canvas.");
-							else
-								states.add("Unable to commit canvas: " + caughtException.getMessage());
-						}
-					}
-				}).start();
-			}
-			else
+			final boolean canPersist = !this.isCreatingMask && this.currentMask == null && !this.isApplyingMask.get() && !this.isPersisting;
+			if (!canPersist)
 			{
 				LOG.error(
 						"Cannot persist canvas: is persisting? {} has mask? {} is creating mask? {} is applying mask? {}",
-						isPersisting,
+						this.isPersisting,
 						this.currentMask != null,
 						this.isCreatingMask,
 						this.isApplyingMask
-				         );
+					);
 				throw new CannotPersist("Can not persist canvas!");
 			}
+			this.isPersisting = true;
 		}
+
+		LOG.debug("Merging canvas into background for blocks {}", this.affectedBlocks);
+		final CachedCellImg<UnsignedLongType, ?> canvas         = this.dataCanvases[0];
+		final long[]                             affectedBlocks = this.affectedBlocks.toArray();
+		this.affectedBlocks.clear();
+		final BooleanProperty proxy = new SimpleBooleanProperty(this.isPersisting);
+		final ObservableList<String> states = FXCollections.observableArrayList();
+		final Runnable dialogHandler = () -> {
+			LOG.warn("Creating commit status dialog.");
+			final Alert isCommittingDialog = PainteraAlerts.alert(Alert.AlertType.INFORMATION);
+			isCommittingDialog.setHeaderText("Committing canvas.");
+			isCommittingDialog.getDialogPane().lookupButton(ButtonType.OK).setDisable(true);
+			isCommittingDialog.initModality(Modality.NONE);
+			states.addListener((ListChangeListener<? super String>) change -> InvokeOnJavaFXApplicationThread.invoke(() -> isCommittingDialog.getDialogPane().setContent(new VBox(asLabels(states)))));
+			synchronized (this) {
+				isCommittingDialog.getDialogPane().lookupButton(ButtonType.OK).disableProperty().bind(proxy);
+			}
+			LOG.info("Will show dialog? {}", proxy.get());
+			if(proxy.get()) isCommittingDialog.show();
+		};
+		new Thread(() -> {
+			Exception caughtException = null;
+			try
+			{
+				try {
+					InvokeOnJavaFXApplicationThread.invokeAndWait(dialogHandler);
+				}
+				catch (final InterruptedException e)
+				{
+					throw new RuntimeException(e);
+				}
+				try {
+					states.add("Persisting painted labels...");
+					final List<TLongObjectMap<PersistCanvas.BlockDiff>> blockDiffs = this.persistCanvas.persistCanvas(canvas, affectedBlocks);
+					states.set(states.size() - 1, "Persisting painted labels...   Done");
+					if (this.persistCanvas.supportsLabelBlockLookupUpdate()) {
+						states.add("Updating label-to-block lookup...");
+						this.persistCanvas.updateLabelBlockLookup(blockDiffs);
+						states.set(states.size() - 1, "Updating label-to-block lookup...   Done");
+					}
+					states.add("Clearing canvases...");
+					clearCanvases();
+					states.set(states.size() - 1, "Clearing canvases...   Done");
+					this.source.invalidateAll();
+				}
+				catch (UnableToPersistCanvas | UnableToUpdateLabelBlockLookup e)
+				{
+					caughtException = e;
+					throw new RuntimeException("Error while trying to persist.", e);
+				}
+				catch (final RuntimeException e)
+				{
+					caughtException = e;
+					throw e;
+				}
+			}
+			finally
+			{
+				synchronized (this)
+				{
+					this.isPersisting = false;
+					proxy.set(false);
+					if (caughtException == null)
+						states.add("Successfully finished committing canvas.");
+					else
+						states.add("Unable to commit canvas: " + caughtException.getMessage());
+				}
+			}
+		}).start();
+
 	}
 
 	@Override

From 536b9ca92c83a4a714d53a10ff34b06f5e6f1544 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 5 Jun 2019 17:39:23 -0400
Subject: [PATCH 57/84] fix bug allowing to enter shape interpolation mode
 while 3d flood-filling

---
 .../saalfeldlab/paintera/control/ShapeInterpolationMode.java     | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 38f26a0ad..3baa1903b 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -249,6 +249,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 						e -> {e.consume(); enterMode(paintera, (ViewerPanelFX) e.getTarget());},
 						e -> e.getTarget() instanceof ViewerPanelFX &&
 							!isModeOn() &&
+							source.getCurrentMask() == null &&
 							!source.isApplyingMaskProperty().get() &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
 					)

From 60468ad5911eed282915a7aa74ffa568d8f9869d Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 5 Jun 2019 18:33:08 -0400
Subject: [PATCH 58/84] attempt to fix race condition when explicitly setting
 label colors

argbCache is accessed concurrently, but the accesses are not fully synchronized due to performance reasons (only write is protected). The clearCache() method changes the collection but was not protected, which sometimes caused the stream to ignore the explicitly specified color. This fix should reduce the chance of this bug, but is not guaranteed to completely solve it.
---
 .../paintera/stream/AbstractHighlightingARGBStream.java    | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/stream/AbstractHighlightingARGBStream.java b/src/main/java/org/janelia/saalfeldlab/paintera/stream/AbstractHighlightingARGBStream.java
index 7e9837bd7..fa6e65f4c 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/stream/AbstractHighlightingARGBStream.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/stream/AbstractHighlightingARGBStream.java
@@ -264,8 +264,11 @@ public int getActiveFragmentAlpha()
 	public void clearCache()
 	{
 		LOG.debug("Before clearing cache: {}", argbCache);
-		argbCache.clear();
-		argbCache.putAll(this.explicitlySpecifiedColors);
+		synchronized (argbCache)
+		{
+			argbCache.clear();
+			argbCache.putAll(this.explicitlySpecifiedColors);
+		}
 		LOG.debug("After clearing cache: {}", argbCache);
 		// TODO is this stateChanged bad here?
 		// stateChanged() probably triggers a re-render, which calls clearCache,

From 04b035bdfa40ebff2039cd3492f62371cdfd6453 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Wed, 5 Jun 2019 18:35:36 -0400
Subject: [PATCH 59/84] fix occasional selection color change bug in shape
 interpolation mode

Previously forgot to remove this line of code that would force the ARGB stream to set the explicit color and clear its cache on every object selection/deselection, which sometimes triggered the race condition described in the previous commit.
---
 .../saalfeldlab/paintera/control/ShapeInterpolationMode.java     | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 3baa1903b..55347289d 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -724,7 +724,6 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 
 		final boolean wasSelected = FOREGROUND_CHECK.test(maskValue);
 		final int numSelectedObjects = selectedObjects.size();
-		converter.setColor(mask.info.value.get(), MASK_COLOR);
 
 		LOG.debug("Object was clicked: deactivateOthers={}, wasSelected={}, numSelectedObjects", deactivateOthers, wasSelected, numSelectedObjects);
 

From b47cbdc99765e1fb24ac29d0b2d8e0ecf2aef9ed Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Thu, 6 Jun 2019 12:12:49 -0400
Subject: [PATCH 60/84] increase thickness of 2d flood fill in shape
 interpolation mode to 2.0

this way it fills 'holes' in a rotated view that actually belong to adjacent sections
---
 .../saalfeldlab/paintera/control/ShapeInterpolationMode.java    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 55347289d..4aa8cab10 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -148,7 +148,7 @@ private static final class SectionInfo
 		}
 	}
 
-	private static final double FILL_DEPTH = 1.0;
+	private static final double FILL_DEPTH = 2.0;
 
 	private static final int MASK_SCALE_LEVEL = 0;
 

From 6ecbe1c0d8566d13467df731bef0a98c01e3791d Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Thu, 6 Jun 2019 12:14:13 -0400
Subject: [PATCH 61/84] consider fragment-segment assignment when flood-filling
 in 2d/3d

---
 .../control/ShapeInterpolationMode.java       |  8 +++-
 .../paintera/control/paint/FloodFill.java     | 45 ++++++++++++-------
 .../paintera/control/paint/FloodFill2D.java   | 41 +++++++++++------
 .../paintera/state/LabelSourceState.java      |  2 +-
 4 files changed, 64 insertions(+), 32 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 4aa8cab10..8b25225e6 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -16,6 +16,7 @@
 import org.janelia.saalfeldlab.paintera.control.actions.MenuAction;
 import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction;
 import org.janelia.saalfeldlab.paintera.control.actions.PaintAction;
+import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment;
 import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D;
 import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
 import org.janelia.saalfeldlab.paintera.data.DataSource;
@@ -165,6 +166,7 @@ private static final class SectionInfo
 	private final SelectedIds selectedIds;
 	private final IdService idService;
 	private final HighlightingStreamConverter<?> converter;
+	private final FragmentSegmentAssignment assignment;
 
 	private final AllowedActions allowedActions;
 	private final AllowedActions allowedActionsWhenSelected;
@@ -194,13 +196,15 @@ public ShapeInterpolationMode(
 			final LabelSourceState<D, ?> sourceState,
 			final SelectedIds selectedIds,
 			final IdService idService,
-			final HighlightingStreamConverter<?> converter)
+			final HighlightingStreamConverter<?> converter,
+			final FragmentSegmentAssignment assignment)
 	{
 		this.source = source;
 		this.sourceState = sourceState;
 		this.selectedIds = selectedIds;
 		this.idService = idService;
 		this.converter = converter;
+		this.assignment = assignment;
 
 		final Consumer<PainteraBaseView> cleanup = baseView -> exitMode(baseView, false);
 		this.allowedActions = new AllowedActions(
@@ -769,7 +773,7 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 	 */
 	private Pair<Long, Interval> runFloodFillToSelect(final double x, final double y)
 	{
-		final Interval affectedInterval = FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, ++currentFillValue, FILL_DEPTH);
+		final Interval affectedInterval = FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, assignment, ++currentFillValue, FILL_DEPTH);
 		return new ValuePair<>(currentFillValue, affectedInterval);
 	}
 
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java
index 41bbf7da2..a0715b40d 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java
@@ -10,12 +10,14 @@
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 
+import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment;
 import org.janelia.saalfeldlab.paintera.data.mask.Mask;
 import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo;
 import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
 import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse;
 import org.janelia.saalfeldlab.paintera.state.HasFloodFillState;
 import org.janelia.saalfeldlab.paintera.state.HasFloodFillState.FloodFillState;
+import org.janelia.saalfeldlab.paintera.state.HasFragmentSegmentAssignments;
 import org.janelia.saalfeldlab.paintera.state.HasMaskForLabel;
 import org.janelia.saalfeldlab.paintera.state.SourceInfo;
 import org.janelia.saalfeldlab.paintera.state.SourceState;
@@ -138,6 +140,17 @@ private void fillAt(final double x, final double y, final long fill)
 			return;
 		}
 
+		final FragmentSegmentAssignment assignment;
+		if (currentSourceState instanceof HasFragmentSegmentAssignments)
+		{
+			LOG.info("Selected source has a fragment-segment assignment that will be used for filling");
+			assignment = ((HasFragmentSegmentAssignments) currentSourceState).assignment();
+		}
+		else
+		{
+			assignment = null;
+		}
+
 		final MaskedSource<?, ?> source = (MaskedSource<?, ?>) currentSource;
 
 		final Type<?> t = source.getDataType();
@@ -168,7 +181,8 @@ private void fillAt(final double x, final double y, final long fill)
 				time,
 				level,
 				fill,
-				p
+				p,
+				assignment
 			);
 		} catch (final MaskInUse e)
 		{
@@ -209,13 +223,15 @@ private <T extends IntegerType<T>> void fill(
 			final int time,
 			final int level,
 			final long fill,
-			final Localizable seed) throws MaskInUse
+			final Localizable seed,
+			final FragmentSegmentAssignment assignment) throws MaskInUse
 	{
 		final RandomAccessibleInterval<T> data = source.getDataSource(time, level);
 		final RandomAccess<T> dataAccess = data.randomAccess();
 		dataAccess.setPosition(seed);
 		final T seedValue = dataAccess.get();
-		final long seedLabel = seedValue instanceof LabelMultisetType ? getArgMaxLabel((LabelMultisetType) seedValue) : seedValue.getIntegerLong();
+		final long seedPrimitiveValue = seedValue instanceof LabelMultisetType ? getArgMaxLabel((LabelMultisetType) seedValue) : seedValue.getIntegerLong();
+		final long seedLabel = assignment != null ? assignment.getSegment(seedPrimitiveValue) : seedPrimitiveValue;
 		if (!Label.regular(seedLabel))
 		{
 			LOG.info("Trying to fill at irregular label: {} ({})", seedLabel, new Point(seed));
@@ -235,9 +251,9 @@ private <T extends IntegerType<T>> void fill(
 		final Thread floodFillThread = new Thread(() -> {
 			try {
 				if (seedValue instanceof LabelMultisetType) {
-					fillMultisetType((RandomAccessibleInterval<LabelMultisetType>) data, accessTracker, seed, seedLabel);
+					fillMultisetType((RandomAccessibleInterval<LabelMultisetType>) data, accessTracker, seed, seedLabel, assignment);
 				} else {
-					fillPrimitiveType(data, accessTracker, seed, seedLabel);
+					fillPrimitiveType(data, accessTracker, seed, seedLabel, assignment);
 				}
 			} catch (final Exception e) {
 				// got an exception, ignore it if the operation has been canceled, or re-throw otherwise
@@ -301,7 +317,8 @@ private static void fillMultisetType(
 			final RandomAccessibleInterval<LabelMultisetType> input,
 			final RandomAccessible<UnsignedLongType> output,
 			final Localizable seed,
-			final long seedLabel)
+			final long seedLabel,
+			final FragmentSegmentAssignment assignment)
 	{
 		net.imglib2.algorithm.fill.FloodFill.fill(
 				Views.extendValue(input, new LabelMultisetType()),
@@ -309,7 +326,7 @@ private static void fillMultisetType(
 				seed,
 				new UnsignedLongType(1),
 				new DiamondShape(1),
-				makePredicateMultisetType(seedLabel)
+				makePredicate(seedLabel, assignment)
 			);
 	}
 
@@ -317,7 +334,8 @@ private static <T extends IntegerType<T>> void fillPrimitiveType(
 			final RandomAccessibleInterval<T> input,
 			final RandomAccessible<UnsignedLongType> output,
 			final Localizable seed,
-			final long seedLabel)
+			final long seedLabel,
+			final FragmentSegmentAssignment assignment)
 	{
 		final T extension = Util.getTypeFromInterval(input).createVariable();
 		extension.setInteger(Label.OUTSIDE);
@@ -328,7 +346,7 @@ private static <T extends IntegerType<T>> void fillPrimitiveType(
 				seed,
 				new UnsignedLongType(1),
 				new DiamondShape(1),
-				makePredicatePrimitiveType(seedLabel)
+				makePredicate(seedLabel, assignment)
 			);
 	}
 
@@ -344,14 +362,9 @@ private void resetFloodFillState(final Source<?> source)
 		setFloodFillState(source, null);
 	}
 
-	private static BiPredicate<LabelMultisetType, UnsignedLongType> makePredicateMultisetType(final long id)
-	{
-		return (l, u) -> !Thread.currentThread().isInterrupted() && u.getInteger() == 0 && l.contains(id);
-	}
-
-	private static <T extends IntegerType<T>> BiPredicate<T, UnsignedLongType> makePredicatePrimitiveType(final long id)
+	private static <T extends IntegerType<T>> BiPredicate<T, UnsignedLongType> makePredicate(final long id, final FragmentSegmentAssignment assignment)
 	{
-		return (t, u) -> !Thread.currentThread().isInterrupted() && u.getInteger() == 0 && t.getIntegerLong() == id;
+		return (t, u) -> !Thread.currentThread().isInterrupted() && u.getInteger() == 0 && (assignment != null ? assignment.getSegment(t.getIntegerLong()) : t.getIntegerLong()) == id;
 	}
 
 	public static class RunAll implements Runnable
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
index 63d0e0f9a..b2d8f6340 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
@@ -6,6 +6,18 @@
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 
+import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment;
+import org.janelia.saalfeldlab.paintera.data.mask.Mask;
+import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo;
+import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
+import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse;
+import org.janelia.saalfeldlab.paintera.state.HasFragmentSegmentAssignments;
+import org.janelia.saalfeldlab.paintera.state.HasMaskForLabel;
+import org.janelia.saalfeldlab.paintera.state.SourceInfo;
+import org.janelia.saalfeldlab.paintera.state.SourceState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import bdv.fx.viewer.ViewerPanelFX;
 import bdv.fx.viewer.ViewerState;
 import bdv.viewer.Source;
@@ -32,16 +44,6 @@
 import net.imglib2.view.MixedTransformView;
 import net.imglib2.view.Views;
 
-import org.janelia.saalfeldlab.paintera.data.mask.Mask;
-import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse;
-import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo;
-import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
-import org.janelia.saalfeldlab.paintera.state.HasMaskForLabel;
-import org.janelia.saalfeldlab.paintera.state.SourceInfo;
-import org.janelia.saalfeldlab.paintera.state.SourceState;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 public class FloodFill2D
 {
 
@@ -137,6 +139,17 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi
 			return;
 		}
 
+		final FragmentSegmentAssignment assignment;
+		if (currentSourceState instanceof HasFragmentSegmentAssignments)
+		{
+			LOG.info("Selected source has a fragment-segment assignment that will be used for filling");
+			assignment = ((HasFragmentSegmentAssignments) currentSourceState).assignment();
+		}
+		else
+		{
+			assignment = null;
+		}
+
 		final MaskedSource<T, ?> source = (MaskedSource<T, ?>) currentSource;
 
 		final T t = source.getDataType();
@@ -157,7 +170,7 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi
 		try
 		{
 			final Mask<UnsignedLongType> mask = source.generateMask(maskInfo, FOREGROUND_CHECK);
-			final Interval affectedInterval = fillMaskAt(x, y, this.viewer, mask, source, FILL_VALUE, this.fillDepth.get());
+			final Interval affectedInterval = fillMaskAt(x, y, this.viewer, mask, source, assignment, FILL_VALUE, this.fillDepth.get());
 			requestRepaint.run();
 			source.applyMask(mask, affectedInterval, FOREGROUND_CHECK);
 		} catch (final MaskInUse e)
@@ -179,6 +192,7 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi
 	 * @param viewer
 	 * @param mask
 	 * @param source
+	 * @param assignment
 	 * @param fillValue
 	 * @param fillDepth
 	 * @return affected interval
@@ -189,6 +203,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 			final ViewerPanelFX viewer,
 			final Mask<UnsignedLongType> mask,
 			final MaskedSource<T, ?> source,
+			final FragmentSegmentAssignment assignment,
 			final long fillValue,
 			final double fillDepth)
 	{
@@ -204,11 +219,11 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 		viewer.displayToSourceCoordinates(x, y, labelTransform, pos);
 		for (int d = 0; d < access.numDimensions(); ++d)
 			access.setPosition(Math.round(pos.getDoublePosition(d)), d);
-		final long seedLabel = access.get().getIntegerLong();
+		final long seedLabel = assignment != null ? assignment.getSegment(access.get().getIntegerLong()) : access.get().getIntegerLong();
 		LOG.debug("Got seed label {}", seedLabel);
 		final RandomAccessibleInterval<BoolType> relevantBackground = Converters.convert(
 				background,
-				(src, tgt) -> tgt.set(src.getIntegerLong() == seedLabel),
+				(src, tgt) -> tgt.set((assignment != null ? assignment.getSegment(src.getIntegerLong()) : src.getIntegerLong()) == seedLabel),
 				new BoolType()
 			);
 
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
index 7f0bf5123..432935abf 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
@@ -159,7 +159,7 @@ public LabelSourceState(
 		this.idSelectorHandler = new LabelSourceStateIdSelectorHandler(dataSource, selectedIds, assignment, lockedSegments);
 		this.mergeDetachHandler = new LabelSourceStateMergeDetachHandler(dataSource, selectedIds, assignment, idService);
 		if (dataSource instanceof MaskedSource<?, ?>)
-			this.shapeInterpolationMode = new ShapeInterpolationMode<>((MaskedSource<D, ?>) dataSource, this, selectedIds, idService, converter);
+			this.shapeInterpolationMode = new ShapeInterpolationMode<>((MaskedSource<D, ?>) dataSource, this, selectedIds, idService, converter, assignment);
 		else
 			this.shapeInterpolationMode = null;
 		this.displayStatus = createDisplayStatus();

From cd271e8be873e4f3e9737db02c13b1d50c62dbcf Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Thu, 6 Jun 2019 13:18:06 -0400
Subject: [PATCH 62/84] remove unneeded code in 3d flood filling

---
 .../paintera/control/paint/FloodFill.java     | 21 +------------------
 1 file changed, 1 insertion(+), 20 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java
index a0715b40d..48473086e 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java
@@ -41,7 +41,6 @@
 import net.imglib2.type.Type;
 import net.imglib2.type.label.Label;
 import net.imglib2.type.label.LabelMultisetType;
-import net.imglib2.type.label.LabelMultisetType.Entry;
 import net.imglib2.type.numeric.IntegerType;
 import net.imglib2.type.numeric.integer.UnsignedLongType;
 import net.imglib2.util.AccessBoxRandomAccessible;
@@ -230,8 +229,7 @@ private <T extends IntegerType<T>> void fill(
 		final RandomAccess<T> dataAccess = data.randomAccess();
 		dataAccess.setPosition(seed);
 		final T seedValue = dataAccess.get();
-		final long seedPrimitiveValue = seedValue instanceof LabelMultisetType ? getArgMaxLabel((LabelMultisetType) seedValue) : seedValue.getIntegerLong();
-		final long seedLabel = assignment != null ? assignment.getSegment(seedPrimitiveValue) : seedPrimitiveValue;
+		final long seedLabel = assignment != null ? assignment.getSegment(seedValue.getIntegerLong()) : seedValue.getIntegerLong();
 		if (!Label.regular(seedLabel))
 		{
 			LOG.info("Trying to fill at irregular label: {} ({})", seedLabel, new Point(seed));
@@ -390,21 +388,4 @@ public void run()
 		}
 
 	}
-
-	public static long getArgMaxLabel(final LabelMultisetType t)
-	{
-		long argmax = Label.INVALID;
-		long max    = 0;
-		for (final Entry<net.imglib2.type.label.Label> e : t.entrySet())
-		{
-			final int count = e.getCount();
-			if (count > max)
-			{
-				max = count;
-				argmax = e.getElement().id();
-			}
-		}
-		return argmax;
-	}
-
 }

From 0043e5c57f0d98a9a972d4e07dea37a7dbb871e7 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Thu, 6 Jun 2019 14:58:47 -0400
Subject: [PATCH 63/84] use last selected id for interpolated shape, or new id
 if wasn't selected

---
 .../control/ShapeInterpolationMode.java       | 31 ++++++++++++++++---
 1 file changed, 27 insertions(+), 4 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 8b25225e6..4fb0e1e38 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -190,6 +190,7 @@ private static final class SectionInfo
 	private final ObjectProperty<ActiveSection> activeSection = new SimpleObjectProperty<>();
 
 	private Thread workerThread;
+	private Pair<RealRandomAccessible<UnsignedLongType>, RealRandomAccessible<VolatileUnsignedLongType>> interpolatedMaskImgs;
 
 	public ShapeInterpolationMode(
 			final MaskedSource<D, ?> source,
@@ -365,16 +366,15 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed)
 					e.printStackTrace();
 				}
 			}
-
-			selectedIds.activate(lastActiveIds);
-			selectedIds.activateAlso(lastSelectedId);
-
 			resetMask();
 		}
 
 		converter.removeColor(newLabelId);
 		newLabelId = Label.INVALID;
 
+		selectedIds.activate(lastActiveIds);
+		selectedIds.activateAlso(lastSelectedId);
+
 		paintera.allowedActionsProperty().set(lastAllowedActions);
 		lastAllowedActions = null;
 
@@ -387,6 +387,7 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed)
 		mask = null;
 
 		workerThread = null;
+		interpolatedMaskImgs = null;
 		lastSelectedId = Label.INVALID;
 		lastActiveIds = null;
 
@@ -493,8 +494,29 @@ private void applyMask(final PainteraBaseView paintera)
 				sectionInfo2.get().sourceBoundingBox
 			);
 		LOG.info("Applying interpolated mask using bounding box of size {}", Intervals.dimensionsAsLongArray(sectionsUnionSourceInterval));
+
+		if (Label.regular(lastSelectedId))
+		{
+			final MaskInfo<UnsignedLongType> maskInfoWithLastSelectedLabelId = new MaskInfo<>(
+					source.getCurrentMask().info.t,
+					source.getCurrentMask().info.level,
+					new UnsignedLongType(lastSelectedId)
+				);
+			resetMask();
+			try {
+				source.setMask(maskInfoWithLastSelectedLabelId, interpolatedMaskImgs.getA(), interpolatedMaskImgs.getB(), FOREGROUND_CHECK);
+			} catch (final MaskInUse e) {
+				e.printStackTrace();
+			}
+		}
+		else
+		{
+			lastSelectedId = newLabelId;
+		}
+
 		source.isApplyingMaskProperty().addListener(doneApplyingMaskListener);
 		source.applyMask(source.getCurrentMask(), sectionsUnionSourceInterval, FOREGROUND_CHECK);
+
 		exitMode(paintera, true);
 	}
 
@@ -611,6 +633,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 							volatileInterpolatedShapeMask,
 							FOREGROUND_CHECK
 						);
+					interpolatedMaskImgs = new ValuePair<>(interpolatedShapeMask, volatileInterpolatedShapeMask);
 				}
 
 				for (final ViewerPanelFX viewer : getViewerPanels(paintera))

From 291e9f5633b01dd0116f627b06fae9f4064cf412 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 7 Jun 2019 11:43:06 -0400
Subject: [PATCH 64/84] improve allowed actions class and add custom predicate
 for actions

---
 .../saalfeldlab/paintera/Paintera.java        |   6 +-
 .../paintera/PainteraBaseView.java            |  86 +++++-----
 .../paintera/PainteraDefaultHandlers.java     |  26 +--
 .../paintera/SaveOnExitDialog.java            |   4 +-
 .../paintera/control/Navigation.java          |  83 ++++-----
 .../control/ShapeInterpolationMode.java       |  37 ++--
 .../paintera/control/actions/ActionType.java  |   8 +
 .../control/actions/AllowedActions.java       | 159 +++++++++++++-----
 .../paintera/control/actions/LabelAction.java |  28 ---
 .../control/actions/LabelActionType.java      |  28 +++
 .../paintera/control/actions/MenuAction.java  |  30 ----
 .../control/actions/MenuActionType.java       |  30 ++++
 .../control/actions/NavigationAction.java     |  26 ---
 .../control/actions/NavigationActionType.java |  26 +++
 .../paintera/control/actions/PaintAction.java |  28 ---
 .../control/actions/PaintActionType.java      |  28 +++
 .../LabelSourceStateIdSelectorHandler.java    |  31 ++--
 .../LabelSourceStateMergeDetachHandler.java   |  58 ++++---
 .../state/LabelSourceStatePaintHandler.java   |  67 ++++----
 19 files changed, 437 insertions(+), 352 deletions(-)
 create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/ActionType.java
 delete mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java
 create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelActionType.java
 delete mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java
 create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuActionType.java
 delete mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java
 create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationActionType.java
 delete mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java
 create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintActionType.java

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java b/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java
index c65f1dbbf..61c4a03aa 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java
@@ -18,7 +18,7 @@
 import org.janelia.saalfeldlab.paintera.config.OrthoSliceConfig;
 import org.janelia.saalfeldlab.paintera.config.ScreenScalesConfig;
 import org.janelia.saalfeldlab.paintera.control.CommitChanges;
-import org.janelia.saalfeldlab.paintera.control.actions.MenuAction;
+import org.janelia.saalfeldlab.paintera.control.actions.MenuActionType;
 import org.janelia.saalfeldlab.paintera.control.assignment.UnableToPersist;
 import org.janelia.saalfeldlab.paintera.data.mask.exception.CannotPersist;
 import org.janelia.saalfeldlab.paintera.serialization.GsonHelpers;
@@ -273,7 +273,7 @@ public void startImpl(final Stage stage) throws Exception
 				e -> {
 					e.consume();
 
-					if (!baseView.allowedActionsProperty().get().isAllowed(MenuAction.SaveProject))
+					if (!baseView.allowedActionsProperty().get().isAllowed(MenuActionType.SaveProject))
 					{
 						final Alert cannotSaveProjectDialog = PainteraAlerts.alert(Alert.AlertType.WARNING);
 						cannotSaveProjectDialog.setHeaderText("Cannot currently save the project.");
@@ -317,7 +317,7 @@ public void startImpl(final Stage stage) throws Exception
 						LOG.error("Unable to persist fragment-segment-assignment: {}", e1.getMessage());
 					}
 				},
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.CommitCanvas) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.C)
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.CommitCanvas) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.C)
 		                   ).installInto(paneWithStatus.getPane());
 
 		keyTracker.installInto(scene);
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java
index 9edf5c31d..53e622cdf 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java
@@ -1,26 +1,12 @@
 package org.janelia.saalfeldlab.paintera;
 
-import bdv.viewer.Interpolation;
-import bdv.viewer.SourceAndConverter;
-import bdv.viewer.ViewerOptions;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.SimpleObjectProperty;
-import javafx.collections.ListChangeListener;
-import javafx.collections.ObservableList;
-import javafx.event.Event;
-import javafx.scene.layout.Pane;
-import net.imglib2.RandomAccessibleInterval;
-import net.imglib2.Volatile;
-import net.imglib2.cache.LoaderCache;
-import net.imglib2.converter.ARGBColorConverter;
-import net.imglib2.converter.ARGBCompositeColorConverter;
-import net.imglib2.type.NativeType;
-import net.imglib2.type.Type;
-import net.imglib2.type.numeric.IntegerType;
-import net.imglib2.type.numeric.RealType;
-import net.imglib2.type.volatiles.AbstractVolatileNativeRealType;
-import net.imglib2.type.volatiles.AbstractVolatileRealType;
-import net.imglib2.view.composite.RealComposite;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.fx.event.MouseTracker;
 import org.janelia.saalfeldlab.fx.ortho.GridConstraintsManager;
@@ -39,16 +25,12 @@
 import org.janelia.saalfeldlab.paintera.config.OrthoSliceConfigBase;
 import org.janelia.saalfeldlab.paintera.config.Viewer3DConfig;
 import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions;
+import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions.AllowedActionsBuilder;
 import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder;
 import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrderNotSupported;
 import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
 import org.janelia.saalfeldlab.paintera.state.ChannelSourceState;
 import org.janelia.saalfeldlab.paintera.state.GlobalTransformManager;
-import org.janelia.saalfeldlab.paintera.state.HasFragmentSegmentAssignments;
-import org.janelia.saalfeldlab.paintera.state.HasHighlightingStreamConverter;
-import org.janelia.saalfeldlab.paintera.state.HasLockedSegments;
-import org.janelia.saalfeldlab.paintera.state.HasMeshes;
-import org.janelia.saalfeldlab.paintera.state.HasSelectedIds;
 import org.janelia.saalfeldlab.paintera.state.LabelSourceState;
 import org.janelia.saalfeldlab.paintera.state.RawSourceState;
 import org.janelia.saalfeldlab.paintera.state.SourceInfo;
@@ -59,12 +41,26 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
-import java.lang.invoke.MethodHandles;
-import java.nio.file.Files;
-import java.util.Optional;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import bdv.viewer.Interpolation;
+import bdv.viewer.SourceAndConverter;
+import bdv.viewer.ViewerOptions;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.scene.layout.Pane;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.Volatile;
+import net.imglib2.cache.LoaderCache;
+import net.imglib2.converter.ARGBColorConverter;
+import net.imglib2.converter.ARGBCompositeColorConverter;
+import net.imglib2.type.NativeType;
+import net.imglib2.type.Type;
+import net.imglib2.type.numeric.IntegerType;
+import net.imglib2.type.numeric.RealType;
+import net.imglib2.type.volatiles.AbstractVolatileNativeRealType;
+import net.imglib2.type.volatiles.AbstractVolatileRealType;
+import net.imglib2.view.composite.RealComposite;
 
 /**
  * Contains all the things necessary to build a Paintera UI, most importantly:
@@ -87,6 +83,8 @@ public class PainteraBaseView
 	// set this absurdly high
 	private static final int MAX_NUM_MIPMAP_LEVELS = 100;
 
+	private static final AllowedActions DEFAULT_ALLOWED_ACTIONS = AllowedActionsBuilder.all();
+
 	private final SourceInfo sourceInfo = new SourceInfo();
 
 	private final GlobalTransformManager manager = new GlobalTransformManager();
@@ -162,7 +160,7 @@ public PainteraBaseView(
 				s -> Optional.ofNullable(sourceInfo.getState(s)).map(SourceState::interpolationProperty).map(ObjectProperty::get).orElse(Interpolation.NLINEAR),
 				s -> Optional.ofNullable(sourceInfo.getState(s)).map(SourceState::getAxisOrder).orElse(null)
 		);
-		this.allowedActionsProperty = new SimpleObjectProperty<>(AllowedActions.all());
+		this.allowedActionsProperty = new SimpleObjectProperty<>(DEFAULT_ALLOWED_ACTIONS);
 		this.vsacUpdate = change -> views.setAllSources(visibleSourcesAndConverters);
 		visibleSourcesAndConverters.addListener(vsacUpdate);
 		LOG.debug("Meshes group={}", viewer3D.meshesGroup());
@@ -222,6 +220,14 @@ public ObjectProperty<AllowedActions> allowedActionsProperty()
 		return this.allowedActionsProperty;
 	}
 
+	/**
+	 * Set allowed actions in the normal application mode.
+	 */
+	public void setDefaultAllowedActions()
+	{
+		this.allowedActionsProperty.set(DEFAULT_ALLOWED_ACTIONS);
+	}
+
 	/**
 	 *
 	 * Add a source and state to the viewer
@@ -285,12 +291,12 @@ public <D, T> void addGenericState(final SourceState<D, T> state)
 	 */
 	public <D extends RealType<D> & NativeType<D>, T extends AbstractVolatileNativeRealType<D, T>> RawSourceState<D, T> addSingleScaleRawSource(
 			final RandomAccessibleInterval<D> data,
-			double[] resolution,
-			double[] offset,
-			double min,
-			String name,
-			double max) throws AxisOrderNotSupported {
-		RawSourceState<D, T> state = RawSourceState.simpleSourceFromSingleRAI(data, resolution, offset, min, max,
+			final double[] resolution,
+			final double[] offset,
+			final double min,
+			final String name,
+			final double max) throws AxisOrderNotSupported {
+		final RawSourceState<D, T> state = RawSourceState.simpleSourceFromSingleRAI(data, resolution, offset, min, max,
 				name);
 		InvokeOnJavaFXApplicationThread.invoke(() -> addRawSource(state));
 		return state;
@@ -364,7 +370,7 @@ public <T extends RealType<T>, U extends RealType<U>> void addRawSource(final Ra
 			final AxisOrder axisOrder,
 			final long maxId,
 			final String name) throws AxisOrderNotSupported {
-		LabelSourceState<D, T> state = LabelSourceState.simpleSourceFromSingleRAI(
+		final LabelSourceState<D, T> state = LabelSourceState.simpleSourceFromSingleRAI(
 				data,
 				resolution,
 				offset,
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
index 50b12d360..2da57a27a 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java
@@ -30,7 +30,7 @@
 import org.janelia.saalfeldlab.paintera.control.OrthogonalViewsValueDisplayListener;
 import org.janelia.saalfeldlab.paintera.control.RunWhenFirstElementIsAdded;
 import org.janelia.saalfeldlab.paintera.control.ShowOnlySelectedInStreamToggle;
-import org.janelia.saalfeldlab.paintera.control.actions.MenuAction;
+import org.janelia.saalfeldlab.paintera.control.actions.MenuActionType;
 import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners;
 import org.janelia.saalfeldlab.paintera.control.navigation.DisplayTransformUpdateOnResize;
 import org.janelia.saalfeldlab.paintera.state.SourceInfo;
@@ -241,7 +241,7 @@ public PainteraDefaultHandlers(
 				KeyEvent.KEY_PRESSED,
 				toggleSideBar));
 
-		baseView.allowedActionsProperty().addListener((obs, oldv, newv) -> paneWithStatus.getSideBar().setDisable(!newv.isAllowed(MenuAction.SidePanel)));
+		baseView.allowedActionsProperty().addListener((obs, oldv, newv) -> paneWithStatus.getSideBar().setDisable(!newv.isAllowed(MenuActionType.SidePanel)));
 
 		EventFX.KEY_PRESSED(
 				"toggle interpolation",
@@ -250,11 +250,11 @@ public PainteraDefaultHandlers(
 		EventFX.KEY_PRESSED(
 				"cycle current source",
 				e -> sourceInfo.incrementCurrentSourceIndex(),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ChangeActiveSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.TAB)).installInto(borderPane);
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ChangeActiveSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.TAB)).installInto(borderPane);
 		EventFX.KEY_PRESSED(
 				"backwards cycle current source",
 				e -> sourceInfo.decrementCurrentSourceIndex(),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ChangeActiveSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.TAB)).installInto(borderPane);
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ChangeActiveSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.TAB)).installInto(borderPane);
 
 		this.resizer = new GridResizer(gridConstraintsManager, 5, baseView.pane(), keyTracker);
 		this.resizer.installInto(baseView.pane());
@@ -286,30 +286,30 @@ public PainteraDefaultHandlers(
 		EventFX.KEY_PRESSED(
 				"toggle maximize viewer",
 				e -> toggleMaximizeTopLeft.toggleMaximizeViewer(),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topLeft().viewer());
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topLeft().viewer());
 		EventFX.KEY_PRESSED(
 				"toggle maximize viewer",
 				e -> toggleMaximizeTopRight.toggleMaximizeViewer(),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topRight().viewer());
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topRight().viewer());
 		EventFX.KEY_PRESSED(
 				"toggle maximize viewer",
 				e -> toggleMaximizeBottomLeft.toggleMaximizeViewer(),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.bottomLeft().viewer());
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.bottomLeft().viewer());
 
 		EventFX.KEY_PRESSED(
 				"toggle maximize viewer and orthoslice",
 				e -> toggleMaximizeTopLeft.toggleMaximizeViewerAndOrthoslice(),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.topLeft().viewer());
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.topLeft().viewer());
 
 		EventFX.KEY_PRESSED(
 				"toggle maximize viewer and orthoslice",
 				e -> toggleMaximizeTopRight.toggleMaximizeViewerAndOrthoslice(),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.topRight().viewer());
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.topRight().viewer());
 
 		EventFX.KEY_PRESSED(
 				"toggle maximize viewer and orthoslice",
 				e -> toggleMaximizeBottomLeft.toggleMaximizeViewerAndOrthoslice(),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.bottomLeft().viewer());
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.bottomLeft().viewer());
 
 
 		final CurrentSourceVisibilityToggle csv = new CurrentSourceVisibilityToggle(sourceInfo.currentState());
@@ -336,7 +336,7 @@ public PainteraDefaultHandlers(
 				MouseEvent.MOUSE_CLICKED,
 				e -> {
 					LOG.debug("Handling event {}", e);
-					if (baseView.allowedActionsProperty().get().isAllowed(MenuAction.OrthoslicesContextMenu) &&
+					if (baseView.allowedActionsProperty().get().isAllowed(MenuActionType.OrthoslicesContextMenu) &&
 							MouseButton.SECONDARY.equals(e.getButton()) &&
 							e.getClickCount() == 1 &&
 							!mouseTracker.isDragging())
@@ -355,7 +355,7 @@ public PainteraDefaultHandlers(
 						projectDirectory,
 						Exceptions.handler("Paintera", "Unable to create new Dataset"),
 						baseView.sourceInfo().currentSourceProperty().get()),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.CreateNewLabelSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.N)).installInto(paneWithStatus.getPane());
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.CreateNewLabelSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.N)).installInto(paneWithStatus.getPane());
 	}
 
 	private final Map<ViewerPanelFX, ViewerAndTransforms> viewerToTransforms = new HashMap<>();
@@ -488,7 +488,7 @@ public static EventHandler<KeyEvent> addOpenDatasetContextMenuHandler(
 		final EventHandler<KeyEvent> handler = OpenDialogMenu.keyPressedHandler(
 				target,
 				exception -> Exceptions.exceptionAlert(Paintera.NAME, "Unable to show open dataset menu", exception),
-				e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.AddSource) && keyTracker.areOnlyTheseKeysDown(triggers),
+				e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.AddSource) && keyTracker.areOnlyTheseKeysDown(triggers),
 				"Open dataset",
 				baseView,
 				projectDirectory,
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java b/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java
index 62e0d831b..64c8b346d 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java
@@ -63,8 +63,8 @@ public void handle(final WindowEvent event)
 				return;
 			}
 
-			// run cleanup if the application is not currently in the normal mode
-			baseView.allowedActionsProperty().get().cleanup(baseView);
+			// ensure that the application is in the normal mode when the project is saved
+			baseView.setDefaultAllowedActions();
 
 			if (saveButton.equals(response))
 			{
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java
index 43bc865a5..1e49961cb 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java
@@ -12,6 +12,22 @@
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 
+import org.janelia.saalfeldlab.fx.event.EventFX;
+import org.janelia.saalfeldlab.fx.event.InstallAndRemove;
+import org.janelia.saalfeldlab.fx.event.KeyTracker;
+import org.janelia.saalfeldlab.fx.event.MouseDragFX;
+import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions;
+import org.janelia.saalfeldlab.paintera.control.actions.NavigationActionType;
+import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners;
+import org.janelia.saalfeldlab.paintera.control.navigation.ButtonRotationSpeedConfig;
+import org.janelia.saalfeldlab.paintera.control.navigation.KeyRotate;
+import org.janelia.saalfeldlab.paintera.control.navigation.RemoveRotation;
+import org.janelia.saalfeldlab.paintera.control.navigation.Rotate;
+import org.janelia.saalfeldlab.paintera.control.navigation.TranslateAlongNormal;
+import org.janelia.saalfeldlab.paintera.control.navigation.TranslateWithinPlane;
+import org.janelia.saalfeldlab.paintera.control.navigation.Zoom;
+import org.janelia.saalfeldlab.paintera.state.GlobalTransformManager;
+
 import bdv.fx.viewer.ViewerPanelFX;
 import javafx.beans.binding.Bindings;
 import javafx.beans.binding.DoubleBinding;
@@ -29,21 +45,6 @@
 import javafx.scene.input.MouseButton;
 import javafx.scene.input.MouseEvent;
 import net.imglib2.realtransform.AffineTransform3D;
-import org.janelia.saalfeldlab.fx.event.EventFX;
-import org.janelia.saalfeldlab.fx.event.InstallAndRemove;
-import org.janelia.saalfeldlab.fx.event.KeyTracker;
-import org.janelia.saalfeldlab.fx.event.MouseDragFX;
-import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions;
-import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction;
-import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners;
-import org.janelia.saalfeldlab.paintera.control.navigation.ButtonRotationSpeedConfig;
-import org.janelia.saalfeldlab.paintera.control.navigation.KeyRotate;
-import org.janelia.saalfeldlab.paintera.control.navigation.RemoveRotation;
-import org.janelia.saalfeldlab.paintera.control.navigation.Rotate;
-import org.janelia.saalfeldlab.paintera.control.navigation.TranslateAlongNormal;
-import org.janelia.saalfeldlab.paintera.control.navigation.TranslateWithinPlane;
-import org.janelia.saalfeldlab.paintera.control.navigation.Zoom;
-import org.janelia.saalfeldlab.paintera.state.GlobalTransformManager;
 
 public class Navigation implements ToOnEnterOnExit
 {
@@ -166,55 +167,55 @@ public Consumer<ViewerPanelFX> getOnEnter()
 				iars.add(EventFX.SCROLL(
 						"translate along normal",
 						e -> scrollDefault.scroll(-ControlUtils.getBiggestScroll(e)),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.noKeysActive()
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.noKeysActive()
 				                       ));
 				iars.add(EventFX.SCROLL(
 						"translate along normal fast",
 						e -> scrollFast.scroll(-ControlUtils.getBiggestScroll(e)),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)
 				                       ));
 				iars.add(EventFX.SCROLL(
 						"translate along normal slow",
 						e -> scrollSlow.scroll(-ControlUtils.getBiggestScroll(e)),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL)
 				                       ));
 
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal bck",
 						e -> scrollDefault.scroll(+1),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA)
 				                            ));
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal fwd",
 						e -> scrollDefault.scroll(-1),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD)
 				                            ));
 
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal fast bck",
 						e -> scrollFast.scroll(+1),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.SHIFT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.SHIFT)
 				                            ));
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal fast fwd",
 						e -> scrollFast.scroll(-1),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.SHIFT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.SHIFT)
 				                            ));
 
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal slow bck",
 						e -> scrollSlow.scroll(+1),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.CONTROL)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.CONTROL)
 				                            ));
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal slow fwd",
 						e -> scrollSlow.scroll(-1),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.CONTROL)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.CONTROL)
 				                            ));
 
 				iars.add(MouseDragFX.createDrag(
 						"translate xy",
-						e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Drag) && e.isSecondaryButtonDown() && keyTracker.noKeysActive(),
+						e -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Drag) && e.isSecondaryButtonDown() && keyTracker.noKeysActive(),
 						true,
 						manager,
 						e -> translateXY.init(),
@@ -226,7 +227,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 				iars.add(EventFX.SCROLL(
 						"zoom",
 						event -> zoom.zoomCenteredAt(-ControlUtils.getBiggestScroll(event), event.getX(), event.getY()),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Zoom) &&
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Zoom) &&
 								(keyTracker.areOnlyTheseKeysDown(KeyCode.META) ||
 								keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT))
 				                       ));
@@ -238,7 +239,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 								mouseXIfInsideElseCenterX.get(),
 								mouseYIfInsideElseCenterY.get()
 						                            ),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Zoom) &&
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Zoom) &&
 								(keyTracker.areOnlyTheseKeysDown(KeyCode.MINUS)
 								|| keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.MINUS)
 								|| keyTracker.areOnlyTheseKeysDown(KeyCode.DOWN))
@@ -251,7 +252,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 								mouseXIfInsideElseCenterX.get(),
 								mouseYIfInsideElseCenterY.get()
 						                            ),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Zoom) &&
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Zoom) &&
 								(keyTracker.areOnlyTheseKeysDown(KeyCode.EQUALS)
 								|| keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.EQUALS)
 								|| keyTracker.areOnlyTheseKeysDown(KeyCode.UP))
@@ -266,7 +267,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalToViewerTransform,
 						manager::setTransform,
 						manager,
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.noKeysActive() && event.getButton().equals(MouseButton.PRIMARY)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.noKeysActive() && event.getButton().equals(MouseButton.PRIMARY)
 				                        ));
 
 				iars.add(rotationHandler(
@@ -278,7 +279,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalToViewerTransform,
 						manager::setTransform,
 						manager,
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT) && event.getButton().equals
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT) && event.getButton().equals
 								(MouseButton.PRIMARY)
 				                        ));
 
@@ -291,7 +292,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalToViewerTransform,
 						manager::setTransform,
 						manager,
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL) && event.getButton().equals(
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL) && event.getButton().equals(
 								MouseButton.PRIMARY)
 				                        ));
 
@@ -299,17 +300,17 @@ public Consumer<ViewerPanelFX> getOnEnter()
 				iars.add(EventFX.KEY_PRESSED(
 						"set key rotation axis x",
 						e -> keyRotationAxis.set(KeyRotate.Axis.X),
-						e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.X)
+						e -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.X)
 				                            ));
 				iars.add(EventFX.KEY_PRESSED(
 						"set key rotation axis y",
 						e -> keyRotationAxis.set(KeyRotate.Axis.Y),
-						e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.Y)
+						e -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.Y)
 				                            ));
 				iars.add(EventFX.KEY_PRESSED(
 						"set key rotation axis z",
 						e -> keyRotationAxis.set(KeyRotate.Axis.Z),
-						e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.Z)
+						e -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.Z)
 				                            ));
 
 				iars.add(keyRotationHandler(
@@ -324,7 +325,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalTransform,
 						manager::setTransform,
 						manager,
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.LEFT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.LEFT)
 				                           ));
 
 				iars.add(keyRotationHandler(
@@ -339,7 +340,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalTransform,
 						manager::setTransform,
 						manager,
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.LEFT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.LEFT)
 				                           ));
 
 				iars.add(keyRotationHandler(
@@ -354,7 +355,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalTransform,
 						manager::setTransform,
 						manager,
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.LEFT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.LEFT)
 				                           ));
 
 				iars.add(keyRotationHandler(
@@ -369,7 +370,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalTransform,
 						manager::setTransform,
 						manager,
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.RIGHT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.RIGHT)
 				                           ));
 
 				iars.add(keyRotationHandler(
@@ -384,7 +385,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalTransform,
 						manager::setTransform,
 						manager,
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.RIGHT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.RIGHT)
 				                           ));
 
 				iars.add(keyRotationHandler(
@@ -399,7 +400,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 						globalTransform,
 						manager::setTransform,
 						manager,
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.RIGHT)
+						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.RIGHT)
 				                           ));
 
 				final RemoveRotation removeRotation = new RemoveRotation(
@@ -414,7 +415,7 @@ public Consumer<ViewerPanelFX> getOnEnter()
 								mouseXIfInsideElseCenterX.get(),
 								mouseYIfInsideElseCenterY.get()
 						                                            ),
-						e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.Z)
+						e -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.Z)
 				                            ));
 
 				this.mouseAndKeyHandlers.put(t, iars);
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 4fb0e1e38..dea7c2368 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -2,7 +2,6 @@
 
 import java.lang.invoke.MethodHandles;
 import java.util.Arrays;
-import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
@@ -12,10 +11,9 @@
 import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread;
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
 import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions;
-import org.janelia.saalfeldlab.paintera.control.actions.LabelAction;
-import org.janelia.saalfeldlab.paintera.control.actions.MenuAction;
-import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction;
-import org.janelia.saalfeldlab.paintera.control.actions.PaintAction;
+import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions.AllowedActionsBuilder;
+import org.janelia.saalfeldlab.paintera.control.actions.MenuActionType;
+import org.janelia.saalfeldlab.paintera.control.actions.NavigationActionType;
 import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment;
 import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D;
 import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
@@ -36,6 +34,7 @@
 import gnu.trove.iterator.TLongObjectIterator;
 import gnu.trove.map.TLongObjectMap;
 import gnu.trove.map.hash.TLongObjectHashMap;
+import javafx.beans.InvalidationListener;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.value.ChangeListener;
@@ -190,6 +189,8 @@ private static final class SectionInfo
 	private final ObjectProperty<ActiveSection> activeSection = new SimpleObjectProperty<>();
 
 	private Thread workerThread;
+	private InvalidationListener modeSwitchListener;
+
 	private Pair<RealRandomAccessible<UnsignedLongType>, RealRandomAccessible<VolatileUnsignedLongType>> interpolatedMaskImgs;
 
 	public ShapeInterpolationMode(
@@ -207,21 +208,10 @@ public ShapeInterpolationMode(
 		this.converter = converter;
 		this.assignment = assignment;
 
-		final Consumer<PainteraBaseView> cleanup = baseView -> exitMode(baseView, false);
-		this.allowedActions = new AllowedActions(
-				NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll),
-				LabelAction.none(),
-				PaintAction.none(),
-				MenuAction.of(MenuAction.ToggleMaximizeViewer),
-				cleanup
-			);
-		this.allowedActionsWhenSelected = new AllowedActions(
-				NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom),
-				LabelAction.none(),
-				PaintAction.none(),
-				MenuAction.of(MenuAction.ToggleMaximizeViewer),
-				cleanup
-			);
+		final AllowedActionsBuilder allowedActionsBuilder = new AllowedActionsBuilder();
+		allowedActionsBuilder.add(NavigationActionType.Drag, NavigationActionType.Zoom, MenuActionType.ToggleMaximizeViewer);
+		this.allowedActionsWhenSelected = allowedActionsBuilder.create();
+		this.allowedActions = allowedActionsBuilder.add(NavigationActionType.Scroll).create();
 
 		this.doneApplyingMaskListener = (obs, oldv, newv) -> {
 			if (!newv)
@@ -335,6 +325,10 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 		lastAllowedActions = paintera.allowedActionsProperty().get();
 		paintera.allowedActionsProperty().set(allowedActions);
 
+		// properly exit the mode if somebody else wants to switch it
+		modeSwitchListener = obs -> exitMode(paintera, false);
+		paintera.allowedActionsProperty().addListener(modeSwitchListener);
+
 		lastSelectedId = selectedIds.getLastSelection();
 		lastActiveIds = selectedIds.getActiveIds();
 		newLabelId = idService.next();
@@ -375,6 +369,9 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed)
 		selectedIds.activate(lastActiveIds);
 		selectedIds.activateAlso(lastSelectedId);
 
+		paintera.allowedActionsProperty().removeListener(modeSwitchListener);
+		modeSwitchListener = null;
+
 		paintera.allowedActionsProperty().set(lastAllowedActions);
 		lastAllowedActions = null;
 
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/ActionType.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/ActionType.java
new file mode 100644
index 000000000..36ae084aa
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/ActionType.java
@@ -0,0 +1,8 @@
+package org.janelia.saalfeldlab.paintera.control.actions;
+
+/**
+ * Marker interface for all action types.
+ */
+public interface ActionType
+{
+}
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 906040bb5..7bb12bbe5 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
@@ -1,70 +1,137 @@
 package org.janelia.saalfeldlab.paintera.control.actions;
 
-import java.util.EnumSet;
-import java.util.function.Consumer;
-
-import org.janelia.saalfeldlab.paintera.PainteraBaseView;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.BooleanSupplier;
+import java.util.stream.Collectors;
 
 /**
  * Describes what actions in the UI are allowed in the current application mode.
+ * An optional custom predicate can be specified for any action type to determine if it is allowed (or do extra handling before the action is performed).
  */
 public final class AllowedActions
 {
-	private final EnumSet<NavigationAction> navigationAllowedActions;
-	private final EnumSet<LabelAction> labelAllowedActions;
-	private final EnumSet<PaintAction> paintAllowedActions;
-	private final EnumSet<MenuAction> menuAllowedActions;
-
-	private final Consumer<PainteraBaseView> cleanup;
+	private final Map<ActionType, BooleanSupplier> actions;
 
-	public AllowedActions(
-			final EnumSet<NavigationAction> navigationAllowedActions,
-			final EnumSet<LabelAction> labelAllowedActions,
-			final EnumSet<PaintAction> paintAllowedActions,
-			final EnumSet<MenuAction> menuAllowedActions,
-			final Consumer<PainteraBaseView> cleanup)
+	private AllowedActions(final Map<ActionType, BooleanSupplier> actions)
 	{
-		this.navigationAllowedActions = navigationAllowedActions;
-		this.labelAllowedActions = labelAllowedActions;
-		this.paintAllowedActions = paintAllowedActions;
-		this.menuAllowedActions = menuAllowedActions;
-		this.cleanup = cleanup;
+		this.actions = actions;
 	}
 
-	public boolean isAllowed(final NavigationAction navigationAction)
+	public boolean isAllowed(final ActionType actionType)
 	{
-		return this.navigationAllowedActions.contains(navigationAction);
+		return Optional.ofNullable(this.actions.get(actionType)).orElse(() -> false).getAsBoolean();
 	}
 
-	public boolean isAllowed(final LabelAction labelAction)
+	public void runIfAllowed(final ActionType actionType, final Runnable action)
 	{
-		return this.labelAllowedActions.contains(labelAction);
+		if (isAllowed(actionType))
+			action.run();
 	}
 
-	public boolean isAllowed(final PaintAction paintAction)
+	/**
+	 * Used to create an instance of {@link AllowedActions}.
+	 */
+	public static class AllowedActionsBuilder
 	{
-		return this.paintAllowedActions.contains(paintAction);
-	}
+		private static final Map<ActionType, BooleanSupplier> ALL;
+		static
+		{
+			final Set<ActionType> actions = new HashSet<>();
+			actions.addAll(NavigationActionType.all());
+			actions.addAll(LabelActionType.all());
+			actions.addAll(PaintActionType.all());
+			actions.addAll(MenuActionType.all());
+			ALL = toMap(actions);
+		}
+		private static Map<ActionType, BooleanSupplier> toMap(final Collection<ActionType> actions)
+		{
+			return actions.stream().collect(Collectors.toMap(t -> t, t -> () -> true));
+		}
 
-	public boolean isAllowed(final MenuAction menuAction)
-	{
-		return this.menuAllowedActions.contains(menuAction);
-	}
+		/**
+		 * Create a new instance of {@link AllowedActions} with all known actions.
+		 *
+		 * @return
+		 */
+		public static AllowedActions all()
+		{
+			return new AllowedActions(ALL);
+		}
 
-	public void cleanup(final PainteraBaseView baseView)
-	{
-		if (this.cleanup != null)
-			this.cleanup.accept(baseView);
-	}
+		private final Map<ActionType, BooleanSupplier> actions;
 
-	public static AllowedActions all()
-	{
-		return new AllowedActions(
-			NavigationAction.all(),
-			LabelAction.all(),
-			PaintAction.all(),
-			MenuAction.all(),
-			null
-		);
+		public AllowedActionsBuilder()
+		{
+			this.actions = new HashMap<>();
+		}
+
+		/**
+		 * Add one or more allowed {@link ActionType actions}.
+		 *
+		 * @param first
+		 * @param rest
+		 * @return
+		 */
+		public AllowedActionsBuilder add(final ActionType first, final ActionType... rest)
+		{
+			final Set<ActionType> set = new HashSet<>();
+			set.add(first);
+			set.addAll(Arrays.asList(rest));
+			add(set);
+			return this;
+		}
+
+		/**
+		 * Add a collection of allowed {@link ActionType actions}.
+		 *
+		 * @param actions
+		 * @return
+		 */
+		public AllowedActionsBuilder add(final Collection<ActionType> actions)
+		{
+			this.actions.putAll(toMap(actions));
+			return this;
+		}
+
+		/**
+		 * Add an {@link ActionType action} with a custom predicate to determine if it is allowed.
+		 *
+		 * @param action
+		 * @param predicate
+		 * @return
+		 */
+		public AllowedActionsBuilder add(final ActionType action, final BooleanSupplier predicate)
+		{
+			this.actions.put(action, predicate);
+			return this;
+		}
+
+		/**
+		 * Add a collection of {@link ActionType actions} with a custom predicate to determine if it is allowed.
+		 *
+		 * @param actions
+		 * @return
+		 */
+		public AllowedActionsBuilder add(final Map<ActionType, BooleanSupplier> actions)
+		{
+			this.actions.putAll(actions);
+			return this;
+		}
+
+		/**
+		 * Create a new instance of {@link AllowedActions} with the current set of actions.
+		 *
+		 * @return
+		 */
+		public AllowedActions create()
+		{
+			return new AllowedActions(new HashMap<>(this.actions));
+		}
 	}
 }
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java
deleted file mode 100644
index 190d5ba4b..000000000
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.janelia.saalfeldlab.paintera.control.actions;
-
-import java.util.EnumSet;
-
-public enum LabelAction
-{
-	Toggle,
-	Append,
-	CreateNew,
-	Lock,
-	Merge,
-	Split;
-
-	public static EnumSet<LabelAction> of(final LabelAction first, final LabelAction... rest)
-	{
-		return EnumSet.of(first, rest);
-	}
-
-	public static EnumSet<LabelAction> all()
-	{
-		return EnumSet.allOf(LabelAction.class);
-	}
-
-	public static EnumSet<LabelAction> none()
-	{
-		return EnumSet.noneOf(LabelAction.class);
-	}
-}
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
new file mode 100644
index 000000000..a1f8aa163
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelActionType.java
@@ -0,0 +1,28 @@
+package org.janelia.saalfeldlab.paintera.control.actions;
+
+import java.util.EnumSet;
+
+public enum LabelActionType implements ActionType
+{
+	Toggle,
+	Append,
+	CreateNew,
+	Lock,
+	Merge,
+	Split;
+
+	public static EnumSet<LabelActionType> of(final LabelActionType first, final LabelActionType... rest)
+	{
+		return EnumSet.of(first, rest);
+	}
+
+	public static EnumSet<LabelActionType> all()
+	{
+		return EnumSet.allOf(LabelActionType.class);
+	}
+
+	public static EnumSet<LabelActionType> none()
+	{
+		return EnumSet.noneOf(LabelActionType.class);
+	}
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java
deleted file mode 100644
index 63b0108c7..000000000
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.janelia.saalfeldlab.paintera.control.actions;
-
-import java.util.EnumSet;
-
-public enum MenuAction
-{
-	AddSource,
-	CreateNewLabelSource,
-	ChangeActiveSource,
-	SidePanel,
-	ToggleMaximizeViewer,
-	OrthoslicesContextMenu,
-	SaveProject,
-	CommitCanvas;
-
-	public static EnumSet<MenuAction> of(final MenuAction first, final MenuAction... rest)
-	{
-		return EnumSet.of(first, rest);
-	}
-
-	public static EnumSet<MenuAction> all()
-	{
-		return EnumSet.allOf(MenuAction.class);
-	}
-
-	public static EnumSet<MenuAction> none()
-	{
-		return EnumSet.noneOf(MenuAction.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
new file mode 100644
index 000000000..74aa3a923
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuActionType.java
@@ -0,0 +1,30 @@
+package org.janelia.saalfeldlab.paintera.control.actions;
+
+import java.util.EnumSet;
+
+public enum MenuActionType implements ActionType
+{
+	AddSource,
+	CreateNewLabelSource,
+	ChangeActiveSource,
+	SidePanel,
+	ToggleMaximizeViewer,
+	OrthoslicesContextMenu,
+	SaveProject,
+	CommitCanvas;
+
+	public static EnumSet<MenuActionType> of(final MenuActionType first, final MenuActionType... rest)
+	{
+		return EnumSet.of(first, rest);
+	}
+
+	public static EnumSet<MenuActionType> all()
+	{
+		return EnumSet.allOf(MenuActionType.class);
+	}
+
+	public static EnumSet<MenuActionType> none()
+	{
+		return EnumSet.noneOf(MenuActionType.class);
+	}
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java
deleted file mode 100644
index 57788a4e8..000000000
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.janelia.saalfeldlab.paintera.control.actions;
-
-import java.util.EnumSet;
-
-public enum NavigationAction
-{
-	Drag,
-	Scroll,
-	Zoom,
-	Rotate;
-
-	public static EnumSet<NavigationAction> of(final NavigationAction first, final NavigationAction... rest)
-	{
-		return EnumSet.of(first, rest);
-	}
-
-	public static EnumSet<NavigationAction> all()
-	{
-		return EnumSet.allOf(NavigationAction.class);
-	}
-
-	public static EnumSet<NavigationAction> none()
-	{
-		return EnumSet.noneOf(NavigationAction.class);
-	}
-}
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationActionType.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationActionType.java
new file mode 100644
index 000000000..d65cb5c3b
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationActionType.java
@@ -0,0 +1,26 @@
+package org.janelia.saalfeldlab.paintera.control.actions;
+
+import java.util.EnumSet;
+
+public enum NavigationActionType implements ActionType
+{
+	Drag,
+	Scroll,
+	Zoom,
+	Rotate;
+
+	public static EnumSet<NavigationActionType> of(final NavigationActionType first, final NavigationActionType... rest)
+	{
+		return EnumSet.of(first, rest);
+	}
+
+	public static EnumSet<NavigationActionType> all()
+	{
+		return EnumSet.allOf(NavigationActionType.class);
+	}
+
+	public static EnumSet<NavigationActionType> none()
+	{
+		return EnumSet.noneOf(NavigationActionType.class);
+	}
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java
deleted file mode 100644
index dc0436c7a..000000000
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.janelia.saalfeldlab.paintera.control.actions;
-
-import java.util.EnumSet;
-
-public enum PaintAction
-{
-	Paint,
-	Erase,
-	Background,
-	Fill,
-	Restrict,
-	SetBrush;
-
-	public static EnumSet<PaintAction> of(final PaintAction first, final PaintAction... rest)
-	{
-		return EnumSet.of(first, rest);
-	}
-
-	public static EnumSet<PaintAction> all()
-	{
-		return EnumSet.allOf(PaintAction.class);
-	}
-
-	public static EnumSet<PaintAction> none()
-	{
-		return EnumSet.noneOf(PaintAction.class);
-	}
-}
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
new file mode 100644
index 000000000..60e7f194e
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintActionType.java
@@ -0,0 +1,28 @@
+package org.janelia.saalfeldlab.paintera.control.actions;
+
+import java.util.EnumSet;
+
+public enum PaintActionType implements ActionType
+{
+	Paint,
+	Erase,
+	Background,
+	Fill,
+	Restrict,
+	SetBrush;
+
+	public static EnumSet<PaintActionType> of(final PaintActionType first, final PaintActionType... rest)
+	{
+		return EnumSet.of(first, rest);
+	}
+
+	public static EnumSet<PaintActionType> all()
+	{
+		return EnumSet.allOf(PaintActionType.class);
+	}
+
+	public static EnumSet<PaintActionType> none()
+	{
+		return EnumSet.noneOf(PaintActionType.class);
+	}
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java
index 714b00165..f6fbc4540 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java
@@ -1,19 +1,14 @@
 package org.janelia.saalfeldlab.paintera.state;
 
-import bdv.fx.viewer.ViewerPanelFX;
-import javafx.event.Event;
-import javafx.event.EventHandler;
-import javafx.event.EventTarget;
-import javafx.scene.Node;
-import javafx.scene.input.KeyCode;
-import javafx.scene.input.MouseEvent;
-import net.imglib2.type.numeric.IntegerType;
+import java.lang.invoke.MethodHandles;
+import java.util.HashMap;
+
 import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
 import org.janelia.saalfeldlab.fx.event.EventFX;
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
 import org.janelia.saalfeldlab.paintera.control.IdSelector;
-import org.janelia.saalfeldlab.paintera.control.actions.LabelAction;
+import org.janelia.saalfeldlab.paintera.control.actions.LabelActionType;
 import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment;
 import org.janelia.saalfeldlab.paintera.control.lock.LockedSegments;
 import org.janelia.saalfeldlab.paintera.control.paint.SelectNextId;
@@ -22,8 +17,14 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.lang.invoke.MethodHandles;
-import java.util.HashMap;
+import bdv.fx.viewer.ViewerPanelFX;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.EventTarget;
+import javafx.scene.Node;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.MouseEvent;
+import net.imglib2.type.numeric.IntegerType;
 
 public class LabelSourceStateIdSelectorHandler {
 
@@ -74,24 +75,24 @@ private EventHandler<Event> makeHandler(final PainteraBaseView paintera, final K
 		// TODO event handlers should probably not be on ANY/RELEASED but on PRESSED
 		handler.addEventHandler(MouseEvent.ANY, selector.selectFragmentWithMaximumCount(
 				"toggle single id",
-				event -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Toggle) && event.isPrimaryButtonDown() && keyTracker.noKeysActive()).handler());
+				event -> paintera.allowedActionsProperty().get().isAllowed(LabelActionType.Toggle) && event.isPrimaryButtonDown() && keyTracker.noKeysActive()).handler());
 		handler.addEventHandler(MouseEvent.ANY, selector.appendFragmentWithMaximumCount(
 				"append id",
-				event -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Append) &&
+				event -> paintera.allowedActionsProperty().get().isAllowed(LabelActionType.Append) &&
 						((event.isSecondaryButtonDown() && keyTracker.noKeysActive()) ||
 						(event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL))))
 			.handler());
 		handler.addOnKeyPressed(EventFX.KEY_PRESSED(
 				"lock segment",
 				e -> selector.toggleLock(selectedIds, assignment, lockedSegments),
-				e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Lock) && keyTracker.areOnlyTheseKeysDown(KeyCode.L)));
+				e -> paintera.allowedActionsProperty().get().isAllowed(LabelActionType.Lock) && keyTracker.areOnlyTheseKeysDown(KeyCode.L)));
 
 		final SourceInfo sourceInfo = paintera.sourceInfo();
 		final SelectNextId nextId = new SelectNextId(sourceInfo);
 		handler.addOnKeyPressed(EventFX.KEY_PRESSED(
 				"next id",
 				event -> nextId.getNextId(),
-				event -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.CreateNew) && keyTracker.areOnlyTheseKeysDown(KeyCode.N)));
+				event -> paintera.allowedActionsProperty().get().isAllowed(LabelActionType.CreateNew) && keyTracker.areOnlyTheseKeysDown(KeyCode.N)));
 		return handler;
 	}
 }
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java
index 4c968b18e..d68610b85 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java
@@ -1,5 +1,26 @@
 package org.janelia.saalfeldlab.paintera.state;
 
+import java.lang.invoke.MethodHandles;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
+import org.janelia.saalfeldlab.fx.event.EventFX;
+import org.janelia.saalfeldlab.fx.event.KeyTracker;
+import org.janelia.saalfeldlab.paintera.PainteraBaseView;
+import org.janelia.saalfeldlab.paintera.control.actions.LabelActionType;
+import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment;
+import org.janelia.saalfeldlab.paintera.control.assignment.action.AssignmentAction;
+import org.janelia.saalfeldlab.paintera.control.assignment.action.Detach;
+import org.janelia.saalfeldlab.paintera.control.assignment.action.Merge;
+import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
+import org.janelia.saalfeldlab.paintera.data.DataSource;
+import org.janelia.saalfeldlab.paintera.id.IdService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import bdv.fx.viewer.ViewerPanelFX;
 import bdv.fx.viewer.ViewerState;
 import bdv.viewer.Interpolation;
@@ -24,26 +45,6 @@
 import net.imglib2.type.numeric.IntegerType;
 import net.imglib2.view.IntervalView;
 import net.imglib2.view.Views;
-import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
-import org.janelia.saalfeldlab.fx.event.EventFX;
-import org.janelia.saalfeldlab.fx.event.KeyTracker;
-import org.janelia.saalfeldlab.paintera.PainteraBaseView;
-import org.janelia.saalfeldlab.paintera.control.actions.LabelAction;
-import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment;
-import org.janelia.saalfeldlab.paintera.control.assignment.action.AssignmentAction;
-import org.janelia.saalfeldlab.paintera.control.assignment.action.Detach;
-import org.janelia.saalfeldlab.paintera.control.assignment.action.Merge;
-import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
-import org.janelia.saalfeldlab.paintera.data.DataSource;
-import org.janelia.saalfeldlab.paintera.id.IdService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.lang.invoke.MethodHandles;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Optional;
-import java.util.function.Consumer;
 
 public class LabelSourceStateMergeDetachHandler {
 
@@ -88,20 +89,20 @@ public EventHandler<Event> viewerHandler(final PainteraBaseView paintera, final
 		};
 	}
 
-	private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker keyTracker, ViewerPanelFX vp) {
+	private EventHandler<Event> makeHandler(final PainteraBaseView paintera, final KeyTracker keyTracker, final ViewerPanelFX vp) {
 		final DelegateEventHandlers.AnyHandler handler = DelegateEventHandlers.handleAny();
 		handler.addOnMousePressed(EventFX.MOUSE_PRESSED(
 				"merge fragments",
 				new MergeFragments(vp),
-				e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Merge) && e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)));
+				e -> paintera.allowedActionsProperty().get().isAllowed(LabelActionType.Merge) && e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)));
 		handler.addOnMousePressed(EventFX.MOUSE_PRESSED(
 				"detach fragment",
 				new DetachFragment(vp),
-				e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Split) && e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)));
+				e -> paintera.allowedActionsProperty().get().isAllowed(LabelActionType.Split) && e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)));
 		handler.addOnMousePressed(EventFX.MOUSE_PRESSED(
 				"detach fragment",
 				new ConfirmSelection(vp),
-				e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Split) && e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.CONTROL)));
+				e -> paintera.allowedActionsProperty().get().isAllowed(LabelActionType.Split) && e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.CONTROL)));
 		return handler;
 	}
 
@@ -109,10 +110,11 @@ private class MergeFragments implements Consumer<MouseEvent> {
 
 		private final ViewerPanelFX viewer;
 
-		private MergeFragments(ViewerPanelFX viewer) {
+		private MergeFragments(final ViewerPanelFX viewer) {
 			this.viewer = viewer;
 		}
 
+		@Override
 		public void accept(final MouseEvent e)
 		{
 			synchronized (viewer)
@@ -156,10 +158,11 @@ private class DetachFragment implements Consumer<MouseEvent>
 	{
 		private final ViewerPanelFX viewer;
 
-		private DetachFragment(ViewerPanelFX viewer) {
+		private DetachFragment(final ViewerPanelFX viewer) {
 			this.viewer = viewer;
 		}
 
+		@Override
 		public void accept(final MouseEvent e)
 		{
 			final long lastSelection = selectedIds.getLastSelection();
@@ -196,10 +199,11 @@ private class ConfirmSelection implements Consumer<MouseEvent>
 
 		private final ViewerPanelFX viewer;
 
-		private ConfirmSelection(ViewerPanelFX viewer) {
+		private ConfirmSelection(final ViewerPanelFX viewer) {
 			this.viewer = viewer;
 		}
 
+		@Override
 		public void accept(final MouseEvent e)
 		{
 						final long[] activeFragments = selectedIds.getActiveIds();
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java
index 483a60dfa..43d33b850 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java
@@ -1,23 +1,16 @@
 package org.janelia.saalfeldlab.paintera.state;
 
-import bdv.fx.viewer.ViewerPanelFX;
-import bdv.viewer.Source;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.SimpleDoubleProperty;
-import javafx.event.Event;
-import javafx.event.EventHandler;
-import javafx.event.EventTarget;
-import javafx.scene.Node;
-import javafx.scene.input.KeyCode;
-import javafx.scene.input.KeyEvent;
-import javafx.scene.input.MouseEvent;
-import net.imglib2.type.label.Label;
+import java.lang.invoke.MethodHandles;
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.function.Supplier;
+
 import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers;
 import org.janelia.saalfeldlab.fx.event.EventFX;
 import org.janelia.saalfeldlab.fx.event.KeyTracker;
 import org.janelia.saalfeldlab.paintera.PainteraBaseView;
 import org.janelia.saalfeldlab.paintera.control.ControlUtils;
-import org.janelia.saalfeldlab.paintera.control.actions.PaintAction;
+import org.janelia.saalfeldlab.paintera.control.actions.PaintActionType;
 import org.janelia.saalfeldlab.paintera.control.paint.Fill2DOverlay;
 import org.janelia.saalfeldlab.paintera.control.paint.FillOverlay;
 import org.janelia.saalfeldlab.paintera.control.paint.FloodFill;
@@ -29,10 +22,18 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.lang.invoke.MethodHandles;
-import java.util.HashMap;
-import java.util.Optional;
-import java.util.function.Supplier;
+import bdv.fx.viewer.ViewerPanelFX;
+import bdv.viewer.Source;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.EventTarget;
+import javafx.scene.Node;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import net.imglib2.type.label.Label;
 
 public class LabelSourceStatePaintHandler {
 
@@ -72,7 +73,7 @@ public EventHandler<Event> viewerHandler(final PainteraBaseView paintera, final
 		};
 	}
 
-	public EventHandler<Event> viewerFilter(PainteraBaseView paintera, KeyTracker keyTracker) {
+	public EventHandler<Event> viewerFilter(final PainteraBaseView paintera, final KeyTracker keyTracker) {
 		return event -> {
 			final EventTarget target = event.getTarget();
 			if (MouseEvent.MOUSE_EXITED.equals(event.getEventType()) && target instanceof ViewerPanelFX)
@@ -80,7 +81,7 @@ public EventHandler<Event> viewerFilter(PainteraBaseView paintera, KeyTracker ke
 		};
 	}
 
-	private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker keyTracker, final ViewerPanelFX t)
+	private EventHandler<Event> makeHandler(final PainteraBaseView paintera, final KeyTracker keyTracker, final ViewerPanelFX t)
 	{
 
 		LOG.debug("Making handler with PainterBaseView {} key Tracker {} and ViewerPanelFX {}", paintera, keyTracker, t);
@@ -125,22 +126,22 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke
 		handler.addEventHandler(KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED(
 				"show brush overlay",
 				event -> {LOG.trace("Showing brush overlay!"); paint2D.showBrushOverlay();},
-				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Paint) && keyTracker.areKeysDown(KeyCode.SPACE)));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Paint) && keyTracker.areKeysDown(KeyCode.SPACE)));
 
 		handler.addEventHandler(KeyEvent.KEY_RELEASED, EventFX.KEY_RELEASED(
 				"hide brush overlay",
 				event -> {LOG.trace("Hiding brush overlay!"); paint2D.hideBrushOverlay();},
-				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Paint) && event.getCode().equals(KeyCode.SPACE) && !keyTracker.areKeysDown(KeyCode.SPACE)));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Paint) && event.getCode().equals(KeyCode.SPACE) && !keyTracker.areKeysDown(KeyCode.SPACE)));
 
 		handler.addOnScroll(EventFX.SCROLL(
 				"change brush size",
 				event -> paint2D.changeBrushRadius(event.getDeltaY()),
-				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.SetBrush) && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE)));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.SetBrush) && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE)));
 
 		handler.addOnScroll(EventFX.SCROLL(
 				"change brush depth",
 				event -> paint2D.changeBrushDepth(-ControlUtils.getBiggestScroll(event)),
-				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.SetBrush) &&
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.SetBrush) &&
 					(keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE, KeyCode.SHIFT) ||
 					keyTracker.areOnlyTheseKeysDown(KeyCode.F) ||
 					keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT,KeyCode.F))));
@@ -148,22 +149,22 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke
 		handler.addOnKeyPressed(EventFX.KEY_PRESSED("show fill 2D overlay", event -> {
 			fill2DOverlay.setVisible(true);
 			fillOverlay.setVisible(false);
-		}, event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && keyTracker.areOnlyTheseKeysDown(KeyCode.F)));
+		}, event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Fill) && keyTracker.areOnlyTheseKeysDown(KeyCode.F)));
 
 		handler.addOnKeyReleased(EventFX.KEY_RELEASED(
 				"show fill 2D overlay",
 				event -> fill2DOverlay.setVisible(false),
-				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && event.getCode().equals(KeyCode.F) && keyTracker.noKeysActive()));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Fill) && event.getCode().equals(KeyCode.F) && keyTracker.noKeysActive()));
 
 		handler.addOnKeyPressed(EventFX.KEY_PRESSED("show fill overlay", event -> {
 			fillOverlay.setVisible(true);
 			fill2DOverlay.setVisible(false);
-		}, event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && keyTracker.areOnlyTheseKeysDown(KeyCode.F, KeyCode.SHIFT)));
+		}, event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Fill) && keyTracker.areOnlyTheseKeysDown(KeyCode.F, KeyCode.SHIFT)));
 
 		handler.addOnKeyReleased(EventFX.KEY_RELEASED(
 				"show fill overlay",
 				event -> fillOverlay.setVisible(false),
-				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) &&
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Fill) &&
 					((event.getCode().equals(KeyCode.F) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)) ||
 					(event.getCode().equals(KeyCode.SHIFT) && keyTracker.areOnlyTheseKeysDown(KeyCode.F))
 			)));
@@ -175,7 +176,7 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke
 				paintSelection,
 				brushRadius::get,
 				brushDepth::get,
-				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Paint) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Paint) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE));
 		handler.addEventHandler(MouseEvent.ANY, paintDrag.singleEventHandler());
 
 		// erase
@@ -185,7 +186,7 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke
 				() -> Label.TRANSPARENT,
 				brushRadius::get,
 				brushDepth::get,
-				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Erase) && event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Erase) && event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE));
 		handler.addEventHandler(MouseEvent.ANY, eraseDrag.singleEventHandler());
 
 		// background
@@ -195,26 +196,26 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke
 				() -> Label.BACKGROUND,
 				brushRadius::get,
 				brushDepth::get,
-				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Background) && event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE, KeyCode.SHIFT));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Background) && event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE, KeyCode.SHIFT));
 		handler.addEventHandler(MouseEvent.ANY, backgroundDrag.singleEventHandler());
 
 		// advanced paint stuff
 		handler.addOnMousePressed((EventFX.MOUSE_PRESSED(
 				"fill",
 				event -> fill.fillAt(event.getX(), event.getY(), paintSelection::get),
-				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Fill) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(
 						KeyCode.SHIFT,
 						KeyCode.F))));
 
 		handler.addOnMousePressed(EventFX.MOUSE_PRESSED(
 				"fill 2D",
 				event -> fill2D.fillAt(event.getX(), event.getY(), paintSelection::get),
-				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.F)));
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Fill) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.F)));
 
 		handler.addOnMousePressed(EventFX.MOUSE_PRESSED(
 				"restrict",
 				event -> restrictor.restrictTo(event.getX(), event.getY()),
-				event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Restrict) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(
+				event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Restrict) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(
 						KeyCode.SHIFT,
 						KeyCode.R)));
 

From e7f537beb8606c4ea10bdb7fc10d4865307181fb Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 7 Jun 2019 11:54:28 -0400
Subject: [PATCH 65/84] fix sections on scroll, commit interpolated shape on
 Enter

---
 .../paintera/control/Navigation.java          | 81 ++++++++++++-------
 .../control/ShapeInterpolationMode.java       | 41 +++-------
 2 files changed, 67 insertions(+), 55 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java
index 1e49961cb..b117deb53 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java
@@ -166,52 +166,79 @@ public Consumer<ViewerPanelFX> getOnEnter()
 
 				iars.add(EventFX.SCROLL(
 						"translate along normal",
-						e -> scrollDefault.scroll(-ControlUtils.getBiggestScroll(e)),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.noKeysActive()
-				                       ));
+						e -> this.allowedActionsProperty.get().runIfAllowed(
+								NavigationActionType.Scroll,
+								() -> scrollDefault.scroll(-ControlUtils.getBiggestScroll(e))
+							),
+						event -> keyTracker.noKeysActive()
+					));
 				iars.add(EventFX.SCROLL(
 						"translate along normal fast",
-						e -> scrollFast.scroll(-ControlUtils.getBiggestScroll(e)),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)
-				                       ));
+						e -> this.allowedActionsProperty.get().runIfAllowed(
+								NavigationActionType.Scroll,
+								() -> scrollFast.scroll(-ControlUtils.getBiggestScroll(e))
+							),
+						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)
+					));
 				iars.add(EventFX.SCROLL(
 						"translate along normal slow",
-						e -> scrollSlow.scroll(-ControlUtils.getBiggestScroll(e)),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL)
-				                       ));
+						e -> this.allowedActionsProperty.get().runIfAllowed(
+								NavigationActionType.Scroll,
+								() -> scrollSlow.scroll(-ControlUtils.getBiggestScroll(e))
+							),
+						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL)
+					));
 
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal bck",
-						e -> scrollDefault.scroll(+1),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA)
-				                            ));
+						e -> this.allowedActionsProperty.get().runIfAllowed(
+								NavigationActionType.Scroll,
+								() -> scrollDefault.scroll(+1)
+							),
+						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA)
+					));
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal fwd",
-						e -> scrollDefault.scroll(-1),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD)
-				                            ));
+						e -> this.allowedActionsProperty.get().runIfAllowed(
+								NavigationActionType.Scroll,
+								() -> scrollDefault.scroll(-1)
+							),
+						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD)
+					));
 
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal fast bck",
-						e -> scrollFast.scroll(+1),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.SHIFT)
-				                            ));
+						e -> this.allowedActionsProperty.get().runIfAllowed(
+								NavigationActionType.Scroll,
+								() -> scrollFast.scroll(+1)
+							),
+						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.SHIFT)
+					));
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal fast fwd",
-						e -> scrollFast.scroll(-1),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.SHIFT)
-				                            ));
+						e -> this.allowedActionsProperty.get().runIfAllowed(
+								NavigationActionType.Scroll,
+								() -> scrollFast.scroll(-1)
+							),
+						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.SHIFT)
+					));
 
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal slow bck",
-						e -> scrollSlow.scroll(+1),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.CONTROL)
-				                            ));
+						e -> this.allowedActionsProperty.get().runIfAllowed(
+								NavigationActionType.Scroll,
+								() -> scrollSlow.scroll(+1)
+							),
+						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.CONTROL)
+					));
 				iars.add(EventFX.KEY_PRESSED(
 						"button translate along normal slow fwd",
-						e -> scrollSlow.scroll(-1),
-						event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.CONTROL)
-				                            ));
+						e -> this.allowedActionsProperty.get().runIfAllowed(
+								NavigationActionType.Scroll,
+								() -> scrollSlow.scroll(-1)
+							),
+						event -> keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.CONTROL)
+					));
 
 				iars.add(MouseDragFX.createDrag(
 						"translate xy",
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index dea7c2368..419921edf 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -167,15 +167,12 @@ private static final class SectionInfo
 	private final HighlightingStreamConverter<?> converter;
 	private final FragmentSegmentAssignment assignment;
 
-	private final AllowedActions allowedActions;
-	private final AllowedActions allowedActionsWhenSelected;
-
-	private final ChangeListener<Boolean> doneApplyingMaskListener;
-
+	private InvalidationListener modeSwitchListener;
 	private AllowedActions lastAllowedActions;
 	private long lastSelectedId;
 	private long[] lastActiveIds;
 
+	private final ChangeListener<Boolean> doneApplyingMaskListener;
 	private Mask<UnsignedLongType> mask;
 	private long newLabelId;
 	private long currentFillValue;
@@ -189,8 +186,6 @@ private static final class SectionInfo
 	private final ObjectProperty<ActiveSection> activeSection = new SimpleObjectProperty<>();
 
 	private Thread workerThread;
-	private InvalidationListener modeSwitchListener;
-
 	private Pair<RealRandomAccessible<UnsignedLongType>, RealRandomAccessible<VolatileUnsignedLongType>> interpolatedMaskImgs;
 
 	public ShapeInterpolationMode(
@@ -208,11 +203,6 @@ public ShapeInterpolationMode(
 		this.converter = converter;
 		this.assignment = assignment;
 
-		final AllowedActionsBuilder allowedActionsBuilder = new AllowedActionsBuilder();
-		allowedActionsBuilder.add(NavigationActionType.Drag, NavigationActionType.Zoom, MenuActionType.ToggleMaximizeViewer);
-		this.allowedActionsWhenSelected = allowedActionsBuilder.create();
-		this.allowedActions = allowedActionsBuilder.add(NavigationActionType.Scroll).create();
-
 		this.doneApplyingMaskListener = (obs, oldv, newv) -> {
 			if (!newv)
 				doneApplyingMask();
@@ -257,23 +247,13 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 						e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE)
 					)
 			);
-		filter.addEventHandler(
-				KeyEvent.KEY_PRESSED,
-				EventFX.KEY_PRESSED(
-						"fix selection",
-						e -> {e.consume(); fixSelection(paintera);},
-						e -> (modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) &&
-							!selectedObjects.isEmpty() &&
-							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
-					)
-			);
 		filter.addEventHandler(
 				KeyEvent.KEY_PRESSED,
 				EventFX.KEY_PRESSED(
 						"apply mask",
 						e -> {e.consume(); applyMask(paintera);},
 						e -> modeState.get() == ModeState.Preview &&
-							keyTracker.areOnlyTheseKeysDown(KeyCode.S)
+							keyTracker.areOnlyTheseKeysDown(KeyCode.ENTER)
 					)
 			);
 		filter.addEventHandler(
@@ -322,8 +302,17 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 		activeViewer.set(viewer);
 		setDisableOtherViewers(paintera, true);
 
+		// set allowed actions in this mode
+		final AllowedActionsBuilder allowedActionsBuilder = new AllowedActionsBuilder();
+		allowedActionsBuilder.add(NavigationActionType.Drag, NavigationActionType.Zoom, MenuActionType.ToggleMaximizeViewer);
+		allowedActionsBuilder.add(NavigationActionType.Scroll, () -> {
+			// allow to scroll through sections, but fix the selection first if the object selection is not empty
+			if ((modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) && !selectedObjects.isEmpty())
+				fixSelection(paintera);
+			return true;
+		});
 		lastAllowedActions = paintera.allowedActionsProperty().get();
-		paintera.allowedActionsProperty().set(allowedActions);
+		paintera.allowedActionsProperty().set(allowedActionsBuilder.create());
 
 		// properly exit the mode if somebody else wants to switch it
 		modeSwitchListener = obs -> exitMode(paintera, false);
@@ -450,7 +439,6 @@ private void fixSelection(final PainteraBaseView paintera)
 			activeSection.set(ActiveSection.Second);
 			resetMask();
 			activeViewerProperty().get().requestRepaint();
-			paintera.allowedActionsProperty().set(allowedActions);
 		}
 		else
 		{
@@ -474,7 +462,6 @@ private void editSelection(final PainteraBaseView paintera, final ActiveSection
 		mask = sectionInfo.mask;
 
 		paintera.manager().setTransform(sectionInfo.globalTransform);
-		paintera.allowedActionsProperty().set(allowedActionsWhenSelected);
 
 		selectedObjects.clear();
 		selectedObjects.putAll(sectionInfo.selectedObjects);
@@ -643,7 +630,6 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 
 			InvokeOnJavaFXApplicationThread.invoke(() -> {
 				modeState.set(ModeState.Preview);
-				paintera.allowedActionsProperty().set(allowedActions);
 			});
 		});
 		workerThread.start();
@@ -781,7 +767,6 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 		}
 
 		activeViewer.get().requestRepaint();
-		paintera.allowedActionsProperty().set(selectedObjects.isEmpty() ? allowedActions : allowedActionsWhenSelected);
 	}
 
 	/**

From ddeefa4f3f5134d9e28a4c0213f7867009deaad1 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 7 Jun 2019 12:10:27 -0400
Subject: [PATCH 66/84] request to repaint in all viewers after object
 selection/deselection

---
 .../control/ShapeInterpolationMode.java       | 39 +++++++++----------
 1 file changed, 18 insertions(+), 21 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 419921edf..1488d7289 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -158,8 +158,6 @@ private static final class SectionInfo
 
 	private static final Predicate<UnsignedLongType> FOREGROUND_CHECK = t -> t.get() > 0;
 
-	private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>();
-
 	private final MaskedSource<D, ?> source;
 	private final LabelSourceState<D, ?> sourceState;
 	private final SelectedIds selectedIds;
@@ -167,6 +165,7 @@ private static final class SectionInfo
 	private final HighlightingStreamConverter<?> converter;
 	private final FragmentSegmentAssignment assignment;
 
+	private ViewerPanelFX activeViewer;
 	private InvalidationListener modeSwitchListener;
 	private AllowedActions lastAllowedActions;
 	private long lastSelectedId;
@@ -209,11 +208,6 @@ public ShapeInterpolationMode(
 		};
 	}
 
-	public ObjectProperty<ViewerPanelFX> activeViewerProperty()
-	{
-		return activeViewer;
-	}
-
 	public ObjectProperty<ModeState> modeStateProperty()
 	{
 		return modeState;
@@ -299,7 +293,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 			return;
 		}
 		LOG.info("Entering shape interpolation mode");
-		activeViewer.set(viewer);
+		activeViewer = viewer;
 		setDisableOtherViewers(paintera, true);
 
 		// set allowed actions in this mode
@@ -377,8 +371,9 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed)
 		lastSelectedId = Label.INVALID;
 		lastActiveIds = null;
 
-		activeViewer.get().requestRepaint();
-		activeViewer.set(null);
+		for (final ViewerPanelFX viewer : getViewerPanels(paintera))
+			viewer.requestRepaint();
+		activeViewer = null;
 	}
 
 	public boolean isModeOn()
@@ -388,7 +383,7 @@ public boolean isModeOn()
 
 	private void createMask() throws MaskInUse
 	{
-		final int time = activeViewer.get().getState().timepointProperty().get();
+		final int time = activeViewer.getState().timepointProperty().get();
 		final int level = MASK_SCALE_LEVEL;
 		final MaskInfo<UnsignedLongType> maskInfo = new MaskInfo<>(time, level, new UnsignedLongType(newLabelId));
 		mask = source.generateMask(maskInfo, FOREGROUND_CHECK);
@@ -408,7 +403,7 @@ private void setDisableOtherViewers(final PainteraBaseView paintera, final boole
 	{
 		for (final ViewerPanelFX viewer : getViewerPanels(paintera))
 		{
-			if (viewer != activeViewer.get())
+			if (viewer != activeViewer)
 			{
 				viewer.setDisable(disable);
 				if (disable)
@@ -438,7 +433,8 @@ private void fixSelection(final PainteraBaseView paintera)
 			// let the user now select the second section
 			activeSection.set(ActiveSection.Second);
 			resetMask();
-			activeViewerProperty().get().requestRepaint();
+			for (final ViewerPanelFX viewer : getViewerPanels(paintera))
+				viewer.requestRepaint();
 		}
 		else
 		{
@@ -766,7 +762,8 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 			resetMask();
 		}
 
-		activeViewer.get().requestRepaint();
+		for (final ViewerPanelFX viewer : getViewerPanels(paintera))
+			viewer.requestRepaint();
 	}
 
 	/**
@@ -778,7 +775,7 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 	 */
 	private Pair<Long, Interval> runFloodFillToSelect(final double x, final double y)
 	{
-		final Interval affectedInterval = FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, assignment, ++currentFillValue, FILL_DEPTH);
+		final Interval affectedInterval = FloodFill2D.fillMaskAt(x, y, activeViewer, mask, source, assignment, ++currentFillValue, FILL_DEPTH);
 		return new ValuePair<>(currentFillValue, affectedInterval);
 	}
 
@@ -798,7 +795,7 @@ private long runFloodFillToDeselect(final double x, final double y)
 				(in, out) -> out.set(in.getIntegerLong() == maskValue),
 				new BoolType()
 			);
-		FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, predicate, getMaskTransform(), Label.BACKGROUND, FILL_DEPTH);
+		FloodFill2D.fillMaskAt(x, y, activeViewer, mask, predicate, getMaskTransform(), Label.BACKGROUND, FILL_DEPTH);
 		return maskValue;
 	}
 
@@ -814,7 +811,7 @@ private UnsignedLongType getMaskValue(final double x, final double y)
 	private AffineTransform3D getMaskTransform()
 	{
 		final AffineTransform3D maskTransform = new AffineTransform3D();
-		final int time = activeViewer.get().getState().timepointProperty().get();
+		final int time = activeViewer.getState().timepointProperty().get();
 		final int level = MASK_SCALE_LEVEL;
 		source.getSourceTransform(time, level, maskTransform);
 		return maskTransform;
@@ -823,7 +820,7 @@ private AffineTransform3D getMaskTransform()
 	private AffineTransform3D getDisplayTransform()
 	{
 		final AffineTransform3D viewerTransform = new AffineTransform3D();
-		activeViewer.get().getState().getViewerTransform(viewerTransform);
+		activeViewer.getState().getViewerTransform(viewerTransform);
 		return viewerTransform;
 	}
 
@@ -846,7 +843,7 @@ private AffineTransform3D getMaskDisplayTransformIgnoreScaling(final int targetL
 		if (targetLevel != maskLevel)
 		{
 			// scale with respect to the given mipmap level
-			final int time = activeViewer.get().getState().timepointProperty().get();
+			final int time = activeViewer.getState().timepointProperty().get();
 			final Scale3D relativeScaleTransform = new Scale3D(DataSource.getRelativeScales(source, time, maskLevel, targetLevel));
 			maskMipmapDisplayTransform.preConcatenate(relativeScaleTransform.inverse());
 		}
@@ -867,7 +864,7 @@ private AffineTransform3D getMaskDisplayTransformIgnoreScaling()
 		Arrays.setAll(viewerScale, d -> Affine3DHelpers.extractScale(viewerTransform, d));
 		final Scale3D scalingTransform = new Scale3D(viewerScale);
 		// neutralize mask scaling if there is any
-		final int time = activeViewer.get().getState().timepointProperty().get();
+		final int time = activeViewer.getState().timepointProperty().get();
 		final int level = MASK_SCALE_LEVEL;
 		scalingTransform.concatenate(new Scale3D(DataSource.getScale(source, time, level)));
 		// build the resulting transform
@@ -878,7 +875,7 @@ private RealPoint getSourceCoordinates(final double x, final double y)
 	{
 		final AffineTransform3D maskTransform = getMaskTransform();
 		final RealPoint sourcePos = new RealPoint(maskTransform.numDimensions());
-		activeViewer.get().displayToSourceCoordinates(x, y, maskTransform, sourcePos);
+		activeViewer.displayToSourceCoordinates(x, y, maskTransform, sourcePos);
 		return sourcePos;
 	}
 

From 1fccf84b6a40bf88fc16632cb879b81b154375dd Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 7 Jun 2019 13:19:06 -0400
Subject: [PATCH 67/84] allow to immediately switch between editing first and
 second section

---
 .../control/ShapeInterpolationMode.java       | 40 ++++++++++++++-----
 1 file changed, 29 insertions(+), 11 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 1488d7289..9457babd1 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -255,7 +255,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				EventFX.KEY_PRESSED(
 						"edit selection 1",
 						e -> {e.consume(); editSelection(paintera, ActiveSection.First);},
-						e -> modeState.get() == ModeState.Preview &&
+						e -> (modeState.get() == ModeState.Interpolate || modeState.get() == ModeState.Preview || modeState.get() == ModeState.Edit) &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT1)
 					)
 			);
@@ -264,7 +264,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				EventFX.KEY_PRESSED(
 						"edit selection 2",
 						e -> {e.consume(); editSelection(paintera, ActiveSection.Second);},
-						e -> modeState.get() == ModeState.Preview &&
+						e -> (modeState.get() == ModeState.Interpolate || modeState.get() == ModeState.Preview || modeState.get() == ModeState.Edit) &&
 							keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT2)
 					)
 			);
@@ -302,7 +302,10 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 		allowedActionsBuilder.add(NavigationActionType.Scroll, () -> {
 			// allow to scroll through sections, but fix the selection first if the object selection is not empty
 			if ((modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) && !selectedObjects.isEmpty())
+			{
 				fixSelection(paintera);
+				advanceMode(paintera);
+			}
 			return true;
 		});
 		lastAllowedActions = paintera.allowedActionsProperty().get();
@@ -334,15 +337,7 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed)
 
 		if (!completed) // extra cleanup if the mode is aborted
 		{
-			if (workerThread != null)
-			{
-				workerThread.interrupt();
-				try {
-					workerThread.join();
-				} catch (final InterruptedException e) {
-					e.printStackTrace();
-				}
-			}
+			interruptInterpolation();
 			resetMask();
 		}
 
@@ -427,7 +422,10 @@ private void fixSelection(final PainteraBaseView paintera)
 		LOG.debug("Fix selection");
 		sectionInfoPropertyToSet.set(createSectionInfo(paintera));
 		selectedObjects.clear();
+	}
 
+	private void advanceMode(final PainteraBaseView paintera)
+	{
 		if (modeState.get() != ModeState.Edit && sectionInfo2.get() == null)
 		{
 			// let the user now select the second section
@@ -446,6 +444,13 @@ private void fixSelection(final PainteraBaseView paintera)
 
 	private void editSelection(final PainteraBaseView paintera, final ActiveSection section)
 	{
+		interruptInterpolation();
+
+		if (activeSection.get() == section)
+			return;
+		else if (activeSection.get() != null)
+			fixSelection(paintera);
+
 		final ObjectProperty<SectionInfo> sectionInfoPropertyToEdit = section == ActiveSection.First ? sectionInfo1 : sectionInfo2;
 		final SectionInfo sectionInfo = sectionInfoPropertyToEdit.get();
 
@@ -631,6 +636,19 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 		workerThread.start();
 	}
 
+	private void interruptInterpolation()
+	{
+		if (workerThread != null)
+		{
+			workerThread.interrupt();
+			try {
+				workerThread.join();
+			} catch (final InterruptedException e) {
+				e.printStackTrace();
+			}
+		}
+	}
+
 	private static <R extends RealType<R> & NativeType<R>, B extends BooleanType<B>> void computeSignedDistanceTransform(
 			final RandomAccessibleInterval<B> mask,
 			final RandomAccessibleInterval<R> target,

From 96ed205684179228a9cac0bd6645c80acfba3c7f Mon Sep 17 00:00:00 2001
From: Igor Pisarev <igorpisarev@users.noreply.github.com>
Date: Fri, 7 Jun 2019 13:33:30 -0400
Subject: [PATCH 68/84] update shape interpolation description and shortcuts

---
 README.md | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index ddd680b36..732e54c7a 100644
--- a/README.md
+++ b/README.md
@@ -162,9 +162,10 @@ Usage: Paintera [-h] [--default-to-temp-directory] [--print-error-codes]
 | `F` + left click | 2D Flood-fill in current viewer plane with id that was last toggled active (if any) |
 | `Shift` + `F` + left click | Flood-fill with id that was last toggled active (if any) |
 | `N` | Select new, previously unused id |
-| `S` | Enter/advance shape interpolation mode |
+| `S` | Enter shape interpolation mode |
 | `1` / `2` | Edit first/second section when previewing interpolated shape |
-| `ESC` | Exit shape interpolation mode |
+| `Enter` | Commit interpolated shape into canvas |
+| `Esc` | Abort shape interpolation mode |
 | `Ctrl` + `C` | Show dialog to commit canvas and/or assignments |
 | `C` | Increment ARGB stream seed by one |
 | `Shift` + `C` | Decrement ARGB stream seed by one |
@@ -179,11 +180,11 @@ Usage: Paintera [-h] [--default-to-temp-directory] [--print-error-codes]
 
 ### Shape interpolation mode
 
-The mode is activated by pressing the `S` key when the current source is a label source. Then, you can select the objects in the first section by left/right clicking and hit `S` to fix the selection and switch to the second section (scrolling through sections works only when there are no selected objects).
+The mode is activated by pressing the `S` key when the current source is a label source. Then, you can select the objects in the sections by left/right clicking (scrolling automatically fixes the selection in the current section).
 
-When you're done with selecting the objects in the second section, hit `S` again to interpolate between the sections. The interpolated preview will be displayed, where you can scroll through the sections and make sure that the interpolated shape is correct. If something is not right, you can edit the selection in the first or second section by pressing `1` or `2`, and then press `S` to update the interpolated shape. When the desired result is reached, hit `S` again to initiate committing the results into the canvas using a new label ID and return back to normal mode.
+When you're done with selecting the objects in the second section and initiate scrolling, the preview of the interpolated shape will be displayed. If something is not right, you can edit the selection in the first or second section by pressing `1` or `2`, which will update the preview. When the desired result is reached, hit `Enter` to commit the results into the canvas and return back to normal mode.
 
-While in the shape interpolation mode, at any point in time you can hit `ESC` to discard the current state and exit the mode.
+While in the shape interpolation mode, at any point in time you can hit `Esc` to discard the current state and exit the mode.
 
 ## Data
 

From fbbbc6a02d5ed1b9ccc3ba46ad68ecb22e93a72a Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 7 Jun 2019 16:00:41 -0400
Subject: [PATCH 69/84] optimize number of 2d flood-fill operations in shape
 interpolation mode

---
 .../control/ShapeInterpolationMode.java       | 39 ++++++++++++-------
 1 file changed, 26 insertions(+), 13 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 9457babd1..030454914 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -747,30 +747,43 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 			return;
 
 		final boolean wasSelected = FOREGROUND_CHECK.test(maskValue);
-		final int numSelectedObjects = selectedObjects.size();
-
-		LOG.debug("Object was clicked: deactivateOthers={}, wasSelected={}, numSelectedObjects", deactivateOthers, wasSelected, numSelectedObjects);
+		LOG.debug("Object was clicked: deactivateOthers={}, wasSelected={}", deactivateOthers, wasSelected);
 
 		if (deactivateOthers)
 		{
+			// If the clicked object is not selected, deselect all other objects and select the clicked object.
+			// If the clicked object is the only selected object, toggle it.
+			// If the clicked object is selected along with some other objects, deselect the others and keep the clicked one selected.
+			final boolean keepClickedObjectSelected = wasSelected && selectedObjects.size() > 1;
 			for (final TLongObjectIterator<SelectedObjectInfo> it = selectedObjects.iterator(); it.hasNext();)
 			{
 				it.advance();
 				final double[] deselectDisplayPos = getDisplayCoordinates(it.value().sourceClickPosition);
-				runFloodFillToDeselect(deselectDisplayPos[0], deselectDisplayPos[1]);
+				if (!keepClickedObjectSelected || !getMaskValue(deselectDisplayPos[0], deselectDisplayPos[1]).valueEquals(maskValue))
+				{
+					runFloodFillToDeselect(deselectDisplayPos[0], deselectDisplayPos[1]);
+					it.remove();
+				}
+			}
+			if (!wasSelected)
+			{
+				final Pair<Long, Interval> fillValueAndInterval = runFloodFillToSelect(x, y);
+				selectedObjects.put(fillValueAndInterval.getA(), new SelectedObjectInfo(getSourceCoordinates(x, y), fillValueAndInterval.getB()));
 			}
-			selectedObjects.clear();
-		}
-
-		if (!wasSelected || (deactivateOthers && numSelectedObjects > 1))
-		{
-			final Pair<Long, Interval> fillValueAndInterval = runFloodFillToSelect(x, y);
-			selectedObjects.put(fillValueAndInterval.getA(), new SelectedObjectInfo(getSourceCoordinates(x, y), fillValueAndInterval.getB()));
 		}
 		else
 		{
-			final long oldFillValue = runFloodFillToDeselect(x, y);
-			selectedObjects.remove(oldFillValue);
+			// Simply toggle the clicked object
+			if (!wasSelected)
+			{
+				final Pair<Long, Interval> fillValueAndInterval = runFloodFillToSelect(x, y);
+				selectedObjects.put(fillValueAndInterval.getA(), new SelectedObjectInfo(getSourceCoordinates(x, y), fillValueAndInterval.getB()));
+			}
+			else
+			{
+				final long oldFillValue = runFloodFillToDeselect(x, y);
+				selectedObjects.remove(oldFillValue);
+			}
 		}
 
 		// free the mask if there are no selected objects

From 064704ef715eeae78c28b58300b579b8b89b71ae Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 7 Jun 2019 16:17:29 -0400
Subject: [PATCH 70/84] floodfill 2d bugfix: check if inside clicked object in
 adjacent sections

When 2D flood-filling in an orthogonal view, there is a special case implementation that runs a genuine 2D flood-fill algorithm on a number of sections around the clicked one depending on the specified depth. If the depth is thicker than 1 section, it would run the flood-filling in adjacent sections starting from the same seed point. However, if this location in the adjacent section happens to be outside the clicked object, the outside value is used in the predicate which leads to filling the entire section.
---
 .../paintera/control/paint/FloodFill2D.java   | 29 +++++++++++--------
 1 file changed, 17 insertions(+), 12 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
index b2d8f6340..e6ecb9e33 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
@@ -308,22 +308,27 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 						extendedFilter,
 						fillNormalAxisInLabelCoordinateSystem,
 						i
-				                                                                             );
+					);
 				final MixedTransformView<UnsignedLongType> relevantAccessTracker = Views.hyperSlice(
 						accessTracker,
 						fillNormalAxisInLabelCoordinateSystem,
 						i
-				                                                                                   );
-
-				FloodFill.fill(
-						relevantBackgroundSlice,
-						relevantAccessTracker,
-						new Point(seed2D),
-						new UnsignedLongType(fillValue),
-						new DiamondShape(1)
-				              );
-				Arrays.setAll(min, d -> Math.min(accessTracker.getMin()[d], min[d]));
-				Arrays.setAll(max, d -> Math.max(accessTracker.getMax()[d], max[d]));
+					);
+
+				final RandomAccess<BoolType> filterRandomAccess = relevantBackgroundSlice.randomAccess();
+				filterRandomAccess.setPosition(seed2D);
+				if (filterRandomAccess.get().get())
+				{
+					FloodFill.fill(
+							relevantBackgroundSlice,
+							relevantAccessTracker,
+							new Point(seed2D),
+							new UnsignedLongType(fillValue),
+							new DiamondShape(1)
+						);
+					Arrays.setAll(min, d -> Math.min(accessTracker.getMin()[d], min[d]));
+					Arrays.setAll(max, d -> Math.max(accessTracker.getMax()[d], max[d]));
+				}
 			}
 			affectedInterval = new FinalInterval(min, max);
 		}

From c2edfaad51f260be1b74ceb29de5c9805b833da0 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 7 Jun 2019 16:33:29 -0400
Subject: [PATCH 71/84] pick fill depth based on orientation to optimize fill
 in orthogonal view

---
 .../control/ShapeInterpolationMode.java       | 20 ++++++++++++++++---
 1 file changed, 17 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 030454914..b1d06be31 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -16,6 +16,7 @@
 import org.janelia.saalfeldlab.paintera.control.actions.NavigationActionType;
 import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment;
 import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D;
+import org.janelia.saalfeldlab.paintera.control.paint.PaintUtils;
 import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds;
 import org.janelia.saalfeldlab.paintera.data.DataSource;
 import org.janelia.saalfeldlab.paintera.data.PredicateDataSource.PredicateConverter;
@@ -150,6 +151,8 @@ private static final class SectionInfo
 
 	private static final double FILL_DEPTH = 2.0;
 
+	private static final double FILL_DEPTH_ORTHOGONAL = 1.0;
+
 	private static final int MASK_SCALE_LEVEL = 0;
 
 	private static final int SHAPE_INTERPOLATION_SCALE_LEVEL = MASK_SCALE_LEVEL;
@@ -806,8 +809,11 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 	 */
 	private Pair<Long, Interval> runFloodFillToSelect(final double x, final double y)
 	{
-		final Interval affectedInterval = FloodFill2D.fillMaskAt(x, y, activeViewer, mask, source, assignment, ++currentFillValue, FILL_DEPTH);
-		return new ValuePair<>(currentFillValue, affectedInterval);
+		final long fillValue = ++currentFillValue;
+		final double fillDepth = determineFillDepth();
+		LOG.debug("Flood-filling to select object: fill value={}, depth={}", fillValue, fillDepth);
+		final Interval affectedInterval = FloodFill2D.fillMaskAt(x, y, activeViewer, mask, source, assignment, fillValue, fillDepth);
+		return new ValuePair<>(fillValue, affectedInterval);
 	}
 
 	/**
@@ -826,10 +832,18 @@ private long runFloodFillToDeselect(final double x, final double y)
 				(in, out) -> out.set(in.getIntegerLong() == maskValue),
 				new BoolType()
 			);
-		FloodFill2D.fillMaskAt(x, y, activeViewer, mask, predicate, getMaskTransform(), Label.BACKGROUND, FILL_DEPTH);
+		final double fillDepth = determineFillDepth();
+		LOG.debug("Flood-filling to deselect object: old value={}, depth={}", maskValue, fillDepth);
+		FloodFill2D.fillMaskAt(x, y, activeViewer, mask, predicate, getMaskTransform(), Label.BACKGROUND, determineFillDepth());
 		return maskValue;
 	}
 
+	private double determineFillDepth()
+	{
+		final int normalAxis = PaintUtils.labelAxisCorrespondingToViewerAxis(getMaskTransform(), getDisplayTransform(), 2);
+		return normalAxis < 0 ? FILL_DEPTH : FILL_DEPTH_ORTHOGONAL;
+	}
+
 	private UnsignedLongType getMaskValue(final double x, final double y)
 	{
 		final RealPoint sourcePos = getSourceCoordinates(x, y);

From b8af372ee7104337cb8fb574416d403cd464d63b Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 7 Jun 2019 17:09:46 -0400
Subject: [PATCH 72/84] run restricted 3d flood-fill when requested to fill in
 2d with depth > 1

---
 .../paintera/control/paint/FloodFill2D.java   | 70 +++++++++++--------
 1 file changed, 41 insertions(+), 29 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
index e6ecb9e33..dd3f26bf0 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
@@ -41,6 +41,7 @@
 import net.imglib2.type.numeric.IntegerType;
 import net.imglib2.type.numeric.integer.UnsignedLongType;
 import net.imglib2.util.AccessBoxRandomAccessibleOnGet;
+import net.imglib2.util.Intervals;
 import net.imglib2.view.MixedTransformView;
 import net.imglib2.view.Views;
 
@@ -270,8 +271,6 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 				Views.extendValue(mask.mask, new UnsignedLongType(fillValue)));
 		accessTracker.initAccessBox();
 
-		final Interval affectedInterval;
-
 		if (fillNormalAxisInLabelCoordinateSystem < 0)
 		{
 			FloodFillTransformedPlane.fill(
@@ -285,7 +284,6 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 					new RealPoint(x, y, 0),
 					fillValue
 				);
-			affectedInterval = new FinalInterval(accessTracker.getMin(), accessTracker.getMax());
 		}
 		else
 		{
@@ -295,44 +293,58 @@ public static <T extends IntegerType<T>> Interval fillMaskAt(
 			         );
 			final long slicePos  = Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem));
 			final long numSlices = Math.max((long) Math.ceil(fillDepth) - 1, 0);
-			final long[] seed2D = {
-					Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem == 0 ? 1 : 0)),
-					Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem != 2 ? 2 : 1))
-			};
-			accessTracker.initAccessBox();
-			final long[] min = {Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE};
-			final long[] max = {Long.MIN_VALUE, Long.MIN_VALUE, Long.MIN_VALUE};
-			for (long i = slicePos - numSlices; i <= slicePos + numSlices; ++i)
+			if (numSlices == 0)
 			{
+				// fill only within the given slice, run 2D flood-fill
+				final long[] seed2D = {
+						Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem == 0 ? 1 : 0)),
+						Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem != 2 ? 2 : 1))
+				};
 				final MixedTransformView<BoolType> relevantBackgroundSlice = Views.hyperSlice(
 						extendedFilter,
 						fillNormalAxisInLabelCoordinateSystem,
-						i
+						slicePos
 					);
 				final MixedTransformView<UnsignedLongType> relevantAccessTracker = Views.hyperSlice(
 						accessTracker,
 						fillNormalAxisInLabelCoordinateSystem,
-						i
+						slicePos
+					);
+				FloodFill.fill(
+						relevantBackgroundSlice,
+						relevantAccessTracker,
+						new Point(seed2D),
+						new UnsignedLongType(fillValue),
+						new DiamondShape(1)
+					);
+			}
+			else
+			{
+				// fill a range around the given slice, run 3D flood-fill restricted by this range
+				final long[] seed3D = new long[3];
+				Arrays.setAll(seed3D, d -> Math.round(pos.getDoublePosition(d)));
+
+				final long[] rangeMin = Intervals.minAsLongArray(filter);
+				final long[] rangeMax = Intervals.maxAsLongArray(filter);
+				rangeMin[fillNormalAxisInLabelCoordinateSystem] = slicePos - numSlices;
+				rangeMax[fillNormalAxisInLabelCoordinateSystem] = slicePos + numSlices;
+				final Interval range = new FinalInterval(rangeMin, rangeMax);
+
+				final RandomAccessible<BoolType> extendedBackgroundRange = Views.extendValue(
+						Views.interval(extendedFilter, range),
+						new BoolType(false)
+					);
+				FloodFill.fill(
+						extendedBackgroundRange,
+						accessTracker,
+						new Point(seed3D),
+						new UnsignedLongType(fillValue),
+						new DiamondShape(1)
 					);
-
-				final RandomAccess<BoolType> filterRandomAccess = relevantBackgroundSlice.randomAccess();
-				filterRandomAccess.setPosition(seed2D);
-				if (filterRandomAccess.get().get())
-				{
-					FloodFill.fill(
-							relevantBackgroundSlice,
-							relevantAccessTracker,
-							new Point(seed2D),
-							new UnsignedLongType(fillValue),
-							new DiamondShape(1)
-						);
-					Arrays.setAll(min, d -> Math.min(accessTracker.getMin()[d], min[d]));
-					Arrays.setAll(max, d -> Math.max(accessTracker.getMax()[d], max[d]));
-				}
 			}
-			affectedInterval = new FinalInterval(min, max);
 		}
 
+		final Interval affectedInterval = new FinalInterval(accessTracker.getMin(), accessTracker.getMax());
 		return affectedInterval;
 	}
 

From 1b4e778172db95e735ab6547d6042d4a403374d9 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 7 Jun 2019 17:28:54 -0400
Subject: [PATCH 73/84] fix NPE when deselecting all in edit mode and switching
 to other section

---
 .../paintera/control/ShapeInterpolationMode.java           | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index b1d06be31..bb81004a9 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -451,8 +451,13 @@ private void editSelection(final PainteraBaseView paintera, final ActiveSection
 
 		if (activeSection.get() == section)
 			return;
-		else if (activeSection.get() != null)
+
+		if (activeSection.get() != null)
+		{
+			if (selectedObjects.isEmpty())
+				return;
 			fixSelection(paintera);
+		}
 
 		final ObjectProperty<SectionInfo> sectionInfoPropertyToEdit = section == ActiveSection.First ? sectionInfo1 : sectionInfo2;
 		final SectionInfo sectionInfo = sectionInfoPropertyToEdit.get();

From 5ae2528f94fb07dc0d90d14ab04ae65db7351857 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 7 Jun 2019 17:38:35 -0400
Subject: [PATCH 74/84] allow to edit #1 immediately after selecting #2

---
 .../paintera/control/ShapeInterpolationMode.java  | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index bb81004a9..1bc82f27d 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -258,8 +258,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				EventFX.KEY_PRESSED(
 						"edit selection 1",
 						e -> {e.consume(); editSelection(paintera, ActiveSection.First);},
-						e -> (modeState.get() == ModeState.Interpolate || modeState.get() == ModeState.Preview || modeState.get() == ModeState.Edit) &&
-							keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT1)
+						e -> keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT1)
 					)
 			);
 		filter.addEventHandler(
@@ -267,8 +266,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				EventFX.KEY_PRESSED(
 						"edit selection 2",
 						e -> {e.consume(); editSelection(paintera, ActiveSection.Second);},
-						e -> (modeState.get() == ModeState.Interpolate || modeState.get() == ModeState.Preview || modeState.get() == ModeState.Edit) &&
-							keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT2)
+						e -> keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT2)
 					)
 			);
 
@@ -462,6 +460,12 @@ private void editSelection(final PainteraBaseView paintera, final ActiveSection
 		final ObjectProperty<SectionInfo> sectionInfoPropertyToEdit = section == ActiveSection.First ? sectionInfo1 : sectionInfo2;
 		final SectionInfo sectionInfo = sectionInfoPropertyToEdit.get();
 
+		if (sectionInfo == null)
+		{
+			advanceMode(paintera);
+			return;
+		}
+
 		resetMask();
 		try {
 			source.setMask(sectionInfo.mask, FOREGROUND_CHECK);
@@ -475,9 +479,10 @@ private void editSelection(final PainteraBaseView paintera, final ActiveSection
 		selectedObjects.clear();
 		selectedObjects.putAll(sectionInfo.selectedObjects);
 
+		modeState.set(sectionInfo1.get() != null && sectionInfo2.get() != null ? ModeState.Edit : ModeState.Select);
+
 		sectionInfoPropertyToEdit.set(null);
 		activeSection.set(section);
-		modeState.set(ModeState.Edit);
 	}
 
 	private void applyMask(final PainteraBaseView paintera)

From 12ec0704439c621c2b8010142f343f38374f1fe0 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 7 Jun 2019 17:44:38 -0400
Subject: [PATCH 75/84] remove superfluous edit state of shape interpolation
 mode

---
 .../paintera/control/ShapeInterpolationMode.java  | 15 +++++++--------
 .../paintera/state/LabelSourceState.java          |  3 ---
 2 files changed, 7 insertions(+), 11 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 1bc82f27d..e4c63bef4 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -89,8 +89,7 @@ public static enum ModeState
 	{
 		Select,
 		Interpolate,
-		Preview,
-		Edit
+		Preview
 	}
 
 	public static enum ActiveSection
@@ -273,12 +272,12 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"select object in current section",
 				e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), true);},
-				e -> (modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
+				e -> modeState.get() == ModeState.Select && e.isPrimaryButtonDown() && keyTracker.noKeysActive())
 			.handler());
 		filter.addEventHandler(MouseEvent.ANY, new MouseClickFX(
 				"toggle object in current section",
 				e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), false);},
-				e -> (modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) &&
+				e -> modeState.get() == ModeState.Select &&
 					((e.isSecondaryButtonDown() && keyTracker.noKeysActive()) ||
 					(e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL))))
 			.handler());
@@ -302,7 +301,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe
 		allowedActionsBuilder.add(NavigationActionType.Drag, NavigationActionType.Zoom, MenuActionType.ToggleMaximizeViewer);
 		allowedActionsBuilder.add(NavigationActionType.Scroll, () -> {
 			// allow to scroll through sections, but fix the selection first if the object selection is not empty
-			if ((modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) && !selectedObjects.isEmpty())
+			if (modeState.get() == ModeState.Select && !selectedObjects.isEmpty())
 			{
 				fixSelection(paintera);
 				advanceMode(paintera);
@@ -427,7 +426,7 @@ private void fixSelection(final PainteraBaseView paintera)
 
 	private void advanceMode(final PainteraBaseView paintera)
 	{
-		if (modeState.get() != ModeState.Edit && sectionInfo2.get() == null)
+		if (sectionInfo1.get() == null || sectionInfo2.get() == null)
 		{
 			// let the user now select the second section
 			activeSection.set(ActiveSection.Second);
@@ -479,10 +478,10 @@ private void editSelection(final PainteraBaseView paintera, final ActiveSection
 		selectedObjects.clear();
 		selectedObjects.putAll(sectionInfo.selectedObjects);
 
-		modeState.set(sectionInfo1.get() != null && sectionInfo2.get() != null ? ModeState.Edit : ModeState.Select);
-
 		sectionInfoPropertyToEdit.set(null);
 		activeSection.set(section);
+
+		modeState.set(ModeState.Select);
 	}
 
 	private void applyMask(final PainteraBaseView paintera)
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
index 432935abf..e90ef7cb6 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
@@ -618,9 +618,6 @@ private HBox createDisplayStatus()
 					case Select:
 						statusTextProperty().set("Select #" + activeSection);
 						break;
-					case Edit:
-						statusTextProperty().set("Edit #" + activeSection);
-						break;
 					case Interpolate:
 						statusTextProperty().set("Interpolating");
 						break;

From aa8efa81eab9ccf9a7bb7545880c4dfadd320b68 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Fri, 7 Jun 2019 18:48:30 -0400
Subject: [PATCH 76/84] consume events when rotating using hotkeys to avoid
 triggering side panel

---
 .../org/janelia/saalfeldlab/paintera/control/Navigation.java     | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java
index b117deb53..3dd7329f8 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java
@@ -548,6 +548,7 @@ private static final EventFX<KeyEvent> keyRotationHandler(
 				event -> {
 					if (allowRotations.getAsBoolean())
 					{
+						event.consume();
 						rotate.rotate(rotationCenterX.getAsDouble(), rotationCenterY.getAsDouble());
 					}
 				},

From 6ee731a8530fbe1fd264f831186038293e1157f6 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Mon, 17 Jun 2019 13:25:40 -0400
Subject: [PATCH 77/84] allow to commit interpolated shape without going
 through preview step

---
 .../control/ShapeInterpolationMode.java       | 47 ++++++++++++++++---
 1 file changed, 40 insertions(+), 7 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index e4c63bef4..b27af04a9 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -187,6 +187,7 @@ private static final class SectionInfo
 	private final ObjectProperty<ActiveSection> activeSection = new SimpleObjectProperty<>();
 
 	private Thread workerThread;
+	private Runnable onInterpolationFinished;
 	private Pair<RealRandomAccessible<UnsignedLongType>, RealRandomAccessible<VolatileUnsignedLongType>> interpolatedMaskImgs;
 
 	public ShapeInterpolationMode(
@@ -248,8 +249,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke
 				EventFX.KEY_PRESSED(
 						"apply mask",
 						e -> {e.consume(); applyMask(paintera);},
-						e -> modeState.get() == ModeState.Preview &&
-							keyTracker.areOnlyTheseKeysDown(KeyCode.ENTER)
+						e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ENTER)
 					)
 			);
 		filter.addEventHandler(
@@ -362,6 +362,7 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed)
 		mask = null;
 
 		workerThread = null;
+		onInterpolationFinished = null;
 		interpolatedMaskImgs = null;
 		lastSelectedId = Label.INVALID;
 		lastActiveIds = null;
@@ -438,6 +439,8 @@ private void advanceMode(final PainteraBaseView paintera)
 		{
 			// both sections are ready, run interpolation
 			activeSection.set(null);
+			modeState.set(ModeState.Interpolate);
+			onInterpolationFinished = () -> modeState.set(ModeState.Preview);
 			interpolateBetweenSections(paintera);
 		}
 	}
@@ -486,6 +489,30 @@ private void editSelection(final PainteraBaseView paintera, final ActiveSection
 
 	private void applyMask(final PainteraBaseView paintera)
 	{
+		if (modeState.get() == ModeState.Select)
+		{
+			final boolean firstSectionReady  = sectionInfo1.get() != null || (activeSection.get() == ActiveSection.First  && !selectedObjects.isEmpty());
+			final boolean secondSectionReady = sectionInfo2.get() != null || (activeSection.get() == ActiveSection.Second && !selectedObjects.isEmpty());
+			if (!firstSectionReady || !secondSectionReady)
+				return;
+
+			fixSelection(paintera);
+			advanceMode(paintera);
+		}
+
+		if (modeState.get() == ModeState.Interpolate)
+		{
+			// wait until the interpolation is done
+			try {
+				workerThread.join();
+			} catch (final InterruptedException e) {
+				e.printStackTrace();
+			};
+			runOnInterpolationFinished();
+		}
+
+		assert modeState.get() == ModeState.Preview;
+
 		final Interval sectionsUnionSourceInterval = Intervals.union(
 				sectionInfo1.get().sourceBoundingBox,
 				sectionInfo2.get().sourceBoundingBox
@@ -551,8 +578,6 @@ private SectionInfo createSectionInfo(final PainteraBaseView paintera)
 	@SuppressWarnings("unchecked")
 	private void interpolateBetweenSections(final PainteraBaseView paintera)
 	{
-		modeState.set(ModeState.Interpolate);
-
 		workerThread = new Thread(() ->
 		{
 			final SectionInfo[] sectionInfoPair = {sectionInfo1.get(), sectionInfo2.get()};
@@ -641,13 +666,20 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 				LOG.error("Label source already has an active mask");
 			}
 
-			InvokeOnJavaFXApplicationThread.invoke(() -> {
-				modeState.set(ModeState.Preview);
-			});
+			InvokeOnJavaFXApplicationThread.invoke(this::runOnInterpolationFinished);
 		});
 		workerThread.start();
 	}
 
+	private void runOnInterpolationFinished()
+	{
+		if (onInterpolationFinished != null)
+		{
+			onInterpolationFinished.run();
+			onInterpolationFinished = null;
+		}
+	}
+
 	private void interruptInterpolation()
 	{
 		if (workerThread != null)
@@ -659,6 +691,7 @@ private void interruptInterpolation()
 				e.printStackTrace();
 			}
 		}
+		onInterpolationFinished = null;
 	}
 
 	private static <R extends RealType<R> & NativeType<R>, B extends BooleanType<B>> void computeSignedDistanceTransform(

From 1bb64aff5d062ec1ea616047b7f3567547f0f9ff Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Mon, 17 Jun 2019 13:55:39 -0400
Subject: [PATCH 78/84] allow to interrupt 3d floodfill operation by pressing
 Esc

---
 .../paintera/state/LabelSourceState.java      | 22 ++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
index e90ef7cb6..9e4fa27f9 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
@@ -469,7 +469,27 @@ public EventHandler<Event> stateSpecificGlobalEventHandler(final PainteraBaseVie
 		final DelegateEventHandlers.AnyHandler handler = DelegateEventHandlers.handleAny();
 		handler.addEventHandler(
 				KeyEvent.KEY_PRESSED,
-				EventFX.KEY_PRESSED("refresh meshes", e -> {LOG.debug("Key event triggered refresh meshes"); refreshMeshes();}, e -> keyTracker.areOnlyTheseKeysDown(KeyCode.R)));
+				EventFX.KEY_PRESSED(
+						"refresh meshes",
+						e -> {
+							e.consume();
+							LOG.debug("Key event triggered refresh meshes");
+							refreshMeshes();
+						},
+						e -> keyTracker.areOnlyTheseKeysDown(KeyCode.R)
+			));
+		handler.addEventHandler(
+				KeyEvent.KEY_PRESSED,
+				EventFX.KEY_PRESSED(
+						"cancel 3d floodfill",
+						e -> {
+							e.consume();
+							final FloodFillState state = floodFillState.get();
+							if (state != null && state.interrupt != null)
+								state.interrupt.run();
+						},
+						e -> floodFillState.get() != null && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE)
+			));
 		return handler;
 	}
 

From 659e57205d52128132b822a11e228a4217a98d59 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Mon, 17 Jun 2019 14:41:01 -0400
Subject: [PATCH 79/84] request repaint in all viewers when running 2d/3d
 floodfill

---
 .../control/ShapeInterpolationMode.java       | 29 +++++++------------
 .../state/LabelSourceStatePaintHandler.java   |  6 ++--
 2 files changed, 14 insertions(+), 21 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index b27af04a9..9928ec12a 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -367,8 +367,7 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed)
 		lastSelectedId = Label.INVALID;
 		lastActiveIds = null;
 
-		for (final ViewerPanelFX viewer : getViewerPanels(paintera))
-			viewer.requestRepaint();
+		paintera.orthogonalViews().requestRepaint();
 		activeViewer = null;
 	}
 
@@ -397,7 +396,13 @@ private void resetMask()
 
 	private void setDisableOtherViewers(final PainteraBaseView paintera, final boolean disable)
 	{
-		for (final ViewerPanelFX viewer : getViewerPanels(paintera))
+		final ViewerPanelFX[] viewers = {
+				paintera.orthogonalViews().topLeft().viewer(),
+				paintera.orthogonalViews().topRight().viewer(),
+				paintera.orthogonalViews().bottomLeft().viewer()
+		};
+
+		for (final ViewerPanelFX viewer : viewers)
 		{
 			if (viewer != activeViewer)
 			{
@@ -432,8 +437,7 @@ private void advanceMode(final PainteraBaseView paintera)
 			// let the user now select the second section
 			activeSection.set(ActiveSection.Second);
 			resetMask();
-			for (final ViewerPanelFX viewer : getViewerPanels(paintera))
-				viewer.requestRepaint();
+			paintera.orthogonalViews().requestRepaint();
 		}
 		else
 		{
@@ -658,8 +662,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 					interpolatedMaskImgs = new ValuePair<>(interpolatedShapeMask, volatileInterpolatedShapeMask);
 				}
 
-				for (final ViewerPanelFX viewer : getViewerPanels(paintera))
-					viewer.requestRepaint();
+				paintera.orthogonalViews().requestRepaint();
 			}
 			catch (final MaskInUse e)
 			{
@@ -838,8 +841,7 @@ private void selectObject(final PainteraBaseView paintera, final double x, final
 			resetMask();
 		}
 
-		for (final ViewerPanelFX viewer : getViewerPanels(paintera))
-			viewer.requestRepaint();
+		paintera.orthogonalViews().requestRepaint();
 	}
 
 	/**
@@ -974,13 +976,4 @@ private double[] getDisplayCoordinates(final RealPoint sourcePos)
 		assert Util.isApproxEqual(displayPos.getDoublePosition(2), 0.0, 1e-10);
 		return new double[] {displayPos.getDoublePosition(0), displayPos.getDoublePosition(1)};
 	}
-
-	private static ViewerPanelFX[] getViewerPanels(final PainteraBaseView paintera)
-	{
-		return new ViewerPanelFX[] {
-				paintera.orthogonalViews().topLeft().viewer(),
-				paintera.orthogonalViews().topRight().viewer(),
-				paintera.orthogonalViews().bottomLeft().viewer()
-			};
-	}
 }
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java
index 43d33b850..4de1b5502 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java
@@ -113,14 +113,14 @@ private EventHandler<Event> makeHandler(final PainteraBaseView paintera, final K
 
 		painters.put(t, paint2D);
 
-		final FloodFill fill = new FloodFill(t, sourceInfo, t::requestRepaint);
-		final FloodFill2D fill2D = new FloodFill2D(t, sourceInfo, t::requestRepaint);
+		final FloodFill fill = new FloodFill(t, sourceInfo, paintera.orthogonalViews()::requestRepaint);
+		final FloodFill2D fill2D = new FloodFill2D(t, sourceInfo, paintera.orthogonalViews()::requestRepaint);
 		fill2D.fillDepthProperty().bindBidirectional(this.brushDepth);
 		final Fill2DOverlay fill2DOverlay = new Fill2DOverlay(t);
 		fill2DOverlay.brushDepthProperty().bindBidirectional(this.brushDepth);
 		final FillOverlay fillOverlay = new FillOverlay(t);
 
-		final RestrictPainting restrictor = new RestrictPainting(t, sourceInfo, t::requestRepaint);
+		final RestrictPainting restrictor = new RestrictPainting(t, sourceInfo, paintera.orthogonalViews()::requestRepaint);
 
 		// brush
 		handler.addEventHandler(KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED(

From fe9b35f68b93c4d08f2b64c3221d2f84de7de563 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Mon, 17 Jun 2019 15:44:18 -0400
Subject: [PATCH 80/84] make sidebar wider and adjust mesh settings layout to
 fit all nodes

---
 .../paintera/BorderPaneWithStatusBars.java    |  2 +-
 .../paintera/ui/source/mesh/MeshPane.java     | 52 +++++++++++--------
 2 files changed, 32 insertions(+), 22 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java
index 4e91ee554..b10ed4ccd 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java
@@ -299,7 +299,7 @@ else if (newv.nameProperty().get() != null)
 		this.sideBar.setHbarPolicy(ScrollBarPolicy.NEVER);
 		this.sideBar.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
 		this.sideBar.setVisible(true);
-		this.sideBar.prefWidthProperty().set(250);
+		this.sideBar.prefWidthProperty().set(280);
 		sourceTabs.widthProperty().bind(sideBar.prefWidthProperty());
 		settingsContents.prefWidthProperty().bind(sideBar.prefWidthProperty());
 
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshPane.java b/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshPane.java
index d4243e97f..d26963cfb 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshPane.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshPane.java
@@ -13,6 +13,7 @@
 import javafx.collections.ListChangeListener;
 import javafx.collections.ObservableList;
 import javafx.collections.ObservableMap;
+import javafx.geometry.HPos;
 import javafx.scene.Node;
 import javafx.scene.control.Button;
 import javafx.scene.control.CheckBox;
@@ -264,66 +265,75 @@ public static int populateGridWithMeshSettings(
 	{
 		int row = initialRow;
 
-		final double textFieldWidth = 95;
+		final double textFieldWidth = 55;
+		final double choiceWidth = 95;
 
-		contents.add(labelWithToolTip("Opacity "), 0, row);
+		contents.add(labelWithToolTip("Opacity"), 0, row);
 		contents.add(opacitySlider.slider(), 1, row);
-		contents.add(opacitySlider.textField(), 2, row);
+		GridPane.setColumnSpan(opacitySlider.slider(), 2);
+		contents.add(opacitySlider.textField(), 3, row);
 		opacitySlider.slider().setShowTickLabels(true);
 		opacitySlider.slider().setTooltip(new Tooltip("Mesh opacity."));
-		opacitySlider.textField().setMinWidth(textFieldWidth);
-		opacitySlider.textField().setMaxWidth(textFieldWidth);
+		opacitySlider.textField().setPrefWidth(textFieldWidth);
 		GridPane.setHgrow(opacitySlider.slider(), Priority.ALWAYS);
 		++row;
 
 		contents.add(labelWithToolTip("Scale"), 0, row);
 		contents.add(scaleSlider.slider(), 1, row);
-		contents.add(scaleSlider.textField(), 2, row);
+		GridPane.setColumnSpan(scaleSlider.slider(), 2);
+		contents.add(scaleSlider.textField(), 3, row);
 		scaleSlider.slider().setShowTickLabels(true);
 		scaleSlider.slider().setTooltip(new Tooltip("Scale level."));
-		scaleSlider.textField().setMinWidth(textFieldWidth);
-		scaleSlider.textField().setMaxWidth(textFieldWidth);
+		scaleSlider.textField().setPrefWidth(textFieldWidth);
 		GridPane.setHgrow(scaleSlider.slider(), Priority.ALWAYS);
 		++row;
 
 		contents.add(labelWithToolTip("Lambda"), 0, row);
 		contents.add(smoothingLambdaSlider.slider(), 1, row);
-		contents.add(smoothingLambdaSlider.textField(), 2, row);
+		GridPane.setColumnSpan(smoothingLambdaSlider.slider(), 2);
+		contents.add(smoothingLambdaSlider.textField(), 3, row);
 		smoothingLambdaSlider.slider().setShowTickLabels(true);
 		smoothingLambdaSlider.slider().setTooltip(new Tooltip("Smoothing lambda."));
-		smoothingLambdaSlider.textField().setMinWidth(textFieldWidth);
-		smoothingLambdaSlider.textField().setMaxWidth(textFieldWidth);
+		smoothingLambdaSlider.textField().setPrefWidth(textFieldWidth);
 		GridPane.setHgrow(smoothingLambdaSlider.slider(), Priority.ALWAYS);
 		++row;
 
 		contents.add(labelWithToolTip("Iterations"), 0, row);
 		contents.add(smoothingIterationsSlider.slider(), 1, row);
-		contents.add(smoothingIterationsSlider.textField(), 2, row);
+		GridPane.setColumnSpan(smoothingIterationsSlider.slider(), 2);
+		contents.add(smoothingIterationsSlider.textField(), 3, row);
 		smoothingIterationsSlider.slider().setShowTickLabels(true);
 		smoothingIterationsSlider.slider().setTooltip(new Tooltip("Smoothing iterations."));
-		smoothingIterationsSlider.textField().setMinWidth(textFieldWidth);
-		smoothingIterationsSlider.textField().setMaxWidth(textFieldWidth);
+		smoothingIterationsSlider.textField().setPrefWidth(textFieldWidth);
 		GridPane.setHgrow(smoothingIterationsSlider.slider(), Priority.ALWAYS);
 		++row;
 
 		contents.add(labelWithToolTip("Inflate"), 0, row);
 		contents.add(inflateSlider.slider(), 1, row);
-		contents.add(inflateSlider.textField(), 2, row);
+		GridPane.setColumnSpan(inflateSlider.slider(), 2);
+		contents.add(inflateSlider.textField(), 3, row);
 		inflateSlider.slider().setShowTickLabels(true);
 		inflateSlider.slider().setTooltip(new Tooltip("Inflate meshes by factor"));
-		inflateSlider.textField().setMinWidth(textFieldWidth);
-		inflateSlider.textField().setMaxWidth(textFieldWidth);
+		inflateSlider.textField().setPrefWidth(textFieldWidth);
 		GridPane.setHgrow(inflateSlider.slider(), Priority.ALWAYS);
 		++row;
 
-		contents.add(labelWithToolTip("Draw Mode"), 0, row);
+		final Node drawModeLabel = labelWithToolTip("Draw Mode");
+		contents.add(drawModeLabel, 0, row);
+		GridPane.setColumnSpan(drawModeLabel, 2);
 		contents.add(drawModeChoice, 2, row);
-		drawModeChoice.setMaxWidth(textFieldWidth);
+		GridPane.setColumnSpan(drawModeChoice, 2);
+		GridPane.setHalignment(drawModeChoice, HPos.RIGHT);
+		drawModeChoice.setPrefWidth(choiceWidth);
 		++row;
 
-		contents.add(labelWithToolTip("CullFace "), 0, row);
+		final Node cullFaceLabel = labelWithToolTip("Cull Face");
+		contents.add(cullFaceLabel, 0, row);
+		GridPane.setColumnSpan(cullFaceLabel, 2);
 		contents.add(cullFaceChoice, 2, row);
-		cullFaceChoice.setMaxWidth(textFieldWidth);
+		GridPane.setColumnSpan(cullFaceChoice, 2);
+		GridPane.setHalignment(cullFaceChoice, HPos.RIGHT);
+		cullFaceChoice.setPrefWidth(choiceWidth);
 		++row;
 
 		return row;

From 9ee43c4071170499b7a431f33224f6f1596a68f4 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 18 Jun 2019 14:48:25 -0400
Subject: [PATCH 81/84] do not make an extra copy when computing distance
 transform on sections

---
 .../paintera/control/ShapeInterpolationMode.java   | 14 ++++----------
 1 file changed, 4 insertions(+), 10 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 9928ec12a..4cf7cca0c 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -591,7 +591,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 					sectionInfoPair[1].sourceBoundingBox
 				);
 
-			final Interval[] displaySectionIntervalPair = new Interval[2];
+			// get the two sections as 2D images
 			final RandomAccessibleInterval<UnsignedLongType>[] sectionPair = new RandomAccessibleInterval[2];
 			for (int i = 0; i < 2; ++i)
 			{
@@ -602,13 +602,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 						affectedUnionSourceInterval,
 						sectionInfoPair[i].selectedObjects
 					);
-				final RandomAccessibleInterval<UnsignedLongType> section = getTransformedMaskSection(newSectionInfo);
-				displaySectionIntervalPair[i] = new FinalInterval(section);
-				sectionPair[i] = new ArrayImgFactory<>(new UnsignedLongType()).create(section);
-				final Cursor<UnsignedLongType> srcCursor = Views.flatIterable(section).cursor();
-				final Cursor<UnsignedLongType> dstCursor = Views.flatIterable(sectionPair[i]).cursor();
-				while (dstCursor.hasNext() || srcCursor.hasNext())
-					dstCursor.next().set(srcCursor.next());
+				sectionPair[i] = getTransformedMaskSection(newSectionInfo);
 			}
 
 			// compute distance transform on both sections
@@ -619,13 +613,13 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 					return;
 				distanceTransformPair[i] = new ArrayImgFactory<>(new FloatType()).create(sectionPair[i]);
 				final RandomAccessibleInterval<BoolType> binarySection = Converters.convert(sectionPair[i], new PredicateConverter<>(FOREGROUND_CHECK), new BoolType());
-				computeSignedDistanceTransform(binarySection, distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN);
+				computeSignedDistanceTransform(Views.zeroMin(binarySection), distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN);
 			}
 
 			final double distanceBetweenSections = computeDistanceBetweenSections(sectionInfoPair[0], sectionInfoPair[1]);
 			final AffineTransform3D transformToSource = new AffineTransform3D();
 			transformToSource
-				.preConcatenate(new Translation3D(displaySectionIntervalPair[0].min(0), displaySectionIntervalPair[0].min(1), 0))
+				.preConcatenate(new Translation3D(sectionPair[0].min(0), sectionPair[0].min(1), 0))
 				.preConcatenate(sectionInfoPair[0].sourceToDisplayTransform.inverse());
 
 			final RealRandomAccessible<UnsignedLongType> interpolatedShapeMask = getInterpolatedDistanceTransformMask(

From 9bdd3581c686d83e129cbef205b38df09ec65be4 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 18 Jun 2019 14:51:09 -0400
Subject: [PATCH 82/84] narrow bounding box of selected shape before computing
 distance transform

---
 .../control/ShapeInterpolationMode.java       | 34 ++++++++++++++++++-
 1 file changed, 33 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index 4cf7cca0c..b86bc0c2e 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -605,12 +605,44 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 				sectionPair[i] = getTransformedMaskSection(newSectionInfo);
 			}
 
+			// Narrow the bounding box of the two sections in the display space.
+			// The initial bounding box may be larger because of transforming the source bounding box into the display space and then taking the bounding box of that.
+			final Interval[] boundingBoxPair = new Interval[2];
+			for (int i = 0; i < 2; ++i)
+			{
+				if (Thread.currentThread().isInterrupted())
+					return;
+
+				final long[] min = new long[2], max = new long[2], position = new long[2];
+				Arrays.fill(min, Long.MAX_VALUE);
+				Arrays.fill(max, Long.MIN_VALUE);
+				final Cursor<UnsignedLongType> cursor = Views.iterable(sectionPair[i]).localizingCursor();
+				while (cursor.hasNext())
+				{
+					if (FOREGROUND_CHECK.test(cursor.next()))
+					{
+						cursor.localize(position);
+						for (int d = 0; d < position.length; ++d)
+						{
+							min[d] = Math.min(min[d], position[d]);
+							max[d] = Math.max(max[d], position[d]);
+						}
+					}
+				}
+				boundingBoxPair[i] = new FinalInterval(min, max);
+			}
+			final Interval boundingBox = Intervals.union(boundingBoxPair[0], boundingBoxPair[1]);
+			LOG.debug("Narrowed the bounding box of the selected shape in both sections from {} to {}", Intervals.dimensionsAsLongArray(sectionPair[0]), Intervals.dimensionsAsLongArray(boundingBox));
+			for (int i = 0; i < 2; ++i)
+				sectionPair[i] = Views.interval(sectionPair[i], boundingBox);
+
 			// compute distance transform on both sections
 			final RandomAccessibleInterval<FloatType>[] distanceTransformPair = new RandomAccessibleInterval[2];
 			for (int i = 0; i < 2; ++i)
 			{
 				if (Thread.currentThread().isInterrupted())
 					return;
+
 				distanceTransformPair[i] = new ArrayImgFactory<>(new FloatType()).create(sectionPair[i]);
 				final RandomAccessibleInterval<BoolType> binarySection = Converters.convert(sectionPair[i], new PredicateConverter<>(FOREGROUND_CHECK), new BoolType());
 				computeSignedDistanceTransform(Views.zeroMin(binarySection), distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN);
@@ -619,7 +651,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 			final double distanceBetweenSections = computeDistanceBetweenSections(sectionInfoPair[0], sectionInfoPair[1]);
 			final AffineTransform3D transformToSource = new AffineTransform3D();
 			transformToSource
-				.preConcatenate(new Translation3D(sectionPair[0].min(0), sectionPair[0].min(1), 0))
+				.preConcatenate(new Translation3D(boundingBox.min(0), boundingBox.min(1), 0))
 				.preConcatenate(sectionInfoPair[0].sourceToDisplayTransform.inverse());
 
 			final RealRandomAccessible<UnsignedLongType> interpolatedShapeMask = getInterpolatedDistanceTransformMask(

From b6ad3e044a963e4c8045d04baed4857fc542aef5 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 18 Jun 2019 16:43:58 -0400
Subject: [PATCH 83/84] bugfix in bounding box adjustment: use
 translation-invariant transforms

---
 .../paintera/control/ShapeInterpolationMode.java       | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
index b86bc0c2e..b5fdd7cbb 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java
@@ -592,6 +592,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 				);
 
 			// get the two sections as 2D images
+			final Interval[] displaySectionIntervalPair = new Interval[2];
 			final RandomAccessibleInterval<UnsignedLongType>[] sectionPair = new RandomAccessibleInterval[2];
 			for (int i = 0; i < 2; ++i)
 			{
@@ -602,7 +603,9 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 						affectedUnionSourceInterval,
 						sectionInfoPair[i].selectedObjects
 					);
-				sectionPair[i] = getTransformedMaskSection(newSectionInfo);
+				final RandomAccessibleInterval<UnsignedLongType> section = getTransformedMaskSection(newSectionInfo);
+				displaySectionIntervalPair[i] = new FinalInterval(section);
+				sectionPair[i] = Views.zeroMin(section);
 			}
 
 			// Narrow the bounding box of the two sections in the display space.
@@ -634,7 +637,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 			final Interval boundingBox = Intervals.union(boundingBoxPair[0], boundingBoxPair[1]);
 			LOG.debug("Narrowed the bounding box of the selected shape in both sections from {} to {}", Intervals.dimensionsAsLongArray(sectionPair[0]), Intervals.dimensionsAsLongArray(boundingBox));
 			for (int i = 0; i < 2; ++i)
-				sectionPair[i] = Views.interval(sectionPair[i], boundingBox);
+				sectionPair[i] = Views.offsetInterval(sectionPair[i], boundingBox);
 
 			// compute distance transform on both sections
 			final RandomAccessibleInterval<FloatType>[] distanceTransformPair = new RandomAccessibleInterval[2];
@@ -645,13 +648,14 @@ private void interpolateBetweenSections(final PainteraBaseView paintera)
 
 				distanceTransformPair[i] = new ArrayImgFactory<>(new FloatType()).create(sectionPair[i]);
 				final RandomAccessibleInterval<BoolType> binarySection = Converters.convert(sectionPair[i], new PredicateConverter<>(FOREGROUND_CHECK), new BoolType());
-				computeSignedDistanceTransform(Views.zeroMin(binarySection), distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN);
+				computeSignedDistanceTransform(binarySection, distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN);
 			}
 
 			final double distanceBetweenSections = computeDistanceBetweenSections(sectionInfoPair[0], sectionInfoPair[1]);
 			final AffineTransform3D transformToSource = new AffineTransform3D();
 			transformToSource
 				.preConcatenate(new Translation3D(boundingBox.min(0), boundingBox.min(1), 0))
+				.preConcatenate(new Translation3D(displaySectionIntervalPair[0].min(0), displaySectionIntervalPair[0].min(1), 0))
 				.preConcatenate(sectionInfoPair[0].sourceToDisplayTransform.inverse());
 
 			final RealRandomAccessible<UnsignedLongType> interpolatedShapeMask = getInterpolatedDistanceTransformMask(

From 32ed7cbe2d1278342d1f81bf128801673071f728 Mon Sep 17 00:00:00 2001
From: Igor Pisarev <pisarevi@janelia.hhmi.org>
Date: Tue, 18 Jun 2019 16:45:26 -0400
Subject: [PATCH 84/84] show both segment and fragment ids in selected label
 indicator tooltip

---
 .../saalfeldlab/paintera/state/LabelSourceState.java     | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
index 9e4fa27f9..7bb4c25ab 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java
@@ -554,7 +554,13 @@ private HBox createDisplayStatus()
 						final Color currSelectedColor = Colors.toColor(colorStream.argb(lastSelectedLabelId));
 						lastSelectedLabelColorRect.setFill(currSelectedColor);
 						lastSelectedLabelColorRect.setVisible(true);
-						lastSelectedLabelColorRectTooltip.setText("Selected label ID: " + lastSelectedLabelId);
+
+						final StringBuilder activeIdText = new StringBuilder();
+						final long segmentId = assignment.getSegment(lastSelectedLabelId);
+						if (segmentId != lastSelectedLabelId)
+							activeIdText.append("Segment: " + segmentId).append(". ");
+						activeIdText.append("Fragment: " + lastSelectedLabelId);
+						lastSelectedLabelColorRectTooltip.setText(activeIdText.toString());
 					}
 				} else {
 					lastSelectedLabelColorRect.setVisible(false);
@@ -562,6 +568,7 @@ private HBox createDisplayStatus()
 			});
 		};
 		selectedIds.addListener(lastSelectedIdUpdater);
+		assignment.addListener(lastSelectedIdUpdater);
 
 		// add the same listener to the color stream (for example, the color should change when a new random seed value is set)
 		final AbstractHighlightingARGBStream colorStream = highlightingStreamConverter().getStream();