diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index f859685105..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,6 +0,0 @@ -*.css -*.md -*.json -packages/adapters/config/webpack/*.js -packages/docs/*.js -packages/tools/src/utilities/segmentation/growCut/runGrowCut.ts diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index a52bf493f6..0000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,156 +0,0 @@ -// This config is a little messy because it needs to work -// for both our library and our example app in /examples. -// A good "@TODO" item would be to separate the linting/bundling -// for them. -{ - "root": true, - "parser": "@typescript-eslint/parser", - - // eslint-plugin-prettier - // - prettier - // - // @typescript-eslint/eslint-plugin - // - @typescript-eslint - // - // - // - import - "plugins": [ - "@typescript-eslint", - "import", - "eslint-plugin-tsdoc", - "prettier" - ], - - // Order matters here. Last will override - // eslint-config-prettier (only turns rules off) - // - prettier # eslint conflicts - // - prettier/react # react conflicts - // - prettier/@typescript-eslint # typescript conflicts - // - // eslint-plugin-prettier - // - plugin:prettier/recommended - // - // eslint-plugin-react - // - plugin:react/recommended - // - // eslint-plugin-react-hooks - // - plugin:react-hooks/recommended - // - // @typescript-eslint/eslint-plugin - // - - "extends": [ - "eslint:recommended", - "plugin:import/errors", - "plugin:import/warnings", - "plugin:import/typescript", - "plugin:@typescript-eslint/recommended", - // sets up the plugin AND eslint-config-prettier - "plugin:prettier/recommended" - ], - - "parserOptions": { - "ecmaVersion": 2022, - "sourceType": "module", - "project": "./tsconfig.json", - "tsconfigRootDir": "./", - "warnOnUnsupportedTypeScriptVersion": false - }, - "ignorePatterns": [ - "packages/docs", - "dist", - "**/*_test.js", - "**/*_jest.js", - "**/*babel*", - "**/*.d.ts", - "**/*_types.ts" - ], - "rules": { - "import/no-cycle": ["error", { "maxDepth": 15 }], - // Enforce consistent brace style for all control statements for readability - "curly": "error", - "tsdoc/syntax": "off", - "@typescript-eslint/ban-ts-comment": "off", - "no-console": "off", - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/consistent-type-exports": "error", - "@typescript-eslint/no-unsafe-function-type": "off", - "@typescript-eslint/no-empty-object-type": "off", - "@typescript-eslint/no-unused-expressions": "warn", - "@typescript-eslint/consistent-type-imports": [ - "error", - { - "prefer": "type-imports", - "disallowTypeAnnotations": true - } - ], - "@typescript-eslint/no-import-type-side-effects": "error", - - "import/extensions": [ - "error", - "ignorePackages", - { - "js": "never", - "jsx": "never", - "ts": "never", - "tsx": "never" - } - ], - "import/prefer-default-export": "off", - "prettier/prettier": [ - "error", - { - "endOfLine": "auto", - "printWidth": 80 - } - ], - "no-undef": "warn", - "import/no-named-as-default": "off", - "import/no-named-as-default-member": "off" - }, - "overrides": [ - { - "files": ["*.ts", "*.tsx"], // Apply the rules to TypeScript files - "rules": { - "import/no-cycle": ["error", { "maxDepth": 15 }] - } - }, - { - "files": ["**/examples/**"], - "rules": { - "@typescript-eslint/no-explicit-any": "off" // Disable the rule for files in the examples directory - } - } - ], - "env": { - "es6": true, - "browser": true, - "node": true - }, - "settings": { - "import/resolver": { - "node": { - "extensions": [".js", ".jsx", ".ts", ".tsx"] - }, - "typescript": { - "project": "./tsconfig.json" - } - } - }, - "globals": { - "context": true, - "jasmine": true, - "es6": true, - "browser": true, - "node": true, - "afterEach": true, - "beforeEach": true, - "done": true, - "describe": "readonly", - "beforeAll": "readonly", - "it": "readonly", - "jest": "readonly", - "expect": "readonly", - "assert": true - } -} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index df00a4bbb2..5f7181b5bf 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -72,8 +72,6 @@ after the commits are squashed. - [] My code has been well-documented (function documentation, inline comments, etc.) - - #### Public Documentation Updates diff --git a/.github/workflows/docusaurus-build.yml b/.github/workflows/docusaurus-build.yml new file mode 100644 index 0000000000..db1311cbb2 --- /dev/null +++ b/.github/workflows/docusaurus-build.yml @@ -0,0 +1,65 @@ +name: Docusaurus Build + +on: + pull_request: + branches: [ main ] + +env: + ACTIONS_STEP_DEBUG: true + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + docusaurus-build: + timeout-minutes: 30 + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.18.1' + cache: 'npm' + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install root dependencies + run: bun install --frozen-lockfile + + - name: reset nx cache + run: rm -rf .nx/cache + + - name: reset nx cache + run: bun nx reset + + - name: Build packages in ESM format + run: bun run build:esm + env: + NX_CACHE_DIRECTORY: ${{ runner.temp }}/nx-cache + NX_DAEMON: false + + - name: Install docs dependencies + run: cd packages/docs && bun install + + - name: Build Docusaurus documentation + run: cd packages/docs && bun run build:ci + env: + NODE_OPTIONS: --max_old_space_size=32384 + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: docusaurus-build-${{ github.sha }} + path: packages/docs/build/ + retention-days: 7 diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index 4a1b3c1402..22411a3351 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -11,7 +11,7 @@ on: branches: [ beta ] jobs: - format-check: + lint: timeout-minutes: 30 runs-on: ubuntu-latest @@ -33,10 +33,8 @@ jobs: - name: Install dependencies run: bun install - - name: Build dependencies - run: bun run build:esm + - name: Run lint + run: bun run lint - name: Run format check run: bun run format-check - env: - NODE_OPTIONS: --max_old_space_size=32896 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index f8bf432e2d..434f499c3c 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,7 +1,7 @@ name: Playwright Tests on: pull_request: - branches: [main, master] + branches: [main, master, beta] concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.gitignore b/.gitignore index f904defb14..1ec3097c4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # dependencies +CLAUDE.md node_modules .nyc_output .nx @@ -63,3 +64,6 @@ coverage/ .cursor **/.claude/settings.local.json +CLAUDE.md +# External repositories +vtk-js/ diff --git a/.husky/pre-commit b/.husky/pre-commit index 84603bca5f..3723623171 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1 @@ -# .husky/pre-commit - yarn lint-staged diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000000..369cef1e94 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,12 @@ +{ + "plugins": ["typescript", "import"], + "ignorePatterns": [ + "utils/**", + "**/examples/**", + "packages/dicomImageLoader/src/types/codec*" + ], + "rules": { + "typescript/no-explicit-any": "error", + "import/no-cycle": "error" + } +} diff --git a/.prettierrc b/.prettierrc index cfb47e1096..e39ec15e72 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,5 +5,6 @@ "tabWidth": 2, "semi": true, "singleQuote": true, - "arrowParens": "always" + "arrowParens": "always", + "plugins": ["@prettier/plugin-oxc"] } diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 0ceb70df6f..02d44498a1 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,7 @@ { - "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "PolyMeilex.wgsl"] + "recommendations": [ + "esbenp.prettier-vscode", + "PolyMeilex.wgsl", + "oxc.oxc-vscode" + ] } diff --git a/README.md b/README.md index 246b19b31a..e30d692545 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,7 @@ Cornerstone is a set of JavaScript libraries that can be used to build web-based ## Documentation -You can find the Cornerstone documentation [on the website](https://cornerstonejs.org/). - -The documentation is divided into several sections +You can find the Cornerstone documentation [on the website](https://cornerstonejs.org/). The documentation is divided into several sections - [Tutorial](https://cornerstonejs.org/docs/category/tutorials) - [Main Concepts](https://cornerstonejs.org/docs/category/concepts) diff --git a/addOns/README.md b/addOns/README.md index 6a102d6968..982b68bdab 100644 --- a/addOns/README.md +++ b/addOns/README.md @@ -1,8 +1,7 @@ -# External Dependencies - -This module contains optional and external dependencies for including in OHIF, such as the DICOM Microscopy Viewer component. - - -# External Components - -This directory contains various external components. These can be fetched in various ways such as NPM dependencies or by fetching files externally. +# External Dependencies + +This module contains optional and external dependencies for including in OHIF, such as the DICOM Microscopy Viewer component. + +# External Components + +This directory contains various external components. These can be fetched in various ways such as NPM dependencies or by fetching files externally. diff --git a/addOns/externals/dicom-microscopy-viewer/package.json b/addOns/externals/dicom-microscopy-viewer/package.json index acd9175e76..c8318274e8 100644 --- a/addOns/externals/dicom-microscopy-viewer/package.json +++ b/addOns/externals/dicom-microscopy-viewer/package.json @@ -9,7 +9,7 @@ "yarn": ">=1.19.1" }, "dependencies": { - "dicom-microscopy-viewer": "^0.46.1" + "dicom-microscopy-viewer": "^0.48.6" }, "scripts": { "build": "echo addOns externals dependency" diff --git a/bun.lock b/bun.lock index c724814294..9907ba0571 100644 --- a/bun.lock +++ b/bun.lock @@ -3,9 +3,11 @@ "workspaces": { "": { "name": "root", + "dependencies": { + "caniuse-lite": "^1.0.30001734", + }, "devDependencies": { "@babel/core": "^7.21.8", - "@babel/eslint-parser": "^7.19.1", "@babel/plugin-external-helpers": "^7.18.6", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-object-rest-spread": "^7.14.7", @@ -21,6 +23,7 @@ "@microsoft/api-extractor": "7.49.2", "@microsoft/tsdoc": "^0.15.0", "@playwright/test": "^1.51.1", + "@prettier/plugin-oxc": "^0.0.4", "@rollup/plugin-babel": "^6.0.3", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.2", @@ -35,8 +38,6 @@ "@types/node": "^20.14.8", "@types/react": "^17.0.58", "@types/react-dom": "^17.0.20", - "@typescript-eslint/eslint-plugin": "^8.1.0", - "@typescript-eslint/parser": "^8.1.0", "@webgpu/types": "^0.1.40", "acorn": "^8.8.2", "acorn-jsx": "^5.3.2", @@ -56,23 +57,16 @@ "cross-env": "^7.0.3", "css-loader": "^6.7.3", "cssnano": "^6.0.1", + "dicomweb-client": "^0.11.2", "docdash": "^1.2.0", "dpdm": "^3.14.0", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsx-a11y": "6.7.1", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-tsdoc": "^0.2.17", - "eslint-webpack-plugin": "^4.0.1", "execa": "^7.2.0", "exports-loader": "^3.0.0", "file-loader": "^6.2.0", "follow-redirects": "^1.15.2", "fs-extra": "^10.0.0", "html-webpack-plugin": "^5.5.1", - "husky": "^9.1.4", + "husky": "^9.1.7", "jasmine": "^4.6.0", "jest": "^29.7.0", "jest-canvas-mock": "^2.5.2", @@ -94,13 +88,14 @@ "netlify-cli": "^17.34.1", "nyc": "^17.1.0", "open-cli": "^7.0.1", + "oxlint": "^1.9.0", "path-browserify": "^1.0.1", "playwright-test-coverage": "^1.2.12", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-loader": "^7.3.0", "postcss-preset-env": "^8.3.2", - "prettier": "^2.8.8", + "prettier": "^3.6.2", "puppeteer": "^13.5.0", "resemblejs": "^5.0.0", "rollup": "^3.21.3", @@ -131,7 +126,7 @@ "name": "@externals/dicom-microscopy-viewer", "version": "2.1.10", "dependencies": { - "dicom-microscopy-viewer": "^0.46.1", + "dicom-microscopy-viewer": "^0.48.6", }, }, "packages/adapters": { @@ -140,7 +135,7 @@ "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", - "dcmjs": "^0.42.0", + "dcmjs": "^0.43.1", "gl-matrix": "^3.4.3", "ndarray": "^1.0.19", }, @@ -155,7 +150,7 @@ "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", - "dcmjs": "^0.42.0", + "dcmjs": "^0.43.1", "gl-matrix": "^3.4.3", "lodash.clonedeep": "^4.5.0", "ndarray": "^1.0.19", @@ -265,8 +260,6 @@ "@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="], - "@babel/eslint-parser": ["@babel/eslint-parser@7.26.10", "", { "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.11.0", "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" } }, "sha512-QsfQZr4AiLpKqn7fz+j7SN+f43z2DZCgGyYbNJ2vJOqKfG4E6MZer1+jqGZqKJaxq/gdO2DC/nUu45+pOL5p2Q=="], - "@babel/generator": ["@babel/generator@7.26.10", "", { "dependencies": { "@babel/parser": "^7.26.10", "@babel/types": "^7.26.10", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang=="], "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="], @@ -661,14 +654,6 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.2", "", { "os": "win32", "cpu": "x64" }, "sha512-VEfTCZicoZnZ6sGkjFPGRFFJuL2fZn2bLhsekZl1CJslflp2cJS/VoKs1jMk+3pDfsGW6CfQVUckP707HwbXeQ=="], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.5.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], - - "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], - "@externals/dicom-microscopy-viewer": ["@externals/dicom-microscopy-viewer@workspace:addOns/externals/dicom-microscopy-viewer"], "@fastify/accept-negotiator": ["@fastify/accept-negotiator@1.1.0", "", {}, "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ=="], @@ -685,14 +670,8 @@ "@fastify/static": ["@fastify/static@7.0.4", "", { "dependencies": { "@fastify/accept-negotiator": "^1.0.0", "@fastify/send": "^2.0.0", "content-disposition": "^0.5.3", "fastify-plugin": "^4.0.0", "fastq": "^1.17.0", "glob": "^10.3.4" } }, "sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q=="], - "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - "@humanwhocodes/momoa": ["@humanwhocodes/momoa@2.0.4", "", {}, "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA=="], - "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], - "@hutson/parse-repository-url": ["@hutson/parse-repository-url@3.0.2", "", {}, "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q=="], "@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="], @@ -781,16 +760,8 @@ "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="], - "@mapbox/jsonlint-lines-primitives": ["@mapbox/jsonlint-lines-primitives@2.0.2", "", {}, "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ=="], - - "@mapbox/mapbox-gl-style-spec": ["@mapbox/mapbox-gl-style-spec@13.28.0", "", { "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/point-geometry": "^0.1.0", "@mapbox/unitbezier": "^0.0.0", "csscolorparser": "~1.0.2", "json-stringify-pretty-compact": "^2.0.0", "minimist": "^1.2.6", "rw": "^1.3.3", "sort-object": "^0.3.2" }, "bin": { "gl-style-format": "bin/gl-style-format.js", "gl-style-migrate": "bin/gl-style-migrate.js", "gl-style-validate": "bin/gl-style-validate.js", "gl-style-composite": "bin/gl-style-composite.js" } }, "sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg=="], - "@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@1.0.11", "", { "dependencies": { "detect-libc": "^2.0.0", "https-proxy-agent": "^5.0.0", "make-dir": "^3.1.0", "node-fetch": "^2.6.7", "nopt": "^5.0.0", "npmlog": "^5.0.1", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.11" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ=="], - "@mapbox/point-geometry": ["@mapbox/point-geometry@0.1.0", "", {}, "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ=="], - - "@mapbox/unitbezier": ["@mapbox/unitbezier@0.0.0", "", {}, "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA=="], - "@microsoft/api-extractor": ["@microsoft/api-extractor@7.49.2", "", { "dependencies": { "@microsoft/api-extractor-model": "7.30.3", "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", "@rushstack/node-core-library": "5.11.0", "@rushstack/rig-package": "0.5.3", "@rushstack/terminal": "0.14.6", "@rushstack/ts-command-line": "4.23.4", "lodash": "~4.17.15", "minimatch": "~3.0.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", "typescript": "5.7.2" }, "bin": { "api-extractor": "bin/api-extractor" } }, "sha512-DI/WnvhbkHcucxxc4ys00ejCiViFls5EKPrEfe4NV3GGpVkoM5ZXF61HZNSGA8IG0oEV4KfTqIa59Rc3wdMopw=="], "@microsoft/api-extractor-model": ["@microsoft/api-extractor-model@7.30.3", "", { "dependencies": { "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", "@rushstack/node-core-library": "5.11.0" } }, "sha512-yEAvq0F78MmStXdqz9TTT4PZ05Xu5R8nqgwI5xmUmQjWBQ9E6R2n8HB/iZMRciG4rf9iwI2mtuQwIzDXBvHn1w=="], @@ -883,16 +854,12 @@ "@netlify/zip-it-and-ship-it": ["@netlify/zip-it-and-ship-it@9.42.1", "", { "dependencies": { "@babel/parser": "^7.22.5", "@babel/types": "7.26.3", "@netlify/binary-info": "^1.0.0", "@netlify/serverless-functions-api": "^1.31.1", "@vercel/nft": "0.27.7", "archiver": "^7.0.0", "common-path-prefix": "^3.0.0", "cp-file": "^10.0.0", "es-module-lexer": "^1.0.0", "esbuild": "0.19.11", "execa": "^6.0.0", "fast-glob": "^3.3.2", "filter-obj": "^5.0.0", "find-up": "^6.0.0", "glob": "^8.0.3", "is-builtin-module": "^3.1.0", "is-path-inside": "^4.0.0", "junk": "^4.0.0", "locate-path": "^7.0.0", "merge-options": "^3.0.4", "minimatch": "^9.0.0", "normalize-path": "^3.0.0", "p-map": "^5.0.0", "path-exists": "^5.0.0", "precinct": "^11.0.0", "require-package-name": "^2.0.1", "resolve": "^2.0.0-next.1", "semver": "^7.3.8", "tmp-promise": "^3.0.2", "toml": "^3.0.0", "unixify": "^1.0.0", "urlpattern-polyfill": "8.0.2", "yargs": "^17.0.0", "zod": "^3.23.8" }, "bin": { "zip-it-and-ship-it": "bin.js" } }, "sha512-ZCGM2OnLbiFOZO+kpODI6BKjH6X4a6vE/tJd0aqIvKWiygZgxhIw5APZUzgwLGv4BahIBG+tcfKgW7krpZYLwA=="], - "@nicolo-ribaudo/eslint-scope-5-internals": ["@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1", "", { "dependencies": { "eslint-scope": "5.1.1" } }, "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], - "@npmcli/agent": ["@npmcli/agent@2.2.2", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og=="], "@npmcli/arborist": ["@npmcli/arborist@7.5.4", "", { "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^3.1.1", "@npmcli/installed-package-contents": "^2.1.0", "@npmcli/map-workspaces": "^3.0.2", "@npmcli/metavuln-calculator": "^7.1.1", "@npmcli/name-from-folder": "^2.0.0", "@npmcli/node-gyp": "^3.0.0", "@npmcli/package-json": "^5.1.0", "@npmcli/query": "^3.1.0", "@npmcli/redact": "^2.0.0", "@npmcli/run-script": "^8.1.0", "bin-links": "^4.0.4", "cacache": "^18.0.3", "common-ancestor-path": "^1.0.1", "hosted-git-info": "^7.0.2", "json-parse-even-better-errors": "^3.0.2", "json-stringify-nice": "^1.1.4", "lru-cache": "^10.2.2", "minimatch": "^9.0.4", "nopt": "^7.2.1", "npm-install-checks": "^6.2.0", "npm-package-arg": "^11.0.2", "npm-pick-manifest": "^9.0.1", "npm-registry-fetch": "^17.0.1", "pacote": "^18.0.6", "parse-conflict-json": "^3.0.0", "proc-log": "^4.2.0", "proggy": "^2.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^3.0.1", "read-package-json-fast": "^3.0.2", "semver": "^7.3.7", "ssri": "^10.0.6", "treeverse": "^3.0.0", "walk-up-path": "^3.0.1" }, "bin": { "arborist": "bin/index.js" } }, "sha512-nWtIc6QwwoUORCRNzKx4ypHqCk3drI+5aeYdMTQQiRCcn4lOOgfQh7WyZobGYTxXPSq1VwV53lkpN/BRlRk08g=="], @@ -979,6 +946,54 @@ "@opentelemetry/api": ["@opentelemetry/api@1.8.0", "", {}, "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w=="], + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.74.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lgq8TJq22eyfojfa2jBFy2m66ckAo7iNRYDdyn9reXYA3I6Wx7tgGWVx1JAp1lO+aUiqdqP/uPlDaETL9tqRcg=="], + + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.74.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xbY/io/hkARggbpYEMFX6CwFzb7f4iS6WuBoBeZtdqRWfIEi7sm/uYWXfyVeB8uqOATvJ07WRFC2upI8PSI83g=="], + + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.74.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-FIj2gAGtFaW0Zk+TnGyenMUoRu1ju+kJ/h71D77xc1owOItbFZFGa+4WSVck1H8rTtceeJlK+kux+vCjGFCl9Q=="], + + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.74.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-W1I+g5TJg0TRRMHgEWNWsTIfe782V3QuaPgZxnfPNmDMywYdtlzllzclBgaDq6qzvZCCQc/UhvNb37KWTCTj8A=="], + + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.74.0", "", { "os": "linux", "cpu": "arm" }, "sha512-gxqkyRGApeVI8dgvJ19SYe59XASW3uVxF1YUgkE7peW/XIg5QRAOVTFKyTjI9acYuK1MF6OJHqx30cmxmZLtiQ=="], + + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.74.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jpnAUP4Fa93VdPPDzxxBguJmldj/Gpz7wTXKFzpAueqBMfZsy9KNC+0qT2uZ9HGUDMzNuKw0Se3bPCpL/gfD2Q=="], + + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.74.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-fcWyM7BNfCkHqIf3kll8fJctbR/PseL4RnS2isD9Y3FFBhp4efGAzhDaxIUK5GK7kIcFh1P+puIRig8WJ6IMVQ=="], + + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.74.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AMY30z/C77HgiRRJX7YtVUaelKq1ex0aaj28XoJu4SCezdS8i0IftUNTtGS1UzGjGZB8zQz5SFwVy4dRu4GLwg=="], + + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.74.0", "", { "os": "linux", "cpu": "none" }, "sha512-/RZAP24TgZo4vV/01TBlzRqs0R7E6xvatww4LnmZEBBulQBU/SkypDywfriFqWuFoa61WFXPV7sLcTjJGjim/w=="], + + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.74.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-620J1beNAlGSPBD+Msb3ptvrwxu04B8iULCH03zlf0JSLy/5sqlD6qBs0XUVkUJv1vbakUw1gfVnUQqv0UTuEg=="], + + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.74.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WBFgQmGtFnPNzHyLKbC1wkYGaRIBxXGofO0+hz1xrrkPgbxbJS1Ukva1EB8sPaVBBQ52Bdc2GjLSp721NWRvww=="], + + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.74.0", "", { "os": "linux", "cpu": "x64" }, "sha512-y4mapxi0RGqlp3t6Sm+knJlAEqdKDYrEue2LlXOka/F2i4sRN0XhEMPiSOB3ppHmvK4I2zY2XBYTsX1Fel0fAg=="], + + "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.74.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-yDS9bRDh5ymobiS2xBmjlrGdUuU61IZoJBaJC5fELdYT5LJNBXlbr3Yc6m2PWfRJwkH6Aq5fRvxAZ4wCbkGa8w=="], + + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.74.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-XFWY52Rfb4N5wEbMCTSBMxRkDLGbAI9CBSL24BIDywwDJMl31gHEVlmHdCDRoXAmanCI6gwbXYTrWe0HvXJ7Aw=="], + + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.74.0", "", { "os": "win32", "cpu": "x64" }, "sha512-1D3x6iU2apLyfTQHygbdaNbX3nZaHu4yaXpD7ilYpoLo7f0MX0tUuoDrqJyJrVGqvyXgc0uz4yXz9tH9ZZhvvg=="], + + "@oxc-project/types": ["@oxc-project/types@0.74.0", "", {}, "sha512-KOw/RZrVlHGhCXh1RufBFF7Nuo7HdY5w1lRJukM/igIl6x9qtz8QycDvZdzb4qnHO7znrPyo2sJrFJK2eKHgfQ=="], + + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.9.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VRxI/T0I4bq+xoaI0qNFeGPxOOganHlfLmx8JbFFZswoxMkm5lIvVYScDKLrsbbPSe4bcZ5v1DmX5sNGQ619Uw=="], + + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.9.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-vMBa3eNrJMSBApXvsx6FgMuWCnNE+ETJJieLPhemZKctMNWOJQX+2i09CG2kb1IkmxagLapH7dZ4i0+Lf13mqg=="], + + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.9.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-8wnMjloRbz7hPKvcgqd8HKNgkEgFJZvp9Los2pdE1CKh33msIxIXPGT3KKhYzyrtBaRaG2LJHshUPub02Q+x9A=="], + + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.9.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-elj9FTNXvq9oP8UedeHHq2R8lCtTmBGvUO/xLez734zVx3tvBl3avmuEfPBqR4wlUKDANExDh3Rg0pssflhw9w=="], + + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.9.0", "", { "os": "linux", "cpu": "x64" }, "sha512-d7VjQttKDgXKIYY3tm2GXTD83dyI+D8POGdPRT8GjhitHKasHQWFMBDG5iLp60FNc5Ky1cIe/2nwKW9ReQvhKw=="], + + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.9.0", "", { "os": "linux", "cpu": "x64" }, "sha512-R9vi5LGxNNi3pisp4dG7Ez00mAvCeki8VI5DUg6W1MR0GTnaejlVtMflziA2jqpdTGOK4bjLDsHKIadGC1SMVg=="], + + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.9.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-TiV7xYBMhkc/r0X+c9cMwGKUWYJRlWnI61m8powqer6lMJ8VfDe1RkRF4ttO5wlPGkiBwBa3vYWAgxaXc9JAMQ=="], + + "@oxlint/win32-x64": ["@oxlint/win32-x64@1.9.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OWqqkXsLrpS1uQsxjNqiOkC9a/CszMLa3VwlRcpm/z3iPxEL/qEVjGfjZX6XZw8Q6YukFB77rgYqzotvtCvI1A=="], + "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="], "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="], @@ -1025,6 +1040,8 @@ "@polka/url": ["@polka/url@1.0.0-next.28", "", {}, "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw=="], + "@prettier/plugin-oxc": ["@prettier/plugin-oxc@0.0.4", "", { "dependencies": { "oxc-parser": "0.74.0" } }, "sha512-UGXe+g/rSRbglL0FOJiar+a+nUrst7KaFmsg05wYbKiInGWP6eAj/f8A2Uobgo5KxEtb2X10zeflNH6RK2xeIQ=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], @@ -1085,8 +1102,6 @@ "@rspack/lite-tapable": ["@rspack/lite-tapable@1.0.1", "", {}, "sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w=="], - "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], - "@rushstack/node-core-library": ["@rushstack/node-core-library@5.11.0", "", { "dependencies": { "ajv": "~8.13.0", "ajv-draft-04": "~1.0.0", "ajv-formats": "~3.0.1", "fs-extra": "~11.3.0", "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", "semver": "~7.5.4" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-I8+VzG9A0F3nH2rLpPd7hF8F7l5Xb7D+ldrWVZYegXM6CsKkvWc670RlgK3WX8/AseZfXA/vVrh0bpXe2Y2UDQ=="], "@rushstack/rig-package": ["@rushstack/rig-package@0.5.3", "", { "dependencies": { "resolve": "~1.22.1", "strip-json-comments": "~3.1.1" } }, "sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow=="], @@ -1231,8 +1246,6 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - "@types/karma": ["@types/karma@6.3.9", "", { "dependencies": { "@types/node": "*", "log4js": "^6.4.1" } }, "sha512-sjE/MHnoAZAQYAKRXAbjTOiBKyGGErEM725bruRcmDdMa2vp1bjWPhApI7/i564PTyHlzc3vIGXLL6TFIpAxFg=="], "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], @@ -1263,6 +1276,8 @@ "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + "@types/rbush": ["@types/rbush@4.0.0", "", {}, "sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ=="], + "@types/react": ["@types/react@17.0.83", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "^0.16", "csstype": "^3.0.2" } }, "sha512-l0m4ArKJvmFtR4e8UmKrj1pB4tUgOhJITf+mADyF/p69Ts1YAR/E+G9XEM0mHXKVRa1dQNHseyyDNzeuAXfXQw=="], "@types/react-dom": ["@types/react-dom@17.0.26", "", { "peerDependencies": { "@types/react": "^17.0.0" } }, "sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg=="], @@ -1299,46 +1314,14 @@ "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.26.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.26.1", "@typescript-eslint/type-utils": "8.26.1", "@typescript-eslint/utils": "8.26.1", "@typescript-eslint/visitor-keys": "8.26.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.26.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.26.1", "@typescript-eslint/types": "8.26.1", "@typescript-eslint/typescript-estree": "8.26.1", "@typescript-eslint/visitor-keys": "8.26.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.26.1", "", { "dependencies": { "@typescript-eslint/types": "8.26.1", "@typescript-eslint/visitor-keys": "8.26.1" } }, "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg=="], + "@typescript-eslint/types": ["@typescript-eslint/types@5.62.0", "", {}, "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.26.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.26.1", "@typescript-eslint/utils": "8.26.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "semver": "^7.3.7", "tsutils": "^3.21.0" } }, "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.26.1", "", {}, "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.26.1", "", { "dependencies": { "@typescript-eslint/types": "8.26.1", "@typescript-eslint/visitor-keys": "8.26.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.26.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.26.1", "@typescript-eslint/types": "8.26.1", "@typescript-eslint/typescript-estree": "8.26.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.26.1", "", { "dependencies": { "@typescript-eslint/types": "8.26.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" } }, "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@unrs/rspack-resolver-binding-darwin-arm64": ["@unrs/rspack-resolver-binding-darwin-arm64@1.1.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bQx2L40UF5XxsXwkD26PzuspqUbUswWVbmclmUC+c83Cv/EFrFJ1JaZj5Q5jyYglKGOtyIWY/hXTCdWRN9vT0Q=="], - - "@unrs/rspack-resolver-binding-darwin-x64": ["@unrs/rspack-resolver-binding-darwin-x64@1.1.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-dMi9a7//BsuPTnhWEDxmdKZ6wxQlPnAob8VSjefGbKX/a+pHfTaX1pm/jv2VPdarP96IIjCKPatJS/TtLQeGQA=="], - - "@unrs/rspack-resolver-binding-freebsd-x64": ["@unrs/rspack-resolver-binding-freebsd-x64@1.1.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-RiBZQ+LSORQObfhV1yH7jGz+4sN3SDYtV53jgc8tUVvqdqVDaUm1KA3zHLffmoiYNGrYkE3sSreGC+FVpsB4Vg=="], - - "@unrs/rspack-resolver-binding-linux-arm-gnueabihf": ["@unrs/rspack-resolver-binding-linux-arm-gnueabihf@1.1.2", "", { "os": "linux", "cpu": "arm" }, "sha512-IyKIFBtOvuPCJt1WPx9e9ovTGhZzrIbW11vWzw4aPmx3VShE+YcMpAldqQubdCep0UVKZyFt+2hQDQZwFiJ4jg=="], - - "@unrs/rspack-resolver-binding-linux-arm64-gnu": ["@unrs/rspack-resolver-binding-linux-arm64-gnu@1.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-RfYtlCtJrv5i6TO4dSlpbyOJX9Zbhmkqrr9hjDfr6YyE5KD0ywLRzw8UjXsohxG1XWgRpb2tvPuRYtURJwbqWg=="], - - "@unrs/rspack-resolver-binding-linux-arm64-musl": ["@unrs/rspack-resolver-binding-linux-arm64-musl@1.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-MaITzkoqsn1Rm3+YnplubgAQEfOt+2jHfFvuFhXseUfcfbxe8Zyc3TM7LKwgv7mRVjIl+/yYN5JqL0cjbnhAnQ=="], - - "@unrs/rspack-resolver-binding-linux-x64-gnu": ["@unrs/rspack-resolver-binding-linux-x64-gnu@1.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Nu981XmzQqis/uB3j4Gi3p5BYCd/zReU5zbJmjMrEH7IIRH0dxZpdOmS/+KwEk6ao7Xd8P2D2gDHpHD/QTp0aQ=="], - - "@unrs/rspack-resolver-binding-linux-x64-musl": ["@unrs/rspack-resolver-binding-linux-x64-musl@1.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-xJupeDvaRpV0ADMuG1dY9jkOjhUzTqtykvchiU2NldSD+nafSUcMWnoqzNUx7HGiqbTMOw9d9xT8ZiFs+6ZFyQ=="], - - "@unrs/rspack-resolver-binding-wasm32-wasi": ["@unrs/rspack-resolver-binding-wasm32-wasi@1.1.2", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.7" }, "cpu": "none" }, "sha512-un6X/xInks+KEgGpIHFV8BdoODHRohaDRvOwtjq+FXuoI4Ga0P6sLRvf4rPSZDvoMnqUhZtVNG0jG9oxOnrrLQ=="], - - "@unrs/rspack-resolver-binding-win32-arm64-msvc": ["@unrs/rspack-resolver-binding-win32-arm64-msvc@1.1.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-2lCFkeT1HYUb/OOStBS1m67aZOf9BQxRA+Wf/xs94CGgzmoQt7H4V/BrkB/GSGKsudXjkiwt2oHNkHiowAS90A=="], - - "@unrs/rspack-resolver-binding-win32-x64-msvc": ["@unrs/rspack-resolver-binding-win32-x64-msvc@1.1.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EYfya5HCQ/8Yfy7rvAAX2rGytu81+d/CIhNCbZfNKLQ690/qFsdEeTXRsMQW1afHoluMM50PsjPYu8ndy8fSQg=="], - "@vercel/nft": ["@vercel/nft@0.27.7", "", { "dependencies": { "@mapbox/node-pre-gyp": "^1.0.11", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "micromatch": "^4.0.8", "node-gyp-build": "^4.2.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-FG6H5YkP4bdw9Ll1qhmbxuE8KwW2E/g8fJpM183fWQLeVDGqzeywMIeJ9h2txdWZ03psgWMn6QymTxaDLmdwUg=="], "@web3-storage/car-block-validator": ["@web3-storage/car-block-validator@1.2.2", "", { "dependencies": { "@multiformats/blake2": "^2.0.2", "@multiformats/murmur3": "^2.1.8", "@multiformats/sha3": "^3.0.2", "multiformats": "^13.3.1", "uint8arrays": "^5.1.0" } }, "sha512-lR9l+ZszhTid5HfZE8ohnGf2RJp2kaBOnoejmsACs3iTNiy+3K09dnPm8MhgBE9RCIgPBKM0CCWXO9l+I6jrKA=="], @@ -1483,32 +1466,18 @@ "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - - "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], - "array-differ": ["array-differ@3.0.0", "", {}, "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg=="], "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], "array-ify": ["array-ify@1.0.0", "", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="], - "array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="], - "array-timsort": ["array-timsort@1.0.3", "", {}, "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ=="], "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], "array-uniq": ["array-uniq@1.0.3", "", {}, "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q=="], - "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], - - "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], - - "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], - - "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], - "arrify": ["arrify@2.0.1", "", {}, "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="], "ascii-table": ["ascii-table@0.0.9", "", {}, "sha512-xpkr6sCDIYTPqzvjG8M3ncw1YOTaloWZOyrUmicoEifBEKzQzt+ooUpRpQ/AbOoJfO/p2ZKiyp79qHThzJDulQ=="], @@ -1521,14 +1490,10 @@ "ast-module-types": ["ast-module-types@5.0.0", "", {}, "sha512-JvqziE0Wc0rXQfma0HZC/aY7URXHFuZV84fJRtP8u+lhp0JYCNd5wJzVXP45t0PH0Mej3ynlzvdyITYIu0G4LQ=="], - "ast-types-flow": ["ast-types-flow@0.0.7", "", {}, "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag=="], - "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], "async": ["async@1.5.2", "", {}, "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w=="], - "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], - "async-sema": ["async-sema@3.1.1", "", {}, "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], @@ -1539,20 +1504,14 @@ "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="], - "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "avvio": ["avvio@8.4.0", "", { "dependencies": { "@fastify/error": "^3.3.0", "fastq": "^1.17.1" } }, "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA=="], "aws-sign2": ["aws-sign2@0.7.0", "", {}, "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA=="], "aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="], - "axe-core": ["axe-core@4.10.3", "", {}, "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg=="], - "axios": ["axios@1.8.3", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A=="], - "axobject-query": ["axobject-query@3.2.4", "", {}, "sha512-aPTElBrbifBU1krmZxGZOlBkslORe7Ll7+BDnI50Wy4LgOt69luMgevkDfTq1O/ZgprooPCtWpjCwKSZw/iZ4A=="], - "b4a": ["b4a@1.6.7", "", {}, "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg=="], "babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], @@ -1683,8 +1642,6 @@ "caching-transform": ["caching-transform@4.0.0", "", { "dependencies": { "hasha": "^5.0.0", "make-dir": "^3.0.0", "package-hash": "^4.0.0", "write-file-atomic": "^3.0.0" } }, "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA=="], - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], @@ -1701,7 +1658,7 @@ "caniuse-api": ["caniuse-api@3.0.0", "", { "dependencies": { "browserslist": "^4.0.0", "caniuse-lite": "^1.0.0", "lodash.memoize": "^4.1.2", "lodash.uniq": "^4.5.0" } }, "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw=="], - "caniuse-lite": ["caniuse-lite@1.0.30001705", "", {}, "sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001734", "", {}, "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A=="], "canvas": ["canvas@3.1.0", "", { "dependencies": { "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.1" } }, "sha512-tTj3CqqukVJ9NgSahykNwtGda7V33VLObwrHfzT0vqJXu7J4d4C/7kQQW3fOEGDfZZoILPut5H00gOjyttPGyg=="], @@ -1933,8 +1890,6 @@ "css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="], - "csscolorparser": ["csscolorparser@1.0.3", "", {}, "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w=="], - "cssdb": ["cssdb@7.11.2", "", {}, "sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], @@ -1975,8 +1930,6 @@ "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], - "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], - "dargs": ["dargs@7.0.0", "", {}, "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg=="], "dashdash": ["dashdash@1.14.1", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g=="], @@ -1985,19 +1938,13 @@ "data-urls": ["data-urls@3.0.2", "", { "dependencies": { "abab": "^2.0.6", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0" } }, "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ=="], - "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], - - "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], - - "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], - "date-format": ["date-format@4.0.14", "", {}, "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg=="], "date-time": ["date-time@3.1.0", "", { "dependencies": { "time-zone": "^1.0.0" } }, "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg=="], "dateformat": ["dateformat@3.0.3", "", {}, "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q=="], - "dcmjs": ["dcmjs@0.42.0", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevel": "^1.8.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-N/SXIzMvAjDRgb8vC8G7DP/2BI1QKpa3TwY47Ok34g5y6ZKIcpAR3sf+Y+OBWkQxvnCTncB6B4tkq/ih8xjmXA=="], + "dcmjs": ["dcmjs@0.43.1", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevel": "^1.8.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-7IAB2cs2F8QYINbfhA7PV+IDraqBuHn8N31xJc6HsyNxZXyGUPQRujcq732ACqjLhl7oJi1z8PJJgLdblQtD3Q=="], "debounce": ["debounce@1.2.1", "", {}, "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="], @@ -2029,8 +1976,6 @@ "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], "default-browser": ["default-browser@4.0.0", "", { "dependencies": { "bundle-name": "^3.0.0", "default-browser-id": "^3.0.0", "execa": "^7.1.1", "titleize": "^3.0.0" } }, "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA=="], @@ -2099,13 +2044,13 @@ "di": ["di@0.0.1", "", {}, "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA=="], - "dicom-microscopy-viewer": ["dicom-microscopy-viewer@0.46.1", "", { "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", "colormap": "^2.3", "dcmjs": "^0.29.8", "dicomicc": "^0.1", "dicomweb-client": "^0.8.4", "image-type": "^4.1", "mathjs": "^11.2", "ol": "^7.1", "uuid": "^9.0" } }, "sha512-QLozX/iM6ZA0TxheHQnTNiLg+RbSVlxYKMNG9qdqV5oNbEDOf+z4/8mDqnAQ8wjlQXE2MbUDX8JRa0LmO9mDTg=="], + "dicom-microscopy-viewer": ["dicom-microscopy-viewer@0.48.6", "", { "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", "colormap": "^2.3", "dcmjs": "^0.41.0", "dicomicc": "^0.1", "dicomweb-client": "^0.10.3", "image-type": "^4.1", "mathjs": "^11.2", "ol": "^10.4.0", "uuid": "^9.0" } }, "sha512-cTQ14qE3zqzlH0QOIhxbrIOsLVNCV6/i9qDrIiOnmD3I1R2W6cwRN4iMgQ0c6zas/9+o645Tu37FA6BqQ3fIlg=="], "dicom-parser": ["dicom-parser@1.8.21", "", {}, "sha512-lYCweHQDsC8UFpXErPlg86Px2A8bay0HiUY+wzoG3xv5GzgqVHU3lziwSc/Gzn7VV7y2KeP072SzCviuOoU02w=="], "dicomicc": ["dicomicc@0.1.0", "", {}, "sha512-kZejPGjLQ9NsgovSyVsiAuCpq6LofNR9Erc8Tt/vQAYGYCoQnTyWDlg5D0TJJQATKul7cSr9k/q0TF8G9qdDkQ=="], - "dicomweb-client": ["dicomweb-client@0.8.4", "", {}, "sha512-/6oY3/Fg9JyAlbTWuJOYbVqici3+nlZt43+Z/Y47RNiqLc028JcxNlY28u4VQqksxfB59f1hhNbsqsHyDT4vhw=="], + "dicomweb-client": ["dicomweb-client@0.11.2", "", {}, "sha512-HsjQtmw4XY35ZyrhW5WY7P5ZgGIrY1M1jfLeq2IKANDIqfWoE4QW496VcsbuX4qnTNoeHBauei73vU0WwMYrYg=="], "diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="], @@ -2117,8 +2062,6 @@ "docdash": ["docdash@1.2.0", "", {}, "sha512-IYZbgYthPTspgqYeciRJNPhSwL51yer7HAwDXhF5p+H7mTDbPvY3PCk/QDjNxdPCpWkaJVFC4t7iCNB/t9E5Kw=="], - "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], - "dom-converter": ["dom-converter@0.2.0", "", { "dependencies": { "utila": "~0.4" } }, "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA=="], "dom-serialize": ["dom-serialize@2.2.1", "", { "dependencies": { "custom-event": "~1.0.0", "ent": "~2.2.0", "extend": "^3.0.0", "void-elements": "^2.0.0" } }, "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ=="], @@ -2149,7 +2092,7 @@ "duplexer2": ["duplexer2@0.1.4", "", { "dependencies": { "readable-stream": "^2.0.2" } }, "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA=="], - "earcut": ["earcut@2.2.4", "", {}, "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ=="], + "earcut": ["earcut@3.0.2", "", {}, "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], @@ -2165,7 +2108,7 @@ "emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="], - "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="], @@ -2203,8 +2146,6 @@ "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], - "es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -2215,10 +2156,6 @@ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], - - "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], "es6-promisify": ["es6-promisify@6.1.1", "", {}, "sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg=="], @@ -2233,43 +2170,19 @@ "escape-latex": ["escape-latex@1.2.0", "", {}, "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], - "eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], - - "eslint-config-prettier": ["eslint-config-prettier@8.10.0", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg=="], - - "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], - - "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.9.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^1.3.0", "rspack-resolver": "^1.1.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.12" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-euxa5rTGqHeqVxmOHT25hpk58PxkQ4mNoX6Yun4ooGaCHAxOCojJYNvjmyeOQxj/LyW+3fulH0+xtk+p2kPPTw=="], - - "eslint-module-utils": ["eslint-module-utils@2.12.0", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg=="], - - "eslint-plugin-import": ["eslint-plugin-import@2.31.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.0", "hasown": "^2.0.2", "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.0", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A=="], - - "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.7.1", "", { "dependencies": { "@babel/runtime": "^7.20.7", "aria-query": "^5.1.3", "array-includes": "^3.1.6", "array.prototype.flatmap": "^1.3.1", "ast-types-flow": "^0.0.7", "axe-core": "^4.6.2", "axobject-query": "^3.1.1", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "has": "^1.0.3", "jsx-ast-utils": "^3.3.3", "language-tags": "=1.0.5", "minimatch": "^3.1.2", "object.entries": "^1.1.6", "object.fromentries": "^2.0.6", "semver": "^6.3.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA=="], - - "eslint-plugin-prettier": ["eslint-plugin-prettier@4.2.1", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0" }, "peerDependencies": { "eslint": ">=7.28.0", "prettier": ">=2.0.0" } }, "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ=="], - - "eslint-plugin-tsdoc": ["eslint-plugin-tsdoc@0.2.17", "", { "dependencies": { "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "0.16.2" } }, "sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA=="], + "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], - "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@2.1.0", "", {}, "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw=="], - - "eslint-webpack-plugin": ["eslint-webpack-plugin@4.2.0", "", { "dependencies": { "@types/eslint": "^8.56.10", "jest-worker": "^29.7.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "schema-utils": "^4.2.0" }, "peerDependencies": { "eslint": "^8.0.0 || ^9.0.0", "webpack": "^5.0.0" } }, "sha512-rsfpFQ01AWQbqtjgPRr2usVRxhWDuG0YDYcG8DJOteD3EFnpeuYuOwk0PQiN7PRBTqS6ElNdtPZPggj8If9WnA=="], - - "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], @@ -2331,8 +2244,6 @@ "fast-json-stringify": ["fast-json-stringify@5.16.1", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.1.0", "ajv": "^8.10.0", "ajv-formats": "^3.0.1", "fast-deep-equal": "^3.1.3", "fast-uri": "^2.1.0", "json-schema-ref-resolver": "^1.0.1", "rfdc": "^1.2.0" } }, "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g=="], - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], @@ -2367,7 +2278,7 @@ "figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="], - "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + "file-entry-cache": ["file-entry-cache@7.0.2", "", { "dependencies": { "flat-cache": "^3.2.0" } }, "sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g=="], "file-loader": ["file-loader@6.2.0", "", { "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^3.0.0" }, "peerDependencies": { "webpack": "^4.0.0 || ^5.0.0" } }, "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw=="], @@ -2413,8 +2324,6 @@ "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], "forever-agent": ["forever-agent@0.6.1", "", {}, "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="], @@ -2455,10 +2364,6 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], - - "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], - "fuzzy": ["fuzzy@0.1.3", "", {}, "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w=="], "gauge": ["gauge@3.0.2", "", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.2", "console-control-strings": "^1.0.0", "has-unicode": "^2.0.1", "object-assign": "^4.1.1", "signal-exit": "^3.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.2" } }, "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q=="], @@ -2491,10 +2396,6 @@ "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - - "get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="], - "getpass": ["getpass@0.1.7", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng=="], "gh-release-fetch": ["gh-release-fetch@4.0.3", "", { "dependencies": { "@xhmikosr/downloader": "^13.0.0", "node-fetch": "^3.3.1", "semver": "^7.5.3" } }, "sha512-TOiP1nwLsH5shG85Yt6v6Kjq5JU/44jXyEpbcfPgmj3C829yeXIlx9nAEwQRaxtRF3SJinn2lz7XUkfG9W/U4g=="], @@ -2531,7 +2432,7 @@ "global-prefix": ["global-prefix@3.0.0", "", { "dependencies": { "ini": "^1.3.5", "kind-of": "^6.0.2", "which": "^1.3.1" } }, "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg=="], - "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], "globalthis": ["globalthis@1.0.3", "", { "dependencies": { "define-properties": "^1.1.3" } }, "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA=="], @@ -2547,8 +2448,6 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], - "guid-typescript": ["guid-typescript@1.0.9", "", {}, "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="], "gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="], @@ -2567,18 +2466,12 @@ "hard-rejection": ["hard-rejection@2.1.0", "", {}, "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA=="], - "has": ["has@1.0.4", "", {}, "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ=="], - - "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-own-prop": ["has-own-prop@2.0.0", "", {}, "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], @@ -2691,8 +2584,6 @@ "interface-store": ["interface-store@6.0.2", "", {}, "sha512-KSFCXtBlNoG0hzwNa0RmhHtrdhzexp+S+UY2s0rWTBJyfdEIgn6i6Zl9otVqrcFYbYrneBT7hbmHQ8gE0C3umA=="], - "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], - "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], "interpret": ["interpret@3.1.1", "", {}, "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ=="], @@ -2713,46 +2604,26 @@ "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], - "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], - "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], - "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], - - "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], - "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], - "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], - "is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="], "is-builtin-module": ["is-builtin-module@3.2.1", "", { "dependencies": { "builtin-modules": "^3.3.0" } }, "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A=="], - "is-bun-module": ["is-bun-module@1.3.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA=="], - - "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - "is-ci": ["is-ci@3.0.1", "", { "dependencies": { "ci-info": "^3.2.0" }, "bin": { "is-ci": "bin.js" } }, "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], - - "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], - "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-generator-fn": ["is-generator-fn@2.1.0", "", {}, "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ=="], - "is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="], - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], "is-in-ci": ["is-in-ci@1.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg=="], @@ -2765,8 +2636,6 @@ "is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="], - "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], - "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], "is-natural-number": ["is-natural-number@4.0.1", "", {}, "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ=="], @@ -2777,15 +2646,13 @@ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], - "is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], "is-path-cwd": ["is-path-cwd@2.2.0", "", {}, "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ=="], "is-path-in-cwd": ["is-path-in-cwd@2.1.0", "", { "dependencies": { "is-path-inside": "^2.1.0" } }, "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ=="], - "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + "is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], "is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="], @@ -2795,22 +2662,12 @@ "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], - "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], - - "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], - "is-ssh": ["is-ssh@1.4.1", "", { "dependencies": { "protocols": "^2.0.1" } }, "sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg=="], "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], - "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], - - "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], - "is-text-path": ["is-text-path@1.0.1", "", { "dependencies": { "text-extensions": "^1.0.0" } }, "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w=="], - "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], - "is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="], "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], @@ -2819,12 +2676,6 @@ "is-url-superb": ["is-url-superb@4.0.0", "", {}, "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA=="], - "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], - - "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], - - "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], - "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], @@ -2945,7 +2796,7 @@ "jest-watcher": ["jest-watcher@29.7.0", "", { "dependencies": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", "jest-util": "^29.7.0", "string-length": "^4.0.1" } }, "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g=="], - "jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], + "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], @@ -2983,12 +2834,8 @@ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - "json-stringify-nice": ["json-stringify-nice@1.1.4", "", {}, "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw=="], - "json-stringify-pretty-compact": ["json-stringify-pretty-compact@2.0.0", "", {}, "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ=="], - "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], @@ -3005,8 +2852,6 @@ "jsprim": ["jsprim@1.4.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw=="], - "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], - "junk": ["junk@4.0.1", "", {}, "sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ=="], "just-diff": ["just-diff@6.0.2", "", {}, "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA=="], @@ -3055,10 +2900,6 @@ "lambda-local": ["lambda-local@2.2.0", "", { "dependencies": { "commander": "^10.0.1", "dotenv": "^16.3.1", "winston": "^3.10.0" }, "bin": { "lambda-local": "build/cli.js" } }, "sha512-bPcgpIXbHnVGfI/omZIlgucDqlf4LrsunwoKue5JdZeGybt8L6KyJz2Zu19ffuZwIwLj2NAI2ZyaqNT6/cetcg=="], - "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], - - "language-tags": ["language-tags@1.0.5", "", { "dependencies": { "language-subtag-registry": "~0.3.2" } }, "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ=="], - "latest-version": ["latest-version@9.0.0", "", { "dependencies": { "package-json": "^10.0.0" } }, "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA=="], "launch-editor": ["launch-editor@2.10.0", "", { "dependencies": { "picocolors": "^1.0.0", "shell-quote": "^1.8.1" } }, "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA=="], @@ -3075,8 +2916,6 @@ "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - "libnpmaccess": ["libnpmaccess@8.0.6", "", { "dependencies": { "npm-package-arg": "^11.0.2", "npm-registry-fetch": "^17.0.1" } }, "sha512-uM8DHDEfYG6G5gVivVl+yQd4pH3uRclHC59lzIbSvy7b5FEwR+mU49Zq1jEyRtRFv7+M99mUW9S0wL/4laT4lw=="], "libnpmpublish": ["libnpmpublish@9.0.9", "", { "dependencies": { "ci-info": "^4.0.0", "normalize-package-data": "^6.0.1", "npm-package-arg": "^11.0.2", "npm-registry-fetch": "^17.0.1", "proc-log": "^4.2.0", "semver": "^7.3.7", "sigstore": "^2.2.0", "ssri": "^10.0.6" } }, "sha512-26zzwoBNAvX9AWOPiqqF6FG4HrSCPsHFkQm7nT+xU1ggAujL/eae81RnCv4CJ2In9q9fh10B88sYSzKCUh/Ghg=="], @@ -3135,8 +2974,6 @@ "lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="], - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], "lodash.transform": ["lodash.transform@4.6.0", "", {}, "sha512-LO37ZnhmBVx0GvOU/caQuipEh4GN82TcWv3yHlebGDgOxbxiwwzW5Pcx2AcvpIv2WmvmSMoC492yQFNhy/l/UQ=="], @@ -3159,8 +2996,6 @@ "loglevel": ["loglevel@1.9.2", "", {}, "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg=="], - "loglevelnext": ["loglevelnext@3.0.1", "", {}, "sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA=="], - "long": ["long@5.3.1", "", {}, "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng=="], "loupe": ["loupe@3.1.3", "", {}, "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug=="], @@ -3187,8 +3022,6 @@ "map-obj": ["map-obj@5.0.2", "", {}, "sha512-K6K2NgKnTXimT3779/4KxSvobxOtMmx1LBZ3NwRxT/MDIR3Br/fQ4Q+WCX5QxjyUR8zg5+RV9Tbf2c5pAWTD2A=="], - "mapbox-to-css-font": ["mapbox-to-css-font@2.4.5", "", {}, "sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w=="], - "markdown-it": ["markdown-it@12.3.2", "", { "dependencies": { "argparse": "^2.0.1", "entities": "~2.1.0", "linkify-it": "^3.0.1", "mdurl": "^1.0.1", "uc.micro": "^1.0.5" }, "bin": { "markdown-it": "bin/markdown-it.js" } }, "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg=="], "markdown-it-anchor": ["markdown-it-anchor@8.6.7", "", { "peerDependencies": { "@types/markdown-it": "*", "markdown-it": "*" } }, "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA=="], @@ -3249,7 +3082,7 @@ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "micromatch": ["micromatch@4.0.5", "", { "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" } }, "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA=="], "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], @@ -3429,23 +3262,11 @@ "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], - "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], - - "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], - - "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], - - "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], - - "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], - "obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="], "ofetch": ["ofetch@1.4.1", "", { "dependencies": { "destr": "^2.0.3", "node-fetch-native": "^1.6.4", "ufo": "^1.5.4" } }, "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw=="], - "ol": ["ol@7.5.2", "", { "dependencies": { "earcut": "^2.2.3", "geotiff": "^2.0.7", "ol-mapbox-style": "^10.1.0", "pbf": "3.2.1", "rbush": "^3.0.1" } }, "sha512-HJbb3CxXrksM6ct367LsP3N+uh+iBBMdP3DeGGipdV9YAYTP0vTJzqGnoqQ6C2IW4qf8krw9yuyQbc9fjOIaOQ=="], - - "ol-mapbox-style": ["ol-mapbox-style@10.7.0", "", { "dependencies": { "@mapbox/mapbox-gl-style-spec": "^13.23.1", "mapbox-to-css-font": "^2.4.1", "ol": "^7.3.0" } }, "sha512-S/UdYBuOjrotcR95Iq9AejGYbifKeZE85D9VtH11ryJLQPTZXZSW1J5bIXcr4AlAH6tyjPPHTK34AdkwB32Myw=="], + "ol": ["ol@10.6.1", "", { "dependencies": { "@types/rbush": "4.0.0", "earcut": "^3.0.0", "geotiff": "^2.1.3", "pbf": "4.0.1", "rbush": "^4.0.0" } }, "sha512-xp174YOwPeLj7c7/8TCIEHQ4d41tgTDDhdv6SqNdySsql5/MaFJEJkjlsYcvOPt7xA6vrum/QG4UdJ0iCGT1cg=="], "omit.js": ["omit.js@2.0.2", "", {}, "sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg=="], @@ -3473,15 +3294,15 @@ "opener": ["opener@1.5.2", "", { "bin": { "opener": "bin/opener-bin.js" } }, "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A=="], - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], "os-name": ["os-name@5.1.0", "", { "dependencies": { "macos-release": "^3.1.0", "windows-release": "^5.0.1" } }, "sha512-YEIoAnM6zFmzw3PQ201gCVCIWbXNyKObGlVvpAVvraAeOHnlYVKFssbA/riRX5R40WA6kKrZ7Dr7dWzO3nKSeQ=="], "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], - "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + "oxc-parser": ["oxc-parser@0.74.0", "", { "dependencies": { "@oxc-project/types": "^0.74.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm64": "0.74.0", "@oxc-parser/binding-darwin-arm64": "0.74.0", "@oxc-parser/binding-darwin-x64": "0.74.0", "@oxc-parser/binding-freebsd-x64": "0.74.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.74.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.74.0", "@oxc-parser/binding-linux-arm64-gnu": "0.74.0", "@oxc-parser/binding-linux-arm64-musl": "0.74.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.74.0", "@oxc-parser/binding-linux-s390x-gnu": "0.74.0", "@oxc-parser/binding-linux-x64-gnu": "0.74.0", "@oxc-parser/binding-linux-x64-musl": "0.74.0", "@oxc-parser/binding-wasm32-wasi": "0.74.0", "@oxc-parser/binding-win32-arm64-msvc": "0.74.0", "@oxc-parser/binding-win32-x64-msvc": "0.74.0" } }, "sha512-2tDN/ttU8WE6oFh8EzKNam7KE7ZXSG5uXmvX85iNzxdJfMssDWcj3gpYzZi1E04XuE7m3v1dVWl/8BE886vPGw=="], + + "oxlint": ["oxlint@1.9.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.9.0", "@oxlint/darwin-x64": "1.9.0", "@oxlint/linux-arm64-gnu": "1.9.0", "@oxlint/linux-arm64-musl": "1.9.0", "@oxlint/linux-x64-gnu": "1.9.0", "@oxlint/linux-x64-musl": "1.9.0", "@oxlint/win32-arm64": "1.9.0", "@oxlint/win32-x64": "1.9.0" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-vvqpkRt7UA1F1DoOJGYO2+T52L6jV4RxS+9hFVCPQ0X+hcNVtbl/TxQuwjobBw8Yw2U8Rb9d8VLJxa8eRSqpQQ=="], "p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], @@ -3579,7 +3400,7 @@ "pathval": ["pathval@2.0.0", "", {}, "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA=="], - "pbf": ["pbf@3.2.1", "", { "dependencies": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ=="], + "pbf": ["pbf@4.0.1", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA=="], "peek-readable": ["peek-readable@5.4.2", "", {}, "sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg=="], @@ -3619,8 +3440,6 @@ "playwright-test-coverage": ["playwright-test-coverage@1.2.12", "", { "peerDependencies": { "@playwright/test": "^1.14.1" } }, "sha512-WdR3shV+7IWtlB1AZcXSysUyZmyAqXM9+DT3iBSS1p4SkJoWZslkTqk04l5ZExSkuaM+5fKl0M9TyUuJWuGgLA=="], - "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], "postcss-attribute-case-insensitive": ["postcss-attribute-case-insensitive@6.0.3", "", { "dependencies": { "postcss-selector-parser": "^6.0.13" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-KHkmCILThWBRtg+Jn1owTnHPnFit4OkqS+eKiGEOPIGke54DCeYGJ6r0Fx/HjfE9M9kznApCLcU0DvnPchazMQ=="], @@ -3759,11 +3578,7 @@ "precond": ["precond@0.2.3", "", {}, "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ=="], - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - - "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], - - "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], "pretty-error": ["pretty-error@4.0.0", "", { "dependencies": { "lodash": "^4.17.20", "renderkid": "^3.0.0" } }, "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw=="], @@ -3845,7 +3660,7 @@ "quick-lru": ["quick-lru@6.1.2", "", {}, "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ=="], - "quickselect": ["quickselect@2.0.0", "", {}, "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw=="], + "quickselect": ["quickselect@3.0.0", "", {}, "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="], "quote-unquote": ["quote-unquote@1.0.0", "", {}, "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg=="], @@ -3861,7 +3676,7 @@ "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], - "rbush": ["rbush@3.0.1", "", { "dependencies": { "quickselect": "^2.0.0" } }, "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w=="], + "rbush": ["rbush@4.0.1", "", { "dependencies": { "quickselect": "^3.0.0" } }, "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ=="], "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], @@ -3895,8 +3710,6 @@ "redent": ["redent@4.0.0", "", { "dependencies": { "indent-string": "^5.0.0", "strip-indent": "^4.0.0" } }, "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag=="], - "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], - "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], "regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.0", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA=="], @@ -3911,8 +3724,6 @@ "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], - "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], - "regexpu-core": ["regexpu-core@6.2.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.0", "regjsgen": "^0.8.0", "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" } }, "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA=="], "registry-auth-token": ["registry-auth-token@5.1.0", "", { "dependencies": { "@pnpm/npm-conf": "^2.1.0" } }, "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw=="], @@ -3957,8 +3768,6 @@ "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="], "resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="], @@ -3979,28 +3788,20 @@ "rollup": ["rollup@3.29.5", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w=="], - "rspack-resolver": ["rspack-resolver@1.1.2", "", { "optionalDependencies": { "@unrs/rspack-resolver-binding-darwin-arm64": "1.1.2", "@unrs/rspack-resolver-binding-darwin-x64": "1.1.2", "@unrs/rspack-resolver-binding-freebsd-x64": "1.1.2", "@unrs/rspack-resolver-binding-linux-arm-gnueabihf": "1.1.2", "@unrs/rspack-resolver-binding-linux-arm64-gnu": "1.1.2", "@unrs/rspack-resolver-binding-linux-arm64-musl": "1.1.2", "@unrs/rspack-resolver-binding-linux-x64-gnu": "1.1.2", "@unrs/rspack-resolver-binding-linux-x64-musl": "1.1.2", "@unrs/rspack-resolver-binding-wasm32-wasi": "1.1.2", "@unrs/rspack-resolver-binding-win32-arm64-msvc": "1.1.2", "@unrs/rspack-resolver-binding-win32-x64-msvc": "1.1.2" } }, "sha512-eHhz+9JWHFdbl/CVVqEP6kviLFZqw1s0MWxLdsGMtUKUspSO3SERptPohmrUIC9jT1bGV9Bd3+r8AmWbdfNAzQ=="], - "run-applescript": ["run-applescript@5.0.0", "", { "dependencies": { "execa": "^5.0.0" } }, "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg=="], "run-async": ["run-async@2.4.1", "", {}, "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], - "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-json-stringify": ["safe-json-stringify@1.2.0", "", {}, "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg=="], - "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], - "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], "safe-regex2": ["safe-regex2@3.1.0", "", { "dependencies": { "ret": "~0.4.0" } }, "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug=="], @@ -4039,12 +3840,6 @@ "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], - "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], - - "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], - - "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], - "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], @@ -4111,16 +3906,10 @@ "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], - "sort-asc": ["sort-asc@0.1.0", "", {}, "sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw=="], - - "sort-desc": ["sort-desc@0.1.1", "", {}, "sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw=="], - "sort-keys": ["sort-keys@2.0.0", "", { "dependencies": { "is-plain-obj": "^1.0.0" } }, "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg=="], "sort-keys-length": ["sort-keys-length@1.0.1", "", { "dependencies": { "sort-keys": "^1.0.0" } }, "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw=="], - "sort-object": ["sort-object@0.3.2", "", { "dependencies": { "sort-asc": "^0.1.0", "sort-desc": "^0.1.1" } }, "sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA=="], - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -4157,8 +3946,6 @@ "ssri": ["ssri@10.0.6", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ=="], - "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], - "stack-generator": ["stack-generator@2.0.10", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ=="], "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], @@ -4187,12 +3974,6 @@ "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], - - "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], - - "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], - "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], @@ -4203,7 +3984,7 @@ "strip-ansi-control-characters": ["strip-ansi-control-characters@2.0.0", "", {}, "sha512-Q0/k5orrVGeaOlIOUn1gybGU0IcAbgHQT1faLo5hik4DqClKVSaka5xOhNNoRgtfztHVxCYxi7j71mrWom0bIw=="], - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], "strip-dirs": ["strip-dirs@2.1.0", "", { "dependencies": { "is-natural-number": "^4.0.1" } }, "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g=="], @@ -4277,8 +4058,6 @@ "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], - "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], - "thingies": ["thingies@1.21.0", "", { "peerDependencies": { "tslib": "^2" } }, "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g=="], "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], @@ -4297,8 +4076,6 @@ "tiny-emitter": ["tiny-emitter@2.1.0", "", {}, "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="], - "tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="], - "titleize": ["titleize@3.0.0", "", {}, "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ=="], "tmp": ["tmp@0.2.3", "", {}, "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w=="], @@ -4341,13 +4118,11 @@ "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], - "ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="], - "ts-loader": ["ts-loader@9.5.1", "", { "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", "micromatch": "^4.0.0", "semver": "^7.3.4", "source-map": "^0.7.4" }, "peerDependencies": { "typescript": "*", "webpack": "^5.0.0" } }, "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg=="], "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="], - "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -4359,22 +4134,12 @@ "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], - "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + "type-fest": ["type-fest@0.6.0", "", {}, "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg=="], "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], - "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], - - "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], - - "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], - - "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], - "typed-function": ["typed-function@4.2.1", "", {}, "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA=="], "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="], @@ -4403,8 +4168,6 @@ "ulid": ["ulid@2.3.0", "", { "bin": { "ulid": "./bin/cli.js" } }, "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw=="], - "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="], "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], @@ -4557,16 +4320,8 @@ "which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], - "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], - - "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], - - "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], - "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], - "wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="], "widest-line": ["widest-line@4.0.1", "", { "dependencies": { "string-width": "^5.0.1" } }, "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig=="], @@ -4579,8 +4334,6 @@ "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], "worker-loader": ["worker-loader@3.0.8", "", { "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^3.0.0" }, "peerDependencies": { "webpack": "^4.0.0 || ^5.0.0" } }, "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g=="], @@ -4653,24 +4406,18 @@ "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/eslint-parser/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/plugin-transform-classes/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], - "@babel/plugin-transform-runtime/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/runtime-corejs2/core-js": ["core-js@2.6.12", "", {}, "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="], - "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], - "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], "@csstools/css-color-parser/@csstools/color-helpers": ["@csstools/color-helpers@4.2.1", "", {}, "sha512-CEypeeykO9AN7JWkr1OEOQb0HRzZlPWGwV0Ya6DuVgFdDi6g3ma/cPZ5ZPZM4AWQikDpq/0llnGGlIL+j8afzw=="], @@ -4679,22 +4426,12 @@ "@csstools/postcss-is-pseudo-class/@csstools/selector-specificity": ["@csstools/selector-specificity@2.2.0", "", { "peerDependencies": { "postcss-selector-parser": "^6.0.10" } }, "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "@eslint/eslintrc/ajv": ["ajv@6.12.6", "", { "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-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - - "@eslint/eslintrc/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - - "@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "@fastify/ajv-compiler/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], "@fastify/ajv-compiler/fast-uri": ["fast-uri@2.4.0", "", {}, "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA=="], "@fastify/send/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], - "@humanwhocodes/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "@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=="], "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], @@ -4709,10 +4446,16 @@ "@jest/core/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + "@jest/core/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "@jest/reporters/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "@jest/reporters/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], + "@jest/transform/babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], + "@jest/transform/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "@jest/transform/write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="], "@jsdevtools/coverage-istanbul-loader/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], @@ -4831,8 +4574,6 @@ "@netlify/edge-bundler/get-port": ["get-port@6.1.2", "", {}, "sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw=="], - "@netlify/edge-bundler/is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], - "@netlify/edge-bundler/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], "@netlify/edge-bundler/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], @@ -4851,6 +4592,8 @@ "@netlify/git-utils/execa": ["execa@6.1.0", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", "human-signals": "^3.0.1", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^3.0.7", "strip-final-newline": "^3.0.0" } }, "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA=="], + "@netlify/git-utils/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "@netlify/git-utils/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], "@netlify/headers-parser/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -4875,8 +4618,6 @@ "@netlify/zip-it-and-ship-it/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], - "@netlify/zip-it-and-ship-it/is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], - "@netlify/zip-it-and-ship-it/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@netlify/zip-it-and-ship-it/p-map": ["p-map@5.5.0", "", { "dependencies": { "aggregate-error": "^4.0.0" } }, "sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg=="], @@ -4885,8 +4626,6 @@ "@netlify/zip-it-and-ship-it/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], - "@nicolo-ribaudo/eslint-scope-5-internals/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], - "@npmcli/agent/http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], "@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -4915,14 +4654,22 @@ "@nx/devkit/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], + "@oxc-parser/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + "@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + "@parcel/watcher/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "@parcel/watcher-wasm/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="], "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], "@rspack/cli/webpack-bundle-analyzer": ["webpack-bundle-analyzer@4.6.1", "", { "dependencies": { "acorn": "^8.0.4", "acorn-walk": "^8.0.0", "chalk": "^4.1.0", "commander": "^7.2.0", "gzip-size": "^6.0.0", "lodash": "^4.17.20", "opener": "^1.5.2", "sirv": "^1.0.7", "ws": "^7.3.1" }, "bin": { "webpack-bundle-analyzer": "lib/bin/analyzer.js" } }, "sha512-oKz9Oz9j3rUciLNfpGFjOb49/jEpXNmWdVH8Ls//zNcnLlQdTGXQQMsBbb/gR7Zl8WNLxVCq+0Hqbx3zv6twBw=="], + "@rspack/core/caniuse-lite": ["caniuse-lite@1.0.30001705", "", {}, "sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg=="], + "@rspack/dev-server/webpack-dev-middleware": ["webpack-dev-middleware@7.4.2", "", { "dependencies": { "colorette": "^2.0.10", "memfs": "^4.6.0", "mime-types": "^2.1.31", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "peerDependencies": { "webpack": "^5.0.0" }, "optionalPeers": ["webpack"] }, "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA=="], "@rspack/dev-server/webpack-dev-server": ["webpack-dev-server@5.0.4", "", { "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", "@types/express": "^4.17.21", "@types/serve-index": "^1.9.4", "@types/serve-static": "^1.15.5", "@types/sockjs": "^0.3.36", "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", "bonjour-service": "^1.2.1", "chokidar": "^3.6.0", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", "default-gateway": "^6.0.3", "express": "^4.17.3", "graceful-fs": "^4.2.6", "html-entities": "^2.4.0", "http-proxy-middleware": "^2.0.3", "ipaddr.js": "^2.1.0", "launch-editor": "^2.6.1", "open": "^10.0.3", "p-retry": "^6.2.0", "rimraf": "^5.0.5", "schema-utils": "^4.2.0", "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", "webpack-dev-middleware": "^7.1.0", "ws": "^8.16.0" }, "peerDependencies": { "webpack": "^5.0.0" }, "optionalPeers": ["webpack"], "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" } }, "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA=="], @@ -4945,14 +4692,12 @@ "@types/express/@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], - - "@unrs/rspack-resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.7", "", { "dependencies": { "@emnapi/core": "^1.3.1", "@emnapi/runtime": "^1.3.1", "@tybys/wasm-util": "^0.9.0" } }, "sha512-5yximcFK5FNompXfJFoWanu5l8v1hNGqNHh9du1xETp9HWk/B/PzvchX55WYOPaIeNglG8++68AAiauBAtbnzw=="], + "@typescript-eslint/typescript-estree/globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], "@vercel/nft/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "@vercel/nft/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "@xhmikosr/decompress/strip-dirs": ["strip-dirs@3.0.0", "", { "dependencies": { "inspect-with-kind": "^1.0.5", "is-plain-obj": "^1.1.0" } }, "sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ=="], "@xhmikosr/decompress-tar/tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], @@ -4987,6 +4732,8 @@ "are-we-there-yet/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "autoprefixer/caniuse-lite": ["caniuse-lite@1.0.30001705", "", {}, "sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg=="], + "axios/form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="], "babel-jest/babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], @@ -5015,6 +4762,8 @@ "brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001705", "", {}, "sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg=="], + "cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "caching-transform/make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="], @@ -5027,6 +4776,8 @@ "camelcase-keys/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + "caniuse-api/caniuse-lite": ["caniuse-lite@1.0.30001705", "", {}, "sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg=="], + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -5091,6 +4842,8 @@ "cpy/cp-file": ["cp-file@9.1.0", "", { "dependencies": { "graceful-fs": "^4.1.2", "make-dir": "^3.0.0", "nested-error-stacks": "^2.0.0", "p-event": "^4.1.0" } }, "sha512-3scnzFj/94eb7y4wyXRWwvzLFaQp87yyfTnChIjlfYrVqp5lVO3E2hIJMeQIltUT0K2ZAB3An1qXcBmwGyvuwA=="], + "cpy/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "cpy/p-filter": ["p-filter@3.0.0", "", { "dependencies": { "p-map": "^5.1.0" } }, "sha512-QtoWLjXAW++uTX67HZQz1dbTpqBfiidsB6VtQUC9iR85S120+s0T5sO6s+B5MLzFcZkrEd/DGMmCjR+f2Qpxwg=="], "cpy/p-map": ["p-map@5.5.0", "", { "dependencies": { "aggregate-error": "^4.0.0" } }, "sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg=="], @@ -5141,8 +4894,6 @@ "default-gateway/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], - "default-require-extensions/strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], - "del/globby": ["globby@6.1.0", "", { "dependencies": { "array-union": "^1.0.1", "glob": "^7.0.3", "object-assign": "^4.0.1", "pify": "^2.0.0", "pinkie-promise": "^2.0.0" } }, "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw=="], "del/p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], @@ -5151,11 +4902,11 @@ "del/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], - "detective-typescript/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "semver": "^7.3.7", "tsutils": "^3.21.0" } }, "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA=="], - "detective-typescript/typescript": ["typescript@5.7.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg=="], - "dicom-microscopy-viewer/dcmjs": ["dcmjs@0.29.13", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevelnext": "^3.0.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-Bf9tKzJNWqk4kbV210N5TLEHDqaZvO3S+MH9vezFAU8WKcG4cR6z4/II3TQVqhLI185eNUL+lhfPCVH1Uu2yTA=="], + "dicom-microscopy-viewer/dcmjs": ["dcmjs@0.41.0", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevel": "^1.8.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-kr46REomItFeWz+0ck4Wif4uS5VVDWVlwdh5GGaCtTYHWfNQmrcCSiQOkrShc7Dc5zP8vNKrHEdORlZXenlg3w=="], + + "dicom-microscopy-viewer/dicomweb-client": ["dicomweb-client@0.10.4", "", {}, "sha512-TEt26c0JI37IGmSqoj1k1/Y/ZIyq33/ysVaUwE0/Haosn2IBM55NEIPkT+AnhFss2nFAMVtKKWKWLox4luthVw=="], "dir-glob/path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], @@ -5175,39 +4926,9 @@ "ent/punycode": ["punycode@1.4.1", "", {}, "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="], - "es-abstract/globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], - - "eslint/ajv": ["ajv@6.12.6", "", { "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-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - - "eslint/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "eslint/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - - "eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-plugin-import/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], - - "eslint-plugin-import/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "eslint-plugin-jsx-a11y/@babel/runtime": ["@babel/runtime@7.21.5", "", { "dependencies": { "regenerator-runtime": "^0.13.11" } }, "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q=="], + "escodegen/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "eslint-plugin-jsx-a11y/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "eslint-plugin-jsx-a11y/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "eslint-plugin-tsdoc/@microsoft/tsdoc": ["@microsoft/tsdoc@0.14.2", "", {}, "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug=="], - - "eslint-plugin-tsdoc/@microsoft/tsdoc-config": ["@microsoft/tsdoc-config@0.16.2", "", { "dependencies": { "@microsoft/tsdoc": "0.14.2", "ajv": "~6.12.6", "jju": "~1.4.0", "resolve": "~1.19.0" } }, "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw=="], - - "espree/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], "express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], @@ -5223,6 +4944,8 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "fast-glob/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "fast-json-stringify/fast-uri": ["fast-uri@2.4.0", "", {}, "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA=="], "fetch-node-website/colors-option": ["colors-option@4.5.0", "", { "dependencies": { "chalk": "^5.0.1", "is-plain-obj": "^4.1.0" } }, "sha512-Soe5lerRg3erMRgYC0EC696/8dMCGpBzcQchFfi55Yrkja8F+P7cUt0LVTIg7u5ob5BexLZ/F1kO+ejmv+nq8w=="], @@ -5295,6 +5018,8 @@ "http-proxy-middleware/is-plain-obj": ["is-plain-obj@3.0.0", "", {}, "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA=="], + "http-proxy-middleware/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "http2-wrapper/quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], "ignore-walk/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -5317,8 +5042,6 @@ "is-ci/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - "is-installed-globally/is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], - "is-path-in-cwd/is-path-inside": ["is-path-inside@2.1.0", "", { "dependencies": { "path-is-inside": "^1.0.2" } }, "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg=="], "istanbul-lib-processinfo/p-map": ["p-map@3.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ=="], @@ -5345,14 +5068,22 @@ "jest-config/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "jest-config/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "jest-haste-map/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], + + "jest-haste-map/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "jest-junit/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "jest-message-util/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "jest-runner/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], + "jest-runner/source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], "jest-runtime/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "jest-runtime/strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], - "jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -5361,8 +5092,6 @@ "jest-watcher/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], - "jsdoc/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], - "jsdom/form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="], "jsdom/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], @@ -5423,8 +5152,6 @@ "lint-staged/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], - "lint-staged/micromatch": ["micromatch@4.0.5", "", { "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" } }, "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA=="], - "listhen/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], "listr2/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], @@ -5433,10 +5160,6 @@ "listr2/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - "load-json-file/strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], - - "load-json-file/type-fest": ["type-fest@0.6.0", "", {}, "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg=="], - "log-process-errors/figures": ["figures@4.0.1", "", { "dependencies": { "escape-string-regexp": "^5.0.0", "is-unicode-supported": "^1.2.0" } }, "sha512-rElJwkA/xS04Vfg+CaZodpso7VqBknOYbzi6I76hI4X80RUjkSxO2oAyPmGbuXUppywjqndOrQDl817hDnI++w=="], "log-process-errors/filter-obj": ["filter-obj@3.0.0", "", {}, "sha512-oQZM+QmVni8MsYzcq9lgTHD/qeLqaG8XaOPOW7dzuSafVxSUlH1+1ZDefj2OD9f2XsmG5lFl2Euc9NI4jgwFWg=="], @@ -5483,6 +5206,8 @@ "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "mocha/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "mocha/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], "mocha/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], @@ -5559,8 +5284,6 @@ "nx/ora": ["ora@5.3.0", "", { "dependencies": { "bl": "^4.0.3", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "log-symbols": "^4.0.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g=="], - "nx/tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], - "nx/yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], "nyc/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], @@ -5663,10 +5386,6 @@ "run-applescript/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], - "safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - - "safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - "schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], "seek-bzip/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], @@ -5721,26 +5440,20 @@ "spdy-transport/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], - "stream-browserify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "streamroller/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "stylelint/cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], - "stylelint/file-entry-cache": ["file-entry-cache@7.0.2", "", { "dependencies": { "flat-cache": "^3.2.0" } }, "sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g=="], - "stylelint/globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], "stylelint/meow": ["meow@10.1.5", "", { "dependencies": { "@types/minimist": "^1.2.2", "camelcase-keys": "^7.0.0", "decamelize": "^5.0.0", "decamelize-keys": "^1.1.0", "hard-rejection": "^2.1.0", "minimist-options": "4.1.0", "normalize-package-data": "^3.0.2", "read-pkg-up": "^8.0.0", "redent": "^4.0.0", "trim-newlines": "^4.0.2", "type-fest": "^1.2.2", "yargs-parser": "^20.2.9" } }, "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw=="], + "stylelint/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "supports-hyperlinks/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], @@ -5771,8 +5484,6 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "terser-webpack-plugin/jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], - "test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "test-exclude/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -5783,11 +5494,13 @@ "trim-repeated/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "ts-loader/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "ts-loader/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], "ts-node/diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], - "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + "tsconfig-paths/strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], "tsutils/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], @@ -5821,12 +5534,12 @@ "wait-port/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], - "webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], - "webpack/schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], "webpack-bundle-analyzer/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + "webpack-bundle-analyzer/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "webpack-bundle-analyzer/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "webpack-dev-server/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], @@ -5837,8 +5550,6 @@ "which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - "widest-line/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=="], "windows-release/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], @@ -5881,9 +5592,7 @@ "zip-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "@eslint/eslintrc/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], @@ -5899,10 +5608,14 @@ "@jest/core/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + "@jest/core/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "@jest/reporters/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "@jest/transform/babel-plugin-istanbul/istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="], + "@jest/transform/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "@jsdevtools/coverage-istanbul-loader/istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@jsdevtools/coverage-istanbul-loader/schema-utils/ajv": ["ajv@6.12.6", "", { "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-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], @@ -5959,6 +5672,8 @@ "@netlify/build/pkg-dir/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="], + "@netlify/build/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "@netlify/build/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "@netlify/config/dot-prop/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], @@ -5991,8 +5706,6 @@ "@netlify/functions-utils/@netlify/zip-it-and-ship-it/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], - "@netlify/functions-utils/@netlify/zip-it-and-ship-it/is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], - "@netlify/functions-utils/@netlify/zip-it-and-ship-it/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@netlify/functions-utils/@netlify/zip-it-and-ship-it/p-map": ["p-map@7.0.2", "", {}, "sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q=="], @@ -6001,6 +5714,8 @@ "@netlify/git-utils/execa/human-signals": ["human-signals@3.0.1", "", {}, "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ=="], + "@netlify/git-utils/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "@netlify/run-utils/execa/human-signals": ["human-signals@3.0.1", "", {}, "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ=="], "@netlify/zip-it-and-ship-it/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g=="], @@ -6057,14 +5772,22 @@ "@netlify/zip-it-and-ship-it/p-map/aggregate-error": ["aggregate-error@4.0.1", "", { "dependencies": { "clean-stack": "^4.0.0", "indent-string": "^5.0.0" } }, "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w=="], - "@nicolo-ribaudo/eslint-scope-5-internals/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], - "@npmcli/arborist/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "@npmcli/map-workspaces/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "@nx/devkit/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "@oxc-parser/binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], + + "@oxc-parser/binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], + + "@oxc-parser/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], + + "@parcel/watcher-wasm/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "@parcel/watcher/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "@rspack/cli/webpack-bundle-analyzer/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], "@rspack/cli/webpack-bundle-analyzer/sirv": ["sirv@1.0.19", "", { "dependencies": { "@polka/url": "^1.0.0-next.20", "mrmime": "^1.0.0", "totalist": "^1.0.0" } }, "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ=="], @@ -6083,10 +5806,10 @@ "@tufjs/models/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "@vercel/nft/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "@vercel/nft/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "@xhmikosr/decompress/strip-dirs/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], "archiver-utils/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -6107,6 +5830,8 @@ "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "boxen/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "boxen/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "boxen/wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], @@ -6115,6 +5840,8 @@ "cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + "cli-truncate/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "clipboardy/execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], @@ -6197,6 +5924,8 @@ "cpy/cp-file/p-event": ["p-event@4.2.0", "", { "dependencies": { "p-timeout": "^3.1.0" } }, "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ=="], + "cpy/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "cpy/p-map/aggregate-error": ["aggregate-error@4.0.1", "", { "dependencies": { "clean-stack": "^4.0.0", "indent-string": "^5.0.0" } }, "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w=="], "crc32-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -6227,24 +5956,10 @@ "del/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "detective-typescript/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@5.62.0", "", {}, "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ=="], - - "detective-typescript/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" } }, "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw=="], - - "detective-typescript/@typescript-eslint/typescript-estree/globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], - - "eslint-plugin-jsx-a11y/@babel/runtime/regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], - - "eslint-plugin-tsdoc/@microsoft/tsdoc-config/ajv": ["ajv@6.12.6", "", { "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-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - - "eslint-plugin-tsdoc/@microsoft/tsdoc-config/resolve": ["resolve@1.19.0", "", { "dependencies": { "is-core-module": "^2.1.0", "path-parse": "^1.0.6" } }, "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg=="], - - "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "eslint/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "fast-glob/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "fetch-node-website/colors-option/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="], "fetch-node-website/figures/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -6307,6 +6022,8 @@ "har-validator/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "http-proxy-middleware/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "ignore-walk/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "inquirer-autocomplete-prompt/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], @@ -6335,6 +6052,12 @@ "jest-config/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "jest-config/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "jest-haste-map/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "jest-message-util/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "jest-runtime/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "jest-watcher/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], @@ -6373,8 +6096,6 @@ "lint-staged/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], - "lint-staged/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "listr2/log-update/ansi-escapes": ["ansi-escapes@5.0.0", "", { "dependencies": { "type-fest": "^1.0.2" } }, "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA=="], "listr2/log-update/cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], @@ -6531,6 +6252,8 @@ "read-pkg/load-json-file/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], + "read-pkg/load-json-file/strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], "read-pkg/normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], @@ -6603,6 +6326,8 @@ "stylelint/meow/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "stylelint/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "svgo/css-select/domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], "svgo/css-select/domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], @@ -6639,6 +6364,8 @@ "through2-map/through2/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "ts-loader/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "typedoc/markdown-it/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "typedoc/markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -6673,12 +6400,12 @@ "webpack-dev-server/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], - "webpack/schema-utils/ajv": ["ajv@6.12.6", "", { "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-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "webpack/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + "widest-line/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "widest-line/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "windows-release/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], @@ -6787,6 +6514,8 @@ "@nx/devkit/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@oxc-parser/binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="], + "@rspack/cli/webpack-bundle-analyzer/sirv/mrmime": ["mrmime@1.0.1", "", {}, "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw=="], "@rspack/cli/webpack-bundle-analyzer/sirv/totalist": ["totalist@1.1.0", "", {}, "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g=="], @@ -6797,8 +6526,6 @@ "@tufjs/models/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "babel-jest/babel-plugin-istanbul/istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], @@ -6881,10 +6608,6 @@ "del/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "detective-typescript/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "eslint-plugin-tsdoc/@microsoft/tsdoc-config/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "file-loader/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -6957,6 +6680,8 @@ "listr2/log-update/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "listr2/wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "listr2/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "log-process-errors/jest-validate/@jest/types/@types/yargs": ["@types/yargs@16.0.9", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA=="], diff --git a/package.json b/package.json index 26e17c560d..4bf15891bc 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "docs:watch": "npx lerna run docs:watch", "preinstall": "node preinstall.js", "prepare": "husky", + "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"", "commit:prepare": "yarn test:ci && yarn test:unit", "start": "yarn run dev", "test:e2e:coverage": "nyc --reporter=html yarn run test:e2e:ci", @@ -56,12 +57,11 @@ "test:unit": "jest --collectCoverage", "test:debug": "karma start ./karma.conf.js --browsers Chrome --no-single-run", "lint-staged": "lint-staged", - "lint": "eslint --quiet -c .eslintrc.json packages/**/src", + "lint": "oxlint packages/**/src --quiet", "webpack:watch": "npx lerna run webpack:watch" }, "devDependencies": { "@babel/core": "^7.21.8", - "@babel/eslint-parser": "^7.19.1", "@babel/plugin-external-helpers": "^7.18.6", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-object-rest-spread": "^7.14.7", @@ -77,6 +77,7 @@ "@microsoft/api-extractor": "7.49.2", "@microsoft/tsdoc": "^0.15.0", "@playwright/test": "^1.51.1", + "@prettier/plugin-oxc": "^0.0.4", "@rollup/plugin-babel": "^6.0.3", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.2", @@ -91,8 +92,6 @@ "@types/node": "^20.14.8", "@types/react": "^17.0.58", "@types/react-dom": "^17.0.20", - "@typescript-eslint/eslint-plugin": "^8.1.0", - "@typescript-eslint/parser": "^8.1.0", "@webgpu/types": "^0.1.40", "acorn": "^8.8.2", "acorn-jsx": "^5.3.2", @@ -112,23 +111,16 @@ "cross-env": "^7.0.3", "css-loader": "^6.7.3", "cssnano": "^6.0.1", + "dicomweb-client": "^0.11.2", "docdash": "^1.2.0", "dpdm": "^3.14.0", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsx-a11y": "6.7.1", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-tsdoc": "^0.2.17", - "eslint-webpack-plugin": "^4.0.1", "execa": "^7.2.0", "exports-loader": "^3.0.0", "file-loader": "^6.2.0", "follow-redirects": "^1.15.2", "fs-extra": "^10.0.0", "html-webpack-plugin": "^5.5.1", - "husky": "^9.1.4", + "husky": "^9.1.7", "jasmine": "^4.6.0", "jest": "^29.7.0", "jest-canvas-mock": "^2.5.2", @@ -150,13 +142,14 @@ "netlify-cli": "^17.34.1", "nyc": "^17.1.0", "open-cli": "^7.0.1", + "oxlint": "^1.9.0", "path-browserify": "^1.0.1", "playwright-test-coverage": "^1.2.12", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-loader": "^7.3.0", "postcss-preset-env": "^8.3.2", - "prettier": "^2.8.8", + "prettier": "^3.6.2", "puppeteer": "^13.5.0", "resemblejs": "^5.0.0", "rollup": "^3.21.3", @@ -184,7 +177,7 @@ }, "lint-staged": { "packages/**/*.{ts,js,jsx,tsx,json,md,css}": [ - "eslint --fix", + "oxlint", "prettier --write" ] }, @@ -195,7 +188,9 @@ "not op_mini all" ], "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", - "dependencies": {}, + "dependencies": { + "caniuse-lite": "^1.0.30001734" + }, "resolutions": { "canvas": "3.1.0" } diff --git a/packages/adapters/.gitignore b/packages/adapters/.gitignore index 19dae9fb8f..d46c37c93e 100644 --- a/packages/adapters/.gitignore +++ b/packages/adapters/.gitignore @@ -1,4 +1,7 @@ +# Ignore the version.js because it is generated +src/version.ts +# Misc items *.sw* *~ diff --git a/packages/adapters/CHANGELOG.md b/packages/adapters/CHANGELOG.md index cbec835762..ca10e3772e 100644 --- a/packages/adapters/CHANGELOG.md +++ b/packages/adapters/CHANGELOG.md @@ -27,7 +27,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **labelmap:** labelmap seg implementation ([#2156](https://github.com/cornerstonejs/cornerstone3D/issues/2156)) ([72e55df](https://github.com/cornerstonejs/cornerstone3D/commit/72e55df74e627b76a7313c0aff5c167d84634bee)) +- **labelmap:** labelmap seg implementation ([#2156](https://github.com/cornerstonejs/cornerstone3D/issues/2156)) ([72e55df](https://github.com/cornerstonejs/cornerstone3D/commit/72e55df74e627b76a7313c0aff5c167d84634bee)) ## [3.23.1](https://github.com/cornerstonejs/cornerstone3D/compare/v3.23.0...v3.23.1) (2025-06-24) @@ -153,7 +153,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- remove random none string ([#2081](https://github.com/cornerstonejs/cornerstone3D/issues/2081)) ([c1df7de](https://github.com/cornerstonejs/cornerstone3D/commit/c1df7dea9a448427c36a7b1dcd60f728e7d54d86)) +- remove random none string ([#2081](https://github.com/cornerstonejs/cornerstone3D/issues/2081)) ([c1df7de](https://github.com/cornerstonejs/cornerstone3D/commit/c1df7dea9a448427c36a7b1dcd60f728e7d54d86)) ## [3.15.2](https://github.com/cornerstonejs/cornerstone3D/compare/v3.15.1...v3.15.2) (2025-05-15) @@ -167,7 +167,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- save scoord3d for tools ([#2035](https://github.com/cornerstonejs/cornerstone3D/issues/2035)) ([4ff099f](https://github.com/cornerstonejs/cornerstone3D/commit/4ff099f16c31115b6fb6371359a51dcd442e4f7b)) +- save scoord3d for tools ([#2035](https://github.com/cornerstonejs/cornerstone3D/issues/2035)) ([4ff099f](https://github.com/cornerstonejs/cornerstone3D/commit/4ff099f16c31115b6fb6371359a51dcd442e4f7b)) ## [3.14.4](https://github.com/cornerstonejs/cornerstone3D/compare/v3.14.3...v3.14.4) (2025-05-15) @@ -189,7 +189,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **version:** add version property ([#2068](https://github.com/cornerstonejs/cornerstone3D/issues/2068)) ([b7db4cc](https://github.com/cornerstonejs/cornerstone3D/commit/b7db4cc157027ebbd108c9ef9e4a01abb0769864)) +- **version:** add version property ([#2068](https://github.com/cornerstonejs/cornerstone3D/issues/2068)) ([b7db4cc](https://github.com/cornerstonejs/cornerstone3D/commit/b7db4cc157027ebbd108c9ef9e4a01abb0769864)) # [3.13.0](https://github.com/cornerstonejs/cornerstone3D/compare/v3.12.3...v3.13.0) (2025-05-13) @@ -255,7 +255,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **copyStudyTags:** missing SeriesInstanceUID ([#2028](https://github.com/cornerstonejs/cornerstone3D/issues/2028)) ([046dad2](https://github.com/cornerstonejs/cornerstone3D/commit/046dad2270fd038adc5d4f81ac55fb4bdc477862)) +- **copyStudyTags:** missing SeriesInstanceUID ([#2028](https://github.com/cornerstonejs/cornerstone3D/issues/2028)) ([046dad2](https://github.com/cornerstonejs/cornerstone3D/commit/046dad2270fd038adc5d4f81ac55fb4bdc477862)) ## [3.10.29](https://github.com/cornerstonejs/cornerstone3D/compare/v3.10.28...v3.10.29) (2025-04-22) @@ -413,7 +413,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **segmentation:** Enhance RTSS generation, surface updates, and contour set processing; address minor tool issues ([#1962](https://github.com/cornerstonejs/cornerstone3D/issues/1962)) ([f51c38c](https://github.com/cornerstonejs/cornerstone3D/commit/f51c38c535b026fc22cf7f9195a749644aee4996)) +- **segmentation:** Enhance RTSS generation, surface updates, and contour set processing; address minor tool issues ([#1962](https://github.com/cornerstonejs/cornerstone3D/issues/1962)) ([f51c38c](https://github.com/cornerstonejs/cornerstone3D/commit/f51c38c535b026fc22cf7f9195a749644aee4996)) ## [3.8.3](https://github.com/cornerstonejs/cornerstone3D/compare/v3.8.2...v3.8.3) (2025-04-02) @@ -439,7 +439,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- Add multiframe SEG support ([#1934](https://github.com/cornerstonejs/cornerstone3D/issues/1934)) ([e469a37](https://github.com/cornerstonejs/cornerstone3D/commit/e469a373534d3413aeac91bc85aecc7b8041568d)) +- Add multiframe SEG support ([#1934](https://github.com/cornerstonejs/cornerstone3D/issues/1934)) ([e469a37](https://github.com/cornerstonejs/cornerstone3D/commit/e469a373534d3413aeac91bc85aecc7b8041568d)) ## [3.7.15](https://github.com/cornerstonejs/cornerstone3D/compare/v3.7.14...v3.7.15) (2025-03-27) @@ -477,13 +477,13 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **segmentation:** Enhance Segmentation and Annotation Tools ([#1929](https://github.com/cornerstonejs/cornerstone3D/issues/1929)) ([b952d9b](https://github.com/cornerstonejs/cornerstone3D/commit/b952d9b05a871d8b8ac91def0e029703d19b57d6)) +- **segmentation:** Enhance Segmentation and Annotation Tools ([#1929](https://github.com/cornerstonejs/cornerstone3D/issues/1929)) ([b952d9b](https://github.com/cornerstonejs/cornerstone3D/commit/b952d9b05a871d8b8ac91def0e029703d19b57d6)) ## [3.7.6](https://github.com/cornerstonejs/cornerstone3D/compare/v3.7.5...v3.7.6) (2025-03-20) ### Bug Fixes -- Only copy patient/study attributes rather than all ([#1887](https://github.com/cornerstonejs/cornerstone3D/issues/1887)) ([7584ec7](https://github.com/cornerstonejs/cornerstone3D/commit/7584ec78da64bf860ce389aad233f38a90020187)) +- Only copy patient/study attributes rather than all ([#1887](https://github.com/cornerstonejs/cornerstone3D/issues/1887)) ([7584ec7](https://github.com/cornerstonejs/cornerstone3D/commit/7584ec78da64bf860ce389aad233f38a90020187)) ## [3.7.5](https://github.com/cornerstonejs/cornerstone3D/compare/v3.7.4...v3.7.5) (2025-03-19) @@ -505,7 +505,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- reintroduce seg events and chunks ([#1913](https://github.com/cornerstonejs/cornerstone3D/issues/1913)) ([c23048c](https://github.com/cornerstonejs/cornerstone3D/commit/c23048c64295954c97f4ccbaefd7b34906bdda1d)) +- reintroduce seg events and chunks ([#1913](https://github.com/cornerstonejs/cornerstone3D/issues/1913)) ([c23048c](https://github.com/cornerstonejs/cornerstone3D/commit/c23048c64295954c97f4ccbaefd7b34906bdda1d)) # [3.7.0](https://github.com/cornerstonejs/cornerstone3D/compare/v3.6.1...v3.7.0) (2025-03-18) @@ -539,7 +539,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **adapters:** overlapping segs with labelmap images ([#1815](https://github.com/cornerstonejs/cornerstone3D/issues/1815)) ([95972e2](https://github.com/cornerstonejs/cornerstone3D/commit/95972e205c722c72f37f951aec0ab6bb25ef0773)) +- **adapters:** overlapping segs with labelmap images ([#1815](https://github.com/cornerstonejs/cornerstone3D/issues/1815)) ([95972e2](https://github.com/cornerstonejs/cornerstone3D/commit/95972e205c722c72f37f951aec0ab6bb25ef0773)) ## [3.3.4](https://github.com/cornerstonejs/cornerstone3D/compare/v3.3.3...v3.3.4) (2025-03-13) @@ -605,7 +605,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **surface-segmentation:** add visibilty configuration for surface segmentation ([#1846](https://github.com/cornerstonejs/cornerstone3D/issues/1846)) ([e1b5bc6](https://github.com/cornerstonejs/cornerstone3D/commit/e1b5bc646f3997be88ec237f86406c310420379a)) +- **surface-segmentation:** add visibilty configuration for surface segmentation ([#1846](https://github.com/cornerstonejs/cornerstone3D/issues/1846)) ([e1b5bc6](https://github.com/cornerstonejs/cornerstone3D/commit/e1b5bc646f3997be88ec237f86406c310420379a)) ## [3.0.1](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0...v3.0.1) (2025-02-27) @@ -615,13 +615,13 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- Cornerstone3D 3.0 ([#1865](https://github.com/cornerstonejs/cornerstone3D/issues/1865)) ([fe65459](https://github.com/cornerstonejs/cornerstone3D/commit/fe654590d16414e76361e1b2826fd64c3734ae87)) +- Cornerstone3D 3.0 ([#1865](https://github.com/cornerstonejs/cornerstone3D/issues/1865)) ([fe65459](https://github.com/cornerstonejs/cornerstone3D/commit/fe654590d16414e76361e1b2826fd64c3734ae87)) # [3.0.0-beta.6](https://github.com/cornerstonejs/cornerstone3D/compare/v3.0.0-beta.5...v3.0.0-beta.6) (2025-02-27) ### Features -- Add key image adapters for key image point mark ([#1754](https://github.com/cornerstonejs/cornerstone3D/issues/1754)) ([a1fd3f9](https://github.com/cornerstonejs/cornerstone3D/commit/a1fd3f9d0ea40d53cafd792d59bc1dbfc90663a5)) +- Add key image adapters for key image point mark ([#1754](https://github.com/cornerstonejs/cornerstone3D/issues/1754)) ([a1fd3f9](https://github.com/cornerstonejs/cornerstone3D/commit/a1fd3f9d0ea40d53cafd792d59bc1dbfc90663a5)) ## [2.17.3](https://github.com/dcmjs-org/dcmjs/compare/v2.17.2...v2.17.3) (2025-01-22) @@ -631,7 +631,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **seg:** Refactor LabelmapBaseTool and SphereScissorsTool to fix sphere bug in stack ([#1772](https://github.com/dcmjs-org/dcmjs/issues/1772)) ([5eeda06](https://github.com/dcmjs-org/dcmjs/commit/5eeda0626a2722397aba910ae4bd4a5e650d32c6)) +- **seg:** Refactor LabelmapBaseTool and SphereScissorsTool to fix sphere bug in stack ([#1772](https://github.com/dcmjs-org/dcmjs/issues/1772)) ([5eeda06](https://github.com/dcmjs-org/dcmjs/commit/5eeda0626a2722397aba910ae4bd4a5e650d32c6)) ## [2.17.1](https://github.com/dcmjs-org/dcmjs/compare/v2.17.0...v2.17.1) (2025-01-22) @@ -745,7 +745,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **adapters:** default to creation of labelmap images in 2D instead of a volume ([#1692](https://github.com/dcmjs-org/dcmjs/issues/1692)) ([960b75a](https://github.com/dcmjs-org/dcmjs/commit/960b75a2ebed3a141ceefcf4f38fb6de388a4ca0)) +- **adapters:** default to creation of labelmap images in 2D instead of a volume ([#1692](https://github.com/dcmjs-org/dcmjs/issues/1692)) ([960b75a](https://github.com/dcmjs-org/dcmjs/commit/960b75a2ebed3a141ceefcf4f38fb6de388a4ca0)) ## [2.11.7](https://github.com/dcmjs-org/dcmjs/compare/v2.11.6...v2.11.7) (2024-12-13) @@ -799,7 +799,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **examples:** stack and volume segmentation ([#1684](https://github.com/dcmjs-org/dcmjs/issues/1684)) ([5149b31](https://github.com/dcmjs-org/dcmjs/commit/5149b31d54dca9ff8d0f24dcf8d432b6df1dd29b)) +- **examples:** stack and volume segmentation ([#1684](https://github.com/dcmjs-org/dcmjs/issues/1684)) ([5149b31](https://github.com/dcmjs-org/dcmjs/commit/5149b31d54dca9ff8d0f24dcf8d432b6df1dd29b)) ## [2.8.3](https://github.com/dcmjs-org/dcmjs/compare/v2.8.2...v2.8.3) (2024-12-06) @@ -813,7 +813,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **examples:** fix the stack and volume segmentation adapters example ([#1680](https://github.com/dcmjs-org/dcmjs/issues/1680)) ([85e8ca4](https://github.com/dcmjs-org/dcmjs/commit/85e8ca4bda0a2948565ac88c9ddd07aa2b02f2d3)) +- **examples:** fix the stack and volume segmentation adapters example ([#1680](https://github.com/dcmjs-org/dcmjs/issues/1680)) ([85e8ca4](https://github.com/dcmjs-org/dcmjs/commit/85e8ca4bda0a2948565ac88c9ddd07aa2b02f2d3)) # [2.8.0](https://github.com/dcmjs-org/dcmjs/compare/v2.7.4...v2.8.0) (2024-12-05) @@ -839,7 +839,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **webgpu:** growcut base tools ([#1670](https://github.com/dcmjs-org/dcmjs/issues/1670)) ([7494c9e](https://github.com/dcmjs-org/dcmjs/commit/7494c9e683cd5d27eb9f6f71d958786381a774d5)) +- **webgpu:** growcut base tools ([#1670](https://github.com/dcmjs-org/dcmjs/issues/1670)) ([7494c9e](https://github.com/dcmjs-org/dcmjs/commit/7494c9e683cd5d27eb9f6f71d958786381a774d5)) ## [2.6.5](https://github.com/dcmjs-org/dcmjs/compare/v2.6.4...v2.6.5) (2024-12-04) @@ -865,7 +865,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- automatic segmentation using onnx to run local AI algorithms ([#1656](https://github.com/dcmjs-org/dcmjs/issues/1656)) ([5b5856f](https://github.com/dcmjs-org/dcmjs/commit/5b5856fe19eb3ac61fe7877283ed5a7f8a7fa53c)) +- automatic segmentation using onnx to run local AI algorithms ([#1656](https://github.com/dcmjs-org/dcmjs/issues/1656)) ([5b5856f](https://github.com/dcmjs-org/dcmjs/commit/5b5856fe19eb3ac61fe7877283ed5a7f8a7fa53c)) ## [2.5.3](https://github.com/dcmjs-org/dcmjs/compare/v2.5.2...v2.5.3) (2024-12-03) @@ -959,7 +959,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **examples:** fix dynamicallyaddannotations, webloader, segmentationVolume examples ([#1592](https://github.com/dcmjs-org/dcmjs/issues/1592)) ([6b07a48](https://github.com/dcmjs-org/dcmjs/commit/6b07a48cb47026adeb8fd6b961140efd65c26c01)) +- **examples:** fix dynamicallyaddannotations, webloader, segmentationVolume examples ([#1592](https://github.com/dcmjs-org/dcmjs/issues/1592)) ([6b07a48](https://github.com/dcmjs-org/dcmjs/commit/6b07a48cb47026adeb8fd6b961140efd65c26c01)) ## [2.2.6](https://github.com/dcmjs-org/dcmjs/compare/v2.2.5...v2.2.6) (2024-11-15) @@ -1037,7 +1037,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **jpeg:** Export JpegImage from JPEG codecs ([#1552](https://github.com/dcmjs-org/dcmjs/issues/1552)) ([fbaa769](https://github.com/dcmjs-org/dcmjs/commit/fbaa7690bbd493ec81e2d729af124c217fd5d46a)) +- **jpeg:** Export JpegImage from JPEG codecs ([#1552](https://github.com/dcmjs-org/dcmjs/issues/1552)) ([fbaa769](https://github.com/dcmjs-org/dcmjs/commit/fbaa7690bbd493ec81e2d729af124c217fd5d46a)) ## [2.1.10](https://github.com/dcmjs-org/dcmjs/compare/v2.1.9...v2.1.10) (2024-11-05) @@ -1075,7 +1075,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **arrow-adapter:** save 2nd point for arrow annotation ([#1450](https://github.com/dcmjs-org/dcmjs/issues/1450)) ([d66a905](https://github.com/dcmjs-org/dcmjs/commit/d66a90591f45d791aeb035736ad40c297650b26d)) +- **arrow-adapter:** save 2nd point for arrow annotation ([#1450](https://github.com/dcmjs-org/dcmjs/issues/1450)) ([d66a905](https://github.com/dcmjs-org/dcmjs/commit/d66a90591f45d791aeb035736ad40c297650b26d)) ## [2.1.1](https://github.com/dcmjs-org/dcmjs/compare/v2.1.0...v2.1.1) (2024-10-30) @@ -1085,7 +1085,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **tools:** Enhance error handling, documentation and code maintainability ([#1522](https://github.com/dcmjs-org/dcmjs/issues/1522)) ([7a1c67f](https://github.com/dcmjs-org/dcmjs/commit/7a1c67f9d1eaf97654a3ec3bfdec729289ae3d6b)) +- **tools:** Enhance error handling, documentation and code maintainability ([#1522](https://github.com/dcmjs-org/dcmjs/issues/1522)) ([7a1c67f](https://github.com/dcmjs-org/dcmjs/commit/7a1c67f9d1eaf97654a3ec3bfdec729289ae3d6b)) ## [2.0.5](https://github.com/dcmjs-org/dcmjs/compare/v2.0.4...v2.0.5) (2024-10-29) @@ -1103,7 +1103,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- peer dependency issues ([#1521](https://github.com/dcmjs-org/dcmjs/issues/1521)) ([a2c2154](https://github.com/dcmjs-org/dcmjs/commit/a2c21542c4858e1568511c337695474391745939)) +- peer dependency issues ([#1521](https://github.com/dcmjs-org/dcmjs/issues/1521)) ([a2c2154](https://github.com/dcmjs-org/dcmjs/commit/a2c21542c4858e1568511c337695474391745939)) ## [2.0.1](https://github.com/dcmjs-org/dcmjs/compare/v2.0.0...v2.0.1) (2024-10-29) @@ -1113,7 +1113,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- Cornerstone3D 2.0 ([#1400](https://github.com/dcmjs-org/dcmjs/issues/1400)) ([692d9af](https://github.com/dcmjs-org/dcmjs/commit/692d9afc6a8bcfa801c4aff0eec7706812bbfed8)) +- Cornerstone3D 2.0 ([#1400](https://github.com/dcmjs-org/dcmjs/issues/1400)) ([692d9af](https://github.com/dcmjs-org/dcmjs/commit/692d9afc6a8bcfa801c4aff0eec7706812bbfed8)) # [2.0.0-beta.30](https://github.com/dcmjs-org/dcmjs/compare/v2.0.0-beta.29...v2.0.0-beta.30) (2024-10-04) @@ -1135,13 +1135,13 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- segmentation and versioned docs([#1488](https://github.com/dcmjs-org/dcmjs/issues/1488)) ([580dc4b](https://github.com/dcmjs-org/dcmjs/commit/580dc4b06a8e9c9698de8f8a37921a581e77ce84)) +- segmentation and versioned docs([#1488](https://github.com/dcmjs-org/dcmjs/issues/1488)) ([580dc4b](https://github.com/dcmjs-org/dcmjs/commit/580dc4b06a8e9c9698de8f8a37921a581e77ce84)) # [2.0.0-beta.28](https://github.com/dcmjs-org/dcmjs/compare/v2.0.0-beta.27...v2.0.0-beta.28) (2024-09-12) ### Features -- **segmentation:** Refactor segmentation and style handling ([#1449](https://github.com/dcmjs-org/dcmjs/issues/1449)) ([51f7cde](https://github.com/dcmjs-org/dcmjs/commit/51f7cde477dda5f580ab020b69a0a54a7d31efcb)) +- **segmentation:** Refactor segmentation and style handling ([#1449](https://github.com/dcmjs-org/dcmjs/issues/1449)) ([51f7cde](https://github.com/dcmjs-org/dcmjs/commit/51f7cde477dda5f580ab020b69a0a54a7d31efcb)) # [2.0.0-beta.27](https://github.com/dcmjs-org/dcmjs/compare/v2.0.0-beta.26...v2.0.0-beta.27) (2024-08-26) @@ -1165,7 +1165,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **pmap:** added parametric map adapter ([#1382](https://github.com/dcmjs-org/dcmjs/issues/1382)) ([4b5705d](https://github.com/dcmjs-org/dcmjs/commit/4b5705d937c202fb9f84c5aac2158e9577aa5aa4)) +- **pmap:** added parametric map adapter ([#1382](https://github.com/dcmjs-org/dcmjs/issues/1382)) ([4b5705d](https://github.com/dcmjs-org/dcmjs/commit/4b5705d937c202fb9f84c5aac2158e9577aa5aa4)) ## [1.82.7](https://github.com/dcmjs-org/dcmjs/compare/v1.82.6...v1.82.7) (2024-07-24) @@ -1191,7 +1191,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- wheel register API change to use binding ([#1422](https://github.com/dcmjs-org/dcmjs/issues/1422)) ([9e1fb8d](https://github.com/dcmjs-org/dcmjs/commit/9e1fb8df7508afc56df96e243be21bc34c3b0809)) +- wheel register API change to use binding ([#1422](https://github.com/dcmjs-org/dcmjs/issues/1422)) ([9e1fb8d](https://github.com/dcmjs-org/dcmjs/commit/9e1fb8df7508afc56df96e243be21bc34c3b0809)) # [2.0.0-beta.19](https://github.com/dcmjs-org/dcmjs/compare/v2.0.0-beta.18...v2.0.0-beta.19) (2024-07-04) @@ -1201,7 +1201,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- new segmentation state model per viewport ([#1374](https://github.com/dcmjs-org/dcmjs/issues/1374)) ([05cb720](https://github.com/dcmjs-org/dcmjs/commit/05cb7206e76ff07aafb953125b8e8e1a1be53d23)) +- new segmentation state model per viewport ([#1374](https://github.com/dcmjs-org/dcmjs/issues/1374)) ([05cb720](https://github.com/dcmjs-org/dcmjs/commit/05cb7206e76ff07aafb953125b8e8e1a1be53d23)) # [2.0.0-beta.17](https://github.com/dcmjs-org/dcmjs/compare/v2.0.0-beta.16...v2.0.0-beta.17) (2024-06-21) @@ -1231,7 +1231,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **viewport:** Various viewport-related changes and improvements ([#1324](https://github.com/dcmjs-org/dcmjs/issues/1324)) ([ea63b3e](https://github.com/dcmjs-org/dcmjs/commit/ea63b3ef88ace08ff1291a2f67989d027e51e41e)) +- **viewport:** Various viewport-related changes and improvements ([#1324](https://github.com/dcmjs-org/dcmjs/issues/1324)) ([ea63b3e](https://github.com/dcmjs-org/dcmjs/commit/ea63b3ef88ace08ff1291a2f67989d027e51e41e)) # [2.0.0-beta.10](https://github.com/dcmjs-org/dcmjs/compare/v2.0.0-beta.9...v2.0.0-beta.10) (2024-06-13) @@ -1241,7 +1241,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **dicom loader:** switch the build to es modules with types ([#1322](https://github.com/dcmjs-org/dcmjs/issues/1322)) ([89e95eb](https://github.com/dcmjs-org/dcmjs/commit/89e95eba292e3322c031d92bcc71a39bdd65e330)) +- **dicom loader:** switch the build to es modules with types ([#1322](https://github.com/dcmjs-org/dcmjs/issues/1322)) ([89e95eb](https://github.com/dcmjs-org/dcmjs/commit/89e95eba292e3322c031d92bcc71a39bdd65e330)) # [2.0.0-beta.8](https://github.com/dcmjs-org/dcmjs/compare/v2.0.0-beta.7...v2.0.0-beta.8) (2024-06-12) @@ -1251,7 +1251,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **structuredClone:** drop lodash.clonedeep in favor of structuredClone ([#517](https://github.com/dcmjs-org/dcmjs/issues/517)) ([04c863d](https://github.com/dcmjs-org/dcmjs/commit/04c863d442195ed9ad8271a581be646d78baca70)) +- **structuredClone:** drop lodash.clonedeep in favor of structuredClone ([#517](https://github.com/dcmjs-org/dcmjs/issues/517)) ([04c863d](https://github.com/dcmjs-org/dcmjs/commit/04c863d442195ed9ad8271a581be646d78baca70)) ## [1.84.1](https://github.com/dcmjs-org/dcmjs/compare/v1.84.0...v1.84.1) (2024-08-19) @@ -1281,7 +1281,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **pmap:** added parametric map adapter ([#1382](https://github.com/dcmjs-org/dcmjs/issues/1382)) ([4b5705d](https://github.com/dcmjs-org/dcmjs/commit/4b5705d937c202fb9f84c5aac2158e9577aa5aa4)) +- **pmap:** added parametric map adapter ([#1382](https://github.com/dcmjs-org/dcmjs/issues/1382)) ([4b5705d](https://github.com/dcmjs-org/dcmjs/commit/4b5705d937c202fb9f84c5aac2158e9577aa5aa4)) ## [1.82.7](https://github.com/dcmjs-org/dcmjs/compare/v1.82.6...v1.82.7) (2024-07-24) @@ -1343,7 +1343,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **pmap:** added parametric map adapter ([#1382](https://github.com/dcmjs-org/dcmjs/issues/1382)) ([4b5705d](https://github.com/dcmjs-org/dcmjs/commit/4b5705d937c202fb9f84c5aac2158e9577aa5aa4)) +- **pmap:** added parametric map adapter ([#1382](https://github.com/dcmjs-org/dcmjs/issues/1382)) ([4b5705d](https://github.com/dcmjs-org/dcmjs/commit/4b5705d937c202fb9f84c5aac2158e9577aa5aa4)) ## [1.82.7](https://github.com/dcmjs-org/dcmjs/compare/v1.82.6...v1.82.7) (2024-07-24) @@ -1553,7 +1553,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **adapters-export:** Set SpecificCharacterSet to "ISO_IR 192" in Cornerstone and Cornerstone3D adapters ([#1257](https://github.com/dcmjs-org/dcmjs/issues/1257)) ([03a51a2](https://github.com/dcmjs-org/dcmjs/commit/03a51a2f81f2126926c7bead455de58d5b364990)) +- **adapters-export:** Set SpecificCharacterSet to "ISO_IR 192" in Cornerstone and Cornerstone3D adapters ([#1257](https://github.com/dcmjs-org/dcmjs/issues/1257)) ([03a51a2](https://github.com/dcmjs-org/dcmjs/commit/03a51a2f81f2126926c7bead455de58d5b364990)) # [1.74.0](https://github.com/dcmjs-org/dcmjs/compare/v1.73.1...v1.74.0) (2024-05-17) @@ -1707,13 +1707,13 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **cornerstone-dicom-sr:** Freehand SR hydration support ([#1160](https://github.com/dcmjs-org/dcmjs/issues/1160)) ([5e778a1](https://github.com/dcmjs-org/dcmjs/commit/5e778a1b637eaeb85fe3d3c3120388a322d33c53)) +- **cornerstone-dicom-sr:** Freehand SR hydration support ([#1160](https://github.com/dcmjs-org/dcmjs/issues/1160)) ([5e778a1](https://github.com/dcmjs-org/dcmjs/commit/5e778a1b637eaeb85fe3d3c3120388a322d33c53)) # [1.68.0](https://github.com/dcmjs-org/dcmjs/compare/v1.67.1...v1.68.0) (2024-03-29) ### Features -- **MetaDataProvider:** Update metadata provider ([#1165](https://github.com/dcmjs-org/dcmjs/issues/1165)) ([df5583d](https://github.com/dcmjs-org/dcmjs/commit/df5583dbac34087182887ea2ca457416a77bf0b6)) +- **MetaDataProvider:** Update metadata provider ([#1165](https://github.com/dcmjs-org/dcmjs/issues/1165)) ([df5583d](https://github.com/dcmjs-org/dcmjs/commit/df5583dbac34087182887ea2ca457416a77bf0b6)) ## [1.67.1](https://github.com/dcmjs-org/dcmjs/compare/v1.67.0...v1.67.1) (2024-03-28) @@ -1803,7 +1803,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **bundles:** try to bundle adapters correctly ([#1124](https://github.com/dcmjs-org/dcmjs/issues/1124)) ([143e2b5](https://github.com/dcmjs-org/dcmjs/commit/143e2b5c8a7e4955f17c4483991ef591f07f452f)) +- **bundles:** try to bundle adapters correctly ([#1124](https://github.com/dcmjs-org/dcmjs/issues/1124)) ([143e2b5](https://github.com/dcmjs-org/dcmjs/commit/143e2b5c8a7e4955f17c4483991ef591f07f452f)) ## [1.64.2](https://github.com/dcmjs-org/dcmjs/compare/v1.64.1...v1.64.2) (2024-02-26) @@ -2017,7 +2017,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **interpolation:** Contour segmentation interpolation for freehand and SplineROI ([#1003](https://github.com/dcmjs-org/dcmjs/issues/1003)) ([8434c8e](https://github.com/dcmjs-org/dcmjs/commit/8434c8e7386c1e5980099c325e087c60e8c270a1)) +- **interpolation:** Contour segmentation interpolation for freehand and SplineROI ([#1003](https://github.com/dcmjs-org/dcmjs/issues/1003)) ([8434c8e](https://github.com/dcmjs-org/dcmjs/commit/8434c8e7386c1e5980099c325e087c60e8c270a1)) ## [1.48.2](https://github.com/dcmjs-org/dcmjs/compare/v1.48.1...v1.48.2) (2024-01-22) @@ -2059,7 +2059,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **UltrasoundDirectionalTool:** add us directional adapter ([#999](https://github.com/dcmjs-org/dcmjs/issues/999)) ([1f78fd2](https://github.com/dcmjs-org/dcmjs/commit/1f78fd2859865ad19200096378ff7ce224209fb5)) +- **UltrasoundDirectionalTool:** add us directional adapter ([#999](https://github.com/dcmjs-org/dcmjs/issues/999)) ([1f78fd2](https://github.com/dcmjs-org/dcmjs/commit/1f78fd2859865ad19200096378ff7ce224209fb5)) ## [1.45.1](https://github.com/dcmjs-org/dcmjs/compare/v1.45.0...v1.45.1) (2024-01-12) @@ -2089,7 +2089,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **segmentation:** notify overlapping segments in generateToolState function ([#989](https://github.com/dcmjs-org/dcmjs/issues/989)) ([626cdbc](https://github.com/dcmjs-org/dcmjs/commit/626cdbc94d27c148ecd18ac8032174e2f202afbd)) +- **segmentation:** notify overlapping segments in generateToolState function ([#989](https://github.com/dcmjs-org/dcmjs/issues/989)) ([626cdbc](https://github.com/dcmjs-org/dcmjs/commit/626cdbc94d27c148ecd18ac8032174e2f202afbd)) ## [1.43.6](https://github.com/dcmjs-org/dcmjs/compare/v1.43.5...v1.43.6) (2024-01-08) @@ -2119,7 +2119,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **vtk.js:** Upgrade version and add Segment Select Tool ([#922](https://github.com/dcmjs-org/dcmjs/issues/922)) ([d5f6abb](https://github.com/dcmjs-org/dcmjs/commit/d5f6abbfd0ca7f868d229696d27f047fb47f99cc)) +- **vtk.js:** Upgrade version and add Segment Select Tool ([#922](https://github.com/dcmjs-org/dcmjs/issues/922)) ([d5f6abb](https://github.com/dcmjs-org/dcmjs/commit/d5f6abbfd0ca7f868d229696d27f047fb47f99cc)) ## [1.42.1](https://github.com/dcmjs-org/dcmjs/compare/v1.42.0...v1.42.1) (2024-01-03) @@ -2129,7 +2129,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **tool:** Bidirectional creation on largest segment slice ([#937](https://github.com/dcmjs-org/dcmjs/issues/937)) ([b4ee6bf](https://github.com/dcmjs-org/dcmjs/commit/b4ee6bfdad64c208e37183a39681ba80c06ffe85)) +- **tool:** Bidirectional creation on largest segment slice ([#937](https://github.com/dcmjs-org/dcmjs/issues/937)) ([b4ee6bf](https://github.com/dcmjs-org/dcmjs/commit/b4ee6bfdad64c208e37183a39681ba80c06ffe85)) # [1.41.0](https://github.com/dcmjs-org/dcmjs/compare/v1.40.3...v1.41.0) (2023-12-15) @@ -2163,7 +2163,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- Provide access to tracking unique measurement to allow combining parts of a measurement ([#932](https://github.com/dcmjs-org/dcmjs/issues/932)) ([65245ce](https://github.com/dcmjs-org/dcmjs/commit/65245ce8924776e20c78b18b6e5a86283b6e2668)) +- Provide access to tracking unique measurement to allow combining parts of a measurement ([#932](https://github.com/dcmjs-org/dcmjs/issues/932)) ([65245ce](https://github.com/dcmjs-org/dcmjs/commit/65245ce8924776e20c78b18b6e5a86283b6e2668)) ## [1.37.1](https://github.com/dcmjs-org/dcmjs/compare/v1.37.0...v1.37.1) (2023-12-11) @@ -2189,7 +2189,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **segmentation:** segmentation color change and fix seg import([#920](https://github.com/dcmjs-org/dcmjs/issues/920)) ([3af4437](https://github.com/dcmjs-org/dcmjs/commit/3af4437c4b20f7cc2556de4d655fc8f118e310a4)) +- **segmentation:** segmentation color change and fix seg import([#920](https://github.com/dcmjs-org/dcmjs/issues/920)) ([3af4437](https://github.com/dcmjs-org/dcmjs/commit/3af4437c4b20f7cc2556de4d655fc8f118e310a4)) ## [1.35.3](https://github.com/dcmjs-org/dcmjs/compare/v1.35.2...v1.35.3) (2023-12-01) @@ -2327,7 +2327,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **colormap:** add colormap props and default properties ([#834](https://github.com/dcmjs-org/dcmjs/issues/834)) ([475914d](https://github.com/dcmjs-org/dcmjs/commit/475914d0eaa35f1ae65b989c74efda042dc6d97a)) +- **colormap:** add colormap props and default properties ([#834](https://github.com/dcmjs-org/dcmjs/issues/834)) ([475914d](https://github.com/dcmjs-org/dcmjs/commit/475914d0eaa35f1ae65b989c74efda042dc6d97a)) ## [1.21.2](https://github.com/dcmjs-org/dcmjs/compare/v1.21.1...v1.21.2) (2023-10-16) @@ -2353,13 +2353,13 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **exports:** clean up rtss exports ([#814](https://github.com/dcmjs-org/dcmjs/issues/814)) ([a0dd324](https://github.com/dcmjs-org/dcmjs/commit/a0dd32499cc58001e4f49e2bda8d034b7f4ef48f)) +- **exports:** clean up rtss exports ([#814](https://github.com/dcmjs-org/dcmjs/issues/814)) ([a0dd324](https://github.com/dcmjs-org/dcmjs/commit/a0dd32499cc58001e4f49e2bda8d034b7f4ef48f)) # [1.20.0](https://github.com/dcmjs-org/dcmjs/compare/v1.19.4...v1.20.0) (2023-10-06) ### Features -- **adapter:** add RTSS Adapter and Labelmaps to Contours convertor ([#734](https://github.com/dcmjs-org/dcmjs/issues/734)) ([e3e05bd](https://github.com/dcmjs-org/dcmjs/commit/e3e05bd5ec0d851576fc76a2440e688c0a6e70d9)) +- **adapter:** add RTSS Adapter and Labelmaps to Contours convertor ([#734](https://github.com/dcmjs-org/dcmjs/issues/734)) ([e3e05bd](https://github.com/dcmjs-org/dcmjs/commit/e3e05bd5ec0d851576fc76a2440e688c0a6e70d9)) ## [1.19.4](https://github.com/dcmjs-org/dcmjs/compare/v1.19.3...v1.19.4) (2023-10-04) @@ -2425,7 +2425,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **cine:** fix cine black images for slow computers ([#761](https://github.com/dcmjs-org/dcmjs/issues/761)) ([b110bda](https://github.com/dcmjs-org/dcmjs/commit/b110bdad1d5c561721d379bbd20cfe07639756ef)) +- **cine:** fix cine black images for slow computers ([#761](https://github.com/dcmjs-org/dcmjs/issues/761)) ([b110bda](https://github.com/dcmjs-org/dcmjs/commit/b110bdad1d5c561721d379bbd20cfe07639756ef)) # [1.15.0](https://github.com/dcmjs-org/dcmjs/compare/v1.14.4...v1.15.0) (2023-09-12) @@ -2455,7 +2455,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **brush size:** brush size should be calculated in world not in canvas ([#771](https://github.com/dcmjs-org/dcmjs/issues/771)) ([6ca1e3a](https://github.com/dcmjs-org/dcmjs/commit/6ca1e3a6d7bc445bbe8aed08a46ec4998f9f8c54)) +- **brush size:** brush size should be calculated in world not in canvas ([#771](https://github.com/dcmjs-org/dcmjs/issues/771)) ([6ca1e3a](https://github.com/dcmjs-org/dcmjs/commit/6ca1e3a6d7bc445bbe8aed08a46ec4998f9f8c54)) ## [1.13.2](https://github.com/dcmjs-org/dcmjs/compare/v1.13.1...v1.13.2) (2023-09-05) @@ -2465,7 +2465,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- **Adapters:** adaptersSEG cornerstoneSR to cornerstoneSEG ([#766](https://github.com/dcmjs-org/dcmjs/issues/766)) ([e5d7826](https://github.com/dcmjs-org/dcmjs/commit/e5d78260320681714c6371a1747bdab8956e6e6b)) +- **Adapters:** adaptersSEG cornerstoneSR to cornerstoneSEG ([#766](https://github.com/dcmjs-org/dcmjs/issues/766)) ([e5d7826](https://github.com/dcmjs-org/dcmjs/commit/e5d78260320681714c6371a1747bdab8956e6e6b)) # [1.13.0](https://github.com/dcmjs-org/dcmjs/compare/v1.12.1...v1.13.0) (2023-08-30) @@ -2547,7 +2547,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **segmentation export:** add new cornerstone3D segmentation export adapter ([#692](https://github.com/dcmjs-org/dcmjs/issues/692)) ([9e743f5](https://github.com/dcmjs-org/dcmjs/commit/9e743f5d2b58dedb17dcbe0de40f42e703f77b14)) +- **segmentation export:** add new cornerstone3D segmentation export adapter ([#692](https://github.com/dcmjs-org/dcmjs/issues/692)) ([9e743f5](https://github.com/dcmjs-org/dcmjs/commit/9e743f5d2b58dedb17dcbe0de40f42e703f77b14)) ## [1.7.2](https://github.com/dcmjs-org/dcmjs/compare/v1.7.1...v1.7.2) (2023-07-27) @@ -2565,7 +2565,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -- **calibration:** Add calibration type labels (ERMF, PROJ, USER) ([#638](https://github.com/dcmjs-org/dcmjs/issues/638)) ([0aafbc2](https://github.com/dcmjs-org/dcmjs/commit/0aafbc2be6f50f4733792b7eb924863ec3200f23)) +- **calibration:** Add calibration type labels (ERMF, PROJ, USER) ([#638](https://github.com/dcmjs-org/dcmjs/issues/638)) ([0aafbc2](https://github.com/dcmjs-org/dcmjs/commit/0aafbc2be6f50f4733792b7eb924863ec3200f23)) # [1.5.0](https://github.com/dcmjs-org/dcmjs/compare/v1.4.6...v1.5.0) (2023-07-18) @@ -2695,263 +2695,263 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -- 🐛 adding readme notes ([#191](https://github.com/dcmjs-org/dcmjs/issues/191)) ([459260d](https://github.com/dcmjs-org/dcmjs/commit/459260d6e2a6f905729ddf68b1f12d3140b53849)) -- 🐛 fix array format regression from commit 70b24332783d63c9db2ed21d512d9f7b526c5222 ([#236](https://github.com/dcmjs-org/dcmjs/issues/236)) ([5441063](https://github.com/dcmjs-org/dcmjs/commit/5441063d0395ede6d9f8bd3ac0d92ee14f6ef209)) -- 🐛 Fix rotation mapping for SEG cornerstone adapter ([#151](https://github.com/dcmjs-org/dcmjs/issues/151)) ([3fab68c](https://github.com/dcmjs-org/dcmjs/commit/3fab68cbfd95f82820663b9fc99a2b0cd07e43c8)) -- 🐛 Harden Segmentation import for different possible SEGs ([#146](https://github.com/dcmjs-org/dcmjs/issues/146)) ([c4952bc](https://github.com/dcmjs-org/dcmjs/commit/c4952bc5842bab80a5d928de0d860f89afc8f400)) -- 🐛 IDC Re [#2003](https://github.com/dcmjs-org/dcmjs/issues/2003): fix regression in parsing segmentation orietations ([#220](https://github.com/dcmjs-org/dcmjs/issues/220)) ([5c0c6a8](https://github.com/dcmjs-org/dcmjs/commit/5c0c6a85e67b25ce5f39412ced37d8e825691481)) -- 🐛 IDC2733: find segmentations reference source image Ids ([#253](https://github.com/dcmjs-org/dcmjs/issues/253)) ([f3e7101](https://github.com/dcmjs-org/dcmjs/commit/f3e71016dffa233bf0fb912cc7cf413718b8a1a9)) -- 🐛 ignore frames without SourceImageSequence information when loading a segmentation ([#198](https://github.com/dcmjs-org/dcmjs/issues/198)) ([82709c4](https://github.com/dcmjs-org/dcmjs/commit/82709c4a8a317aa1354244010300ab9b902802dd)) -- 🐛 indentation in nearlyEqual ([#202](https://github.com/dcmjs-org/dcmjs/issues/202)) ([989d6c9](https://github.com/dcmjs-org/dcmjs/commit/989d6c9a80686425563c55424ac1795e6a06cd7b)) -- 🐛 relax condition in nearlyEquals check for detecting numbers near to zero ([#304](https://github.com/dcmjs-org/dcmjs/issues/304)) ([974cddd](https://github.com/dcmjs-org/dcmjs/commit/974cddd785c076f1ac0211b534a7c0b82a4ba68a)) -- 🐛 When converting to multiframe, fix IPP issues ([#152](https://github.com/dcmjs-org/dcmjs/issues/152)) ([80496e4](https://github.com/dcmjs-org/dcmjs/commit/80496e422152c1a3dfd850a145011dd3dc632964)) -- **adapter:** Removed comment around getTID300RepresentationArguments 'tool' parameter ([#322](https://github.com/dcmjs-org/dcmjs/issues/322)) ([d8f05ff](https://github.com/dcmjs-org/dcmjs/commit/d8f05ffb9ef1b5cce254980a597b4c428ffdfb6e)), closes [#306](https://github.com/dcmjs-org/dcmjs/issues/306) -- **adapters:** Measurement reports can throw exceptions that prevent loading ([#458](https://github.com/dcmjs-org/dcmjs/issues/458)) ([7bc7d8a](https://github.com/dcmjs-org/dcmjs/commit/7bc7d8aaeb5aa0df91c24e278665d14f590ec234)) -- **adapters:** Update rollup to newer version ([#407](https://github.com/dcmjs-org/dcmjs/issues/407)) ([543675f](https://github.com/dcmjs-org/dcmjs/commit/543675f1269f8b739764291b1c27b40470c48c63)) -- **adapters:** Update the build a little to allow debugging into typescript ([#439](https://github.com/dcmjs-org/dcmjs/issues/439)) ([05e6419](https://github.com/dcmjs-org/dcmjs/commit/05e6419e1a6705f40c367ac4b52c3975f6fd25c6)) -- **adapter:** The rectangle encoding of SR ([#437](https://github.com/dcmjs-org/dcmjs/issues/437)) ([bff23ec](https://github.com/dcmjs-org/dcmjs/commit/bff23ecfe551312a1339211ddb7838993514a377)) -- **add check for nullable numeric string vrs:** adds a check for nullable numeric strinv vrs ([#150](https://github.com/dcmjs-org/dcmjs/issues/150)) ([75046c4](https://github.com/dcmjs-org/dcmjs/commit/75046c4e1b2830dd3a32dc8d6938f6def71940a5)) -- **anonymizer:** [FIX & TESTS] cleanTags : check if param is undefined. Add 3 test ([#308](https://github.com/dcmjs-org/dcmjs/issues/308)) ([44d23d6](https://github.com/dcmjs-org/dcmjs/commit/44d23d6a9e347fcf049053129d4d7323a9258b71)) -- ArrowAnnotateTool adapter in Cornerstone3D parsing label ([#270](https://github.com/dcmjs-org/dcmjs/issues/270)) ([cb84979](https://github.com/dcmjs-org/dcmjs/commit/cb84979be6eef8835cc7ced006625751a04356aa)) -- avoid using replaceAll() which isn't available in Node.js 14 ([#296](https://github.com/dcmjs-org/dcmjs/issues/296)) ([7aac3ab](https://github.com/dcmjs-org/dcmjs/commit/7aac3ab05fdedfe1a7a159097690bec00c0bcd2b)) -- bug tolerance parameter was not propagated ([#241](https://github.com/dcmjs-org/dcmjs/issues/241)) ([c2ed627](https://github.com/dcmjs-org/dcmjs/commit/c2ed6275ccb80fbbab3c0f9c67893d6b681b0bab)) -- **build:** adapters build missing files ([#400](https://github.com/dcmjs-org/dcmjs/issues/400)) ([901dd88](https://github.com/dcmjs-org/dcmjs/commit/901dd8815e121f29c50da0b1b9764d90d881114b)) -- **build:** Adding exports and files ([#398](https://github.com/dcmjs-org/dcmjs/issues/398)) ([2e8101f](https://github.com/dcmjs-org/dcmjs/commit/2e8101f229aa4c112405cb03558903956fc7e7f8)) -- **build:** fixing publish of adapters in package json ([#396](https://github.com/dcmjs-org/dcmjs/issues/396)) ([5a45b2f](https://github.com/dcmjs-org/dcmjs/commit/5a45b2f9ebeca9b800642d2735720a8d8400cd12)) -- **build:** Include adapters in circleci config ([#402](https://github.com/dcmjs-org/dcmjs/issues/402)) ([45c8416](https://github.com/dcmjs-org/dcmjs/commit/45c84167b40c6a48bb2c499b6153c200763778a0)) -- **build:** try to publish adapters ([#395](https://github.com/dcmjs-org/dcmjs/issues/395)) ([191a17b](https://github.com/dcmjs-org/dcmjs/commit/191a17b690d2ac60ad98a4b8ecb8379a92638d67)) -- checking the length before writing a DS and using exponential if ([#176](https://github.com/dcmjs-org/dcmjs/issues/176)) ([601aa9e](https://github.com/dcmjs-org/dcmjs/commit/601aa9e8a6df26876b08da739451e538d46aa37b)) -- **coding-scheme:** Fix coding scheme for updated standard ([ae3f0b5](https://github.com/dcmjs-org/dcmjs/commit/ae3f0b5bcd398a4230c8fba2954e225fda97cc2f)) -- **cornerstone:** exceptions caused by undefined cached stats in adapters. Safe programming fix only ([#301](https://github.com/dcmjs-org/dcmjs/issues/301)) ([893be43](https://github.com/dcmjs-org/dcmjs/commit/893be433af05f11a03cfce0a572b448303b9334b)) -- **cs:** [#318](https://github.com/dcmjs-org/dcmjs/issues/318) - check instance's NumberOfFrames property to see if it is a multi-frame file or not ([#320](https://github.com/dcmjs-org/dcmjs/issues/320)) ([0b030a4](https://github.com/dcmjs-org/dcmjs/commit/0b030a45dd48f22a61a90dfe5bbb5848425960ca)) -- **cs:** Resolves [#316](https://github.com/dcmjs-org/dcmjs/issues/316) Cornerstone3D adaptor - Multiframe support - add "frameNumber" to the Annotation.data ([#317](https://github.com/dcmjs-org/dcmjs/issues/317)) ([5fe862e](https://github.com/dcmjs-org/dcmjs/commit/5fe862e22653d30bfc78e5be5de3669610887727)) -- **dcmjs:** Add a set of accessors to the sequence list so the API is more consistent ([#224](https://github.com/dcmjs-org/dcmjs/issues/224)) ([9dad6c5](https://github.com/dcmjs-org/dcmjs/commit/9dad6c549cb4dd5c351caa13386998cbe48a1ba6)) -- **DicomMessage:** Fix readFile after options were added ([c2b62a1](https://github.com/dcmjs-org/dcmjs/commit/c2b62a13b3afc78516f39b906ca7576537037c20)) -- Elliptical roi when in flipped/rotated state ([#479](https://github.com/dcmjs-org/dcmjs/issues/479)) ([f0961ae](https://github.com/dcmjs-org/dcmjs/commit/f0961ae6f5e912230f2bf17be5acfe30f775bcae)) -- **encoding:** encapsulation is applied for only PixelData ([#199](https://github.com/dcmjs-org/dcmjs/issues/199)) ([ede2950](https://github.com/dcmjs-org/dcmjs/commit/ede2950d530fb189bb5817db6b5285e2be74ffee)), closes [#194](https://github.com/dcmjs-org/dcmjs/issues/194) -- Ensure DS and IS Value Representations are returned as arrays ([#83](https://github.com/dcmjs-org/dcmjs/issues/83)) ([a264661](https://github.com/dcmjs-org/dcmjs/commit/a264661d5a0a899b761c58492955f6d18cc03a4d)) -- exception writing NaN and Infinity values of FD tags ([#325](https://github.com/dcmjs-org/dcmjs/issues/325)) ([e86daaa](https://github.com/dcmjs-org/dcmjs/commit/e86daaad4c47e7ec2c135b94f0b0de622de69310)) -- Export loglevelnext logger as dcmjs.log for configuration ([#156](https://github.com/dcmjs-org/dcmjs/issues/156)) ([33515e5](https://github.com/dcmjs-org/dcmjs/commit/33515e5fe3f581d71278674860834ee1ea932faa)) -- Fix UN & AT VR processing logic ([#167](https://github.com/dcmjs-org/dcmjs/issues/167)) ([#168](https://github.com/dcmjs-org/dcmjs/issues/168)) ([7cb975a](https://github.com/dcmjs-org/dcmjs/commit/7cb975af106d2e3e943881a7dac06f1fe391809c)) -- force a release for commit caaac4b ([#240](https://github.com/dcmjs-org/dcmjs/issues/240)) ([f53b630](https://github.com/dcmjs-org/dcmjs/commit/f53b6306ce138c5ad3106015c4751b63fbcca362)) -- **fragment:** Refactor and fragment bug ([#283](https://github.com/dcmjs-org/dcmjs/issues/283)) ([307d60a](https://github.com/dcmjs-org/dcmjs/commit/307d60a6ffecf7d96bc729a37a45775c6b6e189c)), closes [#282](https://github.com/dcmjs-org/dcmjs/issues/282) -- **fragment:** write padding to even length on final fragments of encapsulated frame data ([#294](https://github.com/dcmjs-org/dcmjs/issues/294)) ([34b7561](https://github.com/dcmjs-org/dcmjs/commit/34b7561fa48870a87b948eb427d4e32808b4d40e)), closes [#293](https://github.com/dcmjs-org/dcmjs/issues/293) -- **idc-02252:** typo + release ([#180](https://github.com/dcmjs-org/dcmjs/issues/180)) ([3f5cb24](https://github.com/dcmjs-org/dcmjs/commit/3f5cb24fd0e50668d36dc21390a1ff527505a8db)) -- **image-comments:** Move ImageComments to DerivedPixels ([da11200](https://github.com/dcmjs-org/dcmjs/commit/da112001e3c1c01b30acbd634fd9b2f46a9d3a63)) -- **import:** missing import for addAccessors ([#295](https://github.com/dcmjs-org/dcmjs/issues/295)) ([6b631b6](https://github.com/dcmjs-org/dcmjs/commit/6b631b6c8bdb2a7a70637308c9ebfe849fe9ccaf)) -- infinite loop on dcm with no meta length ([#331](https://github.com/dcmjs-org/dcmjs/issues/331)) ([51b156b](https://github.com/dcmjs-org/dcmjs/commit/51b156bf1278fc0f9476de70c89697576c0f4b55)) -- Invalid VR of the private creator tag of the "Implicit VR Endian" typed DICOM file ([#242](https://github.com/dcmjs-org/dcmjs/issues/242)) ([#243](https://github.com/dcmjs-org/dcmjs/issues/243)) ([6d0552f](https://github.com/dcmjs-org/dcmjs/commit/6d0552fb96c59dcf3e39b3306a50004b24128330)) -- issues in binary tag parsing ([#276](https://github.com/dcmjs-org/dcmjs/issues/276)) ([60c3af1](https://github.com/dcmjs-org/dcmjs/commit/60c3af1654b8f64baea1cd47f1049fd16ea2fee8)) -- **measurement-report:** Fix issues with Measurement Report for Bidirectional measurements ([25cf222](https://github.com/dcmjs-org/dcmjs/commit/25cf222e9d80bbe5b68854362383ac4fcaec7f6c)) -- **measurement-report:** Fix ReferencedFrameNumber usage in MeasurementReport ([b80cd2a](https://github.com/dcmjs-org/dcmjs/commit/b80cd2af070d0280295e88ee24d7e4d43c4af861)) -- **naturalize:** revert single element sequence ([#223](https://github.com/dcmjs-org/dcmjs/issues/223)) ([0743ed3](https://github.com/dcmjs-org/dcmjs/commit/0743ed34f657b622d4868fe5db37015ad1aa7850)) -- **naturalizing:** Fix the exception on naturalize twice ([#237](https://github.com/dcmjs-org/dcmjs/issues/237)) ([abced98](https://github.com/dcmjs-org/dcmjs/commit/abced980dccbabce7c4b159600f89c25ba747076)) -- **parsing:** can't read an encapsulated frame whose size is greater than fragment size ([#205](https://github.com/dcmjs-org/dcmjs/issues/205)) ([176875d](https://github.com/dcmjs-org/dcmjs/commit/176875d0c9c34704302512d2c27103db205b1c8f)), closes [#204](https://github.com/dcmjs-org/dcmjs/issues/204) -- Re IDC [#2761](https://github.com/dcmjs-org/dcmjs/issues/2761) fix loading of segmentations ([#258](https://github.com/dcmjs-org/dcmjs/issues/258)) ([ceaf09a](https://github.com/dcmjs-org/dcmjs/commit/ceaf09af74f5727205e5d5869c97114b2c283ae5)) -- **Segmentation_4X:** Update tag name in getSegmentIndex method for segs ([#183](https://github.com/dcmjs-org/dcmjs/issues/183)) ([1e96ee3](https://github.com/dcmjs-org/dcmjs/commit/1e96ee3ce3280900d56f1887da81471f9b128d30)) -- **seg:** Use ReferencedSegmentNumber in shared fg ([#166](https://github.com/dcmjs-org/dcmjs/issues/166)) ([0ed3347](https://github.com/dcmjs-org/dcmjs/commit/0ed33477bb1c13b05d682c342edaf0a901fe0f7a)) -- several issues with character set handling ([#299](https://github.com/dcmjs-org/dcmjs/issues/299)) ([8e22107](https://github.com/dcmjs-org/dcmjs/commit/8e221074179b6d33b82ab5c6f1ae4c06c5522d6b)) -- **tests:** unified test data loading ([#292](https://github.com/dcmjs-org/dcmjs/issues/292)) ([c34f398](https://github.com/dcmjs-org/dcmjs/commit/c34f39813755227b79f7a0958a81a5e5c0935b73)) -- **update jsdocs, cut release:** release ([#203](https://github.com/dcmjs-org/dcmjs/issues/203)) ([307974c](https://github.com/dcmjs-org/dcmjs/commit/307974cb399d07152e0f6d4dd9f40fe1c17f076e)) -- update readme to trigger release ([#257](https://github.com/dcmjs-org/dcmjs/issues/257)) ([554f50d](https://github.com/dcmjs-org/dcmjs/commit/554f50d838fbcbeed25460c7384e14dc46bebb11)) -- update variable name for frame index ([#332](https://github.com/dcmjs-org/dcmjs/issues/332)) ([5515d6e](https://github.com/dcmjs-org/dcmjs/commit/5515d6e0abe11dee04f9b75272d3fbb5d00b2bd7)) -- use metadataProvider option instead of cornerstone.metaData ([#280](https://github.com/dcmjs-org/dcmjs/issues/280)) ([3a0e484](https://github.com/dcmjs-org/dcmjs/commit/3a0e484d203770cd309e2bd9f78946a733e79d0c)) -- **utilities:** Export Bidirectional and Polyline inside TID300 ([75e0e29](https://github.com/dcmjs-org/dcmjs/commit/75e0e29f771843a80bfa32532d2186a4e7cdbd57)) -- **VR:** added support for specific character set ([#291](https://github.com/dcmjs-org/dcmjs/issues/291)) ([f103d19](https://github.com/dcmjs-org/dcmjs/commit/f103d1918d02780c4881db5c8aa30f653c4da6b6)) -- **vr:** Convert empty DecimalString and NumberString to null instead of to zero ([#278](https://github.com/dcmjs-org/dcmjs/issues/278)) ([43cd8ea](https://github.com/dcmjs-org/dcmjs/commit/43cd8eaa316a08ff1a025b1267fedda239526acb)) -- **writeBytes:** create release from commit ([d9a4105](https://github.com/dcmjs-org/dcmjs/commit/d9a41050e756f9b7e56ef4ae3d5000ce86ea39eb)) +- 🐛 adding readme notes ([#191](https://github.com/dcmjs-org/dcmjs/issues/191)) ([459260d](https://github.com/dcmjs-org/dcmjs/commit/459260d6e2a6f905729ddf68b1f12d3140b53849)) +- 🐛 fix array format regression from commit 70b24332783d63c9db2ed21d512d9f7b526c5222 ([#236](https://github.com/dcmjs-org/dcmjs/issues/236)) ([5441063](https://github.com/dcmjs-org/dcmjs/commit/5441063d0395ede6d9f8bd3ac0d92ee14f6ef209)) +- 🐛 Fix rotation mapping for SEG cornerstone adapter ([#151](https://github.com/dcmjs-org/dcmjs/issues/151)) ([3fab68c](https://github.com/dcmjs-org/dcmjs/commit/3fab68cbfd95f82820663b9fc99a2b0cd07e43c8)) +- 🐛 Harden Segmentation import for different possible SEGs ([#146](https://github.com/dcmjs-org/dcmjs/issues/146)) ([c4952bc](https://github.com/dcmjs-org/dcmjs/commit/c4952bc5842bab80a5d928de0d860f89afc8f400)) +- 🐛 IDC Re [#2003](https://github.com/dcmjs-org/dcmjs/issues/2003): fix regression in parsing segmentation orietations ([#220](https://github.com/dcmjs-org/dcmjs/issues/220)) ([5c0c6a8](https://github.com/dcmjs-org/dcmjs/commit/5c0c6a85e67b25ce5f39412ced37d8e825691481)) +- 🐛 IDC2733: find segmentations reference source image Ids ([#253](https://github.com/dcmjs-org/dcmjs/issues/253)) ([f3e7101](https://github.com/dcmjs-org/dcmjs/commit/f3e71016dffa233bf0fb912cc7cf413718b8a1a9)) +- 🐛 ignore frames without SourceImageSequence information when loading a segmentation ([#198](https://github.com/dcmjs-org/dcmjs/issues/198)) ([82709c4](https://github.com/dcmjs-org/dcmjs/commit/82709c4a8a317aa1354244010300ab9b902802dd)) +- 🐛 indentation in nearlyEqual ([#202](https://github.com/dcmjs-org/dcmjs/issues/202)) ([989d6c9](https://github.com/dcmjs-org/dcmjs/commit/989d6c9a80686425563c55424ac1795e6a06cd7b)) +- 🐛 relax condition in nearlyEquals check for detecting numbers near to zero ([#304](https://github.com/dcmjs-org/dcmjs/issues/304)) ([974cddd](https://github.com/dcmjs-org/dcmjs/commit/974cddd785c076f1ac0211b534a7c0b82a4ba68a)) +- 🐛 When converting to multiframe, fix IPP issues ([#152](https://github.com/dcmjs-org/dcmjs/issues/152)) ([80496e4](https://github.com/dcmjs-org/dcmjs/commit/80496e422152c1a3dfd850a145011dd3dc632964)) +- **adapter:** Removed comment around getTID300RepresentationArguments 'tool' parameter ([#322](https://github.com/dcmjs-org/dcmjs/issues/322)) ([d8f05ff](https://github.com/dcmjs-org/dcmjs/commit/d8f05ffb9ef1b5cce254980a597b4c428ffdfb6e)), closes [#306](https://github.com/dcmjs-org/dcmjs/issues/306) +- **adapters:** Measurement reports can throw exceptions that prevent loading ([#458](https://github.com/dcmjs-org/dcmjs/issues/458)) ([7bc7d8a](https://github.com/dcmjs-org/dcmjs/commit/7bc7d8aaeb5aa0df91c24e278665d14f590ec234)) +- **adapters:** Update rollup to newer version ([#407](https://github.com/dcmjs-org/dcmjs/issues/407)) ([543675f](https://github.com/dcmjs-org/dcmjs/commit/543675f1269f8b739764291b1c27b40470c48c63)) +- **adapters:** Update the build a little to allow debugging into typescript ([#439](https://github.com/dcmjs-org/dcmjs/issues/439)) ([05e6419](https://github.com/dcmjs-org/dcmjs/commit/05e6419e1a6705f40c367ac4b52c3975f6fd25c6)) +- **adapter:** The rectangle encoding of SR ([#437](https://github.com/dcmjs-org/dcmjs/issues/437)) ([bff23ec](https://github.com/dcmjs-org/dcmjs/commit/bff23ecfe551312a1339211ddb7838993514a377)) +- **add check for nullable numeric string vrs:** adds a check for nullable numeric strinv vrs ([#150](https://github.com/dcmjs-org/dcmjs/issues/150)) ([75046c4](https://github.com/dcmjs-org/dcmjs/commit/75046c4e1b2830dd3a32dc8d6938f6def71940a5)) +- **anonymizer:** [FIX & TESTS] cleanTags : check if param is undefined. Add 3 test ([#308](https://github.com/dcmjs-org/dcmjs/issues/308)) ([44d23d6](https://github.com/dcmjs-org/dcmjs/commit/44d23d6a9e347fcf049053129d4d7323a9258b71)) +- ArrowAnnotateTool adapter in Cornerstone3D parsing label ([#270](https://github.com/dcmjs-org/dcmjs/issues/270)) ([cb84979](https://github.com/dcmjs-org/dcmjs/commit/cb84979be6eef8835cc7ced006625751a04356aa)) +- avoid using replaceAll() which isn't available in Node.js 14 ([#296](https://github.com/dcmjs-org/dcmjs/issues/296)) ([7aac3ab](https://github.com/dcmjs-org/dcmjs/commit/7aac3ab05fdedfe1a7a159097690bec00c0bcd2b)) +- bug tolerance parameter was not propagated ([#241](https://github.com/dcmjs-org/dcmjs/issues/241)) ([c2ed627](https://github.com/dcmjs-org/dcmjs/commit/c2ed6275ccb80fbbab3c0f9c67893d6b681b0bab)) +- **build:** adapters build missing files ([#400](https://github.com/dcmjs-org/dcmjs/issues/400)) ([901dd88](https://github.com/dcmjs-org/dcmjs/commit/901dd8815e121f29c50da0b1b9764d90d881114b)) +- **build:** Adding exports and files ([#398](https://github.com/dcmjs-org/dcmjs/issues/398)) ([2e8101f](https://github.com/dcmjs-org/dcmjs/commit/2e8101f229aa4c112405cb03558903956fc7e7f8)) +- **build:** fixing publish of adapters in package json ([#396](https://github.com/dcmjs-org/dcmjs/issues/396)) ([5a45b2f](https://github.com/dcmjs-org/dcmjs/commit/5a45b2f9ebeca9b800642d2735720a8d8400cd12)) +- **build:** Include adapters in circleci config ([#402](https://github.com/dcmjs-org/dcmjs/issues/402)) ([45c8416](https://github.com/dcmjs-org/dcmjs/commit/45c84167b40c6a48bb2c499b6153c200763778a0)) +- **build:** try to publish adapters ([#395](https://github.com/dcmjs-org/dcmjs/issues/395)) ([191a17b](https://github.com/dcmjs-org/dcmjs/commit/191a17b690d2ac60ad98a4b8ecb8379a92638d67)) +- checking the length before writing a DS and using exponential if ([#176](https://github.com/dcmjs-org/dcmjs/issues/176)) ([601aa9e](https://github.com/dcmjs-org/dcmjs/commit/601aa9e8a6df26876b08da739451e538d46aa37b)) +- **coding-scheme:** Fix coding scheme for updated standard ([ae3f0b5](https://github.com/dcmjs-org/dcmjs/commit/ae3f0b5bcd398a4230c8fba2954e225fda97cc2f)) +- **cornerstone:** exceptions caused by undefined cached stats in adapters. Safe programming fix only ([#301](https://github.com/dcmjs-org/dcmjs/issues/301)) ([893be43](https://github.com/dcmjs-org/dcmjs/commit/893be433af05f11a03cfce0a572b448303b9334b)) +- **cs:** [#318](https://github.com/dcmjs-org/dcmjs/issues/318) - check instance's NumberOfFrames property to see if it is a multi-frame file or not ([#320](https://github.com/dcmjs-org/dcmjs/issues/320)) ([0b030a4](https://github.com/dcmjs-org/dcmjs/commit/0b030a45dd48f22a61a90dfe5bbb5848425960ca)) +- **cs:** Resolves [#316](https://github.com/dcmjs-org/dcmjs/issues/316) Cornerstone3D adaptor - Multiframe support - add "frameNumber" to the Annotation.data ([#317](https://github.com/dcmjs-org/dcmjs/issues/317)) ([5fe862e](https://github.com/dcmjs-org/dcmjs/commit/5fe862e22653d30bfc78e5be5de3669610887727)) +- **dcmjs:** Add a set of accessors to the sequence list so the API is more consistent ([#224](https://github.com/dcmjs-org/dcmjs/issues/224)) ([9dad6c5](https://github.com/dcmjs-org/dcmjs/commit/9dad6c549cb4dd5c351caa13386998cbe48a1ba6)) +- **DicomMessage:** Fix readFile after options were added ([c2b62a1](https://github.com/dcmjs-org/dcmjs/commit/c2b62a13b3afc78516f39b906ca7576537037c20)) +- Elliptical roi when in flipped/rotated state ([#479](https://github.com/dcmjs-org/dcmjs/issues/479)) ([f0961ae](https://github.com/dcmjs-org/dcmjs/commit/f0961ae6f5e912230f2bf17be5acfe30f775bcae)) +- **encoding:** encapsulation is applied for only PixelData ([#199](https://github.com/dcmjs-org/dcmjs/issues/199)) ([ede2950](https://github.com/dcmjs-org/dcmjs/commit/ede2950d530fb189bb5817db6b5285e2be74ffee)), closes [#194](https://github.com/dcmjs-org/dcmjs/issues/194) +- Ensure DS and IS Value Representations are returned as arrays ([#83](https://github.com/dcmjs-org/dcmjs/issues/83)) ([a264661](https://github.com/dcmjs-org/dcmjs/commit/a264661d5a0a899b761c58492955f6d18cc03a4d)) +- exception writing NaN and Infinity values of FD tags ([#325](https://github.com/dcmjs-org/dcmjs/issues/325)) ([e86daaa](https://github.com/dcmjs-org/dcmjs/commit/e86daaad4c47e7ec2c135b94f0b0de622de69310)) +- Export loglevelnext logger as dcmjs.log for configuration ([#156](https://github.com/dcmjs-org/dcmjs/issues/156)) ([33515e5](https://github.com/dcmjs-org/dcmjs/commit/33515e5fe3f581d71278674860834ee1ea932faa)) +- Fix UN & AT VR processing logic ([#167](https://github.com/dcmjs-org/dcmjs/issues/167)) ([#168](https://github.com/dcmjs-org/dcmjs/issues/168)) ([7cb975a](https://github.com/dcmjs-org/dcmjs/commit/7cb975af106d2e3e943881a7dac06f1fe391809c)) +- force a release for commit caaac4b ([#240](https://github.com/dcmjs-org/dcmjs/issues/240)) ([f53b630](https://github.com/dcmjs-org/dcmjs/commit/f53b6306ce138c5ad3106015c4751b63fbcca362)) +- **fragment:** Refactor and fragment bug ([#283](https://github.com/dcmjs-org/dcmjs/issues/283)) ([307d60a](https://github.com/dcmjs-org/dcmjs/commit/307d60a6ffecf7d96bc729a37a45775c6b6e189c)), closes [#282](https://github.com/dcmjs-org/dcmjs/issues/282) +- **fragment:** write padding to even length on final fragments of encapsulated frame data ([#294](https://github.com/dcmjs-org/dcmjs/issues/294)) ([34b7561](https://github.com/dcmjs-org/dcmjs/commit/34b7561fa48870a87b948eb427d4e32808b4d40e)), closes [#293](https://github.com/dcmjs-org/dcmjs/issues/293) +- **idc-02252:** typo + release ([#180](https://github.com/dcmjs-org/dcmjs/issues/180)) ([3f5cb24](https://github.com/dcmjs-org/dcmjs/commit/3f5cb24fd0e50668d36dc21390a1ff527505a8db)) +- **image-comments:** Move ImageComments to DerivedPixels ([da11200](https://github.com/dcmjs-org/dcmjs/commit/da112001e3c1c01b30acbd634fd9b2f46a9d3a63)) +- **import:** missing import for addAccessors ([#295](https://github.com/dcmjs-org/dcmjs/issues/295)) ([6b631b6](https://github.com/dcmjs-org/dcmjs/commit/6b631b6c8bdb2a7a70637308c9ebfe849fe9ccaf)) +- infinite loop on dcm with no meta length ([#331](https://github.com/dcmjs-org/dcmjs/issues/331)) ([51b156b](https://github.com/dcmjs-org/dcmjs/commit/51b156bf1278fc0f9476de70c89697576c0f4b55)) +- Invalid VR of the private creator tag of the "Implicit VR Endian" typed DICOM file ([#242](https://github.com/dcmjs-org/dcmjs/issues/242)) ([#243](https://github.com/dcmjs-org/dcmjs/issues/243)) ([6d0552f](https://github.com/dcmjs-org/dcmjs/commit/6d0552fb96c59dcf3e39b3306a50004b24128330)) +- issues in binary tag parsing ([#276](https://github.com/dcmjs-org/dcmjs/issues/276)) ([60c3af1](https://github.com/dcmjs-org/dcmjs/commit/60c3af1654b8f64baea1cd47f1049fd16ea2fee8)) +- **measurement-report:** Fix issues with Measurement Report for Bidirectional measurements ([25cf222](https://github.com/dcmjs-org/dcmjs/commit/25cf222e9d80bbe5b68854362383ac4fcaec7f6c)) +- **measurement-report:** Fix ReferencedFrameNumber usage in MeasurementReport ([b80cd2a](https://github.com/dcmjs-org/dcmjs/commit/b80cd2af070d0280295e88ee24d7e4d43c4af861)) +- **naturalize:** revert single element sequence ([#223](https://github.com/dcmjs-org/dcmjs/issues/223)) ([0743ed3](https://github.com/dcmjs-org/dcmjs/commit/0743ed34f657b622d4868fe5db37015ad1aa7850)) +- **naturalizing:** Fix the exception on naturalize twice ([#237](https://github.com/dcmjs-org/dcmjs/issues/237)) ([abced98](https://github.com/dcmjs-org/dcmjs/commit/abced980dccbabce7c4b159600f89c25ba747076)) +- **parsing:** can't read an encapsulated frame whose size is greater than fragment size ([#205](https://github.com/dcmjs-org/dcmjs/issues/205)) ([176875d](https://github.com/dcmjs-org/dcmjs/commit/176875d0c9c34704302512d2c27103db205b1c8f)), closes [#204](https://github.com/dcmjs-org/dcmjs/issues/204) +- Re IDC [#2761](https://github.com/dcmjs-org/dcmjs/issues/2761) fix loading of segmentations ([#258](https://github.com/dcmjs-org/dcmjs/issues/258)) ([ceaf09a](https://github.com/dcmjs-org/dcmjs/commit/ceaf09af74f5727205e5d5869c97114b2c283ae5)) +- **Segmentation_4X:** Update tag name in getSegmentIndex method for segs ([#183](https://github.com/dcmjs-org/dcmjs/issues/183)) ([1e96ee3](https://github.com/dcmjs-org/dcmjs/commit/1e96ee3ce3280900d56f1887da81471f9b128d30)) +- **seg:** Use ReferencedSegmentNumber in shared fg ([#166](https://github.com/dcmjs-org/dcmjs/issues/166)) ([0ed3347](https://github.com/dcmjs-org/dcmjs/commit/0ed33477bb1c13b05d682c342edaf0a901fe0f7a)) +- several issues with character set handling ([#299](https://github.com/dcmjs-org/dcmjs/issues/299)) ([8e22107](https://github.com/dcmjs-org/dcmjs/commit/8e221074179b6d33b82ab5c6f1ae4c06c5522d6b)) +- **tests:** unified test data loading ([#292](https://github.com/dcmjs-org/dcmjs/issues/292)) ([c34f398](https://github.com/dcmjs-org/dcmjs/commit/c34f39813755227b79f7a0958a81a5e5c0935b73)) +- **update jsdocs, cut release:** release ([#203](https://github.com/dcmjs-org/dcmjs/issues/203)) ([307974c](https://github.com/dcmjs-org/dcmjs/commit/307974cb399d07152e0f6d4dd9f40fe1c17f076e)) +- update readme to trigger release ([#257](https://github.com/dcmjs-org/dcmjs/issues/257)) ([554f50d](https://github.com/dcmjs-org/dcmjs/commit/554f50d838fbcbeed25460c7384e14dc46bebb11)) +- update variable name for frame index ([#332](https://github.com/dcmjs-org/dcmjs/issues/332)) ([5515d6e](https://github.com/dcmjs-org/dcmjs/commit/5515d6e0abe11dee04f9b75272d3fbb5d00b2bd7)) +- use metadataProvider option instead of cornerstone.metaData ([#280](https://github.com/dcmjs-org/dcmjs/issues/280)) ([3a0e484](https://github.com/dcmjs-org/dcmjs/commit/3a0e484d203770cd309e2bd9f78946a733e79d0c)) +- **utilities:** Export Bidirectional and Polyline inside TID300 ([75e0e29](https://github.com/dcmjs-org/dcmjs/commit/75e0e29f771843a80bfa32532d2186a4e7cdbd57)) +- **VR:** added support for specific character set ([#291](https://github.com/dcmjs-org/dcmjs/issues/291)) ([f103d19](https://github.com/dcmjs-org/dcmjs/commit/f103d1918d02780c4881db5c8aa30f653c4da6b6)) +- **vr:** Convert empty DecimalString and NumberString to null instead of to zero ([#278](https://github.com/dcmjs-org/dcmjs/issues/278)) ([43cd8ea](https://github.com/dcmjs-org/dcmjs/commit/43cd8eaa316a08ff1a025b1267fedda239526acb)) +- **writeBytes:** create release from commit ([d9a4105](https://github.com/dcmjs-org/dcmjs/commit/d9a41050e756f9b7e56ef4ae3d5000ce86ea39eb)) ### Features -- **adapters:** Add adapter for exporting polylines from dicom-microscopy-viewer to DICOM-SR ([#44](https://github.com/dcmjs-org/dcmjs/issues/44)) ([7a1947c](https://github.com/dcmjs-org/dcmjs/commit/7a1947c6ec164da4e9f66e90c1bbf1055978b173)) -- **adapters:** Add adapter to generate segments & geometry from SEG for easier use in VTKjs (migrated from vtkDisplay example) ([398b74d](https://github.com/dcmjs-org/dcmjs/commit/398b74d70deb32824a74e98210127326887dfa77)) -- **adapters:** Add adapters for Rectangle, Angle and fix generate DICOM ([#427](https://github.com/dcmjs-org/dcmjs/issues/427)) ([b8ca75e](https://github.com/dcmjs-org/dcmjs/commit/b8ca75e6ba378f175bd987d07f094f44b41a46cf)) -- **adapters:** First steps for DICOM-SR read support for polylines with dicom-microscopy-viewer ([#49](https://github.com/dcmjs-org/dcmjs/issues/49)) ([37f1888](https://github.com/dcmjs-org/dcmjs/commit/37f18881dd7369818de4aa22c5730012c2829616)) -- Add a CircleROI tool ([#459](https://github.com/dcmjs-org/dcmjs/issues/459)) ([1c03ed3](https://github.com/dcmjs-org/dcmjs/commit/1c03ed3457fbb63bbd87315b90bfed99b1cd09cc)) -- Add Cornerstone3D adapter for Length tool ([#261](https://github.com/dcmjs-org/dcmjs/issues/261)) ([2cab0d9](https://github.com/dcmjs-org/dcmjs/commit/2cab0d9edba20e99b11407d41ed2a4bf60a6ab9b)) -- Add overlapping segment check to Cornerstone 4.x DICOM SEG adapter ([#155](https://github.com/dcmjs-org/dcmjs/issues/155)) ([df44e27](https://github.com/dcmjs-org/dcmjs/commit/df44e27b3b1c26082bf3d7a9477ab7f0d5ca1d19)) -- Allow backslashes in UIDs in order to support DICOM Q&R ([#277](https://github.com/dcmjs-org/dcmjs/issues/277)) ([6d2d5c6](https://github.com/dcmjs-org/dcmjs/commit/6d2d5c60f500174abb1be7757094178ec6a1d144)) -- **anonymizer:** export Array tagNamesToEmpty and modify cleanTags ([#303](https://github.com/dcmjs-org/dcmjs/issues/303)) ([e960085](https://github.com/dcmjs-org/dcmjs/commit/e960085ca08fb28a0a8134fbfee7d450722d8c64)) -- Bidirectional Arrow and EllipticalROI adapters for CS3D ([#264](https://github.com/dcmjs-org/dcmjs/issues/264)) ([1fc7932](https://github.com/dcmjs-org/dcmjs/commit/1fc7932e3eed85fa1a0f857d84bab35a2e62d80d)) -- **CobbAngle:** Add CobbAngle tool ([#353](https://github.com/dcmjs-org/dcmjs/issues/353)) ([b9bd701](https://github.com/dcmjs-org/dcmjs/commit/b9bd701df41ae2b8b2afbcf1d092d7587f7b267a)) -- **contour api:** add api for contour rendering configuration ([#443](https://github.com/dcmjs-org/dcmjs/issues/443)) ([4ab751d](https://github.com/dcmjs-org/dcmjs/commit/4ab751df4082c56b64e4b97e9d6ca6de3c60c7e5)) -- **cornerstone:** Feature add cornerstone adapters ([#225](https://github.com/dcmjs-org/dcmjs/issues/225)) ([23c0877](https://github.com/dcmjs-org/dcmjs/commit/23c08777f7a93d4c0576a5e583113029a1a1e05f)) -- **deflated:** Added support for reading datasets with deflated transfer syntax ([#312](https://github.com/dcmjs-org/dcmjs/issues/312)) ([ee8f8f2](https://github.com/dcmjs-org/dcmjs/commit/ee8f8f21babbbd8eac2b19e8e957db2cc5a09325)) -- **dicomImageLoader types:** Add types to the dicom image loader ([#441](https://github.com/dcmjs-org/dcmjs/issues/441)) ([10a3370](https://github.com/dcmjs-org/dcmjs/commit/10a3370b7f23084d1f2c55506079c17dea959659)), closes [#449](https://github.com/dcmjs-org/dcmjs/issues/449) [#450](https://github.com/dcmjs-org/dcmjs/issues/450) -- **mem:** Zero Copy ArrayBuffer ([#279](https://github.com/dcmjs-org/dcmjs/issues/279)) ([a17f2d7](https://github.com/dcmjs-org/dcmjs/commit/a17f2d75bc102cc789289f0eae51e5b416ce1567)) -- Move adapters from dcmjs for Cornerstone/3d and VTK ([b136a21](https://github.com/dcmjs-org/dcmjs/commit/b136a21fb96bb28c3a10a63b6b78083b897f4e19)) -- **npm:** bump minor version (minor readme edits) ([b48f665](https://github.com/dcmjs-org/dcmjs/commit/b48f665893f5e4b643c68ddc2fc3b13c641b6b29)) -- **readme:** stress need for a PR for npm package ([#310](https://github.com/dcmjs-org/dcmjs/issues/310)) ([dafd78d](https://github.com/dcmjs-org/dcmjs/commit/dafd78d004ccb9d0b505f0b7e0fd461eda9c3710)) -- **sr:** export TID300 - Point class ([#323](https://github.com/dcmjs-org/dcmjs/issues/323)) ([d2aebc3](https://github.com/dcmjs-org/dcmjs/commit/d2aebc3166c23e14dce4d7a8b7f1e2fc933f6328)) -- **structured-reports:** Add initial work on Adapters / Utilities for Imaging Measurement Structured Report input / output ([#17](https://github.com/dcmjs-org/dcmjs/issues/17)) ([941ad75](https://github.com/dcmjs-org/dcmjs/commit/941ad75320eece3368104d08247fdc371497c7cd)) -- **testing:** Use the Jest testing framework and switch to GitHub Actions ([#254](https://github.com/dcmjs-org/dcmjs/issues/254)) ([a91ff2b](https://github.com/dcmjs-org/dcmjs/commit/a91ff2babd2c6a44f20ecbd954ab38901276cf41)) +- **adapters:** Add adapter for exporting polylines from dicom-microscopy-viewer to DICOM-SR ([#44](https://github.com/dcmjs-org/dcmjs/issues/44)) ([7a1947c](https://github.com/dcmjs-org/dcmjs/commit/7a1947c6ec164da4e9f66e90c1bbf1055978b173)) +- **adapters:** Add adapter to generate segments & geometry from SEG for easier use in VTKjs (migrated from vtkDisplay example) ([398b74d](https://github.com/dcmjs-org/dcmjs/commit/398b74d70deb32824a74e98210127326887dfa77)) +- **adapters:** Add adapters for Rectangle, Angle and fix generate DICOM ([#427](https://github.com/dcmjs-org/dcmjs/issues/427)) ([b8ca75e](https://github.com/dcmjs-org/dcmjs/commit/b8ca75e6ba378f175bd987d07f094f44b41a46cf)) +- **adapters:** First steps for DICOM-SR read support for polylines with dicom-microscopy-viewer ([#49](https://github.com/dcmjs-org/dcmjs/issues/49)) ([37f1888](https://github.com/dcmjs-org/dcmjs/commit/37f18881dd7369818de4aa22c5730012c2829616)) +- Add a CircleROI tool ([#459](https://github.com/dcmjs-org/dcmjs/issues/459)) ([1c03ed3](https://github.com/dcmjs-org/dcmjs/commit/1c03ed3457fbb63bbd87315b90bfed99b1cd09cc)) +- Add Cornerstone3D adapter for Length tool ([#261](https://github.com/dcmjs-org/dcmjs/issues/261)) ([2cab0d9](https://github.com/dcmjs-org/dcmjs/commit/2cab0d9edba20e99b11407d41ed2a4bf60a6ab9b)) +- Add overlapping segment check to Cornerstone 4.x DICOM SEG adapter ([#155](https://github.com/dcmjs-org/dcmjs/issues/155)) ([df44e27](https://github.com/dcmjs-org/dcmjs/commit/df44e27b3b1c26082bf3d7a9477ab7f0d5ca1d19)) +- Allow backslashes in UIDs in order to support DICOM Q&R ([#277](https://github.com/dcmjs-org/dcmjs/issues/277)) ([6d2d5c6](https://github.com/dcmjs-org/dcmjs/commit/6d2d5c60f500174abb1be7757094178ec6a1d144)) +- **anonymizer:** export Array tagNamesToEmpty and modify cleanTags ([#303](https://github.com/dcmjs-org/dcmjs/issues/303)) ([e960085](https://github.com/dcmjs-org/dcmjs/commit/e960085ca08fb28a0a8134fbfee7d450722d8c64)) +- Bidirectional Arrow and EllipticalROI adapters for CS3D ([#264](https://github.com/dcmjs-org/dcmjs/issues/264)) ([1fc7932](https://github.com/dcmjs-org/dcmjs/commit/1fc7932e3eed85fa1a0f857d84bab35a2e62d80d)) +- **CobbAngle:** Add CobbAngle tool ([#353](https://github.com/dcmjs-org/dcmjs/issues/353)) ([b9bd701](https://github.com/dcmjs-org/dcmjs/commit/b9bd701df41ae2b8b2afbcf1d092d7587f7b267a)) +- **contour api:** add api for contour rendering configuration ([#443](https://github.com/dcmjs-org/dcmjs/issues/443)) ([4ab751d](https://github.com/dcmjs-org/dcmjs/commit/4ab751df4082c56b64e4b97e9d6ca6de3c60c7e5)) +- **cornerstone:** Feature add cornerstone adapters ([#225](https://github.com/dcmjs-org/dcmjs/issues/225)) ([23c0877](https://github.com/dcmjs-org/dcmjs/commit/23c08777f7a93d4c0576a5e583113029a1a1e05f)) +- **deflated:** Added support for reading datasets with deflated transfer syntax ([#312](https://github.com/dcmjs-org/dcmjs/issues/312)) ([ee8f8f2](https://github.com/dcmjs-org/dcmjs/commit/ee8f8f21babbbd8eac2b19e8e957db2cc5a09325)) +- **dicomImageLoader types:** Add types to the dicom image loader ([#441](https://github.com/dcmjs-org/dcmjs/issues/441)) ([10a3370](https://github.com/dcmjs-org/dcmjs/commit/10a3370b7f23084d1f2c55506079c17dea959659)), closes [#449](https://github.com/dcmjs-org/dcmjs/issues/449) [#450](https://github.com/dcmjs-org/dcmjs/issues/450) +- **mem:** Zero Copy ArrayBuffer ([#279](https://github.com/dcmjs-org/dcmjs/issues/279)) ([a17f2d7](https://github.com/dcmjs-org/dcmjs/commit/a17f2d75bc102cc789289f0eae51e5b416ce1567)) +- Move adapters from dcmjs for Cornerstone/3d and VTK ([b136a21](https://github.com/dcmjs-org/dcmjs/commit/b136a21fb96bb28c3a10a63b6b78083b897f4e19)) +- **npm:** bump minor version (minor readme edits) ([b48f665](https://github.com/dcmjs-org/dcmjs/commit/b48f665893f5e4b643c68ddc2fc3b13c641b6b29)) +- **readme:** stress need for a PR for npm package ([#310](https://github.com/dcmjs-org/dcmjs/issues/310)) ([dafd78d](https://github.com/dcmjs-org/dcmjs/commit/dafd78d004ccb9d0b505f0b7e0fd461eda9c3710)) +- **sr:** export TID300 - Point class ([#323](https://github.com/dcmjs-org/dcmjs/issues/323)) ([d2aebc3](https://github.com/dcmjs-org/dcmjs/commit/d2aebc3166c23e14dce4d7a8b7f1e2fc933f6328)) +- **structured-reports:** Add initial work on Adapters / Utilities for Imaging Measurement Structured Report input / output ([#17](https://github.com/dcmjs-org/dcmjs/issues/17)) ([941ad75](https://github.com/dcmjs-org/dcmjs/commit/941ad75320eece3368104d08247fdc371497c7cd)) +- **testing:** Use the Jest testing framework and switch to GitHub Actions ([#254](https://github.com/dcmjs-org/dcmjs/issues/254)) ([a91ff2b](https://github.com/dcmjs-org/dcmjs/commit/a91ff2babd2c6a44f20ecbd954ab38901276cf41)) # [0.6.0](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.5.0...@cornerstonejs/adapters@0.6.0) (2023-03-28) ### Features -- **contour api:** add api for contour rendering configuration ([#443](https://github.com/dcmjs-org/dcmjs/issues/443)) ([4ab751d](https://github.com/dcmjs-org/dcmjs/commit/4ab751df4082c56b64e4b97e9d6ca6de3c60c7e5)) +- **contour api:** add api for contour rendering configuration ([#443](https://github.com/dcmjs-org/dcmjs/issues/443)) ([4ab751d](https://github.com/dcmjs-org/dcmjs/commit/4ab751df4082c56b64e4b97e9d6ca6de3c60c7e5)) # [0.5.0](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.4.2...@cornerstonejs/adapters@0.5.0) (2023-03-22) ### Features -- Add a CircleROI tool ([#459](https://github.com/dcmjs-org/dcmjs/issues/459)) ([1c03ed3](https://github.com/dcmjs-org/dcmjs/commit/1c03ed3457fbb63bbd87315b90bfed99b1cd09cc)) +- Add a CircleROI tool ([#459](https://github.com/dcmjs-org/dcmjs/issues/459)) ([1c03ed3](https://github.com/dcmjs-org/dcmjs/commit/1c03ed3457fbb63bbd87315b90bfed99b1cd09cc)) ## [0.4.2](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.4.1...@cornerstonejs/adapters@0.4.2) (2023-03-22) ### Bug Fixes -- Elliptical roi when in flipped/rotated state ([#479](https://github.com/dcmjs-org/dcmjs/issues/479)) ([f0961ae](https://github.com/dcmjs-org/dcmjs/commit/f0961ae6f5e912230f2bf17be5acfe30f775bcae)) +- Elliptical roi when in flipped/rotated state ([#479](https://github.com/dcmjs-org/dcmjs/issues/479)) ([f0961ae](https://github.com/dcmjs-org/dcmjs/commit/f0961ae6f5e912230f2bf17be5acfe30f775bcae)) ## [0.4.1](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.4.0...@cornerstonejs/adapters@0.4.1) (2023-03-03) ### Bug Fixes -- **adapters:** Measurement reports can throw exceptions that prevent loading ([#458](https://github.com/dcmjs-org/dcmjs/issues/458)) ([7bc7d8a](https://github.com/dcmjs-org/dcmjs/commit/7bc7d8aaeb5aa0df91c24e278665d14f590ec234)) +- **adapters:** Measurement reports can throw exceptions that prevent loading ([#458](https://github.com/dcmjs-org/dcmjs/issues/458)) ([7bc7d8a](https://github.com/dcmjs-org/dcmjs/commit/7bc7d8aaeb5aa0df91c24e278665d14f590ec234)) # [0.4.0](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.3.2...@cornerstonejs/adapters@0.4.0) (2023-03-03) ### Features -- **dicomImageLoader types:** Add types to the dicom image loader ([#441](https://github.com/dcmjs-org/dcmjs/issues/441)) ([10a3370](https://github.com/dcmjs-org/dcmjs/commit/10a3370b7f23084d1f2c55506079c17dea959659)), closes [#449](https://github.com/dcmjs-org/dcmjs/issues/449) [#450](https://github.com/dcmjs-org/dcmjs/issues/450) +- **dicomImageLoader types:** Add types to the dicom image loader ([#441](https://github.com/dcmjs-org/dcmjs/issues/441)) ([10a3370](https://github.com/dcmjs-org/dcmjs/commit/10a3370b7f23084d1f2c55506079c17dea959659)), closes [#449](https://github.com/dcmjs-org/dcmjs/issues/449) [#450](https://github.com/dcmjs-org/dcmjs/issues/450) ## [0.3.2](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.3.1...@cornerstonejs/adapters@0.3.2) (2023-02-17) ### Bug Fixes -- **adapters:** Update the build a little to allow debugging into typescript ([#439](https://github.com/dcmjs-org/dcmjs/issues/439)) ([05e6419](https://github.com/dcmjs-org/dcmjs/commit/05e6419e1a6705f40c367ac4b52c3975f6fd25c6)) +- **adapters:** Update the build a little to allow debugging into typescript ([#439](https://github.com/dcmjs-org/dcmjs/issues/439)) ([05e6419](https://github.com/dcmjs-org/dcmjs/commit/05e6419e1a6705f40c367ac4b52c3975f6fd25c6)) ## [0.3.1](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.3.0...@cornerstonejs/adapters@0.3.1) (2023-02-16) ### Bug Fixes -- **adapter:** The rectangle encoding of SR ([#437](https://github.com/dcmjs-org/dcmjs/issues/437)) ([bff23ec](https://github.com/dcmjs-org/dcmjs/commit/bff23ecfe551312a1339211ddb7838993514a377)) +- **adapter:** The rectangle encoding of SR ([#437](https://github.com/dcmjs-org/dcmjs/issues/437)) ([bff23ec](https://github.com/dcmjs-org/dcmjs/commit/bff23ecfe551312a1339211ddb7838993514a377)) # [0.3.0](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.2.1...@cornerstonejs/adapters@0.3.0) (2023-02-08) ### Features -- **adapters:** Add adapters for Rectangle, Angle and fix generate DICOM ([#427](https://github.com/dcmjs-org/dcmjs/issues/427)) ([b8ca75e](https://github.com/dcmjs-org/dcmjs/commit/b8ca75e6ba378f175bd987d07f094f44b41a46cf)) +- **adapters:** Add adapters for Rectangle, Angle and fix generate DICOM ([#427](https://github.com/dcmjs-org/dcmjs/issues/427)) ([b8ca75e](https://github.com/dcmjs-org/dcmjs/commit/b8ca75e6ba378f175bd987d07f094f44b41a46cf)) ## [0.2.1](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.2.0...@cornerstonejs/adapters@0.2.1) (2023-02-03) ### Bug Fixes -- **adapters:** Update rollup to newer version ([#407](https://github.com/dcmjs-org/dcmjs/issues/407)) ([543675f](https://github.com/dcmjs-org/dcmjs/commit/543675f1269f8b739764291b1c27b40470c48c63)) +- **adapters:** Update rollup to newer version ([#407](https://github.com/dcmjs-org/dcmjs/issues/407)) ([543675f](https://github.com/dcmjs-org/dcmjs/commit/543675f1269f8b739764291b1c27b40470c48c63)) # [0.2.0](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.1.5...@cornerstonejs/adapters@0.2.0) (2023-01-30) ### Features -- **CobbAngle:** Add CobbAngle tool ([#353](https://github.com/dcmjs-org/dcmjs/issues/353)) ([b9bd701](https://github.com/dcmjs-org/dcmjs/commit/b9bd701df41ae2b8b2afbcf1d092d7587f7b267a)) +- **CobbAngle:** Add CobbAngle tool ([#353](https://github.com/dcmjs-org/dcmjs/issues/353)) ([b9bd701](https://github.com/dcmjs-org/dcmjs/commit/b9bd701df41ae2b8b2afbcf1d092d7587f7b267a)) ## [0.1.5](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.1.4...@cornerstonejs/adapters@0.1.5) (2023-01-30) ### Bug Fixes -- **build:** Include adapters in circleci config ([#402](https://github.com/dcmjs-org/dcmjs/issues/402)) ([45c8416](https://github.com/dcmjs-org/dcmjs/commit/45c84167b40c6a48bb2c499b6153c200763778a0)) +- **build:** Include adapters in circleci config ([#402](https://github.com/dcmjs-org/dcmjs/issues/402)) ([45c8416](https://github.com/dcmjs-org/dcmjs/commit/45c84167b40c6a48bb2c499b6153c200763778a0)) ## [0.1.4](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.1.3...@cornerstonejs/adapters@0.1.4) (2023-01-30) ### Bug Fixes -- **build:** adapters build missing files ([#400](https://github.com/dcmjs-org/dcmjs/issues/400)) ([901dd88](https://github.com/dcmjs-org/dcmjs/commit/901dd8815e121f29c50da0b1b9764d90d881114b)) +- **build:** adapters build missing files ([#400](https://github.com/dcmjs-org/dcmjs/issues/400)) ([901dd88](https://github.com/dcmjs-org/dcmjs/commit/901dd8815e121f29c50da0b1b9764d90d881114b)) ## [0.1.3](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.1.2...@cornerstonejs/adapters@0.1.3) (2023-01-28) ### Bug Fixes -- **build:** Adding exports and files ([#398](https://github.com/dcmjs-org/dcmjs/issues/398)) ([2e8101f](https://github.com/dcmjs-org/dcmjs/commit/2e8101f229aa4c112405cb03558903956fc7e7f8)) +- **build:** Adding exports and files ([#398](https://github.com/dcmjs-org/dcmjs/issues/398)) ([2e8101f](https://github.com/dcmjs-org/dcmjs/commit/2e8101f229aa4c112405cb03558903956fc7e7f8)) ## [0.1.2](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.1.1...@cornerstonejs/adapters@0.1.2) (2023-01-27) ### Bug Fixes -- **build:** fixing publish of adapters in package json ([#396](https://github.com/dcmjs-org/dcmjs/issues/396)) ([5a45b2f](https://github.com/dcmjs-org/dcmjs/commit/5a45b2f9ebeca9b800642d2735720a8d8400cd12)) +- **build:** fixing publish of adapters in package json ([#396](https://github.com/dcmjs-org/dcmjs/issues/396)) ([5a45b2f](https://github.com/dcmjs-org/dcmjs/commit/5a45b2f9ebeca9b800642d2735720a8d8400cd12)) ## [0.1.1](https://github.com/dcmjs-org/dcmjs/compare/@cornerstonejs/adapters@0.1.0...@cornerstonejs/adapters@0.1.1) (2023-01-27) ### Bug Fixes -- **build:** try to publish adapters ([#395](https://github.com/dcmjs-org/dcmjs/issues/395)) ([191a17b](https://github.com/dcmjs-org/dcmjs/commit/191a17b690d2ac60ad98a4b8ecb8379a92638d67)) +- **build:** try to publish adapters ([#395](https://github.com/dcmjs-org/dcmjs/issues/395)) ([191a17b](https://github.com/dcmjs-org/dcmjs/commit/191a17b690d2ac60ad98a4b8ecb8379a92638d67)) # 0.1.0 (2023-01-27) ### Bug Fixes -- 🐛 adding readme notes ([#191](https://github.com/dcmjs-org/dcmjs/issues/191)) ([459260d](https://github.com/dcmjs-org/dcmjs/commit/459260d6e2a6f905729ddf68b1f12d3140b53849)) -- 🐛 fix array format regression from commit 70b24332783d63c9db2ed21d512d9f7b526c5222 ([#236](https://github.com/dcmjs-org/dcmjs/issues/236)) ([5441063](https://github.com/dcmjs-org/dcmjs/commit/5441063d0395ede6d9f8bd3ac0d92ee14f6ef209)) -- 🐛 Fix rotation mapping for SEG cornerstone adapter ([#151](https://github.com/dcmjs-org/dcmjs/issues/151)) ([3fab68c](https://github.com/dcmjs-org/dcmjs/commit/3fab68cbfd95f82820663b9fc99a2b0cd07e43c8)) -- 🐛 Harden Segmentation import for different possible SEGs ([#146](https://github.com/dcmjs-org/dcmjs/issues/146)) ([c4952bc](https://github.com/dcmjs-org/dcmjs/commit/c4952bc5842bab80a5d928de0d860f89afc8f400)) -- 🐛 IDC Re [#2003](https://github.com/dcmjs-org/dcmjs/issues/2003): fix regression in parsing segmentation orietations ([#220](https://github.com/dcmjs-org/dcmjs/issues/220)) ([5c0c6a8](https://github.com/dcmjs-org/dcmjs/commit/5c0c6a85e67b25ce5f39412ced37d8e825691481)) -- 🐛 IDC2733: find segmentations reference source image Ids ([#253](https://github.com/dcmjs-org/dcmjs/issues/253)) ([f3e7101](https://github.com/dcmjs-org/dcmjs/commit/f3e71016dffa233bf0fb912cc7cf413718b8a1a9)) -- 🐛 ignore frames without SourceImageSequence information when loading a segmentation ([#198](https://github.com/dcmjs-org/dcmjs/issues/198)) ([82709c4](https://github.com/dcmjs-org/dcmjs/commit/82709c4a8a317aa1354244010300ab9b902802dd)) -- 🐛 indentation in nearlyEqual ([#202](https://github.com/dcmjs-org/dcmjs/issues/202)) ([989d6c9](https://github.com/dcmjs-org/dcmjs/commit/989d6c9a80686425563c55424ac1795e6a06cd7b)) -- 🐛 relax condition in nearlyEquals check for detecting numbers near to zero ([#304](https://github.com/dcmjs-org/dcmjs/issues/304)) ([974cddd](https://github.com/dcmjs-org/dcmjs/commit/974cddd785c076f1ac0211b534a7c0b82a4ba68a)) -- 🐛 When converting to multiframe, fix IPP issues ([#152](https://github.com/dcmjs-org/dcmjs/issues/152)) ([80496e4](https://github.com/dcmjs-org/dcmjs/commit/80496e422152c1a3dfd850a145011dd3dc632964)) -- **adapter:** Removed comment around getTID300RepresentationArguments 'tool' parameter ([#322](https://github.com/dcmjs-org/dcmjs/issues/322)) ([d8f05ff](https://github.com/dcmjs-org/dcmjs/commit/d8f05ffb9ef1b5cce254980a597b4c428ffdfb6e)), closes [#306](https://github.com/dcmjs-org/dcmjs/issues/306) -- **add check for nullable numeric string vrs:** adds a check for nullable numeric strinv vrs ([#150](https://github.com/dcmjs-org/dcmjs/issues/150)) ([75046c4](https://github.com/dcmjs-org/dcmjs/commit/75046c4e1b2830dd3a32dc8d6938f6def71940a5)) -- **anonymizer:** [FIX & TESTS] cleanTags : check if param is undefined. Add 3 test ([#308](https://github.com/dcmjs-org/dcmjs/issues/308)) ([44d23d6](https://github.com/dcmjs-org/dcmjs/commit/44d23d6a9e347fcf049053129d4d7323a9258b71)) -- ArrowAnnotateTool adapter in Cornerstone3D parsing label ([#270](https://github.com/dcmjs-org/dcmjs/issues/270)) ([cb84979](https://github.com/dcmjs-org/dcmjs/commit/cb84979be6eef8835cc7ced006625751a04356aa)) -- avoid using replaceAll() which isn't available in Node.js 14 ([#296](https://github.com/dcmjs-org/dcmjs/issues/296)) ([7aac3ab](https://github.com/dcmjs-org/dcmjs/commit/7aac3ab05fdedfe1a7a159097690bec00c0bcd2b)) -- bug tolerance parameter was not propagated ([#241](https://github.com/dcmjs-org/dcmjs/issues/241)) ([c2ed627](https://github.com/dcmjs-org/dcmjs/commit/c2ed6275ccb80fbbab3c0f9c67893d6b681b0bab)) -- checking the length before writing a DS and using exponential if ([#176](https://github.com/dcmjs-org/dcmjs/issues/176)) ([601aa9e](https://github.com/dcmjs-org/dcmjs/commit/601aa9e8a6df26876b08da739451e538d46aa37b)) -- **coding-scheme:** Fix coding scheme for updated standard ([ae3f0b5](https://github.com/dcmjs-org/dcmjs/commit/ae3f0b5bcd398a4230c8fba2954e225fda97cc2f)) -- **cornerstone:** exceptions caused by undefined cached stats in adapters. Safe programming fix only ([#301](https://github.com/dcmjs-org/dcmjs/issues/301)) ([893be43](https://github.com/dcmjs-org/dcmjs/commit/893be433af05f11a03cfce0a572b448303b9334b)) -- **cs:** [#318](https://github.com/dcmjs-org/dcmjs/issues/318) - check instance's NumberOfFrames property to see if it is a multi-frame file or not ([#320](https://github.com/dcmjs-org/dcmjs/issues/320)) ([0b030a4](https://github.com/dcmjs-org/dcmjs/commit/0b030a45dd48f22a61a90dfe5bbb5848425960ca)) -- **cs:** Resolves [#316](https://github.com/dcmjs-org/dcmjs/issues/316) Cornerstone3D adaptor - Multiframe support - add "frameNumber" to the Annotation.data ([#317](https://github.com/dcmjs-org/dcmjs/issues/317)) ([5fe862e](https://github.com/dcmjs-org/dcmjs/commit/5fe862e22653d30bfc78e5be5de3669610887727)) -- **dcmjs:** Add a set of accessors to the sequence list so the API is more consistent ([#224](https://github.com/dcmjs-org/dcmjs/issues/224)) ([9dad6c5](https://github.com/dcmjs-org/dcmjs/commit/9dad6c549cb4dd5c351caa13386998cbe48a1ba6)) -- **DicomMessage:** Fix readFile after options were added ([c2b62a1](https://github.com/dcmjs-org/dcmjs/commit/c2b62a13b3afc78516f39b906ca7576537037c20)) -- **encoding:** encapsulation is applied for only PixelData ([#199](https://github.com/dcmjs-org/dcmjs/issues/199)) ([ede2950](https://github.com/dcmjs-org/dcmjs/commit/ede2950d530fb189bb5817db6b5285e2be74ffee)), closes [#194](https://github.com/dcmjs-org/dcmjs/issues/194) -- Ensure DS and IS Value Representations are returned as arrays ([#83](https://github.com/dcmjs-org/dcmjs/issues/83)) ([a264661](https://github.com/dcmjs-org/dcmjs/commit/a264661d5a0a899b761c58492955f6d18cc03a4d)) -- exception writing NaN and Infinity values of FD tags ([#325](https://github.com/dcmjs-org/dcmjs/issues/325)) ([e86daaa](https://github.com/dcmjs-org/dcmjs/commit/e86daaad4c47e7ec2c135b94f0b0de622de69310)) -- Export loglevelnext logger as dcmjs.log for configuration ([#156](https://github.com/dcmjs-org/dcmjs/issues/156)) ([33515e5](https://github.com/dcmjs-org/dcmjs/commit/33515e5fe3f581d71278674860834ee1ea932faa)) -- Fix UN & AT VR processing logic ([#167](https://github.com/dcmjs-org/dcmjs/issues/167)) ([#168](https://github.com/dcmjs-org/dcmjs/issues/168)) ([7cb975a](https://github.com/dcmjs-org/dcmjs/commit/7cb975af106d2e3e943881a7dac06f1fe391809c)) -- force a release for commit caaac4b ([#240](https://github.com/dcmjs-org/dcmjs/issues/240)) ([f53b630](https://github.com/dcmjs-org/dcmjs/commit/f53b6306ce138c5ad3106015c4751b63fbcca362)) -- **fragment:** Refactor and fragment bug ([#283](https://github.com/dcmjs-org/dcmjs/issues/283)) ([307d60a](https://github.com/dcmjs-org/dcmjs/commit/307d60a6ffecf7d96bc729a37a45775c6b6e189c)), closes [#282](https://github.com/dcmjs-org/dcmjs/issues/282) -- **fragment:** write padding to even length on final fragments of encapsulated frame data ([#294](https://github.com/dcmjs-org/dcmjs/issues/294)) ([34b7561](https://github.com/dcmjs-org/dcmjs/commit/34b7561fa48870a87b948eb427d4e32808b4d40e)), closes [#293](https://github.com/dcmjs-org/dcmjs/issues/293) -- **idc-02252:** typo + release ([#180](https://github.com/dcmjs-org/dcmjs/issues/180)) ([3f5cb24](https://github.com/dcmjs-org/dcmjs/commit/3f5cb24fd0e50668d36dc21390a1ff527505a8db)) -- **image-comments:** Move ImageComments to DerivedPixels ([da11200](https://github.com/dcmjs-org/dcmjs/commit/da112001e3c1c01b30acbd634fd9b2f46a9d3a63)) -- **import:** missing import for addAccessors ([#295](https://github.com/dcmjs-org/dcmjs/issues/295)) ([6b631b6](https://github.com/dcmjs-org/dcmjs/commit/6b631b6c8bdb2a7a70637308c9ebfe849fe9ccaf)) -- infinite loop on dcm with no meta length ([#331](https://github.com/dcmjs-org/dcmjs/issues/331)) ([51b156b](https://github.com/dcmjs-org/dcmjs/commit/51b156bf1278fc0f9476de70c89697576c0f4b55)) -- Invalid VR of the private creator tag of the "Implicit VR Endian" typed DICOM file ([#242](https://github.com/dcmjs-org/dcmjs/issues/242)) ([#243](https://github.com/dcmjs-org/dcmjs/issues/243)) ([6d0552f](https://github.com/dcmjs-org/dcmjs/commit/6d0552fb96c59dcf3e39b3306a50004b24128330)) -- issues in binary tag parsing ([#276](https://github.com/dcmjs-org/dcmjs/issues/276)) ([60c3af1](https://github.com/dcmjs-org/dcmjs/commit/60c3af1654b8f64baea1cd47f1049fd16ea2fee8)) -- **measurement-report:** Fix issues with Measurement Report for Bidirectional measurements ([25cf222](https://github.com/dcmjs-org/dcmjs/commit/25cf222e9d80bbe5b68854362383ac4fcaec7f6c)) -- **measurement-report:** Fix ReferencedFrameNumber usage in MeasurementReport ([b80cd2a](https://github.com/dcmjs-org/dcmjs/commit/b80cd2af070d0280295e88ee24d7e4d43c4af861)) -- **naturalize:** revert single element sequence ([#223](https://github.com/dcmjs-org/dcmjs/issues/223)) ([0743ed3](https://github.com/dcmjs-org/dcmjs/commit/0743ed34f657b622d4868fe5db37015ad1aa7850)) -- **naturalizing:** Fix the exception on naturalize twice ([#237](https://github.com/dcmjs-org/dcmjs/issues/237)) ([abced98](https://github.com/dcmjs-org/dcmjs/commit/abced980dccbabce7c4b159600f89c25ba747076)) -- **parsing:** can't read an encapsulated frame whose size is greater than fragment size ([#205](https://github.com/dcmjs-org/dcmjs/issues/205)) ([176875d](https://github.com/dcmjs-org/dcmjs/commit/176875d0c9c34704302512d2c27103db205b1c8f)), closes [#204](https://github.com/dcmjs-org/dcmjs/issues/204) -- Re IDC [#2761](https://github.com/dcmjs-org/dcmjs/issues/2761) fix loading of segmentations ([#258](https://github.com/dcmjs-org/dcmjs/issues/258)) ([ceaf09a](https://github.com/dcmjs-org/dcmjs/commit/ceaf09af74f5727205e5d5869c97114b2c283ae5)) -- **Segmentation_4X:** Update tag name in getSegmentIndex method for segs ([#183](https://github.com/dcmjs-org/dcmjs/issues/183)) ([1e96ee3](https://github.com/dcmjs-org/dcmjs/commit/1e96ee3ce3280900d56f1887da81471f9b128d30)) -- **seg:** Use ReferencedSegmentNumber in shared fg ([#166](https://github.com/dcmjs-org/dcmjs/issues/166)) ([0ed3347](https://github.com/dcmjs-org/dcmjs/commit/0ed33477bb1c13b05d682c342edaf0a901fe0f7a)) -- several issues with character set handling ([#299](https://github.com/dcmjs-org/dcmjs/issues/299)) ([8e22107](https://github.com/dcmjs-org/dcmjs/commit/8e221074179b6d33b82ab5c6f1ae4c06c5522d6b)) -- **tests:** unified test data loading ([#292](https://github.com/dcmjs-org/dcmjs/issues/292)) ([c34f398](https://github.com/dcmjs-org/dcmjs/commit/c34f39813755227b79f7a0958a81a5e5c0935b73)) -- **update jsdocs, cut release:** release ([#203](https://github.com/dcmjs-org/dcmjs/issues/203)) ([307974c](https://github.com/dcmjs-org/dcmjs/commit/307974cb399d07152e0f6d4dd9f40fe1c17f076e)) -- update readme to trigger release ([#257](https://github.com/dcmjs-org/dcmjs/issues/257)) ([554f50d](https://github.com/dcmjs-org/dcmjs/commit/554f50d838fbcbeed25460c7384e14dc46bebb11)) -- update variable name for frame index ([#332](https://github.com/dcmjs-org/dcmjs/issues/332)) ([5515d6e](https://github.com/dcmjs-org/dcmjs/commit/5515d6e0abe11dee04f9b75272d3fbb5d00b2bd7)) -- use metadataProvider option instead of cornerstone.metaData ([#280](https://github.com/dcmjs-org/dcmjs/issues/280)) ([3a0e484](https://github.com/dcmjs-org/dcmjs/commit/3a0e484d203770cd309e2bd9f78946a733e79d0c)) -- **utilities:** Export Bidirectional and Polyline inside TID300 ([75e0e29](https://github.com/dcmjs-org/dcmjs/commit/75e0e29f771843a80bfa32532d2186a4e7cdbd57)) -- **VR:** added support for specific character set ([#291](https://github.com/dcmjs-org/dcmjs/issues/291)) ([f103d19](https://github.com/dcmjs-org/dcmjs/commit/f103d1918d02780c4881db5c8aa30f653c4da6b6)) -- **vr:** Convert empty DecimalString and NumberString to null instead of to zero ([#278](https://github.com/dcmjs-org/dcmjs/issues/278)) ([43cd8ea](https://github.com/dcmjs-org/dcmjs/commit/43cd8eaa316a08ff1a025b1267fedda239526acb)) -- **writeBytes:** create release from commit ([d9a4105](https://github.com/dcmjs-org/dcmjs/commit/d9a41050e756f9b7e56ef4ae3d5000ce86ea39eb)) +- 🐛 adding readme notes ([#191](https://github.com/dcmjs-org/dcmjs/issues/191)) ([459260d](https://github.com/dcmjs-org/dcmjs/commit/459260d6e2a6f905729ddf68b1f12d3140b53849)) +- 🐛 fix array format regression from commit 70b24332783d63c9db2ed21d512d9f7b526c5222 ([#236](https://github.com/dcmjs-org/dcmjs/issues/236)) ([5441063](https://github.com/dcmjs-org/dcmjs/commit/5441063d0395ede6d9f8bd3ac0d92ee14f6ef209)) +- 🐛 Fix rotation mapping for SEG cornerstone adapter ([#151](https://github.com/dcmjs-org/dcmjs/issues/151)) ([3fab68c](https://github.com/dcmjs-org/dcmjs/commit/3fab68cbfd95f82820663b9fc99a2b0cd07e43c8)) +- 🐛 Harden Segmentation import for different possible SEGs ([#146](https://github.com/dcmjs-org/dcmjs/issues/146)) ([c4952bc](https://github.com/dcmjs-org/dcmjs/commit/c4952bc5842bab80a5d928de0d860f89afc8f400)) +- 🐛 IDC Re [#2003](https://github.com/dcmjs-org/dcmjs/issues/2003): fix regression in parsing segmentation orietations ([#220](https://github.com/dcmjs-org/dcmjs/issues/220)) ([5c0c6a8](https://github.com/dcmjs-org/dcmjs/commit/5c0c6a85e67b25ce5f39412ced37d8e825691481)) +- 🐛 IDC2733: find segmentations reference source image Ids ([#253](https://github.com/dcmjs-org/dcmjs/issues/253)) ([f3e7101](https://github.com/dcmjs-org/dcmjs/commit/f3e71016dffa233bf0fb912cc7cf413718b8a1a9)) +- 🐛 ignore frames without SourceImageSequence information when loading a segmentation ([#198](https://github.com/dcmjs-org/dcmjs/issues/198)) ([82709c4](https://github.com/dcmjs-org/dcmjs/commit/82709c4a8a317aa1354244010300ab9b902802dd)) +- 🐛 indentation in nearlyEqual ([#202](https://github.com/dcmjs-org/dcmjs/issues/202)) ([989d6c9](https://github.com/dcmjs-org/dcmjs/commit/989d6c9a80686425563c55424ac1795e6a06cd7b)) +- 🐛 relax condition in nearlyEquals check for detecting numbers near to zero ([#304](https://github.com/dcmjs-org/dcmjs/issues/304)) ([974cddd](https://github.com/dcmjs-org/dcmjs/commit/974cddd785c076f1ac0211b534a7c0b82a4ba68a)) +- 🐛 When converting to multiframe, fix IPP issues ([#152](https://github.com/dcmjs-org/dcmjs/issues/152)) ([80496e4](https://github.com/dcmjs-org/dcmjs/commit/80496e422152c1a3dfd850a145011dd3dc632964)) +- **adapter:** Removed comment around getTID300RepresentationArguments 'tool' parameter ([#322](https://github.com/dcmjs-org/dcmjs/issues/322)) ([d8f05ff](https://github.com/dcmjs-org/dcmjs/commit/d8f05ffb9ef1b5cce254980a597b4c428ffdfb6e)), closes [#306](https://github.com/dcmjs-org/dcmjs/issues/306) +- **add check for nullable numeric string vrs:** adds a check for nullable numeric strinv vrs ([#150](https://github.com/dcmjs-org/dcmjs/issues/150)) ([75046c4](https://github.com/dcmjs-org/dcmjs/commit/75046c4e1b2830dd3a32dc8d6938f6def71940a5)) +- **anonymizer:** [FIX & TESTS] cleanTags : check if param is undefined. Add 3 test ([#308](https://github.com/dcmjs-org/dcmjs/issues/308)) ([44d23d6](https://github.com/dcmjs-org/dcmjs/commit/44d23d6a9e347fcf049053129d4d7323a9258b71)) +- ArrowAnnotateTool adapter in Cornerstone3D parsing label ([#270](https://github.com/dcmjs-org/dcmjs/issues/270)) ([cb84979](https://github.com/dcmjs-org/dcmjs/commit/cb84979be6eef8835cc7ced006625751a04356aa)) +- avoid using replaceAll() which isn't available in Node.js 14 ([#296](https://github.com/dcmjs-org/dcmjs/issues/296)) ([7aac3ab](https://github.com/dcmjs-org/dcmjs/commit/7aac3ab05fdedfe1a7a159097690bec00c0bcd2b)) +- bug tolerance parameter was not propagated ([#241](https://github.com/dcmjs-org/dcmjs/issues/241)) ([c2ed627](https://github.com/dcmjs-org/dcmjs/commit/c2ed6275ccb80fbbab3c0f9c67893d6b681b0bab)) +- checking the length before writing a DS and using exponential if ([#176](https://github.com/dcmjs-org/dcmjs/issues/176)) ([601aa9e](https://github.com/dcmjs-org/dcmjs/commit/601aa9e8a6df26876b08da739451e538d46aa37b)) +- **coding-scheme:** Fix coding scheme for updated standard ([ae3f0b5](https://github.com/dcmjs-org/dcmjs/commit/ae3f0b5bcd398a4230c8fba2954e225fda97cc2f)) +- **cornerstone:** exceptions caused by undefined cached stats in adapters. Safe programming fix only ([#301](https://github.com/dcmjs-org/dcmjs/issues/301)) ([893be43](https://github.com/dcmjs-org/dcmjs/commit/893be433af05f11a03cfce0a572b448303b9334b)) +- **cs:** [#318](https://github.com/dcmjs-org/dcmjs/issues/318) - check instance's NumberOfFrames property to see if it is a multi-frame file or not ([#320](https://github.com/dcmjs-org/dcmjs/issues/320)) ([0b030a4](https://github.com/dcmjs-org/dcmjs/commit/0b030a45dd48f22a61a90dfe5bbb5848425960ca)) +- **cs:** Resolves [#316](https://github.com/dcmjs-org/dcmjs/issues/316) Cornerstone3D adaptor - Multiframe support - add "frameNumber" to the Annotation.data ([#317](https://github.com/dcmjs-org/dcmjs/issues/317)) ([5fe862e](https://github.com/dcmjs-org/dcmjs/commit/5fe862e22653d30bfc78e5be5de3669610887727)) +- **dcmjs:** Add a set of accessors to the sequence list so the API is more consistent ([#224](https://github.com/dcmjs-org/dcmjs/issues/224)) ([9dad6c5](https://github.com/dcmjs-org/dcmjs/commit/9dad6c549cb4dd5c351caa13386998cbe48a1ba6)) +- **DicomMessage:** Fix readFile after options were added ([c2b62a1](https://github.com/dcmjs-org/dcmjs/commit/c2b62a13b3afc78516f39b906ca7576537037c20)) +- **encoding:** encapsulation is applied for only PixelData ([#199](https://github.com/dcmjs-org/dcmjs/issues/199)) ([ede2950](https://github.com/dcmjs-org/dcmjs/commit/ede2950d530fb189bb5817db6b5285e2be74ffee)), closes [#194](https://github.com/dcmjs-org/dcmjs/issues/194) +- Ensure DS and IS Value Representations are returned as arrays ([#83](https://github.com/dcmjs-org/dcmjs/issues/83)) ([a264661](https://github.com/dcmjs-org/dcmjs/commit/a264661d5a0a899b761c58492955f6d18cc03a4d)) +- exception writing NaN and Infinity values of FD tags ([#325](https://github.com/dcmjs-org/dcmjs/issues/325)) ([e86daaa](https://github.com/dcmjs-org/dcmjs/commit/e86daaad4c47e7ec2c135b94f0b0de622de69310)) +- Export loglevelnext logger as dcmjs.log for configuration ([#156](https://github.com/dcmjs-org/dcmjs/issues/156)) ([33515e5](https://github.com/dcmjs-org/dcmjs/commit/33515e5fe3f581d71278674860834ee1ea932faa)) +- Fix UN & AT VR processing logic ([#167](https://github.com/dcmjs-org/dcmjs/issues/167)) ([#168](https://github.com/dcmjs-org/dcmjs/issues/168)) ([7cb975a](https://github.com/dcmjs-org/dcmjs/commit/7cb975af106d2e3e943881a7dac06f1fe391809c)) +- force a release for commit caaac4b ([#240](https://github.com/dcmjs-org/dcmjs/issues/240)) ([f53b630](https://github.com/dcmjs-org/dcmjs/commit/f53b6306ce138c5ad3106015c4751b63fbcca362)) +- **fragment:** Refactor and fragment bug ([#283](https://github.com/dcmjs-org/dcmjs/issues/283)) ([307d60a](https://github.com/dcmjs-org/dcmjs/commit/307d60a6ffecf7d96bc729a37a45775c6b6e189c)), closes [#282](https://github.com/dcmjs-org/dcmjs/issues/282) +- **fragment:** write padding to even length on final fragments of encapsulated frame data ([#294](https://github.com/dcmjs-org/dcmjs/issues/294)) ([34b7561](https://github.com/dcmjs-org/dcmjs/commit/34b7561fa48870a87b948eb427d4e32808b4d40e)), closes [#293](https://github.com/dcmjs-org/dcmjs/issues/293) +- **idc-02252:** typo + release ([#180](https://github.com/dcmjs-org/dcmjs/issues/180)) ([3f5cb24](https://github.com/dcmjs-org/dcmjs/commit/3f5cb24fd0e50668d36dc21390a1ff527505a8db)) +- **image-comments:** Move ImageComments to DerivedPixels ([da11200](https://github.com/dcmjs-org/dcmjs/commit/da112001e3c1c01b30acbd634fd9b2f46a9d3a63)) +- **import:** missing import for addAccessors ([#295](https://github.com/dcmjs-org/dcmjs/issues/295)) ([6b631b6](https://github.com/dcmjs-org/dcmjs/commit/6b631b6c8bdb2a7a70637308c9ebfe849fe9ccaf)) +- infinite loop on dcm with no meta length ([#331](https://github.com/dcmjs-org/dcmjs/issues/331)) ([51b156b](https://github.com/dcmjs-org/dcmjs/commit/51b156bf1278fc0f9476de70c89697576c0f4b55)) +- Invalid VR of the private creator tag of the "Implicit VR Endian" typed DICOM file ([#242](https://github.com/dcmjs-org/dcmjs/issues/242)) ([#243](https://github.com/dcmjs-org/dcmjs/issues/243)) ([6d0552f](https://github.com/dcmjs-org/dcmjs/commit/6d0552fb96c59dcf3e39b3306a50004b24128330)) +- issues in binary tag parsing ([#276](https://github.com/dcmjs-org/dcmjs/issues/276)) ([60c3af1](https://github.com/dcmjs-org/dcmjs/commit/60c3af1654b8f64baea1cd47f1049fd16ea2fee8)) +- **measurement-report:** Fix issues with Measurement Report for Bidirectional measurements ([25cf222](https://github.com/dcmjs-org/dcmjs/commit/25cf222e9d80bbe5b68854362383ac4fcaec7f6c)) +- **measurement-report:** Fix ReferencedFrameNumber usage in MeasurementReport ([b80cd2a](https://github.com/dcmjs-org/dcmjs/commit/b80cd2af070d0280295e88ee24d7e4d43c4af861)) +- **naturalize:** revert single element sequence ([#223](https://github.com/dcmjs-org/dcmjs/issues/223)) ([0743ed3](https://github.com/dcmjs-org/dcmjs/commit/0743ed34f657b622d4868fe5db37015ad1aa7850)) +- **naturalizing:** Fix the exception on naturalize twice ([#237](https://github.com/dcmjs-org/dcmjs/issues/237)) ([abced98](https://github.com/dcmjs-org/dcmjs/commit/abced980dccbabce7c4b159600f89c25ba747076)) +- **parsing:** can't read an encapsulated frame whose size is greater than fragment size ([#205](https://github.com/dcmjs-org/dcmjs/issues/205)) ([176875d](https://github.com/dcmjs-org/dcmjs/commit/176875d0c9c34704302512d2c27103db205b1c8f)), closes [#204](https://github.com/dcmjs-org/dcmjs/issues/204) +- Re IDC [#2761](https://github.com/dcmjs-org/dcmjs/issues/2761) fix loading of segmentations ([#258](https://github.com/dcmjs-org/dcmjs/issues/258)) ([ceaf09a](https://github.com/dcmjs-org/dcmjs/commit/ceaf09af74f5727205e5d5869c97114b2c283ae5)) +- **Segmentation_4X:** Update tag name in getSegmentIndex method for segs ([#183](https://github.com/dcmjs-org/dcmjs/issues/183)) ([1e96ee3](https://github.com/dcmjs-org/dcmjs/commit/1e96ee3ce3280900d56f1887da81471f9b128d30)) +- **seg:** Use ReferencedSegmentNumber in shared fg ([#166](https://github.com/dcmjs-org/dcmjs/issues/166)) ([0ed3347](https://github.com/dcmjs-org/dcmjs/commit/0ed33477bb1c13b05d682c342edaf0a901fe0f7a)) +- several issues with character set handling ([#299](https://github.com/dcmjs-org/dcmjs/issues/299)) ([8e22107](https://github.com/dcmjs-org/dcmjs/commit/8e221074179b6d33b82ab5c6f1ae4c06c5522d6b)) +- **tests:** unified test data loading ([#292](https://github.com/dcmjs-org/dcmjs/issues/292)) ([c34f398](https://github.com/dcmjs-org/dcmjs/commit/c34f39813755227b79f7a0958a81a5e5c0935b73)) +- **update jsdocs, cut release:** release ([#203](https://github.com/dcmjs-org/dcmjs/issues/203)) ([307974c](https://github.com/dcmjs-org/dcmjs/commit/307974cb399d07152e0f6d4dd9f40fe1c17f076e)) +- update readme to trigger release ([#257](https://github.com/dcmjs-org/dcmjs/issues/257)) ([554f50d](https://github.com/dcmjs-org/dcmjs/commit/554f50d838fbcbeed25460c7384e14dc46bebb11)) +- update variable name for frame index ([#332](https://github.com/dcmjs-org/dcmjs/issues/332)) ([5515d6e](https://github.com/dcmjs-org/dcmjs/commit/5515d6e0abe11dee04f9b75272d3fbb5d00b2bd7)) +- use metadataProvider option instead of cornerstone.metaData ([#280](https://github.com/dcmjs-org/dcmjs/issues/280)) ([3a0e484](https://github.com/dcmjs-org/dcmjs/commit/3a0e484d203770cd309e2bd9f78946a733e79d0c)) +- **utilities:** Export Bidirectional and Polyline inside TID300 ([75e0e29](https://github.com/dcmjs-org/dcmjs/commit/75e0e29f771843a80bfa32532d2186a4e7cdbd57)) +- **VR:** added support for specific character set ([#291](https://github.com/dcmjs-org/dcmjs/issues/291)) ([f103d19](https://github.com/dcmjs-org/dcmjs/commit/f103d1918d02780c4881db5c8aa30f653c4da6b6)) +- **vr:** Convert empty DecimalString and NumberString to null instead of to zero ([#278](https://github.com/dcmjs-org/dcmjs/issues/278)) ([43cd8ea](https://github.com/dcmjs-org/dcmjs/commit/43cd8eaa316a08ff1a025b1267fedda239526acb)) +- **writeBytes:** create release from commit ([d9a4105](https://github.com/dcmjs-org/dcmjs/commit/d9a41050e756f9b7e56ef4ae3d5000ce86ea39eb)) ### Features -- **adapters:** Add adapter for exporting polylines from dicom-microscopy-viewer to DICOM-SR ([#44](https://github.com/dcmjs-org/dcmjs/issues/44)) ([7a1947c](https://github.com/dcmjs-org/dcmjs/commit/7a1947c6ec164da4e9f66e90c1bbf1055978b173)) -- **adapters:** Add adapter to generate segments & geometry from SEG for easier use in VTKjs (migrated from vtkDisplay example) ([398b74d](https://github.com/dcmjs-org/dcmjs/commit/398b74d70deb32824a74e98210127326887dfa77)) -- **adapters:** First steps for DICOM-SR read support for polylines with dicom-microscopy-viewer ([#49](https://github.com/dcmjs-org/dcmjs/issues/49)) ([37f1888](https://github.com/dcmjs-org/dcmjs/commit/37f18881dd7369818de4aa22c5730012c2829616)) -- Add Cornerstone3D adapter for Length tool ([#261](https://github.com/dcmjs-org/dcmjs/issues/261)) ([2cab0d9](https://github.com/dcmjs-org/dcmjs/commit/2cab0d9edba20e99b11407d41ed2a4bf60a6ab9b)) -- Add overlapping segment check to Cornerstone 4.x DICOM SEG adapter ([#155](https://github.com/dcmjs-org/dcmjs/issues/155)) ([df44e27](https://github.com/dcmjs-org/dcmjs/commit/df44e27b3b1c26082bf3d7a9477ab7f0d5ca1d19)) -- Allow backslashes in UIDs in order to support DICOM Q&R ([#277](https://github.com/dcmjs-org/dcmjs/issues/277)) ([6d2d5c6](https://github.com/dcmjs-org/dcmjs/commit/6d2d5c60f500174abb1be7757094178ec6a1d144)) -- **anonymizer:** export Array tagNamesToEmpty and modify cleanTags ([#303](https://github.com/dcmjs-org/dcmjs/issues/303)) ([e960085](https://github.com/dcmjs-org/dcmjs/commit/e960085ca08fb28a0a8134fbfee7d450722d8c64)) -- Bidirectional Arrow and EllipticalROI adapters for CS3D ([#264](https://github.com/dcmjs-org/dcmjs/issues/264)) ([1fc7932](https://github.com/dcmjs-org/dcmjs/commit/1fc7932e3eed85fa1a0f857d84bab35a2e62d80d)) -- **cornerstone:** Feature add cornerstone adapters ([#225](https://github.com/dcmjs-org/dcmjs/issues/225)) ([23c0877](https://github.com/dcmjs-org/dcmjs/commit/23c08777f7a93d4c0576a5e583113029a1a1e05f)) -- **deflated:** Added support for reading datasets with deflated transfer syntax ([#312](https://github.com/dcmjs-org/dcmjs/issues/312)) ([ee8f8f2](https://github.com/dcmjs-org/dcmjs/commit/ee8f8f21babbbd8eac2b19e8e957db2cc5a09325)) -- **mem:** Zero Copy ArrayBuffer ([#279](https://github.com/dcmjs-org/dcmjs/issues/279)) ([a17f2d7](https://github.com/dcmjs-org/dcmjs/commit/a17f2d75bc102cc789289f0eae51e5b416ce1567)) -- Move adapters from dcmjs for Cornerstone/3d and VTK ([b136a21](https://github.com/dcmjs-org/dcmjs/commit/b136a21fb96bb28c3a10a63b6b78083b897f4e19)) -- **npm:** bump minor version (minor readme edits) ([b48f665](https://github.com/dcmjs-org/dcmjs/commit/b48f665893f5e4b643c68ddc2fc3b13c641b6b29)) -- **readme:** stress need for a PR for npm package ([#310](https://github.com/dcmjs-org/dcmjs/issues/310)) ([dafd78d](https://github.com/dcmjs-org/dcmjs/commit/dafd78d004ccb9d0b505f0b7e0fd461eda9c3710)) -- **sr:** export TID300 - Point class ([#323](https://github.com/dcmjs-org/dcmjs/issues/323)) ([d2aebc3](https://github.com/dcmjs-org/dcmjs/commit/d2aebc3166c23e14dce4d7a8b7f1e2fc933f6328)) -- **structured-reports:** Add initial work on Adapters / Utilities for Imaging Measurement Structured Report input / output ([#17](https://github.com/dcmjs-org/dcmjs/issues/17)) ([941ad75](https://github.com/dcmjs-org/dcmjs/commit/941ad75320eece3368104d08247fdc371497c7cd)) -- **testing:** Use the Jest testing framework and switch to GitHub Actions ([#254](https://github.com/dcmjs-org/dcmjs/issues/254)) ([a91ff2b](https://github.com/dcmjs-org/dcmjs/commit/a91ff2babd2c6a44f20ecbd954ab38901276cf41)) +- **adapters:** Add adapter for exporting polylines from dicom-microscopy-viewer to DICOM-SR ([#44](https://github.com/dcmjs-org/dcmjs/issues/44)) ([7a1947c](https://github.com/dcmjs-org/dcmjs/commit/7a1947c6ec164da4e9f66e90c1bbf1055978b173)) +- **adapters:** Add adapter to generate segments & geometry from SEG for easier use in VTKjs (migrated from vtkDisplay example) ([398b74d](https://github.com/dcmjs-org/dcmjs/commit/398b74d70deb32824a74e98210127326887dfa77)) +- **adapters:** First steps for DICOM-SR read support for polylines with dicom-microscopy-viewer ([#49](https://github.com/dcmjs-org/dcmjs/issues/49)) ([37f1888](https://github.com/dcmjs-org/dcmjs/commit/37f18881dd7369818de4aa22c5730012c2829616)) +- Add Cornerstone3D adapter for Length tool ([#261](https://github.com/dcmjs-org/dcmjs/issues/261)) ([2cab0d9](https://github.com/dcmjs-org/dcmjs/commit/2cab0d9edba20e99b11407d41ed2a4bf60a6ab9b)) +- Add overlapping segment check to Cornerstone 4.x DICOM SEG adapter ([#155](https://github.com/dcmjs-org/dcmjs/issues/155)) ([df44e27](https://github.com/dcmjs-org/dcmjs/commit/df44e27b3b1c26082bf3d7a9477ab7f0d5ca1d19)) +- Allow backslashes in UIDs in order to support DICOM Q&R ([#277](https://github.com/dcmjs-org/dcmjs/issues/277)) ([6d2d5c6](https://github.com/dcmjs-org/dcmjs/commit/6d2d5c60f500174abb1be7757094178ec6a1d144)) +- **anonymizer:** export Array tagNamesToEmpty and modify cleanTags ([#303](https://github.com/dcmjs-org/dcmjs/issues/303)) ([e960085](https://github.com/dcmjs-org/dcmjs/commit/e960085ca08fb28a0a8134fbfee7d450722d8c64)) +- Bidirectional Arrow and EllipticalROI adapters for CS3D ([#264](https://github.com/dcmjs-org/dcmjs/issues/264)) ([1fc7932](https://github.com/dcmjs-org/dcmjs/commit/1fc7932e3eed85fa1a0f857d84bab35a2e62d80d)) +- **cornerstone:** Feature add cornerstone adapters ([#225](https://github.com/dcmjs-org/dcmjs/issues/225)) ([23c0877](https://github.com/dcmjs-org/dcmjs/commit/23c08777f7a93d4c0576a5e583113029a1a1e05f)) +- **deflated:** Added support for reading datasets with deflated transfer syntax ([#312](https://github.com/dcmjs-org/dcmjs/issues/312)) ([ee8f8f2](https://github.com/dcmjs-org/dcmjs/commit/ee8f8f21babbbd8eac2b19e8e957db2cc5a09325)) +- **mem:** Zero Copy ArrayBuffer ([#279](https://github.com/dcmjs-org/dcmjs/issues/279)) ([a17f2d7](https://github.com/dcmjs-org/dcmjs/commit/a17f2d75bc102cc789289f0eae51e5b416ce1567)) +- Move adapters from dcmjs for Cornerstone/3d and VTK ([b136a21](https://github.com/dcmjs-org/dcmjs/commit/b136a21fb96bb28c3a10a63b6b78083b897f4e19)) +- **npm:** bump minor version (minor readme edits) ([b48f665](https://github.com/dcmjs-org/dcmjs/commit/b48f665893f5e4b643c68ddc2fc3b13c641b6b29)) +- **readme:** stress need for a PR for npm package ([#310](https://github.com/dcmjs-org/dcmjs/issues/310)) ([dafd78d](https://github.com/dcmjs-org/dcmjs/commit/dafd78d004ccb9d0b505f0b7e0fd461eda9c3710)) +- **sr:** export TID300 - Point class ([#323](https://github.com/dcmjs-org/dcmjs/issues/323)) ([d2aebc3](https://github.com/dcmjs-org/dcmjs/commit/d2aebc3166c23e14dce4d7a8b7f1e2fc933f6328)) +- **structured-reports:** Add initial work on Adapters / Utilities for Imaging Measurement Structured Report input / output ([#17](https://github.com/dcmjs-org/dcmjs/issues/17)) ([941ad75](https://github.com/dcmjs-org/dcmjs/commit/941ad75320eece3368104d08247fdc371497c7cd)) +- **testing:** Use the Jest testing framework and switch to GitHub Actions ([#254](https://github.com/dcmjs-org/dcmjs/issues/254)) ([a91ff2b](https://github.com/dcmjs-org/dcmjs/commit/a91ff2babd2c6a44f20ecbd954ab38901276cf41)) diff --git a/packages/adapters/README.md b/packages/adapters/README.md index adbaa45389..99e6b67531 100644 --- a/packages/adapters/README.md +++ b/packages/adapters/README.md @@ -9,17 +9,17 @@ # History - 2014 - - [DCMTK](dcmtk.org) cross compiled to javascript at [CTK Hackfest](http://www.commontk.org/index.php/CTK-Hackfest-May-2014). While this was useful and powerful, it was heavyweight for typical web usage. + - [DCMTK](dcmtk.org) cross compiled to javascript at [CTK Hackfest](http://www.commontk.org/index.php/CTK-Hackfest-May-2014). While this was useful and powerful, it was heavyweight for typical web usage. - 2016 - - A [Medical Imaging Web Appliction meeting at Stanford](http://qiicr.org/web/outreach/Medical-Imaging-Web-Apps/) and [follow-on hackfest in Boston](http://qiicr.org/web/outreach/MIWS-hackfest/) helped elaborate the needs for manipulating DICOM in pure Javascript. - - Based on [DICOM Part 10 read/write code](https://github.com/OHIF/dicom-dimse) initiated by Weiwei Wu of [OHIF](http://ohif.org), Steve Pieper [developed further features](https://github.com/pieper/sites/tree/gh-pages/dcmio) and [examples of creating multiframe and segmentation objects](https://github.com/pieper/sites/tree/gh-pages/DICOMzero) discussed with the community at RSNA + - A [Medical Imaging Web Appliction meeting at Stanford](http://qiicr.org/web/outreach/Medical-Imaging-Web-Apps/) and [follow-on hackfest in Boston](http://qiicr.org/web/outreach/MIWS-hackfest/) helped elaborate the needs for manipulating DICOM in pure Javascript. + - Based on [DICOM Part 10 read/write code](https://github.com/OHIF/dicom-dimse) initiated by Weiwei Wu of [OHIF](http://ohif.org), Steve Pieper [developed further features](https://github.com/pieper/sites/tree/gh-pages/dcmio) and [examples of creating multiframe and segmentation objects](https://github.com/pieper/sites/tree/gh-pages/DICOMzero) discussed with the community at RSNA - 2017 - - At [NA-MIC Project Week 25](https://na-mic.org/wiki/Project_Week_25) Erik Ziegler and Steve Pieper [worked](https://na-mic.org/wiki/Project_Week_25/DICOM_Segmentation_Support_for_Cornerstone_and_OHIF_Viewer) - with the community to define some example use cases to mix the pure JavaScript DICOM code with Cornerstone and [CornerstoneTools](https://github.com/chafey/cornerstoneTools). + - At [NA-MIC Project Week 25](https://na-mic.org/wiki/Project_Week_25) Erik Ziegler and Steve Pieper [worked](https://na-mic.org/wiki/Project_Week_25/DICOM_Segmentation_Support_for_Cornerstone_and_OHIF_Viewer) + with the community to define some example use cases to mix the pure JavaScript DICOM code with Cornerstone and [CornerstoneTools](https://github.com/chafey/cornerstoneTools). - 2018-2022 - - Work continues to develop SR and SEG support to [OHIFViewer](http://ohif.org) allow interoperability with [DICOM4QI](https://legacy.gitbook.com/book/qiicr/dicom4qi/details) + - Work continues to develop SR and SEG support to [OHIFViewer](http://ohif.org) allow interoperability with [DICOM4QI](https://legacy.gitbook.com/book/qiicr/dicom4qi/details) - 2023 - - Moved the adapters into cornerstone3D from dcmjsj + - Moved the adapters into cornerstone3D from dcmjsj # Support diff --git a/packages/adapters/generate-dictionary.js b/packages/adapters/generate-dictionary.js index 3715d120e1..0e10d5d606 100644 --- a/packages/adapters/generate-dictionary.js +++ b/packages/adapters/generate-dictionary.js @@ -3,143 +3,156 @@ * Reformat The DICOM dictionary PS3.6 and PS3.7 docbook XML files (from e.g. standard docs) to JavaScript syntax. * Based on https://github.com/pydicom/pydicom/blob/8112bb69bfc0423c3a08cb89e7960defbe7237bf/source/generate_dict/generate_dicom_dict.py */ -const fs = require('fs/promises'); -const http = require('http'); -const xml2js = require('xml2js'); +const fs = require("fs/promises"); +const http = require("http"); +const xml2js = require("xml2js"); -require('@babel/register'); -const DICTIONARY_PATH = './src/dictionary.js'; +require("@babel/register"); +const DICTIONARY_PATH = "./src/dictionary.js"; const dictionary = require(DICTIONARY_PATH).default; -const { Tag } = require('./src/Tag'); +const { Tag } = require("./src/Tag"); async function main() { - const tags = []; + const tags = []; - /** - * Collect DICOM tags from XML documents - */ - const part06 = await getDocbook('part06/part06.xml'); - const part06Rows = part06.book.chapter.find(chapter => chapter.$['xml:id'] === 'chapter_6').table[0].tbody[0].tr; - tags.push(...part06Rows.map(row => { - const retired = getCellData(row.td[5])?.startsWith('RET'); - const name = getCellData(row.td[2]); - return { - tag: getCellData(row.td[0]), - vr: getCellData(row.td[3]), - name: retired ? `RETIRED_${name}` : name, - vm: getCellData(row.td[4]), - version: retired ? 'DICOM/retired' : 'DICOM', - } - })); + /** + * Collect DICOM tags from XML documents + */ + const part06 = await getDocbook("part06/part06.xml"); + const part06Rows = part06.book.chapter.find( + chapter => chapter.$["xml:id"] === "chapter_6" + ).table[0].tbody[0].tr; + tags.push( + ...part06Rows.map(row => { + const retired = getCellData(row.td[5])?.startsWith("RET"); + const name = getCellData(row.td[2]); + return { + tag: getCellData(row.td[0]), + vr: getCellData(row.td[3]), + name: retired ? `RETIRED_${name}` : name, + vm: getCellData(row.td[4]), + version: retired ? "DICOM/retired" : "DICOM" + }; + }) + ); - const part07 = await getDocbook('part07/part07.xml'); - const chapterE = part07.book.chapter.find(chapter => chapter.$['xml:id'] === 'chapter_E'); - const commandFields = chapterE.section[0].table[0].tbody[0].tr; - tags.push(...commandFields.map(row => { - return { - tag: getCellData(row.td[0]), - vr: getCellData(row.td[3]), - name: getCellData(row.td[2]), - vm: getCellData(row.td[4]), - version: 'DICOM', - } - })); - const retiredCommandFields = chapterE.section[1].table[0].tbody[0].tr; - tags.push(...retiredCommandFields.map(row => { - return { - tag: getCellData(row.td[0]), - vr: getCellData(row.td[3]), - name: `RETIRED_${getCellData(row.td[2])}`, - vm: getCellData(row.td[4]), - version: 'DICOM/retired', - } - })); + const part07 = await getDocbook("part07/part07.xml"); + const chapterE = part07.book.chapter.find( + chapter => chapter.$["xml:id"] === "chapter_E" + ); + const commandFields = chapterE.section[0].table[0].tbody[0].tr; + tags.push( + ...commandFields.map(row => { + return { + tag: getCellData(row.td[0]), + vr: getCellData(row.td[3]), + name: getCellData(row.td[2]), + vm: getCellData(row.td[4]), + version: "DICOM" + }; + }) + ); + const retiredCommandFields = chapterE.section[1].table[0].tbody[0].tr; + tags.push( + ...retiredCommandFields.map(row => { + return { + tag: getCellData(row.td[0]), + vr: getCellData(row.td[3]), + name: `RETIRED_${getCellData(row.td[2])}`, + vm: getCellData(row.td[4]), + version: "DICOM/retired" + }; + }) + ); - const newTags = tags.filter(tag => tag.vr && tag.name && tag.vm) - .filter(tag => !(tag.tag in dictionary)) // filter already defined - .filter(tag => !/[(,\dA-F]x+[A-F\d,)]/.test(tag.tag)); // filter repeater tags + const newTags = tags + .filter(tag => tag.vr && tag.name && tag.vm) + .filter(tag => !(tag.tag in dictionary)) // filter already defined + .filter(tag => !/[(,\dA-F]x+[A-F\d,)]/.test(tag.tag)); // filter repeater tags - /** - * Insert new tags into dictionary, ordered among tags with the same version - */ - const dictionaryArray = Object.values(dictionary); - for (const newTag of newTags) { - const parsedTag = Tag.fromPString(newTag.tag); - const insertIndex = dictionaryArray.findIndex(tag => { - if (tag.version !== newTag.version) { - return false; - } - const thisTag = Tag.fromPString(tag.tag); - return thisTag.toCleanString() > parsedTag.toCleanString(); - }); - dictionaryArray.splice(insertIndex, 0, newTag); - } + /** + * Insert new tags into dictionary, ordered among tags with the same version + */ + const dictionaryArray = Object.values(dictionary); + for (const newTag of newTags) { + const parsedTag = Tag.fromPString(newTag.tag); + const insertIndex = dictionaryArray.findIndex(tag => { + if (tag.version !== newTag.version) { + return false; + } + const thisTag = Tag.fromPString(tag.tag); + return thisTag.toCleanString() > parsedTag.toCleanString(); + }); + dictionaryArray.splice(insertIndex, 0, newTag); + } - await writeDictionary(dictionaryArray); + await writeDictionary(dictionaryArray); } async function writeDictionary(tags) { - let data = 'const dictionary = {'; - for (const tag of tags) { - if (!tag.tag) { - data += ` + let data = "const dictionary = {"; + for (const tag of tags) { + if (!tag.tag) { + data += ` "": { tag: "" - },` - continue; - } - const tagKey = tag.tag.includes('"') ? `'${tag.tag}'` : `"${tag.tag}"`; - data += ` + },`; + continue; + } + const tagKey = tag.tag.includes('"') ? `'${tag.tag}'` : `"${tag.tag}"`; + data += ` ${tagKey}: { tag: ${tagKey}, vr: "${tag.vr}", name: "${tag.name}", vm: "${tag.vm}", - version: "${tag.version ?? 'PrivateTag'}" + version: "${tag.version ?? "PrivateTag"}" },`; - } - data += ` + } + data += ` }; export default dictionary; `; - await fs.writeFile(DICTIONARY_PATH, data); + await fs.writeFile(DICTIONARY_PATH, data); } async function getDocbook(part) { - const source = await getUrl(`http://dicom.nema.org/medical/dicom/current/source/docbook/${part}`); - return xml2js.parseStringPromise(source); + const source = await getUrl( + `http://dicom.nema.org/medical/dicom/current/source/docbook/${part}` + ); + return xml2js.parseStringPromise(source); } function getCellData(td) { - const para = td.para?.[0]; - if (!para) { - return undefined; - } - const text = para.emphasis ? para.emphasis[0]._ : para._; - return text?.trim().replace(/[\u200b\uffff]/g, ''); + const para = td.para?.[0]; + if (!para) { + return undefined; + } + const text = para.emphasis ? para.emphasis[0]._ : para._; + return text?.trim().replace(/[\u200b\uffff]/g, ""); } function getUrl(url) { - return new Promise((resolve, reject) => { - http.get(url, request => { - let data = ''; - request.on('error', () => { - reject(error); - }); - request.on('end', () => { - resolve(data); - }); - request.on('data', chunk => { - data += chunk; - }); + return new Promise((resolve, reject) => { + http.get(url, request => { + let data = ""; + request.on("error", () => { + reject(error); + }); + request.on("end", () => { + resolve(data); + }); + request.on("data", chunk => { + data += chunk; + }); + }); }); - }); } if (require.main === module) { - main().catch(error => { - console.log(error); - }); + main().catch(error => { + console.log(error); + }); } diff --git a/packages/adapters/jest.config.js b/packages/adapters/jest.config.js index bd1f3dad96..d4b4624e3e 100644 --- a/packages/adapters/jest.config.js +++ b/packages/adapters/jest.config.js @@ -5,6 +5,7 @@ const path = require("path"); module.exports = { ...base, displayName: "adapters", + testMatch: ["/test/**/*.jest.js"], moduleNameMapper: { "^@cornerstonejs/(.*)$": path.resolve(__dirname, "../$1/src") } diff --git a/packages/adapters/package.json b/packages/adapters/package.json index 191baf4399..80eed14e12 100644 --- a/packages/adapters/package.json +++ b/packages/adapters/package.json @@ -65,11 +65,10 @@ "clean:deep": "yarn run clean && shx rm -rf node_modules", "dev": "rollup --watch -c rollup.config.mjs", "build:all": "yarn build", - "format-check": "npx eslint ./src --quiet", "api-check": "echo 'No API check for this package'", "start": "rollup --watch -c rollup.config.mjs", "format": "prettier --write 'src/**/*.js' 'test/**/*.js'", - "lint": "eslint --fix .", + "lint": "oxlint .", "prebuild": "node ../../scripts/generate-version.js ./" }, "repository": { @@ -85,7 +84,7 @@ "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", - "dcmjs": "^0.42.0", + "dcmjs": "^0.43.1", "gl-matrix": "^3.4.3", "ndarray": "^1.0.19" }, diff --git a/packages/adapters/src/adapters/Cornerstone/MeasurementReport.js b/packages/adapters/src/adapters/Cornerstone/MeasurementReport.js index 6c8a9aea6a..9c26d8dd0e 100644 --- a/packages/adapters/src/adapters/Cornerstone/MeasurementReport.js +++ b/packages/adapters/src/adapters/Cornerstone/MeasurementReport.js @@ -34,10 +34,13 @@ const codeValueMatch = (group, code, oldCode) => { function getTID300ContentItem(tool, ReferencedSOPSequence, adapterClass) { const args = adapterClass.getTID300RepresentationArguments(tool); args.ReferencedSOPSequence = ReferencedSOPSequence; + args.ReferencedFrameOfReferenceUID = args.use3DSpatialCoordinates + ? tool.metadata.FrameOfReferenceUID + : null; - const TID300Measurement = new adapterClass.TID300Representation(args); + const tid300Measurement = new adapterClass.TID300Representation(args); - return TID300Measurement; + return tid300Measurement; } function getMeasurementGroup(toolType, toolData, ReferencedSOPSequence) { @@ -55,11 +58,11 @@ function getMeasurementGroup(toolType, toolData, ReferencedSOPSequence) { // Loop through the array of tool instances // for this tool - const Measurements = toolTypeData.data.map(tool => { + const measurements = toolTypeData.data.map(tool => { return getTID300ContentItem(tool, ReferencedSOPSequence, toolClass); }); - return new TID1501MeasurementGroup(Measurements); + return new TID1501MeasurementGroup(measurements); } export default class MeasurementReport { @@ -184,7 +187,7 @@ export default class MeasurementReport { allMeasurementGroups.concat(measurementGroups); }); - const MeasurementReport = new TID1500MeasurementReport( + const tid1500MeasurementReport = new TID1500MeasurementReport( { TID1501MeasurementGroups: allMeasurementGroups }, options ); @@ -232,7 +235,7 @@ export default class MeasurementReport { const report = new StructuredReport([derivationSourceDataset]); - const contentItem = MeasurementReport.contentItem( + const contentItem = tid1500MeasurementReport.contentItem( derivationSourceDataset ); diff --git a/packages/adapters/src/adapters/Cornerstone/ParametricMap.ts b/packages/adapters/src/adapters/Cornerstone/ParametricMap.ts index eba5a20419..89b3f97939 100644 --- a/packages/adapters/src/adapters/Cornerstone/ParametricMap.ts +++ b/packages/adapters/src/adapters/Cornerstone/ParametricMap.ts @@ -1,6 +1,6 @@ import { log, data as dcmjsData, normalizers } from "dcmjs"; import checkOrientation from "../helpers/checkOrientation"; -import compareArrays from "../helpers/compareArrays"; +import { utilities } from "@cornerstonejs/core"; const { DicomMessage, DicomMetaDictionary } = dcmjsData; const { Normalizer } = normalizers; @@ -338,7 +338,7 @@ function getImageIdOfSourceImagebyGeometry( } if ( - compareArrays( + utilities.isEqual( PerFrameFunctionalGroup.PlanePositionSequence[0] .ImagePositionPatient, sourceImageMetadata.ImagePositionPatient, diff --git a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js index 9e159dbfb4..b11293db58 100644 --- a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js +++ b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js @@ -8,7 +8,7 @@ import { import ndarray from "ndarray"; import getDatasetsFromImages from "../helpers/getDatasetsFromImages"; import checkOrientation from "../helpers/checkOrientation"; -import compareArrays from "../helpers/compareArrays"; +import { utilities as csUtilities } from "@cornerstonejs/core"; import { Events } from "../enums"; @@ -1087,9 +1087,9 @@ export const getSegmentIndex = (multiframe, frame) => { ? PerFrameFunctionalGroups.SegmentIdentificationSequence .ReferencedSegmentNumber : SharedFunctionalGroupsSequence.SegmentIdentificationSequence - ? SharedFunctionalGroupsSequence.SegmentIdentificationSequence - .ReferencedSegmentNumber - : undefined; + ? SharedFunctionalGroupsSequence.SegmentIdentificationSequence + .ReferencedSegmentNumber + : undefined; }; export function insertPixelDataPlanar( @@ -1390,6 +1390,12 @@ export function getImageIdOfSourceImageBySourceImageSequence( /frames\/\d+/, `frames/${ReferencedFrameNumber}` ); + } else if (baseImageId.includes("dicomfile:")) { + // dicomfile base 1, despite having frame= + return baseImageId.replace( + /frame=\d+/, + `frame=${ReferencedFrameNumber}` + ); } else if (baseImageId.includes("frame=")) { return baseImageId.replace( /frame=\d+/, @@ -1476,12 +1482,12 @@ export function getImageIdOfSourceImagebyGeometry( if ( framePosition && - compareArrays(segFramePosition, framePosition, tolerance) + csUtilities.isEqual(segFramePosition, framePosition, tolerance) ) { return imageId; } } else if ( - compareArrays( + csUtilities.isEqual( segFramePosition, sourceImageMetadata.ImagePositionPatient, tolerance @@ -1569,38 +1575,38 @@ export function alignPixelDataWithSourceData( orientations, tolerance ) { - if (compareArrays(iop, orientations[0], tolerance)) { + if (csUtilities.isEqual(iop, orientations[0], tolerance)) { return pixelData2D; - } else if (compareArrays(iop, orientations[1], tolerance)) { + } else if (csUtilities.isEqual(iop, orientations[1], tolerance)) { // Flipped vertically. // Undo Flip return flipMatrix2D.v(pixelData2D); - } else if (compareArrays(iop, orientations[2], tolerance)) { + } else if (csUtilities.isEqual(iop, orientations[2], tolerance)) { // Flipped horizontally. // Unfo flip return flipMatrix2D.h(pixelData2D); - } else if (compareArrays(iop, orientations[3], tolerance)) { + } else if (csUtilities.isEqual(iop, orientations[3], tolerance)) { //Rotated 90 degrees // Rotate back return rotateMatrix902D(pixelData2D); - } else if (compareArrays(iop, orientations[4], tolerance)) { + } else if (csUtilities.isEqual(iop, orientations[4], tolerance)) { //Rotated 90 degrees and fliped horizontally. // Undo flip and rotate back. return rotateMatrix902D(flipMatrix2D.h(pixelData2D)); - } else if (compareArrays(iop, orientations[5], tolerance)) { + } else if (csUtilities.isEqual(iop, orientations[5], tolerance)) { // Rotated 90 degrees and fliped vertically // Unfo flip and rotate back. return rotateMatrix902D(flipMatrix2D.v(pixelData2D)); - } else if (compareArrays(iop, orientations[6], tolerance)) { + } else if (csUtilities.isEqual(iop, orientations[6], tolerance)) { // Rotated 180 degrees. // TODO -> Do this more effeciently, there is a 1:1 mapping like 90 degree rotation. return rotateMatrix902D(rotateMatrix902D(pixelData2D)); - } else if (compareArrays(iop, orientations[7], tolerance)) { + } else if (csUtilities.isEqual(iop, orientations[7], tolerance)) { // Rotated 270 degrees // Rotate back. diff --git a/packages/adapters/src/adapters/Cornerstone3D/Angle.ts b/packages/adapters/src/adapters/Cornerstone3D/Angle.ts index e825ff1c8b..feebb00059 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Angle.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Angle.ts @@ -1,6 +1,7 @@ import { utilities } from "dcmjs"; import MeasurementReport from "./MeasurementReport"; import BaseAdapter3D from "./BaseAdapter3D"; +import { toScoord } from "../helpers"; const { CobbAngle: TID300CobbAngle } = utilities.TID300; @@ -14,165 +15,65 @@ class Angle extends BaseAdapter3D { public static getMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata ) { const { - defaultState, + state, NUMGroup, - SCOORDGroup, - SCOORD3DGroup, + worldCoords, + referencedImageId, ReferencedFrameNumber } = MeasurementReport.getSetupMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, metadata, - Angle.toolType + this.toolType ); - if (SCOORDGroup) { - return this.getMeasurementDataFromScoord({ - defaultState, - SCOORDGroup, - imageToWorldCoords, - NUMGroup, - ReferencedFrameNumber - }); - } else if (SCOORD3DGroup) { - return this.getMeasurementDataFromScoord3D({ - defaultState, - SCOORD3DGroup - }); - } else { - throw new Error( - "Can't get measurement data with missing SCOORD and SCOORD3D groups." - ); - } - } - - static getMeasurementDataFromScoord({ - defaultState, - SCOORDGroup, - imageToWorldCoords, - NUMGroup, - ReferencedFrameNumber - }) { - const referencedImageId = - defaultState.annotation.metadata.referencedImageId; - - const { GraphicData } = SCOORDGroup; - const worldCoords = []; - for (let i = 0; i < GraphicData.length; i += 2) { - const point = imageToWorldCoords(referencedImageId, [ - GraphicData[i], - GraphicData[i + 1] - ]); - worldCoords.push(point); - } - - const state = defaultState; + const cachedStats = referencedImageId + ? { + [`imageId:${referencedImageId}`]: { + angle: NUMGroup + ? NUMGroup.MeasuredValueSequence.NumericValue + : null + } + } + : {}; state.annotation.data = { + ...state.annotation.data, handles: { - points: [worldCoords[0], worldCoords[1], worldCoords[3]], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - }, - cachedStats: { - [`imageId:${referencedImageId}`]: { - angle: NUMGroup - ? NUMGroup.MeasuredValueSequence.NumericValue - : null - } + ...state.annotation.data.handles, + points: [worldCoords[0], worldCoords[1], worldCoords[3]] }, + cachedStats, frameNumber: ReferencedFrameNumber }; return state; } - static getMeasurementDataFromScoord3D({ defaultState, SCOORD3DGroup }) { - const { GraphicData } = SCOORD3DGroup; - const worldCoords = []; - for (let i = 0; i < GraphicData.length; i += 3) { - const point = [ - GraphicData[i], - GraphicData[i + 1], - GraphicData[i + 2] - ]; - worldCoords.push(point); - } - - const state = defaultState; - - state.annotation.data = { - handles: { - points: [worldCoords[0], worldCoords[1], worldCoords[3]], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - }, - cachedStats: {} - }; - - return state; - } - - public static getTID300RepresentationArguments(tool, worldToImageCoords) { + public static getTID300RepresentationArguments( + tool, + is3DMeasurement = false + ) { const { data, finding, findingSites, metadata } = tool; const { cachedStats = {}, handles } = data; const { referencedImageId } = metadata; - - if (!referencedImageId) { - return this.getTID300RepresentationArgumentsSCOORD3D(tool); - } - - // Using image coordinates for 2D points - const start1 = worldToImageCoords(referencedImageId, handles.points[0]); - const middle = worldToImageCoords(referencedImageId, handles.points[1]); - const end = worldToImageCoords(referencedImageId, handles.points[2]); - - const point1 = { x: start1[0], y: start1[1] }; - const point2 = { x: middle[0], y: middle[1] }; - const point3 = point2; - const point4 = { x: end[0], y: end[1] }; - - const { angle } = cachedStats[`imageId:${referencedImageId}`] || {}; - - // Represented as a cobb angle - return { - point1, - point2, - point3, - point4, - rAngle: angle, - trackingIdentifierTextValue: this.trackingIdentifierTextValue, - finding, - findingSites: findingSites || [], - use3DSpatialCoordinates: false + const scoordProps = { + is3DMeasurement, + referencedImageId }; - } - public static getTID300RepresentationArgumentsSCOORD3D(tool) { - const { data, finding, findingSites, metadata } = tool; - const { cachedStats = {}, handles } = data; - - // Using world coordinates for 3D points - const start = handles.points[0]; - const middle = handles.points[1]; - const end = handles.points[2]; + // Do the conversion automatically for the right coord type + const point1 = toScoord(scoordProps, handles.points[0]); + const point2 = toScoord(scoordProps, handles.points[1]); + const point3 = toScoord(scoordProps, handles.points[1]); + const point4 = toScoord(scoordProps, handles.points[2]); - const point1 = { x: start[0], y: start[1], z: start[2] }; - const point2 = { x: middle[0], y: middle[1], z: middle[2] }; - const point3 = point2; - const point4 = { x: end[0], y: end[1], z: end[2] }; - - const cachedStatsKeys = Object.keys(cachedStats)[0]; - const { angle } = cachedStatsKeys ? cachedStats[cachedStatsKeys] : {}; + const angle = cachedStats[`imageId:${referencedImageId}`]?.angle; + // Represented as a cobb angle return { point1, point2, @@ -182,8 +83,10 @@ class Angle extends BaseAdapter3D { trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [], - ReferencedFrameOfReferenceUID: metadata.FrameOfReferenceUID, - use3DSpatialCoordinates: true + ReferencedFrameOfReferenceUID: is3DMeasurement + ? metadata.FrameOfReferenceUID + : null, + use3DSpatialCoordinates: is3DMeasurement }; } } diff --git a/packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.ts b/packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.ts index 08b70b8f77..94ae823386 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.ts @@ -1,9 +1,13 @@ -import MeasurementReport from "./MeasurementReport"; import { utilities } from "dcmjs"; +import { utilities as csUtilities } from "@cornerstonejs/core"; + +import MeasurementReport from "./MeasurementReport"; import BaseAdapter3D from "./BaseAdapter3D"; import CodingScheme from "./CodingScheme"; +import { toScoord } from "../helpers"; const { Point: TID300Point } = utilities.TID300; +const { imageToWorldCoords } = csUtilities; const { codeValues } = CodingScheme; class ArrowAnnotate extends BaseAdapter3D { @@ -15,107 +19,27 @@ class ArrowAnnotate extends BaseAdapter3D { static getMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata, _trackingIdentifier ) { const { - defaultState, + state, SCOORDGroup, - SCOORD3DGroup, + worldCoords, + referencedImageId, ReferencedFrameNumber } = MeasurementReport.getSetupMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, metadata, - ArrowAnnotate.toolType + this.toolType ); - const referencedImageId = - defaultState.annotation.metadata.referencedImageId; - - const text = defaultState.annotation.metadata.label; - - if (SCOORDGroup) { - return this.getMeasurementDataFromScoord({ - SCOORDGroup, - referencedImageId, - metadata, - imageToWorldCoords, - defaultState, - text, - ReferencedFrameNumber - }); - } else if (SCOORD3DGroup) { - return this.getMeasurementDataFromScoord3D({ - SCOORD3DGroup, - defaultState, - text - }); - } else { - throw new Error( - "Can't get measurement data with missing SCOORD and SCOORD3D groups." - ); - } - } - - static getMeasurementDataFromScoord3D({ - SCOORD3DGroup, - defaultState, - text - }) { - const { GraphicData } = SCOORD3DGroup; - - const worldCoords = []; - for (let i = 0; i < GraphicData.length; i += 3) { - const point = [ - GraphicData[i], - GraphicData[i + 1], - GraphicData[i + 2] - ]; - worldCoords.push(point); - } - - const state = defaultState; - - state.annotation.data = { - text, - handles: { - arrowFirst: true, - points: [worldCoords[0], worldCoords[1]], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - } - }; - - return state; - } - - static getMeasurementDataFromScoord({ - SCOORDGroup, - referencedImageId, - metadata, - imageToWorldCoords, - defaultState, - text, - ReferencedFrameNumber - }) { - const { GraphicData } = SCOORDGroup; - - const worldCoords = []; - for (let i = 0; i < GraphicData.length; i += 2) { - const point = imageToWorldCoords(referencedImageId, [ - GraphicData[i], - GraphicData[i + 1] - ]); - worldCoords.push(point); - } + const text = state.annotation.data.label; // Since the arrowAnnotate measurement is just a point, to generate the tool state // we derive the second point based on the image size relative to the first point. - if (worldCoords.length === 1) { + if (worldCoords.length === 1 && SCOORDGroup) { const imagePixelModule = metadata.get( "imagePixelModule", referencedImageId @@ -130,6 +54,7 @@ class ArrowAnnotate extends BaseAdapter3D { yOffset = rows / 10; } + const { GraphicData } = SCOORDGroup; const secondPoint = imageToWorldCoords(referencedImageId, [ GraphicData[0] + xOffset, GraphicData[1] + yOffset @@ -138,17 +63,13 @@ class ArrowAnnotate extends BaseAdapter3D { worldCoords.push(secondPoint); } - const state = defaultState; - state.annotation.data = { + ...state.annotation.data, text, handles: { + ...state.annotation.data.handles, arrowFirst: true, - points: [worldCoords[0], worldCoords[1]], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } + points: worldCoords }, frameNumber: ReferencedFrameNumber }; @@ -156,111 +77,35 @@ class ArrowAnnotate extends BaseAdapter3D { return state; } - static getTID300RepresentationArguments(tool, worldToImageCoords) { + static getTID300RepresentationArguments(tool, is3DMeasurement = false) { const { data, metadata, findingSites } = tool; - let { finding } = tool; + const { finding } = tool; const { referencedImageId } = metadata; - - if (!referencedImageId) { - return this.getTID300RepresentationArgumentsSCOORD3D(tool); - } - - const { points, arrowFirst } = data.handles; - - let point; - let point2; - - if (arrowFirst) { - point = points[0]; - point2 = points[1]; - } else { - point = points[1]; - point2 = points[0]; - } - - // Using image coordinates for 2D points - const pointImage = worldToImageCoords(referencedImageId, point); - const pointImage2 = worldToImageCoords(referencedImageId, point2); - - const TID300RepresentationArguments = { - points: [ - { - x: pointImage[0], - y: pointImage[1] - }, - { - x: pointImage2[0], - y: pointImage2[1] - } - ], - trackingIdentifierTextValue: this.trackingIdentifierTextValue, - findingSites: findingSites || [], - finding, - use3DSpatialCoordinates: false + const scoordProps = { + is3DMeasurement, + referencedImageId }; - // If freetext finding isn't present, add it from the tool text. - if (!finding || finding.CodeValue !== codeValues.CORNERSTONEFREETEXT) { - finding = { - CodeValue: codeValues.CORNERSTONEFREETEXT, - CodingSchemeDesignator: CodingScheme.CodingSchemeDesignator, - CodeMeaning: data.text - }; - } - - return TID300RepresentationArguments; - } - - static getTID300RepresentationArgumentsSCOORD3D(tool) { - const { data, findingSites, metadata } = tool; - let { finding } = tool; - const { points, arrowFirst } = data.handles; - let point; - let point2; - - if (arrowFirst) { - point = points[0]; - point2 = points[1]; - } else { - point = points[1]; - point2 = points[0]; - } + const point = arrowFirst ? points[0] : points[1]; + const point2 = arrowFirst ? points[1] : points[0]; - // Using world coordinates for 3D points - const pointImage = point; - const pointImage2 = point2; + // Using image coordinates for 2D points + const pointImage = toScoord(scoordProps, point); + const pointImage2 = toScoord(scoordProps, point2); const TID300RepresentationArguments = { - points: [ - { - x: pointImage[0], - y: pointImage[1], - z: pointImage[2] - }, - { - x: pointImage2[0], - y: pointImage2[1], - z: pointImage2[2] - } - ], + points: [pointImage, pointImage2], trackingIdentifierTextValue: this.trackingIdentifierTextValue, findingSites: findingSites || [], finding, - ReferencedFrameOfReferenceUID: metadata.FrameOfReferenceUID, - use3DSpatialCoordinates: true + ReferencedFrameOfReferenceUID: is3DMeasurement + ? metadata.FrameOfReferenceUID + : null, + use3DSpatialCoordinates: is3DMeasurement }; - // If freetext finding isn't present, add it from the tool text. - if (!finding || finding.CodeValue !== codeValues.CORNERSTONEFREETEXT) { - finding = { - CodeValue: codeValues.CORNERSTONEFREETEXT, - CodingSchemeDesignator: CodingScheme.CodingSchemeDesignator, - CodeMeaning: data.text - }; - } - return TID300RepresentationArguments; } } diff --git a/packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts b/packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts index 0fc557e532..37371de70a 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts @@ -3,6 +3,7 @@ import MeasurementReport, { type AdapterOptions, type MeasurementAdapter } from "./MeasurementReport"; +import { toScoords } from "../helpers"; export type Point = { x: number; @@ -40,6 +41,56 @@ export default class BaseAdapter3D { */ public static parentType: string; + public static registerType(code = "", type = "", count = 0) { + let key = code; + if (type) { + key = `${key}${key.length ? "-" : ""}${type}`; + } + if (count) { + key = `${key}${key.length ? "-" : ""}${count}`; + } + MeasurementReport.registerAdapterTypes(this, key); + } + + public static getPointsCount(graphicItem) { + const is3DMeasurement = graphicItem.ValueType === "SCOORD3D"; + const pointSize = is3DMeasurement ? 3 : 2; + return graphicItem.GraphicData.length / pointSize; + } + + public static getGraphicItems(measurementGroup, filter) { + const items = measurementGroup.ContentSequence.filter( + group => + group.ValueType === "SCOORD" || group.ValueType === "SCOORD3D" + ); + return filter ? items.filter(filter) : items; + } + + public static getGraphicItem(measurementGroup, offset = 0, type = null) { + const items = this.getGraphicItems( + measurementGroup, + type && (group => group.ValueType === type) + ); + return items[offset]; + } + + public static getGraphicCode(graphicItem) { + const { ConceptNameCodeSequence: conceptNameItem } = graphicItem; + const { + CodeValue: graphicValue, + CodingSchemeDesignator: graphicDesignator + } = conceptNameItem; + return `${graphicDesignator}:${graphicValue}`; + } + + public static getGraphicType(graphicItem) { + return graphicItem.GraphicType; + } + + public static isValidMeasurement(_measurementGroup) { + return false; + } + public static init( toolType: string, representation, @@ -119,7 +170,6 @@ export default class BaseAdapter3D { public static getMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, - _imageToWorldCoords, metadata, trackingIdentifier?: string ) { @@ -142,55 +192,29 @@ export default class BaseAdapter3D { public static getTID300RepresentationArguments( tool, - worldToImageCoords + is3DMeasurement = false ): TID300Arguments { - const { data, metadata } = tool; + const { metadata } = tool; const { finding, findingSites } = tool; const { referencedImageId } = metadata; - - if (!referencedImageId) { - return this.getTID300RepresentationArgumentsSCOORD3D(tool); - } - - const { - handles: { points = [] } - } = data; + const scoordProps = { + is3DMeasurement, + referencedImageId + }; // Using image coordinates for 2D points - const pointsImage = points.map(point => { - const pointImage = worldToImageCoords(referencedImageId, point); - return { - x: pointImage[0], - y: pointImage[1] - }; - }); + const pointsImage = toScoords(scoordProps, tool.data.handles.points); const tidArguments = { points: pointsImage, trackingIdentifierTextValue: this.trackingIdentifierTextValue, findingSites: findingSites || [], - finding + finding, + ReferencedFrameOfReferenceUID: is3DMeasurement + ? metadata.FrameOfReferenceUID + : null }; return tidArguments; } - - static getTID300RepresentationArgumentsSCOORD3D(tool) { - const { data, finding, findingSites } = tool; - const { - handles: { points = [] } - } = data; - - // Using world coordinates for 3D points - const point = points[0]; - - const pointXYZ = { x: point[0], y: point[1], z: point[2] }; - - return { - points: [pointXYZ], - trackingIdentifierTextValue: this.trackingIdentifierTextValue, - findingSites: findingSites || [], - finding - }; - } } diff --git a/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.ts b/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.ts index d101518cca..db81626f43 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.ts @@ -1,7 +1,6 @@ import { utilities } from "dcmjs"; -import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; import MeasurementReport from "./MeasurementReport"; -import { toArray } from "../helpers"; +import { scoordToWorld, toScoord, toArray } from "../helpers"; import BaseAdapter3D from "./BaseAdapter3D"; const { Bidirectional: TID300Bidirectional } = utilities.TID300; @@ -18,19 +17,16 @@ class Bidirectional extends BaseAdapter3D { public static getMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata ) { - const { defaultState, ReferencedFrameNumber } = + const { state, scoordArgs, referencedImageId, ReferencedFrameNumber } = MeasurementReport.getSetupMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, metadata, - Bidirectional.toolType + this.toolType ); - const referencedImageId = - defaultState.annotation.metadata.referencedImageId; const { ContentSequence } = MeasurementGroup; const longAxisNUMGroup = toArray(ContentSequence).find( @@ -40,138 +36,60 @@ class Bidirectional extends BaseAdapter3D { const shortAxisNUMGroup = toArray(ContentSequence).find( group => group.ConceptNameCodeSequence.CodeMeaning === SHORT_AXIS ); - - const longAxisSCOORDGroup = toArray( + const longAxisScoordGroup = toArray( longAxisNUMGroup.ContentSequence - ).find(group => group.ValueType === "SCOORD"); + ).find( + group => + group.ValueType === "SCOORD3D" || group.ValueType === "SCOORD" + ); - const shortAxisSCOORDGroup = toArray( + const shortAxisScoordGroup = toArray( shortAxisNUMGroup.ContentSequence - ).find(group => group.ValueType === "SCOORD"); - - if (longAxisSCOORDGroup && shortAxisSCOORDGroup) { - return this.getMeasurementDataFromScoord({ - longAxisNUMGroup, - shortAxisNUMGroup, - longAxisSCOORDGroup, - shortAxisSCOORDGroup, - referencedImageId, - imageToWorldCoords, - ReferencedFrameNumber, - defaultState - }); - } else { - return this.getMeasurementDataFromScoord3d({ - longAxisNUMGroup, - shortAxisNUMGroup, - defaultState - }); - } - } + ).find( + group => + group.ValueType === "SCOORD3D" || group.ValueType === "SCOORD" + ); - static getMeasurementDataFromScoord({ - longAxisNUMGroup, - shortAxisNUMGroup, - longAxisSCOORDGroup, - shortAxisSCOORDGroup, - referencedImageId, - imageToWorldCoords, - ReferencedFrameNumber, - defaultState - }) { const worldCoords = []; - [longAxisSCOORDGroup, shortAxisSCOORDGroup].forEach(group => { - const { GraphicData } = group; - for (let i = 0; i < GraphicData.length; i += 2) { - const point = imageToWorldCoords(referencedImageId, [ - GraphicData[i], - GraphicData[i + 1] - ]); - worldCoords.push(point); - } - }); - - const state = defaultState; + worldCoords.push(...scoordToWorld(scoordArgs, longAxisScoordGroup)); + worldCoords.push(...scoordToWorld(scoordArgs, shortAxisScoordGroup)); state.annotation.data = { + ...state.annotation.data, handles: { + ...state.annotation.data.handles, points: [ worldCoords[0], worldCoords[1], worldCoords[2], worldCoords[3] - ], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - }, - cachedStats: { - [`imageId:${referencedImageId}`]: { - length: longAxisNUMGroup.MeasuredValueSequence.NumericValue, - width: shortAxisNUMGroup.MeasuredValueSequence.NumericValue - } + ] }, frameNumber: ReferencedFrameNumber }; - return state; - } - - static getMeasurementDataFromScoord3d({ - longAxisNUMGroup, - shortAxisNUMGroup, - defaultState - }) { - const worldCoords = []; - const longAxisSCOORD3DGroup = toArray( - longAxisNUMGroup.ContentSequence - ).find(group => group.ValueType === "SCOORD3D"); - - const shortAxisSCOORD3DGroup = toArray( - shortAxisNUMGroup.ContentSequence - ).find(group => group.ValueType === "SCOORD3D"); - - [longAxisSCOORD3DGroup, shortAxisSCOORD3DGroup].forEach(group => { - const { GraphicData } = group; - for (let i = 0; i < GraphicData.length; i += 3) { - const point = [ - GraphicData[i], - GraphicData[i + 1], - GraphicData[i + 2] - ]; - worldCoords.push(point); - } - }); - - const state = defaultState; - - state.annotation.data = { - handles: { - points: [ - worldCoords[0], - worldCoords[1], - worldCoords[2], - worldCoords[3] - ], - activeHandleIndex: 0, - textBox: { - hasMoved: false + if (referencedImageId) { + state.annotation.data.cachedStats = { + [`imageId:${referencedImageId}`]: { + length: longAxisNUMGroup.MeasuredValueSequence.NumericValue, + width: shortAxisNUMGroup.MeasuredValueSequence.NumericValue } - }, - cachedStats: {} - }; + }; + } return state; } - static getTID300RepresentationArguments(tool, worldToImageCoords) { + static getTID300RepresentationArguments(tool, is3DMeasurement = false) { const { data, finding, findingSites, metadata } = tool; const { cachedStats = {}, handles } = data; const { referencedImageId } = metadata; - + const scoordProps = { + is3DMeasurement, + referencedImageId + }; const { points } = handles; // Find the length and width point pairs by comparing the distances of the points at 0,1 to points at 2,3 @@ -200,116 +118,33 @@ class Bidirectional extends BaseAdapter3D { longAxisPoints = firstPointPairs; } - if (!referencedImageId) { - return this.getTID300RepresentationArgumentsSCOORD3D({ - tool, - shortAxisPoints, - longAxisPoints - }); - } - // Using image coordinates for 2D points - const longAxisStartImage = worldToImageCoords( - referencedImageId, - shortAxisPoints[0] - ); - const longAxisEndImage = worldToImageCoords( - referencedImageId, - shortAxisPoints[1] - ); - const shortAxisStartImage = worldToImageCoords( - referencedImageId, - longAxisPoints[0] - ); - const shortAxisEndImage = worldToImageCoords( - referencedImageId, - longAxisPoints[1] - ); + const longAxisStartImage = toScoord(scoordProps, shortAxisPoints[0]); + const longAxisEndImage = toScoord(scoordProps, shortAxisPoints[1]); + const shortAxisStartImage = toScoord(scoordProps, longAxisPoints[0]); + const shortAxisEndImage = toScoord(scoordProps, longAxisPoints[1]); const { length, width } = cachedStats[`imageId:${referencedImageId}`] || {}; return { longAxis: { - point1: { - x: longAxisStartImage[0], - y: longAxisStartImage[1] - }, - point2: { - x: longAxisEndImage[0], - y: longAxisEndImage[1] - } + point1: longAxisStartImage, + point2: longAxisEndImage }, shortAxis: { - point1: { - x: shortAxisStartImage[0], - y: shortAxisStartImage[1] - }, - point2: { - x: shortAxisEndImage[0], - y: shortAxisEndImage[1] - } - }, - longAxisLength: length, - shortAxisLength: width, - trackingIdentifierTextValue: this.trackingIdentifierTextValue, - finding: finding, - findingSites: findingSites || [], - use3DSpatialCoordinates: false - }; - } - - static getTID300RepresentationArgumentsSCOORD3D({ - tool, - shortAxisPoints, - longAxisPoints - }) { - const { data, finding, findingSites, metadata } = tool; - const { cachedStats = {} } = data; - - // Using world coordinates for 3D points - const longAxisStart = shortAxisPoints[0]; - const longAxisEnd = shortAxisPoints[1]; - const shortAxisStart = longAxisPoints[0]; - const shortAxisEnd = longAxisPoints[1]; - - const cachedStatsKeys = Object.keys(cachedStats)[0]; - const { length, width } = cachedStatsKeys - ? cachedStats[cachedStatsKeys] - : {}; - - return { - longAxis: { - point1: { - x: longAxisStart[0], - y: longAxisStart[1], - z: longAxisStart[2] - }, - point2: { - x: longAxisEnd[0], - y: longAxisEnd[1], - z: longAxisEnd[2] - } - }, - shortAxis: { - point1: { - x: shortAxisStart[0], - y: shortAxisStart[1], - z: shortAxisStart[2] - }, - point2: { - x: shortAxisEnd[0], - y: shortAxisEnd[1], - z: shortAxisEnd[2] - } + point1: shortAxisStartImage, + point2: shortAxisEndImage }, longAxisLength: length, shortAxisLength: width, trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding: finding, findingSites: findingSites || [], - ReferencedFrameOfReferenceUID: metadata.FrameOfReferenceUID, - use3DSpatialCoordinates: true + ReferencedFrameOfReferenceUID: is3DMeasurement + ? metadata.FrameOfReferenceUID + : null, + use3DSpatialCoordinates: is3DMeasurement }; } } diff --git a/packages/adapters/src/adapters/Cornerstone3D/CircleROI.ts b/packages/adapters/src/adapters/Cornerstone3D/CircleROI.ts index 2944412432..3487c0bf87 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/CircleROI.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/CircleROI.ts @@ -1,6 +1,7 @@ import { utilities } from "dcmjs"; import MeasurementReport from "./MeasurementReport"; import BaseAdapter3D from "./BaseAdapter3D"; +import { toScoord } from "../helpers"; const { Circle: TID300Circle } = utilities.TID300; @@ -14,76 +15,31 @@ class CircleROI extends BaseAdapter3D { static getMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata ) { const { - defaultState, + state, NUMGroup, - SCOORDGroup, - SCOORD3DGroup, + worldCoords, + referencedImageId, ReferencedFrameNumber } = MeasurementReport.getSetupMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, metadata, - CircleROI.toolType + this.toolType ); - if (SCOORDGroup) { - return this.getMeasurementDataFromScoord({ - defaultState, - SCOORDGroup, - imageToWorldCoords, - NUMGroup, - ReferencedFrameNumber - }); - } else if (SCOORD3DGroup) { - return this.getMeasurementDataFromScoord3D({ - defaultState, - SCOORD3DGroup - }); - } else { - throw new Error( - "Can't get measurement data with missing SCOORD and SCOORD3D groups." - ); - } - } - - static getMeasurementDataFromScoord({ - defaultState, - SCOORDGroup, - imageToWorldCoords, - NUMGroup, - ReferencedFrameNumber - }) { - const referencedImageId = - defaultState.annotation.metadata.referencedImageId; - - const { GraphicData } = SCOORDGroup; - - // GraphicData is ordered as [centerX, centerY, endX, endY] - const pointsWorld = []; - for (let i = 0; i < GraphicData.length; i += 2) { - const worldPos = imageToWorldCoords(referencedImageId, [ - GraphicData[i], - GraphicData[i + 1] - ]); - - pointsWorld.push(worldPos); - } - - const state = defaultState; - state.annotation.data = { + ...state.annotation.data, handles: { - points: [...pointsWorld], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } + ...state.annotation.data.handles, + points: worldCoords }, - cachedStats: { + frameNumber: ReferencedFrameNumber + }; + if (referencedImageId) { + state.annotation.data.cachedStats = { [`imageId:${referencedImageId}`]: { area: NUMGroup ? NUMGroup.MeasuredValueSequence.NumericValue @@ -92,41 +48,9 @@ class CircleROI extends BaseAdapter3D { radius: 0, perimeter: 0 } - }, - frameNumber: ReferencedFrameNumber - }; - - return state; - } - - static getMeasurementDataFromScoord3D({ defaultState, SCOORD3DGroup }) { - const { GraphicData } = SCOORD3DGroup; - - // GraphicData is ordered as [centerX, centerY, endX, endY] - const pointsWorld = []; - for (let i = 0; i < GraphicData.length; i += 3) { - const worldPos = [ - GraphicData[i], - GraphicData[i + 1], - GraphicData[i + 2] - ]; - - pointsWorld.push(worldPos); + }; } - const state = defaultState; - - state.annotation.data = { - handles: { - points: [...pointsWorld], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - }, - cachedStats: {} - }; - return state; } @@ -136,23 +60,19 @@ class CircleROI extends BaseAdapter3D { * @param {Object} tool * @returns */ - static getTID300RepresentationArguments(tool, worldToImageCoords) { + static getTID300RepresentationArguments(tool, is3DMeasurement = false) { const { data, finding, findingSites, metadata } = tool; const { cachedStats = {}, handles } = data; const { referencedImageId } = metadata; - - if (!referencedImageId) { - return this.getTID300RepresentationArgumentsSCOORD3D(tool); - } + const scoordProps = { + is3DMeasurement, + referencedImageId + }; // Using image coordinates for 2D points - const center = worldToImageCoords(referencedImageId, handles.points[0]); - const end = worldToImageCoords(referencedImageId, handles.points[1]); - - const points = []; - points.push({ x: center[0], y: center[1] }); - points.push({ x: end[0], y: end[1] }); + const center = toScoord(scoordProps, handles.points[0]); + const end = toScoord(scoordProps, handles.points[1]); const { area, radius } = cachedStats[`imageId:${referencedImageId}`] || {}; @@ -162,42 +82,14 @@ class CircleROI extends BaseAdapter3D { area, perimeter, radius, - points, - trackingIdentifierTextValue: this.trackingIdentifierTextValue, - finding, - findingSites: findingSites || [], - use3DSpatialCoordinates: false - }; - } - - static getTID300RepresentationArgumentsSCOORD3D(tool) { - const { data, finding, findingSites, metadata } = tool; - const { cachedStats = {}, handles } = data; - - // Using world coordinates for 3D points - const center = handles.points[0]; - const end = handles.points[1]; - - const points = []; - points.push({ x: center[0], y: center[1], z: center[2] }); - points.push({ x: end[0], y: end[1], z: center[2] }); - - const cachedStatsKeys = Object.keys(cachedStats)[0]; - const { area, radius } = cachedStatsKeys - ? cachedStats[cachedStatsKeys] - : {}; - const perimeter = 2 * Math.PI * radius; - - return { - area, - perimeter, - radius, - points, + points: [center, end], trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [], - ReferencedFrameOfReferenceUID: metadata.FrameOfReferenceUID, - use3DSpatialCoordinates: true + ReferencedFrameOfReferenceUID: is3DMeasurement + ? metadata.FrameOfReferenceUID + : null, + use3DSpatialCoordinates: is3DMeasurement }; } } diff --git a/packages/adapters/src/adapters/Cornerstone3D/CobbAngle.ts b/packages/adapters/src/adapters/Cornerstone3D/CobbAngle.ts index f9097a442c..511eb26395 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/CobbAngle.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/CobbAngle.ts @@ -1,6 +1,7 @@ import { utilities } from "dcmjs"; import MeasurementReport from "./MeasurementReport"; import BaseAdapter3D from "./BaseAdapter3D"; +import { toScoords } from "../helpers"; const { CobbAngle: TID300CobbAngle } = utilities.TID300; @@ -15,14 +16,13 @@ class CobbAngle extends BaseAdapter3D { public static getMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata ) { const { - defaultState, + state, NUMGroup, - SCOORDGroup, - SCOORD3DGroup, + referencedImageId, + worldCoords, ReferencedFrameNumber } = MeasurementReport.getSetupMeasurementData( MeasurementGroup, @@ -31,127 +31,48 @@ class CobbAngle extends BaseAdapter3D { CobbAngle.toolType ); - if (SCOORDGroup) { - return this.getMeasurementDataFromScoord({ - defaultState, - SCOORDGroup, - imageToWorldCoords, - NUMGroup, - ReferencedFrameNumber - }); - } else if (SCOORD3DGroup) { - return this.getMeasurementDataFromScoord3D({ - defaultState, - SCOORD3DGroup - }); - } else { - throw new Error( - "Can't get measurement data with missing SCOORD and SCOORD3D groups." - ); - } - } - - static getMeasurementDataFromScoord({ - defaultState, - SCOORDGroup, - imageToWorldCoords, - NUMGroup, - ReferencedFrameNumber - }) { - const referencedImageId = - defaultState.annotation.metadata.referencedImageId; - - const { GraphicData } = SCOORDGroup; - const worldCoords = []; - for (let i = 0; i < GraphicData.length; i += 2) { - const point = imageToWorldCoords(referencedImageId, [ - GraphicData[i], - GraphicData[i + 1] - ]); - worldCoords.push(point); - } - - const state = defaultState; - state.annotation.data = { + ...state.annotation.data, handles: { + ...state.annotation.data.handles, points: [ worldCoords[0], worldCoords[1], worldCoords[2], worldCoords[3] - ], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } + ] }, - cachedStats: { + frameNumber: ReferencedFrameNumber + }; + if (referencedImageId) { + state.annotation.data.cachedStats = { [`imageId:${referencedImageId}`]: { angle: NUMGroup ? NUMGroup.MeasuredValueSequence.NumericValue : null } - }, - frameNumber: ReferencedFrameNumber - }; - - return state; - } - - static getMeasurementDataFromScoord3D({ defaultState, SCOORD3DGroup }) { - const { GraphicData } = SCOORD3DGroup; - const worldCoords = []; - for (let i = 0; i < GraphicData.length; i += 3) { - const point = [ - GraphicData[i], - GraphicData[i + 1], - GraphicData[i + 2] - ]; - worldCoords.push(point); + }; } - const state = defaultState; - - state.annotation.data = { - handles: { - points: [ - worldCoords[0], - worldCoords[1], - worldCoords[2], - worldCoords[3] - ], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - }, - cachedStats: {} - }; - return state; } - public static getTID300RepresentationArguments(tool, worldToImageCoords) { + public static getTID300RepresentationArguments( + tool, + is3DMeasurement = false + ) { const { data, finding, findingSites, metadata } = tool; const { cachedStats = {}, handles } = data; const { referencedImageId } = metadata; - - if (!referencedImageId) { - return this.getTID300RepresentationArgumentsSCOORD3D(tool); - } + const scoordProps = { + is3DMeasurement, + referencedImageId + }; + const points = toScoords(scoordProps, handles.points); // Using image coordinates for 2D points - const start1 = worldToImageCoords(referencedImageId, handles.points[0]); - const end1 = worldToImageCoords(referencedImageId, handles.points[1]); - const start2 = worldToImageCoords(referencedImageId, handles.points[2]); - const end2 = worldToImageCoords(referencedImageId, handles.points[3]); - - const point1 = { x: start1[0], y: start1[1] }; - const point2 = { x: end1[0], y: end1[1] }; - const point3 = { x: start2[0], y: start2[1] }; - const point4 = { x: end2[0], y: end2[1] }; + const [point1, point2, point3, point4] = points; const { angle } = cachedStats[`imageId:${referencedImageId}`] || {}; @@ -164,39 +85,10 @@ class CobbAngle extends BaseAdapter3D { trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [], - use3DSpatialCoordinates: false - }; - } - - public static getTID300RepresentationArgumentsSCOORD3D(tool) { - const { data, finding, findingSites, metadata } = tool; - const { cachedStats = {}, handles } = data; - - // Using world coordinates for 3D points - const start1 = handles.points[0]; - const end1 = handles.points[1]; - const start2 = handles.points[2]; - const end2 = handles.points[3]; - - const point1 = { x: start1[0], y: start1[1], z: start1[2] }; - const point2 = { x: end1[0], y: end1[1], z: end1[2] }; - const point3 = { x: start2[0], y: start2[1], z: start2[2] }; - const point4 = { x: end2[0], y: end2[1], z: end2[2] }; - - const cachedStatsKeys = Object.keys(cachedStats)[0]; - const { angle } = cachedStatsKeys ? cachedStats[cachedStatsKeys] : {}; - - return { - point1, - point2, - point3, - point4, - rAngle: angle, - trackingIdentifierTextValue: this.trackingIdentifierTextValue, - finding, - findingSites: findingSites || [], - ReferencedFrameOfReferenceUID: metadata.FrameOfReferenceUID, - use3DSpatialCoordinates: true + ReferencedFrameOfReferenceUID: is3DMeasurement + ? metadata.FrameOfReferenceUID + : null, + use3DSpatialCoordinates: is3DMeasurement }; } } diff --git a/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.ts b/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.ts index 7911f983bd..795fbe3307 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.ts @@ -1,9 +1,9 @@ import { vec3 } from "gl-matrix"; import { utilities } from "dcmjs"; + import MeasurementReport from "./MeasurementReport"; import BaseAdapter3D from "./BaseAdapter3D"; - -type Point3 = [number, number, number]; +import { toScoord } from "../helpers"; const { Ellipse: TID300Ellipse } = utilities.TID300; @@ -17,14 +17,13 @@ class EllipticalROI extends BaseAdapter3D { static getMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata ) { const { - defaultState, + state, NUMGroup, - SCOORDGroup, - SCOORD3DGroup, + worldCoords, + referencedImageId, ReferencedFrameNumber } = MeasurementReport.getSetupMeasurementData( MeasurementGroup, @@ -33,264 +32,39 @@ class EllipticalROI extends BaseAdapter3D { EllipticalROI.toolType ); - if (SCOORDGroup) { - return this.getMeasurementDataFromScoord({ - defaultState, - SCOORDGroup, - imageToWorldCoords, - metadata, - NUMGroup, - ReferencedFrameNumber - }); - } else if (SCOORD3DGroup) { - return this.getMeasurementDataFromScoord3D({ - defaultState, - SCOORD3DGroup - }); - } else { - throw new Error( - "Can't get measurement data with missing SCOORD and SCOORD3D groups." - ); - } - } - - static getMeasurementDataFromScoord({ - defaultState, - SCOORDGroup, - imageToWorldCoords, - metadata, - NUMGroup, - ReferencedFrameNumber - }) { - const referencedImageId = - defaultState.annotation.metadata.referencedImageId; - - const { GraphicData } = SCOORDGroup; - - // GraphicData is ordered as [majorAxisStartX, majorAxisStartY, majorAxisEndX, majorAxisEndY, minorAxisStartX, minorAxisStartY, minorAxisEndX, minorAxisEndY] - // But Cornerstone3D points are ordered as top, bottom, left, right for the - // ellipse so we need to identify if the majorAxis is horizontal or vertical - // in the image plane and then choose the correct points to use for the ellipse. - const pointsWorld: Point3[] = []; - for (let i = 0; i < GraphicData.length; i += 2) { - const worldPos = imageToWorldCoords(referencedImageId, [ - GraphicData[i], - GraphicData[i + 1] - ]); - - pointsWorld.push(worldPos); - } - - const majorAxisStart = vec3.fromValues(...pointsWorld[0]); - const majorAxisEnd = vec3.fromValues(...pointsWorld[1]); - const minorAxisStart = vec3.fromValues(...pointsWorld[2]); - const minorAxisEnd = vec3.fromValues(...pointsWorld[3]); - - const majorAxisVec = vec3.create(); - vec3.sub(majorAxisVec, majorAxisEnd, majorAxisStart); - - // normalize majorAxisVec to avoid scaling issues - vec3.normalize(majorAxisVec, majorAxisVec); - - const minorAxisVec = vec3.create(); - vec3.sub(minorAxisVec, minorAxisEnd, minorAxisStart); - vec3.normalize(minorAxisVec, minorAxisVec); - - const imagePlaneModule = metadata.get( - "imagePlaneModule", - referencedImageId - ); - - if (!imagePlaneModule) { - throw new Error("imageId does not have imagePlaneModule metadata"); - } - - const { columnCosines } = imagePlaneModule; - - // find which axis is parallel to the columnCosines - const columnCosinesVec = vec3.fromValues( - columnCosines[0], - columnCosines[1], - columnCosines[2] - ); - const projectedMajorAxisOnColVec = vec3.dot( - columnCosinesVec, - majorAxisVec - ); - - const projectedMinorAxisOnColVec = vec3.dot( - columnCosinesVec, - minorAxisVec - ); - - const absoluteOfMajorDotProduct = Math.abs(projectedMajorAxisOnColVec); - const absoluteOfMinorDotProduct = Math.abs(projectedMinorAxisOnColVec); - - let ellipsePoints = []; - if (Math.abs(absoluteOfMajorDotProduct - 1) < EPSILON) { - ellipsePoints = [ - pointsWorld[0], - pointsWorld[1], - pointsWorld[2], - pointsWorld[3] - ]; - } else if (Math.abs(absoluteOfMinorDotProduct - 1) < EPSILON) { - ellipsePoints = [ - pointsWorld[2], - pointsWorld[3], - pointsWorld[0], - pointsWorld[1] - ]; - } else { - console.warn("OBLIQUE ELLIPSE NOT YET SUPPORTED"); - } - - const state = defaultState; - state.annotation.data = { + ...state.annotation.data, handles: { - points: [...ellipsePoints], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - }, - cachedStats: { - [`imageId:${referencedImageId}`]: { - area: NUMGroup - ? NUMGroup.MeasuredValueSequence.NumericValue - : 0 - } + ...state.annotation.data.handles, + points: worldCoords }, frameNumber: ReferencedFrameNumber }; + state.annotation.data.cachedStats = referencedImageId + ? { + [`imageId:${referencedImageId}`]: { + area: NUMGroup + ? NUMGroup.MeasuredValueSequence.NumericValue + : 0 + } + } + : {}; return state; } - static getMeasurementDataFromScoord3D({ defaultState, SCOORD3DGroup }) { - const { GraphicData } = SCOORD3DGroup; - - // GraphicData is ordered as [majorAxisStartX, majorAxisStartY, majorAxisEndX, majorAxisEndY, minorAxisStartX, minorAxisStartY, minorAxisEndX, minorAxisEndY] - // But Cornerstone3D points are ordered as top, bottom, left, right for the - // ellipse so we need to identify if the majorAxis is horizontal or vertical - // in the image plane and then choose the correct points to use for the ellipse. - const pointsWorld: Point3[] = []; - for (let i = 0; i < GraphicData.length; i += 3) { - const worldPos = [ - GraphicData[i], - GraphicData[i + 1], - GraphicData[i + 2] - ]; - - pointsWorld.push(worldPos); - } - - const majorAxisStart = vec3.fromValues(...pointsWorld[0]); - const majorAxisEnd = vec3.fromValues(...pointsWorld[1]); - const minorAxisStart = vec3.fromValues(...pointsWorld[2]); - const minorAxisEnd = vec3.fromValues(...pointsWorld[3]); - - const majorAxisVec = vec3.create(); - vec3.sub(majorAxisVec, majorAxisEnd, majorAxisStart); - - // normalize majorAxisVec to avoid scaling issues - vec3.normalize(majorAxisVec, majorAxisVec); - - const minorAxisVec = vec3.create(); - vec3.sub(minorAxisVec, minorAxisEnd, minorAxisStart); - vec3.normalize(minorAxisVec, minorAxisVec); - - const state = defaultState; - - state.annotation.data = { - handles: { - points: [ - majorAxisStart, - majorAxisEnd, - minorAxisStart, - minorAxisEnd - ], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - }, - cachedStats: {} - }; - - return state; - } - - static getTID300RepresentationArguments(tool, worldToImageCoords) { + static getTID300RepresentationArguments(tool, is3DMeasurement = false) { const { data, finding, findingSites, metadata } = tool; const { cachedStats = {}, handles } = data; const rotation = data.initialRotation || 0; const { referencedImageId } = metadata; - - if (!referencedImageId) { - return this.getTID300RepresentationArgumentsSCOORD3D(tool); - } - let top, bottom, left, right; - - // Using image coordinates for 2D points - // this way when it's restored we can assume the initial rotation is 0. - if (rotation == 90 || rotation == 270) { - bottom = worldToImageCoords(referencedImageId, handles.points[2]); - top = worldToImageCoords(referencedImageId, handles.points[3]); - left = worldToImageCoords(referencedImageId, handles.points[0]); - right = worldToImageCoords(referencedImageId, handles.points[1]); - } else { - top = worldToImageCoords(referencedImageId, handles.points[0]); - bottom = worldToImageCoords(referencedImageId, handles.points[1]); - left = worldToImageCoords(referencedImageId, handles.points[2]); - right = worldToImageCoords(referencedImageId, handles.points[3]); - } - - // find the major axis and minor axis - const topBottomLength = Math.abs(top[1] - bottom[1]); - const leftRightLength = Math.abs(left[0] - right[0]); - - const points = []; - if (topBottomLength > leftRightLength) { - // major axis is bottom to top - points.push({ x: top[0], y: top[1] }); - points.push({ x: bottom[0], y: bottom[1] }); - - // minor axis is left to right - points.push({ x: left[0], y: left[1] }); - points.push({ x: right[0], y: right[1] }); - } else { - // major axis is left to right - points.push({ x: left[0], y: left[1] }); - points.push({ x: right[0], y: right[1] }); - - // minor axis is bottom to top - points.push({ x: top[0], y: top[1] }); - points.push({ x: bottom[0], y: bottom[1] }); - } - - const { area } = cachedStats[`imageId:${referencedImageId}`] || {}; - - return { - area, - points, - trackingIdentifierTextValue: this.trackingIdentifierTextValue, - finding, - findingSites: findingSites || [], - use3DSpatialCoordinates: false + const scoordProps = { + is3DMeasurement, + referencedImageId }; - } - - static getTID300RepresentationArgumentsSCOORD3D(tool) { - const { data, finding, findingSites, metadata } = tool; - const { cachedStats, handles } = data; - const rotation = data.initialRotation || 0; let top, bottom, left, right; - // Using world coordinates for 3D points - // this way when it's restored we can assume the initial rotation is 0. if (rotation == 90 || rotation == 270) { bottom = handles.points[2]; top = handles.points[3]; @@ -318,33 +92,28 @@ class EllipticalROI extends BaseAdapter3D { const points = []; if (topBottomLength > leftRightLength) { // major axis is bottom to top - points.push({ x: top[0], y: top[1], z: top[2] }); - points.push({ x: bottom[0], y: bottom[1], z: bottom[2] }); - - // minor axis is left to right - points.push({ x: left[0], y: left[1], z: left[2] }); - points.push({ x: right[0], y: right[1], z: right[2] }); + points.push(top, bottom, left, right); } else { // major axis is left to right - points.push({ x: left[0], y: left[1], z: left[2] }); - points.push({ x: right[0], y: right[1], z: right[2] }); - - // minor axis is bottom to top - points.push({ x: top[0], y: top[1], z: top[2] }); - points.push({ x: bottom[0], y: bottom[1], z: bottom[2] }); + points.push(left, right, top, bottom); } - const cachedStatsKeys = Object.keys(cachedStats)[0]; - const { area } = cachedStatsKeys ? cachedStats[cachedStatsKeys] : {}; + const { area } = cachedStats[`imageId:${referencedImageId}`] || {}; + + const convertedPoints = points.map(point => + toScoord(scoordProps, point) + ); return { area, - points, + points: convertedPoints, trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [], - ReferencedFrameOfReferenceUID: metadata.FrameOfReferenceUID, - use3DSpatialCoordinates: true + ReferencedFrameOfReferenceUID: is3DMeasurement + ? metadata.FrameOfReferenceUID + : null, + use3DSpatialCoordinates: is3DMeasurement }; } } diff --git a/packages/adapters/src/adapters/Cornerstone3D/KeyImage.ts b/packages/adapters/src/adapters/Cornerstone3D/KeyImage.ts index 57e59b20d4..7e09e377d0 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/KeyImage.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/KeyImage.ts @@ -13,14 +13,12 @@ export default class KeyImage extends Probe { static getMeasurementData( measurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata, trackingIdentifier ) { const baseData = super.getMeasurementData( measurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata, trackingIdentifier ); @@ -30,11 +28,8 @@ export default class KeyImage extends Probe { return baseData; } - public static getTID300RepresentationArguments(tool, worldToImageCoords) { - const tid300Arguments = super.getTID300RepresentationArguments( - tool, - worldToImageCoords - ); + public static getTID300RepresentationArguments(tool) { + const tid300Arguments = super.getTID300RepresentationArguments(tool); const { data } = tool; if (data.isPoint) { if (data.seriesLevel) { diff --git a/packages/adapters/src/adapters/Cornerstone3D/LabelData.ts b/packages/adapters/src/adapters/Cornerstone3D/LabelData.ts new file mode 100644 index 0000000000..111b9e7d05 --- /dev/null +++ b/packages/adapters/src/adapters/Cornerstone3D/LabelData.ts @@ -0,0 +1,125 @@ +import type { Types } from "@cornerstonejs/tools"; +import { COMMENT_CODE, TEXT_ANNOTATION_POSITION } from "./constants"; +import { toScoord } from "../helpers"; +import dcmjs from "dcmjs"; + +const { + sr: { valueTypes, coding } +} = dcmjs; + +const CORNERSTONEFREETEXT = "CORNERSTONEFREETEXT"; + +/** + * Wrap a dcmjs TID.1501 content item creator with a label text/position store + * enhancements. + * THis removes the incorrect label text encoding inside finding sites, and + * adds it as a qualitative evaluation comment entry + * As well, if the position is included, it will be encoded as an SCOORD entry + * tagged to a private text annotation position value. + */ +export default class LabelData { + protected tid300Item; + protected annotation: Types.Annotation; + public ReferencedSOPSequence; + + constructor(tid300Item, annotation: Types.Annotation) { + this.tid300Item = tid300Item; + this.annotation = annotation; + this.ReferencedSOPSequence = tid300Item.ReferencedSOPSequence; + } + + /** + * This adds the additional information from the annotation data for + * the label data. + */ + public contentItem() { + const contentEntries = this.tid300Item.contentItem(); + const { label, handles } = this.annotation.data; + + if (label) { + contentEntries.push(this.createQualitativeLabel(label)); + this.filterCornerstoneFreeText(contentEntries); + } + if (handles?.textBox?.hasMoved) { + contentEntries.push( + this.createQualitativeLabelPosition(this.annotation) + ); + } + return contentEntries; + } + + /** + * Remove the incorrect finding sites entry for the text label. + */ + public filterCornerstoneFreeText(contentEntries) { + for (let i = 0; i < contentEntries.length; i++) { + const group = contentEntries[i]; + if (!group.ConceptCodeSequence) { + continue; + } + const csLabel = group.ConceptCodeSequence.find( + item => item.CodeValue === CORNERSTONEFREETEXT + ); + if (csLabel !== -1) { + group.ConceptCodeSequence.splice(csLabel, 1); + if (group.ConceptCodeSequence.length === 0) { + contentEntries.splice(i, 1); + } + return; + } + } + } + + /** + * This is the standard TID.1501 method to create a qualitative label + * comment. It returns the DCM:121106 comment code with the text value + * contained within the text. This allows for comments of up to 2^32-4 + * characters + */ + public createQualitativeLabel(label: string) { + const relationshipType = valueTypes.RelationshipTypes.CONTAINS; + return new valueTypes.TextContentItem({ + name: new coding.CodedConcept(COMMENT_CODE), + relationshipType, + value: label + }); + } + + /** + * Creates a qualitative evaluation text label position as an SCOORD (3D) + * position labelled with the text annotation position indicator. + */ + public createQualitativeLabelPosition(annotation: Types.Annotation) { + const { textBox } = annotation.data.handles; + const { referencedImageId, FrameOfReferenceUID: frameOfReferenceUID } = + annotation.metadata; + const is3DMeasurement = !referencedImageId; + const { worldPosition } = textBox; + const { x, y, z } = toScoord( + { is3DMeasurement, referencedImageId }, + worldPosition + ); + const graphicType = valueTypes.GraphicTypes.POINT; + const relationshipType = valueTypes.RelationshipTypes.CONTAINS; + const name = new coding.CodedConcept(TEXT_ANNOTATION_POSITION); + + if (is3DMeasurement) { + const graphicData = [x, y, z]; + return new valueTypes.Scoord3DContentItem({ + name, + relationshipType, + graphicType, + frameOfReferenceUID, + graphicData + }); + } + + const graphicData = [x, y]; + return new valueTypes.ScoordContentItem({ + name, + relationshipType, + graphicType, + graphicData + }); + } +} diff --git a/packages/adapters/src/adapters/Cornerstone3D/Length.ts b/packages/adapters/src/adapters/Cornerstone3D/Length.ts index 994d11b642..49b68d797c 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Length.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Length.ts @@ -1,6 +1,7 @@ import { utilities } from "dcmjs"; import MeasurementReport from "./MeasurementReport"; import BaseAdapter3D from "./BaseAdapter3D"; +import { toScoord } from "../helpers"; const { Length: TID300Length } = utilities.TID300; @@ -13,81 +14,17 @@ export default class Length extends BaseAdapter3D { this.registerLegacy(); } - static getMeasurementDataFromScoord({ - defaultState, - NUMGroup, - SCOORDGroup, - ReferencedFrameNumber, - imageToWorldCoords - }) { - const referencedImageId = - defaultState.annotation.metadata.referencedImageId; - - const { GraphicData } = SCOORDGroup; - const worldCoords = []; - for (let i = 0; i < GraphicData.length; i += 2) { - const point = imageToWorldCoords(referencedImageId, [ - GraphicData[i], - GraphicData[i + 1] - ]); - worldCoords.push(point); - } - - const state = defaultState; - - state.annotation.data = { - handles: { - points: [worldCoords[0], worldCoords[1]], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - }, - cachedStats: { - [`imageId:${referencedImageId}`]: { - length: NUMGroup - ? NUMGroup.MeasuredValueSequence.NumericValue - : 0 - } - }, - frameNumber: ReferencedFrameNumber - }; - - return state; - } - - static getMeasurementDataFromScoord3d({ defaultState, SCOORD3DGroup }) { - const { GraphicData } = SCOORD3DGroup; - const worldCoords = GraphicData; - - const state = defaultState; - - state.annotation.data = { - handles: { - points: [worldCoords.slice(0, 3), worldCoords.slice(3, 6)], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - }, - cachedStats: {} - }; - - return state; - } - // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport. static getMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata ) { const { - defaultState, + state, NUMGroup, - SCOORDGroup, - SCOORD3DGroup, + worldCoords, + referencedImageId, ReferencedFrameNumber } = MeasurementReport.getSetupMeasurementData( MeasurementGroup, @@ -96,42 +33,42 @@ export default class Length extends BaseAdapter3D { this.toolType ); - if (SCOORDGroup) { - return this.getMeasurementDataFromScoord({ - defaultState, - NUMGroup, - SCOORDGroup, - ReferencedFrameNumber, - imageToWorldCoords - }); - } else if (SCOORD3DGroup) { - return this.getMeasurementDataFromScoord3d({ - defaultState, - SCOORD3DGroup - }); - } else { - throw new Error( - "Can't get measurement data with missing SCOORD and SCOORD3D groups." - ); - } + const cachedStats = referencedImageId + ? { + [`imageId:${referencedImageId}`]: { + length: NUMGroup + ? NUMGroup.MeasuredValueSequence.NumericValue + : 0 + } + } + : {}; + state.annotation.data = { + ...state.annotation.data, + handles: { + ...state.annotation.data.handles, + points: [worldCoords[0], worldCoords[1]], + activeHandleIndex: 0 + }, + cachedStats, + frameNumber: ReferencedFrameNumber + }; + + return state; } - static getTID300RepresentationArguments(tool, worldToImageCoords) { + static getTID300RepresentationArguments(tool, is3DMeasurement = false) { const { data, finding, findingSites, metadata } = tool; const { cachedStats = {}, handles } = data; const { referencedImageId } = metadata; + const scoordProps = { + is3DMeasurement, + referencedImageId + }; - if (!referencedImageId) { - return this.getTID300RepresentationArgumentsSCOORD3D(tool); - } - - // Using image coordinates for 2D points - const start = worldToImageCoords(referencedImageId, handles.points[0]); - const end = worldToImageCoords(referencedImageId, handles.points[1]); - - const point1 = { x: start[0], y: start[1] }; - const point2 = { x: end[0], y: end[1] }; + // Do the conversion automatically for hte right coord type + const point1 = toScoord(scoordProps, handles.points[0]); + const point2 = toScoord(scoordProps, handles.points[1]); const { length: distance } = cachedStats[`imageId:${referencedImageId}`] || {}; @@ -143,35 +80,7 @@ export default class Length extends BaseAdapter3D { trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [], - use3DSpatialCoordinates: false - }; - } - - static getTID300RepresentationArgumentsSCOORD3D(tool) { - const { data, finding, findingSites, metadata } = tool; - const { cachedStats = {}, handles } = data; - - // Using world coordinates for 3D points - const start = handles.points[0]; - const end = handles.points[1]; - - const point1 = { x: start[0], y: start[1], z: start[2] }; - const point2 = { x: end[0], y: end[1], z: end[2] }; - - const cachedStatsKeys = Object.keys(cachedStats)[0]; - const { length: distance } = cachedStatsKeys - ? cachedStats[cachedStatsKeys] - : {}; - - return { - point1, - point2, - distance, - trackingIdentifierTextValue: this.trackingIdentifierTextValue, - finding, - findingSites: findingSites || [], - ReferencedFrameOfReferenceUID: metadata.FrameOfReferenceUID, - use3DSpatialCoordinates: true + use3DSpatialCoordinates: is3DMeasurement }; } } diff --git a/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.ts b/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.ts index aaf10c6794..59e3c06349 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.ts @@ -1,13 +1,36 @@ -import { normalizers, data, utilities, derivations } from "dcmjs"; -import { cache } from "@cornerstonejs/core"; +import { + normalizers, + data, + utilities as dcmjsUtilities, + derivations +} from "dcmjs"; +import { + cache, + utilities as csUtilities, + type Types as CSTypes +} from "@cornerstonejs/core"; +import type { Types } from "@cornerstonejs/tools"; import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; -import { toArray, codeMeaningEquals, copyStudyTags } from "../helpers"; +import { + toArray, + codeMeaningEquals, + copyStudyTags, + scoordToWorld +} from "../helpers"; import Cornerstone3DCodingScheme from "./CodingScheme"; import { copySeriesTags } from "../helpers/copySeriesTags"; -import { NO_IMAGE_ID } from "./constants"; +import { toPoint3 } from "../helpers/toPoint3"; +import { + COMMENT_CODE, + NO_IMAGE_ID, + TEXT_ANNOTATION_POSITION +} from "./constants"; +import LabelData from "./LabelData"; -const { TID1500, addAccessors } = utilities; +type Annotation = Types.Annotation; + +const { TID1500, addAccessors } = dcmjsUtilities; const { StructuredReport } = derivations; @@ -18,33 +41,54 @@ const { TID1500MeasurementReport, TID1501MeasurementGroup } = TID1500; const { DicomMetaDictionary } = data; const FINDING = { CodingSchemeDesignator: "DCM", CodeValue: "121071" }; +const COMMENT = { + CodingSchemeDesignator: COMMENT_CODE.schemeDesignator, + CodeValue: COMMENT_CODE.value +}; +const COMMENT_POSITION = { + CodingSchemeDesignator: TEXT_ANNOTATION_POSITION.schemeDesignator, + CodeValue: TEXT_ANNOTATION_POSITION.value +}; + const FINDING_SITE = { CodingSchemeDesignator: "SCT", CodeValue: "363698007" }; const FINDING_SITE_OLD = { CodingSchemeDesignator: "SRT", CodeValue: "G-C0E3" }; type SpatialCoordinatesState = { description?: string; sopInstanceUid?: string; - annotation: { - annotationUID: string; - metadata: { - toolName: string; - referencedImageId?: string; - FrameOfReferenceUID: string; - label: string; - }; - }; + annotation: Annotation; finding?: unknown; findingSites?: unknown; + commentGroup?; + commentPositionGroup?; +}; + +type ScoordType = { + GraphicData: number[]; }; type SetupMeasurementData = { defaultState: SpatialCoordinatesState; - NUMGroup: Record; - SCOORDGroup?: Record; + state?: SpatialCoordinatesState; + is3DMeasurement?: boolean; + scoord?: ScoordType; + worldCoords?: CSTypes.Point3[]; + scoordArgs?: { + referencedImageId: string; + is3DMeasurement: boolean; + }; + NUMGroup: { + MeasuredValueSequence: { + NumericValue: number; + }; + }; + SCOORDGroup?: ScoordType; ReferencedSOPSequence?: Record; ReferencedSOPInstanceUID?: string; + referencedImageId?: string; + textBoxPosition?: ScoordType; ReferencedFrameNumber?: string; - SCOORD3DGroup?: Record; + SCOORD3DGroup?: ScoordType; FrameOfReferenceUID?: string; }; @@ -95,16 +139,17 @@ export interface MeasurementAdapter { getMeasurementData( measurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata, trackingIdentifier: string ); isValidCornerstoneTrackingIdentifier(trackingIdentifier: string): boolean; + isValidMeasurement(measurementGroup): boolean; + getTID300RepresentationArguments( tool, - worldToImageCoords + is3DMeasurement ): Record; } @@ -117,6 +162,12 @@ export default class MeasurementReport { MeasurementAdapter >(); + /** Maps tool type to the adapter name used to serialize this item to SR */ + public static measurementAdaptersByType = new Map< + string, + MeasurementAdapter[] + >(); + /** Maps tracking identifier to tool class to deserialize from SR into a tool instance */ public static measurementAdapterByTrackingIdentifier = new Map< string, @@ -127,16 +178,21 @@ export default class MeasurementReport { tool, ReferencedSOPSequence, toolClass, - worldToImageCoords + is3DMeasurement ) { const args = toolClass.getTID300RepresentationArguments( tool, - worldToImageCoords + is3DMeasurement ); args.ReferencedSOPSequence = ReferencedSOPSequence; + if (args.use3DSpatialCoordinates) { + args.ReferencedFrameOfReferenceUID = + tool.metadata.FrameOfReferenceUID; + } - const TID300Measurement = new toolClass.TID300Representation(args); - return TID300Measurement; + const tid300Measurement = new toolClass.TID300Representation(args); + const labelMeasurement = new LabelData(tid300Measurement, tool); + return labelMeasurement; } public static codeValueMatch = (group, code, oldCode?) => { @@ -158,7 +214,7 @@ export default class MeasurementReport { toolType, toolData, ReferencedSOPSequence, - worldToImageCoords + is3DMeasurement ) { const toolTypeData = toolData[toolType]; const toolClass = this.measurementAdapterByToolType.get(toolType); @@ -178,7 +234,7 @@ export default class MeasurementReport { tool, ReferencedSOPSequence, toolClass, - worldToImageCoords + is3DMeasurement ); }); @@ -186,7 +242,11 @@ export default class MeasurementReport { } static getCornerstoneLabelFromDefaultState(defaultState) { - const { findingSites = [], finding } = defaultState; + const { findingSites = [], finding, commentGroup } = defaultState; + + if (commentGroup?.TextValue) { + return commentGroup.TextValue; + } const cornersoneFreeTextCodingValue = Cornerstone3DCodingScheme.codeValues.CORNERSTONEFREETEXT; @@ -247,7 +307,7 @@ export default class MeasurementReport { toolType, sopInstanceUIDToImageIdMap, metadata - }): SpatialCoordinatesData { + }) { const { ReferencedSOPSequence } = SCOORDGroup.ContentSequence; const { ReferencedSOPInstanceUID, ReferencedFrameNumber } = ReferencedSOPSequence; @@ -259,22 +319,33 @@ export default class MeasurementReport { referencedImageId ); + const annotationUID = DicomMetaDictionary.uid(); return { SCOORDGroup, ReferencedSOPSequence, ReferencedSOPInstanceUID, ReferencedFrameNumber, + referencedImageId, state: { description: undefined, sopInstanceUid: ReferencedSOPInstanceUID, annotation: { - annotationUID: DicomMetaDictionary.uid(), + data: { + annotationUID, + cachedStats: {}, + handles: { + activeHandleIndex: 0, + textBox: { + hasMoved: false + } + } + }, + annotationUID, metadata: { toolName: toolType, referencedImageId, FrameOfReferenceUID: - imagePlaneModule.frameOfReferenceUID, - label: "" + imagePlaneModule.frameOfReferenceUID } } } @@ -285,22 +356,38 @@ export default class MeasurementReport { SCOORD3DGroup, toolType }): SpatialCoordinatesData { - return { + const annotationUID = DicomMetaDictionary.uid(); + const toolData = { SCOORD3DGroup, FrameOfReferenceUID: SCOORD3DGroup.ReferencedFrameOfReferenceUID, state: { description: undefined, annotation: { - annotationUID: DicomMetaDictionary.uid(), + annotationUID, + data: { + annotationUID, + cachedStats: {}, + handles: { + activeHandleIndex: 0, + textBox: { + hasMoved: false + } + } + }, metadata: { toolName: toolType, FrameOfReferenceUID: - SCOORD3DGroup.ReferencedFrameOfReferenceUID, - label: "" + SCOORD3DGroup.ReferencedFrameOfReferenceUID } } } }; + csUtilities.updatePlaneRestriction( + toPoint3(SCOORD3DGroup.GraphicData), + toolData.state.annotation.metadata + ); + + return toolData; } public static getSpatialCoordinatesState({ @@ -309,25 +396,32 @@ export default class MeasurementReport { metadata, toolType }): SpatialCoordinatesData { - const SCOORDGroup = toArray(NUMGroup.ContentSequence).find( + const contentSequenceArr = toArray(NUMGroup.ContentSequence); + const SCOORDGroup = contentSequenceArr.find( group => group.ValueType === "SCOORD" ); - const SCOORD3DGroup = toArray(NUMGroup.ContentSequence).find( + const SCOORD3DGroup = contentSequenceArr.find( group => group.ValueType === "SCOORD3D" ); - if (SCOORDGroup) { - return this.processSCOORDGroup({ - SCOORDGroup, - toolType, - metadata, - sopInstanceUIDToImageIdMap - }); - } else if (SCOORD3DGroup) { - return this.processSCOORD3DGroup({ SCOORD3DGroup, toolType }); - } else { + const result: SpatialCoordinatesData = + (SCOORD3DGroup && + this.processSCOORD3DGroup({ + SCOORD3DGroup, + toolType + })) || + (SCOORDGroup && + this.processSCOORDGroup({ + SCOORDGroup, + toolType, + metadata, + sopInstanceUIDToImageIdMap + })); + if (!result) { throw new Error("No spatial coordinates group found."); } + + return result; } public static processSpatialCoordinatesGroup({ @@ -336,6 +430,8 @@ export default class MeasurementReport { metadata, findingGroup, findingSiteGroups, + commentGroup, + commentPositionGroup, toolType }) { const { @@ -345,7 +441,9 @@ export default class MeasurementReport { ReferencedSOPInstanceUID, ReferencedFrameNumber, SCOORD3DGroup, - FrameOfReferenceUID + FrameOfReferenceUID, + referencedImageId, + textBoxPosition } = this.getSpatialCoordinatesState({ NUMGroup, sopInstanceUIDToImageIdMap, @@ -360,25 +458,45 @@ export default class MeasurementReport { return addAccessors(fsg.ConceptCodeSequence); }); - const defaultState = { - ...state, - finding, - findingSites - }; + if (commentPositionGroup) { + state.commentPositionGroup = commentPositionGroup; + const textBoxCoords = scoordToWorld( + { + is3DMeasurement: !referencedImageId, + referencedImageId + }, + commentPositionGroup + ); + state.annotation.data.handles.textBox = { + hasMoved: true, + worldPosition: textBoxCoords[0] + }; + } + + state.finding = finding; + state.findingSites = findingSites; + state.commentGroup = commentGroup; + state.commentPositionGroup = commentPositionGroup; - if (defaultState.finding) { - defaultState.description = defaultState.finding.CodeMeaning; + if (finding) { + state.description = finding.CodeMeaning; } - defaultState.annotation.metadata.label = - MeasurementReport.getCornerstoneLabelFromDefaultState(defaultState); + state.annotation.data.label = + this.getCornerstoneLabelFromDefaultState(state); return { - defaultState, + // Deprecating the defaultState in favour of state, but there are lots + // of adapters still using defaultState + defaultState: state, + state, NUMGroup, + scoord: SCOORD3DGroup || SCOORDGroup, SCOORDGroup, ReferencedSOPSequence, ReferencedSOPInstanceUID, + referencedImageId, + textBoxPosition, ReferencedFrameNumber, SCOORD3DGroup, FrameOfReferenceUID @@ -397,22 +515,53 @@ export default class MeasurementReport { const findingGroup = contentSequenceArr.find(group => this.codeValueMatch(group, FINDING) ); + const commentGroup = contentSequenceArr.find(group => + this.codeValueMatch(group, COMMENT) + ); + const commentPositionGroup = contentSequenceArr.find(group => + this.codeValueMatch(group, COMMENT_POSITION) + ); const findingSiteGroups = contentSequenceArr.filter(group => this.codeValueMatch(group, FINDING_SITE, FINDING_SITE_OLD) ) || []; const NUMGroup = contentSequenceArr.find( group => group.ValueType === "NUM" - ); + ) || { + ContentSequence: contentSequenceArr.filter( + group => + group.ValueType === "SCOORD" || + group.ValueType === "SCOORD3D" + ) + }; - return this.processSpatialCoordinatesGroup({ + const spatialGroup = this.processSpatialCoordinatesGroup({ NUMGroup, sopInstanceUIDToImageIdMap, metadata, findingGroup, findingSiteGroups, + commentGroup, + commentPositionGroup, toolType }); + + const { referencedImageId } = spatialGroup.state.annotation.metadata; + const is3DMeasurement = !!spatialGroup.SCOORD3DGroup; + const scoordArgs = { + referencedImageId, + is3DMeasurement + }; + const scoord = spatialGroup.SCOORD3DGroup || spatialGroup.SCOORDGroup; + const worldCoords = scoordToWorld(scoordArgs, scoord); + + return { + ...spatialGroup, + is3DMeasurement, + scoordArgs, + scoord, + worldCoords + }; } static generateReferencedSOPSequence({ @@ -479,16 +628,14 @@ export default class MeasurementReport { const referenceToolData = toolData?.[toolTypes?.[0]]?.data?.[0]; const volumeId = referenceToolData?.metadata?.volumeId; const volume = cache.getVolume(volumeId); + if (!volume) { + throw new Error(`No volume found for ${volumeId}`); + } const imageId = volume.imageIds[0]; return imageId; } - static generateReport( - toolState, - metadataProvider, - worldToImageCoords, - options - ) { + static generateReport(toolState, metadataProvider, options) { // ToolState for array of imageIDs to a Report // Assume Cornerstone metadata provider has access to Study / Series / Sop Instance UID let allMeasurementGroups = []; @@ -510,6 +657,7 @@ export default class MeasurementReport { Object.keys(toolState).forEach(imageId => { const toolData = toolState[imageId]; const toolTypes = Object.keys(toolData); + const is3DMeasurement = imageId === NO_IMAGE_ID; const ReferencedSOPSequence = this.generateReferencedSOPSequence({ toolData, @@ -520,7 +668,7 @@ export default class MeasurementReport { derivationSourceDatasets }); - if (imageId === NO_IMAGE_ID) { + if (is3DMeasurement) { is3DSR = true; } @@ -532,7 +680,7 @@ export default class MeasurementReport { toolType, toolData, ReferencedSOPSequence, - worldToImageCoords + is3DMeasurement ); if (group) { measurementGroups.push(group); @@ -563,6 +711,13 @@ export default class MeasurementReport { if (is3DSR) { report.dataset.SOPClassUID = DicomMetaDictionary.sopClassUIDsByName.Comprehensive3DSR; + if (!report.dataset.SOPClassUID) { + throw new Error( + `NO sop class defined for Comprehensive3DSR in ${JSON.stringify( + DicomMetaDictionary.sopClassUIDsByName + )}` + ); + } } return report; @@ -574,7 +729,6 @@ export default class MeasurementReport { static generateToolState( dataset, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata, hooks ) { @@ -637,13 +791,13 @@ export default class MeasurementReport { ) || this.getAdapterForTrackingIdentifier( trackingIdentifierValue - ); + ) || + this.getAdapterForCodeType(measurementGroup); if (toolAdapter) { const measurement = toolAdapter.getMeasurementData( measurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata, trackingIdentifierValue ); @@ -734,4 +888,72 @@ export default class MeasurementReport { } } } + + /** + * This will use the adapter types to figure out which adapters might be + * able to convert this object. + */ + public static getAdapterForCodeType(measurementGroup) { + for (const adapter of this.measurementAdapterByTrackingIdentifier.values()) { + if (adapter.isValidMeasurement(measurementGroup)) { + return adapter; + } + } + } + + /** + * Register an adapter by type + * This will be some combination of the graphic code, type and point count. + * Only the most specific variants should be registered, unless the more + * general variants can be handled. + */ + public static registerAdapterTypes(adapter, ...types) { + for (const type of types) { + if (!this.measurementAdaptersByType.has(type)) { + this.measurementAdaptersByType.set(type, []); + } + const adapters = this.measurementAdaptersByType.get(type); + if (adapters.indexOf(adapter) === -1) { + adapters.push(adapter); + } + } + } + + /** + * Finds possible adapters for the point types + * + * @param graphicCode - in the designator:value format + * @param graphicType - as one of the allowed graphic type values + * @param pointCount - a number indicating how many points were found + * @returns An array of adapters that might handle this type + */ + public static getAdaptersForTypes( + graphicCode: string, + graphicType: string, + pointCount: number + ) { + const adapters = []; + + appendList( + adapters, + this.measurementAdaptersByType.get( + `${graphicCode}-${graphicType}-${pointCount}` + ) + ); + appendList( + adapters, + this.measurementAdaptersByType.get(`${graphicCode}-${graphicType}`) + ); + appendList(adapters, this.measurementAdaptersByType.get(graphicCode)); + appendList(adapters, this.measurementAdaptersByType.get(graphicType)); + + return adapters; + } +} + +function appendList(list, appendList) { + if (!appendList?.length) { + return; + } + list.push(...appendList); } diff --git a/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts b/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts index bc0dc79c60..1c9c05101f 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts @@ -2,6 +2,7 @@ import MeasurementReport from "./MeasurementReport"; import { utilities } from "dcmjs"; import { vec3 } from "gl-matrix"; import BaseAdapter3D from "./BaseAdapter3D"; +import { toScoords } from "../helpers"; const { Polyline: TID300Polyline } = utilities.TID300; @@ -15,64 +16,21 @@ class PlanarFreehandROI extends BaseAdapter3D { static getMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata ) { const { - defaultState, + state, NUMGroup, - SCOORDGroup, - SCOORD3DGroup, + worldCoords, + referencedImageId, ReferencedFrameNumber } = MeasurementReport.getSetupMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, metadata, - PlanarFreehandROI.toolType + this.toolType ); - if (SCOORDGroup) { - return this.getMeasurementDataFromScoord({ - defaultState, - SCOORDGroup, - imageToWorldCoords, - NUMGroup, - ReferencedFrameNumber - }); - } else if (SCOORD3DGroup) { - return this.getMeasurementDataFromScoord3D({ - defaultState, - SCOORD3DGroup - }); - } else { - throw new Error( - "Can't get measurement data with missing SCOORD and SCOORD3D groups." - ); - } - } - - static getMeasurementDataFromScoord({ - defaultState, - SCOORDGroup, - imageToWorldCoords, - NUMGroup, - ReferencedFrameNumber - }) { - const referencedImageId = - defaultState.annotation.metadata.referencedImageId; - const { GraphicData } = SCOORDGroup; - - const worldCoords = []; - - for (let i = 0; i < GraphicData.length; i += 2) { - const point = imageToWorldCoords(referencedImageId, [ - GraphicData[i], - GraphicData[i + 1] - ]); - - worldCoords.push(point); - } - const distanceBetweenFirstAndLastPoint = vec3.distance( worldCoords[worldCoords.length - 1], worldCoords[0] @@ -93,148 +51,50 @@ class PlanarFreehandROI extends BaseAdapter3D { points.push(worldCoords[0], worldCoords[worldCoords.length - 1]); } - const state = defaultState; - state.annotation.data = { + ...state.annotation.data, contour: { polyline: worldCoords, closed: !isOpenContour }, handles: { - points, - activeHandleIndex: null, - textBox: { - hasMoved: false - } + ...state.annotation.data.handles, + points }, - cachedStats: { + frameNumber: ReferencedFrameNumber + }; + + if (referencedImageId) { + state.annotation.data.cachedStats = { [`imageId:${referencedImageId}`]: { area: NUMGroup ? NUMGroup.MeasuredValueSequence.NumericValue : null } - }, - frameNumber: ReferencedFrameNumber - }; - - return state; - } - - static getMeasurementDataFromScoord3D({ defaultState, SCOORD3DGroup }) { - const { GraphicData } = SCOORD3DGroup; - - const worldCoords = []; - - for (let i = 0; i < GraphicData.length; i += 3) { - const point = [ - GraphicData[i], - GraphicData[i + 1], - GraphicData[i + 2] - ]; - - worldCoords.push(point); - } - - const distanceBetweenFirstAndLastPoint = vec3.distance( - worldCoords[worldCoords.length - 1], - worldCoords[0] - ); - - let isOpenContour = true; - - // If the contour is closed, this should have been encoded as exactly the same point, so check for a very small difference. - if (distanceBetweenFirstAndLastPoint < this.closedContourThreshold) { - worldCoords.pop(); // Remove the last element which is duplicated. - - isOpenContour = false; + }; } - - const points = []; - - if (isOpenContour) { - points.push(worldCoords[0], worldCoords[worldCoords.length - 1]); - } - - const state = defaultState; - - state.annotation.data = { - contour: { polyline: worldCoords, closed: !isOpenContour }, - handles: { - points, - activeHandleIndex: null, - textBox: { - hasMoved: false - } - }, - cachedStats: {} - }; - return state; } - static getTID300RepresentationArguments(tool, worldToImageCoords) { + static getTID300RepresentationArguments(tool, is3DMeasurement = false) { const { data, finding, findingSites, metadata } = tool; const { polyline, closed } = data.contour; const isOpenContour = closed !== true; const { referencedImageId } = metadata; - - if (!referencedImageId) { - return this.getTID300RepresentationArgumentsSCOORD3D(tool); - } - - // Using image coordinates for 2D points - const points = polyline.map(worldPos => - worldToImageCoords(referencedImageId, worldPos) - ); - - if (!isOpenContour) { - // Need to repeat the first point at the end of to have an explicitly closed contour. - const firstPoint = points[0]; - - // Explicitly expand to avoid circular references. - points.push([firstPoint[0], firstPoint[1]]); - } - - const { area, areaUnit, modalityUnit, perimeter, mean, max, stdDev } = - data.cachedStats[`imageId:${referencedImageId}`] || {}; - - return { - /** From cachedStats */ - points, - area, - areaUnit, - perimeter, - modalityUnit, - mean, - max, - stdDev, - /** Other */ - trackingIdentifierTextValue: this.trackingIdentifierTextValue, - finding, - findingSites: findingSites || [], - use3DSpatialCoordinates: false + const scoordProps = { + is3DMeasurement, + referencedImageId }; - } - static getTID300RepresentationArgumentsSCOORD3D(tool) { - const { data, finding, findingSites, metadata } = tool; - - const { polyline, closed } = data.contour; - const isOpenContour = closed !== true; - - // Using world coordinates for 3D points - const points = polyline; + const points = toScoords(scoordProps, polyline); if (!isOpenContour) { // Need to repeat the first point at the end of to have an explicitly closed contour. const firstPoint = points[0]; - - // Explicitly expand to avoid circular references. - points.push([firstPoint[0], firstPoint[1], firstPoint[2]]); + points.push(firstPoint); } - const cachedStatsKeys = Object.keys(data.cachedStats)[0]; const { area, areaUnit, modalityUnit, perimeter, mean, max, stdDev } = - cachedStatsKeys ? data.cachedStats[cachedStatsKeys] : {}; + data.cachedStats[`imageId:${referencedImageId}`] || {}; return { /** From cachedStats */ @@ -250,8 +110,10 @@ class PlanarFreehandROI extends BaseAdapter3D { trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [], - ReferencedFrameOfReferenceUID: metadata.FrameOfReferenceUID, - use3DSpatialCoordinates: true + ReferencedFrameOfReferenceUID: is3DMeasurement + ? metadata.FrameOfReferenceUID + : null, + use3DSpatialCoordinates: is3DMeasurement }; } } diff --git a/packages/adapters/src/adapters/Cornerstone3D/Probe.ts b/packages/adapters/src/adapters/Cornerstone3D/Probe.ts index 058d62a6c9..fd7b9693da 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Probe.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Probe.ts @@ -1,6 +1,7 @@ import { utilities } from "dcmjs"; import MeasurementReport from "./MeasurementReport"; import BaseAdapter3D from "./BaseAdapter3D"; +import { toScoords } from "../helpers"; const { Point: TID300Point } = utilities.TID300; @@ -8,160 +9,87 @@ class Probe extends BaseAdapter3D { static { this.init("Probe", TID300Point); this.registerLegacy(); + this.registerType("DCM:111030", "POINT", 1); + this.registerType("DCM:111030", "POINT", 2); + } + + public static isValidMeasurement(measurement) { + const graphicItem = this.getGraphicItem(measurement); + return ( + this.getGraphicType(graphicItem) === "POINT" && + this.getPointsCount(graphicItem) <= 2 + ); } static getMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata, - trackingIdentifier + _trackingIdentifier ) { - const state = super.getMeasurementData( + const { + state, + NUMGroup, + worldCoords, + referencedImageId, + ReferencedFrameNumber + } = MeasurementReport.getSetupMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata, - trackingIdentifier + this.toolType ); - const { defaultState, SCOORDGroup, SCOORD3DGroup } = - MeasurementReport.getSetupMeasurementData( - MeasurementGroup, - sopInstanceUIDToImageIdMap, - metadata, - Probe.toolType - ); - - if (SCOORDGroup) { - return this.getMeasurementDataFromScoord({ - state, - defaultState, - SCOORDGroup, - imageToWorldCoords - }); - } else if (SCOORD3DGroup) { - return this.getMeasurementDataFromScoord3D({ - state, - SCOORD3DGroup - }); - } else { - throw new Error( - "Can't get measurement data with missing SCOORD and SCOORD3D groups." - ); - } - } - - static getMeasurementDataFromScoord({ - state, - defaultState, - SCOORDGroup, - imageToWorldCoords - }) { - const referencedImageId = - defaultState.annotation.metadata.referencedImageId; - - const { GraphicData } = SCOORDGroup; - - const worldCoords = []; - for (let i = 0; i < GraphicData.length; i += 2) { - const point = imageToWorldCoords(referencedImageId, [ - GraphicData[i], - GraphicData[i + 1] - ]); - worldCoords.push(point); - } - - state.annotation.data = { - ...state.annotation.data, - handles: { - points: worldCoords, - activeHandleIndex: null, - textBox: { - hasMoved: false - } - } - }; - - return state; - } - - static getMeasurementDataFromScoord3D({ state, SCOORD3DGroup }) { - const { GraphicData } = SCOORD3DGroup; - - const worldCoords = []; - for (let i = 0; i < GraphicData.length; i += 3) { - const point = [ - GraphicData[i], - GraphicData[i + 1], - GraphicData[i + 2] - ]; - worldCoords.push(point); - } - + const cachedStats = referencedImageId + ? { + [`imageId:${referencedImageId}`]: { + value: + NUMGroup?.MeasuredValueSequence?.NumericValue ?? null + } + } + : {}; state.annotation.data = { ...state.annotation.data, handles: { - points: worldCoords, - activeHandleIndex: null, - textBox: { - hasMoved: false - } - } + ...state.annotation.data.handles, + points: worldCoords + }, + cachedStats, + frameNumber: ReferencedFrameNumber, + invalidated: true }; return state; } - public static getTID300RepresentationArguments(tool, worldToImageCoords) { + public static getTID300RepresentationArguments( + tool, + is3DMeasurement = false + ) { const { data, metadata } = tool; const { finding, findingSites } = tool; const { referencedImageId } = metadata; - - if (!referencedImageId) { - return this.getTID300RepresentationArgumentsSCOORD3D(tool); - } + const scoordProps = { + is3DMeasurement, + referencedImageId + }; const { handles: { points = [] } } = data; // Using image coordinates for 2D points - const pointsImage = points.map(point => { - const pointImage = worldToImageCoords(referencedImageId, point); - return { - x: pointImage[0], - y: pointImage[1] - }; - }); + const pointsImage = toScoords(scoordProps, points); return { points: pointsImage, trackingIdentifierTextValue: this.trackingIdentifierTextValue, findingSites: findingSites || [], finding, - use3DSpatialCoordinates: false - }; - } - - static getTID300RepresentationArgumentsSCOORD3D(tool) { - const { data, finding, findingSites, metadata } = tool; - const { - handles: { points = [] } - } = data; - - // Using world coordinates for 3D points - const point = points[0]; - - const pointXYZ = { x: point[0], y: point[1], z: point[2] }; - - return { - points: [pointXYZ], - trackingIdentifierTextValue: this.trackingIdentifierTextValue, - ReferencedFrameOfReferenceUID: metadata.FrameOfReferenceUID, - findingSites: findingSites || [], - finding, - use3DSpatialCoordinates: true + ReferencedFrameOfReferenceUID: is3DMeasurement + ? metadata.FrameOfReferenceUID + : null, + use3DSpatialCoordinates: is3DMeasurement }; } } diff --git a/packages/adapters/src/adapters/Cornerstone3D/RectangleROI.ts b/packages/adapters/src/adapters/Cornerstone3D/RectangleROI.ts index 33f12bb7fc..1287b86685 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/RectangleROI.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/RectangleROI.ts @@ -1,10 +1,12 @@ import { utilities } from "dcmjs"; + +import { toScoords } from "../helpers"; import MeasurementReport from "./MeasurementReport"; import BaseAdapter3D from "./BaseAdapter3D"; const { Polyline: TID300Polyline } = utilities.TID300; -class RectangleROI extends BaseAdapter3D { +export class RectangleROI extends BaseAdapter3D { static { this.init("RectangleROI", TID300Polyline); // Register using the Cornerstone 1.x name so this tool is used to load it @@ -13,165 +15,63 @@ class RectangleROI extends BaseAdapter3D { public static getMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata ) { - const { - defaultState, - NUMGroup, - SCOORDGroup, - SCOORD3DGroup, - ReferencedFrameNumber - } = MeasurementReport.getSetupMeasurementData( - MeasurementGroup, - sopInstanceUIDToImageIdMap, - metadata, - RectangleROI.toolType - ); - - if (SCOORDGroup) { - return this.getMeasurementDataFromScoord({ - defaultState, - SCOORDGroup, - imageToWorldCoords, - NUMGroup, - ReferencedFrameNumber - }); - } else if (SCOORD3DGroup) { - return this.getMeasurementDataFromScoord3D({ - SCOORD3DGroup, - defaultState - }); - } else { - throw new Error( - "Can't get measurement data with missing SCOORD and SCOORD3D groups." + const { state, worldCoords, referencedImageId, ReferencedFrameNumber } = + MeasurementReport.getSetupMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + metadata, + this.toolType ); - } - } - - static getMeasurementDataFromScoord({ - defaultState, - SCOORDGroup, - imageToWorldCoords, - NUMGroup, - ReferencedFrameNumber - }) { - const referencedImageId = - defaultState.annotation.metadata.referencedImageId; - - const { GraphicData } = SCOORDGroup; - const worldCoords = []; - for (let i = 0; i < GraphicData.length; i += 2) { - const point = imageToWorldCoords(referencedImageId, [ - GraphicData[i], - GraphicData[i + 1] - ]); - worldCoords.push(point); - } - - const state = defaultState; + const areaGroup = MeasurementGroup.ContentSequence.find( + g => + g.ValueType === "NUM" && + g.ConceptNameCodeSequence[0].CodeMeaning === "Area" + ); + const cachedStats = referencedImageId + ? { + [`imageId:${referencedImageId}`]: { + area: + areaGroup?.MeasuredValueSequence?.[0]?.NumericValue || + 0, + areaUnit: + areaGroup?.MeasuredValueSequence?.[0] + ?.MeasurementUnitsCodeSequence?.CodeValue + } + } + : {}; state.annotation.data = { + ...state.annotation.data, handles: { + ...state.annotation.data.handles, points: [ worldCoords[0], worldCoords[1], worldCoords[3], worldCoords[2] - ], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - }, - cachedStats: { - [`imageId:${referencedImageId}`]: { - area: NUMGroup - ? NUMGroup.MeasuredValueSequence.NumericValue - : null - } + ] }, + cachedStats, frameNumber: ReferencedFrameNumber }; - - return state; - } - - static getMeasurementDataFromScoord3D({ SCOORD3DGroup, defaultState }) { - const { GraphicData } = SCOORD3DGroup; - const worldCoords = []; - for (let i = 0; i < GraphicData.length; i += 3) { - const point = [ - GraphicData[i], - GraphicData[i + 1], - GraphicData[i + 2] - ]; - worldCoords.push(point); - } - - const state = defaultState; - - state.annotation.data = { - handles: { - points: [ - worldCoords[0], - worldCoords[1], - worldCoords[3], - worldCoords[2] - ], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } - }, - cachedStats: {} - }; - return state; } - static getTID300RepresentationArguments(tool, worldToImageCoords) { + static getTID300RepresentationArguments(tool, is3DMeasurement = false) { const { data, finding, findingSites, metadata } = tool; - const { cachedStats = {}, handles } = data; const { referencedImageId } = metadata; - - if (!referencedImageId) { - return this.getTID300RepresentationArgumentsSCOORD3D(tool); - } - - //Using image coordinates for 2D points - const corners = handles.points.map(point => - worldToImageCoords(referencedImageId, point) - ); - - const { area, perimeter } = cachedStats; - - return { - points: [ - corners[0], - corners[1], - corners[3], - corners[2], - corners[0] - ], - area, - perimeter, - trackingIdentifierTextValue: this.trackingIdentifierTextValue, - finding, - findingSites: findingSites || [], - use3DSpatialCoordinates: false + const scoordProps = { + is3DMeasurement, + referencedImageId }; - } - - static getTID300RepresentationArgumentsSCOORD3D(tool) { - const { data, finding, findingSites, metadata } = tool; - const { cachedStats = {}, handles } = data; - //Using world coordinates for 3D points - const corners = handles.points; + const corners = toScoords(scoordProps, data.handles.points); - const { area, perimeter } = cachedStats; + const { area, perimeter } = + data.cachedStats[`imageId:${referencedImageId}`] || {}; return { points: [ @@ -186,8 +86,7 @@ class RectangleROI extends BaseAdapter3D { trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [], - ReferencedFrameOfReferenceUID: metadata.FrameOfReferenceUID, - use3DSpatialCoordinates: true + use3DSpatialCoordinates: is3DMeasurement }; } } diff --git a/packages/adapters/src/adapters/Cornerstone3D/UltrasoundDirectional.ts b/packages/adapters/src/adapters/Cornerstone3D/UltrasoundDirectional.ts index affef4a1d2..15e219ab43 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/UltrasoundDirectional.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/UltrasoundDirectional.ts @@ -1,8 +1,10 @@ import { utilities } from "dcmjs"; +import { utilities as csUtilities } from "@cornerstonejs/core"; import MeasurementReport from "./MeasurementReport"; import BaseAdapter3D from "./BaseAdapter3D"; const { Length: TID300Length } = utilities.TID300; +const { worldToImageCoords } = csUtilities; class UltrasoundDirectional extends BaseAdapter3D { static { @@ -10,50 +12,31 @@ class UltrasoundDirectional extends BaseAdapter3D { } // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport. static getMeasurementData( - MeasurementGroup, + measurementGroup, sopInstanceUIDToImageIdMap, - imageToWorldCoords, metadata ) { - const { defaultState, SCOORDGroup, ReferencedFrameNumber } = + const { state, worldCoords, ReferencedFrameNumber } = MeasurementReport.getSetupMeasurementData( - MeasurementGroup, + measurementGroup, sopInstanceUIDToImageIdMap, metadata, - UltrasoundDirectional.toolType + this.toolType ); - const referencedImageId = - defaultState.annotation.metadata.referencedImageId; - - const { GraphicData } = SCOORDGroup; - const worldCoords = []; - for (let i = 0; i < GraphicData.length; i += 2) { - const point = imageToWorldCoords(referencedImageId, [ - GraphicData[i], - GraphicData[i + 1] - ]); - worldCoords.push(point); - } - - const state = defaultState; - state.annotation.data = { + ...state.annotation.data, handles: { - points: [worldCoords[0], worldCoords[1]], - activeHandleIndex: 0, - textBox: { - hasMoved: false - } + ...state.annotation.data.handles, + points: worldCoords }, - cachedStats: {}, frameNumber: ReferencedFrameNumber }; return state; } - static getTID300RepresentationArguments(tool, worldToImageCoords) { + static getTID300RepresentationArguments(tool, is3DMeasurement) { const { data, finding, findingSites, metadata } = tool; const { handles } = data; diff --git a/packages/adapters/src/adapters/Cornerstone3D/constants/index.ts b/packages/adapters/src/adapters/Cornerstone3D/constants/index.ts index 22b2de19c2..ba07980b94 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/constants/index.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/constants/index.ts @@ -1 +1,15 @@ export const NO_IMAGE_ID = "none"; + +export const CS3D_DESIGNATOR = "99CS3D"; + +export const TEXT_ANNOTATION_POSITION = { + schemeDesignator: CS3D_DESIGNATOR, + meaning: "Text Annotation Position", + value: "TextPosition" +}; + +export const COMMENT_CODE = { + schemeDesignator: "DCM", + meaning: "Comment", + value: "121106" +}; diff --git a/packages/adapters/src/adapters/Cornerstone3D/index.ts b/packages/adapters/src/adapters/Cornerstone3D/index.ts index a2a6489489..bfaaf4cb87 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/index.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/index.ts @@ -19,6 +19,11 @@ import * as Segmentation from "./Segmentation"; import * as ParametricMap from "./ParametricMap"; import * as RTSS from "./RTStruct"; import KeyImage from "./KeyImage"; +import { + COMMENT_CODE, + NO_IMAGE_ID, + TEXT_ANNOTATION_POSITION +} from "./constants"; const Cornerstone3DSR = { BaseAdapter3D, @@ -36,7 +41,10 @@ const Cornerstone3DSR = { KeyImage, MeasurementReport, CodeScheme, - CORNERSTONE_3D_TAG + CORNERSTONE_3D_TAG, + COMMENT_CODE, + NO_IMAGE_ID, + TEXT_ANNOTATION_POSITION }; const Cornerstone3DSEG = { diff --git a/packages/adapters/src/adapters/helpers/checkOrientation.ts b/packages/adapters/src/adapters/helpers/checkOrientation.ts index d8d2610cd7..dc5aa13fa1 100644 --- a/packages/adapters/src/adapters/helpers/checkOrientation.ts +++ b/packages/adapters/src/adapters/helpers/checkOrientation.ts @@ -1,5 +1,5 @@ import checkIfPerpendicular from "./checkIfPerpendicular"; -import compareArrays from "./compareArrays"; +import { utilities } from "@cornerstonejs/core"; export default function checkOrientation( multiframe, @@ -25,7 +25,7 @@ export default function checkOrientation( .ImageOrientationPatient; const inPlane = validOrientations.some(operation => - compareArrays(iop, operation, tolerance) + utilities.isEqual(iop, operation, tolerance) ); if (inPlane) { diff --git a/packages/adapters/src/adapters/helpers/compareArrays.ts b/packages/adapters/src/adapters/helpers/compareArrays.ts deleted file mode 100644 index a51b472dc4..0000000000 --- a/packages/adapters/src/adapters/helpers/compareArrays.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { utilities } from "dcmjs"; - -const { nearlyEqual } = utilities.orientation; - -/** - * Returns true if array1 and array2 are equal within a tolerance. - * - * @param array1 - First array - * @param array2 - Second array - * @param tolerance - Tolerance - * @returns True if array1 and array2 are equal. - */ -export default function compareArrays( - array1: number[], - array2: number[], - tolerance: number -): boolean { - if (array1.length !== array2.length) { - return false; - } - - for (let i = 0; i < array1.length; ++i) { - if (!nearlyEqual(array1[i], array2[i], tolerance)) { - return false; - } - } - - return true; -} diff --git a/packages/adapters/src/adapters/helpers/index.ts b/packages/adapters/src/adapters/helpers/index.ts index 737234f64c..fb065a7edf 100644 --- a/packages/adapters/src/adapters/helpers/index.ts +++ b/packages/adapters/src/adapters/helpers/index.ts @@ -5,4 +5,8 @@ import { downloadDICOMData } from "./downloadDICOMData"; export { copyStudyTags } from "./copyStudyTags"; export { copySeriesTags } from "./copySeriesTags"; +export * from "./toScoordType"; +export * from "./scoordToWorld"; +export * from "./toPoint3"; + export { toArray, codeMeaningEquals, graphicTypeEquals, downloadDICOMData }; diff --git a/packages/adapters/src/adapters/helpers/scoordToWorld.ts b/packages/adapters/src/adapters/helpers/scoordToWorld.ts new file mode 100644 index 0000000000..c9415e736f --- /dev/null +++ b/packages/adapters/src/adapters/helpers/scoordToWorld.ts @@ -0,0 +1,36 @@ +import { type Types, utilities } from "@cornerstonejs/core"; + +const { imageToWorldCoords } = utilities; + +/** + * Converts flat listed 2d or 3d coordinates into Point3 world coordinates. + * For 3d points, this will convert just the structure from flat, while for + * 2d points, it will convert image to to world coords. + */ +export function scoordToWorld( + { is3DMeasurement, referencedImageId }, + scoord +): Types.Point3[] { + const worldCoords = []; + if (is3DMeasurement) { + const { GraphicData } = scoord; + for (let i = 0; i < GraphicData.length; i += 3) { + const point = [ + GraphicData[i], + GraphicData[i + 1], + GraphicData[i + 2] + ]; + worldCoords.push(point); + } + } else { + const { GraphicData } = scoord; + for (let i = 0; i < GraphicData.length; i += 2) { + const point = imageToWorldCoords(referencedImageId, [ + GraphicData[i], + GraphicData[i + 1] + ]); + worldCoords.push(point); + } + } + return worldCoords; +} diff --git a/packages/adapters/src/adapters/helpers/toArray.ts b/packages/adapters/src/adapters/helpers/toArray.ts index e91a900928..7a45047cba 100644 --- a/packages/adapters/src/adapters/helpers/toArray.ts +++ b/packages/adapters/src/adapters/helpers/toArray.ts @@ -1,3 +1,3 @@ -const toArray = x => (Array.isArray(x) ? x : [x]); +const toArray = x => (Array.isArray(x) ? x : x !== undefined ? [x] : []); export { toArray }; diff --git a/packages/adapters/src/adapters/helpers/toPoint3.ts b/packages/adapters/src/adapters/helpers/toPoint3.ts new file mode 100644 index 0000000000..e623cf8cca --- /dev/null +++ b/packages/adapters/src/adapters/helpers/toPoint3.ts @@ -0,0 +1,28 @@ +import type { Types } from "@cornerstonejs/core"; + +/** + * Converts a set of flat points, represented as just a flat list of + * `[x1,y1,z1,x2,y2,z2,....]` + * into the equivalent Point3 array + * `[[x1,y1,z1], [x2,y2,z2],...]` + */ +export function toPoint3(flatPoints: number[]): Types.Point3[] { + const points = []; + if (!flatPoints?.length) { + return points; + } + const { length: n } = flatPoints; + if (n % 3 !== 0) { + throw new Error( + `Points array should be divisible by 3 for SCOORD3D, but contents are: ${JSON.stringify( + flatPoints + )} of length ${n}` + ); + } + for (let i = 0; i < n; i += 3) { + points.push([flatPoints[i], flatPoints[i + 1], flatPoints[i + 2]]); + } + return points; +} + +export default toPoint3; diff --git a/packages/adapters/src/adapters/helpers/toScoordType.ts b/packages/adapters/src/adapters/helpers/toScoordType.ts new file mode 100644 index 0000000000..858416fddf --- /dev/null +++ b/packages/adapters/src/adapters/helpers/toScoordType.ts @@ -0,0 +1,28 @@ +import { type Types, utilities } from "@cornerstonejs/core"; + +const { worldToImageCoords: globalWorldToImageCoords } = utilities; + +let useWorldToImageCoords = globalWorldToImageCoords; + +/** + * Converts a Point2 or a Point3 into a SCOORD object x,y, and optional z. + */ +export function toScoord({ is3DMeasurement, referencedImageId }, point) { + if (is3DMeasurement) { + return { x: point[0], y: point[1], z: point[2] }; + } + const point2 = useWorldToImageCoords(referencedImageId, point); + return { x: point2[0], y: point2[1] }; +} + +/** + * Converts an array of Scoord points to 3d + */ +export function toScoords(scoordArgs, points: Array) { + return points.map(point => toScoord(scoordArgs, point)); +} + +/** Over-ride function for testing */ +export function setWorldToImageCoords(worldToImage = globalWorldToImageCoords) { + useWorldToImageCoords = worldToImage; +} diff --git a/packages/adapters/src/version.ts b/packages/adapters/src/version.ts deleted file mode 100644 index d3d26f196b..0000000000 --- a/packages/adapters/src/version.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Auto-generated from version.json - * Do not modify this file directly - */ -export const version = "4.0.0-beta.3"; diff --git a/packages/adapters/test/Length.jest.js b/packages/adapters/test/Length.jest.js new file mode 100644 index 0000000000..4858f447c0 --- /dev/null +++ b/packages/adapters/test/Length.jest.js @@ -0,0 +1,79 @@ +import { describe, it, expect, beforeEach } from "@jest/globals"; +import { utilities } from "@cornerstonejs/core"; +import { Cornerstone3DSR } from "../src/adapters/Cornerstone3D"; +import { setWorldToImageCoords } from "../src/adapters/helpers"; + +const { worldToImageCoords: globalWorldToImageCoords } = utilities; + +const { Length } = Cornerstone3DSR; + +function worldToImageCoords(referencedImageId, point3) { + if (point3[2] !== parseInt(referencedImageId)) { + throw new Error( + `Trying to convert a point with the wrong index: ${point3[2]}!==${referencedImageId}` + ); + } + return [point3[0], point3[1]]; +} + +const tool2d = { + metadata: { + referencedImageId: "2" + }, + + data: { + handles: { + points: [ + [0, 1, 2], + [10, 5, 2] + ] + } + } +}; + +const tool3d = { + metadata: { + FrameOfReferenceUID: "1.2.3" + }, + + data: { + handles: { + points: [ + [0, 1, 2], + [10, 5, 11] + ] + } + } +}; + +describe("Length", () => { + beforeEach(() => { + setWorldToImageCoords(worldToImageCoords); + // Setup adapters + }); + + afterEach(() => { + setWorldToImageCoords(globalWorldToImageCoords); + }); + + it("Must define tool type", () => { + expect(Length.toolType).toBe("Length"); + }); + + it("Must use scoord for planar", () => { + const tidArgs = Length.getTID300RepresentationArguments(tool2d, false); + // Either x,y or [x,y] is allowed + expect(tidArgs.point1).toEqual({ x: 0, y: 1 }); + expect(tidArgs.point2).toEqual({ x: 10, y: 5 }); + }); + + it("Must use scoord3d for mpr points", () => { + const tidArgs = Length.getTID300RepresentationArguments(tool3d, true); + expect(tidArgs.point1).toEqual({ x: 0, y: 1, z: 2 }); + expect(tidArgs.point2).toEqual({ x: 10, y: 5, z: 11 }); + }); + + it("Must convert tid1501 to tool data scoord", () => {}); +}); + +// diff --git a/packages/adapters/tsconfig.json b/packages/adapters/tsconfig.json index 1004fd44f2..f45744c337 100644 --- a/packages/adapters/tsconfig.json +++ b/packages/adapters/tsconfig.json @@ -4,6 +4,6 @@ "outDir": "./dist/esm", "rootDir": "./src" }, - "include": ["./src/**/*"], + "include": ["./src/**/*", "./test/**/*", "./jest.config.js"], "exclude": ["node_modules", "dist"] } diff --git a/packages/ai/package.json b/packages/ai/package.json index 84ac0395d8..2b8fe7c99d 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -36,8 +36,7 @@ "build:all": "yarn run build:esm", "start": "tsc --project ./tsconfig.json --watch", "format": "prettier --write 'src/**/*.js' 'test/**/*.js'", - "lint": "eslint --fix .", - "format-check": "npx eslint ./src --quiet", + "lint": "oxlint .", "api-check": "api-extractor --debug run ", "prepublishOnly": "yarn clean && yarn build" }, @@ -54,7 +53,7 @@ "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", - "dcmjs": "^0.42.0", + "dcmjs": "^0.43.1", "gl-matrix": "^3.4.3", "lodash.clonedeep": "^4.5.0", "ndarray": "^1.0.19", diff --git a/packages/ai/src/MarkerLabelmapTool.ts b/packages/ai/src/MarkerLabelmapTool.ts index af8fc073be..2925e4fa54 100644 --- a/packages/ai/src/MarkerLabelmapTool.ts +++ b/packages/ai/src/MarkerLabelmapTool.ts @@ -2,10 +2,8 @@ import { getEnabledElementByViewportId } from '@cornerstonejs/core'; import { LabelmapBaseTool, ToolGroupManager, - Enums, annotation, ProbeTool, - RectangleROITool, addTool, } from '@cornerstonejs/tools'; import ONNXSegmentationController from './ONNXSegmentationController'; @@ -53,6 +51,37 @@ class MarkerLabelmapTool extends LabelmapBaseTool { super(toolProps, defaultToolProps); } + /** + * Checks if the tool should resolve preview requests. + * This is used to determine if the tool is in a state where it can handle + * preview requests. + * @returns True if the tool should resolve preview requests, false otherwise. + */ + public shouldResolvePreviewRequests() { + const MARKER_TOOLS = [ + ONNXSegmentationController.MarkerInclude, + ONNXSegmentationController.MarkerExclude, + ONNXSegmentationController.BoxPrompt, + ]; + const toolGroup = this._getToolGroupId(); + if (!toolGroup) { + console.debug( + `Tool group not found for tool: ${MarkerLabelmapTool.toolName}` + ); + return false; + } + + return ( + this.hasPreviewData() && + MARKER_TOOLS.some((markerToolName) => { + if (toolGroup.hasTool(markerToolName)) { + const instance = toolGroup.getToolInstance(markerToolName); + return instance.mode === 'Active' || instance.mode === 'Enabled'; + } + }) + ); + } + _init = async () => { const { configuration } = this; diff --git a/packages/ai/src/ONNXSegmentationController.ts b/packages/ai/src/ONNXSegmentationController.ts index 9f7ef59209..6cad9e50d8 100644 --- a/packages/ai/src/ONNXSegmentationController.ts +++ b/packages/ai/src/ONNXSegmentationController.ts @@ -1002,9 +1002,8 @@ export default class ONNXSegmentationController { renderArguments.viewReference = viewRef; renderArguments.imageId = null; } - imageSession.canvasPosition = await utilities.loadImageToCanvas( - renderArguments - ); + imageSession.canvasPosition = + await utilities.loadImageToCanvas(renderArguments); canvas.style.width = size; canvas.style.height = size; if (isCurrent) { diff --git a/packages/core/examples/contextpoolrenderingengine/index.ts b/packages/core/examples/contextpoolrenderingengine/index.ts new file mode 100644 index 0000000000..cf041782be --- /dev/null +++ b/packages/core/examples/contextpoolrenderingengine/index.ts @@ -0,0 +1,182 @@ +import type { Types } from '@cornerstonejs/core'; +import { + Enums, + getRenderingEngine, + RenderingEngine, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + ctVoiRange, +} from '../../../../utils/demo/helpers'; + +const { ViewportType } = Enums; + +// ======== Set up page ======== // +const params = new URLSearchParams(window.location.search); + +const rows = parseInt(params.get('rows')) || 6; +const columns = parseInt(params.get('columns')) || 6; +const count = rows * columns; + +setTitleAndDescription( + `${columns}x${rows} Grid with ContextPoolRenderingEngine`, + `Displays a ${columns}x${rows} grid of viewports using ContextPoolRenderingEngine with resize observer` +); + +const renderingEngineId = 'myContextPoolRenderingEngine'; +let renderingEngine: RenderingEngine; +let resizeTimeout: number; + +// Debounced resize handler +const handleResize = () => { + clearTimeout(resizeTimeout); + resizeTimeout = window.setTimeout(() => { + renderingEngine = getRenderingEngine(renderingEngineId); + + if (renderingEngine) { + renderingEngine.resize(true, false); + } + }, 50); // 50ms debounce +}; + +// Set up ResizeObserver for dynamic resizing +const resizeObserver = new ResizeObserver(handleResize); + +const content = document.getElementById('content'); + +// Create viewport container using flexbox +const viewportContainer = document.createElement('div'); +viewportContainer.id = 'viewportContainer'; +viewportContainer.style.display = 'flex'; +viewportContainer.style.flexDirection = 'column'; +viewportContainer.style.width = '95vw'; +viewportContainer.style.height = '90vh'; +viewportContainer.style.backgroundColor = 'darkred'; +viewportContainer.style.gap = '2px'; + +content.appendChild(viewportContainer); + +// Create rows*columns viewport elements +const elements: HTMLDivElement[] = []; +const viewportIds: string[] = []; + +// Create rows and columns +for (let row = 0; row < rows; row++) { + const rowContainer = document.createElement('div'); + rowContainer.style.display = 'flex'; + rowContainer.style.flexDirection = 'row'; + rowContainer.style.flex = '1'; + rowContainer.style.width = '100%'; + rowContainer.style.gap = '2px'; + + for (let col = 0; col < columns; col++) { + const element = document.createElement('div'); + element.style.flex = '1'; + element.style.position = 'relative'; + element.style.overflow = 'hidden'; + element.style.background = '#1a1a1a'; + element.style.minWidth = '0'; + element.style.minHeight = '0'; + + // Disable right click context menu + element.oncontextmenu = (e) => e.preventDefault(); + + // Add to row + rowContainer.appendChild(element); + + // Store element and create viewport ID + elements.push(element); + viewportIds.push(`viewport_${row}_${col}`); + + // Observe element for resizing + resizeObserver.observe(element); + } + + viewportContainer.appendChild(rowContainer); +} + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + // Get Cornerstone imageIds and fetch metadata into RAM + const imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + SeriesInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + wadoRsRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + }); + + // Instantiate a ContextPoolRenderingEngine + renderingEngine = new RenderingEngine(renderingEngineId); + + // Create viewport input array for all 36 viewports + const viewportInputArray: Types.PublicViewportInput[] = []; + + for (let i = 0; i < count; i++) { + viewportInputArray.push({ + viewportId: viewportIds[i], + type: ViewportType.STACK, + element: elements[i], + defaultOptions: { + background: [0.1, 0.1, 0.1] as Types.Point3, + }, + }); + } + + // Set all viewports at once + renderingEngine.setViewports(viewportInputArray); + + // Prepare stacks for each viewport + // We'll display different images from the series in each viewport + const numImages = imageIds.length; + const imagesPerViewport = Math.max(1, Math.floor(numImages / count)); + + // Set stack and render for each viewport + for (let i = 0; i < count; i++) { + const viewport = renderingEngine.getViewport( + viewportIds[i] + ) as Types.IStackViewport; + + // Calculate which image to show + const imageIndex = Math.min(i * imagesPerViewport, numImages - 1); + const stack = [imageIds[imageIndex]]; + + // Set the stack on the viewport + await viewport.setStack(stack); + + // Set the VOI of the stack + viewport.setProperties({ voiRange: ctVoiRange }); + } + + // Render all viewports + renderingEngine.render(); + + // Add performance info + if (!window.IS_PLAYWRIGHT) { + const info = document.createElement('div'); + info.style.position = 'absolute'; + info.style.top = '10px'; + info.style.right = '10px'; + info.style.color = 'white'; + info.style.backgroundColor = 'rgba(0,0,0,0.7)'; + info.style.padding = '10px'; + info.style.borderRadius = '5px'; + info.innerHTML = ` +

ContextPoolRenderingEngine Example

+

${count} viewports (${columns}x${rows} grid)

+

Using ContextPoolRenderingEngine for better performance with large viewport counts

+

Resize the window to test resize observer

+

Avoids WebGL, browser and OS limits

+ `; + content.appendChild(info); + } +} + +run(); diff --git a/packages/core/examples/dicomLoader/customImageLoader.ts b/packages/core/examples/dicomLoader/customImageLoader.ts index a868a09fa8..8b888e9f0e 100644 --- a/packages/core/examples/dicomLoader/customImageLoader.ts +++ b/packages/core/examples/dicomLoader/customImageLoader.ts @@ -36,7 +36,7 @@ function _loadImageIntoBuffer( imageId, pixelData, transferSyntax, - options as any + options as unknown ); logFn('Loader: done loading image: ', sopInstanceUid); diff --git a/packages/core/examples/meshLoader/index.ts b/packages/core/examples/meshLoader/index.ts index 7b4126f6b3..5555847075 100644 --- a/packages/core/examples/meshLoader/index.ts +++ b/packages/core/examples/meshLoader/index.ts @@ -97,7 +97,16 @@ content.append(instructions); */ async function run() { // Init Cornerstone and related libraries - await csRenderInit(); + + const urlParams = new URLSearchParams(window.location.search); + const debugMode = urlParams.get('debug') === 'true'; + + await csRenderInit({ + debug: { + statsOverlay: debugMode, + }, + }); + await csToolsInit(); const toolGroupId = 'NAVIGATION_TOOL_GROUP_ID'; diff --git a/packages/core/examples/polyDataActorAPI/index.ts b/packages/core/examples/polyDataActorAPI/index.ts index ff602eb8f1..2ac14b6612 100644 --- a/packages/core/examples/polyDataActorAPI/index.ts +++ b/packages/core/examples/polyDataActorAPI/index.ts @@ -1,7 +1,10 @@ import type { Types } from '@cornerstonejs/core'; -import { RenderingEngine, Enums } from '@cornerstonejs/core'; +import { + RenderingEngine, + Enums, + init as csRenderInit, +} from '@cornerstonejs/core'; import { setTitleAndDescription } from '../../../../utils/demo/helpers'; -import { init as csRenderInit } from '@cornerstonejs/core'; import { init as csToolsInit } from '@cornerstonejs/tools'; import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; @@ -65,7 +68,16 @@ function getSphereActor({ */ async function run() { // Init Cornerstone and related libraries - await csRenderInit(); + + const urlParams = new URLSearchParams(window.location.search); + const debugMode = urlParams.get('debug') === 'true'; + + await csRenderInit({ + debug: { + statsOverlay: debugMode, + }, + }); + await csToolsInit(); // Instantiate a rendering engine diff --git a/packages/core/examples/ptctmultimonitor/index.ts b/packages/core/examples/ptctmultimonitor/index.ts new file mode 100644 index 0000000000..38e6b832fc --- /dev/null +++ b/packages/core/examples/ptctmultimonitor/index.ts @@ -0,0 +1,918 @@ +import type { Types } from '@cornerstonejs/core'; +import { + RenderingEngine, + Enums, + setVolumesForViewports, + volumeLoader, + getRenderingEngine, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + setPetColorMapTransferFunctionForVolumeActor, + setPetTransferFunctionForVolumeActor, + setCtTransferFunctionForVolumeActor, + addDropdownToToolbar, +} from '../../../../utils/demo/helpers'; +import * as cornerstoneTools from '@cornerstonejs/tools'; + +const { + ToolGroupManager, + Enums: csToolsEnums, + WindowLevelTool, + PanTool, + ZoomTool, + StackScrollTool, + synchronizers, + MIPJumpToClickTool, + CrosshairsTool, + TrackballRotateTool, + VolumeRotateTool, + RectangleROITool, +} = cornerstoneTools; + +const { MouseBindings } = csToolsEnums; +const { ViewportType, BlendModes } = Enums; + +const { createCameraPositionSynchronizer, createVOISynchronizer } = + synchronizers; + +// Study IDs +const FirstStudyID = `1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339`; +const SecondStudyID = + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463'; +const ThirdStudyID = `1.3.6.1.4.1.9328.50.17.15423521354819720574322014551955370036`; + +// Common configuration +let renderingEngine; +const wadoRsRoot = 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb'; +const renderingEngineId = 'myRenderingEngine'; +const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; + +// Volume IDs for each study +const volumeIds = { + study1: { + ct: `${volumeLoaderScheme}:CT_VOLUME_STUDY1`, + pt: `${volumeLoaderScheme}:PT_VOLUME_STUDY1`, + }, + study2: { + ct: `${volumeLoaderScheme}:CT_VOLUME_STUDY2`, + pt: `${volumeLoaderScheme}:PT_VOLUME_STUDY2`, + }, + study3: { + ct: `${volumeLoaderScheme}:CT_VOLUME_STUDY3`, + pt: `${volumeLoaderScheme}:PT_VOLUME_STUDY3`, + }, +}; + +// Tool group IDs for each study +const toolGroupIds = { + study1: { + ct: 'CT_TOOLGROUP_STUDY1', + pt: 'PT_TOOLGROUP_STUDY1', + fusion: 'FUSION_TOOLGROUP_STUDY1', + mip: 'MIP_TOOLGROUP_STUDY1', + }, + study2: { + ct: 'CT_TOOLGROUP_STUDY2', + pt: 'PT_TOOLGROUP_STUDY2', + fusion: 'FUSION_TOOLGROUP_STUDY2', + mip: 'MIP_TOOLGROUP_STUDY2', + }, + study3: { + ct: 'CT_TOOLGROUP_STUDY3', + pt: 'PT_TOOLGROUP_STUDY3', + fusion: 'FUSION_TOOLGROUP_STUDY3', + mip: 'MIP_TOOLGROUP_STUDY3', + }, +}; + +// Viewport IDs for each study +const viewportIds = { + study1: { + CT: { + AXIAL: 'CT_AXIAL_S1', + SAGITTAL: 'CT_SAGITTAL_S1', + CORONAL: 'CT_CORONAL_S1', + }, + PT: { + AXIAL: 'PT_AXIAL_S1', + SAGITTAL: 'PT_SAGITTAL_S1', + CORONAL: 'PT_CORONAL_S1', + }, + FUSION: { + AXIAL: 'FUSION_AXIAL_S1', + SAGITTAL: 'FUSION_SAGITTAL_S1', + CORONAL: 'FUSION_CORONAL_S1', + }, + PETMIP: { CORONAL: 'PET_MIP_CORONAL_S1' }, + }, + study2: { + CT: { + AXIAL: 'CT_AXIAL_S2', + SAGITTAL: 'CT_SAGITTAL_S2', + CORONAL: 'CT_CORONAL_S2', + }, + PT: { + AXIAL: 'PT_AXIAL_S2', + SAGITTAL: 'PT_SAGITTAL_S2', + CORONAL: 'PT_CORONAL_S2', + }, + FUSION: { + AXIAL: 'FUSION_AXIAL_S2', + SAGITTAL: 'FUSION_SAGITTAL_S2', + CORONAL: 'FUSION_CORONAL_S2', + }, + PETMIP: { CORONAL: 'PET_MIP_CORONAL_S2' }, + }, + study3: { + CT: { + AXIAL: 'CT_AXIAL_S3', + SAGITTAL: 'CT_SAGITTAL_S3', + CORONAL: 'CT_CORONAL_S3', + }, + PT: { + AXIAL: 'PT_AXIAL_S3', + SAGITTAL: 'PT_SAGITTAL_S3', + CORONAL: 'PT_CORONAL_S3', + }, + FUSION: { + AXIAL: 'FUSION_AXIAL_S3', + SAGITTAL: 'FUSION_SAGITTAL_S3', + CORONAL: 'FUSION_CORONAL_S3', + }, + PETMIP: { CORONAL: 'PET_MIP_CORONAL_S3' }, + }, +}; + +// Synchronizer IDs for each study +const synchronizerIds = { + study1: { + axialCamera: 'AXIAL_CAMERA_SYNC_S1', + sagittalCamera: 'SAGITTAL_CAMERA_SYNC_S1', + coronalCamera: 'CORONAL_CAMERA_SYNC_S1', + ctVoi: 'CT_VOI_SYNC_S1', + ptVoi: 'PT_VOI_SYNC_S1', + fusionVoi: 'FUSION_VOI_SYNC_S1', + }, + study2: { + axialCamera: 'AXIAL_CAMERA_SYNC_S2', + sagittalCamera: 'SAGITTAL_CAMERA_SYNC_S2', + coronalCamera: 'CORONAL_CAMERA_SYNC_S2', + ctVoi: 'CT_VOI_SYNC_S2', + ptVoi: 'PT_VOI_SYNC_S2', + fusionVoi: 'FUSION_VOI_SYNC_S2', + }, + study3: { + axialCamera: 'AXIAL_CAMERA_SYNC_S3', + sagittalCamera: 'SAGITTAL_CAMERA_SYNC_S3', + coronalCamera: 'CORONAL_CAMERA_SYNC_S3', + ctVoi: 'CT_VOI_SYNC_S3', + ptVoi: 'PT_VOI_SYNC_S3', + fusionVoi: 'FUSION_VOI_SYNC_S3', + }, +}; + +// Store volumes and synchronizers +const volumes = { + study1: { ct: null, pt: null }, + study2: { ct: null, pt: null }, + study3: { ct: null, pt: null }, +}; + +const allSynchronizers = { + study1: {}, + study2: {}, + study3: {}, +}; + +// Study configurations +const studyConfigs = [ + { + studyId: FirstStudyID, + studyKey: 'study1', + ctSeriesUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.367700692008930469189923116409', + ptSeriesUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.780462962868572737240023906400', + }, + { + studyId: SecondStudyID, + studyKey: 'study2', + ctSeriesUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + ptSeriesUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.879445243400782656317561081015', + }, + { + studyId: ThirdStudyID, + studyKey: 'study3', + ctSeriesUID: + '1.3.6.1.4.1.9328.50.17.135334071755755347832616394678950029105', + ptSeriesUID: + '1.3.6.1.4.1.9328.50.17.171167119696208952720976351719238820467', + }, +]; + +// Store DOM elements +const elements = {}; + +// Viewport colors +const viewportColors = {}; + +// Initialize viewport colors for all studies +['study1', 'study2', 'study3'].forEach((studyKey) => { + const studyViewportIds = viewportIds[studyKey]; + viewportColors[studyViewportIds.CT.AXIAL] = 'rgb(200, 0, 0)'; + viewportColors[studyViewportIds.CT.SAGITTAL] = 'rgb(200, 200, 0)'; + viewportColors[studyViewportIds.CT.CORONAL] = 'rgb(0, 200, 0)'; + viewportColors[studyViewportIds.PT.AXIAL] = 'rgb(200, 0, 0)'; + viewportColors[studyViewportIds.PT.SAGITTAL] = 'rgb(200, 200, 0)'; + viewportColors[studyViewportIds.PT.CORONAL] = 'rgb(0, 200, 0)'; + viewportColors[studyViewportIds.FUSION.AXIAL] = 'rgb(200, 0, 0)'; + viewportColors[studyViewportIds.FUSION.SAGITTAL] = 'rgb(200, 200, 0)'; + viewportColors[studyViewportIds.FUSION.CORONAL] = 'rgb(0, 200, 0)'; +}); + +// ======== Set up page ======== // +setTitleAndDescription( + 'Multi-Monitor PET-CT', + 'Three studies displayed with PET-CT fusion layout, each with separate tool groups but shared rendering engine' +); + +const optionsValues = [ + ZoomTool.toolName, + WindowLevelTool.toolName, + CrosshairsTool.toolName, + RectangleROITool.toolName, +]; + +// ============================= // +addDropdownToToolbar({ + options: { values: optionsValues, defaultValue: ZoomTool.toolName }, + onSelectedValueChange: (toolNameAsStringOrNumber) => { + const toolName = String(toolNameAsStringOrNumber); + + ['study1', 'study2', 'study3'].forEach((studyKey) => { + const studyToolGroupIds = toolGroupIds[studyKey]; + [ + studyToolGroupIds.ct, + studyToolGroupIds.pt, + studyToolGroupIds.fusion, + ].forEach((toolGroupId) => { + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + + if (toolName === ZoomTool.toolName) { + // Check if viewports are initialized before setting tools + if (renderingEngine && renderingEngine.getViewports().length > 0) { + toolGroup.setToolPassive(CrosshairsTool.toolName); + } + toolGroup.setToolDisabled(WindowLevelTool.toolName); + toolGroup.setToolDisabled(RectangleROITool.toolName); + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + } else if (toolName === WindowLevelTool.toolName) { + // Check if viewports are initialized before setting tools + if (renderingEngine && renderingEngine.getViewports().length > 0) { + toolGroup.setToolPassive(CrosshairsTool.toolName); + } + toolGroup.setToolDisabled(ZoomTool.toolName); + toolGroup.setToolDisabled(RectangleROITool.toolName); + toolGroup.setToolActive(WindowLevelTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + } else if (toolName === CrosshairsTool.toolName) { + toolGroup.setToolDisabled(ZoomTool.toolName); + toolGroup.setToolDisabled(WindowLevelTool.toolName); + toolGroup.setToolDisabled(RectangleROITool.toolName); + toolGroup.setToolActive(CrosshairsTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + } else { + toolGroup.setToolDisabled(ZoomTool.toolName); + toolGroup.setToolDisabled(WindowLevelTool.toolName); + toolGroup.setToolDisabled(CrosshairsTool.toolName); + toolGroup.setToolActive(RectangleROITool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + } + }); + }); + }, +}); + +const resizeObserver = new ResizeObserver(() => { + renderingEngine = getRenderingEngine(renderingEngineId); + + if (renderingEngine) { + renderingEngine.resize(true, false); + } +}); + +// Helper functions for crosshairs +function getReferenceLineColor(viewportId) { + return viewportColors[viewportId]; +} + +function getReferenceLineControllable(viewportId) { + return true; +} + +function getReferenceLineDraggableRotatable(viewportId) { + return true; +} + +function getReferenceLineSlabThicknessControlsOn(viewportId) { + return true; +} + +// Create viewport grid +function createViewportGrid() { + const viewportGrid = document.createElement('div'); + + viewportGrid.style.display = 'grid'; + viewportGrid.style.gridTemplateRows = `repeat(3, 33.33%)`; + viewportGrid.style.gridTemplateColumns = `repeat(12, 8.33%)`; + viewportGrid.style.width = '98vw'; + viewportGrid.style.height = '95vh'; + viewportGrid.style.gap = '2px'; + + const content = document.getElementById('content'); + content.appendChild(viewportGrid); + + // Create elements for each study + studyConfigs.forEach((config, studyIndex) => { + const studyKey = config.studyKey; + elements[studyKey] = {}; + + // Create 3x3 grid elements for each study + for (let row = 0; row < 3; row++) { + for (let col = 0; col < 3; col++) { + const element = document.createElement('div'); + element.style.width = '100%'; + element.style.height = '100%'; + element.style.border = '1px solid #333'; + element.oncontextmenu = (e) => e.preventDefault(); + + // Position in the overall grid - studies side by side + const gridRow = row + 1; + const gridColumn = studyIndex * 4 + col + 1; + element.style.gridRow = String(gridRow); + element.style.gridColumn = String(gridColumn); + + viewportGrid.appendChild(element); + resizeObserver.observe(element); + + // Store element reference + const elementKey = `element_${row + 1}_${col + 1}`; + elements[studyKey][elementKey] = element; + } + } + + // Create MIP element + const mipElement = document.createElement('div'); + mipElement.style.width = '100%'; + mipElement.style.height = '100%'; + mipElement.style.border = '1px solid #333'; + mipElement.oncontextmenu = (e) => e.preventDefault(); + + // Position MIP in the 4th column of each study, spanning 3 rows + mipElement.style.gridRow = `1 / span 3`; + mipElement.style.gridColumn = String(studyIndex * 4 + 4); + + viewportGrid.appendChild(mipElement); + resizeObserver.observe(mipElement); + elements[studyKey].element_mip = mipElement; + }); + + return viewportGrid; +} + +// Set up tool groups for a study +function setUpToolGroupsForStudy(studyKey) { + const studyToolGroupIds = toolGroupIds[studyKey]; + const studyViewportIds = viewportIds[studyKey]; + const studyVolumeIds = volumeIds[studyKey]; + + // Create tool groups + const ctToolGroup = ToolGroupManager.createToolGroup(studyToolGroupIds.ct); + const ptToolGroup = ToolGroupManager.createToolGroup(studyToolGroupIds.pt); + const fusionToolGroup = ToolGroupManager.createToolGroup( + studyToolGroupIds.fusion + ); + const mipToolGroup = ToolGroupManager.createToolGroup(studyToolGroupIds.mip); + + // Add viewports to tool groups + ctToolGroup.addViewport(studyViewportIds.CT.AXIAL, renderingEngineId); + ctToolGroup.addViewport(studyViewportIds.CT.SAGITTAL, renderingEngineId); + ctToolGroup.addViewport(studyViewportIds.CT.CORONAL, renderingEngineId); + + ptToolGroup.addViewport(studyViewportIds.PT.AXIAL, renderingEngineId); + ptToolGroup.addViewport(studyViewportIds.PT.SAGITTAL, renderingEngineId); + ptToolGroup.addViewport(studyViewportIds.PT.CORONAL, renderingEngineId); + + fusionToolGroup.addViewport(studyViewportIds.FUSION.AXIAL, renderingEngineId); + fusionToolGroup.addViewport( + studyViewportIds.FUSION.SAGITTAL, + renderingEngineId + ); + fusionToolGroup.addViewport( + studyViewportIds.FUSION.CORONAL, + renderingEngineId + ); + + // Add tools to CT and PT groups + [ctToolGroup, ptToolGroup].forEach((toolGroup) => { + toolGroup.addTool(WindowLevelTool.toolName); + toolGroup.addTool(PanTool.toolName); + toolGroup.addTool(ZoomTool.toolName); + toolGroup.addTool(StackScrollTool.toolName); + toolGroup.addTool(CrosshairsTool.toolName, { + getReferenceLineColor, + getReferenceLineControllable, + getReferenceLineDraggableRotatable, + getReferenceLineSlabThicknessControlsOn, + }); + toolGroup.addTool(RectangleROITool.toolName); + }); + + // Add tools to fusion group + fusionToolGroup.addTool(WindowLevelTool.toolName); + fusionToolGroup.addTool(PanTool.toolName); + fusionToolGroup.addTool(ZoomTool.toolName); + fusionToolGroup.addTool(StackScrollTool.toolName); + fusionToolGroup.addTool(CrosshairsTool.toolName, { + getReferenceLineColor, + getReferenceLineControllable, + getReferenceLineDraggableRotatable, + getReferenceLineSlabThicknessControlsOn, + filterActorUIDsToSetSlabThickness: [studyVolumeIds.ct], + }); + fusionToolGroup.addTool(RectangleROITool.toolName); + + // Set active tools + [ctToolGroup, ptToolGroup, fusionToolGroup].forEach((toolGroup) => { + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + toolGroup.setToolActive(PanTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Auxiliary }], + }); + toolGroup.setToolActive(WindowLevelTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Secondary }], + }); + toolGroup.setToolActive(StackScrollTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Wheel }], + }); + // Don't set CrosshairsTool to passive here - do it after viewports are created + }); + + // MIP Tool Group + mipToolGroup.addTool(VolumeRotateTool.toolName); + mipToolGroup.setToolActive(VolumeRotateTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Wheel }], + }); + mipToolGroup.addTool(MIPJumpToClickTool.toolName, { + toolGroupId: studyToolGroupIds.pt, + }); + mipToolGroup.setToolActive(MIPJumpToClickTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + mipToolGroup.addViewport(studyViewportIds.PETMIP.CORONAL, renderingEngineId); +} + +// Set up synchronizers for a study +function setUpSynchronizersForStudy(studyKey) { + const studySynchronizerIds = synchronizerIds[studyKey]; + const studyViewportIds = viewportIds[studyKey]; + + // Create synchronizers + const axialCameraSync = createCameraPositionSynchronizer( + studySynchronizerIds.axialCamera + ); + const sagittalCameraSync = createCameraPositionSynchronizer( + studySynchronizerIds.sagittalCamera + ); + const coronalCameraSync = createCameraPositionSynchronizer( + studySynchronizerIds.coronalCamera + ); + const ctVoiSync = createVOISynchronizer(studySynchronizerIds.ctVoi, { + syncInvertState: false, + syncColormap: false, + }); + const ptVoiSync = createVOISynchronizer(studySynchronizerIds.ptVoi, { + syncInvertState: false, + syncColormap: false, + }); + const fusionVoiSync = createVOISynchronizer(studySynchronizerIds.fusionVoi, { + syncInvertState: false, + syncColormap: false, + }); + + // Store synchronizers + allSynchronizers[studyKey] = { + axialCamera: axialCameraSync, + sagittalCamera: sagittalCameraSync, + coronalCamera: coronalCameraSync, + ctVoi: ctVoiSync, + ptVoi: ptVoiSync, + fusionVoi: fusionVoiSync, + }; + + // Add viewports to camera synchronizers + [ + studyViewportIds.CT.AXIAL, + studyViewportIds.PT.AXIAL, + studyViewportIds.FUSION.AXIAL, + ].forEach((viewportId) => { + axialCameraSync.add({ renderingEngineId, viewportId }); + }); + + [ + studyViewportIds.CT.SAGITTAL, + studyViewportIds.PT.SAGITTAL, + studyViewportIds.FUSION.SAGITTAL, + ].forEach((viewportId) => { + sagittalCameraSync.add({ renderingEngineId, viewportId }); + }); + + [ + studyViewportIds.CT.CORONAL, + studyViewportIds.PT.CORONAL, + studyViewportIds.FUSION.CORONAL, + ].forEach((viewportId) => { + coronalCameraSync.add({ renderingEngineId, viewportId }); + }); + + // Add viewports to VOI synchronizers + [ + studyViewportIds.CT.AXIAL, + studyViewportIds.CT.SAGITTAL, + studyViewportIds.CT.CORONAL, + ].forEach((viewportId) => { + ctVoiSync.add({ renderingEngineId, viewportId }); + }); + + [ + studyViewportIds.PT.AXIAL, + studyViewportIds.PT.SAGITTAL, + studyViewportIds.PT.CORONAL, + studyViewportIds.PETMIP.CORONAL, + ].forEach((viewportId) => { + ptVoiSync.add({ renderingEngineId, viewportId }); + }); + + [ + studyViewportIds.FUSION.AXIAL, + studyViewportIds.FUSION.SAGITTAL, + studyViewportIds.FUSION.CORONAL, + ].forEach((viewportId) => { + fusionVoiSync.add({ renderingEngineId, viewportId }); + ctVoiSync.addTarget({ renderingEngineId, viewportId }); + ptVoiSync.addTarget({ renderingEngineId, viewportId }); + }); +} + +// Initialize camera synchronization +function initCameraSynchronization(sViewport, tViewport) { + const camera = sViewport.getCamera(); + tViewport.setCamera(camera); +} + +// Initialize camera sync for a study +function initializeCameraSyncForStudy(studyKey) { + const studyViewportIds = viewportIds[studyKey]; + + const axialCtViewport = renderingEngine.getViewport( + studyViewportIds.CT.AXIAL + ); + const sagittalCtViewport = renderingEngine.getViewport( + studyViewportIds.CT.SAGITTAL + ); + const coronalCtViewport = renderingEngine.getViewport( + studyViewportIds.CT.CORONAL + ); + + const axialPtViewport = renderingEngine.getViewport( + studyViewportIds.PT.AXIAL + ); + const sagittalPtViewport = renderingEngine.getViewport( + studyViewportIds.PT.SAGITTAL + ); + const coronalPtViewport = renderingEngine.getViewport( + studyViewportIds.PT.CORONAL + ); + + const axialFusionViewport = renderingEngine.getViewport( + studyViewportIds.FUSION.AXIAL + ); + const sagittalFusionViewport = renderingEngine.getViewport( + studyViewportIds.FUSION.SAGITTAL + ); + const coronalFusionViewport = renderingEngine.getViewport( + studyViewportIds.FUSION.CORONAL + ); + + initCameraSynchronization(axialFusionViewport, axialCtViewport); + initCameraSynchronization(axialFusionViewport, axialPtViewport); + + initCameraSynchronization(sagittalFusionViewport, sagittalCtViewport); + initCameraSynchronization(sagittalFusionViewport, sagittalPtViewport); + + initCameraSynchronization(coronalFusionViewport, coronalCtViewport); + initCameraSynchronization(coronalFusionViewport, coronalPtViewport); +} + +// Load image IDs for a study +async function getImageIdsForStudy(config) { + const ctImageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: config.studyId, + SeriesInstanceUID: config.ctSeriesUID, + wadoRsRoot, + }); + + const ptImageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: config.studyId, + SeriesInstanceUID: config.ptSeriesUID, + wadoRsRoot, + }); + + return { ctImageIds, ptImageIds }; +} + +// Create viewport input array for a study +function createViewportInputArrayForStudy(config, studyIndex) { + const studyKey = config.studyKey; + const studyElements = elements[studyKey]; + const studyViewportIds = viewportIds[studyKey]; + + // Create viewport input array + const viewportInputArray = [ + { + viewportId: studyViewportIds.CT.AXIAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_1_1, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + }, + }, + { + viewportId: studyViewportIds.CT.SAGITTAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_1_2, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + }, + }, + { + viewportId: studyViewportIds.CT.CORONAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_1_3, + defaultOptions: { + orientation: Enums.OrientationAxis.CORONAL, + }, + }, + { + viewportId: studyViewportIds.PT.AXIAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_2_1, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + background: [1, 1, 1], + }, + }, + { + viewportId: studyViewportIds.PT.SAGITTAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_2_2, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + background: [1, 1, 1], + }, + }, + { + viewportId: studyViewportIds.PT.CORONAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_2_3, + defaultOptions: { + orientation: Enums.OrientationAxis.CORONAL, + background: [1, 1, 1], + }, + }, + { + viewportId: studyViewportIds.FUSION.AXIAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_3_1, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + }, + }, + { + viewportId: studyViewportIds.FUSION.SAGITTAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_3_2, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + }, + }, + { + viewportId: studyViewportIds.FUSION.CORONAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_3_3, + defaultOptions: { + orientation: Enums.OrientationAxis.CORONAL, + }, + }, + { + viewportId: studyViewportIds.PETMIP.CORONAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_mip, + defaultOptions: { + orientation: Enums.OrientationAxis.CORONAL, + background: [1, 1, 1], + }, + }, + ]; + + return viewportInputArray; +} + +// Set up display for a study +async function setUpDisplayForStudy(config, studyIndex) { + const studyKey = config.studyKey; + const studyViewportIds = viewportIds[studyKey]; + const studyVolumeIds = volumeIds[studyKey]; + + // Set volumes on the viewports + await setVolumesForViewports( + renderingEngine, + [ + { + volumeId: studyVolumeIds.ct, + callback: setCtTransferFunctionForVolumeActor, + }, + ], + [ + studyViewportIds.CT.AXIAL, + studyViewportIds.CT.SAGITTAL, + studyViewportIds.CT.CORONAL, + ] + ); + + await setVolumesForViewports( + renderingEngine, + [ + { + volumeId: studyVolumeIds.pt, + callback: setPetTransferFunctionForVolumeActor, + }, + ], + [ + studyViewportIds.PT.AXIAL, + studyViewportIds.PT.SAGITTAL, + studyViewportIds.PT.CORONAL, + ] + ); + + await setVolumesForViewports( + renderingEngine, + [ + { + volumeId: studyVolumeIds.ct, + callback: setCtTransferFunctionForVolumeActor, + }, + { + volumeId: studyVolumeIds.pt, + callback: setPetColorMapTransferFunctionForVolumeActor, + }, + ], + [ + studyViewportIds.FUSION.AXIAL, + studyViewportIds.FUSION.SAGITTAL, + studyViewportIds.FUSION.CORONAL, + ] + ); + + // Set up MIP + const ptVolume = volumes[studyKey].pt; + const ptVolumeDimensions = ptVolume.dimensions; + + const slabThickness = Math.sqrt( + ptVolumeDimensions[0] * ptVolumeDimensions[0] + + ptVolumeDimensions[1] * ptVolumeDimensions[1] + + ptVolumeDimensions[2] * ptVolumeDimensions[2] + ); + + await setVolumesForViewports( + renderingEngine, + [ + { + volumeId: studyVolumeIds.pt, + callback: setPetTransferFunctionForVolumeActor, + blendMode: BlendModes.MAXIMUM_INTENSITY_BLEND, + slabThickness, + }, + ], + [studyViewportIds.PETMIP.CORONAL] + ); + + initializeCameraSyncForStudy(studyKey); +} + +// Set crosshairs to passive after viewports are set up +function setCrosshairsToPassive() { + studyConfigs.forEach((config) => { + const studyKey = config.studyKey; + const studyToolGroupIds = toolGroupIds[studyKey]; + + [ + studyToolGroupIds.ct, + studyToolGroupIds.pt, + studyToolGroupIds.fusion, + ].forEach((toolGroupId) => { + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + if (toolGroup) { + toolGroup.setToolPassive(CrosshairsTool.toolName); + } + }); + }); +} + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + // Add tools to Cornerstone3D + cornerstoneTools.addTool(WindowLevelTool); + cornerstoneTools.addTool(PanTool); + cornerstoneTools.addTool(ZoomTool); + cornerstoneTools.addTool(StackScrollTool); + cornerstoneTools.addTool(MIPJumpToClickTool); + cornerstoneTools.addTool(CrosshairsTool); + cornerstoneTools.addTool(TrackballRotateTool); + cornerstoneTools.addTool(VolumeRotateTool); + cornerstoneTools.addTool(RectangleROITool); + + // Instantiate a rendering engine + renderingEngine = new RenderingEngine(renderingEngineId); + + // Create viewport grid + createViewportGrid(); + + // Load all volumes and set up displays + for (const config of studyConfigs) { + const studyKey = config.studyKey; + + // Get image IDs + const { ctImageIds, ptImageIds } = await getImageIdsForStudy(config); + + // Create and cache volumes + volumes[studyKey].ct = await volumeLoader.createAndCacheVolume( + volumeIds[studyKey].ct, + { imageIds: ctImageIds } + ); + volumes[studyKey].pt = await volumeLoader.createAndCacheVolume( + volumeIds[studyKey].pt, + { imageIds: ptImageIds } + ); + + // Load volumes + volumes[studyKey].ct.load(); + volumes[studyKey].pt.load(); + } + + // Set up tool groups and synchronizers for each study + for (const config of studyConfigs) { + const studyKey = config.studyKey; + setUpToolGroupsForStudy(studyKey); + setUpSynchronizersForStudy(studyKey); + } + + // Collect all viewport configurations + const allViewportInputs = []; + for (let i = 0; i < studyConfigs.length; i++) { + const viewportInputs = createViewportInputArrayForStudy(studyConfigs[i], i); + allViewportInputs.push(...viewportInputs); + } + + // Set all viewports at once + renderingEngine.setViewports(allViewportInputs); + + // Set up displays for all studies + for (let i = 0; i < studyConfigs.length; i++) { + await setUpDisplayForStudy(studyConfigs[i], i); + } + + // Set crosshairs to passive after all viewports are initialized + setCrosshairsToPassive(); + + // Render all viewports + renderingEngine.render(); +} + +run(); diff --git a/packages/core/examples/stackBasic/index.ts b/packages/core/examples/stackBasic/index.ts index c34595a60d..52a09de1ee 100644 --- a/packages/core/examples/stackBasic/index.ts +++ b/packages/core/examples/stackBasic/index.ts @@ -36,7 +36,10 @@ content.appendChild(element); */ async function run() { // Init Cornerstone and related libraries - await initDemo(); + const config = (window as any).IS_TILED + ? { core: { renderingEngineMode: 'tiled' } } + : {}; + await initDemo(config); // Get Cornerstone imageIds and fetch metadata into RAM const imageIds = await createImageIdsAndCacheMetaData({ diff --git a/packages/core/examples/stacktovolume/index.ts b/packages/core/examples/stacktovolume/index.ts new file mode 100644 index 0000000000..e5da28cd9d --- /dev/null +++ b/packages/core/examples/stacktovolume/index.ts @@ -0,0 +1,189 @@ +import type { Types } from '@cornerstonejs/core'; +import { + RenderingEngine, + Enums, + volumeLoader, + getRenderingEngine, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + addButtonToToolbar, + ctVoiRange, + setCtTransferFunctionForVolumeActor, +} from '../../../../utils/demo/helpers'; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +const { ViewportType } = Enums; + +// ======== Constants ======= // +const renderingEngineId = 'myRenderingEngine'; +const viewportId = 'CT_VIEWPORT'; + +// Define a unique id for the volume +const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix +const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use +const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id + +// State +let isVolumeViewport = false; + +// ======== Set up page ======== // +setTitleAndDescription( + 'Stack to Volume Conversion', + 'Demonstrates converting between Stack and Volume viewports with a reset button.' +); + +const content = document.getElementById('content'); +const element = document.createElement('div'); +element.id = 'cornerstone-element'; +element.style.width = '500px'; +element.style.height = '500px'; + +content.appendChild(element); + +const info = document.createElement('div'); +content.appendChild(info); + +const viewportTypeInfo = document.createElement('div'); +viewportTypeInfo.innerText = 'Viewport Type: Stack'; +info.appendChild(viewportTypeInfo); + +// Store imageIds globally +let imageIds: string[] = []; + +// Reset Viewport button +addButtonToToolbar({ + title: 'Reset Viewport', + onClick: () => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the viewport + const viewport = renderingEngine.getViewport(viewportId); + + if (!viewport) { + return; + } + + viewport.resetProperties?.(); + + viewport.resetCamera(); + viewport.render(); + }, +}); + +// Convert to Volume button +const convertButton = addButtonToToolbar({ + title: 'Convert to Volume', + onClick: async () => { + if (isVolumeViewport) { + return; // Already a volume viewport + } + + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Disable the current viewport + renderingEngine.disableElement(viewportId); + + // Create a volume viewport + const viewportInput = { + viewportId, + type: ViewportType.ORTHOGRAPHIC, + element, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + background: [0.2, 0, 0.2] as Types.Point3, + }, + }; + + renderingEngine.enableElement(viewportInput); + + // Get the volume viewport that was created + const viewport = renderingEngine.getViewport( + viewportId + ) as Types.IVolumeViewport; + + // Create and load the volume + const volume = await volumeLoader.createAndCacheVolume(volumeId, { + imageIds, + }); + + // Set the volume to load + volume.load(); + + // Set the volume on the viewport + await viewport.setVolumes([ + { volumeId, callback: setCtTransferFunctionForVolumeActor }, + ]); + + // Set the VOI range + viewport.setProperties({ voiRange: ctVoiRange }); + + // Render the image + viewport.render(); + + // Update state and UI + isVolumeViewport = true; + viewportTypeInfo.innerText = 'Viewport Type: Volume'; + + // Gray out the convert button + convertButton.disabled = true; + convertButton.style.opacity = '0.5'; + convertButton.style.cursor = 'not-allowed'; + }, +}); + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + // Get Cornerstone imageIds and fetch metadata into RAM + imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + SeriesInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + wadoRsRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + }); + + // Instantiate a rendering engine + const renderingEngine = new RenderingEngine(renderingEngineId); + + // Create a stack viewport initially + const viewportInput = { + viewportId, + type: ViewportType.STACK, + element, + defaultOptions: { + background: [0.2, 0, 0.2] as Types.Point3, + }, + }; + + renderingEngine.enableElement(viewportInput); + + // Get the stack viewport that was created + const viewport = renderingEngine.getViewport( + viewportId + ) as Types.IStackViewport; + + // Set the stack on the viewport + viewport.setStack(imageIds); + + // Set the VOI of the stack + viewport.setProperties({ voiRange: ctVoiRange }); + + // Render the image + viewport.render(); +} + +run(); diff --git a/packages/core/examples/tmtv/index.ts b/packages/core/examples/tmtv/index.ts new file mode 100644 index 0000000000..28d6b24683 --- /dev/null +++ b/packages/core/examples/tmtv/index.ts @@ -0,0 +1,984 @@ +import type { Types } from '@cornerstonejs/core'; +import { + RenderingEngine, + Enums, + setVolumesForViewports, + volumeLoader, + getRenderingEngine, + cache, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + setPetColorMapTransferFunctionForVolumeActor, + setPetTransferFunctionForVolumeActor, + setCtTransferFunctionForVolumeActor, + addDropdownToToolbar, + addButtonToToolbar, +} from '../../../../utils/demo/helpers'; +import * as cornerstoneTools from '@cornerstonejs/tools'; + +const { + ToolGroupManager, + Enums: csToolsEnums, + WindowLevelTool, + PanTool, + ZoomTool, + StackScrollTool, + synchronizers, + MIPJumpToClickTool, + CrosshairsTool, + TrackballRotateTool, + VolumeRotateTool, + RectangleROITool, + CircleROIStartEndThresholdTool, + RectangleROIStartEndThresholdTool, + segmentation, +} = cornerstoneTools; + +const { MouseBindings } = csToolsEnums; +const { ViewportType, BlendModes } = Enums; + +const { createCameraPositionSynchronizer, createVOISynchronizer } = + synchronizers; + +// Study IDs +const FirstStudyID = `1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339`; +const SecondStudyID = + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463'; +const ThirdStudyID = `1.3.6.1.4.1.9328.50.17.15423521354819720574322014551955370036`; + +// Common configuration +let renderingEngine; +const wadoRsRoot = 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb'; +const renderingEngineId = 'myRenderingEngine'; +const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; + +// Define a unique id for the volume +const volumeName = 'labelmap'; // Id of the volume less loader prefix +const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id + +const segmentationId = 'MY_SEGMENTATION_ID'; + +// Volume IDs for each study +const volumeIds = { + study1: { + ct: `${volumeLoaderScheme}:CT_VOLUME_STUDY1`, + pt: `${volumeLoaderScheme}:PT_VOLUME_STUDY1`, + }, +}; + +// Tool group IDs for each study +const toolGroupIds = { + study1: { + ct: 'CT_TOOLGROUP_STUDY1', + pt: 'PT_TOOLGROUP_STUDY1', + fusion: 'FUSION_TOOLGROUP_STUDY1', + mip: 'MIP_TOOLGROUP_STUDY1', + }, +}; + +// Viewport IDs for each study +const viewportIds = { + study1: { + CT: { + AXIAL: 'CT_AXIAL_S1', + SAGITTAL: 'CT_SAGITTAL_S1', + CORONAL: 'CT_CORONAL_S1', + }, + PT: { + AXIAL: 'PT_AXIAL_S1', + SAGITTAL: 'PT_SAGITTAL_S1', + CORONAL: 'PT_CORONAL_S1', + }, + FUSION: { + AXIAL: 'FUSION_AXIAL_S1', + SAGITTAL: 'FUSION_SAGITTAL_S1', + CORONAL: 'FUSION_CORONAL_S1', + }, + PETMIP: { CORONAL: 'PET_MIP_CORONAL_S1' }, + }, +}; + +// Synchronizer IDs for each study +const synchronizerIds = { + study1: { + axialCamera: 'AXIAL_CAMERA_SYNC_S1', + sagittalCamera: 'SAGITTAL_CAMERA_SYNC_S1', + coronalCamera: 'CORONAL_CAMERA_SYNC_S1', + ctVoi: 'CT_VOI_SYNC_S1', + ptVoi: 'PT_VOI_SYNC_S1', + fusionVoi: 'FUSION_VOI_SYNC_S1', + }, +}; + +// Store volumes and synchronizers +const volumes = { + study1: { ct: null, pt: null }, + study2: { ct: null, pt: null }, + study3: { ct: null, pt: null }, +}; + +const allSynchronizers = { + study1: {}, + study2: {}, + study3: {}, +}; + +// Study configurations +const studyConfigs = [ + { + studyId: FirstStudyID, + studyKey: 'study1', + ctSeriesUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.367700692008930469189923116409', + ptSeriesUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.780462962868572737240023906400', + }, +]; + +// Store DOM elements +const elements = {}; + +// Viewport colors +const viewportColors = {}; + +// Initialize viewport colors for all studies +['study1'].forEach((studyKey) => { + const studyViewportIds = viewportIds[studyKey]; + viewportColors[studyViewportIds.CT.AXIAL] = 'rgb(200, 0, 0)'; + viewportColors[studyViewportIds.CT.SAGITTAL] = 'rgb(200, 200, 0)'; + viewportColors[studyViewportIds.CT.CORONAL] = 'rgb(0, 200, 0)'; + viewportColors[studyViewportIds.PT.AXIAL] = 'rgb(200, 0, 0)'; + viewportColors[studyViewportIds.PT.SAGITTAL] = 'rgb(200, 200, 0)'; + viewportColors[studyViewportIds.PT.CORONAL] = 'rgb(0, 200, 0)'; + viewportColors[studyViewportIds.FUSION.AXIAL] = 'rgb(200, 0, 0)'; + viewportColors[studyViewportIds.FUSION.SAGITTAL] = 'rgb(200, 200, 0)'; + viewportColors[studyViewportIds.FUSION.CORONAL] = 'rgb(0, 200, 0)'; +}); + +// ======== Set up page ======== // +setTitleAndDescription( + 'Multi-Monitor PET-CT', + 'Three studies displayed with PET-CT fusion layout, each with separate tool groups but shared rendering engine' +); + +const optionsValues = [ + ZoomTool.toolName, + WindowLevelTool.toolName, + CrosshairsTool.toolName, + RectangleROITool.toolName, + CircleROIStartEndThresholdTool.toolName, + RectangleROIStartEndThresholdTool.toolName, +]; + +addButtonToToolbar({ + title: 'Run Segmentation', + onClick: () => { + const annotations = cornerstoneTools.annotation.state.getAllAnnotations(); + const labelmapVolume = cache.getVolume(segmentationId); + console.debug(annotations); + annotations.map((annotation, i) => { + // @ts-ignore + const pointsInVolume = annotation.data.cachedStats.pointsInVolume; + for (let i = 0; i < pointsInVolume.length; i++) { + for (let j = 0; j < pointsInVolume[i].length; j++) { + if (pointsInVolume[i][j].value > 2) { + labelmapVolume.voxelManager.setAtIndex( + pointsInVolume[i][j].index, + 1 + ); + } + } + } + }); + + cornerstoneTools.segmentation.triggerSegmentationEvents.triggerSegmentationDataModified( + labelmapVolume.volumeId + ); + labelmapVolume.modified(); + }, +}); + +// ============================= // +addDropdownToToolbar({ + options: { values: optionsValues, defaultValue: ZoomTool.toolName }, + onSelectedValueChange: (toolNameAsStringOrNumber) => { + const toolName = String(toolNameAsStringOrNumber); + + ['study1'].forEach((studyKey) => { + const studyToolGroupIds = toolGroupIds[studyKey]; + [ + studyToolGroupIds.ct, + studyToolGroupIds.pt, + studyToolGroupIds.fusion, + ].forEach((toolGroupId) => { + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + + if (toolName === ZoomTool.toolName) { + // Check if viewports are initialized before setting tools + if (renderingEngine && renderingEngine.getViewports().length > 0) { + toolGroup.setToolPassive(CrosshairsTool.toolName); + } + toolGroup.setToolDisabled(WindowLevelTool.toolName); + toolGroup.setToolDisabled(RectangleROITool.toolName); + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + } else if (toolName === WindowLevelTool.toolName) { + // Check if viewports are initialized before setting tools + if (renderingEngine && renderingEngine.getViewports().length > 0) { + toolGroup.setToolPassive(CrosshairsTool.toolName); + } + toolGroup.setToolDisabled(ZoomTool.toolName); + toolGroup.setToolDisabled(RectangleROITool.toolName); + toolGroup.setToolActive(WindowLevelTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + } else if (toolName === CrosshairsTool.toolName) { + toolGroup.setToolDisabled(ZoomTool.toolName); + toolGroup.setToolDisabled(WindowLevelTool.toolName); + toolGroup.setToolDisabled(RectangleROITool.toolName); + toolGroup.setToolActive(CrosshairsTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + } else if (toolName === CircleROIStartEndThresholdTool.toolName) { + toolGroup.setToolDisabled(ZoomTool.toolName); + toolGroup.setToolDisabled(WindowLevelTool.toolName); + toolGroup.setToolDisabled(CrosshairsTool.toolName); + toolGroup.setToolDisabled(RectangleROITool.toolName); + toolGroup.setToolActive(CircleROIStartEndThresholdTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + } else if (toolName === RectangleROIStartEndThresholdTool.toolName) { + toolGroup.setToolDisabled(ZoomTool.toolName); + toolGroup.setToolDisabled(WindowLevelTool.toolName); + toolGroup.setToolDisabled(CrosshairsTool.toolName); + toolGroup.setToolDisabled(RectangleROITool.toolName); + toolGroup.setToolDisabled(CircleROIStartEndThresholdTool.toolName); + toolGroup.setToolActive(RectangleROIStartEndThresholdTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + } else { + toolGroup.setToolDisabled(ZoomTool.toolName); + toolGroup.setToolDisabled(WindowLevelTool.toolName); + toolGroup.setToolDisabled(CrosshairsTool.toolName); + toolGroup.setToolActive(RectangleROITool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + } + }); + }); + }, +}); + +const resizeObserver = new ResizeObserver(() => { + renderingEngine = getRenderingEngine(renderingEngineId); + + if (renderingEngine) { + renderingEngine.resize(true, false); + } +}); + +// Helper functions for crosshairs +function getReferenceLineColor(viewportId) { + return viewportColors[viewportId]; +} + +function getReferenceLineControllable(viewportId) { + return true; +} + +function getReferenceLineDraggableRotatable(viewportId) { + return true; +} + +function getReferenceLineSlabThicknessControlsOn(viewportId) { + return true; +} + +// Create viewport grid +function createViewportGrid() { + const viewportGrid = document.createElement('div'); + + viewportGrid.style.display = 'grid'; + viewportGrid.style.gridTemplateRows = `repeat(3, 33.33%)`; + viewportGrid.style.gridTemplateColumns = `repeat(12, 8.33%)`; + viewportGrid.style.width = '200vw'; + viewportGrid.style.height = '95vh'; + viewportGrid.style.gap = '2px'; + + const content = document.getElementById('content'); + content.appendChild(viewportGrid); + + // Create elements for each study + studyConfigs.forEach((config, studyIndex) => { + const studyKey = config.studyKey; + elements[studyKey] = {}; + + // Create 3x3 grid elements for each study + for (let row = 0; row < 3; row++) { + for (let col = 0; col < 3; col++) { + const element = document.createElement('div'); + element.style.width = '100%'; + element.style.height = '100%'; + element.style.border = '1px solid #333'; + element.oncontextmenu = (e) => e.preventDefault(); + + // Position in the overall grid - studies side by side + const gridRow = row + 1; + const gridColumn = studyIndex * 4 + col + 1; + element.style.gridRow = String(gridRow); + element.style.gridColumn = String(gridColumn); + + viewportGrid.appendChild(element); + resizeObserver.observe(element); + + // Store element reference + const elementKey = `element_${row + 1}_${col + 1}`; + elements[studyKey][elementKey] = element; + } + } + + // Create MIP element + const mipElement = document.createElement('div'); + mipElement.style.width = '100%'; + mipElement.style.height = '100%'; + mipElement.style.border = '1px solid #333'; + mipElement.oncontextmenu = (e) => e.preventDefault(); + + // Position MIP in the 4th column of each study, spanning 3 rows + mipElement.style.gridRow = `1 / span 3`; + mipElement.style.gridColumn = String(studyIndex * 4 + 4); + + viewportGrid.appendChild(mipElement); + resizeObserver.observe(mipElement); + elements[studyKey].element_mip = mipElement; + }); + + return viewportGrid; +} + +// Set up tool groups for a study +function setUpToolGroupsForStudy(studyKey) { + const studyToolGroupIds = toolGroupIds[studyKey]; + const studyViewportIds = viewportIds[studyKey]; + const studyVolumeIds = volumeIds[studyKey]; + + // Create tool groups + const ctToolGroup = ToolGroupManager.createToolGroup(studyToolGroupIds.ct); + const ptToolGroup = ToolGroupManager.createToolGroup(studyToolGroupIds.pt); + const fusionToolGroup = ToolGroupManager.createToolGroup( + studyToolGroupIds.fusion + ); + const mipToolGroup = ToolGroupManager.createToolGroup(studyToolGroupIds.mip); + + // Add viewports to tool groups + ctToolGroup.addViewport(studyViewportIds.CT.AXIAL, renderingEngineId); + ctToolGroup.addViewport(studyViewportIds.CT.SAGITTAL, renderingEngineId); + ctToolGroup.addViewport(studyViewportIds.CT.CORONAL, renderingEngineId); + + ptToolGroup.addViewport(studyViewportIds.PT.AXIAL, renderingEngineId); + ptToolGroup.addViewport(studyViewportIds.PT.SAGITTAL, renderingEngineId); + ptToolGroup.addViewport(studyViewportIds.PT.CORONAL, renderingEngineId); + + fusionToolGroup.addViewport(studyViewportIds.FUSION.AXIAL, renderingEngineId); + fusionToolGroup.addViewport( + studyViewportIds.FUSION.SAGITTAL, + renderingEngineId + ); + fusionToolGroup.addViewport( + studyViewportIds.FUSION.CORONAL, + renderingEngineId + ); + + // Add tools to CT and PT groups + [ctToolGroup, ptToolGroup].forEach((toolGroup) => { + toolGroup.addTool(WindowLevelTool.toolName); + toolGroup.addTool(PanTool.toolName); + toolGroup.addTool(ZoomTool.toolName); + toolGroup.addTool(StackScrollTool.toolName); + toolGroup.addTool(CrosshairsTool.toolName, { + getReferenceLineColor, + getReferenceLineControllable, + getReferenceLineDraggableRotatable, + getReferenceLineSlabThicknessControlsOn, + }); + toolGroup.addTool(RectangleROITool.toolName); + // if (toolGroup === ptToolGroup) { + toolGroup.addTool(CircleROIStartEndThresholdTool.toolName, { + calculatePointsInsideVolume: true, + showTextBox: false, + storePointData: true, + /* Set a custom wait time */ + throttleTimeout: 100, + /* Simplified handles */ + simplified: false, + }); + toolGroup.addTool(RectangleROIStartEndThresholdTool.toolName, { + calculatePointsInsideVolume: true, + showTextBox: false, + storePointData: true, + /* Set a custom wait time */ + throttleTimeout: 100, + /* Simplified handles */ + simplified: false, + }); + // } + }); + + // Add tools to fusion group + fusionToolGroup.addTool(WindowLevelTool.toolName); + fusionToolGroup.addTool(PanTool.toolName); + fusionToolGroup.addTool(ZoomTool.toolName); + fusionToolGroup.addTool(StackScrollTool.toolName); + fusionToolGroup.addTool(CrosshairsTool.toolName, { + getReferenceLineColor, + getReferenceLineControllable, + getReferenceLineDraggableRotatable, + getReferenceLineSlabThicknessControlsOn, + filterActorUIDsToSetSlabThickness: [studyVolumeIds.ct], + }); + fusionToolGroup.addTool(RectangleROITool.toolName); + fusionToolGroup.addTool(CircleROIStartEndThresholdTool.toolName, { + calculatePointsInsideVolume: true, + showTextBox: false, + storePointData: true, + /* Set a custom wait time */ + throttleTimeout: 100, + /* Simplified handles */ + simplified: false, + }); + fusionToolGroup.addTool(RectangleROIStartEndThresholdTool.toolName, { + calculatePointsInsideVolume: true, + showTextBox: false, + storePointData: true, + /* Set a custom wait time */ + throttleTimeout: 100, + /* Simplified handles */ + simplified: false, + }); + + // Set active tools + [ctToolGroup, ptToolGroup, fusionToolGroup].forEach((toolGroup) => { + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + toolGroup.setToolActive(PanTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Auxiliary }], + }); + toolGroup.setToolActive(WindowLevelTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Secondary }], + }); + toolGroup.setToolActive(StackScrollTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Wheel }], + }); + // Don't set CrosshairsTool to passive here - do it after viewports are created + }); + + // MIP Tool Group + mipToolGroup.addTool(VolumeRotateTool.toolName); + mipToolGroup.setToolActive(VolumeRotateTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Wheel }], + }); + mipToolGroup.addTool(MIPJumpToClickTool.toolName, { + toolGroupId: studyToolGroupIds.pt, + }); + mipToolGroup.setToolActive(MIPJumpToClickTool.toolName, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + mipToolGroup.addViewport(studyViewportIds.PETMIP.CORONAL, renderingEngineId); +} + +// Set up synchronizers for a study +function setUpSynchronizersForStudy(studyKey) { + const studySynchronizerIds = synchronizerIds[studyKey]; + const studyViewportIds = viewportIds[studyKey]; + + // Create synchronizers + const axialCameraSync = createCameraPositionSynchronizer( + studySynchronizerIds.axialCamera + ); + const sagittalCameraSync = createCameraPositionSynchronizer( + studySynchronizerIds.sagittalCamera + ); + const coronalCameraSync = createCameraPositionSynchronizer( + studySynchronizerIds.coronalCamera + ); + const ctVoiSync = createVOISynchronizer(studySynchronizerIds.ctVoi, { + syncInvertState: false, + syncColormap: false, + }); + const ptVoiSync = createVOISynchronizer(studySynchronizerIds.ptVoi, { + syncInvertState: false, + syncColormap: false, + }); + const fusionVoiSync = createVOISynchronizer(studySynchronizerIds.fusionVoi, { + syncInvertState: false, + syncColormap: false, + }); + + // Store synchronizers + allSynchronizers[studyKey] = { + axialCamera: axialCameraSync, + sagittalCamera: sagittalCameraSync, + coronalCamera: coronalCameraSync, + ctVoi: ctVoiSync, + ptVoi: ptVoiSync, + fusionVoi: fusionVoiSync, + }; + + // Add viewports to camera synchronizers + [ + studyViewportIds.CT.AXIAL, + studyViewportIds.PT.AXIAL, + studyViewportIds.FUSION.AXIAL, + ].forEach((viewportId) => { + axialCameraSync.add({ renderingEngineId, viewportId }); + }); + + [ + studyViewportIds.CT.SAGITTAL, + studyViewportIds.PT.SAGITTAL, + studyViewportIds.FUSION.SAGITTAL, + ].forEach((viewportId) => { + sagittalCameraSync.add({ renderingEngineId, viewportId }); + }); + + [ + studyViewportIds.CT.CORONAL, + studyViewportIds.PT.CORONAL, + studyViewportIds.FUSION.CORONAL, + ].forEach((viewportId) => { + coronalCameraSync.add({ renderingEngineId, viewportId }); + }); + + // Add viewports to VOI synchronizers + [ + studyViewportIds.CT.AXIAL, + studyViewportIds.CT.SAGITTAL, + studyViewportIds.CT.CORONAL, + ].forEach((viewportId) => { + ctVoiSync.add({ renderingEngineId, viewportId }); + }); + + [ + studyViewportIds.PT.AXIAL, + studyViewportIds.PT.SAGITTAL, + studyViewportIds.PT.CORONAL, + studyViewportIds.PETMIP.CORONAL, + ].forEach((viewportId) => { + ptVoiSync.add({ renderingEngineId, viewportId }); + }); + + [ + studyViewportIds.FUSION.AXIAL, + studyViewportIds.FUSION.SAGITTAL, + studyViewportIds.FUSION.CORONAL, + ].forEach((viewportId) => { + fusionVoiSync.add({ renderingEngineId, viewportId }); + ctVoiSync.addTarget({ renderingEngineId, viewportId }); + ptVoiSync.addTarget({ renderingEngineId, viewportId }); + }); +} + +// Initialize camera synchronization +function initCameraSynchronization(sViewport, tViewport) { + const camera = sViewport.getCamera(); + tViewport.setCamera(camera); +} + +// Initialize camera sync for a study +function initializeCameraSyncForStudy(studyKey) { + const studyViewportIds = viewportIds[studyKey]; + + const axialCtViewport = renderingEngine.getViewport( + studyViewportIds.CT.AXIAL + ); + const sagittalCtViewport = renderingEngine.getViewport( + studyViewportIds.CT.SAGITTAL + ); + const coronalCtViewport = renderingEngine.getViewport( + studyViewportIds.CT.CORONAL + ); + + const axialPtViewport = renderingEngine.getViewport( + studyViewportIds.PT.AXIAL + ); + const sagittalPtViewport = renderingEngine.getViewport( + studyViewportIds.PT.SAGITTAL + ); + const coronalPtViewport = renderingEngine.getViewport( + studyViewportIds.PT.CORONAL + ); + + const axialFusionViewport = renderingEngine.getViewport( + studyViewportIds.FUSION.AXIAL + ); + const sagittalFusionViewport = renderingEngine.getViewport( + studyViewportIds.FUSION.SAGITTAL + ); + const coronalFusionViewport = renderingEngine.getViewport( + studyViewportIds.FUSION.CORONAL + ); + + initCameraSynchronization(axialFusionViewport, axialCtViewport); + initCameraSynchronization(axialFusionViewport, axialPtViewport); + + initCameraSynchronization(sagittalFusionViewport, sagittalCtViewport); + initCameraSynchronization(sagittalFusionViewport, sagittalPtViewport); + + initCameraSynchronization(coronalFusionViewport, coronalCtViewport); + initCameraSynchronization(coronalFusionViewport, coronalPtViewport); +} + +// Load image IDs for a study +async function getImageIdsForStudy(config) { + const ctImageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: config.studyId, + SeriesInstanceUID: config.ctSeriesUID, + wadoRsRoot, + }); + + const ptImageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: config.studyId, + SeriesInstanceUID: config.ptSeriesUID, + wadoRsRoot, + }); + + return { ctImageIds, ptImageIds }; +} + +// Create viewport input array for a study +function createViewportInputArrayForStudy(config, studyIndex) { + const studyKey = config.studyKey; + const studyElements = elements[studyKey]; + const studyViewportIds = viewportIds[studyKey]; + + // Create viewport input array + const viewportInputArray = [ + { + viewportId: studyViewportIds.CT.AXIAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_1_1, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + }, + }, + { + viewportId: studyViewportIds.CT.SAGITTAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_1_2, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + }, + }, + { + viewportId: studyViewportIds.CT.CORONAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_1_3, + defaultOptions: { + orientation: Enums.OrientationAxis.CORONAL, + }, + }, + { + viewportId: studyViewportIds.PT.AXIAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_2_1, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + background: [1, 1, 1], + }, + }, + { + viewportId: studyViewportIds.PT.SAGITTAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_2_2, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + background: [1, 1, 1], + }, + }, + { + viewportId: studyViewportIds.PT.CORONAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_2_3, + defaultOptions: { + orientation: Enums.OrientationAxis.CORONAL, + background: [1, 1, 1], + }, + }, + { + viewportId: studyViewportIds.FUSION.AXIAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_3_1, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + }, + }, + { + viewportId: studyViewportIds.FUSION.SAGITTAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_3_2, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + }, + }, + { + viewportId: studyViewportIds.FUSION.CORONAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_3_3, + defaultOptions: { + orientation: Enums.OrientationAxis.CORONAL, + }, + }, + { + viewportId: studyViewportIds.PETMIP.CORONAL, + type: ViewportType.ORTHOGRAPHIC, + element: studyElements.element_mip, + defaultOptions: { + orientation: Enums.OrientationAxis.CORONAL, + background: [1, 1, 1], + }, + }, + ]; + + return viewportInputArray; +} + +// Set up display for a study +async function setUpDisplayForStudy(config, studyIndex) { + const studyKey = config.studyKey; + const studyViewportIds = viewportIds[studyKey]; + const studyVolumeIds = volumeIds[studyKey]; + + // Set volumes on the viewports + await setVolumesForViewports( + renderingEngine, + [ + { + volumeId: studyVolumeIds.ct, + callback: setCtTransferFunctionForVolumeActor, + }, + ], + [ + studyViewportIds.CT.AXIAL, + studyViewportIds.CT.SAGITTAL, + studyViewportIds.CT.CORONAL, + ] + ); + + await setVolumesForViewports( + renderingEngine, + [ + { + volumeId: studyVolumeIds.pt, + callback: setPetTransferFunctionForVolumeActor, + }, + ], + [ + studyViewportIds.PT.AXIAL, + studyViewportIds.PT.SAGITTAL, + studyViewportIds.PT.CORONAL, + ] + ); + + await setVolumesForViewports( + renderingEngine, + [ + { + volumeId: studyVolumeIds.ct, + callback: setCtTransferFunctionForVolumeActor, + }, + { + volumeId: studyVolumeIds.pt, + callback: setPetColorMapTransferFunctionForVolumeActor, + }, + ], + [ + studyViewportIds.FUSION.AXIAL, + studyViewportIds.FUSION.SAGITTAL, + studyViewportIds.FUSION.CORONAL, + ] + ); + + // Set up MIP + const ptVolume = volumes[studyKey].pt; + const ptVolumeDimensions = ptVolume.dimensions; + + const slabThickness = Math.sqrt( + ptVolumeDimensions[0] * ptVolumeDimensions[0] + + ptVolumeDimensions[1] * ptVolumeDimensions[1] + + ptVolumeDimensions[2] * ptVolumeDimensions[2] + ); + + await setVolumesForViewports( + renderingEngine, + [ + { + volumeId: studyVolumeIds.pt, + callback: setPetTransferFunctionForVolumeActor, + blendMode: BlendModes.MAXIMUM_INTENSITY_BLEND, + slabThickness, + }, + ], + [studyViewportIds.PETMIP.CORONAL] + ); + + initializeCameraSyncForStudy(studyKey); +} + +// Set crosshairs to passive after viewports are set up +function setCrosshairsToPassive() { + studyConfigs.forEach((config) => { + const studyKey = config.studyKey; + const studyToolGroupIds = toolGroupIds[studyKey]; + + [ + studyToolGroupIds.ct, + studyToolGroupIds.pt, + studyToolGroupIds.fusion, + ].forEach((toolGroupId) => { + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + if (toolGroup) { + toolGroup.setToolPassive(CrosshairsTool.toolName); + } + }); + }); +} + +async function addSegmentationsToState() { + // Create a segmentation of the same resolution as the source data + volumeLoader.createAndCacheDerivedLabelmapVolume(volumeIds.study1.pt, { + volumeId: segmentationId, + }); + + // Add the segmentations to state + segmentation.addSegmentations([ + { + segmentationId, + representation: { + // The type of segmentation + type: csToolsEnums.SegmentationRepresentations.Labelmap, + // The actual segmentation data, in the case of labelmap this is a + // reference to the source volume of the segmentation. + data: { + volumeId: segmentationId, + }, + }, + }, + ]); +} + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + // Add tools to Cornerstone3D + cornerstoneTools.addTool(WindowLevelTool); + cornerstoneTools.addTool(PanTool); + cornerstoneTools.addTool(ZoomTool); + cornerstoneTools.addTool(StackScrollTool); + cornerstoneTools.addTool(MIPJumpToClickTool); + cornerstoneTools.addTool(CrosshairsTool); + cornerstoneTools.addTool(TrackballRotateTool); + cornerstoneTools.addTool(VolumeRotateTool); + cornerstoneTools.addTool(RectangleROITool); + cornerstoneTools.addTool(CircleROIStartEndThresholdTool); + cornerstoneTools.addTool(RectangleROIStartEndThresholdTool); + + // Instantiate a rendering engine + renderingEngine = new RenderingEngine(renderingEngineId); + + // Create viewport grid + createViewportGrid(); + + // Load all volumes and set up displays + for (const config of studyConfigs) { + const studyKey = config.studyKey; + + // Get image IDs + const { ctImageIds, ptImageIds } = await getImageIdsForStudy(config); + + // Create and cache volumes + volumes[studyKey].ct = await volumeLoader.createAndCacheVolume( + volumeIds[studyKey].ct, + { imageIds: ctImageIds } + ); + volumes[studyKey].pt = await volumeLoader.createAndCacheVolume( + volumeIds[studyKey].pt, + { imageIds: ptImageIds } + ); + + // Load volumes + volumes[studyKey].ct.load(); + volumes[studyKey].pt.load(); + } + + // Add some segmentations based on the source data volume + await addSegmentationsToState(); + + // Set up tool groups and synchronizers for each study + for (const config of studyConfigs) { + const studyKey = config.studyKey; + setUpToolGroupsForStudy(studyKey); + setUpSynchronizersForStudy(studyKey); + } + + // Collect all viewport configurations + const allViewportInputs = []; + for (let i = 0; i < studyConfigs.length; i++) { + const viewportInputs = createViewportInputArrayForStudy(studyConfigs[i], i); + allViewportInputs.push(...viewportInputs); + } + + // Set all viewports at once + renderingEngine.setViewports(allViewportInputs); + + // Set up displays for all studies + for (let i = 0; i < studyConfigs.length; i++) { + await setUpDisplayForStudy(studyConfigs[i], i); + } + + // Set crosshairs to passive after all viewports are initialized + setCrosshairsToPassive(); + + Object.values(viewportIds.study1.PT).map(async (viewportId) => { + // Add the segmentation representation to the toolgroup + await segmentation.addSegmentationRepresentations(viewportId, [ + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap, + }, + ]); + }); + + Object.values(viewportIds.study1.CT).map(async (viewportId) => { + // Add the segmentation representation to the toolgroup + await segmentation.addSegmentationRepresentations(viewportId, [ + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap, + }, + ]); + }); + + Object.values(viewportIds.study1.FUSION).map(async (viewportId) => { + // Add the segmentation representation to the toolgroup + await segmentation.addSegmentationRepresentations(viewportId, [ + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap, + }, + ]); + }); + + // Render all viewports + renderingEngine.render(); +} + +run(); diff --git a/packages/core/examples/volumeBasic/index.ts b/packages/core/examples/volumeBasic/index.ts index 57c1b66519..6bca1b5637 100644 --- a/packages/core/examples/volumeBasic/index.ts +++ b/packages/core/examples/volumeBasic/index.ts @@ -39,7 +39,10 @@ content.appendChild(element); */ async function run() { // Init Cornerstone and related libraries - await initDemo(); + const config = (window as any).IS_TILED + ? { core: { renderingEngineMode: 'tiled' } } + : {}; + await initDemo(config); // Get Cornerstone imageIds and fetch metadata into RAM const imageIds = await createImageIdsAndCacheMetaData({ diff --git a/packages/core/examples/volumeViewport3D/index.ts b/packages/core/examples/volumeViewport3D/index.ts index e18985845d..c66a740339 100644 --- a/packages/core/examples/volumeViewport3D/index.ts +++ b/packages/core/examples/volumeViewport3D/index.ts @@ -60,7 +60,8 @@ viewportGrid.appendChild(element1); content.appendChild(viewportGrid); const instructions = document.createElement('p'); -instructions.innerText = 'Click the image to rotate it.'; +instructions.innerText = + 'Click the image to rotate it. Select the preset and sampling distance from the drop downs'; content.append(instructions); @@ -92,6 +93,17 @@ addDropdownToToolbar({ }, }); +addDropdownToToolbar({ + options: { + values: Array.from({ length: 16 }, (_, i) => i + 1), // [1, 2, ..., 16] + defaultValue: 1, + }, + onSelectedValueChange: (sampleDistanceMultiplier) => { + viewport.setProperties({ sampleDistanceMultiplier }); + viewport.render(); + }, +}); + // ============================= // let viewport; diff --git a/packages/core/examples/wadouri/index.ts b/packages/core/examples/wadouri/index.ts index 7cb808a17f..4b9b3cbb37 100644 --- a/packages/core/examples/wadouri/index.ts +++ b/packages/core/examples/wadouri/index.ts @@ -74,7 +74,16 @@ addButtonToToolbar({ */ async function run() { // Init Cornerstone and related libraries - await csRenderInit(); + + const urlParams = new URLSearchParams(window.location.search); + const debugMode = urlParams.get('debug') === 'true'; + + await csRenderInit({ + debug: { + statsOverlay: debugMode, + }, + }); + await initLoader(); const renderingEngine = new RenderingEngine(renderingEngineId); diff --git a/packages/core/examples/webGLContextPooling/index.ts b/packages/core/examples/webGLContextPooling/index.ts new file mode 100644 index 0000000000..f95d0c4578 --- /dev/null +++ b/packages/core/examples/webGLContextPooling/index.ts @@ -0,0 +1,308 @@ +import type { Types } from '@cornerstonejs/core'; +import { + RenderingEngine, + Enums, + init as csRenderInit, + getRenderingEngine, +} from '@cornerstonejs/core'; +import { + createImageIdsAndCacheMetaData, + setTitleAndDescription, + initProviders, + initVolumeLoader, +} from '../../../../utils/demo/helpers'; +import * as cornerstoneTools from '@cornerstonejs/tools'; +import { init as csToolsInit } from '@cornerstonejs/tools'; +import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader'; + +const { + PanTool, + WindowLevelTool, + ZoomTool, + ToolGroupManager, + StackScrollTool, + Enums: csToolsEnums, + synchronizers, +} = cornerstoneTools; + +const { ViewportType } = Enums; +const { MouseBindings } = csToolsEnums; +const { createImageSliceSynchronizer } = synchronizers; + +// Define unique ids +const renderingEngineId = 'myRenderingEngine'; +const toolGroupId = 'STACK_TOOL_GROUP'; +const imageSliceSyncronizerId = 'IMAGE_SLICE_SYNCHRONIZER'; + +// Create viewport IDs for 8x8 grid +const viewportIds: string[] = []; +for (let row = 0; row < 8; row++) { + for (let col = 0; col < 8; col++) { + viewportIds.push(`STACK_VIEWPORT_${row}_${col}`); + } +} + +// ResizeObserver setup +let resizeTimeout: number; + +// Debounced resize handler +const handleResize = () => { + clearTimeout(resizeTimeout); + resizeTimeout = window.setTimeout(() => { + const renderingEngine = getRenderingEngine(renderingEngineId); + if (renderingEngine) { + renderingEngine.resize(true, false); + } + }, 50); // 50ms debounce +}; + +// Create ResizeObserver for dynamic resizing +const resizeObserver = new ResizeObserver(handleResize); + +// ======== Set up page ======== // +setTitleAndDescription( + 'WebGL Context Pooling - 8x8 Grid', + `Demonstrates an 8x8 grid of synchronized stack viewports with configurable WebGL context pooling, use left click to scroll through the stack in all viewports. The current number of WebGL contexts is set to ${ + localStorage.getItem('webglContextCount') || 1 + }.` +); + +const content = document.getElementById('content'); + +// Ensure no scroll overflow +document.body.style.overflow = 'hidden'; +content.style.overflow = 'hidden'; + +// Create WebGL context count control +const controlsDiv = document.createElement('div'); +controlsDiv.style.marginBottom = '10px'; + +const contextCountLabel = document.createElement('label'); +contextCountLabel.innerText = 'WebGL Context Count: '; +contextCountLabel.style.marginRight = '10px'; + +const contextCountDropdown = document.createElement('select'); +const contextOptions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + +// Get stored value or default to 1 +const storedContextCount = localStorage.getItem('webglContextCount'); +const currentContextCount = storedContextCount + ? parseInt(storedContextCount) + : 1; + +contextOptions.forEach((value) => { + const option = document.createElement('option'); + option.value = value.toString(); + option.text = value.toString(); + if (value === currentContextCount) { + option.selected = true; + } + contextCountDropdown.appendChild(option); +}); + +contextCountDropdown.addEventListener('change', (e) => { + const newValue = (e.target as HTMLSelectElement).value; + localStorage.setItem('webglContextCount', newValue); + location.reload(); +}); + +controlsDiv.appendChild(contextCountLabel); +controlsDiv.appendChild(contextCountDropdown); +content.appendChild(controlsDiv); + +// Create viewport grid container +const viewportGrid = document.createElement('div'); +viewportGrid.style.display = 'grid'; +viewportGrid.style.gridTemplateColumns = 'repeat(8, 1fr)'; +viewportGrid.style.gridTemplateRows = 'repeat(8, 1fr)'; +viewportGrid.style.gap = '2px'; +viewportGrid.style.width = '100%'; +viewportGrid.style.height = 'calc(100vh - 200px)'; // Account for controls and text +viewportGrid.style.backgroundColor = '#000'; + +content.appendChild(viewportGrid); + +// Observe the viewport grid for resize events +resizeObserver.observe(viewportGrid); + +// Create viewport elements +const viewportElements: HTMLDivElement[] = []; +const sliceCountElements: HTMLDivElement[] = []; + +for (let i = 0; i < 64; i++) { + const viewportContainer = document.createElement('div'); + viewportContainer.style.position = 'relative'; + viewportContainer.style.width = '100%'; + viewportContainer.style.height = '100%'; + + const element = document.createElement('div'); + element.style.width = '100%'; + element.style.height = '100%'; + element.oncontextmenu = (e) => e.preventDefault(); + + // Create slice count display + const sliceCountDiv = document.createElement('div'); + sliceCountDiv.style.position = 'absolute'; + sliceCountDiv.style.top = '5px'; + sliceCountDiv.style.left = '5px'; + sliceCountDiv.style.color = 'white'; + sliceCountDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; + sliceCountDiv.style.padding = '2px 5px'; + sliceCountDiv.style.fontSize = '12px'; + sliceCountDiv.style.zIndex = '1000'; + sliceCountDiv.style.pointerEvents = 'none'; + sliceCountDiv.innerText = 'Slice: 0/0'; + + viewportContainer.appendChild(element); + viewportContainer.appendChild(sliceCountDiv); + viewportGrid.appendChild(viewportContainer); + + viewportElements.push(element); + sliceCountElements.push(sliceCountDiv); +} + +/** + * Update slice count display for a viewport + */ +function updateSliceCount( + viewportId: string, + currentIndex: number, + totalImages: number +) { + const viewportIndex = viewportIds.indexOf(viewportId); + if (viewportIndex !== -1) { + sliceCountElements[viewportIndex].innerText = `Slice: ${ + currentIndex + 1 + }/${totalImages}`; + } +} + +/** + * Runs the demo + */ +async function run() { + // Initialize providers and loaders + initProviders(); + cornerstoneDICOMImageLoader.init(); + initVolumeLoader(); + + // Initialize Cornerstone with custom WebGL context count + // check query params for ?debug=true + const urlParams = new URLSearchParams(window.location.search); + const debugMode = urlParams.get('debug') === 'true'; + + await csRenderInit({ + rendering: { + webGlContextCount: currentContextCount, + }, + debug: { + statsOverlay: debugMode, + }, + }); + + // Initialize cornerstone tools + await csToolsInit(); + + // Add tools to Cornerstone3D + cornerstoneTools.addTool(PanTool); + cornerstoneTools.addTool(WindowLevelTool); + cornerstoneTools.addTool(StackScrollTool); + cornerstoneTools.addTool(ZoomTool); + + // Create a tool group for all viewports + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + + // Add tools to the tool group + toolGroup.addTool(StackScrollTool.toolName); + + // Set the initial state of the tools + toolGroup.setToolActive(StackScrollTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, // Left Click + }, + ], + }); + + // Get Cornerstone imageIds and fetch metadata into RAM + const imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + SeriesInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + wadoRsRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + }); + + // Instantiate a rendering engine + const renderingEngine = new RenderingEngine(renderingEngineId); + + // Create the viewports + const viewportInputArray: Types.PublicViewportInput[] = viewportIds.map( + (viewportId, index) => ({ + viewportId, + type: ViewportType.STACK, + element: viewportElements[index], + defaultOptions: { + background: [0.1, 0.1, 0.1], + }, + }) + ); + + renderingEngine.setViewports(viewportInputArray); + + // Set the tool group on all viewports + viewportIds.forEach((viewportId) => { + toolGroup.addViewport(viewportId, renderingEngineId); + }); + + // Get all stack viewports + const stackViewports = renderingEngine.getStackViewports(); + + // Set the stack on each viewport + await Promise.all( + stackViewports.map(async (viewport) => { + await viewport.setStack(imageIds); + + // Update initial slice count + updateSliceCount( + viewport.id, + viewport.getCurrentImageIdIndex(), + imageIds.length + ); + + // Add event listener for stack scroll + viewport.element.addEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, (( + evt: Types.EventTypes.StackViewportScrollEvent + ) => { + updateSliceCount( + viewport.id, + evt.detail.newImageIdIndex, + imageIds.length + ); + }) as EventListener); + }) + ); + + // Create and configure the image slice synchronizer + const imageSliceSynchronizer = createImageSliceSynchronizer( + imageSliceSyncronizerId + ); + + // Add all viewports to the synchronizer + viewportIds.forEach((viewportId) => { + imageSliceSynchronizer.add({ + renderingEngineId, + viewportId, + }); + }); + + // Enable stack prefetch for all viewports + stackViewports.forEach((viewport) => { + cornerstoneTools.utilities.stackPrefetch.enable(viewport.element); + }); + + // Render all viewports + renderingEngine.renderViewports(viewportIds); +} + +run(); diff --git a/packages/core/examples/webLoader/index.ts b/packages/core/examples/webLoader/index.ts index 6f6580dbd3..bda596a4be 100644 --- a/packages/core/examples/webLoader/index.ts +++ b/packages/core/examples/webLoader/index.ts @@ -8,10 +8,13 @@ import { setVolumesForViewports, volumeLoader, } from '@cornerstonejs/core'; +import { LengthTool, ToolGroupManager } from '@cornerstonejs/tools'; import { initDemo, setTitleAndDescription, addSliderToToolbar, + addManipulationBindings, + addButtonToToolbar, } from '../../../../utils/demo/helpers'; import hardcodedMetaDataProvider from './hardcodedMetaDataProvider'; import registerWebImageLoader from './registerWebImageLoader'; @@ -23,6 +26,8 @@ console.warn( const { ViewportType } = Enums; +const toolGroupId = 'toolGroup'; + // ======== Set up page ======== // setTitleAndDescription( 'Web Color Images', @@ -67,6 +72,11 @@ content.appendChild(element1); content.appendChild(paraElement); content.appendChild(rowElement); +element1.oncontextmenu = (e) => e.preventDefault(); +element2.oncontextmenu = (e) => e.preventDefault(); +element3.oncontextmenu = (e) => e.preventDefault(); +element4.oncontextmenu = (e) => e.preventDefault(); + const renderingEngineId = 'myRenderingEngine'; const viewportId = 'COLOR_STACK'; @@ -111,12 +121,35 @@ addSliderToToolbar({ viewport.render(); }, }); + +addButtonToToolbar({ + title: 'Invert', + onClick: () => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the volume viewport + const viewport = renderingEngine.getViewport( + viewportId + ) as Types.IVolumeViewport; + + const { invert } = viewport.getProperties(); + + viewport.setProperties({ invert: !invert }); + + viewport.render(); + }, +}); + /** * Runs the demo */ async function run() { // Init Cornerstone and related libraries await initDemo(); + // Define tool groups to add the segmentation display tool to + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + addManipulationBindings(toolGroup); metaData.addProvider( // @ts-ignore @@ -156,6 +189,11 @@ async function run() { }, ]; + toolGroup.addViewport('COLOR_STACK', renderingEngineId); + toolGroup.addViewport('COLOR_VOLUME_1', renderingEngineId); + toolGroup.addViewport('COLOR_VOLUME_2', renderingEngineId); + toolGroup.addViewport('COLOR_VOLUME_3', renderingEngineId); + const volumeId = 'cornerstoneStreamingImageVolume:COLOR_VOLUME'; const volume = await volumeLoader.createAndCacheVolume(volumeId, { diff --git a/packages/core/examples/wsi/index.ts b/packages/core/examples/wsi/index.ts index 05dcd48615..3e564907db 100644 --- a/packages/core/examples/wsi/index.ts +++ b/packages/core/examples/wsi/index.ts @@ -90,13 +90,13 @@ async function run() { // Get Cornerstone imageIds and fetch metadata into RAM // TODO - deploy the testdata publically + const wadoRsRoot = - getLocalUrl() || 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb'; + getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb'; const client = new api.DICOMwebClient({ url: wadoRsRoot }); const imageIds = await createImageIdsAndCacheMetaData({ - StudyInstanceUID: '1.2.276.1.74.1.2.132733202464108492637644434464108492', - SeriesInstanceUID: - '2.16.840.1.113883.3.8467.132733202477512857637644434477512857', + StudyInstanceUID: '2.25.269859997690759739055099378767846712697', + SeriesInstanceUID: '2.25.274641717059635090989922952756233538416', client, wadoRsRoot, convertMultiframe: false, diff --git a/packages/core/package.json b/packages/core/package.json index 872013ab71..690b85b0ea 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -76,8 +76,8 @@ "build": "yarn run build:esm", "build:all": "yarn run build:esm", "dev": "tsc --project ./tsconfig.json --watch", - "format-check": "npx eslint ./src --quiet", "api-check": "api-extractor --debug run ", + "lint": "oxlint .", "prepublishOnly": "yarn run build" }, "dependencies": { diff --git a/packages/core/src/RenderingEngine/BaseRenderingEngine.ts b/packages/core/src/RenderingEngine/BaseRenderingEngine.ts new file mode 100644 index 0000000000..2a3313e791 --- /dev/null +++ b/packages/core/src/RenderingEngine/BaseRenderingEngine.ts @@ -0,0 +1,855 @@ +import Events from '../enums/Events'; +import renderingEngineCache from './renderingEngineCache'; +import eventTarget from '../eventTarget'; +import uuidv4 from '../utilities/uuidv4'; +import triggerEvent from '../utilities/triggerEvent'; +import ViewportType from '../enums/ViewportType'; +import BaseVolumeViewport from './BaseVolumeViewport'; +import StackViewport from './StackViewport'; +import viewportTypeUsesCustomRenderingPipeline from './helpers/viewportTypeUsesCustomRenderingPipeline'; +import getOrCreateCanvas from './helpers/getOrCreateCanvas'; +import { + getShouldUseCPURendering, + isCornerstoneInitialized, + getConfiguration, +} from '../init'; +import type IStackViewport from '../types/IStackViewport'; +import type IVolumeViewport from '../types/IVolumeViewport'; +import viewportTypeToViewportClass from './helpers/viewportTypeToViewportClass'; + +import type * as EventTypes from '../types/EventTypes'; +import type { + PublicViewportInput, + InternalViewportInput, + NormalizedViewportInput, + IViewport, +} from '../types/IViewport'; +import { OrientationAxis } from '../enums'; +import type { VtkOffscreenMultiRenderWindow } from '../types'; +import { StatsOverlay } from './helpers/stats'; + +// Rendering engines seem to not like rendering things less than 2 pixels per side +export const VIEWPORT_MIN_SIZE = 2; + +/** + * Base class for rendering engines that handle viewport rendering. + * This abstract class provides the common functionality shared between + * different rendering strategies + * + * @abstract + */ +abstract class BaseRenderingEngine { + /** Unique identifier for renderingEngine */ + readonly id: string; + /** A flag which tells if the renderingEngine has been destroyed or not */ + public hasBeenDestroyed: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public offscreenMultiRenderWindow: VtkOffscreenMultiRenderWindow; + public offScreenCanvasContainer: HTMLDivElement; + protected _viewports: Map; + protected _needsRender = new Set(); + protected _animationFrameSet = false; + protected _animationFrameHandle: number | null = null; + protected useCPURendering: boolean; + + /** + * @param uid - Unique identifier for RenderingEngine + */ + constructor(id?: string) { + this.id = id ? id : uuidv4(); + this.useCPURendering = getShouldUseCPURendering(); + + renderingEngineCache.set(this); + + if (!isCornerstoneInitialized()) { + throw new Error( + '@cornerstonejs/core is not initialized, run init() first' + ); + } + + this._viewports = new Map(); + this.hasBeenDestroyed = false; + + const config = getConfiguration(); + + if (config?.debug?.statsOverlay) { + StatsOverlay.setup(); + } + } + + /** + * Enables the requested viewport and add it to the viewports. It will + * properly create the Stack viewport or Volume viewport: + * + * 1) Checks if the viewport is defined already, if yes, remove it first + * 2) Checks if the viewport is using a custom rendering pipeline, if no, + * it calculates a new offscreen canvas with the new requested viewport + * 3) Adds the viewport + * + * + * ``` + * renderingEngine.enableElement({ + * viewportId: viewportId, + * type: ViewportType.ORTHOGRAPHIC, + * element, + * defaultOptions: { + * orientation: Enums.OrientationAxis.AXIAL, + * background: [1, 0, 1], + * }, + * }) + * ``` + * + * fires Events.ELEMENT_ENABLED + * + * @param viewportInputEntry - viewport specifications + */ + public enableElement(viewportInputEntry: PublicViewportInput): void { + const viewportInput = this._normalizeViewportInputEntry(viewportInputEntry); + + this._throwIfDestroyed(); + const { element, viewportId } = viewportInput; + + // Throw error if no canvas + if (!element) { + throw new Error('No element provided'); + } + + // 1. Get the viewport from the list of available viewports. + const viewport = this.getViewport(viewportId); + + // 1.a) If there is a found viewport, we remove the viewport and create a new viewport + if (viewport) { + this.disableElement(viewportId); + } + + // 2.a) See if viewport uses a custom rendering pipeline. + const { type } = viewportInput; + + const viewportUsesCustomRenderingPipeline = + viewportTypeUsesCustomRenderingPipeline(type); + + // 2.b) Retrieving the list of viewports for calculation of the new size for + // offScreen canvas. + + // If the viewport being added uses a custom pipeline, or we aren't using + // GPU rendering, we don't need to resize the offscreen canvas. + if (!this.useCPURendering && !viewportUsesCustomRenderingPipeline) { + this.enableVTKjsDrivenViewport(viewportInput); + } else { + // 3 Add the requested viewport to rendering Engine + this.addCustomViewport(viewportInput); + } + + // 5. Set the background color for the canvas + const canvas = getOrCreateCanvas(element); + const { background } = viewportInput.defaultOptions; + this.fillCanvasWithBackgroundColor(canvas, background); + } + + /** + * Disables the requested viewportId from the rendering engine: + * + * 1) It removes the viewport from the the list of viewports + * 2) remove the renderer from the offScreen render window if using vtk.js driven + * rendering pipeline + * 3) resetting the viewport to remove the canvas attributes and canvas data + * 4) resize the offScreen appropriately (if using vtk.js driven rendering pipeline) + * + * fires Events.ELEMENT_ENABLED + * + * @param viewportId - viewport Id + * + */ + public disableElement(viewportId: string): void { + this._throwIfDestroyed(); + // 1. Getting the viewport to remove it + const viewport = this.getViewport(viewportId); + + // 2 To throw if there is no viewport stored in rendering engine + if (!viewport) { + console.warn(`viewport ${viewportId} does not exist`); + return; + } + + // 3. Reset the viewport to remove attributes, and reset the canvas + this._resetViewport(viewport); + + // 4. Remove the related renderer from the offScreenMultiRenderWindow + if ( + !viewportTypeUsesCustomRenderingPipeline(viewport.type) && + !this.useCPURendering + ) { + // Only remove renderer if offscreenMultiRenderWindow exists (not in ContextPoolRenderingEngine) + if (this.offscreenMultiRenderWindow) { + this.offscreenMultiRenderWindow.removeRenderer(viewportId); + } + } + + // 5. Remove the requested viewport from the rendering engine + this._removeViewport(viewportId); + viewport.isDisabled = true; + + // 6. Avoid rendering for the disabled viewport + this._needsRender.delete(viewportId); + + // 7. Clear RAF if no viewport is left + const viewports = this.getViewports(); + if (!viewports.length) { + this._clearAnimationFrame(); + } + + // Note: we should not call resize at the end of here, the reason is that + // in batch rendering, we might disable a viewport and enable others at the same + // time which would interfere with each other. So we just let the enable + // to call resize, and also resize getting called by applications on the + // DOM resize event. + } + + /** + * It takes an array of viewport input objects including element, viewportId, type + * and defaultOptions. It will add the viewport to the rendering engine and enables them. + * + * + * ```typescript + *renderingEngine.setViewports([ + * { + * viewportId: axialViewportId, + * type: ViewportType.ORTHOGRAPHIC, + * element: document.getElementById('axialDiv'), + * defaultOptions: { + * orientation: Enums.OrientationAxis.AXIAL, + * }, + * }, + * { + * viewportId: sagittalViewportId, + * type: ViewportType.ORTHOGRAPHIC, + * element: document.getElementById('sagittalDiv'), + * defaultOptions: { + * orientation: Enums.OrientationAxis.SAGITTAL, + * }, + * }, + * { + * viewportId: customOrientationViewportId, + * type: ViewportType.ORTHOGRAPHIC, + * element: document.getElementById('customOrientationDiv'), + * defaultOptions: { + * orientation: { viewPlaneNormal: [0, 0, 1], viewUp: [0, 1, 0] }, + * }, + * }, + * ]) + * ``` + * + * fires Events.ELEMENT_ENABLED + * + * @param viewportInputEntries - Array + */ + + public setViewports(publicViewportInputEntries: PublicViewportInput[]): void { + const viewportInputEntries = this._normalizeViewportInputEntries( + publicViewportInputEntries + ); + this._throwIfDestroyed(); + this._reset(); + + // 1. Split viewports based on whether they use vtk.js or a custom pipeline. + + const vtkDrivenViewportInputEntries: NormalizedViewportInput[] = []; + const customRenderingViewportInputEntries: NormalizedViewportInput[] = []; + + viewportInputEntries.forEach((vpie) => { + if ( + !this.useCPURendering && + !viewportTypeUsesCustomRenderingPipeline(vpie.type) + ) { + vtkDrivenViewportInputEntries.push(vpie); + } else { + customRenderingViewportInputEntries.push(vpie); + } + }); + + this.setVtkjsDrivenViewports(vtkDrivenViewportInputEntries); + this.setCustomViewports(customRenderingViewportInputEntries); + + // Making sure the setViewports api also can fill the canvas + // properly + viewportInputEntries.forEach((vp) => { + const canvas = getOrCreateCanvas(vp.element); + const { background } = vp.defaultOptions; + this.fillCanvasWithBackgroundColor(canvas, background); + }); + } + + /** + * Resizes the offscreen viewport and recalculates translations to on screen canvases. + * It is up to the parent app to call the resize of the on-screen canvas changes. + * This is left as an app level concern as one might want to debounce the changes, or the like. + * + * @param immediate - Whether all of the viewports should be rendered immediately. + * @param keepCamera - Whether to keep the camera for other viewports while resizing the offscreen canvas + */ + public resize(immediate = true, keepCamera = true): void { + this._throwIfDestroyed(); + // 1. Get the viewports' canvases + const viewports = this._getViewportsAsArray(); + + const vtkDrivenViewports = []; + const customRenderingViewports = []; + + viewports.forEach((vpie) => { + if (!viewportTypeUsesCustomRenderingPipeline(vpie.type)) { + vtkDrivenViewports.push(vpie); + } else { + customRenderingViewports.push(vpie); + } + }); + + if (vtkDrivenViewports.length) { + this._resizeVTKViewports(vtkDrivenViewports, keepCamera, immediate); + } + + if (customRenderingViewports.length) { + this._resizeUsingCustomResizeHandler( + customRenderingViewports, + keepCamera, + immediate + ); + } + } + + /** + * Returns the viewport by Id + * + * @returns viewport + */ + public getViewport(viewportId: string): IViewport { + return this._viewports?.get(viewportId); + } + + /** + * getViewports Returns an array of all `Viewport`s on the `RenderingEngine` instance. + * + * @returns Array of viewports + */ + public getViewports(): IViewport[] { + this._throwIfDestroyed(); + + return this._getViewportsAsArray(); + } + + /** + * Retrieves a stack viewport by its ID. used just for type safety + * + * @param viewportId - The ID of the viewport to retrieve. + * @returns The stack viewport with the specified ID. + * @throws Error if the viewport with the given ID does not exist or is not a StackViewport. + */ + public getStackViewport(viewportId: string): IStackViewport { + this._throwIfDestroyed(); + + const viewport = this.getViewport(viewportId); + + if (!viewport) { + throw new Error(`Viewport with Id ${viewportId} does not exist`); + } + + if (!(viewport instanceof StackViewport)) { + throw new Error(`Viewport with Id ${viewportId} is not a StackViewport.`); + } + + return viewport; + } + + /** + * Filters all the available viewports and return the stack viewports + * @returns stack viewports registered on the rendering Engine + */ + public getStackViewports(): IStackViewport[] { + this._throwIfDestroyed(); + + const viewports = this.getViewports(); + + return viewports.filter( + (vp) => vp instanceof StackViewport + ) as IStackViewport[]; + } + /** + * Return all the viewports that are volume viewports + * @returns An array of VolumeViewport objects. + */ + public getVolumeViewports(): IVolumeViewport[] { + this._throwIfDestroyed(); + + const viewports = this.getViewports(); + + const isVolumeViewport = ( + viewport: IViewport + ): viewport is BaseVolumeViewport => { + return viewport instanceof BaseVolumeViewport; + }; + + return viewports.filter(isVolumeViewport) as IVolumeViewport[]; + } + + /** + * Renders all viewports on the next animation frame. + * + * fires Events.IMAGE_RENDERED + */ + public render(): void { + const viewports = this.getViewports(); + const viewportIds = viewports.map((vp) => vp.id); + + this._setViewportsToBeRenderedNextFrame(viewportIds); + } + + /** + * Renders any viewports viewing the given Frame Of Reference. + * + * @param FrameOfReferenceUID - The unique identifier of the + * Frame Of Reference. + */ + public renderFrameOfReference = (FrameOfReferenceUID: string): void => { + const viewports = this._getViewportsAsArray(); + const viewportIdsWithSameFrameOfReferenceUID = viewports.map((vp) => { + if (vp.getFrameOfReferenceUID() === FrameOfReferenceUID) { + return vp.id; + } + }); + + this.renderViewports(viewportIdsWithSameFrameOfReferenceUID); + }; + + /** + * Renders the provided Viewport IDs. + * + */ + public renderViewports(viewportIds: string[]): void { + this._setViewportsToBeRenderedNextFrame(viewportIds); + } + + /** + * Renders only a specific `Viewport` on the next animation frame. + * + * @param viewportId - The Id of the viewport. + */ + public renderViewport(viewportId: string): void { + this._setViewportsToBeRenderedNextFrame([viewportId]); + } + + /** + * destroy the rendering engine. It will remove all the viewports and, + * if the rendering engine is using the GPU, it will also destroy the GPU + * resources. + */ + public destroy(): void { + if (this.hasBeenDestroyed) { + return; + } + + StatsOverlay.cleanup(); + + // remove vtk rendered first before resetting the viewport + if (!this.useCPURendering) { + const viewports = this._getViewportsAsArray(); + viewports.forEach((vp) => { + if (this.offscreenMultiRenderWindow) { + this.offscreenMultiRenderWindow.removeRenderer(vp.id); + } + }); + + // Free up WebGL resources + if (this.offscreenMultiRenderWindow) { + this.offscreenMultiRenderWindow.delete(); + } + + // Make sure all references go stale and are garbage collected. + delete this.offscreenMultiRenderWindow; + } + + this._reset(); + renderingEngineCache.delete(this.id); + + this.hasBeenDestroyed = true; + } + + /** + * Fill the canvas with the background color + * @param canvas - The canvas element to draw on. + * @param backgroundColor - An array of three numbers between 0 and 1 that + * specify the red, green, and blue values of the background color. + */ + public fillCanvasWithBackgroundColor( + canvas: HTMLCanvasElement, + backgroundColor: [number, number, number] + ): void { + const ctx = canvas.getContext('2d'); + + // Default to black if no background color is set + let fillStyle: string; + if (backgroundColor) { + const rgb = backgroundColor.map((f) => Math.floor(255 * f)); + fillStyle = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; + } else { + fillStyle = 'black'; + } + + // We draw over the previous stack with the background color while we + // wait for the next stack to load + ctx.fillStyle = fillStyle; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + private _normalizeViewportInputEntry( + viewportInputEntry: PublicViewportInput + ) { + const { type, defaultOptions } = viewportInputEntry; + let options = defaultOptions; + + if (!options || Object.keys(options).length === 0) { + options = { + background: [0, 0, 0], + orientation: null, + displayArea: null, + }; + + if (type === ViewportType.ORTHOGRAPHIC) { + options = { + ...options, + orientation: OrientationAxis.AXIAL, + }; + } + } + + return { + ...viewportInputEntry, + defaultOptions: options, + }; + } + + private _normalizeViewportInputEntries( + viewportInputEntries: PublicViewportInput[] + ): NormalizedViewportInput[] { + const normalizedViewportInputs = []; + + viewportInputEntries.forEach((viewportInput) => { + normalizedViewportInputs.push( + this._normalizeViewportInputEntry(viewportInput) + ); + }); + + return normalizedViewportInputs; + } + + private _resizeUsingCustomResizeHandler( + customRenderingViewports: StackViewport[], + keepCamera = true, + immediate = true + ) { + // 1. If viewport has a custom resize method, call it here. + customRenderingViewports.forEach((vp) => { + if (typeof vp.resize === 'function') { + vp.resize(); + } + }); + + // 3. Reset viewport cameras + customRenderingViewports.forEach((vp) => { + const prevCamera = vp.getCamera(); + vp.resetCamera(); + + if (keepCamera) { + vp.setCamera(prevCamera); + } + }); + + // 2. If render is immediate: Render all + if (immediate) { + this.render(); + } + } + + /** + * Disables the requested viewportId from the rendering engine: + * 1) It removes the viewport from the the list of viewports + * 2) remove the renderer from the offScreen render window + * 3) resetting the viewport to remove the canvas attributes and canvas data + * 4) resize the offScreen appropriately + * + * @param viewportId - viewport Id + * + */ + private _removeViewport(viewportId: string): void { + // 1. Get the viewport + const viewport = this.getViewport(viewportId); + if (!viewport) { + console.warn(`viewport ${viewportId} does not exist`); + return; + } + + // 2. Delete the viewports from the the viewports + this._viewports.delete(viewportId); + } + + /** + * Adds a viewport using a custom rendering pipeline to the `RenderingEngine`. + * + * @param viewportInputEntry - Information object used to + * construct and enable the viewport. + */ + private addCustomViewport(viewportInputEntry: PublicViewportInput): void { + const { element, viewportId, type, defaultOptions } = viewportInputEntry; + + // Make the element not focusable, we use this for modifier keys to work + element.tabIndex = -1; + + const canvas = getOrCreateCanvas(element); + + // Add a viewport with no offset + const { clientWidth, clientHeight } = canvas; + + // Set the canvas to be same resolution as the client. + // Note: This ignores devicePixelRatio for now. We may want to change it in the + // future but it has no benefit for the Cornerstone CPU rendering pathway at the + // moment anyway. + if (canvas.width !== clientWidth || canvas.height !== clientHeight) { + canvas.width = clientWidth; + canvas.height = clientHeight; + } + + const viewportInput = { + id: viewportId, + renderingEngineId: this.id, + element, // div + type, + canvas, + sx: 0, // No offset, uses own renderer + sy: 0, + sWidth: clientWidth, + sHeight: clientHeight, + defaultOptions: defaultOptions || {}, + }; + + // 4. Create a proper viewport based on the type of the viewport + const ViewportType = viewportTypeToViewportClass[type]; + const viewport = new ViewportType(viewportInput); + + // 5. Storing the viewports + this._viewports.set(viewportId, viewport); + + const eventDetail: EventTypes.ElementEnabledEventDetail = { + element, + viewportId, + renderingEngineId: this.id, + }; + + triggerEvent(eventTarget, Events.ELEMENT_ENABLED, eventDetail); + } + + /** + * Returns the renderer for a given viewportId. + * @param viewportId - The Id of the viewport. + * @returns The renderer for the viewport. + */ + public getRenderer(viewportId) { + return this.offscreenMultiRenderWindow.getRenderer(viewportId); + } + + /** + * Returns the offscreen multi-render window used for rendering. + */ + public getOffscreenMultiRenderWindow( + viewportId?: string + ): VtkOffscreenMultiRenderWindow { + if (this.useCPURendering) { + throw new Error( + 'Offscreen multi render window is not available when using CPU rendering.' + ); + } + return this.offscreenMultiRenderWindow; + } + + /** + * Sets multiple viewports using custom rendering + * pipelines to the `RenderingEngine`. + * + * @param viewportInputEntries - An array of information + * objects used to construct and enable the viewports. + */ + private setCustomViewports(viewportInputEntries: PublicViewportInput[]) { + viewportInputEntries.forEach((vpie) => { + this.addCustomViewport(vpie); + }); + } + + /** + * @method _getViewportsAsArray Returns an array of all viewports + * + * @returns Array of viewports. + */ + protected _getViewportsAsArray(): IViewport[] { + return Array.from(this._viewports.values()); + } + + private _setViewportsToBeRenderedNextFrame(viewportIds: string[]) { + // Add the viewports to the set of flagged viewports + viewportIds.forEach((viewportId) => { + this._needsRender.add(viewportId); + }); + + // Render any flagged viewports + this._render(); + } + + /** + * Sets up animation frame if necessary + */ + private _render() { + // If we have viewports that need rendering and we have not already + // set the RAF callback to run on the next frame. + if (this._needsRender.size > 0 && !this._animationFrameSet) { + this._animationFrameHandle = window.requestAnimationFrame( + this._renderFlaggedViewports + ); + + // Set the flag that we have already set up the next RAF call. + this._animationFrameSet = true; + } + } + + /** + * Reset the viewport by removing the data attributes + * and clearing the context of draw. It also emits an element disabled event + * + * @param viewport - The `Viewport` to render. + */ + private _resetViewport(viewport: IViewport) { + const renderingEngineId = this.id; + + const { element, canvas, id: viewportId } = viewport; + + const eventDetail: EventTypes.ElementDisabledEventDetail = { + element, + viewportId, + renderingEngineId, + }; + + viewport.removeWidgets(); + + // Trigger first before removing the data attributes, as we need the enabled + // element to remove tools associated with the viewport + triggerEvent(eventTarget, Events.ELEMENT_DISABLED, eventDetail); + + element.removeAttribute('data-viewport-uid'); + element.removeAttribute('data-rendering-engine-uid'); + + // clear drawing + const context = canvas.getContext('2d'); + context.clearRect(0, 0, canvas.width, canvas.height); + } + + private _clearAnimationFrame() { + window.cancelAnimationFrame(this._animationFrameHandle); + + this._needsRender.clear(); + this._animationFrameSet = false; + this._animationFrameHandle = null; + } + + /** + * Resets the `RenderingEngine` + */ + private _reset() { + const viewports = this._getViewportsAsArray(); + + viewports.forEach((viewport) => { + this._resetViewport(viewport); + }); + + this._clearAnimationFrame(); + + this._viewports = new Map(); + } + + /** + * Throws an error if trying to interact with the `RenderingEngine` + * instance after its `destroy` method has been called. + */ + protected _throwIfDestroyed() { + if (this.hasBeenDestroyed) { + throw new Error( + 'this.destroy() has been manually called to free up memory, can not longer use this instance. Instead make a new one.' + ); + } + } + + /** + * Resizes viewports that use VTK.js for rendering. + * This method must be implemented by subclasses to define their specific + * resizing strategy. + */ + protected abstract _resizeVTKViewports( + vtkDrivenViewports: (IStackViewport | IVolumeViewport)[], + keepCamera: boolean, + immediate: boolean + ): void; + + /** + * Enables a viewport to be driven by the offscreen vtk.js rendering engine. + * This method must be implemented by subclasses to define their specific + * viewport enabling strategy. + * + * @param viewportInputEntry - Information object used to + * construct and enable the viewport. + */ + protected abstract enableVTKjsDrivenViewport( + viewportInputEntry: NormalizedViewportInput + ): void; + + /** + * Adds a viewport driven by vtk.js to the `RenderingEngine`. + * This method must be implemented by subclasses to define their specific + * approach to adding VTK.js driven viewports. + * + * @param viewportInputEntry - Information object used to construct and enable the viewport. + * @param offscreenCanvasProperties - Optional properties for configuring the offscreen canvas. + */ + protected abstract addVtkjsDrivenViewport( + viewportInputEntry: InternalViewportInput, + offscreenCanvasProperties?: unknown + ): void; + + /** + * Renders all viewports. + * This method must be implemented by subclasses to define their specific + * rendering strategy + */ + protected abstract _renderFlaggedViewports(): void; + + /** + * Sets multiple vtk.js driven viewports to + * the `RenderingEngine`. + * This method must be implemented by subclasses to define their specific + * approach to setting multiple VTK.js driven viewports. + * + * @param viewportInputEntries - An array of information + * objects used to construct and enable the viewports. + */ + protected abstract setVtkjsDrivenViewports( + viewportInputEntries: NormalizedViewportInput[] + ): void; + + /** + * Renders a particular `Viewport`'s on screen canvas. + * This method must be implemented by subclasses to define how to copy + * from the offscreen canvas to the onscreen canvas. + * + * @param viewport - The `Viewport` to render. + * @param offScreenCanvas - The offscreen canvas to render from. + */ + protected abstract _renderViewportFromVtkCanvasToOnscreenCanvas( + viewport: IViewport, + offScreenCanvas: HTMLCanvasElement + ): EventTypes.ImageRenderedEventDetail; +} + +export default BaseRenderingEngine; diff --git a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts index f3f2241916..0b59acf972 100644 --- a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts +++ b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts @@ -40,7 +40,7 @@ import type { ICamera, } from '../types'; import type { VoiModifiedEventDetail } from '../types/EventTypes'; -import type { ViewportInput } from '../types/IViewport'; +import type { PlaneRestriction, ViewportInput } from '../types/IViewport'; import triggerEvent from '../utilities/triggerEvent'; import * as colormapUtils from '../utilities/colormap'; import invertRgbTransferFunction from '../utilities/invertRgbTransferFunction'; @@ -68,13 +68,17 @@ import getVolumeViewportScrollInfo from '../utilities/getVolumeViewportScrollInf import { actorIsA, isImageActor } from '../utilities/actorCheck'; import snapFocalPointToSlice from '../utilities/snapFocalPointToSlice'; import getVoiFromSigmoidRGBTransferFunction from '../utilities/getVoiFromSigmoidRGBTransferFunction'; -import isEqual, { isEqualNegative } from '../utilities/isEqual'; +import isEqual, { isEqualAbs, isEqualNegative } from '../utilities/isEqual'; import applyPreset from '../utilities/applyPreset'; import imageIdToURI from '../utilities/imageIdToURI'; import uuidv4 from '../utilities/uuidv4'; import * as metaData from '../metaData'; import { getCameraVectors } from './helpers/getCameraVectors'; - +import { isContextPoolRenderingEngine } from './helpers/isContextPoolRenderingEngine'; +import type vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; +import mprCameraValues from '../constants/mprCameraValues'; +import { setConfiguration, getConfiguration } from '@cornerstonejs/core'; +import type { Types } from '@cornerstonejs/core'; /** * Abstract base class for volume viewports. VolumeViewports are used to render * 3D volumes from which various orientations can be viewed. Since VolumeViewports @@ -111,6 +115,8 @@ abstract class BaseVolumeViewport extends Viewport { ); } + this._configureRenderingPipeline(); + const renderer = this.getRenderer(); const camera = vtkSlabCamera.newInstance(); @@ -664,7 +670,8 @@ abstract class BaseVolumeViewport extends Viewport { return false; } if (options?.withNavigation) { - return true; + const { referencedImageId } = viewRef; + return !referencedImageId || this.hasImageURI(referencedImageId); } const currentSliceIndex = this.getSliceIndex(); const { sliceIndex } = viewRef; @@ -712,6 +719,80 @@ abstract class BaseVolumeViewport extends Viewport { abstract isInAcquisitionPlane(): boolean; + /** + * This will apply a camera orientation that is compatible with inPlaneVector1 and 2 + * + * 1. If inPlaneVector1 and inPlaneVector2 are compatible with, no change. + * 2. If dot products of the current view plane normal and inPlaneVector 1 and 2 are zero, no change + * + */ + public setBestOrentation(inPlaneVector1, inPlaneVector2) { + if (!inPlaneVector1 && !inPlaneVector2) { + // Any view is compatible with a point position + return; + } + const { viewPlaneNormal } = this.getCamera(); + if ( + isCompatible(viewPlaneNormal, inPlaneVector2) && + isCompatible(viewPlaneNormal, inPlaneVector1) + ) { + // Orthogonal view to the current view, so no change. + return; + } + + const acquisition = this._getAcquisitionPlaneOrientation(); + if ( + isCompatible(acquisition.viewPlaneNormal, inPlaneVector2) && + isCompatible(acquisition.viewPlaneNormal, inPlaneVector1) + ) { + // Orthogonal view to the current view, so no change. + this.setOrientation(acquisition); + return; + } + for (const orientation of <{ viewPlaneNormal: Point3 }[]>( + Object.values(mprCameraValues) + )) { + if ( + isCompatible(orientation.viewPlaneNormal, inPlaneVector2) && + isCompatible(orientation.viewPlaneNormal, inPlaneVector1) + ) { + // Orthogonal view to the current view, so no change. + this.setOrientation(orientation); + return; + } + } + + const planeNormal = ( + vec3.cross( + vec3.create(), + inPlaneVector2 || acquisition.viewPlaneNormal, + inPlaneVector1 + ) + ); + vec3.normalize(planeNormal, planeNormal); + this.setOrientation({ viewPlaneNormal: planeNormal }); + } + + /** + * Sets the view reference given a referenced plane and the current + * view plane normal being applied. + * This will use the existing normal if compatible, otherwise will calculate + * a new view plane normal as the referenced plane normal, or else the + * cross product of the existing view plane normal and the inPlaneVector1 + */ + public setViewPlane(planeRestriction: PlaneRestriction) { + const { point, inPlaneVector1, inPlaneVector2, FrameOfReferenceUID } = + planeRestriction; + + this.setBestOrentation(inPlaneVector1, inPlaneVector2); + + this.setViewReference({ + FrameOfReferenceUID, + cameraFocalPoint: point, + viewPlaneNormal: this.getCamera().viewPlaneNormal, + }); + } + /** * Navigates to the specified view reference. */ @@ -721,14 +802,21 @@ abstract class BaseVolumeViewport extends Viewport { } const volumeId = this.getVolumeId(); const { - viewPlaneNormal: refViewPlaneNormal, FrameOfReferenceUID: refFrameOfReference, cameraFocalPoint, referencedImageId, + planeRestriction, + viewPlaneNormal: refViewPlaneNormal, viewUp, } = viewRef; let { sliceIndex } = viewRef; + + if (planeRestriction && !refViewPlaneNormal) { + return this.setViewPlane(planeRestriction); + } + const { focalPoint, viewPlaneNormal, position } = this.getCamera(); + const isNegativeNormal = isEqualNegative( viewPlaneNormal, refViewPlaneNormal @@ -892,6 +980,7 @@ abstract class BaseVolumeViewport extends Viewport { preset, interpolationType, slabThickness, + sampleDistanceMultiplier, }: VolumeViewportProperties = {}, volumeId?: string, suppressEvents = false @@ -945,6 +1034,9 @@ abstract class BaseVolumeViewport extends Viewport { if (slabThickness !== undefined) { this.setSlabThickness(slabThickness); } + if (sampleDistanceMultiplier !== undefined) { + this.setSampleDistanceMultiplier(sampleDistanceMultiplier); + } } /** @@ -978,14 +1070,13 @@ abstract class BaseVolumeViewport extends Viewport { this.viewportProperties.slabThickness = properties.slabThickness; } - if (properties.preset !== undefined) { - this.setPreset(properties.preset, volumeId, false); + if (properties.sampleDistanceMultiplier !== undefined) { + this.setSampleDistanceMultiplier(properties.sampleDistanceMultiplier); } if (properties.preset !== undefined) { this.setPreset(properties.preset, volumeId, false); } - this.render(); } @@ -1034,6 +1125,8 @@ abstract class BaseVolumeViewport extends Viewport { } } + public setSampleDistanceMultiplier(multiplier: number): void {} + /** * Retrieve the viewport default properties * @param volumeId If given, we retrieve the default properties of a volumeId if it exists @@ -1600,7 +1693,8 @@ abstract class BaseVolumeViewport extends Viewport { * @returns The corresponding world coordinates. * @public */ - public canvasToWorld = (canvasPos: Point2): Point3 => { + + public canvasToWorldTiled = (canvasPos: Point2): Point3 => { const vtkCamera = this.getVtkActiveCamera() as vtkSlabCameraType; /** @@ -1628,7 +1722,7 @@ abstract class BaseVolumeViewport extends Viewport { vtkCamera.setIsPerformingCoordinateTransformation?.(true); const renderer = this.getRenderer(); - const displayCoords = this.getVtkDisplayCoords(canvasPos); + const displayCoords = this.getVtkDisplayCoordsTiled(canvasPos); const offscreenMultiRenderWindow = this.getRenderingEngine().offscreenMultiRenderWindow; const openGLRenderWindow = @@ -1645,6 +1739,74 @@ abstract class BaseVolumeViewport extends Viewport { return [worldCoord[0], worldCoord[1], worldCoord[2]]; }; + public canvasToWorldContextPool = (canvasPos: Point2): Point3 => { + const vtkCamera = this.getVtkActiveCamera() as vtkSlabCameraType; + + /** + * NOTE: this is necessary because we want the coordinate transformation + * respect to the view plane (plane orthogonal to the camera and passing to + * the focal point). + * + * When vtk.js computes the coordinate transformations, it simply uses the + * camera matrix (no ray casting). + * + * However for the volume viewport the clipping range is set to be + * (-RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE, RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE). + * The clipping range is used in the camera method getProjectionMatrix(). + * The projection matrix is used then for viewToWorld/worldToView methods of + * the renderer. This means that vkt.js will not return the coordinates of + * the point on the view plane (i.e. the depth coordinate will correspond + * to the focal point). + * + * Therefore the clipping range has to be set to (distance, distance + 0.01), + * where now distance is the distance between the camera position and focal + * point. This is done internally, in our camera customization when the flag + * isPerformingCoordinateTransformation is set to true. + */ + + vtkCamera.setIsPerformingCoordinateTransformation?.(true); + + const renderer = this.getRenderer(); + const devicePixelRatio = window.devicePixelRatio || 1; + const { width, height } = this.canvas; + const aspectRatio = width / height; + + // Convert canvas coordinates to normalized display coordinates + const canvasPosWithDPR = [ + canvasPos[0] * devicePixelRatio, + canvasPos[1] * devicePixelRatio, + ]; + + // Normalize to [0,1] range + const normalizedDisplay = [ + canvasPosWithDPR[0] / width, + 1 - canvasPosWithDPR[1] / height, // Flip Y axis + 0, + ]; + + // Transform from normalized display to world coordinates + const projCoords = renderer.normalizedDisplayToProjection( + normalizedDisplay[0], + normalizedDisplay[1], + normalizedDisplay[2] + ); + const viewCoords = renderer.projectionToView( + projCoords[0], + projCoords[1], + projCoords[2], + aspectRatio + ); + const worldCoord = renderer.viewToWorld( + viewCoords[0], + viewCoords[1], + viewCoords[2] + ); + + vtkCamera.setIsPerformingCoordinateTransformation?.(false); + + return [worldCoord[0], worldCoord[1], worldCoord[2]]; + }; + /** * Returns the VTK.js display coordinates of the given `canvasPos` projected onto the * `Viewport`'s `vtkCamera`'s focal point and the direction of projection. @@ -1652,7 +1814,7 @@ abstract class BaseVolumeViewport extends Viewport { * @returns The corresponding display coordinates. * */ - public getVtkDisplayCoords = (canvasPos: Point2): Point3 => { + public getVtkDisplayCoordsTiled = (canvasPos: Point2): Point3 => { const devicePixelRatio = window.devicePixelRatio || 1; const canvasPosWithDPR = [ canvasPos[0] * devicePixelRatio, @@ -1672,6 +1834,23 @@ abstract class BaseVolumeViewport extends Viewport { displayCoord[1] = size[1] - displayCoord[1]; return [displayCoord[0], displayCoord[1], 0]; }; + + public getVtkDisplayCoordsContextPool = (canvasPos: Point2): Point3 => { + const devicePixelRatio = window.devicePixelRatio || 1; + const canvasPosWithDPR = [ + canvasPos[0] * devicePixelRatio, + canvasPos[1] * devicePixelRatio, + ]; + + const { height } = this.canvas; + + // Canvas coordinates with origin at top-left + // VTK display coordinates have origin at bottom-left + const displayCoord = [canvasPosWithDPR[0], height - canvasPosWithDPR[1]]; + + return [displayCoord[0], displayCoord[1], 0]; + }; + /** * Returns the canvas coordinates of the given `worldPos` * projected onto the `Viewport`'s `canvas`. @@ -1680,7 +1859,7 @@ abstract class BaseVolumeViewport extends Viewport { * @returns The corresponding canvas coordinates. * @public */ - public worldToCanvas = (worldPos: Point3): Point2 => { + public worldToCanvasTiled = (worldPos: Point3): Point2 => { const vtkCamera = this.getVtkActiveCamera() as vtkSlabCameraType; /** @@ -1737,15 +1916,103 @@ abstract class BaseVolumeViewport extends Viewport { return canvasCoordWithDPR; }; + public worldToCanvasContextPool = (worldPos: Point3): Point2 => { + const vtkCamera = this.getVtkActiveCamera() as vtkSlabCameraType; + + /** + * NOTE: this is necessary because we want the coordinate transformation + * respect to the view plane (plane orthogonal to the camera and passing to + * the focal point). + * + * When vtk.js computes the coordinate transformations, it simply uses the + * camera matrix (no ray casting). + * + * However for the volume viewport the clipping range is set to be + * (-RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE, RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE). + * The clipping range is used in the camera method getProjectionMatrix(). + * The projection matrix is used then for viewToWorld/worldToView methods of + * the renderer. This means that vkt.js will not return the coordinates of + * the point on the view plane (i.e. the depth coordinate will corresponded + * to the focal point). + * + * Therefore the clipping range has to be set to (distance, distance + 0.01), + * where now distance is the distance between the camera position and focal + * point. This is done internally, in our camera customization when the flag + * isPerformingCoordinateTransformation is set to true. + */ + + vtkCamera.setIsPerformingCoordinateTransformation?.(true); + + const renderer = this.getRenderer(); + const { width, height } = this.canvas; + const aspectRatio = width / height; + + // Transform from world to view coordinates + const viewCoords = renderer.worldToView( + worldPos[0], + worldPos[1], + worldPos[2] + ); + + // Transform from view to projection coordinates + const projCoords = renderer.viewToProjection( + viewCoords[0], + viewCoords[1], + viewCoords[2], + aspectRatio + ); + + // Transform from projection to normalized display coordinates + const normalizedDisplay = renderer.projectionToNormalizedDisplay( + projCoords[0], + projCoords[1], + projCoords[2] + ); + + // Convert normalized display [0,1] to canvas pixels + const canvasX = normalizedDisplay[0] * width; + const canvasY = (1 - normalizedDisplay[1]) * height; // Flip Y axis + + const devicePixelRatio = window.devicePixelRatio || 1; + const canvasCoordWithDPR = [ + canvasX / devicePixelRatio, + canvasY / devicePixelRatio, + ] as Point2; + + vtkCamera.setIsPerformingCoordinateTransformation(false); + + return canvasCoordWithDPR; + }; + + /** + * Get the renderer for this viewport - handles ContextPoolRenderingEngine + */ + public getRendererContextPool(): vtkRenderer { + const renderingEngine = this.getRenderingEngine(); + return renderingEngine.getRenderer(this.id); + } + + /** + * Returns the `vtkRenderer` responsible for rendering the `Viewport`. + * + * @returns The `vtkRenderer` for the `Viewport`. + */ + public getRendererTiled(): vtkRenderer { + const renderingEngine = this.getRenderingEngine(); + + if (!renderingEngine || renderingEngine.hasBeenDestroyed) { + throw new Error('Rendering engine has been destroyed'); + } + + return renderingEngine.offscreenMultiRenderWindow?.getRenderer(this.id); + } + /* - * Checking if the imageURI is in the volumes that are being - * rendered by the viewport. imageURI is the imageId without the schema - * for instance for the imageId of `wadors:http://...`, the `http://...` is the imageURI. - * Why we don't check the imageId is because the same image can be shown in - * another viewport (StackViewport) with a different schema + * Checking if the imageURI (as a URI or an ID) is in the volumes that are being + * rendered by the viewport. * - * @param imageURI - The imageURI to check - * @returns True if the imageURI is in the volumes that are being rendered by the viewport + * @param imageURI - The imageURI or imageID to check + * @returns True if the image is in the volumes that are being rendered by the viewport */ public hasImageURI = (imageURI: string): boolean => { const volumeActors = this.getActors().filter((actorEntry) => @@ -1755,25 +2022,59 @@ abstract class BaseVolumeViewport extends Viewport { return volumeActors.some(({ uid, referencedId }) => { const volume = cache.getVolume(referencedId || uid); - if (!volume?.imageIds) { + if (!volume?.getImageIdIndex) { return false; } - const volumeImageURIs = volume.imageIds.map(imageIdToURI); - - return volumeImageURIs.includes(imageURI); + return ( + volume.getImageIdIndex(imageURI) !== undefined || + volume.getImageURIIndex(imageURI) !== undefined + ); }); }; + /** + * Gets a view up given a view plane normal and the current orientation + * Chooses the current view up if orthogonal, otherwise the default view ups + * for axial, sagittal and coronal + * Otherwise runs the Gram-Schmidt algorithm with the current viewUp + */ + protected _getViewUp(viewPlaneNormal): Point3 { + const { viewUp } = this.getCamera(); + const dot = vec3.dot(viewUp, viewPlaneNormal); + if (isEqual(dot, 0)) { + // Don't change the view up if not needed + return viewUp; + } + if (isEqualAbs(viewPlaneNormal[0], 1)) { + return [0, 0, 1]; + } + if (isEqualAbs(viewPlaneNormal[1], 1)) { + return [0, 0, 1]; + } + if (isEqualAbs(viewPlaneNormal[2], 1)) { + return [0, -1, 0]; + } + const vupOrthogonal = ( + vec3.scaleAndAdd(vec3.create(), viewUp, viewPlaneNormal, -dot) + ); + vec3.normalize(vupOrthogonal, vupOrthogonal); + return vupOrthogonal; + } + protected _getOrientationVectors( orientation: OrientationAxis | OrientationVectors ): OrientationVectors { if (typeof orientation === 'object') { - if (orientation.viewPlaneNormal && orientation.viewUp) { - return orientation; + if (orientation.viewPlaneNormal) { + return { + ...orientation, + viewUp: + orientation.viewUp || this._getViewUp(orientation.viewPlaneNormal), + }; } else { throw new Error( - 'Invalid orientation object. It must contain viewPlaneNormal and viewUp' + 'Invalid orientation object. It must contain viewPlaneNormal' ); } } else if (typeof orientation === 'string') { @@ -1947,9 +2248,10 @@ abstract class BaseVolumeViewport extends Viewport { sliceIndex ??= currentIndex; const { viewPlaneNormal, focalPoint } = this.getCamera(); const querySeparator = volumeId.includes('?') ? '&' : '?'; - return `volumeId:${volumeId}${querySeparator}sliceIndex=${sliceIndex}&viewPlaneNormal=${viewPlaneNormal.join( - ',' - )}`; + // Format each element of viewPlaneNormal to 3 decimal places + // to avoid floating point precision issues + const formattedNormal = viewPlaneNormal.map((v) => v.toFixed(3)).join(','); + return `volumeId:${volumeId}${querySeparator}sliceIndex=${sliceIndex}&viewPlaneNormal=${formattedNormal}`; } private _addVolumeId(volumeId: string): void { @@ -1979,6 +2281,47 @@ abstract class BaseVolumeViewport extends Viewport { public getAllVolumeIds(): string[] { return Array.from(this.volumeIds); } + + private _configureRenderingPipeline() { + const isContextPool = isContextPoolRenderingEngine(); + + for (const key in this.renderingPipelineFunctions) { + if ( + Object.prototype.hasOwnProperty.call( + this.renderingPipelineFunctions, + key + ) + ) { + const functions = this.renderingPipelineFunctions[key]; + + this[key] = isContextPool ? functions.contextPool : functions.tiled; + } + } + } + + protected renderingPipelineFunctions = { + worldToCanvas: { + tiled: this.worldToCanvasTiled, + contextPool: this.worldToCanvasContextPool, + }, + canvasToWorld: { + tiled: this.canvasToWorldTiled, + contextPool: this.canvasToWorldContextPool, + }, + getVtkDisplayCoords: { + tiled: this.getVtkDisplayCoordsTiled, + contextPool: this.getVtkDisplayCoordsContextPool, + }, + getRenderer: { + tiled: this.getRendererTiled, + contextPool: this.getRendererContextPool, + }, + }; +} + +/** Checks of a vector is compatible with the view plane normal */ +function isCompatible(viewPlaneNormal, vector) { + return !vector || isEqual(vec3.dot(viewPlaneNormal, vector), 0); } export default BaseVolumeViewport; diff --git a/packages/core/src/RenderingEngine/ContextPoolRenderingEngine.ts b/packages/core/src/RenderingEngine/ContextPoolRenderingEngine.ts new file mode 100644 index 0000000000..e3dfac6bc5 --- /dev/null +++ b/packages/core/src/RenderingEngine/ContextPoolRenderingEngine.ts @@ -0,0 +1,557 @@ +import BaseRenderingEngine, { VIEWPORT_MIN_SIZE } from './BaseRenderingEngine'; +import WebGLContextPool from './WebGLContextPool'; +import { getConfiguration } from '../init'; +import Events from '../enums/Events'; +import eventTarget from '../eventTarget'; +import triggerEvent from '../utilities/triggerEvent'; +import ViewportType from '../enums/ViewportType'; +import VolumeViewport from './VolumeViewport'; +import StackViewport from './StackViewport'; +import VolumeViewport3D from './VolumeViewport3D'; +import viewportTypeUsesCustomRenderingPipeline from './helpers/viewportTypeUsesCustomRenderingPipeline'; +import getOrCreateCanvas from './helpers/getOrCreateCanvas'; +import type IStackViewport from '../types/IStackViewport'; +import type IVolumeViewport from '../types/IVolumeViewport'; + +import type * as EventTypes from '../types/EventTypes'; +import type { + ViewportInput, + InternalViewportInput, + NormalizedViewportInput, + IViewport, +} from '../types/IViewport'; +import type vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; +import type { VtkOffscreenMultiRenderWindow } from '../types'; + +/** + * ContextPoolRenderingEngine extends BaseRenderingEngine to provide parallel rendering + * capabilities using multiple WebGL contexts for improved performance. + * + * @public + */ +class ContextPoolRenderingEngine extends BaseRenderingEngine { + private contextPool: WebGLContextPool; + + constructor(id?: string) { + super(id); + const { rendering } = getConfiguration(); + const { webGlContextCount } = rendering; + + if (!this.useCPURendering) { + this.contextPool = new WebGLContextPool(webGlContextCount); + } + } + + /** + * Enables a viewport to be driven by the offscreen vtk.js rendering engine. + * + * @param viewportInputEntry - Information object used to + * construct and enable the viewport. + */ + protected enableVTKjsDrivenViewport( + viewportInputEntry: NormalizedViewportInput + ) { + const viewports = this._getViewportsAsArray(); + const viewportsDrivenByVtkJs = viewports.filter( + (vp) => viewportTypeUsesCustomRenderingPipeline(vp.type) === false + ); + + const canvasesDrivenByVtkJs = viewportsDrivenByVtkJs.map((vp) => vp.canvas); + + const canvas = getOrCreateCanvas(viewportInputEntry.element); + canvasesDrivenByVtkJs.push(canvas); + + const internalViewportEntry = { ...viewportInputEntry, canvas }; + + this.addVtkjsDrivenViewport(internalViewportEntry); + } + + /** + * Adds a viewport driven by vtk.js to the `RenderingEngine`. + * + * @param viewportInputEntry - Information object used to construct and enable the viewport. + */ + protected addVtkjsDrivenViewport( + viewportInputEntry: InternalViewportInput + ): void { + const { element, canvas, viewportId, type, defaultOptions } = + viewportInputEntry; + + element.tabIndex = -1; + + // Assign viewport to a context + // Stack viewports get distributed across contexts, all others use context 0 + let contextIndex = 0; + if (type === ViewportType.STACK) { + const contexts = this.contextPool.getAllContexts(); + contextIndex = this._viewports.size % contexts.length; + } + this.contextPool.assignViewportToContext(viewportId, contextIndex); + + // Get the context and add the renderer + const contextData = this.contextPool.getContextByIndex(contextIndex); + + const { context: offscreenMultiRenderWindow } = contextData; + offscreenMultiRenderWindow.addRenderer({ + viewport: [0, 0, 1, 1], + id: viewportId, + background: defaultOptions.background + ? defaultOptions.background + : [0, 0, 0], + }); + + const viewportInput = { + id: viewportId, + element, + renderingEngineId: this.id, + type, + canvas, + sx: 0, + sy: 0, + sWidth: canvas.width, + sHeight: canvas.height, + defaultOptions: defaultOptions || {}, + } as ViewportInput; + + let viewport: IViewport; + if (type === ViewportType.STACK) { + viewport = new StackViewport(viewportInput); + } else if ( + type === ViewportType.ORTHOGRAPHIC || + type === ViewportType.PERSPECTIVE + ) { + viewport = new VolumeViewport(viewportInput); + } else if (type === ViewportType.VOLUME_3D) { + viewport = new VolumeViewport3D(viewportInput); + } else { + throw new Error(`Viewport Type ${type} is not supported`); + } + + this._viewports.set(viewportId, viewport); + + const eventDetail: EventTypes.ElementEnabledEventDetail = { + element, + viewportId, + renderingEngineId: this.id, + }; + + if (!viewport.suppressEvents) { + triggerEvent(eventTarget, Events.ELEMENT_ENABLED, eventDetail); + } + } + + /** + * Sets multiple vtk.js driven viewports to + * the `RenderingEngine`. + * + * @param viewportInputEntries - An array of information + * objects used to construct and enable the viewports. + */ + protected setVtkjsDrivenViewports( + viewportInputEntries: NormalizedViewportInput[] + ) { + if (viewportInputEntries.length) { + const vtkDrivenCanvases = viewportInputEntries.map((vp) => + getOrCreateCanvas(vp.element) + ); + + vtkDrivenCanvases.forEach((canvas) => { + const devicePixelRatio = window.devicePixelRatio || 1; + + const rect = canvas.getBoundingClientRect(); + canvas.width = rect.width * devicePixelRatio; + canvas.height = rect.height * devicePixelRatio; + }); + + for (let i = 0; i < viewportInputEntries.length; i++) { + const vtkDrivenViewportInputEntry = viewportInputEntries[i]; + const canvas = vtkDrivenCanvases[i]; + const internalViewportEntry = { + ...vtkDrivenViewportInputEntry, + canvas, + }; + + this.addVtkjsDrivenViewport(internalViewportEntry); + } + } + } + + /** + * Resizes viewports that use VTK.js for rendering. + */ + protected _resizeVTKViewports( + vtkDrivenViewports: (IStackViewport | IVolumeViewport)[], + keepCamera = true, + immediate = true + ) { + const canvasesDrivenByVtkJs = vtkDrivenViewports.map( + (vp: IStackViewport | IVolumeViewport) => { + return getOrCreateCanvas(vp.element); + } + ); + + canvasesDrivenByVtkJs.forEach((canvas) => { + const devicePixelRatio = window.devicePixelRatio || 1; + canvas.width = canvas.clientWidth * devicePixelRatio; + canvas.height = canvas.clientHeight * devicePixelRatio; + }); + + if (canvasesDrivenByVtkJs.length) { + this._resize(vtkDrivenViewports); + } + + vtkDrivenViewports.forEach((vp: IStackViewport | IVolumeViewport) => { + const prevCamera = vp.getCamera(); + const rotation = vp.getRotation(); + const { flipHorizontal } = prevCamera; + vp.resetCameraForResize(); + + const displayArea = vp.getDisplayArea(); + + if (keepCamera) { + if (displayArea) { + if (flipHorizontal) { + vp.setCamera({ flipHorizontal }); + } + if (rotation) { + vp.setViewPresentation({ rotation }); + } + } else { + vp.setCamera(prevCamera); + } + } + }); + + if (immediate) { + this.render(); + } + } + + /** + * Renders all viewports. + */ + protected _renderFlaggedViewports = (): void => { + this._throwIfDestroyed(); + + const viewports = this._getViewportsAsArray(); + const viewportsToRender = viewports.filter((vp) => + this._needsRender.has(vp.id) + ); + + if (viewportsToRender.length === 0) { + this._animationFrameSet = false; + this._animationFrameHandle = null; + return; + } + + // Render all viewports synchronously + const eventDetails = viewportsToRender.map((viewport) => { + const eventDetail = this.renderViewportUsingCustomOrVtkPipeline(viewport); + viewport.setRendered(); + this._needsRender.delete(viewport.id); + return eventDetail; + }); + + this._animationFrameSet = false; + this._animationFrameHandle = null; + + // Trigger all events after rendering is complete + eventDetails.forEach((eventDetail) => { + if (eventDetail?.element) { + triggerEvent(eventDetail.element, Events.IMAGE_RENDERED, eventDetail); + } + }); + }; + + private renderViewportUsingCustomOrVtkPipeline( + viewport: IViewport + ): EventTypes.ImageRenderedEventDetail { + // Handle custom rendering pipeline viewports + if (viewportTypeUsesCustomRenderingPipeline(viewport.type)) { + const eventDetail = + viewport.customRenderViewportToCanvas() as EventTypes.ImageRenderedEventDetail; + return eventDetail; + } + + // If using CPU rendering, throw error + if (this.useCPURendering) { + throw new Error( + 'GPU not available, and using a viewport with no custom render pipeline.' + ); + } + + // Get the context assigned to this viewport + const assignedContextIndex = this.contextPool.getContextIndexForViewport( + viewport.id + ); + + const contextData = + this.contextPool.getContextByIndex(assignedContextIndex); + + const { context, container } = contextData; + + const eventDetail = this._renderViewportWithContext( + viewport, + context, + container + ); + return eventDetail; + } + + private _renderViewportWithContext( + viewport: IViewport, + offscreenMultiRenderWindow: VtkOffscreenMultiRenderWindow, + offScreenCanvasContainer: HTMLDivElement + ): EventTypes.ImageRenderedEventDetail { + // Check viewport size + if ( + viewport.sWidth < VIEWPORT_MIN_SIZE || + viewport.sHeight < VIEWPORT_MIN_SIZE + ) { + console.warn('Viewport is too small', viewport.sWidth, viewport.sHeight); + return; + } + + if (viewportTypeUsesCustomRenderingPipeline(viewport.type)) { + return viewport.customRenderViewportToCanvas() as EventTypes.ImageRenderedEventDetail; + } + + if (this.useCPURendering) { + throw new Error( + 'GPU not available, and using a viewport with no custom render pipeline.' + ); + } + + // Add renderer if not already present + if (!offscreenMultiRenderWindow.getRenderer(viewport.id)) { + offscreenMultiRenderWindow.addRenderer({ + viewport: [0, 0, 1, 1], + id: viewport.id, + background: viewport.defaultOptions?.background || [0, 0, 0], + }); + } + + const renderWindow = offscreenMultiRenderWindow.getRenderWindow(); + + this._resizeOffScreenCanvasForViewport( + viewport.canvas, + offScreenCanvasContainer, + offscreenMultiRenderWindow + ); + + const renderer = offscreenMultiRenderWindow.getRenderer(viewport.id); + renderer.setViewport(0, 0, 1, 1); + + // Set only this renderer to draw + const allRenderers = offscreenMultiRenderWindow.getRenderers(); + allRenderers.forEach(({ renderer: r, id }) => { + r.setDraw(id === viewport.id); + }); + + // Handle widget renderers + const widgetRenderers = this.getWidgetRenderers(); + widgetRenderers.forEach((viewportId, renderer) => { + renderer.setDraw(viewportId === viewport.id); + }); + + renderWindow.render(); + + allRenderers.forEach(({ renderer: r }) => r.setDraw(false)); + + widgetRenderers.forEach((_, renderer) => { + renderer.setDraw(false); + }); + + const openGLRenderWindow = + offscreenMultiRenderWindow.getOpenGLRenderWindow(); + const context = openGLRenderWindow.get3DContext(); + const offScreenCanvas = context.canvas; + + const eventDetail = this._copyToOnscreenCanvas(viewport, offScreenCanvas); + + return eventDetail; + } + + /** + * Renders a particular `Viewport`'s on screen canvas. + * @param viewport - The `Viewport` to render. + * @param offScreenCanvas - The offscreen canvas to render from. + */ + protected _renderViewportFromVtkCanvasToOnscreenCanvas( + viewport: IViewport, + offScreenCanvas: HTMLCanvasElement + ): EventTypes.ImageRenderedEventDetail { + return this._copyToOnscreenCanvas(viewport, offScreenCanvas); + } + + private _resizeOffScreenCanvasForViewport( + viewportCanvas: HTMLCanvasElement, + offScreenCanvasContainer: HTMLDivElement, + offscreenMultiRenderWindow: VtkOffscreenMultiRenderWindow + ): void { + const offScreenCanvasWidth = viewportCanvas.width; + const offScreenCanvasHeight = viewportCanvas.height; + + if ( + // @ts-expect-error + offScreenCanvasContainer.height === offScreenCanvasHeight && + // @ts-expect-error + offScreenCanvasContainer.width === offScreenCanvasWidth + ) { + return; + } + + // @ts-expect-error + offScreenCanvasContainer.width = offScreenCanvasWidth; + // @ts-expect-error + offScreenCanvasContainer.height = offScreenCanvasHeight; + + offscreenMultiRenderWindow.resize(); + } + + private _copyToOnscreenCanvas( + viewport: IViewport, + offScreenCanvas: HTMLCanvasElement + ): EventTypes.ImageRenderedEventDetail { + const { + element, + canvas, + id: viewportId, + renderingEngineId, + suppressEvents, + } = viewport; + const { width: dWidth, height: dHeight } = canvas; + + const onScreenContext = canvas.getContext('2d'); + onScreenContext.drawImage( + offScreenCanvas, + 0, + 0, + dWidth, + dHeight, + 0, + 0, + dWidth, + dHeight + ); + + return { + element, + suppressEvents, + viewportId, + renderingEngineId, + viewportStatus: viewport.viewportStatus, + }; + } + + private _resize( + viewportsDrivenByVtkJs: (IStackViewport | IVolumeViewport)[] + ): void { + for (const viewport of viewportsDrivenByVtkJs) { + viewport.sx = 0; + viewport.sy = 0; + viewport.sWidth = viewport.canvas.width; + viewport.sHeight = viewport.canvas.height; + + // Get the context assigned to this viewport + const contextIndex = this.contextPool.getContextIndexForViewport( + viewport.id + ); + + const contextData = this.contextPool.getContextByIndex(contextIndex); + const { context: offscreenMultiRenderWindow } = contextData; + const renderer = offscreenMultiRenderWindow.getRenderer(viewport.id); + + renderer.setViewport(0, 0, 1, 1); + } + } + + private getWidgetRenderers(): Map { + const allViewports = this._getViewportsAsArray(); + const widgetRenderers = new Map(); + + allViewports.forEach((vp) => { + const widgets = vp.getWidgets ? vp.getWidgets() : []; + widgets.forEach((widget) => { + const renderer = widget.getRenderer ? widget.getRenderer() : null; + if (renderer) { + widgetRenderers.set(renderer, vp.id); + } + }); + }); + + return widgetRenderers; + } + + /** + * Get the renderer for a specific viewport + * @param viewportId - The ID of the viewport + * @returns The vtkRenderer instance or undefined + */ + public getRenderer(viewportId: string): vtkRenderer | undefined { + const contextIndex = + this.contextPool?.getContextIndexForViewport(viewportId); + + const contextData = this.contextPool.getContextByIndex(contextIndex); + + const { context: offscreenMultiRenderWindow } = contextData; + return offscreenMultiRenderWindow.getRenderer(viewportId); + } + + /** + * Disables the requested viewportId from the rendering engine. + * Calls the base implementation and then handles context-specific cleanup. + * + * @param viewportId - viewport Id + */ + public disableElement(viewportId: string): void { + const viewport = this.getViewport(viewportId); + if (!viewport) { + return; + } + super.disableElement(viewportId); + + if ( + !viewportTypeUsesCustomRenderingPipeline(viewport.type) && + !this.useCPURendering + ) { + const contextIndex = + this.contextPool.getContextIndexForViewport(viewportId); + + if (contextIndex !== undefined) { + const contextData = this.contextPool.getContextByIndex(contextIndex); + + if (contextData) { + const { context: offscreenMultiRenderWindow } = contextData; + offscreenMultiRenderWindow.removeRenderer(viewportId); + } + } + } + } + + public destroy(): void { + if (this.contextPool) { + this.contextPool.destroy(); + } + super.destroy(); + } + + public getOffscreenMultiRenderWindow( + viewportId: string + ): VtkOffscreenMultiRenderWindow { + if (this.useCPURendering) { + throw new Error( + 'Offscreen multi render window is not available when using CPU rendering.' + ); + } + + const contextIndex = + this.contextPool.getContextIndexForViewport(viewportId); + + const contextData = this.contextPool.getContextByIndex(contextIndex); + + return contextData.context; + } +} + +export default ContextPoolRenderingEngine; diff --git a/packages/core/src/RenderingEngine/RenderingEngine.ts b/packages/core/src/RenderingEngine/RenderingEngine.ts index 04026a37c3..e6d3a006a5 100644 --- a/packages/core/src/RenderingEngine/RenderingEngine.ts +++ b/packages/core/src/RenderingEngine/RenderingEngine.ts @@ -1,1417 +1,118 @@ -import Events from '../enums/Events'; -import renderingEngineCache from './renderingEngineCache'; -import eventTarget from '../eventTarget'; -import uuidv4 from '../utilities/uuidv4'; -import triggerEvent from '../utilities/triggerEvent'; -import { vtkOffscreenMultiRenderWindow } from './vtkClasses'; -import ViewportType from '../enums/ViewportType'; -import VolumeViewport from './VolumeViewport'; -import BaseVolumeViewport from './BaseVolumeViewport'; -import StackViewport from './StackViewport'; -import viewportTypeUsesCustomRenderingPipeline from './helpers/viewportTypeUsesCustomRenderingPipeline'; -import getOrCreateCanvas from './helpers/getOrCreateCanvas'; -import { getShouldUseCPURendering, isCornerstoneInitialized } from '../init'; -import type IStackViewport from '../types/IStackViewport'; -import type IVolumeViewport from '../types/IVolumeViewport'; -import viewportTypeToViewportClass from './helpers/viewportTypeToViewportClass'; - -import type * as EventTypes from '../types/EventTypes'; +import { getConfiguration } from '../init'; +import TiledRenderingEngine from './TiledRenderingEngine'; +import ContextPoolRenderingEngine from './ContextPoolRenderingEngine'; +import type BaseRenderingEngine from './BaseRenderingEngine'; import type { - ViewportInput, - PublicViewportInput, - InternalViewportInput, - NormalizedViewportInput, + IStackViewport, + IVolumeViewport, IViewport, -} from '../types/IViewport'; -import { OrientationAxis } from '../enums'; -import VolumeViewport3D from './VolumeViewport3D'; - -interface ViewportDisplayCoords { - sxStartDisplayCoords: number; - syStartDisplayCoords: number; - sxEndDisplayCoords: number; - syEndDisplayCoords: number; - sx: number; - sy: number; - sWidth: number; - sHeight: number; -} - -// Rendering engines seem to not like rendering things less than 2 pixels per side -const VIEWPORT_MIN_SIZE = 2; + PublicViewportInput, + VtkOffscreenMultiRenderWindow, +} from '../types'; +import { RenderingEngineModeEnum } from '../enums'; -/** - * A RenderingEngine takes care of the full pipeline of creating viewports and rendering - * them on a large offscreen canvas and transmitting this data back to the screen. This allows us - * to leverage the power of vtk.js whilst only using one WebGL context for the processing, and allowing - * us to share texture memory across on-screen viewports that show the same data. - * - * - * Instantiating a rendering engine: - * ```js - * const renderingEngine = new RenderingEngine('pet-ct-rendering-engine'); - * ``` - * - * There are various ways you can trigger a render on viewports. The simplest is to call `render()` - * on the rendering engine; however, it will trigger a render on all viewports. A more efficient - * way to do this is to call `renderViewports([viewportId])` on the rendering engine to - * trigger a render on a specific viewport(s). Each viewport also has a `.render` method which can be used to trigger a render on that - * viewport. - * - * - * RenderingEngine checks for WebGL context availability to determine if GPU rendering is possible. - * If a WebGL context is not available, RenderingEngine will fall back to CPU rendering for StackViewports. - * However, for volume rendering, GPU availability is required, and an error will be thrown if attempted without GPU support. - * - * By default, RenderingEngine will use the vtk.js-enabled pipeline for rendering viewports. - * However, if a custom rendering pipeline is specified by a custom viewport, it will be used instead. - * We use this custom pipeline to render a StackViewport on CPU using the Cornerstone legacy CPU rendering pipeline. - * - * @public - */ class RenderingEngine { - /** Unique identifier for renderingEngine */ - readonly id: string; - /** A flag which tells if the renderingEngine has been destroyed or not */ public hasBeenDestroyed: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any - public offscreenMultiRenderWindow: any; - readonly offScreenCanvasContainer: HTMLDivElement; - private _viewports: Map; - private _needsRender = new Set(); - private _animationFrameSet = false; - private _animationFrameHandle: number | null = null; - private useCPURendering: boolean; + public offscreenMultiRenderWindow: VtkOffscreenMultiRenderWindow; + private _implementation?: BaseRenderingEngine; - /** - * @param uid - Unique identifier for RenderingEngine - */ constructor(id?: string) { - this.id = id ? id : uuidv4(); - this.useCPURendering = getShouldUseCPURendering(); - - renderingEngineCache.set(this); - - if (!isCornerstoneInitialized()) { - throw new Error( - '@cornerstonejs/core is not initialized, run init() first' - ); - } - - if (!this.useCPURendering) { - this.offscreenMultiRenderWindow = - vtkOffscreenMultiRenderWindow.newInstance(); - this.offScreenCanvasContainer = document.createElement('div'); - this.offscreenMultiRenderWindow.setContainer( - this.offScreenCanvasContainer - ); + const config = getConfiguration(); + const renderingEngineMode = config?.rendering?.renderingEngineMode; + + switch (renderingEngineMode) { + case RenderingEngineModeEnum.Tiled: + this._implementation = new TiledRenderingEngine(id); + break; + case RenderingEngineModeEnum.ContextPool: + this._implementation = new ContextPoolRenderingEngine(id); + break; + default: + console.warn( + `RenderingEngine: Unknown rendering engine mode "${renderingEngineMode}". Defaulting to Next rendering engine.` + ); + this._implementation = new ContextPoolRenderingEngine(id); + break; } + } - this._viewports = new Map(); - this.hasBeenDestroyed = false; + get id(): string { + return this._implementation.id; } - /** - * Enables the requested viewport and add it to the viewports. It will - * properly create the Stack viewport or Volume viewport: - * - * 1) Checks if the viewport is defined already, if yes, remove it first - * 2) Checks if the viewport is using a custom rendering pipeline, if no, - * it calculates a new offscreen canvas with the new requested viewport - * 3) Adds the viewport - * - * - * ``` - * renderingEngine.enableElement({ - * viewportId: viewportId, - * type: ViewportType.ORTHOGRAPHIC, - * element, - * defaultOptions: { - * orientation: Enums.OrientationAxis.AXIAL, - * background: [1, 0, 1], - * }, - * }) - * ``` - * - * fires Events.ELEMENT_ENABLED - * - * @param viewportInputEntry - viewport specifications - */ public enableElement(viewportInputEntry: PublicViewportInput): void { - const viewportInput = this._normalizeViewportInputEntry(viewportInputEntry); - - this._throwIfDestroyed(); - const { element, viewportId } = viewportInput; - - // Throw error if no canvas - if (!element) { - throw new Error('No element provided'); - } - - // 1. Get the viewport from the list of available viewports. - const viewport = this.getViewport(viewportId); - - // 1.a) If there is a found viewport, we remove the viewport and create a new viewport - if (viewport) { - this.disableElement(viewportId); - } - - // 2.a) See if viewport uses a custom rendering pipeline. - const { type } = viewportInput; - - const viewportUsesCustomRenderingPipeline = - viewportTypeUsesCustomRenderingPipeline(type); - - // 2.b) Retrieving the list of viewports for calculation of the new size for - // offScreen canvas. - - // If the viewport being added uses a custom pipeline, or we aren't using - // GPU rendering, we don't need to resize the offscreen canvas. - if (!this.useCPURendering && !viewportUsesCustomRenderingPipeline) { - this.enableVTKjsDrivenViewport(viewportInput); - } else { - // 3 Add the requested viewport to rendering Engine - this.addCustomViewport(viewportInput); - } - - // 5. Set the background color for the canvas - const canvas = getOrCreateCanvas(element); - const { background } = viewportInput.defaultOptions; - this.fillCanvasWithBackgroundColor(canvas, background); + return this._implementation.enableElement(viewportInputEntry); } - /** - * Disables the requested viewportId from the rendering engine: - * - * 1) It removes the viewport from the the list of viewports - * 2) remove the renderer from the offScreen render window if using vtk.js driven - * rendering pipeline - * 3) resetting the viewport to remove the canvas attributes and canvas data - * 4) resize the offScreen appropriately (if using vtk.js driven rendering pipeline) - * - * fires Events.ELEMENT_ENABLED - * - * @param viewportId - viewport Id - * - */ public disableElement(viewportId: string): void { - this._throwIfDestroyed(); - // 1. Getting the viewport to remove it - const viewport = this.getViewport(viewportId); - - // 2 To throw if there is no viewport stored in rendering engine - if (!viewport) { - console.warn(`viewport ${viewportId} does not exist`); - return; - } - - // 3. Reset the viewport to remove attributes, and reset the canvas - this._resetViewport(viewport); - - // 4. Remove the related renderer from the offScreenMultiRenderWindow - if ( - !viewportTypeUsesCustomRenderingPipeline(viewport.type) && - !this.useCPURendering - ) { - this.offscreenMultiRenderWindow.removeRenderer(viewportId); - } - - // 5. Remove the requested viewport from the rendering engine - this._removeViewport(viewportId); - viewport.isDisabled = true; - - // 6. Avoid rendering for the disabled viewport - this._needsRender.delete(viewportId); - - // 7. Clear RAF if no viewport is left - const viewports = this.getViewports(); - if (!viewports.length) { - this._clearAnimationFrame(); - } - - // Note: we should not call resize at the end of here, the reason is that - // in batch rendering, we might disable a viewport and enable others at the same - // time which would interfere with each other. So we just let the enable - // to call resize, and also resize getting called by applications on the - // DOM resize event. + return this._implementation.disableElement(viewportId); } - /** - * It takes an array of viewport input objects including element, viewportId, type - * and defaultOptions. It will add the viewport to the rendering engine and enables them. - * - * - * ```typescript - *renderingEngine.setViewports([ - * { - * viewportId: axialViewportId, - * type: ViewportType.ORTHOGRAPHIC, - * element: document.getElementById('axialDiv'), - * defaultOptions: { - * orientation: Enums.OrientationAxis.AXIAL, - * }, - * }, - * { - * viewportId: sagittalViewportId, - * type: ViewportType.ORTHOGRAPHIC, - * element: document.getElementById('sagittalDiv'), - * defaultOptions: { - * orientation: Enums.OrientationAxis.SAGITTAL, - * }, - * }, - * { - * viewportId: customOrientationViewportId, - * type: ViewportType.ORTHOGRAPHIC, - * element: document.getElementById('customOrientationDiv'), - * defaultOptions: { - * orientation: { viewPlaneNormal: [0, 0, 1], viewUp: [0, 1, 0] }, - * }, - * }, - * ]) - * ``` - * - * fires Events.ELEMENT_ENABLED - * - * @param viewportInputEntries - Array - */ - public setViewports(publicViewportInputEntries: PublicViewportInput[]): void { - const viewportInputEntries = this._normalizeViewportInputEntries( - publicViewportInputEntries - ); - this._throwIfDestroyed(); - this._reset(); - - // 1. Split viewports based on whether they use vtk.js or a custom pipeline. - - const vtkDrivenViewportInputEntries: NormalizedViewportInput[] = []; - const customRenderingViewportInputEntries: NormalizedViewportInput[] = []; - - viewportInputEntries.forEach((vpie) => { - if ( - !this.useCPURendering && - !viewportTypeUsesCustomRenderingPipeline(vpie.type) - ) { - vtkDrivenViewportInputEntries.push(vpie); - } else { - customRenderingViewportInputEntries.push(vpie); - } - }); - - this.setVtkjsDrivenViewports(vtkDrivenViewportInputEntries); - this.setCustomViewports(customRenderingViewportInputEntries); - - // Making sure the setViewports api also can fill the canvas - // properly - viewportInputEntries.forEach((vp) => { - const canvas = getOrCreateCanvas(vp.element); - const { background } = vp.defaultOptions; - this.fillCanvasWithBackgroundColor(canvas, background); - }); + return this._implementation.setViewports(publicViewportInputEntries); } - /** - * Resizes the offscreen viewport and recalculates translations to on screen canvases. - * It is up to the parent app to call the resize of the on-screen canvas changes. - * This is left as an app level concern as one might want to debounce the changes, or the like. - * - * @param immediate - Whether all of the viewports should be rendered immediately. - * @param keepCamera - Whether to keep the camera for other viewports while resizing the offscreen canvas - */ public resize(immediate = true, keepCamera = true): void { - this._throwIfDestroyed(); - // 1. Get the viewports' canvases - const viewports = this._getViewportsAsArray(); - - const vtkDrivenViewports = []; - const customRenderingViewports = []; - - viewports.forEach((vpie) => { - if (!viewportTypeUsesCustomRenderingPipeline(vpie.type)) { - vtkDrivenViewports.push(vpie); - } else { - customRenderingViewports.push(vpie); - } - }); - - if (vtkDrivenViewports.length) { - this._resizeVTKViewports(vtkDrivenViewports, keepCamera, immediate); - } - - if (customRenderingViewports.length) { - this._resizeUsingCustomResizeHandler( - customRenderingViewports, - keepCamera, - immediate - ); - } + return this._implementation.resize(immediate, keepCamera); } - /** - * Returns the viewport by Id - * - * @returns viewport - */ public getViewport(viewportId: string): IViewport { - return this._viewports?.get(viewportId); + return this._implementation.getViewport(viewportId); } - /** - * getViewports Returns an array of all `Viewport`s on the `RenderingEngine` instance. - * - * @returns Array of viewports - */ public getViewports(): IViewport[] { - this._throwIfDestroyed(); - - return this._getViewportsAsArray(); + return this._implementation.getViewports(); } - /** - * Retrieves a stack viewport by its ID. used just for type safety - * - * @param viewportId - The ID of the viewport to retrieve. - * @returns The stack viewport with the specified ID. - * @throws Error if the viewport with the given ID does not exist or is not a StackViewport. - */ public getStackViewport(viewportId: string): IStackViewport { - this._throwIfDestroyed(); - - const viewport = this.getViewport(viewportId); - - if (!viewport) { - throw new Error(`Viewport with Id ${viewportId} does not exist`); - } - - if (!(viewport instanceof StackViewport)) { - throw new Error(`Viewport with Id ${viewportId} is not a StackViewport.`); - } - - return viewport; + return this._implementation.getStackViewport(viewportId); } - /** - * Filters all the available viewports and return the stack viewports - * @returns stack viewports registered on the rendering Engine - */ public getStackViewports(): IStackViewport[] { - this._throwIfDestroyed(); - - const viewports = this.getViewports(); - - return viewports.filter( - (vp) => vp instanceof StackViewport - ) as IStackViewport[]; + return this._implementation.getStackViewports(); } - /** - * Return all the viewports that are volume viewports - * @returns An array of VolumeViewport objects. - */ - public getVolumeViewports(): IVolumeViewport[] { - this._throwIfDestroyed(); - - const viewports = this.getViewports(); - - const isVolumeViewport = ( - viewport: IViewport - ): viewport is BaseVolumeViewport => { - return viewport instanceof BaseVolumeViewport; - }; - - return viewports.filter(isVolumeViewport) as IVolumeViewport[]; - } - - /** - * Renders all viewports on the next animation frame. - * - * fires Events.IMAGE_RENDERED - */ - public render(): void { - const viewports = this.getViewports(); - const viewportIds = viewports.map((vp) => vp.id); - - this._setViewportsToBeRenderedNextFrame(viewportIds); - } - - /** - * Renders any viewports viewing the given Frame Of Reference. - * - * @param FrameOfReferenceUID - The unique identifier of the - * Frame Of Reference. - */ - public renderFrameOfReference = (FrameOfReferenceUID: string): void => { - const viewports = this._getViewportsAsArray(); - const viewportIdsWithSameFrameOfReferenceUID = viewports.map((vp) => { - if (vp.getFrameOfReferenceUID() === FrameOfReferenceUID) { - return vp.id; - } - }); - - this.renderViewports(viewportIdsWithSameFrameOfReferenceUID); - }; - /** - * Renders the provided Viewport IDs. - * - */ - public renderViewports(viewportIds: string[]): void { - this._setViewportsToBeRenderedNextFrame(viewportIds); + public getVolumeViewports(): IVolumeViewport[] { + return this._implementation.getVolumeViewports(); } - /** - * Renders only a specific `Viewport` on the next animation frame. - * - * @param viewportId - The Id of the viewport. - */ - public renderViewport(viewportId: string): void { - this._setViewportsToBeRenderedNextFrame([viewportId]); + public getRenderer(viewportId: string) { + return this._implementation.getRenderer(viewportId); } - /** - * destroy the rendering engine. It will remove all the viewports and, - * if the rendering engine is using the GPU, it will also destroy the GPU - * resources. - */ - public destroy(): void { - if (this.hasBeenDestroyed) { - return; - } - - // remove vtk rendered first before resetting the viewport - if (!this.useCPURendering) { - const viewports = this._getViewportsAsArray(); - viewports.forEach((vp) => { - this.offscreenMultiRenderWindow.removeRenderer(vp.id); - }); - - // Free up WebGL resources - this.offscreenMultiRenderWindow.delete(); - - // Make sure all references go stale and are garbage collected. - delete this.offscreenMultiRenderWindow; - } - - this._reset(); - renderingEngineCache.delete(this.id); - - this.hasBeenDestroyed = true; - } - - /** - * Fill the canvas with the background color - * @param canvas - The canvas element to draw on. - * @param backgroundColor - An array of three numbers between 0 and 1 that - * specify the red, green, and blue values of the background color. - */ public fillCanvasWithBackgroundColor( canvas: HTMLCanvasElement, backgroundColor: [number, number, number] - ): void { - const ctx = canvas.getContext('2d'); - - // Default to black if no background color is set - let fillStyle; - if (backgroundColor) { - const rgb = backgroundColor.map((f) => Math.floor(255 * f)); - fillStyle = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; - } else { - fillStyle = 'black'; - } - - // We draw over the previous stack with the background color while we - // wait for the next stack to load - ctx.fillStyle = fillStyle; - ctx.fillRect(0, 0, canvas.width, canvas.height); - } - - private _normalizeViewportInputEntry( - viewportInputEntry: PublicViewportInput ) { - const { type, defaultOptions } = viewportInputEntry; - let options = defaultOptions; - - if (!options || Object.keys(options).length === 0) { - options = { - background: [0, 0, 0], - orientation: null, - displayArea: null, - }; - - if (type === ViewportType.ORTHOGRAPHIC) { - options = { - ...options, - orientation: OrientationAxis.AXIAL, - }; - } - } - - return { - ...viewportInputEntry, - defaultOptions: options, - }; - } - - private _normalizeViewportInputEntries( - viewportInputEntries: PublicViewportInput[] - ): NormalizedViewportInput[] { - const normalizedViewportInputs = []; - - viewportInputEntries.forEach((viewportInput) => { - normalizedViewportInputs.push( - this._normalizeViewportInputEntry(viewportInput) - ); - }); - - return normalizedViewportInputs; - } - - private _resizeUsingCustomResizeHandler( - customRenderingViewports: StackViewport[], - keepCamera = true, - immediate = true - ) { - // 1. If viewport has a custom resize method, call it here. - customRenderingViewports.forEach((vp) => { - if (typeof vp.resize === 'function') { - vp.resize(); - } - }); - - // 3. Reset viewport cameras - customRenderingViewports.forEach((vp) => { - const prevCamera = vp.getCamera(); - vp.resetCamera(); - - if (keepCamera) { - vp.setCamera(prevCamera); - } - }); - - // 2. If render is immediate: Render all - if (immediate) { - this.render(); - } - } - - private _resizeVTKViewports( - vtkDrivenViewports: (IStackViewport | IVolumeViewport)[], - keepCamera = true, - immediate = true - ) { - // Ensure all the canvases are ready for rendering - const canvasesDrivenByVtkJs = vtkDrivenViewports.map( - (vp: IStackViewport | IVolumeViewport) => { - return getOrCreateCanvas(vp.element); - } - ); - - // reset the canvas size to the client size - canvasesDrivenByVtkJs.forEach((canvas) => { - const devicePixelRatio = window.devicePixelRatio || 1; - canvas.width = canvas.clientWidth * devicePixelRatio; - canvas.height = canvas.clientHeight * devicePixelRatio; - }); - - if (canvasesDrivenByVtkJs.length) { - // 1. Recalculate and resize the offscreen canvas size - const { offScreenCanvasWidth, offScreenCanvasHeight } = - this._resizeOffScreenCanvas(canvasesDrivenByVtkJs); - - // 2. Recalculate the viewports location on the off screen canvas - this._resize( - vtkDrivenViewports, - offScreenCanvasWidth, - offScreenCanvasHeight - ); - } - - // 3. Reset viewport cameras - vtkDrivenViewports.forEach((vp: IStackViewport | IVolumeViewport) => { - const prevCamera = vp.getCamera(); - const rotation = vp.getRotation(); - const { flipHorizontal } = prevCamera; - vp.resetCameraForResize(); - - const displayArea = vp.getDisplayArea(); - - // TODO - make this use get/set Presentation or in some way preserve the - // basic presentation info on this viewport, rather than preserving camera - if (keepCamera) { - if (displayArea) { - if (flipHorizontal) { - vp.setCamera({ flipHorizontal }); - } - if (rotation) { - vp.setViewPresentation({ rotation }); - } - } else { - vp.setCamera(prevCamera); - } - } - }); - - // 4. If render is immediate: Render all - if (immediate) { - this.render(); - } - } - - /** - * Enables a viewport to be driven by the offscreen vtk.js rendering engine. - * - * @param viewportInputEntry - Information object used to - * construct and enable the viewport. - */ - private enableVTKjsDrivenViewport( - viewportInputEntry: NormalizedViewportInput - ) { - const viewports = this._getViewportsAsArray(); - const viewportsDrivenByVtkJs = viewports.filter( - (vp) => viewportTypeUsesCustomRenderingPipeline(vp.type) === false - ); - - const canvasesDrivenByVtkJs = viewportsDrivenByVtkJs.map((vp) => vp.canvas); - - const canvas = getOrCreateCanvas(viewportInputEntry.element); - canvasesDrivenByVtkJs.push(canvas); - - // 2.c Calculating the new size for offScreen Canvas - const { offScreenCanvasWidth, offScreenCanvasHeight } = - this._resizeOffScreenCanvas(canvasesDrivenByVtkJs); - - // 2.d Re-position previous viewports on the offScreen Canvas based on the new - // offScreen canvas size - const xOffset = this._resize( - viewportsDrivenByVtkJs as (IStackViewport | IVolumeViewport)[], - offScreenCanvasWidth, - offScreenCanvasHeight - ); - - const internalViewportEntry = { ...viewportInputEntry, canvas }; - - // 3 Add the requested viewport to rendering Engine - this.addVtkjsDrivenViewport(internalViewportEntry, { - offScreenCanvasWidth, - offScreenCanvasHeight, - xOffset, - }); - } - - /** - * Disables the requested viewportId from the rendering engine: - * 1) It removes the viewport from the the list of viewports - * 2) remove the renderer from the offScreen render window - * 3) resetting the viewport to remove the canvas attributes and canvas data - * 4) resize the offScreen appropriately - * - * @param viewportId - viewport Id - * - */ - private _removeViewport(viewportId: string): void { - // 1. Get the viewport - const viewport = this.getViewport(viewportId); - if (!viewport) { - console.warn(`viewport ${viewportId} does not exist`); - return; - } - - // 2. Delete the viewports from the the viewports - this._viewports.delete(viewportId); - } - - /** - * Adds a viewport driven by vtk.js to the `RenderingEngine`. - * - * @param viewportInputEntry - Information object used to construct and enable the viewport. - * @param options - Options object used to configure the viewport. - * @param options.offScreenCanvasWidth - The width of the offscreen canvas. - * @param options.offScreenCanvasHeight - The height of the offscreen canvas. - * @param options.xOffset - The x offset of the viewport on the offscreen canvas. - */ - private addVtkjsDrivenViewport( - viewportInputEntry: InternalViewportInput, - offscreenCanvasProperties?: { - offScreenCanvasWidth: number; - offScreenCanvasHeight: number; - xOffset: number; - } - ): void { - const { element, canvas, viewportId, type, defaultOptions } = - viewportInputEntry; - - // Make the element not focusable, we use this for modifier keys to work - element.tabIndex = -1; - - const { offScreenCanvasWidth, offScreenCanvasHeight, xOffset } = - offscreenCanvasProperties; - - // 1. Calculate the size of location of the viewport on the offScreen canvas - const { - sxStartDisplayCoords, - syStartDisplayCoords, - sxEndDisplayCoords, - syEndDisplayCoords, - sx, - sy, - sWidth, - sHeight, - } = this._getViewportCoordsOnOffScreenCanvas( - viewportInputEntry, - offScreenCanvasWidth, - offScreenCanvasHeight, - xOffset - ); - // 2. Add a renderer to the offScreenMultiRenderWindow - this.offscreenMultiRenderWindow.addRenderer({ - viewport: [ - sxStartDisplayCoords, - syStartDisplayCoords, - sxEndDisplayCoords, - syEndDisplayCoords, - ], - id: viewportId, - background: defaultOptions.background - ? defaultOptions.background - : [0, 0, 0], - }); - - // 3. ViewportInput to be passed to a stack/volume viewport - const viewportInput = { - id: viewportId, - element, // div - renderingEngineId: this.id, - type, - canvas, - sx, - sy, - sWidth, - sHeight, - defaultOptions: defaultOptions || {}, - } as ViewportInput; - - // 4. Create a proper viewport based on the type of the viewport - let viewport; - if (type === ViewportType.STACK) { - // 4.a Create stack viewport - viewport = new StackViewport(viewportInput); - } else if ( - type === ViewportType.ORTHOGRAPHIC || - type === ViewportType.PERSPECTIVE - ) { - // 4.b Create a volume viewport - viewport = new VolumeViewport(viewportInput); - } else if (type === ViewportType.VOLUME_3D) { - viewport = new VolumeViewport3D(viewportInput); - } else { - throw new Error(`Viewport Type ${type} is not supported`); - } - - // 5. Storing the viewports - this._viewports.set(viewportId, viewport); - - const eventDetail: EventTypes.ElementEnabledEventDetail = { - element, - viewportId, - renderingEngineId: this.id, - }; - - if (!viewport.suppressEvents) { - triggerEvent(eventTarget, Events.ELEMENT_ENABLED, eventDetail); - } - } - - /** - * Adds a viewport using a custom rendering pipeline to the `RenderingEngine`. - * - * @param viewportInputEntry - Information object used to - * construct and enable the viewport. - */ - private addCustomViewport(viewportInputEntry: PublicViewportInput): void { - const { element, viewportId, type, defaultOptions } = viewportInputEntry; - - // Make the element not focusable, we use this for modifier keys to work - element.tabIndex = -1; - - const canvas = getOrCreateCanvas(element); - - // Add a viewport with no offset - const { clientWidth, clientHeight } = canvas; - - // Set the canvas to be same resolution as the client. - // Note: This ignores devicePixelRatio for now. We may want to change it in the - // future but it has no benefit for the Cornerstone CPU rendering pathway at the - // moment anyway. - if (canvas.width !== clientWidth || canvas.height !== clientHeight) { - canvas.width = clientWidth; - canvas.height = clientHeight; - } - - const viewportInput = { - id: viewportId, - renderingEngineId: this.id, - element, // div - type, - canvas, - sx: 0, // No offset, uses own renderer - sy: 0, - sWidth: clientWidth, - sHeight: clientHeight, - defaultOptions: defaultOptions || {}, - } as ViewportInput; - - // 4. Create a proper viewport based on the type of the viewport - const ViewportType = viewportTypeToViewportClass[type]; - const viewport = new ViewportType(viewportInput); - - // 5. Storing the viewports - this._viewports.set(viewportId, viewport); - - const eventDetail: EventTypes.ElementEnabledEventDetail = { - element, - viewportId, - renderingEngineId: this.id, - }; - - triggerEvent(eventTarget, Events.ELEMENT_ENABLED, eventDetail); - } - - /** - * Sets multiple viewports using custom rendering - * pipelines to the `RenderingEngine`. - * - * @param viewportInputEntries - An array of information - * objects used to construct and enable the viewports. - */ - private setCustomViewports(viewportInputEntries: PublicViewportInput[]) { - viewportInputEntries.forEach((vpie) => { - this.addCustomViewport(vpie); - }); - } - - /** - * Sets multiple vtk.js driven viewports to - * the `RenderingEngine`. - * - * @param viewportInputEntries - An array of information - * objects used to construct and enable the viewports. - */ - private setVtkjsDrivenViewports( - viewportInputEntries: NormalizedViewportInput[] - ) { - // Deal with vtkjs driven viewports - if (viewportInputEntries.length) { - // 1. Getting all the canvases from viewports calculation of the new offScreen size - const vtkDrivenCanvases = viewportInputEntries.map((vp) => - getOrCreateCanvas(vp.element) - ); - - // Ensure the canvas size includes any scaling due to device pixel ratio - vtkDrivenCanvases.forEach((canvas) => { - const devicePixelRatio = window.devicePixelRatio || 1; - - const rect = canvas.getBoundingClientRect(); - canvas.width = rect.width * devicePixelRatio; - canvas.height = rect.height * devicePixelRatio; - }); - - // 2. Set canvas size based on height and sum of widths - const { offScreenCanvasWidth, offScreenCanvasHeight } = - this._resizeOffScreenCanvas(vtkDrivenCanvases); - - /* - TODO: Commenting this out until we can mock the Canvas usage in the tests (or use jsdom?) - if (!offScreenCanvasWidth || !offScreenCanvasHeight) { - throw new Error('Invalid offscreen canvas width or height') - }*/ - - // 3. Adding the viewports based on the viewportInputEntry definition to the - // rendering engine. - let xOffset = 0; - for (let i = 0; i < viewportInputEntries.length; i++) { - const vtkDrivenViewportInputEntry = viewportInputEntries[i]; - const canvas = vtkDrivenCanvases[i]; - const internalViewportEntry = { - ...vtkDrivenViewportInputEntry, - canvas, - }; - - this.addVtkjsDrivenViewport(internalViewportEntry, { - offScreenCanvasWidth, - offScreenCanvasHeight, - xOffset, - }); - - // Incrementing the xOffset which provides the horizontal location of each - // viewport on the offScreen canvas - xOffset += canvas.width; - } - } - } - - /** - * Resizes the offscreen canvas based on the provided vtk.js driven canvases. - * - * @param canvases - An array of HTML Canvas - */ - private _resizeOffScreenCanvas(canvasesDrivenByVtkJs: HTMLCanvasElement[]): { - offScreenCanvasWidth: number; - offScreenCanvasHeight: number; - } { - const { offScreenCanvasContainer, offscreenMultiRenderWindow } = this; - - // 1. Calculated the height of the offScreen canvas to be the maximum height - // between canvases - const offScreenCanvasHeight = Math.max( - ...canvasesDrivenByVtkJs.map((canvas) => canvas.height) - ); - - // 2. Calculating the width of the offScreen canvas to be the sum of all - let offScreenCanvasWidth = 0; - - canvasesDrivenByVtkJs.forEach((canvas) => { - offScreenCanvasWidth += canvas.width; - }); - - // @ts-expect-error - offScreenCanvasContainer.width = offScreenCanvasWidth; - // @ts-expect-error - offScreenCanvasContainer.height = offScreenCanvasHeight; - - // 3. Resize command - offscreenMultiRenderWindow.resize(); - - return { offScreenCanvasWidth, offScreenCanvasHeight }; - } - - /** - * Recalculates and updates the viewports location on the offScreen canvas upon its resize - * - * @param viewports - An array of viewports - * @param offScreenCanvasWidth - new offScreen canvas width - * @param offScreenCanvasHeight - new offScreen canvas height - * - * @returns _xOffset the final offset which will be used for the next viewport - */ - private _resize( - viewportsDrivenByVtkJs: (IStackViewport | IVolumeViewport)[], - offScreenCanvasWidth: number, - offScreenCanvasHeight: number - ): number { - // Redefine viewport properties - let _xOffset = 0; - - for (let i = 0; i < viewportsDrivenByVtkJs.length; i++) { - const viewport = viewportsDrivenByVtkJs[i]; - const { - sxStartDisplayCoords, - syStartDisplayCoords, - sxEndDisplayCoords, - syEndDisplayCoords, - sx, - sy, - sWidth, - sHeight, - } = this._getViewportCoordsOnOffScreenCanvas( - viewport as IViewport, - offScreenCanvasWidth, - offScreenCanvasHeight, - _xOffset - ); - - _xOffset += viewport.canvas.width; - - viewport.sx = sx; - viewport.sy = sy; - viewport.sWidth = sWidth; - viewport.sHeight = sHeight; - - // Updating the renderer for the viewport - const renderer = this.offscreenMultiRenderWindow.getRenderer(viewport.id); - renderer.setViewport([ - sxStartDisplayCoords, - syStartDisplayCoords, - sxEndDisplayCoords, - syEndDisplayCoords, - ]); - } - - // Returns the final xOffset - return _xOffset; - } - - /** - * Calculates the location of the provided viewport on the offScreenCanvas - * - * @param viewports - An array of viewports - * @param offScreenCanvasWidth - new offScreen canvas width - * @param offScreenCanvasHeight - new offScreen canvas height - * @param _xOffset - xOffSet to draw - */ - private _getViewportCoordsOnOffScreenCanvas( - viewport: InternalViewportInput | IViewport, - offScreenCanvasWidth: number, - offScreenCanvasHeight: number, - _xOffset: number - ): ViewportDisplayCoords { - const { canvas } = viewport; - const { width: sWidth, height: sHeight } = canvas; - - // Update the canvas drawImage offsets. - const sx = _xOffset; - const sy = 0; - - const sxStartDisplayCoords = sx / offScreenCanvasWidth; - - // Need to offset y if it not max height - const syStartDisplayCoords = - sy + (offScreenCanvasHeight - sHeight) / offScreenCanvasHeight; - - const sWidthDisplayCoords = sWidth / offScreenCanvasWidth; - const sHeightDisplayCoords = sHeight / offScreenCanvasHeight; - - return { - sxStartDisplayCoords, - syStartDisplayCoords, - sxEndDisplayCoords: sxStartDisplayCoords + sWidthDisplayCoords, - syEndDisplayCoords: syStartDisplayCoords + sHeightDisplayCoords, - sx, - sy, - sWidth, - sHeight, - }; - } - - /** - * @method _getViewportsAsArray Returns an array of all viewports - * - * @returns {Array} Array of viewports. - */ - private _getViewportsAsArray() { - return Array.from(this._viewports.values()); - } - - private _setViewportsToBeRenderedNextFrame(viewportIds: string[]) { - // Add the viewports to the set of flagged viewports - viewportIds.forEach((viewportId) => { - this._needsRender.add(viewportId); - }); - - // Render any flagged viewports - this._render(); - } - - /** - * Sets up animation frame if necessary - */ - private _render() { - // If we have viewports that need rendering and we have not already - // set the RAF callback to run on the next frame. - if (this._needsRender.size > 0 && !this._animationFrameSet) { - this._animationFrameHandle = window.requestAnimationFrame( - this._renderFlaggedViewports - ); - - // Set the flag that we have already set up the next RAF call. - this._animationFrameSet = true; - } - } - - /** - * Renders all viewports. - */ - private _renderFlaggedViewports = () => { - this._throwIfDestroyed(); - - if (!this.useCPURendering) { - this.performVtkDrawCall(); - } - - const viewports = this._getViewportsAsArray(); - const eventDetailArray = []; - - for (let i = 0; i < viewports.length; i++) { - const viewport = viewports[i]; - if (this._needsRender.has(viewport.id)) { - const eventDetail = - this.renderViewportUsingCustomOrVtkPipeline(viewport); - eventDetailArray.push(eventDetail); - viewport.setRendered(); - - // This viewport has been rendered, we can remove it from the set - this._needsRender.delete(viewport.id); - - // If there is nothing left that is flagged for rendering, stop the loop - if (this._needsRender.size === 0) { - break; - } - } - } - - // allow RAF to be called again - this._animationFrameSet = false; - this._animationFrameHandle = null; - - eventDetailArray.forEach((eventDetail) => { - // Very small viewports won't have an element - if (!eventDetail?.element) { - return; - } - triggerEvent(eventDetail.element, Events.IMAGE_RENDERED, eventDetail); - }); - }; - - /** - * Performs the single `vtk.js` draw call which is used to render the offscreen - * canvas for vtk.js. This is a bulk rendering step for all Volume and Stack - * viewports when GPU rendering is available. - */ - private performVtkDrawCall() { - // Render all viewports under vtk.js' control. - const { offscreenMultiRenderWindow } = this; - const renderWindow = offscreenMultiRenderWindow.getRenderWindow(); - - const renderers = offscreenMultiRenderWindow.getRenderers(); - - if (!renderers.length) { - return; - } - - for (let i = 0; i < renderers.length; i++) { - const { renderer, id } = renderers[i]; - - // Requesting viewports that need rendering to be rendered only - if (this._needsRender.has(id)) { - renderer.setDraw(true); - } else { - renderer.setDraw(false); - } - } - - renderWindow.render(); - - // After redraw we set all renderers to not render until necessary - for (let i = 0; i < renderers.length; i++) { - renderers[i].renderer.setDraw(false); - } - } - - /** - * Renders the given viewport - * using its proffered method. - * - * @param viewport - The viewport to render - */ - private renderViewportUsingCustomOrVtkPipeline( - viewport: IViewport - ): EventTypes.ImageRenderedEventDetail[] { - let eventDetail; - - // Rendering engines start having issues without at least two pixels - // in each direction - if ( - viewport.sWidth < VIEWPORT_MIN_SIZE || - viewport.sHeight < VIEWPORT_MIN_SIZE - ) { - console.warn('Viewport is too small', viewport.sWidth, viewport.sHeight); - return; - } - if (viewportTypeUsesCustomRenderingPipeline(viewport.type) === true) { - eventDetail = - viewport.customRenderViewportToCanvas() as EventTypes.ImageRenderedEventDetail; - } else { - if (this.useCPURendering) { - throw new Error( - 'GPU not available, and using a viewport with no custom render pipeline.' - ); - } - - const { offscreenMultiRenderWindow } = this; - const openGLRenderWindow = - offscreenMultiRenderWindow.getOpenGLRenderWindow(); - const context = openGLRenderWindow.get3DContext(); - const offScreenCanvas = context.canvas; - - eventDetail = this._renderViewportFromVtkCanvasToOnscreenCanvas( - viewport, - offScreenCanvas - ); - } - - return eventDetail; - } - - /** - * Renders a particular `Viewport`'s on screen canvas. - * @param viewport - The `Viewport` to render. - * @param offScreenCanvas - The offscreen canvas to render from. - */ - private _renderViewportFromVtkCanvasToOnscreenCanvas( - viewport: IViewport, - offScreenCanvas - ): EventTypes.ImageRenderedEventDetail { - const { - element, + return this._implementation.fillCanvasWithBackgroundColor( canvas, - sx, - sy, - sWidth, - sHeight, - id: viewportId, - renderingEngineId, - suppressEvents, - } = viewport; - - const { width: dWidth, height: dHeight } = canvas; - - const onScreenContext = canvas.getContext('2d'); - - onScreenContext.drawImage( - offScreenCanvas, - sx, - sy, - sWidth, - sHeight, - 0, //dx - 0, // dy - dWidth, - dHeight + backgroundColor ); - - return { - element, - suppressEvents, - viewportId, - renderingEngineId, - viewportStatus: viewport.viewportStatus, - }; } - /** - * Reset the viewport by removing the data attributes - * and clearing the context of draw. It also emits an element disabled event - * - * @param viewport - The `Viewport` to render. - */ - private _resetViewport(viewport: IViewport) { - const renderingEngineId = this.id; - - const { element, canvas, id: viewportId } = viewport; - - const eventDetail: EventTypes.ElementDisabledEventDetail = { - element, - viewportId, - renderingEngineId, - }; - - viewport.removeWidgets(); - - // Trigger first before removing the data attributes, as we need the enabled - // element to remove tools associated with the viewport - triggerEvent(eventTarget, Events.ELEMENT_DISABLED, eventDetail); - - element.removeAttribute('data-viewport-uid'); - element.removeAttribute('data-rendering-engine-uid'); - - // clear drawing - const context = canvas.getContext('2d'); - context.clearRect(0, 0, canvas.width, canvas.height); - } - - private _clearAnimationFrame() { - window.cancelAnimationFrame(this._animationFrameHandle); - - this._needsRender.clear(); - this._animationFrameSet = false; - this._animationFrameHandle = null; + public render(): void { + return this._implementation.render(); } - /** - * Resets the `RenderingEngine` - */ - private _reset() { - const viewports = this._getViewportsAsArray(); - - viewports.forEach((viewport) => { - this._resetViewport(viewport); - }); - - this._clearAnimationFrame(); - - this._viewports = new Map(); + public renderViewports(viewportIds: string[]): void { + return this._implementation.renderViewports(viewportIds); } - /** - * Throws an error if trying to interact with the `RenderingEngine` - * instance after its `destroy` method has been called. - */ - private _throwIfDestroyed() { - if (this.hasBeenDestroyed) { - throw new Error( - 'this.destroy() has been manually called to free up memory, can not longer use this instance. Instead make a new one.' - ); - } + public renderViewport(viewportId: string): void { + return this._implementation.renderViewport(viewportId); } - // debugging utils for offScreen canvas - _downloadOffScreenCanvas() { - const dataURL = this._debugRender(); - _TEMPDownloadURI(dataURL); + public destroy(): void { + return this._implementation.destroy(); } - // debugging utils for offScreen canvas - _debugRender(): void { - const { offscreenMultiRenderWindow } = this; - const renderWindow = offscreenMultiRenderWindow.getRenderWindow(); - - const renderers = offscreenMultiRenderWindow.getRenderers(); - - for (let i = 0; i < renderers.length; i++) { - renderers[i].renderer.setDraw(true); - } - - renderWindow.render(); - const openGLRenderWindow = - offscreenMultiRenderWindow.getOpenGLRenderWindow(); - const context = openGLRenderWindow.get3DContext(); - - const offScreenCanvas = context.canvas; - const dataURL = offScreenCanvas.toDataURL(); - - this._getViewportsAsArray().forEach((viewport) => { - const { sx, sy, sWidth, sHeight } = viewport; - - const canvas = viewport.canvas; - const { width: dWidth, height: dHeight } = canvas; - - const onScreenContext = canvas.getContext('2d'); - - //sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight - onScreenContext.drawImage( - offScreenCanvas, - sx, - sy, - sWidth, - sHeight, - 0, //dx - 0, // dy - dWidth, - dHeight - ); - }); - - return dataURL; + public getOffscreenMultiRenderWindow( + viewportId?: string + ): VtkOffscreenMultiRenderWindow { + return this._implementation.getOffscreenMultiRenderWindow(viewportId); } } export default RenderingEngine; - -// debugging utils for offScreen canvas -function _TEMPDownloadURI(uri) { - const link = document.createElement('a'); - - link.download = 'viewport.png'; - link.href = uri; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); -} diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index 1ac9813646..e91496afbb 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -97,6 +97,7 @@ import uuidv4 from '../utilities/uuidv4'; import getSpacingInNormalDirection from '../utilities/getSpacingInNormalDirection'; import getClosestImageId from '../utilities/getClosestImageId'; import { adjustInitialViewUp } from '../utilities/adjustInitialViewUp'; +import { isContextPoolRenderingEngine } from './helpers/isContextPoolRenderingEngine'; const EPSILON = 1; // Slice Thickness @@ -234,6 +235,8 @@ class StackViewport extends Viewport { }; private _configureRenderingPipeline(value?: boolean) { + const isContextPool = isContextPoolRenderingEngine(); + this.useCPURendering = value ?? getShouldUseCPURendering(); for (const key in this.renderingPipelineFunctions) { @@ -244,7 +247,21 @@ class StackViewport extends Viewport { ) ) { const functions = this.renderingPipelineFunctions[key]; - this[key] = this.useCPURendering ? functions.cpu : functions.gpu; + if (this.useCPURendering) { + this[key] = functions.cpu; + } else { + if ( + typeof functions.gpu === 'object' && + functions.gpu.tiled && + functions.gpu.contextPool + ) { + this[key] = isContextPool + ? functions.gpu.contextPool + : functions.gpu.tiled; + } else { + this[key] = functions.gpu; + } + } } } @@ -351,13 +368,6 @@ class StackViewport extends Viewport { */ public worldToCanvas: (worldPos: Point3) => Point2; - /** - * If the renderer is CPU based, throw an error. Otherwise, returns the `vtkRenderer` responsible for rendering the `Viewport`. - * - * @returns The `vtkRenderer` for the `Viewport`. - */ - public getRenderer: () => vtkRenderer; - /** * If the renderer is CPU based, throw an error. Otherwise, return the default * actor which is the first actor in the renderer. @@ -2826,7 +2836,61 @@ class StackViewport extends Viewport { return canvasPoint; }; - private canvasToWorldGPU = (canvasPos: Point2): Point3 => { + private canvasToWorldGPUContextPool = (canvasPos: Point2): Point3 => { + const renderer = this.getRenderer(); + + // Temporary setting the clipping range to the distance and distance + 0.1 + // in order to calculate the transformations correctly. + // This is similar to the vtkSlabCamera isPerformingCoordinateTransformations + // You can read more about it here there. + const vtkCamera = this.getVtkActiveCamera(); + const crange = vtkCamera.getClippingRange(); + const distance = vtkCamera.getDistance(); + + vtkCamera.setClippingRange(distance, distance + 0.1); + + const devicePixelRatio = window.devicePixelRatio || 1; + const { width, height } = this.canvas; + const aspectRatio = width / height; + + // Convert canvas coordinates to normalized display coordinates + const canvasPosWithDPR = [ + canvasPos[0] * devicePixelRatio, + canvasPos[1] * devicePixelRatio, + ]; + + // Normalize to [0,1] range + const normalizedDisplay = [ + canvasPosWithDPR[0] / width, + 1 - canvasPosWithDPR[1] / height, // Flip Y axis + 0, + ]; + + // Transform from normalized display to world coordinates + const projCoords = renderer.normalizedDisplayToProjection( + normalizedDisplay[0], + normalizedDisplay[1], + normalizedDisplay[2] + ); + const viewCoords = renderer.projectionToView( + projCoords[0], + projCoords[1], + projCoords[2], + aspectRatio + ); + const worldCoord = renderer.viewToWorld( + viewCoords[0], + viewCoords[1], + viewCoords[2] + ); + + // set clipping range back to original to be able + vtkCamera.setClippingRange(crange[0], crange[1]); + + return [worldCoord[0], worldCoord[1], worldCoord[2]]; + }; + + private canvasToWorldGPUTiled = (canvasPos: Point2): Point3 => { const renderer = this.getRenderer(); // Temporary setting the clipping range to the distance and distance + 0.1 @@ -2846,6 +2910,7 @@ class StackViewport extends Viewport { const size = openGLRenderWindow.getSize(); const devicePixelRatio = window.devicePixelRatio || 1; + const canvasPosWithDPR = [ canvasPos[0] * devicePixelRatio, canvasPos[1] * devicePixelRatio, @@ -2871,7 +2936,59 @@ class StackViewport extends Viewport { return [worldCoord[0], worldCoord[1], worldCoord[2]]; }; - private worldToCanvasGPU = (worldPos: Point3): Point2 => { + private worldToCanvasGPUContextPool = (worldPos: Point3): Point2 => { + const renderer = this.getRenderer(); + + // Temporary setting the clipping range to the distance and distance + 0.1 + // in order to calculate the transformations correctly. + // This is similar to the vtkSlabCamera isPerformingCoordinateTransformations + // You can read more about it here there. + const vtkCamera = this.getVtkActiveCamera(); + const crange = vtkCamera.getClippingRange(); + const distance = vtkCamera.getDistance(); + + vtkCamera.setClippingRange(distance, distance + 0.1); + + const devicePixelRatio = window.devicePixelRatio || 1; + + const { width, height } = this.canvas; + + const aspectRatio = width / height; + + const viewCoords = renderer.worldToView( + worldPos[0], + worldPos[1], + worldPos[2] + ); + + const projCoords = renderer.viewToProjection( + viewCoords[0], + viewCoords[1], + viewCoords[2], + aspectRatio + ); + + const normalizedDisplay = renderer.projectionToNormalizedDisplay( + projCoords[0], + projCoords[1], + projCoords[2] + ); + + const canvasX = normalizedDisplay[0] * width; + const canvasY = (1 - normalizedDisplay[1]) * height; + + // set clipping range back to original to be able + vtkCamera.setClippingRange(crange[0], crange[1]); + + const canvasCoordWithDPR = [ + canvasX / devicePixelRatio, + canvasY / devicePixelRatio, + ] as Point2; + + return canvasCoordWithDPR; + }; + + private worldToCanvasGPUTiled = (worldPos: Point3): Point2 => { const renderer = this.getRenderer(); // Temporary setting the clipping range to the distance and distance + 0.1 @@ -2914,6 +3031,29 @@ class StackViewport extends Viewport { return canvasCoordWithDPR; }; + /** + * Get the renderer for this viewport - handles ContextPoolRenderingEngine + */ + public getRendererContextPool(): vtkRenderer { + const renderingEngine = this.getRenderingEngine(); + return renderingEngine.getRenderer(this.id); + } + + /** + * Returns the `vtkRenderer` responsible for rendering the `Viewport`. + * + * @returns The `vtkRenderer` for the `Viewport`. + */ + public getRendererTiled(): vtkRenderer { + const renderingEngine = this.getRenderingEngine(); + + if (!renderingEngine || renderingEngine.hasBeenDestroyed) { + throw new Error('Rendering engine has been destroyed'); + } + + return renderingEngine.offscreenMultiRenderWindow?.getRenderer(this.id); + } + private _getVOIRangeForCurrentImage() { const { windowCenter, windowWidth, voiLUTFunction } = this.csImage; @@ -3022,11 +3162,18 @@ class StackViewport extends Viewport { return testIndex <= rangeEndSliceIndex && testIndex >= foundSliceIndex; } - if (!super.isReferenceViewable(viewRef, options)) { + // Apply asVolume to withOrientation to indicate allowing changing orientation + // only if this gets converted to a volume. + if ( + !super.isReferenceViewable(viewRef, { + ...options, + withOrientation: options?.asVolume, + }) + ) { return false; } - if (viewRef.volumeId) { + if (viewRef.volumeId || viewRef.FrameOfReferenceUID) { return options.asVolume; } @@ -3382,15 +3529,24 @@ class StackViewport extends Viewport { }, canvasToWorld: { cpu: this.canvasToWorldCPU, - gpu: this.canvasToWorldGPU, + gpu: { + tiled: this.canvasToWorldGPUTiled, + contextPool: this.canvasToWorldGPUContextPool, + }, }, worldToCanvas: { cpu: this.worldToCanvasCPU, - gpu: this.worldToCanvasGPU, + gpu: { + tiled: this.worldToCanvasGPUTiled, + contextPool: this.worldToCanvasGPUContextPool, + }, }, getRenderer: { cpu: () => this.getCPUFallbackError('getRenderer'), - gpu: super.getRenderer, + gpu: { + tiled: this.getRendererTiled, + contextPool: this.getRendererContextPool, + }, }, getDefaultActor: { cpu: () => this.getCPUFallbackError('getDefaultActor'), diff --git a/packages/core/src/RenderingEngine/TiledRenderingEngine.ts b/packages/core/src/RenderingEngine/TiledRenderingEngine.ts new file mode 100644 index 0000000000..9a2405573b --- /dev/null +++ b/packages/core/src/RenderingEngine/TiledRenderingEngine.ts @@ -0,0 +1,648 @@ +import BaseRenderingEngine, { VIEWPORT_MIN_SIZE } from './BaseRenderingEngine'; +import Events from '../enums/Events'; +import eventTarget from '../eventTarget'; +import triggerEvent from '../utilities/triggerEvent'; +import ViewportType from '../enums/ViewportType'; +import VolumeViewport from './VolumeViewport'; +import StackViewport from './StackViewport'; +import viewportTypeUsesCustomRenderingPipeline from './helpers/viewportTypeUsesCustomRenderingPipeline'; +import getOrCreateCanvas from './helpers/getOrCreateCanvas'; +import type IStackViewport from '../types/IStackViewport'; +import type IVolumeViewport from '../types/IVolumeViewport'; +import VolumeViewport3D from './VolumeViewport3D'; +import { vtkOffscreenMultiRenderWindow } from './vtkClasses'; + +import type * as EventTypes from '../types/EventTypes'; +import type { + ViewportInput, + InternalViewportInput, + NormalizedViewportInput, + IViewport, +} from '../types/IViewport'; + +interface ViewportDisplayCoords { + sxStartDisplayCoords: number; + syStartDisplayCoords: number; + sxEndDisplayCoords: number; + syEndDisplayCoords: number; + sx: number; + sy: number; + sWidth: number; + sHeight: number; +} + +/** + * A RenderingEngine takes care of the full pipeline of creating viewports and rendering + * them on a large offscreen canvas and transmitting this data back to the screen. This allows us + * to leverage the power of vtk.js whilst only using one WebGL context for the processing, and allowing + * us to share texture memory across on-screen viewports that show the same data. + * + * + * Instantiating a rendering engine: + * ```js + * const renderingEngine = new RenderingEngine('pet-ct-rendering-engine'); + * ``` + * + * There are various ways you can trigger a render on viewports. The simplest is to call `render()` + * on the rendering engine; however, it will trigger a render on all viewports. A more efficient + * way to do this is to call `renderViewports([viewportId])` on the rendering engine to + * trigger a render on a specific viewport(s). Each viewport also has a `.render` method which can be used to trigger a render on that + * viewport. + * + * + * RenderingEngine checks for WebGL context availability to determine if GPU rendering is possible. + * If a WebGL context is not available, RenderingEngine will fall back to CPU rendering for StackViewports. + * However, for volume rendering, GPU availability is required, and an error will be thrown if attempted without GPU support. + * + * By default, RenderingEngine will use the vtk.js-enabled pipeline for rendering viewports. + * However, if a custom rendering pipeline is specified by a custom viewport, it will be used instead. + * We use this custom pipeline to render a StackViewport on CPU using the Cornerstone legacy CPU rendering pipeline. + * + * @public + */ +class TiledRenderingEngine extends BaseRenderingEngine { + constructor(id?: string) { + super(id); + + if (!this.useCPURendering) { + this.offscreenMultiRenderWindow = + vtkOffscreenMultiRenderWindow.newInstance(); + this.offScreenCanvasContainer = document.createElement('div'); + this.offscreenMultiRenderWindow.setContainer( + this.offScreenCanvasContainer + ); + } + } + /** + * Enables a viewport to be driven by the offscreen vtk.js rendering engine. + * + * @param viewportInputEntry - Information object used to + * construct and enable the viewport. + */ + protected enableVTKjsDrivenViewport( + viewportInputEntry: NormalizedViewportInput + ) { + const viewports = this._getViewportsAsArray(); + const viewportsDrivenByVtkJs = viewports.filter( + (vp) => viewportTypeUsesCustomRenderingPipeline(vp.type) === false + ); + + const canvasesDrivenByVtkJs = viewportsDrivenByVtkJs.map((vp) => vp.canvas); + + const canvas = getOrCreateCanvas(viewportInputEntry.element); + canvasesDrivenByVtkJs.push(canvas); + + // 2.c Calculating the new size for offScreen Canvas + const { offScreenCanvasWidth, offScreenCanvasHeight } = + this._resizeOffScreenCanvas(canvasesDrivenByVtkJs); + + // 2.d Re-position previous viewports on the offScreen Canvas based on the new + // offScreen canvas size + const xOffset = this._resize( + viewportsDrivenByVtkJs as (IStackViewport | IVolumeViewport)[], + offScreenCanvasWidth, + offScreenCanvasHeight + ); + + const internalViewportEntry = { ...viewportInputEntry, canvas }; + + // 3 Add the requested viewport to rendering Engine + this.addVtkjsDrivenViewport(internalViewportEntry, { + offScreenCanvasWidth, + offScreenCanvasHeight, + xOffset, + }); + } + + /** + * Adds a viewport driven by vtk.js to the `RenderingEngine`. + * + * @param viewportInputEntry - Information object used to construct and enable the viewport. + * @param options - Options object used to configure the viewport. + * @param options.offScreenCanvasWidth - The width of the offscreen canvas. + * @param options.offScreenCanvasHeight - The height of the offscreen canvas. + * @param options.xOffset - The x offset of the viewport on the offscreen canvas. + */ + protected addVtkjsDrivenViewport( + viewportInputEntry: InternalViewportInput, + offscreenCanvasProperties?: { + offScreenCanvasWidth: number; + offScreenCanvasHeight: number; + xOffset: number; + } + ): void { + const { element, canvas, viewportId, type, defaultOptions } = + viewportInputEntry; + + // Make the element not focusable, we use this for modifier keys to work + element.tabIndex = -1; + + const { offScreenCanvasWidth, offScreenCanvasHeight, xOffset } = + offscreenCanvasProperties; + + // 1. Calculate the size of location of the viewport on the offScreen canvas + const { + sxStartDisplayCoords, + syStartDisplayCoords, + sxEndDisplayCoords, + syEndDisplayCoords, + sx, + sy, + sWidth, + sHeight, + } = this._getViewportCoordsOnOffScreenCanvas( + viewportInputEntry, + offScreenCanvasWidth, + offScreenCanvasHeight, + xOffset + ); + // 2. Add a renderer to the offScreenMultiRenderWindow + this.offscreenMultiRenderWindow.addRenderer({ + viewport: [ + sxStartDisplayCoords, + syStartDisplayCoords, + sxEndDisplayCoords, + syEndDisplayCoords, + ], + id: viewportId, + background: defaultOptions.background + ? defaultOptions.background + : [0, 0, 0], + }); + + // 3. ViewportInput to be passed to a stack/volume viewport + const viewportInput = { + id: viewportId, + element, // div + renderingEngineId: this.id, + type, + canvas, + sx, + sy, + sWidth, + sHeight, + defaultOptions: defaultOptions || {}, + } as ViewportInput; + + // 4. Create a proper viewport based on the type of the viewport + let viewport: IViewport; + if (type === ViewportType.STACK) { + // 4.a Create stack viewport + viewport = new StackViewport(viewportInput); + } else if ( + type === ViewportType.ORTHOGRAPHIC || + type === ViewportType.PERSPECTIVE + ) { + // 4.b Create a volume viewport + viewport = new VolumeViewport(viewportInput); + } else if (type === ViewportType.VOLUME_3D) { + viewport = new VolumeViewport3D(viewportInput); + } else { + throw new Error(`Viewport Type ${type} is not supported`); + } + + // 5. Storing the viewports + this._viewports.set(viewportId, viewport); + + const eventDetail: EventTypes.ElementEnabledEventDetail = { + element, + viewportId, + renderingEngineId: this.id, + }; + + if (!viewport.suppressEvents) { + triggerEvent(eventTarget, Events.ELEMENT_ENABLED, eventDetail); + } + } + + /** + * Sets multiple vtk.js driven viewports to + * the `RenderingEngine`. + * + * @param viewportInputEntries - An array of information + * objects used to construct and enable the viewports. + */ + protected setVtkjsDrivenViewports( + viewportInputEntries: NormalizedViewportInput[] + ) { + // Deal with vtkjs driven viewports + if (viewportInputEntries.length) { + // 1. Getting all the canvases from viewports calculation of the new offScreen size + const vtkDrivenCanvases = viewportInputEntries.map((vp) => + getOrCreateCanvas(vp.element) + ); + + // Ensure the canvas size includes any scaling due to device pixel ratio + vtkDrivenCanvases.forEach((canvas) => { + const devicePixelRatio = window.devicePixelRatio || 1; + + const rect = canvas.getBoundingClientRect(); + canvas.width = rect.width * devicePixelRatio; + canvas.height = rect.height * devicePixelRatio; + }); + + // 2. Set canvas size based on height and sum of widths + const { offScreenCanvasWidth, offScreenCanvasHeight } = + this._resizeOffScreenCanvas(vtkDrivenCanvases); + + /* + TODO: Commenting this out until we can mock the Canvas usage in the tests (or use jsdom?) + if (!offScreenCanvasWidth || !offScreenCanvasHeight) { + throw new Error('Invalid offscreen canvas width or height') + }*/ + + // 3. Adding the viewports based on the viewportInputEntry definition to the + // rendering engine. + let xOffset = 0; + for (let i = 0; i < viewportInputEntries.length; i++) { + const vtkDrivenViewportInputEntry = viewportInputEntries[i]; + const canvas = vtkDrivenCanvases[i]; + const internalViewportEntry = { + ...vtkDrivenViewportInputEntry, + canvas, + }; + + this.addVtkjsDrivenViewport(internalViewportEntry, { + offScreenCanvasWidth, + offScreenCanvasHeight, + xOffset, + }); + + // Incrementing the xOffset which provides the horizontal location of each + // viewport on the offScreen canvas + xOffset += canvas.width; + } + } + } + + /** + * Resizes viewports that use VTK.js for rendering. + */ + protected _resizeVTKViewports( + vtkDrivenViewports: (IStackViewport | IVolumeViewport)[], + keepCamera = true, + immediate = true + ) { + // Ensure all the canvases are ready for rendering + const canvasesDrivenByVtkJs = vtkDrivenViewports.map( + (vp: IStackViewport | IVolumeViewport) => { + return getOrCreateCanvas(vp.element); + } + ); + + // reset the canvas size to the client size + canvasesDrivenByVtkJs.forEach((canvas) => { + const devicePixelRatio = window.devicePixelRatio || 1; + canvas.width = canvas.clientWidth * devicePixelRatio; + canvas.height = canvas.clientHeight * devicePixelRatio; + }); + + if (canvasesDrivenByVtkJs.length) { + // 1. Recalculate and resize the offscreen canvas size + const { offScreenCanvasWidth, offScreenCanvasHeight } = + this._resizeOffScreenCanvas(canvasesDrivenByVtkJs); + + // 2. Recalculate the viewports location on the off screen canvas + this._resize( + vtkDrivenViewports, + offScreenCanvasWidth, + offScreenCanvasHeight + ); + } + + // 3. Reset viewport cameras + vtkDrivenViewports.forEach((vp: IStackViewport | IVolumeViewport) => { + const prevCamera = vp.getCamera(); + const rotation = vp.getRotation(); + const { flipHorizontal } = prevCamera; + vp.resetCameraForResize(); + + const displayArea = vp.getDisplayArea(); + + // TODO - make this use get/set Presentation or in some way preserve the + // basic presentation info on this viewport, rather than preserving camera + if (keepCamera) { + if (displayArea) { + if (flipHorizontal) { + vp.setCamera({ flipHorizontal }); + } + if (rotation) { + vp.setViewPresentation({ rotation }); + } + } else { + vp.setCamera(prevCamera); + } + } + }); + + // 4. If render is immediate: Render all + if (immediate) { + this.render(); + } + } + + /** + * Renders all viewports. + */ + protected _renderFlaggedViewports = () => { + this._throwIfDestroyed(); + + if (!this.useCPURendering) { + this.performVtkDrawCall(); + } + + const viewports = this._getViewportsAsArray(); + const eventDetailArray = []; + + for (let i = 0; i < viewports.length; i++) { + const viewport = viewports[i]; + if (this._needsRender.has(viewport.id)) { + const eventDetail = + this.renderViewportUsingCustomOrVtkPipeline(viewport); + eventDetailArray.push(eventDetail); + viewport.setRendered(); + + // This viewport has been rendered, we can remove it from the set + this._needsRender.delete(viewport.id); + + // If there is nothing left that is flagged for rendering, stop the loop + if (this._needsRender.size === 0) { + break; + } + } + } + + // allow RAF to be called again + this._animationFrameSet = false; + this._animationFrameHandle = null; + + eventDetailArray.forEach((eventDetail) => { + // Very small viewports won't have an element + if (!eventDetail?.element) { + return; + } + triggerEvent(eventDetail.element, Events.IMAGE_RENDERED, eventDetail); + }); + }; + + /** + * Performs the single `vtk.js` draw call which is used to render the offscreen + * canvas for vtk.js. This is a bulk rendering step for all Volume and Stack + * viewports when GPU rendering is available. + */ + private performVtkDrawCall() { + // Render all viewports under vtk.js' control. + const { offscreenMultiRenderWindow } = this; + const renderWindow = offscreenMultiRenderWindow.getRenderWindow(); + + const renderers = offscreenMultiRenderWindow.getRenderers(); + + if (!renderers.length) { + return; + } + + for (let i = 0; i < renderers.length; i++) { + const { renderer, id } = renderers[i]; + + // Requesting viewports that need rendering to be rendered only + if (this._needsRender.has(id)) { + renderer.setDraw(true); + } else { + renderer.setDraw(false); + } + } + + renderWindow.render(); + + // After redraw we set all renderers to not render until necessary + for (let i = 0; i < renderers.length; i++) { + renderers[i].renderer.setDraw(false); + } + } + + /** + * Renders the given viewport + * using its proffered method. + * + * @param viewport - The viewport to render + */ + protected renderViewportUsingCustomOrVtkPipeline( + viewport: IViewport + ): EventTypes.ImageRenderedEventDetail { + let eventDetail: EventTypes.ImageRenderedEventDetail; + + // Rendering engines start having issues without at least two pixels + // in each direction + if ( + viewport.sWidth < VIEWPORT_MIN_SIZE || + viewport.sHeight < VIEWPORT_MIN_SIZE + ) { + console.warn('Viewport is too small', viewport.sWidth, viewport.sHeight); + return; + } + if (viewportTypeUsesCustomRenderingPipeline(viewport.type) === true) { + eventDetail = + viewport.customRenderViewportToCanvas() as EventTypes.ImageRenderedEventDetail; + } else { + if (this.useCPURendering) { + throw new Error( + 'GPU not available, and using a viewport with no custom render pipeline.' + ); + } + + const { offscreenMultiRenderWindow } = this; + const openGLRenderWindow = + offscreenMultiRenderWindow.getOpenGLRenderWindow(); + const context = openGLRenderWindow.get3DContext(); + const offScreenCanvas = context.canvas; + + eventDetail = this._renderViewportFromVtkCanvasToOnscreenCanvas( + viewport, + offScreenCanvas + ); + } + + return eventDetail; + } + + /** + * Renders a particular `Viewport`'s on screen canvas. + * @param viewport - The `Viewport` to render. + * @param offScreenCanvas - The offscreen canvas to render from. + */ + protected _renderViewportFromVtkCanvasToOnscreenCanvas( + viewport: IViewport, + offScreenCanvas: HTMLCanvasElement + ): EventTypes.ImageRenderedEventDetail { + const { + element, + canvas, + sx, + sy, + sWidth, + sHeight, + id: viewportId, + renderingEngineId, + suppressEvents, + } = viewport; + + const { width: dWidth, height: dHeight } = canvas; + + const onScreenContext = canvas.getContext('2d'); + + onScreenContext.drawImage( + offScreenCanvas, + sx, + sy, + sWidth, + sHeight, + 0, //dx + 0, // dy + dWidth, + dHeight + ); + + return { + element, + suppressEvents, + viewportId, + renderingEngineId, + viewportStatus: viewport.viewportStatus, + }; + } + + /** + * Resizes the offscreen canvas based on the provided vtk.js driven canvases. + * + * @param canvases - An array of HTML Canvas + */ + private _resizeOffScreenCanvas(canvasesDrivenByVtkJs: HTMLCanvasElement[]): { + offScreenCanvasWidth: number; + offScreenCanvasHeight: number; + } { + const { offScreenCanvasContainer, offscreenMultiRenderWindow } = this; + + // 1. Calculated the height of the offScreen canvas to be the maximum height + // between canvases + const offScreenCanvasHeight = Math.max( + ...canvasesDrivenByVtkJs.map((canvas) => canvas.height) + ); + + // 2. Calculating the width of the offScreen canvas to be the sum of all + let offScreenCanvasWidth = 0; + + canvasesDrivenByVtkJs.forEach((canvas) => { + offScreenCanvasWidth += canvas.width; + }); + + // @ts-expect-error + offScreenCanvasContainer.width = offScreenCanvasWidth; + // @ts-expect-error + offScreenCanvasContainer.height = offScreenCanvasHeight; + + // 3. Resize command + offscreenMultiRenderWindow.resize(); + + return { offScreenCanvasWidth, offScreenCanvasHeight }; + } + + /** + * Recalculates and updates the viewports location on the offScreen canvas upon its resize + * + * @param viewports - An array of viewports + * @param offScreenCanvasWidth - new offScreen canvas width + * @param offScreenCanvasHeight - new offScreen canvas height + * + * @returns _xOffset the final offset which will be used for the next viewport + */ + private _resize( + viewportsDrivenByVtkJs: (IStackViewport | IVolumeViewport)[], + offScreenCanvasWidth: number, + offScreenCanvasHeight: number + ): number { + // Redefine viewport properties + let _xOffset = 0; + + for (let i = 0; i < viewportsDrivenByVtkJs.length; i++) { + const viewport = viewportsDrivenByVtkJs[i]; + const { + sxStartDisplayCoords, + syStartDisplayCoords, + sxEndDisplayCoords, + syEndDisplayCoords, + sx, + sy, + sWidth, + sHeight, + } = this._getViewportCoordsOnOffScreenCanvas( + viewport as IViewport, + offScreenCanvasWidth, + offScreenCanvasHeight, + _xOffset + ); + + _xOffset += viewport.canvas.width; + + viewport.sx = sx; + viewport.sy = sy; + viewport.sWidth = sWidth; + viewport.sHeight = sHeight; + + // Updating the renderer for the viewport + const renderer = this.offscreenMultiRenderWindow.getRenderer(viewport.id); + renderer.setViewport( + sxStartDisplayCoords, + syStartDisplayCoords, + sxEndDisplayCoords, + syEndDisplayCoords + ); + } + + // Returns the final xOffset + return _xOffset; + } + + /** + * Calculates the location of the provided viewport on the offScreenCanvas + * + * @param viewports - An array of viewports + * @param offScreenCanvasWidth - new offScreen canvas width + * @param offScreenCanvasHeight - new offScreen canvas height + * @param _xOffset - xOffSet to draw + */ + private _getViewportCoordsOnOffScreenCanvas( + viewport: InternalViewportInput | IViewport, + offScreenCanvasWidth: number, + offScreenCanvasHeight: number, + _xOffset: number + ): ViewportDisplayCoords { + const { canvas } = viewport; + const { width: sWidth, height: sHeight } = canvas; + + // Update the canvas drawImage offsets. + const sx = _xOffset; + const sy = 0; + + const sxStartDisplayCoords = sx / offScreenCanvasWidth; + + // Need to offset y if it not max height + const syStartDisplayCoords = + sy + (offScreenCanvasHeight - sHeight) / offScreenCanvasHeight; + + const sWidthDisplayCoords = sWidth / offScreenCanvasWidth; + const sHeightDisplayCoords = sHeight / offScreenCanvasHeight; + + return { + sxStartDisplayCoords, + syStartDisplayCoords, + sxEndDisplayCoords: sxStartDisplayCoords + sWidthDisplayCoords, + syEndDisplayCoords: syStartDisplayCoords + sHeightDisplayCoords, + sx, + sy, + sWidth, + sHeight, + }; + } +} + +export default TiledRenderingEngine; diff --git a/packages/core/src/RenderingEngine/VideoViewport.ts b/packages/core/src/RenderingEngine/VideoViewport.ts index 5c92d81551..385bb63a2e 100644 --- a/packages/core/src/RenderingEngine/VideoViewport.ts +++ b/packages/core/src/RenderingEngine/VideoViewport.ts @@ -765,7 +765,7 @@ class VideoViewport extends Viewport { * @returns an imageID for video */ public getCurrentImageId(index = this.getCurrentImageIdIndex()) { - const current = this.imageId.replace('/frames/1', `/frames/${index + 1}`); + const current = this.imageId?.replace('/frames/1', `/frames/${index + 1}`); return current; } diff --git a/packages/core/src/RenderingEngine/Viewport.ts b/packages/core/src/RenderingEngine/Viewport.ts index 99b72a3e25..b236914450 100644 --- a/packages/core/src/RenderingEngine/Viewport.ts +++ b/packages/core/src/RenderingEngine/Viewport.ts @@ -38,6 +38,7 @@ import type { ReferenceCompatibleOptions, ViewPresentationSelector, DataSetOptions, + PlaneRestriction, } from '../types/IViewport'; import type { vtkSlabCamera } from './vtkClasses/vtkSlabCamera'; import type IImageCalibration from '../types/IImageCalibration'; @@ -47,6 +48,7 @@ import type vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; import type vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; import type vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; import { deepClone } from '../utilities/deepClone'; +import { updatePlaneRestriction } from '../utilities/updatePlaneRestriction'; /** * An object representing a single viewport, which is a camera @@ -96,7 +98,7 @@ class Viewport { /** * The amount by which the images are inset in a viewport by default. */ - protected insetImageMultiplier = 1.1; + protected insetImageMultiplier = 1; protected flipHorizontal = false; protected flipVertical = false; @@ -1110,8 +1112,22 @@ class Viewport { imageData.indexToWorld(idx, focalPoint); } - const { widthWorld, heightWorld } = - this._getWorldDistanceViewUpAndViewRight(bounds, viewUp, viewPlaneNormal); + let widthWorld; + let heightWorld; + + if (imageData) { + const extent = imageData.getExtent(); + const spacing = imageData.getSpacing(); + + widthWorld = (extent[1] - extent[0]) * spacing[0]; + heightWorld = (extent[3] - extent[2]) * spacing[1]; + } else { + ({ widthWorld, heightWorld } = this._getWorldDistanceViewUpAndViewRight( + bounds, + viewUp, + viewPlaneNormal + )); + } const canvasSize = [this.sWidth, this.sHeight]; @@ -1823,16 +1839,66 @@ class Viewport { viewPlaneNormal, viewUp, } = this.getCamera(); + const FrameOfReferenceUID = this.getFrameOfReferenceUID(); const target: ViewReference = { - FrameOfReferenceUID: this.getFrameOfReferenceUID(), + FrameOfReferenceUID, cameraFocalPoint, viewPlaneNormal, viewUp, sliceIndex: viewRefSpecifier?.sliceIndex ?? this.getSliceIndex(), + /** The referenced plane is the canonical specifier for whether + * this view reference is visible or not. + */ + planeRestriction: { + FrameOfReferenceUID, + point: viewRefSpecifier?.points?.[0] || cameraFocalPoint, + inPlaneVector1: viewUp, + inPlaneVector2: ( + vec3.cross(vec3.create(), viewUp, viewPlaneNormal) + ), + }, }; + if (viewRefSpecifier?.points) { + updatePlaneRestriction(viewRefSpecifier.points, target.planeRestriction); + } return target; } + public isPlaneViewable( + planeRestriction: PlaneRestriction, + options?: ReferenceCompatibleOptions + ): boolean { + if ( + planeRestriction.FrameOfReferenceUID !== this.getFrameOfReferenceUID() + ) { + return false; + } + const { focalPoint, viewPlaneNormal } = this.getCamera(); + const { point, inPlaneVector1, inPlaneVector2 } = planeRestriction; + if (options?.withOrientation) { + // Don't need to check the normal or the navigation if asking as a volume + // since those can both be updated + return true; + } + if ( + inPlaneVector1 && + !isEqual(0, vec3.dot(viewPlaneNormal, inPlaneVector1)) + ) { + return false; + } + if ( + inPlaneVector2 && + !isEqual(0, vec3.dot(viewPlaneNormal, inPlaneVector2)) + ) { + return false; + } + if (options?.withNavigation) { + return true; + } + const pointVector = vec3.sub(vec3.create(), point, focalPoint); + return isEqual(0, vec3.dot(pointVector, viewPlaneNormal)); + } + /** * Find out if this viewport does or could show this view reference. * @param options - allows specifying whether the view COULD display this with @@ -1843,6 +1909,9 @@ class Viewport { viewRef: ViewReference, options?: ReferenceCompatibleOptions ): boolean { + if (viewRef.planeRestriction) { + return this.isPlaneViewable(viewRef.planeRestriction, options); + } if ( viewRef.FrameOfReferenceUID && viewRef.FrameOfReferenceUID !== this.getFrameOfReferenceUID() diff --git a/packages/core/src/RenderingEngine/VolumeViewport3D.ts b/packages/core/src/RenderingEngine/VolumeViewport3D.ts index da477d981b..6130fcb882 100644 --- a/packages/core/src/RenderingEngine/VolumeViewport3D.ts +++ b/packages/core/src/RenderingEngine/VolumeViewport3D.ts @@ -4,13 +4,13 @@ import { OrientationAxis, Events } from '../enums'; import cache from '../cache/cache'; import setDefaultVolumeVOI from './helpers/setDefaultVolumeVOI'; import triggerEvent from '../utilities/triggerEvent'; -import { isImageActor } from '../utilities/actorCheck'; +import { actorIsA, isImageActor } from '../utilities/actorCheck'; import { setTransferFunctionNodes } from '../utilities/transferFunctionUtils'; import type vtkVolume from '@kitware/vtk.js/Rendering/Core/Volume'; import type { ViewportInput } from '../types/IViewport'; import type { ImageActor } from '../types/IActor'; import BaseVolumeViewport from './BaseVolumeViewport'; - +import type { Types } from '@cornerstonejs/core'; /** * An object representing a 3-dimensional volume viewport. VolumeViewport3Ds are used to render * 3D volumes in their entirety, and not just load a single slice at a time. @@ -35,6 +35,40 @@ class VolumeViewport3D extends BaseVolumeViewport { } } + public setSampleDistanceMultiplier = (multiplier: number): void => { + const actors = this.getActors(); + actors.forEach((actorEntry) => { + if (actorIsA(actorEntry, 'vtkVolume')) { + const actor = actorEntry.actor as Types.VolumeActor; + const mapper = actor.getMapper(); + + if (mapper && mapper.getInputData) { + const imageData = mapper.getInputData(); + + if (imageData) { + const spacing = imageData.getSpacing(); + + //Calculate sample distance + const defaultSampleDistance = + (spacing[0] + spacing[1] + spacing[2]) / 6; + + const sampleDistanceMultiplier = multiplier || 1; + let sampleDistance = + defaultSampleDistance * sampleDistanceMultiplier; + + // Apply sample distance if specified + if (sampleDistance !== undefined && mapper.setSampleDistance) { + const currentSampleDistance = mapper.getSampleDistance(); + mapper.setSampleDistance(sampleDistance); + } + } + } + } + }); + + this.render(); + }; + public getNumberOfSlices = (): number => { return 1; }; diff --git a/packages/core/src/RenderingEngine/WSIViewport.ts b/packages/core/src/RenderingEngine/WSIViewport.ts index e174cb561e..c5c8922a61 100644 --- a/packages/core/src/RenderingEngine/WSIViewport.ts +++ b/packages/core/src/RenderingEngine/WSIViewport.ts @@ -8,7 +8,6 @@ import type { VOIRange, CPUIImageData, ViewportInput, - BoundsIJK, } from '../types'; import uuidv4 from '../utilities/uuidv4'; import * as metaData from '../metaData'; @@ -18,13 +17,10 @@ import { getOrCreateCanvas } from './helpers'; import { EPSILON } from '../constants'; import triggerEvent from '../utilities/triggerEvent'; import { peerImport } from '../init'; -import { pointInShapeCallback } from '../utilities/pointInShapeCallback'; import microscopyViewportCss from '../constants/microscopyViewportCss'; import type { DataSetOptions } from '../types/IViewport'; let WSIUtilFunctions = null; -const _map = Symbol.for('map'); -const affineSymbol = Symbol.for('affine'); const EVENT_POSTRENDER = 'postrender'; /** * A viewport which shows a microscopy view using the dicom-microscopy-viewer @@ -492,7 +488,7 @@ class WSIViewport extends Viewport { if (!WSIUtilFunctions) { return; } - const affine = this.viewer[affineSymbol]; + const affine = this.viewer.getAffine(); const pixelCoords = WSIUtilFunctions.applyInverseTransform({ coordinate: [point[0], point[1]], affine, @@ -511,7 +507,7 @@ class WSIViewport extends Viewport { } const sliceCoords = WSIUtilFunctions.applyTransform({ coordinate: [point[0], point[1]], - affine: this.viewer[affineSymbol], + affine: this.viewer.getAffine(), }); return [sliceCoords[0], sliceCoords[1], 0] as Point3; } @@ -654,7 +650,7 @@ class WSIViewport extends Viewport { viewer.deactivateDragPanInteraction(); this.viewer = viewer; - this.map = viewer[_map]; + this.map = viewer.getMap(); this.map.on(EVENT_POSTRENDER, this.postrender); this.resize(); this.microscopyElement.innerText = ''; @@ -721,7 +717,7 @@ class WSIViewport extends Viewport { return; } // TODO - use a native method rather than accessing internals directly - const map = this.viewer[_map]; + const map = this.viewer.getMap(); // TODO - remove the globals setter const anyWindow = window as unknown as Record; anyWindow.map = map; diff --git a/packages/core/src/RenderingEngine/WebGLContextPool.ts b/packages/core/src/RenderingEngine/WebGLContextPool.ts new file mode 100644 index 0000000000..def39ddeb2 --- /dev/null +++ b/packages/core/src/RenderingEngine/WebGLContextPool.ts @@ -0,0 +1,95 @@ +import { vtkOffscreenMultiRenderWindow } from './vtkClasses'; +import type { VtkOffscreenMultiRenderWindow } from '../types'; + +/** + * Manages a pool of WebGL contexts for parallel rendering. + * Enables us distribute viewports across multiple contexts + * for improved performance. + */ +class WebGLContextPool { + private contexts: VtkOffscreenMultiRenderWindow[] = []; + private offScreenCanvasContainers: HTMLDivElement[] = []; + private viewportToContext: Map = new Map(); + + /** + * Creates a pool with the specified number of WebGL contexts + * @param count - Number of contexts to create + */ + constructor(count: number) { + for (let i = 0; i < count; i++) { + const offscreenMultiRenderWindow = + vtkOffscreenMultiRenderWindow.newInstance(); + const container = document.createElement('div'); + offscreenMultiRenderWindow.setContainer(container); + + this.contexts.push(offscreenMultiRenderWindow); + this.offScreenCanvasContainers.push(container); + } + } + + /** + * Gets the context and container at the specified index + * @param index - Context index + * @returns Context and container, or null if index is invalid + */ + getContextByIndex(index: number): { + context: VtkOffscreenMultiRenderWindow; + container: HTMLDivElement; + } | null { + if (index >= 0 && index < this.contexts.length) { + return { + context: this.contexts[index], + container: this.offScreenCanvasContainers[index], + }; + } + return null; + } + + /** + * Associates a viewport with a specific context index + * @param viewportId - ID of the viewport + * @param contextIndex - Index of the context to assign + */ + assignViewportToContext(viewportId: string, contextIndex: number): void { + this.viewportToContext.set(viewportId, contextIndex); + } + + /** + * Gets the context index assigned to a viewport + * @param viewportId - ID of the viewport + * @returns Context index, or undefined if not assigned + */ + getContextIndexForViewport(viewportId: string): number | undefined { + return this.viewportToContext.get(viewportId); + } + + /** + * Gets all contexts in the pool + * @returns Array of all contexts + */ + getAllContexts(): VtkOffscreenMultiRenderWindow[] { + return this.contexts; + } + + /** + * Gets the number of contexts in the pool + * @returns Number of contexts + */ + getContextCount(): number { + return this.contexts.length; + } + + /** + * Cleans up all contexts and releases resources + */ + destroy(): void { + this.contexts.forEach((context: VtkOffscreenMultiRenderWindow) => { + context.delete(); + }); + this.contexts = []; + this.offScreenCanvasContainers = []; + this.viewportToContext.clear(); + } +} + +export default WebGLContextPool; diff --git a/packages/core/src/RenderingEngine/helpers/createVolumeMapper.ts b/packages/core/src/RenderingEngine/helpers/createVolumeMapper.ts index b20633977e..9aafb36690 100644 --- a/packages/core/src/RenderingEngine/helpers/createVolumeMapper.ts +++ b/packages/core/src/RenderingEngine/helpers/createVolumeMapper.ts @@ -30,7 +30,11 @@ export default function createVolumeMapper( const spacing = imageData.getSpacing(); // Set the sample distance to half the mean length of one side. This is where the divide by 6 comes from. // https://github.com/Kitware/VTK/blob/6b559c65bb90614fb02eb6d1b9e3f0fca3fe4b0b/Rendering/VolumeOpenGL2/vtkSmartVolumeMapper.cxx#L344 - const sampleDistance = (spacing[0] + spacing[1] + spacing[2]) / 6; + const sampleDistanceMultiplier = + getConfiguration().rendering?.volumeRendering?.sampleDistanceMultiplier || + 1; + const sampleDistance = + (sampleDistanceMultiplier * (spacing[0] + spacing[1] + spacing[2])) / 6; // This is to allow for good pixel level image quality. // Todo: why we are setting this to 4000? Is this a good number? it should be configurable diff --git a/packages/core/src/RenderingEngine/helpers/getCameraVectors.ts b/packages/core/src/RenderingEngine/helpers/getCameraVectors.ts index 220f7b2d62..a1c576bf1a 100644 --- a/packages/core/src/RenderingEngine/helpers/getCameraVectors.ts +++ b/packages/core/src/RenderingEngine/helpers/getCameraVectors.ts @@ -176,7 +176,7 @@ export function calculateCameraPosition( viewRight: [viewRight[0], viewRight[1], viewRight[2]] as [ number, number, - number + number, ], }; } diff --git a/packages/core/src/RenderingEngine/helpers/isContextPoolRenderingEngine.ts b/packages/core/src/RenderingEngine/helpers/isContextPoolRenderingEngine.ts new file mode 100644 index 0000000000..60b5f6946b --- /dev/null +++ b/packages/core/src/RenderingEngine/helpers/isContextPoolRenderingEngine.ts @@ -0,0 +1,10 @@ +import { getConfiguration } from '../../init'; +import { RenderingEngineModeEnum } from '../../enums'; + +export function isContextPoolRenderingEngine(): boolean { + const config = getConfiguration(); + return ( + config?.rendering?.renderingEngineMode === + RenderingEngineModeEnum.ContextPool + ); +} diff --git a/packages/core/src/RenderingEngine/helpers/stats/StatsOverlay.ts b/packages/core/src/RenderingEngine/helpers/stats/StatsOverlay.ts new file mode 100644 index 0000000000..eab4d167c9 --- /dev/null +++ b/packages/core/src/RenderingEngine/helpers/stats/StatsOverlay.ts @@ -0,0 +1,239 @@ +import { StatsPanel } from './StatsPanel'; +import type { Panel, StatsInstance, PerformanceWithMemory } from './types'; +import { PanelType } from './enums'; +import { STATS_CONFIG, PANEL_CONFIGS, CONVERSION } from './constants'; + +/** + * Singleton class for managing the stats overlay. + * Provides FPS, MS, and memory usage monitoring. + * Credits: https://github.com/mrdoob/stats.js/blob/master/LICENSE + */ +export class StatsOverlay implements StatsInstance { + private static instance: StatsOverlay | null = null; + + public dom: HTMLDivElement | null = null; + private currentMode = 0; + private startTime: number = 0; + private lastUpdateTime: number = 0; + private frameCount = 0; + private panels: Map = new Map(); + private animationFrameId: number | null = null; + private isSetup = false; + + private constructor() {} + + /** + * Gets the singleton instance of StatsOverlay. + */ + public static getInstance(): StatsOverlay { + if (!StatsOverlay.instance) { + StatsOverlay.instance = new StatsOverlay(); + } + return StatsOverlay.instance; + } + + /** + * Sets up the stats overlay and starts the animation loop. + */ + public setup(): void { + if (this.isSetup) { + return; + } + + try { + // Initialize DOM and timing + this.dom = this.createOverlayElement(); + this.startTime = performance.now(); + this.lastUpdateTime = this.startTime; + + // Initialize panels and show default + this.initializePanels(); + this.showPanel(PanelType.FPS); + + // Apply styles and add to DOM + this.applyOverlayStyles(); + document.body.appendChild(this.dom); + this.startLoop(); + this.isSetup = true; + } catch (error) { + console.warn('Failed to setup stats overlay:', error); + } + } + + /** + * Cleans up the stats overlay by removing it from the DOM and stopping the animation loop. + */ + public cleanup(): void { + this.stopLoop(); + + if (this.dom && this.dom.parentNode) { + this.dom.parentNode.removeChild(this.dom); + } + + this.dom = null; + this.panels.clear(); + this.isSetup = false; + } + + /** + * Shows a specific panel by its type. + */ + public showPanel(panelType: number): void { + const children = Array.from(this.dom.children) as HTMLElement[]; + children.forEach((child, index) => { + child.style.display = index === panelType ? 'block' : 'none'; + }); + this.currentMode = panelType; + } + + /** + * Updates the stats display. + */ + public update(): void { + this.startTime = this.updateStats(); + } + + /** + * Creates the overlay DOM element. + */ + private createOverlayElement(): HTMLDivElement { + const element = document.createElement('div'); + element.addEventListener('click', this.handleClick.bind(this), false); + return element; + } + + /** + * Applies styles to the overlay element. + */ + private applyOverlayStyles(): void { + Object.assign(this.dom.style, STATS_CONFIG.OVERLAY_STYLES); + } + + /** + * Handles click events on the overlay. + */ + private handleClick(event: MouseEvent): void { + event.preventDefault(); + const panelCount = this.dom.children.length; + this.showPanel((this.currentMode + 1) % panelCount); + } + + /** + * Initializes all panels. + */ + private initializePanels(): void { + // Always create FPS and MS panels + const fpsPanel = new StatsPanel( + PANEL_CONFIGS[PanelType.FPS].name, + PANEL_CONFIGS[PanelType.FPS].foregroundColor, + PANEL_CONFIGS[PanelType.FPS].backgroundColor + ); + this.addPanel(PanelType.FPS, fpsPanel); + + const msPanel = new StatsPanel( + PANEL_CONFIGS[PanelType.MS].name, + PANEL_CONFIGS[PanelType.MS].foregroundColor, + PANEL_CONFIGS[PanelType.MS].backgroundColor + ); + this.addPanel(PanelType.MS, msPanel); + + // Only create memory panel if available + if (this.isMemoryAvailable()) { + const memPanel = new StatsPanel( + PANEL_CONFIGS[PanelType.MEMORY].name, + PANEL_CONFIGS[PanelType.MEMORY].foregroundColor, + PANEL_CONFIGS[PanelType.MEMORY].backgroundColor + ); + this.addPanel(PanelType.MEMORY, memPanel); + } + } + + /** + * Checks if memory monitoring is available. + */ + private isMemoryAvailable(): boolean { + const perf = performance as PerformanceWithMemory; + return perf.memory !== undefined; + } + + /** + * Adds a panel to the overlay. + */ + private addPanel(type: PanelType, panel: Panel): void { + this.dom.appendChild(panel.dom); + this.panels.set(type, panel); + } + + /** + * Starts the animation frame loop. + */ + private startLoop(): void { + const loop = () => { + this.update(); + this.animationFrameId = requestAnimationFrame(loop); + }; + this.animationFrameId = requestAnimationFrame(loop); + } + + /** + * Stops the animation frame loop. + */ + private stopLoop(): void { + if (this.animationFrameId !== null) { + cancelAnimationFrame(this.animationFrameId); + this.animationFrameId = null; + } + } + + /** + * Updates all stats panels. + */ + private updateStats(): number { + this.frameCount++; + const currentTime = performance.now(); + const deltaTime = currentTime - this.startTime; + + // Update MS panel + const msPanel = this.panels.get(PanelType.MS); + if (msPanel) { + msPanel.update(deltaTime, STATS_CONFIG.MAX_MS_VALUE); + } + + // Update FPS panel every second + if (currentTime >= this.lastUpdateTime + STATS_CONFIG.UPDATE_INTERVAL) { + const fps = + (this.frameCount * CONVERSION.MS_PER_SECOND) / + (currentTime - this.lastUpdateTime); + + const fpsPanel = this.panels.get(PanelType.FPS); + if (fpsPanel) { + fpsPanel.update(fps, STATS_CONFIG.MAX_FPS_VALUE); + } + + this.lastUpdateTime = currentTime; + this.frameCount = 0; + + // Update memory panel if available + this.updateMemoryPanel(); + } + + return currentTime; + } + + /** + * Updates the memory panel if available. + */ + private updateMemoryPanel(): void { + const memPanel = this.panels.get(PanelType.MEMORY); + if (!memPanel) { + return; + } + + const perf = performance as PerformanceWithMemory; + if (perf.memory) { + const memoryMB = perf.memory.usedJSHeapSize / CONVERSION.BYTES_TO_MB; + const maxMemoryMB = perf.memory.jsHeapSizeLimit / CONVERSION.BYTES_TO_MB; + memPanel.update(memoryMB, maxMemoryMB); + } + } +} diff --git a/packages/core/src/RenderingEngine/helpers/stats/StatsPanel.ts b/packages/core/src/RenderingEngine/helpers/stats/StatsPanel.ts new file mode 100644 index 0000000000..96cb443c00 --- /dev/null +++ b/packages/core/src/RenderingEngine/helpers/stats/StatsPanel.ts @@ -0,0 +1,214 @@ +import type { Panel } from './types'; +import { PANEL_CONFIG } from './constants'; + +/** + * Individual panel for displaying stats (FPS, MS, MB). + * Credits: https://github.com/mrdoob/stats.js/blob/master/LICENSE + */ +export class StatsPanel implements Panel { + public dom: HTMLCanvasElement; + private context: CanvasRenderingContext2D; + private minValue = Infinity; + private maxValue = 0; + + private readonly name: string; + private readonly foregroundColor: string; + private readonly backgroundColor: string; + private readonly devicePixelRatio: number; + + // Calculated dimensions + private readonly dimensions: { + width: number; + height: number; + textX: number; + textY: number; + graphX: number; + graphY: number; + graphWidth: number; + graphHeight: number; + }; + + constructor(name: string, foregroundColor: string, backgroundColor: string) { + this.name = name; + this.foregroundColor = foregroundColor; + this.backgroundColor = backgroundColor; + this.devicePixelRatio = Math.round(window.devicePixelRatio || 1); + + // Calculate dimensions based on device pixel ratio + this.dimensions = this.calculateDimensions(); + + // Initialize canvas + this.dom = this.createCanvas(); + this.context = this.initializeContext(); + + // Draw initial panel + this.drawInitialPanel(); + } + + /** + * Updates the panel with a new value. + */ + public update(value: number, maxValue: number): void { + this.updateMinMax(value); + this.clearTextArea(); + this.drawText(value); + this.scrollGraph(); + this.drawNewValue(value, maxValue); + } + + /** + * Calculates panel dimensions based on device pixel ratio. + */ + private calculateDimensions() { + const pr = this.devicePixelRatio; + return { + width: PANEL_CONFIG.WIDTH * pr, + height: PANEL_CONFIG.HEIGHT * pr, + textX: PANEL_CONFIG.TEXT_PADDING * pr, + textY: PANEL_CONFIG.TEXT_Y_OFFSET * pr, + graphX: PANEL_CONFIG.TEXT_PADDING * pr, + graphY: PANEL_CONFIG.GRAPH_Y_OFFSET * pr, + graphWidth: PANEL_CONFIG.GRAPH_WIDTH * pr, + graphHeight: PANEL_CONFIG.GRAPH_HEIGHT * pr, + }; + } + + /** + * Creates the canvas element. + */ + private createCanvas(): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + canvas.width = this.dimensions.width; + canvas.height = this.dimensions.height; + canvas.style.cssText = `width:${PANEL_CONFIG.WIDTH}px;height:${PANEL_CONFIG.HEIGHT}px`; + return canvas; + } + + /** + * Initializes the canvas context. + */ + private initializeContext(): CanvasRenderingContext2D { + const ctx = this.dom.getContext('2d'); + if (!ctx) { + throw new Error('Failed to get 2D context'); + } + + ctx.font = `bold ${PANEL_CONFIG.FONT_SIZE * this.devicePixelRatio}px ${ + PANEL_CONFIG.FONT_FAMILY + }`; + ctx.textBaseline = 'top'; + + return ctx; + } + + /** + * Draws the initial panel background and text. + */ + private drawInitialPanel(): void { + const { + width, + height, + textX, + textY, + graphX, + graphY, + graphWidth, + graphHeight, + } = this.dimensions; + + // Draw background + this.context.fillStyle = this.backgroundColor; + this.context.fillRect(0, 0, width, height); + + // Draw name + this.context.fillStyle = this.foregroundColor; + this.context.fillText(this.name, textX, textY); + + // Draw graph outline + this.context.fillRect(graphX, graphY, graphWidth, graphHeight); + + // Draw graph background + this.context.fillStyle = this.backgroundColor; + this.context.globalAlpha = PANEL_CONFIG.GRAPH_ALPHA; + this.context.fillRect(graphX, graphY, graphWidth, graphHeight); + this.context.globalAlpha = 1; + } + + /** + * Updates min and max values. + */ + private updateMinMax(value: number): void { + this.minValue = Math.min(this.minValue, value); + this.maxValue = Math.max(this.maxValue, value); + } + + /** + * Clears the text area for redrawing. + */ + private clearTextArea(): void { + const { width, graphY } = this.dimensions; + this.context.fillStyle = this.backgroundColor; + this.context.fillRect(0, 0, width, graphY); + } + + /** + * Draws the current value text. + */ + private drawText(value: number): void { + const { textX, textY } = this.dimensions; + const text = this.formatText(value); + + this.context.fillStyle = this.foregroundColor; + this.context.fillText(text, textX, textY); + } + + /** + * Formats the display text. + */ + private formatText(value: number): string { + const roundedValue = Math.round(value); + const roundedMin = Math.round(this.minValue); + const roundedMax = Math.round(this.maxValue); + return `${roundedValue} ${this.name} (${roundedMin}-${roundedMax})`; + } + + /** + * Scrolls the graph to the left. + */ + private scrollGraph(): void { + const { graphX, graphY, graphWidth, graphHeight } = this.dimensions; + const pr = this.devicePixelRatio; + + this.context.drawImage( + this.dom, + graphX + pr, + graphY, + graphWidth - pr, + graphHeight, + graphX, + graphY, + graphWidth - pr, + graphHeight + ); + } + + /** + * Draws the new value on the graph. + */ + private drawNewValue(value: number, maxValue: number): void { + const { graphX, graphY, graphWidth, graphHeight } = this.dimensions; + const pr = this.devicePixelRatio; + const x = graphX + graphWidth - pr; + + // Draw full height bar in foreground color + this.context.fillStyle = this.foregroundColor; + this.context.fillRect(x, graphY, pr, graphHeight); + + // Draw partial height bar in background color + const normalizedHeight = Math.round((1 - value / maxValue) * graphHeight); + this.context.fillStyle = this.backgroundColor; + this.context.globalAlpha = PANEL_CONFIG.GRAPH_ALPHA; + this.context.fillRect(x, graphY, pr, normalizedHeight); + this.context.globalAlpha = 1; + } +} diff --git a/packages/core/src/RenderingEngine/helpers/stats/constants.ts b/packages/core/src/RenderingEngine/helpers/stats/constants.ts new file mode 100644 index 0000000000..a503a41ac1 --- /dev/null +++ b/packages/core/src/RenderingEngine/helpers/stats/constants.ts @@ -0,0 +1,52 @@ +/** + * Constants for panel rendering + */ +const PANEL_CONFIG = { + WIDTH: 160, + HEIGHT: 96, + TEXT_PADDING: 3, + TEXT_Y_OFFSET: 2, + GRAPH_Y_OFFSET: 15, + GRAPH_WIDTH: 150, + GRAPH_HEIGHT: 70, + FONT_SIZE: 9, + FONT_FAMILY: 'Helvetica,Arial,sans-serif', + GRAPH_ALPHA: 0.9, +} as const; + +/** + * Constants for stats overlay + */ +const STATS_CONFIG = { + UPDATE_INTERVAL: 1000, // ms + MAX_MS_VALUE: 200, + MAX_FPS_VALUE: 300, // don't use 60 since no one has a 60hz monitor anynmore + OVERLAY_STYLES: { + position: 'fixed', + top: '0px', + right: '0px', + left: 'auto', + zIndex: '9999', + cursor: 'pointer', + opacity: '0.9', + }, +} as const; + +/** + * Conversion constants + */ +const CONVERSION = { + BYTES_TO_MB: 1048576, + MS_PER_SECOND: 1000, +} as const; + +/** + * Panel configurations with colors + */ +const PANEL_CONFIGS = [ + { name: 'FPS', foregroundColor: '#0ff', backgroundColor: '#002' }, + { name: 'MS', foregroundColor: '#0f0', backgroundColor: '#020' }, + { name: 'MB', foregroundColor: '#f08', backgroundColor: '#201' }, +] as const; + +export { PANEL_CONFIG, STATS_CONFIG, CONVERSION, PANEL_CONFIGS }; diff --git a/packages/core/src/RenderingEngine/helpers/stats/enums.ts b/packages/core/src/RenderingEngine/helpers/stats/enums.ts new file mode 100644 index 0000000000..0b7869cf85 --- /dev/null +++ b/packages/core/src/RenderingEngine/helpers/stats/enums.ts @@ -0,0 +1,10 @@ +/** + * Panel types + */ +enum PanelType { + FPS = 0, + MS = 1, + MEMORY = 2, +} + +export { PanelType }; diff --git a/packages/core/src/RenderingEngine/helpers/stats/index.ts b/packages/core/src/RenderingEngine/helpers/stats/index.ts new file mode 100644 index 0000000000..efe7ca9194 --- /dev/null +++ b/packages/core/src/RenderingEngine/helpers/stats/index.ts @@ -0,0 +1,3 @@ +import { StatsOverlay as Class } from './StatsOverlay'; + +export const StatsOverlay = Class.getInstance(); diff --git a/packages/core/src/RenderingEngine/helpers/stats/types.ts b/packages/core/src/RenderingEngine/helpers/stats/types.ts new file mode 100644 index 0000000000..4734bce7ff --- /dev/null +++ b/packages/core/src/RenderingEngine/helpers/stats/types.ts @@ -0,0 +1,38 @@ +/** + * Interface for individual stats panels (FPS, MS, MB). + */ +interface Panel { + dom: HTMLCanvasElement; + update: (value: number, maxValue: number) => void; +} + +/** + * Interface for the main stats instance. + */ +interface StatsInstance { + dom: HTMLDivElement; + showPanel: (id: number) => void; + update: () => void; + destroy?: () => void; +} + +/** + * Extended Performance interface with memory property + */ +interface PerformanceWithMemory extends Performance { + memory?: { + usedJSHeapSize: number; + jsHeapSizeLimit: number; + }; +} + +/** + * Configuration for panel styling + */ +interface PanelConfig { + name: string; + foregroundColor: string; + backgroundColor: string; +} + +export type { Panel, StatsInstance, PerformanceWithMemory, PanelConfig }; diff --git a/packages/core/src/RenderingEngine/index.ts b/packages/core/src/RenderingEngine/index.ts index 3b41cdfdbd..0f8465684e 100644 --- a/packages/core/src/RenderingEngine/index.ts +++ b/packages/core/src/RenderingEngine/index.ts @@ -1,4 +1,7 @@ import RenderingEngine from './RenderingEngine'; +import BaseRenderingEngine from './BaseRenderingEngine'; +import TiledRenderingEngine from './TiledRenderingEngine'; +import ContextPoolRenderingEngine from './ContextPoolRenderingEngine'; import getRenderingEngine from './getRenderingEngine'; import VolumeViewport from './VolumeViewport'; import StackViewport from './StackViewport'; @@ -8,6 +11,9 @@ export * from './helpers'; export { getRenderingEngine, RenderingEngine, + BaseRenderingEngine, + TiledRenderingEngine, + ContextPoolRenderingEngine, VolumeViewport, VolumeViewport3D, StackViewport, diff --git a/packages/core/src/cache/cache.ts b/packages/core/src/cache/cache.ts index d439492193..a9016afc00 100644 --- a/packages/core/src/cache/cache.ts +++ b/packages/core/src/cache/cache.ts @@ -505,8 +505,6 @@ class Cache { sizeInBytes: 0, }; - this._imageCache.set(imageId, cachedImage); - // For some reason we need to put it here after the rework of volumes this._imageCache.set(imageId, cachedImage); diff --git a/packages/core/src/cache/classes/BaseStreamingImageVolume.ts b/packages/core/src/cache/classes/BaseStreamingImageVolume.ts index 6b49180033..556d2b6e4f 100644 --- a/packages/core/src/cache/classes/BaseStreamingImageVolume.ts +++ b/packages/core/src/cache/classes/BaseStreamingImageVolume.ts @@ -283,11 +283,11 @@ export default class BaseStreamingImageVolume this.imagesLoader = this.isDynamicVolume() ? this : imageRetrieveConfiguration - ? ( - imageRetrieveConfiguration.create || - ProgressiveRetrieveImages.createProgressive - )(imageRetrieveConfiguration) - : this; + ? ( + imageRetrieveConfiguration.create || + ProgressiveRetrieveImages.createProgressive + )(imageRetrieveConfiguration) + : this; if (loadStatus.loading === true) { return; // Already loading, will get callbacks from main load. @@ -405,6 +405,7 @@ export default class BaseStreamingImageVolume imageIdIndex, volumeId: this.volumeId, }, + retrieveOptions: undefined, }; } @@ -440,11 +441,14 @@ export default class BaseStreamingImageVolume loadAndCacheImage(imageId, options) ); - return uncompressedIterator.forEach((image) => { - // scalarData is the volume container we are progressively loading into - // image is the pixelData decoded from workers in cornerstoneDICOMImageLoader - this.successCallback(imageId, image); - }, this.errorCallback.bind(this, imageIdIndex, imageId)); + return uncompressedIterator.forEach( + (image) => { + // scalarData is the volume container we are progressively loading into + // image is the pixelData decoded from workers in cornerstoneDICOMImageLoader + this.successCallback(imageId, image); + }, + this.errorCallback.bind(this, imageIdIndex, imageId) + ); } protected getImageIdsRequests(imageIds: string[], priorityDefault: number) { @@ -471,6 +475,19 @@ export default class BaseStreamingImageVolume const priority = priorityDefault; const options = this.getLoaderImageOptions(imageId); + const { retrieveOptions = {} } = + metaData.get( + imageRetrieveMetadataProvider.IMAGE_RETRIEVE_CONFIGURATION, + imageId, + 'volume' + ) || {}; + options.retrieveOptions = { + ...options.retrieveOptions, + ...(retrieveOptions.default || + Object.values(retrieveOptions)?.[0] || + {}), + }; + return { callLoadImage: this.callLoadImage.bind(this), imageId, diff --git a/packages/core/src/enums/RenderingEngineModeEnum.ts b/packages/core/src/enums/RenderingEngineModeEnum.ts new file mode 100644 index 0000000000..b289da1c1d --- /dev/null +++ b/packages/core/src/enums/RenderingEngineModeEnum.ts @@ -0,0 +1,6 @@ +enum RenderingEngineModeEnum { + Tiled = 'tiled', + ContextPool = 'contextPool', +} + +export default RenderingEngineModeEnum; diff --git a/packages/core/src/enums/index.ts b/packages/core/src/enums/index.ts index 646aecca85..e2f295994e 100644 --- a/packages/core/src/enums/index.ts +++ b/packages/core/src/enums/index.ts @@ -16,6 +16,7 @@ import * as VideoEnums from './VideoEnums'; import MetadataModules from './MetadataModules'; import { GenerateImageType } from './GenerateImageType'; import VoxelManagerEnum from './VoxelManagerEnum'; +import RenderingEngineModeEnum from './RenderingEngineModeEnum'; export { Events, @@ -36,4 +37,5 @@ export { ImageQualityStatus, VoxelManagerEnum, GenerateImageType, + RenderingEngineModeEnum, }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f1b2ed078b..140a992875 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,7 +1,11 @@ import * as Enums from './enums'; import * as CONSTANTS from './constants'; import { Events } from './enums'; -import RenderingEngine from './RenderingEngine'; +import RenderingEngine, { + BaseRenderingEngine, + TiledRenderingEngine, + ContextPoolRenderingEngine, +} from './RenderingEngine'; import createVolumeActor from './RenderingEngine/helpers/createVolumeActor'; import createVolumeMapper, { convertMapperToNotSharedMapper, @@ -120,6 +124,9 @@ export { VideoViewport, WSIViewport, RenderingEngine, + BaseRenderingEngine, + TiledRenderingEngine, + ContextPoolRenderingEngine, ImageVolume, Surface, // Helpers diff --git a/packages/core/src/init.ts b/packages/core/src/init.ts index 430c0fbb8e..22ccce2c74 100644 --- a/packages/core/src/init.ts +++ b/packages/core/src/init.ts @@ -4,6 +4,7 @@ import deepMerge from './utilities/deepMerge'; import type { Cornerstone3DConfig } from './types'; import CentralizedWebWorkerManager from './webWorkerManager/webWorkerManager'; import { getSupportedTextureFormats } from './utilities/textureSupport'; +import { RenderingEngineModeEnum } from './enums'; // TODO: change config into a class with methods to better control get/set const defaultConfig: Cornerstone3DConfig = { @@ -14,7 +15,34 @@ const defaultConfig: Cornerstone3DConfig = { // GPU rendering options preferSizeOverAccuracy: false, strictZSpacingForVolumeViewport: true, + /** + * The rendering engine mode to use. + * 'contextPool' is the a rendering engine that uses sequential rendering, pararllization and has enhanced support/performance for multi-monitor and high resolution displays. + * 'tiled' is a rendering engine that uses tiled rendering. + */ + renderingEngineMode: RenderingEngineModeEnum.ContextPool, + + /** + * The number of WebGL contexts to create. This is used for parallel rendering. + * The default value is 7, which is suitable for mobile/desktop. + */ + webGlContextCount: 7, + volumeRendering: { + /** Multiplier for the calculated sample distance */ + sampleDistanceMultiplier: 1, + }, }, + + debug: { + /** + * Wether or not to show the stats overlay for debugging purposes, stats include: + * - FPS Frames rendered in the last second. The higher the number the better. + * - MS Milliseconds needed to render a frame. The lower the number the better. + * - MB MBytes of allocated memory. (Run Chrome with --enable-precise-memory-info) + */ + statsOverlay: false, + }, + /** * Imports peer modules. * This may just fallback to the default import, but many packaging @@ -97,6 +125,11 @@ function init(configuration = config): boolean { // merge configs config = deepMerge(defaultConfig, configuration); + // mobile safe + if (config.isMobile) { + config.rendering.webGlContextCount = 1; + } + if (isIOS()) { if (configuration.rendering?.preferSizeOverAccuracy) { config.rendering.preferSizeOverAccuracy = true; diff --git a/packages/core/src/loaders/ProgressiveRetrieveImages.ts b/packages/core/src/loaders/ProgressiveRetrieveImages.ts index 24dcd1e0d4..ed7fb20072 100644 --- a/packages/core/src/loaders/ProgressiveRetrieveImages.ts +++ b/packages/core/src/loaders/ProgressiveRetrieveImages.ts @@ -301,8 +301,8 @@ class ProgressiveRetrieveImagesInstance { position < 0 ? this.imageIds.length + position : position < 1 - ? Math.floor((this.imageIds.length - 1) * position) - : position; + ? Math.floor((this.imageIds.length - 1) * position) + : position; const imageId = this.imageIds[index]; if (!imageId) { throw new Error(`No value found to add to requests at ${position}`); diff --git a/packages/core/src/loaders/volumeLoader.ts b/packages/core/src/loaders/volumeLoader.ts index a99b0556cc..305629bb93 100644 --- a/packages/core/src/loaders/volumeLoader.ts +++ b/packages/core/src/loaders/volumeLoader.ts @@ -218,7 +218,7 @@ export function createAndCacheDerivedVolume( ? ( referencedVolume as StreamingDynamicImageVolume ).getCurrentDimensionGroupImageIds() - : referencedVolume.imageIds ?? []; + : (referencedVolume.imageIds ?? []); // Todo: fix later // const byteLength = referencedImageIds.reduce((total, imageId) => { @@ -377,7 +377,7 @@ export function createLocalVolume( const dataType = scalarData ? (scalarData.constructor.name as PixelDataTypedArrayString) - : targetBuffer?.type ?? 'Float32Array'; + : (targetBuffer?.type ?? 'Float32Array'); const totalNumberOfVoxels = sliceLength * dimensions[2]; let byteLength; diff --git a/packages/core/src/types/AffineMatrix.ts b/packages/core/src/types/AffineMatrix.ts index 5d75c4d049..4553244611 100644 --- a/packages/core/src/types/AffineMatrix.ts +++ b/packages/core/src/types/AffineMatrix.ts @@ -2,7 +2,7 @@ type AffineMatrix = [ [number, number, number, number], [number, number, number, number], [number, number, number, number], - [number, number, number, number] + [number, number, number, number], ]; export type { AffineMatrix }; diff --git a/packages/core/src/types/Cornerstone3DConfig.ts b/packages/core/src/types/Cornerstone3DConfig.ts index 7241a947e6..ba6934211c 100644 --- a/packages/core/src/types/Cornerstone3DConfig.ts +++ b/packages/core/src/types/Cornerstone3DConfig.ts @@ -1,11 +1,13 @@ +import type { RenderingEngineModeType } from '../types'; + interface Cornerstone3DConfig { - gpuTier: { tier: number }; + gpuTier?: { tier?: number }; /** * Whether the device is mobile or not. */ - isMobile: boolean; + isMobile?: boolean; - rendering: { + rendering?: { // vtk.js supports 8bit integer textures and 32bit float textures. // However, if the client has norm16 textures (it can be seen by visiting // the webGl report at https://webglreport.com/?v=2), vtk will be default @@ -18,8 +20,8 @@ interface Cornerstone3DConfig { // Read more in the following Pull Request: // 1. HalfFloat: https://github.com/Kitware/vtk-js/pull/2046 // 2. Norm16: https://github.com/Kitware/vtk-js/pull/2058 - preferSizeOverAccuracy: boolean; - useCPURendering: boolean; + preferSizeOverAccuracy?: boolean; + useCPURendering?: boolean; /** * flag to control whether to use fallback behavior for z-spacing calculation in * volume viewports when the necessary metadata is missing. If enabled, @@ -29,7 +31,34 @@ interface Cornerstone3DConfig { * in scenarios where the metadata is incomplete or missing, but * it might be wrong assumption in certain scenarios. */ - strictZSpacingForVolumeViewport: boolean; + strictZSpacingForVolumeViewport?: boolean; + + /** + * The rendering engine mode to use. + * 'contextPool' is the a rendering engine that uses sequential rendering, pararllization and has enhanced support/performance for multi-monitor and high resolution displays. + * 'tiled' is a rendering engine that uses tiled rendering. + */ + renderingEngineMode?: RenderingEngineModeType; + + /** + * The number of WebGL contexts to create. This is used for parallel rendering. + * The default value is 7, which is suitable for mobile/desktop. + */ + webGlContextCount?: number; + volumeRendering?: { + /** Multiplier for the calculated sample distance */ + sampleDistanceMultiplier?: number; + }; + }; + + debug: { + /** + * Wether or not to show the stats overlay for debugging purposes, stats include: + * - FPS Frames rendered in the last second. The higher the number the better. + * - MS Milliseconds needed to render a frame. The lower the number the better. + * - MB MBytes of allocated memory. (Run Chrome with --enable-precise-memory-info) + */ + statsOverlay?: boolean; }; /** diff --git a/packages/core/src/types/IRenderingEngine.ts b/packages/core/src/types/IRenderingEngine.ts index d18799cef5..c4fe4bc715 100644 --- a/packages/core/src/types/IRenderingEngine.ts +++ b/packages/core/src/types/IRenderingEngine.ts @@ -1,4 +1,4 @@ -import type RenderingEngine from '../RenderingEngine/RenderingEngine'; +import type { RenderingEngine } from '../RenderingEngine'; type IRenderingEngine = RenderingEngine; diff --git a/packages/core/src/types/IViewport.ts b/packages/core/src/types/IViewport.ts index ee5af20fe8..cf9a4069f3 100644 --- a/packages/core/src/types/IViewport.ts +++ b/packages/core/src/types/IViewport.ts @@ -32,7 +32,9 @@ export type ViewReferenceSpecifier = { */ rangeEndSliceIndex?: number; - /** The frame number for a multiframe */ + /** + * The frame number for a multiframe + */ frameNumber?: number; /** @@ -42,9 +44,13 @@ export type ViewReferenceSpecifier = { * reference UID. */ forFrameOfReference?: boolean; - /** Set of points to get a reference for, in world space */ + /** + * Set of points to get a reference for, in world space + */ points?: Point3[]; - /** The volumeId to reference */ + /** + * The volumeId to reference + */ volumeId?: string; }; @@ -110,17 +116,73 @@ export type ReferencedImageRange = ViewReference & { referencedImageId: string; }; +/** + * A plane restriction is an object that restricts which camera views are + * compatible with the restriction. Currently the restriction only + * allows being specified via the frame of reference and a point in the + * view and up to two in plane vectors. + * + * If only the FOR and a point is specified, any camera view containing that point + * is viewable. + * + * If a inPlaneVector(s) are specified, they must also be orthogonal to the view plane normal. + * + * Other types of plane restrictions may be defined at a later point. + */ +export type PlaneRestriction = { + FrameOfReferenceUID: string; + + /** + * A single point within the reference plane is required for all references + */ + point: Point3; + + /** + * An inPlaneVector1 is required for all colinear referenced planes. + * Shall not be undefined if inPlaneVector2 is defined. + */ + inPlaneVector1?: Point3; + + /** + * An inPlaneVector2 is required for all full planar definitions. + * Shall have a non-zero dot product with inPlaneVector1, that is, shall be + * non-colinear with inPlaneVector1. + */ + inPlaneVector2?: Point3; +}; + /** * A view reference references the image/location of an image. Typical use * cases include remembering the current position of a viewport to allow returning * to it later, as well as determining whether specific views should show annotations * or other overlay information. + * + * Note this is an interface as it is designed to allow extension/customization + * by additional Viewport modules. */ -export type ViewReference = { +export interface ViewReference { /** * The FrameOfReferenceUID */ FrameOfReferenceUID?: string; + + /** + * A referenced plane identifies one or more planes. + * Currently this has a point within the plane to identify the focal depth + * (but NOT the focal point), and up to two coplanar vectors. + * + * The referenced plane is visible depth wise if the point minus the focal point of hte + * current view is orthogonal to the view plane normal of the viewport. + * This is sufficient for a single-point identifier. + * For a line, the inPlaneVector must be orthogonal to the view plane normal of the viewport. + * For a planar annotation, both inPlaneVectors must be orthogonal to the view plane normal of the viewport. + * + * This extension from the least specific to the most specific types of views allows + * determining whether a view can be seen in a variety of conditions. It does not + * allow recovering the original view reference. + */ + planeRestriction?: PlaneRestriction; + /** * An optional property used to specify the particular image that this view includes. * For volumes, that will specify which image is closest to the requested @@ -206,7 +268,7 @@ export type ViewReference = { * particular bounds or not. This will be in world coordinates. */ bounds?: BoundsLPS; -}; +} /** * A view presentation stores information about how the view is presented to the diff --git a/packages/core/src/types/OrientationVectors.ts b/packages/core/src/types/OrientationVectors.ts index e275e30813..37913ece53 100644 --- a/packages/core/src/types/OrientationVectors.ts +++ b/packages/core/src/types/OrientationVectors.ts @@ -30,7 +30,7 @@ interface OrientationVectors { /** Slice Normal for the viewport - the normal that points in the opposite direction of the slice normal out of screen and is negative of direction of projection */ viewPlaneNormal: Point3; /** viewUp direction for the viewport - the vector that points from bottom to top of the viewport */ - viewUp: Point3; + viewUp?: Point3; } export type { OrientationVectors as default }; diff --git a/packages/core/src/types/RenderingEngineMode.ts b/packages/core/src/types/RenderingEngineMode.ts new file mode 100644 index 0000000000..0239161d01 --- /dev/null +++ b/packages/core/src/types/RenderingEngineMode.ts @@ -0,0 +1,3 @@ +type RenderingEngineModeType = 'tiled' | 'contextPool'; + +export type { RenderingEngineModeType }; diff --git a/packages/core/src/types/ViewportProperties.ts b/packages/core/src/types/ViewportProperties.ts index d48b3a8f8a..f6d170c7f7 100644 --- a/packages/core/src/types/ViewportProperties.ts +++ b/packages/core/src/types/ViewportProperties.ts @@ -18,4 +18,6 @@ export interface ViewportProperties { interpolationType?: InterpolationType; preset?: string; + + sampleDistanceMultiplier?: number; } diff --git a/packages/core/src/types/VtkOffscreenMultiRenderWindow.ts b/packages/core/src/types/VtkOffscreenMultiRenderWindow.ts new file mode 100644 index 0000000000..10bdabdae9 --- /dev/null +++ b/packages/core/src/types/VtkOffscreenMultiRenderWindow.ts @@ -0,0 +1,45 @@ +import type { vtkObject } from '@kitware/vtk.js/interfaces'; +import type vtkStreamingOpenGLRenderWindow from '../RenderingEngine/vtkClasses/vtkStreamingOpenGLRenderWindow'; +import type vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; +import type vtkRenderWindow from '@kitware/vtk.js/Rendering/Core/RenderWindow'; +import type vtkRenderWindowInteractor from '@kitware/vtk.js/Rendering/Core/RenderWindowInteractor'; + +import '@kitware/vtk.js/Common/Core/Points'; +import '@kitware/vtk.js/Common/Core/DataArray'; +import '@kitware/vtk.js/Common/DataModel/PolyData'; +import '@kitware/vtk.js/Rendering/Core/Actor'; +import '@kitware/vtk.js/Rendering/Core/Mapper'; + +type Viewport = [number, number, number, number]; + +interface RendererConfig { + id: string; + viewport: Viewport; + background?: [number, number, number]; +} + +export interface VtkOffscreenMultiRenderWindow extends vtkObject { + renderWindow: vtkRenderWindow; + getRenderWindow: () => vtkRenderWindow; + + openGLRenderWindow: ReturnType< + typeof vtkStreamingOpenGLRenderWindow.newInstance + >; + getOpenGLRenderWindow: () => ReturnType< + typeof vtkStreamingOpenGLRenderWindow.newInstance + >; + + interactor: vtkRenderWindowInteractor; + getInteractor: () => vtkRenderWindowInteractor; + + container: HTMLDivElement | null; + getContainer: () => HTMLDivElement | null; + + addRenderer: (config: RendererConfig) => void; + removeRenderer: (id: string) => void; + getRenderer: (id: string) => vtkRenderer; + getRenderers: () => Array<{ id: string; renderer: vtkRenderer }>; + resize: () => void; + setContainer: (el: HTMLDivElement) => void; + destroy: () => void; +} diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 52f1f26fce..4cef5b476b 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -161,6 +161,9 @@ import type JumpToSliceOptions from './JumpToSliceOptions'; import type GeometryLoaderFn from './GeometryLoaderFn'; +import type { RenderingEngineModeType } from './RenderingEngineMode'; +import type { VtkOffscreenMultiRenderWindow } from './VtkOffscreenMultiRenderWindow'; + export type { // config Cornerstone3DConfig, @@ -313,4 +316,6 @@ export type { HistoryMemo, VoxelManager, RLEVoxelMap, + RenderingEngineModeType, + VtkOffscreenMultiRenderWindow, }; diff --git a/packages/core/src/utilities/asArray.ts b/packages/core/src/utilities/asArray.ts new file mode 100644 index 0000000000..dac8f6df76 --- /dev/null +++ b/packages/core/src/utilities/asArray.ts @@ -0,0 +1,12 @@ +/** + * Returns an array with the item if it is a object/primitive, otherwise, if it is an array, returns the array itself. + * + * @param item array or single object/primitive + * @returns an array with the object/primitive as the single element or the original array + */ +export function asArray(item: T | T[]): T[] { + if (Array.isArray(item)) { + return item; + } + return [item]; +} diff --git a/packages/core/src/utilities/colormap.ts b/packages/core/src/utilities/colormap.ts index 7328acf6b6..016245fcae 100644 --- a/packages/core/src/utilities/colormap.ts +++ b/packages/core/src/utilities/colormap.ts @@ -100,10 +100,13 @@ function findMatchingColormap(rgbPoints, actor): ColormapPublic | null { } } - return { + const result = { name: matchedColormap.Name, - opacity, + ...(Array.isArray(opacity) && opacity.length > 0 && { opacity }), + ...(typeof opacity === 'number' && { opacity }), }; + + return result; } export function setColorMapTransferFunctionForVolumeActor(volumeInfo) { diff --git a/packages/core/src/utilities/getPixelSpacingInformation.ts b/packages/core/src/utilities/getPixelSpacingInformation.ts index c2545c2251..94e423ee61 100644 --- a/packages/core/src/utilities/getPixelSpacingInformation.ts +++ b/packages/core/src/utilities/getPixelSpacingInformation.ts @@ -110,7 +110,10 @@ function calculateUSPixelSpacing(instance) { const { PhysicalDeltaX, PhysicalDeltaY } = isArrayOfSequences ? SequenceOfUltrasoundRegions[0] : SequenceOfUltrasoundRegions; - const USPixelSpacing = [PhysicalDeltaX * 10, PhysicalDeltaY * 10]; + const USPixelSpacing = [ + Math.abs(PhysicalDeltaX) * 10, + Math.abs(PhysicalDeltaY) * 10, + ]; return { PixelSpacing: USPixelSpacing, diff --git a/packages/core/src/utilities/historyMemo/index.ts b/packages/core/src/utilities/historyMemo/index.ts index 3a2af0daf0..b615663c6c 100644 --- a/packages/core/src/utilities/historyMemo/index.ts +++ b/packages/core/src/utilities/historyMemo/index.ts @@ -1,4 +1,5 @@ import eventTarget from '../../eventTarget'; +import { asArray } from '../asArray'; // Define Events from tools package // Note: We can't directly import from tools package due to circular dependency @@ -58,7 +59,8 @@ export class HistoryMemo { private position = -1; private redoAvailable = 0; private undoAvailable = 0; - private ring = new Array(); + private ring = new Array(); + private isRecordingGrouped = false; constructor(label = 'Tools', size = 50) { this.label = label; @@ -79,26 +81,25 @@ export class HistoryMemo { this.undoAvailable = 0; } + public get canUndo() { + return this.undoAvailable > 0; + } + + public get canRedo() { + return this.redoAvailable > 0; + } + /** - * Undoes up to the given number of items off the ring + * Undoes up to the given number of items off the ring. + * If one is a group (array) item it will undo every item inside */ public undo(items = 1) { while (items > 0 && this.undoAvailable > 0) { const item = this.ring[this.position]; - item.restoreMemo(true); - - // Dispatch history undo event - if (item.id) { - eventTarget.dispatchEvent( - new CustomEvent(Events.HISTORY_UNDO, { - detail: { - isUndo: true, - id: item.id, - operationType: item.operationType || 'annotation', - memo: item, - }, - }) - ); + + for (const subitem of asArray(item).reverse()) { + subitem.restoreMemo(true); + this.dispatchHistoryEvent({ item: subitem, isUndo: true }); } items--; @@ -113,7 +114,7 @@ export class HistoryMemo { * @param condition - Function that evaluates if the undo should be performed * @returns True if an undo was performed, false otherwise */ - public undoIf(condition: (item: Memo) => boolean): boolean { + public undoIf(condition: (item: Memo | Memo[]) => boolean): boolean { if (this.undoAvailable > 0 && condition(this.ring[this.position])) { this.undo(); return true; @@ -121,27 +122,38 @@ export class HistoryMemo { return false; } + /** + * If item has an id, dispatches a undo or redo event. + * @param args.item memo with id and operation type + * @param args.isUndo true if it is for undo and false if it is for redo + */ + private dispatchHistoryEvent({ item, isUndo }) { + if (item.id) { + eventTarget.dispatchEvent( + new CustomEvent(isUndo ? Events.HISTORY_UNDO : Events.HISTORY_REDO, { + detail: { + isUndo, + id: item.id, + operationType: item.operationType || 'annotation', + memo: item, + }, + }) + ); + } + } + /** * Redoes up to the given number of items, adding them to the top of the ring. + * If one is a group (array) item it will redo every item inside */ public redo(items = 1) { while (items > 0 && this.redoAvailable > 0) { const newPosition = (this.position + 1) % this.size; const item = this.ring[newPosition]; - item.restoreMemo(false); - - // Dispatch history redo event - if (item.id) { - eventTarget.dispatchEvent( - new CustomEvent(Events.HISTORY_REDO, { - detail: { - isUndo: false, - id: item.id, - operationType: item.operationType || 'annotation', - memo: item, - }, - }) - ); + + for (const subitem of asArray(item).reverse()) { + subitem.restoreMemo(false); + this.dispatchHistoryEvent({ item: subitem, isUndo: false }); } items--; @@ -151,6 +163,55 @@ export class HistoryMemo { } } + /** initializes an array for the group item */ + private initializeGroupItem() { + this.redoAvailable = 0; + if (this.undoAvailable < this._size) { + this.undoAvailable++; + } + this.position = (this.position + 1) % this._size; + this.ring[this.position] = []; + } + + /** + * Starts a group recording, so that with a single undo you can undo multiple actions that are related to each other. + * Requires endGroupRecording to be called after the group action is done. + */ + public startGroupRecording() { + this.isRecordingGrouped = true; + this.initializeGroupItem(); + } + + /** Rolls back an initialized but unused group item (an empty array) */ + private rollbackUnusedGroupItem() { + this.ring[this.position] = undefined; + this.position = (this.position - 1) % this._size; + this.undoAvailable--; + } + + /** Ends a group recording. Must be called after the group action is finished */ + public endGroupRecording() { + this.isRecordingGrouped = false; + + const lastItem = this.ring[this.position]; + const lastItemIsEmpty = Array.isArray(lastItem) && lastItem.length === 0; + + if (lastItemIsEmpty) { + this.rollbackUnusedGroupItem(); + } + } + + /** Add grouped items to the ring. If the current item is not a array, it will generate a new array. Otherwise it will push to the current array */ + private pushGrouped(memo: Memo) { + const lastMemo = this.ring[this.position]; + if (Array.isArray(lastMemo)) { + lastMemo.push(memo); + return memo; + } + + throw new Error('Last item should be an array for grouped memos.'); + } + /** * Pushes a new memo onto the ring. This will remove all redoable items * from the ring if a memo was pushed. Ignores undefined or null items. @@ -166,6 +227,11 @@ export class HistoryMemo { if (!memo) { return; } + + if (this.isRecordingGrouped) { + return this.pushGrouped(memo); + } + this.redoAvailable = 0; if (this.undoAvailable < this._size) { this.undoAvailable++; diff --git a/packages/core/src/utilities/index.ts b/packages/core/src/utilities/index.ts index 6f751d670d..82f5198eef 100644 --- a/packages/core/src/utilities/index.ts +++ b/packages/core/src/utilities/index.ts @@ -100,6 +100,8 @@ import calculateSpacingBetweenImageIds from './calculateSpacingBetweenImageIds'; export * as logger from './logger'; import { calculateNeighborhoodStats } from './calculateNeighborhoodStats'; import getPixelSpacingInformation from './getPixelSpacingInformation'; +import { asArray } from './asArray'; +export { updatePlaneRestriction } from './updatePlaneRestriction'; const getViewportModality = (viewport: IViewport, volumeId?: string) => _getViewportModality(viewport, volumeId, cache.getVolume); @@ -203,4 +205,5 @@ export { buildMetadata, calculateNeighborhoodStats, getPixelSpacingInformation, + asArray, }; diff --git a/packages/core/src/utilities/isEqual.ts b/packages/core/src/utilities/isEqual.ts index 76e9475565..24d343ed1f 100644 --- a/packages/core/src/utilities/isEqual.ts +++ b/packages/core/src/utilities/isEqual.ts @@ -50,7 +50,7 @@ function isNumberArrayLike(value: unknown): value is ArrayLike { * * @returns True if the two values are within the tolerance levels. */ -export default function isEqual( +export function isEqual( v1: ValueType, v2: ValueType, tolerance = 1e-5 @@ -80,7 +80,7 @@ const abs = (v) => /** * Compare negative values of both single numbers and vectors */ -const isEqualNegative = ( +export const isEqualNegative = ( v1: ValueType, v2: ValueType, tolerance = undefined @@ -90,7 +90,7 @@ const isEqualNegative = ( * Compare absolute values for single numbers and vectors. * Not recommended for large vectors as this creates a copy */ -const isEqualAbs = ( +export const isEqualAbs = ( v1: ValueType, v2: ValueType, tolerance = undefined @@ -100,11 +100,11 @@ const isEqualAbs = ( * @param n - array of numbers or a simple number * @returns True if n or the first element of n is finite and not NaN */ -function isNumber(n: number[] | number): boolean { +export function isNumber(n: number[] | number): boolean { if (Array.isArray(n)) { return isNumber(n[0]); } return isFinite(n) && !isNaN(n); } -export { isEqualNegative, isEqual, isEqualAbs, isNumber }; +export default isEqual; diff --git a/packages/core/src/utilities/logger.ts b/packages/core/src/utilities/logger.ts index d1a3111a75..4764998639 100644 --- a/packages/core/src/utilities/logger.ts +++ b/packages/core/src/utilities/logger.ts @@ -13,7 +13,6 @@ if (typeof window !== 'undefined') { (window as unknown as WindowLog).log = loglevel; } - export type Logger = LogLevelLogger & { getLogger: (...categories: string[]) => Logger; }; diff --git a/packages/core/src/utilities/renderToCanvasGPU.ts b/packages/core/src/utilities/renderToCanvasGPU.ts index 654a5eb6d0..89190fd1de 100644 --- a/packages/core/src/utilities/renderToCanvasGPU.ts +++ b/packages/core/src/utilities/renderToCanvasGPU.ts @@ -14,7 +14,8 @@ import type { Point3, } from '../types'; import { getRenderingEngine } from '../RenderingEngine/getRenderingEngine'; -import RenderingEngine from '../RenderingEngine'; +import type RenderingEngine from '../RenderingEngine'; +import TiledRenderingEngine from '../RenderingEngine/TiledRenderingEngine'; import isPTPrescaledWithSUV from './isPTPrescaledWithSUV'; import type { CanvasLoadPosition } from './loadImageToCanvas'; @@ -88,7 +89,7 @@ export default function renderToCanvasGPU( const temporaryCanvas = getOrCreateCanvas(element); const renderingEngine = (getRenderingEngine(renderingEngineId) as RenderingEngine) || - new RenderingEngine(renderingEngineId); + new TiledRenderingEngine(renderingEngineId); let viewport = renderingEngine.getViewport(viewportId); diff --git a/packages/core/src/utilities/roundNumber.ts b/packages/core/src/utilities/roundNumber.ts index cfb8f54a67..59dd01390e 100644 --- a/packages/core/src/utilities/roundNumber.ts +++ b/packages/core/src/utilities/roundNumber.ts @@ -31,16 +31,16 @@ function roundNumber( absValue >= 100 ? precision - 2 : absValue >= 10 - ? precision - 1 - : absValue >= 1 - ? precision - : absValue >= 0.1 - ? precision + 1 - : absValue >= 0.01 - ? precision + 2 - : absValue >= 0.001 - ? precision + 3 - : precision + 4; + ? precision - 1 + : absValue >= 1 + ? precision + : absValue >= 0.1 + ? precision + 1 + : absValue >= 0.01 + ? precision + 2 + : absValue >= 0.001 + ? precision + 3 + : precision + 4; return value.toFixed(fixedPrecision); } diff --git a/packages/core/src/utilities/updatePlaneRestriction.ts b/packages/core/src/utilities/updatePlaneRestriction.ts new file mode 100644 index 0000000000..f7c6cb73ce --- /dev/null +++ b/packages/core/src/utilities/updatePlaneRestriction.ts @@ -0,0 +1,74 @@ +import type { Point3, ViewReference } from '../types'; +import { isEqual } from '../utilities/isEqual'; +import { vec3 } from 'gl-matrix'; + +/** + * A value to compare how orthogonal two vectors are. As long as the dot + * product of the previous in-plane vector and the new vector as unit vectors, + * this vector will be considered for using for testing for orthogonality with + * view plane normals. + */ +const ORTHOGONAL_TEST_VALUE = 0.95; + +/** + * Updates the planeRestriction(s) inside the view reference + * This will create a reference containing a point and up to two non-collinear + * in-plane vectors, selected from the set of points provided. + * + * This type of reference restricts the allowed camera views to those + * which contain the point, and whose view plane normal is orthogonal to the in + * plane vectors. + */ +export function updatePlaneRestriction( + points: Point3[], + reference: ViewReference +) { + if (!points?.length || !reference.FrameOfReferenceUID) { + return; + } + reference.planeRestriction ||= { + FrameOfReferenceUID: reference.FrameOfReferenceUID, + point: points[0], + inPlaneVector1: null, + inPlaneVector2: null, + }; + const { planeRestriction } = reference; + + if (points.length === 1) { + planeRestriction.inPlaneVector1 = null; + planeRestriction.inPlaneVector2 = null; + return planeRestriction; + } + + const v1 = vec3.sub( + vec3.create(), + points[0], + points[Math.floor(points.length / 2)] + ); + vec3.normalize(v1, v1); + planeRestriction.inPlaneVector1 = v1; + + planeRestriction.inPlaneVector2 = null; + const n = points.length; + if (n > 2) { + // Try to find a second vector that isn't colinear with the first one + // to form a plane specifier. + for (let i = Math.floor(n / 3); i < n; i++) { + const testVector = vec3.sub(vec3.create(), points[i], points[0]); + const length = vec3.length(testVector); + if (isEqual(length, 0)) { + continue; + } + if ( + vec3.dot(testVector, planeRestriction.inPlaneVector1) < + length * ORTHOGONAL_TEST_VALUE + ) { + vec3.normalize(testVector, testVector); + planeRestriction.inPlaneVector2 = testVector; + return planeRestriction; + } + } + } + + return planeRestriction; +} diff --git a/packages/core/test/utilities/historyMemo.jest.js b/packages/core/test/utilities/historyMemo.jest.js index 09ba58834d..4ceebff4a6 100644 --- a/packages/core/test/utilities/historyMemo.jest.js +++ b/packages/core/test/utilities/historyMemo.jest.js @@ -1,4 +1,4 @@ -import { DefaultHistoryMemo } from '../../src/utilities/historyMemo'; +import { HistoryMemo } from '../../src/utilities/historyMemo'; import { describe, it, expect } from '@jest/globals'; @@ -17,13 +17,39 @@ function createMemo(rememberState) { }; } +let historyMemo; +beforeEach(() => { + historyMemo = new HistoryMemo(); +}); + describe('HistoryMemo', function () { - it('Simple state remembering', () => { - DefaultHistoryMemo.push(state); + it('remembers state changes', () => { + historyMemo.push(state); state.testState = 1; - DefaultHistoryMemo.undo(); + + historyMemo.undo(); expect(state.testState).toBe(0); - DefaultHistoryMemo.redo(); + historyMemo.redo(); expect(state.testState).toBe(1); }); + + it('tracks undo/redo availability', () => { + expect(historyMemo.canUndo).toBe(false); + expect(historyMemo.canRedo).toBe(false); + + historyMemo.push(state); + state.testState = 1; + + expect(historyMemo.canUndo).toBe(true); + historyMemo.undo(); + expect(historyMemo.canRedo).toBe(true); + }); + + it('works with DefaultHistoryMemo', () => { + const defaultHistoryMemo = new HistoryMemo(); + defaultHistoryMemo.push(state); + expect(defaultHistoryMemo.canUndo).toBe(true); + defaultHistoryMemo.undo(); + expect(defaultHistoryMemo.canRedo).toBe(true); + }); }); diff --git a/packages/dicomImageLoader/docs/Building.md b/packages/dicomImageLoader/docs/Building.md index 537edcac2a..a8768735bd 100644 --- a/packages/dicomImageLoader/docs/Building.md +++ b/packages/dicomImageLoader/docs/Building.md @@ -1,21 +1,21 @@ -Build System -============ +# Build System This project uses Webpack to build the software. -Pre-requisites: ---------------- +## Pre-requisites: NodeJs - [click to visit web site for installation instructions](http://nodejs.org). -Common Tasks ------------- +## Common Tasks Update dependencies (after each pull): + > npm install Running the build: + > npm run build Automatically running the build and unit tests after each source change: -> npm run watch \ No newline at end of file + +> npm run watch diff --git a/packages/dicomImageLoader/docs/ImageIds.md b/packages/dicomImageLoader/docs/ImageIds.md index 9a9d5fdd72..a058255566 100644 --- a/packages/dicomImageLoader/docs/ImageIds.md +++ b/packages/dicomImageLoader/docs/ImageIds.md @@ -1,8 +1,7 @@ -ImageIds -======== +# ImageIds The image loader prefix is 'wadouri' (note that the prefix dicomweb is also supported but is deprecated and will eventually -be removed). Here are some example imageId's: +be removed). Here are some example imageId's: absolute url: @@ -29,7 +28,7 @@ wadouri:http://localhost:8042/instances/8cce70aa-576ad738-b76cb63f-caedb3c7-2b21 ``` Note that the web server must support [Cross origin resource sharing](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) -or the image will fail to load. If you are unable to get CORS enabled on the web server that you are loading DICOM P10 -instances from, you can use a [reverse proxy](http://en.wikipedia.org/wiki/Reverse_proxy). Here is a +or the image will fail to load. If you are unable to get CORS enabled on the web server that you are loading DICOM P10 +instances from, you can use a [reverse proxy](http://en.wikipedia.org/wiki/Reverse_proxy). Here is a [simple Node.js based http-proxy](http://chafey.blogspot.com/2014/09/working-around-cors.html) that adds CORS headers -that you might find useful. \ No newline at end of file +that you might find useful. diff --git a/packages/dicomImageLoader/docs/TransferSyntaxes.md b/packages/dicomImageLoader/docs/TransferSyntaxes.md index 24605dff66..27f24deb8a 100644 --- a/packages/dicomImageLoader/docs/TransferSyntaxes.md +++ b/packages/dicomImageLoader/docs/TransferSyntaxes.md @@ -1,36 +1,35 @@ -Transfer Syntaxes -================= +# Transfer Syntaxes This image loader supports the following transfer syntaxes: -Uncompressed ------------- -* 1.2.840.10008.1.2 Implicit VR Endian -* 1.2.840.10008.1.2.1 Explicit VR Little Endian -* 1.2.840.10008.1.2.2 Explicit VR Big Endian +## Uncompressed -Compressed (requires codec, see below) --------------------------------------- -* 1.2.840.10008.1.2.5 RLE Lossless -* 1.2.840.10008.1.2.4.50 JPEG Baseline (Process 1 - 8 bit) -* 1.2.840.10008.1.2.4.51 JPEG Baseline (Processes 2 & 4 - 12 bit) -* 1.2.840.10008.1.2.4.57 JPEG Lossless, Nonhierarchical (Processes 14) -* 1.2.840.10008.1.2.4.70 JPEG Lossless, Nonhierarchical (Processes 14 [Selection 1]) -* 1.2.840.10008.1.2.4.80 JPEG-LS Lossless Image Compression -* 1.2.840.10008.1.2.4.81 JPEG-LS Lossy (Near-Lossless) Image Compression -* 1.2.840.10008.1.2.4.90 JPEG 2000 Image Compression (Lossless Only) -* 1.2.840.10008.1.2.4.91 JPEG 2000 Image Compression -* 3.2.840.10008.1.2.4.96 HTJ2K (private TSUID for HTJ2K) -* 1.2.840.10008.1.2.4.202 High Throughput JPEG 2000 (HTJ2K with RPCL) -* 1.2.840.10008.1.2.1.99 Deflate Transfer Syntax +- 1.2.840.10008.1.2 Implicit VR Endian +- 1.2.840.10008.1.2.1 Explicit VR Little Endian +- 1.2.840.10008.1.2.2 Explicit VR Big Endian -Photometric Interpretations ---------------------------- -* MONOCHROME1 -* MONOCHROME2 -* RGB (pixel and planar configurations) -* PALETTE COLOR -* YBR_FULL -* YBR_FULL_422 -* YBR_RCT -* YBR_ICT +## Compressed (requires codec, see below) + +- 1.2.840.10008.1.2.5 RLE Lossless +- 1.2.840.10008.1.2.4.50 JPEG Baseline (Process 1 - 8 bit) +- 1.2.840.10008.1.2.4.51 JPEG Baseline (Processes 2 & 4 - 12 bit) +- 1.2.840.10008.1.2.4.57 JPEG Lossless, Nonhierarchical (Processes 14) +- 1.2.840.10008.1.2.4.70 JPEG Lossless, Nonhierarchical (Processes 14 [Selection 1]) +- 1.2.840.10008.1.2.4.80 JPEG-LS Lossless Image Compression +- 1.2.840.10008.1.2.4.81 JPEG-LS Lossy (Near-Lossless) Image Compression +- 1.2.840.10008.1.2.4.90 JPEG 2000 Image Compression (Lossless Only) +- 1.2.840.10008.1.2.4.91 JPEG 2000 Image Compression +- 3.2.840.10008.1.2.4.96 HTJ2K (private TSUID for HTJ2K) +- 1.2.840.10008.1.2.4.202 High Throughput JPEG 2000 (HTJ2K with RPCL) +- 1.2.840.10008.1.2.1.99 Deflate Transfer Syntax + +## Photometric Interpretations + +- MONOCHROME1 +- MONOCHROME2 +- RGB (pixel and planar configurations) +- PALETTE COLOR +- YBR_FULL +- YBR_FULL_422 +- YBR_RCT +- YBR_ICT diff --git a/packages/dicomImageLoader/package.json b/packages/dicomImageLoader/package.json index 898f5907ab..29405ed5d3 100644 --- a/packages/dicomImageLoader/package.json +++ b/packages/dicomImageLoader/package.json @@ -74,7 +74,6 @@ "copy-dts": "echo 'not implemented yet'", "clean": "shx rm -rf dist", "clean:deep": "yarn run clean && shx rm -rf node_modules", - "format-check": "npx eslint ./src --quiet", "api-check": "api-extractor --debug run ", "cm": "npx git-cz", "clean:dist": "shx rm -rf dist", @@ -83,10 +82,7 @@ "doc": "npm run doc:generate && opn documentation/index.html", "doc:generate": "npm run clean:docs && jsdoc -c .jsdocrc", "dev": "tsc --project ./tsconfig.json --watch", - "eslint": "eslint -c .eslintrc.js src", - "eslint-quiet": "eslint -c .eslintrc.js --quiet src", - "eslint-fix": "eslint -c .eslintrc.js --fix src", - "eslint-fix-test": "eslint -c .eslintrc.js --fix test", + "lint": "oxlint .", "start": "npm run webpack:dev", "start:dev": "webpack-dev-server --config .webpack/webpack-dev", "test": "npm run test:chrome", @@ -119,13 +115,6 @@ "@cornerstonejs/core": "^4.0.0-beta.3", "dicom-parser": "^1.8.9" }, - "lint-staged": { - "src/**/*.{js,jsx,json,css}": [ - "eslint --fix", - "prettier --write", - "git add" - ] - }, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" diff --git a/packages/dicomImageLoader/src/codecs/jpeg.js b/packages/dicomImageLoader/src/codecs/jpeg.js index 4fbec0a6b7..7bf175b1ae 100644 --- a/packages/dicomImageLoader/src/codecs/jpeg.js +++ b/packages/dicomImageLoader/src/codecs/jpeg.js @@ -527,8 +527,8 @@ function quantizeAndInverse(component, blockBufferOffset, p) { q <= -2056 / component.bitConversion ? 0 : q >= 2024 / component.bitConversion - ? 255 / component.bitConversion - : (q + 2056 / component.bitConversion) >> 4; + ? 255 / component.bitConversion + : (q + 2056 / component.bitConversion) >> 4; component.blockData[index] = q; } } diff --git a/packages/dicomImageLoader/src/imageLoader/internal/rangeRequest.ts b/packages/dicomImageLoader/src/imageLoader/internal/rangeRequest.ts index b4800bd8e1..48f9e89d9d 100644 --- a/packages/dicomImageLoader/src/imageLoader/internal/rangeRequest.ts +++ b/packages/dicomImageLoader/src/imageLoader/internal/rangeRequest.ts @@ -104,7 +104,7 @@ export default function rangeRequest( // Allow over-writing the done status to indicate complete on partial const imageQualityStatus = getImageQualityStatus( retrieveOptions, - doneAllBytes || extract.extractDone + doneAllBytes || extract.extractDone === true ); resolve({ ...extract, diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLossless.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLossless.ts index 2b50974bdd..69fdc88a68 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLossless.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLossless.ts @@ -3,7 +3,7 @@ import type { Types } from '@cornerstonejs/core'; import type { WebWorkerDecodeConfig } from '../../types'; const local = { - jpeg: undefined, + DecoderClass: undefined, decodeConfig: {} as WebWorkerDecodeConfig, }; @@ -12,14 +12,13 @@ export function initialize( ): Promise { local.decodeConfig = decodeConfig; - if (local.jpeg) { + if (local.DecoderClass) { return Promise.resolve(); } return new Promise((resolve, reject) => { import('jpeg-lossless-decoder-js').then(({ Decoder }) => { - const decoder = new Decoder(); - local.jpeg = decoder; + local.DecoderClass = Decoder; resolve(); }, reject); }); @@ -32,13 +31,16 @@ async function decodeJPEGLossless( await initialize(); // check to make sure codec is loaded - if (typeof local.jpeg === 'undefined') { + if (typeof local.DecoderClass === 'undefined') { throw new Error('No JPEG Lossless decoder loaded'); } + // Create a new decoder instance for each decode operation to ensure thread safety + const decoder = new local.DecoderClass(); + const byteOutput = imageFrame.bitsAllocated <= 8 ? 1 : 2; const buffer = pixelData.buffer; - const decompressedData = local.jpeg.decode( + const decompressedData = decoder.decode( buffer, pixelData.byteOffset, pixelData.length, diff --git a/packages/dicomImageLoader/src/types/WADORSMetaData.ts b/packages/dicomImageLoader/src/types/WADORSMetaData.ts index bf3db80140..87de6d748d 100644 --- a/packages/dicomImageLoader/src/types/WADORSMetaData.ts +++ b/packages/dicomImageLoader/src/types/WADORSMetaData.ts @@ -1,5 +1,5 @@ export interface WADORSMetaDataElement< - ValueType = string[] | number[] | boolean + ValueType = string[] | number[] | boolean, > { Value: ValueType; } diff --git a/packages/docs/bun.lock b/packages/docs/bun.lock index 543602fd4a..065142a7e8 100644 --- a/packages/docs/bun.lock +++ b/packages/docs/bun.lock @@ -4,11 +4,11 @@ "": { "name": "docs", "dependencies": { - "@cornerstonejs/adapters": "^3.10.10", - "@cornerstonejs/core": "^3.10.10", - "@cornerstonejs/dicom-image-loader": "^3.10.10", - "@cornerstonejs/nifti-volume-loader": "^3.10.10", - "@cornerstonejs/tools": "^3.10.10", + "@cornerstonejs/adapters": "^3.32.5", + "@cornerstonejs/core": "^3.32.5", + "@cornerstonejs/dicom-image-loader": "^3.32.5", + "@cornerstonejs/nifti-volume-loader": "^3.32.5", + "@cornerstonejs/tools": "^3.32.5", "@docusaurus/core": "3.6.3", "@docusaurus/faster": "3.6.3", "@docusaurus/module-type-aliases": "3.6.3", @@ -18,7 +18,8 @@ "@mdx-js/react": "^3.0.1", "@svgr/webpack": "^8.1.0", "clsx": "^1.1.1", - "dcmjs": "^0.33.0", + "cross-env": "^7.0.3", + "dcmjs": "^0.43.1", "dicom-parser": "^1.8.21", "dicomweb-client": "0.10.4", "docusaurus-plugin-copy": "0.1.1", @@ -38,6 +39,7 @@ "devDependencies": { "copyfiles": "2.4.1", "esbuild-loader": "^2.18.0", + "glob": "^10.3.10", "karma-chrome-launcher": "^3.1.0", "netlify-plugin-cache": "^1.0.3", "puppeteer": "^13.1.3", @@ -307,7 +309,7 @@ "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], - "@cornerstonejs/adapters": ["@cornerstonejs/adapters@3.10.10", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "ndarray": "^1.0.19" }, "peerDependencies": { "@cornerstonejs/core": "^3.10.10", "@cornerstonejs/tools": "^3.10.10" } }, "sha512-Z84aEjXR3R4rvBav52bt3VuSCgs14dNQEozGcWz1kKDmBdon+ozBw8j8kmIeIT82kZKiqd3sLBa69NF4usuk4g=="], + "@cornerstonejs/adapters": ["@cornerstonejs/adapters@3.32.5", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.43.1", "gl-matrix": "^3.4.3", "ndarray": "^1.0.19" }, "peerDependencies": { "@cornerstonejs/core": "^3.32.5", "@cornerstonejs/tools": "^3.32.5" } }, "sha512-tpd+sHjE54VOqme83HZ9XqGKztjikrwbrWR4M3e8X+fuR6LJ+qNQ6+YkPdfThzu0KxN+q8mO5aSvGyJo82rRnA=="], "@cornerstonejs/codec-charls": ["@cornerstonejs/codec-charls@1.2.3", "", {}, "sha512-qKUe6DN0dnGzhhfZLYhH9UZacMcudjxcaLXCrpxJImT/M/PQvZCT2rllu6VGJbWKJWG+dMVV2zmmleZcdJ7/cA=="], @@ -317,13 +319,13 @@ "@cornerstonejs/codec-openjph": ["@cornerstonejs/codec-openjph@2.4.7", "", {}, "sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w=="], - "@cornerstonejs/core": ["@cornerstonejs/core@3.10.10", "", { "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "^4.4.1", "gl-matrix": "^3.4.3", "loglevel": "^1.9.2" } }, "sha512-M3Mh23xgkdJD5x9g33eGarKWrIek+Cv1ghIPVUGKRZjale1Z9PvGHXF6UXPskBQHw5D1RzNA5DurJQBEf26ZZw=="], + "@cornerstonejs/core": ["@cornerstonejs/core@3.32.5", "", { "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "^4.4.1", "gl-matrix": "^3.4.3", "loglevel": "^1.9.2" } }, "sha512-6m/ODfSyM+8/ZigD2wM2wl8yjxmDx9DTen/R5nxCHUe4sqK97PKjfmcN5OK9cOCzwlzqQBZTFow1Ra6UAfkKCA=="], - "@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@3.10.10", "", { "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", "comlink": "^4.4.1", "dicom-parser": "^1.8.9", "jpeg-lossless-decoder-js": "^2.1.0", "pako": "^2.0.4", "uuid": "^9.0.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.10.10" } }, "sha512-Tqj+VuwupA3p5BPBSZDFcTXjb54fighnG2LKkIQzyGjHvFQSyoeQN56+XPrITKCRY71H/5z9bSqG7h3jeVX9wQ=="], + "@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@3.32.5", "", { "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", "comlink": "^4.4.1", "dicom-parser": "^1.8.9", "jpeg-lossless-decoder-js": "^2.1.0", "pako": "^2.0.4", "uuid": "^9.0.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.32.5" } }, "sha512-fOUkphjdzTG8Flw0XV4bGUEZtbASeE2/Y+HssKr4PiwiDvGcVvVQF7RxyKkvabP3OsZIAaBOhPB8ghKEEXDp2w=="], - "@cornerstonejs/nifti-volume-loader": ["@cornerstonejs/nifti-volume-loader@3.10.10", "", { "dependencies": { "nifti-reader-js": "^0.6.8" }, "peerDependencies": { "@cornerstonejs/core": "^3.10.10" } }, "sha512-9ZBC11kjFEia6EQk2xlXYiXM5l/rf+2VQqds+7S1zET+nbgnFSfIf9T558CuBZEu9xDcrZHWNP7I+73i7phX4A=="], + "@cornerstonejs/nifti-volume-loader": ["@cornerstonejs/nifti-volume-loader@3.32.5", "", { "dependencies": { "nifti-reader-js": "^0.6.8" }, "peerDependencies": { "@cornerstonejs/core": "^3.32.5" } }, "sha512-Lf+CC4ZjsEHEQjo7lxuYjffexyImP4SDy6m/rnU5le1KP5bC+ucF4Sduy0sC3aEPHfSumJJISQ9HI9oDwxyONQ=="], - "@cornerstonejs/tools": ["@cornerstonejs/tools@3.10.10", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^3.10.10", "@kitware/vtk.js": "32.12.1", "@types/d3-array": "^3.0.4", "@types/d3-interpolate": "^3.0.1", "d3-array": "^3.2.3", "d3-interpolate": "^3.0.1", "gl-matrix": "^3.4.3" } }, "sha512-U8AhBhIaq0FpToQymN2mdO+2ei4cCnTclal5cCLFsd2x5/0A6YtLq7HfJiQ+XY/eJRgTOHpG4qnisOYiumj8YQ=="], + "@cornerstonejs/tools": ["@cornerstonejs/tools@3.32.5", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^3.32.5", "@kitware/vtk.js": "32.12.1", "@types/d3-array": "^3.0.4", "@types/d3-interpolate": "^3.0.1", "d3-array": "^3.2.3", "d3-interpolate": "^3.0.1", "gl-matrix": "^3.4.3" } }, "sha512-h1UWy/u8Cz1QPsMvbNF+rRGwaEdRnGwCbOSZQwLzYOuvaV+rvGAI6l+Xbhe9zUgzE1lSbBigiQjdGHiNtnVWPQ=="], "@csstools/cascade-layer-name-parser": ["@csstools/cascade-layer-name-parser@2.0.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3" } }, "sha512-7DFHlPuIxviKYZrOiwVU/PiHLm3lLUR23OMuEEtfEOQTOp9hzQ2JjdY6X5H18RVuUPJqSCI+qNnD5iOLMVE0bA=="], @@ -513,6 +515,8 @@ "@hapi/topo": ["@hapi/topo@5.1.0", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], "@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], @@ -565,6 +569,8 @@ "@oozcitak/util": ["@oozcitak/util@8.3.8", "", {}, "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="], "@pnpm/network.ca-file": ["@pnpm/network.ca-file@1.0.2", "", { "dependencies": { "graceful-fs": "4.2.10" } }, "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA=="], @@ -1085,6 +1091,8 @@ "cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], + "cross-fetch": ["cross-fetch@3.1.5", "", { "dependencies": { "node-fetch": "2.6.7" } }, "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -1141,7 +1149,7 @@ "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], - "dcmjs": ["dcmjs@0.33.1", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevelnext": "^3.0.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-/vwOPAX5fOQSwqf8t7bV7sy1h2h+gTWw5t/be8OIY7IwUpuKuKRChZvxINAjNZ9DDWHMB/uSOS7NJceu1YChfQ=="], + "dcmjs": ["dcmjs@0.43.1", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevel": "^1.8.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-7IAB2cs2F8QYINbfhA7PV+IDraqBuHn8N31xJc6HsyNxZXyGUPQRujcq732ACqjLhl7oJi1z8PJJgLdblQtD3Q=="], "debounce": ["debounce@1.2.1", "", {}, "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="], @@ -1357,6 +1365,8 @@ "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "fork-ts-checker-webpack-plugin": ["fork-ts-checker-webpack-plugin@6.5.3", "", { "dependencies": { "@babel/code-frame": "^7.8.3", "@types/json-schema": "^7.0.5", "chalk": "^4.1.0", "chokidar": "^3.4.2", "cosmiconfig": "^6.0.0", "deepmerge": "^4.2.2", "fs-extra": "^9.0.0", "glob": "^7.1.6", "memfs": "^3.1.2", "minimatch": "^3.0.4", "schema-utils": "2.7.0", "semver": "^7.3.2", "tapable": "^1.0.0" }, "peerDependencies": { "eslint": ">= 6", "typescript": ">= 2.7", "vue-template-compiler": "*", "webpack": ">= 4" }, "optionalPeers": ["eslint", "vue-template-compiler"] }, "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ=="], "form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], @@ -1401,7 +1411,7 @@ "gl-matrix": ["gl-matrix@3.4.3", "", {}, "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="], - "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "glob": ["glob@10.4.5", "", { "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-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1607,6 +1617,8 @@ "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], @@ -1703,8 +1715,6 @@ "loglevel": ["loglevel@1.9.2", "", {}, "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg=="], - "loglevelnext": ["loglevelnext@3.0.1", "", {}, "sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA=="], - "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], @@ -1713,7 +1723,7 @@ "lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "lunr": ["lunr@2.3.9", "", {}, "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="], @@ -1873,6 +1883,8 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "mississippi": ["mississippi@3.0.0", "", { "dependencies": { "concat-stream": "^1.5.0", "duplexify": "^3.4.2", "end-of-stream": "^1.1.0", "flush-write-stream": "^1.0.0", "from2": "^2.1.0", "parallel-transform": "^1.1.0", "pump": "^3.0.0", "pumpify": "^1.3.3", "stream-each": "^1.1.0", "through2": "^2.0.0" } }, "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA=="], "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], @@ -1965,6 +1977,8 @@ "package-json": ["package-json@8.1.1", "", { "dependencies": { "got": "^12.1.0", "registry-auth-token": "^5.0.1", "registry-url": "^6.0.0", "semver": "^7.3.7" } }, "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="], "parallel-transform": ["parallel-transform@1.2.0", "", { "dependencies": { "cyclist": "^1.0.1", "inherits": "^2.0.3", "readable-stream": "^2.1.5" } }, "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg=="], @@ -2001,6 +2015,8 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "path-to-regexp": ["path-to-regexp@1.9.0", "", { "dependencies": { "isarray": "0.0.1" } }, "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g=="], "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], @@ -2393,7 +2409,7 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "sirv": ["sirv@2.0.4", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ=="], @@ -2445,6 +2461,8 @@ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], @@ -2453,6 +2471,8 @@ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], @@ -2641,6 +2661,8 @@ "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "write-file-atomic": ["write-file-atomic@3.0.3", "", { "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", "signal-exit": "^3.0.2", "typedarray-to-buffer": "^3.1.5" } }, "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q=="], @@ -2709,6 +2731,8 @@ "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -2725,8 +2749,6 @@ "@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "@cornerstonejs/adapters/dcmjs": ["dcmjs@0.29.13", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevelnext": "^3.0.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-Bf9tKzJNWqk4kbV210N5TLEHDqaZvO3S+MH9vezFAU8WKcG4cR6z4/II3TQVqhLI185eNUL+lhfPCVH1Uu2yTA=="], - "@docsearch/react/algoliasearch": ["algoliasearch@5.23.4", "", { "dependencies": { "@algolia/client-abtesting": "5.23.4", "@algolia/client-analytics": "5.23.4", "@algolia/client-common": "5.23.4", "@algolia/client-insights": "5.23.4", "@algolia/client-personalization": "5.23.4", "@algolia/client-query-suggestions": "5.23.4", "@algolia/client-search": "5.23.4", "@algolia/ingestion": "1.23.4", "@algolia/monitoring": "1.23.4", "@algolia/recommend": "5.23.4", "@algolia/requester-browser-xhr": "5.23.4", "@algolia/requester-fetch": "5.23.4", "@algolia/requester-node-http": "5.23.4" } }, "sha512-QzAKFHl3fm53s44VHrTdEo0TkpL3XVUYQpnZy1r6/EHvMAyIg+O4hwprzlsNmcCHTNyVcF2S13DAUn7XhkC6qg=="], "@docusaurus/babel/@babel/runtime": ["@babel/runtime@7.27.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw=="], @@ -2745,6 +2767,10 @@ "@docusaurus/types/webpack-merge": ["webpack-merge@5.10.0", "", { "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", "wildcard": "^2.0.0" } }, "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA=="], + "@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=="], + + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "@kitware/vtk.js/commander": ["commander@9.2.0", "", {}, "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w=="], "@mdx-js/mdx/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], @@ -2775,6 +2801,10 @@ "boxen/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=="], + "cacache/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "cacache/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "cacache/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], "cacache/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], @@ -2805,6 +2835,8 @@ "copy-webpack-plugin/schema-utils": ["schema-utils@1.0.0", "", { "dependencies": { "ajv": "^6.1.0", "ajv-errors": "^1.0.0", "ajv-keywords": "^3.1.0" } }, "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g=="], + "copyfiles/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "crypto-random-string/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], @@ -2835,6 +2867,8 @@ "execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -2859,6 +2893,8 @@ "fork-ts-checker-webpack-plugin/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=="], + "fork-ts-checker-webpack-plugin/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "fork-ts-checker-webpack-plugin/schema-utils": ["schema-utils@2.7.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "ajv": "^6.12.2", "ajv-keywords": "^3.4.1" } }, "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A=="], "fork-ts-checker-webpack-plugin/tapable": ["tapable@1.1.3", "", {}, "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA=="], @@ -2867,6 +2903,8 @@ "fs-write-stream-atomic/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "global-dirs/ini": ["ini@2.0.0", "", {}, "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA=="], "globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -3089,6 +3127,8 @@ "renderkid/htmlparser2": ["htmlparser2@6.1.0", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", "domutils": "^2.5.2", "entities": "^2.0.0" } }, "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A=="], + "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], @@ -3107,6 +3147,8 @@ "shader-loader/loader-utils": ["loader-utils@1.4.2", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^1.0.1" } }, "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg=="], + "shelljs/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "sitemap/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], "sockjs/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], @@ -3175,6 +3217,8 @@ "wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "xmlbuilder2/js-yaml": ["js-yaml@3.14.0", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A=="], "@babel/core/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -3209,6 +3253,10 @@ "@docusaurus/theme-classic/react-router-dom/@babel/runtime": ["@babel/runtime@7.27.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "agent-base/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -3233,12 +3281,16 @@ "concat-stream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "copy-concurrently/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "copy-webpack-plugin/glob-parent/is-glob": ["is-glob@3.1.0", "", { "dependencies": { "is-extglob": "^2.1.0" } }, "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw=="], "copy-webpack-plugin/globby/array-union": ["array-union@1.0.2", "", { "dependencies": { "array-uniq": "^1.0.1" } }, "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng=="], "copy-webpack-plugin/globby/dir-glob": ["dir-glob@2.2.2", "", { "dependencies": { "path-type": "^3.0.0" } }, "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw=="], + "copy-webpack-plugin/globby/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "copy-webpack-plugin/globby/ignore": ["ignore@3.3.10", "", {}, "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug=="], "copy-webpack-plugin/globby/slash": ["slash@1.0.0", "", {}, "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg=="], @@ -3283,6 +3335,8 @@ "fs-write-stream-atomic/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "hpack.js/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], @@ -3301,10 +3355,14 @@ "mini-css-extract-plugin/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], + "move-concurrently/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "parallel-transform/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], "parallel-transform/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "patch-package/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], "react-dev-utils/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], diff --git a/packages/docs/docs/concepts/cornerstone-core/cahce.md b/packages/docs/docs/concepts/cornerstone-core/cahce.md index e8944376c4..a2a995920e 100644 --- a/packages/docs/docs/concepts/cornerstone-core/cahce.md +++ b/packages/docs/docs/concepts/cornerstone-core/cahce.md @@ -32,14 +32,13 @@ If you ever actually need the full voxel data of a volume, you can use the `Voxe This new change that was introduced in `Cornerstone3D` 2.x is part of the new image-based approach that aims to improve performance, reduce memory usage, and provide more efficient data access, especially for large datasets. Here are other benefits of the new approach: -1. Single Source of Truth +1. Single Source of Truth - Previously: Data existed in both image cache and volume cache, leading to synchronization issues. - Now: Only one source of truth - the image cache. - Benefits: Improved syncing between stack and volume segmentations. 2. New Volume Creation Approach - - Everything now loads as images. - Volume streaming is performed image by image. - Only images are cached in the image cache. @@ -47,7 +46,6 @@ Here are other benefits of the new approach: - Benefits: Eliminated need for scalar data in CPU, reduced memory usage, improved performance. 3. VoxelManager for Tools - - Acts as an intermediary between indexes and scalar data. - Provides mappers from IJK to indexes. - Retrieves information without creating scalar data. @@ -55,12 +53,10 @@ Here are other benefits of the new approach: - Benefits: Efficient handling of tools requiring pixel data in CPU. 4. Handling Non-Image Volumes - - Volumes without images (e.g., NIFTI) are chopped and converted to stack format. - Makes non-image volumes compatible with the new image-based approach. 5. Optimized Caching Mechanism - - Data stored in native format instead of always caching as float32. - On-the-fly conversion to required format when updating GPU textures. - Benefits: Reduced memory usage, eliminated unnecessary data type conversions. diff --git a/packages/docs/docs/concepts/cornerstone-core/renderingEngine.md b/packages/docs/docs/concepts/cornerstone-core/renderingEngine.md index 6fb38f46ed..3dfb346e66 100644 --- a/packages/docs/docs/concepts/cornerstone-core/renderingEngine.md +++ b/packages/docs/docs/concepts/cornerstone-core/renderingEngine.md @@ -30,6 +30,83 @@ canvas get updated, and at render time, we copy from offscreen to onscreen for e For instance for PET-CT fusion which has 3x3 layout which includes CT (Axial, Sagittal, Coronal), PET (Axial, Sagittal, Coronal) and Fusion (Axial, Sagittal, Coronal), we create two volume mappers for CT and PET individually, and for the Fusion viewports we re-use both created textures instead of re-creating a new one. +## Rendering Engine Implementations + +Cornerstone3D provides two rendering engine implementations to handle different use cases and overcome technical limitations: + +### TiledRenderingEngine + +The `TiledRenderingEngine` is the original implementation that uses a single, large offscreen canvas for all viewports. This approach: + +- Creates one massive offscreen canvas that grows horizontally as viewports are added +- Renders all viewports to specific coordinates on this single offscreen canvas +- Copies pixel data from the offscreen canvas to individual onscreen viewports + +**Limitations of TiledRenderingEngine:** + +- **Canvas Size Limits**: Browsers impose maximum canvas dimensions (e.g., 16,384px in Chrome). When the combined width of all viewports exceeds this limit, the offscreen canvas is silently cropped, causing severe visual artifacts, misaligned viewports, and blank viewports +- **Performance Degradation**: As the offscreen canvas approaches size limits, performance degrades significantly, especially on high-resolution displays or layouts with many viewports +- **Multi-Monitor Issues**: Practically impossible to use across multiple high-resolution monitors due to canvas size limitations +- **Memory Consumption**: Allocates a huge, memory-intensive offscreen canvas regardless of actual viewport usage + +**Advantages of TiledRenderingEngine:** + +- **Simplicity**: Straightforward implementation that works well for small numbers of viewports +- **Track Record**: Proven reliability for 5 years, and for most basic use cases, it performs adequately + +### ContextPoolRenderingEngine (SequentialRenderingEngine) + +The `ContextPoolRenderingEngine` (internally called `SequentialRenderingEngine`) fundamentally solves the limitations of the tiled approach by using a different rendering strategy: + +- Renders each viewport individually to a viewport-sized offscreen canvas +- Copies the result to the corresponding onscreen canvas +- Proceeds sequentially to the next viewport, reusing the same offscreen canvas +- Utilizes WebGL context pooling to render in batches (e.g., batches of 8 for 8 WebGL contexts) + +**Advantages of ContextPoolRenderingEngine:** + +- **No Canvas Size Limits**: The browser's maximum canvas size now applies to individual viewports, not the combined width +- **Improved Performance**: Consistent performance regardless of the number of viewports or display resolution +- **Better Memory Usage**: Avoids allocating massive offscreen canvases +- **Multi-Monitor Support**: Enables smooth performance across multiple high-resolution monitors +- **Enhanced Stability**: Reduces WebGL context loss associated with huge canvas surfaces + +### Configuring the Rendering Engine + +The `ContextPoolRenderingEngine` is now the default in Cornerstone3D. If you need to use the legacy `TiledRenderingEngine`, you can configure it during initialization: + +```js +import { init } from '@cornerstonejs/core'; + +// To use the legacy TiledRenderingEngine +init({ + rendering: { + renderingEngineMode: 'standard', + }, +}); + +// The ContextPoolRenderingEngine is used by default, or you can explicitly set it +init({ + rendering: { + renderingEngineMode: 'next', + }, +}); +``` + +For `ContextPoolRenderingEngine` you can also configure the number of WebGL contexts to use for batch rendering: + +```js +import { init } from '@cornerstonejs/core'; + +// To use the ContextPoolRenderingEngine with a specific number of WebGL contexts +init({ + rendering: { + renderingEngineMode: 'next', + webGLContextCount: 7, // Default is 7, can be adjusted based on your needs + }, +}); +``` + ## General usage After creating a renderingEngine, we can assign viewports to it for rendering. There are two main approach for creating `Stack` or `Volume` viewports which we will discuss now. diff --git a/packages/docs/docs/concepts/cornerstone-core/volumeLoader.md b/packages/docs/docs/concepts/cornerstone-core/volumeLoader.md index f6603895b0..73f8c39896 100644 --- a/packages/docs/docs/concepts/cornerstone-core/volumeLoader.md +++ b/packages/docs/docs/concepts/cornerstone-core/volumeLoader.md @@ -21,7 +21,6 @@ Below you can see a simplified code for our `cornerstoneStreamingImageVolumeLoad 1. Based on a set of imageIds, we compute volume metadata such as: spacing, origin, direction, etc. 2. Instantiate a new [`StreamingImageVolume`](/docs/api/core/classes/streamingimagevolume/) - - `StreamingImageVolume` implements methods for loading (`.load`) - It implements load via using `imageLoadPoolManager` - Each loaded frame (imageId) is put at the correct slice in the 3D volume diff --git a/packages/docs/docs/concepts/cornerstone-tools/annotation/annotationManager.md b/packages/docs/docs/concepts/cornerstone-tools/annotation/annotationManager.md index a8e6804a87..295a55a0b1 100644 --- a/packages/docs/docs/concepts/cornerstone-tools/annotation/annotationManager.md +++ b/packages/docs/docs/concepts/cornerstone-tools/annotation/annotationManager.md @@ -8,6 +8,7 @@ The Annotation Manager is a singleton class that manages annotations in Cornerst We use the Annotation Manager to store annotations, retrieve annotations, and save and restore annotations. ## Default Annotation Manager + The default Annotation Manager, `FrameOfReferenceSpecificAnnotationManager`, stores annotations based on the FrameOfReferenceUID. This means that annotations are stored separately for each FrameOfReferenceUID. @@ -16,11 +17,10 @@ FrameOfReferenceUID, they will share the same annotations. However, StackViewpor works on the per imageId basis, so annotations are not shared between StackViewports. ### GroupKey + Annotation groups are identified by a groupKey. The groupKey is a string that is used to identify the group of annotations. As mentioned above, the default Annotation Manager stores annotations based on the FrameOfReferenceUID, so the groupKey is the `FrameOfReferenceUID`. - - ## Custom Annotation Manager You can create your own custom Annotation Manager by implementing the `IAnnotationManager` interface: diff --git a/packages/docs/docs/concepts/cornerstone-tools/segmentation/active.md b/packages/docs/docs/concepts/cornerstone-tools/segmentation/active.md index d9e806e2de..7a4f66230f 100644 --- a/packages/docs/docs/concepts/cornerstone-tools/segmentation/active.md +++ b/packages/docs/docs/concepts/cornerstone-tools/segmentation/active.md @@ -4,22 +4,20 @@ title: Active Segmentation summary: Viewport-specific system for determining which segmentation is currently being edited, with distinct visual styling for active versus inactive segmentations --- - # Active Segmentation ![](../../../assets/active-segmentation.png) - Each viewport can display multiple segmentation representations simultaneously, but only one segmentation can be active per viewport. The active segmentation is the one that will be modified by segmentation tools. You can have different styles for active and inactive segmentations. For instance, you can configure different fill and outline properties for active versus inactive segmentations in each viewport. - As shown in the image above, you can display multiple labelmap segmentations in the same viewport. By default, active segmentations have a higher outline width to make them more visually distinct from inactive segmentations. ## Viewport-Specific Active Segmentations An important concept in version 2.x is that active segmentations are viewport-specific. This means: + - Each viewport can have its own active segmentation - The same segmentation can be active in one viewport and inactive in another - Segmentation tools will only modify the active segmentation in the viewport they're being used in @@ -44,7 +42,6 @@ Once you have the active segmentation, you can access various properties: ```js const activeSegmentation = segmentation.getActiveSegmentation(viewportId); - ``` ### Working with Multiple Viewports diff --git a/packages/docs/docs/concepts/cornerstone-tools/segmentation/config.md b/packages/docs/docs/concepts/cornerstone-tools/segmentation/config.md index da7fb2798e..ede5f5e182 100644 --- a/packages/docs/docs/concepts/cornerstone-tools/segmentation/config.md +++ b/packages/docs/docs/concepts/cornerstone-tools/segmentation/config.md @@ -4,7 +4,6 @@ title: Config summary: Multi-level styling system for segmentation visualizations, with a hierarchy that ranges from global settings to segment-specific properties like color, outline, and fill --- - # Configuration In version 2.x, segmentation configurations are managed through a unified style system that can be applied at different levels of specificity using a specifier object. @@ -12,6 +11,7 @@ In version 2.x, segmentation configurations are managed through a unified style ## Style System Styles can be applied at multiple levels: + - Global styles for all segmentations - Type-specific styles (e.g., all Labelmaps) - Viewport-specific styles @@ -53,10 +53,10 @@ import { segmentation } from '@cornerstonejs/tools'; // Get style for a specific context const style = segmentation.getStyle({ - viewportId: 'viewport1', // optional - segmentationId: 'segmentation1', // optional - type: Enums.SegmentationRepresentations.Labelmap, // required - segmentIndex: 1 // optional + viewportId: 'viewport1', // optional + segmentationId: 'segmentation1', // optional + type: Enums.SegmentationRepresentations.Labelmap, // required + segmentIndex: 1, // optional }); // Set style for a specific context @@ -64,12 +64,12 @@ segmentation.setStyle( { viewportId: 'viewport1', segmentationId: 'segmentation1', - type: Enums.SegmentationRepresentations.Labelmap + type: Enums.SegmentationRepresentations.Labelmap, }, { renderFill: true, renderOutline: true, - outlineWidth: 3 + outlineWidth: 3, } ); @@ -80,7 +80,7 @@ segmentation.resetToGlobalStyle(); const hasCustomStyle = segmentation.hasCustomStyle({ viewportId: 'viewport1', segmentationId: 'segmentation1', - type: Enums.SegmentationRepresentations.Labelmap + type: Enums.SegmentationRepresentations.Labelmap, }); ``` @@ -121,13 +121,14 @@ segmentation.setSegmentIndexColor( 'viewport1', 'segmentation1', segmentIndex, - [255, 0, 0, 255] // RGBA color + [255, 0, 0, 255] // RGBA color ); ``` ### Style Hierarchy Styles are applied in the following order of precedence (highest to lowest): + 1. Segment-specific style (when segmentIndex is provided) 2. Viewport-specific style (when viewportId is provided) 3. Segmentation-specific style (when segmentationId is provided) @@ -135,6 +136,7 @@ Styles are applied in the following order of precedence (highest to lowest): 5. Global style Example: + ```js // Set global style for all labelmaps segmentation.setStyle( @@ -146,7 +148,7 @@ segmentation.setStyle( segmentation.setStyle( { viewportId: 'viewport1', - type: Enums.SegmentationRepresentations.Labelmap + type: Enums.SegmentationRepresentations.Labelmap, }, { renderOutline: false } ); @@ -157,7 +159,7 @@ segmentation.setStyle( viewportId: 'viewport1', segmentationId: 'segmentation1', type: Enums.SegmentationRepresentations.Labelmap, - segmentIndex: 1 + segmentIndex: 1, }, { outlineWidth: 5 } ); diff --git a/packages/docs/docs/concepts/cornerstone-tools/segmentation/index.md b/packages/docs/docs/concepts/cornerstone-tools/segmentation/index.md index 42ee1aca76..ee51feb56b 100644 --- a/packages/docs/docs/concepts/cornerstone-tools/segmentation/index.md +++ b/packages/docs/docs/concepts/cornerstone-tools/segmentation/index.md @@ -23,8 +23,6 @@ Similar relationship structure has been adapted in popular medical imaging softw such as [3D Slicer](https://www.slicer.org/) with the addition of [polymorph segmentation](https://github.com/PerkLab/PolySeg). ::: - - ## API `Segmentation` related functions and classes are available in the `segmentation` module. diff --git a/packages/docs/docs/concepts/cornerstone-tools/segmentation/locking.md b/packages/docs/docs/concepts/cornerstone-tools/segmentation/locking.md index a352e19e56..119c2a4e8f 100644 --- a/packages/docs/docs/concepts/cornerstone-tools/segmentation/locking.md +++ b/packages/docs/docs/concepts/cornerstone-tools/segmentation/locking.md @@ -6,13 +6,12 @@ summary: Feature that prevents modification of specified segments, protecting co # Segment Locking - ![](../../../assets/segment-locking.png) - You can lock specific segments in a segmentation to prevent them from being modified by any tools. For example, consider the following image with an overlaid labelmap: + - Left image: shows `segment index 1` - Middle image: shows the result when `segment index 2` is drawn on top of `segment index 1` - Right image: shows the result when `segment index 1` is locked and `segment index 2` is drawn on top of `segment index 1` @@ -36,7 +35,8 @@ segmentation.locking.setSegmentIndexLocked( ); // Get all locked segment indices for a segmentation -const lockedIndices = segmentation.locking.getLockedSegmentIndices(segmentationId); +const lockedIndices = + segmentation.locking.getLockedSegmentIndices(segmentationId); // Check if a segment index is locked const isLocked = segmentation.locking.isSegmentIndexLocked( @@ -56,7 +56,8 @@ const isLocked = segmentation.locking.isSegmentIndexLocked('segmentation1', 1); console.log(`Segment 1 is ${isLocked ? 'locked' : 'unlocked'}`); // Get all locked segments -const lockedIndices = segmentation.locking.getLockedSegmentIndices('segmentation1'); +const lockedIndices = + segmentation.locking.getLockedSegmentIndices('segmentation1'); console.log('Locked segment indices:', lockedIndices); // Unlock segment 1 @@ -67,6 +68,7 @@ segmentation.locking.setSegmentIndexLocked('segmentation1', 1, false); 1. Renamed `getLockedSegments` to `getLockedSegmentIndices` for clarity 2. The locked state is now stored in the segment data structure: + ```js { segments: { diff --git a/packages/docs/docs/concepts/cornerstone-tools/segmentation/state.md b/packages/docs/docs/concepts/cornerstone-tools/segmentation/state.md index 9ace1a170d..4b33e47890 100644 --- a/packages/docs/docs/concepts/cornerstone-tools/segmentation/state.md +++ b/packages/docs/docs/concepts/cornerstone-tools/segmentation/state.md @@ -70,10 +70,10 @@ segmentation.addSegmentations([ representation: { type: Enums.SegmentationRepresentations.Labelmap, data: { - imageIds: segmentationImageIds - } - } - } + imageIds: segmentationImageIds, + }, + }, + }, ]); ``` @@ -93,12 +93,11 @@ import { segmentation, Enums } from '@cornerstonejs/tools'; await segmentation.addSegmentationRepresentations(viewportId, [ { segmentationId, - type: Enums.SegmentationRepresentations.Labelmap - } + type: Enums.SegmentationRepresentations.Labelmap, + }, ]); ``` - ### Representation-Specific Methods Cornerstone3D v2 provides dedicated methods for adding different types of segmentation representations: @@ -137,15 +136,15 @@ const viewportInputMap = { viewport1: [ { segmentationId: 'seg1', - type: Enums.SegmentationRepresentations.Labelmap - } + type: Enums.SegmentationRepresentations.Labelmap, + }, ], viewport2: [ { segmentationId: 'seg1', - type: Enums.SegmentationRepresentations.Labelmap - } - ] + type: Enums.SegmentationRepresentations.Labelmap, + }, + ], }; await segmentation.addLabelmapRepresentationToViewportMap(viewportInputMap); diff --git a/packages/docs/docs/concepts/streaming-image-volume/index.md b/packages/docs/docs/concepts/streaming-image-volume/index.md index 3e72b63128..bcb33369df 100644 --- a/packages/docs/docs/concepts/streaming-image-volume/index.md +++ b/packages/docs/docs/concepts/streaming-image-volume/index.md @@ -4,7 +4,6 @@ title: Streaming ImageVolume summary: Introduction to the volume loader that implements progressive loading of volume data to the GPU for efficient rendering --- - # Streaming ImageVolume We have added a new volume loader which implements a progressive loading of volumes to the GPU. You can read diff --git a/packages/docs/docs/concepts/streaming-image-volume/streaming.md b/packages/docs/docs/concepts/streaming-image-volume/streaming.md index 0d226c6258..0330e4d6fd 100644 --- a/packages/docs/docs/concepts/streaming-image-volume/streaming.md +++ b/packages/docs/docs/concepts/streaming-image-volume/streaming.md @@ -17,10 +17,8 @@ Therefore, an initial call to fetch images metadata is required for this loader. not only we can pre-allocate and cache a `Volume` in memory, but we also can render the volume as the 2D images are being loaded (progressive loading). - ![](../../assets/volume-building.png) - By pre-fetching the metadata from all images (`imageIds`), we don't need to create the [`Image`](../cornerstone-core/images.md) object for each imageId. Instead, we can just insert the pixelData of the image is directly inserted into the volume @@ -57,7 +55,6 @@ Since the volume loader does not need to create the [`Image`](../cornerstone-cor the `StreamingImageVolume`, it will use the `skipCreateImage` option internally to skip the creation of the image object. Otherwise, the volume's image loader is the same as wadors image loader written in `cornerstone-wado-image-loader`. - ```js const imageIds = ['wadors:imageId1', 'wadors:imageId2']; diff --git a/packages/docs/docs/help.md b/packages/docs/docs/help.md index 885c590545..2e2cfbf964 100644 --- a/packages/docs/docs/help.md +++ b/packages/docs/docs/help.md @@ -24,5 +24,4 @@ resources and must be judicious with how we allocate them. If you find yourself in this situation and in need of assistance, it may be in your best interest to persue paid support. - [gh-issues]: https://github.com/cornerstonejs/cornerstone3D/issues/ diff --git a/packages/docs/docs/migration-guides/2x/2-streaming-loader.md b/packages/docs/docs/migration-guides/2x/2-streaming-loader.md index c9e6dce079..9864dd424d 100644 --- a/packages/docs/docs/migration-guides/2x/2-streaming-loader.md +++ b/packages/docs/docs/migration-guides/2x/2-streaming-loader.md @@ -7,7 +7,6 @@ summary: Migration guide for changes to the streaming-image-volume-loader when u import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; - # @cornerstonejs/streaming-image-volume-loader After years of development on Cornerstone3D, we recognized that volume loading should be treated as a first-class feature rather than a separate library. As a result, we have merged all functionality related to streaming image loading into the core library. diff --git a/packages/docs/docs/migration-guides/2x/3-core.md b/packages/docs/docs/migration-guides/2x/3-core.md index af239c5ba1..386fc4cafe 100644 --- a/packages/docs/docs/migration-guides/2x/3-core.md +++ b/packages/docs/docs/migration-guides/2x/3-core.md @@ -7,8 +7,6 @@ summary: Changes to the core package when migrating from Cornerstone3D 1.x to 2. import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; - - # @cornerstonejs/core ## Initialization @@ -226,13 +224,11 @@ It is more accurate to use `getViewReferenceId` to reflect the actual function o The Cornerstone library has undergone significant changes in how it handles image volumes and texture management. These changes aim to improve performance, reduce memory usage, and provide more efficient data access, especially for large datasets. 1. Single Source of Truth - - Previously: Data existed in both image cache and volume cache, leading to synchronization issues. - Now: Only one source of truth - the image cache. - Benefits: Improved syncing between stack and volume segmentations. 2. New Volume Creation Approach - - Everything now loads as images. - Volume streaming is performed image by image. - Only images are cached in the image cache. @@ -240,7 +236,6 @@ The Cornerstone library has undergone significant changes in how it handles imag - Benefits: Eliminated need for scalar data in CPU, reduced memory usage, improved performance. 3. VoxelManager for Tools - - Acts as an intermediary between indexes and scalar data. - Provides mappers from IJK to indexes. - Retrieves information without creating scalar data. @@ -248,12 +243,10 @@ The Cornerstone library has undergone significant changes in how it handles imag - Benefits: Efficient handling of tools requiring pixel data in CPU. 4. Handling Non-Image Volumes - - Volumes without images (e.g., NIFTI) are chopped and converted to stack format. - Makes non-image volumes compatible with the new image-based approach. 5. Optimized Caching Mechanism - - Data stored in native format instead of always caching as float32. - On-the-fly conversion to required format when updating GPU textures. - Benefits: Reduced memory usage, eliminated unnecessary data type conversions. @@ -292,7 +285,6 @@ A new `VoxelManager` class has been introduced to handle voxel data more efficie b. For 3D coordinates, use `getAtIJK(i, j, k)` and `setAtIJK(i, j, k, value)`. 4. Available VoxelManager Methods: - - `getScalarData()`: Returns the entire scalar data array (only for IImage, not for volumes). - `getScalarDataLength()`: Returns the total number of voxels. - `getAtIndex(index)`: Gets the value at a specific index. @@ -327,7 +319,6 @@ A new `VoxelManager` class has been introduced to handle voxel data more efficie ``` 7. Getting volume information: - - Dimensions: `volume.dimensions` - Spacing: `volume.spacing` - Direction: `volume.direction` @@ -338,14 +329,12 @@ A new `VoxelManager` class has been introduced to handle voxel data more efficie If dealing with RGB data, the `getAtIndex` and `getAtIJK` methods will return an array `[r, g, b]`. 9. Performance considerations: - - Use `getAtIndex` and `setAtIndex` for bulk operations when possible, as they're generally faster than `getAtIJK` and `setAtIJK`. - When iterating over a large portion of the volume, consider using `forEach` for optimized performance. 10. Dynamic Volumes: For 4D datasets, additional methods are available: - - `setTimePoint(timePoint)`: Sets the current time point. - `getAtIndexAndTimePoint(index, timePoint)`: Gets a value for a specific index and time point. diff --git a/packages/docs/docs/migration-guides/2x/5-tools.md b/packages/docs/docs/migration-guides/2x/5-tools.md index b9f6c84698..dfdf4d4400 100644 --- a/packages/docs/docs/migration-guides/2x/5-tools.md +++ b/packages/docs/docs/migration-guides/2x/5-tools.md @@ -7,7 +7,6 @@ summary: Updates to the tools package when migrating from Cornerstone3D 1.x to 2 import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; - # @cornerstonejs/tools ## triggerAnnotationRenderForViewportIds @@ -94,25 +93,21 @@ We have a new segmentation model that is more flexible and easier to use. In Cornerstone3D version 2, we've made significant architectural changes to our segmentation model while maintaining familiar terminology. This redesign aims to provide a more flexible and intuitive approach to working with segmentations across different viewports. Here are the key changes and the reasons behind them: 1. **Viewport-Specific, Not Tool Group-Based**: - - Old: Segmentations were tied to tool groups, which typically consist of multiple viewports. This created complications when users wanted to add segmentations to some viewports but not others within the same tool group. - New: Segmentations are now viewport-specific. Instead of adding or removing representations to a tool group, users can add them directly to viewports. This provides much finer control over what each viewport renders. - Why: We discovered that tying rendering to a tool group is not an effective approach. It often necessitated creating an extra tool group for a specific viewport to customize or prevent rendering. 2. **Simplified Identification of Segmentation Representations**: - - Old: Required a unique segmentationRepresentationUID for identification. - New: Segmentation representations are identified by a combination of `segmentationId` and representation `type`. This allows each viewport to have different representations of the same segmentation. - Why: This simplification makes it easier to manage and reference segmentation representations across different viewports. 3. **Decoupling of Data and Visualization**: - - Old: Segmentation rendering was tightly coupled with tool groups. - New: Segmentation is now treated purely as data, separate from the tools used to interact with it. - Why: While it's appropriate for tools to be bound to tool groups, viewport-specific functionalities like segmentation rendering should be the responsibility of individual viewports. This separation allows for more flexible rendering and interaction options across different viewports. 4. **Polymorphic Segmentation Support**: - - The new architecture better supports the concept of polymorphic segmentations, where a single segmentation can have multiple representations (e.g., labelmap, contour, surface) that can be efficiently converted between each other. - Why: This flexibility allows for more efficient storage, analysis, and real-time visualization of segmentations. @@ -356,7 +351,6 @@ The previous model required users to provide an imageIdReferenceMap, which linke 1. Manual creation of the map was error-prone, particularly regarding the order of imageIds. 2. Once a segmentation was associated with specific viewport imageIds, rendering it elsewhere became problematic. For example: - - Rendering a CT image stack segmentation on a single key image. - Rendering a CT image stack segmentation on a stack that includes both CT and other images. - Rendering a DX dual energy segmentation from energy 1 on energy 2. diff --git a/packages/docs/docs/migration-guides/2x/6-dicom-image-loader.md b/packages/docs/docs/migration-guides/2x/6-dicom-image-loader.md index ae4d8c7004..6fe9b46acb 100644 --- a/packages/docs/docs/migration-guides/2x/6-dicom-image-loader.md +++ b/packages/docs/docs/migration-guides/2x/6-dicom-image-loader.md @@ -4,12 +4,9 @@ title: '@cornerstonejs/dicom-image-loader' summary: Changes to the DICOM image loader package when upgrading from Cornerstone3D 1.x to 2.x --- - import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; - - # @cornerstonejs/dicom-image-loader ## Initialization and Configuration diff --git a/packages/docs/docs/migration-guides/2x/7-nifti-volume-loader.md b/packages/docs/docs/migration-guides/2x/7-nifti-volume-loader.md index ee5de8e853..f612013a73 100644 --- a/packages/docs/docs/migration-guides/2x/7-nifti-volume-loader.md +++ b/packages/docs/docs/migration-guides/2x/7-nifti-volume-loader.md @@ -4,12 +4,9 @@ title: '@cornerstonejs/nifti-volume-loader' summary: Migration guide for the NIFTI volume loader when updating from Cornerstone3D 1.x to 2.x --- - import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; - - # `@cornerstonejs/nifti-image-volume-loader` After migrating to the new pixel data model for volumes, we have also updated the Nifti image volume loader to align with this model. diff --git a/packages/docs/docs/migration-guides/2x/8-deverloper-experience.md b/packages/docs/docs/migration-guides/2x/8-deverloper-experience.md index 890dd373b5..77e28ac9de 100644 --- a/packages/docs/docs/migration-guides/2x/8-deverloper-experience.md +++ b/packages/docs/docs/migration-guides/2x/8-deverloper-experience.md @@ -7,14 +7,12 @@ summary: Improvements to the developer experience when migrating from Cornerston import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; - # Developer Experience ### Dependency Cycles We have removed all dependency cycles in the library, ensuring it is now free of any such issues. To maintain this, we have added rules in our linters that will catch any dependency cycles in pull requests during continuous integration. Additionally, you can run `yarn run format-check` to ensure that the formatting is correct and to check for dependencies as well. - ### Karma tests There has been a lot of work to clean up tests let's dive in diff --git a/packages/docs/docs/migration-guides/3x/2-threshold-tools.md b/packages/docs/docs/migration-guides/3x/2-threshold-tools.md index d2b1b27f91..5845ba4f14 100644 --- a/packages/docs/docs/migration-guides/3x/2-threshold-tools.md +++ b/packages/docs/docs/migration-guides/3x/2-threshold-tools.md @@ -9,20 +9,21 @@ import TabItem from '@theme/TabItem'; ## Key Changes: -* The nested `strategySpecificConfiguration` object has been removed completely -* Configuration properties have been moved to the root level of the configuration object -* Threshold configuration has been restructured: - * `threshold` array is now a `range` property inside a `threshold` object - * Additional threshold properties (`isDynamic`, `dynamicRadius`) are part of the same object -* `setBrushThresholdForToolGroup()` function signature has changed to accept a structured threshold object -* Strategy-specific properties like `useCenterSegmentIndex` have been moved to the root configuration level -* `activeStrategy` is now a standalone property in tool operations data, no longer inside a nested configuration +- The nested `strategySpecificConfiguration` object has been removed completely +- Configuration properties have been moved to the root level of the configuration object +- Threshold configuration has been restructured: + - `threshold` array is now a `range` property inside a `threshold` object + - Additional threshold properties (`isDynamic`, `dynamicRadius`) are part of the same object +- `setBrushThresholdForToolGroup()` function signature has changed to accept a structured threshold object +- Strategy-specific properties like `useCenterSegmentIndex` have been moved to the root configuration level +- `activeStrategy` is now a standalone property in tool operations data, no longer inside a nested configuration ## Migration Steps: ### 1. Replace strategySpecificConfiguration with direct properties **Before:** + ```diff - configuration: { - activeStrategy: 'THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL', @@ -37,6 +38,7 @@ import TabItem from '@theme/TabItem'; ``` **After:** + ```diff + configuration: { + activeStrategy: 'THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL', @@ -52,6 +54,7 @@ import TabItem from '@theme/TabItem'; ### 2. Update threshold configuration structure **Before:** + ```diff - strategySpecificConfiguration: { - THRESHOLD: { @@ -63,6 +66,7 @@ import TabItem from '@theme/TabItem'; ``` **After:** + ```diff + threshold: { + range: [-150, -70], // New 'range' property replaces 'threshold' @@ -74,6 +78,7 @@ import TabItem from '@theme/TabItem'; ### 3. Update setBrushThresholdForToolGroup calls **Before:** + ```diff - segmentationUtils.setBrushThresholdForToolGroup( - toolGroupId, @@ -83,6 +88,7 @@ import TabItem from '@theme/TabItem'; ``` **After:** + ```diff + segmentationUtils.setBrushThresholdForToolGroup( + toolGroupId, @@ -91,6 +97,7 @@ import TabItem from '@theme/TabItem'; ``` Note that `thresholdArgs` should now be an object with the structure: + ```javascript { range: [min, max], // Previously 'threshold' diff --git a/packages/docs/docs/migration-guides/3x/4-get-statistics.md b/packages/docs/docs/migration-guides/3x/4-get-statistics.md index e0bf7b8154..a5a788a006 100644 --- a/packages/docs/docs/migration-guides/3x/4-get-statistics.md +++ b/packages/docs/docs/migration-guides/3x/4-get-statistics.md @@ -7,19 +7,19 @@ summary: Updates to the segmentation statistics API when migrating to Cornerston import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; - ## Key Changes: -* Statistics calculation has been moved from brush tool methods to a dedicated utility function -* Statistics are now calculated asynchronously using web workers -* The function signature for getting statistics has changed completely -* Progress events are now emitted during statistics calculation +- Statistics calculation has been moved from brush tool methods to a dedicated utility function +- Statistics are now calculated asynchronously using web workers +- The function signature for getting statistics has changed completely +- Progress events are now emitted during statistics calculation ## Migration Steps: ### 1. Replace tool-based statistics methods with the standalone utility **Before:** + ```diff - const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); - const activeName = toolGroup.getActivePrimaryMouseButtonTool(); @@ -28,6 +28,7 @@ import TabItem from '@theme/TabItem'; ``` **After:** + ```diff + const stats = await segmentationUtils.getStatistics({ + segmentationId, diff --git a/packages/docs/docs/migration-guides/3x/5-adapters.md b/packages/docs/docs/migration-guides/3x/5-adapters.md index 4994fcc58e..373dd06598 100644 --- a/packages/docs/docs/migration-guides/3x/5-adapters.md +++ b/packages/docs/docs/migration-guides/3x/5-adapters.md @@ -7,35 +7,39 @@ summary: Guide for using the new adapters API in Cornerstone3D 3.x import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; - ## Key Changes: -* MeasurementsReport has two maps instead of objects for setting the +- MeasurementsReport has two maps instead of objects for setting the adapter classes, mapping the tool type to adapter class and the tracking id to adapter class. -* A new register additional tracking id method exists to allow adding +- A new register additional tracking id method exists to allow adding custom adapter methods. -* Adapter implementations now have a base class to handle some of the - definition. This allows calling into the base class to handle some of the +- Adapter implementations now have a base class to handle some of the + definition. This allows calling into the base class to handle some of the definition such as the is tracking handling. -* The MeasurementsReport class is now extensible to create a new class with - completely different default handling. To do this, the two map attributes +- The MeasurementsReport class is now extensible to create a new class with + completely different default handling. To do this, the two map attributes need to be redeclared, and the new instance registered for the handlers. -* There is now an init method to create tracking identifiers and register a new +- There is now an init method to create tracking identifiers and register a new handler. -* The annotation changed event no longer requires the viewport id/rendering id - * This change is done so that measurements can be updated when not visible +- The annotation changed event no longer requires the viewport id/rendering id + - This change is done so that measurements can be updated when not visible +- The measurement report no longer takes the image to/from world coords as this + is provided as a method exported from `@cornerstonejs/core/utilities` +- The adapters can hydrate world coordinates, eg for MPR reconstruction ## Migration Steps: ### 1. Replace MeasurementsReports.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE **Before:** + ```diff - const toolClass = MeasurementReports.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE[toolType]; ``` **After:** + ```diff - const toolClass = MeasurementReports.measurementAdapterByToolType.get(toolType); ``` @@ -43,11 +47,13 @@ import TabItem from '@theme/TabItem'; ### 2. Replace Tool instance adapter registration which is identical to existing registration **Before:** + ```diff - class MyNewToolAdapter { ... identical to eg Probe Adapter } ``` **After:** + ```diff - const MyNewToolAdapter = Probe.initCopy('MyNewTool'); ``` @@ -55,11 +61,46 @@ import TabItem from '@theme/TabItem'; ### 3. Replace old tool registration with registerTrackingIdentifier **Before:** + ```diff - class OldToolAdapter { ... identical to eg Length v1.0 except has :v1.0 at end of tracking identifier } ``` **After:** + ```diff - MeasurementReport.registerTrackingIdentifier(Length, `${Length.trackingIdentifierTextValue}:v1.0`); ``` + +### 4. Remove image to/from world coords in use of MeasurementReport + +**Before:** + +``` + // Use cs3d adapters to generate toolState. + let storedMeasurementByAnnotationType = MeasurementReport.generateToolState( + datasetToUse, + // NOTE: we need to pass in the imageIds to dcmjs since the we use them + // for the imageToWorld transformation. The following assumes that the order + // that measurements were added to the display set are the same order as + // the measurementGroups in the instance. + sopInstanceUIDToImageId, + metaData, + csUtilities.imageToWorldCoords + ); +``` + +**After:** + +``` + // Use cs3d adapters to generate toolState. + let storedMeasurementByAnnotationType = MeasurementReport.generateToolState( + datasetToUse, + // NOTE: we need to pass in the imageIds to dcmjs since the we use them + // for the imageToWorld transformation. The following assumes that the order + // that measurements were added to the display set are the same order as + // the measurementGroups in the instance. + sopInstanceUIDToImageId, + metaData + ); +``` diff --git a/packages/docs/docs/migration-guides/legacy-to-3d.md b/packages/docs/docs/migration-guides/legacy-to-3d.md index e967e47ca4..a780ddfb8e 100644 --- a/packages/docs/docs/migration-guides/legacy-to-3d.md +++ b/packages/docs/docs/migration-guides/legacy-to-3d.md @@ -269,7 +269,7 @@ renderingEngine.destroy(); // The ELEMENT_DISABLED event contains just a reference to the canvas element which is now disabled, and related IDs. eventDetail: { - viewportId, renderingEngineId, canvas; + (viewportId, renderingEngineId, canvas); } ``` @@ -409,12 +409,12 @@ viewport: { ```js camera: { - viewUp, + (viewUp, viewPlaneNormal, position, focalPoint, orthogonalOrPerspective, - viewAngle; + viewAngle); } ``` @@ -445,7 +445,7 @@ startPoints / lastPoints / currentPoints / deltaPoints: { ```js // Location in 3D in world space { - CanvasCoord, WorldCoord; + (CanvasCoord, WorldCoord); } ``` diff --git a/packages/docs/docs/test-coverage.md b/packages/docs/docs/test-coverage.md index eb27e48bc2..6f95153e20 100644 --- a/packages/docs/docs/test-coverage.md +++ b/packages/docs/docs/test-coverage.md @@ -10,9 +10,8 @@ summary: Information about Cornerstone3D's test coverage using Playwright, with ## Playwright - Here's the test coverage report for our Playwright tests. Keep in mind that this doesn't include our Karma tests, so our actual test coverage is likely higher than what's shown. We're focusing on Playwright for future Cornerstone3D testing, and we're really pushing to improve that coverage number. - You can view our latest test coverage report here: + - [Cornerstone3D Playwright Test Coverage Report](https://www.cornerstonejs.org/coverage) diff --git a/packages/docs/docusaurus.config.js b/packages/docs/docusaurus.config.js index 744f789db8..e6184e74b0 100644 --- a/packages/docs/docusaurus.config.js +++ b/packages/docs/docusaurus.config.js @@ -254,6 +254,8 @@ module.exports = { out: `./docs/api/${pkg}`, entryPoints: [`../${pkg}/src/index.ts`], tsconfig: `../${pkg}/tsconfig.json`, + exclude: [`../${pkg}/test/**/*`, `../${pkg}/jest.config.js`], + skipErrorChecking: true, }, ]); } diff --git a/packages/docs/package.json b/packages/docs/package.json index 7bc0f007d5..4f1dffa5d7 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -7,12 +7,13 @@ "preinstall": "node preinstall.js", "build": "echo 'build'", "build:docs": "cd ../../ && yarn install && yarn run build:esm && cd packages/docs && cross-env NODE_OPTIONS=--max_old_space_size=32384 yarn run prep-examples && docusaurus build && node ./scripts/prepare-markdown-files.js && node ./scripts/generate-llms-txt.js && node ./scripts/generate-llms-full-txt.js && node ./scripts/generate-redirects.js", + "build:ci": "cross-env NODE_OPTIONS=--max_old_space_size=32384 yarn run prep-examples && docusaurus build && node ./scripts/prepare-markdown-files.js && node ./scripts/generate-llms-txt.js && node ./scripts/generate-llms-full-txt.js && node ./scripts/generate-redirects.js", "prepare-markdown-files": "node ./scripts/prepare-markdown-files.js", "generate-llms-txt": "node ./scripts/generate-llms-txt.js", "generate-llms-full-txt": "node ./scripts/generate-llms-full-txt.js", "generate-redirects": "node ./scripts/generate-redirects.js", "copy-coverage-to-docs": "cp -R ../../coverage/*Chrome*/. ./static/test-coverage/", - "copy-examples-to-docs": "cp -R ../../.static-examples/. ./static/live-examples/ && cp -R ../../node_modules/dicom-microscopy-viewer/dist/dynamic-import/dicom-microscopy-viewer ./static/dicom-microscopy-viewer", + "copy-examples-to-docs": "cp -R ../../.static-examples/. ./static/live-examples/ && cp -R ../../node_modules/dicom-microscopy-viewer/dist/dynamic-import ./static/dicom-microscopy-viewer", "docusaurus": "docusaurus", "docusaurus:build": "docusaurus build", "docs": "cross-env NODE_OPTIONS=--max_old_space_size=16384 docusaurus start --port 3333", @@ -52,7 +53,7 @@ "@svgr/webpack": "^8.1.0", "clsx": "^1.1.1", "cross-env": "^7.0.3", - "dcmjs": "^0.42.0", + "dcmjs": "^0.43.1", "dicom-parser": "^1.8.21", "dicomweb-client": "0.10.4", "docusaurus-plugin-copy": "0.1.1", diff --git a/packages/docs/scripts/README.md b/packages/docs/scripts/README.md index aceac4b0e6..9f04f45acf 100644 --- a/packages/docs/scripts/README.md +++ b/packages/docs/scripts/README.md @@ -88,6 +88,7 @@ The script is automatically run as part of the `build:docs` command (after `gene 5. Save the file to the build directory root so it will be accessible at the root of the website The output file has the following structure: + - Title and introduction for Cornerstone3D - Files are organized by their directory structure (e.g., concepts, tutorials) - Each file has a header with its title and source URL diff --git a/packages/docs/scripts/generate-llms-full-txt.js b/packages/docs/scripts/generate-llms-full-txt.js index 8b2010e678..8a5643644f 100644 --- a/packages/docs/scripts/generate-llms-full-txt.js +++ b/packages/docs/scripts/generate-llms-full-txt.js @@ -18,15 +18,15 @@ const outputFilePath = path.join(__dirname, '../build/llms-full.txt'); // Get all subdirectories to create sections async function getDirectorySections() { const directories = new Set(); - + // Find all subdirectories that contain markdown files const markdownFiles = await glob(`${llmDir}/**/*.md`); - + markdownFiles.forEach((filePath) => { // Get the relative directory from the llm dir const relativePath = path.relative(llmDir, filePath); const dirPath = path.dirname(relativePath); - + // Add to set if it's not in the root if (dirPath !== '.') { // Get the top-level directory @@ -34,7 +34,7 @@ async function getDirectorySections() { directories.add(topDir); } }); - + return Array.from(directories).sort(); } @@ -42,21 +42,21 @@ async function getDirectorySections() { async function getSubdirectories(section) { const sectionPath = path.join(llmDir, section); const subdirectories = new Set(); - + // Find all markdown files in this section const markdownFiles = await glob(`${sectionPath}/**/*.md`); - + markdownFiles.forEach((filePath) => { // Get the relative directory from the section path const relativePath = path.relative(sectionPath, filePath); const dirPath = path.dirname(relativePath); - + // Add to set if it's not in the root if (dirPath !== '.') { subdirectories.add(dirPath); } }); - + return Array.from(subdirectories).sort(); } @@ -64,15 +64,18 @@ async function getSubdirectories(section) { function adjustHeadingLevels(content, baseLevel) { // Replace headings with adjusted levels while limiting max depth to h4 let processedContent = content; - + // Process all heading levels from h1 to h6 for (let i = 1; i <= 6; i++) { // Limit maximum heading level to h4 const newLevel = Math.min(i + baseLevel, 4); const pattern = new RegExp(`^(#{${i}})\\s+(.+)$`, 'gm'); - processedContent = processedContent.replace(pattern, `${'#'.repeat(newLevel)} $2`); + processedContent = processedContent.replace( + pattern, + `${'#'.repeat(newLevel)} $2` + ); } - + return processedContent; } @@ -80,17 +83,17 @@ function adjustHeadingLevels(content, baseLevel) { function processMarkdownFile(filePath, baseLevel) { // Read the file content const content = fs.readFileSync(filePath, 'utf8'); - + // Process content to remove frontmatter let processedContent = content; const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/); if (frontmatterMatch) { processedContent = content.replace(/^---\n[\s\S]*?\n---\n/, ''); } - + // Adjust heading levels processedContent = adjustHeadingLevels(processedContent, baseLevel); - + return processedContent; } @@ -98,24 +101,24 @@ function processMarkdownFile(filePath, baseLevel) { async function processSubsectionFiles(section, subsection) { const subsectionPath = path.join(llmDir, section, subsection); const subsectionContent = []; - + // Get all markdown files for this subsection const markdownFiles = await glob(`${subsectionPath}/*.md`); - + // Sort files alphabetically markdownFiles.sort(); - + for (const filePath of markdownFiles) { // Get the file name const fileName = path.basename(filePath, '.md'); const relativePath = path.relative(llmDir, filePath); - + // Extract title from frontmatter or filename const content = fs.readFileSync(filePath, 'utf8'); let title = ''; const frontmatterTitleMatch = content.match(/title:\s*([^\n]+)/); const firstHeadingMatch = content.match(/# ([^\n]+)/); - + if (frontmatterTitleMatch) { title = frontmatterTitleMatch[1].trim(); // Remove quotes if present @@ -126,19 +129,21 @@ async function processSubsectionFiles(section, subsection) { // Use filename as fallback title = fileName; } - + // Add file header (as h3, since section is h1 and subsection is h2) subsectionContent.push(`\n\n### ${title}\n`); - subsectionContent.push(`Source: https://cornerstonejs.org/docs/llm/${relativePath}\n`); - + subsectionContent.push( + `Source: https://cornerstonejs.org/docs/llm/${relativePath}\n` + ); + // Process the file content with adjusted heading levels (h1->h4, h2->h5, etc.) const processedContent = processMarkdownFile(filePath, 3); subsectionContent.push(processedContent); - + // Add separator subsectionContent.push('\n\n---\n'); } - + return subsectionContent.join('\n'); } @@ -146,24 +151,24 @@ async function processSubsectionFiles(section, subsection) { async function processSectionRootFiles(section) { const sectionPath = path.join(llmDir, section); const sectionContent = []; - + // Get all markdown files directly in the section root const markdownFiles = await glob(`${sectionPath}/*.md`); - + // Sort files alphabetically markdownFiles.sort(); - + for (const filePath of markdownFiles) { // Get the file name const fileName = path.basename(filePath, '.md'); const relativePath = path.relative(llmDir, filePath); - + // Extract title from frontmatter or filename const content = fs.readFileSync(filePath, 'utf8'); let title = ''; const frontmatterTitleMatch = content.match(/title:\s*([^\n]+)/); const firstHeadingMatch = content.match(/# ([^\n]+)/); - + if (frontmatterTitleMatch) { title = frontmatterTitleMatch[1].trim(); // Remove quotes if present @@ -174,42 +179,44 @@ async function processSectionRootFiles(section) { // Use filename as fallback title = fileName; } - + // Add file header (as h2, since section is h1) sectionContent.push(`\n\n## ${title}\n`); - sectionContent.push(`Source: https://cornerstonejs.org/docs/llm/${relativePath}\n`); - + sectionContent.push( + `Source: https://cornerstonejs.org/docs/llm/${relativePath}\n` + ); + // Process the file content with adjusted heading levels (h1->h3, h2->h4, etc.) const processedContent = processMarkdownFile(filePath, 2); sectionContent.push(processedContent); - + // Add separator sectionContent.push('\n\n---\n'); } - + return sectionContent.join('\n'); } // Process root files async function processRootFiles() { const rootContent = []; - + // Get all markdown files in the root directory const rootFiles = await glob(`${llmDir}/*.md`); - + // Sort files alphabetically rootFiles.sort(); - + for (const filePath of rootFiles) { // Get the file name const fileName = path.basename(filePath, '.md'); - + // Extract title from frontmatter or filename const content = fs.readFileSync(filePath, 'utf8'); let title = ''; const frontmatterTitleMatch = content.match(/title:\s*([^\n]+)/); const firstHeadingMatch = content.match(/# ([^\n]+)/); - + if (frontmatterTitleMatch) { title = frontmatterTitleMatch[1].trim(); // Remove quotes if present @@ -220,72 +227,83 @@ async function processRootFiles() { // Use filename as fallback title = fileName; } - + // Add file header (as h2, since root is h1) rootContent.push(`\n\n## ${title}\n`); - rootContent.push(`Source: https://cornerstonejs.org/docs/llm/${fileName}\n`); - + rootContent.push( + `Source: https://cornerstonejs.org/docs/llm/${fileName}\n` + ); + // Process the file content with adjusted heading levels (h1->h3, h2->h4, etc.) const processedContent = processMarkdownFile(filePath, 2); rootContent.push(processedContent); - + // Add separator rootContent.push('\n\n---\n'); } - + return rootContent.join('\n'); } // Generate the full concatenated content async function generateLlmsFullTxt() { let content = ''; - + // Add title and introduction content += '# Cornerstone3D Documentation\n\n'; - - content += '> Cornerstone3D is a modern, high-performance JavaScript library for medical imaging, designed for building web-based medical imaging applications. It provides tools for rendering, manipulating, and analyzing medical images in various formats including DICOM.\n\n'; - - content += 'This file contains the complete documentation for Cornerstone3D, concatenated for easy reference and searching. Each section is clearly marked with its source URL.\n\n'; - + + content += + '> Cornerstone3D is a modern, high-performance JavaScript library for medical imaging, designed for building web-based medical imaging applications. It provides tools for rendering, manipulating, and analyzing medical images in various formats including DICOM.\n\n'; + + content += + 'This file contains the complete documentation for Cornerstone3D, concatenated for easy reference and searching. Each section is clearly marked with its source URL.\n\n'; + // Process root files first (if any) const rootFiles = await glob(`${llmDir}/*.md`); if (rootFiles.length > 0) { content += '# Root Documentation\n\n'; - + // Process root files const rootContent = await processRootFiles(); content += rootContent; } - + // Get all sections const sections = await getDirectorySections(); - + // Process each section for (const section of sections) { // Add section header (as h1) - content += `\n\n# ${section.charAt(0).toUpperCase() + section.slice(1)}\n\n`; - + content += `\n\n# ${ + section.charAt(0).toUpperCase() + section.slice(1) + }\n\n`; + // Process files directly in the section root const sectionRootContent = await processSectionRootFiles(section); content += sectionRootContent; - + // Process subsections if any const subsections = await getSubdirectories(section); - + for (const subsection of subsections) { // Add subsection header (as h2) - content += `\n\n## ${subsection.charAt(0).toUpperCase() + subsection.slice(1)}\n\n`; - + content += `\n\n## ${ + subsection.charAt(0).toUpperCase() + subsection.slice(1) + }\n\n`; + // Process files in this subsection - const subsectionContent = await processSubsectionFiles(section, subsection); + const subsectionContent = await processSubsectionFiles( + section, + subsection + ); content += subsectionContent; } } - + // Write the llms-full.txt file fs.writeFileSync(outputFilePath, content); console.log(`Generated llms-full.txt file at ${outputFilePath}`); } // Run the script -generateLlmsFullTxt(); \ No newline at end of file +generateLlmsFullTxt(); diff --git a/packages/docs/scripts/generate-redirects.js b/packages/docs/scripts/generate-redirects.js index 345e2eef45..ad1b4e10ca 100644 --- a/packages/docs/scripts/generate-redirects.js +++ b/packages/docs/scripts/generate-redirects.js @@ -15,11 +15,13 @@ const redirectsContent = `# Netlify redirects // Ensure the build directory exists if (!fs.existsSync(buildDir)) { - console.error('Build directory does not exist. Make sure the Docusaurus build has completed.'); + console.error( + 'Build directory does not exist. Make sure the Docusaurus build has completed.' + ); process.exit(1); } // Write the _redirects file fs.writeFileSync(redirectsPath, redirectsContent); -console.log('_redirects file generated successfully in', redirectsPath); \ No newline at end of file +console.log('_redirects file generated successfully in', redirectsPath); diff --git a/packages/docs/src/components/HomepageFeatures.js b/packages/docs/src/components/HomepageFeatures.js index ea64d37760..2083cde32b 100644 --- a/packages/docs/src/components/HomepageFeatures.js +++ b/packages/docs/src/components/HomepageFeatures.js @@ -8,7 +8,8 @@ const FeatureList = [ title: 'Standards Compliant', description: ( <> - Robust DICOM Parsing. Supports DICOMweb and all transfer syntaxes out-of-the-box. + Robust DICOM Parsing. Supports DICOMweb and all transfer syntaxes + out-of-the-box. ), }, @@ -16,7 +17,8 @@ const FeatureList = [ title: 'Fast', description: ( <> - High performance GPU-accelerated image display. Multi-threaded image decoding. Progressive data streaming. + High performance GPU-accelerated image display. Multi-threaded image + decoding. Progressive data streaming. ), }, diff --git a/packages/docs/src/pages/index.module.css b/packages/docs/src/pages/index.module.css index 4be86d113d..061fdb2c72 100644 --- a/packages/docs/src/pages/index.module.css +++ b/packages/docs/src/pages/index.module.css @@ -83,12 +83,12 @@ border-bottom-left-radius: 8px; background-color: #5e16eb; margin-left: 15px; - font-size:16px + font-size: 16px; } .announcementBar { display: flex; - font-size:16px + font-size: 16px; } .smallScreenAnnouncement { diff --git a/packages/labelmap-interpolation/README.md b/packages/labelmap-interpolation/README.md index 568ed58f98..6a38aaba36 100644 --- a/packages/labelmap-interpolation/README.md +++ b/packages/labelmap-interpolation/README.md @@ -40,8 +40,8 @@ interpolate({ noHeuristicAlignment: false, // Whether to disable heuristic alignment noUseDistanceTransform: false, // Whether to disable distance transform useCustomSlicePositions: false, // Whether to use custom slice positions - preview: false // Whether to preview the interpolation result - } + preview: false, // Whether to preview the interpolation result + }, }); ``` diff --git a/packages/labelmap-interpolation/package.json b/packages/labelmap-interpolation/package.json index be24d82ff5..994b17beb2 100644 --- a/packages/labelmap-interpolation/package.json +++ b/packages/labelmap-interpolation/package.json @@ -36,8 +36,7 @@ "build:all": "yarn run build:esm", "start": "tsc --project ./tsconfig.json --watch", "format": "prettier --write 'src/**/*.js' 'test/**/*.js'", - "lint": "eslint --fix .", - "format-check": "npx eslint ./src --quiet", + "lint": "oxlint .", "api-check": "api-extractor --debug run ", "prepublishOnly": "yarn clean && yarn build" }, diff --git a/packages/nifti-volume-loader/package.json b/packages/nifti-volume-loader/package.json index 95279a3e71..4dfe720f27 100644 --- a/packages/nifti-volume-loader/package.json +++ b/packages/nifti-volume-loader/package.json @@ -56,7 +56,7 @@ "build": "yarn run build:all", "clean": "rm -rf node_modules/.cache/storybook && shx rm -rf dist", "clean:deep": "yarn run clean && shx rm -rf node_modules", - "format-check": "npx eslint ./src --quiet", + "lint": "oxlint .", "api-check": "api-extractor --debug run ", "prepublishOnly": "yarn run build", "webpack:watch": "webpack --mode development --progress --watch --config ./.webpack/webpack.dev.js" diff --git a/packages/polymorphic-segmentation/examples/PolySegWasmStackLabelmapToSurface/index.ts b/packages/polymorphic-segmentation/examples/PolySegWasmStackLabelmapToSurface/index.ts index 0f6d667ab5..c08b23564f 100644 --- a/packages/polymorphic-segmentation/examples/PolySegWasmStackLabelmapToSurface/index.ts +++ b/packages/polymorphic-segmentation/examples/PolySegWasmStackLabelmapToSurface/index.ts @@ -225,9 +225,8 @@ async function run() { cornerstoneTools.utilities.stackContextPrefetch.enable(element1); - const segImages = await imageLoader.createAndCacheDerivedLabelmapImages( - imageIds - ); + const segImages = + await imageLoader.createAndCacheDerivedLabelmapImages(imageIds); const segmentationImageIds = segImages.map((it) => it.imageId); diff --git a/packages/polymorphic-segmentation/package.json b/packages/polymorphic-segmentation/package.json index bd6c0ec46b..678e022b87 100644 --- a/packages/polymorphic-segmentation/package.json +++ b/packages/polymorphic-segmentation/package.json @@ -35,8 +35,7 @@ "build:all": "yarn run build:esm", "start": "tsc --project ./tsconfig.json --watch", "format": "prettier --write 'src/**/*.js' 'test/**/*.js'", - "lint": "eslint --fix .", - "format-check": "npx eslint ./src --quiet", + "lint": "oxlint .", "api-check": "api-extractor --debug run ", "prepublishOnly": "yarn clean && yarn build" }, diff --git a/packages/polymorphic-segmentation/src/Labelmap/convertContourToLabelmap.ts b/packages/polymorphic-segmentation/src/Labelmap/convertContourToLabelmap.ts index 11ef3f3076..4dcfd6ac56 100644 --- a/packages/polymorphic-segmentation/src/Labelmap/convertContourToLabelmap.ts +++ b/packages/polymorphic-segmentation/src/Labelmap/convertContourToLabelmap.ts @@ -124,9 +124,8 @@ export async function convertContourToStackLabelmap( }); // create - const segImages = await imageLoader.createAndCacheDerivedLabelmapImages( - imageIds - ); + const segImages = + await imageLoader.createAndCacheDerivedLabelmapImages(imageIds); const segmentationImageIds = segImages.map((it) => it.imageId); diff --git a/packages/polymorphic-segmentation/src/Surface/updateSurfaceData.ts b/packages/polymorphic-segmentation/src/Surface/updateSurfaceData.ts index d45a13ee8e..d79afbe1d6 100644 --- a/packages/polymorphic-segmentation/src/Surface/updateSurfaceData.ts +++ b/packages/polymorphic-segmentation/src/Surface/updateSurfaceData.ts @@ -21,9 +21,8 @@ const { } = cornerstoneTools; export async function updateSurfaceData(segmentationId) { - const surfacesObj = await computeSurfaceFromLabelmapSegmentation( - segmentationId - ); + const surfacesObj = + await computeSurfaceFromLabelmapSegmentation(segmentationId); if (!surfacesObj) { return; diff --git a/packages/tools/examples/CINETool/index.ts b/packages/tools/examples/CINETool/index.ts index fcddd745a7..a02cf78d7f 100644 --- a/packages/tools/examples/CINETool/index.ts +++ b/packages/tools/examples/CINETool/index.ts @@ -146,9 +146,8 @@ function setActiveElement(element) { (document.querySelector('#fpsSlider')).value = fps; - (( - document.querySelector('#fpsSlider-label') - )).innerText = ` Frames per second: ${fps}`; + (document.querySelector('#fpsSlider-label')).innerText = + ` Frames per second: ${fps}`; } /** diff --git a/packages/tools/examples/circleROIStartEndThresholdWithSegmentation/index.ts b/packages/tools/examples/circleROIStartEndThresholdWithSegmentation/index.ts index 74b2b0d413..2c9a2650d3 100644 --- a/packages/tools/examples/circleROIStartEndThresholdWithSegmentation/index.ts +++ b/packages/tools/examples/circleROIStartEndThresholdWithSegmentation/index.ts @@ -243,7 +243,6 @@ async function run() { throttleTimeout: 100, /* Simplified handles */ simplified: true, - }); toolGroup.setToolActive(CircleROIStartEndThresholdTool.toolName, { diff --git a/packages/tools/examples/contourRendering/index.ts b/packages/tools/examples/contourRendering/index.ts index e3605dedef..7b16619618 100644 --- a/packages/tools/examples/contourRendering/index.ts +++ b/packages/tools/examples/contourRendering/index.ts @@ -70,7 +70,10 @@ content.append(instructions); */ async function run() { // Init Cornerstone and related libraries - await initDemo(); + const config = (window as any).IS_TILED + ? { core: { renderingEngineMode: 'tiled' } } + : {}; + await initDemo(config); // Add tools to Cornerstone3D cornerstoneTools.addTool(PanTool); @@ -124,9 +127,8 @@ async function run() { // Add some segmentations based on the source data volume - const geometriesInfo = await createAndCacheGeometriesFromContours( - 'SampleContour' - ); + const geometriesInfo = + await createAndCacheGeometriesFromContours('SampleContour'); // Add the segmentations to state segmentation.addSegmentations([ diff --git a/packages/tools/examples/contourRenderingConfiguration/index.ts b/packages/tools/examples/contourRenderingConfiguration/index.ts index 1d57704fe3..9fcea17725 100644 --- a/packages/tools/examples/contourRenderingConfiguration/index.ts +++ b/packages/tools/examples/contourRenderingConfiguration/index.ts @@ -31,7 +31,7 @@ const { TrackballRotateTool, } = cornerstoneTools; const { MouseBindings } = csToolsEnums; -const { ViewportType, GeometryType } = Enums; +const { ViewportType } = Enums; // Define a unique id for the volume const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix @@ -89,8 +89,10 @@ addToggleButtonToToolbar({ const segmentIndex = 1; segmentation.config.visibility.setSegmentIndexVisibility( viewportId, - segmentationId, - csToolsEnums.SegmentationRepresentations.Contour, + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Contour, + }, segmentIndex, !toggle ); @@ -103,8 +105,10 @@ addToggleButtonToToolbar({ const segmentIndex = 2; segmentation.config.visibility.setSegmentIndexVisibility( viewportId, - segmentationId, - csToolsEnums.SegmentationRepresentations.Contour, + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Contour, + }, segmentIndex, !toggle ); @@ -130,9 +134,8 @@ addSliderToToolbar({ async function addSegmentationsToState() { // load the contour data - const geometryIds = await createAndCacheGeometriesFromContours( - 'CircleContour' - ); + const geometryIds = + await createAndCacheGeometriesFromContours('CircleContour'); // Add the segmentations to state segmentation.addSegmentations([ diff --git a/packages/tools/examples/contourRenderingMultiple/index.ts b/packages/tools/examples/contourRenderingMultiple/index.ts index a96f41d5cd..98d833fc10 100644 --- a/packages/tools/examples/contourRenderingMultiple/index.ts +++ b/packages/tools/examples/contourRenderingMultiple/index.ts @@ -145,9 +145,8 @@ async function run() { }); // Add some segmentations based on the source data volume - const geometriesInfo = await createAndCacheGeometriesFromContours( - 'SampleContour' - ); + const geometriesInfo = + await createAndCacheGeometriesFromContours('SampleContour'); // Add the segmentations to state segmentation.addSegmentations([ diff --git a/packages/tools/examples/dynamicallyAddAnnotations/angleToolUI.ts b/packages/tools/examples/dynamicallyAddAnnotations/angleToolUI.ts index a80d6ff903..c30675d49e 100644 --- a/packages/tools/examples/dynamicallyAddAnnotations/angleToolUI.ts +++ b/packages/tools/examples/dynamicallyAddAnnotations/angleToolUI.ts @@ -39,29 +39,29 @@ function createFormElement(): HTMLFormElement { coordType.charAt(0).toUpperCase() + coordType.slice(1) } Coords: Point 1 [${coordType === 'canvas' ? 'x, y' : 'i, j'}]: + coordType === 'canvas' ? 'x' : 'i' + }" value="10"> + coordType === 'canvas' ? 'y' : 'j' + }" value="10"> + coordType === 'canvas' ? 'x' : 'i' + }" value="100"> + coordType === 'canvas' ? 'y' : 'j' + }" value="100"> + coordType === 'canvas' ? 'x' : 'i' + }" value="100"> + coordType === 'canvas' ? 'y' : 'j' + }" value="10">
@@ -84,7 +84,7 @@ function addButtonListeners(form: HTMLFormElement): void { const [type, viewportType, useImageId] = button.id.split('-') as [ 'canvas' | 'image', keyof typeof typeToIdMap, - 'imageId'? + 'imageId'?, ]; const enabledElement = getEnabledElementByViewportId( typeToIdMap[viewportType] diff --git a/packages/tools/examples/dynamicallyAddAnnotations/arrowAnnotateToolUI.ts b/packages/tools/examples/dynamicallyAddAnnotations/arrowAnnotateToolUI.ts index d0578d4ed5..f9c96d6148 100644 --- a/packages/tools/examples/dynamicallyAddAnnotations/arrowAnnotateToolUI.ts +++ b/packages/tools/examples/dynamicallyAddAnnotations/arrowAnnotateToolUI.ts @@ -34,20 +34,20 @@ function createFormElement(): HTMLFormElement { coordType.charAt(0).toUpperCase() + coordType.slice(1) } Coords: Start [${coordType === 'canvas' ? 'x, y' : 'i, j'}]: + coordType === 'canvas' ? 'x' : 'i' + }" value="10"> + coordType === 'canvas' ? 'y' : 'j' + }" value="10"> + coordType === 'canvas' ? 'x' : 'i' + }" value="100"> + coordType === 'canvas' ? 'y' : 'j' + }" value="100">
@@ -72,7 +72,7 @@ function addButtonListeners(form: HTMLFormElement): void { const [type, viewportType, useImageId] = button.id.split('-') as [ 'canvas' | 'image', keyof typeof typeToIdMap, - 'imageId'? + 'imageId'?, ]; const enabledElement = getEnabledElementByViewportId( typeToIdMap[viewportType] diff --git a/packages/tools/examples/dynamicallyAddAnnotations/circleROIToolUI.ts b/packages/tools/examples/dynamicallyAddAnnotations/circleROIToolUI.ts index f83446704b..7267848dff 100644 --- a/packages/tools/examples/dynamicallyAddAnnotations/circleROIToolUI.ts +++ b/packages/tools/examples/dynamicallyAddAnnotations/circleROIToolUI.ts @@ -34,20 +34,20 @@ function createFormElement(): HTMLFormElement { coordType.charAt(0).toUpperCase() + coordType.slice(1) } Coords: Center [${coordType === 'canvas' ? 'x, y' : 'i, j'}]: + coordType === 'canvas' ? 'x' : 'i' + }" value="50"> + coordType === 'canvas' ? 'y' : 'j' + }" value="50"> + coordType === 'canvas' ? 'x' : 'i' + }" value="100"> + coordType === 'canvas' ? 'y' : 'j' + }" value="50">
@@ -70,7 +70,7 @@ function addButtonListeners(form: HTMLFormElement): void { const [type, viewportType, useImageId] = button.id.split('-') as [ 'canvas' | 'image', keyof typeof typeToIdMap, - 'imageId'? + 'imageId'?, ]; const enabledElement = getEnabledElementByViewportId( typeToIdMap[viewportType] diff --git a/packages/tools/examples/dynamicallyAddAnnotations/ellipticalROIToolUI.ts b/packages/tools/examples/dynamicallyAddAnnotations/ellipticalROIToolUI.ts index 4f64d6f789..38e7235823 100644 --- a/packages/tools/examples/dynamicallyAddAnnotations/ellipticalROIToolUI.ts +++ b/packages/tools/examples/dynamicallyAddAnnotations/ellipticalROIToolUI.ts @@ -46,11 +46,11 @@ function createFormElement(): HTMLFormElement { coordType.charAt(0).toUpperCase() + coordType.slice(1) } Coords: Top Left [${coordType === 'canvas' ? 'x, y' : 'i, j'}]: + coordType === 'canvas' ? 'x' : 'i' + }" value="10"> + coordType === 'canvas' ? 'y' : 'j' + }" value="10"> @@ -89,7 +89,7 @@ function addButtonListeners(form: HTMLFormElement): void { const [type, viewportType, useImageId] = button.id.split('-') as [ 'canvas' | 'image', keyof typeof typeToIdMap, - 'imageId'? + 'imageId'?, ]; const enabledElement = getEnabledElementByViewportId( typeToIdMap[viewportType] @@ -101,7 +101,10 @@ function addButtonListeners(form: HTMLFormElement): void { const convertPoint = (point: Point2): Point3 => type === 'image' - ? (utilities.imageToWorldCoords(imageId || currentImageId, point) as Point3) + ? (utilities.imageToWorldCoords( + imageId || currentImageId, + point + ) as Point3) : viewport.canvasToWorld(point); const points: Point3[] = [ diff --git a/packages/tools/examples/dynamicallyAddAnnotations/labelToolUI.ts b/packages/tools/examples/dynamicallyAddAnnotations/labelToolUI.ts index 41b04dc412..3509dff897 100644 --- a/packages/tools/examples/dynamicallyAddAnnotations/labelToolUI.ts +++ b/packages/tools/examples/dynamicallyAddAnnotations/labelToolUI.ts @@ -28,11 +28,11 @@ function createFormElement(): HTMLFormElement { coordType.charAt(0).toUpperCase() + coordType.slice(1) } Coords: Start [${coordType === 'canvas' ? 'x, y' : 'i, j'}]: + coordType === 'canvas' ? 'x' : 'i' + }" value="10"> + coordType === 'canvas' ? 'y' : 'j' + }" value="10">
@@ -51,7 +51,7 @@ function addButtonListeners(form: HTMLFormElement): void { button.addEventListener('click', () => { const [type, viewportType] = button.id.split('-') as [ 'canvas' | 'image', - keyof typeof typeToIdMap + keyof typeof typeToIdMap, ]; const enabledElement = getEnabledElementByViewportId( typeToIdMap[viewportType] diff --git a/packages/tools/examples/dynamicallyAddAnnotations/lengthToolUI.ts b/packages/tools/examples/dynamicallyAddAnnotations/lengthToolUI.ts index 4a065b20ea..23c9a765fe 100644 --- a/packages/tools/examples/dynamicallyAddAnnotations/lengthToolUI.ts +++ b/packages/tools/examples/dynamicallyAddAnnotations/lengthToolUI.ts @@ -35,20 +35,20 @@ function createFormElement(): HTMLFormElement { coordType.charAt(0).toUpperCase() + coordType.slice(1) } Coords: Start [${coordType === 'canvas' ? 'x, y' : 'i, j'}]: + coordType === 'canvas' ? 'x' : 'i' + }" value="10"> + coordType === 'canvas' ? 'y' : 'j' + }" value="10"> + coordType === 'canvas' ? 'x' : 'i' + }" value="100"> + coordType === 'canvas' ? 'y' : 'j' + }" value="100">
@@ -71,7 +71,7 @@ function addButtonListeners(form: HTMLFormElement): void { const [type, viewportType, useImageId] = button.id.split('-') as [ 'canvas' | 'image', keyof typeof typeToIdMap, - 'imageId'? + 'imageId'?, ]; const enabledElement = getEnabledElementByViewportId( typeToIdMap[viewportType] diff --git a/packages/tools/examples/dynamicallyAddAnnotations/probeToolUI.ts b/packages/tools/examples/dynamicallyAddAnnotations/probeToolUI.ts index 71df27ed8d..d2cd03ab64 100644 --- a/packages/tools/examples/dynamicallyAddAnnotations/probeToolUI.ts +++ b/packages/tools/examples/dynamicallyAddAnnotations/probeToolUI.ts @@ -34,11 +34,11 @@ function createFormElement(): HTMLFormElement { coordType.charAt(0).toUpperCase() + coordType.slice(1) } Coords: Point [${coordType === 'canvas' ? 'x, y' : 'i, j'}]: + coordType === 'canvas' ? 'x' : 'i' + }" value="10"> + coordType === 'canvas' ? 'y' : 'j' + }" value="10">
@@ -61,7 +61,7 @@ function addButtonListeners(form: HTMLFormElement): void { const [type, viewportType, useImageId] = button.id.split('-') as [ 'canvas' | 'image', keyof typeof typeToIdMap, - 'imageId'? + 'imageId'?, ]; const enabledElement = getEnabledElementByViewportId( typeToIdMap[viewportType] diff --git a/packages/tools/examples/dynamicallyAddAnnotations/rectangleROIToolUI.ts b/packages/tools/examples/dynamicallyAddAnnotations/rectangleROIToolUI.ts index 0e5acad432..bf73e9e1cd 100644 --- a/packages/tools/examples/dynamicallyAddAnnotations/rectangleROIToolUI.ts +++ b/packages/tools/examples/dynamicallyAddAnnotations/rectangleROIToolUI.ts @@ -47,11 +47,11 @@ function createFormElement(): HTMLFormElement { coordType.charAt(0).toUpperCase() + coordType.slice(1) } Coords: Top Left [${coordType === 'canvas' ? 'x, y' : 'i, j'}]: + coordType === 'canvas' ? 'x' : 'i' + }" value="10"> + coordType === 'canvas' ? 'y' : 'j' + }" value="10"> @@ -90,7 +90,7 @@ function addButtonListeners(form: HTMLFormElement): void { const [type, viewportType, useImageId] = button.id.split('-') as [ 'canvas' | 'image', keyof typeof typeToIdMap, - 'imageId'? + 'imageId'?, ]; const enabledElement = getEnabledElementByViewportId( typeToIdMap[viewportType] diff --git a/packages/tools/examples/dynamicallyAddAnnotations/splineROIToolUI.ts b/packages/tools/examples/dynamicallyAddAnnotations/splineROIToolUI.ts index d2dcb39983..05f31ddc0a 100644 --- a/packages/tools/examples/dynamicallyAddAnnotations/splineROIToolUI.ts +++ b/packages/tools/examples/dynamicallyAddAnnotations/splineROIToolUI.ts @@ -39,29 +39,29 @@ function createFormElement(): HTMLFormElement { coordType.charAt(0).toUpperCase() + coordType.slice(1) } Coords: Point 1 [${coordType === 'canvas' ? 'x, y' : 'i, j'}]: + coordType === 'canvas' ? 'x' : 'i' + }" value="50"> + coordType === 'canvas' ? 'y' : 'j' + }" value="50"> + coordType === 'canvas' ? 'x' : 'i' + }" value="100"> + coordType === 'canvas' ? 'y' : 'j' + }" value="100"> + coordType === 'canvas' ? 'x' : 'i' + }" value="150"> + coordType === 'canvas' ? 'y' : 'j' + }" value="50">
@@ -84,7 +84,7 @@ function addButtonListeners(form: HTMLFormElement): void { const [type, viewportType, useImageId] = button.id.split('-') as [ 'canvas' | 'image', keyof typeof typeToIdMap, - 'imageId'? + 'imageId'?, ]; const enabledElement = getEnabledElementByViewportId( typeToIdMap[viewportType] diff --git a/packages/tools/examples/labelmapRendering/index.ts b/packages/tools/examples/labelmapRendering/index.ts index 584f45b16b..8e87bc70bf 100644 --- a/packages/tools/examples/labelmapRendering/index.ts +++ b/packages/tools/examples/labelmapRendering/index.ts @@ -71,7 +71,10 @@ content.appendChild(viewportGrid); */ async function run() { // Init Cornerstone and related libraries - await initDemo(); + const config = (window as any).IS_TILED + ? { core: { renderingEngineMode: 'tiled' } } + : {}; + await initDemo(config); // Define tool groups to add the segmentation display tool to const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); diff --git a/packages/tools/examples/labelmapStatistics/index.ts b/packages/tools/examples/labelmapStatistics/index.ts index 87a433a4c3..9edd2b47d3 100644 --- a/packages/tools/examples/labelmapStatistics/index.ts +++ b/packages/tools/examples/labelmapStatistics/index.ts @@ -275,9 +275,8 @@ async function run() { // Create a stack of images const imageIdsArray = imageIds.slice(0, 10); // Create segmentation images for the stack - const segImages = await imageLoader.createAndCacheDerivedLabelmapImages( - imageIdsArray - ); + const segImages = + await imageLoader.createAndCacheDerivedLabelmapImages(imageIdsArray); // Instantiate a rendering engine renderingEngine = new RenderingEngine(renderingEngineId); diff --git a/packages/tools/examples/livewireContourSegmentation/index.ts b/packages/tools/examples/livewireContourSegmentation/index.ts index e114fe3484..1c70765efb 100644 --- a/packages/tools/examples/livewireContourSegmentation/index.ts +++ b/packages/tools/examples/livewireContourSegmentation/index.ts @@ -155,22 +155,28 @@ addButtonToToolbar({ segmentation.config.visibility.setSegmentIndexVisibility( viewportIds[0], - segmentationId, - csToolsEnums.SegmentationRepresentations.Contour, + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Contour, + }, activeSegmentIndex, visible ); segmentation.config.visibility.setSegmentIndexVisibility( viewportIds[1], - segmentationId, - csToolsEnums.SegmentationRepresentations.Contour, + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Contour, + }, activeSegmentIndex, visible ); segmentation.config.visibility.setSegmentIndexVisibility( viewportIds[2], - segmentationId, - csToolsEnums.SegmentationRepresentations.Contour, + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Contour, + }, activeSegmentIndex, visible ); diff --git a/packages/tools/examples/petCt/index.ts b/packages/tools/examples/petCt/index.ts index 99831cd73c..69352891df 100644 --- a/packages/tools/examples/petCt/index.ts +++ b/packages/tools/examples/petCt/index.ts @@ -30,7 +30,7 @@ const { CrosshairsTool, TrackballRotateTool, VolumeRotateTool, - RectangleROITool + RectangleROITool, } = cornerstoneTools; const { MouseBindings } = csToolsEnums; @@ -89,7 +89,11 @@ setTitleAndDescription( 'PT-CT fusion layout with Crosshairs, and synchronized cameras, CT W/L and PET threshold' ); -const optionsValues = [WindowLevelTool.toolName, CrosshairsTool.toolName, RectangleROITool.toolName]; +const optionsValues = [ + WindowLevelTool.toolName, + CrosshairsTool.toolName, + RectangleROITool.toolName, +]; // ============================= // addDropdownToToolbar({ diff --git a/packages/tools/examples/planarFreehandContourSegmentationTool/index.ts b/packages/tools/examples/planarFreehandContourSegmentationTool/index.ts index d83def9d65..0e64121d1e 100644 --- a/packages/tools/examples/planarFreehandContourSegmentationTool/index.ts +++ b/packages/tools/examples/planarFreehandContourSegmentationTool/index.ts @@ -213,15 +213,19 @@ addButtonToToolbar({ segmentation.config.visibility.setSegmentIndexVisibility( viewportIds[0], - segmentationId, - csToolsEnums.SegmentationRepresentations.Contour, + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Contour, + }, activeSegmentIndex, visible ); segmentation.config.visibility.setSegmentIndexVisibility( viewportIds[1], - segmentationId, - csToolsEnums.SegmentationRepresentations.Contour, + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Contour, + }, activeSegmentIndex, visible ); diff --git a/packages/tools/examples/polyDataActorManipulationTools/index.ts b/packages/tools/examples/polyDataActorManipulationTools/index.ts index 461c884dca..10bcf34bbb 100644 --- a/packages/tools/examples/polyDataActorManipulationTools/index.ts +++ b/packages/tools/examples/polyDataActorManipulationTools/index.ts @@ -1,7 +1,10 @@ import type { Types } from '@cornerstonejs/core'; -import { RenderingEngine, Enums } from '@cornerstonejs/core'; +import { + RenderingEngine, + Enums, + init as csRenderInit, +} from '@cornerstonejs/core'; import { setTitleAndDescription } from '../../../../utils/demo/helpers'; -import { init as csRenderInit } from '@cornerstonejs/core'; import { init as csToolsInit } from '@cornerstonejs/tools'; import * as cornerstoneTools from '@cornerstonejs/tools'; @@ -101,7 +104,16 @@ function getSphereActor({ */ async function run() { // Init Cornerstone and related libraries - await csRenderInit(); + + const urlParams = new URLSearchParams(window.location.search); + const debugMode = urlParams.get('debug') === 'true'; + + await csRenderInit({ + debug: { + statsOverlay: debugMode, + }, + }); + await csToolsInit(); const toolGroupId = 'NAVIGATION_TOOL_GROUP_ID'; diff --git a/packages/tools/examples/rectangleROIStartEndThresholdWithSegmentation/index.ts b/packages/tools/examples/rectangleROIStartEndThresholdWithSegmentation/index.ts index 1c99cf838d..096c3bbb15 100644 --- a/packages/tools/examples/rectangleROIStartEndThresholdWithSegmentation/index.ts +++ b/packages/tools/examples/rectangleROIStartEndThresholdWithSegmentation/index.ts @@ -372,7 +372,7 @@ async function run() { showTextBox: true, storePointData: true, /*Set a custom wait time */ - throttleTimeout: 100 + throttleTimeout: 100, }); toolGroup.setToolActive(RectangleROIStartEndThresholdTool.toolName, { diff --git a/packages/tools/examples/referenceCursors/index.ts b/packages/tools/examples/referenceCursors/index.ts index d3c8bb17cf..3caade170f 100644 --- a/packages/tools/examples/referenceCursors/index.ts +++ b/packages/tools/examples/referenceCursors/index.ts @@ -34,8 +34,8 @@ const initialDisableCursor = false; // ======== Set up page ======== // setTitleAndDescription( - 'Cursor corsshair syncing example', - 'This example shows how to sync the crosshair cursors between 3 viewports (2 Stack viewports and 1 Volume viewport with a slightly different orientation). To disable orther cursors, set disableCursor to on and then disable and reactivate the tool.' + 'Cursor crosshair syncing example', + 'This example shows how to sync the crosshair cursors between 3 viewports (2 Stack viewports and 1 Volume viewport with a slightly different orientation). To disable other cursors, set disableCursor to on and then disable and reactivate the tool.' ); const size = '500px'; diff --git a/packages/tools/examples/regionSegmentPlus/index.ts b/packages/tools/examples/regionSegmentPlus/index.ts index f4c6a3f7f0..a69d3f2b56 100644 --- a/packages/tools/examples/regionSegmentPlus/index.ts +++ b/packages/tools/examples/regionSegmentPlus/index.ts @@ -152,9 +152,8 @@ const updateSeedVariancesConfig = cstUtils.throttle( async function addSegmentationToState(imageIds) { // Create segmentation images for each image in the stack - const segImages = await imageLoader.createAndCacheDerivedLabelmapImages( - imageIds - ); + const segImages = + await imageLoader.createAndCacheDerivedLabelmapImages(imageIds); // Add the segmentation to state segmentation.addSegmentations([ diff --git a/packages/tools/examples/segmentBidirectionalToolStack/index.ts b/packages/tools/examples/segmentBidirectionalToolStack/index.ts index f0722ec5fa..44bc65559d 100644 --- a/packages/tools/examples/segmentBidirectionalToolStack/index.ts +++ b/packages/tools/examples/segmentBidirectionalToolStack/index.ts @@ -202,9 +202,8 @@ addButtonToToolbar({ async function addSegmentationsToState(imageIds) { // Create a segmentation of the same resolution as the source data - const segImages = await imageLoader.createAndCacheDerivedLabelmapImages( - imageIds - ); + const segImages = + await imageLoader.createAndCacheDerivedLabelmapImages(imageIds); // Add the segmentations to state segmentation.addSegmentations([ diff --git a/packages/tools/examples/segmentLabel/index.ts b/packages/tools/examples/segmentLabel/index.ts index 96fbcd81c2..df5345eb2b 100644 --- a/packages/tools/examples/segmentLabel/index.ts +++ b/packages/tools/examples/segmentLabel/index.ts @@ -317,9 +317,8 @@ async function _handleStackViewports(stackImageIds: string[]) { const imageIdsArray = [stackImageIds[0]]; - const segImages = await imageLoader.createAndCacheDerivedLabelmapImages( - imageIdsArray - ); + const segImages = + await imageLoader.createAndCacheDerivedLabelmapImages(imageIdsArray); const segmentationImageIds = segImages.map((it) => it.imageId); diff --git a/packages/tools/examples/segmentSelect/index.ts b/packages/tools/examples/segmentSelect/index.ts index 78cc2cac6d..ae6553e0b6 100644 --- a/packages/tools/examples/segmentSelect/index.ts +++ b/packages/tools/examples/segmentSelect/index.ts @@ -317,9 +317,8 @@ async function _handleStackViewports(stackImageIds: string[]) { const imageIdsArray = [stackImageIds[0]]; - const segImages = await imageLoader.createAndCacheDerivedLabelmapImages( - imageIdsArray - ); + const segImages = + await imageLoader.createAndCacheDerivedLabelmapImages(imageIdsArray); const segmentationImageIds = segImages.map((it) => it.imageId); diff --git a/packages/tools/examples/splineContourSegmentationTools/index.ts b/packages/tools/examples/splineContourSegmentationTools/index.ts index ad42252228..8cd4bfd1d4 100644 --- a/packages/tools/examples/splineContourSegmentationTools/index.ts +++ b/packages/tools/examples/splineContourSegmentationTools/index.ts @@ -182,8 +182,10 @@ addButtonToToolbar({ segmentation.config.visibility.setSegmentIndexVisibility( viewportId, - segmentationId, - csToolsEnums.SegmentationRepresentations.Contour, + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Contour, + }, activeSegmentIndex, visible ); diff --git a/packages/tools/examples/splineContourSegmentationToolsAdvanced/index.ts b/packages/tools/examples/splineContourSegmentationToolsAdvanced/index.ts index 6a7e19b390..f323fc795e 100644 --- a/packages/tools/examples/splineContourSegmentationToolsAdvanced/index.ts +++ b/packages/tools/examples/splineContourSegmentationToolsAdvanced/index.ts @@ -320,8 +320,10 @@ addButtonToToolbar({ viewportIds.forEach((viewportId) => { segmentation.config.visibility.setSegmentIndexVisibility( viewportId, - activeSegmentationId, - csToolsEnums.SegmentationRepresentations.Contour, + { + segmentationId: activeSegmentationId, + type: csToolsEnums.SegmentationRepresentations.Contour, + }, activeSegmentIndex, visible ); diff --git a/packages/tools/examples/stackAnnotationTools/index.ts b/packages/tools/examples/stackAnnotationTools/index.ts index 977f1e75d5..37b90b4b09 100644 --- a/packages/tools/examples/stackAnnotationTools/index.ts +++ b/packages/tools/examples/stackAnnotationTools/index.ts @@ -211,7 +211,10 @@ addButtonToToolbar({ */ async function run() { // Init Cornerstone and related libraries - await initDemo(); + const config = (window as any).IS_TILED + ? { core: { renderingEngineMode: 'tiled' } } + : {}; + await initDemo(config); // Add tools to Cornerstone3D cornerstoneTools.addTool(LengthTool); diff --git a/packages/tools/examples/stackLabelmapSegmentation/index.ts b/packages/tools/examples/stackLabelmapSegmentation/index.ts index 0a12af81f0..bd4e688746 100644 --- a/packages/tools/examples/stackLabelmapSegmentation/index.ts +++ b/packages/tools/examples/stackLabelmapSegmentation/index.ts @@ -484,13 +484,11 @@ async function run() { viewport = renderingEngine.getViewport(viewportId); const ctImageIds = imageIds.slice(0, 3); - const ctSegImages = await imageLoader.createAndCacheDerivedLabelmapImages( - ctImageIds - ); + const ctSegImages = + await imageLoader.createAndCacheDerivedLabelmapImages(ctImageIds); - const mgSegImages = await imageLoader.createAndCacheDerivedLabelmapImages( - mgImageIds - ); + const mgSegImages = + await imageLoader.createAndCacheDerivedLabelmapImages(mgImageIds); const viewport2 = renderingEngine.getViewport(viewportId2); await viewport.setStack(ctImageIds, 0); diff --git a/packages/tools/examples/stackRange/index.ts b/packages/tools/examples/stackRange/index.ts index 02b7be9c2c..26f69e6933 100644 --- a/packages/tools/examples/stackRange/index.ts +++ b/packages/tools/examples/stackRange/index.ts @@ -218,8 +218,8 @@ function updateAnnotationDiv(uid) { const range = AnnotationMultiSlice.getFrameRangeStr(annotation); selectionDiv.innerHTML = ` ${toolName} Annotation UID:${uid} Label:${ - data.label || data.text - } ${annotation.isVisible ? 'visible' : 'not visible'}
+ data.label || data.text + } ${annotation.isVisible ? 'visible' : 'not visible'}
Range: Frames: ${range}
`; } diff --git a/packages/tools/examples/toolHistoryGrouping/index.ts b/packages/tools/examples/toolHistoryGrouping/index.ts new file mode 100644 index 0000000000..65816762a9 --- /dev/null +++ b/packages/tools/examples/toolHistoryGrouping/index.ts @@ -0,0 +1,324 @@ +import type { Types } from '@cornerstonejs/core'; +import { + RenderingEngine, + Enums, + getRenderingEngine, + volumeLoader, + setVolumesForViewports, + utilities as csUtils, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + addDropdownToToolbar, + addButtonToToolbar, + annotationTools, + labelmapTools, + contourTools, + addManipulationBindings, +} from '../../../../utils/demo/helpers'; +import * as cornerstoneTools from '@cornerstonejs/tools'; + +const { segmentation } = cornerstoneTools; +const { MouseBindings } = cornerstoneTools.Enums; +const { DefaultHistoryMemo } = csUtils.HistoryMemo; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +const { + ToolGroupManager, + Enums: csToolsEnums, + AnnotationTool, +} = cornerstoneTools; + +const { ViewportType } = Enums; +const renderingEngineId = 'myRenderingEngine'; +const viewportId = 'CT_STACK'; +const labelmapSegmentationId = 'labelmapSegmentationId'; +const contourSegmentationId = 'contourSegmentationId'; +const defaultTool = 'ThresholdCircle'; + +const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix +const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use +const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id + +const toolMap = new Map(annotationTools); +for (const [key, value] of labelmapTools.toolMap) { + toolMap.set(key, value); +} +for (const [key, value] of contourTools.toolMap) { + toolMap.set(key, value); +} + +const demoToolbar = document.getElementById('demo-toolbar'); + +const group0 = document.createElement('div'); +group0.style.marginBottom = '10px'; +group0.style.color = 'red'; +group0.innerText = 'Recording is OFF'; +demoToolbar.appendChild(group0); + +const group1 = document.createElement('div'); +group1.style.marginBottom = '10px'; +demoToolbar.appendChild(group1); + +const group2 = document.createElement('div'); +group2.style.marginBottom = '10px'; +demoToolbar.appendChild(group2); + +// ======== Set up page ======== // +setTitleAndDescription( + 'Tool History Grouping', + "Demonstrate undo/redo grouped actions on tools. Notice that in most cases this should be done programmatically and the user shouldn't have to be aware of this api." +); + +const content = document.getElementById('content'); +const element = document.createElement('div'); + +// Disable right click context menu so we can have right click tools +element.oncontextmenu = (e) => e.preventDefault(); + +element.id = 'cornerstone-element'; +element.style.width = '500px'; +element.style.height = '500px'; + +content.appendChild(element); + +const info = document.createElement('div'); +content.appendChild(info); + +const instructions = document.createElement('p'); +instructions.innerText = ` +Left Click to use selected tool +z to undo, y to redo +`; +info.appendChild(instructions); + +// ============================= // + +const toolGroupId = 'STACK_TOOL_GROUP_ID'; + +const cancelToolDrawing = (evt) => { + const { element, key } = evt.detail; + if (key === 'Escape') { + cornerstoneTools.cancelActiveManipulations(element); + } +}; + +element.addEventListener(csToolsEnums.Events.KEY_DOWN, (evt) => { + cancelToolDrawing(evt); +}); + +addDropdownToToolbar({ + options: { map: toolMap, defaultValue: defaultTool }, + onSelectedValueChange: (newSelectedToolName, data) => { + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + + // Set the old tool passive + const selectedToolName = toolGroup.getActivePrimaryMouseButtonTool(); + if (selectedToolName) { + toolGroup.setToolPassive(selectedToolName); + } + + // Set the new tool active + toolGroup.setToolActive(newSelectedToolName as string, { + bindings: [ + { + mouseButton: MouseBindings.Primary, // Left Click + }, + ], + }); + const isContour = + data?.segmentationType === + csToolsEnums.SegmentationRepresentations.Contour; + segmentation.activeSegmentation.setActiveSegmentation( + viewportId, + isContour ? contourSegmentationId : labelmapSegmentationId + ); + }, +}); + +addDropdownToToolbar({ + options: { values: ['1', '2', '3'], defaultValue: '1' }, + labelText: 'Segment', + onSelectedValueChange: (segmentIndex) => { + segmentation.segmentIndex.setActiveSegmentIndex( + labelmapSegmentationId, + Number(segmentIndex) + ); + segmentation.segmentIndex.setActiveSegmentIndex( + contourSegmentationId, + Number(segmentIndex) + ); + }, +}); + +addButtonToToolbar({ + id: 'startRecording', + title: 'Start Group Recordings', + onClick() { + DefaultHistoryMemo.startGroupRecording(); + group0.style.color = 'green'; + group0.innerText = 'Recording is ON'; + }, + container: group1, +}); + +addButtonToToolbar({ + id: 'endRecording', + title: 'End Group Recordings', + onClick() { + DefaultHistoryMemo.endGroupRecording(); + group0.style.color = 'red'; + group0.innerText = 'Recording is OFF'; + }, + container: group1, +}); + +addButtonToToolbar({ + id: 'Undo', + title: 'Undo', + onClick() { + DefaultHistoryMemo.undo(); + }, + container: group2, +}); + +addButtonToToolbar({ + id: 'Redo', + title: 'Redo', + onClick() { + DefaultHistoryMemo.redo(); + }, + container: group2, +}); + +addButtonToToolbar({ + id: 'Delete', + title: 'Delete Annotation', + onClick() { + const annotationUIDs = + cornerstoneTools.annotation.selection.getAnnotationsSelected(); + + if (annotationUIDs.length === 0) { + return; + } + + const annotation = cornerstoneTools.annotation.state.getAnnotation( + annotationUIDs[0] + ); + + if (annotation) { + // Note that delete needs to have a memo created for it, as the underlying + // state manager doesn't record this directly. + // The deleting flag is set to true meaning that this annotation is about + // to be deleted (but is NOT yet deleted). + AnnotationTool.createAnnotationMemo(element, annotation, { + deleting: true, + }); + cornerstoneTools.annotation.state.removeAnnotation( + annotation.annotationUID + ); + getRenderingEngine(renderingEngineId).render(); + } + }, + container: group2, +}); + +async function addSegmentationsToState() { + // Create a segmentation of the same resolution as the source data + await volumeLoader.createAndCacheDerivedLabelmapVolume(volumeId, { + volumeId: labelmapSegmentationId, + }); + + // Add the segmentations to state + segmentation.addSegmentations([ + { + segmentationId: labelmapSegmentationId, + representation: { + // The type of segmentation + type: csToolsEnums.SegmentationRepresentations.Labelmap, + // The actual segmentation data, in the case of labelmap this is a + // reference to the source volume of the segmentation. + data: { + volumeId: labelmapSegmentationId, + }, + }, + }, + { + segmentationId: contourSegmentationId, + representation: { + type: csToolsEnums.SegmentationRepresentations.Contour, + }, + }, + ]); +} + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + // Define a tool group, which defines how mouse events map to tool commands for + // Any viewport using the group + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + addManipulationBindings(toolGroup, { toolMap }); + + // Get Cornerstone imageIds and fetch metadata into RAM + const imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + SeriesInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + wadoRsRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + }); + + // Define a volume in memory + const volume = await volumeLoader.createAndCacheVolume(volumeId, { + imageIds, + }); + + await addSegmentationsToState(); + + // Instantiate a rendering engine + const renderingEngine = new RenderingEngine(renderingEngineId); + + // Create a stack viewport + const viewportInput = { + viewportId, + type: ViewportType.ORTHOGRAPHIC, + element, + defaultOptions: { + background: [0.2, 0, 0.2], + }, + }; + + renderingEngine.enableElement(viewportInput); + + // Set the tool group on the viewport + toolGroup.addViewport(viewportId, renderingEngineId); + + // Get the stack viewport that was created + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + volume.load(); + + // Set volumes on the viewports + await setVolumesForViewports(renderingEngine, [{ volumeId }], [viewportId]); + + // Render the image + viewport.render(); + + await segmentation.addLabelmapRepresentationToViewport(viewportId, [ + { segmentationId: labelmapSegmentationId }, + ]); +} + +run(); diff --git a/packages/tools/examples/videoColor/index.ts b/packages/tools/examples/videoColor/index.ts index 3c74076c85..162f5e4682 100644 --- a/packages/tools/examples/videoColor/index.ts +++ b/packages/tools/examples/videoColor/index.ts @@ -142,9 +142,8 @@ addButtonToToolbar({ ); console.log('White=', white); viewport.setAverageWhite(white); - document.getElementById( - 'Color Correct' - ).innerText = `Avg Color: ${white.join(',')}`; + document.getElementById('Color Correct').innerText = + `Avg Color: ${white.join(',')}`; currentWhite = -1; }, }); diff --git a/packages/tools/examples/videoContourSegmentation/index.ts b/packages/tools/examples/videoContourSegmentation/index.ts index 95d89eac79..51a0908927 100644 --- a/packages/tools/examples/videoContourSegmentation/index.ts +++ b/packages/tools/examples/videoContourSegmentation/index.ts @@ -193,7 +193,10 @@ addButtonToToolbar({ segmentation.config.visibility.setSegmentIndexVisibility( viewportId, - segmentationRepresentationUID, + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Contour, + }, activeSegmentIndex, visible ); diff --git a/packages/tools/examples/videoGroup/index.ts b/packages/tools/examples/videoGroup/index.ts index e4ea2b446d..90a0604441 100644 --- a/packages/tools/examples/videoGroup/index.ts +++ b/packages/tools/examples/videoGroup/index.ts @@ -256,13 +256,13 @@ function updateAnnotationDiv(uid) { const { fps } = viewport; selectionDiv.innerHTML = ` ${toolName} Annotation UID:${uid} Label:${ - data.label || data.text - } ${annotation.isVisible ? 'visible' : 'not visible'}
+ data.label || data.text + } ${annotation.isVisible ? 'visible' : 'not visible'}
Range: ${rangeArr.join('-')} Time ${rangeArr - .map((it) => Math.round((it * 10) / fps) / 10) - .join('-')} Groups: ${group1.has(uid) ? '1' : ''} ${ - group2.has(uid) ? '2' : '' - }
+ .map((it) => Math.round((it * 10) / fps) / 10) + .join('-')} Groups: ${group1.has(uid) ? '1' : ''} ${ + group2.has(uid) ? '2' : '' + }
`; } diff --git a/packages/tools/examples/videoRange/index.ts b/packages/tools/examples/videoRange/index.ts index f02003cf12..24f5581bdb 100644 --- a/packages/tools/examples/videoRange/index.ts +++ b/packages/tools/examples/videoRange/index.ts @@ -234,11 +234,11 @@ function updateAnnotationDiv(uid) { const { fps } = viewport; selectionDiv.innerHTML = ` ${toolName} Annotation UID:${uid} Label:${ - data.label || data.text - } ${annotation.isVisible ? 'visible' : 'not visible'}
+ data.label || data.text + } ${annotation.isVisible ? 'visible' : 'not visible'}
Range: Frames: ${rangeArr.join('-')} Times ${rangeArr - .map((it) => Math.round((it * 10) / fps) / 10) - .join('-')}
+ .map((it) => Math.round((it * 10) / fps) / 10) + .join('-')}
`; } diff --git a/packages/tools/examples/videoTools/index.ts b/packages/tools/examples/videoTools/index.ts index d874705ac8..33d33f7604 100644 --- a/packages/tools/examples/videoTools/index.ts +++ b/packages/tools/examples/videoTools/index.ts @@ -149,8 +149,8 @@ function updateAnnotationDiv(uid) { const { toolName } = metadata; selectionDiv.innerHTML = ` ${toolName} Annotation UID:${uid} Label:${ - data.label || data.text - } ${annotation.isVisible ? 'visible' : 'not visible'} + data.label || data.text + } ${annotation.isVisible ? 'visible' : 'not visible'} `; } diff --git a/packages/tools/examples/volumeAnnotationTools/index.ts b/packages/tools/examples/volumeAnnotationTools/index.ts index ecac301618..cbbeb3ee64 100644 --- a/packages/tools/examples/volumeAnnotationTools/index.ts +++ b/packages/tools/examples/volumeAnnotationTools/index.ts @@ -79,7 +79,10 @@ content.append(instructions); */ async function run() { // Init Cornerstone and related libraries - await initDemo(); + const config = (window as any).IS_TILED + ? { core: { renderingEngineMode: 'tiled' } } + : {}; + await initDemo(config); const toolGroupId = 'STACK_TOOL_GROUP_ID'; diff --git a/packages/tools/examples/volumeCroppingTool/index.ts b/packages/tools/examples/volumeCroppingTool/index.ts new file mode 100644 index 0000000000..70de70e81f --- /dev/null +++ b/packages/tools/examples/volumeCroppingTool/index.ts @@ -0,0 +1,407 @@ +import type { Types, VolumeViewport3D } from '@cornerstonejs/core'; +import { + RenderingEngine, + Enums, + setVolumesForViewports, + volumeLoader, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + setCtTransferFunctionForVolumeActor, + addDropdownToToolbar, + getLocalUrl, + addToggleButtonToToolbar, +} from '../../../../utils/demo/helpers'; + +import * as cornerstoneTools from '@cornerstonejs/tools'; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +const { + ToolGroupManager, + Enums: csToolsEnums, + VolumeCroppingTool, + VolumeCroppingControlTool, + TrackballRotateTool, + ZoomTool, + PanTool, + OrientationMarkerTool, + StackScrollTool, + CrosshairsTool, +} = cornerstoneTools; + +const { MouseBindings } = csToolsEnums; +const { ViewportType } = Enums; + +// Define a unique id for the volume +const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix +const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use +const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id +const toolGroupId = 'MY_TOOLGROUP_ID'; +const toolGroupIdVRT = 'MY_TOOLGROUP_VRT_ID'; + +const viewportId1 = 'CT_AXIAL'; +const viewportId2 = 'CT_CORONAL'; +const viewportId3 = 'CT_SAGITTAL'; +const viewportId4 = 'CT_3D_VOLUME'; // New 3D volume viewport +const viewportIds = [viewportId1, viewportId2, viewportId3, viewportId4]; + +// Add dropdown to toolbar to select number of orthographic viewports (reloads page with URL param) +addDropdownToToolbar({ + labelText: 'Number of Orthographic Viewports', + options: { + values: [1, 2, 3], + defaultValue: getNumViewportsFromUrl(), + }, + onSelectedValueChange: (selectedValue) => { + const url = new URL(window.location.href); + url.searchParams.set('numViewports', selectedValue); + window.location.href = url.toString(); + }, +}); +const renderingEngineId = 'myRenderingEngine'; + +///////////////////////////////////////// +// ======== Set up page ======== // +setTitleAndDescription( + 'Volume Cropping', + 'Here we demonstrate how to crop a 3D volume with 6 clipping planes aligned on the x,y and z axes.' +); + +const size = '400px'; +const content = document.getElementById('content'); +const viewportGrid = document.createElement('div'); + +viewportGrid.style.display = 'flex'; +viewportGrid.style.flexDirection = 'row'; +viewportGrid.style.width = '100%'; +viewportGrid.style.height = '800px'; + +// Create elements for the viewports +const element1 = document.createElement('div'); // Axial +const element2 = document.createElement('div'); // Sagittal +const element3 = document.createElement('div'); // Coronal +const element4 = document.createElement('div'); // 3D Volume + +// Create a container for the right side viewports +const rightViewportsContainer = document.createElement('div'); +rightViewportsContainer.style.display = 'flex'; +rightViewportsContainer.style.flexDirection = 'column'; +rightViewportsContainer.style.width = '20%'; +rightViewportsContainer.style.height = '100%'; + +// Set styles for the 2D viewports (stacked vertically on the right) +element1.style.width = '100%'; +element1.style.height = '33.33%'; +element1.style.minHeight = '200px'; + +element2.style.width = '100%'; +element2.style.height = '33.33%'; +element2.style.minHeight = '200px'; + +element3.style.width = '100%'; +element3.style.height = '33.33%'; +element3.style.minHeight = '200px'; + +// Set styles for the 3D viewport (on the left) +element4.style.width = '75%'; +element4.style.height = '100%'; +element4.style.minHeight = '600px'; +element4.style.position = 'relative'; + +// Disable right click context menu so we can have right click tools +element1.oncontextmenu = (e) => e.preventDefault(); +element2.oncontextmenu = (e) => e.preventDefault(); +element3.oncontextmenu = (e) => e.preventDefault(); +element4.oncontextmenu = (e) => e.preventDefault(); + +// Add elements to the viewport grid +// First add the 3D viewport on the left +viewportGrid.appendChild(element4); + +// Add the 2D viewports stacked vertically on the right +rightViewportsContainer.appendChild(element1); +rightViewportsContainer.appendChild(element2); +rightViewportsContainer.appendChild(element3); +viewportGrid.appendChild(rightViewportsContainer); + +content.appendChild(viewportGrid); + +const instructions = document.createElement('p'); +instructions.innerText = ` + Basic controls: + - Click/Drag the spheres in VRT or reference lines in the orthographic viewports. + - Rotate , pan or zoom the 3D viewport using the mouse. + - Use the scroll wheel to scroll through the slices in the orthographic viewports. + `; + +content.append(instructions); + +addToggleButtonToToolbar({ + title: 'Toggle 3D handles', + defaultToggle: false, + onClick: (toggle) => { + // Get the tool group for the 3D viewport + const toolGroupVRT = + cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupIdVRT); + // Get the VolumeCroppingTool instance from the tool group + const croppingTool = toolGroupVRT.getToolInstance('VolumeCropping'); + // Call setHandlesVisible on the tool instance + if (croppingTool && typeof croppingTool.setHandlesVisible === 'function') { + croppingTool.setHandlesVisible(!croppingTool.getHandlesVisible()); + } + }, +}); + +addToggleButtonToToolbar({ + title: 'Toggle Cropping Planes', + defaultToggle: false, + onClick: (toggle) => { + // Get the tool group for the 3D viewport + const toolGroupVRT = + cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupIdVRT); + // Get the VolumeCroppingTool instance from the tool group + const croppingTool = toolGroupVRT.getToolInstance('VolumeCropping'); + // Call setClippingPlanesVisible on the tool instance + if ( + croppingTool && + typeof croppingTool.setClippingPlanesVisible === 'function' + ) { + croppingTool.setClippingPlanesVisible( + !croppingTool.getClippingPlanesVisible() + ); + } + }, +}); + +const viewportColors = { + [viewportId1]: 'rgb(200, 0, 0)', + [viewportId2]: 'rgb(0, 200, 0)', + [viewportId3]: 'rgb(200, 200, 0)', + [viewportId4]: 'rgb(0, 200, 200)', +}; + +function getReferenceLineColor(viewportId) { + return viewportColors[viewportId]; +} + +const viewportReferenceLineControllable = [ + viewportId1, + viewportId2, + viewportId3, +]; + +/** + * Get the number of orthographic viewports from the URL (?numViewports=1|2|3) + */ +function getNumViewportsFromUrl() { + const params = new URLSearchParams(window.location.search); + const value = params.get('numViewports'); + const num = Number(value); + if ([1, 2, 3].includes(num)) { + return num; + } + return 3; // default +} + +/** + * Runs the demo with a configurable number of orthographic viewports + */ +async function run(numViewports = getNumViewportsFromUrl()) { + await initDemo(); + + cornerstoneTools.addTool(VolumeCroppingTool); + cornerstoneTools.addTool(VolumeCroppingControlTool); + cornerstoneTools.addTool(TrackballRotateTool); + cornerstoneTools.addTool(ZoomTool); + cornerstoneTools.addTool(PanTool); + cornerstoneTools.addTool(OrientationMarkerTool); + cornerstoneTools.addTool(StackScrollTool); + cornerstoneTools.addTool(CrosshairsTool); + + const imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + SeriesInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + wadoRsRoot: + getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + }); + + const volume = await volumeLoader.createAndCacheVolume(volumeId, { + imageIds, + }); + + const renderingEngine = new RenderingEngine(renderingEngineId); + + // Only include the requested number of orthographic viewports + const orthographicViewports = [ + { + viewportId: viewportId1, + type: ViewportType.ORTHOGRAPHIC, + element: element1, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + background: [0, 0, 0], + }, + }, + { + viewportId: viewportId2, + type: ViewportType.ORTHOGRAPHIC, + element: element2, + defaultOptions: { + orientation: Enums.OrientationAxis.CORONAL, + background: [0, 0, 0], + }, + }, + { + viewportId: viewportId3, + type: ViewportType.ORTHOGRAPHIC, + element: element3, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + background: [0, 0, 0], + }, + }, + ].slice(0, numViewports); + + // Show/hide orthographic viewport elements based on numViewports + [element1, element2, element3].forEach((el, idx) => { + if (idx < numViewports) { + el.style.display = 'block'; + el.style.height = `${100 / numViewports}%`; + } else { + el.style.display = 'none'; + } + }); + + // Always set viewport4 (3D viewport) orientation to CORONAL + const viewportInputArray = [ + ...orthographicViewports, + { + viewportId: viewportId4, + type: ViewportType.VOLUME_3D, + element: element4, + defaultOptions: { + background: [0, 0, 0], + orientation: Enums.OrientationAxis.CORONAL, + }, + }, + ]; + + renderingEngine.setViewports(viewportInputArray); + + volume.load(); + + // Only set volumes for the active viewport IDs + const activeViewportIds = [ + ...orthographicViewports.map((vp) => vp.viewportId), + viewportId4, + ]; + await setVolumesForViewports( + renderingEngine, + [ + { + volumeId, + callback: setCtTransferFunctionForVolumeActor, + }, + ], + activeViewportIds + ); + + // Tool group for orthographic viewports + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + orthographicViewports.forEach((vp) => { + toolGroup.addViewport(vp.viewportId, renderingEngineId); + }); + + toolGroup.addTool(VolumeCroppingControlTool.toolName, { + getReferenceLineColor, + viewportIndicators: true, + }); + toolGroup.setToolActive(VolumeCroppingControlTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, + }, + ], + }); + toolGroup.addTool(StackScrollTool.toolName, { + viewportIndicators: true, + }); + toolGroup.setToolActive(StackScrollTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Wheel, + }, + { + mouseButton: MouseBindings.Secondary, + }, + ], + }); + + // Tool group for 3D viewport + const toolGroupVRT = ToolGroupManager.createToolGroup(toolGroupIdVRT); + toolGroupVRT.addTool(ZoomTool.toolName); + toolGroupVRT.setToolActive(ZoomTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Secondary, + }, + ], + }); + toolGroupVRT.addTool(PanTool.toolName); + toolGroupVRT.setToolActive(PanTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Auxiliary, + }, + ], + }); + toolGroupVRT.addTool(OrientationMarkerTool.toolName, { + overlayMarkerType: + OrientationMarkerTool.OVERLAY_MARKER_TYPES.ANNOTATED_CUBE, + }); + // toolGroupVRT.setToolActive(OrientationMarkerTool.toolName); + + const isMobile = window.matchMedia('(any-pointer:coarse)').matches; + const viewport = renderingEngine.getViewport(viewportId4) as VolumeViewport3D; + renderingEngine.renderViewports(activeViewportIds); + await setVolumesForViewports( + renderingEngine, + [{ volumeId }], + [viewportId4] + ).then(() => { + viewport.setProperties({ + preset: 'CT-Bone', + }); + toolGroupVRT.addViewport(viewportId4, renderingEngineId); + toolGroupVRT.addTool(VolumeCroppingTool.toolName, { + sphereRadius: 7, + sphereColors: { + x: [1, 1, 0], + y: [0, 1, 0], + z: [1, 0, 0], + corners: [0, 0, 1], + }, + showCornerSpheres: true, + initialCropFactor: 0.2, + }); + toolGroupVRT.setToolActive(VolumeCroppingTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, + }, + ], + }); + viewport.setZoom(1.2); + viewport.render(); + }); +} + +run(); diff --git a/packages/tools/examples/volumeProgressive/index.ts b/packages/tools/examples/volumeProgressive/index.ts index 4072f1cf27..f0eb4b884a 100644 --- a/packages/tools/examples/volumeProgressive/index.ts +++ b/packages/tools/examples/volumeProgressive/index.ts @@ -173,7 +173,7 @@ content.append(instructions); * Generate the various configurations by using the options on static DICOMweb: * Base lossy/full thumbnail configuration for HTJ2K: * ``` - * mkdicomweb create -t jhc --recompress true --alternate jhc --alternate-name lossy d:\src\viewer-testdata\dcm\Juno + * mkdicomweb create -t jhc --recompress true --alternate jhc --alternate-name lossy /src/viewer-testdata/dcm/Juno * ``` * * JLS and JLS thumbnails: diff --git a/packages/tools/examples/webWorker/index.ts b/packages/tools/examples/webWorker/index.ts index bf853a2d5c..485d3bfcab 100644 --- a/packages/tools/examples/webWorker/index.ts +++ b/packages/tools/examples/webWorker/index.ts @@ -79,16 +79,14 @@ workerManager.registerWorker('test-worker', workerFn, options); addButtonToToolbar({ title: 'Run a heavy task off the main thread', onClick: () => { - document.getElementById( - 'sleep-result' - ).innerText = `Running a heavy task off the main thread for 5 seconds...`; + document.getElementById('sleep-result').innerText = + `Running a heavy task off the main thread for 5 seconds...`; workerManager .executeTask('test-worker', 'sleep', { time: 5000 }) .then((result) => { - document.getElementById( - 'sleep-result' - ).innerText = `Heavy task completed!`; + document.getElementById('sleep-result').innerText = + `Heavy task completed!`; }) .catch((error) => { console.error('error', error); @@ -99,16 +97,14 @@ addButtonToToolbar({ addButtonToToolbar({ title: 'Calculate Fibonacci number 43', onClick: () => { - document.getElementById( - 'fib-result' - ).innerText = `Calculating Fibonacci number 43...`; + document.getElementById('fib-result').innerText = + `Calculating Fibonacci number 43...`; workerManager .executeTask('test-worker', 'fib', { value: 43 }) .then((result) => { - document.getElementById( - 'fib-result' - ).innerText = `Fibonacci number 43 is ${result}`; + document.getElementById('fib-result').innerText = + `Fibonacci number 43 is ${result}`; }) .catch((error) => { console.error('error', error); diff --git a/packages/tools/examples/wsiAnnotationTools/index.ts b/packages/tools/examples/wsiAnnotationTools/index.ts index cd02abbcfa..5079889f2f 100644 --- a/packages/tools/examples/wsiAnnotationTools/index.ts +++ b/packages/tools/examples/wsiAnnotationTools/index.ts @@ -150,12 +150,11 @@ async function run() { // Get Cornerstone imageIds and fetch metadata const wadoRsRoot = - getLocalUrl() || 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb'; + getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb'; const client = new api.DICOMwebClient({ url: wadoRsRoot }); const imageIds = await createImageIdsAndCacheMetaData({ - StudyInstanceUID: '1.2.276.1.74.1.2.132733202464108492637644434464108492', - SeriesInstanceUID: - '2.16.840.1.113883.3.8467.132733202477512857637644434477512857', + StudyInstanceUID: '2.25.269859997690759739055099378767846712697', + SeriesInstanceUID: '2.25.274641717059635090989922952756233538416', client, wadoRsRoot, convertMultiframe: false, diff --git a/packages/tools/package.json b/packages/tools/package.json index feeb00ac7c..3a29e0b966 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -94,7 +94,7 @@ "clean": "rm -rf node_modules/.cache/storybook && shx rm -rf dist", "clean:deep": "yarn run clean && shx rm -rf node_modules", "dev": "tsc --project ./tsconfig.json --watch", - "format-check": "npx eslint ./src --quiet", + "lint": "oxlint .", "api-check": "api-extractor --debug run ", "prepublishOnly": "yarn run build", "webpack:watch": "webpack --mode development --progress --watch --config ./.webpack/webpack.dev.js" diff --git a/packages/tools/src/enums/Events.ts b/packages/tools/src/enums/Events.ts index 6fce4fd7c2..f32a532921 100644 --- a/packages/tools/src/enums/Events.ts +++ b/packages/tools/src/enums/Events.ts @@ -35,6 +35,10 @@ enum Events { CROSSHAIR_TOOL_CENTER_CHANGED = 'CORNERSTONE_TOOLS_CROSSHAIR_TOOL_CENTER_CHANGED', + VOLUMECROPPINGCONTROL_TOOL_CHANGED = 'CORNERSTONE_TOOLS_VOLUMECROPPINGCONTROL_TOOL_CHANGED', + + VOLUMECROPPING_TOOL_CHANGED = 'CORNERSTONE_TOOLS_VOLUMECROPPING_TOOL_CHANGED', + /////////////////////////////////////// // Annotations /////////////////////////////////////// diff --git a/packages/tools/src/eventListeners/segmentation/imageChangeEventListener.ts b/packages/tools/src/eventListeners/segmentation/imageChangeEventListener.ts index 0ad67bf645..e68a32d321 100644 --- a/packages/tools/src/eventListeners/segmentation/imageChangeEventListener.ts +++ b/packages/tools/src/eventListeners/segmentation/imageChangeEventListener.ts @@ -139,6 +139,7 @@ function _imageChangeEventListener(evt) { return; } + let shouldTriggerSegmentationRender = false; const updateSegmentationActor = (derivedImageId) => { const derivedImage = cache.getImage(derivedImageId); @@ -210,7 +211,7 @@ function _imageChangeEventListener(evt) { }, ]); - triggerSegmentationRender(viewportId); + shouldTriggerSegmentationRender = true; return; } else { // if actor found @@ -234,6 +235,11 @@ function _imageChangeEventListener(evt) { }; derivedImageIds.forEach(updateSegmentationActor); + // if one or more actors were added to the viewport + // we need to trigger a segmentation render + if (shouldTriggerSegmentationRender) { + triggerSegmentationRender(viewportId); + } viewport.render(); diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index fb449d1e54..670f962cff 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -28,6 +28,8 @@ import { AnnotationDisplayTool, PanTool, TrackballRotateTool, + VolumeCroppingTool, + VolumeCroppingControlTool, DragProbeTool, WindowLevelTool, ZoomTool, @@ -106,6 +108,8 @@ export { PanTool, SegmentBidirectionalTool, TrackballRotateTool, + VolumeCroppingTool, + VolumeCroppingControlTool, DragProbeTool, WindowLevelTool, WindowLevelRegionTool, diff --git a/packages/tools/src/stateManagement/annotation/annotationVisibility.ts b/packages/tools/src/stateManagement/annotation/annotationVisibility.ts index 7bb17eb317..3d0bb4c4f8 100644 --- a/packages/tools/src/stateManagement/annotation/annotationVisibility.ts +++ b/packages/tools/src/stateManagement/annotation/annotationVisibility.ts @@ -98,6 +98,8 @@ function hide( deselectAnnotation(annotationUID); } detail.lastHidden.push(annotationUID); + const annotation = getAnnotation(annotationUID); + annotation.isVisible = false; } } diff --git a/packages/tools/src/stateManagement/segmentation/SegmentationRenderingEngine.ts b/packages/tools/src/stateManagement/segmentation/SegmentationRenderingEngine.ts index 01fa81bb23..54a2ac4865 100644 --- a/packages/tools/src/stateManagement/segmentation/SegmentationRenderingEngine.ts +++ b/packages/tools/src/stateManagement/segmentation/SegmentationRenderingEngine.ts @@ -40,6 +40,7 @@ const planarContourToolName = PlanarFreehandContourSegmentationTool.toolName; */ class SegmentationRenderingEngine { private _needsRender: Set = new Set(); + private _pendingRenderQueue: string[][] = []; private _animationFrameSet = false; private _animationFrameHandle: number | null = null; public hasBeenDestroyed: boolean; @@ -116,11 +117,15 @@ class SegmentationRenderingEngine { } private _setViewportsToBeRenderedNextFrame(viewportIds: string[]) { + if (this._animationFrameSet) { + // If a render is already scheduled, queue this set for after the current one + this._pendingRenderQueue.push(viewportIds); + return; + } // Add the viewports to the set of flagged viewports viewportIds.forEach((viewportId) => { this._needsRender.add(viewportId); }); - // Render any flagged viewports this._render(); } @@ -156,6 +161,14 @@ class SegmentationRenderingEngine { // Allow RAF to be called again this._animationFrameSet = false; this._animationFrameHandle = null; + + // If there are pending viewportId sets, schedule the next one + if (this._pendingRenderQueue.length > 0) { + const nextViewportIds = this._pendingRenderQueue.shift(); + if (nextViewportIds && nextViewportIds.length > 0) { + this._setViewportsToBeRenderedNextFrame(nextViewportIds); + } + } }; _triggerRender(viewportId?: string) { diff --git a/packages/tools/src/stateManagement/segmentation/config/segmentationVisibility.ts b/packages/tools/src/stateManagement/segmentation/config/segmentationVisibility.ts index ab88afb9a3..a7839ea0cf 100644 --- a/packages/tools/src/stateManagement/segmentation/config/segmentationVisibility.ts +++ b/packages/tools/src/stateManagement/segmentation/config/segmentationVisibility.ts @@ -97,6 +97,10 @@ function setSegmentIndexVisibility( } representations.forEach((representation) => { + // If the representation does not have segments, we cannot set visibility + if (!representation.segments || !representation.segments[segmentIndex]) { + return; + } representation.segments[segmentIndex].visible = visibility; }); diff --git a/packages/tools/src/stateManagement/segmentation/config/styleHelpers.ts b/packages/tools/src/stateManagement/segmentation/config/styleHelpers.ts index e1cffd0b0e..5c359a462b 100644 --- a/packages/tools/src/stateManagement/segmentation/config/styleHelpers.ts +++ b/packages/tools/src/stateManagement/segmentation/config/styleHelpers.ts @@ -24,10 +24,10 @@ type StyleForType = T extends SegmentationRepresentations.Labelmap ? LabelmapStyle : T extends SegmentationRepresentations.Contour - ? ContourStyle - : T extends SegmentationRepresentations.Surface - ? SurfaceStyle - : never; + ? ContourStyle + : T extends SegmentationRepresentations.Surface + ? SurfaceStyle + : never; /** * Get the style for a given segmentation representation. diff --git a/packages/tools/src/stateManagement/segmentation/helpers/normalizeSegmentationInput.ts b/packages/tools/src/stateManagement/segmentation/helpers/normalizeSegmentationInput.ts index ca416c3c57..cfae91f50e 100644 --- a/packages/tools/src/stateManagement/segmentation/helpers/normalizeSegmentationInput.ts +++ b/packages/tools/src/stateManagement/segmentation/helpers/normalizeSegmentationInput.ts @@ -84,6 +84,11 @@ function normalizeSegments( } as Segment; normalizedSegments[segmentIndex] = normalizedSegment; }); + } else if (type === SegmentationRepresentations.Contour) { + normalizeContourSegments( + normalizedSegments, + data as ContourSegmentationData + ); } else if (type === SegmentationRepresentations.Surface) { normalizeSurfaceSegments( normalizedSegments, @@ -96,6 +101,25 @@ function normalizeSegments( return normalizedSegments; } +/** + * Normalize surface segmentation segments using geometry data from cache. + * @param normalizedSegments - The object to store normalized segments. + * @param surfaceData - SurfaceSegmentationData to extract geometry information. + */ +function normalizeContourSegments( + normalizedSegments: { [key: number]: Segment }, + contourData: ContourSegmentationData +): void { + const { geometryIds } = contourData; + geometryIds?.forEach((geometryId) => { + const geometry = cache.getGeometry(geometryId) as Types.IGeometry; + if (geometry?.data) { + const { segmentIndex } = geometry.data as Types.IContourSet; + normalizedSegments[segmentIndex] = { segmentIndex } as Segment; + } + }); +} + /** * Normalize surface segmentation segments using geometry data from cache. * @param normalizedSegments - The object to store normalized segments. diff --git a/packages/tools/src/stateManagement/segmentation/internalAddSegmentationRepresentation.ts b/packages/tools/src/stateManagement/segmentation/internalAddSegmentationRepresentation.ts index a5d90dd1c7..773ec0ce07 100644 --- a/packages/tools/src/stateManagement/segmentation/internalAddSegmentationRepresentation.ts +++ b/packages/tools/src/stateManagement/segmentation/internalAddSegmentationRepresentation.ts @@ -1,4 +1,3 @@ -import type { Types } from '@cornerstonejs/core'; import type { RenderingConfig, RepresentationPublicInput, @@ -9,6 +8,7 @@ import { SegmentationRepresentations } from '../../enums'; import { triggerSegmentationModified } from './triggerSegmentationEvents'; import { addColorLUT } from './addColorLUT'; import { defaultSegmentationStateManager } from './SegmentationStateManager'; +import { getActiveSegmentIndex, setActiveSegmentIndex } from './segmentIndex'; function internalAddSegmentationRepresentation( viewportId: string, @@ -29,6 +29,20 @@ function internalAddSegmentationRepresentation( renderingConfig ); + // If no active segment index is set, default to the first segment + if (!getActiveSegmentIndex(segmentationId)) { + const segmentation = + defaultSegmentationStateManager.getSegmentation(segmentationId); + + if (segmentation) { + const segmentKeys = Object.keys(segmentation.segments); + if (segmentKeys.length > 0) { + const firstSegmentIndex = segmentKeys.map((k) => Number(k)).sort()[0]; + setActiveSegmentIndex(segmentationId, firstSegmentIndex); + } + } + } + if (representationInput.type === SegmentationRepresentations.Contour) { triggerAnnotationRenderForViewportIds([viewportId]); } diff --git a/packages/tools/src/stateManagement/segmentation/utilities/getAnnotationsUIDMapFromSegmentation.ts b/packages/tools/src/stateManagement/segmentation/utilities/getAnnotationsUIDMapFromSegmentation.ts index c166279e88..ac10804d5d 100644 --- a/packages/tools/src/stateManagement/segmentation/utilities/getAnnotationsUIDMapFromSegmentation.ts +++ b/packages/tools/src/stateManagement/segmentation/utilities/getAnnotationsUIDMapFromSegmentation.ts @@ -5,7 +5,7 @@ import { getSegmentation } from '../getSegmentation'; * Retrieves the annotationUIDsMap for a given segmentation ID, if available. * * @param segmentationId - The segmentation ID - * @returns The annotationUIDsMap (Map>) or undefined + * @returns The annotationUIDsMap (Map of segmentIndex or Set of annotationUID) or undefined */ export function getAnnotationsUIDMapFromSegmentation(segmentationId: string) { const segmentation = getSegmentation(segmentationId); diff --git a/packages/tools/src/store/ToolGroupManager/ToolGroup.ts b/packages/tools/src/store/ToolGroupManager/ToolGroup.ts index 9c561120bc..8537b8aba8 100644 --- a/packages/tools/src/store/ToolGroupManager/ToolGroup.ts +++ b/packages/tools/src/store/ToolGroupManager/ToolGroup.ts @@ -225,10 +225,7 @@ export default class ToolGroup { // Handle the newly added viewport's mouse cursor const toolName = this.getActivePrimaryMouseButtonTool(); - const runtimeSettings = Settings.getRuntimeSettings(); - if (runtimeSettings.get('useCursors')) { - this.setViewportsCursorByToolName(toolName); - } + this.setViewportsCursorByToolName(toolName); const eventDetail = { toolGroupId: this.id, @@ -400,19 +397,16 @@ export default class ToolGroup { this.toolOptions[toolName] = toolOptions; this._toolInstances[toolName].mode = Active; - // reset the mouse cursor if tool has left click binding - const runtimeSettings = Settings.getRuntimeSettings(); - const useCursor = runtimeSettings.get('useCursors'); - - if (this._hasMousePrimaryButtonBinding(toolBindingsOptions) && useCursor) { - this.setViewportsCursorByToolName(toolName); - } else { + if (!this._hasMousePrimaryButtonBinding(toolBindingsOptions)) { // reset to default cursor only if there is no other tool with primary binding const activeToolIdentifier = this.getActivePrimaryMouseButtonTool(); - if (!activeToolIdentifier && useCursor) { + if (!activeToolIdentifier) { const cursor = MouseCursor.getDefinedCursor('default'); this._setCursorForViewports(cursor); } + } else { + // reset the mouse cursor if tool has left click binding + this.setViewportsCursorByToolName(toolName); } // if it is a primary tool binding, we should store it as the previous primary tool @@ -663,6 +657,11 @@ export default class ToolGroup { } _setCursorForViewports(cursor: MouseCursor): void { + const runtimeSettings = Settings.getRuntimeSettings(); + if (!runtimeSettings.get('useCursors')) { + return; + } + this.viewportsInfo.forEach(({ renderingEngineId, viewportId }) => { const enabledElement = getEnabledElementByIds( viewportId, diff --git a/packages/tools/src/tools/CrosshairsTool.ts b/packages/tools/src/tools/CrosshairsTool.ts index 34662960d4..c122fb0ca5 100644 --- a/packages/tools/src/tools/CrosshairsTool.ts +++ b/packages/tools/src/tools/CrosshairsTool.ts @@ -43,6 +43,7 @@ import liangBarksyClip from '../utilities/math/vec2/liangBarksyClip'; import * as lineSegment from '../utilities/math/line'; import type { Annotation, + AnnotationData, Annotations, EventTypes, ToolHandle, @@ -56,18 +57,20 @@ import triggerAnnotationRenderForViewportIds from '../utilities/triggerAnnotatio const { RENDERING_DEFAULTS } = CONSTANTS; -interface CrosshairsAnnotation extends Annotation { - data: { - handles: { - rotationPoints: Types.Point3[]; // rotation handles, used for rotation interactions - slabThicknessPoints: Types.Point3[]; // slab thickness handles, used for setting the slab thickness - activeOperation: number | null; // 0 translation, 1 rotation handles, 2 slab thickness handles - toolCenter: Types.Point3; - }; - activeViewportIds: string[]; // a list of the viewport ids connected to the reference lines being translated - viewportId: string; +export type CrosshairsAnnotationData = AnnotationData & { + handles: { + rotationPoints: Types.Point3[]; // rotation handles, used for rotation interactions + slabThicknessPoints: Types.Point3[]; // slab thickness handles, used for setting the slab thickness + activeOperation: number | null; // 0 translation, 1 rotation handles, 2 slab thickness handles + toolCenter: Types.Point3; }; -} + activeViewportIds: string[]; // a list of the viewport ids connected to the reference lines being translated + viewportId: string; +}; + +export type CrosshairsAnnotation = Annotation & { + data: CrosshairsAnnotationData; +}; function defaultReferenceLineColor() { return 'rgb(0, 200, 0)'; @@ -91,8 +94,6 @@ const OPERATION = { SLAB: 3, }; -const EPSILON = 1e-3; - /** * CrosshairsTool is a tool that provides reference lines between different viewports * of a toolGroup. Using crosshairs, you can jump to a specific location in one @@ -144,6 +145,14 @@ class CrosshairsTool extends AnnotationTool { // the reference lines will not be rendered. This is only used when // having 3 viewports in the toolGroup. referenceLinesCenterGapRadius: 20, + // The ratio is a fraction of the minimum canvas dimension (width or height). + // For example, if referenceLinesCenterGapRatio is set to 0.05, the gap will be 5% of the smallest side of the canvas. + // If set to 1, the gap will be equal to the minimum canvas dimension (which would likely hide the crosshairs). + // referenceLinesCenterGapRatio: null|undefined → gap is referenceLinesCenterGapRadius (default: 20 pixels) + // referenceLinesCenterGapRatio: 0.05 → gap is 5% of the canvas min dimension + // referenceLinesCenterGapRatio: 0.1 → gap is 10% of the canvas min dimension + // referenceLinesCenterGapRatio: 1 → gap is 100% (not recommended) + referenceLinesCenterGapRatio: null, // actorUIDs for slabThickness application, if not defined, the slab thickness // will be applied to all actors of the viewport filterActorUIDsToSetSlabThickness: [], @@ -153,6 +162,7 @@ class CrosshairsTool extends AnnotationTool { enabled: false, opacity: 0.8, handleRadius: 9, + referenceLinesCenterGapRatio: 0.05, }, }, } @@ -956,7 +966,17 @@ class CrosshairsTool extends AnnotationTool { canvasMinDimensionLength * 0.2 ); const canvasVectorFromCenterStart = vec2.create(); - const centerGap = this.configuration.referenceLinesCenterGapRadius; + // Calculate center gap using ratio if provided, else fallback to pixel value + const mobileConfig = this.configuration.mobile; + const { referenceLinesCenterGapRatio } = mobileConfig?.enabled + ? mobileConfig + : this.configuration; + + const centerGap = + referenceLinesCenterGapRatio > 0 + ? canvasMinDimensionLength * referenceLinesCenterGapRatio + : this.configuration.referenceLinesCenterGapRadius; + vec2.scale( canvasVectorFromCenterStart, canvasUnitVectorFromCenter, @@ -2268,17 +2288,21 @@ class CrosshairsTool extends AnnotationTool { const viewportDraggableRotatable = this._getReferenceLineDraggableRotatable(otherViewport.id); if (!viewportDraggableRotatable) { - const { rotationPoints } = this.editData.annotation.data.handles; + const { rotationPoints } = (( + this.editData.annotation.data + )).handles; // Todo: what is a point uid? - // @ts-expect-error const otherViewportRotationPoints = rotationPoints.filter( + // @ts-expect-error (point) => point[1].uid === otherViewport.id ); if (otherViewportRotationPoints.length === 2) { const point1 = viewport.canvasToWorld( + // @ts-expect-error otherViewportRotationPoints[0][3] ); const point2 = viewport.canvasToWorld( + // @ts-expect-error otherViewportRotationPoints[1][3] ); vtkMath.add(point1, point2, currentCenter); diff --git a/packages/tools/src/tools/OrientationMarkerTool.ts b/packages/tools/src/tools/OrientationMarkerTool.ts index af8a5c902f..a270cd4b04 100644 --- a/packages/tools/src/tools/OrientationMarkerTool.ts +++ b/packages/tools/src/tools/OrientationMarkerTool.ts @@ -257,7 +257,8 @@ class OrientationMarkerTool extends BaseTool { const renderWindow = viewport .getRenderingEngine() - .offscreenMultiRenderWindow.getRenderWindow(); + .getOffscreenMultiRenderWindow(viewport.id) + .getRenderWindow(); renderWindow.render(); viewport.getRenderingEngine().render(); @@ -314,7 +315,8 @@ class OrientationMarkerTool extends BaseTool { const renderer = viewport.getRenderer(); const renderWindow = viewport .getRenderingEngine() - .offscreenMultiRenderWindow.getRenderWindow(); + .getOffscreenMultiRenderWindow(viewportId) + .getRenderWindow(); const { enabled, diff --git a/packages/tools/src/tools/OverlayGridTool.ts b/packages/tools/src/tools/OverlayGridTool.ts index 612900296a..ce0d951910 100644 --- a/packages/tools/src/tools/OverlayGridTool.ts +++ b/packages/tools/src/tools/OverlayGridTool.ts @@ -28,12 +28,12 @@ import AnnotationDisplayTool from './base/AnnotationDisplayTool'; const { EPSILON } = CONSTANTS; -export interface OverlayGridAnnotation extends Annotation { +export type OverlayGridAnnotation = Annotation & { data: { viewportData: Map; pointSets: Array; }; -} +}; /** * @public @@ -228,13 +228,12 @@ class OverlayGridTool extends AnnotationDisplayTool { ); const pointSets = annotation.data.pointSets; - const viewportData = annotation.data.viewportData; + const viewportData = (annotation).data.viewportData; for (let i = 0; i < sourceImageIds.length; i++) { // check if pointSets for the imageId was calculated. If not calculate and store const { pointSet1, pointSet2 } = pointSets[i]; const targetData = - // @ts-expect-error viewportData.get(targetViewport.id) || this.initializeViewportData(viewportData, targetViewport.id); diff --git a/packages/tools/src/tools/SegmentationIntersectionTool.ts b/packages/tools/src/tools/SegmentationIntersectionTool.ts index c31ef983c5..21c8e7353f 100644 --- a/packages/tools/src/tools/SegmentationIntersectionTool.ts +++ b/packages/tools/src/tools/SegmentationIntersectionTool.ts @@ -8,18 +8,27 @@ import { import { drawPath } from '../drawingSvg'; import { getToolGroup } from '../store/ToolGroupManager'; import triggerAnnotationRenderForViewportIds from '../utilities/triggerAnnotationRenderForViewportIds'; -import type { PublicToolProps, ToolProps, SVGDrawingHelper } from '../types'; +import type { + PublicToolProps, + ToolProps, + SVGDrawingHelper, + Annotation, +} from '../types'; import AnnotationDisplayTool from './base/AnnotationDisplayTool'; -import type { Annotation } from '../types'; import { distanceToPoint } from '../utilities/math/point'; import { pointToString } from '../utilities/pointToString'; import { polyDataUtils } from '../utilities'; -export interface SegmentationIntersectionAnnotation extends Annotation { +export type WorldPointSet = { + worldPointsSet; + color; +}; + +export type SegmentationIntersectionAnnotation = Annotation & { data: { - actorsWorldPointsMap: Map>; + actorsWorldPointsMap: Map>; }; -} +}; class SegmentationIntersectionTool extends AnnotationDisplayTool { static toolName; @@ -112,7 +121,9 @@ class SegmentationIntersectionTool extends AnnotationDisplayTool { } const annotation = annotations[0]; const { annotationUID } = annotation; - const actorsWorldPointsMap = annotation.data.actorsWorldPointsMap; + const actorsWorldPointsMap = (( + annotation + )).data.actorsWorldPointsMap; calculateSurfaceSegmentationIntersectionsForViewport( actorsWorldPointsMap, @@ -126,7 +137,6 @@ class SegmentationIntersectionTool extends AnnotationDisplayTool { if (!actorEntry?.clippingFilter) { return; } - // @ts-expect-error const actorWorldPointMap = actorsWorldPointsMap.get(actorEntry.uid); if (!actorWorldPointMap) { return; diff --git a/packages/tools/src/tools/VolumeCroppingControlTool.ts b/packages/tools/src/tools/VolumeCroppingControlTool.ts new file mode 100644 index 0000000000..1db0f07e4d --- /dev/null +++ b/packages/tools/src/tools/VolumeCroppingControlTool.ts @@ -0,0 +1,1937 @@ +import { vec2, vec3 } from 'gl-matrix'; +import vtkMath from '@kitware/vtk.js/Common/Core/Math'; +import type vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane'; + +import { AnnotationTool } from './base'; + +import type { Types } from '@cornerstonejs/core'; +import { + getRenderingEngine, + getEnabledElementByIds, + getEnabledElement, + utilities as csUtils, + Enums, + CONSTANTS, + triggerEvent, + eventTarget, +} from '@cornerstonejs/core'; + +import { + getToolGroup, + getToolGroupForViewport, +} from '../store/ToolGroupManager'; + +import { + addAnnotation, + getAnnotations, + removeAnnotation, +} from '../stateManagement/annotation/annotationState'; + +import { + drawCircle as drawCircleSvg, + drawLine as drawLineSvg, +} from '../drawingSvg'; +import { state } from '../store/state'; +import { Events } from '../enums'; +import { getViewportIdsWithToolToRender } from '../utilities/viewportFilters'; +import { + resetElementCursor, + hideElementCursor, +} from '../cursors/elementCursor'; +import liangBarksyClip from '../utilities/math/vec2/liangBarksyClip'; + +import * as lineSegment from '../utilities/math/line'; +import type { + Annotation, + Annotations, + EventTypes, + ToolHandle, + PublicToolProps, + ToolProps, + InteractionTypes, + SVGDrawingHelper, +} from '../types'; +import { isAnnotationLocked } from '../stateManagement/annotation/annotationLocking'; +import triggerAnnotationRenderForViewportIds from '../utilities/triggerAnnotationRenderForViewportIds'; + +const { RENDERING_DEFAULTS } = CONSTANTS; + +type ReferenceLine = [ + viewport: { + id: string; + canvas?: HTMLCanvasElement; + canvasToWorld?: (...args: unknown[]) => Types.Point3; + }, + startPoint: Types.Point2, + endPoint: Types.Point2, + type: 'min' | 'max', +]; + +interface VolumeCroppingAnnotation extends Annotation { + data: { + handles: { + activeOperation: number | null; // 0 translation, 1 rotation handles, 2 slab thickness handles + toolCenter: Types.Point3; + toolCenterMin: Types.Point3; + toolCenterMax: Types.Point3; + }; + activeViewportIds: string[]; // a list of the viewport ids connected to the reference lines being translated + viewportId: string; + referenceLines: ReferenceLine[]; // set in renderAnnotation + clippingPlanes?: vtkPlane[]; // clipping planes for the viewport + clippingPlaneReferenceLines?: ReferenceLine[]; + orientation?: string; // AXIAL, CORONAL, SAGITTAL + }; + isVirtual?: boolean; + virtualNormal?: Types.Point3; +} + +function defaultReferenceLineColor() { + return 'rgb(0, 200, 0)'; +} + +function defaultReferenceLineControllable() { + return true; +} + +const OPERATION = { + DRAG: 1, + ROTATE: 2, + SLAB: 3, +}; + +/** + * VolumeCroppingControlTool provides interactive reference lines to modify the cropping planes + * of the VolumeCroppingTool. It renders reference lines across 1 to 3 orthographic viewports and allows + * users to drag these lines to adjust volume cropping boundaries in real-time. + * + * @remarks + * This tool has no standalone functionality and must be used in conjunction with a VolumeCroppingTool that will be receiving volume. + * Messaging between this tool and the main cropping tool is handled through Cornerstone events that are validated by the series instance UID of the volume. + * Therefore the tool does not need to be in the same tool group as the volume cropping tool and + * multiple cropping & control instances can be used on different series in the same display. + * + * @example + * ```typescript + * // Basic setup + * const toolGroup = ToolGroupManager.createToolGroup('myToolGroup'); + * toolGroup.addTool(VolumeCroppingControlTool.toolName); + * toolGroup.addTool(VolumeCroppingTool.toolName); + * + * // Configure with custom colors and settings + * toolGroup.setToolConfiguration(VolumeCroppingControlTool.toolName, { + * lineColors: { + * AXIAL: [1.0, 0.0, 0.0], // Red for axial views + * CORONAL: [0.0, 1.0, 0.0], // Green for coronal views + * SAGITTAL: [1.0, 1.0, 0.0], // Yellow for sagittal views + * }, + * lineWidth: 2.0, + * extendReferenceLines: true, + * viewportIndicators: true + * }); + * + * // Activate the tool + * toolGroup.setToolActive(VolumeCroppingControlTool.toolName); + * ``` + * + * @public + * @class VolumeCroppingControlTool + * @extends AnnotationTool + * + * @property {string} seriesInstanceUID - Frame of reference for the tool + * @property {VolumeCroppingAnnotation[]} _virtualAnnotations - Store virtual annotations for missing viewport orientations (e.g., CT_CORONAL when only axial and sagittal are present) + * @property {string} toolName - Static tool identifier: 'VolumeCroppingControl' + * @property {Array} sphereStates - Array of sphere state objects for 3D volume manipulation handles + * @property {number|null} draggingSphereIndex - Index of currently dragged sphere, null when not dragging + * @property {Types.Point3} toolCenter - Center point of the cropping volume in world coordinates [x, y, z] + * @property {Types.Point3} toolCenterMin - Minimum bounds of the cropping volume in world coordinates [xMin, yMin, zMin] + * @property {Types.Point3} toolCenterMax - Maximum bounds of the cropping volume in world coordinates [xMax, yMax, zMax] + * @property {Function} _getReferenceLineColor - Optional callback to determine reference line color per viewport + * @property {Function} _getReferenceLineControllable - Optional callback to determine if reference lines are interactive per viewport + * + * @configuration + * @property {boolean} viewportIndicators - Whether to show colored circle indicators in viewport corners (default: false) + * @property {Object} viewportIndicatorsConfig - Configuration for viewport indicators + * @property {number} viewportIndicatorsConfig.radius - Radius of indicator circles in pixels (default: 5) + * @property {number|null} viewportIndicatorsConfig.x - X position offset, null for auto-positioning + * @property {number|null} viewportIndicatorsConfig.y - Y position offset, null for auto-positioning + * @property {number} viewportIndicatorsConfig.xOffset - X position as fraction of viewport width (default: 0.95) + * @property {number} viewportIndicatorsConfig.yOffset - Y position as fraction of viewport height (default: 0.05) + * @property {number} viewportIndicatorsConfig.circleRadius - Circle radius as fraction of diagonal length + * @property {boolean} extendReferenceLines - Whether to extend reference lines beyond intersection points with dashed lines (default: true) + * @property {number} initialCropFactor - Initial cropping factor as percentage of volume bounds (default: 0.2) + * @property {Object} mobile - Mobile-specific configuration + * @property {boolean} mobile.enabled - Enable mobile touch interactions (default: false) + * @property {number} mobile.opacity - Opacity for mobile interactions (default: 0.8) + * @property {Object} lineColors - Color configuration for different viewport orientations + * @property {number[]} lineColors.AXIAL - RGB color array for axial viewport lines [r, g, b] (default: [1.0, 0.0, 0.0]) + * @property {number[]} lineColors.CORONAL - RGB color array for coronal viewport lines [r, g, b] (default: [0.0, 1.0, 0.0]) + * @property {number[]} lineColors.SAGITTAL - RGB color array for sagittal viewport lines [r, g, b] (default: [1.0, 1.0, 0.0]) + * @property {number[]} lineColors.UNKNOWN - RGB color array for unknown orientation lines [r, g, b] (default: [0.0, 0.0, 1.0]) + * @property {number} lineWidth - Default width of reference lines in pixels (default: 1.5) + * @property {number} lineWidthActive - Width of reference lines when actively dragging in pixels (default: 2.5) + * @property {number} activeLineWidth - Alias for lineWidthActive for backward compatibility + + * @events + * @event VOLUMECROPPINGCONTROL_TOOL_CHANGED - Fired when reference lines are dragged or tool state changes + * @event VOLUMECROPPING_TOOL_CHANGED - Listens for changes from the main VolumeCroppingTool to synchronize state + * + * + * @limitations + * - Does not function independently without VolumeCroppingTool + * - Requires volume data to be loaded before activation + * - Limited to orthogonal viewport orientations (axial, coronal, sagittal)l + */ +class VolumeCroppingControlTool extends AnnotationTool { + // Store virtual annotations (e.g., for missing orientations like CT_CORONAL) + _virtualAnnotations: VolumeCroppingAnnotation[] = []; + static toolName; + seriesInstanceUID?: string; + sphereStates: { + point: Types.Point3; + axis: string; + uid: string; + sphereSource; + sphereActor; + }[] = []; + draggingSphereIndex: number | null = null; + toolCenter: Types.Point3 = [0, 0, 0]; + toolCenterMin: Types.Point3 = [0, 0, 0]; + toolCenterMax: Types.Point3 = [0, 0, 0]; + _getReferenceLineColor?: (viewportId: string) => string; + _getReferenceLineControllable?: (viewportId: string) => boolean; + constructor( + toolProps: PublicToolProps = {}, + defaultToolProps: ToolProps = { + supportedInteractionTypes: ['Mouse'], + configuration: { + // renders a colored circle on top right of the viewports whose color + // matches the color of the reference line + viewportIndicators: false, + viewportIndicatorsConfig: { + radius: 5, + x: null, + y: null, + }, + extendReferenceLines: true, + initialCropFactor: 0.2, + mobile: { + enabled: false, + opacity: 0.8, + }, + lineColors: { + AXIAL: [1.0, 0.0, 0.0], // Red for axial + CORONAL: [0.0, 1.0, 0.0], // Green for coronal + SAGITTAL: [1.0, 1.0, 0.0], // Yellow for sagittal + UNKNOWN: [0.0, 0.0, 1.0], // Blue for unknown + }, + lineWidth: 1.5, + lineWidthActive: 2.5, + }, + } + ) { + super(toolProps, defaultToolProps); + + this._getReferenceLineColor = + toolProps.configuration?.getReferenceLineColor || + defaultReferenceLineColor; + this._getReferenceLineControllable = + toolProps.configuration?.getReferenceLineControllable || + defaultReferenceLineControllable; + + const viewportsInfo = getToolGroup(this.toolGroupId)?.viewportsInfo; + + eventTarget.addEventListener( + Events.VOLUMECROPPING_TOOL_CHANGED, + this._onSphereMoved + ); + + if (viewportsInfo && viewportsInfo.length > 0) { + const { viewportId, renderingEngineId } = viewportsInfo[0]; + const enabledElement = getEnabledElementByIds( + viewportId, + renderingEngineId + ); + const renderingEngine = getRenderingEngine(renderingEngineId); + const viewport = renderingEngine.getViewport(viewportId); + const volumeActors = viewport.getActors(); + if (!volumeActors || !volumeActors.length) { + console.warn( + `VolumeCroppingControlTool: No volume actors found in viewport ${viewportId}.` + ); + return; + } + const imageData = volumeActors[0].actor.getMapper().getInputData(); + if (imageData) { + const dimensions = imageData.getDimensions(); + const spacing = imageData.getSpacing(); + const origin = imageData.getOrigin(); + this.seriesInstanceUID = imageData.seriesInstanceUID || 'unknown'; + const cropFactor = this.configuration.initialCropFactor ?? 0.2; + this.toolCenter = [ + origin[0] + cropFactor * (dimensions[0] - 1) * spacing[0], + origin[1] + cropFactor * (dimensions[1] - 1) * spacing[1], + origin[2] + cropFactor * (dimensions[2] - 1) * spacing[2], + ]; + const maxCropFactor = 1 - cropFactor; + this.toolCenterMin = [ + origin[0] + cropFactor * (dimensions[0] - 1) * spacing[0], + origin[1] + cropFactor * (dimensions[1] - 1) * spacing[1], + origin[2] + cropFactor * (dimensions[2] - 1) * spacing[2], + ]; + this.toolCenterMax = [ + origin[0] + maxCropFactor * (dimensions[0] - 1) * spacing[0], + origin[1] + maxCropFactor * (dimensions[1] - 1) * spacing[1], + origin[2] + maxCropFactor * (dimensions[2] - 1) * spacing[2], + ]; + } + } + } + + _updateToolCentersFromViewport(viewport) { + const volumeActors = viewport.getActors(); + if (!volumeActors || !volumeActors.length) { + return; + } + const imageData = volumeActors[0].actor.getMapper().getInputData(); + if (!imageData) { + return; + } + this.seriesInstanceUID = imageData.seriesInstanceUID || 'unknown'; + const dimensions = imageData.getDimensions(); + const spacing = imageData.getSpacing(); + const origin = imageData.getOrigin(); + const cropFactor = this.configuration.initialCropFactor ?? 0.2; + const cropStart = cropFactor / 2; + const cropEnd = 1 - cropFactor / 2; + this.toolCenter = [ + origin[0] + + ((cropStart + cropEnd) / 2) * (dimensions[0] - 1) * spacing[0], + origin[1] + + ((cropStart + cropEnd) / 2) * (dimensions[1] - 1) * spacing[1], + origin[2] + + ((cropStart + cropEnd) / 2) * (dimensions[2] - 1) * spacing[2], + ]; + this.toolCenterMin = [ + origin[0] + cropStart * (dimensions[0] - 1) * spacing[0], + origin[1] + cropStart * (dimensions[1] - 1) * spacing[1], + origin[2] + cropStart * (dimensions[2] - 1) * spacing[2], + ]; + this.toolCenterMax = [ + origin[0] + cropEnd * (dimensions[0] - 1) * spacing[0], + origin[1] + cropEnd * (dimensions[1] - 1) * spacing[1], + origin[2] + cropEnd * (dimensions[2] - 1) * spacing[2], + ]; + } + /** + * Gets the camera from the viewport, and adds annotation for the viewport + * to the annotationManager. If any annotation is found in the annotationManager, it + * overwrites it. + * @param viewportInfo - The viewportInfo for the viewport + * @returns viewPlaneNormal and center of viewport canvas in world space + */ + initializeViewport = ({ + renderingEngineId, + viewportId, + }: Types.IViewportId): { + normal: Types.Point3; + point: Types.Point3; + } => { + if (!renderingEngineId || !viewportId) { + console.warn( + 'VolumeCroppingControlTool: Missing renderingEngineId or viewportId' + ); + return; + } + const enabledElement = getEnabledElementByIds( + viewportId, + renderingEngineId + ); + if (!enabledElement) { + return; + } + + const { viewport } = enabledElement; + this._updateToolCentersFromViewport(viewport); + const { element } = viewport; + const { position, focalPoint, viewPlaneNormal } = viewport.getCamera(); + + // Check if there is already annotation for this viewport + let annotations = this._getAnnotations(enabledElement); + annotations = this.filterInteractableAnnotationsForElement( + element, + annotations + ); + + if (annotations?.length) { + // If found, it will override it by removing the annotation and adding it later + removeAnnotation(annotations[0].annotationUID); + } + + // Determine orientation from camera normal, fallback to viewportId string + const orientation = this._getOrientationFromNormal( + viewport.getCamera().viewPlaneNormal + ); + + const annotation = { + highlighted: false, + metadata: { + cameraPosition: [...position], + cameraFocalPoint: [...focalPoint], + toolName: this.getToolName(), + }, + data: { + handles: { + toolCenter: this.toolCenter, + toolCenterMin: this.toolCenterMin, + toolCenterMax: this.toolCenterMax, + }, + activeOperation: null, // 0 translation, 1 rotation handles, 2 slab thickness handles + activeViewportIds: [], // a list of the viewport ids connected to the reference lines being translated + viewportId, + referenceLines: [], // set in renderAnnotation + orientation, + }, + }; + + addAnnotation(annotation, element); + return { + normal: viewPlaneNormal, + point: viewport.canvasToWorld([100, 100]), + }; + }; + + _getViewportsInfo = () => { + const viewports = getToolGroup(this.toolGroupId).viewportsInfo; + return viewports; + }; + + onSetToolInactive() { + console.debug( + `VolumeCroppingControlTool: onSetToolInactive called for tool ${this.getToolName()}` + ); + } + + onSetToolActive() { + // console.debug( + // `VolumeCroppingControlTool: onSetToolActive called for tool ${this.getToolName()}` + // ); + const viewportsInfo = this._getViewportsInfo(); + + // Check if any annotation exists before proceeding + let anyAnnotationExists = false; + for (const vpInfo of viewportsInfo) { + const enabledElement = getEnabledElementByIds( + vpInfo.viewportId, + vpInfo.renderingEngineId + ); + const annotations = this._getAnnotations(enabledElement); + if (annotations && annotations.length > 0) { + anyAnnotationExists = true; + break; + } + } + if (!anyAnnotationExists) { + this._unsubscribeToViewportNewVolumeSet(viewportsInfo); + this._subscribeToViewportNewVolumeSet(viewportsInfo); + // Request the volume cropping tool to send current planes + this._computeToolCenter(viewportsInfo); + triggerEvent(eventTarget, Events.VOLUMECROPPINGCONTROL_TOOL_CHANGED, { + toolGroupId: this.toolGroupId, + viewportsInfo: viewportsInfo, + seriesInstanceUID: this.seriesInstanceUID, + }); + } else { + // Turn off visibility of existing annotations + for (const vpInfo of viewportsInfo) { + const enabledElement = getEnabledElementByIds( + vpInfo.viewportId, + vpInfo.renderingEngineId + ); + + if (!enabledElement) { + continue; + } + + const annotations = this._getAnnotations(enabledElement); + if (annotations && annotations.length > 0) { + annotations.forEach((annotation) => { + removeAnnotation(annotation.annotationUID); + }); + } + + // Render after removing annotations to clear reference lines + enabledElement.viewport.render(); + } + } + } + + onSetToolEnabled() { + console.debug( + `VolumeCroppingControlTool: onSetToolEnabled called for tool ${this.getToolName()}` + ); + const viewportsInfo = this._getViewportsInfo(); + + //this._computeToolCenter(viewportsInfo); + } + + onSetToolDisabled() { + console.debug( + `VolumeCroppingControlTool: onSetToolDisabled called for tool ${this.getToolName()}` + ); + const viewportsInfo = this._getViewportsInfo(); + + this._unsubscribeToViewportNewVolumeSet(viewportsInfo); + + // has no value when the tool is disabled + // since viewports can change (zoom, pan, scroll) + // between disabled and enabled/active states. + // so we just remove the annotations from the state + viewportsInfo.forEach(({ renderingEngineId, viewportId }) => { + const enabledElement = getEnabledElementByIds( + viewportId, + renderingEngineId + ); + + if (!enabledElement) { + return; + } + + const annotations = this._getAnnotations(enabledElement); + if (annotations?.length) { + annotations.forEach((annotation) => { + removeAnnotation(annotation.annotationUID); + }); + } + }); + } + + resetCroppingSpheres = () => { + const viewportsInfo = this._getViewportsInfo(); + for (const viewportInfo of viewportsInfo) { + const { viewportId, renderingEngineId } = viewportInfo; + const enabledElement = getEnabledElementByIds( + viewportId, + renderingEngineId + ); + const viewport = enabledElement.viewport as Types.IVolumeViewport; + const resetPan = true; + const resetZoom = true; + const resetToCenter = true; + const resetRotation = true; + const suppressEvents = true; + viewport.resetCamera({ + resetPan, + resetZoom, + resetToCenter, + resetRotation, + suppressEvents, + }); + (viewport as Types.IVolumeViewport).resetSlabThickness(); + const { element } = viewport; + let annotations = this._getAnnotations(enabledElement); + annotations = this.filterInteractableAnnotationsForElement( + element, + annotations + ); + if (annotations.length) { + removeAnnotation(annotations[0].annotationUID); + } + viewport.render(); + } + + this._computeToolCenter(viewportsInfo); + }; + + computeToolCenter = () => { + const viewportsInfo = this._getViewportsInfo(); + }; + + _computeToolCenter = (viewportsInfo): void => { + if (!viewportsInfo || !viewportsInfo[0]) { + console.warn( + ' _computeToolCenter : No valid viewportsInfo for computeToolCenter.' + ); + return; + } + // Support any missing orientation + const orientationIds = ['AXIAL', 'CORONAL', 'SAGITTAL']; + // Get present orientations from viewportsInfo + const presentOrientations = viewportsInfo + .map((vp) => { + if (vp.renderingEngineId) { + const renderingEngine = getRenderingEngine(vp.renderingEngineId); + const viewport = renderingEngine.getViewport(vp.viewportId); + if (viewport && viewport.getCamera) { + const orientation = this._getOrientationFromNormal( + viewport.getCamera().viewPlaneNormal + ); + if (orientation) { + return orientation; + } + } + } + return null; + }) + .filter(Boolean); + + const missingOrientation = orientationIds.find( + (id) => !presentOrientations.includes(id) + ); + + // Initialize present viewports + + const presentNormals: Types.Point3[] = []; + const presentCenters: Types.Point3[] = []; + // Find present viewport infos by matching orientation, not viewportId + const presentViewportInfos = viewportsInfo.filter((vp) => { + let orientation = null; + if (vp.renderingEngineId) { + const renderingEngine = getRenderingEngine(vp.renderingEngineId); + const viewport = renderingEngine.getViewport(vp.viewportId); + if (viewport && viewport.getCamera) { + orientation = this._getOrientationFromNormal( + viewport.getCamera().viewPlaneNormal + ); + } + } + return orientation && orientationIds.includes(orientation); + }); + presentViewportInfos.forEach((vpInfo) => { + const { normal, point } = this.initializeViewport(vpInfo); + presentNormals.push(normal); + presentCenters.push(point); + }); + + // If all three orientations are present, nothing to synthesize + if (presentViewportInfos.length === 2 && missingOrientation) { + // Synthesize virtual annotation for the missing orientation + const virtualNormal: Types.Point3 = [0, 0, 0]; + vec3.cross(virtualNormal, presentNormals[0], presentNormals[1]); + vec3.normalize(virtualNormal, virtualNormal); + const virtualCenter: Types.Point3 = [ + (presentCenters[0][0] + presentCenters[1][0]) / 2, + (presentCenters[0][1] + presentCenters[1][1]) / 2, + (presentCenters[0][2] + presentCenters[1][2]) / 2, + ]; + const orientation = null; + const virtualAnnotation: VolumeCroppingAnnotation = { + highlighted: false, + metadata: { + cameraPosition: [...virtualCenter], + cameraFocalPoint: [...virtualCenter], + toolName: this.getToolName(), + }, + data: { + handles: { + activeOperation: null, + toolCenter: this.toolCenter, + toolCenterMin: this.toolCenterMin, + toolCenterMax: this.toolCenterMax, + }, + activeViewportIds: [], + viewportId: missingOrientation, + referenceLines: [], + orientation, + }, + isVirtual: true, + virtualNormal, + }; + this._virtualAnnotations = [virtualAnnotation]; + } else if (presentViewportInfos.length === 1) { + // Synthesize two virtual annotations for the two missing orientations + // Get present orientation from camera normal + let presentOrientation = null; + const vpInfo = presentViewportInfos[0]; + if (vpInfo.renderingEngineId) { + const renderingEngine = getRenderingEngine(vpInfo.renderingEngineId); + const viewport = renderingEngine.getViewport(vpInfo.viewportId); + if (viewport && viewport.getCamera) { + presentOrientation = this._getOrientationFromNormal( + viewport.getCamera().viewPlaneNormal + ); + } + } + const presentCenter = presentCenters[0]; + // Map canonical normals to orientation strings + const canonicalNormals = { + AXIAL: [0, 0, 1], + CORONAL: [0, 1, 0], + SAGITTAL: [1, 0, 0], + }; + // missingIds: AXIAL, CORONAL, SAGITTAL + const missingIds = orientationIds.filter( + (id) => id !== presentOrientation + ); + const virtualAnnotations: VolumeCroppingAnnotation[] = missingIds.map( + (orientation) => { + // Use orientation string to get canonical normal + const normal = canonicalNormals[orientation]; + const virtualAnnotation = { + highlighted: false, + metadata: { + cameraPosition: [...presentCenter], + cameraFocalPoint: [...presentCenter], + toolName: this.getToolName(), + }, + data: { + handles: { + activeOperation: null, + toolCenter: this.toolCenter, + toolCenterMin: this.toolCenterMin, + toolCenterMax: this.toolCenterMax, + }, + activeViewportIds: [], + viewportId: orientation, // Use orientation string for virtual annotation + referenceLines: [], + orientation, + }, + isVirtual: true, + virtualNormal: normal, + }; + + return virtualAnnotation; + } + ); + this._virtualAnnotations = virtualAnnotations; + } + + if (viewportsInfo && viewportsInfo.length) { + triggerAnnotationRenderForViewportIds( + viewportsInfo.map(({ viewportId }) => viewportId) + ); + } + }; + /** + * Utility function to map a camera normal to an orientation string. + * Returns 'AXIAL', 'CORONAL', 'SAGITTAL', or null if not matched. + */ + _getOrientationFromNormal(normal: Types.Point3): string | null { + if (!normal) { + return null; + } + // Canonical normals + const canonical = { + AXIAL: [0, 0, 1], + CORONAL: [0, 1, 0], + SAGITTAL: [1, 0, 0], + }; + // Use a tolerance for floating point comparison + const tol = 1e-2; + for (const [key, value] of Object.entries(canonical)) { + if ( + Math.abs(normal[0] - value[0]) < tol && + Math.abs(normal[1] - value[1]) < tol && + Math.abs(normal[2] - value[2]) < tol + ) { + return key; + } + // Also check negative direction + if ( + Math.abs(normal[0] + value[0]) < tol && + Math.abs(normal[1] + value[1]) < tol && + Math.abs(normal[2] + value[2]) < tol + ) { + return key; + } + } + return null; + } + _syncWithVolumeCroppingTool(originalClippingPlanes) { + // Sync our tool centers with the clipping plane bounds + const planes = originalClippingPlanes; + if (planes.length >= 6) { + this.toolCenterMin = [ + planes[0].origin[0], // XMIN + planes[2].origin[1], // YMIN + planes[4].origin[2], // ZMIN + ]; + this.toolCenterMax = [ + planes[1].origin[0], // XMAX + planes[3].origin[1], // YMAX + planes[5].origin[2], // ZMAX + ]; + this.toolCenter = [ + (this.toolCenterMin[0] + this.toolCenterMax[0]) / 2, + (this.toolCenterMin[1] + this.toolCenterMax[1]) / 2, + (this.toolCenterMin[2] + this.toolCenterMax[2]) / 2, + ]; + + // Update annotations based on their specific orientation + const viewportsInfo = this._getViewportsInfo(); + viewportsInfo.forEach(({ viewportId, renderingEngineId }) => { + const enabledElement = getEnabledElementByIds( + viewportId, + renderingEngineId + ); + if (enabledElement) { + const annotations = this._getAnnotations(enabledElement); + annotations.forEach((annotation) => { + if ( + annotation.data && + annotation.data.handles && + annotation.data.orientation + ) { + const orientation = annotation.data.orientation; + + // Update tool centers based on the specific orientation + if (orientation === 'AXIAL') { + // Axial views see X and Y clipping planes + annotation.data.handles.toolCenterMin = [ + planes[0].origin[0], // XMIN + planes[2].origin[1], // YMIN + annotation.data.handles.toolCenterMin[2], // Keep existing Z + ]; + annotation.data.handles.toolCenterMax = [ + planes[1].origin[0], // XMAX + planes[3].origin[1], // YMAX + annotation.data.handles.toolCenterMax[2], // Keep existing Z + ]; + } else if (orientation === 'CORONAL') { + // Coronal views see X and Z clipping planes + annotation.data.handles.toolCenterMin = [ + planes[0].origin[0], // XMIN + annotation.data.handles.toolCenterMin[1], // Keep existing Y + planes[4].origin[2], // ZMIN + ]; + annotation.data.handles.toolCenterMax = [ + planes[1].origin[0], // XMAX + annotation.data.handles.toolCenterMax[1], // Keep existing Y + planes[5].origin[2], // ZMAX + ]; + } else if (orientation === 'SAGITTAL') { + // Sagittal views see Y and Z clipping planes + annotation.data.handles.toolCenterMin = [ + annotation.data.handles.toolCenterMin[0], // Keep existing X + planes[2].origin[1], // YMIN + planes[4].origin[2], // ZMIN + ]; + annotation.data.handles.toolCenterMax = [ + annotation.data.handles.toolCenterMax[0], // Keep existing X + planes[3].origin[1], // YMAX + planes[5].origin[2], // ZMAX + ]; + } + + // Update the tool center as midpoint + annotation.data.handles.toolCenter = [ + (annotation.data.handles.toolCenterMin[0] + + annotation.data.handles.toolCenterMax[0]) / + 2, + (annotation.data.handles.toolCenterMin[1] + + annotation.data.handles.toolCenterMax[1]) / + 2, + (annotation.data.handles.toolCenterMin[2] + + annotation.data.handles.toolCenterMax[2]) / + 2, + ]; + } + }); + } + }); + + // Update virtual annotations as well + if (this._virtualAnnotations && this._virtualAnnotations.length > 0) { + this._virtualAnnotations.forEach((annotation) => { + if ( + annotation.data && + annotation.data.handles && + annotation.data.orientation + ) { + const orientation = annotation.data.orientation.toUpperCase(); + + // Apply the same orientation-specific logic to virtual annotations + if (orientation === 'AXIAL') { + annotation.data.handles.toolCenterMin = [ + planes[0].origin[0], // XMIN + planes[2].origin[1], // YMIN + annotation.data.handles.toolCenterMin[2], + ]; + annotation.data.handles.toolCenterMax = [ + planes[1].origin[0], // XMAX + planes[3].origin[1], // YMAX + annotation.data.handles.toolCenterMax[2], + ]; + } else if (orientation === 'CORONAL') { + annotation.data.handles.toolCenterMin = [ + planes[0].origin[0], // XMIN + annotation.data.handles.toolCenterMin[1], + planes[4].origin[2], // ZMIN + ]; + annotation.data.handles.toolCenterMax = [ + planes[1].origin[0], // XMAX + annotation.data.handles.toolCenterMax[1], + planes[5].origin[2], // ZMAX + ]; + } else if (orientation === 'SAGITTAL') { + annotation.data.handles.toolCenterMin = [ + annotation.data.handles.toolCenterMin[0], + planes[2].origin[1], // YMIN + planes[4].origin[2], // ZMIN + ]; + annotation.data.handles.toolCenterMax = [ + annotation.data.handles.toolCenterMax[0], + planes[3].origin[1], // YMAX + planes[5].origin[2], // ZMAX + ]; + } + + annotation.data.handles.toolCenter = [ + (annotation.data.handles.toolCenterMin[0] + + annotation.data.handles.toolCenterMax[0]) / + 2, + (annotation.data.handles.toolCenterMin[1] + + annotation.data.handles.toolCenterMax[1]) / + 2, + (annotation.data.handles.toolCenterMin[2] + + annotation.data.handles.toolCenterMax[2]) / + 2, + ]; + } + }); + } + + // Trigger re-render to show updated reference lines + triggerAnnotationRenderForViewportIds( + viewportsInfo.map(({ viewportId }) => viewportId) + ); + } + } + + setToolCenter(toolCenter: Types.Point3, handleType): void { + if (handleType === 'min') { + this.toolCenterMin = [...toolCenter]; + } else if (handleType === 'max') { + this.toolCenterMax = [...toolCenter]; + } + const viewportsInfo = this._getViewportsInfo(); + + // assuming all viewports are in the same rendering engine + triggerAnnotationRenderForViewportIds( + viewportsInfo.map(({ viewportId }) => viewportId) + ); + } + + /** + * addNewAnnotation is called when the user clicks on the image. + * It does not store the annotation in the stateManager though. + * + * @param evt - The mouse event + * @param interactionType - The type of interaction (e.g., mouse, touch, etc.) + * @returns annotation + */ + + addNewAnnotation( + evt: EventTypes.InteractionEventType + ): VolumeCroppingAnnotation { + const eventDetail = evt.detail; + const { element } = eventDetail; + const enabledElement = getEnabledElement(element); + const { viewport } = enabledElement; + const annotations = this._getAnnotations(enabledElement); + const filteredAnnotations = this.filterInteractableAnnotationsForElement( + viewport.element, + annotations + ); + + // Guard clause: if no interactable annotation, return null + if ( + !filteredAnnotations || + filteredAnnotations.length === 0 || + !filteredAnnotations[0] + ) { + return null; + } + + const { data } = filteredAnnotations[0]; + + const viewportIdArray = []; + // put all the draggable reference lines in the viewportIdArray + + const referenceLines = data.referenceLines || []; + for (let i = 0; i < referenceLines.length; ++i) { + const otherViewport = referenceLines[i][0]; + const viewportControllable = this._getReferenceLineControllable( + otherViewport.id + ); + + if (!viewportControllable) { + continue; + } + viewportIdArray.push(otherViewport.id); + i++; + } + + data.activeViewportIds = [...viewportIdArray]; + // set translation operation + data.handles.activeOperation = OPERATION.DRAG; + + evt.preventDefault(); + + hideElementCursor(element); + + this._activateModify(element); + return filteredAnnotations[0]; + } + + cancel = () => { + console.log('Not implemented yet'); + }; + + /** + * It returns if the canvas point is near the provided volume cropping annotation in the + * provided element or not. A proximity is passed to the function to determine the + * proximity of the point to the annotation in number of pixels. + * + * @param element - HTML Element + * @param annotation - Annotation + * @param canvasCoords - Canvas coordinates + * @param proximity - Proximity to tool to consider + * @returns Boolean, whether the canvas point is near tool + */ + isPointNearTool = ( + element: HTMLDivElement, + annotation: VolumeCroppingAnnotation, + canvasCoords: Types.Point2, + proximity: number + ): boolean => { + if (this._pointNearTool(element, annotation, canvasCoords, 6)) { + return true; + } + + return false; + }; + + toolSelectedCallback = ( + evt: EventTypes.InteractionEventType, + annotation: Annotation, + interactionType: InteractionTypes + ): void => { + const eventDetail = evt.detail; + const { element } = eventDetail; + annotation.highlighted = true; + this._activateModify(element); + + hideElementCursor(element); + + evt.preventDefault(); + }; + + handleSelectedCallback( + evt: EventTypes.InteractionEventType, + annotation: Annotation, + handle: ToolHandle, + interactionType: InteractionTypes + ): void { + // You can customize this logic as needed + // For now, just call toolSelectedCallback if you want default behavior + this.toolSelectedCallback(evt, annotation, interactionType); + } + + onResetCamera = (evt) => { + this.resetCroppingSpheres(); + }; + + mouseMoveCallback = ( + evt: EventTypes.MouseMoveEventType, + filteredToolAnnotations: Annotations + ): boolean => { + if (!filteredToolAnnotations) { + return; + } + const { element, currentPoints } = evt.detail; + const canvasCoords = currentPoints.canvas; + let imageNeedsUpdate = false; + + for (let i = 0; i < filteredToolAnnotations.length; i++) { + const annotation = filteredToolAnnotations[i] as VolumeCroppingAnnotation; + + if (isAnnotationLocked(annotation.annotationUID)) { + continue; + } + + const { data, highlighted } = annotation; + if (!data.handles) { + continue; + } + + const previousActiveOperation = data.handles.activeOperation; + const previousActiveViewportIds = + data.activeViewportIds && data.activeViewportIds.length > 0 + ? [...data.activeViewportIds] + : []; + + // This init are necessary, because when we move the mouse they are not cleaned by _endCallback + data.activeViewportIds = []; + let near = false; + near = this._pointNearTool(element, annotation, canvasCoords, 6); + + const nearToolAndNotMarkedActive = near && !highlighted; + const notNearToolAndMarkedActive = !near && highlighted; + if (nearToolAndNotMarkedActive || notNearToolAndMarkedActive) { + annotation.highlighted = !highlighted; + imageNeedsUpdate = true; + } + } + + return imageNeedsUpdate; + }; + + filterInteractableAnnotationsForElement = (element, annotations) => { + if (!annotations || !annotations.length) { + return []; + } + + const enabledElement = getEnabledElement(element); + // Use orientation property for matching + let orientation = null; + if (enabledElement.viewport && enabledElement.viewport.getCamera) { + orientation = this._getOrientationFromNormal( + enabledElement.viewport.getCamera().viewPlaneNormal + ); + } + + // Filter annotations for this orientation, including virtual annotations + const filtered = annotations.filter((annotation) => { + // Always include virtual annotations for reference line rendering + if (annotation.isVirtual) { + return true; + } + // Match by orientation property + if ( + annotation.data.orientation && + orientation && + annotation.data.orientation === orientation + ) { + return true; + } + return false; + }); + + return filtered; + }; + + /** + * renders the volume cropping lines and handles in the requestAnimationFrame callback + * + * @param enabledElement - The Cornerstone's enabledElement. + * @param svgDrawingHelper - The svgDrawingHelper providing the context for drawing. + */ + renderAnnotation = ( + enabledElement: Types.IEnabledElement, + svgDrawingHelper: SVGDrawingHelper + ): boolean => { + function lineIntersection2D(p1, p2, q1, q2) { + const s1_x = p2[0] - p1[0]; + const s1_y = p2[1] - p1[1]; + const s2_x = q2[0] - q1[0]; + const s2_y = q2[1] - q1[1]; + const denom = -s2_x * s1_y + s1_x * s2_y; + if (Math.abs(denom) < 1e-8) { + return null; + } // Parallel + const s = (-s1_y * (p1[0] - q1[0]) + s1_x * (p1[1] - q1[1])) / denom; + const t = (s2_x * (p1[1] - q1[1]) - s2_y * (p1[0] - q1[0])) / denom; + if (s >= 0 && s <= 1 && t >= 0 && t <= 1) { + return [p1[0] + t * s1_x, p1[1] + t * s1_y]; + } + return null; + } + const viewportsInfo = this._getViewportsInfo(); + if (!viewportsInfo || viewportsInfo.length === 0) { + // No viewports available + return false; + } + let renderStatus = false; + const { viewport, renderingEngine } = enabledElement; + const { element } = viewport; + let annotations = this._getAnnotations(enabledElement); + // If we have virtual annotations , always include them + if (this._virtualAnnotations && this._virtualAnnotations.length) { + annotations = annotations.concat(this._virtualAnnotations); + } + const camera = viewport.getCamera(); + const filteredToolAnnotations = + this.filterInteractableAnnotationsForElement(element, annotations); + + // viewport Annotation: use the first annotation for the current viewport + const viewportAnnotation = filteredToolAnnotations[0]; + if (!viewportAnnotation || !viewportAnnotation.data) { + // No annotation for this viewport + return renderStatus; + } + + const annotationUID = viewportAnnotation.annotationUID; + + // Get cameras/canvases for each of these. + // -- Get two world positions for this canvas in this line (e.g. the diagonal) + // -- Convert these world positions to this canvas. + // -- Extend/confine this line to fit in this canvas. + // -- Render this line. + const { clientWidth, clientHeight } = viewport.canvas; + const canvasDiagonalLength = Math.sqrt( + clientWidth * clientWidth + clientHeight * clientHeight + ); + + const data = viewportAnnotation.data; + // Get all other annotations except the current viewport's + const otherViewportAnnotations = annotations; + + const volumeCroppingCenterCanvasMin = viewport.worldToCanvas( + this.toolCenterMin + ); + const volumeCroppingCenterCanvasMax = viewport.worldToCanvas( + this.toolCenterMax + ); + + const referenceLines = []; + + // get canvas information for points and lines (canvas box, canvas horizontal distances) + const canvasBox = [0, 0, clientWidth, clientHeight]; + + otherViewportAnnotations.forEach((annotation) => { + const data = annotation.data; + // Type guard for isVirtual property + const isVirtual = + 'isVirtual' in annotation && + (annotation as { isVirtual?: boolean }).isVirtual === true; + data.handles.toolCenter = this.toolCenter; + let otherViewport, + otherCamera, + clientWidth, + clientHeight, + otherCanvasDiagonalLength, + otherCanvasCenter, + otherViewportCenterWorld; + if (isVirtual) { + // Synthesize a virtual viewport/camera for any missing orientation + const realViewports = viewportsInfo.filter( + (vp) => vp.viewportId !== data.viewportId + ); + if (realViewports.length === 2) { + const vp1 = renderingEngine.getViewport(realViewports[0].viewportId); + const vp2 = renderingEngine.getViewport(realViewports[1].viewportId); + const normal1 = vp1.getCamera().viewPlaneNormal; + const normal2 = vp2.getCamera().viewPlaneNormal; + const virtualNormal = vec3.create(); + vec3.cross(virtualNormal, normal1, normal2); + vec3.normalize(virtualNormal, virtualNormal); + otherCamera = { + viewPlaneNormal: virtualNormal, + position: data.handles.toolCenter, + focalPoint: data.handles.toolCenter, + viewUp: [0, 1, 0], + }; + clientWidth = viewport.canvas.clientWidth; + clientHeight = viewport.canvas.clientHeight; + otherCanvasDiagonalLength = Math.sqrt( + clientWidth * clientWidth + clientHeight * clientHeight + ); + otherCanvasCenter = [clientWidth * 0.5, clientHeight * 0.5]; + otherViewportCenterWorld = data.handles.toolCenter; + otherViewport = { + id: data.viewportId, + canvas: viewport.canvas, + canvasToWorld: () => data.handles.toolCenter, + }; + } else { + // Only one real viewport: use canonical normal from virtual annotation + const virtualNormal = (annotation as VolumeCroppingAnnotation) + .virtualNormal ?? [0, 0, 1]; + otherCamera = { + viewPlaneNormal: virtualNormal, + position: data.handles.toolCenter, + focalPoint: data.handles.toolCenter, + viewUp: [0, 1, 0], + }; + clientWidth = viewport.canvas.clientWidth; + clientHeight = viewport.canvas.clientHeight; + otherCanvasDiagonalLength = Math.sqrt( + clientWidth * clientWidth + clientHeight * clientHeight + ); + otherCanvasCenter = [clientWidth * 0.5, clientHeight * 0.5]; + otherViewportCenterWorld = data.handles.toolCenter; + otherViewport = { + id: data.viewportId, + canvas: viewport.canvas, + canvasToWorld: () => data.handles.toolCenter, + }; + } + } else { + otherViewport = renderingEngine.getViewport(data.viewportId as string); + otherCamera = otherViewport.getCamera(); + clientWidth = otherViewport.canvas.clientWidth; + clientHeight = otherViewport.canvas.clientHeight; + otherCanvasDiagonalLength = Math.sqrt( + clientWidth * clientWidth + clientHeight * clientHeight + ); + otherCanvasCenter = [clientWidth * 0.5, clientHeight * 0.5]; + otherViewportCenterWorld = + otherViewport.canvasToWorld(otherCanvasCenter); + } + + const otherViewportControllable = this._getReferenceLineControllable( + otherViewport.id + ); + + const direction = [0, 0, 0]; + vtkMath.cross( + camera.viewPlaneNormal as [number, number, number], + otherCamera.viewPlaneNormal as [number, number, number], + direction as [number, number, number] + ); + vtkMath.normalize(direction as [number, number, number]); + vtkMath.multiplyScalar( + direction as [number, number, number], + otherCanvasDiagonalLength + ); + + const pointWorld0: [number, number, number] = [0, 0, 0]; + vtkMath.add( + otherViewportCenterWorld as [number, number, number], + direction as [number, number, number], + pointWorld0 + ); + const pointWorld1: [number, number, number] = [0, 0, 0]; + vtkMath.subtract( + otherViewportCenterWorld as [number, number, number], + direction as [number, number, number], + pointWorld1 + ); + + const pointCanvas0 = viewport.worldToCanvas(pointWorld0 as Types.Point3); + const otherViewportCenterCanvas = viewport.worldToCanvas([ + otherViewportCenterWorld[0] ?? 0, + otherViewportCenterWorld[1] ?? 0, + otherViewportCenterWorld[2] ?? 0, + ] as [number, number, number] as Types.Point3); + + const canvasUnitVectorFromCenter = vec2.create(); + vec2.subtract( + canvasUnitVectorFromCenter, + pointCanvas0, + otherViewportCenterCanvas + ); + vec2.normalize(canvasUnitVectorFromCenter, canvasUnitVectorFromCenter); + + const canvasVectorFromCenterLong = vec2.create(); + vec2.scale( + canvasVectorFromCenterLong, + canvasUnitVectorFromCenter, + canvasDiagonalLength * 100 + ); + + // For min + const refLinesCenterMin = otherViewportControllable + ? vec2.clone(volumeCroppingCenterCanvasMin) + : vec2.clone(otherViewportCenterCanvas); + const refLinePointMinOne = vec2.create(); + const refLinePointMinTwo = vec2.create(); + vec2.add( + refLinePointMinOne, + refLinesCenterMin, + canvasVectorFromCenterLong + ); + vec2.subtract( + refLinePointMinTwo, + refLinesCenterMin, + canvasVectorFromCenterLong + ); + liangBarksyClip(refLinePointMinOne, refLinePointMinTwo, canvasBox); + referenceLines.push([ + otherViewport, + refLinePointMinOne, + refLinePointMinTwo, + 'min', + ]); + + // For max center + const refLinesCenterMax = otherViewportControllable + ? vec2.clone(volumeCroppingCenterCanvasMax) + : vec2.clone(otherViewportCenterCanvas); + const refLinePointMaxOne = vec2.create(); + const refLinePointMaxTwo = vec2.create(); + vec2.add( + refLinePointMaxOne, + refLinesCenterMax, + canvasVectorFromCenterLong + ); + vec2.subtract( + refLinePointMaxTwo, + refLinesCenterMax, + canvasVectorFromCenterLong + ); + liangBarksyClip(refLinePointMaxOne, refLinePointMaxTwo, canvasBox); + referenceLines.push([ + otherViewport, + refLinePointMaxOne, + refLinePointMaxTwo, + 'max', + ]); + }); + + data.referenceLines = referenceLines; + + const viewportColor = this._getReferenceLineColor(viewport.id); + const color = + viewportColor !== undefined ? viewportColor : 'rgb(200, 200, 200)'; + + referenceLines.forEach((line, lineIndex) => { + // Calculate intersections with other lines in this viewport + const intersections = []; + for (let j = 0; j < referenceLines.length; ++j) { + if (j === lineIndex) { + continue; + } + const otherLine = referenceLines[j]; + const intersection = lineIntersection2D( + line[1], + line[2], + otherLine[1], + otherLine[2] + ); + if (intersection) { + intersections.push({ + with: otherLine[3], // 'min' or 'max' + point: intersection, + }); + } + } + + // get color for the reference line using orientation + const otherViewport = line[0]; + let orientation = null; + // Try to get orientation from annotation data or viewportId + if (otherViewport && otherViewport.id) { + // Try to get from annotation if available + const annotationForViewport = annotations.find( + (a) => a.data.viewportId === otherViewport.id + ); + if (annotationForViewport && annotationForViewport.data.orientation) { + orientation = String( + annotationForViewport.data.orientation + ).toUpperCase(); + } else { + // Fallback: try to infer from viewportId + const idUpper = otherViewport.id.toUpperCase(); + if (idUpper.includes('AXIAL')) { + orientation = 'AXIAL'; + } else if (idUpper.includes('CORONAL')) { + orientation = 'CORONAL'; + } else if (idUpper.includes('SAGITTAL')) { + orientation = 'SAGITTAL'; + } + } + } + // Use lineColors from configuration + const lineColors = this.configuration.lineColors || {}; + const colorArr = lineColors[orientation] || + lineColors.unknown || [1.0, 0.0, 0.0]; // fallback to red + // Convert [r,g,b] to rgb string if needed + const color = Array.isArray(colorArr) + ? `rgb(${colorArr.map((v) => Math.round(v * 255)).join(',')})` + : colorArr; + + const viewportControllable = this._getReferenceLineControllable( + otherViewport.id + ); + const selectedViewportId = data.activeViewportIds.find( + (id) => id === otherViewport.id + ); + + let lineWidth = this.configuration.lineWidth ?? 1.5; + const lineActive = + data.handles.activeOperation !== null && + data.handles.activeOperation === OPERATION.DRAG && + selectedViewportId; + if (lineActive) { + lineWidth = this.configuration.activeLineWidth ?? 2.5; + } + + const lineUID = `${lineIndex}`; + if (viewportControllable) { + if (intersections.length === 2) { + drawLineSvg( + svgDrawingHelper, + annotationUID, + lineUID, + intersections[0].point, + intersections[1].point, + { + color, + lineWidth, + } + ); + } + if ( + this.configuration.extendReferenceLines && + intersections.length === 2 + ) { + if ( + this.configuration.extendReferenceLines && + intersections.length === 2 + ) { + // Sort intersections by distance from line start + const sortedIntersections = intersections + .map((intersection) => ({ + ...intersection, + distance: vec2.distance(line[1], intersection.point), + })) + .sort((a, b) => a.distance - b.distance); + + // Draw dashed lines in correct order + drawLineSvg( + svgDrawingHelper, + annotationUID, + lineUID + '_dashed_before', + line[1], + sortedIntersections[0].point, + { color, lineWidth, lineDash: [4, 4] } + ); + + drawLineSvg( + svgDrawingHelper, + annotationUID, + lineUID + '_dashed_after', + sortedIntersections[1].point, + line[2], + { color, lineWidth, lineDash: [4, 4] } + ); + } + } + } + }); + + renderStatus = true; + + if (this.configuration.viewportIndicators) { + const { viewportIndicatorsConfig } = this.configuration; + const xOffset = viewportIndicatorsConfig?.xOffset || 0.95; + const yOffset = viewportIndicatorsConfig?.yOffset || 0.05; + const referenceColorCoordinates = [ + clientWidth * xOffset, + clientHeight * yOffset, + ]; + + const circleRadius = + viewportIndicatorsConfig?.circleRadius || canvasDiagonalLength * 0.01; + + const circleUID = '0'; + drawCircleSvg( + svgDrawingHelper, + annotationUID, + circleUID, + referenceColorCoordinates as Types.Point2, + circleRadius, + { color, fill: color } + ); + } + + return renderStatus; + }; + + _getAnnotations = (enabledElement: Types.IEnabledElement) => { + const { viewport } = enabledElement; + const annotations = + getAnnotations(this.getToolName(), viewport.element) || []; + const viewportIds = this._getViewportsInfo().map( + ({ viewportId }) => viewportId + ); + + // filter the annotations to only keep that are for this toolGroup + const toolGroupAnnotations = annotations.filter((annotation) => { + const { data } = annotation; + return viewportIds.includes(data.viewportId); + }); + + return toolGroupAnnotations; + }; + + _onSphereMoved = (evt) => { + if (evt.detail.originalClippingPlanes) { + this._syncWithVolumeCroppingTool(evt.detail.originalClippingPlanes); + } else { + if (evt.detail.seriesInstanceUID !== this.seriesInstanceUID) { + return; + } + // This is called when a sphere is moved + const { draggingSphereIndex, toolCenter } = evt.detail; + const newMin: [number, number, number] = [...this.toolCenterMin]; + const newMax: [number, number, number] = [...this.toolCenterMax]; + // face spheres + if (draggingSphereIndex >= 0 && draggingSphereIndex <= 5) { + const axis = Math.floor(draggingSphereIndex / 2); + const isMin = draggingSphereIndex % 2 === 0; + (isMin ? newMin : newMax)[axis] = toolCenter[axis]; + this.setToolCenter(newMin, 'min'); + this.setToolCenter(newMax, 'max'); + return; + } + // corner spheres + if (draggingSphereIndex >= 6 && draggingSphereIndex <= 13) { + const idx = draggingSphereIndex; + if (idx < 10) { + newMin[0] = toolCenter[0]; + } else { + newMax[0] = toolCenter[0]; + } + if ([6, 7, 10, 11].includes(idx)) { + newMin[1] = toolCenter[1]; + } else { + newMax[1] = toolCenter[1]; + } + if (idx % 2 === 0) { + newMin[2] = toolCenter[2]; + } else { + newMax[2] = toolCenter[2]; + } + this.setToolCenter(newMin, 'min'); + this.setToolCenter(newMax, 'max'); + } + } + }; + + _onNewVolume = () => { + const viewportsInfo = this._getViewportsInfo(); + if (viewportsInfo && viewportsInfo.length > 0) { + const { viewportId, renderingEngineId } = viewportsInfo[0]; + const renderingEngine = getRenderingEngine(renderingEngineId); + const viewport = renderingEngine.getViewport(viewportId); + const volumeActors = viewport.getActors(); + if (volumeActors.length > 0) { + const imageData = volumeActors[0].actor.getMapper().getInputData(); + if (imageData) { + this.seriesInstanceUID = imageData.seriesInstanceUID; + this._updateToolCentersFromViewport(viewport); + // Update all annotations' handles.toolCenter + const annotations = + getAnnotations(this.getToolName(), viewportId) || []; + annotations.forEach((annotation) => { + if (annotation.data && annotation.data.handles) { + annotation.data.handles.toolCenter = [...this.toolCenter]; + } + }); + } + } + } + this._computeToolCenter(viewportsInfo); + triggerEvent(eventTarget, Events.VOLUMECROPPINGCONTROL_TOOL_CHANGED, { + toolGroupId: this.toolGroupId, + viewportsInfo: viewportsInfo, + seriesInstanceUID: this.seriesInstanceUID, + }); + }; + + _unsubscribeToViewportNewVolumeSet(viewportsInfo) { + viewportsInfo.forEach(({ viewportId, renderingEngineId }) => { + const { viewport } = getEnabledElementByIds( + viewportId, + renderingEngineId + ); + const { element } = viewport; + + element.removeEventListener( + Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, + this._onNewVolume + ); + }); + } + + _subscribeToViewportNewVolumeSet(viewports) { + viewports.forEach(({ viewportId, renderingEngineId }) => { + const { viewport } = getEnabledElementByIds( + viewportId, + renderingEngineId + ); + const { element } = viewport; + + element.addEventListener( + Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, + this._onNewVolume + ); + }); + } + + _getAnnotationsForViewportsWithDifferentCameras = ( + enabledElement, + annotations + ) => { + const { viewportId, renderingEngine, viewport } = enabledElement; + + const otherViewportAnnotations = annotations.filter( + (annotation) => annotation.data.viewportId !== viewportId + ); + + if (!otherViewportAnnotations || !otherViewportAnnotations.length) { + return []; + } + + const camera = viewport.getCamera(); + const { viewPlaneNormal, position } = camera; + + const viewportsWithDifferentCameras = otherViewportAnnotations.filter( + (annotation) => { + const { viewportId } = annotation.data; + const targetViewport = renderingEngine.getViewport(viewportId); + const cameraOfTarget = targetViewport.getCamera(); + + return !( + csUtils.isEqual( + cameraOfTarget.viewPlaneNormal, + viewPlaneNormal, + 1e-2 + ) && csUtils.isEqual(cameraOfTarget.position, position, 1) + ); + } + ); + + return viewportsWithDifferentCameras; + }; + + _filterViewportWithSameOrientation = ( + enabledElement, + referenceAnnotation, + annotations + ) => { + const { renderingEngine } = enabledElement; + const { data } = referenceAnnotation; + const viewport = renderingEngine.getViewport(data.viewportId); + + const linkedViewportAnnotations = annotations.filter((annotation) => { + const { data } = annotation; + const otherViewport = renderingEngine.getViewport(data.viewportId); + const otherViewportControllable = this._getReferenceLineControllable( + otherViewport.id + ); + + return otherViewportControllable === true; + }); + + if (!linkedViewportAnnotations || !linkedViewportAnnotations.length) { + return []; + } + + const camera = viewport.getCamera(); + const viewPlaneNormal = camera.viewPlaneNormal; + vtkMath.normalize(viewPlaneNormal); + + const otherViewportsAnnotationsWithSameCameraDirection = + linkedViewportAnnotations.filter((annotation) => { + const { viewportId } = annotation.data; + const otherViewport = renderingEngine.getViewport(viewportId); + const otherCamera = otherViewport.getCamera(); + const otherViewPlaneNormal = otherCamera.viewPlaneNormal; + vtkMath.normalize(otherViewPlaneNormal); + + return ( + csUtils.isEqual(viewPlaneNormal, otherViewPlaneNormal, 1e-2) && + csUtils.isEqual(camera.viewUp, otherCamera.viewUp, 1e-2) + ); + }); + + return otherViewportsAnnotationsWithSameCameraDirection; + }; + + _activateModify = (element) => { + // mobile sometimes has lingering interaction even when touchEnd triggers + // this check allows for multiple handles to be active which doesn't affect + // tool usage. + state.isInteractingWithTool = !this.configuration.mobile?.enabled; + + element.addEventListener(Events.MOUSE_UP, this._endCallback); + element.addEventListener(Events.MOUSE_DRAG, this._dragCallback); + element.addEventListener(Events.MOUSE_CLICK, this._endCallback); + + element.addEventListener(Events.TOUCH_END, this._endCallback); + element.addEventListener(Events.TOUCH_DRAG, this._dragCallback); + element.addEventListener(Events.TOUCH_TAP, this._endCallback); + }; + + _deactivateModify = (element) => { + state.isInteractingWithTool = false; + + element.removeEventListener(Events.MOUSE_UP, this._endCallback); + element.removeEventListener(Events.MOUSE_DRAG, this._dragCallback); + element.removeEventListener(Events.MOUSE_CLICK, this._endCallback); + + element.removeEventListener(Events.TOUCH_END, this._endCallback); + element.removeEventListener(Events.TOUCH_DRAG, this._dragCallback); + element.removeEventListener(Events.TOUCH_TAP, this._endCallback); + }; + + _endCallback = (evt: EventTypes.InteractionEventType) => { + const eventDetail = evt.detail; + const { element } = eventDetail; + + this.editData.annotation.data.handles.activeOperation = null; + this.editData.annotation.data.activeViewportIds = []; + + this._deactivateModify(element); + + resetElementCursor(element); + + this.editData = null; + + const requireSameOrientation = false; + const viewportIdsToRender = getViewportIdsWithToolToRender( + element, + this.getToolName(), + requireSameOrientation + ); + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + }; + + _dragCallback = (evt: EventTypes.InteractionEventType) => { + const eventDetail = evt.detail; + const delta = eventDetail.deltaPoints.world; + + if ( + Math.abs(delta[0]) < 1e-3 && + Math.abs(delta[1]) < 1e-3 && + Math.abs(delta[2]) < 1e-3 + ) { + return; + } + + const { element } = eventDetail; + const enabledElement = getEnabledElement(element); + const { viewport } = enabledElement; + if (viewport.type === Enums.ViewportType.VOLUME_3D) { + return; + } + const annotations = this._getAnnotations( + enabledElement + ) as VolumeCroppingAnnotation[]; + const filteredToolAnnotations = + this.filterInteractableAnnotationsForElement(element, annotations); + + // viewport Annotation + const viewportAnnotation = filteredToolAnnotations[0]; + if (!viewportAnnotation) { + return; + } + + const { handles } = viewportAnnotation.data; + + if (handles.activeOperation === OPERATION.DRAG) { + if (handles.activeType === 'min') { + this.toolCenterMin[0] += delta[0]; + this.toolCenterMin[1] += delta[1]; + this.toolCenterMin[2] += delta[2]; + } else if (handles.activeType === 'max') { + this.toolCenterMax[0] += delta[0]; + this.toolCenterMax[1] += delta[1]; + this.toolCenterMax[2] += delta[2]; + } else { + this.toolCenter[0] += delta[0]; + this.toolCenter[1] += delta[1]; + this.toolCenter[2] += delta[2]; + } + const viewportsInfo = this._getViewportsInfo(); + triggerAnnotationRenderForViewportIds( + viewportsInfo.map(({ viewportId }) => viewportId) + ); + triggerEvent(eventTarget, Events.VOLUMECROPPINGCONTROL_TOOL_CHANGED, { + toolGroupId: this.toolGroupId, + toolCenter: this.toolCenter, + toolCenterMin: this.toolCenterMin, + toolCenterMax: this.toolCenterMax, + handleType: handles.activeType, + viewportOrientation: [], + seriesInstanceUID: this.seriesInstanceUID, + }); + } + }; + + _applyDeltaShiftToSelectedViewportCameras( + renderingEngine, + viewportsAnnotationsToUpdate, + delta + ) { + // update camera for the other viewports. + // NOTE1: The lines then are rendered by the onCameraModified + viewportsAnnotationsToUpdate.forEach((annotation) => { + this._applyDeltaShiftToViewportCamera(renderingEngine, annotation, delta); + }); + } + + _applyDeltaShiftToViewportCamera( + renderingEngine: Types.IRenderingEngine, + annotation, + delta + ) { + const { data } = annotation; + + const viewport = renderingEngine.getViewport(data.viewportId); + const camera = viewport.getCamera(); + const normal = camera.viewPlaneNormal; + + // Project delta over camera normal + // (we don't need to pan, we need only to scroll the camera as in the wheel stack scroll tool) + const dotProd = vtkMath.dot(delta, normal); + const projectedDelta: Types.Point3 = [...normal]; + vtkMath.multiplyScalar(projectedDelta, dotProd); + + if ( + Math.abs(projectedDelta[0]) > 1e-3 || + Math.abs(projectedDelta[1]) > 1e-3 || + Math.abs(projectedDelta[2]) > 1e-3 + ) { + const newFocalPoint: Types.Point3 = [0, 0, 0]; + const newPosition: Types.Point3 = [0, 0, 0]; + + vtkMath.add(camera.focalPoint, projectedDelta, newFocalPoint); + vtkMath.add(camera.position, projectedDelta, newPosition); + + viewport.setCamera({ + focalPoint: newFocalPoint, + position: newPosition, + }); + viewport.render(); + } + } + + _pointNearTool(element, annotation, canvasCoords, proximity) { + const { data } = annotation; + + // You must have referenceLines available in annotation.data. + // If not, you can recompute them here or store them in renderAnnotation. + // For this example, let's assume you store them as data.referenceLines. + const referenceLines = data.referenceLines; + + const viewportIdArray = []; + + if (referenceLines) { + for (let i = 0; i < referenceLines.length; ++i) { + // Each line: [otherViewport, refLinePointOne, refLinePointMinOne, ...] + const otherViewport = referenceLines[i][0]; + // First segment + const start1 = referenceLines[i][1]; + const end1 = referenceLines[i][2]; + const type = referenceLines[i][3]; // 'min' or 'max' + + const distance1 = lineSegment.distanceToPoint(start1, end1, [ + canvasCoords[0], + canvasCoords[1], + ]); + + if (distance1 <= proximity) { + viewportIdArray.push(otherViewport.id); + data.handles.activeOperation = 1; // DRAG + data.handles.activeType = type; + } + } + } + + data.activeViewportIds = [...viewportIdArray]; + + this.editData = { + annotation, + }; + return data.handles.activeOperation === 1 ? true : false; + } +} + +VolumeCroppingControlTool.toolName = 'VolumeCroppingControl'; +export default VolumeCroppingControlTool; diff --git a/packages/tools/src/tools/VolumeCroppingTool.ts b/packages/tools/src/tools/VolumeCroppingTool.ts new file mode 100644 index 0000000000..f8bda9c21a --- /dev/null +++ b/packages/tools/src/tools/VolumeCroppingTool.ts @@ -0,0 +1,1727 @@ +import vtkPolyData from '@kitware/vtk.js/Common/DataModel/PolyData'; +import vtkPoints from '@kitware/vtk.js/Common/Core/Points'; +import vtkCellArray from '@kitware/vtk.js/Common/Core/CellArray'; +import { mat3, mat4, vec3 } from 'gl-matrix'; +import vtkMath from '@kitware/vtk.js/Common/Core/Math'; +import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import vtkSphereSource from '@kitware/vtk.js/Filters/Sources/SphereSource'; +import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; +import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane'; +import type vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper'; + +import { BaseTool } from './base'; + +import type { Types } from '@cornerstonejs/core'; +import { + getRenderingEngine, + getEnabledElementByIds, + getEnabledElement, + Enums, + triggerEvent, + eventTarget, +} from '@cornerstonejs/core'; + +import { getToolGroup } from '../store/ToolGroupManager'; +import { Events } from '../enums'; + +import type { EventTypes, PublicToolProps, ToolProps } from '../types'; + +const PLANEINDEX = { + XMIN: 0, + XMAX: 1, + YMIN: 2, + YMAX: 3, + ZMIN: 4, + ZMAX: 5, +}; +const SPHEREINDEX = { + // cube faces + XMIN: 0, + XMAX: 1, + YMIN: 2, + YMAX: 3, + ZMIN: 4, + ZMAX: 5, + // cube corners + XMIN_YMIN_ZMIN: 6, + XMIN_YMIN_ZMAX: 7, + XMIN_YMAX_ZMIN: 8, + XMIN_YMAX_ZMAX: 9, + XMAX_YMIN_ZMIN: 10, + XMAX_YMIN_ZMAX: 11, + XMAX_YMAX_ZMIN: 12, + XMAX_YMAX_ZMAX: 13, +}; + +/** + * VolumeCroppingTool provides manipulatable spheres and real-time volume cropping capabilities. + * It renders interactive handles (spheres) at face centers and corners of a cropping box, allowing users to precisely adjust volume boundaries through direct manipulation in 3D space. + * + * @remarks + * This tool creates a complete 3D cropping interface with: + * - 6 face spheres for individual axis cropping + * - 8 corner spheres for multi-axis cropping + * - 12 edge lines connecting corner spheres + * - Real-time clipping plane updates + * - Synchronization with VolumeCroppingControlTool working on the same series instance UID for cross-viewport interaction + * + * + * @example + * ```typescript + * // Basic setup + * const toolGroup = ToolGroupManager.createToolGroup('volume3D'); + * toolGroup.addTool(VolumeCroppingTool.toolName); + * + * // Configure with custom settings + * toolGroup.setToolConfiguration(VolumeCroppingTool.toolName, { + * showCornerSpheres: true, + * showHandles: true, + * initialCropFactor: 0.1, + * sphereColors: { + * SAGITTAL: [1.0, 1.0, 0.0], // Yellow for sagittal (X-axis) spheres + * CORONAL: [0.0, 1.0, 0.0], // Green for coronal (Y-axis) spheres + * AXIAL: [1.0, 0.0, 0.0], // Red for axial (Z-axis) spheres + * CORNERS: [0.0, 0.0, 1.0] // Blue for corner spheres + * }, + * sphereRadius: 10, + * grabSpherePixelDistance: 25 + * }); + * + * // Activate the tool + * toolGroup.setToolActive(VolumeCroppingTool.toolName); + * + * // Programmatically control visibility + * const tool = toolGroup.getToolInstance(VolumeCroppingTool.toolName); + * tool.setHandlesVisible(true); + * tool.setClippingPlanesVisible(true); + * + * // Toggle visibility for interactive UI + * function toggleCroppingInterface() { + * const handlesVisible = tool.getHandlesVisible(); + * const planesVisible = tool.getClippingPlanesVisible(); + * + * // Toggle handles (spheres and edge lines) + * tool.setHandlesVisible(!handlesVisible); + * + * // Toggle clipping effect + * tool.setClippingPlanesVisible(!planesVisible); + * + * console.log(`Handles: ${!handlesVisible ? 'shown' : 'hidden'}`); + * console.log(`Cropping: ${!planesVisible ? 'active' : 'disabled'}`); + * } + * + * // Common UI scenarios + * // Show handles but disable cropping (for positioning) + * tool.setHandlesVisible(true); + * tool.setClippingPlanesVisible(false); + * + * // Hide handles but keep cropping active (for clean view) + * tool.setHandlesVisible(false); + * tool.setClippingPlanesVisible(true); + * ``` + * + * @public + * @class VolumeCroppingTool + * @extends BaseTool + * + * @property {string} toolName - Static tool identifier: 'VolumeCropping' + * @property {string} seriesInstanceUID - Frame of reference for the tool + * @property {Function} touchDragCallback - Touch drag event handler for mobile interactions + * @property {Function} mouseDragCallback - Mouse drag event handler for desktop interactions + * @property {Function} cleanUp - Cleanup function for resetting tool state after interactions + * @property {Map} _resizeObservers - Map of ResizeObserver instances for viewport resize handling + * @property {Function} _viewportAddedListener - Event listener for new viewport additions + * @property {boolean} _hasResolutionChanged - Flag tracking if rendering resolution has been modified during interaction + * @property {Array} originalClippingPlanes - Array of clipping plane objects with origin and normal vectors + * @property {number|null} draggingSphereIndex - Index of currently dragged sphere, null when not dragging + * @property {Types.Point3} toolCenter - Center point of the cropping volume in world coordinates [x, y, z] + * @property {number[]|null} cornerDragOffset - 3D offset vector for corner sphere dragging [dx, dy, dz] + * @property {number|null} faceDragOffset - 1D offset value for face sphere dragging along single axis + * @property {Array} sphereStates - Array of sphere state objects containing position, VTK actors, and metadata + * @property {Object} edgeLines - Dictionary of edge line actors connecting corner spheres for wireframe visualization + * + * @typedef {Object} SphereState + * @property {Types.Point3} point - World coordinates of sphere center [x, y, z] + * @property {string} axis - Axis identifier ('x', 'y', 'z', or 'corner') + * @property {string} uid - Unique identifier for the sphere (e.g., 'x_min', 'corner_XMIN_YMIN_ZMIN') + * @property {vtkSphereSource} sphereSource - VTK sphere geometry source + * @property {vtkActor} sphereActor - VTK actor for rendering the sphere + * @property {boolean} isCorner - Flag indicating if sphere is a corner (true) or face (false) sphere + * @property {number[]} color - RGB color array [r, g, b] for sphere rendering + * + * @typedef {Object} EdgeLine + * @property {vtkActor} actor - VTK actor for rendering the edge line + * @property {vtkPolyData} source - VTK polydata source containing line geometry + * @property {string} key1 - First corner identifier (e.g., 'XMIN_YMIN_ZMIN') + * @property {string} key2 - Second corner identifier (e.g., 'XMAX_YMIN_ZMIN') + * + * @configuration + * @property {boolean} showCornerSpheres - Whether to show corner manipulation spheres (default: true) + * @property {boolean} showHandles - Whether to show all manipulation handles (spheres and edges) (default: true) + * @property {boolean} showClippingPlanes - Whether to apply clipping planes to volume rendering (default: true) + * @property {Object} mobile - Mobile device interaction settings + * @property {boolean} mobile.enabled - Enable touch-based interactions on mobile devices (default: false) + * @property {number} mobile.opacity - Opacity for mobile interaction feedback (default: 0.8) + * @property {number} initialCropFactor - Initial cropping factor as fraction of volume bounds (default: 0.08) + * @property {Object} sphereColors - Color configuration for different sphere types + * @property {number[]} sphereColors.SAGITTAL - RGB color for sagittal (X-axis) face spheres [r, g, b] (default: [1.0, 1.0, 0.0]) + * @property {number[]} sphereColors.CORONAL - RGB color for coronal (Y-axis) face spheres [r, g, b] (default: [0.0, 1.0, 0.0]) + * @property {number[]} sphereColors.AXIAL - RGB color for axial (Z-axis) face spheres [r, g, b] (default: [1.0, 0.0, 0.0]) + * @property {number[]} sphereColors.CORNERS - RGB color for corner spheres [r, g, b] (default: [0.0, 0.0, 1.0]) + * @property {number} sphereRadius - Radius of manipulation spheres in world units (default: 8) + * @property {number} grabSpherePixelDistance - Pixel distance threshold for sphere selection (default: 20) + * @property {number} rotateIncrementDegrees - Rotation increment for camera rotation (default: 2) + * @property {number} rotateSampleDistanceFactor - Sample distance multiplier during rotation for performance (default: 2) + * + * @events + * @event VOLUMECROPPING_TOOL_CHANGED - Fired when sphere positions change or clipping planes are updated + * @event VOLUMECROPPINGCONTROL_TOOL_CHANGED - Listens for changes from VolumeCroppingControlTool + * @event VOLUME_VIEWPORT_NEW_VOLUME - Listens for new volume loading to reinitialize cropping bounds + * @event TOOLGROUP_VIEWPORT_ADDED - Listens for new viewport additions to extend resize observation + * + * @methods + * - **setHandlesVisible(visible: boolean)**: Show/hide manipulation spheres and edge lines + * - **setClippingPlanesVisible(visible: boolean)**: Enable/disable volume clipping planes + * - **getHandlesVisible()**: Get current handle visibility state + * - **getClippingPlanesVisible()**: Get current clipping plane visibility state + * + * + * @see {@link VolumeCroppingControlTool} - Companion tool for 2D viewport reference lines + * @see {@link BaseTool} - Base class providing core tool functionality + * + */ +class VolumeCroppingTool extends BaseTool { + static toolName; + seriesInstanceUID?: string; + touchDragCallback: (evt: EventTypes.InteractionEventType) => void; + mouseDragCallback: (evt: EventTypes.InteractionEventType) => void; + cleanUp: () => void; + _resizeObservers = new Map(); + _viewportAddedListener: (evt) => void; + _hasResolutionChanged = false; + originalClippingPlanes: { origin: number[]; normal: number[] }[] = []; + draggingSphereIndex: number | null = null; + toolCenter: Types.Point3 = [0, 0, 0]; + cornerDragOffset: [number, number, number] | null = null; + faceDragOffset: number | null = null; + // Store spheres for show/hide actor. + sphereStates: { + point: Types.Point3; + axis: string; + uid: string; + sphereSource; + sphereActor; + isCorner: boolean; + color: number[]; // [r, g, b] color for the sphere + }[] = []; + // Store 2D edge lines between corner spheres for show/hide actor. + edgeLines: { + [uid: string]: { + actor: vtkActor; + source: vtkPolyData; + key1: string; // key1,key2: Corner identifiers such as XMIN_YMIN_ZMIN to XMAX_YMIN_ZMIN + key2: string; + }; + } = {}; + + constructor( + toolProps: PublicToolProps = {}, + defaultToolProps: ToolProps = { + configuration: { + showCornerSpheres: true, + showHandles: true, + showClippingPlanes: true, + mobile: { + enabled: false, + opacity: 0.8, + }, + initialCropFactor: 0.08, + sphereColors: { + SAGITTAL: [1.0, 1.0, 0.0], // Yellow for sagittal (X-axis) + CORONAL: [0.0, 1.0, 0.0], // Green for coronal (Y-axis) + AXIAL: [1.0, 0.0, 0.0], // Red for axial (Z-axis) + CORNERS: [0.0, 0.0, 1.0], // Blue for corners + }, + sphereRadius: 8, + grabSpherePixelDistance: 20, //pixels threshold for closeness to the sphere being grabbed + rotateIncrementDegrees: 2, + rotateSampleDistanceFactor: 2, // Factor to increase sample distance (lower resolution) when rotating + }, + } + ) { + super(toolProps, defaultToolProps); + this.touchDragCallback = this._dragCallback.bind(this); + this.mouseDragCallback = this._dragCallback.bind(this); + } + + onSetToolActive() { + // console.debug('Setting tool active: volumeCropping', this.sphereStates); + if (this.sphereStates && this.sphereStates.length > 0) { + if (this.configuration.showHandles) { + this.setHandlesVisible(false); + this.setClippingPlanesVisible(false); + // console.debug('Setting tool active: hiding controls'); + } else { + this.setHandlesVisible(true); + this.setClippingPlanesVisible(true); + // console.debug('Setting tool active: showing controls'); + } + } else { + const viewportsInfo = this._getViewportsInfo(); + const subscribeToElementResize = () => { + viewportsInfo.forEach(({ viewportId, renderingEngineId }) => { + if (!this._resizeObservers.has(viewportId)) { + const { viewport } = getEnabledElementByIds( + viewportId, + renderingEngineId + ) || { viewport: null }; + if (!viewport) { + return; + } + const { element } = viewport; + const resizeObserver = new ResizeObserver(() => { + const element = getEnabledElementByIds( + viewportId, + renderingEngineId + ); + if (!element) { + return; + } + const { viewport } = element; + const viewPresentation = viewport.getViewPresentation(); + viewport.resetCamera(); + viewport.setViewPresentation(viewPresentation); + viewport.render(); + }); + resizeObserver.observe(element); + this._resizeObservers.set(viewportId, resizeObserver); + } + }); + }; + + subscribeToElementResize(); + + this._viewportAddedListener = (evt) => { + if (evt.detail.toolGroupId === this.toolGroupId) { + subscribeToElementResize(); + } + }; + + eventTarget.addEventListener( + Events.TOOLGROUP_VIEWPORT_ADDED, + this._viewportAddedListener + ); + + this._unsubscribeToViewportNewVolumeSet(viewportsInfo); + this._subscribeToViewportNewVolumeSet(viewportsInfo); + this._initialize3DViewports(viewportsInfo); + + if (this.sphereStates && this.sphereStates.length > 0) { + this.setHandlesVisible(true); + } else { + //console.warn('Setting tool active: no spheres! trying to initialize'); + this.originalClippingPlanes = []; + this._initialize3DViewports(viewportsInfo); + } + } + } + + onSetToolConfiguration = (): void => { + console.debug('Setting tool settoolconfiguration : volumeCropping'); + //this._init(); + }; + + onSetToolEnabled = (): void => { + console.debug('Setting tool enabled: volumeCropping'); + }; + + onSetToolDisabled() { + // console.debug('Setting tool disabled: volumeCropping'); + // Disconnect all resize observers + this._resizeObservers.forEach((resizeObserver, viewportId) => { + resizeObserver.disconnect(); + this._resizeObservers.delete(viewportId); + }); + + if (this._viewportAddedListener) { + eventTarget.removeEventListener( + Events.TOOLGROUP_VIEWPORT_ADDED, + this._viewportAddedListener + ); + this._viewportAddedListener = null; // Clear the reference to the listener + } + + const viewportsInfo = this._getViewportsInfo(); + this._unsubscribeToViewportNewVolumeSet(viewportsInfo); + } + + onCameraModified = (evt) => { + const { element } = evt.currentTarget + ? { element: evt.currentTarget } + : evt.detail; + const enabledElement = getEnabledElement(element); + this._updateClippingPlanes(enabledElement.viewport); + enabledElement.viewport.render(); + }; + + preMouseDownCallback = (evt: EventTypes.InteractionEventType) => { + //console.debug('VolumeCroppingTool.preMouseDownCallback called'); + const eventDetail = evt.detail; + const { element } = eventDetail; + const enabledElement = getEnabledElement(element); + const { viewport } = enabledElement; + const actorEntry = viewport.getDefaultActor(); + const actor = actorEntry.actor as Types.VolumeActor; + const mapper = actor.getMapper(); + + const mouseCanvas: [number, number] = [ + evt.detail.currentPoints.canvas[0], + evt.detail.currentPoints.canvas[1], + ]; + // Find the sphere under the mouse + this.draggingSphereIndex = null; + this.cornerDragOffset = null; + this.faceDragOffset = null; + for (let i = 0; i < this.sphereStates.length; ++i) { + const sphereCanvas = viewport.worldToCanvas(this.sphereStates[i].point); + const dist = Math.sqrt( + Math.pow(mouseCanvas[0] - sphereCanvas[0], 2) + + Math.pow(mouseCanvas[1] - sphereCanvas[1], 2) + ); + if (dist < this.configuration.grabSpherePixelDistance) { + this.draggingSphereIndex = i; + element.style.cursor = 'grabbing'; + + // --- Store offset for corners --- + const sphereState = this.sphereStates[i]; + const mouseWorld = viewport.canvasToWorld(mouseCanvas); + if (sphereState.isCorner) { + this.cornerDragOffset = [ + sphereState.point[0] - mouseWorld[0], + sphereState.point[1] - mouseWorld[1], + sphereState.point[2] - mouseWorld[2], + ]; + this.faceDragOffset = null; + } else { + // For face spheres, only store the offset along the axis of movement + const axisIdx = { x: 0, y: 1, z: 2 }[sphereState.axis]; + this.faceDragOffset = + sphereState.point[axisIdx] - mouseWorld[axisIdx]; + this.cornerDragOffset = null; + } + + return true; + } + } + + const hasSampleDistance = + 'getSampleDistance' in mapper || 'getCurrentSampleDistance' in mapper; + + if (!hasSampleDistance) { + return true; + } + + const originalSampleDistance = mapper.getSampleDistance(); + + if (!this._hasResolutionChanged) { + const { rotateSampleDistanceFactor } = this.configuration; + mapper.setSampleDistance( + originalSampleDistance * rotateSampleDistanceFactor + ); + this._hasResolutionChanged = true; + + if (this.cleanUp !== null) { + // Clean up previous event listener + document.removeEventListener('mouseup', this.cleanUp); + } + + this.cleanUp = () => { + mapper.setSampleDistance(originalSampleDistance); + + // Reset cursor style + (evt.target as HTMLElement).style.cursor = ''; + if (this.draggingSphereIndex !== null) { + const sphereState = this.sphereStates[this.draggingSphereIndex]; + const [viewport3D] = this._getViewportsInfo(); + const renderingEngine = getRenderingEngine( + viewport3D.renderingEngineId + ); + const viewport = renderingEngine.getViewport(viewport3D.viewportId); + + if (sphereState.isCorner) { + this._updateCornerSpheres(); + this._updateFaceSpheresFromCorners(); + this._updateClippingPlanesFromFaceSpheres(viewport); + } + } + this.draggingSphereIndex = null; + this.cornerDragOffset = null; + this.faceDragOffset = null; + + viewport.render(); + this._hasResolutionChanged = false; + }; + + document.addEventListener('mouseup', this.cleanUp, { once: true }); + } + + return true; + }; + + /** + * Sets the visibility of the cropping handles (spheres and edge lines). + * + * When handles are being shown, this method automatically synchronizes the sphere positions + * with the current clipping plane positions to ensure visual consistency. This includes + * updating face spheres, corner spheres, and edge lines to match the current crop bounds. + * + * @param visible - Whether to show or hide the cropping handles + * + * @example + * ```typescript + * // Hide all cropping handles + * volumeCroppingTool.setHandlesVisible(false); + * + * // Show handles and sync with current crop state + * volumeCroppingTool.setHandlesVisible(true); + * ``` + * + */ + setHandlesVisible(visible: boolean) { + this.configuration.showHandles = visible; + // Before showing, update sphere positions to match clipping planes + if (visible) { + // Update face spheres from the current clipping planes + this.sphereStates[SPHEREINDEX.XMIN].point[0] = + this.originalClippingPlanes[PLANEINDEX.XMIN].origin[0]; + this.sphereStates[SPHEREINDEX.XMAX].point[0] = + this.originalClippingPlanes[PLANEINDEX.XMAX].origin[0]; + this.sphereStates[SPHEREINDEX.YMIN].point[1] = + this.originalClippingPlanes[PLANEINDEX.YMIN].origin[1]; + this.sphereStates[SPHEREINDEX.YMAX].point[1] = + this.originalClippingPlanes[PLANEINDEX.YMAX].origin[1]; + this.sphereStates[SPHEREINDEX.ZMIN].point[2] = + this.originalClippingPlanes[PLANEINDEX.ZMIN].origin[2]; + this.sphereStates[SPHEREINDEX.ZMAX].point[2] = + this.originalClippingPlanes[PLANEINDEX.ZMAX].origin[2]; + + // Update all sphere actors' positions + [ + SPHEREINDEX.XMIN, + SPHEREINDEX.XMAX, + SPHEREINDEX.YMIN, + SPHEREINDEX.YMAX, + SPHEREINDEX.ZMIN, + SPHEREINDEX.ZMAX, + ].forEach((idx) => { + const s = this.sphereStates[idx]; + s.sphereSource.setCenter(...s.point); + s.sphereSource.modified(); + }); + + // Update corners and edges as well + this._updateCornerSpheres(); + } + + // Show/hide actors + this._updateHandlesVisibility(); + + // Render + const viewportsInfo = this._getViewportsInfo(); + const [viewport3D] = viewportsInfo; + const renderingEngine = getRenderingEngine(viewport3D.renderingEngineId); + const viewport = renderingEngine.getViewport(viewport3D.viewportId); + viewport.render(); + } + + /** + * Gets the current visibility state of the cropping handles. + * + * @returns Whether the cropping handles (spheres and edge lines) are currently visible + * + * @example + * ```typescript + * // Check if handles are currently visible + * const handlesVisible = volumeCroppingTool.getHandlesVisible(); + * if (handlesVisible) { + * console.log('Cropping handles are currently shown'); + * } else { + * console.log('Cropping handles are currently hidden'); + * } + * ``` + * + * @remarks + * This method returns the configuration state, which controls the visibility of: + * - Face spheres (6 spheres for individual axis cropping) + * - Corner spheres (8 spheres for multi-axis cropping) + * - Edge lines connecting the corner spheres + */ + getHandlesVisible() { + return this.configuration.showHandles; + } + + /** + * Gets the current visibility state of the clipping planes. + * + * @returns Whether the clipping planes are currently visible and actively cropping the volume + * + * @example + * ```typescript + * // Check if clipping planes are currently active + * const planesVisible = volumeCroppingTool.getClippingPlanesVisible(); + * if (planesVisible) { + * console.log('Volume is currently being cropped'); + * } else { + * console.log('Volume is displayed in full'); + * } + * ``` + * + * @remarks + * This method returns the configuration state that controls whether: + * - The volume rendering respects the current clipping plane boundaries + * - Parts of the volume outside the crop bounds are hidden from view + * - The cropping effect is applied to the 3D volume visualization + */ + getClippingPlanesVisible() { + return this.configuration.showClippingPlanes; + } + + /** + * Sets the visibility of the clipping planes to enable or disable volume cropping. + * + * When clipping planes are visible, the volume rendering is cropped according to the + * current sphere positions. When disabled, the full volume is displayed without cropping. + * The viewport is automatically re-rendered after the change. + * + * @param visible - Whether to enable (true) or disable (false) volume clipping + * + * @example + * ```typescript + * // Enable volume cropping + * volumeCroppingTool.setClippingPlanesVisible(true); + * + * // Disable volume cropping to show full volume + * volumeCroppingTool.setClippingPlanesVisible(false); + * ``` + * + * @remarks + * - When enabled, parts of the volume outside the crop bounds are hidden + * - When disabled, all clipping planes are removed from the volume mapper + * - The cropping bounds are determined by the current sphere positions + * - The viewport is automatically re-rendered to reflect the change + * - This method updates the internal configuration and applies changes immediately + */ + setClippingPlanesVisible(visible: boolean) { + this.configuration.showClippingPlanes = visible; + const viewport = this._getViewport(); + this._updateClippingPlanes(viewport); + viewport.render(); + } + + _dragCallback(evt: EventTypes.InteractionEventType): void { + const { element, currentPoints, lastPoints } = evt.detail; + + if (this.draggingSphereIndex !== null) { + // crop handles + this._onMouseMoveSphere(evt); + } else { + // rotate + const currentPointsCanvas = currentPoints.canvas; + const lastPointsCanvas = lastPoints.canvas; + const { rotateIncrementDegrees } = this.configuration; + const enabledElement = getEnabledElement(element); + const { viewport } = enabledElement; + + const camera = viewport.getCamera(); + const width = element.clientWidth; + const height = element.clientHeight; + + const normalizedPosition = [ + currentPointsCanvas[0] / width, + currentPointsCanvas[1] / height, + ]; + + const normalizedPreviousPosition = [ + lastPointsCanvas[0] / width, + lastPointsCanvas[1] / height, + ]; + + const center: Types.Point2 = [width * 0.5, height * 0.5]; + // NOTE: centerWorld corresponds to the focal point in cornerstone3D + const centerWorld = viewport.canvasToWorld(center); + const normalizedCenter = [0.5, 0.5]; + + const radsq = (1.0 + Math.abs(normalizedCenter[0])) ** 2.0; + const op = [normalizedPreviousPosition[0], 0, 0]; + const oe = [normalizedPosition[0], 0, 0]; + + const opsq = op[0] ** 2; + const oesq = oe[0] ** 2; + + const lop = opsq > radsq ? 0 : Math.sqrt(radsq - opsq); + const loe = oesq > radsq ? 0 : Math.sqrt(radsq - oesq); + + const nop: Types.Point3 = [op[0], 0, lop]; + vtkMath.normalize(nop); + const noe: Types.Point3 = [oe[0], 0, loe]; + vtkMath.normalize(noe); + + const dot = vtkMath.dot(nop, noe); + if (Math.abs(dot) > 0.0001) { + const angleX = + -2 * + Math.acos(vtkMath.clampValue(dot, -1.0, 1.0)) * + Math.sign(normalizedPosition[0] - normalizedPreviousPosition[0]) * + rotateIncrementDegrees; + + const upVec = camera.viewUp; + const atV = camera.viewPlaneNormal; + const rightV: Types.Point3 = [0, 0, 0]; + const forwardV: Types.Point3 = [0, 0, 0]; + + vtkMath.cross(upVec, atV, rightV); + vtkMath.normalize(rightV); + + vtkMath.cross(atV, rightV, forwardV); + vtkMath.normalize(forwardV); + vtkMath.normalize(upVec); + + this._rotateCamera(viewport, centerWorld, forwardV, angleX); + + const angleY = + (normalizedPreviousPosition[1] - normalizedPosition[1]) * + rotateIncrementDegrees; + + this._rotateCamera(viewport, centerWorld, rightV, angleY); + } + + viewport.render(); + } + } + + _onMouseMoveSphere = (evt) => { + if (this.draggingSphereIndex === null) { + return false; + } + + const sphereState = this.sphereStates[this.draggingSphereIndex]; + if (!sphereState) { + return false; + } + + // Get viewport and world coordinates + const { viewport, world } = this._getViewportAndWorldCoords(evt); + if (!viewport || !world) { + return false; + } + + // Handle sphere movement based on type WITHOUT updating clipping planes yet + if (sphereState.isCorner) { + // Calculate and update just the dragged corner position + const newCorner = this._calculateNewCornerPosition(world); + this._updateSpherePosition(sphereState, newCorner); + + // Update related corners + const axisFlags = this._parseCornerKey(sphereState.uid); + this._updateRelatedCorners(sphereState, newCorner, axisFlags); + + // Update face spheres and corners + this._updateFaceSpheresFromCorners(); + this._updateCornerSpheres(); + } else { + // Update face sphere position + const axisIdx = { x: 0, y: 1, z: 2 }[sphereState.axis]; + let newValue = world[axisIdx]; + if (this.faceDragOffset !== null) { + newValue += this.faceDragOffset; + } + sphereState.point[axisIdx] = newValue; + sphereState.sphereSource.setCenter(...sphereState.point); + sphereState.sphereSource.modified(); + + // Update corners from face spheres + this._updateCornerSpheresFromFaces(); + this._updateFaceSpheresFromCorners(); + this._updateCornerSpheres(); + } + + // // THEN update clipping planes + this._updateClippingPlanesFromFaceSpheres(viewport); + + // Final render and event trigger + viewport.render(); + + this._triggerToolChangedEvent(sphereState); + + return true; + }; + + _onControlToolChange = (evt) => { + const viewport = this._getViewport(); + if (!evt.detail.toolCenter) { + triggerEvent(eventTarget, Events.VOLUMECROPPING_TOOL_CHANGED, { + originalClippingPlanes: this.originalClippingPlanes, + viewportId: viewport.id, + renderingEngineId: viewport.renderingEngineId, + seriesInstanceUID: this.seriesInstanceUID, + }); + } else { + if (evt.detail.seriesInstanceUID !== this.seriesInstanceUID) { + return; + } + const isMin = evt.detail.handleType === 'min'; + const toolCenter = isMin + ? evt.detail.toolCenterMin + : evt.detail.toolCenterMax; + const normals = isMin + ? [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1], + ] + : [ + [-1, 0, 0], + [0, -1, 0], + [0, 0, -1], + ]; + const planeIndices = isMin + ? [PLANEINDEX.XMIN, PLANEINDEX.YMIN, PLANEINDEX.ZMIN] + : [PLANEINDEX.XMAX, PLANEINDEX.YMAX, PLANEINDEX.ZMAX]; + const sphereIndices = isMin + ? [SPHEREINDEX.XMIN, SPHEREINDEX.YMIN, SPHEREINDEX.ZMIN] + : [SPHEREINDEX.XMAX, SPHEREINDEX.YMAX, SPHEREINDEX.ZMAX]; + const axes = ['x', 'y', 'z']; + const orientationAxes = [ + Enums.OrientationAxis.SAGITTAL, + Enums.OrientationAxis.CORONAL, + Enums.OrientationAxis.AXIAL, + ]; + + // Update planes and spheres for each axis + for (let i = 0; i < 3; ++i) { + const origin: [number, number, number] = [0, 0, 0]; + origin[i] = toolCenter[i]; + const plane = vtkPlane.newInstance({ + origin, + normal: normals[i] as [number, number, number], + }); + this.originalClippingPlanes[planeIndices[i]].origin = plane.getOrigin(); + + // Update face sphere + this.sphereStates[sphereIndices[i]].point[i] = plane.getOrigin()[i]; + this.sphereStates[sphereIndices[i]].sphereSource.setCenter( + ...this.sphereStates[sphereIndices[i]].point + ); + this.sphereStates[sphereIndices[i]].sphereSource.modified(); + + // Update center for other face spheres (not on this axis) + const otherSphere = this.sphereStates.find( + (s, idx) => s.axis === axes[i] && idx !== sphereIndices[i] + ); + const newCenter = (otherSphere.point[i] + plane.getOrigin()[i]) / 2; + this.sphereStates.forEach((state) => { + if ( + !state.isCorner && + state.axis !== axes[i] && + !evt.detail.viewportOrientation.includes(orientationAxes[i]) + ) { + state.point[i] = newCenter; + state.sphereSource.setCenter(state.point); + state.sphereActor.getProperty().setColor(state.color); + state.sphereSource.modified(); + } + }); + + // Update vtk clipping plane origin + const volumeActor = viewport.getDefaultActor()?.actor; + if (volumeActor) { + const mapper = volumeActor.getMapper() as vtkVolumeMapper; + const clippingPlanes = mapper.getClippingPlanes(); + if (clippingPlanes) { + clippingPlanes[planeIndices[i]].setOrigin(plane.getOrigin()); + } + } + } + this._updateCornerSpheres(); + viewport.render(); + } + }; + _updateClippingPlanes(viewport) { + // Get the actor and transformation matrix + const actorEntry = viewport.getDefaultActor(); + if (!actorEntry || !actorEntry.actor) { + // Only warn once per session for missing actor + if (!viewport._missingActorWarned) { + console.warn( + 'VolumeCroppingTool._updateClippingPlanes: No default actor found in viewport.' + ); + viewport._missingActorWarned = true; + } + return; + } + const actor = actorEntry.actor; + const mapper = actor.getMapper(); + const matrix = actor.getMatrix(); + + // Only update if clipping planes are visible + if (!this.configuration.showClippingPlanes) { + mapper.removeAllClippingPlanes(); + return; + } + + // Extract rotation part for normals + const rot = mat3.create(); + mat3.fromMat4(rot, matrix); + // Compute inverse transpose for normal transformation + const normalMatrix = mat3.create(); + mat3.invert(normalMatrix, rot); + mat3.transpose(normalMatrix, normalMatrix); + + // Cache transformed origins/normals to avoid repeated work + const originalPlanes = this.originalClippingPlanes; + if (!originalPlanes || !originalPlanes.length) { + return; + } + + // Only remove/add if the number of planes has changed or matrix has changed + // (Assume matrix changes frequently, so always update for now) + mapper.removeAllClippingPlanes(); + + // Preallocate arrays for transformed origins/normals + const transformedOrigins: Types.Point3[] = []; + const transformedNormals: Types.Point3[] = []; + + for (let i = 0; i < originalPlanes.length; ++i) { + const plane = originalPlanes[i]; + // Transform origin (full 4x4) + const oVec = vec3.create(); + vec3.transformMat4(oVec, new Float32Array(plane.origin), matrix); + const o: [number, number, number] = [oVec[0], oVec[1], oVec[2]]; + // Transform normal (rotation only) + const nVec = vec3.create(); + vec3.transformMat3(nVec, new Float32Array(plane.normal), normalMatrix); + vec3.normalize(nVec, nVec); + const n: [number, number, number] = [nVec[0], nVec[1], nVec[2]]; + transformedOrigins.push(o); + transformedNormals.push(n); + } + + // Create and add planes in a single loop + for (let i = 0; i < transformedOrigins.length; ++i) { + // Use cached transformed values + const planeInstance = vtkPlane.newInstance({ + origin: transformedOrigins[i], + normal: transformedNormals[i], + }); + mapper.addClippingPlane(planeInstance); + } + } + + _updateHandlesVisibility() { + // Spheres + this.sphereStates.forEach((state) => { + if (state.sphereActor) { + state.sphereActor.setVisibility(this.configuration.showHandles); + } + }); + + // Edge lines (box edges) + Object.values(this.edgeLines).forEach(({ actor }) => { + if (actor) { + actor.setVisibility(this.configuration.showHandles); + } + }); + } + + _addLine3DBetweenPoints( + viewport, + point1, + point2, + color: [number, number, number] = [0.7, 0.7, 0.7], + uid = '' + ) { + // Avoid creating a line if the points are the same + if ( + point1[0] === point2[0] && + point1[1] === point2[1] && + point1[2] === point2[2] + ) { + return { actor: null, source: null }; + } + const points = vtkPoints.newInstance(); + points.setNumberOfPoints(2); + points.setPoint(0, point1[0], point1[1], point1[2]); + points.setPoint(1, point2[0], point2[1], point2[2]); + + const lines = vtkCellArray.newInstance({ values: [2, 0, 1] }); + const polyData = vtkPolyData.newInstance(); + polyData.setPoints(points); + polyData.setLines(lines); + + const mapper = vtkMapper.newInstance(); + mapper.setInputData(polyData); + const actor = vtkActor.newInstance(); + actor.setMapper(mapper); + actor.getProperty().setColor(...color); + actor.getProperty().setLineWidth(0.5); // Thinner line + actor.getProperty().setOpacity(1.0); + actor.getProperty().setInterpolationToFlat(); // No shading + actor.getProperty().setAmbient(1.0); // Full ambient + actor.getProperty().setDiffuse(0.0); // No diffuse + actor.getProperty().setSpecular(0.0); // No specular + actor.setVisibility(this.configuration.showHandles); + viewport.addActor({ actor, uid }); + return { actor, source: polyData }; + } + + _getViewportsInfo = () => { + const viewports = getToolGroup(this.toolGroupId).viewportsInfo; + return viewports; + }; + + _addSphere( + viewport, + point, + axis, + position, + cornerKey = null, + adaptiveRadius + ) { + // Generate a unique UID for each sphere based on its axis and position + // Use cornerKey for corners, otherwise axis+position for faces + const uid = cornerKey ? `corner_${cornerKey}` : `${axis}_${position}`; + const sphereState = this.sphereStates.find((s) => s.uid === uid); + if (sphereState) { + return; + } + const sphereSource = vtkSphereSource.newInstance(); + sphereSource.setCenter(point); + sphereSource.setRadius(adaptiveRadius); + const sphereMapper = vtkMapper.newInstance(); + sphereMapper.setInputConnection(sphereSource.getOutputPort()); + const sphereActor = vtkActor.newInstance(); + sphereActor.setMapper(sphereMapper); + let color: [number, number, number] = [0.0, 1.0, 0.0]; // Default green + const sphereColors = this.configuration.sphereColors || {}; + + if (cornerKey) { + color = sphereColors.CORNERS || [0.0, 0.0, 1.0]; // Use corners color from config, fallback to blue + } else if (axis === 'z') { + color = sphereColors.AXIAL || [1.0, 0.0, 0.0]; // Z-axis = AXIAL planes + } else if (axis === 'x') { + color = sphereColors.SAGITTAL || [1.0, 1.0, 0.0]; // X-axis = SAGITTAL planes + } else if (axis === 'y') { + color = sphereColors.CORONAL || [0.0, 1.0, 0.0]; // Y-axis = CORONAL planes + } + // Store or update the sphere position + const idx = this.sphereStates.findIndex((s) => s.uid === uid); + if (idx === -1) { + this.sphereStates.push({ + point: point.slice(), + axis, + uid, + sphereSource, + sphereActor, + isCorner: !!cornerKey, + color, + }); + } else { + this.sphereStates[idx].point = point.slice(); + this.sphereStates[idx].sphereSource = sphereSource; + } + + // Remove existing actor with this UID if present + const existingActors = viewport.getActors(); + const existing = existingActors.find((a) => a.uid === uid); + if (existing) { + //viewport.removeActor(uid); + return; + } + sphereActor.getProperty().setColor(color); + sphereActor.setVisibility(this.configuration.showHandles); + viewport.addActor({ actor: sphereActor, uid: uid }); + } + /** + * Calculate an adaptive sphere radius based on the diagonal of the volume. + * This allows the sphere size to scale with the volume size. + * @param diagonal The diagonal length of the volume in world coordinates. + * @returns The calculated adaptive radius, clamped between min and max limits. + */ + _calculateAdaptiveSphereRadius(diagonal): number { + // Get base radius from configuration (acts as a scaling factor) + const baseRadius = + this.configuration.sphereRadius !== undefined + ? this.configuration.sphereRadius + : 8; + + // Scale radius as a percentage of diagonal (adjustable factor) + const scaleFactor = this.configuration.sphereRadiusScale || 0.01; // 1% of diagonal by default + const adaptiveRadius = diagonal * scaleFactor; + + // Apply min/max limits to prevent too small or too large spheres + const minRadius = this.configuration.minSphereRadius || 2; + const maxRadius = this.configuration.maxSphereRadius || 50; + + return Math.max(minRadius, Math.min(maxRadius, adaptiveRadius)); + } + + _initialize3DViewports = (viewportsInfo): void => { + if (!viewportsInfo || !viewportsInfo.length || !viewportsInfo[0]) { + console.warn( + 'VolumeCroppingTool: No viewportsInfo available for initialization of volumecroppingtool.' + ); + return; + } + const viewport = this._getViewport(); + const volumeActors = viewport.getActors(); + if (!volumeActors || volumeActors.length === 0) { + console.warn( + 'VolumeCroppingTool: No volume actors found in the viewport.' + ); + return; + } + const imageData = volumeActors[0].actor.getMapper().getInputData(); + if (!imageData) { + console.warn('VolumeCroppingTool: No image data found for volume actor.'); + return; + } + this.seriesInstanceUID = imageData.seriesInstanceUID || 'unknown'; + const worldBounds = imageData.getBounds(); // Already in world coordinates + const cropFactor = this.configuration.initialCropFactor || 0.1; + + // Calculate cropping bounds using world bounds + const xRange = worldBounds[1] - worldBounds[0]; + const yRange = worldBounds[3] - worldBounds[2]; + const zRange = worldBounds[5] - worldBounds[4]; + + const xMin = worldBounds[0] + cropFactor * xRange; + const xMax = worldBounds[1] - cropFactor * xRange; + const yMin = worldBounds[2] + cropFactor * yRange; + const yMax = worldBounds[3] - cropFactor * yRange; + const zMin = worldBounds[4] + cropFactor * zRange; + const zMax = worldBounds[5] - cropFactor * zRange; + + const planes: vtkPlane[] = []; + + // X min plane (cuts everything left of xMin) + const planeXmin = vtkPlane.newInstance({ + origin: [xMin, 0, 0], + normal: [1, 0, 0], + }); + const planeXmax = vtkPlane.newInstance({ + origin: [xMax, 0, 0], + normal: [-1, 0, 0], + }); + const planeYmin = vtkPlane.newInstance({ + origin: [0, yMin, 0], + normal: [0, 1, 0], + }); + const planeYmax = vtkPlane.newInstance({ + origin: [0, yMax, 0], + normal: [0, -1, 0], + }); + const planeZmin = vtkPlane.newInstance({ + origin: [0, 0, zMin], + normal: [0, 0, 1], + }); + const planeZmax = vtkPlane.newInstance({ + origin: [0, 0, zMax], + normal: [0, 0, -1], + }); + const mapper = viewport + .getDefaultActor() + .actor.getMapper() as vtkVolumeMapper; + planes.push(planeXmin); + planes.push(planeXmax); + planes.push(planeYmin); + planes.push(planeYmax); + planes.push(planeZmin); + planes.push(planeZmax); + + const originalPlanes = planes.map((plane) => ({ + origin: [...plane.getOrigin()], + normal: [...plane.getNormal()], + })); + + this.originalClippingPlanes = originalPlanes; + const sphereXminPoint = [xMin, (yMax + yMin) / 2, (zMax + zMin) / 2]; + const sphereXmaxPoint = [xMax, (yMax + yMin) / 2, (zMax + zMin) / 2]; + const sphereYminPoint = [(xMax + xMin) / 2, yMin, (zMax + zMin) / 2]; + const sphereYmaxPoint = [(xMax + xMin) / 2, yMax, (zMax + zMin) / 2]; + const sphereZminPoint = [(xMax + xMin) / 2, (yMax + yMin) / 2, zMin]; + const sphereZmaxPoint = [(xMax + xMin) / 2, (yMax + yMin) / 2, zMax]; + const adaptiveRadius = this._calculateAdaptiveSphereRadius( + Math.sqrt(xRange * xRange + yRange * yRange + zRange * zRange) + ); + this._addSphere( + viewport, + sphereXminPoint, + 'x', + 'min', + null, + adaptiveRadius + ); + this._addSphere( + viewport, + sphereXmaxPoint, + 'x', + 'max', + null, + adaptiveRadius + ); + this._addSphere( + viewport, + sphereYminPoint, + 'y', + 'min', + null, + adaptiveRadius + ); + this._addSphere( + viewport, + sphereYmaxPoint, + 'y', + 'max', + null, + adaptiveRadius + ); + this._addSphere( + viewport, + sphereZminPoint, + 'z', + 'min', + null, + adaptiveRadius + ); + this._addSphere( + viewport, + sphereZmaxPoint, + 'z', + 'max', + null, + adaptiveRadius + ); + + const corners = [ + [xMin, yMin, zMin], + [xMin, yMin, zMax], + [xMin, yMax, zMin], + [xMin, yMax, zMax], + [xMax, yMin, zMin], + [xMax, yMin, zMax], + [xMax, yMax, zMin], + [xMax, yMax, zMax], + ]; + + const cornerKeys = [ + 'XMIN_YMIN_ZMIN', + 'XMIN_YMIN_ZMAX', + 'XMIN_YMAX_ZMIN', + 'XMIN_YMAX_ZMAX', + 'XMAX_YMIN_ZMIN', + 'XMAX_YMIN_ZMAX', + 'XMAX_YMAX_ZMIN', + 'XMAX_YMAX_ZMAX', + ]; + + for (let i = 0; i < corners.length; i++) { + this._addSphere( + viewport, + corners[i], + 'corner', + null, + cornerKeys[i], + adaptiveRadius + ); + } + + // draw the lines between corners + // All 12 edges as pairs of corner keys + const edgeCornerPairs = [ + // X edges + ['XMIN_YMIN_ZMIN', 'XMAX_YMIN_ZMIN'], + ['XMIN_YMIN_ZMAX', 'XMAX_YMIN_ZMAX'], + ['XMIN_YMAX_ZMIN', 'XMAX_YMAX_ZMIN'], + ['XMIN_YMAX_ZMAX', 'XMAX_YMAX_ZMAX'], + // Y edges + ['XMIN_YMIN_ZMIN', 'XMIN_YMAX_ZMIN'], + ['XMIN_YMIN_ZMAX', 'XMIN_YMAX_ZMAX'], + ['XMAX_YMIN_ZMIN', 'XMAX_YMAX_ZMIN'], + ['XMAX_YMIN_ZMAX', 'XMAX_YMAX_ZMAX'], + // Z edges + ['XMIN_YMIN_ZMIN', 'XMIN_YMIN_ZMAX'], + ['XMIN_YMAX_ZMIN', 'XMIN_YMAX_ZMAX'], + ['XMAX_YMIN_ZMIN', 'XMAX_YMIN_ZMAX'], + ['XMAX_YMAX_ZMIN', 'XMAX_YMAX_ZMAX'], + ]; + + edgeCornerPairs.forEach(([key1, key2], i) => { + const state1 = this.sphereStates.find((s) => s.uid === `corner_${key1}`); + const state2 = this.sphereStates.find((s) => s.uid === `corner_${key2}`); + if (state1 && state2) { + const uid = `edge_${key1}_${key2}`; + const { actor, source } = this._addLine3DBetweenPoints( + viewport, + state1.point, + state2.point, + [0.7, 0.7, 0.7], + uid + ); + this.edgeLines[uid] = { actor, source, key1, key2 }; + } + }); + + mapper.addClippingPlane(planeXmin); + mapper.addClippingPlane(planeXmax); + mapper.addClippingPlane(planeYmin); + mapper.addClippingPlane(planeYmax); + mapper.addClippingPlane(planeZmin); + mapper.addClippingPlane(planeZmax); + + eventTarget.addEventListener( + Events.VOLUMECROPPINGCONTROL_TOOL_CHANGED, + (evt) => { + this._onControlToolChange(evt); + } + ); + viewport.render(); + }; + + // Helper method to get viewport and world coordinates + _getViewportAndWorldCoords = (evt) => { + const viewport = this._getViewport(); + const x = evt.detail.currentPoints.canvas[0]; + const y = evt.detail.currentPoints.canvas[1]; + const world = viewport.canvasToWorld([x, y]); + + return { viewport, world }; + }; + + _getViewport = () => { + const [viewport3D] = this._getViewportsInfo(); + const renderingEngine = getRenderingEngine(viewport3D.renderingEngineId); + return renderingEngine.getViewport(viewport3D.viewportId); + }; + + // Handle corner sphere movement + _handleCornerSphereMovement = (sphereState, world, viewport) => { + // Calculate new position with offset + const newCorner = this._calculateNewCornerPosition(world); + + // Update the dragged corner + this._updateSpherePosition(sphereState, newCorner); + + // Parse corner key to determine axes + const axisFlags = this._parseCornerKey(sphereState.uid); + + // Update related corners efficiently + this._updateRelatedCorners(sphereState, newCorner, axisFlags); + + // Update dependent elements + this._updateAfterCornerMovement(viewport); + }; + + // Handle face sphere movement + _handleFaceSphereMovement = (sphereState, world, viewport) => { + const axisIdx = { x: 0, y: 1, z: 2 }[sphereState.axis]; + let newValue = world[axisIdx]; + + if (this.faceDragOffset !== null) { + newValue += this.faceDragOffset; + } + + // Update only the relevant axis + sphereState.point[axisIdx] = newValue; + sphereState.sphereSource.setCenter(...sphereState.point); + sphereState.sphereSource.modified(); + + // Update dependent elements + this._updateAfterFaceMovement(viewport); + }; + + // Calculate new corner position with offset + _calculateNewCornerPosition = (world) => { + let newCorner = [world[0], world[1], world[2]]; + + if (this.cornerDragOffset) { + newCorner = [ + world[0] + this.cornerDragOffset[0], + world[1] + this.cornerDragOffset[1], + world[2] + this.cornerDragOffset[2], + ]; + } + + return newCorner; + }; + + // Parse corner key to determine which axes are min/max + _parseCornerKey = (uid) => { + const cornerKey = uid.replace('corner_', ''); + return { + isXMin: cornerKey.includes('XMIN'), + isXMax: cornerKey.includes('XMAX'), + isYMin: cornerKey.includes('YMIN'), + isYMax: cornerKey.includes('YMAX'), + isZMin: cornerKey.includes('ZMIN'), + isZMax: cornerKey.includes('ZMAX'), + }; + }; + + // Update sphere position + _updateSpherePosition = (sphereState, newPosition) => { + sphereState.point = newPosition; + sphereState.sphereSource.setCenter(...newPosition); + sphereState.sphereSource.modified(); + }; + + // Update related corners that share axes + _updateRelatedCorners = (draggedSphere, newCorner, axisFlags) => { + this.sphereStates.forEach((state) => { + if (!state.isCorner || state === draggedSphere) { + return; + } + + const key = state.uid.replace('corner_', ''); + const shouldUpdate = this._shouldUpdateCorner(key, axisFlags); + + if (shouldUpdate) { + this._updateCornerCoordinates(state, newCorner, key, axisFlags); + } + }); + }; + + // Determine if a corner should be updated + _shouldUpdateCorner = (cornerKey, axisFlags) => { + return ( + (axisFlags.isXMin && cornerKey.includes('XMIN')) || + (axisFlags.isXMax && cornerKey.includes('XMAX')) || + (axisFlags.isYMin && cornerKey.includes('YMIN')) || + (axisFlags.isYMax && cornerKey.includes('YMAX')) || + (axisFlags.isZMin && cornerKey.includes('ZMIN')) || + (axisFlags.isZMax && cornerKey.includes('ZMAX')) + ); + }; + + // Update individual corner coordinates + _updateCornerCoordinates = (state, newCorner, cornerKey, axisFlags) => { + // X axis updates + if ( + (axisFlags.isXMin && cornerKey.includes('XMIN')) || + (axisFlags.isXMax && cornerKey.includes('XMAX')) + ) { + state.point[0] = newCorner[0]; + } + + // Y axis updates + if ( + (axisFlags.isYMin && cornerKey.includes('YMIN')) || + (axisFlags.isYMax && cornerKey.includes('YMAX')) + ) { + state.point[1] = newCorner[1]; + } + + // Z axis updates + if ( + (axisFlags.isZMin && cornerKey.includes('ZMIN')) || + (axisFlags.isZMax && cornerKey.includes('ZMAX')) + ) { + state.point[2] = newCorner[2]; + } + + // Apply changes + state.sphereSource.setCenter(...state.point); + state.sphereSource.modified(); + }; + + // Update after corner movement + _updateAfterCornerMovement = (viewport) => { + this._updateFaceSpheresFromCorners(); + this._updateCornerSpheres(); + this._updateClippingPlanesFromFaceSpheres(viewport); + }; + + // Update after face movement + _updateAfterFaceMovement = (viewport) => { + this._updateCornerSpheresFromFaces(); + // this._updateFaceSpheresFromCorners(); + // this._updateCornerSpheres(); + this._updateClippingPlanesFromFaceSpheres(viewport); + }; + + // Trigger tool changed event + _triggerToolChangedEvent = (sphereState) => { + triggerEvent(eventTarget, Events.VOLUMECROPPING_TOOL_CHANGED, { + toolCenter: sphereState.point, + axis: sphereState.isCorner ? 'corner' : sphereState.axis, + draggingSphereIndex: this.draggingSphereIndex, + seriesInstanceUID: this.seriesInstanceUID, + }); + }; + + _updateClippingPlanesFromFaceSpheres(viewport) { + const mapper = viewport.getDefaultActor().actor.getMapper(); + // Update origins in originalClippingPlanes + this.originalClippingPlanes[0].origin = [ + ...this.sphereStates[SPHEREINDEX.XMIN].point, + ]; + this.originalClippingPlanes[1].origin = [ + ...this.sphereStates[SPHEREINDEX.XMAX].point, + ]; + this.originalClippingPlanes[2].origin = [ + ...this.sphereStates[SPHEREINDEX.YMIN].point, + ]; + this.originalClippingPlanes[3].origin = [ + ...this.sphereStates[SPHEREINDEX.YMAX].point, + ]; + this.originalClippingPlanes[4].origin = [ + ...this.sphereStates[SPHEREINDEX.ZMIN].point, + ]; + this.originalClippingPlanes[5].origin = [ + ...this.sphereStates[SPHEREINDEX.ZMAX].point, + ]; + + mapper.removeAllClippingPlanes(); + for (let i = 0; i < 6; ++i) { + const origin = this.originalClippingPlanes[i].origin as [ + number, + number, + number, + ]; + const normal = this.originalClippingPlanes[i].normal as [ + number, + number, + number, + ]; + const plane = vtkPlane.newInstance({ + origin, + normal, + }); + mapper.addClippingPlane(plane); + } + } + + _updateCornerSpheresFromFaces() { + // Get face sphere positions + const xMin = this.sphereStates[SPHEREINDEX.XMIN].point[0]; + const xMax = this.sphereStates[SPHEREINDEX.XMAX].point[0]; + const yMin = this.sphereStates[SPHEREINDEX.YMIN].point[1]; + const yMax = this.sphereStates[SPHEREINDEX.YMAX].point[1]; + const zMin = this.sphereStates[SPHEREINDEX.ZMIN].point[2]; + const zMax = this.sphereStates[SPHEREINDEX.ZMAX].point[2]; + + const corners = [ + { key: 'XMIN_YMIN_ZMIN', pos: [xMin, yMin, zMin] }, + { key: 'XMIN_YMIN_ZMAX', pos: [xMin, yMin, zMax] }, + { key: 'XMIN_YMAX_ZMIN', pos: [xMin, yMax, zMin] }, + { key: 'XMIN_YMAX_ZMAX', pos: [xMin, yMax, zMax] }, + { key: 'XMAX_YMIN_ZMIN', pos: [xMax, yMin, zMin] }, + { key: 'XMAX_YMIN_ZMAX', pos: [xMax, yMin, zMax] }, + { key: 'XMAX_YMAX_ZMIN', pos: [xMax, yMax, zMin] }, + { key: 'XMAX_YMAX_ZMAX', pos: [xMax, yMax, zMax] }, + ]; + + for (const corner of corners) { + const state = this.sphereStates.find( + (s) => s.uid === `corner_${corner.key}` + ); + if (state) { + state.point[0] = corner.pos[0]; + state.point[1] = corner.pos[1]; + state.point[2] = corner.pos[2]; + state.sphereSource.setCenter(...state.point); + state.sphereSource.modified(); + } + } + } + _updateFaceSpheresFromCorners() { + // Get all corner points + const corners = [ + this.sphereStates[SPHEREINDEX.XMIN_YMIN_ZMIN].point, + this.sphereStates[SPHEREINDEX.XMIN_YMIN_ZMAX].point, + this.sphereStates[SPHEREINDEX.XMIN_YMAX_ZMIN].point, + this.sphereStates[SPHEREINDEX.XMIN_YMAX_ZMAX].point, + this.sphereStates[SPHEREINDEX.XMAX_YMIN_ZMIN].point, + this.sphereStates[SPHEREINDEX.XMAX_YMIN_ZMAX].point, + this.sphereStates[SPHEREINDEX.XMAX_YMAX_ZMIN].point, + this.sphereStates[SPHEREINDEX.XMAX_YMAX_ZMAX].point, + ]; + + const xs = corners.map((p) => p[0]); + const ys = corners.map((p) => p[1]); + const zs = corners.map((p) => p[2]); + + const xMin = Math.min(...xs), + xMax = Math.max(...xs); + const yMin = Math.min(...ys), + yMax = Math.max(...ys); + const zMin = Math.min(...zs), + zMax = Math.max(...zs); + + // Face spheres should always be at the center of their face + this.sphereStates[SPHEREINDEX.XMIN].point = [ + xMin, + (yMin + yMax) / 2, + (zMin + zMax) / 2, + ]; + this.sphereStates[SPHEREINDEX.XMAX].point = [ + xMax, + (yMin + yMax) / 2, + (zMin + zMax) / 2, + ]; + this.sphereStates[SPHEREINDEX.YMIN].point = [ + (xMin + xMax) / 2, + yMin, + (zMin + zMax) / 2, + ]; + this.sphereStates[SPHEREINDEX.YMAX].point = [ + (xMin + xMax) / 2, + yMax, + (zMin + zMax) / 2, + ]; + this.sphereStates[SPHEREINDEX.ZMIN].point = [ + (xMin + xMax) / 2, + (yMin + yMax) / 2, + zMin, + ]; + this.sphereStates[SPHEREINDEX.ZMAX].point = [ + (xMin + xMax) / 2, + (yMin + yMax) / 2, + zMax, + ]; + + [ + SPHEREINDEX.XMIN, + SPHEREINDEX.XMAX, + SPHEREINDEX.YMIN, + SPHEREINDEX.YMAX, + SPHEREINDEX.ZMIN, + SPHEREINDEX.ZMAX, + ].forEach((idx) => { + const s = this.sphereStates[idx]; + s.sphereSource.setCenter(...s.point); + s.sphereSource.modified(); + }); + } + + _updateCornerSpheres() { + // Get face sphere positions + const xMin = this.sphereStates[SPHEREINDEX.XMIN].point[0]; + const xMax = this.sphereStates[SPHEREINDEX.XMAX].point[0]; + const yMin = this.sphereStates[SPHEREINDEX.YMIN].point[1]; + const yMax = this.sphereStates[SPHEREINDEX.YMAX].point[1]; + const zMin = this.sphereStates[SPHEREINDEX.ZMIN].point[2]; + const zMax = this.sphereStates[SPHEREINDEX.ZMAX].point[2]; + + // Define all 8 corners from face sphere positions + const corners = [ + { key: 'XMIN_YMIN_ZMIN', pos: [xMin, yMin, zMin] }, + { key: 'XMIN_YMIN_ZMAX', pos: [xMin, yMin, zMax] }, + { key: 'XMIN_YMAX_ZMIN', pos: [xMin, yMax, zMin] }, + { key: 'XMIN_YMAX_ZMAX', pos: [xMin, yMax, zMax] }, + { key: 'XMAX_YMIN_ZMIN', pos: [xMax, yMin, zMin] }, + { key: 'XMAX_YMIN_ZMAX', pos: [xMax, yMin, zMax] }, + { key: 'XMAX_YMAX_ZMIN', pos: [xMax, yMax, zMin] }, + { key: 'XMAX_YMAX_ZMAX', pos: [xMax, yMax, zMax] }, + ]; + + // Update corner spheres + for (const corner of corners) { + const state = this.sphereStates.find( + (s) => s.uid === `corner_${corner.key}` + ); + if (state) { + state.point[0] = corner.pos[0]; + state.point[1] = corner.pos[1]; + state.point[2] = corner.pos[2]; + state.sphereSource.setCenter(...state.point); + state.sphereSource.modified(); + } + } + + // Update edge lines to follow the corner spheres + Object.values(this.edgeLines).forEach(({ source, key1, key2 }) => { + const state1 = this.sphereStates.find((s) => s.uid === `corner_${key1}`); + const state2 = this.sphereStates.find((s) => s.uid === `corner_${key2}`); + if (state1 && state2) { + const points = source.getPoints(); + points.setPoint(0, state1.point[0], state1.point[1], state1.point[2]); + points.setPoint(1, state2.point[0], state2.point[1], state2.point[2]); + points.modified(); + source.modified(); + } + }); + } + + _onNewVolume = () => { + const viewportsInfo = this._getViewportsInfo(); + this.originalClippingPlanes = []; + this.sphereStates = []; + this.edgeLines = {}; + this._initialize3DViewports(viewportsInfo); + }; + + _unsubscribeToViewportNewVolumeSet(viewportsInfo) { + viewportsInfo.forEach(({ viewportId, renderingEngineId }) => { + const { viewport } = getEnabledElementByIds( + viewportId, + renderingEngineId + ); + const { element } = viewport; + + element.removeEventListener( + Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, + this._onNewVolume + ); + }); + } + + _subscribeToViewportNewVolumeSet(viewports) { + viewports.forEach(({ viewportId, renderingEngineId }) => { + const { viewport } = getEnabledElementByIds( + viewportId, + renderingEngineId + ); + const { element } = viewport; + + element.addEventListener( + Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, + this._onNewVolume + ); + }); + } + + _rotateCamera = (viewport, centerWorld, axis, angle) => { + const vtkCamera = viewport.getVtkActiveCamera(); + const viewUp = vtkCamera.getViewUp(); + const focalPoint = vtkCamera.getFocalPoint(); + const position = vtkCamera.getPosition(); + + const newPosition: Types.Point3 = [0, 0, 0]; + const newFocalPoint: Types.Point3 = [0, 0, 0]; + const newViewUp: Types.Point3 = [0, 0, 0]; + + const transform = mat4.identity(new Float32Array(16)); + mat4.translate(transform, transform, centerWorld); + mat4.rotate(transform, transform, angle, axis); + mat4.translate(transform, transform, [ + -centerWorld[0], + -centerWorld[1], + -centerWorld[2], + ]); + vec3.transformMat4(newPosition, position, transform); + vec3.transformMat4(newFocalPoint, focalPoint, transform); + + mat4.identity(transform); + mat4.rotate(transform, transform, angle, axis); + vec3.transformMat4(newViewUp, viewUp, transform); + + viewport.setCamera({ + position: newPosition, + viewUp: newViewUp, + focalPoint: newFocalPoint, + }); + }; +} + +VolumeCroppingTool.toolName = 'VolumeCropping'; +export default VolumeCroppingTool; diff --git a/packages/tools/src/tools/WindowLevelTool.ts b/packages/tools/src/tools/WindowLevelTool.ts index 664919f120..90478384b8 100644 --- a/packages/tools/src/tools/WindowLevelTool.ts +++ b/packages/tools/src/tools/WindowLevelTool.ts @@ -180,21 +180,22 @@ class WindowLevelTool extends BaseTool { const { voxelManager } = viewport.getImageData(); const middleSlicePixelData = voxelManager.getMiddleSliceData(); - const calculatedDynamicRange = middleSlicePixelData.reduce( - (acc, pixel) => { - return [Math.min(acc[0], pixel), Math.max(acc[1], pixel)]; - }, - [Infinity, -Infinity] - ); + const calculatedDynamicRange: [number, number] = + middleSlicePixelData.reduce( + (acc, pixel) => { + return [Math.min(acc[0], pixel), Math.max(acc[1], pixel)]; + }, + [Infinity, -Infinity] + ); const BitsStored = imageVolume?.metadata?.BitsStored; const metadataDynamicRange = BitsStored ? 2 ** BitsStored : Infinity; // Burned in Pixels often use pixel values above the BitsStored. // This results in a multiplier which is way higher than what you would // want in practice. Thus we take the min between the metadata dynamic - // range and actual middle slice dynamic range. + // range upper value and actual middle slice dynamic range. imageDynamicRange = Math.min( - calculatedDynamicRange, + calculatedDynamicRange[1] - calculatedDynamicRange[0], metadataDynamicRange ); } else { diff --git a/packages/tools/src/tools/annotation/CircleROITool.ts b/packages/tools/src/tools/annotation/CircleROITool.ts index 03642e0232..9aea758180 100644 --- a/packages/tools/src/tools/annotation/CircleROITool.ts +++ b/packages/tools/src/tools/annotation/CircleROITool.ts @@ -201,7 +201,7 @@ class CircleROITool extends AnnotationTool { Types.Point3, Types.Point3, Types.Point3, - Types.Point3 + Types.Point3, ]; } diff --git a/packages/tools/src/tools/annotation/CobbAngleTool.ts b/packages/tools/src/tools/annotation/CobbAngleTool.ts index d13e80be57..449281e59f 100644 --- a/packages/tools/src/tools/annotation/CobbAngleTool.ts +++ b/packages/tools/src/tools/annotation/CobbAngleTool.ts @@ -805,11 +805,11 @@ class CobbAngleTool extends AnnotationTool { const firstLine = [canvasCoordinates[0], canvasCoordinates[1]] as [ Types.Point2, - Types.Point2 + Types.Point2, ]; const secondLine = [canvasCoordinates[2], canvasCoordinates[3]] as [ Types.Point2, - Types.Point2 + Types.Point2, ]; let lineUID = 'line1'; @@ -1036,11 +1036,11 @@ class CobbAngleTool extends AnnotationTool { const firstLine = [canvasPoints[0], canvasPoints[1]] as [ Types.Point2, - Types.Point2 + Types.Point2, ]; const secondLine = [canvasPoints[2], canvasPoints[3]] as [ Types.Point2, - Types.Point2 + Types.Point2, ]; const mid1 = midPoint2(firstLine[0], firstLine[1]); diff --git a/packages/tools/src/tools/annotation/EllipticalROITool.ts b/packages/tools/src/tools/annotation/EllipticalROITool.ts index 40c5fdd049..ed483a0c2b 100644 --- a/packages/tools/src/tools/annotation/EllipticalROITool.ts +++ b/packages/tools/src/tools/annotation/EllipticalROITool.ts @@ -337,7 +337,7 @@ class EllipticalROITool extends AnnotationTool { Types.Point2, Types.Point2, Types.Point2, - Types.Point2 + Types.Point2, ]; const [bottom, top, left, right] = canvasCoordinates; diff --git a/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts b/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts index 33e69bc6f5..72d242fcad 100644 --- a/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts +++ b/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts @@ -997,8 +997,8 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { return a[index] === b[index] ? 0 : a[index] < b[index] - ? -1 - : 1; + ? -1 + : 1; }; })(0) ); diff --git a/packages/tools/src/tools/annotation/ProbeTool.ts b/packages/tools/src/tools/annotation/ProbeTool.ts index d18d582f38..a803d5632f 100644 --- a/packages/tools/src/tools/annotation/ProbeTool.ts +++ b/packages/tools/src/tools/annotation/ProbeTool.ts @@ -658,6 +658,7 @@ class ProbeTool extends AnnotationTool { Modality: modality, modalityUnit, }; + annotation.invalidated = true; } else { this.isHandleOutsideImage = true; cachedStats[targetId] = { diff --git a/packages/tools/src/tools/annotation/UltrasoundPleuraBLineTool/utils/generateConvexHullFromContour.ts b/packages/tools/src/tools/annotation/UltrasoundPleuraBLineTool/utils/generateConvexHullFromContour.ts index 2897a01f16..f99da3bdad 100644 --- a/packages/tools/src/tools/annotation/UltrasoundPleuraBLineTool/utils/generateConvexHullFromContour.ts +++ b/packages/tools/src/tools/annotation/UltrasoundPleuraBLineTool/utils/generateConvexHullFromContour.ts @@ -1,5 +1,5 @@ import type { Types } from '@cornerstonejs/core'; -import { utilities } from '@cornerstonejs/tools'; +import * as math from '../../../../utilities/math'; /** * Generate a convex hull from a contour by simplifying, smoothing, and computing the hull @@ -12,9 +12,9 @@ import { utilities } from '@cornerstonejs/tools'; */ export function generateConvexHullFromContour(contour: Array) { // 1) Simplify jagged bits (ε = e.g. 2px): - const simplified = utilities.math.polyline.decimate(contour, 2); + const simplified = math.polyline.decimate(contour, 2); // calculate convex hull - const hull = utilities.math.polyline.convexHull(simplified); + const hull = math.polyline.convexHull(simplified); return { simplified, hull }; } diff --git a/packages/tools/src/tools/annotation/VideoRedactionTool.ts b/packages/tools/src/tools/annotation/VideoRedactionTool.ts index e838d9e065..3c9a473ab2 100644 --- a/packages/tools/src/tools/annotation/VideoRedactionTool.ts +++ b/packages/tools/src/tools/annotation/VideoRedactionTool.ts @@ -305,8 +305,7 @@ class VideoRedactionTool extends AnnotationTool { const { points } = data.handles; // Move this handle. - // @ts-expect-error - points[handleIndex] = [...worldPos]; + points[handleIndex] = [...worldPos]; let bottomLeftCanvas; let bottomRightCanvas; @@ -389,8 +388,7 @@ class VideoRedactionTool extends AnnotationTool { triggerAnnotationRenderForViewportIds(viewportUIDsToRender); this.editData = null; - // @ts-expect-error - return annotation.metadata.annotationUID; + return annotation.annotationUID; } /** * Add event handlers for the modify event loop, and prevent default event prapogation. diff --git a/packages/tools/src/tools/base/BaseTool.ts b/packages/tools/src/tools/base/BaseTool.ts index 33c71818ff..3baf6d507b 100644 --- a/packages/tools/src/tools/base/BaseTool.ts +++ b/packages/tools/src/tools/base/BaseTool.ts @@ -308,6 +308,16 @@ abstract class BaseTool { } this.memo = null; } + + /** Starts a group recording of history memo, so that with a single undo you can undo multiple actions that are related to each other */ + public static startGroupRecording() { + DefaultHistoryMemo.startGroupRecording(); + } + + /** Ends a group recording of history memo */ + public static endGroupRecording() { + DefaultHistoryMemo.endGroupRecording(); + } } // Note: this is a workaround since terser plugin does not support static blocks diff --git a/packages/tools/src/tools/base/ContourBaseTool.ts b/packages/tools/src/tools/base/ContourBaseTool.ts index 8865be226c..bddfe41768 100644 --- a/packages/tools/src/tools/base/ContourBaseTool.ts +++ b/packages/tools/src/tools/base/ContourBaseTool.ts @@ -210,7 +210,6 @@ abstract class ContourBaseTool extends AnnotationTool { protected getPolylinePoints(annotation: ContourAnnotation): Types.Point3[] { // Attention: `contour.polyline` is the new way to store a polyline but it // may be undefined because it was `data.polyline` before (fallback) - // @ts-expect-error return annotation.data.contour?.polyline ?? annotation.data.polyline; } diff --git a/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts b/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts index afaadf4e9c..0b796731ef 100644 --- a/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts +++ b/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts @@ -32,6 +32,7 @@ import { getPolySeg } from '../../../config'; import { computeAndAddRepresentation } from '../../../utilities/segmentation/computeAndAddRepresentation'; import { triggerSegmentationDataModified } from '../../../stateManagement/segmentation/triggerSegmentationEvents'; import { defaultSegmentationStateManager } from '../../../stateManagement/segmentation/SegmentationStateManager'; +import type vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice'; // 255 itself is used as preview color, so basically // we have 254 colors to use for the segments if we are using the preview. @@ -200,6 +201,7 @@ async function render( } for (const labelmapActorEntry of labelmapActorEntries) { + // call the function to set the color and opacity _setLabelmapColorAndOpacity( viewport.id, labelmapActorEntry, @@ -312,7 +314,7 @@ function _setLabelmapColorAndOpacity( } ofun.setClamping(false); - const labelmapActor = labelmapActorEntry.actor as vtkVolume; + const labelmapActor = labelmapActorEntry.actor as vtkVolume | vtkImageSlice; // @ts-ignore - fix type in vtk const { preLoad } = labelmapActor.get?.('preLoad') || { preLoad: null }; @@ -328,7 +330,6 @@ function _setLabelmapColorAndOpacity( if (renderOutline) { // @ts-ignore - fix type in vtk labelmapActor.getProperty().setUseLabelOutline(renderOutline); - // @ts-ignore - fix type in vtk labelmapActor.getProperty().setLabelOutlineOpacity(outlineOpacity); @@ -358,7 +359,7 @@ function _setLabelmapColorAndOpacity( } labelmapActor.getProperty().setLabelOutlineThickness(outlineWidths); - + // Mark the actor as modified to ensure the changes are applied labelmapActor.modified(); labelmapActor.getProperty().modified(); labelmapActor.getMapper().modified(); diff --git a/packages/tools/src/tools/index.ts b/packages/tools/src/tools/index.ts index 35393622f9..186d2c7d76 100644 --- a/packages/tools/src/tools/index.ts +++ b/packages/tools/src/tools/index.ts @@ -1,6 +1,8 @@ import { BaseTool, AnnotationTool, AnnotationDisplayTool } from './base'; import PanTool from './PanTool'; import TrackballRotateTool from './TrackballRotateTool'; +import VolumeCroppingTool from './VolumeCroppingTool'; +import VolumeCroppingControlTool from './VolumeCroppingControlTool'; import WindowLevelTool from './WindowLevelTool'; import WindowLevelRegionTool from './WindowLevelRegionTool'; import StackScrollTool from './StackScrollTool'; @@ -72,6 +74,8 @@ export { // Manipulation Tools PanTool, TrackballRotateTool, + VolumeCroppingTool, + VolumeCroppingControlTool, DragProbeTool, WindowLevelTool, WindowLevelRegionTool, diff --git a/packages/tools/src/tools/segmentation/CircleROIStartEndThresholdTool.ts b/packages/tools/src/tools/segmentation/CircleROIStartEndThresholdTool.ts index 07741a17e3..26277e30ce 100644 --- a/packages/tools/src/tools/segmentation/CircleROIStartEndThresholdTool.ts +++ b/packages/tools/src/tools/segmentation/CircleROIStartEndThresholdTool.ts @@ -40,6 +40,7 @@ import type { EventTypes, SVGDrawingHelper, Annotation, + AnnotationMetadata, } from '../../types'; import type { CircleROIStartEndThresholdAnnotation, @@ -186,7 +187,7 @@ class CircleROIStartEndThresholdTool extends CircleROITool { Types.Point3, Types.Point3, Types.Point3, - Types.Point3 + Types.Point3, ]; } @@ -284,7 +285,8 @@ class CircleROIStartEndThresholdTool extends CircleROITool { resetElementCursor(element); - const enabledElement = getEnabledElement(element); + const { metadata } = annotation; + const { enabledElement } = metadata; this.editData = null; this.isDrawing = false; @@ -348,9 +350,11 @@ class CircleROIStartEndThresholdTool extends CircleROITool { for (let i = 0; i < annotations.length; i++) { const annotation = annotations[i] as CircleROIStartEndThresholdAnnotation; + const { annotationUID, data, metadata } = annotation; const { startCoordinate, endCoordinate } = data; const { points, activeHandleIndex } = data.handles; + const { enabledElement: annotationEnabledElement } = metadata; styleSpecifier.annotationUID = annotationUID; @@ -434,9 +438,20 @@ class CircleROIStartEndThresholdTool extends CircleROITool { ] = middleCoordinate; // WE HAVE TO CACHE STATS BEFORE FETCHING TEXT - - if (annotation.invalidated) { - this._throttledCalculateCachedStats(annotation, enabledElement); + const iteratorVolumeIDs = + // @ts-ignore + annotationEnabledElement.viewport?.volumeIds.values(); + + for (const volumeId of iteratorVolumeIDs) { + if ( + annotation.invalidated && + annotation.metadata.volumeId === volumeId + ) { + this._throttledCalculateCachedStats( + annotation, + annotationEnabledElement + ); + } } // If rendering engine has been destroyed while rendering @@ -578,7 +593,6 @@ class CircleROIStartEndThresholdTool extends CircleROITool { return renderStatus; }; - //Now works for axial, sagitall and coronal _computeProjectionPoints( annotation: CircleROIStartEndThresholdAnnotation, imageVolume: Types.IImageVolume @@ -588,44 +602,47 @@ class CircleROIStartEndThresholdTool extends CircleROITool { const { startCoordinate, endCoordinate } = data; const { points } = data.handles; - const handlesToStart = csUtils.deepClone(points) as Types.Point3[]; + const projectionAxisIndex = + this._getIndexOfCoordinatesForViewplaneNormal(viewPlaneNormal); const startWorld = vec3.clone(points[0]); + startWorld[projectionAxisIndex] = startCoordinate; + const endWorld = vec3.clone(points[0]); - const indexOfNormal = - this._getIndexOfCoordinatesForViewplaneNormal(viewPlaneNormal); + endWorld[projectionAxisIndex] = endCoordinate; - startWorld[indexOfNormal] = startCoordinate; - endWorld[indexOfNormal] = endCoordinate; + const direction = vec3.create(); + vec3.subtract(direction, endWorld, startWorld); - handlesToStart.forEach((handlePoint) => { - handlePoint[indexOfNormal] = startCoordinate; - }); + const distance = vec3.length(direction); - // distance between start and end slice in the world coordinate - const distance = vec3.distance(startWorld, endWorld); + if (distance === 0) { + const handlesOnStartPlane = points.map((p) => { + const newPoint = vec3.clone(p as vec3); + newPoint[projectionAxisIndex] = startCoordinate; + return Array.from(newPoint) as Types.Point3; + }); + data.cachedStats.projectionPoints = [handlesOnStartPlane]; + return; + } + + vec3.normalize(direction, direction); + + const handlesToStart = csUtils.deepClone(points) as typeof points; + handlesToStart[0][projectionAxisIndex] = startCoordinate; + handlesToStart[1][projectionAxisIndex] = startCoordinate; - // for each point inside points, navigate in the direction of the viewPlaneNormal - // with amount of spacingInNormal, and calculate the next slice until we reach the distance const newProjectionPoints = []; - if (distance >= 0) { - newProjectionPoints.push( - handlesToStart.map((p) => Array.from(p as vec3)) - ); - } - for ( - let dist = spacingInNormal; - dist <= distance; - dist += spacingInNormal - ) { + for (let dist = 0; dist <= distance + 1e-6; dist += spacingInNormal) { newProjectionPoints.push( handlesToStart.map((point) => { const newPoint = vec3.create(); - vec3.scaleAndAdd(newPoint, point as vec3, viewPlaneNormal, dist); - return Array.from(newPoint); + vec3.scaleAndAdd(newPoint, point as vec3, direction, dist); + return Array.from(newPoint) as Types.Point3; }) ); } + data.cachedStats.projectionPoints = newProjectionPoints; } @@ -692,6 +709,7 @@ class CircleROIStartEndThresholdTool extends CircleROITool { modalityUnitOptions ); + // console.debug(projectionPoints) for (let i = 0; i < projectionPoints.length; i++) { // If image does not exists for the targetId, skip. This can be due // to various reasons such as if the target was a volumeViewport, and @@ -786,6 +804,7 @@ class CircleROIStartEndThresholdTool extends CircleROITool { pointsInsideVolume.push(pointsInShape); } } + // console.debug(pointsInsideVolume) const stats = this.configuration.statsCalculator.getStatistics(); data.cachedStats.pointsInVolume = pointsInsideVolume; data.cachedStats.statistics = { @@ -802,6 +821,7 @@ class CircleROIStartEndThresholdTool extends CircleROITool { _calculateCachedStatsTool(annotation, enabledElement) { const data = annotation.data; + const { viewport } = enabledElement; const { cachedStats } = data; diff --git a/packages/tools/src/tools/segmentation/CircleScissorsTool.ts b/packages/tools/src/tools/segmentation/CircleScissorsTool.ts index 45c5cc85a6..5a9faf306e 100644 --- a/packages/tools/src/tools/segmentation/CircleScissorsTool.ts +++ b/packages/tools/src/tools/segmentation/CircleScissorsTool.ts @@ -380,7 +380,6 @@ class CircleScissorsTool extends LabelmapBaseTool { const radius = Math.abs(bottom[1] - Math.floor((bottom[1] + top[1]) / 2)); - // @ts-expect-error const color = `rgb(${toolMetadata.segmentColor.slice(0, 3)})`; // If rendering engine has been destroyed while rendering diff --git a/packages/tools/src/tools/segmentation/LabelmapBaseTool.ts b/packages/tools/src/tools/segmentation/LabelmapBaseTool.ts index a2348c03db..80c41e27be 100644 --- a/packages/tools/src/tools/segmentation/LabelmapBaseTool.ts +++ b/packages/tools/src/tools/segmentation/LabelmapBaseTool.ts @@ -187,6 +187,27 @@ export default class LabelmapBaseTool extends BaseTool { return LabelmapBaseTool.previewData; } + /** + * Checks if the tool has a preview data associated. + * @returns True if the tool has preview data, false otherwise. + */ + public hasPreviewData() { + return !!this._previewData.preview; + } + + /** + * Checks if the tool should resolve preview requests. + * This is used to determine if the tool is in a state where it can handle + * preview requests. + * @returns True if the tool should resolve preview requests, false otherwise. + */ + public shouldResolvePreviewRequests() { + return ( + (this.mode === 'Active' || this.mode === 'Enabled') && + this.hasPreviewData() + ); + } + /** * Creates a labelmap memo instance, which stores the changes made to the * labelmap rather than the initial state. @@ -455,6 +476,11 @@ export default class LabelmapBaseTool extends BaseTool { StrategyCallbacks.AddPreview ); _previewData.isDrag = true; + // If the results are modified, we store it as preview data + if (results?.modified) { + _previewData.preview = results; + _previewData.element = element; + } return results; } @@ -604,8 +630,8 @@ export default class LabelmapBaseTool extends BaseTool { const segmentIndex = hasBoth ? startValue : startValue === 0 - ? activeIndex - : 0; + ? activeIndex + : 0; for (let i = boundsIJK[0][0]; i <= boundsIJK[0][1]; i++) { for (let j = boundsIJK[1][0]; j <= boundsIJK[1][1]; j++) { for (let k = boundsIJK[2][0]; k <= boundsIJK[2][1]; k++) { diff --git a/packages/tools/src/tools/segmentation/LabelmapEditWithContour.ts b/packages/tools/src/tools/segmentation/LabelmapEditWithContour.ts index 192502e913..5bce524e02 100644 --- a/packages/tools/src/tools/segmentation/LabelmapEditWithContour.ts +++ b/packages/tools/src/tools/segmentation/LabelmapEditWithContour.ts @@ -164,7 +164,7 @@ class LabelMapEditWithContourTool extends PlanarFreehandContourSegmentationTool * representation. If not present, it automatically adds one to enable contour-based editing. * * @param viewportId - The ID of the viewport to check - * @returns Promise - True if contour representation is available or was successfully added, + * @returns Promise boolean or undefined - True if contour representation is available or was successfully added, * false if no active segmentation exists, undefined if already checked * * @remarks diff --git a/packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts b/packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts index 252eacf72c..72155cd609 100644 --- a/packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts +++ b/packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts @@ -260,7 +260,8 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool { resetElementCursor(element); - const enabledElement = getEnabledElement(element); + const { metadata } = annotation; + const { enabledElement } = metadata; this.editData = null; this.isDrawing = false; @@ -545,6 +546,7 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool { const { annotationUID, data, metadata } = annotation; const { startCoordinate, endCoordinate } = data; const { points, activeHandleIndex } = data.handles; + const { enabledElement: annotationEnabledElement } = metadata; const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p)); @@ -600,8 +602,20 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool { } // WE HAVE TO CACHE STATS BEFORE FETCHING TEXT - if (annotation.invalidated) { - this._throttledCalculateCachedStats(annotation, enabledElement); + const iteratorVolumeIDs = + // @ts-ignore + annotationEnabledElement.viewport?.volumeIds.values(); + + for (const volumeId of iteratorVolumeIDs) { + if ( + annotation.invalidated && + annotation.metadata.volumeId === volumeId + ) { + this._throttledCalculateCachedStats( + annotation, + annotationEnabledElement + ); + } } // if it is inside the start/end slice, but not exactly the first or diff --git a/packages/tools/src/tools/segmentation/RectangleScissorsTool.ts b/packages/tools/src/tools/segmentation/RectangleScissorsTool.ts index 87f2ffc8cf..2146daf485 100644 --- a/packages/tools/src/tools/segmentation/RectangleScissorsTool.ts +++ b/packages/tools/src/tools/segmentation/RectangleScissorsTool.ts @@ -383,7 +383,6 @@ class RectangleScissorsTool extends LabelmapBaseTool { const data = annotation.data; const { points } = data.handles; const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p)); - // @ts-expect-error const color = `rgb(${toolMetadata.segmentColor.slice(0, 3)})`; // If rendering engine has been destroyed while rendering diff --git a/packages/tools/src/tools/segmentation/SegmentLabelTool.ts b/packages/tools/src/tools/segmentation/SegmentLabelTool.ts index 049863689b..5e1902e2da 100644 --- a/packages/tools/src/tools/segmentation/SegmentLabelTool.ts +++ b/packages/tools/src/tools/segmentation/SegmentLabelTool.ts @@ -1,6 +1,8 @@ import { getEnabledElement } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; +import { config as segmentationConfig } from '../../stateManagement/segmentation'; + import { BaseTool } from '../base'; import type { PublicToolProps, @@ -14,7 +16,8 @@ import { getActiveSegmentation } from '../../stateManagement/segmentation/active import { getSegmentIndexAtWorldPoint } from '../../utilities/segmentation'; import { state } from '../../store/state'; import type { Segmentation } from '../../types/SegmentationStateTypes'; -import { drawLinkedTextBox as drawLinkedTextBoxSvg } from '../../drawingSvg'; +import { drawTextBox as drawTextBoxSvg } from '../../drawingSvg'; +import type { Point2 } from 'packages/core/dist/esm/types'; /** * Represents a tool used for segment selection. It is used to select a segment @@ -48,6 +51,8 @@ class SegmentLabelTool extends BaseTool { configuration: { hoverTimeout: 100, searchRadius: 6, // search for border in a 6px radius + color: null, + background: null, }, } ) { @@ -140,13 +145,20 @@ class SegmentLabelTool extends BaseTool { } ); const segment = activeSegmentation.segments[hoveredSegmentIndex]; + const color = + this.configuration.color ?? + segmentationConfig.color.getSegmentIndexColor( + viewport.id, + segmentationId, + hoveredSegmentIndex + ); const label = segment?.label; const canvasCoordinates = viewport.worldToCanvas(worldPoint); this._editData = { hoveredSegmentIndex, hoveredSegmentLabel: label, canvasCoordinates, - worldPoint, + color, }; // No need to select background @@ -176,24 +188,29 @@ class SegmentLabelTool extends BaseTool { hoveredSegmentIndex, hoveredSegmentLabel, canvasCoordinates, - worldPoint, + color, } = this._editData; if (!hoveredSegmentIndex) { return; } - const textBoxPosition = viewport.worldToCanvas(worldPoint); + const offset = -15; + const textBoxPosition = [ + canvasCoordinates[0] + offset, + canvasCoordinates[1] + offset, + ] as Point2; - const boundingBox = drawLinkedTextBoxSvg( + const boundingBox = drawTextBoxSvg( svgDrawingHelper, 'segmentSelectLabelAnnotation', 'segmentSelectLabelTextBox', - [hoveredSegmentLabel ? hoveredSegmentLabel : '(unnamed segment)'], + [hoveredSegmentLabel ?? '(unnamed segment)'], textBoxPosition, - [canvasCoordinates], - {}, - {} + { + color: `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]})`, + background: this.configuration.background ?? undefined, + } ); const left = canvasCoordinates[0]; diff --git a/packages/tools/src/tools/segmentation/SphereScissorsTool.ts b/packages/tools/src/tools/segmentation/SphereScissorsTool.ts index d004d4eb62..d76e284451 100644 --- a/packages/tools/src/tools/segmentation/SphereScissorsTool.ts +++ b/packages/tools/src/tools/segmentation/SphereScissorsTool.ts @@ -363,7 +363,6 @@ class SphereScissorsTool extends LabelmapBaseTool { const radius = Math.abs(bottom[1] - Math.floor((bottom[1] + top[1]) / 2)); - // @ts-expect-error const color = `rgb(${toolMetadata.segmentColor.slice(0, 3)})`; // If rendering engine has been destroyed while rendering diff --git a/packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts b/packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts index bdc91d687a..3e893cf5a9 100644 --- a/packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts +++ b/packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts @@ -51,6 +51,7 @@ export type InitializedOperationData = LabelmapToolOperationDataAny & { }; }; memo?: LabelmapMemo; + modified?: boolean; }; export type StrategyFunction = ( diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/preview.ts b/packages/tools/src/tools/segmentation/strategies/compositions/preview.ts index 1770ecf43d..66b9304e59 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/preview.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/preview.ts @@ -38,6 +38,7 @@ export default { [StrategyCallbacks.Initialize]: (operationData: InitializedOperationData) => { const { segmentIndex, previewColor, previewSegmentIndex } = operationData; + operationData.modified = false; if (previewSegmentIndex == null || segmentIndex == null) { return; } @@ -58,6 +59,7 @@ export default { previewColor as Types.Color ); }); + operationData.modified = true; }, [StrategyCallbacks.AcceptPreview]: ( diff --git a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts index cc6e39f5d2..d5ab593796 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts @@ -2,10 +2,6 @@ import { vec3 } from 'gl-matrix'; import { utilities as csUtils } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; -import { - getCanvasEllipseCorners, - precalculatePointInEllipse, -} from '../../../utilities/math/ellipse'; import { getBoundingBoxAroundShapeIJK } from '../../../utilities/boundingBox'; import BrushStrategy from './BrushStrategy'; import type { Composition, InitializedOperationData } from './BrushStrategy'; @@ -16,6 +12,24 @@ import { pointInSphere } from '../../../utilities/math/sphere'; const { transformWorldToIndex, isEqual } = csUtils; +/** + * Returns the corners of an ellipse in canvas coordinates. + * The corners are returned in the order: topLeft, bottomRight, bottomLeft, topRight. + * + * @param canvasCoordinates - The coordinates of the ellipse in the canvas. + * @returns An array of four points representing the corners of the ellipse. + */ +export function getEllipseCornersFromCanvasCoordinates( + canvasCoordinates: CanvasCoordinates +): Array { + const [bottom, top, left, right] = canvasCoordinates; + const topLeft = [left[0], top[1]]; + const bottomRight = [right[0], bottom[1]]; + const bottomLeft = [left[0], bottom[1]]; + const topRight = [right[0], top[1]]; + return [topLeft, bottomRight, bottomLeft, topRight]; +} + const initializeCircle = { [StrategyCallbacks.Initialize]: (operationData: InitializedOperationData) => { const { @@ -28,12 +42,17 @@ const initializeCircle = { if (!points) { return; } - // Average the points to get the center of the ellipse - const center = vec3.fromValues(0, 0, 0); - points.forEach((point) => { - vec3.add(center, center, point); - }); - vec3.scale(center, center, 1 / points.length); + + // Calculate the center as the midpoint between the first two points + // That calculation serves both for orthogonal and oblique planes + const center = vec3.create(); + if (points.length >= 2) { + vec3.add(center, points[0], points[1]); + vec3.scale(center, center, 0.5); + } else { + // Fallback to the first point if less than 2 points are provided + vec3.copy(center, points[0]); + } operationData.centerWorld = center as Types.Point3; operationData.centerIJK = transformWorldToIndex( @@ -45,15 +64,12 @@ const initializeCircle = { viewport.worldToCanvas(p) ) as CanvasCoordinates; - // 1. From the drawn tool: Get the ellipse (circle) topLeft and bottomRight - // corners in canvas coordinates - const [topLeftCanvas, bottomRightCanvas] = - getCanvasEllipseCorners(canvasCoordinates); - + // 1. From the drawn tool: Get the ellipse (circle) corners in canvas coordinates + const corners = getEllipseCornersFromCanvasCoordinates(canvasCoordinates); + const cornersInWorld = corners.map((corner) => + viewport.canvasToWorld(corner) + ); // 2. Find the extent of the ellipse (circle) in IJK index space of the image - const topLeftWorld = viewport.canvasToWorld(topLeftCanvas); - const bottomRightWorld = viewport.canvasToWorld(bottomRightCanvas); - const circleCornersIJK = points.map((world) => { return transformWorldToIndex(segmentationImageData, world); }); @@ -65,11 +81,8 @@ const initializeCircle = { segmentationImageData.getDimensions() ); - operationData.isInObject = createPointInEllipse({ - topLeftWorld, - bottomRightWorld, - center, - }); + // 3. Derives the ellipse function from the corners + operationData.isInObject = createPointInEllipse(cornersInWorld); operationData.isInObjectBoundsIJK = boundsIJK; }, @@ -83,23 +96,37 @@ const initializeCircle = { * sphere shape (same radius in two or three dimensions), or an elliptical shape * if they differ. */ -function createPointInEllipse(worldInfo: { - topLeftWorld: Types.Point3; - bottomRightWorld: Types.Point3; - center: Types.Point3 | vec3; -}) { - const { topLeftWorld, bottomRightWorld, center } = worldInfo; - - const xRadius = Math.abs(topLeftWorld[0] - bottomRightWorld[0]) / 2; - const yRadius = Math.abs(topLeftWorld[1] - bottomRightWorld[1]) / 2; - const zRadius = Math.abs(topLeftWorld[2] - bottomRightWorld[2]) / 2; - - const radius = Math.max(xRadius, yRadius, zRadius); - if ( - isEqual(xRadius, radius) && - isEqual(yRadius, radius) && - isEqual(zRadius, radius) - ) { +function createPointInEllipse(cornersInWorld: Types.Point3[] = []) { + if (!cornersInWorld || cornersInWorld.length !== 4) { + throw new Error('createPointInEllipse: cornersInWorld must have 4 points'); + } + const [topLeft, bottomRight, bottomLeft, topRight] = cornersInWorld; + + // Center is the midpoint of the diagonal + const center = vec3.create(); + vec3.add(center, topLeft, bottomRight); + vec3.scale(center, center, 0.5); + + // Major axis: from topLeft to topRight + const majorAxisVec = vec3.create(); + vec3.subtract(majorAxisVec, topRight, topLeft); + const xRadius = vec3.length(majorAxisVec) / 2; + vec3.normalize(majorAxisVec, majorAxisVec); + + // Minor axis: from topLeft to bottomLeft + const minorAxisVec = vec3.create(); + vec3.subtract(minorAxisVec, bottomLeft, topLeft); + const yRadius = vec3.length(minorAxisVec) / 2; + vec3.normalize(minorAxisVec, minorAxisVec); + + // Plane normal + const normal = vec3.create(); + vec3.cross(normal, majorAxisVec, minorAxisVec); + vec3.normalize(normal, normal); + + // If radii are equal, treat as sphere + if (isEqual(xRadius, yRadius)) { + const radius = xRadius; const sphereObj = { center, radius, @@ -107,16 +134,29 @@ function createPointInEllipse(worldInfo: { }; return (pointLPS) => pointInSphere(sphereObj, pointLPS); } - // using circle as a form of ellipse - const ellipseObj = { - center: center as Types.Point3, - xRadius, - yRadius, - zRadius, - }; - const { precalculated } = precalculatePointInEllipse(ellipseObj, {}); - return precalculated; + // Otherwise, treat as ellipse in oblique plane + return (pointLPS: Types.Point3) => { + // Project point onto the plane + const pointVec = vec3.create(); + vec3.subtract(pointVec, pointLPS, center); + // Remove component along normal + const distToPlane = vec3.dot(pointVec, normal); + const proj = vec3.create(); + vec3.scaleAndAdd(proj, pointVec, normal, -distToPlane); + + // Express proj in (majorAxis, minorAxis) coordinates + // Project from center, so shift origin to topLeft + const fromTopLeft = vec3.create(); + const centerToTopLeft = vec3.create(); + vec3.subtract(centerToTopLeft, center, topLeft); + vec3.subtract(fromTopLeft, proj, centerToTopLeft); + const x = vec3.dot(fromTopLeft, majorAxisVec); + const y = vec3.dot(fromTopLeft, minorAxisVec); + + // Ellipse equation: (x/xRadius)^2 + (y/yRadius)^2 <= 1 + return (x * x) / (xRadius * xRadius) + (y * y) / (yRadius * yRadius) <= 1; + }; } const CIRCLE_STRATEGY = new BrushStrategy( diff --git a/packages/tools/src/tools/segmentation/strategies/fillRectangle.ts b/packages/tools/src/tools/segmentation/strategies/fillRectangle.ts index 4513f18848..ddfe557f1a 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillRectangle.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillRectangle.ts @@ -2,14 +2,8 @@ import { vec3 } from 'gl-matrix'; import { utilities as csUtils, StackViewport } from '@cornerstonejs/core'; import type { Types, BaseVolumeViewport } from '@cornerstonejs/core'; -import { - getBoundingBoxAroundShapeIJK, - getBoundingBoxAroundShapeWorld, -} from '../../../utilities/boundingBox'; -import { triggerSegmentationDataModified } from '../../../stateManagement/segmentation/triggerSegmentationEvents'; +import { getBoundingBoxAroundShapeIJK } from '../../../utilities/boundingBox'; import type { LabelmapToolOperationData } from '../../../types'; -import { getStrategyData } from './utils/getStrategyData'; -import { isAxisAlignedRectangle } from '../../../utilities/rectangleROITool/isAxisAlignedRectangle'; import BrushStrategy from './BrushStrategy'; import type { Composition, InitializedOperationData } from './BrushStrategy'; import { StrategyCallbacks } from '../../../enums'; @@ -26,10 +20,8 @@ const initializeRectangle = { [StrategyCallbacks.Initialize]: (operationData: InitializedOperationData) => { const { points, // bottom, top, left, right - imageVoxelManager, viewport, segmentationImageData, - segmentationVoxelManager, } = operationData; // Happens on a preview setup @@ -91,19 +83,26 @@ function createPointInRectangle( segmentationImageData.getDimensions() ); - const isStackViewport = viewport instanceof StackViewport; - - // Are we working with 2D rectangle in axis aligned viewport view or not - const isAligned = - isStackViewport || isAxisAlignedRectangle(rectangleCornersIJK); - + // Rectangle corners in world: [topLeft, topRight, bottomRight, bottomLeft] + const [p0, p1, p2, p3] = points; + // Two axes in the plane + const axisU = vec3.create(); + const axisV = vec3.create(); + vec3.subtract(axisU, p1, p0); // p0 -> p1 + vec3.subtract(axisV, p3, p0); // p0 -> p3 + const uLen = vec3.length(axisU); + const vLen = vec3.length(axisV); + vec3.normalize(axisU, axisU); + vec3.normalize(axisV, axisV); + // Plane normal + const normal = vec3.create(); + vec3.cross(normal, axisU, axisV); + vec3.normalize(normal, normal); + + // For tolerance in the normal direction const direction = segmentationImageData.getDirection(); const spacing = segmentationImageData.getSpacing(); const { viewPlaneNormal } = viewport.getCamera(); - - // In case that we are working on oblique, our EPS is really the spacing in the - // normal direction, since we can't really test each voxel against a 2D rectangle - // we need some tolerance in the normal direction. const EPS = csUtils.getSpacingInNormalDirection( { direction, @@ -112,27 +111,25 @@ function createPointInRectangle( viewPlaneNormal ); - const pointsBoundsLPS = getBoundingBoxAroundShapeWorld(points); - let [[xMin, xMax], [yMin, yMax], [zMin, zMax]] = pointsBoundsLPS; - - // Update the bounds with +/- EPS - xMin -= EPS; - xMax += EPS; - yMin -= EPS; - yMax += EPS; - zMin -= EPS; - zMax += EPS; - - const pointInShapeFn = isAligned - ? () => true - : (pointLPS) => { - const [x, y, z] = pointLPS; - const xInside = x >= xMin && x <= xMax; - const yInside = y >= yMin && y <= yMax; - const zInside = z >= zMin && z <= zMax; - - return xInside && yInside && zInside; - }; + // Function to check if a point is inside the oblique rectangle + const pointInShapeFn = (pointLPS) => { + // Vector from p0 to point + const v = vec3.create(); + vec3.subtract(v, pointLPS, p0); + // Project onto axes + const u = vec3.dot(v, axisU); + const vproj = vec3.dot(v, axisV); + // Project onto normal + const d = Math.abs(vec3.dot(v, normal)); + // Check bounds with tolerance in normal direction + return ( + u >= -EPS && + u <= uLen + EPS && + vproj >= -EPS && + vproj <= vLen + EPS && + d <= EPS + ); + }; return { boundsIJK, pointInShapeFn }; } diff --git a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts index 31b3327c82..59583c1b11 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts @@ -6,9 +6,13 @@ import BrushStrategy from './BrushStrategy'; import type { InitializedOperationData, Composition } from './BrushStrategy'; import compositions from './compositions'; import StrategyCallbacks from '../../../enums/StrategyCallbacks'; -import { createEllipseInPoint } from './fillCircle'; +import { + createEllipseInPoint, + getEllipseCornersFromCanvasCoordinates, +} from './fillCircle'; const { transformWorldToIndex } = csUtils; import { getSphereBoundsInfoFromViewport } from '../../../utilities/getSphereBoundsInfo'; +import type { CanvasCoordinates } from '../../../types'; const sphereComposition = { [StrategyCallbacks.Initialize]: (operationData: InitializedOperationData) => { @@ -18,12 +22,16 @@ const sphereComposition = { if (!points) { return; } - // Average the points to get the center of the ellipse - const center = vec3.fromValues(0, 0, 0); - points.forEach((point) => { - vec3.add(center, center, point); - }); - vec3.scale(center, center, 1 / points.length); + // Calculate the center as the midpoint between the first two points + // That calculation serves both for orthogonal and oblique planes + const center = vec3.create(); + if (points.length >= 2) { + vec3.add(center, points[0], points[1]); + vec3.scale(center, center, 0.5); + } else { + // Fallback to the first point if less than 2 points are provided + vec3.copy(center, points[0]); + } operationData.centerWorld = center as Types.Point3; operationData.centerIJK = transformWorldToIndex( @@ -31,22 +39,23 @@ const sphereComposition = { center as Types.Point3 ); - const { - boundsIJK: newBoundsIJK, - topLeftWorld, - bottomRightWorld, - } = getSphereBoundsInfoFromViewport( + const { boundsIJK: newBoundsIJK } = getSphereBoundsInfoFromViewport( points.slice(0, 2) as [Types.Point3, Types.Point3], segmentationImageData, viewport ); + const canvasCoordinates = points.map((p) => + viewport.worldToCanvas(p) + ) as CanvasCoordinates; + + // 1. From the drawn tool: Get the ellipse (circle) corners in canvas coordinates + const corners = getEllipseCornersFromCanvasCoordinates(canvasCoordinates); + const cornersInWorld = corners.map((corner) => + viewport.canvasToWorld(corner) + ); operationData.isInObjectBoundsIJK = newBoundsIJK; - operationData.isInObject = createEllipseInPoint({ - topLeftWorld, - bottomRightWorld, - center, - }); + operationData.isInObject = createEllipseInPoint(cornersInWorld); // } }, } as Composition; diff --git a/packages/tools/src/types/AnnotationTypes.ts b/packages/tools/src/types/AnnotationTypes.ts index 815de61b7f..7baa0d9ce3 100644 --- a/packages/tools/src/types/AnnotationTypes.ts +++ b/packages/tools/src/types/AnnotationTypes.ts @@ -1,6 +1,55 @@ import type { Types } from '@cornerstonejs/core'; -type Annotation = { +export type AnnotationMetadata = Types.ViewReference & { + /** + * The registered name of the tool + */ + toolName: string; + /** + * The position of the camera in world space. + */ + cameraPosition?: Types.Point3; + /** + * The viewUp for the view position + */ + viewUp?: Types.Point3; + + /** The color of the segmentation - really this is annotation data */ + segmentColor?; + + /** The enable element of the annotation */ + enabledElement?: Types.IEnabledElement; +}; + +export type AnnotationData = { + /** + * Annotation handles that are grabbable for manipulation + */ + handles?: Handles; + /** + * Cached Annotation statistics which is specific to the tool + */ + cachedStats?: Record; + /** + * Label of an annotation + */ + label?: string; + /** + * contour data + */ + contour?: Contour; + + /** + * Other data/keys + */ + [key: string]: unknown; +}; + +/** + * Defines the basic annotation type. This SHOULD be an interface, but + * typescript doesn't properly handle extending interfaces. + */ +export type Annotation = { /** A unique identifier for this annotation */ annotationUID?: string; /** @@ -33,38 +82,15 @@ type Annotation = { isSelected?: boolean; /** If the annotation is auto generated from other annotations*/ autoGenerated?: boolean; - /** Metadata for annotation */ - metadata: Types.ViewReference & { - /** - * The registered name of the tool - */ - toolName: string; - /** - * The position of the camera in world space. - */ - cameraPosition?: Types.Point3; - /** - * The viewUp for the view position - */ - viewUp?: Types.Point3; - }; + /** Metadata for annotation. Optional for any type, but required for measurements */ + metadata?: AnnotationMetadata; /** * Data for annotation, Derivatives need to define their own data types. */ - data: { - /** Annotation handles that are grabbable for manipulation */ - handles?: Handles; - [key: string]: unknown; - /** Cached Annotation statistics which is specific to the tool */ - cachedStats?: Record; - /** Label of an annotation */ - label?: string; - /** contour data */ - contour?: Contour; - }; + data: AnnotationData; }; -type Contour = { +export type Contour = { /** world location of the polyline in the space */ polyline?: Types.Point3[]; /** PointsManager */ @@ -74,9 +100,9 @@ type Contour = { }; /** Array of annotations */ -type Annotations = Array; +export type Annotations = Array; -type GroupSpecificAnnotations = { +export type GroupSpecificAnnotations = { /** Each tool annotations */ [toolName: string]: Annotations; }; @@ -84,7 +110,7 @@ type GroupSpecificAnnotations = { /** * All frame of reference specific annotations for all tools. */ -type AnnotationState = { +export type AnnotationState = { /** * A string representing the key that can be used * to retrieve the key-specific annotations. For instance, our default @@ -95,7 +121,7 @@ type AnnotationState = { [key: string]: GroupSpecificAnnotations; }; -type Handles = { +export type Handles = { /** world location of the handles in the space */ points?: Types.Point3[]; /** index of the active handle being manipulated */ @@ -120,11 +146,3 @@ type Handles = { }; [key: string]: unknown; }; - -export type { - Annotation, - Annotations, - GroupSpecificAnnotations, - AnnotationState, - Handles, -}; diff --git a/packages/tools/src/types/ContourAnnotation.ts b/packages/tools/src/types/ContourAnnotation.ts index 7623def6fd..cb54ab7c4c 100644 --- a/packages/tools/src/types/ContourAnnotation.ts +++ b/packages/tools/src/types/ContourAnnotation.ts @@ -16,6 +16,8 @@ export enum ContourWindingDirection { export type ContourAnnotationData = { data: { cachedStats?: Record; + /** @deprecated Use contour.polyline */ + polyline?: Types.Point3[]; contour: { polyline: Types.Point3[]; closed: boolean; diff --git a/packages/tools/src/types/ToolSpecificAnnotationTypes.ts b/packages/tools/src/types/ToolSpecificAnnotationTypes.ts index c1e03f2df1..04f2720f7d 100644 --- a/packages/tools/src/types/ToolSpecificAnnotationTypes.ts +++ b/packages/tools/src/types/ToolSpecificAnnotationTypes.ts @@ -124,7 +124,7 @@ export interface CircleROIAnnotation extends Annotation { Types.Point3, Types.Point3, Types.Point3, - Types.Point3 + Types.Point3, ]; // [center, top, bottom, left, right] activeHandleIndex: number | null; textBox?: { diff --git a/packages/tools/src/types/index.ts b/packages/tools/src/types/index.ts index 2b19bf6ffd..7b45feb5d3 100644 --- a/packages/tools/src/types/index.ts +++ b/packages/tools/src/types/index.ts @@ -1,9 +1,4 @@ -import type { - Annotation, - Annotations, - AnnotationState, - GroupSpecificAnnotations, -} from './AnnotationTypes'; +export type * from './AnnotationTypes'; import type { ContourAnnotationData, ContourAnnotation, @@ -101,8 +96,6 @@ import type { export type { // AnnotationState - Annotation, - Annotations, ContourAnnotationData, ContourAnnotation, ContourSegmentationAnnotationData, @@ -112,8 +105,6 @@ export type { IAnnotationManager, InterpolationViewportData, ImageInterpolationData, - GroupSpecificAnnotations, - AnnotationState, AnnotationStyle, ToolSpecificAnnotationTypes, AnnotationGroupSelector, diff --git a/packages/tools/src/utilities/contours/interpolation/interpolate.ts b/packages/tools/src/utilities/contours/interpolation/interpolate.ts index 03c949df9b..2ed98bf701 100644 --- a/packages/tools/src/utilities/contours/interpolation/interpolate.ts +++ b/packages/tools/src/utilities/contours/interpolation/interpolate.ts @@ -611,8 +611,7 @@ function _getNodesPerSegment(perimInterp, perimInd) { arr.push(i); } return arr; - }, - []); + }, []); const nodesPerSegment = []; diff --git a/packages/tools/src/utilities/math/circle/_types.ts b/packages/tools/src/utilities/math/circle/_types.ts index c7a09f0eff..3f4b1603d2 100644 --- a/packages/tools/src/utilities/math/circle/_types.ts +++ b/packages/tools/src/utilities/math/circle/_types.ts @@ -2,5 +2,5 @@ import type { Types } from '@cornerstonejs/core'; export type canvasCoordinates = [ Types.Point2, // center - Types.Point2 // end + Types.Point2, // end ]; diff --git a/packages/tools/src/utilities/math/ellipse/getCanvasEllipseCorners.ts b/packages/tools/src/utilities/math/ellipse/getCanvasEllipseCorners.ts index 059359632d..da3dae3124 100644 --- a/packages/tools/src/utilities/math/ellipse/getCanvasEllipseCorners.ts +++ b/packages/tools/src/utilities/math/ellipse/getCanvasEllipseCorners.ts @@ -4,7 +4,7 @@ export type CanvasCoordinates = [ Types.Point2, // bottom Types.Point2, // top Types.Point2, // left - Types.Point2 // right + Types.Point2, // right ]; /** diff --git a/packages/tools/src/utilities/math/line/intersectLine.ts b/packages/tools/src/utilities/math/line/intersectLine.ts index 2dafd3aa34..53a450057e 100644 --- a/packages/tools/src/utilities/math/line/intersectLine.ts +++ b/packages/tools/src/utilities/math/line/intersectLine.ts @@ -8,8 +8,8 @@ function sign(x: number | string): number { ? -1 : 1 : x === x - ? 0 - : NaN + ? 0 + : NaN : NaN; } diff --git a/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts b/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts index 22bc57fc62..ee9c5a2f81 100644 --- a/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts +++ b/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts @@ -35,7 +35,25 @@ export default function filterAnnotationsWithinSlice( // logic down below. const annotationsWithParallelNormals = annotations.filter( (td: Annotation) => { - let annotationViewPlaneNormal = td.metadata.viewPlaneNormal; + const { planeRestriction, referencedImageId } = td.metadata; + let { viewPlaneNormal: annotationViewPlaneNormal } = td.metadata; + + if (planeRestriction) { + const { inPlaneVector1, inPlaneVector2 } = planeRestriction; + if ( + inPlaneVector1 && + !isEqual(0, vec3.dot(viewPlaneNormal, inPlaneVector1)) + ) { + return false; + } + if ( + inPlaneVector2 && + !isEqual(0, vec3.dot(viewPlaneNormal, inPlaneVector2)) + ) { + return false; + } + return true; + } if ( !td.metadata.referencedImageId && @@ -44,20 +62,19 @@ export default function filterAnnotationsWithinSlice( ) { for (const point of td.data.handles.points) { const vector = vec3.sub(vec3.create(), point, camera.focalPoint); - const dotProduct = vec3.dot(vector, camera.viewPlaneNormal); + const dotProduct = vec3.dot(vector, viewPlaneNormal); if (!isEqual(dotProduct, 0)) { return false; } } - td.metadata.viewPlaneNormal = camera.viewPlaneNormal; + td.metadata.viewPlaneNormal = viewPlaneNormal; td.metadata.cameraFocalPoint = camera.focalPoint; return true; } - if (!annotationViewPlaneNormal) { + if (!annotationViewPlaneNormal && referencedImageId) { // This code is run to set the annotation view plane normal // for historical data which was saved without the normal. - const { referencedImageId } = td.metadata; const { imageOrientationPatient } = metaData.get( 'imagePlaneModule', referencedImageId @@ -79,6 +96,7 @@ export default function filterAnnotationsWithinSlice( vec3.cross(annotationViewPlaneNormal, rowCosineVec, colCosineVec); td.metadata.viewPlaneNormal = annotationViewPlaneNormal; } + const isParallel = Math.abs(vec3.dot(viewPlaneNormal, annotationViewPlaneNormal)) > PARALLEL_THRESHOLD; diff --git a/packages/tools/src/utilities/segmentation/getBrushToolInstances.ts b/packages/tools/src/utilities/segmentation/getBrushToolInstances.ts index d68919a893..2a70e95d42 100644 --- a/packages/tools/src/utilities/segmentation/getBrushToolInstances.ts +++ b/packages/tools/src/utilities/segmentation/getBrushToolInstances.ts @@ -5,13 +5,13 @@ export function getBrushToolInstances(toolGroupId: string, toolName?: string) { const toolGroup = getToolGroup(toolGroupId); if (toolGroup === undefined) { - return; + return []; } const toolInstances = toolGroup._toolInstances; if (!Object.keys(toolInstances).length) { - return; + return []; } if (toolName && toolInstances[toolName]) { diff --git a/packages/tools/src/utilities/stackPrefetch/stackContextPrefetch.ts b/packages/tools/src/utilities/stackPrefetch/stackContextPrefetch.ts index 859dba9c01..14af8af32c 100644 --- a/packages/tools/src/utilities/stackPrefetch/stackContextPrefetch.ts +++ b/packages/tools/src/utilities/stackPrefetch/stackContextPrefetch.ts @@ -4,16 +4,19 @@ import { eventTarget, imageLoadPoolManager, cache, + metaData, + utilities, } from '@cornerstonejs/core'; import { addToolState, getToolState, type StackPrefetchData } from './state'; import { getStackData, requestType, - priority, clearFromImageIds, getPromiseRemovedHandler, } from './stackPrefetchUtils'; +const { imageRetrieveMetadataProvider } = utilities; + let configuration = { maxImagesToPrefetch: Infinity, // Fetch up to 2 image before and after @@ -29,6 +32,8 @@ let resetPrefetchTimeout; // loaded, so a 5 ms prefetch delay is fine const resetPrefetchDelay = 5; +const priorities = {}; + /** * Call this to enable stack context sensitive prefetch. Should be called * before stack data is set in order to start prefetch after load first image. @@ -58,8 +63,9 @@ const resetPrefetchDelay = 5; * * Up to 100 images in the direction of travel will be prefetched * * @param element - to prefetch on + * @param priority - priority to be used for the request manager */ -const enable = (element): void => { +const enable = (element, priority = 0): void => { const stack = getStackData(element); if (!stack) { return; @@ -71,7 +77,9 @@ const enable = (element): void => { updateToolState(element); - prefetch(element); + priorities[element] = priority; + + prefetch(element, priority); element.removeEventListener(Enums.Events.STACK_NEW_IMAGE, onImageUpdated); element.addEventListener(Enums.Events.STACK_NEW_IMAGE, onImageUpdated); @@ -88,7 +96,7 @@ const enable = (element): void => { ); }; -function prefetch(element) { +function prefetch(element, priority = 0) { const stack = getStackData(element); if (!stack) { return; @@ -190,7 +198,7 @@ function prefetch(element) { stats.initialTime = Date.now() - stats.start; stats.initialSize = stats.imageIds.size; updateToolState(element, usage); - prefetch(element); + prefetch(element, priority); } else if (stats.imageIds.size) { stats.fillTime = Date.now() - stats.start; const { size } = stats.imageIds; @@ -216,10 +224,21 @@ function prefetch(element) { } } - const requestFn = (imageId, options) => - imageLoader + const requestFn = (imageId, options) => { + const { retrieveOptions = {} } = + metaData.get( + imageRetrieveMetadataProvider.IMAGE_RETRIEVE_CONFIGURATION, + imageId, + 'stack' + ) || {}; + options.retrieveOptions = { + ...options.retrieveOptions, + ...(retrieveOptions.default || Object.values(retrieveOptions)?.[0] || {}), + }; + return imageLoader .loadAndCacheImage(imageId, options) .then(() => doneCallback(imageId)); + }; stackPrefetch.indicesToRequest.forEach((imageIdIndex) => { const imageId = stack.imageIds[imageIdIndex]; @@ -253,7 +272,7 @@ function onImageUpdated(e) { // An exception will be thrown because the element will not be enabled anymore try { updateToolState(element); - prefetch(element); + prefetch(element, priorities[element]); } catch (error) { return; } diff --git a/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts b/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts index 0dbeab6ffb..4aecca1ce2 100644 --- a/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts +++ b/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts @@ -4,7 +4,8 @@ import { eventTarget, imageLoadPoolManager, cache, - getConfiguration as getCoreConfiguration, + metaData, + utilities, } from '@cornerstonejs/core'; import { addToolState, getToolState, type StackPrefetchData } from './state'; import { @@ -16,6 +17,8 @@ import { range, } from './stackPrefetchUtils'; +const { imageRetrieveMetadataProvider } = utilities; + let configuration = { maxImagesToPrefetch: Infinity, // Preserving the existing pool should be the default behaviour, as there might @@ -164,8 +167,21 @@ function prefetch(element) { } } - const requestFn = (imageId, options) => - imageLoader.loadAndCacheImage(imageId, options); + const requestFn = (imageId, options) => { + const { retrieveOptions = {} } = + metaData.get( + imageRetrieveMetadataProvider.IMAGE_RETRIEVE_CONFIGURATION, + imageId, + 'stack' + ) || {}; + options.retrieveOptions = { + ...options.retrieveOptions, + ...(retrieveOptions.default || Object.values(retrieveOptions)?.[0] || {}), + }; + return imageLoader + .loadAndCacheImage(imageId, options) + .then(() => doneCallback(imageId)); + }; imageIdsToPrefetch.forEach((imageId) => { // IMPORTANT: Request type should be passed if not the 'interaction' diff --git a/packages/tools/test/CircleROIStartEnd_test.js b/packages/tools/test/CircleROIStartEnd_test.js new file mode 100644 index 0000000000..850b9447d4 --- /dev/null +++ b/packages/tools/test/CircleROIStartEnd_test.js @@ -0,0 +1,208 @@ +import * as cornerstone3D from '@cornerstonejs/core'; +import * as csTools3d from '../src/index'; +import * as testUtils from '../../../utils/test/testUtils'; + +const { + cache, + RenderingEngine, + Enums, + utilities, + imageLoader, + metaData, + eventTarget, + volumeLoader, + setVolumesForViewports, + getEnabledElement, +} = cornerstone3D; + +const { Events, ViewportType } = Enums; +const { registerVolumeLoader, createAndCacheVolume } = volumeLoader; + +const { + CircleROIStartEndThresholdTool, + ToolGroupManager, + cancelActiveManipulations, + annotation, + Enums: csToolsEnums, +} = csTools3d; + +const { Events: csToolsEvents } = csToolsEnums; + +const { + fakeImageLoader, + fakeVolumeLoader, + fakeMetaDataProvider, + createNormalizedMouseEvent, +} = testUtils; + +const renderingEngineId = utilities.uuidv4(); + +const viewportId = 'VIEWPORT'; + +const AXIAL = 'AXIAL'; + +const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 4, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, +}); + +describe('Circle Start End Tool: ', () => { + let renderingEngine; + let toolGroup; + + beforeEach(function () { + const testEnv = testUtils.setupTestEnvironment({ + renderingEngineId, + toolGroupIds: ['volume'], + viewportIds: [viewportId], + tools: [CircleROIStartEndThresholdTool], + toolConfigurations: { + [CircleROIStartEndThresholdTool.toolName]: { + configuration: { volumeId: volumeId }, + }, + }, + toolActivations: { + [CircleROIStartEndThresholdTool.toolName]: { + bindings: [{ mouseButton: 1 }], + }, + }, + }); + renderingEngine = testEnv.renderingEngine; + toolGroup = testEnv.toolGroups['volume']; + }); + + afterEach(function () { + testUtils.cleanupTestEnvironment({ + renderingEngineId: renderingEngineId, + toolGroupIds: ['volume'], + cleanupDOMElements: true, + }); + }); + + it('Should successfully create a circle start end tool on a canvas with mouse drag - 512 x 128', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + viewportType: ViewportType.ORTHOGRAPHIC, + width: 512, + height: 128, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + }); + + const vp = renderingEngine.getViewport(viewportId); + + const addEventListenerForAnnotationRendered = () => { + element.addEventListener(csToolsEvents.ANNOTATION_RENDERED, () => { + const circleAnnotations = annotation.state.getAnnotations( + CircleROIStartEndThresholdTool.toolName, + element + ); + expect(circleAnnotations).toBeDefined(); + expect(circleAnnotations.length).toBe(1); + + const circleAnnotation = circleAnnotations[0]; + //expect(circleAnnotation.metadata.referencedImageId).toBe(imageId1); + expect(circleAnnotation.metadata.toolName).toBe( + CircleROIStartEndThresholdTool.toolName + ); + expect(circleAnnotation.invalidated).toBe(false); + + const data = circleAnnotation.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(3); + expect(data.pointsInVolume).toBeInstanceOf(Array); + + // the rectangle is drawn on the strip + expect(data.statistics.mean).toBeCloseTo(28.33); + + annotation.state.removeAnnotation(circleAnnotation.annotationUID); + done(); + }); + }; + + element.addEventListener(Events.IMAGE_RENDERED, () => { + // Since circle draws from center to out, we are picking a very center + // point in the image (strip is 255 from 10-15 in X and from 0-64 in Y) + const index1 = [12, 30, 0]; + const index2 = [14, 30, 0]; + + if (!vp.getImageData()) { + return; + } + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = testUtils.createNormalizedMouseEvent(imageData, index1, element, vp); + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = testUtils.createNormalizedMouseEvent(imageData, index2, element, vp); + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: element, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + element.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: element, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + try { + createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId }], + [viewportId] + ); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); +}); diff --git a/packages/tools/test/RectangleROIStartEnd_test.js b/packages/tools/test/RectangleROIStartEnd_test.js new file mode 100644 index 0000000000..7f8275e5af --- /dev/null +++ b/packages/tools/test/RectangleROIStartEnd_test.js @@ -0,0 +1,191 @@ +import * as cornerstone3D from '@cornerstonejs/core'; +import * as csTools3d from '../src/index'; +import * as testUtils from '../../../utils/test/testUtils'; +import { performMouseDownAndUp } from '../../../utils/test/testUtilsMouseEvents'; +import { + encodeImageIdInfo, + createViewports, +} from '../../../utils/test/testUtils'; + +const { + cache, + RenderingEngine, + Enums, + utilities, + imageLoader, + eventTarget, + metaData, + volumeLoader, + setVolumesForViewports, +} = cornerstone3D; + +const { Events, ViewportType } = Enums; + +const { + RectangleROIStartEndThresholdTool, + ToolGroupManager, + Enums: csToolsEnums, + cancelActiveManipulations, + annotation, +} = csTools3d; + +const { Events: csToolsEvents } = csToolsEnums; + +const { + fakeImageLoader, + fakeVolumeLoader, + fakeMetaDataProvider, + createNormalizedMouseEvent, +} = testUtils; + +const renderingEngineId = utilities.uuidv4(); + +const viewportId = 'VIEWPORT'; + +const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 4, + xSpacing: 1, + ySpacing: 1, +}); + +describe('Rectangle ROI StartEnd Tool:', () => { + let testEnv; + let renderingEngine; + + beforeAll(() => { + cornerstone3D.setUseCPURendering(false); + }); + + beforeEach(function () { + testEnv = testUtils.setupTestEnvironment({ + renderingEngineId: renderingEngineId, + toolGroupIds: ['volume'], + tools: [RectangleROIStartEndThresholdTool], + toolActivations: { + [RectangleROIStartEndThresholdTool.toolName]: { + bindings: [{ mouseButton: 1 }], + }, + }, + viewportIds: [viewportId], + }); + + renderingEngine = testEnv.renderingEngine; + }); + + afterEach(function () { + testUtils.cleanupTestEnvironment({ + renderingEngineId: renderingEngineId, + toolGroupIds: ['volume'], + cleanupDOMElements: true, + }); + }); + + it('Should successfully create a rectangle tool on a canvas with mouse drag in a Volume viewport - 512 x 128', function (done) { + const element = createViewports(renderingEngine, { + viewportType: ViewportType.ORTHOGRAPHIC, + width: 512, + height: 128, + viewportId: viewportId, + }); + + const vp = renderingEngine.getViewport(viewportId); + + const addEventListenerForAnnotationRendered = () => { + element.addEventListener(csToolsEvents.ANNOTATION_RENDERED, () => { + const rectangleAnnotations = annotation.state.getAnnotations( + RectangleROIStartEndThresholdTool.toolName, + element + ); + // Can successfully add rectangleROI to annotationManager + expect(rectangleAnnotations).toBeDefined(); + expect(rectangleAnnotations.length).toBe(1); + + const rectangleAnnotation = rectangleAnnotations[0]; + expect(rectangleAnnotation.metadata.toolName).toBe( + RectangleROIStartEndThresholdTool.toolName + ); + expect(rectangleAnnotation.invalidated).toBe(false); + + const data = rectangleAnnotation.data.cachedStats; + const targets = Array.from(Object.keys(data)); + expect(targets.length).toBe(4); + expect(data.statistics.mean).toBe(85); + expect(data.statistics.stdDev).toBeCloseTo(120.208); + + annotation.state.removeAnnotation(rectangleAnnotation.annotationUID); + done(); + }); + }; + + element.addEventListener(Events.IMAGE_RENDERED, () => { + // Inside the strip which is from 50-75 in slice 2 + // volumeURI_100_100_4_1_1_1_0 + // The strip is from + const index1 = [50, 10, 2]; + const index2 = [52, 20, 2]; + + const { imageData } = vp.getImageData(); + + const { + pageX: pageX1, + pageY: pageY1, + clientX: clientX1, + clientY: clientY1, + worldCoord: worldCoord1, + } = createNormalizedMouseEvent(imageData, index1, element, vp); + + const { + pageX: pageX2, + pageY: pageY2, + clientX: clientX2, + clientY: clientY2, + worldCoord: worldCoord2, + } = createNormalizedMouseEvent(imageData, index2, element, vp); + + // Mouse Down + let evt = new MouseEvent('mousedown', { + target: element, + buttons: 1, + clientX: clientX1, + clientY: clientY1, + pageX: pageX1, + pageY: pageY1, + }); + element.dispatchEvent(evt); + + // Mouse move to put the end somewhere else + evt = new MouseEvent('mousemove', { + target: element, + buttons: 1, + clientX: clientX2, + clientY: clientY2, + pageX: pageX2, + pageY: pageY2, + }); + document.dispatchEvent(evt); + + // Mouse Up instantly after + evt = new MouseEvent('mouseup'); + + addEventListenerForAnnotationRendered(); + document.dispatchEvent(evt); + }); + + try { + volumeLoader.createAndCacheVolume(volumeId, { imageIds: [] }).then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId }], + [viewportId] + ); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); +}); diff --git a/packages/tools/test/groundTruth/windowLevel_canvas2.png b/packages/tools/test/groundTruth/windowLevel_canvas2.png index 6e2502f9da..56ef0ebf51 100644 Binary files a/packages/tools/test/groundTruth/windowLevel_canvas2.png and b/packages/tools/test/groundTruth/windowLevel_canvas2.png differ diff --git a/packages/tools/test/segmentationSegmentIndexController_test.js b/packages/tools/test/segmentationSegmentIndexController_test.js index a1da4cc886..4e89fa8ea3 100644 --- a/packages/tools/test/segmentationSegmentIndexController_test.js +++ b/packages/tools/test/segmentationSegmentIndexController_test.js @@ -134,6 +134,11 @@ describe('Segmentation Index Controller:', () => { }; const compareImageCallback = () => { + // Remove the event listener to prevent multiple done() calls + eventTarget.removeEventListener( + Events.SEGMENTATION_RENDERED, + compareImageCallback + ); const canvas1 = vp1.getCanvas(); const image1 = canvas1.toDataURL('image/png'); diff --git a/packages/tools/test/stackSegmentation_test.js b/packages/tools/test/stackSegmentation_test.js index 0237a5e2f5..23024aca46 100644 --- a/packages/tools/test/stackSegmentation_test.js +++ b/packages/tools/test/stackSegmentation_test.js @@ -82,7 +82,11 @@ describe('Stack Segmentation Rendering:', () => { const imageId1 = encodeImageIdInfo(imageInfo1); const vp = renderingEngine.getViewport(viewportId1); - eventTarget.addEventListener(Events.SEGMENTATION_RENDERED, (evt) => { + const segmentationRenderedCallback = (evt) => { + eventTarget.removeEventListener( + Events.SEGMENTATION_RENDERED, + segmentationRenderedCallback + ); const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); @@ -91,7 +95,11 @@ describe('Stack Segmentation Rendering:', () => { imageURI_64_64_10_5_1_1_0_SEG_Mocked, 'imageURI_64_64_10_5_1_1_0_SEG_Mocked' ).then(done, done.fail); - }); + }; + eventTarget.addEventListener( + Events.SEGMENTATION_RENDERED, + segmentationRenderedCallback + ); try { vp.setStack([imageId1], 0).then(() => { @@ -153,10 +161,14 @@ describe('Stack Segmentation Rendering:', () => { let renderCount = 0; const expectedRenderCount = 2; // We expect two segmentations to be rendered - eventTarget.addEventListener(Events.SEGMENTATION_RENDERED, (evt) => { + const segmentationRenderedCallback = (evt) => { renderCount++; if (renderCount === expectedRenderCount) { + eventTarget.removeEventListener( + Events.SEGMENTATION_RENDERED, + segmentationRenderedCallback + ); const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); @@ -166,7 +178,11 @@ describe('Stack Segmentation Rendering:', () => { 'imageURI_64_64_10_5_1_1_0_SEG_Double_Mocked' ).then(done, done.fail); } - }); + }; + eventTarget.addEventListener( + Events.SEGMENTATION_RENDERED, + segmentationRenderedCallback + ); try { vp.setStack([imageId1], 0).then(() => { @@ -254,6 +270,10 @@ describe('Stack Segmentation Rendering:', () => { const vp = renderingEngine.getViewport(viewportId1); const compareImageCallback = (evt) => { + eventTarget.removeEventListener( + Events.SEGMENTATION_RENDERED, + compareImageCallback + ); const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); diff --git a/publish-version.mjs b/publish-version.mjs index 89228eb950..93483e0948 100644 --- a/publish-version.mjs +++ b/publish-version.mjs @@ -115,6 +115,7 @@ async function run() { await execa('git', ['add', '-A']); await execa('git', [ 'commit', + '--no-verify', '-m', 'chore(version): version.json [skip ci]', ]); @@ -146,6 +147,7 @@ async function run() { await execa('git', ['add', '.']); await execa('git', [ 'commit', + '--no-verify', '-m', 'chore: update generated version file [skip ci]', ]); diff --git a/tests/contextPoolRenderingEngine.spec.ts b/tests/contextPoolRenderingEngine.spec.ts new file mode 100644 index 0000000000..f168d74d3d --- /dev/null +++ b/tests/contextPoolRenderingEngine.spec.ts @@ -0,0 +1,24 @@ +import { test } from 'playwright-test-coverage'; +import { + visitExample, + checkForScreenshot, + screenShotPaths, +} from './utils/index'; + +test.beforeEach(async ({ page, context }) => { + await context.addInitScript(() => (window.IS_PLAYWRIGHT = true)); + await visitExample(page, 'contextPoolRenderingEngine'); +}); + +test.describe('Context Pool Rendering Engine', async () => { + test('should display context pool rendering engine example', async ({ + page, + }) => { + const locator = page.locator('#viewportContainer'); + await checkForScreenshot( + page, + locator, + screenShotPaths.contextPoolRenderingEngine.viewport + ); + }); +}); diff --git a/tests/contourRendering.spec.ts b/tests/contourRendering.spec.ts index b626e12107..8615914915 100644 --- a/tests/contourRendering.spec.ts +++ b/tests/contourRendering.spec.ts @@ -13,7 +13,7 @@ test.describe('Contour Rendering', async () => { test('should add a contour as a segmentation to a volume viewport', async ({ page, }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await checkForScreenshot( page, diff --git a/tests/contourRenderingTiled.spec.ts b/tests/contourRenderingTiled.spec.ts new file mode 100644 index 0000000000..c6b2dd668a --- /dev/null +++ b/tests/contourRenderingTiled.spec.ts @@ -0,0 +1,25 @@ +import { test } from 'playwright-test-coverage'; +import { + checkForScreenshot, + visitExample, + screenShotPaths, +} from './utils/index'; + +test.beforeEach(async ({ page, context }) => { + await visitExample(page, 'contourRendering'); + await context.addInitScript(() => (window.IS_TILED = true)); +}); + +test.describe('Contour Rendering', async () => { + test('should add a contour as a segmentation to a volume viewport', async ({ + page, + }) => { + const canvas = await page.locator('canvas.cornerstone-canvas'); + + await checkForScreenshot( + page, + canvas, + screenShotPaths.contourRenderingTiled.viewport + ); + }); +}); diff --git a/tests/labelmapGlobalConfiguration.spec.ts b/tests/labelmapGlobalConfiguration.spec.ts index 6b0f93362d..82f244e663 100644 --- a/tests/labelmapGlobalConfiguration.spec.ts +++ b/tests/labelmapGlobalConfiguration.spec.ts @@ -21,7 +21,7 @@ test.describe('Labelmap Global Configuration', async () => { test('should render the segmentations using the default global configuration', async ({ page, }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await checkForScreenshot( page, @@ -33,7 +33,7 @@ test.describe('Labelmap Global Configuration', async () => { test.describe('when toggling inactive segmentations', async () => { test('should hide the inactive segmentation', async ({ page }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await page .getByRole('button', { name: 'toggle render inactive' }) @@ -49,7 +49,7 @@ test.describe('Labelmap Global Configuration', async () => { test.describe('when toggling outline rendering', async () => { test('should render segmentations with no outline', async ({ page }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await page .getByRole('button', { name: 'toggle outline rendering' }) @@ -65,7 +65,7 @@ test.describe('Labelmap Global Configuration', async () => { test.describe('when toggling fill rendering', async () => { test('should not fill the active segmentation', async ({ page }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await page.getByRole('button', { name: 'toggle fill rendering' }).click(); await checkForScreenshot( @@ -80,7 +80,7 @@ test.describe('Labelmap Global Configuration', async () => { test('should render the active segmentation with the new outline width', async ({ page, }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await page.locator('#outlineWidthActive').fill('5'); await checkForScreenshot( @@ -95,7 +95,7 @@ test.describe('Labelmap Global Configuration', async () => { test('should render the active segmentation with the new outline alpha', async ({ page, }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await page.locator('#outlineWidthActive').fill('5'); await page.locator('#outlineAlphaActive').fill('0'); @@ -111,7 +111,7 @@ test.describe('Labelmap Global Configuration', async () => { test('should render the inactive segmentation with the new outline width', async ({ page, }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await page.locator('#outlineWidthInactive').fill('5'); await checkForScreenshot( @@ -126,7 +126,7 @@ test.describe('Labelmap Global Configuration', async () => { test('should render the active segmentation with the new fill alpha', async ({ page, }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await page.locator('#fillAlphaActive').fill('25'); await checkForScreenshot( @@ -141,7 +141,7 @@ test.describe('Labelmap Global Configuration', async () => { test('should render the inactive segmentation with the new fill alpha', async ({ page, }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await page.locator('#fillAlphaInactive').fill('20'); await checkForScreenshot( diff --git a/tests/labelmapRenderingTiled.spec.ts b/tests/labelmapRenderingTiled.spec.ts new file mode 100644 index 0000000000..76a3ad4d04 --- /dev/null +++ b/tests/labelmapRenderingTiled.spec.ts @@ -0,0 +1,43 @@ +import { test } from 'playwright-test-coverage'; +import { + checkForScreenshot, + visitExample, + screenShotPaths, + reduceViewportsSize, + attemptAction, +} from './utils/index'; + +test.beforeEach(async ({ page, context }) => { + await visitExample(page, 'labelmapRendering', 0, false, false); + await context.addInitScript(() => (window.IS_TILED = true)); +}); + +test.describe('Labelmap Rendering', async () => { + test('should render the labelmap in axial/coronal/sagittal orientations', async ({ + page, + }) => { + await attemptAction(() => reduceViewportsSize(page), 1000, 10); + + const axial = await page.locator('canvas').nth(0); + const coronal = await page.locator('canvas').nth(1); + const sagittal = await page.locator('canvas').nth(2); + + await checkForScreenshot( + page, + axial, + screenShotPaths.labelmapRenderingTiled.axial + ); + + await checkForScreenshot( + page, + coronal, + screenShotPaths.labelmapRenderingTiled.coronal + ); + + await checkForScreenshot( + page, + sagittal, + screenShotPaths.labelmapRenderingTiled.sagittal + ); + }); +}); diff --git a/tests/labelmapSwapping.spec.ts b/tests/labelmapSwapping.spec.ts index 9f2f0dd51d..4bbb7cbee6 100644 --- a/tests/labelmapSwapping.spec.ts +++ b/tests/labelmapSwapping.spec.ts @@ -13,7 +13,7 @@ test.describe('Swapping labelmap segmentations on a viewport', async () => { test('should load the default segmentation with two segments (circles)', async ({ page, }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await checkForScreenshot( page, @@ -25,7 +25,7 @@ test.describe('Swapping labelmap segmentations on a viewport', async () => { test('should swap the segmentation after clicking on "Swap Segmentation" button', async ({ page, }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await page.getByRole('button', { name: 'Swap Segmentation' }).click(); await checkForScreenshot( diff --git a/tests/screenshots/Mobile-Android/contextPoolRenderingEngine.spec.ts/viewport.png b/tests/screenshots/Mobile-Android/contextPoolRenderingEngine.spec.ts/viewport.png new file mode 100644 index 0000000000..6641f2eccc Binary files /dev/null and b/tests/screenshots/Mobile-Android/contextPoolRenderingEngine.spec.ts/viewport.png differ diff --git a/tests/screenshots/Mobile-Android/contourRenderingTiled.spec.ts/viewport.png b/tests/screenshots/Mobile-Android/contourRenderingTiled.spec.ts/viewport.png new file mode 100644 index 0000000000..8a8777c82e Binary files /dev/null and b/tests/screenshots/Mobile-Android/contourRenderingTiled.spec.ts/viewport.png differ diff --git a/tests/screenshots/Mobile-Android/labelmapRenderingTiled.spec.ts/axial.png b/tests/screenshots/Mobile-Android/labelmapRenderingTiled.spec.ts/axial.png new file mode 100644 index 0000000000..7cd77bc243 Binary files /dev/null and b/tests/screenshots/Mobile-Android/labelmapRenderingTiled.spec.ts/axial.png differ diff --git a/tests/screenshots/Mobile-Android/labelmapRenderingTiled.spec.ts/coronal.png b/tests/screenshots/Mobile-Android/labelmapRenderingTiled.spec.ts/coronal.png new file mode 100644 index 0000000000..fed0a3a386 Binary files /dev/null and b/tests/screenshots/Mobile-Android/labelmapRenderingTiled.spec.ts/coronal.png differ diff --git a/tests/screenshots/Mobile-Android/labelmapRenderingTiled.spec.ts/sagittal.png b/tests/screenshots/Mobile-Android/labelmapRenderingTiled.spec.ts/sagittal.png new file mode 100644 index 0000000000..5cbbaf4bb3 Binary files /dev/null and b/tests/screenshots/Mobile-Android/labelmapRenderingTiled.spec.ts/sagittal.png differ diff --git a/tests/screenshots/Mobile-Android/stackAnnotation.spec.ts/lengthTool.png b/tests/screenshots/Mobile-Android/stackAnnotation.spec.ts/lengthTool.png new file mode 100644 index 0000000000..c38551a813 Binary files /dev/null and b/tests/screenshots/Mobile-Android/stackAnnotation.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/Mobile-Android/stackAnnotationTiled.spec.ts/lengthTool.png b/tests/screenshots/Mobile-Android/stackAnnotationTiled.spec.ts/lengthTool.png new file mode 100644 index 0000000000..c38551a813 Binary files /dev/null and b/tests/screenshots/Mobile-Android/stackAnnotationTiled.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/Mobile-Android/stackBasicTiled.spec.ts/viewport.png b/tests/screenshots/Mobile-Android/stackBasicTiled.spec.ts/viewport.png new file mode 100644 index 0000000000..a4272ac6b1 Binary files /dev/null and b/tests/screenshots/Mobile-Android/stackBasicTiled.spec.ts/viewport.png differ diff --git a/tests/screenshots/Mobile-Android/volumeAnnotation.spec.ts/lengthTool.png b/tests/screenshots/Mobile-Android/volumeAnnotation.spec.ts/lengthTool.png new file mode 100644 index 0000000000..7495db8dcb Binary files /dev/null and b/tests/screenshots/Mobile-Android/volumeAnnotation.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/Mobile-Android/volumeAnnotationTiled.spec.ts/lengthTool.png b/tests/screenshots/Mobile-Android/volumeAnnotationTiled.spec.ts/lengthTool.png new file mode 100644 index 0000000000..7495db8dcb Binary files /dev/null and b/tests/screenshots/Mobile-Android/volumeAnnotationTiled.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/Mobile-Android/volumeBasicTiled.spec.ts/viewport.png b/tests/screenshots/Mobile-Android/volumeBasicTiled.spec.ts/viewport.png new file mode 100644 index 0000000000..c150d675ee Binary files /dev/null and b/tests/screenshots/Mobile-Android/volumeBasicTiled.spec.ts/viewport.png differ diff --git a/tests/screenshots/Mobile-Safari/contextPoolRenderingEngine.spec.ts/viewport.png b/tests/screenshots/Mobile-Safari/contextPoolRenderingEngine.spec.ts/viewport.png new file mode 100644 index 0000000000..ade9fb70b6 Binary files /dev/null and b/tests/screenshots/Mobile-Safari/contextPoolRenderingEngine.spec.ts/viewport.png differ diff --git a/tests/screenshots/Mobile-Safari/contourRenderingTiled.spec.ts/viewport.png b/tests/screenshots/Mobile-Safari/contourRenderingTiled.spec.ts/viewport.png new file mode 100644 index 0000000000..1cdc2d125f Binary files /dev/null and b/tests/screenshots/Mobile-Safari/contourRenderingTiled.spec.ts/viewport.png differ diff --git a/tests/screenshots/Mobile-Safari/labelmapRenderingTiled.spec.ts/axial.png b/tests/screenshots/Mobile-Safari/labelmapRenderingTiled.spec.ts/axial.png new file mode 100644 index 0000000000..04b0969fdc Binary files /dev/null and b/tests/screenshots/Mobile-Safari/labelmapRenderingTiled.spec.ts/axial.png differ diff --git a/tests/screenshots/Mobile-Safari/labelmapRenderingTiled.spec.ts/coronal.png b/tests/screenshots/Mobile-Safari/labelmapRenderingTiled.spec.ts/coronal.png new file mode 100644 index 0000000000..15d9b65e41 Binary files /dev/null and b/tests/screenshots/Mobile-Safari/labelmapRenderingTiled.spec.ts/coronal.png differ diff --git a/tests/screenshots/Mobile-Safari/labelmapRenderingTiled.spec.ts/sagittal.png b/tests/screenshots/Mobile-Safari/labelmapRenderingTiled.spec.ts/sagittal.png new file mode 100644 index 0000000000..843c7666cb Binary files /dev/null and b/tests/screenshots/Mobile-Safari/labelmapRenderingTiled.spec.ts/sagittal.png differ diff --git a/tests/screenshots/Mobile-Safari/stackAnnotation.spec.ts/lengthTool.png b/tests/screenshots/Mobile-Safari/stackAnnotation.spec.ts/lengthTool.png new file mode 100644 index 0000000000..72e16b4413 Binary files /dev/null and b/tests/screenshots/Mobile-Safari/stackAnnotation.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/Mobile-Safari/stackAnnotationTiled.spec.ts/lengthTool.png b/tests/screenshots/Mobile-Safari/stackAnnotationTiled.spec.ts/lengthTool.png new file mode 100644 index 0000000000..72e16b4413 Binary files /dev/null and b/tests/screenshots/Mobile-Safari/stackAnnotationTiled.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/Mobile-Safari/stackBasicTiled.spec.ts/viewport.png b/tests/screenshots/Mobile-Safari/stackBasicTiled.spec.ts/viewport.png new file mode 100644 index 0000000000..15c15c84e6 Binary files /dev/null and b/tests/screenshots/Mobile-Safari/stackBasicTiled.spec.ts/viewport.png differ diff --git a/tests/screenshots/Mobile-Safari/volumeAnnotation.spec.ts/lengthTool.png b/tests/screenshots/Mobile-Safari/volumeAnnotation.spec.ts/lengthTool.png new file mode 100644 index 0000000000..79a90bd44f Binary files /dev/null and b/tests/screenshots/Mobile-Safari/volumeAnnotation.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/Mobile-Safari/volumeAnnotationTiled.spec.ts/lengthTool.png b/tests/screenshots/Mobile-Safari/volumeAnnotationTiled.spec.ts/lengthTool.png new file mode 100644 index 0000000000..79a90bd44f Binary files /dev/null and b/tests/screenshots/Mobile-Safari/volumeAnnotationTiled.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/Mobile-Safari/volumeBasicTiled.spec.ts/viewport.png b/tests/screenshots/Mobile-Safari/volumeBasicTiled.spec.ts/viewport.png new file mode 100644 index 0000000000..ca77e01f76 Binary files /dev/null and b/tests/screenshots/Mobile-Safari/volumeBasicTiled.spec.ts/viewport.png differ diff --git a/tests/screenshots/chromium/contextPoolRenderingEngine.spec.ts/viewport.png b/tests/screenshots/chromium/contextPoolRenderingEngine.spec.ts/viewport.png new file mode 100644 index 0000000000..1d245c33a0 Binary files /dev/null and b/tests/screenshots/chromium/contextPoolRenderingEngine.spec.ts/viewport.png differ diff --git a/tests/screenshots/chromium/contourRenderingTiled.spec.ts/viewport.png b/tests/screenshots/chromium/contourRenderingTiled.spec.ts/viewport.png new file mode 100644 index 0000000000..710a7e1440 Binary files /dev/null and b/tests/screenshots/chromium/contourRenderingTiled.spec.ts/viewport.png differ diff --git a/tests/screenshots/chromium/labelmapRenderingTiled.spec.ts/axial.png b/tests/screenshots/chromium/labelmapRenderingTiled.spec.ts/axial.png new file mode 100644 index 0000000000..c955ca6785 Binary files /dev/null and b/tests/screenshots/chromium/labelmapRenderingTiled.spec.ts/axial.png differ diff --git a/tests/screenshots/chromium/labelmapRenderingTiled.spec.ts/coronal.png b/tests/screenshots/chromium/labelmapRenderingTiled.spec.ts/coronal.png new file mode 100644 index 0000000000..b15c170980 Binary files /dev/null and b/tests/screenshots/chromium/labelmapRenderingTiled.spec.ts/coronal.png differ diff --git a/tests/screenshots/chromium/labelmapRenderingTiled.spec.ts/sagittal.png b/tests/screenshots/chromium/labelmapRenderingTiled.spec.ts/sagittal.png new file mode 100644 index 0000000000..091b25345b Binary files /dev/null and b/tests/screenshots/chromium/labelmapRenderingTiled.spec.ts/sagittal.png differ diff --git a/tests/screenshots/chromium/stackAnnotation.spec.ts/lengthTool.png b/tests/screenshots/chromium/stackAnnotation.spec.ts/lengthTool.png new file mode 100644 index 0000000000..c38551a813 Binary files /dev/null and b/tests/screenshots/chromium/stackAnnotation.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/chromium/stackAnnotationTiled.spec.ts/lengthTool.png b/tests/screenshots/chromium/stackAnnotationTiled.spec.ts/lengthTool.png new file mode 100644 index 0000000000..c38551a813 Binary files /dev/null and b/tests/screenshots/chromium/stackAnnotationTiled.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/chromium/stackBasicTiled.spec.ts/viewport.png b/tests/screenshots/chromium/stackBasicTiled.spec.ts/viewport.png new file mode 100644 index 0000000000..a4272ac6b1 Binary files /dev/null and b/tests/screenshots/chromium/stackBasicTiled.spec.ts/viewport.png differ diff --git a/tests/screenshots/chromium/volumeAnnotation.spec.ts/lengthTool.png b/tests/screenshots/chromium/volumeAnnotation.spec.ts/lengthTool.png new file mode 100644 index 0000000000..ac2368ceee Binary files /dev/null and b/tests/screenshots/chromium/volumeAnnotation.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/chromium/volumeAnnotationTiled.spec.ts/lengthTool.png b/tests/screenshots/chromium/volumeAnnotationTiled.spec.ts/lengthTool.png new file mode 100644 index 0000000000..ac2368ceee Binary files /dev/null and b/tests/screenshots/chromium/volumeAnnotationTiled.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/chromium/volumeBasicTiled.spec.ts/viewport.png b/tests/screenshots/chromium/volumeBasicTiled.spec.ts/viewport.png new file mode 100644 index 0000000000..c150d675ee Binary files /dev/null and b/tests/screenshots/chromium/volumeBasicTiled.spec.ts/viewport.png differ diff --git a/tests/screenshots/webkit/contextPoolRenderingEngine.spec.ts/viewport.png b/tests/screenshots/webkit/contextPoolRenderingEngine.spec.ts/viewport.png new file mode 100644 index 0000000000..c341338fb0 Binary files /dev/null and b/tests/screenshots/webkit/contextPoolRenderingEngine.spec.ts/viewport.png differ diff --git a/tests/screenshots/webkit/contourRenderingTiled.spec.ts/viewport.png b/tests/screenshots/webkit/contourRenderingTiled.spec.ts/viewport.png new file mode 100644 index 0000000000..4ceb9d5511 Binary files /dev/null and b/tests/screenshots/webkit/contourRenderingTiled.spec.ts/viewport.png differ diff --git a/tests/screenshots/webkit/labelmapRenderingTiled.spec.ts/axial.png b/tests/screenshots/webkit/labelmapRenderingTiled.spec.ts/axial.png new file mode 100644 index 0000000000..b146955322 Binary files /dev/null and b/tests/screenshots/webkit/labelmapRenderingTiled.spec.ts/axial.png differ diff --git a/tests/screenshots/webkit/labelmapRenderingTiled.spec.ts/coronal.png b/tests/screenshots/webkit/labelmapRenderingTiled.spec.ts/coronal.png new file mode 100644 index 0000000000..a6e33ccb8e Binary files /dev/null and b/tests/screenshots/webkit/labelmapRenderingTiled.spec.ts/coronal.png differ diff --git a/tests/screenshots/webkit/labelmapRenderingTiled.spec.ts/sagittal.png b/tests/screenshots/webkit/labelmapRenderingTiled.spec.ts/sagittal.png new file mode 100644 index 0000000000..20bd0cf8b2 Binary files /dev/null and b/tests/screenshots/webkit/labelmapRenderingTiled.spec.ts/sagittal.png differ diff --git a/tests/screenshots/webkit/stackAnnotation.spec.ts/lengthTool.png b/tests/screenshots/webkit/stackAnnotation.spec.ts/lengthTool.png new file mode 100644 index 0000000000..72e16b4413 Binary files /dev/null and b/tests/screenshots/webkit/stackAnnotation.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/webkit/stackAnnotationTiled.spec.ts/lengthTool.png b/tests/screenshots/webkit/stackAnnotationTiled.spec.ts/lengthTool.png new file mode 100644 index 0000000000..72e16b4413 Binary files /dev/null and b/tests/screenshots/webkit/stackAnnotationTiled.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/webkit/stackBasicTiled.spec.ts/viewport.png b/tests/screenshots/webkit/stackBasicTiled.spec.ts/viewport.png new file mode 100644 index 0000000000..15c15c84e6 Binary files /dev/null and b/tests/screenshots/webkit/stackBasicTiled.spec.ts/viewport.png differ diff --git a/tests/screenshots/webkit/volumeAnnotation.spec.ts/lengthTool.png b/tests/screenshots/webkit/volumeAnnotation.spec.ts/lengthTool.png new file mode 100644 index 0000000000..e6fa107912 Binary files /dev/null and b/tests/screenshots/webkit/volumeAnnotation.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/webkit/volumeAnnotationTiled.spec.ts/lengthTool.png b/tests/screenshots/webkit/volumeAnnotationTiled.spec.ts/lengthTool.png new file mode 100644 index 0000000000..e6fa107912 Binary files /dev/null and b/tests/screenshots/webkit/volumeAnnotationTiled.spec.ts/lengthTool.png differ diff --git a/tests/screenshots/webkit/volumeBasicTiled.spec.ts/viewport.png b/tests/screenshots/webkit/volumeBasicTiled.spec.ts/viewport.png new file mode 100644 index 0000000000..ca77e01f76 Binary files /dev/null and b/tests/screenshots/webkit/volumeBasicTiled.spec.ts/viewport.png differ diff --git a/tests/splineContourSegmentationTools.spec.ts b/tests/splineContourSegmentationTools.spec.ts index b7b5aadadd..1486b12508 100644 --- a/tests/splineContourSegmentationTools.spec.ts +++ b/tests/splineContourSegmentationTools.spec.ts @@ -14,7 +14,7 @@ test.describe('Spline Contour Segmentation Tools', async () => { test('should draw a CatmullRom Spline ROI when CatmullRom Spline ROI is selected', async ({ page, }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await drawCatmullROMSplineOnViewportLeft({ page, @@ -31,7 +31,7 @@ test.describe('Spline Contour Segmentation Tools', async () => { test('should draw a Linear Spline ROI when Linear Spline ROI is selected', async ({ page, }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await drawLinearSplineOnViewportCenter({ page, @@ -48,7 +48,7 @@ test.describe('Spline Contour Segmentation Tools', async () => { test('should draw a BSpline ROI when BSpline ROI is selected when BSpline ROI is selected', async ({ page, }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await drawBSplineOnViewportRight({ page, @@ -65,7 +65,7 @@ test.describe('Spline Contour Segmentation Tools', async () => { test('should have different colors when splines are added to different segments', async ({ page, }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); await drawCatmullROMSplineOnViewportLeft({ page, @@ -84,7 +84,7 @@ test.describe('Spline Contour Segmentation Tools', async () => { test.skip('should apply the styles to the splines appropriately when splines are drawn with different styles', async ({ page, }) => { - const canvas = await page.locator('canvas'); + const canvas = await page.locator('canvas.cornerstone-canvas'); const splineStyle = { outlineWidth: 1.7, outlineOpacity: 0.5, diff --git a/tests/stackAnnotation.spec.ts b/tests/stackAnnotation.spec.ts new file mode 100644 index 0000000000..fce9bcaeb2 --- /dev/null +++ b/tests/stackAnnotation.spec.ts @@ -0,0 +1,23 @@ +import { test } from 'playwright-test-coverage'; +import { + visitExample, + checkForScreenshot, + screenShotPaths, + simulateDrag, +} from './utils/index'; + +test.beforeEach(async ({ page }) => { + await visitExample(page, 'stackAnnotationTools'); +}); + +test.describe('Stack Annotation Tools', async () => { + test('should draw a length measurement on the viewport', async ({ page }) => { + const locator = page.locator('.cornerstone-canvas').nth(0); + await simulateDrag(page, locator); + await checkForScreenshot( + page, + locator, + screenShotPaths.stackAnnotation.lengthTool + ); + }); +}); diff --git a/tests/stackAnnotationTiled.spec.ts b/tests/stackAnnotationTiled.spec.ts new file mode 100644 index 0000000000..e4534667bd --- /dev/null +++ b/tests/stackAnnotationTiled.spec.ts @@ -0,0 +1,24 @@ +import { test } from 'playwright-test-coverage'; +import { + visitExample, + checkForScreenshot, + screenShotPaths, + simulateDrag, +} from './utils/index'; + +test.beforeEach(async ({ page, context }) => { + await context.addInitScript(() => (window.IS_TILED = true)); + await visitExample(page, 'stackAnnotationTools'); +}); + +test.describe('Stack Annotation Tools - Tiled', async () => { + test('should draw a length measurement on the viewport', async ({ page }) => { + const locator = page.locator('.cornerstone-canvas').nth(0); + await simulateDrag(page, locator); + await checkForScreenshot( + page, + locator, + screenShotPaths.stackAnnotationTiled.lengthTool + ); + }); +}); diff --git a/tests/stackBasicTiled.spec.ts b/tests/stackBasicTiled.spec.ts new file mode 100644 index 0000000000..ae04aaed4f --- /dev/null +++ b/tests/stackBasicTiled.spec.ts @@ -0,0 +1,24 @@ +import { test } from 'playwright-test-coverage'; +import { + visitExample, + checkForScreenshot, + screenShotPaths, +} from './utils/index'; + +test.beforeEach(async ({ page, context }) => { + await visitExample(page, 'stackBasic'); + await context.addInitScript(() => (window.IS_TILED = true)); +}); + +test.describe('Basic Stack', async () => { + test('should display a single DICOM image in a Stack viewport', async ({ + page, + }) => { + const locator = page.locator('.cornerstone-canvas'); + await checkForScreenshot( + page, + locator, + screenShotPaths.stackBasicTiled.viewport + ); + }); +}); diff --git a/tests/utils/reduceViewportsSize.ts b/tests/utils/reduceViewportsSize.ts index 2c13956353..4402ddcf95 100644 --- a/tests/utils/reduceViewportsSize.ts +++ b/tests/utils/reduceViewportsSize.ts @@ -5,12 +5,15 @@ */ export const reduceViewportsSize = async (page) => { - await page.evaluate(({ cornerstone }) => { - const enabledElements = cornerstone.getEnabledElements(); + await page.evaluate( + ({ cornerstone }) => { + const enabledElements = cornerstone.getEnabledElements(); - enabledElements.forEach(({ viewport }) => { - viewport.setZoom(0.4); - viewport.render(); - }); - }, await page.evaluateHandle('window')); + enabledElements.forEach(({ viewport }) => { + viewport.setZoom(0.4); + viewport.render(); + }); + }, + await page.evaluateHandle('window') + ); }; diff --git a/tests/utils/screenShotPaths.ts b/tests/utils/screenShotPaths.ts index c572e2833e..ae04c76e5c 100644 --- a/tests/utils/screenShotPaths.ts +++ b/tests/utils/screenShotPaths.ts @@ -14,6 +14,9 @@ const screenShotPaths = { stackBasic: { viewport: 'viewport.png', }, + stackBasicTiled: { + viewport: 'viewport.png', + }, stackToVolumeFusion: { viewport: 'viewport.png', }, @@ -50,6 +53,9 @@ const screenShotPaths = { volumeBasic: { viewport: 'viewport.png', }, + volumeBasicTiled: { + viewport: 'viewport.png', + }, ultrasoundColors: { slice1: 'slice1.png', slice2: 'slice2.png', @@ -116,9 +122,17 @@ const screenShotPaths = { coronal: 'coronal.png', sagittal: 'sagittal.png', }, + labelmapRenderingTiled: { + axial: 'axial.png', + coronal: 'coronal.png', + sagittal: 'sagittal.png', + }, contourRendering: { viewport: 'viewport.png', }, + contourRenderingTiled: { + viewport: 'viewport.png', + }, labelmapGlobalConfiguration: { defaultGlobalConfig: 'defaultGlobalConfig.png', toggleInactiveSegmentation: 'toggleInactiveSegmentation.png', @@ -194,6 +208,21 @@ const screenShotPaths = { afterReformat: 'mpr-reformat-after.png', afterInteraction: 'mpr-reformat-after-interaction.png', }, + contextPoolRenderingEngine: { + viewport: 'viewport.png', + }, + volumeAnnotation: { + lengthTool: 'lengthTool.png', + }, + volumeAnnotationTiled: { + lengthTool: 'lengthTool.png', + }, + stackAnnotation: { + lengthTool: 'lengthTool.png', + }, + stackAnnotationTiled: { + lengthTool: 'lengthTool.png', + }, }; export { screenShotPaths }; diff --git a/tests/volumeAnnotation.spec.ts b/tests/volumeAnnotation.spec.ts new file mode 100644 index 0000000000..fed7bf4504 --- /dev/null +++ b/tests/volumeAnnotation.spec.ts @@ -0,0 +1,23 @@ +import { test } from 'playwright-test-coverage'; +import { + visitExample, + checkForScreenshot, + screenShotPaths, + simulateDrag, +} from './utils/index'; + +test.beforeEach(async ({ page }) => { + await visitExample(page, 'volumeAnnotationTools'); +}); + +test.describe('Volume Annotation Tools', async () => { + test('should draw a length measurement on the viewport', async ({ page }) => { + const locator = page.locator('.cornerstone-canvas').nth(0); + await simulateDrag(page, locator); + await checkForScreenshot( + page, + locator, + screenShotPaths.volumeAnnotation.lengthTool + ); + }); +}); diff --git a/tests/volumeAnnotationTiled.spec.ts b/tests/volumeAnnotationTiled.spec.ts new file mode 100644 index 0000000000..c00cd95b33 --- /dev/null +++ b/tests/volumeAnnotationTiled.spec.ts @@ -0,0 +1,24 @@ +import { test } from 'playwright-test-coverage'; +import { + visitExample, + checkForScreenshot, + screenShotPaths, + simulateDrag, +} from './utils/index'; + +test.beforeEach(async ({ page, context }) => { + await context.addInitScript(() => (window.IS_TILED = true)); + await visitExample(page, 'volumeAnnotationTools'); +}); + +test.describe('Volume Annotation Tools - Tiled', async () => { + test('should draw a length measurement on the viewport', async ({ page }) => { + const locator = page.locator('.cornerstone-canvas').nth(0); + await simulateDrag(page, locator); + await checkForScreenshot( + page, + locator, + screenShotPaths.volumeAnnotationTiled.lengthTool + ); + }); +}); diff --git a/tests/volumeBasicTiled.spec.ts b/tests/volumeBasicTiled.spec.ts new file mode 100644 index 0000000000..f95eef83bb --- /dev/null +++ b/tests/volumeBasicTiled.spec.ts @@ -0,0 +1,25 @@ +import { test } from 'playwright-test-coverage'; +import { + visitExample, + checkForScreenshot, + screenShotPaths, +} from './utils/index'; + +test.beforeEach(async ({ page, context }) => { + await visitExample(page, 'volumeBasic'); + await context.addInitScript(() => (window.IS_TILED = true)); +}); + +test.describe('Basic Volume', async () => { + test('should display a single DICOM series in a Volume viewport.', async ({ + page, + }) => { + // Now take the screenshot + const locator = page.locator('.cornerstone-canvas'); + await checkForScreenshot( + page, + locator, + screenShotPaths.volumeBasicTiled.viewport + ); + }); +}); diff --git a/tsconfig.base.json b/tsconfig.base.json index a0ae8957e7..f976a16fcd 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -36,7 +36,6 @@ "packages/**/dist", "packages/**/lib", "packages/**/lib-esm", - "packages/adapters/**/*.js", "packages/dicomImageLoader/codecs/**/*", "packages/docs", "packages/docs/**/*", diff --git a/tsconfig.json b/tsconfig.json index 21596cd2c1..c824c6790e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,9 @@ "@cornerstonejs/nifti-volume-loader": ["nifti-volume-loader/src"], "@cornerstonejs/ai": ["ai/src"], "@cornerstonejs/labelmap-interpolation": ["labelmap-interpolation/src"], - "@cornerstonejs/polymorphic-segmentation": ["polymorphic-segmentation/src"], + "@cornerstonejs/polymorphic-segmentation": [ + "polymorphic-segmentation/src" + ], "@cornerstonejs/adapters": ["adapters/src"] } } diff --git a/utils/ExampleRunner/example-info.json b/utils/ExampleRunner/example-info.json index a60649716e..05cbb38bac 100644 --- a/utils/ExampleRunner/example-info.json +++ b/utils/ExampleRunner/example-info.json @@ -149,9 +149,25 @@ "name": "Apply view reference and/or presentation parameters", "description": "Demonstrates how to apply various view/reference presentation parameters." }, + "stackToVolume": { + "name": "Stack to Volume Viewport", + "description": "Demonstrates how to convert a Stack Viewport to a Volume Viewport" + }, "webWorker": { "name": "Custom Web Worker Function", "description": "Demonstrates how to use the web worker manager to register and execute custom web worker functions off the main thread" + }, + "contextpoolrenderingengine": { + "name": "6x6 Grid with ContextPoolRenderingEngine", + "description": "Displays a 6x6 grid of viewports using ContextPoolRenderingEngine for better performance with large viewport counts" + }, + "ptctmultimonitor": { + "name": "PET-CT Multi-Monitor Layout", + "description": "Demonstrates how to create a multi-monitor layout with PET-CT fusion using the ContextPoolRenderingEngine" + }, + "webGLContextPooling": { + "name": "WebGL Context Pooling", + "description": "Demonstrates how to use WebGL context pooling to render many viewports in sync using ContextPoolRenderingEngine" } }, "tools-basic": { @@ -163,6 +179,10 @@ "name": "3D Volume Picking", "description": "Demonstrates how to use the VTK.js vtkCellPicker object to pick 3D point in volume rendering scene. Also shows how to synchronize between 3D and 2D viewports." }, + "volumeCroppingTool": { + "name": "3D Volume Cropping", + "description": "Demonstrates how to use the VolumeCropping and VolumeCroppingControl tools." + }, "multipleToolGroups": { "name": "Multiple Tool Groups", "description": "Demonstrates the usage of multiple tool groups for a set of viewports." @@ -534,6 +554,10 @@ "toolHistory": { "name": "Tool History", "description": "Demonstrates how to use the tool history to undo and redo tool actions" + }, + "toolHistoryGrouping": { + "name": "Tool History Grouping", + "description": "Demonstrates how to use the tool history grouping to undo and redo batched tool actions that are related to one another" } }, "polymorph-segmentation": { diff --git a/utils/ExampleRunner/template-config.js b/utils/ExampleRunner/template-config.js index 5a3e4a2380..56f729f112 100644 --- a/utils/ExampleRunner/template-config.js +++ b/utils/ExampleRunner/template-config.js @@ -40,7 +40,7 @@ module.exports = { { from: '../../../node_modules/dicom-microscopy-viewer/dist/dynamic-import/', - to: '${destPath.replace(/\\/g, '/')}', + to: '${destPath.replace(/\\/g, '/') + '/dicom-microscopy-viewer'}', noErrorOnMissing: true, }, { diff --git a/utils/ExampleRunner/template-multiexample-config.js b/utils/ExampleRunner/template-multiexample-config.js index 5afd8b0b6d..01eef335b1 100644 --- a/utils/ExampleRunner/template-multiexample-config.js +++ b/utils/ExampleRunner/template-multiexample-config.js @@ -73,7 +73,7 @@ module.exports = { }, { from: '../../../node_modules/dicom-microscopy-viewer/dist/dynamic-import/', - to: '${destPath.replace(/\\/g, '/')}', + to: '${destPath.replace(/\\/g, '/') + '/dicom-microscopy-viewer/'}', noErrorOnMissing: true, }, { diff --git a/utils/assets/assetsURL.json b/utils/assets/assetsURL.json index fd928c5ef3..cfd910779c 100644 --- a/utils/assets/assetsURL.json +++ b/utils/assets/assetsURL.json @@ -7,5 +7,4 @@ "SurfaceLung16": "https://ohif-assets.s3.us-east-2.amazonaws.com/cornerstone3D/segmentation/surface/lung16.json", "SurfaceLung17": "https://ohif-assets.s3.us-east-2.amazonaws.com/cornerstone3D/segmentation/surface/lung17.json", "Labelmap": "https://ohif-assets.s3.us-east-2.amazonaws.com/cornerstone3D/segmentation/labelmap/lung/labelMap.json" - } diff --git a/utils/demo/helpers/index.js b/utils/demo/helpers/index.js index b399c7fcc5..220bf30626 100644 --- a/utils/demo/helpers/index.js +++ b/utils/demo/helpers/index.js @@ -20,6 +20,8 @@ import createInfoSection from './createInfoSection'; import downloadSurfacesData from './downloadSurfacesData'; import getLocalUrl from './getLocalUrl'; import initDemo from './initDemo'; +import initProviders from './initProviders'; +import initVolumeLoader from './initVolumeLoader'; import labelmapTools from './labelmapTools'; import setCtTransferFunctionForVolumeActor, { ctVoiRange, @@ -59,6 +61,8 @@ export { downloadSurfacesData, getLocalUrl, initDemo, + initProviders, + initVolumeLoader, labelmapTools, setCtTransferFunctionForVolumeActor, setPetColorMapTransferFunctionForVolumeActor, diff --git a/utils/demo/helpers/initDemo.ts b/utils/demo/helpers/initDemo.ts index 55ee47480c..2e6f123764 100644 --- a/utils/demo/helpers/initDemo.ts +++ b/utils/demo/helpers/initDemo.ts @@ -20,13 +20,28 @@ import * as polySeg from '@cornerstonejs/polymorphic-segmentation'; window.cornerstone = cornerstone; window.cornerstoneTools = cornerstoneTools; -export default async function initDemo(config = {}) { +export default async function initDemo(config: any = {}) { initProviders(); cornerstoneDICOMImageLoader.init(); initVolumeLoader(); + + const urlParams = new URLSearchParams(window.location.search); + const debugEnabled = urlParams.get('debug') === 'true'; + await csRenderInit({ peerImport, - ...(config?.core ? config.core : {}), + ...(config?.core + ? { + ...config.core, + debug: { + statsOverlay: debugEnabled, + }, + } + : { + debug: { + statsOverlay: debugEnabled, + }, + }), }); await csToolsInit({ addons: { @@ -47,6 +62,10 @@ export default async function initDemo(config = {}) { */ export async function peerImport(moduleId) { if (moduleId === 'dicom-microscopy-viewer') { + // The microscopy viewer loads relative to the public URL + window.PUBLIC_URL ||= '/'; + // Use a relative library path that includes the component name + window.PUBLIC_LIB_URL ||= './${component}/'; return importGlobal( '/dicom-microscopy-viewer/dicomMicroscopyViewer.min.js', 'dicomMicroscopyViewer' diff --git a/yarn.lock b/yarn.lock index 7a0450b0b9..f624b407f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -62,15 +62,6 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/eslint-parser@^7.19.1": - version "7.25.1" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.25.1.tgz#469cee4bd18a88ff3edbdfbd227bd20e82aa9b82" - integrity sha512-Y956ghgTT4j7rKesabkh5WeqgSFZVFwaPR0IWFm7KFHFmmJ4afbG49SmfW4S+GyRPx0Dy5jxEWA5t0rpxfElWg== - dependencies: - "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" - eslint-visitor-keys "^2.1.0" - semver "^6.3.1" - "@babel/generator@^7.25.0", "@babel/generator@^7.7.2": version "7.25.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.0.tgz#f858ddfa984350bc3d3b7f125073c9af6988f18e" @@ -1125,7 +1116,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@7.26.10", "@babel/runtime@^7.20.7", "@babel/runtime@^7.23.2", "@babel/runtime@^7.8.4": +"@babel/runtime@7.26.10", "@babel/runtime@^7.23.2", "@babel/runtime@^7.8.4": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== @@ -1288,7 +1279,7 @@ resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-libjpeg-turbo-8bit/-/codec-libjpeg-turbo-8bit-1.2.2.tgz#ae384b149d6655e3dd6e18b9891fab479ab5e144" integrity sha512-aAUMK2958YNpOb/7G6e2/aG7hExTiFTASlMt/v90XA0pRHdWiNg5ny4S5SAju0FbIw4zcMnR0qfY+yW3VG2ivg== -"@cornerstonejs/codec-openjpeg@^1.2.2": +"@cornerstonejs/codec-openjpeg@^1.2.2", "@cornerstonejs/codec-openjpeg@^1.2.4": version "1.2.4" resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-1.2.4.tgz#c7cf67a34091eb74a6676abec80a5251c412b551" integrity sha512-UT2su6xZZnCPSuWf2ldzKa/2+guQ7BGgfBSKqxanggwJHh48gZqIAzekmsLyJHMMK5YDK+ti+fzvVJhBS3Xi/g== @@ -1580,6 +1571,14 @@ "@emnapi/wasi-threads" "1.0.1" tslib "^2.4.0" +"@emnapi/core@^1.4.3": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.5.tgz#bfbb0cbbbb9f96ec4e2c4fd917b7bbe5495ceccb" + integrity sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q== + dependencies: + "@emnapi/wasi-threads" "1.0.4" + tslib "^2.4.0" + "@emnapi/runtime@^1.1.0": version "1.3.1" resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.3.1.tgz#0fcaa575afc31f455fd33534c19381cfce6c6f60" @@ -1587,6 +1586,13 @@ dependencies: tslib "^2.4.0" +"@emnapi/runtime@^1.4.3": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.5.tgz#c67710d0661070f38418b6474584f159de38aba9" + integrity sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg== + dependencies: + tslib "^2.4.0" + "@emnapi/wasi-threads@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz#d7ae71fd2166b1c916c6cd2d0df2ef565a2e1a5b" @@ -1594,6 +1600,13 @@ dependencies: tslib "^2.4.0" +"@emnapi/wasi-threads@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz#703fc094d969e273b1b71c292523b2f792862bf4" + integrity sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g== + dependencies: + tslib "^2.4.0" + "@esbuild/aix-ppc64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz#2acd20be6d4f0458bc8c784103495ff24f13b1d3" @@ -1824,38 +1837,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.2.tgz#825b4e7c89b7e7ec64c450ed494a8af7e405a84d" integrity sha512-VEfTCZicoZnZ6sGkjFPGRFFJuL2fZn2bLhsekZl1CJslflp2cJS/VoKs1jMk+3pDfsGW6CfQVUckP707HwbXeQ== -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" - integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== - dependencies: - eslint-visitor-keys "^3.3.0" - -"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1": - version "4.11.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae" - integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A== - -"@eslint/eslintrc@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" - integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.6.0" - globals "^13.19.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@eslint/js@8.57.0": - version "8.57.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" - integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== - "@fastify/accept-negotiator@^1.0.0", "@fastify/accept-negotiator@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz#c1c66b3b771c09742a54dd5bc87c582f6b0630ff" @@ -1912,30 +1893,11 @@ fastq "^1.17.0" glob "^10.3.4" -"@humanwhocodes/config-array@^0.11.14": - version "0.11.14" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" - integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== - dependencies: - "@humanwhocodes/object-schema" "^2.0.2" - debug "^4.3.1" - minimatch "^3.0.5" - -"@humanwhocodes/module-importer@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" - integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== - "@humanwhocodes/momoa@^2.0.2": version "2.0.4" resolved "https://registry.yarnpkg.com/@humanwhocodes/momoa/-/momoa-2.0.4.tgz#8b9e7a629651d15009c3587d07a222deeb829385" integrity sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA== -"@humanwhocodes/object-schema@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" - integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== - "@hutson/parse-repository-url@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" @@ -2453,25 +2415,6 @@ resolved "https://registry.yarnpkg.com/@lukeed/ms/-/ms-2.0.2.tgz#07f09e59a74c52f4d88c6db5c1054e819538e2a8" integrity sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA== -"@mapbox/jsonlint-lines-primitives@~2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234" - integrity sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ== - -"@mapbox/mapbox-gl-style-spec@^13.23.1": - version "13.28.0" - resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.28.0.tgz#2ec226320a0f77856046e000df9b419303a56458" - integrity sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg== - dependencies: - "@mapbox/jsonlint-lines-primitives" "~2.0.2" - "@mapbox/point-geometry" "^0.1.0" - "@mapbox/unitbezier" "^0.0.0" - csscolorparser "~1.0.2" - json-stringify-pretty-compact "^2.0.0" - minimist "^1.2.6" - rw "^1.3.3" - sort-object "^0.3.2" - "@mapbox/node-pre-gyp@^1.0.5": version "1.0.11" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" @@ -2487,16 +2430,6 @@ semver "^7.3.5" tar "^6.1.11" -"@mapbox/point-geometry@^0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2" - integrity sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ== - -"@mapbox/unitbezier@^0.0.0": - version "0.0.0" - resolved "https://registry.yarnpkg.com/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz#15651bd553a67b8581fb398810c98ad86a34524e" - integrity sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA== - "@microsoft/api-extractor-model@7.30.3": version "7.30.3" resolved "https://registry.yarnpkg.com/@microsoft/api-extractor-model/-/api-extractor-model-7.30.3.tgz#d1256b6955c8c2a1115e0cfe99e1e8f9802e52cc" @@ -2525,16 +2458,6 @@ source-map "~0.6.1" typescript "5.7.2" -"@microsoft/tsdoc-config@0.16.2": - version "0.16.2" - resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz#b786bb4ead00d54f53839a458ce626c8548d3adf" - integrity sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw== - dependencies: - "@microsoft/tsdoc" "0.14.2" - ajv "~6.12.6" - jju "~1.4.0" - resolve "~1.19.0" - "@microsoft/tsdoc-config@~0.17.1": version "0.17.1" resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.17.1.tgz#e0f0b50628f4ad7fe121ca616beacfe6a25b9335" @@ -2545,11 +2468,6 @@ jju "~1.4.0" resolve "~1.22.2" -"@microsoft/tsdoc@0.14.2": - version "0.14.2" - resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz#c3ec604a0b54b9a9b87e9735dfc59e1a5da6a5fb" - integrity sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug== - "@microsoft/tsdoc@0.15.1", "@microsoft/tsdoc@~0.15.1": version "0.15.1" resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz#d4f6937353bc4568292654efb0a0e0532adbcba2" @@ -2637,6 +2555,15 @@ "@emnapi/runtime" "^1.1.0" "@tybys/wasm-util" "^0.9.0" +"@napi-rs/wasm-runtime@^0.2.11": + version "0.2.12" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" + integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ== + dependencies: + "@emnapi/core" "^1.4.3" + "@emnapi/runtime" "^1.4.3" + "@tybys/wasm-util" "^0.10.0" + "@netlify/binary-info@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@netlify/binary-info/-/binary-info-1.0.0.tgz#cd0d86fb783fb03e52067f0cd284865e57be86c8" @@ -2998,13 +2925,6 @@ yargs "^17.0.0" zod "^3.23.8" -"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": - version "5.1.1-v1" - resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" - integrity sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg== - dependencies: - eslint-scope "5.1.1" - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -3018,7 +2938,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": +"@nodelib/fs.walk@^1.2.3": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -3505,6 +3425,128 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.8.0.tgz#5aa7abb48f23f693068ed2999ae627d2f7d902ec" integrity sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w== +"@oxc-parser/binding-android-arm64@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.74.0.tgz#011237edea650491eb716cd3b5717ca989e22218" + integrity sha512-lgq8TJq22eyfojfa2jBFy2m66ckAo7iNRYDdyn9reXYA3I6Wx7tgGWVx1JAp1lO+aUiqdqP/uPlDaETL9tqRcg== + +"@oxc-parser/binding-darwin-arm64@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.74.0.tgz#7240779fcf19f671f8eab6602ec4a721fdbd6f9b" + integrity sha512-xbY/io/hkARggbpYEMFX6CwFzb7f4iS6WuBoBeZtdqRWfIEi7sm/uYWXfyVeB8uqOATvJ07WRFC2upI8PSI83g== + +"@oxc-parser/binding-darwin-x64@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.74.0.tgz#7f8caef0a2772a370bd7169113f9aca179c23236" + integrity sha512-FIj2gAGtFaW0Zk+TnGyenMUoRu1ju+kJ/h71D77xc1owOItbFZFGa+4WSVck1H8rTtceeJlK+kux+vCjGFCl9Q== + +"@oxc-parser/binding-freebsd-x64@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.74.0.tgz#aed875794b21c6b88eb8304baa24c22c28433205" + integrity sha512-W1I+g5TJg0TRRMHgEWNWsTIfe782V3QuaPgZxnfPNmDMywYdtlzllzclBgaDq6qzvZCCQc/UhvNb37KWTCTj8A== + +"@oxc-parser/binding-linux-arm-gnueabihf@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.74.0.tgz#d000011a8e9959d9a5d517371dfc79c7e93bf495" + integrity sha512-gxqkyRGApeVI8dgvJ19SYe59XASW3uVxF1YUgkE7peW/XIg5QRAOVTFKyTjI9acYuK1MF6OJHqx30cmxmZLtiQ== + +"@oxc-parser/binding-linux-arm-musleabihf@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.74.0.tgz#c3c045017845b55fb91aae89de09e6a6e5281461" + integrity sha512-jpnAUP4Fa93VdPPDzxxBguJmldj/Gpz7wTXKFzpAueqBMfZsy9KNC+0qT2uZ9HGUDMzNuKw0Se3bPCpL/gfD2Q== + +"@oxc-parser/binding-linux-arm64-gnu@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.74.0.tgz#cb13b831352231d8f7f87e1aa007c1146a4b5e58" + integrity sha512-fcWyM7BNfCkHqIf3kll8fJctbR/PseL4RnS2isD9Y3FFBhp4efGAzhDaxIUK5GK7kIcFh1P+puIRig8WJ6IMVQ== + +"@oxc-parser/binding-linux-arm64-musl@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.74.0.tgz#c7ac40e70ba656815ad53d7d33b26a870107456f" + integrity sha512-AMY30z/C77HgiRRJX7YtVUaelKq1ex0aaj28XoJu4SCezdS8i0IftUNTtGS1UzGjGZB8zQz5SFwVy4dRu4GLwg== + +"@oxc-parser/binding-linux-riscv64-gnu@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.74.0.tgz#3b8d6317803b44610ed7c6297821ca834104c303" + integrity sha512-/RZAP24TgZo4vV/01TBlzRqs0R7E6xvatww4LnmZEBBulQBU/SkypDywfriFqWuFoa61WFXPV7sLcTjJGjim/w== + +"@oxc-parser/binding-linux-s390x-gnu@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.74.0.tgz#c111ac2c83d38ce4cd40c90c2f7ad8b61dacd52d" + integrity sha512-620J1beNAlGSPBD+Msb3ptvrwxu04B8iULCH03zlf0JSLy/5sqlD6qBs0XUVkUJv1vbakUw1gfVnUQqv0UTuEg== + +"@oxc-parser/binding-linux-x64-gnu@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.74.0.tgz#a8483cf6aaa63c76dc78a478853de570411b652c" + integrity sha512-WBFgQmGtFnPNzHyLKbC1wkYGaRIBxXGofO0+hz1xrrkPgbxbJS1Ukva1EB8sPaVBBQ52Bdc2GjLSp721NWRvww== + +"@oxc-parser/binding-linux-x64-musl@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.74.0.tgz#aa933b967509f936da7b3ebc7387e53d20f1d6ed" + integrity sha512-y4mapxi0RGqlp3t6Sm+knJlAEqdKDYrEue2LlXOka/F2i4sRN0XhEMPiSOB3ppHmvK4I2zY2XBYTsX1Fel0fAg== + +"@oxc-parser/binding-wasm32-wasi@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.74.0.tgz#f610be484451235419fc14837c5c2c5c127a5b2b" + integrity sha512-yDS9bRDh5ymobiS2xBmjlrGdUuU61IZoJBaJC5fELdYT5LJNBXlbr3Yc6m2PWfRJwkH6Aq5fRvxAZ4wCbkGa8w== + dependencies: + "@napi-rs/wasm-runtime" "^0.2.11" + +"@oxc-parser/binding-win32-arm64-msvc@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.74.0.tgz#84daac72cb58d51f5cc83dd41f4faae430f83e55" + integrity sha512-XFWY52Rfb4N5wEbMCTSBMxRkDLGbAI9CBSL24BIDywwDJMl31gHEVlmHdCDRoXAmanCI6gwbXYTrWe0HvXJ7Aw== + +"@oxc-parser/binding-win32-x64-msvc@0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.74.0.tgz#ff6f9f711b28e7042d0c84937ae28aa9ee4d10a0" + integrity sha512-1D3x6iU2apLyfTQHygbdaNbX3nZaHu4yaXpD7ilYpoLo7f0MX0tUuoDrqJyJrVGqvyXgc0uz4yXz9tH9ZZhvvg== + +"@oxc-project/types@^0.74.0": + version "0.74.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.74.0.tgz#3e4e997ec22fd7dac3fd05dd2edf2fca143e253a" + integrity sha512-KOw/RZrVlHGhCXh1RufBFF7Nuo7HdY5w1lRJukM/igIl6x9qtz8QycDvZdzb4qnHO7znrPyo2sJrFJK2eKHgfQ== + +"@oxlint/darwin-arm64@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@oxlint/darwin-arm64/-/darwin-arm64-1.9.0.tgz#5a772911f6d9dd56231be5cf4beb3db8a0bdb918" + integrity sha512-VRxI/T0I4bq+xoaI0qNFeGPxOOganHlfLmx8JbFFZswoxMkm5lIvVYScDKLrsbbPSe4bcZ5v1DmX5sNGQ619Uw== + +"@oxlint/darwin-x64@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@oxlint/darwin-x64/-/darwin-x64-1.9.0.tgz#fb917ece591b28c8dacf689daa2e9b5e173252bf" + integrity sha512-vMBa3eNrJMSBApXvsx6FgMuWCnNE+ETJJieLPhemZKctMNWOJQX+2i09CG2kb1IkmxagLapH7dZ4i0+Lf13mqg== + +"@oxlint/linux-arm64-gnu@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-1.9.0.tgz#d0f5064dc260eef30c72cfc01db57f68472cc599" + integrity sha512-8wnMjloRbz7hPKvcgqd8HKNgkEgFJZvp9Los2pdE1CKh33msIxIXPGT3KKhYzyrtBaRaG2LJHshUPub02Q+x9A== + +"@oxlint/linux-arm64-musl@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@oxlint/linux-arm64-musl/-/linux-arm64-musl-1.9.0.tgz#5f79d284fb1000cd57b58258cccd3271c3ec8156" + integrity sha512-elj9FTNXvq9oP8UedeHHq2R8lCtTmBGvUO/xLez734zVx3tvBl3avmuEfPBqR4wlUKDANExDh3Rg0pssflhw9w== + +"@oxlint/linux-x64-gnu@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@oxlint/linux-x64-gnu/-/linux-x64-gnu-1.9.0.tgz#bd6da8a5602498d71781f6fe9104e678ba4ead92" + integrity sha512-d7VjQttKDgXKIYY3tm2GXTD83dyI+D8POGdPRT8GjhitHKasHQWFMBDG5iLp60FNc5Ky1cIe/2nwKW9ReQvhKw== + +"@oxlint/linux-x64-musl@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@oxlint/linux-x64-musl/-/linux-x64-musl-1.9.0.tgz#d4ba804d9fabbcf6742cb84f0d16ee72eb228ef6" + integrity sha512-R9vi5LGxNNi3pisp4dG7Ez00mAvCeki8VI5DUg6W1MR0GTnaejlVtMflziA2jqpdTGOK4bjLDsHKIadGC1SMVg== + +"@oxlint/win32-arm64@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@oxlint/win32-arm64/-/win32-arm64-1.9.0.tgz#01c051ddeba05596dfac61dd8a7addaa57bf1bf9" + integrity sha512-TiV7xYBMhkc/r0X+c9cMwGKUWYJRlWnI61m8powqer6lMJ8VfDe1RkRF4ttO5wlPGkiBwBa3vYWAgxaXc9JAMQ== + +"@oxlint/win32-x64@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@oxlint/win32-x64/-/win32-x64-1.9.0.tgz#9a41059ea95262592fdf19060e3b132999e863bc" + integrity sha512-OWqqkXsLrpS1uQsxjNqiOkC9a/CszMLa3VwlRcpm/z3iPxEL/qEVjGfjZX6XZw8Q6YukFB77rgYqzotvtCvI1A== + "@parcel/watcher-android-arm64@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz#c2c19a3c442313ff007d2d7a9c2c1dd3e1c9ca84" @@ -3653,6 +3695,13 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817" integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ== +"@prettier/plugin-oxc@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@prettier/plugin-oxc/-/plugin-oxc-0.0.4.tgz#17fd31c29f3b4df604db6119820f3e75cd5677b7" + integrity sha512-UGXe+g/rSRbglL0FOJiar+a+nUrst7KaFmsg05wYbKiInGWP6eAj/f8A2Uobgo5KxEtb2X10zeflNH6RK2xeIQ== + dependencies: + oxc-parser "0.74.0" + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -4127,6 +4176,13 @@ "@tufjs/canonical-json" "2.0.0" minimatch "^9.0.4" +"@tybys/wasm-util@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.0.tgz#2fd3cd754b94b378734ce17058d0507c45c88369" + integrity sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ== + dependencies: + tslib "^2.4.0" + "@tybys/wasm-util@^0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz#3e75eb00604c8d6db470bf18c37b7d984a0e3355" @@ -4245,14 +4301,6 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/eslint@^8.56.10": - version "8.56.11" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.11.tgz#e2ff61510a3b9454b3329fe7731e3b4c6f780041" - integrity sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - "@types/estree@*", "@types/estree@^1.0.0": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" @@ -4368,11 +4416,6 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/json5@^0.0.29": - version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" - integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== - "@types/karma@^6.3.3": version "6.3.8" resolved "https://registry.yarnpkg.com/@types/karma/-/karma-6.3.8.tgz#06cb3ecddabece81eb43a1087a8f01adecf64645" @@ -4479,6 +4522,11 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== +"@types/rbush@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/rbush/-/rbush-4.0.0.tgz#b327bf54952e9c924ea6702c36904c2ce1d47f35" + integrity sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ== + "@types/react-dom@^17.0.20": version "17.0.25" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.25.tgz#e0e5b3571e1069625b3a3da2b279379aa33a0cb5" @@ -4616,74 +4664,11 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.1.0.tgz#3c020deeaaba82a6f741d00dacf172c53be4911f" - integrity sha512-LlNBaHFCEBPHyD4pZXb35mzjGkuGKXU5eeCA1SxvHfiRES0E82dOounfVpL4DCqYvJEKab0bZIA0gCRpdLKkCw== - dependencies: - "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.1.0" - "@typescript-eslint/type-utils" "8.1.0" - "@typescript-eslint/utils" "8.1.0" - "@typescript-eslint/visitor-keys" "8.1.0" - graphemer "^1.4.0" - ignore "^5.3.1" - natural-compare "^1.4.0" - ts-api-utils "^1.3.0" - -"@typescript-eslint/parser@^8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.1.0.tgz#b7e77f5fa212df59eba51ecd4986f194bccc2303" - integrity sha512-U7iTAtGgJk6DPX9wIWPPOlt1gO57097G06gIcl0N0EEnNw8RGD62c+2/DiP/zL7KrkqnnqF7gtFGR7YgzPllTA== - dependencies: - "@typescript-eslint/scope-manager" "8.1.0" - "@typescript-eslint/types" "8.1.0" - "@typescript-eslint/typescript-estree" "8.1.0" - "@typescript-eslint/visitor-keys" "8.1.0" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.1.0.tgz#dd8987d2efebb71d230a1c71d82e84a7aead5c3d" - integrity sha512-DsuOZQji687sQUjm4N6c9xABJa7fjvfIdjqpSIIVOgaENf2jFXiM9hIBZOL3hb6DHK9Nvd2d7zZnoMLf9e0OtQ== - dependencies: - "@typescript-eslint/types" "8.1.0" - "@typescript-eslint/visitor-keys" "8.1.0" - -"@typescript-eslint/type-utils@8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.1.0.tgz#dbf5a4308166dfc37a36305390dea04a3a3b5048" - integrity sha512-oLYvTxljVvsMnldfl6jIKxTaU7ok7km0KDrwOt1RHYu6nxlhN3TIx8k5Q52L6wR33nOwDgM7VwW1fT1qMNfFIA== - dependencies: - "@typescript-eslint/typescript-estree" "8.1.0" - "@typescript-eslint/utils" "8.1.0" - debug "^4.3.4" - ts-api-utils "^1.3.0" - "@typescript-eslint/types@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/types@8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.1.0.tgz#fbf1eaa668a7e444ac507732ca9d3c3468e5db9c" - integrity sha512-q2/Bxa0gMOu/2/AKALI0tCKbG2zppccnRIRCW6BaaTlRVaPKft4oVYPp7WOPpcnsgbr0qROAVCVKCvIQ0tbWog== - -"@typescript-eslint/typescript-estree@8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.1.0.tgz#c44e5667683c0bb5caa43192e27de6a994f4e4c4" - integrity sha512-NTHhmufocEkMiAord/g++gWKb0Fr34e9AExBRdqgWdVBaKoei2dIyYKD9Q0jBnvfbEA5zaf8plUFMUH6kQ0vGg== - dependencies: - "@typescript-eslint/types" "8.1.0" - "@typescript-eslint/visitor-keys" "8.1.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^1.3.0" - "@typescript-eslint/typescript-estree@^5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" @@ -4697,16 +4682,6 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.1.0.tgz#a922985a43d2560ce0d293be79148fa80c1325e0" - integrity sha512-ypRueFNKTIFwqPeJBfeIpxZ895PQhNyH4YID6js0UoBImWYoSjBsahUn9KMiJXh94uOjVBgHD9AmkyPsPnFwJA== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.1.0" - "@typescript-eslint/types" "8.1.0" - "@typescript-eslint/typescript-estree" "8.1.0" - "@typescript-eslint/visitor-keys@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" @@ -4715,15 +4690,7 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.1.0.tgz#ab2b3a9699a8ddebf0c205e133f114c1fed9daad" - integrity sha512-ba0lNI19awqZ5ZNKh6wCModMwoZs457StTebQ0q1NP58zSi2F6MOZRXwfKZy+jB78JNJ/WH8GSh2IQNzXX8Nag== - dependencies: - "@typescript-eslint/types" "8.1.0" - eslint-visitor-keys "^3.4.3" - -"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0": +"@ungap/structured-clone@^1.0.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== @@ -5075,7 +5042,7 @@ acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1: dependencies: acorn "^8.11.0" -acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.11.3, acorn@^8.4.1, acorn@^8.6.0, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.11.3, acorn@^8.4.1, acorn@^8.6.0, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2: version "8.12.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== @@ -5161,7 +5128,7 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5, ajv@~6.12.6: +ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -5390,21 +5357,6 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@^5.1.3: - version "5.3.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" - integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== - dependencies: - dequal "^2.0.3" - -array-buffer-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" - integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== - dependencies: - call-bind "^1.0.5" - is-array-buffer "^3.0.4" - array-differ@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" @@ -5420,18 +5372,6 @@ array-ify@^1.0.0: resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng== -array-includes@^3.1.6, array-includes@^3.1.7: - version "3.1.8" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" - integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - get-intrinsic "^1.2.4" - is-string "^1.0.7" - array-timsort@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/array-timsort/-/array-timsort-1.0.3.tgz#3c9e4199e54fb2b9c3fe5976396a21614ef0d926" @@ -5454,52 +5394,6 @@ array-uniq@^1.0.1: resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== -array.prototype.findlastindex@^1.2.3: - version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" - integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - es-shim-unscopables "^1.0.2" - -array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" - integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" - -array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" - integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" - -arraybuffer.prototype.slice@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" - integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== - dependencies: - array-buffer-byte-length "^1.0.1" - call-bind "^1.0.5" - define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.2.1" - get-intrinsic "^1.2.3" - is-array-buffer "^3.0.4" - is-shared-array-buffer "^1.0.2" - arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -5542,11 +5436,6 @@ ast-module-types@^5.0.0: resolved "https://registry.yarnpkg.com/ast-module-types/-/ast-module-types-5.0.0.tgz#32b2b05c56067ff38e95df66f11d6afd6c9ba16b" integrity sha512-JvqziE0Wc0rXQfma0HZC/aY7URXHFuZV84fJRtP8u+lhp0JYCNd5wJzVXP45t0PH0Mej3ynlzvdyITYIu0G4LQ== -ast-types-flow@^0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" - integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== - astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -5589,13 +5478,6 @@ autoprefixer@^10.4.14: picocolors "^1.0.1" postcss-value-parser "^4.2.0" -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== - dependencies: - possible-typed-array-names "^1.0.0" - avvio@^8.3.0: version "8.4.0" resolved "https://registry.yarnpkg.com/avvio/-/avvio-8.4.0.tgz#7cbd5bca74f0c9effa944ced601f94ffd8afc5ed" @@ -5614,11 +5496,6 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.1.tgz#bb5f8b8a20739f6ae1caeaf7eea2c7913df8048e" integrity sha512-u5w79Rd7SU4JaIlA/zFqG+gOiuq25q5VLyZ8E+ijJeILuTxVzZgp2CaGw/UTw6pXYN9XMO9yiqj/nEHmhTG5CA== -axe-core@^4.6.2: - version "4.10.0" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.0.tgz#d9e56ab0147278272739a000880196cdfe113b59" - integrity sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g== - axios@^1.4.0, axios@^1.6.2, axios@^1.7.4: version "1.8.2" resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979" @@ -5628,11 +5505,6 @@ axios@^1.4.0, axios@^1.6.2, axios@^1.7.4: form-data "^4.0.0" proxy-from-env "^1.1.0" -axobject-query@^3.1.1: - version "3.2.4" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.4.tgz#6dfba930294ea14d7d2fc68b9d007211baedb94c" - integrity sha512-aPTElBrbifBU1krmZxGZOlBkslORe7Ll7+BDnI50Wy4LgOt69luMgevkDfTq1O/ZgprooPCtWpjCwKSZw/iZ4A== - b4a@^1.6.4: version "1.6.6" resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.6.tgz#a4cc349a3851987c3c4ac2d7785c18744f6da9ba" @@ -6207,7 +6079,7 @@ caching-transform@^4.0.0: package-hash "^4.0.0" write-file-atomic "^3.0.0" -call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: +call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== @@ -6300,6 +6172,11 @@ caniuse-lite@^1.0.30001700: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001701.tgz#ad9c90301f7153cf6b3314d16cc30757285bf9e7" integrity sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw== +caniuse-lite@^1.0.30001734: + version "1.0.30001737" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz#8292bb7591932ff09e9a765f12fdf5629a241ccc" + integrity sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw== + canvas@2.11.2, canvas@3.1.0, canvas@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/canvas/-/canvas-3.1.0.tgz#6cdf094b859fef8e39b0e2c386728a376f1727b2" @@ -7247,7 +7124,7 @@ cross-fetch@3.1.5: dependencies: node-fetch "2.6.7" -cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -7356,11 +7233,6 @@ css-what@^6.0.1, css-what@^6.1.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== -csscolorparser@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" - integrity sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w== - cssdb@^7.6.0: version "7.11.2" resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.11.2.tgz#127a2f5b946ee653361a5af5333ea85a39df5ae5" @@ -7518,11 +7390,6 @@ d3-scale@4.0.2: dependencies: d3-array "2 - 3" -damerau-levenshtein@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" - integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== - dargs@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" @@ -7549,33 +7416,6 @@ data-urls@^3.0.2: whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" -data-view-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" - integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - is-data-view "^1.0.1" - -data-view-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" - integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - is-data-view "^1.0.1" - -data-view-byte-offset@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" - integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - is-data-view "^1.0.1" - date-format@^4.0.14: version "4.0.14" resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" @@ -7593,23 +7433,23 @@ dateformat@^3.0.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -dcmjs@^0.29.8: - version "0.29.13" - resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.29.13.tgz#a74df541bf3948366b25630dbe1e8376f108ea3b" - integrity sha512-Bf9tKzJNWqk4kbV210N5TLEHDqaZvO3S+MH9vezFAU8WKcG4cR6z4/II3TQVqhLI185eNUL+lhfPCVH1Uu2yTA== +dcmjs@^0.41.0: + version "0.41.0" + resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.41.0.tgz#4804099980ab769f8902a948ad330abdf3b577d2" + integrity sha512-kr46REomItFeWz+0ck4Wif4uS5VVDWVlwdh5GGaCtTYHWfNQmrcCSiQOkrShc7Dc5zP8vNKrHEdORlZXenlg3w== dependencies: "@babel/runtime-corejs3" "^7.22.5" adm-zip "^0.5.10" gl-matrix "^3.1.0" lodash.clonedeep "^4.5.0" - loglevelnext "^3.0.1" + loglevel "^1.8.1" ndarray "^1.0.19" pako "^2.0.4" -dcmjs@^0.42.0: - version "0.42.0" - resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.42.0.tgz#40a41330ccb00bd2a37f4ed4552874c45975489a" - integrity sha512-N/SXIzMvAjDRgb8vC8G7DP/2BI1QKpa3TwY47Ok34g5y6ZKIcpAR3sf+Y+OBWkQxvnCTncB6B4tkq/ih8xjmXA== +dcmjs@^0.43.1: + version "0.43.1" + resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.43.1.tgz#03223e56d52e9ec2c3b8c10afe9200d754befe62" + integrity sha512-7IAB2cs2F8QYINbfhA7PV+IDraqBuHn8N31xJc6HsyNxZXyGUPQRujcq732ACqjLhl7oJi1z8PJJgLdblQtD3Q== dependencies: "@babel/runtime-corejs3" "^7.22.5" adm-zip "^0.5.10" @@ -7631,7 +7471,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.6, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: +debug@4, debug@4.3.6, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.3.5, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: version "4.3.6" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== @@ -7645,13 +7485,6 @@ debug@4.3.4: dependencies: ms "2.1.2" -debug@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - decache@4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/decache/-/decache-4.6.2.tgz#c1df1325a2f36d53922e08f33380f083148199cd" @@ -7767,11 +7600,6 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - deepmerge@^4.2.2: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" @@ -7853,7 +7681,7 @@ define-lazy-prop@^3.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== -define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: +define-properties@^1.1.3: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== @@ -7905,7 +7733,7 @@ deprecation@^2.0.0: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== -dequal@^2.0.0, dequal@^2.0.3: +dequal@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -8027,22 +7855,22 @@ di@^0.0.1: resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== -dicom-microscopy-viewer@^0.46.1: - version "0.46.1" - resolved "https://registry.yarnpkg.com/dicom-microscopy-viewer/-/dicom-microscopy-viewer-0.46.1.tgz#33405d8cbe0e5d51dfe515af9c9742d2efbb1a23" - integrity sha512-QLozX/iM6ZA0TxheHQnTNiLg+RbSVlxYKMNG9qdqV5oNbEDOf+z4/8mDqnAQ8wjlQXE2MbUDX8JRa0LmO9mDTg== +dicom-microscopy-viewer@^0.48.6: + version "0.48.7" + resolved "https://registry.yarnpkg.com/dicom-microscopy-viewer/-/dicom-microscopy-viewer-0.48.7.tgz#f40248f5e5bd0ac7c47235de3e079d5a96b6a9a9" + integrity sha512-Kmt6uQourv5HL29bnPWvy2hm49LL4fvYXhj2BTHtbCHdyLZ3LOWUxNWZR9NzuK7QoiHCA7ZPSFFnkdUfWLzF6g== dependencies: "@cornerstonejs/codec-charls" "^1.2.3" "@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2" - "@cornerstonejs/codec-openjpeg" "^1.2.2" + "@cornerstonejs/codec-openjpeg" "^1.2.4" "@cornerstonejs/codec-openjph" "^2.4.5" colormap "^2.3" - dcmjs "^0.29.8" + dcmjs "^0.41.0" dicomicc "^0.1" - dicomweb-client "^0.8.4" + dicomweb-client "^0.10.3" image-type "^4.1" mathjs "^11.2" - ol "^7.1" + ol "^10.6.0" uuid "^9.0" dicom-parser@^1.8.9: @@ -8055,10 +7883,15 @@ dicomicc@^0.1: resolved "https://registry.yarnpkg.com/dicomicc/-/dicomicc-0.1.0.tgz#c73acc60a8e2d73a20f462c8c7d0e1e0d977c486" integrity sha512-kZejPGjLQ9NsgovSyVsiAuCpq6LofNR9Erc8Tt/vQAYGYCoQnTyWDlg5D0TJJQATKul7cSr9k/q0TF8G9qdDkQ== -dicomweb-client@^0.8.4: - version "0.8.4" - resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.8.4.tgz#3da814cedb9415facb50bc5f43af8d961a991c74" - integrity sha512-/6oY3/Fg9JyAlbTWuJOYbVqici3+nlZt43+Z/Y47RNiqLc028JcxNlY28u4VQqksxfB59f1hhNbsqsHyDT4vhw== +dicomweb-client@^0.10.3: + version "0.10.4" + resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.10.4.tgz#f32f8659e51fbaf3f924c81fe8b7ad80297f2bc5" + integrity sha512-TEt26c0JI37IGmSqoj1k1/Y/ZIyq33/ysVaUwE0/Haosn2IBM55NEIPkT+AnhFss2nFAMVtKKWKWLox4luthVw== + +dicomweb-client@^0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.11.2.tgz#c68f319f24815b7cc9c0cbee25d6cc092f637c84" + integrity sha512-HsjQtmw4XY35ZyrhW5WY7P5ZgGIrY1M1jfLeq2IKANDIqfWoE4QW496VcsbuX4qnTNoeHBauei73vU0WwMYrYg== diff-sequences@^29.6.3: version "29.6.3" @@ -8094,20 +7927,6 @@ docdash@^1.2.0: resolved "https://registry.yarnpkg.com/docdash/-/docdash-1.2.0.tgz#f99dde5b8a89aa4ed083a3150383e042d06c7f49" integrity sha512-IYZbgYthPTspgqYeciRJNPhSwL51yer7HAwDXhF5p+H7mTDbPvY3PCk/QDjNxdPCpWkaJVFC4t7iCNB/t9E5Kw== -doctrine@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== - dependencies: - esutils "^2.0.2" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -8260,10 +8079,10 @@ duplexer@^0.1.1, duplexer@^0.1.2: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== -earcut@^2.2.3: - version "2.2.4" - resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a" - integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ== +earcut@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/earcut/-/earcut-3.0.2.tgz#d478a29aaf99acf418151493048aa197d0512248" + integrity sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ== eastasianwidth@^0.2.0: version "0.2.0" @@ -8377,7 +8196,7 @@ engine.io@~6.5.2: engine.io-parser "~5.2.1" ws "~8.17.1" -enhanced-resolve@^5.0.0, enhanced-resolve@^5.12.0, enhanced-resolve@^5.13.0: +enhanced-resolve@^5.0.0, enhanced-resolve@^5.13.0: version "5.17.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== @@ -8453,58 +8272,6 @@ error-stack-parser@^2.0.2, error-stack-parser@^2.0.3: dependencies: stackframe "^1.3.4" -es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2: - version "1.23.3" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" - integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== - dependencies: - array-buffer-byte-length "^1.0.1" - arraybuffer.prototype.slice "^1.0.3" - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - data-view-buffer "^1.0.1" - data-view-byte-length "^1.0.1" - data-view-byte-offset "^1.0.0" - es-define-property "^1.0.0" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - es-set-tostringtag "^2.0.3" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.6" - get-intrinsic "^1.2.4" - get-symbol-description "^1.0.2" - globalthis "^1.0.3" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - has-proto "^1.0.3" - has-symbols "^1.0.3" - hasown "^2.0.2" - internal-slot "^1.0.7" - is-array-buffer "^3.0.4" - is-callable "^1.2.7" - is-data-view "^1.0.1" - is-negative-zero "^2.0.3" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.3" - is-string "^1.0.7" - is-typed-array "^1.1.13" - is-weakref "^1.0.2" - object-inspect "^1.13.1" - object-keys "^1.1.1" - object.assign "^4.1.5" - regexp.prototype.flags "^1.5.2" - safe-array-concat "^1.1.2" - safe-regex-test "^1.0.3" - string.prototype.trim "^1.2.9" - string.prototype.trimend "^1.0.8" - string.prototype.trimstart "^1.0.8" - typed-array-buffer "^1.0.2" - typed-array-byte-length "^1.0.1" - typed-array-byte-offset "^1.0.2" - typed-array-length "^1.0.6" - unbox-primitive "^1.0.2" - which-typed-array "^1.1.15" - es-define-property@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" @@ -8512,7 +8279,7 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" -es-errors@^1.2.1, es-errors@^1.3.0: +es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== @@ -8522,38 +8289,6 @@ es-module-lexer@^1.0.0, es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== -es-object-atoms@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" - integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== - dependencies: - es-errors "^1.3.0" - -es-set-tostringtag@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" - integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== - dependencies: - get-intrinsic "^1.2.4" - has-tostringtag "^1.0.2" - hasown "^2.0.1" - -es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" - integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== - dependencies: - hasown "^2.0.0" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - es6-error@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" @@ -8673,100 +8408,6 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@^8.8.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" - integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== - -eslint-import-resolver-node@^0.3.9: - version "0.3.9" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" - integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== - dependencies: - debug "^3.2.7" - is-core-module "^2.13.0" - resolve "^1.22.4" - -eslint-import-resolver-typescript@^3.6.1: - version "3.6.1" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz#7b983680edd3f1c5bce1a5829ae0bc2d57fe9efa" - integrity sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg== - dependencies: - debug "^4.3.4" - enhanced-resolve "^5.12.0" - eslint-module-utils "^2.7.4" - fast-glob "^3.3.1" - get-tsconfig "^4.5.0" - is-core-module "^2.11.0" - is-glob "^4.0.3" - -eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34" - integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q== - dependencies: - debug "^3.2.7" - -eslint-plugin-import@^2.29.1: - version "2.29.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" - integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== - dependencies: - array-includes "^3.1.7" - array.prototype.findlastindex "^1.2.3" - array.prototype.flat "^1.3.2" - array.prototype.flatmap "^1.3.2" - debug "^3.2.7" - doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.9" - eslint-module-utils "^2.8.0" - hasown "^2.0.0" - is-core-module "^2.13.1" - is-glob "^4.0.3" - minimatch "^3.1.2" - object.fromentries "^2.0.7" - object.groupby "^1.0.1" - object.values "^1.1.7" - semver "^6.3.1" - tsconfig-paths "^3.15.0" - -eslint-plugin-jsx-a11y@6.7.1: - version "6.7.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz#fca5e02d115f48c9a597a6894d5bcec2f7a76976" - integrity sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA== - dependencies: - "@babel/runtime" "^7.20.7" - aria-query "^5.1.3" - array-includes "^3.1.6" - array.prototype.flatmap "^1.3.1" - ast-types-flow "^0.0.7" - axe-core "^4.6.2" - axobject-query "^3.1.1" - damerau-levenshtein "^1.0.8" - emoji-regex "^9.2.2" - has "^1.0.3" - jsx-ast-utils "^3.3.3" - language-tags "=1.0.5" - minimatch "^3.1.2" - object.entries "^1.1.6" - object.fromentries "^2.0.6" - semver "^6.3.0" - -eslint-plugin-prettier@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b" - integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== - dependencies: - prettier-linter-helpers "^1.0.0" - -eslint-plugin-tsdoc@^0.2.17: - version "0.2.17" - resolved "https://registry.yarnpkg.com/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.17.tgz#27789495bbd8778abbf92db1707fec2ed3dfe281" - integrity sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA== - dependencies: - "@microsoft/tsdoc" "0.14.2" - "@microsoft/tsdoc-config" "0.16.2" - eslint-scope@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -8775,100 +8416,16 @@ eslint-scope@5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-visitor-keys@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: +eslint-visitor-keys@^3.3.0: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint-webpack-plugin@^4.0.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/eslint-webpack-plugin/-/eslint-webpack-plugin-4.2.0.tgz#41f54b25379908eb9eca8645bc997c90cfdbd34e" - integrity sha512-rsfpFQ01AWQbqtjgPRr2usVRxhWDuG0YDYcG8DJOteD3EFnpeuYuOwk0PQiN7PRBTqS6ElNdtPZPggj8If9WnA== - dependencies: - "@types/eslint" "^8.56.10" - jest-worker "^29.7.0" - micromatch "^4.0.5" - normalize-path "^3.0.0" - schema-utils "^4.2.0" - -eslint@^8.39.0: - version "8.57.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" - integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.57.0" - "@humanwhocodes/config-array" "^0.11.14" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - "@ungap/structured-clone" "^1.2.0" - ajv "^6.12.4" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" - esquery "^1.4.2" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - graphemer "^1.4.0" - ignore "^5.2.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - -espree@^9.6.0, espree@^9.6.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" - integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== - dependencies: - acorn "^8.9.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" - esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.2: - version "1.6.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" - integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== - dependencies: - estraverse "^5.1.0" - esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" @@ -8881,7 +8438,7 @@ estraverse@^4.1.1: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== -estraverse@^5.1.0, estraverse@^5.2.0: +estraverse@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== @@ -9180,7 +8737,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-diff@^1.1.2, fast-diff@^1.2.0: +fast-diff@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== @@ -9224,11 +8781,6 @@ fast-json-stringify@^5.7.0, fast-json-stringify@^5.8.0: json-schema-ref-resolver "^1.0.1" rfdc "^1.2.0" -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - fast-querystring@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/fast-querystring/-/fast-querystring-1.1.2.tgz#a6d24937b4fc6f791b4ee31dcb6f53aeafb89f53" @@ -9385,13 +8937,6 @@ figures@^5.0.0: escape-string-regexp "^5.0.0" is-unicode-supported "^1.2.0" -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - file-entry-cache@^7.0.0: version "7.0.2" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-7.0.2.tgz#2d61bb70ba89b9548e3035b7c9173fe91deafff0" @@ -9588,7 +9133,7 @@ find-up@^6.0.0, find-up@^6.3.0: locate-path "^7.1.0" path-exists "^5.0.0" -flat-cache@^3.0.4, flat-cache@^3.2.0: +flat-cache@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== @@ -9637,13 +9182,6 @@ follow-redirects@^1.0.0, follow-redirects@^1.15.2, follow-redirects@^1.15.6: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - foreground-child@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53" @@ -9840,21 +9378,6 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function.prototype.name@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" - integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - functions-have-names "^1.2.3" - -functions-have-names@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" - integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== - fuzzy@0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/fuzzy/-/fuzzy-0.1.3.tgz#4c76ec2ff0ac1a36a9dccf9a00df8623078d4ed8" @@ -9880,7 +9403,7 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -geotiff@^2.0.7: +geotiff@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/geotiff/-/geotiff-2.1.3.tgz#993f40f2aa6aa65fb1e0451d86dd22ca8e66910c" integrity sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA== @@ -9912,7 +9435,7 @@ get-east-asian-width@^1.0.0: resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== @@ -9993,22 +9516,6 @@ get-stream@^8.0.1: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== -get-symbol-description@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" - integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== - dependencies: - call-bind "^1.0.5" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - -get-tsconfig@^4.5.0: - version "4.7.6" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.6.tgz#118fd5b7b9bae234cc7705a00cd771d7eb65d62a" - integrity sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA== - dependencies: - resolve-pkg-maps "^1.0.0" - getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -10094,7 +9601,7 @@ gl-matrix@3.4.3, gl-matrix@^3.1.0, gl-matrix@^3.4.3: resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.4.3.tgz#fc1191e8320009fd4d20e9339595c6041ddc22c9" integrity sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA== -glob-parent@6.0.2, glob-parent@^6.0.1, glob-parent@^6.0.2: +glob-parent@6.0.2, glob-parent@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== @@ -10194,13 +9701,6 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.19.0: - version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" - integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== - dependencies: - type-fest "^0.20.2" - globalthis@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" @@ -10208,14 +9708,6 @@ globalthis@1.0.3: dependencies: define-properties "^1.1.3" -globalthis@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" - integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== - dependencies: - define-properties "^1.2.1" - gopd "^1.0.1" - globby@11.1.0, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" @@ -10296,11 +9788,6 @@ graceful-fs@4.2.11, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.1 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - guid-typescript@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/guid-typescript/-/guid-typescript-1.0.9.tgz#e35f77003535b0297ea08548f5ace6adb1480ddc" @@ -10372,11 +9859,6 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== -has-bigints@^1.0.1, has-bigints@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -10399,33 +9881,21 @@ has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: dependencies: es-define-property "^1.0.0" -has-proto@^1.0.1, has-proto@^1.0.3: +has-proto@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== -has-symbols@^1.0.2, has-symbols@^1.0.3: +has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - has-unicode@2.0.1, has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== -has@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" - integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== - hasbin@1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/hasbin/-/hasbin-1.2.3.tgz#78c5926893c80215c2b568ae1fd3fcab7a2696b0" @@ -10441,7 +9911,7 @@ hasha@5.2.2, hasha@^5.0.0: is-stream "^2.0.0" type-fest "^0.8.0" -hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: +hasown@^2.0.0, hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== @@ -10734,10 +10204,10 @@ human-signals@^5.0.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== -husky@^9.1.4: - version "9.1.4" - resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.4.tgz#926fd19c18d345add5eab0a42b2b6d9a80259b34" - integrity sha512-bho94YyReb4JV7LYWRWxZ/xr6TtOTt8cMfmQ39MQYJ7f/YE268s3GdghGwi+y4zAeqewE5zYLvuhV0M0ijsDEA== +husky@^9.1.7: + version "9.1.7" + resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d" + integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== hyperdyperid@^1.2.0: version "1.2.0" @@ -10763,7 +10233,7 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -ieee754@^1.1.12, ieee754@^1.1.13, ieee754@^1.2.1: +ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -10775,7 +10245,7 @@ ignore-walk@^6.0.4: dependencies: minimatch "^9.0.0" -ignore@^5.0.4, ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1: +ignore@^5.0.4, ignore@^5.2.0, ignore@^5.2.4: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== @@ -10792,7 +10262,7 @@ image-type@^4.1: dependencies: file-type "^10.10.0" -import-fresh@^3.2.1, import-fresh@^3.3.0: +import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -10958,15 +10428,6 @@ interface-store@^6.0.0: resolved "https://registry.yarnpkg.com/interface-store/-/interface-store-6.0.2.tgz#1746a1ee07634f7678b3aa778738b79e3f75c909" integrity sha512-KSFCXtBlNoG0hzwNa0RmhHtrdhzexp+S+UY2s0rWTBJyfdEIgn6i6Zl9otVqrcFYbYrneBT7hbmHQ8gE0C3umA== -internal-slot@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" - integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== - dependencies: - es-errors "^1.3.0" - hasown "^2.0.0" - side-channel "^1.0.4" - "internmap@1 - 2": version "2.0.3" resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" @@ -11079,14 +10540,6 @@ iron-webcrypto@^1.1.1: resolved "https://registry.yarnpkg.com/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz#aa60ff2aa10550630f4c0b11fd2442becdb35a6f" integrity sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg== -is-array-buffer@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" - integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -11097,13 +10550,6 @@ is-arrayish@^0.3.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -11111,14 +10557,6 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-buffer@^1.0.2: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -11131,11 +10569,6 @@ is-builtin-module@^3.1.0, is-builtin-module@^3.2.1: dependencies: builtin-modules "^3.3.0" -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - is-ci@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" @@ -11143,27 +10576,13 @@ is-ci@3.0.1: dependencies: ci-info "^3.2.0" -is-core-module@^2.1.0, is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.13.1, is-core-module@^2.5.0, is-core-module@^2.8.1: +is-core-module@^2.13.0, is-core-module@^2.5.0, is-core-module@^2.8.1: version "2.15.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== dependencies: hasown "^2.0.2" -is-data-view@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" - integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== - dependencies: - is-typed-array "^1.1.13" - -is-date-object@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - is-docker@3.0.0, is-docker@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" @@ -11206,7 +10625,7 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -11258,11 +10677,6 @@ is-natural-number@^4.0.1: resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" integrity sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ== -is-negative-zero@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" - integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== - is-network-error@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.1.0.tgz#d26a760e3770226d11c169052f266a4803d9c997" @@ -11273,13 +10687,6 @@ is-npm@^6.0.0: resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-6.0.0.tgz#b59e75e8915543ca5d881ecff864077cba095261" integrity sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ== -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== - dependencies: - has-tostringtag "^1.0.0" - is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -11309,7 +10716,7 @@ is-path-inside@^2.1.0: dependencies: path-is-inside "^1.0.2" -is-path-inside@^3.0.2, is-path-inside@^3.0.3: +is-path-inside@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== @@ -11356,21 +10763,6 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" - integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== - dependencies: - call-bind "^1.0.7" - is-ssh@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.4.0.tgz#4f8220601d2839d8fa624b3106f8e8884f01b8b2" @@ -11403,20 +10795,6 @@ is-stream@^3.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - is-text-path@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" @@ -11424,13 +10802,6 @@ is-text-path@^1.0.1: dependencies: text-extensions "^1.0.0" -is-typed-array@^1.1.13: - version "1.1.13" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== - dependencies: - which-typed-array "^1.1.14" - is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -11461,13 +10832,6 @@ is-url@^1.2.4: resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== - dependencies: - call-bind "^1.0.2" - is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -11494,11 +10858,6 @@ is64bit@^2.0.0: dependencies: system-architecture "^0.1.0" -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -12350,27 +11709,17 @@ json-schema@0.4.0: resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== - json-stringify-nice@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz#2c937962b80181d3f317dd39aa323e14f5a60a67" integrity sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw== -json-stringify-pretty-compact@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz#e77c419f52ff00c45a31f07f4c820c2433143885" - integrity sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ== - json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^1.0.1, json5@^1.0.2: +json5@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== @@ -12444,16 +11793,6 @@ jsprim@^1.2.2: json-schema "0.4.0" verror "1.10.0" -jsx-ast-utils@^3.3.3: - version "3.3.5" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" - integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== - dependencies: - array-includes "^3.1.6" - array.prototype.flat "^1.3.1" - object.assign "^4.1.4" - object.values "^1.1.6" - junk@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/junk/-/junk-4.0.1.tgz#7ee31f876388c05177fe36529ee714b07b50fbed" @@ -12633,18 +11972,6 @@ lambda-local@2.2.0: dotenv "^16.3.1" winston "^3.10.0" -language-subtag-registry@~0.3.2: - version "0.3.23" - resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" - integrity sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== - -language-tags@=1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" - integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ== - dependencies: - language-subtag-registry "~0.3.2" - latest-version@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-7.0.0.tgz#843201591ea81a4d404932eeb61240fe04e9e5da" @@ -12782,14 +12109,6 @@ leven@^3.1.0, "leven@^3.1.0 < 4": resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - libnpmaccess@8.0.6: version "8.0.6" resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-8.0.6.tgz#73be4c236258babc0a0bca6d3b6a93a6adf937cf" @@ -13066,11 +12385,6 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" @@ -13191,11 +12505,6 @@ loglevel@^1.8.1, loglevel@^1.9.2: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.2.tgz#c2e028d6c757720107df4e64508530db6621ba08" integrity sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg== -loglevelnext@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-3.0.1.tgz#e3e4659c4061c09264f6812c33586dc55a009a04" - integrity sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA== - long@^5.0.0, long@^5.2.3: version "5.2.3" resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" @@ -13331,11 +12640,6 @@ map-obj@^5.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-5.0.2.tgz#174ad9f7e5e4e777a219126d9a734ff3e14a1c68" integrity sha512-K6K2NgKnTXimT3779/4KxSvobxOtMmx1LBZ3NwRxT/MDIR3Br/fQ4Q+WCX5QxjyUR8zg5+RV9Tbf2c5pAWTD2A== -mapbox-to-css-font@^2.4.1: - version "2.4.5" - resolved "https://registry.yarnpkg.com/mapbox-to-css-font/-/mapbox-to-css-font-2.4.5.tgz#b10a7a33af3e1a9a1369e4d5e8285492a7943c46" - integrity sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w== - markdown-it-anchor@^8.4.1: version "8.6.7" resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz#ee6926daf3ad1ed5e4e3968b1740eef1c6399634" @@ -13714,7 +13018,7 @@ minimatch@9.0.3: dependencies: brace-expansion "^2.0.1" -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -14656,53 +13960,6 @@ object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.4, object.assign@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" - integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== - dependencies: - call-bind "^1.0.5" - define-properties "^1.2.1" - has-symbols "^1.0.3" - object-keys "^1.1.1" - -object.entries@^1.1.6: - version "1.1.8" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" - integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -object.fromentries@^2.0.6, object.fromentries@^2.0.7: - version "2.0.8" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" - integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - -object.groupby@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" - integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - -object.values@^1.1.6, object.values@^1.1.7: - version "1.2.0" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" - integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" @@ -14722,25 +13979,16 @@ ohash@^1.1.3: resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.3.tgz#f12c3c50bfe7271ce3fd1097d42568122ccdcf07" integrity sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw== -ol-mapbox-style@^10.1.0: - version "10.7.0" - resolved "https://registry.yarnpkg.com/ol-mapbox-style/-/ol-mapbox-style-10.7.0.tgz#8837912da2a16fbd22992d76cbc4f491c838b973" - integrity sha512-S/UdYBuOjrotcR95Iq9AejGYbifKeZE85D9VtH11ryJLQPTZXZSW1J5bIXcr4AlAH6tyjPPHTK34AdkwB32Myw== - dependencies: - "@mapbox/mapbox-gl-style-spec" "^13.23.1" - mapbox-to-css-font "^2.4.1" - ol "^7.3.0" - -ol@^7.1, ol@^7.3.0: - version "7.5.2" - resolved "https://registry.yarnpkg.com/ol/-/ol-7.5.2.tgz#2e40a16b45331dbee86ca86876fcc7846be0dbb7" - integrity sha512-HJbb3CxXrksM6ct367LsP3N+uh+iBBMdP3DeGGipdV9YAYTP0vTJzqGnoqQ6C2IW4qf8krw9yuyQbc9fjOIaOQ== +ol@^10.6.0: + version "10.6.1" + resolved "https://registry.yarnpkg.com/ol/-/ol-10.6.1.tgz#950f3914b4eec978f087b36aa74ce1e18c41ab09" + integrity sha512-xp174YOwPeLj7c7/8TCIEHQ4d41tgTDDhdv6SqNdySsql5/MaFJEJkjlsYcvOPt7xA6vrum/QG4UdJ0iCGT1cg== dependencies: - earcut "^2.2.3" - geotiff "^2.0.7" - ol-mapbox-style "^10.1.0" - pbf "3.2.1" - rbush "^3.0.1" + "@types/rbush" "4.0.0" + earcut "^3.0.0" + geotiff "^2.1.3" + pbf "4.0.1" + rbush "^4.0.0" omit.js@^2.0.2: version "2.0.2" @@ -14882,18 +14130,6 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -optionator@^0.9.3: - version "0.9.4" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" - integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== - dependencies: - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.5" - ora@5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/ora/-/ora-5.3.0.tgz#fb832899d3a1372fe71c8b2c534bbfe74961bb6f" @@ -14951,6 +14187,43 @@ os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== +oxc-parser@0.74.0: + version "0.74.0" + resolved "https://registry.yarnpkg.com/oxc-parser/-/oxc-parser-0.74.0.tgz#309e406adb3d2e6ceae4695c4255d379ec70229d" + integrity sha512-2tDN/ttU8WE6oFh8EzKNam7KE7ZXSG5uXmvX85iNzxdJfMssDWcj3gpYzZi1E04XuE7m3v1dVWl/8BE886vPGw== + dependencies: + "@oxc-project/types" "^0.74.0" + optionalDependencies: + "@oxc-parser/binding-android-arm64" "0.74.0" + "@oxc-parser/binding-darwin-arm64" "0.74.0" + "@oxc-parser/binding-darwin-x64" "0.74.0" + "@oxc-parser/binding-freebsd-x64" "0.74.0" + "@oxc-parser/binding-linux-arm-gnueabihf" "0.74.0" + "@oxc-parser/binding-linux-arm-musleabihf" "0.74.0" + "@oxc-parser/binding-linux-arm64-gnu" "0.74.0" + "@oxc-parser/binding-linux-arm64-musl" "0.74.0" + "@oxc-parser/binding-linux-riscv64-gnu" "0.74.0" + "@oxc-parser/binding-linux-s390x-gnu" "0.74.0" + "@oxc-parser/binding-linux-x64-gnu" "0.74.0" + "@oxc-parser/binding-linux-x64-musl" "0.74.0" + "@oxc-parser/binding-wasm32-wasi" "0.74.0" + "@oxc-parser/binding-win32-arm64-msvc" "0.74.0" + "@oxc-parser/binding-win32-x64-msvc" "0.74.0" + +oxlint@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/oxlint/-/oxlint-1.9.0.tgz#688504678127c546e14ac84407bace079b8f02a7" + integrity sha512-vvqpkRt7UA1F1DoOJGYO2+T52L6jV4RxS+9hFVCPQ0X+hcNVtbl/TxQuwjobBw8Yw2U8Rb9d8VLJxa8eRSqpQQ== + optionalDependencies: + "@oxlint/darwin-arm64" "1.9.0" + "@oxlint/darwin-x64" "1.9.0" + "@oxlint/linux-arm64-gnu" "1.9.0" + "@oxlint/linux-arm64-musl" "1.9.0" + "@oxlint/linux-x64-gnu" "1.9.0" + "@oxlint/linux-x64-musl" "1.9.0" + "@oxlint/win32-arm64" "1.9.0" + "@oxlint/win32-x64" "1.9.0" + p-cancelable@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" @@ -15419,7 +14692,7 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6, path-parse@^1.0.7: +path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== @@ -15474,12 +14747,11 @@ pathval@^2.0.0: resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== -pbf@3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.2.1.tgz#b4c1b9e72af966cd82c6531691115cc0409ffe2a" - integrity sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ== +pbf@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pbf/-/pbf-4.0.1.tgz#ad9015e022b235dcdbe05fc468a9acadf483f0d4" + integrity sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA== dependencies: - ieee754 "^1.1.12" resolve-protobuf-schema "^2.1.0" peek-readable@^5.1.3: @@ -15626,11 +14898,6 @@ playwright@1.51.1: optionalDependencies: fsevents "2.3.2" -possible-typed-array-names@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" - integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== - postcss-attribute-case-insensitive@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-6.0.3.tgz#d118023911a768dfccfc0b0147f5ff06d8485806" @@ -16204,22 +15471,10 @@ precond@0.2: resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac" integrity sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ== -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -prettier-linter-helpers@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" - integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== - dependencies: - fast-diff "^1.1.2" - -prettier@^2.8.8: - version "2.8.8" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +prettier@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393" + integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== pretty-error@^4.0.0: version "4.0.0" @@ -16537,10 +15792,10 @@ quick-lru@^6.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-6.1.2.tgz#e9a90524108629be35287d0b864e7ad6ceb3659e" integrity sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ== -quickselect@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" - integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== +quickselect@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-3.0.0.tgz#a37fc953867d56f095a20ac71c6d27063d2de603" + integrity sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g== quote-unquote@^1.0.0: version "1.0.0" @@ -16584,12 +15839,12 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" -rbush@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf" - integrity sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w== +rbush@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/rbush/-/rbush-4.0.1.tgz#1f55afa64a978f71bf9e9a99bc14ff84f3cb0d6d" + integrity sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ== dependencies: - quickselect "^2.0.0" + quickselect "^3.0.0" rc@1.2.8, rc@^1.2.7: version "1.2.8" @@ -16850,16 +16105,6 @@ regex@^4.3.2: resolved "https://registry.yarnpkg.com/regex/-/regex-4.3.3.tgz#8cda73ccbdfa7c5691881d02f9bb142dba9daa6a" integrity sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg== -regexp.prototype.flags@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" - integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== - dependencies: - call-bind "^1.0.6" - define-properties "^1.2.1" - es-errors "^1.3.0" - set-function-name "^2.0.1" - regexpu-core@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" @@ -17013,11 +16258,6 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve-pkg-maps@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" - integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== - resolve-protobuf-schema@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758" @@ -17030,7 +16270,7 @@ resolve.exports@^2.0.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.22.4, resolve@~1.22.1, resolve@~1.22.2: +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.1, resolve@~1.22.1, resolve@~1.22.2: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -17048,14 +16288,6 @@ resolve@^2.0.0-next.1: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@~1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" - integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== - dependencies: - is-core-module "^2.1.0" - path-parse "^1.0.6" - responselike@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/responselike/-/responselike-3.0.0.tgz#20decb6c298aff0dbee1c355ca95461d42823626" @@ -17179,11 +16411,6 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rw@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" - integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== - rxjs@^6.4.0, rxjs@^6.6.2: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" @@ -17205,16 +16432,6 @@ sade@^1.8.1: dependencies: mri "^1.1.0" -safe-array-concat@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" - integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== - dependencies: - call-bind "^1.0.7" - get-intrinsic "^1.2.4" - has-symbols "^1.0.3" - isarray "^2.0.5" - safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -17230,15 +16447,6 @@ safe-json-stringify@^1.2.0: resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== -safe-regex-test@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" - integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - is-regex "^1.1.4" - safe-regex2@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-3.1.0.tgz#fd7ec23908e2c730e1ce7359a5b72883a87d2763" @@ -17338,7 +16546,7 @@ semver-diff@^4.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@7.6.3, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0: +semver@7.6.3, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: version "7.6.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== @@ -17455,16 +16663,6 @@ set-function-length@^1.2.1: gopd "^1.0.1" has-property-descriptors "^1.0.2" -set-function-name@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" - integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - functions-have-names "^1.2.3" - has-property-descriptors "^1.0.2" - setimmediate@~1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -17744,16 +16942,6 @@ sonic-boom@^4.0.1: dependencies: atomic-sleep "^1.0.0" -sort-asc@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/sort-asc/-/sort-asc-0.1.0.tgz#ab799df61fc73ea0956c79c4b531ed1e9e7727e9" - integrity sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw== - -sort-desc@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/sort-desc/-/sort-desc-0.1.1.tgz#198b8c0cdeb095c463341861e3925d4ee359a9ee" - integrity sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw== - sort-keys-length@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188" @@ -17775,14 +16963,6 @@ sort-keys@^2.0.0: dependencies: is-plain-obj "^1.0.0" -sort-object@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/sort-object/-/sort-object-0.3.2.tgz#98e0d199ede40e07c61a84403c61d6c3b290f9e2" - integrity sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA== - dependencies: - sort-asc "^0.1.0" - sort-desc "^0.1.1" - source-map-js@^1.0.1, source-map-js@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" @@ -18077,34 +17257,6 @@ string-width@^7.0.0: get-east-asian-width "^1.0.0" strip-ansi "^7.1.0" -string.prototype.trim@^1.2.9: - version "1.2.9" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" - integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.0" - es-object-atoms "^1.0.0" - -string.prototype.trimend@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" - integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -string.prototype.trimstart@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" - integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -18566,11 +17718,6 @@ text-hex@1.0.x: resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - thingies@^1.20.0: version "1.21.0" resolved "https://registry.yarnpkg.com/thingies/-/thingies-1.21.0.tgz#e80fbe58fd6fdaaab8fad9b67bd0a5c943c445c1" @@ -18788,11 +17935,6 @@ triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== -ts-api-utils@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" - integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== - ts-loader@9.5.1: version "9.5.1" resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.5.1.tgz#63d5912a86312f1fbe32cef0859fb8b2193d9b89" @@ -18823,16 +17965,6 @@ ts-node@^10.9.1, ts-node@^10.9.2: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tsconfig-paths@^3.15.0: - version "3.15.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" - integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== - dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.2" - minimist "^1.2.6" - strip-bom "^3.0.0" - tsconfig-paths@^4.1.2: version "4.2.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" @@ -18885,13 +18017,6 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -18907,11 +18032,6 @@ type-fest@^0.18.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" @@ -18960,50 +18080,6 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typed-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" - integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - is-typed-array "^1.1.13" - -typed-array-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" - integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== - dependencies: - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" - -typed-array-byte-offset@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" - integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" - -typed-array-length@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" - integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== - dependencies: - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" - possible-typed-array-names "^1.0.0" - typed-function@^4.1.1: version "4.2.1" resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-4.2.1.tgz#19aa51847aa2dea9ef5e7fb7641c060179a74426" @@ -19113,16 +18189,6 @@ ulid@2.3.0: resolved "https://registry.yarnpkg.com/ulid/-/ulid-2.3.0.tgz#93063522771a9774121a84d126ecd3eb9804071f" integrity sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw== -unbox-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" - integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== - dependencies: - call-bind "^1.0.2" - has-bigints "^1.0.2" - has-symbols "^1.0.3" - which-boxed-primitive "^1.0.2" - unbzip2-stream@1.4.3, unbzip2-stream@^1.0.9, unbzip2-stream@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" @@ -19860,33 +18926,11 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - which-module@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== -which-typed-array@^1.1.14, which-typed-array@^1.1.15: - version "1.1.15" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.2" - which@^1.2.1, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -19960,11 +19004,6 @@ winston@^3.10.0: triple-beam "^1.3.0" winston-transport "^4.7.0" -word-wrap@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" - integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== - wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"