diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..2790995 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: lightningpixel diff --git a/api/main.py b/api/main.py index d5b3d4a..fc11f16 100644 --- a/api/main.py +++ b/api/main.py @@ -23,7 +23,7 @@ async def lifespan(app: FastAPI): app = FastAPI( title="Modly API", - version="0.2.1", + version="0.3.0", lifespan=lifespan, ) diff --git a/api/requirements.txt b/api/requirements.txt index 6947586..a3888d5 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -1,36 +1,14 @@ -# PyTorch CUDA — index primaire pour torch/torchvision -# cu128 = CUDA 12.8 (driver ≥520, RTX 30xx/40xx/50xx) -# Changer cu128 → cu126 si le driver est plus ancien ---index-url https://download.pytorch.org/whl/cu128 ---extra-index-url https://pypi.org/simple - # Web server fastapi==0.115.6 uvicorn[standard]==0.34.0 python-multipart==0.0.20 -# AI / ML -torch>=2.1.0 -torchvision>=0.16.0 -huggingface_hub>=0.27.0 -hf_xet>=0.1.0 -diffusers>=0.32.0 -transformers>=4.48.0,<5.0.0 -accelerate>=1.3.0 -pillow>=11.0.0 +# Mesh processing (optimize + export endpoints) trimesh>=4.5.0 -numpy>=1.26.0 -rembg>=2.0.0 # background removal (SF3D + Hunyuan3D) -onnxruntime-gpu>=1.17.0 # runtime ONNX requis par rembg (GPU-accelerated) -einops>=0.7.0 # tensor ops used by Hunyuan3D / hy3dshape pipeline -pymeshlab>=2023.12 # mesh processing required by hy3dshape postprocessors -xatlas>=0.0.8 # UV unwrapping for Hunyuan3D texture generation -pygltflib>=1.15.0 # GLB/GLTF I/O for Hunyuan3D texture pipeline +pymeshlab>=2023.12 -opencv-python-headless>=4.8.0 # required by hy3dgen.shapegen.preprocessors (cv2) -peft>=0.10.0 # LoRA/adapter support required by TripoSG transformer -scikit-image>=0.21.0 # image processing utilities required by TripoSG +# Model downloads (HuggingFace) +huggingface_hub>=0.27.0 +hf_xet>=0.1.0 -# Utils -aiofiles==24.1.0 cryptography>=42.0.0 diff --git a/api/routers/extensions.py b/api/routers/extensions.py index 67758a2..2313d3b 100644 --- a/api/routers/extensions.py +++ b/api/routers/extensions.py @@ -1,4 +1,7 @@ -from fastapi import APIRouter +import asyncio +import subprocess +import sys +from fastapi import APIRouter, HTTPException router = APIRouter(tags=["extensions"]) @@ -18,8 +21,65 @@ async def reload_extensions(): } +@router.post("/setup/{ext_id}") +async def setup_extension(ext_id: str): + """ + Creates the isolated venv for an extension by running its setup.py. + Called automatically after installing an extension from GitHub. + Runs setup.py with Modly's embedded Python and the detected GPU SM. + """ + from services.generator_registry import EXTENSIONS_DIR + + if EXTENSIONS_DIR is None or not EXTENSIONS_DIR.exists(): + raise HTTPException(400, "EXTENSIONS_DIR not configured") + + ext_dir = EXTENSIONS_DIR / ext_id + setup_py = ext_dir / "setup.py" + + if not ext_dir.exists(): + raise HTTPException(404, f"Extension '{ext_id}' not found in {EXTENSIONS_DIR}") + if not setup_py.exists(): + # No setup.py → legacy extension, nothing to do + return {"status": "skipped", "reason": "no setup.py"} + + # Detect GPU compute capability + gpu_sm = _detect_gpu_sm() + + # Run setup.py using Modly's embedded Python (sys.executable) + loop = asyncio.get_running_loop() + result = await loop.run_in_executor( + None, + lambda: subprocess.run( + [sys.executable, str(setup_py), sys.executable, str(ext_dir), str(gpu_sm)], + capture_output=True, + text=True, + ) + ) + + if result.returncode != 0: + raise HTTPException(500, f"setup.py failed:\n{result.stderr}") + + return { + "status": "ok", + "gpu_sm": gpu_sm, + "output": result.stdout, + } + + @router.get("/errors") async def extension_errors(): """Returns extension loading errors (invalid manifest, failed import, etc.).""" from services.generator_registry import generator_registry return generator_registry.load_errors() + + +def _detect_gpu_sm() -> int: + """Returns GPU compute capability as integer (e.g. 86 for SM 8.6), or 0 if no GPU.""" + try: + import torch + if torch.cuda.is_available(): + major, minor = torch.cuda.get_device_capability(0) + return major * 10 + minor + except Exception: + pass + return 0 diff --git a/api/routers/generation.py b/api/routers/generation.py index 77b9420..2c4eb18 100644 --- a/api/routers/generation.py +++ b/api/routers/generation.py @@ -91,6 +91,17 @@ async def cancel_job(job_id: str): _cancel_events[job_id].set() if job.status in ("pending", "running"): job.status = "cancelled" + # Kill the active generator subprocess immediately so inference stops now. + # _run_generation will catch the resulting exception, see job_id in _cancelled, + # and return cleanly without setting an error status. + try: + gen = generator_registry._generators.get(generator_registry._active_id) + if gen is not None and hasattr(gen, "_proc") and gen._proc and gen._proc.poll() is None: + gen._proc.kill() + gen._loaded = False + gen._proc = None + except Exception: + pass return {"cancelled": True} @@ -149,9 +160,13 @@ def progress_cb(pct: int, step: str = "") -> None: if job_id in _cancelled: return - job.status = "done" - job.progress = 100 - job.output_url = f"/workspace/{collection}/{output_path.name}" + job.status = "done" + job.progress = 100 + try: + rel = output_path.relative_to(WORKSPACE_DIR) + job.output_url = f"/workspace/{rel.as_posix()}" + except ValueError: + job.output_url = f"/workspace/{collection}/{output_path.name}" except GenerationCancelled: job.status = "cancelled" diff --git a/api/routers/optimize.py b/api/routers/optimize.py index 3adb616..db7152c 100644 --- a/api/routers/optimize.py +++ b/api/routers/optimize.py @@ -2,11 +2,21 @@ import re import shutil import tempfile +import uuid + +try: + import pymeshlab as _pymeshlab + _PYMESHLAB_AVAILABLE = True +except ImportError: + _pymeshlab = None + _PYMESHLAB_AVAILABLE = False -import pymeshlab import trimesh import trimesh.visual -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException, UploadFile, File +from fastapi.responses import FileResponse, Response +from pathlib import Path +from urllib.parse import quote from pydantic import BaseModel from services.generator_registry import WORKSPACE_DIR @@ -19,8 +29,19 @@ class OptimizeRequest(BaseModel): target_faces: int +class SmoothRequest(BaseModel): + path: str # format: "{collection}/{filename}" + iterations: int + + +def _require_pymeshlab(): + if not _PYMESHLAB_AVAILABLE: + raise HTTPException(503, "pymeshlab is unavailable on this system (DLL blocked by Windows Application Control policy)") + + @router.post("/mesh") def optimize_mesh(body: OptimizeRequest): + _require_pymeshlab() target_faces = max(100, min(500_000, body.target_faces)) # Security: prevent path traversal @@ -63,7 +84,7 @@ def _decimate(input_path: str, target_faces: int, tmp_dir: str) -> trimesh.Trime else: geom = loaded - ms = pymeshlab.MeshSet() + ms = _pymeshlab.MeshSet() if _has_texture(geom): # ── Textured path: OBJ intermediate to preserve UV coordinates ────── @@ -118,3 +139,141 @@ def _decimate(input_path: str, target_faces: int, tmp_dir: str) -> trimesh.Trime ) ms.save_current_mesh(ply_out) return trimesh.load(ply_out, force="mesh") + + +@router.post("/smooth") +def smooth_mesh(body: SmoothRequest): + _require_pymeshlab() + iterations = max(1, min(20, body.iterations)) + + input_path = (WORKSPACE_DIR / body.path).resolve() + if not str(input_path).startswith(str(WORKSPACE_DIR.resolve())): + raise HTTPException(400, "Invalid path") + if not input_path.exists(): + raise HTTPException(404, f"File not found: {body.path}") + + tmp_dir = tempfile.mkdtemp() + try: + result = _smooth(str(input_path), iterations, tmp_dir) + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) + + stem = input_path.stem + output_name = f"{stem}_smooth{iterations}.glb" + output_path = input_path.parent / output_name + result.export(str(output_path)) + + collection_name = body.path.split("/")[0] + return {"url": f"/workspace/{collection_name}/{output_name}"} + + +def _smooth(input_path: str, iterations: int, tmp_dir: str) -> trimesh.Trimesh: + loaded = trimesh.load(input_path) + if isinstance(loaded, trimesh.Scene): + geoms = list(loaded.geometry.values()) + geom = trimesh.util.concatenate(geoms) if len(geoms) > 1 else geoms[0] + else: + geom = loaded + + ms = _pymeshlab.MeshSet() + + if _has_texture(geom): + obj_in = os.path.join(tmp_dir, "input.obj") + mtl_in = os.path.join(tmp_dir, "input.mtl") + tex_in = os.path.join(tmp_dir, "texture.png") + obj_out = os.path.join(tmp_dir, "output.obj") + + geom.visual.material.image.save(tex_in) + geom.export(obj_in) + + if os.path.exists(mtl_in): + mtl = open(mtl_in).read() + mtl = re.sub(r"map_Kd\s+\S+", "map_Kd texture.png", mtl) + open(mtl_in, "w").write(mtl) + + ms.load_new_mesh(obj_in) + ms.apply_coord_laplacian_smoothing(stepsmoothnum=iterations) + ms.save_current_mesh(obj_out) + + mtl_out = obj_out.replace(".obj", ".mtl") + if os.path.exists(mtl_out): + mtl = open(mtl_out).read() + mtl = re.sub(r"map_Kd\s+\S+", "map_Kd texture.png", mtl) + open(mtl_out, "w").write(mtl) + + return trimesh.load(obj_out) + + else: + ply_in = os.path.join(tmp_dir, "input.ply") + ply_out = os.path.join(tmp_dir, "output.ply") + + geom.export(ply_in) + ms.load_new_mesh(ply_in) + ms.apply_coord_laplacian_smoothing(stepsmoothnum=iterations) + ms.save_current_mesh(ply_out) + return trimesh.load(ply_out, force="mesh") + + +class ImportByPathRequest(BaseModel): + path: str # absolute path on disk + + +@router.post("/import-by-path") +async def import_mesh_by_path(body: ImportByPathRequest): + file_path = Path(body.path) + if not file_path.is_file(): + raise HTTPException(400, "File not found") + + ext = file_path.suffix.lstrip(".").lower() + if ext not in ("glb", "obj", "stl", "ply"): + raise HTTPException(400, f"Unsupported format: {ext}") + + if ext == "glb": + # Serve the original file directly — no copy + return {"url": f"/optimize/serve-file?path={quote(str(file_path))}"} + + # Non-GLB: convert to GLB in a temp directory (not the workspace) + tmp_dir = tempfile.mkdtemp(prefix="modly_import_") + output_path = os.path.join(tmp_dir, "mesh.glb") + loaded = trimesh.load(str(file_path)) + loaded.export(output_path) + return {"url": f"/optimize/serve-file?path={quote(output_path)}"} + + +@router.get("/serve-file") +def serve_file(path: str): + file_path = Path(path) + if not file_path.is_file(): + raise HTTPException(404, "File not found") + if file_path.suffix.lower() != ".glb": + raise HTTPException(400, "Only GLB files can be served") + return FileResponse(str(file_path), media_type="model/gltf-binary") + + +@router.get("/export") +def export_mesh(path: str, format: str): + if format not in ("obj", "stl", "ply"): + raise HTTPException(400, "Supported formats: obj, stl, ply") + + input_path = (WORKSPACE_DIR / path).resolve() + if not str(input_path).startswith(str(WORKSPACE_DIR.resolve())): + raise HTTPException(400, "Invalid path") + if not input_path.exists(): + raise HTTPException(404, f"File not found: {path}") + + loaded = trimesh.load(str(input_path)) + if isinstance(loaded, trimesh.Scene): + geoms = list(loaded.geometry.values()) + mesh = trimesh.util.concatenate(geoms) if len(geoms) > 1 else geoms[0] + else: + mesh = loaded + + data = mesh.export(file_type=format) + stem = input_path.stem + mime = "text/plain" if format == "obj" else "application/octet-stream" + # trimesh exports ply as bytes even in text mode — octet-stream is fine for all binary formats + return Response( + content=data, + media_type=mime, + headers={"Content-Disposition": f'attachment; filename="{stem}.{format}"'}, + ) diff --git a/api/runner.py b/api/runner.py new file mode 100644 index 0000000..43a040c --- /dev/null +++ b/api/runner.py @@ -0,0 +1,188 @@ +""" +Modly Extension Runner — generic subprocess entry point. + +Runs inside the extension's own venv. Loaded by ExtensionProcess via: + {venv_python} {runner_path} + +Environment variables (set by ExtensionProcess): + EXTENSION_DIR — absolute path to the extension directory + MODELS_DIR — where model weights are stored + WORKSPACE_DIR — where generated files are saved + MODLY_API_DIR — path to Modly's api/ dir (so generator.py can import + from services.generators.base) + +Protocol: newline-delimited JSON on stdin/stdout. +Stderr is captured separately by ExtensionProcess for logging. +""" +import sys +import json +import os +import traceback +import base64 +import threading +import importlib.util +from pathlib import Path + +# ------------------------------------------------------------------ # +# Env +# ------------------------------------------------------------------ # + +EXT_DIR = Path(os.environ["EXTENSION_DIR"]) +MODELS_DIR = Path(os.environ.get("MODELS_DIR", Path.home() / ".modly" / "models")) +WORKSPACE_DIR = Path(os.environ.get("WORKSPACE_DIR", Path.home() / ".modly" / "workspace")) +MODLY_API_DIR = os.environ.get("MODLY_API_DIR", "") +# MODEL_DIR is set by ExtensionProcess to match its own model_dir (composite node id path). +# Falls back to MODELS_DIR/manifest_id for standalone/legacy use. +_MODEL_DIR_OVERRIDE = os.environ.get("MODEL_DIR", "") + +# Inject Modly's api/ so generator.py can do: +# from services.generators.base import BaseGenerator, ... +if MODLY_API_DIR and MODLY_API_DIR not in sys.path: + sys.path.insert(0, MODLY_API_DIR) + +# Inject ext dir so generator.py can import local vendor modules +if str(EXT_DIR) not in sys.path: + sys.path.insert(0, str(EXT_DIR)) + + +# ------------------------------------------------------------------ # +# Protocol helpers +# ------------------------------------------------------------------ # + +def send(msg: dict) -> None: + sys.stdout.write(json.dumps(msg) + "\n") + sys.stdout.flush() + + +def recv(): + """Yields parsed JSON messages from stdin, one per line.""" + for raw in sys.stdin: + raw = raw.strip() + if raw: + try: + yield json.loads(raw) + except json.JSONDecodeError as exc: + send({"type": "log", "level": "error", + "message": f"Runner: invalid JSON on stdin: {exc}"}) + + +# ------------------------------------------------------------------ # +# Generator loader +# ------------------------------------------------------------------ # + +def load_generator(manifest: dict): + """Dynamically load the generator class from generator.py.""" + spec = importlib.util.spec_from_file_location( + "generator", EXT_DIR / "generator.py" + ) + mod = importlib.util.module_from_spec(spec) + sys.modules["generator"] = mod + spec.loader.exec_module(mod) + return getattr(mod, manifest["generator_class"]) + + +# ------------------------------------------------------------------ # +# Main loop +# ------------------------------------------------------------------ # + +def main() -> None: + manifest = json.loads((EXT_DIR / "manifest.json").read_text(encoding="utf-8")) + model_id = manifest["id"] + + try: + GenClass = load_generator(manifest) + except Exception: + send({"type": "error", "id": None, + "message": "Failed to load generator class", + "traceback": traceback.format_exc()}) + return + + # Announce readiness and send params_schema so ExtensionProcess + # can serve it without needing to query the subprocess later. + # We try to get it from the generator class (may be a classmethod), + # falling back to the manifest field. + try: + schema = GenClass.params_schema() + except Exception: + node0 = (manifest.get("nodes") or [{}])[0] + schema = manifest.get("params_schema", []) or node0.get("params_schema", []) + send({"type": "ready", "params_schema": schema}) + + # Support both flat manifest (legacy) and nodes[] format. + # Node-level fields take precedence; fall back to top-level for compatibility. + node = (manifest.get("nodes") or [{}])[0] + + # Use MODEL_DIR env var (set by ExtensionProcess) when available so the + # generator uses the exact same path that is_downloaded() checks against. + # Falls back to MODELS_DIR/manifest_id for legacy / standalone use. + model_dir = Path(_MODEL_DIR_OVERRIDE) if _MODEL_DIR_OVERRIDE else MODELS_DIR / model_id + gen = GenClass(model_dir, WORKSPACE_DIR) + gen.hf_repo = manifest.get("hf_repo", "") or node.get("hf_repo", "") + gen.hf_skip_prefixes = manifest.get("hf_skip_prefixes", []) or node.get("hf_skip_prefixes", []) + gen.download_check = manifest.get("download_check", "") or node.get("download_check", "") + gen._params_schema = manifest.get("params_schema", []) or node.get("params_schema", []) + + # Active cancel events keyed by request id + _cancel: dict[str, threading.Event] = {} + + for msg in recv(): + action = msg.get("action") + rid = msg.get("id") + + try: + # ---- load ------------------------------------------------ + if action == "load": + gen.load() + send({"type": "loaded"}) + + # ---- generate -------------------------------------------- + elif action == "generate": + cancel_evt = threading.Event() + _cancel[rid] = cancel_evt + image_bytes = base64.b64decode(msg["image_b64"]) + params = msg.get("params", {}) + if msg.get("outputs_dir"): + gen.outputs_dir = Path(msg["outputs_dir"]) + gen.outputs_dir.mkdir(parents=True, exist_ok=True) + + def progress_cb(pct: int, step: str = "") -> None: + send({"type": "progress", "id": rid, "pct": pct, "step": step}) + + try: + output_path = gen.generate(image_bytes, params, progress_cb, cancel_evt) + send({"type": "done", "id": rid, "output_path": str(output_path)}) + except Exception as exc: + # Detect GenerationCancelled by name to avoid import issues + if type(exc).__name__ == "GenerationCancelled": + send({"type": "cancelled", "id": rid}) + else: + send({"type": "error", "id": rid, + "message": str(exc), + "traceback": traceback.format_exc()}) + finally: + _cancel.pop(rid, None) + + # ---- cancel ---------------------------------------------- + elif action == "cancel": + evt = _cancel.get(rid) + if evt: + evt.set() + + # ---- unload ---------------------------------------------- + elif action == "unload": + gen.unload() + send({"type": "unloaded"}) + + # ---- shutdown -------------------------------------------- + elif action == "shutdown": + gen.unload() + break + + except Exception: + send({"type": "error", "id": rid, + "message": "Unexpected runner error", + "traceback": traceback.format_exc()}) + + +if __name__ == "__main__": + main() diff --git a/api/services/extension_process.py b/api/services/extension_process.py new file mode 100644 index 0000000..47653a4 --- /dev/null +++ b/api/services/extension_process.py @@ -0,0 +1,252 @@ +""" +ExtensionProcess — manages a generator running in an isolated subprocess. + +Each extension runs in its own venv via runner.py. +Communication is done via newline-delimited JSON on stdin/stdout. + +Interface is intentionally compatible with direct BaseGenerator usage +so GeneratorRegistry can treat both transparently. +""" +import base64 +import json +import os +import platform +import queue +import subprocess +import sys +import threading +import uuid +from pathlib import Path +from typing import Callable, Optional + +_RUNNER_PATH = Path(__file__).parent.parent / "runner.py" + + +def _venv_python(ext_dir: Path) -> Path: + """Returns the path to the venv's Python executable.""" + if platform.system() == "Windows": + return ext_dir / "venv" / "Scripts" / "python.exe" + return ext_dir / "venv" / "bin" / "python" + + +class ExtensionProcess: + """ + Wraps an extension subprocess. Presents the same interface as a + direct generator (load / unload / generate / is_loaded / params_schema). + """ + + def __init__(self, ext_dir: Path, manifest: dict) -> None: + self.ext_dir = ext_dir + self.manifest = manifest + self.model_dir = None # set by registry after init + self.outputs_dir = None # set by registry after init + + self._proc: Optional[subprocess.Popen] = None + self._queue: queue.Queue = queue.Queue() + self._lock: threading.Lock = threading.Lock() + self._loaded: bool = False + + # Mirrors BaseGenerator attributes used by the registry + self.hf_repo = manifest.get("hf_repo", "") + self.hf_skip_prefixes = manifest.get("hf_skip_prefixes", []) + self.download_check = manifest.get("download_check", "") + self._params_schema = manifest.get("params_schema", []) + + # Public metadata + self.MODEL_ID = manifest.get("id", "") + self.DISPLAY_NAME = manifest.get("name", "") + self.VRAM_GB = manifest.get("vram_gb", 0) + + # ------------------------------------------------------------------ # + # Subprocess lifecycle + # ------------------------------------------------------------------ # + + def _build_env(self) -> dict: + from services.generator_registry import MODELS_DIR, WORKSPACE_DIR + env = os.environ.copy() + env["EXTENSION_DIR"] = str(self.ext_dir) + env["MODELS_DIR"] = str(MODELS_DIR) + env["WORKSPACE_DIR"] = str(WORKSPACE_DIR) + env["MODLY_API_DIR"] = str(Path(__file__).parent.parent) + # Pass the exact model_dir so runner.py doesn't have to re-derive it + # from manifest["id"] (which is the ext_id, not the composite node id). + if self.model_dir is not None: + env["MODEL_DIR"] = str(self.model_dir) + return env + + def _start(self) -> None: + """Launch the subprocess and wait for the 'ready' signal.""" + python = _venv_python(self.ext_dir) + if not python.exists(): + raise RuntimeError( + f"[{self.MODEL_ID}] venv not found at {python}. " + "Run the extension's setup.py first." + ) + + self._proc = subprocess.Popen( + [str(python), str(_RUNNER_PATH)], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, + env=self._build_env(), + ) + + # Background thread: read stdout → queue + reader = threading.Thread(target=self._read_loop, daemon=True) + reader.start() + + # Background thread: forward stderr to our stderr + stderr_fwd = threading.Thread(target=self._stderr_loop, daemon=True) + stderr_fwd.start() + + # Wait for ready — runner sends params_schema in this message + msg = self._recv(timeout=None) + if msg.get("type") != "ready": + self._proc.kill() + raise RuntimeError(f"[{self.MODEL_ID}] Expected 'ready', got: {msg}") + + # Override params_schema with what the generator class actually declares + if msg.get("params_schema"): + self._params_schema = msg["params_schema"] + + print(f"[ExtensionProcess] {self.MODEL_ID} subprocess started (pid {self._proc.pid})") + + def _read_loop(self) -> None: + """Continuously reads stdout and pushes parsed JSON to the queue.""" + try: + for line in self._proc.stdout: + line = line.strip() + if line: + try: + self._queue.put(json.loads(line)) + except json.JSONDecodeError: + print(f"[{self.MODEL_ID}] bad JSON: {line}", file=sys.stderr) + finally: + self._queue.put(None) # sentinel: process is done + + def _stderr_loop(self) -> None: + """Forwards subprocess stderr to the main process stderr.""" + for line in self._proc.stderr: + print(f"[{self.MODEL_ID}] {line}", end="", file=sys.stderr) + + def _send(self, msg: dict) -> None: + with self._lock: + self._proc.stdin.write(json.dumps(msg) + "\n") + self._proc.stdin.flush() + + def _recv(self, timeout: float | None = 120.0) -> dict: + try: + msg = self._queue.get(timeout=timeout) + except queue.Empty: + raise TimeoutError(f"[{self.MODEL_ID}] No response from subprocess after {timeout}s") + if msg is None: + raise RuntimeError(f"[{self.MODEL_ID}] Subprocess died unexpectedly") + return msg + + def _ensure_started(self) -> None: + if self._proc is None or self._proc.poll() is not None: + self._start() + + # ------------------------------------------------------------------ # + # BaseGenerator-compatible interface + # ------------------------------------------------------------------ # + + def is_downloaded(self) -> bool: + if self.download_check: + return (self.model_dir / self.download_check).exists() + return self.model_dir.exists() and any(self.model_dir.iterdir()) + + def is_loaded(self) -> bool: + return self._loaded and self._proc is not None and self._proc.poll() is None + + def load(self) -> None: + self._ensure_started() + self._send({"action": "load"}) + + msg = self._recv(timeout=None) # model load can be arbitrarily slow + if msg.get("type") == "loaded": + self._loaded = True + elif msg.get("type") == "error": + raise RuntimeError(msg.get("traceback") or msg.get("message")) + else: + raise RuntimeError(f"[{self.MODEL_ID}] Unexpected response to load: {msg}") + + def unload(self) -> None: + if self._proc and self._proc.poll() is None: + try: + self._send({"action": "unload"}) + self._recv(timeout=30.0) + except Exception: + pass + self._loaded = False + + def generate( + self, + image_bytes: bytes, + params: dict, + progress_cb: Optional[Callable[[int, str], None]] = None, + cancel_event: Optional[threading.Event] = None, + ) -> Path: + from services.generators.base import GenerationCancelled + + req_id = str(uuid.uuid4()) + self._send({ + "action": "generate", + "id": req_id, + "image_b64": base64.b64encode(image_bytes).decode(), + "params": params, + "outputs_dir": str(self.outputs_dir) if self.outputs_dir else None, + }) + + while True: + # Check for cancellation + if cancel_event and cancel_event.is_set(): + self._send({"action": "cancel", "id": req_id}) + # Drain until the subprocess acknowledges + while True: + msg = self._recv(timeout=30.0) + if msg.get("type") in ("cancelled", "done", "error"): + raise GenerationCancelled() + + # Poll queue with short timeout so we can re-check cancel_event + try: + msg = self._queue.get(timeout=0.5) + except queue.Empty: + continue + + if msg is None: + raise RuntimeError(f"[{self.MODEL_ID}] Subprocess died during generation") + + t = msg.get("type") + + if t == "progress": + if progress_cb: + progress_cb(msg.get("pct", 0), msg.get("step", "")) + + elif t == "done": + return Path(msg["output_path"]) + + elif t == "error": + raise RuntimeError(msg.get("traceback") or msg.get("message", "Unknown error")) + + elif t == "cancelled": + raise GenerationCancelled() + + elif t == "log": + print(f"[{self.MODEL_ID}] {msg.get('message', '')}", file=sys.stderr) + + def params_schema(self) -> list: + return self._params_schema + + def stop(self) -> None: + """Gracefully shut down the subprocess.""" + if self._proc and self._proc.poll() is None: + try: + self._send({"action": "shutdown"}) + self._proc.wait(timeout=15) + except Exception: + self._proc.kill() + self._loaded = False + self._proc = None diff --git a/api/services/generator_registry.py b/api/services/generator_registry.py index 01854b1..039db3b 100644 --- a/api/services/generator_registry.py +++ b/api/services/generator_registry.py @@ -7,7 +7,6 @@ - generator.py (class extending BaseGenerator) No other file needs to be modified. """ -import base64 import importlib.util import json import os @@ -16,50 +15,7 @@ from typing import Dict, Optional, Tuple from services.generators.base import BaseGenerator - -# ------------------------------------------------------------------ # -# Signature verification -# ------------------------------------------------------------------ # - -_PUBLIC_KEY_PATH = Path(__file__).parent.parent / "resources" / "public_key.pem" - - -def _verify_signature(generator_path: Path, manifest: dict) -> tuple: - """ - Verifies the signature of a generator.py file against the manifest. - - Returns (is_verified: bool, status: str) where status is one of: - "verified" — signature present and valid - "unsigned" — no signature in manifest (third-party extension) - "invalid" — signature present but verification failed (tampered file) - "error" — verification could not be performed - """ - signature_b64 = manifest.get("signature") - - if not signature_b64: - return False, "unsigned" - - if not _PUBLIC_KEY_PATH.exists(): - print(f"[Registry] WARNING: public_key.pem not found at {_PUBLIC_KEY_PATH}, skipping verification") - return False, "error" - - try: - from cryptography.exceptions import InvalidSignature - from cryptography.hazmat.primitives.serialization import load_pem_public_key - - public_key = load_pem_public_key(_PUBLIC_KEY_PATH.read_bytes()) - signature = base64.b64decode(signature_b64) - generator_content = generator_path.read_bytes().replace(b"\r\n", b"\n") - - try: - public_key.verify(signature, generator_content) - return True, "verified" - except InvalidSignature: - return False, "invalid" - - except Exception as exc: - print(f"[Registry] WARNING: Signature verification error: {exc}") - return False, "error" +from services.extension_process import ExtensionProcess, _venv_python # ------------------------------------------------------------------ # # Global paths @@ -90,7 +46,8 @@ def _discover_extensions() -> Dict[str, Tuple[type, dict]]: """ Scans EXTENSIONS_DIR to find valid extensions. Each extension must have manifest.json + generator.py. - Returns {model_id: (GeneratorClass, manifest_dict)}. + Returns {full_id: (GeneratorClass, node_manifest, ext_dir)} + where full_id is "ext_id/node_id". """ result: Dict[str, Tuple[type, dict]] = {} @@ -117,53 +74,61 @@ def _discover_extensions() -> Dict[str, Tuple[type, dict]]: ext_id = manifest["id"] class_name = manifest["generator_class"] - # Verify signature before loading - is_verified, sig_status = _verify_signature(generator_path, manifest) - manifest["_verified"] = is_verified - manifest["_sig_status"] = sig_status - - if sig_status == "invalid": - print( - f"[Registry] SECURITY: Extension '{ext_dir.name}' has an INVALID signature. " - "The generator.py may have been tampered with. Skipping." - ) - continue - elif sig_status == "unsigned": - print( - f"[Registry] WARNING: Extension '{ext_dir.name}' is unsigned " - "(unverified third-party extension). Loading with caution." - ) - elif sig_status == "verified": - print(f"[Registry] OK: Extension '{ext_dir.name}' signature verified.") - - # Dynamically load the generator.py module - module_name = f"extensions.{ext_id}.generator" - spec = importlib.util.spec_from_file_location(module_name, generator_path) - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - - cls = getattr(module, class_name) - - # Multi-variant: register one generator per variant if manifest.models[] is present - variants = [v for v in manifest.get("models", []) if v.get("id") and v.get("hf_repo")] - if variants: - for variant in variants: - variant_manifest = { + nodes = [n for n in manifest.get("nodes", []) if n.get("id")] + + # --- Subprocess mode (new): venv present → use ExtensionProcess --- + # Also force subprocess mode for extensions that ship a build_vendor.py + # but whose vendor/ directory hasn't been built yet: this surfaces a + # loadError in the UI (Repair button) so the user can run setup.py. + has_venv = _venv_python(ext_dir).exists() + has_build_vendor = (ext_dir / "build_vendor.py").exists() + vendor_built = (ext_dir / "vendor").exists() + subprocess_mode = has_venv or (has_build_vendor and not vendor_built) + + cls_or_None = None + if not subprocess_mode: + # --- Direct mode (legacy): no venv → load generator.py directly --- + module_name = f"extensions.{ext_id}.generator" + spec = importlib.util.spec_from_file_location(module_name, generator_path) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + cls_or_None = getattr(module, class_name) + + if nodes: + for node in nodes: + node_manifest = { **manifest, - "id": variant["id"], - "name": variant.get("name", variant["id"]), - "hf_repo": variant["hf_repo"], + "id": f"{ext_id}/{node['id']}", + "ext_id": ext_id, + "node_id": node["id"], + "name": node.get("name", node["id"]), + "hf_repo": node.get("hf_repo", ""), + "download_check": node.get("download_check", ""), + "hf_skip_prefixes": node.get("hf_skip_prefixes", []), + "params_schema": node.get("params_schema", []), + "input": node.get("input", "image"), + "output": node.get("output", "mesh"), } - # Per-variant fields override the top-level ones if present - for field in ("hf_skip_prefixes", "download_check"): - if field in variant: - variant_manifest[field] = variant[field] - result[variant["id"]] = (cls, variant_manifest) - print(f"[Registry] Loaded extension variant: {variant['id']} (from '{ext_id}')") + full_id = f"{ext_id}/{node['id']}" + result[full_id] = (cls_or_None, node_manifest, ext_dir) + if subprocess_mode: + if has_venv: + print(f"[Registry] Loaded subprocess node: {full_id}") + else: + print(f"[Registry] Node '{full_id}' needs setup (venv missing)") + else: + print(f"[Registry] Loaded node: {full_id} ({class_name})") else: - result[ext_id] = (cls, manifest) - print(f"[Registry] Loaded extension: {ext_id} ({class_name})") + # No nodes defined — register by ext_id as fallback + result[ext_id] = (cls_or_None, manifest, ext_dir) + if subprocess_mode: + if has_venv: + print(f"[Registry] Loaded subprocess extension: {ext_id}") + else: + print(f"[Registry] Extension '{ext_id}' needs setup (venv missing)") + else: + print(f"[Registry] Loaded extension: {ext_id} ({class_name})") except Exception as exc: print(f"[Registry] ERROR loading extension '{ext_dir.name}': {exc}") @@ -186,17 +151,30 @@ def initialize(self) -> None: """Discovers and instantiates all extensions. Call at startup.""" extensions = _discover_extensions() - for model_id, (cls, manifest) in extensions.items(): + for model_id, entry in extensions.items(): + cls, manifest, ext_dir = entry try: - gen = cls(MODELS_DIR / model_id, WORKSPACE_DIR) - # Inject manifest fields onto the generator - gen.hf_repo = manifest.get("hf_repo", "") - gen.hf_skip_prefixes = manifest.get("hf_skip_prefixes", []) - gen.download_check = manifest.get("download_check", "") - gen._params_schema = manifest.get("params_schema", []) + if cls is None: + # Subprocess mode: venv must exist + if not _venv_python(ext_dir).exists(): + raise RuntimeError( + "venv not found — extension needs setup. " + "Click 'Repair' on the Models page to run setup.py." + ) + # Subprocess mode: wrap in ExtensionProcess + gen = ExtensionProcess(ext_dir, manifest) + gen.model_dir = MODELS_DIR / model_id + gen.outputs_dir = WORKSPACE_DIR + else: + # Legacy direct mode + gen = cls(MODELS_DIR / model_id, WORKSPACE_DIR) + gen.hf_repo = manifest.get("hf_repo", "") + gen.hf_skip_prefixes = manifest.get("hf_skip_prefixes", []) + gen.download_check = manifest.get("download_check", "") + gen._params_schema = manifest.get("params_schema", []) + self._generators[model_id] = gen self._manifests[model_id] = manifest - # Clear any previous error for this extension self._errors.pop(model_id, None) except Exception as exc: msg = f"Failed to instantiate generator '{model_id}': {exc}" @@ -248,6 +226,11 @@ def get_active(self) -> BaseGenerator: gen = self._generators[self._active_id] if not gen.is_loaded(): if not gen.is_downloaded(): + if isinstance(gen, ExtensionProcess): + raise RuntimeError( + f"Model '{self._active_id}' is not downloaded. " + "Please install it from the Models page first." + ) gen._auto_download() gen.load() return gen @@ -307,8 +290,6 @@ def all_status(self) -> list: "downloaded": gen.is_downloaded(), "loaded": gen.is_loaded(), "active": model_id == self._active_id, - "verified": manifest.get("_verified", False), - "sig_status": manifest.get("_sig_status", "unsigned"), }) return result @@ -341,7 +322,10 @@ def update_paths(self, models_dir: Optional[Path], workspace_dir: Optional[Path] def unload_all(self) -> None: for gen in self._generators.values(): - gen.unload() + if isinstance(gen, ExtensionProcess): + gen.stop() + else: + gen.unload() # Singleton diff --git a/electron/main/builtin-sync.ts b/electron/main/builtin-sync.ts new file mode 100644 index 0000000..e230ab7 --- /dev/null +++ b/electron/main/builtin-sync.ts @@ -0,0 +1,38 @@ +import { join } from 'path' +import { app } from 'electron' +import { cpSync, existsSync, mkdirSync, rmSync } from 'fs' +import { logger } from './logger' + +export function getBuiltinExtensionsDir(): string { + return join(app.getPath('userData'), 'builtin-extensions') +} + +function getBuiltinResourcesDir(): string { + if (app.isPackaged) { + return join(process.resourcesPath, 'builtin-extensions') + } + // Dev: built-ins compiled to out/builtin-extensions by build-builtins.mjs + return join(__dirname, '../../out/builtin-extensions') +} + +/** + * Copies built-in extensions from app resources to userData/builtin-extensions. + * Always overwrites — ensures built-ins are always up to date with the app version. + */ +export function syncBuiltinExtensions(): void { + const resourcesDir = getBuiltinResourcesDir() + + if (!existsSync(resourcesDir)) { + logger.info('[builtin-sync] No built-in extensions resources found, skipping.') + return + } + + const destDir = getBuiltinExtensionsDir() + + // Wipe dest first so removed extensions don't linger + if (existsSync(destDir)) rmSync(destDir, { recursive: true, force: true }) + mkdirSync(destDir, { recursive: true }) + + cpSync(resourcesDir, destDir, { recursive: true }) + logger.info(`[builtin-sync] Built-in extensions synced to ${destDir}`) +} diff --git a/electron/main/index.ts b/electron/main/index.ts index 7938ffe..19b75df 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -5,6 +5,7 @@ import { setupIpcHandlers } from './ipc-handlers' import { PythonBridge } from './python-bridge' import { logger, archiveCurrentSession } from './logger' import { initAutoUpdater } from './updater' +import { syncBuiltinExtensions } from './builtin-sync' let mainWindow: BrowserWindow | null = null let pythonBridge: PythonBridge | null = null @@ -70,6 +71,9 @@ app.whenReady().then(async () => { optimizer.watchWindowShortcuts(window) }) + // Sync built-in extensions from app resources to userData + syncBuiltinExtensions() + // Start Python FastAPI backend pythonBridge = new PythonBridge() pythonBridge.setWindowGetter(() => mainWindow) diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 831dda3..6cadd7d 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -1,4 +1,5 @@ import { ipcMain, BrowserWindow, dialog, app, shell } from 'electron' +import { buildSync } from 'esbuild' import { autoUpdater } from 'electron-updater' import { join } from 'path' import { rm as rmAsync, readFile, writeFile, mkdir, readdir, rename, cp } from 'fs/promises' @@ -12,11 +13,90 @@ import { downloadModelFromHF, } from './model-downloader' import { getSettings, setSettings } from './settings-store' -import { checkSetupNeeded, markSetupDone, runFullSetup } from './python-setup' +import { checkSetupNeeded, markSetupDone, runFullSetup, getVenvPythonExe } from './python-setup' import { logger } from './logger' +import { getProcessRunner, getPythonProcessRunner, getExtPythonExe, terminateProcessRunner, terminateAllProcessRunners } from './process-runner' +import { getBuiltinExtensionsDir } from './builtin-sync' +import { spawn } from 'child_process' type WindowGetter = () => BrowserWindow | null +// ─── GPU detect (best-effort, no Python required) ───────────────────────────── + +interface GpuInfo { sm: number; cudaVersion: number } + +function detectGpuInfo(): Promise { + return new Promise((resolve) => { + // Query compute cap + driver version in one call + const proc = spawn('nvidia-smi', ['--query-gpu=compute_cap,driver_version', '--format=csv,noheader'], { + stdio: ['ignore', 'pipe', 'ignore'], + }) + let out = '' + proc.stdout?.on('data', (d: Buffer) => { out += d.toString() }) + proc.on('close', (code) => { + if (code === 0) { + const line = out.trim().split('\n')[0].trim() // e.g. "8.6, 551.61" + const parts = line.split(',').map(s => s.trim()) + const sm = Math.round(parseFloat(parts[0] ?? '') * 10) // → 86 + // Derive max supported CUDA version from driver version + // Driver ≥ 520 → CUDA 11.8, ≥ 525 → 12.0, ≥ 530 → 12.1, ≥ 535 → 12.2, + // ≥ 545 → 12.3, ≥ 550 → 12.4, ≥ 555 → 12.5, ≥ 560 → 12.6 + const driverMajor = parseInt((parts[1] ?? '').split('.')[0] ?? '0', 10) + let cudaVersion = 118 // safe minimum + if (driverMajor >= 570) cudaVersion = 128 // Blackwell (RTX 50xx, sm_120) + else if (driverMajor >= 560) cudaVersion = 126 + else if (driverMajor >= 555) cudaVersion = 125 + else if (driverMajor >= 550) cudaVersion = 124 + else if (driverMajor >= 545) cudaVersion = 123 + else if (driverMajor >= 535) cudaVersion = 122 + else if (driverMajor >= 530) cudaVersion = 121 + else if (driverMajor >= 525) cudaVersion = 120 + else if (driverMajor >= 520) cudaVersion = 118 + resolve({ sm: isNaN(sm) ? 86 : sm, cudaVersion }) + } else { + resolve({ sm: 86, cudaVersion: 118 }) + } + }) + proc.on('error', () => resolve({ sm: 86, cudaVersion: 118 })) + }) +} + +// ─── Run an extension's setup.py directly (no FastAPI needed) ───────────────── + +function runExtensionSetup( + extDir: string, + gpuSm: number, + cudaVersion: number, + onLog?: (line: string) => void, +): Promise { + return new Promise((resolve, reject) => { + const userData = app.getPath('userData') + const pythonExe = getVenvPythonExe(userData) + const setupPy = join(extDir, 'setup.py') + + const args = JSON.stringify({ python_exe: pythonExe, ext_dir: extDir, gpu_sm: gpuSm, cuda_version: cudaVersion }) + const proc = spawn(pythonExe, [setupPy, args], { + stdio: ['ignore', 'pipe', 'pipe'], + }) + + const handleLine = (line: string) => { if (line) onLog?.(line) } + + let stderr = '' + proc.stdout?.on('data', (d: Buffer) => d.toString().split('\n').forEach(handleLine)) + proc.stderr?.on('data', (d: Buffer) => { + const s = d.toString() + stderr += s + s.split('\n').forEach(handleLine) + }) + + proc.on('close', (code) => { + if (code === 0) resolve() + else reject(new Error(`setup.py exited with code ${code}\n${stderr.slice(-2000)}`)) + }) + proc.on('error', reject) + }) +} + export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGetter): void { // Logging from renderer ipcMain.on('log:error', (_event, message: string) => logger.error(`[Renderer] ${message}`)) @@ -70,6 +150,7 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe setSettings(userData, { modelsDir: join(baseDir, 'models'), workspaceDir: join(baseDir, 'workspace'), + workflowsDir: join(baseDir, 'workflows'), extensionsDir: join(baseDir, 'extensions'), dependenciesDir: join(baseDir, 'dependencies'), }) @@ -117,6 +198,19 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe return result.canceled ? null : result.filePaths[0] }) + ipcMain.handle('fs:selectMeshFile', async () => { + const win = getWindow() + if (!win) return null + + const result = await dialog.showOpenDialog(win, { + title: 'Select a 3D mesh file', + filters: [{ name: '3D Mesh', extensions: ['glb', 'obj', 'stl', 'ply'] }], + properties: ['openFile'] + }) + + return result.canceled ? null : result.filePaths[0] + }) + ipcMain.handle('fs:saveModel', async (_, defaultName: string) => { const win = getWindow() if (!win) return null @@ -134,6 +228,17 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe return result.canceled ? null : result.filePath }) + ipcMain.handle('fs:savePath', async (_, args: { filters: { name: string; extensions: string[] }[]; defaultPath?: string }) => { + const win = getWindow() + if (!win) return null + const result = await dialog.showSaveDialog(win, { + title: 'Choose output path', + filters: args.filters, + defaultPath: args.defaultPath, + }) + return result.canceled ? null : result.filePath + }) + ipcMain.handle('model:unloadAll', async (): Promise<{ success: boolean; error?: string }> => { try { await axios.post(`${API_BASE_URL}/model/unload-all`, {}, { timeout: 10_000 }) @@ -171,6 +276,14 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe return buffer.toString('base64') }) + ipcMain.handle('fs:readScreenshotDataUrl', async (_, filename: string) => { + const filePath = app.isPackaged + ? join(process.resourcesPath, 'screenshots', filename) + : join(app.getAppPath(), 'src/assets', filename) + const buffer = await readFile(filePath) + return `data:image/png;base64,${buffer.toString('base64')}` + }) + // Model management ipcMain.handle('model:listDownloaded', () => { const modelsDir = getSettings(app.getPath('userData')).modelsDir @@ -220,6 +333,9 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe } }) + // Shell + ipcMain.handle('shell:openExternal', (_, url: string) => shell.openExternal(url)) + // App info ipcMain.handle('app:info', () => ({ version: app.getVersion(), @@ -386,61 +502,91 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe type ParsedManifest = { id?: string; name?: string; displayName?: string; version?: string description?: string; author?: string | { name?: string } - hf_repo?: string; source?: string; generator_class?: string - model?: { repoId?: string; modelId?: string } - models?: { id?: string; name?: string; hf_repo?: string; description?: string; hf_skip_prefixes?: string[] }[] + source?: string; generator_class?: string + // extension type + type?: 'model' | 'process' + entry?: string + nodes?: { + id: string + name?: string + input?: 'mesh' | 'image' | 'text' + output?: 'mesh' | 'image' | 'text' + params_schema?: unknown[] + hf_repo?: string + download_check?: string + hf_skip_prefixes?: string[] + }[] } - function parseExtensionManifest(parsed: ParsedManifest, fallbackId: string, trustedRepos: Set) { - let models: { id: string; name: string; repoId: string; description?: string; hfSkipPrefixes?: string[] }[] = [] - if (parsed.models?.length) { - models = parsed.models - .filter(v => v.hf_repo && v.id) - .map(v => ({ id: v.id!, name: v.name ?? v.id!, repoId: v.hf_repo!, description: v.description, hfSkipPrefixes: v.hf_skip_prefixes })) - } else { - const repoId = parsed.model?.repoId ?? parsed.hf_repo - const modelId = parsed.model?.modelId ?? parsed.id ?? fallbackId - if (repoId) models = [{ id: modelId, name: modelId, repoId }] - } - return { + function parseExtensionManifest(parsed: ParsedManifest, fallbackId: string, trustedRepos: Set, builtin = false) { + const common = { id: parsed.id ?? fallbackId, name: parsed.displayName ?? parsed.name ?? fallbackId, version: parsed.version, description: parsed.description, author: typeof parsed.author === 'string' ? parsed.author : parsed.author?.name, - models, - trusted: isTrustedSource(parsed.source, trustedRepos), + trusted: builtin || isTrustedSource(parsed.source, trustedRepos), source: parsed.source, + builtin, + } + + const nodes = (parsed.nodes ?? []).map(n => ({ + id: n.id, + name: n.name ?? n.id, + input: n.input ?? 'image' as const, + output: n.output ?? 'mesh' as const, + paramsSchema: n.params_schema ?? [], + hfRepo: n.hf_repo, + downloadCheck: n.download_check, + hfSkipPrefixes: n.hf_skip_prefixes, + })) + + if (parsed.type === 'process') { + return { ...common, type: 'process' as const, entry: parsed.entry ?? 'processor.js', nodes } } + + return { ...common, type: 'model' as const, nodes } } - // Extensions — reads configured extensions directory + // Extensions — reads user extensions directory + built-in extensions directory ipcMain.handle('extensions:list', async () => { - const extensionsDir = getSettings(app.getPath('userData')).extensionsDir - try { - if (!existsSync(extensionsDir)) return [] - const [entries, trustedRepos] = await Promise.all([ - readdir(extensionsDir, { withFileTypes: true }), - fetchTrustedRepos(), - ]) - const dirs = entries.filter(e => e.isDirectory()) - return Promise.all(dirs.map(async (entry) => { - const base = { id: entry.name, name: entry.name, trusted: false, models: [] } - for (const manifestFile of ['manifest.json', 'package.json']) { - const p = join(extensionsDir, entry.name, manifestFile) - if (existsSync(p)) { - try { - const raw = await readFile(p, 'utf-8') - const parsed = JSON.parse(raw) as ParsedManifest - return parseExtensionManifest(parsed, entry.name, trustedRepos) - } catch { /* ignore parse errors, fall through */ } + const userData = app.getPath('userData') + const extensionsDir = getSettings(userData).extensionsDir + const builtinDir = getBuiltinExtensionsDir() + + const trustedRepos = await fetchTrustedRepos() + + async function readExtensionsFromDir(dir: string, isBuiltin: boolean) { + if (!existsSync(dir)) return [] + try { + const entries = await readdir(dir, { withFileTypes: true }) + const dirs = entries.filter(e => e.isDirectory()) + return Promise.all(dirs.map(async (entry) => { + const base = { type: 'model' as const, id: entry.name, name: entry.name, trusted: isBuiltin, builtin: isBuiltin, nodes: [] } + for (const manifestFile of ['manifest.json', 'package.json']) { + const p = join(dir, entry.name, manifestFile) + if (existsSync(p)) { + try { + const raw = await readFile(p, 'utf-8') + const parsed = JSON.parse(raw) as ParsedManifest + return parseExtensionManifest(parsed, entry.name, trustedRepos, isBuiltin) + } catch { /* ignore parse errors, fall through */ } + } } - } - return base - })) - } catch { - return [] + return base + })) + } catch { + return [] + } } + + const [userExts, builtinExts] = await Promise.all([ + readExtensionsFromDir(extensionsDir, false), + readExtensionsFromDir(builtinDir, true), + ]) + + // Built-ins come first, then user extensions + return [...builtinExts, ...userExts] }) // Install an extension from a GitHub repo URL @@ -488,17 +634,30 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe // 4. Validate manifest.json emit({ step: 'validating' }) - const manifestPath = join(extractDir, 'manifest.json') - const generatorPath = join(extractDir, 'generator.py') + const manifestPath = join(extractDir, 'manifest.json') - if (!existsSync(manifestPath)) throw new Error('manifest.json missing from repository') - if (!existsSync(generatorPath)) throw new Error('generator.py missing from repository') + if (!existsSync(manifestPath)) throw new Error('manifest.json missing from repository') const manifestRaw = await readFile(manifestPath, 'utf-8') const manifest = JSON.parse(manifestRaw) as ParsedManifest - if (!manifest.id) throw new Error('manifest.json: required field "id" missing') - if (!manifest.generator_class) throw new Error('manifest.json: required field "generator_class" missing') + if (!manifest.id) throw new Error('manifest.json: required field "id" missing') + if (!manifest.nodes?.length) throw new Error('manifest.json: required field "nodes" missing or empty') + + const isProcess = manifest.type === 'process' + const entryFile = manifest.entry ?? 'processor.js' + const isPythonProcess = isProcess && entryFile.endsWith('.py') + + if (isProcess) { + // Process extension validation + if (!existsSync(join(extractDir, entryFile))) + throw new Error(`manifest.json: entry file "${entryFile}" missing from repository`) + } else { + // Model extension validation + const generatorPath = join(extractDir, 'generator.py') + if (!existsSync(generatorPath)) throw new Error('generator.py missing from repository') + if (!manifest.generator_class) throw new Error('manifest.json: required field "generator_class" missing') + } // Override source field with the actual GitHub URL so trust is based on origin manifest.source = `https://github.com/${owner}/${repo}` @@ -514,10 +673,82 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe } await cp(extractDir, destDir, { recursive: true }) - // 6. Hot-reload Python registry - try { - await axios.post(`${API_BASE_URL}/extensions/reload`, {}, { timeout: 10_000 }) - } catch { /* Python might not be running yet */ } + // Compile TypeScript entry to JS at install time (once, no runtime overhead) + if (isProcess && entryFile.endsWith('.ts')) { + emit({ step: 'setting_up', message: 'Compiling TypeScript entry…' }) + const compiledEntry = entryFile.replace(/\.ts$/, '.js') + buildSync({ + entryPoints: [join(destDir, entryFile)], + outfile: join(destDir, compiledEntry), + bundle: true, + platform: 'node', + format: 'cjs', + external: ['electron'], + }) + manifest.entry = compiledEntry + await writeFile(join(destDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf-8') + } + + if (isPythonProcess) { + // 6a. Python process extension: run setup.py if present (same as model extensions) + if (existsSync(join(destDir, 'setup.py'))) { + emit({ step: 'setting_up', message: 'Setting up Python environment…' }) + const { sm: gpuSm, cudaVersion } = await detectGpuInfo() + try { + await runExtensionSetup(destDir, gpuSm, cudaVersion, (line) => { + logger.info(`[ext-setup] ${line}`) + emit({ step: 'setting_up', message: line }) + }) + } catch (err) { + logger.warn(`[ext-setup] setup.py failed: ${err}`) + emit({ step: 'setting_up', message: `Warning: setup failed — ${err}` }) + } + } + } else if (isProcess) { + // 6b. JS process extension: npm install if package.json present + if (existsSync(join(destDir, 'package.json'))) { + emit({ step: 'setting_up', message: 'Installing dependencies…' }) + await new Promise((resolve, reject) => { + const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm' + const child = spawn(npm, ['install', '--omit=dev', '--no-audit', '--no-fund'], { + cwd: destDir, + stdio: 'pipe', + }) + let buf = '' + const onData = (chunk: Buffer) => { + buf += chunk.toString() + const lines = buf.split('\n') + buf = lines.pop() ?? '' + for (const raw of lines) { + const line = raw.replace(/\x1b\[[0-9;]*m/g, '').trim() + if (line) emit({ step: 'setting_up', message: line }) + } + } + child.stdout?.on('data', onData) + child.stderr?.on('data', onData) + child.on('close', (code) => code === 0 ? resolve() : reject(new Error(`npm install failed (exit ${code})`))) + child.on('error', reject) + }) + } + } else { + // 6b. Model extension: run setup.py directly (no FastAPI required) + if (existsSync(join(destDir, 'setup.py'))) { + emit({ step: 'setting_up', message: 'Setting up Python environment…' }) + const { sm: gpuSm, cudaVersion } = await detectGpuInfo() + try { + await runExtensionSetup(destDir, gpuSm, cudaVersion, (line) => { + logger.info(`[ext-setup] ${line}`) + emit({ step: 'setting_up', message: line }) + }) + } catch (setupErr: any) { + throw new Error(`Extension setup failed: ${setupErr?.message ?? setupErr}`) + } + } + + try { + await axios.post(`${API_BASE_URL}/extensions/reload`, {}, { timeout: 10_000 }) + } catch { /* Python might not be running yet */ } + } emit({ step: 'done', extensionId: manifest.id }) @@ -535,13 +766,22 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe } }) - // Uninstall an extension — deletes its directory and reloads Python + // Uninstall an extension — built-ins cannot be uninstalled ipcMain.handle('extensions:uninstall', async (_, extensionId: string) => { - const extensionsDir = getSettings(app.getPath('userData')).extensionsDir + const userData = app.getPath('userData') + const builtinPath = join(getBuiltinExtensionsDir(), extensionId) + if (existsSync(builtinPath)) { + return { success: false, error: `"${extensionId}" is a built-in extension and cannot be uninstalled.` } + } + + const extensionsDir = getSettings(userData).extensionsDir const extPath = join(extensionsDir, extensionId) try { + // Terminate process runner if it's a process extension + terminateProcessRunner(extensionId) + await rmAsync(extPath, { recursive: true, force: true }) - // Hot-reload Python so it stops using the deleted extension + // Hot-reload Python so it stops using the deleted model extension try { await axios.post(`${API_BASE_URL}/extensions/reload`, {}, { timeout: 10_000 }) } catch { /* ignore if Python is not running */ } @@ -551,6 +791,24 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe } }) + // Re-run setup.py for a model extension (creates the venv if missing) + ipcMain.handle('extensions:repair', async (_, extensionId: string) => { + try { + const extDir = join(getSettings(app.getPath('userData')).extensionsDir, extensionId) + if (!existsSync(join(extDir, 'setup.py'))) { + return { success: false, error: 'No setup.py found for this extension' } + } + const { sm: gpuSm, cudaVersion } = await detectGpuInfo() + await runExtensionSetup(extDir, gpuSm, cudaVersion, (line) => logger.info(`[ext-repair] ${line}`)) + try { + await axios.post(`${API_BASE_URL}/extensions/reload`, {}, { timeout: 10_000 }) + } catch { /* ignore if Python is not running yet */ } + return { success: true } + } catch (err: any) { + return { success: false, error: `Repair failed: ${err?.message ?? err}` } + } + }) + // Trigger Python extension reload (without touching the filesystem) ipcMain.handle('extensions:reload', async () => { try { @@ -561,6 +819,45 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe } }) + // Run a process extension in an isolated worker thread + ipcMain.handle('extensions:runProcess', async (_, extensionId: string, input: { filePath?: string; text?: string; nodeId?: string }, params: Record) => { + const userData = app.getPath('userData') + const { extensionsDir, workspaceDir } = getSettings(userData) + + // Resolve extension directory: check built-ins first, then user extensions + const builtinExtDir = join(getBuiltinExtensionsDir(), extensionId) + const userExtDir = join(extensionsDir, extensionId) + const extDir = existsSync(builtinExtDir) ? builtinExtDir : userExtDir + + if (!existsSync(extDir)) return { success: false, error: `Extension "${extensionId}" not found` } + + try { + const manifestRaw = await readFile(join(extDir, 'manifest.json'), 'utf-8') + const manifest = JSON.parse(manifestRaw) as ParsedManifest + if (manifest.type !== 'process') return { success: false, error: `Extension "${extensionId}" is not a process extension` } + + const entry = manifest.entry ?? 'processor.js' + const isPythonEntry = entry.endsWith('.py') + const userData = app.getPath('userData') + + let runner + if (isPythonEntry) { + const pythonExe = getExtPythonExe(extDir) ?? getVenvPythonExe(userData) + runner = getPythonProcessRunner(extensionId, pythonExe, extDir, entry, workspaceDir, app.getPath('temp')) + } else { + runner = getProcessRunner(extensionId, extDir, entry, workspaceDir, app.getPath('temp')) + } + + const result = await runner.run(input, params) + return { success: true, result } + } catch (err) { + return { success: false, error: String(err) } + } + }) + + // Terminate all process runners on app quit + app.on('before-quit', () => terminateAllProcessRunners()) + // Auto-updater ipcMain.handle('updater:check', async () => { if (!app.isPackaged) return { success: false } @@ -592,4 +889,81 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe return { success: false, error: String(err) } } }) + + // ── Workflows ──────────────────────────────────────────────────────────── + + function workflowsDir(): string { + const dir = getSettings(app.getPath('userData')).workflowsDir + if (!existsSync(dir)) require('fs').mkdirSync(dir, { recursive: true }) + return dir + } + + ipcMain.handle('workflows:list', async () => { + const dir = workflowsDir() + const files = readdirSync(dir).filter(f => f.endsWith('.json')) + const workflows = [] + for (const file of files) { + try { + const raw = await readFile(join(dir, file), 'utf-8') + workflows.push(JSON.parse(raw)) + } catch { /* skip corrupted files */ } + } + return workflows.sort((a: { updatedAt?: string }, b: { updatedAt?: string }) => + (b.updatedAt ?? '').localeCompare(a.updatedAt ?? '') + ) + }) + + ipcMain.handle('workflows:save', async (_, workflow: { id: string; [key: string]: unknown }) => { + try { + const path = join(workflowsDir(), `${workflow.id}.json`) + await writeFile(path, JSON.stringify(workflow, null, 2), 'utf-8') + return { success: true } + } catch (err) { + return { success: false, error: String(err) } + } + }) + + ipcMain.handle('workflows:delete', async (_, id: string) => { + try { + await rmAsync(join(workflowsDir(), `${id}.json`), { force: true }) + return { success: true } + } catch (err) { + return { success: false, error: String(err) } + } + }) + + ipcMain.handle('workflows:import', async () => { + const win = getWindow() + const result = await dialog.showOpenDialog(win!, { + title: 'Import Workflow', + filters: [{ name: 'Workflow', extensions: ['json'] }], + properties: ['openFile'], + }) + if (result.canceled || result.filePaths.length === 0) return { success: false } + try { + const raw = await readFile(result.filePaths[0], 'utf-8') + const workflow = JSON.parse(raw) + if (!workflow.id || !workflow.nodes) return { success: false, error: 'Invalid workflow file' } + await writeFile(join(workflowsDir(), `${workflow.id}.json`), JSON.stringify(workflow, null, 2), 'utf-8') + return { success: true, workflow } + } catch (err) { + return { success: false, error: String(err) } + } + }) + + ipcMain.handle('workflows:export', async (_, workflow: { id: string; name?: string; [key: string]: unknown }) => { + const win = getWindow() + const result = await dialog.showSaveDialog(win!, { + title: 'Export Workflow', + defaultPath: `${workflow.name ?? workflow.id}.json`, + filters: [{ name: 'Workflow', extensions: ['json'] }], + }) + if (result.canceled || !result.filePath) return { success: false } + try { + await writeFile(result.filePath, JSON.stringify(workflow, null, 2), 'utf-8') + return { success: true } + } catch (err) { + return { success: false, error: String(err) } + } + }) } diff --git a/electron/main/process-runner.ts b/electron/main/process-runner.ts new file mode 100644 index 0000000..15e8c10 --- /dev/null +++ b/electron/main/process-runner.ts @@ -0,0 +1,304 @@ +import { Worker } from 'worker_threads' +import { spawn } from 'child_process' +import { existsSync } from 'fs' +import { join } from 'path' + +// ─── Worker code for JS process extensions ──────────────────────────────────── + +const WORKER_CODE = /* js */ ` +const { workerData, parentPort } = require('worker_threads') +const path = require('path') +const Module = require('module') + +// Resolve modules from the extension's own node_modules +const require_ext = Module.createRequire(path.join(workerData.extDir, '_')) + +let processor +try { + processor = require_ext(path.join(workerData.extDir, workerData.entry)) + if (typeof processor !== 'function') { + throw new Error('processor.js must export a function as module.exports') + } +} catch (err) { + parentPort.postMessage({ type: 'error', message: 'Failed to load processor: ' + String(err) }) + process.exit(1) +} + +parentPort.postMessage({ type: 'ready' }) + +parentPort.on('message', async (msg) => { + if (msg.action !== 'run') return + try { + const context = { + workspaceDir: workerData.workspaceDir, + tempDir: workerData.tempDir, + nodeId: msg.input?.nodeId ?? '', + log: (m) => parentPort.postMessage({ type: 'log', message: String(m) }), + progress: (pct, label) => parentPort.postMessage({ type: 'progress', percent: pct, label }), + } + const result = await processor(msg.input, msg.params, context) + parentPort.postMessage({ type: 'done', result }) + } catch (err) { + parentPort.postMessage({ type: 'error', message: String(err) }) + } +}) +` + +// ─── Types ──────────────────────────────────────────────────────────────────── + +export interface ProcessInput { + filePath?: string + text?: string + nodeId?: string +} + +export interface ProcessResult { + filePath?: string + text?: string +} + +export interface IProcessRunner { + run( + input: ProcessInput, + params: Record, + onProgress?: (percent: number, label: string) => void, + onLog?: (message: string) => void, + ): Promise + terminate(): void +} + +// ─── JS ProcessRunner (Worker thread) ──────────────────────────────────────── + +export class ProcessRunner implements IProcessRunner { + private worker: Worker | null = null + private ready: boolean = false + private extDir: string + private entry: string + private workspaceDir: string + private tempDir: string + + constructor(extDir: string, entry: string, workspaceDir: string, tempDir: string) { + this.extDir = extDir + this.entry = entry + this.workspaceDir = workspaceDir + this.tempDir = tempDir + } + + private async ensureReady(): Promise { + if (this.ready && this.worker) return + + return new Promise((resolve, reject) => { + const worker = new Worker(WORKER_CODE, { + eval: true, + workerData: { + extDir: this.extDir, + entry: this.entry, + workspaceDir: this.workspaceDir, + tempDir: this.tempDir, + }, + }) + + worker.once('message', (msg) => { + if (msg.type === 'ready') { + this.worker = worker + this.ready = true + resolve() + } else if (msg.type === 'error') { + worker.terminate() + reject(new Error(msg.message)) + } + }) + + worker.once('error', (err) => { + reject(err) + }) + }) + } + + async run( + input: ProcessInput, + params: Record, + onProgress?: (percent: number, label: string) => void, + onLog?: (message: string) => void, + ): Promise { + await this.ensureReady() + const worker = this.worker! + + return new Promise((resolve, reject) => { + const handler = (msg: { type: string; result?: ProcessResult; message?: string; percent?: number; label?: string }) => { + if (msg.type === 'progress') { + onProgress?.(msg.percent ?? 0, msg.label ?? '') + } else if (msg.type === 'log') { + onLog?.(msg.message ?? '') + } else if (msg.type === 'done') { + worker.off('message', handler) + resolve(msg.result ?? {}) + } else if (msg.type === 'error') { + worker.off('message', handler) + reject(new Error(msg.message)) + } + } + + worker.on('message', handler) + worker.postMessage({ action: 'run', input, params }) + }) + } + + terminate(): void { + this.worker?.terminate() + this.worker = null + this.ready = false + } +} + +// ─── Python ProcessRunner (subprocess, one process per run) ─────────────────── +// +// Protocol — stdin: one JSON line { input, params, workspaceDir, tempDir } +// Protocol — stdout: JSON lines { type: 'progress'|'log'|'done'|'error', ... } + +export class PythonProcessRunner implements IProcessRunner { + private pythonExe: string + private scriptPath: string + private workspaceDir: string + private tempDir: string + + constructor(pythonExe: string, extDir: string, entry: string, workspaceDir: string, tempDir: string) { + this.pythonExe = pythonExe + this.scriptPath = join(extDir, entry) + this.workspaceDir = workspaceDir + this.tempDir = tempDir + } + + async run( + input: ProcessInput, + params: Record, + onProgress?: (percent: number, label: string) => void, + onLog?: (message: string) => void, + ): Promise { + return new Promise((resolve, reject) => { + const proc = spawn(this.pythonExe, [this.scriptPath], { + stdio: ['pipe', 'pipe', 'pipe'], + }) + + // Send input as a single JSON line on stdin + proc.stdin.write(JSON.stringify({ + input, + params, + nodeId: input.nodeId ?? '', + workspaceDir: this.workspaceDir, + tempDir: this.tempDir, + }) + '\n') + proc.stdin.end() + + let stdoutBuf = '' + let resolved = false + + proc.stdout.on('data', (chunk: Buffer) => { + stdoutBuf += chunk.toString() + const lines = stdoutBuf.split('\n') + stdoutBuf = lines.pop() ?? '' + + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed) continue + try { + const msg = JSON.parse(trimmed) as { type: string; percent?: number; label?: string; message?: string; result?: ProcessResult } + if (msg.type === 'progress') { + onProgress?.(msg.percent ?? 0, msg.label ?? '') + } else if (msg.type === 'log') { + onLog?.(msg.message ?? '') + } else if (msg.type === 'done') { + resolved = true + resolve(msg.result ?? {}) + } else if (msg.type === 'error') { + resolved = true + reject(new Error(msg.message ?? 'Unknown error')) + } + } catch { + // Non-JSON stdout line — treat as a log message + onLog?.(trimmed) + } + } + }) + + let stderrBuf = '' + proc.stderr.on('data', (chunk: Buffer) => { + stderrBuf += chunk.toString() + }) + + proc.on('close', (code) => { + if (!resolved) { + if (code === 0) { + resolve({}) + } else { + reject(new Error(stderrBuf.trim() || `Python process exited with code ${code}`)) + } + } + }) + + proc.on('error', (err) => { + if (!resolved) { + resolved = true + reject(err) + } + }) + }) + } + + // Python processes are spawned per run — nothing persistent to terminate + terminate(): void {} +} + +// ─── Helper: find Python executable for an extension ───────────────────────── + +export function getExtPythonExe(extDir: string): string | null { + const candidates = process.platform === 'win32' + ? [join(extDir, 'venv', 'Scripts', 'python.exe')] + : [join(extDir, 'venv', 'bin', 'python'), join(extDir, 'venv', 'bin', 'python3')] + + for (const p of candidates) { + if (existsSync(p)) return p + } + return null +} + +// ─── Registry (one runner per extension id, reused across calls) ────────────── + +const registry = new Map() + +export function getProcessRunner( + extensionId: string, + extDir: string, + entry: string, + workspaceDir: string, + tempDir: string, +): ProcessRunner { + if (!registry.has(extensionId)) { + registry.set(extensionId, new ProcessRunner(extDir, entry, workspaceDir, tempDir)) + } + return registry.get(extensionId)! as ProcessRunner +} + +export function getPythonProcessRunner( + extensionId: string, + pythonExe: string, + extDir: string, + entry: string, + workspaceDir: string, + tempDir: string, +): PythonProcessRunner { + if (!registry.has(extensionId)) { + registry.set(extensionId, new PythonProcessRunner(pythonExe, extDir, entry, workspaceDir, tempDir)) + } + return registry.get(extensionId)! as PythonProcessRunner +} + +export function terminateProcessRunner(extensionId: string): void { + registry.get(extensionId)?.terminate() + registry.delete(extensionId) +} + +export function terminateAllProcessRunners(): void { + for (const runner of registry.values()) runner.terminate() + registry.clear() +} diff --git a/electron/main/settings-store.ts b/electron/main/settings-store.ts index 177eea1..6d421b0 100644 --- a/electron/main/settings-store.ts +++ b/electron/main/settings-store.ts @@ -4,6 +4,7 @@ import { readFileSync, writeFileSync, existsSync } from 'fs' export interface AppSettings { modelsDir: string workspaceDir: string + workflowsDir: string extensionsDir: string dependenciesDir: string } @@ -16,6 +17,7 @@ export function getSettings(userData: string): AppSettings { const defaults: AppSettings = { modelsDir: join(userData, 'models'), workspaceDir: join(userData, 'workspace'), + workflowsDir: join(userData, 'workflows'), extensionsDir: join(userData, 'extensions'), dependenciesDir: join(userData, 'dependencies'), } diff --git a/electron/main/updater.ts b/electron/main/updater.ts index 8c7ad50..b0326e5 100644 --- a/electron/main/updater.ts +++ b/electron/main/updater.ts @@ -1,31 +1,10 @@ import { app, BrowserWindow } from 'electron' import { autoUpdater } from 'electron-updater' -import { existsSync, rmSync, writeFileSync, readFileSync } from 'fs' -import { join } from 'path' import { logger } from './logger' type WindowGetter = () => BrowserWindow | null -function pendingFilePath(): string { - return join(app.getPath('userData'), '.last-update-installer') -} - -function cleanupLastInstaller(): void { - const marker = pendingFilePath() - if (!existsSync(marker)) return - try { - const installerPath = readFileSync(marker, 'utf-8').trim() - if (existsSync(installerPath)) { - rmSync(installerPath) - logger.info(`[updater] Cleaned up installer: ${installerPath}`) - } - rmSync(marker) - } catch {} -} - export function initAutoUpdater(getWindow: WindowGetter): void { - cleanupLastInstaller() - // eslint-disable-next-line @typescript-eslint/no-explicit-any autoUpdater.logger = logger as any autoUpdater.autoDownload = false @@ -33,14 +12,14 @@ export function initAutoUpdater(getWindow: WindowGetter): void { autoUpdater.disableWebInstaller = true autoUpdater.on('update-available', (info) => { - const running = app.getVersion() + const running = app.getVersion() const incoming = info.version const [rMaj, rMin] = running.split('.').map(Number) const [iMaj, iMin] = incoming.split('.').map(Number) const isPatch = rMaj === iMaj && rMin === iMin if (isPatch) { - logger.info(`[updater] Patch update ${incoming} available — downloading silently`) + logger.info(`[updater] Patch ${incoming} available — downloading`) autoUpdater.downloadUpdate().catch((err: Error) => { logger.error(`[updater] Download failed: ${err.message}`) }) @@ -51,11 +30,12 @@ export function initAutoUpdater(getWindow: WindowGetter): void { }) autoUpdater.on('update-downloaded', (info) => { - logger.info(`[updater] Patch update ${info.version} downloaded — showing badge`) - try { - writeFileSync(pendingFilePath(), info.downloadedFile, 'utf-8') - } catch {} - getWindow()?.webContents.send('updater:patch-ready', { version: info.version }) + logger.info(`[updater] Patch ${info.version} downloaded — applying now`) + getWindow()?.webContents.send('updater:applying', { version: info.version }) + // Small delay so the renderer can render the "Applying…" panel before quit + setTimeout(() => { + autoUpdater.quitAndInstall(true, true) + }, 800) }) autoUpdater.on('update-not-available', () => { @@ -65,4 +45,14 @@ export function initAutoUpdater(getWindow: WindowGetter): void { autoUpdater.on('error', (err: Error) => { logger.error(`[updater] Error: ${err.message}`) }) + + // Check immediately on startup so it can apply during the setup screen + autoUpdater.checkForUpdates().catch((err: Error) => { + logger.error(`[updater] Initial check failed: ${err.message}`) + }) + + // Re-check every 2 hours for long-running sessions + setInterval(() => { + autoUpdater.checkForUpdates().catch(() => {}) + }, 2 * 60 * 60 * 1000) } diff --git a/electron/preload/index.ts b/electron/preload/index.ts index ba80015..a360697 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -9,6 +9,11 @@ contextBridge.exposeInMainWorld('electron', { close: () => ipcRenderer.send('window:close') }, + // Shell utilities + shell: { + openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url), + }, + // Python / FastAPI bridge python: { start: (): Promise<{ success: boolean; port?: number; error?: string }> => @@ -29,25 +34,31 @@ contextBridge.exposeInMainWorld('electron', { fs: { selectImage: (): Promise => ipcRenderer.invoke('fs:selectImage'), + selectMeshFile: (): Promise => + ipcRenderer.invoke('fs:selectMeshFile'), saveModel: (defaultName: string): Promise => ipcRenderer.invoke('fs:saveModel', defaultName), readFileBase64: (filePath: string): Promise => ipcRenderer.invoke('fs:readFileBase64', filePath), selectDirectory: (): Promise => ipcRenderer.invoke('fs:selectDirectory'), + savePath: (args: { filters: { name: string; extensions: string[] }[]; defaultPath?: string }): Promise => + ipcRenderer.invoke('fs:savePath', args), listDir: (dirPath: string): Promise => ipcRenderer.invoke('fs:listDir', dirPath), moveDirectory: (args: { src: string; dest: string }): Promise<{ success: boolean; error?: string }> => ipcRenderer.invoke('fs:moveDirectory', args), deleteDirectory: (dirPath: string): Promise<{ success: boolean; error?: string }> => ipcRenderer.invoke('fs:deleteDirectory', dirPath), + readScreenshotDataUrl: (filename: string): Promise => + ipcRenderer.invoke('fs:readScreenshotDataUrl', filename), }, // Settings settings: { - get: (): Promise<{ modelsDir: string; workspaceDir: string; extensionsDir: string }> => + get: (): Promise<{ modelsDir: string; workspaceDir: string; workflowsDir: string; extensionsDir: string }> => ipcRenderer.invoke('settings:get'), - set: (patch: { modelsDir?: string; workspaceDir?: string; extensionsDir?: string }): Promise<{ modelsDir: string; workspaceDir: string; extensionsDir: string }> => + set: (patch: { modelsDir?: string; workspaceDir?: string; workflowsDir?: string; extensionsDir?: string }): Promise<{ modelsDir: string; workspaceDir: string; workflowsDir: string; extensionsDir: string }> => ipcRenderer.invoke('settings:set', patch), }, @@ -116,31 +127,33 @@ contextBridge.exposeInMainWorld('electron', { // Extensions extensions: { - list: (): Promise> => ipcRenderer.invoke('extensions:list'), + list: (): Promise => + ipcRenderer.invoke('extensions:list'), installFromGitHub: (url: string): Promise<{ success: boolean; error?: string extensionId?: string - extension?: { - id: string; name: string; version?: string; description?: string - author?: string; trusted: boolean - models: { id: string; name: string; repoId: string; description?: string }[] - } + extension?: unknown }> => ipcRenderer.invoke('extensions:installFromGitHub', url), uninstall: (extensionId: string): Promise<{ success: boolean; error?: string }> => ipcRenderer.invoke('extensions:uninstall', extensionId), + repair: (extensionId: string): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('extensions:repair', extensionId), + reload: (): Promise<{ success: boolean; error?: string }> => ipcRenderer.invoke('extensions:reload'), + runProcess: ( + extensionId: string, + input: { filePath?: string; text?: string; nodeId?: string }, + params: Record, + ): Promise<{ success: boolean; result?: { filePath?: string; text?: string }; error?: string }> => + ipcRenderer.invoke('extensions:runProcess', extensionId, input, params), + onInstallProgress: (cb: (data: { - step: 'downloading' | 'extracting' | 'validating' | 'done' | 'error' + step: 'downloading' | 'extracting' | 'validating' | 'setting_up' | 'done' | 'error' percent?: number extensionId?: string message?: string @@ -150,16 +163,25 @@ contextBridge.exposeInMainWorld('electron', { offInstallProgress: () => ipcRenderer.removeAllListeners('extensions:installProgress'), }, + // Workflows + workflows: { + list: (): Promise => ipcRenderer.invoke('workflows:list'), + save: (workflow: { id: string; [key: string]: unknown }): Promise<{ success: boolean; error?: string }> => ipcRenderer.invoke('workflows:save', workflow), + delete: (id: string): Promise<{ success: boolean; error?: string }> => ipcRenderer.invoke('workflows:delete', id), + import: (): Promise<{ success: boolean; error?: string; workflow?: unknown }> => ipcRenderer.invoke('workflows:import'), + export: (workflow: { id: string; name?: string; [key: string]: unknown }): Promise<{ success: boolean; error?: string }> => ipcRenderer.invoke('workflows:export', workflow), + }, + // Auto-updater updater: { check: (): Promise<{ success: boolean }> => ipcRenderer.invoke('updater:check'), quitAndInstall: (): Promise => ipcRenderer.invoke('updater:quitAndInstall'), - onPatchReady: (cb: (data: { version: string }) => void) => { - ipcRenderer.on('updater:patch-ready', (_event, data) => cb(data)) + onApplying: (cb: (data: { version: string }) => void) => { + ipcRenderer.on('updater:applying', (_event, data) => cb(data)) }, - offPatchReady: () => ipcRenderer.removeAllListeners('updater:patch-ready'), + offApplying: () => ipcRenderer.removeAllListeners('updater:applying'), onMajorMinorAvailable: (cb: (data: { version: string }) => void) => { ipcRenderer.on('updater:major-minor-available', (_event, data) => cb(data)) }, diff --git a/package-lock.json b/package-lock.json index da58bda..28cca19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,26 @@ { "name": "modly", - "version": "0.1.4", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "modly", - "version": "0.1.4", + "version": "0.3.0", "dependencies": { "@electron-toolkit/utils": "^4.0.0", "@react-three/drei": "^9.120.0", "@react-three/fiber": "^8.17.10", + "@react-three/postprocessing": "^2.19.1", + "@xyflow/react": "^12.10.2", "axios": "^1.7.9", "electron-updater": "^6.8.3", + "esbuild": "^0.28.0", "react": "^18.3.1", "react-dom": "^18.3.1", "tar": "^7.5.9", "three": "^0.171.0", + "three-mesh-bvh": "^0.9.9", "zustand": "^5.0.3" }, "devDependencies": { @@ -641,371 +645,419 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1666,6 +1718,16 @@ "yarn": ">=1" } }, + "node_modules/@react-three/drei/node_modules/three-mesh-bvh": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.8.tgz", + "integrity": "sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==", + "deprecated": "Deprecated due to three.js version incompatibility. Please use v0.8.0, instead.", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.151.0" + } + }, "node_modules/@react-three/fiber": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz", @@ -1730,6 +1792,34 @@ } } }, + "node_modules/@react-three/postprocessing": { + "version": "2.19.1", + "resolved": "https://registry.npmjs.org/@react-three/postprocessing/-/postprocessing-2.19.1.tgz", + "integrity": "sha512-7P25LOSToH/I6b3UipNK17IIFlX4FDUmWcaomfwu82+CzhXTOz8Fcc1ZXEZ7vFA/5Fr/2peNlXgXZJvoa+aCdA==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "maath": "^0.6.0", + "n8ao": "^1.6.6", + "postprocessing": "^6.32.1", + "three-stdlib": "^2.23.4" + }, + "peerDependencies": { + "@react-three/fiber": "^8.0", + "react": "^18.0", + "three": ">= 0.138.0" + } + }, + "node_modules/@react-three/postprocessing/node_modules/maath": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.6.0.tgz", + "integrity": "sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.144.0", + "three": ">=0.144.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -2150,6 +2240,55 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -2372,20 +2511,80 @@ "node": ">=10.0.0" } }, - "node_modules/7zip-bin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "bin": { - "acorn": "bin/acorn" + "node_modules/@xyflow/react": { + "version": "12.10.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz", + "integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.76", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/react/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.76", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz", + "integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" }, "engines": { "node": ">=0.4.0" @@ -3281,6 +3480,12 @@ "node": ">=8" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/cli-truncate": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", @@ -3583,6 +3788,111 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4181,127 +4491,560 @@ } } }, - "node_modules/electron/node_modules/@types/node": { - "version": "20.19.33", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", - "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/electron-vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=6" + "node": ">=12" } }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "node_modules/electron-vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/electron-vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": { - "es-errors": "^1.3.0" - }, + "node_modules/electron-vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, + "node_modules/electron-vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "optional": true - }, - "node_modules/esbuild": { + "node_modules/electron-vite/node_modules/@esbuild/darwin-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], "dev": true, - "hasInstallScript": true, - "bin": { + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/escalade": { @@ -5949,6 +6692,16 @@ "thenify-all": "^1.0.0" } }, + "node_modules/n8ao": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.10.1.tgz", + "integrity": "sha512-hhI1pC+BfOZBV1KMwynBrVlIm8wqLxj/abAWhF2nZ0qQKyzTSQa1QtLVS2veRiuoBQXojxobcnp0oe+PUoxf/w==", + "license": "ISC", + "peerDependencies": { + "postprocessing": ">=6.30.0", + "three": ">=0.137" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -6384,6 +7137,15 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/postprocessing": { + "version": "6.39.0", + "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.0.tgz", + "integrity": "sha512-/G6JY8hs426lcto/pBZlnFSkyEo1fHsh4gy7FPJtq1SaSUOzJgDW6f6f1K/+aMOYzK/eQEefyOb3++jPPIUeDA==", + "license": "Zlib", + "peerDependencies": { + "three": ">= 0.168.0 < 0.184.0" + } + }, "node_modules/potpack": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", @@ -6841,939 +7603,1369 @@ "license": "MIT", "peer": true }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==" }, - "node_modules/sanitize-filename": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", - "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, - "license": "WTFPL OR ISC", + "license": "MIT", + "peer": true, "dependencies": { - "truncate-utf8-bytes": "^1.0.0" + "safe-buffer": "~5.2.0" } }, - "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "license": "BlueOak-1.0.0", + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, "engines": { - "node": ">=11.0.0" + "node": ">=8" } }, - "node_modules/scheduler": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", - "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "optional": true - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "optional": true, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", "dependencies": { - "type-fest": "^0.13.1" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=10" + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, "dependencies": { - "shebang-regex": "^3.0.0" + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, - "license": "ISC", "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "dependencies": { - "semver": "^7.5.3" + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" }, "engines": { - "node": ">=10" + "node": ">=14.0.0" } }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "node_modules/tar": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", + "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", - "optional": true, + "peer": true, "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/tar/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" + "node": ">=18" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/tar/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "engines": { - "node": ">=0.10.0" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, + "node_modules/tar/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dependencies": { + "minipass": "^7.1.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 18" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", "dev": true, "license": "MIT", "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "optional": true - }, - "node_modules/stat-mode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/stats-gl": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", - "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", "dependencies": { - "@types/three": "*", - "three": "^0.170.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, - "peerDependencies": { - "@types/three": "*", - "three": "*" + "engines": { + "node": ">=12" } }, - "node_modules/stats-gl/node_modules/three": { - "version": "0.170.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", - "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==" - }, - "node_modules/stats.js": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", - "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "safe-buffer": "~5.2.0" + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, "engines": { - "node": ">=8" + "node": ">= 10.0.0" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, - "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" + "any-promise": "^1.0.0" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "dev": true, - "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "thenify": ">= 3.1.0 < 4" }, "engines": { - "node": ">=8" + "node": ">=0.8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "node_modules/three": { + "version": "0.171.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.171.0.tgz", + "integrity": "sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ==" + }, + "node_modules/three-mesh-bvh": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.9.9.tgz", + "integrity": "sha512-FJKitcjvbALmeQRK+Sc+nLGorCpkrZBrbgJZFzhdyWboak37DZikn46hvQkNqSbJPm227ahYmS6k3N/GXaAyXw==", "license": "MIT", + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.1", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", + "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", "dependencies": { - "ansi-regex": "^5.0.1" + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "three": ">=0.128.0" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==" + }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, "engines": { - "node": ">=8" + "node": ">=12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" + "engines": { + "node": ">=12.0.0" }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" + "peerDependencies": { + "picomatch": "^3 || ^4" }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=14.14" } }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" + "tmp": "^0.2.0" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "is-number": "^7.0.0" }, "engines": { - "node": ">=8" + "node": ">=8.0" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" + "node_modules/troika-three-text": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", + "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.4", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "three": ">=0.125.0" } }, - "node_modules/suspend-react": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", - "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "node_modules/troika-three-utils": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", + "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", "peerDependencies": { - "react": ">=17.0" + "three": ">=0.125.0" } }, - "node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==" + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", "dev": true, + "license": "WTFPL", "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" + "utf8-byte-length": "^1.0.1" } }, - "node_modules/tar": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", - "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "dependencies": { + "use-sync-external-store": "^1.2.2" }, "engines": { - "node": ">=18" + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } } }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "license": "MIT", - "peer": true, "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" + "prelude-ls": "^1.2.1" }, "engines": { - "node": ">=6" + "node": ">= 0.8.0" } }, - "node_modules/tar/node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "optional": true, "engines": { - "node": ">=18" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tar/node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=14.17" } }, - "node_modules/tar/node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" + "punycode": "^2.1.0" } }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "engines": { - "node": ">=18" + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/temp-file": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", "dev": true, - "license": "MIT", - "dependencies": { - "async-exit-hook": "^2.0.1", - "fs-extra": "^10.0.0" + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "engines": { + "node": ">= 4" } }, - "node_modules/temp-file/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" }, "engines": { - "node": ">=12" + "node": ">=0.6.0" } }, - "node_modules/temp-file/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, - "license": "MIT", "dependencies": { - "universalify": "^2.0.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "graceful-fs": "^4.1.6" + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } } }, - "node_modules/temp-file/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">= 10.0.0" + "node": ">=12" } }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "any-promise": "^1.0.0" + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=0.8" - } - }, - "node_modules/three": { - "version": "0.171.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.171.0.tgz", - "integrity": "sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ==" - }, - "node_modules/three-mesh-bvh": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.8.tgz", - "integrity": "sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==", - "deprecated": "Deprecated due to three.js version incompatibility. Please use v0.8.0, instead.", - "peerDependencies": { - "three": ">= 0.151.0" - } - }, - "node_modules/three-stdlib": { - "version": "2.36.1", - "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", - "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", - "dependencies": { - "@types/draco3d": "^1.4.0", - "@types/offscreencanvas": "^2019.6.4", - "@types/webxr": "^0.5.2", - "draco3d": "^1.4.1", - "fflate": "^0.6.9", - "potpack": "^1.0.1" - }, - "peerDependencies": { - "three": ">=0.128.0" + "node": ">=12" } }, - "node_modules/three-stdlib/node_modules/fflate": { - "version": "0.6.10", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", - "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==" - }, - "node_modules/tiny-typed-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", - "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": ">=12" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">=12" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=14.14" + "node": ">=12" } }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8.0" + "node": ">=12" } }, - "node_modules/troika-three-text": { - "version": "0.52.4", - "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", - "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", - "dependencies": { - "bidi-js": "^1.0.2", - "troika-three-utils": "^0.52.4", - "troika-worker-utils": "^0.52.0", - "webgl-sdf-generator": "1.1.1" - }, - "peerDependencies": { - "three": ">=0.125.0" + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/troika-three-utils": { - "version": "0.52.4", - "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", - "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", - "peerDependencies": { - "three": ">=0.125.0" + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/troika-worker-utils": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", - "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==" - }, - "node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "WTFPL", - "dependencies": { - "utf8-byte-length": "^1.0.1" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true - }, - "node_modules/tunnel-rat": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", - "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", - "dependencies": { - "zustand": "^4.3.2" + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/tunnel-rat/node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } + "node": ">=12" } }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.8.0" + "node": ">=12" } }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.17" + "node": ">=12" } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">= 4.0.0" + "node": ">=12" } }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "license": "MIT", + "optional": true, + "os": [ + "openbsd" ], - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" + "engines": { + "node": ">=12" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "punycode": "^2.1.0" + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" } }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, - "node_modules/utf8-byte-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/utility-types": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", - "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 4" + "node": ">=12" } }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, + "os": [ + "win32" + ], "engines": { - "node": ">=0.6.0" + "node": ">=12" } }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, + "hasInstallScript": true, + "license": "MIT", "bin": { - "vite": "bin/vite.js" + "esbuild": "bin/esbuild" }, "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" + "node": ">=12" }, "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/webgl-constants": { diff --git a/package.json b/package.json index 7bc3bea..f684695 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "modly", - "version": "0.2.1", + "version": "0.3.0", "description": "Local AI-powered 3D mesh generation from images", "main": "./out/main/index.js", "author": "Modly", "private": true, "scripts": { - "dev": "electron-vite dev", - "build": "electron-vite build", + "dev": "node scripts/build-builtins.mjs && electron-vite dev", + "build": "node scripts/build-builtins.mjs && electron-vite build", "preview": "electron-vite preview", "prepare-resources": "node scripts/download-python-embed.js", "package": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false npm run build && npm run prepare-resources && electron-builder", @@ -17,12 +17,16 @@ "@electron-toolkit/utils": "^4.0.0", "@react-three/drei": "^9.120.0", "@react-three/fiber": "^8.17.10", + "@react-three/postprocessing": "^2.19.1", + "@xyflow/react": "^12.10.2", "axios": "^1.7.9", "electron-updater": "^6.8.3", + "esbuild": "^0.28.0", "react": "^18.3.1", "react-dom": "^18.3.1", "tar": "^7.5.9", "three": "^0.171.0", + "three-mesh-bvh": "^0.9.9", "zustand": "^5.0.3" }, "devDependencies": { @@ -61,6 +65,10 @@ "!.venv/**/*", "!__pycache__/**/*" ] + }, + { + "from": "out/builtin-extensions", + "to": "builtin-extensions" } ], "publish": { diff --git a/resources/screenshots/helper.png b/resources/screenshots/helper.png new file mode 100644 index 0000000..599fc28 Binary files /dev/null and b/resources/screenshots/helper.png differ diff --git a/scripts/build-builtins.mjs b/scripts/build-builtins.mjs new file mode 100644 index 0000000..85a205d --- /dev/null +++ b/scripts/build-builtins.mjs @@ -0,0 +1,54 @@ +/** + * Compile built-in extensions (TypeScript → CommonJS JS) and copy manifests. + * Output: out/builtin-extensions/{id}/processor.js + manifest.json + */ + +import { execSync } from 'child_process' +import { readdirSync, existsSync, cpSync, mkdirSync, statSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..') +const srcDir = join(root, 'src', 'areas', 'workflows', 'nodes') +const outDir = join(root, 'out', 'builtin-extensions') + +if (!existsSync(srcDir)) { + console.log('[build-builtins] No builtin-extensions directory found, skipping.') + process.exit(0) +} + +// 1. Compile TypeScript +console.log('[build-builtins] Compiling TypeScript…') +execSync('npx tsc -p tsconfig.builtins.json', { cwd: root, stdio: 'inherit' }) + +// 2. Copy manifest.json, and optionally package.json + npm install +for (const id of readdirSync(srcDir)) { + const extSrcDir = join(srcDir, id) + if (!statSync(extSrcDir).isDirectory()) continue + // Only process extension folders (those with a manifest.json) + if (!existsSync(join(extSrcDir, 'manifest.json'))) continue + + const extOutDir = join(outDir, id) + mkdirSync(extOutDir, { recursive: true }) + + const manifestSrc = join(extSrcDir, 'manifest.json') + if (existsSync(manifestSrc)) { + cpSync(manifestSrc, join(extOutDir, 'manifest.json')) + console.log(`[build-builtins] ${id}: manifest.json copied`) + } else { + console.warn(`[build-builtins] ${id}: manifest.json missing — skipping`) + } + + const pkgSrc = join(extSrcDir, 'package.json') + if (existsSync(pkgSrc)) { + cpSync(pkgSrc, join(extOutDir, 'package.json')) + console.log(`[build-builtins] ${id}: Installing npm dependencies…`) + execSync('npm install --omit=dev --no-audit --no-fund', { + cwd: extOutDir, + stdio: 'inherit', + }) + console.log(`[build-builtins] ${id}: npm install done`) + } +} + +console.log('[build-builtins] Done.') diff --git a/src/App.tsx b/src/App.tsx index e115217..82180ee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,22 +6,18 @@ import { UpdateModal } from '@shared/components/ui/UpdateModal' import { ErrorModal } from '@shared/components/ui/ErrorModal' export default function App(): JSX.Element { - const { checkSetup, setupStatus, initApp, backendStatus, showError, setPatchUpdateReady } = useAppStore() + const { checkSetup, setupStatus, initApp, backendStatus, showError } = useAppStore() const [updateVersion, setUpdateVersion] = useState(null) const [currentVersion, setCurrentVersion] = useState('') useEffect(() => { checkSetup() window.electron.app.onError((message) => showError(message)) - window.electron.updater.onPatchReady(() => { - setPatchUpdateReady(true) - }) window.electron.updater.onMajorMinorAvailable(({ version }) => { setUpdateVersion(`v${version}`) }) return () => { window.electron.app.offError() - window.electron.updater.offPatchReady() window.electron.updater.offMajorMinorAvailable() } }, []) @@ -32,10 +28,7 @@ export default function App(): JSX.Element { useEffect(() => { if (backendStatus !== 'ready') return - window.electron.app.info().then(({ version }) => { - setCurrentVersion(version) - window.electron.updater.check() - }) + window.electron.app.info().then(({ version }) => setCurrentVersion(version)) }, [backendStatus]) if (backendStatus === 'ready') return ( diff --git a/src/areas/generate/GeneratePage.tsx b/src/areas/generate/GeneratePage.tsx index 4aee54c..d4889aa 100644 --- a/src/areas/generate/GeneratePage.tsx +++ b/src/areas/generate/GeneratePage.tsx @@ -1,22 +1,228 @@ -import { useState } from 'react' +import { useState, useRef, useCallback, useEffect } from 'react' import { useAppStore } from '@shared/stores/appStore' -import { useGeneration } from '@shared/hooks/useGeneration' -import ImageUpload from './components/ImageUpload' -import GenerationOptions from './components/GenerationOptions' +import type { GenerationJob } from '@shared/stores/appStore' +import { useApi } from '@shared/hooks/useApi' import GenerationHUD from './components/GenerationHUD' -import WorkspacePanel from './components/WorkspacePanel' import Viewer3D from './components/Viewer3D' +import WorkflowPanel from './components/WorkflowPanel' -export default function GeneratePage(): JSX.Element { - const selectedImagePath = useAppStore((s) => s.selectedImagePath) - const modelId = useAppStore((s) => s.generationOptions.modelId) - const { currentJob, startGeneration, cancelGeneration } = useGeneration() - const isGenerating = currentJob?.status === 'uploading' || currentJob?.status === 'generating' +const MIN_WIDTH = 220 +const MAX_WIDTH = 520 +const DEFAULT_WIDTH = 320 + +// --------------------------------------------------------------------------- +// Export dropdown +// --------------------------------------------------------------------------- + +const EXPORT_FORMATS = [ + { fmt: 'glb' as const, desc: 'Binary glTF' }, + { fmt: 'obj' as const, desc: 'Wavefront' }, + { fmt: 'stl' as const, desc: '3D Print' }, + { fmt: 'ply' as const, desc: 'Polygon File' }, +] + +function ExportDropdown({ + onExport, + onClose, +}: { + onExport: (f: 'glb' | 'obj' | 'stl' | 'ply') => void + onClose: () => void +}) { + return ( +
+ {EXPORT_FORMATS.map(({ fmt, desc }) => ( + + ))} +
+ ) +} + +// --------------------------------------------------------------------------- +// Decimate popover +// --------------------------------------------------------------------------- + +function DecimatePopover({ + currentTriangles, + decimating, + onDecimate, + onClose, +}: { + currentTriangles: number | null + decimating: boolean + onDecimate: (targetFaces: number) => void + onClose: () => void +}) { + const defaultTarget = currentTriangles ? Math.round(currentTriangles * 0.5) : 5000 + const [inputValue, setInputValue] = useState(String(defaultTarget)) + + const parsed = parseInt(inputValue, 10) + const validTarget = !isNaN(parsed) && parsed >= 100 ? parsed : null + const reduction = + currentTriangles && validTarget + ? Math.round((1 - Math.min(validTarget, currentTriangles) / currentTriangles) * 100) + : null + + return ( +
+

Decimate mesh

+ + {currentTriangles && ( +

+ Current: {currentTriangles.toLocaleString()} tri +

+ )} + +
+ + setInputValue(e.target.value)} + min={100} + step={500} + className="bg-zinc-800 border border-zinc-700 rounded-lg px-2.5 py-1.5 text-xs text-zinc-200 w-full focus:outline-none focus:border-violet-500 transition-colors" + /> + {reduction !== null && ( +

+ Reduction: {reduction}% +

+ )} +
+ +
+ + +
+
+ ) +} + +// --------------------------------------------------------------------------- +// Smooth popover +// --------------------------------------------------------------------------- + +function SmoothPopover({ + smoothing, + onSmooth, + onClose, +}: { + smoothing: boolean + onSmooth: (iterations: number) => void + onClose: () => void +}) { + const [inputValue, setInputValue] = useState('3') + + const parsed = parseInt(inputValue, 10) + const valid = !isNaN(parsed) && parsed >= 1 && parsed <= 20 + + return ( +
+

Smooth mesh

+ +
+ + setInputValue(e.target.value)} + min={1} + max={20} + step={1} + className="bg-zinc-800 border border-zinc-700 rounded-lg px-2.5 py-1.5 text-xs text-zinc-200 w-full focus:outline-none focus:border-violet-500 transition-colors" + /> +

More iterations = smoother, but loses detail

+
+ +
+ + +
+
+ ) +} + +// --------------------------------------------------------------------------- +// GeneratePage +// --------------------------------------------------------------------------- +export default function GeneratePage(): JSX.Element { const [unloadStatus, setUnloadStatus] = useState<'idle' | 'done'>('idle') + const [panelWidth, setPanelWidth] = useState(DEFAULT_WIDTH) + const [openPanel, setOpenPanel] = useState<'export' | 'decimate' | 'smooth' | 'import' | null>(null) + const [decimating, setDecimating] = useState(false) + const [smoothing, setSmoothing] = useState(false) + const [importing, setImporting] = useState(false) + const dragging = useRef(false) + + const isGenerating = useAppStore((s) => + s.currentJob?.status === 'uploading' || s.currentJob?.status === 'generating' + ) + const currentJob = useAppStore((s) => s.currentJob) + const apiUrl = useAppStore((s) => s.apiUrl) + const updateCurrentJob = useAppStore((s) => s.updateCurrentJob) + const setCurrentJob = useAppStore((s) => s.setCurrentJob) + const meshStats = useAppStore((s) => s.meshStats) + const pushMeshUrl = useAppStore((s) => s.pushMeshUrl) + const undoMesh = useAppStore((s) => s.undoMesh) + const redoMesh = useAppStore((s) => s.redoMesh) + const canUndo = useAppStore((s) => s.historyIndex > 0) + const canRedo = useAppStore((s) => s.historyIndex < s.meshHistory.length - 1) + const { optimizeMesh, smoothMesh, importMesh } = useApi() - const canGenerate = !!selectedImagePath && !!modelId && !isGenerating - const disabledReason = !selectedImagePath ? 'Select an image first' : !modelId ? 'No model selected — install one in the Models tab' : undefined + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (!e.ctrlKey && !e.metaKey) return + if (e.key === 'z') { e.preventDefault(); undoMesh() } + if (e.key === 'y') { e.preventDefault(); redoMesh() } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }, [undoMesh, redoMesh]) + + const hasModel = currentJob?.status === 'done' && !!currentJob.outputUrl async function handleUnloadAll() { await window.electron.model.unloadAll() @@ -24,54 +230,297 @@ export default function GeneratePage(): JSX.Element { setTimeout(() => setUnloadStatus('idle'), 2000) } + function handleExport(format: 'glb' | 'obj' | 'stl' | 'ply') { + if (!currentJob?.outputUrl) return + const stem = `modly-${Date.now()}` + const link = document.createElement('a') + if (format === 'glb') { + link.href = `${apiUrl}${currentJob.outputUrl}` + } else { + const path = encodeURIComponent(currentJob.outputUrl.replace('/workspace/', '')) + link.href = `${apiUrl}/optimize/export?path=${path}&format=${format}` + } + link.download = `${stem}.${format}` + link.click() + } + + async function handleImportMesh() { + const filePath = await window.electron.fs.selectMeshFile() + if (!filePath) return + setOpenPanel(null) + setImporting(true) + try { + const { url } = await importMesh(filePath) + const job: GenerationJob = { + id: `import-${Date.now()}`, + imageFile: '', + status: 'done', + progress: 100, + outputUrl: url, + originalOutputUrl: url, + createdAt: Date.now(), + } + setCurrentJob(job) + pushMeshUrl(url) + } finally { + setImporting(false) + } + } + + async function handleSmooth(iterations: number) { + if (!currentJob?.outputUrl) return + setSmoothing(true) + try { + const path = currentJob.outputUrl.replace('/workspace/', '') + const { url } = await smoothMesh(path, iterations) + updateCurrentJob({ outputUrl: url }) + pushMeshUrl(url) + setOpenPanel(null) + } finally { + setSmoothing(false) + } + } + + async function handleDecimate(targetFaces: number) { + if (!currentJob?.outputUrl) return + setDecimating(true) + try { + const path = currentJob.outputUrl.replace('/workspace/', '') + const { url } = await optimizeMesh(path, targetFaces) + updateCurrentJob({ outputUrl: url }) + pushMeshUrl(url) + setOpenPanel(null) + } finally { + setDecimating(false) + } + } + + const onMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault() + dragging.current = true + + const onMouseMove = (ev: MouseEvent) => { + if (!dragging.current) return + setPanelWidth((w) => Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, w + ev.movementX))) + } + const onMouseUp = () => { + dragging.current = false + window.removeEventListener('mousemove', onMouseMove) + window.removeEventListener('mouseup', onMouseUp) + } + window.addEventListener('mousemove', onMouseMove) + window.addEventListener('mouseup', onMouseUp) + }, []) + return ( <> -
- {/* Scrollable content */} -
- - -
+
+ +
- {/* Sticky bottom: Generate / Stop button */} -
- {isGenerating ? ( - - ) : ( + {/* Resize handle */} +
+ +
+ {/* Header bar */} +
+ + {/* Undo / Redo */} + + + +
+ + {/* Import */} +
+ {openPanel === 'import' && ( +
+ +
+ )} +
+ + {hasModel && ( + <> +
+ + {/* Export */} +
+ + {openPanel === 'export' && ( + void} + onClose={() => setOpenPanel(null)} + /> + )} +
+ + {/* Smooth */} +
+ + {openPanel === 'smooth' && ( + setOpenPanel(null)} + /> + )} +
+ + {/* Decimate */} +
+ + {openPanel === 'decimate' && ( + setOpenPanel(null)} + /> + )} +
+ )}
-
-
- - - + {/* Viewer area */} +
+ + - {/* Free memory button — top-left overlay */} - + {/* Free memory — overlay top-left */} + +
) diff --git a/src/areas/generate/components/GenerationHUD.tsx b/src/areas/generate/components/GenerationHUD.tsx index c3d980a..abef34c 100644 --- a/src/areas/generate/components/GenerationHUD.tsx +++ b/src/areas/generate/components/GenerationHUD.tsx @@ -1,7 +1,5 @@ -import { useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' import { useGeneration } from '@shared/hooks/useGeneration' -import { useAppStore } from '@shared/stores/appStore' -import { useApi } from '@shared/hooks/useApi' function formatElapsed(seconds: number): string { const m = Math.floor(seconds / 60) @@ -11,79 +9,24 @@ function formatElapsed(seconds: number): string { export default function GenerationHUD(): JSX.Element | null { const { currentJob, reset } = useGeneration() - const apiUrl = useAppStore((s) => s.apiUrl) - const meshStats = useAppStore((s) => s.meshStats) - const updateCurrentJob = useAppStore((s) => s.updateCurrentJob) - const { optimizeMesh } = useApi() - const [elapsed, setElapsed] = useState(0) - const startRef = useRef(null) - const [exportFormat, setExportFormat] = useState('glb') - const [exporting, setExporting] = useState(false) const [tqdmLog, setTqdmLog] = useState(null) - async function handleExport() { - if (!currentJob?.outputUrl || exporting) return - setExporting(true) - await window.electron.model.export({ outputUrl: currentJob.outputUrl, format: exportFormat }) - setExporting(false) - } - - const maxFaces = meshStats?.triangles ?? 50000 - const [targetFaces, setTargetFaces] = useState(Math.round(maxFaces / 2)) - const [optimizing, setOptimizing] = useState(false) - const [originalUrl, setOriginalUrl] = useState(null) - const [optimizedUrl, setOptimizedUrl] = useState(null) - const [showingOptimized, setShowingOptimized] = useState(false) - - // Reset optimization state when a new model is loaded - useEffect(() => { - setTargetFaces(Math.round(maxFaces / 2)) - setOriginalUrl(null) - setOptimizedUrl(null) - setShowingOptimized(false) - }, [maxFaces]) - - const handleOptimize = async () => { - if (!currentJob?.outputUrl) return - // Always decimate from the original to avoid chaining degradation - const baseUrl = originalUrl ?? currentJob.outputUrl - const path = baseUrl.replace('/workspace/', '') - setOptimizing(true) - try { - const { url } = await optimizeMesh(path, targetFaces) - if (!originalUrl) setOriginalUrl(currentJob.outputUrl) - setOptimizedUrl(url) - setShowingOptimized(true) - updateCurrentJob({ outputUrl: url }) - } finally { - setOptimizing(false) - } - } - - const handleToggle = (wantOptimized: boolean) => { - if (!originalUrl || !optimizedUrl) return - setShowingOptimized(wantOptimized) - updateCurrentJob({ outputUrl: wantOptimized ? optimizedUrl : originalUrl }) - } - const status = currentJob?.status const isActive = status === 'uploading' || status === 'generating' - const isVisible = status === 'uploading' || status === 'generating' || status === 'done' || status === 'error' + const isVisible = status === 'uploading' || status === 'generating' || status === 'error' - // Elapsed timer + // Elapsed timer — based on currentJob.createdAt so it survives navigation useEffect(() => { - if (isActive) { - if (!startRef.current) startRef.current = Date.now() + if (isActive && currentJob?.createdAt) { const id = setInterval(() => { - setElapsed(Math.floor((Date.now() - startRef.current!) / 1000)) + setElapsed(Math.floor((Date.now() - currentJob.createdAt) / 1000)) }, 1000) return () => clearInterval(id) } else { - startRef.current = null setElapsed(0) } - }, [isActive]) + }, [isActive, currentJob?.createdAt]) // tqdm log listener useEffect(() => { @@ -96,7 +39,7 @@ export default function GenerationHUD(): JSX.Element | null { if (!currentJob || !isVisible) return null - const { progress, step, error, outputUrl } = currentJob + const { progress, step, error } = currentJob return (
@@ -132,105 +75,6 @@ export default function GenerationHUD(): JSX.Element | null {
)} - {/* Done */} - {status === 'done' && ( -
- {/* Header row */} -
-
-
- - - -
- Generation complete -
- {meshStats && ( - - {meshStats.triangles.toLocaleString()} tri - - )} -
- -
- - {/* Toggle original / optimized */} - {optimizedUrl && ( -
- - -
- )} - - {/* Slider + Optimize row */} -
-
-
- Polygons - {targetFaces.toLocaleString()} -
- setTargetFaces(Number(e.target.value))} - className="w-full accent-violet-500 disabled:opacity-40" - /> -
- -
- - {/* Export row */} -
- - -
- -
- - -
- )} - {/* Error */} {status === 'error' && (
diff --git a/src/areas/generate/components/GenerationOptions.tsx b/src/areas/generate/components/GenerationOptions.tsx index 60a297b..7638f67 100644 --- a/src/areas/generate/components/GenerationOptions.tsx +++ b/src/areas/generate/components/GenerationOptions.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react' import { useAppStore } from '@shared/stores/appStore' +import { useApi } from '@shared/hooks/useApi' import { FieldLabel, Tooltip, ConfirmModal } from '@shared/components/ui' import type { CatalogModel } from '../models' @@ -83,19 +84,42 @@ function FloatParam({ schema, value, onChange }: { schema: ParamSchema; value: a ) } +function IntInput({ value, onChange, placeholder, className }: { value: number; onChange: (v: number) => void; placeholder?: string; className: string }) { + const [text, setText] = useState(String(value)) + const prevValue = useRef(value) + if (prevValue.current !== value && parseInt(text, 10) !== value) { + prevValue.current = value + setText(String(value)) + } + return ( + { + const raw = e.target.value + if (raw !== '' && raw !== '-' && !/^-?\d+$/.test(raw)) return + setText(raw) + const n = parseInt(raw, 10) + if (!isNaN(n)) { prevValue.current = n; onChange(n) } + }} + className={className} + /> + ) +} + function IntParam({ schema, value, onChange }: { schema: ParamSchema; value: any; onChange: (v: any) => void }): JSX.Element { const isSeed = schema.id === 'seed' return (
- onChange(parseInt(e.target.value) || schema.default)} - className="w-full px-3 py-1.5 text-xs rounded-lg bg-zinc-900 border border-zinc-700/60 text-zinc-200 focus:outline-none focus:border-zinc-500 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + onChange={onChange} + placeholder={isSeed ? '-1 = random' : undefined} + className="w-full px-3 py-1.5 text-xs rounded-lg bg-zinc-900 border border-zinc-700/60 text-zinc-200 focus:outline-none focus:border-zinc-500" /> {isSeed && ( +
+ ) + } + if (param.type === 'float') { + return onChange(v)} className={inputCls} /> + } + // int + return onChange(v)} className={inputCls} /> +} + +// ─── Workflow dropdown ──────────────────────────────────────────────────────── + +function WorkflowDropdown({ workflows, value, onChange }: { + workflows: Workflow[] + value: string | null + onChange: (id: string) => void +}) { + const [open, setOpen] = useState(false) + const selected = workflows.find((w) => w.id === value) + + if (workflows.length === 0) { + return ( +
+ No workflows yet +
+ ) + } + + return ( +
+ + + {open && ( +
+ {workflows.map((wf, i) => ( + + ))} +
+ )} +
+ ) +} + +// ─── Node param rows ────────────────────────────────────────────────────────── +// These components receive nodes + onPatch directly from EmbeddedCanvas +// to avoid relying on the React Flow store (which requires a mounted ). + +type PatchFn = (nodeId: string, patch: Record) => void + +function ImageParamRow({ nodeId, nodes, onPatch }: { nodeId: string; nodes: FlowNode[]; onPatch: PatchFn }) { + const node = nodes.find((n) => n.id === nodeId) + const data = node?.data as { params: Record } | undefined + const preview = data?.params.preview as string | undefined + + const browse = useCallback(async () => { + const p = await window.electron.fs.selectImage() + if (!p) return + const base64 = await window.electron.fs.readFileBase64(p) + const src = `data:${mimeFromPath(p)};base64,${base64}` + onPatch(nodeId, { params: { ...(data?.params ?? {}), filePath: p, preview: src } }) + }, [nodeId, data?.params, onPatch]) + + return ( +
+
+ + + + + Image +
+ {preview ? ( + + ) : ( + + )} +
+ ) +} + +function MeshParamRow({ nodeId, nodes, onPatch }: { nodeId: string; nodes: FlowNode[]; onPatch: PatchFn }) { + const node = nodes.find((n) => n.id === nodeId) + const data = node?.data as { params: Record } | undefined + const source = (data?.params.source as 'file' | 'current' | undefined) ?? 'file' + const fileName = data?.params.fileName as string | undefined + + const browse = useCallback(async () => { + const p = await window.electron.fs.selectMeshFile() + if (!p) return + const name = p.split(/[\\/]/).pop() ?? p + onPatch(nodeId, { params: { ...(data?.params ?? {}), filePath: p, fileName: name, source: 'file' } }) + }, [nodeId, data?.params, onPatch]) + + const toggleSource = useCallback(() => { + const next = source === 'file' ? 'current' : 'file' + onPatch(nodeId, { params: { ...(data?.params ?? {}), source: next } }) + }, [nodeId, data?.params, source, onPatch]) + + return ( +
+
+ + + + Load 3D Mesh +
+ + {/* Toggle: use current model */} + + + {source === 'file' ? ( + fileName ? ( + + ) : ( + + ) + ) : ( +
+ Uses the model currently loaded in the 3D viewer +
+ )} +
+ ) +} + +function TextParamRow({ nodeId, nodes, onPatch }: { nodeId: string; nodes: FlowNode[]; onPatch: PatchFn }) { + const node = nodes.find((n) => n.id === nodeId) + const data = node?.data as { params: Record } | undefined + const text = (data?.params.text as string | undefined) ?? '' + + return ( +
+
+ + + + Text +
+