poc/poctools.py
2017-08-12 10:57:31 -05:00

584 lines
16 KiB
Python

#! python -*- coding: utf-8 -*-
# primitives for 'poc' modeling program
# Copyright © 2017 Jeff Epler <jepler@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import contextlib
import __future__
import itertools
import math
import os
import six
import shutil
import struct
import sys
import tempfile
import traceback
import OCC.BRepAlgo
import OCC.BRepAlgoAPI
import OCC.BRepBuilderAPI
import OCC.BRepBndLib
import OCC.BRepFilletAPI
import OCC.BRepGProp
import OCC.BRepLib
import OCC.BRepOffsetAPI
import OCC.BRepPrimAPI
import OCC.BRepTools
import OCC.Bnd
import OCC.GC
import OCC.GCE2d
import OCC.Geom
import OCC.Geom2d
import OCC.gp
import OCC.GProp
import OCC.StlAPI
import OCC.TopAbs
import OCC.TopExp
import OCC.TopTools
import OCC.TopoDS
def _dir(x):
if isinstance(x, (tuple, list)):
return OCC.gp.gp_Dir(*x)
return x
def _vec(x):
if isinstance(x, (tuple, list)):
return OCC.gp.gp_Vec(*x)
return x
def _pt(x):
if isinstance(x, (tuple, list)):
return OCC.gp.gp_Pnt(*x)
return x
def _axpt(p1, p2):
p1 = _pt(p1)
p2 = _pt(p2)
dx = p2.X() - p1.X()
dy = p2.Y() - p1.Y()
dz = p2.Z() - p1.Z()
length = (dx*dx + dy*dy + dz*dz) ** .5
return OCC.gp.gp_Ax2(p1, OCC.gp.gp_Dir(dx/length, dy/length, dz/length))
def _axcn(p, n):
p = _pt(p)
n = _pt(n)
dx = n.X()
dy = n.Y()
dz = n.Z()
length = (dx*dx + dy*dy + dz*dz) ** .5
return OCC.gp.gp_Ax2(p, OCC.gp.gp_Dir(dx/length, dy/length, dz/length))
__all__ = [
'Box', 'Cylinder', 'Cone', 'Sphere', 'Text', 'Torus',
'Extrude', 'Revolve', 'Loft', 'Pipe',
'Chamfer', 'Fillet', 'Rotate', 'Translate', 'Transform',
'Chamfered', 'Filleted', 'Rotated', 'Translated', 'Transformed',
'Intersection', 'Difference', 'Union', 'Op',
'Object', 'Bbox', 'CenterOfMass', 'CentreOfMass',
'Edges', 'Faces', 'Vertices', 'Wires',
'Matrix', 'Vertex', 'Edge', 'Wire', 'Face',
'execpoc', 'occ_to_stl', 'do_op',
]
### Supporting routines
def initial_ns():
ns = {
'__builtins__': __builtins__,
'__name__': '__main__',
'__doc__': None,
'__package__': None
}
six.exec_("""if 1:
from math import *
from poctools import *
""", ns)
return ns
compile_flags = (__future__.division.compiler_flag
| __future__.print_function.compiler_flag)
def getsource(filename):
with open(filename, "rU") as f:
return f.read()
def execpoc(args, **kw):
"""Execute the named .poc file from disk
Returns the resulting top level object"""
oldargv = sys.argv[:]
try:
filename = args[0]
code = compile(getsource(filename), filename, 'exec', compile_flags)
sys.argv[:] = args
ns = initial_ns()
ns['__file__'] = filename
ns.update(kw)
start()
six.exec_(code, ns)
return ns
finally: sys.argv[:] = oldargv
def do_op(b):
"""Adds the object 'b' to the current operation"""
if b is None:
raise ValueError
n = next(op)
n(obj, b)
def _assign(a, b):
global obj
obj = b
def _fuse(a, b):
global obj
obj = OCC.BRepAlgoAPI.BRepAlgoAPI_Fuse(a, b).Shape()
def _common(a, b):
global obj
obj = OCC.BRepAlgoAPI.BRepAlgoAPI_Common(a, b).Shape()
def _cut(a, b):
global obj
obj = OCC.BRepAlgoAPI.BRepAlgoAPI_Cut(a, b).Shape()
def op1(x):
return iter(itertools.chain([_assign], itertools.repeat(x)))
def start():
global obj, op
obj = OCC.TopoDS.TopoDS_Shape()
op = op1(_fuse)
def output(fn):
occ_to_stl(obj, fn)
def occ_to_stl(obj, filename, prec=.05):
"""Convert a solid to stl"""
w = OCC.StlAPI.StlAPI_Writer()
w.SetASCIIMode(False)
w.SetDeflection(prec)
w.SetRelativeMode(False)
w.Write(obj, filename + ".tmp", True)
os.rename(filename + ".tmp", filename)
@contextlib.contextmanager
def withhelper(newop, newobj=None, finalop=None):
global obj, op
holdobj = obj
holdop = op
obj = newobj = newobj or OCC.TopoDS.TopoDS_Shape()
op = iter(newop)
try:
yield
finally:
if finalop: finalop()
newobj = obj
obj = holdobj
op = holdop
do_op(newobj)
@contextlib.contextmanager
def TemporaryDirectory(*args):
d = tempfile.mkdtemp(*args)
try:
yield d
finally:
shutil.rmtree(d)
### Primitives
def Box(p1, p2):
"""Create a box primitive"""
do_op(OCC.BRepPrimAPI.BRepPrimAPI_MakeBox(_pt(p1), _pt(p2)).Shape())
def Cylinder(p1, p2, radius):
"""Create a cylinder primitive"""
p1 = _pt(p1)
p2 = _pt(p2)
dx = p2.X() - p1.X()
dy = p2.Y() - p1.Y()
dz = p2.Z() - p1.Z()
length = (dx*dx + dy*dy + dz*dz) ** .5
ax = OCC.gp.gp_Ax2(p1, OCC.gp.gp_Dir(dx/length, dy/length, dz/length))
do_op(OCC.BRepPrimAPI.BRepPrimAPI_MakeCylinder(ax, radius, length).Shape())
def Cone(p1, p2, radius1, radius2):
"""Create a cone primitive"""
p1 = _pt(p1)
p2 = _pt(p2)
dx = p2.X() - p1.X()
dy = p2.Y() - p1.Y()
dz = p2.Z() - p1.Z()
length = (dx*dx + dy*dy + dz*dz) ** .5
ax = OCC.gp.gp_Ax2(p1, OCC.gp.gp_Dir(dx/length, dy/length, dz/length))
builder = OCC.BRepPrimAPI.BRepPrimAPI_MakeCone(
ax, radius1, radius2, length)
shape = builder.Shape()
do_op(shape)
def Sphere(center, radius):
"""Create a sphere primitive"""
do_op(OCC.BRepPrimAPI.BRepPrimAPI_MakeSphere(_pt(center), radius).Shape())
def Text(height, depth, text, fontpath=None):
"""TODO Create extruded text
Note that the text may not contain whitespace!
(this appears to be a bug in occmodel, failing with occmodel.OCCError:
b'failed to create edges')"""
return Box((0,0,0), (1,1,1))
def Torus(p1, p2, ringRadius, radius):
"""Create a torus"""
axis = _axpt(p1, p2)
builder = OCC.BRepPrimAPI.BRepPrimAPI_MakeTorus(axis, ringRadius, radius)
do_op(builder.Shape())
def Extrude(obj, p1, p2):
"""Create a solid by extruding edge, wire, or face from p1 to p2"""
p1 = _pt(p1)
p2 = _pt(p2)
direction = OCC.gp.gp_Vec(p1, p2)
do_op(OCC.BRepPrimAPI.BRepPrimAPI_MakePrism(obj, direction).Shape())
def Revolve(face, p1, p2, angle):
"""Create a solid by revolving the face around the given axis"""
p1 = _pt(p1)
p2 = _pt(p2)
dx = p2.X() - p1.X()
dy = p2.Y() - p1.Y()
dz = p2.Z() - p1.Z()
axis = OCC.gp.gp_Ax1(p1, _dir((dx, dy, dz)))
angle = math.radians(angle)
do_op(OCC.BRepPrimAPI.BRepPrimAPI_MakeRevol(face, axis, angle, False).Shape())
def Loft(profiles, ruled=True, tolerance=1e-6):
"""Create a solid by lofting through a sequence of wires or closed edges"""
builder = OCC.BRepOffsetAPI.BRepOffsetAPI_ThruSections(True, ruled,
tolerance)
for i in profiles:
if isinstance(i, OCC.TopoDS.TopoDS_Wire):
builder.AddWire(i)
elif isinstance(i, OCC.TopoDS.TopoDS_Vertex):
builder.AddVertex(i)
else:
builder.AddWire(Wire.createWire(i))
do_op(builder.Shape())
def Pipe(face, path):
if isinstance(path, OCC.TopoDS.TopoDS_Edge):
wire = Wire.createWire((path,))
else:
wire = path
builder = OCC.BRepOffsetAPI.BRepOffsetAPI_MakePipe(wire, face)
do_op(builder.Shape())
### Group operations
def Intersection():
"""Perform an intersection operation"""
return withhelper(op1(_common))
def Union():
"""Perform a union operation"""
return withhelper(op1(_fuse))
def Difference():
"""Perform a difference operation"""
return withhelper(op1(_cut))
def Op(fn, *args, **kw):
"""Convert a postfix operation into a group operation
the following are roughly equivalent::
with Union():
Box(p1, p2)
Box(p3, p4)
Fillet(8)
and::
with Op(Fillet, 8):
Box(p1, p2)
Box(p3, p4)
"""
return withhelper(op1(_fuse), finalop=lambda: fn(*args, **kw))
def Rotated(angle, axis, center=(0,0,0)):
"""Perform a rotate operation"""
return Op(Rotate, angle, axis, center)
def Translated(delta):
"""Perform a translate operation"""
return Op(Translate, delta)
def Transformed(mat):
"""Perform a transformation."""
return Op(Transform, mat)
def Filleted(radius, edges=None):
"""Perform a fillet operation"""
return Op(Fillet, radius, edges)
def Chamfered(distance, edges=None):
"""Perform a fillet operation"""
return Op(Chamfer, distance, edges)
### Postfix operations
def _transform(obj, t):
_assign(obj,
OCC.BRepBuilderAPI.BRepBuilderAPI_Transform(obj, t, True).Shape())
def Rotate(angle, axis, center=(0,0,0)):
"""Rotate the active object"""
angle = math.radians(angle)
a = OCC.gp.gp_Ax1()
a.SetLocation(_pt(center))
a.SetDirection(_dir(axis))
t = OCC.gp.gp_Trsf()
t.SetRotation(a, angle)
_transform(obj, t)
def Translate(delta):
"""Translate the active object"""
t = OCC.gp.gp_Trsf()
t.SetTranslation(_vec(delta))
_transform(obj, t)
def Matrix(*args):
"""Construct a 4x3 matrix from arguments, which may be
- A list of 12 values
- A list of 9 values (in which case the last column is taken to be zeros)
- A list of 3 4-tuples, each taken as a row
- A list of 3 3-tuples, each taken as a row (the last column taken to be zeros)
"""
result = OCC.gp.gp_Trsf()
if len(args) == 0:
return result
if len(args) == 3:
if len(args[0]) == 4:
result.SetValues(*(args[0] + args[1] + args[2]))
return result
elif len(args[0]) == 3:
result.SetValues(*(args[0] + (0,) + args[1] + (0,) + args[2] + (0,)))
return result
elif len(args) == 9:
result.SetValues(
args[0], args[1], args[2], 0,
args[3], args[4], args[5], 0,
args[6], args[7], args[8], 0)
return result
result.SetValues(*args)
return result
def Transform(mat):
"""Transform the active object
Note that `geotools.Transform` is imported as `Xform` within poc files."""
_transform(obj, mat)
def Fillet(radius, edges=None):
"""Fillet the active object
If `edges` is None, then all edges are filletted.
If `edges` is callable, it is treated as a predicate which returns
True for each edge that should be filleted.
Otherwise, `edges` must be a sequence of edges to fillet.
"""
if callable(edges):
edges = [e for e in Edges() if edges(e)]
elif edges is None:
edges = [e for e in Edges()]
fillet = OCC.BRepFilletAPI.BRepFilletAPI_MakeFillet(obj)
for e in edges:
fillet.Add(radius, e)
_assign(obj, fillet.Shape())
def Chamfer(distance, edges=None):
"""Chamfer the active object
If `edges` is None, then all edges are filletted.
If `edges` is callable, it is treated as a predicate which returns
True for each edge that should be filleted.
Otherwise, `edges` must be a sequence of edges to fillet.
"""
if callable(edges):
edges = [e for e in Edges() if edges(e)]
elif edges is None:
edges = [e for e in Edges()]
chamfer = OCC.BRepFilletAPI.BRepFilletAPI_MakeChamfer(obj)
m = OCC.TopTools.TopTools_IndexedDataMapOfShapeListOfShape();
OCC.TopExp.topexp.MapShapesAndAncestors(obj, OCC.TopAbs.TopAbs_EDGE,
OCC.TopAbs.TopAbs_FACE, m)
for e in edges:
f = m.FindFromKey(e).First()
f = OCC.TopoDS.topods.Face(f)
chamfer.Add(distance, e, f)
_assign(obj, chamfer.Shape())
### Inquiries
def visit(shape, topologyType, factory):
explorer = OCC.TopExp.TopExp_Explorer()
explorer.Init(shape, topologyType)
while explorer.More():
it = explorer.Current()
# XXX pythonocc-core examples _loop_topo avoids yielding
# items with equal _hash__ more than once but this seems bogus
yield factory(it)
explorer.Next()
def Object():
return obj
def CenterOfMass():
"""Return the center of mass box of the current item"""
prop = OCC.GProp.GProp_GProps()
OCC.BRepGProp.brepgprop_VolumeProperties(obj, prop)
return prop.CentreOfMass()
CentreOfMass = CenterOfMass
def Bbox(o=None):
"""Return the bounding box of given object or the current item a a 6-tuple
(minx, miny, minz, maxx, maxy, maxz)"""
box = OCC.Bnd.Bnd_Box()
OCC.BRepBndLib.brepbndlib.Add(o or obj, box)
lo = box.CornerMin()
hi = box.CornerMax()
return ((lo.X(), lo.Y(), lo.Z(), hi.X(), hi.Y(), hi.Z()))
def Edges():
"""Return the edge iterator of the current item"""
return visit(Object(), OCC.TopAbs.TopAbs_EDGE, OCC.TopoDS.topods.Edge)
def Faces():
"""Return the face iterator of the current item"""
return visit(Object(), OCC.TopAbs.TopAbs_FACE, OCC.TopoDS.topods.Face)
def Vertices():
"""Return the vertex iterator of the current item"""
return visit(Object(), OCC.TopAbs.TopAbs_VERTEX, OCC.TopoDS.topods.Vertex)
def Wires():
"""Return the wire iterator of the current item"""
return visit(Object(), OCC.TopAbs.TopAbs_WIRE, OCC.TopoDS.topods.Wire)
class Edge:
@classmethod
def createLine(cls, p1, p2):
p1 = _pt(p1)
p2 = _pt(p2)
return OCC.BRepBuilderAPI.BRepBuilderAPI_MakeEdge(p1, p2).Edge()
@classmethod
def createArc3P(cls, start, end, mid):
p1 = _pt(start)
p2 = _pt(mid)
p3 = _pt(end)
arc = OCC.GC.GC_MakeArcOfCircle(p1, p2, p3).Value()
return OCC.BRepBuilderAPI.BRepBuilderAPI_MakeEdge(arc).Edge()
@classmethod
def createCircle(cls, center, normal, radius):
ax = _axcn(center, normal)
arc = OCC.GC.GC_MakeCircle(ax, radius).Value()
return OCC.BRepBuilderAPI.BRepBuilderAPI_MakeEdge(arc).Edge()
@classmethod
def createHelix(cls, pitch, height, radius, angle, leftHanded=False):
axis = OCC.gp.gp_Ax2(_pt((0,0,0)), OCC.gp.gp.DZ())
axis = OCC.gp.gp_Ax3(axis)
if angle <= 0:
surf = OCC.Geom.Geom_CylindricalSurface(axis, radius)
else:
angle = math.radians(angle)
surf = OCC.Geom.Geom_ConicalSurface(axis, angle, radius)
if leftHanded:
p = OCC.gp.gp_Pnt2d(2*math.pi,0)
d = OCC.gp.gp_Dir2d(-2 * math.pi, pitch)
else:
p = OCC.gp.gp_Pnt2d(0,0)
d = OCC.gp.gp_Dir2d(2 * math.pi, pitch)
axis2 = OCC.gp.gp_Ax2d(p, d)
line = OCC.Geom2d.Geom2d_Line(axis2)
end_u = (4*math.pi*math.pi+pitch*pitch)**.5*(height/pitch)
begin = line.Value(0)
end = line.Value(end_u)
seg = OCC.GCE2d.GCE2d_MakeSegment(begin, end).Value()
edge = OCC.BRepBuilderAPI.BRepBuilderAPI_MakeEdge(seg,
OCC.Geom.Handle_Geom_Surface(surf)).Edge()
OCC.BRepLib.breplib.BuildCurves3d(edge)
return edge
@classmethod
def createEllipse(cls, center, normal, rMajor, rMinor):
ax = _axcn(center, normal)
arc = OCC.GC.GC_MakeEllipse(ax, rMajor, rMinor).Value()
return OCC.BRepBuilderAPI.BRepBuilderAPI_MakeEdge(arc).Edge()
class Wire:
@classmethod
def createWire(cls, arg):
if isinstance(arg, (list, tuple)): pass
else: arg = (arg,)
builder = OCC.BRepBuilderAPI.BRepBuilderAPI_MakeWire()
for i in arg:
builder.Add(i)
return builder.Wire()
def Vertex(x,y,z):
builder = OCC.BRepBuilderAPI.BRepBuilderAPI_MakeVertex(_pt((x,y,z)))
return builder.Vertex()
def _dump(obj):
import OCC.BRepTools
OCC.BRepTools.breptools.Write(obj, "/dev/stdout")
class Face:
@classmethod
def createFace(cls, arg):
def f(x):
if isinstance(x, OCC.TopoDS.TopoDS_Edge):
return Wire.createWire(x)
else:
return x
if isinstance(arg, (list, tuple)):
pass
else:
arg = (arg,)
outer = arg[0]
rest = arg[1:]
outer = f(outer)
builder = OCC.BRepBuilderAPI.BRepBuilderAPI_MakeFace(outer)
for i in rest:
i = f(i)
builder.Add(i)
return builder.Face()