From a325cbcb6c5a545d5547ca65d8e97ec99ba1287e Mon Sep 17 00:00:00 2001 From: milanofthe Date: Tue, 5 May 2026 13:56:38 +0200 Subject: [PATCH 1/4] Update example models --- static/examples/bouncing-ball.json | 24 ++++++++-------- static/examples/feedback-system.json | 8 +++--- static/examples/pid-subsystem.json | 15 +++++----- static/examples/squarewave-lpf.json | 26 ++++++++--------- static/examples/vanderpol.json | 43 +++++++++++----------------- 5 files changed, 53 insertions(+), 63 deletions(-) diff --git a/static/examples/bouncing-ball.json b/static/examples/bouncing-ball.json index 5de35e44..dd546186 100644 --- a/static/examples/bouncing-ball.json +++ b/static/examples/bouncing-ball.json @@ -1,8 +1,8 @@ { "version": "1.0.0", "metadata": { - "created": "2026-01-25T00:40:48.165Z", - "modified": "2026-01-25T00:40:48.165Z", + "created": "2026-05-05T11:55:55.289Z", + "modified": "2026-05-05T11:55:55.289Z", "name": "bouncing-ball" }, "graph": { @@ -99,8 +99,8 @@ "type": "Constant", "name": "gravity", "position": { - "x": 390, - "y": 250 + "x": 440, + "y": 160 }, "inputs": [], "outputs": [ @@ -115,7 +115,7 @@ ], "params": { "value": "-g", - "_rotation": 0 + "_rotation": 1 }, "pinnedParams": [] }, @@ -285,7 +285,7 @@ "type": "pathsim.events.ZeroCrossing", "name": "Bounce", "position": { - "x": 1320, + "x": 1220, "y": 200 }, "params": { @@ -298,7 +298,7 @@ "type": "pathsim.events.Condition", "name": "Zeno", "position": { - "x": 1380, + "x": 1280, "y": 260 }, "params": { @@ -312,13 +312,13 @@ }, "simulationSettings": { "duration": "50", - "dt": "", + "dt": null, "solver": "RKBS32", "adaptive": true, - "atol": "", - "rtol": "", - "ftol": "", - "dt_min": "", + "atol": null, + "rtol": null, + "ftol": null, + "dt_min": null, "dt_max": "0.05", "ghostTraces": 8, "plotResults": true diff --git a/static/examples/feedback-system.json b/static/examples/feedback-system.json index fc4497dc..3332a47e 100644 --- a/static/examples/feedback-system.json +++ b/static/examples/feedback-system.json @@ -1,8 +1,8 @@ { "version": "1.0.0", "metadata": { - "created": "2026-01-25T00:44:51.546Z", - "modified": "2026-01-25T00:44:51.546Z", + "created": "2026-05-05T11:54:54.306Z", + "modified": "2026-05-05T11:54:54.306Z", "name": "feedback-system" }, "graph": { @@ -53,7 +53,7 @@ "name": "Feedback", "position": { "x": 910, - "y": 460 + "y": 480 }, "inputs": [ { @@ -238,7 +238,7 @@ "atol": "1e-6", "rtol": "1e-4", "ftol": "1e-9", - "dt_min": "", + "dt_min": null, "dt_max": "0.1", "ghostTraces": 6, "plotResults": true diff --git a/static/examples/pid-subsystem.json b/static/examples/pid-subsystem.json index 69a64065..04559674 100644 --- a/static/examples/pid-subsystem.json +++ b/static/examples/pid-subsystem.json @@ -1,8 +1,8 @@ { "version": "1.0.0", "metadata": { - "created": "2026-01-25T00:43:37.789Z", - "modified": "2026-01-25T00:43:37.789Z", + "created": "2026-05-05T11:37:27.297Z", + "modified": "2026-05-05T11:37:27.297Z", "name": "pid-subsystem" }, "graph": { @@ -393,7 +393,8 @@ "sourceNodeId": "pid", "sourcePortIndex": 0, "targetNodeId": "4674b251-a882-45b9-baf1-01db4d0f7a22", - "targetPortIndex": 0 + "targetPortIndex": 0, + "waypoints": [] }, { "id": "09fb7ca4-d71c-46dd-b4bf-95d7275f74b2", @@ -412,8 +413,8 @@ { "id": "4d19383a-86fe-4605-82d4-405f83b4cb27", "position": { - "x": 1060, - "y": 350 + "x": 1070, + "y": 360 }, "isUserWaypoint": true } @@ -445,8 +446,8 @@ "atol": "1e-8", "rtol": "1e-5", "ftol": "1e-9", - "dt_min": "", - "dt_max": "", + "dt_min": null, + "dt_max": null, "ghostTraces": 6, "plotResults": true } diff --git a/static/examples/squarewave-lpf.json b/static/examples/squarewave-lpf.json index e4097a20..88fae494 100644 --- a/static/examples/squarewave-lpf.json +++ b/static/examples/squarewave-lpf.json @@ -1,8 +1,8 @@ { "version": "1.0.0", "metadata": { - "created": "2026-01-25T00:49:57.129Z", - "modified": "2026-01-25T00:49:57.130Z", + "created": "2026-05-05T11:33:21.201Z", + "modified": "2026-05-05T11:33:21.201Z", "name": "squarewave-lpf" }, "graph": { @@ -12,7 +12,7 @@ "type": "SquareWaveSource", "name": "Signal", "position": { - "x": 710, + "x": 640, "y": 260 }, "inputs": [], @@ -36,7 +36,7 @@ "type": "ButterworthLowpassFilter", "name": "LPF", "position": { - "x": 880, + "x": 890, "y": 260 }, "inputs": [ @@ -70,7 +70,7 @@ "type": "Scope", "name": "Result", "position": { - "x": 1040, + "x": 1100, "y": 250 }, "inputs": [ @@ -125,8 +125,8 @@ { "id": "34f0e638-760e-421b-9d4a-6fc606387a87", "position": { - "x": 650, - "y": 110 + "x": 580, + "y": 60 }, "content": "Filtering the high frequency contents out of a square wave with a low pass filter. \n\nParameters can be pinned, see block property dialog (double click).\n\nHover over **Scope** blocks to see plot preview. Or just pin all previews (P).", "width": 440, @@ -140,15 +140,15 @@ "code": "" }, "simulationSettings": { - "duration": "", - "dt": "", + "duration": null, + "dt": null, "solver": "SSPRK22", "adaptive": true, - "atol": "", + "atol": null, "rtol": "1e-2", - "ftol": "", - "dt_min": "", - "dt_max": "", + "ftol": null, + "dt_min": null, + "dt_max": null, "ghostTraces": 0, "plotResults": true } diff --git a/static/examples/vanderpol.json b/static/examples/vanderpol.json index f6d47ac2..cc007dba 100644 --- a/static/examples/vanderpol.json +++ b/static/examples/vanderpol.json @@ -1,8 +1,8 @@ { "version": "1.0.0", "metadata": { - "created": "2026-01-25T00:35:40.025Z", - "modified": "2026-01-25T00:35:40.025Z", + "created": "2026-05-05T11:35:51.117Z", + "modified": "2026-05-05T11:35:51.117Z", "name": "vanderpol" }, "graph": { @@ -37,8 +37,7 @@ ], "params": { "initial_value": "x20" - }, - "color": "#E57373" + } }, { "id": "440919f7-c63c-40d7-8f78-67871a0f89f6", @@ -70,8 +69,7 @@ ], "params": { "initial_value": "x10" - }, - "color": "#E57373" + } }, { "id": "03d10f15-3f0e-48f4-881b-281485faefd6", @@ -112,8 +110,7 @@ "params": { "operations": "\"-+\"", "_rotation": 2 - }, - "color": "#E57373" + } }, { "id": "9e7d89cc-bd31-4615-b34b-1b7a11850be3", @@ -146,8 +143,7 @@ "params": { "exponent": "2", "_rotation": 0 - }, - "color": "#E57373" + } }, { "id": "8db3f239-5418-4068-b92c-18ee1d42c427", @@ -180,8 +176,7 @@ "params": { "gain": "mu", "_rotation": 0 - }, - "color": "#E57373" + } }, { "id": "3c9f84cd-ee77-4232-9596-aaf057dd0254", @@ -205,8 +200,7 @@ "params": { "value": "1", "_rotation": 2 - }, - "color": "#E57373" + } }, { "id": "f94de1eb-7d85-42ab-8af5-69e0bd3cbf7d", @@ -246,8 +240,7 @@ ], "params": { "_rotation": 2 - }, - "color": "#E57373" + } }, { "id": "ee2391d1-5c61-45de-859b-ff98ad13f4a4", @@ -288,8 +281,7 @@ "params": { "operations": "\"+-\"", "_rotation": 0 - }, - "color": "#E57373" + } }, { "id": "34e172de-4aa4-4f33-9844-f78184aa1a19", @@ -312,8 +304,7 @@ "outputs": [], "params": { "_rotation": 3 - }, - "color": "#E57373" + } }, { "id": "b2b48c31-985f-4917-bef9-6dcecdc2e928", @@ -336,8 +327,7 @@ "outputs": [], "params": { "_rotation": 3 - }, - "color": "#E57373" + } } ], "connections": [ @@ -436,8 +426,7 @@ }, "content": "# Van der Pol\n\nThis is the Van der Pol system \n\n$$ \\ddot{x} - \\mu (1 - x^2) \\dot{x} + x = 0$$\n\nmodeled as discrete components.\n", "width": 200, - "height": 120, - "color": "#E57373" + "height": 120 } ] }, @@ -447,14 +436,14 @@ }, "simulationSettings": { "duration": "3*mu", - "dt": "", + "dt": null, "solver": "GEAR52A", "adaptive": true, "atol": "1e-6", "rtol": "1e-4", "ftol": "1e-8", - "dt_min": "", - "dt_max": "", + "dt_min": null, + "dt_max": null, "ghostTraces": 0, "plotResults": true } From e49e67a0d82d4e3ae76edfe51bd250f19fa34b9a Mon Sep 17 00:00:00 2001 From: milanofthe Date: Tue, 5 May 2026 13:56:43 +0200 Subject: [PATCH 2/4] Add guided tours via driver.js with three interactive walkthroughs --- package-lock.json | 22 +- package.json | 1 + src/app.css | 230 ++++++ src/lib/components/ResizablePanel.svelte | 1 + src/lib/components/WelcomeModal.svelte | 67 ++ src/lib/utils/guidedTour.ts | 912 +++++++++++++++++++++++ 6 files changed, 1219 insertions(+), 14 deletions(-) create mode 100644 src/lib/utils/guidedTour.ts diff --git a/package-lock.json b/package-lock.json index 462b3da6..58f333eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@codemirror/theme-one-dark": "^6.0.0", "@xyflow/svelte": "^1.5.0", "codemirror": "^6.0.0", + "driver.js": "^1.4.0", "jspdf": "^4.1.0", "katex": "^0.16.0", "opentype.js": "^1.3.4", @@ -1272,7 +1273,6 @@ "integrity": "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -1312,7 +1312,6 @@ "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -1512,7 +1511,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2055,7 +2053,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -2175,8 +2172,7 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz", "integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dompurify": { "version": "3.3.1", @@ -2188,6 +2184,12 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/driver.js": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.4.0.tgz", + "integrity": "sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2298,7 +2300,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2968,7 +2969,6 @@ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.1.0.tgz", "integrity": "sha512-xd1d/XRkwqnsq6FP3zH1Q+Ejqn2ULIJeDZ+FTKpaabVpZREjsJKRJwuokTNgdqOU+fl55KgbvgZ1pRTSWCP2kQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", @@ -3363,7 +3363,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3397,7 +3396,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3521,7 +3519,6 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -3970,7 +3967,6 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.46.0.tgz", "integrity": "sha512-ZhLtvroYxUxr+HQJfMZEDRsGsmU46x12RvAv/zi9584f5KOX7bUrEbhPJ7cKFmUvZTJXi/CFZUYwDC6M1FigPw==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -4239,7 +4235,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4288,7 +4283,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/package.json b/package.json index 2e96f67e..e197eea7 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@codemirror/theme-one-dark": "^6.0.0", "@xyflow/svelte": "^1.5.0", "codemirror": "^6.0.0", + "driver.js": "^1.4.0", "jspdf": "^4.1.0", "katex": "^0.16.0", "opentype.js": "^1.3.4", diff --git a/src/app.css b/src/app.css index ae20dca8..9b1f9219 100644 --- a/src/app.css +++ b/src/app.css @@ -717,3 +717,233 @@ body.assembly-pending .svelte-flow__edge { transform: scale(1); } } + +/* --- Guided tour (driver.js) — pathview theme ------------------------- */ + +/* Theme-aware overlay: light scrim on dark, dark scrim on light. Driver.js + * sets fill via inline attribute on the inner path, so we override with + * !important. Vars live on :root so toggling theme updates immediately. */ +:root[data-theme='dark'], +:root:not([data-theme]) { + --tour-overlay-color: rgba(255, 255, 255, 0.22); +} + +:root[data-theme='light'] { + --tour-overlay-color: rgba(0, 0, 0, 0.55); +} + +.driver-overlay, +.driver-overlay path { + fill: var(--tour-overlay-color) !important; +} + +.driver-popover { + background-color: var(--surface) !important; + color: var(--text) !important; + border: 1px solid var(--border) !important; + border-radius: var(--radius-lg) !important; + box-shadow: var(--shadow-lg) !important; + max-width: 420px !important; + min-width: 320px !important; + padding: 0 !important; + overflow: hidden !important; +} + +.driver-popover, +.driver-popover * { + font-family: var(--font-ui) !important; +} + +.driver-popover-title { + display: flex !important; + align-items: center !important; + min-height: var(--header-height) !important; + margin: 0 !important; + padding: 0 var(--space-md) !important; + background: var(--surface-raised) !important; + border-bottom: 1px solid var(--border) !important; + border-radius: var(--radius-lg) var(--radius-lg) 0 0 !important; + font-size: var(--font-base) !important; + font-weight: 500 !important; + line-height: 1.3 !important; + color: var(--text-muted) !important; + text-transform: uppercase !important; + letter-spacing: 0.5px !important; +} + +.driver-popover-description { + font-size: 12px !important; + color: var(--text-muted) !important; + line-height: 1.55 !important; + margin: 0 !important; + padding: 14px 16px 0 !important; +} + +.driver-popover-description p { + margin: 0 0 8px !important; +} + +.driver-popover-description p:last-child { + margin-bottom: 0 !important; +} + +.driver-popover-description ul { + margin: 6px 0 10px !important; + padding-left: 18px !important; +} + +.driver-popover-description li { + margin: 2px 0 !important; +} + +.driver-popover-description table { + width: 100% !important; + border-collapse: collapse !important; + margin: 6px 0 !important; + font-size: 11px !important; +} + +.driver-popover-description table td { + padding: 4px 6px !important; + border-bottom: 1px solid var(--border) !important; + vertical-align: middle !important; +} + +.driver-popover-description table tr:last-child td { + border-bottom: none !important; +} + +.driver-popover-description table td:last-child { + text-align: right !important; + white-space: nowrap !important; +} + +.driver-popover-description kbd { + display: inline-block; + padding: 2px 5px; + font-family: inherit !important; + font-size: 10px; + background: var(--surface-raised); + border: 1px solid var(--border); + border-radius: 3px; + color: var(--text-muted); + min-width: 16px; + text-align: center; +} + +.driver-popover-description code { + font-family: var(--font-mono) !important; + color: var(--accent); + background: transparent; + padding: 0; +} + +.driver-popover-progress-text { + font-size: 10px !important; + color: var(--text-disabled) !important; +} + +.driver-popover-footer { + margin-top: 0 !important; + padding: 12px 16px 14px !important; +} + +/* Footer buttons — match the modal action-button styles (with border). */ +.driver-popover-footer button { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + gap: var(--space-sm) !important; + cursor: pointer !important; + border: 1px solid var(--border) !important; + background: var(--surface-raised) !important; + color: var(--text) !important; + padding: var(--space-sm) var(--space-md) !important; + border-radius: var(--radius-md) !important; + font-size: 11px !important; + font-weight: 500 !important; + text-shadow: none !important; + transition: all var(--transition-fast) !important; +} + +.driver-popover-footer button:hover { + background: var(--surface-hover) !important; + border-color: var(--border-focus) !important; +} + +.driver-popover-footer button:active { + transform: scale(0.98) !important; +} + +.driver-popover-footer button:focus { + outline: none !important; +} + +/* Back button as ghost (secondary action). */ +.driver-popover-prev-btn { + background: transparent !important; + color: var(--text-muted) !important; +} + +.driver-popover-prev-btn:hover { + background: var(--surface-hover) !important; + color: var(--text) !important; + border-color: var(--border-focus) !important; +} + + +/* Close button: matches the .icon-btn pattern used in modal headers. + * The default "×" character is hidden and replaced by a Lucide X icon + * via a pseudo-element mask so theme colours apply on hover. */ +.driver-popover-close-btn { + position: absolute !important; + top: 6px !important; + right: 6px !important; + width: 28px !important; + height: 28px !important; + border: none !important; + border-radius: var(--radius-md) !important; + background: transparent !important; + color: transparent !important; + font-size: 0 !important; + cursor: pointer !important; + text-shadow: none !important; + transition: background var(--transition-fast) !important; + z-index: 2 !important; +} + +.driver-popover-close-btn::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background: var(--text-muted) !important; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3C/svg%3E") center / 16px no-repeat; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3C/svg%3E") center / 16px no-repeat; + transition: background var(--transition-fast) !important; +} + +.driver-popover-close-btn:hover { + background: var(--surface-hover) !important; +} + +.driver-popover-close-btn:hover::before { + background: var(--text) !important; +} + +.driver-popover-close-btn:focus { + outline: none !important; +} + +/* Arrow tip color matches popover background; only the visible side counts. */ +.driver-popover-arrow-side-left { + border-left-color: var(--surface) !important; +} +.driver-popover-arrow-side-right { + border-right-color: var(--surface) !important; +} +.driver-popover-arrow-side-top { + border-top-color: var(--surface) !important; +} +.driver-popover-arrow-side-bottom { + border-bottom-color: var(--surface) !important; +} diff --git a/src/lib/components/ResizablePanel.svelte b/src/lib/components/ResizablePanel.svelte index cd56bf33..a28c9345 100644 --- a/src/lib/components/ResizablePanel.svelte +++ b/src/lib/components/ResizablePanel.svelte @@ -179,6 +179,7 @@