diff --git a/appveyor.yml b/appveyor.yml index 171ed7316..5364fb68a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,25 +20,27 @@ init: - cmd: Miniforge.exe /InstallationType=JustMe /RegisterPython=0 /S /D=%MINICONDA_DIRNAME% - cmd: set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%" - cmd: activate + - cmd: set MAMBA_ROOT_PREFIX=C:/Miniforge/Library - sh: curl -sL https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$OS-x86_64.sh > miniconda.sh - sh: bash miniconda.sh -b -p $HOME/miniconda; - sh: export PATH="$HOME/miniconda/bin:$HOME/miniconda/lib:$PATH"; - sh: source $HOME/miniconda/bin/activate + - sh: export MAMBA_ROOT_PREFIX=$HOME/miniconda install: + - conda config --set always_yes yes - mamba env create -f environment.yml - - conda activate cadquery - - conda list + - mamba list -n cadquery build: false test_script: - - black . --diff --check - - mypy cadquery - - pytest -v --cov + - mamba run -n cadquery black . --diff --check + - mamba run -n cadquery mypy cadquery + - mamba run -n cadquery pytest -v --cov on_success: - - codecov + - mamba run -n cadquery codecov #on_finish: # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/cadquery/vis.py b/cadquery/vis.py index 7e9c8d14f..b236dac98 100644 --- a/cadquery/vis.py +++ b/cadquery/vis.py @@ -1,6 +1,5 @@ from . import Shape, Workplane, Assembly, Sketch, Compound, Color, Vector, Location -from .occ_impl.exporters.assembly import _vtkRenderWindow -from .occ_impl.assembly import _loc2vtk +from .occ_impl.assembly import _loc2vtk, toVTK from typing import Union, Any, List, Tuple @@ -15,8 +14,10 @@ vtkMapper, vtkRenderWindowInteractor, vtkActor, + vtkProp, vtkPolyDataMapper, vtkAssembly, + vtkRenderWindow, ) from vtkmodules.vtkCommonCore import vtkPoints from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkPolyData @@ -27,8 +28,14 @@ DEFAULT_PT_SIZE = 7.5 DEFAULT_PT_COLOR = "darkviolet" +SPECULAR = 0.3 +SPECULAR_POWER = 100 +SPECULAR_COLOR = vtkNamedColors().GetColor3d("White") + ShapeLike = Union[Shape, Workplane, Assembly, Sketch, TopoDS_Shape] -Showable = Union[ShapeLike, List[ShapeLike], Vector, List[Vector]] +Showable = Union[ + ShapeLike, List[ShapeLike], Vector, List[Vector], vtkProp, List[vtkProp] +] def _to_assy(*objs: ShapeLike, alpha: float = 1) -> Assembly: @@ -50,7 +57,9 @@ def _to_assy(*objs: ShapeLike, alpha: float = 1) -> Assembly: return assy -def _split_showables(objs) -> Tuple[List[ShapeLike], List[Vector], List[Location]]: +def _split_showables( + objs, +) -> Tuple[List[ShapeLike], List[Vector], List[Location], List[vtkProp]]: """ Split into showables and others. """ @@ -58,6 +67,7 @@ def _split_showables(objs) -> Tuple[List[ShapeLike], List[Vector], List[Location rv_s: List[ShapeLike] = [] rv_v: List[Vector] = [] rv_l: List[Location] = [] + rv_a: List[vtkProp] = [] for el in objs: if instance_of(el, ShapeLike): @@ -66,21 +76,24 @@ def _split_showables(objs) -> Tuple[List[ShapeLike], List[Vector], List[Location rv_v.append(el) elif isinstance(el, Location): rv_l.append(el) + elif isinstance(el, vtkProp): + rv_a.append(el) elif isinstance(el, list): - tmp1, tmp2, tmp3 = _split_showables(el) # split recursively + tmp1, tmp2, tmp3, tmp4 = _split_showables(el) # split recursively rv_s.extend(tmp1) rv_v.extend(tmp2) rv_l.extend(tmp3) + rv_a.extend(tmp4) - return rv_s, rv_v, rv_l + return rv_s, rv_v, rv_l, rv_a def _to_vtk_pts( vecs: List[Vector], size: float = DEFAULT_PT_SIZE, color: str = DEFAULT_PT_COLOR ) -> vtkActor: """ - Convert vectors to vtkActor. + Convert Vectors to vtkActor. """ rv = vtkActor() @@ -110,7 +123,7 @@ def _to_vtk_pts( def _to_vtk_axs(locs: List[Location], scale: float = 0.1) -> vtkActor: """ - Convert vectors to vtkActor. + Convert Locations to vtkActor. """ rv = vtkAssembly() @@ -135,6 +148,8 @@ def show( alpha: float = 1, tolerance: float = 1e-3, edges: bool = False, + specular: bool = True, + title: str = "CQ viewer", **kwrags: Any, ): """ @@ -142,7 +157,7 @@ def show( """ # split objects - shapes, vecs, locs = _split_showables(objs) + shapes, vecs, locs, props = _split_showables(objs) # construct the assy assy = _to_assy(*shapes, alpha=alpha) @@ -151,19 +166,28 @@ def show( pts = _to_vtk_pts(vecs) axs = _to_vtk_axs(locs, scale=scale) - # create a VTK window - win = _vtkRenderWindow(assy, tolerance=tolerance) + # assy+renderer + renderer = toVTK(assy, tolerance=tolerance) - win.SetWindowName("CQ viewer") + # VTK window boilerplate + win = vtkRenderWindow() + win.SetWindowName(title) + win.AddRenderer(renderer) # get renderer and actor - if edges: - ren = win.GetRenderers().GetFirstRenderer() - for act in ren.GetActors(): - act.GetProperty().EdgeVisibilityOn() + for act in renderer.GetActors(): + + propt = act.GetProperty() + + if edges: + propt.EdgeVisibilityOn() + + if specular: + propt.SetSpecular(SPECULAR) + propt.SetSpecularPower(SPECULAR_POWER) + propt.SetSpecularColor(SPECULAR_COLOR) # rendering related settings - win.SetMultiSamples(16) vtkMapper.SetResolveCoincidentTopologyToPolygonOffset() vtkMapper.SetResolveCoincidentTopologyPolygonOffsetParameters(1, 0) vtkMapper.SetResolveCoincidentTopologyLineOffsetParameters(-1, 0) @@ -193,7 +217,7 @@ def show( orient_widget.InteractiveOff() # use gradient background - renderer = win.GetRenderers().GetFirstRenderer() + renderer.SetBackground(1, 1, 1) renderer.GradientBackgroundOn() # use FXXAA @@ -209,9 +233,15 @@ def show( renderer.AddActor(pts) renderer.AddActor(axs) + # add other vtk actors + for p in props: + renderer.AddActor(p) + # initialize and set size inter.Initialize() - win.SetSize(*win.GetScreenSize()) + + w, h = win.GetScreenSize() + win.SetSize((w // 2, h // 2)) win.SetPosition(-10, 0) # show and return diff --git a/doc/_static/show.PNG b/doc/_static/show.PNG new file mode 100644 index 000000000..2fa8ff4e8 Binary files /dev/null and b/doc/_static/show.PNG differ diff --git a/doc/_static/show_demo.PNG b/doc/_static/show_demo.PNG new file mode 100644 index 000000000..88de9e5b6 Binary files /dev/null and b/doc/_static/show_demo.PNG differ diff --git a/doc/_static/show_jupyter.PNG b/doc/_static/show_jupyter.PNG new file mode 100644 index 000000000..49212b089 Binary files /dev/null and b/doc/_static/show_jupyter.PNG differ diff --git a/doc/_static/show_vtk.PNG b/doc/_static/show_vtk.PNG new file mode 100644 index 000000000..f838bba15 Binary files /dev/null and b/doc/_static/show_vtk.PNG differ diff --git a/doc/index.rst b/doc/index.rst index c3858a855..20e818301 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -41,6 +41,7 @@ Table Of Contents sketch.rst assy.rst free-func.rst + vis.rst fileformat.rst examples.rst apireference.rst diff --git a/doc/primer.rst b/doc/primer.rst index 2d0572483..bdf25fe2e 100644 --- a/doc/primer.rst +++ b/doc/primer.rst @@ -27,157 +27,24 @@ kernel, there is another set of Geometrical constructs involved as well. For exa hold a reference to an underlying curve that is a full circle, and each linear edge holds underneath it the equation for a line. CadQuery shields you from these constructs. - -Workplane class ---------------------------- - -The Workplane class contains the currently selected objects (a list of Shapes, Vectors or Locations -in the :attr:`~cadquery.Workplane.objects` attribute), the modelling context (in the -:attr:`~cadquery.Workplane.ctx` attribute), and CadQuery's fluent api methods. It is the main class -that users will instantiate. - -See :ref:`apireference` to learn more. - - -Workplanes ---------------------------- - -Most CAD programs use the concept of Workplanes. If you have experience with other CAD programs you will probably -feel comfortable with CadQuery's Workplanes, but if you don't have experience then they are an essential concept to -understand. - -Workplanes represent a plane in space, from which other features can be located. They have a center point and a local -coordinate system. Most methods that create an object do so relative to the current workplane. - -Usually the first workplane created is the "XY" plane, also known as the "front" plane. Once a solid is defined the most -common way to create a workplane is to select a face on the solid that you intend to modify and create a new workplane -relative to it. You can also create new workplanes in anywhere in world coordinate system, or relative to other planes -using offsets or rotations. - -The most powerful feature of workplanes is that they allow you to work in 2D space in the coordinate system of the -workplane, and then CadQuery will transform these points from the workplane coordinate system to the world coordinate -system so your 3D features are located where you intended. This makes scripts much easier to create and maintain. - -See :py:class:`cadquery.Workplane` to learn more. - - -2D Construction ---------------------------- - -Once you create a workplane, you can work in 2D, and then later use the features you create to make 3D objects. -You'll find all of the 2D constructs you expect -- circles, lines, arcs, mirroring, points, etc. - -See :ref:`2dOperations` to learn more. - - -3D Construction ---------------------------- - -You can construct 3D primitives such as boxes, wedges, cylinders and spheres directly. You can also sweep, extrude, -and loft 2D geometry to form 3D features. Of course the basic primitive operations are also available. - -See :ref:`3doperations` to learn more. - - - -Selectors ---------------------------- - -Selectors allow you to select one or more features, in order to define new features. As an example, you might -extrude a box, and then select the top face as the location for a new feature. Or, you might extrude a box, and -then select all of the vertical edges so that you can apply a fillet to them. - -You can select Vertices, Edges, Faces, Solids, and Wires using selectors. - -Think of selectors as the equivalent of your hand and mouse, if you were to build an object using a conventional CAD system. - -See :ref:`selectors` to learn more. - - -Construction Geometry ---------------------------- -Construction geometry are features that are not part of the object, but are only defined to aid in building the object. -A common example might be to define a rectangle, and then use the corners to define the location of a set of holes. - -Most CadQuery construction methods provide a ``forConstruction`` keyword, which creates a feature that will only be used -to locate other features. - - -The Stack ---------------------------- - -As you work in CadQuery, each operation returns a new Workplane object with the result of that -operations. Each Workplane object has a list of objects, and a reference to its parent. - -You can always go backwards to older operations by removing the current object from the stack. For example:: - - Workplane(someObject).faces(">Z").first().vertices() - -returns a CadQuery object that contains all of the vertices on the highest face of someObject. But you can always move -backwards in the stack to get the face as well:: - - Workplane(someObject).faces(">Z").first().vertices().end() - -You can browse stack access methods here: :ref:`stackMethods`. - - -.. _chaining: - -Chaining ---------------------------- - -All Workplane methods return another Workplane object, so that you can chain the methods together -fluently. Use the core Workplane methods to get at the objects that were created. - -Each time a new Workplane object is produced during these chained calls, it has a -:attr:`~cadquery.Workplane.parent` attribute that points to the Workplane object that created it. -Several CadQuery methods search this parent chain, for example when searching for the context solid. -You can also give a Workplane object a tag, and further down your chain of calls you can refer back -to this particular object using its tag. - - -The Context Solid ---------------------------- - -Most of the time, you are building a single object, and adding features to that single object. CadQuery watches -your operations, and defines the first solid object created as the 'context solid'. After that, any features -you create are automatically combined (unless you specify otherwise) with that solid. This happens even if the -solid was created a long way up in the stack. For example:: - - Workplane("XY").box(1, 2, 3).faces(">Z").circle(0.25).extrude(1) - -Will create a 1x2x3 box, with a cylindrical boss extending from the top face. It was not necessary to manually -combine the cylinder created by extruding the circle with the box, because the default behavior for extrude is -to combine the result with the context solid. The hole() method works similarly -- CadQuery presumes that you want -to subtract the hole from the context solid. - -If you want to avoid this, you can specify ``combine=False``, and CadQuery will create the solid separately. - - -Iteration ---------------------------- - -CAD models often have repeated geometry, and its really annoying to resort to for loops to construct features. -Many CadQuery methods operate automatically on each element on the stack, so that you don't have to write loops. -For example, this:: - - Workplane("XY").box(1, 2, 3).faces(">Z").vertices().circle(0.5) - -Will actually create 4 circles, because ``vertices()`` selects 4 vertices of a rectangular face, and the ``circle()`` method -iterates on each member of the stack. - -This is really useful to remember when you author your own plugins. :py:meth:`cadquery.Workplane.each` is useful for this purpose. - CadQuery API layers --------------------------- Once you start to dive a bit more into CadQuery, you may find yourself a bit confused juggling between different types of objects the CadQuery APIs can return. This chapter aims to give an explanation on this topic and to provide background on the underlying implementation and kernel layers so you can leverage more of CadQuery functionality. -CadQuery is composed of 3 different API, which are implemented on top of each other. +CadQuery is composed of 4 different API, which are implemented on top of each other. 1. The Fluent API + #. :class:`~cadquery.Workplane` + #. :class:`~cadquery.Sketch` + #. :class:`~cadquery.Assembly` 2. The Direct API + #. :class:`~cadquery.Shape` +2. The Geometry API + #. :class:`~cadquery.Vector` + #. :class:`~cadquery.Plane` + #. :class:`~cadquery.Location` 3. The OCCT API The Fluent API @@ -799,6 +666,148 @@ that is shown in the 3D view above. rather than the object from the previous step. +Workplane class +--------------------------- + +The Workplane class contains the currently selected objects (a list of Shapes, Vectors or Locations +in the :attr:`~cadquery.Workplane.objects` attribute), the modelling context (in the +:attr:`~cadquery.Workplane.ctx` attribute), and CadQuery's fluent api methods. It is the main class +that users will instantiate. + +See :ref:`apireference` to learn more. + + +Workplanes +--------------------------- + +Most CAD programs use the concept of Workplanes. If you have experience with other CAD programs you will probably +feel comfortable with CadQuery's Workplanes, but if you don't have experience then they are an essential concept to +understand. + +Workplanes represent a plane in space, from which other features can be located. They have a center point and a local +coordinate system. Most methods that create an object do so relative to the current workplane. + +Usually the first workplane created is the "XY" plane, also known as the "front" plane. Once a solid is defined the most +common way to create a workplane is to select a face on the solid that you intend to modify and create a new workplane +relative to it. You can also create new workplanes anywhere in the world coordinate system, or relative to other planes +using offsets or rotations. + +The most powerful feature of workplanes is that they allow you to work in 2D space in the coordinate system of the +workplane, and then CadQuery will transform these points from the workplane coordinate system to the world coordinate +system so your 3D features are located where you intended. This makes scripts much easier to create and maintain. + +See :py:class:`cadquery.Workplane` to learn more. + + +2D Construction +--------------------------- + +Once you create a workplane, you can work in 2D, and then later use the features you create to make 3D objects. +You'll find all of the 2D constructs you expect -- circles, lines, arcs, mirroring, points, etc. + +See :ref:`2dOperations` to learn more. + + +3D Construction +--------------------------- + +You can construct 3D primitives such as boxes, wedges, cylinders and spheres directly. You can also sweep, extrude, +and loft 2D geometry to form 3D features. Of course the basic primitive operations are also available. + +See :ref:`3doperations` to learn more. + + + +Selectors +--------------------------- + +Selectors allow you to select one or more features, in order to define new features. As an example, you might +extrude a box, and then select the top face as the location for a new feature. Or, you might extrude a box, and +then select all of the vertical edges so that you can apply a fillet to them. + +You can select Vertices, Edges, Faces, Solids, and Wires using selectors. + +Think of selectors as the equivalent of your hand and mouse, if you were to build an object using a conventional CAD system. + +See :ref:`selectors` to learn more. + + +Construction Geometry +--------------------------- +Construction geometry are features that are not part of the object, but are only defined to aid in building the object. +A common example might be to define a rectangle, and then use the corners to define the location of a set of holes. + +Most CadQuery construction methods provide a ``forConstruction`` keyword, which creates a feature that will only be used +to locate other features. + + +The Stack +--------------------------- + +As you work in CadQuery, each operation returns a new Workplane object with the result of that +operations. Each Workplane object has a list of objects, and a reference to its parent. + +You can always go backwards to older operations by removing the current object from the stack. For example:: + + Workplane(someObject).faces(">Z").first().vertices() + +returns a CadQuery object that contains all of the vertices on the highest face of someObject. But you can always move +backwards in the stack to get the face as well:: + + Workplane(someObject).faces(">Z").first().vertices().end() + +You can browse stack access methods here: :ref:`stackMethods`. + + +.. _chaining: + +Chaining +--------------------------- + +All Workplane methods return another Workplane object, so that you can chain the methods together +fluently. Use the core Workplane methods to get at the objects that were created. + +Each time a new Workplane object is produced during these chained calls, it has a +:attr:`~cadquery.Workplane.parent` attribute that points to the Workplane object that created it. +Several CadQuery methods search this parent chain, for example when searching for the context solid. +You can also give a Workplane object a tag, and further down your chain of calls you can refer back +to this particular object using its tag. + + +The Context Solid +--------------------------- + +Most of the time, you are building a single object, and adding features to that single object. CadQuery watches +your operations, and defines the first solid object created as the 'context solid'. After that, any features +you create are automatically combined (unless you specify otherwise) with that solid. This happens even if the +solid was created a long way up in the stack. For example:: + + Workplane("XY").box(1, 2, 3).faces(">Z").circle(0.25).extrude(1) + +Will create a 1x2x3 box, with a cylindrical boss extending from the top face. It was not necessary to manually +combine the cylinder created by extruding the circle with the box, because the default behavior for extrude is +to combine the result with the context solid. The hole() method works similarly -- CadQuery presumes that you want +to subtract the hole from the context solid. + +If you want to avoid this, you can specify ``combine=False``, and CadQuery will create the solid separately. + + +Iteration +--------------------------- + +CAD models often have repeated geometry, and its really annoying to resort to for loops to construct features. +Many CadQuery methods operate automatically on each element on the stack, so that you don't have to write loops. +For example, this:: + + Workplane("XY").box(1, 2, 3).faces(">Z").vertices().circle(0.5) + +Will actually create 4 circles, because ``vertices()`` selects 4 vertices of a rectangular face, and the ``circle()`` method +iterates on each member of the stack. + +This is really useful to remember when you author your own plugins. :py:meth:`cadquery.Workplane.each` is useful for this purpose. + + + Assemblies ---------- diff --git a/doc/vis.rst b/doc/vis.rst new file mode 100644 index 000000000..bb448ea78 --- /dev/null +++ b/doc/vis.rst @@ -0,0 +1,94 @@ +.. _vis: + +=========================== +Visualization +=========================== + + +Pure Python +=========== + +Since version 2.4 CadQuery supports visualization without any external tools. Those facilities are based on the VTK library +and are not tied to any external tool. + +.. code-block:: python + + from cadquery import * + from cadquery.vis import show + + w = Workplane().sphere(1).split(keepBottom=True) - Workplane().sphere(0.5) + r = w.faces('>Z').fillet(0.1) + + # Show the result + show(r, alpha=0.5) + + +.. image:: _static/show.PNG + + +One can visualize objects of type :class:`~cadquery.Workplane`, :class:`~cadquery.Sketch`, :class:`~cadquery.Assembly`, :class:`~cadquery.Shape`, +:class:`~cadquery.Vector`, :class:`~cadquery.Location` and lists thereof. + + +.. code-block:: python + + from cadquery import * + from cadquery.occ_impl.shapes import * + from cadquery.vis import show + + w = Workplane().sphere(0.5).split(keepTop=True) + sk = Sketch().rect(1.5, 1.5) + sh = torus(5, 0.5) + + r = rect(2, 2) + c = circle(2) + + N = 50 + params = [i/N for i in range(N)] + + vecs = r.positions(params) + locs = c.locations(params) + + # Render the solid + show(w, sk, sh, vecs, locs) + + +.. image:: _static/show_demo.PNG + + +Additionally it is possible to integrate with other libraries using VTK and display any `vtkProp` object. + + +.. code-block:: python + + from cadquery.vis import show + from cadquery.occ_impl.shapes import torus + + from vtkmodules.vtkRenderingAnnotation import vtkAnnotatedCubeActor + + + a = vtkAnnotatedCubeActor() + t = torus(5,1) + + show(t, a) + +.. image:: _static/show_vtk.PNG + + +Note that currently the show function is blocking. + + +Jupyter/JupterLab +================= + +There is also more limited support for displaying :class:`~cadquery.Workplane`, :class:`~cadquery.Sketch`, :class:`~cadquery.Assembly`, +:class:`~cadquery.Shape` in Jupyter and JupyterLab. This functionality is implemented using VTK.js. + +.. code-block:: python + + from cadquery import * + + Workplane().sphere(1).split(keepTop=True) + +.. image:: _static/show_jupyter.PNG + diff --git a/tests/test_vis.py b/tests/test_vis.py index e7cbbe391..45b0a53df 100644 --- a/tests/test_vis.py +++ b/tests/test_vis.py @@ -1,12 +1,12 @@ -from cadquery import Workplane, Assembly, Sketch, Location, Vector, Location -from cadquery.vis import show, show_object +from cadquery import Workplane, Assembly, Sketch, Location, Vector +from cadquery.vis import show, show_object, vtkAxesActor -import cadquery.occ_impl.exporters.assembly as assembly import cadquery.vis as vis from vtkmodules.vtkRenderingCore import vtkRenderWindow, vtkRenderWindowInteractor +from vtkmodules.vtkRenderingAnnotation import vtkAnnotatedCubeActor -from pytest import fixture, raises +from pytest import fixture @fixture @@ -59,7 +59,7 @@ def test_show(wp, assy, sk, monkeypatch): # use some dummy vtk objects monkeypatch.setattr(vis, "vtkRenderWindowInteractor", FakeInteractor) - monkeypatch.setattr(assembly, "vtkRenderWindow", FakeWindow) + monkeypatch.setattr(vis, "vtkRenderWindow", FakeWindow) # simple smoke test show(wp) @@ -89,3 +89,6 @@ def test_show(wp, assy, sk, monkeypatch): # for now a workaround to be compatible with more complicated CQ-editor invocations show(1) + + # show a raw vtkProp + show(vtkAxesActor(), [vtkAnnotatedCubeActor()])