Source code for editor.ui.main_window

from PyQt6.QtWidgets import QMainWindow, QToolBar, QFileDialog, QMessageBox, QTabWidget, QApplication, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QDockWidget, QScrollArea, QFrame, QWidget, QSizePolicy
from PyQt6.QtGui import QAction, QKeySequence, QIcon, QPixmap, QPainter, QColor, QFont
from PyQt6.QtCore import Qt, QRect, QTimer
import qtawesome as qta
from editor.ui.engine_settings import theme_icon_color
import os
import sys
import json

from core.scene import Scene
from core.serializer import SceneSerializer
from editor.ui.viewport import PygameViewport
from editor.ui.hierarchy import HierarchyDock
from editor.ui.inspector import InspectorDock
from editor.ui.asset_manager import AssetManagerDock
from editor.ui.code_editor import ScriptEditorWidget
from editor.ui.animation_editor import AnimationEditor
from editor.ui.project_settings import ProjectSettingsDialog
from editor.ui.export_dialog import ExportDialog
from editor.ui.engine_settings import EngineSettings
from editor.ui.project_hub import ProjectHub
from editor.ui.console_dock import ConsoleDock
from editor.ui.chat_dock import ChatDock
from core.ai.chat_manager import ChatManager
from core.ai.providers.openai_provider import OpenAIProvider
from core.ai.providers.local_provider import LocalLLMProvider
from core.ai.providers.openrouter_provider import OpenRouterProvider
from core.ai.providers.google_provider import GoogleProvider
from core.ai.providers.anthropic_provider import AnthropicProvider
from core.ai.providers.nvidia_provider import NvidiaProvider
from core.resources import ResourceManager
from core.logger import get_logger
from core.runtime_launch import LaunchProfile, RuntimeCommandBuilder
from plugins.plugin_manager import PluginManager
from editor.undo_manager import UndoManager
from editor.ui.tilemap_editor import TilemapEditorDock
from core.components import Transform, TilemapComponent



[docs] class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("AxisPy Engine - Editor") self.resize(1280, 720) self.project_path = None self.current_scene_path = None self._last_launch_handle = None self.plugin_manager = PluginManager() self.plugin_manager.load_all_plugins() self._logger = get_logger("editor") # Initialize core scene self.scene = Scene() # self.scene.setup_default() # Initialize UndoManager self.undo_manager = UndoManager(self) # Central Widget - Tab Widget self.central_tabs = QTabWidget() self.setCentralWidget(self.central_tabs) # Main viewport (Scene Tab) self.project_config = {} self.viewport = PygameViewport(self.scene, self, self.project_config) self.central_tabs.addTab(self.viewport, "Scene") # Code Editor (Script Tab) self.script_editor = ScriptEditorWidget(self) self.central_tabs.addTab(self.script_editor, "Scripts Editor") # Animation Editor self.animation_editor = AnimationEditor(self) self.central_tabs.addTab(self.animation_editor, "Animation Editor") # Docks self.hierarchy_dock = HierarchyDock(self.scene, self) self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.hierarchy_dock) self.inspector_dock = InspectorDock(self) self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.inspector_dock) # Set default size for Inspector self.resizeDocks([self.inspector_dock], [360], Qt.Orientation.Horizontal) self.asset_manager_dock = AssetManagerDock(self) self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.asset_manager_dock) # Tilemap editor dock self.tilemap_editor_dock = TilemapEditorDock(self, self) self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.tilemap_editor_dock) self.tilemap_editor_dock.hide() self.console_dock = ConsoleDock(self) self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.console_dock) self.splitDockWidget(self.asset_manager_dock, self.console_dock, Qt.Orientation.Vertical) # AI Chat dock - tabified with inspector self.chat_manager = ChatManager() self.chat_dock = ChatDock(self.chat_manager, self) self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.chat_dock) self.tabifyDockWidget(self.inspector_dock, self.chat_dock) self.inspector_dock.raise_() # Make inspector the active tab # Connect hierarchy selection to inspector self.hierarchy_dock.tree.itemSelectionChanged.connect(self.on_entity_selected) # Connect viewport modification to inspector self.viewport.entity_selected.connect(self.on_viewport_entity_selected) self.viewport.entity_modified.connect(self.inspector_dock.refresh_values) self.viewport.entity_deleted.connect(self.on_entity_deleted) # Tilemap editor <-> viewport integration (selection/edit mode handled in viewport todo) self.tilemap_editor_dock.edit_mode_changed.connect(self.viewport.set_tilemap_edit_mode) self.tilemap_editor_dock.tool_changed.connect(self.viewport.set_tilemap_tool) self.tilemap_editor_dock.active_layer_index_changed.connect(self.viewport.set_tilemap_active_layer) self.tilemap_editor_dock.selected_tile_changed.connect(self.viewport.set_tilemap_selected_tile) # Create Menus self.create_menus() # Toolbar for Run button self.toolbar = QToolBar("Main Toolbar") self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.toolbar) c = theme_icon_color() self.save_action = self.toolbar.addAction(qta.icon("fa5s.save", color=c), "Save") self.save_action.triggered.connect(self.save_current_context) # Add spacer to center the run buttons spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) self.toolbar.addWidget(spacer) self.run_current_scene_action = self.toolbar.addAction(qta.icon("fa5s.play", color="#5adc78"), "Run Current Scene") self.run_current_scene_action.triggered.connect(self.run_current_scene) self.run_game_action = self.toolbar.addAction(qta.icon("fa5s.gamepad", color="#5adc78"), "Run Game") self.run_game_action.triggered.connect(self.run_game) # Add spacer on the right to keep run buttons centered spacer_right = QWidget() spacer_right.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) self.toolbar.addWidget(spacer_right)
[docs] def closeEvent(self, event): self.console_dock.cleanup() super().closeEvent(event)
[docs] def open_engine_settings(self): dialog = EngineSettings(self) dialog.exec()
[docs] def show_about_dialog(self): dialog = QDialog(self) dialog.setWindowTitle("About AxisPy Engine") dialog.setFixedSize(560, 620) dialog.setWindowModality(Qt.WindowModality.ApplicationModal) layout = QVBoxLayout(dialog) layout.setAlignment(Qt.AlignmentFlag.AlignCenter) # Logo logo_path = os.path.join(os.path.dirname(__file__), "assets", "images", "logo.png") if os.path.exists(logo_path): logo_label = QLabel() logo_pixmap = QPixmap(logo_path) logo_pixmap = logo_pixmap.scaled(150, 150, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) logo_label.setPixmap(logo_pixmap) logo_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(logo_label) # Title title_label = QLabel("AxisPy Engine") title_font = QFont() title_font.setPointSize(20) title_font.setBold(True) title_label.setFont(title_font) title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(title_label) # Description (scrollable) scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) scroll_area.setFrameShape(QFrame.Shape.NoFrame) description = QLabel( "<p>AxisPy Game Engine is a Python-based 2D game engine designed for developers " "who are passionate about both Python and game development in AI era.</p>" "<p>Built on top of Pygame, AxisPy aims to provide a structured and intuitive " "development experience inspired by modern engines, while staying " "fully within the Python ecosystem.</p>" "<p>Instead of competing with high-performance engines, AxisPy focuses on:</p>" "<ul>" "<li><b>Developer experience first</b>: clean architecture, readable code, and fast iteration</li>" "<li><b>Python-native workflows</b>: no need to switch languages or toolchains</li>" "<li><b>Rapid prototyping</b>: ideal for experimenting, learning, and building indie projects</li>" "<li><b>Extensibility</b>: leverage Python's vast ecosystem (AI, data, tools, etc.) directly in your games</li>" "</ul>" "<p>AxisPy is especially suited for:</p>" "<ul>" "<li>Indie developers who prefer Python</li>" "<li>Educators and students learning game development</li>" "<li>Developers exploring game ideas without heavy engine overhead</li>" "</ul>" "<p><i>AxisPy is not about replacing existing AAA engines, it's about empowering " "Python developers to create games in an environment they already love.</i></p>" ) description.setTextFormat(Qt.TextFormat.RichText) description.setWordWrap(True) description.setAlignment(Qt.AlignmentFlag.AlignLeft) description.setContentsMargins(10, 10, 10, 10) scroll_area.setWidget(description) scroll_area.setMinimumHeight(400) layout.addWidget(scroll_area) # Close button close_btn = QPushButton("Close") close_btn.clicked.connect(dialog.accept) layout.addWidget(close_btn) dialog.exec()
[docs] def update_theme_icons(self): c = theme_icon_color() # Toolbar self.save_action.setIcon(qta.icon("fa5s.save", color=c)) self.run_current_scene_action.setIcon(qta.icon("fa5s.play", color="#5adc78")) self.run_game_action.setIcon(qta.icon("fa5s.gamepad", color="#5adc78")) # Menu actions for icon_name, action in self._menu_actions: action.setIcon(qta.icon(icon_name, color=c)) # Propagate to panels if hasattr(self, 'hierarchy_dock'): self.hierarchy_dock.update_theme_icons() if hasattr(self, 'inspector_dock'): self.inspector_dock.update_theme_icons() if hasattr(self, 'asset_manager_dock'): self.asset_manager_dock.update_theme_icons() self.script_editor.apply_theme()
def _create_symbol_icon(self, symbol, color, pixel_size): pixmap = QPixmap(24, 24) pixmap.fill(Qt.GlobalColor.transparent) painter = QPainter(pixmap) painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) painter.setPen(color) font = QFont() font.setPixelSize(pixel_size) painter.setFont(font) painter.drawText(QRect(0, 0, 24, 24), Qt.AlignmentFlag.AlignCenter, symbol) painter.end() return QIcon(pixmap)
[docs] def create_menus(self): menubar = self.menuBar() # File Menu file_menu = menubar.addMenu("&File") file_menu.addSeparator() c = theme_icon_color() self._menu_actions = [] # (icon_name, QAction) for theme updates def _ma(icon_name, text, parent=self): act = QAction(qta.icon(icon_name, color=c), text, parent) self._menu_actions.append((icon_name, act)) return act new_scene_action = _ma("fa5s.file", "New Scene") new_scene_action.setShortcut("Ctrl+N") new_scene_action.triggered.connect(self.new_scene) file_menu.addAction(new_scene_action) save_scene_action = _ma("fa5s.save", "Save") save_scene_action.setShortcut("Ctrl+S") save_scene_action.triggered.connect(self.save_current_context) file_menu.addAction(save_scene_action) open_scene_action = _ma("fa5s.folder-open", "Open Scene") open_scene_action.setShortcut("Ctrl+O") open_scene_action.triggered.connect(self.open_scene) file_menu.addAction(open_scene_action) file_menu.addSeparator() project_hub_action = _ma("fa5s.home", "Go To Projects Hub") project_hub_action.triggered.connect(self.open_project_hub) file_menu.addAction(project_hub_action) # Edit Menu edit_menu = menubar.addMenu("&Edit") self.undo_action = _ma("fa5s.undo", "Undo") self.undo_action.setShortcut(QKeySequence.StandardKey.Undo) self.undo_action.triggered.connect(self.undo_manager.undo) edit_menu.addAction(self.undo_action) self.redo_action = _ma("fa5s.redo", "Redo") self.redo_action.setShortcut(QKeySequence.StandardKey.Redo) self.redo_action.triggered.connect(self.undo_manager.redo) edit_menu.addAction(self.redo_action) edit_menu.addSeparator() settings_action = _ma("fa5s.cog", "Engine Settings") settings_action.triggered.connect(self.open_engine_settings) edit_menu.addAction(settings_action) project_menu = menubar.addMenu("&Project") project_settings_action = _ma("fa5s.sliders-h", "Project Settings") project_settings_action.triggered.connect(self.open_project_settings) project_menu.addAction(project_settings_action) export_action = _ma("fa5s.file-export", "Export...") export_action.triggered.connect(self.open_export_dialog) project_menu.addAction(export_action) # View Menu view_menu = menubar.addMenu("&View") # Hierarchy dock hierarchy_action = _ma("fa5s.sitemap", "Hierarchy") hierarchy_action.setCheckable(True) hierarchy_action.setChecked(True) hierarchy_action.triggered.connect(lambda checked: self.hierarchy_dock.setVisible(checked)) view_menu.addAction(hierarchy_action) self.hierarchy_dock.visibilityChanged.connect(hierarchy_action.setChecked) # Inspector dock inspector_action = _ma("fa5s.info-circle", "Inspector") inspector_action.setCheckable(True) inspector_action.setChecked(True) inspector_action.triggered.connect(lambda checked: self.inspector_dock.setVisible(checked)) view_menu.addAction(inspector_action) self.inspector_dock.visibilityChanged.connect(inspector_action.setChecked) # Asset Manager dock asset_manager_action = _ma("fa5s.folder", "Asset Manager") asset_manager_action.setCheckable(True) asset_manager_action.setChecked(True) asset_manager_action.triggered.connect(lambda checked: self.asset_manager_dock.setVisible(checked)) view_menu.addAction(asset_manager_action) self.asset_manager_dock.visibilityChanged.connect(asset_manager_action.setChecked) # Console dock console_action = _ma("fa5s.terminal", "Console") console_action.setCheckable(True) console_action.setChecked(True) console_action.triggered.connect(lambda checked: self.console_dock.setVisible(checked)) view_menu.addAction(console_action) self.console_dock.visibilityChanged.connect(console_action.setChecked) # AI Chat dock chat_action = _ma("fa5s.robot", "AI Assistant") chat_action.setCheckable(True) chat_action.setChecked(False) chat_action.triggered.connect(lambda checked: self.chat_dock.setVisible(checked)) view_menu.addAction(chat_action) self.chat_dock.visibilityChanged.connect(chat_action.setChecked) # Animation Editor dock (if it exists as a dock) if hasattr(self, 'animation_dock'): animation_action = _ma("fa5s.film", "Animation Editor") animation_action.setCheckable(True) animation_action.setChecked(True) animation_action.triggered.connect(lambda checked: self.animation_dock.setVisible(checked)) view_menu.addAction(animation_action) self.animation_dock.visibilityChanged.connect(animation_action.setChecked) # GameObject Menu go_menu = menubar.addMenu("&GameObject") create_empty_action = _ma("fa5s.cube", "Create Empty") create_empty_action.triggered.connect(self.create_empty_entity) go_menu.addAction(create_empty_action) # Help Menu help_menu = menubar.addMenu("&Help") about_action = _ma("fa5s.question-circle", "About") about_action.triggered.connect(self.show_about_dialog) help_menu.addAction(about_action)
[docs] def open_script(self, path): self.script_editor.open_file(path) self.central_tabs.setCurrentWidget(self.script_editor)
[docs] def open_animation_controller_editor(self, path): self.animation_editor.project_dir = self.project_path or "." self.animation_editor.open_file(path) self.central_tabs.setCurrentWidget(self.animation_editor)
[docs] def open_animation_clip(self, path): self.animation_editor.project_dir = self.project_path or "." self.animation_editor.open_file(path) self.central_tabs.setCurrentWidget(self.animation_editor)
[docs] def new_project(self): dir_path = QFileDialog.getExistingDirectory(self, "Select New Project Directory") if dir_path: dir_path = os.path.normpath(dir_path) self.project_path = dir_path os.environ["AXISPY_PROJECT_PATH"] = dir_path ResourceManager.set_base_path(dir_path) self.plugin_manager.notify_project_open(dir_path) self.asset_manager_dock.set_project_path(dir_path) self.setWindowTitle(f"AxisPy Engine - {dir_path}") self.load_project_settings() # Create project.config config_path = os.path.join(dir_path, "project.config") if not os.path.exists(config_path): default_config = { "game_name": os.path.basename(dir_path), "game_icon": "", "entry_scene": "", "resolution": { "width": 800, "height": 600 }, "display": { "window": { "width": 800, "height": 600, "resizable": True, "fullscreen": False }, "virtual_resolution": { "width": 800, "height": 600 }, "stretch": { "mode": "fit", "aspect": "keep", "scale": "fractional" } }, "groups": [], "physics_collision_matrix": {}, "version": "1.0.0" } default_config["layers"] = ["Default"] try: with open(config_path, "w") as f: json.dump(default_config, f, indent=4) except Exception as e: print(f"Failed to create project.config: {e}") # Reset scene self.scene = Scene() self.scene.ensure_main_camera() self.viewport.bind_scene(self.scene) self.hierarchy_dock.scene = self.scene self.hierarchy_dock.refresh() self.inspector_dock.set_entity(None) # Hide tilemap editor when creating new project (now using component UI) # self._update_tilemap_editor_visibility([]) self.load_last_opened_scene()
[docs] def open_project(self): dir_path = QFileDialog.getExistingDirectory(self, "Open Project Directory") if dir_path: dir_path = os.path.normpath(dir_path) self.project_path = dir_path os.environ["AXISPY_PROJECT_PATH"] = dir_path ResourceManager.set_base_path(dir_path) self.plugin_manager.notify_project_open(dir_path) self.asset_manager_dock.set_project_path(dir_path) self.setWindowTitle(f"AxisPy Engine - {dir_path}") self.load_project_settings() # Hide tilemap editor when opening project (now using component UI) # self._update_tilemap_editor_visibility([]) self.load_last_opened_scene()
[docs] def new_scene(self): if not self.project_path: QMessageBox.warning(self, "Warning", "Please create or open a project first.") return # Confirm save current scene? # For now, just reset self.scene = Scene("New Scene") self.scene.ensure_main_camera() self.current_scene_path = None # Re-link viewport self.viewport.bind_scene(self.scene) # Refresh UI self.hierarchy_dock.scene = self.scene self.hierarchy_dock.refresh() self.inspector_dock.set_entity(None)
# Hide tilemap editor when creating new project (now using component UI) # self._update_tilemap_editor_visibility([])
[docs] def save_current_context(self): # Check which tab is active current_index = self.central_tabs.currentIndex() if current_index == 0: # Scene tab self.save_scene() elif current_index == 1: # Script tab self.script_editor.save_file()
[docs] def save_scene(self): if not self.project_path: QMessageBox.warning(self, "Warning", "Please create or open a project first.") return False if not self.current_scene_path: file_path, _ = QFileDialog.getSaveFileName(self, "Save Scene", self.project_path, "Scene Files (*.scn)") if file_path: self.current_scene_path = file_path else: return False try: self.scene.editor_view_state = self.viewport.get_scene_view_state() data = SceneSerializer.to_json(self.scene) with open(self.current_scene_path, "w") as f: f.write(data) self.scene._file_path = self.current_scene_path # Track file path for AI tools self._persist_last_opened_scene(self.current_scene_path) print(f"Scene saved to {self.current_scene_path}") return True except Exception as e: QMessageBox.critical(self, "Error", f"Failed to save scene: {e}") return False
[docs] def open_scene(self): if not self.project_path: QMessageBox.warning(self, "Warning", "Please create or open a project first.") return file_path, _ = QFileDialog.getOpenFileName(self, "Open Scene", self.project_path, "Scene Files (*.scn)") if file_path: self._load_scene_from_path(file_path, show_error=True)
[docs] def create_empty_entity(self): entity = self.scene.world.create_entity("New Entity") # Add Transform by default from core.components import Transform entity.add_component(Transform()) self.hierarchy_dock.refresh()
[docs] def load_project_settings(self): if not self.project_path: return config_path = os.path.join(self.project_path, "project.config") if os.path.exists(config_path): try: with open(config_path, "r") as f: config = json.load(f) # Store config and update viewport self.project_config = config self.viewport.update_project_config(config) bg_color = config.get("background_color", [33, 33, 33]) # Ensure integer values bg_color = [int(c) for c in bg_color] self.viewport.bg_color = tuple(bg_color) display = config.get("display", {}) virtual = display.get("virtual_resolution", {}) res = config.get("resolution", {"width": 800, "height": 600}) game_width = int(virtual.get("width", res.get("width", 800))) game_height = int(virtual.get("height", res.get("height", 600))) self.viewport.game_resolution = (game_width, game_height) layers = config.get("layers", ["Default"]) normalized_layers = [] seen = set() if isinstance(layers, list): for layer in layers: name = str(layer).strip() if not name: continue lowered = name.lower() if lowered in seen: continue seen.add(lowered) normalized_layers.append(name) if "default" in seen: normalized_layers = [layer for layer in normalized_layers if layer.lower() != "default"] normalized_layers.insert(0, "Default") self.scene.world.layers = normalized_layers valid_layers = set(normalized_layers) for entity in self.scene.world.entities: if entity.layer not in valid_layers: entity.set_layer("Default") groups = config.get("groups", []) normalized_groups = [] group_seen = set() if isinstance(groups, list): for group_name in groups: group_text = str(group_name).strip() if not group_text: continue lowered_group = group_text.lower() if lowered_group in group_seen: continue group_seen.add(lowered_group) normalized_groups.append(group_text) world = self.scene.world for group_name in list(world.groups.keys()): if group_name not in normalized_groups: members = list(world.groups.get(group_name, set())) for entity in members: entity.remove_group(group_name) for group_name in normalized_groups: world.groups.setdefault(group_name, set()) raw_matrix = config.get("physics_collision_matrix", {}) if not isinstance(raw_matrix, dict): raw_matrix = {} normalized_matrix = {} for row_group in normalized_groups: targets = raw_matrix.get(row_group, normalized_groups) if not isinstance(targets, list): targets = normalized_groups allowed_targets = [] target_seen = set() for target in targets: target_name = str(target).strip() if target_name not in normalized_groups: continue lowered_target = target_name.lower() if lowered_target in target_seen: continue target_seen.add(lowered_target) allowed_targets.append(target_name) normalized_matrix[row_group] = allowed_targets for row_group in normalized_groups: for target in list(normalized_matrix.get(row_group, [])): peer = normalized_matrix.setdefault(target, []) if row_group not in peer: peer.append(row_group) world.physics_group_order = list(normalized_groups) world.physics_collision_matrix = normalized_matrix print(f"Loaded bg_color: {bg_color}, res: {self.viewport.game_resolution}") # Configure AI provider self._configure_ai_provider(config) except Exception as e: print(f"Failed to load project settings: {e}")
def _configure_ai_provider(self, config: dict): """Create and set the AI provider from project config.""" # First: ensure sessions are loaded (before any callbacks are set) if self.project_path: self.chat_manager.set_project_path(self.project_path) # Wire context and tool executor self.chat_manager.context_builder.set_project_path(self.project_path) self.chat_manager.tool_executor.set_project_path(self.project_path) # Refresh UI self.chat_dock.refresh_sessions() self.chat_dock._reload_messages() ai_cfg = config.get("ai", {}) provider_name = ai_cfg.get("provider", "openai") if provider_name == "openrouter": provider = OpenRouterProvider( api_key=ai_cfg.get("openrouter_api_key", ""), model=ai_cfg.get("openrouter_model", "deepseek/deepseek-chat:free"), base_url=ai_cfg.get("openrouter_url", ""), ) elif provider_name == "local": provider = LocalLLMProvider( model=ai_cfg.get("local_model", "llama3"), base_url=ai_cfg.get("local_url", "http://localhost:11434/v1"), ) elif provider_name == "google": provider = GoogleProvider( api_key=ai_cfg.get("google_api_key", ""), model=ai_cfg.get("google_model", "gemini-2.5-flash"), ) elif provider_name == "anthropic": provider = AnthropicProvider( api_key=ai_cfg.get("anthropic_api_key", ""), model=ai_cfg.get("anthropic_model", "claude-3-5-sonnet-latest"), ) elif provider_name == "nvidia": provider = NvidiaProvider( api_key=ai_cfg.get("nvidia_api_key", ""), model=ai_cfg.get("nvidia_model", "google/gemma-4-31b-it"), base_url=ai_cfg.get("nvidia_url", "https://integrate.api.nvidia.com/v1"), ) else: provider = OpenAIProvider( api_key=ai_cfg.get("api_key", ""), model=ai_cfg.get("model", "gpt-4o-mini"), base_url=ai_cfg.get("base_url", "https://api.openai.com/v1"), ) self.chat_manager.set_provider(provider) # Wire scene getters self.chat_manager.context_builder.set_scene_getter(lambda: self.scene) self.chat_manager.context_builder.set_selected_entities_getter( lambda: self.inspector_dock.current_entities if hasattr(self.inspector_dock, 'current_entities') else [] ) self.chat_manager.tool_executor.set_scene_getter(lambda: self.scene) self.chat_manager.tool_executor.set_selected_entities_getter( lambda: self.inspector_dock.current_entities if hasattr(self.inspector_dock, 'current_entities') else [] ) self.chat_manager.tool_executor.set_scene_reload_callback(self._reload_scene_from_ai) self.chat_dock.update_model_label()
[docs] def load_last_opened_scene(self): if not self.project_path: return False config = self._read_project_config() if not config: return False scene_path = self._resolve_scene_path(config.get("last_opened_scene", "")) if not scene_path: return False if not os.path.exists(scene_path): return False return self._load_scene_from_path(scene_path, show_error=False)
def _load_scene_from_path(self, file_path, show_error=True): try: with open(file_path, "r") as f: self.scene = SceneSerializer.from_json(f.read()) self.scene._file_path = file_path # Track file path for AI tools self.current_scene_path = file_path self.viewport.bind_scene(self.scene) self.hierarchy_dock.scene = self.scene self.hierarchy_dock.refresh() self.inspector_dock.set_entity(None) # Hide tilemap editor when loading scene (now using component UI) # self._update_tilemap_editor_visibility([]) self.load_project_settings() self._persist_last_opened_scene(file_path) print(f"Scene loaded from {file_path}") return True except Exception as e: if show_error: QMessageBox.critical(self, "Error", f"Failed to load scene: {e}") else: print(f"Failed to auto-load scene: {e}") return False def _reload_scene_from_ai(self, file_path: str): """Reload scene after AI tool has edited the scene file.""" try: with open(file_path, "r", encoding="utf-8") as f: scene_data = json.load(f) # Reload entities from the modified file from core.serializer import SceneSerializer restored = SceneSerializer.from_json(json.dumps(scene_data)) restored._file_path = file_path self.scene = restored self.current_scene_path = file_path self.viewport.bind_scene(self.scene) self.hierarchy_dock.scene = self.scene self.hierarchy_dock.refresh() self.inspector_dock.set_entity(None) print(f"Scene reloaded after AI edit: {file_path}") except Exception as e: print(f"Failed to reload scene after AI edit: {e}") def _read_project_config(self): if not self.project_path: return {} config_path = os.path.join(self.project_path, "project.config") if not os.path.exists(config_path): return {} try: with open(config_path, "r") as f: return json.load(f) except Exception: return {} def _write_project_config(self, config): if not self.project_path: return config_path = os.path.join(self.project_path, "project.config") try: with open(config_path, "w") as f: json.dump(config, f, indent=4) except Exception as e: print(f"Failed to save project settings: {e}") def _persist_last_opened_scene(self, scene_path): if not self.project_path or not scene_path: return config = self._read_project_config() abs_project = os.path.abspath(self.project_path) abs_scene = os.path.abspath(scene_path) try: rel_scene = os.path.relpath(abs_scene, abs_project) if rel_scene.startswith(".."): scene_value = abs_scene else: scene_value = rel_scene except ValueError: scene_value = abs_scene config["last_opened_scene"] = ResourceManager.portable_path(scene_value) self._write_project_config(config) def _resolve_scene_path(self, scene_value): if not scene_value: return "" native_value = ResourceManager.to_os_path(scene_value) if os.path.isabs(native_value): return native_value if not self.project_path: return "" return os.path.normpath(os.path.join(self.project_path, native_value))
[docs] def open_project_settings(self): if not self.project_path: QMessageBox.warning(self, "Warning", "Please create or open a project first.") return dialog = ProjectSettingsDialog(self.project_path, self) if dialog.exec(): # Reload settings if saved self.load_project_settings()
[docs] def open_project_hub(self): if not self.save_scene(): return hub = ProjectHub() hub.project_selected.connect(self._launch_editor_from_hub) app = QApplication.instance() if app and not hasattr(app, "_axispy_windows"): app._axispy_windows = [] if app: app._axispy_windows.append(hub) hub.show() self.close()
def _launch_editor_from_hub(self, project_path): project_path = os.path.normpath(project_path) os.environ["AXISPY_PROJECT_PATH"] = project_path ResourceManager.set_base_path(project_path) main_window = MainWindow() main_window.project_path = project_path main_window.asset_manager_dock.set_project_path(project_path) main_window.setWindowTitle(f"AxisPy Engine - {project_path}") main_window.load_project_settings() main_window.load_last_opened_scene() app = QApplication.instance() if app and not hasattr(app, "_axispy_windows"): app._axispy_windows = [] if app: app._axispy_windows.append(main_window) main_window.show()
[docs] def on_viewport_entity_selected(self, entities): # Update hierarchy selection to match viewport without triggering recursion self.hierarchy_dock.tree.blockSignals(True) # Ensure it's a list if not isinstance(entities, list): entities = [entities] if entities else [] self.hierarchy_dock.select_entities(entities) self.hierarchy_dock.tree.blockSignals(False) self.inspector_dock.set_entities(entities)
# Update tilemap editor visibility (now using component UI) # self._update_tilemap_editor_visibility(entities)
[docs] def on_entity_deleted(self, _): # Refresh hierarchy and clear inspector when an entity is deleted from viewport self.hierarchy_dock.refresh() self.inspector_dock.set_entities([])
# Hide tilemap editor when entity is deleted (now using component UI) # self._update_tilemap_editor_visibility([])
[docs] def on_entity_selected(self): items = self.hierarchy_dock.tree.selectedItems() entities = [] for item in items: entity_id = item.data(0, Qt.ItemDataRole.UserRole) # Resolve entity from ID entity = self.scene.world.get_entity_by_id(entity_id) if entity: entities.append(entity) self.inspector_dock.set_entities(entities) # Update tilemap editor visibility (now using component UI) # self._update_tilemap_editor_visibility(entities) # Sync viewport selection self.viewport.selected_entities = entities if entities: # Pass all entities to gizmo self.viewport.gizmo.set_targets(entities) else: self.viewport.gizmo.set_targets([])
def _launch_runtime(self, scene_data: str, scene_name: str): base_path = self.project_path if self.project_path else os.getcwd() engine_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) temp_scene_path = os.path.join(base_path, "temp_scene.scn") profile = LaunchProfile( module="core.player", scene_file_name=temp_scene_path, working_directory=engine_root, report_directory=os.path.join(base_path, ".axispy", "reports"), python_executable=sys.executable, python_path_entries=[engine_root], env_overrides={ "AXISPY_PROJECT_PATH": base_path, "AXISPY_EDITOR_MODE": "1" # Flag to indicate running in editor } ) try: handle = RuntimeCommandBuilder.launch(profile, scene_data, use_pipe=True) self._last_launch_handle = handle self.console_dock.set_player_process(handle.process) self._logger.info( "Runtime launched", scene_name=scene_name, command=" ".join(handle.command), scene_path=handle.scene_path, stdout_report=handle.report_stdout_path, stderr_report=handle.report_stderr_path, pid=handle.process.pid ) self.statusBar().showMessage(f"Runtime launched (PID {handle.process.pid})", 4000) QTimer.singleShot(500, self._check_runtime_process_health) except Exception as error: self._logger.error("Runtime launch failed", scene_name=scene_name, error=str(error)) QMessageBox.critical(self, "Runtime Launch Failed", str(error))
[docs] def run_current_scene(self): self.scene.editor_view_state = self.viewport.get_scene_view_state() scene_data = SceneSerializer.to_json(self.scene) self._launch_runtime(scene_data, self.scene.name)
[docs] def run_game(self): if not self.project_path: QMessageBox.warning(self, "Warning", "Please create or open a project first.") return config = self._read_project_config() entry_scene_value = str(config.get("entry_scene", "")).strip() if not entry_scene_value: QMessageBox.warning(self, "Run Game", "No entry scene set in Project Settings.") return entry_scene_path = self._resolve_scene_path(entry_scene_value) if not entry_scene_path or not os.path.exists(entry_scene_path): QMessageBox.warning(self, "Run Game", f"Entry scene not found:\n{entry_scene_value}") return try: with open(entry_scene_path, "r", encoding="utf-8") as f: scene_data = f.read() scene_name = os.path.basename(entry_scene_path) self._launch_runtime(scene_data, scene_name) except Exception as error: QMessageBox.critical(self, "Run Game", f"Failed to read entry scene:\n{error}")
[docs] def open_export_dialog(self): if not self.project_path: QMessageBox.warning(self, "Warning", "Please create or open a project first.") return dialog = ExportDialog(self.project_path, self) dialog.exec()
def _check_runtime_process_health(self): handle = self._last_launch_handle if not handle: return process = handle.process if process.poll() is None: return # Check if process exited with an error if process.returncode == 0: # Normal exit - just log info and clean up self._logger.info( "Runtime exited normally", return_code=process.returncode ) handle.close_logs() self._last_launch_handle = None return # Process exited with error handle.close_logs() error_details = "" try: if os.path.exists(handle.report_stderr_path): with open(handle.report_stderr_path, "r", encoding="utf-8") as file: error_details = file.read(1000).strip() except Exception: error_details = "" self._logger.error( "Runtime exited with error", return_code=process.returncode, stderr_report=handle.report_stderr_path, stderr_preview=error_details ) message = f"Runtime exited with error code {process.returncode}." if error_details: message = f"{message}\n\n{error_details}" QMessageBox.critical(self, "Runtime Error", message) self._last_launch_handle = None