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.
- stepfg-2.0.0/LICENSE.md +21 -0
- stepfg-2.0.0/PKG-INFO +106 -0
- stepfg-2.0.0/README.md +81 -0
- stepfg-2.0.0/pyproject.toml +38 -0
- stepfg-2.0.0/setup.cfg +4 -0
- stepfg-2.0.0/stepfg/__init__.py +23 -0
- stepfg-2.0.0/stepfg/__main__.py +66 -0
- stepfg-2.0.0/stepfg/_builder.py +261 -0
- stepfg-2.0.0/stepfg/_geometry.py +43 -0
- stepfg-2.0.0/stepfg/_template.py +102 -0
- stepfg-2.0.0/stepfg.egg-info/PKG-INFO +106 -0
- stepfg-2.0.0/stepfg.egg-info/SOURCES.txt +16 -0
- stepfg-2.0.0/stepfg.egg-info/dependency_links.txt +1 -0
- stepfg-2.0.0/stepfg.egg-info/entry_points.txt +2 -0
- stepfg-2.0.0/stepfg.egg-info/top_level.txt +1 -0
- stepfg-2.0.0/tests/test_builder.py +71 -0
- stepfg-2.0.0/tests/test_cli.py +40 -0
- stepfg-2.0.0/tests/test_geometry.py +61 -0
stepfg-2.0.0/LICENSE.md
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -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]])
|