├── .gitignore ├── .idea ├── inspectionProfiles │ └── profiles_settings.xml ├── kodacad.iml ├── misc.xml ├── modules.xml └── vcs.xml ├── LICENSE ├── OCCUtils ├── Common.py ├── Construct.py ├── Image.py ├── Iteration.py ├── Topology.py ├── __init__.py ├── __pycache__ │ ├── Common.cpython-39.pyc │ ├── Construct.cpython-39.pyc │ ├── Topology.cpython-39.pyc │ ├── __init__.cpython-39.pyc │ └── types_lut.cpython-39.pyc ├── base.py ├── edge.py ├── face.py ├── shell.py ├── solid.py ├── types_lut.py ├── vertex.py └── wire.py ├── README.md ├── __pycache__ ├── docmodel.cpython-39.pyc ├── m2d.cpython-39.pyc ├── mainwindow.cpython-39.pyc ├── rpnCalculator.cpython-39.pyc ├── version.cpython-39.pyc └── workplane.cpython-39.pyc ├── docmodel.py ├── docs ├── assembly_structure │ ├── assembly_structure.md │ └── images │ │ ├── as1-loaded-under-top.png │ │ ├── button_created.png │ │ ├── components.png │ │ └── ready_to_create_button.png ├── bottle_tutorial │ ├── images │ │ ├── bot1.png │ │ ├── bot2.png │ │ ├── bot3.png │ │ ├── bot4.png │ │ ├── bot5.png │ │ ├── wp1.png │ │ ├── wp2.png │ │ ├── wp3.png │ │ ├── wp4.png │ │ ├── wp5.png │ │ ├── wp6.png │ │ ├── wp7.png │ │ └── wp8.png │ └── index.html ├── images │ ├── bottle.png │ ├── img8.png │ ├── ui.png │ └── wp.png ├── index.html ├── index.html~ └── load_mod_save_demo │ ├── images │ ├── img1.png │ ├── img2.png │ ├── img3.png │ ├── img4.png │ ├── img5.png │ ├── img6.png │ ├── img7.png │ ├── img8.png │ └── img9.png │ ├── index.html │ └── index.html~ ├── icons ├── abcl.gif ├── acl.gif ├── arc3p.gif ├── arcc2p.gif ├── array.gif ├── cc3p.gif ├── cccirc.gif ├── ccirc.gif ├── cctan2.gif ├── cctan3.gif ├── circ.gif ├── cltan1.gif ├── cltan2.gif ├── del_c.gif ├── del_cel.gif ├── del_el.gif ├── del_g.gif ├── fillet.gif ├── hcl.gif ├── hvcl.gif ├── join.gif ├── lbcl.gif ├── line.gif ├── parcl.gif ├── perpcl.gif ├── poly.gif ├── rect.gif ├── refangcl.gif ├── rotate.gif ├── sep.gif ├── slot.gif ├── split.gif ├── stretch.gif ├── tpcl.gif ├── translate.gif └── vcl.gif ├── kodacad.py ├── m2d.py ├── mainwindow.py ├── myDisplay ├── OCCViewer.py ├── Readme.txt ├── SimpleGui.py ├── __init__.py ├── backend.py ├── icons │ ├── cursor-magnify-area.png │ ├── cursor-magnify.png │ ├── cursor-pan.png │ └── cursor-rotate.png ├── qtDisplay.py └── wxDisplay.py ├── rpnCalculator.py ├── save_files ├── as1-oc-214-at_top.xbf ├── as1-oc-214-under_top.xbf └── rt.xbf ├── step ├── Bottle.stp ├── as1-oc-214.stp └── as1_pe_203.stp ├── stepanalyzer.py ├── version.py └── workplane.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | __pycache__/* 3 | foo* 4 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/kodacad.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /OCCUtils/Image.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ##Copyright 2008-2015 Thomas Paviot (tpaviot@gmail.com) 4 | ## 5 | ##This file is part of pythonOCC. 6 | ## 7 | ##pythonOCC is free software: you can redistribute it and/or modify 8 | ##it under the terms of the GNU Lesser General Public License as published by 9 | ##the Free Software Foundation, either version 3 of the License, or 10 | ##(at your option) any later version. 11 | ## 12 | ##pythonOCC is distributed in the hope that it will be useful, 13 | ##but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ##GNU Lesser General Public License for more details. 16 | ## 17 | ##You should have received a copy of the GNU Lesser General Public License 18 | ##along with pythonOCC. If not, see . 19 | 20 | import os 21 | import os.path 22 | 23 | 24 | class Texture(object): 25 | """ 26 | This class encapsulates the necessary texture properties: 27 | Filename, toScaleU, etc. 28 | """ 29 | 30 | def __init__(self, filename): 31 | if not os.path.isfile(filename): 32 | raise IOError("File %s not found.\n" % filename) 33 | self._filename = filename 34 | self._toScaleU = 1.0 35 | self._toScaleV = 1.0 36 | self._toRepeatU = 1.0 37 | self._toRepeatV = 1.0 38 | self._originU = 0.0 39 | self._originV = 0.0 40 | 41 | def TextureScale(self, toScaleU, toScaleV): 42 | self._toScaleU = toScaleU 43 | self._toScaleV = toScaleV 44 | 45 | def TextureRepeat(self, toRepeatU, toRepeatV): 46 | self._toRepeatU = toRepeatU 47 | self._toRepeatV = toRepeatV 48 | 49 | def TextureOrigin(self, originU, originV): 50 | self._originU = originU 51 | self._originV = originV 52 | 53 | def GetProperties(self): 54 | return ( 55 | self._filename, 56 | self._toScaleU, 57 | self._toScaleV, 58 | self._toRepeatU, 59 | self._toRepeatV, 60 | self._originU, 61 | self._originV, 62 | ) 63 | -------------------------------------------------------------------------------- /OCCUtils/Iteration.py: -------------------------------------------------------------------------------- 1 | ##Copyright 2008-2015 Jelle Feringa (jelleferinga@gmail.com) 2 | ## 3 | ##This file is part of pythonOCC. 4 | ## 5 | ##pythonOCC is free software: you can redistribute it and/or modify 6 | ##it under the terms of the GNU Lesser General Public License as published by 7 | ##the Free Software Foundation, either version 3 of the License, or 8 | ##(at your option) any later version. 9 | ## 10 | ##pythonOCC is distributed in the hope that it will be useful, 11 | ##but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ##GNU Lesser General Public License for more details. 14 | ## 15 | ##You should have received a copy of the GNU Lesser General Public License 16 | ##along with pythonOCC. If not, see . 17 | 18 | """ 19 | This module helps looping through topology 20 | """ 21 | from OCC.Core.BRep import BRep_Tool 22 | 23 | from OCCUtils.Topology import WireExplorer, Topo 24 | from OCCUtils.edge import Edge 25 | 26 | 27 | class EdgePairsFromWire(object): 28 | """ 29 | helper class to loop through a wire and return ordered pairs of edges 30 | """ 31 | 32 | def __init__(self, wire): 33 | self.wire = wire 34 | self.edge_pairs = [] 35 | self.prev_edge = None 36 | self.we = WireExplorer(self.wire).ordered_edges() 37 | self.number_of_edges = self.we.__length_hint__() 38 | self.previous_edge = None 39 | self.current_edge = None 40 | self.first_edge = None 41 | self.index = 0 42 | 43 | def next(self): 44 | if self.index == 0: 45 | # first edge, need to set self.previous_edge 46 | self.previous_edge = next(self.we) 47 | self.current_edge = next(self.we) 48 | self.first_edge = self.previous_edge # for the last iteration 49 | self.index += 1 50 | return [self.previous_edge, self.current_edge] 51 | elif self.index == self.number_of_edges - 1: 52 | # no next edge 53 | self.index += 1 54 | return [self.current_edge, self.first_edge] 55 | else: 56 | self.previous_edge = self.current_edge 57 | self.current_edge = next(self.we) 58 | self.index += 1 59 | return [self.previous_edge, self.current_edge] 60 | 61 | def __iter__(self): 62 | return self 63 | 64 | 65 | class LoopWirePairs(object): 66 | """ 67 | for looping through consequtive wires 68 | assures that the returned edge pairs are ordered 69 | """ 70 | 71 | def __init__(self, wireA, wireB): 72 | self.wireA = wireA 73 | self.wireB = wireB 74 | self.we_A = WireExplorer(self.wireA) 75 | self.we_B = WireExplorer(self.wireB) 76 | self.tp_A = Topo(self.wireA) 77 | self.tp_B = Topo(self.wireB) 78 | self.bt = BRep_Tool() 79 | self.vertsA = [v for v in self.we_A.ordered_vertices()] 80 | self.vertsB = [v for v in self.we_B.ordered_vertices()] 81 | 82 | self.edgesA = [v for v in WireExplorer(wireA).ordered_edges()] 83 | self.edgesB = [v for v in WireExplorer(wireB).ordered_edges()] 84 | 85 | self.pntsB = [self.bt.Pnt(v) for v in self.vertsB] 86 | self.number_of_vertices = len(self.vertsA) 87 | self.index = 0 88 | 89 | def closest_point(self, vertexFromWireA): 90 | pt = self.bt.Pnt(vertexFromWireA) 91 | distances = [pt.Distance(i) for i in self.pntsB] 92 | indx_max_dist = distances.index(min(distances)) 93 | return self.vertsB[indx_max_dist] 94 | 95 | def next(self): 96 | if self.index == self.number_of_vertices: 97 | raise StopIteration 98 | 99 | vert = self.vertsA[self.index] 100 | closest = self.closest_point(vert) 101 | edges_a = self.tp_A.edges_from_vertex(vert) 102 | edges_b = self.tp_B.edges_from_vertex(closest) 103 | a1, a2 = Edge(next(edges_a)), Edge(next(edges_a)) 104 | b1, b2 = Edge(next(edges_b)), Edge(next(edges_b)) 105 | mpA = a1.mid_point() 106 | self.index += 1 107 | 108 | if mpA.Distance(b1.mid_point()) < mpA.Distance(b2.mid_point()): 109 | return iter([a1, a2]), iter([b1, b2]) 110 | else: 111 | return iter([a1, a2]), iter([b2, b1]) 112 | 113 | def __iter__(self): 114 | return self 115 | -------------------------------------------------------------------------------- /OCCUtils/__init__.py: -------------------------------------------------------------------------------- 1 | from OCCUtils.Common import get_boundingbox 2 | from OCCUtils.Topology import Topo 3 | -------------------------------------------------------------------------------- /OCCUtils/__pycache__/Common.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/OCCUtils/__pycache__/Common.cpython-39.pyc -------------------------------------------------------------------------------- /OCCUtils/__pycache__/Construct.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/OCCUtils/__pycache__/Construct.cpython-39.pyc -------------------------------------------------------------------------------- /OCCUtils/__pycache__/Topology.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/OCCUtils/__pycache__/Topology.cpython-39.pyc -------------------------------------------------------------------------------- /OCCUtils/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/OCCUtils/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /OCCUtils/__pycache__/types_lut.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/OCCUtils/__pycache__/types_lut.cpython-39.pyc -------------------------------------------------------------------------------- /OCCUtils/base.py: -------------------------------------------------------------------------------- 1 | ##Copyright 2008-2013 Jelle Feringa (jelleferinga@gmail.com) 2 | ## 3 | ##This file is part of pythonOCC. 4 | ## 5 | ##pythonOCC is free software: you can redistribute it and/or modify 6 | ##it under the terms of the GNU Lesser General Public License as published by 7 | ##the Free Software Foundation, either version 3 of the License, or 8 | ##(at your option) any later version. 9 | ## 10 | ##pythonOCC is distributed in the hope that it will be useful, 11 | ##but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ##GNU Lesser General Public License for more details. 14 | ## 15 | ##You should have received a copy of the GNU Lesser General Public License 16 | ##along with pythonOCC. If not, see 17 | 18 | """ 19 | Please note the following; 20 | @readonly 21 | means that the decorated method is a readonly descriptor 22 | @property 23 | means that the decorated method can be set / get using the descriptor 24 | ( irony is that just using @property *would* 25 | result in a readonly descriptor :") 26 | 27 | Sometimes a set of methods should be contained in another module or class, 28 | or simply grouped. 29 | For instance the set of methods after: 30 | #=========================================================================== 31 | # Curve.local_properties 32 | #=========================================================================== 33 | 34 | Can be a module, class or namespace. 35 | 36 | """ 37 | 38 | import functools 39 | 40 | from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Copy 41 | from OCC.Core.BRepGProp import ( 42 | brepgprop_VolumeProperties, 43 | brepgprop_LinearProperties, 44 | brepgprop_SurfaceProperties, 45 | ) 46 | from OCC.Core.BRepCheck import ( 47 | BRepCheck_Vertex, 48 | BRepCheck_Edge, 49 | BRepCheck_Wire, 50 | BRepCheck_Face, 51 | BRepCheck_Shell, 52 | BRepCheck_Analyzer, 53 | ) 54 | from OCC.Core.GProp import GProp_GProps 55 | from OCC.Display.SimpleGui import init_display 56 | 57 | from OCCUtils.Common import get_boundingbox 58 | from OCCUtils.Construct import make_vertex, TOLERANCE 59 | from OCCUtils.types_lut import shape_lut, topo_lut, curve_lut, surface_lut 60 | 61 | # =========================================================================== 62 | # DISPLAY 63 | # =========================================================================== 64 | global display 65 | 66 | 67 | class singleton(object): 68 | def __init__(self, cls): 69 | self.cls = cls 70 | self.instance_container = [] 71 | 72 | def __call__(self, *args, **kwargs): 73 | if not len(self.instance_container): 74 | cls = functools.partial(self.cls, *args, **kwargs) 75 | self.instance_container.append(cls()) 76 | return self.instance_container[0] 77 | 78 | 79 | @singleton 80 | class Display(object): 81 | def __init__(self): 82 | ( 83 | self.display, 84 | self.start_display, 85 | self.add_menu, 86 | self.add_function_to_menu, 87 | ) = init_display() 88 | 89 | def __call__(self, *args, **kwargs): 90 | return self.display.DisplayShape(*args, **kwargs) 91 | 92 | 93 | # ============ 94 | # base class 95 | # ============ 96 | 97 | 98 | class BaseObject(object): 99 | """base class for all objects""" 100 | 101 | def __init__(self, name=None, tolerance=TOLERANCE): 102 | self.GlobalProperties = GlobalProperties(self) 103 | self.name = name 104 | self._dirty = False 105 | self.tolerance = tolerance 106 | self.display_set = False 107 | 108 | @property 109 | def is_dirty(self): 110 | """when an object is dirty, its topology will be 111 | rebuild when update is called""" 112 | return self._dirty 113 | 114 | @is_dirty.setter 115 | def is_dirty(self, _bool): 116 | self._dirty = bool(_bool) 117 | 118 | @property 119 | def topo_type(self): 120 | return topo_lut[self.ShapeType()] 121 | 122 | @property 123 | def geom_type(self): 124 | if self.topo_type == "edge": 125 | return curve_lut[self.ShapeType()] 126 | if self.topo_type == "face": 127 | return surface_lut[self.adaptor.GetType()] 128 | else: 129 | raise ValueError("geom_type works only for edges and faces...") 130 | 131 | def set_display(self, display): 132 | if hasattr(display, "DisplayShape"): 133 | self.display_set = True 134 | self.display = display 135 | else: 136 | raise ValueError("not a display") 137 | 138 | def check(self): 139 | """ """ 140 | _check = dict( 141 | vertex=BRepCheck_Vertex, 142 | edge=BRepCheck_Edge, 143 | wire=BRepCheck_Wire, 144 | face=BRepCheck_Face, 145 | shell=BRepCheck_Shell, 146 | ) 147 | _check[self.topo_type] 148 | # TODO: BRepCheck will be able to inform *what* actually is the matter, 149 | # though implementing this still is a bit of work... 150 | raise NotImplementedError 151 | 152 | def is_valid(self): 153 | analyse = BRepCheck_Analyzer(self) 154 | ok = analyse.IsValid() 155 | if ok: 156 | return True 157 | else: 158 | return False 159 | 160 | def copy(self): 161 | """ 162 | 163 | :return: 164 | """ 165 | cp = BRepBuilderAPI_Copy(self) 166 | cp.Perform(self) 167 | # get the class, construct a new instance 168 | # cast the cp.Shape() to its specific TopoDS topology 169 | _copy = self.__class__(shape_lut(cp.Shape())) 170 | return _copy 171 | 172 | def distance(self, other): 173 | """ 174 | return the minimum distance 175 | 176 | :return: minimum distance, 177 | minimum distance points on shp1 178 | minimum distance points on shp2 179 | """ 180 | return minimum_distance(self, other) 181 | 182 | def show(self, *args, **kwargs): 183 | """ 184 | renders the topological entity in the viewer 185 | 186 | :param update: redraw the scene or not 187 | """ 188 | if not self.display_set: 189 | Display()(self, *args, **kwargs) 190 | else: 191 | self.disp.DisplayShape(*args, **kwargs) 192 | 193 | def build(self): 194 | if self.name.startswith("Vertex"): 195 | self = make_vertex(self) 196 | 197 | def __eq__(self, other): 198 | return self.IsEqual(other) 199 | 200 | def __ne__(self, other): 201 | return not self.__eq__(other) 202 | 203 | 204 | class GlobalProperties(object): 205 | """ 206 | global properties for all topologies 207 | """ 208 | 209 | def __init__(self, instance): 210 | self.instance = instance 211 | 212 | @property 213 | def system(self): 214 | self._system = GProp_GProps() 215 | # todo, type should be abstracted with TopoDS... 216 | _topo_type = self.instance.topo_type 217 | if _topo_type == "face" or _topo_type == "shell": 218 | brepgprop_SurfaceProperties(self.instance, self._system) 219 | elif _topo_type == "edge": 220 | brepgprop_LinearProperties(self.instance, self._system) 221 | elif _topo_type == "solid": 222 | brepgprop_VolumeProperties(self.instance, self._system) 223 | return self._system 224 | 225 | def centre(self): 226 | """ 227 | :return: centre of the entity 228 | """ 229 | return self.system.CentreOfMass() 230 | 231 | def inertia(self): 232 | """returns the inertia matrix""" 233 | return self.system.MatrixOfInertia(), self.system.MomentOfInertia() 234 | 235 | def area(self): 236 | """returns the area of the surface""" 237 | return self.system.Mass() 238 | 239 | def bbox(self): 240 | """ 241 | returns the bounding box of the face 242 | """ 243 | return get_boundingbox(self.instance) 244 | -------------------------------------------------------------------------------- /OCCUtils/edge.py: -------------------------------------------------------------------------------- 1 | ##Copyright 2008-2015 Jelle Feringa (jelleferinga@gmail.com) 2 | ## 3 | ##This file is part of pythonOCC. 4 | ## 5 | ##pythonOCC is free software: you can redistribute it and/or modify 6 | ##it under the terms of the GNU Lesser General Public License as published by 7 | ##the Free Software Foundation, either version 3 of the License, or 8 | ##(at your option) any later version. 9 | ## 10 | ##pythonOCC is distributed in the hope that it will be useful, 11 | ##but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ##GNU Lesser General Public License for more details. 14 | ## 15 | ##You should have received a copy of the GNU Lesser General Public License 16 | ##along with pythonOCC. If not, see 17 | 18 | from OCC.Core.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Curve 19 | from OCC.Core.GCPnts import GCPnts_UniformAbscissa 20 | from OCC.Core.Geom import Geom_OffsetCurve, Geom_TrimmedCurve 21 | from OCC.Core.TopExp import topexp 22 | from OCC.Core.TopoDS import TopoDS_Edge, TopoDS_Vertex, TopoDS_Face 23 | from OCC.Core.gp import gp_Vec, gp_Dir, gp_Pnt 24 | from OCC.Core.GeomLProp import GeomLProp_CurveTool 25 | from OCC.Core.BRepLProp import BRepLProp_CLProps 26 | from OCC.Core.GeomLib import geomlib 27 | from OCC.Core.GCPnts import GCPnts_AbscissaPoint 28 | from OCC.Core.GeomAPI import GeomAPI_ProjectPointOnCurve 29 | from OCC.Core.ShapeAnalysis import ShapeAnalysis_Edge 30 | from OCC.Core.BRep import BRep_Tool, BRep_Tool_Continuity 31 | from OCC.Core.BRepIntCurveSurface import BRepIntCurveSurface_Inter 32 | 33 | # high-level 34 | from OCCUtils.Common import vertex2pnt, minimum_distance, assert_isdone, fix_continuity 35 | from OCCUtils.Construct import make_edge 36 | from OCCUtils.types_lut import geom_lut 37 | from OCCUtils.base import BaseObject 38 | 39 | 40 | class IntersectCurve(object): 41 | def __init__(self, instance): 42 | self.instance = instance 43 | 44 | def intersect(self, other, tolerance=1e-2): 45 | """Intersect self with a point, curve, edge, face, solid 46 | method wraps dealing with the various topologies 47 | """ 48 | if isinstance(other, TopoDS_Face): 49 | face_curve_intersect = BRepIntCurveSurface_Inter() 50 | face_curve_intersect.Init(other, self.instance.adaptor.Curve(), tolerance) 51 | pnts = [] 52 | while face_curve_intersect.More(): 53 | next(face_curve_intersect) 54 | pnts.append(face_curve_intersect.Pnt()) 55 | return pnts 56 | 57 | 58 | class DiffGeomCurve(object): 59 | def __init__(self, instance): 60 | self.instance = instance 61 | self._local_props = BRepLProp_CLProps( 62 | self.instance.adaptor, 2, self.instance.tolerance 63 | ) 64 | 65 | @property 66 | def _curvature(self): 67 | return self._local_props 68 | 69 | def radius(self, u): 70 | """returns the radius at u""" 71 | # NOT SO SURE IF THIS IS THE SAME THING!!! 72 | self._curvature.SetParameter(u) 73 | pnt = gp_Pnt() 74 | self._curvature.CentreOfCurvature(pnt) 75 | return pnt 76 | 77 | def curvature(self, u): 78 | # ugly 79 | self._curvature.SetParameter(u) 80 | return self._curvature.Curvature() 81 | 82 | def tangent(self, u): 83 | """sets or gets ( iff vector ) the tangency at the u parameter 84 | tangency can be constrained so when setting the tangency, 85 | you're constrainting it in fact 86 | """ 87 | self._curvature.SetParameter(u) 88 | if self._curvature.IsTangentDefined(): 89 | ddd = gp_Dir() 90 | self._curvature.Tangent(ddd) 91 | return ddd 92 | else: 93 | raise ValueError("no tangent defined") 94 | 95 | def normal(self, u): 96 | """returns the normal at u 97 | 98 | computes the main normal if no normal is found 99 | see: 100 | www.opencascade.org/org/forum/thread_645+&cd=10&hl=nl&ct=clnk&gl=nl 101 | """ 102 | try: 103 | self._curvature.SetParameter(u) 104 | a_dir = gp_Dir() 105 | self._curvature.Normal(a_dir) 106 | return a_dir 107 | except: 108 | raise ValueError("no normal was found") 109 | 110 | def derivative(self, u, n): 111 | """ 112 | returns n derivatives at parameter b 113 | """ 114 | self._curvature.SetParameter(u) 115 | deriv = { 116 | 1: self._curvature.D1, 117 | 2: self._curvature.D2, 118 | 3: self._curvature.D3, 119 | } 120 | try: 121 | return deriv[n] 122 | except KeyError: 123 | raise AssertionError("n of derivative is one of [1,2,3]") 124 | 125 | def points_from_tangential_deflection(self): 126 | pass 127 | 128 | 129 | # =========================================================================== 130 | # Curve.Construct 131 | # =========================================================================== 132 | 133 | 134 | class ConstructFromCurve: 135 | def __init__(self, instance): 136 | self.instance = instance 137 | 138 | def make_offset(self, offset, vec): 139 | """ 140 | returns an offsetted curve 141 | @param offset: the distance between self.crv and the curve to offset 142 | @param vec: offset direction 143 | """ 144 | return Geom_OffsetCurve(self.instance.h_crv, offset, vec) 145 | 146 | 147 | class Edge(TopoDS_Edge, BaseObject): 148 | def __init__(self, edge): 149 | assert isinstance(edge, TopoDS_Edge), ( 150 | "need a TopoDS_Edge, got a %s" % edge.__class__ 151 | ) 152 | assert not edge.IsNull() 153 | super(Edge, self).__init__() 154 | BaseObject.__init__(self, "edge") 155 | # we need to copy the base shape using the following three 156 | # lines 157 | assert self.IsNull() 158 | self.TShape(edge.TShape()) 159 | self.Location(edge.Location()) 160 | self.Orientation(edge.Orientation()) 161 | assert not self.IsNull() 162 | 163 | # tracking state 164 | self._local_properties_init = False 165 | self._curvature_init = False 166 | self._geometry_lookup_init = False 167 | self._curve = None 168 | self._adaptor = None 169 | 170 | # instantiating cooperative classes 171 | # cooperative classes are distinct through CamelCaps from 172 | # normal method -> pep8 173 | self.DiffGeom = DiffGeomCurve(self) 174 | self.Intersect = IntersectCurve(self) 175 | self.Construct = ConstructFromCurve(self) 176 | 177 | # GeomLProp object 178 | self._curvature = None 179 | 180 | def is_closed(self): 181 | return self.adaptor.IsClosed() 182 | 183 | def is_periodic(self): 184 | return self.adaptor.IsPeriodic() 185 | 186 | def is_rational(self): 187 | return self.adaptor.IsRational() 188 | 189 | def continuity(self): 190 | return self.adaptor.Continuity 191 | 192 | def degree(self): 193 | if "line" in self.type: 194 | return 1 195 | elif "curve" in self.type: 196 | return self.adaptor.Degree() 197 | else: 198 | # hyperbola, parabola, circle 199 | return 2 200 | 201 | def nb_knots(self): 202 | return self.adaptor.NbKnots() 203 | 204 | def nb_poles(self): 205 | return self.adaptor.NbPoles() 206 | 207 | @property 208 | def curve(self): 209 | if self._curve is not None and not self.is_dirty: 210 | pass 211 | else: 212 | self._curve = BRep_Tool().Curve(self)[0] 213 | return self._curve 214 | 215 | @property 216 | def adaptor(self): 217 | if self._adaptor is not None and not self.is_dirty: 218 | pass 219 | else: 220 | self._adaptor = BRepAdaptor_Curve(self) 221 | return self._adaptor 222 | 223 | @property 224 | def type(self): 225 | return geom_lut[self.adaptor.Curve().GetType()] 226 | 227 | def pcurve(self, face): 228 | """ 229 | computes the 2d parametric spline that lies on the surface of the face 230 | :return: Geom2d_Curve, u, v 231 | """ 232 | crv, u, v = BRep_Tool().CurveOnSurface(self, face) 233 | return crv, u, v 234 | 235 | def _local_properties(self): 236 | self._lprops_curve_tool = GeomLProp_CurveTool() 237 | self._local_properties_init = True 238 | 239 | def domain(self): 240 | """returns the u,v domain of the curve""" 241 | return self.adaptor.FirstParameter(), self.adaptor.LastParameter() 242 | 243 | # =========================================================================== 244 | # Curve.GlobalProperties 245 | # =========================================================================== 246 | 247 | def length(self, lbound=None, ubound=None, tolerance=1e-5): 248 | """returns the curve length 249 | if either lbound | ubound | both are given, than the length 250 | of the curve will be measured over that interval 251 | """ 252 | _min, _max = self.domain() 253 | if _min < self.adaptor.FirstParameter(): 254 | raise ValueError( 255 | "the lbound argument is lower than the first parameter of the curve: %s " 256 | % (self.adaptor.FirstParameter()) 257 | ) 258 | if _max > self.adaptor.LastParameter(): 259 | raise ValueError( 260 | "the ubound argument is greater than the last parameter of the curve: %s " 261 | % (self.adaptor.LastParameter()) 262 | ) 263 | 264 | lbound = _min if lbound is None else lbound 265 | ubound = _max if ubound is None else ubound 266 | return GCPnts_AbscissaPoint().Length(self.adaptor, lbound, ubound, tolerance) 267 | 268 | # =========================================================================== 269 | # Curve.modify 270 | # =========================================================================== 271 | 272 | def trim(self, lbound, ubound): 273 | """ 274 | trim the curve 275 | @param lbound: 276 | @param ubound: 277 | """ 278 | a, b = sorted([lbound, ubound]) 279 | tr = Geom_TrimmedCurve(self.adaptor.Curve().Curve(), a, b) 280 | return Edge(make_edge(tr)) 281 | 282 | def extend_by_point(self, pnt, degree=3, beginning=True): 283 | """extends the curve to point 284 | 285 | does not extend if the degree of self.curve > 3 286 | @param pnt: 287 | @param degree: 288 | @param beginning: 289 | """ 290 | if self.degree > 3: 291 | raise ValueError( 292 | "to extend you self.curve should be <= 3, is %s" % (self.degree) 293 | ) 294 | return geomlib.ExtendCurveToPoint(self.curve, pnt, degree, beginning) 295 | 296 | # =========================================================================== 297 | # Curve. 298 | # =========================================================================== 299 | def closest(self, other): 300 | return minimum_distance(self, other) 301 | 302 | def project_vertex(self, pnt_or_vertex): 303 | """returns the closest orthogonal project on `pnt` on edge""" 304 | if isinstance(pnt_or_vertex, TopoDS_Vertex): 305 | pnt_or_vertex = vertex2pnt(pnt_or_vertex) 306 | 307 | poc = GeomAPI_ProjectPointOnCurve(pnt_or_vertex, self.curve) 308 | return poc.LowerDistanceParameter(), poc.NearestPoint() 309 | 310 | def distance_on_curve(self, distance, close_parameter, estimate_parameter): 311 | """returns the parameter if there is a parameter 312 | on the curve with a distance length from u 313 | raises OutOfBoundary if no such parameter exists 314 | """ 315 | gcpa = GCPnts_AbscissaPoint( 316 | self.adaptor, distance, close_parameter, estimate_parameter, 1e-5 317 | ) 318 | with assert_isdone(gcpa, "couldnt compute distance on curve"): 319 | return gcpa.Parameter() 320 | 321 | def mid_point(self): 322 | """ 323 | :return: the parameter at the mid point of the curve, and 324 | its corresponding gp_Pnt 325 | """ 326 | _min, _max = self.domain() 327 | _mid = (_min + _max) / 2.0 328 | return _mid, self.adaptor.Value(_mid) 329 | 330 | def divide_by_number_of_points(self, n_pts, lbound=None, ubound=None): 331 | """returns a nested list of parameters and points on the edge 332 | at the requested interval [(param, gp_Pnt),...] 333 | """ 334 | _lbound, _ubound = self.domain() 335 | if lbound: 336 | _lbound = lbound 337 | elif ubound: 338 | _ubound = ubound 339 | 340 | # minimally two points or a Standard_ConstructionError is raised 341 | if n_pts <= 1: 342 | n_pts = 2 343 | 344 | try: 345 | npts = GCPnts_UniformAbscissa(self.adaptor, n_pts, _lbound, _ubound) 346 | except: 347 | print("Warning : GCPnts_UniformAbscissa failed") 348 | if npts.IsDone(): 349 | tmp = [] 350 | for i in xrange(1, npts.NbPoints() + 1): 351 | param = npts.Parameter(i) 352 | pnt = self.adaptor.Value(param) 353 | tmp.append((param, pnt)) 354 | return tmp 355 | else: 356 | return None 357 | 358 | def __eq__(self, other): 359 | if hasattr(other, "topo"): 360 | return self.IsEqual(other) 361 | else: 362 | return self.IsEqual(other) 363 | 364 | def __ne__(self, other): 365 | return not self.__eq__(other) 366 | 367 | def first_vertex(self): 368 | return topexp.FirstVertex(self) 369 | 370 | def last_vertex(self): 371 | return topexp.LastVertex(self) 372 | 373 | def common_vertex(self, edge): 374 | vert = TopoDS_Vertex() 375 | if topexp.CommonVertex(self, edge, vert): 376 | return vert 377 | else: 378 | return False 379 | 380 | def as_vec(self): 381 | if self.is_line(): 382 | first, last = map(vertex2pnt, [self.first_vertex(), self.last_vertex()]) 383 | return gp_Vec(first, last) 384 | else: 385 | raise ValueError( 386 | "edge is not a line, hence no meaningful vector can be returned" 387 | ) 388 | 389 | # =========================================================================== 390 | # Curve. 391 | # =========================================================================== 392 | 393 | def parameter_to_point(self, u): 394 | """returns the coordinate at parameter u""" 395 | return self.adaptor.Value(u) 396 | 397 | def fix_continuity(self, continuity): 398 | """ 399 | splits an edge to achieve a level of continuity 400 | :param continuity: GeomAbs_C* 401 | """ 402 | return fix_continuity(self, continuity) 403 | 404 | def continuity_from_faces(self, f1, f2): 405 | return BRep_Tool_Continuity(self, f1, f2) 406 | 407 | # =========================================================================== 408 | # Curve. 409 | # =========================================================================== 410 | 411 | def is_line(self): 412 | """checks if the curve is planar""" 413 | if self.nb_knots() == 2 and self.nb_poles() == 2: 414 | return True 415 | else: 416 | return False 417 | 418 | def is_seam(self, face): 419 | """ 420 | :return: True if the edge has two pcurves on one surface 421 | ( in the case of a sphere for example... ) 422 | """ 423 | sae = ShapeAnalysis_Edge() 424 | return sae.IsSeam(self, face) 425 | 426 | def is_edge_on_face(self, face): 427 | """checks whether curve lies on a surface or a face""" 428 | return ShapeAnalysis_Edge().HasPCurve(self, face) 429 | 430 | # =========================================================================== 431 | # Curve.graphic 432 | # =========================================================================== 433 | def show(self): 434 | """ 435 | poles, knots, should render all slightly different. 436 | here's how... 437 | 438 | http://www.opencascade.org/org/forum/thread_1125/ 439 | """ 440 | super(Edge, self).show() 441 | 442 | 443 | if __name__ == "__main__": 444 | from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox 445 | from OCCUtils.Topology import Topo 446 | 447 | b = BRepPrimAPI_MakeBox(10, 20, 30).Shape() 448 | t = Topo(b) 449 | ed = next(t.edges()) 450 | my_e = Edge(ed) 451 | print(my_e.tolerance) 452 | -------------------------------------------------------------------------------- /OCCUtils/face.py: -------------------------------------------------------------------------------- 1 | ##Copyright 2008-2013 Jelle Feringa (jelleferinga@gmail.com) 2 | ## 3 | ##This file is part of pythonOCC. 4 | ## 5 | ##pythonOCC is free software: you can redistribute it and/or modify 6 | ##it under the terms of the GNU Lesser General Public License as published by 7 | ##the Free Software Foundation, either version 3 of the License, or 8 | ##(at your option) any later version. 9 | ## 10 | ##pythonOCC is distributed in the hope that it will be useful, 11 | ##but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ##GNU Lesser General Public License for more details. 14 | ## 15 | ##You should have received a copy of the GNU Lesser General Public License 16 | ##along with pythonOCC. If not, see 17 | 18 | from OCC.Core.BRep import BRep_Tool_Surface, BRep_Tool 19 | from OCC.Core.BRepTopAdaptor import BRepTopAdaptor_FClass2d 20 | from OCC.Core.Geom import Geom_Curve 21 | from OCC.Core.GeomAPI import GeomAPI_ProjectPointOnSurf 22 | from OCC.Core.GeomLib import GeomLib_IsPlanarSurface 23 | from OCC.Core.TopAbs import TopAbs_IN 24 | from OCC.Core.TopExp import topexp 25 | from OCC.Core.TopoDS import TopoDS_Vertex, TopoDS_Face, TopoDS_Edge 26 | from OCC.Core.GeomLProp import GeomLProp_SLProps 27 | from OCC.Core.BRepTools import breptools_UVBounds 28 | from OCC.Core.BRepAdaptor import BRepAdaptor_Surface 29 | from OCC.Core.ShapeAnalysis import ShapeAnalysis_Surface 30 | from OCC.Core.GeomProjLib import geomprojlib 31 | from OCC.Core.Adaptor3d import Adaptor3d_IsoCurve 32 | from OCC.Core.gp import gp_Pnt2d, gp_Dir 33 | 34 | from OCCUtils.base import BaseObject 35 | from OCCUtils.edge import Edge 36 | from OCCUtils.Construct import TOLERANCE, to_adaptor_3d 37 | from OCCUtils.Topology import Topo, WireExplorer 38 | 39 | 40 | class DiffGeomSurface(object): 41 | def __init__(self, instance): 42 | self.instance = instance 43 | self._curvature = None 44 | self._curvature_initiated = False 45 | 46 | def curvature(self, u, v): 47 | """returns the curvature at the u parameter 48 | the curvature object can be returned too using 49 | curvatureType == curvatureType 50 | curvatureTypes are: 51 | gaussian 52 | minimum 53 | maximum 54 | mean 55 | curvatureType 56 | """ 57 | if not self._curvature_initiated: 58 | self._curvature = GeomLProp_SLProps(self.instance.surface, u, v, 2, 1e-7) 59 | 60 | _domain = self.instance.domain() 61 | if u in _domain or v in _domain: 62 | print("<<>>") 63 | div = 1000 64 | delta_u, delta_v = (_domain[0] - _domain[1]) / div, ( 65 | _domain[2] - _domain[3] 66 | ) / div 67 | 68 | if u in _domain: 69 | low, hi = u - _domain[0], u - _domain[1] 70 | if low < hi: 71 | u = u - delta_u 72 | else: 73 | u = u + delta_u 74 | 75 | if v in _domain: 76 | low, hi = v - _domain[2], v - _domain[3] 77 | if low < hi: 78 | v = v - delta_v 79 | else: 80 | v = v + delta_v 81 | 82 | self._curvature.SetParameters(u, v) 83 | self._curvature_initiated = True 84 | 85 | return self._curvature 86 | 87 | def gaussian_curvature(self, u, v): 88 | return self.curvature(u, v).GaussianCurvature() 89 | 90 | def min_curvature(self, u, v): 91 | return self.curvature(u, v).MinCurvature() 92 | 93 | def mean_curvature(self, u, v): 94 | return self.curvature(u, v).MeanCurvature() 95 | 96 | def max_curvature(self, u, v): 97 | return self.curvature(u, v).MaxCurvature() 98 | 99 | def normal(self, u, v): 100 | # TODO: should make this return a gp_Vec 101 | curv = self.curvature(u, v) 102 | if curv.IsNormalDefined(): 103 | return curv.Normal() 104 | else: 105 | raise ValueError("normal is not defined at u,v: {0}, {1}".format(u, v)) 106 | 107 | def tangent(self, u, v): 108 | dU, dV = gp_Dir(), gp_Dir() 109 | curv = self.curvature(u, v) 110 | if curv.IsTangentUDefined() and curv.IsTangentVDefined(): 111 | curv.TangentU(dU), curv.TangentV(dV) 112 | return dU, dV 113 | else: 114 | return None, None 115 | 116 | def radius(self, u, v): 117 | """returns the radius at u""" 118 | # TODO: SHOULD WE RETURN A SIGNED RADIUS? ( get rid of abs() )? 119 | try: 120 | _crv_min = 1.0 / self.min_curvature(u, v) 121 | except ZeroDivisionError: 122 | _crv_min = 0.0 123 | 124 | try: 125 | _crv_max = 1.0 / self.max_curvature(u, v) 126 | except ZeroDivisionError: 127 | _crv_max = 0.0 128 | return abs((_crv_min + _crv_max) / 2.0) 129 | 130 | 131 | class Face(TopoDS_Face, BaseObject): 132 | """high level surface API 133 | object is a Face if part of a Solid 134 | otherwise the same methods do apply, apart from the topology obviously 135 | """ 136 | 137 | def __init__(self, face): 138 | """ """ 139 | assert isinstance(face, TopoDS_Face), ( 140 | "need a TopoDS_Face, got a %s" % face.__class__ 141 | ) 142 | assert not face.IsNull() 143 | super(Face, self).__init__() 144 | BaseObject.__init__(self, "face") 145 | # we need to copy the base shape using the following three 146 | # lines 147 | assert self.IsNull() 148 | self.TShape(face.TShape()) 149 | self.Location(face.Location()) 150 | self.Orientation(face.Orientation()) 151 | assert not self.IsNull() 152 | 153 | # cooperative classes 154 | self.DiffGeom = DiffGeomSurface(self) 155 | 156 | # STATE; whether cooperative classes are yet initialized 157 | self._curvature_initiated = False 158 | self._geometry_lookup_init = False 159 | 160 | # =================================================================== 161 | # properties 162 | # =================================================================== 163 | self._h_srf = None 164 | self._srf = None 165 | self._adaptor = None 166 | self._classify_uv = ( 167 | None # cache the u,v classifier, no need to rebuild for every sample 168 | ) 169 | self._topo = None 170 | 171 | # aliasing of useful methods 172 | def is_u_periodic(self): 173 | return self.adaptor.IsUPeriodic() 174 | 175 | def is_v_periodic(self): 176 | return self.adaptor.IsVPeriodic() 177 | 178 | def is_u_closed(self): 179 | return self.adaptor.IsUClosed() 180 | 181 | def is_v_closed(self): 182 | return self.adaptor.IsVClosed() 183 | 184 | def is_u_rational(self): 185 | return self.adaptor.IsURational() 186 | 187 | def is_v_rational(self): 188 | return self.adaptor.IsVRational() 189 | 190 | def u_degree(self): 191 | return self.adaptor.UDegree() 192 | 193 | def v_degree(self): 194 | return self.adaptor.VDegree() 195 | 196 | def u_continuity(self): 197 | return self.adaptor.UContinuity() 198 | 199 | def v_continuity(self): 200 | return self.adaptor.VContinuity() 201 | 202 | def domain(self): 203 | """the u,v domain of the curve 204 | :return: UMin, UMax, VMin, VMax 205 | """ 206 | return breptools_UVBounds(self) 207 | 208 | def mid_point(self): 209 | """ 210 | :return: the parameter at the mid point of the face, 211 | and its corresponding gp_Pnt 212 | """ 213 | u_min, u_max, v_min, v_max = self.domain() 214 | u_mid = (u_min + u_max) / 2.0 215 | v_mid = (v_min + v_max) / 2.0 216 | return ((u_mid, v_mid), self.adaptor.Value(u_mid, v_mid)) 217 | 218 | @property 219 | def topo(self): 220 | if self._topo is not None: 221 | return self._topo 222 | else: 223 | self._topo = Topo(self) 224 | return self._topo 225 | 226 | @property 227 | def surface(self): 228 | if self._srf is None or self.is_dirty: 229 | self._srf = BRep_Tool_Surface(self) 230 | return self._srf 231 | 232 | @property 233 | def adaptor(self): 234 | if self._adaptor is not None and not self.is_dirty: 235 | pass 236 | else: 237 | self._adaptor = BRepAdaptor_Surface(self) 238 | return self._adaptor 239 | 240 | def is_closed(self): 241 | sa = ShapeAnalysis_Surface(self.surface) 242 | return sa.IsUClosed(), sa.IsVClosed() 243 | 244 | def is_planar(self, tol=TOLERANCE): 245 | """checks if the surface is planar within a tolerance 246 | :return: bool, gp_Pln 247 | """ 248 | is_planar_surface = GeomLib_IsPlanarSurface(self.surface, tol) 249 | return is_planar_surface.IsPlanar() 250 | 251 | def is_trimmed(self): 252 | """ 253 | :return: True if the Wire delimiting the Face lies on the bounds 254 | of the surface 255 | if this is not the case, the wire represents a contour that delimits 256 | the face [ think cookie cutter ] 257 | and implies that the surface is trimmed 258 | """ 259 | _round = lambda x: round(x, 3) 260 | a = map(_round, breptools_UVBounds(self)) 261 | b = map(_round, self.adaptor.Surface().Surface().Bounds()) 262 | if a != b: 263 | print("a,b", a, b) 264 | return True 265 | return False 266 | 267 | def on_trimmed(self, u, v): 268 | """tests whether the surface at the u,v parameter has been trimmed""" 269 | if self._classify_uv is None: 270 | self._classify_uv = BRepTopAdaptor_FClass2d(self, 1e-9) 271 | uv = gp_Pnt2d(u, v) 272 | if self._classify_uv.Perform(uv) == TopAbs_IN: 273 | return True 274 | else: 275 | return False 276 | 277 | def parameter_to_point(self, u, v): 278 | """returns the coordinate at u,v""" 279 | return self.surface.Value(u, v) 280 | 281 | def point_to_parameter(self, pt): 282 | """ 283 | returns the uv value of a point on a surface 284 | @param pt: 285 | """ 286 | sas = ShapeAnalysis_Surface(self.surface) 287 | uv = sas.ValueOfUV(pt, self.tolerance) 288 | return uv.Coord() 289 | 290 | def continuity_edge_face(self, edge, face): 291 | """ 292 | compute the continuity between two faces at :edge: 293 | 294 | :param edge: an Edge or TopoDS_Edge from :face: 295 | :param face: a Face or TopoDS_Face 296 | :return: bool, GeomAbs_Shape if it has continuity, otherwise 297 | False, None 298 | """ 299 | bt = BRep_Tool() 300 | if bt.HasContinuity(edge, self, face): 301 | continuity = bt.Continuity(edge, self, face) 302 | return True, continuity 303 | else: 304 | return False, None 305 | 306 | # =========================================================================== 307 | # Surface.project 308 | # project curve, point on face 309 | # =========================================================================== 310 | 311 | def project_vertex(self, pnt, tol=TOLERANCE): 312 | """projects self with a point, curve, edge, face, solid 313 | method wraps dealing with the various topologies 314 | 315 | if other is a point: 316 | returns uv, point 317 | 318 | """ 319 | if isinstance(pnt, TopoDS_Vertex): 320 | pnt = BRep_Tool.Pnt(pnt) 321 | 322 | proj = GeomAPI_ProjectPointOnSurf(pnt, self.surface, tol) 323 | uv = proj.LowerDistanceParameters() 324 | proj_pnt = proj.NearestPoint() 325 | 326 | return uv, proj_pnt 327 | 328 | def project_curve(self, other): 329 | # this way Geom_Circle and alike are valid too 330 | if ( 331 | isinstance(other, TopoDS_Edge) 332 | or isinstance(other, Geom_Curve) 333 | or issubclass(other, Geom_Curve) 334 | ): 335 | # convert edge to curve 336 | first, last = topexp.FirstVertex(other), topexp.LastVertex(other) 337 | lbound, ubound = BRep_Tool().Parameter(first, other), BRep_Tool().Parameter( 338 | last, other 339 | ) 340 | other = BRep_Tool.Curve(other, lbound, ubound) 341 | return geomprojlib.Project(other, self.surface) 342 | 343 | def project_edge(self, edg): 344 | if hasattr(edg, "adaptor"): 345 | return self.project_curve(self, self.adaptor) 346 | return self.project_curve(self, to_adaptor_3d(edg)) 347 | 348 | def iso_curve(self, u_or_v, param): 349 | """ 350 | get the iso curve from a u,v + parameter 351 | :param u_or_v: 352 | :param param: 353 | :return: 354 | """ 355 | uv = 0 if u_or_v == "u" else 1 356 | iso = Adaptor3d_IsoCurve(self.adaptor, uv, param) 357 | return iso 358 | 359 | def edges(self): 360 | return [Edge(i) for i in WireExplorer(next(self.topo.wires())).ordered_edges()] 361 | 362 | def __repr__(self): 363 | return self.name 364 | 365 | def __str__(self): 366 | return self.__repr__() 367 | 368 | 369 | if __name__ == "__main__": 370 | from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeSphere 371 | 372 | sph = BRepPrimAPI_MakeSphere(1, 1).Face() 373 | fc = Face(sph) 374 | print(fc.is_trimmed()) 375 | print(fc.is_planar()) 376 | -------------------------------------------------------------------------------- /OCCUtils/shell.py: -------------------------------------------------------------------------------- 1 | ##Copyright 2008-2015 Jelle Feringa (jelleferinga@gmail.com) 2 | ## 3 | ##This file is part of pythonOCC. 4 | ## 5 | ##pythonOCC is free software: you can redistribute it and/or modify 6 | ##it under the terms of the GNU Lesser General Public License as published by 7 | ##the Free Software Foundation, either version 3 of the License, or 8 | ##(at your option) any later version. 9 | ## 10 | ##pythonOCC is distributed in the hope that it will be useful, 11 | ##but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ##GNU Lesser General Public License for more details. 14 | ## 15 | ##You should have received a copy of the GNU Lesser General Public License 16 | ##along with pythonOCC. If not, see 17 | 18 | from OCC.Core.TopoDS import TopoDS_Shell 19 | from OCC.Core.ShapeAnalysis import ShapeAnalysis_Shell 20 | 21 | from OCCUtils.Topology import Topo 22 | from OCCUtils.base import BaseObject, GlobalProperties 23 | 24 | 25 | class Shell(TopoDS_Shell, BaseObject): 26 | _n = 0 27 | 28 | def __init__(self, shell): 29 | assert isinstance(shell, TopoDS_Shell), ( 30 | "need a TopoDS_Shell, got a %s" % shell.__class__ 31 | ) 32 | assert not shell.IsNull() 33 | super(Shell, self).__init__() 34 | BaseObject.__init__(self, "shell") 35 | # we need to copy the base shape using the following three 36 | # lines 37 | assert self.IsNull() 38 | self.TShape(shell.TShape()) 39 | self.Location(shell.Location()) 40 | self.Orientation(shell.Orientation()) 41 | assert not self.IsNull() 42 | 43 | self.GlobalProperties = GlobalProperties(self) 44 | self._n += 1 45 | 46 | def analyse(self): 47 | """ 48 | 49 | :return: 50 | """ 51 | ss = ShapeAnalysis_Shell(self) 52 | if ss.HasFreeEdges(): 53 | bad_edges = [e for e in Topo(ss.BadEdges()).edges()] 54 | return bad_edges 55 | 56 | def Faces(self): 57 | """ 58 | 59 | :return: 60 | """ 61 | return Topo(self, True).faces() 62 | 63 | def Wires(self): 64 | """ 65 | :return: 66 | """ 67 | return Topo(self, True).wires() 68 | 69 | def Edges(self): 70 | return Topo(self, True).edges() 71 | -------------------------------------------------------------------------------- /OCCUtils/solid.py: -------------------------------------------------------------------------------- 1 | ##Copyright 2008-2013 Jelle Feringa (jelleferinga@gmail.com) 2 | ## 3 | ##This file is part of pythonOCC. 4 | ## 5 | ##pythonOCC is free software: you can redistribute it and/or modify 6 | ##it under the terms of the GNU Lesser General Public License as published by 7 | ##the Free Software Foundation, either version 3 of the License, or 8 | ##(at your option) any later version. 9 | ## 10 | ##pythonOCC is distributed in the hope that it will be useful, 11 | ##but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ##GNU Lesser General Public License for more details. 14 | ## 15 | ##You should have received a copy of the GNU Lesser General Public License 16 | ##along with pythonOCC. If not, see 17 | 18 | from OCC.Core.TopoDS import TopoDS_Solid 19 | 20 | from OCCUtils.Topology import Topo 21 | from OCCUtils.base import GlobalProperties, BaseObject 22 | from OCCUtils.shell import Shell 23 | 24 | 25 | class Solid(TopoDS_Solid, BaseObject): 26 | def __init__(self, solid): 27 | assert isinstance(solid, TopoDS_Solid), ( 28 | "need a TopoDS_Solid, got a %s" % solid.__class__ 29 | ) 30 | assert not solid.IsNull() 31 | super(Solid, self).__init__() 32 | BaseObject.__init__(self, "solid") 33 | # we need to copy the base shape using the following three 34 | # lines 35 | assert self.IsNull() 36 | self.TShape(solid.TShape()) 37 | self.Location(solid.Location()) 38 | self.Orientation(solid.Orientation()) 39 | assert not self.IsNull() 40 | 41 | self.GlobalProperties = GlobalProperties(self) 42 | 43 | def shells(self): 44 | return (Shell(sh) for sh in Topo(self)) 45 | -------------------------------------------------------------------------------- /OCCUtils/types_lut.py: -------------------------------------------------------------------------------- 1 | ##Copyright 2008-2015 Jelle Feringa (jelleferinga@gmail.com) 2 | ## 3 | ##This file is part of pythonOCC. 4 | ## 5 | ##pythonOCC is free software: you can redistribute it and/or modify 6 | ##it under the terms of the GNU Lesser General Public License as published by 7 | ##the Free Software Foundation, either version 3 of the License, or 8 | ##(at your option) any later version. 9 | ## 10 | ##pythonOCC is distributed in the hope that it will be useful, 11 | ##but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ##GNU Lesser General Public License for more details. 14 | ## 15 | ##You should have received a copy of the GNU Lesser General Public License 16 | ##along with pythonOCC. If not, see 17 | 18 | from OCC.Core.BRepCheck import * 19 | from OCC.Core.GeomAbs import * 20 | from OCC.Core.TopoDS import topods, TopoDS_Shape 21 | from OCC.Core.BRep import BRep_Tool_Surface 22 | from OCC.Core.TopAbs import * 23 | from OCC.Core.Geom import Geom_CylindricalSurface, Geom_Plane 24 | 25 | 26 | class ShapeToTopology(object): 27 | """ 28 | looks up the topology type and returns the corresponding topological entity 29 | """ 30 | 31 | def __init__(self): 32 | self.topoTypes = { 33 | TopAbs_VERTEX: topods.Vertex, 34 | TopAbs_EDGE: topods.Edge, 35 | TopAbs_FACE: topods.Face, 36 | TopAbs_WIRE: topods.Wire, 37 | TopAbs_SHELL: topods.Shell, 38 | TopAbs_SOLID: topods.Solid, 39 | TopAbs_COMPOUND: topods.Compound, 40 | TopAbs_COMPSOLID: topods.CompSolid, 41 | } 42 | 43 | def __call__(self, shape): 44 | if isinstance(shape, TopoDS_Shape): 45 | return self.topoTypes[shape.ShapeType()](shape) 46 | else: 47 | raise AttributeError("shape has not method `ShapeType`") 48 | 49 | def __getitem__(self, item): 50 | return self(item) 51 | 52 | 53 | class EnumLookup(object): 54 | """ 55 | perform bi-directional lookup of Enums'... 56 | """ 57 | 58 | def __init__(self, li_in, li_out): 59 | self.d = {} 60 | for a, b in zip(li_in, li_out): 61 | self.d[a] = b 62 | self.d[b] = a 63 | 64 | def __getitem__(self, item): 65 | return self.d[item] 66 | 67 | 68 | _curve_typesA = ( 69 | GeomAbs_Line, 70 | GeomAbs_Circle, 71 | GeomAbs_Ellipse, 72 | GeomAbs_Hyperbola, 73 | GeomAbs_Parabola, 74 | GeomAbs_BezierCurve, 75 | GeomAbs_BSplineCurve, 76 | GeomAbs_OtherCurve, 77 | ) 78 | _curve_typesB = ( 79 | "line", 80 | "circle", 81 | "ellipse", 82 | "hyperbola", 83 | "parabola", 84 | "bezier", 85 | "spline", 86 | "other", 87 | ) 88 | _surface_typesA = ( 89 | GeomAbs_Plane, 90 | GeomAbs_Cylinder, 91 | GeomAbs_Cone, 92 | GeomAbs_Sphere, 93 | GeomAbs_Torus, 94 | GeomAbs_BezierSurface, 95 | GeomAbs_BSplineSurface, 96 | GeomAbs_SurfaceOfRevolution, 97 | GeomAbs_SurfaceOfExtrusion, 98 | GeomAbs_OffsetSurface, 99 | GeomAbs_OtherSurface, 100 | ) 101 | _surface_typesB = ( 102 | "plane", 103 | "cylinder", 104 | "cone", 105 | "sphere", 106 | "torus", 107 | "bezier", 108 | "spline", 109 | "revolution", 110 | "extrusion", 111 | "offset", 112 | "other", 113 | ) 114 | 115 | 116 | _stateA = ("in", "out", "on", "unknown") 117 | _stateB = (TopAbs_IN, TopAbs_OUT, TopAbs_ON, TopAbs_UNKNOWN) 118 | 119 | 120 | _orientA = ["TopAbs_FORWARD", "TopAbs_REVERSED", "TopAbs_INTERNAL", "TopAbs_EXTERNAL"] 121 | _orientB = [TopAbs_FORWARD, TopAbs_REVERSED, TopAbs_INTERNAL, TopAbs_EXTERNAL] 122 | 123 | 124 | _topoTypesA = [ 125 | "vertex", 126 | "edge", 127 | "wire", 128 | "face", 129 | "shell", 130 | "solid", 131 | "compsolid", 132 | "compound", 133 | "shape", 134 | ] 135 | _topoTypesB = [ 136 | TopAbs_VERTEX, 137 | TopAbs_EDGE, 138 | TopAbs_WIRE, 139 | TopAbs_FACE, 140 | TopAbs_SHELL, 141 | TopAbs_SOLID, 142 | TopAbs_COMPSOLID, 143 | TopAbs_COMPOUND, 144 | TopAbs_SHAPE, 145 | ] 146 | 147 | 148 | _geom_types_a = [ 149 | "line", 150 | "circle", 151 | "ellipse", 152 | "hyperbola", 153 | "parabola", 154 | "beziercurve", 155 | "bsplinecurve", 156 | "othercurve", 157 | ] 158 | _geom_types_b = [ 159 | GeomAbs_Line, 160 | GeomAbs_Circle, 161 | GeomAbs_Ellipse, 162 | GeomAbs_Hyperbola, 163 | GeomAbs_Parabola, 164 | GeomAbs_BezierCurve, 165 | GeomAbs_BSplineCurve, 166 | GeomAbs_OtherCurve, 167 | ] 168 | 169 | 170 | # TODO: make a function that generalizes this, there is absolutely 171 | # no need for 2 lists to define an EnumLookup 172 | 173 | 174 | def fix_formatting(_str): 175 | return [i.strip() for i in _str.split(",")] 176 | 177 | 178 | _brep_check_a = fix_formatting( 179 | "NoError, InvalidPointOnCurve,\ 180 | InvalidPointOnCurveOnSurface, InvalidPointOnSurface,\ 181 | No3DCurve, Multiple3DCurve, Invalid3DCurve, NoCurveOnSurface,\ 182 | InvalidCurveOnSurface, InvalidCurveOnClosedSurface, InvalidSameRangeFlag,\ 183 | InvalidSameParameterFlag,\ 184 | InvalidDegeneratedFlag, FreeEdge, InvalidMultiConnexity, InvalidRange,\ 185 | EmptyWire, RedundantEdge, SelfIntersectingWire, NoSurface,\ 186 | InvalidWire, RedundantWire, IntersectingWires, InvalidImbricationOfWires,\ 187 | EmptyShell, RedundantFace, UnorientableShape, NotClosed,\ 188 | NotConnected, SubshapeNotInShape, BadOrientation, BadOrientationOfSubshape,\ 189 | InvalidToleranceValue, CheckFail" 190 | ) 191 | 192 | _brep_check_b = [ 193 | BRepCheck_NoError, 194 | BRepCheck_InvalidPointOnCurve, 195 | BRepCheck_InvalidPointOnCurveOnSurface, 196 | BRepCheck_InvalidPointOnSurface, 197 | BRepCheck_No3DCurve, 198 | BRepCheck_Multiple3DCurve, 199 | BRepCheck_Invalid3DCurve, 200 | BRepCheck_NoCurveOnSurface, 201 | BRepCheck_InvalidCurveOnSurface, 202 | BRepCheck_InvalidCurveOnClosedSurface, 203 | BRepCheck_InvalidSameRangeFlag, 204 | BRepCheck_InvalidSameParameterFlag, 205 | BRepCheck_InvalidDegeneratedFlag, 206 | BRepCheck_FreeEdge, 207 | BRepCheck_InvalidMultiConnexity, 208 | BRepCheck_InvalidRange, 209 | BRepCheck_EmptyWire, 210 | BRepCheck_RedundantEdge, 211 | BRepCheck_SelfIntersectingWire, 212 | BRepCheck_NoSurface, 213 | BRepCheck_InvalidWire, 214 | BRepCheck_RedundantWire, 215 | BRepCheck_IntersectingWires, 216 | BRepCheck_InvalidImbricationOfWires, 217 | BRepCheck_EmptyShell, 218 | BRepCheck_RedundantFace, 219 | BRepCheck_UnorientableShape, 220 | BRepCheck_NotClosed, 221 | BRepCheck_NotConnected, 222 | BRepCheck_SubshapeNotInShape, 223 | BRepCheck_BadOrientation, 224 | BRepCheck_BadOrientationOfSubshape, 225 | BRepCheck_InvalidToleranceValue, 226 | BRepCheck_CheckFail, 227 | ] 228 | 229 | brepcheck_lut = EnumLookup(_brep_check_a, _brep_check_b) 230 | curve_lut = EnumLookup(_curve_typesA, _curve_typesB) 231 | surface_lut = EnumLookup(_surface_typesA, _surface_typesB) 232 | state_lut = EnumLookup(_stateA, _stateB) 233 | orient_lut = EnumLookup(_orientA, _orientB) 234 | topo_lut = EnumLookup(_topoTypesA, _topoTypesB) 235 | shape_lut = ShapeToTopology() 236 | geom_lut = EnumLookup(_geom_types_a, _geom_types_b) 237 | 238 | # todo: refactor, these classes have been moved from the "Topology" directory 239 | # which had too many overlapping methods & classes, that are 240 | # now part of the KBE module... 241 | # still need to think what to do with these... 242 | # what_is_face should surely become a lut [ geom_lut? ] 243 | # i'm not sure whether casting to a gp_* is useful... 244 | 245 | classes = dir() 246 | geom_classes = [] 247 | for elem in classes: 248 | if elem.startswith("Geom") and not "swig" in elem: 249 | geom_classes.append(elem) 250 | 251 | 252 | def what_is_face(face): 253 | """Returns all class names for which this class can be downcasted""" 254 | if not face.ShapeType() == TopAbs_FACE: 255 | print("%s is not a TopAbs_FACE. Conversion impossible") 256 | return None 257 | hs = BRep_Tool_Surface(face) 258 | obj = hs.GetObject() 259 | result = [] 260 | for elem in classes: 261 | if elem.startswith("Geom") and not "swig" in elem: 262 | geom_classes.append(elem) 263 | # Run the test for each class 264 | for geom_class in geom_classes: 265 | if obj.IsKind(geom_class) and not geom_class in result: 266 | result.append(geom_class) 267 | return result 268 | 269 | 270 | def face_is_plane(face): 271 | """Returns True if the TopoDS_Shape is a plane, False otherwise""" 272 | hs = BRep_Tool_Surface(face) 273 | downcast_result = Geom_Plane().DownCast(hs) 274 | # the handle is null if downcast failed or is not possible, 275 | # that is to say the face is not a plane 276 | if downcast_result.IsNull(): 277 | return False 278 | else: 279 | return True 280 | 281 | 282 | def shape_is_cylinder(face): 283 | """Returns True is the TopoDS_Shape is a cylinder, False otherwise""" 284 | hs = BRep_Tool_Surface(face) 285 | downcast_result = Geom_CylindricalSurface().DownCast(hs) 286 | if downcast_result.IsNull(): 287 | return False 288 | else: 289 | return True 290 | -------------------------------------------------------------------------------- /OCCUtils/vertex.py: -------------------------------------------------------------------------------- 1 | ##Copyright 2008-2013 Jelle Feringa (jelleferinga@gmail.com) 2 | ## 3 | ##This file is part of pythonOCC. 4 | ## 5 | ##pythonOCC is free software: you can redistribute it and/or modify 6 | ##it under the terms of the GNU Lesser General Public License as published by 7 | ##the Free Software Foundation, either version 3 of the License, or 8 | ##(at your option) any later version. 9 | ## 10 | ##pythonOCC is distributed in the hope that it will be useful, 11 | ##but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ##GNU Lesser General Public License for more details. 14 | ## 15 | ##You should have received a copy of the GNU Lesser General Public License 16 | ##along with pythonOCC. If not, see 17 | 18 | from OCC.Core.gp import gp_Pnt, gp_Vec, gp_Dir, gp_XYZ, gp_Pnt2d 19 | from OCC.Core.TopoDS import TopoDS_Vertex 20 | from OCC.Core.ShapeBuild import ShapeBuild_ReShape 21 | 22 | from OCCUtils.base import BaseObject 23 | from OCCUtils.Construct import make_vertex 24 | 25 | 26 | class Vertex(TopoDS_Vertex, BaseObject): 27 | """ 28 | wraps gp_Pnt 29 | """ 30 | 31 | _n = 0 32 | 33 | def __init__(self, x, y, z): 34 | super(Vertex, self).__init__() 35 | """Constructor for KbeVertex""" 36 | BaseObject.__init__(self, name="Vertex #{0}".format(self._n)) 37 | 38 | self._n += 1 # should be a property of KbeObject 39 | self._pnt = gp_Pnt(x, y, z) 40 | self._vertex = make_vertex(self._pnt) 41 | TopoDS_Vertex.__init__(self, self._vertex) 42 | 43 | def _update(self): 44 | """ """ 45 | # TODO: perhaps should take an argument until which topological level 46 | # topological entities bound to the vertex should be updated too... 47 | reshape = ShapeBuild_ReShape() 48 | reshape.Replace(self._vertex, make_vertex(self._pnt)) 49 | 50 | @staticmethod 51 | def from_pnt(cls, pnt): 52 | x, y, z = pnt.X(), pnt.Y(), pnt.Z() 53 | return cls(x, y, z) 54 | 55 | @property 56 | def x(self): 57 | return self._pnt.X() 58 | 59 | @x.setter 60 | def x(self, val): 61 | self._pnt.SetX(val) 62 | self._update() 63 | 64 | @property 65 | def y(self): 66 | return self._pnt.Y() 67 | 68 | @y.setter 69 | def y(self, val): 70 | self._pnt.SetY(val) 71 | self._update() 72 | 73 | @property 74 | def z(self): 75 | return self._pnt.Z() 76 | 77 | @z.setter 78 | def z(self, val): 79 | self._pnt.SetZ(val) 80 | self._update() 81 | 82 | @property 83 | def xyz(self): 84 | return self._pnt.Coord() 85 | 86 | @xyz.setter 87 | def xyz(self, *val): 88 | self._pnt.SetXYZ(*val) 89 | self._update() 90 | 91 | def __repr__(self): 92 | return self.name 93 | 94 | @property 95 | def as_vec(self): 96 | """returns a gp_Vec version of self""" 97 | return gp_Vec(*self._pnt.Coord()) 98 | 99 | @property 100 | def as_dir(self): 101 | """returns a gp_Dir version of self""" 102 | return gp_Dir(*self._pnt.Coord()) 103 | 104 | @property 105 | def as_xyz(self): 106 | """returns a gp_XYZ version of self""" 107 | return gp_XYZ(*self._pnt.Coord()) 108 | 109 | @property 110 | def as_pnt(self): 111 | return self._pnt 112 | 113 | @property 114 | def as_2d(self): 115 | """returns a gp_Pnt2d version of self""" 116 | return gp_Pnt2d(*self._pnt.Coord()[:2]) 117 | -------------------------------------------------------------------------------- /OCCUtils/wire.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ##Copyright 2008-2013 Jelle Feringa (jelleferinga@gmail.com) 4 | ## 5 | ##This file is part of pythonOCC. 6 | ## 7 | ##pythonOCC is free software: you can redistribute it and/or modify 8 | ##it under the terms of the GNU Lesser General Public License as published by 9 | ##the Free Software Foundation, either version 3 of the License, or 10 | ##(at your option) any later version. 11 | ## 12 | ##pythonOCC is distributed in the hope that it will be useful, 13 | ##but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ##GNU Lesser General Public License for more details. 16 | ## 17 | ##You should have received a copy of the GNU Lesser General Public License 18 | ##along with pythonOCC. If not, see 19 | 20 | from OCC.Core.TopoDS import TopoDS_Wire 21 | 22 | from OCCUtils.base import BaseObject 23 | 24 | 25 | class Wire(TopoDS_Wire, BaseObject): 26 | def __init__(self, wire): 27 | """ """ 28 | assert isinstance(wire, TopoDS_Wire), ( 29 | "need a TopoDS_Wire, got a %s" % wire.__class__ 30 | ) 31 | assert not wire.IsNull() 32 | super(Wire, self).__init__() 33 | BaseObject.__init__(self, "wire") 34 | # we need to copy the base shape using the following three 35 | # lines 36 | assert self.IsNull() 37 | self.TShape(wire.TShape()) 38 | self.Location(wire.Location()) 39 | self.Orientation(wire.Orientation()) 40 | assert not self.IsNull() 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kodacad 2 | Simple 3D CAD application using PythonOCC with PyQt5 backend. 3 | 4 | Intended to be simple & easy to use, yet useful to get real work done. 5 | A brief "Getting Started" guide is available at: https://dblanding.github.io/kodacad/ 6 | -------------------------------------------------------------------------------- /__pycache__/docmodel.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/__pycache__/docmodel.cpython-39.pyc -------------------------------------------------------------------------------- /__pycache__/m2d.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/__pycache__/m2d.cpython-39.pyc -------------------------------------------------------------------------------- /__pycache__/mainwindow.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/__pycache__/mainwindow.cpython-39.pyc -------------------------------------------------------------------------------- /__pycache__/rpnCalculator.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/__pycache__/rpnCalculator.cpython-39.pyc -------------------------------------------------------------------------------- /__pycache__/version.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/__pycache__/version.cpython-39.pyc -------------------------------------------------------------------------------- /__pycache__/workplane.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/__pycache__/workplane.cpython-39.pyc -------------------------------------------------------------------------------- /docs/assembly_structure/assembly_structure.md: -------------------------------------------------------------------------------- 1 | # Working with an OCAF document 2 | 3 | * We will use the file `as-ooc-214.stp` as an example in this discussion 4 | 5 | ### If the file is loaded as **component** (without any assembly structure) 6 | * Use `File -> Load STEP Component` option 7 | 8 | ![components only](images/components.png) 9 | 10 | * We see these five **prototype shapes** located in a pile, right where they were created: 11 | * Only the 5 simple shapes can be seen in the display, not the assemblies 12 | 13 | ``` 14 | Entry | Name 15 | [0:1:1:1] Top 16 | [0:1:1:2] as1 17 | [0:1:1:3] rod-assembly 18 | [0:1:1:4] nut 19 | [0:1:1:5] rod 20 | [0:1:1:6] l-bracket-assembly 21 | [0:1:1:7] nut-bolt-assembly 22 | [0:1:1:8] bolt 23 | [0:1:1:9] l-bracket 24 | [0:1:1:10] plate 25 | ``` 26 | * Clicking on `Utility -> dump doc` shows the cad model document: 27 | ``` 28 | Assembly structure of doc: 29 | 30 | 0:1:1:1.0 [0:1:1:1] Top Number of labels at root = 6 31 | 0:1:1:1:1.0 [0:1:1:1:1] nut => [0:1:1:2] SOLID 32 | 0:1:1:1:2.0 [0:1:1:1:2] rod => [0:1:1:3] SOLID 33 | 0:1:1:1:3.0 [0:1:1:1:3] bolt => [0:1:1:4] SOLID 34 | 0:1:1:1:4.0 [0:1:1:1:4] l-bracket => [0:1:1:5] SOLID 35 | 0:1:1:1:5.0 [0:1:1:1:5] plate => [0:1:1:6] SOLID 36 | ``` 37 | 38 | ## The same file, loaded with its assembly structure is much more useful: 39 | ![step file loaded under Top](images/as1-loaded-under-top.png) 40 | * Use `File -> Load STEP At Top` option. 41 | * Below is the Kodacad `document dump` immediately after loading this file in **under** TOP. 42 | * The left column are UID values, needed by Kodacad to identify and differentiate between multiple shared instances of CAD components. 43 | * UID's are comprised of OCAF label `entries` (':' separated integers) with an appended '.' and trailing integer (used to distinguish between multiple instances of the same part or assembly). 44 | * To the right, the **entry** and **name** of component labels are shown, followed by **=>** and the **entry** and **name** of the **referred label**. 45 | * The referred label contains the actual CAD shape or assembly data, of which the component is an instance. 46 | * The level of indentation shows the depth of the parent / child relationship in the tree structure. 47 | 48 | ``` 49 | Assembly structure of doc: 50 | 51 | 0:1:1:1.0 [0:1:1:1] Top Number of labels at root = 10 52 | 0:1:1:1:1.0 [0:1:1:1:1] as1-oc-214 => [0:1:1:2] as1 53 | 0:1:1:2:1.0 [0:1:1:2:1] rod-assembly_1 => [0:1:1:3] rod-assembly 54 | 0:1:1:3:1.0 [0:1:1:3:1] nut_1 => [0:1:1:4] nut 55 | 0:1:1:3:2.0 [0:1:1:3:2] nut_2 => [0:1:1:4] nut 56 | 0:1:1:3:3.0 [0:1:1:3:3] rod_1 => [0:1:1:5] rod 57 | 0:1:1:2:2.0 [0:1:1:2:2] l-bracket-assembly_1 => [0:1:1:6] l-bracket-assembly 58 | 0:1:1:6:1.0 [0:1:1:6:1] nut-bolt-assembly_1 => [0:1:1:7] nut-bolt-assembly 59 | 0:1:1:7:1.0 [0:1:1:7:1] bolt_1 => [0:1:1:8] bolt 60 | 0:1:1:7:2.0 [0:1:1:7:2] nut_3 => [0:1:1:4] nut 61 | 0:1:1:6:2.0 [0:1:1:6:2] nut-bolt-assembly_2 => [0:1:1:7] nut-bolt-assembly 62 | 0:1:1:7:1.1 [0:1:1:7:1] bolt_1 => [0:1:1:8] bolt 63 | 0:1:1:7:2.1 [0:1:1:7:2] nut_3 => [0:1:1:4] nut 64 | 0:1:1:6:3.0 [0:1:1:6:3] nut-bolt-assembly_3 => [0:1:1:7] nut-bolt-assembly 65 | 0:1:1:7:1.2 [0:1:1:7:1] bolt_1 => [0:1:1:8] bolt 66 | 0:1:1:7:2.2 [0:1:1:7:2] nut_3 => [0:1:1:4] nut 67 | 0:1:1:6:4.0 [0:1:1:6:4] l-bracket_1 => [0:1:1:9] l-bracket 68 | 0:1:1:2:3.0 [0:1:1:2:3] plate_1 => [0:1:1:10] plate 69 | 0:1:1:2:4.0 [0:1:1:2:4] l-bracket-assembly_2 => [0:1:1:6] l-bracket-assembly 70 | 0:1:1:6:1.1 [0:1:1:6:1] nut-bolt-assembly_1 => [0:1:1:7] nut-bolt-assembly 71 | 0:1:1:7:1.3 [0:1:1:7:1] bolt_1 => [0:1:1:8] bolt 72 | 0:1:1:7:2.3 [0:1:1:7:2] nut_3 => [0:1:1:4] nut 73 | 0:1:1:6:2.1 [0:1:1:6:2] nut-bolt-assembly_2 => [0:1:1:7] nut-bolt-assembly 74 | 0:1:1:7:1.4 [0:1:1:7:1] bolt_1 => [0:1:1:8] bolt 75 | 0:1:1:7:2.4 [0:1:1:7:2] nut_3 => [0:1:1:4] nut 76 | 0:1:1:6:3.1 [0:1:1:6:3] nut-bolt-assembly_3 => [0:1:1:7] nut-bolt-assembly 77 | 0:1:1:7:1.5 [0:1:1:7:1] bolt_1 => [0:1:1:8] bolt 78 | 0:1:1:7:2.5 [0:1:1:7:2] nut_3 => [0:1:1:4] nut 79 | 0:1:1:6:4.1 [0:1:1:6:4] l-bracket_1 => [0:1:1:9] l-bracket 80 | ``` 81 | * By always loading component step files in **Under TOP**, the root label (TOP) is preserved as the **top-level assembly** which contains all the various other subassemblies and parts comprising the entire project. 82 | * When we want to save / load our our entire document to file, use save/load **At TOP**. 83 | * `Save all at TOP`would be used to save project to file. 84 | * `Load STEP at TOP` would be used to reload the project from file. 85 | 86 | ## How assembly structure is represented in an OCAF (or XCAF) document 87 | * When a STEP file is read in, it is parsed and then organized into a hierarchical tree structure in which each leaf of the tree (called a **label**) contains CAD data for a particular component, and also contains a unique **entry** to keep track of its place in the tree structure. 88 | * Each and every **unique component** (part or assembly) is a **located instance** of a **prototype shape** attached to a label which is a "sibling" of the root label (TOP assembly). 89 | 90 | * In our example, there are 10 labels at root. As mentioned above, CAD data are attached to these labels as attributes. 91 | * The data for 5 of these labels are parts (TopoDS_shapes) and the other 5 are assemblies. 92 | * Each of these labels has an **entry** of depth=4. 93 | 94 | ### The hierarchical assembly structure is represented with labels having depth=5 95 | 96 | * Our method of exploration will be to drill down, depth first, into the first label at root. 97 | * In this particular step file, this is the only **Free Shape**. 98 | * As we will soon see, all the other shapes at root will be accessed by reference from componenet labels 99 | * Each successive row of the document discovers a component of the parent assembly above. 100 | * For example, the top assembly `[0:1:1:1] Top` (depth=4) has exactly 1 component (1 child label) named `as1-oc-214` whose entry is composed of its parent's entry plus its own tag (:1) appended `[0:1:1:1:1]`. 101 | * In the 2nd row of our doc dump example, you can see that `as1-oc-214` is shown 'referring to' assembly `as1`. This means that `as1-oc-214` is an instance of the assembly `as1`. In this case, it's the **only** instance. 102 | * Notice that `[0:1:1:1:1] as1-oc-214` has no children. 103 | * However, its referred label `[0:1:1:2] as1` does have children. 104 | * Only siblings of the root label (depth=4) have child labels. 105 | * In the third row of the doc dump, we drill down into `[0:1:1:2] as1`, looking for its children. 106 | * `[0:1:1:2:1] rod-assembly_1` is found to be the first child of `[0:1:1:2] as1` 107 | * `[0:1:1:2:1] rod-assembly_1` refers to `[0:1:1:3] rod-assembly` 108 | * On the 4th, 5th, and 6th rows, we drill into `[0:1:1:3] rod-assembly` looking for its children, but find only parts, no assemblies. So we have drilled as deep as we can go. 109 | * On the 7th row, we find the 2nd component of `[0:1:1:2] as1` to be `[0:1:1:2:2] l-bracket-assembly_1` 110 | * ... And so on ... Eventually, we will have visisted all the labels at root. 111 | 112 | ### Location data are stored on component labels 113 | 114 | * Components of an assembly are, by definition, instances referring to a root label representing either a simple shape (part) or a compound shape (assembly), but which are usually positioned somewhere else. 115 | * Components get their shape information from their referred shape but they also carry a location vector specifing where their referred shape or assembly is to be located. 116 | * When parsing our STEP file, we need to keep track of the location vectors of each component with respect to its referred shape. 117 | * Let's look at the 2 instances of `[0:1:1:6] l-bracket-assembly` for example. 118 | * `[0:1:1:2:2] l-bracket-assembly_1` and `[0:1:1:2:4] l-bracket-assembly_2` are very clearly identical instances with different locations. 119 | * They each have a different location with respect to their parent `[0:1:1:2] as1`. 120 | * But their parent `[0:1:1:2] as1` will also have a location w/r/t its parent `[0:1:1:1] Top`. 121 | * Both of these location vectors must be applied, in sequence, to show the instances in their correct locations. 122 | 123 | ### Add a new component to one of the 2 L-bracket-assemblies 124 | 125 | * A picture is worth a thousand words. Let's construct a small 'Button' component and add it to `[0:1:1:2:2] l-bracket-assembly_2`. 126 | * First, we set `l-bracket-assembly_2` active 127 | * Create a workplane on the top face of the bracket 128 | * Draw a 5 mm diameter circle 129 | * ![Ready to create button](images/ready_to_create_button.png) 130 | * Then extrude the circle 3 mm high to create the new component. 131 | * ![Button created](images/button_created.png) 132 | * Not surprisingly, both L-bracket assemblies get the new button, since they are shared instances. 133 | * But a user of this CAD would probably be annoyed when the button shows up somewhere other than where it was intended to be! 134 | * Apparently, by virtue of being hierarchically contained within the assembly `[0:1:1:2:2] l-bracket-assembly_2`, the button component is having its postion transformed by the location vector of its containing assembly. 135 | * In ordeer to fix this, when we create a new component, we need to apply the reverse transform of the containing assembly, allowing the component to stay where we want it. 136 | 137 | -------------------------------------------------------------------------------- /docs/assembly_structure/images/as1-loaded-under-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/assembly_structure/images/as1-loaded-under-top.png -------------------------------------------------------------------------------- /docs/assembly_structure/images/button_created.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/assembly_structure/images/button_created.png -------------------------------------------------------------------------------- /docs/assembly_structure/images/components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/assembly_structure/images/components.png -------------------------------------------------------------------------------- /docs/assembly_structure/images/ready_to_create_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/assembly_structure/images/ready_to_create_button.png -------------------------------------------------------------------------------- /docs/bottle_tutorial/images/bot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/bottle_tutorial/images/bot1.png -------------------------------------------------------------------------------- /docs/bottle_tutorial/images/bot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/bottle_tutorial/images/bot2.png -------------------------------------------------------------------------------- /docs/bottle_tutorial/images/bot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/bottle_tutorial/images/bot3.png -------------------------------------------------------------------------------- /docs/bottle_tutorial/images/bot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/bottle_tutorial/images/bot4.png -------------------------------------------------------------------------------- /docs/bottle_tutorial/images/bot5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/bottle_tutorial/images/bot5.png -------------------------------------------------------------------------------- /docs/bottle_tutorial/images/wp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/bottle_tutorial/images/wp1.png -------------------------------------------------------------------------------- /docs/bottle_tutorial/images/wp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/bottle_tutorial/images/wp2.png -------------------------------------------------------------------------------- /docs/bottle_tutorial/images/wp3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/bottle_tutorial/images/wp3.png -------------------------------------------------------------------------------- /docs/bottle_tutorial/images/wp4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/bottle_tutorial/images/wp4.png -------------------------------------------------------------------------------- /docs/bottle_tutorial/images/wp5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/bottle_tutorial/images/wp5.png -------------------------------------------------------------------------------- /docs/bottle_tutorial/images/wp6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/bottle_tutorial/images/wp6.png -------------------------------------------------------------------------------- /docs/bottle_tutorial/images/wp7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/bottle_tutorial/images/wp7.png -------------------------------------------------------------------------------- /docs/bottle_tutorial/images/wp8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/bottle_tutorial/images/wp8.png -------------------------------------------------------------------------------- /docs/bottle_tutorial/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Bottle Tutorial 9 | 10 | 11 | 12 |

Tutorial: Creating the Classic OCC Bottle

13 | 14 |

Create a New Workplane

15 | 16 |

In the menubar, click on Workplane then At Origin, XY Plane.

17 | 18 | 19 | 20 |

wp1 is created in the X-Y plane of the global coordinate system. It is 21 | Active (as signified by the green background color in tree view). Also notice 22 | that a pair of construction lines has been created. One horizontal and one 23 | vertical. They intersect at a point (shown as a yellow '+' symbol) which is 24 | located at the workplane's (u=0, v=0) coordinates. Every workplane will have its own 25 | (u, v, w) local coordinate system. If you think of the workplane as being represented 26 | by a sheet of paper laying on a table in front of you, u is the horizontal direction to 27 | the right, v is the vertical direction away from you, and w is the direction normal to 28 | the paper (out of the table).

30 | 31 |

Now zoom in using one of these techniques:

32 | 33 |
    34 |
  • MMB scroll wheel
  • 35 | 36 |
  • Ctrl+RMB
  • 37 | 38 |
  • RMB
  • 39 | 40 |
  • 41 |
      42 |
    • Draw
    • 43 | 44 |
    • 45 |
        46 |
      • Fit All
      • 47 |
      48 |
    • 49 |
    50 |
  • 51 |
52 | 53 |

Draw construction lines

54 | 55 |

Click on top-most toolbar button (Horizontal Construction line), then Enter 56 | 30 into the User Input widget.

58 | 59 |

Draw 5 more construction lines at the following values: 15, 7.5, -7.5, -15, -30 and 60 | zoom in a bit more.

62 | 63 |

Draw construction circles

64 | 65 |

Click on "construction circe" toolbar button, then click on the point at 0, 30 66 | (center of circle), then the point at 0, -15.

68 | 69 |

Draw a second construction circle by first entering the coordinates of its center 70 | 0,-30 then enter the value 45 for the radius.

71 | 72 |

In general, when a 2d point is needed, you have the option of either clicking on a 73 | point or entering the point coordinates in the User Input widget. When in doubt, 74 | check the status bar in the lower left corner to find out what data is expected 75 | next.

76 | 77 |

Draw the Profile

78 | 79 |

Click on the (blue) line toolbar button and then add two lines as shown by 80 | clicking on the end points. Next, use the Arc by3Pts tool to create two arcs. 81 | (Clicking order is end, mid, end.) The profile is now complete. It needs to be a closed 82 | loop, otherwise the next step won't work.

84 | 85 |

Extrude the Bottle

86 | 87 |

In the menubar, click on Create 3D then Extrude. Enter 70 for 88 | the extrusion distance and enter Bottle for the name.

89 | 90 |

The new part Bottle is now displayed in both the 3D Display window and in the 91 | Tree View widget. Notice also that the Tree View shows Bottle highlighted in 92 | yellow, signifying that this is the Active part. It is important to remember 93 | which part is active, because any subsequent modifications will be carried out on the 94 | Active part.

95 96 | 97 |

Blend (fillet) the edges of the Bottle

98 | 99 |

Hide wp1 from the display widow by unchecking its checkbox in the Tree View. This is 100 | done to make sure that we don't accidentally select profile lines in the next step.

101 | 102 |

In the menubar, click on Modify Active Part then Fillet. Carefully 103 | select all 12 edges (one by one) of the Bottle. (I know this is a pain. Box 104 | select would be really handy, but it's not implemented yet.) Enter the fillet radius 105 | value of 3.

106 | 107 |

The Bottle part is now represented by a newly created shape which has taken 108 | the place of the previous shape (prior to blending). The previous shape is still 109 | accesible (if needed). It has been appended to a list of ancestor shapes. 110 |

111 | 112 |

Create the neck profile

113 | 114 |

In the menubar, click on Workpane then On face. Select the top face of 115 | the bottle. (The face normal will be the w direction of the new workplane.) Now 116 | select one of the flat side faces. (The face normal will be the u direction of 117 | the new workplane.) A new workplane w2 has now been created (and is active). Its 118 | 0,0 coordinates are located at the center of the bottle's top face.

119 | 120 |

Next, make a Profile circle on w2 with center at 0,0 and radius = 121 | 7.5

122 |

123 | 124 |

Create the Neck of the Bottle

125 | 126 |

Now click on Modify Active Part then Pull. Enter the value 7 127 | (the height of the Neck). This will pull a circular boss 7mm tall on the top face of 128 | the bottle.

129 | 130 |

The Bottle now has a neck. Uncheck the check box of w2. 131 | We don't need to see that any longer.

133 | 134 |

Shell the Bottle

135 | 136 |

Just for fun, try filleting the base of the neck with a radius value of 2 137 | before shelling.

138 | 139 |

Now in the menubar, click on Modify Active Part then Shell. Click 140 | first on the top circular face of the bottle, then enter a shell thickness of 141 | 1.

142 | 143 |

The Bottle is complete. If desired, it can be saved in STEP format. 144 |

145 | 146 | 147 | -------------------------------------------------------------------------------- /docs/images/bottle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/images/bottle.png -------------------------------------------------------------------------------- /docs/images/img8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/images/img8.png -------------------------------------------------------------------------------- /docs/images/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/images/ui.png -------------------------------------------------------------------------------- /docs/images/wp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/images/wp.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | KodaCAD 11 | 12 | 13 | 14 |

Getting Started with KodaCAD

15 | 16 |

Creating 3D models

18 | 19 |
    20 |
  • The basic paradigm for creating a 3D model is to start by drawing a sketch on a 21 | 2D workplane.
  • 22 | 23 |
  • The workplane contains two types of drawn elements: Construction and 24 | Profile.
  • 25 | 26 |
  • Construction elements, as their name implies, are used to construct an 27 | accurate layout. They are the dotted magenta colored lines, infinite in length.
  • 28 | 29 |
  • Where Construction elements intersect, 'selectable' yellow Points are 30 | shown.
  • 31 | 32 |
  • Profile elements, if they form a closed loop, get converted to a wire 33 | which is then used to create or modify 3D bodies.
  • 34 | 35 |
  • Both Construction elements and Profile elements are drawn by 36 | clicking on Points or by entering values on the User Input widget.
  • 37 |
38 | 39 |

The User Interface

41 | 42 |
    43 |
  • The main 3D Display window is the 3D view of workplanes and 3D parts.
  • 44 | 45 |
  • To the left of the 3D Display is the Tree View window, showing the 46 | hierarchical relationship among Parts, Assemblies and Workplanes. In addition to 47 | displaying hierarchical relationships, the Tree View window also allows user editing 48 | of certain parameters, allows Parts, Assemblies and Workplanes to be shown/hidden 49 | (using the checkboxes) and shows (by color): 50 | 51 |
      52 |
    • Which Part is Active (the part that will be acted upon by 53 | the current modification or operation).
    • 54 | 55 |
    • Which Workplane is Active (the workplane that will be 56 | acted upon by any toolbar buttons).
    • 57 | 58 |
    • Which Assembly is Active (Newly created parts will be added to 59 | the active assembly).
    • 60 |
    61 |
  • 62 | 63 |
  • The Menu Bar is located across the top of the application window. In 64 | general, 3D parts are created with a workflow that starts with the menu buttons on 65 | the left (Workplane) and prcceeds to the right (Create, then 66 | Modify).
  • 67 | 68 |
  • The Toolbar Buttons, located on the far right, are used to create drawing 69 | elements on the Active Workplane.
  • 70 | 71 |
  • Along the bottom of the application window, from left to right are: 72 | 73 |
      74 |
    • A StatusBar, showing instructions, if any, for the user
    • 75 | 76 |
    • A User Input widget into which numerical values (v), 2D coordinates 77 | (x, y), or other text (such as part name) can be entered.
    • 78 | 79 |
    • A Current Operation label, showing the current opertion.
    • 80 | 81 |
    • An End Operation button, allowing the user to end the current 82 | opertion.
    • 83 | 84 |
    • A Units label, showing the currently selected units.
    • 85 |
    86 |
  • 87 |
88 | 89 |

Navigation

90 | 91 |

Using a scroll-wheel mouse, the user can rotate, pan and zoom the model in 92 | the 3D Display Window.

93 | 94 |
    95 |
  • LMB for rotation
  • 96 | 97 |
  • MMB to pan the view
  • 98 | 99 |
  • Scroll wheel to Zoom the view
  • 100 | 101 |
  • RMB for popup options (on both Display and Tree View)
  • 102 |
103 | 104 |

Download & Installation:

105 | 106 |

KodaCAD is very much still in development and is not being made available 107 | in an executable binary format. In order to run it, you need to have Python version 3.7 108 | (or higher) and pythonOCC (version 7.4 or 7.5) installed on your computer. The easiest 109 | way to get this (the way I did it) is to: 110 | 111 |

    112 | 113 |
  • Download and install Conda (Choose Python3, not Python2.7.)
  • 116 | 117 |
  • Once you have got that, set up a PythonOCC environment within Conda.
  • 118 | 119 |
  • Within that environment, install pythonocc-core and pyqt.
  • 122 | 123 |
  • Clone the KodaCAD GitHub repository onto your computer.
  • 125 | 126 |
  • Then from within the PythonOCC environment, run the file "kodacad.py".
  • 127 | 128 |
129 | 130 |

131 | 132 |

Tutorial: Creating the Classic OCC Bottle

133 | 134 |

Step by step tutorial

137 | 138 |

Demo: Load STEP / Modify / Save STEP

139 | 140 |

Demonstration of Load STEP / Modify / Save STEP

142 | 143 | 144 |

Author and Maintainer of KodaCAD:

Doug Blanding
145 | dBlanding@gmail.com 146 | 147 | 148 | -------------------------------------------------------------------------------- /docs/index.html~: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | KodaCAD 11 | 12 | 13 | 14 |

Getting Started with KodaCAD

15 | 16 |

Creating 3D models

18 | 19 |
    20 |
  • The basic paradigm for creating a 3D model is to start by drawing a sketch on a 21 | 2D workplane.
  • 22 | 23 |
  • The workplane contains two types of drawn elements: Construction and 24 | Profile.
  • 25 | 26 |
  • Construction elements, as their name implies, are used to construct an 27 | accurate layout. They are the dotted magenta colored lines, infinite in length.
  • 28 | 29 |
  • Where Construction elements intersect, 'selectable' yellow Points are 30 | shown.
  • 31 | 32 |
  • Profile elements are the ones that get assembled into a closed loop which 33 | is then used to create or modify 3D bodies. They are drawn by clicking on 34 | Points or by entering values on the User Input widget.
  • 35 |
36 | 37 |

The User Interface

39 | 40 |
    41 |
  • The main 3D Display window is the 3D view of workplanes and 3D parts.
  • 42 | 43 |
  • To the left of the 3D Display is the Tree View window, showing the 44 | hierarchical relationship among Parts, Assemblies and Workplanes. In addition to 45 | displaying hierarchical relationships, the Tree View window also allows user editing 46 | of certain parameters, allows Parts, Assemblies and Workplanes to be shown/hidden 47 | (using the checkboxes) and shows (by color): 48 | 49 |
      50 |
    • Which part is Active, (the part that will be the subject of the 51 | current modification or operation).
    • 52 | 53 |
    • Which Workplane is Active, (the workplane that will receive the 54 | effects from any toolbar buttons).
    • 55 |
    56 |
  • 57 | 58 |
  • The Menu Bar is located across the top of the application window. In 59 | general, 3D parts are created with a workflow that starts with the menu buttons on 60 | the left (Workplane) and prcceeds to the right (Create, then 61 | Modify).
  • 62 | 63 |
  • The Toolbar Buttons, located on the far right, are used to create drawing 64 | elements on the Active Workplane.
  • 65 | 66 |
  • Along the bottom of the application window, from left to right are: 67 | 68 |
      69 |
    • A StatusBar, showing instructions, if any, for the user
    • 70 | 71 |
    • A User Input widget into which numerical values (v), 2D coordinates 72 | (x, y), or other text (such as part name) can be entered.
    • 73 | 74 |
    • A Current Operation label, showing the current opertion.
    • 75 | 76 |
    • An End Operation button, allowing the user to end the current 77 | opertion.
    • 78 | 79 |
    • A Units label, showing the currently selected units.
    • 80 |
    81 |
  • 82 |
83 | 84 |

Navigation

85 | 86 |

In order to avoid accidental screen picks when using the mouse for navigation, the 87 | control key must be used with the mouse when panning or rotating the view.

88 | 89 |
    90 |
  • LMB for screen picks
  • 91 | 92 |
  • CTRL-LMB to pan the view
  • 93 | 94 |
  • CTRL-MMB to rotate the view
  • 95 | 96 |
  • CTRL-RMB (Or scroll wheel) to Zoom the view
  • 97 | 98 |
  • RMB for popup options (on both Display and Tree View)
  • 99 |
100 | 101 |

Download & Installation:

102 | 103 |

KodaCAD is very much still in development, so it isn't being made available 104 | in an executable binary format. In order to run it, you need to have Python version 3.7 105 | (or higher) and the latest version of pythonOCC installed on your computer. The easiest 106 | way to get this (the way I did it) is to download and install Conda (Choose Python3, not Python2.7.) Then install the latest 109 | version of PythonOCC. Once you have got that, set up a PythonOCC environment within 111 | Conda. Clone the KodaCAD GitHub repository onto your computer, then from within the PythonOCC environment, 113 | run the file "kodacad.py".

114 | 115 |

Tutorial: Creating the Classic OCC Bottle

116 | 117 |

Step by step tutorial

120 | 121 |

Demonstration of Load STEP / Modify / Save STEP

123 | 124 | 125 |

Author and Maintainer of KodaCAD:

Doug Blanding
126 | dBlanding@gmail.com 127 | 128 | 129 | -------------------------------------------------------------------------------- /docs/load_mod_save_demo/images/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/load_mod_save_demo/images/img1.png -------------------------------------------------------------------------------- /docs/load_mod_save_demo/images/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/load_mod_save_demo/images/img2.png -------------------------------------------------------------------------------- /docs/load_mod_save_demo/images/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/load_mod_save_demo/images/img3.png -------------------------------------------------------------------------------- /docs/load_mod_save_demo/images/img4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/load_mod_save_demo/images/img4.png -------------------------------------------------------------------------------- /docs/load_mod_save_demo/images/img5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/load_mod_save_demo/images/img5.png -------------------------------------------------------------------------------- /docs/load_mod_save_demo/images/img6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/load_mod_save_demo/images/img6.png -------------------------------------------------------------------------------- /docs/load_mod_save_demo/images/img7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/load_mod_save_demo/images/img7.png -------------------------------------------------------------------------------- /docs/load_mod_save_demo/images/img8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/load_mod_save_demo/images/img8.png -------------------------------------------------------------------------------- /docs/load_mod_save_demo/images/img9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/docs/load_mod_save_demo/images/img9.png -------------------------------------------------------------------------------- /docs/load_mod_save_demo/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Load Modify Save Demo 9 | 10 | 11 | 12 |

Demo: Load from STEP file / Modify / Save to STEP file

13 | 14 |

Load the STEP file

15 | 16 |

In the menubar, click on File then Load STEP At Top.

17 |

Navigate to the file 'as1-oc-214.stp' and load it.

18 | 19 | 20 | 21 |

In the tree view window, left click on the part l-bracket_1 22 | then right click on it to get the drop down menu.

23 |

Select Set Active. The color of the tree view item will turn 24 | yellow.

26 | 27 |

Now use the check boxes to show only l-bracket-assembly_1

28 |

Then zoom in using one of these techniques:

29 | 30 |
    31 |
  • MMB scroll wheel
  • 32 | 33 |
  • Ctrl+RMB
  • 34 | 35 |
  • RMB
  • 36 | 37 |
  • 38 |
      39 |
    • Draw
    • 40 | 41 |
    • 42 |
        43 |
      • Fit All
      • 44 |
      45 |
    • 46 |
    47 |
  • 48 |
49 | 50 |

Modify the active part

51 | 52 |

Apply fillets to the corners of the bracket, as shown. I applied a 53 | radius of 5mm to the inside corner and 10mm to the outside corners.

54 | 55 | 56 |

Now use the check boxes to show the entire assembly and right click 57 | in the graphics window to select Draw -> Fit. Notice that the changes 58 | made on the bracket are applied to both parts because they are shared 59 | instances of one part.

60 |

Next, let's edit the name of one of the bolts. Left click on one of 61 | the bolt_1 tree view items, then right click and select Edit Name.

62 | 64 | 65 |

Edit Part Names

66 | 67 |

Change the name to BOLT

68 | 69 | 70 |

Notice tht all the bolts now have the new name because they are all 71 | shared instances of a single part.

72 | 73 | 74 |

Save the Modified Assembly

75 | 76 |

Click File then Save STEP (Top) and save the modified 77 | assembly to a file.

78 | 79 | 80 |

Reload the saved file

81 | 82 |

Close Kodacad, then restart it and load the saved file

83 | 84 |

Notice that the modifications (both the geometry mods to the bracket 85 | and the name change) are still there but the color of the modified bracket 86 | has been lost somewhere along the way.

87 | 88 | 89 |

Todo: Figure out why modified parts lose their color

90 | 91 |

Seriously, if anybody can help me to figure this out, I would love 92 | to hear from you.

93 | -Doug 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /docs/load_mod_save_demo/index.html~: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Load Modify Save Demo 9 | 10 | 11 | 12 |

Demo: Load from STEP file / Modify / Save to STEP file

13 | 14 |

Load the STEP file

15 | 16 |

In the menubar, click on File then Load STEP At Top.

17 |

Navigate to the file 'as1-oc-214.stp' and load it.

18 | 19 | 20 | 21 |

In the tree view window, left click on the part l-bracket_1 22 | then right click on it to get the drop down menu.

23 |

Select Set Active. The color of the tree view item will turn 24 | yellow.

26 | 27 |

Now use the check boxes to show only l-bracket-assembly_1

28 |

Then zoom in using one of these techniques:

29 | 30 |
    31 |
  • MMB scroll wheel
  • 32 | 33 |
  • Ctrl+RMB
  • 34 | 35 |
  • RMB
  • 36 | 37 |
  • 38 |
      39 |
    • Draw
    • 40 | 41 |
    • 42 |
        43 |
      • Fit All
      • 44 |
      45 |
    • 46 |
    47 |
  • 48 |
49 | 50 |

Modify the active part

51 | 52 |

Apply fillets to the corners of the bracket, as shown. I applied a 53 | radius of 5mm to the inside corner and 10mm to the outside corners.

54 | 55 | 56 |

Now use the check boxes to show the entire assembly and right click 57 | in the graphics window to select Draw -> Fit. Notice that the changes 58 | made on the bracket are applied to both parts because they are shared 59 | instances of one part.

60 |

Next, let's edit the name of one of the bolts. Left click on one of 61 | the bolt_1 tree view items, then right click and select Edit Name.

62 | 64 | 65 |

Edit Part Names

66 | 67 |

Change the name to BOLT

68 | 69 | 70 |

Notice tht all the bolts now have the new name because they are all 71 | shared instances of a single part.

72 | 73 | 74 |

Save the Modified Assembly

75 | 76 |

Click File then Save STEP (Top) and save the modified 77 | assembly to a file.

78 | 79 | 80 |

Reload the saved file

81 | 82 |

Close Kodacad, then restart it and load the saved file

83 | 84 |

Notice that the modifications (both the geometry mods to the bracket 85 | and the name change) are still there but the color of the modified bracket 86 | has been lost somewhere along the line.

87 | 88 | 89 |

Todo: Figure out why modified parts lose their color

90 | 91 |

Seriously, if anybody can help me to figure this out, I would love 92 | to hear from you.

93 | -Doug 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /icons/abcl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/abcl.gif -------------------------------------------------------------------------------- /icons/acl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/acl.gif -------------------------------------------------------------------------------- /icons/arc3p.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/arc3p.gif -------------------------------------------------------------------------------- /icons/arcc2p.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/arcc2p.gif -------------------------------------------------------------------------------- /icons/array.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/array.gif -------------------------------------------------------------------------------- /icons/cc3p.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/cc3p.gif -------------------------------------------------------------------------------- /icons/cccirc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/cccirc.gif -------------------------------------------------------------------------------- /icons/ccirc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/ccirc.gif -------------------------------------------------------------------------------- /icons/cctan2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/cctan2.gif -------------------------------------------------------------------------------- /icons/cctan3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/cctan3.gif -------------------------------------------------------------------------------- /icons/circ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/circ.gif -------------------------------------------------------------------------------- /icons/cltan1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/cltan1.gif -------------------------------------------------------------------------------- /icons/cltan2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/cltan2.gif -------------------------------------------------------------------------------- /icons/del_c.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/del_c.gif -------------------------------------------------------------------------------- /icons/del_cel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/del_cel.gif -------------------------------------------------------------------------------- /icons/del_el.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/del_el.gif -------------------------------------------------------------------------------- /icons/del_g.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/del_g.gif -------------------------------------------------------------------------------- /icons/fillet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/fillet.gif -------------------------------------------------------------------------------- /icons/hcl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/hcl.gif -------------------------------------------------------------------------------- /icons/hvcl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/hvcl.gif -------------------------------------------------------------------------------- /icons/join.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/join.gif -------------------------------------------------------------------------------- /icons/lbcl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/lbcl.gif -------------------------------------------------------------------------------- /icons/line.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/line.gif -------------------------------------------------------------------------------- /icons/parcl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/parcl.gif -------------------------------------------------------------------------------- /icons/perpcl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/perpcl.gif -------------------------------------------------------------------------------- /icons/poly.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/poly.gif -------------------------------------------------------------------------------- /icons/rect.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/rect.gif -------------------------------------------------------------------------------- /icons/refangcl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/refangcl.gif -------------------------------------------------------------------------------- /icons/rotate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/rotate.gif -------------------------------------------------------------------------------- /icons/sep.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/sep.gif -------------------------------------------------------------------------------- /icons/slot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/slot.gif -------------------------------------------------------------------------------- /icons/split.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/split.gif -------------------------------------------------------------------------------- /icons/stretch.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/stretch.gif -------------------------------------------------------------------------------- /icons/tpcl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/tpcl.gif -------------------------------------------------------------------------------- /icons/translate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/translate.gif -------------------------------------------------------------------------------- /icons/vcl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/icons/vcl.gif -------------------------------------------------------------------------------- /m2d.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2020 Doug Blanding (dblanding@gmail.com) 4 | # 5 | # This file is part of kodacad. 6 | # The latest version of this file can be found at: 7 | # //https://github.com/dblanding/kodacad 8 | # 9 | # kodacad is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # kodacad is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # if not, write to the Free Software Foundation, Inc. 21 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 22 | # 23 | 24 | 25 | from OCC.Core.BRep import BRep_Tool 26 | from OCC.Core.TopoDS import TopoDS_Vertex, topods_Vertex 27 | 28 | 29 | class M2D: 30 | """Methods for creating and drawing elements on 2D workplanes""" 31 | 32 | def __init__(self, win, display): 33 | self.win = win 34 | self.display = display 35 | 36 | ############################################# 37 | # 38 | # Create 2d Construction Line functions 39 | # 40 | ############################################# 41 | 42 | def add_vertex_to_xyPtStack(self, shapeList): 43 | """Helper function to convert vertex to gp_Pnt and put on ptStack.""" 44 | wp = self.win.activeWp 45 | for shape in shapeList: 46 | if isinstance(shape, TopoDS_Vertex): # Guard against wrong type 47 | vrtx = topods_Vertex(shape) 48 | pnt = BRep_Tool.Pnt(vrtx) # convert vertex to type 49 | trsf = wp.Trsf.Inverted() # New transform. Don't invert wp.Trsf 50 | pnt.Transform(trsf) 51 | pt2d = (pnt.X(), pnt.Y()) # 2d point 52 | self.win.xyPtStack.append(pt2d) 53 | else: 54 | print(f"(Unwanted) shape type: {type(shape)}") 55 | 56 | def processLineEdit(self): 57 | """pop value from lineEditStack and place on floatStack or ptStack.""" 58 | 59 | text = self.win.lineEditStack.pop() 60 | if "," in text: 61 | try: 62 | xstr, ystr = text.split(",") 63 | p = (float(xstr) * self.win.unitscale, 64 | float(ystr) * self.win.unitscale) 65 | self.win.xyPtStack.append(p) 66 | except: 67 | print("Problem with processing line edit stack") 68 | else: 69 | try: 70 | self.win.floatStack.append(float(text)) 71 | except ValueError as e: 72 | print(f"{e}") 73 | 74 | def clineH(self): 75 | """Horizontal construction line""" 76 | if self.win.xyPtStack: 77 | wp = self.win.activeWp 78 | p = self.win.xyPtStack.pop() 79 | self.win.xyPtStack = [] 80 | wp.hcl(p) 81 | self.win.draw_wp(self.win.activeWpUID) 82 | else: 83 | self.win.registerCallback(self.clineHC) 84 | self.display.SetSelectionModeVertex() 85 | self.win.xyPtStack = [] 86 | self.win.clearLEStack() 87 | self.win.lineEdit.setFocus() 88 | statusText = "Select point or enter Y-value for horizontal cline." 89 | self.win.statusBar().showMessage(statusText) 90 | 91 | def clineHC(self, shapeList, *args): 92 | """Callback (collector) for clineH""" 93 | self.add_vertex_to_xyPtStack(shapeList) 94 | if self.win.lineEditStack: 95 | self.processLineEdit() 96 | if self.win.floatStack: 97 | y = self.win.floatStack.pop() * self.win.unitscale 98 | pnt = (0, y) 99 | self.win.xyPtStack.append(pnt) 100 | if self.win.xyPtStack: 101 | self.clineH() 102 | 103 | def clineV(self): 104 | """Vertical construction line""" 105 | if self.win.xyPtStack: 106 | wp = self.win.activeWp 107 | p = self.win.xyPtStack.pop() 108 | self.win.xyPtStack = [] 109 | wp.vcl(p) 110 | self.win.draw_wp(self.win.activeWpUID) 111 | else: 112 | self.win.registerCallback(self.clineVC) 113 | self.display.SetSelectionModeVertex() 114 | self.win.xyPtStack = [] 115 | self.win.clearLEStack() 116 | self.win.lineEdit.setFocus() 117 | statusText = "Select point or enter X-value for vertcal cline." 118 | self.win.statusBar().showMessage(statusText) 119 | 120 | def clineVC(self, shapeList, *args): 121 | """Callback (collector) for clineV""" 122 | self.add_vertex_to_xyPtStack(shapeList) 123 | if self.win.lineEditStack: 124 | self.processLineEdit() 125 | if self.win.floatStack: 126 | x = self.win.floatStack.pop() * self.win.unitscale 127 | pnt = (x, 0) 128 | self.win.xyPtStack.append(pnt) 129 | if self.win.xyPtStack: 130 | self.clineV() 131 | 132 | def clineHV(self): 133 | """Horizontal + Vertical construction lines""" 134 | if self.win.xyPtStack: 135 | wp = self.win.activeWp 136 | p = self.win.xyPtStack.pop() 137 | self.win.xyPtStack = [] 138 | wp.hvcl(p) 139 | self.win.draw_wp(self.win.activeWpUID) 140 | else: 141 | self.win.registerCallback(self.clineHVC) 142 | self.display.SetSelectionModeVertex() 143 | self.win.xyPtStack = [] 144 | self.win.clearLEStack() 145 | self.win.lineEdit.setFocus() 146 | statusText = "Select point or enter x,y coords for H+V cline." 147 | self.win.statusBar().showMessage(statusText) 148 | 149 | def clineHVC(self, shapeList, *args): 150 | """Callback (collector) for clineHV""" 151 | self.add_vertex_to_xyPtStack(shapeList) 152 | if self.win.lineEditStack: 153 | self.processLineEdit() 154 | if self.win.xyPtStack: 155 | self.clineHV() 156 | 157 | def cline2Pts(self): 158 | """Construction line through two points""" 159 | if len(self.win.xyPtStack) == 2: 160 | wp = self.win.activeWp 161 | p2 = self.win.xyPtStack.pop() 162 | p1 = self.win.xyPtStack.pop() 163 | wp.acl(p1, p2) 164 | self.win.xyPtStack = [] 165 | self.win.draw_wp(self.win.activeWpUID) 166 | else: 167 | self.win.registerCallback(self.cline2PtsC) 168 | self.display.SetSelectionModeVertex() 169 | self.win.xyPtStack = [] 170 | self.win.clearLEStack() 171 | self.win.lineEdit.setFocus() 172 | statusText = "Select 2 points for Construction Line." 173 | self.win.statusBar().showMessage(statusText) 174 | 175 | def cline2PtsC(self, shapeList, *args): 176 | """Callback (collector) for cline2Pts""" 177 | self.add_vertex_to_xyPtStack(shapeList) 178 | if self.win.lineEditStack: 179 | self.processLineEdit() 180 | if len(self.win.xyPtStack) == 2: 181 | self.cline2Pts() 182 | 183 | def clineAng(self): 184 | """Construction line through a point and at an angle""" 185 | if self.win.xyPtStack and self.win.floatStack: 186 | wp = self.win.activeWp 187 | text = self.win.floatStack.pop() 188 | angle = float(text) 189 | pnt = self.win.xyPtStack.pop() 190 | wp.acl(pnt, ang=angle) 191 | self.win.xyPtStack = [] 192 | self.win.draw_wp(self.win.activeWpUID) 193 | else: 194 | self.win.registerCallback(self.clineAngC) 195 | self.display.SetSelectionModeVertex() 196 | self.win.xyPtStack = [] 197 | self.win.floatStack = [] 198 | self.win.lineEditStack = [] 199 | self.win.lineEdit.setFocus() 200 | statusText = "Select point on WP (or enter x,y coords) then enter angle." 201 | self.win.statusBar().showMessage(statusText) 202 | 203 | def clineAngC(self, shapeList, *args): 204 | """Callback (collector) for clineAng""" 205 | self.add_vertex_to_xyPtStack(shapeList) 206 | self.win.lineEdit.setFocus() 207 | if self.win.lineEditStack: 208 | self.processLineEdit() 209 | if self.win.xyPtStack and self.win.floatStack: 210 | self.clineAng() 211 | 212 | def clineRefAng(self): 213 | pass 214 | 215 | def clineAngBisec(self): 216 | pass 217 | 218 | def clineLinBisec(self): 219 | """Linear bisector between two points""" 220 | if len(self.win.xyPtStack) == 2: 221 | wp = self.win.activeWp 222 | pnt2 = self.win.xyPtStack.pop() 223 | pnt1 = self.win.xyPtStack.pop() 224 | wp.lbcl(pnt1, pnt2) 225 | self.win.xyPtStack = [] 226 | self.win.draw_wp(self.win.activeWpUID) 227 | else: 228 | self.win.registerCallback(self.clineLinBisecC) 229 | self.display.SetSelectionModeVertex() 230 | 231 | def clineLinBisecC(self, shapeList, *args): 232 | """Callback (collector) for clineLinBisec""" 233 | self.add_vertex_to_xyPtStack(shapeList) 234 | if len(self.win.xyPtStack) == 2: 235 | self.clineLinBisec() 236 | 237 | def clinePara(self): 238 | pass 239 | 240 | def clinePerp(self): 241 | pass 242 | 243 | def clineTan1(self): 244 | pass 245 | 246 | def clineTan2(self): 247 | pass 248 | 249 | def ccirc(self): 250 | """Create a c-circle from center & radius or center & Pnt on circle""" 251 | wp = self.win.activeWp 252 | if len(self.win.xyPtStack) == 2: 253 | p2 = self.win.xyPtStack.pop() 254 | p1 = self.win.xyPtStack.pop() 255 | rad = wp.p2p_dist(p1, p2) 256 | wp.circle(p1, rad, constr=True) 257 | self.win.xyPtStack = [] 258 | self.win.floatStack = [] 259 | self.win.draw_wp(self.win.activeWpUID) 260 | elif self.win.xyPtStack and self.win.floatStack: 261 | pnt = self.win.xyPtStack.pop() 262 | rad = self.win.floatStack.pop() * self.win.unitscale 263 | wp.circle(pnt, rad, constr=True) 264 | self.win.xyPtStack = [] 265 | self.win.floatStack = [] 266 | self.win.draw_wp(self.win.activeWpUID) 267 | else: 268 | self.win.registerCallback(self.ccircC) 269 | self.display.SetSelectionModeVertex() 270 | self.win.xyPtStack = [] 271 | self.win.floatStack = [] 272 | self.win.lineEditStack = [] 273 | self.win.lineEdit.setFocus() 274 | statusText = "Pick center of construction circle and enter radius." 275 | self.win.statusBar().showMessage(statusText) 276 | 277 | def ccircC(self, shapeList, *args): 278 | """callback (collector) for ccirc""" 279 | self.add_vertex_to_xyPtStack(shapeList) 280 | self.win.lineEdit.setFocus() 281 | if self.win.lineEditStack: 282 | self.processLineEdit() 283 | if len(self.win.xyPtStack) == 2: 284 | self.ccirc() 285 | if self.win.xyPtStack and self.win.floatStack: 286 | self.ccirc() 287 | 288 | ############################################# 289 | # 290 | # Create 2d Edge Profile functions 291 | # 292 | ############################################# 293 | 294 | def line(self): 295 | """Create a profile geometry line between two end points.""" 296 | if len(self.win.xyPtStack) == 2: 297 | wp = self.win.activeWp 298 | pnt2 = self.win.xyPtStack.pop() 299 | pnt1 = self.win.xyPtStack.pop() 300 | wp.line(pnt1, pnt2) 301 | self.win.xyPtStack = [] 302 | self.win.draw_wp(self.win.activeWpUID) 303 | else: 304 | self.win.registerCallback(self.lineC) 305 | self.display.SetSelectionModeVertex() 306 | self.win.xyPtStack = [] 307 | self.win.lineEdit.setFocus() 308 | statusText = "Select 2 end points for line." 309 | self.win.statusBar().showMessage(statusText) 310 | 311 | def lineC(self, shapeList, *args): 312 | """callback (collector) for line""" 313 | self.add_vertex_to_xyPtStack(shapeList) 314 | self.win.lineEdit.setFocus() 315 | if self.win.lineEditStack: 316 | self.processLineEdit() 317 | if len(self.win.xyPtStack) == 2: 318 | self.line() 319 | 320 | def rect(self): 321 | """Create a profile geometry rectangle from two diagonally opposite corners.""" 322 | if len(self.win.xyPtStack) == 2: 323 | wp = self.win.activeWp 324 | pnt2 = self.win.xyPtStack.pop() 325 | pnt1 = self.win.xyPtStack.pop() 326 | wp.rect(pnt1, pnt2) 327 | self.win.xyPtStack = [] 328 | self.win.draw_wp(self.win.activeWpUID) 329 | else: 330 | self.win.registerCallback(self.rectC) 331 | self.display.SetSelectionModeVertex() 332 | self.win.xyPtStack = [] 333 | self.win.lineEdit.setFocus() 334 | statusText = "Select 2 points for Rectangle." 335 | self.win.statusBar().showMessage(statusText) 336 | 337 | def rectC(self, shapeList, *args): 338 | """callback (collector) for rect""" 339 | self.add_vertex_to_xyPtStack(shapeList) 340 | self.win.lineEdit.setFocus() 341 | if self.win.lineEditStack: 342 | self.processLineEdit() 343 | if len(self.win.xyPtStack) == 2: 344 | self.rect() 345 | 346 | def circle(self): 347 | """Create a geometry circle from cntr & rad or cntr & pnt on circle.""" 348 | wp = self.win.activeWp 349 | if len(self.win.xyPtStack) == 2: 350 | p2 = self.win.xyPtStack.pop() 351 | p1 = self.win.xyPtStack.pop() 352 | rad = wp.p2p_dist(p1, p2) 353 | wp.circle(p1, rad, constr=False) 354 | self.win.xyPtStack = [] 355 | self.win.floatStack = [] 356 | self.win.draw_wp(self.win.activeWpUID) 357 | elif self.win.xyPtStack and self.win.floatStack: 358 | pnt = self.win.xyPtStack.pop() 359 | rad = self.win.floatStack.pop() * self.win.unitscale 360 | wp.circle(pnt, rad, constr=False) 361 | self.win.xyPtStack = [] 362 | self.win.floatStack = [] 363 | self.win.draw_wp(self.win.activeWpUID) 364 | else: 365 | self.win.registerCallback(self.circleC) 366 | self.display.SetSelectionModeVertex() 367 | self.win.xyPtStack = [] 368 | self.win.floatStack = [] 369 | self.win.lineEditStack = [] 370 | self.win.lineEdit.setFocus() 371 | statusText = "Pick center and enter radius or pick center & 2nd point." 372 | self.win.statusBar().showMessage(statusText) 373 | 374 | def circleC(self, shapeList, *args): 375 | """callback (collector) for circle""" 376 | self.add_vertex_to_xyPtStack(shapeList) 377 | self.win.lineEdit.setFocus() 378 | if self.win.lineEditStack: 379 | self.processLineEdit() 380 | if len(self.win.xyPtStack) == 2: 381 | self.circle() 382 | if self.win.xyPtStack and self.win.floatStack: 383 | self.circle() 384 | 385 | def arcc2p(self): 386 | """Create an arc from center pt, start pt and end pt.""" 387 | wp = self.win.activeWp 388 | if len(self.win.xyPtStack) == 3: 389 | pe = self.win.xyPtStack.pop() 390 | ps = self.win.xyPtStack.pop() 391 | pc = self.win.xyPtStack.pop() 392 | wp.arcc2p(pc, ps, pe) 393 | self.win.xyPtStack = [] 394 | self.win.floatStack = [] 395 | self.win.draw_wp(self.win.activeWpUID) 396 | else: 397 | self.win.registerCallback(self.arcc2pC) 398 | self.display.SetSelectionModeVertex() 399 | self.win.xyPtStack = [] 400 | statusText = "Pick center of arc, then start then end point." 401 | self.win.statusBar().showMessage(statusText) 402 | 403 | def arcc2pC(self, shapeList, *args): 404 | """callback (collector) for arcc2p""" 405 | self.add_vertex_to_xyPtStack(shapeList) 406 | self.win.lineEdit.setFocus() 407 | if self.win.lineEditStack: 408 | self.processLineEdit() 409 | if len(self.win.xyPtStack) == 3: 410 | self.arcc2p() 411 | 412 | def arc3p(self): 413 | """Create an arc from start pt, end pt, and 3rd pt on the arc.""" 414 | wp = self.win.activeWp 415 | if len(self.win.xyPtStack) == 3: 416 | ps = self.win.xyPtStack.pop() 417 | pe = self.win.xyPtStack.pop() 418 | p3 = self.win.xyPtStack.pop() 419 | wp.arc3p(ps, pe, p3) 420 | self.win.xyPtStack = [] 421 | self.win.floatStack = [] 422 | self.win.draw_wp(self.win.activeWpUID) 423 | else: 424 | self.win.registerCallback(self.arc3pC) 425 | self.display.SetSelectionModeVertex() 426 | self.win.xyPtStack = [] 427 | statusText = "Pick start point on arc, then end then 3rd point on arc." 428 | self.win.statusBar().showMessage(statusText) 429 | 430 | def arc3pC(self, shapeList, *args): 431 | """Callback (collector) for arc3p""" 432 | self.add_vertex_to_xyPtStack(shapeList) 433 | self.win.lineEdit.setFocus() 434 | if self.win.lineEditStack: 435 | self.processLineEdit() 436 | if len(self.win.xyPtStack) == 3: 437 | self.arc3p() 438 | 439 | def geom(self): 440 | pass 441 | 442 | ############################################# 443 | # 444 | # 2D Delete functions 445 | # 446 | ############################################# 447 | 448 | def delCl(self): 449 | """Delete selected 2d construction element. 450 | 451 | Todo: Get this working. Able to pre-select lines from the display 452 | as type but haven't figured out how to get 453 | the type (or the cline or Geom_Line that was used to make 454 | it).""" 455 | self.win.registerCallback(self.delClC) 456 | statusText = "Select a construction element to delete." 457 | self.win.statusBar().showMessage(statusText) 458 | self.display = self.win.canvas._self.display.Context 459 | print(self.display.NbSelected()) # Use shift-select for multiple lines 460 | selected_line = self.display.SelectedInteractive() 461 | if selected_line: 462 | print(type(selected_line)) # 463 | print(selected_line.GetOwner()) # 464 | 465 | def delClC(self, shapeList, *args): 466 | """Callback (collector) for delCl""" 467 | print(shapeList) 468 | print(args) 469 | self.delCl() 470 | 471 | def delEl(self): 472 | """Delete selected geometry profile element.""" 473 | wp = self.win.activeWp 474 | if self.win.shapeStack: 475 | while self.win.shapeStack: 476 | shape = self.win.shapeStack.pop() 477 | if shape in wp.edgeList: 478 | wp.edgeList.remove(shape) 479 | self.win.redraw() 480 | else: 481 | self.win.registerCallback(self.delElC) 482 | self.display.SetSelectionModeEdge() 483 | self.win.xyPtStack = [] 484 | statusText = "Select a geometry profile element to delete." 485 | self.win.statusBar().showMessage(statusText) 486 | 487 | def delElC(self, shapeList, *args): 488 | """Callback (collector) for delEl""" 489 | for shape in shapeList: 490 | self.win.shapeStack.append(shape) 491 | if self.win.shapeStack: 492 | self.delEl() 493 | -------------------------------------------------------------------------------- /myDisplay/Readme.txt: -------------------------------------------------------------------------------- 1 | This folder is a copy of "site-packages/OCC/Display". 2 | Having this local copy facilitates changing the navigation controls. 3 | Mouse button navigation controls are defined in the file qtDisplay.py. 4 | 5 | Navigation controls have been modified as follows: 6 | Ctrl LMB Pan 7 | Ctrl MMB Rotate 8 | Ctrl RMB Zoom 9 | 10 | Using the Ctrl key as a modifier is intended to make it less likely to 11 | accidentally make a screen selection while navigating. 12 | This should reduce the problem of unwanted screen picks being sent to 13 | the registered callback function. 14 | -------------------------------------------------------------------------------- /myDisplay/SimpleGui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2009-2016 Thomas Paviot (tpaviot@gmail.com) 4 | ## 5 | # This file is part of pythonOCC. 6 | ## 7 | # pythonOCC is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | ## 12 | # pythonOCC is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | ## 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with pythonOCC. If not, see . 19 | 20 | import logging 21 | import os 22 | import sys 23 | 24 | from OCC import VERSION 25 | from OCC.Display.backend import load_backend, get_qt_modules 26 | from OCC.Display.OCCViewer import OffscreenRenderer 27 | 28 | log = logging.getLogger(__name__) 29 | 30 | 31 | def check_callable(_callable): 32 | if not callable(_callable): 33 | raise AssertionError("The function supplied is not callable") 34 | 35 | 36 | def init_display(backend_str=None, 37 | size=(1024, 768), 38 | display_triedron=True, 39 | background_gradient_color1=[206, 215, 222], 40 | background_gradient_color2=[128, 128, 128]): 41 | """ This function loads and initialize a GUI using either wx, pyq4, pyqt5 or pyside. 42 | If ever the environment variable PYTHONOCC_OFFSCREEN_RENDERER, then the GUI is simply 43 | ignored and an offscreen renderer is returned. 44 | init_display returns 4 objects : 45 | * display : an instance of Viewer3d ; 46 | * start_display : a function (the GUI mainloop) ; 47 | * add_menu : a function that creates a menu in the GUI 48 | * add_function_to_menu : adds a menu option 49 | 50 | In case an offscreen renderer is returned, start_display and add_menu are ignored, i.e. 51 | an empty function is returned (named do_nothing). add_function_to_menu just execute the 52 | function taken as a paramter. 53 | 54 | Note : the offscreen renderer is used on the travis side. 55 | """ 56 | if os.getenv("PYTHONOCC_OFFSCREEN_RENDERER") == "1": 57 | # create the offscreen renderer 58 | offscreen_renderer = OffscreenRenderer() 59 | 60 | def do_nothing(*kargs, **kwargs): 61 | """ takes as many parameters as you want, 62 | ans does nothing 63 | """ 64 | pass 65 | 66 | def call_function(s, func): 67 | """ A function that calls another function. 68 | Helpfull to bypass add_function_to_menu. s should be a string 69 | """ 70 | check_callable(func) 71 | log.info("Execute %s :: %s menu fonction" % (s, func.__name__)) 72 | func() 73 | log.info("done") 74 | 75 | # returns empty classes and functions 76 | return offscreen_renderer, do_nothing, do_nothing, call_function 77 | used_backend = load_backend(backend_str) 78 | log.info("GUI backend set to: %s", used_backend) 79 | # wxPython based simple GUI 80 | if used_backend == 'wx': 81 | import wx 82 | from OCC.Display.wxDisplay import wxViewer3d 83 | 84 | class AppFrame(wx.Frame): 85 | 86 | def __init__(self, parent): 87 | wx.Frame.__init__(self, parent, -1, "pythonOCC-%s 3d viewer ('wx' backend)" % VERSION, 88 | style=wx.DEFAULT_FRAME_STYLE, size=size) 89 | self.canva = wxViewer3d(self) 90 | self.menuBar = wx.MenuBar() 91 | self._menus = {} 92 | self._menu_methods = {} 93 | 94 | def add_menu(self, menu_name): 95 | _menu = wx.Menu() 96 | self.menuBar.Append(_menu, "&" + menu_name) 97 | self.SetMenuBar(self.menuBar) 98 | self._menus[menu_name] = _menu 99 | 100 | def add_function_to_menu(self, menu_name, _callable): 101 | # point on curve 102 | _id = wx.NewId() 103 | check_callable(_callable) 104 | try: 105 | self._menus[menu_name].Append(_id, 106 | _callable.__name__.replace('_', ' ').lower()) 107 | except KeyError: 108 | raise ValueError('the menu item %s does not exist' % menu_name) 109 | self.Bind(wx.EVT_MENU, _callable, id=_id) 110 | 111 | app = wx.App(False) 112 | win = AppFrame(None) 113 | win.Show(True) 114 | wx.SafeYield() 115 | win.canva.InitDriver() 116 | app.SetTopWindow(win) 117 | display = win.canva._display 118 | 119 | def add_menu(*args, **kwargs): 120 | win.add_menu(*args, **kwargs) 121 | 122 | def add_function_to_menu(*args, **kwargs): 123 | win.add_function_to_menu(*args, **kwargs) 124 | 125 | def start_display(): 126 | app.MainLoop() 127 | 128 | # Qt based simple GUI 129 | elif 'qt' in used_backend: 130 | from OCC.Display.qtDisplay import qtViewer3d 131 | QtCore, QtGui, QtWidgets, QtOpenGL = get_qt_modules() 132 | 133 | class MainWindow(QtWidgets.QMainWindow): 134 | 135 | def __init__(self, *args): 136 | QtWidgets.QMainWindow.__init__(self, *args) 137 | self.canva = qtViewer3d(self) 138 | self.setWindowTitle("pythonOCC-%s 3d viewer ('%s' backend)" % (VERSION, used_backend)) 139 | self.setCentralWidget(self.canva) 140 | if sys.platform != 'darwin': 141 | self.menu_bar = self.menuBar() 142 | else: 143 | # create a parentless menubar 144 | # see: http://stackoverflow.com/questions/11375176/qmenubar-and-qmenu-doesnt-show-in-mac-os-x?lq=1 145 | # noticeable is that the menu ( alas ) is created in the 146 | # topleft of the screen, just 147 | # next to the apple icon 148 | # still does ugly things like showing the "Python" menu in 149 | # bold 150 | self.menu_bar = QtWidgets.QMenuBar() 151 | self._menus = {} 152 | self._menu_methods = {} 153 | # place the window in the center of the screen, at half the 154 | # screen size 155 | self.centerOnScreen() 156 | 157 | def centerOnScreen(self): 158 | '''Centers the window on the screen.''' 159 | resolution = QtWidgets.QApplication.desktop().screenGeometry() 160 | x = (resolution.width() - self.frameSize().width()) / 2 161 | y = (resolution.height() - self.frameSize().height()) / 2 162 | self.move(x, y) 163 | 164 | def add_menu(self, menu_name): 165 | _menu = self.menu_bar.addMenu("&" + menu_name) 166 | self._menus[menu_name] = _menu 167 | 168 | def add_function_to_menu(self, menu_name, _callable): 169 | check_callable(_callable) 170 | try: 171 | _action = QtWidgets.QAction(_callable.__name__.replace('_', ' ').lower(), self) 172 | # if not, the "exit" action is now shown... 173 | _action.setMenuRole(QtWidgets.QAction.NoRole) 174 | _action.triggered.connect(_callable) 175 | 176 | self._menus[menu_name].addAction(_action) 177 | except KeyError: 178 | raise ValueError('the menu item %s does not exist' % menu_name) 179 | 180 | # following couple of lines is a tweak to enable ipython --gui='qt' 181 | app = QtWidgets.QApplication.instance() # checks if QApplication already exists 182 | if not app: # create QApplication if it doesnt exist 183 | app = QtWidgets.QApplication(sys.argv) 184 | win = MainWindow() 185 | win.resize(size[0] -1, size[1] -1) 186 | win.show() 187 | win.centerOnScreen() 188 | win.canva.InitDriver() 189 | win.resize(size[0], size[1]) 190 | win.canva.qApp = app 191 | display = win.canva._display 192 | 193 | def add_menu(*args, **kwargs): 194 | win.add_menu(*args, **kwargs) 195 | 196 | def add_function_to_menu(*args, **kwargs): 197 | win.add_function_to_menu(*args, **kwargs) 198 | 199 | def start_display(): 200 | win.raise_() # make the application float to the top 201 | app.exec_() 202 | 203 | if display_triedron: 204 | display.display_triedron() 205 | 206 | if background_gradient_color1 and background_gradient_color2: 207 | # background gradient 208 | display.set_bg_gradient_color(background_gradient_color1, background_gradient_color2) 209 | 210 | return display, start_display, add_menu, add_function_to_menu 211 | 212 | 213 | if __name__ == '__main__': 214 | display, start_display, add_menu, add_function_to_menu = init_display("qt-pyqt5") 215 | from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeSphere, BRepPrimAPI_MakeBox 216 | 217 | def sphere(event=None): 218 | display.DisplayShape(BRepPrimAPI_MakeSphere(100).Shape(), update=True) 219 | 220 | def cube(event=None): 221 | display.DisplayShape(BRepPrimAPI_MakeBox(1, 1, 1).Shape(), update=True) 222 | 223 | def quit(event=None): 224 | sys.exit() 225 | 226 | add_menu('primitives') 227 | add_function_to_menu('primitives', sphere) 228 | add_function_to_menu('primitives', cube) 229 | add_function_to_menu('primitives', quit) 230 | start_display() 231 | -------------------------------------------------------------------------------- /myDisplay/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/myDisplay/__init__.py -------------------------------------------------------------------------------- /myDisplay/backend.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | # backend constants 5 | WX = "wx" 6 | PYSIDE = "qt-pyside" 7 | PYQT4 = "qt-pyqt4" 8 | PYQT5 = "qt-pyqt5" 9 | 10 | # backend module 11 | HAVE_PYQT5, HAVE_PYQT4, HAVE_PYSIDE, HAVE_WX = False, False, False, False 12 | 13 | # is any backend imported? 14 | HAVE_BACKEND = False 15 | BACKEND_MODULE = "No backend loaded" 16 | 17 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 18 | log = logging.getLogger(__name__) 19 | 20 | 21 | def load_pyqt5(): 22 | """ returns True is PyQt5 found, else False 23 | """ 24 | global HAVE_PYQT5, QtCore, QtGui, QtWidgets, QtOpenGL 25 | 26 | # backend already loaded, dont load another one 27 | if loaded_backend(): 28 | return False 29 | 30 | try: 31 | from PyQt5 import QtCore, QtGui, QtOpenGL, QtWidgets 32 | HAVE_PYQT5 = True 33 | except ImportError: 34 | HAVE_PYQT5 = False 35 | return HAVE_PYQT5 36 | 37 | 38 | def load_pyqt4(): 39 | """ returns True is PyQt4 found, else False 40 | """ 41 | global HAVE_PYQT4, QtCore, QtGui, QtWidgets, QtOpenGL 42 | 43 | # backend already loaded, dont load another one 44 | if loaded_backend(): 45 | return False 46 | 47 | try: 48 | from PyQt4 import QtCore, QtGui, QtOpenGL 49 | QtWidgets = QtGui 50 | HAVE_PYQT4 = True 51 | except ImportError: 52 | HAVE_PYQT4 = False 53 | return HAVE_PYQT4 54 | 55 | 56 | def load_pyside(): 57 | """ returns True is PySide found, else False 58 | """ 59 | global HAVE_PYSIDE, QtCore, QtGui, QtWidgets, QtOpenGL 60 | 61 | # backend already loaded, dont load another one 62 | if loaded_backend(): 63 | return False 64 | 65 | try: 66 | from PySide import QtCore, QtGui, QtOpenGL 67 | QtWidgets = QtGui 68 | HAVE_PYSIDE = True 69 | except ImportError: 70 | HAVE_PYSIDE = False 71 | return HAVE_PYSIDE 72 | 73 | 74 | def load_wx(): 75 | """ returns True is wxPython found, else False 76 | """ 77 | 78 | # backend already loaded, dont load another one 79 | if loaded_backend(): 80 | return False 81 | 82 | global HAVE_WX 83 | try: 84 | import wx 85 | HAVE_WX = True 86 | except ImportError: 87 | HAVE_WX = False 88 | return HAVE_WX 89 | 90 | 91 | def loaded_backend(): 92 | return HAVE_BACKEND 93 | 94 | 95 | def get_loaded_backend(): 96 | return BACKEND_MODULE 97 | 98 | 99 | def load_any_qt_backend(): 100 | """ loads any qt based backend. First try to load 101 | PyQt5, then PyQt4 and finally PySide. Raise an exception 102 | if none of them are available 103 | """ 104 | pyqt5_loaded = False 105 | pyqt4_loaded = False 106 | pyside_loaded = False 107 | # by default, load PyQt5 108 | pyqt5_loaded = load_backend(PYQT5) 109 | if not pyqt5_loaded: 110 | # load pyqt4 111 | pyqt4_loaded = load_backend(PYQT4) 112 | # finally try to load pyside 113 | if not pyqt4_loaded: 114 | pyside_loaded = load_backend(PYSIDE) 115 | if not (pyqt5_loaded or pyqt4_loaded or pyside_loaded): 116 | raise AssertionError("None of the PyQt5 orPtQt4 or PySide backend can be loaded") 117 | else: 118 | return True 119 | 120 | 121 | def load_backend(backend_str=None): 122 | """ loads a gui backend 123 | 124 | If no Qt (such as PyQt5, PyQt4 or PySide) backend is found, wx is loaded 125 | 126 | The search order for pythonocc compatible gui modules is: 127 | PyQt5, PyQt4, PySide, Wx 128 | 129 | Note 130 | ---- 131 | Wx is imported when no Qt backend is found. 132 | 133 | Parameters 134 | ---------- 135 | backend_str : str 136 | 137 | specifies which backend to load 138 | 139 | backend_str is one of ( "qt-pyqt5", "qt-pyqt4", "qt-pyside", "wx" ) 140 | 141 | if no value has been set, load the first module in gui module search 142 | order 143 | 144 | Returns 145 | ------- 146 | str 147 | the name of the loaded backend 148 | one of ( "qt-pyqt5", "qt-pyqt4", "qt-pyside", "wx" ) 149 | 150 | Raises 151 | ------ 152 | 153 | ValueError 154 | * when a backend is already loaded 155 | * when an invalid backend_str is specified 156 | 157 | ImportError 158 | when the backend specified in ``backend_str`` could not be imported 159 | 160 | """ 161 | global HAVE_BACKEND, BACKEND_MODULE 162 | 163 | if HAVE_BACKEND: 164 | msg = "The {0} backend is already loaded..." \ 165 | "``load_backend`` can only be called once per session".format(BACKEND_MODULE) 166 | log.info(msg) 167 | return BACKEND_MODULE 168 | 169 | if backend_str is not None: 170 | compatible_backends = (PYQT5, PYQT4, PYSIDE, WX) 171 | if not backend_str in compatible_backends: 172 | msg = "incompatible backend_str specified: {0}\n" \ 173 | "backend is one of : {1}".format(backend_str, 174 | compatible_backends) 175 | log.critical(msg) 176 | raise ValueError(msg) 177 | 178 | if backend_str == PYQT5 or backend_str is None: 179 | if load_pyqt5(): 180 | HAVE_BACKEND = True 181 | BACKEND_MODULE = 'qt-pyqt5' 182 | log.info("backend loaded: {0}".format(BACKEND_MODULE)) 183 | return BACKEND_MODULE 184 | if backend_str == PYQT5 and not HAVE_BACKEND: 185 | msg = "{0} backend could not be loaded".format(backend_str) 186 | log.exception(msg) 187 | raise ValueError(msg) 188 | else: 189 | pass 190 | 191 | if backend_str == PYQT4 or (backend_str is None and not HAVE_BACKEND): 192 | if load_pyqt4(): 193 | HAVE_BACKEND = True 194 | BACKEND_MODULE = 'qt-pyqt4' 195 | log.info("backend loaded: {0}".format(BACKEND_MODULE)) 196 | return BACKEND_MODULE 197 | elif backend_str == PYQT4 and not HAVE_BACKEND: 198 | msg = "{0} backend could not be loaded".format(backend_str) 199 | log.exception(msg) 200 | raise ValueError(msg) 201 | 202 | else: 203 | pass 204 | 205 | if backend_str == PYSIDE or (backend_str is None and not HAVE_BACKEND): 206 | if load_pyside(): 207 | HAVE_BACKEND = True 208 | BACKEND_MODULE = 'qt-pyside' 209 | log.info("backend loaded: {0}".format(BACKEND_MODULE)) 210 | return BACKEND_MODULE 211 | elif backend_str == PYSIDE and not HAVE_BACKEND: 212 | msg = "{0} backend could not be loaded".format(backend_str) 213 | log.exception(msg) 214 | raise ValueError(msg) 215 | else: 216 | pass 217 | 218 | if backend_str == WX or (backend_str is None and not HAVE_BACKEND): 219 | if load_wx(): 220 | HAVE_BACKEND = True 221 | BACKEND_MODULE = 'wx' 222 | log.info("backend loaded: {0}".format(BACKEND_MODULE)) 223 | return BACKEND_MODULE 224 | elif backend_str == WX and not HAVE_BACKEND: 225 | msg = "{0} backend could not be loaded".format(backend_str) 226 | log.exception(msg) 227 | raise ValueError(msg) 228 | else: 229 | pass 230 | 231 | if not HAVE_BACKEND: 232 | raise ImportError("No compliant GUI library could be imported.\n" 233 | "Either PyQt5, PyQt4, PySide, or wxPython " 234 | "is required") 235 | 236 | 237 | def get_qt_modules(): 238 | """ 239 | 240 | Returns 241 | ------- 242 | tuple : ( QtCore, QtGui, QtWidgets, QtOpenGL ) 243 | here QtWidgets shadows QtGui when a PyQt4 or PySide module is loaded 244 | this is the most coherent way to get PyQt5 compliant code 245 | 246 | Raises 247 | ------ 248 | 249 | ValueError 250 | when no Qt backend has been yet loaded 251 | informs the user to call `load_backend` or that no Qt python module 252 | ( PyQt5, PyQt4 or PySide ) is found 253 | 254 | """ 255 | if not HAVE_BACKEND: 256 | raise ValueError("no backend has been imported yet with " 257 | "``load_backend``... ") 258 | 259 | if HAVE_PYQT5 or HAVE_PYQT4 or HAVE_PYSIDE: 260 | return QtCore, QtGui, QtWidgets, QtOpenGL 261 | elif HAVE_WX: 262 | raise ValueError("the Wx backend is already loaded") 263 | else: 264 | msg = ("no Qt backend is loaded, hence cannot return any modules\n" 265 | "either you havent got PyQt5, PyQt4 or PySide installed\n" 266 | "or you havent yet loaded a backend with the " 267 | "`OCC.Display.backend.load_backend` function") 268 | raise ValueError(msg) 269 | -------------------------------------------------------------------------------- /myDisplay/icons/cursor-magnify-area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/myDisplay/icons/cursor-magnify-area.png -------------------------------------------------------------------------------- /myDisplay/icons/cursor-magnify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/myDisplay/icons/cursor-magnify.png -------------------------------------------------------------------------------- /myDisplay/icons/cursor-pan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/myDisplay/icons/cursor-pan.png -------------------------------------------------------------------------------- /myDisplay/icons/cursor-rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/myDisplay/icons/cursor-rotate.png -------------------------------------------------------------------------------- /myDisplay/qtDisplay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ##Copyright 2009-2019 Thomas Paviot (tpaviot@gmail.com) 4 | ## 5 | ##This file is part of pythonOCC. 6 | ## 7 | ##pythonOCC is free software: you can redistribute it and/or modify 8 | ##it under the terms of the GNU Lesser General Public License as published by 9 | ##the Free Software Foundation, either version 3 of the License, or 10 | ##(at your option) any later version. 11 | ## 12 | ##pythonOCC is distributed in the hope that it will be useful, 13 | ##but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ##GNU Lesser General Public License for more details. 16 | ## 17 | ##You should have received a copy of the GNU Lesser General Public License 18 | ##along with pythonOCC. If not, see . 19 | 20 | from __future__ import print_function 21 | 22 | import logging 23 | import os 24 | import sys 25 | 26 | from OCC.Display import OCCViewer 27 | from OCC.Display.backend import get_qt_modules 28 | 29 | QtCore, QtGui, QtWidgets, QtOpenGL = get_qt_modules() 30 | 31 | # check if signal available, not available 32 | # on PySide 33 | HAVE_PYQT_SIGNAL = hasattr(QtCore, 'pyqtSignal') 34 | 35 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 36 | log = logging.getLogger(__name__) 37 | 38 | 39 | class qtBaseViewer(QtOpenGL.QGLWidget): 40 | ''' The base Qt Widget for an OCC viewer 41 | ''' 42 | def __init__(self, parent=None): 43 | super(qtBaseViewer, self).__init__(parent) 44 | self._display = None 45 | self._inited = False 46 | 47 | # enable Mouse Tracking 48 | self.setMouseTracking(True) 49 | 50 | # Strong focus 51 | self.setFocusPolicy(QtCore.Qt.WheelFocus) 52 | 53 | # required for overpainting the widget 54 | self.setAttribute(QtCore.Qt.WA_PaintOnScreen) 55 | self.setAttribute(QtCore.Qt.WA_NoSystemBackground) 56 | 57 | self.setAutoFillBackground(False) 58 | 59 | def GetHandle(self): 60 | ''' returns an the identifier of the GUI widget. 61 | It must be an integer 62 | ''' 63 | win_id = self.winId() # this returns either an int or voitptr 64 | if "%s" % type(win_id) == "": # PySide 65 | ### with PySide, self.winId() does not return an integer 66 | if sys.platform == "win32": 67 | ## Be careful, this hack is py27 specific 68 | ## does not work with python31 or higher 69 | ## since the PyCObject api was changed 70 | import ctypes 71 | ctypes.pythonapi.PyCObject_AsVoidPtr.restype = ctypes.c_void_p 72 | ctypes.pythonapi.PyCObject_AsVoidPtr.argtypes = [ctypes.py_object] 73 | win_id = ctypes.pythonapi.PyCObject_AsVoidPtr(win_id) 74 | elif not isinstance(win_id, int): # PyQt4 or 5 75 | ## below integer cast may be required because self.winId() can 76 | ## returns a sip.voitptr according to the PyQt version used 77 | ## as well as the python version 78 | win_id = int(win_id) 79 | return win_id 80 | 81 | def resizeEvent(self, event): 82 | if self._inited: 83 | super(qtBaseViewer, self).resizeEvent(event) 84 | self._display.OnResize() 85 | 86 | 87 | class qtViewer3d(qtBaseViewer): 88 | 89 | # emit signal when selection is changed 90 | # is a list of TopoDS_* 91 | if HAVE_PYQT_SIGNAL: 92 | sig_topods_selected = QtCore.pyqtSignal(list) 93 | 94 | def __init__(self, *kargs): 95 | qtBaseViewer.__init__(self, *kargs) 96 | 97 | self.setObjectName("qt_viewer_3d") 98 | 99 | self._drawbox = False 100 | self._zoom_area = False 101 | self._select_area = False 102 | self._inited = False 103 | self._leftisdown = False 104 | self._middleisdown = False 105 | self._rightisdown = False 106 | self._selection = None 107 | self._drawtext = True 108 | self._qApp = QtWidgets.QApplication.instance() 109 | self._key_map = {} 110 | self._current_cursor = "arrow" 111 | self._available_cursors = {} 112 | 113 | @property 114 | def qApp(self): 115 | # reference to QApplication instance 116 | return self._qApp 117 | 118 | @qApp.setter 119 | def qApp(self, value): 120 | self._qApp = value 121 | 122 | def InitDriver(self): 123 | self._display = OCCViewer.Viewer3d(window_handle=self.GetHandle(), parent=self) 124 | self._display.Create() 125 | # background gradient 126 | self._display.SetModeShaded() 127 | self._inited = True 128 | # dict mapping keys to functions 129 | self._key_map = {ord('W'): self._display.SetModeWireFrame, 130 | ord('S'): self._display.SetModeShaded, 131 | ord('A'): self._display.EnableAntiAliasing, 132 | ord('B'): self._display.DisableAntiAliasing, 133 | ord('H'): self._display.SetModeHLR, 134 | ord('F'): self._display.FitAll, 135 | ord('G'): self._display.SetSelectionMode} 136 | self.createCursors() 137 | 138 | def createCursors(self): 139 | module_pth = os.path.abspath(os.path.dirname(__file__)) 140 | icon_pth = os.path.join(module_pth, "icons") 141 | 142 | _CURSOR_PIX_ROT = QtGui.QPixmap(os.path.join(icon_pth, "cursor-rotate.png")) 143 | _CURSOR_PIX_PAN = QtGui.QPixmap(os.path.join(icon_pth, "cursor-pan.png")) 144 | _CURSOR_PIX_ZOOM = QtGui.QPixmap(os.path.join(icon_pth, "cursor-magnify.png")) 145 | _CURSOR_PIX_ZOOM_AREA = QtGui.QPixmap(os.path.join(icon_pth, "cursor-magnify-area.png")) 146 | 147 | self._available_cursors = { 148 | "arrow": QtGui.QCursor(QtCore.Qt.ArrowCursor), # default 149 | "pan": QtGui.QCursor(_CURSOR_PIX_PAN), 150 | "rotate": QtGui.QCursor(_CURSOR_PIX_ROT), 151 | "zoom": QtGui.QCursor(_CURSOR_PIX_ZOOM), 152 | "zoom-area": QtGui.QCursor(_CURSOR_PIX_ZOOM_AREA), 153 | } 154 | 155 | self._current_cursor = "arrow" 156 | 157 | def keyPressEvent(self, event): # original 158 | code = event.key() 159 | if code in self._key_map: 160 | self._key_map[code]() 161 | elif code in range(256): 162 | log.info('key: "%s"(code %i) not mapped to any function' % (chr(code), code)) 163 | else: 164 | log.info('key: code %i not mapped to any function' % code) 165 | 166 | def keyPressEvent(self, event): # modified 167 | code = event.key() 168 | if code in self._key_map: 169 | self._key_map[code]() 170 | elif code in range(256): 171 | log.info('key: "%s"(code %i) not mapped to any function' % (chr(code), code)) 172 | else: 173 | pass # Keep quiet about pressing 'ctrl' key 174 | 175 | def focusInEvent(self, event): 176 | if self._inited: 177 | self._display.Repaint() 178 | 179 | def focusOutEvent(self, event): 180 | if self._inited: 181 | self._display.Repaint() 182 | 183 | def paintEvent(self, event): 184 | if self._drawbox: 185 | self._display.Repaint() 186 | self._display.Repaint() 187 | painter = QtGui.QPainter(self) 188 | painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0), 2)) 189 | rect = QtCore.QRect(*self._drawbox) 190 | painter.drawRect(rect) 191 | 192 | def wheelEvent(self, event): 193 | try: # PyQt4/PySide 194 | delta = event.delta() 195 | except: # PyQt5 196 | delta = event.angleDelta().y() 197 | if delta > 0: 198 | zoom_factor = 2. 199 | else: 200 | zoom_factor = 0.5 201 | self._display.ZoomFactor(zoom_factor) 202 | 203 | @property 204 | def cursor(self): 205 | return self._current_cursor 206 | 207 | @cursor.setter 208 | def cursor(self, value): 209 | if not self._current_cursor == value: 210 | 211 | self._current_cursor = value 212 | cursor = self._available_cursors.get(value) 213 | 214 | if cursor: 215 | self.qApp.setOverrideCursor(cursor) 216 | else: 217 | self.qApp.restoreOverrideCursor() 218 | 219 | def mousePressEvent(self, event): 220 | self.setFocus() 221 | ev = event.pos() 222 | self.dragStartPosX = ev.x() 223 | self.dragStartPosY = ev.y() 224 | self._display.StartRotation(self.dragStartPosX, self.dragStartPosY) 225 | 226 | def mouseReleaseEvent(self, event): 227 | pt = event.pos() 228 | modifiers = event.modifiers() 229 | 230 | if event.button() == QtCore.Qt.LeftButton: 231 | if self._select_area: 232 | [Xmin, Ymin, dx, dy] = self._drawbox 233 | self._display.SelectArea(Xmin, Ymin, Xmin + dx, Ymin + dy) 234 | self._select_area = False 235 | else: 236 | # multiple select if shift is pressed 237 | if modifiers == QtCore.Qt.ShiftModifier: 238 | self._display.ShiftSelect(pt.x(), pt.y()) 239 | else: 240 | # single select otherwise 241 | self._display.Select(pt.x(), pt.y()) 242 | 243 | if (self._display.selected_shapes is not None) and HAVE_PYQT_SIGNAL: 244 | self.sig_topods_selected.emit(self._display.selected_shapes) 245 | 246 | 247 | elif event.button() == QtCore.Qt.RightButton: 248 | if self._zoom_area: 249 | [Xmin, Ymin, dx, dy] = self._drawbox 250 | self._display.ZoomArea(Xmin, Ymin, Xmin + dx, Ymin + dy) 251 | self._zoom_area = False 252 | 253 | self.cursor = "arrow" 254 | 255 | def DrawBox(self, event): 256 | tolerance = 2 257 | pt = event.pos() 258 | dx = pt.x() - self.dragStartPosX 259 | dy = pt.y() - self.dragStartPosY 260 | if abs(dx) <= tolerance and abs(dy) <= tolerance: 261 | return 262 | self._drawbox = [self.dragStartPosX, self.dragStartPosY, dx, dy] 263 | 264 | 265 | def mouseMoveEvent(self, evt): # Original version 266 | pt = evt.pos() 267 | buttons = int(evt.buttons()) 268 | modifiers = evt.modifiers() 269 | # ROTATE 270 | if (buttons == QtCore.Qt.LeftButton and 271 | not modifiers == QtCore.Qt.ShiftModifier): 272 | self.cursor = "rotate" 273 | self._display.Rotation(pt.x(), pt.y()) 274 | self._drawbox = False 275 | # DYNAMIC ZOOM 276 | elif (buttons == QtCore.Qt.RightButton and 277 | not modifiers == QtCore.Qt.ShiftModifier): 278 | self.cursor = "zoom" 279 | self._display.Repaint() 280 | self._display.DynamicZoom(abs(self.dragStartPosX), 281 | abs(self.dragStartPosY), abs(pt.x()), 282 | abs(pt.y())) 283 | self.dragStartPosX = pt.x() 284 | self.dragStartPosY = pt.y() 285 | self._drawbox = False 286 | # PAN 287 | elif buttons == QtCore.Qt.MidButton: 288 | dx = pt.x() - self.dragStartPosX 289 | dy = pt.y() - self.dragStartPosY 290 | self.dragStartPosX = pt.x() 291 | self.dragStartPosY = pt.y() 292 | self.cursor = "pan" 293 | self._display.Pan(dx, -dy) 294 | self._drawbox = False 295 | # DRAW BOX 296 | # ZOOM WINDOW 297 | elif (buttons == QtCore.Qt.RightButton and 298 | modifiers == QtCore.Qt.ShiftModifier): 299 | self._zoom_area = True 300 | self.cursor = "zoom-area" 301 | self.DrawBox(evt) 302 | self.update() 303 | # SELECT AREA 304 | elif (buttons == QtCore.Qt.LeftButton and 305 | modifiers == QtCore.Qt.ShiftModifier): 306 | self._select_area = True 307 | self.DrawBox(evt) 308 | self.update() 309 | else: 310 | self._drawbox = False 311 | self._display.MoveTo(pt.x(), pt.y()) 312 | self.cursor = "arrow" 313 | 314 | def mouseMoveEvent(self, evt): # Modified version 315 | pt = evt.pos() 316 | buttons = int(evt.buttons()) 317 | modifiers = evt.modifiers() 318 | # ROTATE 319 | if (buttons == QtCore.Qt.MidButton and 320 | modifiers == QtCore.Qt.ControlModifier): 321 | self.cursor = "rotate" 322 | self._display.Rotation(pt.x(), pt.y()) 323 | self._drawbox = False 324 | # DYNAMIC ZOOM 325 | elif (buttons == QtCore.Qt.RightButton and 326 | modifiers == QtCore.Qt.ControlModifier): 327 | self.cursor = "zoom" 328 | self._display.Repaint() 329 | self._display.DynamicZoom(abs(self.dragStartPosX), 330 | abs(self.dragStartPosY), abs(pt.x()), 331 | abs(pt.y())) 332 | self.dragStartPosX = pt.x() 333 | self.dragStartPosY = pt.y() 334 | self._drawbox = False 335 | # PAN 336 | elif (buttons == QtCore.Qt.LeftButton and 337 | modifiers == QtCore.Qt.ControlModifier): 338 | dx = pt.x() - self.dragStartPosX 339 | dy = pt.y() - self.dragStartPosY 340 | self.dragStartPosX = pt.x() 341 | self.dragStartPosY = pt.y() 342 | self.cursor = "pan" 343 | self._display.Pan(dx, -dy) 344 | self._drawbox = False 345 | # DRAW BOX 346 | # ZOOM WINDOW 347 | elif (buttons == QtCore.Qt.RightButton and 348 | modifiers == QtCore.Qt.ControlModifier): 349 | self._zoom_area = True 350 | self.cursor = "zoom-area" 351 | self.DrawBox(evt) 352 | self.update() 353 | # SELECT AREA 354 | elif (buttons == QtCore.Qt.LeftButton and 355 | modifiers == QtCore.Qt.ControlModifier): 356 | self._select_area = True 357 | self.DrawBox(evt) 358 | self.update() 359 | else: 360 | self._drawbox = False 361 | self._display.MoveTo(pt.x(), pt.y()) 362 | self.cursor = "arrow" 363 | -------------------------------------------------------------------------------- /myDisplay/wxDisplay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ##Copyright 2008-2017 Thomas Paviot (tpaviot@gmail.com) 4 | ## 5 | ##This file is part of pythonOCC. 6 | ## 7 | ##pythonOCC is free software: you can redistribute it and/or modify 8 | ##it under the terms of the GNU Lesser General Public License as published by 9 | ##the Free Software Foundation, either version 3 of the License, or 10 | ##(at your option) any later version. 11 | ## 12 | ##pythonOCC is distributed in the hope that it will be useful, 13 | ##but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ##GNU Lesser General Public License for more details. 16 | ## 17 | ##You should have received a copy of the GNU Lesser General Public License 18 | ##along with pythonOCC. If not, see . 19 | 20 | from __future__ import print_function 21 | 22 | import time 23 | 24 | try: 25 | import wx 26 | except ImportError: 27 | raise ImportError('Please install wxPython.') 28 | from OCC.Display import OCCViewer 29 | 30 | 31 | class wxBaseViewer(wx.Panel): 32 | def __init__(self, parent=None): 33 | wx.Panel.__init__(self, parent) 34 | self.Bind(wx.EVT_SIZE, self.OnSize) 35 | self.Bind(wx.EVT_IDLE, self.OnIdle) 36 | self.Bind(wx.EVT_MOVE, self.OnMove) 37 | self.Bind(wx.EVT_SET_FOCUS, self.OnFocus) 38 | self.Bind(wx.EVT_KILL_FOCUS, self.OnLostFocus) 39 | self.Bind(wx.EVT_MAXIMIZE, self.OnMaximize) 40 | self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) 41 | self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) 42 | self.Bind(wx.EVT_MIDDLE_DOWN, self.OnMiddleDown) 43 | self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) 44 | self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) 45 | self.Bind(wx.EVT_MIDDLE_UP, self.OnMiddleUp) 46 | self.Bind(wx.EVT_MOTION, self.OnMotion) 47 | self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) 48 | self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheelScroll) 49 | 50 | self._display = None 51 | self._inited = False 52 | 53 | def GetWinId(self): 54 | """ Returns the windows Id as an integer. 55 | issue with GetHandle on Linux for wx versions 56 | >3 or 4. Window must be displayed before GetHandle is 57 | called. For that, just wait for a few milliseconds/seconds 58 | before calling InitDriver 59 | a solution is given here 60 | see https://github.com/cztomczak/cefpython/issues/349 61 | but raises an issue with wxPython 4.x 62 | finally, it seems that the sleep function does the job 63 | reported as a pythonocc issue 64 | https://github.com/tpaviot/pythonocc-core/476 65 | """ 66 | timeout = 10 # 10 seconds 67 | win_id = self.GetHandle() 68 | init_time = time.time() 69 | delta_t = 0. # elapsed time, initialized to 0 before the while loop 70 | # if ever win_id is 0, enter the loop untill it gets a value 71 | while win_id == 0 and delta_t < timeout: 72 | time.sleep(0.1) 73 | wx.SafeYield() 74 | win_id = self.GetHandle() 75 | delta_t = time.time() - init_time 76 | # check that win_id is different from 0 77 | if win_id == 0: 78 | raise AssertionError("Can't get win Id") 79 | # otherwise returns the window Id 80 | return win_id 81 | 82 | def OnSize(self, event): 83 | if self._inited: 84 | self._display.OnResize() 85 | 86 | def OnIdle(self, event): 87 | pass 88 | 89 | def OnMove(self, event): 90 | pass 91 | 92 | def OnFocus(self, event): 93 | pass 94 | 95 | def OnLostFocus(self, event): 96 | pass 97 | 98 | def OnMaximize(self, event): 99 | pass 100 | 101 | def OnLeftDown(self, event): 102 | pass 103 | 104 | def OnRightDown(self, event): 105 | pass 106 | 107 | def OnMiddleDown(self, event): 108 | pass 109 | 110 | def OnLeftUp(self, event): 111 | pass 112 | 113 | def OnRightUp(self, event): 114 | pass 115 | 116 | def OnMiddleUp(self, event): 117 | pass 118 | 119 | def OnMotion(self, event): 120 | pass 121 | 122 | def OnKeyDown(self, event): 123 | pass 124 | 125 | 126 | class wxViewer3d(wxBaseViewer): 127 | def __init__(self, *kargs): 128 | wxBaseViewer.__init__(self, *kargs) 129 | 130 | self._drawbox = False 131 | self._zoom_area = False 132 | self._select_area = False 133 | self._inited = False 134 | self._leftisdown = False 135 | self._middleisdown = False 136 | self._rightisdown = False 137 | self._selection = None 138 | self._scrollwheel = False 139 | self._key_map = {} 140 | self.dragStartPos = None 141 | 142 | def InitDriver(self): 143 | self._display = OCCViewer.Viewer3d(self.GetWinId()) 144 | self._display.Create() 145 | self._display.SetModeShaded() 146 | self._inited = True 147 | 148 | # dict mapping keys to functions 149 | self._SetupKeyMap() 150 | 151 | def _SetupKeyMap(self): 152 | def set_shade_mode(): 153 | self._display.DisableAntiAliasing() 154 | self._display.SetModeShaded() 155 | self._key_map = {ord('W'): self._display.SetModeWireFrame, 156 | ord('S'): set_shade_mode, 157 | ord('A'): self._display.EnableAntiAliasing, 158 | ord('B'): self._display.DisableAntiAliasing, 159 | ord('H'): self._display.SetModeHLR, 160 | ord('G'): self._display.SetSelectionModeVertex 161 | } 162 | 163 | def OnKeyDown(self, evt): 164 | code = evt.GetKeyCode() 165 | try: 166 | self._key_map[code]() 167 | except KeyError: 168 | print('unrecognized key %i' % evt.GetKeyCode()) 169 | 170 | def OnMaximize(self, event): 171 | if self._inited: 172 | self._display.Repaint() 173 | 174 | def OnMove(self, event): 175 | if self._inited: 176 | self._display.Repaint() 177 | 178 | def OnIdle(self, event): 179 | if self._drawbox: 180 | pass 181 | elif self._inited: 182 | self._display.Repaint() 183 | 184 | def Test(self): 185 | if self._inited: 186 | self._display.Test() 187 | 188 | def OnFocus(self, event): 189 | if self._inited: 190 | self._display.Repaint() 191 | 192 | def OnLostFocus(self, event): 193 | if self._inited: 194 | self._display.Repaint() 195 | 196 | def OnPaint(self, event): 197 | if self._inited: 198 | self._display.Repaint() 199 | 200 | def ZoomAll(self, evt): 201 | self._display.FitAll() 202 | 203 | def Repaint(self, evt): 204 | if self._inited: 205 | self._display.Repaint() 206 | 207 | def OnLeftDown(self, evt): 208 | self.SetFocus() 209 | self.dragStartPos = evt.GetPosition() 210 | self._display.StartRotation(self.dragStartPos.x, self.dragStartPos.y) 211 | 212 | def OnLeftUp(self, evt): 213 | pt = evt.GetPosition() 214 | if self._select_area: 215 | [Xmin, Ymin, dx, dy] = self._drawbox 216 | self._display.SelectArea(Xmin, Ymin, Xmin+dx, Ymin+dy) 217 | self._select_area = False 218 | else: 219 | self._display.Select(pt.x, pt.y) 220 | 221 | def OnRightUp(self, evt): 222 | if self._zoom_area: 223 | [Xmin, Ymin, dx, dy] = self._drawbox 224 | self._display.ZoomArea(Xmin, Ymin, Xmin+dx, Ymin+dy) 225 | self._zoom_area = False 226 | 227 | def OnMiddleUp(self, evt): 228 | pass 229 | 230 | def OnRightDown(self, evt): 231 | self.dragStartPos = evt.GetPosition() 232 | self._display.StartRotation(self.dragStartPos.x, self.dragStartPos.y) 233 | 234 | def OnMiddleDown(self, evt): 235 | self.dragStartPos = evt.GetPosition() 236 | self._display.StartRotation(self.dragStartPos.x, self.dragStartPos.y) 237 | 238 | def OnWheelScroll(self, evt): 239 | # Zooming by wheel 240 | if evt.GetWheelRotation() > 0: 241 | zoom_factor = 2. 242 | else: 243 | zoom_factor = 0.5 244 | self._display.Repaint() 245 | self._display.ZoomFactor(zoom_factor) 246 | 247 | def DrawBox(self, event): 248 | tolerance = 2 249 | pt = event.GetPosition() 250 | dx = pt.x - self.dragStartPos.x 251 | dy = pt.y - self.dragStartPos.y 252 | if abs(dx) <= tolerance and abs(dy) <= tolerance: 253 | return 254 | dc = wx.ClientDC(self) 255 | dc.BeginDrawing() 256 | dc.SetPen(wx.Pen(wx.WHITE, 1, wx.DOT)) 257 | dc.SetBrush(wx.TRANSPARENT_BRUSH) 258 | dc.SetLogicalFunction(wx.XOR) 259 | if self._drawbox: 260 | r = wx.Rect(*self._drawbox) 261 | dc.DrawRectangleRect(r) 262 | r = wx.Rect(self.dragStartPos.x, self.dragStartPos.y, dx, dy) 263 | dc.DrawRectangleRect(r) 264 | dc.EndDrawing() 265 | self._drawbox = [self.dragStartPos.x, self.dragStartPos.y, dx, dy] 266 | 267 | def OnMotion(self, evt): 268 | pt = evt.GetPosition() 269 | 270 | # ROTATE 271 | if evt.LeftIsDown() and not evt.ShiftDown(): 272 | self._display.Rotation(pt.x, pt.y) 273 | self._drawbox = False 274 | # DYNAMIC ZOOM 275 | elif evt.RightIsDown() and not evt.ShiftDown(): 276 | self._display.Repaint() 277 | self._display.DynamicZoom(abs(self.dragStartPos.x), abs(self.dragStartPos.y), abs(pt.x), abs(pt.y)) 278 | self.dragStartPos.x = pt.x 279 | self.dragStartPos.y = pt.y 280 | self._drawbox = False 281 | # PAN 282 | elif evt.MiddleIsDown(): 283 | dx = pt.x - self.dragStartPos.x 284 | dy = pt.y - self.dragStartPos.y 285 | self.dragStartPos.x = pt.x 286 | self.dragStartPos.y = pt.y 287 | self._display.Pan(dx, -dy) 288 | self._drawbox = False 289 | # DRAW BOX 290 | elif evt.RightIsDown() and evt.ShiftDown(): # ZOOM WINDOW 291 | self._zoom_area = True 292 | self.DrawBox(evt) 293 | elif evt.LeftIsDown() and evt.ShiftDown(): # SELECT AREA 294 | self._select_area = True 295 | self.DrawBox(evt) 296 | else: 297 | self._drawbox = False 298 | self._display.MoveTo(pt.x, pt.y) 299 | 300 | 301 | def TestWxDisplay(): 302 | class AppFrame(wx.Frame): 303 | def __init__(self, parent): 304 | wx.Frame.__init__(self, parent, -1, "wxDisplay3d sample", 305 | style=wx.DEFAULT_FRAME_STYLE, size=(640, 480)) 306 | self.canva = wxViewer3d(self) 307 | 308 | def runTests(self): 309 | self.canva._display.Test() 310 | 311 | app = wx.App(False) 312 | wx.InitAllImageHandlers() 313 | frame = AppFrame(None) 314 | frame.Show(True) 315 | wx.SafeYield() 316 | frame.canva.InitDriver() 317 | frame.runTests() 318 | app.SetTopWindow(frame) 319 | app.MainLoop() 320 | 321 | if __name__ == "__main__": 322 | TestWxDisplay() 323 | -------------------------------------------------------------------------------- /rpnCalculator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2020 Doug Blanding (dblanding@gmail.com) 4 | # 5 | # This file is part of kodacad. 6 | # The latest version of this file can be found at: 7 | # //https://github.com/dblanding/kodacad 8 | # 9 | # kodacad is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # kodacad is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # if not, write to the Free Software Foundation, Inc. 21 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 22 | # 23 | 24 | 25 | import math 26 | import sys 27 | 28 | from PyQt5.QtCore import Qt 29 | from PyQt5.QtWidgets import (QApplication, QDialog, QGridLayout, QLayout, 30 | QLineEdit, QSizePolicy, QToolButton) 31 | 32 | 33 | def nyi(): 34 | print("Not yet implemented") 35 | 36 | 37 | class Button(QToolButton): 38 | """Convenience class for buttons""" 39 | 40 | def __init__(self, text, parent=None): 41 | super(Button, self).__init__(parent) 42 | 43 | self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) 44 | self.setText(text) 45 | 46 | def sizeHint(self): 47 | size = super(Button, self).sizeHint() 48 | size.setHeight(size.height()) 49 | size.setWidth(max(size.width(), size.height())) 50 | return size 51 | 52 | 53 | class Calculator(QDialog): 54 | """RPN calculator styled after the one in CoCreate SolidDesigner CAD.""" 55 | 56 | mem = "" 57 | keip = False # Flag set when keyboard entry is in progress 58 | needrup = False # Flag signaling need to rotate up with next keyboard entry 59 | 60 | NumDigitButtons = 10 61 | 62 | def __init__(self, parent=None): 63 | super(Calculator, self).__init__(parent) 64 | self.caller = parent 65 | self.setWindowTitle("RPN Calculator") 66 | 67 | self.x = 0 68 | self.y = 0 69 | self.z = 0 70 | self.t = 0 71 | 72 | self.xdisplay = self.display() 73 | self.ydisplay = self.display() 74 | self.zdisplay = self.display() 75 | self.tdisplay = self.display() 76 | 77 | myblue1 = "steelblue" 78 | myblue2 = "darkslateblue" # hsv(240,200,160) 79 | mygray = "rgb(120,120,120)" # dimgray 80 | mygreen = "green" 81 | myred = "hsv(0,255,180)" 82 | mygold = "goldenrod" 83 | 84 | self.mainLayout = QGridLayout() 85 | self.mainLayout.setSpacing(0) 86 | self.mainLayout.setSizeConstraint(QLayout.SetFixedSize) 87 | 88 | # Grid is 36 columns across 89 | self.butn("T", 0, 0, lambda state, r="t": self.pr(r), colspan=4) 90 | self.butn("Z", 1, 0, lambda state, r="z": self.pr(r), colspan=4) 91 | self.butn("Y", 2, 0, lambda state, r="y": self.pr(r), colspan=4) 92 | self.butn("X", 3, 0, lambda state, r="x": self.pr(r), colspan=4) 93 | self.mainLayout.addWidget(self.tdisplay, 0, 4, 1, 26) 94 | self.mainLayout.addWidget(self.zdisplay, 1, 4, 1, 26) 95 | self.mainLayout.addWidget(self.ydisplay, 2, 4, 1, 26) 96 | self.mainLayout.addWidget(self.xdisplay, 3, 4, 1, 26) 97 | self.butn("pi", 0, 30, self.pi, colspan=6) 98 | self.butn("1/x", 1, 30, lambda state, 99 | op="1/x": self.func(op), colspan=6) 100 | self.butn("2x", 2, 30, lambda state, 101 | op="x*2": self.func(op), colspan=6) 102 | self.butn("x/2", 3, 30, lambda state, 103 | op="x/2": self.func(op), colspan=6) 104 | 105 | self.butn("mm -> in", 4, 0, self.mm2in, colspan=12) 106 | self.butn("in -> mm", 4, 12, self.in2mm, colspan=12) 107 | self.butn("STO", 4, 24, self.storex, clr=mygreen, colspan=6) 108 | self.butn("RCL", 4, 30, self.recallx, clr=mygreen, colspan=6) 109 | 110 | self.butn("7", 5, 0, lambda state, c="7": self.keyin(c), clr=myblue1) 111 | self.butn("8", 5, 6, lambda state, c="8": self.keyin(c), clr=myblue1) 112 | self.butn("9", 5, 12, lambda state, c="9": self.keyin(c), clr=myblue1) 113 | self.butn("+", 5, 18, lambda state, 114 | op="+": self.calculate(op), clr=myblue2) 115 | self.butn("R up", 5, 24, self.rotateup, clr=mygreen, colspan=6) 116 | self.butn("R dn", 5, 30, self.rotatedn, clr=mygreen, colspan=6) 117 | 118 | self.butn("4", 6, 0, lambda state, c="4": self.keyin(c), clr=myblue1) 119 | self.butn("5", 6, 6, lambda state, c="5": self.keyin(c), clr=myblue1) 120 | self.butn("6", 6, 12, lambda state, c="6": self.keyin(c), clr=myblue1) 121 | self.butn("-", 6, 18, lambda state, 122 | op="-": self.calculate(op), clr=myblue2) 123 | self.butn("<-", 6, 24, self.trimx, clr=myred, colspan=4) 124 | self.butn("X<>Y", 6, 28, self.swapxy, clr=mygreen, colspan=8) 125 | 126 | self.butn("1", 7, 0, lambda state, c="1": self.keyin(c), clr=myblue1) 127 | self.butn("2", 7, 6, lambda state, c="2": self.keyin(c), clr=myblue1) 128 | self.butn("3", 7, 12, lambda state, c="3": self.keyin(c), clr=myblue1) 129 | self.butn("*", 7, 18, lambda state, 130 | op="*": self.calculate(op), clr=myblue2) 131 | self.butn("CL X", 7, 24, self.clearx, clr=myred) 132 | self.butn("CLR", 7, 30, self.clearall, clr=myred) 133 | 134 | self.butn("0", 8, 0, lambda state, c="0": self.keyin(c), clr=myblue1) 135 | self.butn(".", 8, 6, lambda state, c=".": self.keyin(c), clr=myblue2) 136 | self.butn("+/-", 8, 12, lambda state, 137 | op="+/-": self.calculate(op), clr=myblue2) 138 | self.butn("/", 8, 18, lambda state, 139 | c="/": self.calculate(c), clr=myblue2) 140 | self.butn("ENTER", 8, 24, self.enter, clr=mygold, colspan=12) 141 | 142 | self.butn( 143 | "Sin", 144 | 9, 145 | 0, 146 | lambda state, op="math.sin(x)": self.func(op, in_cnvrt=1), 147 | clr=mygold, 148 | colspan=8, 149 | ) 150 | self.butn( 151 | "Cos", 152 | 9, 153 | 8, 154 | lambda state, op="math.cos(x)": self.func(op, in_cnvrt=1), 155 | clr=mygold, 156 | colspan=8, 157 | ) 158 | self.butn( 159 | "Tan", 160 | 9, 161 | 16, 162 | lambda state, op="math.tan(x)": self.func(op, in_cnvrt=1), 163 | clr=mygold, 164 | colspan=8, 165 | ) 166 | self.butn("x^2", 9, 24, lambda state, 167 | op="x*x": self.func(op), clr=mygold) 168 | self.butn("10^x", 9, 30, lambda state, 169 | op="10**x": self.func(op), clr=mygold) 170 | self.butn( 171 | "ASin", 172 | 10, 173 | 0, 174 | lambda state, op="math.asin(x)": self.func(op, out_cnvrt=1), 175 | clr=mygold, 176 | colspan=8, 177 | ) 178 | self.butn( 179 | "ACos", 180 | 10, 181 | 8, 182 | lambda state, op="math.acos(x)": self.func(op, out_cnvrt=1), 183 | clr=mygold, 184 | colspan=8, 185 | ) 186 | self.butn( 187 | "ATan", 188 | 10, 189 | 16, 190 | lambda state, op="math.atan(x)": self.func(op, out_cnvrt=1), 191 | clr=mygold, 192 | colspan=8, 193 | ) 194 | self.butn( 195 | "Sqrt x", 10, 24, lambda state, op="math.sqrt(x)": self.func(op), clr=mygold 196 | ) 197 | self.butn("y^x", 10, 30, lambda state, 198 | op="y**x": self.func(op), clr=mygold) 199 | 200 | self.butn("Dist", 11, 0, self.caller.distPtPt, clr=mygray, colspan=8) 201 | self.butn("Len", 11, 8, self.caller.edgeLen, clr=mygray, colspan=8) 202 | self.butn("Rad", 11, 16, self.noop, clr=mygray, colspan=8) 203 | self.butn("Ang", 11, 24, self.noop, clr=mygray) 204 | self.butn("", 11, 30, self.noop, clr=mygray) 205 | 206 | self.setLayout(self.mainLayout) 207 | 208 | def butn(self, text, row, col, com=None, clr="dimgray", rowspan=1, colspan=6): 209 | b = Button(text) 210 | b.clicked.connect(com) 211 | b.setStyleSheet("color: white; background-color: %s" % clr) 212 | self.mainLayout.addWidget(b, row, col, rowspan, colspan) 213 | 214 | def display(self): 215 | d = QLineEdit("0") 216 | d.setAlignment(Qt.AlignRight) 217 | d.setMaxLength(18) 218 | font = d.font() 219 | font.setPointSize(font.pointSize() + 2) 220 | d.setFont(font) 221 | return d 222 | 223 | def closeEvent(self, event): 224 | print("calculator closing") 225 | try: 226 | self.caller.calculator = None 227 | except: 228 | pass 229 | event.accept() 230 | 231 | def pr(self, register): 232 | """Send value to caller.""" 233 | value = eval("self." + register) 234 | if self.caller: 235 | self.caller.valueFromCalc(value) 236 | else: 237 | print(value) 238 | self.keip = False 239 | self.needrup = True 240 | 241 | def keyin(self, c): 242 | if self.keip: 243 | dispVal = self.xdisplay.text() + c 244 | self.xdisplay.setText(dispVal) 245 | self.x = float(dispVal) 246 | else: 247 | self.keip = True 248 | if self.needrup: 249 | self.rotateup(loop=0) 250 | self.xdisplay.setText("") 251 | if c == ".": 252 | c = "0." 253 | self.keyin(c) 254 | 255 | def pi(self): 256 | self.rotateup() 257 | self.x = math.pi 258 | self.updateDisplays() 259 | self.needrup = True 260 | 261 | def updateDisplays(self): 262 | self.xdisplay.setText(str(self.x)) 263 | self.ydisplay.setText(str(self.y)) 264 | self.zdisplay.setText(str(self.z)) 265 | self.tdisplay.setText(str(self.t)) 266 | 267 | def enter(self): 268 | self.t = self.z 269 | self.z = self.y 270 | self.y = self.x 271 | self.x = self.x 272 | self.updateDisplays() 273 | self.keip = False 274 | self.needrup = False 275 | 276 | def calculate(self, op): 277 | """Arithmetic calculations between x and y registers, then rotate down.""" 278 | try: 279 | if op == "+/-": 280 | self.x = self.x * -1 281 | self.xdisplay.setText(str(self.x)) 282 | else: 283 | if op == "+": 284 | res = self.y + self.x 285 | elif op == "-": 286 | res = self.y - self.x 287 | elif op == "*": 288 | res = self.y * self.x 289 | elif op == "/": 290 | res = self.y / self.x 291 | self.x = res 292 | self.y = self.z 293 | self.z = self.t 294 | self.updateDisplays() 295 | self.keip = False 296 | self.needrup = True 297 | except: 298 | self.xdisplay.setText("ERROR") 299 | 300 | def func(self, op, in_cnvrt=0, out_cnvrt=0): 301 | """Evaluate function op then put result in x-register, don't rotate stack. 302 | if in_cnvrt: convert input value from degrees to radians. 303 | if out_cnvrt: convert output value from radians to degrees.""" 304 | x = self.x 305 | # y = self.y 306 | if in_cnvrt: 307 | x = x * math.pi / 180 308 | result = eval(op) 309 | if out_cnvrt: 310 | result = result * 180 / math.pi 311 | self.x = result 312 | self.xdisplay.setText(str(self.x)) 313 | self.keip = False 314 | self.needrup = True 315 | 316 | def mm2in(self): 317 | if self.xdisplay.text(): 318 | self.x = self.x / 25.4 319 | self.xdisplay.setText(str(self.x)) 320 | self.keip = False 321 | self.needrup = True 322 | 323 | def in2mm(self): 324 | if self.xdisplay.text(): 325 | self.x = self.x * 25.4 326 | self.xdisplay.setText(str(self.x)) 327 | self.keip = False 328 | self.needrup = True 329 | 330 | def storex(self): 331 | self.mem = self.x 332 | self.keip = False 333 | self.needrup = True 334 | 335 | def recallx(self): 336 | self.rotateup() 337 | self.xdisplay.setText(str(self.mem)) 338 | self.keip = False 339 | self.needrup = True 340 | 341 | def rotateup(self, loop=1): 342 | x = self.t 343 | self.t = self.z 344 | self.z = self.y 345 | self.y = self.x 346 | if loop: 347 | self.x = x 348 | self.updateDisplays() 349 | 350 | def rotatedn(self): 351 | x = self.x 352 | self.x = self.y 353 | self.y = self.z 354 | self.z = self.t 355 | self.t = x 356 | self.updateDisplays() 357 | 358 | def trimx(self): 359 | trimmedStrVal = self.xdisplay.text()[:-1] 360 | try: 361 | self.xdisplay.setText(trimmedStrVal) 362 | self.x = float(trimmedStrVal) 363 | except ValueError: 364 | self.clearx() 365 | 366 | def swapxy(self): 367 | self.x, self.y = (self.y, self.x) 368 | self.updateDisplays() 369 | 370 | def clearx(self): 371 | self.x = 0 372 | self.xdisplay.setText("0") 373 | 374 | def clearall(self): 375 | self.x = self.y = self.z = self.t = 0 376 | self.updateDisplays() 377 | 378 | def putx(self, value): 379 | if self.needrup: 380 | self.rotateup(loop=0) 381 | self.x = value 382 | self.xdisplay.setText(str(value)) 383 | self.keip = False 384 | self.needrup = True 385 | 386 | def noop(self): 387 | pass 388 | 389 | 390 | if __name__ == "__main__": 391 | 392 | app = QApplication(sys.argv) 393 | calc = Calculator() 394 | sys.exit(calc.exec_()) 395 | -------------------------------------------------------------------------------- /save_files/as1-oc-214-at_top.xbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/save_files/as1-oc-214-at_top.xbf -------------------------------------------------------------------------------- /save_files/as1-oc-214-under_top.xbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/save_files/as1-oc-214-under_top.xbf -------------------------------------------------------------------------------- /save_files/rt.xbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dblanding/kodacad/8a126d418483298038ef1ec38790f907db1d1d81/save_files/rt.xbf -------------------------------------------------------------------------------- /stepanalyzer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2020 Doug Blanding (dblanding@gmail.com) 4 | # 5 | # The latest version of this file can be found at: 6 | # //https://github.com/dblanding/step-analyzer 7 | # 8 | # stepanalyzer is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # stepanalyzer is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # if not, write to the Free Software Foundation, Inc. 20 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 21 | # 22 | """A tool which examines the hierarchical structure of a TDocStd_Document 23 | containing CAD data in OCAF format, either loaded directly or read from a 24 | STEP file. The structure is presented as an indented text outline.""" 25 | 26 | from OCC.Core.IFSelect import IFSelect_RetDone 27 | from OCC.Core.STEPCAFControl import STEPCAFControl_Reader 28 | from OCC.Core.TDF import TDF_Label, TDF_LabelSequence 29 | from OCC.Core.TCollection import TCollection_ExtendedString 30 | from OCC.Core.TDocStd import TDocStd_Document 31 | from OCC.Core.XCAFApp import XCAFApp_Application_GetApplication 32 | from OCC.Core.XCAFDoc import XCAFDoc_DocumentTool_ShapeTool 33 | 34 | 35 | class StepAnalyzer(): 36 | """A class that analyzes the structure of an OCAF document.""" 37 | 38 | def __init__(self, document=None, filename=None): 39 | """Supply one or the other: document or STEP filename.""" 40 | 41 | self.uid = 1 42 | self.indent = 0 43 | self.output = "" 44 | self.fname = filename 45 | if filename: 46 | self.doc = self.read_file(filename) 47 | elif document: 48 | self.doc = document 49 | self.shape_tool = XCAFDoc_DocumentTool_ShapeTool(self.doc.Main()) 50 | else: 51 | print("Supply one or the other: document or STEP filename.") 52 | 53 | def read_file(self, fname): 54 | """Read STEP file and return .""" 55 | 56 | # Create the application, empty document and shape_tool 57 | doc = TDocStd_Document(TCollection_ExtendedString("STEP")) 58 | app = XCAFApp_Application_GetApplication() 59 | app.NewDocument(TCollection_ExtendedString("MDTV-XCAF"), doc) 60 | self.shape_tool = XCAFDoc_DocumentTool_ShapeTool(doc.Main()) 61 | self.shape_tool.SetAutoNaming(True) 62 | 63 | # Read file and return populated doc 64 | step_reader = STEPCAFControl_Reader() 65 | step_reader.SetColorMode(True) 66 | step_reader.SetLayerMode(True) 67 | step_reader.SetNameMode(True) 68 | step_reader.SetMatMode(True) 69 | status = step_reader.ReadFile(fname) 70 | if status == IFSelect_RetDone: 71 | step_reader.Transfer(doc) 72 | return doc 73 | 74 | def dump(self): 75 | """Return assembly structure in indented outline form. 76 | 77 | Format of lines: 78 | Component Name [entry] => Referred Label Name [entry] 79 | Components are shown indented w/r/t line above.""" 80 | 81 | if self.fname: 82 | self.output += f"Assembly structure of file: {self.fname}\n\n" 83 | else: 84 | self.output += "Assembly structure of doc:\n\n" 85 | self.indent = 0 86 | 87 | # Find root label of step doc 88 | labels = TDF_LabelSequence() 89 | self.shape_tool.GetShapes(labels) 90 | nbr = labels.Length() 91 | try: 92 | rootlabel = labels.Value(1) # First label at root 93 | except RuntimeError as e: 94 | return e 95 | 96 | # Get information from root label 97 | name = rootlabel.GetLabelName() 98 | entry = rootlabel.EntryDumpToString() 99 | is_assy = self.shape_tool.IsAssembly(rootlabel) 100 | if is_assy: 101 | # If 1st label at root holds an assembly, it is the Top Assy. 102 | # Through this label, the entire assembly is accessible. 103 | # There is no need to explicitly examine other labels at root. 104 | self.output += f"{self.uid}\t[{entry}] {name}\t" 105 | self.uid += 1 106 | self.indent += 2 107 | top_comps = TDF_LabelSequence() # Components of Top Assy 108 | subchilds = False 109 | is_assy = self.shape_tool.GetComponents(rootlabel, top_comps, 110 | subchilds) 111 | self.output += f"Number of labels at root = {nbr}\n" 112 | if top_comps.Length(): 113 | self.find_components(top_comps) 114 | return self.output 115 | 116 | def find_components(self, comps): 117 | """Discover components from comps (LabelSequence) of an assembly. 118 | 119 | Components of an assembly are, by definition, references which refer 120 | to either a shape or another assembly. Components are essentially 121 | 'instances' of the referred shape or assembly, and carry a location 122 | vector specifing the location of the referred shape or assembly. 123 | """ 124 | for j in range(comps.Length()): 125 | c_label = comps.Value(j+1) # component label 126 | c_name = c_label.GetLabelName() 127 | c_entry = c_label.EntryDumpToString() 128 | ref_label = TDF_Label() # label of referred shape (or assembly) 129 | is_ref = self.shape_tool.GetReferredShape(c_label, ref_label) 130 | if is_ref: # just in case all components are not references 131 | ref_entry = ref_label.EntryDumpToString() 132 | ref_name = ref_label.GetLabelName() 133 | indent = "\t" * self.indent 134 | self.output += f"{self.uid}{indent}[{c_entry}] {c_name}" 135 | self.output += f" => [{ref_entry}] {ref_name}\n" 136 | self.uid += 1 137 | if self.shape_tool.IsAssembly(ref_label): 138 | self.indent += 1 139 | ref_comps = TDF_LabelSequence() # Components of Assy 140 | subchilds = False 141 | _ = self.shape_tool.GetComponents(ref_label, ref_comps, 142 | subchilds) 143 | if ref_comps.Length(): 144 | self.find_components(ref_comps) 145 | 146 | self.indent -= 1 147 | 148 | 149 | if __name__ == "__main__": 150 | SA = StepAnalyzer(filename="step/as1-oc-214.stp") 151 | print(SA.dump()) 152 | 153 | SA2 = StepAnalyzer(filename="step/as1_pe_203.stp") 154 | print(SA2.dump()) 155 | -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | # 4/20/2020 Incremental draw/hide of shapes and use hide_list instead of draw_list. 2 | APP_VERSION = "0.2.2" 3 | # APP_VERSION = "0.2.1" # 4/10/2020 Able to modify a shape and have all shared instances display correctly. 4 | # APP_VERSION = "0.2.0" # 4/8/2020 Merged dev branch which uses 'entry' based uid's (rather than integers). 5 | # APP_VERSION = "0.1.2" # 3/19/2020 OCAF document format adopted for CAD data model 6 | # APP_VERSION = "0.1.1" # 2/24/2020 'Round trip' Load/Save STEP works crudely (save all parts in _partDict) 7 | # APP_VERSION = "0.1.0" # 2/17/2020 Initial release 8 | --------------------------------------------------------------------------------