add the interface with curves
Browse files- .gitignore +1 -0
- app.py +177 -2
- curves_parameters.yaml +227 -0
- lsystc.py +586 -0
- requirements.txt +12 -0
- requirements_dev.txt +5 -0
- requirements_other.txt +1 -0
- specific_values.py +30 -0
- test_app.py +8 -0
- test_curves.py +109 -0
.gitignore
CHANGED
@@ -1,2 +1,3 @@
|
|
1 |
/.idea
|
2 |
/_tmp
|
|
|
|
1 |
/.idea
|
2 |
/_tmp
|
3 |
+
/__pycache__
|
app.py
CHANGED
@@ -1,4 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
-
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Streamlit application
|
3 |
+
"""
|
4 |
+
from loguru import logger
|
5 |
+
import mpld3
|
6 |
import streamlit as st
|
7 |
+
import streamlit.components.v1 as components
|
8 |
+
from streamlit.errors import StreamlitAPIException
|
9 |
+
import yaml
|
10 |
+
import lsystc as ls
|
11 |
+
import specific_values as sv
|
12 |
|
13 |
+
st.set_page_config(page_title="Curves with L-systems", page_icon="🖼️", layout="wide")
|
14 |
+
|
15 |
+
|
16 |
+
@st.cache_data
|
17 |
+
def load_result(axiom, mult_axiom, rules, rotation_angle, starting_angle, skipped, nb_iterations, coeff):
|
18 |
+
"""
|
19 |
+
Return the result of the L-system with the specified parameters
|
20 |
+
|
21 |
+
:param axiom:
|
22 |
+
:param mult_axiom:
|
23 |
+
:param rules:
|
24 |
+
:param rotation_angle:
|
25 |
+
:param starting_angle:
|
26 |
+
:param skipped:
|
27 |
+
:param nb_iterations:
|
28 |
+
:param coeff:
|
29 |
+
:return: the result of the "L-System rendering"
|
30 |
+
"""
|
31 |
+
try:
|
32 |
+
config = ls.Config(skipped=skipped)
|
33 |
+
rules = rules.strip("; ")
|
34 |
+
rules_list = []
|
35 |
+
|
36 |
+
if rules:
|
37 |
+
for item in rules.split(";"):
|
38 |
+
if ':' in item:
|
39 |
+
splits = item.split(":")
|
40 |
+
if len(splits) == 2 and splits[0].strip():
|
41 |
+
left = splits[0].strip()
|
42 |
+
right = splits[1].strip()
|
43 |
+
rules_list.append((left, right))
|
44 |
+
else:
|
45 |
+
raise ValueError(f"Every rule must be correctly written ( {item} )")
|
46 |
+
else:
|
47 |
+
raise ValueError(f"Every non empty rule must include a column character ( {item} ) ")
|
48 |
+
|
49 |
+
rls = ls.Lsystc(config, axiom * mult_axiom, rules_list, nbiter=nb_iterations, verbose=sv.verbose)
|
50 |
+
rls.turtle(step=sv.step, angle=rotation_angle, angleinit=starting_angle, coeff=coeff,
|
51 |
+
color_length=sv.color_length, color_map=sv.color_map)
|
52 |
+
|
53 |
+
result = rls.render(sv.renderer, save_files=sv.save_files, show_more=sv.show_more, show_3d=sv.show_3d,
|
54 |
+
return_type=sv.return_type)
|
55 |
+
except ValueError as ex:
|
56 |
+
st.warning(f"Please verify your parameters - {ex}")
|
57 |
+
st.stop()
|
58 |
+
except Exception as ex:
|
59 |
+
st.warning("Please verify your parameters")
|
60 |
+
if sv.verbose:
|
61 |
+
logger.exception(f"Something went wrong : {ex}")
|
62 |
+
st.stop()
|
63 |
+
else:
|
64 |
+
return result
|
65 |
+
|
66 |
+
|
67 |
+
def write_specific(content):
|
68 |
+
"""
|
69 |
+
Write streamlit content
|
70 |
+
|
71 |
+
:param content: streamlit content
|
72 |
+
:return: None
|
73 |
+
"""
|
74 |
+
try:
|
75 |
+
st.write(content)
|
76 |
+
except StreamlitAPIException as exc:
|
77 |
+
# Currently : streamlit.errors.StreamlitAPIException: `_repr_html_()` is not a valid Streamlit command.
|
78 |
+
if sv.verbose:
|
79 |
+
logger.error(exc)
|
80 |
+
|
81 |
+
|
82 |
+
def on_change_selection():
|
83 |
+
"""
|
84 |
+
Change the parameters when the starting example is changed
|
85 |
+
|
86 |
+
:return: None
|
87 |
+
"""
|
88 |
+
current_selection = st.session_state.my_selection
|
89 |
+
axiom, mult_axiom, rules, rotation_angle, starting_angle, nb_iter, skipped, coeff = examples_data[current_selection]
|
90 |
+
st.session_state.my_axiom = axiom
|
91 |
+
st.session_state.my_mult_axiom = mult_axiom
|
92 |
+
st.session_state.my_rules = rules
|
93 |
+
st.session_state.my_rotation_angle = rotation_angle
|
94 |
+
st.session_state.my_starting_angle = starting_angle
|
95 |
+
st.session_state.my_nb_iter = nb_iter
|
96 |
+
st.session_state.my_skipped = skipped
|
97 |
+
st.session_state.my_coeff = coeff
|
98 |
+
sv.redraw_auto = True
|
99 |
+
|
100 |
+
|
101 |
+
with open("curves_parameters.yaml", 'r', encoding='utf8') as f:
|
102 |
+
curves_parameters = yaml.safe_load(f)
|
103 |
+
|
104 |
+
EXAMPLES_DEFAULT = 0
|
105 |
+
examples_names = []
|
106 |
+
examples_data = {}
|
107 |
+
for c_name, c_params in curves_parameters.items():
|
108 |
+
examples_names.append(c_name)
|
109 |
+
c_axiom = c_params.get('axiom', '')
|
110 |
+
c_axiom_multiplier = c_params.get('axiom_multiplier', 1)
|
111 |
+
c_rules = c_params.get('rules', '')
|
112 |
+
c_rotation_angle = c_params.get('rotation_angle', 90.0)
|
113 |
+
c_starting_angle = c_params.get('starting_angle', 0)
|
114 |
+
c_nb_iter = c_params.get('nb_iter', 1)
|
115 |
+
c_skipped = c_params.get('skipped', '')
|
116 |
+
c_coeff = c_params.get('coeff', 1.0)
|
117 |
+
examples_data[c_name] = (c_axiom, c_axiom_multiplier, c_rules,
|
118 |
+
c_rotation_angle, c_starting_angle, c_nb_iter, c_skipped, c_coeff)
|
119 |
+
|
120 |
+
|
121 |
+
EXAMPLES_DEFAULT_name = examples_names[EXAMPLES_DEFAULT]
|
122 |
+
def_axiom, def_mult_axiom, def_rules, def_rotation_angle, def_starting_angle, def_nb_iter, def_skipped, def_coeff = \
|
123 |
+
examples_data[EXAMPLES_DEFAULT_name]
|
124 |
+
|
125 |
+
with st.sidebar:
|
126 |
+
MD_INTRO = """
|
127 |
+
You have the flexibility to select a starting example and to change the parameters :sunglasses:
|
128 |
+
"""
|
129 |
+
|
130 |
+
st.markdown(MD_INTRO)
|
131 |
+
|
132 |
+
input_selection = st.selectbox('Starting example', examples_names,
|
133 |
+
index=EXAMPLES_DEFAULT, on_change=on_change_selection, key="my_selection")
|
134 |
+
|
135 |
+
input_axiom = st.text_input('Starting axiom', def_axiom, key="my_axiom")
|
136 |
+
input_mult_axiom = st.number_input('Multiplier for axiom', value=def_mult_axiom, min_value=1, key="my_mult_axiom")
|
137 |
+
|
138 |
+
input_rules = st.text_input('Rules', def_rules, help="Example for 2 rules -> A: ABC ; B: CAB ; ",
|
139 |
+
key="my_rules")
|
140 |
+
input_rotation_angle = st.number_input('Angle of rotation',
|
141 |
+
value=def_rotation_angle, min_value=1.0, max_value=360.0, step=1.0,
|
142 |
+
format='%f', key="my_rotation_angle")
|
143 |
+
input_starting_angle = st.number_input('Starting angle',
|
144 |
+
value=def_starting_angle, min_value=0, max_value=360,
|
145 |
+
format='%d', key="my_starting_angle")
|
146 |
+
|
147 |
+
input_skipped = st.text_input('Skipped characters', def_skipped, key="my_skipped")
|
148 |
+
|
149 |
+
input_coeff = st.number_input('Coefficient',
|
150 |
+
value=def_coeff, format='%f', key="my_coeff")
|
151 |
+
|
152 |
+
input_nb_iter = st.number_input('Number of iterations',
|
153 |
+
value=def_nb_iter, min_value=1, max_value=15, format='%d', key="my_nb_iter")
|
154 |
+
|
155 |
+
st.markdown("# Curves with L-systems")
|
156 |
+
|
157 |
+
st.markdown("""**Click on "Draw" to display the curve**""")
|
158 |
+
|
159 |
+
if st.button('Draw') or sv.redraw_auto or 'start' not in st.session_state:
|
160 |
+
sv.redraw_auto = False
|
161 |
+
st.session_state['start'] = True
|
162 |
+
res = load_result(input_axiom, input_mult_axiom, input_rules, input_rotation_angle, input_starting_angle,
|
163 |
+
input_skipped, input_nb_iter, input_coeff)
|
164 |
+
|
165 |
+
if sv.return_type == 'image':
|
166 |
+
write_specific(st.image(res, caption='Generated image'))
|
167 |
+
else:
|
168 |
+
if sv.renderer == 'matplot':
|
169 |
+
# Pyplot figure
|
170 |
+
# write_specific(st.pyplot(res))
|
171 |
+
fig_html = mpld3.fig_to_html(res)
|
172 |
+
components.html(fig_html, height=600)
|
173 |
+
elif sv.renderer == 'bokeh':
|
174 |
+
st.bokeh_chart(res, use_container_width=True)
|
175 |
+
elif sv.renderer == 'plotly':
|
176 |
+
st.plotly_chart(res, use_container_width=True)
|
177 |
+
|
178 |
+
st.markdown("---")
|
179 |
+
st.markdown("More details on Lindenmayer systems here : [Wikipedia](https://en.wikipedia.org/wiki/L-system)")
|
curves_parameters.yaml
ADDED
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
Dragon:
|
3 |
+
axiom: "FX"
|
4 |
+
axiom_multiplier: 1
|
5 |
+
rules: "X:X+YF+. ; Y:-FX-Y"
|
6 |
+
rotation_angle: 90.0
|
7 |
+
nb_iter: 12
|
8 |
+
|
9 |
+
Gosper:
|
10 |
+
axiom: "AB"
|
11 |
+
rules: "A: A-B--B+A++AA+B-. ; B: +A-BB--B-A++A+B."
|
12 |
+
rotation_angle: 60.0
|
13 |
+
nb_iter: 3
|
14 |
+
|
15 |
+
Hilbert:
|
16 |
+
axiom: "L"
|
17 |
+
rules: "L:-RF+LFL+FR-. ; R:+LF-RFR-FL+."
|
18 |
+
rotation_angle: 90.0
|
19 |
+
nb_iter: 5
|
20 |
+
skipped: LR
|
21 |
+
|
22 |
+
Icy:
|
23 |
+
axiom: "F+F+F+F"
|
24 |
+
rules: "F: FF+F++.F+F "
|
25 |
+
rotation_angle: 90.0
|
26 |
+
nb_iter: 5
|
27 |
+
|
28 |
+
Islands:
|
29 |
+
axiom: "F-F-F-F"
|
30 |
+
rules: "F:F-U+FF-F-FF-FU-FF+U-FF+F+FF+FU+FFF ; U:UUUUUU"
|
31 |
+
rotation_angle: 90.0
|
32 |
+
nb_iter: 2
|
33 |
+
|
34 |
+
Koch:
|
35 |
+
axiom: "F--F--F"
|
36 |
+
rules: "F: F+F-.-F+F"
|
37 |
+
rotation_angle: 60.0
|
38 |
+
nb_iter: 6
|
39 |
+
|
40 |
+
Minkowski:
|
41 |
+
axiom: "F"
|
42 |
+
rules: "F: F+F-F-F.F+F+F-F "
|
43 |
+
rotation_angle: 90.0
|
44 |
+
nb_iter: 3
|
45 |
+
|
46 |
+
Peano:
|
47 |
+
axiom: "++FA"
|
48 |
+
rules: "A:A-BA+CA+CA+CA-BA-BA-BA+CA. ; B:F-F-F-F. ; C:F+F+F+F."
|
49 |
+
rotation_angle: 22.5
|
50 |
+
nb_iter: 4
|
51 |
+
skipped: ABC
|
52 |
+
|
53 |
+
Penrose tiling:
|
54 |
+
axiom: "[N]++[N]++[N]++[N]++[N]"
|
55 |
+
rules: "A:O++B----N[-O----A]++ ; N:+O--B[---A--N]+ ; O:-A++N[+++O++B]- ; B:--O++++A[+B++++N]--N"
|
56 |
+
rotation_angle: 36.0
|
57 |
+
nb_iter: 5
|
58 |
+
|
59 |
+
Pentadendrite:
|
60 |
+
axiom: "F-F-F-F-F"
|
61 |
+
rules: "F:F-F-F+.+F+F-F"
|
62 |
+
rotation_angle: 72.0
|
63 |
+
nb_iter: 4
|
64 |
+
|
65 |
+
Polygon with angle 30 degrees:
|
66 |
+
axiom: "F+."
|
67 |
+
rules: ""
|
68 |
+
rotation_angle: 30.0
|
69 |
+
nb_iter: 1
|
70 |
+
|
71 |
+
axiom_multiplier: 12
|
72 |
+
|
73 |
+
Polygon with angle 150 degrees:
|
74 |
+
axiom: "F+."
|
75 |
+
rules: ""
|
76 |
+
rotation_angle: 150.0
|
77 |
+
nb_iter: 1
|
78 |
+
|
79 |
+
axiom_multiplier: 12
|
80 |
+
|
81 |
+
Polygon with angle 175 degrees:
|
82 |
+
axiom: "F+."
|
83 |
+
rules: ""
|
84 |
+
rotation_angle: 175.0
|
85 |
+
nb_iter: 1
|
86 |
+
|
87 |
+
axiom_multiplier: 72
|
88 |
+
|
89 |
+
Polygon with angle 179 degrees:
|
90 |
+
axiom: "F+."
|
91 |
+
rules: ""
|
92 |
+
rotation_angle: 179.0
|
93 |
+
nb_iter: 1
|
94 |
+
|
95 |
+
axiom_multiplier: 360
|
96 |
+
|
97 |
+
Polygon 9*5:
|
98 |
+
axiom: "r+"
|
99 |
+
rules: "r: F++F--F--F++F."
|
100 |
+
rotation_angle: 40.0
|
101 |
+
nb_iter: 1
|
102 |
+
|
103 |
+
axiom_multiplier: 9
|
104 |
+
|
105 |
+
Repetition 3:
|
106 |
+
axiom: "F#+F#+F#"
|
107 |
+
rules: ""
|
108 |
+
rotation_angle: 120.0
|
109 |
+
nb_iter: 1
|
110 |
+
|
111 |
+
axiom_multiplier: 1
|
112 |
+
|
113 |
+
Repetition 5:
|
114 |
+
axiom: "F#+F#+F#+F#+F#"
|
115 |
+
rules: ""
|
116 |
+
rotation_angle: 72.0
|
117 |
+
nb_iter: 1
|
118 |
+
|
119 |
+
axiom_multiplier: 1
|
120 |
+
|
121 |
+
Repetition 6:
|
122 |
+
axiom: "F#+F#+F#+F#+F#+F#"
|
123 |
+
rules: ""
|
124 |
+
rotation_angle: 60.0
|
125 |
+
nb_iter: 1
|
126 |
+
|
127 |
+
axiom_multiplier: 1
|
128 |
+
|
129 |
+
Returns with angle 5 degrees:
|
130 |
+
axiom: "|+."
|
131 |
+
rules: ""
|
132 |
+
rotation_angle: 5.0
|
133 |
+
nb_iter: 1
|
134 |
+
|
135 |
+
axiom_multiplier: 72
|
136 |
+
|
137 |
+
Round star:
|
138 |
+
axiom: "F"
|
139 |
+
rules: "F:F+.+F"
|
140 |
+
rotation_angle: 77.0
|
141 |
+
nb_iter: 7
|
142 |
+
|
143 |
+
Sierpinsky carpet:
|
144 |
+
axiom: "A"
|
145 |
+
rules: "A:B-A-B. ; B:A+B+A."
|
146 |
+
rotation_angle: 60.0
|
147 |
+
nb_iter: 6
|
148 |
+
|
149 |
+
Spider:
|
150 |
+
axiom: "F*+."
|
151 |
+
rules: ""
|
152 |
+
rotation_angle: 61.0
|
153 |
+
nb_iter: 1
|
154 |
+
|
155 |
+
axiom_multiplier: 500
|
156 |
+
coeff: 0.99
|
157 |
+
|
158 |
+
Spiral 1:
|
159 |
+
axiom: "Fu+."
|
160 |
+
rules: ""
|
161 |
+
rotation_angle: 10.0
|
162 |
+
nb_iter: 1
|
163 |
+
|
164 |
+
axiom_multiplier: 500
|
165 |
+
|
166 |
+
Spiral 2:
|
167 |
+
axiom: "F*+."
|
168 |
+
rules: ""
|
169 |
+
rotation_angle: 10.0
|
170 |
+
nb_iter: 1
|
171 |
+
|
172 |
+
axiom_multiplier: 500
|
173 |
+
coeff: 1.01
|
174 |
+
|
175 |
+
Spiral 3:
|
176 |
+
axiom: "F*+."
|
177 |
+
rules: ""
|
178 |
+
rotation_angle: 10.0
|
179 |
+
nb_iter: 1
|
180 |
+
|
181 |
+
axiom_multiplier: 500
|
182 |
+
coeff: -1.01
|
183 |
+
|
184 |
+
Squigly:
|
185 |
+
axiom: "r"
|
186 |
+
rules: "r: Fr+FL+Fr. ; L: FL-Fr-FL"
|
187 |
+
rotation_angle: 60.0
|
188 |
+
nb_iter: 9
|
189 |
+
|
190 |
+
Tree:
|
191 |
+
axiom: "B"
|
192 |
+
rules: "A: AA; B: A[-B][+B]"
|
193 |
+
rotation_angle: 20.0
|
194 |
+
nb_iter: 5
|
195 |
+
|
196 |
+
starting_angle: 90
|
197 |
+
|
198 |
+
Tree 2:
|
199 |
+
axiom: "F"
|
200 |
+
rules: "F: F[.+FF][.-FF]F[.-F][.+F]F"
|
201 |
+
rotation_angle: 36.0
|
202 |
+
nb_iter: 3
|
203 |
+
|
204 |
+
starting_angle: 90
|
205 |
+
|
206 |
+
Triangle with spiral:
|
207 |
+
axiom: "F*+."
|
208 |
+
rules: ""
|
209 |
+
rotation_angle: 122.0
|
210 |
+
nb_iter: 1
|
211 |
+
|
212 |
+
axiom_multiplier: 500
|
213 |
+
coeff: 0.94
|
214 |
+
|
215 |
+
3D example:
|
216 |
+
axiom: "F+F+F .MFM. F+F+F"
|
217 |
+
rules: ""
|
218 |
+
rotation_angle: 90.0
|
219 |
+
nb_iter: 1
|
220 |
+
|
221 |
+
3D spring:
|
222 |
+
axiom: "F+P."
|
223 |
+
rules: ""
|
224 |
+
rotation_angle: 10.0
|
225 |
+
nb_iter: 1
|
226 |
+
|
227 |
+
axiom_multiplier: 500
|
lsystc.py
ADDED
@@ -0,0 +1,586 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Lindenmayer System (L-system) with personal customizations
|
3 |
+
|
4 |
+
See also
|
5 |
+
|
6 |
+
https://en.wikipedia.org/wiki/L-system
|
7 |
+
|
8 |
+
https://onlinemathtools.com/l-system-generator
|
9 |
+
"""
|
10 |
+
|
11 |
+
import io
|
12 |
+
import math
|
13 |
+
from collections import deque
|
14 |
+
from bokeh.plotting import figure, show, output_file
|
15 |
+
from bokeh.io import export_png, export_svgs
|
16 |
+
from bokeh.io.export import get_screenshot_as_png
|
17 |
+
from loguru import logger
|
18 |
+
from scipy.spatial.transform import Rotation
|
19 |
+
import attrs as at
|
20 |
+
import matplotlib as mpl
|
21 |
+
import matplotlib.pyplot as plt
|
22 |
+
import numpy as np
|
23 |
+
import plotly.graph_objects as go
|
24 |
+
import PIL.Image as pimage
|
25 |
+
|
26 |
+
|
27 |
+
@at.define
|
28 |
+
class Config: # pylint: disable=too-few-public-methods
|
29 |
+
"""
|
30 |
+
Configuration of the important active characters
|
31 |
+
"""
|
32 |
+
reserved: str = ':; ' # reserved characters
|
33 |
+
|
34 |
+
color: str = '.'
|
35 |
+
move_lifted_pen: str = 'UVW'
|
36 |
+
move_angle_init: str = '_'
|
37 |
+
move: str = 'ABCDEFGHIJKLNOQRST' # M and P are reserved for 3d rotations
|
38 |
+
|
39 |
+
move_up_3d: str = '⇧'
|
40 |
+
move_down_3d: str = '⇩'
|
41 |
+
|
42 |
+
r3d_1_plus: str = 'p' # Axis of rotation : "X"
|
43 |
+
r3d_1_minus: str = 'm'
|
44 |
+
|
45 |
+
r3d_2_plus: str = 'P' # Axis of rotation : "Y"
|
46 |
+
r3d_2_minus: str = 'M'
|
47 |
+
|
48 |
+
r3d_all: str = at.field(init=False)
|
49 |
+
|
50 |
+
move_multi: str = at.field(init=False)
|
51 |
+
move_all: str = at.field(init=False)
|
52 |
+
|
53 |
+
delta_add: str = 'u'
|
54 |
+
delta_sub: str = 'v'
|
55 |
+
|
56 |
+
outer_repetition: str = '#'
|
57 |
+
outer_repetition_max: int = 100
|
58 |
+
|
59 |
+
skipped: str = ''
|
60 |
+
total_skipped: str = at.field(init=False) # all skipped characters
|
61 |
+
|
62 |
+
def __attrs_post_init__(self):
|
63 |
+
self.move_multi = self.move.lower()
|
64 |
+
self.move_all = self.color + self.move_lifted_pen + self.move_angle_init + self.move_multi + self.move
|
65 |
+
self.total_skipped = ' ' + self.skipped
|
66 |
+
self.r3d_all = self.r3d_1_plus + self.r3d_1_minus + self.r3d_2_plus + self.r3d_2_minus
|
67 |
+
|
68 |
+
|
69 |
+
class Lsystc:
|
70 |
+
"""
|
71 |
+
Classic L-System with few customizations
|
72 |
+
"""
|
73 |
+
def __init__(self, config: Config, axiom: str, rules: list[tuple[str, str]], nbiter: int,
|
74 |
+
dev_ini: bool = True, verbose: bool = False) -> None:
|
75 |
+
self.config = config
|
76 |
+
self.axiom = axiom
|
77 |
+
self.rules = rules
|
78 |
+
self.nbiter = nbiter
|
79 |
+
self.dev_ini = dev_ini
|
80 |
+
self.verbose = verbose
|
81 |
+
|
82 |
+
self.dev = ''
|
83 |
+
self.turt = []
|
84 |
+
|
85 |
+
self.dimension = 2
|
86 |
+
|
87 |
+
self.rotation_3d_done = False
|
88 |
+
self.mobile_vectors: list[np.array] = [np.array([1.0, 0.0, 0.0]),
|
89 |
+
np.array([0.0, 1.0, 0.0]),
|
90 |
+
np.array([0.0, 0.0, 1.0])]
|
91 |
+
|
92 |
+
self.log('info', f"Axiom: {self.axiom:.50} ; Rules : {self.rules} ; Nb iterations : {self.nbiter}")
|
93 |
+
|
94 |
+
if self.dev_ini:
|
95 |
+
self.develop()
|
96 |
+
self.log('info', f"Axiom: {self.axiom:.50} ; Rules : {self.rules} ; "
|
97 |
+
f"Nb iterations : {self.nbiter} Expanded value : {self.dev[:50]+'...'} ; After")
|
98 |
+
|
99 |
+
@staticmethod
|
100 |
+
def apply_rot(rot: Rotation, vec: np.array) -> np.array:
|
101 |
+
"""
|
102 |
+
Apply a rotation on a vector
|
103 |
+
|
104 |
+
:param rot: rotation to apply
|
105 |
+
:param vec: concerned vector
|
106 |
+
:return: rotated vector
|
107 |
+
"""
|
108 |
+
return rot.apply(vec).round(decimals=6)
|
109 |
+
|
110 |
+
@staticmethod
|
111 |
+
def dev_unit(source: str, rules: list[tuple[str, str]]) -> str:
|
112 |
+
"""
|
113 |
+
Develop source with rules
|
114 |
+
"""
|
115 |
+
result = source
|
116 |
+
position = 0
|
117 |
+
lreg = None
|
118 |
+
|
119 |
+
while True:
|
120 |
+
# The leftmost usable rule is applied
|
121 |
+
newpos = None
|
122 |
+
for lr, regle in enumerate(rules):
|
123 |
+
lpos = result.find(regle[0], position)
|
124 |
+
if lpos >= 0:
|
125 |
+
if newpos is None or lpos < newpos:
|
126 |
+
newpos = lpos
|
127 |
+
lreg = lr
|
128 |
+
|
129 |
+
if newpos is None:
|
130 |
+
break
|
131 |
+
|
132 |
+
result = result[0:newpos] + result[newpos:].replace(rules[lreg][0], rules[lreg][1], 1)
|
133 |
+
position = newpos + len(rules[lreg][1])
|
134 |
+
|
135 |
+
return result
|
136 |
+
|
137 |
+
@staticmethod
|
138 |
+
def color_from_map(name: str, index: int) -> tuple[int, int, int]:
|
139 |
+
"""
|
140 |
+
:param name: name of the discrete colormap (matplotlib source) to be used
|
141 |
+
:param index: index of the color in the map
|
142 |
+
:return: tuple (red, green, blue)
|
143 |
+
"""
|
144 |
+
r, g, b = mpl.colormaps[name].colors[index]
|
145 |
+
r, g, b = int(r * 255), int(g * 255), int(b * 255)
|
146 |
+
|
147 |
+
return r, g, b
|
148 |
+
|
149 |
+
def log(self, ltype: str, message: str, *args, **kwargs):
|
150 |
+
"""
|
151 |
+
Log a message with consideration for the verbosity property
|
152 |
+
|
153 |
+
:param ltype: type of log
|
154 |
+
:param message: message
|
155 |
+
:return: None
|
156 |
+
"""
|
157 |
+
if self.verbose:
|
158 |
+
func_dict = {'info': logger.info, 'debug': logger.debug, 'warning': logger.warning,
|
159 |
+
'error': logger.error, 'exception': logger.exception}
|
160 |
+
|
161 |
+
func = func_dict.get(ltype)
|
162 |
+
if not func:
|
163 |
+
raise ValueError(f"This type of log is unknown : {ltype}")
|
164 |
+
|
165 |
+
func(message, *args, **kwargs)
|
166 |
+
|
167 |
+
def new_pos(self, ax: float, ay: float, az: float, astep: float, aangle: float) -> tuple[float, float, float]:
|
168 |
+
"""
|
169 |
+
New position from (ax, ay, az) with the use of astep and aangle
|
170 |
+
|
171 |
+
The angle is not used if a 3D rotation has been done
|
172 |
+
|
173 |
+
:param ax: 1st coordinate of starting point
|
174 |
+
:param ay: 2nd coordinate of starting point
|
175 |
+
:param az: 3rd coordinate of starting point
|
176 |
+
:param astep: step size
|
177 |
+
:param aangle: step angle
|
178 |
+
"""
|
179 |
+
if self.rotation_3d_done:
|
180 |
+
forward_vector = self.mobile_vectors[0]
|
181 |
+
lx = ax + astep * forward_vector[0]
|
182 |
+
ly = ay + astep * forward_vector[1]
|
183 |
+
lz = az + astep * forward_vector[2]
|
184 |
+
|
185 |
+
return lx, ly, lz
|
186 |
+
else:
|
187 |
+
if aangle == 0.0:
|
188 |
+
lx = ax + astep
|
189 |
+
ly = ay
|
190 |
+
elif aangle == 90.0:
|
191 |
+
lx = ax
|
192 |
+
ly = ay + astep
|
193 |
+
elif aangle == 180.0:
|
194 |
+
lx = ax - astep
|
195 |
+
ly = ay
|
196 |
+
elif aangle == 270.0:
|
197 |
+
lx = ax
|
198 |
+
ly = ay - astep
|
199 |
+
else:
|
200 |
+
lx = ax + astep * math.cos(math.radians(aangle))
|
201 |
+
ly = ay + astep * math.sin(math.radians(aangle))
|
202 |
+
|
203 |
+
return lx, ly, az
|
204 |
+
|
205 |
+
def develop(self) -> None:
|
206 |
+
"""
|
207 |
+
Develop self.axiom from the list of rules (self.rules) with nbiter iterations
|
208 |
+
|
209 |
+
A rule is a couple (source, target) where source can be replaced by target
|
210 |
+
|
211 |
+
Example of Koch :
|
212 |
+
axiom = 'F'
|
213 |
+
rules = [('F','F+F-F-F+F')]
|
214 |
+
|
215 |
+
Example with 2 rules :
|
216 |
+
axiom = 'A'
|
217 |
+
rules = [('A','AB'),('B','A')]
|
218 |
+
"""
|
219 |
+
result = self.axiom
|
220 |
+
|
221 |
+
if self.rules:
|
222 |
+
for _ in range(self.nbiter):
|
223 |
+
result = self.dev_unit(result, self.rules)
|
224 |
+
|
225 |
+
self.dev = result
|
226 |
+
|
227 |
+
def init_3d(self, angle: float) -> None:
|
228 |
+
"""
|
229 |
+
Initialization of the 3D and of the mobile vectors
|
230 |
+
|
231 |
+
:param angle: current angle
|
232 |
+
:return: None
|
233 |
+
"""
|
234 |
+
self.rotation_3d_done = True
|
235 |
+
self.dimension = 3
|
236 |
+
|
237 |
+
vec_x = self.mobile_vectors[0]
|
238 |
+
vec_y = self.mobile_vectors[1]
|
239 |
+
axis = self.mobile_vectors[2]
|
240 |
+
|
241 |
+
rot = Rotation.from_rotvec(angle * axis, degrees=True)
|
242 |
+
new_vec_x = self.apply_rot(rot, vec_x)
|
243 |
+
new_vec_y = self.apply_rot(rot, vec_y)
|
244 |
+
|
245 |
+
self.mobile_vectors[0] = new_vec_x
|
246 |
+
self.mobile_vectors[1] = new_vec_y
|
247 |
+
|
248 |
+
def rotate_3d(self, rtype: str, rangle: float) -> None:
|
249 |
+
"""
|
250 |
+
Apply a 3D rotation on the mobile vectors
|
251 |
+
|
252 |
+
:param rtype: type of rotation
|
253 |
+
:param rangle: angle of rotation
|
254 |
+
:return: None
|
255 |
+
"""
|
256 |
+
vec_x = self.mobile_vectors[0]
|
257 |
+
vec_y = self.mobile_vectors[1]
|
258 |
+
vec_z = self.mobile_vectors[2]
|
259 |
+
rsign = 1 if rtype in self.config.r3d_1_plus + self.config.r3d_2_plus + '+>' else -1
|
260 |
+
if rtype in self.config.r3d_1_minus + self.config.r3d_1_plus:
|
261 |
+
# Axis of rotation is "X"
|
262 |
+
axis = vec_x
|
263 |
+
rot = Rotation.from_rotvec(rsign * rangle * axis, degrees=True)
|
264 |
+
new_vec_x = axis
|
265 |
+
new_vec_y = self.apply_rot(rot, vec_y)
|
266 |
+
new_vec_z = self.apply_rot(rot, vec_z)
|
267 |
+
elif rtype in self.config.r3d_2_minus + self.config.r3d_2_plus:
|
268 |
+
# Axis of rotation is "Y"
|
269 |
+
axis = vec_y
|
270 |
+
rot = Rotation.from_rotvec(rsign * rangle * axis, degrees=True)
|
271 |
+
new_vec_x = self.apply_rot(rot, vec_x)
|
272 |
+
new_vec_y = axis
|
273 |
+
new_vec_z = self.apply_rot(rot, vec_z)
|
274 |
+
else:
|
275 |
+
# Axis of rotation is "Z" ( +->< )
|
276 |
+
axis = vec_z
|
277 |
+
rot = Rotation.from_rotvec(rsign * rangle * axis, degrees=True)
|
278 |
+
new_vec_x = self.apply_rot(rot, vec_x)
|
279 |
+
new_vec_y = self.apply_rot(rot, vec_y)
|
280 |
+
new_vec_z = axis
|
281 |
+
|
282 |
+
self.mobile_vectors[0] = new_vec_x
|
283 |
+
self.mobile_vectors[1] = new_vec_y
|
284 |
+
self.mobile_vectors[2] = new_vec_z
|
285 |
+
|
286 |
+
def turtle(self, step: float = 10.0, angle: float = 90.0, angleinit: float = 0.0, coeff: float = 1.1,
|
287 |
+
angle2: float = 10.0, color_length: int = 3, color_map: str = "Set1",
|
288 |
+
delta: float = 0.1) -> None:
|
289 |
+
"""
|
290 |
+
Develop self.dev in [(lx, ly, lz, color),...] where lx, ly, lz are lists of positions
|
291 |
+
The result goes to self.turt
|
292 |
+
|
293 |
+
:param step: the turtle step size
|
294 |
+
:param angle: angle of rotation (in degrees) ( + - )
|
295 |
+
:param angleinit: initial angle
|
296 |
+
:param coeff: magnification or reduction factor for the step ( * / ) and factor for "lowered" characters
|
297 |
+
:param angle2: 2nd usable angle ( < > )
|
298 |
+
:param color_length: maximal number of colours
|
299 |
+
:param color_map: color map to use (matplotlib name)
|
300 |
+
:param delta: value to add to the step
|
301 |
+
"""
|
302 |
+
res = []
|
303 |
+
stock: list = [] # List of ("point", angle, ...) kept for [] et ()
|
304 |
+
|
305 |
+
lix: list[float] = []
|
306 |
+
liy: list[float] = []
|
307 |
+
liz: list[float] = []
|
308 |
+
|
309 |
+
tx = 0.0
|
310 |
+
ty = 0.0
|
311 |
+
tz = 0.0
|
312 |
+
tstep = step
|
313 |
+
tangle = angleinit
|
314 |
+
tsens = 1
|
315 |
+
color_index = 0
|
316 |
+
tcouleur = self.color_from_map(color_map, color_index)
|
317 |
+
|
318 |
+
nb_iterations: int = 0
|
319 |
+
stock_outer: deque = deque([(tx, ty, tz, tangle, tcouleur, tstep, self.mobile_vectors)])
|
320 |
+
|
321 |
+
while stock_outer:
|
322 |
+
nb_iterations += 1
|
323 |
+
if len(lix) > 1:
|
324 |
+
res.append((lix, liy, liz, tcouleur))
|
325 |
+
|
326 |
+
tx, ty, tz, tangle, tcouleur, tstep, self.mobile_vectors = stock_outer.popleft()
|
327 |
+
lix = [tx]
|
328 |
+
liy = [ty]
|
329 |
+
liz = [tz]
|
330 |
+
|
331 |
+
for car in self.dev:
|
332 |
+
|
333 |
+
if car in self.config.total_skipped:
|
334 |
+
continue
|
335 |
+
|
336 |
+
npos = False
|
337 |
+
npospos = False
|
338 |
+
nliste = False
|
339 |
+
ncolor = False
|
340 |
+
|
341 |
+
if car in self.config.move_all:
|
342 |
+
if car in self.config.color:
|
343 |
+
ncolor = True # Change of color
|
344 |
+
else:
|
345 |
+
ltstep = tstep
|
346 |
+
|
347 |
+
if car in self.config.move_multi:
|
348 |
+
ltstep = tstep * coeff
|
349 |
+
elif car in self.config.move_angle_init:
|
350 |
+
tangle = angleinit
|
351 |
+
|
352 |
+
ltangle = tangle
|
353 |
+
|
354 |
+
tx, ty, tz = self.new_pos(tx, ty, tz, ltstep, ltangle)
|
355 |
+
|
356 |
+
# npos true <-> new position with the pen down
|
357 |
+
npos = car in self.config.move + self.config.move_multi + self.config.move_angle_init
|
358 |
+
|
359 |
+
# nliste true <-> new list because of a change of color or a raised pen
|
360 |
+
nliste = car in self.config.color + self.config.move_lifted_pen
|
361 |
+
elif car in self.config.move_up_3d or car in self.config.move_down_3d:
|
362 |
+
npos = True
|
363 |
+
self.dimension = 3
|
364 |
+
if car in self.config.move_up_3d:
|
365 |
+
tz += tstep
|
366 |
+
else:
|
367 |
+
tz -= tstep
|
368 |
+
elif car in '+-><' + self.config.r3d_all:
|
369 |
+
if self.rotation_3d_done:
|
370 |
+
if car in '+-' + self.config.r3d_all:
|
371 |
+
langle = angle
|
372 |
+
else:
|
373 |
+
langle = angle2
|
374 |
+
|
375 |
+
self.rotate_3d(car, langle * tsens)
|
376 |
+
else:
|
377 |
+
if car in '+':
|
378 |
+
tangle = (tangle + angle * tsens) % 360.0
|
379 |
+
elif car in '-':
|
380 |
+
tangle = (tangle - angle * tsens) % 360.0
|
381 |
+
elif car in '>':
|
382 |
+
tangle = (tangle + angle2 * tsens) % 360.0
|
383 |
+
elif car in '<':
|
384 |
+
tangle = (tangle - angle2 * tsens) % 360.0
|
385 |
+
else:
|
386 |
+
# There is a not trivial 3D rotation ( PMpm )
|
387 |
+
self.init_3d(tangle)
|
388 |
+
self.rotate_3d(car, angle * tsens)
|
389 |
+
|
390 |
+
elif car == '*':
|
391 |
+
tstep *= coeff
|
392 |
+
elif car == '/':
|
393 |
+
tstep /= coeff
|
394 |
+
elif car in self.config.delta_add:
|
395 |
+
tstep += delta
|
396 |
+
elif car in self.config.delta_sub:
|
397 |
+
tstep -= delta
|
398 |
+
elif car in '[(':
|
399 |
+
stock.append((tx, ty, tz, tangle, tcouleur, tstep, self.mobile_vectors))
|
400 |
+
elif car in '])':
|
401 |
+
if stock:
|
402 |
+
tx, ty, tz, tangle, tcouleur, tstep, self.mobile_vectors = stock.pop()
|
403 |
+
nliste = True # the pen is raised to go back to the stocked position
|
404 |
+
elif car == '|':
|
405 |
+
# Single "return" ("round-trip")
|
406 |
+
npospos = True
|
407 |
+
elif car == '!':
|
408 |
+
# Change the sens of rotation
|
409 |
+
tsens = tsens * -1
|
410 |
+
elif car in self.config.outer_repetition:
|
411 |
+
# A new possible item in the "outer stock"
|
412 |
+
if nb_iterations <= self.config.outer_repetition_max:
|
413 |
+
stock_outer.append((tx, ty, tz, tangle, tcouleur, tstep, self.mobile_vectors))
|
414 |
+
else:
|
415 |
+
continue
|
416 |
+
|
417 |
+
# Take into account the read character
|
418 |
+
# ------------------------------------
|
419 |
+
if nliste:
|
420 |
+
# New list because of new color or lifted pen
|
421 |
+
if len(lix) > 1:
|
422 |
+
res.append((lix, liy, liz, tcouleur))
|
423 |
+
|
424 |
+
if ncolor:
|
425 |
+
# Change of color
|
426 |
+
color_index = (color_index + 1) % color_length
|
427 |
+
tcouleur = self.color_from_map(color_map, color_index)
|
428 |
+
|
429 |
+
lix = [tx]
|
430 |
+
liy = [ty]
|
431 |
+
liz = [tz]
|
432 |
+
elif npos:
|
433 |
+
# New position and no new list
|
434 |
+
lix.append(tx)
|
435 |
+
liy.append(ty)
|
436 |
+
liz.append(tz)
|
437 |
+
elif npospos:
|
438 |
+
# 2 new positions for a "round-trip"
|
439 |
+
tnx, tny, tnz = self.new_pos(tx, ty, tz, tstep, tangle)
|
440 |
+
|
441 |
+
lix.append(tnx)
|
442 |
+
liy.append(tny)
|
443 |
+
liz.append(tnz)
|
444 |
+
|
445 |
+
lix.append(tx)
|
446 |
+
liy.append(ty)
|
447 |
+
liz.append(tz)
|
448 |
+
|
449 |
+
if len(lix) > 1:
|
450 |
+
# Finally, append the last points
|
451 |
+
res.append((lix, liy, liz, tcouleur))
|
452 |
+
|
453 |
+
self.turt = res
|
454 |
+
|
455 |
+
def render(self, show_type: str = 'matplot', image_destination: str = 'images_out/',
|
456 |
+
save_files: bool = True, show_more: bool = False, show_3d: bool = False,
|
457 |
+
return_type: str = ''):
|
458 |
+
"""
|
459 |
+
Render self.turt using a specific show type
|
460 |
+
|
461 |
+
:param show_type: 'matplot' or 'bokeh'
|
462 |
+
:param image_destination: folder for images backup
|
463 |
+
:param save_files: True to save files
|
464 |
+
:param show_more: True to show with specific show_type
|
465 |
+
:param show_3d: True to show 3D (implemented with plotly only)
|
466 |
+
:param return_type: '', 'image' or 'figure'
|
467 |
+
:return: None or an image if return_type is 'image' or a figure if return_type is 'figure'
|
468 |
+
"""
|
469 |
+
if show_type == 'matplot':
|
470 |
+
fig, ax = plt.subplots()
|
471 |
+
|
472 |
+
for (lx, ly, _, coul) in self.turt:
|
473 |
+
r, g, b = coul
|
474 |
+
ax.plot(lx, ly, color=(r / 255., g / 255., b / 255., 1.0))
|
475 |
+
|
476 |
+
ax.set_axis_off()
|
477 |
+
ax.grid(visible=False)
|
478 |
+
|
479 |
+
if show_more:
|
480 |
+
plt.show()
|
481 |
+
|
482 |
+
if save_files:
|
483 |
+
fig.savefig(f'{image_destination}plot_{show_type}.png', bbox_inches='tight')
|
484 |
+
fig.savefig(f'{image_destination}plot_{show_type}.svg', bbox_inches='tight')
|
485 |
+
|
486 |
+
if return_type == 'image':
|
487 |
+
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
|
488 |
+
fig.canvas.draw()
|
489 |
+
|
490 |
+
# Return an image : PIL.Image
|
491 |
+
return pimage.frombytes('RGB', fig.canvas.get_width_height(), fig.canvas.tostring_rgb())
|
492 |
+
|
493 |
+
if return_type == 'figure':
|
494 |
+
return fig
|
495 |
+
|
496 |
+
elif show_type == 'bokeh':
|
497 |
+
if save_files:
|
498 |
+
output_file(f'{image_destination}lines_{show_type}.html')
|
499 |
+
|
500 |
+
fig = figure(title="LSyst", x_axis_label='x', y_axis_label='y', width=800, height=800)
|
501 |
+
|
502 |
+
for (lx, ly, _, coul) in self.turt:
|
503 |
+
cr, cg, cb = coul
|
504 |
+
fig.line(lx, ly, line_color=(cr, cg, cb))
|
505 |
+
|
506 |
+
fig.xgrid.grid_line_color = None
|
507 |
+
fig.ygrid.grid_line_color = None
|
508 |
+
|
509 |
+
if show_more:
|
510 |
+
_ = show(fig)
|
511 |
+
|
512 |
+
if save_files:
|
513 |
+
export_png(fig, filename=f'{image_destination}plot_{show_type}.png')
|
514 |
+
|
515 |
+
fig.output_backend = "svg"
|
516 |
+
export_svgs(fig, filename=f'{image_destination}plot_{show_type}.svg')
|
517 |
+
|
518 |
+
if return_type == 'image':
|
519 |
+
fig.toolbar_location = None
|
520 |
+
fig.axis.visible = False
|
521 |
+
fig.title = ""
|
522 |
+
|
523 |
+
# Return an image : PIL.Image
|
524 |
+
return get_screenshot_as_png(fig)
|
525 |
+
|
526 |
+
if return_type == 'figure':
|
527 |
+
return fig
|
528 |
+
|
529 |
+
elif show_type == 'plotly':
|
530 |
+
fig = go.Figure()
|
531 |
+
|
532 |
+
axis_dict = {
|
533 |
+
"showline": True,
|
534 |
+
"showgrid": False,
|
535 |
+
"showticklabels": True,
|
536 |
+
"zeroline": False,
|
537 |
+
"ticks": 'outside',
|
538 |
+
}
|
539 |
+
|
540 |
+
index = 0
|
541 |
+
|
542 |
+
if self.dimension == 2 or not show_3d:
|
543 |
+
fig.update_yaxes(
|
544 |
+
scaleanchor="x",
|
545 |
+
scaleratio=1,
|
546 |
+
)
|
547 |
+
for (lx, ly, lz, coul) in self.turt:
|
548 |
+
index += 1
|
549 |
+
cr, cg, cb = coul
|
550 |
+
fig.add_trace(go.Scatter(x=lx, y=ly, mode='lines',
|
551 |
+
name=f"t{index}", line={"color": f'rgb({cr},{cg},{cb})', "width": 1}))
|
552 |
+
|
553 |
+
else:
|
554 |
+
# 3D
|
555 |
+
for (lx, ly, lz, coul) in self.turt:
|
556 |
+
index += 1
|
557 |
+
cr, cg, cb = coul
|
558 |
+
fig.add_trace(go.Scatter3d(x=lx, y=ly, z=lz, mode='lines',
|
559 |
+
name=f"t{index}", line={"color": f'rgb({cr},{cg},{cb})', "width": 1}))
|
560 |
+
|
561 |
+
fig.update_layout(
|
562 |
+
xaxis=axis_dict,
|
563 |
+
yaxis=axis_dict,
|
564 |
+
autosize=True,
|
565 |
+
showlegend=False
|
566 |
+
)
|
567 |
+
|
568 |
+
if show_more:
|
569 |
+
fig.show()
|
570 |
+
|
571 |
+
if save_files:
|
572 |
+
fig.write_image(f'{image_destination}plot_{show_type}.png')
|
573 |
+
fig.write_image(f'{image_destination}plot_{show_type}.svg')
|
574 |
+
|
575 |
+
if return_type == 'image':
|
576 |
+
fig_bytes = fig.to_image(format="png")
|
577 |
+
buf = io.BytesIO(fig_bytes)
|
578 |
+
|
579 |
+
# Return an image : PIL.Image
|
580 |
+
return pimage.open(buf)
|
581 |
+
|
582 |
+
if return_type == 'figure':
|
583 |
+
return fig
|
584 |
+
|
585 |
+
else:
|
586 |
+
raise ValueError("The given show_type is not correct")
|
requirements.txt
CHANGED
@@ -1 +1,13 @@
|
|
1 |
# requirements apart from streamlit (sdk)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# requirements apart from streamlit (sdk)
|
2 |
+
attrs
|
3 |
+
bokeh
|
4 |
+
kaleido
|
5 |
+
loguru
|
6 |
+
matplotlib
|
7 |
+
mpld3
|
8 |
+
numpy
|
9 |
+
Pillow
|
10 |
+
plotly
|
11 |
+
pyyaml
|
12 |
+
scipy
|
13 |
+
selenium
|
requirements_dev.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
mypy
|
2 |
+
pylint
|
3 |
+
pytest
|
4 |
+
types-Pillow
|
5 |
+
types-PyYAML
|
requirements_other.txt
CHANGED
@@ -1 +1,2 @@
|
|
|
|
1 |
streamlit
|
|
|
1 |
+
# possible sdk requirements
|
2 |
streamlit
|
specific_values.py
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Specific parameters and values to customize the look of the app
|
3 |
+
|
4 |
+
The possible color maps are
|
5 |
+
['Pastel1', 'Pastel2', 'Paired', 'Accent', 'Dark2', 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b','tab20c']
|
6 |
+
|
7 |
+
"""
|
8 |
+
|
9 |
+
# Initial value
|
10 |
+
redraw_auto = True
|
11 |
+
|
12 |
+
# Turtle parameters
|
13 |
+
step = 10.0
|
14 |
+
|
15 |
+
# Number of the maximum of colors and color map
|
16 |
+
color_length = 3
|
17 |
+
color_map = "Set1"
|
18 |
+
|
19 |
+
# Other
|
20 |
+
renderer = 'plotly' # 'matplot' or 'bokeh' or 'plotly'
|
21 |
+
return_type = 'figure' # 'figure' or 'image'
|
22 |
+
if (renderer, return_type) not in [('matplot', 'image'), ('matplot', 'figure'),
|
23 |
+
('plotly', 'image'), ('plotly', 'figure'),
|
24 |
+
('bokeh', 'image')]:
|
25 |
+
raise NotImplementedError("The current combination of renderer and return_type is not implemented")
|
26 |
+
|
27 |
+
save_files = False
|
28 |
+
verbose = False # Set to true to see more logs
|
29 |
+
show_more = False # Set to true to see more details "locally"
|
30 |
+
show_3d = True
|
test_app.py
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pytest
|
2 |
+
from streamlit.testing.v1 import AppTest
|
3 |
+
|
4 |
+
|
5 |
+
@pytest.mark.filterwarnings("ignore:coroutine")
|
6 |
+
def test_app():
|
7 |
+
at = AppTest.from_file("app.py").run()
|
8 |
+
at.button[0].click().run()
|
test_curves.py
ADDED
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import lsystc as ls
|
2 |
+
import pytest
|
3 |
+
from numpy.testing import assert_allclose
|
4 |
+
|
5 |
+
|
6 |
+
@pytest.mark.parametrize("axiom, rules, expected_result", [
|
7 |
+
('A', [('A', 'AB'), ('B', 'BB')], 'AB'),
|
8 |
+
('AB', [('A', 'AB'), ('B', 'BB')], 'ABBB'),
|
9 |
+
])
|
10 |
+
def test_single_iteration(axiom, rules, expected_result):
|
11 |
+
syst = ls.Lsystc(ls.Config(), axiom, rules, nbiter=1)
|
12 |
+
assert syst.dev == expected_result
|
13 |
+
|
14 |
+
|
15 |
+
@pytest.mark.parametrize("axiom, rules, nbiterations, expected_result", [
|
16 |
+
('A', [('A', 'AB'), ('B', 'BB')], 2, 'ABBB'),
|
17 |
+
('AB', [('A', 'AB'), ('B', 'BB')], 3, 'ABBBBBBBBBBBBBBB'),
|
18 |
+
])
|
19 |
+
def test_more_iterations(axiom, rules, nbiterations, expected_result):
|
20 |
+
syst = ls.Lsystc(ls.Config(), axiom, rules, nbiter=nbiterations)
|
21 |
+
assert syst.dev == expected_result
|
22 |
+
|
23 |
+
|
24 |
+
@pytest.mark.parametrize("axiom, expected_result", [
|
25 |
+
('F', [([0, 10], [0, 0], [0, 0], (228, 26, 28))]),
|
26 |
+
('F+F', [([0, 10, 10], [0, 0, 10], [0, 0, 0], (228, 26, 28))]),
|
27 |
+
])
|
28 |
+
def test_turt(axiom, expected_result):
|
29 |
+
syst = ls.Lsystc(ls.Config(), axiom, [], nbiter=1)
|
30 |
+
syst.turtle()
|
31 |
+
assert syst.turt == expected_result
|
32 |
+
|
33 |
+
|
34 |
+
@pytest.mark.parametrize("axiom, expected_result", [
|
35 |
+
('⇧F', [([0, 0, 10], [0, 0, 0], [0, 10, 10], (228, 26, 28))]),
|
36 |
+
('⇩F', [([0, 0, 10], [0, 0, 0], [0, -10, -10], (228, 26, 28))]),
|
37 |
+
])
|
38 |
+
def test_turt_3d_up_down(axiom, expected_result):
|
39 |
+
syst = ls.Lsystc(ls.Config(), axiom, [], nbiter=1)
|
40 |
+
syst.turtle()
|
41 |
+
assert syst.turt == expected_result
|
42 |
+
|
43 |
+
|
44 |
+
@pytest.mark.parametrize("axiom, expected_result", [
|
45 |
+
('m+F', [([0.0, 0.0], [0.0, 0.0], [0.0, -10.0], (228, 26, 28))]),
|
46 |
+
('p+F', [([0.0, 0.0], [0.0, 0.0], [0.0, 10.0], (228, 26, 28))]),
|
47 |
+
])
|
48 |
+
def test_turt_3d_rotation_1(axiom, expected_result):
|
49 |
+
syst = ls.Lsystc(ls.Config(), axiom, [], nbiter=1)
|
50 |
+
syst.turtle()
|
51 |
+
|
52 |
+
lxr = syst.turt[0][0]
|
53 |
+
lxe = expected_result[0][0]
|
54 |
+
|
55 |
+
assert_allclose(lxr, lxe, atol=1e-6)
|
56 |
+
|
57 |
+
lyr = syst.turt[0][1]
|
58 |
+
lye = expected_result[0][1]
|
59 |
+
|
60 |
+
assert_allclose(lyr, lye, atol=1e-6)
|
61 |
+
|
62 |
+
lzr = syst.turt[0][2]
|
63 |
+
lze = expected_result[0][2]
|
64 |
+
|
65 |
+
assert_allclose(lzr, lze, atol=1e-6)
|
66 |
+
|
67 |
+
lcr = syst.turt[0][3]
|
68 |
+
lce = expected_result[0][3]
|
69 |
+
|
70 |
+
assert lcr == lce
|
71 |
+
|
72 |
+
|
73 |
+
@pytest.mark.parametrize("axiom, expected_result", [
|
74 |
+
('MF', [([0.0, 0.0], [0.0, 0.0], [0.0, 10.0], (228, 26, 28))]),
|
75 |
+
('PF', [([0.0, 0.0], [0.0, 0.0], [0.0, -10.0], (228, 26, 28))]),
|
76 |
+
])
|
77 |
+
def test_turt_3d_rotation_2(axiom, expected_result):
|
78 |
+
syst = ls.Lsystc(ls.Config(), axiom, [], nbiter=1)
|
79 |
+
syst.turtle()
|
80 |
+
|
81 |
+
lxr = syst.turt[0][0]
|
82 |
+
lxe = expected_result[0][0]
|
83 |
+
|
84 |
+
assert_allclose(lxr, lxe, atol=1e-6)
|
85 |
+
|
86 |
+
lyr = syst.turt[0][1]
|
87 |
+
lye = expected_result[0][1]
|
88 |
+
|
89 |
+
assert_allclose(lyr, lye, atol=1e-6)
|
90 |
+
|
91 |
+
lzr = syst.turt[0][2]
|
92 |
+
lze = expected_result[0][2]
|
93 |
+
|
94 |
+
assert_allclose(lzr, lze, atol=1e-6)
|
95 |
+
|
96 |
+
lcr = syst.turt[0][3]
|
97 |
+
lce = expected_result[0][3]
|
98 |
+
|
99 |
+
assert lcr == lce
|
100 |
+
|
101 |
+
|
102 |
+
@pytest.mark.parametrize("axiom, expected_result", [
|
103 |
+
('uF', [([0, 10.1], [0, 0], [0, 0], (228, 26, 28))]),
|
104 |
+
('vF', [([0, 9.9], [0, 0], [0, 0], (228, 26, 28))]),
|
105 |
+
])
|
106 |
+
def test_turt_delta(axiom, expected_result):
|
107 |
+
syst = ls.Lsystc(ls.Config(), axiom, [], nbiter=1)
|
108 |
+
syst.turtle()
|
109 |
+
assert syst.turt == expected_result
|