Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/easymanim/gui/properties_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ def _create_property_widget(self, key: str, value: Any, row: int):
display_key_name = 'Stroke Width'
elif key == 'stroke_opacity':
display_key_name = 'Stroke Opacity'
elif key == 'pos_z':
display_key_name = 'Z Position (Depth)'

label = ttk.Label(self, text=f"{display_key_name}:")
label.grid(row=row, column=0, sticky=tk.W, padx=5, pady=2)
Expand All @@ -140,6 +142,13 @@ def _create_property_widget(self, key: str, value: Any, row: int):
# We should only create entry if key is actually in props (checked by caller display_properties)
widget = self._create_entry(key, value)
widget.grid(row=row, column=1, sticky=tk.EW, padx=5, pady=2)

# Add helper text for Z position
if key == 'pos_z':
helper_text = ttk.Label(self, text="Higher value = closer to viewer",
font=("", 8), foreground="gray")
helper_text.grid(row=row, column=2, sticky=tk.W, padx=5, pady=2)
self.widgets[f"{key}_helper"] = helper_text
else:
# Fallback for any other properties, display as non-editable text for now
widget = ttk.Label(self, text=str(value))
Expand Down
115 changes: 103 additions & 12 deletions src/easymanim/logic/scene_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,20 @@ def generate_script(self, script_type: Literal['preview', 'render']) -> tuple[st
"import numpy as np # Often needed for positioning"
]

# Generate code for each object
# Sort objects by z-position for creation order
objects_by_type = {
'Text': [],
'Other': []
}

# Separate text objects from other objects
for obj in self.objects:
if obj['type'] == 'Text':
objects_by_type['Text'].append(obj)
else:
objects_by_type['Other'].append(obj)

# All objects need to be created in the regular way
for obj in self.objects:
obj_id = obj['id']
obj_type = obj['type']
Expand All @@ -224,25 +237,33 @@ def generate_script(self, script_type: Literal['preview', 'render']) -> tuple[st
var_name = f"{obj_type.lower()}_{obj_id[-6:]}"

# --- Object Instantiation (common for all) ---
prop_args_for_script = [] # Renamed to avoid conflict if you had prop_args locally
prop_args_for_script = []
pos_x = properties.get('pos_x', 0.0)
pos_y = properties.get('pos_y', 0.0)
pos_z = properties.get('pos_z', 0.0)
move_to_str = f".move_to(np.array([{pos_x}, {pos_y}, {pos_z}]))"

# Use move_to for x,y positioning only
move_to_str = f".move_to(np.array([{pos_x}, {pos_y}, 0]))"

# Convert z position to z_index in a safe way - only for non-text objects
# Higher z values = higher z_index = appears on top
z_index_value = max(0, int(pos_z * 10))

# For non-text objects, handle z_index as a parameter
if obj_type != 'Text' and z_index_value > 0:
prop_args_for_script.append(f"z_index={z_index_value}")

instantiation_base = f"{var_name} = {obj_type}("
if obj_type == 'Text':
text_content_formatted = self._format_manim_prop(properties.get('text_content', ''))
instantiation_base += text_content_formatted
font_size_val = properties.get('font_size')
if font_size_val is not None:
prop_args_for_script.append(f"font_size={self._format_manim_prop(font_size_val)}")
# Manim's Text uses 'color' for fill, but also accepts 'fill_color'. Using 'color' for primary text color.
prop_args_for_script.append(f"color={self._format_manim_prop(properties.get('fill_color', '#FFFFFF'))}")
prop_args_for_script.append(f"fill_opacity={self._format_manim_prop(properties.get('opacity', 1.0))}")
prop_args_for_script.append(f"stroke_color={self._format_manim_prop(properties.get('stroke_color', '#000000'))}")
prop_args_for_script.append(f"stroke_opacity={self._format_manim_prop(properties.get('stroke_opacity', 1.0))}")
# stroke_width is deliberately omitted for Text for now as it behaves differently than shapes.
elif obj_type == 'Circle':
prop_args_for_script.append(f"radius={self._format_manim_prop(properties.get('radius', 1.0))}")
prop_args_for_script.append(f"fill_color={self._format_manim_prop(properties.get('fill_color', '#FFFFFF'))}")
Expand All @@ -262,6 +283,8 @@ def generate_script(self, script_type: Literal['preview', 'render']) -> tuple[st
if obj_type == 'Text' and instantiation_base.endswith(text_content_formatted):
instantiation_base += ", "
instantiation_base += ", ".join(prop_args_for_script)

# Base instantiation
final_instantiation = f"{instantiation_base}){move_to_str}"
object_creation_lines.append(final_instantiation)

Expand All @@ -280,28 +303,96 @@ def generate_script(self, script_type: Literal['preview', 'render']) -> tuple[st
elif animation == 'Write' and obj_type == 'Text':
animation_command_str = f"Write({var_name})"
is_intro_anim = True
# Add other known intro animations here and set is_intro_anim = True


if animation_command_str:
play_lines.append(f"self.play({animation_command_str})")
if is_intro_anim:
added_by_intro_animation = True
elif animation == 'Write' and obj_type != 'Text': # Non-applicable animation
elif animation == 'Write' and obj_type != 'Text':
add_lines.append(f"self.add({var_name}) # 'Write' animation not applicable to {obj_type}")
else: # Unknown animation type, or known non-intro animation
else:
add_lines.append(f"self.add({var_name}) # Default add for animation: {animation}")

# Add object if it wasn't added by an intro animation, or if it's a preview
if not added_by_intro_animation:
add_lines.append(f"self.add({var_name})")
# Skip adding objects here - we'll handle them based on z-position
if not added_by_intro_animation and animation != 'None':
add_lines.append(f"self.add({var_name}) # For animation: {animation}")

# Collect objects for z-ordering
shape_objects = [] # Non-text objects
text_objects = [] # Text objects only

for obj in self.objects:
obj_id = obj['id']
obj_type = obj['type']
animation = obj['properties'].get('animation', 'None')
var_name = f"{obj_type.lower()}_{obj_id[-6:]}"
pos_z = obj['properties'].get('pos_z', 0)

# Skip objects already handled by animations
if script_type == 'render':
if animation == 'FadeIn' or animation == 'GrowFromCenter':
continue
if animation == 'Write' and obj_type == 'Text':
continue

# Separate text and non-text objects
if obj_type == 'Text':
text_objects.append((var_name, pos_z))
else:
shape_objects.append((var_name, pos_z, obj_type))

# First add all non-text objects in z-order
if shape_objects:
# Sort by z-position (low to high)
shape_objects.sort(key=lambda x: x[1])

add_lines.append("\n# Non-text objects added in z-order")
for var_name, z_pos, obj_type in shape_objects:
add_lines.append(f"self.add({var_name}) # {obj_type} with z={z_pos}")

# Then add all text objects in z-order
if text_objects:
# Sort by z-position (low to high)
text_objects.sort(key=lambda x: x[1])

add_lines.append("\n# Text objects added last to ensure proper z-ordering")
for var_name, z_pos in text_objects:
add_lines.append(f"self.add({var_name}) # Text with z={z_pos}")

# Then bring ALL text objects to front explicitly
if text_objects:
add_lines.append("\n# Ensure ALL text objects appear on top")
for var_name, z_pos in text_objects:
add_lines.append(f"self.bring_to_front({var_name}) # Force text on top")

# Extra handling for text with positive z-values
texts_with_positive_z = [(name, z) for name, z in text_objects if z > 0]
if texts_with_positive_z:
# Sort by descending z-position (highest z first) for proper layering
texts_with_positive_z.sort(key=lambda x: -x[1]) # Note the negative sign for descending sort

add_lines.append("\n# Extra handling for text with positive z-values")
for var_name, z_pos in texts_with_positive_z:
# Apply multiple bring_to_front calls for higher z-values
# This is a hack, but it helps ensure text with higher z appears on top
repeats = max(1, int(z_pos))
for _ in range(repeats):
add_lines.append(f"self.bring_to_front({var_name}) # Extra priority for z={z_pos}")

# Determine Scene Name and Class Definition
scene_name = "PreviewScene" if script_type == 'preview' else "EasyManimScene"
class_def = f"class {scene_name}(Scene):"
construct_def = " def construct(self):"

# Assemble the final script
construct_body_lines = []

# If z-position is used, add a comment explaining z-index
if any(abs(obj['properties'].get('pos_z', 0.0)) > 0.001 for obj in self.objects):
construct_body_lines.append("# Z-positioning is being used via z_index")
construct_body_lines.append("# Higher z_index values appear on top (closer to viewer)")

# Add all object creation code
construct_body_lines.extend(object_creation_lines)

# For render scripts, play lines (animations) come first, then residual add lines.
Expand Down
5 changes: 3 additions & 2 deletions src/easymanim/ui/ui_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,12 @@ def handle_refresh_preview_request(self):
print(" render_async called on ManimInterface")

except Exception as e:
import traceback
print(f"[UIManager Error] Failed during refresh preview setup: {e}")
print(f"Traceback: {traceback.format_exc()}")
statusbar_panel = self.panels.get("statusbar")
if statusbar_panel:
statusbar_panel.set_status("Error generating preview script")
statusbar_panel.set_status(f"Error generating preview: {str(e)}")
# Potentially show error to user or update preview panel state
preview_panel = self.panels.get("preview")
if preview_panel:
Expand Down Expand Up @@ -313,4 +315,3 @@ def _render_callback(self, success: bool, result: Union[str, str]):
# Reset UI state (e.g., re-enable buttons) regardless of success/failure
if preview_panel:
preview_panel.show_idle_state()