optionally group geometries in engine to beautify openscad export

This commit is contained in:
Laura Klünder 2017-11-14 14:27:50 +01:00
parent 1c91c9a6db
commit 13f4f5164b
7 changed files with 67 additions and 32 deletions

View file

@ -65,8 +65,11 @@ class RenderEngine(ABC):
return (*(i/255 for i in color[:3]), color[3] if alpha is None else alpha)
raise ValueError('invalid color string!')
def add_group(self, group):
pass
def add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
altitude=None, height=None, shape_cache_key=None):
altitude=None, height=None, shape_cache_key=None, category=None):
# draw a shapely geometry with a given style
# altitude is the absolute altitude of the upper bound of the element
# height is the height of the element
@ -77,7 +80,7 @@ class RenderEngine(ABC):
return
self._add_geometry(geometry=geometry, fill=fill, stroke=stroke,
altitude=altitude, height=height, shape_cache_key=shape_cache_key)
altitude=altitude, height=height, shape_cache_key=shape_cache_key, category=category)
@abstractmethod
def _add_geometry(self, geometry, fill: Optional[FillAttribs], stroke: Optional[StrokeAttribs],

View file

@ -1,3 +1,4 @@
from collections import OrderedDict
from itertools import chain
from typing import Optional
@ -14,7 +15,9 @@ class Base3DEngine(RenderEngine):
def __init__(self, *args, center=True, **kwargs):
super().__init__(*args, **kwargs)
self.vertices = []
self._current_group = None
self.groups = OrderedDict()
self.vertices = OrderedDict()
self.np_scale = np.array((self.scale, self.scale, self.scale))
self.np_offset = np.array((-self.minx * self.scale, -self.miny * self.scale, 0))
@ -23,9 +26,17 @@ class Base3DEngine(RenderEngine):
(self.miny - self.maxy) * self.scale / 2,
0))
def _add_geometry(self, geometry, fill: Optional[FillAttribs], stroke: Optional[StrokeAttribs], **kwargs):
def add_group(self, group):
self._current_group = group
self.groups.setdefault(group, [])
def _add_geometry(self, geometry, fill: Optional[FillAttribs], stroke: Optional[StrokeAttribs], category=None,
**kwargs):
if fill is not None:
self.vertices.append(self._place_geometry(geometry))
key = '%s_%s' % (self._current_group, category)
if key not in self.vertices:
self.groups[self._current_group].append(key)
self.vertices.setdefault(key, []).append(self._place_geometry(geometry))
@staticmethod
def _append_to_vertices(vertices, append=None):

View file

@ -156,6 +156,8 @@ class OpenGLEngine(Base3DEngine):
self.gl_scale = (scale_x, -scale_y, scale_z)
self.gl_offset = (-self.minx * scale_x - 1, self.maxy * scale_y - 1, 0)
self.vertices = []
def _add_geometry(self, geometry, fill: Optional[FillAttribs], stroke: Optional[StrokeAttribs], **kwargs):
if fill is not None:
self.vertices.append(self._place_geometry(geometry, self.color_to_rgb(fill.color), offset=False))

View file

@ -8,14 +8,13 @@ from c3nav.mapdata.render.engines.base3d import Base3DEngine
class OpenSCADEngine(Base3DEngine):
filetype = 'scad'
def render(self) -> bytes:
facets = np.vstack(self.vertices)
result = b''
for facets in self.vertices:
def _create_polyhedron(self, name, vertices):
facets = np.vstack(vertices)
vertices = tuple(set(tuple(vertex) for vertex in facets.reshape((-1, 3))))
lookup = {vertex: i for i, vertex in enumerate(vertices)}
result += (b'polyhedron(\n' +
return (b'module ' + name.encode() + b'() {\n' +
b' polyhedron(\n' +
b' points = [\n' +
b'\n'.join((b' [%.3f, %.3f, %.3f],' % tuple(vertex)) for vertex in vertices) + b'\n' +
b' ],\n' +
@ -24,5 +23,19 @@ class OpenSCADEngine(Base3DEngine):
for a, b, c in facets) + b'\n' +
b' ],\n' +
b' convexity = 10\n' +
b');\n')
b' );\n'
b'}\n')
def render(self) -> bytes:
result = (b'scale([100, 100, 100]) c3nav_export();\n\n' +
b'module c3nav_export() {\n' +
b'\n'.join((b' %s();' % group.encode()) for group in self.groups.keys()) + b'\n' +
b'}\n\n')
for group, subgroups in self.groups.items():
result += (b'module ' + group.encode() + b'() {\n' +
b'\n'.join((b' %s();' % subgroup.encode()) for subgroup in subgroups) + b'\n' +
b'}\n')
result += b'\n'
for group, vertices in self.vertices.items():
result += self._create_polyhedron(group, vertices)
return result

View file

@ -1,3 +1,5 @@
from itertools import chain
import numpy as np
from c3nav.mapdata.render.engines import register_engine
@ -20,7 +22,7 @@ class STLEngine(Base3DEngine):
return self.facet_template % tuple(facet.flatten())
def render(self) -> bytes:
facets = np.vstack(self.vertices)
facets = np.vstack(chain(*self.vertices.values()))
facets = np.hstack((np.cross(facets[:, 1]-facets[:, 0], facets[:, 2]-facets[:, 1]).reshape((-1, 1, 3)),
facets))
return (b'solid c3nav_export\n' +

View file

@ -215,7 +215,7 @@ class SVGEngine(RenderEngine):
self.altitudes[new_altitude] = new_geometry
def _add_geometry(self, geometry, fill: Optional[FillAttribs], stroke: Optional[StrokeAttribs],
altitude=None, height=None, shape_cache_key=None):
altitude=None, height=None, shape_cache_key=None, category=None):
geometry = self.buffered_bbox.intersection(geometry.geom)
if geometry.is_empty:

View file

@ -90,6 +90,8 @@ class MapRenderer:
if not bbox.intersects(geoms.affected_area):
continue
engine.add_group('level_%s' % geoms.pk)
# hide indoor and outdoor rooms if their access restriction was not unlocked
add_walls = hybrid_union(tuple(area for access_restriction, area in geoms.restricted_spaces_indoors.items()
if access_restriction not in unlocked_access_restrictions))
@ -99,17 +101,17 @@ class MapRenderer:
).union(add_walls)
if not self.full_levels and engine.is_3d:
engine.add_geometry(geoms.walls_base, fill=FillAttribs('#aaaaaa'))
engine.add_geometry(geoms.walls_base, fill=FillAttribs('#aaaaaa'), category='walls')
if min_altitude < geoms.min_altitude:
engine.add_geometry(geoms.walls_bottom.fit(scale=geoms.min_altitude-min_altitude,
offset=min_altitude),
fill=FillAttribs('#aaaaaa'))
fill=FillAttribs('#aaaaaa'), category='walls')
for altitudearea in geoms.altitudeareas:
bottom = altitudearea.altitude - int(0.7 * 1000)
scale = (bottom - min_altitude) / int(0.7 * 1000)
offset = min_altitude - bottom * scale
engine.add_geometry(altitudearea.geometry.fit(scale=scale, offset=offset).filter(top=False),
fill=FillAttribs('#aaaaaa'))
fill=FillAttribs('#aaaaaa'), category='ground')
# render altitude areas in default ground color and add ground colors to each one afterwards
# shadows are directly calculated and added by the engine
@ -117,14 +119,15 @@ class MapRenderer:
geometry = altitudearea.geometry.difference(crop_areas)
if not self.full_levels and engine.is_3d:
geometry = geometry.filter(bottom=False)
engine.add_geometry(geometry, altitude=altitudearea.altitude, fill=FillAttribs('#eeeeee'))
engine.add_geometry(geometry, altitude=altitudearea.altitude, fill=FillAttribs('#eeeeee'),
category='ground')
for color, areas in altitudearea.colors.items():
# only select ground colors if their access restriction is unlocked
areas = tuple(area for access_restriction, area in areas.items()
if access_restriction in unlocked_access_restrictions)
if areas:
engine.add_geometry(hybrid_union(areas), fill=FillAttribs(color))
engine.add_geometry(hybrid_union(areas), fill=FillAttribs(color), category='ground')
# add walls, stroke_px makes sure that all walls are at least 1px thick on all zoom levels,
walls = None
@ -133,16 +136,17 @@ class MapRenderer:
if walls is not None:
engine.add_geometry(walls.filter(bottom=(self.full_levels or not engine.is_3d)),
height=geoms.default_height, fill=FillAttribs('#aaaaaa'))
height=geoms.default_height, fill=FillAttribs('#aaaaaa'), category='walls')
if geoms.walls_extended and self.full_levels and engine.is_3d:
engine.add_geometry(geoms.walls_extended, height=geoms.default_height, fill=FillAttribs('#aaaaaa'))
engine.add_geometry(geoms.walls_extended, height=geoms.default_height, fill=FillAttribs('#aaaaaa'),
category='walls')
if not geoms.doors.is_empty:
engine.add_geometry(geoms.doors.difference(add_walls), fill=FillAttribs('#ffffff'),
stroke=StrokeAttribs('#ffffff', 0.05, min_px=0.2))
stroke=StrokeAttribs('#ffffff', 0.05, min_px=0.2), category='doors')
if walls is not None:
engine.add_geometry(walls, stroke=StrokeAttribs('#666666', 0.05, min_px=0.2))
engine.add_geometry(walls, stroke=StrokeAttribs('#666666', 0.05, min_px=0.2), category='walls')
return engine