278 lines
7.3 KiB
Python
278 lines
7.3 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
"""
|
|
Import and export STL files
|
|
|
|
Used as a blender script, it load all the stl files in the scene:
|
|
|
|
blender --python stl_utils.py -- file1.stl file2.stl file3.stl ...
|
|
"""
|
|
|
|
# TODO: endian
|
|
|
|
|
|
class ListDict(dict):
|
|
"""
|
|
Set struct with order.
|
|
|
|
You can:
|
|
- insert data into without doubles
|
|
- get the list of data in insertion order with self.list
|
|
|
|
Like collections.OrderedDict, but quicker, can be replaced if
|
|
ODict is optimised.
|
|
"""
|
|
|
|
def __init__(self):
|
|
dict.__init__(self)
|
|
self.list = []
|
|
self._len = 0
|
|
|
|
def add(self, item):
|
|
"""
|
|
Add a value to the Set, return its position in it.
|
|
"""
|
|
value = self.setdefault(item, self._len)
|
|
if value == self._len:
|
|
self.list.append(item)
|
|
self._len += 1
|
|
|
|
return value
|
|
|
|
|
|
# an stl binary file is
|
|
# - 80 bytes of description
|
|
# - 4 bytes of size (unsigned int)
|
|
# - size triangles :
|
|
#
|
|
# - 12 bytes of normal
|
|
# - 9 * 4 bytes of coordinate (3*3 floats)
|
|
# - 2 bytes of garbage (usually 0)
|
|
BINARY_HEADER = 80
|
|
BINARY_STRIDE = 12 * 4 + 2
|
|
|
|
|
|
def _header_version():
|
|
import bpy
|
|
return "Exported from Blender-" + bpy.app.version_string
|
|
|
|
|
|
def _is_ascii_file(data):
|
|
"""
|
|
This function returns True if the data represents an ASCII file.
|
|
|
|
Please note that a False value does not necessary means that the data
|
|
represents a binary file. It can be a (very *RARE* in real life, but
|
|
can easily be forged) ascii file.
|
|
"""
|
|
|
|
import os
|
|
import struct
|
|
|
|
# Skip header...
|
|
data.seek(BINARY_HEADER)
|
|
size = struct.unpack('<I', data.read(4))[0]
|
|
# Use seek() method to get size of the file.
|
|
data.seek(0, os.SEEK_END)
|
|
file_size = data.tell()
|
|
# Reset to the start of the file.
|
|
data.seek(0)
|
|
|
|
if size == 0: # Odds to get that result from an ASCII file are null...
|
|
print("WARNING! Reported size (facet number) is 0, assuming invalid binary STL file.")
|
|
return False # Assume binary in this case.
|
|
|
|
return (file_size != BINARY_HEADER + 4 + BINARY_STRIDE * size)
|
|
|
|
|
|
def _binary_read(data):
|
|
# Skip header...
|
|
|
|
import os
|
|
import struct
|
|
|
|
data.seek(BINARY_HEADER)
|
|
size = struct.unpack('<I', data.read(4))[0]
|
|
|
|
if size == 0:
|
|
# Workaround invalid crap.
|
|
data.seek(0, os.SEEK_END)
|
|
file_size = data.tell()
|
|
# Reset to after-the-size in the file.
|
|
data.seek(BINARY_HEADER + 4)
|
|
|
|
file_size -= BINARY_HEADER + 4
|
|
size = file_size // BINARY_STRIDE
|
|
print("WARNING! Reported size (facet number) is 0, inferring %d facets from file size." % size)
|
|
|
|
# We read 4096 elements at once, avoids too much calls to read()!
|
|
CHUNK_LEN = 4096
|
|
chunks = [CHUNK_LEN] * (size // CHUNK_LEN)
|
|
chunks.append(size % CHUNK_LEN)
|
|
|
|
unpack = struct.Struct('<12f').unpack_from
|
|
for chunk_len in chunks:
|
|
if chunk_len == 0:
|
|
continue
|
|
buf = data.read(BINARY_STRIDE * chunk_len)
|
|
for i in range(chunk_len):
|
|
# read the normal and points coordinates of each triangle
|
|
pt = unpack(buf, BINARY_STRIDE * i)
|
|
yield pt[:3], (pt[3:6], pt[6:9], pt[9:])
|
|
|
|
|
|
def _ascii_read(data):
|
|
# an stl ascii file is like
|
|
# HEADER: solid some name
|
|
# for each face:
|
|
#
|
|
# facet normal x y z
|
|
# outerloop
|
|
# vertex x y z
|
|
# vertex x y z
|
|
# vertex x y z
|
|
# endloop
|
|
# endfacet
|
|
|
|
# strip header
|
|
data.readline()
|
|
|
|
curr_nor = None
|
|
|
|
for l in data:
|
|
l = l.lstrip()
|
|
if l.startswith(b'facet'):
|
|
curr_nor = tuple(map(float, l.split()[2:]))
|
|
# if we encounter a vertex, read next 2
|
|
if l.startswith(b'vertex'):
|
|
yield curr_nor, [tuple(map(float, l_item.split()[1:])) for l_item in (l, data.readline(), data.readline())]
|
|
|
|
|
|
def _binary_write(filepath, faces):
|
|
import struct
|
|
import itertools
|
|
from mathutils.geometry import normal
|
|
|
|
with open(filepath, 'wb') as data:
|
|
fw = data.write
|
|
# header
|
|
# we write padding at header beginning to avoid to
|
|
# call len(list(faces)) which may be expensive
|
|
fw(struct.calcsize('<80sI') * b'\0')
|
|
|
|
# 3 vertex == 9f
|
|
pack = struct.Struct('<9f').pack
|
|
|
|
# number of vertices written
|
|
nb = 0
|
|
|
|
for face in faces:
|
|
# calculate face normal
|
|
# write normal + vertices + pad as attributes
|
|
fw(struct.pack('<3f', *normal(*face)) + pack(*itertools.chain.from_iterable(face)))
|
|
# attribute byte count (unused)
|
|
fw(b'\0\0')
|
|
nb += 1
|
|
|
|
# header, with correct value now
|
|
data.seek(0)
|
|
fw(struct.pack('<80sI', _header_version().encode('ascii'), nb))
|
|
|
|
|
|
def _ascii_write(filepath, faces):
|
|
from mathutils.geometry import normal
|
|
|
|
with open(filepath, 'w') as data:
|
|
fw = data.write
|
|
header = _header_version()
|
|
fw('solid %s\n' % header)
|
|
|
|
for face in faces:
|
|
# calculate face normal
|
|
fw('facet normal %f %f %f\nouter loop\n' % normal(*face)[:])
|
|
for vert in face:
|
|
fw('vertex %f %f %f\n' % vert[:])
|
|
fw('endloop\nendfacet\n')
|
|
|
|
fw('endsolid %s\n' % header)
|
|
|
|
|
|
def write_stl(filepath="", faces=(), ascii=False):
|
|
"""
|
|
Write a stl file from faces,
|
|
|
|
filepath
|
|
output filepath
|
|
|
|
faces
|
|
iterable of tuple of 3 vertex, vertex is tuple of 3 coordinates as float
|
|
|
|
ascii
|
|
save the file in ascii format (very huge)
|
|
"""
|
|
(_ascii_write if ascii else _binary_write)(filepath, faces)
|
|
|
|
|
|
def read_stl(filepath):
|
|
"""
|
|
Return the triangles and points of an stl binary file.
|
|
|
|
Please note that this process can take lot of time if the file is
|
|
huge (~1m30 for a 1 Go stl file on an quad core i7).
|
|
|
|
- returns a tuple(triangles, triangles' normals, points).
|
|
|
|
triangles
|
|
A list of triangles, each triangle as a tuple of 3 index of
|
|
point in *points*.
|
|
|
|
triangles' normals
|
|
A list of vectors3 (tuples, xyz).
|
|
|
|
points
|
|
An indexed list of points, each point is a tuple of 3 float
|
|
(xyz).
|
|
|
|
Example of use:
|
|
|
|
>>> tris, tri_nors, pts = read_stl(filepath)
|
|
>>> pts = list(pts)
|
|
>>>
|
|
>>> # print the coordinate of the triangle n
|
|
>>> print(pts[i] for i in tris[n])
|
|
"""
|
|
import time
|
|
start_time = time.process_time()
|
|
|
|
tris, tri_nors, pts = [], [], ListDict()
|
|
|
|
with open(filepath, 'rb') as data:
|
|
# check for ascii or binary
|
|
gen = _ascii_read if _is_ascii_file(data) else _binary_read
|
|
|
|
for nor, pt in gen(data):
|
|
# Add the triangle and the point.
|
|
# If the point is already in the list of points, the
|
|
# index returned by pts.add() will be the one from the
|
|
# first equal point inserted.
|
|
tris.append([pts.add(p) for p in pt])
|
|
tri_nors.append(nor)
|
|
|
|
print('Import finished in %.4f sec.' % (time.process_time() - start_time))
|
|
|
|
return tris, tri_nors, pts.list
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
import bpy
|
|
from io_mesh_stl import blender_utils
|
|
|
|
filepaths = sys.argv[sys.argv.index('--') + 1:]
|
|
|
|
for filepath in filepaths:
|
|
objName = bpy.path.display_name(filepath)
|
|
tris, pts = read_stl(filepath)
|
|
|
|
blender_utils.create_and_link_mesh(objName, tris, pts)
|