Major rewrite: switch from occmodel to pythonocc-core + vtk

.. this requires reverting to python2 due to lack of python3
vtk packages in debian stretch.

pythonocc-core is much more verbose, but it exposes the whole
oce API to Python, which means that I'm not stuck when I want
to expose something not in occmodel.  I hope that it's a better
way forward.
This commit is contained in:
Jeff Epler 2017-08-09 21:04:30 -05:00
parent 34be878d10
commit db5b15bfc4
18 changed files with 469 additions and 145 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
examples/*.stl
examples/*.png
.coverage
*.py[oc]
build

View file

@ -5,30 +5,33 @@
*poc* is a tool in the vein of *OpenSCAD* for creating 3D models in a high
level language with a minimum of boilerplate.
*poc* programs are Python3 programs, executed in an environment that
*poc* programs are Python2 programs, executed in an environment that
provides convenient shorthand for performing geometric operations.
*poc* uses OpenCASCADE (via occmodel) to implement its geometric operations.
This means it has different strengths and weaknesses compared to *OpenSCAD*,
which uses CGAL. For instance, OpenCASCADE has `fillet` as a first-class
operation, while it lacks `minkowski` and `hull` which are quite frequently
used in *OpenSCAD*.
Python2 is used instead of Python3 because a python3 compatible version of
vtk is not availble in debian stretch. However, the python3-like features of
`print_function` and `division` are automatically enabled.
*poc* uses OpenCASCADE (via pythonocc-core) to implement its geometric
operations. This means it has different strengths and weaknesses compared to
*OpenSCAD*, which uses CGAL. For instance, OpenCASCADE has `fillet` as a
first-class operation, while it lacks `minkowski` and `hull` which are quite
frequently used in *OpenSCAD*.
![pocview showing selective fillet of CSG object](/images/selective_fillet.png)
# Setup
* Install dependencies
* Run setup, e.g., `sudo python3 setup.py install`
* Invoke `pocview somefile.poc` to lanuch a viewer. It autoupdates if you modify the input file. Note that some versions of `gltools` create fullscreen windows unconditionally (and iconify them when they lose focus), which is inconvenient for this use.
* Run setup, e.g., `sudo python setup.py install`
* Invoke `pocview somefile.poc` to lanuch a viewer. It autoupdates if you modify the input file.
* Invoke `poc somefile.poc` to create `somefile.stl`
* or use `#!/usr/bin/env poc` so that `./somefile.poc` is executable
# Dependencies
* [OpenCASCADE Community Edition (OCE)](https://github.com/tpaviot/oce/pulls)
* [occmodel](https://github.com/tenko/occmodel)
* [geotools](https://github.com/tenko/geotools)
* [gltools](https://github.com/tenko/gltools)
* pythonocc-core
* python-vtk6
# Stability

View file

@ -1,7 +1,11 @@
#!/bin/sh
python3-coverage run ./poc || true
for i in examples/*.poc; do
echo $i
python3-coverage run -a ./poc $i || exit $?
done
python3-coverage report -m
#!/bin/bash
set -eo pipefail
python-coverage erase
python-coverage run ./poc || true
find examples -name \*.poc -print0 | xargs -0n1 -P`getconf _NPROCESSORS_ONLN` python-coverage run -p ./poc
#for i in examples/*.poc; do
# echo $i
# python-coverage run -a ./poc $i || exit $?
#done
python-coverage combine -a .coverage.*
python-coverage report -m --include poctools.py

View file

@ -3,7 +3,7 @@
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx3-build
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build

View file

@ -18,8 +18,34 @@
#
import os
import sys
import mock
sys.path.insert(0, os.path.abspath('..'))
sys.modules['occmodel'] = object()
for s in '''
OCC
OCC.BRepAlgo
OCC.BRepAlgoAPI
OCC.BRepBuilderAPI
OCC.BRepBndLib
OCC.BRepFilletAPI
OCC.BRepGProp
OCC.BRepLib
OCC.BRepOffsetAPI
OCC.BRepPrimAPI
OCC.BRepTools
OCC.Bnd
OCC.GC
OCC.GCE2d
OCC.Geom
OCC.Geom2d
OCC.gp
OCC.GProp
OCC.StlAPI
OCC.TopAbs
OCC.TopExp
OCC.TopTools
OCC.TopoDS
'''.split():
sys.modules[s] = mock.Mock()
# -- General configuration ------------------------------------------------

View file

@ -9,10 +9,10 @@ Python OCE Composer (poc)
*poc* is a tool in the vein of *OpenSCAD* for creating 3D models in a high
level language with a minimum of boilerplate.
*poc* programs are Python3 programs, executed in an environment that
*poc* programs are Python2 programs, executed in an environment that
provides convenient shorthand for performing geometric operations.
*poc* uses OpenCASCADE (via occmodel) to implement its geometric operations.
*poc* uses OpenCASCADE to implement its geometric operations.
This means it has different strengths and weaknesses compared to *OpenSCAD*,
which uses CGAL. For instance, OpenCASCADE has `fillet` as a first-class
operation, while it lacks `minkowski` and `hull` which are quite frequently
@ -21,6 +21,8 @@ used in *OpenSCAD*.
The the design of the *poc* standard library is very much in flux, and
there are likely to be compatibility-breaking changes as it develops.
For installation and setup instructions, see README.md.
Contents:
.. toctree::

View file

@ -3,8 +3,8 @@ poctools module
.. automodule:: poctools
Primitives
~~~~~~~~~~
Solid Primitives
~~~~~~~~~~~~~~~~
Each primitive is added to the currently active group operation.
At the outermost level, the active group operation is union().
@ -12,13 +12,28 @@ At the outermost level, the active group operation is union().
.. autofunction:: Cylinder
.. autofunction:: Cone
.. autofunction:: Sphere
.. autofunction:: Text
.. autofunction:: Torus
.. autofunction:: Extrude
.. autofunction:: Loft
.. autofunction:: Pipe
.. autofunction:: Revolve
Other primitives
~~~~~~~~~~~~~~~~
These classes have classmethods to construct objects of the given
type. They may be useful in constructing solid primitives;
for example, `poctools.Loft` needs a sequence of `Edge` objects.
This syntax is for compatibility with `occmodel`. A future improvement
to `poc` should change them so that e.g., `Loft` becomes a group operation,
and `Edge` becomes a solid primitive that implicitly adds itself to the
surrounding `Loft`.
.. autoclass:: Edge
.. autoclass:: Face
.. autoclass:: Wire
.. autoclass:: Vertex
Postfix operations
~~~~~~~~~~~~~~~~~~
A postfix operation modifies the currently active group operation

View file

@ -1,9 +1,8 @@
# This example isn't useful, except that it increases code coverage
import geotools
with Union():
with Rotated(1, (0,0,1)):
Sphere((0,0,0), 6)
with Filleted(2), Transformed(geotools.Transform()):
with Filleted(.5), Transformed(Matrix()), Chamfered(2):
Box((-5,-5,-5), (5,5,5))
print(Bbox())
print(CenterOfMass())
@ -12,3 +11,31 @@ with Union():
print(len(list(Wires())))
Text(8, .25, 'Hello_world')
import poctools
poctools._dir(poctools._dir((1,2,3)))
poctools._vec(poctools._vec((1,2,3)))
poctools._pt(poctools._pt((1,2,3)))
poctools.Matrix(
(1,0,0),
(0,1,0),
(0,0,1))
poctools.Matrix(
(1,0,0,0),
(0,1,0,0),
(0,0,1,0))
poctools.Matrix(
1,0,0,0,
0,1,0,0,
0,0,1,0)
poctools.Matrix(
1,0,0,
0,1,0,
0,0,1)
poctools._dump(Object())
try:
poctools.do_op(None)
except ValueError:
pass
else:
1/0

View file

@ -1,6 +1,8 @@
e1 = Edge().createLine((-.5,0.,0.),(.5,0.,0.))
e2 = Edge().createArc3P((.5,0.,0.),(-.5,0.,0.),(0.,.5,0.))
w1 = Wire().createWire((e1,e2))
f1 = Face().createFace(w1)
x = Edge().createCircle((0,.25,0), (0,0,1), (.1))
f1 = Face().createFace((w1, x))
#do_op(f1)
Extrude(f1, (0.,0.,0.), (0.,0.,1.))
Fillet(.05)

View file

@ -2,4 +2,4 @@ e1 = Edge().createCircle(center=(.25,0.,0.),normal=(-.25,0.,1.),radius = .25)
e2 = Edge().createCircle(center=(.25,0.,.5),normal=(0.,.25,1.),radius = .5)
v1 = Vertex(.25,0.,1.)
Loft((e1, e2, v1))
Loft((e1, Wire.createWire((e2)), v1))

View file

@ -1,8 +1,11 @@
Fudge = Xform().scale((1+1e-7, 1+1e-7, 1+1e-7))
e1 = Edge().createHelix(.5, 4, 1, 0)
e2 = Edge().createCircle(center=(1.,0.,0.),normal=(0.,-1.,0.),radius = 0.2)
f1 = Face().createFace(e2)
e1b = Edge().createHelix(.5, 4, 1, 15, True)
with Translated((10,0,0)):
Pipe(f1, Wire.createWire(e1b))
with Difference():
Cylinder((0,0,-.5), (0,0,3.5), 1)
Pipe(f1, e1)

View file

@ -1,6 +1,4 @@
#!/usr/bin/env poc
import geotools
with Translated((0,0,0)):
Sphere((0,0,0), 12)
@ -10,7 +8,7 @@ with Translated((-20,0,0)):
with Translated((20,0,0)), Filleted(1):
Cylinder((0,0,-5), (0,0,5), 5)
with Translated((0,20,0)), Chamfered(1, lambda e: e.boundingBox().max.z < 0):
with Translated((0,20,0)), Chamfered(2, lambda e: Bbox(e)[2] < 0):
Cone((0,0,-5), (0,0,5), 5, 2)
with Translated((0,-20,0)):

View file

@ -1,4 +1,4 @@
e = Edge().createEllipse(center=(0.,0.,0.),normal=(0.,0.,1.), rMajor = .5, rMinor=.2)
f = Face().createFace(e)
Revolve(f, (1.,0.,0.), (1.,1.,0.), pi/2.)
Revolve(f, (1.,0.,0.), (1.,1.,0.), 90)

View file

@ -3,4 +3,4 @@ with Difference():
Cylinder((-100,0,0), (100,0,0), 25)
Cylinder((0,-100,0), (0,100,0), 25)
Cylinder((0,0,-100), (0,0,100), 25)
Fillet(12, lambda e: e.boundingBox().min.z > 0)
Fillet(12, lambda e: Bbox(e)[2] > 0)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 117 KiB

2
poc
View file

@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/python2
# -*- coding: utf-8 -*-
# main module of 'poc' modeling program
# Copyright © 2017 Jeff Epler <jepler@gmail.com>

View file

@ -19,13 +19,68 @@
import contextlib
import __future__
import itertools
import geotools
import math
import occmodel
import os
import six
import struct
import sys
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',
@ -35,6 +90,7 @@ __all__ = [
'Intersection', 'Difference', 'Union', 'Op',
'Object', 'Bbox', 'CenterOfMass', 'CentreOfMass',
'Edges', 'Faces', 'Vertices', 'Wires',
'Matrix', 'Vertex', 'Edge', 'Wire', 'Face',
'execpoc', 'occ_to_stl', 'do_op',
]
@ -48,15 +104,16 @@ def initial_ns():
}
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()
with open(filename, "rU") as f:
return f.read()
def execpoc(args, **kw):
"""Execute the named .poc file from disk
@ -66,7 +123,7 @@ Returns the resulting top level object"""
oldargv = sys.argv[:]
try:
filename = args[0]
code = compile(getsource(filename), filename, 'exec')
code = compile(getsource(filename), filename, 'exec', compile_flags)
sys.argv[:] = args
ns = initial_ns()
ns['__file__'] = filename
@ -78,67 +135,59 @@ Returns the resulting top level object"""
def do_op(b):
"""Adds the object 'b' to the current operation"""
if b is None: raise ValueError
if b is None:
raise ValueError
n = next(op)
n(obj, b)
def _assign(a, b):
return a.copyFrom(b)
global obj
obj = b
def _fuse(a, b):
return a.fuse(b)
global obj
obj = OCC.BRepAlgoAPI.BRepAlgoAPI_Fuse(a, b).Shape()
def _common(a, b):
return a.common(b)
global obj
obj = OCC.BRepAlgoAPI.BRepAlgoAPI_Common(a, b).Shape()
def _cut(a, b):
return a.cut(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 = occmodel.Solid()
obj = OCC.TopoDS.TopoDS_Shape()
op = op1(_fuse)
def output(fn):
with open(fn + ".tmp", "wb") as f: occ_to_stl(obj, f)
os.rename(fn + ".tmp", fn)
occ_to_stl(obj, 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)
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 occmodel.Solid()
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)
@ -147,58 +196,89 @@ def withhelper(newop, newobj=None, finalop=None):
def Box(p1, p2):
"""Create a box primitive"""
do_op(occmodel.Solid().createBox(p1, p2))
do_op(OCC.BRepPrimAPI.BRepPrimAPI_MakeBox(_pt(p1), _pt(p2)).Shape())
def Cylinder(p1, p2, radius):
"""Create a cylinder primitive"""
do_op(occmodel.Solid().createCylinder(p1, p2, radius))
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"""
do_op(occmodel.Solid().createCone(p1, p2, radius1, radius2))
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"""
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)
do_op(OCC.BRepPrimAPI.BRepPrimAPI_MakeSphere(_pt(center), radius).Shape())
def Text(height, depth, text, fontpath=None):
"""Create extruded text
"""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')"""
do_op(occmodel.Solid().createText(height, depth, text, fontpath))
return Box((0,0,0), (1,1,1))
def Torus(p1, p2, ringRadius, radius):
"""Create a torus"""
do_op(occmodel.Solid().createTorus(p1, p2, ringRadius, radius))
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"""
do_op(occmodel.Solid().extrude(obj, p1, 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"""
do_op(occmodel.Solid().revolve(face, p1, p2, angle))
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"""
do_op(occmodel.Solid().loft(profiles, ruled, tolerance))
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):
do_op(occmodel.Solid().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
@ -256,19 +336,51 @@ def Chamfered(distance, edges=None):
### 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"""
obj.rotate(angle, axis, center)
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"""
obj.translate(delta)
t = OCC.gp.gp_Trsf()
t.SetTranslation(_vec(delta))
_transform(obj, t)
def Matrix(*args):
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."""
obj.transform(mat)
_transform(obj, mat)
def Fillet(radius, edges=None):
"""Fillet the active object
@ -280,8 +392,14 @@ 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)
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
@ -293,35 +411,159 @@ 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)
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 bounding box of the current item"""
return obj.centreOfMass()
prop = OCC.GProp.GProp_GProps()
OCC.BRepGProp.brepgprop_VolumeProperties(obj, prop)
return prop.CentreOfMass()
CentreOfMass = CenterOfMass
def Bbox():
"""Return the bounding box of the current item"""
return obj.boundingBox()
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 occmodel.EdgeIterator(Object())
return visit(Object(), OCC.TopAbs.TopAbs_EDGE, OCC.TopoDS.topods.Edge)
def Faces():
"""Return the face iterator of the current item"""
return occmodel.FaceIterator(Object())
return visit(Object(), OCC.TopAbs.TopAbs_FACE, OCC.TopoDS.topods.Face)
def Vertices():
"""Return the vertex iterator of the current item"""
return occmodel.VertexIterator(Object())
return visit(Object(), OCC.TopAbs.TopAbs_VERTEX, OCC.TopoDS.topods.Vertex)
def Wires():
"""Return the wire iterator of the current item"""
return occmodel.WireIterator(Object())
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()

67
pocview
View file

@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/python2
# -*- coding: utf-8 -*-
# auto-updating viewer of 'poc' modeling program
# Copyright © 2017 Jeff Epler <jepler@gmail.com>
@ -17,13 +17,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from occmodelviewer import Viewer
import gltools
import geotools
import os
import poctools
import sys
import time
import traceback
import vtk
filename = sys.argv[1]
@ -35,46 +34,48 @@ def getmtime(filename):
class PocViewer(Viewer):
def __init__(self, filename):
title = "poc viewer - 'r' to reload, 'q' to quit"
Viewer.__init__(self, 640, 480, title, fullscreen=False)
self.defaultColor = gltools.ColorRGBA(100,100,100,255)
self.edgeColor = gltools.ColorRGBA(255,255,255,255)
self.filename = filename
self.reloadModel()
self.modtime = -2
self.actor = vtk.vtkActor()
self.ren = vtk.vtkRenderer()
self.renWin = vtk.vtkRenderWindow()
self.renWin.AddRenderer(self.ren)
def onChar(self, ch):
if ch == 'r': self.reloadModel()
if ch == 'q': self.running = False
self.iren = vtk.vtkRenderWindowInteractor()
self.iren.SetRenderWindow(self.renWin)
self.ren.AddActor(self.actor)
self.iren.Initialize()
self.iren.CreateRepeatingTimer(100)
self.iren.AddObserver('TimerEvent', self.idle)
def Start(self):
self.reloadModel()
self.iren.Start()
def reloadModel(self, modtime=None):
self.modtime = modtime or getmtime(self.filename)
try:
poctools.execpoc(sys.argv[1:])
ns = poctools.execpoc(sys.argv[1:],
__output__= os.path.splitext(filename)[0] + ".stl")
poctools.output(ns['__output__'])
except:
traceback.print_exc()
return
self.clear()
o = poctools.obj
bb = o.boundingBox()
if bb.min != bb.max:
self.add(o)
reader = vtk.vtkSTLReader()
reader.SetFileName(ns['__output__'])
# Viewer.mainLoop seems to have no provision for code to run when
# idle, so we are forced to do this..
def mainLoop(self):
while self.running:
time.sleep(1/120.)
gltools.PollEvents()
newmodtime = getmtime(self.filename)
now = time.time()
if newmodtime + .1 > now and newmodtime != self.modtime:
self.reloadModel(newmodtime)
self.updateBounds()
self.redraw()
self.mapper = vtk.vtkPolyDataMapper()
self.mapper.SetInputConnection(reader.GetOutputPort())
self.actor.SetMapper(self.mapper)
def idle(self, obj, event):
newmodtime = getmtime(self.filename)
now = time.time()
if newmodtime + .1 < now and newmodtime != self.modtime:
self.reloadModel(newmodtime)
self.renWin.Render()
mw = PocViewer(filename)
mw.running = True
mw.onIsoView()
mw.mainLoop()
mw.Start()