├── .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 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kodacad.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
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 | 
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 | 
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 | * 
130 | * Then extrude the circle 3 mm high to create the new component.
131 | * 
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.
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 |
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.
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".
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.
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 |
--------------------------------------------------------------------------------