import gradio as gr import matplotlib.pyplot as plt import numpy as np import random # --- Configure Matplotlib --- plt.style.use('ggplot') plt.rcParams['figure.figsize'] = [10, 10] # --- Core Data Structure: Circle Class --- class Circle: """Represents a circle in the drawing and its state""" def __init__(self, x, y, radius, color): self.x = x self.y = y self.radius = radius self.color = color def __repr__(self): return f"Circle(Center:({self.x:.2f}, {self.y:.2f}), R:{self.radius}, Color:{self.color})" # --- Shape Grammar Core Function --- def run_shape_grammar(N_iterations): """ Runs the shape grammar iterative process N times and yields the state after each iteration. """ # 1. Initial Condition (Start) circles = [Circle(0, 0, 2, 'blue')] lines = [] # Stores all line segments for plotting current_iteration = 0 print(f"--- Starting Shape Grammar: Target Iterations N = {N_iterations} ---") # Yield initial state before any iterations yield circles, lines while current_iteration < N_iterations: # Filter out non-red circles active_circles = [c for c in circles if c.color != 'red'] # 2. Stop Condition Check if not active_circles: print(f"\n✅ Stop: All circles have turned red. Total iterations: {current_iteration}") break # 3. Randomly select an original circle (Pick a random circle) C_original = random.choice(active_circles) # 4. Determine Direction and Line Segment (Direction and Line) # Random angle (multiple of 45°) angle_deg = random.choice([0, 45, 90, 135, 180, 225, 270, 315]) angle_rad = np.deg2rad(angle_deg) # Random line length L_length = random.choice([6, 8, 10]) # Randomly choose starting point (center or tangent point) start_from_center = random.choice([True, False]) if start_from_center: # Option A: Start from the center P_start_x, P_start_y = C_original.x, C_original.y else: # Option B: Start from a tangent point on the circumference # The tangent point is the center translated by R in the opposite direction of 'angle_rad' P_start_x = C_original.x + C_original.radius * np.cos(angle_rad + np.pi) P_start_y = C_original.y + C_original.radius * np.sin(angle_rad + np.pi) # Calculate the end point of the line segment P_end_x = P_start_x + L_length * np.cos(angle_rad) P_end_y = P_start_y + L_length * np.sin(angle_rad) # Store the line segment lines.append(((P_start_x, P_start_y), (P_end_x, P_end_y))) # 5. Create New Circle (New Circle) # Random radius and color R_new = random.choice([1, 2, 3, 4]) # Color selection: Kept uniformly random for faster generation colors = ['blue'] * 9 + ['green'] * 9 + ['red'] * 2 # 9:9:2 比例 Color_new = random.choice(colors) # Random placement position placement_option = random.choice(['tangential_left', 'tangential_right', 'centered']) if placement_option == 'centered': # Option C: Centered at the end point of the line segment C_new_x, C_new_y = P_end_x, P_end_y else: # Option A/B: Tangential to the line segment # Normal direction: Angle plus/minus 90 degrees # Tangential to the line (L) left or right normal_angle = angle_rad + (np.pi/2 if placement_option == 'tangential_left' else -np.pi/2) # The center of the new circle is located at the end point P_end, moved by R_new distance along the normal direction C_new_x = P_end_x + R_new * np.cos(normal_angle) # Corrected the calculation for C_new_y - it should use P_end_y C_new_y = P_end_y + R_new * np.sin(normal_angle) C_new = Circle(C_new_x, C_new_y, R_new, Color_new) circles.append(C_new) # 6. Color Updates (Color Updates) - Modifying Green's mortality rate if C_original.color == 'blue': C_original.color = 'green' elif C_original.color == 'green': # 只有 50% 的概率从 Green 变为 Red,另 50% 保持 Green if random.random() < 0.5: C_original.color = 'red' # 否则 C_original.color 保持 'green' current_iteration += 1 print(f"Iteration {current_iteration}/{N_iterations}: Original circle turned {C_original.color}, New circle {C_new.color}, Total circles: {len(circles)}") # Yield the current state yield circles, lines if current_iteration == N_iterations: print(f"\n✅ Stop: Maximum number of iterations N = {N_iterations} reached.") # --- Plotting Function --- def plot_grammar_result(circles, lines): """ Plots the generated shape using Matplotlib and returns the figure. """ fig, ax = plt.subplots() # Plot all circles for c in circles: # Plot using matplotlib.patches.Circle circle_patch = plt.Circle((c.x, c.y), c.radius, color=c.color, alpha=0.6, # Transparency fill=True, linewidth=1, edgecolor='black') ax.add_patch(circle_patch) # Plot all line segments for (start, end) in lines: ax.plot([start[0], end[0]], [start[1], end[1]], color='gray', linestyle='-', linewidth=0.5, zorder=-1) # Place below circles # Set axes and limits if circles: all_x = [c.x for c in circles] all_y = [c.y for c in circles] # Automatically calculate boundaries to ensure all circles are visible x_min, x_max = min(all_x), max(all_x) y_min, y_max = min(all_y), max(all_y) padding = 10 # Additional padding # Add a check to prevent errors if min/max are the same (e.g., only one circle at 0,0) if x_min == x_max: x_min -= padding x_max += padding if y_min == y_max: y_min -= padding y_max += padding ax.set_xlim(x_min - padding, x_max + padding) ax.set_ylim(y_min - padding, y_max + padding) ax.set_aspect('equal', adjustable='box') # Maintain equal aspect ratio ax.set_title(f"Shape Grammar Generation Result (Total {len(circles)} circles)") ax.set_xlabel("X Coordinate") ax.set_ylabel("Y Coordinate") # Don't call plt.show() here, return the figure object return fig # Store the generated plots generated_plots = [] def run_shape_grammar_and_generate_plots(num_iterations): """ Runs the shape grammar and generates plots for each iteration. Returns a list of Matplotlib figure objects. """ global generated_plots generated_plots = [] # Clear previous plots print(f"run_shape_grammar_and_generate_plots called with {num_iterations} iterations") # Ensure the input is a positive integer if num_iterations is None or num_iterations <= 0: num_iterations = 50 # Default to 50 if invalid # Run the generation process and iterate through yielded results for i, (circles, lines) in enumerate(run_shape_grammar(int(num_iterations))): # Plot the result for the current step and get the figure object fig = plot_grammar_result(circles, lines) generated_plots.append(fig) # Close the figure to free up memory plt.close(fig) print(f"Generated {len(generated_plots)} plots.") return generated_plots def display_iteration(iteration_index): """ Displays the plot for a specific iteration based on the slider value. """ global generated_plots print(f"display_iteration called with index {iteration_index}") if generated_plots and 0 <= iteration_index < len(generated_plots): print(f"Displaying plot for iteration {iteration_index}") return generated_plots[int(iteration_index)] # Ensure index is integer else: print("No plot available or index out of range.") # Return an empty plot or a placeholder if no plots are available or index is out of range fig, ax = plt.subplots() ax.text(0.5, 0.5, "No plot available", horizontalalignment='center', verticalalignment='center') ax.set_title("Error") return fig # Create the Gradio interface with gr.Blocks() as demo: gr.Markdown("## Shape Grammar Generator with Iteration Slider") gr.Markdown("Generate abstract shapes using a simple shape grammar and explore each step of the process.") with gr.Row(): num_iterations_input = gr.Number(label="Number of Iterations", value=50, precision=0) generate_button = gr.Button("Generate") # Output area for the plots plot_output = gr.Plot() # Slider to control the displayed iteration iteration_slider = gr.Slider(minimum=0, maximum=0, step=1, label="Iteration") # Link the generate button to the function that runs the grammar and generates plots # Update the slider's maximum value after generation generate_button.click( fn=run_shape_grammar_and_generate_plots, inputs=num_iterations_input, outputs=None # We don't directly output here, we just generate and store plots ).then( fn=lambda: gr.update(maximum=len(generated_plots)-1, value=0), # Update slider max and reset to 0 inputs=None, # No direct input needed, accessing global variable outputs=iteration_slider ).then( fn=lambda: generated_plots[0] if generated_plots else None, # Display the first plot initially inputs=None, # No direct input needed, accessing global variable outputs=plot_output ) # Link the slider to the function that displays the selected iteration's plot iteration_slider.change( fn=display_iteration, inputs=iteration_slider, outputs=plot_output ) # Launch the interface for Hugging Face Spaces if __name__ == "__main__": # Modified to include share=True for Colab/hosted environments demo.launch(share=True)