Skip to content
Snippets Groups Projects
Commit e75551f7 authored by Bastien Montagne's avatar Bastien Montagne
Browse files

Fix T44536: Add (limited!) normal import for STL.

Limited, because STL only stores face normals, so we can only fake this by setting
all clnors of a same face to that face normal... Guess use case are rather limited,
but does not hurt to have it either.
parent b5afec73
Branches
Tags
No related merge requests found
...@@ -112,6 +112,12 @@ class ImportSTL(Operator, ImportHelper, IOSTLOrientationHelper): ...@@ -112,6 +112,12 @@ class ImportSTL(Operator, ImportHelper, IOSTLOrientationHelper):
default=True, default=True,
) )
use_facet_normal = BoolProperty(
name="Facet Normals",
description="Use (import) facet normals (note that this will still give flat shading)",
default=False,
)
def execute(self, context): def execute(self, context):
from . import stl_utils from . import stl_utils
from . import blender_utils from . import blender_utils
...@@ -142,8 +148,9 @@ class ImportSTL(Operator, ImportHelper, IOSTLOrientationHelper): ...@@ -142,8 +148,9 @@ class ImportSTL(Operator, ImportHelper, IOSTLOrientationHelper):
for path in paths: for path in paths:
objName = bpy.path.display_name(os.path.basename(path)) objName = bpy.path.display_name(os.path.basename(path))
tris, pts = stl_utils.read_stl(path) tris, tri_nors, pts = stl_utils.read_stl(path)
blender_utils.create_and_link_mesh(objName, tris, pts, global_matrix) tri_nors = tri_nors if self.use_facet_normal else None
blender_utils.create_and_link_mesh(objName, tris, tri_nors, pts, global_matrix)
return {'FINISHED'} return {'FINISHED'}
......
...@@ -19,9 +19,11 @@ ...@@ -19,9 +19,11 @@
# <pep8 compliant> # <pep8 compliant>
import bpy import bpy
import array
from itertools import chain
def create_and_link_mesh(name, faces, points, global_matrix): def create_and_link_mesh(name, faces, face_nors, points, global_matrix):
""" """
Create a blender mesh and object called name from a list of Create a blender mesh and object called name from a list of
*points* and *faces* and link it in the current scene. *points* and *faces* and link it in the current scene.
...@@ -29,10 +31,30 @@ def create_and_link_mesh(name, faces, points, global_matrix): ...@@ -29,10 +31,30 @@ def create_and_link_mesh(name, faces, points, global_matrix):
mesh = bpy.data.meshes.new(name) mesh = bpy.data.meshes.new(name)
mesh.from_pydata(points, [], faces) mesh.from_pydata(points, [], faces)
if face_nors:
# Note: we store 'temp' normals in loops, since validate() may alter final mesh,
# we can only set custom lnors *after* calling it.
mesh.create_normals_split()
lnors = tuple(chain(*chain(*zip(face_nors, face_nors, face_nors))))
mesh.loops.foreach_set("normal", lnors)
mesh.transform(global_matrix) mesh.transform(global_matrix)
# update mesh to allow proper display # update mesh to allow proper display
mesh.validate() mesh.validate(clean_customdata=False) # *Very* important to not remove lnors here!
if face_nors:
clnors = array.array('f', [0.0] * (len(mesh.loops) * 3))
mesh.loops.foreach_get("normal", clnors)
mesh.polygons.foreach_set("use_smooth", [True] * len(mesh.polygons))
mesh.normals_split_custom_set(tuple(zip(*(iter(clnors),) * 3)))
mesh.use_auto_smooth = True
mesh.show_edge_sharp = True
mesh.free_normals_split()
mesh.update() mesh.update()
scene = bpy.context.scene scene = bpy.context.scene
......
...@@ -62,6 +62,15 @@ class ListDict(dict): ...@@ -62,6 +62,15 @@ class ListDict(dict):
return value 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_HEADER = 80
BINARY_STRIDE = 12 * 4 + 2 BINARY_STRIDE = 12 * 4 + 2
...@@ -96,19 +105,6 @@ def _is_ascii_file(data): ...@@ -96,19 +105,6 @@ def _is_ascii_file(data):
def _binary_read(data): def _binary_read(data):
# 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)
# OFFSET is to skip normal bytes
# STRIDE between each triangle (first normal + coordinates + garbage)
OFFSET = 12
# Skip header... # Skip header...
data.seek(BINARY_HEADER) data.seek(BINARY_HEADER)
size = struct.unpack('<I', data.read(4))[0] size = struct.unpack('<I', data.read(4))[0]
...@@ -129,15 +125,15 @@ def _binary_read(data): ...@@ -129,15 +125,15 @@ def _binary_read(data):
chunks = [CHUNK_LEN] * (size // CHUNK_LEN) chunks = [CHUNK_LEN] * (size // CHUNK_LEN)
chunks.append(size % CHUNK_LEN) chunks.append(size % CHUNK_LEN)
unpack = struct.Struct('<9f').unpack_from unpack = struct.Struct('<12f').unpack_from
for chunk_len in chunks: for chunk_len in chunks:
if chunk_len == 0: if chunk_len == 0:
continue continue
buf = data.read(BINARY_STRIDE * chunk_len) buf = data.read(BINARY_STRIDE * chunk_len)
for i in range(chunk_len): for i in range(chunk_len):
# read the points coordinates of each triangle # read the normal and points coordinates of each triangle
pt = unpack(buf, OFFSET + BINARY_STRIDE * i) pt = unpack(buf, BINARY_STRIDE * i)
yield pt[:3], pt[3:6], pt[6:] yield pt[:3], (pt[3:6], pt[6:9], pt[9:])
def _ascii_read(data): def _ascii_read(data):
...@@ -156,11 +152,15 @@ def _ascii_read(data): ...@@ -156,11 +152,15 @@ def _ascii_read(data):
# strip header # strip header
data.readline() data.readline()
curr_nor = None
for l in data: for l in data:
# if we encounter a vertex, read next 2
l = l.lstrip() l = l.lstrip()
if l.startswith(b'facet'):
curr_nor = tuple(map(float, l_item.split()[2:]))
# if we encounter a vertex, read next 2
if l.startswith(b'vertex'): if l.startswith(b'vertex'):
yield [tuple(map(float, l_item.split()[1:])) for l_item in (l, data.readline(), data.readline())] yield curr_nor, [tuple(map(float, l_item.split()[1:])) for l_item in (l, data.readline(), data.readline())]
def _binary_write(filepath, faces): def _binary_write(filepath, faces):
...@@ -232,19 +232,22 @@ def read_stl(filepath): ...@@ -232,19 +232,22 @@ def read_stl(filepath):
Please note that this process can take lot of time if the file is 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). huge (~1m30 for a 1 Go stl file on an quad core i7).
- returns a tuple(triangles, points). - returns a tuple(triangles, triangles' normals, points).
triangles triangles
A list of triangles, each triangle as a tuple of 3 index of A list of triangles, each triangle as a tuple of 3 index of
point in *points*. point in *points*.
triangles' normals
A list of vectors3 (tuples, xyz).
points points
An indexed list of points, each point is a tuple of 3 float An indexed list of points, each point is a tuple of 3 float
(xyz). (xyz).
Example of use: Example of use:
>>> tris, pts = read_stl(filepath, lambda x:) >>> tris, tri_nors, pts = read_stl(filepath)
>>> pts = list(pts) >>> pts = list(pts)
>>> >>>
>>> # print the coordinate of the triangle n >>> # print the coordinate of the triangle n
...@@ -253,22 +256,23 @@ def read_stl(filepath): ...@@ -253,22 +256,23 @@ def read_stl(filepath):
import time import time
start_time = time.process_time() start_time = time.process_time()
tris, pts = [], ListDict() tris, tri_nors, pts = [], [], ListDict()
with open(filepath, 'rb') as data: with open(filepath, 'rb') as data:
# check for ascii or binary # check for ascii or binary
gen = _ascii_read if _is_ascii_file(data) else _binary_read gen = _ascii_read if _is_ascii_file(data) else _binary_read
for pt in gen(data): for nor, pt in gen(data):
# Add the triangle and the point. # Add the triangle and the point.
# If the point is allready in the list of points, the # If the point is allready in the list of points, the
# index returned by pts.add() will be the one from the # index returned by pts.add() will be the one from the
# first equal point inserted. # first equal point inserted.
tris.append([pts.add(p) for p in pt]) tris.append([pts.add(p) for p in pt])
tri_nors.append(nor)
print('Import finished in %.4f sec.' % (time.process_time() - start_time)) print('Import finished in %.4f sec.' % (time.process_time() - start_time))
return tris, pts.list return tris, tri_nors, pts.list
if __name__ == '__main__': if __name__ == '__main__':
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment