Moved the CadQuery library into a git subtree to ease confusion and maintenance problems.
This commit is contained in:
parent
cae32defee
commit
385ee15b43
|
@ -51,6 +51,10 @@ class CadQueryWorkbench (Workbench):
|
|||
libs_dir_path = os.path.join(module_base_path, 'Libs')
|
||||
sys.path.insert(0, libs_dir_path)
|
||||
|
||||
# Tack on our CadQuery library git subtree
|
||||
cq_lib_path = os.path.join(libs_dir_path, 'cadquery-lib')
|
||||
sys.path.insert(1, cq_lib_path)
|
||||
|
||||
#Make sure we get the right libs under the FreeCAD installation
|
||||
fc_base_path = os.path.dirname(os.path.dirname(module_base_path))
|
||||
fc_lib_path = os.path.join(fc_base_path, 'lib')
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,8 +0,0 @@
|
|||
***
|
||||
Core CadQuery implementation.
|
||||
|
||||
No files should depend on or import FreeCAD , pythonOCC, or other CAD Kernel libraries!!!
|
||||
Dependencies should be on the classes provided by implementation packages, which in turn
|
||||
can depend on CAD libraries.
|
||||
|
||||
***
|
|
@ -1,19 +0,0 @@
|
|||
#these items point to the freecad implementation
|
||||
from .freecad_impl.geom import Plane,BoundBox,Vector,Matrix,sortWiresByBuildOrder
|
||||
from .freecad_impl.shapes import Shape,Vertex,Edge,Face,Wire,Solid,Shell,Compound
|
||||
from .freecad_impl import exporters
|
||||
from .freecad_impl import importers
|
||||
|
||||
#these items are the common implementation
|
||||
|
||||
#the order of these matter
|
||||
from .selectors import NearestToPointSelector,ParallelDirSelector,DirectionSelector,PerpendicularDirSelector,TypeSelector,DirectionMinMaxSelector,StringSyntaxSelector,Selector
|
||||
from .CQ import CQ,CQContext,Workplane
|
||||
|
||||
|
||||
__all__ = [
|
||||
'CQ','Workplane','plugins','selectors','Plane','BoundBox','Matrix','Vector','sortWiresByBuildOrder',
|
||||
'Shape','Vertex','Edge','Wire','Solid','Shell','Compound','exporters', 'importers', 'NearestToPointSelector','ParallelDirSelector','DirectionSelector','PerpendicularDirSelector','TypeSelector','DirectionMinMaxSelector','StringSyntaxSelector','Selector','plugins'
|
||||
]
|
||||
|
||||
__version__ = "0.1.8"
|
|
@ -1,404 +0,0 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<title>CadQuery Cheatsheet</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
|
||||
<style type="text/css">
|
||||
.section {
|
||||
margin: 0.5em;
|
||||
padding: 0px 0.5em 0.5em;
|
||||
background-color: #EBEBEB;
|
||||
}
|
||||
.column {
|
||||
float: left;
|
||||
width: 375px;
|
||||
}
|
||||
tr {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
td {
|
||||
text-align: center;
|
||||
width: 5em;
|
||||
}
|
||||
h2 {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="column" style="width:475px;">
|
||||
<div class="section">
|
||||
<h2>Documentation</h2>
|
||||
<ul style="background-color:#ffffff;margin-bottom:0px;margin-top:0px;">
|
||||
<li><a href="http://parametricparts.com/docs/#">ParametricParts Documentation</a></li>
|
||||
<li><a href="https://github.com/dcowden/cadquery/blob/master/README.md">CadQuery Readme</a></li>
|
||||
<li><a href="http://parametricparts.com/docs/examples.html#examples">CadQuery Examples</a></li>
|
||||
<li><a href="http://parametricparts.com/docs/classreference.html">CadQuery Class Reference</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>BREP Terminology</h2><br />
|
||||
<table style="width:100%;">
|
||||
<tr>
|
||||
<td style="width:10%;"><strong>vertex</strong></td>
|
||||
<td style="width:90%;">A single point in space</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>edge</strong></td>
|
||||
<td>A connection between two or more vertices along a particular path (called a curve)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>wire</strong></td>
|
||||
<td>A collection of edges that are connected together</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>face</strong></td>
|
||||
<td>A set of edges or wires that enclose a surface</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>shell</strong></td>
|
||||
<td>A collection of faces that are connected together along some of their edges</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>solid</strong></td>
|
||||
<td>A shell that has a closed interior</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>compound</strong></td>
|
||||
<td>A collection of solids</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Named Planes</h2><br />
|
||||
Available named planes are as follows. Direction references refer to the global directions.
|
||||
<table style="width:100%;">
|
||||
<tr>
|
||||
<th style="width:25%;">Name</th>
|
||||
<th style="width:25%;">xDir</th>
|
||||
<th style="width:25%;">yDir</th>
|
||||
<th style="width:25%;">zDir</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>XY</td>
|
||||
<td>+x</td>
|
||||
<td>+y</td>
|
||||
<td>+z</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>YZ</td>
|
||||
<td>+y</td>
|
||||
<td>+z</td>
|
||||
<td>+x</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>XZ</td>
|
||||
<td>+x</td>
|
||||
<td>+z</td>
|
||||
<td>-y</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>front</td>
|
||||
<td>+x</td>
|
||||
<td>+y</td>
|
||||
<td>+z</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>back</td>
|
||||
<td>-x</td>
|
||||
<td>+y</td>
|
||||
<td>-z</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>left</td>
|
||||
<td>+z</td>
|
||||
<td>+y</td>
|
||||
<td>-x</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>right</td>
|
||||
<td>-z</td>
|
||||
<td>+y</td>
|
||||
<td>+x</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>top</td>
|
||||
<td>+x</td>
|
||||
<td>-z</td>
|
||||
<td>+y</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>bottom</td>
|
||||
<td>+x</td>
|
||||
<td>+z</td>
|
||||
<td>-y</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Core Classes</h2><br />
|
||||
<table style="width:100%;">
|
||||
<tr>
|
||||
<th style="width:40%;">Class</th>
|
||||
<th style="width:60%;">Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CQ(obj)</td>
|
||||
<td>Provides enhanced functionality for a wrapped CAD primitive.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Plane(origin, xDir, normal)</td>
|
||||
<td>A 2d coordinate system in space, with the x-y axes on the a plane, and a particular point as the origin.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Workplane(inPlane[origin, obj])</td>
|
||||
<td>Defines a coordinate system in space, in which 2-d coordinates can be used.</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column" style="width:600px;">
|
||||
<div class="section">
|
||||
<h2>Selector Methods</h2><br />
|
||||
CadQuery selector strings allow filtering various types of object lists.
|
||||
Most commonly, Edges, Faces, and Vertices are used, but all objects types can be filtered.<br />
|
||||
<table style="width:100%;">
|
||||
<tr>
|
||||
<th style="width:40%;">Selector Method</th>
|
||||
<th style="width:60%;">Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="http://parametricparts.com/docs/classreference.html#cadfile.cadutils.cadquery.CQ.faces">CQ.faces(selector=None)</a></td>
|
||||
<td>Select the faces of objects on the stack, optionally filtering the selection.</td>
|
||||
<tr>
|
||||
<td><a href="http://parametricparts.com/docs/classreference.html#cadfile.cadutils.cadquery.CQ.edges">CQ.edges(selector=None)</a></td>
|
||||
<td>Select the edges of objects on the stack, optionally filtering the selection.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="http://parametricparts.com/docs/classreference.html#cadfile.cadutils.cadquery.CQ.vertices">CQ.vertices(selector=None)</a></td>
|
||||
<td>Select the vertices of objects on the stack, optionally filtering the selection.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="http://parametricparts.com/docs/classreference.html#cadfile.cadutils.cadquery.CQ.solids">CQ.solids(selector=None)</a></td>
|
||||
<td>Select the solids of objects on the stack, optionally filtering the selection.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="http://parametricparts.com/docs/classreference.html#cadfile.cadutils.cadquery.CQ.shells">CQ.shells(selector=None)</a></td>
|
||||
<td>Select the shells of objects on the stack, optionally filtering the selection.</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Selector Classes</h2><br />
|
||||
<table style="width:100%;">
|
||||
<tr>
|
||||
<th style="width:40%;">Class</th>
|
||||
<th style="width:60%;">Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NearestToPointSelector(pnt)</td>
|
||||
<td>Selects object nearest the provided point.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ParallelDirSelector(vector[tolerance])</td>
|
||||
<td>Selects objects parallel with the provided direction.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DirectionSelector(vector[tolerance])</td>
|
||||
<td>Selects objects aligned with the provided direction.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PerpendicularDirSelector(vector[tolerance])</td>
|
||||
<td>Selects objects perpendicular with the provided direction.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TypeSelector(typeString)</td>
|
||||
<td>Selects objects of the prescribed topological type.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DirectionMinMaxSelector(vector[directionMax])</td>
|
||||
<td>Selects objects closest or farthest in the specified direction.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>StringSyntaxSelector(selectorString)</td>
|
||||
<td>Filter lists objects using a simple string syntax.</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Selector String Modifiers</h2><br />
|
||||
Selectors are a complex topic: see <a href="http://parametricparts.com/docs/selectors.html">CadQuery String Selectors</a> for more information.<br />
|
||||
Axis Strings are: X, Y, Z, XY, YZ, XZ
|
||||
<table style="width:100%;">
|
||||
<tr>
|
||||
<th style="width:10%;">Modifier</th>
|
||||
<th style="width:90%;">Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>|</td>
|
||||
<td>Parallel to (same as <a href="http://parametricparts.com/docs/classreference.html?highlight=paralleldirselector#cadfile.cadutils.cadquery.ParallelDirSelector">ParallelDirSelector</a>). Can return multiple objects.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>#</td>
|
||||
<td>Perpendicular to (same as <a href="http://parametricparts.com/docs/classreference.html?highlight=perpendiculardirselector#cadfile.cadutils.cadquery.PerpendicularDirSelector">PerpendicularDirSelector</a>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>+</td>
|
||||
<td>Positive direction (same as <a href="http://parametricparts.com/docs/classreference.html?highlight=directionselector#cadfile.cadutils.cadquery.DirectionSelector">DirectionSelector</a>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>-</td>
|
||||
<td>Negative direction (same as <a href="http://parametricparts.com/docs/classreference.html?highlight=directionselector#cadfile.cadutils.cadquery.DirectionSelector">DirectionSelector</a>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>></td>
|
||||
<td>Maximize (same as <a href="http://parametricparts.com/docs/classreference.html?highlight=directionminmaxselector#cadfile.cadutils.cadquery.DirectionMinMaxSelector">DirectionMinMaxSelector</a> with directionMax=True)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><</td>
|
||||
<td>Minimize (same as <a href="http://parametricparts.com/docs/classreference.html?highlight=directionminmaxselector#cadfile.cadutils.cadquery.DirectionMinMaxSelector">DirectionMinMaxSelector</a> with directionMax=False)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>%</td>
|
||||
<td>Curve/surface type (same as <a href="http://parametricparts.com/docs/classreference.html?highlight=typeselector#cadfile.cadutils.cadquery.TypeSelector">TypeSelector</a>)</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Examples of Filtering Faces</h2><br />
|
||||
All types of filters work on faces. In most cases, the selector refers to the direction of the normal vector of the face.
|
||||
If a face is not planar, selectors are evaluated at the center of mass of the face. This can lead to results that are quite unexpected.
|
||||
<table style="width:100%;">
|
||||
<tr>
|
||||
<th style="width:10%;">Selector</th>
|
||||
<th style="width:40%;">Selector Class</th>
|
||||
<th style="width:40%;">Selects</th>
|
||||
<th style="width:10%;"># Objects Returned</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>+Z</td>
|
||||
<td>DirectionSelector</td>
|
||||
<td>Faces with normal in +z direction</td>
|
||||
<td>0 or 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>|Z</td>
|
||||
<td>ParallelDirSelector</td>
|
||||
<td>Faces parallel to xy plane</td>
|
||||
<td>0..many</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>-X</td>
|
||||
<td>DirectionSelector</td>
|
||||
<td>Faces with normal in neg x direction</td>
|
||||
<td>0..many</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>#Z</td>
|
||||
<td>PerpendicularDirSelector</td>
|
||||
<td>Faces perpendicular to z direction</td>
|
||||
<td>0..many</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>%Plane</td>
|
||||
<td>TypeSelector</td>
|
||||
<td>Faces of type plane</td>
|
||||
<td>0..many</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>>Y</td>
|
||||
<td>DirectionMinMaxSelector</td>
|
||||
<td>Face farthest in the positive y dir</td>
|
||||
<td>0 or 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><Y</td>
|
||||
<td>DirectionMinMaxSelector</td>
|
||||
<td>Face farthest in the negative y dir</td>
|
||||
<td>0 or 1</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Examples of Filtering Edges</h2><br />
|
||||
Some filter types are not supported for edges. The selector usually refers to the direction of the edge.
|
||||
Non-linear edges are not selected for any selectors except type (%). Non-linear edges are never returned when these filters are applied.
|
||||
<table style="width:100%;">
|
||||
<tr>
|
||||
<th style="width:10%;">Selector</th>
|
||||
<th style="width:40%;">Selector Class</th>
|
||||
<th style="width:40%;">Selects</th>
|
||||
<th style="width:10%;"># Objects Returned</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>+Z</td>
|
||||
<td>DirectionSelector</td>
|
||||
<td>Edges aligned in the Z direction</td>
|
||||
<td>0..many</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>|Z</td>
|
||||
<td>ParallelDirSelector</td>
|
||||
<td>Edges parallel to z direction</td>
|
||||
<td>0..many</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>-X</td>
|
||||
<td>DirectionSelector</td>
|
||||
<td>Edges aligned in neg x direction</td>
|
||||
<td>0..many</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>#Z</td>
|
||||
<td>PerpendicularDirSelector</td>
|
||||
<td>Edges perpendicular to z direction</td>
|
||||
<td>0..many</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>%Plane</td>
|
||||
<td>TypeSelector</td>
|
||||
<td>Edges type line</td>
|
||||
<td>0..many</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>>Y</td>
|
||||
<td>DirectionMinMaxSelector</td>
|
||||
<td>Edges farthest in the positive y dir</td>
|
||||
<td>0 or 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><Y</td>
|
||||
<td>DirectionMinMaxSelector</td>
|
||||
<td>Edges farthest in the negative y dir</td>
|
||||
<td>0 or 1</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Examples of Filtering Vertices</h2><br />
|
||||
Only a few of the filter types apply to vertices. The location of the vertex is the subject of the filter.
|
||||
<table style="width:100%;">
|
||||
<tr>
|
||||
<th style="width:10%;">Selector</th>
|
||||
<th style="width:40%;">Selector Class</th>
|
||||
<th style="width:40%;">Selects</th>
|
||||
<th style="width:10%;"># Objects Returned</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>>Y</td>
|
||||
<td>DirectionMinMaxSelector</td>
|
||||
<td>Vertices farthest in the positive y dir</td>
|
||||
<td>0 or 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><Y</td>
|
||||
<td>DirectionMinMaxSelector</td>
|
||||
<td>Vertices farthest in the negative y dir</td>
|
||||
<td>0 or 1</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,18 +0,0 @@
|
|||
"""
|
||||
Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC
|
||||
|
||||
This file is part of CadQuery.
|
||||
|
||||
CadQuery is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
CadQuery is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; If not, see <http://www.gnu.org/licenses/>
|
||||
"""
|
|
@ -1,85 +0,0 @@
|
|||
"""
|
||||
A special directive for including a cq object.
|
||||
|
||||
"""
|
||||
|
||||
import sys, os, shutil, imp, warnings, cStringIO, re,traceback
|
||||
|
||||
from cadquery import *
|
||||
import StringIO
|
||||
from docutils.parsers.rst import directives
|
||||
|
||||
|
||||
template = """
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<div class="cq" style="text-align:%(txtAlign)s;float:left;">
|
||||
%(outSVG)s
|
||||
</div>
|
||||
<div style="clear:both;">
|
||||
</div>
|
||||
|
||||
"""
|
||||
template_content_indent = ' '
|
||||
|
||||
|
||||
def cq_directive(name, arguments, options, content, lineno,
|
||||
content_offset, block_text, state, state_machine):
|
||||
|
||||
#only consider inline snippets
|
||||
plot_code = '\n'.join(content)
|
||||
|
||||
# Since we don't have a filename, use a hash based on the content
|
||||
#the script must define a variable called 'out', which is expected to
|
||||
#be a CQ object
|
||||
outSVG = "Your Script Did not assign the 'result' variable!"
|
||||
|
||||
|
||||
try:
|
||||
_s = StringIO.StringIO()
|
||||
exec(plot_code)
|
||||
|
||||
exporters.exportShape(result,"SVG",_s)
|
||||
outSVG = _s.getvalue()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
outSVG = traceback.format_exc()
|
||||
|
||||
#now out
|
||||
# Now start generating the lines of output
|
||||
lines = []
|
||||
|
||||
#get rid of new lines
|
||||
outSVG = outSVG.replace('\n','')
|
||||
|
||||
txtAlign = "left"
|
||||
if options.has_key("align"):
|
||||
txtAlign = options['align']
|
||||
|
||||
lines.extend((template % locals()).split('\n'))
|
||||
|
||||
lines.extend(['::', ''])
|
||||
lines.extend([' %s' % row.rstrip()
|
||||
for row in plot_code.split('\n')])
|
||||
lines.append('')
|
||||
|
||||
if len(lines):
|
||||
state_machine.insert_input(
|
||||
lines, state_machine.input_lines.source(0))
|
||||
|
||||
return []
|
||||
|
||||
def setup(app):
|
||||
setup.app = app
|
||||
setup.config = app.config
|
||||
setup.confdir = app.confdir
|
||||
|
||||
options = {'height': directives.length_or_unitless,
|
||||
'width': directives.length_or_percentage_or_unitless,
|
||||
'align': directives.unchanged
|
||||
}
|
||||
|
||||
app.add_directive('cq_plot', cq_directive, True, (0, 2, 0), **options)
|
||||
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
It is ok for files in this directory to import FreeCAD, FreeCAD.Base, and FreeCAD.Part.
|
||||
|
||||
Other modules should _not_ depend on FreeCAD
|
|
@ -1,112 +0,0 @@
|
|||
"""
|
||||
Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC
|
||||
|
||||
This file is part of CadQuery.
|
||||
|
||||
CadQuery is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
CadQuery is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; If not, see <http://www.gnu.org/licenses/>
|
||||
"""
|
||||
import os, sys
|
||||
|
||||
|
||||
def _fc_path():
|
||||
"""Find FreeCAD"""
|
||||
_PATH = ""
|
||||
if _PATH:
|
||||
return _PATH
|
||||
|
||||
#look for FREECAD_LIB env variable
|
||||
if os.environ.has_key('FREECAD_LIB'):
|
||||
_PATH = os.environ.get('FREECAD_LIB')
|
||||
if os.path.exists( _PATH):
|
||||
return _PATH
|
||||
|
||||
if sys.platform.startswith('linux'):
|
||||
#Make some dangerous assumptions...
|
||||
for _PATH in [
|
||||
os.path.join(os.path.expanduser("~"), "lib/freecad/lib"),
|
||||
"/usr/local/lib/freecad/lib",
|
||||
"/usr/lib/freecad/lib",
|
||||
"/opt/freecad/lib/",
|
||||
"/usr/bin/freecad/lib",
|
||||
"/usr/lib/freecad",
|
||||
]:
|
||||
if os.path.exists(_PATH):
|
||||
return _PATH
|
||||
|
||||
elif sys.platform.startswith('win'):
|
||||
#try all the usual suspects
|
||||
for _PATH in [
|
||||
"c:/Program Files/FreeCAD0.12/bin",
|
||||
"c:/Program Files/FreeCAD0.13/bin",
|
||||
"c:/Program Files/FreeCAD0.14/bin",
|
||||
"c:/Program Files/FreeCAD0.15/bin",
|
||||
"c:/Program Files/FreeCAD0.16/bin",
|
||||
"c:/Program Files/FreeCAD0.17/bin",
|
||||
"c:/Program Files (x86)/FreeCAD0.12/bin",
|
||||
"c:/Program Files (x86)/FreeCAD0.13/bin",
|
||||
"c:/Program Files (x86)/FreeCAD0.14/bin",
|
||||
"c:/Program Files (x86)/FreeCAD0.15/bin",
|
||||
"c:/Program Files (x86)/FreeCAD0.16/bin",
|
||||
"c:/Program Files (x86)/FreeCAD0.17/bin",
|
||||
"c:/apps/FreeCAD0.12/bin",
|
||||
"c:/apps/FreeCAD0.13/bin",
|
||||
"c:/apps/FreeCAD0.14/bin",
|
||||
"c:/apps/FreeCAD0.15/bin",
|
||||
"c:/apps/FreeCAD0.16/bin",
|
||||
"c:/apps/FreeCAD0.17/bin",
|
||||
"c:/Program Files/FreeCAD 0.12/bin",
|
||||
"c:/Program Files/FreeCAD 0.13/bin",
|
||||
"c:/Program Files/FreeCAD 0.14/bin",
|
||||
"c:/Program Files/FreeCAD 0.15/bin",
|
||||
"c:/Program Files/FreeCAD 0.16/bin",
|
||||
"c:/Program Files/FreeCAD 0.17/bin",
|
||||
"c:/Program Files (x86)/FreeCAD 0.12/bin",
|
||||
"c:/Program Files (x86)/FreeCAD 0.13/bin",
|
||||
"c:/Program Files (x86)/FreeCAD 0.14/bin",
|
||||
"c:/Program Files (x86)/FreeCAD 0.15/bin",
|
||||
"c:/Program Files (x86)/FreeCAD 0.16/bin",
|
||||
"c:/Program Files (x86)/FreeCAD 0.17/bin",
|
||||
"c:/apps/FreeCAD 0.12/bin",
|
||||
"c:/apps/FreeCAD 0.13/bin",
|
||||
"c:/apps/FreeCAD 0.14/bin",
|
||||
"c:/apps/FreeCAD 0.15/bin",
|
||||
"c:/apps/FreeCAD 0.16/bin",
|
||||
"c:/apps/FreeCAD 0.17/bin",
|
||||
]:
|
||||
if os.path.exists(_PATH):
|
||||
return _PATH
|
||||
elif sys.platform.startswith('darwin'):
|
||||
#Assume we're dealing with a Mac
|
||||
for _PATH in [
|
||||
"/Applications/FreeCAD.app/Contents/lib",
|
||||
os.path.join(os.path.expanduser("~"), "Library/Application Support/FreeCAD/lib"),
|
||||
]:
|
||||
if os.path.exists(_PATH):
|
||||
return _PATH
|
||||
|
||||
|
||||
|
||||
#Make sure that the correct FreeCAD path shows up in Python's system path
|
||||
no_library_path = ImportError('cadquery was unable to determine freecads library path')
|
||||
try:
|
||||
import FreeCAD
|
||||
except ImportError:
|
||||
path = _fc_path()
|
||||
if path:
|
||||
sys.path.insert(0, path)
|
||||
try:
|
||||
import FreeCAD
|
||||
except ImportError:
|
||||
raise no_library_path
|
||||
else: raise no_library_path
|
|
@ -1,413 +0,0 @@
|
|||
"""
|
||||
Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC
|
||||
|
||||
This file is part of CadQuery.
|
||||
|
||||
CadQuery is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
CadQuery is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
An exporter should provide functionality to accept a shape, and return
|
||||
a string containing the model content.
|
||||
"""
|
||||
import cadquery
|
||||
|
||||
import FreeCAD
|
||||
import Drawing
|
||||
|
||||
import tempfile, os, StringIO
|
||||
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
class ExportTypes:
|
||||
STL = "STL"
|
||||
STEP = "STEP"
|
||||
AMF = "AMF"
|
||||
SVG = "SVG"
|
||||
TJS = "TJS"
|
||||
|
||||
|
||||
class UNITS:
|
||||
MM = "mm"
|
||||
IN = "in"
|
||||
|
||||
|
||||
def toString(shape, exportType, tolerance=0.1):
|
||||
s = StringIO.StringIO()
|
||||
exportShape(shape, exportType, s, tolerance)
|
||||
return s.getvalue()
|
||||
|
||||
|
||||
def exportShape(shape,exportType,fileLike,tolerance=0.1):
|
||||
"""
|
||||
:param shape: the shape to export. it can be a shape object, or a cadquery object. If a cadquery
|
||||
object, the first value is exported
|
||||
:param exportFormat: the exportFormat to use
|
||||
:param tolerance: the tolerance, in model units
|
||||
:param fileLike: a file like object to which the content will be written.
|
||||
The object should be already open and ready to write. The caller is responsible
|
||||
for closing the object
|
||||
"""
|
||||
|
||||
|
||||
if isinstance(shape,cadquery.CQ):
|
||||
shape = shape.val()
|
||||
|
||||
if exportType == ExportTypes.TJS:
|
||||
#tessellate the model
|
||||
tess = shape.tessellate(tolerance)
|
||||
|
||||
mesher = JsonMesh() #warning: needs to be changed to remove buildTime and exportTime!!!
|
||||
#add vertices
|
||||
for vec in tess[0]:
|
||||
mesher.addVertex(vec.x, vec.y, vec.z)
|
||||
|
||||
#add faces
|
||||
for f in tess[1]:
|
||||
mesher.addTriangleFace(f[0],f[1], f[2])
|
||||
fileLike.write( mesher.toJson())
|
||||
elif exportType == ExportTypes.SVG:
|
||||
fileLike.write(getSVG(shape.wrapped))
|
||||
elif exportType == ExportTypes.AMF:
|
||||
tess = shape.tessellate(tolerance)
|
||||
aw = AmfWriter(tess).writeAmf(fileLike)
|
||||
else:
|
||||
|
||||
#all these types required writing to a file and then
|
||||
#re-reading. this is due to the fact that FreeCAD writes these
|
||||
(h, outFileName) = tempfile.mkstemp()
|
||||
#weird, but we need to close this file. the next step is going to write to
|
||||
#it from c code, so it needs to be closed.
|
||||
os.close(h)
|
||||
|
||||
if exportType == ExportTypes.STEP:
|
||||
shape.exportStep(outFileName)
|
||||
elif exportType == ExportTypes.STL:
|
||||
shape.wrapped.exportStl(outFileName)
|
||||
else:
|
||||
raise ValueError("No idea how i got here")
|
||||
|
||||
res = readAndDeleteFile(outFileName)
|
||||
fileLike.write(res)
|
||||
|
||||
def readAndDeleteFile(fileName):
|
||||
"""
|
||||
read data from file provided, and delete it when done
|
||||
return the contents as a string
|
||||
"""
|
||||
res = ""
|
||||
with open(fileName,'r') as f:
|
||||
res = f.read()
|
||||
|
||||
os.remove(fileName)
|
||||
return res
|
||||
|
||||
|
||||
def guessUnitOfMeasure(shape):
|
||||
"""
|
||||
Guess the unit of measure of a shape.
|
||||
"""
|
||||
bb = shape.BoundBox
|
||||
|
||||
dimList = [ bb.XLength, bb.YLength,bb.ZLength ]
|
||||
#no real part would likely be bigger than 10 inches on any side
|
||||
if max(dimList) > 10:
|
||||
return UNITS.MM
|
||||
|
||||
#no real part would likely be smaller than 0.1 mm on all dimensions
|
||||
if min(dimList) < 0.1:
|
||||
return UNITS.IN
|
||||
|
||||
#no real part would have the sum of its dimensions less than about 5mm
|
||||
if sum(dimList) < 10:
|
||||
return UNITS.IN
|
||||
|
||||
return UNITS.MM
|
||||
|
||||
|
||||
class AmfWriter(object):
|
||||
def __init__(self,tessellation):
|
||||
|
||||
self.units = "mm"
|
||||
self.tessellation = tessellation
|
||||
|
||||
def writeAmf(self,outFile):
|
||||
amf = ET.Element('amf',units=self.units)
|
||||
#TODO: if result is a compound, we need to loop through them
|
||||
object = ET.SubElement(amf,'object',id="0")
|
||||
mesh = ET.SubElement(object,'mesh')
|
||||
vertices = ET.SubElement(mesh,'vertices')
|
||||
volume = ET.SubElement(mesh,'volume')
|
||||
|
||||
#add vertices
|
||||
for v in self.tessellation[0]:
|
||||
vtx = ET.SubElement(vertices,'vertex')
|
||||
coord = ET.SubElement(vtx,'coordinates')
|
||||
x = ET.SubElement(coord,'x')
|
||||
x.text = str(v.x)
|
||||
y = ET.SubElement(coord,'y')
|
||||
y.text = str(v.y)
|
||||
z = ET.SubElement(coord,'z')
|
||||
z.text = str(v.z)
|
||||
|
||||
#add triangles
|
||||
for t in self.tessellation[1]:
|
||||
triangle = ET.SubElement(volume,'triangle')
|
||||
v1 = ET.SubElement(triangle,'v1')
|
||||
v1.text = str(t[0])
|
||||
v2 = ET.SubElement(triangle,'v2')
|
||||
v2.text = str(t[1])
|
||||
v3 = ET.SubElement(triangle,'v3')
|
||||
v3.text = str(t[2])
|
||||
|
||||
|
||||
ET.ElementTree(amf).write(outFile,encoding='ISO-8859-1')
|
||||
|
||||
"""
|
||||
Objects that represent
|
||||
three.js JSON object notation
|
||||
https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3.0
|
||||
"""
|
||||
class JsonMesh(object):
|
||||
def __init__(self):
|
||||
|
||||
self.vertices = [];
|
||||
self.faces = [];
|
||||
self.nVertices = 0;
|
||||
self.nFaces = 0;
|
||||
|
||||
def addVertex(self,x,y,z):
|
||||
self.nVertices += 1;
|
||||
self.vertices.extend([x,y,z]);
|
||||
|
||||
#add triangle composed of the three provided vertex indices
|
||||
def addTriangleFace(self, i,j,k):
|
||||
#first position means justa simple triangle
|
||||
self.nFaces += 1;
|
||||
self.faces.extend([0,int(i),int(j),int(k)]);
|
||||
|
||||
"""
|
||||
Get a json model from this model.
|
||||
For now we'll forget about colors, vertex normals, and all that stuff
|
||||
"""
|
||||
def toJson(self):
|
||||
return JSON_TEMPLATE % {
|
||||
'vertices' : str(self.vertices),
|
||||
'faces' : str(self.faces),
|
||||
'nVertices': self.nVertices,
|
||||
'nFaces' : self.nFaces
|
||||
};
|
||||
|
||||
|
||||
def getPaths(freeCadSVG):
|
||||
"""
|
||||
freeCad svg is worthless-- except for paths, which are fairly useful
|
||||
this method accepts svg from fReeCAD and returns a list of strings suitable for inclusion in a path element
|
||||
returns two lists-- one list of visible lines, and one list of hidden lines
|
||||
|
||||
HACK ALERT!!!!!
|
||||
FreeCAD does not give a way to determine which lines are hidden and which are not
|
||||
the only way to tell is that hidden lines are in a <g> with 0.15 stroke and visible are 0.35 stroke.
|
||||
so we actually look for that as a way to parse.
|
||||
|
||||
to make it worse, elementTree xpath attribute selectors do not work in python 2.6, and we
|
||||
cannot use python 2.7 due to freecad. So its necessary to look for the pure strings! ick!
|
||||
"""
|
||||
|
||||
hiddenPaths = []
|
||||
visiblePaths = []
|
||||
if len(freeCadSVG) > 0:
|
||||
#yuk, freecad returns svg fragments. stupid stupid
|
||||
fullDoc = "<root>%s</root>" % freeCadSVG
|
||||
e = ET.ElementTree(ET.fromstring(fullDoc))
|
||||
segments = e.findall(".//g")
|
||||
for s in segments:
|
||||
paths = s.findall("path")
|
||||
|
||||
if s.get("stroke-width") == "0.15": #hidden line HACK HACK HACK
|
||||
mylist = hiddenPaths
|
||||
else:
|
||||
mylist = visiblePaths
|
||||
|
||||
for p in paths:
|
||||
mylist.append(p.get("d"))
|
||||
return (hiddenPaths,visiblePaths)
|
||||
else:
|
||||
return ([],[])
|
||||
|
||||
|
||||
def getSVG(shape,opts=None):
|
||||
"""
|
||||
Export a shape to SVG
|
||||
"""
|
||||
|
||||
d = {'width':800,'height':240,'marginLeft':200,'marginTop':20}
|
||||
|
||||
if opts:
|
||||
d.update(opts)
|
||||
|
||||
#need to guess the scale and the coordinate center
|
||||
uom = guessUnitOfMeasure(shape)
|
||||
|
||||
width=float(d['width'])
|
||||
height=float(d['height'])
|
||||
marginLeft=float(d['marginLeft'])
|
||||
marginTop=float(d['marginTop'])
|
||||
|
||||
#TODO: provide option to give 3 views
|
||||
viewVector = FreeCAD.Base.Vector(-1.75,1.1,5)
|
||||
(visibleG0,visibleG1,hiddenG0,hiddenG1) = Drawing.project(shape,viewVector)
|
||||
|
||||
(hiddenPaths,visiblePaths) = getPaths(Drawing.projectToSVG(shape,viewVector,"ShowHiddenLines")) #this param is totally undocumented!
|
||||
|
||||
#get bounding box -- these are all in 2-d space
|
||||
bb = visibleG0.BoundBox
|
||||
bb.add(visibleG1.BoundBox)
|
||||
bb.add(hiddenG0.BoundBox)
|
||||
bb.add(hiddenG1.BoundBox)
|
||||
|
||||
#width pixels for x, height pixesl for y
|
||||
unitScale = min( width / bb.XLength * 0.75 , height / bb.YLength * 0.75 )
|
||||
|
||||
#compute amount to translate-- move the top left into view
|
||||
(xTranslate,yTranslate) = ( (0 - bb.XMin) + marginLeft/unitScale ,(0- bb.YMax) - marginTop/unitScale)
|
||||
|
||||
#compute paths ( again -- had to strip out freecad crap )
|
||||
hiddenContent = ""
|
||||
for p in hiddenPaths:
|
||||
hiddenContent += PATHTEMPLATE % p
|
||||
|
||||
visibleContent = ""
|
||||
for p in visiblePaths:
|
||||
visibleContent += PATHTEMPLATE % p
|
||||
|
||||
svg = SVG_TEMPLATE % (
|
||||
{
|
||||
"unitScale" : str(unitScale),
|
||||
"strokeWidth" : str(1.0/unitScale),
|
||||
"hiddenContent" : hiddenContent ,
|
||||
"visibleContent" :visibleContent,
|
||||
"xTranslate" : str(xTranslate),
|
||||
"yTranslate" : str(yTranslate),
|
||||
"width" : str(width),
|
||||
"height" : str(height),
|
||||
"textboxY" :str(height - 30),
|
||||
"uom" : str(uom)
|
||||
}
|
||||
)
|
||||
#svg = SVG_TEMPLATE % (
|
||||
# {"content": projectedContent}
|
||||
#)
|
||||
return svg
|
||||
|
||||
|
||||
def exportSVG(shape, fileName):
|
||||
"""
|
||||
accept a cadquery shape, and export it to the provided file
|
||||
TODO: should use file-like objects, not a fileName, and/or be able to return a string instead
|
||||
export a view of a part to svg
|
||||
"""
|
||||
|
||||
svg = getSVG(shape.val().wrapped)
|
||||
f = open(fileName,'w')
|
||||
f.write(svg)
|
||||
f.close()
|
||||
|
||||
|
||||
|
||||
JSON_TEMPLATE= """\
|
||||
{
|
||||
"metadata" :
|
||||
{
|
||||
"formatVersion" : 3,
|
||||
"generatedBy" : "ParametricParts",
|
||||
"vertices" : %(nVertices)d,
|
||||
"faces" : %(nFaces)d,
|
||||
"normals" : 0,
|
||||
"colors" : 0,
|
||||
"uvs" : 0,
|
||||
"materials" : 1,
|
||||
"morphTargets" : 0
|
||||
},
|
||||
|
||||
"scale" : 1.0,
|
||||
|
||||
"materials": [ {
|
||||
"DbgColor" : 15658734,
|
||||
"DbgIndex" : 0,
|
||||
"DbgName" : "Material",
|
||||
"colorAmbient" : [0.0, 0.0, 0.0],
|
||||
"colorDiffuse" : [0.6400000190734865, 0.10179081114814892, 0.126246120426746],
|
||||
"colorSpecular" : [0.5, 0.5, 0.5],
|
||||
"shading" : "Lambert",
|
||||
"specularCoef" : 50,
|
||||
"transparency" : 1.0,
|
||||
"vertexColors" : false
|
||||
}],
|
||||
|
||||
"vertices": %(vertices)s,
|
||||
|
||||
"morphTargets": [],
|
||||
|
||||
"normals": [],
|
||||
|
||||
"colors": [],
|
||||
|
||||
"uvs": [[]],
|
||||
|
||||
"faces": %(faces)s
|
||||
}
|
||||
"""
|
||||
|
||||
SVG_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="%(width)s"
|
||||
height="%(height)s"
|
||||
|
||||
>
|
||||
<g transform="scale(%(unitScale)s, -%(unitScale)s) translate(%(xTranslate)s,%(yTranslate)s)" stroke-width="%(strokeWidth)s" fill="none">
|
||||
<!-- hidden lines -->
|
||||
<g stroke="rgb(160, 160, 160)" fill="none" stroke-dasharray="%(strokeWidth)s,%(strokeWidth)s" >
|
||||
%(hiddenContent)s
|
||||
</g>
|
||||
|
||||
<!-- solid lines -->
|
||||
<g stroke="rgb(0, 0, 0)" fill="none">
|
||||
%(visibleContent)s
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(20,%(textboxY)s)" stroke="rgb(0,0,255)">
|
||||
<line x1="30" y1="-30" x2="75" y2="-33" stroke-width="3" stroke="#000000" />
|
||||
<text x="80" y="-30" style="stroke:#000000">X </text>
|
||||
|
||||
<line x1="30" y1="-30" x2="30" y2="-75" stroke-width="3" stroke="#000000" />
|
||||
<text x="25" y="-85" style="stroke:#000000">Y </text>
|
||||
|
||||
<line x1="30" y1="-30" x2="58" y2="-15" stroke-width="3" stroke="#000000" />
|
||||
<text x="65" y="-5" style="stroke:#000000">Z </text>
|
||||
<!--
|
||||
<line x1="0" y1="0" x2="%(unitScale)s" y2="0" stroke-width="3" />
|
||||
<text x="0" y="20" style="stroke:#000000">1 %(uom)s </text>
|
||||
-->
|
||||
</g>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
PATHTEMPLATE="\t\t\t<path d=\"%s\" />\n"
|
||||
|
|
@ -1,642 +0,0 @@
|
|||
"""
|
||||
Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC
|
||||
|
||||
This file is part of CadQuery.
|
||||
|
||||
CadQuery is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
CadQuery is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; If not, see <http://www.gnu.org/licenses/>
|
||||
"""
|
||||
|
||||
import math
|
||||
import cadquery
|
||||
import FreeCAD
|
||||
import Part as FreeCADPart
|
||||
|
||||
|
||||
def sortWiresByBuildOrder(wireList, plane, result=[]):
|
||||
"""Tries to determine how wires should be combined into faces.
|
||||
|
||||
Assume:
|
||||
The wires make up one or more faces, which could have 'holes'
|
||||
Outer wires are listed ahead of inner wires
|
||||
there are no wires inside wires inside wires
|
||||
( IE, islands -- we can deal with that later on )
|
||||
none of the wires are construction wires
|
||||
Compute:
|
||||
one or more sets of wires, with the outer wire listed first, and inner
|
||||
ones
|
||||
Returns, list of lists.
|
||||
"""
|
||||
result = []
|
||||
|
||||
remainingWires = list(wireList)
|
||||
while remainingWires:
|
||||
outerWire = remainingWires.pop(0)
|
||||
group = [outerWire]
|
||||
otherWires = list(remainingWires)
|
||||
for w in otherWires:
|
||||
if plane.isWireInside(outerWire, w):
|
||||
group.append(w)
|
||||
remainingWires.remove(w)
|
||||
result.append(group)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class Vector(object):
|
||||
"""Create a 3-dimensional vector
|
||||
|
||||
:param *args: a 3-d vector, with x-y-z parts.
|
||||
|
||||
you can either provide:
|
||||
* nothing (in which case the null vector is return)
|
||||
* a FreeCAD vector
|
||||
* a vector ( in which case it is copied )
|
||||
* a 3-tuple
|
||||
* three float values, x, y, and z
|
||||
"""
|
||||
def __init__(self, *args):
|
||||
if len(args) == 3:
|
||||
fV = FreeCAD.Base.Vector(args[0], args[1], args[2])
|
||||
elif len(args) == 1:
|
||||
if isinstance(args[0], Vector):
|
||||
fV = args[0].wrapped
|
||||
elif isinstance(args[0], tuple):
|
||||
fV = FreeCAD.Base.Vector(args[0][0], args[0][1], args[0][2])
|
||||
elif isinstance(args[0], FreeCAD.Base.Vector):
|
||||
fV = args[0]
|
||||
else:
|
||||
fV = args[0]
|
||||
elif len(args) == 0:
|
||||
fV = FreeCAD.Base.Vector(0, 0, 0)
|
||||
else:
|
||||
raise ValueError("Expected three floats, FreeCAD Vector, or 3-tuple")
|
||||
|
||||
self._wrapped = fV
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
return self.wrapped.x
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
return self.wrapped.y
|
||||
|
||||
@property
|
||||
def z(self):
|
||||
return self.wrapped.z
|
||||
|
||||
@property
|
||||
def Length(self):
|
||||
return self.wrapped.Length
|
||||
|
||||
@property
|
||||
def wrapped(self):
|
||||
return self._wrapped
|
||||
|
||||
def toTuple(self):
|
||||
return (self.x, self.y, self.z)
|
||||
|
||||
# TODO: is it possible to create a dynamic proxy without all this code?
|
||||
def cross(self, v):
|
||||
return Vector(self.wrapped.cross(v.wrapped))
|
||||
|
||||
def dot(self, v):
|
||||
return self.wrapped.dot(v.wrapped)
|
||||
|
||||
def sub(self, v):
|
||||
return Vector(self.wrapped.sub(v.wrapped))
|
||||
|
||||
def add(self, v):
|
||||
return Vector(self.wrapped.add(v.wrapped))
|
||||
|
||||
def multiply(self, scale):
|
||||
"""Return a copy multiplied by the provided scalar"""
|
||||
tmp_fc_vector = FreeCAD.Base.Vector(self.wrapped)
|
||||
return Vector(tmp_fc_vector.multiply(scale))
|
||||
|
||||
def normalize(self):
|
||||
"""Return a normalized version of this vector"""
|
||||
tmp_fc_vector = FreeCAD.Base.Vector(self.wrapped)
|
||||
tmp_fc_vector.normalize()
|
||||
return Vector(tmp_fc_vector)
|
||||
|
||||
def Center(self):
|
||||
"""Return the vector itself
|
||||
|
||||
The center of myself is myself.
|
||||
Provided so that vectors, vertexes, and other shapes all support a
|
||||
common interface, when Center() is requested for all objects on the
|
||||
stack.
|
||||
"""
|
||||
return self
|
||||
|
||||
def getAngle(self, v):
|
||||
return self.wrapped.getAngle(v.wrapped)
|
||||
|
||||
def distanceToLine(self):
|
||||
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
|
||||
|
||||
def projectToLine(self):
|
||||
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
|
||||
|
||||
def distanceToPlane(self):
|
||||
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
|
||||
|
||||
def projectToPlane(self):
|
||||
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
|
||||
|
||||
def __add__(self, v):
|
||||
return self.add(v)
|
||||
|
||||
def __repr__(self):
|
||||
return self.wrapped.__repr__()
|
||||
|
||||
def __str__(self):
|
||||
return self.wrapped.__str__()
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.wrapped.__ne__(other)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.wrapped.__eq__(other)
|
||||
|
||||
|
||||
class Matrix:
|
||||
"""A 3d , 4x4 transformation matrix.
|
||||
|
||||
Used to move geometry in space.
|
||||
"""
|
||||
def __init__(self, matrix=None):
|
||||
if matrix is None:
|
||||
self.wrapped = FreeCAD.Base.Matrix()
|
||||
else:
|
||||
self.wrapped = matrix
|
||||
|
||||
def rotateX(self, angle):
|
||||
self.wrapped.rotateX(angle)
|
||||
|
||||
def rotateY(self, angle):
|
||||
self.wrapped.rotateY(angle)
|
||||
|
||||
|
||||
class Plane(object):
|
||||
"""A 2D coordinate system in space
|
||||
|
||||
A 2D coordinate system in space, with the x-y axes on the plane, and a
|
||||
particular point as the origin.
|
||||
|
||||
A plane allows the use of 2-d coordinates, which are later converted to
|
||||
global, 3d coordinates when the operations are complete.
|
||||
|
||||
Frequently, it is not necessary to create work planes, as they can be
|
||||
created automatically from faces.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def named(cls, stdName, origin=(0, 0, 0)):
|
||||
"""Create a predefined Plane based on the conventional names.
|
||||
|
||||
:param stdName: one of (XY|YZ|ZX|XZ|YX|ZY|front|back|left|right|top|bottom)
|
||||
:type stdName: string
|
||||
:param origin: the desired origin, specified in global coordinates
|
||||
:type origin: 3-tuple of the origin of the new plane, in global coorindates.
|
||||
|
||||
Available named planes are as follows. Direction references refer to
|
||||
the global directions.
|
||||
|
||||
=========== ======= ======= ======
|
||||
Name xDir yDir zDir
|
||||
=========== ======= ======= ======
|
||||
XY +x +y +z
|
||||
YZ +y +z +x
|
||||
ZX +z +x +y
|
||||
XZ +x +z -y
|
||||
YX +y +x -z
|
||||
ZY +z +y -x
|
||||
front +x +y +z
|
||||
back -x +y -z
|
||||
left +z +y -x
|
||||
right -z +y +x
|
||||
top +x -z +y
|
||||
bottom +x +z -y
|
||||
=========== ======= ======= ======
|
||||
"""
|
||||
|
||||
namedPlanes = {
|
||||
# origin, xDir, normal
|
||||
'XY': Plane(origin, (1, 0, 0), (0, 0, 1)),
|
||||
'YZ': Plane(origin, (0, 1, 0), (1, 0, 0)),
|
||||
'ZX': Plane(origin, (0, 0, 1), (0, 1, 0)),
|
||||
'XZ': Plane(origin, (1, 0, 0), (0, -1, 0)),
|
||||
'YX': Plane(origin, (0, 1, 0), (0, 0, -1)),
|
||||
'ZY': Plane(origin, (0, 0, 1), (-1, 0, 0)),
|
||||
'front': Plane(origin, (1, 0, 0), (0, 0, 1)),
|
||||
'back': Plane(origin, (-1, 0, 0), (0, 0, -1)),
|
||||
'left': Plane(origin, (0, 0, 1), (-1, 0, 0)),
|
||||
'right': Plane(origin, (0, 0, -1), (1, 0, 0)),
|
||||
'top': Plane(origin, (1, 0, 0), (0, 1, 0)),
|
||||
'bottom': Plane(origin, (1, 0, 0), (0, -1, 0))
|
||||
}
|
||||
|
||||
try:
|
||||
return namedPlanes[stdName]
|
||||
except KeyError:
|
||||
raise ValueError('Supported names are {}'.format(
|
||||
namedPlanes.keys()))
|
||||
|
||||
@classmethod
|
||||
def XY(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
|
||||
plane = Plane.named('XY', origin)
|
||||
plane._setPlaneDir(xDir)
|
||||
return plane
|
||||
|
||||
@classmethod
|
||||
def YZ(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)):
|
||||
plane = Plane.named('YZ', origin)
|
||||
plane._setPlaneDir(xDir)
|
||||
return plane
|
||||
|
||||
@classmethod
|
||||
def ZX(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
|
||||
plane = Plane.named('ZX', origin)
|
||||
plane._setPlaneDir(xDir)
|
||||
return plane
|
||||
|
||||
@classmethod
|
||||
def XZ(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
|
||||
plane = Plane.named('XZ', origin)
|
||||
plane._setPlaneDir(xDir)
|
||||
return plane
|
||||
|
||||
@classmethod
|
||||
def YX(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)):
|
||||
plane = Plane.named('YX', origin)
|
||||
plane._setPlaneDir(xDir)
|
||||
return plane
|
||||
|
||||
@classmethod
|
||||
def ZY(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
|
||||
plane = Plane.named('ZY', origin)
|
||||
plane._setPlaneDir(xDir)
|
||||
return plane
|
||||
|
||||
@classmethod
|
||||
def front(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
|
||||
plane = Plane.named('front', origin)
|
||||
plane._setPlaneDir(xDir)
|
||||
return plane
|
||||
|
||||
@classmethod
|
||||
def back(cls, origin=(0, 0, 0), xDir=Vector(-1, 0, 0)):
|
||||
plane = Plane.named('back', origin)
|
||||
plane._setPlaneDir(xDir)
|
||||
return plane
|
||||
|
||||
@classmethod
|
||||
def left(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
|
||||
plane = Plane.named('left', origin)
|
||||
plane._setPlaneDir(xDir)
|
||||
return plane
|
||||
|
||||
@classmethod
|
||||
def right(cls, origin=(0, 0, 0), xDir=Vector(0, 0, -1)):
|
||||
plane = Plane.named('right', origin)
|
||||
plane._setPlaneDir(xDir)
|
||||
return plane
|
||||
|
||||
@classmethod
|
||||
def top(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
|
||||
plane = Plane.named('top', origin)
|
||||
plane._setPlaneDir(xDir)
|
||||
return plane
|
||||
|
||||
@classmethod
|
||||
def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
|
||||
plane = Plane.named('bottom', origin)
|
||||
plane._setPlaneDir(xDir)
|
||||
return plane
|
||||
|
||||
def __init__(self, origin, xDir, normal):
|
||||
"""Create a Plane with an arbitrary orientation
|
||||
|
||||
TODO: project x and y vectors so they work even if not orthogonal
|
||||
:param origin: the origin
|
||||
:type origin: a three-tuple of the origin, in global coordinates
|
||||
:param xDir: a vector representing the xDirection.
|
||||
:type xDir: a three-tuple representing a vector, or a FreeCAD Vector
|
||||
:param normal: the normal direction for the new plane
|
||||
:type normal: a FreeCAD Vector
|
||||
:raises: ValueError if the specified xDir is not orthogonal to the provided normal.
|
||||
:return: a plane in the global space, with the xDirection of the plane in the specified direction.
|
||||
"""
|
||||
normal = Vector(normal)
|
||||
if (normal.Length == 0.0):
|
||||
raise ValueError('normal should be non null')
|
||||
self.zDir = normal.normalize()
|
||||
xDir = Vector(xDir)
|
||||
if (xDir.Length == 0.0):
|
||||
raise ValueError('xDir should be non null')
|
||||
self._setPlaneDir(xDir)
|
||||
|
||||
self.invZDir = self.zDir.multiply(-1.0)
|
||||
|
||||
self.origin = origin
|
||||
|
||||
@property
|
||||
def origin(self):
|
||||
return self._origin
|
||||
|
||||
@origin.setter
|
||||
def origin(self, value):
|
||||
self._origin = Vector(value)
|
||||
self._calcTransforms()
|
||||
|
||||
def setOrigin2d(self, x, y):
|
||||
"""Set a new origin in the plane itself
|
||||
|
||||
Set a new origin in the plane itself. The plane's orientation and
|
||||
xDrection are unaffected.
|
||||
|
||||
:param float x: offset in the x direction
|
||||
:param float y: offset in the y direction
|
||||
:return: void
|
||||
|
||||
The new coordinates are specified in terms of the current 2-d system.
|
||||
As an example:
|
||||
p = Plane.XY()
|
||||
p.setOrigin2d(2, 2)
|
||||
p.setOrigin2d(2, 2)
|
||||
results in a plane with its origin at (x, y) = (4, 4) in global
|
||||
coordinates. Both operations were relative to local coordinates of the
|
||||
plane.
|
||||
"""
|
||||
self.origin = self.toWorldCoords((x, y))
|
||||
|
||||
def isWireInside(self, baseWire, testWire):
|
||||
"""Determine if testWire is inside baseWire
|
||||
|
||||
Determine if testWire is inside baseWire, after both wires are projected
|
||||
into the current plane.
|
||||
|
||||
:param baseWire: a reference wire
|
||||
:type baseWire: a FreeCAD wire
|
||||
:param testWire: another wire
|
||||
:type testWire: a FreeCAD wire
|
||||
:return: True if testWire is inside baseWire, otherwise False
|
||||
|
||||
If either wire does not lie in the current plane, it is projected into
|
||||
the plane first.
|
||||
|
||||
*WARNING*: This method is not 100% reliable. It uses bounding box
|
||||
tests, but needs more work to check for cases when curves are complex.
|
||||
|
||||
Future Enhancements:
|
||||
* Discretizing points along each curve to provide a more reliable
|
||||
test.
|
||||
"""
|
||||
# TODO: also use a set of points along the wire to test as well.
|
||||
# TODO: would it be more efficient to create objects in the local
|
||||
# coordinate system, and then transform to global
|
||||
# coordinates upon extrusion?
|
||||
|
||||
tBaseWire = baseWire.transformGeometry(self.fG)
|
||||
tTestWire = testWire.transformGeometry(self.fG)
|
||||
|
||||
# These bounding boxes will have z=0, since we transformed them into the
|
||||
# space of the plane.
|
||||
bb = tBaseWire.BoundingBox()
|
||||
tb = tTestWire.BoundingBox()
|
||||
|
||||
# findOutsideBox actually inspects both ways, here we only want to
|
||||
# know if one is inside the other
|
||||
return bb == BoundBox.findOutsideBox2D(bb, tb)
|
||||
|
||||
def toLocalCoords(self, obj):
|
||||
"""Project the provided coordinates onto this plane
|
||||
|
||||
:param obj: an object or vector to convert
|
||||
:type vector: a vector or shape
|
||||
:return: an object of the same type, but converted to local coordinates
|
||||
|
||||
|
||||
Most of the time, the z-coordinate returned will be zero, because most
|
||||
operations based on a plane are all 2-d. Occasionally, though, 3-d
|
||||
points outside of the current plane are transformed. One such example is
|
||||
:py:meth:`Workplane.box`, where 3-d corners of a box are transformed to
|
||||
orient the box in space correctly.
|
||||
|
||||
"""
|
||||
if isinstance(obj, Vector):
|
||||
return Vector(self.fG.multiply(obj.wrapped))
|
||||
elif isinstance(obj, cadquery.Shape):
|
||||
return obj.transformShape(self.rG)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Don't know how to convert type {} to local coordinates".format(
|
||||
type(obj)))
|
||||
|
||||
def toWorldCoords(self, tuplePoint):
|
||||
"""Convert a point in local coordinates to global coordinates
|
||||
|
||||
:param tuplePoint: point in local coordinates to convert.
|
||||
:type tuplePoint: a 2 or three tuple of float. The third value is taken to be zero if not supplied.
|
||||
:return: a Vector in global coordinates
|
||||
"""
|
||||
if isinstance(tuplePoint, Vector):
|
||||
v = tuplePoint
|
||||
elif len(tuplePoint) == 2:
|
||||
v = Vector(tuplePoint[0], tuplePoint[1], 0)
|
||||
else:
|
||||
v = Vector(tuplePoint)
|
||||
return Vector(self.rG.multiply(v.wrapped))
|
||||
|
||||
def rotated(self, rotate=(0, 0, 0)):
|
||||
"""Returns a copy of this plane, rotated about the specified axes
|
||||
|
||||
Since the z axis is always normal the plane, rotating around Z will
|
||||
always produce a plane that is parallel to this one.
|
||||
|
||||
The origin of the workplane is unaffected by the rotation.
|
||||
|
||||
Rotations are done in order x, y, z. If you need a different order,
|
||||
manually chain together multiple rotate() commands.
|
||||
|
||||
:param rotate: Vector [xDegrees, yDegrees, zDegrees]
|
||||
:return: a copy of this plane rotated as requested.
|
||||
"""
|
||||
rotate = Vector(rotate)
|
||||
# Convert to radians.
|
||||
rotate = rotate.multiply(math.pi / 180.0)
|
||||
|
||||
# Compute rotation matrix.
|
||||
m = FreeCAD.Base.Matrix()
|
||||
m.rotateX(rotate.x)
|
||||
m.rotateY(rotate.y)
|
||||
m.rotateZ(rotate.z)
|
||||
|
||||
# Compute the new plane.
|
||||
newXdir = Vector(m.multiply(self.xDir.wrapped))
|
||||
newZdir = Vector(m.multiply(self.zDir.wrapped))
|
||||
|
||||
return Plane(self.origin, newXdir, newZdir)
|
||||
|
||||
def rotateShapes(self, listOfShapes, rotationMatrix):
|
||||
"""Rotate the listOfShapes by the supplied rotationMatrix
|
||||
|
||||
@param listOfShapes is a list of shape objects
|
||||
@param rotationMatrix is a geom.Matrix object.
|
||||
returns a list of shape objects rotated according to the rotationMatrix.
|
||||
"""
|
||||
# Compute rotation matrix (global --> local --> rotate --> global).
|
||||
# rm = self.plane.fG.multiply(matrix).multiply(self.plane.rG)
|
||||
# rm = self.computeTransform(rotationMatrix)
|
||||
|
||||
# There might be a better way, but to do this rotation takes 3 steps:
|
||||
# - transform geometry to local coordinates
|
||||
# - then rotate about x
|
||||
# - then transform back to global coordinates.
|
||||
|
||||
resultWires = []
|
||||
for w in listOfShapes:
|
||||
mirrored = w.transformGeometry(rotationMatrix.wrapped)
|
||||
|
||||
# If the first vertex of the second wire is not coincident with the
|
||||
# first or last vertices of the first wire we have to fix the wire
|
||||
# so that it will mirror correctly.
|
||||
if ((mirrored.wrapped.Vertexes[0].X == w.wrapped.Vertexes[0].X and
|
||||
mirrored.wrapped.Vertexes[0].Y == w.wrapped.Vertexes[0].Y and
|
||||
mirrored.wrapped.Vertexes[0].Z == w.wrapped.Vertexes[0].Z) or
|
||||
(mirrored.wrapped.Vertexes[0].X == w.wrapped.Vertexes[-1].X and
|
||||
mirrored.wrapped.Vertexes[0].Y == w.wrapped.Vertexes[-1].Y and
|
||||
mirrored.wrapped.Vertexes[0].Z == w.wrapped.Vertexes[-1].Z)):
|
||||
|
||||
resultWires.append(mirrored)
|
||||
else:
|
||||
# Make sure that our mirrored edges meet up and are ordered
|
||||
# properly.
|
||||
aEdges = w.wrapped.Edges
|
||||
aEdges.extend(mirrored.wrapped.Edges)
|
||||
comp = FreeCADPart.Compound(aEdges)
|
||||
mirroredWire = comp.connectEdgesToWires(False).Wires[0]
|
||||
|
||||
resultWires.append(cadquery.Shape.cast(mirroredWire))
|
||||
|
||||
return resultWires
|
||||
|
||||
def _setPlaneDir(self, xDir):
|
||||
"""Set the vectors parallel to the plane, i.e. xDir and yDir"""
|
||||
if (self.zDir.dot(xDir) > 1e-5):
|
||||
raise ValueError('xDir must be parralel to the plane')
|
||||
xDir = Vector(xDir)
|
||||
self.xDir = xDir.normalize()
|
||||
self.yDir = self.zDir.cross(self.xDir).normalize()
|
||||
|
||||
def _calcTransforms(self):
|
||||
"""Computes transformation matrices to convert between coordinates
|
||||
|
||||
Computes transformation matrices to convert between local and global
|
||||
coordinates.
|
||||
"""
|
||||
# r is the forward transformation matrix from world to local coordinates
|
||||
# ok i will be really honest, i cannot understand exactly why this works
|
||||
# something bout the order of the translation and the rotation.
|
||||
# the double-inverting is strange, and I don't understand it.
|
||||
r = FreeCAD.Base.Matrix()
|
||||
|
||||
# Forward transform must rotate and adjust for origin.
|
||||
(r.A11, r.A12, r.A13) = (self.xDir.x, self.xDir.y, self.xDir.z)
|
||||
(r.A21, r.A22, r.A23) = (self.yDir.x, self.yDir.y, self.yDir.z)
|
||||
(r.A31, r.A32, r.A33) = (self.zDir.x, self.zDir.y, self.zDir.z)
|
||||
|
||||
invR = r.inverse()
|
||||
invR.A14 = self.origin.x
|
||||
invR.A24 = self.origin.y
|
||||
invR.A34 = self.origin.z
|
||||
|
||||
self.rG = invR
|
||||
self.fG = invR.inverse()
|
||||
|
||||
def computeTransform(self, tMatrix):
|
||||
"""Computes the 2-d projection of the supplied matrix"""
|
||||
|
||||
return Matrix(self.fG.multiply(tMatrix.wrapped).multiply(self.rG))
|
||||
|
||||
|
||||
class BoundBox(object):
|
||||
"""A BoundingBox for an object or set of objects. Wraps the FreeCAD one"""
|
||||
def __init__(self, bb):
|
||||
self.wrapped = bb
|
||||
self.xmin = bb.XMin
|
||||
self.xmax = bb.XMax
|
||||
self.xlen = bb.XLength
|
||||
self.ymin = bb.YMin
|
||||
self.ymax = bb.YMax
|
||||
self.ylen = bb.YLength
|
||||
self.zmin = bb.ZMin
|
||||
self.zmax = bb.ZMax
|
||||
self.zlen = bb.ZLength
|
||||
self.center = Vector(bb.Center)
|
||||
self.DiagonalLength = bb.DiagonalLength
|
||||
|
||||
def add(self, obj):
|
||||
"""Returns a modified (expanded) bounding box
|
||||
|
||||
obj can be one of several things:
|
||||
1. a 3-tuple corresponding to x,y, and z amounts to add
|
||||
2. a vector, containing the x,y,z values to add
|
||||
3. another bounding box, where a new box will be created that
|
||||
encloses both.
|
||||
|
||||
This bounding box is not changed.
|
||||
"""
|
||||
tmp = FreeCAD.Base.BoundBox(self.wrapped)
|
||||
if isinstance(obj, tuple):
|
||||
tmp.add(obj[0], obj[1], obj[2])
|
||||
elif isinstance(obj, Vector):
|
||||
tmp.add(obj.fV)
|
||||
elif isinstance(obj, BoundBox):
|
||||
tmp.add(obj.wrapped)
|
||||
|
||||
return BoundBox(tmp)
|
||||
|
||||
@classmethod
|
||||
def findOutsideBox2D(cls, b1, b2):
|
||||
"""Compares bounding boxes
|
||||
|
||||
Compares bounding boxes. Returns none if neither is inside the other.
|
||||
Returns the outer one if either is outside the other.
|
||||
|
||||
BoundBox.isInside works in 3d, but this is a 2d bounding box, so it
|
||||
doesn't work correctly plus, there was all kinds of rounding error in
|
||||
the built-in implementation i do not understand.
|
||||
"""
|
||||
fc_bb1 = b1.wrapped
|
||||
fc_bb2 = b2.wrapped
|
||||
if (fc_bb1.XMin < fc_bb2.XMin and
|
||||
fc_bb1.XMax > fc_bb2.XMax and
|
||||
fc_bb1.YMin < fc_bb2.YMin and
|
||||
fc_bb1.YMax > fc_bb2.YMax):
|
||||
return b1
|
||||
|
||||
if (fc_bb2.XMin < fc_bb1.XMin and
|
||||
fc_bb2.XMax > fc_bb1.XMax and
|
||||
fc_bb2.YMin < fc_bb1.YMin and
|
||||
fc_bb2.YMax > fc_bb1.YMax):
|
||||
return b2
|
||||
|
||||
return None
|
||||
|
||||
def isInside(self, anotherBox):
|
||||
"""Is the provided bounding box inside this one?"""
|
||||
return self.wrapped.isInside(anotherBox.wrapped)
|
|
@ -1,66 +0,0 @@
|
|||
"""
|
||||
Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC
|
||||
|
||||
This file is part of CadQuery.
|
||||
|
||||
CadQuery is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
CadQuery is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
An exporter should provide functionality to accept a shape, and return
|
||||
a string containing the model content.
|
||||
"""
|
||||
import cadquery
|
||||
from .shapes import Shape
|
||||
|
||||
import FreeCAD
|
||||
import Part
|
||||
|
||||
class ImportTypes:
|
||||
STEP = "STEP"
|
||||
|
||||
class UNITS:
|
||||
MM = "mm"
|
||||
IN = "in"
|
||||
|
||||
|
||||
def importShape(importType, fileName):
|
||||
"""
|
||||
Imports a file based on the type (STEP, STL, etc)
|
||||
:param importType: The type of file that we're importing
|
||||
:param fileName: THe name of the file that we're importing
|
||||
"""
|
||||
|
||||
#Check to see what type of file we're working with
|
||||
if importType == ImportTypes.STEP:
|
||||
return importStep(fileName)
|
||||
|
||||
|
||||
#Loads a STEP file into a CQ.Workplane object
|
||||
def importStep(fileName):
|
||||
"""
|
||||
Accepts a file name and loads the STEP file into a cadquery shape
|
||||
:param fileName: The path and name of the STEP file to be imported
|
||||
"""
|
||||
|
||||
#Now read and return the shape
|
||||
try:
|
||||
rshape = Part.read(fileName)
|
||||
|
||||
#Make sure that we extract all the solids
|
||||
solids = []
|
||||
for solid in rshape.Solids:
|
||||
solids.append(Shape.cast(solid))
|
||||
|
||||
return cadquery.Workplane("XY").newObject(solids)
|
||||
except:
|
||||
raise ValueError("STEP File Could not be loaded")
|
|
@ -1,908 +0,0 @@
|
|||
"""
|
||||
Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC
|
||||
|
||||
This file is part of CadQuery.
|
||||
|
||||
CadQuery is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
CadQuery is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
Wrapper Classes for FreeCAD
|
||||
These classes provide a stable interface for 3d objects,
|
||||
independent of the FreeCAD interface.
|
||||
|
||||
Future work might include use of pythonOCC, OCC, or even
|
||||
another CAD kernel directly, so this interface layer is quite important.
|
||||
|
||||
Funny, in java this is one of those few areas where i'd actually spend the time
|
||||
to make an interface and an implementation, but for new these are just rolled together
|
||||
|
||||
This interface layer provides three distinct values:
|
||||
|
||||
1. It allows us to avoid changing key api points if we change underlying implementations.
|
||||
It would be a disaster if script and plugin authors had to change models because we
|
||||
changed implementations
|
||||
|
||||
2. Allow better documentation. One of the reasons FreeCAD is no more popular is because
|
||||
its docs are terrible. This allows us to provide good documentation via docstrings
|
||||
for each wrapper
|
||||
|
||||
3. Work around bugs. there are a quite a feb bugs in free this layer allows fixing them
|
||||
|
||||
4. allows for enhanced functionality. Many objects are missing features we need. For example
|
||||
we need a 'forConstruction' flag on the Wire object. this allows adding those kinds of things
|
||||
|
||||
5. allow changing interfaces when we'd like. there are few cases where the FreeCAD api is not
|
||||
very user friendly: we like to change those when necessary. As an example, in the FreeCAD api,
|
||||
all factory methods are on the 'Part' object, but it is very useful to know what kind of
|
||||
object each one returns, so these are better grouped by the type of object they return.
|
||||
(who would know that Part.makeCircle() returns an Edge, but Part.makePolygon() returns a Wire ?
|
||||
"""
|
||||
from cadquery import Vector, BoundBox
|
||||
import FreeCAD
|
||||
import Part as FreeCADPart
|
||||
|
||||
|
||||
class Shape(object):
|
||||
"""
|
||||
Represents a shape in the system.
|
||||
Wrappers the FreeCAD api
|
||||
"""
|
||||
|
||||
def __init__(self, obj):
|
||||
self.wrapped = obj
|
||||
self.forConstruction = False
|
||||
|
||||
@classmethod
|
||||
def cast(cls, obj, forConstruction=False):
|
||||
"Returns the right type of wrapper, given a FreeCAD object"
|
||||
s = obj.ShapeType
|
||||
if type(obj) == FreeCAD.Base.Vector:
|
||||
return Vector(obj)
|
||||
tr = None
|
||||
|
||||
# TODO: there is a clever way to do this i'm sure with a lookup
|
||||
# but it is not a perfect mapping, because we are trying to hide
|
||||
# a bit of the complexity of Compounds in FreeCAD.
|
||||
if s == 'Vertex':
|
||||
tr = Vertex(obj)
|
||||
elif s == 'Edge':
|
||||
tr = Edge(obj)
|
||||
elif s == 'Wire':
|
||||
tr = Wire(obj)
|
||||
elif s == 'Face':
|
||||
tr = Face(obj)
|
||||
elif s == 'Shell':
|
||||
tr = Shell(obj)
|
||||
elif s == 'Solid':
|
||||
tr = Solid(obj)
|
||||
elif s == 'Compound':
|
||||
#compound of solids, lets return a solid instead
|
||||
if len(obj.Solids) > 1:
|
||||
tr = Solid(obj)
|
||||
elif len(obj.Solids) == 1:
|
||||
tr = Solid(obj.Solids[0])
|
||||
elif len(obj.Wires) > 0:
|
||||
tr = Wire(obj)
|
||||
else:
|
||||
tr = Compound(obj)
|
||||
else:
|
||||
raise ValueError("cast:unknown shape type %s" % s)
|
||||
|
||||
tr.forConstruction = forConstruction
|
||||
return tr
|
||||
|
||||
# TODO: all these should move into the exporters folder.
|
||||
# we dont need a bunch of exporting code stored in here!
|
||||
#
|
||||
def exportStl(self, fileName):
|
||||
self.wrapped.exportStl(fileName)
|
||||
|
||||
def exportStep(self, fileName):
|
||||
self.wrapped.exportStep(fileName)
|
||||
|
||||
def exportShape(self, fileName, fileFormat):
|
||||
if fileFormat == ExportFormats.STL:
|
||||
self.wrapped.exportStl(fileName)
|
||||
elif fileFormat == ExportFormats.BREP:
|
||||
self.wrapped.exportBrep(fileName)
|
||||
elif fileFormat == ExportFormats.STEP:
|
||||
self.wrapped.exportStep(fileName)
|
||||
elif fileFormat == ExportFormats.AMF:
|
||||
# not built into FreeCAD
|
||||
#TODO: user selected tolerance
|
||||
tess = self.wrapped.tessellate(0.1)
|
||||
aw = amfUtils.AMFWriter(tess)
|
||||
aw.writeAmf(fileName)
|
||||
elif fileFormat == ExportFormats.IGES:
|
||||
self.wrapped.exportIges(fileName)
|
||||
else:
|
||||
raise ValueError("Unknown export format: %s" % format)
|
||||
|
||||
def geomType(self):
|
||||
"""
|
||||
Gets the underlying geometry type
|
||||
:return: a string according to the geometry type.
|
||||
|
||||
Implementations can return any values desired, but the
|
||||
values the user uses in type filters should correspond to these.
|
||||
|
||||
As an example, if a user does::
|
||||
|
||||
CQ(object).faces("%mytype")
|
||||
|
||||
The expectation is that the geomType attribute will return 'mytype'
|
||||
|
||||
The return values depend on the type of the shape:
|
||||
|
||||
Vertex: always 'Vertex'
|
||||
Edge: LINE, ARC, CIRCLE, SPLINE
|
||||
Face: PLANE, SPHERE, CONE
|
||||
Solid: 'Solid'
|
||||
Shell: 'Shell'
|
||||
Compound: 'Compound'
|
||||
Wire: 'Wire'
|
||||
"""
|
||||
return self.wrapped.ShapeType
|
||||
|
||||
def isType(self, obj, strType):
|
||||
"""
|
||||
Returns True if the shape is the specified type, false otherwise
|
||||
|
||||
contrast with ShapeType, which will raise an exception
|
||||
if the provide object is not a shape at all
|
||||
"""
|
||||
if hasattr(obj, 'ShapeType'):
|
||||
return obj.ShapeType == strType
|
||||
else:
|
||||
return False
|
||||
|
||||
def hashCode(self):
|
||||
return self.wrapped.hashCode()
|
||||
|
||||
def isNull(self):
|
||||
return self.wrapped.isNull()
|
||||
|
||||
def isSame(self, other):
|
||||
return self.wrapped.isSame(other.wrapped)
|
||||
|
||||
def isEqual(self, other):
|
||||
return self.wrapped.isEqual(other.wrapped)
|
||||
|
||||
def isValid(self):
|
||||
return self.wrapped.isValid()
|
||||
|
||||
def BoundingBox(self):
|
||||
return BoundBox(self.wrapped.BoundBox)
|
||||
|
||||
def Center(self):
|
||||
# A Part.Shape object doesn't have the CenterOfMass function, but it's wrapped Solid(s) does
|
||||
if isinstance(self.wrapped, FreeCADPart.Shape):
|
||||
# If there are no Solids, we're probably dealing with a Face or something similar
|
||||
if len(self.Solids()) == 0:
|
||||
return Vector(self.wrapped.CenterOfMass)
|
||||
elif len(self.Solids()) == 1:
|
||||
return Vector(self.Solids()[0].wrapped.CenterOfMass)
|
||||
elif len(self.Solids()) > 1:
|
||||
return self.CombinedCenter(self.Solids())
|
||||
elif isinstance(self.wrapped, FreeCADPart.Solid):
|
||||
return Vector(self.wrapped.CenterOfMass)
|
||||
else:
|
||||
raise ValueError("Cannot find the center of %s object type" % str(type(self.Solids()[0].wrapped)))
|
||||
|
||||
@staticmethod
|
||||
def CombinedCenter(objects):
|
||||
"""
|
||||
Calculates the center of mass of multiple objects.
|
||||
|
||||
:param objects: a list of objects with mass
|
||||
"""
|
||||
total_mass = sum(o.wrapped.Mass for o in objects)
|
||||
weighted_centers = [o.wrapped.CenterOfMass.multiply(o.wrapped.Mass) for o in objects]
|
||||
|
||||
sum_wc = weighted_centers[0]
|
||||
for wc in weighted_centers[1:] :
|
||||
sum_wc = sum_wc.add(wc)
|
||||
|
||||
return Vector(sum_wc.multiply(1./total_mass))
|
||||
|
||||
def Closed(self):
|
||||
return self.wrapped.Closed
|
||||
|
||||
def ShapeType(self):
|
||||
return self.wrapped.ShapeType
|
||||
|
||||
def Vertices(self):
|
||||
return [Vertex(i) for i in self.wrapped.Vertexes]
|
||||
|
||||
def Edges(self):
|
||||
return [Edge(i) for i in self.wrapped.Edges]
|
||||
|
||||
def Compounds(self):
|
||||
return [Compound(i) for i in self.wrapped.Compounds]
|
||||
|
||||
def Wires(self):
|
||||
return [Wire(i) for i in self.wrapped.Wires]
|
||||
|
||||
def Faces(self):
|
||||
return [Face(i) for i in self.wrapped.Faces]
|
||||
|
||||
def Shells(self):
|
||||
return [Shell(i) for i in self.wrapped.Shells]
|
||||
|
||||
def Solids(self):
|
||||
return [Solid(i) for i in self.wrapped.Solids]
|
||||
|
||||
def Area(self):
|
||||
return self.wrapped.Area
|
||||
|
||||
def Length(self):
|
||||
return self.wrapped.Length
|
||||
|
||||
def rotate(self, startVector, endVector, angleDegrees):
|
||||
"""
|
||||
Rotates a shape around an axis
|
||||
:param startVector: start point of rotation axis either a 3-tuple or a Vector
|
||||
:param endVector: end point of rotation axis, either a 3-tuple or a Vector
|
||||
:param angleDegrees: angle to rotate, in degrees
|
||||
:return: a copy of the shape, rotated
|
||||
"""
|
||||
if type(startVector) == tuple:
|
||||
startVector = Vector(startVector)
|
||||
|
||||
if type(endVector) == tuple:
|
||||
endVector = Vector(endVector)
|
||||
|
||||
tmp = self.wrapped.copy()
|
||||
tmp.rotate(startVector.wrapped, endVector.wrapped, angleDegrees)
|
||||
return Shape.cast(tmp)
|
||||
|
||||
def translate(self, vector):
|
||||
|
||||
if type(vector) == tuple:
|
||||
vector = Vector(vector)
|
||||
tmp = self.wrapped.copy()
|
||||
tmp.translate(vector.wrapped)
|
||||
return Shape.cast(tmp)
|
||||
|
||||
def scale(self, factor):
|
||||
tmp = self.wrapped.copy()
|
||||
tmp.scale(factor)
|
||||
return Shape.cast(tmp)
|
||||
|
||||
def copy(self):
|
||||
return Shape.cast(self.wrapped.copy())
|
||||
|
||||
def transformShape(self, tMatrix):
|
||||
"""
|
||||
tMatrix is a matrix object.
|
||||
returns a copy of the ojbect, transformed by the provided matrix,
|
||||
with all objects keeping their type
|
||||
"""
|
||||
tmp = self.wrapped.copy()
|
||||
tmp.transformShape(tMatrix)
|
||||
r = Shape.cast(tmp)
|
||||
r.forConstruction = self.forConstruction
|
||||
return r
|
||||
|
||||
def transformGeometry(self, tMatrix):
|
||||
"""
|
||||
tMatrix is a matrix object.
|
||||
|
||||
returns a copy of the object, but with geometry transformed insetad of just
|
||||
rotated.
|
||||
|
||||
WARNING: transformGeometry will sometimes convert lines and circles to splines,
|
||||
but it also has the ability to handle skew and stretching transformations.
|
||||
|
||||
If your transformation is only translation and rotation, it is safer to use transformShape,
|
||||
which doesnt change the underlying type of the geometry, but cannot handle skew transformations
|
||||
"""
|
||||
tmp = self.wrapped.copy()
|
||||
tmp = tmp.transformGeometry(tMatrix)
|
||||
return Shape.cast(tmp)
|
||||
|
||||
def __hash__(self):
|
||||
return self.wrapped.hashCode()
|
||||
|
||||
|
||||
class Vertex(Shape):
|
||||
def __init__(self, obj, forConstruction=False):
|
||||
"""
|
||||
Create a vertex from a FreeCAD Vertex
|
||||
"""
|
||||
self.wrapped = obj
|
||||
self.forConstruction = forConstruction
|
||||
self.X = obj.X
|
||||
self.Y = obj.Y
|
||||
self.Z = obj.Z
|
||||
|
||||
def toTuple(self):
|
||||
return (self.X, self.Y, self.Z)
|
||||
|
||||
def Center(self):
|
||||
"""
|
||||
The center of a vertex is itself!
|
||||
"""
|
||||
return Vector(self.wrapped.Point)
|
||||
|
||||
|
||||
class Edge(Shape):
|
||||
def __init__(self, obj):
|
||||
"""
|
||||
An Edge
|
||||
"""
|
||||
self.wrapped = obj
|
||||
# self.startPoint = None
|
||||
# self.endPoint = None
|
||||
|
||||
self.edgetypes = {
|
||||
FreeCADPart.Line: 'LINE',
|
||||
FreeCADPart.ArcOfCircle: 'ARC',
|
||||
FreeCADPart.Circle: 'CIRCLE'
|
||||
}
|
||||
|
||||
def geomType(self):
|
||||
t = type(self.wrapped.Curve)
|
||||
if self.edgetypes.has_key(t):
|
||||
return self.edgetypes[t]
|
||||
else:
|
||||
return "Unknown Edge Curve Type: %s" % str(t)
|
||||
|
||||
def startPoint(self):
|
||||
"""
|
||||
|
||||
:return: a vector representing the start poing of this edge
|
||||
|
||||
Note, circles may have the start and end points the same
|
||||
"""
|
||||
# work around freecad bug where valueAt is unreliable
|
||||
curve = self.wrapped.Curve
|
||||
return Vector(curve.value(self.wrapped.ParameterRange[0]))
|
||||
|
||||
def endPoint(self):
|
||||
"""
|
||||
|
||||
:return: a vector representing the end point of this edge.
|
||||
|
||||
Note, circles may have the start and end points the same
|
||||
|
||||
"""
|
||||
# warning: easier syntax in freecad of <Edge>.valueAt(<Edge>.ParameterRange[1]) has
|
||||
# a bug with curves other than arcs, but using the underlying curve directly seems to work
|
||||
# that's the solution i'm using below
|
||||
curve = self.wrapped.Curve
|
||||
v = Vector(curve.value(self.wrapped.ParameterRange[1]))
|
||||
return v
|
||||
|
||||
def tangentAt(self, locationVector=None):
|
||||
"""
|
||||
Compute tangent vector at the specified location.
|
||||
:param locationVector: location to use. Use the center point if None
|
||||
:return: tangent vector
|
||||
"""
|
||||
if locationVector is None:
|
||||
locationVector = self.Center()
|
||||
|
||||
p = self.wrapped.Curve.parameter(locationVector.wrapped)
|
||||
return Vector(self.wrapped.tangentAt(p))
|
||||
|
||||
@classmethod
|
||||
def makeCircle(cls, radius, pnt=(0, 0, 0), dir=(0, 0, 1), angle1=360.0, angle2=360):
|
||||
return Edge(FreeCADPart.makeCircle(radius, toVector(pnt), toVector(dir), angle1, angle2))
|
||||
|
||||
@classmethod
|
||||
def makeSpline(cls, listOfVector):
|
||||
"""
|
||||
Interpolate a spline through the provided points.
|
||||
:param cls:
|
||||
:param listOfVector: a list of Vectors that represent the points
|
||||
:return: an Edge
|
||||
"""
|
||||
vecs = [v.wrapped for v in listOfVector]
|
||||
|
||||
spline = FreeCADPart.BSplineCurve()
|
||||
spline.interpolate(vecs, False)
|
||||
return Edge(spline.toShape())
|
||||
|
||||
@classmethod
|
||||
def makeThreePointArc(cls, v1, v2, v3):
|
||||
"""
|
||||
Makes a three point arc through the provided points
|
||||
:param cls:
|
||||
:param v1: start vector
|
||||
:param v2: middle vector
|
||||
:param v3: end vector
|
||||
:return: an edge object through the three points
|
||||
"""
|
||||
arc = FreeCADPart.Arc(v1.wrapped, v2.wrapped, v3.wrapped)
|
||||
e = Edge(arc.toShape())
|
||||
return e # arcane and undocumented, this creates an Edge object
|
||||
|
||||
@classmethod
|
||||
def makeLine(cls, v1, v2):
|
||||
"""
|
||||
Create a line between two points
|
||||
:param v1: Vector that represents the first point
|
||||
:param v2: Vector that represents the second point
|
||||
:return: A linear edge between the two provided points
|
||||
"""
|
||||
return Edge(FreeCADPart.makeLine(v1.toTuple(), v2.toTuple()))
|
||||
|
||||
|
||||
class Wire(Shape):
|
||||
def __init__(self, obj):
|
||||
"""
|
||||
A Wire
|
||||
"""
|
||||
self.wrapped = obj
|
||||
|
||||
@classmethod
|
||||
def combine(cls, listOfWires):
|
||||
"""
|
||||
Attempt to combine a list of wires into a new wire.
|
||||
the wires are returned in a list.
|
||||
:param cls:
|
||||
:param listOfWires:
|
||||
:return:
|
||||
"""
|
||||
return Shape.cast(FreeCADPart.Wire([w.wrapped for w in listOfWires]))
|
||||
|
||||
@classmethod
|
||||
def assembleEdges(cls, listOfEdges):
|
||||
"""
|
||||
Attempts to build a wire that consists of the edges in the provided list
|
||||
:param cls:
|
||||
:param listOfEdges: a list of Edge objects
|
||||
:return: a wire with the edges assembled
|
||||
"""
|
||||
fCEdges = [a.wrapped for a in listOfEdges]
|
||||
|
||||
wa = Wire(FreeCADPart.Wire(fCEdges))
|
||||
return wa
|
||||
|
||||
@classmethod
|
||||
def makeCircle(cls, radius, center, normal):
|
||||
"""
|
||||
Makes a Circle centered at the provided point, having normal in the provided direction
|
||||
:param radius: floating point radius of the circle, must be > 0
|
||||
:param center: vector representing the center of the circle
|
||||
:param normal: vector representing the direction of the plane the circle should lie in
|
||||
:return:
|
||||
"""
|
||||
w = Wire(FreeCADPart.Wire([FreeCADPart.makeCircle(radius, center.wrapped, normal.wrapped)]))
|
||||
return w
|
||||
|
||||
@classmethod
|
||||
def makePolygon(cls, listOfVertices, forConstruction=False):
|
||||
# convert list of tuples into Vectors.
|
||||
w = Wire(FreeCADPart.makePolygon([i.wrapped for i in listOfVertices]))
|
||||
w.forConstruction = forConstruction
|
||||
return w
|
||||
|
||||
@classmethod
|
||||
def makeHelix(cls, pitch, height, radius, angle=360.0):
|
||||
"""
|
||||
Make a helix with a given pitch, height and radius
|
||||
By default a cylindrical surface is used to create the helix. If
|
||||
the fourth parameter is set (the apex given in degree) a conical surface is used instead'
|
||||
"""
|
||||
return Wire(FreeCADPart.makeHelix(pitch, height, radius, angle))
|
||||
|
||||
def clean(self):
|
||||
"""This method is not implemented yet."""
|
||||
return self
|
||||
|
||||
class Face(Shape):
|
||||
def __init__(self, obj):
|
||||
"""
|
||||
A Face
|
||||
"""
|
||||
self.wrapped = obj
|
||||
|
||||
self.facetypes = {
|
||||
# TODO: bezier,bspline etc
|
||||
FreeCADPart.Plane: 'PLANE',
|
||||
FreeCADPart.Sphere: 'SPHERE',
|
||||
FreeCADPart.Cone: 'CONE'
|
||||
}
|
||||
|
||||
def geomType(self):
|
||||
t = type(self.wrapped.Surface)
|
||||
if self.facetypes.has_key(t):
|
||||
return self.facetypes[t]
|
||||
else:
|
||||
return "Unknown Face Surface Type: %s" % str(t)
|
||||
|
||||
def normalAt(self, locationVector=None):
|
||||
"""
|
||||
Computes the normal vector at the desired location on the face.
|
||||
|
||||
:returns: a vector representing the direction
|
||||
:param locationVector: the location to compute the normal at. If none, the center of the face is used.
|
||||
:type locationVector: a vector that lies on the surface.
|
||||
"""
|
||||
if locationVector == None:
|
||||
locationVector = self.Center()
|
||||
(u, v) = self.wrapped.Surface.parameter(locationVector.wrapped)
|
||||
|
||||
return Vector(self.wrapped.normalAt(u, v).normalize())
|
||||
|
||||
@classmethod
|
||||
def makePlane(cls, length, width, basePnt=None, dir=None):
|
||||
return Face(FreeCADPart.makePlan(length, width, toVector(basePnt), toVector(dir)))
|
||||
|
||||
@classmethod
|
||||
def makeRuledSurface(cls, edgeOrWire1, edgeOrWire2, dist=None):
|
||||
"""
|
||||
'makeRuledSurface(Edge|Wire,Edge|Wire) -- Make a ruled surface
|
||||
Create a ruled surface out of two edges or wires. If wires are used then
|
||||
these must have the same
|
||||
"""
|
||||
return Shape.cast(FreeCADPart.makeRuledSurface(edgeOrWire1.obj, edgeOrWire2.obj, dist))
|
||||
|
||||
def cut(self, faceToCut):
|
||||
"Remove a face from another one"
|
||||
return Shape.cast(self.obj.cut(faceToCut.obj))
|
||||
|
||||
def fuse(self, faceToJoin):
|
||||
return Shape.cast(self.obj.fuse(faceToJoin.obj))
|
||||
|
||||
def intersect(self, faceToIntersect):
|
||||
"""
|
||||
computes the intersection between the face and the supplied one.
|
||||
The result could be a face or a compound of faces
|
||||
"""
|
||||
return Shape.cast(self.obj.common(faceToIntersect.obj))
|
||||
|
||||
|
||||
class Shell(Shape):
|
||||
def __init__(self, wrapped):
|
||||
"""
|
||||
A Shell
|
||||
"""
|
||||
self.wrapped = wrapped
|
||||
|
||||
@classmethod
|
||||
def makeShell(cls, listOfFaces):
|
||||
return Shell(FreeCADPart.makeShell([i.obj for i in listOfFaces]))
|
||||
|
||||
|
||||
class Solid(Shape):
|
||||
def __init__(self, obj):
|
||||
"""
|
||||
A Solid
|
||||
"""
|
||||
self.wrapped = obj
|
||||
|
||||
@classmethod
|
||||
def isSolid(cls, obj):
|
||||
"""
|
||||
Returns true if the object is a FreeCAD solid, false otherwise
|
||||
"""
|
||||
if hasattr(obj, 'ShapeType'):
|
||||
if obj.ShapeType == 'Solid' or \
|
||||
(obj.ShapeType == 'Compound' and len(obj.Solids) > 0):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def makeBox(cls, length, width, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1)):
|
||||
"""
|
||||
makeBox(length,width,height,[pnt,dir]) -- Make a box located\nin pnt with the d
|
||||
imensions (length,width,height)\nBy default pnt=Vector(0,0,0) and dir=Vector(0,0,1)'
|
||||
"""
|
||||
return Shape.cast(FreeCADPart.makeBox(length, width, height, pnt.wrapped, dir.wrapped))
|
||||
|
||||
@classmethod
|
||||
def makeCone(cls, radius1, radius2, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360):
|
||||
"""
|
||||
'makeCone(radius1,radius2,height,[pnt,dir,angle]) --
|
||||
Make a cone with given radii and height\nBy default pnt=Vector(0,0,0),
|
||||
dir=Vector(0,0,1) and angle=360'
|
||||
"""
|
||||
return Shape.cast(FreeCADPart.makeCone(radius1, radius2, height, pnt.wrapped, dir.wrapped, angleDegrees))
|
||||
|
||||
@classmethod
|
||||
def makeCylinder(cls, radius, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360):
|
||||
"""
|
||||
makeCylinder(radius,height,[pnt,dir,angle]) --
|
||||
Make a cylinder with a given radius and height
|
||||
By default pnt=Vector(0,0,0),dir=Vector(0,0,1) and angle=360'
|
||||
"""
|
||||
return Shape.cast(FreeCADPart.makeCylinder(radius, height, pnt.wrapped, dir.wrapped, angleDegrees))
|
||||
|
||||
@classmethod
|
||||
def makeTorus(cls, radius1, radius2, pnt=None, dir=None, angleDegrees1=None, angleDegrees2=None):
|
||||
"""
|
||||
makeTorus(radius1,radius2,[pnt,dir,angle1,angle2,angle]) --
|
||||
Make a torus with agiven radii and angles
|
||||
By default pnt=Vector(0,0,0),dir=Vector(0,0,1),angle1=0
|
||||
,angle1=360 and angle=360'
|
||||
"""
|
||||
return Shape.cast(FreeCADPart.makeTorus(radius1, radius2, pnt, dir, angleDegrees1, angleDegrees2))
|
||||
|
||||
@classmethod
|
||||
def sweep(cls, profileWire, pathWire):
|
||||
"""
|
||||
make a solid by sweeping the profileWire along the specified path
|
||||
:param cls:
|
||||
:param profileWire:
|
||||
:param pathWire:
|
||||
:return:
|
||||
"""
|
||||
# needs to use freecad wire.makePipe or makePipeShell
|
||||
# needs to allow free-space wires ( those not made from a workplane )
|
||||
|
||||
@classmethod
|
||||
def makeLoft(cls, listOfWire, ruled=False):
|
||||
"""
|
||||
makes a loft from a list of wires
|
||||
The wires will be converted into faces when possible-- it is presumed that nobody ever actually
|
||||
wants to make an infinitely thin shell for a real FreeCADPart.
|
||||
"""
|
||||
# the True flag requests building a solid instead of a shell.
|
||||
|
||||
return Shape.cast(FreeCADPart.makeLoft([i.wrapped for i in listOfWire], True, ruled))
|
||||
|
||||
@classmethod
|
||||
def makeWedge(cls, xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt=None, dir=None):
|
||||
"""
|
||||
'makeWedge(xmin, ymin, zmin, z2min, x2min,
|
||||
xmax, ymax, zmax, z2max, x2max,[pnt, dir])
|
||||
Make a wedge located in pnt\nBy default pnt=Vector(0,0,0) and dir=Vec
|
||||
tor(0,0,1)'
|
||||
"""
|
||||
return Shape.cast(
|
||||
FreeCADPart.makeWedge(xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt, dir))
|
||||
|
||||
@classmethod
|
||||
def makeSphere(cls, radius, pnt=None, dir=None, angleDegrees1=None, angleDegrees2=None, angleDegrees3=None):
|
||||
"""
|
||||
'makeSphere(radius,[pnt, dir, angle1,angle2,angle3]) --
|
||||
Make a sphere with a giv
|
||||
en radius\nBy default pnt=Vector(0,0,0), dir=Vector(0,0,1), angle1=0, angle2=90 and angle3=360'
|
||||
"""
|
||||
return Shape.cast(FreeCADPart.makeSphere(radius, pnt.wrapped, dir.wrapped, angleDegrees1, angleDegrees2, angleDegrees3))
|
||||
|
||||
@classmethod
|
||||
def extrudeLinearWithRotation(cls, outerWire, innerWires, vecCenter, vecNormal, angleDegrees):
|
||||
"""
|
||||
Creates a 'twisted prism' by extruding, while simultaneously rotating around the extrusion vector.
|
||||
|
||||
Though the signature may appear to be similar enough to extrudeLinear to merit combining them, the
|
||||
construction methods used here are different enough that they should be separate.
|
||||
|
||||
At a high level, the steps followed ar:
|
||||
(1) accept a set of wires
|
||||
(2) create another set of wires like this one, but which are transformed and rotated
|
||||
(3) create a ruledSurface between the sets of wires
|
||||
(40 create a shell and compute the resulting object
|
||||
|
||||
:param outerWire: the outermost wire, a cad.Wire
|
||||
:param innerWires: a list of inner wires, a list of cad.Wire
|
||||
:param vecCenter: the center point about which to rotate. the axis of rotation is defined by
|
||||
vecNormal, located at vecCenter. ( a cad.Vector )
|
||||
:param vecNormal: a vector along which to extrude the wires ( a cad.Vector )
|
||||
:param angleDegrees: the angle to rotate through while extruding
|
||||
:return: a cad.Solid object
|
||||
"""
|
||||
|
||||
# from this point down we are dealing with FreeCAD wires not cad.wires
|
||||
startWires = [outerWire.wrapped] + [i.wrapped for i in innerWires]
|
||||
endWires = []
|
||||
p1 = vecCenter.wrapped
|
||||
p2 = vecCenter.add(vecNormal).wrapped
|
||||
|
||||
# make translated and rotated copy of each wire
|
||||
for w in startWires:
|
||||
w2 = w.copy()
|
||||
w2.translate(vecNormal.wrapped)
|
||||
w2.rotate(p1, p2, angleDegrees)
|
||||
endWires.append(w2)
|
||||
|
||||
# make a ruled surface for each set of wires
|
||||
sides = []
|
||||
for w1, w2 in zip(startWires, endWires):
|
||||
rs = FreeCADPart.makeRuledSurface(w1, w2)
|
||||
sides.append(rs)
|
||||
|
||||
#make faces for the top and bottom
|
||||
startFace = FreeCADPart.Face(startWires)
|
||||
endFace = FreeCADPart.Face(endWires)
|
||||
|
||||
#collect all the faces from the sides
|
||||
faceList = [startFace]
|
||||
for s in sides:
|
||||
faceList.extend(s.Faces)
|
||||
faceList.append(endFace)
|
||||
|
||||
shell = FreeCADPart.makeShell(faceList)
|
||||
solid = FreeCADPart.makeSolid(shell)
|
||||
return Shape.cast(solid)
|
||||
|
||||
@classmethod
|
||||
def extrudeLinear(cls, outerWire, innerWires, vecNormal):
|
||||
"""
|
||||
Attempt to extrude the list of wires into a prismatic solid in the provided direction
|
||||
|
||||
:param outerWire: the outermost wire
|
||||
:param innerWires: a list of inner wires
|
||||
:param vecNormal: a vector along which to extrude the wires
|
||||
:return: a Solid object
|
||||
|
||||
The wires must not intersect
|
||||
|
||||
Extruding wires is very non-trivial. Nested wires imply very different geometry, and
|
||||
there are many geometries that are invalid. In general, the following conditions must be met:
|
||||
|
||||
* all wires must be closed
|
||||
* there cannot be any intersecting or self-intersecting wires
|
||||
* wires must be listed from outside in
|
||||
* more than one levels of nesting is not supported reliably
|
||||
|
||||
This method will attempt to sort the wires, but there is much work remaining to make this method
|
||||
reliable.
|
||||
"""
|
||||
|
||||
# one would think that fusing faces into a compound and then extruding would work,
|
||||
# but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
|
||||
# but then cutting it from the main solid fails with BRep_NotDone.
|
||||
#the work around is to extrude each and then join the resulting solids, which seems to work
|
||||
|
||||
#FreeCAD allows this in one operation, but others might not
|
||||
freeCADWires = [outerWire.wrapped]
|
||||
for w in innerWires:
|
||||
freeCADWires.append(w.wrapped)
|
||||
|
||||
f = FreeCADPart.Face(freeCADWires)
|
||||
result = f.extrude(vecNormal.wrapped)
|
||||
|
||||
return Shape.cast(result)
|
||||
|
||||
@classmethod
|
||||
def revolve(cls, outerWire, innerWires, angleDegrees, axisStart, axisEnd):
|
||||
"""
|
||||
Attempt to revolve the list of wires into a solid in the provided direction
|
||||
|
||||
:param outerWire: the outermost wire
|
||||
:param innerWires: a list of inner wires
|
||||
:param angleDegrees: the angle to revolve through.
|
||||
:type angleDegrees: float, anything less than 360 degrees will leave the shape open
|
||||
:param axisStart: the start point of the axis of rotation
|
||||
:type axisStart: tuple, a two tuple
|
||||
:param axisEnd: the end point of the axis of rotation
|
||||
:type axisEnd: tuple, a two tuple
|
||||
:return: a Solid object
|
||||
|
||||
The wires must not intersect
|
||||
|
||||
* all wires must be closed
|
||||
* there cannot be any intersecting or self-intersecting wires
|
||||
* wires must be listed from outside in
|
||||
* more than one levels of nesting is not supported reliably
|
||||
* the wire(s) that you're revolving cannot be centered
|
||||
|
||||
This method will attempt to sort the wires, but there is much work remaining to make this method
|
||||
reliable.
|
||||
"""
|
||||
freeCADWires = [outerWire.wrapped]
|
||||
|
||||
for w in innerWires:
|
||||
freeCADWires.append(w.wrapped)
|
||||
|
||||
f = FreeCADPart.Face(freeCADWires)
|
||||
|
||||
rotateCenter = FreeCAD.Base.Vector(axisStart)
|
||||
rotateAxis = FreeCAD.Base.Vector(axisEnd)
|
||||
|
||||
#Convert our axis end vector into to something FreeCAD will understand (an axis specification vector)
|
||||
rotateAxis = rotateCenter.sub(rotateAxis)
|
||||
|
||||
#FreeCAD wants a rotation center and then an axis to rotate around rather than an axis of rotation
|
||||
result = f.revolve(rotateCenter, rotateAxis, angleDegrees)
|
||||
|
||||
return Shape.cast(result)
|
||||
|
||||
def tessellate(self, tolerance):
|
||||
return self.wrapped.tessellate(tolerance)
|
||||
|
||||
def intersect(self, toIntersect):
|
||||
"""
|
||||
computes the intersection between this solid and the supplied one
|
||||
The result could be a face or a compound of faces
|
||||
"""
|
||||
return Shape.cast(self.wrapped.common(toIntersect.wrapped))
|
||||
|
||||
def cut(self, solidToCut):
|
||||
"Remove a solid from another one"
|
||||
return Shape.cast(self.wrapped.cut(solidToCut.wrapped))
|
||||
|
||||
def fuse(self, solidToJoin):
|
||||
return Shape.cast(self.wrapped.fuse(solidToJoin.wrapped))
|
||||
|
||||
def clean(self):
|
||||
"""Clean faces by removing splitter edges."""
|
||||
r = self.wrapped.removeSplitter()
|
||||
# removeSplitter() returns a generic Shape type, cast to actual type of object
|
||||
r = FreeCADPart.cast_to_shape(r)
|
||||
return Shape.cast(r)
|
||||
|
||||
def fillet(self, radius, edgeList):
|
||||
"""
|
||||
Fillets the specified edges of this solid.
|
||||
:param radius: float > 0, the radius of the fillet
|
||||
:param edgeList: a list of Edge objects, which must belong to this solid
|
||||
:return: Filleted solid
|
||||
"""
|
||||
nativeEdges = [e.wrapped for e in edgeList]
|
||||
return Shape.cast(self.wrapped.makeFillet(radius, nativeEdges))
|
||||
|
||||
def chamfer(self, length, length2, edgeList):
|
||||
"""
|
||||
Chamfers the specified edges of this solid.
|
||||
:param length: length > 0, the length (length) of the chamfer
|
||||
:param length2: length2 > 0, optional parameter for asymmetrical chamfer. Should be `None` if not required.
|
||||
:param edgeList: a list of Edge objects, which must belong to this solid
|
||||
:return: Chamfered solid
|
||||
"""
|
||||
nativeEdges = [e.wrapped for e in edgeList]
|
||||
# note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API
|
||||
if length2:
|
||||
return Shape.cast(self.wrapped.makeChamfer(length, length2, nativeEdges))
|
||||
else:
|
||||
return Shape.cast(self.wrapped.makeChamfer(length, nativeEdges))
|
||||
|
||||
def shell(self, faceList, thickness, tolerance=0.0001):
|
||||
"""
|
||||
make a shelled solid of given by removing the list of faces
|
||||
|
||||
:param faceList: list of face objects, which must be part of the solid.
|
||||
:param thickness: floating point thickness. positive shells outwards, negative shells inwards
|
||||
:param tolerance: modelling tolerance of the method, default=0.0001
|
||||
:return: a shelled solid
|
||||
|
||||
**WARNING** The underlying FreeCAD implementation can very frequently have problems
|
||||
with shelling complex geometries!
|
||||
"""
|
||||
nativeFaces = [f.wrapped for f in faceList]
|
||||
return Shape.cast(self.wrapped.makeThickness(nativeFaces, thickness, tolerance))
|
||||
|
||||
|
||||
class Compound(Shape):
|
||||
def __init__(self, obj):
|
||||
"""
|
||||
An Edge
|
||||
"""
|
||||
self.wrapped = obj
|
||||
|
||||
def Center(self):
|
||||
return self.Center()
|
||||
|
||||
@classmethod
|
||||
def makeCompound(cls, listOfShapes):
|
||||
"""
|
||||
Create a compound out of a list of shapes
|
||||
"""
|
||||
solids = [s.wrapped for s in listOfShapes]
|
||||
c = FreeCADPart.Compound(solids)
|
||||
return Shape.cast(c)
|
||||
|
||||
def fuse(self, toJoin):
|
||||
return Shape.cast(self.wrapped.fuse(toJoin.wrapped))
|
||||
|
||||
def tessellate(self, tolerance):
|
||||
return self.wrapped.tessellate(tolerance)
|
||||
|
||||
def clean(self):
|
||||
"""This method is not implemented yet."""
|
||||
return self
|
|
@ -1,18 +0,0 @@
|
|||
"""
|
||||
CadQuery
|
||||
Copyright (C) 2015 Parametric Products Intellectual Holdings, LLC
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
"""
|
|
@ -1,474 +0,0 @@
|
|||
"""
|
||||
Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC
|
||||
|
||||
This file is part of CadQuery.
|
||||
|
||||
CadQuery is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
CadQuery is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; If not, see <http://www.gnu.org/licenses/>
|
||||
"""
|
||||
|
||||
import re
|
||||
import math
|
||||
from cadquery import Vector,Edge,Vertex,Face,Solid,Shell,Compound
|
||||
|
||||
|
||||
class Selector(object):
|
||||
"""
|
||||
Filters a list of objects
|
||||
|
||||
Filters must provide a single method that filters objects.
|
||||
"""
|
||||
def filter(self,objectList):
|
||||
"""
|
||||
Filter the provided list
|
||||
:param objectList: list to filter
|
||||
:type objectList: list of FreeCAD primatives
|
||||
:return: filtered list
|
||||
|
||||
The default implementation returns the original list unfiltered
|
||||
|
||||
"""
|
||||
return objectList
|
||||
|
||||
def __and__(self, other):
|
||||
return AndSelector(self, other)
|
||||
|
||||
def __add__(self, other):
|
||||
return SumSelector(self, other)
|
||||
|
||||
def __sub__(self, other):
|
||||
return SubtractSelector(self, other)
|
||||
|
||||
def __neg__(self):
|
||||
return InverseSelector(self)
|
||||
|
||||
class NearestToPointSelector(Selector):
|
||||
"""
|
||||
Selects object nearest the provided point.
|
||||
|
||||
If the object is a vertex or point, the distance
|
||||
is used. For other kinds of shapes, the center of mass
|
||||
is used to to compute which is closest.
|
||||
|
||||
Applicability: All Types of Shapes
|
||||
|
||||
Example::
|
||||
|
||||
CQ(aCube).vertices(NearestToPointSelector((0,1,0))
|
||||
|
||||
returns the vertex of the unit cube closest to the point x=0,y=1,z=0
|
||||
|
||||
"""
|
||||
def __init__(self,pnt ):
|
||||
self.pnt = pnt
|
||||
def filter(self,objectList):
|
||||
|
||||
def dist(tShape):
|
||||
return tShape.Center().sub(Vector(*self.pnt)).Length
|
||||
#if tShape.ShapeType == 'Vertex':
|
||||
# return tShape.Point.sub(toVector(self.pnt)).Length
|
||||
#else:
|
||||
# return tShape.CenterOfMass.sub(toVector(self.pnt)).Length
|
||||
|
||||
return [ min(objectList,key=dist) ]
|
||||
|
||||
class BoxSelector(Selector):
|
||||
"""
|
||||
Selects objects inside the 3D box defined by 2 points.
|
||||
|
||||
If `boundingbox` is True only the objects that have their bounding
|
||||
box inside the given box is selected. Otherwise only center point
|
||||
of the object is tested.
|
||||
|
||||
Applicability: all types of shapes
|
||||
|
||||
Example::
|
||||
|
||||
CQ(aCube).edges(BoxSelector((0,1,0), (1,2,1))
|
||||
"""
|
||||
def __init__(self, point0, point1, boundingbox=False):
|
||||
self.p0 = Vector(*point0)
|
||||
self.p1 = Vector(*point1)
|
||||
self.test_boundingbox = boundingbox
|
||||
|
||||
def filter(self, objectList):
|
||||
|
||||
result = []
|
||||
x0, y0, z0 = self.p0.toTuple()
|
||||
x1, y1, z1 = self.p1.toTuple()
|
||||
|
||||
def isInsideBox(p):
|
||||
# using XOR for checking if x/y/z is in between regardless
|
||||
# of order of x/y/z0 and x/y/z1
|
||||
return ((p.x < x0) ^ (p.x < x1)) and \
|
||||
((p.y < y0) ^ (p.y < y1)) and \
|
||||
((p.z < z0) ^ (p.z < z1))
|
||||
|
||||
for o in objectList:
|
||||
if self.test_boundingbox:
|
||||
bb = o.BoundingBox()
|
||||
if isInsideBox(Vector(bb.xmin, bb.ymin, bb.zmin)) and \
|
||||
isInsideBox(Vector(bb.xmax, bb.ymax, bb.zmax)):
|
||||
result.append(o)
|
||||
else:
|
||||
if isInsideBox(o.Center()):
|
||||
result.append(o)
|
||||
|
||||
return result
|
||||
|
||||
class BaseDirSelector(Selector):
|
||||
"""
|
||||
A selector that handles selection on the basis of a single
|
||||
direction vector
|
||||
"""
|
||||
def __init__(self,vector,tolerance=0.0001 ):
|
||||
self.direction = vector
|
||||
self.TOLERANCE = tolerance
|
||||
|
||||
def test(self,vec):
|
||||
"Test a specified vector. Subclasses override to provide other implementations"
|
||||
return True
|
||||
|
||||
def filter(self,objectList):
|
||||
"""
|
||||
There are lots of kinds of filters, but
|
||||
for planes they are always based on the normal of the plane,
|
||||
and for edges on the tangent vector along the edge
|
||||
"""
|
||||
r = []
|
||||
for o in objectList:
|
||||
#no really good way to avoid a switch here, edges and faces are simply different!
|
||||
|
||||
if type(o) == Face:
|
||||
# a face is only parallell to a direction if it is a plane, and its normal is parallel to the dir
|
||||
normal = o.normalAt(None)
|
||||
|
||||
if self.test(normal):
|
||||
r.append(o)
|
||||
elif type(o) == Edge and o.geomType() == 'LINE':
|
||||
#an edge is parallel to a direction if it is a line, and the line is parallel to the dir
|
||||
tangent = o.tangentAt(None)
|
||||
if self.test(tangent):
|
||||
r.append(o)
|
||||
|
||||
return r
|
||||
|
||||
class ParallelDirSelector(BaseDirSelector):
|
||||
"""
|
||||
Selects objects parallel with the provided direction
|
||||
|
||||
Applicability:
|
||||
Linear Edges
|
||||
Planar Faces
|
||||
|
||||
Use the string syntax shortcut \|(X|Y|Z) if you want to select
|
||||
based on a cardinal direction.
|
||||
|
||||
Example::
|
||||
|
||||
CQ(aCube).faces(ParallelDirSelector((0,0,1))
|
||||
|
||||
selects faces with a normals in the z direction, and is equivalent to::
|
||||
|
||||
CQ(aCube).faces("|Z")
|
||||
"""
|
||||
|
||||
def test(self,vec):
|
||||
return self.direction.cross(vec).Length < self.TOLERANCE
|
||||
|
||||
class DirectionSelector(BaseDirSelector):
|
||||
"""
|
||||
Selects objects aligned with the provided direction
|
||||
|
||||
Applicability:
|
||||
Linear Edges
|
||||
Planar Faces
|
||||
|
||||
Use the string syntax shortcut +/-(X|Y|Z) if you want to select
|
||||
based on a cardinal direction.
|
||||
|
||||
Example::
|
||||
|
||||
CQ(aCube).faces(DirectionSelector((0,0,1))
|
||||
|
||||
selects faces with a normals in the z direction, and is equivalent to::
|
||||
|
||||
CQ(aCube).faces("+Z")
|
||||
"""
|
||||
|
||||
def test(self,vec):
|
||||
return abs(self.direction.getAngle(vec) < self.TOLERANCE)
|
||||
|
||||
class PerpendicularDirSelector(BaseDirSelector):
|
||||
"""
|
||||
Selects objects perpendicular with the provided direction
|
||||
|
||||
Applicability:
|
||||
Linear Edges
|
||||
Planar Faces
|
||||
|
||||
Use the string syntax shortcut #(X|Y|Z) if you want to select
|
||||
based on a cardinal direction.
|
||||
|
||||
Example::
|
||||
|
||||
CQ(aCube).faces(PerpendicularDirSelector((0,0,1))
|
||||
|
||||
selects faces with a normals perpendicular to the z direction, and is equivalent to::
|
||||
|
||||
CQ(aCube).faces("#Z")
|
||||
"""
|
||||
|
||||
def test(self,vec):
|
||||
angle = self.direction.getAngle(vec)
|
||||
r = (abs(angle) < self.TOLERANCE) or (abs(angle - math.pi) < self.TOLERANCE )
|
||||
return not r
|
||||
|
||||
|
||||
class TypeSelector(Selector):
|
||||
"""
|
||||
Selects objects of the prescribed topological type.
|
||||
|
||||
Applicability:
|
||||
Faces: Plane,Cylinder,Sphere
|
||||
Edges: Line,Circle,Arc
|
||||
|
||||
You can use the shortcut selector %(PLANE|SPHERE|CONE) for faces,
|
||||
and %(LINE|ARC|CIRCLE) for edges.
|
||||
|
||||
For example this::
|
||||
|
||||
CQ(aCube).faces ( TypeSelector("PLANE") )
|
||||
|
||||
will select 6 faces, and is equivalent to::
|
||||
|
||||
CQ(aCube).faces( "%PLANE" )
|
||||
|
||||
"""
|
||||
def __init__(self,typeString):
|
||||
self.typeString = typeString.upper()
|
||||
|
||||
def filter(self,objectList):
|
||||
r = []
|
||||
for o in objectList:
|
||||
if o.geomType() == self.typeString:
|
||||
r.append(o)
|
||||
return r
|
||||
|
||||
class DirectionMinMaxSelector(Selector):
|
||||
"""
|
||||
Selects objects closest or farthest in the specified direction
|
||||
Used for faces, points, and edges
|
||||
|
||||
Applicability:
|
||||
All object types. for a vertex, its point is used. for all other kinds
|
||||
of objects, the center of mass of the object is used.
|
||||
|
||||
You can use the string shortcuts >(X|Y|Z) or <(X|Y|Z) if you want to
|
||||
select based on a cardinal direction.
|
||||
|
||||
For example this::
|
||||
|
||||
CQ(aCube).faces ( DirectionMinMaxSelector((0,0,1),True )
|
||||
|
||||
Means to select the face having the center of mass farthest in the positive z direction,
|
||||
and is the same as:
|
||||
|
||||
CQ(aCube).faces( ">Z" )
|
||||
|
||||
Future Enhancements:
|
||||
provide a nicer way to select in arbitrary directions. IE, a bit more code could
|
||||
allow '>(0,0,1)' to work.
|
||||
|
||||
"""
|
||||
def __init__(self, vector, directionMax=True, tolerance=0.0001):
|
||||
self.vector = vector
|
||||
self.max = max
|
||||
self.directionMax = directionMax
|
||||
self.TOLERANCE = tolerance
|
||||
def filter(self,objectList):
|
||||
|
||||
def distance(tShape):
|
||||
return tShape.Center().dot(self.vector)
|
||||
#if tShape.ShapeType == 'Vertex':
|
||||
# pnt = tShape.Point
|
||||
#else:
|
||||
# pnt = tShape.Center()
|
||||
#return pnt.dot(self.vector)
|
||||
|
||||
# find out the max/min distance
|
||||
if self.directionMax:
|
||||
d = max(map(distance, objectList))
|
||||
else:
|
||||
d = min(map(distance, objectList))
|
||||
|
||||
# return all objects at the max/min distance (within a tolerance)
|
||||
return filter(lambda o: abs(d - distance(o)) < self.TOLERANCE, objectList)
|
||||
|
||||
class BinarySelector(Selector):
|
||||
"""
|
||||
Base class for selectors that operates with two other
|
||||
selectors. Subclass must implement the :filterResults(): method.
|
||||
"""
|
||||
def __init__(self, left, right):
|
||||
self.left = left
|
||||
self.right = right
|
||||
|
||||
def filter(self, objectList):
|
||||
return self.filterResults(self.left.filter(objectList),
|
||||
self.right.filter(objectList))
|
||||
|
||||
def filterResults(self, r_left, r_right):
|
||||
raise NotImplementedError
|
||||
|
||||
class AndSelector(BinarySelector):
|
||||
"""
|
||||
Intersection selector. Returns objects that is selected by both selectors.
|
||||
"""
|
||||
def filterResults(self, r_left, r_right):
|
||||
# return intersection of lists
|
||||
return list(set(r_left) & set(r_right))
|
||||
|
||||
class SumSelector(BinarySelector):
|
||||
"""
|
||||
Union selector. Returns the sum of two selectors results.
|
||||
"""
|
||||
def filterResults(self, r_left, r_right):
|
||||
# return the union (no duplicates) of lists
|
||||
return list(set(r_left + r_right))
|
||||
|
||||
class SubtractSelector(BinarySelector):
|
||||
"""
|
||||
Difference selector. Substract results of a selector from another
|
||||
selectors results.
|
||||
"""
|
||||
def filterResults(self, r_left, r_right):
|
||||
return list(set(r_left) - set(r_right))
|
||||
|
||||
class InverseSelector(Selector):
|
||||
"""
|
||||
Inverts the selection of given selector. In other words, selects
|
||||
all objects that is not selected by given selector.
|
||||
"""
|
||||
def __init__(self, selector):
|
||||
self.selector = selector
|
||||
|
||||
def filter(self, objectList):
|
||||
# note that Selector() selects everything
|
||||
return SubtractSelector(Selector(), self.selector).filter(objectList)
|
||||
|
||||
class StringSyntaxSelector(Selector):
|
||||
"""
|
||||
Filter lists objects using a simple string syntax. All of the filters available in the string syntax
|
||||
are also available ( usually with more functionality ) through the creation of full-fledged
|
||||
selector objects. see :py:class:`Selector` and its subclasses
|
||||
|
||||
Filtering works differently depending on the type of object list being filtered.
|
||||
|
||||
:param selectorString: A two-part selector string, [selector][axis]
|
||||
|
||||
:return: objects that match the specified selector
|
||||
|
||||
***Modfiers*** are ``('|','+','-','<','>','%')``
|
||||
|
||||
:\|:
|
||||
parallel to ( same as :py:class:`ParallelDirSelector` ). Can return multiple objects.
|
||||
:#:
|
||||
perpendicular to (same as :py:class:`PerpendicularDirSelector` )
|
||||
:+:
|
||||
positive direction (same as :py:class:`DirectionSelector` )
|
||||
:-:
|
||||
negative direction (same as :py:class:`DirectionSelector` )
|
||||
:>:
|
||||
maximize (same as :py:class:`DirectionMinMaxSelector` with directionMax=True)
|
||||
:<:
|
||||
minimize (same as :py:class:`DirectionMinMaxSelector` with directionMax=False )
|
||||
:%:
|
||||
curve/surface type (same as :py:class:`TypeSelector`)
|
||||
|
||||
***axisStrings*** are: ``X,Y,Z,XY,YZ,XZ``
|
||||
|
||||
Selectors are a complex topic: see :ref:`selector_reference` for more information
|
||||
|
||||
|
||||
|
||||
"""
|
||||
def __init__(self,selectorString):
|
||||
|
||||
self.axes = {
|
||||
'X': Vector(1,0,0),
|
||||
'Y': Vector(0,1,0),
|
||||
'Z': Vector(0,0,1),
|
||||
'XY': Vector(1,1,0),
|
||||
'YZ': Vector(0,1,1),
|
||||
'XZ': Vector(1,0,1)
|
||||
}
|
||||
|
||||
namedViews = {
|
||||
'front': ('>','Z' ),
|
||||
'back': ('<','Z'),
|
||||
'left':('<', 'X'),
|
||||
'right': ('>', 'X'),
|
||||
'top': ('>','Y'),
|
||||
'bottom': ('<','Y')
|
||||
}
|
||||
self.selectorString = selectorString
|
||||
r = re.compile("\s*([-\+<>\|\%#])*\s*(\w+)\s*",re.IGNORECASE)
|
||||
m = r.match(selectorString)
|
||||
|
||||
if m != None:
|
||||
if namedViews.has_key(selectorString):
|
||||
(a,b) = namedViews[selectorString]
|
||||
self.mySelector = self._chooseSelector(a,b )
|
||||
else:
|
||||
self.mySelector = self._chooseSelector(m.groups()[0],m.groups()[1])
|
||||
else:
|
||||
raise ValueError ("Selector String format must be [-+<>|#%] X|Y|Z ")
|
||||
|
||||
|
||||
def _chooseSelector(self,selType,selAxis):
|
||||
"""Sets up the underlying filters accordingly"""
|
||||
|
||||
if selType == "%":
|
||||
return TypeSelector(selAxis)
|
||||
|
||||
#all other types need to select axis as a vector
|
||||
#get the axis vector first, will throw an except if an unknown axis is used
|
||||
try:
|
||||
vec = self.axes[selAxis]
|
||||
except KeyError:
|
||||
raise ValueError ("Axis value %s not allowed: must be one of %s" % (selAxis, str(self.axes)))
|
||||
|
||||
if selType in (None, "+"):
|
||||
#use direction filter
|
||||
return DirectionSelector(vec)
|
||||
elif selType == '-':
|
||||
#just use the reverse of the direction vector
|
||||
return DirectionSelector(vec.multiply(-1.0))
|
||||
elif selType == "|":
|
||||
return ParallelDirSelector(vec)
|
||||
elif selType == ">":
|
||||
return DirectionMinMaxSelector(vec,True)
|
||||
elif selType == "<":
|
||||
return DirectionMinMaxSelector(vec,False)
|
||||
elif selType == '#':
|
||||
return PerpendicularDirSelector(vec)
|
||||
else:
|
||||
raise ValueError ("Selector String format must be [-+<>|] X|Y|Z ")
|
||||
|
||||
def filter(self,objectList):
|
||||
"""
|
||||
selects minimum, maximum, positive or negative values relative to a direction
|
||||
[+\|-\|<\|>\|] \<X\|Y\|Z>
|
||||
"""
|
||||
return self.mySelector.filter(objectList)
|
Loading…
Reference in New Issue
Block a user