Skip to content

Commit 8e928d1

Browse files
authored
Merge pull request #2887 from Wurschdhaud/implement-stl-interop
Add STL file import support for DirectContext3D server
2 parents abdb5c1 + 96ed5ec commit 8e928d1

File tree

5 files changed

+512
-296
lines changed

5 files changed

+512
-296
lines changed

extensions/pyRevitDevTools.extension/pyRevitDev.tab/Developer Examples.panel/DirectContext3D.pushbutton/dc3dtest_script.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
logger = script.get_logger()
1212
output = script.get_output()
1313

14+
1415
class UI(forms.WPFWindow):
1516
def __init__(self, xaml_file_name):
1617
forms.WPFWindow.__init__(self, xaml_file_name, handle_esc=False)
@@ -40,7 +41,7 @@ def button_apply(self, sender, args):
4041
int(self.txtRed.Text),
4142
int(self.txtGreen.Text),
4243
int(self.txtBlue.Text),
43-
int(self.txtTransp.Text)
44+
int(self.txtTransp.Text),
4445
)
4546

4647
except ValueError:
@@ -49,24 +50,19 @@ def button_apply(self, sender, args):
4950

5051
bb = DB.BoundingBoxXYZ()
5152
bb.Min = DB.XYZ(
52-
- width / 2,
53-
- length / 2,
54-
- height / 2,
53+
-width / 2,
54+
-length / 2,
55+
-height / 2,
5556
)
5657
bb.Max = DB.XYZ(
5758
width / 2,
5859
length / 2,
5960
height / 2,
6061
)
6162

62-
bb.Transform = DB.Transform.CreateTranslation(
63-
DB.XYZ(x, y, z)
64-
)
63+
bb.Transform = DB.Transform.CreateTranslation(DB.XYZ(x, y, z))
6564

66-
mesh = revit.dc3dserver.Mesh.from_boundingbox(
67-
bb,
68-
color
69-
)
65+
mesh = revit.dc3dserver.Mesh.from_boundingbox(bb, color)
7066

7167
self.server.meshes = [mesh]
7268
uidoc.RefreshActiveView()
@@ -87,5 +83,41 @@ def button_select(self, sender, args):
8783
self.server.meshes = mesh
8884
uidoc.RefreshActiveView()
8985

86+
def button_load_stl(self, sender, args):
87+
try:
88+
x = float(self.txtCoordX.Text)
89+
y = float(self.txtCoordY.Text)
90+
z = float(self.txtCoordZ.Text)
91+
color = DB.ColorWithTransparency(
92+
int(self.txtRed.Text),
93+
int(self.txtGreen.Text),
94+
int(self.txtBlue.Text),
95+
int(self.txtTransp.Text),
96+
)
97+
scale = float(self.txtSTLScale.Text)
98+
except ValueError:
99+
forms.alert("Input value invalid!", sub_msg=traceback.format_exc())
100+
return
101+
102+
stl_path = script.get_bundle_file("model.STL")
103+
104+
transform = DB.Transform.CreateTranslation(DB.XYZ(x, y, z))
105+
transform = transform.ScaleBasis(scale)
106+
107+
mesh = revit.dc3dserver.Mesh.from_stl(stl_path, color, transform=transform)
108+
109+
if mesh:
110+
self.server.meshes = [mesh]
111+
uidoc.RefreshActiveView()
112+
forms.alert(
113+
"STL loaded successfully!"
114+
)
115+
else:
116+
forms.alert(
117+
"Failed to load STL",
118+
sub_msg="Check the output window for error details.",
119+
)
120+
121+
90122
ui = UI("ui.xaml")
91123
ui.show(modal=False)
484 Bytes
Binary file not shown.

extensions/pyRevitDevTools.extension/pyRevitDev.tab/Developer Examples.panel/DirectContext3D.pushbutton/ui.xaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<RowDefinition />
1818
<RowDefinition />
1919
<RowDefinition />
20+
<RowDefinition />
2021
</Grid.RowDefinitions>
2122
<Grid.ColumnDefinitions>
2223
<ColumnDefinition Width="*"/>
@@ -42,8 +43,11 @@
4243
<TextBox Margin="5" Grid.Row="8" Grid.Column="1" Name="txtBlue" Text="0" />
4344
<TextBlock Margin="5" Grid.Row="9" Grid.Column="0">Transparency</TextBlock>
4445
<TextBox Margin="5" Grid.Row="9" Grid.Column="1" Name="txtTransp" Text="0" />
46+
<TextBlock Margin="5" Grid.Row="10" Grid.Column="0">STL Scale</TextBlock>
47+
<TextBox Margin="5" Grid.Row="10" Grid.Column="1" Name="txtSTLScale" Text="1" />
4548
</Grid>
4649
<Button x:Name="btnApply" Click="button_apply" Content="Create Box" />
4750
<Button x:Name="btnSelect" Margin="0,5,0,0" Click="button_select" Content="Pick Source Element" />
51+
<Button x:Name="btnLoadSTL" Margin="0,5,0,0" Click="button_load_stl" Content="Load STL (model.STL)" />
4852
</StackPanel>
4953
</Window>

pyrevitlib/pyrevit/interop/stl.py

Lines changed: 183 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,184 @@
11
"""Read and Write STL Binary and ASCII Files."""
2-
#
3-
# import struct
4-
#
5-
#
6-
# def load(inputfile):
7-
# pass
8-
#
9-
#
10-
# def dump(outputfile):
11-
# struct.pack('<I', bboxe_count * 12)
12-
# for bbox in bboxes:
13-
# minx = bbox.Min.X
14-
# miny = bbox.Min.Y
15-
# minz = bbox.Min.Z
16-
# maxx = bbox.Max.X
17-
# maxy = bbox.Max.Y
18-
# maxz = bbox.Max.Z
19-
# facets = [
20-
# [(0.0, -1.0, 0.0), [(minx, miny, minz),
21-
# (maxx, miny, minz),
22-
# (minx, miny, maxz)]],
23-
# [(0.0, -1.0, 0.0), [(minx, miny, maxz),
24-
# (maxx, miny, minz),
25-
# (maxx, miny, maxz)]],
26-
# [(0.0, 1.0, 0.0), [(minx, maxy, minz),
27-
# (maxx, maxy, minz),
28-
# (minx, maxy, maxz)]],
29-
# [(0.0, 1.0, 0.0), [(minx, maxy, maxz),
30-
# (maxx, maxy, minz),
31-
# (maxx, maxy, maxz)]],
32-
# [(-1.0, 0.0, 0.0), [(minx, miny, minz),
33-
# (minx, miny, maxz),
34-
# (minx, maxy, minz)]],
35-
# [(-1.0, 0.0, 0.0), [(minx, maxy, minz),
36-
# (minx, miny, maxz),
37-
# (minx, maxy, maxz)]],
38-
# [(1.0, 0.0, 0.0), [(maxx, miny, minz),
39-
# (maxx, miny, maxz),
40-
# (maxx, maxy, minz)]],
41-
# [(1.0, 0.0, 0.0), [(maxx, maxy, minz),
42-
# (maxx, miny, maxz),
43-
# (maxx, maxy, maxz)]],
44-
# [(0.0, 0.0, -1.0), [(minx, miny, minz),
45-
# (minx, maxy, minz),
46-
# (maxx, miny, minz)]],
47-
# [(0.0, 0.0, -1.0), [(maxx, miny, minz),
48-
# (minx, maxy, minz),
49-
# (maxx, maxy, minz)]],
50-
# [(0.0, 0.0, 1.0), [(minx, miny, maxz),
51-
# (minx, maxy, maxz),
52-
# (maxx, miny, maxz)]],
53-
# [(0.0, 0.0, 1.0), [(maxx, miny, maxz),
54-
# (minx, maxy, maxz),
55-
# (maxx, maxy, maxz)]],
56-
# ]
57-
# for facet in facets:
58-
# bboxfile.write(struct.pack('<3f', *facet[0]))
59-
# for vertix in facet[1]:
60-
# bboxfile.write(struct.pack('<3f', *vertix))
61-
# # attribute byte count (should be 0 per specs)
62-
# bboxfile.write('\0\0')
2+
3+
import struct
4+
import os.path as op
5+
6+
7+
class STLMesh(object):
8+
"""Container for STL mesh data."""
9+
10+
def __init__(self):
11+
self.triangles = []
12+
13+
def add_triangle(self, normal, vertices):
14+
"""Add a triangle to the mesh.
15+
16+
Args:
17+
normal: tuple of 3 floats (nx, ny, nz)
18+
vertices: list of 3 tuples, each with 3 floats (x, y, z)
19+
"""
20+
self.triangles.append({"normal": normal, "vertices": vertices})
21+
22+
23+
def load(filepath):
24+
"""Load an STL file (binary or ASCII).
25+
26+
Args:
27+
filepath: path to the STL file
28+
29+
Returns:
30+
STLMesh object containing the mesh data
31+
"""
32+
if not op.exists(filepath):
33+
raise IOError("File not found: {0}".format(filepath))
34+
35+
# Try to determine if file is binary or ASCII
36+
with open(filepath, "rb") as f:
37+
header = f.read(80)
38+
# ASCII files start with "solid"
39+
if header.startswith(b"solid") or header.startswith(b"SOLID"):
40+
# Could be ASCII, but need to verify
41+
f.seek(0)
42+
try:
43+
# Try reading as ASCII
44+
first_line = f.readline().decode("ascii", errors="strict")
45+
if first_line.strip().startswith("solid"):
46+
f.seek(0)
47+
return _load_ascii(f)
48+
except (UnicodeDecodeError, ValueError):
49+
pass
50+
51+
# If not ASCII, load as binary
52+
f.seek(0)
53+
return _load_binary(f)
54+
55+
56+
def _load_binary(file_handle):
57+
"""Load a binary STL file.
58+
59+
Args:
60+
file_handle: open file handle
61+
62+
Returns:
63+
STLMesh object
64+
"""
65+
mesh = STLMesh()
66+
67+
# Read 80-byte header (ignored)
68+
header = file_handle.read(80)
69+
70+
# Read number of triangles
71+
triangle_count_data = file_handle.read(4)
72+
if len(triangle_count_data) < 4:
73+
raise ValueError("Invalid STL file: cannot read triangle count")
74+
75+
triangle_count = struct.unpack("<I", triangle_count_data)[0]
76+
77+
# Read each triangle
78+
for i in range(triangle_count):
79+
# Each triangle is 50 bytes:
80+
# - 3 floats for normal (12 bytes)
81+
# - 3 vertices * 3 floats each (36 bytes)
82+
# - 1 uint16 attribute (2 bytes, usually unused)
83+
84+
triangle_data = file_handle.read(50)
85+
if len(triangle_data) < 50:
86+
raise ValueError(
87+
"Invalid STL file: incomplete triangle data at triangle {0}".format(i)
88+
)
89+
90+
# Unpack normal (3 floats)
91+
normal = struct.unpack("<3f", triangle_data[0:12])
92+
93+
# Unpack vertices (3 vertices * 3 floats)
94+
v1 = struct.unpack("<3f", triangle_data[12:24])
95+
v2 = struct.unpack("<3f", triangle_data[24:36])
96+
v3 = struct.unpack("<3f", triangle_data[36:48])
97+
98+
vertices = [v1, v2, v3]
99+
100+
# Attribute bytes (ignored)
101+
# attr = struct.unpack('<H', triangle_data[48:50])[0]
102+
103+
mesh.add_triangle(normal, vertices)
104+
105+
return mesh
106+
107+
108+
def _load_ascii(file_handle):
109+
"""Load an ASCII STL file.
110+
111+
Args:
112+
file_handle: open file handle
113+
114+
Returns:
115+
STLMesh object
116+
"""
117+
mesh = STLMesh()
118+
119+
current_normal = None
120+
current_vertices = []
121+
in_facet = False
122+
123+
for line in file_handle:
124+
line = line.strip()
125+
126+
if not line:
127+
continue
128+
129+
# Handle both bytes and str (for Python 2/3 compatibility)
130+
if isinstance(line, bytes):
131+
line = line.decode("ascii", errors="ignore")
132+
133+
line = line.lower()
134+
parts = line.split()
135+
136+
if not parts:
137+
continue
138+
139+
if parts[0] == "facet" and len(parts) >= 5 and parts[1] == "normal":
140+
in_facet = True
141+
current_normal = (float(parts[2]), float(parts[3]), float(parts[4]))
142+
current_vertices = []
143+
144+
elif parts[0] == "vertex" and len(parts) >= 4:
145+
vertex = (float(parts[1]), float(parts[2]), float(parts[3]))
146+
current_vertices.append(vertex)
147+
148+
elif parts[0] == "endfacet":
149+
if in_facet and current_normal and len(current_vertices) == 3:
150+
mesh.add_triangle(current_normal, current_vertices)
151+
in_facet = False
152+
current_normal = None
153+
current_vertices = []
154+
155+
return mesh
156+
157+
158+
def dump(outputfile, mesh):
159+
"""Save mesh to binary STL file.
160+
161+
Args:
162+
outputfile: path to output file
163+
mesh: STLMesh object to save
164+
"""
165+
with open(outputfile, "wb") as f:
166+
# Write 80-byte header
167+
header = b"Binary STL file generated by pyRevit"
168+
header = header + b" " * (80 - len(header))
169+
f.write(header[:80])
170+
171+
# Write triangle count
172+
f.write(struct.pack("<I", len(mesh.triangles)))
173+
174+
# Write each triangle
175+
for triangle in mesh.triangles:
176+
# Write normal
177+
f.write(struct.pack("<3f", *triangle["normal"]))
178+
179+
# Write vertices
180+
for vertex in triangle["vertices"]:
181+
f.write(struct.pack("<3f", *vertex))
182+
183+
# Write attribute byte count (0)
184+
f.write(struct.pack("<H", 0))

0 commit comments

Comments
 (0)