diff --git a/.gitignore b/.gitignore index fc9dc57cd4..80fbb01a00 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ /src/ifcmax/out/ /src/ifcwrap/out/ /src/qtviewer/out/ +/src/ifctester/webapp/public/pyodide/ /win/BuildDepsCache*.txt diff --git a/src/common.mk b/src/common.mk index 24537d06de..765655339b 100644 --- a/src/common.mk +++ b/src/common.mk @@ -28,6 +28,7 @@ dist: mkdir -p dist cp -r $(PACKAGE_NAME) build/ cp pyproject.toml build/ + if [ -f README.md ]; then cp README.md build/; fi ifeq ($(IS_STABLE), TRUE) $(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml ifdef IS_MODULE diff --git a/src/ifctester/Makefile b/src/ifctester/Makefile index 63ce7bc311..71ecbd48d7 100644 --- a/src/ifctester/Makefile +++ b/src/ifctester/Makefile @@ -25,9 +25,19 @@ WEBAPP_BUILD_DIR := $(WEBAPP_DIR)/dist PYODIDE_DIR := $(WEBAPP_DIR)/public/pyodide PYODIDE_VERSION := 0.28.0 PYODIDE_URL := https://github.com/pyodide/pyodide/releases/download/$(PYODIDE_VERSION)/pyodide-$(PYODIDE_VERSION).tar.bz2 +WORKER_BIN_DIR := $(WEBAPP_DIR)/public/worker/bin +IFCOPENSHELL_WASM_WHEEL := ifcopenshell-0.8.5+a51b2c5-cp313-cp313-pyodide_2025_0_wasm32.whl +IFCOPENSHELL_WASM_WHEEL_URL := https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-0.8.5%2Ba51b2c5-cp313-cp313-pyodide_2025_0_wasm32.whl +IFCOPENSHELL_WASM_WHEEL_PATH := $(WORKER_BIN_DIR)/$(IFCOPENSHELL_WASM_WHEEL) +WEBAPP_GENERATED_DIR := $(WEBAPP_DIR)/public/worker/generated +WEBAPP_IFCTESTER_MANIFEST := $(WEBAPP_GENERATED_DIR)/ifctester.json +WEBAPP_IFCTESTER_BUILD_DIR := build-webapp-wheel +PACKAGE_WEBAPP_DIR := $(PACKAGE_NAME)/webapp +PACKAGE_WEBAPP_WWW_DIR := $(PACKAGE_WEBAPP_DIR)/www .PHONY: webapp-dev -webapp-dev: +webapp-dev: pyodide-download ifcopenshell-wasm-download webapp-stage-ifctester-wheel + cd $(WEBAPP_DIR) && npm install cd $(WEBAPP_DIR) && npm run dev .PHONY: pyodide-download @@ -62,8 +72,43 @@ pyodide-download: echo "Pyodide $(PYODIDE_VERSION) prepared in $(PYODIDE_DIR)"; \ fi +.PHONY: ifcopenshell-wasm-download +ifcopenshell-wasm-download: + @if [ -f "$(IFCOPENSHELL_WASM_WHEEL_PATH)" ]; then \ + echo "IfcOpenShell wasm wheel already exists at $(IFCOPENSHELL_WASM_WHEEL_PATH), skipping download"; \ + else \ + echo "Downloading IfcOpenShell wasm wheel..."; \ + mkdir -p $(WORKER_BIN_DIR); \ + rm -f $(WORKER_BIN_DIR)/ifcopenshell-*.whl; \ + curl -fL -o "$(IFCOPENSHELL_WASM_WHEEL_PATH)" "$(IFCOPENSHELL_WASM_WHEEL_URL)"; \ + echo "IfcOpenShell wasm wheel prepared at $(IFCOPENSHELL_WASM_WHEEL_PATH)"; \ + fi + +.PHONY: webapp-stage-ifctester-wheel +webapp-stage-ifctester-wheel: + rm -rf $(WEBAPP_GENERATED_DIR) + rm -rf $(WEBAPP_IFCTESTER_BUILD_DIR) + mkdir -p $(WEBAPP_IFCTESTER_BUILD_DIR) + cp -r $(PACKAGE_NAME) $(WEBAPP_IFCTESTER_BUILD_DIR)/ + rm -rf $(WEBAPP_IFCTESTER_BUILD_DIR)/$(PACKAGE_NAME)/webapp + cp pyproject.toml $(WEBAPP_IFCTESTER_BUILD_DIR)/ + cp README.md $(WEBAPP_IFCTESTER_BUILD_DIR)/ +ifeq ($(IS_STABLE), TRUE) + $(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' $(WEBAPP_IFCTESTER_BUILD_DIR)/pyproject.toml + $(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' $(WEBAPP_IFCTESTER_BUILD_DIR)/$(PACKAGE_NAME)/__init__.py +else + $(SED) 's/version = "0.0.0"/version = "$(VERSION)a$(VERSION_DATE)"/' $(WEBAPP_IFCTESTER_BUILD_DIR)/pyproject.toml + $(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' $(WEBAPP_IFCTESTER_BUILD_DIR)/$(PACKAGE_NAME)/__init__.py +endif + cd $(WEBAPP_IFCTESTER_BUILD_DIR) && $(PYTHON) -m venv env --system-site-packages && . env/$(VENV_ACTIVATE) && python -m pip install build && python -m build --wheel --no-isolation + mkdir -p $(WEBAPP_GENERATED_DIR) + wheel=$$(basename $(WEBAPP_IFCTESTER_BUILD_DIR)/dist/$(PACKAGE_NAME)-*.whl); \ + cp "$(WEBAPP_IFCTESTER_BUILD_DIR)/dist/$$wheel" "$(WEBAPP_GENERATED_DIR)/$$wheel"; \ + printf '{\n "wheel_url": "/worker/generated/%s"\n}\n' "$$wheel" > "$(WEBAPP_IFCTESTER_MANIFEST)" + rm -rf $(WEBAPP_IFCTESTER_BUILD_DIR) + .PHONY: webapp-build -webapp-build: pyodide-download +webapp-build: pyodide-download ifcopenshell-wasm-download webapp-stage-ifctester-wheel cd $(WEBAPP_DIR) && npm install cd $(WEBAPP_DIR) && npm run build @@ -76,22 +121,30 @@ clean: rm -rf $(WEBAPP_BUILD_DIR) rm -rf $(WEBAPP_DIR)/node_modules rm -rf $(PYODIDE_DIR) + rm -f $(WORKER_BIN_DIR)/ifcopenshell-*.whl + rm -rf $(WEBAPP_GENERATED_DIR) + rm -rf $(WEBAPP_IFCTESTER_BUILD_DIR) rm -rf $(PACKAGE_NAME)/webapp rm -rf dist -.PHONY: dist -dist: webapp-prepare +.PHONY: python-dist +python-dist: + rm -rf dist # For some reason OS is not initalized when we call common.mk dist, which matters on Windows. # So we pass it explicitly. - $(MAKE) -f ../common.mk dist PACKAGE_NAME=$(PACKAGE_NAME) OS=$(OS) + $(MAKE) -f ../common.mk dist PACKAGE_NAME=$(PACKAGE_NAME) OS=$(OS) IS_STABLE=$(IS_STABLE) + +.PHONY: dist +dist: webapp-prepare + $(MAKE) python-dist OS=$(OS) IS_STABLE=$(IS_STABLE) .PHONY: webapp-prepare webapp-prepare: webapp-build - rm -rf $(PACKAGE_NAME)/webapp/www/* - mkdir -p $(PACKAGE_NAME)/webapp/www - cp -r $(WEBAPP_BUILD_DIR)/* $(PACKAGE_NAME)/webapp/www/ - cp $(WEBAPP_DIR)/__init__.py $(PACKAGE_NAME)/webapp/__init__.py - cp $(WEBAPP_DIR)/serve.py $(PACKAGE_NAME)/webapp/serve.py + rm -rf $(PACKAGE_WEBAPP_WWW_DIR) + mkdir -p $(PACKAGE_WEBAPP_WWW_DIR) + cp -r $(WEBAPP_BUILD_DIR)/* $(PACKAGE_WEBAPP_WWW_DIR)/ + cp $(WEBAPP_DIR)/__init__.py $(PACKAGE_WEBAPP_DIR)/__init__.py + cp $(WEBAPP_DIR)/serve.py $(PACKAGE_WEBAPP_DIR)/serve.py .PHONY: test test: diff --git a/src/ifctester/webapp/.gitignore b/src/ifctester/webapp/.gitignore index 4205867691..f7daf321ef 100644 --- a/src/ifctester/webapp/.gitignore +++ b/src/ifctester/webapp/.gitignore @@ -11,6 +11,8 @@ node_modules dist dist-ssr *.local +public/worker/bin/ifcopenshell-*.whl +public/worker/generated # Editor directories and files .vscode/* @@ -24,4 +26,4 @@ dist-ssr *.sw? .claude -experiment/* \ No newline at end of file +experiment/* diff --git a/src/ifctester/webapp/biome.json b/src/ifctester/webapp/biome.json new file mode 100644 index 0000000000..ce01c80c3f --- /dev/null +++ b/src/ifctester/webapp/biome.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "files": { + "ignore": [ + "dist", + "node_modules", + "public/pyodide", + "**/*.svelte" + ] + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "overrides": [ + { + "include": [ + "src/modules/wasm/worker/**" + ], + "linter": { + "rules": { + "suspicious": { + "noExplicitAny": "off" + } + } + } + } + ] +} diff --git a/src/ifctester/webapp/index.html b/src/ifctester/webapp/index.html index 27c32440dc..48ac4cc1c8 100644 --- a/src/ifctester/webapp/index.html +++ b/src/ifctester/webapp/index.html @@ -9,6 +9,6 @@
- + diff --git a/src/ifctester/webapp/jsconfig.json b/src/ifctester/webapp/jsconfig.json deleted file mode 100644 index 2aef2313e4..0000000000 --- a/src/ifctester/webapp/jsconfig.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "compilerOptions": { - "moduleResolution": "bundler", - "target": "ESNext", - "module": "ESNext", - /** - * svelte-preprocess cannot figure out whether you have - * a value or a type, so tell TypeScript to enforce using - * `import type` instead of `import` for Types. - */ - "verbatimModuleSyntax": true, - "isolatedModules": true, - "resolveJsonModule": true, - /** - * To have warnings / errors of the Svelte compiler at the - * correct position, enable source maps by default. - */ - "sourceMap": true, - "esModuleInterop": true, - "skipLibCheck": true, - /** - * Typecheck JS in `.svelte` and `.js` files by default. - * Disable this if you'd like to use dynamic types. - */ - "checkJs": false, - "baseUrl": ".", - "paths": { - "$lib": ["./src/lib"], - "$lib/*": ["./src/lib/*"], - "$src": ["./src"], - "$src/*": ["./src/*"] - } - }, - /** - * Use global.d.ts instead of compilerOptions.types - * to avoid limiting type declarations. - */ - "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] -} diff --git a/src/ifctester/webapp/package-lock.json b/src/ifctester/webapp/package-lock.json index c167ebcecc..3afc3428de 100644 --- a/src/ifctester/webapp/package-lock.json +++ b/src/ifctester/webapp/package-lock.json @@ -16,6 +16,7 @@ "svelte-spa-router": "^4.0.1" }, "devDependencies": { + "@biomejs/biome": "^1.9.4", "@internationalized/date": "^3.8.1", "@lucide/svelte": "^0.515.0", "@sveltejs/vite-plugin-svelte": "^5.0.3", @@ -25,11 +26,13 @@ "mode-watcher": "^1.1.0", "sass-embedded": "^1.89.0", "svelte": "^5.53.6", + "svelte-check": "^4.0.0", "svelte-sonner": "^1.0.5", "tailwind-merge": "^3.3.0", "tailwind-variants": "^1.0.0", "tailwindcss": "^4.0.0", "tw-animate-css": "^1.3.2", + "typescript": "^5.8.3", "vite": "^6.4.1" } }, @@ -47,6 +50,170 @@ "node": ">=6.0.0" } }, + "node_modules/@biomejs/biome": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@bufbuild/protobuf": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.5.1.tgz", @@ -1468,6 +1635,22 @@ "dev": true, "license": "MIT/X11" }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -2126,6 +2309,16 @@ "svelte": "^5.7.0" } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2200,6 +2393,20 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/regexparam": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz", @@ -2281,6 +2488,19 @@ "tslib": "^2.1.0" } }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/sass-embedded": { "version": "1.89.0", "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.89.0.tgz", @@ -2783,6 +3003,30 @@ "node": ">=18" } }, + "node_modules/svelte-check": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.5.tgz", + "integrity": "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, "node_modules/svelte-sonner": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.0.5.tgz", @@ -2983,6 +3227,20 @@ "url": "https://github.com/sponsors/Wombosvideo" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", diff --git a/src/ifctester/webapp/package.json b/src/ifctester/webapp/package.json index bb7d58567e..14b54ad8ed 100644 --- a/src/ifctester/webapp/package.json +++ b/src/ifctester/webapp/package.json @@ -6,11 +6,15 @@ "scripts": { "dev": "vite", "build": "vite build", + "check": "tsc -p tsconfig.json --noEmit && svelte-check && biome lint .", + "lint": "biome lint .", + "lint:fix": "biome lint --write .", "preview": "vite preview", "deploy": "npm run build && npx wrangler pages deploy dist" }, "devDependencies": { "@internationalized/date": "^3.8.1", + "@biomejs/biome": "^1.9.4", "@lucide/svelte": "^0.515.0", "@sveltejs/vite-plugin-svelte": "^5.0.3", "@tailwindcss/vite": "^4.0.0", @@ -19,10 +23,12 @@ "mode-watcher": "^1.1.0", "sass-embedded": "^1.89.0", "svelte": "^5.53.6", + "svelte-check": "^4.0.0", "svelte-sonner": "^1.0.5", "tailwind-merge": "^3.3.0", "tailwind-variants": "^1.0.0", "tailwindcss": "^4.0.0", + "typescript": "^5.8.3", "tw-animate-css": "^1.3.2", "vite": "^6.4.1" }, diff --git a/src/ifctester/webapp/public/worker/bin/ifcopenshell-0.8.3+bb329af-cp313-cp313-emscripten_4_0_9_wasm32.whl b/src/ifctester/webapp/public/worker/bin/ifcopenshell-0.8.3+bb329af-cp313-cp313-emscripten_4_0_9_wasm32.whl deleted file mode 100644 index 703ae3b877..0000000000 Binary files a/src/ifctester/webapp/public/worker/bin/ifcopenshell-0.8.3+bb329af-cp313-cp313-emscripten_4_0_9_wasm32.whl and /dev/null differ diff --git a/src/ifctester/webapp/serve.py b/src/ifctester/webapp/serve.py index 5b18f15545..c114b47542 100644 --- a/src/ifctester/webapp/serve.py +++ b/src/ifctester/webapp/serve.py @@ -22,9 +22,6 @@ import os import sys bonsai_lib_path = os.environ.get("BONSAI_LIB_PATH") -print(os.environ) -print(bonsai_lib_path) -bonsai_version = os.environ.get("BONSAI_VERSION") if bonsai_lib_path: sys.path.insert(0, bonsai_lib_path) diff --git a/src/ifctester/webapp/src/App.svelte b/src/ifctester/webapp/src/App.svelte index 19222ad8d1..27b5c8001b 100644 --- a/src/ifctester/webapp/src/App.svelte +++ b/src/ifctester/webapp/src/App.svelte @@ -1,4 +1,4 @@ - diff --git a/src/ifctester/webapp/src/app.d.ts b/src/ifctester/webapp/src/app.d.ts new file mode 100644 index 0000000000..ff86489626 --- /dev/null +++ b/src/ifctester/webapp/src/app.d.ts @@ -0,0 +1,4 @@ +declare module "*.svelte" { + import type { SvelteComponent } from "svelte"; + export default class Component extends SvelteComponent {} +} diff --git a/src/ifctester/webapp/src/components/AppHeader.svelte b/src/ifctester/webapp/src/components/AppHeader.svelte index 61f8a5b3b2..8fb71b6180 100644 --- a/src/ifctester/webapp/src/components/AppHeader.svelte +++ b/src/ifctester/webapp/src/components/AppHeader.svelte @@ -1,11 +1,11 @@ - @@ -142,4 +143,4 @@ - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/components/AppRibbon.svelte b/src/ifctester/webapp/src/components/AppRibbon.svelte index 4f9643637e..662a93b500 100644 --- a/src/ifctester/webapp/src/components/AppRibbon.svelte +++ b/src/ifctester/webapp/src/components/AppRibbon.svelte @@ -1,6 +1,5 @@ -
@@ -20,4 +19,4 @@ Error
{/if} - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/components/AppToolbar.svelte b/src/ifctester/webapp/src/components/AppToolbar.svelte index 3552b13969..c9b3dd2968 100644 --- a/src/ifctester/webapp/src/components/AppToolbar.svelte +++ b/src/ifctester/webapp/src/components/AppToolbar.svelte @@ -1,14 +1,16 @@ - @@ -63,4 +65,4 @@ Part Of - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/components/IdsTabs.svelte b/src/ifctester/webapp/src/components/IdsTabs.svelte index 0f6f38a569..25d6891ab7 100644 --- a/src/ifctester/webapp/src/components/IdsTabs.svelte +++ b/src/ifctester/webapp/src/components/IdsTabs.svelte @@ -1,13 +1,22 @@ -
@@ -15,10 +24,13 @@
switchDocument(docId)} - aria-label={doc.info.title || "Untitled"} + onkeydown={(event) => handleActivation(event, () => switchDocument(docId))} + aria-label={(doc as IdsDocument).info.title || "Untitled"} > - {doc.info.title || "Untitled"} + {(doc as IdsDocument).info.title || "Untitled"}
{/each}
-
\ No newline at end of file + diff --git a/src/ifctester/webapp/src/config.json b/src/ifctester/webapp/src/config.json index d5c450f460..f4f469178f 100644 --- a/src/ifctester/webapp/src/config.json +++ b/src/ifctester/webapp/src/config.json @@ -1,8 +1,7 @@ { "wasm": { - "wheel_url": "/worker/bin/ifcopenshell-0.8.3+bb329af-cp313-cp313-emscripten_4_0_9_wasm32.whl", + "wheel_url": "/worker/bin/ifcopenshell-0.8.5+a51b2c5-cp313-cp313-pyodide_2025_0_wasm32.whl", "odfpy_url": "/worker/bin/odfpy-1.4.2-py2.py3-none-any.whl", - "api_py_url": "/worker/api.py", - "pyodide_url": "https://cdn.jsdelivr.net/pyodide/v0.28.0/full/pyodide.js" + "api_py_url": "/worker/api.py" } -} \ No newline at end of file +} diff --git a/src/ifctester/webapp/src/css/app.css b/src/ifctester/webapp/src/css/app.css index a8210b43e8..09f12f3b9f 100644 --- a/src/ifctester/webapp/src/css/app.css +++ b/src/ifctester/webapp/src/css/app.css @@ -576,7 +576,8 @@ html, body { display: flex; flex-direction: column; - label { + label, + .form-label { margin-bottom: 4px; font-size: 14px; font-weight: 500; diff --git a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-close.svelte b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-close.svelte index 94266a6315..a6ac3f979a 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-close.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-close.svelte @@ -1,7 +1,11 @@ - - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-content.svelte b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-content.svelte index 9ceaa7faf7..17d98fe392 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-content.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-content.svelte @@ -1,8 +1,17 @@ - @@ -35,4 +44,4 @@ {/if} - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-description.svelte b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-description.svelte index 419a27e449..0f91cf0670 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-description.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-description.svelte @@ -1,12 +1,17 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-footer.svelte b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-footer.svelte index b2d4035e6e..7071ae2a0c 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-footer.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-footer.svelte @@ -1,11 +1,19 @@ -
{@render children?.()} -
\ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-header.svelte b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-header.svelte index 791dcde5d7..9bb73aa601 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-header.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-header.svelte @@ -1,12 +1,19 @@ -
{@render children?.()} -
\ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-overlay.svelte b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-overlay.svelte index adb8a38e94..90e3104148 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-overlay.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -1,12 +1,17 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-title.svelte b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-title.svelte index 2d36371d01..866d5ef826 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-title.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-title.svelte @@ -1,12 +1,17 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-trigger.svelte b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-trigger.svelte index 7a38ff9e6a..5636ea2df1 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-trigger.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -1,7 +1,11 @@ - - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/dialog/index.js b/src/ifctester/webapp/src/lib/components/ui/dialog/index.ts similarity index 100% rename from src/ifctester/webapp/src/lib/components/ui/dialog/index.js rename to src/ifctester/webapp/src/lib/components/ui/dialog/index.ts diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte index fc034efd31..242f3088ef 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -1,8 +1,16 @@ - {@render childrenProp?.()} {/snippet} - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte index d13c1ad08d..b793c23e85 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -1,14 +1,21 @@ - @@ -22,4 +29,4 @@ )} {...restProps} /> - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte index 89454b26df..83032fee1e 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -1,12 +1,17 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte index 4f3421fbe8..55220a4783 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -1,7 +1,11 @@ - - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte index a06f2099dd..20744df288 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -1,14 +1,21 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte index e0aceee7d3..2fc90b933b 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -1,12 +1,20 @@ -
{@render children?.()} -
\ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte index 009a4dd02d..0be2895631 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -1,11 +1,16 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte index 218db7f78a..629cc37563 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -1,14 +1,22 @@ - {@render childrenProp?.({ checked })} {/snippet} - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte index 4d02884d17..533e47c574 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -1,12 +1,17 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte index 20f2210c5a..93b6175647 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -1,12 +1,19 @@ - {@render children?.()} - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte index 0f3c698fea..7a33ac199c 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -1,12 +1,17 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte index e4e236e111..a2be23a136 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -1,7 +1,15 @@ - {@render children?.()} - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte index 720d196b77..1aa23bc512 100644 --- a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -1,7 +1,11 @@ - - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/index.js b/src/ifctester/webapp/src/lib/components/ui/dropdown-menu/index.ts similarity index 100% rename from src/ifctester/webapp/src/lib/components/ui/dropdown-menu/index.js rename to src/ifctester/webapp/src/lib/components/ui/dropdown-menu/index.ts diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/index.js b/src/ifctester/webapp/src/lib/components/ui/menubar/index.ts similarity index 100% rename from src/ifctester/webapp/src/lib/components/ui/menubar/index.js rename to src/ifctester/webapp/src/lib/components/ui/menubar/index.ts diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-checkbox-item.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-checkbox-item.svelte index 0d6e298b0f..bb020eec7b 100644 --- a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-checkbox-item.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-checkbox-item.svelte @@ -1,8 +1,16 @@ - {@render childrenProp?.()} {/snippet} - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-content.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-content.svelte index 2706fc76e2..4c7696d218 100644 --- a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-content.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-content.svelte @@ -1,6 +1,19 @@ - @@ -28,4 +41,4 @@ )} {...restProps} /> - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-group-heading.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-group-heading.svelte index c64dd91534..13417d54ec 100644 --- a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-group-heading.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-group-heading.svelte @@ -1,12 +1,17 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-group.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-group.svelte index 8acc8b9549..2c77293f76 100644 --- a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-group.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-group.svelte @@ -1,10 +1,14 @@ - - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-item.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-item.svelte index f4fee4711e..1ff76231a5 100644 --- a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-item.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-item.svelte @@ -1,6 +1,13 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-label.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-label.svelte index 129be16dd5..efbbb9b017 100644 --- a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-label.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-label.svelte @@ -1,12 +1,19 @@ -
{@render children?.()} -
\ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-radio-item.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-radio-item.svelte index c7c122881f..1947458beb 100644 --- a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-radio-item.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-radio-item.svelte @@ -1,14 +1,22 @@ - {@render childrenProp?.({ checked })} {/snippet} - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-separator.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-separator.svelte index d32bbab673..7674b560bc 100644 --- a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-separator.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-separator.svelte @@ -1,12 +1,17 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-shortcut.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-shortcut.svelte index 7df7a0d118..05cf1bc13b 100644 --- a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-shortcut.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-shortcut.svelte @@ -1,12 +1,19 @@ - {@render children?.()} - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-sub-content.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-sub-content.svelte index 5a79de2c58..67b68ca633 100644 --- a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-sub-content.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-sub-content.svelte @@ -1,12 +1,17 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-sub-trigger.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-sub-trigger.svelte index f9fa0461ee..c14976bd12 100644 --- a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-sub-trigger.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-sub-trigger.svelte @@ -1,7 +1,15 @@ - {@render children?.()} - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-trigger.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-trigger.svelte index c39921a2a1..33119d913c 100644 --- a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-trigger.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar-trigger.svelte @@ -1,12 +1,17 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar.svelte b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar.svelte index cddff1b908..ebc3534d09 100644 --- a/src/ifctester/webapp/src/lib/components/ui/menubar/menubar.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/menubar/menubar.svelte @@ -1,12 +1,17 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/sonner/index.js b/src/ifctester/webapp/src/lib/components/ui/sonner/index.ts similarity index 100% rename from src/ifctester/webapp/src/lib/components/ui/sonner/index.js rename to src/ifctester/webapp/src/lib/components/ui/sonner/index.ts diff --git a/src/ifctester/webapp/src/lib/components/ui/sonner/sonner.svelte b/src/ifctester/webapp/src/lib/components/ui/sonner/sonner.svelte index 91981de7e6..28a794ca0f 100644 --- a/src/ifctester/webapp/src/lib/components/ui/sonner/sonner.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/sonner/sonner.svelte @@ -1,8 +1,8 @@ - \ No newline at end of file +/> diff --git a/src/ifctester/webapp/src/lib/components/ui/tooltip/index.js b/src/ifctester/webapp/src/lib/components/ui/tooltip/index.ts similarity index 100% rename from src/ifctester/webapp/src/lib/components/ui/tooltip/index.js rename to src/ifctester/webapp/src/lib/components/ui/tooltip/index.ts diff --git a/src/ifctester/webapp/src/lib/components/ui/tooltip/tooltip-content.svelte b/src/ifctester/webapp/src/lib/components/ui/tooltip/tooltip-content.svelte index 3c25c65b9a..6db2203857 100644 --- a/src/ifctester/webapp/src/lib/components/ui/tooltip/tooltip-content.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -1,6 +1,18 @@ - @@ -42,4 +54,4 @@ {/snippet} - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/src/ifctester/webapp/src/lib/components/ui/tooltip/tooltip-trigger.svelte index a2885f2cca..b1524c3a3c 100644 --- a/src/ifctester/webapp/src/lib/components/ui/tooltip/tooltip-trigger.svelte +++ b/src/ifctester/webapp/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -1,7 +1,11 @@ - - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/lib/utils.js b/src/ifctester/webapp/src/lib/utils.js deleted file mode 100644 index f79e2db4c9..0000000000 --- a/src/ifctester/webapp/src/lib/utils.js +++ /dev/null @@ -1,8 +0,0 @@ -import { clsx, } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs) { - return twMerge(clsx(inputs)); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any \ No newline at end of file diff --git a/src/ifctester/webapp/src/lib/utils.ts b/src/ifctester/webapp/src/lib/utils.ts new file mode 100644 index 0000000000..be010d3ced --- /dev/null +++ b/src/ifctester/webapp/src/lib/utils.ts @@ -0,0 +1,7 @@ +import { clsx } from "clsx"; +import type { ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/ifctester/webapp/src/main.js b/src/ifctester/webapp/src/main.js deleted file mode 100644 index 14fb0c3be8..0000000000 --- a/src/ifctester/webapp/src/main.js +++ /dev/null @@ -1,9 +0,0 @@ -import { mount } from 'svelte'; -import './css/app.css'; -import App from './App.svelte'; - -const app = mount(App, { - target: document.getElementById('root'), -}); - -export default app; \ No newline at end of file diff --git a/src/ifctester/webapp/src/main.ts b/src/ifctester/webapp/src/main.ts new file mode 100644 index 0000000000..54ddb22cf9 --- /dev/null +++ b/src/ifctester/webapp/src/main.ts @@ -0,0 +1,14 @@ +import { mount } from 'svelte'; +import './css/app.css'; +import App from './App.svelte'; + +const root = document.getElementById('root'); +if (!root) { + throw new Error('Missing root element'); +} + +const app = mount(App, { + target: root, +}); + +export default app; diff --git a/src/ifctester/webapp/src/modules/api/api.svelte.js b/src/ifctester/webapp/src/modules/api/api.svelte.ts similarity index 72% rename from src/ifctester/webapp/src/modules/api/api.svelte.js rename to src/ifctester/webapp/src/modules/api/api.svelte.ts index ad2a7b8c3f..b4ebe01fdc 100644 --- a/src/ifctester/webapp/src/modules/api/api.svelte.js +++ b/src/ifctester/webapp/src/modules/api/api.svelte.ts @@ -1,8 +1,31 @@ import wasm from "$src/modules/wasm"; -import * as IDS from "$src/modules/api/ids.svelte.js"; +import * as IDS from "$src/modules/api/ids.svelte"; import hyperid from "hyperid"; +import type { AuditReport, AuditReportData } from "$src/types/report"; +import type { IdsDocument } from "$src/types/ids"; -export let Autocompletions = $state({ +type AutocompletionState = { + entityClasses: string[]; + materialCategories: string[]; + classificationSystems: Record; + dataTypes: string[]; + isLoaded: boolean; +}; + +type IfcModel = { + id: string; + fileName: string; + fileSize: number; + loadedAt: Date; +}; + +type IfcModelState = { + models: IfcModel[]; + isLoading: boolean; + audits: AuditReport[]; +}; + +export const Autocompletions: AutocompletionState = $state({ entityClasses: [], materialCategories: [], classificationSystems: {}, @@ -10,13 +33,13 @@ export let Autocompletions = $state({ isLoaded: false }); -export let IFCModels = $state({ +export const IFCModels: IfcModelState = $state({ models: [], isLoading: false, audits: [] }); -const id = hyperid(); +const id: () => string = hyperid(); // Preload autocompletions on initialization wasm.init().then(async () => { @@ -30,26 +53,30 @@ export async function preloadAutocompletions() { // Entity classes const entitySets = await Promise.all( schemas.map(schema => wasm.getAllEntityClasses(schema)) - ); - const allEntities = new Set(); - entitySets.forEach(entities => { - entities.forEach(entity => allEntities.add(entity.toUpperCase())); - }); + ) as string[][]; + const allEntities = new Set(); + for (const entities of entitySets) { + for (const entity of entities) { + allEntities.add(entity.toUpperCase()); + } + } // Data types const dataTypeSets = await Promise.all( schemas.map(schema => wasm.getAllDataTypes(schema)) - ); - const allDataTypes = new Set(); - dataTypeSets.forEach(dataTypes => { - Object.keys(dataTypes).forEach(dataType => allDataTypes.add(dataType)); - }); + ) as Record[]; + const allDataTypes = new Set(); + for (const dataTypes of dataTypeSets) { + for (const dataType of Object.keys(dataTypes as Record)) { + allDataTypes.add(dataType); + } + } // Material categories and Classification systems const [materialCategories, classificationSystems] = await Promise.all([ wasm.getMaterialCategories(), wasm.getStandardClassificationSystems() - ]); + ]) as [string[], AutocompletionState["classificationSystems"]]; // Cache autocompletions Autocompletions.entityClasses = Array.from(allEntities).sort(); @@ -64,15 +91,15 @@ export async function preloadAutocompletions() { } } -export async function getPredefinedTypes(schema, entity) { +export async function getPredefinedTypes(schema: string, entity: string) { return await wasm.getPredefinedTypes(schema, entity); } -export async function getEntityAttributes(schema, entity) { +export async function getEntityAttributes(schema: string, entity: string) { return await wasm.getEntityAttributes(schema, entity); } -export async function getApplicablePsets(schema, entity, predefinedType = '') { +export async function getApplicablePsets(schema: string, entity: string, predefinedType = '') { return await wasm.getApplicablePsets(schema, entity, predefinedType); } @@ -92,7 +119,7 @@ export function getDataTypes() { return Autocompletions.dataTypes; } -export async function loadIfc(file) { +export async function loadIfc(file: File): Promise { try { IFCModels.isLoading = true; @@ -100,10 +127,10 @@ export async function loadIfc(file) { const uint8Array = new Uint8Array(arrayBuffer); // Load IFC model - const ifcId = await wasm.loadIfc(Array.from(uint8Array)); + const ifcId = await wasm.loadIfc(Array.from(uint8Array)) as string; // Add to models list - const model = { + const model: IfcModel = { id: ifcId, fileName: file.name, fileSize: file.size, @@ -121,7 +148,7 @@ export async function loadIfc(file) { } } -export async function unloadIfc(modelId) { +export async function unloadIfc(modelId: string) { try { // Unload model await wasm.unloadIfc(modelId); @@ -136,9 +163,9 @@ export async function unloadIfc(modelId) { } } -export async function auditIfc(modelId, idsData) { +export async function auditIfc(modelId: string, idsData: string | Uint8Array | ArrayBuffer) { try { - let idsBytes; + let idsBytes: Uint8Array; if (typeof idsData === 'string') { idsBytes = new TextEncoder().encode(idsData); } else if (idsData instanceof ArrayBuffer) { @@ -148,7 +175,7 @@ export async function auditIfc(modelId, idsData) { } // Run audit - const auditResult = await wasm.auditIfc(modelId, idsBytes); + const auditResult = await wasm.auditIfc(modelId, idsBytes) as { json: AuditReportData; html: string }; console.log(`Audit completed for model ${modelId}`); return auditResult; @@ -163,13 +190,14 @@ export function getLoadedModels() { } export async function openIfc() { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.ifc'; fileInput.onchange = async (event) => { - const file = event.target.files[0]; + const target = event.target as HTMLInputElement | null; + const file = target?.files?.[0]; if (!file) { reject(new Error('No file selected')); return; @@ -194,15 +222,20 @@ export async function openIfc() { }); } -export function getIfcById(modelId) { +export function getIfcById(modelId: string) { return IFCModels.models.find(model => model.id === modelId); } -export function createAuditReport(modelId, document, auditData, htmlReport = null) { +export function createAuditReport( + modelId: string, + document: string, + auditData: AuditReportData, + htmlReport: string | null = null +): AuditReport | undefined { const model = getIfcById(modelId); if (!model) return; - const auditReport = { + const auditReport: AuditReport = { id: id(), modelId: modelId, modelName: model.fileName, @@ -216,19 +249,19 @@ export function createAuditReport(modelId, document, auditData, htmlReport = nul return auditReport; } -export function getAuditReportsForIfc(modelId) { +export function getAuditReportsForIfc(modelId: string) { return IFCModels.audits.filter(audit => audit.modelId === modelId); } -export function getAuditReportById(auditId) { +export function getAuditReportById(auditId: string) { return IFCModels.audits.find(audit => audit.id === auditId); } -export function clearIdsAuditReports(document) { +export function clearIdsAuditReports(document: string) { IFCModels.audits = IFCModels.audits.filter(audit => audit.document !== document); } -export async function downloadAuditReport(auditId) { +export async function downloadAuditReport(auditId: string) { const audit = getAuditReportById(auditId); if (!audit || !audit.htmlReport) { throw new Error('HTML report not available for this audit'); @@ -237,7 +270,7 @@ export async function downloadAuditReport(auditId) { // Get IDS document title for filename let filename = 'report.html'; if (audit.document && IDS.Module.documents[audit.document]) { - const doc = IDS.Module.documents[audit.document]; + const doc = IDS.Module.documents[audit.document] as IdsDocument; const title = doc.info?.title || 'untitled'; filename = `report_${title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.html`; } @@ -270,9 +303,12 @@ export async function runAudit() { // Get the active IDS document XML const idsXml = await IDS.exportActiveDocument(); + if (!idsXml) { + throw new Error('Failed to export IDS document'); + } // Run audit on all loaded models - let firstAuditReport = null; + let firstAuditReport: AuditReport | undefined; for (const model of IFCModels.models) { const result = await auditIfc(model.id, idsXml); @@ -280,7 +316,10 @@ export async function runAudit() { const jsonData = result.json || null; const htmlReport = result.html || null; - const auditReport = createAuditReport(model.id, IDS.Module.activeDocument, jsonData, htmlReport); + if (!jsonData) { + continue; + } + const auditReport = createAuditReport(model.id, IDS.Module.activeDocument as string, jsonData, htmlReport); // Store the first audit report to open in viewer if (!firstAuditReport) { diff --git a/src/ifctester/webapp/src/modules/api/bonsai.svelte.js b/src/ifctester/webapp/src/modules/api/bonsai.svelte.ts similarity index 64% rename from src/ifctester/webapp/src/modules/api/bonsai.svelte.js rename to src/ifctester/webapp/src/modules/api/bonsai.svelte.ts index bcc4b8965d..5ed9b562f4 100644 --- a/src/ifctester/webapp/src/modules/api/bonsai.svelte.js +++ b/src/ifctester/webapp/src/modules/api/bonsai.svelte.ts @@ -1,12 +1,37 @@ import { io } from 'socket.io-client'; -import { IFCModels } from './api.svelte.js'; -import * as IDS from './ids.svelte.js'; -import { error, success } from '../utils/toast.svelte.js'; +import type { Socket } from 'socket.io-client'; +import { IFCModels } from './api.svelte'; +import * as IDS from './ids.svelte'; +import { error, success } from '../utils/toast.svelte'; import hyperid from 'hyperid'; -import { onMount } from 'svelte'; +import type { AuditReport, AuditReportData } from "$src/types/report"; // Bonsai connection state -export let Bonsai = $state({ +type BonsaiState = { + enabled: boolean; + port: string | null; + socket: Socket | null; + connected: boolean; + auditing: boolean; +}; + +type PendingAudit = { + resolve: (value: string | null) => void; + reject: (reason?: unknown) => void; +}; + +type AuditResultPayload = { + id?: string; + json_report?: string; + html_report?: string; +}; + +type AuditErrorPayload = { + id?: string; + error?: string; +}; + +export const Bonsai: BonsaiState = $state({ enabled: false, port: null, socket: null, @@ -14,8 +39,8 @@ export let Bonsai = $state({ auditing: false }); -const id = hyperid(); -const pendingAudits = new Map(); +const id: () => string = hyperid(); +const pendingAudits = new Map(); // Check for Bonsai server port in URL parameters const urlParams = new URLSearchParams(window.location.search); @@ -29,8 +54,11 @@ if (serverPort) { /** * Connect to Bonsai server */ -export const connect = () => new Promise((resolve, reject) => { - if (!Bonsai.port) return; +export const connect = () => new Promise((resolve, reject) => { + if (!Bonsai.port) { + resolve(); + return; + } try { Bonsai.socket = io(`ws://127.0.0.1:${Bonsai.port}/ifctester`, { @@ -49,7 +77,7 @@ export const connect = () => new Promise((resolve, reject) => { Bonsai.connected = false; }); - Bonsai.socket.on('connect_error', (err) => { + Bonsai.socket.on('connect_error', (err: Error) => { Bonsai.connected = false; error(`Failed to connect to Bonsai: ${err.message}`); reject(err); @@ -59,7 +87,8 @@ export const connect = () => new Promise((resolve, reject) => { Bonsai.socket.on('error', handleAuditError); } catch (err) { - error(`Failed to connect to Bonsai: ${err.message}`); + const message = err instanceof Error ? err.message : String(err); + error(`Failed to connect to Bonsai: ${message}`); reject(err); } }); @@ -93,14 +122,21 @@ export const runAudit = async () => { // Convert IDS document to XML string const idsXml = await IDS.exportActiveDocument(); + if (!idsXml) { + throw new Error('Failed to export IDS document'); + } const requestId = id(); + const socket = Bonsai.socket; + if (!socket) { + throw new Error('Bonsai socket not connected'); + } - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { // Store request with resolve/reject functions pendingAudits.set(requestId, { resolve, reject }); - Bonsai.socket.emit('audit_ids', { + socket.emit('audit_ids', { id: requestId, ids: idsXml }); @@ -108,7 +144,8 @@ export const runAudit = async () => { } catch (err) { Bonsai.auditing = false; - error(`Failed to run Bonsai audit: ${err.message}`); + const message = err instanceof Error ? err.message : String(err); + error(`Failed to run Bonsai audit: ${message}`); return null; } }; @@ -117,7 +154,7 @@ export const runAudit = async () => { * Handles audit results from Bonsai server * @param {Object} data - Audit result data */ -const handleAuditResult = (data) => { +const handleAuditResult = (data: AuditResultPayload) => { if (!data.id || !data.json_report) return; const pendingAudit = pendingAudits.get(data.id); @@ -130,13 +167,14 @@ const handleAuditResult = (data) => { const { resolve } = pendingAudit; try { - const reportData = JSON.parse(data.json_report); + const reportData = JSON.parse(data.json_report) as AuditReportData; - const auditReport = { + const auditReport: AuditReport = { id: data.id, + modelId: `bonsai:${data.id}`, date: new Date().toISOString(), modelName: 'Bonsai IFC Model', - document: IDS.Module.activeDocument, + document: IDS.Module.activeDocument ?? "", data: reportData, htmlReport: data.html_report }; @@ -152,7 +190,8 @@ const handleAuditResult = (data) => { } catch (err) { Bonsai.auditing = false; - error(`Failed to process audit result: ${err.message}`); + const message = err instanceof Error ? err.message : String(err); + error(`Failed to process audit result: ${message}`); resolve(null); } }; @@ -161,7 +200,7 @@ const handleAuditResult = (data) => { * Handles audit errors from Bonsai server * @param {Object} data - Error data */ -const handleAuditError = (data) => { +const handleAuditError = (data: AuditErrorPayload) => { if (!data.id) return; const pendingAudit = pendingAudits.get(data.id); @@ -174,7 +213,6 @@ const handleAuditError = (data) => { const { resolve } = pendingAudit; Bonsai.auditing = false; - error(`Audit failed (Bonsai): ${data.error}`); + error(`Audit failed (Bonsai): ${data.error ?? "Unknown error"}`); resolve(null); }; - diff --git a/src/ifctester/webapp/src/modules/api/ids.svelte.js b/src/ifctester/webapp/src/modules/api/ids.svelte.ts similarity index 63% rename from src/ifctester/webapp/src/modules/api/ids.svelte.js rename to src/ifctester/webapp/src/modules/api/ids.svelte.ts index 3d30948d3d..3035113654 100644 --- a/src/ifctester/webapp/src/modules/api/ids.svelte.js +++ b/src/ifctester/webapp/src/modules/api/ids.svelte.ts @@ -1,10 +1,18 @@ import wasm from "$src/modules/wasm"; -import { clearIdsAuditReports } from "./api.svelte.js"; +import { clearIdsAuditReports } from "./api.svelte"; import hyperid from "hyperid"; import {tick} from "svelte"; +import type { DocumentState, Facet, FacetValue, IdsDocument, IdsCardinality, Restriction, Specification } from "$src/types/ids"; -export let Module = $state({ - documents: [], +type ModuleState = { + documents: Record; + activeDocument: string | null; + status: "loading" | "ready" | "error"; + states: Record; +}; + +export const Module: ModuleState = $state({ + documents: {}, activeDocument: null, status: "loading", states: {} @@ -17,14 +25,15 @@ wasm.init().then(() => { Module.status = "error"; }); -const id = hyperid() +const id: () => string = hyperid(); -export function setDocumentState(docId, updates) { +export function setDocumentState(docId: string, updates: Partial) { if (!Module.states[docId]) { Module.states[docId] = { activeTab: 'info', viewMode: 'editor', - activeSpecification: null + activeSpecification: null, + auditReport: null }; } Object.assign(Module.states[docId], updates); @@ -32,7 +41,7 @@ export function setDocumentState(docId, updates) { export async function createDocument() { const docId = id(); - const doc = await wasm.createIDS(); + const doc = await wasm.createIDS() as IdsDocument; Module.documents[docId] = doc; @@ -43,14 +52,14 @@ export async function createDocument() { Module.activeDocument = docId; } -export async function deleteDocument(id) { +export async function deleteDocument(id: string) { // Clear any audit reports generated using this IDS document clearIdsAuditReports(id); delete Module.documents[id]; delete Module.states[id]; - if (Module.activeDocument == id) { + if (Module.activeDocument === id) { // If there are other documents, set the first one as active if (Object.keys(Module.documents).length > 0) { Module.activeDocument = Object.keys(Module.documents)[0]; @@ -62,19 +71,19 @@ export async function deleteDocument(id) { // Normalize (remove xs: prefix) from JSON dict returned from Python // We need this because the backend exports with xs: prefix, yet expects a dict without prefixes. -function normalizeIdsDict(obj) { +function normalizeIdsDict(obj: unknown): unknown { if (typeof obj !== 'object' || obj === null) return obj; if (Array.isArray(obj)) { return obj.map(normalizeIdsDict); } - const result = {}; + const result: Record = {}; for (const [key, value] of Object.entries(obj)) { if (key === 'xs:restriction' && Array.isArray(value) && value.length > 0) { // Convert xs:restriction array to restriction object - const restriction = value[0]; - const newRestriction = {}; + const restriction = value[0] as Record; + const newRestriction: Record = {}; for (const [restrictionKey, restrictionValue] of Object.entries(restriction)) { if (restrictionKey.startsWith('xs:')) { @@ -86,7 +95,7 @@ function normalizeIdsDict(obj) { } } - result['restriction'] = newRestriction; + result.restriction = newRestriction; } else { result[key] = normalizeIdsDict(value); } @@ -96,13 +105,16 @@ function normalizeIdsDict(obj) { } export async function openDocument() { - return new Promise((resolve, reject) => { - const fileInput = document.createElement('input'); + return new Promise((resolve, reject) => { + const fileInput = document.createElement('input') as HTMLInputElement & { + oncancel?: ((this: HTMLInputElement, ev: Event) => void) | null; + }; fileInput.type = 'file'; fileInput.accept = '.ids,.xml'; fileInput.onchange = async (event) => { - const file = event.target.files[0]; + const target = event.target as HTMLInputElement | null; + const file = target?.files?.[0]; if (!file) { reject(new Error('No file selected')); return; @@ -112,8 +124,8 @@ export async function openDocument() { const reader = new FileReader(); reader.onload = async (e) => { try { - const fileContent = e.target.result; - const doc = normalizeIdsDict(await wasm.openIDS(fileContent, false)); + const fileContent = (e.target as FileReader).result; + const doc = normalizeIdsDict(await wasm.openIDS(String(fileContent), false)) as IdsDocument; const docId = id(); // Add document to list and set as active @@ -145,16 +157,16 @@ export async function openDocument() { }); } -export async function exportActiveDocument() { +export async function exportActiveDocument(): Promise { if (!Module.activeDocument) return null; const doc = $state.snapshot(Module.documents[Module.activeDocument]); - const xmlString = await wasm.exportIDS(doc); + const xmlString = await wasm.exportIDS(doc as Record) as string; return xmlString; } -export async function exportDocument(docId) { +export async function exportDocument(docId: string) { const doc = $state.snapshot(Module.documents[docId]); // Validate @@ -162,37 +174,38 @@ export async function exportDocument(docId) { throw new Error("Please create at least one specification before exporting the document."); } - const xmlString = await wasm.exportIDS(doc); + const xmlString = await wasm.exportIDS(doc as Record) as string; // Create and download file const blob = new Blob([xmlString], { type: 'application/xml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `${Module.documents[docId].info.title.replace(/[^a-zA-Z0-9]/g, '_')}.ids`; + const title = Module.documents[docId].info.title || "untitled"; + a.download = `${title.replace(/[^a-zA-Z0-9]/g, '_')}.ids`; a.click(); URL.revokeObjectURL(url); } -export async function createSpecification(docId) { - const spec = await wasm.createSpecification(); +export async function createSpecification(docId: string) { + const spec = await wasm.createSpecification() as Specification; // Add specification to document Module.documents[docId].specifications.specification.push(spec); // Set as active specification - if (Module.activeDocument == docId) { + if (Module.activeDocument === docId) { const state = Module.states[docId]; state.activeSpecification = Module.documents[docId].specifications.specification.length - 1; } } -export async function deleteSpecification(docId, specId) { +export async function deleteSpecification(docId: string, specId: number) { Module.documents[docId].specifications.specification.splice(specId, 1); - if (Module.activeDocument == docId) { + if (Module.activeDocument === docId) { const state = Module.states[docId]; - if (state.activeSpecification == specId) { + if (state.activeSpecification === specId) { // We need to wait for the next tick here because of Svelte's internal shenanigans await tick(); setDocumentState(docId, { activeSpecification: null }); @@ -209,124 +222,148 @@ export async function deleteSpecification(docId, specId) { * clause: "applicability", "requirements" * facet: "entity", "attribute", "classification", "partOf", "property", "material" */ -export async function createFacet(docId, specId, clause, facet) { - let facetObj; - if (facet == "entity") { - facetObj = await wasm.createEntityFacet(clause, {}); - } else if (facet == "attribute") { - facetObj = await wasm.createAttributeFacet(clause, {}); - } else if (facet == "classification") { - facetObj = await wasm.createClassificationFacet(clause, {}); - } else if (facet == "partOf") { - facetObj = await wasm.createPartOfFacet(clause, {}); - } else if (facet == "property") { - facetObj = await wasm.createPropertyFacet(clause, {}); - } else if (facet == "material") { - facetObj = await wasm.createMaterialFacet(clause, {}); +export async function createFacet( + docId: string, + specId: number, + clause: "applicability" | "requirements", + facet: "entity" | "attribute" | "classification" | "partOf" | "property" | "material" +) { + let facetObj: Facet | undefined; + if (facet === "entity") { + facetObj = await wasm.createEntityFacet(clause, {}) as Facet; + } else if (facet === "attribute") { + facetObj = await wasm.createAttributeFacet(clause, {}) as Facet; + } else if (facet === "classification") { + facetObj = await wasm.createClassificationFacet(clause, {}) as Facet; + } else if (facet === "partOf") { + facetObj = await wasm.createPartOfFacet(clause, {}) as Facet; + } else if (facet === "property") { + facetObj = await wasm.createPropertyFacet(clause, {}) as Facet; + } else if (facet === "material") { + facetObj = await wasm.createMaterialFacet(clause, {}) as Facet; } - if (!(facet in Module.documents[docId].specifications.specification[specId][clause])) { - Module.documents[docId].specifications.specification[specId][clause][facet] = []; + if (!facetObj) return; + + const spec = Module.documents[docId].specifications.specification[specId]; + const clauseKey = clause as "applicability" | "requirements"; + if (!spec[clauseKey]) spec[clauseKey] = {}; + if (!(facet in (spec[clauseKey] as Record))) { + (spec[clauseKey] as Record)[facet] = []; } - Module.documents[docId].specifications.specification[specId][clause][facet].push(facetObj); + ((spec[clauseKey] as Record)[facet] as Facet[]).push(facetObj); } -export async function deleteFacet(docId, specId, clause, facet, facetId) { - delete Module.documents[docId].specifications.specification[specId][clause][facet][facetId]; +export async function deleteFacet( + docId: string, + specId: number, + clause: "applicability" | "requirements", + facet: "entity" | "attribute" | "classification" | "partOf" | "property" | "material", + facetId: number +) { + const spec = Module.documents[docId].specifications.specification[specId]; + const list = (spec[clause] as Record | undefined)?.[facet] as Facet[] | undefined; + if (!list) return; + list.splice(facetId, 1); } -export function getSpecUsage(spec) { +export function getSpecUsage(spec?: Specification | null): IdsCardinality { if (!spec?.applicability) return 'required'; - const minOccurs = spec.applicability["@minOccurs"]; - const maxOccurs = spec.applicability["@maxOccurs"]; - - if (minOccurs === 1 && maxOccurs === "unbounded") return 'required'; - if (minOccurs === 0 && maxOccurs === "unbounded") return 'optional'; - if (minOccurs === 0 && maxOccurs === 0) return 'prohibited'; + const minOccurs = spec.applicability["@minOccurs"] as number | undefined; + const maxOccurs = spec.applicability["@maxOccurs"] as number | "unbounded" | undefined; + + if (minOccurs !== 0) return 'required'; + if (minOccurs === 0 && maxOccurs !== 0) return 'optional'; + if (maxOccurs === 0) return 'prohibited'; return 'required'; }; // Converts facet to human-readable description -export function stringifyFacet(clauseType, facet, facetType, spec) { +export function stringifyFacet( + clauseType: "applicability" | "requirements", + facet: Facet, + facetType: string, + spec?: Specification | null +) { if (!facet) return ""; const usage = getSpecUsage(spec); - const descriptions = []; + const descriptions: string[] = []; // Entity facet if (facetType === "entity") { if (clauseType === "applicability") { - descriptions.push(`All data where IFC class ${stringifyValue(facet.name)}`); + descriptions.push(`All data where IFC class ${stringifyValue(facet.name as FacetValue)}`); } else { - descriptions.push(`Shall be data where IFC class ${stringifyValue(facet.name)}`); + descriptions.push(`Shall be data where IFC class ${stringifyValue(facet.name as FacetValue)}`); } if (facet.predefinedType) { - descriptions.push(`and type ${stringifyValue(facet.predefinedType)}`); + descriptions.push(`and type ${stringifyValue(facet.predefinedType as FacetValue)}`); } } // Attribute facet else if (facetType === "attribute") { if (clauseType === "applicability") { - descriptions.push(`All data where attribute ${stringifyValue(facet.name)}`); + descriptions.push(`All data where attribute ${stringifyValue(facet.name as FacetValue)}`); } else { - descriptions.push(`Shall be data where attribute ${stringifyValue(facet.name)}`); + descriptions.push(`Shall be data where attribute ${stringifyValue(facet.name as FacetValue)}`); } - descriptions.push(`and value ${stringifyValue(facet.value)}`); + descriptions.push(`and value ${stringifyValue(facet.value as FacetValue)}`); } // Property facet else if (facetType === "property") { if (clauseType === "applicability") { - descriptions.push(`Elements where property ${stringifyValue(facet.baseName)}`); + descriptions.push(`Elements where property ${stringifyValue(facet.baseName as FacetValue)}`); } else { - descriptions.push(`Shall be elements where property ${stringifyValue(facet.baseName)}`); + descriptions.push(`Shall be elements where property ${stringifyValue(facet.baseName as FacetValue)}`); } if (facet.value) { - descriptions.push(`and value ${stringifyValue(facet.value)}`); + descriptions.push(`and value ${stringifyValue(facet.value as FacetValue)}`); } - descriptions.push(`and dataset ${stringifyValue(facet.propertySet)}`); + descriptions.push(`and dataset ${stringifyValue(facet.propertySet as FacetValue)}`); } // Classification facet else if (facetType === "classification") { if (clauseType === "applicability") { - descriptions.push(`All data where classification system ${stringifyValue(facet.system)}`); + descriptions.push(`All data where classification system ${stringifyValue(facet.system as FacetValue)}`); } else { - descriptions.push(`Shall be data where classification system ${stringifyValue(facet.system)}`); + descriptions.push(`Shall be data where classification system ${stringifyValue(facet.system as FacetValue)}`); } if (facet.value) { - descriptions.push(`and classification ${stringifyValue(facet.value)}`); + descriptions.push(`and classification ${stringifyValue(facet.value as FacetValue)}`); } } // Material facet else if (facetType === "material") { if (clauseType === "applicability") { - descriptions.push(`All data where material ${stringifyValue(facet.value)}`); + descriptions.push(`All data where material ${stringifyValue(facet.value as FacetValue)}`); } else { - descriptions.push(`Shall be data where material ${stringifyValue(facet.value)}`); + descriptions.push(`Shall be data where material ${stringifyValue(facet.value as FacetValue)}`); } } // PartOf facet else if (facetType === "partOf") { if (clauseType === "applicability") { - descriptions.push(`An element with an **${facet['@relation']}** relationship`); + descriptions.push(`An element with an **${String(facet['@relation'] ?? "")}** relationship`); if (facet.name) { - descriptions.push(`with an entity where IFC class ${stringifyValue(facet.name)}`); + descriptions.push(`with an entity where IFC class ${stringifyValue(facet.name as FacetValue)}`); } } else { - descriptions.push(`An element shall have an **${facet['@relation']}** relationship`); + descriptions.push(`An element shall have an **${String(facet['@relation'] ?? "")}** relationship`); if (facet.name) { - descriptions.push(`with an entity where IFC class ${stringifyValue(facet.name)}`); + descriptions.push(`with an entity where IFC class ${stringifyValue(facet.name as FacetValue)}`); } if (facet.predefinedType) { - descriptions.push(`and predefined type ${stringifyValue(facet.predefinedType)}`); + descriptions.push(`and predefined type ${stringifyValue(facet.predefinedType as FacetValue)}`); } } } @@ -336,20 +373,22 @@ export function stringifyFacet(clauseType, facet, facetType, spec) { // Post-process for prohibited and optional requirements let isProhibited = false; - if (usage == "prohibited") isProhibited = !isProhibited; - if (clauseType == "requirements" && "@cardinality" in facet && facet["@cardinality"] == "prohibited") isProhibited = !isProhibited; + if (usage === "prohibited") isProhibited = !isProhibited; + if (clauseType === "requirements" && "@cardinality" in facet && facet["@cardinality"] === "prohibited") { + isProhibited = !isProhibited; + } if (isProhibited) combined = combined.replace("Shall", "Shall not").replace("shall", "shall not"); - if (clauseType == "requirements" && "@cardinality" in facet && facet["@cardinality"] == "optional") + if (clauseType === "requirements" && "@cardinality" in facet && facet["@cardinality"] === "optional") combined = combined.replace("Shall", "May").replace("shall", "may"); return renderFacetString(combined); } // Converts value objects to human-readable strings -function stringifyValue(value) { +function stringifyValue(value?: FacetValue) { if (!value) return "is provided"; if (value.simpleValue) return `is **${value.simpleValue}**`; if (value.restriction) return stringifyRestriction(value.restriction); @@ -357,7 +396,7 @@ function stringifyValue(value) { } // Converts restriction objects to human-readable strings -function stringifyRestriction(restriction) { +function stringifyRestriction(restriction: Restriction) { if (!restriction) return ""; // Handle enumeration @@ -394,7 +433,7 @@ function stringifyRestriction(restriction) { if (restriction.maxExclusive && restriction.maxExclusive.length > 0) { parts.push(`**< ${restriction.maxExclusive[0]['@value'] || ''}**`); } - return parts.length > 0 ? "is in range " + parts.join(", ") : "has range restriction"; + return parts.length > 0 ? `is in range ${parts.join(", ")}` : "has range restriction"; } // Handle length range restrictions @@ -406,18 +445,18 @@ function stringifyRestriction(restriction) { if (restriction.maxLength && restriction.maxLength.length > 0) { parts.push(`**max length ${restriction.maxLength[0]['@value'] || ''}**`); } - return parts.length > 0 ? "has " + parts.join(", ") : "has length range restriction"; + return parts.length > 0 ? `has ${parts.join(", ")}` : "has length range restriction"; } return "has complex restriction"; } -function renderFacetString(text) { +function renderFacetString(text: string): string { // Convert **text** to text - text = text.replace(/\*\*([^*]+)\*\*/g, '$1'); + const withStrong = text.replace(/\*\*([^*]+)\*\*/g, '$1'); // Convert `text` to text - text = text.replace(/`([^`]+)`/g, '$1'); + const withCode = withStrong.replace(/`([^`]+)`/g, '$1'); - return text; -} \ No newline at end of file + return withCode; +} diff --git a/src/ifctester/webapp/src/modules/utils/toast.svelte.js b/src/ifctester/webapp/src/modules/utils/toast.svelte.ts similarity index 73% rename from src/ifctester/webapp/src/modules/utils/toast.svelte.js rename to src/ifctester/webapp/src/modules/utils/toast.svelte.ts index eaf9b373f0..335be48645 100644 --- a/src/ifctester/webapp/src/modules/utils/toast.svelte.js +++ b/src/ifctester/webapp/src/modules/utils/toast.svelte.ts @@ -4,7 +4,7 @@ import { toast } from "svelte-sonner"; * Show an error toast notification * @param {string} message - The error message to display */ -export function error(message) { +export function error(message: string): void { toast.error(message); } @@ -12,7 +12,7 @@ export function error(message) { * Show a success toast notification * @param {string} message - The success message to display */ -export function success(message) { +export function success(message: string): void { toast.success(message); } @@ -20,7 +20,7 @@ export function success(message) { * Show an info toast notification * @param {string} message - The info message to display */ -export function info(message) { +export function info(message: string): void { toast.info(message); } @@ -28,7 +28,7 @@ export function info(message) { * Show a warning toast notification * @param {string} message - The warning message to display */ -export function warning(message) { +export function warning(message: string): void { toast.warning(message); } @@ -37,7 +37,7 @@ export function warning(message) { * @param {string} message - The loading message to display * @returns {string} - Toast ID for dismissing later */ -export function loading(message) { +export function loading(message: string): string | number { return toast.loading(message); } @@ -45,7 +45,7 @@ export function loading(message) { * Dismiss a specific toast * @param {string} toastId - The toast ID to dismiss */ -export function dismiss(toastId) { +export function dismiss(toastId: string | number): void { toast.dismiss(toastId); } @@ -57,10 +57,16 @@ export function dismiss(toastId) { * @param {string} messages.success - Success message * @param {string} messages.error - Error message */ -export function promise(promiseToTrack, messages) { +type PromiseToastMessages = { + loading: string; + success: string; + error: string; +}; + +export function promise(promiseToTrack: Promise, messages: PromiseToastMessages) { return toast.promise(promiseToTrack, { loading: messages.loading, success: messages.success, error: messages.error, }); -} \ No newline at end of file +} diff --git a/src/ifctester/webapp/src/modules/wasm/index.js b/src/ifctester/webapp/src/modules/wasm/index.ts similarity index 69% rename from src/ifctester/webapp/src/modules/wasm/index.js rename to src/ifctester/webapp/src/modules/wasm/index.ts index cecb655f73..d7c4f1e40a 100644 --- a/src/ifctester/webapp/src/modules/wasm/index.js +++ b/src/ifctester/webapp/src/modules/wasm/index.ts @@ -5,6 +5,7 @@ import hyperid from "hyperid"; import EventEmitter from "eventemitter3"; +import type { WorkerResponse } from "$src/types/wasm"; // Message types export const MessageType = { @@ -25,52 +26,57 @@ export const MessageType = { // WASM module disposed DISPOSED: 'disposed' +} as const; + +type PendingMessage = { + resolve: (value?: unknown) => void; + reject: (reason?: unknown) => void; }; +type WasmReadyState = boolean | Promise; + class WASMModule extends EventEmitter { id = hyperid(); - ready = false; - worker = null; - pendingMessages = new Map(); + ready: WasmReadyState = false; + worker: Worker | null = null; + pendingMessages = new Map(); async init() { if (this.ready === true) return; - else if (this.ready instanceof Promise) return this.ready; + if (this.ready instanceof Promise) return this.ready; - this.worker = new Worker(new URL('./worker/worker.js', import.meta.url), {type: 'module'}); + this.worker = new Worker(new URL('./worker/worker.ts', import.meta.url), {type: 'module'}); - this.worker.onmessage = (event) => { + this.worker.onmessage = (event: MessageEvent) => { this._handleWorkerMessage(event.data); }; - this.worker.onerror = (error) => { + this.worker.onerror = (error: ErrorEvent) => { console.error('[WASM] Web worker error:', error); this._rejectPendingMessages(error); }; - this.ready = new Promise(async (resolve, reject) => { - try { - await this._sendMessage(MessageType.INIT); - resolve(true); - } catch (error) { + this.ready = this._sendMessage(MessageType.INIT) + .then(() => true) + .catch((error) => { console.error('[WASM] Failed to initialize:', error); this.ready = false; - reject(error); - } - }); + throw error; + }); return this.ready; } - async _sendMessage(type, payload = {}) { - if (!this.worker) throw new Error('Worker not initialized'); + async _sendMessage(type: string, payload: Record = {}): Promise { + const worker = this.worker; + if (!worker) throw new Error('Worker not initialized'); const id = this.id(); return new Promise((resolve, reject) => { this.pendingMessages.set(id, { resolve, reject }); - this.worker.postMessage({ + worker.postMessage({ type, payload, id @@ -78,7 +84,7 @@ class WASMModule extends EventEmitter { }); } - _handleWorkerMessage({ type, payload, id }) { + _handleWorkerMessage({ type, payload, id }: WorkerResponse) { const pendingMessage = this.pendingMessages.get(id); if (!pendingMessage) { @@ -97,23 +103,28 @@ class WASMModule extends EventEmitter { case MessageType.API_RESPONSE: resolve(payload); break; - case MessageType.ERROR: - reject(new Error(payload.message)); + case MessageType.ERROR: { + const message = + payload && typeof payload === "object" && "message" in payload + ? String(payload.message) + : "Unknown worker error"; + reject(new Error(message)); break; + } default: console.warn('[WASM] Unknown message type:', type); reject(new Error(`Unknown message type: ${type}`)); } } - _rejectPendingMessages(error) { + _rejectPendingMessages(error: unknown) { for (const { reject } of this.pendingMessages.values()) { reject(error); } this.pendingMessages.clear(); } - async _apiCall(method, ...args) { + async _apiCall(method: string, ...args: unknown[]) { if (!this.ready) await this.init(); const result = await this._sendMessage(MessageType.API_CALL, { method, args }); @@ -123,35 +134,35 @@ class WASMModule extends EventEmitter { /** * Get all entity classes in a given IFC schema */ - async getAllEntityClasses(schema) { + async getAllEntityClasses(schema: string) { return this._apiCall('getAllEntityClasses', schema); } /** * Get all data types in a given IFC schema */ - async getAllDataTypes(schema) { + async getAllDataTypes(schema: string) { return this._apiCall('getAllDataTypes', schema); } /** * Get predefined types for a given IFC entity */ - async getPredefinedTypes(schema, entity) { + async getPredefinedTypes(schema: string, entity: string) { return this._apiCall('getPredefinedTypes', schema, entity); } /** * Get all attributes for a given IFC entity */ - async getEntityAttributes(schema, entity) { + async getEntityAttributes(schema: string, entity: string) { return this._apiCall('getEntityAttributes', schema, entity); } /** * Get applicable property sets for a given IFC entity */ - async getApplicablePsets(schema, entity, predefinedType = '') { + async getApplicablePsets(schema: string, entity: string, predefinedType = '') { return this._apiCall('getApplicablePsets', schema, entity, predefinedType); } @@ -172,21 +183,21 @@ class WASMModule extends EventEmitter { /** * Load an IFC file. Returns a unique ID for the loaded file. */ - async loadIfc(ifcData) { + async loadIfc(ifcData: number[] | Uint8Array | ArrayBuffer) { return this._apiCall('loadIfc', ifcData); } /** * Unload an IFC file */ - async unloadIfc(ifcId) { + async unloadIfc(ifcId: string) { return this._apiCall('unloadIfc', ifcId); } /** * Audit a loaded IFC file against IDS specifications */ - async auditIfc(ifcId, idsData) { + async auditIfc(ifcId: string, idsData: ArrayBuffer | Uint8Array | number[]) { const idsBytes = idsData instanceof ArrayBuffer ? new Uint8Array(idsData) : idsData; return this._apiCall('auditIfc', ifcId, Array.from(idsBytes)); @@ -204,70 +215,70 @@ class WASMModule extends EventEmitter { /** * Open an existing IDS from XML string */ - async openIDS(idsXml, validate = false) { + async openIDS(idsXml: string, validate = false) { return this._apiCall('openIDS', idsXml, validate); } /** * Create a specification */ - async createSpecification(options = {}) { + async createSpecification(options: Record = {}) { return this._apiCall('createSpecification', options); } /** * Create an entity facet */ - async createEntityFacet(clause, options = {}) { + async createEntityFacet(clause: string, options: Record = {}) { return this._apiCall('createEntityFacet', clause, options); } /** * Create an attribute facet */ - async createAttributeFacet(clause, options = {}) { + async createAttributeFacet(clause: string, options: Record = {}) { return this._apiCall('createAttributeFacet', clause, options); } /** * Create a property facet */ - async createPropertyFacet(clause, options = {}) { + async createPropertyFacet(clause: string, options: Record = {}) { return this._apiCall('createPropertyFacet', clause, options); } /** * Create a material facet */ - async createMaterialFacet(clause, options = {}) { + async createMaterialFacet(clause: string, options: Record = {}) { return this._apiCall('createMaterialFacet', clause, options); } /** * Create a classification facet */ - async createClassificationFacet(clause, options = {}) { + async createClassificationFacet(clause: string, options: Record = {}) { return this._apiCall('createClassificationFacet', clause, options); } /** * Create a part-of facet */ - async createPartOfFacet(clause, options = {}) { + async createPartOfFacet(clause: string, options: Record = {}) { return this._apiCall('createPartOfFacet', clause, options); } /** * Validate an IDS object */ - async validateIDS(idsObj) { + async validateIDS(idsObj: Record) { return await this._apiCall('validateIDS', idsObj); } /** * Export IDS instance to XML string */ - async exportIDS(idsObj) { + async exportIDS(idsObj: Record) { return this._apiCall('exportIDS', idsObj); } @@ -319,4 +330,4 @@ export const { dispose } = wasm; -export default wasm; \ No newline at end of file +export default wasm; diff --git a/src/ifctester/webapp/src/modules/wasm/worker/api.js b/src/ifctester/webapp/src/modules/wasm/worker/api.ts similarity index 80% rename from src/ifctester/webapp/src/modules/wasm/worker/api.js rename to src/ifctester/webapp/src/modules/wasm/worker/api.ts index d08c17efe2..283971bc27 100644 --- a/src/ifctester/webapp/src/modules/wasm/worker/api.js +++ b/src/ifctester/webapp/src/modules/wasm/worker/api.ts @@ -1,12 +1,13 @@ -import config from '../../../config.json'; import hyperid from 'hyperid'; +import config from '../../../config.json'; +import type { AuditReportData } from "$src/types/report"; -let pyodide = null; -let id = hyperid(); +let pyodide: any = null; +const id = hyperid(); -let LoadedIFC = new Map(); +const LoadedIFC = new Map(); -export async function init(pdide) { +export async function init(pdide: any) { pyodide = pdide; // Load Python API bindings @@ -18,7 +19,7 @@ export async function init(pdide) { `); } -export async function getPredefinedTypes(schema, entity) { +export async function getPredefinedTypes(schema: string, entity: string) { const result = await pyodide.runPythonAsync(` from api import get_predefined_types_for_entity predef_types = get_predefined_types_for_entity("${schema}", "${entity}") @@ -27,7 +28,7 @@ export async function getPredefinedTypes(schema, entity) { return result.toJs({ dict_converter: Object.fromEntries }); } -export async function getAllEntityClasses(schema) { +export async function getAllEntityClasses(schema: string) { const result = await pyodide.runPythonAsync(` from api import get_all_entity_classes entities = get_all_entity_classes("${schema}") @@ -36,7 +37,7 @@ export async function getAllEntityClasses(schema) { return result.toJs({ dict_converter: Object.fromEntries }); } -export async function getAllDataTypes(schema) { +export async function getAllDataTypes(schema: string) { const result = await pyodide.runPythonAsync(` from api import get_all_data_types data_types = get_all_data_types("${schema}") @@ -45,7 +46,7 @@ export async function getAllDataTypes(schema) { return result.toJs({ dict_converter: Object.fromEntries }); } -export async function getEntityAttributes(schema, entity) { +export async function getEntityAttributes(schema: string, entity: string) { const result = await pyodide.runPythonAsync(` from api import get_entity_attributes attrs = get_entity_attributes("${schema}", "${entity}") @@ -54,7 +55,7 @@ export async function getEntityAttributes(schema, entity) { return result.toJs({ dict_converter: Object.fromEntries }); } -export async function getApplicablePsets(schema, entity, predefinedType = '') { +export async function getApplicablePsets(schema: string, entity: string, predefinedType = '') { const result = await pyodide.runPythonAsync(` from api import get_applicable_psets psets = get_applicable_psets("${schema}", "${entity}", "${predefinedType}") @@ -81,7 +82,7 @@ export async function getStandardClassificationSystems() { return result.toJs({ dict_converter: Object.fromEntries }); } -export async function loadIfc(ifcData) { +export async function loadIfc(ifcData: number[] | Uint8Array | ArrayBuffer) { const ifc_id = id(); const path = `/tmp/${encodeURIComponent(ifc_id)}.ifc`; @@ -97,14 +98,14 @@ export async function loadIfc(ifcData) { return ifc_id; } -export async function unloadIfc(ifcId) { +export async function unloadIfc(ifcId: string) { const path = `/tmp/${encodeURIComponent(ifcId)}.ifc`; pyodide.FS.unlink(path); LoadedIFC.delete(ifcId); } -export async function auditIfc(ifcId, idsData) { +export async function auditIfc(ifcId: string, idsData: number[] | Uint8Array | ArrayBuffer) { const reporter = pyodide.pyimport("ifctester.reporter"); const api = pyodide.pyimport("api"); @@ -116,16 +117,16 @@ export async function auditIfc(ifcId, idsData) { specs.validate(ifc); // Create report in both HTML and JSON formats - let jsonReporter = reporter.Json(specs); + const jsonReporter = reporter.Json(specs); jsonReporter.report(); const jsonReport = jsonReporter.to_string(); - let htmlReporter = reporter.Html(specs); + const htmlReporter = reporter.Html(specs); htmlReporter.report(); const htmlReport = htmlReporter.to_string(); return { - json: JSON.parse(jsonReport), + json: JSON.parse(jsonReport) as AuditReportData, html: htmlReport }; } @@ -142,4 +143,4 @@ export const API = { "loadIfc": loadIfc, "unloadIfc": unloadIfc, "auditIfc": auditIfc -}; \ No newline at end of file +}; diff --git a/src/ifctester/webapp/src/modules/wasm/worker/ids.js b/src/ifctester/webapp/src/modules/wasm/worker/ids.ts similarity index 57% rename from src/ifctester/webapp/src/modules/wasm/worker/ids.js rename to src/ifctester/webapp/src/modules/wasm/worker/ids.ts index 8afc023cec..ca34b21d75 100644 --- a/src/ifctester/webapp/src/modules/wasm/worker/ids.js +++ b/src/ifctester/webapp/src/modules/wasm/worker/ids.ts @@ -2,13 +2,19 @@ * IDS module */ -let pyodide = null; +let pyodide: any = null; // IDS Python classes -let Ids, Specification; -let Entity, Attribute, Property, Material, Classification, PartOf; +let Ids: any; +let Specification: any; +let Entity: any; +let Attribute: any; +let Property: any; +let Material: any; +let Classification: any; +let PartOf: any; -export async function init(pdide) { +export async function init(pdide: any) { pyodide = pdide; await pyodide.loadPackagesFromImports(` @@ -29,24 +35,24 @@ export async function init(pdide) { PartOf = pyodide.pyimport("ifctester.facet").PartOf; } -function _idsToInstance(idsObj) { +function _idsToInstance(idsObj: Record) { const ids_raw = Ids(); return ids_raw.parse(pyodide.toPy(idsObj)) } -export function createIDS() { +export function createIDS(): Record { const ids_raw = Ids() return ids_raw.asdict().toJs({dict_converter: Object.fromEntries}); } -export function openIDS(ids_xml, validate = false) { +export function openIDS(ids_xml: string, validate = false): Record { const ids_from_xml_string = pyodide.pyimport("api").ids_from_xml_string; const ids_raw = ids_from_xml_string(ids_xml, validate); return ids_raw.asdict().toJs({dict_converter: Object.fromEntries}); } -export function validateIDS(idsObj) { +export function validateIDS(idsObj: Record): boolean { const ids_raw = _idsToInstance(idsObj) const tempFilename = `temp_${Date.now()}.xml`; const isValid = ids_raw.to_xml(tempFilename); // to_xml validates the XML as well, as far as I understand @@ -60,12 +66,26 @@ export function validateIDS(idsObj) { return isValid; } -export function exportIDS(idsObj) { +export function exportIDS(idsObj: Record): string { const ids_raw = _idsToInstance(idsObj) return ids_raw.to_string(); } -export function createSpecification({name = "Unnamed", ifcVersion = ["IFC2X3", "IFC4"], identifier = null, description = null, instructions = null, usage = "required"}) { +export function createSpecification({ + name = "Unnamed", + ifcVersion = ["IFC2X3", "IFC4"], + identifier = null, + description = null, + instructions = null, + usage = "required" +}: { + name?: string; + ifcVersion?: string[]; + identifier?: string | null; + description?: string | null; + instructions?: string | null; + usage?: string; +} = {}): Record { const spec = Specification.callKwargs({ name: name, ifcVersion: ifcVersion, @@ -79,7 +99,14 @@ export function createSpecification({name = "Unnamed", ifcVersion = ["IFC2X3", " } // @instructions -export function createEntityFacet(clause, {name = "IFCWALL", predefinedType = null, instructions = null}) { +export function createEntityFacet( + clause: string, + {name = "IFCWALL", predefinedType = null, instructions = null}: { + name?: string; + predefinedType?: string | null; + instructions?: string | null; + } = {} +): Record { const entity = Entity.callKwargs({ name: name, predefinedType: predefinedType, @@ -89,7 +116,15 @@ export function createEntityFacet(clause, {name = "IFCWALL", predefinedType = nu } // @cardinality, @instructions -export function createAttributeFacet(clause, {name = "Name", value = null, cardinality = "required", instructions = null}) { +export function createAttributeFacet( + clause: string, + {name = "Name", value = null, cardinality = "required", instructions = null}: { + name?: string; + value?: string | null; + cardinality?: string; + instructions?: string | null; + } = {} +): Record { const attribute = Attribute.callKwargs({ name: name, value: value, @@ -100,7 +135,16 @@ export function createAttributeFacet(clause, {name = "Name", value = null, cardi } // @uri, @cardinality, @instructions -export function createClassificationFacet(clause, {value = null, system = null, uri = null, cardinality = "required", instructions = null}) { +export function createClassificationFacet( + clause: string, + {value = null, system = null, uri = null, cardinality = "required", instructions = null}: { + value?: string | null; + system?: string | null; + uri?: string | null; + cardinality?: string; + instructions?: string | null; + } = {} +): Record { const classification = Classification.callKwargs({ value: value, system: system, @@ -112,7 +156,16 @@ export function createClassificationFacet(clause, {value = null, system = null, } // @relation, @cardinality, @instructions -export function createPartOfFacet(clause, {name = "IFCWALL", predefinedType = null, relation = null, cardinality = "required", instructions = null}) { +export function createPartOfFacet( + clause: string, + {name = "IFCWALL", predefinedType = null, relation = null, cardinality = "required", instructions = null}: { + name?: string; + predefinedType?: string | null; + relation?: string | null; + cardinality?: string; + instructions?: string | null; + } = {} +): Record { const part_of = PartOf.callKwargs({ name: name, predefinedType: predefinedType, @@ -124,7 +177,26 @@ export function createPartOfFacet(clause, {name = "IFCWALL", predefinedType = nu } // @dataType, @uri, @cardinality, @instructions -export function createPropertyFacet(clause, {propertySet = "Property_Set", baseName = "propertyName", value = null, dataType = null, uri = null, cardinality = "required", instructions = null}) { +export function createPropertyFacet( + clause: string, + { + propertySet = "Property_Set", + baseName = "propertyName", + value = null, + dataType = null, + uri = null, + cardinality = "required", + instructions = null + }: { + propertySet?: string; + baseName?: string; + value?: string | null; + dataType?: string | null; + uri?: string | null; + cardinality?: string; + instructions?: string | null; + } = {} +): Record { const property = Property.callKwargs({ propertySet: propertySet, baseName: baseName, @@ -138,7 +210,15 @@ export function createPropertyFacet(clause, {propertySet = "Property_Set", baseN } // @uri, @cardinality, @instructions -export function createMaterialFacet(clause, {value = null, uri = null, cardinality = "required", instructions = null}) { +export function createMaterialFacet( + clause: string, + {value = null, uri = null, cardinality = "required", instructions = null}: { + value?: string | null; + uri?: string | null; + cardinality?: string; + instructions?: string | null; + } = {} +): Record { const material = Material.callKwargs({ value: value, uri: uri, @@ -149,7 +229,7 @@ export function createMaterialFacet(clause, {value = null, uri = null, cardinali } // Helper function to convert date to ISO format string -export function formatDate(date) { +export function formatDate(date?: string | number | Date | null): string | null { if (!date) return null; const d = new Date(date); return d.toISOString().split('T')[0]; @@ -168,4 +248,4 @@ export const API = { "createPartOfFacet": createPartOfFacet, "createPropertyFacet": createPropertyFacet, "createMaterialFacet": createMaterialFacet, -}; \ No newline at end of file +}; diff --git a/src/ifctester/webapp/src/modules/wasm/worker/worker.js b/src/ifctester/webapp/src/modules/wasm/worker/worker.ts similarity index 61% rename from src/ifctester/webapp/src/modules/wasm/worker/worker.js rename to src/ifctester/webapp/src/modules/wasm/worker/worker.ts index 6bf47e46fe..fe4b188644 100644 --- a/src/ifctester/webapp/src/modules/wasm/worker/worker.js +++ b/src/ifctester/webapp/src/modules/wasm/worker/worker.ts @@ -4,13 +4,14 @@ import { MessageType } from '../index'; import config from '../../../config.json'; -import * as IDS from './ids.js'; +import * as IDS from './ids'; import * as API from './api'; +import type { ApiCallPayload, WorkerRequest } from "$src/types/wasm"; -let pyodide = null; +let pyodide: any = null; let ready = false; -self.addEventListener('message', async (event) => { +self.addEventListener('message', async (event: MessageEvent) => { console.log("[worker] Received message:", event.data); const { type, payload, id } = event.data; @@ -25,27 +26,33 @@ self.addEventListener('message', async (event) => { }); break; - case MessageType.API_CALL: + case MessageType.API_CALL: { if (!ready) { throw new Error('[worker] Pyodide not initialized'); } - const result = await handleApiCall(payload); + if (!payload) { + throw new Error('[worker] Missing payload for API call'); + } + const result = await handleApiCall(payload as ApiCallPayload); self.postMessage({ type: MessageType.API_RESPONSE, payload: result, id }); break; + } default: throw new Error(`[worker] Unknown message type: ${type}`); } } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const stack = error instanceof Error ? error.stack : undefined; self.postMessage({ type: MessageType.ERROR, payload: { - message: error.message, - stack: error.stack + message, + stack }, id }); @@ -76,7 +83,13 @@ async function initEnvironment() { await pyodide.loadPackage("shapely"); // Install IfcTester - await micropip.install('ifctester'); + const ifctesterManifest = await fetch('/worker/generated/ifctester.json').then((response) => { + if (!response.ok) { + throw new Error(`[worker] Failed to load IfcTester wheel manifest: ${response.status} ${response.statusText}`); + } + return response.json() as Promise<{ wheel_url: string }>; + }); + await micropip.install(ifctesterManifest.wheel_url); // Initialize IDS and API await API.init(pyodide); @@ -93,17 +106,17 @@ async function cleanupEnvironment() { console.log("[worker] Closed environment"); } -async function handleApiCall({ method, args = [] }) { +async function handleApiCall({ method, args = [] }: ApiCallPayload) { if (method === 'internal.cleanup') { await cleanupEnvironment(); return true; } if (method in API.API) { - return await API.API[method](...args); - } else if (method in IDS.API) { - return await IDS.API[method](...args); - } else { - throw new Error(`[worker] Unknown API method: ${method}`); + return await (API.API as Record unknown>)[method](...args); } -} \ No newline at end of file + if (method in IDS.API) { + return await (IDS.API as Record unknown>)[method](...args); + } + throw new Error(`[worker] Unknown API method: ${method}`); +} diff --git a/src/ifctester/webapp/src/pages/Home/ApplicabilityPanel.svelte b/src/ifctester/webapp/src/pages/Home/ApplicabilityPanel.svelte index 2bf687ecc0..df2cfbe9b0 100644 --- a/src/ifctester/webapp/src/pages/Home/ApplicabilityPanel.svelte +++ b/src/ifctester/webapp/src/pages/Home/ApplicabilityPanel.svelte @@ -1,37 +1,56 @@ -
@@ -40,22 +59,19 @@
- {#if activeSpecification?.applicability} - {#each Object.entries(activeSpecification.applicability) as [facetType, facets]} - {#if facetType !== "@minOccurs" && facetType !== "@maxOccurs"} - {#each facets as facet, index} - - {/each} - {/if} + {#if activeSpecification && applicabilityEntries.length > 0} + {#each applicabilityEntries as [facetType, facets]} + {#each facets as facet, index} + + {/each} {/each} {/if}
- \ No newline at end of file + diff --git a/src/ifctester/webapp/src/pages/Home/FacetEditor.svelte b/src/ifctester/webapp/src/pages/Home/FacetEditor.svelte index 4d807625b3..dbc6091f95 100644 --- a/src/ifctester/webapp/src/pages/Home/FacetEditor.svelte +++ b/src/ifctester/webapp/src/pages/Home/FacetEditor.svelte @@ -1,20 +1,40 @@ -
@@ -48,8 +68,8 @@
- - getSpecialProp("@relation"), (v) => setSpecialProp("@relation", v)}> @@ -62,8 +82,8 @@ {#if activeTab === 'requirements'} {#if facetType !== 'entity'}
- - getSpecialProp("@cardinality"), (v) => setSpecialProp("@cardinality", v)}> @@ -71,9 +91,9 @@
{/if}
- - + +
{/if}
-
\ No newline at end of file + diff --git a/src/ifctester/webapp/src/pages/Home/IdsMetadataEditor.svelte b/src/ifctester/webapp/src/pages/Home/IdsMetadataEditor.svelte index 59cd95c0a5..63b762fc5e 100644 --- a/src/ifctester/webapp/src/pages/Home/IdsMetadataEditor.svelte +++ b/src/ifctester/webapp/src/pages/Home/IdsMetadataEditor.svelte @@ -1,13 +1,17 @@ - @@ -18,36 +22,36 @@
- - getProp("title"), (v) => setProp("title", v)} placeholder="Enter IDS title"> + + getProp("title"), (v) => setProp("title", v)} placeholder="Enter IDS title">
- - getProp("author"), (v) => setProp("author", v)} placeholder="Enter author"> + + getProp("author"), (v) => setProp("author", v)} placeholder="Enter author">
- - getProp("version"), (v) => setProp("version", v)} placeholder="Enter version"> + + getProp("version"), (v) => setProp("version", v)} placeholder="Enter version">
- - getProp("date"), (v) => setProp("date", v)}> + + getProp("date"), (v) => setProp("date", v)}>
- - + +
- - getProp("purpose"), (v) => setProp("purpose", v)} placeholder="Enter purpose"> + + getProp("purpose"), (v) => setProp("purpose", v)} placeholder="Enter purpose">
- - getProp("milestone"), (v) => setProp("milestone", v)} placeholder="Enter milestone"> + + getProp("milestone"), (v) => setProp("milestone", v)} placeholder="Enter milestone">
- - getProp("copyright"), (v) => setProp("copyright", v)} placeholder="Enter copyright"> + + getProp("copyright"), (v) => setProp("copyright", v)} placeholder="Enter copyright">
diff --git a/src/ifctester/webapp/src/pages/Home/IdsViewer.svelte b/src/ifctester/webapp/src/pages/Home/IdsViewer.svelte index 2fda5f0c2c..beadd151b8 100644 --- a/src/ifctester/webapp/src/pages/Home/IdsViewer.svelte +++ b/src/ifctester/webapp/src/pages/Home/IdsViewer.svelte @@ -1,18 +1,28 @@ -
@@ -207,8 +254,17 @@
{#each activeDocument.specifications.specification as spec, index} + {@const usage = getDocumentSpecificationUsage(spec)} + {@const requirementGroups = getRequirementGroups(spec)}
-
toggleSpecification(index)}> +
toggleSpecification(index)} + onkeydown={(event) => handleActivation(event, () => toggleSpecification(index))} + >

{spec["@name"] || `Specification ${index + 1}`}

@@ -237,19 +293,19 @@

{spec["@description"]}

{/if}
- {#if spec.applicability["@minOccurs"] === 1 && spec.applicability["@maxOccurs"] === 'unbounded'} + {#if usage === 'required'} Required {/if} - {#if spec.applicability["@minOccurs"] === 0 && spec.applicability["@maxOccurs"] === 'unbounded'} + {#if usage === 'optional'} Optional {/if} - {#if spec.applicability["@minOccurs"] === 0 && spec.applicability["@maxOccurs"] === 0} + {#if usage === 'prohibited'} Prohibited {/if} {#if auditReport} {@const stats = getSpecificationStats(index, auditReport.data)} {@const status = getSpecificationStatus(index, auditReport.data)} - {#if stats && spec.applicability["@maxOccurs"] !== 0 && status !== 'skipped'} + {#if stats && usage !== 'prohibited' && status !== 'skipped'} Checks: {stats.checksPassed}/{stats.checksTotal} Requirements: {stats.requirementsPassed}/{stats.requirements} {/if} @@ -302,12 +358,13 @@ {#if auditReport} {@const status = getSpecificationStatus(index, auditReport.data)} - {#if ! status && spec.applicability["@maxOccurs"] == 0} + {#if status === false && usage === 'prohibited'} {@const specReport = auditReport.data.specifications[index]} + {@const applicableEntities = specReport.applicable_entities ?? []}
- {#if specReport.applicable_entities && specReport.applicable_entities.length > 0} + {#if applicableEntities.length > 0}
-

Failed Elements ({specReport.applicable_entities.length})

+

Failed Elements ({applicableEntities.length})

@@ -323,7 +380,7 @@ - {#each specReport.applicable_entities.slice(0, 10) as entity} + {#each applicableEntities.slice(0, 10) as entity} @@ -379,9 +436,9 @@ {/each} - {#if specReport.applicable_entities.length > 10} + {#if applicableEntities.length > 10} - + {/if} @@ -397,37 +454,36 @@ - {#if Array.isArray(spec.requirements) && spec.requirements.length > 0} + {#if requirementGroups.length > 0}

Requirements

- {#each Object.entries(spec.requirements || {}) as [facetType, facets]} - {#if Array.isArray(facets) && facets.length > 0} -
- {#each facets as facet, facetIndex} - {@const reqAuditData = auditReport ? getRequirementStatus(index, facetIndex, auditReport.data) : null} - {@const specStatus = auditReport ? getSpecificationStatus(index, auditReport.data) : null} -
- - {#if isRequirementDetailsExpanded(index, facetIndex)} + {/if} + + {#if reqAuditData && isRequirementDetailsExpanded(index, item.reqIndex)}
{#if reqAuditData.passed_entities && reqAuditData.passed_entities.length > 0} @@ -592,11 +648,10 @@ {/if}
- {/if} -
- {/each} -
- {/if} + {/if} +
+ {/each} +
{/each} diff --git a/src/ifctester/webapp/src/pages/Home/RequirementsPanel.svelte b/src/ifctester/webapp/src/pages/Home/RequirementsPanel.svelte index 189d9c7066..b034778a67 100644 --- a/src/ifctester/webapp/src/pages/Home/RequirementsPanel.svelte +++ b/src/ifctester/webapp/src/pages/Home/RequirementsPanel.svelte @@ -1,37 +1,53 @@ -
@@ -40,8 +56,8 @@
- {#if activeSpecification?.requirements} - {#each Object.entries(activeSpecification.requirements) as [facetType, facets]} + {#if activeSpecification && requirementEntries.length > 0} + {#each requirementEntries as [facetType, facets]} {#each facets as facet, index} {/each} {/each} {/if}
- \ No newline at end of file + diff --git a/src/ifctester/webapp/src/pages/Home/RestrictionEditor.svelte b/src/ifctester/webapp/src/pages/Home/RestrictionEditor.svelte index 3bc5042065..c8942b2a52 100644 --- a/src/ifctester/webapp/src/pages/Home/RestrictionEditor.svelte +++ b/src/ifctester/webapp/src/pages/Home/RestrictionEditor.svelte @@ -1,34 +1,86 @@ -
- -
+ {label} +
{#if !isSpecialProp}
- handleTypeChange((e.target as HTMLSelectElement).value)} + > @@ -477,7 +537,7 @@ placeholder={placeholder} /> {:else} - getSimpleValue(), (v) => setSimpleValue(v)} {placeholder}> + getSimpleValue(), (v) => setSimpleValue(v)} {placeholder} aria-label={label}> {/if} {:else if restrictionType === 'Enumeration'} @@ -497,7 +557,14 @@ placeholder={placeholder} /> {:else} - updateEnumerationValue(index, e.target.value)} {placeholder}> + updateEnumerationValue(index, (e.target as HTMLInputElement).value)} + {placeholder} + aria-label={`${label} option ${index + 1}`} + > {/if}
{:else if restrictionType === 'Pattern'} - getPatternValue(), (v) => setPatternValue(v)} placeholder="Enter regex pattern (e.g., DT[0-9]{2})"> + getPatternValue(), (v) => setPatternValue(v)} placeholder="Enter regex pattern (e.g., DT[0-9]{2})" aria-label={`${label} pattern`}> {:else if restrictionType === 'Range'} + {@const range = getRangeValues()}
- - getRangeValues().min, (v) => { const range = getRangeValues(); setRangeValues(v, range.max, range.minType, range.maxType); }} placeholder="0"> - range.min, (v) => setRangeValues(v, range.max, range.minType, range.maxType)} + placeholder="0" + > +
- - getRangeValues().max, (v) => { const range = getRangeValues(); setRangeValues(range.min, v, range.minType, range.maxType); }} placeholder="0"> - range.max, (v) => setRangeValues(range.min, v, range.minType, range.maxType)} + placeholder="0" + > + @@ -538,17 +626,18 @@
{:else if restrictionType === 'Length'} - getLengthValue(), (v) => setLengthValue(v)} placeholder="Enter exact length"> + getLengthValue(), (v) => setLengthValue(v)} placeholder="Enter exact length" aria-label={`${label} length`}> {:else if restrictionType === 'Length Range'} + {@const lengthRange = getLengthRangeValues()}
- - getLengthRangeValues().min, (v) => { const range = getLengthRangeValues(); setLengthRangeValues(v, range.max); }} placeholder="0"> + + lengthRange.min, (v) => setLengthRangeValues(v, lengthRange.max)} placeholder="0">
- - getLengthRangeValues().max, (v) => { const range = getLengthRangeValues(); setLengthRangeValues(range.min, v); }} placeholder="0"> + + lengthRange.max, (v) => setLengthRangeValues(lengthRange.min, v)} placeholder="0">
{/if} @@ -656,4 +745,4 @@ color: #666; margin: 0; } - \ No newline at end of file + diff --git a/src/ifctester/webapp/src/pages/Home/SpecificationEditor.svelte b/src/ifctester/webapp/src/pages/Home/SpecificationEditor.svelte index be524ea411..cc340f6cbe 100644 --- a/src/ifctester/webapp/src/pages/Home/SpecificationEditor.svelte +++ b/src/ifctester/webapp/src/pages/Home/SpecificationEditor.svelte @@ -1,33 +1,44 @@ - {#if IDS.Module.status != "ready"} @@ -130,8 +158,7 @@ Import from IDS - {#each Object.entries(IDS.Module.documents) as [docId, doc]} - {#if docId !== IDS.Module.activeDocument && doc.specifications?.specification?.length > 0} + {#each importableDocuments as [docId, doc], docIndex} {doc.info?.title || 'Untitled Document'} @@ -146,12 +173,11 @@ {/each} - {#if Object.entries(IDS.Module.documents).filter(([id, d]) => id !== IDS.Module.activeDocument && d.specifications?.specification?.length > 0).indexOf([docId, doc]) < Object.entries(IDS.Module.documents).filter(([id, d]) => id !== IDS.Module.activeDocument && d.specifications?.specification?.length > 0).length - 1} + {#if docIndex < importableDocuments.length - 1} {/if} - {/if} {/each} - {#if Object.entries(IDS.Module.documents).filter(([docId, doc]) => docId !== IDS.Module.activeDocument && doc.specifications?.specification?.length > 0).length === 0} + {#if importableDocuments.length === 0} No specifications available to import @@ -162,16 +188,30 @@
-
{ if (IDS.Module.activeDocument) IDS.setDocumentState(IDS.Module.activeDocument, { activeSpecification: null }); }}> +
{ if (IDS.Module.activeDocument) IDS.setDocumentState(IDS.Module.activeDocument, { activeSpecification: null }); }} + onkeydown={(event) => handleActivation(event, () => { if (IDS.Module.activeDocument) IDS.setDocumentState(IDS.Module.activeDocument, { activeSpecification: null }); })} + > ℹ️ IDS Information
{#if activeDocument?.specifications?.specification} {#each activeDocument.specifications.specification as spec, index} -
selectSpecification(index)}> +
selectSpecification(index)} + onkeydown={(event) => handleActivation(event, () => selectSpecification(index))} + > 📄 {spec["@name"] || "Specification " + (index + 1)} - -
@@ -209,18 +249,18 @@

{activeSpecification ? activeSpecification["@name"] || "Specification" : "Specification"}

- - - + + +
{#if documentState?.activeTab === 'info'} {:else if documentState?.activeTab === 'applicability'} - + {:else if documentState?.activeTab === 'requirements'} - + {/if}
{/if} diff --git a/src/ifctester/webapp/src/pages/index.js b/src/ifctester/webapp/src/pages/index.ts similarity index 100% rename from src/ifctester/webapp/src/pages/index.js rename to src/ifctester/webapp/src/pages/index.ts diff --git a/src/ifctester/webapp/src/routes.js b/src/ifctester/webapp/src/routes.ts similarity index 100% rename from src/ifctester/webapp/src/routes.js rename to src/ifctester/webapp/src/routes.ts diff --git a/src/ifctester/webapp/src/types/ids.ts b/src/ifctester/webapp/src/types/ids.ts new file mode 100644 index 0000000000..a8213e6baa --- /dev/null +++ b/src/ifctester/webapp/src/types/ids.ts @@ -0,0 +1,70 @@ +export type IdsCardinality = "required" | "optional" | "prohibited"; + +export type RestrictionValue = { + "@value": string; +}; + +export type Restriction = { + "@base"?: string; + enumeration?: RestrictionValue[]; + pattern?: RestrictionValue[]; + length?: RestrictionValue[]; + minLength?: RestrictionValue[]; + maxLength?: RestrictionValue[]; + minInclusive?: RestrictionValue[]; + maxInclusive?: RestrictionValue[]; + minExclusive?: RestrictionValue[]; + maxExclusive?: RestrictionValue[]; +}; + +export type SimpleValue = { + simpleValue: string; +}; + +export type FacetValue = { + simpleValue?: string; + restriction?: Restriction; +}; + +export type Facet = Record; + +export type FacetClause = Record; + +export type Specification = { + "@name"?: string; + "@identifier"?: string; + "@description"?: string; + "@instructions"?: string; + "@ifcVersion"?: string[]; + applicability?: FacetClause; + requirements?: FacetClause; +}; + +export type IdsInfo = { + title?: string; + copyright?: string; + version?: string; + description?: string; + author?: string; + date?: string; + purpose?: string; + milestone?: string; +}; + +export type IdsDocument = { + "@xmlns"?: string; + "@xmlns:xs"?: string; + "@xmlns:xsi"?: string; + "@xsi:schemaLocation"?: string; + info: IdsInfo; + specifications: { + specification: Specification[]; + }; +}; + +export type DocumentState = { + activeTab: "info" | "applicability" | "requirements"; + viewMode: "editor" | "viewer"; + activeSpecification: number | null; + auditReport?: string | null; +}; diff --git a/src/ifctester/webapp/src/types/report.ts b/src/ifctester/webapp/src/types/report.ts new file mode 100644 index 0000000000..ee683e0808 --- /dev/null +++ b/src/ifctester/webapp/src/types/report.ts @@ -0,0 +1,95 @@ +export type ResultsPercent = number | "N/A"; + +export type AuditReportEntity = { + reason?: string; + element?: unknown; + element_type?: unknown; + class?: string; + predefined_type?: string; + name?: string | null; + description?: string | null; + id?: number; + global_id?: string | null; + tag?: string | null; + type_name?: string; + type_tag?: string | null; + type_global_id?: string | null; + extra_of_type?: number; +}; + +export type AuditRequirement = { + facet_type: string; + metadata: Record; + label: string; + value: string; + description: string; + status: boolean; + passed_entities: AuditReportEntity[]; + failed_entities: AuditReportEntity[]; + total_applicable: number; + total_pass: number; + total_fail: number; + percent_pass: ResultsPercent; + instructions?: string; + total_failed_entities?: number; + total_omitted_failures?: number; + has_omitted_failures?: boolean; + total_passed_entities?: number; + total_omitted_passes?: number; + has_omitted_passes?: boolean; +}; + +export type AuditSpecification = { + name: string; + description: string; + instructions: string; + status: boolean; + is_skipped?: boolean; + is_ifc_version: boolean; + total_applicable: number; + total_applicable_pass: number; + total_applicable_fail: number; + percent_applicable_pass: ResultsPercent; + total_checks: number; + total_checks_pass: number; + total_checks_fail: number; + percent_checks_pass: ResultsPercent; + cardinality: string; + applicability: string[]; + applicable_entities?: AuditReportEntity[]; + requirements: AuditRequirement[]; + total_requirements?: number; + total_requirements_pass?: number; +}; + +export type AuditReportData = { + title: string; + date: string; + filepath: string | null; + filename: string | null; + hide_skipped: boolean; + specifications: AuditSpecification[]; + status: boolean; + total_specifications: number; + total_specifications_pass: number; + total_specifications_fail: number; + percent_specifications_pass: ResultsPercent; + total_requirements: number; + total_requirements_pass: number; + total_requirements_fail: number; + percent_requirements_pass: ResultsPercent; + total_checks: number; + total_checks_pass: number; + total_checks_fail: number; + percent_checks_pass: ResultsPercent; +}; + +export type AuditReport = { + id: string; + modelId: string; + modelName: string; + document: string; + date: string; + data: AuditReportData; + htmlReport?: string | null; +}; diff --git a/src/ifctester/webapp/src/types/wasm.ts b/src/ifctester/webapp/src/types/wasm.ts new file mode 100644 index 0000000000..101796bd11 --- /dev/null +++ b/src/ifctester/webapp/src/types/wasm.ts @@ -0,0 +1,24 @@ +export type WorkerMessageType = + | "init" + | "api_call" + | "ready" + | "api_response" + | "error" + | "disposed"; + +export type WorkerRequest = { + type: WorkerMessageType; + payload?: Record; + id: string; +}; + +export type WorkerResponse = { + type: WorkerMessageType; + payload?: Record; + id: string; +}; + +export type ApiCallPayload = { + method: string; + args?: unknown[]; +}; diff --git a/src/ifctester/webapp/tsconfig.json b/src/ifctester/webapp/tsconfig.json new file mode 100644 index 0000000000..39e15a0e10 --- /dev/null +++ b/src/ifctester/webapp/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "moduleResolution": "bundler", + "target": "ESNext", + "module": "ESNext", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "baseUrl": ".", + "paths": { + "$lib": ["./src/lib"], + "$lib/*": ["./src/lib/*"], + "$src": ["./src"], + "$src/*": ["./src/*"] + } + }, + "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.svelte"] +} diff --git a/src/ifctester/webapp/vite.config.js b/src/ifctester/webapp/vite.config.js index cfc33f9356..46cecde534 100644 --- a/src/ifctester/webapp/vite.config.js +++ b/src/ifctester/webapp/vite.config.js @@ -1,7 +1,7 @@ import tailwindcss from '@tailwindcss/vite'; import { defineConfig } from 'vite'; import { svelte } from '@sveltejs/vite-plugin-svelte'; -import path from "path"; +import path from "node:path"; export default defineConfig({ plugins: [tailwindcss(), svelte()], @@ -11,4 +11,4 @@ export default defineConfig({ $src: path.resolve("./src"), }, }, -}); \ No newline at end of file +});
{entity.class} {entity.predefined_type || '-'}
... {specReport.applicable_entities.length - 10} more failing elements not shown ...... {applicableEntities.length - 10} more failing elements not shown ...