diff --git a/glue_ar/export_dialog.ui b/glue_ar/export_dialog.ui index b382746..10f7ed9 100644 --- a/glue_ar/export_dialog.ui +++ b/glue_ar/export_dialog.ui @@ -14,12 +14,11 @@ Export 3D Volume - - - - Set the export settings for each layer - - + + + + + @@ -60,12 +59,6 @@ - - - - - - @@ -73,8 +66,15 @@ - - + + + + + + + Set the export settings for each layer + + diff --git a/glue_ar/scatter.py b/glue_ar/scatter.py index 3b33cf0..749fd77 100644 --- a/glue_ar/scatter.py +++ b/glue_ar/scatter.py @@ -1,3 +1,5 @@ +from math import floor + import pyvista as pv from glue_ar.utils import layer_color, xyz_bounds, xyz_for_layer @@ -6,7 +8,7 @@ def scatter_layer_as_points(viewer_state, layer_state): xyz = xyz_for_layer(viewer_state, layer_state) return { - "data": xyz, + "mesh": xyz, "color": layer_color(layer_state), "opacity": layer_state.alpha, "style": "points_gaussian", @@ -18,7 +20,7 @@ def scatter_layer_as_points(viewer_state, layer_state): def scatter_layer_as_spheres(viewer_state, layer_state): data = xyz_for_layer(viewer_state, layer_state) return { - "data": [pv.Sphere(center=p) for p in data] + "mesh": [pv.Sphere(center=p) for p in data] } @@ -27,7 +29,7 @@ def scatter_layer_as_glyphs(viewer_state, layer_state, glyph): points = pv.PointSet(data) glyphs = points.glyph(geom=glyph, orient=False, scale=False) return { - "data": glyphs, + "mesh": glyphs, "color": layer_color(layer_state), "opacity": layer_state.alpha, } @@ -36,16 +38,21 @@ def scatter_layer_as_glyphs(viewer_state, layer_state, glyph): def scatter_layer_as_multiblock(viewer_state, layer_state, theta_resolution=8, phi_resolution=8, - scaled=True): - data = xyz_for_layer(viewer_state, layer_state, scaled=scaled) + scaled=True, + clip_to_bounds=True): + data = xyz_for_layer(viewer_state, layer_state, + preserve_aspect=viewer_state.native_aspect, + clip_to_bounds=clip_to_bounds, + scaled=scaled) bounds = xyz_bounds(viewer_state) factor = max((abs(b[1] - b[0]) for b in bounds)) - radius = layer_state.size_scaling * layer_state.size / factor + radius = (layer_state.size_scaling * layer_state.size) / factor spheres = [pv.Sphere(center=p, radius=radius, phi_resolution=phi_resolution, theta_resolution=theta_resolution) for p in data] blocks = pv.MultiBlock(spheres) geometry = blocks.extract_geometry() + info = { - "data": geometry, + "mesh": geometry, "opacity": layer_state.alpha } if layer_state.color_mode == "Fixed": @@ -66,4 +73,5 @@ def scatter_layer_as_multiblock(viewer_state, layer_state, info["cmap"] = cmap info["clim"] = clim info["scalars"] = "colors" + return info diff --git a/glue_ar/tools.py b/glue_ar/tools.py index f1b90d1..7fa5ca8 100644 --- a/glue_ar/tools.py +++ b/glue_ar/tools.py @@ -13,6 +13,7 @@ from glue_ar.scatter import scatter_layer_as_multiblock from glue_ar.export import export_gl, export_modelviewer +from glue_ar.utils import bounds_3d_from_layers from glue_ar.volume import bounds_3d, meshes_for_volume_layer __all__ = ["GLScatterExportTool", "GLVolumeExportTool"] @@ -45,9 +46,13 @@ def activate(self): layer_states = [layer.state for layer in self.viewer.layers if layer.enabled and layer.state.visible] for layer_state in layer_states: layer_info = dialog.state_dictionary[layer_state.layer.label].as_dict() - mesh_info = scatter_layer_as_multiblock(self.viewer.state, layer_state, **layer_info, scaled=True) - data = mesh_info.pop("data") - plotter.add_mesh(data, **mesh_info) + mesh_info = scatter_layer_as_multiblock(self.viewer.state, layer_state, + scaled=True, + clip_to_bounds=self.viewer.state.clip_data, + **layer_info) + mesh = mesh_info.pop("mesh") + if len(mesh.points) > 0: + plotter.add_mesh(mesh, **mesh_info) dir, base = split(export_path) name, ext = splitext(base) @@ -79,8 +84,11 @@ def activate(self): plotter = pv.Plotter() layer_states = [layer.state for layer in self.viewer.layers if layer.enabled and layer.state.visible] - bounds = bounds_3d(self.viewer.state) frbs = {} + if self.viewer.state.clip_data: + bounds = bounds_3d(self.viewer.state) + else: + bounds = bounds_3d_from_layers(self.viewer.state, layer_states) for layer_state in layer_states: layer_info = dialog.state_dictionary[layer_state.layer.label].as_dict() if isinstance(layer_state, VolumeLayerState): @@ -89,9 +97,13 @@ def activate(self): precomputed_frbs=frbs, **layer_info) else: - mesh_info = scatter_layer_as_multiblock(self.viewer.state, layer_state, **layer_info, scaled=False) - data = mesh_info.pop("data") - plotter.add_mesh(data, **mesh_info) + mesh_info = scatter_layer_as_multiblock(self.viewer.state, layer_state, + scaled=False, + clip_to_bounds=self.viewer.state.clip_data, + **layer_info) + mesh = mesh_info.pop("mesh") + if len(mesh.points) > 0: + plotter.add_mesh(mesh, **mesh_info) dir, base = split(export_path) name, ext = splitext(base) diff --git a/glue_ar/utils.py b/glue_ar/utils.py index d638157..855d183 100644 --- a/glue_ar/utils.py +++ b/glue_ar/utils.py @@ -1,5 +1,5 @@ from glue.core.subset_group import GroupedSubset -from numpy import array +from numpy import array, inf def isomin_for_layer(viewer_state, layer): @@ -17,6 +17,18 @@ def xyz_bounds(viewer_state): (viewer_state.z_min, viewer_state.z_max)] +def bounds_3d_from_layers(viewer_state, layer_states): + mins = [inf, inf, inf] + maxes = [-inf, -inf, -inf] + atts = viewer_state.x_att, viewer_state.y_att, viewer_state.z_att + for state in layer_states: + data = state.layer.layer + mins = [min(min(data[att]), m) for m, att in zip(mins, atts)] + maxes = [max(max(data[att]), m) for m, att in zip(maxes, atts)] + print(mins) + return [(l, u) for l, u in zip(mins, maxes)] + + # TODO: Make this better? # glue-plotly has had to deal with similar issues, # the utilities there are at least better than this @@ -27,8 +39,6 @@ def layer_color(layer_state): return layer_color -# data should be list of numpy arrays -# Think about being more format-agnostic later def scale(data, bounds, preserve_aspect=True): if preserve_aspect: ranges = [abs(bds[1] - bds[0]) for bds in bounds] @@ -37,25 +47,42 @@ def scale(data, bounds, preserve_aspect=True): bds = bounds[index] m = 2 / (bds[1] - bds[0]) b = (bds[0] + bds[1]) / (bds[1] - bds[0]) - return [m * d + b for d in data] + scaled = [[m * v + b for v in d] for d in data] else: scaled = [] for idx, bds in enumerate(bounds): m = 2 / (bds[1] - bds[0]) b = (bds[0] + bds[1]) / (bds[1] - bds[0]) - scaled.append(m * data[idx] + b) - return scaled + scaled.append([m * d + b for d in data[idx]]) + + return scaled # TODO: Worry about efficiency later -def xyz_for_layer(viewer_state, layer_state, scaled=False, preserve_aspect=True): +# and just generally make this better +def xyz_for_layer(viewer_state, layer_state, + scaled=False, + preserve_aspect=True, + clip_to_bounds=True): xs = layer_state.layer[viewer_state.x_att] ys = layer_state.layer[viewer_state.y_att] zs = layer_state.layer[viewer_state.z_att] vals = [xs, ys, zs] - if scaled: + if scaled or clip_to_bounds: bounds = xyz_bounds(viewer_state) - vals = scale(vals, bounds, preserve_aspect=preserve_aspect) + if clip_to_bounds: + xs, ys, zs = [], [], [] + for x, y, z in zip(*vals): + if (x >= bounds[0][0] and x <= bounds[0][1]) and \ + (y >= bounds[1][0] and y <= bounds[1][1]) and \ + (z >= bounds[2][0] and z <= bounds[2][1]): + xs.append(x) + ys.append(y) + zs.append(z) + vals = [xs, ys, zs] + + if scaled: + vals = scale(vals, bounds, preserve_aspect=preserve_aspect) return array(list(zip(*vals))) diff --git a/glue_ar/volume.py b/glue_ar/volume.py index 532167b..977b142 100644 --- a/glue_ar/volume.py +++ b/glue_ar/volume.py @@ -16,20 +16,19 @@ def meshes_for_volume_layer(viewer_state, layer_state, bounds, use_gaussian_filter=False, smoothing_iterations=0, precomputed_frbs=None): - precomputed_frbs = precomputed_frbs or {} - layer_content = layer_state.layer parent = layer_content.data if isinstance(layer_content, GroupedSubset) else layer_content parent_label = parent.label - if parent_label in precomputed_frbs: + if precomputed_frbs is not None and parent_label in precomputed_frbs: data = precomputed_frbs[parent_label] else: data = parent.compute_fixed_resolution_buffer( target_data=viewer_state.reference_data, bounds=bounds, target_cid=layer_state.attribute) - precomputed_frbs[parent_label] = data + if precomputed_frbs is not None: + precomputed_frbs[parent_label] = data if isinstance(layer_state.layer, GroupedSubset): subcube = parent.compute_fixed_resolution_buffer( @@ -60,7 +59,7 @@ def meshes_for_volume_layer(viewer_state, layer_state, bounds, isodata = isodata.smooth(n_iter=int(smoothing_iterations)) return { - "data": isodata, + "mesh": isodata, "color": layer_color(layer_state), "opacity": layer_state.alpha, # "isomin": isomin,