Skip to content

Commit 79a5f77

Browse files
author
XorUnison
committed
Added Honeycomb, even more verbose docstrings, fixed Tiling variables
1 parent 79b4d54 commit 79a5f77

File tree

5 files changed

+225
-79
lines changed

5 files changed

+225
-79
lines changed

manim/mobject/geometry.py

Lines changed: 120 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -807,9 +807,17 @@ class ArcPolygon(VMobject):
807807
"""
808808
The ArcPolygon is what it says, a polygon, but made from arcs.
809809
More versatile than the standard Polygon.
810-
Accepts both Arc and ArcBetweenPoints and is instantiated like this:
810+
811+
Parameters
812+
----------
813+
*arcs : Arc or ArcBetweenPoints
814+
815+
Example
816+
-------
811817
ArcPolygon(arc0,arc1,arc2,arcN,**kwargs)
812-
For proper appearance the arcs should be seamlessly connected:
818+
819+
820+
For proper appearance the arcs should seamlessly connect:
813821
[a,b][b,c][c,a]
814822
If they don't, the gaps will be filled in with straight lines.
815823
@@ -822,8 +830,8 @@ class ArcPolygon(VMobject):
822830
def __init__(self, *arcs, **kwargs):
823831
if not all([isinstance(m, Arc) or
824832
isinstance(m, ArcBetweenPoints) for m in arcs]):
825-
raise Exception("All ArcPolygon submobjects must be of"
826-
"type Arc/ArcBetweenPoints")
833+
raise ValueError("All ArcPolygon submobjects must be of"
834+
"type Arc/ArcBetweenPoints")
827835
VMobject.__init__(self, **kwargs)
828836
# Adding the arcs like this makes arcpolygon double as a group
829837
self.add(*arcs)
@@ -836,9 +844,7 @@ def __init__(self, *arcs, **kwargs):
836844
len_ratio = line.get_length() / arc1.get_arc_length()
837845
if math.isnan(len_ratio) or math.isinf(len_ratio):
838846
continue
839-
line.insert_n_curves(
840-
int(arc1.get_num_curves() * len_ratio)
841-
)
847+
line.insert_n_curves(int(arc1.get_num_curves() * len_ratio))
842848
self.append_points(line.get_points())
843849

844850

@@ -924,71 +930,116 @@ class Tiling(VMobject):
924930
rotated the correct way, as well as having a vertex setup that
925931
allows proper transformations.
926932
927-
It's instantiated like this:
928-
Tiling(tile_prototype, xOffset, yOffset, xRange, yRange, **kwargs)
929-
The tile prototype can be any Mobject, a VGroup or a function.
930-
The function format is function(x,y), returning a Mobject/VGroup.
933+
Parameters
934+
----------
935+
tile_prototype : Mobject or function(x,y) that returns a Mobject
936+
x_offset : nested list of Mobject methods and values
937+
y_offset : nested list of Mobject methods and values
938+
x_range : range
939+
y_range : range
940+
941+
942+
The tile prototype can be any Mobject (also groups) or a function.
943+
The function format is function(x,y), returning a Mobject.
931944
Using groups or functions allows the tiling to contain multiple
932945
different tiles or to simplify the following offset functions.
933946
934947
Next are two nested lists that determine how the tiles are arranged.
935948
The functions are typically Mobject.shift and Mobject.rotate.
936949
Each list has to contain sublists with Function/Value pairs.
937-
Example for a simple shift along the X-Axis:
950+
More on this in the Examples section.
951+
952+
Last are two ranges: If both ranges are range(-1,1,1),
953+
that would result in a square grid of 9 tiles.
954+
955+
A Tiling can be directly drawn like a VGroup.
956+
Tiling.tile_dictionary[x][y] can be used to access individual tiles,
957+
to color them for example.
958+
959+
Examples
960+
--------
961+
The nested lists determining arrangement need more explanation.
962+
Example for a shift along the X-Axis, 1 in the positive direction:
938963
[[Mobject.shift,[1,0,0]]]
964+
965+
The origin tile at [x0,y0] won't be moved, but the tile at [x1,y0]
966+
will be moved to [1,0,0]. Likewise the tile at [x4,y0] will be moved
967+
to [4,0,0]
939968
940-
Every move within the tiling applies a full sublist.
969+
Every step within the tiling applies a full sublist.
941970
Example for a shift with simultaneous rotation:
942971
[[Mobject.shift,[1,0,0],Mobject.rotate,np.pi]]
972+
973+
This would move the tile at [x1,y0] to [1,0,0] and rotates it 180°.
943974
944975
When multiple sublists are passed, they are applied alternating.
945976
Example for alternating shifting and rotating:
946977
[[Mobject.shift,[1,0,0]],[Mobject.rotate,np.pi]]
947978
948-
Last are two ranges: If both ranges are range(-1,1,1),
949-
that would result in a grid of 9 tiles.
979+
This would move the tile at [x1,y0] to [1,0,0], but wouldn't rotate
980+
it yet. The tile at [x2,y0] would still be moved to [1,0,0] and also
981+
rotated by 180°. The tile at [x3,y0] would be moved to [2,0,0] and
982+
still rotated by 180°.
950983
951984
Full example:
952985
Tiling(Square(),
953986
[[Mobject.shift,[2.1,0,0]]],
954987
[[Mobject.shift,[0,2.1,0]]],
955988
range(-1,1),
956989
range(-1,1))
957-
958-
A Tiling can be directly drawn like a VGroup.
959-
Tiling.tile_dictionary[x][y] can be used to access individual tiles,
960-
to color them for example.
961990
"""
962-
def __init__(self, tile_prototype, xOffset, yOffset, xRange, yRange, **kwargs):
991+
def __init__(self, tile_prototype, x_offset, y_offset, x_range, y_range, **kwargs):
963992
VMobject.__init__(self, **kwargs)
964-
# First we add one more to the range,
965-
# so that a -1,1 step 1 range also gives us 3 tiles,
966-
# [-1,0,1] as opposed to 2 [-1,0]
967-
self.xRange=range(xRange.start,xRange.stop+xRange.step,xRange.step)
968-
self.yRange=range(yRange.start,yRange.stop+yRange.step,yRange.step)
969-
993+
# Add one more to the ranges, so that a range(-1,1,1)
994+
# also gives us 3 tiles, [-1,0,1] as opposed to 2 [-1,0]
995+
self.x_range=range(x_range.start,x_range.stop+x_range.step,x_range.step)
996+
self.y_range=range(y_range.start,y_range.stop+y_range.step,y_range.step)
997+
self.x_offset=x_offset
998+
self.y_offset=y_offset
999+
9701000
# We need the tiles array for a VGroup, which in turn we need
9711001
# to draw the tiling and adjust it.
9721002
# Trying to draw the tiling directly will not properly work.
1003+
self.tile_prototype=tile_prototype
9731004
self.tile_dictionary={}
974-
self.kwargs = kwargs
975-
for x in self.xRange:
1005+
self.tile_init_loop()
1006+
1007+
def tile_init_loop(self):
1008+
"""
1009+
Loops through the ranges, creates the tiles by copying the
1010+
prototype, adds them to self and sorts them into the dictionary.
1011+
Calls apply_transforms to apply passed methods.
1012+
"""
1013+
for x in self.x_range:
9761014
self.tile_dictionary[x]={}
977-
for y in self.yRange:
978-
if callable(tile_prototype):
979-
tile=tile_prototype(x,y).deepcopy()
1015+
for y in self.y_range:
1016+
if callable(self.tile_prototype):
1017+
tile=self.tile_prototype(x,y).deepcopy()
9801018
else:
981-
tile=tile_prototype.deepcopy()
982-
self.transform_tile(x,xOffset,tile)
983-
self.transform_tile(y,yOffset,tile)
1019+
tile=self.tile_prototype.deepcopy()
1020+
self.apply_transforms(x,y,tile)
9841021
self.add(tile)
9851022
self.tile_dictionary[x][y]=tile
986-
# TODO: Once the config overhaul is far enough:
987-
# Implement a way to apply kwargs/some dict to all tiles
1023+
# TODO: Once the config overhaul is far enough:
1024+
# Implement a way to apply kwargs to all tiles.
1025+
# The reason for this is that if multiple tilings
1026+
# are instantiated from one prototype, having a different basic
1027+
# tile setup is rather complicated now (set_fill etc).
1028+
1029+
def apply_transforms(self,x,y,tile):
1030+
"""
1031+
Calls transform_tile once per dimension to position tiles.
1032+
Written like this to allow easy extending by Honeycomb.
1033+
"""
1034+
self.transform_tile(x,self.x_offset,tile)
1035+
self.transform_tile(y,self.y_offset,tile)
9881036

989-
# This method computes and applies the offsets for the tiles.
990-
# Also multiplies inputs, which requires arrays to be numpy arrays.
9911037
def transform_tile(self,position,offset,tile):
1038+
"""
1039+
This method computes and applies the offsets for the tiles,
1040+
in the given dimension.
1041+
multiplies inputs, which requires arrays to be numpy arrays.
1042+
"""
9921043
# The number of different offsets the current axis has
9931044
offsets_nr=len(offset)
9941045
for i in range(offsets_nr):
@@ -1004,17 +1055,25 @@ def transform_tile(self,position,offset,tile):
10041055
else:
10051056
magnitude=len(range(i,position,offsets_nr))
10061057
offset[i][0+j*2](tile,magnitude*np.array(offset[i][1+j*2]))
1007-
1058+
10081059

10091060
class Graph():
10101061
"""
10111062
This class is for visual representation of graphs for graph theory.
10121063
(Not graphs of functions. Same term but entirely different things.)
10131064
1014-
It's instantiated with a dictionary that represents the graph,
1015-
a configuration dictionary for vertex appearance, and one for edges.
1016-
The configuration dictionaries are optional.
1017-
Graph(graph_dictionary, vertex_config=vc, edge_config=ec)
1065+
It's instantiated with a dictionary that represents the graph, and
1066+
optionally which types of Mobject to use as vertices/edges and
1067+
dicts for their standard attributes.
1068+
1069+
Parameters
1070+
----------
1071+
graph : dict
1072+
vertex_type : MobjectClass, optional (default: Circle)
1073+
vertex_config : dict, optional
1074+
edge_type : MobjectClass, optional (default: ArcBetweenPoints)
1075+
edge_config : dict, optional
1076+
10181077
10191078
The keys for the graph have to be of type int in ascending order,
10201079
with each number denoting a vertex.
@@ -1026,23 +1085,27 @@ class Graph():
10261085
directions, but it'll be drawn once, from lower to higher number.
10271086
For example if vertex 2 is connected to vertex 0, that's ignored.
10281087
The config dictionary is used to initialize a Circle as the vertex.
1029-
(Or an Annulus if annulus=True is passed to this class.)
10301088
It will override values passed as vertex_config to the Graph.
1089+
1090+
Examples
1091+
--------
10311092
Full example:
10321093
g = {0: [[0,0,0], [1, 2], {"color": BLUE}],
10331094
1: [[1,0,0], [0, 2], {"color": GRAY}],
10341095
2: [[0,1,0], [0, 1], {"color": PINK}]}
10351096
Graph(g,vertex_config={"radius": 0.2,"fill_opacity": 1},
10361097
edge_config={"stroke_width": 5,"color": RED})
10371098
1038-
An edge is instantiated as an ArcBetweenPoints, and optionally
1099+
An edge usually instantiated as an ArcBetweenPoints, and optionally
10391100
individual config dictionaries can also be passed to them.
10401101
Example:
10411102
g = {0: [[0,0,0], [[1,{"angle": 2}], [2,{"color": WHITE}]]...
1103+
10421104
1043-
Use Graph.vertices/Graph.edges/Graph.annuli for drawing.
1105+
Use Graph.vertices/Graph.edges for drawing.
10441106
"""
1045-
def __init__(self, graph, vertex_config={}, edge_config={}, **kwargs):
1107+
def __init__(self, graph, vertex_type=Circle, vertex_config={},
1108+
edge_type=ArcBetweenPoints, edge_config={}, **kwargs):
10461109
if not all(isinstance(n,int) for n in graph.keys()):
10471110
raise ValueError("All keys for the Graph dictionary have to be of type int")
10481111
if not all(all(isinstance(m,int) or isinstance(m,list)
@@ -1051,26 +1114,13 @@ def __init__(self, graph, vertex_config={}, edge_config={}, **kwargs):
10511114
raise ValueError("Invalid Edge definition in Graph class. Use int or "
10521115
"[int,dict].")
10531116

1054-
self.graph = graph
1055-
self.vertex_config = vertex_config
1056-
self.edge_config = edge_config
1057-
if kwargs.get('annulus', False):
1058-
self.annulus = True
1059-
else:
1060-
self.annulus = False
1061-
self.make_graph()
1062-
1063-
def make_graph(self):
10641117
self.vertices = VGroup()
10651118
self.edges = VGroup()
1066-
self.annuli = VGroup()
1067-
for vertex, attributes in self.graph.items():
1068-
if self.annulus:
1069-
self.annuli.add(Annulus(**{**self.vertex_config,
1070-
**attributes[2]}).shift(attributes[0]))
1071-
else:
1072-
self.vertices.add(Circle(**{**self.vertex_config,
1073-
**attributes[2]}).shift(attributes[0]))
1119+
1120+
# Loops over all key/value pairs of the graph dict.
1121+
for vertex, attributes in graph.items():
1122+
self.vertices.add(vertex_type(**{**vertex_config,
1123+
**attributes[2]}).shift(attributes[0]))
10741124
for edge_definition in attributes[1]:
10751125
if isinstance(edge_definition, int):
10761126
vertex_number=edge_definition
@@ -1079,9 +1129,9 @@ def make_graph(self):
10791129
vertex_number=edge_definition[0]
10801130
edge_kwargs=edge_definition[1]
10811131
if vertex < vertex_number:
1082-
edge = ArcBetweenPoints(attributes[0],
1083-
self.graph[vertex_number][0],
1084-
**{"angle": 0,
1085-
**self.edge_config,
1086-
**edge_kwargs})
1132+
edge = edge_type(attributes[0],
1133+
graph[vertex_number][0],
1134+
**{"angle": 0,
1135+
**edge_config,
1136+
**edge_kwargs})
10871137
self.edges.add(edge)

manim/mobject/three_dimensions.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from ..constants import *
22
from ..mobject.geometry import Square
3+
from ..mobject.geometry import Tiling
34
from ..mobject.types.vectorized_mobject import VGroup
45
from ..mobject.types.vectorized_mobject import VMobject
56
from ..utils.iterables import tuplify
@@ -153,4 +154,61 @@ class Prism(Cube):
153154
def generate_points(self):
154155
Cube.generate_points(self)
155156
for dim, value in enumerate(self.dimensions):
156-
self.rescale_to_fit(value, dim, stretch=True)
157+
self.rescale_to_fit(value, dim, stretch=True)
158+
159+
160+
class Honeycomb(Tiling):
161+
"""
162+
Inherits from Tiling and works effectively the same, just adding
163+
the third dimension.
164+
To achieve this __init__ and apply_transforms are extended,
165+
while tile_init_loop is overridden.
166+
Since it's a 3D honeycomb also allows for tile_dictionary to be
167+
alternatively called as cell_dictionary.
168+
See Tiling for more details.
169+
170+
Parameters
171+
----------
172+
tile_prototype : Mobject or function(x,y,z) that returns a Mobject
173+
x_offset : nested list of Mobject methods and values
174+
y_offset : nested list of Mobject methods and values
175+
z_offset : nested list of Mobject methods and values
176+
x_range : range
177+
y_range : range
178+
z_range : range
179+
180+
Example
181+
-------
182+
Honeycomb(Cube(),
183+
[[Mobject.shift,[2.1,0,0]]],
184+
[[Mobject.shift,[0,2.1,0]]],
185+
[[Mobject.shift,[0,0,2.1]]],
186+
range(-1,1),
187+
range(-1,1),
188+
range(-1,1))
189+
"""
190+
def __init__(self, tile_prototype, x_offset, y_offset, z_offset,
191+
x_range, y_range, z_range, **kwargs):
192+
self.z_range=range(z_range.start,z_range.stop+z_range.step,z_range.step)
193+
self.z_offset=z_offset
194+
super().__init__(tile_prototype, x_offset, y_offset, x_range, y_range, **kwargs)
195+
self.cell_dictionary=self.tile_dictionary
196+
197+
def tile_init_loop(self):
198+
for x in self.x_range:
199+
self.tile_dictionary[x]={}
200+
self.tile_dictionary[x]={}
201+
for y in self.y_range:
202+
self.tile_dictionary[x][y]={}
203+
for z in self.z_range:
204+
if callable(self.tile_prototype):
205+
tile=self.tile_prototype(x,y,z).deepcopy()
206+
else:
207+
tile=self.tile_prototype.deepcopy()
208+
self.apply_transforms(x,y,z,tile)
209+
self.add(tile)
210+
self.tile_dictionary[x][y][z]=tile
211+
212+
def apply_transforms(self,x,y,z,tile):
213+
super().apply_transforms(x,y,tile)
214+
self.transform_tile(z,self.z_offset,tile)

0 commit comments

Comments
 (0)