diff --git a/scripts/capture-screenshots.js b/scripts/capture-screenshots.js index 871e5de5..e8f72e64 100644 --- a/scripts/capture-screenshots.js +++ b/scripts/capture-screenshots.js @@ -8,15 +8,23 @@ import puppeteer from 'puppeteer-core'; import { readFileSync, existsSync, mkdirSync } from 'fs'; -import { dirname, join } from 'path'; +import { dirname, join, resolve } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const STATIC_DIR = join(__dirname, '..', 'static', 'examples'); -const SCREENSHOTS_DIR = join(STATIC_DIR, 'screenshots'); const MANIFEST_PATH = join(STATIC_DIR, 'manifest.json'); -const BASE_URL = 'https://view.pathsim.org'; +// Where to write the PNGs. Override (SCREENSHOT_OUT_DIR) so a fastsim build can +// capture straight into its build output instead of the default static dir. +const SCREENSHOTS_DIR = process.env.SCREENSHOT_OUT_DIR + ? resolve(process.env.SCREENSHOT_OUT_DIR) + : join(STATIC_DIR, 'screenshots'); + +// Origin (+ base path) to screenshot. Defaults to the public blue pathview. +// The fastsim build points this at a local `vite preview` of the red /app +// build so the tiles match its styling (SCREENSHOT_BASE_URL=http://localhost:PORT/app). +const BASE_URL = process.env.SCREENSHOT_BASE_URL || 'https://view.pathsim.org'; const VIEWPORT = { width: 1000, height: 600 }; const DEVICE_SCALE_FACTOR = 1; const SETTLE_DELAY = 5000; diff --git a/src/lib/components/WelcomeModal.svelte b/src/lib/components/WelcomeModal.svelte index 689a892c..4c0eaa53 100644 --- a/src/lib/components/WelcomeModal.svelte +++ b/src/lib/components/WelcomeModal.svelte @@ -5,6 +5,7 @@ import { cubicOut } from 'svelte/easing'; import Icon from '$lib/components/icons/Icon.svelte'; import { PATHVIEW_VERSION, EXTRACTED_VERSIONS } from '$lib/constants/dependencies'; + import { BRAND } from '$lib/constants/brand'; import { startGuidedTour, type TourId } from '$lib/tours'; interface Example { @@ -102,8 +103,8 @@
- -

Visual block-diagram editor for the PathSim simulation framework

+ +

Visual block-diagram editor for the {BRAND.framework} simulation framework

@@ -112,7 +113,7 @@ New - + Home diff --git a/src/lib/constants/brand.ts b/src/lib/constants/brand.ts new file mode 100644 index 00000000..1f1c4027 --- /dev/null +++ b/src/lib/constants/brand.ts @@ -0,0 +1,25 @@ +/** + * Visible product branding, configurable at build time. + * + * Defaults to PathView (PathSim blue). A re-branded distribution overrides any + * of these via `VITE_BRAND_*` env vars at build time, without touching the + * components that read them. `key` is set as `data-brand` on so CSS can + * key an accent override off it; the JS accent (`accent` / `keywordColor`) feeds + * the canvas default color and the CodeMirror palette. + */ +export const BRAND = { + /** Short key, set as `data-brand` on for CSS overrides. */ + key: import.meta.env.VITE_BRAND_KEY || 'pathsim', + /** Display name (window title, logo alt, autosave prompt, welcome header). */ + name: import.meta.env.VITE_BRAND_NAME || 'PathView', + /** Logo asset filename under static/. */ + logo: import.meta.env.VITE_BRAND_LOGO || 'pathview_logo.png', + /** Primary accent (matches the CSS `--accent` default). */ + accent: import.meta.env.VITE_BRAND_ACCENT || '#0070C0', + /** CodeMirror keyword color (control flow / imports). */ + keywordColor: import.meta.env.VITE_BRAND_KEYWORD || '#E57373', + /** Home link target (welcome modal). */ + home: import.meta.env.VITE_BRAND_HOME || 'https://pathsim.org', + /** Simulation framework name (welcome tagline). */ + framework: import.meta.env.VITE_BRAND_FRAMEWORK || 'PathSim' +}; diff --git a/src/lib/constants/engine.ts b/src/lib/constants/engine.ts new file mode 100644 index 00000000..8cbe9611 --- /dev/null +++ b/src/lib/constants/engine.ts @@ -0,0 +1,34 @@ +/** + * Simulation engine selection. + * + * pathview generates Python that imports from the `pathsim` package tree. The + * engine is parameterised so a drop-in replacement with the same module layout + * (``, `.blocks`, `.solvers`, `.events`) and + * class names can be selected at build time via the `VITE_ENGINE` env var. + * + * Defaults to `pathsim`, so an unconfigured build behaves exactly as before: + * `ENGINE_MODULE` is `pathsim` and `enginePath()` is the identity. + * (Uses the VITE_ prefix to match the repo's existing import.meta.env usage.) + */ + +/** Active engine module name, fixed at build time. Defaults to pathsim. */ +export const ENGINE: string = import.meta.env.VITE_ENGINE || 'pathsim'; + +/** Root import module for the active engine (alias of {@link ENGINE}). */ +export const ENGINE_MODULE: string = ENGINE; + +/** + * Map a `pathsim` package import path to the active engine's package tree. + * + * Core paths (`pathsim`, `pathsim.blocks`, `pathsim.solvers`, ...) are rewritten + * to the engine module; everything else (e.g. toolbox import paths like + * `pathsim_chem.blocks`) is left untouched. In the default pathsim build this is + * the identity function. + */ +export function enginePath(path: string): string { + if (ENGINE === 'pathsim') return path; + if (path === 'pathsim' || path.startsWith('pathsim.')) { + return ENGINE_MODULE + path.slice('pathsim'.length); + } + return path; +} diff --git a/src/lib/pyodide/backend/pyodide/backend.ts b/src/lib/pyodide/backend/pyodide/backend.ts index 51462fdd..b06d1247 100644 --- a/src/lib/pyodide/backend/pyodide/backend.ts +++ b/src/lib/pyodide/backend/pyodide/backend.ts @@ -6,6 +6,7 @@ import { get } from 'svelte/store'; import type { BackendState, REPLRequest, REPLResponse, REPLErrorResponse } from '../types'; import { AbstractBackend } from '../abstract'; +import { enginePreInit } from './engineHooks'; import { backendState } from '../state'; import { TIMEOUTS } from '$lib/constants/python'; import { PROGRESS_MESSAGES, STATUS_MESSAGES } from '$lib/constants/messages'; @@ -82,8 +83,10 @@ export class PyodideBackend extends AbstractBackend { })); }; - // Send init message - this.sendRequest({ type: 'init' }); + // Engine pre-init seam (default no-op → null). An alternate engine + // can obtain an auth token here before the worker installs it. + const token = await enginePreInit(); + this.sendRequest({ type: 'init', token }); // Wait for ready await new Promise((resolve, reject) => { diff --git a/src/lib/pyodide/backend/pyodide/engineHooks.ts b/src/lib/pyodide/backend/pyodide/engineHooks.ts new file mode 100644 index 00000000..6278e493 --- /dev/null +++ b/src/lib/pyodide/backend/pyodide/engineHooks.ts @@ -0,0 +1,13 @@ +/** + * Engine hooks (main thread). + * + * `enginePreInit` runs right before the Pyodide worker is initialized. The + * default is a no-op that returns no token. A dedicated, stable seam so an + * alternate-engine build can swap *only* this module to, for example, obtain an + * auth token (and open a sign-in UI) before the engine install. The returned + * token is forwarded in the worker's `init` message and handed to the engine + * install seam ({@link ./engineInstall}). + */ +export async function enginePreInit(): Promise { + return null; +} diff --git a/src/lib/pyodide/backend/pyodide/engineInstall.ts b/src/lib/pyodide/backend/pyodide/engineInstall.ts new file mode 100644 index 00000000..cbd7f4ad --- /dev/null +++ b/src/lib/pyodide/backend/pyodide/engineInstall.ts @@ -0,0 +1,52 @@ +/** + * Engine install seam (worker side). + * + * Installs the simulation engine into the Pyodide runtime. The default is the + * configured PyPI packages (pathsim). This is a dedicated, stable seam so an + * alternate-engine build can swap *only* this module (e.g. to install a wasm + * wheel, optionally gated behind `ctx.token`) without touching the worker's + * lifecycle code. `PYODIDE_PRELOAD` is loaded by the caller before this runs. + */ + +import { PYTHON_PACKAGES } from '$lib/constants/dependencies'; +import { PROGRESS_MESSAGES } from '$lib/constants/messages'; +import type { PyodideInterface } from 'https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide.mjs'; + +export interface EngineInstallContext { + /** Emit a progress message to the UI. */ + send: (msg: { type: 'progress'; value: string }) => void; + /** Auth token for a gated engine download (unused by the pathsim default). */ + token?: string | null; +} + +export async function installEngine( + pyodide: PyodideInterface, + ctx: EngineInstallContext +): Promise { + for (const pkg of PYTHON_PACKAGES) { + const progressKey = `INSTALLING_${pkg.import.toUpperCase()}` as keyof typeof PROGRESS_MESSAGES; + ctx.send({ + type: 'progress', + value: PROGRESS_MESSAGES[progressKey] ?? `Installing ${pkg.import}...` + }); + + try { + const preFlag = pkg.pre ? ', pre=True' : ''; + await pyodide.runPythonAsync(` +import micropip +await micropip.install('${pkg.pip}'${preFlag}) + `); + + // Verify installation + await pyodide.runPythonAsync(` +import ${pkg.import} +print(f"${pkg.import} {${pkg.import}.__version__} loaded successfully") + `); + } catch (error) { + if (pkg.required) { + throw new Error(`Failed to install required package ${pkg.pip}: ${error}`); + } + console.warn(`Optional package ${pkg.pip} failed to install:`, error); + } + } +} diff --git a/src/lib/pyodide/backend/pyodide/worker.ts b/src/lib/pyodide/backend/pyodide/worker.ts index ff36ac30..9903e5d6 100644 --- a/src/lib/pyodide/backend/pyodide/worker.ts +++ b/src/lib/pyodide/backend/pyodide/worker.ts @@ -3,13 +3,9 @@ * Executes Python code via Pyodide in a separate thread */ -import { - PYODIDE_CDN_URL, - PYODIDE_PRELOAD, - PYTHON_PACKAGES, - type PackageConfig -} from '$lib/constants/dependencies'; +import { PYODIDE_CDN_URL, PYODIDE_PRELOAD } from '$lib/constants/dependencies'; import { PROGRESS_MESSAGES, ERROR_MESSAGES } from '$lib/constants/messages'; +import { installEngine } from './engineInstall'; import type { REPLRequest, REPLResponse } from '../types'; import type { PyodideInterface } from 'https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide.mjs'; @@ -29,7 +25,7 @@ function send(response: REPLResponse): void { /** * Initialize Pyodide and install packages */ -async function initialize(): Promise { +async function initialize(token?: string | null): Promise { if (isInitialized) { send({ type: 'ready' }); return; @@ -56,33 +52,9 @@ async function initialize(): Promise { send({ type: 'progress', value: PROGRESS_MESSAGES.INSTALLING_DEPS }); await pyodide.loadPackage([...PYODIDE_PRELOAD]); - // Install packages from config - for (const pkg of PYTHON_PACKAGES) { - const progressKey = `INSTALLING_${pkg.import.toUpperCase()}` as keyof typeof PROGRESS_MESSAGES; - send({ - type: 'progress', - value: PROGRESS_MESSAGES[progressKey] ?? `Installing ${pkg.import}...` - }); - - try { - const preFlag = pkg.pre ? ', pre=True' : ''; - await pyodide.runPythonAsync(` -import micropip -await micropip.install('${pkg.pip}'${preFlag}) - `); - - // Verify installation - await pyodide.runPythonAsync(` -import ${pkg.import} -print(f"${pkg.import} {${pkg.import}.__version__} loaded successfully") - `); - } catch (error) { - if (pkg.required) { - throw new Error(`Failed to install required package ${pkg.pip}: ${error}`); - } - console.warn(`Optional package ${pkg.pip} failed to install:`, error); - } - } + // Install the simulation engine (default: configured PyPI packages). The + // engineInstall seam lets an alternate-engine build swap this step. + await installEngine(pyodide, { send, token }); // Import numpy as np and gc globally await pyodide.runPythonAsync(`import numpy as np`); @@ -236,7 +208,7 @@ self.onmessage = async (event: MessageEvent) => { try { switch (type) { case 'init': - await initialize(); + await initialize('token' in event.data ? event.data.token : undefined); break; case 'exec': diff --git a/src/lib/pyodide/backend/types.ts b/src/lib/pyodide/backend/types.ts index a37418ad..6061a41e 100644 --- a/src/lib/pyodide/backend/types.ts +++ b/src/lib/pyodide/backend/types.ts @@ -11,7 +11,7 @@ * Request messages (main thread → backend) */ export type REPLRequest = - | { type: 'init' } + | { type: 'init'; token?: string | null } | { type: 'exec'; id: string; code: string } | { type: 'eval'; id: string; expr: string } | { type: 'stream-start'; id: string; expr: string } diff --git a/src/lib/pyodide/pathsimRunner.ts b/src/lib/pyodide/pathsimRunner.ts index 5bd54b0a..c13ea372 100644 --- a/src/lib/pyodide/pathsimRunner.ts +++ b/src/lib/pyodide/pathsimRunner.ts @@ -12,6 +12,7 @@ import { NODE_TYPES } from '$lib/constants/nodeTypes'; import { BLOCK_CATEGORY_ORDER } from '$lib/constants/python'; import { isSubsystem, isInterface } from '$lib/nodes/shapes'; import { blockImportPaths } from '$lib/nodes/generated/blocks'; +import { ENGINE_MODULE, enginePath } from '$lib/constants/engine'; import { graphStore, findParentSubsystem } from '$lib/stores/graph'; import { runStreamingSimulation, @@ -165,8 +166,11 @@ function collectBlockImportGroups(nodes: NodeInstance[]): Map pkg +// `pathsim-chem`, importPath `pathsim_chem`) never match these exact names. +const ENGINE_MODULES = new Set(['pathsim', 'fastsim', ENGINE_MODULE]); + +function isEngineRequirement(r: ToolboxRequirement): boolean { + const s = (r.source ?? {}) as { pkg?: string; url?: string }; + const candidates = [r.importPath, r.id, s.pkg, s.url].filter(Boolean).map(String); + return candidates.some((c) => ENGINE_MODULES.has(c) || ENGINE_MODULES.has(c.replace(/-/g, '_'))); +} + function walkNodeTypes(nodes: NodeInstance[], out: Set): void { for (const node of nodes) { if (node.type !== NODE_TYPES.SUBSYSTEM && node.type !== NODE_TYPES.INTERFACE) { @@ -57,7 +72,8 @@ export function collectRequiredToolboxes(nodes: NodeInstance[]): ToolboxRequirem const t = installed.find((tb) => tb.id === id); if (t) result.push(toRequirement(t)); } - return result; + // Never persist the engine itself as a requirement (see isEngineRequirement). + return result.filter((r) => !isEngineRequirement(r)); } /** @@ -74,5 +90,5 @@ export function collectRequiredToolboxes(nodes: NodeInstance[]): ToolboxRequirem export function findMissingRequirements(reqs: ToolboxRequirement[]): ToolboxRequirement[] { if (!reqs || reqs.length === 0) return []; const installedKeys = new Set(get(toolboxes).map((t) => toolboxSourceKey(t.source))); - return reqs.filter((r) => !installedKeys.has(toolboxSourceKey(r.source))); + return reqs.filter((r) => !isEngineRequirement(r) && !installedKeys.has(toolboxSourceKey(r.source))); } diff --git a/src/lib/utils/codemirror.ts b/src/lib/utils/codemirror.ts index 47f40608..b6d1fa81 100644 --- a/src/lib/utils/codemirror.ts +++ b/src/lib/utils/codemirror.ts @@ -2,14 +2,17 @@ * Shared CodeMirror utilities for consistent editor setup across the app. */ -// Syntax highlighting colors - aligned with node color palette +import { BRAND } from '$lib/constants/brand'; + +// Syntax highlighting colors - aligned with node color palette. keyword and the +// operator/function accent follow the active brand (defaults: red + PathSim blue). export const SYNTAX_COLORS = { - keyword: '#E57373', // Red - control flow, imports - operator: '#0070C0', // PathSim blue - symbols, operators + keyword: BRAND.keywordColor, // control flow, imports + operator: BRAND.accent, // brand accent - symbols, operators special: '#FFB74D', // Orange - classes, types, decorators number: '#4DB6AC', // Teal - numeric literals string: '#81C784', // Green - string literals - function: '#0070C0', // PathSim blue - function names + function: BRAND.accent, // brand accent - function names comment: { dark: '#505060', light: '#909098' }, // Same as line numbers invalid: '#BA68C8', // Purple - errors // Theme-specific values for variables and punctuation diff --git a/src/lib/utils/colors.ts b/src/lib/utils/colors.ts index 25ed5fe2..dd266f40 100644 --- a/src/lib/utils/colors.ts +++ b/src/lib/utils/colors.ts @@ -2,8 +2,11 @@ * Color definitions for PathView */ -// Default node/event color (matches --pathsim-blue CSS variable) -export const DEFAULT_NODE_COLOR = '#0070C0'; +import { BRAND } from '$lib/constants/brand'; + +// Default node/event/annotation color: the brand accent (matches the CSS +// `--accent` default). Items without an explicit color follow the active brand. +export const DEFAULT_NODE_COLOR = BRAND.accent; // Port colors export const PORT_COLORS = { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 56a29925..cfb38377 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,6 +1,7 @@