Python-Course

Math << Previous Next >> CNC

STL

stl.pdf

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
        )


Math << Previous Next >> CNC