Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added support for .stl export #66

Merged
merged 2 commits into from
Oct 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 55 additions & 7 deletions cjio/cityjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -1512,9 +1512,7 @@ def upgrade_version(self, newversion):


def triangulate_face(self, face, vnp):
#-- if already a triangle then return it
if ( (len(face) == 1) and (len(face[0]) == 3) ):
return (face, True)

sf = np.array([], dtype=np.int32)
for ring in face:
sf = np.hstack( (sf, np.array(ring)) )
Expand All @@ -1530,9 +1528,14 @@ def triangulate_face(self, face, vnp):

# 1. normal with Newell's method
n, b = geom_help.get_normal_newell(sfv)

#-- if already a triangle then return it
if ( (len(face) == 1) and (len(face[0]) == 3) ):
return (face, True, n)
if b == False:
return (n, False)
return (n, False, n)
# print ("Newell:", n)

# 2. project to the plane to get xy
sfv2d = np.zeros( (sfv.shape[0], 2))
# print (sfv2d)
Expand All @@ -1549,7 +1552,7 @@ def triangulate_face(self, face, vnp):
result[i] = sf[each]

# print (result.reshape(-1, 3))
return (result.reshape(-1, 3), True)
return (result.reshape(-1, 3), True, n)


def export2b3dm(self):
Expand Down Expand Up @@ -1591,19 +1594,64 @@ def export2obj(self):
out.write('o ' + str(theid) + '\n')
if ( (geom['type'] == 'MultiSurface') or (geom['type'] == 'CompositeSurface') ):
for face in geom['boundaries']:
re, b = self.triangulate_face(face, vnp)
re, b, n = self.triangulate_face(face, vnp)
if b == True:
for t in re:
out.write("f %d %d %d\n" % (t[0] + 1, t[1] + 1, t[2] + 1))
elif (geom['type'] == 'Solid'):
for shell in geom['boundaries']:
for i, face in enumerate(shell):
re, b = self.triangulate_face(face, vnp)
re, b, n = self.triangulate_face(face, vnp)
if b == True:
for t in re:
out.write("f %d %d %d\n" % (t[0] + 1, t[1] + 1, t[2] + 1))
return out

def export2stl(self):
#TODO: refectoring, duplicated code from 2obj()
out = StringIO()
out.write("solid\n")

#-- translate to minx,miny
vnp = np.array(self.j["vertices"])
minx = 9e9
miny = 9e9
for each in vnp:
if each[0] < minx:
minx = each[0]
if each[1] < miny:
miny = each[1]
for each in vnp:
each[0] -= minx
each[1] -= miny
# print ("min", minx, miny)
# print(vnp)
#-- start with the CO
for theid in self.j['CityObjects']:
for geom in self.j['CityObjects'][theid]['geometry']:
if ( (geom['type'] == 'MultiSurface') or (geom['type'] == 'CompositeSurface') ):
for face in geom['boundaries']:
re, b, n = self.triangulate_face(face, vnp)
if b == True:
for t in re:
out.write("facet normal %f %f %f\nouter loop\n" % (n[0], n[1], n[2]))
out.write("vertex %s %s %s\n" % (str(self.j["vertices"][t[0]][0]), str(self.j["vertices"][t[0]][1]), str(self.j["vertices"][t[0]][2])))
out.write("vertex %s %s %s\n" % (str(self.j["vertices"][t[1]][0]), str(self.j["vertices"][t[1]][1]), str(self.j["vertices"][t[1]][2])))
out.write("vertex %s %s %s\n" % (str(self.j["vertices"][t[2]][0]), str(self.j["vertices"][t[2]][1]), str(self.j["vertices"][t[2]][2])))
out.write("endloop\nendfacet\n")
elif (geom['type'] == 'Solid'):
for shell in geom['boundaries']:
for i, face in enumerate(shell):
re, b, n = self.triangulate_face(face, vnp)
if b == True:
for t in re:
out.write("facet normal %f %f %f\nouter loop\n" % (n[0], n[1], n[2]))
out.write("vertex %s %s %s\n" % (str(self.j["vertices"][t[0]][0]), str(self.j["vertices"][t[0]][1]), str(self.j["vertices"][t[0]][2])))
out.write("vertex %s %s %s\n" % (str(self.j["vertices"][t[1]][0]), str(self.j["vertices"][t[1]][1]), str(self.j["vertices"][t[1]][2])))
out.write("vertex %s %s %s\n" % (str(self.j["vertices"][t[2]][0]), str(self.j["vertices"][t[2]][1]), str(self.j["vertices"][t[2]][2])))
out.write("endloop\nendfacet\n")
out.write("endsolid")
return out

def reproject(self, epsg):
if not MODULE_PYPROJ_AVAILABLE:
Expand Down
17 changes: 12 additions & 5 deletions cjio/cjio.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def processor(cm):
@cli.command('export')
@click.argument('filename')
@click.option('--format',
type=click.Choice(['obj', 'glb', 'b3dm']),
type=click.Choice(['obj', 'stl', 'glb', 'b3dm']),
help="Export format")
def export_cmd(filename, format):
"""Export the CityJSON to another format.
Expand All @@ -137,10 +137,17 @@ def exporter(cm):
if format.lower() == 'obj':
utils.print_cmd_status("Exporting CityJSON to OBJ (%s)" % (output['path']))
try:
fo = click.open_file(output['path'], mode='w')
re = cm.export2obj()
# TODO B: why don't you close the file @hugoledoux?
fo.write(re.getvalue())
with click.open_file(output['path'], mode='w') as fo:
re = cm.export2obj()
fo.write(re.getvalue())
except IOError as e:
raise click.ClickException('Invalid output file: "%s".\n%s' % (output['path'], e))
elif format.lower() == 'stl':
utils.print_cmd_status("Exporting CityJSON to STL (%s)" % (output['path']))
try:
with click.open_file(output['path'], mode='w') as fo:
re = cm.export2stl()
fo.write(re.getvalue())
except IOError as e:
raise click.ClickException('Invalid output file: "%s".\n%s' % (output['path'], e))
elif format.lower() == 'glb':
Expand Down
17 changes: 17 additions & 0 deletions tests/test_cityjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

"""
import pytest
import os.path
from click.testing import CliRunner
import copy
from cjio import cityjson, models
from cjio import cjio
from math import isclose


Expand Down Expand Up @@ -145,3 +148,17 @@ def test_reproject(self, delft_1b):
cm.reproject(4937) #-- z values should stay the same
assert isclose(cm.j["vertices"][0][0], 4.36772776578513, abs_tol=0.00001)
assert (cm.j["metadata"]["geographicalExtent"][5] - cm.j["metadata"]["geographicalExtent"][2]) == 6.1

def test_convert_to_stl(self, delft):
cm = copy.deepcopy(delft)
obj = cm.export2stl()

def test_export_stl_cmd(self, data_dir, data_output_dir):
"""Debugging"""
p = os.path.join(data_dir, 'delft.json')
runner = CliRunner()
result = runner.invoke(cjio.cli,
args=[p,
'export',
'--format', 'stl',
data_output_dir])