poc/poctools.py
2017-08-09 21:04:30 -05:00

330 lines
8.9 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 geotools
import math
import occmodel
import os
import six
import struct
import sys
__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',
'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 geotools import *
Xform = Transform
from occmodel 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):
return a.copyFrom(b)
def _fuse(a, b):
return a.fuse(b)
def _common(a, b):
return a.common(b)
def _cut(a, b):
return a.cut(b)
def op1(x):
return iter(itertools.chain([_assign], itertools.repeat(x)))
def start():
global obj, op
obj = occmodel.Solid()
op = op1(_fuse)
def output(fn):
with open(fn + ".tmp", "wb") as f: occ_to_stl(obj, f)
os.rename(fn + ".tmp", fn)
def mesh_to_stl(m, dest):
dest.write(b"\0" * 80)
dest.write(struct.pack("<i", m.ntriangles()))
n0 = struct.pack("<fff", 0., 0., 0.)
for i in range(0, m.ntriangles()*3, 3):
dest.write(n0)
dest.write(struct.pack("<fff", *m.vertex(m.triangles[i])))
dest.write(struct.pack("<fff", *m.vertex(m.triangles[i+1])))
dest.write(struct.pack("<fff", *m.vertex(m.triangles[i+2])))
dest.write(b'\0\0')
def occ_to_stl(o, dest, prec=.001):
"""Convert a mesh or solid to stl
Writes to the open file object 'dest'
If a solid is passed it, it is converted to a mesh with the given
precision, defaulting to .001."""
if isinstance(o, occmodel.Mesh): mesh_to_stl(o, dest)
mesh_to_stl(o.createMesh(prec), dest)
@contextlib.contextmanager
def withhelper(newop, newobj=None, finalop=None):
global obj, op
holdobj = obj
holdop = op
obj = newobj = newobj or occmodel.Solid()
op = iter(newop)
try:
yield
finally:
if finalop: finalop()
obj = holdobj
op = holdop
do_op(newobj)
### Primitives
def Box(p1, p2):
"""Create a box primitive"""
do_op(occmodel.Solid().createBox(p1, p2))
def Cylinder(p1, p2, radius):
"""Create a cylinder primitive"""
do_op(occmodel.Solid().createCylinder(p1, p2, radius))
def Cone(p1, p2, radius1, radius2):
"""Create a cone primitive"""
do_op(occmodel.Solid().createCone(p1, p2, radius1, radius2))
def Sphere(center, radius):
"""Create a sphere primitive"""
e1 = occmodel.Edge().createArc((1,0,0), (0,1,0), (0,0,0))
e2 = occmodel.Edge().createLine((0,1,0), (0,0,0))
e3 = occmodel.Edge().createLine((0,0,0), (1,0,0))
w1 = occmodel.Wire().createWire((e1, e2, e3))
f1 = occmodel.Face().createFace(w1)
o = occmodel.Solid()
o.revolve(f1, (0,0,0), (1,0,0), 2*math.pi)
p = o.copy().mirror(geotools.Plane.fromNormal((0,0,0), (1,0,0)))
o.fuse(p)
o.scale((0,0,0), radius)
o.translate(center)
do_op(o)
def Text(height, depth, text, fontpath=None):
"""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')"""
do_op(occmodel.Solid().createText(height, depth, text, fontpath))
def Torus(p1, p2, ringRadius, radius):
"""Create a torus"""
do_op(occmodel.Solid().createTorus(p1, p2, ringRadius, radius))
def Extrude(obj, p1, p2):
"""Create a solid by extruding edge, wire, or face from p1 to p2"""
do_op(occmodel.Solid().extrude(obj, p1, p2))
def Revolve(face, p1, p2, angle):
"""Create a solid by revolving the face around the given axis"""
do_op(occmodel.Solid().revolve(face, p1, p2, angle))
def Loft(profiles, ruled=True, tolerance=1e-6):
"""Create a solid by lofting through a sequence of wires or closed edges"""
do_op(occmodel.Solid().loft(profiles, ruled, tolerance))
def Pipe(face, path):
do_op(occmodel.Solid().pipe(face, path))
### 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.
Note that `geotools.Transform` is imported as `Xform` within poc files."""
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 Rotate(angle, axis, center=(0,0,0)):
"""Rotate the active object"""
obj.rotate(angle, axis, center)
def Translate(delta):
"""Translate the active object"""
obj.translate(delta)
def Transform(mat):
"""Transform the active object
Note that `geotools.Transform` is imported as `Xform` within poc files."""
obj.transform(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)]
obj.fillet(radius, edges)
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)]
obj.chamfer(distance, edges)
### Inquiries
def Object():
return obj
def CenterOfMass():
"""Return the bounding box of the current item"""
return obj.centreOfMass()
CentreOfMass = CenterOfMass
def Bbox():
"""Return the bounding box of the current item"""
return obj.boundingBox()
def Edges():
"""Return the edge iterator of the current item"""
return occmodel.EdgeIterator(Object())
def Faces():
"""Return the face iterator of the current item"""
return occmodel.FaceIterator(Object())
def Vertices():
"""Return the vertex iterator of the current item"""
return occmodel.VertexIterator(Object())
def Wires():
"""Return the wire iterator of the current item"""
return occmodel.WireIterator(Object())