diff --git a/docs/changes.md b/docs/changes.md index 9ab5cc39..4182d138 100644 --- a/docs/changes.md +++ b/docs/changes.md @@ -11,6 +11,8 @@ - add `rotation=..` to `Arrow2D()` class - improvements to `applications.MorphPlotter` - add `FlyOverSurface` class and `examples/basic/interaction_modes3.py` +- address #1072 for pyinstaller +- add `mesh.extrude_and_trim_with()` method out of #1077 ## Soft-breaking Changes diff --git a/examples/basic/interaction_modes3.py b/examples/basic/interaction_modes3.py index a84ca279..8d91d8d0 100644 --- a/examples/basic/interaction_modes3.py +++ b/examples/basic/interaction_modes3.py @@ -3,6 +3,7 @@ - "t" and "g" will move the camera up and down along z. - "x" and "X" will reset the camera to the default position towards +/-x. - "y" and "Y" will reset the camera to the default position towards +/-y. +- "." and "," will rotate azimuth to the right or left. - "r" will reset the camera to the default position.""" from vedo import * from vedo.interactor_modes import FlyOverSurface diff --git a/examples/notebooks/align1.ipynb b/examples/notebooks/align1.ipynb index 489c316e..343ddeb2 100644 --- a/examples/notebooks/align1.ipynb +++ b/examples/notebooks/align1.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { "scrolled": false }, @@ -113,7 +113,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.10" + "version": "3.12.2" } }, "nbformat": 4, diff --git a/tests/pipeline.txt b/tests/pipeline.txt index 31cf7608..ac3f8004 100644 --- a/tests/pipeline.txt +++ b/tests/pipeline.txt @@ -152,7 +152,7 @@ cd ~/Projects/4d-gene-reconstruction/useful_scripts python interp_to_tetras.py -# DOCUMETATION ############################################################### +# DOCUMENTATION ############################################################### mount_staging pip install pdoc cd $VEDODIR/docs/pdoc diff --git a/vedo/__init__.py b/vedo/__init__.py index 2e3ac557..72fb6453 100644 --- a/vedo/__init__.py +++ b/vedo/__init__.py @@ -116,6 +116,10 @@ def format(self, record): logger = logging.getLogger("vedo") _chsh = logging.StreamHandler() +if sys.stdout is None: + sys.stdout = open(os.devnull, "w") +if sys.stderr is None: + sys.stderr = open(os.devnull, "w") _chsh.flush = sys.stdout.flush _chsh.setLevel(logging.DEBUG) _chsh.setFormatter(_LoggingCustomFormatter()) diff --git a/vedo/interactor_modes.py b/vedo/interactor_modes.py index ffe6fa03..40fee330 100644 --- a/vedo/interactor_modes.py +++ b/vedo/interactor_modes.py @@ -136,6 +136,7 @@ def _mouse_move(self, w, e): if self.right: self._mouse_right_move() + ############################################################################# class FlyOverSurface(vtki.vtkInteractorStyleUser): """ @@ -147,9 +148,10 @@ class FlyOverSurface(vtki.vtkInteractorStyleUser): - "g" (or "PageDown") will move the camera lower in z. - "x" and "X" will reset the camera to the default position towards positive or negative x. - "y" and "Y" will reset the camera to the default position towards positive or negative y. + - "." and "," will rotate azimuth to the right or left. - "r" will reset the camera to the default position. - Only keyboard interaction is active, mouse interaction is not. + Left button: Select a point on the surface to focus the camera on it. """ def __init__(self, move_step=0.05, angle_step=1.5): @@ -169,71 +171,111 @@ def __init__(self, move_step=0.05, angle_step=1.5): super().__init__() self.interactor = None # filled in plotter.py - self.renderer = None # filled in plotter.py - + self.renderer = None # filled in plotter.py + self.angle_step = angle_step self.move_step = move_step self.tleft = vtki.vtkTransform() self.tleft.RotateZ(self.angle_step) - + self.tright = vtki.vtkTransform() self.tright.RotateZ(-self.angle_step) - + self.AddObserver("KeyPressEvent", self._key) + self.AddObserver("RightButtonPressEvent", self._right_button_press) + self.AddObserver("MouseWheelForwardEvent", self._mouse_wheel_forward) + self.AddObserver("MouseWheelBackwardEvent", self._mouse_wheel_backward) + self.AddObserver("LeftButtonPressEvent", self._left_button_press) @property def camera(self): return self.renderer.GetActiveCamera() - - def _key(self, obj, _): - k = obj.GetKeySym() - if obj.GetShiftKey(): - k = k.upper() - # print("Key press event", k, obj.GetShiftKey()) + @property + def position(self): + return np.array(self.camera.GetPosition()) + + @position.setter + def position(self, value): + self.camera.SetPosition(value[:3]) + self.camera.SetViewUp(0.00001, 0, 1) + self.renderer.ResetCameraClippingRange() + + @property + def focal_point(self): + return np.array(self.camera.GetFocalPoint()) + + @focal_point.setter + def focal_point(self, value): + self.camera.SetFocalPoint(value[:3]) + self.camera.SetViewUp(0.00001, 0, 1) + self.renderer.ResetCameraClippingRange() + + def _right_button_press(self, obj, event): + # print("Right button", event) + self._key("Down", event) + + def _left_button_press(self, obj, event): + # print("Left button", event) + newPickPoint = [0, 0, 0, 0] + focalDepth = 0 + x, y = obj.interactor.GetEventPosition() + self.ComputeDisplayToWorld(self.renderer, x, y, focalDepth, newPickPoint) + self.focal_point = np.array(newPickPoint) + self.interactor.Render() + + def _mouse_wheel_backward(self, obj, event): + # print("mouse_wheel_backward ", event) + self._key("Down", event) + + def _mouse_wheel_forward(self, obj, event): + # print("mouse_wheel_forward ", event) + self._key("Up", event) + + def _key(self, obj, event): + + if "Mouse" in event or "Button" in event: + k = obj + else: + k = obj.GetKeySym() + if obj.GetShiftKey(): + k = k.upper() + # print("FlyOverSurface. Key press", k) if k in ["r", "x"]: # print("r pressed, reset camera") self.bounds = self.renderer.ComputeVisiblePropBounds() x0, x1, y0, y1, z0, z1 = self.bounds dx = x1 - x0 - z = max( z1 * 1, z0+(y1 - y0)/4, z0+(x1 - x0)/4) - self.camera.SetViewUp(0.00001, 0, 1) - self.camera.SetPosition(x0-dx, (y0 + y1) / 2, z) - self.camera.SetFocalPoint(x0+dx, (y0 + y1) / 2, z) - self.renderer.ResetCameraClippingRange() + z = max(z1 * 1, z0 + (y1 - y0) / 4, z0 + (x1 - x0) / 4) + self.position = [x0 - dx, (y0 + y1) / 2, z] + self.focal_point = [x0 + dx / 2, (y0 + y1) / 2, z] elif k in ["X"]: # print("X pressed, reset camera") self.bounds = self.renderer.ComputeVisiblePropBounds() x0, x1, y0, y1, z0, z1 = self.bounds dx = x1 - x0 - z = max( z1 * 1, (y1 - y0)/4, (x1 - x0)/4) - self.camera.SetViewUp(0.00001, 0, 1) - self.camera.SetPosition(x1+dx, (y0 + y1) / 2, z) - self.camera.SetFocalPoint(x0-dx/2,(y0+y1)/2,z0) - self.renderer.ResetCameraClippingRange() + z = max(z1 * 1, (y1 - y0) / 4, (x1 - x0) / 4) + self.position = [x1 + dx, (y0 + y1) / 2, z] + self.focal_point = [x0 - dx / 2, (y0 + y1) / 2, z] elif k in ["y"]: # print("y pressed, reset camera") self.bounds = self.renderer.ComputeVisiblePropBounds() x0, x1, y0, y1, z0, z1 = self.bounds dy = y1 - y0 - z = max( z1 * 1, z0+(y1 - y0)/4, z0+(x1 - x0)/4) - self.camera.SetViewUp(0.00001, 0, 1) - self.camera.SetPosition( (x0 + x1) / 2, y0-dy, z) - self.camera.SetFocalPoint((x0 + x1) / 2, y1+dy/2, z0) - self.renderer.ResetCameraClippingRange() + z = max(z1 * 1, z0 + (y1 - y0) / 4, z0 + (x1 - x0) / 4) + self.position = [(x0 + x1) / 2, y0 - dy, z] + self.focal_point = [(x0 + x1) / 2, y1 + dy / 2, z] elif k in ["Y"]: # print("Y pressed, reset camera") self.bounds = self.renderer.ComputeVisiblePropBounds() x0, x1, y0, y1, z0, z1 = self.bounds dy = y1 - y0 - z = max( z1 * 1, z0+(y1 - y0)/4, z0+(x1 - x0)/4) - self.camera.SetViewUp(0.00001, 0, 1) - self.camera.SetPosition( (x0 + x1) / 2, y1+dy, z) - self.camera.SetFocalPoint((x0 + x1) / 2, y0-dy/2, z0) - self.renderer.ResetCameraClippingRange() + z = max(z1 * 1, z0 + (y1 - y0) / 4, z0 + (x1 - x0) / 4) + self.position = [(x0 + x1) / 2, y1 + dy, z] + self.focal_point = [(x0 + x1) / 2, y0 - dy / 2, z] elif k in ["Up", "w"]: # print("Up pressed, move forward") @@ -243,10 +285,8 @@ def _key(self, obj, _): p = np.array(self.camera.GetPosition()) v = np.array(self.camera.GetDirectionOfProjection()) newp = p + dx * v - self.camera.SetPosition(newp[0], newp[1], p[2]) - self.camera.SetViewUp(0.00001, 0, 1) - self.renderer.ResetCameraClippingRange() - + self.position = [newp[0], newp[1], p[2]] + self.focal_point = self.focal_point + dx * v elif k in ["Down", "s"]: # print("Down pressed, move backward") self.bounds = self.renderer.ComputeVisiblePropBounds() @@ -255,9 +295,8 @@ def _key(self, obj, _): p = np.array(self.camera.GetPosition()) v = np.array(self.camera.GetDirectionOfProjection()) newp = p - dx * v - self.camera.SetPosition(newp[0], newp[1], p[2]) - self.camera.SetViewUp(0.00001, 0, 1) - self.renderer.ResetCameraClippingRange() + self.position = [newp[0], newp[1], p[2]] + self.focal_point = self.focal_point - dx * v elif k in ["Left", "a"]: # print("Left pressed, rotate to the left") @@ -266,10 +305,7 @@ def _key(self, obj, _): w = np.array(self.camera.GetDirectionOfProjection()) p = np.array(self.camera.GetPosition()) w2 = np.array(self.tleft.TransformFloatPoint(w)) - fc = np.array(self.camera.GetFocalPoint()) - self.camera.SetFocalPoint(fc + np.linalg.norm(p-fc) * w2) - self.camera.SetViewUp(0.00001, 0, 1) - self.renderer.ResetCameraClippingRange() + self.focal_point = self.focal_point + np.linalg.norm(p-self.focal_point) * w2 elif k in ["Right", "d"]: # print("Right pressed, rotate to the right") @@ -278,35 +314,59 @@ def _key(self, obj, _): w = np.array(self.camera.GetDirectionOfProjection()) p = np.array(self.camera.GetPosition()) w2 = np.array(self.tright.TransformFloatPoint(w)) - fc = np.array(self.camera.GetFocalPoint()) - self.camera.SetFocalPoint(fc + np.linalg.norm(p-fc) * w2) - self.camera.SetViewUp(0.00001, 0, 1) - self.renderer.ResetCameraClippingRange() + self.focal_point = self.focal_point + np.linalg.norm(p-self.focal_point) * w2 elif k in ["t", "Prior"]: # print("t pressed, move z up") self.bounds = self.renderer.ComputeVisiblePropBounds() diagonal = np.linalg.norm(np.array(self.bounds[1::2]) - np.array(self.bounds[::2])) dx = self.move_step * diagonal - p = np.array(self.camera.GetPosition()) - self.camera.SetPosition(p[0], p[1], p[2]+dx/4) - self.camera.SetViewUp(0.00001, 0, 1) - self.renderer.ResetCameraClippingRange() + p = self.position + self.position = [p[0], p[1], p[2] + dx / 4] elif k in ["g", "Next"]: # print("g pressed, move z down") self.bounds = self.renderer.ComputeVisiblePropBounds() diagonal = np.linalg.norm(np.array(self.bounds[1::2]) - np.array(self.bounds[::2])) dx = self.move_step * diagonal - p = np.array(self.camera.GetPosition()) - self.camera.SetPosition(p[0], p[1], p[2]-dx/4) - self.camera.SetViewUp(0.00001, 0, 1) - self.renderer.ResetCameraClippingRange() + p = self.position + self.position = [p[0], p[1], p[2] - dx / 4] + + elif k in ["comma", "COMMA"]: + # print("< pressed, rotate azimuth to the left") + self.bounds = self.renderer.ComputeVisiblePropBounds() + scene_center = [ + self.bounds[0] + self.bounds[1], + self.bounds[2] + self.bounds[3], + self.bounds[4] + self.bounds[5], + ] + scene_center = np.array(scene_center) / 2 + p = self.position + v = p - scene_center + newp = scene_center + self.tleft.TransformFloatPoint(v) + self.position = [newp[0], newp[1], p[2]] + + elif k in ["period", "PERIOD"]: + # print("< pressed, rotate azimuth to the right") + self.bounds = self.renderer.ComputeVisiblePropBounds() + scene_center = [ + self.bounds[0] + self.bounds[1], + self.bounds[2] + self.bounds[3], + self.bounds[4] + self.bounds[5], + ] + scene_center = np.array(scene_center) / 2 + p = self.position + v = p - scene_center + newp = scene_center + self.tright.TransformFloatPoint(v) + self.position = [newp[0], newp[1], p[2]] elif k in ["q", "Return"]: self.interactor.ExitCallback() return - + + else: + return + self.interactor.Render() @@ -734,7 +794,7 @@ def key_press(self, obj, event): self.end_x = self.start_x self.end_y = self.start_y self.initialize_screen_drawing() - elif KEY in ('2', '3'): + elif KEY in ("2", "3"): self.toggle_parallel_projection() elif KEY == "A": self.zoom_fit() @@ -949,7 +1009,7 @@ def start_drag(self): self.start_drag_on_props(self.picked_props) else: pass - # print('Can not start drag, + # print('Can not start drag, # nothing selected and callback_start_drag not assigned') def finish_drag(self): @@ -959,7 +1019,7 @@ def finish_drag(self): # by called functions for pos0, actor in zip( self.draginfo.dragged_actors_original_positions, - self.draginfo.actors_dragging + self.draginfo.actors_dragging, ): actor.SetPosition(pos0) self.callback_end_drag(self.draginfo) @@ -978,7 +1038,7 @@ def start_drag_on_props(self, props): draginfo.actors_dragging = props # [*actors, *outlines] for a in draginfo.actors_dragging: - draginfo.dragged_actors_original_positions.append(a.GetPosition()) # numpy ndarray + draginfo.dragged_actors_original_positions.append(a.GetPosition()) # Get the start position of the drag in 3d rwi = self.GetInteractor() @@ -988,22 +1048,19 @@ def start_drag_on_props(self, props): temp_out = [0, 0, 0] self.ComputeWorldToDisplay( - ren, viewFocus[0], viewFocus[1], viewFocus[2], - temp_out + ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out ) focalDepth = temp_out[2] newPickPoint = [0, 0, 0, 0] x, y = rwi.GetEventPosition() - self.ComputeDisplayToWorld( - ren, x, y, focalDepth, newPickPoint) + self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint) mouse_pos_3d = np.array(newPickPoint[:3]) draginfo.start_position_3d = mouse_pos_3d self.draginfo = draginfo def execute_drag(self): - rwi = self.GetInteractor() ren = self.GetCurrentRenderer() @@ -1033,8 +1090,8 @@ def execute_drag(self): # print(f'delta_inplane = {delta_inplane}') for pos0, actor in zip( - self.draginfo.dragged_actors_original_positions, - self.draginfo.actors_dragging + self.draginfo.dragged_actors_original_positions, + self.draginfo.actors_dragging, ): m = actor.GetUserMatrix() if m: @@ -1052,8 +1109,8 @@ def execute_drag(self): def cancel_drag(self): """Cancels the drag and restored the original positions of all dragged actors""" for pos0, actor in zip( - self.draginfo.dragged_actors_original_positions, - self.draginfo.actors_dragging + self.draginfo.dragged_actors_original_positions, + self.draginfo.actors_dragging, ): actor.SetPosition(pos0) self.draginfo = None @@ -1065,7 +1122,6 @@ def zoom(self): rwi = self.GetInteractor() x, y = rwi.GetEventPosition() xp, yp = rwi.GetLastEventPosition() - direction = y - yp self.move_mouse_wheel(direction / 10) @@ -1090,15 +1146,14 @@ def pan(self): x, y = rwi.GetEventPosition() self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint) - # // Has to recalc old mouse point since the viewport has moved, - # // so can't move it outside the loop + # Has to recalc old mouse point since the viewport has moved, + # so can't move it outside the loop oldPickPoint = [0, 0, 0, 0] xp, yp = rwi.GetLastEventPosition() self.ComputeDisplayToWorld(ren, xp, yp, focalDepth, oldPickPoint) # - # // Camera motion is reversed - # + # Camera motion is reversed motionVector = ( oldPickPoint[0] - newPickPoint[0], oldPickPoint[1] - newPickPoint[1], @@ -1158,7 +1213,6 @@ def rotate_turtable_by(self, rxf, ryf): upside_down_factor = -1 if up[2] < 0 else 1 # rotate about focal point - P = campos - focal # camera position # Rotate left/right about the global Z axis @@ -1179,7 +1233,6 @@ def rotate_turtable_by(self, rxf, ryf): # apply the change in azimuth and elevation azi_new = azi + rxf / 60 - elev_new = elev + upside_down_factor * ryf / 60 # the changed elevation changes H (D stays the same) @@ -1195,14 +1248,10 @@ def rotate_turtable_by(self, rxf, ryf): # if upside_down: # up_z = -up_z # up_h = -up_h - up = (-up_h * np.cos(azi_new), -up_h * np.sin(azi_new), up_z) - new_pos = focal + Pnew - camera.SetViewUp(up) camera.SetPosition(new_pos) - camera.OrthogonalizeViewUp() # Update @@ -1293,8 +1342,6 @@ def focus_on(self, prop3D): position = prop3D.GetPosition() - # print(f"Focus on {position}") - ren = self.GetCurrentRenderer() camera = ren.GetActiveCamera() diff --git a/vedo/mesh.py b/vedo/mesh.py index 78b74f33..485a1959 100644 --- a/vedo/mesh.py +++ b/vedo/mesh.py @@ -2064,6 +2064,111 @@ def extrude(self, zshift=1.0, direction=(), rotation=0.0, dr=0.0, cap=True, res= m.name = "ExtrudedMesh" return m + def extrude_and_trim_with( + self, + surface: "Mesh", + direction=(), + strategy="all", + cap=True, + cap_strategy="max", + ) -> "Mesh": + """ + Extrude a Mesh and trim it with an input surface mesh. + + Arguments: + surface : (Mesh) + the surface mesh to trim with. + direction : (list) + extrusion direction in the xy plane. + strategy : (str) + either "boundary_edges" or "all_edges". + cap : (bool) + enable or disable capping. + cap_strategy : (str) + either "intersection", "minimum_distance", "maximum_distance", "average_distance". + + The input Mesh is swept along a specified direction forming a "skirt" + from the boundary edges 2D primitives (i.e., edges used by only one polygon); + and/or from vertices and lines. + The extent of the sweeping is limited by a second input: defined where + the sweep intersects a user-specified surface. + + Capping of the extrusion can be enabled. + In this case the input, generating primitive is copied inplace as well + as to the end of the extrusion skirt. + (See warnings below on what happens if the intersecting sweep does not + intersect, or partially intersects the trim surface.) + + Note that this method operates in two fundamentally different modes + based on the extrusion strategy. + If the strategy is "boundary_edges", then only the boundary edges of the input's + 2D primitives are extruded (verts and lines are extruded to generate lines and quads). + However, if the extrusions strategy is "all_edges", then every edge of the 2D primitives + is used to sweep out a quadrilateral polygon (again verts and lines are swept to produce lines and quads). + + Warning: + The extrusion direction is assumed to define an infinite line. + The intersection with the trim surface is along a ray from the - to + direction, + however only the first intersection is taken. + Some polygonal objects have no free edges (e.g., sphere). When swept, this will result in two separate + surfaces if capping is on and "boundary_edges" enabled, + or no surface if capping is off and "boundary_edges" is enabled. + If all the extrusion lines emanating from an extruding primitive do not intersect the trim surface, + then no output for that primitive will be generated. In extreme cases, it is possible that no output + whatsoever will be generated. + + Example: + ```python + from vedo import * + sphere = Sphere([-1,0,4]).rotate_x(25).wireframe().color('red5') + circle = Circle([0,0,0], r=2, res=100).color('b6') + extruded_circle = circle.extrude_and_trim_with( + sphere, + direction=[0,-0.2,1], + strategy="bound", + cap=True, + cap_strategy="intersection", + ) + circle.lw(3).color("tomato").shift(dz=-0.1) + show(circle, sphere, extruded_circle, axes=1).close() + ``` + """ + trimmer = vtki.new("TrimmedExtrusionFilter") + trimmer.SetInputData(self.dataset) + trimmer.SetCapping(cap) + trimmer.SetExtrusionDirection(direction) + trimmer.SetTrimSurfaceData(surface.dataset) + if "bound" in strategy: + trimmer.SetExtrusionStrategyToBoundaryEdges() + elif "all" in strategy: + trimmer.SetExtrusionStrategyToAllEdges() + else: + vedo.logger.warning(f"extrude_and_trim(): unknown strategy {strategy}") + # print (trimmer.GetExtrusionStrategy()) + + if "intersect" in cap_strategy: + trimmer.SetCappingStrategyToIntersection() + elif "min" in cap_strategy: + trimmer.SetCappingStrategyToMinimumDistance() + elif "max" in cap_strategy: + trimmer.SetCappingStrategyToMaximumDistance() + elif "ave" in cap_strategy: + trimmer.SetCappingStrategyToAverageDistance() + else: + vedo.logger.warning(f"extrude_and_trim(): unknown cap_strategy {cap_strategy}") + # print (trimmer.GetCappingStrategy()) + + trimmer.Update() + + m = Mesh(trimmer.GetOutput()) + m.copy_properties_from(self).flat().lighting("default") + m.pipeline = OperationNode( + "extrude_and_trim", parents=[self, surface], + comment=f"#pts {m.dataset.GetNumberOfPoints()}" + ) + m.name = "ExtrudedAndTrimmedMesh" + return m + def split( self, maxdepth=1000, flag=False, must_share_edge=False, sort_by_area=True ) -> Union[List["Mesh"], "Mesh"]: diff --git a/vedo/shapes.py b/vedo/shapes.py index 28006114..92d31638 100644 --- a/vedo/shapes.py +++ b/vedo/shapes.py @@ -1409,7 +1409,7 @@ class NormalLines(Mesh): def __init__(self, msh, ratio=1, on="cells", scale=1.0) -> None: - poly = msh.clone().compute_normals().dataset + poly = msh.clone().dataset if "cell" in on: centers = vtki.new("CellCenters") diff --git a/vedo/vtkclasses.py b/vedo/vtkclasses.py index e4badd19..c4e8ccee 100644 --- a/vedo/vtkclasses.py +++ b/vedo/vtkclasses.py @@ -372,6 +372,7 @@ "vtkSelectEnclosedPoints", "vtkSelectPolyData", "vtkSubdivideTetra", + "vtkTrimmedExtrusionFilter", ]: location[name] = "vtkFiltersModeling"