workable version of cqgi version

This commit is contained in:
Dave Cowden 2015-12-09 21:01:14 -05:00
parent 190980d4a1
commit 142a5c88d8
18 changed files with 796 additions and 257 deletions

View File

@ -8,7 +8,7 @@ from .freecad_impl import importers
#the order of these matter
from .selectors import *
from .CQ import *
from .cq import *
__all__ = [

View File

@ -35,7 +35,7 @@ def cq_directive(name, arguments, options, content, lineno,
try:
_s = StringIO.StringIO()
result = cqgi.execute(plot_code)
result = cqgi.parse(plot_code).build()
if result.success:
exporters.exportShape(result.first_result, "SVG", _s)

View File

@ -1,70 +1,99 @@
"""
The CadQuery Container Environment.
The CadQuery Gateway Interface.
Provides classes and tools for executing CadQuery scripts
"""
import ast
import traceback
import re
import time
import cadquery
CQSCRIPT = "<cqscript>"
def execute(script_source, build_parameters=None):
def parse(script_source):
"""
Executes the provided model, using the specified variables.
Parses the script as a model, and returns a model.
If you would prefer to access the underlying model without building it,
for example, to inspect its available parameters, construct a CQModel object.
:param script_source: the script to run. Must be a valid cadquery script
:param build_parameters: a dictionary of variables. The variables must be
assignable to the underlying variable type.
:raises: Nothing. If there is an exception, it will be on the exception property of the result.
This is the interface so that we can return other information onthe result, such as the build time
:return: a BuildResult object, which includes the status of the result, and either
a resulting shape or an exception
:return: a CQModel object that defines the script and allows execution
"""
model = CQModel(script_source)
return model.build(build_parameters)
return model
class CQModel(object):
"""
Object that provides a nice interface to a cq script that
is following the cce model
Represents a Cadquery Script.
After construction, the metadata property contains
a ScriptMetaData object, which describes the model in more detail,
and can be used to retrive the parameters defined by the model.
the build method can be used to generate a 3d model
"""
def __init__(self, script_source):
"""
Create an object by parsing the supplied python script.
:param script_source: a python script to parse
"""
self.metadata = ScriptMetadata()
self.ast_tree = ast.parse(script_source, CQSCRIPT)
ConstantAssignmentFinder(self.metadata).visit(self.ast_tree)
self.script_source = script_source
self._find_vars()
# TODO: pick up other scirpt metadata:
# describe
# pick up validation methods
def _find_vars(self):
"""
Parse the script, and populate variables that appear to be
overridable.
"""
#assumption here: we assume that variable declarations
#are only at the top level of the script. IE, we'll ignore any
#variable definitions at lower levels of the script
#we dont want to use the visit interface because here we excplicitly
#want to walk only the top level of the tree.
assignment_finder = ConstantAssignmentFinder(self.metadata)
for node in self.ast_tree.body:
if isinstance(node, ast.Assign):
assignment_finder.visit_Assign(node)
def validate(self, params):
"""
Determine if the supplied parameters are valid.
NOT IMPLEMENTED YET-- raises NotImplementedError
:param params: a dictionary of parameters
"""
raise NotImplementedError("not yet implemented")
def build(self, params=None):
def build(self, build_parameters=None):
"""
:param params: dictionary of parameter values to build with
:return:
Executes the script, using the optional parameters to override those in the model
:param build_parameters: a dictionary of variables. The variables must be
assignable to the underlying variable type.
:raises: Nothing. If there is an exception, it will be on the exception property of the result.
This is the interface so that we can return other information onthe result, such as the build time
:return: a BuildResult object, which includes the status of the result, and either
a resulting shape or an exception
"""
if not params:
params = {}
if not build_parameters:
build_parameters = {}
start = time.clock()
result = BuildResult()
try:
self.set_param_values(params)
collector = BuildObjectCollector()
self.set_param_values(build_parameters)
collector = ScriptCallback()
env = EnvironmentBuilder().with_real_builtins().with_cadquery_objects() \
.add_entry("build_object", collector.build_object).build()
@ -75,7 +104,11 @@ class CQModel(object):
else:
raise NoOutputError("Script did not call build_object-- no output available.")
except Exception, ex:
print "Error Executing Script:"
result.set_failure_result(ex)
traceback.print_exc()
print "Full Text of Script:"
print self.script_source
end = time.clock()
result.buildTime = end - start
@ -93,6 +126,16 @@ class CQModel(object):
class BuildResult(object):
"""
The result of executing a CadQuery script.
The success property contains whether the exeuction was successful.
If successful, the results property contains a list of all results,
and the first_result property contains the first result.
If unsuccessful, the exception property contains a reference to
the stack trace that occurred.
"""
def __init__(self):
self.buildTime = None
self.results = []
@ -111,6 +154,10 @@ class BuildResult(object):
class ScriptMetadata(object):
"""
Defines the metadata for a parsed CQ Script.
the parameters property is a dict of InputParameter objects.
"""
def __init__(self):
self.parameters = {}
@ -135,12 +182,37 @@ class BooleanParameterType(ParameterType):
class InputParameter:
"""
Defines a parameter that can be supplied when the model is executed.
Name, varType, and default_value are always available, because they are computed
from a variable assignment line of code:
The others are only available if the script has used define_parameter() to
provide additional metadata
"""
def __init__(self):
self.name = None
self.shortDesc = None
self.varType = None
self.valid_values = []
#: the default value for the variable.
self.default_value = None
#: the name of the parameter.
self.name = None
#: type of the variable: BooleanParameter, StringParameter, NumericParameter
self.varType = None
#: help text describing the variable. Only available if the script used describe_parameter()
self.shortDesc = None
#: valid values for the variable. Only available if the script used describe_parameter()
self.valid_values = []
self.ast_node = None
@staticmethod
@ -181,9 +253,9 @@ class InputParameter:
self.ast_node.s = str(new_value)
elif self.varType == BooleanParameterType:
if new_value:
self.ast_node.value.id = 'True'
self.ast_node.id = 'True'
else:
self.ast_node.value.id = 'False'
self.ast_node.id = 'False'
else:
raise ValueError("Unknown Type of var: ", str(self.varType))
@ -192,56 +264,54 @@ class InputParameter:
self.name, str(self.varType), str(self.default_value))
class BuildObjectCollector(object):
class ScriptCallback(object):
"""
Allows a script to provide output objects
Allows a script to communicate with the container
the build_object() method is exposed to CQ scripts, to allow them
to return objects to the execution environment
"""
def __init__(self):
self.outputObjects = []
def build_object(self, shape):
"""
return an object to the executing environment
:param shape: a cadquery object
"""
self.outputObjects.append(shape)
def describe_parameter(self,var, valid_values, short_desc):
"""
Not yet implemented: allows a script to document
extra metadata about the parameters
"""
pass
def add_error(self, param, field_list):
"""
Not implemented yet: allows scripts to indicate that there are problems with inputs
"""
pass
def has_results(self):
return len(self.outputObjects) > 0
class ScriptExecutor(object):
"""
executes a script in a given environment.
"""
def __init__(self, environment, ast_tree):
try:
exec ast_tree in environment
except Exception, ex:
# an error here means there was a problem compiling the script
# try to figure out what line the error was on
traceback.print_exc()
formatted_lines = traceback.format_exc().splitlines()
line_text = ""
for f in formatted_lines:
if f.find(CQSCRIPT) > -1:
m = re.search("line\\s+(\\d+)", f, re.IGNORECASE)
if m and m.group(1):
line_text = m.group(1)
else:
line_text = 0
sse = ScriptExecutionError()
sse.line = int(line_text)
sse.message = str(ex)
raise sse
class InvalidParameterError(Exception):
"""
Raised when an attempt is made to provide a new parameter value
that cannot be assigned to the model
"""
pass
class NoOutputError(Exception):
"""
Raised when the script does not execute the build_output() method to
return a solid
"""
pass
@ -274,6 +344,11 @@ class ScriptExecutionError(Exception):
class EnvironmentBuilder(object):
"""
Builds an execution environment for a cadquery script.
The environment includes the builtins, as well as
the other methods the script will need.
"""
def __init__(self):
self.env = {}
@ -286,6 +361,7 @@ class EnvironmentBuilder(object):
def with_cadquery_objects(self):
self.env['cadquery'] = cadquery
self.env['cq'] = cadquery
return self
def add_entry(self, name, value):
@ -305,26 +381,45 @@ class ConstantAssignmentFinder(ast.NodeTransformer):
self.cqModel = cq_model
def handle_assignment(self, var_name, value_node):
if type(value_node) == ast.Num:
self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, NumberParameterType, value_node.n))
elif type(value_node) == ast.Str:
self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, StringParameterType, value_node.s))
elif type(value_node == ast.Name):
if value_node.value.Id == 'True':
try:
if type(value_node) == ast.Num:
self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, BooleanParameterType, True))
elif value_node.value.Id == 'False':
InputParameter.create(value_node, var_name, NumberParameterType, value_node.n))
elif type(value_node) == ast.Str:
self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, BooleanParameterType, True))
InputParameter.create(value_node, var_name, StringParameterType, value_node.s))
elif type(value_node == ast.Name):
if value_node.id == 'True':
self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, BooleanParameterType, True))
elif value_node.id == 'False':
self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, BooleanParameterType, True))
except:
print "Unable to handle assignment for variable '%s'" % var_name
pass
def visit_Assign(self, node):
left_side = node.targets[0]
if type(node.value) in [ast.Num, ast.Str, ast.Name]:
self.handle_assignment(left_side.id, node.value)
elif type(node.value) == ast.Tuple:
# we have a multi-value assignment
for n, v in zip(left_side.elts, node.value.elts):
self.handle_assignment(n.id, v)
try:
left_side = node.targets[0]
#do not handle attribute assignments
if isinstance(left_side,ast.Attribute):
return
if type(node.value) in [ast.Num, ast.Str, ast.Name]:
self.handle_assignment(left_side.id, node.value)
elif type(node.value) == ast.Tuple:
# we have a multi-value assignment
for n, v in zip(left_side.elts, node.value.elts):
self.handle_assignment(n.id, v)
except:
traceback.print_exc()
print "Unable to handle assignment for node '%s'" % ast.dump(left_side)
return node

View File

@ -32,9 +32,11 @@ def sortWiresByBuildOrder(wireList, plane, result=[]):
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 = []
@ -56,7 +58,7 @@ def sortWiresByBuildOrder(wireList, plane, result=[]):
class Vector(object):
"""Create a 3-dimensional vector
:param *args: a 3-d vector, with x-y-z parts.
:param args: a 3-d vector, with x-y-z parts.
you can either provide:
* nothing (in which case the null vector is return)
@ -375,9 +377,10 @@ class Plane(object):
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)
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

View File

@ -433,7 +433,7 @@ class Edge(Shape):
def tangentAt(self, locationVector=None):
"""
Compute tangent vector at the specified location.
Compute tangent vector at the specified location.
:param locationVector: location to use. Use the center point if None
:return: tangent vector
"""
@ -668,16 +668,16 @@ class Solid(Shape):
@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)'
makeBox(length,width,height,[pnt,dir]) -- Make a box located in pnt with the dimensions (length,width,height)
By 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),
Make a cone with given radii and height
By 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))
@ -727,10 +727,8 @@ class Solid(Shape):
@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)'
Make a wedge located in pnt
By default pnt=Vector(0,0,0) and dir=Vector(0,0,1)
"""
return Shape.cast(
FreeCADPart.makeWedge(xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt, dir))
@ -738,9 +736,8 @@ class Solid(Shape):
@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'
Make a sphere with a given radius
By 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))
@ -752,11 +749,11 @@ class Solid(Shape):
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
At a high level, the steps followed are:
(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
(4) 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

View File

@ -10,7 +10,7 @@ This page documents all of the methods and functions of the CadQuery classes, or
For a listing organized by functional area, see the :ref:`apireference`
.. module:: cadquery
.. currentmodule:: cadquery
Core Classes
---------------------

125
doc/cqgi.rst Normal file
View File

@ -0,0 +1,125 @@
.. _cqgi:
The CadQuery Gateway Interface
====================================
CadQuery is first and foremost designed as a library, which can be used as a part of any project.
In this context, there is no need for a standard script format or gateway api.
Though the embedded use case is the most common, several tools have been created which run
cadquery scripts on behalf of the user, and then render the result of the script visually.
These execution environments (EE) generally accept a script and user input values for
script parameters, and then display the resulting objects visually to the user.
Today, three execution environments exist:
* `The CadQuery Freecad Module <https://github.com/jmwright/cadquery-freecad-module>`_, which runs scripts
inside of the FreeCAD IDE, and displays objects in the display window
* the cq-directive, which is used to execute scripts inside of sphinx-doc,
producing documented examples that include both a script and an SVG representation of the object that results
* `ParametricParts.com <https://www.parametricparts.com>`_, which provides a web-based way to prompt user input for
variables, and then display the result output in a web page.
The CQGI is distributed with cadquery, and standardizes the interface between execution environments and cadquery scripts.
The Script Side
-----------------
CQGI compliant containers provide an execution environment for scripts. The environment includes:
* the cadquery library is automatically imported as 'cq'.
* the :py:meth:`cadquery.cqgi.ScriptCallback.build_object()` method is defined that should be used to export a shape to the execution environment
Scripts must call build_output at least once. Invoking build_object more than once will send multiple objects to
the container. An error will occur if the script does not return an object using the build_object() method.
Future enhancements will include several other methods, used to provide more metadata for the execution environment:
* :py:meth:`cadquery.cqgi.ScriptCallback.add_error()`, indicates an error with an input parameter
* :py:meth:`cadquery.cqgi.ScriptCallback.describe_parameter()`, provides extra information about a parameter in the script,
The execution environment side
-------------------------------
CQGI makes it easy to run cadquery scripts in a standard way. To run a script from an execution environment,
run code like this::
from cadquery import cqgi
user_script = ...
build_result = cqgi.parse(user_script).build()
The :py:meth:`cadquery.cqgi.parse()` method returns a :py:class:`cadquery.cqgi.CQModel` object.
Calling :py:meth:`cadquery.cqgi.CQModel.build()` returns a :py:class:`cadquery.cqgi.BuildResult` object,
,which includes the script execution time, and a success flag.
If the script was successful, the results property will include a list of results returned by the script.
If the script failed, the exception property contains the exception object.
If your have a way to get inputs from a user, you can override any of the constants defined in the user script
with new values::
from cadquery import cqgi
user_script = ...
build_result = cqgi.parse(user_script).build({ 'param': 2 } )
If a parameter called 'param' is defined in the model, it will be assigned the value 2 before the script runs.
An error will occur if a value is provided that is not defined in the model, or if the value provided cannot
be assigned to a variable with the given name.
More about script variables
-----------------------------
CQGI uses the following rules to find input variables for a script:
* only top-level statements are considered
* only assignments of constant values to a local name are considered.
For example, in the following script::
h = 1.0
w = 2.0
foo = 'bar'
def some_function():
x = 1
h, w, and foo will be overridable script variables, but x is not.
You can list the variables defined in the model by using the return value of the parse method::
model = cqgi.parse(user_script)
//a dictionary of InputParameter objects
parameters = model.metadata.parameters
The key of the dictionary is a string , and the value is a :py:class:`cadquery.cqgi.InputParameter` object
See the CQGI API docs for more details.
Future enhancments will include a safer sandbox to prevent malicious scripts.
Important CQGI Methods
-------------------------
These are the most important Methods and classes of the CQGI
.. currentmodule:: cadquery.cqgi
.. autosummary::
parse
CQModel.build
BuildResult
ScriptCallback.build_object
Complete CQGI api
-----------------
.. automodule:: cadquery.cqgi
:members:

View File

@ -1,6 +1,5 @@
.. _designprinciples:
.. automodule:: cadquery
===========================
CadQuery Design Principles

View File

@ -4,7 +4,6 @@
CadQuery Examples
*********************************
.. automodule:: cadquery
The examples on this page can help you learn how to build objects with CadQuery.
@ -64,9 +63,18 @@ of a working plane is at the center of the face. The default hole depth is thro
.. cq_plot::
result = Workplane("front").box(2.0,2.0,0.5).faces(">Z").hole(0.5)
build_output(result)
# The dimensions of the box. These can be modified rather than changing the
# object's code directly.
length = 80.0
height = 60.0
thickness = 10.0
center_hole_dia = 22.0
# Create a box based on the dimensions above and add a 22mm center hole
result = cq.Workplane("XY").box(length, height, thickness) \
.faces(">Z").workplane().hole(center_hole_dia)
build_object(result)
.. topic:: Api References
@ -88,7 +96,8 @@ By default, rectangles and circles are centered around the previous working poin
.. cq_plot::
result = Workplane("front").circle(2.0).rect(0.5,0.75).extrude(0.5)
result = cq.Workplane("front").circle(2.0).rect(0.5,0.75).extrude(0.5)
build_object(result)
.. topic:: Api References
@ -112,8 +121,9 @@ closed curve.
.. cq_plot::
result = Workplane("front").lineTo(2.0,0).lineTo(2.0,1.0).threePointArc((1.0,1.5),(0.0,1.0))\
result = cq.Workplane("front").lineTo(2.0,0).lineTo(2.0,1.0).threePointArc((1.0,1.5),(0.0,1.0))\
.close().extrude(0.25)
build_object(result)
.. topic:: Api References
@ -138,13 +148,14 @@ A new work plane center can be established at any point.
.. cq_plot::
result = Workplane("front").circle(3.0) #current point is the center of the circle, at (0,0)
result = cq.Workplane("front").circle(3.0) #current point is the center of the circle, at (0,0)
result = result.center(1.5,0.0).rect(0.5,0.5) # new work center is (1.5,0.0)
result = result.center(-1.5,1.5).circle(0.25) # new work center is ( 0.0,1.5).
#the new center is specified relative to the previous center, not global coordinates!
result = result.extrude(0.25)
build_object(result)
.. topic:: Api References
@ -169,10 +180,11 @@ like :py:meth:`Workplane.circle` and :py:meth:`Workplane.rect`, will operate on
.. cq_plot::
r = Workplane("front").circle(2.0) # make base
r = cq.Workplane("front").circle(2.0) # make base
r = r.pushPoints( [ (1.5,0),(0,1.5),(-1.5,0),(0,-1.5) ] ) # now four points are on the stack
r = r.circle( 0.25 ) # circle will operate on all four points
result = r.extrude(0.125 ) # make prism
build_object(result)
.. topic:: Api References
@ -192,8 +204,9 @@ correct for small hole sizes.
.. cq_plot::
result = Workplane("front").box(3.0,4.0,0.25).pushPoints ( [ ( 0,0.75 ),(0,-0.75) ]) \
result = cq.Workplane("front").box(3.0,4.0,0.25).pushPoints ( [ ( 0,0.75 ),(0,-0.75) ]) \
.polygon(6,1.0).cutThruAll()
build_object(result)
.. topic:: Api References
@ -224,7 +237,8 @@ This example uses a polyline to create one half of an i-beam shape, which is mir
(W/2.0,H/-2.0),
(0,H/-2.0)
]
result = Workplane("front").polyline(pts).mirrorY().extrude(L)
result = cq.Workplane("front").polyline(pts).mirrorY().extrude(L)
build_object(result)
.. topic:: Api References
@ -246,7 +260,7 @@ needs a complex profile
.. cq_plot::
s = Workplane("XY")
s = cq.Workplane("XY")
sPnts = [
(2.75,1.5),
(2.5,1.75),
@ -258,6 +272,7 @@ needs a complex profile
]
r = s.lineTo(3.0,0).lineTo(3.0,1.0).spline(sPnts).close()
result = r.extrude(0.5)
build_object(result)
.. topic:: Api References
@ -279,9 +294,10 @@ introduce horizontal and vertical lines, which make for slightly easier coding.
.. cq_plot::
r = Workplane("front").hLine(1.0) # 1.0 is the distance, not coordinate
r = cq.Workplane("front").hLine(1.0) # 1.0 is the distance, not coordinate
r = r.vLine(0.5).hLine(-0.25).vLine(-0.25).hLineTo(0.0) # hLineTo allows using xCoordinate not distance
result =r.mirrorY().extrude(0.25 ) # mirror the geometry and extrude
build_object(result)
.. topic:: Api References
@ -315,8 +331,9 @@ Keep in mind that the origin of new workplanes are located at the center of a fa
.. cq_plot::
result = Workplane("front").box(2,3,0.5) #make a basic prism
result = cq.Workplane("front").box(2,3,0.5) #make a basic prism
result = result.faces(">Z").workplane().hole(0.5) #find the top-most face and make a hole
build_object(result)
.. topic:: Api References
@ -342,9 +359,10 @@ how deep the part is
.. cq_plot::
result = Workplane("front").box(3,2,0.5) #make a basic prism
result = cq.Workplane("front").box(3,2,0.5) #make a basic prism
result = result.faces(">Z").vertices("<XY").workplane() #select the lower left vertex and make a workplane
result = result.circle(1.0).cutThruAll() #cut the corner out
build_object(result)
.. topic:: Api References
@ -369,9 +387,10 @@ This example uses an offset workplane to make a compound object, which is perfec
.. cq_plot::
result = Workplane("front").box(3,2,0.5) #make a basic prism
result = cq.Workplane("front").box(3,2,0.5) #make a basic prism
result = result.faces("<X").workplane(offset=0.75) #workplane is offset from the object surface
result = result.circle(1.0).extrude(0.5) #disc
build_object(result)
.. topic:: Api References
@ -390,9 +409,10 @@ You can create a rotated work plane by specifying angles of rotation relative to
.. cq_plot::
result = Workplane("front").box(4.0,4.0,0.25).faces(">Z").workplane() \
.transformed(offset=Vector(0,-1.5,1.0),rotate=Vector(60,0,0)) \
result = cq.Workplane("front").box(4.0,4.0,0.25).faces(">Z").workplane() \
.transformed(offset=cq.Vector(0,-1.5,1.0),rotate=cq.Vector(60,0,0)) \
.rect(1.5,1.5,forConstruction=True).vertices().hole(0.25)
build_object(result)
.. topic:: Api References
@ -414,8 +434,9 @@ In the example below, a rectangle is drawn, and its vertices are used to locate
.. cq_plot::
result = Workplane("front").box(2,2,0.5).faces(">Z").workplane() \
result = cq.Workplane("front").box(2,2,0.5).faces(">Z").workplane() \
.rect(1.5,1.5,forConstruction=True).vertices().hole(0.125 )
build_object(result)
.. topic:: Api References
@ -438,7 +459,8 @@ are removed, and then the inside of the solid is 'hollowed out' to make the shel
.. cq_plot::
result = Workplane("front").box(2,2,2).faces("+Z").shell(0.05)
result = cq.Workplane("front").box(2,2,2).faces("+Z").shell(0.05)
build_object(result)
.. topic:: Api References
@ -458,9 +480,11 @@ and a circular section.
.. cq_plot::
result = Workplane("front").box(4.0,4.0,0.25).faces(">Z").circle(1.5) \
result = cq.Workplane("front").box(4.0,4.0,0.25).faces(">Z").circle(1.5) \
.workplane(offset=3.0).rect(0.75,0.5).loft(combine=True)
build_object(result)
.. topic:: Api References
.. hlist::
@ -481,9 +505,11 @@ Similar to :py:meth:`Workplane.hole` , these functions operate on a list of poin
.. cq_plot::
result = Workplane(Plane.XY()).box(4,2,0.5).faces(">Z").workplane().rect(3.5,1.5,forConstruction=True)\
result = cq.Workplane(cq.Plane.XY()).box(4,2,0.5).faces(">Z").workplane().rect(3.5,1.5,forConstruction=True)\
.vertices().cboreHole(0.125, 0.25,0.125,depth=None)
build_object(result)
.. topic:: Api References
.. hlist::
@ -507,7 +533,8 @@ Here we fillet all of the edges of a simple plate.
.. cq_plot::
result = Workplane("XY" ).box(3,3,0.5).edges("|Z").fillet(0.125)
result = cq.Workplane("XY" ).box(3,3,0.5).edges("|Z").fillet(0.125)
build_object(result)
.. topic:: Api References
@ -529,12 +556,12 @@ with just a few lines of code.
(length,height,bearing_diam, thickness,padding) = ( 30.0,40.0,22.0,10.0,8.0)
result = Workplane("XY").box(length,height,thickness).faces(">Z").workplane().hole(bearing_diam) \
result = cq.Workplane("XY").box(length,height,thickness).faces(">Z").workplane().hole(bearing_diam) \
.faces(">Z").workplane() \
.rect(length-padding,height-padding,forConstruction=True) \
.vertices().cboreHole(2.4,4.4,2.1)
build_output(result)
build_object(result)
Splitting an Object
@ -544,10 +571,11 @@ You can split an object using a workplane, and retain either or both halves
.. cq_plot::
c = Workplane("XY").box(1,1,1).faces(">Z").workplane().circle(0.25).cutThruAll()
c = cq.Workplane("XY").box(1,1,1).faces(">Z").workplane().circle(0.25).cutThruAll()
#now cut it in half sideways
result = c.faces(">Y").workplane(-0.5).split(keepTop=True)
build_object(result)
.. topic:: Api References
@ -577,7 +605,7 @@ ones at 13 lines, but that's very short compared to the pythonOCC version, which
.. cq_plot::
(L,w,t) = (20.0,6.0,3.0)
s = Workplane("XY")
s = cq.Workplane("XY")
#draw half the profile of the bottle and extrude it
p = s.center(-L/2.0,0).vLine(w/2.0) \
@ -589,6 +617,7 @@ ones at 13 lines, but that's very short compared to the pythonOCC version, which
#make a shell
result = p.faces(">Z").shell(0.3)
build_object(result)
.. topic:: Api References
@ -631,7 +660,7 @@ A Parametric Enclosure
p_lipHeight = 1.0 #Height of lip on the underside of the lid.\nSits inside the box body for a snug fit.
#outer shell
oshell = Workplane("XY").rect(p_outerWidth,p_outerLength).extrude(p_outerHeight + p_lipHeight)
oshell = cq.Workplane("XY").rect(p_outerWidth,p_outerLength).extrude(p_outerHeight + p_lipHeight)
#weird geometry happens if we make the fillets in the wrong order
if p_sideRadius > p_topAndBottomRadius:
@ -687,6 +716,8 @@ A Parametric Enclosure
#return the combined result
result =topOfLid.combineSolids(bottom)
build_object(result)
.. topic:: Api References
.. hlist::
@ -709,9 +740,299 @@ A Parametric Enclosure
* :py:meth:`Workplane.cskHole`
* :py:meth:`Workplane.hole`
Lego Brick
-------------------
More Examples available in cadquery-freecad-modules
----------------------------------------------------
This script will produce any size regular rectangular Lego(TM) brick. Its only tricky because of the logic
regarding the underside of the brick.
If you have installed the `cadquery-freecad-module <https://github.com/jmwright/cadquery-freecad-module>`_,
there are > 20 examples available that you can interact with
.. cq_plot::
:height: 400
#####
# Inputs
######
lbumps = 6 # number of bumps long
wbumps = 2 # number of bumps wide
thin = True # True for thin, False for thick
#
# Lego Brick Constants-- these make a lego brick a lego :)
#
pitch = 8.0
clearance = 0.1
bumpDiam = 4.8
bumpHeight = 1.8
if thin:
height = 3.2
else:
height = 9.6
t = (pitch - (2 * clearance) - bumpDiam) / 2.0
postDiam = pitch - t # works out to 6.5
total_length = lbumps*pitch - 2.0*clearance
total_width = wbumps*pitch - 2.0*clearance
# make the base
s = cq.Workplane("XY").box(total_length, total_width, height)
# shell inwards not outwards
s = s.faces("<Z").shell(-1.0 * t)
# make the bumps on the top
s = s.faces(">Z").workplane(). \
rarray(pitch, pitch, lbumps, wbumps, True).circle(bumpDiam / 2.0) \
.extrude(bumpHeight)
# add posts on the bottom. posts are different diameter depending on geometry
# solid studs for 1 bump, tubes for multiple, none for 1x1
tmp = s.faces("<Z").workplane(invert=True)
if lbumps > 1 and wbumps > 1:
tmp = tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True). \
circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t)
elif lbumps > 1:
tmp = tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True). \
circle(t).extrude(height - t)
elif wbumps > 1:
tmp = tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True). \
circle(t).extrude(height - t)
else:
tmp = s
# Render the solid
build_object(tmp)
Braille Example
---------------------
.. cq_plot::
:height: 400
from __future__ import unicode_literals, division
from collections import namedtuple
# text_lines is a list of text lines.
# FreeCAD in braille (converted with braille-converter:
# https://github.com/jpaugh/braille-converter.git).
text_lines = ['⠠ ⠋ ⠗ ⠑ ⠑ ⠠ ⠉ ⠠ ⠁ ⠠ ⠙']
# See http://www.tiresias.org/research/reports/braille_cell.htm for examples
# of braille cell geometry.
horizontal_interdot = 2.5
vertical_interdot = 2.5
horizontal_intercell = 6
vertical_interline = 10
dot_height = 0.5
dot_diameter = 1.3
base_thickness = 1.5
# End of configuration.
BrailleCellGeometry = namedtuple('BrailleCellGeometry',
('horizontal_interdot',
'vertical_interdot',
'intercell',
'interline',
'dot_height',
'dot_diameter'))
class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __len__(self):
return 2
def __getitem__(self, index):
return (self.x, self.y)[index]
def __str__(self):
return '({}, {})'.format(self.x, self.y)
def brailleToPoints(text, cell_geometry):
# Unicode bit pattern (cf. https://en.wikipedia.org/wiki/Braille_Patterns).
mask1 = 0b00000001
mask2 = 0b00000010
mask3 = 0b00000100
mask4 = 0b00001000
mask5 = 0b00010000
mask6 = 0b00100000
mask7 = 0b01000000
mask8 = 0b10000000
masks = (mask1, mask2, mask3, mask4, mask5, mask6, mask7, mask8)
# Corresponding dot position
w = cell_geometry.horizontal_interdot
h = cell_geometry.vertical_interdot
pos1 = Point(0, 2 * h)
pos2 = Point(0, h)
pos3 = Point(0, 0)
pos4 = Point(w, 2 * h)
pos5 = Point(w, h)
pos6 = Point(w, 0)
pos7 = Point(0, -h)
pos8 = Point(w, -h)
pos = (pos1, pos2, pos3, pos4, pos5, pos6, pos7, pos8)
# Braille blank pattern (u'\u2800').
blank = ''
points = []
# Position of dot1 along the x-axis (horizontal).
character_origin = 0
for c in text:
for m, p in zip(masks, pos):
delta_to_blank = ord(c) - ord(blank)
if (m & delta_to_blank):
points.append(p + Point(character_origin, 0))
character_origin += cell_geometry.intercell
return points
def get_plate_height(text_lines, cell_geometry):
# cell_geometry.vertical_interdot is also used as space between base
# borders and characters.
return (2 * cell_geometry.vertical_interdot +
2 * cell_geometry.vertical_interdot +
(len(text_lines) - 1) * cell_geometry.interline)
def get_plate_width(text_lines, cell_geometry):
# cell_geometry.horizontal_interdot is also used as space between base
# borders and characters.
max_len = max([len(t) for t in text_lines])
return (2 * cell_geometry.horizontal_interdot +
cell_geometry.horizontal_interdot +
(max_len - 1) * cell_geometry.intercell)
def get_cylinder_radius(cell_geometry):
"""Return the radius the cylinder should have
The cylinder have the same radius as the half-sphere make the dots (the
hidden and the shown part of the dots).
The radius is such that the spherical cap with diameter
cell_geometry.dot_diameter has a height of cell_geometry.dot_height.
"""
h = cell_geometry.dot_height
r = cell_geometry.dot_diameter / 2
return (r ** 2 + h ** 2) / 2 / h
def get_base_plate_thickness(plate_thickness, cell_geometry):
"""Return the height on which the half spheres will sit"""
return (plate_thickness +
get_cylinder_radius(cell_geometry) -
cell_geometry.dot_height)
def make_base(text_lines, cell_geometry, plate_thickness):
base_width = get_plate_width(text_lines, cell_geometry)
base_height = get_plate_height(text_lines, cell_geometry)
base_thickness = get_base_plate_thickness(plate_thickness, cell_geometry)
base = cq.Workplane('XY').box(base_width, base_height, base_thickness,
centered=(False, False, False))
return base
def make_embossed_plate(text_lines, cell_geometry):
"""Make an embossed plate with dots as spherical caps
Method:
- make a thin plate on which sit cylinders
- fillet the upper edge of the cylinders so to get pseudo half-spheres
- make the union with a thicker plate so that only the sphere caps stay
"visible".
"""
base = make_base(text_lines, cell_geometry, base_thickness)
dot_pos = []
base_width = get_plate_width(text_lines, cell_geometry)
base_height = get_plate_height(text_lines, cell_geometry)
y = base_height - 3 * cell_geometry.vertical_interdot
line_start_pos = Point(cell_geometry.horizontal_interdot, y)
for text in text_lines:
dots = brailleToPoints(text, cell_geometry)
dots = [p + line_start_pos for p in dots]
dot_pos += dots
line_start_pos += Point(0, -cell_geometry.interline)
r = get_cylinder_radius(cell_geometry)
base = base.faces('>Z').vertices('<XY').workplane() \
.pushPoints(dot_pos).circle(r) \
.extrude(r)
# Make a fillet almost the same radius to get a pseudo spherical cap.
base = base.faces('>Z').edges() \
.fillet(r - 0.001)
hidding_box = cq.Workplane('XY').box(
base_width, base_height, base_thickness, centered=(False, False, False))
result = hidding_box.union(base)
return result
_cell_geometry = BrailleCellGeometry(
horizontal_interdot,
vertical_interdot,
horizontal_intercell,
vertical_interline,
dot_height,
dot_diameter)
if base_thickness < get_cylinder_radius(_cell_geometry):
raise ValueError('Base thickness should be at least {}'.format(dot_height))
build_object(make_embossed_plate(text_lines, _cell_geometry))
Panel With Various Connector Holes
-----------------------------------
.. cq_plot::
:height: 400
# The dimensions of the model. These can be modified rather than changing the
# object's code directly.
width = 400
height = 500
thickness = 2
# Create a plate with two polygons cut through it
result = cq.Workplane("front").box(width, height, thickness)
h_sep = 60
for idx in range(4):
result = result.workplane(offset=1, centerOption='CenterOfBoundBox').center(157,210-idx*h_sep).moveTo(-23.5,0).circle(1.6).moveTo(23.5,0).circle(1.6).moveTo(-17.038896,-5.7).threePointArc((-19.44306,-4.70416),(-20.438896,-2.3)).lineTo(-21.25,2.3).threePointArc((-20.25416,4.70416),(-17.85,5.7)).lineTo(17.85,5.7).threePointArc((20.25416,4.70416),(21.25,2.3)).lineTo(20.438896,-2.3).threePointArc((19.44306,-4.70416),(17.038896,-5.7)).close().cutThruAll()
for idx in range(4):
result = result.workplane(offset=1, centerOption='CenterOfBoundBox').center(157,-30-idx*h_sep).moveTo(-16.65,0).circle(1.6).moveTo(16.65,0).circle(1.6).moveTo(-10.1889,-5.7).threePointArc((-12.59306,-4.70416),(-13.5889,-2.3)).lineTo(-14.4,2.3).threePointArc((-13.40416,4.70416),(-11,5.7)).lineTo(11,5.7).threePointArc((13.40416,4.70416),(14.4,2.3)).lineTo(13.5889,-2.3).threePointArc((12.59306,-4.70416),(10.1889,-5.7)).close().cutThruAll()
h_sep4DB9 = 30
for idx in range(8):
result = result.workplane(offset=1, centerOption='CenterOfBoundBox').center(91,225-idx*h_sep4DB9).moveTo(-12.5,0).circle(1.6).moveTo(12.5,0).circle(1.6).moveTo(-6.038896,-5.7).threePointArc((-8.44306,-4.70416),(-9.438896,-2.3)).lineTo(-10.25,2.3).threePointArc((-9.25416,4.70416),(-6.85,5.7)).lineTo(6.85,5.7).threePointArc((9.25416,4.70416),(10.25,2.3)).lineTo(9.438896,-2.3).threePointArc((8.44306,-4.70416),(6.038896,-5.7)).close().cutThruAll()
for idx in range(4):
result = result.workplane(offset=1, centerOption='CenterOfBoundBox').center(25,210-idx*h_sep).moveTo(-23.5,0).circle(1.6).moveTo(23.5,0).circle(1.6).moveTo(-17.038896,-5.7).threePointArc((-19.44306,-4.70416),(-20.438896,-2.3)).lineTo(-21.25,2.3).threePointArc((-20.25416,4.70416),(-17.85,5.7)).lineTo(17.85,5.7).threePointArc((20.25416,4.70416),(21.25,2.3)).lineTo(20.438896,-2.3).threePointArc((19.44306,-4.70416),(17.038896,-5.7)).close().cutThruAll()
for idx in range(4):
result = result.workplane(offset=1, centerOption='CenterOfBoundBox').center(25,-30-idx*h_sep).moveTo(-16.65,0).circle(1.6).moveTo(16.65,0).circle(1.6).moveTo(-10.1889,-5.7).threePointArc((-12.59306,-4.70416),(-13.5889,-2.3)).lineTo(-14.4,2.3).threePointArc((-13.40416,4.70416),(-11,5.7)).lineTo(11,5.7).threePointArc((13.40416,4.70416),(14.4,2.3)).lineTo(13.5889,-2.3).threePointArc((12.59306,-4.70416),(10.1889,-5.7)).close().cutThruAll()
for idx in range(8):
result = result.workplane(offset=1, centerOption='CenterOfBoundBox').center(-41,225-idx*h_sep4DB9).moveTo(-12.5,0).circle(1.6).moveTo(12.5,0).circle(1.6).moveTo(-6.038896,-5.7).threePointArc((-8.44306,-4.70416),(-9.438896,-2.3)).lineTo(-10.25,2.3).threePointArc((-9.25416,4.70416),(-6.85,5.7)).lineTo(6.85,5.7).threePointArc((9.25416,4.70416),(10.25,2.3)).lineTo(9.438896,-2.3).threePointArc((8.44306,-4.70416),(6.038896,-5.7)).close().cutThruAll()
for idx in range(4):
result = result.workplane(offset=1, centerOption='CenterOfBoundBox').center(-107,210-idx*h_sep).moveTo(-23.5,0).circle(1.6).moveTo(23.5,0).circle(1.6).moveTo(-17.038896,-5.7).threePointArc((-19.44306,-4.70416),(-20.438896,-2.3)).lineTo(-21.25,2.3).threePointArc((-20.25416,4.70416),(-17.85,5.7)).lineTo(17.85,5.7).threePointArc((20.25416,4.70416),(21.25,2.3)).lineTo(20.438896,-2.3).threePointArc((19.44306,-4.70416),(17.038896,-5.7)).close().cutThruAll()
for idx in range(4):
result = result.workplane(offset=1, centerOption='CenterOfBoundBox').center(-107,-30-idx*h_sep).circle(14).rect(24.7487,24.7487, forConstruction=True).vertices().hole(3.2).cutThruAll()
for idx in range(8):
result = result.workplane(offset=1, centerOption='CenterOfBoundBox').center(-173,225-idx*h_sep4DB9).moveTo(-12.5,0).circle(1.6).moveTo(12.5,0).circle(1.6).moveTo(-6.038896,-5.7).threePointArc((-8.44306,-4.70416),(-9.438896,-2.3)).lineTo(-10.25,2.3).threePointArc((-9.25416,4.70416),(-6.85,5.7)).lineTo(6.85,5.7).threePointArc((9.25416,4.70416),(10.25,2.3)).lineTo(9.438896,-2.3).threePointArc((8.44306,-4.70416),(6.038896,-5.7)).close().cutThruAll()
for idx in range(4):
result = result.workplane(offset=1, centerOption='CenterOfBoundBox').center(-173,-30-idx*h_sep).moveTo(-2.9176,-5.3).threePointArc((-6.05,0),(-2.9176,5.3)).lineTo(2.9176,5.3).threePointArc((6.05,0),(2.9176,-5.3)).close().cutThruAll()
# Render the solid
build_object(result)

View File

@ -3,7 +3,6 @@
Extending CadQuery
======================
.. module:: cadquery
If you find that CadQuery doesnt suit your needs, you can easily extend it. CadQuery provides several extension
methods:
@ -19,7 +18,7 @@ Using FreeCAD Script
The easiest way to extend CadQuery is to simply use FreeCAD script inside of your build method. Just about
any valid FreeCAD script will execute just fine. For example, this simple CadQuery script::
return Workplane("XY").box(1.0,2.0,3.0).val()
return cq.Workplane("XY").box(1.0,2.0,3.0).val()
is actually equivalent to::
@ -40,7 +39,7 @@ a lot of the complexity of the FreeCAD api.
You can get the best of both worlds by wrapping your freecad script into a CadQuery plugin.
A CadQuery plugin is simply a function that is attached to the CadQuery :py:meth:`cadquery.CQ.CQ` or :py:meth:`cadquery.CQ.Workplane` class.
A CadQuery plugin is simply a function that is attached to the CadQuery :py:meth:`cadquery.CQ` or :py:meth:`cadquery.Workplane` class.
When connected, your plugin can be used in the chain just like the built-in functions.
There are a few key concepts important to understand when building a plugin
@ -68,14 +67,14 @@ Preserving the Chain
CadQuery's fluent api relies on the ability to chain calls together one after another. For this to work,
you must return a valid CadQuery object as a return value. If you choose not to return a CadQuery object,
then your plugin will end the chain. Sometimes this is desired for example :py:meth:`cadquery.CQ.CQ.size`
then your plugin will end the chain. Sometimes this is desired for example :py:meth:`cadquery.CQ.size`
There are two ways you can safely continue the chain:
1. **return self** If you simply wish to modify the stack contents, you can simply return a reference to
self. This approach is destructive, because the contents of the stack are modified, but it is also the
simplest.
2. :py:meth:`cadquery.CQ.CQ.newObject` Most of the time, you will want to return a new object. Using newObject will
2. :py:meth:`cadquery.CQ.newObject` Most of the time, you will want to return a new object. Using newObject will
return a new CQ or Workplane object having the stack you specify, and will link this object to the
previous one. This preserves the original object and its stack.
@ -87,29 +86,26 @@ When you implement a CadQuery plugin, you are extending CadQuery's base objects.
CadQuery or Workplane methods from inside of your extension. You can also call a number of internal methods that
are designed to aid in plugin creation:
* :py:meth:`cadquery.CQ.Workplane._pointsOnStack` returns a FreeCAD Vector ( a point ) for each item on the stack. Useful if you
are writing a plugin that you'd like to operate on all values on the stack, like :py:meth:`cadquery.CQ.Workplane.circle` and
most other built-ins do
* :py:meth:`cadquery.CQ.Workplane._makeWireAtPoints` will invoke a factory function you supply for all points on the stack,
* :py:meth:`cadquery.Workplane._makeWireAtPoints` will invoke a factory function you supply for all points on the stack,
and return a properly constructed cadquery object. This function takes care of registering wires for you
and everything like that
* :py:meth:`cadquery.CQ.Workplane.newObject` returns a new Workplane object with the provided stack, and with its parent set
* :py:meth:`cadquery.Workplane.newObject` returns a new Workplane object with the provided stack, and with its parent set
to the current object. The preferred way to continue the chain
* :py:meth:`cadquery.CQ.Workplane.findSolid` returns the first Solid found in the chain, working from the current object upwards
* :py:meth:`cadquery.CQ.findSolid` returns the first Solid found in the chain, working from the current object upwards
in the chain. commonly used when your plugin will modify an existing solid, or needs to create objects and
then combine them onto the 'main' part that is in progress
* :py:meth:`cadquery.CQ.Workplane._addWire` must be called if you add a wire. This allows the base class to track all the wires
* :py:meth:`cadquery.Workplane._addPendingWire` must be called if you add a wire. This allows the base class to track all the wires
that are created, so that they can be managed when extrusion occurs.
* :py:meth:`cadquery.CQ.Workplane.wire` gathers up all of the edges that have been drawn ( eg, by line, vline, etc ), and
* :py:meth:`cadquery.Workplane.wire` gathers up all of the edges that have been drawn ( eg, by line, vline, etc ), and
attempts to combine them into a single wire, which is returned. This should be used when your plugin creates
2-d edges, and you know it is time to collect them into a single wire.
* :py:meth:`cadquery.CQ.Workplane.plane` provides a reference to the workplane, which allows you to convert between workplane
* :py:meth:`cadquery.Workplane.plane` provides a reference to the workplane, which allows you to convert between workplane
coordinates and global coordinates:
* :py:meth:`cadquery.freecad_impl.geom.Plane.toWorldCoords` will convert local coordinates to global ones
* :py:meth:`cadquery.freecad_impl.geom.Plane.toLocalCoords` will convet from global coordinates to local coordinates
@ -138,7 +134,7 @@ To install it, simply attach it to the CadQuery or Workplane object, like this::
do stuff
return whatever_you_want
Workplane.yourPlugin = _yourFunction
cq.Workplane.yourPlugin = _yourFunction
That's it!
@ -147,8 +143,8 @@ CadQueryExample Plugins
Some core cadquery code is intentionally written exactly like a plugin.
If you are writing your own plugins, have a look at these methods for inspiration:
* :py:meth:`cadquery.CQ.Workplane.polygon`
* :py:meth:`cadquery.CQ.Workplane.cboreHole`
* :py:meth:`cadquery.Workplane.polygon`
* :py:meth:`cadquery.Workplane.cboreHole`
Plugin Example
@ -167,18 +163,18 @@ This ultra simple plugin makes cubes of the specified size for each stack point.
def _singleCube(pnt):
#pnt is a location in local coordinates
#since we're using eachpoint with useLocalCoordinates=True
return Solid.makeBox(length,length,length,pnt)
return cq.Solid.makeBox(length,length,length,pnt)
#use CQ utility method to iterate over the stack, call our
#method, and convert to/from local coordinates.
return self.eachpoint(_singleCube,True)
#link the plugin into cadQuery
Workplane.makeCubes = makeCubes
cq.Workplane.makeCubes = makeCubes
#use the plugin
result = Workplane("XY").box(6.0,8.0,0.5).faces(">Z")\
result = cq.Workplane("XY").box(6.0,8.0,0.5).faces(">Z")\
.rect(4.0,4.0,forConstruction=True).vertices() \
.makeCubes(1.0).combineSolids()
build_object(result)

View File

@ -3,40 +3,23 @@
CadQuery Scripts and Object Output
======================================
CadQuery scripts are pure python scripts that follow a standard format.
CadQuery scripts are pure python scripts, that may follow a few conventions.
If you are using cadquery as a library, there are no constraints.
If you are using cadquery scripts inside of a caduquer execution environment,
like `The CadQuery Freecad Module <https://github.com/jmwright/cadquery-freecad-module>`_ or
`parametricParts.com <https://www.parametricparts.com>`_, there are a few conventions you need to be aware of:
* cadquery is already imported as 'cq'
* to return an object to the container, you need to call the build_object() method.
See the
Each script generally has three sections:
* Variable Assignments
* cadquery and other python code
* object exports, via the export_object() function
Execution Environments
-----------------------
When your script runs, the container does not know which objects you wish to yeild for output.
Further, what 'output' means is different depending on the execution environment.
Most containers supply an export_object() method that allows you to export an object.
There are three execution environments:
1. **Native Library**. In this context, there is no execution environment. Your scripts will only generate output
when you manually invoke a method to save your object to disk, for example using the exporters library
1. **cadquery-freecad-module**. In this context, exporting an object means displaying it on the screen, and
registering it with FreeCAD for further manipulation.
2. **parametricparts.com** In this context, exporting an object means exporting it into a format chosen by the
user executing the script.
Variable Substitution
-----------------------
When a cadquery script runs, the values of the variables assume their hard-coded values.
Some execution environments, such as the `The CadQuery Freecad Module <https://github.com/jmwright/cadquery-freecad-module>`_
or `parametricParts.com <https://www.parametricparts.com>`_ , may subsitute other values supplied by a user of your script.
When this happens, your script will not know the difference: variables will appear to have been initialized the same
as they had be before.
see the :ref:`cqgi` section for more details.

View File

@ -43,6 +43,7 @@ Table Of Contents
apireference.rst
selectors.rst
classreference.rst
cqgi.rst
extending.rst
roadmap.rst

View File

@ -1,6 +1,5 @@
.. _3d_cad_primer:
.. module:: cadquery
CadQuery Concepts
===================================
@ -49,7 +48,7 @@ in space, or relative to other planes using offsets or rotations.
The most powerful feature of workplanes is that they allow you to work in 2D space in the coordinate system of the
workplane, and then build 3D features based on local coordinates. This makes scripts much easier to create and maintain.
See :py:class:`cadquery.CQ.Workplane` to learn more
See :py:class:`cadquery.Workplane` to learn more
2D Construction

View File

@ -1,12 +1,11 @@
.. module:: cadquery
.. _quickstart:
***********************
CadQuery QuickStart
***********************
.. module:: cadquery
Want a quick glimpse of what CadQuery can do? This quickstart will demonstrate the basics of cadQuery using a simple example
Prerequisites: FreeCAD + cadQuery-freeCAD-module in FreeCAD
@ -55,18 +54,15 @@ with place-holders for the dimensions. Paste this into the CodeWindow:
.. code-block:: python
:linenos:
import cadquery
from Helpers import show
height = 60.0
width = 80.0
thickness = 10.0
# make the base
result = cadquery.Workplane("XY").box(height, width, thickness)
result = cq.Workplane("XY").box(height, width, thickness)
# Render the solid
show(result)
build_object(result)
Press F2 to run the script. You should see Our basic base.
@ -83,10 +79,7 @@ This modification will do the trick:
.. code-block:: python
:linenos:
:emphasize-lines: 7,11
import cadquery
from Helpers import show
:emphasize-lines: 4,8
height = 60.0
width = 80.0
@ -94,11 +87,11 @@ This modification will do the trick:
diameter = 22.0
# make the base
result = cadquery.Workplane("XY").box(height, width, thickness)\
result = cq.Workplane("XY").box(height, width, thickness)\
.faces(">Z").workplane().hole(diameter)
# Render the solid
show(result)
build_object(result)
Rebuild your model by pressing F2. Your block should look like this:
@ -107,13 +100,13 @@ Rebuild your model by pressing F2. Your block should look like this:
The code is pretty compact, lets step through it.
**Line 7** adds a new parameter, diameter, for the diamter of the hole
**Line 4** adds a new parameter, diameter, for the diamter of the hole
**Line 11**, we're adding the hole.
:py:meth:`cadquery.CQ.CQ.faces` selects the top-most face in the Z direction, and then
:py:meth:`cadquery.CQ.CQ.workplane` begins a new workplane located on this face. The center of this workplane
**Line 8**, we're adding the hole.
:py:meth:`cadquery.CQ.faces` selects the top-most face in the Z direction, and then
:py:meth:`cadquery.CQ.workplane` begins a new workplane located on this face. The center of this workplane
is located at the geometric center of the shape, which in this case is the center of the plate.
Finally, :py:meth:`cadquery.CQ.Workplane.hole` drills a hole through the part 22mm in diamter
Finally, :py:meth:`cadquery.Workplane.hole` drills a hole through the part 22mm in diamter
.. note::
@ -139,10 +132,7 @@ Good news!-- we can get the job done with just two lines of code. Here's the cod
.. code-block:: python
:linenos:
:emphasize-lines: 8,13-15
import cadquery
from Helpers import show
:emphasize-lines: 5,10-13
height = 60.0
width = 80.0
@ -151,7 +141,7 @@ Good news!-- we can get the job done with just two lines of code. Here's the cod
padding = 12.0
# make the base
result = cadquery.Workplane("XY").box(height, width, thickness)\
result = cq.Workplane("XY").box(height, width, thickness)\
.faces(">Z").workplane().hole(diameter)\
.faces(">Z").workplane() \
.rect(height - padding,width - padding,forConstruction=True)\
@ -159,7 +149,7 @@ Good news!-- we can get the job done with just two lines of code. Here's the cod
.cboreHole(2.4, 4.4, 2.1)
# Render the solid
show(result)
build_object(result)
After pressing F2 to re-execute the model, you should see something like this:
@ -169,14 +159,14 @@ After pressing F2 to re-execute the model, you should see something like this:
There is quite a bit going on here, so lets break it down a bit.
**Line 8** creates a new padding parameter that decides how far the holes are from the edges of the plate.
**Line 5** creates a new padding parameter that decides how far the holes are from the edges of the plate.
**Line 13** selects the top-most face of the block, and creates a workplane on the top that face, which we'll use to
**Line 10** selects the top-most face of the block, and creates a workplane on the top that face, which we'll use to
define the centers of the holes in the corners.
There are a couple of things to note about this line:
1. The :py:meth:`cadquery.CQ.Workplane.rect` function draws a rectangle. **forConstruction=True**
1. The :py:meth:`cadquery.Workplane.rect` function draws a rectangle. **forConstruction=True**
tells CadQuery that this rectangle will not form a part of the solid,
but we are just using it to help define some other geometry.
2. The center point of a workplane on a face is always at the center of the face, which works well here
@ -184,15 +174,15 @@ There are a couple of things to note about this line:
this case, the center of the top face of the block. So this rectangle will be centered on the face
**Line 14** draws a rectangle 8mm smaller than the overall length and width of the block,which we will use to
**Line 11** draws a rectangle 8mm smaller than the overall length and width of the block,which we will use to
locate the corner holes. We'll use the vertices ( corners ) of this rectangle to locate the holes. The rectangle's
center is at the center of the workplane, which in this case co-incides with the center of the bearing hole.
**Line 15** selects the vertices of the rectangle, which we will use for the centers of the holes.
The :py:meth:`cadquery.CQ.CQ.vertices` function selects the corners of the rectangle
**Line 12** selects the vertices of the rectangle, which we will use for the centers of the holes.
The :py:meth:`cadquery.CQ.vertices` function selects the corners of the rectangle
**Line 16** uses the cboreHole function to draw the holes.
The :py:meth:`cadquery.CQ.Workplane.cboreHole` function is a handy CadQuery function that makes a counterbored hole,
**Line 13** uses the cboreHole function to draw the holes.
The :py:meth:`cadquery.Workplane.cboreHole` function is a handy CadQuery function that makes a counterbored hole,
like most other CadQuery functions, operate on the values on the stack. In this case, since we
selected the four vertices before calling the function, the function operates on each of the four points--
which results in a counterbore hole at the corners.
@ -208,10 +198,7 @@ We can do that using the preset dictionaries in the parameter definition:
.. code-block:: python
:linenos:
:emphasize-lines: 16
import cadquery
from Helpers import show
:emphasize-lines: 13
height = 60.0
width = 80.0
@ -220,7 +207,7 @@ We can do that using the preset dictionaries in the parameter definition:
padding = 12.0
# make the base
result = cadquery.Workplane("XY").box(height, width, thickness)\
result = cq.Workplane("XY").box(height, width, thickness)\
.faces(">Z").workplane().hole(diameter)\
.faces(">Z").workplane() \
.rect(height - padding, width - padding, forConstruction=True)\
@ -228,11 +215,12 @@ We can do that using the preset dictionaries in the parameter definition:
.edges("|Z").fillet(2.0)
# Render the solid
show(result)
build_object(result)
On **Line 16**, we're filleting the edges using the :py:meth:`cadquery.CQ.CQ.fillet` method.
To grab the right edges, the :py:meth:`cadquery.CQ.CQ.edges`
selects all of the edges that are parallel to the Z axis ("|Z"),
**Line 13** fillets the edges using the :py:meth:`cadquery.CQ.fillet` method.
To grab the right edges, the :py:meth:`cadquery.CQ.edges` selects all of the
edges that are parallel to the Z axis ("\|Z"),
The finished product looks like this:

View File

@ -3,7 +3,6 @@
String Selectors Reference
=============================
.. module:: cadquery
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.
@ -11,11 +10,11 @@ used, but all objects types can be filtered.
String selectors are simply shortcuts for using the full object equivalents. If you pass one of the
string patterns in, CadQuery will automatically use the associated selector object.
* :py:meth:`cadquery.CQ.CQ.faces`
* :py:meth:`cadquery.CQ.CQ.edges`
* :py:meth:`cadquery.CQ.CQ.vertices`
* :py:meth:`cadquery.CQ.CQ.solids`
* :py:meth:`cadquery.CQ.CQ.shells`
* :py:meth:`cadquery.CQ.faces`
* :py:meth:`cadquery.CQ.edges`
* :py:meth:`cadquery.CQ.vertices`
* :py:meth:`cadquery.CQ.solids`
* :py:meth:`cadquery.CQ.shells`
.. note::
@ -45,13 +44,13 @@ The axis used in the listing below are for illustration: any axis would work sim
========= ====================================== ======================================================= ==========================
Selector Selects Selector Class # objects returned
========= ====================================== ======================================================= ==========================
+Z Faces with normal in +z direction :py:class:`cadquery.selectors.DirectionSelector` 0 or 1
\|Z Faces parallel to xy plane :py:class:`cadquery.selectors.ParallelDirSelector` 0..many
-X Faces with normal in neg x direction :py:class:`cadquery.selectors.DirectionSelector` 0..many
#Z Faces perpendicular to z direction :py:class:`cadquery.selectors.PerpendicularDirSelector` 0..many
%Plane Faces of type plane :py:class:`cadquery.selectors.TypeSelector` 0..many
>Y Face farthest in the positive y dir :py:class:`cadquery.selectors.DirectionMinMaxSelector` 0 or 1
<Y Face farthest in the negative y dir :py:class:`cadquery.selectors.DirectionMinMaxSelector` 0 or 1
+Z Faces with normal in +z direction :py:class:`cadquery.DirectionSelector` 0 or 1
\|Z Faces parallel to xy plane :py:class:`cadquery.ParallelDirSelector` 0..many
-X Faces with normal in neg x direction :py:class:`cadquery.DirectionSelector` 0..many
#Z Faces perpendicular to z direction :py:class:`cadquery.PerpendicularDirSelector` 0..many
%Plane Faces of type plane :py:class:`cadquery.TypeSelector` 0..many
>Y Face farthest in the positive y dir :py:class:`cadquery.DirectionMinMaxSelector` 0 or 1
<Y Face farthest in the negative y dir :py:class:`cadquery.DirectionMinMaxSelector` 0 or 1
========= ====================================== ======================================================= ==========================
@ -73,13 +72,13 @@ The axis used in the listing below are for illustration: any axis would work sim
========= ==================================== ======================================================= ==========================
Selector Selects Selector Class # objects returned
========= ==================================== ======================================================= ==========================
+Z Edges aligned in the Z direction :py:class:`cadquery.selectors.DirectionSelector` 0..many
\|Z Edges parallel to z direction :py:class:`cadquery.selectors.ParallelDirSelector` 0..many
-X Edges aligned in neg x direction :py:class:`cadquery.selectors.DirectionSelector` 0..many
#Z Edges perpendicular to z direction :py:class:`cadquery.selectors.PerpendicularDirSelector` 0..many
%Line Edges of type line :py:class:`cadquery.selectors.TypeSelector` 0..many
>Y Edges farthest in the positive y dir :py:class:`cadquery.selectors.DirectionMinMaxSelector` 0 or 1
<Y Edges farthest in the negative y dir :py:class:`cadquery.selectors.DirectionMinMaxSelector` 0 or 1
+Z Edges aligned in the Z direction :py:class:`cadquery.DirectionSelector` 0..many
\|Z Edges parallel to z direction :py:class:`cadquery.ParallelDirSelector` 0..many
-X Edges aligned in neg x direction :py:class:`cadquery.DirectionSelector` 0..many
#Z Edges perpendicular to z direction :py:class:`cadquery.PerpendicularDirSelector` 0..many
%Line Edges of type line :py:class:`cadquery.TypeSelector` 0..many
>Y Edges farthest in the positive y dir :py:class:`cadquery.DirectionMinMaxSelector` 0 or 1
<Y Edges farthest in the negative y dir :py:class:`cadquery.DirectionMinMaxSelector` 0 or 1
========= ==================================== ======================================================= ==========================
@ -93,8 +92,8 @@ Only a few of the filter types apply to vertices. The location of the vertex is
========= ======================================= ======================================================= ==========================
Selector Selects Selector Class # objects returned
========= ======================================= ======================================================= ==========================
>Y Vertices farthest in the positive y dir :py:class:`cadquery.selectors.DirectionMinMaxSelector` 0 or 1
<Y Vertices farthest in the negative y dir :py:class:`cadquery.selectors.DirectionMinMaxSelector` 0 or 1
>Y Vertices farthest in the positive y dir :py:class:`cadquery.DirectionMinMaxSelector` 0 or 1
<Y Vertices farthest in the negative y dir :py:class:`cadquery.DirectionMinMaxSelector` 0 or 1
========= ======================================= ======================================================= ==========================
Future Enhancements

View File

@ -14,4 +14,5 @@ suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQSelectors.TestC
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadQuery.TestCadQuery))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestExporters.TestExporters))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestImporters.TestImporters))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQGI.TestCQGI))
unittest.TextTestRunner().run(suite)

View File

@ -92,7 +92,7 @@ class TestCQGI(BaseTest):
build_object(h)
"""
)
result = cqgi.execute(script, {'h': 33.33})
result = cqgi.parse(script).build( {'h': 33.33})
self.assertEquals(result.results[0], "33.33")
def test_that_assigning_string_to_number_fails(self):
@ -102,7 +102,7 @@ class TestCQGI(BaseTest):
build_object(h)
"""
)
result = cqgi.execute(script, {'h': "a string"})
result = cqgi.parse(script).build( {'h': "a string"})
self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError))
def test_that_assigning_unknown_var_fails(self):
@ -113,7 +113,7 @@ class TestCQGI(BaseTest):
"""
)
result = cqgi.execute(script, {'w': "var is not there"})
result = cqgi.parse(script).build( {'w': "var is not there"})
self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError))
def test_that_not_calling_build_object_raises_error(self):
@ -122,7 +122,7 @@ class TestCQGI(BaseTest):
h = 20.0
"""
)
result = cqgi.execute(script)
result = cqgi.parse(script).build()
self.assertTrue(isinstance(result.exception, cqgi.NoOutputError))
def test_that_cq_objects_are_visible(self):
@ -133,6 +133,38 @@ class TestCQGI(BaseTest):
"""
)
result = cqgi.execute(script)
result = cqgi.parse(script).build()
self.assertTrue(result.success)
self.assertIsNotNone(result.first_result)
def test_setting_boolean_variable(self):
script = textwrap.dedent(
"""
h = True
build_object( "*%s*" % str(h) )
"""
)
#result = cqgi.execute(script)
result = cqgi.parse(script).build({'h': False})
self.assertTrue(result.success)
self.assertEquals(result.first_result,'*False*')
def test_that_only_top_level_vars_are_detected(self):
script = textwrap.dedent(
"""
h = 1.0
w = 2.0
def do_stuff():
x = 1
y = 2
build_object( "result" )
"""
)
model = cqgi.parse(script)
self.assertEquals(2, len(model.metadata.parameters))