stepfg 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Eremey Valetov and Martin Berz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
stepfg-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: stepfg
3
+ Version: 2.0.0
4
+ Summary: Convert 2D polygon cross-sections into 3D STEP files via extrusion
5
+ Author: Martin Berz
6
+ Author-email: Eremey Valetov <evv@msu.edu>
7
+ License-Expression: MIT
8
+ Project-URL: Repository, https://github.com/evvaletov/stepfg
9
+ Keywords: STEP,CAD,polygon,extrusion,3D,ISO-10303
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE.md
24
+ Dynamic: license-file
25
+
26
+ # stepfg: STEP File Generator
27
+
28
+ **Python 3.8+** | **Zero dependencies** | [MIT License](LICENSE.md)
29
+
30
+ Authors: E. Valetov and M. Berz
31
+ Organization: Michigan State University
32
+ Creation date: 03-Feb-2017
33
+
34
+ ## Introduction
35
+
36
+ stepfg converts lists of 2D polygons (specified by vertices in the x-y plane)
37
+ into 3D STEP files (ISO 10303-242) by extruding the polygon interiors along
38
+ the z-axis. Pure Python, no external dependencies.
39
+
40
+ ## Installation
41
+
42
+ ```
43
+ pip install stepfg
44
+ ```
45
+
46
+ Or from source:
47
+
48
+ ```
49
+ git clone https://github.com/evvaletov/stepfg.git
50
+ cd stepfg
51
+ pip install -e .
52
+ ```
53
+
54
+ ## Library usage
55
+
56
+ ```python
57
+ from stepfg import StepBuilder, generate_step
58
+
59
+ # Quick one-liner
60
+ content = generate_step(
61
+ polygons=[[[0, 0], [1, 0], [1, 1], [0, 1]]],
62
+ z_range=[0, 10],
63
+ scale=1,
64
+ )
65
+
66
+ # Or use StepBuilder for more control
67
+ builder = StepBuilder('output.stp')
68
+ builder.generate_assembly(
69
+ list_vert_list=[[[0, 0], [1, 0], [1, 1], [0, 1]]],
70
+ geom_depth_list=[0, 10],
71
+ p_coeff=1,
72
+ )
73
+ builder.to_file('output.stp')
74
+ ```
75
+
76
+ ## Command-line usage
77
+
78
+ ```
79
+ stepfg [input_file [output_file]]
80
+ python -m stepfg [input_file [output_file]]
81
+ ```
82
+
83
+ Run with the included sample geometry (a Muon g-2 quadrupole):
84
+
85
+ ```
86
+ stepfg part_geometry.txt quadrupole.stp
87
+ ```
88
+
89
+ ## Input file format
90
+
91
+ The input file is a Python literal containing three elements:
92
+
93
+ ```python
94
+ [polygons, z_range, scale]
95
+ ```
96
+
97
+ - **polygons**: `[[vertex, ...], ...]` — each vertex is `[x, y]` or `[x, y, 0]`
98
+ - **z_range**: `[z1, z2]` — extrusion interval
99
+ - **scale**: proportionality coefficient (output is in mm; use 10 for cm input)
100
+
101
+ A sample input file `part_geometry.txt` containing a Muon g-2 Collaboration
102
+ quadrupole cross-section is included.
103
+
104
+ ## Copyright Notice
105
+
106
+ © 2017 Eremey Valetov and Martin Berz
stepfg-2.0.0/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # stepfg: STEP File Generator
2
+
3
+ **Python 3.8+** | **Zero dependencies** | [MIT License](LICENSE.md)
4
+
5
+ Authors: E. Valetov and M. Berz
6
+ Organization: Michigan State University
7
+ Creation date: 03-Feb-2017
8
+
9
+ ## Introduction
10
+
11
+ stepfg converts lists of 2D polygons (specified by vertices in the x-y plane)
12
+ into 3D STEP files (ISO 10303-242) by extruding the polygon interiors along
13
+ the z-axis. Pure Python, no external dependencies.
14
+
15
+ ## Installation
16
+
17
+ ```
18
+ pip install stepfg
19
+ ```
20
+
21
+ Or from source:
22
+
23
+ ```
24
+ git clone https://github.com/evvaletov/stepfg.git
25
+ cd stepfg
26
+ pip install -e .
27
+ ```
28
+
29
+ ## Library usage
30
+
31
+ ```python
32
+ from stepfg import StepBuilder, generate_step
33
+
34
+ # Quick one-liner
35
+ content = generate_step(
36
+ polygons=[[[0, 0], [1, 0], [1, 1], [0, 1]]],
37
+ z_range=[0, 10],
38
+ scale=1,
39
+ )
40
+
41
+ # Or use StepBuilder for more control
42
+ builder = StepBuilder('output.stp')
43
+ builder.generate_assembly(
44
+ list_vert_list=[[[0, 0], [1, 0], [1, 1], [0, 1]]],
45
+ geom_depth_list=[0, 10],
46
+ p_coeff=1,
47
+ )
48
+ builder.to_file('output.stp')
49
+ ```
50
+
51
+ ## Command-line usage
52
+
53
+ ```
54
+ stepfg [input_file [output_file]]
55
+ python -m stepfg [input_file [output_file]]
56
+ ```
57
+
58
+ Run with the included sample geometry (a Muon g-2 quadrupole):
59
+
60
+ ```
61
+ stepfg part_geometry.txt quadrupole.stp
62
+ ```
63
+
64
+ ## Input file format
65
+
66
+ The input file is a Python literal containing three elements:
67
+
68
+ ```python
69
+ [polygons, z_range, scale]
70
+ ```
71
+
72
+ - **polygons**: `[[vertex, ...], ...]` — each vertex is `[x, y]` or `[x, y, 0]`
73
+ - **z_range**: `[z1, z2]` — extrusion interval
74
+ - **scale**: proportionality coefficient (output is in mm; use 10 for cm input)
75
+
76
+ A sample input file `part_geometry.txt` containing a Muon g-2 Collaboration
77
+ quadrupole cross-section is included.
78
+
79
+ ## Copyright Notice
80
+
81
+ © 2017 Eremey Valetov and Martin Berz
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "stepfg"
7
+ version = "2.0.0"
8
+ description = "Convert 2D polygon cross-sections into 3D STEP files via extrusion"
9
+ authors = [
10
+ { name = "Eremey Valetov", email = "evv@msu.edu" },
11
+ { name = "Martin Berz" },
12
+ ]
13
+ license = "MIT"
14
+ requires-python = ">=3.8"
15
+ readme = "README.md"
16
+ keywords = ["STEP", "CAD", "polygon", "extrusion", "3D", "ISO-10303"]
17
+ classifiers = [
18
+ "Development Status :: 5 - Production/Stable",
19
+ "Intended Audience :: Science/Research",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.8",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: Scientific/Engineering",
29
+ ]
30
+
31
+ [project.scripts]
32
+ stepfg = "stepfg.__main__:main"
33
+
34
+ [project.urls]
35
+ Repository = "https://github.com/evvaletov/stepfg"
36
+
37
+ [tool.setuptools.packages.find]
38
+ include = ["stepfg*"]
stepfg-2.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,23 @@
1
+ """stepfg — convert 2D polygon cross-sections to 3D STEP files via extrusion."""
2
+
3
+ __version__ = "2.0.0"
4
+
5
+ from ._builder import StepBuilder
6
+ from ._geometry import normalize, cross_product, convert_3d, convert_to_clockwise
7
+
8
+
9
+ def generate_step(polygons, z_range, scale=1, filename='part_out.stp'):
10
+ """Generate STEP content from polygon cross-sections.
11
+
12
+ Args:
13
+ polygons: List of polygons, each a list of [x,y] or [x,y,z] vertices.
14
+ z_range: [z1, z2] extrusion interval.
15
+ scale: Proportionality coefficient (default 1). Use 10 for cm→mm.
16
+ filename: Filename embedded in the STEP header.
17
+
18
+ Returns:
19
+ STEP file content as a string.
20
+ """
21
+ builder = StepBuilder(filename)
22
+ builder.generate_assembly(polygons, z_range, scale)
23
+ return builder.to_string()
@@ -0,0 +1,66 @@
1
+ """CLI entry point: python -m stepfg"""
2
+
3
+ import argparse
4
+ import ast
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from . import __version__, generate_step
9
+
10
+
11
+ BANNER = """\
12
+ ----------------------------------------------------
13
+ STEP File Generator
14
+ E. Valetov and M. Berz
15
+ Michigan State University
16
+ Created 03-Feb-2017
17
+ Email: valetove@msu.edu
18
+ ----------------------------------------------------"""
19
+
20
+
21
+ def main(argv=None):
22
+ parser = argparse.ArgumentParser(
23
+ prog='stepfg',
24
+ description='Convert 2D polygon geometry into 3D STEP files via extrusion.',
25
+ )
26
+ parser.add_argument('input', nargs='?', default='part_geometry.txt',
27
+ help='Input file containing 2D geometry data (default: part_geometry.txt)')
28
+ parser.add_argument('output', nargs='?', default='part_out.stp',
29
+ help='Output STEP file (default: part_out.stp)')
30
+ parser.add_argument('-V', '--version', action='version', version=f'stepfg {__version__}')
31
+
32
+ args = parser.parse_args(argv)
33
+
34
+ print(BANNER)
35
+
36
+ input_path = Path(args.input)
37
+ if not input_path.is_file():
38
+ print(f"Error: input file '{args.input}' not found.", file=sys.stderr)
39
+ sys.exit(1)
40
+
41
+ print(f"Reading 2D geometry file {args.input}... ", end="")
42
+ data = ast.literal_eval(input_path.read_text())
43
+ if not isinstance(data, list) or len(data) != 3:
44
+ print("[FAILED]")
45
+ print("Error: input must be a list of [polygons, z_range, scale].", file=sys.stderr)
46
+ sys.exit(1)
47
+ print("[DONE]")
48
+
49
+ polygons, z_range, scale = data
50
+
51
+ print("Generating assembly... ", end="")
52
+ try:
53
+ content = generate_step(polygons, z_range, scale, args.output)
54
+ except ValueError as e:
55
+ print("[FAILED]")
56
+ print(f"Error: {e}", file=sys.stderr)
57
+ sys.exit(1)
58
+ print("[DONE]")
59
+
60
+ print(f"Writing STEP file {args.output}... ", end="")
61
+ Path(args.output).write_text(content)
62
+ print("[DONE]")
63
+
64
+
65
+ if __name__ == '__main__':
66
+ main()
@@ -0,0 +1,261 @@
1
+ import operator
2
+ import re
3
+ from numbers import Number
4
+
5
+ from ._geometry import normalize, cross_product, rotate, convert_3d, convert_to_clockwise
6
+ from ._template import step_header
7
+
8
+
9
+ def _line_index(line):
10
+ m = re.search(r'#(.+?)=', line)
11
+ return 0 if m is None else int(m.group(1))
12
+
13
+
14
+ def _to_coord(clist):
15
+ if len(clist) != 3:
16
+ raise ValueError("to_coord: coordinates must be 3D")
17
+ return str(clist[0]) + ',' + str(clist[1]) + ',' + str(clist[2])
18
+
19
+
20
+ def _to_step_list(slist):
21
+ if not isinstance(slist, list):
22
+ return '#' + str(slist)
23
+ return ','.join('#' + str(s) for s in slist)
24
+
25
+
26
+ def _fort_bool(bool_in):
27
+ return '.T.' if (bool_in is True) or (bool_in == '.T.') else '.F.'
28
+
29
+
30
+ class StepBuilder:
31
+ def __init__(self, filename='part_out.stp'):
32
+ self._filename = filename
33
+ self._file_array, self._index1, highest = step_header(filename)
34
+ self._current_index = highest + 1
35
+ self._work_array = []
36
+ self._part_body_index = 1
37
+
38
+ def _item_exists(self, string_in):
39
+ if not self._work_array:
40
+ return False
41
+ return any(item.endswith(string_in) for item in self._work_array)
42
+
43
+ def _existing_item_ln(self, string_in):
44
+ if not self._work_array or not self._item_exists(string_in):
45
+ return False
46
+ match = next(item for item in self._work_array if item.endswith(string_in))
47
+ return _line_index(match)
48
+
49
+ def new_item(self, string_in):
50
+ if self._item_exists(string_in):
51
+ return self._existing_item_ln(string_in)
52
+ self._work_array.append('#' + str(self._current_index) + '=' + string_in)
53
+ idx = self._current_index
54
+ self._current_index += 1
55
+ return idx
56
+
57
+ def point(self, coord_in):
58
+ return self.new_item(
59
+ "CARTESIAN_POINT('',(" + _to_coord(coord_in) + ")) ;\n")
60
+
61
+ def line(self, origin, direction):
62
+ coord_ln = self.new_item(
63
+ "CARTESIAN_POINT('Origin Line',(" + _to_coord(origin) + ")) ;\n")
64
+ dir_ln = self.new_item(
65
+ "DIRECTION('Vector Direction',("
66
+ + _to_coord(normalize(direction)) + ")) ;\n")
67
+ vec_ln = self.new_item(
68
+ "VECTOR('Line Direction',#" + str(dir_ln) + ",1.) ;\n")
69
+ return self.new_item(
70
+ "LINE('Line',#" + str(coord_ln) + ",#" + str(vec_ln) + ") ;\n")
71
+
72
+ def vertex(self, coord_in):
73
+ coord_ln = self.new_item(
74
+ "CARTESIAN_POINT('Vertex',(" + _to_coord(coord_in) + ")) ;\n")
75
+ return self.new_item("VERTEX_POINT('',#" + str(coord_ln) + ") ;\n")
76
+
77
+ def edge_curve(self, vertex1_ln, vertex2_ln, line_coord_ln, same_sense=True):
78
+ return self.new_item(
79
+ "EDGE_CURVE('',#" + str(vertex1_ln) + ",#" + str(vertex2_ln)
80
+ + ",#" + str(line_coord_ln) + "," + _fort_bool(same_sense) + ") ;\n")
81
+
82
+ def _edge_curve_0(self, v1, v2, same_sense=True):
83
+ return self.edge_curve(
84
+ self.vertex(v1), self.vertex(v2),
85
+ self.line(
86
+ [x / 2 for x in list(map(operator.add, v1, v2))],
87
+ list(map(operator.sub, v2, v1))),
88
+ _fort_bool(same_sense))
89
+
90
+ def oriented_edge(self, edge_curve_ln, same_sense=True):
91
+ return self.new_item(
92
+ "ORIENTED_EDGE('',*,*,#" + str(edge_curve_ln) + ","
93
+ + _fort_bool(same_sense) + ") ;\n")
94
+
95
+ def edge_loop(self, lines):
96
+ return self.new_item(
97
+ "EDGE_LOOP('',(" + _to_step_list(lines) + ")) ;\n")
98
+
99
+ def _edge_loop_0(self, vertices):
100
+ return self.edge_loop([
101
+ self.oriented_edge(self._edge_curve_0(x1, x2))
102
+ for x1, x2 in zip(vertices, rotate(vertices, -1))
103
+ ])
104
+
105
+ def face_outer_bound(self, edge_loop_ln, same_sense=True):
106
+ return self.new_item(
107
+ "FACE_OUTER_BOUND('',#" + str(edge_loop_ln) + ","
108
+ + _fort_bool(same_sense) + ") ;\n")
109
+
110
+ def _edge_loop_1(self, vertices, same_sense=True):
111
+ return self.face_outer_bound(self._edge_loop_0(vertices), same_sense)
112
+
113
+ def _axis2_placement_3d(self, origin_coord, direction1, direction2):
114
+ origin_ln = self.new_item(
115
+ "CARTESIAN_POINT('Axis2P3D Location',("
116
+ + _to_coord(origin_coord) + ")) ;\n")
117
+ d1_ln = self.new_item(
118
+ "DIRECTION('Axis2P3D ZDirection',("
119
+ + _to_coord(direction1) + ")) ;\n")
120
+ d2_ln = self.new_item(
121
+ "DIRECTION('Axis2P3D XDirection',("
122
+ + _to_coord(direction2) + ")) ;\n")
123
+ return self.new_item(
124
+ "AXIS2_PLACEMENT_3D('Plane Axis2P3D',#" + str(origin_ln)
125
+ + ",#" + str(d1_ln) + ",#" + str(d2_ln) + ") ;\n")
126
+
127
+ def plane(self, axis2_placement_3d_ln):
128
+ return self.new_item(
129
+ "PLANE('',#" + str(axis2_placement_3d_ln) + ") ;\n")
130
+
131
+ def advanced_face(self, face_outer_bound_ln, plane_ln, same_sense_plane=True):
132
+ return self.new_item(
133
+ "ADVANCED_FACE('PartBody',(" + _to_step_list(face_outer_bound_ln)
134
+ + "),#" + str(plane_ln) + "," + _fort_bool(same_sense_plane) + ") ;\n")
135
+
136
+ def _advanced_face_0(self, vertices, zaxis, same_sense_1=True, same_sense_2=True):
137
+ cp = cross_product(
138
+ list(map(operator.sub, vertices[2], vertices[1])),
139
+ list(map(operator.sub, vertices[2], vertices[0])))
140
+ if list(map(operator.add, normalize(zaxis), normalize(cp))) == [0, 0, 0]:
141
+ return self.advanced_face(
142
+ self._edge_loop_1(vertices, same_sense_1),
143
+ self.plane(self._axis2_placement_3d(
144
+ vertices[0], normalize(zaxis),
145
+ normalize(list(map(operator.sub, vertices[1], vertices[0]))))),
146
+ same_sense_2)
147
+ else:
148
+ rev = list(reversed(vertices))
149
+ return self.advanced_face(
150
+ self._edge_loop_1(rev, same_sense_1),
151
+ self.plane(self._axis2_placement_3d(
152
+ vertices[0], normalize(zaxis),
153
+ normalize(list(map(operator.sub, rev[1], rev[0]))))),
154
+ same_sense_2)
155
+
156
+ def closed_shell(self, advanced_face_ln_list):
157
+ return self.new_item(
158
+ "CLOSED_SHELL('Closed Shell',("
159
+ + _to_step_list(advanced_face_ln_list) + ")) ;\n")
160
+
161
+ def manifold_solid_brep(self, closed_shell_ln):
162
+ msb = self.new_item(
163
+ "MANIFOLD_SOLID_BREP('PartBody." + str(self._part_body_index)
164
+ + "',#" + str(closed_shell_ln) + ") ;\n")
165
+ self._part_body_index += 1
166
+ return msb
167
+
168
+ def _advanced_brep_shape_representation(self, msb_list, init_ln=45):
169
+ return self.new_item(
170
+ "ADVANCED_BREP_SHAPE_REPRESENTATION('NONE',("
171
+ + _to_step_list(msb_list) + "),#" + str(init_ln) + ") ;\n")
172
+
173
+ def _shape_representation_relationship(self, absr_ln, sr_ln=48):
174
+ return self.new_item(
175
+ "SHAPE_REPRESENTATION_RELATIONSHIP(' ',' ',#" + str(sr_ln)
176
+ + ",#" + str(absr_ln) + ") ;\n")
177
+
178
+ def zface(self, vertex1, vertex2, geom_depth_list):
179
+ z_neg, z_pos = geom_depth_list
180
+ return self._advanced_face_0(
181
+ [list(map(operator.add, vertex1, [0, 0, z_neg])),
182
+ list(map(operator.add, vertex2, [0, 0, z_neg])),
183
+ list(map(operator.add, vertex2, [0, 0, z_pos])),
184
+ list(map(operator.add, vertex1, [0, 0, z_pos]))],
185
+ normalize(cross_product(
186
+ list(map(operator.sub, vertex2, vertex1)),
187
+ [0, 0, -(z_pos - z_neg)])))
188
+
189
+ def xyface(self, vertex_list, depth, zdir):
190
+ return self._advanced_face_0(
191
+ [list(map(operator.add, x, [0, 0, depth])) for x in vertex_list],
192
+ zdir)
193
+
194
+ def af2d3d(self, vertex_list, geom_depth_list):
195
+ z_neg, z_pos = geom_depth_list
196
+ faces = [
197
+ self.xyface(vertex_list, z_pos, [0, 0, 1]),
198
+ self.xyface(vertex_list, z_neg, [0, 0, -1]),
199
+ ]
200
+ faces += [
201
+ self.zface(x1, x2, geom_depth_list)
202
+ for x1, x2 in zip(vertex_list, rotate(vertex_list, -1))
203
+ ]
204
+ return faces
205
+
206
+ def generate_part(self, vert_list, geom_depth):
207
+ return self.manifold_solid_brep(
208
+ self.closed_shell(self.af2d3d(vert_list, geom_depth)))
209
+
210
+ def generate_assembly(self, list_vert_list, geom_depth_list, p_coeff=1):
211
+ if not isinstance(p_coeff, Number):
212
+ raise ValueError("NaN supplied for proportionality coefficient")
213
+ if p_coeff == 0:
214
+ raise ValueError("Zero supplied as the proportionality coefficient")
215
+ if not isinstance(geom_depth_list, list) or len(geom_depth_list) != 2:
216
+ raise ValueError("z-coordinate interval [z1, z2] expected")
217
+ for d in geom_depth_list:
218
+ if not isinstance(d, Number):
219
+ raise ValueError("NaN found in the z-coordinate interval")
220
+ if geom_depth_list[0] == geom_depth_list[1]:
221
+ raise ValueError("z2 must be different from z1")
222
+ if geom_depth_list[0] > geom_depth_list[1]:
223
+ geom_depth_list = [geom_depth_list[1], geom_depth_list[0]]
224
+ if not isinstance(list_vert_list, list) or not list_vert_list:
225
+ raise ValueError("Non-empty list of vertex lists expected")
226
+ for part in list_vert_list:
227
+ if not isinstance(part, list) or not part:
228
+ raise ValueError("Non-empty list of vertices expected")
229
+ for v in part:
230
+ if not isinstance(v, list) or not v:
231
+ raise ValueError("Non-empty vertex coordinate list expected")
232
+ if len(v) < 2 or len(v) > 3:
233
+ raise ValueError(
234
+ f"Vertex must have 2 or 3 coordinates, got {len(v)}")
235
+ for c in v:
236
+ if not isinstance(c, Number):
237
+ raise ValueError("NaN supplied for a vertex coordinate")
238
+
239
+ list_vert_list = [
240
+ [convert_3d(v) for v in part] for part in list_vert_list
241
+ ]
242
+ list_vert_list = [convert_to_clockwise(x) for x in list_vert_list]
243
+ list_vert_list = [
244
+ [[p_coeff * 1.0 * c for c in v] for v in part]
245
+ for part in list_vert_list
246
+ ]
247
+ geom_depth_list = [p_coeff * 1.0 * i for i in geom_depth_list]
248
+
249
+ part_list = [self.generate_part(x, geom_depth_list) for x in list_vert_list]
250
+ self._shape_representation_relationship(
251
+ self._advanced_brep_shape_representation(part_list))
252
+
253
+ def to_string(self):
254
+ result = (self._file_array[:self._index1]
255
+ + self._work_array
256
+ + self._file_array[self._index1 + 1:])
257
+ return ''.join(result)
258
+
259
+ def to_file(self, path):
260
+ with open(path, 'w') as f:
261
+ f.write(self.to_string())
@@ -0,0 +1,43 @@
1
+ import math
2
+ import operator
3
+ from numbers import Number
4
+
5
+
6
+ def rotate(list_in, x):
7
+ return list_in[-x:] + list_in[:-x]
8
+
9
+
10
+ def normalize(vector_in):
11
+ if len(vector_in) != 3:
12
+ raise ValueError("normalize: coordinates must be 3D")
13
+ magnitude = math.sqrt(sum(i ** 2 for i in vector_in))
14
+ return [x / magnitude for x in vector_in]
15
+
16
+
17
+ def cross_product(x, y):
18
+ return [
19
+ -x[2] * y[1] + x[1] * y[2],
20
+ x[2] * y[0] - x[0] * y[2],
21
+ -x[1] * y[0] + x[0] * y[1],
22
+ ]
23
+
24
+
25
+ def convert_3d(element_in):
26
+ if (isinstance(element_in, list) and len(element_in) == 2
27
+ and isinstance(element_in[0], Number)
28
+ and isinstance(element_in[1], Number)):
29
+ return [element_in[0], element_in[1], 0]
30
+ return element_in
31
+
32
+
33
+ def convert_to_clockwise(part_list):
34
+ pol_sum = sum(
35
+ (x2[0] - x1[0]) * (x2[1] + x1[1])
36
+ for x1, x2 in zip(part_list, rotate(part_list, 1))
37
+ )
38
+ if pol_sum == 0:
39
+ raise ValueError("Polygon is neither clockwise nor counter-clockwise")
40
+ elif pol_sum > 0:
41
+ return list(reversed(part_list))
42
+ else:
43
+ return list(part_list)
@@ -0,0 +1,102 @@
1
+ import datetime
2
+ import re
3
+
4
+
5
+ def _line_index(line):
6
+ m = re.search(r'#(.+?)=', line)
7
+ return 0 if m is None else int(m.group(1))
8
+
9
+
10
+ def step_header(filename='part_out.stp'):
11
+ d = datetime.datetime.now()
12
+ lines = [
13
+ "ISO-10303-21;",
14
+ "HEADER;",
15
+ "FILE_DESCRIPTION(('none'),'2;1');",
16
+ "",
17
+ "FILE_NAME('" + filename + "','none',('none'),('none'),"
18
+ "'none','none','none');",
19
+ "",
20
+ "FILE_SCHEMA(('CONFIG_CONTROL_DESIGN'));",
21
+ "",
22
+ "ENDSEC;",
23
+ "DATA;",
24
+ "#1=APPLICATION_CONTEXT('configuration controlled 3D design of"
25
+ " mechanical parts and assemblies') ;",
26
+ "#2=MECHANICAL_CONTEXT(' ',#1,'mechanical') ;",
27
+ "#3=DESIGN_CONTEXT(' ',#1,'design') ;",
28
+ "#4=APPLICATION_PROTOCOL_DEFINITION('international standard',"
29
+ "'config_control_design',1994,#1) ;",
30
+ "#5=PRODUCT('Part1','','',(#2)) ;",
31
+ "#6=PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE('',' '"
32
+ ",#5,.NOT_KNOWN.) ;",
33
+ "#7=PRODUCT_CATEGORY('part',$) ;",
34
+ "#8=PRODUCT_RELATED_PRODUCT_CATEGORY('detail',$,(#5)) ;",
35
+ "#9=PRODUCT_CATEGORY_RELATIONSHIP(' ',' ',#7,#8) ;",
36
+ "#10=COORDINATED_UNIVERSAL_TIME_OFFSET(0,0,.AHEAD.) ;",
37
+ "#11=CALENDAR_DATE(" + str(d.year) + "," + str(d.month) + ","
38
+ + str(d.day) + ") ;",
39
+ "#12=LOCAL_TIME(" + str(d.hour) + "," + str(d.minute) + ","
40
+ + str(d.second) + ".,#10) ;",
41
+ "#13=DATE_AND_TIME(#11,#12) ;",
42
+ "#14=PRODUCT_DEFINITION('',' ',#6,#3) ;",
43
+ "#15=SECURITY_CLASSIFICATION_LEVEL('unclassified') ;",
44
+ "#16=SECURITY_CLASSIFICATION(' ',' ',#15) ;",
45
+ "#17=DATE_TIME_ROLE('classification_date') ;",
46
+ "#18=CC_DESIGN_DATE_AND_TIME_ASSIGNMENT(#13,#17,(#16)) ;",
47
+ "#19=APPROVAL_ROLE('APPROVER') ;",
48
+ "#20=APPROVAL_STATUS('not_yet_approved') ;",
49
+ "#21=APPROVAL(#20,' ') ;",
50
+ "#22=PERSON(' ',' ',' ',$,$,$) ;",
51
+ "#23=ORGANIZATION(' ',' ',' ') ;",
52
+ "#24=PERSONAL_ADDRESS(' ',' ',' ',' ',' ',' ',' ',' ',' ',"
53
+ "' ',' ',' ',(#22),' ') ;",
54
+ "#25=PERSON_AND_ORGANIZATION(#22,#23) ;",
55
+ "#26=PERSON_AND_ORGANIZATION_ROLE('classification_officer') ;",
56
+ "#27=CC_DESIGN_PERSON_AND_ORGANIZATION_ASSIGNMENT(#25,#26,"
57
+ "(#16)) ;",
58
+ "#28=DATE_TIME_ROLE('creation_date') ;",
59
+ "#29=CC_DESIGN_DATE_AND_TIME_ASSIGNMENT(#13,#28,(#14)) ;",
60
+ "#30=CC_DESIGN_APPROVAL(#21,(#16,#6,#14)) ;",
61
+ "#31=APPROVAL_PERSON_ORGANIZATION(#25,#21,#19) ;",
62
+ "#32=APPROVAL_DATE_TIME(#13,#21) ;",
63
+ "#33=CC_DESIGN_PERSON_AND_ORGANIZATION_ASSIGNMENT(#25,#34,"
64
+ "(#6)) ;",
65
+ "#34=PERSON_AND_ORGANIZATION_ROLE('design_supplier') ;",
66
+ "#35=CC_DESIGN_PERSON_AND_ORGANIZATION_ASSIGNMENT(#25,#36,"
67
+ "(#6,#14)) ;",
68
+ "#36=PERSON_AND_ORGANIZATION_ROLE('creator') ;",
69
+ "#37=CC_DESIGN_PERSON_AND_ORGANIZATION_ASSIGNMENT(#25,#38,"
70
+ "(#5)) ;",
71
+ "#38=PERSON_AND_ORGANIZATION_ROLE('design_owner') ;",
72
+ "#39=CC_DESIGN_SECURITY_CLASSIFICATION(#16,(#6)) ;",
73
+ "",
74
+ "#40=PRODUCT_DEFINITION_SHAPE(' ',' ',#14) ;",
75
+ "#41=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.)) ;",
76
+ "#42=(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.)) ;",
77
+ "#43=(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT()) ;",
78
+ "#44=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(0.005),#41,"
79
+ "'distance_accuracy_value','CONFUSED CURVE"
80
+ " UNCERTAINTY') ;",
81
+ "#45=(GEOMETRIC_REPRESENTATION_CONTEXT(3)"
82
+ "GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#44))"
83
+ "GLOBAL_UNIT_ASSIGNED_CONTEXT((#41,#42,#43))"
84
+ "REPRESENTATION_CONTEXT(' ',' ')) ;",
85
+ "",
86
+ "#46=CARTESIAN_POINT(' ',(0.,0.,0.)) ;",
87
+ "#47=AXIS2_PLACEMENT_3D(' ',#46,$,$) ;",
88
+ "#48=SHAPE_REPRESENTATION(' ',(#47),#45) ;",
89
+ "#49=SHAPE_DEFINITION_REPRESENTATION(#40,#48) ;",
90
+ "",
91
+ "/* Part Specification */",
92
+ "",
93
+ "ENDSEC;",
94
+ "END-ISO-10303-21;",
95
+ ]
96
+ file_array = [i + "\n" for i in lines]
97
+ index1 = file_array.index("/* Part Specification */\n")
98
+ initial_work_array = [k for k in file_array if k.startswith('#')]
99
+ highest_index = _line_index(
100
+ sorted(initial_work_array, key=_line_index)[-1]
101
+ )
102
+ return file_array, index1, highest_index
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: stepfg
3
+ Version: 2.0.0
4
+ Summary: Convert 2D polygon cross-sections into 3D STEP files via extrusion
5
+ Author: Martin Berz
6
+ Author-email: Eremey Valetov <evv@msu.edu>
7
+ License-Expression: MIT
8
+ Project-URL: Repository, https://github.com/evvaletov/stepfg
9
+ Keywords: STEP,CAD,polygon,extrusion,3D,ISO-10303
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE.md
24
+ Dynamic: license-file
25
+
26
+ # stepfg: STEP File Generator
27
+
28
+ **Python 3.8+** | **Zero dependencies** | [MIT License](LICENSE.md)
29
+
30
+ Authors: E. Valetov and M. Berz
31
+ Organization: Michigan State University
32
+ Creation date: 03-Feb-2017
33
+
34
+ ## Introduction
35
+
36
+ stepfg converts lists of 2D polygons (specified by vertices in the x-y plane)
37
+ into 3D STEP files (ISO 10303-242) by extruding the polygon interiors along
38
+ the z-axis. Pure Python, no external dependencies.
39
+
40
+ ## Installation
41
+
42
+ ```
43
+ pip install stepfg
44
+ ```
45
+
46
+ Or from source:
47
+
48
+ ```
49
+ git clone https://github.com/evvaletov/stepfg.git
50
+ cd stepfg
51
+ pip install -e .
52
+ ```
53
+
54
+ ## Library usage
55
+
56
+ ```python
57
+ from stepfg import StepBuilder, generate_step
58
+
59
+ # Quick one-liner
60
+ content = generate_step(
61
+ polygons=[[[0, 0], [1, 0], [1, 1], [0, 1]]],
62
+ z_range=[0, 10],
63
+ scale=1,
64
+ )
65
+
66
+ # Or use StepBuilder for more control
67
+ builder = StepBuilder('output.stp')
68
+ builder.generate_assembly(
69
+ list_vert_list=[[[0, 0], [1, 0], [1, 1], [0, 1]]],
70
+ geom_depth_list=[0, 10],
71
+ p_coeff=1,
72
+ )
73
+ builder.to_file('output.stp')
74
+ ```
75
+
76
+ ## Command-line usage
77
+
78
+ ```
79
+ stepfg [input_file [output_file]]
80
+ python -m stepfg [input_file [output_file]]
81
+ ```
82
+
83
+ Run with the included sample geometry (a Muon g-2 quadrupole):
84
+
85
+ ```
86
+ stepfg part_geometry.txt quadrupole.stp
87
+ ```
88
+
89
+ ## Input file format
90
+
91
+ The input file is a Python literal containing three elements:
92
+
93
+ ```python
94
+ [polygons, z_range, scale]
95
+ ```
96
+
97
+ - **polygons**: `[[vertex, ...], ...]` — each vertex is `[x, y]` or `[x, y, 0]`
98
+ - **z_range**: `[z1, z2]` — extrusion interval
99
+ - **scale**: proportionality coefficient (output is in mm; use 10 for cm input)
100
+
101
+ A sample input file `part_geometry.txt` containing a Muon g-2 Collaboration
102
+ quadrupole cross-section is included.
103
+
104
+ ## Copyright Notice
105
+
106
+ © 2017 Eremey Valetov and Martin Berz
@@ -0,0 +1,16 @@
1
+ LICENSE.md
2
+ README.md
3
+ pyproject.toml
4
+ stepfg/__init__.py
5
+ stepfg/__main__.py
6
+ stepfg/_builder.py
7
+ stepfg/_geometry.py
8
+ stepfg/_template.py
9
+ stepfg.egg-info/PKG-INFO
10
+ stepfg.egg-info/SOURCES.txt
11
+ stepfg.egg-info/dependency_links.txt
12
+ stepfg.egg-info/entry_points.txt
13
+ stepfg.egg-info/top_level.txt
14
+ tests/test_builder.py
15
+ tests/test_cli.py
16
+ tests/test_geometry.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ stepfg = stepfg.__main__:main
@@ -0,0 +1 @@
1
+ stepfg
@@ -0,0 +1,71 @@
1
+ import pytest
2
+ from stepfg import StepBuilder
3
+
4
+
5
+ class TestStepBuilder:
6
+ def test_square_extrusion(self):
7
+ b = StepBuilder()
8
+ square = [[[0, 0], [1, 0], [1, 1], [0, 1]]]
9
+ b.generate_assembly(square, [0, 1])
10
+ content = b.to_string()
11
+
12
+ assert content.startswith("ISO-10303-21;\n")
13
+ assert content.endswith("END-ISO-10303-21;\n")
14
+ assert "CLOSED_SHELL" in content
15
+ assert "MANIFOLD_SOLID_BREP" in content
16
+ assert "ADVANCED_FACE" in content
17
+
18
+ def test_entity_count(self):
19
+ b = StepBuilder()
20
+ square = [[[0, 0], [1, 0], [1, 1], [0, 1]]]
21
+ b.generate_assembly(square, [0, 1])
22
+ content = b.to_string()
23
+ # Count ADVANCED_FACE entities: 4 sides + 2 caps = 6
24
+ assert content.count("ADVANCED_FACE") == 6
25
+
26
+ def test_scale_factor(self):
27
+ b = StepBuilder()
28
+ square = [[[0, 0], [1, 0], [1, 1], [0, 1]]]
29
+ b.generate_assembly(square, [0, 5], p_coeff=10)
30
+ content = b.to_string()
31
+ # Vertices should be scaled: 10.0 instead of 1.0
32
+ assert "10.0" in content
33
+
34
+ def test_multi_polygon(self):
35
+ b = StepBuilder()
36
+ polys = [
37
+ [[0, 0], [1, 0], [1, 1], [0, 1]],
38
+ [[2, 0], [3, 0], [3, 1], [2, 1]],
39
+ ]
40
+ b.generate_assembly(polys, [0, 1])
41
+ content = b.to_string()
42
+ # Two closed shells → two manifold solid breps
43
+ assert content.count("MANIFOLD_SOLID_BREP") == 2
44
+
45
+ def test_to_file(self, tmp_path):
46
+ b = StepBuilder()
47
+ square = [[[0, 0], [1, 0], [1, 1], [0, 1]]]
48
+ b.generate_assembly(square, [0, 1])
49
+ out = tmp_path / "test.stp"
50
+ b.to_file(str(out))
51
+ assert out.exists()
52
+ assert out.stat().st_size > 0
53
+ assert out.read_text() == b.to_string()
54
+
55
+
56
+ class TestValidation:
57
+ def test_zero_coeff(self):
58
+ with pytest.raises(ValueError, match="Zero"):
59
+ StepBuilder().generate_assembly([[[0, 0], [1, 0], [1, 1]]], [0, 1], 0)
60
+
61
+ def test_nan_coeff(self):
62
+ with pytest.raises(ValueError, match="NaN"):
63
+ StepBuilder().generate_assembly([[[0, 0], [1, 0], [1, 1]]], [0, 1], "x")
64
+
65
+ def test_equal_z(self):
66
+ with pytest.raises(ValueError, match="different"):
67
+ StepBuilder().generate_assembly([[[0, 0], [1, 0], [1, 1]]], [5, 5])
68
+
69
+ def test_empty_polygons(self):
70
+ with pytest.raises(ValueError):
71
+ StepBuilder().generate_assembly([], [0, 1])
@@ -0,0 +1,40 @@
1
+ import subprocess
2
+ import sys
3
+ from pathlib import Path
4
+
5
+
6
+ STEPFG_DIR = Path(__file__).resolve().parent.parent
7
+
8
+
9
+ def test_cli_with_sample_input(tmp_path):
10
+ out_file = tmp_path / "test_output.stp"
11
+ result = subprocess.run(
12
+ [sys.executable, "-m", "stepfg",
13
+ str(STEPFG_DIR / "part_geometry.txt"),
14
+ str(out_file)],
15
+ capture_output=True, text=True,
16
+ cwd=str(STEPFG_DIR),
17
+ )
18
+ assert result.returncode == 0
19
+ assert "[DONE]" in result.stdout
20
+ assert out_file.exists()
21
+ content = out_file.read_text()
22
+ assert content.startswith("ISO-10303-21;")
23
+ assert "CLOSED_SHELL" in content
24
+
25
+
26
+ def test_cli_missing_file(tmp_path):
27
+ result = subprocess.run(
28
+ [sys.executable, "-m", "stepfg", "nonexistent.txt"],
29
+ capture_output=True, text=True,
30
+ )
31
+ assert result.returncode != 0
32
+
33
+
34
+ def test_cli_version():
35
+ result = subprocess.run(
36
+ [sys.executable, "-m", "stepfg", "--version"],
37
+ capture_output=True, text=True,
38
+ )
39
+ assert result.returncode == 0
40
+ assert "2.0.0" in result.stdout
@@ -0,0 +1,61 @@
1
+ import math
2
+ import pytest
3
+ from stepfg._geometry import normalize, cross_product, convert_3d, convert_to_clockwise
4
+
5
+
6
+ class TestNormalize:
7
+ def test_unit_vector(self):
8
+ assert normalize([1, 0, 0]) == [1, 0, 0]
9
+
10
+ def test_scales_to_unit(self):
11
+ result = normalize([3, 4, 0])
12
+ assert math.isclose(result[0], 0.6)
13
+ assert math.isclose(result[1], 0.8)
14
+ assert math.isclose(result[2], 0.0)
15
+
16
+ def test_magnitude_one(self):
17
+ result = normalize([1, 2, 3])
18
+ mag = math.sqrt(sum(x ** 2 for x in result))
19
+ assert math.isclose(mag, 1.0)
20
+
21
+ def test_rejects_non_3d(self):
22
+ with pytest.raises(ValueError):
23
+ normalize([1, 2])
24
+
25
+
26
+ class TestCrossProduct:
27
+ def test_x_cross_y(self):
28
+ assert cross_product([1, 0, 0], [0, 1, 0]) == [0, 0, 1]
29
+
30
+ def test_y_cross_x(self):
31
+ assert cross_product([0, 1, 0], [1, 0, 0]) == [0, 0, -1]
32
+
33
+ def test_parallel_is_zero(self):
34
+ assert cross_product([1, 0, 0], [2, 0, 0]) == [0, 0, 0]
35
+
36
+
37
+ class TestConvert3d:
38
+ def test_2d_to_3d(self):
39
+ assert convert_3d([1, 2]) == [1, 2, 0]
40
+
41
+ def test_3d_unchanged(self):
42
+ assert convert_3d([1, 2, 3]) == [1, 2, 3]
43
+
44
+ def test_non_numeric_unchanged(self):
45
+ assert convert_3d([[1, 2], [3, 4]]) == [[1, 2], [3, 4]]
46
+
47
+
48
+ class TestConvertToClockwise:
49
+ def test_ccw_square_reversed(self):
50
+ ccw = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]
51
+ result = convert_to_clockwise(ccw)
52
+ assert result == list(reversed(ccw))
53
+
54
+ def test_cw_square_unchanged(self):
55
+ cw = [[0, 1, 0], [1, 1, 0], [1, 0, 0], [0, 0, 0]]
56
+ result = convert_to_clockwise(cw)
57
+ assert result == cw
58
+
59
+ def test_degenerate_raises(self):
60
+ with pytest.raises(ValueError):
61
+ convert_to_clockwise([[0, 0, 0], [0, 0, 0], [0, 0, 0]])