optionally group geometries in engine to beautify openscad export
This commit is contained in:
parent
1c91c9a6db
commit
13f4f5164b
7 changed files with 67 additions and 32 deletions
|
@ -65,8 +65,11 @@ class RenderEngine(ABC):
|
||||||
return (*(i/255 for i in color[:3]), color[3] if alpha is None else alpha)
|
return (*(i/255 for i in color[:3]), color[3] if alpha is None else alpha)
|
||||||
raise ValueError('invalid color string!')
|
raise ValueError('invalid color string!')
|
||||||
|
|
||||||
|
def add_group(self, group):
|
||||||
|
pass
|
||||||
|
|
||||||
def add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
|
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
|
# draw a shapely geometry with a given style
|
||||||
# altitude is the absolute altitude of the upper bound of the element
|
# altitude is the absolute altitude of the upper bound of the element
|
||||||
# height is the height of the element
|
# height is the height of the element
|
||||||
|
@ -77,7 +80,7 @@ class RenderEngine(ABC):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._add_geometry(geometry=geometry, fill=fill, stroke=stroke,
|
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
|
@abstractmethod
|
||||||
def _add_geometry(self, geometry, fill: Optional[FillAttribs], stroke: Optional[StrokeAttribs],
|
def _add_geometry(self, geometry, fill: Optional[FillAttribs], stroke: Optional[StrokeAttribs],
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from collections import OrderedDict
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
@ -14,7 +15,9 @@ class Base3DEngine(RenderEngine):
|
||||||
def __init__(self, *args, center=True, **kwargs):
|
def __init__(self, *args, center=True, **kwargs):
|
||||||
super().__init__(*args, **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_scale = np.array((self.scale, self.scale, self.scale))
|
||||||
self.np_offset = np.array((-self.minx * self.scale, -self.miny * self.scale, 0))
|
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,
|
(self.miny - self.maxy) * self.scale / 2,
|
||||||
0))
|
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:
|
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
|
@staticmethod
|
||||||
def _append_to_vertices(vertices, append=None):
|
def _append_to_vertices(vertices, append=None):
|
||||||
|
|
|
@ -156,6 +156,8 @@ class OpenGLEngine(Base3DEngine):
|
||||||
self.gl_scale = (scale_x, -scale_y, scale_z)
|
self.gl_scale = (scale_x, -scale_y, scale_z)
|
||||||
self.gl_offset = (-self.minx * scale_x - 1, self.maxy * scale_y - 1, 0)
|
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):
|
def _add_geometry(self, geometry, fill: Optional[FillAttribs], stroke: Optional[StrokeAttribs], **kwargs):
|
||||||
if fill is not None:
|
if fill is not None:
|
||||||
self.vertices.append(self._place_geometry(geometry, self.color_to_rgb(fill.color), offset=False))
|
self.vertices.append(self._place_geometry(geometry, self.color_to_rgb(fill.color), offset=False))
|
||||||
|
|
|
@ -8,21 +8,34 @@ from c3nav.mapdata.render.engines.base3d import Base3DEngine
|
||||||
class OpenSCADEngine(Base3DEngine):
|
class OpenSCADEngine(Base3DEngine):
|
||||||
filetype = 'scad'
|
filetype = 'scad'
|
||||||
|
|
||||||
def render(self) -> bytes:
|
def _create_polyhedron(self, name, vertices):
|
||||||
facets = np.vstack(self.vertices)
|
facets = np.vstack(vertices)
|
||||||
result = b''
|
vertices = tuple(set(tuple(vertex) for vertex in facets.reshape((-1, 3))))
|
||||||
for facets in self.vertices:
|
lookup = {vertex: i for i, vertex in enumerate(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' points = [\n' +
|
b' polyhedron(\n' +
|
||||||
b'\n'.join((b' [%.3f, %.3f, %.3f],' % tuple(vertex)) for vertex in vertices) + b'\n' +
|
b' points = [\n' +
|
||||||
b' ],\n' +
|
b'\n'.join((b' [%.3f, %.3f, %.3f],' % tuple(vertex)) for vertex in vertices) + b'\n' +
|
||||||
b' faces = [\n' +
|
b' ],\n' +
|
||||||
b'\n'.join((b' [%d, %d, %d],' % (lookup[tuple(a)], lookup[tuple(b)], lookup[tuple(c)]))
|
b' faces = [\n' +
|
||||||
for a, b, c in facets) + b'\n' +
|
b'\n'.join((b' [%d, %d, %d],' % (lookup[tuple(a)], lookup[tuple(b)], lookup[tuple(c)]))
|
||||||
b' ],\n' +
|
for a, b, c in facets) + b'\n' +
|
||||||
b' convexity = 10\n' +
|
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
|
return result
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from c3nav.mapdata.render.engines import register_engine
|
from c3nav.mapdata.render.engines import register_engine
|
||||||
|
@ -20,7 +22,7 @@ class STLEngine(Base3DEngine):
|
||||||
return self.facet_template % tuple(facet.flatten())
|
return self.facet_template % tuple(facet.flatten())
|
||||||
|
|
||||||
def render(self) -> bytes:
|
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 = np.hstack((np.cross(facets[:, 1]-facets[:, 0], facets[:, 2]-facets[:, 1]).reshape((-1, 1, 3)),
|
||||||
facets))
|
facets))
|
||||||
return (b'solid c3nav_export\n' +
|
return (b'solid c3nav_export\n' +
|
||||||
|
|
|
@ -215,7 +215,7 @@ class SVGEngine(RenderEngine):
|
||||||
self.altitudes[new_altitude] = new_geometry
|
self.altitudes[new_altitude] = new_geometry
|
||||||
|
|
||||||
def _add_geometry(self, geometry, fill: Optional[FillAttribs], stroke: Optional[StrokeAttribs],
|
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)
|
geometry = self.buffered_bbox.intersection(geometry.geom)
|
||||||
|
|
||||||
if geometry.is_empty:
|
if geometry.is_empty:
|
||||||
|
|
|
@ -90,6 +90,8 @@ class MapRenderer:
|
||||||
if not bbox.intersects(geoms.affected_area):
|
if not bbox.intersects(geoms.affected_area):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
engine.add_group('level_%s' % geoms.pk)
|
||||||
|
|
||||||
# hide indoor and outdoor rooms if their access restriction was not unlocked
|
# 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()
|
add_walls = hybrid_union(tuple(area for access_restriction, area in geoms.restricted_spaces_indoors.items()
|
||||||
if access_restriction not in unlocked_access_restrictions))
|
if access_restriction not in unlocked_access_restrictions))
|
||||||
|
@ -99,17 +101,17 @@ class MapRenderer:
|
||||||
).union(add_walls)
|
).union(add_walls)
|
||||||
|
|
||||||
if not self.full_levels and engine.is_3d:
|
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:
|
if min_altitude < geoms.min_altitude:
|
||||||
engine.add_geometry(geoms.walls_bottom.fit(scale=geoms.min_altitude-min_altitude,
|
engine.add_geometry(geoms.walls_bottom.fit(scale=geoms.min_altitude-min_altitude,
|
||||||
offset=min_altitude),
|
offset=min_altitude),
|
||||||
fill=FillAttribs('#aaaaaa'))
|
fill=FillAttribs('#aaaaaa'), category='walls')
|
||||||
for altitudearea in geoms.altitudeareas:
|
for altitudearea in geoms.altitudeareas:
|
||||||
bottom = altitudearea.altitude - int(0.7 * 1000)
|
bottom = altitudearea.altitude - int(0.7 * 1000)
|
||||||
scale = (bottom - min_altitude) / int(0.7 * 1000)
|
scale = (bottom - min_altitude) / int(0.7 * 1000)
|
||||||
offset = min_altitude - bottom * scale
|
offset = min_altitude - bottom * scale
|
||||||
engine.add_geometry(altitudearea.geometry.fit(scale=scale, offset=offset).filter(top=False),
|
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
|
# render altitude areas in default ground color and add ground colors to each one afterwards
|
||||||
# shadows are directly calculated and added by the engine
|
# shadows are directly calculated and added by the engine
|
||||||
|
@ -117,14 +119,15 @@ class MapRenderer:
|
||||||
geometry = altitudearea.geometry.difference(crop_areas)
|
geometry = altitudearea.geometry.difference(crop_areas)
|
||||||
if not self.full_levels and engine.is_3d:
|
if not self.full_levels and engine.is_3d:
|
||||||
geometry = geometry.filter(bottom=False)
|
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():
|
for color, areas in altitudearea.colors.items():
|
||||||
# only select ground colors if their access restriction is unlocked
|
# only select ground colors if their access restriction is unlocked
|
||||||
areas = tuple(area for access_restriction, area in areas.items()
|
areas = tuple(area for access_restriction, area in areas.items()
|
||||||
if access_restriction in unlocked_access_restrictions)
|
if access_restriction in unlocked_access_restrictions)
|
||||||
if areas:
|
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,
|
# add walls, stroke_px makes sure that all walls are at least 1px thick on all zoom levels,
|
||||||
walls = None
|
walls = None
|
||||||
|
@ -133,16 +136,17 @@ class MapRenderer:
|
||||||
|
|
||||||
if walls is not None:
|
if walls is not None:
|
||||||
engine.add_geometry(walls.filter(bottom=(self.full_levels or not engine.is_3d)),
|
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:
|
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:
|
if not geoms.doors.is_empty:
|
||||||
engine.add_geometry(geoms.doors.difference(add_walls), fill=FillAttribs('#ffffff'),
|
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:
|
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
|
return engine
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue