HANSOL commited on
Commit
e60b579
ยท
1 Parent(s): a8ee0d8

Add 6 new landforms, improve animation, add Overview page

Browse files
app/pages/1_๐Ÿ“–_Gallery.py CHANGED
@@ -54,6 +54,7 @@ if category == "๐ŸŒŠ ํ•˜์ฒœ ์ง€ํ˜•":
54
  "๐Ÿ”๏ธ V์ž๊ณก (V-Valley)": "v_valley",
55
  "๐ŸŒŠ ๋ง์ƒํ•˜์ฒœ (Braided River)": "braided_river",
56
  "๐Ÿ’ง ํญํฌ (Waterfall)": "waterfall",
 
57
  }
58
  elif category == "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•":
59
  landform_options = {
@@ -61,6 +62,7 @@ elif category == "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•":
61
  "๐Ÿฆถ ์กฐ์กฑ์ƒ ์‚ผ๊ฐ์ฃผ (Bird-foot)": "bird_foot_delta",
62
  "๐ŸŒ™ ํ˜ธ์ƒ ์‚ผ๊ฐ์ฃผ (Arcuate)": "arcuate_delta",
63
  "๐Ÿ“ ์ฒจ๋‘์ƒ ์‚ผ๊ฐ์ฃผ (Cuspate)": "cuspate_delta",
 
64
  }
65
  elif category == "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•":
66
  landform_options = {
@@ -70,6 +72,7 @@ elif category == "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•":
70
  "๐ŸŒŠ ํ”ผ์˜ค๋ฅด๋“œ (Fjord)": "fjord",
71
  "๐Ÿฅš ๋“œ๋Ÿผ๋ฆฐ (Drumlin)": "drumlin",
72
  "๐Ÿชจ ๋น™ํ‡ด์„ (Moraine)": "moraine",
 
73
  }
74
  elif category == "๐ŸŒ‹ ํ™”์‚ฐ ์ง€ํ˜•":
75
  landform_options = {
@@ -92,6 +95,9 @@ elif category == "๐Ÿœ๏ธ ๊ฑด์กฐ ์ง€ํ˜•":
92
  "๐ŸŸฐ ํšก์‚ฌ๊ตฌ (Transverse Dune)": "transverse_dune",
93
  "โญ ์„ฑ์‚ฌ๊ตฌ (Star Dune)": "star_dune",
94
  "๐Ÿ—ฟ ๋ฉ”์‚ฌ/๋ทฐํŠธ (Mesa/Butte)": "mesa_butte",
 
 
 
95
  }
96
  else: # ํ•ด์•ˆ ์ง€ํ˜•
97
  landform_options = {
 
54
  "๐Ÿ”๏ธ V์ž๊ณก (V-Valley)": "v_valley",
55
  "๐ŸŒŠ ๋ง์ƒํ•˜์ฒœ (Braided River)": "braided_river",
56
  "๐Ÿ’ง ํญํฌ (Waterfall)": "waterfall",
57
+ "๐Ÿšง ์ฒœ์ •์ฒœ (Perched River)": "perched_river",
58
  }
59
  elif category == "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•":
60
  landform_options = {
 
62
  "๐Ÿฆถ ์กฐ์กฑ์ƒ ์‚ผ๊ฐ์ฃผ (Bird-foot)": "bird_foot_delta",
63
  "๐ŸŒ™ ํ˜ธ์ƒ ์‚ผ๊ฐ์ฃผ (Arcuate)": "arcuate_delta",
64
  "๐Ÿ“ ์ฒจ๋‘์ƒ ์‚ผ๊ฐ์ฃผ (Cuspate)": "cuspate_delta",
65
+ "๐ŸŒŠ ์—์Šค์ถ”์–ด๋ฆฌ (Estuary)": "estuary",
66
  }
67
  elif category == "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•":
68
  landform_options = {
 
72
  "๐ŸŒŠ ํ”ผ์˜ค๋ฅด๋“œ (Fjord)": "fjord",
73
  "๐Ÿฅš ๋“œ๋Ÿผ๋ฆฐ (Drumlin)": "drumlin",
74
  "๐Ÿชจ ๋น™ํ‡ด์„ (Moraine)": "moraine",
75
+ "๐Ÿ—ก๏ธ ์•„๋ ˆํŠธ (Arรชte)": "arete",
76
  }
77
  elif category == "๐ŸŒ‹ ํ™”์‚ฐ ์ง€ํ˜•":
78
  landform_options = {
 
95
  "๐ŸŸฐ ํšก์‚ฌ๊ตฌ (Transverse Dune)": "transverse_dune",
96
  "โญ ์„ฑ์‚ฌ๊ตฌ (Star Dune)": "star_dune",
97
  "๐Ÿ—ฟ ๋ฉ”์‚ฌ/๋ทฐํŠธ (Mesa/Butte)": "mesa_butte",
98
+ "๐Ÿœ๏ธ ์™€๋”” (Wadi)": "wadi",
99
+ "๐Ÿชถ ํ”Œ๋ผ์•ผ (Playa)": "playa",
100
+ "๐Ÿ„ ๋ฒ„์„ฏ๋ฐ”์œ„ (Pedestal Rock)": "pedestal_rock",
101
  }
102
  else: # ํ•ด์•ˆ ์ง€ํ˜•
103
  landform_options = {
engine/ideal_landforms.py CHANGED
@@ -2063,7 +2063,281 @@ def create_star_dune(grid_size: int = 100, stage: float = 1.0,
2063
  return elevation
2064
 
2065
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2066
  # ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ๊ธฐ ๋งคํ•‘
 
2067
  ANIMATED_LANDFORM_GENERATORS = {
2068
  'delta': create_delta_animated,
2069
  'alluvial_fan': create_alluvial_fan_animated,
@@ -2104,6 +2378,13 @@ ANIMATED_LANDFORM_GENERATORS = {
2104
  'karren': create_karren,
2105
  'transverse_dune': create_transverse_dune,
2106
  'star_dune': create_star_dune,
 
 
 
 
 
 
 
2107
  }
2108
 
2109
  # ์ง€ํ˜• ์ƒ์„ฑ ํ•จ์ˆ˜ ๋งคํ•‘
@@ -2147,5 +2428,12 @@ IDEAL_LANDFORM_GENERATORS = {
2147
  'karren': lambda gs: create_karren(gs, 1.0),
2148
  'transverse_dune': lambda gs: create_transverse_dune(gs, 1.0),
2149
  'star_dune': lambda gs: create_star_dune(gs, 1.0),
 
 
 
 
 
 
 
2150
  }
2151
 
 
2063
  return elevation
2064
 
2065
 
2066
+ # ============================================
2067
+ # ์ถ”๊ฐ€ ํ™•์žฅ ์ง€ํ˜•๋“ค (Additional Expansion)
2068
+ # ============================================
2069
+
2070
+ def create_perched_river(grid_size: int = 100, stage: float = 1.0):
2071
+ """์ฒœ์ •์ฒœ (Perched River) - ์ž์—ฐ์ œ๋ฐฉ ๋ฐœ๋‹ฌ๋กœ ํ•˜์ƒ์ด ์ฃผ๋ณ€๋ณด๋‹ค ๋†’์Œ
2072
+
2073
+ Stage 0~0.5: ๋ฒ”๋žŒ์› ํ˜•์„ฑ + ์ž์—ฐ์ œ๋ฐฉ ๋ฐœ๋‹ฌ
2074
+ Stage 0.5~1.0: ํ•˜์ƒ ํ‡ด์ ์œผ๋กœ ์ฃผ๋ณ€๋ณด๋‹ค ๋†’์•„์ง (์ฒœ์ •์ฒœ)
2075
+ """
2076
+ h, w = grid_size, grid_size
2077
+ elevation = np.zeros((h, w))
2078
+
2079
+ # ๋ฒ”๋žŒ์› ๊ธฐ๋ณธ ๋†’์ด
2080
+ base_height = 10.0
2081
+ elevation[:] = base_height
2082
+
2083
+ # ํ•˜์ฒœ ์ค‘์‹ฌ์„ 
2084
+ center = w // 2
2085
+
2086
+ # ์ž์—ฐ์ œ๋ฐฉ ๋ฐœ๋‹ฌ (stage์— ๋”ฐ๋ผ)
2087
+ levee_height = 8.0 * stage
2088
+ levee_width = int(w * 0.15)
2089
+
2090
+ for c in range(w):
2091
+ dist_from_center = abs(c - center)
2092
+
2093
+ if dist_from_center < levee_width:
2094
+ # ํ•˜์ƒ (ํ•˜์ฒœ ๋ฐ”๋‹ฅ) - ์ฃผ๋ณ€๋ณด๋‹ค ๋†’์•„์ง
2095
+ if dist_from_center < 5:
2096
+ river_bed_height = base_height + levee_height * 0.8 * stage
2097
+ elevation[:, c] = river_bed_height
2098
+ else:
2099
+ # ์ž์—ฐ์ œ๋ฐฉ (์ œ๋ฐฉ)
2100
+ decay = 1 - (dist_from_center - 5) / (levee_width - 5)
2101
+ elevation[:, c] = base_height + levee_height * decay * stage
2102
+ else:
2103
+ # ๋ฐฐํ›„์Šต์ง€ (๋‚ฎ์€ ๊ณณ)
2104
+ backswamp_depth = 3.0 * stage
2105
+ elevation[:, c] = base_height - backswamp_depth
2106
+
2107
+ return elevation
2108
+
2109
+
2110
+ def create_arete(grid_size: int = 100, stage: float = 1.0):
2111
+ """์•„๋ ˆํŠธ (Arรชte) - ๋น™ํ•˜์— ์˜ํ•ด ํ˜•์„ฑ๋œ ๋‚ ์นด๋กœ์šด ๋Šฅ์„ 
2112
+
2113
+ ๋‘ ๊ถŒ๊ณก ์‚ฌ์ด์˜ ๋‚ ์นด๋กœ์šด ๋Šฅ์„ 
2114
+ """
2115
+ h, w = grid_size, grid_size
2116
+ elevation = np.zeros((h, w))
2117
+
2118
+ # ๊ธฐ๋ณธ ๊ณ ์‚ฐ ์ง€ํ˜•
2119
+ base_height = 100.0
2120
+ elevation[:] = base_height
2121
+
2122
+ center = w // 2
2123
+
2124
+ # ์–‘์ชฝ์— ๊ถŒ๊ณก ํ˜•์„ฑ
2125
+ cirque_depth = 60.0 * stage
2126
+ cirque_radius = int(w * 0.35)
2127
+
2128
+ for r in range(h):
2129
+ for c in range(w):
2130
+ # ์™ผ์ชฝ ๊ถŒ๊ณก
2131
+ left_cx = center - int(w * 0.25)
2132
+ left_cy = int(h * 0.5)
2133
+ dist_left = np.sqrt((r - left_cy)**2 + (c - left_cx)**2)
2134
+
2135
+ # ์˜ค๋ฅธ์ชฝ ๊ถŒ๊ณก
2136
+ right_cx = center + int(w * 0.25)
2137
+ right_cy = int(h * 0.5)
2138
+ dist_right = np.sqrt((r - right_cy)**2 + (c - right_cx)**2)
2139
+
2140
+ if dist_left < cirque_radius:
2141
+ bowl_depth = cirque_depth * (1 - (dist_left / cirque_radius)**2)
2142
+ elevation[r, c] = min(elevation[r, c], base_height - bowl_depth)
2143
+
2144
+ if dist_right < cirque_radius:
2145
+ bowl_depth = cirque_depth * (1 - (dist_right / cirque_radius)**2)
2146
+ elevation[r, c] = min(elevation[r, c], base_height - bowl_depth)
2147
+
2148
+ # ์ค‘์•™ ๋Šฅ์„  (์•„๋ ˆํŠธ) ๊ฐ•์กฐ
2149
+ ridge_width = 5
2150
+ for c in range(center - ridge_width, center + ridge_width):
2151
+ if 0 <= c < w:
2152
+ sharpness = 1 - abs(c - center) / ridge_width
2153
+ elevation[:, c] = base_height + 10.0 * sharpness * stage
2154
+
2155
+ return elevation
2156
+
2157
+
2158
+ def create_wadi(grid_size: int = 100, stage: float = 1.0):
2159
+ """์™€๋”” (Wadi) - ๊ฑด์กฐ์ง€์—ญ ์ผ์‹œ์  ํ•˜์ฒœ ๊ณ„๊ณก
2160
+
2161
+ ํ‰์ƒ์‹œ ๊ฑด์กฐ, ์šฐ๊ธฐ์—๋งŒ ๋ฌผ์ด ํ๋ฅด๋Š” ๊ณ„๊ณก
2162
+ """
2163
+ h, w = grid_size, grid_size
2164
+ elevation = np.zeros((h, w))
2165
+
2166
+ # ๊ฑด์กฐ ๊ณ ์›
2167
+ base_height = 50.0
2168
+ elevation[:] = base_height
2169
+
2170
+ # ์™€๋”” ๊ณ„๊ณก ์ƒ์„ฑ (๊ตฌ๋ถˆ๊ตฌ๋ถˆ)
2171
+ center = w // 2
2172
+ valley_depth = 25.0 * stage
2173
+ valley_width = int(w * 0.2)
2174
+
2175
+ for r in range(h):
2176
+ # ๊ตฌ๋ถˆ๊ตฌ๋ถˆํ•œ ๊ณ„๊ณก ์ค‘์‹ฌ
2177
+ offset = int(15 * np.sin(r * 0.08))
2178
+ valley_center = center + offset
2179
+
2180
+ for c in range(w):
2181
+ dist = abs(c - valley_center)
2182
+ if dist < valley_width:
2183
+ # V์žํ˜• ๊ณ„๊ณก
2184
+ depth = valley_depth * (1 - dist / valley_width)
2185
+ elevation[r, c] = base_height - depth
2186
+
2187
+ # ๋ชจ๋ž˜/์ž๊ฐˆ ๋ฐ”๋‹ฅ (ํ‰ํƒ„)
2188
+ for r in range(h):
2189
+ offset = int(15 * np.sin(r * 0.08))
2190
+ valley_center = center + offset
2191
+ for c in range(valley_center - 3, valley_center + 3):
2192
+ if 0 <= c < w:
2193
+ elevation[r, c] = base_height - valley_depth + 2 # ํ‰ํƒ„ํ•œ ๋ฐ”๋‹ฅ
2194
+
2195
+ return elevation
2196
+
2197
+
2198
+ def create_playa(grid_size: int = 100, stage: float = 1.0):
2199
+ """ํ”Œ๋ผ์•ผ (Playa) - ๊ฑด์กฐ ํ˜ธ์ˆ˜ ๋ฐ”๋‹ฅ
2200
+
2201
+ ๊ฑด์กฐ์ง€์—ญ์—์„œ ๋ฌผ์ด ์ฆ๋ฐœํ•˜๊ณ  ๋‚จ์€ ํ‰ํƒ„ํ•œ ํ˜ธ์ˆ˜ ๋ฐ”๋‹ฅ
2202
+ """
2203
+ h, w = grid_size, grid_size
2204
+ elevation = np.zeros((h, w))
2205
+
2206
+ # ๋ถ„์ง€ ์ง€ํ˜•
2207
+ center_r, center_c = h // 2, w // 2
2208
+ basin_radius = int(min(h, w) * 0.4)
2209
+
2210
+ for r in range(h):
2211
+ for c in range(w):
2212
+ dist = np.sqrt((r - center_r)**2 + (c - center_c)**2)
2213
+
2214
+ if dist < basin_radius:
2215
+ # ๋ถ„์ง€ ๋‚ด๋ถ€ (ํ”Œ๋ผ์•ผ)
2216
+ # ๋งค์šฐ ํ‰ํƒ„ํ•œ ํ˜ธ์ˆ˜ ๋ฐ”๋‹ฅ
2217
+ elevation[r, c] = 10.0 + np.random.uniform(0, 0.5) # ๊ฑฐ์˜ ํ‰ํƒ„
2218
+ else:
2219
+ # ๋ถ„์ง€ ์™ธ๋ถ€ (์‚ฐ์ง€)
2220
+ rim_height = 50.0 * (1 - basin_radius / (dist + 1))
2221
+ elevation[r, c] = 30.0 + rim_height * stage
2222
+
2223
+ # ์†Œ๊ธˆ ๊ฒฐ์ • ํŒจํ„ด (๋‹ค๊ฐํ˜•)
2224
+ if stage > 0.7:
2225
+ for i in range(10):
2226
+ poly_r = center_r + np.random.randint(-basin_radius//2, basin_radius//2)
2227
+ poly_c = center_c + np.random.randint(-basin_radius//2, basin_radius//2)
2228
+ poly_size = np.random.randint(5, 15)
2229
+ for dr in range(-poly_size, poly_size):
2230
+ for dc in range(-poly_size, poly_size):
2231
+ if 0 <= poly_r+dr < h and 0 <= poly_c+dc < w:
2232
+ if abs(dr) + abs(dc) == poly_size - 1: # ํ…Œ๋‘๋ฆฌ
2233
+ elevation[poly_r+dr, poly_c+dc] += 0.3
2234
+
2235
+ return elevation
2236
+
2237
+
2238
+ def create_pedestal_rock(grid_size: int = 100, stage: float = 1.0):
2239
+ """๋ฒ„์„ฏ๋ฐ”์œ„ (Pedestal Rock) - ๋ฐ”๋žŒ์— ์˜ํ•œ ์ฐจ๋ณ„ํ’ํ™” ์ง€ํ˜•
2240
+
2241
+ Stage 0~0.3: ์›๋ž˜ ์•”์„ ๊ธฐ๋‘ฅ
2242
+ Stage 0.3~0.7: ๋ฐ”๋žŒ์— ์˜ํ•œ ํ•˜๋ถ€ ์นจ์‹ (์—ฐ๋งˆ์ž‘์šฉ)
2243
+ Stage 0.7~1.0: ๋ฒ„์„ฏ ๋ชจ์–‘ ์™„์„ฑ (์ค„๊ธฐ๊ฐ€ ๋งค์šฐ ์–‡์•„์ง)
2244
+
2245
+ ๋ฐ”๋žŒ์— ์‹ค๋ ค์˜จ ๋ชจ๋ž˜๊ฐ€ ํ•˜๋ถ€๋ฅผ ๊นŽ์•„๋ƒ„ (์ง€ํ‘œ ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ๋ชจ๋ž˜ ๋†๋„ ๋†’์Œ)
2246
+ """
2247
+ h, w = grid_size, grid_size
2248
+ elevation = np.zeros((h, w))
2249
+
2250
+ # ์‚ฌ๋ง‰ ํ‰์›
2251
+ base_height = 5.0
2252
+ elevation[:] = base_height
2253
+
2254
+ # ๋ฒ„์„ฏ๋ฐ”์œ„ ์—ฌ๋Ÿฌ ๊ฐœ
2255
+ num_rocks = 3
2256
+ np.random.seed(42)
2257
+
2258
+ for i in range(num_rocks):
2259
+ # ์œ„์น˜
2260
+ rock_r = np.random.randint(h // 4, 3 * h // 4)
2261
+ rock_c = np.random.randint(w // 4, 3 * w // 4)
2262
+
2263
+ # ์›๋ž˜ ๋ฐ”์œ„ ํฌ๊ธฐ (stage 0์—์„œ์˜ ํฌ๊ธฐ)
2264
+ original_radius = np.random.randint(10, 16)
2265
+ rock_height = np.random.uniform(25, 40)
2266
+
2267
+ # stage์— ๋”ฐ๋ฅธ ์นจ์‹ ์ •๋„ (stage ๋†’์„์ˆ˜๋ก ํ•˜๋ถ€ ๋” ๊นŽ์ž„)
2268
+ erosion_factor = stage # 0~1
2269
+
2270
+ for r in range(h):
2271
+ for c in range(w):
2272
+ dist = np.sqrt((r - rock_r)**2 + (c - rock_c)**2)
2273
+
2274
+ if dist < original_radius:
2275
+ # ๋ฐ”์œ„ ๋‚ด๋ถ€ - ๋†’์ด์— ๋”ฐ๋ผ ๋ฐ˜๊ฒฝ์ด ๋‹ค๋ฆ„
2276
+ # ์ƒ๋ถ€: ์›๋ž˜ ๋ฐ˜๊ฒฝ ์œ ์ง€
2277
+ # ํ•˜๋ถ€: stage์— ๋”ฐ๋ผ ๊นŽ์ž„
2278
+
2279
+ # ๊ฐ ๋†’์ด์—์„œ์˜ ์œ ํšจ ๋ฐ˜๊ฒฝ ๊ณ„์‚ฐ
2280
+ for z_level in range(int(rock_height)):
2281
+ # ์ง€ํ‘œ์—์„œ์˜ ๋†’์ด ๋น„์œจ (0=๋ฐ”๋‹ฅ, 1=๊ผญ๋Œ€๊ธฐ)
2282
+ height_ratio = z_level / rock_height
2283
+
2284
+ # ํ•˜๋ถ€์ผ์ˆ˜๋ก ๋ฐ”๋žŒ ์นจ์‹ ์‹ฌํ•จ (์ง€ํ‘œ ๊ฐ€๊นŒ์šธ์ˆ˜๋ก)
2285
+ if height_ratio < 0.5:
2286
+ # ํ•˜๋ถ€: ์นจ์‹์œผ๋กœ ๋ฐ˜๊ฒฝ ๊ฐ์†Œ
2287
+ erosion_at_height = erosion_factor * (1 - height_ratio * 2) # ๋ฐ”๋‹ฅ์—์„œ ์ตœ๋Œ€
2288
+ current_radius = original_radius * (1 - erosion_at_height * 0.7)
2289
+ else:
2290
+ # ์ƒ๋ถ€: ์›๋ž˜ ๋ฐ˜๊ฒฝ ์œ ์ง€ (๋ชจ์ž ๋ถ€๋ถ„)
2291
+ current_radius = original_radius
2292
+
2293
+ if dist < current_radius:
2294
+ elevation[r, c] = max(elevation[r, c], base_height + z_level)
2295
+
2296
+ return elevation
2297
+
2298
+
2299
+ def create_estuary(grid_size: int = 100, stage: float = 1.0):
2300
+ """์—์Šค์ถ”์–ด๋ฆฌ (Estuary) - ์‚ผ๊ฐ๊ฐ•, ์กฐ์„ ์˜ํ–ฅ
2301
+
2302
+ ์กฐ์„์˜ ์˜ํ–ฅ์„ ๋ฐ›๋Š” ๋„“์€ ํ•˜๊ตฌ
2303
+ """
2304
+ h, w = grid_size, grid_size
2305
+ elevation = np.zeros((h, w))
2306
+
2307
+ # ์œก์ง€ ๊ธฐ๋ณธ
2308
+ land_height = 20.0
2309
+ elevation[:] = land_height
2310
+
2311
+ # ์—์Šค์ถ”์–ด๋ฆฌ (๊น”๋•Œ๊ธฐ ๋ชจ์–‘)
2312
+ apex_row = int(h * 0.1)
2313
+ center = w // 2
2314
+
2315
+ for r in range(h):
2316
+ # ํ•˜๋ฅ˜๋กœ ๊ฐˆ์ˆ˜๋ก ๋„“์–ด์ง
2317
+ progress = (r - apex_row) / (h - apex_row) if r > apex_row else 0
2318
+ estuary_width = int(5 + 40 * progress * stage)
2319
+
2320
+ for c in range(w):
2321
+ dist = abs(c - center)
2322
+
2323
+ if r < apex_row:
2324
+ # ์ƒ๋ฅ˜ ํ•˜์ฒœ (์ข์Œ)
2325
+ if dist < 5:
2326
+ elevation[r, c] = -5.0
2327
+ elif dist < estuary_width:
2328
+ # ์—์Šค์ถ”์–ด๋ฆฌ ์˜์—ญ
2329
+ depth = 10.0 * (1 - dist / estuary_width) * (0.5 + 0.5 * progress)
2330
+ elevation[r, c] = -depth
2331
+
2332
+ # ์กฐ๊ฐ„๋Œ€ (tide flat)
2333
+ if dist >= estuary_width - 10 and dist < estuary_width and r > apex_row:
2334
+ elevation[r, c] = max(elevation[r, c], -1.0) # ์กฐ๊ฐ„๋Œ€ (์–•์Œ)
2335
+
2336
+ return elevation
2337
+
2338
+
2339
  # ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ๊ธฐ ๋งคํ•‘
2340
+
2341
  ANIMATED_LANDFORM_GENERATORS = {
2342
  'delta': create_delta_animated,
2343
  'alluvial_fan': create_alluvial_fan_animated,
 
2378
  'karren': create_karren,
2379
  'transverse_dune': create_transverse_dune,
2380
  'star_dune': create_star_dune,
2381
+ # ์ถ”๊ฐ€ ํ™•์žฅ ์ง€ํ˜•
2382
+ 'perched_river': create_perched_river,
2383
+ 'arete': create_arete,
2384
+ 'wadi': create_wadi,
2385
+ 'playa': create_playa,
2386
+ 'pedestal_rock': create_pedestal_rock,
2387
+ 'estuary': create_estuary,
2388
  }
2389
 
2390
  # ์ง€ํ˜• ์ƒ์„ฑ ํ•จ์ˆ˜ ๋งคํ•‘
 
2428
  'karren': lambda gs: create_karren(gs, 1.0),
2429
  'transverse_dune': lambda gs: create_transverse_dune(gs, 1.0),
2430
  'star_dune': lambda gs: create_star_dune(gs, 1.0),
2431
+ # ์ถ”๊ฐ€ ํ™•์žฅ ์ง€ํ˜•
2432
+ 'perched_river': lambda gs: create_perched_river(gs, 1.0),
2433
+ 'arete': lambda gs: create_arete(gs, 1.0),
2434
+ 'wadi': lambda gs: create_wadi(gs, 1.0),
2435
+ 'playa': lambda gs: create_playa(gs, 1.0),
2436
+ 'pedestal_rock': lambda gs: create_pedestal_rock(gs, 1.0),
2437
+ 'estuary': lambda gs: create_estuary(gs, 1.0),
2438
  }
2439
 
pages/1_๐Ÿ“–_Gallery.py CHANGED
@@ -8,25 +8,12 @@ import matplotlib.pyplot as plt
8
  import sys
9
  import os
10
 
11
- # ์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๊ฒฝ๋กœ์— ์ถ”๊ฐ€ (HuggingFace ํ˜ธํ™˜)
12
- root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13
- sys.path.insert(0, root_dir)
14
 
15
- try:
16
- from engine.ideal_landforms import IDEAL_LANDFORM_GENERATORS, ANIMATED_LANDFORM_GENERATORS
17
- from renderer import render_terrain_plotly
18
- IMPORT_OK = True
19
- except Exception as e:
20
- st.error(f"Import Error: {e}")
21
- IMPORT_OK = False
22
- IDEAL_LANDFORM_GENERATORS = {}
23
- ANIMATED_LANDFORM_GENERATORS = {}
24
-
25
- # ์„ธ์…˜ ์ƒํƒœ ์ดˆ๊ธฐํ™” (auto_playing ์ƒํƒœ ํ™•์ธ)
26
- if 'auto_playing' not in st.session_state:
27
- st.session_state['auto_playing'] = False
28
- if 'auto_stage' not in st.session_state:
29
- st.session_state['auto_stage'] = 0.0
30
 
31
  st.header("๐Ÿ“– ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ")
32
  st.markdown("_๊ต๊ณผ์„œ์ ์ธ ์ง€ํ˜• ํ˜•ํƒœ๋ฅผ ๊ธฐํ•˜ํ•™์  ๋ชจ๋ธ๋กœ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค._")
@@ -67,6 +54,7 @@ if category == "๐ŸŒŠ ํ•˜์ฒœ ์ง€ํ˜•":
67
  "๐Ÿ”๏ธ V์ž๊ณก (V-Valley)": "v_valley",
68
  "๐ŸŒŠ ๋ง์ƒํ•˜์ฒœ (Braided River)": "braided_river",
69
  "๐Ÿ’ง ํญํฌ (Waterfall)": "waterfall",
 
70
  }
71
  elif category == "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•":
72
  landform_options = {
@@ -74,6 +62,7 @@ elif category == "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•":
74
  "๐Ÿฆถ ์กฐ์กฑ์ƒ ์‚ผ๊ฐ์ฃผ (Bird-foot)": "bird_foot_delta",
75
  "๐ŸŒ™ ํ˜ธ์ƒ ์‚ผ๊ฐ์ฃผ (Arcuate)": "arcuate_delta",
76
  "๐Ÿ“ ์ฒจ๋‘์ƒ ์‚ผ๊ฐ์ฃผ (Cuspate)": "cuspate_delta",
 
77
  }
78
  elif category == "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•":
79
  landform_options = {
@@ -83,6 +72,7 @@ elif category == "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•":
83
  "๐ŸŒŠ ํ”ผ์˜ค๋ฅด๋“œ (Fjord)": "fjord",
84
  "๐Ÿฅš ๋“œ๋Ÿผ๋ฆฐ (Drumlin)": "drumlin",
85
  "๐Ÿชจ ๋น™ํ‡ด์„ (Moraine)": "moraine",
 
86
  }
87
  elif category == "๐ŸŒ‹ ํ™”์‚ฐ ์ง€ํ˜•":
88
  landform_options = {
@@ -105,6 +95,9 @@ elif category == "๐Ÿœ๏ธ ๊ฑด์กฐ ์ง€ํ˜•":
105
  "๐ŸŸฐ ํšก์‚ฌ๊ตฌ (Transverse Dune)": "transverse_dune",
106
  "โญ ์„ฑ์‚ฌ๊ตฌ (Star Dune)": "star_dune",
107
  "๐Ÿ—ฟ ๋ฉ”์‚ฌ/๋ทฐํŠธ (Mesa/Butte)": "mesa_butte",
 
 
 
108
  }
109
  else: # ํ•ด์•ˆ ์ง€ํ˜•
110
  landform_options = {
@@ -220,14 +213,14 @@ if landform_key in ANIMATED_LANDFORM_GENERATORS:
220
  stage_value = st.session_state.get('auto_stage', 0.0)
221
  st.slider(
222
  "ํ˜•์„ฑ ๋‹จ๊ณ„ (์ž๋™ ์žฌ์ƒ ์ค‘...)",
223
- 0.0, 1.0, stage_value, 0.05,
224
  key="gallery_stage_slider",
225
  disabled=True
226
  )
227
  else:
228
  stage_value = st.slider(
229
  "ํ˜•์„ฑ ๋‹จ๊ณ„ (0% = ์‹œ์ž‘, 100% = ์™„์„ฑ)",
230
- 0.0, 1.0, 1.0, 0.05,
231
  key="gallery_stage_slider"
232
  )
233
 
@@ -249,50 +242,37 @@ if landform_key in ANIMATED_LANDFORM_GENERATORS:
249
  stage_water[r, c] = 3.0
250
 
251
  # 3D ๋ Œ๋”๋ง
252
- st.write(f"๐Ÿ” Debug: stage_elev shape = {stage_elev.shape}, min={stage_elev.min():.1f}, max={stage_elev.max():.1f}")
 
 
 
 
 
 
 
 
 
253
 
254
- try:
255
- fig_stage = render_terrain_plotly(
256
- stage_elev,
257
- f"{selected_landform} - {int(stage_value*100)}%",
258
- add_water=True,
259
- water_depth_grid=stage_water,
260
- water_level=-999,
261
- force_camera=False, # ์นด๋ฉ”๋ผ ์ด๋™ ํ—ˆ์šฉ
262
- landform_type=landform_type
263
- )
264
- st.write("โœ… Debug: render_terrain_plotly ์„ฑ๊ณต!")
265
- st.plotly_chart(fig_stage, use_container_width=True, key="stage_view")
266
- except Exception as e:
267
- st.error(f"โŒ Render Error: {e}")
268
 
269
- # ํ™˜๊ฒฝ ๊ฐ์ง€: HuggingFace์—์„œ๋Š” SPACE_ID ํ™˜๊ฒฝ๋ณ€์ˆ˜๏ฟฝ๏ฟฝ ์žˆ์Œ
270
- is_huggingface = os.environ.get('SPACE_ID') is not None
 
 
 
 
 
 
 
 
 
271
 
272
- if not is_huggingface:
273
- # ๋กœ์ปฌ ํ™˜๊ฒฝ: ์ž๋™ ์žฌ์ƒ ๊ธฐ๋Šฅ ํ™œ์„ฑํ™”
274
- col_play, col_step = st.columns(2)
275
- with col_play:
276
- if st.button("โ–ถ๏ธ ์ž๋™ ์žฌ์ƒ ์‹œ์ž‘", key="auto_play"):
277
- st.session_state['auto_playing'] = True
278
- st.session_state['auto_stage'] = 0.0
279
- with col_step:
280
- if st.button("โน๏ธ ์ •์ง€", key="stop_play"):
281
- st.session_state['auto_playing'] = False
282
-
283
- if st.session_state.get('auto_playing', False):
284
- current_stage = st.session_state.get('auto_stage', 0.0)
285
- if current_stage < 1.0:
286
- st.session_state['auto_stage'] = current_stage + 0.1
287
- import time
288
- time.sleep(0.5)
289
- st.rerun()
290
- else:
291
- st.session_state['auto_playing'] = False
292
- st.success("โœ… ์™„๋ฃŒ!")
293
-
294
- st.caption("๐Ÿ’ก **Tip:** ์นด๋ฉ”๋ผ ๊ฐ๋„๋ฅผ ๋จผ์ € ์กฐ์ •ํ•œ ํ›„ ์ž๋™ ์žฌ์ƒํ•˜๋ฉด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.")
295
- else:
296
- # HuggingFace: ์Šฌ๋ผ์ด๋”๋งŒ ์‚ฌ์šฉ
297
- st.caption("๐Ÿ’ก **Tip:** ์Šฌ๋ผ์ด๋”๋ฅผ ๋“œ๋ž˜๊ทธํ•ด์„œ ํ˜•์„ฑ ๊ณผ์ •์„ ํ™•์ธํ•˜์„ธ์š”. ๋งˆ์šฐ์Šค๋กœ 3D ํšŒ์ „ ๊ฐ€๋Šฅ!")
298
-
 
8
  import sys
9
  import os
10
 
11
+ # ์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๊ฒฝ๋กœ์— ์ถ”๊ฐ€
12
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
14
 
15
+ from engine.ideal_landforms import IDEAL_LANDFORM_GENERATORS, ANIMATED_LANDFORM_GENERATORS
16
+ from app.main import render_terrain_plotly
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  st.header("๐Ÿ“– ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ")
19
  st.markdown("_๊ต๊ณผ์„œ์ ์ธ ์ง€ํ˜• ํ˜•ํƒœ๋ฅผ ๊ธฐํ•˜ํ•™์  ๋ชจ๋ธ๋กœ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค._")
 
54
  "๐Ÿ”๏ธ V์ž๊ณก (V-Valley)": "v_valley",
55
  "๐ŸŒŠ ๋ง์ƒํ•˜์ฒœ (Braided River)": "braided_river",
56
  "๐Ÿ’ง ํญํฌ (Waterfall)": "waterfall",
57
+ "๐Ÿšง ์ฒœ์ •์ฒœ (Perched River)": "perched_river",
58
  }
59
  elif category == "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•":
60
  landform_options = {
 
62
  "๐Ÿฆถ ์กฐ์กฑ์ƒ ์‚ผ๊ฐ์ฃผ (Bird-foot)": "bird_foot_delta",
63
  "๐ŸŒ™ ํ˜ธ์ƒ ์‚ผ๊ฐ์ฃผ (Arcuate)": "arcuate_delta",
64
  "๐Ÿ“ ์ฒจ๋‘์ƒ ์‚ผ๊ฐ์ฃผ (Cuspate)": "cuspate_delta",
65
+ "๐ŸŒŠ ์—์Šค์ถ”์–ด๋ฆฌ (Estuary)": "estuary",
66
  }
67
  elif category == "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•":
68
  landform_options = {
 
72
  "๐ŸŒŠ ํ”ผ์˜ค๋ฅด๋“œ (Fjord)": "fjord",
73
  "๐Ÿฅš ๋“œ๋Ÿผ๋ฆฐ (Drumlin)": "drumlin",
74
  "๐Ÿชจ ๋น™ํ‡ด์„ (Moraine)": "moraine",
75
+ "๐Ÿ—ก๏ธ ์•„๋ ˆํŠธ (Arรชte)": "arete",
76
  }
77
  elif category == "๐ŸŒ‹ ํ™”์‚ฐ ์ง€ํ˜•":
78
  landform_options = {
 
95
  "๐ŸŸฐ ํšก์‚ฌ๊ตฌ (Transverse Dune)": "transverse_dune",
96
  "โญ ์„ฑ์‚ฌ๊ตฌ (Star Dune)": "star_dune",
97
  "๐Ÿ—ฟ ๋ฉ”์‚ฌ/๋ทฐํŠธ (Mesa/Butte)": "mesa_butte",
98
+ "๐Ÿœ๏ธ ์™€๋”” (Wadi)": "wadi",
99
+ "๐Ÿชถ ํ”Œ๋ผ์•ผ (Playa)": "playa",
100
+ "๐Ÿ„ ๋ฒ„์„ฏ๋ฐ”์œ„ (Pedestal Rock)": "pedestal_rock",
101
  }
102
  else: # ํ•ด์•ˆ ์ง€ํ˜•
103
  landform_options = {
 
213
  stage_value = st.session_state.get('auto_stage', 0.0)
214
  st.slider(
215
  "ํ˜•์„ฑ ๋‹จ๊ณ„ (์ž๋™ ์žฌ์ƒ ์ค‘...)",
216
+ 0.0, 1.0, stage_value, 0.02,
217
  key="gallery_stage_slider",
218
  disabled=True
219
  )
220
  else:
221
  stage_value = st.slider(
222
  "ํ˜•์„ฑ ๋‹จ๊ณ„ (0% = ์‹œ์ž‘, 100% = ์™„์„ฑ)",
223
+ 0.0, 1.0, 1.0, 0.02,
224
  key="gallery_stage_slider"
225
  )
226
 
 
242
  stage_water[r, c] = 3.0
243
 
244
  # 3D ๋ Œ๋”๋ง
245
+ fig_stage = render_terrain_plotly(
246
+ stage_elev,
247
+ f"{selected_landform} - {int(stage_value*100)}%",
248
+ add_water=True,
249
+ water_depth_grid=stage_water,
250
+ water_level=-999,
251
+ force_camera=False, # ์นด๋ฉ”๋ผ ์ด๋™ ํ—ˆ์šฉ
252
+ landform_type=landform_type
253
+ )
254
+ st.plotly_chart(fig_stage, use_container_width=True, key="stage_view")
255
 
256
+ # ์ž๋™ ์žฌ์ƒ (์„ธ์…˜ ์ƒํƒœ ํ™œ์šฉ)
257
+ col_play, col_step = st.columns(2)
258
+ with col_play:
259
+ if st.button("โ–ถ๏ธ ์ž๋™ ์žฌ์ƒ ์‹œ์ž‘", key="auto_play"):
260
+ st.session_state['auto_playing'] = True
261
+ st.session_state['auto_stage'] = 0.0
262
+ with col_step:
263
+ if st.button("โน๏ธ ์ •์ง€", key="stop_play"):
264
+ st.session_state['auto_playing'] = False
 
 
 
 
 
265
 
266
+ # ์ž๋™ ์žฌ์ƒ ์ค‘์ด๋ฉด stage ์ž๋™ ์ฆ๊ฐ€
267
+ if st.session_state.get('auto_playing', False):
268
+ current_stage = st.session_state.get('auto_stage', 0.0)
269
+ if current_stage < 1.0:
270
+ st.session_state['auto_stage'] = current_stage + 0.04
271
+ import time
272
+ time.sleep(0.15)
273
+ st.rerun()
274
+ else:
275
+ st.session_state['auto_playing'] = False
276
+ st.success("โœ… ์™„๋ฃŒ!")
277
 
278
+ st.caption("๐Ÿ’ก **Tip:** ์นด๋ฉ”๋ผ ๊ฐ๋„๋ฅผ ๋จผ์ € ์กฐ์ •ํ•œ ํ›„ ์ž๋™ ์žฌ์ƒํ•˜๋ฉด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/2_๐Ÿ—บ๏ธ_Overview.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๐Ÿ—บ๏ธ ์นดํ…Œ๊ณ ๋ฆฌ ์ „์ฒด ๋ทฐ
3
+ ๊ฐ ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋ชจ๋“  ์ง€ํ˜•์„ ํ•œ๋ˆˆ์— ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค.
4
+ """
5
+ import streamlit as st
6
+ import numpy as np
7
+ import matplotlib.pyplot as plt
8
+ import sys
9
+ import os
10
+
11
+ # ์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๊ฒฝ๋กœ์— ์ถ”๊ฐ€
12
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
+
14
+ from engine.ideal_landforms import IDEAL_LANDFORM_GENERATORS
15
+
16
+ st.set_page_config(layout="wide")
17
+ st.header("๐Ÿ—บ๏ธ ์นดํ…Œ๊ณ ๋ฆฌ ์ „์ฒด ๋ทฐ")
18
+ st.markdown("_๊ฐ ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋ชจ๋“  ์ง€ํ˜•์„ ํ•œ๋ˆˆ์— ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค._")
19
+
20
+ # ์นดํ…Œ๊ณ ๋ฆฌ ์ •์˜
21
+ CATEGORIES = {
22
+ "๐ŸŒŠ ํ•˜์ฒœ ์ง€ํ˜•": {
23
+ "alluvial_fan": "์„ ์ƒ์ง€",
24
+ "free_meander": "์ž์œ ๊ณก๋ฅ˜",
25
+ "incised_meander": "๊ฐ์ž…๊ณก๋ฅ˜",
26
+ "v_valley": "V์ž๊ณก",
27
+ "braided_river": "๋ง์ƒํ•˜์ฒœ",
28
+ "waterfall": "ํญํฌ",
29
+ "perched_river": "์ฒœ์ •์ฒœ",
30
+ },
31
+ "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•": {
32
+ "delta": "์ผ๋ฐ˜ ์‚ผ๊ฐ์ฃผ",
33
+ "bird_foot_delta": "์กฐ์กฑ์ƒ",
34
+ "arcuate_delta": "ํ˜ธ์ƒ",
35
+ "cuspate_delta": "์ฒจ๋‘์ƒ",
36
+ "estuary": "์—์Šค์ถ”์–ด๋ฆฌ",
37
+ },
38
+ "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•": {
39
+ "u_valley": "U์ž๊ณก",
40
+ "cirque": "๊ถŒ๊ณก",
41
+ "horn": "ํ˜ธ๋ฅธ",
42
+ "fjord": "ํ”ผ์˜ค๋ฅด๋“œ",
43
+ "drumlin": "๋“œ๋Ÿผ๋ฆฐ",
44
+ "moraine": "๋น™ํ‡ด์„",
45
+ "arete": "์•„๋ ˆํŠธ",
46
+ },
47
+ "๐ŸŒ‹ ํ™”์‚ฐ ์ง€ํ˜•": {
48
+ "shield_volcano": "์ˆœ์ƒํ™”์‚ฐ",
49
+ "stratovolcano": "์„ฑ์ธตํ™”์‚ฐ",
50
+ "caldera": "์นผ๋ฐ๋ผ",
51
+ "crater_lake": "ํ™”๊ตฌํ˜ธ",
52
+ "lava_plateau": "์šฉ์•”๋Œ€์ง€",
53
+ },
54
+ "๐Ÿฆ‡ ์นด๋ฅด์ŠคํŠธ ์ง€ํ˜•": {
55
+ "karst_doline": "๋Œ๋ฆฌ๋„ค",
56
+ "uvala": "์šฐ๋ฐœ๋ผ",
57
+ "tower_karst": "ํƒ‘์นด๋ฅด์ŠคํŠธ",
58
+ "karren": "์นด๋ Œ",
59
+ },
60
+ "๐Ÿœ๏ธ ๊ฑด์กฐ ์ง€ํ˜•": {
61
+ "barchan": "๋ฐ”๋ฅดํ•œ",
62
+ "transverse_dune": "ํšก์‚ฌ๊ตฌ",
63
+ "star_dune": "์„ฑ์‚ฌ๊ตฌ",
64
+ "mesa_butte": "๋ฉ”์‚ฌ/๋ทฐํŠธ",
65
+ "wadi": "์™€๋””",
66
+ "playa": "ํ”Œ๋ผ์•ผ",
67
+ "pedestal_rock": "๋ฒ„์„ฏ๋ฐ”์œ„",
68
+ },
69
+ "๐Ÿ–๏ธ ํ•ด์•ˆ ์ง€ํ˜•": {
70
+ "coastal_cliff": "ํ•ด์•ˆ์ ˆ๋ฒฝ",
71
+ "spit_lagoon": "์‚ฌ์ทจ+์„ํ˜ธ",
72
+ "tombolo": "์œก๊ณ„์‚ฌ์ฃผ",
73
+ "ria_coast": "๋ฆฌ์•„์Šคํ•ด์•ˆ",
74
+ "sea_arch": "ํ•ด์‹์•„์น˜",
75
+ "coastal_dune": "ํ•ด์•ˆ์‚ฌ๊ตฌ",
76
+ },
77
+ }
78
+
79
+ # ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ
80
+ category = st.sidebar.selectbox("์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ", list(CATEGORIES.keys()))
81
+ landforms = CATEGORIES[category]
82
+
83
+ # ๊ทธ๋ฆฌ๋“œ ํฌ๊ธฐ
84
+ grid_size = st.sidebar.slider("ํ•ด์ƒ๋„", 50, 120, 80)
85
+
86
+ st.subheader(f"{category} - {len(landforms)}์ข…")
87
+
88
+ # ์ปฌ๋Ÿผ ์ˆ˜ ๊ณ„์‚ฐ
89
+ num_landforms = len(landforms)
90
+ cols_per_row = min(4, num_landforms)
91
+ rows = (num_landforms + cols_per_row - 1) // cols_per_row
92
+
93
+ # ์ง€ํ˜• ์ƒ์„ฑ ๋ฐ ํ‘œ์‹œ
94
+ landform_items = list(landforms.items())
95
+
96
+ for row_idx in range(rows):
97
+ cols = st.columns(cols_per_row)
98
+ for col_idx, col in enumerate(cols):
99
+ item_idx = row_idx * cols_per_row + col_idx
100
+ if item_idx < num_landforms:
101
+ key, name = landform_items[item_idx]
102
+
103
+ with col:
104
+ st.markdown(f"**{name}**")
105
+
106
+ if key in IDEAL_LANDFORM_GENERATORS:
107
+ try:
108
+ elevation = IDEAL_LANDFORM_GENERATORS[key](grid_size)
109
+
110
+ # 2D ํƒ‘๋ทฐ ์ด๋ฏธ์ง€
111
+ fig, ax = plt.subplots(figsize=(4, 4))
112
+ im = ax.imshow(elevation, cmap='terrain', origin='lower')
113
+ ax.set_title(name, fontsize=10)
114
+ ax.axis('off')
115
+ plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
116
+ st.pyplot(fig)
117
+ plt.close(fig)
118
+ except Exception as e:
119
+ st.error(f"์˜ค๋ฅ˜: {e}")
120
+ else:
121
+ st.warning(f"{key} ๋ฏธ๊ตฌํ˜„")
122
+
123
+ st.markdown("---")
124
+ st.caption("๐Ÿ’ก ๊ฐ ์ง€ํ˜•์„ ํด๋ฆญํ•˜๋ฉด Gallery ํŽ˜์ด์ง€์—์„œ 3D๋กœ ์ƒ์„ธํ•˜๊ฒŒ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")