HANSOL commited on
Commit
857bbdd
ยท
1 Parent(s): 0f8040e

Fix landform animations: barchan, U-valley, volcanoes, lava plateau; add Lab page, zone overlay

Browse files
app/components/animation_renderer.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๐ŸŽฌ Plotly Animation Renderer
3
+ ๋ถ€๋“œ๋Ÿฌ์šด 3D ์ง€ํ˜• ์• ๋‹ˆ๋ฉ”์ด์…˜ (์นด๋ฉ”๋ผ ์œ ์ง€)
4
+ """
5
+ import numpy as np
6
+ import plotly.graph_objects as go
7
+ from typing import Callable
8
+
9
+
10
+ def create_animated_terrain_figure(
11
+ landform_func: Callable,
12
+ grid_size: int = 80,
13
+ num_frames: int = 25,
14
+ title: str = "์ง€ํ˜• ํ˜•์„ฑ ๊ณผ์ •",
15
+ landform_type: str = "river"
16
+ ) -> go.Figure:
17
+ """Plotly ๋„ค์ดํ‹ฐ๋ธŒ ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ๋ถ€๋“œ๋Ÿฌ์šด 3D ์ง€ํ˜• ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ
18
+
19
+ Args:
20
+ landform_func: ์ง€ํ˜• ์ƒ์„ฑ ํ•จ์ˆ˜ (grid_size, stage) -> elevation
21
+ grid_size: ๊ทธ๋ฆฌ๋“œ ํฌ๊ธฐ
22
+ num_frames: ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ”„๋ ˆ์ž„ ์ˆ˜ (๋งŽ์„์ˆ˜๋ก ๋ถ€๋“œ๋Ÿฌ์›€)
23
+ title: ๊ทธ๋ž˜ํ”„ ์ œ๋ชฉ
24
+ landform_type: ์ง€ํ˜• ์œ ํ˜• (colorscale ๊ฒฐ์ •)
25
+
26
+ Returns:
27
+ go.Figure: ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ํฌํ•จ๋œ Plotly Figure
28
+ """
29
+ h, w = grid_size, grid_size
30
+ x = np.arange(w)
31
+ y = np.arange(h)
32
+
33
+ # ์ปฌ๋Ÿฌ์Šค์ผ€์ผ ์„ค์ •
34
+ colorscale = _get_colorscale(landform_type)
35
+
36
+ # ๋ชจ๋“  ํ”„๋ ˆ์ž„ ๋ฏธ๋ฆฌ ์ƒ์„ฑ
37
+ frames = []
38
+ all_elevations = []
39
+
40
+ for i in range(num_frames):
41
+ stage = i / (num_frames - 1)
42
+
43
+ try:
44
+ # return_metadata ์ง€์› ์—ฌ๋ถ€ ํ™•์ธ
45
+ result = landform_func(grid_size, stage)
46
+ if isinstance(result, tuple):
47
+ elevation = result[0]
48
+ else:
49
+ elevation = result
50
+ except:
51
+ elevation = landform_func(grid_size, stage)
52
+
53
+ all_elevations.append(elevation)
54
+
55
+ # Biome ๊ณ„์‚ฐ (๊ฐ„์†Œํ™”)
56
+ dy, dx = np.gradient(elevation)
57
+ slope = np.sqrt(dx**2 + dy**2)
58
+ biome = np.ones_like(elevation) # ๊ธฐ๋ณธ: ํ’€
59
+ biome[slope > 1.2] = 2 # ์•”์„
60
+ biome[elevation < 5] = 0 # ๋ฌผ/๋ชจ๋ž˜
61
+
62
+ frames.append(go.Frame(
63
+ data=[go.Surface(
64
+ z=elevation,
65
+ x=x, y=y,
66
+ surfacecolor=biome,
67
+ colorscale=colorscale,
68
+ cmin=0, cmax=3,
69
+ showscale=False,
70
+ lighting=dict(ambient=0.4, diffuse=0.5, roughness=0.9, specular=0.1)
71
+ )],
72
+ name=f"{int(stage * 100)}%"
73
+ ))
74
+
75
+ # ์ดˆ๊ธฐ ํ”„๋ ˆ์ž„ (stage=0)
76
+ initial_elevation = all_elevations[0]
77
+ dy, dx = np.gradient(initial_elevation)
78
+ slope = np.sqrt(dx**2 + dy**2)
79
+ initial_biome = np.ones_like(initial_elevation)
80
+ initial_biome[slope > 1.2] = 2
81
+ initial_biome[initial_elevation < 5] = 0
82
+
83
+ fig = go.Figure(
84
+ data=[go.Surface(
85
+ z=initial_elevation,
86
+ x=x, y=y,
87
+ surfacecolor=initial_biome,
88
+ colorscale=colorscale,
89
+ cmin=0, cmax=3,
90
+ showscale=False,
91
+ lighting=dict(ambient=0.4, diffuse=0.5, roughness=0.9, specular=0.1)
92
+ )],
93
+ frames=frames
94
+ )
95
+
96
+ # ์Šฌ๋ผ์ด๋” (ํ”„๋ ˆ์ž„ ์ด๋™์šฉ)
97
+ sliders = [{
98
+ 'active': 0,
99
+ 'yanchor': 'top',
100
+ 'xanchor': 'left',
101
+ 'currentvalue': {
102
+ 'font': {'size': 14, 'color': 'white'},
103
+ 'prefix': 'ํ˜•์„ฑ ๋‹จ๊ณ„: ',
104
+ 'suffix': '',
105
+ 'visible': True,
106
+ 'xanchor': 'center'
107
+ },
108
+ 'transition': {'duration': 50, 'easing': 'cubic-in-out'},
109
+ 'pad': {'b': 10, 't': 50},
110
+ 'len': 0.9,
111
+ 'x': 0.05,
112
+ 'y': 0,
113
+ 'steps': [
114
+ {
115
+ 'args': [[f.name], {'frame': {'duration': 50, 'redraw': True}, 'mode': 'immediate', 'transition': {'duration': 50}}],
116
+ 'label': f.name,
117
+ 'method': 'animate'
118
+ }
119
+ for f in frames
120
+ ]
121
+ }]
122
+
123
+ # ์žฌ์ƒ/์ •์ง€ ๋ฒ„ํŠผ
124
+ updatemenus = [{
125
+ 'type': 'buttons',
126
+ 'showactive': False,
127
+ 'y': 1.15,
128
+ 'x': 0.05,
129
+ 'xanchor': 'left',
130
+ 'yanchor': 'top',
131
+ 'pad': {'t': 0, 'r': 10},
132
+ 'buttons': [
133
+ {
134
+ 'label': 'โ–ถ๏ธ ์žฌ์ƒ',
135
+ 'method': 'animate',
136
+ 'args': [
137
+ None,
138
+ {
139
+ 'frame': {'duration': 150, 'redraw': True},
140
+ 'fromcurrent': True,
141
+ 'transition': {'duration': 100, 'easing': 'quadratic-in-out'}
142
+ }
143
+ ]
144
+ },
145
+ {
146
+ 'label': 'โธ๏ธ ์ •์ง€',
147
+ 'method': 'animate',
148
+ 'args': [
149
+ [None],
150
+ {
151
+ 'frame': {'duration': 0, 'redraw': False},
152
+ 'mode': 'immediate',
153
+ 'transition': {'duration': 0}
154
+ }
155
+ ]
156
+ },
157
+ {
158
+ 'label': 'โฎ๏ธ ์ฒ˜์Œ',
159
+ 'method': 'animate',
160
+ 'args': [
161
+ ['0%'],
162
+ {
163
+ 'frame': {'duration': 0, 'redraw': True},
164
+ 'mode': 'immediate',
165
+ 'transition': {'duration': 0}
166
+ }
167
+ ]
168
+ }
169
+ ]
170
+ }]
171
+ # ์ง€ํ˜• ์œ ํ˜•๋ณ„ ์ตœ์  ์นด๋ฉ”๋ผ ๊ฐ๋„
172
+ camera_settings = _get_optimal_camera(landform_type)
173
+
174
+ # ๋ ˆ์ด์•„์›ƒ
175
+ fig.update_layout(
176
+ title=dict(text=title, font=dict(color='white', size=16)),
177
+ scene=dict(
178
+ xaxis=dict(title='X (m)', backgroundcolor='#1a1a2e', gridcolor='#444', color='#cccccc'),
179
+ yaxis=dict(title='Y (m)', backgroundcolor='#1a1a2e', gridcolor='#444', color='#cccccc'),
180
+ zaxis=dict(title='Elevation', backgroundcolor='#1a1a2e', gridcolor='#444', color='#cccccc'),
181
+ bgcolor='#0e1117',
182
+ camera=camera_settings,
183
+ aspectmode='manual',
184
+ aspectratio=dict(x=1, y=1, z=0.4)
185
+ ),
186
+ paper_bgcolor='#0e1117',
187
+ plot_bgcolor='#0e1117',
188
+ height=700,
189
+ margin=dict(l=10, r=10, t=80, b=80),
190
+ updatemenus=updatemenus,
191
+ sliders=sliders
192
+ )
193
+
194
+ return fig
195
+
196
+
197
+ def _get_colorscale(landform_type: str):
198
+ """์ง€ํ˜• ์œ ํ˜•์— ๋”ฐ๋ฅธ ์ปฌ๋Ÿฌ์Šค์ผ€์ผ ๋ฐ˜ํ™˜"""
199
+ if landform_type == 'glacial':
200
+ return [
201
+ [0.0, '#4682B4'], [0.33, '#4682B4'],
202
+ [0.33, '#556B2F'], [0.66, '#556B2F'],
203
+ [0.66, '#808080'], [1.0, '#E0FFFF']
204
+ ]
205
+ elif landform_type in ['river', 'coastal']:
206
+ return [
207
+ [0.0, '#4682B4'], [0.33, '#4682B4'],
208
+ [0.33, '#556B2F'], [0.66, '#556B2F'],
209
+ [0.66, '#808080'], [1.0, '#D2B48C']
210
+ ]
211
+ elif landform_type == 'arid':
212
+ return [
213
+ [0.0, '#EDC9AF'], [0.33, '#EDC9AF'],
214
+ [0.33, '#CD853F'], [0.66, '#CD853F'],
215
+ [0.66, '#808080'], [1.0, '#DAA520']
216
+ ]
217
+ else:
218
+ return [
219
+ [0.0, '#E6C288'], [0.33, '#E6C288'],
220
+ [0.33, '#556B2F'], [0.66, '#556B2F'],
221
+ [0.66, '#808080'], [1.0, '#A0522D']
222
+ ]
223
+
224
+
225
+ def _get_optimal_camera(landform_type: str) -> dict:
226
+ """์ง€ํ˜• ์œ ํ˜•๋ณ„ ์ตœ์  ์นด๋ฉ”๋ผ ๊ฐ๋„ ๋ฐ˜ํ™˜
227
+
228
+ ๊ฐ ์ง€ํ˜• ์œ ํ˜•์˜ ํ˜•์„ฑ ๊ณผ์ •์ด ์ž˜ ๋ณด์ด๋Š” ๊ฐ๋„๋กœ ์„ค์ •
229
+ """
230
+ if landform_type == 'river':
231
+ # ํ•˜์ฒœ/์„ ์ƒ์ง€: ์‚ฐ์ชฝ(์ƒ๋ฅ˜)์—์„œ ํ‰์ง€(ํ•˜๋ฅ˜) ๋ฐฉํ–ฅ์œผ๋กœ ๋‚ด๋ ค๋‹ค๋ด„
232
+ # ์„ ์ƒ์ง€๊ฐ€ ๋ถ€์ฑ„๊ผด๋กœ ํŽผ์ณ์ง€๋Š” ๋ชจ์Šต์ด ์ž˜ ๋ณด์ด๋Š” ๊ฐ๋„
233
+ return dict(
234
+ eye=dict(x=-0.3, y=-2.2, z=1.8),
235
+ center=dict(x=0, y=0.2, z=-0.2),
236
+ up=dict(x=0, y=0, z=1)
237
+ )
238
+ elif landform_type == 'glacial':
239
+ # ๋น™ํ•˜: ์œ„์—์„œ ๋‚ด๋ ค๋‹ค๋ณด๋Š” ๊ฐ๋„๋กœ U์ž๊ณก/๊ถŒ๊ณก ์ž˜ ๋ณด์ด๊ฒŒ
240
+ return dict(
241
+ eye=dict(x=0.8, y=-1.5, z=1.5),
242
+ center=dict(x=0, y=0, z=-0.2),
243
+ up=dict(x=0, y=0, z=1)
244
+ )
245
+ elif landform_type == 'volcanic':
246
+ # ํ™”์‚ฐ: ์ธก๋ฉด์—์„œ ๋ด์„œ ์‚ฐ์ฒด ํ˜•ํƒœ ์ž˜ ๋ณด์ด๊ฒŒ
247
+ return dict(
248
+ eye=dict(x=1.8, y=-1.2, z=0.8),
249
+ center=dict(x=0, y=0, z=0),
250
+ up=dict(x=0, y=0, z=1)
251
+ )
252
+ elif landform_type == 'coastal':
253
+ # ํ•ด์•ˆ: ๋ฐ”๋‹คโ†’์œก์ง€ ๋ฐฉํ–ฅ์œผ๋กœ ์ ˆ๋ฒฝ ์ž˜ ๋ณด์ด๊ฒŒ
254
+ return dict(
255
+ eye=dict(x=0.5, y=2.0, z=0.8),
256
+ center=dict(x=0, y=0, z=-0.1),
257
+ up=dict(x=0, y=0, z=1)
258
+ )
259
+ elif landform_type == 'arid':
260
+ # ๊ฑด์กฐ: ์‚ฌ๊ตฌ ํ˜•ํƒœ ์ž˜ ๋ณด์ด๊ฒŒ ๋‚ฎ์€ ๊ฐ๋„
261
+ return dict(
262
+ eye=dict(x=2.0, y=-1.0, z=0.6),
263
+ center=dict(x=0, y=0, z=-0.1),
264
+ up=dict(x=0, y=0, z=1)
265
+ )
266
+ elif landform_type == 'karst':
267
+ # ์นด๋ฅด์ŠคํŠธ: ์œ„์—์„œ ๋Œ๋ฆฌ๋„ค/์šฐ๋ฐœ๋ผ ์ž˜ ๋ณด์ด๊ฒŒ
268
+ return dict(
269
+ eye=dict(x=1.0, y=-1.0, z=1.8),
270
+ center=dict(x=0, y=0, z=-0.2),
271
+ up=dict(x=0, y=0, z=1)
272
+ )
273
+ else:
274
+ # ๊ธฐ๋ณธ๊ฐ’: ๋Œ€๊ฐ์„  ๋ฐฉํ–ฅ
275
+ return dict(
276
+ eye=dict(x=1.5, y=-1.5, z=1.0),
277
+ center=dict(x=0, y=0, z=-0.1),
278
+ up=dict(x=0, y=0, z=1)
279
+ )
280
+
281
+
282
+ def get_multi_angle_cameras() -> dict:
283
+ """๋‹ค์ค‘ ์‹œ์  ์นด๋ฉ”๋ผ ํ”„๋ฆฌ์…‹
284
+
285
+ X์ถ•(์ •๋ฉด), Y์ถ•(์ธก๋ฉด), Z์ถ•(ํ‰๋ฉด๋„), ๋“ฑ๊ฐํˆฌ์˜ 4๊ฐ€์ง€ ์‹œ์ 
286
+ """
287
+ return {
288
+ "๐ŸŽฏ ๋“ฑ๊ฐ (๊ธฐ๋ณธ)": dict(
289
+ eye=dict(x=1.5, y=-1.5, z=1.2),
290
+ center=dict(x=0, y=0, z=-0.1),
291
+ up=dict(x=0, y=0, z=1)
292
+ ),
293
+ "โžก๏ธ X์ถ• (์ธก๋ฉด)": dict(
294
+ eye=dict(x=2.5, y=0, z=0.3),
295
+ center=dict(x=0, y=0, z=0),
296
+ up=dict(x=0, y=0, z=1)
297
+ ),
298
+ "โฌ†๏ธ Y์ถ• (์ •๋ฉด)": dict(
299
+ eye=dict(x=0, y=-2.5, z=0.3),
300
+ center=dict(x=0, y=0, z=0),
301
+ up=dict(x=0, y=0, z=1)
302
+ ),
303
+ "โฌ‡๏ธ Z์ถ• (ํ‰๋ฉด๋„)": dict(
304
+ eye=dict(x=0, y=0, z=2.5),
305
+ center=dict(x=0, y=0, z=0),
306
+ up=dict(x=0, y=1, z=0)
307
+ ),
308
+ "๐Ÿ”„ ๋Œ€๊ฐ์„  ๋‚ฎ์Œ": dict(
309
+ eye=dict(x=2.0, y=-2.0, z=0.5),
310
+ center=dict(x=0, y=0, z=-0.1),
311
+ up=dict(x=0, y=0, z=1)
312
+ ),
313
+ "๐ŸŒ„ ์ƒ๋ฅ˜โ†’ํ•˜๋ฅ˜": dict(
314
+ eye=dict(x=-0.3, y=-2.5, z=1.5),
315
+ center=dict(x=0, y=0.2, z=-0.2),
316
+ up=dict(x=0, y=0, z=1)
317
+ )
318
+ }
engine/ideal_landforms.py CHANGED
@@ -722,80 +722,150 @@ def create_meander_animated(grid_size: int, stage: float,
722
 
723
 
724
  def create_u_valley_animated(grid_size: int, stage: float,
725
- valley_depth: float = 100.0, valley_width: float = 0.4) -> np.ndarray:
726
- """U์ž๊ณก ํ˜•์„ฑ๊ณผ์ • (๋น™ํ•˜ ์„ฑ์žฅ โ†’ ์นจ์‹ โ†’ ๋น™ํ•˜ ํ›„ํ‡ด โ†’ U์ž๊ณก)
 
727
 
728
- Stage 0.0~0.3: ๋น™ํ•˜ ์„ฑ์žฅ (V์ž๊ณก์— ๋น™ํ•˜ ์ฑ„์›Œ์ง)
729
- Stage 0.3~0.6: ๋น™ํ•˜ ์นจ์‹ (U์ž ํ˜•ํƒœ๋กœ ๋ณ€ํ˜•)
730
- Stage 0.6~1.0: ๋น™ํ•˜ ํ›„ํ‡ด (๋น™ํ•˜ ๋…น์œผ๋ฉด์„œ U์ž๊ณก ๋“œ๋Ÿฌ๋‚จ)
 
 
 
 
 
 
731
  """
732
  h, w = grid_size, grid_size
733
  elevation = np.zeros((h, w))
734
  center = w // 2
735
 
736
- # 1๋‹จ๊ณ„: V์ž๊ณก โ†’ U์ž๊ณก ๋ณ€ํ˜• (์นจ์‹)
737
- if stage < 0.6:
738
- u_factor = min(stage / 0.6, 1.0) # 0~1๋กœ ์ •๊ทœํ™”
 
 
 
 
 
 
 
 
 
 
739
  else:
740
- u_factor = 1.0 # ์™„์ „ U์ž
 
 
741
 
742
- half_width = int(w * valley_width / 2) * u_factor # U ๋ฐ”๋‹ฅ ๋„ˆ๋น„
 
743
 
 
744
  for r in range(h):
 
 
 
745
  for c in range(w):
746
  dx = abs(c - center)
747
 
748
- if dx < half_width:
749
- # U์ž ๋ฐ”๋‹ฅ
750
- elevation[r, c] = 0
 
751
  else:
752
- # V์—์„œ U๋กœ ์ „ํ™˜
753
- normalized_x = (dx - half_width) / max(1, w // 2 - half_width)
754
- v_height = valley_depth * normalized_x # V shape
755
- u_height = valley_depth * (normalized_x ** 2) # U shape
756
- elevation[r, c] = v_height * (1 - u_factor) + u_height * u_factor
757
 
758
- # ์ƒ๋ฅ˜๋กœ ๊ฐˆ์ˆ˜๋ก ๋†’์•„์ง
759
- elevation[r, :] += (h - r) / h * 30.0
760
-
761
- # 2๋‹จ๊ณ„: ๋น™ํ•˜ ์ถ”๊ฐ€ (stage์— ๋”ฐ๋ผ ์„ฑ์žฅ/ํ›„ํ‡ด)
762
- # stage 0~0.3: ๋น™ํ•˜ ์„ฑ์žฅ (ํ•˜๋ฅ˜๋กœ ์ „์ง„)
763
- # stage 0.3~0.6: ์ตœ๋Œ€ ๋ฒ”์œ„
764
- # stage 0.6~1.0: ๋น™ํ•˜ ํ›„ํ‡ด (์ƒ๋ฅ˜๋กœ ํ›„ํ‡ด)
765
-
766
- glacier_grid = np.zeros((h, w))
 
767
 
768
- if stage < 0.3:
769
- # ๋น™ํ•˜ ์„ฑ์žฅ: ์ƒ๋ฅ˜์—์„œ ํ•˜๋ฅ˜๋กœ ์ „์ง„
770
- glacier_extent = int(h * (stage / 0.3) * 0.8) # ์ตœ๋Œ€ 80%๊นŒ์ง€ ์ „์ง„
771
- glacier_start = 0
772
- glacier_end = glacier_extent
773
- elif stage < 0.6:
774
- # ์ตœ๋Œ€ ๋น™ํ•˜ ๋ฒ”์œ„
775
- glacier_start = 0
776
- glacier_end = int(h * 0.8)
777
- else:
778
- # ๋น™ํ•˜ ํ›„ํ‡ด
779
- retreat_factor = (stage - 0.6) / 0.4
780
- glacier_start = int(h * 0.8 * retreat_factor) # ํ•˜๋ฅ˜์—์„œ ๋…น์Œ
781
- glacier_end = int(h * 0.8 * (1 - retreat_factor * 0.5)) # ์ƒ๋ฅ˜๋„ ์ค„์–ด๋“ฆ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
782
 
783
- # ๋น™ํ•˜ ํ‘œ์‹œ (๊ณจ์งœ๊ธฐ ์ฑ„์›€)
784
- for r in range(glacier_start, min(glacier_end, h)):
785
- for c in range(w):
786
- dx = abs(c - center)
787
- if dx < half_width + 5: # U์ž๊ณก ๋ฐ”๋‹ฅ + ์•ฝ๊ฐ„ ๋„“๊ฒŒ
788
- glacier_thickness = 20.0 * (1 - abs(c - center) / (half_width + 5))
789
- if stage < 0.6:
790
- elevation[r, c] += glacier_thickness
791
- else:
792
- # ํ›„ํ‡ด ์ค‘: ๋น™ํ•˜ ๋†’์ด ๊ฐ์†Œ
793
- retreat_factor = (stage - 0.6) / 0.4
794
- elevation[r, c] += glacier_thickness * (1 - retreat_factor)
795
 
796
  return elevation
797
 
798
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
799
  def create_coastal_cliff_animated(grid_size: int, stage: float,
800
  cliff_height: float = 30.0, num_stacks: int = 2) -> np.ndarray:
801
  """ํ•ด์•ˆ ์ ˆ๋ฒฝ ํ›„ํ‡ด ๊ณผ์ •"""
@@ -880,15 +950,18 @@ def create_v_valley_animated(grid_size: int, stage: float,
880
 
881
 
882
  def create_barchan_animated(grid_size: int, stage: float,
883
- num_dunes: int = 3) -> np.ndarray:
884
- """๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ ์ด๋™ ์• ๋‹ˆ๋ฉ”์ด์…˜
885
-
886
- ์œ„์—์„œ ๋ณผ ๋•Œ ์ดˆ์Šน๋‹ฌ(๐ŸŒ™) ๋ชจ์–‘:
887
- - ๋ณผ๋ก๋ฉด(convex): ๋ฐ”๋žŒ ๋ถˆ์–ด์˜ค๋Š” ์ชฝ (์ƒ๋‹จ)
888
- - ์˜ค๋ชฉ๋ฉด(concave): ๋ฐ”๋žŒ ๊ฐ€๋Š” ์ชฝ (ํ•˜๋‹จ) + ๋ฟ”
889
- - ๋ฟ”(horns): ๋ฐ”๋žŒ ๋ฐฉํ–ฅ์œผ๋กœ ๋ป—์Œ
890
-
891
- ๋ฐ”๋žŒ ๋ฐฉํ–ฅ: ์œ„ โ†’ ์•„๋ž˜
 
 
 
892
  """
893
  h, w = grid_size, grid_size
894
  elevation = np.zeros((h, w))
@@ -898,75 +971,127 @@ def create_barchan_animated(grid_size: int, stage: float,
898
 
899
  np.random.seed(42)
900
 
901
- # ์‚ฌ๊ตฌ ์ด๋™
902
- move_distance = int(h * 0.5 * stage)
903
-
904
  for i in range(num_dunes):
905
- # ์œ„์น˜
906
- initial_y = h // 5 + i * (h // (num_dunes + 1))
907
  cx = w // 4 + (i % 2) * (w // 2)
908
- cy = initial_y + move_distance
909
 
910
- if cy >= h - 20:
911
  continue
912
 
913
- # ์‚ฌ๊ตฌ ํฌ๊ธฐ
914
- dune_height = 10.0 + i * 2.0
915
- outer_r = w // 7 # ๋ฐ”๊นฅ ์› ๋ฐ˜์ง€๋ฆ„
916
- inner_r = outer_r * 0.6 # ์•ˆ์ชฝ ์› ๋ฐ˜์ง€๋ฆ„
917
- inner_offset = outer_r * 0.5 # ์•ˆ์ชฝ ์› ์˜คํ”„์…‹ (์•„๋ž˜๋กœ)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
918
 
919
  for r in range(h):
920
  for c in range(w):
921
  dy = r - cy
922
  dx = c - cx
923
 
924
- # ๋ฐ”๊นฅ ์› (๋ณผ๋ก๋ฉด - ์ƒ๋‹จ)
925
- dist_outer = np.sqrt(dx**2 + dy**2)
926
-
927
- # ์•ˆ์ชฝ ์› (์˜ค๋ชฉ๋ฉด - ํ•˜๋‹จ์œผ๋กœ ์˜คํ”„์…‹)
928
- dist_inner = np.sqrt(dx**2 + (dy - inner_offset)**2)
929
-
930
- # ์ดˆ์Šน๋‹ฌ ์˜์—ญ: ๋ฐ”๊นฅ ์› ์•ˆ AND ์•ˆ์ชฝ ์› ๋ฐ–
931
- in_crescent = (dist_outer < outer_r) and (dist_inner > inner_r)
932
 
933
- if in_crescent:
934
- # ๋†’์ด ๊ณ„์‚ฐ: ์ค‘์‹ฌ์—์„œ ๋ฉ€์ˆ˜๋ก ๋‚ฎ์•„์ง
935
- height_factor = 1 - (dist_outer / outer_r)
 
 
 
 
 
 
936
 
937
- # ๋ฐ”๋žŒ๋ฐ›์ด(์ƒ๋‹จ) ์™„๋งŒ, ๋ฐ”๋žŒ๊ทธ๋Š˜(ํ•˜๋‹จ) ๊ธ‰
 
 
 
938
  if dy < 0:
939
- # ๋ฐ”๋žŒ๋ฐ›์ด: ์™„๋งŒํ•œ ๊ฒฝ์‚ฌ
940
- slope = height_factor * 0.8
941
  else:
942
- # ๋ฐ”๋žŒ๊ทธ๋Š˜: ๋” ๋†’๊ฒŒ (๊ธ‰๊ฒฝ์‚ฌ ํšจ๊ณผ)
943
- slope = height_factor * 1.2
944
 
945
- z = dune_height * slope
946
- elevation[r, c] = max(elevation[r, c], 5.0 + z)
947
-
948
- # ๋ฟ” (horn) - ์–‘์ชฝ์œผ๋กœ ๋ฐ”๋žŒ ๋ฐฉํ–ฅ์œผ๋กœ ๋ป—์Œ
949
- horn_width = outer_r * 0.3
950
- horn_length = outer_r * 0.8
951
 
952
- for side in [-1, 1]: # ์™ผ์ชฝ, ์˜ค๋ฅธ์ชฝ ๋ฟ”
953
- horn_cx = cx + side * (outer_r - horn_width)
954
- horn_cy = cy + inner_offset
955
-
956
- dx_horn = c - horn_cx
957
- dy_horn = r - horn_cy
958
-
959
- # ๋ฟ” ์˜์—ญ (๋ฐ”๋žŒ ๋ฐฉํ–ฅ์œผ๋กœ ๊ธธ์ญ‰)
960
- if abs(dx_horn) < horn_width and 0 < dy_horn < horn_length:
961
- # ๋ฟ” ๋†’์ด: ๋์œผ๋กœ ๊ฐˆ์ˆ˜๋ก ๋‚ฎ์•„์ง
962
- horn_factor = 1 - dy_horn / horn_length
963
- width_factor = 1 - abs(dx_horn) / horn_width
964
- z = dune_height * 0.5 * horn_factor * width_factor
965
 
966
- if z > 0.3:
967
- elevation[r, c] = max(elevation[r, c], 5.0 + z)
 
 
 
 
 
 
 
 
 
 
 
968
 
969
  return elevation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
970
  # ============================================
971
  # ํ™•์žฅ ์ง€ํ˜• (Extended Landforms)
972
  # ============================================
@@ -1605,88 +1730,318 @@ def _get_horn_stage_desc(stage: float) -> str:
1605
 
1606
 
1607
  def create_shield_volcano(grid_size: int = 100, stage: float = 1.0,
1608
- max_height: float = 40.0) -> np.ndarray:
1609
- """์ˆœ์ƒํ™”์‚ฐ (Shield Volcano) - ์™„๋งŒํ•œ ๊ฒฝ์‚ฌ"""
 
 
 
 
 
 
 
 
 
 
 
1610
  h, w = grid_size, grid_size
1611
  elevation = np.zeros((h, w))
1612
 
1613
  center = (h // 2, w // 2)
1614
- radius = w // 2
 
 
 
 
1615
 
1616
  for r in range(h):
1617
  for c in range(w):
1618
  dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
1619
 
1620
- if dist < radius:
1621
- # ์™„๋งŒํ•œ ํฌ๋ฌผ์„  ํ˜•ํƒœ (๊ฒฝ์‚ฌ 5-10๋„)
1622
- elevation[r, c] = max_height * (1 - (dist / radius)**2) * stage
1623
-
1624
- # ์ •์ƒ๋ถ€ ํ™”๊ตฌ
1625
- crater_radius = int(radius * 0.1)
1626
- for r in range(h):
1627
- for c in range(w):
1628
- dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
1629
- if dist < crater_radius:
1630
- elevation[r, c] = max_height * 0.9 * stage
 
 
 
 
 
 
1631
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1632
  return elevation
1633
 
1634
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1635
  def create_stratovolcano(grid_size: int = 100, stage: float = 1.0,
1636
- max_height: float = 80.0) -> np.ndarray:
1637
- """์„ฑ์ธตํ™”์‚ฐ (Stratovolcano) - ๊ธ‰ํ•œ ์›๋ฟ”ํ˜•"""
 
 
 
 
 
 
 
 
 
 
 
1638
  h, w = grid_size, grid_size
1639
  elevation = np.zeros((h, w))
1640
 
1641
  center = (h // 2, w // 2)
1642
- radius = int(w * 0.4)
 
 
 
 
1643
 
1644
  for r in range(h):
1645
  for c in range(w):
1646
  dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
1647
 
1648
- if dist < radius:
1649
  # ๊ธ‰ํ•œ ์›๋ฟ” (๊ฒฝ์‚ฌ 25-35๋„)
1650
- elevation[r, c] = max_height * (1 - dist / radius) * stage
1651
-
1652
- # ์ •์ƒ๋ถ€ ํ™”๊ตฌ
1653
- crater_radius = int(radius * 0.08)
1654
- crater_depth = 10.0
1655
- for r in range(h):
1656
- for c in range(w):
1657
- dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
1658
- if dist < crater_radius:
1659
- elevation[r, c] = max_height * stage - crater_depth
1660
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1661
  return elevation
1662
 
1663
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1664
  def create_caldera(grid_size: int = 100, stage: float = 1.0,
1665
- rim_height: float = 50.0) -> np.ndarray:
1666
- """์นผ๋ฐ๋ผ (Caldera) - ํ™”๊ตฌ ํ•จ๋ชฐ"""
 
 
 
 
 
 
 
 
 
 
 
1667
  h, w = grid_size, grid_size
1668
  elevation = np.zeros((h, w))
1669
 
1670
  center = (h // 2, w // 2)
1671
- outer_radius = int(w * 0.45)
1672
- caldera_radius = int(w * 0.3)
1673
 
1674
- for r in range(h):
1675
- for c in range(w):
1676
- dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
1677
-
1678
- if dist < outer_radius:
1679
- if dist < caldera_radius:
1680
- # ์นผ๋ฐ๋ผ ๋ฐ”๋‹ฅ (ํ‰ํƒ„, ํ˜ธ์ˆ˜ ๊ฐ€๋Šฅ)
1681
- elevation[r, c] = 5.0
1682
- else:
1683
- # ์นผ๋ฐ๋ผ ๋ฒฝ (๊ธ‰๊ฒฝ์‚ฌ)
1684
- t = (dist - caldera_radius) / (outer_radius - caldera_radius)
1685
- elevation[r, c] = 5.0 + rim_height * (1 - t) * stage
 
 
 
 
 
 
 
 
1686
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1687
  return elevation
1688
 
1689
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1690
  def create_mesa_butte(grid_size: int = 100, stage: float = 1.0,
1691
  num_mesas: int = 2) -> np.ndarray:
1692
  """๋ฉ”์‚ฌ/๋ทฐํŠธ (Mesa/Butte) - ํƒ์ƒ์ง€"""
@@ -2323,80 +2678,133 @@ def create_crater_lake(grid_size: int = 100, stage: float = 1.0,
2323
  return elevation
2324
 
2325
 
2326
- def create_lava_plateau(grid_size: int = 100, stage: float = 1.0) -> np.ndarray:
2327
- """์šฉ์•”๋Œ€์ง€ (Lava Plateau) - ํ•œํƒ„๊ฐ• ํ˜•์„ฑ๊ณผ์ •
 
2328
 
2329
- Stage 0.0~0.3: ์›๋ž˜ V์ž๊ณก ์กด์žฌ
2330
- Stage 0.3~0.6: ์—ดํ•˜๋ถ„์ถœ๋กœ V์ž๊ณก ๋ฉ”์›Œ์ง (์šฉ์•”๋Œ€์ง€ ํ˜•์„ฑ)
2331
- Stage 0.6~1.0: ํ•˜์ฒœ ์žฌ์นจ์‹์œผ๋กœ ์ƒˆ๋กœ์šด ํ˜‘๊ณก ํ˜•์„ฑ
 
 
 
 
 
 
2332
  """
2333
  h, w = grid_size, grid_size
2334
  elevation = np.zeros((h, w))
 
2335
  center = w // 2
2336
 
2337
  # ๊ธฐ๋ฐ˜ ๊ณ ์› ๋†’์ด
2338
  plateau_base = 30.0
2339
 
2340
- if stage < 0.3:
2341
- # ์›๋ž˜ V์ž๊ณก ์ƒํƒœ
2342
- v_factor = 1.0
2343
- lava_fill = 0.0
2344
- new_valley = 0.0
2345
- elif stage < 0.6:
2346
- # ์—ดํ•˜๋ถ„์ถœ๋กœ V์ž๊ณก ๋ฉ”์›Œ์ง
2347
- v_factor = 1.0 - ((stage - 0.3) / 0.3) # V์ž๊ณก ์ ์  ์‚ฌ๋ผ์ง
2348
- lava_fill = (stage - 0.3) / 0.3 # ์šฉ์•” ์ฑ„์›Œ์ง
2349
- new_valley = 0.0
2350
- else:
2351
- # ์ƒˆ ํ˜‘๊ณก ํ˜•์„ฑ
2352
- v_factor = 0.0 # ์›๋ž˜ V์ž๊ณก ์™„์ „ํžˆ ๋ฎ์ž„
2353
- lava_fill = 1.0
2354
- new_valley = (stage - 0.6) / 0.4 # ์ƒˆ ํ˜‘๊ณก ๋ฐœ๋‹ฌ
2355
-
2356
- for r in range(h):
2357
- for c in range(w):
2358
- dx = abs(c - center)
2359
-
2360
- # ๊ธฐ๋ณธ ๊ณ ์›
2361
- elevation[r, c] = plateau_base
2362
-
2363
- # ์›๋ž˜ V์ž๊ณก (์—ดํ•˜๋ถ„์ถœ ์ „)
2364
- if v_factor > 0:
2365
- valley_depth = 25.0 * v_factor
2366
- if dx < 15:
2367
- v_shape = valley_depth * (1 - dx / 15)
2368
  elevation[r, c] -= v_shape
 
 
 
 
 
 
 
 
 
 
2369
 
2370
- # ์šฉ์•” ์ฑ„์›€ (ํ‰ํƒ„ํ™”)
2371
- if lava_fill > 0:
2372
- # ์šฉ์•”์ด V์ž๊ณก์„ ๋ฉ”์›€
2373
- if dx < 15:
2374
- fill_amount = 25.0 * lava_fill * (1 - dx / 15)
2375
- elevation[r, c] += fill_amount * 0.8 # ์•ฝ๊ฐ„ ๋‚ฎ๊ฒŒ
 
 
 
 
 
 
 
 
2376
 
2377
- # ์ƒˆ๋กœ์šด ํ˜‘๊ณก (ํ•˜์ฒœ ์žฌ์นจ์‹)
2378
- if new_valley > 0:
2379
- # ์ƒˆ ํ•˜์ฒœ์ด ์šฉ์•”๋Œ€์ง€๋ฅผ ํŒŒ๊ณ ๋“ฆ
2380
- new_valley_width = int(8 * new_valley)
2381
- new_valley_depth = 20.0 * new_valley
 
 
2382
 
2383
- if dx < new_valley_width:
2384
- # ๋” ์ข๊ณ  ๊นŠ์€ ํ˜‘๊ณก
2385
- gorge_shape = new_valley_depth * (1 - dx / max(new_valley_width, 1))
2386
- elevation[r, c] -= gorge_shape
 
 
 
 
2387
 
2388
- # ๊ฐ€์žฅ์ž๋ฆฌ ๊ฒฝ์‚ฌ
2389
- margin = int(w * 0.1)
2390
- for r in range(h):
2391
- for c in range(w):
2392
- edge_dist = min(r, h - r - 1, c, w - c - 1)
2393
- if edge_dist < margin:
2394
- t = edge_dist / margin
2395
- elevation[r, c] = elevation[r, c] * t + 5.0 * (1 - t)
 
 
 
 
 
 
 
 
 
2396
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2397
  return elevation
2398
 
2399
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2400
  def create_coastal_dune(grid_size: int = 100, stage: float = 1.0,
2401
  num_dunes: int = 3) -> np.ndarray:
2402
  """ํ•ด์•ˆ์‚ฌ๊ตฌ (Coastal Dune) - ํ•ด์•ˆ๊ฐ€ ๋ชจ๋ž˜ ์–ธ๋•"""
 
722
 
723
 
724
  def create_u_valley_animated(grid_size: int, stage: float,
725
+ valley_depth: float = 100.0, valley_width: float = 0.4,
726
+ return_metadata: bool = False) -> np.ndarray:
727
+ """U์ž๊ณก ํ˜•์„ฑ๊ณผ์ • (V์ž๊ณก โ†’ ๋น™ํ•˜ ์นจ์‹ โ†’ U์ž๊ณก ๋…ธ์ถœ)
728
 
729
+ Stage 0~0.2: V์ž๊ณก ์ƒํƒœ (ํ•˜์ฒœ ์นจ์‹์— ์˜ํ•ด ํ˜•์„ฑ๋œ ์ดˆ๊ธฐ ๊ณ„๊ณก)
730
+ Stage 0.2~0.5: ๋น™ํ•˜ ์ฑ„์›Œ์ง + ์นจ์‹ ์‹œ์ž‘ (ํ”Œ๋Ÿฌํ‚น, ๋งˆ์‹)
731
+ Stage 0.5~0.8: U์ž ํ˜•ํƒœ ๋ฐœ๋‹ฌ (๋ฐ”๋‹ฅ ๋„“์–ด์ง, ๋ฒฝ ๊ธ‰๊ฒฝ์‚ฌ)
732
+ Stage 0.8~1.0: ๋น™ํ•˜ ํ›„ํ‡ด โ†’ U์ž๊ณก ๋“œ๋Ÿฌ๋‚จ + ํ˜„์ˆ˜๊ณก + ๋น™ํ•˜ํ˜ธ
733
+
734
+ ํ•ต์‹ฌ ๊ณผ์ •:
735
+ - ํ”Œ๋Ÿฌํ‚น(Plucking): ์•”์„ ํŒŒ์‡„ ํ›„ ์šด๋ฐ˜
736
+ - ๋งˆ์‹(Abrasion): ๋น™ํ•˜ ๋ฐ”๋‹ฅ ์—ฐ๋งˆ
737
+ - ํ˜„์ˆ˜๊ณก(Hanging Valley): ์ง€๋ฅ˜ ๋น™ํ•˜๊ฐ€ ๋œ ์นจ์‹
738
  """
739
  h, w = grid_size, grid_size
740
  elevation = np.zeros((h, w))
741
  center = w // 2
742
 
743
+ # Stage์— ๋”ฐ๋ฅธ U์ž ๋ณ€ํ˜• ์ •๋„
744
+ if stage < 0.2:
745
+ # V์ž๊ณก ์ƒํƒœ
746
+ u_transform = 0
747
+ glacier_visible = False
748
+ elif stage < 0.5:
749
+ # ๋น™ํ•˜ ์„ฑ์žฅ + ์ดˆ๊ธฐ ์นจ์‹
750
+ u_transform = (stage - 0.2) / 0.3 * 0.5
751
+ glacier_visible = True
752
+ elif stage < 0.8:
753
+ # U์ž ํ˜•ํƒœ ๋ฐœ๋‹ฌ
754
+ u_transform = 0.5 + (stage - 0.5) / 0.3 * 0.5
755
+ glacier_visible = True
756
  else:
757
+ # ๋น™ํ•˜ ํ›„ํ‡ด, U์ž๊ณก ๋…ธ์ถœ
758
+ u_transform = 1.0
759
+ glacier_visible = False
760
 
761
+ # U์ž ๋ฐ”๋‹ฅ ๋„ˆ๋น„ (์ ์ฐจ ๋„“์–ด์ง)
762
+ floor_width = int(w * valley_width * 0.3 * u_transform)
763
 
764
+ # ๊ธฐ๋ณธ ์ง€ํ˜• ์ƒ์„ฑ
765
  for r in range(h):
766
+ # ์ƒ๋ฅ˜๋กœ ๊ฐˆ์ˆ˜๋ก ๊ธฐ๋ฐ˜ ๋†’์•„์ง
767
+ base_height = (h - r) / h * 40.0
768
+
769
  for c in range(w):
770
  dx = abs(c - center)
771
 
772
+ # V์ž๊ณก โ†’ U์ž๊ณก ๋‹จ๋ฉด ๋ณ€ํ˜•
773
+ if dx < floor_width:
774
+ # U์ž ๋ฐ”๋‹ฅ (ํ‰ํƒ„)
775
+ valley_floor = 0
776
  else:
777
+ # ๋ฒฝ๋ฉด
778
+ wall_dist = (dx - floor_width) / max(1, w // 2 - floor_width)
 
 
 
779
 
780
+ # V์ž ๋‹จ๋ฉด (์‚ผ๊ฐํ˜•)
781
+ v_profile = valley_depth * wall_dist
782
+
783
+ # U์ž ๋‹จ๋ฉด (ํฌ๋ฌผ์„ )
784
+ u_profile = valley_depth * (wall_dist ** 0.5)
785
+
786
+ # ๋ณ€ํ™˜ ์ ์šฉ
787
+ valley_floor = v_profile * (1 - u_transform) + u_profile * u_transform
788
+
789
+ elevation[r, c] = base_height + valley_floor
790
 
791
+ # ํ˜„์ˆ˜๊ณก (Hanging Valley) - stage 0.6 ์ดํ›„ ์‹œ๊ฐํ™”
792
+ if stage > 0.6:
793
+ hanging_valleys = [
794
+ (int(h * 0.3), -1), # ์ขŒ์ธก ํ˜„์ˆ˜๊ณก
795
+ (int(h * 0.6), 1), # ์šฐ์ธก ํ˜„์ˆ˜๊ณก
796
+ ]
797
+
798
+ for hy, side in hanging_valleys:
799
+ hx = center + side * int(w * 0.35)
800
+ hang_height = 30.0 * u_transform # ํ˜„์ˆ˜ ๋†’์ด
801
+
802
+ for dy in range(-10, 11):
803
+ for dx in range(-8, 9):
804
+ r, c = hy + dy, hx + dx
805
+ if 0 <= r < h and 0 <= c < w:
806
+ dist = np.sqrt(dy**2 + dx**2)
807
+ if dist < 10:
808
+ # ํ˜„์ˆ˜๊ณก ์ž…๊ตฌ (๋†’๊ฒŒ ๋งค๋‹ฌ๋ฆผ)
809
+ notch = 10.0 * (1 - dist / 10)
810
+ elevation[r, c] = max(elevation[r, c], hang_height + notch)
811
+
812
+ # ๋น™ํ•˜ ์‹œ๊ฐํ™” (์ฑ„์›€)
813
+ if glacier_visible:
814
+ glacier_progress = min(1.0, (stage - 0.2) / 0.6)
815
+ glacier_end = int(h * 0.85 * glacier_progress)
816
+ glacier_thickness = 25.0 * glacier_progress
817
+
818
+ for r in range(glacier_end):
819
+ for c in range(w):
820
+ dx = abs(c - center)
821
+ if dx < floor_width + 10:
822
+ # ๋น™ํ•˜ ๋ณผ๋ก ํ‘œ๋ฉด
823
+ ice_surface = glacier_thickness * (1 - (dx / (floor_width + 10)) ** 2)
824
+ if stage < 0.8:
825
+ elevation[r, c] += ice_surface
826
+
827
+ # ๋น™ํ•˜ํ˜ธ(Tarn) - ๋น™ํ•˜ ํ›„ํ‡ด ํ›„
828
+ if stage > 0.85:
829
+ tarn_progress = (stage - 0.85) / 0.15
830
+ tarn_y = int(h * 0.15)
831
+ tarn_radius = int(w * 0.12 * tarn_progress)
832
+
833
+ for dy in range(-tarn_radius, tarn_radius + 1):
834
+ for dx in range(-tarn_radius, tarn_radius + 1):
835
+ r, c = tarn_y + dy, center + dx
836
+ if 0 <= r < h and 0 <= c < w:
837
+ dist = np.sqrt(dy**2 + dx**2)
838
+ if dist < tarn_radius:
839
+ # ํ˜ธ์ˆ˜ ๋ฐ”๋‹ฅ
840
+ elevation[r, c] = min(elevation[r, c], -5.0 * (1 - dist / tarn_radius))
841
 
842
+ if return_metadata:
843
+ return elevation, {
844
+ 'u_transform': u_transform,
845
+ 'glacier_visible': glacier_visible,
846
+ 'floor_width': floor_width,
847
+ 'stage_description': _get_u_valley_stage_desc(stage)
848
+ }
 
 
 
 
 
849
 
850
  return elevation
851
 
852
 
853
+ def _get_u_valley_stage_desc(stage: float) -> str:
854
+ """U์ž๊ณก ๋‹จ๊ณ„๋ณ„ ์„ค๋ช…"""
855
+ if stage < 0.15:
856
+ return "๐Ÿž๏ธ V์ž๊ณก ์ƒํƒœ: ํ•˜์ฒœ ์นจ์‹์— ์˜ํ•œ ์ดˆ๊ธฐ ๊ณ„๊ณก"
857
+ elif stage < 0.35:
858
+ return "โ„๏ธ ๋น™ํ•˜ ์„ฑ์žฅ: ๊ณ„๊ณก์— ๋น™ํ•˜ ์ฑ„์›Œ์ง"
859
+ elif stage < 0.55:
860
+ return "โ›๏ธ ๋น™ํ•˜ ์นจ์‹: ํ”Œ๋Ÿฌํ‚น + ๋งˆ์‹ โ†’ ๋ฐ”๋‹ฅ ํ™•์žฅ"
861
+ elif stage < 0.75:
862
+ return "๐Ÿ—ป U์ž ํ˜•ํƒœ: ํ‰ํƒ„ ๋ฐ”๋‹ฅ + ๊ธ‰๊ฒฝ์‚ฌ ๋ฒฝ"
863
+ elif stage < 0.9:
864
+ return "๐ŸŒŠ ๋น™ํ•˜ ํ›„ํ‡ด: ํ˜„์ˆ˜๊ณก(Hanging Valley) ๋“œ๋Ÿฌ๋‚จ"
865
+ else:
866
+ return "๐Ÿ’ง ๋น™ํ•˜ํ˜ธ(Tarn): ๋น™ํ•˜ ์œตํ•ด์ˆ˜ ๊ณ ์ž„"
867
+
868
+
869
  def create_coastal_cliff_animated(grid_size: int, stage: float,
870
  cliff_height: float = 30.0, num_stacks: int = 2) -> np.ndarray:
871
  """ํ•ด์•ˆ ์ ˆ๋ฒฝ ํ›„ํ‡ด ๊ณผ์ •"""
 
950
 
951
 
952
  def create_barchan_animated(grid_size: int, stage: float,
953
+ num_dunes: int = 3, return_metadata: bool = False) -> np.ndarray:
954
+ """๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ ํ˜•์„ฑ ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜
955
+
956
+ Stage 0~0.25: ๋ชจ๋ž˜ ์ถ•์  (์ž‘์€ ์›ํ˜• ์–ธ๋• ํ˜•์„ฑ)
957
+ Stage 0.25~0.5: ๋น„๋Œ€์นญ ๋ฐœ๋‹ฌ (๋ฐ”๋žŒ๋ฐ›์ด ์™„๊ฒฝ์‚ฌ, ๋ฐ”๋žŒ๊ทธ๋Š˜ ๊ธ‰๊ฒฝ์‚ฌ)
958
+ Stage 0.5~0.75: ์ดˆ์Šน๋‹ฌ ํ˜•ํƒœ ๋ฐœ๋‹ฌ (์˜ค๋ชฉ๋ฉด ํ˜•์„ฑ)
959
+ Stage 0.75~1.0: ๋ฟ”(horn) ์™„์„ฑ (๋ฐ”๋žŒ ๋ฐฉํ–ฅ์œผ๋กœ ์—ฐ์žฅ)
960
+
961
+ ํ˜•์„ฑ ์›๋ฆฌ:
962
+ - ๋ฐ”๋žŒ์ด ๋ชจ๋ž˜๋ฅผ ๋ฐ”๋žŒ๋ฐ›์ด ์‚ฌ๋ฉด์œผ๋กœ ์šด๋ฐ˜
963
+ - ์ •์ƒ ๋„˜์–ด ๋ฐ”๋žŒ๊ทธ๋Š˜์— ํ‡ด์  (๋‚™์‚ฌ๋ฉด, slip face)
964
+ - ๊ฐ€์žฅ์ž๋ฆฌ ๋ชจ๋ž˜๊ฐ€ ๋” ๋นจ๋ฆฌ ์ด๋™ โ†’ ๋ฟ” ํ˜•์„ฑ
965
  """
966
  h, w = grid_size, grid_size
967
  elevation = np.zeros((h, w))
 
971
 
972
  np.random.seed(42)
973
 
 
 
 
974
  for i in range(num_dunes):
975
+ # ์‚ฌ๊ตฌ ์œ„์น˜ (๊ณ ์ •)
 
976
  cx = w // 4 + (i % 2) * (w // 2)
977
+ cy = int(h * 0.3) + i * (h // (num_dunes + 1))
978
 
979
+ if cy >= h - 15:
980
  continue
981
 
982
+ # Stage์— ๋”ฐ๋ฅธ ํฌ๊ธฐ ๋ฐœ๋‹ฌ
983
+ max_height = 12.0 + i * 3.0
984
+ max_radius = int(w * 0.12)
985
+
986
+ # Stage 0~0.25: ์ž‘์€ ์›ํ˜• ์–ธ๋•
987
+ if stage < 0.25:
988
+ progress = stage / 0.25
989
+ current_height = max_height * 0.3 * progress
990
+ current_radius = int(max_radius * 0.4 * progress)
991
+ asymmetry = 0 # ๋Œ€์นญ
992
+ horn_length = 0
993
+
994
+ # Stage 0.25~0.5: ๋น„๋Œ€์นญ ๋ฐœ๋‹ฌ
995
+ elif stage < 0.5:
996
+ progress = (stage - 0.25) / 0.25
997
+ current_height = max_height * (0.3 + 0.4 * progress)
998
+ current_radius = int(max_radius * (0.4 + 0.3 * progress))
999
+ asymmetry = progress # ์ ์ฐจ ๋น„๋Œ€์นญ
1000
+ horn_length = 0
1001
+
1002
+ # Stage 0.5~0.75: ์ดˆ์Šน๋‹ฌ ํ˜•ํƒœ
1003
+ elif stage < 0.75:
1004
+ progress = (stage - 0.5) / 0.25
1005
+ current_height = max_height * (0.7 + 0.2 * progress)
1006
+ current_radius = int(max_radius * (0.7 + 0.2 * progress))
1007
+ asymmetry = 1.0
1008
+ horn_length = int(max_radius * 0.4 * progress)
1009
+
1010
+ # Stage 0.75~1.0: ๋ฟ” ์™„์„ฑ
1011
+ else:
1012
+ progress = (stage - 0.75) / 0.25
1013
+ current_height = max_height * (0.9 + 0.1 * progress)
1014
+ current_radius = max_radius
1015
+ asymmetry = 1.0
1016
+ horn_length = int(max_radius * (0.4 + 0.4 * progress))
1017
+
1018
+ if current_radius < 2:
1019
+ continue
1020
+
1021
+ # ์ดˆ์Šน๋‹ฌ ํŒŒ๋ผ๋ฏธํ„ฐ
1022
+ inner_ratio = 0.5 + 0.2 * asymmetry # ์•ˆ์ชฝ ์› ๋น„์œจ
1023
+ inner_offset = current_radius * 0.4 * asymmetry # ์˜คํ”„์…‹
1024
 
1025
  for r in range(h):
1026
  for c in range(w):
1027
  dy = r - cy
1028
  dx = c - cx
1029
 
1030
+ dist = np.sqrt(dx**2 + dy**2)
 
 
 
 
 
 
 
1031
 
1032
+ # ๋ฐ”๊นฅ ์› ์˜์—ญ
1033
+ if dist < current_radius:
1034
+ # ์•ˆ์ชฝ ์› (์˜ค๋ชฉ๋ฉด) - ๋น„๋Œ€์นญ์ผ ๋•Œ๋งŒ
1035
+ dist_inner = np.sqrt(dx**2 + (dy - inner_offset)**2)
1036
+ inner_r = current_radius * inner_ratio
1037
+
1038
+ if asymmetry > 0.5 and dist_inner < inner_r:
1039
+ # ์˜ค๋ชฉ๋ฉด ์•ˆ์ชฝ์€ ๋‚ฎ๊ฒŒ
1040
+ continue
1041
 
1042
+ # ๋†’์ด ๊ณ„์‚ฐ
1043
+ radial_factor = 1 - (dist / current_radius) ** 1.5
1044
+
1045
+ # ๋ฐ”๋žŒ๋ฐ›์ด(์ƒ๋‹จ) vs ๋ฐ”๋žŒ๊ทธ๋Š˜(ํ•˜๋‹จ) ๋น„๋Œ€์นญ
1046
  if dy < 0:
1047
+ # ๋ฐ”๋žŒ๋ฐ›์ด: ์™„๋งŒ (5-12ยฐ ๊ฒฝ์‚ฌ)
1048
+ slope_factor = 0.6 + 0.4 * (1 - asymmetry)
1049
  else:
1050
+ # ๋ฐ”๋žŒ๊ทธ๋Š˜: ๊ธ‰๊ฒฝ์‚ฌ (30-34ยฐ ์•ˆ์‹๊ฐ)
1051
+ slope_factor = 0.8 + 0.5 * asymmetry
1052
 
1053
+ z = current_height * radial_factor * slope_factor
1054
+ if z > 0.5:
1055
+ elevation[r, c] = max(elevation[r, c], 5.0 + z)
 
 
 
1056
 
1057
+ # ๋ฟ” (horns) - stage 0.5 ์ดํ›„
1058
+ if horn_length > 2:
1059
+ for side in [-1, 1]:
1060
+ horn_cx = cx + side * (current_radius * 0.7)
1061
+ horn_cy = cy + inner_offset
1062
+
1063
+ dx_h = c - horn_cx
1064
+ dy_h = r - horn_cy
 
 
 
 
 
1065
 
1066
+ # ๋ฟ” ์˜์—ญ: ๋ฐ”๋žŒ ๋ฐฉํ–ฅ์œผ๋กœ ๊ธธ์ญ‰
1067
+ horn_width = max(2, current_radius * 0.25)
1068
+ if abs(dx_h) < horn_width and 0 < dy_h < horn_length:
1069
+ horn_factor = (1 - dy_h / horn_length) ** 0.7
1070
+ width_factor = 1 - (abs(dx_h) / horn_width) ** 2
1071
+ z = current_height * 0.4 * horn_factor * width_factor
1072
+ if z > 0.3:
1073
+ elevation[r, c] = max(elevation[r, c], 5.0 + z)
1074
+
1075
+ if return_metadata:
1076
+ return elevation, {
1077
+ 'stage_description': _get_barchan_stage_desc(stage)
1078
+ }
1079
 
1080
  return elevation
1081
+
1082
+
1083
+ def _get_barchan_stage_desc(stage: float) -> str:
1084
+ """๋ฐ”๋ฅดํ•œ ๋‹จ๊ณ„๋ณ„ ์„ค๋ช…"""
1085
+ if stage < 0.2:
1086
+ return "๐Ÿœ๏ธ ๋ชจ๋ž˜ ์ถ•์ : ์žฅ์• ๋ฌผ ์ฃผ๋ณ€ ๋ชจ๋ž˜ ์Œ“์ž„ ์‹œ์ž‘"
1087
+ elif stage < 0.4:
1088
+ return "โฌ†๏ธ ์–ธ๋• ์„ฑ์žฅ: ์›ํ˜• ๋ชจ๋ž˜์–ธ๋• ํ˜•์„ฑ"
1089
+ elif stage < 0.6:
1090
+ return "โ†—๏ธ ๋น„๋Œ€์นญ ๋ฐœ๋‹ฌ: ๋ฐ”๋žŒ๋ฐ›์ด ์™„๊ฒฝ์‚ฌ, ๋ฐ”๋žŒ๊ทธ๋Š˜ ๊ธ‰๊ฒฝ์‚ฌ"
1091
+ elif stage < 0.8:
1092
+ return "๐ŸŒ™ ์ดˆ์Šน๋‹ฌ ํ˜•ํƒœ: ์˜ค๋ชฉ๋ฉด ํ˜•์„ฑ, ๋ฟ” ๋ฐœ๋‹ฌ ์‹œ์ž‘"
1093
+ else:
1094
+ return "๐Ÿœ๏ธ ๋ฐ”๋ฅดํ•œ ์™„์„ฑ: ๋ฟ”์ด ๋ฐ”๋žŒ ๋ฐฉํ–ฅ์œผ๋กœ ์—ฐ์žฅ"
1095
  # ============================================
1096
  # ํ™•์žฅ ์ง€ํ˜• (Extended Landforms)
1097
  # ============================================
 
1730
 
1731
 
1732
  def create_shield_volcano(grid_size: int = 100, stage: float = 1.0,
1733
+ max_height: float = 40.0, return_metadata: bool = False) -> np.ndarray:
1734
+ """์ˆœ์ƒํ™”์‚ฐ (Shield Volcano) - ํ•˜์™€์ดํ˜•
1735
+
1736
+ Stage 0~0.3: ํ•ด์ € ๋ถ„์ถœ โ†’ ํ•ด์ˆ˜๋ฉด ๋„๋‹ฌ
1737
+ Stage 0.3~0.6: ์šฉ์•”๋ฅ˜ ๋ฐ˜๋ณต โ†’ ์™„๋งŒํ•œ ์ˆœ์ƒ ํ˜•์„ฑ
1738
+ Stage 0.6~0.8: ์ •์ƒ๋ถ€ ํ™•์žฅ + ์ค‘์•™ ํ™”๊ตฌ ํ˜•์„ฑ
1739
+ Stage 0.8~1.0: ์ •์ƒ ์นผ๋ฐ๋ผ + ์šฉ์•” ํ๋ฆ„ ํ”์ 
1740
+
1741
+ ํŠน์ง•:
1742
+ - ํ˜„๋ฌด์•”์งˆ ์šฉ์•” (์œ ๋™์„ฑ ๋†’์Œ)
1743
+ - ๊ฒฝ์‚ฌ 5-10ยฐ
1744
+ - ์šฉ์•”๋ฅ˜๊ฐ€ ๋„“๊ฒŒ ํผ์ง
1745
+ """
1746
  h, w = grid_size, grid_size
1747
  elevation = np.zeros((h, w))
1748
 
1749
  center = (h // 2, w // 2)
1750
+ max_radius = int(w * 0.45)
1751
+
1752
+ # Stage์— ๋”ฐ๋ฅธ ๋ฐ˜๊ฒฝ ์„ฑ์žฅ
1753
+ current_radius = int(max_radius * min(1.0, stage * 1.3))
1754
+ current_height = max_height * stage
1755
 
1756
  for r in range(h):
1757
  for c in range(w):
1758
  dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
1759
 
1760
+ if dist < current_radius and current_radius > 0:
1761
+ # ์™„๋งŒํ•œ ํฌ๋ฌผ์„  ํ˜•ํƒœ (๊ฒฝ์‚ฌ 5-10๋„)
1762
+ radial_factor = 1 - (dist / current_radius) ** 1.8
1763
+ elevation[r, c] = current_height * radial_factor
1764
+
1765
+ # ์šฉ์•”๋ฅ˜ ํ”์  (๋ฐฉ์‚ฌ์ƒ) - stage 0.4 ์ดํ›„
1766
+ if stage > 0.4:
1767
+ np.random.seed(42)
1768
+ num_flows = 6
1769
+ for i in range(num_flows):
1770
+ angle = 2 * np.pi * i / num_flows + np.random.random() * 0.3
1771
+ flow_length = int(current_radius * (0.6 + 0.4 * stage))
1772
+ flow_width = 3 + int(2 * stage)
1773
+
1774
+ for d in range(10, flow_length):
1775
+ fx = int(center[1] + d * np.cos(angle))
1776
+ fy = int(center[0] + d * np.sin(angle))
1777
 
1778
+ for dw in range(-flow_width, flow_width + 1):
1779
+ tx = int(fx + dw * np.sin(angle))
1780
+ ty = int(fy - dw * np.cos(angle))
1781
+
1782
+ if 0 <= ty < h and 0 <= tx < w:
1783
+ # ์šฉ์•”๋ฅ˜ ์œต๊ธฐ
1784
+ flow_height = 2.0 * (1 - abs(dw) / flow_width) * (1 - d / flow_length)
1785
+ elevation[ty, tx] += flow_height
1786
+
1787
+ # ์ •์ƒ๋ถ€ ํ™”๊ตฌ/์นผ๋ฐ๋ผ - stage 0.6 ์ดํ›„
1788
+ if stage > 0.6:
1789
+ caldera_progress = (stage - 0.6) / 0.4
1790
+ crater_radius = int(max_radius * 0.08 * (1 + caldera_progress))
1791
+ crater_depth = 5.0 * caldera_progress
1792
+
1793
+ for r in range(h):
1794
+ for c in range(w):
1795
+ dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
1796
+ if dist < crater_radius:
1797
+ # ํ•จ๋ชฐ ์นผ๋ฐ๋ผ
1798
+ depression = crater_depth * (1 - (dist / crater_radius) ** 2)
1799
+ elevation[r, c] = max(elevation[r, c] - depression, current_height * 0.85)
1800
+
1801
+ if return_metadata:
1802
+ return elevation, {
1803
+ 'current_radius': current_radius,
1804
+ 'current_height': current_height,
1805
+ 'stage_description': _get_shield_stage_desc(stage)
1806
+ }
1807
+
1808
  return elevation
1809
 
1810
 
1811
+ def _get_shield_stage_desc(stage: float) -> str:
1812
+ """์ˆœ์ƒํ™”์‚ฐ ๋‹จ๊ณ„๋ณ„ ์„ค๋ช…"""
1813
+ if stage < 0.2:
1814
+ return "๐ŸŒ‹ ํ•ด์ € ๋ถ„์ถœ: ํ˜„๋ฌด์•” ์šฉ์•” ๋ถ„์ถœ ์‹œ์ž‘"
1815
+ elif stage < 0.4:
1816
+ return "๐Ÿ๏ธ ํ•ด์ˆ˜๋ฉด ๋„๋‹ฌ: ํ™”์‚ฐ์„ฌ ํ˜•์„ฑ"
1817
+ elif stage < 0.6:
1818
+ return "๐Ÿ”ฅ ์šฉ์•”๋ฅ˜ ํ™•์žฅ: ํŒŒํ˜ธ์ดํ˜ธ์ด ์šฉ์•” ํ๋ฆ„"
1819
+ elif stage < 0.8:
1820
+ return "โ›ฐ๏ธ ์ˆœ์ƒ ํ˜•์„ฑ: ์™„๋งŒํ•œ ๊ฒฝ์‚ฌ (5-10ยฐ)"
1821
+ else:
1822
+ return "๐Ÿ•ณ๏ธ ์ •์ƒ ์นผ๋ฐ๋ผ: ๋งˆ๊ทธ๋งˆ ๋น ์ง โ†’ ํ•จ๋ชฐ"
1823
+
1824
+
1825
  def create_stratovolcano(grid_size: int = 100, stage: float = 1.0,
1826
+ max_height: float = 80.0, return_metadata: bool = False) -> np.ndarray:
1827
+ """์„ฑ์ธตํ™”์‚ฐ (Stratovolcano) - ๋ณตํ•ฉํ™”์‚ฐ
1828
+
1829
+ Stage 0~0.25: ์ดˆ๊ธฐ ๋ถ„์ถœ โ†’ ์›๋ฟ” ํ˜•์„ฑ ์‹œ์ž‘
1830
+ Stage 0.25~0.5: ์šฉ์•”+ํ™”์‡„๋ฌผ ๊ต๋Œ€ โ†’ ๊ธ‰๊ฒฝ์‚ฌ ์›๋ฟ”
1831
+ Stage 0.5~0.75: ๊ณ ๋„ ์ƒ์Šน + ๋ถ„ํ™”๊ตฌ ๋ฐœ๋‹ฌ
1832
+ Stage 0.75~1.0: ์ •์ƒ ๋ถ„ํ™”๊ตฌ + ํ™”์‚ฐ์‡„์„ค๋ฌผ ์‚ฌ๋ฉด
1833
+
1834
+ ํŠน์ง•:
1835
+ - ์•ˆ์‚ฐ์•”์งˆ/์œ ๋ฌธ์•”์งˆ ๋งˆ๊ทธ๋งˆ
1836
+ - ๊ฒฝ์‚ฌ 25-35ยฐ
1837
+ - ์šฉ์•”๋ฅ˜ + ํ™”์‡„๋ฅ˜ ๊ต๋Œ€์ธต
1838
+ """
1839
  h, w = grid_size, grid_size
1840
  elevation = np.zeros((h, w))
1841
 
1842
  center = (h // 2, w // 2)
1843
+ max_radius = int(w * 0.4)
1844
+
1845
+ # Stage์— ๋”ฐ๋ฅธ ์„ฑ์žฅ
1846
+ current_radius = int(max_radius * min(1.0, stage * 1.2))
1847
+ current_height = max_height * stage
1848
 
1849
  for r in range(h):
1850
  for c in range(w):
1851
  dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
1852
 
1853
+ if dist < current_radius and current_radius > 0:
1854
  # ๊ธ‰ํ•œ ์›๋ฟ” (๊ฒฝ์‚ฌ 25-35๋„)
1855
+ radial_factor = 1 - (dist / current_radius) ** 0.8
1856
+ elevation[r, c] = current_height * radial_factor
1857
+
1858
+ # ์ธต๋ฆฌ ํ‘œํ˜„ (์ž‘์€ ์š”์ฒ ) - stage 0.3 ์ดํ›„
1859
+ if stage > 0.3:
1860
+ np.random.seed(42)
1861
+ num_layers = int(5 * stage)
1862
+ for layer in range(num_layers):
1863
+ layer_radius = current_radius * (0.3 + 0.7 * layer / max(1, num_layers))
1864
+ layer_height = current_height * (0.2 + 0.6 * layer / max(1, num_layers))
1865
+
1866
+ for r in range(h):
1867
+ for c in range(w):
1868
+ dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
1869
+ if abs(dist - layer_radius) < 3:
1870
+ # ์ธต๋ฆฌ ๊ฒฝ๊ณ„์— ์•ฝ๊ฐ„์˜ ์š”์ฒ 
1871
+ bump = 1.5 * np.sin(np.arctan2(r - center[0], c - center[1]) * 8)
1872
+ elevation[r, c] += bump
1873
+
1874
+ # ์ •์ƒ๋ถ€ ๋ถ„ํ™”๊ตฌ - stage 0.5 ์ดํ›„
1875
+ if stage > 0.5:
1876
+ crater_progress = (stage - 0.5) / 0.5
1877
+ crater_radius = int(max_radius * 0.06 * (1 + crater_progress * 0.5))
1878
+ crater_depth = 12.0 * crater_progress
1879
+
1880
+ for r in range(h):
1881
+ for c in range(w):
1882
+ dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
1883
+ if dist < crater_radius:
1884
+ # ๋ถ„ํ™”๊ตฌ
1885
+ if dist < crater_radius * 0.7:
1886
+ elevation[r, c] = current_height - crater_depth
1887
+ else:
1888
+ # ๋ถ„ํ™”๊ตฌ ํ…Œ๋‘๋ฆฌ
1889
+ rim_factor = (dist - crater_radius * 0.7) / (crater_radius * 0.3)
1890
+ elevation[r, c] = current_height - crater_depth + crater_depth * rim_factor
1891
+
1892
+ if return_metadata:
1893
+ return elevation, {
1894
+ 'current_height': current_height,
1895
+ 'stage_description': _get_strato_stage_desc(stage)
1896
+ }
1897
+
1898
  return elevation
1899
 
1900
 
1901
+ def _get_strato_stage_desc(stage: float) -> str:
1902
+ """์„ฑ์ธตํ™”์‚ฐ ๋‹จ๊ณ„๋ณ„ ์„ค๋ช…"""
1903
+ if stage < 0.2:
1904
+ return "๐ŸŒ‹ ์ดˆ๊ธฐ ๋ถ„์ถœ: ํ™”์‚ฐ์‡„์„ค๋ฌผ ๋ถ„์ถœ"
1905
+ elif stage < 0.4:
1906
+ return "๐Ÿ”ฅ ์›๋ฟ” ํ˜•์„ฑ: ์šฉ์•” + ํ™”์‡„๋ฅ˜ ๊ต๋Œ€"
1907
+ elif stage < 0.6:
1908
+ return "โ›ฐ๏ธ ๊ธ‰๊ฒฝ์‚ฌ ๋ฐœ๋‹ฌ: ์„ฑ์ธต ๊ตฌ์กฐ ํ˜•์„ฑ"
1909
+ elif stage < 0.8:
1910
+ return "๐Ÿ—ป ๊ณ ๋„ ์ƒ์Šน: ๋ถ„ํ™”๊ตฌ ๋ฐœ๋‹ฌ"
1911
+ else:
1912
+ return "๐Ÿ’จ ์ •์ƒ ๋ถ„ํ™”๊ตฌ: ๋ถ„์—ฐ ํ™œ๋™ ๊ฐ€๋Šฅ"
1913
+
1914
+
1915
  def create_caldera(grid_size: int = 100, stage: float = 1.0,
1916
+ rim_height: float = 50.0, return_metadata: bool = False) -> np.ndarray:
1917
+ """์นผ๋ฐ๋ผ (Caldera) - ํ™”์‚ฐ ์ •์ƒ๋ถ€ ํ•จ๋ชฐ
1918
+
1919
+ Stage 0~0.3: ์„ฑ์ธตํ™”์‚ฐ ์„ฑ์žฅ (๋ถ„ํ™” ํ™œ๋™)
1920
+ Stage 0.3~0.5: ๋Œ€๋ถ„ํ™” โ†’ ๋งˆ๊ทธ๋งˆ๋ฐฉ ๊ณต๋™ํ™”
1921
+ Stage 0.5~0.8: ์ •์ƒ๋ถ€ ํ•จ๋ชฐ (์นผ๋ฐ๋ผ ํ˜•์„ฑ)
1922
+ Stage 0.8~1.0: ์นผ๋ฐ๋ผ ํ™•์žฅ + ํ˜ธ์ˆ˜ ํ˜•์„ฑ (๋ฐฑ๋‘์‚ฐ ์ฒœ์ง€)
1923
+
1924
+ ํ•ต์‹ฌ ๊ณผ์ •:
1925
+ - ๋งˆ๊ทธ๋งˆ๋ฐฉ ๋น„์›Œ์ง โ†’ ์ง€์ง€๋ ฅ ์ƒ์‹ค
1926
+ - ์ •์ƒ๋ถ€ ํ•จ๋ชฐ โ†’ ๋„“์€ ์›ํ˜• ๋ถ„์ง€
1927
+ - ์นผ๋ฐ๋ผ ์ง๊ฒฝ ์ˆ˜ km ~ ์ˆ˜์‹ญ km
1928
+ """
1929
  h, w = grid_size, grid_size
1930
  elevation = np.zeros((h, w))
1931
 
1932
  center = (h // 2, w // 2)
1933
+ max_outer = int(w * 0.45)
 
1934
 
1935
+ if stage < 0.3:
1936
+ # Stage 0~0.3: ์„ฑ์ธตํ™”์‚ฐ ์„ฑ์žฅ
1937
+ progress = stage / 0.3
1938
+ volcano_height = rim_height * 1.5 * progress
1939
+ volcano_radius = int(max_outer * 0.8 * progress)
1940
+
1941
+ for r in range(h):
1942
+ for c in range(w):
1943
+ dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
1944
+ if dist < volcano_radius and volcano_radius > 0:
1945
+ # ์„ฑ์ธตํ™”์‚ฐ ํ˜•ํƒœ
1946
+ elevation[r, c] = volcano_height * (1 - (dist / volcano_radius) ** 0.9)
1947
+
1948
+ # ์ž‘์€ ๋ถ„ํ™”๊ตฌ
1949
+ crater_r = max(2, int(volcano_radius * 0.08))
1950
+ for r in range(h):
1951
+ for c in range(w):
1952
+ dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
1953
+ if dist < crater_r:
1954
+ elevation[r, c] = volcano_height * 0.85
1955
 
1956
+ elif stage < 0.5:
1957
+ # Stage 0.3~0.5: ๋Œ€๋ถ„ํ™” ์‹œ์ž‘, ํ•จ๋ชฐ ์‹œ์ž‘
1958
+ progress = (stage - 0.3) / 0.2
1959
+ volcano_height = rim_height * 1.5
1960
+ collapse_depth = rim_height * 0.5 * progress
1961
+ collapse_radius = int(max_outer * 0.15 * (1 + progress))
1962
+
1963
+ for r in range(h):
1964
+ for c in range(w):
1965
+ dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
1966
+ if dist < max_outer * 0.8:
1967
+ # ํ™”์‚ฐ์ฒด
1968
+ base = volcano_height * (1 - (dist / (max_outer * 0.8)) ** 0.9)
1969
+
1970
+ if dist < collapse_radius:
1971
+ # ํ•จ๋ชฐ ์‹œ์ž‘
1972
+ elevation[r, c] = base - collapse_depth * (1 - (dist / collapse_radius) ** 2)
1973
+ else:
1974
+ elevation[r, c] = base
1975
+
1976
+ elif stage < 0.8:
1977
+ # Stage 0.5~0.8: ์นผ๋ฐ๋ผ ํ™•์žฅ
1978
+ progress = (stage - 0.5) / 0.3
1979
+ caldera_radius = int(max_outer * (0.2 + 0.25 * progress)) # ์ ์  ๋„“์–ด์ง
1980
+ collapse_depth = rim_height * (0.5 + 0.4 * progress)
1981
+
1982
+ for r in range(h):
1983
+ for c in range(w):
1984
+ dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
1985
+
1986
+ if dist < max_outer:
1987
+ if dist < caldera_radius:
1988
+ # ์นผ๋ฐ๋ผ ๋ฐ”๋‹ฅ (ํ‰ํƒ„)
1989
+ elevation[r, c] = rim_height * 1.5 - collapse_depth
1990
+ else:
1991
+ # ์นผ๋ฐ๋ผ ๋ฒฝ + ์™ธ๋ฅœ์‚ฐ
1992
+ wall_progress = (dist - caldera_radius) / (max_outer - caldera_radius)
1993
+ if wall_progress < 0.3:
1994
+ # ๊ธ‰๊ฒฝ์‚ฌ ๋ฒฝ
1995
+ elevation[r, c] = (rim_height * 1.5 - collapse_depth) + rim_height * 0.8 * (wall_progress / 0.3)
1996
+ else:
1997
+ # ์™ธ๋ฅœ์‚ฐ ์‚ฌ๋ฉด
1998
+ elevation[r, c] = rim_height * (1 - (wall_progress - 0.3) / 0.7) * 1.2
1999
+
2000
+ else:
2001
+ # Stage 0.8~1.0: ์นผ๋ฐ๋ผ ์™„์„ฑ + ํ˜ธ์ˆ˜
2002
+ progress = (stage - 0.8) / 0.2
2003
+ caldera_radius = int(max_outer * 0.45) # ์ตœ์ข… ํฌ๊ธฐ
2004
+
2005
+ for r in range(h):
2006
+ for c in range(w):
2007
+ dist = np.sqrt((r - center[0])**2 + (c - center[1])**2)
2008
+
2009
+ if dist < max_outer:
2010
+ if dist < caldera_radius:
2011
+ # ์นผ๋ฐ๋ผ ๋ฐ”๋‹ฅ (ํ˜ธ์ˆ˜)
2012
+ water_level = 5.0
2013
+ elevation[r, c] = water_level - 3.0 * (1 - (dist / caldera_radius) ** 2)
2014
+ elif dist < caldera_radius + 8:
2015
+ # ๊ธ‰๊ฒฝ์‚ฌ ๋ฒฝ
2016
+ wall_t = (dist - caldera_radius) / 8
2017
+ elevation[r, c] = 5.0 + rim_height * 0.9 * wall_t
2018
+ else:
2019
+ # ์™ธ๋ฅœ์‚ฐ
2020
+ outer_t = (dist - caldera_radius - 8) / (max_outer - caldera_radius - 8)
2021
+ elevation[r, c] = rim_height * (1 - outer_t ** 0.8) * 0.9
2022
+
2023
+ if return_metadata:
2024
+ return elevation, {
2025
+ 'stage_description': _get_caldera_stage_desc(stage)
2026
+ }
2027
+
2028
  return elevation
2029
 
2030
 
2031
+ def _get_caldera_stage_desc(stage: float) -> str:
2032
+ """์นผ๋ฐ๋ผ ๋‹จ๊ณ„๋ณ„ ์„ค๋ช…"""
2033
+ if stage < 0.2:
2034
+ return "๐ŸŒ‹ ์„ฑ์ธตํ™”์‚ฐ ์„ฑ์žฅ: ๋ถ„ํ™” ํ™œ๋™์œผ๋กœ ์‚ฐ์ฒด ํ˜•์„ฑ"
2035
+ elif stage < 0.4:
2036
+ return "๐Ÿ’ฅ ๋Œ€๋ถ„ํ™”: ๋งˆ๊ทธ๋งˆ ๋Œ€๋Ÿ‰ ๋ถ„์ถœ"
2037
+ elif stage < 0.6:
2038
+ return "๐Ÿ•ณ๏ธ ํ•จ๋ชฐ ์‹œ์ž‘: ๋งˆ๊ทธ๋งˆ๋ฐฉ ๋น„์›Œ์ง โ†’ ์ง€์ง€๋ ฅ ์ƒ์‹ค"
2039
+ elif stage < 0.8:
2040
+ return "โฌ‡๏ธ ์นผ๋ฐ๋ผ ํ™•์žฅ: ์ •์ƒ๋ถ€ ํ•จ๋ชฐ ํ™•๋Œ€"
2041
+ else:
2042
+ return "๐Ÿ’ง ์นผ๋ฐ๋ผ ํ˜ธ์ˆ˜: ์œตํ•ด์ˆ˜ ๊ณ ์ž„ (๋ฐฑ๋‘์‚ฐ ์ฒœ์ง€)"
2043
+
2044
+
2045
  def create_mesa_butte(grid_size: int = 100, stage: float = 1.0,
2046
  num_mesas: int = 2) -> np.ndarray:
2047
  """๋ฉ”์‚ฌ/๋ทฐํŠธ (Mesa/Butte) - ํƒ์ƒ์ง€"""
 
2678
  return elevation
2679
 
2680
 
2681
+ def create_lava_plateau(grid_size: int = 100, stage: float = 1.0,
2682
+ return_metadata: bool = False) -> np.ndarray:
2683
+ """์šฉ์•”๋Œ€์ง€ (Lava Plateau) - ํ•œํƒ„๊ฐ•/์ œ์ฃผ๋„ํ˜•
2684
 
2685
+ Stage 0~0.25: ์›๋ž˜ V์ž๊ณก ์กด์žฌ (ํ•˜์ฒœ ํ๋ฆ„)
2686
+ Stage 0.25~0.5: ์—ดํ•˜๋ถ„์ถœ โ†’ ์šฉ์•”์ด V์ž๊ณก ๋ฉ”์›€ (์šฉ์•”๋ฅ˜)
2687
+ Stage 0.5~0.75: ์šฉ์•”๋Œ€์ง€ ํ˜•์„ฑ (ํ‰ํƒ„ํ™”)
2688
+ Stage 0.75~1.0: ํ•˜์ฒœ ์žฌ์นจ์‹ โ†’ ์ƒˆ๋กœ์šด ํ˜‘๊ณก ํ˜•์„ฑ
2689
+
2690
+ ํ•ต์‹ฌ ๊ณผ์ •:
2691
+ - ์—ดํ•˜๋ถ„์ถœ(fissure eruption): ์„ ์ƒ์œผ๋กœ ์šฉ์•” ๋ถ„์ถœ
2692
+ - ํ™์ˆ˜ํ˜„๋ฌด์•”(flood basalt): ๋„“์€ ์ง€์—ญ ๋’ค๋ฎ์Œ
2693
+ - ์žฌ์นจ์‹(rejuvenation): ์ƒˆ ํ•˜์ฒœ์ด ํ˜‘๊ณก ํ˜•์„ฑ
2694
  """
2695
  h, w = grid_size, grid_size
2696
  elevation = np.zeros((h, w))
2697
+ lava_mask = np.zeros((h, w), dtype=bool) # ์šฉ์•” ์œ„์น˜ ํ‘œ์‹œ
2698
  center = w // 2
2699
 
2700
  # ๊ธฐ๋ฐ˜ ๊ณ ์› ๋†’์ด
2701
  plateau_base = 30.0
2702
 
2703
+ if stage < 0.25:
2704
+ # Stage 0~0.25: ์›๋ž˜ V์ž๊ณก (ํ•˜์ฒœ ํ๋ฆ„)
2705
+ v_depth = 30.0
2706
+ for r in range(h):
2707
+ for c in range(w):
2708
+ dx = abs(c - center)
2709
+ elevation[r, c] = plateau_base
2710
+
2711
+ # V์ž๊ณก
2712
+ if dx < 18:
2713
+ v_shape = v_depth * (1 - dx / 18) ** 1.2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2714
  elevation[r, c] -= v_shape
2715
+
2716
+ elif stage < 0.5:
2717
+ # Stage 0.25~0.5: ์šฉ์•” ๋ถ„์ถœ โ†’ ๊ณจ์งœ๊ธฐ ๋ฉ”์›€
2718
+ progress = (stage - 0.25) / 0.25
2719
+ v_depth = 30.0 * (1 - progress * 0.9) # V์ž๊ณก ์ ์  ๋ฉ”์›Œ์ง
2720
+ lava_thickness = 25.0 * progress
2721
+
2722
+ for r in range(h):
2723
+ # ์šฉ์•” ํ๋ฆ„ ๋ฒ”์œ„ (์ƒ๋ฅ˜์—์„œ ํ•˜๋ฅ˜๋กœ ์ง„ํ–‰)
2724
+ flow_reach = int(h * progress)
2725
 
2726
+ for c in range(w):
2727
+ dx = abs(c - center)
2728
+ elevation[r, c] = plateau_base
2729
+
2730
+ # ์ž”์—ฌ V์ž๊ณก
2731
+ if dx < 18:
2732
+ v_shape = v_depth * (1 - dx / 18) ** 1.2
2733
+ elevation[r, c] -= v_shape
2734
+
2735
+ # ์šฉ์•” ์ฑ„์›€
2736
+ if r < flow_reach and dx < 20:
2737
+ lava_fill = lava_thickness * (1 - dx / 20) ** 0.8
2738
+ elevation[r, c] += lava_fill
2739
+ lava_mask[r, c] = True
2740
 
2741
+ elif stage < 0.75:
2742
+ # Stage 0.5~0.75: ์šฉ์•”๋Œ€์ง€ ํ‰ํƒ„ํ™”
2743
+ progress = (stage - 0.5) / 0.25
2744
+
2745
+ for r in range(h):
2746
+ for c in range(w):
2747
+ dx = abs(c - center)
2748
 
2749
+ # ํ‰ํƒ„ํ•œ ์šฉ์•”๋Œ€์ง€
2750
+ if dx < 25:
2751
+ elevation[r, c] = plateau_base + 5.0
2752
+ lava_mask[r, c] = True
2753
+ else:
2754
+ # ๊ฐ€์žฅ์ž๋ฆฌ ๊ฒฝ์‚ฌ
2755
+ edge_t = (dx - 25) / (w // 2 - 25)
2756
+ elevation[r, c] = (plateau_base + 5.0) * (1 - edge_t ** 0.7)
2757
 
2758
+ else:
2759
+ # Stage 0.75~1.0: ์ƒˆ ํ˜‘๊ณก ํ˜•์„ฑ
2760
+ progress = (stage - 0.75) / 0.25
2761
+ gorge_width = int(6 + 6 * progress)
2762
+ gorge_depth = 35.0 * progress
2763
+
2764
+ for r in range(h):
2765
+ for c in range(w):
2766
+ dx = abs(c - center)
2767
+
2768
+ # ์šฉ์•”๋Œ€์ง€ ๊ธฐ๋ฐ˜
2769
+ if dx < 25:
2770
+ elevation[r, c] = plateau_base + 5.0
2771
+ lava_mask[r, c] = True
2772
+ else:
2773
+ edge_t = (dx - 25) / (w // 2 - 25)
2774
+ elevation[r, c] = (plateau_base + 5.0) * (1 - edge_t ** 0.7)
2775
 
2776
+ # ์ƒˆ๋กœ์šด ํ˜‘๊ณก (ํ•˜์ฒœ ์žฌ์นจ์‹)
2777
+ if dx < gorge_width:
2778
+ gorge_shape = gorge_depth * (1 - (dx / gorge_width) ** 2)
2779
+ elevation[r, c] -= gorge_shape
2780
+
2781
+ # ์ˆ˜์ง ์ ˆ๋ฒฝ ํ˜•์„ฑ (์ฃผ์ƒ์ ˆ๋ฆฌ ํšจ๊ณผ)
2782
+ if dx > gorge_width * 0.7:
2783
+ elevation[r, c] -= 3.0 # ๊ธ‰๊ฒฝ์‚ฌ
2784
+
2785
+ if return_metadata:
2786
+ return elevation, {
2787
+ 'lava_mask': lava_mask,
2788
+ 'stage_description': _get_lava_plateau_stage_desc(stage)
2789
+ }
2790
+
2791
  return elevation
2792
 
2793
 
2794
+ def _get_lava_plateau_stage_desc(stage: float) -> str:
2795
+ """์šฉ์•”๋Œ€์ง€ ๋‹จ๊ณ„๋ณ„ ์„ค๋ช…"""
2796
+ if stage < 0.2:
2797
+ return "๐Ÿž๏ธ ์›๋ž˜ V์ž๊ณก: ํ•˜์ฒœ ์นจ์‹์— ์˜ํ•œ ๊ณ„๊ณก"
2798
+ elif stage < 0.4:
2799
+ return "๐ŸŒ‹ ์—ดํ•˜๋ถ„์ถœ: ์šฉ์•”์ด ๊ณ„๊ณก์„ ๋”ฐ๋ผ ํ๋ฆ„"
2800
+ elif stage < 0.6:
2801
+ return "๐Ÿ”ฅ ์šฉ์•” ํ™์ˆ˜: ๊ณ„๊ณก์„ ์™„์ „ํžˆ ๋ฉ”์›€"
2802
+ elif stage < 0.8:
2803
+ return "โฌ› ์šฉ์•”๋Œ€์ง€ ํ˜•์„ฑ: ํ‰ํƒ„ํ•œ ํ˜„๋ฌด์•” ๋Œ€์ง€"
2804
+ else:
2805
+ return "๐Ÿž๏ธ ์žฌ์นจ์‹: ์ƒˆ๋กœ์šด ํ•˜์ฒœ์ด ํ˜‘๊ณก ํ˜•์„ฑ (์ฃผ์ƒ์ ˆ๋ฆฌ)"
2806
+
2807
+
2808
  def create_coastal_dune(grid_size: int = 100, stage: float = 1.0,
2809
  num_dunes: int = 3) -> np.ndarray:
2810
  """ํ•ด์•ˆ์‚ฌ๊ตฌ (Coastal Dune) - ํ•ด์•ˆ๊ฐ€ ๋ชจ๋ž˜ ์–ธ๋•"""
engine/script_engine.py CHANGED
@@ -54,7 +54,24 @@ class ScriptExecutor:
54
  'min': min,
55
  'abs': abs,
56
  'pow': pow,
57
- 'print': print # ๋””๋ฒ„๊น…์šฉ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  }
59
 
60
  # 2. ๊ธˆ์ง€๋œ ํ‚ค์›Œ๋“œ ์ฒดํฌ (๊ธฐ๋ณธ์ ์ธ ๋ณด์•ˆ)
 
54
  'min': min,
55
  'abs': abs,
56
  'pow': pow,
57
+ 'print': print, # ๋””๋ฒ„๊น…์šฉ
58
+ 'range': range,
59
+ 'len': len,
60
+ 'int': int,
61
+ 'float': float,
62
+ 'round': round,
63
+ 'sum': sum,
64
+ 'enumerate': enumerate,
65
+ 'zip': zip,
66
+ 'list': list,
67
+ 'tuple': tuple,
68
+ 'dict': dict,
69
+ 'set': set,
70
+ 'str': str,
71
+ 'bool': bool,
72
+ 'True': True,
73
+ 'False': False,
74
+ 'None': None,
75
  }
76
 
77
  # 2. ๊ธˆ์ง€๋œ ํ‚ค์›Œ๋“œ ์ฒดํฌ (๊ธฐ๋ณธ์ ์ธ ๋ณด์•ˆ)
pages/1_๐Ÿ“–_Gallery.py CHANGED
@@ -14,6 +14,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspa
14
 
15
  from engine.ideal_landforms import IDEAL_LANDFORM_GENERATORS, ANIMATED_LANDFORM_GENERATORS
16
  from app.components.renderer import render_terrain_plotly
 
17
 
18
  st.header("๐Ÿ“– ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ")
19
  st.markdown("_๊ต๊ณผ์„œ์ ์ธ ์ง€ํ˜• ํ˜•ํƒœ๋ฅผ ๊ธฐํ•˜ํ•™์  ๋ชจ๋ธ๋กœ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค._")
@@ -239,11 +240,44 @@ if landform_key in ANIMATED_LANDFORM_GENERATORS:
239
  # ๋‹จ๊ณ„๋ณ„ ์„ค๋ช… ํ‘œ์‹œ
240
  st.success(metadata.get('stage_description', ''))
241
 
242
- # ์„ ์ƒ์ง€ ์กด ์ •๋ณด
243
  if landform_key == 'alluvial_fan' and 'zone_info' in metadata:
244
- with st.expander("๐Ÿ“Š ์„ธ๋ถ€ ๊ตฌ์กฐ ๋ณด๊ธฐ"):
245
- for zone_id, info in metadata['zone_info'].items():
246
- st.markdown(f"**{info['name']}**: ๊ฒฝ์‚ฌ {info['slope']}, {info['sediment']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
  # ํ”ผ์˜ค๋ฅด๋“œ ํ”„๋กœ์„ธ์Šค ์ •๋ณด
249
  if landform_key == 'fjord' and 'process_info' in metadata:
@@ -309,38 +343,52 @@ if landform_key in ANIMATED_LANDFORM_GENERATORS:
309
  if 0 <= c < gallery_grid_size:
310
  stage_water[r, c] = 3.0
311
 
312
- # 3D ๋ Œ๋”๋ง
313
- fig_stage = render_terrain_plotly(
314
- stage_elev,
315
- f"{selected_landform} - {int(stage_value*100)}%",
316
- add_water=True,
317
- water_depth_grid=stage_water,
318
- water_level=-999,
319
- force_camera=False, # ์นด๋ฉ”๋ผ ์ด๋™ ํ—ˆ์šฉ
320
- landform_type=landform_type
321
  )
322
- st.plotly_chart(fig_stage, use_container_width=True, key="stage_view")
323
 
324
- # ์ž๋™ ์žฌ์ƒ (์„ธ์…˜ ์ƒํƒœ ํ™œ์šฉ)
325
- col_play, col_step = st.columns(2)
326
- with col_play:
327
- if st.button("โ–ถ๏ธ ์ž๋™ ์žฌ์ƒ ์‹œ์ž‘", key="auto_play"):
328
- st.session_state['auto_playing'] = True
329
- st.session_state['auto_stage'] = 0.0
330
- with col_step:
331
- if st.button("โน๏ธ ์ •์ง€", key="stop_play"):
332
- st.session_state['auto_playing'] = False
333
-
334
- # ์ž๋™ ์žฌ์ƒ ์ค‘์ด๋ฉด stage ์ž๋™ ์ฆ๊ฐ€
335
- if st.session_state.get('auto_playing', False):
336
- current_stage = st.session_state.get('auto_stage', 0.0)
337
- if current_stage < 1.0:
338
- st.session_state['auto_stage'] = current_stage + 0.04
339
- import time
340
- time.sleep(0.15)
341
- st.rerun()
342
- else:
343
- st.session_state['auto_playing'] = False
344
- st.success("โœ… ์™„๋ฃŒ!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
- st.caption("๐Ÿ’ก **Tip:** ์นด๋ฉ”๋ผ ๊ฐ๋„๋ฅผ ๋จผ์ € ์กฐ์ •ํ•œ ํ›„ ์ž๋™ ์žฌ์ƒํ•˜๋ฉด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.")
 
14
 
15
  from engine.ideal_landforms import IDEAL_LANDFORM_GENERATORS, ANIMATED_LANDFORM_GENERATORS
16
  from app.components.renderer import render_terrain_plotly
17
+ from app.components.animation_renderer import create_animated_terrain_figure
18
 
19
  st.header("๐Ÿ“– ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ")
20
  st.markdown("_๊ต๊ณผ์„œ์ ์ธ ์ง€ํ˜• ํ˜•ํƒœ๋ฅผ ๊ธฐํ•˜ํ•™์  ๋ชจ๋ธ๋กœ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค._")
 
240
  # ๋‹จ๊ณ„๋ณ„ ์„ค๋ช… ํ‘œ์‹œ
241
  st.success(metadata.get('stage_description', ''))
242
 
243
+ # ์„ ์ƒ์ง€ ์กด ์ •๋ณด + ์ƒ‰์ƒ ํ•˜์ด๋ผ์ดํŠธ
244
  if landform_key == 'alluvial_fan' and 'zone_info' in metadata:
245
+ with st.expander("๐Ÿ“Š ์„ธ๋ถ€ ๊ตฌ์กฐ ๋ณด๊ธฐ", expanded=True):
246
+ col_z1, col_z2, col_z3 = st.columns(3)
247
+ col_z1.markdown("๐Ÿ”ด **์„ ์ •(Apex)**<br>๊ฒฝ์‚ฌ 5-15ยฐ, ์—ญ๋ ฅ", unsafe_allow_html=True)
248
+ col_z2.markdown("๐ŸŸก **์„ ์•™(Mid)**<br>๊ฒฝ์‚ฌ 2-5ยฐ, ์‚ฌ์งˆ", unsafe_allow_html=True)
249
+ col_z3.markdown("๐Ÿ”ต **์„ ๋‹จ(Toe)**<br>๊ฒฝ์‚ฌ <2ยฐ, ๋‹ˆ์งˆ", unsafe_allow_html=True)
250
+
251
+ show_zones = st.checkbox("๐ŸŽจ ์กด ์ƒ‰์ƒ ์˜ค๋ฒ„๋ ˆ์ด ํ‘œ์‹œ", value=False, key="show_zone_colors")
252
+
253
+ if show_zones and 'zone_mask' in metadata:
254
+ # ์กด ๋งˆ์Šคํฌ๋ฅผ ์ƒ‰์ƒ์œผ๋กœ ํ‘œ์‹œ
255
+ st.info("๐Ÿ”ด ์„ ์ • | ๐ŸŸก ์„ ์•™ | ๐Ÿ”ต ์„ ๋‹จ")
256
+
257
+ import matplotlib.pyplot as plt
258
+ from matplotlib.colors import ListedColormap
259
+
260
+ zone_mask = metadata['zone_mask']
261
+ cmap = ListedColormap(['#4682B4', '#FFD700', '#FF6347', '#228B22']) # ๋ฐฐ๊ฒฝ, ์„ ๋‹จ, ์„ ์•™, ์„ ์ •
262
+
263
+ fig_zone, ax = plt.subplots(figsize=(8, 6))
264
+ im = ax.imshow(zone_mask, cmap=cmap, origin='lower', alpha=0.8)
265
+ ax.contour(stage_elev, levels=10, colors='white', linewidths=0.5, alpha=0.5)
266
+ ax.set_title("์„ ์ƒ์ง€ ์กด ๊ตฌ๋ถ„")
267
+ ax.set_xlabel("X")
268
+ ax.set_ylabel("Y")
269
+
270
+ # ๋ฒ”๋ก€
271
+ from matplotlib.patches import Patch
272
+ legend_elements = [
273
+ Patch(facecolor='#FF6347', label='์„ ์ •(Apex)'),
274
+ Patch(facecolor='#FFD700', label='์„ ์•™(Mid)'),
275
+ Patch(facecolor='#4682B4', label='์„ ๋‹จ(Toe)')
276
+ ]
277
+ ax.legend(handles=legend_elements, loc='upper right')
278
+
279
+ st.pyplot(fig_zone)
280
+ plt.close(fig_zone)
281
 
282
  # ํ”ผ์˜ค๋ฅด๋“œ ํ”„๋กœ์„ธ์Šค ์ •๋ณด
283
  if landform_key == 'fjord' and 'process_info' in metadata:
 
343
  if 0 <= c < gallery_grid_size:
344
  stage_water[r, c] = 3.0
345
 
346
+ # ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ชจ๋“œ ์„ ํƒ
347
+ st.markdown("---")
348
+ animation_mode = st.radio(
349
+ "์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ชจ๋“œ",
350
+ ["๐ŸŽฌ ๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜ (์ถ”์ฒœ)", "๐Ÿ“Š ์Šฌ๋ผ์ด๋” ์ˆ˜๋™ ์กฐ์ž‘"],
351
+ horizontal=True,
352
+ key="anim_mode"
 
 
353
  )
 
354
 
355
+ if animation_mode == "๐ŸŽฌ ๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜ (์ถ”์ฒœ)":
356
+ # Plotly ๋„ค์ดํ‹ฐ๋ธŒ ์• ๋‹ˆ๋ฉ”์ด์…˜ (์นด๋ฉ”๋ผ ์œ ์ง€!)
357
+ st.info("โ–ถ๏ธ **์žฌ์ƒ** ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค. **์นด๋ฉ”๋ผ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์กฐ์ž‘**ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!")
358
+
359
+ try:
360
+ fig_animated = create_animated_terrain_figure(
361
+ landform_func=anim_func,
362
+ grid_size=gallery_grid_size,
363
+ num_frames=20,
364
+ title=f"{selected_landform} ํ˜•์„ฑ ๊ณผ์ •",
365
+ landform_type=landform_type
366
+ )
367
+ st.plotly_chart(fig_animated, use_container_width=True, key="animated_view")
368
+ except Exception as e:
369
+ st.error(f"์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ ์˜ค๋ฅ˜: {e}")
370
+ # ํด๋ฐฑ: ์ •์  ๋ Œ๋”๋ง
371
+ fig_stage = render_terrain_plotly(
372
+ stage_elev,
373
+ f"{selected_landform} - {int(stage_value*100)}%",
374
+ add_water=True,
375
+ water_depth_grid=stage_water,
376
+ water_level=-999,
377
+ force_camera=False,
378
+ landform_type=landform_type
379
+ )
380
+ st.plotly_chart(fig_stage, use_container_width=True, key="stage_view_fallback")
381
+ else:
382
+ # ๊ธฐ์กด ์Šฌ๋ผ์ด๋” ๋ฐฉ์‹
383
+ fig_stage = render_terrain_plotly(
384
+ stage_elev,
385
+ f"{selected_landform} - {int(stage_value*100)}%",
386
+ add_water=True,
387
+ water_depth_grid=stage_water,
388
+ water_level=-999,
389
+ force_camera=False,
390
+ landform_type=landform_type
391
+ )
392
+ st.plotly_chart(fig_stage, use_container_width=True, key="stage_view")
393
 
394
+ st.caption("๐Ÿ’ก **Tip:** '๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜' ๋ชจ๋“œ์—์„œ๋Š” ์นด๋ฉ”๋ผ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์กฐ์ž‘ํ•˜๋ฉด์„œ ์žฌ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!")
pages/3_๐Ÿงช_Lab.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๐Ÿงช Geo-Lab Script: ์‚ฌ์šฉ์ž ์ฝ”๋“œ๋กœ ์ง€ํ˜• ์ƒ์„ฑ
3
+ Python ์ฝ”๋“œ๋กœ ์ง์ ‘ ์ง€ํ˜•์„ ์ƒ์„ฑํ•˜๊ณ  ์กฐ์ž‘ํ•ฉ๋‹ˆ๋‹ค.
4
+ """
5
+ import streamlit as st
6
+ import numpy as np
7
+ import sys
8
+ import os
9
+
10
+ # ์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ ์ถ”๊ฐ€
11
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
12
+
13
+ from engine.grid import WorldGrid
14
+ from engine.script_engine import ScriptExecutor
15
+ from app.components.renderer import render_terrain_plotly
16
+ from app.components.animation_renderer import create_animated_terrain_figure
17
+ from engine.ideal_landforms import IDEAL_LANDFORM_GENERATORS
18
+
19
+ st.set_page_config(page_title="๐Ÿงช Lab Script", page_icon="๐Ÿงช", layout="wide")
20
+
21
+ st.header("๐Ÿงช Geo-Lab Script")
22
+ st.markdown("_Python ์ฝ”๋“œ๋กœ ์ง์ ‘ ์ง€ํ˜•์„ ์ƒ์„ฑํ•˜๊ณ  ์กฐ์ž‘ํ•ฉ๋‹ˆ๋‹ค._")
23
+
24
+ # ์‚ฌ์ด๋“œ๋ฐ” ์„ค์ •
25
+ st.sidebar.subheader("โš™๏ธ ๊ทธ๋ฆฌ๋“œ ์„ค์ •")
26
+ grid_size = st.sidebar.slider("๊ทธ๋ฆฌ๋“œ ํฌ๊ธฐ", 50, 200, 100)
27
+
28
+ # ํƒญ ๊ตฌ์„ฑ
29
+ tab1, tab2, tab3 = st.tabs(["๐Ÿ“ ์ฝ”๋“œ ํŽธ์ง‘", "๐Ÿ“š ์˜ˆ์ œ ์ฝ”๋“œ", "๐Ÿ“– ๋„์›€๋ง"])
30
+
31
+ with tab1:
32
+ st.subheader("๐Ÿ“ ์ฝ”๋“œ ํŽธ์ง‘๊ธฐ")
33
+
34
+ # ๊ธฐ๋ณธ ์ฝ”๋“œ ํ…œํ”Œ๋ฆฟ
35
+ default_code = '''# Geo-Lab Script ์˜ˆ์ œ
36
+ # ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ณ€์ˆ˜: elevation, bedrock, sediment, water_depth, np, math
37
+
38
+ # 1. ๊ธฐ๋ณธ ์ง€ํ˜• ์ƒ์„ฑ (ํ‰ํƒ„ํ•œ ํ‰์›)
39
+ h, w = elevation.shape
40
+ elevation[:, :] = 10.0
41
+
42
+ # 2. ์ค‘์•™์— ์›๋ฟ”ํ˜• ์‚ฐ ์ถ”๊ฐ€
43
+ center_y, center_x = h // 2, w // 2
44
+ for y in range(h):
45
+ for x in range(w):
46
+ dist = np.sqrt((y - center_y)**2 + (x - center_x)**2)
47
+ if dist < 30:
48
+ peak_height = 50.0 * (1 - dist / 30)
49
+ elevation[y, x] += peak_height
50
+
51
+ # 3. ํ•˜์ฒœ ์ถ”๊ฐ€ (์™ผ์ชฝ์—์„œ ์˜ค๋ฅธ์ชฝ์œผ๋กœ)
52
+ for x in range(w):
53
+ river_y = int(center_y + 10 * np.sin(x * 0.1))
54
+ for dy in range(-2, 3):
55
+ if 0 <= river_y + dy < h:
56
+ elevation[river_y + dy, x] -= 5.0
57
+ water_depth[river_y + dy, x] = 3.0
58
+
59
+ print("์ง€ํ˜• ์ƒ์„ฑ ์™„๋ฃŒ!")
60
+ '''
61
+
62
+ # ์„ธ์…˜ ์ƒํƒœ์— ์ฝ”๋“œ ์ €์žฅ
63
+ if 'user_script' not in st.session_state:
64
+ st.session_state['user_script'] = default_code
65
+
66
+ # ์ฝ”๋“œ ํŽธ์ง‘๊ธฐ
67
+ user_code = st.text_area(
68
+ "Python ์ฝ”๋“œ",
69
+ value=st.session_state.get('user_script', default_code),
70
+ height=400,
71
+ key="code_editor"
72
+ )
73
+ st.session_state['user_script'] = user_code
74
+
75
+ col1, col2 = st.columns(2)
76
+
77
+ with col1:
78
+ run_button = st.button("โ–ถ๏ธ ์‹คํ–‰", type="primary", use_container_width=True)
79
+ with col2:
80
+ reset_button = st.button("๐Ÿ”„ ์ดˆ๊ธฐํ™”", use_container_width=True)
81
+
82
+ if reset_button:
83
+ st.session_state['user_script'] = default_code
84
+ st.rerun()
85
+
86
+ if run_button:
87
+ with st.spinner("์ฝ”๋“œ ์‹คํ–‰ ์ค‘..."):
88
+ try:
89
+ # ๊ทธ๋ฆฌ๋“œ ์ƒ์„ฑ
90
+ grid = WorldGrid(grid_size, grid_size)
91
+ executor = ScriptExecutor(grid)
92
+
93
+ # ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰
94
+ success, message = executor.execute(user_code)
95
+
96
+ if success:
97
+ st.success(f"โœ… {message}")
98
+
99
+ # ๊ฒฐ๊ณผ ์‹œ๊ฐํ™”
100
+ st.subheader("๐Ÿ“Š ๊ฒฐ๊ณผ")
101
+
102
+ # 2D / 3D ์„ ํƒ
103
+ view_mode = st.radio("๋ทฐ ๋ชจ๋“œ", ["3D", "2D"], horizontal=True)
104
+
105
+ if view_mode == "3D":
106
+ fig = render_terrain_plotly(
107
+ grid.elevation,
108
+ "์Šคํฌ๋ฆฝํŠธ ๊ฒฐ๊ณผ",
109
+ add_water=True,
110
+ water_depth_grid=grid.water_depth,
111
+ water_level=-999,
112
+ force_camera=False
113
+ )
114
+ st.plotly_chart(fig, use_container_width=True)
115
+ else:
116
+ import matplotlib.pyplot as plt
117
+ fig, ax = plt.subplots(figsize=(10, 8))
118
+ im = ax.imshow(grid.elevation, cmap='terrain', origin='lower')
119
+ plt.colorbar(im, ax=ax, label='Elevation (m)')
120
+ ax.set_title("2D ๊ณ ๋„ ๋งต")
121
+ st.pyplot(fig)
122
+
123
+ # ํ†ต๊ณ„
124
+ st.markdown("**๐Ÿ“ˆ ํ†ต๊ณ„:**")
125
+ col_s1, col_s2, col_s3, col_s4 = st.columns(4)
126
+ col_s1.metric("์ตœ์ € ๊ณ ๋„", f"{grid.elevation.min():.1f}m")
127
+ col_s2.metric("์ตœ๊ณ  ๊ณ ๋„", f"{grid.elevation.max():.1f}m")
128
+ col_s3.metric("ํ‰๊ท  ๊ณ ๋„", f"{grid.elevation.mean():.1f}m")
129
+ col_s4.metric("์ˆ˜์—ญ ๋น„์œจ", f"{(grid.water_depth > 0).sum() / grid.water_depth.size * 100:.1f}%")
130
+
131
+ else:
132
+ st.error(f"โŒ {message}")
133
+
134
+ except Exception as e:
135
+ st.error(f"โŒ ์˜ค๋ฅ˜: {str(e)}")
136
+
137
+ with tab2:
138
+ st.subheader("๐Ÿ“š ๊ฒ€์ฆ๋œ ์ง€ํ˜• ๋ถˆ๋Ÿฌ์˜ค๊ธฐ")
139
+ st.markdown("_์ด๋ฏธ ๊ตฌํ˜„๋œ ์ง€ํ˜•์„ ๋ถˆ๋Ÿฌ์™€์„œ ๋ณ€ํ˜•ํ•˜๊ฑฐ๋‚˜ ํ•™์Šตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค._")
140
+
141
+ # ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง€ํ˜• ๋ชฉ๋ก
142
+ landform_categories = {
143
+ "๐ŸŒŠ ํ•˜์ฒœ ์ง€ํ˜•": ["v_valley", "meander", "free_meander", "alluvial_fan", "incised_meander", "delta", "waterfall"],
144
+ "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ": ["bird_foot_delta", "arcuate_delta", "cuspate_delta"],
145
+ "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•": ["u_valley", "cirque", "horn", "arete", "fjord", "drumlin"],
146
+ "๐ŸŒ‹ ํ™”์‚ฐ ์ง€ํ˜•": ["shield_volcano", "stratovolcano", "caldera", "cinder_cone"],
147
+ "๐Ÿฆ‡ ์นด๋ฅด์ŠคํŠธ": ["karst_doline", "uvala", "tower_karst"],
148
+ "๐Ÿœ๏ธ ๊ฑด์กฐ ์ง€ํ˜•": ["barchan_dune", "mesa", "pedestal_rock", "wadi", "playa"],
149
+ "๐Ÿ–๏ธ ํ•ด์•ˆ ์ง€ํ˜•": ["coastal_cliff", "spit", "lagoon", "tombolo"]
150
+ }
151
+
152
+ selected_cat = st.selectbox("์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ", list(landform_categories.keys()))
153
+ available_landforms = [lf for lf in landform_categories[selected_cat] if lf in IDEAL_LANDFORM_GENERATORS]
154
+
155
+ if available_landforms:
156
+ selected_landform = st.selectbox("์ง€ํ˜• ์„ ํƒ", available_landforms)
157
+
158
+ col1, col2 = st.columns(2)
159
+
160
+ with col1:
161
+ load_size = st.slider("๊ทธ๋ฆฌ๋“œ ํฌ๊ธฐ", 50, 150, 100, key="load_size")
162
+ with col2:
163
+ load_stage = st.slider("ํ˜•์„ฑ ๋‹จ๊ณ„", 0.0, 1.0, 1.0, 0.1, key="load_stage")
164
+
165
+ if st.button("๐Ÿ”„ ์ง€ํ˜• ๋ถˆ๋Ÿฌ์˜ค๊ธฐ", type="primary", use_container_width=True):
166
+ try:
167
+ # ์ง€ํ˜• ์ƒ์„ฑ ํ•จ์ˆ˜ ํ˜ธ์ถœ
168
+ landform_func = IDEAL_LANDFORM_GENERATORS[selected_landform]
169
+
170
+ # stage ํŒŒ๋ผ๋ฏธํ„ฐ ์ง€์› ์—ฌ๋ถ€ ํ™•์ธ
171
+ import inspect
172
+ sig = inspect.signature(landform_func)
173
+ params = list(sig.parameters.keys())
174
+
175
+ if 'stage' in params:
176
+ result = landform_func(load_size, load_stage)
177
+ else:
178
+ result = landform_func(load_size)
179
+
180
+ # ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ (tuple์ธ ๊ฒฝ์šฐ elevation๋งŒ ์ถ”์ถœ)
181
+ if isinstance(result, tuple):
182
+ elevation = result[0]
183
+ else:
184
+ elevation = result
185
+
186
+ st.success(f"โœ… {selected_landform} ์ง€ํ˜• ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์™„๋ฃŒ!")
187
+
188
+ # 3D ์‹œ๊ฐํ™”
189
+ fig = render_terrain_plotly(
190
+ elevation,
191
+ f"{selected_landform} (Stage {int(load_stage*100)}%)",
192
+ add_water=True,
193
+ water_level=-999,
194
+ force_camera=False
195
+ )
196
+ st.plotly_chart(fig, use_container_width=True)
197
+
198
+ # ํ†ต๊ณ„
199
+ col_s1, col_s2, col_s3 = st.columns(3)
200
+ col_s1.metric("์ตœ์ € ๊ณ ๋„", f"{elevation.min():.1f}m")
201
+ col_s2.metric("์ตœ๊ณ  ๊ณ ๋„", f"{elevation.max():.1f}m")
202
+ col_s3.metric("ํ‰๊ท  ๊ณ ๋„", f"{elevation.mean():.1f}m")
203
+
204
+ # ์ฝ”๋“œ ํ™•์ธ
205
+ with st.expander("๐Ÿ’ป ์ด ์ง€ํ˜•์˜ ์ฝ”๋“œ ๋ณด๊ธฐ"):
206
+ st.markdown(f"""
207
+ ```python
208
+ from engine.ideal_landforms import IDEAL_LANDFORM_GENERATORS
209
+
210
+ # ์ง€ํ˜• ์ƒ์„ฑ
211
+ landform_func = IDEAL_LANDFORM_GENERATORS['{selected_landform}']
212
+ elevation = landform_func(grid_size={load_size}, stage={load_stage})
213
+
214
+ # ๊ฒฐ๊ณผ: {elevation.shape} ํฌ๊ธฐ์˜ ๊ณ ๋„ ๋ฐฐ์—ด
215
+ # ์ตœ์ €: {elevation.min():.1f}m, ์ตœ๊ณ : {elevation.max():.1f}m
216
+ ```
217
+
218
+ **ํ•จ์ˆ˜ ์†Œ์Šค ์œ„์น˜:** `engine/ideal_landforms.py` โ†’ `create_{selected_landform}()`
219
+ """)
220
+
221
+ except Exception as e:
222
+ st.error(f"โŒ ์˜ค๋ฅ˜: {str(e)}")
223
+ else:
224
+ st.warning("์ด ์นดํ…Œ๊ณ ๋ฆฌ์— ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ง€ํ˜•์ด ์—†์Šต๋‹ˆ๋‹ค.")
225
+
226
+ st.markdown("---")
227
+ st.markdown("""
228
+ ### ๐Ÿ’ก ํŒ: ์ง์ ‘ ์ฝ”๋“œ ์งœ๋Š” ๋ฒ•
229
+
230
+ **๊ธฐ๋ณธ ํŒจํ„ด:**
231
+ ```python
232
+ h, w = elevation.shape
233
+ center_y, center_x = h // 2, w // 2
234
+
235
+ for y in range(h):
236
+ for x in range(w):
237
+ dist = np.sqrt((y - center_y)**2 + (x - center_x)**2)
238
+ # ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ˜ ๋†’์ด ๊ณ„์‚ฐ
239
+ elevation[y, x] = ๋†’์ด๊ณต์‹(dist)
240
+ ```
241
+
242
+ **ํ•ต์‹ฌ ์›์น™:**
243
+ 1. `elevation[:, :]`๋กœ ์ „์ฒด ์ดˆ๊ธฐํ™”
244
+ 2. for ๋ฃจํ”„๋กœ ๊ฐ ์…€ ์ˆœํšŒ
245
+ 3. ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ˜ ๋†’์ด ๊ณต์‹ ์ ์šฉ
246
+ 4. `water_depth`๋กœ ๋ฌผ ํ‘œ์‹œ
247
+ """)
248
+
249
+ with tab3:
250
+ st.subheader("๐Ÿ“– ๋„์›€๋ง")
251
+
252
+ st.markdown("""
253
+ ### ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ณ€์ˆ˜
254
+
255
+ | ๋ณ€์ˆ˜ | ํƒ€์ž… | ์„ค๋ช… |
256
+ |------|------|------|
257
+ | `elevation` | np.ndarray | ๊ณ ๋„ ๋ฐฐ์—ด (์ˆ˜์ • ๊ฐ€๋Šฅ) |
258
+ | `bedrock` | np.ndarray | ๊ธฐ๋ฐ˜์•” ๋ฐฐ์—ด |
259
+ | `sediment` | np.ndarray | ํ‡ด์ ๋ฌผ ๋ฐฐ์—ด |
260
+ | `water_depth` | np.ndarray | ์ˆ˜์‹ฌ ๋ฐฐ์—ด |
261
+ | `np` | module | NumPy ๋ชจ๋“ˆ |
262
+ | `math` | module | math ๋ชจ๋“ˆ |
263
+
264
+ ### ๊ธฐ๋ณธ ํŒจํ„ด
265
+
266
+ ```python
267
+ # ๊ทธ๋ฆฌ๋“œ ํฌ๊ธฐ ๊ฐ€์ ธ์˜ค๊ธฐ
268
+ h, w = elevation.shape
269
+
270
+ # ์ „์ฒด ๊ณ ๋„ ์„ค์ •
271
+ elevation[:, :] = 10.0
272
+
273
+ # ํŠน์ • ์˜์—ญ ์ˆ˜์ •
274
+ elevation[10:20, 30:40] = 50.0
275
+
276
+ # ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ˜ ์ง€ํ˜•
277
+ for y in range(h):
278
+ for x in range(w):
279
+ dist = np.sqrt((y - center_y)**2 + (x - center_x)**2)
280
+ elevation[y, x] = some_function(dist)
281
+ ```
282
+
283
+ ### ์ฃผ์˜์‚ฌํ•ญ
284
+
285
+ - `import` ๋ฌธ์€ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (๋ณด์•ˆ)
286
+ - `open()`, `exec()`, `eval()` ์‚ฌ์šฉ ๋ถˆ๊ฐ€
287
+ - ๋ฌดํ•œ ๋ฃจํ”„ ์ฃผ์˜ (๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ฉˆ์ถœ ์ˆ˜ ์žˆ์Œ)
288
+ """)