From 86faf1578fb3836b69ac69bc98854507cff716f0 Mon Sep 17 00:00:00 2001 From: Caleb Hulbert Date: Wed, 22 Jan 2025 16:53:09 -0500 Subject: [PATCH] refactor!: abstract mesh export with progress bar --- .../paintera/meshes/MeshExporter.java | 33 +++++-- .../control/actions/ExportSourceState.kt | 4 +- .../meshes/ui/MeshSettingsController.kt | 93 ++++++++++++++++--- ...tersectingSourceStatePreferencePaneNode.kt | 12 +-- .../state/LabelSourceStateMeshPaneNode.kt | 23 ++--- ...resholdingSourceStatePreferencePaneNode.kt | 12 +-- .../ui/dialogs/AnimatedProgressBarAlert.kt | 54 ++++++++++- 7 files changed, 175 insertions(+), 56 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshExporter.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshExporter.java index b688ab759..6ca42b5ef 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshExporter.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshExporter.java @@ -2,9 +2,11 @@ import gnu.trove.list.array.TFloatArrayList; import gnu.trove.list.array.TIntArrayList; +import javafx.beans.property.SimpleIntegerProperty; import net.imglib2.Interval; import net.imglib2.util.Intervals; import org.janelia.saalfeldlab.fx.ui.Exceptions; +import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; import org.janelia.saalfeldlab.paintera.meshes.managed.GetBlockListFor; import org.janelia.saalfeldlab.paintera.meshes.managed.GetMeshFor; import org.janelia.saalfeldlab.util.HashWrapper; @@ -14,24 +16,37 @@ import java.io.IOException; import java.lang.invoke.MethodHandles; import java.util.*; +import java.util.concurrent.CancellationException; public abstract class MeshExporter { + public final SimpleIntegerProperty blocksProcessed = new SimpleIntegerProperty(0); + boolean cancelled = false; + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); public void exportMesh( final GetBlockListFor getBlockListFor, final GetMeshFor getMeshFor, final MeshSettings[] meshSettings, - final T[] ids, + final List ids, final int scale, final String path) { - for (int i = 0; i < ids.length; i++) { - exportMesh(getBlockListFor, getMeshFor, meshSettings[i], ids[i], scale, path, i != 0); + for (int i = 0; i < ids.size(); i++) { + exportMesh(getBlockListFor, getMeshFor, meshSettings[i], ids.get(i), scale, path, i != 0); } } - public void exportMesh( + public void cancel() { + + cancelled = true; + } + + public boolean isCancelled() { + return cancelled; + } + + private void exportMesh( final GetBlockListFor getBlockListFor, final GetMeshFor getMeshFor, final MeshSettings meshSettings, @@ -53,8 +68,8 @@ public void exportMesh( // generate keys from blocks, scaleIndex, and id final List> keys = new ArrayList<>(); for (final Interval block : blocks) { - // ignoring simplification iterations parameter - // TODO consider smoothing parameters + if (cancelled) + throw new CancellationException("Mesh Export Cancelled"); keys.add(new ShapeKey<>( id, scaleIndex, @@ -72,8 +87,11 @@ public void exportMesh( final var normals = new TFloatArrayList(); final var indices = new TIntArrayList(); for (final ShapeKey key : keys) { + if (cancelled) + throw new CancellationException("Mesh Export Cancelled"); PainteraTriangleMesh verticesAndNormals; verticesAndNormals = getMeshFor.getMeshFor(key); + InvokeOnJavaFXApplicationThread.invoke(() -> blocksProcessed.set(blocksProcessed.get() + 1)); if (verticesAndNormals == null) { continue; } @@ -86,6 +104,9 @@ public void exportMesh( } } + if (cancelled) + throw new CancellationException("Mesh Export Cancelled"); + try { save(path, id.toString(), vertices.toArray(), normals.toArray(), indices.toArray(), append); } catch (final IOException e) { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/ExportSourceState.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/ExportSourceState.kt index e37877e5a..399f7a1a6 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/ExportSourceState.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/ExportSourceState.kt @@ -10,6 +10,7 @@ import net.imglib2.type.NativeType import net.imglib2.type.numeric.IntegerType import net.imglib2.type.numeric.integer.AbstractIntegerType import org.janelia.saalfeldlab.fx.extensions.createObservableBinding +import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.n5.DataType import org.janelia.saalfeldlab.n5.DatasetAttributes import org.janelia.saalfeldlab.n5.GsonKeyValueN5Reader @@ -32,7 +33,6 @@ import org.janelia.saalfeldlab.util.convertRAI import org.janelia.saalfeldlab.util.interval import org.janelia.saalfeldlab.util.n5.N5Helpers.MAX_ID_KEY import org.janelia.saalfeldlab.util.n5.N5Helpers.forEachBlockExists -import java.util.concurrent.atomic.AtomicInteger class ExportSourceState { @@ -127,7 +127,7 @@ class ExportSourceState { } progressUpdater?.apply { exportJob.invokeOnCompletion { finish() } - showAndStart() + InvokeOnJavaFXApplicationThread { showAndWait() } } } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/ui/MeshSettingsController.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/ui/MeshSettingsController.kt index 0c5863958..1a76267d7 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/ui/MeshSettingsController.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/ui/MeshSettingsController.kt @@ -1,5 +1,6 @@ package org.janelia.saalfeldlab.paintera.meshes.ui +import io.github.oshai.kotlinlogging.KotlinLogging import javafx.beans.property.* import javafx.collections.FXCollections import javafx.collections.ObservableList @@ -13,22 +14,33 @@ import javafx.scene.paint.Color import javafx.scene.shape.CullFace import javafx.scene.shape.DrawMode import javafx.stage.Modality +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.javafx.awaitPulse import net.imglib2.type.label.LabelMultisetType import org.janelia.saalfeldlab.fx.Buttons import org.janelia.saalfeldlab.fx.Labels import org.janelia.saalfeldlab.fx.extensions.TitledPaneExtensions +import org.janelia.saalfeldlab.fx.extensions.createNonNullValueBinding import org.janelia.saalfeldlab.fx.ui.NamedNode import org.janelia.saalfeldlab.fx.ui.NumericSliderWithField import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.paintera.meshes.MeshExporterObj import org.janelia.saalfeldlab.paintera.meshes.MeshInfo import org.janelia.saalfeldlab.paintera.meshes.MeshSettings +import org.janelia.saalfeldlab.paintera.meshes.managed.GetBlockListFor +import org.janelia.saalfeldlab.paintera.meshes.managed.GetMeshFor import org.janelia.saalfeldlab.paintera.meshes.managed.MeshManager +import org.janelia.saalfeldlab.paintera.meshes.managed.MeshManagerWithAssignmentForSegments +import org.janelia.saalfeldlab.paintera.meshes.ui.MeshInfoPane.Companion.LOG import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts import org.janelia.saalfeldlab.paintera.ui.RefreshButton +import org.janelia.saalfeldlab.paintera.ui.dialogs.AnimatedProgressBarAlert +import org.janelia.saalfeldlab.paintera.ui.source.mesh.MeshExportResult import org.janelia.saalfeldlab.paintera.ui.source.mesh.MeshExporterDialog import org.janelia.saalfeldlab.paintera.ui.source.mesh.MeshProgressBar +import java.util.concurrent.CancellationException import kotlin.math.max import kotlin.math.min @@ -190,7 +202,7 @@ class MeshSettingsController @JvmOverloads constructor( // min label ratio slider only makes sense for sources of label multiset type if (addMinLabelratioSlider) { val tooltipText = "Min label percentage for a pixel to be filled." + System.lineSeparator() + - "0.0 means that a pixel will always be filled if it contains the given label." + "0.0 means that a pixel will always be filled if it contains the given label." addGridOption("Min label ratio", minLabelRatioSlider, tooltipText) } @@ -318,14 +330,14 @@ open class MeshInfoPane(private val meshInfo: MeshInfo) : TitledPane(null, val result = exportDialog.showAndWait() if (!result.isPresent) return@setOnAction + val manager: MeshManager = meshInfo.manager val parameters = result.get() + manager.exportMeshWithProgressPopup(parameters) + val meshExporter = parameters.meshExporter val ids = parameters.meshKeys - if (ids.isEmpty()) return@setOnAction - val filePath = parameters.filePath - val scale = parameters.scale if (meshExporter is MeshExporterObj) { meshExporter.exportMaterial( @@ -334,19 +346,72 @@ open class MeshInfoPane(private val meshInfo: MeshInfo) : TitledPane(null, arrayOf(meshInfo.manager.getStateFor(ids[0])?.color ?: Color.WHITE) ) } - - meshExporter.exportMesh( - meshInfo.manager.getBlockListFor, - meshInfo.manager.getMeshFor, - meshInfo.meshSettings, - ids[0], - scale, - filePath, - false - ) } return exportMeshButton } + + companion object { + private val LOG = KotlinLogging.logger { } + } +} + +fun MeshManager.exportMeshWithProgressPopup(result : MeshExportResult) { + val log = KotlinLogging.logger { } + val meshExporter = result.meshExporter + val blocksProcessed = meshExporter.blocksProcessed + val ids = result.meshKeys + if (ids.isEmpty()) return + val (getBlocks, getMesh) = when(this) { + is MeshManagerWithAssignmentForSegments -> getBlockListForSegment as GetBlockListFor to getMeshForLongKey as GetMeshFor + else -> getBlockListFor to getMeshFor + } + val totalBlocks = ids.sumOf { getBlocks.getBlocksFor(result.scale, it).count() } + val labelProp = SimpleStringProperty().apply { + bind(blocksProcessed.createNonNullValueBinding { "Blocks processed: ${it}/$totalBlocks" }) + } + val progressProp = SimpleDoubleProperty(0.0).apply { + bind(blocksProcessed.createNonNullValueBinding { it.toDouble() / totalBlocks }) + } + val progressUpdater = AnimatedProgressBarAlert( + "Export Mesh", + "Exporting Mesh", + labelProp, + progressProp + ) + val exportJob = CoroutineScope(Dispatchers.IO).async { + meshExporter.exportMesh( + getBlocks, + getMesh, + ids.map { getSettings(it) }.toTypedArray(), + ids, + result.scale, + result.filePath + ) + } + exportJob.invokeOnCompletion { cause -> + cause?.let { + if (it is CancellationException) { + log.info { "Export Mesh Cancelled by User" } + progressUpdater.stopAndClose() + return@invokeOnCompletion + } + log.error(it) { "Error exporting meshes" } + progressUpdater.stopAndClose() + InvokeOnJavaFXApplicationThread { + PainteraAlerts.alert(Alert.AlertType.ERROR, true).apply { + contentText = "Error exporting meshes\n${it.message}" + }.showAndWait() + } + } ?: progressUpdater.finish() + } + InvokeOnJavaFXApplicationThread { + if (exportJob.isActive) { + progressUpdater.showAndWait() + if (progressUpdater.cancelled) + meshExporter.cancel() + } + + } } abstract class MeshInfoList, K>( diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/IntersectingSourceStatePreferencePaneNode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/IntersectingSourceStatePreferencePaneNode.kt index e05d39dca..aaf3d6fd4 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/IntersectingSourceStatePreferencePaneNode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/IntersectingSourceStatePreferencePaneNode.kt @@ -12,6 +12,7 @@ import org.janelia.saalfeldlab.paintera.meshes.MeshInfo import org.janelia.saalfeldlab.paintera.meshes.managed.MeshManagerWithSingleMesh import org.janelia.saalfeldlab.paintera.meshes.ui.MeshSettingsController import org.janelia.saalfeldlab.paintera.meshes.ui.MeshSettingsController.Companion.addGridOption +import org.janelia.saalfeldlab.paintera.meshes.ui.exportMeshWithProgressPopup import org.janelia.saalfeldlab.paintera.ui.source.mesh.MeshExporterDialog import org.janelia.saalfeldlab.util.Colors @@ -46,19 +47,12 @@ class IntersectingSourceStatePreferencePaneNode(private val state: IntersectingS val exportDialog = MeshExporterDialog(MeshInfo(key, manager)) val result = exportDialog.showAndWait() if (result.isPresent) { + manager.exportMeshWithProgressPopup(result.get()) result.get().run { + if (meshExporter.isCancelled()) return@run (meshExporter as? MeshExporterObj<*>)?.run { exportMaterial(filePath, arrayOf(""), arrayOf(Colors.toColor(state.converter().color))) } - meshExporter.exportMesh( - manager.getBlockListFor, - manager.getMeshFor, - manager.getSettings(key), - key, - scale, - filePath, - false - ) } } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMeshPaneNode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMeshPaneNode.kt index bf8a1fe7c..b897005c3 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMeshPaneNode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMeshPaneNode.kt @@ -1,5 +1,6 @@ package org.janelia.saalfeldlab.paintera.state +import io.github.oshai.kotlinlogging.KotlinLogging import javafx.collections.ObservableList import javafx.event.EventHandler import javafx.geometry.Insets @@ -18,14 +19,16 @@ import org.janelia.saalfeldlab.fx.extensions.TitledPaneExtensions.Companion.expa import org.janelia.saalfeldlab.fx.extensions.TitledPaneExtensions.Companion.graphicsOnly import org.janelia.saalfeldlab.fx.extensions.createNonNullValueBinding import org.janelia.saalfeldlab.paintera.data.DataSource -import org.janelia.saalfeldlab.paintera.meshes.* +import org.janelia.saalfeldlab.paintera.meshes.GlobalMeshProgressState +import org.janelia.saalfeldlab.paintera.meshes.MeshExporterObj +import org.janelia.saalfeldlab.paintera.meshes.MeshInfo +import org.janelia.saalfeldlab.paintera.meshes.SegmentMeshInfoList import org.janelia.saalfeldlab.paintera.meshes.managed.MeshManagerWithAssignmentForSegments import org.janelia.saalfeldlab.paintera.meshes.ui.MeshSettingsController +import org.janelia.saalfeldlab.paintera.meshes.ui.exportMeshWithProgressPopup import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts import org.janelia.saalfeldlab.paintera.ui.source.mesh.MeshExporterDialog import org.janelia.saalfeldlab.paintera.ui.source.mesh.MeshProgressBar -import org.slf4j.LoggerFactory -import java.lang.invoke.MethodHandles typealias TPE = TitledPaneExtensions @@ -73,10 +76,12 @@ class LabelSourceStateMeshPaneNode( val exportDialog = MeshExporterDialog(meshInfoList.meshInfos as ObservableList>) val result = exportDialog.showAndWait() if (result.isPresent) { + manager.exportMeshWithProgressPopup(result.get()) result.get().run { + if (meshExporter.isCancelled()) return@run + val ids = meshKeys.toTypedArray() val meshSettings = ids.map { manager.getSettings(it) }.toTypedArray() - (meshExporter as? MeshExporterObj<*>)?.run { val colors: Array = ids.mapIndexed { idx, it -> val color = manager.getStateFor(it)?.color ?: Color.WHITE @@ -84,14 +89,6 @@ class LabelSourceStateMeshPaneNode( }.toTypedArray() exportMaterial(filePath, ids.map { it.toString() }.toTypedArray(), colors) } - meshExporter.exportMesh( - manager.getBlockListForSegment, - manager.getMeshForLongKey, - meshSettings, - ids, - scale, - filePath - ) } } } @@ -131,7 +128,7 @@ class LabelSourceStateMeshPaneNode( companion object { - private val LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()) + private val LOG = KotlinLogging.logger { } private fun Node.asVBox() = if (this is VBox) this else VBox(this) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/ThresholdingSourceStatePreferencePaneNode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/ThresholdingSourceStatePreferencePaneNode.kt index baa05392b..90ddd84ad 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/ThresholdingSourceStatePreferencePaneNode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/ThresholdingSourceStatePreferencePaneNode.kt @@ -19,6 +19,7 @@ import org.janelia.saalfeldlab.fx.ui.ObjectField import org.janelia.saalfeldlab.paintera.meshes.MeshExporterObj import org.janelia.saalfeldlab.paintera.meshes.MeshInfo import org.janelia.saalfeldlab.paintera.meshes.ui.MeshSettingsController +import org.janelia.saalfeldlab.paintera.meshes.ui.exportMeshWithProgressPopup import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts import org.janelia.saalfeldlab.paintera.ui.source.mesh.MeshExporterDialog @@ -93,19 +94,12 @@ class ThresholdingSourceStatePreferencePaneNode(private val state: ThresholdingS val exportDialog = MeshExporterDialog(MeshInfo(state.meshManager.meshKey, state.meshManager)) val result = exportDialog.showAndWait() if (result.isPresent) { + state.meshManager.exportMeshWithProgressPopup(result.get()) result.get().run { + if (meshExporter.isCancelled()) return@run (meshExporter as? MeshExporterObj<*>)?.run { exportMaterial(filePath, arrayOf(""), arrayOf(state.colorProperty().get())) } - meshExporter.exportMesh( - state.meshManager.getBlockListFor, - state.meshManager.getMeshFor, - state.meshSettings, - state.thresholdBounds, - scale, - filePath, - false - ) } } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/AnimatedProgressBarAlert.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/AnimatedProgressBarAlert.kt index 9ff4e1550..5a4e7b5a9 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/AnimatedProgressBarAlert.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/AnimatedProgressBarAlert.kt @@ -2,6 +2,11 @@ package org.janelia.saalfeldlab.paintera.ui.dialogs import javafx.beans.binding.DoubleExpression import javafx.beans.binding.StringExpression +import javafx.beans.property.SimpleBooleanProperty +import javafx.event.EventHandler +import javafx.scene.control.Alert +import javafx.scene.control.Button +import javafx.scene.control.ButtonType import javafx.scene.control.Label import javafx.scene.layout.HBox import javafx.scene.layout.VBox @@ -20,17 +25,36 @@ class AnimatedProgressBarAlert( progressTargetProperty.bind(progressBinding) } - private val progressAlert = PainteraAlerts.information("Ok", false).apply { + private val canCloseBinding = SimpleBooleanProperty(true) + var cancelled = false + private set + + private val progressAlert : Alert = PainteraAlerts.confirmation("Ok", "Cancel", false).apply { this.title = title this.headerText = header val progressBar = progressBar.apply { prefWidth = 300.0 } + onCloseRequest = EventHandler { + if (progressBar.progress < 1.0) + cancelled = true + stop() + } + + (dialogPane.lookupButton(ButtonType.OK) as Button).apply { + disableProperty().bind(canCloseBinding.not()) + } + + (dialogPane.lookupButton(ButtonType.CANCEL) as Button).onAction = EventHandler { + cancelled = true + stopAndClose() + } + val doneLabel = Label("Done!") doneLabel.visibleProperty().bind(progressBar.progressProperty().greaterThanOrEqualTo(1.0)) dialogPane.content = VBox(10.0, createProgressLabel(), progressBar, HBox(doneLabel)) - isResizable = false + isResizable = true } private fun createProgressLabel() = Label().apply { @@ -38,16 +62,40 @@ class AnimatedProgressBarAlert( } - fun showAndStart() = InvokeOnJavaFXApplicationThread { + /** + * Show Dialog and wait for it to finish. Should be called on the JavaFx Thread. + * + */ + fun showAndWait() { + canCloseBinding.set(false) progressAlert.showAndWait() } + /** + * Set progress to the end, and allow the dialog to be closed + * + */ fun finish() = InvokeOnJavaFXApplicationThread { progressBar.finish() + canCloseBinding.set(true) } + /** + * Stop progress at its current state without finishing and close the dialog + * + */ fun stopAndClose() = InvokeOnJavaFXApplicationThread { progressBar.stop() + canCloseBinding.set(true) progressAlert.close() } + + /** + * Stop progress without finishing, leave the dialog open, but allow it to be closed. + * + */ + fun stop() = InvokeOnJavaFXApplicationThread { + progressBar.stop() + canCloseBinding.set(true) + } } \ No newline at end of file