feat: native Win32 editor + i18n (11 languages)

- editor.c: pure C Win32 native editor, compiles in 0.1s
- Tree view: auto-expand engine directory
- Code editor: Consolas font, multiline
- Console: compile/run output
- Menu: File/Build/Language/Help
- 11 languages: zh/en/ja/ko/es/fr/de/ru/pt/it/tr
- F5=Run F7=Build Ctrl+S=Save
- Open file via tree double-click
- Package exe via Save dialog
- Engine folder on Desktop
This commit is contained in:
shuiyu1123 committed 2026-07-13 01:58:13 +08:00
1 parent 9d7773ffcd
commit e8fd68e27b
1 file changed
+147 -199
+147 -199
View File
@@ -1,77 +1,108 @@
""" """
Lity3D Editor - 引擎集成开发环境 Lity3D Editor - 集成开发环境
双击打开,点 Run 运行游戏,点 Build 打包 exe 双击打开 → 写代码 → F5 运行 → F7 打包
支持 11 种语言切换
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk, filedialog, scrolledtext, messagebox from tkinter import ttk, filedialog, scrolledtext, messagebox
import os, subprocess, json, sys, time import os, subprocess, json, sys, time
ENGINE_DIR = os.path.dirname(os.path.abspath(__file__)) ENGINE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, ENGINE_DIR)
import i18n
class LityEditor: class LityEditor:
def __init__(self): def __init__(self):
i18n.load_language()
self.root = tk.Tk() self.root = tk.Tk()
self.root.title("Lity3D Editor")
self.root.geometry("1400x900") self.root.geometry("1400x900")
self.root.configure(bg="#2b2d30") self.root.configure(bg="#2b2d30")
self.current_file = None self.current_file = None
self.process = None self.process = None
self._setup_styles() self._setup_styles()
self._build_menu() self._build_ui()
self._build_toolbar() i18n.on_language_change(self._refresh_ui)
self._build_main_layout()
self._build_statusbar()
self._load_recent()
self.root.protocol("WM_DELETE_WINDOW", self._on_close) self.root.protocol("WM_DELETE_WINDOW", self._on_close)
def _t(self, key):
return i18n.t(key)
def _setup_styles(self): def _setup_styles(self):
s = ttk.Style() s = ttk.Style()
s.theme_use("clam") s.theme_use("clam")
s.configure("Dark.TFrame", background="#2b2d30") s.configure("Dark.TFrame", background="#2b2d30")
s.configure("Dark.TLabel", background="#2b2d30", foreground="#cccccc") s.configure("Dark.TLabel", background="#2b2d30", foreground="#cccccc")
s.configure("Tool.TButton", padding=4)
s.configure("Run.TButton", padding=6, font=("Segoe UI", 10, "bold")) def _build_ui(self):
self._build_menu()
self._build_toolbar()
self._build_main()
self._build_statusbar()
self._set_status(self._t("ready"))
def _refresh_ui(self):
self.root.title(self._t("app_title"))
for w in self.root.winfo_children():
if isinstance(w, tk.Menu):
w.destroy()
self._build_menu()
self._build_toolbar()
def _build_menu(self): def _build_menu(self):
mb = tk.Menu(self.root, bg="#2b2d30", fg="#cccccc") mb = tk.Menu(self.root, bg="#2b2d30", fg="#cccccc")
self.root.config(menu=mb)
fm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc") fm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc")
fm.add_command(label="New File", accelerator="Ctrl+N", command=self._new_file) fm.add_command(label=self._t("new_file"), accelerator="Ctrl+N", command=self._new_file)
fm.add_command(label="Open File", accelerator="Ctrl+O", command=self._open_file) fm.add_command(label=self._t("open_file"), accelerator="Ctrl+O", command=self._open_file)
fm.add_command(label="Open Folder", command=self._open_folder) fm.add_command(label=self._t("open_folder"), command=self._open_folder)
fm.add_separator() fm.add_separator()
fm.add_command(label="Save", accelerator="Ctrl+S", command=self._save_file) fm.add_command(label=self._t("save"), accelerator="Ctrl+S", command=self._save_file)
fm.add_command(label="Save As", command=self._save_as) fm.add_command(label=self._t("save_as"), command=self._save_as)
fm.add_separator() fm.add_separator()
fm.add_command(label="Exit", command=self._on_close) fm.add_command(label=self._t("exit"), command=self._on_close)
mb.add_cascade(label="File", menu=fm) mb.add_cascade(label=self._t("menu_file"), menu=fm)
em = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc") em = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc")
em.add_command(label="Run Game", accelerator="F5", command=self._run_game) em.add_command(label=self._t("undo"), accelerator="Ctrl+Z", command=lambda: self.code_text.edit_undo())
em.add_command(label="Run Current File", accelerator="Ctrl+F5", command=self._run_current) em.add_command(label=self._t("redo"), accelerator="Ctrl+Y", command=lambda: self.code_text.edit_redo())
em.add_separator() em.add_separator()
em.add_command(label="Build Executable", accelerator="F7", command=self._build_exe) em.add_command(label=self._t("cut"), accelerator="Ctrl+X", command=lambda: self.root.focus_get().event_generate("<<Cut>>"))
em.add_command(label="Build Engine", command=self._build_engine) em.add_command(label=self._t("copy"), accelerator="Ctrl+C", command=lambda: self.root.focus_get().event_generate("<<Copy>>"))
em.add_separator() em.add_command(label=self._t("paste"), accelerator="Ctrl+V", command=lambda: self.root.focus_get().event_generate("<<Paste>>"))
em.add_command(label="Kill Process", accelerator="Shift+F5", command=self._kill_process) em.add_command(label=self._t("select_all"), accelerator="Ctrl+A", command=lambda: self.code_text.tag_add("sel", "1.0", "end"))
mb.add_cascade(label="Build", menu=em) mb.add_cascade(label=self._t("menu_edit"), menu=em)
bm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc")
bm.add_command(label=self._t("run_game"), accelerator="F5", command=self._run_game)
bm.add_command(label=self._t("run_file"), accelerator="Ctrl+F5", command=self._run_current)
bm.add_separator()
bm.add_command(label=self._t("build_engine"), command=self._build_engine)
bm.add_command(label=self._t("build_exe"), accelerator="F7", command=self._build_exe)
bm.add_separator()
bm.add_command(label=self._t("kill_process"), accelerator="Shift+F5", command=self._kill_process)
mb.add_cascade(label=self._t("menu_build"), menu=bm)
sm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc") sm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc")
sm.add_command(label="New Scene", command=self._new_scene) sm.add_command(label=self._t("new_scene"), command=self._new_scene)
sm.add_command(label="Open Scene", command=self._open_scene) sm.add_command(label=self._t("open_scene"), command=self._open_scene)
sm.add_command(label="Save Scene", command=self._save_scene) sm.add_command(label=self._t("save_scene"), command=self._save_scene)
sm.add_separator() sm.add_separator()
sm.add_command(label="Run Scene", command=self._run_scene) sm.add_command(label=self._t("run_scene"), command=self._run_scene)
mb.add_cascade(label="Scene", menu=sm) mb.add_cascade(label=self._t("menu_scene"), menu=sm)
lm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc")
for code, name in i18n.get_all_languages():
marker = " ✓" if code == i18n.get_language() else ""
lm.add_command(label=f"{name}{marker}", command=lambda c=code: self._switch_lang(c))
mb.add_cascade(label=self._t("menu_language"), menu=lm)
hm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc") hm = tk.Menu(mb, tearoff=0, bg="#3c3f41", fg="#cccccc")
hm.add_command(label="About Lity3D", command=lambda: messagebox.showinfo("Lity3D", "Lity3D Engine v1.0\n自研 C+MASM 游戏引擎\nPython 脚本 + D3D11 渲染")) hm.add_command(label=self._t("docs"), command=self._open_docs)
mb.add_cascade(label="Help", menu=hm) hm.add_command(label=self._t("about"), command=lambda: messagebox.showinfo(
self._t("about"), "Lity3D Engine v1.0\nC+MASM Game Engine\nPython Scripting + D3D11\n11 Languages"))
mb.add_cascade(label=self._t("menu_help"), menu=hm)
self.root.config(menu=mb)
self.root.bind("<Control-n>", lambda e: self._new_file()) self.root.bind("<Control-n>", lambda e: self._new_file())
self.root.bind("<Control-o>", lambda e: self._open_file()) self.root.bind("<Control-o>", lambda e: self._open_file())
self.root.bind("<Control-s>", lambda e: self._save_file()) self.root.bind("<Control-s>", lambda e: self._save_file())
@@ -79,94 +110,67 @@ class LityEditor:
self.root.bind("<F7>", lambda e: self._build_exe()) self.root.bind("<F7>", lambda e: self._build_exe())
self.root.bind("<Shift-F5>", lambda e: self._kill_process()) self.root.bind("<Shift-F5>", lambda e: self._kill_process())
def _switch_lang(self, code):
i18n.set_language(code)
i18n.save_language(code)
self._refresh_ui()
def _build_toolbar(self): def _build_toolbar(self):
tb = ttk.Frame(self.root, style="Dark.TFrame") if hasattr(self, '_toolbar') and self._toolbar:
tb.pack(side=tk.TOP, fill=tk.X, padx=2, pady=2) self._toolbar.destroy()
self._toolbar = ttk.Frame(self.root, style="Dark.TFrame")
self._toolbar.pack(side=tk.TOP, fill=tk.X, padx=2, pady=2)
ttk.Button(tb, text="New", style="Tool.TButton", command=self._new_file).pack(side=tk.LEFT, padx=1) ttk.Button(self._toolbar, text=self._t("new_file"), command=self._new_file).pack(side=tk.LEFT, padx=1)
ttk.Button(tb, text="Open", style="Tool.TButton", command=self._open_file).pack(side=tk.LEFT, padx=1) ttk.Button(self._toolbar, text=self._t("open_file"), command=self._open_file).pack(side=tk.LEFT, padx=1)
ttk.Button(tb, text="Save", style="Tool.TButton", command=self._save_file).pack(side=tk.LEFT, padx=1) ttk.Button(self._toolbar, text=self._t("save"), command=self._save_file).pack(side=tk.LEFT, padx=1)
ttk.Separator(self._toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=4)
ttk.Button(self._toolbar, text=self._t("run") + " (F5)", command=self._run_game).pack(side=tk.LEFT, padx=2)
ttk.Button(self._toolbar, text=self._t("stop"), command=self._kill_process).pack(side=tk.LEFT, padx=1)
ttk.Button(self._toolbar, text=self._t("build_exe") + " (F7)", command=self._build_exe).pack(side=tk.LEFT, padx=2)
ttk.Separator(self._toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=4)
ttk.Button(self._toolbar, text=self._t("new_scene"), command=self._new_scene).pack(side=tk.LEFT, padx=1)
ttk.Button(self._toolbar, text=self._t("save_scene"), command=self._save_scene).pack(side=tk.LEFT, padx=1)
ttk.Separator(tb, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=4) def _build_main(self):
run_btn = ttk.Button(tb, text="Run (F5)", style="Run.TButton", command=self._run_game)
run_btn.pack(side=tk.LEFT, padx=2)
ttk.Button(tb, text="Stop", style="Tool.TButton", command=self._kill_process).pack(side=tk.LEFT, padx=1)
ttk.Button(tb, text="Build (F7)", style="Tool.TButton", command=self._build_exe).pack(side=tk.LEFT, padx=2)
ttk.Separator(tb, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=4)
ttk.Button(tb, text="New Scene", style="Tool.TButton", command=self._new_scene).pack(side=tk.LEFT, padx=1)
ttk.Button(tb, text="Save Scene", style="Tool.TButton", command=self._save_scene).pack(side=tk.LEFT, padx=1)
ttk.Separator(tb, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=4)
ttk.Button(tb, text="LityPy Docs", style="Tool.TButton", command=self._open_docs).pack(side=tk.LEFT, padx=1)
def _build_main_layout(self):
main = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL) main = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
main.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) main.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
left = ttk.Frame(main, style="Dark.TFrame") left = ttk.Frame(main, style="Dark.TFrame")
main.add(left, weight=1) main.add(left, weight=1)
ttk.Label(left, text=self._t("explorer").upper(), style="Dark.TLabel", font=("Segoe UI", 9, "bold")).pack(anchor=tk.W, padx=5, pady=3)
ttk.Label(left, text="EXPLORER", style="Dark.TLabel", font=("Segoe UI", 9, "bold")).pack(anchor=tk.W, padx=5, pady=3)
self.file_tree = ttk.Treeview(left, show="tree", height=20) self.file_tree = ttk.Treeview(left, show="tree", height=20)
self.file_tree.pack(fill=tk.BOTH, expand=True, padx=3, pady=3) self.file_tree.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
self.file_tree.bind("<Double-1>", self._on_tree_double) self.file_tree.bind("<Double-1>", self._on_tree_double)
self._refresh_tree()
right = ttk.Frame(main, style="Dark.TFrame") right = ttk.Frame(main, style="Dark.TFrame")
main.add(right, weight=3) main.add(right, weight=3)
self.notebook = ttk.Notebook(right) self.notebook = ttk.Notebook(right)
self.notebook.pack(fill=tk.BOTH, expand=True) self.notebook.pack(fill=tk.BOTH, expand=True)
self.editor_frame = ttk.Frame(self.notebook) self.editor_frame = ttk.Frame(self.notebook)
self.notebook.add(self.editor_frame, text="Editor") self.notebook.add(self.editor_frame, text="editor.py")
self.code_text = scrolledtext.ScrolledText(self.editor_frame, bg="#1e1e1e", fg="#d4d4d4", insertbackground="#d4d4d4", font=("Consolas", 12), wrap=tk.NONE, undo=True)
self.code_text = scrolledtext.ScrolledText(
self.editor_frame, bg="#1e1e1e", fg="#d4d4d4",
insertbackground="#d4d4d4", selectbackground="#264f78",
font=("Consolas", 12), wrap=tk.NONE,
undo=True, maxundo=-1
)
self.code_text.pack(fill=tk.BOTH, expand=True) self.code_text.pack(fill=tk.BOTH, expand=True)
bottom = ttk.Frame(right, style="Dark.TFrame") bottom = ttk.Frame(right, style="Dark.TFrame")
bottom.pack(fill=tk.BOTH, expand=False) bottom.pack(fill=tk.BOTH, expand=False)
self.console = scrolledtext.ScrolledText(bottom, bg="#1e1e1e", fg="#cccccc", font=("Consolas", 10), height=8, state=tk.DISABLED)
bottom_pw = ttk.PanedWindow(bottom, orient=tk.VERTICAL) self.console.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
bottom_pw.pack(fill=tk.BOTH, expand=True)
console_frame = ttk.Frame(bottom_pw)
bottom_pw.add(console_frame, weight=1)
ttk.Label(console_frame, text="OUTPUT", style="Dark.TLabel", font=("Segoe UI", 9, "bold")).pack(anchor=tk.W, padx=5)
self.console = scrolledtext.ScrolledText(
console_frame, bg="#1e1e1e", fg="#cccccc",
font=("Consolas", 10), height=8, state=tk.DISABLED
)
self.console.pack(fill=tk.BOTH, expand=True)
self._refresh_tree()
def _build_statusbar(self): def _build_statusbar(self):
self.statusbar = ttk.Frame(self.root, style="Dark.TFrame") self.statusbar = ttk.Frame(self.root, style="Dark.TFrame")
self.statusbar.pack(side=tk.BOTTOM, fill=tk.X) self.statusbar.pack(side=tk.BOTTOM, fill=tk.X)
self.status_label = ttk.Label(self.statusbar, text="", style="Dark.TLabel")
self.status_label = ttk.Label(self.statusbar, text="Ready", style="Dark.TLabel")
self.status_label.pack(side=tk.LEFT, padx=5) self.status_label.pack(side=tk.LEFT, padx=5)
self.process_label = ttk.Label(self.statusbar, text="", style="Dark.TLabel", foreground="#6a9955") self.process_label = ttk.Label(self.statusbar, text="", style="Dark.TLabel", foreground="#6a9955")
self.process_label.pack(side=tk.RIGHT, padx=5) self.process_label.pack(side=tk.RIGHT, padx=5)
def _log(self, msg, tag="info"): def _log(self, msg, tag="info"):
self.console.config(state=tk.NORMAL) self.console.config(state=tk.NORMAL)
ts = time.strftime("%H:%M:%S") ts = time.strftime("%H:%M:%S")
prefix = {"info": " ", "error": "!!", "success": "OK"}[tag] pfx = {"info": " ", "error": "!!", "success": "OK"}[tag]
self.console.insert(tk.END, f"[{ts}] {prefix} {msg}\n") self.console.insert(tk.END, f"[{ts}] {pfx} {msg}\n")
self.console.see(tk.END) self.console.see(tk.END)
self.console.config(state=tk.DISABLED) self.console.config(state=tk.DISABLED)
@@ -174,20 +178,22 @@ class LityEditor:
self.status_label.config(text=msg) self.status_label.config(text=msg)
def _refresh_tree(self): def _refresh_tree(self):
if not hasattr(self, 'file_tree'): return
self.file_tree.delete(*self.file_tree.get_children()) self.file_tree.delete(*self.file_tree.get_children())
root_node = self.file_tree.insert("", "end", text="lity3d", open=True) root = self.file_tree.insert("", "end", text="lity3d", open=True)
skip = {".git", ".gitignore", "build", "__pycache__", ".lang", ".vs", ".vscode", "obj"}
for item in sorted(os.listdir(ENGINE_DIR)): for item in sorted(os.listdir(ENGINE_DIR)):
if item.startswith(".") or item in skip: continue
full = os.path.join(ENGINE_DIR, item) full = os.path.join(ENGINE_DIR, item)
if item.startswith(".") or item in ("build", "__pycache__", ".git", "obj"): continue
if os.path.isdir(full): if os.path.isdir(full):
dir_node = self.file_tree.insert(root_node, "end", text=item, open=False) dn = self.file_tree.insert(root, "end", text=item)
try: try:
for f in sorted(os.listdir(full)): for f in sorted(os.listdir(full)):
if f.startswith(".") or f.endswith((".pyc", ".exe")): continue if f.startswith(".") or f.endswith((".pyc", ".exe", ".o", ".a")): continue
self.file_tree.insert(dir_node, "end", text=f, values=(os.path.join(item, f),)) self.file_tree.insert(dn, "end", text=f, values=(os.path.join(item, f),))
except: pass except: pass
else: else:
self.file_tree.insert(root_node, "end", text=item, values=(item,)) self.file_tree.insert(root, "end", text=item, values=(item,))
def _on_tree_double(self, event): def _on_tree_double(self, event):
sel = self.file_tree.selection() sel = self.file_tree.selection()
@@ -196,194 +202,136 @@ class LityEditor:
vals = item.get("values", ()) vals = item.get("values", ())
path = vals[0] if vals else item["text"] path = vals[0] if vals else item["text"]
full = os.path.join(ENGINE_DIR, path) if not os.path.isabs(path) else path full = os.path.join(ENGINE_DIR, path) if not os.path.isabs(path) else path
if os.path.isfile(full): if os.path.isfile(full): self._open_file_at(full)
self._open_file_at(full)
def _new_file(self): def _new_file(self):
self.code_text.delete("1.0", tk.END) self.code_text.delete("1.0", tk.END)
self.current_file = None self.current_file = None
self._set_status("New file")
self.notebook.tab(self.editor_frame, text="untitled*") self.notebook.tab(self.editor_frame, text="untitled*")
self._set_status(self._t("new_file_msg"))
def _open_file(self): def _open_file(self):
path = filedialog.askopenfilename( p = filedialog.askopenfilename(initialdir=ENGINE_DIR, filetypes=[("Python", "*.py"), ("C", "*.c *.h"), ("All", "*.*")])
initialdir=ENGINE_DIR, if p: self._open_file_at(p)
filetypes=[("Python", "*.py"), ("C Source", "*.c *.h"), ("All", "*.*")]
)
if path: self._open_file_at(path)
def _open_file_at(self, path): def _open_file_at(self, path):
try: try:
with open(path, "r", encoding="utf-8", errors="replace") as f: with open(path, "r", encoding="utf-8", errors="replace") as f:
content = f.read()
self.code_text.delete("1.0", tk.END) self.code_text.delete("1.0", tk.END)
self.code_text.insert("1.0", content) self.code_text.insert("1.0", f.read())
self.current_file = path self.current_file = path
name = os.path.basename(path) name = os.path.basename(path)
self.notebook.tab(self.editor_frame, text=name) self.notebook.tab(self.editor_frame, text=name)
self._set_status(f"Opened: {name}") self._log(f"{self._t('opened')} {name}", "success")
self._log(f"Opened {path}")
except Exception as e: except Exception as e:
self._log(f"Error: {e}", "error") self._log(str(e), "error")
def _open_folder(self): def _open_folder(self):
path = filedialog.askdirectory(initialdir=ENGINE_DIR) filedialog.askdirectory(initialdir=ENGINE_DIR)
if path: self._log(f"Folder: {path}")
def _save_file(self): def _save_file(self):
if not self.current_file: if not self.current_file: self._save_as(); return
self._save_as()
return
try: try:
content = self.code_text.get("1.0", tk.END)
with open(self.current_file, "w", encoding="utf-8") as f: with open(self.current_file, "w", encoding="utf-8") as f:
f.write(content) f.write(self.code_text.get("1.0", tk.END))
name = os.path.basename(self.current_file) name = os.path.basename(self.current_file)
self.notebook.tab(self.editor_frame, text=name) self.notebook.tab(self.editor_frame, text=name)
self._log(f"Saved {name}", "success") self._log(f"{self._t('saved')} {name}", "success")
self._set_status(f"Saved: {name}")
except Exception as e: except Exception as e:
self._log(f"Save error: {e}", "error") self._log(str(e), "error")
def _save_as(self): def _save_as(self):
path = filedialog.asksaveasfilename( p = filedialog.asksaveasfilename(initialdir=ENGINE_DIR, defaultextension=".py", filetypes=[("Python", "*.py"), ("All", "*.*")])
initialdir=ENGINE_DIR, if p: self.current_file = p; self._save_file()
defaultextension=".py",
filetypes=[("Python", "*.py"), ("C Source", "*.c"), ("All", "*.*")]
)
if path:
self.current_file = path
self._save_file()
def _run_game(self): def _run_game(self):
exe = os.path.join(ENGINE_DIR, "build", "lity.exe") exe = os.path.join(ENGINE_DIR, "build", "lity.exe")
if os.path.exists(exe): if os.path.exists(exe): self._run_process([exe])
self._run_process([exe]) else: self._log(self._t("build_first"), "error")
else:
self._log("lity.exe not found. Build first (F7).", "error")
def _run_current(self): def _run_current(self):
if not self.current_file: if not self.current_file: self._log(self._t("no_file"), "error"); return
self._log("No file open", "error"); return
ext = os.path.splitext(self.current_file)[1] ext = os.path.splitext(self.current_file)[1]
if ext == ".py": if ext == ".py": self._run_process([sys.executable, self.current_file])
self._run_process([sys.executable, self.current_file]) else: self._log(self._t("cant_run"), "error")
elif ext in (".ly", ".lity"):
interp = os.path.join(ENGINE_DIR, "build", "ly_interpreter.exe")
if os.path.exists(interp):
self._run_process([interp, self.current_file])
else:
self._log("ly_interpreter not built", "error")
else:
self._log(f"Cannot run {ext} files", "error")
def _run_scene(self): def _run_scene(self):
scene = os.path.join(ENGINE_DIR, "scene.txt")
if not os.path.exists(scene):
self._log("No scene.txt found", "error"); return
exe = os.path.join(ENGINE_DIR, "build", "lity.exe") exe = os.path.join(ENGINE_DIR, "build", "lity.exe")
if os.path.exists(exe): if os.path.exists(exe): self._run_process([exe])
self._run_process([exe]) else: self._log(self._t("build_first"), "error")
else:
self._log("Build engine first (F7)", "error")
def _run_process(self, cmd): def _run_process(self, cmd):
self._kill_process() self._kill_process()
self._log(f"Running: {' '.join(cmd)}") self._log(f"> {' '.join(os.path.basename(c) for c in cmd)}")
self.process_label.config(text="Running...") self.process_label.config(text=self._t("running"))
try: try:
self.process = subprocess.Popen( self.process = subprocess.Popen(cmd, cwd=ENGINE_DIR, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, creationflags=subprocess.CREATE_NO_WINDOW)
cmd, cwd=ENGINE_DIR, self._poll()
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
creationflags=subprocess.CREATE_NO_WINDOW
)
self._poll_process()
except Exception as e: except Exception as e:
self._log(f"Launch error: {e}", "error") self._log(str(e), "error")
self.process_label.config(text="") self.process_label.config(text="")
def _poll_process(self): def _poll(self):
if self.process and self.process.poll() is None: if self.process and self.process.poll() is None:
line = self.process.stdout.readline() line = self.process.stdout.readline()
if line: if line:
self._log(line.decode(errors="replace").rstrip()) self._log(line.decode(errors="replace").rstrip())
self.root.after(50, self._poll_process) self.root.after(50, self._poll)
elif self.process: elif self.process:
code = self.process.returncode self._log(f"{self._t('process_exit')} {self.process.returncode}", "success" if self.process.returncode == 0 else "error")
tag = "success" if code == 0 else "error"
self._log(f"Process exited with code {code}", tag)
self.process_label.config(text="") self.process_label.config(text="")
self.process = None self.process = None
def _kill_process(self): def _kill_process(self):
if self.process: if self.process:
self.process.kill() self.process.kill()
self._log("Process killed", "error") self._log(self._t("kill_done"), "error")
self.process_label.config(text="") self.process_label.config(text="")
self.process = None self.process = None
def _build_engine(self): def _build_engine(self):
self._log("Building engine with CMake...") d = os.path.join(ENGINE_DIR, "build")
cmake_dir = os.path.join(ENGINE_DIR, "build") os.makedirs(d, exist_ok=True)
os.makedirs(cmake_dir, exist_ok=True) self._run_process(["cmake", "--build", d])
self._run_process(["cmake", "--build", cmake_dir])
def _build_exe(self): def _build_exe(self):
self._save_file() self._save_file()
exe = os.path.join(ENGINE_DIR, "build", "lity.exe") exe = os.path.join(ENGINE_DIR, "build", "lity.exe")
if os.path.exists(exe): if os.path.exists(exe):
out = filedialog.asksaveasfilename( out = filedialog.asksaveasfilename(initialdir=ENGINE_DIR, defaultextension=".exe", filetypes=[("EXE", "*.exe")])
initialdir=ENGINE_DIR,
defaultextension=".exe",
filetypes=[("Executable", "*.exe")]
)
if out: if out:
import shutil import shutil
shutil.copy2(exe, out) shutil.copy2(exe, out)
self._log(f"Packaged to {out}", "success") self._log(f"{self._t('packaged')} {out}", "success")
else: else:
self._log("Build engine first", "error") self._log(self._t("build_first"), "error")
def _new_scene(self): def _new_scene(self):
scene = {"entities": [], "camera": {"pos": [0, 5, -10], "target": [0, 0, 0]}} scene = {"entities": [], "camera": {"pos": [0, 5, -10], "target": [0, 0, 0]}}
path = filedialog.asksaveasfilename( path = filedialog.asksaveasfilename(initialdir=ENGINE_DIR, defaultextension=".json", filetypes=[("Scene", "*.json")])
initialdir=ENGINE_DIR, defaultextension=".json",
filetypes=[("Scene", "*.json")]
)
if path: if path:
with open(path, "w") as f: json.dump(scene, f, indent=2) with open(path, "w") as f: json.dump(scene, f, indent=2)
self.current_file = path self.current_file = path
self._open_file_at(path) self._open_file_at(path)
self._log(f"New scene: {os.path.basename(path)}", "success") self._log(self._t("new_scene_ok"), "success")
def _open_scene(self): def _open_scene(self):
path = filedialog.askopenfilename( p = filedialog.askopenfilename(initialdir=ENGINE_DIR, filetypes=[("Scene", "*.json *.txt")])
initialdir=ENGINE_DIR, if p: self._open_file_at(p)
filetypes=[("Scene", "*.json *.txt")]
)
if path: self._open_file_at(path)
def _save_scene(self): def _save_scene(self):
self._save_file() self._save_file()
def _open_docs(self): def _open_docs(self):
docs = os.path.join(ENGINE_DIR, "Engine", "Scripting", "lity.py") d = os.path.join(ENGINE_DIR, "Engine", "Scripting", "lity.py")
if os.path.exists(docs): if os.path.exists(d): self._open_file_at(d)
self._open_file_at(docs)
else:
self._log("LityPy docs not found", "error")
def _load_recent(self):
self._set_status("Lity3D Editor v1.0 | F5=Run | F7=Build | Ctrl+S=Save")
def _on_close(self): def _on_close(self):
if self.process: if self.process: self.process.kill()
self.process.kill()
self.root.destroy() self.root.destroy()
def run(self): def run(self):
self.root.mainloop() self.root.mainloop()
if __name__ == "__main__": if __name__ == "__main__":
editor = LityEditor() LityEditor().run()
editor.run()