1# Purpose: menger sponge addon for ezdxf
2# Created: 06.12.2016
3# Copyright (c) 2016-2020 Manfred Moitzi
4# License: MIT License
5from typing import TYPE_CHECKING, Iterable, List, Tuple
6from ezdxf.math import Vec3
7from ezdxf.render.mesh import MeshVertexMerger, MeshTransformer
8
9if TYPE_CHECKING:
10    from ezdxf.eztypes import Vertex, GenericLayoutType, Matrix44, UCS
11
12all_cubes_size_3_template = [
13    (0, 0, 0), (1, 0, 0), (2, 0, 0), (0, 1, 0), (1, 1, 0), (2, 1, 0), (0, 2, 0), (1, 2, 0), (2, 2, 0),
14    (0, 0, 1), (1, 0, 1), (2, 0, 1), (0, 1, 1), (1, 1, 1), (2, 1, 1), (0, 2, 1), (1, 2, 1), (2, 2, 1),
15    (0, 0, 2), (1, 0, 2), (2, 0, 2), (0, 1, 2), (1, 1, 2), (2, 1, 2), (0, 2, 2), (1, 2, 2), (2, 2, 2),
16]
17
18original_menger_cubes = [
19    (0, 0, 0), (1, 0, 0), (2, 0, 0), (0, 1, 0), (2, 1, 0), (0, 2, 0), (1, 2, 0), (2, 2, 0),
20    (0, 0, 1), (2, 0, 1), (0, 2, 1), (2, 2, 1),
21    (0, 0, 2), (1, 0, 2), (2, 0, 2), (0, 1, 2), (2, 1, 2), (0, 2, 2), (1, 2, 2), (2, 2, 2),
22]
23
24menger_v1 = [
25    (0, 0, 0), (2, 0, 0), (1, 1, 0), (0, 2, 0), (2, 2, 0),
26    (1, 0, 1), (0, 1, 1), (2, 1, 1), (1, 2, 1),
27    (0, 0, 2), (2, 0, 2), (1, 1, 2), (0, 2, 2), (2, 2, 2),
28]
29
30menger_v2 = [
31    (1, 0, 0), (0, 1, 0), (2, 1, 0), (1, 2, 0),
32    (0, 0, 1), (2, 0, 1), (1, 1, 1), (0, 2, 1), (2, 2, 1),
33    (1, 0, 2), (0, 1, 2), (2, 1, 2), (1, 2, 2),
34]
35
36jerusalem_cube = [
37    (0, 0, 0), (1, 0, 0), (2, 0, 0), (3, 0, 0), (4, 0, 0), (0, 1, 0), (1, 1, 0), (3, 1, 0), (4, 1, 0), (0, 2, 0),
38    (4, 2, 0), (0, 3, 0), (1, 3, 0), (3, 3, 0), (4, 3, 0), (0, 4, 0), (1, 4, 0), (2, 4, 0), (3, 4, 0), (4, 4, 0),
39    (0, 0, 1), (1, 0, 1), (3, 0, 1), (4, 0, 1), (0, 1, 1), (1, 1, 1), (3, 1, 1), (4, 1, 1), (0, 3, 1), (1, 3, 1),
40    (3, 3, 1), (4, 3, 1), (0, 4, 1), (1, 4, 1), (3, 4, 1), (4, 4, 1), (0, 0, 2), (4, 0, 2), (0, 4, 2), (4, 4, 2),
41    (0, 0, 3), (1, 0, 3), (3, 0, 3), (4, 0, 3), (0, 1, 3), (1, 1, 3), (3, 1, 3), (4, 1, 3), (0, 3, 3), (1, 3, 3),
42    (3, 3, 3), (4, 3, 3), (0, 4, 3), (1, 4, 3), (3, 4, 3), (4, 4, 3), (0, 0, 4), (1, 0, 4), (2, 0, 4), (3, 0, 4),
43    (4, 0, 4), (0, 1, 4), (1, 1, 4), (3, 1, 4), (4, 1, 4), (0, 2, 4), (4, 2, 4), (0, 3, 4), (1, 3, 4), (3, 3, 4),
44    (4, 3, 4), (0, 4, 4), (1, 4, 4), (2, 4, 4), (3, 4, 4), (4, 4, 4),
45]
46
47building_schemas = [
48    original_menger_cubes,
49    menger_v1,
50    menger_v2,
51    jerusalem_cube,
52]
53
54# subdivide level in order of building_schemas
55cube_sizes = [3., 3., 3., 5.]
56
57# 8 corner vertices
58_cube_vertices = [
59    (0, 0, 0),
60    (1, 0, 0),
61    (1, 1, 0),
62    (0, 1, 0),
63    (0, 0, 1),
64    (1, 0, 1),
65    (1, 1, 1),
66    (0, 1, 1),
67]
68
69# 6 cube faces
70cube_faces = [
71    [0, 3, 2, 1],
72    [4, 5, 6, 7],
73    [0, 1, 5, 4],
74    [1, 2, 6, 5],
75    [3, 7, 6, 2],
76    [0, 4, 7, 3],
77]
78
79
80class MengerSponge:
81    """
82
83    Args:
84        location: location of lower left corner as (x, y, z) tuple
85        length: side length
86        level: subdivide level
87        kind: type of menger sponge
88
89    === ===========================
90    0   Original Menger Sponge
91    1   Variant XOX
92    2   Variant OXO
93    3   Jerusalem Cube
94    === ===========================
95
96    """
97
98    def __init__(self, location: 'Vertex' = (0., 0., 0.), length: float = 1., level: int = 1, kind: int = 0):
99        self.cube_definitions = _menger_sponge(location=location, length=length, level=level, kind=kind)
100
101    def vertices(self) -> Iterable['Vertex']:
102        """
103        Yields the cube vertices as list of (x, y, z) tuples.
104
105        """
106        for location, length in self.cube_definitions:
107            x, y, z = location
108            yield [Vec3(x + xf * length, y + yf * length, z + zf * length) for xf, yf, zf in _cube_vertices]
109
110    __iter__ = vertices
111
112    @staticmethod
113    def faces() -> List[List[int]]:
114        """
115        Returns list of cube faces. All cube vertices have the same order, so one faces list fits them all.
116
117        """
118        return cube_faces
119
120    def render(self, layout: 'GenericLayoutType', merge: bool = False, dxfattribs: dict = None,
121               matrix: 'Matrix44' = None, ucs: 'UCS' = None) -> None:
122        """
123        Renders the menger sponge into layout, set `merge` to ``True`` for rendering the whole menger sponge into
124        one MESH entity, set `merge` to ``False`` for rendering the individual cubes of the menger sponge as
125        MESH entities.
126
127        Args:
128            layout: DXF target layout
129            merge: ``True`` for one MESH entity, ``False`` for individual MESH entities per cube
130            dxfattribs: DXF attributes for the MESH entities
131            matrix: apply transformation matrix at rendering
132            ucs: apply UCS transformation at rendering
133
134        """
135        if merge:
136            mesh = self.mesh()
137            mesh.render_mesh(layout, dxfattribs=dxfattribs, matrix=matrix, ucs=ucs)
138        else:
139            for cube in self.cubes():
140                cube.render_mesh(layout, dxfattribs, matrix=matrix, ucs=ucs)
141
142    def cubes(self) -> Iterable[MeshTransformer]:
143        """ Yields all cubes of the menger sponge as individual :class:`MeshTransformer` objects.
144        """
145        faces = self.faces()
146        for vertices in self:
147            mesh = MeshVertexMerger()
148            mesh.add_mesh(vertices=vertices, faces=faces)
149            yield MeshTransformer.from_builder(mesh)
150
151    def mesh(self) -> MeshTransformer:
152        """ Returns geometry as one :class:`MeshTransformer` object.
153        """
154        faces = self.faces()
155        mesh = MeshVertexMerger()
156        for vertices in self:
157            mesh.add_mesh(vertices=vertices, faces=faces)
158        return MeshTransformer.from_builder(mesh)
159
160
161def _subdivide(location: 'Vertex' = (0., 0., 0.), length: float = 1., kind: int = 0) -> List[Tuple['Vertex', float]]:
162    """
163    Divides a cube in sub-cubes and keeps only cubes determined by the building schema.
164
165    All sides are parallel to x-, y- and z-axis, location is a (x, y, z) tuple and represents the coordinates of the
166    lower left corner (nearest to the axis origin) of the cube, length is the side-length of the cube
167
168    Args:
169        location: (x, y, z) tuple, coordinates of the lower left corner of the cube
170        length: side length of the cube
171        kind: int for 0: original menger sponge; 1: Variant XOX; 2: Variant OXO; 3: Jerusalem Cube;
172
173    Returns: list of sub-cubes (location, length)
174
175    """
176
177    init_x, init_y, init_z = location
178    step_size = float(length) / cube_sizes[kind]
179    remaining_cubes = building_schemas[kind]
180
181    def sub_location(indices) -> Vec3:
182        x, y, z = indices
183        return Vec3(
184            init_x + x * step_size,
185            init_y + y * step_size,
186            init_z + z * step_size,
187        )
188
189    return [(sub_location(indices), step_size) for indices in remaining_cubes]
190
191
192def _menger_sponge(location: 'Vertex' = (0., 0., 0.), length: float = 1., level: int = 1, kind: int = 0) -> List[
193    Tuple[Vec3, float]]:
194    """
195    Builds a menger sponge for given level.
196
197    Args:
198        location: (x, y, z) tuple, coordinates of the lower left corner of the cube
199        length: side length of the cube
200        level: level of menger sponge, has to be 1 or bigger
201        kind: int for 0: original menger sponge; 1: Variant XOX; 2: Variant OXO; 3: Jerusalem Cube;
202
203    Returns: list of sub-cubes (location, length)
204
205    """
206    kind = int(kind)
207    if kind not in (0, 1, 2, 3):
208        raise ValueError('kind has to be 0, 1, 2 or 3.')
209    level = int(level)
210    if level < 1:
211        raise ValueError("level has to be 1 or bigger.")
212    cubes = _subdivide(location, length, kind=kind)
213    for _ in range(level - 1):
214        next_level_cubes = []
215        for location, length in cubes:
216            next_level_cubes.extend(_subdivide(location, length, kind=kind))
217        cubes = next_level_cubes
218    return cubes
219