Library to make reading, writing and modifying both binary and ascii STL files easy.
https://pypi.org/project/numpy-stl/
原始碼: https://github.com/WoLpH/numpy-stl
手冊: https://numpy-stl.readthedocs.io/en/latest/
作者網誌: https://w.wol.ph/
from stl import mesh
from mpl_toolkits import mplot3d
from matplotlib import pyplot
# Create a new plot
figure = pyplot.figure()
axes = mplot3d.Axes3D(figure)
# Load the STL files and add the vectors to the plot
your_mesh = mesh.Mesh.from_file('tests/stl_binary/HalfDonut.stl')
axes.add_collection3d(mplot3d.art3d.Poly3DCollection(your_mesh.vectors))
# Auto scale to the mesh size
scale = your_mesh.points.flatten(-1)
axes.auto_scale_xyz(scale, scale, scale)
# Show the plot to the screen
pyplot.show()
列出 STL 零件檔案尺寸
# Python script to find STL dimensions
# Requrements: sudo pip install numpy-stl
import math
import stl
from stl import mesh
import numpy
import os
import sys
if len(sys.argv) < 2:
sys.exit('Usage: %s [stl file]' % sys.argv[0])
if not os.path.exists(sys.argv[1]):
sys.exit('ERROR: file %s was not found!' % sys.argv[1])
# this stolen from numpy-stl documentation
# https://pypi.python.org/pypi/numpy-stl
# find the max dimensions, so we can know the bounding box, getting the height,
# width, length (because these are the step size)...
def find_mins_maxs(obj):
minx = maxx = miny = maxy = minz = maxz = None
for p in obj.points:
# p contains (x, y, z)
if minx is None:
minx = p[stl.Dimension.X]
maxx = p[stl.Dimension.X]
miny = p[stl.Dimension.Y]
maxy = p[stl.Dimension.Y]
minz = p[stl.Dimension.Z]
maxz = p[stl.Dimension.Z]
else:
maxx = max(p[stl.Dimension.X], maxx)
minx = min(p[stl.Dimension.X], minx)
maxy = max(p[stl.Dimension.Y], maxy)
miny = min(p[stl.Dimension.Y], miny)
maxz = max(p[stl.Dimension.Z], maxz)
minz = min(p[stl.Dimension.Z], minz)
return minx, maxx, miny, maxy, minz, maxz
main_body = mesh.Mesh.from_file(sys.argv[1])
minx, maxx, miny, maxy, minz, maxz = find_mins_maxs(main_body)
# the logic is easy from there
print("File:", sys.argv[1])
print("X:", maxx - minx)
print("Y:", maxy - miny)
print("Z:", maxz - minz)
相關工具: https://github.com/WoLpH/python-utils
手冊: https://python-utils.readthedocs.io/en/latest/
https://github.com/apparentlymart/python-stl
python-stl 內容
__init__.py
import stl.ascii
import stl.binary
from stl.types import Solid, Facet, Vector3d
def read_ascii_file(file):
"""
Read an STL file in the *ASCII* format.
Takes a :py:class:`file`-like object (supporting a ``read`` method)
and returns a :py:class:`stl.Solid` object representing the data
from the file.
If the file is invalid in any way, raises
:py:class:`stl.ascii.SyntaxError`.
"""
return stl.ascii.parse(file)
def read_binary_file(file):
"""
Read an STL file in the *binary* format.
Takes a :py:class:`file`-like object (supporting a ``read`` method)
and returns a :py:class:`stl.Solid` object representing the data
from the file.
If the file is invalid in any way, raises
:py:class:`stl.binary.FormatError`.
"""
return stl.binary.parse(file)
def read_ascii_string(data):
"""
Read geometry from a :py:class:`str` containing data in the STL *ASCII*
format.
This is just a wrapper around :py:func:`read_ascii_file` that first wraps
the provided string in a :py:class:`StringIO.StringIO` object.
"""
from StringIO import StringIO
return parse_ascii_file(StringIO(data))
def read_binary_string(data):
"""
Read geometry from a :py:class:`str` containing data in the STL *binary*
format.
This is just a wrapper around :py:func:`read_binary_file` that first wraps
the provided string in a :py:class:`StringIO.StringIO` object.
"""
from StringIO import StringIO
return parse_binary_file(StringIO(data))
ascii.py
from stl.types import *
class KeywordToken(str):
pass
class NumberToken(float):
pass
def _token_type_name(token_type):
NoneType = type(None)
if token_type is NoneType:
return 'end of file'
elif token_type is KeywordToken:
return 'keyword'
elif token_type is NumberToken:
return 'number'
else:
return 'unknown'
class Scanner(object):
def __init__(self, file):
self.file = file
self.peeked = None
self.peeked_byte = None
self.peeked_col = 0
self.peeked_row = 1
def peek_byte(self):
if self.peeked_byte is None:
self.peeked_byte = self.file.read(1)
if self.peeked_byte == '\n':
self.peeked_row += 1
self.peeked_col = 0
else:
self.peeked_col += 1
return self.peeked_byte
def get_byte(self):
byte = self.peek_byte()
self.peeked_byte = None
return byte
def peek_token(self):
while self.peeked is None:
b = self.peek_byte()
self.token_start_row = self.peeked_row
self.token_start_col = self.peeked_col
if b == '':
return None
elif b.isalpha() or b == '_':
self.peeked = self._read_keyword()
elif b.isdigit() or b == '.' or b == '-':
self.peeked = self._read_number()
elif b.isspace():
# Just skip over spaces
self.get_byte()
continue
else:
raise SyntaxError(
"Invalid character %r at line %i, column %i" % (
b, self.peeked_row, self.peeked_col
)
)
return self.peeked
def get_token(self):
token = self.peek_token()
self.peeked = None
return token
def require_token(self, token_type, required_value=None):
token = self.get_token()
if isinstance(token, token_type):
if required_value is None or token == required_value:
return token
else:
got_token_type = _token_type_name(type(token))
expected_token_type = _token_type_name(token_type)
raise SyntaxError(
"Expected %s %r but got %s %r at line %i, column %i" % (
expected_token_type,
required_value,
got_token_type,
token,
self.token_start_row,
self.token_start_col,
)
)
else:
got_token_type = _token_type_name(type(token))
expected_token_type = _token_type_name(token_type)
raise SyntaxError(
"Expected %s but got %s at line %i, column %i" % (
expected_token_type,
got_token_type,
self.token_start_row,
self.token_start_col,
)
)
def _read_keyword(self):
ret_bytes = []
start_row = self.peeked_row
start_col = self.peeked_col
while True:
b = self.peek_byte()
if b.isalpha() or b == '_' or b.isdigit():
ret_bytes.append(self.get_byte())
else:
break
ret = KeywordToken(''.join(ret_bytes))
ret.start_row = start_row
ret.start_col = start_col
return ret
def _read_number(self):
ret_bytes = []
start_row = self.peeked_row
start_col = self.peeked_col
while True:
b = self.peek_byte()
if b.isdigit() or b in ('.', '+', '-', 'e', 'E'):
ret_bytes.append(self.get_byte())
else:
break
try:
ret = NumberToken(''.join(ret_bytes))
except ValueError:
raise SyntaxError(
"Invalid float number at line %i, column %i" % (
start_row, start_col,
)
)
ret.start_row = start_row
ret.start_col = start_col
return ret
class SyntaxError(ValueError):
pass
def parse(file):
scanner = Scanner(file)
scanner.require_token(KeywordToken, "solid")
name = str(scanner.require_token(KeywordToken))
ret = Solid(name=name)
def parse_facet():
scanner.require_token(KeywordToken, "facet")
scanner.require_token(KeywordToken, "normal")
normal_x = scanner.require_token(NumberToken)
normal_y = scanner.require_token(NumberToken)
normal_z = scanner.require_token(NumberToken)
normal = Vector3d(
x=normal_x,
y=normal_y,
z=normal_z,
)
scanner.require_token(KeywordToken, "outer")
scanner.require_token(KeywordToken, "loop")
vertices = []
for i in xrange(0, 3):
scanner.require_token(KeywordToken, "vertex")
vertex_x = scanner.require_token(NumberToken)
vertex_y = scanner.require_token(NumberToken)
vertex_z = scanner.require_token(NumberToken)
vertices.append(
Vector3d(
x=vertex_x,
y=vertex_y,
z=vertex_z,
)
)
ret = Facet(
normal=normal,
vertices=vertices,
)
scanner.require_token(KeywordToken, "endloop")
scanner.require_token(KeywordToken, "endfacet")
return ret
while True:
token = scanner.peek_token()
token_type = type(token)
if token_type is KeywordToken and token == 'endsolid':
break
elif token_type is KeywordToken and token == 'facet':
facet = parse_facet()
ret.facets.append(facet)
else:
got_token_type = _token_type_name(token_type)
expected_token_type = _token_type_name(token_type)
raise SyntaxError(
"Unexpected %s %r at line %i, column %i" % (
got_token_type,
token,
token.start_row,
token.start_col,
)
)
scanner.require_token(KeywordToken, "endsolid")
end_name = str(scanner.require_token(KeywordToken))
if name != end_name:
raise SyntaxError(
"Solid started named %r but ended named %r" % (
name, end_name,
)
)
return ret
def write(solid, file):
name = solid.name
if name is None:
name = "unnamed"
file.write(("solid %s\n" % name).encode())
for facet in solid.facets:
file.write((" facet normal %g %g %g\n" % facet.normal).encode())
file.write(b" outer loop\n")
for vertex in facet.vertices:
file.write((" vertex %g %g %g\n" % vertex).encode())
file.write(b" endloop\n")
file.write(b" endfacet\n")
file.write(("endsolid %s\n" % name).encode())
binary.py
import struct
from stl.types import *
class Reader(object):
def __init__(self, file):
self.file = file
self.offset = 0
def read_bytes(self, byte_count):
bytes = self.file.read(byte_count)
if len(bytes) < byte_count:
raise FormatError(
"Unexpected end of file at offset %i" % (
self.offset + len(bytes),
)
)
self.offset += byte_count
return bytes
def read_uint32(self):
bytes = self.read_bytes(4)
return struct.unpack('<I', bytes)[0]
def read_uint16(self):
bytes = self.read_bytes(2)
return struct.unpack('<H', bytes)[0]
def read_float(self):
bytes = self.read_bytes(4)
return struct.unpack('<f', bytes)[0]
def read_vector3d(self):
x = self.read_float()
y = self.read_float()
z = self.read_float()
return Vector3d(x, y, z)
def read_header(self):
bytes = self.read_bytes(80)
return struct.unpack('80s', bytes)[0].strip('\0')
class FormatError(ValueError):
pass
def parse(file):
r = Reader(file)
name = r.read_header()[6:]
ret = Solid(name=name)
num_facets = r.read_uint32()
for i in xrange(0, num_facets):
normal = r.read_vector3d()
vertices = tuple(
r.read_vector3d() for j in xrange(0, 3)
)
attr_byte_count = r.read_uint16()
if attr_byte_count > 0:
# The attribute bytes are not standardized, but some software
# encodes additional information here. We return the raw bytes
# to allow the caller to potentially do something with them if
# the format for a particular file is known.
attr_bytes = r.read_bytes(attr_byte_count)
else:
attr_bytes = None
ret.add_facet(
normal=normal,
vertices=vertices,
attributes=attr_bytes,
)
return ret
def write(solid, file):
# Empty header
file.write(b'\0' * 80)
# Number of facets
file.write(struct.pack('<I', len(solid.facets)))
for facet in solid.facets:
file.write(struct.pack('<3f', *facet.normal))
for vertex in facet.vertices:
file.write(struct.pack('<3f', *vertex))
file.write(b'\0\0') # no attribute bytes
types.py
import math
class Solid(object):
"""
A solid object; the root element of an STL file.
"""
#: The name given to the object by the STL file header.
name = None
#: :py:class:`list` of :py:class:`stl.Facet` objects representing the
#: facets (triangles) that make up the exterior surface of this object.
facets = []
def __init__(self, name=None, facets=None):
self.name = name
self.facets = facets if facets is not None else []
def add_facet(self, *args, **kwargs):
"""
Append a new facet to the object. Takes the same arguments as the
:py:class:`stl.Facet` type.
"""
self.facets.append(Facet(*args, **kwargs))
@property
def surface_area(self):
"""
The sum of the areas of all facets in the object.
"""
return reduce(
lambda accum, facet: accum + facet.area,
self.facets,
0.0,
)
def write_binary(self, file):
"""
Write this object to a file in STL *binary* format.
``file`` must be a file-like object (supporting a ``write`` method),
to which the data will be written.
"""
from stl.binary import write
write(self, file)
def write_ascii(self, file):
"""
Write this object to a file in STL *ascii* format.
``file`` must be a file-like object (supporting a ``write`` method),
to which the data will be written.
"""
from stl.ascii import write
write(self, file)
def __eq__(self, other):
if type(other) is Solid:
if self.name != other.name:
return False
if len(self.facets) != len(other.facets):
return False
for i, self_facet in enumerate(self.facets):
if self_facet != other.facets[i]:
return False
return True
else:
return False
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return '<stl.types.Solid name=%r, facets=%r>' % (
self.name,
self.facets,
)
class Facet(object):
"""
A facet (triangle) from a :py:class:`stl.Solid`.
"""
#: Raw binary attribute bytes. According to the STL spec these are unused
#: and thus this should always be empty, but some modeling software
#: encodes non-standard data in here which callers may wish to access.
#:
#: At present these attribute bytes are populated only when reading binary
#: STL files (since ASCII STL files have no place for this data) *and*
#: they are ignored when *writing* a binary STL file, so round-tripping
#: a file through this library will lose the non-standard attribute data.
attributes = None
#: The 'normal' vector of the facet, as a :py:class:`stl.Vector3d`.
normal = None
#: 3-element sequence of :py:class:`stl.Vector3d` representing the
#: facet's three vertices, in order.
vertices = None
def __init__(self, normal, vertices, attributes=None):
self.normal = Vector3d(*normal)
self.vertices = tuple(
Vector3d(*x) for x in vertices
)
if len(self.vertices) != 3:
raise ValueError('Must pass exactly three vertices')
def __eq__(self, other):
if type(other) is Facet:
return (
self.normal == other.normal and
self.vertices == other.vertices
)
else:
return False
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return '<stl.types.Facet normal=%r, vertices=%r, area=%r>' % (
self.normal,
self.vertices,
self.area,
)
@property
def a(self):
"""
The length the side of the facet between vertices[0] and vertices[1]
"""
return abs(
math.sqrt(
pow((self.vertices[0].x - self.vertices[1].x), 2) +
pow((self.vertices[0].y - self.vertices[1].y), 2) +
pow((self.vertices[0].z - self.vertices[1].z), 2)
)
)
@property
def b(self):
"""
The length of the side of the facet between vertices[0] and vertices[2]
"""
return abs(
math.sqrt(
pow((self.vertices[0].x - self.vertices[2].x), 2) +
pow((self.vertices[0].y - self.vertices[2].y), 2) +
pow((self.vertices[0].z - self.vertices[2].z), 2)
)
)
@property
def c(self):
"""
The length of the side of the facet between vertices[1] and vertices[2]
"""
return abs(
math.sqrt(
pow((self.vertices[1].x - self.vertices[2].x), 2) +
pow((self.vertices[1].y - self.vertices[2].y), 2) +
pow((self.vertices[1].z - self.vertices[2].z), 2)
)
)
@property
def perimeter(self):
"""
The length of the perimeter of the facet.
"""
return self.a + self.b + self.c
@property
def area(self):
"""
The surface area of the facet, as computed by Heron's Formula.
"""
p = self.perimeter / 2.0
return abs(math.sqrt(p * (p - self.a) * (p - self.b) * (p - self.c)))
class Vector3d(tuple):
"""
Three-dimensional vector.
Used to represent both normals and vertices of :py:class:`stl.Facet`
objects.
This is a subtype of :py:class:`tuple`, so can also be treated like a
three-element tuple in (``x``, ``y``, ``z``) order.
"""
def __new__(cls, x, y, z):
return tuple.__new__(cls, (x, y, z))
def __init__(self, x, y, z):
pass
@property
def x(self):
"""
The X value of the vector, which most applications interpret
as the left-right axis.
"""
return self[0]
@x.setter
def x(self, value):
self[0] = value
@property
def y(self):
"""
The Y value of the vector, which most applications interpret
as the in-out axis.
"""
return self[1]
@y.setter
def y(self, value):
self[1] = value
@property
def z(self):
"""
The Z value of the vector, which most applications interpret
as the up-down axis.
"""
return self[2]
@z.setter
def z(self, value):
self[2] = value
test_types.py
import unittest
from stl.types import *
class TestTypes(unittest.TestCase):
def test_facet_geometry(self):
facet = Facet(
(1, 0, 0),
[
(0, 0, 0),
(1, 0, 0),
(0, 1, 0),
],
)
self.assertEqual(facet.a, 1.0)
self.assertEqual(facet.b, 1.0)
self.assertAlmostEqual(facet.c, 1.4142135623730951)
self.assertAlmostEqual(
facet.perimeter,
1.0 + 1.0 + 1.4142135623730951,
)
self.assertAlmostEqual(facet.area, 0.5)
def test_solid_geometry(self):
solid = Solid(
"test",
[
Facet(
(1, 0, 0),
[
(0, 0, 0),
(1, 0, 0),
(0, 1, 0),
],
),
Facet(
(1, 0, 0),
[
(0, 0, 0),
(1, 0, 0),
(0, 0, 1),
],
),
],
)
self.assertAlmostEqual(solid.surface_area, 0.5 + 0.5)
test_code_style.py
import unittest
import pep8
import os.path
tests_dir = os.path.dirname(__file__)
modules_dir = os.path.abspath(os.path.join(tests_dir, "..", "stl"))
class TestCodeStyle(unittest.TestCase):
def test_pep8_conformance(self):
pep8style = pep8.StyleGuide()
result = pep8style.check_files([tests_dir, modules_dir])
self.assertEqual(
result.total_errors,
0,
"Found pep8 conformance issues",
)
test_ascii.py
from StringIO import StringIO
import unittest
from stl.ascii import *
class TestScanner(unittest.TestCase):
def _scanner_for_str(self, string):
return Scanner(StringIO(string))
def _get_tokens(self, string):
scanner = self._scanner_for_str(string)
tokens = []
while True:
token = scanner.get_token()
if token is not None:
tokens.append(token)
else:
break
return tokens
def test_numbers(self):
tokens = self._get_tokens("1 0.1 -0.1 1e2\n1.2e2 1.2e-2 1.2e+2 1.2E+2")
self.assertEqual(
tokens,
[
1, 0.1, -0.1, 100, 120, 0.012, 120.0, 120.0,
],
)
self.assertEqual(
[tokens[1].start_row, tokens[1].start_col],
[1, 3],
)
self.assertEqual(
[tokens[5].start_row, tokens[5].start_col],
[2, 7],
)
with self.assertRaises(SyntaxError):
self._get_tokens("1e1e1")
with self.assertRaises(SyntaxError):
self._get_tokens("1.1.1")
with self.assertRaises(SyntaxError):
self._get_tokens("--2")
def test_keywords(self):
tokens = self._get_tokens("hello world a\nb c _d e_f g2")
self.assertEqual(
tokens,
[
'hello', 'world', 'a', 'b', 'c', '_d', 'e_f', 'g2',
],
)
self.assertEqual(
[tokens[1].start_row, tokens[1].start_col],
[1, 7],
)
self.assertEqual(
[tokens[4].start_row, tokens[4].start_col],
[2, 3],
)
def test_spaces(self):
tokens = self._get_tokens(" \n\t")
self.assertEqual(
tokens,
[],
)
def test_require_token(self):
scanner = self._scanner_for_str("baz")
try:
scanner.require_token(KeywordToken)
except SyntaxError:
self.fail('Unexpected SyntaxError')
scanner = self._scanner_for_str("baz")
try:
scanner.require_token(KeywordToken, "baz")
except SyntaxError:
self.fail('Unexpected SyntaxError')
scanner = self._scanner_for_str("baz")
with self.assertRaises(SyntaxError):
scanner.require_token(NumberToken)
scanner = self._scanner_for_str("baz")
with self.assertRaises(SyntaxError):
scanner.require_token(KeywordToken, "foo")
class TestParser(unittest.TestCase):
def _parse_str(self, string):
return parse(StringIO(string))
def test_empty(self):
with self.assertRaises(SyntaxError):
self._parse_str('')
def test_no_facets(self):
self.assertEqual(
self._parse_str("solid Baz\nendsolid Baz\n"),
Solid(name="Baz"),
)
def test_inconsistent_name(self):
with self.assertRaises(SyntaxError):
self._parse_str("solid Baz\nendsolid Bonk\n")
def test_facets(self):
self.assertEqual(
self._parse_str(
"solid Baz\n"
" facet normal 1 2 3\n"
" outer loop\n"
" vertex 4 5 6\n"
" vertex 7 8 9\n"
" vertex 10 11 12\n"
" endloop\n"
" endfacet\n"
" facet normal 1.1 2.1 3.1\n"
" outer loop\n"
" vertex 4.1 5.1 6.1\n"
" vertex 7.1 8.1 9.1\n"
" vertex 10.1 11.1 12.1\n"
" endloop\n"
" endfacet\n"
"endsolid Baz\n"
),
Solid(
name="Baz",
facets=[
Facet(
normal=Vector3d(1.0, 2.0, 3.0),
vertices=(
Vector3d(4.0, 5.0, 6.0),
Vector3d(7.0, 8.0, 9.0),
Vector3d(10.0, 11.0, 12.0),
),
),
Facet(
normal=Vector3d(1.1, 2.1, 3.1),
vertices=(
Vector3d(4.1, 5.1, 6.1),
Vector3d(7.1, 8.1, 9.1),
Vector3d(10.1, 11.1, 12.1),
),
),
],
),
)
class TestWriter(unittest.TestCase):
def assertResultEqual(self, solid, expected):
f = StringIO('')
solid.write_ascii(f)
self.assertEqual(
f.getvalue(),
expected,
)
def test_empty(self):
self.assertResultEqual(
Solid(),
'solid unnamed\n'
'endsolid unnamed\n'
)
def test_with_facets(self):
self.assertResultEqual(
Solid(
name='withfacets',
facets=[
Facet(
normal=(1, 2, 3),
vertices=[
(4, 5, 6),
(7, 8, 9),
(10, 11, 12),
],
),
Facet(
normal=(1.1, 2.1, 3.1),
vertices=[
(4.1, 5.1, 6.1),
(7.1, 8.1, 9.1),
(10.1, 11.1, 12.1),
],
),
],
),
'solid withfacets\n'
' facet normal 1 2 3\n'
' outer loop\n'
' vertex 4 5 6\n'
' vertex 7 8 9\n'
' vertex 10 11 12\n'
' endloop\n'
' endfacet\n'
' facet normal 1.1 2.1 3.1\n'
' outer loop\n'
' vertex 4.1 5.1 6.1\n'
' vertex 7.1 8.1 9.1\n'
' vertex 10.1 11.1 12.1\n'
' endloop\n'
' endfacet\n'
'endsolid withfacets\n'
)
test_binary.py
from StringIO import StringIO
import unittest
from stl.binary import *
EMPTY_HEADER = '\0' * 80
T_HDR = '\x73\x6f\x6c\x69\x64\x20\x54\x65\x73\x74\x66\x69\x6c\x65' + ('\0'*66)
class TestParser(unittest.TestCase):
def _parse_str(self, string):
return parse(StringIO(string))
def test_empty(self):
with self.assertRaises(FormatError):
self._parse_str('')
def test_no_facets(self):
solid = self._parse_str(
T_HDR + '\0\0\0\0'
)
self.assertEqual(
solid,
Solid(
name='Testfile',
facets=[],
),
)
def test_missing_facets(self):
with self.assertRaises(FormatError):
# Declared that we have two facets but we
# actually have none.
self._parse_str(
T_HDR + '\x02\x00\x00\x00'
)
def test_valid(self):
solid = self._parse_str(
T_HDR +
'\x02\x00\x00\x00' # two facets
# first facet
'\x00\x00\x80\x3f' # normal x = 1.0
'\x00\x00\x00\x40' # normal y = 2.0
'\x00\x00\x40\x40' # normal z = 3.0
'\x00\x00\x80\x40' # vertex x = 4.0
'\x00\x00\xa0\x40' # vertex y = 5.0
'\x00\x00\xc0\x40' # vertex z = 6.0
'\x00\x00\xe0\x40' # vertex x = 7.0
'\x00\x00\x00\x41' # vertex y = 8.0
'\x00\x00\x10\x41' # vertex z = 9.0
'\x00\x00\x20\x41' # vertex x = 10.0
'\x00\x00\x30\x41' # vertex y = 11.0
'\x00\x00\x40\x41' # vertex z = 12.0
'\x04\x00' # four attribute bytes
'\x00\x00\x80\x7f' # dummy attribute bytes (float Infinity)
# second facet
'\x00\x00\x80\x3f' # normal x = 1.0
'\x00\x00\x80\x3f' # normal y = 1.0
'\x00\x00\x80\x3f' # normal z = 1.0
'\x00\x00\x80\x3f' # vertex x = 1.0
'\x00\x00\x80\x3f' # vertex y = 1.0
'\x00\x00\x80\x3f' # vertex z = 1.0
'\x00\x00\x80\x3f' # vertex x = 1.0
'\x00\x00\x80\x3f' # vertex y = 1.0
'\x00\x00\x80\x3f' # vertex z = 1.0
'\x00\x00\x80\x3f' # vertex x = 1.0
'\x00\x00\x80\x3f' # vertex y = 1.0
'\x00\x00\x80\x3f' # vertex z = 1.0
'\x00\x00' # no attribute bytes
)
self.assertEqual(
solid,
Solid(
name='Testfile',
facets=[
Facet(
normal=Vector3d(1.0, 2.0, 3.0),
vertices=(
Vector3d(4.0, 5.0, 6.0),
Vector3d(7.0, 8.0, 9.0),
Vector3d(10.0, 11.0, 12.0),
),
attributes='\x00\x00\x80\x7f',
),
Facet(
normal=Vector3d(1.0, 1.0, 1.0),
vertices=(
Vector3d(1.0, 1.0, 1.0),
Vector3d(1.0, 1.0, 1.0),
Vector3d(1.0, 1.0, 1.0),
),
attributes=None,
),
],
),
)
class TestWriter(unittest.TestCase):
def assertResultEqual(self, solid, expected):
f = StringIO('')
solid.write_binary(f)
self.assertEqual(
f.getvalue(),
expected,
)
def test_empty(self):
self.assertResultEqual(
Solid(),
EMPTY_HEADER +
'\0\0\0\0'
)
def test_with_facets(self):
self.assertResultEqual(
Solid(
name=None,
facets=[
Facet(
normal=Vector3d(1.0, 2.0, 3.0),
vertices=(
Vector3d(4.0, 5.0, 6.0),
Vector3d(7.0, 8.0, 9.0),
Vector3d(10.0, 11.0, 12.0),
),
attributes=None,
),
Facet(
normal=Vector3d(1.0, 1.0, 1.0),
vertices=(
Vector3d(1.0, 1.0, 1.0),
Vector3d(1.0, 1.0, 1.0),
Vector3d(1.0, 1.0, 1.0),
),
attributes=None,
),
],
),
EMPTY_HEADER +
'\x02\x00\x00\x00' # two facets
# first facet
'\x00\x00\x80\x3f' # normal x = 1.0
'\x00\x00\x00\x40' # normal y = 2.0
'\x00\x00\x40\x40' # normal z = 3.0
'\x00\x00\x80\x40' # vertex x = 4.0
'\x00\x00\xa0\x40' # vertex y = 5.0
'\x00\x00\xc0\x40' # vertex z = 6.0
'\x00\x00\xe0\x40' # vertex x = 7.0
'\x00\x00\x00\x41' # vertex y = 8.0
'\x00\x00\x10\x41' # vertex z = 9.0
'\x00\x00\x20\x41' # vertex x = 10.0
'\x00\x00\x30\x41' # vertex y = 11.0
'\x00\x00\x40\x41' # vertex z = 12.0
'\x00\x00' # no attribute bytes
# second facet
'\x00\x00\x80\x3f' # normal x = 1.0
'\x00\x00\x80\x3f' # normal y = 1.0
'\x00\x00\x80\x3f' # normal z = 1.0
'\x00\x00\x80\x3f' # vertex x = 1.0
'\x00\x00\x80\x3f' # vertex y = 1.0
'\x00\x00\x80\x3f' # vertex z = 1.0
'\x00\x00\x80\x3f' # vertex x = 1.0
'\x00\x00\x80\x3f' # vertex y = 1.0
'\x00\x00\x80\x3f' # vertex z = 1.0
'\x00\x00\x80\x3f' # vertex x = 1.0
'\x00\x00\x80\x3f' # vertex y = 1.0
'\x00\x00\x80\x3f' # vertex z = 1.0
'\x00\x00' # no attribute bytes
)