From 13f4f5164ba340271eb17680a337a2f8526de5df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Tue, 14 Nov 2017 14:27:50 +0100 Subject: [PATCH] optionally group geometries in engine to beautify openscad export --- src/c3nav/mapdata/render/engines/base.py | 7 ++- src/c3nav/mapdata/render/engines/base3d.py | 17 ++++++-- src/c3nav/mapdata/render/engines/opengl.py | 2 + src/c3nav/mapdata/render/engines/openscad.py | 45 +++++++++++++------- src/c3nav/mapdata/render/engines/stl.py | 4 +- src/c3nav/mapdata/render/engines/svg.py | 2 +- src/c3nav/mapdata/render/renderer.py | 22 ++++++---- 7 files changed, 67 insertions(+), 32 deletions(-) diff --git a/src/c3nav/mapdata/render/engines/base.py b/src/c3nav/mapdata/render/engines/base.py index f070c8ea..03d55ada 100644 --- a/src/c3nav/mapdata/render/engines/base.py +++ b/src/c3nav/mapdata/render/engines/base.py @@ -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], diff --git a/src/c3nav/mapdata/render/engines/base3d.py b/src/c3nav/mapdata/render/engines/base3d.py index 631d146a..0afb0fc1 100644 --- a/src/c3nav/mapdata/render/engines/base3d.py +++ b/src/c3nav/mapdata/render/engines/base3d.py @@ -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): diff --git a/src/c3nav/mapdata/render/engines/opengl.py b/src/c3nav/mapdata/render/engines/opengl.py index 5093d5cc..2f4efcb1 100644 --- a/src/c3nav/mapdata/render/engines/opengl.py +++ b/src/c3nav/mapdata/render/engines/opengl.py @@ -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)) diff --git a/src/c3nav/mapdata/render/engines/openscad.py b/src/c3nav/mapdata/render/engines/openscad.py index 980d1b5a..b89b5ae1 100644 --- a/src/c3nav/mapdata/render/engines/openscad.py +++ b/src/c3nav/mapdata/render/engines/openscad.py @@ -8,21 +8,34 @@ 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: - vertices = tuple(set(tuple(vertex) for vertex in facets.reshape((-1, 3)))) - lookup = {vertex: i for i, vertex in enumerate(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' + - b' points = [\n' + - b'\n'.join((b' [%.3f, %.3f, %.3f],' % tuple(vertex)) for vertex in vertices) + b'\n' + - b' ],\n' + - b' faces = [\n' + - b'\n'.join((b' [%d, %d, %d],' % (lookup[tuple(a)], lookup[tuple(b)], lookup[tuple(c)])) - for a, b, c in facets) + b'\n' + - b' ],\n' + - b' convexity = 10\n' + - b');\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' + + b' faces = [\n' + + b'\n'.join((b' [%d, %d, %d],' % (lookup[tuple(a)], lookup[tuple(b)], lookup[tuple(c)])) + for a, b, c in facets) + b'\n' + + b' ],\n' + + b' convexity = 10\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 diff --git a/src/c3nav/mapdata/render/engines/stl.py b/src/c3nav/mapdata/render/engines/stl.py index c687b61f..b777c571 100644 --- a/src/c3nav/mapdata/render/engines/stl.py +++ b/src/c3nav/mapdata/render/engines/stl.py @@ -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' + diff --git a/src/c3nav/mapdata/render/engines/svg.py b/src/c3nav/mapdata/render/engines/svg.py index 4dc0557f..56d739fc 100644 --- a/src/c3nav/mapdata/render/engines/svg.py +++ b/src/c3nav/mapdata/render/engines/svg.py @@ -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: diff --git a/src/c3nav/mapdata/render/renderer.py b/src/c3nav/mapdata/render/renderer.py index ec4700f3..7e4bb821 100644 --- a/src/c3nav/mapdata/render/renderer.py +++ b/src/c3nav/mapdata/render/renderer.py @@ -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