diff --git a/.gitignore b/.gitignore index 1c0ae2ea3..537a23aa2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ canvases *.class *.java-version *dockerfile +/meshes diff --git a/pom.xml b/pom.xml index 9c780c55c..f056b9375 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 11 - 1.8.0 + 1.9.20 true @@ -88,6 +88,11 @@ + + javax.xml.bind + jaxb-api + 2.2.2 + dev.dirs directories @@ -151,6 +156,11 @@ org.jetbrains.kotlin kotlin-stdlib + + io.github.oshai + kotlin-logging-jvm + 5.1.0 + org.scijava scijava-common @@ -541,7 +551,7 @@ org.jetbrains.kotlinx kotlinx-coroutines-core - 1.6.4 + 1.7.3 compile diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/ManagedMeshSettings.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/ManagedMeshSettings.java index 659bebdeb..ae67da06c 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/ManagedMeshSettings.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/ManagedMeshSettings.java @@ -23,7 +23,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -public class ManagedMeshSettings { +public class ManagedMeshSettings { public static final String MESH_SETTINGS_KEY = "meshSettings"; @@ -35,9 +35,9 @@ public class ManagedMeshSettings { private final MeshSettings globalSettings; - private final Map individualSettings = new HashMap<>(); + private final Map individualSettings = new HashMap<>(); - private final HashMap isManagedProperties = new HashMap<>(); + private final HashMap isManagedProperties = new HashMap<>(); private final SimpleBooleanProperty isMeshListEnabled = new SimpleBooleanProperty(DEFAULT_IS_MESH_LIST_ENABLED); @@ -53,7 +53,18 @@ public ManagedMeshSettings(final MeshSettings globalSettings) { this.globalSettings = globalSettings; } - public MeshSettings getOrAddMesh(final Long id, final boolean isManaged) { + /** + * Retrieves a MeshSettings object for the given id. + * If the mesh settings are managed, the existing managed settings will be returned. + * Otherwise, a new MeshSettings object will be created based on the global settings. + *

+ * The resulting MeshSettings will be added to the `individualSettings` for this `id`, either `isManaged` or not. + * + * @param id The id of the mesh to retrieve the settings for. + * @param isManaged True if the individual mesh settings should be used. + * @return The mesh settings for the given id, or a new MeshSettings object if the id is not found. + */ + public MeshSettings getMeshSettings(final K id, final boolean isManaged) { if (!isManagedProperties.containsKey(id)) { final SimpleBooleanProperty isManagedProperty = new SimpleBooleanProperty(isManaged); @@ -69,12 +80,25 @@ public MeshSettings getOrAddMesh(final Long id, final boolean isManaged) { return individualSettings.get(id); } - public MeshSettings getOrAddMesh(final Long id) { + /** + * Retrieves a MeshSettings for the given key. + * If the mesh settings are managed, then the managed settings will be returned. + * Otherwise, a new MeshSettings object will be created based on global settings. + * + * @param meshKey the key of the mesh to retrieve the settings for + * @return the mesh settings for the given mesh key, or a new MeshSettings object if the key is not found + */ + public MeshSettings getMeshSettings(final K meshKey) { + + final var isManagedProperty = isManagedProperties.get(meshKey); + + final boolean isManaged; + if (isManagedProperty != null) + isManaged = isManagedProperty.get(); + else + isManaged = false; - if (!isManagedProperties.containsKey(id)) { - return new MeshSettings(globalSettings.getNumScaleLevels()); - } - return individualSettings.get(id); + return getMeshSettings(meshKey, isManaged); } public MeshSettings getGlobalSettings() { @@ -82,12 +106,18 @@ public MeshSettings getGlobalSettings() { return globalSettings; } - public boolean isPresent(final Long t) { + /** + * Checks if the given mesh key is managed. + * + * @param meshKey the mesh key + * @return true if the mesh key is managed, false otherwise + */ + public boolean isManaged(final K meshKey) { - return this.isManagedProperties.containsKey(t); + return this.isManagedProperties.containsKey(meshKey); } - public BooleanProperty isManagedProperty(final Long t) { + public BooleanProperty isManagedProperty(final K t) { return this.isManagedProperties.get(t); } @@ -108,9 +138,9 @@ public void clearSettings() { this.individualSettings.clear(); } - public void keepOnlyMatching(final Predicate filter) { + public void keepOnlyMatching(final Predicate filter) { - final Set toBeRemoved = isManagedProperties + final Set toBeRemoved = isManagedProperties .keySet() .stream() .filter(filter.negate()) @@ -125,16 +155,16 @@ public static Serializer jsonSerializer() { return new Serializer(); } - public void set(final ManagedMeshSettings that) { + public void set(final ManagedMeshSettings that) { clearSettings(); globalSettings.setTo(that.globalSettings); isMeshListEnabled.set(that.isMeshListEnabled.get()); meshesEnabled.set(that.meshesEnabled.get()); - for (final Entry entry : that.individualSettings.entrySet()) { - final Long id = entry.getKey(); + for (final Entry entry : that.individualSettings.entrySet()) { + final K id = entry.getKey(); final boolean isManaged = that.isManagedProperties.get(id).get(); - this.getOrAddMesh(id, isManaged); + this.getMeshSettings(id, isManaged); if (!isManaged) this.individualSettings.get(id).setTo(entry.getValue()); } @@ -188,10 +218,12 @@ public ManagedMeshSettings deserialize( if (!settingsMap.has(ID_KEY)) { continue; } + //TODO Caleb: This needs to store and deserialize based on generic key type, not just Long + // Necessary to share logic for meshes across segment and virtual sources final Long id = context.deserialize(settingsMap.get(ID_KEY), Long.class); final MeshSettings settings = Optional .ofNullable(settingsMap.get(SETTINGS_KEY)) - .map(el -> (MeshSettings)context.deserialize(el, MeshSettings.class)) + .map(el -> (MeshSettings) context.deserialize(el, MeshSettings.class)) .orElseGet(globalSettings::copy); final boolean isManaged = Optional .ofNullable(settingsMap.get(IS_MANAGED_KEY)) @@ -199,13 +231,13 @@ public ManagedMeshSettings deserialize( .orElse(true); LOG.debug("{} is managed? {}", id, isManaged); if (!isManaged) - managedSettings.getOrAddMesh(id, false).setTo(settings); + managedSettings.getMeshSettings(id, false).setTo(settings); else - managedSettings.getOrAddMesh(id, true); + managedSettings.getMeshSettings(id, true); } return managedSettings; } catch (final Exception e) { - throw e instanceof JsonParseException ? (JsonParseException)e : new JsonParseException(e); + throw e instanceof JsonParseException ? (JsonParseException) e : new JsonParseException(e); } } @@ -215,19 +247,22 @@ public JsonElement serialize( final Type typeOfSrc, final JsonSerializationContext context) { + //TODO Caleb: This also needs to serialize the generic type, instead of casting to Object + ManagedMeshSettings managedMeshSettings = (ManagedMeshSettings) src; + final JsonObject map = new JsonObject(); - map.add(GLOBAL_SETTINGS_KEY, context.serialize(src.globalSettings)); + map.add(GLOBAL_SETTINGS_KEY, context.serialize(managedMeshSettings.globalSettings)); - if (DEFAULT_IS_MESH_LIST_ENABLED != src.isMeshListEnabledProperty().get()) - map.addProperty(IS_MESH_LIST_ENABLED_KEY, src.isMeshListEnabledProperty().get()); + if (DEFAULT_IS_MESH_LIST_ENABLED != managedMeshSettings.isMeshListEnabledProperty().get()) + map.addProperty(IS_MESH_LIST_ENABLED_KEY, managedMeshSettings.isMeshListEnabledProperty().get()); - if (DEFAULT_ARE_MESHES_ENABLED != src.getMeshesEnabledProperty().get()) - map.addProperty(MESHES_ENABLED_KEY, src.getMeshesEnabledProperty().get()); + if (DEFAULT_ARE_MESHES_ENABLED != managedMeshSettings.getMeshesEnabledProperty().get()) + map.addProperty(MESHES_ENABLED_KEY, managedMeshSettings.getMeshesEnabledProperty().get()); final JsonArray meshSettingsList = new JsonArray(); - for (final Entry entry : src.individualSettings.entrySet()) { - final Long id = entry.getKey(); - final Boolean isManaged = Optional.ofNullable(src.isManagedProperty(id)).map(BooleanProperty::get).orElse(true); + for (final Entry entry : managedMeshSettings.individualSettings.entrySet()) { + final Object id = entry.getKey(); + final Boolean isManaged = Optional.ofNullable(managedMeshSettings.isManagedProperty(id)).map(BooleanProperty::get).orElse(true); if (!isManaged) { final JsonObject settingsMap = new JsonObject(); settingsMap.addProperty(IS_MANAGED_KEY, false); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MarchingCubes.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MarchingCubes.java index 8a3d009cd..3f35459cf 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MarchingCubes.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MarchingCubes.java @@ -1,18 +1,14 @@ package org.janelia.saalfeldlab.paintera.meshes; import gnu.trove.list.array.TFloatArrayList; -import net.imglib2.FinalInterval; import net.imglib2.Interval; -import net.imglib2.RandomAccess; import net.imglib2.RandomAccessible; import net.imglib2.loops.LoopBuilder; import net.imglib2.type.BooleanType; -import net.imglib2.util.Intervals; -import paintera.net.imglib2.view.BundleView; -import net.imglib2.view.IntervalView; import net.imglib2.view.Views; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import paintera.net.imglib2.view.BundleView; /** * This class implements the marching cubes algorithm. Based on http://paulbourke.net/geometry/polygonise/ @@ -381,13 +377,6 @@ public float[] generateMesh() { // This way, we need to remap the cube vertices. // @formatter:on - final FinalInterval expandedInterval = new FinalInterval( - new long[]{ - interval.min(0) - 1, - interval.min(1) - 1, - interval.min(2) - 1}, - Intervals.maxAsLongArray(interval)); - final TFloatArrayList vertices = new TFloatArrayList(); final float[][] interpolationPoints = new float[12][3]; @@ -395,53 +384,52 @@ public float[] generateMesh() { final int[] idxRemap = new int[]{5, 7, 3, 1, 4, 6, 2, 0}; final long[] prevPos = new long[]{Long.MIN_VALUE, 0, 0}; final long[] curPos = new long[3]; - final BundleView bundledInput = new BundleView<>(Views.interval(input, interval)); - final IntervalView> expandedBundle = Views.interval(bundledInput, expandedInterval); - - LoopBuilder.setImages(expandedBundle).forEachPixel(pixelAccess -> { - pixelAccess.localize(curPos); - if (prevPos[0] != Long.MIN_VALUE && curPos[0] - prevPos[0] == 1 && curPos[1] - prevPos[1] == 0 && curPos[2] - prevPos[2] == 0) { - vertexVals[0] = vertexVals[1]; - vertexVals[2] = vertexVals[3]; - vertexVals[4] = vertexVals[5]; - vertexVals[6] = vertexVals[7]; - } else { - vertexVals[0] = pixelAccess.get().get(); - pixelAccess.move(1, 1); //move to offset (0, 1, 0) - vertexVals[2] = pixelAccess.get().get(); - pixelAccess.move(1, 2); //move to offset (0, 1, 1) - vertexVals[6] = pixelAccess.get().get(); - pixelAccess.move(-1, 1); //move to offset (0, 0, 1) - vertexVals[4] = pixelAccess.get().get(); - pixelAccess.move(-1, 2); - } - pixelAccess.move(1, 0); //move to offset (1, 0, 0) - vertexVals[1] = pixelAccess.get().get(); - pixelAccess.move(1, 1); //move to offset (1, 1, 0) - vertexVals[3] = pixelAccess.get().get(); - pixelAccess.move(1, 2); //move to offset (1, 1, 1) - vertexVals[7] = pixelAccess.get().get(); - pixelAccess.move(-1, 1); //move to offset (1, 0, 1) - vertexVals[5] = pixelAccess.get().get(); - - /* setup for next loop*/ - pixelAccess.setPosition(curPos); - System.arraycopy(curPos, 0, prevPos, 0, 3); - - var vertexIn = 0b00000000; - for (int i = 0; i < vertexVals.length; i++) { - vertexIn |= (vertexVals[idxRemap[i]] ? 1 : 0) << i; - } - triangulation( - vertexIn, - curPos[0], //cursor0.getLongPosition(0), - curPos[1], //cursor0.getLongPosition(1), - curPos[2], //cursor0.getLongPosition(2), - vertices, - interpolationPoints - ); - }); + LoopBuilder.setImages(Views.interval(new BundleView<>(input), interval)) + .forEachPixel(pixelAccess -> { + pixelAccess.localize(curPos); + if (prevPos[0] != Long.MIN_VALUE && curPos[0] - prevPos[0] == 1 && curPos[1] - prevPos[1] == 0 && curPos[2] - prevPos[2] == 0) { + vertexVals[0] = vertexVals[1]; + vertexVals[2] = vertexVals[3]; + vertexVals[4] = vertexVals[5]; + vertexVals[6] = vertexVals[7]; + } else { + vertexVals[0] = pixelAccess.get().get(); + pixelAccess.move(1, 1); //move to offset (0, 1, 0) + vertexVals[2] = pixelAccess.get().get(); + pixelAccess.move(1, 2); //move to offset (0, 1, 1) + vertexVals[6] = pixelAccess.get().get(); + pixelAccess.move(-1, 1); //move to offset (0, 0, 1) + vertexVals[4] = pixelAccess.get().get(); + pixelAccess.move(-1, 2); + } + pixelAccess.move(1, 0); //move to offset (1, 0, 0) + vertexVals[1] = pixelAccess.get().get(); + pixelAccess.move(1, 1); //move to offset (1, 1, 0) + vertexVals[3] = pixelAccess.get().get(); + pixelAccess.move(1, 2); //move to offset (1, 1, 1) + vertexVals[7] = pixelAccess.get().get(); + pixelAccess.move(-1, 1); //move to offset (1, 0, 1) + vertexVals[5] = pixelAccess.get().get(); + + /* setup for next loop*/ + pixelAccess.setPosition(curPos); + System.arraycopy(curPos, 0, prevPos, 0, 3); + + var vertexIn = 0b00000000; + for (int i = 0; i < vertexVals.length; i++) { + vertexIn |= (vertexVals[idxRemap[i]] ? 1 : 0) << i; + } + + triangulation( + vertexIn, + curPos[0], + curPos[1], + curPos[2], + vertices, + interpolationPoints + ); + }); return vertices.toArray(); } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/Mesh.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/Mesh.java index 559c4bf6c..a937135b4 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/Mesh.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/Mesh.java @@ -91,6 +91,10 @@ public FloatLocalizable(float[] position) { * Of the form [ t1_v0_x, t1_v1_y, t1_v2_z, t2_v0_x, t2_v1_y, t2_v2_z, ..., tn_v0_x, tn_v1_y, tn_v2_z] */ public Mesh(final float[] flatTrianglesAndVertices, final Interval interval, final AffineTransform3D transform) { + this(flatTrianglesAndVertices, interval, transform, true); + } + + public Mesh(final float[] flatTrianglesAndVertices, final Interval interval, final AffineTransform3D transform, final boolean overlap) { assert flatTrianglesAndVertices.length % 9 == 0; @@ -103,9 +107,10 @@ public Mesh(final float[] flatTrianglesAndVertices, final Interval interval, fin final double minX = interval.min(0) - 1; final double minZ = interval.min(2) - 1; - final double maxX = interval.max(0) + 1; // overlap 1 - final double maxY = interval.max(1) + 1; // overlap 1 - final double maxZ = interval.max(2) + 1; // overlap 1 + final int overlapOffset = overlap ? 1 : 0; + final double maxX = interval.max(0) + overlapOffset; + final double maxY = interval.max(1) + overlapOffset; + final double maxZ = interval.max(2) + overlapOffset; final RealInterval vertexBounds = new FinalRealInterval( new double[]{minX, minY, minZ}, 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 4d2c12490..b688ab759 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshExporter.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshExporter.java @@ -62,6 +62,7 @@ public void exportMesh( meshSettings.getSmoothingLambda(), meshSettings.getSmoothingIterations(), meshSettings.getMinLabelRatio(), + meshSettings.getOverlap(), Intervals.minAsLongArray(block), Intervals.maxAsLongArray(block) )); @@ -94,4 +95,7 @@ public void exportMesh( protected abstract void save(String path, String id, float[] vertices, float[] normals, int[] indices, boolean append) throws IOException; + public enum MeshFileFormat { + Obj, Binary; + } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshExporterObj.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshExporterObj.java index 13fa81dd2..e81d99c68 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshExporterObj.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshExporterObj.java @@ -15,12 +15,12 @@ public class MeshExporterObj extends MeshExporter { private long numVertices = 0; - public void exportMaterial(String path, final long[] ids, final Color[] colors) { + public void exportMaterial(String path, final String[] ids, final Color[] colors) { final String materialPath = path + ".mtl"; try (final FileWriter writer = new FileWriter(materialPath, false)) { final StringBuilder sb = new StringBuilder(); for (int i = 0; i < ids.length; i++) { - final long id = ids[i]; + final String id = ids[i]; final Color specularColor = colors[i]; final double red = specularColor.getRed(); final double green = specularColor.getGreen(); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshGenerator.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshGenerator.java index df908c0d9..9df465c3a 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshGenerator.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshGenerator.java @@ -93,7 +93,6 @@ public void setShowBlockBoundaries(final boolean isShowBlockBoundaries) { showBlockBoundaries.set(isShowBlockBoundaries); } - } private static class SceneUpdateParameters { @@ -104,6 +103,7 @@ private static class SceneUpdateParameters { final double smoothingLambda; final int smoothingIterations; final double minLabelRatio; + final boolean overlap; SceneUpdateParameters( final BlockTree> sceneBlockTree, @@ -111,7 +111,8 @@ private static class SceneUpdateParameters { final int meshSimplificationIterations, final double smoothingLambda, final int smoothingIterations, - final double minLabelRatio) { + final double minLabelRatio, + final boolean overlap) { this.sceneBlockTree = sceneBlockTree; this.rendererGrids = rendererGrids; @@ -119,6 +120,7 @@ private static class SceneUpdateParameters { this.smoothingLambda = smoothingLambda; this.smoothingIterations = smoothingIterations; this.minLabelRatio = minLabelRatio; + this.overlap = overlap; } } @@ -186,10 +188,11 @@ public MeshGenerator( sceneUpdateParameters = new SceneUpdateParameters( sceneUpdateParameters != null ? sceneUpdateParameters.sceneBlockTree : null, sceneUpdateParameters != null ? sceneUpdateParameters.rendererGrids : null, - this.state.settings.getSimplificationIterations(),// this.meshSimplificationIterations.get(), - this.state.settings.getSmoothingLambda(),//this.smoothingLambda.get(), - this.state.settings.getSmoothingIterations(),//this.smoothingIterations.get(), - this.state.settings.getMinLabelRatio()//this.minLabelRatio.get() + this.state.settings.getSimplificationIterations(), + this.state.settings.getSmoothingLambda(), + this.state.settings.getSmoothingIterations(), + this.state.settings.getMinLabelRatio(), + this.state.settings.getOverlap() ); updateMeshes(); } @@ -199,6 +202,7 @@ public MeshGenerator( this.state.settings.getSmoothingLambdaProperty().addListener(updateInvalidationListener); this.state.settings.getSmoothingIterationsProperty().addListener(updateInvalidationListener); this.state.settings.getMinLabelRatioProperty().addListener(updateInvalidationListener); + this.state.settings.getOverlapProperty().addListener(updateInvalidationListener); // initialize updateInvalidationListener.invalidated(null); @@ -232,12 +236,12 @@ public MeshGenerator( workers, state.progress); - this.meshesAndBlocks.addListener((MapChangeListener, Pair>)change -> + this.meshesAndBlocks.addListener((MapChangeListener, Pair>) change -> { if (change.wasRemoved()) { if (change.getValueRemoved().getA() != null) { final MeshView meshRemoved = change.getValueRemoved().getA(); - ((PhongMaterial)meshRemoved.getMaterial()).diffuseColorProperty().unbind(); + ((PhongMaterial) meshRemoved.getMaterial()).diffuseColorProperty().unbind(); meshRemoved.drawModeProperty().unbind(); meshRemoved.cullFaceProperty().unbind(); meshRemoved.scaleXProperty().unbind(); @@ -249,13 +253,13 @@ public MeshGenerator( final Node blockOutlineRemoved = change.getValueRemoved().getB(); final Material material; if (blockOutlineRemoved instanceof PolygonMeshView) - material = ((PolygonMeshView)blockOutlineRemoved).getMaterial(); + material = ((PolygonMeshView) blockOutlineRemoved).getMaterial(); else if (blockOutlineRemoved instanceof Shape3D) - material = ((Shape3D)blockOutlineRemoved).getMaterial(); + material = ((Shape3D) blockOutlineRemoved).getMaterial(); else material = null; if (material instanceof PhongMaterial) - ((PhongMaterial)material).diffuseColorProperty().unbind(); + ((PhongMaterial) material).diffuseColorProperty().unbind(); blockOutlineRemoved.scaleXProperty().unbind(); blockOutlineRemoved.scaleYProperty().unbind(); blockOutlineRemoved.scaleZProperty().unbind(); @@ -265,7 +269,7 @@ else if (blockOutlineRemoved instanceof Shape3D) if (change.wasAdded()) { if (change.getValueAdded().getA() != null) { final MeshView meshAdded = change.getValueAdded().getA(); - ((PhongMaterial)meshAdded.getMaterial()).diffuseColorProperty().bind(this.state.premultipliedColor); + ((PhongMaterial) meshAdded.getMaterial()).diffuseColorProperty().bind(this.state.premultipliedColor); meshAdded.drawModeProperty().bind(this.state.settings.getDrawModeProperty()); meshAdded.cullFaceProperty().bind(this.state.settings.getCullFaceProperty()); } @@ -274,13 +278,13 @@ else if (blockOutlineRemoved instanceof Shape3D) final Node blockOutlineAdded = change.getValueAdded().getB(); final Material material; if (blockOutlineAdded instanceof PolygonMeshView) - material = ((PolygonMeshView)blockOutlineAdded).getMaterial(); + material = ((PolygonMeshView) blockOutlineAdded).getMaterial(); else if (blockOutlineAdded instanceof Shape3D) - material = ((Shape3D)blockOutlineAdded).getMaterial(); + material = ((Shape3D) blockOutlineAdded).getMaterial(); else material = null; if (material instanceof PhongMaterial) - ((PhongMaterial)material).diffuseColorProperty().bind(this.state.premultipliedColor); + ((PhongMaterial) material).diffuseColorProperty().bind(this.state.premultipliedColor); blockOutlineAdded.setDisable(true); } } @@ -306,7 +310,8 @@ public synchronized void update(final BlockTree> sceneBlockTree, @@ -78,7 +79,8 @@ private static final class SceneUpdateParameters { final int simplificationIterations, final double smoothingLambda, final int smoothingIterations, - final double minLabelRatio) { + final double minLabelRatio, + final boolean overlap) { this.sceneBlockTree = sceneBlockTree; this.rendererGrids = rendererGrids; @@ -86,6 +88,7 @@ private static final class SceneUpdateParameters { this.smoothingLambda = smoothingLambda; this.smoothingIterations = smoothingIterations; this.minLabelRatio = minLabelRatio; + this.overlap = overlap; } @Override @@ -95,7 +98,7 @@ public boolean equals(final Object obj) { return true; if (obj instanceof SceneUpdateParameters) { - final SceneUpdateParameters other = (SceneUpdateParameters)obj; + final SceneUpdateParameters other = (SceneUpdateParameters) obj; if (rendererGrids.length != other.rendererGrids.length) return false; @@ -104,8 +107,8 @@ public boolean equals(final Object obj) { for (int i = 0; i < rendererGrids.length; ++i) { //noinspection RedundantCast sameBlockSize &= Intervals.equalDimensions( - (Dimensions)Grids.getCellInterval(rendererGrids[i], 0), - (Dimensions)Grids.getCellInterval(other.rendererGrids[i], 0)); + (Dimensions) Grids.getCellInterval(rendererGrids[i], 0), + (Dimensions) Grids.getCellInterval(other.rendererGrids[i], 0)); } return @@ -113,7 +116,8 @@ public boolean equals(final Object obj) { simplificationIterations == other.simplificationIterations && smoothingLambda == other.smoothingLambda && smoothingIterations == other.smoothingIterations && - minLabelRatio == other.minLabelRatio; + minLabelRatio == other.minLabelRatio && + overlap == other.overlap; } return false; @@ -294,7 +298,8 @@ public void submit( final int simplificationIterations, final double smoothingLambda, final int smoothingIterations, - final double minLabelRatio) { + final double minLabelRatio, + final boolean overlap) { if (isInterrupted.get()) return; @@ -305,7 +310,8 @@ public void submit( simplificationIterations, smoothingLambda, smoothingIterations, - minLabelRatio + minLabelRatio, + overlap ); synchronized (sceneUpdateParametersProperty) { @@ -398,7 +404,7 @@ private synchronized void updateScene() { // calculate how many tasks are already completed final int numCompletedBlocks = numTotalBlocks - blocksToRender.size() - tasks.size(); meshProgress.set(numTotalBlocks, numCompletedBlocks); - final int numExistingNonEmptyMeshes = (int)meshesAndBlocks.values().stream().filter(pair -> pair.getA() != null).count(); + final int numExistingNonEmptyMeshes = (int) meshesAndBlocks.values().stream().filter(pair -> pair.getA() != null).count(); LOG.debug("ID {}: numTasks={}, numCompletedTasks={}, numActualBlocksToRender={}. Number of meshes in the scene: {} ({} of them are non-empty)", identifier, numTotalBlocks, numCompletedBlocks, blocksToRender.size(), meshesAndBlocks.size(), numExistingNonEmptyMeshes); @@ -611,11 +617,11 @@ private synchronized void onMeshGenerated(final ShapeKey key, final PainteraT "Mesh for block has been generated but it already exists in the current set of generated/visible meshes: " + key; LOG.trace("ID {}: block {} has been generated", identifier, key); - final boolean nonEmptyMesh = triangleMesh.isNotEmpty(); - final MeshView mv = nonEmptyMesh ? makeMeshView(triangleMesh) : null; - final Node blockShape = nonEmptyMesh ? createBlockShape(key) : null; + final boolean meshIsEmpty = triangleMesh.isEmpty(); + final MeshView mv = meshIsEmpty ? null : makeMeshView(triangleMesh); + final Node blockShape = meshIsEmpty ? null : createBlockShape(key); final Pair meshAndBlock = new ValuePair<>(mv, blockShape); - LOG.trace("Found {}/3 vertices and {}/3 normals", triangleMesh.getVertices(), triangleMesh.getNormals()); + LOG.trace("Found {}/3 vertices and {}/3 normals", triangleMesh.getVertices().length, triangleMesh.getNormals().length); final StatefulBlockTreeNode> treeNode = blockTree.nodes.get(key); treeNode.state = BlockTreeNodeState.RENDERED; @@ -1015,6 +1021,7 @@ private ShapeKey createShapeKey( sceneUpdateParameters.smoothingLambda, sceneUpdateParameters.smoothingIterations, sceneUpdateParameters.minLabelRatio, + sceneUpdateParameters.overlap, Intervals.minAsLongArray(blockInterval), Intervals.maxAsLongArray(blockInterval) ); @@ -1071,9 +1078,9 @@ private Node createBlockShape(final ShapeKey key) { // blockWorldSize[2] // ); final PolygonMeshView box = new PolygonMeshView(Meshes.createQuadrilateralMesh( - (float)blockWorldSize[0], - (float)blockWorldSize[1], - (float)blockWorldSize[2] + (float) blockWorldSize[0], + (float) blockWorldSize[1], + (float) blockWorldSize[2] )); final double[] blockWorldTranslation = new double[blockWorldInterval.numDimensions()]; diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshInfo.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshInfo.java deleted file mode 100644 index c420fb0e9..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/MeshInfo.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.janelia.saalfeldlab.paintera.meshes; - -import org.janelia.saalfeldlab.paintera.meshes.managed.MeshManagerWithSingleMesh; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.invoke.MethodHandles; - -public class MeshInfo { - - private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private final MeshManagerWithSingleMesh meshManager; - - public MeshInfo(final MeshManagerWithSingleMesh meshManager) { - - this.meshManager = meshManager; - } - - public MeshSettings getMeshSettings() { - - return meshManager.getSettings(); - } - - public MeshManagerWithSingleMesh meshManager() { - - return this.meshManager; - } - - public T getKey() { - - return this.meshManager.getMeshKey(); - } -} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/SegmentMeshInfo.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/SegmentMeshInfo.java deleted file mode 100644 index aa943cc4e..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/SegmentMeshInfo.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.janelia.saalfeldlab.paintera.meshes; - -import gnu.trove.set.hash.TLongHashSet; -import javafx.beans.property.BooleanProperty; -import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; -import org.janelia.saalfeldlab.paintera.meshes.managed.MeshManagerWithAssignmentForSegments; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.invoke.MethodHandles; -import java.util.Objects; - -public class SegmentMeshInfo { - - private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private final Long segmentId; - - private final MeshSettings meshSettings; - - private final FragmentSegmentAssignment assignment; - - private final MeshManagerWithAssignmentForSegments meshManager; - - private final ObservableMeshProgress meshProgress; - - private final BooleanProperty isManaged; - - public SegmentMeshInfo( - final Long segmentId, - final MeshSettings meshSettings, - final BooleanProperty isManaged, - final FragmentSegmentAssignment assignment, - final MeshManagerWithAssignmentForSegments meshManager) { - - this.segmentId = segmentId; - this.meshSettings = meshSettings; - this.isManaged = isManaged; - this.assignment = assignment; - this.meshManager = meshManager; - - final MeshGenerator.State meshGeneratorState = meshManager.getStateFor(segmentId); - this.meshProgress = meshGeneratorState == null ? null : meshGeneratorState.getProgress(); - } - - public Long segmentId() { - - return this.segmentId; - } - - public MeshSettings getMeshSettings() { - - return this.meshSettings; - } - - @Override - public int hashCode() { - - return segmentId.hashCode(); - } - - @Override - public boolean equals(final Object o) { - - return o instanceof SegmentMeshInfo && Objects.equals(((SegmentMeshInfo)o).segmentId, segmentId); - } - - public ObservableMeshProgress meshProgress() { - - return this.meshProgress; - } - - public MeshManagerWithAssignmentForSegments meshManager() { - - return this.meshManager; - } - - public long[] containedFragments() { - - final TLongHashSet fragments = meshManager.getContainedFragmentsFor(segmentId); - return fragments == null ? new long[]{} : fragments.toArray(); - } - - public BooleanProperty isManagedProperty() { - - return this.isManaged; - } - -} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/SegmentMeshInfos.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/SegmentMeshInfos.java deleted file mode 100644 index 8604e5f7e..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/SegmentMeshInfos.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.janelia.saalfeldlab.paintera.meshes; - -import javafx.beans.InvalidationListener; -import javafx.beans.property.ReadOnlyListProperty; -import javafx.beans.property.ReadOnlyListWrapper; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.scene.paint.Color; -import org.janelia.saalfeldlab.paintera.control.selection.SelectedSegments; -import org.janelia.saalfeldlab.paintera.meshes.managed.MeshManagerWithAssignmentForSegments; - -import java.util.ArrayList; -import java.util.Arrays; - -public class SegmentMeshInfos { - - private final ReadOnlyListWrapper readOnlyInfos = new ReadOnlyListWrapper<>(FXCollections.observableArrayList()); - private final ManagedMeshSettings meshSettings; - - private final int numScaleLevels; - - public SegmentMeshInfos( - final SelectedSegments selectedSegments, - final MeshManagerWithAssignmentForSegments meshManager, - final ManagedMeshSettings meshSettings, - final int numScaleLevels) { - - super(); - - this.meshSettings = meshSettings; - this.numScaleLevels = numScaleLevels; - - final InvalidationListener updateMeshInfosHandler = obs -> { - final long[] segments = selectedSegments.getSelectedSegmentsCopyAsArray(); - this.infos().clear(); - final var addInfos = new ArrayList(); - Arrays.stream(segments).forEach(id -> { - final MeshSettings settings = meshSettings.getOrAddMesh(id, true); - addInfos.add(new SegmentMeshInfo( - id, - settings, - meshSettings.isManagedProperty(id), - selectedSegments.getAssignment(), - meshManager)); - }); - this.infos().addAll(addInfos); - }; - - meshManager.getMeshUpdateObservable().addListener(updateMeshInfosHandler); - meshSettings.isMeshListEnabledProperty().addListener(updateMeshInfosHandler); - } - - public Color getColor(long id) { - return readOnlyInfos.stream() - .filter(it -> it.segmentId() == id) - .map(it -> it.meshManager().getStateFor(it.segmentId()).getColor()) - .findFirst() - .orElse(Color.WHITE); - } - - private ObservableList infos() { - - return readOnlyInfos.get(); - } - - public ReadOnlyListProperty readOnlyInfos() { - - return this.readOnlyInfos.getReadOnlyProperty(); - } - - public ManagedMeshSettings meshSettings() { - - return meshSettings; - } - - public int getNumScaleLevels() { - - return this.numScaleLevels; - } -} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/ShapeKey.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/ShapeKey.java index 486a94318..1ff219c76 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/ShapeKey.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/ShapeKey.java @@ -22,6 +22,8 @@ public class ShapeKey { private final double minLabelRatio; + private final boolean overlap; + private final long[] min; private final long[] max; @@ -37,6 +39,7 @@ public ShapeKey( final double smoothingLambda, final int smoothingIterations, final double minLabelRatio, + final boolean overlap, final long[] min, final long[] max) { @@ -47,6 +50,7 @@ public ShapeKey( smoothingLambda, smoothingIterations, minLabelRatio, + overlap, min, max, Objects::hashCode, @@ -60,6 +64,7 @@ public ShapeKey( final double smoothingLambda, final int smoothingIterations, final double minLabelRatio, + final boolean overlap, final long[] min, final long[] max, final ToIntFunction shapeIdHashCode, @@ -71,6 +76,7 @@ public ShapeKey( this.smoothingLambda = smoothingLambda; this.smoothingIterations = smoothingIterations; this.minLabelRatio = minLabelRatio; + this.overlap = overlap; this.min = min; this.max = max; this.shapeIdHashCode = shapeIdHashCode; @@ -81,13 +87,14 @@ public ShapeKey( public String toString() { return String.format( - "{shapeId=%s, scaleIndex=%d, simplifications=%d, smoothingLambda=%.2f, smoothings=%d, minLabelRatio=%.2f, min=%s, max=%s}", + "{shapeId=%s, scaleIndex=%d, simplifications=%d, smoothingLambda=%.2f, smoothings=%d, minLabelRatio=%.2f, overlap=%b, min=%s, max=%s}", shapeId, scaleIndex, simplificationIterations, smoothingLambda, smoothingIterations, minLabelRatio, + overlap, Arrays.toString(min), Arrays.toString(max)); } @@ -102,6 +109,7 @@ public int hashCode() { result = 31 * result + Double.hashCode(smoothingLambda); result = 31 * result + smoothingIterations; result = 31 * result + Double.hashCode(minLabelRatio); + result = 31 * result + Boolean.hashCode(overlap); result = 31 * result + Arrays.hashCode(this.min); result = 31 * result + Arrays.hashCode(this.max); return result; @@ -122,6 +130,7 @@ public boolean equals(final Object obj) { smoothingLambda == other.smoothingLambda && smoothingIterations == other.smoothingIterations && minLabelRatio == other.minLabelRatio && + overlap == other.overlap && Arrays.equals(min, other.min) && Arrays.equals(max, other.max); } @@ -184,4 +193,8 @@ public Interval interval() { return new FinalInterval(min, max); } + public boolean overlap() { + return overlap; + } + } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/AbstractMeshCacheLoader.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/AbstractMeshCacheLoader.java index 034d0ed66..4d8960d0f 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/AbstractMeshCacheLoader.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/AbstractMeshCacheLoader.java @@ -64,7 +64,7 @@ public PainteraTriangleMesh get(final ShapeKey key) throws Exception { Intervals.expand(key.interval(), smoothingIterations + 2) ).generateMesh(); - Mesh meshMesh = new Mesh(vertices, key.interval(), transform); + Mesh meshMesh = new Mesh(vertices, key.interval(), transform, key.overlap()); if (smoothingIterations > 0) meshMesh.smooth(key.smoothingLambda(), smoothingIterations); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/GenericMeshCacheLoader.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/GenericMeshCacheLoader.java index ea43bf93f..6d2ff6a91 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/GenericMeshCacheLoader.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/GenericMeshCacheLoader.java @@ -32,7 +32,6 @@ public GenericMeshCacheLoader( LOG.debug("Constructing {}", getClass().getName()); this.data = data; this.transform = transform; - // this.containedLabelsInBlock = containedLabelsInBlock; } @Override @@ -49,7 +48,7 @@ public PainteraTriangleMesh get(final ShapeKey key) throws Exception { Intervals.expand(key.interval(), smoothingIterations + 2) ).generateMesh(); - final Mesh meshMesh = new Mesh(vertices, key.interval(), transform); + final Mesh meshMesh = new Mesh(vertices, key.interval(), transform, key.overlap()); if (smoothingIterations > 0) meshMesh.smooth(key.smoothingLambda(), smoothingIterations); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/SegmentMaskGenerators.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/SegmentMaskGenerators.java index 42c374414..fa38c1a15 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/SegmentMaskGenerators.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/SegmentMaskGenerators.java @@ -29,18 +29,22 @@ public class SegmentMaskGenerators { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - public static > BiFunction> create( + public static > BiFunction> create( final DataSource source, - final int level) { + final int level, + final Function fragmentsForSegment + ) { final T t = source.getDataType(); - if (t instanceof LabelMultisetType) - return new LabelMultisetTypeMaskGenerator(source, level); + if (t instanceof LabelMultisetType) { + final LabelMultisetTypeMaskGenerator labelMultisetTypeMaskGenerator = new LabelMultisetTypeMaskGenerator(source, level); + return (l, minLabelRatio) -> labelMultisetTypeMaskGenerator.apply(fragmentsForSegment.apply(l), minLabelRatio); + } if (t instanceof IntegerType) { final IntegerTypeMaskGenerator integerTypeMaskGenerator = new IntegerTypeMaskGenerator(); - return (l, minLabelRatio) -> integerTypeMaskGenerator.apply(l); + return (l, minLabelRatio) -> integerTypeMaskGenerator.apply(fragmentsForSegment.apply(l)); } return null; diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/SegmentMeshCacheLoader.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/SegmentMeshCacheLoader.java index a47c2ebdc..613590a03 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/SegmentMeshCacheLoader.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/SegmentMeshCacheLoader.java @@ -9,11 +9,11 @@ import java.util.function.BiFunction; import java.util.function.Supplier; -public class SegmentMeshCacheLoader extends AbstractMeshCacheLoader { +public class SegmentMeshCacheLoader extends AbstractMeshCacheLoader { public SegmentMeshCacheLoader( final Supplier> data, - final BiFunction> getMaskGenerator, + final BiFunction> getMaskGenerator, final AffineTransform3D transform) { super(data, getMaskGenerator, transform); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/serialization/config/MeshSettingsSerializer.java b/src/main/java/org/janelia/saalfeldlab/paintera/serialization/config/MeshSettingsSerializer.java index f3ee04180..1f13226ba 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/serialization/config/MeshSettingsSerializer.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/serialization/config/MeshSettingsSerializer.java @@ -33,6 +33,8 @@ public class MeshSettingsSerializer implements PainteraSerialization.PainteraAda private static final String MIN_LABEL_RATIO_KEY = "minLabelRatio"; + private static final String OVERLAP_KEY = "overlap"; + private static final String OPACITY_KEY = "opacity"; private static final String DRAW_MODE_KEY = "drawMode"; @@ -56,6 +58,7 @@ public static MeshSettings deserializeInto( Optional.ofNullable(map.get(CULL_FACE_KEY)).map(el -> (CullFace)context.deserialize(el, CullFace.class)).ifPresent(settings::setCullFace); Optional.ofNullable(map.get(IS_VISIBLE_KEY)).map(JsonElement::getAsBoolean).ifPresent(settings::setVisible); Optional.ofNullable(map.get(MIN_LABEL_RATIO_KEY)).map(JsonElement::getAsDouble).ifPresent(settings::setMinLabelRatio); + Optional.ofNullable(map.get(OVERLAP_KEY)).map(JsonElement::getAsBoolean).ifPresent(settings::setOverlap); Optional.ofNullable(map.get(LEVEL_OF_DETAIL_KEY)).map(JsonElement::getAsInt).ifPresent(settings::setLevelOfDetail); return settings; } @@ -106,6 +109,9 @@ public JsonElement serialize(final MeshSettings src, final Type typeOfSrc, final if (defaults.getMinLabelRatio() != src.getMinLabelRatioProperty().get()) map.addProperty(MIN_LABEL_RATIO_KEY, src.getMinLabelRatioProperty().get()); + if (defaults.getOverlap() != src.getOverlapProperty().get()) + map.addProperty(OVERLAP_KEY, src.getOverlapProperty().get()); + if (defaults.getOpacity() != src.getOpacityProperty().get()) map.addProperty(OPACITY_KEY, src.getOpacityProperty().get()); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/IntersectingSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/IntersectingSourceState.java index 40c56ed0c..b86674090 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/IntersectingSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/IntersectingSourceState.java @@ -10,6 +10,8 @@ import javafx.beans.value.ObservableValue; import javafx.scene.Node; import javafx.scene.paint.Color; +import kotlin.coroutines.EmptyCoroutineContext; +import kotlinx.coroutines.BuildersKt; import net.imglib2.Cursor; import net.imglib2.FinalInterval; import net.imglib2.Interval; @@ -183,7 +185,14 @@ private IntersectingSourceState( INTERSECTION_FILL_SERVICE.submit(() -> { getDataSource().invalidateAll(); requestRepaint(); - getMeshManager().createMeshFor(newv); + try { + BuildersKt.runBlocking( + EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> getMeshManager().createMeshFor(newv, continuation) + ); + } catch (InterruptedException e) { + LOG.debug("create mesh interrupted"); + } }); } }); @@ -232,17 +241,20 @@ private CacheLoader>, Paint ); } - @Override public DataSource> getIntersectableMask() { + @Override + public DataSource> getIntersectableMask() { return predicateDataSource; } - @Override public ObjectBinding> getMeshCacheKeyBinding() { + @Override + public ObjectBinding> getMeshCacheKeyBinding() { return this.intersectionMeshCacheKeyBinding; } - @Override public GetBlockListFor> getGetBlockListFor() { + @Override + public GetBlockListFor> getGetBlockListFor() { return this.getGetUnionBlockListFor; } @@ -282,7 +294,8 @@ public Interval[] getBlocksFor(int level, IntersectingSourceStateMeshCacheKey this.requestRepaint()); - getMeshManager().createMeshFor(getMeshCacheKeyBinding().get()); + try { + BuildersKt.runBlocking( + EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> getMeshManager().createMeshFor(getMeshCacheKeyBinding().get(), continuation) + ); + } catch (InterruptedException e) { + LOG.debug("create mesh interrupted"); + } } private void requestRepaint() { @@ -310,7 +330,8 @@ private void requestRepaint() { requestRepaintProperty.set(true); } - @Override public void onRemoval(SourceInfo paintera) { + @Override + public void onRemoval(SourceInfo paintera) { LOG.info("Removed IntersectingSourceState {}", nameProperty().get()); meshManager.removeAllMeshes(); @@ -328,7 +349,7 @@ public void setMeshesEnabled(final boolean enabled) { public void refreshMeshes() { - this.meshManager.refreshMeshes(); + meshManager.refreshMeshes(); } public MeshManagerWithSingleMesh> getMeshManager() { @@ -385,9 +406,9 @@ private static > ObservableDataSource)seedRAI).getSource() instanceof CachedCellImg) { - var crai2 = (ConvertedRandomAccessibleInterval)seedRAI; - var cachedCellImg = (CachedCellImg)crai2.getSource(); + && ((ConvertedRandomAccessibleInterval) seedRAI).getSource() instanceof CachedCellImg) { + var crai2 = (ConvertedRandomAccessibleInterval) seedRAI; + var cachedCellImg = (CachedCellImg) crai2.getSource(); var grid = cachedCellImg.getCellGrid(); cellDimensions = new int[grid.numDimensions()]; grid.cellDimensions(cellDimensions); @@ -407,7 +428,7 @@ private static > ObservableDataSource vimg = TmpVolatileHelpers.createVolatileCachedCellImgWithInvalidate( - (CachedCellImg)img, + (CachedCellImg) img, queue, new CacheHints(LoadingStrategy.VOLATILE, priority, true)); @@ -474,7 +495,7 @@ private static > void addFillFromSeedsListener( } private static > boolean fillFromSeedPoints(RandomAccessibleInterval fillRAI, RandomAccessibleInterval img, - Point[] seedPointsCopy) { + Point[] seedPointsCopy) { LOG.debug("Filling from seed points"); boolean filledFromSeed = false; @@ -487,7 +508,7 @@ private static > boolean fillFromSeedPoints(RandomAcces seed, new UnsignedByteType(1), new DiamondShape(1), - (BiPredicate)(source, target) -> source.get() && target.get() == 0 + (BiPredicate) (source, target) -> source.get() && target.get() == 0 ); filledFromSeed = true; } @@ -496,7 +517,7 @@ private static > boolean fillFromSeedPoints(RandomAcces } private static > HashSet detectIntersectionPoints(RandomAccessibleInterval seedRAI, RandomAccessibleInterval fillRAI, - RandomAccessibleInterval cell) { + RandomAccessibleInterval cell) { LOG.trace( "Detecting seed point in cell {} {}", diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/ThresholdingSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/ThresholdingSourceState.java index 66247781c..22faef565 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/ThresholdingSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/ThresholdingSourceState.java @@ -11,6 +11,8 @@ import javafx.beans.value.ObservableDoubleValue; import javafx.scene.Node; import javafx.scene.paint.Color; +import kotlin.coroutines.EmptyCoroutineContext; +import kotlinx.coroutines.BuildersKt; import net.imglib2.Interval; import net.imglib2.Volatile; import net.imglib2.algorithm.util.Grids; @@ -165,17 +167,20 @@ public Threshold getThreshold() { return this.threshold; } - @Override public ObjectBinding getMeshCacheKeyBinding() { + @Override + public ObjectBinding getMeshCacheKeyBinding() { return meshCacheKeyProperty; } - @Override public GetBlockListFor getGetBlockListFor() { + @Override + public GetBlockListFor getGetBlockListFor() { return getGetBlockListForMeshCaheKey(); } - @Override public DataSource> getIntersectableMask() { + @Override + public DataSource> getIntersectableMask() { return getDataSource(); } @@ -217,7 +222,7 @@ public DoubleProperty maxProperty() { public MeshSettings getMeshSettings() { - return this.meshes.getSettings(); + return this.meshes.getGlobalSettings(); } public void refreshMeshes() { @@ -266,7 +271,14 @@ private void setMeshId() { private void setMeshId(final MeshManagerWithSingleMesh meshes) { - meshes.createMeshFor(getThresholdBounds()); + try { + BuildersKt.runBlocking( + EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> meshes.createMeshFor(getThresholdBounds(), continuation) + ); + } catch (InterruptedException e) { + LOG.debug("create mesh interrupted"); + } } public Bounds getThresholdBounds() { @@ -289,17 +301,20 @@ public ThresholdMeshCacheKey(final Bounds bounds) { this.bounds = bounds; } - @Override public int hashCode() { + @Override + public int hashCode() { return new HashCodeBuilder().append(bounds).toHashCode(); } - @Override public boolean equals(Object obj) { + @Override + public boolean equals(Object obj) { - return obj instanceof ThresholdMeshCacheKey && ((ThresholdMeshCacheKey)obj).bounds.equals(this.bounds); + return obj instanceof ThresholdMeshCacheKey && ((ThresholdMeshCacheKey) obj).bounds.equals(this.bounds); } - @Override public String toString() { + @Override + public String toString() { return "ThresholdMeshCacheKey: (" + bounds.toString() + ")"; } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshExportResult.java b/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshExportResult.java index b992d7394..e6f1d6244 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshExportResult.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshExportResult.java @@ -2,9 +2,12 @@ import org.janelia.saalfeldlab.paintera.meshes.MeshExporter; +import java.util.List; + public class MeshExportResult { private final MeshExporter meshExporter; + private final List meshKeys; private final String filePath; @@ -13,9 +16,19 @@ public class MeshExportResult { public MeshExportResult( final MeshExporter meshExporter, final String filePath, - final int scale) { + final int scale, + final T meshKey) { + this(meshExporter, filePath, scale, List.of(meshKey)); + } + + public MeshExportResult( + final MeshExporter meshExporter, + final String filePath, + final int scale, + final List meshKeys) { this.meshExporter = meshExporter; + this.meshKeys = meshKeys; this.filePath = filePath; this.scale = scale; } @@ -34,4 +47,8 @@ public int getScale() { return scale; } + + public List getMeshKeys() { + return meshKeys; + } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/SegmentMeshExporterDialog.java b/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshExporterDialog.java similarity index 80% rename from src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/SegmentMeshExporterDialog.java rename to src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshExporterDialog.java index 631e1d5b7..b4dab541c 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/SegmentMeshExporterDialog.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshExporterDialog.java @@ -4,23 +4,33 @@ import javafx.beans.binding.BooleanBinding; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import javafx.scene.control.*; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; +import javafx.scene.control.Spinner; +import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.stage.DirectoryChooser; import org.controlsfx.control.CheckListView; -import org.janelia.saalfeldlab.paintera.meshes.*; +import org.janelia.saalfeldlab.paintera.meshes.MeshExporter; +import org.janelia.saalfeldlab.paintera.meshes.MeshExporterBinary; +import org.janelia.saalfeldlab.paintera.meshes.MeshExporterObj; +import org.janelia.saalfeldlab.paintera.meshes.MeshInfo; +import org.janelia.saalfeldlab.paintera.meshes.MeshSettings; import org.jetbrains.annotations.NotNull; import java.io.File; import java.nio.file.Path; -import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -public class SegmentMeshExporterDialog extends Dialog> { +public class MeshExporterDialog extends Dialog> { public enum MeshFileFormat { @@ -29,39 +39,32 @@ public enum MeshFileFormat { final int LIST_CELL_HEIGHT = 25; - private final Spinner scale; + protected final Spinner scale; private final TextField filePathField; private final TextField meshFileNameField; - private MeshExporter meshExporter; - - private long[][] fragmentIds; - - private long[] segmentIds; + private MeshExporter meshExporter; private ComboBox fileFormats; - private CheckListView checkListView; + private CheckListView checkListView; private CheckBox selectAll; private final BooleanBinding isError; private static String previousFilePath = null; - public SegmentMeshExporterDialog(final SegmentMeshInfo meshInfo) { - + public MeshExporterDialog(MeshInfo meshInfo) { super(); - this.segmentIds = new long[]{meshInfo.segmentId()}; - this.fragmentIds = new long[][]{meshInfo.containedFragments()}; this.filePathField = new TextField(); if (previousFilePath != null) filePathField.setText(previousFilePath); this.meshFileNameField = new TextField(); - meshFileNameField.setText(meshInfo.segmentId().toString()); - this.setTitle("Export Mesh " + Arrays.toString(segmentIds)); + this.setTitle("Export Mesh "); this.isError = (Bindings.createBooleanBinding(() -> filePathField.getText().isEmpty() || meshFileNameField.getText().isEmpty() || pathExists(), filePathField.textProperty(), meshFileNameField.textProperty())); - final MeshSettings settings = meshInfo.getMeshSettings(); + final K meshKey = meshInfo.getKey(); + final MeshSettings settings = meshInfo.getManager().getManagedSettings().getMeshSettings(meshKey); this.scale = new Spinner<>(0, settings.getNumScaleLevels() - 1, settings.getFinestScaleLevel()); this.scale.setEditable(true); @@ -70,32 +73,19 @@ public SegmentMeshExporterDialog(final SegmentMeshInfo meshInfo) { return null; } previousFilePath = filePathField.getText(); - return new SegmentMeshExportResult<>( + + return new MeshExportResult<>( meshExporter, - fragmentIds, - segmentIds, + resolveFilePath(), scale.getValue(), - resolveFilePath() + meshKey ); }); createDialog(); - - } - - private boolean pathExists() { - - return Path.of(resolveFilePath()).toFile().exists(); - } - - @NotNull - private String resolveFilePath() { - final String file = Path.of(filePathField.getText(), meshFileNameField.getText()).toString(); - final String path = file.replace("~", System.getProperty("user.home")); - return path; } - public SegmentMeshExporterDialog(ObservableList meshInfoList) { + public MeshExporterDialog(ObservableList> meshInfoList) { super(); this.filePathField = new TextField(); @@ -103,8 +93,6 @@ public SegmentMeshExporterDialog(ObservableList meshInfoList) { filePathField.setText(previousFilePath); this.meshFileNameField = new TextField(); this.setTitle("Export mesh "); - this.segmentIds = new long[meshInfoList.size()]; - this.fragmentIds = new long[meshInfoList.size()][]; this.checkListView = new CheckListView<>(); this.selectAll = new CheckBox("Select All"); selectAll.selectedProperty().addListener((obs, oldv, selected) -> { @@ -124,15 +112,13 @@ public SegmentMeshExporterDialog(ObservableList meshInfoList) { checkListView.itemsProperty() )); - int minCommonScaleLevels = Integer.MAX_VALUE; + int minCommonScaleLevels = Integer.MAX_VALUE; int minCommonScale = Integer.MAX_VALUE; - final ObservableList ids = FXCollections.observableArrayList(); + final ObservableList ids = FXCollections.observableArrayList(); for (int i = 0; i < meshInfoList.size(); i++) { - final SegmentMeshInfo info = meshInfoList.get(i); - this.segmentIds[i] = info.segmentId(); - this.fragmentIds[i] = info.containedFragments(); + final MeshInfo info = meshInfoList.get(i); final MeshSettings settings = info.getMeshSettings(); - ids.add(info.segmentId()); + ids.add(info.getKey()); if (minCommonScaleLevels > settings.getNumScaleLevels()) { minCommonScaleLevels = settings.getNumScaleLevels(); @@ -151,16 +137,30 @@ public SegmentMeshExporterDialog(ObservableList meshInfoList) { return null; } previousFilePath = filePathField.getText(); - return new SegmentMeshExportResult<>( + + return new MeshExportResult<>( meshExporter, - fragmentIds, - segmentIds, + resolveFilePath(), scale.getValue(), - resolveFilePath() + checkListView.getCheckModel().getCheckedItems() ); }); - createMultiIdsDialog(ids); + createMultiKeyDialog(ids); + + + } + + private boolean pathExists() { + + return Path.of(resolveFilePath()).toFile().exists(); + } + + @NotNull + private String resolveFilePath() { + final String file = Path.of(filePathField.getText(), meshFileNameField.getText()).toString(); + final String path = file.replace("~", System.getProperty("user.home")); + return path; } private void createDialog() { @@ -186,13 +186,13 @@ private void createDialog() { this.getDialogPane().setContent(vbox); } - private void createMultiIdsDialog(final ObservableList ids) { + private void createMultiKeyDialog(final ObservableList keys) { final VBox vbox = new VBox(); final GridPane contents = new GridPane(); int row = createCommonDialog(contents); - checkListView.setItems(ids); + checkListView.setItems(keys); checkListView.getSelectionModel().selectAll(); selectAll.setSelected(true); checkListView.prefHeightProperty().bind(Bindings.size(checkListView.itemsProperty().get()).multiply(LIST_CELL_HEIGHT)); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/SegmentMeshExportResult.java b/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/SegmentMeshExportResult.java deleted file mode 100644 index 97fe144ad..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/SegmentMeshExportResult.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.janelia.saalfeldlab.paintera.ui.source.mesh; - -import org.janelia.saalfeldlab.paintera.meshes.MeshExporter; - -public class SegmentMeshExportResult { - - private final MeshExporter meshExporter; - - private final long[][] fragmentIds; - - private final long[] segmentId; - - private final String filePath; - - private final int scale; - - // TODO: change scale parameter when the interface allows to export - // different scales for different meshes at the same time - public SegmentMeshExportResult( - final MeshExporter meshExporter, - final long[][] fragmentIds, - final long[] segmentId, - final int scale, - final String filePath) { - - this.meshExporter = meshExporter; - this.fragmentIds = fragmentIds; - this.segmentId = segmentId; - this.filePath = filePath; - this.scale = scale; - } - - public MeshExporter getMeshExporter() { - - return meshExporter; - } - - public long[][] getFragmentIds() { - - return fragmentIds; - } - - public long[] getSegmentId() { - - return segmentId; - } - - public String getFilePath() { - - return filePath; - } - - public int getScale() { - - return scale; - } -} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/SegmentMeshInfoNode.java b/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/SegmentMeshInfoNode.java deleted file mode 100644 index 7900d5391..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/SegmentMeshInfoNode.java +++ /dev/null @@ -1,152 +0,0 @@ -package org.janelia.saalfeldlab.paintera.ui.source.mesh; - -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.geometry.VPos; -import javafx.scene.Node; -import javafx.scene.control.*; -import javafx.scene.layout.*; -import javafx.scene.paint.Color; -import net.imglib2.type.label.LabelMultisetType; -import org.fxmisc.flowless.VirtualizedScrollPane; -import org.fxmisc.richtext.InlineCssTextArea; -import org.janelia.saalfeldlab.fx.ui.NamedNode; -import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; -import org.janelia.saalfeldlab.paintera.data.DataSource; -import org.janelia.saalfeldlab.paintera.meshes.MeshExporter; -import org.janelia.saalfeldlab.paintera.meshes.MeshExporterObj; -import org.janelia.saalfeldlab.paintera.meshes.MeshSettings; -import org.janelia.saalfeldlab.paintera.meshes.SegmentMeshInfo; -import org.janelia.saalfeldlab.paintera.meshes.ui.MeshSettingsController; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.invoke.MethodHandles; -import java.util.Arrays; -import java.util.Optional; - -public class SegmentMeshInfoNode { - - private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private final DataSource source; - - private final SegmentMeshInfo meshInfo; - - private final Region contents; - - private final CheckBox hasIndividualSettings = new CheckBox("Individual Settings"); - - private final BooleanProperty isManaged = new SimpleBooleanProperty(); - - private final MeshSettingsController controller; - - { - hasIndividualSettings.selectedProperty().addListener((obs, oldv, newv) -> isManaged.set(!newv)); - isManaged.addListener((obs, oldv, newv) -> hasIndividualSettings.setSelected(!newv)); - isManaged.set(!hasIndividualSettings.isSelected()); - } - - private final MeshProgressBar progressBar = new MeshProgressBar(); - - private final MeshSettings settings; - - public SegmentMeshInfoNode(final DataSource source, final SegmentMeshInfo meshInfo) { - - this.source = source; - this.meshInfo = meshInfo; - this.settings = meshInfo.getMeshSettings(); - this.controller = new MeshSettingsController(this.settings); - - LOG.debug("Initializing MeshinfoNode with draw mode {}", settings.getDrawModeProperty()); - this.contents = createContents(); - } - - public Region getNode() { - - return contents; - } - - private Region createContents() { - - final TitledPane pane = new TitledPane(null, null); - pane.setExpanded(false); - pane.expandedProperty().addListener((obs, wasExpanded, isExpanded) -> { - if (isExpanded && pane.getContent() == null) { - InvokeOnJavaFXApplicationThread.invoke(() -> pane.setContent(getMeshInfoGrid())); - } - }); - - // TODO come up with better way to ensure proper size of this! - progressBar.setPrefWidth(200); - progressBar.setMinWidth(Control.USE_PREF_SIZE); - progressBar.setMaxWidth(Control.USE_PREF_SIZE); - progressBar.setText("" + meshInfo.segmentId()); - pane.setGraphic(progressBar); - - return pane; - } - - private GridPane getMeshInfoGrid() { - - final var grid = new GridPane(); - - final GridPane settingsGrid = controller.createContents(source.getDataType() instanceof LabelMultisetType); - final VBox individualSettingsBox = new VBox(hasIndividualSettings, settingsGrid); - individualSettingsBox.setSpacing(5.0); - settingsGrid.visibleProperty().bind(hasIndividualSettings.selectedProperty()); - settingsGrid.managedProperty().bind(settingsGrid.visibleProperty()); - hasIndividualSettings.setSelected(!meshInfo.isManagedProperty().get()); - isManaged.bindBidirectional(meshInfo.isManagedProperty()); - progressBar.bindTo(meshInfo.meshProgress()); - - final var ids = new InlineCssTextArea(Arrays.toString(meshInfo.containedFragments())); - final var virtualPane = new VirtualizedScrollPane<>(ids); - ids.setWrapText(true); - - final Label idsLabel = new Label("ids: "); - idsLabel.setMinWidth(30); - idsLabel.setMaxWidth(30); - final Node spacer = NamedNode.bufferNode(); - final HBox idsHeader = new HBox(idsLabel, spacer); - - final Button exportMeshButton = new Button("Export"); - exportMeshButton.setOnAction(event -> { - final SegmentMeshExporterDialog exportDialog = new SegmentMeshExporterDialog<>(meshInfo); - final Optional> result = exportDialog.showAndWait(); - if (result.isPresent()) { - final SegmentMeshExportResult parameters = result.get(); - final MeshExporter meshExporter = parameters.getMeshExporter(); - - final long id = parameters.getSegmentId()[0]; - final String filePath = parameters.getFilePath(); - final int scale = parameters.getScale(); - - if (meshExporter instanceof MeshExporterObj) { - ((MeshExporterObj) meshExporter).exportMaterial( - filePath, - new long[]{id}, - new Color[]{meshInfo.meshManager().getStateFor(id).getColor()}); - } - - meshExporter.exportMesh( - meshInfo.meshManager().getGetBlockListForLongKey(), - meshInfo.meshManager().getGetMeshForLongKey(), - meshInfo.getMeshSettings(), - id, - scale, - filePath, - false); - } - }); - - grid.add(idsHeader, 0, 0); - GridPane.setValignment(idsHeader, VPos.CENTER); - grid.add(virtualPane, 1, 0); - GridPane.setHgrow(virtualPane, Priority.ALWAYS); - grid.add(exportMeshButton, 0, 1, 2, 1); - grid.add(individualSettingsBox, 0, 2, 2, 1); - - return grid; - } -} diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/MeshInfo.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/MeshInfo.kt new file mode 100644 index 000000000..5fa7f5ef0 --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/MeshInfo.kt @@ -0,0 +1,23 @@ +package org.janelia.saalfeldlab.paintera.meshes + +import javafx.beans.property.BooleanProperty +import org.janelia.saalfeldlab.paintera.meshes.managed.MeshManager + +open class MeshInfo(val key: T, open val manager: MeshManager) { + val progressProperty: ObservableMeshProgress? + get() = manager.getStateFor(key)?.progress + + + override fun hashCode() = key.hashCode() + + override fun equals(obj: Any?): Boolean { + val aClass: Class?> = this.javaClass + return aClass == obj!!.javaClass && key == aClass.cast(obj)!!.key + } + + val meshSettings: MeshSettings + get() = manager.getSettings(key) + + val isManagedProperty: BooleanProperty + get() = manager.managedSettings.isManagedProperty(key) +} diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/MeshSettings.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/MeshSettings.kt index a6c7a7cc6..c92135785 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/MeshSettings.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/MeshSettings.kt @@ -22,6 +22,7 @@ class MeshSettings @JvmOverloads constructor( val cullFace: CullFace val isVisible: Boolean val minLabelRatio: Double + val overlap: Boolean val levelOfDetail: Int @@ -47,6 +48,9 @@ class MeshSettings @JvmOverloads constructor( @JvmStatic val isVisible = true + @JvmStatic + val overlap = true + @JvmStatic val minLabelRatio = 0.25 @@ -81,6 +85,7 @@ class MeshSettings @JvmOverloads constructor( override var cullFace: CullFace = Defaults.Values.cullFace override var isVisible: Boolean = Defaults.Values.isVisible override var minLabelRatio: Double = Defaults.Values.minLabelRatio + override var overlap: Boolean = Defaults.Values.overlap override var levelOfDetail: Int = Defaults.Values.levelOfDetail val asImmutable: Defaults @@ -100,6 +105,7 @@ class MeshSettings @JvmOverloads constructor( val cullFaceProperty: ObjectProperty = SimpleObjectProperty(defaults.cullFace) val isVisibleProperty: BooleanProperty = SimpleBooleanProperty(defaults.isVisible) val minLabelRatioProperty: DoubleProperty = SimpleDoubleProperty(defaults.minLabelRatio) + val overlapProperty: BooleanProperty = SimpleBooleanProperty(defaults.overlap) val levelOfDetailProperty: IntegerProperty = SimpleIntegerProperty(defaults.levelOfDetail) var coarsestScaleLevel by coarsestScaleLevelProperty.nonnull() @@ -112,6 +118,7 @@ class MeshSettings @JvmOverloads constructor( var cullFace by cullFaceProperty.nonnull() var isVisible by isVisibleProperty.nonnull() var minLabelRatio by minLabelRatioProperty.nonnull() + var overlap by overlapProperty.nonnull() var levelOfDetail by levelOfDetailProperty.nonnull() init { @@ -137,6 +144,7 @@ class MeshSettings @JvmOverloads constructor( smoothingLambda = that.smoothingLambda smoothingIterations = that.smoothingIterations minLabelRatio = that.minLabelRatio + overlap = that.overlap opacity = that.opacity drawMode = that.drawMode cullFace = that.cullFace @@ -160,6 +168,7 @@ class MeshSettings @JvmOverloads constructor( smoothingLambdaProperty.bind(that.smoothingLambdaProperty) smoothingIterationsProperty.bind(that.smoothingIterationsProperty) minLabelRatioProperty.bind(that.minLabelRatioProperty) + overlapProperty.bind(that.overlapProperty) opacityProperty.bind(that.opacityProperty) drawModeProperty.bind(that.drawModeProperty) cullFaceProperty.bind(that.cullFaceProperty) @@ -174,6 +183,7 @@ class MeshSettings @JvmOverloads constructor( smoothingLambdaProperty.unbind() smoothingIterationsProperty.unbind() minLabelRatioProperty.unbind() + overlapProperty.unbind() opacityProperty.unbind() drawModeProperty.unbind() cullFaceProperty.unbind() @@ -188,6 +198,7 @@ class MeshSettings @JvmOverloads constructor( smoothingLambdaProperty.bindBidirectional(that.smoothingLambdaProperty) smoothingIterationsProperty.bindBidirectional(that.smoothingIterationsProperty) minLabelRatioProperty.bindBidirectional(that.minLabelRatioProperty) + overlapProperty.bindBidirectional(that.overlapProperty) opacityProperty.bindBidirectional(that.opacityProperty) drawModeProperty.bindBidirectional(that.drawModeProperty) cullFaceProperty.bindBidirectional(that.cullFaceProperty) @@ -202,6 +213,7 @@ class MeshSettings @JvmOverloads constructor( smoothingLambdaProperty.unbindBidirectional(that.smoothingLambdaProperty) smoothingIterationsProperty.unbindBidirectional(that.smoothingIterationsProperty) minLabelRatioProperty.unbindBidirectional(that.minLabelRatioProperty) + overlapProperty.unbindBidirectional(that.overlapProperty) opacityProperty.unbindBidirectional(that.opacityProperty) drawModeProperty.unbindBidirectional(that.drawModeProperty) cullFaceProperty.unbindBidirectional(that.cullFaceProperty) @@ -214,6 +226,8 @@ class MeshSettings @JvmOverloads constructor( && simplificationIterations == defaults.simplificationIterations && smoothingLambda == defaults.smoothingLambda && smoothingIterations == defaults.smoothingIterations + && minLabelRatio == defaults.minLabelRatio + && overlap == defaults.overlap && opacity == defaults.opacity && defaults.drawMode == drawMode && defaults.cullFace == cullFace diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/SegmentMeshInfo.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/SegmentMeshInfo.kt new file mode 100644 index 000000000..530ad52a6 --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/SegmentMeshInfo.kt @@ -0,0 +1,14 @@ +package org.janelia.saalfeldlab.paintera.meshes + +import org.janelia.saalfeldlab.paintera.meshes.managed.MeshManagerWithAssignmentForSegments +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.lang.invoke.MethodHandles + +class SegmentMeshInfo(segmentId: Long, override val manager: MeshManagerWithAssignmentForSegments) : MeshInfo(segmentId, manager) { + + fun fragments(): LongArray { + return manager.getFragmentsFor(key).toArray() + } +} + diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/SegmentMeshInfoList.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/SegmentMeshInfoList.kt new file mode 100644 index 000000000..3a2e47889 --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/SegmentMeshInfoList.kt @@ -0,0 +1,39 @@ +package org.janelia.saalfeldlab.paintera.meshes + +import javafx.collections.FXCollections +import javafx.collections.ListChangeListener +import javafx.collections.ObservableList +import org.janelia.saalfeldlab.paintera.meshes.managed.MeshManagerWithAssignmentForSegments +import org.janelia.saalfeldlab.paintera.meshes.ui.MeshInfoList +import org.janelia.saalfeldlab.paintera.ui.source.mesh.SegmentMeshInfoNode + +class SegmentMeshInfoList( + selectedSegments: ObservableList, + val manager: MeshManagerWithAssignmentForSegments +) : MeshInfoList(FXCollections.observableArrayList(), manager) { + + init { + selectedSegments.addListener(ListChangeListener {change -> + if (manager.managedSettings.isMeshListEnabledProperty.get()) { + val toRemove = mutableSetOf() + val toAdd = mutableSetOf() + while (change.next()) { + val removed = change.removed + val added = change.addedSubList + + /* remove the outdated things */ + toRemove.removeAll(added) + toAdd.removeAll(removed) + + /* add the new things */ + toAdd.addAll(added) + toRemove.addAll(removed) + } + meshInfoList.removeIf { toRemove.contains(it.key) } + meshInfoList += toAdd.map { SegmentMeshInfo(it, manager) }.toSet() + } + }) + } + + override fun meshNodeFactory(meshInfo: SegmentMeshInfo) = SegmentMeshInfoNode(meshInfo) +} \ No newline at end of file diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/MeshManagerWithAssignmentForSegments.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/MeshManagerWithAssignmentForSegments.kt index 8484e9202..3b1d9ebd8 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/MeshManagerWithAssignmentForSegments.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/MeshManagerWithAssignmentForSegments.kt @@ -1,15 +1,14 @@ package org.janelia.saalfeldlab.paintera.meshes.managed +import com.google.common.collect.HashBiMap import gnu.trove.set.hash.TLongHashSet import javafx.beans.InvalidationListener -import javafx.beans.property.BooleanProperty import javafx.beans.property.ObjectProperty -import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleObjectProperty import javafx.beans.value.ObservableValue import javafx.collections.FXCollections -import javafx.scene.Group import javafx.scene.paint.Color +import kotlinx.coroutines.* import net.imglib2.FinalInterval import net.imglib2.Interval import net.imglib2.cache.Invalidate @@ -26,7 +25,6 @@ import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource import org.janelia.saalfeldlab.paintera.meshes.* import org.janelia.saalfeldlab.paintera.meshes.cache.SegmentMaskGenerators import org.janelia.saalfeldlab.paintera.meshes.cache.SegmentMeshCacheLoader -import org.janelia.saalfeldlab.paintera.meshes.managed.adaptive.AdaptiveResolutionMeshManager import org.janelia.saalfeldlab.paintera.state.label.FragmentLabelMeshCacheKey import org.janelia.saalfeldlab.paintera.stream.AbstractHighlightingARGBStream import org.janelia.saalfeldlab.paintera.viewer3d.ViewFrustum @@ -36,39 +34,49 @@ import org.janelia.saalfeldlab.util.NamedThreadFactory import org.janelia.saalfeldlab.util.concurrent.HashPriorityQueueBasedTaskExecutor import org.slf4j.LoggerFactory import java.lang.invoke.MethodHandles -import java.util.Arrays +import java.util.* import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import kotlin.coroutines.coroutineContext import kotlin.math.min +private typealias Segments = TLongHashSet +private typealias Fragments = TLongHashSet + /** * @author Philipp Hanslovsky * @author Igor Pisarev */ class MeshManagerWithAssignmentForSegments( source: DataSource<*, *>, - val labelBlockLookup: LabelBlockLookup, - private val getMeshFor: GetMeshFor, + getMeshFor: GetMeshFor, viewFrustumProperty: ObservableValue, eyeToWorldTransformProperty: ObservableValue, - private val selectedSegments: SelectedSegments, - private val argbStream: AbstractHighlightingARGBStream, - val managers: ExecutorService, - val workers: HashPriorityQueueBasedTaskExecutor, - val meshViewUpdateQueue: MeshViewUpdateQueue, -) { - - private class CancelableTask(private val task: (() -> Boolean) -> Unit) : Runnable { - - private var isCanceled: Boolean = false - - fun cancel() { - isCanceled = true + managers: ExecutorService, + workers: HashPriorityQueueBasedTaskExecutor, + meshViewUpdateQueue: MeshViewUpdateQueue, + val labelBlockLookup: LabelBlockLookup, + val selectedSegments: SelectedSegments, + val argbStream: AbstractHighlightingARGBStream, +) : MeshManager( + source, + GetBlockListFor { level, segment -> + val intervals = mutableSetOf>() + val fragments = selectedSegments.assignment.getFragments(segment) + fragments.forEach { id -> + labelBlockLookup.read(level, id).map { HashWrapper.interval(it) }.let { intervals.addAll(it) } + true } + intervals.map { it.data }.toTypedArray() + }, + getMeshFor, + viewFrustumProperty, + eyeToWorldTransformProperty, + managers, + workers, + meshViewUpdateQueue +) { - override fun run() = task { isCanceled } - - } private class RelevantBindingsAndProperties(private val key: Long, private val stream: AbstractHighlightingARGBStream) { private val color: ObjectProperty = SimpleObjectProperty(Color.WHITE) @@ -85,17 +93,8 @@ class MeshManagerWithAssignmentForSegments( "meshmanager-with-assignment-update-%d", true ) - ) - private var currentTask: CancelableTask? = null - - private val getBlockList: GetBlockListFor = GetBlockListFor { level, key -> - val intervals = mutableSetOf>() - key.forEach { id -> - labelBlockLookup.read(level, id).map { HashWrapper.interval(it) }.let { intervals.addAll(it) } - true - } - intervals.map { it.data }.toTypedArray() - } + ).asCoroutineDispatcher() + private var currentTask: Job? = null // setMeshesCompleted is only visible to enclosing manager if // _meshUpdateObservable is private @@ -106,194 +105,134 @@ class MeshManagerWithAssignmentForSegments( } val meshUpdateObservable = _meshUpdateObservable - private val segmentFragmentMap = - FXCollections.synchronizedObservableMap(FXCollections.observableHashMap()) - private val fragmentSegmentMap = - FXCollections.synchronizedObservableMap(FXCollections.observableHashMap()) - private val relevantBindingsAndPropertiesMap = - FXCollections.synchronizedObservableMap(FXCollections.observableHashMap()) - - private val viewerEnabled: SimpleBooleanProperty = SimpleBooleanProperty(false) - var isViewerEnabled: Boolean - get() = viewerEnabled.get() - set(enabled) = viewerEnabled.set(enabled) - - fun viewerEnabledProperty(): BooleanProperty = viewerEnabled - - private val manager: AdaptiveResolutionMeshManager = AdaptiveResolutionMeshManager( - source, - getBlockList, - getMeshFor, - viewFrustumProperty, - eyeToWorldTransformProperty, - viewerEnabled, - managers, - workers, - meshViewUpdateQueue - ) - - val rendererSettings get() = manager.rendererSettings - - val managedSettings = ManagedMeshSettings(source.numMipmapLevels).apply { rendererSettings.meshesEnabledProperty.bind(meshesEnabledProperty) } - - val settings: MeshSettings - get() = managedSettings.globalSettings - - val meshesGroup: Group - get() = manager.meshesGroup - - // TODO This listener is added to all mesh states. This is a problem if a lot of ids are selected - // and all use global mesh settings. Whenever the global mesh settings are changed, the - // managerCancelAndUpdate would be notified for each of the meshes, which can temporarily slow down - // the UI for quite some time (tens of seconds). A smarter way might be a single thread executor that - // executes only the last request and has a delay. - // This may be fixed now by using manager.requestCancelAndUpdate(), which submits a task - // to a LatestTaskExecutor with a delay of 100ms. - private val managerCancelAndUpdate = InvalidationListener { manager.requestCancelAndUpdate() } + private val segmentFragmentBiMap = HashBiMap.create() + private val relevantBindingsAndPropertiesMap = FXCollections.synchronizedObservableMap(FXCollections.observableHashMap()) + + override fun releaseMeshState(key: Long, state: MeshGenerator.State) { + synchronized(this) { + segmentFragmentBiMap.remove(key) + relevantBindingsAndPropertiesMap.remove(key)?.release() + super.releaseMeshState(key, state) + } + } - @Synchronized fun setMeshesToSelection() { + currentTask?.cancel() - currentTask = null - val task = CancelableTask { setMeshesToSelectionImpl(it) } - currentTask = task - updateExecutors.submit(task) + runBlocking { + currentTask = launch(updateExecutors) { setMeshesToSelectionImpl() } + } } - private fun setMeshesToSelectionImpl(isCanceled: () -> Boolean) { - if (isCanceled()) return + private suspend fun setMeshesToSelectionImpl() { + yield() - val (selection, presentKeys) = synchronized(this) { - val selection = TLongHashSet(selectedSegments.selectedSegments) - val presentKeys = TLongHashSet().also { set -> segmentFragmentMap.keys.forEach { set.add(it) } } - Pair(selection, presentKeys) + val (selectedSegments, presentSegments) = synchronized(this) { + val selectedSegments = Segments(selectedSegments.selectedSegments) + val presentSegments = Segments().also { set -> segmentFragmentBiMap.keys.forEach { set.add(it) } } + selectedSegments to presentSegments } // We need to collect all ids that are selected but not yet present in the 3d viewer and vice versa // to generate a diff and only apply the diff to the current mesh selection. // Additionally, segments that are inconsistent, i.e. if the set of fragments has changed for a segment // we need to replace it as well. - val presentButNotSelected = TLongHashSet() - val selectedButNotPresent = TLongHashSet() - val inconsistentIds = TLongHashSet() + val segmentsToRemove = Segments() + val segmentsToAdd = Segments() - selection.forEach { id -> - if (id !in presentKeys) - selectedButNotPresent.add(id) + val inconsistentIds = Segments() + + selectedSegments.forEach { segment -> + if (segment !in presentSegments) + segmentsToAdd.add(segment) true } - presentKeys.forEach { id -> - if (id !in selection) - presentButNotSelected.add(id) - else if (segmentFragmentMap[id]?.let { selectedSegments.assignment.isSegmentConsistent(id, it) } == false) - inconsistentIds.add(id) - true + synchronized(this) { + presentSegments.forEach { segment -> + if (segment !in selectedSegments) + segmentsToRemove.add(segment) + else if (segmentFragmentBiMap[segment]?.let { this.selectedSegments.assignment.isSegmentConsistent(segment, it) } == false) + inconsistentIds.add(segment) + true + } } - presentButNotSelected.addAll(inconsistentIds) - selectedButNotPresent.addAll(inconsistentIds) + + segmentsToRemove.addAll(inconsistentIds) + segmentsToAdd.addAll(inconsistentIds) // remove meshes that are present but not in selection - removeMeshesFor(presentButNotSelected.toArray().toList()) + removeMeshes(segmentsToRemove.toArray().toList()) // add meshes for all selected ids that are not present yet // removing mesh if is canceled is necessary because could be canceled between call to isCanceled.asBoolean and createMeshFor // another option would be to synchronize on a lock object but that is not necessary - for (id in selectedButNotPresent) { - if (isCanceled()) break - createMeshFor(id) - if (isCanceled()) removeMeshFor(id) - } + createMeshes(segmentsToAdd) - if (!isCanceled()) - manager.requestCancelAndUpdate() + manager.requestCancelAndUpdate() - if (!isCanceled()) - this._meshUpdateObservable.meshUpdateCompleted() + this._meshUpdateObservable.meshUpdateCompleted() } - private fun createMeshFor(key: Long) { - if (key in segmentFragmentMap) return - selectedSegments - .assignment - .getFragments(key) - ?.takeUnless { it.isEmpty } - ?.let { fragments -> - segmentFragmentMap[key] = fragments - fragmentSegmentMap[fragments] = key - manager.createMeshFor(fragments, false) { setupGeneratorState(key, it) } - } + private suspend fun createMeshes(segments: Segments) { + for (segment in segments) { + yield() + if (segment in segmentFragmentBiMap) return + selectedSegments.assignment.getFragments(segment) + ?.takeUnless { it.isEmpty } + ?.let { fragments -> + segmentFragmentBiMap[segment] = fragments + createMeshFor(segment) + } + } + } + + override suspend fun createMeshFor(key: Long) { + super.createMeshFor(key) + if (!coroutineContext.isActive) { + removeMesh(key) + } } - private fun setupGeneratorState(key: Long, state: MeshGenerator.State) { - state.settings.bindTo(managedSettings.getOrAddMesh(key, true)) + override fun setupMeshState(key: Long, state: MeshGenerator.State) { + state.settings.bindTo(managedSettings.getMeshSettings(key, true)) val relevantBindingsAndProperties = relevantBindingsAndPropertiesMap.computeIfAbsent(key) { RelevantBindingsAndProperties(key, argbStream) } state.colorProperty().bind(relevantBindingsAndProperties.colorProperty()) - state.settings.levelOfDetailProperty.addListener(managerCancelAndUpdate) - state.settings.coarsestScaleLevelProperty.addListener(managerCancelAndUpdate) - state.settings.finestScaleLevelProperty.addListener(managerCancelAndUpdate) - } + super.setupMeshState(key, state) - private fun MeshGenerator.State.release() { - settings.levelOfDetailProperty.removeListener(managerCancelAndUpdate) - settings.coarsestScaleLevelProperty.removeListener(managerCancelAndUpdate) - settings.finestScaleLevelProperty.removeListener(managerCancelAndUpdate) - settings.unbind() - colorProperty().unbind() } - private fun removeMeshFor(key: Long) { - segmentFragmentMap.remove(key)?.let { fragmentSet -> - fragmentSegmentMap.remove(fragmentSet) - manager.removeMeshFor(fragmentSet) { - it.release() - relevantBindingsAndPropertiesMap.remove(key)?.release() - } - } - } - - private fun removeMeshesFor(keys: Iterable) { - val fragmentSetKeys = keys.mapNotNull { key -> - relevantBindingsAndPropertiesMap.remove(key)?.release() - segmentFragmentMap.remove(key)?.also { fragmentSegmentMap.remove(it) } - } - manager.removeMeshesFor(fragmentSetKeys) { it.release() } + @Synchronized + override fun removeMesh(key: Long) { + super.removeMesh(key) + _meshUpdateObservable.meshUpdateCompleted() } @Synchronized - fun removeAllMeshes() { - currentTask?.cancel() - currentTask = null - val task = CancelableTask { removeAllMeshesImpl() } - updateExecutors.submit(task) - currentTask = task + override fun removeMeshes(keys: Iterable) { + super.removeMeshes(keys) + _meshUpdateObservable.meshUpdateCompleted() } - private fun removeAllMeshesImpl() { - removeMeshesFor(segmentFragmentMap.keys.toList()) + @Synchronized + override fun removeAllMeshes() { + super.removeAllMeshes() _meshUpdateObservable.meshUpdateCompleted() } - @Synchronized - fun refreshMeshes() { - currentTask?.cancel() - currentTask = null - val task = CancelableTask { isCanceled -> - this.removeAllMeshesImpl() - if (labelBlockLookup is Invalidate<*>) labelBlockLookup.invalidateAll() - if (getMeshFor is Invalidate<*>) getMeshFor.invalidateAll() - this.setMeshesToSelectionImpl(isCanceled) - } - updateExecutors.submit(task) - currentTask = task + override fun refreshMeshes() { + super.removeAllMeshes() + if (labelBlockLookup is Invalidate<*>) labelBlockLookup.invalidateAll() + if (getMeshFor is Invalidate<*>) getMeshFor.invalidateAll() + setMeshesToSelection() } companion object { private val LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()) - fun LabelBlockLookup.read(level: Int, id: Long) = read(LabelBlockLookupKey(level, id)) + fun LabelBlockLookup.read(level: Int, fragmentId: Long) = read(LabelBlockLookupKey(level, fragmentId)) @JvmStatic fun > fromBlockLookup( @@ -307,12 +246,12 @@ class MeshManagerWithAssignmentForSegments( meshWorkersExecutors: HashPriorityQueueBasedTaskExecutor, ): MeshManagerWithAssignmentForSegments { LOG.debug("Data source is type {}", dataSource.javaClass) - val actualLookup = when (dataSource) { - is MaskedSource -> LabeLBlockLookupWithMaskedSource.create(labelBlockLookup, dataSource) - else -> labelBlockLookup - } + val actualLookup = (dataSource as? MaskedSource) + ?.let { LabelBlockLookupWithMaskedSource.create(labelBlockLookup, it) } + ?: labelBlockLookup + // Set up mesh caches - val segmentMaskGenerators = Array(dataSource.numMipmapLevels) { SegmentMaskGenerators.create(dataSource, it) } + val segmentMaskGenerators = Array(dataSource.numMipmapLevels) { SegmentMaskGenerators.create(dataSource, it) { segment -> selectedSegments.assignment.getFragments(segment) } } val loaders = Array(dataSource.numMipmapLevels) { SegmentMeshCacheLoader( { dataSource.getDataSource(0, it) }, @@ -324,27 +263,27 @@ class MeshManagerWithAssignmentForSegments( return MeshManagerWithAssignmentForSegments( dataSource, - actualLookup, getMeshFor, viewFrustumProperty, eyeToWorldTransformProperty, - selectedSegments, - argbStream, meshManagerExecutors, meshWorkersExecutors, - MeshViewUpdateQueue() + MeshViewUpdateQueue(), + actualLookup, + selectedSegments, + argbStream ) } } - private class CachedLabeLBlockLookupWithMaskedSource>( + private class CachedLabelBlockLookupWithMaskedSource>( private val delegate: CachedLabelBlockLookup, private val maskedSource: MaskedSource, ) : - LabeLBlockLookupWithMaskedSource(delegate, maskedSource), + LabelBlockLookupWithMaskedSource(delegate, maskedSource), Invalidate by delegate - private open class LabeLBlockLookupWithMaskedSource>( + private open class LabelBlockLookupWithMaskedSource>( private val delegate: LabelBlockLookup, private val maskedSource: MaskedSource, ) : LabelBlockLookup by delegate { @@ -359,8 +298,8 @@ class MeshManagerWithAssignmentForSegments( private val LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()) fun > create(delegate: LabelBlockLookup, maskedSource: MaskedSource) = when (delegate) { - is CachedLabelBlockLookup -> CachedLabeLBlockLookupWithMaskedSource(delegate, maskedSource) - else -> LabeLBlockLookupWithMaskedSource(delegate, maskedSource) + is CachedLabelBlockLookup -> CachedLabelBlockLookupWithMaskedSource(delegate, maskedSource) + else -> LabelBlockLookupWithMaskedSource(delegate, maskedSource) } private fun affectedBlocksForLabel(source: MaskedSource<*, *>, level: Int, id: Long): Array { @@ -388,30 +327,34 @@ class MeshManagerWithAssignmentForSegments( } - @Synchronized - fun getStateFor(key: Long) = segmentFragmentMap[key]?.let { manager.getStateFor(it) } - - fun getContainedFragmentsFor(key: Long) = segmentFragmentMap[key] + fun getFragmentsFor(segment: Long): Fragments = segmentFragmentBiMap[segment] ?: selectedSegments.assignment.getFragments(segment) - val getBlockListForLongKey: GetBlockListFor - get() = GetBlockListFor { level, key -> - getBlockList.getBlocksFor(level, getContainedFragmentsFor(key) ?: TLongHashSet()) + val getBlockListForSegment: GetBlockListFor + get() = GetBlockListFor { level, segment -> + getBlockListFor.getBlocksFor(level, segment) } - val getBlockListForMeshCacheKey: GetBlockListFor = GetBlockListFor { level, key -> - getBlockList.getBlocksFor(level, key.fragments) + val getBlockListForFragments: GetBlockListFor = GetBlockListFor { level, key -> + val intervals = mutableSetOf>() + val fragments = key.fragments + fragments.forEach { id -> + labelBlockLookup.read(level, id).map { HashWrapper.interval(it) }.let { intervals.addAll(it) } + true + } + intervals.map { it.data }.toTypedArray() } val getMeshForLongKey: GetMeshFor get() = object : GetMeshFor { override fun getMeshFor(key: ShapeKey): PainteraTriangleMesh? = getMeshFor.getMeshFor( ShapeKey( - getContainedFragmentsFor(key.shapeId()) ?: TLongHashSet(), + key.shapeId(), key.scaleIndex(), key.simplificationIterations(), key.smoothingLambda(), key.smoothingIterations(), key.minLabelRatio(), + key.overlap(), key.min(), key.max() ) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/MeshManagerWithSingleMesh.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/MeshManagerWithSingleMesh.kt index 60daea178..b0790b65d 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/MeshManagerWithSingleMesh.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/MeshManagerWithSingleMesh.kt @@ -1,5 +1,6 @@ package org.janelia.saalfeldlab.paintera.meshes.managed +import io.github.oshai.kotlinlogging.KotlinLogging import javafx.beans.InvalidationListener import javafx.beans.property.BooleanProperty import javafx.beans.property.ObjectProperty @@ -8,25 +9,25 @@ import javafx.beans.property.SimpleObjectProperty import javafx.beans.value.ObservableValue import javafx.scene.Group import javafx.scene.paint.Color +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import net.imglib2.cache.Invalidate import net.imglib2.realtransform.AffineTransform3D import org.janelia.saalfeldlab.fx.extensions.nonnull import org.janelia.saalfeldlab.paintera.data.DataSource -import org.janelia.saalfeldlab.paintera.meshes.* +import org.janelia.saalfeldlab.paintera.meshes.ManagedMeshSettings +import org.janelia.saalfeldlab.paintera.meshes.MeshGenerator.State +import org.janelia.saalfeldlab.paintera.meshes.MeshSettings +import org.janelia.saalfeldlab.paintera.meshes.MeshViewUpdateQueue +import org.janelia.saalfeldlab.paintera.meshes.MeshWorkerPriority import org.janelia.saalfeldlab.paintera.meshes.managed.adaptive.AdaptiveResolutionMeshManager import org.janelia.saalfeldlab.paintera.viewer3d.ViewFrustum import org.janelia.saalfeldlab.util.concurrent.HashPriorityQueueBasedTaskExecutor -import org.slf4j.LoggerFactory -import java.lang.invoke.MethodHandles import java.util.concurrent.ExecutorService -/** - * @author Philipp Hanslovsky - * @author Igor Pisarev - */ -class MeshManagerWithSingleMesh( - source: DataSource<*, *>, - val getBlockList: GetBlockListFor, +abstract class MeshManager( + val source: DataSource<*, *>, + val getBlockListFor: GetBlockListFor, val getMeshFor: GetMeshFor, viewFrustumProperty: ObservableValue, eyeToWorldTransformProperty: ObservableValue, @@ -35,19 +36,16 @@ class MeshManagerWithSingleMesh( meshViewUpdateQueue: MeshViewUpdateQueue, ) { - var meshKey: Key? = null - @Synchronized get - @Synchronized private set + companion object { + val LOG = KotlinLogging.logger { } + } val viewerEnabledProperty: BooleanProperty = SimpleBooleanProperty(false) var isViewerEnabled: Boolean by viewerEnabledProperty.nonnull() - val colorProperty: ObjectProperty = SimpleObjectProperty(Color.WHITE) - var color: Color by colorProperty.nonnull() - - private val manager: AdaptiveResolutionMeshManager = AdaptiveResolutionMeshManager( + protected val manager: AdaptiveResolutionMeshManager = AdaptiveResolutionMeshManager( source, - getBlockList, + getBlockListFor, getMeshFor, viewFrustumProperty, eyeToWorldTransformProperty, @@ -57,63 +55,142 @@ class MeshManagerWithSingleMesh( meshViewUpdateQueue ) - val rendererSettings = manager.rendererSettings - val settings: MeshSettings = MeshSettings(source.numMipmapLevels) + val managedSettings = ManagedMeshSettings(source.numMipmapLevels).apply { rendererSettings.meshesEnabledProperty.bind(meshesEnabledProperty) } - val managedSettings = ManagedMeshSettings(source.numMipmapLevels).apply { rendererSettings.meshesEnabledProperty.bind(meshesEnabledProperty) } + val globalSettings: MeshSettings = managedSettings.globalSettings val meshesGroup: Group = manager.meshesGroup - private val managerCancelAndUpdate = InvalidationListener { manager.requestCancelAndUpdate() } + // TODO This listener is added to all mesh states. This is a problem if a lot of ids are selected + // and all use global mesh settings. Whenever the global mesh settings are changed, the + // managerCancelAndUpdate would be notified for each of the meshes, which can temporarily slow down + // the UI for quite some time (tens of seconds). A smarter way might be a single thread executor that + // executes only the last request and has a delay. + // This may be fixed now by using manager.requestCancelAndUpdate(), which submits a task + // to a LatestTaskExecutor with a delay of 100ms. + protected val managerCancelAndUpdate = InvalidationListener { manager.requestCancelAndUpdate() } + + open fun getStateFor(key: Key): State? { + return manager.getStateFor(key) + } + + fun getSettings(key: Key): MeshSettings { + return managedSettings.getMeshSettings(key) + } + + open suspend fun createMeshFor(key: Key) { + manager.createMeshFor(key, true, stateSetup = ::setupMeshState) + } + + protected open fun setupMeshState(key: Key, state: State) { + LOG.debug { "Setting up state for mesh key $key" } + with(globalSettings) { + levelOfDetailProperty.addListener(managerCancelAndUpdate) + coarsestScaleLevelProperty.addListener(managerCancelAndUpdate) + finestScaleLevelProperty.addListener(managerCancelAndUpdate) + } + } @Synchronized - fun createMeshFor(key: Key) { - if (key == meshKey) - return - this.removeAllMeshes() - meshKey = key - manager.createMeshFor(key, true) { it.setup() } + protected open fun releaseMeshState(key: Key, state: State) { + state.colorProperty().unbind() + with(globalSettings) { + unbind() + levelOfDetailProperty.removeListener(managerCancelAndUpdate) + coarsestScaleLevelProperty.removeListener(managerCancelAndUpdate) + finestScaleLevelProperty.removeListener(managerCancelAndUpdate) + } } @Synchronized - fun removeAllMeshes() { - manager.removeAllMeshes { it.release() } - meshKey = null + open fun removeAllMeshes() { + manager.removeAllMeshes { key, state -> releaseMeshState(key, state) } } @Synchronized - fun refreshMeshes() { - val key = meshKey + open fun removeMesh(key: Key) { + manager.removeMeshFor(key) { meshKey, state -> releaseMeshState(meshKey, state) } + } + + @Synchronized + open fun removeMeshes(keys: Iterable) { + manager.removeMeshesFor(keys) { key, state -> releaseMeshState(key, state) } + } + + open fun refreshMeshes() { + val currentMeshKeys = manager.allMeshKeys this.removeAllMeshes() if (getMeshFor is Invalidate<*>) getMeshFor.invalidateAll() - key?.let { createMeshFor(it) } + runBlocking { + launch { + currentMeshKeys.forEach { + createMeshFor(it) + } + } + } } +} - private fun MeshGenerator.State.setup() = setupGeneratorState(this) - @Synchronized - private fun setupGeneratorState(state: MeshGenerator.State) { - LOG.debug("Setting up state for mesh key {}", meshKey) - state.colorProperty().bind(colorProperty) - state.settings.bindTo(settings) - state.settings.levelOfDetailProperty.addListener(managerCancelAndUpdate) - state.settings.coarsestScaleLevelProperty.addListener(managerCancelAndUpdate) - state.settings.finestScaleLevelProperty.addListener(managerCancelAndUpdate) +/** + * @author Philipp Hanslovsky + * @author Igor Pisarev + */ +class MeshManagerWithSingleMesh( + source: DataSource<*, *>, + getBlockList: GetBlockListFor, + getMeshFor: GetMeshFor, + viewFrustumProperty: ObservableValue, + eyeToWorldTransformProperty: ObservableValue, + managers: ExecutorService, + workers: HashPriorityQueueBasedTaskExecutor, + meshViewUpdateQueue: MeshViewUpdateQueue +) : MeshManager( + source, + getBlockList, + getMeshFor, + viewFrustumProperty, + eyeToWorldTransformProperty, + managers, + workers, + meshViewUpdateQueue +) { + + var meshKey: Key? = null + @Synchronized get + @Synchronized private set + + val colorProperty: ObjectProperty = SimpleObjectProperty(Color.WHITE) + var color: Color by colorProperty.nonnull() + + override suspend fun createMeshFor(key: Key) { + if (key == meshKey) + return + this.removeAllMeshes() + meshKey = key + super.createMeshFor(key) + + } + + override fun setupMeshState(key: Key, state: State) { + with(state) { + colorProperty().bind(colorProperty) + settings.bindTo(this@MeshManagerWithSingleMesh.globalSettings) + } + super.setupMeshState(key, state) } @Synchronized - private fun MeshGenerator.State.release() { - colorProperty().unbind() - settings.unbind() - settings.levelOfDetailProperty.removeListener(managerCancelAndUpdate) - settings.coarsestScaleLevelProperty.removeListener(managerCancelAndUpdate) - settings.finestScaleLevelProperty.removeListener(managerCancelAndUpdate) + override fun removeAllMeshes() { + meshKey?.let { removeMesh(it) } } - companion object { - private val LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()) + @Synchronized + override fun removeMesh(key: Key) { + meshKey = null + super.removeMesh(key) } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/adaptive/AdaptiveResolutionMeshManager.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/adaptive/AdaptiveResolutionMeshManager.kt index fa4bd3a42..38efef920 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/adaptive/AdaptiveResolutionMeshManager.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/adaptive/AdaptiveResolutionMeshManager.kt @@ -23,11 +23,12 @@ import org.janelia.saalfeldlab.util.concurrent.HashPriorityQueueBasedTaskExecuto import org.janelia.saalfeldlab.util.concurrent.LatestTaskExecutor import org.slf4j.LoggerFactory import java.lang.invoke.MethodHandles -import java.util.Collections +import java.util.* import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.Future import java.util.concurrent.RejectedExecutionException +import java.util.function.BiConsumer import java.util.function.BooleanSupplier import java.util.function.Consumer @@ -35,7 +36,7 @@ import java.util.function.Consumer * @author Philipp Hanslovsky * @author Igor Pisarev */ -class AdaptiveResolutionMeshManager constructor( +class AdaptiveResolutionMeshManager( private val source: DataSource<*, *>, private val getBlockListFor: GetBlockListFor, private val getMeshFor: GetMeshFor, @@ -115,25 +116,25 @@ class AdaptiveResolutionMeshManager constructor( @Synchronized private fun replaceMesh(key: ObjectKey, cancelAndUpdate: Boolean) { - val state = removeMeshFor(key) { } + val state = removeMeshFor(key) { _, _ -> } state - ?.let { s -> createMeshFor(key, cancelAndUpdate = cancelAndUpdate, state = s, stateSetup = { }) } - ?: createMeshFor(key, cancelAndUpdate = cancelAndUpdate, stateSetup = { }) + ?.let { s -> createMeshFor(key, cancelAndUpdate = cancelAndUpdate, state = s, stateSetup = { _, _ -> }) } + ?: createMeshFor(key, cancelAndUpdate = cancelAndUpdate, stateSetup = { _, _ -> }) } @Synchronized private fun replaceAllMeshes() = allMeshKeys.map { replaceMesh(it, false) }.also { cancelAndUpdate() } - fun removeMeshFor(key: ObjectKey, releaseState: (MeshGenerator.State) -> Unit) = removeMeshFor(key, Consumer { releaseState(it) }) + fun removeMeshFor(key: ObjectKey, releaseState: (ObjectKey, MeshGenerator.State) -> Unit) = removeMeshFor(key, BiConsumer { key, state -> releaseState(key, state) }) @Synchronized - fun removeMeshFor(key: ObjectKey, releaseState: Consumer): MeshGenerator.State? { + fun removeMeshFor(key: ObjectKey, releaseState: BiConsumer): MeshGenerator.State? { return meshes.remove(key)?.let { generator -> unbindService.submit { generator.interrupt() generator.unbindFromThis() generator.root.visibleProperty().unbind() - releaseState.accept(generator.state) + releaseState.accept(key, generator.state) Platform.runLater { generator.root.isVisible = false meshesGroup.children -= generator.root @@ -143,18 +144,20 @@ class AdaptiveResolutionMeshManager constructor( } } - fun removeMeshesFor(keys: Iterable, releaseState: (MeshGenerator.State) -> Unit) = removeMeshesFor(keys, Consumer { releaseState(it) }) + fun removeMeshesFor(keys: Iterable, releaseState: (ObjectKey, MeshGenerator.State) -> Unit) = removeMeshesFor(keys, BiConsumer { key, state -> releaseState(key, state) }) @Synchronized - fun removeMeshesFor(keys: Iterable, releaseState: Consumer) { - val generators = synchronized(this) { keys.map { meshes.remove(it) } } + fun removeMeshesFor(keys: Iterable, releaseState: BiConsumer) { + val keysAndGenerators = synchronized(this) { keys.map { it to meshes.remove(it) } } unbindService.submit { - val roots = generators.mapNotNull { generator -> - generator?.interrupt() - generator?.unbindFromThis() - generator?.root?.visibleProperty()?.unbind() - generator?.let { releaseState.accept(it.state) } - generator?.root + val roots = keysAndGenerators.mapNotNull { (key, generator) -> + generator?.run { + interrupt() + unbindFromThis() + root?.visibleProperty()?.unbind() + let { releaseState.accept(key, state) } + root + } } Platform.runLater { // The roots list has to be converted to array first and then passed as vararg @@ -165,16 +168,16 @@ class AdaptiveResolutionMeshManager constructor( } } - fun removeAllMeshes(releaseState: (MeshGenerator.State) -> Unit) = removeAllMeshes(Consumer { releaseState(it) }) + fun removeAllMeshes(releaseState: (ObjectKey, MeshGenerator.State) -> Unit) = removeAllMeshes(BiConsumer { key, state -> releaseState(key, state) }) - fun removeAllMeshes(releaseState: Consumer) = removeMeshesFor(allMeshKeys, releaseState) + fun removeAllMeshes(releaseState: BiConsumer) = removeMeshesFor(allMeshKeys, releaseState) fun createMeshFor( key: ObjectKey, cancelAndUpdate: Boolean, state: MeshGenerator.State = MeshGenerator.State(), - stateSetup: (MeshGenerator.State) -> Unit - ) = createMeshFor(key, cancelAndUpdate, state, Consumer { stateSetup(it) }) + stateSetup: (ObjectKey, MeshGenerator.State) -> Unit + ) = createMeshFor(key, cancelAndUpdate, state, Consumer { stateSetup(key, it) }) @JvmOverloads fun createMeshFor( 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 0537b709e..1f1c797e4 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,27 +1,33 @@ package org.janelia.saalfeldlab.paintera.meshes.ui -import javafx.beans.property.BooleanProperty -import javafx.beans.property.DoubleProperty -import javafx.beans.property.IntegerProperty -import javafx.beans.property.Property +import javafx.beans.property.* import javafx.collections.FXCollections +import javafx.collections.ObservableList import javafx.event.EventHandler import javafx.geometry.HPos import javafx.geometry.Pos import javafx.scene.Node import javafx.scene.control.* import javafx.scene.layout.* +import javafx.scene.paint.Color import javafx.scene.shape.CullFace import javafx.scene.shape.DrawMode import javafx.stage.Modality +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.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.MeshManager import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts import org.janelia.saalfeldlab.paintera.ui.RefreshButton +import org.janelia.saalfeldlab.paintera.ui.source.mesh.MeshExporterDialog +import org.janelia.saalfeldlab.paintera.ui.source.mesh.MeshProgressBar import kotlin.math.max import kotlin.math.min @@ -34,6 +40,7 @@ class MeshSettingsController @JvmOverloads constructor( private val smoothingLambda: DoubleProperty, private val smoothingIterations: IntegerProperty, private val minLabelRatio: DoubleProperty, + private val overlap: BooleanProperty, private val drawMode: Property, private val cullFace: Property, private val isVisible: BooleanProperty, @@ -50,15 +57,14 @@ class MeshSettingsController @JvmOverloads constructor( meshSettings.smoothingLambdaProperty, meshSettings.smoothingIterationsProperty, meshSettings.minLabelRatioProperty, + meshSettings.overlapProperty, meshSettings.drawModeProperty, meshSettings.cullFaceProperty, meshSettings.isVisibleProperty, refreshMeshes ) - fun createContents( - addMinLabelRatioSlider: Boolean - ): GridPane { + fun createContents(addMinLabelRatioSlider: Boolean): GridPane { return GridPane().populateGridWithMeshSettings( addMinLabelRatioSlider, CheckBox().also { it.selectedProperty().bindBidirectional(isVisible) }, @@ -75,6 +81,7 @@ class MeshSettingsController @JvmOverloads constructor( NumericSliderWithField(0.0, 1.00, 1.0).apply { slider.valueProperty().bindBidirectional(smoothingLambda) }, NumericSliderWithField(0, 10, 1).apply { slider.valueProperty().bindBidirectional(smoothingIterations) }, NumericSliderWithField(0.0, 1.0, 0.5).apply { slider.valueProperty().bindBidirectional(minLabelRatio) }, + CheckBox().also { it.selectedProperty().bindBidirectional(overlap) }, ComboBox(FXCollections.observableArrayList(*DrawMode.values())).apply { valueProperty().bindBidirectional(drawMode) }, ComboBox(FXCollections.observableArrayList(*CullFace.values())).apply { valueProperty().bindBidirectional(cullFace) }) } @@ -115,6 +122,7 @@ class MeshSettingsController @JvmOverloads constructor( alignment = Pos.CENTER } + return TitledPane("", contents).apply { minWidthProperty().set(0.0) isExpanded = false @@ -151,6 +159,7 @@ class MeshSettingsController @JvmOverloads constructor( smoothingLambdaSlider: NumericSliderWithField, smoothingIterationsSlider: NumericSliderWithField, minLabelRatioSlider: NumericSliderWithField, + overlapToggle: CheckBox, drawModeChoice: ComboBox, cullFaceChoice: ComboBox, ): GridPane { @@ -180,10 +189,11 @@ 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) } + addGridOption("Overlap", overlapToggle) addGridOption("Draw Mode", drawModeChoice) addGridOption("Cull Face", cullFaceChoice) return this @@ -245,5 +255,126 @@ class MeshSettingsController @JvmOverloads constructor( } } } +} + +open class MeshInfoPane(private val meshInfo: MeshInfo) : TitledPane(null, null) { + + private val hasIndividualSettings = CheckBox("Individual Settings") + private var isManaged = SimpleBooleanProperty() + private var controller: MeshSettingsController = MeshSettingsController(meshInfo.meshSettings) + private var progressBar = MeshProgressBar() + + init { + hasIndividualSettings.selectedProperty().addListener { _, _, newv -> isManaged.set(!newv) } + isManaged.addListener { _, _, newv -> hasIndividualSettings.isSelected = !newv } + isManaged.set(!hasIndividualSettings.isSelected) + isExpanded = false + expandedProperty().addListener { _, _, isExpanded -> + if (isExpanded && content == null) { + content = createMeshInfoGrid() + } + } + progressBar.prefWidth = 200.0 + progressBar.minWidth = Control.USE_PREF_SIZE + progressBar.maxWidth = Control.USE_PREF_SIZE + progressBar.text = "" + meshInfo.key + progressBar.bindTo(meshInfo.progressProperty) + graphic = progressBar + } + + protected open fun createMeshInfoGrid(grid: GridPane = GridPane()): GridPane { + + val settingsGrid = controller.createContents(meshInfo.manager.source.getDataType() is LabelMultisetType) + val individualSettingsBox = VBox(hasIndividualSettings, settingsGrid) + individualSettingsBox.spacing = 5.0 + settingsGrid.visibleProperty().bind(hasIndividualSettings.selectedProperty()) + settingsGrid.managedProperty().bind(settingsGrid.visibleProperty()) + hasIndividualSettings.isSelected = !meshInfo.isManagedProperty.get() + isManaged.bindBidirectional(meshInfo.isManagedProperty) + return grid.apply { + add(createExportMeshButton(), 0, rowCount, 2, 1) + add(individualSettingsBox, 0, rowCount, 2, 1) + } + } + + private fun createExportMeshButton(): Button { + val exportMeshButton = Button("Export") + exportMeshButton.setOnAction { event -> + val exportDialog = MeshExporterDialog(meshInfo) + val result = exportDialog.showAndWait() + if (!result.isPresent) return@setOnAction + + val parameters = result.get() + 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( + filePath, + arrayOf(ids[0].toString()), + 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 + } +} + +abstract class MeshInfoList, K>( + protected val meshInfoList: ObservableList = FXCollections.observableArrayList(), + manager: MeshManager +) : ListView(meshInfoList) { + + + val meshInfos: ReadOnlyListWrapper = ReadOnlyListWrapper(meshInfoList) + + init { + setCellFactory { MeshInfoListCell() } + manager.managedSettings.isMeshListEnabledProperty.addListener { _, _, enabled -> + if (!enabled) { + itemsProperty().set(FXCollections.emptyObservableList()) + } else { + itemsProperty().set(meshInfoList) + } + } + } + + open fun meshNodeFactory(meshInfo: T): Node = MeshInfoPane(meshInfo) + + private inner class MeshInfoListCell : ListCell() { + + init { + style = "-fx-padding: 0px" + } + + override fun updateItem(item: T?, empty: Boolean) { + super.updateItem(item, empty) + text = null + if (empty || item == null) { + graphic = null + } else { + InvokeOnJavaFXApplicationThread { + graphic = meshNodeFactory(item).apply { + prefWidthProperty().addListener { _, _, pref -> prefWidth = pref.toDouble() } + } + } + } + } + } } 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 5b09c4f9d..e05d39dca 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/IntersectingSourceStatePreferencePaneNode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/IntersectingSourceStatePreferencePaneNode.kt @@ -1,10 +1,18 @@ package org.janelia.saalfeldlab.paintera.state +import javafx.geometry.Pos import javafx.scene.Node +import javafx.scene.control.Button import javafx.scene.control.ColorPicker +import javafx.scene.layout.HBox +import javafx.scene.layout.VBox import org.janelia.saalfeldlab.fx.extensions.createNullableValueBinding +import org.janelia.saalfeldlab.paintera.meshes.MeshExporterObj +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.ui.source.mesh.MeshExporterDialog import org.janelia.saalfeldlab.util.Colors class IntersectingSourceStatePreferencePaneNode(private val state: IntersectingSourceState<*, *>) { @@ -12,7 +20,7 @@ class IntersectingSourceStatePreferencePaneNode(private val state: IntersectingS val node: Node get() { val manager = state.meshManager - val settings = manager.settings + val settings = manager.globalSettings return SourceState.defaultPreferencePaneNode(state.compositeProperty()).apply { children += MeshSettingsController(settings, state::refreshMeshes).createTitledPane( @@ -29,7 +37,36 @@ class IntersectingSourceStatePreferencePaneNode(private val state: IntersectingS } addGridOption("Color", colorPicker) + }.also { + it.content = VBox(it.content).apply { + val exportMeshButton = Button("Export all") + exportMeshButton.setOnAction { _ -> + val manager = state.meshManager as MeshManagerWithSingleMesh> + val key = manager.meshKey!! + val exportDialog = MeshExporterDialog(MeshInfo(key, manager)) + val result = exportDialog.showAndWait() + if (result.isPresent) { + result.get().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 + ) + } + } + } + val buttonBox = HBox(exportMeshButton).also { it.alignment = Pos.BOTTOM_RIGHT } + children += buttonBox + + } } } } -} +} \ No newline at end of file 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 feabb2660..943b051dc 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMeshPaneNode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMeshPaneNode.kt @@ -2,6 +2,7 @@ package org.janelia.saalfeldlab.paintera.state import javafx.beans.property.ReadOnlyListProperty import javafx.collections.ListChangeListener +import javafx.collections.ObservableList import javafx.event.EventHandler import javafx.geometry.Insets import javafx.geometry.Orientation @@ -15,19 +16,16 @@ import javafx.scene.paint.Color import javafx.stage.Modality import net.imglib2.type.label.LabelMultisetType import org.janelia.saalfeldlab.fx.extensions.TitledPaneExtensions +import org.janelia.saalfeldlab.fx.extensions.TitledPaneExtensions.Companion.expandIfEnabled +import org.janelia.saalfeldlab.fx.extensions.TitledPaneExtensions.Companion.graphicsOnly import org.janelia.saalfeldlab.fx.extensions.createNonNullValueBinding -import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.paintera.data.DataSource -import org.janelia.saalfeldlab.paintera.meshes.GlobalMeshProgress -import org.janelia.saalfeldlab.paintera.meshes.MeshExporterObj -import org.janelia.saalfeldlab.paintera.meshes.SegmentMeshInfo -import org.janelia.saalfeldlab.paintera.meshes.SegmentMeshInfos +import org.janelia.saalfeldlab.paintera.meshes.* import org.janelia.saalfeldlab.paintera.meshes.managed.MeshManagerWithAssignmentForSegments import org.janelia.saalfeldlab.paintera.meshes.ui.MeshSettingsController 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.janelia.saalfeldlab.paintera.ui.source.mesh.SegmentMeshExporterDialog -import org.janelia.saalfeldlab.paintera.ui.source.mesh.SegmentMeshInfoNode import org.slf4j.LoggerFactory import java.lang.invoke.MethodHandles import java.util.* @@ -38,14 +36,14 @@ typealias TPE = TitledPaneExtensions class LabelSourceStateMeshPaneNode( private val source: DataSource<*, *>, private val manager: MeshManagerWithAssignmentForSegments, - private val meshInfos: SegmentMeshInfos, + private val meshInfos: SegmentMeshInfoList, ) { val node: Node get() = makeNode() private fun makeNode(): Node { - val settings = manager.settings + val settings = manager.globalSettings val tp = MeshSettingsController(settings, manager::refreshMeshes).createTitledPane( source.dataType is LabelMultisetType, manager.managedSettings.meshesEnabledProperty, @@ -54,78 +52,39 @@ class LabelSourceStateMeshPaneNode( ) with(tp.content.asVBox()) { tp.content = this - children.add(MeshesList(source, manager, meshInfos).node) + children.add(MeshSettingsPane(manager, meshInfos)) return tp } } - private class MeshesList( - private val source: DataSource<*, *>, + private class MeshSettingsPane( private val manager: MeshManagerWithAssignmentForSegments, - private val meshInfos: SegmentMeshInfos, - ) { - - private class Listener( - private val meshInfos: ReadOnlyListProperty, - private val totalProgressBar: MeshProgressBar, - ) : ListChangeListener { - - override fun onChanged(change: ListChangeListener.Change) { - updateTotalProgressBindings() - } - - private fun updateTotalProgressBindings() { - val individualProgresses = meshInfos.stream().map { it.meshProgress() }.filter { Objects.nonNull(it) }.collect(Collectors.toList()) - val globalProgress = GlobalMeshProgress(individualProgresses) - this.totalProgressBar.bindTo(globalProgress) - } - } - - val node: Node - get() = createNode() + private val meshInfoList: SegmentMeshInfoList, + ) : TitledPane("Mesh List", null) { private val isMeshListEnabledCheckBox = CheckBox() private val totalProgressBar = MeshProgressBar() - private fun createNode(): TitledPane { - - val meshesInfoList = ListView(meshInfos.readOnlyInfos()) - - meshesInfoList.setCellFactory { - object : ListCell() { - - init { - style = "-fx-padding: 0px" - } - - override fun updateItem(item: SegmentMeshInfo?, empty: Boolean) { - super.updateItem(item, empty) - text = null - InvokeOnJavaFXApplicationThread { - graphic = if (empty || item == null) null else SegmentMeshInfoNode(source, item).node.also { cell -> - cell.prefWidthProperty().bind(it.prefWidthProperty()) - } - } - } - } - } - + init { val exportMeshButton = Button("Export all") exportMeshButton.setOnAction { _ -> - val exportDialog = SegmentMeshExporterDialog(meshesInfoList.items) + val exportDialog = MeshExporterDialog(meshInfoList.meshInfos as ObservableList>) val result = exportDialog.showAndWait() if (result.isPresent) { result.get().run { - val ids = segmentId.map { it }.toTypedArray() - val meshSettings = ids.map { meshInfos.meshSettings().getOrAddMesh(it) }.toTypedArray() + val ids = meshKeys.toTypedArray() + val meshSettings = ids.map { manager.getSettings(it) }.toTypedArray() (meshExporter as? MeshExporterObj<*>)?.run { - val colors : Array = ids.map { meshInfos.getColor(it) }.toTypedArray() - exportMaterial(filePath, ids.toLongArray(), colors) + val colors: Array = ids.mapIndexed { idx, it -> + val color = manager.getStateFor(it)?.color ?: Color.WHITE + color.deriveColor(0.0, 1.0, 1.0, meshSettings[idx].opacity) + }.toTypedArray() + exportMaterial(filePath, ids.map { it.toString() }.toTypedArray(), colors) } meshExporter.exportMesh( - manager.getBlockListForLongKey, + manager.getBlockListForSegment, manager.getMeshForLongKey, meshSettings, ids, @@ -137,10 +96,10 @@ class LabelSourceStateMeshPaneNode( } val buttonBox = HBox(exportMeshButton).also { it.alignment = Pos.BOTTOM_RIGHT } - val meshesBox = VBox(meshesInfoList, Separator(Orientation.HORIZONTAL), buttonBox) - listOf(meshesBox, meshesInfoList).forEach { it.padding = Insets.EMPTY } + val meshesBox = VBox(meshInfoList, Separator(Orientation.HORIZONTAL), buttonBox) + listOf(meshesBox, meshInfoList).forEach { it.padding = Insets.EMPTY } - isMeshListEnabledCheckBox.also { it.selectedProperty().bindBidirectional(meshInfos.meshSettings().isMeshListEnabledProperty) } + isMeshListEnabledCheckBox.also { it.selectedProperty().bindBidirectional(manager.managedSettings.isMeshListEnabledProperty) } val helpDialog = PainteraAlerts.alert(Alert.AlertType.INFORMATION, true).apply { initModality(Modality.NONE) @@ -160,17 +119,32 @@ class LabelSourceStateMeshPaneNode( isFillHeight = true } - meshInfos.readOnlyInfos().addListener(Listener(meshInfos.readOnlyInfos(), totalProgressBar)) + meshInfoList.meshInfos.addListener(Listener(meshInfoList.meshInfos, totalProgressBar)) - return TitledPane("Mesh List", meshesBox).apply { - with(TPE) { - expandIfEnabled(isMeshListEnabledCheckBox.selectedProperty()) - graphicsOnly(tpGraphics) - alignment = Pos.CENTER_RIGHT - meshesInfoList.prefWidthProperty().bind(layoutBoundsProperty().createNonNullValueBinding { it.width - 5 }) - } + expandIfEnabled(isMeshListEnabledCheckBox.selectedProperty()) + graphicsOnly(tpGraphics) + alignment = Pos.CENTER_RIGHT + meshInfoList.prefWidthProperty().bind(layoutBoundsProperty().createNonNullValueBinding { it.width - 5 }) + content = meshesBox + } + + private class Listener( + private val meshInfos: ReadOnlyListProperty, + private val totalProgressBar: MeshProgressBar, + ) : ListChangeListener { + + override fun onChanged(change: ListChangeListener.Change) { + updateTotalProgressBindings() + } + + private fun updateTotalProgressBindings() { + val individualProgresses = meshInfos.stream().map { it.progressProperty }.filter { Objects.nonNull(it) }.collect(Collectors.toList()) + val globalProgress = GlobalMeshProgress(individualProgresses) + this.totalProgressBar.bindTo(globalProgress) } } + + } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePreferencePaneNode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePreferencePaneNode.kt index f77158120..79febd827 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePreferencePaneNode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePreferencePaneNode.kt @@ -2,8 +2,10 @@ package org.janelia.saalfeldlab.paintera.state import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon import javafx.application.Platform +import javafx.beans.Observable import javafx.beans.property.ObjectProperty import javafx.beans.property.SimpleObjectProperty +import javafx.collections.FXCollections import javafx.event.EventHandler import javafx.geometry.Insets import javafx.geometry.Pos @@ -35,8 +37,7 @@ import org.janelia.saalfeldlab.paintera.control.selection.SelectedSegments import org.janelia.saalfeldlab.paintera.data.DataSource import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource import org.janelia.saalfeldlab.paintera.data.mask.exception.CannotClearCanvas -import org.janelia.saalfeldlab.paintera.meshes.ManagedMeshSettings -import org.janelia.saalfeldlab.paintera.meshes.SegmentMeshInfos +import org.janelia.saalfeldlab.paintera.meshes.SegmentMeshInfoList import org.janelia.saalfeldlab.paintera.meshes.managed.MeshManagerWithAssignmentForSegments import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverter import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverterConfigNode @@ -53,7 +54,6 @@ class LabelSourceStatePreferencePaneNode( private val composite: ObjectProperty>, private val converter: HighlightingStreamConverter<*>, private val meshManager: MeshManagerWithAssignmentForSegments, - private val meshSettings: ManagedMeshSettings, private val brushProperties: BrushProperties? ) { @@ -65,10 +65,25 @@ class LabelSourceStatePreferencePaneNode( val node: Node get() { val box = SourceState.defaultPreferencePaneNode(composite) + + val observableSelectedSegmentsList = FXCollections.observableArrayList() + val selectedSegmentUpdateListener: (observable: Observable) -> Unit = { + val segements = selectedSegments.selectedSegments.toArray().toList() + observableSelectedSegmentsList.removeIf { it !in segements } + observableSelectedSegmentsList.addAll(segements.filter { it !in observableSelectedSegmentsList }.toList()) + } + selectedSegments.addListener(selectedSegmentUpdateListener) + box.visibleProperty().addListener { _, _, visible -> + if (!visible) { + selectedSegments.removeListener(selectedSegmentUpdateListener) + } else { + selectedSegments.addListener(selectedSegmentUpdateListener) + } + } val nodes = arrayOf( HighlightingStreamConverterConfigNode(converter).node, SelectedIdsNode(selectedIds, assignment, selectedSegments).node, - LabelSourceStateMeshPaneNode(source, meshManager, SegmentMeshInfos(selectedSegments, meshManager, meshSettings, source.numMipmapLevels)).node, + LabelSourceStateMeshPaneNode(source, meshManager, SegmentMeshInfoList(observableSelectedSegmentsList, meshManager)).node, AssignmentsNode(assignment).node, when (source) { is MaskedSource -> brushProperties?.let { MaskedSourceNode(source, brushProperties, meshManager::refreshMeshes).node } 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 a738d015a..baa05392b 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/ThresholdingSourceStatePreferencePaneNode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/ThresholdingSourceStatePreferencePaneNode.kt @@ -16,8 +16,11 @@ import org.janelia.saalfeldlab.fx.TitledPanes import org.janelia.saalfeldlab.fx.ui.NamedNode import org.janelia.saalfeldlab.fx.ui.NumberField 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.ui.PainteraAlerts +import org.janelia.saalfeldlab.paintera.ui.source.mesh.MeshExporterDialog class ThresholdingSourceStatePreferencePaneNode(private val state: ThresholdingSourceState<*, *>) { @@ -26,6 +29,7 @@ class ThresholdingSourceStatePreferencePaneNode(private val state: ThresholdingS .also { it.children.addAll(createBasicNote(), createMeshesNode()) } private fun createBasicNote(): Node { + val min = NumberField .doubleField(state.minProperty().get(), { true }, ObjectField.SubmitOn.ENTER_PRESSED, ObjectField.SubmitOn.FOCUS_LOST) .also { it.valueProperty().addListener { _, _, new -> state.minProperty().set(new.toDouble()) } } @@ -82,6 +86,33 @@ class ThresholdingSourceStatePreferencePaneNode(private val state: ThresholdingS state.meshManager.managedSettings.meshesEnabledProperty, titledPaneGraphicsSettings = MeshSettingsController.TitledPaneGraphicsSettings("Meshes"), helpDialogSettings = MeshSettingsController.HelpDialogSettings(headerText = "Meshes") - ) + ).also { + it.content = VBox(it.content).apply { + val exportMeshButton = Button("Export all") + exportMeshButton.setOnAction { _ -> + val exportDialog = MeshExporterDialog(MeshInfo(state.meshManager.meshKey, state.meshManager)) + val result = exportDialog.showAndWait() + if (result.isPresent) { + result.get().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 + ) + } + } + } + val buttonBox = HBox(exportMeshButton).also { it.alignment = Pos.BOTTOM_RIGHT } + children += buttonBox + + } + } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ConnectomicsLabelState.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ConnectomicsLabelState.kt index 1d6c5857b..9e06fe89d 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ConnectomicsLabelState.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ConnectomicsLabelState.kt @@ -144,16 +144,14 @@ class ConnectomicsLabelState, T>( meshManagerExecutors, meshWorkersExecutors ).apply { - InvokeOnJavaFXApplicationThread { - refreshMeshes() - } + refreshMeshes() } val meshCacheKeyProperty: ObjectBinding = fragmentsInSelectedSegments.createNonNullValueBinding { FragmentLabelMeshCacheKey(it) } override fun getMeshCacheKeyBinding(): ObjectBinding = meshCacheKeyProperty - override fun getGetBlockListFor(): GetBlockListFor = this.meshManager.getBlockListForMeshCacheKey + override fun getGetBlockListFor(): GetBlockListFor = this.meshManager.getBlockListForFragments override fun getIntersectableMask(): DataSource> = labelToBooleanFragmentMaskSource(this) @@ -269,7 +267,7 @@ class ConnectomicsLabelState, T>( paintera.viewer3D().meshesGroup.children.add(meshManager.meshesGroup) selectedSegments.addListener { meshManager.setMeshesToSelection() } - meshManager.viewerEnabledProperty().bind(paintera.viewer3D().meshesEnabled) + meshManager.viewerEnabledProperty.bind(paintera.viewer3D().meshesEnabled) meshManager.rendererSettings.showBlockBoundariesProperty.bind(paintera.viewer3D().showBlockBoundaries) meshManager.rendererSettings.blockSizeProperty.bind(paintera.viewer3D().rendererBlockSize) meshManager.rendererSettings.numElementsPerFrameProperty.bind(paintera.viewer3D().numElementsPerFrame) @@ -342,7 +340,6 @@ class ConnectomicsLabelState, T>( compositeProperty(), converter(), meshManager, - meshManager.managedSettings, brushProperties ).node.let { if (it is VBox) it else VBox(it) } @@ -669,7 +666,7 @@ class ConnectomicsLabelState, T>( interpolation = context[json, INTERPOLATION]!! isVisible = context[json, IS_VISIBLE]!! context.get(json, SELECTED_IDS) { selectedIds.activate(*it) } - context.get(json, ManagedMeshSettings.MESH_SETTINGS_KEY) { meshManager.managedSettings.set(it) } + context.get>(json, ManagedMeshSettings.MESH_SETTINGS_KEY) { meshManager.managedSettings.set(it as ManagedMeshSettings) } context.get(json, LOCKED_SEGMENTS)?.forEach { lockedSegments.lock(it) } @@ -689,7 +686,7 @@ class ConnectomicsLabelState, T>( } -class FragmentLabelMeshCacheKey constructor(fragmentsInSelectedSegments: FragmentsInSelectedSegments) : MeshCacheKey { +class FragmentLabelMeshCacheKey(fragmentsInSelectedSegments: FragmentsInSelectedSegments) : MeshCacheKey { val fragments: TLongHashSet = TLongHashSet(fragmentsInSelectedSegments.fragments) val segments: TLongHashSet = TLongHashSet(fragmentsInSelectedSegments.selectedSegments.selectedSegmentsCopyAsArray) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/source/mesh/SegmentMeshInfoNode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/source/mesh/SegmentMeshInfoNode.kt new file mode 100644 index 000000000..a1f365e03 --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/source/mesh/SegmentMeshInfoNode.kt @@ -0,0 +1,37 @@ +package org.janelia.saalfeldlab.paintera.ui.source.mesh + +import javafx.geometry.VPos +import javafx.scene.control.Label +import javafx.scene.layout.GridPane +import javafx.scene.layout.HBox +import javafx.scene.layout.Priority +import org.fxmisc.flowless.VirtualizedScrollPane +import org.fxmisc.richtext.InlineCssTextArea +import org.janelia.saalfeldlab.fx.ui.NamedNode +import org.janelia.saalfeldlab.paintera.data.DataSource +import org.janelia.saalfeldlab.paintera.meshes.SegmentMeshInfo +import org.janelia.saalfeldlab.paintera.meshes.ui.MeshInfoPane + +class SegmentMeshInfoNode(private val meshInfo: SegmentMeshInfo) : MeshInfoPane(meshInfo) { + + override fun createMeshInfoGrid(grid: GridPane): GridPane { + + val textIds = InlineCssTextArea(meshInfo.fragments().contentToString()) + val virtualPane = VirtualizedScrollPane(textIds) + textIds.isWrapText = true + + val idsLabel = Label("ids: ") + idsLabel.minWidth = 30.0 + idsLabel.maxWidth = 30.0 + val spacer = NamedNode.bufferNode() + val idsHeader = HBox(idsLabel, spacer) + + return grid.apply { + add(idsHeader, 0, 0) + GridPane.setValignment(idsHeader, VPos.CENTER) + add(virtualPane, 1, 0) + GridPane.setHgrow(virtualPane, Priority.ALWAYS) + super.createMeshInfoGrid(grid) + } + } +} \ No newline at end of file diff --git a/src/main/java/org/janelia/saalfeldlab/util/n5/universe/N5FactoryWithCache.kt b/src/main/kotlin/org/janelia/saalfeldlab/util/n5/universe/N5FactoryWithCache.kt similarity index 75% rename from src/main/java/org/janelia/saalfeldlab/util/n5/universe/N5FactoryWithCache.kt rename to src/main/kotlin/org/janelia/saalfeldlab/util/n5/universe/N5FactoryWithCache.kt index d3b2bca42..c6a641bb0 100644 --- a/src/main/java/org/janelia/saalfeldlab/util/n5/universe/N5FactoryWithCache.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/util/n5/universe/N5FactoryWithCache.kt @@ -1,33 +1,3 @@ -/** - * Copyright (c) 2017-2021, Saalfeld lab, HHMI Janelia - * All rights reserved. - * - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ package org.janelia.saalfeldlab.util.n5.universe import org.janelia.saalfeldlab.n5.N5Exception @@ -39,16 +9,6 @@ import org.janelia.saalfeldlab.n5.universe.N5Factory import org.slf4j.LoggerFactory import java.lang.invoke.MethodHandles -/** - * Factory for various N5 readers and writers. Implementation specific - * parameters can be provided to the factory instance and will be used when - * such implementations are generated and ignored otherwise. Reasonable - * defaults are provided. - * - * @author Stephan Saalfeld - * @author John Bogovic - * @author Igor Pisarev - */ class N5FactoryWithCache : N5Factory() { companion object { @@ -203,9 +163,8 @@ class N5DatasetDoesntExist : N5Exception { constructor(uri: String, dataset : String, cause: Throwable) : super("Dataset \"${displayDataset(dataset)}\" not found in container $uri", cause) } - class N5ContainerDoesntExist : N5Exception { constructor(location: String) : super("Cannot Open $location") constructor(location: String, cause: Throwable) : super("Cannot Open $location", cause) -} +} \ No newline at end of file