1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
|
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
Converts MCDISPLAY geometry information, read from an instrument binary, to STEP format.
Uses the CadQuery package, https://cadquery.readthedocs.io, and its interface to
OpenCASCADE to handle some operations, most importantly, the saving to STEP file.
'''
__author__ = "Gregory Tucker"
__date__ = "2022-11-11"
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).resolve().parent.parent.parent))
from mccodelib.instrgeom import Vector3d, DrawLine, DrawMultiline, DrawCircle
def vector_to_tuple(v: Vector3d):
return v.x, v.y, v.z
def transform_shape(global_transformation_matrix, local_shape):
"""Perform the coordinate transformation for a shape
>>> placed = transform_shape(global_transformation_matrix, local_shape)
Including handling of various possible input shape types and transformation types
"""
# Import types which the global transformation matrix could be:
from cadquery.occ_impl.geom import Matrix
from OCP.gp import gp_Trsf, gp_GTrsf # plus more transformatation types?
# Import types which the local shape could be:
from cadquery.occ_impl.shapes import Compound, Shape
from cadquery.cq import Workplane
from OCP.TopoDS import TopoDS_Compound, TopoDS_Shape
# Get a handle to the underlying OpenCascade shape object, depending on what the input type is
global_shape = None
if isinstance(local_shape, Workplane):
global_shape = local_shape.toOCC()
elif isinstance(local_shape, (Shape, Compound)):
global_shape = local_shape.wrapped
elif insinstance(local_shape, (TopoDS_Shape, TopoDS_Compound)):
# We are already a suitable type for local to global transformation
global_shape = local_shape
else:
print(f"Unknown shape type {type(local_shape)}, attempting the 'wrapped' property, expect errors!")
global_shape = local_shape.wrapped
transformation = None
if isinstance(global_transformation_matrix, Matrix):
transformation = global_transformation_matrix.wrapped
elif isinstance(global_transformation_matrix, (gp_Trsf, gp_GTrsf)):
# Already a suitable transformation type
transformation = global_transformation_matrix
else:
gtmt = type(global_transformation_matrix)
print(f"Unknown transformation implementation type {gtmt}, attemping the 'wrapped' property, expect errors!")
transformation = global_transformation_matrix.wrapped
# Now decide which transformation algorithm can be used based on what the input transformation represents
if isinstance(transformation, gp_Trsf):
# This is a unitary transformation and can only reposition/reorient the shape:
from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform as Trans
global_shape = Trans(global_shape, transformation).Shape()
elif isinstance(transformation, gp_GTrsf):
# This is a general transformation which can additionally rescale/stretch/skew the shape:
from OCP.BRepBuilderAPI import BRepBuilderAPI_GTransform as GTrans
global_shape = GTrans(global_shape, transformation).Shape()
else:
print(f"Unknown transformation type {type(matrix)}. No transformation applied!")
# (Re)wrap the OpenCascade shape back to a cadquery.Shape
shape = Shape(global_shape)
return shape
def circle_to_brep(comp: DrawCircle):
from cadquery import Workplane
plane = comp.plane.upper()
center = vector_to_tuple(comp.center)
radius = comp.radius # / meter
w = Workplane(plane, origin=center).circle(radius)
return w
def multiline_to_brep(comp: DrawMultiline):
from cadquery import Workplane
vs = [vector_to_tuple(x) for x in comp.points]
bad = [vs[i] == vs[i+1] for i in range(len(vs)-1)]
while any(bad):
first = [idx for idx, b in enumerate(bad) if b][0]
vs = vs[:first] + vs[first+1:]
bad = [vs[i] == vs[i+1] for i in range(len(vs)-1)]
w = None
if len(vs) > 1:
w = Workplane('XY').polyline(vs)
return w
def drawcalls_to_shape(drawcalls):
from cadquery import Assembly
a = Assembly()
for d in drawcalls:
if isinstance(d, DrawCircle):
brep = circle_to_brep(d)
elif isinstance(d, (DrawLine, DrawMultiline)):
brep = multiline_to_brep(d)
else:
print(f"Drawing of {type(d)} not supported; please extend {__file__}!")
print('\n'.join([f'\t{x}' for x in dir(d) if x[0] != '_']))
brep = None
if brep is not None:
a = a.add(brep)
a = a.toCompound()
return a
def component_to_brep(comp):
from cadquery import Matrix
orient = Matrix([[comp.m4[i+j*4] for i in range(4)] for j in range(3)])
# A more-complex system would check if the draw calls of the component are, e.g.,
# - three perpendicular circles sharing center and radius -> a sphere
# - two circles connected by lines -> a cyllinder
# - any number of lines forming closed faces
# - any number of closed faces forming closed shells
return transform_shape(orient, drawcalls_to_shape(comp.drawcalls))
def instrument_to_assembly(inst):
from cadquery import Assembly
a = Assembly()
for component in inst.components:
a= a.add(component_to_brep(component), name = component.name)
return a
def main(instr=None, dirname=None, **kwds):
from logging import basicConfig, INFO
basicConfig(level=INFO)
# output directory
if dirname is None:
from datetime import datetime as dt
p = Path(instr).resolve()
dirname = str(p.parent.joinpath(f"{p.stem}_{dt.strftime(dt.now(), '%Y%m%d_%H%M%S')}"))
from mccodelib.mcdisplayutils import McDisplayReader
reader = McDisplayReader(dir=dirname, instr=instr, **kwds)
instrument = reader.read_instrument()
# The reader fails if this is done before reader.read_instrument() above ?!
root = Path(dirname)
root.mkdir(parents=True, exist_ok=True)
# Do the conversion to a CadQuery assembly, then save it to the desired location
assembly = instrument_to_assembly(instrument)
assembly.save(str(root.joinpath(f"{Path(instr).stem}.{kwds.get('format','step')}")))
def output_format(astring):
f = astring.lower()
if f in ('step', 'stl', 'xml', 'vrml', 'gltf', 'vtkjs'):
return f
print(f"Unknown file format {astring}. Choose between STEP, STL, VRML, GLTF, VTKJS, and OpenCASCADE Technology XML")
return 'step'
if __name__ == '__main__':
from mccodelib.mcdisplayutils import make_common_parser
# Only pre-sets instr, --default, options
parser, prefix = make_common_parser(__file__, __doc__)
parser.add_argument('--dirname', help='output directory name override')
parser.add_argument('-n', '--ncount', dest='n', type=float, default=0, help='Number of particles to simulate')
parser.add_argument('-f', '--format', dest='format', type=output_format, default='step', help='Output CAD file format')
args, unknown = parser.parse_known_args()
# if --inspect --first or --last are given after instr, the remaining args become "unknown",
# but we assume that they are instr_options
args = {k: args.__getattribute__(k) for k in dir(args) if k[0] != '_'}
if len(unknown):
args['options'] = unknown
main(**args)
|