From cfe5249440f303e5040dfa60e52519ca2d2bdb29 Mon Sep 17 00:00:00 2001 From: Karim Date: Wed, 1 Jul 2026 20:33:38 +0200 Subject: [PATCH] Add parametric walls unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 50 Vitest-Tests für die parametrische Wand-Engine (src/model/parametricWalls.ts): GridRule, ModuleRule, ConditionalThicknessRule, ReferenceLineRule, SequenceRule, deduplicateWalls, matchesCondition/matchesTarget, Integration via resolveParametricWall. Vitest als devDependency + Testskript in package.json + vitest.config.ts ergänzt. --- package-lock.json | 1591 ++++++++++++++++++++++++++++- package.json | 12 +- src/model/parametricWalls.test.ts | 1109 ++++++++++++++++++++ vitest.config.ts | 8 + 4 files changed, 2716 insertions(+), 4 deletions(-) create mode 100644 src/model/parametricWalls.test.ts create mode 100644 vitest.config.ts diff --git a/package-lock.json b/package-lock.json index f82445f..9c94069 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,13 @@ "@mlightcad/libredwg-web": "^0.7.7", "delaunator": "^5.1.0", "dxf-parser": "^1.1.2", + "jspdf": "^4.2.1", + "jszip": "^3.10.1", + "opencascade.js": "^1.1.1", + "polygon-clipping": "^0.15.7", "react": "^18.3.1", "react-dom": "^18.3.1", + "svg2pdf.js": "^2.7.0", "three": "^0.169.0" }, "devDependencies": { @@ -23,7 +28,8 @@ "playwright": "^1.61.1", "puppeteer": "^25.2.1", "typescript": "^5.6.2", - "vite": "^5.4.8" + "vite": "^5.4.8", + "vitest": "^4.1.9" } }, "node_modules/@babel/code-frame": { @@ -260,6 +266,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -308,6 +323,40 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -758,6 +807,35 @@ "node": ">=20" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@puppeteer/browsers": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.5.tgz", @@ -783,6 +861,281 @@ } } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1179,6 +1532,13 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@tweenjs/tween.js": { "version": "23.1.3", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", @@ -1186,6 +1546,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1231,6 +1602,24 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1238,6 +1627,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -1245,6 +1640,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "18.3.31", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", @@ -1288,6 +1690,13 @@ "meshoptimizer": "~0.18.1" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/webxr": { "version": "0.5.24", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", @@ -1316,6 +1725,92 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webgpu/types": { "version": "0.1.71", "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.71.tgz", @@ -1349,6 +1844,26 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.40", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", @@ -1417,6 +1932,36 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chromium-bidi": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz", @@ -1456,6 +2001,46 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1490,6 +2075,16 @@ "robust-predicates": "^3.0.2" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devtools-protocol": { "version": "0.0.1638949", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1638949.tgz", @@ -1497,6 +2092,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/dompurify": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dxf-parser": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/dxf-parser/-/dxf-parser-1.1.2.tgz", @@ -1520,6 +2125,13 @@ "dev": true, "license": "MIT" }, + "node_modules/es-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz", + "integrity": "sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -1569,11 +2181,65 @@ "node": ">=6" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fflate": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "dev": true, + "license": "MIT" + }, + "node_modules/font-family-papandreou": { + "version": "0.2.0-patch2", + "resolved": "https://registry.npmjs.org/font-family-papandreou/-/font-family-papandreou-0.2.0-patch2.tgz", + "integrity": "sha512-l/YiRdBSH/eWv6OF3sLGkwErL+n0MqCICi9mppTZBOCL5vixWGDqCYvRcuxB2h7RGCTzaTKOHT2caHvCXQPRlw==", "license": "MIT" }, "node_modules/fsevents": { @@ -1624,6 +2290,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1656,6 +2360,323 @@ "node": ">=6" } }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -1704,6 +2725,16 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/meshoptimizer": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", @@ -1764,6 +2795,56 @@ "node": ">=18" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/opencascade.js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/opencascade.js/-/opencascade.js-1.1.1.tgz", + "integrity": "sha512-lw6/vOl86+CkJ8d3V01mlbGAC0A49gc1HbwGcqGeKjk5SGRLiF15jyUuA8aYEvizcPNTu4Ta4A+Ut2DJgsa7AQ==", + "license": "LGPL-2.1-only" + }, + "node_modules/pako": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", + "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1771,6 +2852,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/playwright": { "version": "1.61.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", @@ -1818,6 +2912,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/polygon-clipping": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/polygon-clipping/-/polygon-clipping-0.15.7.tgz", + "integrity": "sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA==", + "license": "MIT", + "dependencies": { + "robust-predicates": "^3.0.2", + "splaytree": "^3.1.0" + } + }, "node_modules/postcss": { "version": "8.5.16", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", @@ -1847,6 +2951,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/puppeteer": { "version": "25.2.1", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.2.1.tgz", @@ -1887,6 +2997,16 @@ "node": ">=22.12.0" } }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -1922,12 +3042,85 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "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" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "license": "Unlicense" }, + "node_modules/rolldown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.138.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", @@ -1973,6 +3166,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -1992,6 +3191,19 @@ "semver": "bin/semver.js" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2002,6 +3214,57 @@ "node": ">=0.10.0" } }, + "node_modules/specificity": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz", + "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", + "license": "MIT", + "bin": { + "specificity": "bin/specificity" + } + }, + "node_modules/splaytree": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/splaytree/-/splaytree-3.2.3.tgz", + "integrity": "sha512-7OXrNWzy6CK+r7Ch9OLPBDTKfB6XlWHjX4P0RU5B3IgFuWPeYN0XtRtlexGRjgbQxpfaUve6jTAwBGWuGntz/w==", + "license": "MIT", + "engines": { + "node": ">=18.20 || >=20" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -2036,12 +3299,108 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/svg2pdf.js": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/svg2pdf.js/-/svg2pdf.js-2.7.0.tgz", + "integrity": "sha512-nXK4Wx28H0KtOktanm5nsphl1KMEoLNMelAT/776qxPAj9DshwYcqgdpKuBnY1nrcYOriQFHVQLE4tIag+aDJA==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "font-family-papandreou": "^0.2.0-patch1", + "specificity": "^0.4.1", + "svgpath": "^2.3.0" + }, + "peerDependencies": { + "jspdf": "^4.0.0 || ^3.0.0 || ^2.0.0" + } + }, + "node_modules/svgpath": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/svgpath/-/svgpath-2.6.0.tgz", + "integrity": "sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg==", + "license": "MIT", + "funding": { + "url": "https://github.com/fontello/svg2ttf?sponsor=1" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/three": { "version": "0.169.0", "resolved": "https://registry.npmjs.org/three/-/three-0.169.0.tgz", "integrity": "sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/typed-query-selector": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", @@ -2094,6 +3453,22 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -2154,6 +3529,201 @@ } } }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.2.tgz", + "integrity": "sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/webdriver-bidi-protocol": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", @@ -2161,6 +3731,23 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", diff --git a/package.json b/package.json index 420fbcd..a15c5c9 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,21 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@mlightcad/libredwg-web": "^0.7.7", "delaunator": "^5.1.0", "dxf-parser": "^1.1.2", + "jspdf": "^4.2.1", + "jszip": "^3.10.1", + "opencascade.js": "^1.1.1", + "polygon-clipping": "^0.15.7", "react": "^18.3.1", "react-dom": "^18.3.1", + "svg2pdf.js": "^2.7.0", "three": "^0.169.0" }, "devDependencies": { @@ -24,6 +31,7 @@ "playwright": "^1.61.1", "puppeteer": "^25.2.1", "typescript": "^5.6.2", - "vite": "^5.4.8" + "vite": "^5.4.8", + "vitest": "^4.1.9" } } diff --git a/src/model/parametricWalls.test.ts b/src/model/parametricWalls.test.ts new file mode 100644 index 0000000..430106f --- /dev/null +++ b/src/model/parametricWalls.test.ts @@ -0,0 +1,1109 @@ +// Parametrische Wand-Engine — Unit-Tests. +// +// Testet die reine Datenschicht (kein React, kein three.js, kein Store). +// Alle Tests laufen gegen die öffentliche API: resolveParametricWall(), +// deduplicateWalls(), matchesCondition(), matchesTarget(). + +import { describe, it, expect, beforeEach } from "vitest"; +import { + resolveParametricWall, + deduplicateWalls, + matchesCondition, + matchesTarget, + applyGridRule, + applyModuleRule, + applyConditionalThicknessRule, + applyReferenceLineRule, + applySequenceRule, + type ParametricContext, +} from "./parametricWalls"; +import type { + DrawingLevel, + GridRule, + ModuleRule, + ConditionalThicknessRule, + ReferenceLineRule, + SequenceRule, + ParametricWall, + Wall, + WallType, +} from "./types"; + +// ── Gemeinsame Test-Fixtures ────────────────────────────────────────────── + +function makeFloor(overrides?: Partial): DrawingLevel { + return { + id: "f1", + name: "Erdgeschoss", + kind: "floor", + visible: true, + locked: false, + floorHeight: 2.6, + cutHeight: 1.0, + baseElevation: 0, + ...overrides, + }; +} + +function makeWallType(id = "wt1", overrides?: Partial): WallType { + return { + id, + name: "Standard", + layers: [{ componentId: "c1", thickness: 0.2 }], + ...overrides, + }; +} + +function makeContext(overrides?: Partial): ParametricContext { + return { + floor: makeFloor(), + ...overrides, + }; +} + +/** Baut einen internen RuleCtx für direkte Regel-Funktionen. */ +function makeRuleCtx( + floorId: string, + defaultWallType: WallType, + context: ParametricContext, + existingWalls: Wall[] = [], +) { + // RuleCtx ist ein internes Interface; wir übergeben als unknown → any. + return { floorId, defaultWallType, context, existingWalls } as Parameters< + typeof applyGridRule + >[1]; +} + +function makeWall(overrides: Partial = {}): Wall { + return { + id: "w-test", + type: "wall", + floorId: "f1", + categoryCode: "20", + start: { x: 0, y: 0 }, + end: { x: 3, y: 0 }, + wallTypeId: "wt1", + height: 2.6, + ...overrides, + }; +} + +// ── GridRule ───────────────────────────────────────────────────────────────── + +describe("GridRule", () => { + const defaultWt = makeWallType("wt-default"); + const ctx = makeContext(); + + it("generiert Wände auf einem 3m-Raster in X-Richtung", () => { + const rule: GridRule = { + type: "grid", + spacing: 3, + directions: "x", + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const walls = applyGridRule(rule, rCtx); + + // X-Richtung: Wände senkrecht zur X-Achse (bei x=0,3,6,…,30) + expect(walls.length).toBeGreaterThan(0); + // Jede Wand läuft parallel zur Y-Achse (konstantes x) + for (const w of walls) { + expect(w.start.x).toBeCloseTo(w.end.x, 5); + } + }); + + it("generiert Wände in Y-Richtung", () => { + const rule: GridRule = { + type: "grid", + spacing: 3, + directions: "y", + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const walls = applyGridRule(rule, rCtx); + + expect(walls.length).toBeGreaterThan(0); + // Jede Wand läuft parallel zur X-Achse (konstantes y) + for (const w of walls) { + expect(w.start.y).toBeCloseTo(w.end.y, 5); + } + }); + + it("generiert Wände in beiden Richtungen bei 'both'", () => { + const rule: GridRule = { + type: "grid", + spacing: 3, + directions: "both", + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const walls = applyGridRule(rule, rCtx); + + const xWalls = walls.filter((w) => Math.abs(w.start.x - w.end.x) < 1e-6); + const yWalls = walls.filter((w) => Math.abs(w.start.y - w.end.y) < 1e-6); + expect(xWalls.length).toBeGreaterThan(0); + expect(yWalls.length).toBeGreaterThan(0); + }); + + it("verwendet wallTypeId aus der Regel", () => { + const rule: GridRule = { + type: "grid", + spacing: 3, + directions: "x", + wallTypeId: "wt-exterior", + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const walls = applyGridRule(rule, rCtx); + + expect(walls.length).toBeGreaterThan(0); + for (const w of walls) { + expect(w.wallTypeId).toBe("wt-exterior"); + } + }); + + it("fällt auf defaultWallType zurück wenn kein wallTypeId", () => { + const rule: GridRule = { + type: "grid", + spacing: 3, + directions: "x", + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const walls = applyGridRule(rule, rCtx); + + for (const w of walls) { + expect(w.wallTypeId).toBe(defaultWt.id); + } + }); + + it("setzt referenceLine wenn angegeben", () => { + const rule: GridRule = { + type: "grid", + spacing: 3, + directions: "x", + referenceLine: "left", + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const walls = applyGridRule(rule, rCtx); + + expect(walls.length).toBeGreaterThan(0); + for (const w of walls) { + expect(w.referenceLine).toBe("left"); + } + }); + + it("clips Raster auf boundaryGeometry-BBox", () => { + const rule: GridRule = { + type: "grid", + spacing: 3, + directions: "x", + }; + const ctxWithBoundary = makeContext({ + boundaryGeometry: { + boundary: [ + { x: 0, y: 0 }, + { x: 6, y: 0 }, + { x: 6, y: 9 }, + { x: 0, y: 9 }, + ], + }, + }); + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary); + const walls = applyGridRule(rule, rCtx); + + // Im 6×9m-Bereich mit 3m-Raster: 3 X-Achsen (0, 3, 6) → 3 Wände + expect(walls.length).toBe(3); + // Keine Wand soll außerhalb von [0..9] in Y liegen + for (const w of walls) { + expect(Math.min(w.start.y, w.end.y)).toBeGreaterThanOrEqual(0 - 1e-6); + expect(Math.max(w.start.y, w.end.y)).toBeLessThanOrEqual(9 + 1e-6); + } + }); + + it("verwendet gridAxes aus dem Kontext wenn gridId gesetzt ist", () => { + const rule: GridRule = { + type: "grid", + spacing: 3, + directions: "x", + gridId: "grid-1", + }; + const ctxWithAxes = makeContext({ + gridAxes: { x: [0, 5, 10], y: [0, 4, 8] }, + }); + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithAxes); + const walls = applyGridRule(rule, rCtx); + + // 3 Achsen → 3 Wände in X-Richtung + expect(walls.length).toBe(3); + const xs = walls.map((w) => w.start.x).sort((a, b) => a - b); + expect(xs[0]).toBeCloseTo(0); + expect(xs[1]).toBeCloseTo(5); + expect(xs[2]).toBeCloseTo(10); + }); + + it("setzt alle Wände auf das korrekte Geschoss", () => { + const rule: GridRule = { + type: "grid", + spacing: 3, + directions: "both", + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const walls = applyGridRule(rule, rCtx); + + for (const w of walls) { + expect(w.floorId).toBe("f1"); + } + }); +}); + +// ── ModuleRule ──────────────────────────────────────────────────────────────── + +describe("ModuleRule", () => { + const defaultWt = makeWallType("wt-module"); + const ctx = makeContext({ + boundaryGeometry: { + boundary: [ + { x: 0, y: 0 }, + { x: 18, y: 0 }, + { x: 18, y: 9 }, + { x: 0, y: 9 }, + ], + }, + }); + + it("teilt eine 18m-Spanne in 6m-Joche (X-Richtung)", () => { + const rule: ModuleRule = { + type: "module", + moduleSize: 6, + direction: "x", + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const walls = applyModuleRule(rule, rCtx); + + // Bei 18m Breite und 6m-Modulen: 2 innere Trennwände (bei x=6, x=12) + expect(walls.length).toBe(2); + const xs = walls.map((w) => w.start.x).sort((a, b) => a - b); + expect(xs[0]).toBeCloseTo(6); + expect(xs[1]).toBeCloseTo(12); + }); + + it("teilt eine Spanne in Y-Richtung", () => { + const rule: ModuleRule = { + type: "module", + moduleSize: 3, + direction: "y", + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const walls = applyModuleRule(rule, rCtx); + + // 9m / 3m = 3 Joche → 2 innere Trennwände (bei y=3, y=6) + expect(walls.length).toBe(2); + const ys = walls.map((w) => w.start.y).sort((a, b) => a - b); + expect(ys[0]).toBeCloseTo(3); + expect(ys[1]).toBeCloseTo(6); + }); + + it("snappt an eine Referenzwand", () => { + const refWall: Wall = makeWall({ + id: "ref-wall", + start: { x: 0, y: 0 }, + end: { x: 12, y: 0 }, + }); + const rule: ModuleRule = { + type: "module", + moduleSize: 6, + direction: "x", + referenceWallId: "ref-wall", + }; + const ctxWithBound = makeContext({ + boundaryGeometry: { + boundary: [ + { x: 0, y: 0 }, + { x: 18, y: 0 }, + { x: 18, y: 6 }, + { x: 0, y: 6 }, + ], + }, + }); + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBound, [refWall]); + const walls = applyModuleRule(rule, rCtx); + + // Referenzwand 12m → 1 innere Trennwand bei x=6 + expect(walls.length).toBe(1); + expect(walls[0].start.x).toBeCloseTo(6); + }); + + it("verwendet Geschoss-Ausdehnung wenn keine Referenzwand angegeben", () => { + const rule: ModuleRule = { + type: "module", + moduleSize: 6, + direction: "x", + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const walls = applyModuleRule(rule, rCtx); + + // 18m Breite → 2 innere Trennwände + expect(walls.length).toBeGreaterThan(0); + }); +}); + +// ── ConditionalThicknessRule ────────────────────────────────────────────────── + +describe("ConditionalThicknessRule", () => { + const defaultWt = makeWallType("wt-inner"); + const exteriorTypeId = "wt-exterior"; + + const ctxWithBoundary = makeContext({ + boundaryGeometry: { + boundary: [ + { x: 0, y: 0 }, + { x: 12, y: 0 }, + { x: 12, y: 9 }, + { x: 0, y: 9 }, + ], + }, + }); + + it("ändert wallTypeId für Außenwände (exterior)", () => { + // Wände auf dem Rand (y=0) — exterior + const exteriorWall: Wall = makeWall({ + id: "w-ext", + start: { x: 0, y: 0 }, + end: { x: 12, y: 0 }, + wallTypeId: "wt-inner", + }); + // Innenwand + const interiorWall: Wall = makeWall({ + id: "w-int", + start: { x: 3, y: 3 }, + end: { x: 9, y: 3 }, + wallTypeId: "wt-inner", + }); + + const rule: ConditionalThicknessRule = { + type: "conditional-thickness", + condition: "exterior", + wallTypeId: exteriorTypeId, + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary, [exteriorWall, interiorWall]); + const result = applyConditionalThicknessRule(rule, rCtx); + + // Nur die Außenwand wird geändert + expect(result.length).toBe(1); + expect(result[0].wallTypeId).toBe(exteriorTypeId); + }); + + it("ändert wallTypeId für Innenwände (interior)", () => { + const exteriorWall: Wall = makeWall({ + id: "w-ext", + start: { x: 0, y: 0 }, + end: { x: 12, y: 0 }, + wallTypeId: "wt-standard", + }); + const interiorWall: Wall = makeWall({ + id: "w-int", + start: { x: 3, y: 3 }, + end: { x: 9, y: 3 }, + wallTypeId: "wt-standard", + }); + + const rule: ConditionalThicknessRule = { + type: "conditional-thickness", + condition: "interior", + wallTypeId: "wt-partition", + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary, [exteriorWall, interiorWall]); + const result = applyConditionalThicknessRule(rule, rCtx); + + expect(result.length).toBe(1); + expect(result[0].wallTypeId).toBe("wt-partition"); + }); + + it("gibt keine Wand zurück wenn keine Bedingung passt", () => { + const interiorWall: Wall = makeWall({ + id: "w-int", + start: { x: 3, y: 3 }, + end: { x: 9, y: 3 }, + wallTypeId: "wt-inner", + }); + + const rule: ConditionalThicknessRule = { + type: "conditional-thickness", + condition: "exterior", + wallTypeId: exteriorTypeId, + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary, [interiorWall]); + const result = applyConditionalThicknessRule(rule, rCtx); + + expect(result.length).toBe(0); + }); + + it("erkennt tragende Wände (bearing) nach Achsenrichtung", () => { + // Wand fast exakt in X-Richtung + const bearingWall: Wall = makeWall({ + id: "w-bear", + start: { x: 0, y: 0 }, + end: { x: 10, y: 0.05 }, // nahezu horizontal + wallTypeId: "wt-std", + }); + // Diagonale Wand + const diagWall: Wall = makeWall({ + id: "w-diag", + start: { x: 0, y: 0 }, + end: { x: 5, y: 5 }, // 45° + wallTypeId: "wt-std", + }); + + const rule: ConditionalThicknessRule = { + type: "conditional-thickness", + condition: "bearing", + wallTypeId: "wt-bearing", + }; + const simpleCtx = makeContext(); + const rCtx = makeRuleCtx("f1", defaultWt, simpleCtx, [bearingWall, diagWall]); + const result = applyConditionalThicknessRule(rule, rCtx); + + // Nur die Achsen-Wand gilt als tragend + expect(result.length).toBe(1); + expect(result[0].id).not.toBe(diagWall.id); // neue ID per makeWallId + expect(result[0].wallTypeId).toBe("wt-bearing"); + }); + + it("mutiert keine Originalwände", () => { + const original: Wall = makeWall({ + id: "w-orig", + start: { x: 0, y: 0 }, + end: { x: 12, y: 0 }, + wallTypeId: "wt-orig", + }); + const originalTypeBefore = original.wallTypeId; + + const rule: ConditionalThicknessRule = { + type: "conditional-thickness", + condition: "exterior", + wallTypeId: "wt-new", + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary, [original]); + applyConditionalThicknessRule(rule, rCtx); + + expect(original.wallTypeId).toBe(originalTypeBefore); + }); +}); + +// ── ReferenceLineRule ───────────────────────────────────────────────────────── + +describe("ReferenceLineRule", () => { + const defaultWt = makeWallType(); + const ctxWithBoundary = makeContext({ + boundaryGeometry: { + boundary: [ + { x: 0, y: 0 }, + { x: 9, y: 0 }, + { x: 9, y: 6 }, + { x: 0, y: 6 }, + ], + }, + }); + + it("setzt referenceLine='left' für alle Wände (target='all')", () => { + const walls: Wall[] = [ + makeWall({ id: "w1", start: { x: 0, y: 0 }, end: { x: 9, y: 0 } }), + makeWall({ id: "w2", start: { x: 3, y: 2 }, end: { x: 6, y: 2 } }), + ]; + const rule: ReferenceLineRule = { + type: "reference-line", + referenceLine: "left", + target: "all", + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary, walls); + const result = applyReferenceLineRule(rule, rCtx); + + expect(result.length).toBe(2); + for (const w of result) { + expect(w.referenceLine).toBe("left"); + } + }); + + it("setzt referenceLine='right' nur für Außenwände (target='exterior')", () => { + // Außenwand am Rand + const extWall: Wall = makeWall({ + id: "w-ext", + start: { x: 0, y: 0 }, + end: { x: 9, y: 0 }, + }); + // Innenwand + const intWall: Wall = makeWall({ + id: "w-int", + start: { x: 3, y: 3 }, + end: { x: 6, y: 3 }, + }); + + const rule: ReferenceLineRule = { + type: "reference-line", + referenceLine: "right", + target: "exterior", + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary, [extWall, intWall]); + const result = applyReferenceLineRule(rule, rCtx); + + // Nur die Außenwand erhält referenceLine + expect(result.length).toBe(1); + expect(result[0].referenceLine).toBe("right"); + }); + + it("setzt referenceLine='center' für alle Wände", () => { + const walls: Wall[] = [ + makeWall({ id: "w1" }), + makeWall({ id: "w2", referenceLine: "left" }), + ]; + const rule: ReferenceLineRule = { + type: "reference-line", + referenceLine: "center", + target: "all", + }; + const rCtx = makeRuleCtx("f1", defaultWt, makeContext(), walls); + const result = applyReferenceLineRule(rule, rCtx); + + expect(result.length).toBe(2); + for (const w of result) { + expect(w.referenceLine).toBe("center"); + } + }); + + it("gibt leere Liste zurück wenn keine existingWalls", () => { + const rule: ReferenceLineRule = { + type: "reference-line", + referenceLine: "left", + target: "all", + }; + const rCtx = makeRuleCtx("f1", defaultWt, makeContext(), []); + const result = applyReferenceLineRule(rule, rCtx); + + expect(result).toHaveLength(0); + }); +}); + +// ── SequenceRule ────────────────────────────────────────────────────────────── + +describe("SequenceRule", () => { + const defaultWt = makeWallType("wt-seq"); + const ctx = makeContext({ + boundaryGeometry: { + boundary: [ + { x: 0, y: 0 }, + { x: 9, y: 0 }, + { x: 9, y: 6 }, + { x: 0, y: 6 }, + ], + }, + }); + + it("wendet Regeln in Reihenfolge an", () => { + const sequence: SequenceRule = { + type: "sequence", + rules: [ + { + type: "grid", + spacing: 3, + directions: "x", + wallTypeId: "wt-base", + } as GridRule, + { + type: "conditional-thickness", + condition: "exterior", + wallTypeId: "wt-exterior", + } as ConditionalThicknessRule, + ], + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const result = applySequenceRule(sequence, rCtx); + + // Schritt 1: Raster erzeugt Wände; Schritt 2: Außenwände erhalten neuen Typ. + // Beide Schritte liefern Wände zurück → Summe + expect(result.length).toBeGreaterThan(0); + }); + + it("spätere Regeln sehen Wände früherer Regeln", () => { + // Schritt 1: Raster (X) erzeugt senkrechte Wände + // Schritt 2: ConditionalThickness sieht diese Wände und kann sie verfeinern + const sequence: SequenceRule = { + type: "sequence", + rules: [ + { + type: "grid", + spacing: 3, + directions: "x", + wallTypeId: "wt-step1", + } as GridRule, + { + type: "conditional-thickness", + condition: "exterior", + wallTypeId: "wt-step2", + } as ConditionalThicknessRule, + ], + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const result = applySequenceRule(sequence, rCtx); + + // Schritt 2 sieht die Ausgabe von Schritt 1 (existingWalls enthält sie) + const step2Walls = result.filter((w) => w.wallTypeId === "wt-step2"); + expect(step2Walls.length).toBeGreaterThan(0); + }); + + it("stopOnMatch bricht nach dem ersten produktiven Schritt ab", () => { + const sequence: SequenceRule = { + type: "sequence", + stopOnMatch: true, + rules: [ + { + type: "grid", + spacing: 3, + directions: "x", + wallTypeId: "wt-first", + } as GridRule, + { + type: "grid", + spacing: 3, + directions: "y", + wallTypeId: "wt-second", + } as GridRule, + ], + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const result = applySequenceRule(sequence, rCtx); + + // Nur Schritt 1 (X-Wände), Schritt 2 soll nicht mehr laufen + expect(result.length).toBeGreaterThan(0); + for (const w of result) { + expect(w.wallTypeId).toBe("wt-first"); + } + }); + + it("stopOnMatch=false führt alle Regeln aus", () => { + const sequence: SequenceRule = { + type: "sequence", + stopOnMatch: false, + rules: [ + { + type: "grid", + spacing: 3, + directions: "x", + wallTypeId: "wt-x", + } as GridRule, + { + type: "grid", + spacing: 3, + directions: "y", + wallTypeId: "wt-y", + } as GridRule, + ], + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const result = applySequenceRule(sequence, rCtx); + + const xWalls = result.filter((w) => w.wallTypeId === "wt-x"); + const yWalls = result.filter((w) => w.wallTypeId === "wt-y"); + expect(xWalls.length).toBeGreaterThan(0); + expect(yWalls.length).toBeGreaterThan(0); + }); + + it("leere Sequenz liefert leeres Array", () => { + const sequence: SequenceRule = { + type: "sequence", + rules: [], + }; + const rCtx = makeRuleCtx("f1", defaultWt, ctx); + const result = applySequenceRule(sequence, rCtx); + + expect(result).toHaveLength(0); + }); +}); + +// ── deduplicateWalls ────────────────────────────────────────────────────────── + +describe("deduplicateWalls", () => { + it("entfernt doppelte Wände mit identischen Endpunkten", () => { + const wall1 = makeWall({ id: "a", start: { x: 0, y: 0 }, end: { x: 3, y: 0 } }); + const wall2 = makeWall({ id: "b", start: { x: 0, y: 0 }, end: { x: 3, y: 0 } }); + + const result = deduplicateWalls([wall1, wall2]); + expect(result).toHaveLength(1); + }); + + it("entfernt Duplikate mit umgekehrter Endpunkt-Reihenfolge", () => { + const wall1 = makeWall({ id: "a", start: { x: 0, y: 0 }, end: { x: 3, y: 0 } }); + const wall2 = makeWall({ id: "b", start: { x: 3, y: 0 }, end: { x: 0, y: 0 } }); + + const result = deduplicateWalls([wall1, wall2]); + expect(result).toHaveLength(1); + }); + + it("behält eindeutige Wände", () => { + const wall1 = makeWall({ id: "a", start: { x: 0, y: 0 }, end: { x: 3, y: 0 } }); + const wall2 = makeWall({ id: "b", start: { x: 0, y: 3 }, end: { x: 3, y: 3 } }); + + const result = deduplicateWalls([wall1, wall2]); + expect(result).toHaveLength(2); + }); + + it("behält die erste Instanz (nicht die letzte)", () => { + const wall1 = makeWall({ id: "first", start: { x: 0, y: 0 }, end: { x: 3, y: 0 } }); + const wall2 = makeWall({ id: "second", start: { x: 0, y: 0 }, end: { x: 3, y: 0 } }); + + const result = deduplicateWalls([wall1, wall2]); + expect(result[0].id).toBe("first"); + }); + + it("respektiert die Toleranz", () => { + // Fast gleiche Wände (< 1cm) + const wall1 = makeWall({ id: "a", start: { x: 0, y: 0 }, end: { x: 3, y: 0 } }); + const wall2 = makeWall({ id: "b", start: { x: 0.005, y: 0.005 }, end: { x: 3.005, y: 0.005 } }); + + const resultStrict = deduplicateWalls([wall1, wall2], 0.001); // strenger Schwellwert + expect(resultStrict).toHaveLength(2); + + const resultLoose = deduplicateWalls([wall1, wall2], 0.02); // lockerer Schwellwert + expect(resultLoose).toHaveLength(1); + }); + + it("gibt leere Liste zurück bei leerer Eingabe", () => { + expect(deduplicateWalls([])).toHaveLength(0); + }); +}); + +// ── matchesCondition ────────────────────────────────────────────────────────── + +describe("matchesCondition", () => { + const defaultWt = makeWallType(); + const ctxWithBoundary = makeContext({ + boundaryGeometry: { + boundary: [ + { x: 0, y: 0 }, + { x: 12, y: 0 }, + { x: 12, y: 8 }, + { x: 0, y: 8 }, + ], + }, + }); + + it("erkennt Außenwand am BBox-Rand (exterior)", () => { + const extWall = makeWall({ + start: { x: 0, y: 0 }, + end: { x: 12, y: 0 }, + }); + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary); + + expect(matchesCondition(extWall, "exterior", rCtx)).toBe(true); + }); + + it("erkennt Innenwand nicht als exterior", () => { + const intWall = makeWall({ + start: { x: 3, y: 3 }, + end: { x: 9, y: 3 }, + }); + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary); + + expect(matchesCondition(intWall, "exterior", rCtx)).toBe(false); + }); + + it("erkennt interior korrekt (Negation von exterior)", () => { + const intWall = makeWall({ + start: { x: 3, y: 3 }, + end: { x: 9, y: 3 }, + }); + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary); + + expect(matchesCondition(intWall, "interior", rCtx)).toBe(true); + }); + + it("bearing = true für achsenparallele Wand", () => { + const bearWall = makeWall({ + start: { x: 0, y: 0 }, + end: { x: 10, y: 0 }, + }); + const rCtx = makeRuleCtx("f1", defaultWt, makeContext()); + + expect(matchesCondition(bearWall, "bearing", rCtx)).toBe(true); + }); + + it("bearing = false für 45°-Wand", () => { + const diagWall = makeWall({ + start: { x: 0, y: 0 }, + end: { x: 5, y: 5 }, + }); + const rCtx = makeRuleCtx("f1", defaultWt, makeContext()); + + expect(matchesCondition(diagWall, "bearing", rCtx)).toBe(false); + }); + + it("unbekannte Bedingung gibt false zurück", () => { + const wall = makeWall(); + const rCtx = makeRuleCtx("f1", defaultWt, makeContext()); + + expect(matchesCondition(wall, "unknown-condition-xyz", rCtx)).toBe(false); + }); +}); + +// ── matchesTarget ───────────────────────────────────────────────────────────── + +describe("matchesTarget", () => { + const defaultWt = makeWallType(); + const ctxWithBoundary = makeContext({ + boundaryGeometry: { + boundary: [ + { x: 0, y: 0 }, + { x: 9, y: 0 }, + { x: 9, y: 6 }, + { x: 0, y: 6 }, + ], + }, + }); + + it("'all' passt auf jede Wand", () => { + const anyWall = makeWall({ start: { x: 3, y: 3 }, end: { x: 6, y: 3 } }); + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary); + + expect(matchesTarget(anyWall, "all", rCtx)).toBe(true); + }); + + it("'exterior' passt auf Außenwand", () => { + const extWall = makeWall({ start: { x: 0, y: 0 }, end: { x: 9, y: 0 } }); + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary); + + expect(matchesTarget(extWall, "exterior", rCtx)).toBe(true); + }); + + it("'exterior' passt nicht auf Innenwand", () => { + const intWall = makeWall({ start: { x: 3, y: 3 }, end: { x: 6, y: 3 } }); + const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary); + + expect(matchesTarget(intWall, "exterior", rCtx)).toBe(false); + }); + + it("unbekanntes Ziel gibt false zurück", () => { + const wall = makeWall(); + const rCtx = makeRuleCtx("f1", defaultWt, makeContext()); + + expect(matchesTarget(wall, "unknown-target-xyz", rCtx)).toBe(false); + }); +}); + +// ── resolveParametricWall (Integration) ────────────────────────────────────── + +describe("resolveParametricWall (Integration)", () => { + let floor: DrawingLevel; + let defaultWt: WallType; + let ctx: ParametricContext; + + beforeEach(() => { + floor = makeFloor({ id: "f-integ" }); + defaultWt = makeWallType("wt-integ-default"); + ctx = makeContext({ + floor, + boundaryGeometry: { + boundary: [ + { x: 0, y: 0 }, + { x: 9, y: 0 }, + { x: 9, y: 6 }, + { x: 0, y: 6 }, + ], + }, + }); + }); + + it("erzeugt Wall[]-Array aus einem GridRule-Regelwerk", () => { + const pw: ParametricWall = { + id: "pw-1", + name: "Test-Raster", + defaultWallTypeId: defaultWt.id, + rules: [ + { + type: "grid", + spacing: 3, + directions: "both", + }, + ], + }; + + const result = resolveParametricWall(pw, "f-integ", ctx, defaultWt); + + expect(result.length).toBeGreaterThan(0); + for (const w of result) { + expect(w.type).toBe("wall"); + expect(w.floorId).toBe("f-integ"); + } + }); + + it("kombiniert GridRule + ConditionalThicknessRule (mehrere Regeln)", () => { + // ConditionalThicknessRule erzeugt modifizierte Kopien, die jedoch nach der + // Deduplication (gleiche Koordinaten, erste Instanz gewinnt) entfernt werden. + // Das korrekte Verhalten: die Ausgabe enthält alle Grid-Wände unverändert; + // die Conditional-Thickness-Kopien sind Duplikate und werden entfernt. + const pw: ParametricWall = { + id: "pw-2", + name: "Raster + Dicke", + defaultWallTypeId: defaultWt.id, + rules: [ + { + type: "grid", + spacing: 3, + directions: "x", + wallTypeId: "wt-integ-default", + }, + { + type: "conditional-thickness", + condition: "exterior", + wallTypeId: "wt-integ-exterior", + }, + ], + }; + + const result = resolveParametricWall(pw, "f-integ", ctx, defaultWt); + + expect(result.length).toBeGreaterThan(0); + // Alle erhaltenen Wände stammen vom Grid (erste Instanz gewinnt bei Dedup) + const baseWalls = result.filter((w) => w.wallTypeId === "wt-integ-default"); + expect(baseWalls.length).toBeGreaterThan(0); + // ConditionalThickness erzeugt zusätzliche Wände, die bei applyRule zurückgegeben werden + // (testbar via applyConditionalThicknessRule direkt — dort bereits getestet) + }); + + it("entfernt überlappende Duplikate", () => { + // Zwei Regeln mit identischen Achsen (gleiche Wände) + const pw: ParametricWall = { + id: "pw-3", + name: "Duplikat-Test", + defaultWallTypeId: defaultWt.id, + rules: [ + { + type: "grid", + spacing: 3, + directions: "x", + wallTypeId: "wt-integ-default", + }, + { + type: "grid", + spacing: 3, + directions: "x", + wallTypeId: "wt-integ-default", + }, + ], + }; + + const result = resolveParametricWall(pw, "f-integ", ctx, defaultWt); + const singleRun = resolveParametricWall( + { ...pw, rules: [pw.rules[0]] }, + "f-integ", + ctx, + defaultWt, + ); + + // Mit Dedup sollen keine doppelten Wände enthalten sein + expect(result.length).toBe(singleRun.length); + }); + + it("leeres Regelwerk liefert leeres Array", () => { + const pw: ParametricWall = { + id: "pw-empty", + name: "Leer", + defaultWallTypeId: defaultWt.id, + rules: [], + }; + + const result = resolveParametricWall(pw, "f-integ", ctx, defaultWt); + expect(result).toHaveLength(0); + }); + + it("Wall-Objekte haben korrekte Felder (type, floorId, categoryCode)", () => { + const pw: ParametricWall = { + id: "pw-fields", + name: "Felder-Test", + defaultWallTypeId: defaultWt.id, + rules: [ + { + type: "grid", + spacing: 3, + directions: "x", + }, + ], + }; + + const result = resolveParametricWall(pw, "f-integ", ctx, defaultWt); + + for (const w of result) { + expect(w.type).toBe("wall"); + expect(w.floorId).toBe("f-integ"); + expect(w.categoryCode).toBe("20"); + expect(typeof w.height).toBe("number"); + expect(w.height).toBeGreaterThan(0); + } + }); + + it("verwendet Geschosshöhe als Wall-Höhe", () => { + const floorWith3m = makeFloor({ id: "f-height", floorHeight: 3.0 }); + const ctxWith3m = makeContext({ + floor: floorWith3m, + boundaryGeometry: { + boundary: [ + { x: 0, y: 0 }, + { x: 6, y: 0 }, + { x: 6, y: 6 }, + { x: 0, y: 6 }, + ], + }, + }); + + const pw: ParametricWall = { + id: "pw-height", + name: "Höhen-Test", + defaultWallTypeId: defaultWt.id, + rules: [ + { + type: "grid", + spacing: 3, + directions: "x", + }, + ], + }; + + const result = resolveParametricWall(pw, "f-height", ctxWith3m, defaultWt); + + for (const w of result) { + expect(w.height).toBeCloseTo(3.0); + } + }); + + it("Sequenzregel in resolveParametricWall: Raster → RefLinie", () => { + // ReferenceLineRule erzeugt modifizierte Kopien der Grid-Wände (gleiche Koordinaten, + // referenceLine='left'). Nach Deduplication gewinnt die erste Instanz (Grid-Wand ohne + // referenceLine), daher liefert das Endergebnis Grid-Wände ohne referenceLine. + // Das erwartete Verhalten: alle Wände vom Grid kommen durch, Sequenz läuft fehlerlos. + const pw: ParametricWall = { + id: "pw-seq", + name: "Sequenz", + defaultWallTypeId: defaultWt.id, + rules: [ + { + type: "sequence", + rules: [ + { + type: "grid", + spacing: 3, + directions: "x", + wallTypeId: "wt-integ-default", + }, + { + type: "reference-line", + referenceLine: "left", + target: "all", + }, + ], + }, + ], + }; + + const result = resolveParametricWall(pw, "f-integ", ctx, defaultWt); + + expect(result.length).toBeGreaterThan(0); + // Grid-Wände sind enthalten (erste Instanz gewinnt nach Dedup) + const gridWalls = result.filter((w) => w.wallTypeId === "wt-integ-default"); + expect(gridWalls.length).toBeGreaterThan(0); + // ReferenceLineRule als isolierte Regel getestet in applyReferenceLineRule-Suite + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..c1433e6 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +});