diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 53ebd75c..72f13481 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -94,8 +94,6 @@ jobs: - name: OpenCOR dependencies (Windows ARM only) if: ${{ matrix.name == 'Windows (ARM)' }} run: bun install --cpu=arm64 - - name: Build libOpenCOR - run: bun libopencor - name: Build OpenCOR env: VITE_FIREBASE_API_KEY: ${{ secrets.VITE_FIREBASE_API_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d68b595..18c33daf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,9 +60,6 @@ jobs: - name: OpenCOR dependencies (Windows ARM only) if: ${{ matrix.name == 'Windows (ARM)' }} run: bun install --cpu=arm64 - - name: Build libOpenCOR - if: ${{ matrix.name != 'Code formatting' && matrix.name != 'Linting' }} - run: bun libopencor - name: Build OpenCOR if: ${{ matrix.name != 'Code formatting' && matrix.name != 'Linting' }} run: bun run build diff --git a/BUILD.md b/BUILD.md index 893cb2c3..1907c76d 100644 --- a/BUILD.md +++ b/BUILD.md @@ -38,13 +38,7 @@ npm install -g bun bun install ``` -2. **Build libOpenCOR's native Node.js module:** - - ```bash - bun libopencor - ``` - -3. **Start the development version of OpenCOR:** +2. **Start the development version of OpenCOR:** - **Desktop version:** ```bash @@ -57,7 +51,7 @@ npm install -g bun bun dev:web ``` -4. **Test OpenCOR:** +3. **Test OpenCOR:** - **Desktop version:** the application will open automatically; and - **Web app version:** open your browser and navigate to the local development URL (typically http://localhost:5173). @@ -74,7 +68,6 @@ npm install -g bun | `dev:web` | (Build and) start OpenCOR's Web app with hot reload | | `format` | Format the code | | `format:check` | Check code formatting without making changes | -| `libopencor` | Build libOpenCOR's native Node.js module | | `lint` | Lint and automatically fix issues | | `release` | Release OpenCOR for the current platform | | `release:local` | Release OpenCOR for the current platform without code signing | diff --git a/CMakeLists.txt b/CMakeLists.txt index 87f1c319..0a3bf1c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ endforeach() # Project details. -project(libOpenCOR VERSION 0.20260211.0) +project(libOpenCOR VERSION 0.20260226.0) # Enable C++20. diff --git a/bun.lock b/bun.lock index 17a4dbdb..771e7b7c 100644 --- a/bun.lock +++ b/bun.lock @@ -20,14 +20,14 @@ "@electron-toolkit/utils": "^4.0.0", "@tailwindcss/postcss": "^4.2.1", "@tailwindcss/vite": "^4.2.1", - "@types/node": "^25.3.0", + "@types/node": "^25.3.1", "@types/plotly.js": "^3.0.10", "@vitejs/plugin-vue": "^6.0.4", "@vue/tsconfig": "^0.8.1", - "@wasm-fmt/clang-format": "^21.1.8", - "autoprefixer": "^10.4.24", + "@wasm-fmt/clang-format": "^22.1.0", + "autoprefixer": "^10.4.27", "cmake-js": "^8.0.0", - "electron": "^40.6.0", + "electron": "^40.6.1", "electron-builder": "^26.8.1", "electron-conf": "^1.3.0", "electron-updater": "^6.8.3", @@ -35,7 +35,7 @@ "esbuild": "^0.27.3", "node-addon-api": "^8.5.0", "rollup-plugin-visualizer": "^7.0.0", - "stylelint": "^17.3.0", + "stylelint": "^17.4.0", "stylelint-config-standard": "^40.0.0", "tailwindcss": "^4.2.1", "tailwindcss-primeui": "^0.6.1", @@ -378,7 +378,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], + "@types/node": ["@types/node@25.3.1", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-hj9YIJimBCipHVfHKRMnvmHg+wfhKc0o4mTtXh9pKBjC8TLJzz0nzGmLi5UJsYAUgSvXFHgb0V2oY10DUFtImw=="], "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], @@ -420,7 +420,7 @@ "@vueuse/shared": ["@vueuse/shared@14.2.1", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw=="], - "@wasm-fmt/clang-format": ["@wasm-fmt/clang-format@21.1.8", "", { "bin": { "clang-format": "clang-format-cli.cjs", "git-clang-format": "git-clang-format", "clang-format-diff": "clang-format-diff.py" } }, "sha512-RA+6pPO4LEQpn3nrIUGzbC+Wzw88nMmfBINWccSSmng2M7UZPdIbk7mfMJZqjxIDhMEXJZb3Z+OufDm746LtFw=="], + "@wasm-fmt/clang-format": ["@wasm-fmt/clang-format@22.1.0", "", { "bin": { "clang-format": "clang-format-cli.cjs", "git-clang-format": "git-clang-format", "clang-format-diff": "clang-format-diff.py" } }, "sha512-l2p82yc/XzYPmZ/KM8o15s7s+mQtDBaqHmTQ3aiZLeDk9wj/9rT9fF41cfO/339xTw3LipzpZ5vjrGyEgwRTnA=="], "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], @@ -456,9 +456,9 @@ "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], - "autoprefixer": ["autoprefixer@10.4.24", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw=="], + "autoprefixer": ["autoprefixer@10.4.27", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA=="], - "balanced-match": ["balanced-match@3.0.1", "", {}, "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], @@ -604,7 +604,7 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - "electron": ["electron@40.6.0", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-ett8W+yOFGDuM0vhJMamYSkrbV3LoaffzJd9GfjI96zRAxyrNqUSKqBpf/WGbQCweDxX2pkUCUfrv4wwKpsFZA=="], + "electron": ["electron@40.6.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-u9YfoixttdauciHV9Ut9Zf3YipJoU093kR1GSYTTXTAXqhiXI0G1A0NnL/f0O2m2UULCXaXMf2W71PloR6V9pQ=="], "electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="], @@ -686,7 +686,7 @@ "file-entry-cache": ["file-entry-cache@11.1.2", "", { "dependencies": { "flat-cache": "^6.1.20" } }, "sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log=="], - "filelist": ["filelist@1.0.5", "", { "dependencies": { "minimatch": "^10.2.1" } }, "sha512-ct/ckWBV/9Dg3MlvCXsLcSUyoWwv9mCKqlhLNB2DAuXR/NZolSXlQqP5dyy6guWlPXBhodZyZ5lGPQcbQDxrEQ=="], + "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -850,8 +850,6 @@ "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], - "known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="], - "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], @@ -928,7 +926,7 @@ "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], - "minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], + "minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -1154,7 +1152,7 @@ "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "stylelint": ["stylelint@17.3.0", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-syntax-patches-for-csstree": "^1.0.26", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", "@csstools/selector-specificity": "^6.0.0", "balanced-match": "^3.0.1", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^11.1.2", "global-modules": "^2.0.0", "globby": "^16.1.0", "globjoin": "^0.1.4", "html-tags": "^5.1.0", "ignore": "^7.0.5", "import-meta-resolve": "^4.2.0", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.37.0", "mathml-tag-names": "^4.0.0", "meow": "^14.0.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.1", "postcss-value-parser": "^4.2.0", "string-width": "^8.1.1", "supports-hyperlinks": "^4.4.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^7.0.0" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-1POV91lcEMhj6SLVaOeA0KlS9yattS+qq+cyWqP/nYzWco7K5jznpGH1ExngvPlTM9QF1Kjd2bmuzJu9TH2OcA=="], + "stylelint": ["stylelint@17.4.0", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-syntax-patches-for-csstree": "^1.0.27", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", "@csstools/selector-specificity": "^6.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.3.3", "css-tree": "^3.1.0", "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^11.1.2", "global-modules": "^2.0.0", "globby": "^16.1.0", "globjoin": "^0.1.4", "html-tags": "^5.1.0", "ignore": "^7.0.5", "import-meta-resolve": "^4.2.0", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "mathml-tag-names": "^4.0.0", "meow": "^14.0.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.1", "postcss-value-parser": "^4.2.0", "string-width": "^8.1.1", "supports-hyperlinks": "^4.4.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^7.0.0" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-3kQ2/cHv3Zt8OBg+h2B8XCx9evEABQIrv4hh3uXahGz/ZEHrTR80zxBiK2NfXNaSoyBzxO1pjsz1Vhdzwn5XSw=="], "stylelint-config-recommended": ["stylelint-config-recommended@18.0.0", "", { "peerDependencies": { "stylelint": "^17.0.0" } }, "sha512-mxgT2XY6YZ3HWWe3Di8umG6aBmWmHTblTgu/f10rqFXnyWxjKWwNdjSWkgkwCtxIKnqjSJzvFmPT5yabVIRxZg=="], @@ -1278,7 +1276,7 @@ "@develar/schema-utils/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], - "@electron/asar/minimatch": ["minimatch@3.1.3", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA=="], + "@electron/asar/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], @@ -1292,7 +1290,7 @@ "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], - "@electron/universal/minimatch": ["minimatch@9.0.6", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ=="], + "@electron/universal/minimatch": ["minimatch@9.0.8", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-reYkDYtj/b19TeqbNZCV4q9t+Yxylf/rYBsLb42SXJatTv4/ylq5lEiAmhA/IToxO7NI2UzNMghHoHuaqDkAjw=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -1330,8 +1328,6 @@ "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], - "brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "builder-util/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], "cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -1352,13 +1348,13 @@ "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "dir-compare/minimatch": ["minimatch@3.1.3", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA=="], + "dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "dmg-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], "dmg-license/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], - "electron/@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="], + "electron/@types/node": ["@types/node@24.10.14", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-OowOUbD1lBCOFIPOZ8xnMIhgqA4sCutMiYOmPHL1PTLt5+y1XA+g2+yC9OOyz8p+deMZqPZLxfMjYIfrKsPeFg=="], "electron-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], @@ -1370,7 +1366,9 @@ "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], - "glob/minimatch": ["minimatch@3.1.3", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA=="], + "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], @@ -1454,7 +1452,7 @@ "app-builder-lib/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], - "cacache/glob/minimatch": ["minimatch@9.0.6", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ=="], + "cacache/glob/minimatch": ["minimatch@9.0.8", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-reYkDYtj/b19TeqbNZCV4q9t+Yxylf/rYBsLb42SXJatTv4/ylq5lEiAmhA/IToxO7NI2UzNMghHoHuaqDkAjw=="], "cli-truncate/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1524,6 +1522,8 @@ "electron/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "global-prefix/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -1566,6 +1566,8 @@ "dir-compare/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "rollup-plugin-visualizer/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], diff --git a/package.json b/package.json index b0e59a62..ac00f0d4 100644 --- a/package.json +++ b/package.json @@ -23,22 +23,21 @@ "url": "git+https://github.com/opencor/webapp.git" }, "type": "module", - "version": "0.20260224.1", + "version": "0.20260227.0", "scripts": { "archive:web": "bun src/renderer/scripts/archive.web.js", - "build": "electron-vite build", + "build": "bun src/renderer/scripts/libopencor.js && electron-vite build", "build:web": "cd ./src/renderer && vite build && bun scripts/generate.version.js", "clean": "bun src/renderer/scripts/clean.js", "dependencies:update": "bun clean && bun update -i && bun install && bun clean && cd ./src/renderer && bun clean && bun update -i && bun install && bun clean", - "dev": "electron-vite dev --watch", + "dev": "bun src/renderer/scripts/libopencor.js && electron-vite dev --watch", "dev:web": "cd ./src/renderer && vite dev", "format": "bunx --bun biome format --fix --max-diagnostics=none && clang-format -i src/renderer/src/libopencor/src/*", "format:check": "bunx --bun biome format --max-diagnostics=none && clang-format --dry-run -Werror src/renderer/src/libopencor/src/*", - "libopencor": "cmake-js build -B Release -O ./dist/libOpenCOR", "lint": "bunx --bun biome lint --fix --error-on-warnings --max-diagnostics=none && bunx stylelint '**/*.css' --fix", - "release": "electron-builder", - "release:local": "CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --config.mac.notarize=false --publish=never", - "start": "electron-vite preview", + "release": "bun src/renderer/scripts/libopencor.js && electron-builder", + "release:local": "bun src/renderer/scripts/libopencor.js && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --config.mac.notarize=false --publish=never", + "start": "bun src/renderer/scripts/libopencor.js && electron-vite preview", "start:web": "bun build:web && cd ./src/renderer && vite preview", "version:new": "bun src/renderer/scripts/version.js" }, @@ -67,14 +66,14 @@ "@electron-toolkit/utils": "^4.0.0", "@tailwindcss/postcss": "^4.2.1", "@tailwindcss/vite": "^4.2.1", - "@types/node": "^25.3.0", + "@types/node": "^25.3.1", "@types/plotly.js": "^3.0.10", "@vitejs/plugin-vue": "^6.0.4", "@vue/tsconfig": "^0.8.1", - "@wasm-fmt/clang-format": "^21.1.8", - "autoprefixer": "^10.4.24", + "@wasm-fmt/clang-format": "^22.1.0", + "autoprefixer": "^10.4.27", "cmake-js": "^8.0.0", - "electron": "^40.6.0", + "electron": "^40.6.1", "electron-builder": "^26.8.1", "electron-conf": "^1.3.0", "electron-updater": "^6.8.3", @@ -82,7 +81,7 @@ "esbuild": "^0.27.3", "node-addon-api": "^8.5.0", "rollup-plugin-visualizer": "^7.0.0", - "stylelint": "^17.3.0", + "stylelint": "^17.4.0", "stylelint-config-standard": "^40.0.0", "tailwindcss": "^4.2.1", "tailwindcss-primeui": "^0.6.1", diff --git a/src/main/MainWindow.ts b/src/main/MainWindow.ts index eb8cc189..4cdc5c73 100644 --- a/src/main/MainWindow.ts +++ b/src/main/MainWindow.ts @@ -81,6 +81,14 @@ export const resetAll = (): void => { let recentFilePaths: string[] = []; +const removeRecentFilePath = (filePath: string): void => { + for (let i = recentFilePaths.length - 1; i >= 0; --i) { + if (recentFilePaths[i] === filePath) { + recentFilePaths.splice(i, 1); + } + } +}; + export const clearRecentFiles = (): void => { recentFilePaths = []; @@ -94,20 +102,25 @@ export const fileClosed = (filePath: string): void => { return; } + removeRecentFilePath(filePath); + recentFilePaths.unshift(filePath); - recentFilePaths = recentFilePaths.slice(0, 10); + + if (recentFilePaths.length > 10) { + recentFilePaths.length = 10; + } updateReopenMenu(recentFilePaths); }; export const fileIssue = (filePath: string): void => { - recentFilePaths = recentFilePaths.filter((recentFilePath) => recentFilePath !== filePath); + removeRecentFilePath(filePath); updateReopenMenu(recentFilePaths); }; export const fileOpened = (filePath: string): void => { - recentFilePaths = recentFilePaths.filter((recentFilePath) => recentFilePath !== filePath); + removeRecentFilePath(filePath); updateReopenMenu(recentFilePaths); @@ -142,6 +155,7 @@ export class MainWindow extends ApplicationWindow { private _splashScreenWindowClosed = false; private _openedFilePaths: string[] = []; + private _openedFilePathIndex = 0; private _selectedFilePath = ''; // Constructor. @@ -209,6 +223,7 @@ export class MainWindow extends ApplicationWindow { // Reopen previously opened files, if any, and select the previously selected file. this._openedFilePaths = electronConf.get('app.files.opened'); + this._openedFilePathIndex = 0; this._selectedFilePath = electronConf.get('app.files.selected'); this.reopenFilePathsAndSelectFilePath(); @@ -360,16 +375,21 @@ export class MainWindow extends ApplicationWindow { // reopen. So, we need to wait for the file to be reopened before reopening the next one. reopenFilePathsAndSelectFilePath(): void { - if (this._openedFilePaths.length) { - const filePath = this._openedFilePaths[0]; + if (this._openedFilePathIndex < this._openedFilePaths.length) { + const filePath = this._openedFilePaths[this._openedFilePathIndex]; - this.webContents.send('open', filePath); + if (filePath) { + this.webContents.send('open', filePath); + } - this._openedFilePaths = this._openedFilePaths.slice(1); + ++this._openedFilePathIndex; - if (this._openedFilePaths.length) { + if (this._openedFilePathIndex < this._openedFilePaths.length) { return; } + + this._openedFilePaths = []; + this._openedFilePathIndex = 0; } if (this._selectedFilePath) { @@ -394,21 +414,27 @@ export class MainWindow extends ApplicationWindow { return; } - commandLine.forEach((argument: string) => { + for (let argument of commandLine) { if (this.isAction(argument)) { this.webContents.send('action', argument.slice(FULL_URI_SCHEME.length)); - } else if (argument !== '--allow-file-access-from-files' && argument !== '--enable-avfoundation') { - // The argument is not an action (and not --allow-file-access-from-files or --enable-avfoundation either), so it - // must be a file to open. But, first, check whether the argument is a relative path and, if so, convert it to - // an absolute path. - if (!path.isAbsolute(argument)) { - argument = path.resolve(argument); - } + continue; + } - this.webContents.send('open', argument); + if (argument === '--allow-file-access-from-files' || argument === '--enable-avfoundation') { + continue; } - }); + + // The argument is not an action (and not --allow-file-access-from-files or --enable-avfoundation either), so it + // must be a file to open. But, first, check whether the argument is a relative path and, if so, convert it to + // an absolute path. + + if (!path.isAbsolute(argument)) { + argument = path.resolve(argument); + } + + this.webContents.send('open', argument); + } } // Enable/disable our UI. diff --git a/src/main/index.ts b/src/main/index.ts index 80fd6825..92b8be64 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -90,44 +90,68 @@ electron.app.on('second-instance', (_event, argv) => { electron.app.setAsDefaultProtocolClient(URI_SCHEME, isWindows() ? process.execPath : undefined); -if (isLinux()) { - // Make our application icon available so that it can be referenced by our desktop file. +// Set up Linux desktop integration by creating a desktop file and making our application icon available. +// Note: this is not needed on Windows and macOS since they automatically pick up the necessary information from +// OpenCOR. - const localShareFolder = path.join(electron.app.getPath('home'), '.local/share'); - const localShareOpencorFolder = path.join(localShareFolder, URI_SCHEME); +const setupLinuxDesktopIntegration = async (): Promise => { + try { + // Make our application icon available so that it can be referenced by our desktop file. - // Check whether localShareOpencorFolder exists and, if not, create it. + const localShareFolder = path.join(electron.app.getPath('home'), '.local/share'); + const localShareOpencorFolder = path.join(localShareFolder, URI_SCHEME); + const iconSourcePath = path.join(import.meta.dirname, '../../src/main/assets/icon.png'); + const iconTargetPath = path.join(localShareOpencorFolder, 'icon.png'); - if (!fs.existsSync(localShareOpencorFolder)) { - fs.mkdirSync(localShareOpencorFolder); - } + await fs.promises.mkdir(localShareOpencorFolder, { recursive: true }); + await fs.promises.copyFile(iconSourcePath, iconTargetPath); + + // Create a desktop file for OpenCOR and its URI scheme. - fs.copyFileSync( - path.join(import.meta.dirname, '../../src/main/assets/icon.png'), - path.join(`${localShareOpencorFolder}/icon.png`) - ); + const localApplicationsFolder = path.join(localShareFolder, 'applications'); - // Create a desktop file for OpenCOR and its URI scheme. + await fs.promises.mkdir(localApplicationsFolder, { recursive: true }); - fs.writeFileSync( - path.join(`${localShareFolder}/applications/${URI_SCHEME}.desktop`), - `[Desktop Entry] + const desktopFilePath = path.join(localApplicationsFolder, `${URI_SCHEME}.desktop`); + const desktopFileContents = `[Desktop Entry] Type=Application Name=OpenCOR Exec=${process.execPath} %u -Icon=${localShareOpencorFolder}/icon.png +Icon=${iconTargetPath} Terminal=false -MimeType=x-scheme-handler/${URI_SCHEME}` - ); +MimeType=x-scheme-handler/${URI_SCHEME}`; + let updateDesktopDatabase: boolean = false; + + try { + const currentDesktopFileContents = await fs.promises.readFile(desktopFilePath, { encoding: 'utf8' }); + + if (currentDesktopFileContents !== desktopFileContents) { + await fs.promises.writeFile(desktopFilePath, desktopFileContents); - // Update the desktop database. + updateDesktopDatabase = true; + } + } catch { + await fs.promises.writeFile(desktopFilePath, desktopFileContents); - nodeChildProcess.exec('update-desktop-database ~/.local/share/applications', (error) => { - if (error) { - console.error('Failed to update the desktop database:', error); + updateDesktopDatabase = true; } - }); -} + + // Update the desktop database. + + if (updateDesktopDatabase) { + nodeChildProcess.exec( + 'update-desktop-database ~/.local/share/applications', + (error: nodeChildProcess.ExecException | null) => { + if (error) { + console.error('Failed to update the desktop database:', formatError(error)); + } + } + ); + } + } catch (error: unknown) { + console.error('Failed to set up Linux desktop integration:', formatError(error)); + } +}; // Handle the clicking of an opencor:// link. @@ -144,6 +168,12 @@ electron.app.on('open-url', (_event, url) => { electron.app .whenReady() .then(() => { + // Set up Linux desktop integration. + + if (isLinux()) { + setupLinuxDesktopIntegration(); + } + // Set process.env.NODE_ENV to 'production' if we are not the default app. // Note: we do this because some packages rely on the value of process.env.NODE_ENV to determine whether they // should run in development mode (default) or production mode. diff --git a/src/renderer/bun.lock b/src/renderer/bun.lock index 2ed62736..82dcc6cb 100644 --- a/src/renderer/bun.lock +++ b/src/renderer/bun.lock @@ -18,13 +18,13 @@ "@biomejs/biome": "^2.4.4", "@tailwindcss/postcss": "^4.2.1", "@tailwindcss/vite": "^4.2.1", - "@types/node": "^25.3.0", + "@types/node": "^25.3.1", "@types/plotly.js": "^3.0.10", "@vitejs/plugin-vue": "^6.0.4", "@vue/tsconfig": "^0.8.1", - "autoprefixer": "^10.4.24", + "autoprefixer": "^10.4.27", "esbuild": "^0.27.3", - "stylelint": "^17.3.0", + "stylelint": "^17.4.0", "stylelint-config-standard": "^40.0.0", "tailwindcss": "^4.2.1", "tailwindcss-primeui": "^0.6.1", @@ -286,7 +286,7 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], + "@types/node": ["@types/node@25.3.1", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-hj9YIJimBCipHVfHKRMnvmHg+wfhKc0o4mTtXh9pKBjC8TLJzz0nzGmLi5UJsYAUgSvXFHgb0V2oY10DUFtImw=="], "@types/plotly.js": ["@types/plotly.js@3.0.10", "", {}, "sha512-q+MgO4aajC2HrO7FllTYWzrpdfbTjboSMfjkz/aXKjg1v7HNo1zMEFfAW7quKfk6SL+bH74A5ThBEps/7hZxOA=="], @@ -332,9 +332,7 @@ "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], - "autoprefixer": ["autoprefixer@10.4.24", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw=="], - - "balanced-match": ["balanced-match@3.0.1", "", {}, "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w=="], + "autoprefixer": ["autoprefixer@10.4.27", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], @@ -482,8 +480,6 @@ "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], - "known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="], - "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], @@ -604,7 +600,7 @@ "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "stylelint": ["stylelint@17.3.0", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-syntax-patches-for-csstree": "^1.0.26", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", "@csstools/selector-specificity": "^6.0.0", "balanced-match": "^3.0.1", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^11.1.2", "global-modules": "^2.0.0", "globby": "^16.1.0", "globjoin": "^0.1.4", "html-tags": "^5.1.0", "ignore": "^7.0.5", "import-meta-resolve": "^4.2.0", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.37.0", "mathml-tag-names": "^4.0.0", "meow": "^14.0.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.1", "postcss-value-parser": "^4.2.0", "string-width": "^8.1.1", "supports-hyperlinks": "^4.4.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^7.0.0" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-1POV91lcEMhj6SLVaOeA0KlS9yattS+qq+cyWqP/nYzWco7K5jznpGH1ExngvPlTM9QF1Kjd2bmuzJu9TH2OcA=="], + "stylelint": ["stylelint@17.4.0", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-syntax-patches-for-csstree": "^1.0.27", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", "@csstools/selector-specificity": "^6.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.3.3", "css-tree": "^3.1.0", "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^11.1.2", "global-modules": "^2.0.0", "globby": "^16.1.0", "globjoin": "^0.1.4", "html-tags": "^5.1.0", "ignore": "^7.0.5", "import-meta-resolve": "^4.2.0", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "mathml-tag-names": "^4.0.0", "meow": "^14.0.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.1", "postcss-value-parser": "^4.2.0", "string-width": "^8.1.1", "supports-hyperlinks": "^4.4.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^7.0.0" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-3kQ2/cHv3Zt8OBg+h2B8XCx9evEABQIrv4hh3uXahGz/ZEHrTR80zxBiK2NfXNaSoyBzxO1pjsz1Vhdzwn5XSw=="], "stylelint-config-recommended": ["stylelint-config-recommended@18.0.0", "", { "peerDependencies": { "stylelint": "^17.0.0" } }, "sha512-mxgT2XY6YZ3HWWe3Di8umG6aBmWmHTblTgu/f10rqFXnyWxjKWwNdjSWkgkwCtxIKnqjSJzvFmPT5yabVIRxZg=="], diff --git a/src/renderer/package.json b/src/renderer/package.json index e5fd5b3d..5adf4d25 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -39,7 +39,7 @@ }, "./style.css": "./dist/opencor.css" }, - "version": "0.20260224.1", + "version": "0.20260227.0", "scripts": { "build": "vite build && bun scripts/generate.version.js", "build:lib": "vite build --config vite.lib.config.ts && cp index.d.ts dist/index.d.ts", @@ -75,13 +75,13 @@ "@biomejs/biome": "^2.4.4", "@tailwindcss/postcss": "^4.2.1", "@tailwindcss/vite": "^4.2.1", - "@types/node": "^25.3.0", + "@types/node": "^25.3.1", "@types/plotly.js": "^3.0.10", "@vitejs/plugin-vue": "^6.0.4", "@vue/tsconfig": "^0.8.1", - "autoprefixer": "^10.4.24", + "autoprefixer": "^10.4.27", "esbuild": "^0.27.3", - "stylelint": "^17.3.0", + "stylelint": "^17.4.0", "stylelint-config-standard": "^40.0.0", "tailwindcss": "^4.2.1", "tailwindcss-primeui": "^0.6.1", diff --git a/src/renderer/scripts/archive.web.js b/src/renderer/scripts/archive.web.js index 75d3ce02..03126325 100644 --- a/src/renderer/scripts/archive.web.js +++ b/src/renderer/scripts/archive.web.js @@ -1,3 +1,5 @@ +#!/usr/bin/env bun + import fs from 'node:fs'; import * as tar from 'tar'; diff --git a/src/renderer/scripts/clean.js b/src/renderer/scripts/clean.js index 4438f916..87b8c401 100644 --- a/src/renderer/scripts/clean.js +++ b/src/renderer/scripts/clean.js @@ -1,3 +1,5 @@ +#!/usr/bin/env bun + import fs from 'node:fs'; for (const path of [ diff --git a/src/renderer/scripts/generate.version.js b/src/renderer/scripts/generate.version.js index b9edf6a8..4a13ceec 100644 --- a/src/renderer/scripts/generate.version.js +++ b/src/renderer/scripts/generate.version.js @@ -1,3 +1,5 @@ +#!/usr/bin/env bun + import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; diff --git a/src/renderer/scripts/libopencor.js b/src/renderer/scripts/libopencor.js new file mode 100644 index 00000000..70406cf6 --- /dev/null +++ b/src/renderer/scripts/libopencor.js @@ -0,0 +1,16 @@ +#!/usr/bin/env bun + +import * as bun from 'bun'; + +(async () => { + if (!(await bun.file('./dist/libOpenCOR/Release/libOpenCOR.node').exists())) { + const build = await bun.spawn(['cmake-js', 'build', '-B', 'Release', '-O', './dist/libOpenCOR'], { + stdio: ['inherit', 'inherit', 'inherit'] + }); + const exitCode = await build.exited; + + if (exitCode !== 0) { + process.exit(exitCode); + } + } +})(); diff --git a/src/renderer/scripts/version.js b/src/renderer/scripts/version.js index 7f54858e..c2ae61ff 100644 --- a/src/renderer/scripts/version.js +++ b/src/renderer/scripts/version.js @@ -1,3 +1,5 @@ +#!/usr/bin/env bun + import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; diff --git a/src/renderer/src/common/common.ts b/src/renderer/src/common/common.ts index 97209ffa..42d5f2ee 100644 --- a/src/renderer/src/common/common.ts +++ b/src/renderer/src/common/common.ts @@ -15,34 +15,34 @@ export interface ISettings { // Note: the order of the checks in osName() is important. For instance, we need to check for "iPhone" before checking // for "Mac" since the user agent of iPhones contains both "iPhone" and "Mac". +let _osName: string | null = null; + const osName = (): string => { + if (_osName) { + return _osName; + } + try { const userAgent = window.navigator.userAgent; if (/Android/i.test(userAgent)) { - return 'Android'; - } - - if (/iPhone|iPad|iPod/i.test(userAgent)) { - return 'iOS'; - } - - if (/Windows/i.test(userAgent)) { - return 'Windows'; + _osName = 'Android'; + } else if (/iPhone|iPad|iPod/i.test(userAgent)) { + _osName = 'iOS'; + } else if (/Windows/i.test(userAgent)) { + _osName = 'Windows'; + } else if (/Linux/i.test(userAgent)) { + _osName = 'Linux'; + } else if (/Mac/i.test(userAgent)) { + _osName = 'macOS'; + } else { + _osName = 'Unknown'; } - - if (/Linux/i.test(userAgent)) { - return 'Linux'; - } - - if (/Mac/i.test(userAgent)) { - return 'macOS'; - } - - return 'Unknown'; } catch { - return 'Unknown'; + _osName = 'Unknown'; } + + return _osName; }; export const isWindows = (): boolean => { diff --git a/src/renderer/src/common/initialisation.ts b/src/renderer/src/common/initialisation.ts index 12572e75..16a6dbb2 100644 --- a/src/renderer/src/common/initialisation.ts +++ b/src/renderer/src/common/initialisation.ts @@ -73,7 +73,7 @@ export const initialiseLocApi = async (): Promise => { const libOpenCOR = ( await import( /* @vite-ignore */ common.corsProxyUrl( - 'https://opencor.ws/libopencor/downloads/wasm/libopencor-0.20260211.0.js' + 'https://opencor.ws/libopencor/downloads/wasm/libopencor-0.20260226.0.js' ) ) ).default; @@ -185,11 +185,11 @@ dependencies.initialiseXxhash // Let people know whether initialisation is done and how it's progressing. -export const done = vue.computed(() => { +export const done = vue.computed(() => { return progress.value >= 100; }); export const failed = vue.ref(false); export const issues = vue.ref([]); -export const progress = vue.computed(() => { +export const progress = vue.computed(() => { return Math.round((100 * crtNbOfSteps.value) / totalNbOfSteps); }); diff --git a/src/renderer/src/common/settings.ts b/src/renderer/src/common/settings.ts index 639b34ce..883862df 100644 --- a/src/renderer/src/common/settings.ts +++ b/src/renderer/src/common/settings.ts @@ -4,6 +4,7 @@ import { electronApi } from './electronApi.ts'; class Settings { protected static _instance: Settings | null = null; private _settings!: ISettings; + private _oldRawSettings: string | null = null; private _isInitialised = false; private _initialisationListeners: (() => void)[] = []; @@ -45,6 +46,7 @@ class Settings { if (electronApi) { electronApi.loadSettings().then((settings: ISettings) => { this._settings = settings; + this._oldRawSettings = JSON.stringify(settings); this.emitInitialised(); }); @@ -54,6 +56,7 @@ class Settings { if (raw) { this._settings = JSON.parse(raw); + this._oldRawSettings = raw; } } catch (error: unknown) { console.error( @@ -69,11 +72,21 @@ class Settings { } save(): void { + const rawSettings = JSON.stringify(this._settings); + + if (rawSettings === this._oldRawSettings) { + return; + } + if (electronApi) { electronApi.saveSettings(this._settings); + + this._oldRawSettings = rawSettings; } else { try { - window.localStorage.setItem('settings', JSON.stringify(this._settings)); + window.localStorage.setItem('settings', rawSettings); + + this._oldRawSettings = rawSettings; } catch (error: unknown) { console.error('Failed to save the settings to the local storage:', formatError(error)); } @@ -86,6 +99,8 @@ class Settings { checkForUpdatesAtStartup: true } }; + + this._oldRawSettings = null; } toString(): string { diff --git a/src/renderer/src/components/ContentsComponent.vue b/src/renderer/src/components/ContentsComponent.vue index 7f2252a0..e3a6092b 100644 --- a/src/renderer/src/components/ContentsComponent.vue +++ b/src/renderer/src/components/ContentsComponent.vue @@ -84,7 +84,7 @@ defineEmits<(event: 'error', message: string) => void>(); const fileTabs = vue.ref([]); const activeFile = vue.ref(''); -const filePaths = vue.computed(() => { +const filePaths = vue.computed(() => { const res: string[] = []; for (const fileTab of fileTabs.value) { diff --git a/src/renderer/src/components/OpenCOR.vue b/src/renderer/src/components/OpenCOR.vue index f8a8da49..ec6696e1 100644 --- a/src/renderer/src/components/OpenCOR.vue +++ b/src/renderer/src/components/OpenCOR.vue @@ -146,7 +146,7 @@ const mainMenuRef = vue.ref | null>(null); const filesRef = vue.ref(null); const contentsRef = vue.ref | null>(null); const issues = vue.ref([]); -const compIssues = vue.computed(() => { +const compIssues = vue.computed(() => { return [...initialisation.issues.value, ...issues.value]; }); const activeInstanceUid = vueCommon.activeInstanceUid(); @@ -163,7 +163,7 @@ const activateInstance = (): void => { activeInstanceUid.value = crtInstanceUid; }; -const compIsActive = vue.computed(() => { +const compIsActive = vue.computed(() => { return activeInstanceUid.value === crtInstanceUid; }); @@ -183,7 +183,7 @@ electronApi?.onEnableDisableUi((enable: boolean) => { // since a dialog already has some overlaying effect and the BlockUI's overlay would just make things look darker // and worse. -const compBlockUiEnabled = vue.computed(() => { +const compBlockUiEnabled = vue.computed(() => { return ( !electronUiEnabled.value || initialisingOpencorMessageVisible.value || @@ -193,7 +193,7 @@ const compBlockUiEnabled = vue.computed(() => { ); }); -const compUiEnabled = vue.computed(() => { +const compUiEnabled = vue.computed(() => { return !compBlockUiEnabled.value && !isDialogActive.value; }); @@ -228,13 +228,13 @@ vue.onUnmounted(() => { // Determine whether to show the main menu or not. -const showMainMenu = vue.computed(() => { +const showMainMenu = vue.computed(() => { return !electronApi && !props.omex; }); // Determine whether the background should be visible. -const compBackgroundVisible = vue.computed(() => { +const compBackgroundVisible = vue.computed(() => { return ( (initialisingOpencorMessageVisible.value || loadingModelMessageVisible.value || progressMessageVisible.value) && !!props.omex @@ -318,11 +318,14 @@ const addToast = (options: Parameters[0]) => { const crtGlobalProperties = crtVueAppInstance?.config.globalProperties as Record | undefined; const vueTippyInstalledFlag = 'opencorVueTippyInstalled'; +let postInitialisationDone = false; vue.watch( initialisation.done, async (newInitialisationDone: boolean) => { - if (newInitialisationDone) { + if (newInitialisationDone && !postInitialisationDone) { + postInitialisationDone = true; + // OpenCOR is now fully initialised, so we can finalise a few things, namely let the current Vue app instance use // VueTippy. @@ -430,7 +433,7 @@ const handleAction = (action: string): void => { // Enable/disable some menu items. -const hasFiles = vue.computed(() => { +const hasFiles = vue.computed(() => { return contentsRef.value?.hasFiles() ?? false; }); diff --git a/src/renderer/src/components/dialogs/BaseDialog.vue b/src/renderer/src/components/dialogs/BaseDialog.vue index 75b5755b..79753555 100644 --- a/src/renderer/src/components/dialogs/BaseDialog.vue +++ b/src/renderer/src/components/dialogs/BaseDialog.vue @@ -95,7 +95,9 @@ interface IDialogState { export const provideDialogState = (): IDialogState => { const activeDialogs = vue.ref(0); - const isDialogActive = vue.computed(() => activeDialogs.value > 0); + const isDialogActive = vue.computed(() => { + return activeDialogs.value > 0; + }); const incrementDialogs = () => { ++activeDialogs.value; diff --git a/src/renderer/src/components/dialogs/SimulationExperimentViewSettingsDialog.vue b/src/renderer/src/components/dialogs/SimulationExperimentViewSettingsDialog.vue index a0e26a73..9c899e91 100644 --- a/src/renderer/src/components/dialogs/SimulationExperimentViewSettingsDialog.vue +++ b/src/renderer/src/components/dialogs/SimulationExperimentViewSettingsDialog.vue @@ -796,7 +796,7 @@ const showSimulationSettingsIssuesPanel = vue.ref(false); const showSolversSettingsIssuesPanel = vue.ref(false); const showUiJsonIssuesPanel = vue.ref(false); const localSettings = vue.ref(JSON.parse(JSON.stringify(props.settings))); -const numberOfDataPoints = vue.computed(() => { +const numberOfDataPoints = vue.computed(() => { // Our total number of data points. // Note: only calculate when simulation settings are valid. @@ -814,7 +814,7 @@ const numberOfDataPoints = vue.computed(() => { return res; }); -const simulationSettingsIssues = vue.computed(() => { +const simulationSettingsIssues = vue.computed(() => { // Validate our simulation settings and return any issues. const res: locApi.IIssue[] = []; @@ -860,7 +860,7 @@ const simulationSettingsIssues = vue.computed(() => { return res; }); -const solversSettingsIssues = vue.computed(() => { +const solversSettingsIssues = vue.computed(() => { // Validate our solvers settings and return any issues. const res: locApi.IIssue[] = []; @@ -881,7 +881,7 @@ const solversSettingsIssues = vue.computed(() => { return res; }); -const uiJsonIssues = vue.computed(() => { +const uiJsonIssues = vue.computed(() => { // Validate our local UI JSON and return any issues. return validateUiJson(localSettings.value.interactive.uiJson); diff --git a/src/renderer/src/components/views/IssuesView.vue b/src/renderer/src/components/views/IssuesView.vue index 42fca59e..8c553f26 100644 --- a/src/renderer/src/components/views/IssuesView.vue +++ b/src/renderer/src/components/views/IssuesView.vue @@ -70,17 +70,17 @@ const props = withDefaults( } ); -const errorCount = vue.computed(() => { +const errorCount = vue.computed(() => { return props.issues.filter((i) => i.type === locApi.EIssueType.ERROR).length; }); -const warningCount = vue.computed(() => { +const warningCount = vue.computed(() => { return props.issues.filter((i) => i.type === locApi.EIssueType.WARNING).length; }); -const informationCount = vue.computed(() => { +const informationCount = vue.computed(() => { return props.issues.filter((i) => i.type === locApi.EIssueType.INFORMATION).length; }); -const severityClass = vue.computed(() => { +const severityClass = vue.computed(() => { if (errorCount.value > 0) { return 'error'; } diff --git a/src/renderer/src/components/views/SimulationExperimentView.vue b/src/renderer/src/components/views/SimulationExperimentView.vue index 94ede7b1..9f1e6fa4 100644 --- a/src/renderer/src/components/views/SimulationExperimentView.vue +++ b/src/renderer/src/components/views/SimulationExperimentView.vue @@ -447,8 +447,12 @@ const traceName = (name: string | undefined, xValue: string, yValue: string): st return name ?? `${yValue} vs. ${xValue}`; }; -const xInfo = vue.computed(() => locCommon.simulationDataInfo(standardInstanceTask, standardXParameter.value)); -const yInfo = vue.computed(() => locCommon.simulationDataInfo(standardInstanceTask, standardYParameter.value)); +const xInfo = vue.computed(() => + locCommon.simulationDataInfo(standardInstanceTask, standardXParameter.value) +); +const yInfo = vue.computed(() => + locCommon.simulationDataInfo(standardInstanceTask, standardYParameter.value) +); const updatePlot = () => { standardData.value = { @@ -499,7 +503,7 @@ const interactiveUiJson = vue.ref( parameters: [] } ); -const interactiveUiJsonEmpty = vue.computed(() => { +const interactiveUiJsonEmpty = vue.computed(() => { if ( interactiveUiJson.value.input.length === 0 && interactiveUiJson.value.output.plots.length === 0 && @@ -520,7 +524,7 @@ const interactiveUiJsonEmpty = vue.computed(() => { }); const interactiveMath = dependencies._mathJs.create(dependencies._mathJs.all, {}); const interactiveModel = interactiveDocument.model(0); -const interactiveData = vue.ref([]); +const interactiveLiveData = vue.ref([]); let interactiveMargins: Record = {}; const interactiveCompMargins = vue.ref(); const interactiveUiJsonIssues = vue.ref(locApi.validateUiJson(interactiveUiJson.value)); @@ -541,53 +545,67 @@ const interactiveRuns = vue.ref([ const interactiveRunColorPopoverIndex = vue.ref(-1); const interactiveRunColorPopoverRefs = vue.ref | undefined>>({}); const interactiveGraphPanelRefs = vue.ref | undefined>>({}); -const interactiveCompData = vue.computed(() => { +const interactiveCompData = vue.computed(() => { // Combine the live data with the data from the tracked runs. + const liveData = interactiveLiveData.value; + const runs = interactiveRuns.value; + const runsCount = runs.length; const res: IGraphPanelData[] = []; + const paletteColors = colors.PALETTE_COLORS; + const paletteColorsLength = paletteColors.length; - for ( - let interactiveDataIndex = 0; - interactiveDataIndex < (interactiveData.value.length || 0); - ++interactiveDataIndex - ) { + for (let liveDataIndex = 0; liveDataIndex < liveData.length; ++liveDataIndex) { const traces: IGraphPanelPlotTrace[] = []; - interactiveRuns.value.forEach((interactiveRun: ISimulationRun, runIndex: number) => { + for (let runIndex = 0; runIndex < runsCount; ++runIndex) { + const interactiveRun = runs[runIndex]; + + if (!interactiveRun) { + continue; + } + if (!interactiveRun.isVisible) { - return; + continue; } - const data = interactiveRun.isLiveRun - ? interactiveData.value[interactiveDataIndex] - : interactiveRun.data[interactiveDataIndex]; - const runTraces = (data?.traces ?? []).map((trace, traceIndex) => { - return { - ...trace, - name: - trace.name + - (interactiveRuns.value.length === 1 ? '' : interactiveRun.isLiveRun ? ' [Live]' : ` [#${runIndex}]`), - color: - colors.PALETTE_COLORS[ - (colors.PALETTE_COLORS.indexOf(interactiveRun.color) + traceIndex) % colors.PALETTE_COLORS.length - ] ?? colors.DEFAULT_COLOR, - zorder: interactiveRun.isLiveRun ? 1 : undefined - }; - }); + const runColorIndex = paletteColors.indexOf(interactiveRun.color); + const baseColorIndex = runColorIndex >= 0 ? runColorIndex : 0; + const data = interactiveRun.isLiveRun ? liveData[liveDataIndex] : interactiveRun.data[liveDataIndex]; + const dataTraces = data?.traces; - traces.push(...runTraces); - }); + if (!dataTraces?.length) { + continue; + } + + const suffix = runsCount === 1 ? '' : interactiveRun.isLiveRun ? ' [Live]' : ` [#${runIndex}]`; + + for (let dataTraceIndex = 0; dataTraceIndex < dataTraces.length; ++dataTraceIndex) { + const dataTrace = dataTraces[dataTraceIndex]; + + if (!dataTrace) { + continue; + } + + traces.push({ + ...dataTrace, + name: dataTrace.name + suffix, + color: paletteColors[(baseColorIndex + dataTraceIndex) % paletteColorsLength] ?? colors.DEFAULT_COLOR, + zorder: interactiveRun.isLiveRun ? 1 : undefined + }); + } + } res.push({ - xAxisTitle: interactiveData.value[interactiveDataIndex]?.xAxisTitle, - yAxisTitle: interactiveData.value[interactiveDataIndex]?.yAxisTitle, - traces: traces + xAxisTitle: liveData[liveDataIndex]?.xAxisTitle, + yAxisTitle: liveData[liveDataIndex]?.yAxisTitle, + traces }); } return res; }); -const interactiveSettings = vue.computed(() => ({ +const interactiveSettings = vue.computed(() => ({ simulation: { startingPoint: interactiveUniformTimeCourse.outputStartTime(), endingPoint: interactiveUniformTimeCourse.outputEndTime(), @@ -609,7 +627,7 @@ const interactiveOldSettings = vue.ref(JSON.stringify(vue.toRaw(interact // Determine whether to show the toolbar. -const showToolbar = vue.computed(() => { +const showToolbar = vue.computed(() => { return (props.simulationOnly && interactiveUiJsonEmpty.value) || !props.simulationOnly; }); @@ -662,16 +680,67 @@ interactiveMath.import( { override: true } ); -const evaluateValue = (value: string): unknown => { - const parser = interactiveMath.parser(); - let index = -1; +const NON_ELEMENTWISE_MULTIPLY_REGEX = /(?(() => { + const plots = interactiveUiJson.value.output.plots; + + for (let i = 0; i < plots.length; ++i) { + const plot = plots[i]; + + if (plot) { + if ( + plot.xValue.includes('*') || + plot.xValue.includes('/') || + plot.yValue.includes('*') || + plot.yValue.includes('/') + ) { + return true; + } - interactiveUiJson.value.input.forEach((input: locApi.IUiJsonInput) => { - parser.set(input.id, interactiveInputValues.value[++index]); - }); + const additionalTraces = plot.additionalTraces; + + if (additionalTraces) { + for (let j = 0; j < additionalTraces.length; ++j) { + const additionalTrace = additionalTraces[j]; + + if ( + additionalTrace && + (additionalTrace.xValue.includes('*') || + additionalTrace.xValue.includes('/') || + additionalTrace.yValue.includes('*') || + additionalTrace.yValue.includes('/')) + ) { + return true; + } + } + } + } + } - return parser.evaluate(value); -}; + return false; +}); + +// Cache for normalised plot expressions. + +const normalisedExpressionCache = new Map(); + +vue.watch( + () => interactiveUiJson.value, + () => { + normalisedExpressionCache.clear(); + }, + { deep: true } +); + +// Function to update our interactive simulation. const updateInteractiveSimulation = (forceUpdate: boolean = false): void => { // Make sure that there are no issues with the UI JSON and that live updates are enabled (unless forced). @@ -680,11 +749,27 @@ const updateInteractiveSimulation = (forceUpdate: boolean = false): void => { return; } + // Create a parser with the current input values as variables. + + const parser = interactiveMath.parser(); + + for (let index = 0; index < interactiveUiJson.value.input.length; ++index) { + const input = interactiveUiJson.value.input[index]; + + if (input) { + parser.set(input.id, interactiveInputValues.value[index]); + } + } + // Show/hide the input widgets. - interactiveUiJson.value.input.forEach((input: locApi.IUiJsonInput, index: number) => { - interactiveShowInput.value[index] = evaluateValue(input.visible ?? 'true') as string; - }); + for (let index = 0; index < interactiveUiJson.value.input.length; ++index) { + const input = interactiveUiJson.value.input[index]; + + if (input) { + interactiveShowInput.value[index] = parser.evaluate(input.visible ?? 'true') as string; + } + } // Update the SED-ML document. @@ -697,7 +782,7 @@ const updateInteractiveSimulation = (forceUpdate: boolean = false): void => { interactiveModel.removeAllChanges(); interactiveInstanceIssues.value = []; - interactiveUiJson.value.parameters.forEach((parameter: locApi.IUiJsonParameter) => { + for (const parameter of interactiveUiJson.value.parameters) { const componentVariableNames = parameter.name.split('/'); if (componentVariableNames[0] && componentVariableNames[1]) { @@ -705,7 +790,7 @@ const updateInteractiveSimulation = (forceUpdate: boolean = false): void => { interactiveModel.addChange( componentVariableNames[0], componentVariableNames[1], - String(evaluateValue(parameter.value)) + String(parser.evaluate(parameter.value)) ); } catch (error: unknown) { interactiveInstanceIssues.value.push({ @@ -714,7 +799,7 @@ const updateInteractiveSimulation = (forceUpdate: boolean = false): void => { }); } } - }); + } if (interactiveInstanceIssues.value.length) { interactiveInstanceIssues.value.push(informationIssue); @@ -736,27 +821,49 @@ const updateInteractiveSimulation = (forceUpdate: boolean = false): void => { return; } - const parser = interactiveMath.parser(); + // Configure our parser to have the simulation data as variables, so that the plot expressions can refer to them. - interactiveUiJson.value.output.data.forEach((data: locApi.IUiJsonOutputData) => { + for (const data of interactiveUiJson.value.output.data) { const info = interactiveIdToInfo[data.id]; if (info) { - parser.set(data.id, locCommon.simulationData(interactiveInstanceTask, info)); + parser.set( + data.id, + containsMultiplicationsOrDivisions.value + ? Array.from(locCommon.simulationData(interactiveInstanceTask, info)) + : locCommon.simulationData(interactiveInstanceTask, info) + ); } else { parser.set(data.id, []); } - }); + } - const parserEvaluate = (value: string): Float64Array => { - // Note: we replace `*` and `/` (but not `.*` and `./`) with `.*` and `./`, respectively, to ensure element-wise - // operations. + // Evaluate the plot expressions to get the data to display. - return new Float64Array(parser.evaluate(value.replace(/(? { + let normalisedExpression = normalisedExpressionCache.get(expression); + + if (!normalisedExpression) { + normalisedExpression = expression + .replace(NON_ELEMENTWISE_MULTIPLY_REGEX, '.*') + .replace(NON_ELEMENTWISE_DIVIDE_REGEX, './'); + // Note: we replace `*` and `/` (but not `.*` and `./`) with `.*` and `./`, respectively, to ensure + // element-wise operations. + + normalisedExpressionCache.set(expression, normalisedExpression); + } + + return Float64Array.from(parser.evaluate(normalisedExpression)); + } + : (expression: string): Float64Array => { + return parser.evaluate(expression); + }; try { - interactiveData.value = interactiveUiJson.value.output.plots.map((plot: locApi.IUiJsonOutputPlot) => { + const newInteractiveData: IGraphPanelData[] = []; + + for (const plot of interactiveUiJson.value.output.plots) { const traces: IGraphPanelPlotTrace[] = [ { name: traceName(plot.name, plot.xValue, plot.yValue), @@ -768,7 +875,7 @@ const updateInteractiveSimulation = (forceUpdate: boolean = false): void => { } ]; - plot.additionalTraces?.forEach((additionalTrace: locApi.IUiJsonOutputPlotAdditionalTrace) => { + for (const additionalTrace of plot.additionalTraces ?? []) { traces.push({ name: traceName(additionalTrace.name, additionalTrace.xValue, additionalTrace.yValue), xValue: additionalTrace.xValue, @@ -777,14 +884,16 @@ const updateInteractiveSimulation = (forceUpdate: boolean = false): void => { y: parserEvaluate(additionalTrace.yValue), color: colors.DEFAULT_COLOR }); - }); + } - return { + newInteractiveData.push({ xAxisTitle: plot.xAxisTitle, yAxisTitle: plot.yAxisTitle, traces - }; - }); + }); + } + + interactiveLiveData.value = newInteractiveData; } catch (error: unknown) { interactiveInstanceIssues.value = [ { @@ -801,17 +910,32 @@ const updateInteractiveSimulation = (forceUpdate: boolean = false): void => { const onMarginsUpdated = (plotId: string, newMargins: IGraphPanelMargins): void => { interactiveMargins[plotId] = newMargins; - const margins = Object.values(interactiveMargins); + let marginCount = 0; + let maxLeft = 0; + let maxRight = 0; + + for (const key in interactiveMargins) { + const margin = interactiveMargins[key]; + + if (!margin) { + continue; + } + + ++marginCount; - if (margins.length !== interactiveUiJson.value.output.plots.length) { + maxLeft = Math.max(maxLeft, margin.left); + maxRight = Math.max(maxRight, margin.right); + } + + if (marginCount !== interactiveUiJson.value.output.plots.length) { interactiveCompMargins.value = undefined; return; } interactiveCompMargins.value = { - left: Math.max(...margins.map((margin) => margin.left)), - right: Math.max(...margins.map((margin) => margin.right)) + left: maxLeft, + right: maxRight }; }; @@ -827,57 +951,74 @@ const onTrackRun = (): void => { const inputParameters: Record = {}; - interactiveUiJson.value.input.forEach((input: locApi.IUiJsonInput, index: number) => { + for (let index = 0; index < interactiveUiJson.value.input.length; ++index) { + const input = interactiveUiJson.value.input[index]; + + if (!input) { + continue; + } + const interactiveInputValue = interactiveInputValues.value[index]; - if (interactiveInputValue) { + if (interactiveInputValue !== undefined) { inputParameters[input.id] = interactiveInputValue; } - }); + } // Compute the tooltip for this run, keeping in mind that some simulation inputs may not be visible. - const tooltipLines = interactiveUiJson.value.input - .map((input, index) => ({ - input: input, - visible: interactiveShowInput.value[index] - })) - .filter((input) => input.visible) - .map(({ input }) => { - let inputValue: string | number | undefined = inputParameters[input.id]; - - if (locApi.isDiscreteInput(input)) { - const selectedValue = input.possibleValues.find( - (possibleValue) => possibleValue.value === inputParameters[input.id] - ); + const tooltipRows: string[] = []; - if (selectedValue?.name) { - inputValue = selectedValue.name.charAt(0).toLowerCase() + selectedValue.name.slice(1); - } else { - inputValue = inputParameters[input.id]; - } + for (let index = 0; index < interactiveUiJson.value.input.length; ++index) { + if (!interactiveShowInput.value[index]) { + continue; + } + + const input = interactiveUiJson.value.input[index]; + + if (!input) { + continue; + } + + let inputValue: string | number | undefined = inputParameters[input.id]; + + if (locApi.isDiscreteInput(input)) { + const selectedValue = input.possibleValues.find( + (possibleValue) => possibleValue.value === inputParameters[input.id] + ); + + if (selectedValue?.name) { + inputValue = selectedValue.name.charAt(0).toLowerCase() + selectedValue.name.slice(1); + } else { + inputValue = inputParameters[input.id]; } + } - return ` + tooltipRows.push(` ${input.name}: ${inputValue} - `; - }) - .join(''); + `); + } + const tooltip = ` - ${tooltipLines} + ${tooltipRows.join('')}
`; // Determine the colour (of the first trace) by using the next unused colour in the palette unless all the colours // have already been used. - const usedColors = new Set(interactiveRuns.value.map((run) => run.color)); + const usedColors = new Set(); + + for (const run of interactiveRuns.value) { + usedColors.add(run.color); + } + const lastColor = interactiveRuns.value[interactiveRuns.value.length - 1]?.color ?? colors.DEFAULT_COLOR; const lastColorIndex = colors.PALETTE_COLORS.indexOf(lastColor); let color: string = colors.DEFAULT_COLOR; @@ -897,7 +1038,7 @@ const onTrackRun = (): void => { interactiveRuns.value.push({ inputParameters, isVisible: true, - data: interactiveData.value, + data: interactiveLiveData.value, color, tooltip, isLiveRun: false diff --git a/src/renderer/src/components/widgets/GraphPanelWidget.vue b/src/renderer/src/components/widgets/GraphPanelWidget.vue index 433eafd1..3f347e63 100644 --- a/src/renderer/src/components/widgets/GraphPanelWidget.vue +++ b/src/renderer/src/components/widgets/GraphPanelWidget.vue @@ -311,17 +311,19 @@ const exportToCsv = async (): Promise => { const allXValuesEqual = props.data.traces.every((trace) => trace.xValue === firstTrace.xValue); const headerParts: string[] = [allXValuesEqual ? firstTrace.xValue : 'X']; - props.data.traces.forEach((trace) => { + for (const trace of props.data.traces) { headerParts.push(trace.name.replace(/<[^>]*>|,/g, '') || trace.yValue); // Note: we remove any HTML tags and commas to ensure the CSV is well-formed. - }); + } csvLines.push(headerParts.join(',')); // Data rows: collect all unique X values and build value maps. const allXValues = new Set(); - const traceMaps = props.data.traces.map((trace) => { + const traceMaps: Map[] = []; + + for (const trace of props.data.traces) { const map = new Map(); for (let i = 0; i < trace.x.length; ++i) { @@ -335,8 +337,8 @@ const exportToCsv = async (): Promise => { } } - return map; - }); + traceMaps.push(map); + } // Process the rows and update the progress message at regular intervals to keep the UI responsive. @@ -348,11 +350,11 @@ const exportToCsv = async (): Promise => { for (const sortedXValue of sortedXValues) { const rowParts: string[] = [String(sortedXValue)]; - props.data.traces.forEach((_trace, traceIndex) => { + for (let traceIndex = 0; traceIndex < props.data.traces.length; ++traceIndex) { const yValue = traceMaps[traceIndex]?.get(sortedXValue); rowParts.push(yValue !== undefined ? String(yValue) : ''); - }); + } csvLines.push(rowParts.join(',')); @@ -414,21 +416,23 @@ interface IThemeData { const themeData = (): IThemeData => { // Note: the various keys can be found at https://plotly.com/javascript/reference/. + const useLightMode = theme.useLightMode(); + const axisThemeData = (): IAxisThemeData => { return { - zerolinecolor: theme.useLightMode() ? '#94a3b8' : '#71717a', // --p-surface-400 / --p-surface-500 - gridcolor: theme.useLightMode() ? '#e2e8f0' : '#3f3f46', // --p-surface-200 / --p-surface-700 + zerolinecolor: useLightMode ? '#94a3b8' : '#71717a', // --p-surface-400 / --p-surface-500 + gridcolor: useLightMode ? '#e2e8f0' : '#3f3f46', // --p-surface-200 / --p-surface-700 minor: { - gridcolor: theme.useLightMode() ? '#f1f5f9' : '#27272a' // --p-surface-100 / --p-surface-800 + gridcolor: useLightMode ? '#f1f5f9' : '#27272a' // --p-surface-100 / --p-surface-800 } }; }; return { - paper_bgcolor: theme.useLightMode() ? '#ffffff' : '#18181b', // --p-content-background - plot_bgcolor: theme.useLightMode() ? '#ffffff' : '#18181b', // --p-content-background + paper_bgcolor: useLightMode ? '#ffffff' : '#18181b', // --p-content-background + plot_bgcolor: useLightMode ? '#ffffff' : '#18181b', // --p-content-background font: { - color: theme.useLightMode() ? '#334155' : '#ffffff' // --p-text-color + color: useLightMode ? '#334155' : '#ffffff' // --p-text-color }, colorway: colors.PALETTE_COLORS, xaxis: axisThemeData(), @@ -596,10 +600,8 @@ const updatePlot = (): void => { const traces = props.data.traces.map((trace) => ({ ...trace, - ...{ - line: { color: trace.color }, - legendrank: trace.zorder - } + line: { color: trace.color }, + legendrank: trace.zorder })); dependencies._plotlyJs diff --git a/src/renderer/src/components/widgets/InputWidget.vue b/src/renderer/src/components/widgets/InputWidget.vue index ea20c790..39b1044c 100644 --- a/src/renderer/src/components/widgets/InputWidget.vue +++ b/src/renderer/src/components/widgets/InputWidget.vue @@ -45,7 +45,7 @@ let oldValue = value.value; const discreteValue = vue.ref( props.possibleValues?.find((possibleValue) => possibleValue.value === value.value) ); -const compStepValue = vue.computed(() => { +const compStepValue = vue.computed(() => { if (props.stepValue !== undefined) { return props.stepValue; }