diff --git a/assets/licenses.txt b/assets/licenses.txt index bb39b4a..14f128b 100644 --- a/assets/licenses.txt +++ b/assets/licenses.txt @@ -1543,7 +1543,7 @@ The following npm packages may be included in this product: - @babel/helper-string-parser@7.27.1 - @babel/helper-validator-identifier@7.27.1 - - @babel/types@7.28.1 + - @babel/types@7.28.2 These packages each contain the following license: @@ -2028,7 +2028,7 @@ SOFTWARE. The following npm package may be included in this product: - - vuetify@3.9.0 + - vuetify@3.9.2 This package contains the following license: @@ -2058,16 +2058,16 @@ THE SOFTWARE. The following npm packages may be included in this product: - - @vue/compiler-core@3.5.17 - - @vue/compiler-dom@3.5.17 - - @vue/compiler-sfc@3.5.17 - - @vue/compiler-ssr@3.5.17 - - @vue/reactivity@3.5.17 - - @vue/runtime-core@3.5.17 - - @vue/runtime-dom@3.5.17 - - @vue/server-renderer@3.5.17 - - @vue/shared@3.5.17 - - vue@3.5.17 + - @vue/compiler-core@3.5.18 + - @vue/compiler-dom@3.5.18 + - @vue/compiler-sfc@3.5.18 + - @vue/compiler-ssr@3.5.18 + - @vue/reactivity@3.5.18 + - @vue/runtime-core@3.5.18 + - @vue/runtime-dom@3.5.18 + - @vue/server-renderer@3.5.18 + - @vue/shared@3.5.18 + - vue@3.5.18 These packages each contain the following license: diff --git a/frontend/tools/PlaygroundDialogContent.vue b/frontend/tools/PlaygroundDialogContent.vue index 80c0b1c..ec4cfa2 100644 --- a/frontend/tools/PlaygroundDialogContent.vue +++ b/frontend/tools/PlaygroundDialogContent.vue @@ -14,6 +14,8 @@ import {b66Encode} from "./b66.ts"; import {Base64} from 'js-base64'; // More compatible with binary data from python... import {NetworkUpdateEvent, NetworkUpdateEventModel} from "../misc/network.ts"; import {settings} from "../misc/settings.ts"; +// @ts-expect-error +import playgroundStartupCode from './PlaygroundStartup.py?raw'; const props = defineProps<{ initialCode: string }>(); const emit = defineEmits<{ close: [], updateModel: [NetworkUpdateEvent] }>() @@ -21,7 +23,7 @@ const emit = defineEmits<{ close: [], updateModel: [NetworkUpdateEvent] }>() // ============ LOAD MONACO EDITOR ============ setupMonaco() // Must be called before using the editor -const code = ref(props.initialCode); // TODO: Default code as input (and autorun!) +const code = ref((import.meta as any)?.hot?.data?.code || props.initialCode); const outputText = ref(``); function output(text: string) { @@ -69,13 +71,7 @@ async function setupPyodide() { output("Reusing existing Pyodide instance...\n"); } output("Preloading packages...\n"); - await pyodideWorker.asyncRun(`import micropip, asyncio -micropip.set_index_urls(["https://yeicor.github.io/OCP.wasm", "https://pypi.org/simple"]) -await (micropip.install("lib3mf")) -micropip.add_mock_package("py-lib3mf", "2.4.1", modules={"py_lib3mf": 'from lib3mf import *'}) -await (micropip.install(["https://files.pythonhosted.org/packages/67/25/80be117f39ff5652a4fdbd761b061123c5425e379f4b0a5ece4353215d86/yacv_server-0.10.0a4-py3-none-any.whl"])) -from yacv_server import * -micropip.add_mock_package("ocp-vscode", "2.8.9", modules={"ocp_vscode": 'from yacv_server import *'})`, output, output); // Also import yacv_server and mock ocp_vscode here for faster custom code execution + await pyodideWorker.asyncRun(playgroundStartupCode, output, output); // Also import yacv_server and mock ocp_vscode here for faster custom code execution running.value = false; // Indicate that Pyodide is ready output("Pyodide worker initialized.\n"); } @@ -92,6 +88,7 @@ async function runCode() { output("Running code...\n"); try { running.value = true; + if ((import.meta as any).hot) (import.meta as any).hot.data.code = code.value; // Save code for hot reload await pyodideWorker.asyncRun(code.value, output, (msg: string) => { // Detect models printed to console (since http server is not available in pyodide) if (msg.startsWith(yacvServerModelPrefix)) { diff --git a/frontend/tools/PlaygroundStartup.py b/frontend/tools/PlaygroundStartup.py new file mode 100644 index 0000000..0aab0a0 --- /dev/null +++ b/frontend/tools/PlaygroundStartup.py @@ -0,0 +1,50 @@ +import micropip + +# Prioritize the OCP.wasm package repository for finding the ported dependencies. +micropip.set_index_urls(["https://yeicor.github.io/OCP.wasm", "https://pypi.org/simple"]) + +# For build123d < 0.10.0, we need to install the mock the py-lib3mf package (before the main install). +await micropip.install("lib3mf") +micropip.add_mock_package("py-lib3mf", "2.4.1", modules={"py_lib3mf": 'from lib3mf import *'}) + +# Install the yacv_server package, which is the main server for the OCP.wasm playground; and also preinstalls build123d. +await micropip.install("yacv_server") + +# Preimport the yacv_server package to ensure it is available in the global scope, and mock the ocp_vscode package. +from yacv_server import * +micropip.add_mock_package("ocp-vscode", "2.8.9", modules={"ocp_vscode": 'from yacv_server import *'}) +show_object = show + +# Preinstall a font to avoid issues with no font being available. +def install_font_to_ocp(font_url, font_name=None): + # noinspection PyUnresolvedReferences + from pyodide.http import pyfetch + from OCP.Font import Font_FontMgr, Font_SystemFont, Font_FA_Regular + from OCP.TCollection import TCollection_AsciiString + import os, asyncio + + font_name = font_name if font_name is not None else font_url.split("/")[-1] + + # Choose a "system-like" font directory + font_path = os.path.join("/tmp", font_name) + os.makedirs(os.path.dirname(font_path), exist_ok=True) + + # Download the font using pyfetch + loop = asyncio.get_event_loop() + response = loop.run_until_complete(pyfetch(font_url)) + font_data = loop.run_until_complete(response.bytes()) + + # Save it to the system-like folder + with open(font_path, "wb") as f: + f.write(font_data) + + mgr = Font_FontMgr.GetInstance_s() + font_t = Font_SystemFont(TCollection_AsciiString(font_path)) + font_t.SetFontPath(Font_FA_Regular, TCollection_AsciiString(font_path)) + assert mgr.RegisterFont(font_t, False) + #print(f"✅ Font installed at: {font_path}") + return font_path + + +# Make sure there is at least one font installed, so that the tests can run +install_font_to_ocp("https://raw.githubusercontent.com/xbmc/xbmc/d3a7f95f3f017b8e861d5d95cc4b33eef4286ce2/media/Fonts/arial.ttf") diff --git a/frontend/tools/pyodide-worker-api.ts b/frontend/tools/pyodide-worker-api.ts index 945e56a..e7367fe 100644 --- a/frontend/tools/pyodide-worker-api.ts +++ b/frontend/tools/pyodide-worker-api.ts @@ -1,30 +1,43 @@ import type {loadPyodide} from "pyodide"; +import type {MessageEventDataIn} from "./pyodide-worker.ts"; + +let requestId = 0; /** Simple API for the Pyodide worker. */ export function newPyodideWorker(initOpts: Parameters[0]) { let worker = new Worker(new URL('./pyodide-worker.ts', import.meta.url), {type: "module"}); worker.postMessage(initOpts); - return { - asyncRun: (code: String, stdout: (msg: string) => void, stderr: (msg: string) => void) => new Promise((resolve, reject) => { - worker.addEventListener("message", function listener(event) { - if (event.data?.stdout) { - stdout(event.data.stdout); + const commonRequestResponse = (event: MessageEventDataIn, stdout?: (msg: string) => void, stderr?: (msg: string) => void) => { + return new Promise((resolve, reject) => { + worker.addEventListener("message", function listener(event: MessageEvent) { + if (stdout && event.data?.stdout) { + stdout(event.data.stdout); // No clue if associated with this request, but we handle it anyway. return; } - if (event.data?.stderr) { - stderr(event.data.stderr); + if (stderr && event.data?.stderr) { + stderr(event.data.stderr); // No clue if associated with this request, but we handle it anyway. return; } - // Result or error. - worker.removeEventListener("message", listener); + if (event.data?.id !== event.data.id) return; // Ignore messages that are not for this request. if (event.data?.error) { + worker.removeEventListener("message", listener); reject(event.data.error); + } else if (event.data?.hasOwnProperty("result")) { + worker.removeEventListener("message", listener); + resolve(event.data.result); } else { - resolve(event.data?.result); + throw new Error("Unexpected message from worker: " + JSON.stringify(event.data)); } - }); - worker.postMessage(code); - }), + }) + worker.postMessage(event); + }); + } + return { + asyncRun: (code: string, stdout: (msg: string) => void, stderr: (msg: string) => void) => + commonRequestResponse({type: "asyncRun", id: requestId++, code}, stdout, stderr), + mkdirTree: (path: string) => commonRequestResponse({type: "mkdirTree", id: requestId++, path}), + writeFile: (path: string, content: string) => + commonRequestResponse({type: "writeFile", id: requestId++, path, content}), terminate: () => worker.terminate() } } diff --git a/frontend/tools/pyodide-worker.ts b/frontend/tools/pyodide-worker.ts index f76a6f5..db4d0d0 100644 --- a/frontend/tools/pyodide-worker.ts +++ b/frontend/tools/pyodide-worker.ts @@ -12,22 +12,60 @@ let myLoadPyodide = (initOpts: Parameters[0]) => loadPyodide let pyodideReadyPromise: Promise | null = null; -self.onmessage = async (event: MessageEvent) => { +export type MessageEventDataIn = { + type: 'asyncRun'; + id: number; + code: string; +} | { + type: 'mkdirTree'; + id: number; + path: string; +} | { + type: 'writeFile'; + id: number; + path: string; + content: string; +} + +self.onmessage = async (event: MessageEvent) => { if (!pyodideReadyPromise) { // First message is always the init message // If we haven't loaded Pyodide yet, do so now. // This is a singleton, so we only load it once. pyodideReadyPromise = myLoadPyodide(event.data as Parameters[0]); return; } - // All other messages are code to run. - let code = event.data as string; - // make sure loading is done - const pyodide = await pyodideReadyPromise; - // Now load any packages we need, run the code, and send the result back. - await pyodide.loadPackagesFromImports(code); - try { - self.postMessage({result: await pyodide.runPythonAsync(code)}); - } catch (error: any) { - self.postMessage({error: error.message}); + if (event.data.type === 'mkdirTree') { + // Create a directory tree in the Pyodide filesystem. + const pyodide = await pyodideReadyPromise; + try { + pyodide.FS.mkdirTree(event.data.path); + self.postMessage({id: event.data.id, result: true}); + } catch (error: any) { + self.postMessage({id: event.data.id, error: error.message}); + } + return; + } else if (event.data.type === 'writeFile') { + // Write a file to the Pyodide filesystem. + const pyodide = await pyodideReadyPromise; + try { + pyodide.FS.writeFile(event.data.path, event.data.content); + self.postMessage({id: event.data.id, result: true}); + } catch (error: any) { + self.postMessage({id: event.data.id, error: error.message}); + } + } else if (event.data.type === 'asyncRun') { + let code = event.data.code; + // make sure loading is done + const pyodide = await pyodideReadyPromise; + // Now load any packages we need, run the code, and send the result back. + await pyodide.loadPackagesFromImports(code); + try { + self.postMessage({id: event.data.id, result: await pyodide.runPythonAsync(code)}); + } catch (error: any) { + self.postMessage({id: event.data.id, error: error.message}); + } + } else { + console.error("Unknown message type:", (event.data as any)?.type); + self.postMessage({id: (event.data as any)?.id, error: "Unknown message type: " + (event.data as any)?.type}); } }; \ No newline at end of file